[
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\ncharset = utf-8\nend_of_line = lf\nindent_size = 2\ninsert_final_newline = true\n\n[*.lua]\ncharset = utf-8\nindent_size = 4\n\n[*.rs]\ncharset = utf-8\nend_of_line = lf\nindent_size = 4\ninsert_final_newline = true\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yaml",
    "content": "# thanks https://github.com/Ehviewer-Overhauled/Ehviewer templates.\n\nname: Bug 反馈 / Bug report\ndescription: 提交一个问题报告 / Create a bug report\nlabels:\n  - 'T: Bug'\n  - 'S: Untriaged'\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        提交问题报告前，还请首先完成文末的自查步骤。\n\n        Please finish verify steps which list in the end first before create bug report.\n\n  - type: textarea\n    id: reproduce\n    attributes:\n      label: 复现步骤 / Step to reproduce\n      description: |\n        请在此处写下复现的方式，并携带错误日志，必要情况请带上截图/录屏。\n        Please write down the reproduction steps here and include the error log. If necessary, please provide screenshots or recordings.\n      placeholder: |\n        1. \n        2.\n        3.\n        [录屏] / [Screen recording]\n    validations:\n      required: true\n\n  - type: textarea\n    id: expected\n    attributes:\n      label: 预期行为 / Expected behavior\n      description: |\n        在此处说明正常情况下应用的预期行为。\n        Describe what should happened here.\n      placeholder: |\n        它应该 XXX……\n        It should be ...\n    validations:\n      required: true\n\n  - type: textarea\n    id: actual\n    attributes:\n      label: 实际行为 / Actual behavior\n      description: |\n        在此处描绘应用的实际行为，最好附上截图。\n        Describe what actually happened here, screenshots is better.\n      placeholder: |\n        实际上它 XXX……\n        Actually it ...\n        [截图] / [Screenshots]\n    validations:\n      required: true\n\n  - type: textarea\n    id: log\n    attributes:\n      label: 应用日志 / App logs\n      description: |\n        请确保您已移除所有敏感信息，并确保你的日志等级为 `Trace` 或 `Debug`。请参考 [FAQ - 日志目录](https://nyanpasu.elaina.moe/zh-CN/others/faq.html#_2-clash-nyanpasu-%E5%BA%94%E7%94%A8-%E6%97%A5%E8%AE%B0%E7%9B%AE%E5%BD%95%E5%9C%A8%E5%93%AA%E9%87%8C)  \n        如果您可以打开主界面，可以在设置页的“日志目录”旁找到“收集日志”按钮，点击即可收集日志。（1.5.0 不可用）\n        如果日志过长，请使用 [Gist](https://gist.github.com/) 或 [Hastebin](https://hastebin.com/) 并附上链接。\n        Please make sure you have removed all sensitive information and your log level is `Trace` or `Debug`. Please refer to [FAQ - Logs directory](https://nyanpasu.elaina.moe/others/faq.html#_2-where-is-the-clash-nyanpasu-application-logs-directory)\n        If you can open the main interface, you can find the \"Collect logs\" button next to the \"Open Logs Dir\" in the settings page, click to collect the logs. (Not available in 1.5.0)\n        If the log is too long, please use [Gist](https://gist.github.com/) or [Hastebin](https://hastebin.com/) and provide the link.\n      placeholder: |\n        填写一个链接、一个代码块或一个压缩文件\n        Should be a link, a code block or a archive file\n    validations:\n      required: false\n\n  - type: textarea\n    id: more\n    attributes:\n      label: 备注 / Addition details\n      description: |\n        在此处写下其他您想说的内容。\n        Describe addition details here.\n      placeholder: |\n        其他有用的信息与附件\n        Additional details and attachments\n    validations:\n      required: false\n  - type: textarea\n    id: env_infos\n    attributes:\n      label: 环境信息 / Environment information\n      description: |\n        请在此处提供您的环境信息，例如操作系统、Clash Nyanpasu 版本号、Clash 内核及其版本号等。\n        Please provide your environment information here, such as operating system, Clash Nyanpasu version number, Clash core and its version number, etc.\n      placeholder: |\n        此处应由 Nyanpasu 设置页面的反馈按钮自动填写。如果是老版本，请手动填写。\n        This should be automatically filled in by the feedback button on the Nyanpasu settings page. If it is an old version, please fill it in manually.\n    validations:\n      required: true\n  # - type: input\n  #   id: version\n  #   attributes:\n  #     label: Clash Nyanpasu 版本号 / Clash Nyanpasu version\n  #     description: |\n  #       您可以在 **设置 - Nyanpasu 版本** 或在 **托盘 - 更多** 中找到版本号。\n  #       You can find the version number in **Settings - Nyanpasu Version** or **Tray - More**.\n  #     placeholder: 1.5.0\n  #   validations:\n  #     required: true\n\n  # - type: input\n  #   id: core-version\n  #   attributes:\n  #     label: Clash 核心及其版本号 / Clash core and version\n  #     description: |\n  #       您可以在 **设置 - Clash 内核** 中找到内核及其版本号。\n  #       You can find the core and its version number in **Settings - Clash Core**.\n  #     placeholder: v1.18.1 Meta\n  #   validations:\n  #     required: true\n  # - type: input\n  #   id: pre-release\n  #   attributes:\n  #     label: 是否为 Pre-release / Is pre-release version\n  #     description: |\n  #       是否为 Pre-release 下载的应用，若是则填写对应的 commit hash。\n  #       Is this an app downloaded from Pre-release? If so, please fill in the corresponding commit hash.\n  #     placeholder: 26f05a0\n  #   validations:\n  #     required: true\n\n  # - type: input\n  #   id: system\n  #   attributes:\n  #     label: 操作系统及版本 / OS version\n  #     description: 操作系统 + 版本号 / OS + version number\n  #     placeholder: Windows 11, macOS 14\n  #   validations:\n  #     required: true\n\n  - type: checkboxes\n    id: check\n    attributes:\n      label: 自查步骤 / Verify steps\n      description: |\n        请确认您已经遵守所有必选项。\n        Please ensure you have obtained all needed options.\n      options:\n        - label: 如果您有足够的时间和能力，并愿意为此提交 PR，请勾上此复选框 / Pull request is welcome. Check this if you want to start a pull request\n          required: false\n        - label: 您已知悉如果没有提供正确的系统信息，以及日志，您的 Issue 会直接被关闭 / You have known that if you don't provide correct system information and logs, your issue will be closed directly\n          required: true\n        - label: 您已仔细查看并知情 [Q&A](https://nyanpasu.elaina.moe/zh-CN/others/issues) 和 [FAQ](https://nyanpasu.elaina.moe/zh-CN/others/faq) 中的内容 / You have read and understood the contents of [Q&A](https://nyanpasu.elaina.moe/others/issues) and [FAQ](https://nyanpasu.elaina.moe/others/faq)\n          required: true\n\n        - label: 您已搜索过 [Issue Tracker](https://github.com/libnyanpasu/clash-nyanpasu/issues)，没有找到类似内容 / I have searched on [Issue Tracker](https://github.com/libnyanpasu/clash-nyanpasu/issues), No duplicate or related open issue has been found\n          required: true\n\n        - label: 您确保这个 Issue 只提及一个问题。如果您有多个问题报告，烦请发起多个 Issue / Ensure there is only one bug report in this issue. Please make multiply issue for multiply bugs\n          required: true\n\n        - label: 您确保已使用最新 Pre-release 版本测试，并且该问题在最新 Pre-release 版本中并未解决 / This bug have not solved in latest Pre-release version\n          required: true\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n  - name: 所有其他问题 / All other questions\n    url: https://github.com/libnyanpasu/clash-nyanpasu/discussions\n    about: 转到 Discussions / Turn to discussions\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yaml",
    "content": "# thanks https://github.com/Ehviewer-Overhauled/Ehviewer templates.\n\nname: 功能请求 / Feature request\ndescription: 提出一个功能建议 / Suggest an idea\nlabels:\n  - 'T: Feature'\n  - 'S: Untriaged'\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        提交功能建议前，还请首先完成文末的自查步骤。\n\n        Please finish verify steps which list in the end first before suggest an idea.\n\n  - type: textarea\n    id: request\n    attributes:\n      label: 需求 / Requirement\n      description: |\n        在此处描述您的需求。这通常会是一个您想要的功能。\n        Describe what you need here.\n      placeholder: |\n        我需要 XXX 功能……\n        I want ABC feature ...\n    validations:\n      required: true\n\n  - type: textarea\n    id: impl\n    attributes:\n      label: 建议实现 / Suggested implements\n      description: |\n        在此处表述您建议的实现方式。如有可能，UI 类功能请求还请尽量附上图示。\n        Describe your suggested implements here. It's recommend to add a photo if you are making a UI feature request.\n      placeholder: |\n        建议在 XX 处添加 XX……\n        I recommend add ABC feature to DEF ...\n        图片（如果有）/ Photos (if exists)\n    validations:\n      required: true\n\n  - type: textarea\n    id: more\n    attributes:\n      label: 备注 / Addition details\n      description: |\n        在此处写下其他您想说的内容。\n        Describe addition details here.\n      placeholder: |\n        其他有用的信息与附件\n        Additional details and attachments\n    validations:\n      required: false\n\n  - type: input\n    id: version\n    attributes:\n      label: Clash Nyanpasu 版本号 / Clash Nyanpasu\n      description: |\n        您可以在 **设置 - Nyanpasu 版本** 处找到版本号。\n        You can get version code in **Settings - Nyanpasu Version**.\n      placeholder: 1.4.1\n    validations:\n      required: true\n\n  - type: input\n    id: pre-release\n    attributes:\n      label: 是否为 Pre-release / Is pre-release version\n      description: |\n        是否为 Pre-release 下载的应用，若是则填写对应的 commit hash。\n        Is this an app downloaded from Pre-release? If so, please fill in the corresponding commit hash.\n      placeholder: 26f05a0\n    validations:\n      required: true\n\n  - type: checkboxes\n    id: check\n    attributes:\n      label: 自查步骤 / Verify steps\n      description: |\n        请确认您已经遵守所有必选项。\n        Please ensure you have obtained all needed options.\n      options:\n        - label: 如果您有足够的时间和能力，并愿意为此提交 PR，请勾上此复选框 / Pull request is welcome. Check this if you want to start a pull request\n          required: false\n\n        - label: 您已仔细查看并知情 [Q&A](https://nyanpasu.elaina.moe/zh-CN/others/issues) 中的内容 / You have checked [Q&A](https://nyanpasu.elaina.moe/others/issues) carefully\n          required: true\n\n        - label: 您已搜索过 [Issue Tracker](https://github.com/libnyanpasu/clash-nyanpasu/issues)，没有找到类似内容 / I have searched on [Issue Tracker](https://github.com/libnyanpasu/clash-nyanpasu/issues), No duplicate or related open issue has been found\n          required: true\n\n        - label: 您确保这个 Issue 只提及一个功能。如果您有多个功能请求，烦请发起多个 Issue / Ensure there is only one feature request in this issue. Please make multiply issue for multiply feature request\n          required: true\n\n        - label: 您确保已使用最新 Pre-release 版本测试，并且该功能在最新 Pre-release 版本中并未实现 / This feature have not implemented in latest Pre-release version\n          required: true\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "on:\n  pull_request:\n    branches:\n      - main\n      - dev\n      - release-*\n  push:\n    branches:\n      - main\n      - dev\n      - release-*\n\n# the name of our workflow\nname: CI\n\njobs:\n  lint:\n    name: Lint\n    strategy:\n      matrix:\n        targets:\n          - os: ubuntu-latest\n          - os: macos-latest\n          - os: windows-latest\n    runs-on: ${{ matrix.targets.os }}\n    steps:\n      - uses: actions/checkout@v6\n      - name: Rust\n        run: |\n          rustup toolchain install nightly --profile minimal --no-self-update\n          rustup default nightly\n          rustup component add clippy rustfmt\n          rustc --version\n          cargo --version\n          rustup show\n\n      - name: Tauri dependencies\n        if: startsWith(matrix.targets.os, 'ubuntu-')\n        run: >-\n          sudo apt-get update &&\n          sudo apt-get install -y\n          libgtk-3-dev\n          libayatana-appindicator3-dev\n          libwebkit2gtk-4.1-dev\n          librsvg2-dev\n          libxdo-dev\n          webkit2gtk-driver\n          xvfb\n\n      - uses: maxim-lobanov/setup-xcode@v1\n        if: startsWith(matrix.targets.os, 'macos-')\n        with:\n          xcode-version: 'latest-stable'\n\n      - name: Install Node.js\n        uses: actions/setup-node@v6\n        with:\n          node-version: 24\n\n      - uses: Swatinem/rust-cache@v2\n        name: Cache Rust dependencies\n        with:\n          workspaces: 'backend'\n          save-if: ${{ github.event_name == 'push' }}\n\n      - uses: pnpm/action-setup@v5\n        name: Install pnpm\n        with:\n          run_install: false\n\n      - uses: denoland/setup-deno@v2\n        with:\n          deno-version: v2.x\n\n      - name: Get pnpm store directory\n        shell: bash\n        run: |\n          echo \"STORE_PATH=$(pnpm store path --silent)\" >> $GITHUB_ENV\n\n      - uses: actions/cache@v5\n        name: Setup pnpm cache\n        with:\n          path: ${{ env.STORE_PATH }}\n          key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}\n          restore-keys: |\n            ${{ runner.os }}-pnpm-store-\n          save-always: ${{ github.event_name == 'push' }}\n\n      - name: Install dependencies\n        run: pnpm install --no-frozen-lockfile\n\n      - name: Prepare fronend\n        run: pnpm -r build # Build frontend\n        env:\n          NODE_OPTIONS: '--max_old_space_size=4096'\n      - name: Prepare sidecar and resources\n        run: pnpm prepare:check\n      - name: Lint\n        if: startsWith(matrix.targets.os, 'ubuntu-')\n        run: pnpm lint # Lint\n      - name: Lint\n        if: startsWith(matrix.targets.os, 'ubuntu-') == false\n        run: pnpm run-p lint:clippy lint:rustfmt # Lint\n        env:\n          NODE_OPTIONS: '--max_old_space_size=4096'\n\n  # TODO: support test cross-platform\n  build:\n    name: Build Tauri\n    strategy:\n      matrix:\n        targets:\n          - os: ubuntu-latest\n          - os: macos-latest\n          - os: windows-latest\n      fail-fast: false\n    if: >\n      github.event_name != 'pull_request' ||\n      contains(github.event.pull_request.title, 'crate') ||\n      github.event.pull_request.user.login != 'renovate[bot]'\n    runs-on: ${{ matrix.targets.os }}\n    needs: lint\n    steps:\n      - uses: actions/checkout@v6\n      - name: Tauri dependencies\n        if: startsWith(matrix.targets.os, 'ubuntu-')\n        run: >-\n          sudo apt-get update &&\n          sudo apt-get install -y\n          libgtk-3-dev\n          libayatana-appindicator3-dev\n          libwebkit2gtk-4.1-dev\n          librsvg2-dev\n          libxdo-dev\n          webkit2gtk-driver\n          xvfb\n      - uses: maxim-lobanov/setup-xcode@v1\n        if: startsWith(matrix.targets.os, 'macos-')\n        with:\n          xcode-version: 'latest-stable'\n\n      - name: Install Node.js\n        uses: actions/setup-node@v6\n        with:\n          node-version: 24\n\n      - name: Install Deno\n        uses: denoland/setup-deno@v2\n        with:\n          deno-version: v2.x\n\n      - uses: Swatinem/rust-cache@v2\n        name: Cache Rust dependencies\n        with:\n          workspaces: 'backend'\n          save-if: ${{ github.event_name == 'push' }}\n\n      - uses: pnpm/action-setup@v5\n        name: Install pnpm\n        with:\n          run_install: false\n\n      - name: Get pnpm store directory\n        shell: bash\n        run: |\n          echo \"STORE_PATH=$(pnpm store path --silent)\" >> $GITHUB_ENV\n\n      - uses: actions/cache@v5\n        name: Setup pnpm cache\n        with:\n          path: ${{ env.STORE_PATH }}\n          key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}\n          restore-keys: |\n            ${{ runner.os }}-pnpm-store-\n          save-always: ${{ github.event_name == 'push' }}\n\n      - name: Install dependencies\n        run: pnpm install --no-frozen-lockfile\n\n      - name: Prepare sidecar and resources\n        run: pnpm prepare:check\n\n      - name: Prepare frontend\n        run: pnpm -r build\n        env:\n          NODE_OPTIONS: '--max_old_space_size=4096'\n\n      - name: Build Backend\n        run: cargo build --release --manifest-path backend/Cargo.toml\n\n  test_unit:\n    name: Unit Test\n    needs: lint\n    if: >\n      github.event_name != 'pull_request' ||\n      contains(github.event.pull_request.title, 'crate') ||\n      github.event.pull_request.user.login != 'renovate[bot]'\n\n    # we want to run on the latest linux environment\n    strategy:\n      matrix:\n        os:\n          - ubuntu-latest\n          - macos-latest\n          - windows-latest\n      fail-fast: false\n    runs-on: ${{ matrix.os }}\n\n    # the steps our job runs **in order**\n    steps:\n      # checkout the code on the workflow runner\n      - uses: actions/checkout@v6\n\n      # install system dependencies that Tauri needs to compile on Linux.\n      # note the extra dependencies for `tauri-driver` to run which are: `webkit2gtk-driver` and `xvfb`\n      - name: Tauri dependencies\n        if: startsWith(matrix.os, 'ubuntu-')\n        run: >-\n          sudo apt-get update &&\n          sudo apt-get install -y\n          libgtk-3-dev\n          libayatana-appindicator3-dev\n          libwebkit2gtk-4.1-dev\n          librsvg2-dev\n          libxdo-dev\n          webkit2gtk-driver\n          xvfb\n\n      - uses: maxim-lobanov/setup-xcode@v1\n        if: startsWith(matrix.os, 'macos-')\n        with:\n          xcode-version: 'latest-stable'\n\n      - name: Install Node.js\n        uses: actions/setup-node@v6\n        with:\n          node-version: 24\n\n      - name: Install Deno\n        uses: denoland/setup-deno@v2\n        with:\n          deno-version: v2.x\n\n      - uses: Swatinem/rust-cache@v2\n        name: Cache Rust dependencies\n        with:\n          workspaces: 'backend'\n          save-if: ${{ github.event_name == 'push' }}\n\n      - uses: pnpm/action-setup@v5\n        name: Install pnpm\n        with:\n          run_install: false\n\n      - name: Get pnpm store directory\n        shell: bash\n        run: |\n          echo \"STORE_PATH=$(pnpm store path --silent)\" >> $GITHUB_ENV\n\n      - uses: actions/cache@v5\n        name: Setup pnpm cache\n        with:\n          path: ${{ env.STORE_PATH }}\n          key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}\n          restore-keys: |\n            ${{ runner.os }}-pnpm-store-\n          save-always: ${{ github.event_name == 'push' }}\n\n      - name: Install dependencies\n        run: pnpm install --no-frozen-lockfile\n\n      - name: Prepare sidecar and resources\n        run: pnpm prepare:check\n\n      - name: Prepare frontend\n        run: pnpm -r build\n        env:\n          NODE_OPTIONS: '--max_old_space_size=4096'\n\n      - name: Free up disk space\n        if: startsWith(matrix.os, 'ubuntu-')\n        run: |\n          df -h\n          sudo rm -rf /usr/share/dotnet\n          sudo rm -rf /usr/local/lib/android\n          sudo rm -rf /opt/ghc\n          sudo rm -rf /opt/hostedtoolcache/CodeQL\n          sudo docker image prune --all --force\n          df -h\n\n      - name: Test\n        run: pnpm test\n"
  },
  {
    "path": ".github/workflows/daily.yml",
    "content": "on:\n  workflow_dispatch:\n  schedule:\n    - cron: '15 22 * * *' # 每天 06:15 UTC+8 自动构建\n\nname: Daily\n\njobs:\n  generate_manifest:\n    name: Generate Manifest\n    runs-on: ubuntu-latest\n    if: startsWith(github.repository, 'libnyanpasu')\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6\n      - name: Install Node\n        uses: actions/setup-node@v6\n        with:\n          node-version: '24'\n\n      - uses: pnpm/action-setup@v5\n        name: Install pnpm\n        with:\n          run_install: false\n      - name: Install Deno\n        uses: denoland/setup-deno@v2\n        with:\n          deno-version: v2.x\n      - name: Install dependencies\n        run: pnpm install\n      - name: Generate Manifest\n        run: pnpm generate:manifest\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n      # if nothing changed, skip commit\n      - name: Check for changes\n        id: git-check\n        run: echo ::set-output name=has-changes::$(if git diff --quiet; then echo \"false\"; else echo \"true\"; fi)\n      - uses: oleksiyrudenko/gha-git-credentials@v2-latest\n        if: steps.git-check.outputs.has-changes == 'true'\n        with:\n          token: '${{ secrets.GITHUB_TOKEN }}'\n          name: 'github-actions[bot]'\n          email: '41898282+github-actions[bot]@users.noreply.github.com'\n\n      - name: Commit Manifest\n        if: steps.git-check.outputs.has-changes == 'true'\n        run: |\n          git add .\n          git commit -m \"chore(manifest): update manifest [skip ci]\"\n          git push\n  generate_manifest_v1:\n    name: Generate Manifest V1\n    runs-on: ubuntu-latest\n    if: startsWith(github.repository, 'libnyanpasu')\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6\n        with:\n          ref: dev\n      - name: Install Node\n        uses: actions/setup-node@v6\n        with:\n          node-version: '24'\n\n      - uses: pnpm/action-setup@v5\n        name: Install pnpm\n        with:\n          run_install: false\n      - name: Install Deno\n        uses: denoland/setup-deno@v2\n        with:\n          deno-version: v2.x\n      - name: Install dependencies\n        run: pnpm install\n      - name: Generate Manifest\n        run: pnpm generate:manifest\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n      # if nothing changed, skip commit\n      - name: Check for changes\n        id: git-check\n        run: echo ::set-output name=has-changes::$(if git diff --quiet; then echo \"false\"; else echo \"true\"; fi)\n      - uses: oleksiyrudenko/gha-git-credentials@v2-latest\n        if: steps.git-check.outputs.has-changes == 'true'\n        with:\n          token: '${{ secrets.GITHUB_TOKEN }}'\n          name: 'github-actions[bot]'\n          email: '41898282+github-actions[bot]@users.noreply.github.com'\n\n      - name: Commit Manifest\n        if: steps.git-check.outputs.has-changes == 'true'\n        run: |\n          git add .\n          git commit -m \"chore(manifest): update manifest [skip ci]\"\n          git push\n"
  },
  {
    "path": ".github/workflows/deps-build-linux.yaml",
    "content": "name: '[Single] Build Linux'\n\non:\n  workflow_dispatch:\n    inputs:\n      nightly:\n        description: 'Nightly prepare'\n        required: true\n        type: boolean\n        default: false\n      tag:\n        description: 'Release Tag'\n        required: true\n        type: string\n      arch:\n        type: choice\n        description: 'build arch target'\n        required: true\n        default: 'x86_64'\n        options:\n          - x86_64\n          - i686\n          - aarch64\n          - armel\n          - armhf\n\n  workflow_call:\n    inputs:\n      nightly:\n        description: 'Nightly prepare'\n        required: true\n        type: boolean\n        default: false\n      tag:\n        description: 'Release Tag'\n        required: true\n        type: string\n      arch:\n        type: string\n        description: 'build arch target'\n        required: true\n        default: 'x86_64'\n\njobs:\n  build:\n    runs-on: ubuntu-24.04\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v6\n\n      - name: Install Rust nightly\n        run: |\n          rustup install nightly --profile minimal --no-self-update\n          rustup default nightly\n\n      - name: Setup Cargo binstall\n        if: ${{ inputs.arch != 'x86_64' }}\n        uses: cargo-bins/cargo-binstall@main\n\n      - name: Setup Cross Toolchain\n        if: ${{ inputs.arch != 'x86_64' }}\n        shell: bash\n        run: |\n          case \"${{ inputs.arch }}\" in\n            \"i686\")\n              rustup target add i686-unknown-linux-gnu ;;\n            \"aarch64\")\n              rustup target add aarch64-unknown-linux-gnu ;;\n            \"armel\")\n              rustup target add armv7-unknown-linux-gnueabi ;;\n            \"armhf\")\n              rustup target add armv7-unknown-linux-gnueabihf ;;\n          esac\n          cargo binstall -y cross\n\n      - name: Setup Toolchain\n        run: |\n          sudo apt-get update\n          sudo apt-get install -y libwebkit2gtk-4.1-dev libxdo-dev libappindicator3-dev librsvg2-dev patchelf openssl\n\n      - name: Install Node latest\n        uses: actions/setup-node@v6\n        with:\n          node-version: 24\n\n      - uses: pnpm/action-setup@v5\n        name: Install pnpm\n        with:\n          run_install: false\n\n      - uses: denoland/setup-deno@v2\n        with:\n          deno-version: v2.x\n\n      - name: Get pnpm store directory\n        shell: bash\n        run: |\n          echo \"STORE_PATH=$(pnpm store path --silent)\" >> $GITHUB_ENV\n\n      - uses: actions/cache@v5\n        name: Setup pnpm cache\n        with:\n          path: ${{ env.STORE_PATH }}\n          key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}\n          restore-keys: |\n            ${{ runner.os }}-pnpm-store-\n\n      - uses: Swatinem/rust-cache@v2\n        name: Cache Rust dependencies\n        with:\n          workspaces: 'backend'\n          key: ${{ inputs.arch }}\n\n      - name: Install Node.js dependencies\n        run: pnpm i\n\n      - name: Prepare sidecars and resources\n        shell: bash\n        run: |\n          case \"${{ inputs.arch }}\" in\n            \"x86_64\")\n              pnpm prepare:check ;;\n            \"i686\")\n              pnpm prepare:check --arch ia32 --sidecar-host i686-unknown-linux-gnu ;;\n            \"aarch64\")\n              pnpm prepare:check --arch arm64 --sidecar-host aarch64-unknown-linux-gnu ;;\n            \"armel\")\n              pnpm prepare:check --arch armel --sidecar-host armv7-unknown-linux-gnueabi ;;\n            \"armhf\")\n              pnpm prepare:check --arch arm --sidecar-host armv7-unknown-linux-gnueabihf ;;\n          esac\n\n      - name: Nightly Prepare\n        if: ${{ inputs.nightly == true }}\n        run: |\n          pnpm prepare:nightly ${{ inputs.arch != 'x86_64' && '--disable-updater'}}\n\n      - name: Build UI\n        run: pnpm -F ui build\n\n      # ===========================\n      # GTK 图标修复步骤（适用于所有架构）\n      # ===========================\n      - name: Fix GTK Icon Names\n        run: |\n          ORIGINAL_NAME=\"Clash Nyanpasu\"\n          FIXED_NAME=\"clash_nyanpasu\"\n          ICON_SIZES=(\"32x32\" \"128x128\" \"256x256@2\")\n\n          case \"${{ inputs.arch }}\" in\n            \"x86_64\")\n              TARGET_DIR=\"backend/target/release\" ;;\n            \"i686\")\n              TARGET_DIR=\"backend/target/i686-unknown-linux-gnu/release\" ;;\n            \"aarch64\")\n              TARGET_DIR=\"backend/target/aarch64-unknown-linux-gnu/release\" ;;\n            \"armel\")\n              TARGET_DIR=\"backend/target/armv7-unknown-linux-gnueabi/release\" ;;\n            \"armhf\")\n              TARGET_DIR=\"backend/target/armv7-unknown-linux-gnueabihf/release\" ;;\n            *)\n              TARGET_DIR=\"backend/target/release\" ;;\n          esac\n\n          for size in \"${ICON_SIZES[@]}\"; do\n            ICON_PATH=\"$TARGET_DIR/icons/hicolor/${size}/apps/${ORIGINAL_NAME}.png\"\n            FIXED_ICON_PATH=\"$TARGET_DIR/icons/hicolor/${size}/apps/${FIXED_NAME}.png\"\n            if [ -f \"$ICON_PATH\" ]; then\n              mv \"$ICON_PATH\" \"$FIXED_ICON_PATH\"\n              echo \"Renamed $ICON_PATH -> $FIXED_ICON_PATH\"\n            fi\n          done\n\n          DESKTOP_FILE=\"$TARGET_DIR/share/applications/${ORIGINAL_NAME}.desktop\"\n          if [ -f \"$DESKTOP_FILE\" ]; then\n            sed -i \"s/Icon=${ORIGINAL_NAME}/Icon=${FIXED_NAME}/g\" \"$DESKTOP_FILE\"\n            echo \"Updated desktop file Icon field\"\n          fi\n\n          ICON_CACHE_DIR=\"$TARGET_DIR/icons/hicolor\"\n          if [ -d \"$ICON_CACHE_DIR\" ]; then\n            gtk-update-icon-cache -f -t \"$ICON_CACHE_DIR\"\n            echo \"GTK icon cache updated\"\n          fi\n      # ===========================\n      - name: Tauri build (x86_64)\n        if: ${{ inputs.arch == 'x86_64' }}\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}\n          TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}\n          NIGHTLY: ${{ inputs.nightly == true && 'true' || 'false' }}\n        run: |\n          pnpm tauri build ${{ inputs.nightly == true && '-f nightly -c ./backend/tauri/tauri.nightly.conf.json' || '-f default-meta' }}\n\n      - name: Tauri build and upload (cross)\n        if: ${{ inputs.arch != 'x86_64' }}\n        shell: bash\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}\n          TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}\n          NIGHTLY: ${{ inputs.nightly == true && 'true' || 'false' }}\n        run: |\n          case \"${{ inputs.arch }}\" in\n            \"i686\")\n              ${{ inputs.nightly == true && 'pnpm build:nightly -r cross --target i686-unknown-linux-gnu -b \"rpm,deb\"' || 'pnpm build -r cross --target i686-unknown-linux-gnu -b \"rpm,deb\" -c \"{ \"bundle\": { \"createUpdaterArtifacts\": false } }\"' }} ;;\n            \"aarch64\")\n              ${{ inputs.nightly == true && 'pnpm build:nightly -r cross --target aarch64-unknown-linux-gnu -b \"rpm,deb\"' || 'pnpm build -r cross --target aarch64-unknown-linux-gnu -b \"rpm,deb\" -c \"{ \"bundle\": { \"createUpdaterArtifacts\": false } }\"' }} ;;\n            \"armel\")\n              ${{ inputs.nightly == true && 'pnpm build:nightly -r cross --target armv7-unknown-linux-gnueabi -b \"rpm,deb\"' || 'pnpm build -r cross --target armv7-unknown-linux-gnueabi -b \"rpm,deb\" -c \"{ \"bundle\": { \"createUpdaterArtifacts\": false } }\"' }} ;;\n            \"armhf\")\n              ${{ inputs.nightly == true && 'pnpm build:nightly -r cross --target armv7-unknown-linux-gnueabihf -b \"rpm,deb\"' || 'pnpm build -r cross --target armv7-unknown-linux-gnueabihf -b \"rpm,deb\" -c \"{ \"bundle\": { \"createUpdaterArtifacts\": false } }\"' }} ;;\n          esac\n\n      - name: Calc the archive signature\n        run: |\n          find ./backend/target \\( -name \"*.deb\" -o -name \"*.rpm\" \\) | while read file; do\n            sha_file=\"$file.sha256\"\n            if [[ ! -f \"$sha_file\" ]]; then\n              sha256sum \"$file\" > \"$sha_file\"\n              echo \"Created checksum file for: $file\"\n            fi\n          done\n\n      - name: Upload AppImage to Github Artifact\n        if: ${{ inputs.arch == 'x86_64' }}\n        uses: actions/upload-artifact@v7\n        with:\n          name: Clash.Nyanpasu-linux-${{ inputs.arch }}-appimage\n          path: |\n            ./backend/target/**/*.AppImage\n            ./backend/target/**/*.AppImage.tar.gz\n            ./backend/target/**/*.AppImage.tar.gz.sig\n\n      - name: Upload deb to Github Artifact\n        uses: actions/upload-artifact@v7\n        with:\n          name: Clash.Nyanpasu-linux-${{ inputs.arch }}-deb\n          path: |\n            ./backend/target/**/*.deb\n            ./backend/target/**/*.deb.sha256\n\n      - name: Upload rpm to Github Artifact\n        uses: actions/upload-artifact@v7\n        with:\n          name: Clash.Nyanpasu-linux-${{ inputs.arch }}-rpm\n          path: |\n            ./backend/target/**/*.rpm\n            ./backend/target/**/*.rpm.sha256\n\n      - name: Set file server folder path\n        if: ${{ inputs.nightly == true }}\n        shell: bash\n        run: |\n          GIT_HASH=$(git rev-parse --short HEAD)\n          echo \"FOLDER_PATH=nightly/${GIT_HASH}\" >> $GITHUB_ENV\n\n      - name: Upload to file server\n        if: ${{ inputs.nightly == true }}\n        shell: bash\n        continue-on-error: true\n        run: |\n          case \"${{ inputs.arch }}\" in\n            \"x86_64\")\n              deno run -A scripts/deno/upload-build-artifacts.ts \\\n                \"backend/target/**/*.deb\" \\\n                \"backend/target/**/*.AppImage\" ;;\n            *)\n              deno run -A scripts/deno/upload-build-artifacts.ts \\\n                \"backend/target/**/*.deb\" \\\n                \"backend/target/**/*.rpm\" ;;\n          esac\n        env:\n          FILE_SERVER_TOKEN: ${{ secrets.FILE_SERVER_TOKEN }}\n          FOLDER_PATH: ${{ env.FOLDER_PATH }}\n\n      - name: Upload file server results\n        if: ${{ inputs.nightly == true }}\n        uses: actions/upload-artifact@v7\n        with:\n          name: upload-results-linux-${{ inputs.arch }}\n          path: ./upload-results.json\n          if-no-files-found: ignore\n"
  },
  {
    "path": ".github/workflows/deps-build-macos.yaml",
    "content": "name: '[Single] Build macOS'\n\non:\n  workflow_dispatch:\n    inputs:\n      aarch64:\n        description: 'Build aarch64 pkg'\n        required: true\n        type: boolean\n        default: false\n\n      nightly:\n        description: 'Nightly prepare'\n        required: true\n        type: boolean\n        default: false\n\n      tag:\n        description: 'Release Tag'\n        required: true\n        type: string\n\n  workflow_call:\n    inputs:\n      aarch64:\n        description: 'Build aarch64 pkg'\n        required: true\n        type: boolean\n        default: false\n\n      nightly:\n        description: 'Nightly prepare'\n        required: true\n        type: boolean\n        default: false\n\n      tag:\n        description: 'Release Tag'\n        required: true\n        type: string\n\njobs:\n  build:\n    runs-on: macos-latest\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v6\n\n      - uses: maxim-lobanov/setup-xcode@v1\n        with:\n          xcode-version: latest-stable\n\n      - name: install Rust nightly\n        run: |\n          rustup install nightly --profile minimal --no-self-update\n          rustup default nightly\n\n      - name: Install Rust intel target\n        if: ${{ inputs.aarch64 == false }}\n        run: |\n          rustup target add x86_64-apple-darwin\n      - name: Install Rust aarch64 target\n        if: ${{ inputs.aarch64 == true }}\n        run: |\n          rustup target add aarch64-apple-darwin\n\n      - name: Install Node latest\n        uses: actions/setup-node@v6\n        with:\n          node-version: 24\n      - uses: denoland/setup-deno@v2\n        with:\n          deno-version: v2.x\n\n      - uses: pnpm/action-setup@v5\n        name: Install pnpm\n        with:\n          run_install: false\n\n      - uses: denoland/setup-deno@v2\n        with:\n          deno-version: v2.x\n\n      - name: Get pnpm store directory\n        shell: bash\n        run: |\n          echo \"STORE_PATH=$(pnpm store path --silent)\" >> $GITHUB_ENV\n\n      - uses: actions/cache@v5\n        name: Setup pnpm cache\n        with:\n          path: ${{ env.STORE_PATH }}\n          key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}\n          restore-keys: |\n            ${{ runner.os }}-pnpm-store-\n\n      - uses: Swatinem/rust-cache@v2\n        name: Cache Rust dependencies\n        with:\n          workspaces: 'backend'\n          key: ${{ inputs.aarch64 == true && 'aarch64' || 'x86_64' }}\n\n      - name: Pnpm install\n        shell: bash\n        run: |\n          pnpm i\n\n      - name: Download Sidecars aarch64\n        if: ${{ inputs.aarch64 == true }}\n        run: pnpm prepare:check --arch arm64 --sidecar-host aarch64-apple-darwin\n      - name: Download Sidecars x64\n        if: ${{ inputs.aarch64 == false }}\n        run: pnpm prepare:check --arch x64 --sidecar-host x86_64-apple-darwin\n      - name: Nightly Prepare\n        if: ${{ inputs.nightly == true }}\n        run: |\n          pnpm prepare:nightly\n      - name: Build UI\n        run: |\n          pnpm -F ui build\n      - name: Build Clash Nyanpasu (Stable)\n        if: ${{ inputs.nightly == false }}\n        run: |\n          pnpm tauri build --verbose -f default-meta ${{ inputs.aarch64 == true && '--target aarch64-apple-darwin' || '--target x86_64-apple-darwin' }}\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}\n          TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}\n          NIGHTLY: ${{ inputs.nightly == true  && 'true' || 'false' }}\n          NODE_OPTIONS: '--max_old_space_size=4096'\n      - name: Build Clash Nyanpasu (Nightly)\n        if: ${{ inputs.nightly == true }}\n        run: |\n          pnpm build:nightly --verbose ${{ inputs.aarch64 == true && '--target aarch64-apple-darwin' || '--target x86_64-apple-darwin' }}\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}\n          TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}\n          NIGHTLY: ${{ inputs.nightly == true  && 'true' || 'false' }}\n          NODE_OPTIONS: '--max_old_space_size=4096'\n\n      - name: Rename updater files\n        run: |\n          deno run -A scripts/deno/upload-macos-updater.ts\n        env:\n          TARGET_ARCH: ${{ inputs.aarch64 == true && 'aarch64' || 'x86_64' }}\n\n      - name: Upload to Github Artifact\n        uses: actions/upload-artifact@v7\n        with:\n          name: Clash.Nyanpasu-macOS-${{ inputs.aarch64 == true && 'aarch64' || 'amd64' }}\n          path: |\n            ./backend/target/**/*.dmg\n            ./backend/target/**/*.tar.gz\n            ./backend/target/**/*.tar.gz.sig\n\n      - name: Set file server folder path\n        if: ${{ inputs.nightly == true }}\n        shell: bash\n        run: |\n          GIT_HASH=$(git rev-parse --short HEAD)\n          echo \"FOLDER_PATH=nightly/${GIT_HASH}\" >> $GITHUB_ENV\n\n      - name: Upload to file server\n        if: ${{ inputs.nightly == true }}\n        shell: bash\n        continue-on-error: true\n        run: |\n          deno run -A scripts/deno/upload-build-artifacts.ts \\\n            \"backend/target/**/*.dmg\"\n        env:\n          FILE_SERVER_TOKEN: ${{ secrets.FILE_SERVER_TOKEN }}\n          FOLDER_PATH: ${{ env.FOLDER_PATH }}\n\n      - name: Upload file server results\n        if: ${{ inputs.nightly == true }}\n        uses: actions/upload-artifact@v7\n        with:\n          name: upload-results-macos-${{ inputs.aarch64 == true && 'aarch64' || 'amd64' }}\n          path: ./upload-results.json\n          if-no-files-found: ignore\n"
  },
  {
    "path": ".github/workflows/deps-build-windows-nsis.yaml",
    "content": "name: '[Single] Build Windows NSIS'\n\non:\n  workflow_dispatch:\n    inputs:\n      portable:\n        description: 'Build Portable pkg'\n        required: true\n        type: boolean\n        default: false\n\n      fixed-webview:\n        description: 'Fixed WebView'\n        required: true\n        type: boolean\n        default: false\n\n      nightly:\n        description: 'Nightly prepare'\n        required: true\n        type: boolean\n        default: false\n\n      tag:\n        description: 'Release Tag'\n        required: true\n        type: string\n\n      arch:\n        type: choice\n        description: 'build arch target'\n        required: true\n        default: 'x86_64'\n        options:\n          - x86_64\n          - i686\n          - aarch64\n\n  workflow_call:\n    inputs:\n      portable:\n        description: 'Build Portable pkg'\n        required: true\n        type: boolean\n        default: false\n\n      fixed-webview:\n        description: 'Fixed WebView'\n        required: true\n        type: boolean\n        default: false\n\n      nightly:\n        description: 'Nightly prepare'\n        required: true\n        type: boolean\n        default: false\n\n      tag:\n        description: 'Release Tag'\n        required: true\n        type: string\n\n      arch:\n        type: string\n        description: 'build arch target'\n        required: true\n        default: 'x86_64'\n\njobs:\n  build:\n    runs-on: windows-latest\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v6\n\n      - name: Install Rust nightly\n        run: |\n          rustup install nightly --profile minimal --no-self-update\n          rustup default nightly\n\n      - name: Setup Rust target\n        if: ${{ inputs.arch != 'x86_64' }}\n        run: |\n          rustup target add ${{ inputs.arch }}-pc-windows-msvc\n\n      - name: Install Node latest\n        uses: actions/setup-node@v6\n        with:\n          node-version: 24\n\n      - uses: pnpm/action-setup@v5\n        name: Install pnpm\n        with:\n          run_install: false\n\n      - uses: denoland/setup-deno@v2\n        with:\n          deno-version: v2.x\n\n      - name: Get pnpm store directory\n        shell: bash\n        run: |\n          echo \"STORE_PATH=$(pnpm store path --silent)\" >> $GITHUB_ENV\n\n      - uses: actions/cache@v5\n        name: Setup pnpm cache\n        with:\n          path: ${{ env.STORE_PATH }}\n          key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}\n          restore-keys: |\n            ${{ runner.os }}-pnpm-store-\n\n      - uses: Swatinem/rust-cache@v2\n        name: Cache Rust dependencies\n        with:\n          workspaces: 'backend'\n          key: ${{ inputs.arch }}\n\n      - name: Install Node.js dependencies\n        run: |\n          pnpm i\n\n      - name: Prepare sidecars and resources\n        run: |\n          $condition = '${{ inputs.arch }}'\n          switch ($condition) {\n            'x86_64' {\n              pnpm prepare:check\n            }\n            'i686' {\n              pnpm prepare:check --arch ia32 --sidecar-host i686-pc-windows-msvc\n            }\n            'aarch64' {\n              pnpm prepare:check --arch arm64 --sidecar-host aarch64-pc-windows-msvc\n            }\n          }\n\n      - name: Download fixed WebView\n        if: ${{ inputs.fixed-webview == true }}\n        run: |\n          $condition = '${{ inputs.arch }}'\n          switch ($condition) {\n            'x86_64' {\n              $arch= 'x64'\n            }\n            'i686' {\n              $arch = 'x86'\n            }\n            'aarch64' {\n              $arch = 'arm64'\n            }\n          }\n\n          $version = '127.0.2651.105'\n          $uri = \"https://github.com/westinyang/WebView2RuntimeArchive/releases/download/$version/Microsoft.WebView2.FixedVersionRuntime.$version.$arch.cab\"\n          $outfile = \"Microsoft.WebView2.FixedVersionRuntime.$version.$arch.cab\"\n          echo \"Downloading $uri to $outfile\"\n          invoke-webrequest -uri $uri -outfile $outfile\n          echo \"Download finished, attempting to extract\"\n          expand.exe $outfile -F:* ./backend/tauri\n          echo \"Extraction finished\"\n\n      - name: Prepare (Windows NSIS and Portable)\n        if: ${{ inputs.fixed-webview == false }}\n        run: ${{ inputs.nightly == true && 'pnpm prepare:nightly --nsis' || 'pnpm prepare:release --nsis' }}\n\n      - name: Prepare (Windows NSIS and Portable) with fixed WebView\n        if: ${{ inputs.fixed-webview == true }}\n        run: ${{ inputs.nightly == true && 'pnpm prepare:nightly --nsis --fixed-webview' || 'pnpm prepare:release --nsis --fixed-webview' }}\n\n      - name: Build UI\n        run: |\n          pnpm -F ui build\n      # TODO: optimize strategy\n      - name: Tauri build x86_64\n        if: ${{ inputs.arch == 'x86_64' }}\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}\n          TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}\n          NIGHTLY: ${{ inputs.nightly == true  && 'true' || 'false' }}\n        run: |\n          pnpm tauri build ${{ inputs.nightly == true && '-f nightly -c ./backend/tauri/tauri.nightly.conf.json' || '-f default-meta' }}\n\n      - name: Tauri build i686\n        if: ${{ inputs.arch == 'i686' }}\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}\n          TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}\n          NIGHTLY: ${{ inputs.nightly == true  && 'true' || 'false' }}\n        run: |\n          pnpm tauri build ${{ inputs.nightly == true && '-f nightly -c ./backend/tauri/tauri.nightly.conf.json --target i686-pc-windows-msvc' || '-f default-meta --target i686-pc-windows-msvc' }}\n      - name: Tauri build arm64\n        if: ${{ inputs.arch == 'aarch64' }}\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}\n          TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}\n          NIGHTLY: ${{ inputs.nightly == true  && 'true' || 'false' }}\n        run: |\n          pnpm tauri build ${{ inputs.nightly == true && '-f nightly -c ./backend/tauri/tauri.nightly.conf.json --target aarch64-pc-windows-msvc' || '-f default-meta --target aarch64-pc-windows-msvc' }}\n\n      - name: Rename fixed webview bundle name\n        if: ${{ inputs.fixed-webview == true }}\n        run: |\n          $files = Get-ChildItem -Path \"./backend/target\" -Recurse -Include \"*.exe\", \"*.zip\", \"*.zip.sig\" | Where-Object { $_.FullName -like \"*\\bundle\\*\" }\n          $condition = '${{ inputs.arch }}'\n          switch ($condition) {\n            'x86_64' {\n              $arch= 'x64'\n            }\n            'i686' {\n              $arch = 'x86'\n            }\n            'aarch64' {\n              $arch = 'arm64'\n            }\n          }\n\n          foreach ($file in $files) {\n            echo \"Renaming $file\"\n            $newname = $file.FullName -replace $arch, \"fixed-webview-$arch\"\n            Rename-Item -Path $file -NewName $newname\n          }\n\n      - name: Portable Bundle\n        if: ${{ inputs.portable == true }}\n        run: |\n          pnpm portable ${{ inputs.fixed-webview == true && '--fixed-webview' || '' }}\n        env:\n          RUST_ARCH: ${{ inputs.arch }}\n          TAG_NAME: ${{ inputs.tag }}\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}\n          TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}\n          NIGHTLY: ${{ inputs.nightly == true  && 'true' || 'false' }}\n          VITE_WIN_PORTABLE: 1\n\n      - name: Upload NSIS Installer\n        uses: actions/upload-artifact@v7\n        with:\n          name: Clash.Nyanpasu-windows-${{ inputs.arch }}${{ inputs.fixed-webview == true && '-fixed-webview' || '' }}-nsis-installer\n          path: |\n            ./backend/target/**/bundle/**/*.exe\n            ./backend/target/**/bundle/**/*.zip\n            ./backend/target/**/bundle/**/*.zip.sig\n\n      - name: Upload portable\n        if: ${{ inputs.portable == true }}\n        uses: actions/upload-artifact@v7\n        with:\n          name: Clash.Nyanpasu-windows-${{ inputs.arch }}${{ inputs.fixed-webview == true && '-fixed-webview' || '' }}-portable\n          path: |\n            ./*_portable.zip\n\n      - name: Set file server folder path\n        if: ${{ inputs.nightly == true }}\n        shell: bash\n        run: |\n          GIT_HASH=$(git rev-parse --short HEAD)\n          echo \"FOLDER_PATH=nightly/${GIT_HASH}\" >> $GITHUB_ENV\n\n      - name: Upload to file server\n        if: ${{ inputs.nightly == true }}\n        shell: bash\n        continue-on-error: true\n        run: |\n          deno run -A scripts/deno/upload-build-artifacts.ts \\\n            \"backend/target/**/bundle/**/*.exe\" \\\n            \"*_portable.zip\"\n        env:\n          FILE_SERVER_TOKEN: ${{ secrets.FILE_SERVER_TOKEN }}\n          FOLDER_PATH: ${{ env.FOLDER_PATH }}\n\n      - name: Upload file server results\n        if: ${{ inputs.nightly == true }}\n        uses: actions/upload-artifact@v7\n        with:\n          name: upload-results-windows-${{ inputs.arch }}${{ inputs.fixed-webview == true && '-fixed-webview' || '' }}\n          path: ./upload-results.json\n          if-no-files-found: ignore\n"
  },
  {
    "path": ".github/workflows/deps-create-updater.yaml",
    "content": "name: '[Single] Create Updater'\n\non:\n  workflow_dispatch:\n    inputs:\n      nightly:\n        description: 'Nightly'\n        required: true\n        type: boolean\n        default: false\n      release_body:\n        description: 'Release Body'\n        required: false\n        type: string\n  workflow_call:\n    inputs:\n      nightly:\n        description: 'Nightly'\n        required: true\n        type: boolean\n        default: false\n      release_body:\n        description: 'Release Body'\n        required: false\n        type: string\n    secrets:\n      SURGE_TOKEN:\n        required: true\n\njobs:\n  updater:\n    name: Update Updater\n    runs-on: ubuntu-latest\n    permissions:\n      id-token: write # This is required to allow the GitHub Action to authenticate with Deno Deploy.\n      contents: write\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v6\n        with:\n          ref: ${{ github.ref }}\n          # blocked by https://github.com/actions/checkout/issues/1467\n      - name: Fetch git tags\n        run: git fetch --tags\n      - name: Install Node latest\n        uses: actions/setup-node@v6\n        with:\n          node-version: 24\n\n      - uses: pnpm/action-setup@v5\n        name: Install pnpm\n        with:\n          run_install: false\n\n      - name: Pnpm install\n        run: pnpm i\n\n      - name: Update Nightly Updater\n        if: ${{ inputs.nightly == true }}\n        run: pnpm updater:nightly\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n      - name: Update Nightly Fixed Webview Updater\n        if: ${{ inputs.nightly == true }}\n        run: pnpm updater:nightly --fixed-webview\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Update Stable Updater\n        if: ${{ inputs.nightly == false }}\n        run: pnpm updater\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          RELEASE_BODY: ${{ inputs.release_body || github.event.release.body }}\n\n      - name: Update Stable Fixed Webview Updater\n        if: ${{ inputs.nightly == false }}\n        run: pnpm updater --fixed-webview\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          RELEASE_BODY: ${{ inputs.release_body || github.event.release.body }}\n\n      - name: Download updater files from Github release\n        uses: robinraju/release-downloader@v1\n        with:\n          tag: updater\n          repository: libnyanpasu/clash-nyanpasu\n          fileName: '*.json'\n          token: ${{ secrets.GITHUB_TOKEN }}\n          out-file-path: manifest/site/updater\n      - name: Upload updater to surge.sh\n        run: |\n          pnpm i -g surge\n          surge manifest/site surge.elaina.moe\n          surge manifest/site nyanpasu.surge.sh\n        env:\n          SURGE_TOKEN: ${{ secrets.SURGE_TOKEN }}\n      - name: Deploy to Deno Deploy\n        uses: denoland/deployctl@v1\n        with:\n          project: clash-nyanpasu-manifest\n          entrypoint: jsr:@std/http/file-server\n          root: manifest/site\n"
  },
  {
    "path": ".github/workflows/deps-delete-releases.yaml",
    "content": "name: '[Single] Delete Current Releases'\n\non:\n  workflow_dispatch:\n    inputs:\n      tag:\n        description: 'Release Tag'\n        required: true\n        type: string\n\n  workflow_call:\n    inputs:\n      tag:\n        description: 'Release Tag'\n        required: true\n        type: string\n\njobs:\n  delete:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v6\n\n      - name: Delete current release assets\n        uses: mknejp/delete-release-assets@v1\n        with:\n          token: ${{ secrets.GITHUB_TOKEN }}\n          tag: ${{ inputs.tag }}\n          fail-if-no-assets: false\n          fail-if-no-release: false\n          assets: |\n            *.zip\n            *.gz\n            *.AppImage\n            *.deb\n            *.rpm\n            *.dmg\n            *.msi\n            *.sig\n            *.sha256\n            *.exe\n            *.json\n"
  },
  {
    "path": ".github/workflows/deps-message-telegram.yaml",
    "content": "name: '[Single] Send Message to Telegram'\n\non:\n  workflow_dispatch:\n    inputs:\n      nightly:\n        description: 'Nightly'\n        required: true\n        type: boolean\n        default: false\n\n      from-local:\n        description: 'Use per-build uploaded results instead of downloading from release'\n        required: false\n        type: boolean\n        default: false\n\n  workflow_call:\n    inputs:\n      nightly:\n        description: 'Nightly'\n        required: true\n        type: boolean\n        default: false\n\n      from-local:\n        description: 'Use per-build uploaded results instead of downloading from release'\n        required: false\n        type: boolean\n        default: false\n\njobs:\n  telegram:\n    name: Notify Telegram\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v6\n\n      - uses: actions/setup-node@v6\n        with:\n          node-version: 24\n\n      - uses: denoland/setup-deno@v2\n        with:\n          deno-version: v2.x\n\n      - name: Download upload results\n        if: ${{ inputs.from-local == true }}\n        uses: actions/download-artifact@v8\n        continue-on-error: true\n        with:\n          pattern: upload-results-*\n          path: ./upload-results\n          merge-multiple: false\n\n      - name: Send Releases\n        run: |\n          ARGS=\"\"\n          if [ \"${{ inputs.nightly }}\" = \"true\" ]; then\n            ARGS=\"$ARGS --nightly\"\n          fi\n          if [ \"${{ inputs.from-local }}\" = \"true\" ]; then\n            ARGS=\"$ARGS --from-local\"\n          fi\n          deno run -A scripts/deno/telegram-notify.ts $ARGS\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }}\n          TELEGRAM_API_ID: ${{ secrets.TELEGRAM_API_ID }}\n          TELEGRAM_API_HASH: ${{ secrets.TELEGRAM_API_HASH }}\n          FILE_SERVER_TOKEN: ${{ secrets.FILE_SERVER_TOKEN }}\n          TELEGRAM_TO: '@keikolog'\n          TELEGRAM_TO_NIGHTLY: '@ClashNyanpasu'\n          WORKFLOW_RUN_ID: ${{ github.run_id }}\n          UPLOAD_RESULTS_DIR: ./upload-results\n"
  },
  {
    "path": ".github/workflows/deps-update-tag.yaml",
    "content": "name: '[Single] Update Tag'\n\non:\n  workflow_dispatch:\n    inputs:\n      tag:\n        description: 'Release Tag'\n        required: true\n        type: string\n\n  workflow_call:\n    inputs:\n      tag:\n        description: 'Release Tag'\n        required: true\n        type: string\n\njobs:\n  update_tag:\n    name: Update tag\n    runs-on: ubuntu-latest\n    permissions:\n      contents: write\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v6\n\n      - name: Set Env\n        run: |\n          echo \"BUILDTIME=$(TZ=Asia/Shanghai date)\" >> $GITHUB_ENV\n          echo \"CURRENT_GIT_SHA=$(git rev-parse HEAD)\" >> $GITHUB_ENV\n        shell: bash\n\n      - name: Update Tag\n        uses: greenhat616/update-tag@v1\n        with:\n          tag_name: ${{ inputs.tag }}\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Create release body\n        run: |\n          cat > release.txt << 'EOF'\n          ## Clash Nyanpasu Nightly Build\n          Release created at  ${{ env.BUILDTIME }}.\n          Daily build of **Clash Nyanpasu** on *main* branch.\n          You could download previous Nightly Builds from [here](https://t.me/ClashNyanpasu).\n          ***[See the development log here](https://t.me/s/keikolog/)***\n          EOF\n\n      - name: Update Release\n        uses: softprops/action-gh-release@v2\n        with:\n          name: Clash Nyanpasu Dev\n          tag_name: ${{ inputs.tag }}\n          body_path: release.txt\n          prerelease: true\n          generate_release_notes: true\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/deps-upload-release-assets.yaml",
    "content": "name: '[Single] Upload Release Assets'\n\non:\n  workflow_call:\n    inputs:\n      tag:\n        description: 'Release Tag'\n        required: true\n        type: string\n\njobs:\n  upload:\n    runs-on: ubuntu-latest\n    permissions:\n      contents: write\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v6\n\n      - name: Download all build artifacts\n        uses: actions/download-artifact@v8\n        with:\n          pattern: Clash.Nyanpasu-*\n          path: ./release-assets\n          merge-multiple: true\n\n      - name: Upload to release\n        run: |\n          find ./release-assets -type f -print0 | while IFS= read -r -d '' file; do\n            echo \"Uploading $file\"\n            gh release upload \"${{ inputs.tag }}\" \"$file\" --clobber\n          done\n        env:\n          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/macos-aarch64.yaml",
    "content": "name: macOS aarch64 Build\n\non:\n  workflow_dispatch:\nenv:\n  CARGO_INCREMENTAL: 0\n  RUST_BACKTRACE: short\n\njobs:\n  macos-aarch64:\n    runs-on: macos-15\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v6\n\n      - name: install Rust nightly\n        run: |\n          rustup install nightly --profile minimal --no-self-update\n          rustup default nightly\n\n      - uses: Swatinem/rust-cache@v2\n        with:\n          workspaces: './backend/'\n          prefix-key: 'rust-nightly'\n          key: 'macos-13'\n          shared-key: 'release'\n      - uses: maxim-lobanov/setup-xcode@v1\n        with:\n          xcode-version: 16\n      - name: install the missing rust target\n        run: |\n          rustup target add aarch64-apple-darwin\n\n      - name: Install Node\n        uses: actions/setup-node@v6\n        with:\n          node-version: '24'\n\n      - uses: pnpm/action-setup@v5\n        name: Install pnpm\n        with:\n          run_install: false\n\n      - uses: denoland/setup-deno@v2\n        with:\n          deno-version: v2.x\n\n      - name: Pnpm install and check\n        run: |\n          pnpm i\n          pnpm prepare:check --arch arm64 --sidecar-host aarch64-apple-darwin\n\n      - name: Tauri build with Upload (cmd)\n        env:\n          TAG_NAME: dev\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}\n          TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}\n        run: |\n          pnpm build --target aarch64-apple-darwin\n          pnpm upload:osx-aarch64\n"
  },
  {
    "path": ".github/workflows/publish.yml",
    "content": "name: Publish\n\non:\n  workflow_dispatch:\n    inputs:\n      versionType:\n        type: choice\n        description: '<major|minor|patch>'\n        required: true\n        default: 'patch'\n        options:\n          - major\n          - minor\n          - patch\n\njobs:\n  publish:\n    name: Publish ${{ inputs.versionType }} release\n    permissions:\n      # Give the default GITHUB_TOKEN write permission to commit and push the\n      # added or changed files to the repository.\n      contents: write\n      discussions: write\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6\n        with:\n          token: ${{ secrets.GITHUB_TOKEN }}\n          fetch-depth: 0\n      - name: Prepare Node\n        uses: actions/setup-node@v6\n        with:\n          node-version: 24\n      - uses: pnpm/action-setup@v5\n        name: Install pnpm\n        with:\n          run_install: false\n      - name: Get pnpm store directory\n        shell: bash\n        run: |\n          echo \"STORE_PATH=$(pnpm store path --silent)\" >> $GITHUB_ENV\n      - uses: actions/cache@v5\n        name: Setup pnpm cache\n        with:\n          path: ${{ env.STORE_PATH }}\n          key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}\n          restore-keys: |\n            ${{ runner.os }}-pnpm-store-\n      - name: Install\n        run: pnpm i\n      - name: Install git-cliff\n        uses: taiki-e/install-action@git-cliff\n      - id: update-version\n        shell: bash\n        name: Bump version\n        # Use npm because yarn is for some reason not able to output only the version name\n        run: |\n          echo \"version=$(pnpm run publish ${{ inputs.versionType }} | tail -n1)\" >> $GITHUB_OUTPUT\n          git add .\n      - name: Generate a changelog for the new version\n        shell: bash\n        id: build-changelog\n        run: |\n          touch /tmp/changelog.md\n          git-cliff --config  cliff.toml --verbose --strip header --unreleased --tag v${{ steps.update-version.outputs.version }} > /tmp/changelog.md\n          if [ $? -eq 0 ]; then\n            CONTENT=$(cat /tmp/changelog.md)\n            cat /tmp/changelog.md | cat - ./CHANGELOG.md > temp && mv temp ./CHANGELOG.md\n            {\n              echo 'content<<EOF'\n              echo \"$CONTENT\"\n              echo EOF\n            } >> $GITHUB_OUTPUT\n            echo \"version=${{ steps.update-version.outputs.version }}\" >> $GITHUB_OUTPUT\n          else\n            echo \"Failed to generate changelog\"\n            exit 1\n          fi\n        env:\n          GITHUB_REPO: ${{ github.repository }}\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n      - name: Commit changes\n        uses: stefanzweifel/git-auto-commit-action@v7\n        with:\n          commit_message: 'chore: bump version to v${{ steps.update-version.outputs.version }}'\n          commit_user_name: 'github-actions[bot]'\n          commit_user_email: '41898282+github-actions[bot]@users.noreply.github.com'\n          tagging_message: 'v${{ steps.update-version.outputs.version }}'\n      - name: Release\n        uses: softprops/action-gh-release@v2\n        with:\n          draft: true\n          body: ${{steps.build-changelog.outputs.content}}\n          name: Clash Nyanpasu v${{steps.update-version.outputs.version}}\n          tag_name: 'v${{ steps.update-version.outputs.version }}'\n          # target_commitish: ${{ steps.tag.outputs.sha }}\n"
  },
  {
    "path": ".github/workflows/stale.yml",
    "content": "name: 'Close stale issues and PRs'\non:\n  schedule:\n    - cron: '30 1 * * *'\n  workflow_dispatch:\n\npermissions:\n  contents: write # only for delete-branch option\n  issues: write\n  pull-requests: write\n\njobs:\n  stale:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/stale@v10\n        with:\n          stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.'\n          close-issue-message: 'This issue is closed because it has been stale for 5 days with no activity.'\n          days-before-stale: 30\n          days-before-close: 5\n          stale-issue-label: 'S: Stale'\n          only-issue-labels: 'S: Untriaged'\n"
  },
  {
    "path": ".github/workflows/target-dev-build.yaml",
    "content": "name: '[Entire] Build Developer Version'\n\non:\n  workflow_dispatch:\n  schedule:\n    - cron: '15 0 * * *' # 每天 08:15 UTC+8 自动构建\n\nconcurrency:\n  group: dev-build\n  cancel-in-progress: true\n\nenv:\n  CARGO_INCREMENTAL: 0\n  RUST_BACKTRACE: short\n\njobs:\n  windows_amd64_build:\n    name: Windows x86_64 Build\n    if: ${{ github.event_name == 'workflow_dispatch' || startsWith(github.repository, 'libnyanpasu') }}\n    uses: ./.github/workflows/deps-build-windows-nsis.yaml\n    with:\n      portable: true\n      nightly: true\n      fixed-webview: false\n      arch: 'x86_64'\n      tag: 'pre-release'\n    secrets: inherit\n\n  windows_aarch64_build:\n    name: Windows aarch64 Build\n    if: ${{ github.event_name == 'workflow_dispatch' || startsWith(github.repository, 'libnyanpasu') }}\n    uses: ./.github/workflows/deps-build-windows-nsis.yaml\n    with:\n      portable: true\n      nightly: true\n      fixed-webview: false\n      arch: 'aarch64'\n      tag: 'pre-release'\n    secrets: inherit\n\n  windows_i686_build:\n    name: Windows i686 Build\n    if: ${{ github.event_name == 'workflow_dispatch' || startsWith(github.repository, 'libnyanpasu') }}\n    uses: ./.github/workflows/deps-build-windows-nsis.yaml\n    with:\n      portable: true\n      nightly: true\n      fixed-webview: false\n      arch: 'i686'\n      tag: 'pre-release'\n    secrets: inherit\n\n  windows_amd64_build_fixed_webview:\n    name: Windows x86_64 Build with Fixed WebView\n    if: ${{ github.event_name == 'workflow_dispatch' || startsWith(github.repository, 'libnyanpasu') }}\n    uses: ./.github/workflows/deps-build-windows-nsis.yaml\n    with:\n      portable: true\n      nightly: true\n      arch: 'x86_64'\n      fixed-webview: true\n      tag: 'pre-release'\n    secrets: inherit\n\n  windows_aarch64_build_fixed_webview:\n    name: Windows aarch64 Build with Fixed WebView\n    if: ${{ github.event_name == 'workflow_dispatch' || startsWith(github.repository, 'libnyanpasu') }}\n    uses: ./.github/workflows/deps-build-windows-nsis.yaml\n    with:\n      portable: true\n      nightly: true\n      arch: 'aarch64'\n      fixed-webview: true\n      tag: 'pre-release'\n    secrets: inherit\n\n  windows_i686_build_fixed_webview:\n    name: Windows i686 Build with Fixed WebView\n    if: ${{ github.event_name == 'workflow_dispatch' || startsWith(github.repository, 'libnyanpasu') }}\n    uses: ./.github/workflows/deps-build-windows-nsis.yaml\n    with:\n      portable: true\n      nightly: true\n      arch: 'i686'\n      fixed-webview: true\n      tag: 'pre-release'\n    secrets: inherit\n\n  linux_amd64_build:\n    name: Linux amd64 Build\n    if: ${{ github.event_name == 'workflow_dispatch' || startsWith(github.repository, 'libnyanpasu') }}\n    uses: ./.github/workflows/deps-build-linux.yaml\n    with:\n      nightly: true\n      tag: 'pre-release'\n      arch: 'x86_64'\n    secrets: inherit\n\n  linux_i686_build:\n    name: Linux i686 Build\n    if: ${{ github.event_name == 'workflow_dispatch' || startsWith(github.repository, 'libnyanpasu') }}\n    uses: ./.github/workflows/deps-build-linux.yaml\n    with:\n      nightly: true\n      tag: 'pre-release'\n      arch: 'i686'\n    secrets: inherit\n\n  linux_aarch64_build:\n    name: Linux aarch64 Build\n    if: ${{ github.event_name == 'workflow_dispatch' || startsWith(github.repository, 'libnyanpasu') }}\n    uses: ./.github/workflows/deps-build-linux.yaml\n    with:\n      nightly: true\n      tag: 'pre-release'\n      arch: 'aarch64'\n    secrets: inherit\n\n  linux_armhf_build:\n    name: Linux armhf Build\n    if: ${{ github.event_name == 'workflow_dispatch' || startsWith(github.repository, 'libnyanpasu') }}\n    uses: ./.github/workflows/deps-build-linux.yaml\n    with:\n      nightly: true\n      tag: 'pre-release'\n      arch: 'armhf'\n    secrets: inherit\n\n  linux_armel_build:\n    name: Linux armel Build\n    if: ${{ github.event_name == 'workflow_dispatch' || startsWith(github.repository, 'libnyanpasu') }}\n    uses: ./.github/workflows/deps-build-linux.yaml\n    with:\n      nightly: true\n      tag: 'pre-release'\n      arch: 'armel'\n    secrets: inherit\n\n  macos_amd64_build:\n    name: macOS amd64 Build\n    if: ${{ github.event_name == 'workflow_dispatch' || startsWith(github.repository, 'libnyanpasu') }}\n    uses: ./.github/workflows/deps-build-macos.yaml\n    with:\n      nightly: true\n      aarch64: false\n      tag: 'pre-release'\n    secrets: inherit\n\n  macos_aarch64_build:\n    name: macOS aarch64 Build\n    if: ${{ github.event_name == 'workflow_dispatch' || startsWith(github.repository, 'libnyanpasu') }}\n    uses: ./.github/workflows/deps-build-macos.yaml\n    with:\n      nightly: true\n      aarch64: true\n      tag: 'pre-release'\n    secrets: inherit\n\n  update_tag:\n    name: Update tag\n    needs:\n      [\n        windows_amd64_build,\n        windows_i686_build,\n        windows_aarch64_build,\n        windows_amd64_build_fixed_webview,\n        windows_i686_build_fixed_webview,\n        windows_aarch64_build_fixed_webview,\n        linux_amd64_build,\n        linux_i686_build,\n        linux_aarch64_build,\n        linux_armhf_build,\n        linux_armel_build,\n        macos_amd64_build,\n        macos_aarch64_build,\n      ]\n    uses: ./.github/workflows/deps-update-tag.yaml\n    with:\n      tag: 'pre-release'\n\n  delete_current_releases:\n    name: Delete Current Releases\n    needs: [update_tag]\n    uses: ./.github/workflows/deps-delete-releases.yaml\n    with:\n      tag: 'pre-release'\n\n  upload_release_assets:\n    name: Upload Release Assets\n    needs: [delete_current_releases]\n    uses: ./.github/workflows/deps-upload-release-assets.yaml\n    with:\n      tag: 'pre-release'\n\n  updater:\n    name: Create Updater\n    needs: [upload_release_assets]\n    uses: ./.github/workflows/deps-create-updater.yaml\n    with:\n      nightly: true\n    secrets: inherit\n\n  telegram:\n    name: Send Release Message to Telegram\n    if: startsWith(github.repository, 'libnyanpasu')\n    needs: [update_tag]\n    uses: ./.github/workflows/deps-message-telegram.yaml\n    with:\n      nightly: true\n      from-local: true\n    secrets: inherit\n"
  },
  {
    "path": ".github/workflows/target-release-build.yaml",
    "content": "name: '[Entire] Build Release Version'\n\non:\n  release:\n    types: [published]\n\nenv:\n  CARGO_INCREMENTAL: 0\n  RUST_BACKTRACE: short\n\njobs:\n  windows_build:\n    name: Windows Build\n    uses: ./.github/workflows/deps-build-windows-nsis.yaml\n    with:\n      portable: true\n      nightly: false\n      tag: ${{ github.event.release.tag_name }}\n    secrets: inherit\n\n  linux_build:\n    name: Linux Build\n    uses: ./.github/workflows/deps-build-linux.yaml\n    with:\n      nightly: false\n      tag: ${{ github.event.release.tag_name }}\n    secrets: inherit\n\n  macos_amd64_build:\n    name: macOS amd64 Build\n    uses: ./.github/workflows/deps-build-macos.yaml\n    with:\n      nightly: false\n      aarch64: false\n      tag: ${{ github.event.release.tag_name }}\n    secrets: inherit\n\n  macos_aarch64_build:\n    name: macOS aarch64 Build\n    uses: ./.github/workflows/deps-build-macos.yaml\n    with:\n      nightly: false\n      aarch64: true\n      tag: ${{ github.event.release.tag_name }}\n    secrets: inherit\n\n  upload_release_assets:\n    name: Upload Release Assets\n    needs: [windows_build, linux_build, macos_amd64_build, macos_aarch64_build]\n    uses: ./.github/workflows/deps-upload-release-assets.yaml\n    with:\n      tag: ${{ github.event.release.tag_name }}\n\n  updater:\n    name: Create Updater\n    needs: [upload_release_assets]\n    uses: ./.github/workflows/deps-create-updater.yaml\n    with:\n      nightly: false\n\n  telegram:\n    name: Send Release Message to Telegram\n    if: startsWith(github.repository, 'libnyanpasu')\n    needs: [upload_release_assets]\n    uses: ./.github/workflows/deps-message-telegram.yaml\n    with:\n      nightly: false\n    secrets: inherit\n"
  },
  {
    "path": ".gitignore",
    "content": "node_modules\n.DS_Store\ndist\ndist-ssr\n*.local\nupdate.json\nscripts/_env.sh\n\n.eslintcache\n.stylelintcache\n\ntauri.nightly.conf.json\ntauri.preview.conf.json\n\n.idea\n\n*.tsbuildinfo\n\n\nmanifest/site/updater/*\n!manifest/site/updater/.gitkeep\n/backend/tauri/gen/\n"
  },
  {
    "path": ".husky/commit-msg",
    "content": "pnpm commitlint --edit ${1}\n"
  },
  {
    "path": ".husky/pre-commit",
    "content": "# If tty is available, apply fix from https://github.com/typicode/husky/issues/968#issuecomment-1176848345\nif sh -c \": >/dev/tty\" >/dev/null 2>/dev/null; then exec >/dev/tty 2>&1; fi\n\npnpm lint-staged\n"
  },
  {
    "path": ".lintstagedrc.js",
    "content": "export default {\n  '*.{js,cjs,.mjs,jsx}': (filenames) => {\n    const configFiles = [\n      '.oxlintrc.json',\n      '.lintstagedrc.js',\n      'commitlint.config.js',\n    ]\n    const filtered = filenames.filter(\n      (file) => !configFiles.some((config) => file.endsWith(config)),\n    )\n    if (filtered.length === 0) return []\n    return ['prettier --write', 'oxlint --fix']\n  },\n  'scripts/**/*.{ts,tsx}': [\n    'prettier --write',\n    'oxlint --fix',\n    () => 'tsc -p scripts/tsconfig.json --noEmit',\n  ],\n  'frontend/interface/**/*.{ts,tsx}': [\n    'prettier --write',\n    'oxlint --fix',\n    () => 'tsc -p frontend/interface/tsconfig.json --noEmit',\n  ],\n  'frontend/ui/**/*.{ts,tsx}': [\n    'prettier --write',\n    'oxlint --fix',\n    () => 'tsc -p frontend/ui/tsconfig.json --noEmit',\n  ],\n  'frontend/nyanpasu/**/*.{ts,tsx}': [\n    'prettier --write',\n    'oxlint --fix',\n    () => 'tsc -p frontend/nyanpasu/tsconfig.json --noEmit',\n  ],\n  'backend/**/*.{rs,toml}': [\n    () =>\n      'cargo clippy --manifest-path=./backend/Cargo.toml --all-targets --all-features',\n    () => 'cargo fmt --manifest-path ./backend/Cargo.toml --all',\n    // () => 'cargo test --manifest-path=./backend/Cargo.toml',\n    // () => \"cargo fmt --manifest-path=./backend/Cargo.toml --all\",\n    // do not submit untracked files\n    // () => 'git add -u',\n  ],\n  '*.{html,sass,scss,less}': ['prettier --write', 'stylelint --fix'],\n  'package.json': ['prettier --write'],\n  '*.{md,json,jsonc,json5,yaml,yml,toml}': (filenames) => {\n    // exclude frontend/nyanpasu/messages directory\n    const filtered = filenames.filter(\n      (file) => !file.includes('frontend/nyanpasu/messages/'),\n    )\n    if (filtered.length === 0) return []\n    return `prettier --write ${filtered.join(' ')}`\n  },\n}\n"
  },
  {
    "path": ".oxlintrc.json",
    "content": "{\n  \"$schema\": \"./node_modules/oxlint/configuration_schema.json\",\n  \"plugins\": [],\n  \"categories\": {\n    \"correctness\": \"off\"\n  },\n  \"env\": {\n    \"builtin\": true\n  },\n  \"ignorePatterns\": [\n    \"**/node_modules\",\n    \"**/.DS_Store\",\n    \"**/dist\",\n    \"**/*.local\",\n    \"**/update.json\",\n    \"scripts/_env.sh\",\n    \"**/.eslintcache\",\n    \"**/.stylelintcache\",\n    \"**/tauri.nightly.conf.json\",\n    \"**/tauri.preview.conf.json\",\n    \"**/.idea\",\n    \"**/*.tsbuildinfo\",\n    \"manifest/site/updater/*\",\n    \"!manifest/site/updater/.gitkeep\",\n    \"backend/tauri/gen/\",\n    \"**/index.html\",\n    \"**/node_modules/\",\n    \"node_modules/\",\n    \"backend/\",\n    \"backend/**/target\",\n    \"scripts/deno/**\",\n    \".lintstagedrc.js\",\n    \"commitlint.config.js\"\n  ],\n  \"overrides\": [\n    {\n      \"files\": [\"**/*.{jsx,mjsx,tsx,mtsx}\"],\n      \"rules\": {\n        \"react/display-name\": \"error\",\n        \"react/jsx-key\": \"error\",\n        \"react/jsx-no-comment-textnodes\": \"error\",\n        \"react/jsx-no-duplicate-props\": \"error\",\n        \"react/jsx-no-target-blank\": \"error\",\n        // \"react/jsx-no-undef\": \"error\",\n        \"react/no-children-prop\": \"error\",\n        \"react/no-danger-with-children\": \"error\",\n        \"react/no-direct-mutation-state\": \"error\",\n        \"react/no-find-dom-node\": \"error\",\n        \"react/no-is-mounted\": \"error\",\n        \"react/no-render-return-value\": \"error\",\n        \"react/no-string-refs\": \"error\",\n        \"react/no-unknown-property\": \"error\",\n        \"react/no-unsafe\": \"off\",\n        \"react/react-in-jsx-scope\": \"error\"\n      },\n      \"plugins\": [\"react\"]\n    },\n    {\n      \"files\": [\"**/*.{jsx,mjsx,tsx,mtsx}\"],\n      \"rules\": {\n        \"react-hooks/rules-of-hooks\": \"error\",\n        \"react-hooks/exhaustive-deps\": \"warn\"\n      },\n      \"plugins\": [\"react\"]\n    },\n    {\n      \"files\": [\"**/*.{js,mjs,cjs,jsx,mjsx,ts,tsx,mtsx}\"],\n      \"rules\": {\n        \"no-var\": \"warn\",\n        \"accessor-pairs\": [\n          \"error\",\n          {\n            \"setWithoutGet\": true,\n            \"enforceForClassMembers\": true\n          }\n        ],\n        \"array-callback-return\": [\n          \"error\",\n          {\n            \"allowImplicit\": false,\n            \"checkForEach\": false\n          }\n        ],\n        \"constructor-super\": \"error\",\n        \"curly\": [\"error\", \"multi-line\"],\n        \"default-case-last\": \"error\",\n        \"eqeqeq\": [\n          \"error\",\n          \"always\",\n          {\n            \"null\": \"ignore\"\n          }\n        ],\n        \"new-cap\": [\n          \"error\",\n          {\n            \"newIsCap\": true,\n            \"capIsNew\": false,\n            \"properties\": true\n          }\n        ],\n        \"no-array-constructor\": \"error\",\n        \"no-async-promise-executor\": \"error\",\n        \"no-caller\": \"error\",\n        \"no-case-declarations\": \"error\",\n        \"no-class-assign\": \"error\",\n        \"no-compare-neg-zero\": \"error\",\n        \"no-cond-assign\": \"error\",\n        \"no-const-assign\": \"error\",\n        \"no-constant-condition\": [\n          \"error\",\n          {\n            \"checkLoops\": false\n          }\n        ],\n        \"no-control-regex\": \"error\",\n        \"no-debugger\": \"error\",\n        \"no-delete-var\": \"error\",\n        \"no-dupe-class-members\": \"error\",\n        \"no-dupe-keys\": \"error\",\n        \"no-duplicate-case\": \"error\",\n        \"no-useless-backreference\": \"error\",\n        \"no-empty\": [\n          \"error\",\n          {\n            \"allowEmptyCatch\": true\n          }\n        ],\n        \"no-empty-character-class\": \"error\",\n        \"no-empty-pattern\": \"error\",\n        \"no-eval\": \"error\",\n        \"no-ex-assign\": \"error\",\n        \"no-extend-native\": \"error\",\n        \"no-extra-bind\": \"error\",\n        \"no-extra-boolean-cast\": \"error\",\n        \"no-fallthrough\": \"error\",\n        \"no-func-assign\": \"error\",\n        \"no-global-assign\": \"error\",\n        \"no-import-assign\": \"error\",\n        \"no-invalid-regexp\": \"error\",\n        \"no-irregular-whitespace\": \"error\",\n        \"no-iterator\": \"error\",\n        \"no-labels\": [\n          \"error\",\n          {\n            \"allowLoop\": false,\n            \"allowSwitch\": false\n          }\n        ],\n        \"no-lone-blocks\": \"error\",\n        \"no-loss-of-precision\": \"error\",\n        \"no-prototype-builtins\": \"error\",\n        \"no-useless-catch\": \"error\",\n        \"no-multi-str\": \"error\",\n        \"no-new\": \"error\",\n        \"no-new-func\": \"error\",\n        \"no-object-constructor\": \"error\",\n        \"no-new-native-nonconstructor\": \"error\",\n        \"no-new-wrappers\": \"error\",\n        \"no-obj-calls\": \"error\",\n        \"no-proto\": \"error\",\n        \"no-redeclare\": [\n          \"error\",\n          {\n            \"builtinGlobals\": false\n          }\n        ],\n        \"no-regex-spaces\": \"error\",\n        \"no-return-assign\": [\"error\", \"except-parens\"],\n        \"no-self-assign\": [\n          \"error\",\n          {\n            \"props\": true\n          }\n        ],\n        \"no-self-compare\": \"error\",\n        \"no-sequences\": \"error\",\n        \"no-shadow-restricted-names\": \"error\",\n        \"no-sparse-arrays\": \"error\",\n        \"no-template-curly-in-string\": \"error\",\n        \"no-this-before-super\": \"error\",\n        \"no-throw-literal\": \"error\",\n        \"no-unexpected-multiline\": \"error\",\n        \"no-unneeded-ternary\": [\n          \"error\",\n          {\n            \"defaultAssignment\": false\n          }\n        ],\n        \"no-unsafe-finally\": \"error\",\n        \"no-unsafe-negation\": \"error\",\n        \"no-unused-expressions\": [\n          \"error\",\n          {\n            \"allowShortCircuit\": true,\n            \"allowTernary\": true,\n            \"allowTaggedTemplates\": true\n          }\n        ],\n        \"no-unused-vars\": [\n          \"error\",\n          {\n            \"args\": \"none\",\n            \"caughtErrors\": \"none\",\n            \"ignoreRestSiblings\": true,\n            \"vars\": \"all\"\n          }\n        ],\n        \"no-useless-call\": \"error\",\n        \"no-useless-computed-key\": \"error\",\n        \"no-useless-constructor\": \"error\",\n        \"no-useless-escape\": \"error\",\n        \"no-useless-rename\": \"error\",\n        \"no-useless-return\": \"error\",\n        \"no-void\": \"error\",\n        \"no-with\": \"error\",\n        \"prefer-const\": [\n          \"error\",\n          {\n            \"destructuring\": \"all\"\n          }\n        ],\n        \"prefer-promise-reject-errors\": \"error\",\n        \"symbol-description\": \"error\",\n        \"unicode-bom\": [\"error\", \"never\"],\n        \"use-isnan\": [\n          \"error\",\n          {\n            \"enforceForSwitchCase\": true,\n            \"enforceForIndexOf\": true\n          }\n        ],\n        \"valid-typeof\": [\n          \"error\",\n          {\n            \"requireStringLiterals\": true\n          }\n        ],\n        \"yoda\": [\"error\", \"never\"],\n        \"import-x/first\": \"error\",\n        \"import-x/no-absolute-path\": [\n          \"error\",\n          {\n            \"esmodule\": true,\n            \"commonjs\": true,\n            \"amd\": false\n          }\n        ],\n        \"import-x/no-duplicates\": \"error\",\n        \"import-x/no-named-default\": \"error\",\n        \"import-x/no-webpack-loader-syntax\": \"error\",\n        \"promise/param-names\": \"error\",\n        \"node/no-exports-assign\": \"error\",\n        \"node/no-new-require\": \"error\"\n      },\n      \"globals\": {\n        \"__dirname\": \"readonly\",\n        \"__filename\": \"readonly\",\n        \"AbortController\": \"readonly\",\n        \"AbortSignal\": \"readonly\",\n        \"atob\": \"readonly\",\n        \"Blob\": \"readonly\",\n        \"BroadcastChannel\": \"readonly\",\n        \"btoa\": \"readonly\",\n        \"Buffer\": \"readonly\",\n        \"ByteLengthQueuingStrategy\": \"readonly\",\n        \"clearImmediate\": \"readonly\",\n        \"clearInterval\": \"readonly\",\n        \"clearTimeout\": \"readonly\",\n        \"CloseEvent\": \"readonly\",\n        \"CompressionStream\": \"readonly\",\n        \"console\": \"readonly\",\n        \"CountQueuingStrategy\": \"readonly\",\n        \"crypto\": \"readonly\",\n        \"Crypto\": \"readonly\",\n        \"CryptoKey\": \"readonly\",\n        \"CustomEvent\": \"readonly\",\n        \"DecompressionStream\": \"readonly\",\n        \"DOMException\": \"readonly\",\n        \"Event\": \"readonly\",\n        \"EventTarget\": \"readonly\",\n        \"fetch\": \"readonly\",\n        \"File\": \"readonly\",\n        \"FormData\": \"readonly\",\n        \"Headers\": \"readonly\",\n        \"MessageChannel\": \"readonly\",\n        \"MessageEvent\": \"readonly\",\n        \"MessagePort\": \"readonly\",\n        \"navigator\": \"readonly\",\n        \"Navigator\": \"readonly\",\n        \"performance\": \"readonly\",\n        \"Performance\": \"readonly\",\n        \"PerformanceEntry\": \"readonly\",\n        \"PerformanceMark\": \"readonly\",\n        \"PerformanceMeasure\": \"readonly\",\n        \"PerformanceObserver\": \"readonly\",\n        \"PerformanceObserverEntryList\": \"readonly\",\n        \"PerformanceResourceTiming\": \"readonly\",\n        \"process\": \"readonly\",\n        \"queueMicrotask\": \"readonly\",\n        \"ReadableByteStreamController\": \"readonly\",\n        \"ReadableStream\": \"readonly\",\n        \"ReadableStreamBYOBReader\": \"readonly\",\n        \"ReadableStreamBYOBRequest\": \"readonly\",\n        \"ReadableStreamDefaultController\": \"readonly\",\n        \"ReadableStreamDefaultReader\": \"readonly\",\n        \"Request\": \"readonly\",\n        \"Response\": \"readonly\",\n        \"setImmediate\": \"readonly\",\n        \"setInterval\": \"readonly\",\n        \"setTimeout\": \"readonly\",\n        \"structuredClone\": \"readonly\",\n        \"SubtleCrypto\": \"readonly\",\n        \"TextDecoder\": \"readonly\",\n        \"TextDecoderStream\": \"readonly\",\n        \"TextEncoder\": \"readonly\",\n        \"TextEncoderStream\": \"readonly\",\n        \"TransformStream\": \"readonly\",\n        \"TransformStreamDefaultController\": \"readonly\",\n        \"URL\": \"readonly\",\n        \"URLSearchParams\": \"readonly\",\n        \"WebAssembly\": \"readonly\",\n        \"WebSocket\": \"readonly\",\n        \"WritableStream\": \"readonly\",\n        \"WritableStreamDefaultController\": \"readonly\",\n        \"WritableStreamDefaultWriter\": \"readonly\",\n        \"document\": \"readonly\",\n        \"window\": \"readonly\"\n      },\n      \"env\": {\n        \"commonjs\": true,\n        \"es2024\": true\n      },\n      \"plugins\": [\"import\", \"node\", \"promise\"]\n    },\n    {\n      \"files\": [\"**/*.{js,mjs,cjs,jsx,mjsx,ts,tsx,mtsx}\"],\n      \"rules\": {\n        \"react/jsx-boolean-value\": \"error\",\n        \"react/jsx-fragments\": [\"error\", \"syntax\"],\n        \"react/jsx-handler-names\": \"error\",\n        \"react/jsx-key\": [\n          \"error\",\n          {\n            \"checkFragmentShorthand\": true\n          }\n        ],\n        \"react/jsx-no-comment-textnodes\": \"error\",\n        \"react/jsx-no-duplicate-props\": \"error\",\n        \"react/jsx-no-target-blank\": [\n          \"error\",\n          {\n            \"enforceDynamicLinks\": \"always\"\n          }\n        ],\n        // \"react/jsx-no-undef\": [\n        //   \"error\",\n        //   {\n        //     \"allowGlobals\": true\n        //   }\n        // ],\n        \"react/no-children-prop\": \"error\",\n        \"react/no-danger-with-children\": \"error\",\n        \"react/no-direct-mutation-state\": \"error\",\n        \"react/no-find-dom-node\": \"error\",\n        \"react/no-is-mounted\": \"error\",\n        \"react/no-string-refs\": [\n          \"error\",\n          {\n            \"noTemplateLiterals\": true\n          }\n        ],\n        \"react/no-unescaped-entities\": \"off\",\n        \"react/no-render-return-value\": \"error\",\n        \"react/self-closing-comp\": \"error\"\n      },\n      \"plugins\": [\"react\"]\n    },\n    {\n      \"files\": [\"**/*.{js,mjs,cjs,jsx,mjsx,ts,tsx,mtsx}\"],\n      \"rules\": {\n        \"constructor-super\": \"off\",\n        \"no-const-assign\": \"off\",\n        \"no-dupe-class-members\": \"error\",\n        \"no-dupe-keys\": \"off\",\n        \"no-func-assign\": \"off\",\n        \"no-import-assign\": \"off\",\n        \"no-new-native-nonconstructor\": \"off\",\n        \"no-obj-calls\": \"off\",\n        \"no-redeclare\": [\n          \"error\",\n          {\n            \"builtinGlobals\": false\n          }\n        ],\n        \"no-this-before-super\": \"off\",\n        \"no-unsafe-negation\": \"off\",\n        \"no-array-constructor\": \"error\",\n        \"no-loss-of-precision\": \"error\",\n        \"no-unused-expressions\": [\n          \"error\",\n          {\n            \"allowShortCircuit\": true,\n            \"allowTernary\": true,\n            \"allowTaggedTemplates\": true\n          }\n        ],\n        \"no-unused-vars\": [\n          \"error\",\n          {\n            \"args\": \"none\",\n            \"caughtErrors\": \"none\",\n            \"ignoreRestSiblings\": true,\n            \"vars\": \"all\"\n          }\n        ],\n        \"no-useless-constructor\": \"error\"\n      },\n      \"plugins\": [\"typescript\"]\n    },\n    {\n      \"files\": [\"**/*.{js,mjs,cjs,jsx,mjsx,ts,tsx,mtsx}\"],\n      \"rules\": {\n        \"curly\": \"off\",\n        \"no-unexpected-multiline\": \"off\",\n        \"unicorn/empty-brace-spaces\": \"off\",\n        \"unicorn/no-nested-ternary\": \"off\",\n        \"unicorn/number-literal-case\": \"off\"\n      },\n      \"plugins\": [\"unicorn\"]\n    },\n    {\n      \"files\": [\"**/*.{js,mjs,cjs,jsx,mjsx,ts,tsx,mtsx}\"],\n      \"rules\": {\n        \"curly\": \"off\",\n        \"no-unexpected-multiline\": \"off\",\n        \"unicorn/empty-brace-spaces\": \"off\",\n        \"unicorn/no-nested-ternary\": \"off\",\n        \"unicorn/number-literal-case\": \"off\",\n        \"arrow-body-style\": \"off\"\n      },\n      \"plugins\": [\"unicorn\"]\n    },\n    {\n      \"files\": [\"**/*.{js,mjs,cjs,jsx,mjsx,ts,tsx,mtsx}\"],\n      \"rules\": {\n        \"no-console\": \"off\",\n        \"no-debugger\": \"off\",\n        \"no-unused-vars\": \"warn\",\n        \"react/react-in-jsx-scope\": \"off\"\n      },\n      \"plugins\": [\"typescript\", \"react\"]\n    },\n    {\n      \"files\": [\"**/*.{ts,tsx,mtsx}\"],\n      \"rules\": {\n        \"constructor-super\": \"off\",\n        \"no-class-assign\": \"off\",\n        \"no-const-assign\": \"off\",\n        \"no-dupe-class-members\": \"off\",\n        \"no-dupe-keys\": \"off\",\n        \"no-func-assign\": \"off\",\n        \"no-import-assign\": \"off\",\n        \"no-new-native-nonconstructor\": \"off\",\n        \"no-obj-calls\": \"off\",\n        \"no-redeclare\": \"off\",\n        \"no-setter-return\": \"off\",\n        \"no-this-before-super\": \"off\",\n        \"no-unsafe-negation\": \"off\",\n        \"no-var\": \"error\",\n        \"no-with\": \"off\",\n        \"prefer-const\": \"error\",\n        \"prefer-rest-params\": \"error\",\n        \"prefer-spread\": \"error\"\n      }\n    },\n    {\n      \"files\": [\"**/*.{ts,tsx,mtsx}\"],\n      \"rules\": {\n        \"@typescript-eslint/ban-ts-comment\": \"error\",\n        \"no-array-constructor\": \"error\",\n        \"@typescript-eslint/no-duplicate-enum-values\": \"error\",\n        \"@typescript-eslint/no-empty-object-type\": \"error\",\n        \"@typescript-eslint/no-explicit-any\": \"error\",\n        \"@typescript-eslint/no-extra-non-null-assertion\": \"error\",\n        \"@typescript-eslint/no-misused-new\": \"error\",\n        \"@typescript-eslint/no-namespace\": \"error\",\n        \"@typescript-eslint/no-non-null-asserted-optional-chain\": \"error\",\n        \"@typescript-eslint/no-require-imports\": \"error\",\n        \"@typescript-eslint/no-this-alias\": \"error\",\n        \"@typescript-eslint/no-unnecessary-type-constraint\": \"error\",\n        \"@typescript-eslint/no-unsafe-declaration-merging\": \"error\",\n        \"@typescript-eslint/no-unsafe-function-type\": \"error\",\n        \"no-unused-expressions\": \"error\",\n        \"no-unused-vars\": \"error\",\n        \"@typescript-eslint/no-wrapper-object-types\": \"error\",\n        \"@typescript-eslint/prefer-as-const\": \"error\",\n        \"@typescript-eslint/prefer-namespace-keyword\": \"error\",\n        \"@typescript-eslint/triple-slash-reference\": \"error\"\n      },\n      \"plugins\": [\"typescript\"]\n    },\n    {\n      \"files\": [\"**/*.{ts,tsx,mtsx}\"],\n      \"rules\": {\n        \"@typescript-eslint/no-explicit-any\": \"warn\",\n        \"no-unused-vars\": \"warn\"\n      },\n      \"plugins\": [\"typescript\"]\n    },\n    {\n      \"files\": [\n        \"frontend/nyanpasu/vite.config.ts\",\n        \"frontend/nyanpasu/tailwind.config.ts\"\n      ],\n      \"rules\": {\n        \"constructor-super\": \"off\",\n        \"no-class-assign\": \"off\",\n        \"no-const-assign\": \"off\",\n        \"no-dupe-class-members\": \"off\",\n        \"no-dupe-keys\": \"off\",\n        \"no-func-assign\": \"off\",\n        \"no-import-assign\": \"off\",\n        \"no-new-native-nonconstructor\": \"off\",\n        \"no-obj-calls\": \"off\",\n        \"no-redeclare\": \"off\",\n        \"no-setter-return\": \"off\",\n        \"no-this-before-super\": \"off\",\n        \"no-unsafe-negation\": \"off\",\n        \"no-var\": \"error\",\n        \"no-with\": \"off\",\n        \"prefer-const\": \"error\",\n        \"prefer-rest-params\": \"error\",\n        \"prefer-spread\": \"error\"\n      }\n    },\n    {\n      \"files\": [\n        \"frontend/nyanpasu/vite.config.ts\",\n        \"frontend/nyanpasu/tailwind.config.ts\"\n      ],\n      \"rules\": {\n        \"@typescript-eslint/ban-ts-comment\": \"error\",\n        \"no-array-constructor\": \"error\",\n        \"@typescript-eslint/no-duplicate-enum-values\": \"error\",\n        \"@typescript-eslint/no-empty-object-type\": \"error\",\n        \"@typescript-eslint/no-explicit-any\": \"error\",\n        \"@typescript-eslint/no-extra-non-null-assertion\": \"error\",\n        \"@typescript-eslint/no-misused-new\": \"error\",\n        \"@typescript-eslint/no-namespace\": \"error\",\n        \"@typescript-eslint/no-non-null-asserted-optional-chain\": \"error\",\n        \"@typescript-eslint/no-require-imports\": \"error\",\n        \"@typescript-eslint/no-this-alias\": \"error\",\n        \"@typescript-eslint/no-unnecessary-type-constraint\": \"error\",\n        \"@typescript-eslint/no-unsafe-declaration-merging\": \"error\",\n        \"@typescript-eslint/no-unsafe-function-type\": \"error\",\n        \"no-unused-expressions\": \"error\",\n        \"no-unused-vars\": \"error\",\n        \"@typescript-eslint/no-wrapper-object-types\": \"error\",\n        \"@typescript-eslint/prefer-as-const\": \"error\",\n        \"@typescript-eslint/prefer-namespace-keyword\": \"error\",\n        \"@typescript-eslint/triple-slash-reference\": \"error\"\n      },\n      \"plugins\": [\"typescript\"]\n    },\n    {\n      \"files\": [\n        \"frontend/nyanpasu/vite.config.ts\",\n        \"frontend/nyanpasu/tailwind.config.ts\"\n      ],\n      \"rules\": {\n        \"@typescript-eslint/no-explicit-any\": \"warn\",\n        \"no-unused-vars\": \"warn\"\n      },\n      \"plugins\": [\"typescript\"]\n    },\n    {\n      \"files\": [\"frontend/ui/vite.config.ts\"],\n      \"rules\": {\n        \"constructor-super\": \"off\",\n        \"no-class-assign\": \"off\",\n        \"no-const-assign\": \"off\",\n        \"no-dupe-class-members\": \"off\",\n        \"no-dupe-keys\": \"off\",\n        \"no-func-assign\": \"off\",\n        \"no-import-assign\": \"off\",\n        \"no-new-native-nonconstructor\": \"off\",\n        \"no-obj-calls\": \"off\",\n        \"no-redeclare\": \"off\",\n        \"no-setter-return\": \"off\",\n        \"no-this-before-super\": \"off\",\n        \"no-unsafe-negation\": \"off\",\n        \"no-var\": \"error\",\n        \"no-with\": \"off\",\n        \"prefer-const\": \"error\",\n        \"prefer-rest-params\": \"error\",\n        \"prefer-spread\": \"error\"\n      }\n    },\n    {\n      \"files\": [\"frontend/ui/vite.config.ts\"],\n      \"rules\": {\n        \"@typescript-eslint/ban-ts-comment\": \"error\",\n        \"no-array-constructor\": \"error\",\n        \"@typescript-eslint/no-duplicate-enum-values\": \"error\",\n        \"@typescript-eslint/no-empty-object-type\": \"error\",\n        \"@typescript-eslint/no-explicit-any\": \"error\",\n        \"@typescript-eslint/no-extra-non-null-assertion\": \"error\",\n        \"@typescript-eslint/no-misused-new\": \"error\",\n        \"@typescript-eslint/no-namespace\": \"error\",\n        \"@typescript-eslint/no-non-null-asserted-optional-chain\": \"error\",\n        \"@typescript-eslint/no-require-imports\": \"error\",\n        \"@typescript-eslint/no-this-alias\": \"error\",\n        \"@typescript-eslint/no-unnecessary-type-constraint\": \"error\",\n        \"@typescript-eslint/no-unsafe-declaration-merging\": \"error\",\n        \"@typescript-eslint/no-unsafe-function-type\": \"error\",\n        \"no-unused-expressions\": \"error\",\n        \"no-unused-vars\": \"error\",\n        \"@typescript-eslint/no-wrapper-object-types\": \"error\",\n        \"@typescript-eslint/prefer-as-const\": \"error\",\n        \"@typescript-eslint/prefer-namespace-keyword\": \"error\",\n        \"@typescript-eslint/triple-slash-reference\": \"error\"\n      },\n      \"plugins\": [\"typescript\"]\n    },\n    {\n      \"files\": [\"frontend/ui/vite.config.ts\"],\n      \"rules\": {\n        \"@typescript-eslint/no-explicit-any\": \"warn\",\n        \"no-unused-vars\": \"warn\"\n      },\n      \"plugins\": [\"typescript\"]\n    },\n    {\n      \"files\": [\"**/*.{jsx,mjsx,tsx,mtsx}\"],\n      \"globals\": {\n        \"AudioWorkletGlobalScope\": \"readonly\",\n        \"AudioWorkletProcessor\": \"readonly\",\n        \"currentFrame\": \"readonly\",\n        \"currentTime\": \"readonly\",\n        \"registerProcessor\": \"readonly\",\n        \"sampleRate\": \"readonly\",\n        \"WorkletGlobalScope\": \"readonly\"\n      },\n      \"env\": {\n        \"browser\": true,\n        \"serviceworker\": true\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": ".prettierignore",
    "content": "*.rs\n*.lock\n**/target/\ndist/\n**/node_modules/\npnpm-lock.yaml\n*.lock\n*.wxs\nfrontend/nyanpasu/src/route-tree.gen.ts\nfrontend/nyanpasu/auto-imports.d.ts\nfrontend/nyanpasu/src/paraglide/\nfrontend/nyanpasu/project.inlang/\nbackend/tauri/gen/schemas/\n"
  },
  {
    "path": ".prettierrc.cjs",
    "content": "/** @type {import(\"prettier\").Config} */\nmodule.exports = {\n  endOfLine: 'lf',\n  semi: false,\n  singleQuote: true,\n  bracketSpacing: true,\n  tabWidth: 2,\n  trailingComma: 'all',\n  overrides: [\n    {\n      files: ['tsconfig.json', 'jsconfig.json'],\n      options: {\n        parser: 'jsonc',\n      },\n    },\n  ],\n  importOrder: [\n    '^@nyanpasu/ui/(.*)$',\n    '^@nyanpasu/interface/(.*)$',\n    '^@/(.*)$',\n    '^@(.*)$',\n    '^[./]',\n  ],\n  importOrderParserPlugins: ['typescript', 'jsx', 'decorators-legacy'],\n  importOrderTypeScriptVersion: '5.0.0',\n  plugins: [\n    '@ianvs/prettier-plugin-sort-imports',\n    'prettier-plugin-tailwindcss',\n    'prettier-plugin-toml',\n  ],\n}\n"
  },
  {
    "path": ".stylelintignore",
    "content": "dist/\nbackend/**/target\n"
  },
  {
    "path": ".stylelintrc.js",
    "content": "import PostCssScss from 'postcss-scss'\n\nexport default {\n  root: true,\n  defaultSeverity: 'error',\n  plugins: [\n    'stylelint-scss',\n    'stylelint-order',\n    'stylelint-declaration-block-no-ignored-properties',\n  ],\n  extends: [\n    'stylelint-config-standard',\n    'stylelint-config-html/html', // the shareable html config for Stylelint.\n    'stylelint-config-recess-order',\n    // 'stylelint-config-prettier'\n  ],\n  rules: {\n    'selector-pseudo-class-no-unknown': [\n      true,\n      { ignorePseudoClasses: ['global'] },\n    ],\n    'font-family-name-quotes': null,\n    'font-family-no-missing-generic-family-keyword': null,\n    'max-nesting-depth': [\n      10,\n      {\n        ignore: ['blockless-at-rules', 'pseudo-classes'],\n      },\n    ],\n    'declaration-block-no-duplicate-properties': true,\n    'no-duplicate-selectors': true,\n    'no-descending-specificity': null,\n    'selector-class-pattern': null,\n    'value-no-vendor-prefix': [true, { ignoreValues: ['box'] }],\n    'at-rule-no-unknown': [\n      true,\n      {\n        ignoreAtRules: [\n          'tailwind',\n          'unocss',\n          'layer',\n          'apply',\n          'variants',\n          'responsive',\n          'screen',\n          'config',\n          'plugin',\n          'theme',\n          'variant',\n          'custom-variant',\n          'utility',\n          'source',\n          'reference',\n        ],\n      },\n    ],\n    'at-rule-no-deprecated': [\n      true,\n      {\n        ignoreAtRules: ['apply'],\n      },\n    ],\n  },\n  overrides: [\n    {\n      files: ['**/*.scss', '*.scss'],\n      customSyntax: PostCssScss,\n      rules: {\n        'at-rule-no-unknown': null,\n        'import-notation': null,\n        'scss/at-rule-no-unknown': [\n          true,\n          {\n            ignoreAtRules: [\n              'tailwind',\n              'unocss',\n              'layer',\n              'apply',\n              'variants',\n              'responsive',\n              'screen',\n              'config',\n              'plugin',\n              'theme',\n              'variant',\n              'custom-variant',\n              'utility',\n              'source',\n              'reference',\n            ],\n          },\n        ],\n      },\n    },\n  ],\n}\n"
  },
  {
    "path": ".vscode/extensions.json",
    "content": "{\n  \"recommendations\": [\n    \"inlang.vs-code-extension\",\n    \"editorconfig.editorconfig\",\n    \"vadimcn.vscode-lldb\",\n    \"denoland.vscode-deno\",\n    \"esbenp.prettier-vscode\",\n    \"yoavbls.pretty-ts-errors\",\n    \"rust-lang.rust-analyzer\",\n    \"syler.sass-indented\",\n    \"stylelint.vscode-stylelint\",\n    \"bradlc.vscode-tailwindcss\",\n    \"oxc.oxc-vscode\",\n    \"tamasfe.even-better-toml\"\n  ]\n}\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n  \"tailwindCSS.experimental.classRegex\": [\n    [\"cva\\\\(([^)]*)\\\\)\", \"[\\\"'`]([^\\\"'`]*).*?[\\\"'`]\"],\n    [\"cx\\\\(([^)]*)\\\\)\", \"(?:'|\\\"|`)([^']*)(?:'|\\\"|`)\"]\n  ],\n  \"files.eol\": \"\\n\",\n  \"js/ts.tsdk.path\": \"node_modules\\\\typescript\\\\lib\"\n}\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "## [1.6.1] - 2024-09-07\n\n### ✨ Features\n\n- **dock:** Try to setup macos dock handler by @greenhat616\n\n- **enhance:** Finish all filter test suites by @greenhat616\n\n- **enhance:** Add sequence filter support and partial test suite by @greenhat616\n\n- **enhance:** Add complex filter syntax support by @greenhat616\n\n- **monaco:** Add onValidation before submit, and close #1491 by @greenhat616\n\n- **monaco:** Add yaml config prompt by @greenhat616\n\n- **nsis:** Cleanup reg while uninstall by @greenhat616\n\n- **service:** Add manual prompt for service uninstall, stop, start by @greenhat616\n\n- **service:** Add a manual install prompt while service install failed by @greenhat616\n\n- **tun:** Support auto-route while clash-rs support it by @greenhat616\n\n- Use cross-rs to build aarch64 by @greenhat616\n\n- Try to support linux aarch64 build by @greenhat616\n\n### 🐛 Bug Fixes\n\n- **ci:** Update publish script by @greenhat616\n\n- **dialog:** Position func err by @keiko233\n\n- **nsis:** Cleanup app config and data dir if option is selected by @greenhat616\n\n- **os:** Create no window by @greenhat616\n\n- **shiki:** Shell lang loader by @greenhat616\n\n- Monaco clash config prompt by @greenhat616\n\n- Monaco url resolve issue by @greenhat616\n\n- Try to resolve the yaml schema by @greenhat616\n\n- Try to escape the string by @greenhat616\n\n- Add service install error prompt by @greenhat616\n\n- Shiki import by @greenhat616\n\n- Try to fix create no window by @greenhat616\n\n- Typo by @greenhat616\n\n- Windows nightly build version issue by @greenhat616\n\n- Build by @greenhat616\n\n- Aarch build by @greenhat616\n\n- Dont merge falsy theme settings by @greenhat616\n\n### 🔨 Refactor\n\n- Use @monaco-editor/react instead by @greenhat616\n\n- Service shoutcuts use core manager internal state by @greenhat616\n\n---\n\n**Full Changelog**: https://github.com/libnyanpasu/clash-nyanpasu/compare/v1.6.0...v1.6.1\n\n## [1.6.0] - 2024-08-29\n\n### 💥 Breaking Changes\n\n- Tsconfig options by @keiko233\n\n### ⚡ Performance Improvements\n\n- **hook:** Add debounce callback & do nothing when minimized by @keiko233\n\n- **proxies:** Add useTransition by @keiko233\n\n- **ui:** Memoized children node by @keiko233\n\n- **ui:** Add ref support for BasePage by @keiko233\n\n- Switch log page & rule page to async component by @keiko233\n\n### ✨ Features\n\n- **component:** Add children props support for PaperButton by @keiko233\n\n- **connections:** Lazy load connections and close #1208 by @greenhat616\n\n- **connections:** Add no connection display by @keiko233\n\n- **connections:** New design for ConnectionsPage by @keiko233\n\n- **custom-schema:** Experimental compatible with common clash schema by @greenhat616\n\n- **custom-scheme:** Use one desktop file to process mime by @greenhat616\n\n- **custom-theme:** Background color picker minor tweak by @keiko233\n\n- **dashboard:** Add service status shortcuts card by @keiko233\n\n- **dashboard:** Add proxy shortcuts panel by @keiko233\n\n- **dashboard:** Special grid layout for drawer by @keiko233\n\n- **dashboard:** Add health panel by @keiko233\n\n- **dashboard:** Init Dashboard Page by @keiko233\n\n- **delay-button:** Minor tweaks for animetion by @keiko233\n\n- **downloader:** Make downloader status readable by @greenhat616\n\n- **drawer:** Enable panel collapsible by @keiko233\n\n- **drawer:** Add small size layout by @keiko233\n\n- **drawer:** Minor tweak for small size by @keiko233\n\n- **enhance:** Experimental add lua runner support by @greenhat616\n\n- **enhance:** Make merge process more powerful by @greenhat616\n\n- **experimental:** Initial react compiler support by @keiko233\n\n- **interface:** Initial ClashWS by @keiko233\n\n- **interface:** Add profile js interface by @keiko233\n\n- **interface:** Add current clash mode interface by @keiko233\n\n- **interface:** Add useClashCore hook method by @keiko233\n\n- **interface:** Add app tauri invoke interface by @keiko233\n\n- **interface:** Add profiles api with SWR by @keiko233\n\n- **interface:** Add ClashInfo interface with SWR by @keiko233\n\n- **interface:** Init code by @keiko233\n\n- **ipc:** Replace timing utils ofetch to tokio by @keiko233\n\n- **ipc:** Export delay test and core status call by @greenhat616\n\n- **layout:** Add scrollbar track margin by @keiko233\n\n- **logs:** New design LogsPage by @keiko233\n\n- **macos:** Try to impl dock show/hide api by @greenhat616\n\n- **macos:** Add traffic control offset for macos by @keiko233\n\n- **migration:** Add discard method for discarding changes while migration failed by @greenhat616\n\n- **monaco:** Add monaco types support by @keiko233\n\n- **monaco:** Add typescript language service by @keiko233\n\n- **monaco:** Import lua language support by @keiko233\n\n- **monaco-edit:** Switch to lazy load module by @keiko233\n\n- **monaco-editor:** Support props value changes and language switching by @keiko233\n\n- **monaco-editor:** Support language change on prop by @keiko233\n\n- **motion:** Add lighten animation effects config by @keiko233\n\n- **nyanpasu:** Node list support proxy delay testing by @keiko233\n\n- **nyanpasu:** Import react devtools on dev env by @keiko233\n\n- **nyanpasu:** Use new design Proxies Page by @keiko233\n\n- **nyanpasu:** Import tailwind css by @keiko233\n\n- **nyanpasu:** Experimentally added new settings interface by @keiko233\n\n- **nyanpasu:** Add SettingLegacy component by @keiko233\n\n- **nyanpasu:** Add SettingNyanpasuVersion component by @keiko233\n\n- **nyanpasu:** Add SettingNyanpasuUI component by @keiko233\n\n- **nyanpasu:** Add SettingNyanpasuPath component by @keiko233\n\n- **nyanpasu:** Add SettingNyanpasuPath component by @keiko233\n\n- **nyanpasu:** Add PaperButton component by @keiko233\n\n- **nyanpasu:** Add SettingNyanpasuTasks component by @keiko233\n\n- **nyanpasu:** Add SettingSystemService component by @keiko233\n\n- **nyanpasu:** Add SettingSystemBehavior component by @keiko233\n\n- **nyanpasu:** Add SettingSystemClash component by @keiko233\n\n- **nyanpasu:** Add SettingClashCore component by @keiko233\n\n- **nyanpasu:** Use grid layout for SettingClashWeb by @keiko233\n\n- **nyanpasu:** Add SettingClashField component by @keiko233\n\n- **nyanpasu:** Add SettingClashWeb component by @keiko233\n\n- **nyanpasu:** Add SettingClashExternal component by @keiko233\n\n- **nyanpasu:** Add SettingClashPort component by @keiko233\n\n- **nyanpasu:** Add SettingClashBase component by @keiko233\n\n- **nyanpasu:** Add nyanpasu setting props creator by @keiko233\n\n- **nyanpasu:** Use new theme create method by @keiko233\n\n- **nynapasu:** Add SettingNyanpasuMisc component by @keiko233\n\n- **profiles:** Adapting scroll area & add position animation by @keiko233\n\n- **profiles:** Add diff dialog hint by @greenhat616\n\n- **profiles:** Add max log level triggered notice, and close #1291 by @greenhat616\n\n- **profiles:** Add black touch new option by @greenhat616\n\n- **profiles:** Add text carousel for subscription expires and updated time by @greenhat616\n\n- **profiles:** Minor tweaks & add click card to apply profile by @keiko233\n\n- **profiles:** Add split pane support & minor tweaks by @keiko233\n\n- **profiles:** Profiles new design by @keiko233\n\n- **profiles:** Add proxy chain side page by @keiko233\n\n- **profiles:** Add monaco editor for ProfileItem by @keiko233\n\n- **profiles:** Complete profile operation menu by @keiko233\n\n- **profiles:** Redesign profile cards & new profile editor by @keiko233\n\n- **profiles:** Profile dialog support edit mode by @keiko233\n\n- **profiles:** Add QuickImport text arae component by @keiko233\n\n- **profiles:** Init new profile page by @keiko233\n\n- **providers:** Add proxy provider traffic display support by @keiko233\n\n- **providers:** Support proxies providers by @keiko233\n\n- **providers:** New design ProvidersPage by @keiko233\n\n- **proxies:** Filter proxies nodes by @greenhat616\n\n- **proxies:** Adapting scroll area by @keiko233\n\n- **proxies:** Support proxy group test url by @keiko233\n\n- **proxies:** Add scroll to current node button by @keiko233\n\n- **proxies:** Add node card animation by @keiko233\n\n- **proxies:** Group name transition use framer motion by @keiko233\n\n- **proxies:** Add none proxies tips by @keiko233\n\n- **proxies:** Add virtual scrolling to grid node list by @keiko233\n\n- **proxies:** Group list use virtual scrolling by @keiko233\n\n- **proxies:** Add node list sorting function by @keiko233\n\n- **proxies:** Add group name text transition by @keiko233\n\n- **proxies:** Add diff clash mode page layout by @keiko233\n\n- **proxies:** Support group icon show by @keiko233\n\n- **proxies:** Disable button when type is not selecor by @keiko233\n\n- **rules:** Move filter text input to header by @keiko233\n\n- **rules:** New design for RulesPage by @keiko233\n\n- **service:** Add a service control panel and sidecar check script by @greenhat616\n\n- **setting-clash-base:** Add uwp tools support by @keiko233\n\n- **setting-clash-core:** Support core update by @keiko233\n\n- **setting-clash-field:** Add ClashFieldFilter switch by @keiko233\n\n- **sotre:** Add persistence support by @keiko233\n\n- **theme:** Add MDYPaper style override by @keiko233\n\n- **tray:** Add custom tray icon support by @greenhat616\n\n- **tray:** Add submenu proxies selector by @greenhat616\n\n- **ui:** Md3 style segmented button by @greenhat616\n\n- **ui:** Add scroll area support for side page by @keiko233\n\n- **ui:** Tailwind css support mui breakpoint by @keiko233\n\n- **ui:** Base page use radix-ui scroll area by @keiko233\n\n- **ui:** Dialog allow windows drag when prop full is true by @keiko233\n\n- **ui:** Add full screen style for dialog by @keiko233\n\n- **ui:** Minor tweaks for border radius by @keiko233\n\n- **ui:** Replace Switch to LoadingSwitch for SwitchItem by @keiko233\n\n- **ui:** Init sparkline chart by @keiko233\n\n- **ui:** Add sideClassName props for SidePage component by @keiko233\n\n- **ui:** Add reverse icon props for ExpandMore component by @keiko233\n\n- **ui:** Add MuiLinearProgress material you style override by @keiko233\n\n- **ui:** Add more props support for BaseDialog by @keiko233\n\n- **ui:** Add side toggle animation & reverse layout props by @keiko233\n\n- **ui:** Add SidePage component by @keiko233\n\n- **ui:** Add TextItem component by @keiko233\n\n- **ui:** Add BaseItem component by @keiko233\n\n- **ui:** Add TextFieldProps for NumberItem by @keiko233\n\n- **ui:** Add ExpandMore component by @keiko233\n\n- **ui:** Add loading props support for BaseCard by @keiko233\n\n- **ui:** Add LoadingSwitch component by @keiko233\n\n- **ui:** Add divider props support for BaseDialog by @keiko233\n\n- **ui:** Add BaseDialog component by @keiko233\n\n- **ui:** Add MuiDialog material you override by @keiko233\n\n- **ui:** Add disabled props for MenuItem by @keiko233\n\n- **ui:** Add selectSx for MenuItem component by @keiko233\n\n- **ui:** Add divider props for NumberItem by @keiko233\n\n- **ui:** Add Expand component by @keiko233\n\n- **ui:** Add NumberItem component by @keiko233\n\n- **ui:** Add MenuItem component by @keiko233\n\n- **ui:** Add SwitchItem component by @keiko233\n\n- **ui:** Add BaseCard label props undefined type support by @keiko233\n\n- **ui:** Add MDYBaseCard component by @keiko233\n\n- **ui:** Add MuiSwitch material you override by @keiko233\n\n- **ui:** Add MuiCard & MuiCardContent material you override by @keiko233\n\n- **ui:** Custom breakpoints by @keiko233\n\n- **ui:** Add memo suuport for MDYBasePage header by @keiko233\n\n- **ui:** Add MuiPaper material you override by @keiko233\n\n- **ui:** Add MDYBasePage component by @keiko233\n\n- **ui:** Add MuiButtonGroup material you override by @keiko233\n\n- **ui:** Add MuiButton material you override by @keiko233\n\n- **ui:** Add new mui theme create method for material you by @keiko233\n\n- **updater:** Add a view github button by @greenhat616\n\n- **use-message:** Add nyanpasu title prefix by @keiko233\n\n- **util:** Add a util to collect env infos to submit issues by @greenhat616\n\n- **web:** Replace default utl to Dashboard Page by @keiko233\n\n- **window:** Always on top by @greenhat616\n\n- Minor tweaks for app layout by @keiko233\n\n- Draft updater dialog, and close #1328 by @greenhat616\n\n- Add core updater progress by @keiko233\n\n- Draft core updater progres by @greenhat616\n\n- Add lazy loading for proxies icons by @greenhat616\n\n- Allow select on rule page & log page by @keiko233\n\n- Add clash icon local cache by @greenhat616\n\n- Add runtime config diff dialog by @greenhat616\n\n- Add tun stack selector by @greenhat616\n\n- Impl script esm and async support (#1266) by @greenhat616 in [#1266](https://github.com/libnyanpasu/clash-nyanpasu/pull/1266)\n\n- Should hidden speed chip while no history by @greenhat616\n\n- Add auto migration before app run by @greenhat616\n\n- Add migrations manager and cmds to run migration by @greenhat616\n\n- Add swift feedback button by @greenhat616\n\n- Print better build info by @greenhat616\n\n- Add a experimental mutlithread file download util by @greenhat616\n\n- Experimental add draggable logo by @greenhat616\n\n- Resizable sidebar without config presistant by @greenhat616\n\n- Use node octokit deps by @keiko233\n\n- Profile spec chains support by @greenhat616\n\n- Support lua script type and do a lot refactor by @greenhat616\n\n### 🐛 Bug Fixes\n\n- **app-setting:** Missing fields with template by @keiko233\n\n- **chians:** Throw backend log on use native dialog by @keiko233\n\n- **ci:** Update publish script by @greenhat616\n\n- **ci:** Updater checkout issue by @greenhat616\n\n- **ci:** Updater checkout issue by @greenhat616\n\n- **ci:** Prepend changelog by @greenhat616\n\n- **ci:** Build by @greenhat616\n\n- **clash:** Accpet clash rs status code and handle status error by @greenhat616\n\n- **clash:** Hidden ipv6 setting while clash rs by @greenhat616\n\n- **clash-web:** Fix reversed Boolean value by @keiko233\n\n- **clash-web:** Empty array err by @keiko233\n\n- **config:** Replace enable_auto_check_update by @keiko233\n\n- **connections:** Table type filed err by @keiko233\n\n- **connections:** Host undefined err by @keiko233\n\n- **csp:** Allow loading local cache server assets by @greenhat616\n\n- **csp:** Allow img-src from https by @keiko233\n\n- **custom-scheme:** Xdg-mime default wrong call format by @greenhat616\n\n- **custom-scheme:** Front page redirect by @greenhat616\n\n- **custom-scheme:** Should pass single-instance while launched by custom schema by @greenhat616\n\n- **custom-scheme:** Support mutiple scheme by @greenhat616\n\n- **custom-theme:** Unregister event when the themoe mode is not system by @keiko233\n\n- **custom-theme:** Fix custom theme effect & system theme sync event by @keiko233\n\n- **dashboard:** Data panel layer size err by @keiko233\n\n- **dashboard:** Zero value display err by @keiko233\n\n- **deep link:** Use different identifiers in dev mode by @keiko233\n\n- **deps:** Add misssing deps by @keiko233\n\n- **deps:** Vite-plugin-monaco-editor version err by @keiko233\n\n- **dev:** When dev feature force use dev app dir by @keiko233\n\n- **drawer:** Style prop merge err by @keiko233\n\n- **drawer:** Offset value err by @keiko233\n\n- **drawer:** Small size drawer layout err by @keiko233\n\n- **drawer:** Minor tweaks by @keiko233\n\n- **drawer:** Fix scroll err & hidden scrollbar by @keiko233\n\n- **drawer:** Fix padding & text position by @keiko233\n\n- **enhance:** Rm useless use_lowercase hook, and close #1323 by @greenhat616\n\n- **enhance:** Use oxc ast to wrap function main, close #1298 by @greenhat616\n\n- **enhance:** Should update after editing activated chain item by @greenhat616\n\n- **enhance:** Transform allow lan decrepation by @greenhat616\n\n- **enhance:** Should export default by @greenhat616\n\n- **enhance:** Use indexmap to ensure the process order by @greenhat616\n\n- **enhance:** Mark process fn async by @greenhat616\n\n- **guard:** Remove ipv6 field while core is clash rs by @greenhat616\n\n- **hook:** Replace DebounceFn to ThrottleFn by @keiko233\n\n- **image-resize:** Correct image buffer extraction and resizing logic by @keiko233\n\n- **interface:** Close all connections err by @keiko233\n\n- **interface:** Drop defalut clash mode set by @keiko233\n\n- **interface:** Bad references by @keiko233\n\n- **interface:** Add clash rs version format method by @keiko233\n\n- **interface:** Request clash when use set by @keiko233\n\n- **interface:** Data type err by @keiko233\n\n- **interface:** Typos by @keiko233\n\n- **layout:** Bringup layout control to top layer by @keiko233\n\n- **lint:** Prettier plugin load err by @keiko233\n\n- **linux:** Replace backdrop blur to background opacity by @keiko233\n\n- **linux:** Service controls gui prompt, and close #1443 by @greenhat616\n\n- **linux:** Try to use symbol to fix tray issue by @greenhat616\n\n- **linux:** Use a workaround to make tray select work by @greenhat616\n\n- **linux:** Try to solve sysproxy resolver in appimage by @greenhat616\n\n- **linux:** Try to solve xdg-open in AppImage by @greenhat616\n\n- **logs:** Disable log state err by @keiko233\n\n- **logs:** Logs page freeze by @keiko233\n\n- **logs:** Logs page style err by @keiko233\n\n- **macos:** App icon size by @keiko233\n\n- **macos:** Dialog layout position err by @keiko233\n\n- **macos:** Remove prevent close block in macos by @greenhat616\n\n- **macos:** Rename single instance check path by @greenhat616\n\n- **macos:** Try to use another name to fix create dir error by @greenhat616\n\n- **node-card:** Layout err by @keiko233\n\n- **nsis:** Uninstall service check by @greenhat616\n\n- **nsis:** Stop running core by service while install and rm service dir while uninstall by @greenhat616\n\n- **nyanpasu:** Missing of recoil drop commit by @keiko233\n\n- **nyanpasu:** Missing tailwind css import by @keiko233\n\n- **nyanpasu:** Word typos by @keiko233\n\n- **nyanpasu:** Undfined value err by @keiko233\n\n- **nyanpasu:** Props usage error by @keiko233\n\n- **nyanpasu:** Drop tooltips to fix mui warning by @keiko233\n\n- **portable:** Add nyanpasu service binary by @greenhat616\n\n- **profile:** Dialog padding err by @keiko233\n\n- **profile:** Just invisble progress by @greenhat616\n\n- **profile:** Correctly handle filtering of script types in filterProfiles function by @keiko233\n\n- **profile-viewer:** Replace default profile user agent to clash-nyanpasu by @keiko233\n\n- **profiles:** Dont use sub component to solve the loss data issue by @greenhat616\n\n- **profiles:** Scoped chians state update err by @keiko233\n\n- **profiles:** Add missing open file on chains menu by @keiko233\n\n- **profiles:** Monaco dialog style err by @keiko233\n\n- **profiles:** Fix new chain method err by @keiko233\n\n- **profiles:** Fix profile item selected color on dark mode by @keiko233\n\n- **profiles:** Fix color on dark mode by @keiko233\n\n- **profiles:** Add missing open file method by @keiko233\n\n- **profiles:** Profile traffic percent calculation error by @keiko233\n\n- **profiles:** Add selected props for ProfileItem by @keiko233\n\n- **providers:** Single line layout err by @keiko233\n\n- **proxies:** Proxy node select err & render err by @keiko233\n\n- **proxies:** Sorting cannot be performed in global mode by @keiko233\n\n- **proxies:** Nodecard transition by @keiko233\n\n- **proxies:** Delay sort & timeout string by @keiko233\n\n- **proxies:** Global proxy select err by @keiko233\n\n- **proxies:** Incorrect judgment leading to value transfer error by @keiko233\n\n- **proxies:** Missing import by @keiko233\n\n- **proxies:** Current group get err by @keiko233\n\n- **route:** Reaplce icon dashboard to Dashboard by @keiko233\n\n- **rules:** Rules page display err by @keiko233\n\n- **script:** Decompress nyanpasu-service by @greenhat616\n\n- **script:** Replace appimage to rpm pkg by @keiko233\n\n- **script:** Use latest node version by @keiko233\n\n- **script:** Fix build with nightly prepare script by @keiko233\n\n- **script:** Nightly prepare package.json path by @keiko233\n\n- **scripts:** Typos by @keiko233\n\n- **scripts:** Telegram notify failed to request github repo releases info by @keiko233\n\n- **service:** Restart core while service mode enabled and service state changed by @greenhat616\n\n- **service:** Adapt the current ui by @greenhat616\n\n- **setting:** Service mod toggle by @keiko233\n\n- **setting-clash-core:** Disable initial animetion by @keiko233\n\n- **setting-clash-core:** Add user triger check update loading status by @keiko233\n\n- **setting-nyanpasu-version:** Incorrect value passing by @keiko233\n\n- **setting-system-proxy:** Grid layout breakpoint value by @keiko233\n\n- **setting-web-ui:** Zero value for index err by @keiko233\n\n- **settings:** Version pkg import err by @keiko233\n\n- **settings:** Swr use err by @keiko233\n\n- **settings:** Page masonry layout err by @keiko233\n\n- **settings:** Fix auto check update fileld stats err by @keiko233\n\n- **single-instance:** Should use path instead of namespace in linux by @greenhat616\n\n- **string:** Typo in side-chain.tsx (#999) by @NalCol in [#999](https://github.com/libnyanpasu/clash-nyanpasu/pull/999)\n\n- **styles:** Try to use normalize.css to solve webkit font issue by @greenhat616\n\n- **tauri:** Missing dialog features by @keiko233\n\n- **tauri:** Mixed content err by @keiko233\n\n- **theme:** Fix value merge null err by @keiko233\n\n- **theme:** Update breakpoint value by @keiko233\n\n- **tray:** Add a barrier to try to solve the tray selector issue in linux by @greenhat616\n\n- **tsconfig:** Typescript type reference issue by @keiko233\n\n- **tun:** Compatible with clash rs by @greenhat616\n\n- **ui:** Dialog exit animation err by @keiko233\n\n- **ui:** Close animetion position err by @keiko233\n\n- **ui:** Fix dialog unmount err by @keiko233\n\n- **ui:** Missing dialog z index css prop by @keiko233\n\n- **ui:** Refactor dialog use radix ui portal by @keiko233\n\n- **ui:** Scroll bar hidden on no padding by @keiko233\n\n- **ui:** Base page dom layout err by @keiko233\n\n- **ui:** Add Menu Paper box shadow by @keiko233\n\n- **ui:** Fixed FloatingButton position by @keiko233\n\n- **ui:** Fixed FloatingButton position by @keiko233\n\n- **ui:** Force set FloadtingButton posotion absolute by @keiko233\n\n- **ui:** Drop memo children too by @keiko233\n\n- **ui:** Drop SidePage memo by @keiko233\n\n- **ui:** Hide SidePage side content when there is no side by @keiko233\n\n- **ui:** Drop width for MDYBasePage-content by @keiko233\n\n- **ui:** Fix BasePage content width by @keiko233\n\n- **ui:** Disable loading mask animetion initial for BaseCard by @keiko233\n\n- **ui:** Default unmount dialog modal by @keiko233\n\n- **ui:** Replace padding to Box element by @keiko233\n\n- **ui:** Disable initial animetion for Expand component by @keiko233\n\n- **ui:** Add disabled overlay for MuiSwitch by @keiko233\n\n- **ui:** Fix BaseDialog content height err by @keiko233\n\n- **ui:** Pin MenuItem width by @keiko233\n\n- **ui:** Disbale MuiPaper override by @keiko233\n\n- **updater:** Invaild date issue by @greenhat616\n\n- **updater:** Fetch version.json from main branch (#968) by @aviraxp in [#968](https://github.com/libnyanpasu/clash-nyanpasu/pull/968)\n\n- **util:** Speed test should use desc order by @greenhat616\n\n- **webkit:** Border radius not apply on absolute layout by @keiko233\n\n- **window:** Show window when frontend mounted by @keiko233\n\n- **windows:** Window controller position by @keiko233\n\n- **windows:** Custom scheme call by @greenhat616\n\n- Disable migrate app dir feature in macos, linux by @greenhat616\n\n- Custom scheme url parser in webkit by @greenhat616\n\n- Try to fix read profile state again by @greenhat616\n\n- Add a key to try to solve read profile issue by @greenhat616\n\n- Log time issue, and close #1447 by @greenhat616\n\n- Disable core update check in linux by @greenhat616\n\n- Disable app updater for linux expect AppImage by @greenhat616\n\n- Rm macos unsupport transparent by @greenhat616\n\n- Try to fix cross platform save win state issue by @greenhat616\n\n- Lint by @greenhat616\n\n- Lint by @greenhat616\n\n- Use open_that workaround for appimage by @greenhat616\n\n- React deps by @greenhat616\n\n- Check button issue by @greenhat616\n\n- Lint by @greenhat616\n\n- Profile runtime config button color by @greenhat616\n\n- Nsis build issue by @greenhat616\n\n- Exhaustive-deps lint by @greenhat616\n\n- Disable react complier lint until it fixes bug by @greenhat616\n\n- Add 172.16.0.0/12 system proxy passby on windows (#1405) by @Remonli in [#1405](https://github.com/libnyanpasu/clash-nyanpasu/pull/1405)\n\n- Use tauri client for asn request by @greenhat616\n\n- Proxies nodes list update issue, and close #1402 by @greenhat616\n\n- Lint by @greenhat616\n\n- Mutate core version while updater finished by @greenhat616\n\n- Updater replace issue, and close #1377 by @greenhat616\n\n- Script prepare gh token by @greenhat616\n\n- Lint by @greenhat616\n\n- Build by @greenhat616\n\n- Build by @greenhat616\n\n- Build by @greenhat616\n\n- Lint by @greenhat616\n\n- Lint by @greenhat616\n\n- Try to fix ts project import issue by @greenhat616\n\n- Ts project settings (#1394) by @greenhat616 in [#1394](https://github.com/libnyanpasu/clash-nyanpasu/pull/1394)\n\n- Ts project lint by @greenhat616\n\n- Correct the update order to ensure the script changes get applied by @greenhat616\n\n- Clash config select issue, and close #1303 by @greenhat616\n\n- Spawn orientation random updater id by @keiko233\n\n- Throw single instance create error by @greenhat616\n\n- Connection page lazy loading by @greenhat616\n\n- Config detect, and close #1305 by @greenhat616\n\n- Quick import submit when enter press by @greenhat616\n\n- Icon loader should not lazy by @greenhat616\n\n- Icon lazy image by @greenhat616\n\n- Show a error dialog while check latest cores error, and close #1302 by @greenhat616\n\n- Issues by @greenhat616\n\n- Marquee by @greenhat616\n\n- No need retry while os error 232 by @greenhat616\n\n- Not save clash overrides config, close #1295 by @greenhat616\n\n- Fix broken pipe causing too many logs #637 by @4o3F\n\n- Fix tray not able to reset by @4o3F\n\n- Update sysproxy-rs to support KDE by @4o3F\n\n- Fix url scheme issue #902 by @4o3F\n\n- Use window open counter to prevent double-click opening the window immediately by @greenhat616\n\n- Should update match by @greenhat616\n\n- Make profile yaml file to be formatted by serde yaml by @greenhat616\n\n- Update config while patch profile scoped chain by @greenhat616\n\n- Lint by @greenhat616\n\n- Lint by @greenhat616\n\n- Lint by @greenhat616\n\n- Clash rs core switch by @greenhat616\n\n- Patch profile chains by @greenhat616\n\n- Patch profile chains by @greenhat616\n\n- Lint by @greenhat616\n\n- Ignore deleteConnection error while applying new profile by @greenhat616\n\n- Make port strategy check better by @greenhat616\n\n- No exit code on unix platform by @greenhat616\n\n- Try to solve the migration failed issue by @greenhat616\n\n- Lint by @greenhat616\n\n- Ui service control and updater path by @greenhat616\n\n- Cleanup codes by @greenhat616\n\n- Lint by @greenhat616\n\n- Lint by @greenhat616\n\n- Skip migration while home dir is not exist, and close #1235 by @greenhat616\n\n- Skip migration while home dir is not exist, and close #1235 by @greenhat616\n\n- Lint by @greenhat616\n\n- Should create data dir and config dir when fetch it if not exist by @greenhat616\n\n- Styles by @greenhat616\n\n- Lint by @greenhat616\n\n- Migration panic by @greenhat616\n\n- Migrate all upcoming migrations while pending by @greenhat616\n\n- Migration missing dirs touch by @keiko233\n\n- Left container scrollbar gutter (#1225) by @fu050409 in [#1225](https://github.com/libnyanpasu/clash-nyanpasu/pull/1225)\n\n- Add quote prefix, and solve the undefined issue by @greenhat616\n\n- Drawer resize panel style by @keiko233\n\n- Lint by @greenhat616\n\n- Lint by @greenhat616\n\n- Build by @keiko233\n\n- Build by @keiko233\n\n- Missing export by @keiko233\n\n- Lint in linux by @greenhat616\n\n- Enhance process panic while profiles is empty by @greenhat616\n\n- Fmt by @greenhat616\n\n- Log path by @greenhat616\n\n- Use webview2-com-bridge to solve ra crash issue by @greenhat616\n\n- Lint by @greenhat616\n\n- Minor issues (#884) by @greenhat616 in [#884](https://github.com/libnyanpasu/clash-nyanpasu/pull/884)\n\n- Ci by @greenhat616\n\n- Lint by @greenhat616\n\n- Vite plugin monaco editor overrides by @greenhat616\n\n- Fix issue #776 by @4o3F\n\n- Mac x64 use mihomo compatible core (#773) by @Sakurasan\n\n- Lint by @keiko233\n\n- Change storage_db name by @4o3F\n\n- Fix database creation issue by @4o3F\n\n### 📚 Documentation\n\n- **readme:** Add nyanpasu 1.6.0 label by @keiko233\n\n- **readme:** Fix resource path err by @keiko233\n\n- Fix dev build shields card link err by @keiko233\n\n- Update screenshot & clean up docs by @keiko233\n\n### 🔨 Refactor\n\n- **chains:** Use bitflags instead of custom support struct by @greenhat616\n\n- **connections:** Drop mui/x-data-grid & use material-react-table by @keiko233\n\n- **core:** Use new core manager from nyanpasu utils to prepare for new nyanpasu service by @greenhat616\n\n- **custom-scheme:** Use nonblocking io and create window if window is not exist by @greenhat616\n\n- **dashboard:** Split health panel by @keiko233\n\n- **dirs:** Split home_dir into config_dir and data_dir by @greenhat616\n\n- **drawer:** Use react-split-grid replace react-resizable-panels by @keiko233\n\n- **frontend:** Make monorepo by @keiko233\n\n- **hook:** Use-breakpoint hook with react-use by @keiko233\n\n- **hook:** Optimize useBreakpoint hook to reduce unnecessary updates by @keiko233\n\n- **hotkeys:** First draft hotkeys setting dialog by @greenhat616\n\n- **interface!:** Increase code readability by @keiko233\n\n- **interface/service:** Tauri interface writing by @keiko233\n\n- **layout:** New layout design by @keiko233\n\n- **nsis:** Use nsis's built-in com plugin instead of ApplicationID plugin (#9606) by @amrbashir\n\n- **profiles:** Chians component by @keiko233\n\n- **proxies:** Drop memo use effert to update by @keiko233\n\n- **proxies:** Delay button using tailwind css and memo by @keiko233\n\n- **script:** Manifest generator script by @keiko233\n\n- **script:** Resource check script by @keiko233\n\n- **service:** Add new service backend support by @greenhat616\n\n- **theme:** Migrating to CSS theme variables by @keiko233\n\n- **ui:** Drop mui dialog & use redix-ui with framer motion by @keiko233\n\n- **updater:** Support speedtest and updater concurrency by @greenhat616\n\n- Drop async component use react suspense by @keiko233\n\n- Proxies page use new interface by @keiko233\n\n- Refactor rocksdb into redb, this should solve #452 by @4o3F in [#755](https://github.com/libnyanpasu/clash-nyanpasu/pull/755)\n\n- Refactor rocksdb into redb, this should fix #452 by @4o3F\n\n---\n\n## New Contributors\n\n- @Remonli made their first contribution in [#1405](https://github.com/libnyanpasu/clash-nyanpasu/pull/1405)\n- @fu050409 made their first contribution in [#1225](https://github.com/libnyanpasu/clash-nyanpasu/pull/1225)\n- @NalCol made their first contribution in [#999](https://github.com/libnyanpasu/clash-nyanpasu/pull/999)\n- @aviraxp made their first contribution in [#968](https://github.com/libnyanpasu/clash-nyanpasu/pull/968)\n- @amrbashir made their first contribution\n- @Sakurasan made their first contribution\n\n**Full Changelog**: https://github.com/libnyanpasu/clash-nyanpasu/compare/v1.5.1...v1.6.0\n\n## [1.5.1] - 2024-04-08\n\n### ✨ Features\n\n- **backend:** Allow to hide tray selector (#626) by @greenhat616 in [#626](https://github.com/libnyanpasu/clash-nyanpasu/pull/626)\n\n- **config:** Support custom app dir in windows (#582) by @greenhat616 in [#582](https://github.com/libnyanpasu/clash-nyanpasu/pull/582)\n\n- **custom-schema:** Add support for name and desc fields by @greenhat616\n\n- Perf motion transition by @keiko233\n\n- Lock rustup toolchain to stable channel by @4o3F\n\n- New design log page by @keiko233\n\n- New desigin rules page by @keiko233\n\n- Improve WebSocket reconnection in useWebsocket hook by @keiko233\n\n### 🐛 Bug Fixes\n\n- **bundler/nsis:** Don't use /R flag on installation dir by @keiko233\n\n- **chains:** Only guard fields should be overwritten (#629) by @greenhat616 in [#629](https://github.com/libnyanpasu/clash-nyanpasu/pull/629)\n\n- **cmds:** Migrate custom app dir typo (#628) by @greenhat616 in [#628](https://github.com/libnyanpasu/clash-nyanpasu/pull/628)\n\n- **cmds:** `path` in changing app dir call (#591) by @greenhat616 in [#591](https://github.com/libnyanpasu/clash-nyanpasu/pull/591)\n\n- **docs:** Fix url typos by @keiko233\n\n- **notification:** Unexpected `}` (#563) by @WOSHIZHAZHA120 in [#563](https://github.com/libnyanpasu/clash-nyanpasu/pull/563)\n\n- Revert previous commit by @greenhat616\n\n- Subscription info parse issue, closing #729 by @greenhat616\n\n- Fix misinterprete of tauri's application args by @4o3F\n\n- Missing github repo context by @keiko233\n\n- Try to add a launch command to make restart application work by @greenhat616\n\n- Try to use delayed singleton check to make restart app work by @greenhat616\n\n- Panic while quit application by @greenhat616\n\n- Restart application not work by @greenhat616\n\n- Fix migration issue for path with space by @4o3F\n\n- Fix migration child process issue by @4o3F\n\n- Fix rename permission issue by @4o3F\n\n- Connection page NaN and first enter animation by @greenhat616\n\n- Use shiki intead of shikiji by @greenhat616\n\n- Use clash verge rev patch to resolve Content-Disposition Filename issue, closing #703 by @greenhat616\n\n- Lint by @greenhat616\n\n- Command path by @greenhat616\n\n- Draft patch to resolve custom app config migration by @greenhat616\n\n- Proxy groups virtuoso also overscan by @keiko233\n\n- Top item no padding by @keiko233\n\n- Use overscan to prevent blank scrolling by @keiko233\n\n- Profiles when drag sort container scroll style by @keiko233\n\n- Profile-box border radius value by @keiko233\n\n- Slinet start get_window err by @keiko233\n\n- MDYSwitch-thumb size by @keiko233\n\n- Build by @keiko233\n\n- Disable webview2 SwipeNavigation by @keiko233\n\n- Fix wrong window size and position by @4o3F\n\n- Fix single instance check failing on macos by @4o3F\n\n### 📚 Documentation\n\n- Add clash-verge-rev acknowledgement by @greenhat616\n\n- Add twitter img tag by @keiko233\n\n- Add license img tag by @keiko233\n\n- Align center tag imgs by @keiko233\n\n- Update readme by @keiko233\n\n- Update issues template by @greenhat616\n\n### 🔨 Refactor\n\n- Use lazy load routes to improve performance by @greenhat616\n\n---\n\n## New Contributors\n\n- @WOSHIZHAZHA120 made their first contribution in [#563](https://github.com/libnyanpasu/clash-nyanpasu/pull/563)\n\n**Full Changelog**: https://github.com/libnyanpasu/clash-nyanpasu/compare/v1.5.0...v1.5.1\n\n## [1.5.0] - 2024-03-03\n\n### 💥 Breaking Changes\n\n- **backend:** Add tray proxies selector support (#417) by @greenhat616 in [#417](https://github.com/libnyanpasu/clash-nyanpasu/pull/417)\n\n- **clash:** Add default core secret and impl port checker before clash start (#533) by @greenhat616 in [#533](https://github.com/libnyanpasu/clash-nyanpasu/pull/533)\n\n### ✨ Features\n\n- **config:** Add migration for old config dir (#419) by @4o3F in [#419](https://github.com/libnyanpasu/clash-nyanpasu/pull/419)\n\n- **connection:** Allow filter out process name by @greenhat616\n\n- **locale:** Use system locale as default (#437) by @greenhat616 in [#437](https://github.com/libnyanpasu/clash-nyanpasu/pull/437)\n\n- **tray:** Add tray icon resize logic to improve icon rendering (#540) by @greenhat616 in [#540](https://github.com/libnyanpasu/clash-nyanpasu/pull/540)\n\n- **tray:** Add diff check for system tray partial update (#477) by @4o3F in [#477](https://github.com/libnyanpasu/clash-nyanpasu/pull/477)\n\n- Custom schema support (#516) by @4o3F in [#516](https://github.com/libnyanpasu/clash-nyanpasu/pull/516)\n\n- Add Auto Check Updates Switch by @keiko233\n\n- Refactor UpdateViewer by @keiko233\n\n- OnCheckUpdate button supports loading animation & refactoring error removal notification using dialog by @keiko233\n\n- Add margin for SettingItem extra element by @keiko233\n\n- Add useMessage hook by @keiko233\n\n- Refactor GuardStatus & support loading status by @keiko233\n\n- MDYSwitch support loading prop by @keiko233\n\n- Add MDYSwitch & replace all Switches with MDYSwitch by @keiko233\n\n- Color select use MuiColorInput by @keiko233\n\n- Make profile material you by @keiko233\n\n- New style design profile item drag sort by @keiko233\n\n### 🐛 Bug Fixes\n\n- **ci:** Replace github workflow token by @keiko233\n\n- **config:** Fix config migration (#433) by @4o3F in [#433](https://github.com/libnyanpasu/clash-nyanpasu/pull/433)\n\n- **custom-schema:** Fix schema not working for new opening and dialog not showing with certain route (#534) by @4o3F in [#534](https://github.com/libnyanpasu/clash-nyanpasu/pull/534)\n\n- **deps:** Update rust crates by @greenhat616\n\n- **macos:** Use rfd to prevent panic by @greenhat616\n\n- **nsis:** Should not stop verge service while updating by @greenhat616\n\n- **proxies:** Use indexmap instead to correct order by @greenhat616\n\n- **proxies:** Reduce tray updating interval by @greenhat616\n\n- **tray:** Use base64 encoded id to fix item not found issue by @greenhat616\n\n- **tray:** Should disable click expect Selector and Fallback type by @greenhat616\n\n- **tray:** Proxies updating deadlock by @greenhat616\n\n- Release ci by @greenhat616\n\n- Release ci by @greenhat616\n\n- Fix wrong window position and size with multiple screen by @4o3F\n\n- Resolve save windows state event by @greenhat616\n\n- Media screen value typos by @keiko233\n\n- Layout error when window width is small by @keiko233\n\n- Lint by @greenhat616\n\n- Line breaks typos by @keiko233\n\n- MDYSwitch switchBase padding value by @keiko233\n\n- Lint by @greenhat616\n\n- Fmt by @greenhat616\n\n- Build issue by @greenhat616\n\n- Config migration issue by @greenhat616\n\n- Ci by @greenhat616\n\n- Proxy item box-shadow err by @keiko233\n\n### 🔨 Refactor\n\n- **clash:** Move api and core manager into one mod (#411) by @greenhat616 in [#411](https://github.com/libnyanpasu/clash-nyanpasu/pull/411)\n\n- **i18n:** Change backend localization to rust-i18n (#425) by @4o3F in [#425](https://github.com/libnyanpasu/clash-nyanpasu/pull/425)\n\n- **logging:** Use `tracing` instead of `log4rs` (#486) by @greenhat616 in [#486](https://github.com/libnyanpasu/clash-nyanpasu/pull/486)\n\n- **proxies:** Proxies hash and diff logic by @greenhat616\n\n- **single-instance:** Refactor single instance check (#499) by @4o3F in [#499](https://github.com/libnyanpasu/clash-nyanpasu/pull/499)\n\n---\n\n**Full Changelog**: https://github.com/libnyanpasu/clash-nyanpasu/compare/v1.4.5...v1.5.0\n\n## [1.4.5] - 2024-02-08\n\n### 💥 Breaking Changes\n\n- **nsis:** Switch to both installMode by @greenhat616\n\n- **updater:** Use nsis instead of msi by @greenhat616\n\n### 🐛 Bug Fixes\n\n- **bundle:** Instance is running while updating app (#393) by @greenhat616 in [#393](https://github.com/libnyanpasu/clash-nyanpasu/pull/393)\n\n- **bundler:** Kill processes while updating in windows by @greenhat616\n\n- **ci:** Daily updater issue (#392) by @greenhat616 in [#392](https://github.com/libnyanpasu/clash-nyanpasu/pull/392)\n\n- **ci:** Nightly updater issue by @greenhat616\n\n- **nsis:** Kill nyanpasu processes while updating (#403) by @greenhat616 in [#403](https://github.com/libnyanpasu/clash-nyanpasu/pull/403)\n\n- Portable issues (#395) by @greenhat616 in [#395](https://github.com/libnyanpasu/clash-nyanpasu/pull/395)\n\n- Minimize icon is wrong while resize window (#394) by @greenhat616 in [#394](https://github.com/libnyanpasu/clash-nyanpasu/pull/394)\n\n- Sort connection in numerical comparison for `Download`, `DL Speed`, etc (#367) by @Jeremy-Hibiki in [#367](https://github.com/libnyanpasu/clash-nyanpasu/pull/367)\n\n- Resources missing by @greenhat616 in [#354](https://github.com/libnyanpasu/clash-nyanpasu/pull/354)\n\n---\n\n## New Contributors\n\n- @Jeremy-Hibiki made their first contribution in [#367](https://github.com/libnyanpasu/clash-nyanpasu/pull/367)\n\n**Full Changelog**: https://github.com/libnyanpasu/clash-nyanpasu/compare/v1.4.4...v1.4.5\n\n## [1.4.4] - 2024-01-29\n\n### 🐛 Bug Fixes\n\n- **backend:** Fix deadlock issue on config (#312) by @4o3F in [#312](https://github.com/libnyanpasu/clash-nyanpasu/pull/312)\n\n- **ci:** Publish & updater by @greenhat616\n\n- **ci:** Should generate manifest in dev branch for compatible with <= 1.4.3 (#292) by @greenhat616 in [#292](https://github.com/libnyanpasu/clash-nyanpasu/pull/292)\n\n- **deps:** Update deps (#294) by @greenhat616 in [#294](https://github.com/libnyanpasu/clash-nyanpasu/pull/294)\n\n- **portable:** Portable bundle issue (#335) by @greenhat616 in [#335](https://github.com/libnyanpasu/clash-nyanpasu/pull/335)\n\n- **portable:** Do not use system notification api while app is portable (#334) by @greenhat616 in [#334](https://github.com/libnyanpasu/clash-nyanpasu/pull/334)\n\n- **updater:** Use release body as updater note (#333) by @greenhat616 in [#333](https://github.com/libnyanpasu/clash-nyanpasu/pull/333)\n\n- Use if let instead (#309) by @greenhat616 in [#309](https://github.com/libnyanpasu/clash-nyanpasu/pull/309)\n\n### 📚 Documentation\n\n- Add ArchLinux AUR install suggestion (#293) by @Kimiblock in [#293](https://github.com/libnyanpasu/clash-nyanpasu/pull/293)\n\n### 🔨 Refactor\n\n- **backend:** Improve code robustness (#303) by @greenhat616 in [#303](https://github.com/libnyanpasu/clash-nyanpasu/pull/303)\n\n---\n\n**Full Changelog**: https://github.com/libnyanpasu/clash-nyanpasu/compare/v1.4.3...v1.4.4\n\n## [1.4.3] - 2024-01-20\n\n### ✨ Features\n\n- New release workflow (#284) by @greenhat616 in [#284](https://github.com/libnyanpasu/clash-nyanpasu/pull/284)\n\n- Proxies ui minor tweaks by @keiko233\n\n- Make proxies material you by @keiko233\n\n### 🐛 Bug Fixes\n\n- **ci:** Pin rust version to 1.74.1 (#213) by @greenhat616 in [#213](https://github.com/libnyanpasu/clash-nyanpasu/pull/213)\n\n- **ci:** Use latest action by @greenhat616\n\n- **ci:** Use dev commit hash when schedule dispatch by @greenhat616\n\n- **log:** Incorrect color in light mode by @greenhat616\n\n- **rocksdb:** Use TransactionDB instead of OptimisticTransactionDB (#194) by @greenhat616 in [#194](https://github.com/libnyanpasu/clash-nyanpasu/pull/194)\n\n- **updater:** Should use nyanpasu proxy or system proxy when performing request (#273) by @greenhat616 in [#273](https://github.com/libnyanpasu/clash-nyanpasu/pull/273)\n\n- **updater:** Add status code judge by @greenhat616\n\n- **updater:** Allow to use elevated permission to copy and override core by @greenhat616\n\n- **vite:** Rm useless shikiji langs support (#267) by @greenhat616 in [#267](https://github.com/libnyanpasu/clash-nyanpasu/pull/267)\n\n- Release ci by @greenhat616\n\n- Publish ci by @greenhat616\n\n- Notification premission check (#263) by @greenhat616 in [#263](https://github.com/libnyanpasu/clash-nyanpasu/pull/263)\n\n- Notification fallback (#262) by @greenhat616 in [#262](https://github.com/libnyanpasu/clash-nyanpasu/pull/262)\n\n- Stable channel build issue (#248) by @greenhat616 in [#248](https://github.com/libnyanpasu/clash-nyanpasu/pull/248)\n\n- Virtuoso scroller bottom not padding by @keiko233\n\n- Windrag err by @keiko233\n\n- Same text color for `REJECT-DROP` policy as `REJECT` (#236) by @xkww3n in [#236](https://github.com/libnyanpasu/clash-nyanpasu/pull/236)\n\n- Enable_tun block the process (#232) by @dyxushuai\n\n- #212 by @greenhat616\n\n- Lint by @greenhat616\n\n- Updater by @greenhat616\n\n- Dark mode flash in win by @greenhat616\n\n- Open file, closing #197 by @greenhat616\n\n- Add a panic hook to collect logs and show a dialog (#191) by @greenhat616 in [#191](https://github.com/libnyanpasu/clash-nyanpasu/pull/191)\n\n---\n\n## New Contributors\n\n- @xkww3n made their first contribution in [#236](https://github.com/libnyanpasu/clash-nyanpasu/pull/236)\n\n**Full Changelog**: https://github.com/libnyanpasu/clash-nyanpasu/compare/v1.4.2...v1.4.3\n\n## [1.4.2] - 2023-12-24\n\n### ✨ Features\n\n- **updater:** Finish ui by @greenhat616\n\n- **updater:** Finish core updater backend by @greenhat616\n\n- Use christmas logo by @keiko233\n\n- Auto add dns according this method by @yswtrue\n\n- Backport concurrency of latency test by @greenhat616\n\n- Auto log clear by @greenhat616\n\n- Nightly build with updater by @greenhat616\n\n- Rules providers by @greenhat616\n\n- Improve animations by @greenhat616\n\n- Quick logs collect by @greenhat616\n\n- Bundled mihomo alpha by @greenhat616\n\n- New style win tray icon & add blue icon when tun enable by @keiko233\n\n### 🐛 Bug Fixes\n\n- **ci:** Release build by @greenhat616\n\n- **ci:** Updater and dev build by @greenhat616\n\n- **dialog:** Align center and overflow issue by @greenhat616\n\n- **lint:** Toml fmt by @greenhat616\n\n- **resources:** Win service support and mihomo alpha version proxy by @greenhat616\n\n- **updater:** Copy logic by @greenhat616\n\n- **window:** Preserve window state before window minimized by @greenhat616\n\n- **window:** Add a workaround for close event in windows by @greenhat616\n\n- Minor tweak base-content width by @keiko233\n\n- Shikiji text wrapping err by @keiko233\n\n- Dark shikiji display color err by @keiko233\n\n- Pin runas to v1.0.0 by @greenhat616\n\n- Lint by @greenhat616\n\n- Bump nightly version after publish by @greenhat616\n\n- I18n resources by @greenhat616\n\n- Format ansi in log viewer by @greenhat616\n\n- Delay color, closing #124 by @greenhat616\n\n- #96 by @greenhat616\n\n- #92 by @greenhat616\n\n- Lint by @greenhat616\n\n- Ci by @greenhat616\n\n- Ci by @greenhat616\n\n- Ci by @greenhat616\n\n- Dev build branch issue by @greenhat616\n\n- Icon issues, close #55 by @greenhat616\n\n- Use a workaroud to reduce #59 by @greenhat616\n\n- Win state by @greenhat616\n\n### 📚 Documentation\n\n- Put issue config into effect (#148) by @txyyh in [#148](https://github.com/libnyanpasu/clash-nyanpasu/pull/148)\n\n- Upload missing issue config by @txyyh\n\n- Update issues template & upload ISSUE.md by @keiko233\n\n### 🔨 Refactor\n\n- **tasks:** Provide a universal abstract layer for task managing (#15) by @greenhat616\n\n- Profile updater by @greenhat616\n\n---\n\n## New Contributors\n\n- @yswtrue made their first contribution\n- @txyyh made their first contribution in [#148](https://github.com/libnyanpasu/clash-nyanpasu/pull/148)\n\n**Full Changelog**: https://github.com/libnyanpasu/clash-nyanpasu/compare/v1.4.1...v1.4.2\n\n## [1.4.1] - 2023-12-06\n\n### ✨ Features\n\n- **transition:** Add none and transparent variants by @greenhat616\n\n- Use twemoji to display flags in win (#48) by @greenhat616 in [#48](https://github.com/libnyanpasu/clash-nyanpasu/pull/48)\n\n- Add page transition mode and duration options by @keiko233 in [#42](https://github.com/libnyanpasu/clash-nyanpasu/pull/42)\n\n- Add page transition duration options by @greenhat616\n\n- Add page transition mode switch by @greenhat616\n\n- Use framer-motion for smooth page transition by @greenhat616\n\n- Support new clash field by @greenhat616\n\n- Support drag profile item (#36) by @Kuingsmile in [#36](https://github.com/libnyanpasu/clash-nyanpasu/pull/36)\n\n- Use tauri notification api by @keiko233\n\n- Update new clash.meta close #20 (#30) by @Kuingsmile in [#30](https://github.com/libnyanpasu/clash-nyanpasu/pull/30)\n\n- Support random mixed port (#29) by @Kuingsmile in [#29](https://github.com/libnyanpasu/clash-nyanpasu/pull/29)\n\n- Use workspace in backend by @greenhat616\n\n- New style win tray icon by @keiko233\n\n- Add tooltip for tray (#24) by @Kuingsmile in [#24](https://github.com/libnyanpasu/clash-nyanpasu/pull/24)\n\n- Experimental support `clash-rs` (#23) by @greenhat616 in [#23](https://github.com/libnyanpasu/clash-nyanpasu/pull/23)\n\n- Add UWP tool support, fix install service bug (#19) by @Kuingsmile in [#19](https://github.com/libnyanpasu/clash-nyanpasu/pull/19)\n\n### 🐛 Bug Fixes\n\n- Taskbar maximize toggle icon state (#46) by @greenhat616 in [#46](https://github.com/libnyanpasu/clash-nyanpasu/pull/46)\n\n- Missing scss import by @greenhat616\n\n- Lint by @greenhat616\n\n- Lint by @greenhat616\n\n- Workflow script typos by @keiko233\n\n- Osx-aarch64-upload bundlePath typos by @keiko233\n\n- Portable target dir by @keiko233\n\n- Portable missing clash-rs core by @keiko233\n\n- Item col width too narrow by @keiko233\n\n- I18n typos by @keiko233\n\n### 📚 Documentation\n\n- Add preview gif by @keiko233\n\n### 🔨 Refactor\n\n- **scripts:** Use ts and consola instead by @greenhat616\n\n- Use `workspace` in backend by @keiko233 in [#28](https://github.com/libnyanpasu/clash-nyanpasu/pull/28)\n\n---\n\n## New Contributors\n\n- @Kuingsmile made their first contribution in [#36](https://github.com/libnyanpasu/clash-nyanpasu/pull/36)\n\n**Full Changelog**: https://github.com/libnyanpasu/clash-nyanpasu/compare/v1.4.0...v1.4.1\n\n## [1.4.0] - 2023-11-15\n\n### ✅ Testing\n\n- Windows service by @zzzgydi\n\n### ✨ Features\n\n- **layout:** Add logo & update style by @zzzgydi\n\n- **macOS:** Support cmd+w and cmd+q by @zzzgydi\n\n- **proxy:** Finish proxy page ui and api support by @zzzgydi\n\n- **style:** Adjust style impl by @zzzgydi\n\n- **system tray:** Support switch rule/global/direct/script mode in system tray by @Limsanity\n\n- **traffic:** Api support & adjust by @zzzgydi\n\n- Minor tweaks by @keiko233\n\n- Nyanpasu Misc by @keiko233\n\n- Add baseContentIn animation by @keiko233\n\n- Add route transition by @keiko233\n\n- Material You! by @keiko233\n\n- Default disable ipv6 by @keiko233\n\n- Default enable unified-delay & tcp-concurrent with use meta core by @keiko233\n\n- Support copy CMD & PowerShell proxy env by @keiko233\n\n- Default use meta core by @keiko233\n\n- Update Clash Default bypass addrs by @keiko233\n\n- Theme: change color by @keiko233\n\n- Profiles: import btn with loading state by @keiko233\n\n- Profile-viewer: handleOk with loading state by @keiko233\n\n- Base-dialog: okBtn use LoadingButton by @keiko233\n\n- Nyanpasu Misc by @keiko233\n\n- Theme support modify --background-color by @keiko233\n\n- Settings use Grid layout by @keiko233\n\n- Add Connections Info to ConnectionsPage by @keiko233\n\n- ClashFieldViewer BaseDialog maxHeight usage percentage (#813) by @keiko233\n\n- Add Open Dashboard to the hotkey, close #723 by @zzzgydi\n\n- Add check for updates button, close #766 by @zzzgydi\n\n- Add paste and clear icon by @zzzgydi\n\n- Subscription URL TextField use multiline (#761) by @keiko233\n\n- Show loading when change profile by @zzzgydi\n\n- Support proxy provider update by @zzzgydi\n\n- Add repo link by @zzzgydi\n\n- Support clash meta memory usage display by @zzzgydi\n\n- Supports show connection detail by @zzzgydi\n\n- Update connection table with wider process column and click to show full detail (#696) by @whitemirror33\n\n- More trace logs by @zzzgydi\n\n- Add Russian Language (#697) by @shvchk\n\n- Center window when out of monitor by @zzzgydi\n\n- Support copy environment variable by @zzzgydi\n\n- Save window size and position by @zzzgydi\n\n- App log level add silent by @zzzgydi\n\n- Overwrite resource file according to file modified by @zzzgydi\n\n- Support app log level settings by @zzzgydi\n\n- Use polkit to elevate permission instaed of sudo (#678) by @Kimiblock\n\n- Add unified-delay field by @zzzgydi\n\n- Add error boundary to the app root by @zzzgydi\n\n- Show tray icon variants in different status (#537) by @w568w\n\n- Auto restart core after grand permission by @zzzgydi\n\n- Add restart core button by @zzzgydi\n\n- Support update all profiles by @zzzgydi\n\n- Support to grant permission to clash core by @zzzgydi\n\n- Support clash fields filter in ui by @zzzgydi\n\n- Open dir on the tray by @zzzgydi\n\n- Support to disable clash fields filter by @zzzgydi\n\n- Adjust macOS window style by @zzzgydi\n\n- Recover core after panic, close #353 by @zzzgydi\n\n- Use decorations in Linux, close #354 by @zzzgydi\n\n- Auto proxy layout column by @zzzgydi\n\n- Support to change proxy layout column by @zzzgydi\n\n- Support to open core dir by @zzzgydi\n\n- Profile page ui by @zzzgydi\n\n- Save some fields in the runtime config, close #292 by @zzzgydi\n\n- Add meta feature by @zzzgydi\n\n- Display proxy group type by @zzzgydi\n\n- Add use clash hook by @zzzgydi\n\n- Guard the mixed-port and external-controller by @zzzgydi\n\n- Adjust builtin script and support meta guard script by @zzzgydi\n\n- Disable script mode when use clash meta by @zzzgydi\n\n- Check config when change core by @zzzgydi\n\n- Support builtin script for enhanced mode by @zzzgydi\n\n- Adjust profiles page ui by @zzzgydi\n\n- Optimize proxy page ui by @zzzgydi\n\n- Add error boundary by @zzzgydi\n\n- Adjust clash log by @zzzgydi\n\n- Add draft by @zzzgydi\n\n- Change default latency test url by @zzzgydi\n\n- Auto close connection when proxy changed by @zzzgydi\n\n- Support to change external controller by @zzzgydi\n\n- Add sub-rules by @zzzgydi\n\n- Add version on tray by @zzzgydi\n\n- Add animation by @zzzgydi\n\n- Add animation to ProfileNew component (#252) by @angryLid\n\n- Check remote profile field by @zzzgydi\n\n- System tray support zh language by @zzzgydi\n\n- Display delay check result timely by @zzzgydi\n\n- Update profile with system proxy/clash proxy by @zzzgydi\n\n- Change global mode ui, close #226 by @zzzgydi\n\n- Default user agent same with app version by @zzzgydi\n\n- Optimize config feedback by @zzzgydi\n\n- Show connections with table layout by @zzzgydi\n\n- Show loading on proxy group delay check by @zzzgydi\n\n- Add chains[0] and process to connections display (#205) by @riverscn\n\n- Adjust connection page ui by @zzzgydi\n\n- Yaml merge key by @zzzgydi\n\n- Toggle log ws by @zzzgydi\n\n- Add rule page by @zzzgydi\n\n- Hotkey viewer by @zzzgydi\n\n- Refresh ui when hotkey clicked by @zzzgydi\n\n- Support hotkey (wip) by @zzzgydi\n\n- Hide window on macos by @zzzgydi\n\n- System proxy setting by @zzzgydi\n\n- Change default singleton port and support to change the port by @zzzgydi\n\n- Log info by @zzzgydi\n\n- Kill clash by pid by @zzzgydi\n\n- Change clash port in dialog by @zzzgydi\n\n- Add proxy item check loading by @zzzgydi\n\n- Compatible with proxy providers health check by @zzzgydi\n\n- Add empty ui by @zzzgydi\n\n- Complete i18n by @zzzgydi\n\n- Windows portable version do not check update by @zzzgydi\n\n- Adjust clash info parsing logs by @zzzgydi\n\n- Adjust runtime config by @zzzgydi\n\n- Support restart app on tray by @zzzgydi\n\n- Optimize profile page by @zzzgydi\n\n- Refactor by @zzzgydi\n\n- Adjust tun mode config by @zzzgydi\n\n- Reimplement enhanced mode by @zzzgydi\n\n- Use rquickjs crate by @zzzgydi\n\n- Reimplement enhanced mode by @zzzgydi\n\n- Finish clash field control by @zzzgydi\n\n- Clash field viewer wip by @zzzgydi\n\n- Support web ui by @zzzgydi\n\n- Adjust setting page style by @zzzgydi\n\n- Runtime config viewer by @zzzgydi\n\n- Improve log rule by @zzzgydi\n\n- Theme mode support follows system by @zzzgydi\n\n- Improve yaml file error log by @zzzgydi\n\n- Save proxy page state by @zzzgydi\n\n- Light mode wip (#96) by @ctaoist\n\n- Clash meta core supports by @zzzgydi\n\n- Script mode by @zzzgydi\n\n- Clash meta core support (wip) by @zzzgydi\n\n- Reduce gpu usage when hidden by @zzzgydi\n\n- Interval update from now field by @zzzgydi\n\n- Adjust theme by @zzzgydi\n\n- Supports more remote headers close #81 by @zzzgydi\n\n- Check the remote profile by @zzzgydi\n\n- Fix typo by tianyoulan\n\n- Remove trailing comma by tianyoulan\n\n- Remove outdated config by tianyoulan\n\n- Windows service mode ui by @zzzgydi\n\n- Add some commands by @zzzgydi\n\n- Windows service mode by @zzzgydi\n\n- Add update interval by @zzzgydi\n\n- Refactor and supports cron tasks by @zzzgydi\n\n- Supports cron update profiles by @zzzgydi\n\n- Optimize traffic graph quadratic curve by @zzzgydi\n\n- Optimize the animation of the traffic graph by @zzzgydi\n\n- System tray add tun mode by @zzzgydi\n\n- Supports change config dir by @zzzgydi\n\n- Add default user agent by @zzzgydi\n\n- Connections page supports filter by @zzzgydi\n\n- Log page supports filter by @zzzgydi\n\n- Optimize delay checker concurrency strategy by @zzzgydi\n\n- Support sort proxy node and custom test url by @zzzgydi\n\n- Handle remote clash config fields by @zzzgydi\n\n- Add text color by @zzzgydi\n\n- Control final tun config by @zzzgydi\n\n- Support css injection by @zzzgydi\n\n- Support theme setting by @zzzgydi\n\n- Add text color by @zzzgydi\n\n- Add theme setting by @zzzgydi\n\n- Enhanced mode supports more fields by @zzzgydi\n\n- Supports edit profile file by @zzzgydi\n\n- Supports silent start by @zzzgydi\n\n- Use crate open by @zzzgydi\n\n- Enhance connections display order by @zzzgydi\n\n- Save global selected by @zzzgydi\n\n- System tray supports system proxy setting by @zzzgydi\n\n- Prevent context menu on Windows close #22 by @zzzgydi\n\n- Create local profile with selected file by @zzzgydi\n\n- Reduce the impact of the enhanced mode by @zzzgydi\n\n- Parse update log by @zzzgydi\n\n- Fill i18n by @zzzgydi\n\n- Dayjs i18n by @zzzgydi\n\n- Connections page simply support by @zzzgydi\n\n- Add wintun.dll by default by @zzzgydi\n\n- Event emit when clash config update by @zzzgydi\n\n- I18n supports by @zzzgydi\n\n- Change open command on linux by @zzzgydi\n\n- Support more options for remote profile by @zzzgydi\n\n- Linux system proxy by @zzzgydi\n\n- Enhance profile status by @zzzgydi\n\n- Menu item refresh enhanced mode by @zzzgydi\n\n- Profile enhanced mode by @zzzgydi\n\n- Profile enhanced ui by @zzzgydi\n\n- Profile item adjust by @zzzgydi\n\n- Enhanced profile (wip) by @zzzgydi\n\n- Edit profile item by @zzzgydi\n\n- Use nanoid by @zzzgydi\n\n- Compatible profile config by @zzzgydi\n\n- Native menu supports by @zzzgydi\n\n- Filter proxy and display type by @zzzgydi\n\n- Use lock fn by @zzzgydi\n\n- Refactor proxy page by @zzzgydi\n\n- Proxy group auto scroll to current by @zzzgydi\n\n- Clash tun mode supports by @zzzgydi\n\n- Use enhanced guard-state by @zzzgydi\n\n- Guard state supports debounce guard by @zzzgydi\n\n- Adjust clash version display by @zzzgydi\n\n- Hide command window by @zzzgydi\n\n- Enhance log data by @zzzgydi\n\n- Change window style by @zzzgydi\n\n- Fill verge template by @zzzgydi\n\n- Enable customize guard duration by @zzzgydi\n\n- System proxy guard by @zzzgydi\n\n- Enable show or hide traffic graph by @zzzgydi\n\n- Traffic line graph by @zzzgydi\n\n- Adjust profile item ui by @zzzgydi\n\n- Adjust fetch profile url by @zzzgydi\n\n- Inline config file template by @zzzgydi\n\n- Kill sidecars when update app by @zzzgydi\n\n- Delete file by @zzzgydi\n\n- Lock some async functions by @zzzgydi\n\n- Support open dir by @zzzgydi\n\n- Change allow list by @zzzgydi\n\n- Support check delay by @zzzgydi\n\n- Scroll to proxy item by @zzzgydi\n\n- Edit system proxy bypass by @zzzgydi\n\n- Disable user select by @zzzgydi\n\n- New profile able to edit name and desc by @zzzgydi\n\n- Update tauri version by @zzzgydi\n\n- Display clash core version by @zzzgydi\n\n- Adjust profile item menu by @zzzgydi\n\n- Profile item ui by @zzzgydi\n\n- Support new profile by @zzzgydi\n\n- Support open command for viewing by @zzzgydi\n\n- Global proxies use virtual list by @zzzgydi\n\n- Enable change proxy mode by @zzzgydi\n\n- Update styles by @zzzgydi\n\n- Manage clash mode by @zzzgydi\n\n- Change system porxy when changed port by @zzzgydi\n\n- Enable change mixed port by @zzzgydi\n\n- Manage clash config by @zzzgydi\n\n- Enable update clash info by @zzzgydi\n\n- Rename edit as view by @zzzgydi\n\n- Test auto gen update.json ci by @zzzgydi\n\n- Adjust setting typography by @zzzgydi\n\n- Enable force select profile by @zzzgydi\n\n- Support edit profile item by @zzzgydi\n\n- Adjust control ui by @zzzgydi\n\n- Update profile supports noproxy by @zzzgydi\n\n- Rename page by @zzzgydi\n\n- Refactor and adjust ui by @zzzgydi\n\n- Rm some commands by @zzzgydi\n\n- Change type by @zzzgydi\n\n- Supports auto launch on macos and windows by @zzzgydi\n\n- Adjust proxy page by @zzzgydi\n\n- Press esc hide the window by @zzzgydi\n\n- Show system proxy info by @zzzgydi\n\n- Support blur window by @zzzgydi\n\n- Windows support startup by @zzzgydi\n\n- Window self startup by @zzzgydi\n\n- Use tauri updater by @zzzgydi\n\n- Support update checker by @zzzgydi\n\n- Support macos proxy config by @zzzgydi\n\n- Custom window decorations by @zzzgydi\n\n- Profiles add menu and delete button by @zzzgydi\n\n- Delay put profiles and retry by @zzzgydi\n\n- Window Send and Sync by @zzzgydi\n\n- Support restart sidecar tray event by @zzzgydi\n\n- Prevent click same by @zzzgydi\n\n- Scroller stable by @zzzgydi\n\n- Compatible with macos(wip) by @zzzgydi\n\n- Record selected proxy by @zzzgydi\n\n- Display version by @zzzgydi\n\n- Enhance system proxy setting by @zzzgydi\n\n- Profile loading animation by @zzzgydi\n\n- Github actions support by @zzzgydi\n\n- Rename profile page by @zzzgydi\n\n- Add pre-dev script by @zzzgydi\n\n- Implement a simple singleton process by @zzzgydi\n\n- Use paper for list bg by @zzzgydi\n\n- Supprt log ui by @zzzgydi\n\n- Auto update profiles by @zzzgydi\n\n- Proxy page use swr by @zzzgydi\n\n- Profile item support display updated time by @zzzgydi\n\n- Change the log level order by @zzzgydi\n\n- Only put some fields by @zzzgydi\n\n- Setting page by @zzzgydi\n\n- Add serval commands by @zzzgydi\n\n- Change log file format by @zzzgydi\n\n- Adjust code by @zzzgydi\n\n- Refactor commands and support update profile by @zzzgydi\n\n- System proxy command demo by @zzzgydi\n\n- Support set system proxy command by @zzzgydi\n\n- Profiles ui and put profile support by @zzzgydi\n\n- Remove sec field by @zzzgydi\n\n- Put profile works by @zzzgydi\n\n- Distinguish level notice by @zzzgydi\n\n- Add use-notice hook by @zzzgydi\n\n- Pus_clash_profile support `secret` field by @zzzgydi\n\n- Add put_profiles cmd by @zzzgydi\n\n- Update rule page by @zzzgydi\n\n- Use external controller field by @zzzgydi\n\n- Lock profiles file and support more cmds by @zzzgydi\n\n- Put new profile to clash by default by @zzzgydi\n\n- Enhance clash caller & support more commands by @zzzgydi\n\n- Read clash config by @zzzgydi\n\n- Get profile file name from response by @zzzgydi\n\n- Change the naming strategy by @zzzgydi\n\n- Change rule page by @zzzgydi\n\n- Import profile support by @zzzgydi\n\n- Init verge config struct by @zzzgydi\n\n- Add some clash api by @zzzgydi\n\n- Optimize the proxy group order by @zzzgydi\n\n- Refactor system proxy config by @zzzgydi\n\n- Use resources dir to save files by @zzzgydi\n\n- New setting page by @zzzgydi\n\n- Sort groups by @zzzgydi\n\n- Add favicon by @zzzgydi\n\n- Update icons by @zzzgydi\n\n- Update layout style by @zzzgydi\n\n- Support dark mode by @zzzgydi\n\n- Set min windows by @zzzgydi\n\n- Finish some features by @zzzgydi\n\n- Finish main layout by @zzzgydi\n\n- Use vite by @zzzgydi\n\n### 🐛 Bug Fixes\n\n- **icon:** Change ico file to fix windows tray by @zzzgydi\n\n- **macos:** Set auto launch path to application by @zzzgydi\n\n- **style:** Reduce my by @zzzgydi\n\n- Rust lint by @keiko233\n\n- Valid with unified-delay & tcp-concurrent by @keiko233\n\n- Touchpad scrolling causes blank area to appear by @keiko233\n\n- Typos by @keiko233\n\n- Download clash core from backup repo by @keiko233\n\n- Use meta Country.mmdb by @keiko233\n\n- I18n by @zzzgydi\n\n- Fix page undefined exception, close #770 by @zzzgydi\n\n- Set min window size, close #734 by @zzzgydi\n\n- Rm debug code by @zzzgydi\n\n- Use sudo when pkexec not found by @zzzgydi\n\n- Remove div by @zzzgydi\n\n- List key by @zzzgydi\n\n- Websocket disconnect when window focus by @zzzgydi\n\n- Try fix undefined error by @zzzgydi\n\n- Blurry tray icon in Windows by @zzzgydi\n\n- Enable context menu in editable element by @zzzgydi\n\n- Save window size and pos in Windows by @zzzgydi\n\n- Optimize traffic graph high CPU usage when hidden by @zzzgydi\n\n- Remove fallback group select status, close #659 by @zzzgydi\n\n- Error boundary with key by @zzzgydi\n\n- Connections is null by @zzzgydi\n\n- Font family not works in some interfaces, close #639 by @zzzgydi\n\n- EncodeURIComponent secret by @zzzgydi\n\n- Encode controller secret, close #601 by @zzzgydi\n\n- Linux not change icon by @zzzgydi\n\n- Try fix blank error by @zzzgydi\n\n- Close all connections when change mode by @zzzgydi\n\n- Macos not change icon by @zzzgydi\n\n- Error message null by @zzzgydi\n\n- Profile data undefined error, close #566 by @zzzgydi\n\n- Import url error (#543) by @yettera765\n\n- Linux DEFAULT_BYPASS (#503) by @Mr-Spade\n\n- Open file with vscode by @zzzgydi\n\n- Do not render div as a descendant of p (#494) by @tatiustaitus\n\n- Use replace instead by @zzzgydi\n\n- Escape path space by @zzzgydi\n\n- Escape the space in path (#451) by @dyxushuai\n\n- Add target os linux by @zzzgydi\n\n- Appimage path unwrap panic by @zzzgydi\n\n- Remove esc key listener in macOS by @zzzgydi\n\n- Adjust style by @zzzgydi\n\n- Adjust swr option by @zzzgydi\n\n- Infinite retry when websocket error by @zzzgydi\n\n- Type error by @zzzgydi\n\n- Do not parse log except the clash core by @zzzgydi\n\n- Field sort for filter by @zzzgydi\n\n- Add meta fields by @zzzgydi\n\n- Runtime config user select by @zzzgydi\n\n- App_handle as_ref by @zzzgydi\n\n- Use crate by @zzzgydi\n\n- Appimage auto launch, close #403 by @zzzgydi\n\n- Compatible with UTF8 BOM, close #283 by @zzzgydi\n\n- Use selected proxy after profile changed by @zzzgydi\n\n- Error log by @zzzgydi\n\n- Adjust fields order by @zzzgydi\n\n- Add meta fields by @zzzgydi\n\n- Add os platform value by @zzzgydi\n\n- Reconnect traffic websocket by @zzzgydi\n\n- Parse bytes precision, close #334 by @zzzgydi\n\n- Trigger new profile dialog, close #356 by @zzzgydi\n\n- Parse log cause panic by @zzzgydi\n\n- Avoid setting login item repeatedly, close #326 by @zzzgydi\n\n- Adjust code by @zzzgydi\n\n- Adjust delay check concurrency by @zzzgydi\n\n- Change default column to auto by @zzzgydi\n\n- Change default app version by @zzzgydi\n\n- Adjust rule ui by @zzzgydi\n\n- Adjust log ui by @zzzgydi\n\n- Keep delay data by @zzzgydi\n\n- Use list item button by @zzzgydi\n\n- Proxy item style by @zzzgydi\n\n- Virtuoso no work in legacy browsers (#318) by @moeshin\n\n- Adjust ui by @zzzgydi\n\n- Refresh websocket by @zzzgydi\n\n- Adjust ui by @zzzgydi\n\n- Parse bytes base 1024 by @zzzgydi\n\n- Add clash fields by @zzzgydi\n\n- Direct mode hide proxies by @zzzgydi\n\n- Profile can not edit by @zzzgydi\n\n- Parse logger time by @zzzgydi\n\n- Adjust service mode ui by @zzzgydi\n\n- Adjust style by @zzzgydi\n\n- Check hotkey and optimize hotkey input, close #287 by @zzzgydi\n\n- Mutex dead lock by @zzzgydi\n\n- Adjust item ui by @zzzgydi\n\n- Regenerate config before change core by @zzzgydi\n\n- Close connections when profile change by @zzzgydi\n\n- Lint by @zzzgydi\n\n- Windows service mode by @zzzgydi\n\n- Init config file by @zzzgydi\n\n- Service mode error and fallback to sidecar by @zzzgydi\n\n- Service mode viewer ui by @zzzgydi\n\n- Create theme error, close #294 by @zzzgydi\n\n- MatchMedia().addEventListener #258 (#296) by @moeshin\n\n- Check config by @zzzgydi\n\n- Show global when no rule groups by @zzzgydi\n\n- Service viewer ref by @zzzgydi\n\n- Service ref error by @zzzgydi\n\n- Group proxies render list is null by @zzzgydi\n\n- Pretty bytes by @zzzgydi\n\n- Use verge hook by @zzzgydi\n\n- Adjust notice by @zzzgydi\n\n- Windows issue by @zzzgydi\n\n- Change dev log level by @zzzgydi\n\n- Patch clash config by @zzzgydi\n\n- Cmds params by @zzzgydi\n\n- Adjust singleton detect by @zzzgydi\n\n- Change template by @zzzgydi\n\n- Copy resource file by @zzzgydi\n\n- MediaQueryList addEventListener polyfill by @zzzgydi\n\n- Change default tun dns-hijack by @zzzgydi\n\n- Something by @zzzgydi\n\n- Provider proxy sort by delay by @zzzgydi\n\n- Profile item menu ui dense by @zzzgydi\n\n- Disable auto scroll to proxy by @zzzgydi\n\n- Check remote profile by @zzzgydi\n\n- Remove smoother by @zzzgydi\n\n- Icon button color by @zzzgydi\n\n- Init system proxy correctly by @zzzgydi\n\n- Open file by @zzzgydi\n\n- Reset proxy by @zzzgydi\n\n- Init config error by @zzzgydi\n\n- Adjust reset proxy by @zzzgydi\n\n- Adjust code by @zzzgydi\n\n- Add https proxy by @zzzgydi\n\n- Auto scroll into view when sorted proxies changed by @zzzgydi\n\n- Refresh proxies interval, close #235 by @zzzgydi\n\n- Style by @zzzgydi\n\n- Fetch profile with system proxy, close #249 by @zzzgydi\n\n- The profile is replaced when the request fails. (#246) by @loosheng\n\n- Default dns config by @zzzgydi\n\n- Kill clash when exit in service mode, close #241 by @zzzgydi\n\n- Icon button color inherit by @zzzgydi\n\n- App version to string by @zzzgydi\n\n- Break loop when core terminated by @zzzgydi\n\n- Api error handle by @zzzgydi\n\n- Clash meta not load geoip, close #212 by @zzzgydi\n\n- Sort proxy during loading, close #221 by @zzzgydi\n\n- Not create windows when enable slient start by @zzzgydi\n\n- Root background color by @zzzgydi\n\n- Create window correctly by @zzzgydi\n\n- Set_activation_policy by @zzzgydi\n\n- Disable spell check by @zzzgydi\n\n- Adjust init launch on dev by @zzzgydi\n\n- Ignore disable auto launch error by @zzzgydi\n\n- I18n by @zzzgydi\n\n- Style by @zzzgydi\n\n- Save enable log on localstorage by @zzzgydi\n\n- Typo in api.ts (#207) by @Priestch\n\n- Refresh clash ui await patch by @zzzgydi\n\n- Remove dead code by @zzzgydi\n\n- Style by @zzzgydi\n\n- Handle is none by @zzzgydi\n\n- Unused by @zzzgydi\n\n- Style by @zzzgydi\n\n- Windows logo size by @zzzgydi\n\n- Do not kill sidecar during updating by @zzzgydi\n\n- Delay update config by @zzzgydi\n\n- Reduce logo size by @zzzgydi\n\n- Window center by @zzzgydi\n\n- Log level warn value by @zzzgydi\n\n- Increase delay checker concurrency by @zzzgydi\n\n- External controller allow lan by @zzzgydi\n\n- Remove useless optimizations by @zzzgydi\n\n- Reduce unsafe unwrap by @zzzgydi\n\n- Timer restore at app launch by @FoundTheWOUT\n\n- Adjust log text by @zzzgydi\n\n- Only script profile can display console by @zzzgydi\n\n- Fill button title attr by @zzzgydi\n\n- Do not reset system proxy when consistent by @zzzgydi\n\n- Adjust web ui item style by @zzzgydi\n\n- Clash field state error by @zzzgydi\n\n- Badge color error by @zzzgydi\n\n- Web ui port value error by @zzzgydi\n\n- Delay show window by @zzzgydi\n\n- Adjust dialog action button variant by @zzzgydi\n\n- Script code error by @zzzgydi\n\n- Script exception handle by @zzzgydi\n\n- Change fields by @zzzgydi\n\n- Silent start (#150) by @FoundTheWOUT\n\n- Save profile when update by @zzzgydi\n\n- List compare wrong by @zzzgydi\n\n- Button color by @zzzgydi\n\n- Limit theme mode value by @zzzgydi\n\n- Add valid clash field by @zzzgydi\n\n- Icon style by @zzzgydi\n\n- Reduce unwrap by @zzzgydi\n\n- Import mod by @zzzgydi\n\n- Add tray separator by @zzzgydi\n\n- Instantiate core after init app, close #122 by @zzzgydi\n\n- Rm macOS transition props by @zzzgydi\n\n- Improve external-controller parse and log by @zzzgydi\n\n- Show windows on click by @zzzgydi\n\n- Adjust update profile notice error by @zzzgydi\n\n- Style issue on mac by @zzzgydi\n\n- Check script run on all OS by @FoundTheWOUT\n\n- MacOS disable transparent by @zzzgydi\n\n- Window transparent and can not get hwnd by @zzzgydi\n\n- Create main window by @zzzgydi\n\n- Adjust notice by @zzzgydi\n\n- Label text by @zzzgydi\n\n- Icon path by @zzzgydi\n\n- Icon issue by @zzzgydi\n\n- Notice ui blocking by @zzzgydi\n\n- Service mode error by @zzzgydi\n\n- Win11 drag lag by @zzzgydi\n\n- Rm unwrap by @zzzgydi\n\n- Edit profile info by @zzzgydi\n\n- Change window default size by @zzzgydi\n\n- Change service installer and uninstaller by @zzzgydi\n\n- Adjust connection scroll by @zzzgydi\n\n- Adjust something by @zzzgydi\n\n- Adjust debounce wait time by @zzzgydi\n\n- Adjust dns config by @zzzgydi\n\n- Traffic graph adapt to different fps by @zzzgydi\n\n- Optimize clash launch by @zzzgydi\n\n- Reset after exit by @zzzgydi\n\n- Adjust code by @zzzgydi\n\n- Adjust log by @zzzgydi\n\n- Check button hover style by @zzzgydi\n\n- Icon button color inherit by @zzzgydi\n\n- Remove the lonely zero by @zzzgydi\n\n- I18n add value by @zzzgydi\n\n- Proxy page first render by @zzzgydi\n\n- Console warning by @zzzgydi\n\n- Icon button title by @zzzgydi\n\n- MacOS transition flickers close #47 by @zzzgydi\n\n- Csp image data by @zzzgydi\n\n- Close dialog after save by @zzzgydi\n\n- Change to deep copy by @zzzgydi\n\n- Window style close #45 by @zzzgydi\n\n- Manage global proxy correctly by @zzzgydi\n\n- Tauri csp by @zzzgydi\n\n- Windows style by @zzzgydi\n\n- Update state by @zzzgydi\n\n- Profile item loading state by @zzzgydi\n\n- Adjust windows style by @zzzgydi\n\n- Change mixed port error by @zzzgydi\n\n- Auto launch path by @zzzgydi\n\n- Tun mode config by @zzzgydi\n\n- Adjsut open cmd error by @zzzgydi\n\n- Parse external-controller by @zzzgydi\n\n- Config file case close #18 by @zzzgydi\n\n- Patch item option by @zzzgydi\n\n- User agent not works by @zzzgydi\n\n- External-controller by @zzzgydi\n\n- Change proxy bypass on mac by @zzzgydi\n\n- Kill sidecars after install still in test by @zzzgydi\n\n- Log some error by @zzzgydi\n\n- Apply_blur parameter by @zzzgydi\n\n- Limit enhanced profile range by @zzzgydi\n\n- Profile updated field by @zzzgydi\n\n- Profile field check by @zzzgydi\n\n- Create dir panic by @zzzgydi\n\n- Only error when selected by @zzzgydi\n\n- Enhanced profile consistency by @zzzgydi\n\n- Simply compatible with proxy providers by @zzzgydi\n\n- Component warning by @zzzgydi\n\n- When updater failed by @zzzgydi\n\n- Log file by @zzzgydi\n\n- Result by @zzzgydi\n\n- Cover profile extra by @zzzgydi\n\n- Display menu only on macos by @zzzgydi\n\n- Proxy global showType by @zzzgydi\n\n- Use full clash config by @zzzgydi\n\n- Reconnect websocket when restart clash by @zzzgydi\n\n- Wrong exe path by @zzzgydi\n\n- Patch verge config by @zzzgydi\n\n- Fetch profile panic by @zzzgydi\n\n- Spawn command by @zzzgydi\n\n- Import error by @zzzgydi\n\n- Not open file when new profile by @zzzgydi\n\n- Reset value correctly by @zzzgydi\n\n- Something by @zzzgydi\n\n- Menu without fragment by @zzzgydi\n\n- Proxy list error by @zzzgydi\n\n- Something by @zzzgydi\n\n- Macos auto launch fail by @zzzgydi\n\n- Type error by @zzzgydi\n\n- Restart clash should update something by @zzzgydi\n\n- Script error... by @zzzgydi\n\n- Tag error by @zzzgydi\n\n- Script error by @zzzgydi\n\n- Remove cargo test by @zzzgydi\n\n- Reduce proxy item height by @zzzgydi\n\n- Put profile request with no proxy by @zzzgydi\n\n- Ci strategy by @zzzgydi\n\n- Version update error by @zzzgydi\n\n- Text by @zzzgydi\n\n- Update profile after restart clash by @zzzgydi\n\n- Get proxies multiple times by @zzzgydi\n\n- Delete profile item command by @zzzgydi\n\n- Initialize profiles state by @zzzgydi\n\n- Item header bgcolor by @zzzgydi\n\n- Null type error by @zzzgydi\n\n- Api loading delay by @zzzgydi\n\n- Mutate at the same time may be wrong by @zzzgydi\n\n- Port value not rerender by @zzzgydi\n\n- Change log file format by @zzzgydi\n\n- Proxy bypass add <local> by @zzzgydi\n\n- Sidecar dir by @zzzgydi\n\n- Web resource outDir by @zzzgydi\n\n- Use io by @zzzgydi\n\n### 💅 Styling\n\n- Resolve formatting problem by @Limsanity\n\n### 📚 Documentation\n\n- Fix img width by @zzzgydi\n\n- Update by @zzzgydi\n\n### 🔨 Refactor\n\n- **hotkey:** Use tauri global shortcut by @zzzgydi\n\n- Copy_clash_env by @keiko233\n\n- Adjust base components export by @zzzgydi\n\n- Adjust setting dialog component by @zzzgydi\n\n- Done by @zzzgydi\n\n- Adjust all path methods and reduce unwrap by @zzzgydi\n\n- Rm code by @zzzgydi\n\n- Fix by @zzzgydi\n\n- Rm dead code by @zzzgydi\n\n- For windows by @zzzgydi\n\n- Wip by @zzzgydi\n\n- Wip by @zzzgydi\n\n- Wip by @zzzgydi\n\n- Rm update item block_on by @zzzgydi\n\n- Fix by @zzzgydi\n\n- Fix by @zzzgydi\n\n- Wip by @zzzgydi\n\n- Optimize by @zzzgydi\n\n- Ts path alias by @zzzgydi\n\n- Mode manage on tray by @zzzgydi\n\n- Verge by @zzzgydi\n\n- Wip by @zzzgydi\n\n- Mutex by @zzzgydi\n\n- Wip by @zzzgydi\n\n- Proxy head by @zzzgydi\n\n- Update profile menu by @zzzgydi\n\n- Enhanced mode ui component by @zzzgydi\n\n- Ui theme by @zzzgydi\n\n- Optimize enhance mode strategy by @zzzgydi\n\n- Profile config by @zzzgydi\n\n- Use anyhow to handle error by @zzzgydi\n\n- Rename profiles & command state by @zzzgydi\n\n- Something by @zzzgydi\n\n- Notice caller by @zzzgydi\n\n- Setting page by @zzzgydi\n\n- Rename by @zzzgydi\n\n- Impl structs methods by @zzzgydi\n\n- Impl as struct methods by @zzzgydi\n\n- Api and command by @zzzgydi\n\n- Import profile by @zzzgydi\n\n- Adjust dirs structure by @zzzgydi\n\n---\n\n## New Contributors\n\n- @zzzgydi made their first contribution\n- @whitemirror33 made their first contribution\n- @shvchk made their first contribution\n- @w568w made their first contribution\n- @yettera765 made their first contribution\n- @tatiustaitus made their first contribution\n- @Mr-Spade made their first contribution\n- @solancer made their first contribution\n- @me1ting made their first contribution\n- @boatrainlsz made their first contribution\n- @inRm3D made their first contribution\n- @moeshin made their first contribution\n- @angryLid made their first contribution\n- @loosheng made their first contribution\n- @ParticleG made their first contribution\n- @HougeLangley made their first contribution\n- @Priestch made their first contribution\n- @riverscn made their first contribution\n- @FoundTheWOUT made their first contribution\n- @Limsanity made their first contribution\n- @ctaoist made their first contribution\n- @ made their first contribution\n- @ttys3 made their first contribution\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nWe as members, contributors, and leaders pledge to make participation in our\ncommunity a harassment-free experience for everyone, regardless of age, body\nsize, visible or invisible disability, ethnicity, sex characteristics, gender\nidentity and expression, level of experience, education, socio-economic status,\nnationality, personal appearance, race, religion, or sexual identity\nand orientation.\n\nWe pledge to act and interact in ways that contribute to an open, welcoming,\ndiverse, inclusive, and healthy community.\n\n## Our Standards\n\nExamples of behavior that contributes to a positive environment for our\ncommunity include:\n\n- Demonstrating empathy and kindness toward other people\n- Being respectful of differing opinions, viewpoints, and experiences\n- Giving and gracefully accepting constructive feedback\n- Accepting responsibility and apologizing to those affected by our mistakes,\n  and learning from the experience\n- Focusing on what is best not just for us as individuals, but for the\n  overall community\n\nExamples of unacceptable behavior include:\n\n- The use of sexualized language or imagery, and sexual attention or\n  advances of any kind\n- Trolling, insulting or derogatory comments, and personal or political attacks\n- Public or private harassment\n- Publishing others' private information, such as a physical or email\n  address, without their explicit permission\n- Other conduct which could reasonably be considered inappropriate in a\n  professional setting\n\n## Enforcement Responsibilities\n\nCommunity leaders are responsible for clarifying and enforcing our standards of\nacceptable behavior and will take appropriate and fair corrective action in\nresponse to any behavior that they deem inappropriate, threatening, offensive,\nor harmful.\n\nCommunity leaders have the right and responsibility to remove, edit, or reject\ncomments, commits, code, wiki edits, issues, and other contributions that are\nnot aligned to this Code of Conduct, and will communicate reasons for moderation\ndecisions when appropriate.\n\n## Scope\n\nThis Code of Conduct applies within all community spaces, and also applies when\nan individual is officially representing the community in public spaces.\nExamples of representing our community include using an official e-mail address,\nposting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported to the community leaders responsible for enforcement at\ni@elaina.moe.\nAll complaints will be reviewed and investigated promptly and fairly.\n\nAll community leaders are obligated to respect the privacy and security of the\nreporter of any incident.\n\n## Enforcement Guidelines\n\nCommunity leaders will follow these Community Impact Guidelines in determining\nthe consequences for any action they deem in violation of this Code of Conduct:\n\n### 1. Correction\n\n**Community Impact**: Use of inappropriate language or other behavior deemed\nunprofessional or unwelcome in the community.\n\n**Consequence**: A private, written warning from community leaders, providing\nclarity around the nature of the violation and an explanation of why the\nbehavior was inappropriate. A public apology may be requested.\n\n### 2. Warning\n\n**Community Impact**: A violation through a single incident or series\nof actions.\n\n**Consequence**: A warning with consequences for continued behavior. No\ninteraction with the people involved, including unsolicited interaction with\nthose enforcing the Code of Conduct, for a specified period of time. This\nincludes avoiding interactions in community spaces as well as external channels\nlike social media. Violating these terms may lead to a temporary or\npermanent ban.\n\n### 3. Temporary Ban\n\n**Community Impact**: A serious violation of community standards, including\nsustained inappropriate behavior.\n\n**Consequence**: A temporary ban from any sort of interaction or public\ncommunication with the community for a specified period of time. No public or\nprivate interaction with the people involved, including unsolicited interaction\nwith those enforcing the Code of Conduct, is allowed during this period.\nViolating these terms may lead to a permanent ban.\n\n### 4. Permanent Ban\n\n**Community Impact**: Demonstrating a pattern of violation of community\nstandards, including sustained inappropriate behavior, harassment of an\nindividual, or aggression toward or disparagement of classes of individuals.\n\n**Consequence**: A permanent ban from any sort of public interaction within\nthe community.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage],\nversion 2.0, available at\nhttps://www.contributor-covenant.org/version/2/0/code_of_conduct.html.\n\nCommunity Impact Guidelines were inspired by [Mozilla's code of conduct\nenforcement ladder](https://github.com/mozilla/diversity).\n\n[homepage]: https://www.contributor-covenant.org\n\nFor answers to common questions about this code of conduct, see the FAQ at\nhttps://www.contributor-covenant.org/faq. Translations are available at\nhttps://www.contributor-covenant.org/translations.\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to Nyanpasu\n\nWelcome to **Nyanpasu** development!  \nTo ensure the quality and stability of the project, please read this guide carefully. Even if you are new, you can follow these steps to set up the development environment, write code, and submit contributions.\n\n---\n\n## 1. Development Guidelines\n\nBefore submitting code, please follow these rules:\n\n### 1. Code Style Checks\n\n| Language                | Tools                       |\n| ----------------------- | --------------------------- |\n| JavaScript / TypeScript | ESLint, Prettier, Stylelint |\n| Rust                    | Clippy, Rustfmt             |\n\n- ⚠️ **Ensure there are no style errors before committing**\n- ❌ **Do not use `git commit -n` or skip checks**, CI will automatically enforce style validation\n\n### 2. Submission Requirements\n\n- Avoid submitting useless code, files, or folders\n- For major refactors or new features, open an **Issue** first for discussion\n- If unsure about implementation or have questions, communicate in **Issue** or **PR**\n\n### 3. Communication & Collaboration\n\n- Respect others' code and opinions\n- Keep commit messages and PR descriptions clear\n- All discussions should be on GitHub for transparency and traceability\n\n---\n\n## 2. Environment Requirements\n\nTo ensure the project runs correctly locally, the following dependencies are required.\n\n### 1. Required Dependencies\n\n| Tool    | Version  | Link                                                        | Notes                                         |\n| ------- | -------- | ----------------------------------------------------------- | --------------------------------------------- |\n| Rust    | ≥ 1.78   | [Official Install](https://www.rust-lang.org/tools/install) | Stable version; use MSVC toolchain on Windows |\n| Node.js | ≥ 20 LTS | [Official Site](https://nodejs.org/)                        | Install LTS or Latest version                 |\n| pnpm    | ≥ 9      | [Official Documentation](https://pnpm.io/)                  | Node.js package manager                       |\n| git     | Latest   | [Official Site](https://git-scm.com/)                       | Version control                               |\n\n### 2. Build Dependencies\n\n| Tool  | Link                                                                              | Notes                               |\n| ----- | --------------------------------------------------------------------------------- | ----------------------------------- |\n| cmake | [Official Site](https://cmake.org/)                                               | Required by `zip` crate             |\n| llvm  | [Official Site](https://llvm.org/)                                                | Required by `rquickjs` or `rocksdb` |\n| patch | [Windows Installation Guide](https://gnuwin32.sourceforge.net/packages/patch.htm) | Required by `rquickjs`              |\n\n### 3. Windows Special Requirements\n\n- Use **Administrator privileges** when opening the project for the first time; `patch` requires admin rights\n- Recommended to install `gsudo` (via `scoop`, `choco`, or `winget`)\n- Always use the **MSVC toolchain** on Windows\n- 💡 Admin privileges are only needed for initial setup; normal terminal is fine for daily development\n\n---\n\n## 3. Pre-Development Setup\n\nBefore starting development, initialize the environment and download required resources.\n\n### 1. Install Frontend Dependencies\n\n```bash\npnpm i\n```\n\n> This installs all frontend dependencies including UI components, toolchains, and testing tools.\n\n### 2. Download Core & Resource Files\n\n```\npnpm prepare:check\n```\n\n> This command downloads binaries like `sidecar` and `resource` to ensure the project runs properly\n\nIf files are missing or you want to force update:\n\n```\npnpm prepare:check --force\n```\n\n💡 **Tip**: Configure terminal proxy if network issues occur\n\n---\n\n## 4. Start Development Environment\n\nThe project provides two types of development instances:\n\n### 1. Dedicated Development Instance (Recommended)\n\n```\npnpm dev:diff\n```\n\n> Suitable for daily development and debugging; changes do not affect the release version\n\n### 2. Release-Like Development Instance\n\n```\npnpm dev\n```\n\n> Behaves similarly to the official release; useful to test overall functionality\n\n---\n\n## 5. Commit Code & Create PR\n\n### 1. Pull Latest Code\n\n```\ngit pull origin main\n```\n\n### 2. Create a New Branch\n\n```\ngit checkout -b feature/my-feature\n```\n\n> ⚠️ Avoid developing directly on `main`\n\n### 3. Pre-Commit Checks\n\n- Ensure code style is correct\n- All unit tests pass\n- No useless files\n\n### 4. Commit and Push\n\n```\ngit add .\ngit commit -m \"feat: add my feature\"\ngit push origin feature/my-feature\n```\n\n### 5. Create a PR\n\n- Choose `main` as the target branch\n- Briefly describe the feature or changes\n- Link related Issue if available\n\n---\n\n💡 **Tips**:\n\n- Keep each commit focused on a single feature or issue; avoid large, messy commits\n- PR descriptions should be clear so reviewers immediately understand the changes\n"
  },
  {
    "path": "LICENSE",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU General Public License is a free, copyleft license for\nsoftware and other kinds of works.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nthe GNU General Public License is intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.  We, the Free Software Foundation, use the\nGNU General Public License for most of our software; it applies also to\nany other work released this way by its authors.  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  To protect your rights, we need to prevent others from denying you\nthese rights or asking you to surrender the rights.  Therefore, you have\ncertain responsibilities if you distribute copies of the software, or if\nyou modify it: responsibilities to respect the freedom of others.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must pass on to the recipients the same\nfreedoms that you received.  You must make sure that they, too, receive\nor can get the source code.  And you must show them these terms so they\nknow their rights.\n\n  Developers that use the GNU GPL protect your rights with two steps:\n(1) assert copyright on the software, and (2) offer you this License\ngiving you legal permission to copy, distribute and/or modify it.\n\n  For the developers' and authors' protection, the GPL clearly explains\nthat there is no warranty for this free software.  For both users' and\nauthors' sake, the GPL requires that modified versions be marked as\nchanged, so that their problems will not be attributed erroneously to\nauthors of previous versions.\n\n  Some devices are designed to deny users access to install or run\nmodified versions of the software inside them, although the manufacturer\ncan do so.  This is fundamentally incompatible with the aim of\nprotecting users' freedom to change the software.  The systematic\npattern of such abuse occurs in the area of products for individuals to\nuse, which is precisely where it is most unacceptable.  Therefore, we\nhave designed this version of the GPL to prohibit the practice for those\nproducts.  If such problems arise substantially in other domains, we\nstand ready to extend this provision to those domains in future versions\nof the GPL, as needed to protect the freedom of users.\n\n  Finally, every program is threatened constantly by software patents.\nStates should not allow patents to restrict development and use of\nsoftware on general-purpose computers, but in those that do, we wish to\navoid the special danger that patents applied to a free program could\nmake it effectively proprietary.  To prevent this, the GPL assures that\npatents cannot be used to render the program non-free.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Use with the GNU Affero General Public License.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU Affero General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the special requirements of the GNU Affero General Public License,\nsection 13, concerning interaction through a network will apply to the\ncombination as such.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If the program does terminal interaction, make it output a short\nnotice like this when it starts in an interactive mode:\n\n    <program>  Copyright (C) <year>  <name of author>\n    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, your program's commands\nmight be different; for a GUI interface, you would use an \"about box\".\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU GPL, see\n<https://www.gnu.org/licenses/>.\n\n  The GNU General Public License does not permit incorporating your program\ninto proprietary programs.  If your program is a subroutine library, you\nmay consider it more useful to permit linking proprietary applications with\nthe library.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.  But first, please read\n<https://www.gnu.org/licenses/why-not-lgpl.html>.\n"
  },
  {
    "path": "README.md",
    "content": "<h1 align=\"center\">\n  <img src=\"https://nyanpasu.elaina.moe/images/banner/nyanpasu_banner.png\" alt=\"Clash Nyanpasu Banner\" />\n</h1>\n\n<h3>Clash Nyanpasu</h3>\n\n<h3>\n  A <a href=\"https://github.com/Dreamacro/clash\">Clash</a> GUI based on <a href=\"https://github.com/tauri-apps/tauri\">Tauri</a>.\n</h3>\n\n<p>\n  <a href=\"https://github.com/libnyanpasu/clash-nyanpasu/releases/latest\"><img src=\"https://img.shields.io/github/v/release/libnyanpasu/clash-nyanpasu?style=flat-square\" alt=\"Nyanpasu Release\" /></a>\n  <a href=\"https://github.com/libnyanpasu/clash-nyanpasu/releases/pre-release\"><img src=\"https://img.shields.io/github/actions/workflow/status/libnyanpasu/clash-nyanpasu/target-dev-build.yaml?style=flat-square\" alt=\"Dev Build Status\" /></a>\n  <a href=\"https://github.com/libnyanpasu/clash-nyanpasu/stargazers\"><img src=\"https://img.shields.io/github/stars/libnyanpasu/clash-nyanpasu?style=flat-square\" alt=\"Nyanpasu Stars\" /></a>\n  <a href=\"https://github.com/libnyanpasu/clash-nyanpasu/releases/latest\"><img src=\"https://img.shields.io/github/downloads/libnyanpasu/clash-nyanpasu/total?style=flat-square\" alt=\"GitHub Downloads (all assets, all releases)\" /></a>\n  <a href=\"https://github.com/libnyanpasu/clash-nyanpasu/blob/main/LICENSE\"><img src=\"https://img.shields.io/github/license/libnyanpasu/clash-nyanpasu?style=flat-square\" alt=\"Nyanpasu License\" /></a>\n  <a href=\"https://twitter.com/ClashNyanpasu\"><img src=\"https://img.shields.io/twitter/follow/ClashNyanpasu?style=flat-square\" alt=\"Nyanpasu Twitter\" /></a>\n  <a href=\"https://deepwiki.com/libnyanpasu/clash-nyanpasu\"><img src=\"https://deepwiki.com/badge.svg\" alt=\"Ask DeepWiki\"></a>\n</p>\n\n## Features\n\n- Built-in support [Clash Premium](https://github.com/Dreamacro/clash), [Mihomo](https://github.com/MetaCubeX/mihomo) & [Clash Rust](https://github.com/Watfaq/clash-rs).\n- Profiles management and enhancement (by YAML, JavaScript & Lua). [Doc](https://nyanpasu.elaina.moe/tutorial/proxy-chain)\n- Provider management support.\n- Google Material You Design UI and animation support.\n\n## Preview\n\n![preview-light](https://nyanpasu.elaina.moe/images/screenshot/app-dashboard-light.png)\n\n![preview-dark](https://nyanpasu.elaina.moe/images/screenshot/app-dashboard-dark.png)\n\n## Links\n\n- [Install](https://nyanpasu.elaina.moe/tutorial/install)\n- [FAQ](https://nyanpasu.elaina.moe/others/faq)\n- [Q&A Convention](https://nyanpasu.elaina.moe/others/issues)\n- [How To Ask Questions](https://nyanpasu.elaina.moe/others/how-to-ask)\n\n## Development\n\n### Configure your development environment\n\nYou should install Rust and Node.js, see [here](https://v2.tauri.app/start/prerequisites/) for more details.\n\nClash Nyanpasu uses the pnpm package manager. See [here](https://pnpm.io/installation) for installation instructions. Then, install Node.js packages.\n\n```shell\npnpm i\n```\n\n### Download the Clash binary & other dependencies\n\n```shell\n# force update to latest version\n# pnpm prepare:check --force\n\npnpm prepare:check\n```\n\n### Run dev\n\n```shell\npnpm dev\n\n# run it in another way if app instance exists\npnpm dev:diff\n```\n\n### Build application\n\n```shell\npnpm build\n```\n\n## Contributions\n\nIssue and PR welcome!\n\n## Acknowledgement\n\nClash Nyanpasu was based on or inspired by these projects and so on:\n\n- [zzzgydi/clash-verge](https://github.com/zzzgydi/clash-verge): A Clash GUI based on Tauri. Supports Windows, macOS and Linux.\n- [clash-verge-rev/clash-verge-rev](https://github.com/clash-verge-rev/clash-verge-rev): Another fork of Clash Verge. Some patches are included for bug fixes.\n- [tauri-apps/tauri](https://github.com/tauri-apps/tauri): Build smaller, faster, and more secure desktop applications with a web frontend.\n- [Dreamacro/clash](https://github.com/Dreamacro/clash): A rule-based tunnel in Go.\n- [MetaCubeX/mihomo](https://github.com/MetaCubeX/mihomo): A rule-based tunnel in Go.\n- [Watfaq/clash-rs](https://github.com/Watfaq/clash-rs): A custom protocol, rule based network proxy software.\n- [Fndroid/clash_for_windows_pkg](https://github.com/Fndroid/clash_for_windows_pkg): A Windows/macOS GUI based on Clash.\n- [vitejs/vite](https://github.com/vitejs/vite): Next generation frontend tooling. It's fast!\n- [mui/material-ui](https://github.com/mui/material-ui): Ready-to-use foundational React components, free forever.\n\n## Contributors\n\n![Contributors](https://contrib.rocks/image?repo=libnyanpasu/clash-nyanpasu)\n\n## License\n\nGPL-3.0 License. See [License here](./LICENSE) for details.\n"
  },
  {
    "path": "UPDATELOG.md",
    "content": "## v1.4.2\n\n### Features\n\n- Support Clash-rs v0.1.10. [@greenhat616](https://github.com/greenhat616)\n- New Windows tray icon & support tun mode icon. [@keiko233](https://github.com/keiko233)\n- Support Log file export. [@greenhat616](https://github.com/greenhat616)\n- Hotkey Support toggle. [@greenhat616](https://github.com/greenhat616)\n- Built-in Mihomo alpha. [@greenhat616](https://github.com/greenhat616)\n- Use shikiji process log. [@greenhat616](https://github.com/greenhat616)\n- More Animation support. [@greenhat616](https://github.com/greenhat616)\n- New Built-in updater support. [@greenhat616](https://github.com/greenhat616)\n- Support DNS auto config in macos. [@greenhat616](https://github.com/greenhat616)\n- Support log file auto clean. [@keiko233](https://github.com/keiko233)\n- Use Christmas Logo. [@keiko233](https://github.com/keiko233)\n\n### Bug Fixes\n\n- Fix Windows resize bug. [@greenhat616](https://github.com/greenhat616)\n- Fix Hotkey repeat binding . [@greenhat616](https://github.com/greenhat616)\n- Fix Proxies dalay value rending color. [@greenhat616](https://github.com/greenhat616)\n- Fix Mihomo alpha & Clash-rs Service not working. [@greenhat616](https://github.com/greenhat616)\n- Fix dialog position. [@greenhat616](https://github.com/greenhat616)\n- Fix shikiji color rending err. [@keiko233](https://github.com/keiko233)\n\n### Others\n\n- Use GitHub issues template. [@greenhat616](https://github.com/greenhat616) [@keiko233](https://github.com/keiko233) [@txyyh](https://github.com/txyyh)\n- Support nightly builds. [@greenhat616](https://github.com/greenhat616)\n\n---\n\n## v1.4.1\n\n### Features\n\n- Support macOS aarch64 build. [@keiko233](https://github.com/keiko233)\n- Built-in Windows UWP Loopback Tool. [@Kuingsmile](https://github.com/Kuingsmile)\n- Built-in Clash-rs support. [@greenhat616](https://github.com/greenhat616)\n- Add tooltip for tray. [@Kuingsmile](https://github.com/Kuingsmile)\n- Update HD tray icons. [@keiko233](https://github.com/keiko233)\n- Support random mixed port. [@Kuingsmile](https://github.com/Kuingsmile)\n- Update Clash.Meta to v1.17.0. [@Kuingsmile](https://github.com/Kuingsmile) [@keiko233](https://github.com/keiko233)\n- Use system notification. [@keiko233](https://github.com/keiko233)\n- Support drag profile item to sort. [@Kuingsmile](https://github.com/Kuingsmile)\n- Add skip-auth-prefixes fields for Clash.Meta v1.17.0. [@greenhat616](https://github.com/greenhat616)\n- Support more animations. [@greenhat616](https://github.com/greenhat616)\n- Use twemoji on Windows. [@greenhat616](https://github.com/greenhat616)\n\n### Bug Fixes\n\n- Fix install Service bug. [@Kuingsmile](https://github.com/Kuingsmile)\n- Fix Windows proxy bug when VPN enabled. [@greenhat616](https://github.com/greenhat616)\n\n### Others\n\n- Fixed several build issues. [@keiko233](https://github.com/keiko233)\n- Switch rust code to workspace. [@greenhat616](https://github.com/greenhat616)\n- Add renovate bot support. [@greenhat616](https://github.com/greenhat616)\n\n---\n\n## v1.4.0\n\n### Features\n\n- Default use Meta Core.\n- Support copy PowerShell, CMD and sh env command.\n- Add Upload Traffic, Download Traffic and Active Connections to ConnectionsPage.\n- SettingPage use Grid layout.\n- Import LoadingButton & use when Download Profile.\n- New default theme color.\n- Add Nyanpasu Element. (Logo designer [@ReallySnow](https://github.com/ReallySnow))\n- Default enable unified-delay & tcp-concurrent.\n- Use Meta Country.mmdb.\n- Disable IPv6 by default.\n- Add Material You element.\n- Add Router switch transition.\n\n### Bug Fixes\n\n- Fix touchpad scrolling causes blank area to appear.\n\n---\n\n## v1.3.7\n\n### Features\n\n- update clash and clash meta core\n- profiles page add paste button\n- subscriptions url textfield use multi lines\n- set min window size\n- add check for updates buttons\n- add open dashboard to the hotkey list\n\n### Bug Fixes\n\n- fix profiles page undefined exception\n\n---\n\n## v1.3.6\n\n### Features\n\n- add russian translation\n- support to show connection detail\n- support clash meta memory usage display\n- support proxy provider update ui\n- update geo data file from meta repo\n- adjust setting page\n\n### Bug Fixes\n\n- center the window when it is out of screen\n- use `sudo` when `pkexec` not found (Linux)\n- reconnect websocket when window focus\n\n### Notes\n\n- The current version of the Linux installation package is built by Ubuntu 20.04 (Github Action).\n\n---\n\n## v1.3.5\n\n### Features\n\n- update clash core\n\n### Bug Fixes\n\n- fix blurry system tray icon (Windows)\n- fix v1.3.4 wintun.dll not found (Windows)\n- fix v1.3.4 clash core not found (macOS, Linux)\n\n---\n\n## v1.3.4\n\n### Features\n\n- update clash and clash meta core\n- optimize traffic graph high CPU usage when window hidden\n- use polkit to elevate permission (Linux)\n- support app log level setting\n- support copy environment variable\n- overwrite resource file according to file modified\n- save window size and position\n\n### Bug Fixes\n\n- remove fallback group select status\n- enable context menu on editable element (Windows)\n\n---\n\n## v1.3.3\n\n### Features\n\n- update clash and clash meta core\n- show tray icon variants in different system proxy status (Windows)\n- close all connections when mode changed\n\n### Bug Fixes\n\n- encode controller secret into uri\n- error boundary for each page\n\n---\n\n## v1.3.2\n\n### Features\n\n- update clash and clash meta core\n\n### Bug Fixes\n\n- fix import url issue\n- fix profile undefined issue\n\n---\n\n## v1.3.1\n\n### Features\n\n- update clash and clash meta core\n\n### Bug Fixes\n\n- fix open url issue\n- fix appimage path panic\n- fix grant root permission in macOS\n- fix linux system proxy default bypass\n\n---\n\n## v1.3.0\n\n### Features\n\n- update clash and clash meta\n- support opening dir on tray\n- support updating all profiles with one click\n- support granting root permission to clash core(Linux, macOS)\n- support enable/disable clash fields filter, feel free to experience the latest features of Clash Meta\n\n### Bug Fixes\n\n- deb add openssl depend(Linux)\n- fix the AppImage auto launch path(Linux)\n- fix get the default network service(macOS)\n- remove the esc key listener in macOS, cmd+w instead(macOS)\n- fix infinite retry when websocket error\n\n---\n\n## v1.2.3\n\n### Features\n\n- update clash\n- adjust macOS window style\n- profile supports UTF8 with BOM\n\n### Bug Fixes\n\n- fix selected proxy\n- fix error log\n\n---\n\n## v1.2.2\n\n### Features\n\n- update clash meta\n- recover clash core after panic\n- use system window decorations(Linux)\n\n### Bug Fixes\n\n- flush system proxy settings(Windows)\n- fix parse log panic\n- fix ui bug\n\n---\n\n## v1.2.1\n\n### Features\n\n- update clash version\n- proxy groups support multi columns\n- optimize ui\n\n### Bug Fixes\n\n- fix ui websocket connection\n- adjust delay check concurrency\n- avoid setting login item repeatedly(macOS)\n\n---\n\n## v1.2.0\n\n### Features\n\n- update clash meta version\n- support to change external-controller\n- support to change default latency test URL\n- close all connections when proxy changed or profile changed\n- check the config by using the core\n- increase the robustness of the program\n- optimize windows service mode (need to reinstall)\n- optimize ui\n\n### Bug Fixes\n\n- invalid hotkey cause panic\n- invalid theme setting cause panic\n- fix some other glitches\n\n---\n\n## v1.1.2\n\n### Features\n\n- the system tray follows i18n\n- change the proxy group ui of global mode\n- support to update profile with the system proxy/clash proxy\n- check the remote profile more strictly\n\n### Bug Fixes\n\n- use app version as default user agent\n- the clash not exit in service mode\n- reset the system proxy when quit the app\n- fix some other glitches\n\n---\n\n## v1.1.1\n\n### Features\n\n- optimize clash config feedback\n- hide macOS dock icon\n- use clash meta compatible version (Linux)\n\n### Bug Fixes\n\n- fix some other glitches\n\n---\n\n## v1.1.0\n\n### Features\n\n- add rule page\n- supports proxy providers delay check\n- add proxy delay check loading status\n- supports hotkey/shortcut management\n- supports displaying connections data in table layout(refer to yacd)\n\n### Bug Fixes\n\n- supports yaml merge key in clash config\n- detect the network interface and set the system proxy(macOS)\n- fix some other glitches\n\n---\n\n## v1.0.6\n\n### Features\n\n- update clash and clash.meta\n\n### Bug Fixes\n\n- only script profile display console\n- automatic configuration update on demand at launch\n\n---\n\n## v1.0.5\n\n### Features\n\n- reimplement profile enhanced mode with quick-js\n- optimize the runtime config generation process\n- support web ui management\n- support clash field management\n- support viewing the runtime config\n- adjust some pages style\n\n### Bug Fixes\n\n- fix silent start\n- fix incorrectly reset system proxy on exit\n\n---\n\n## v1.0.4\n\n### Features\n\n- update clash core and clash meta version\n- support switch clash mode on system tray\n- theme mode support follows system\n\n### Bug Fixes\n\n- config load error on first use\n\n---\n\n## v1.0.3\n\n### Features\n\n- save some states such as URL test, filter, etc\n- update clash core and clash-meta core\n- new icon for macOS\n\n---\n\n## v1.0.2\n\n### Features\n\n- supports for switching clash core\n- supports release UI processes\n- supports script mode setting\n\n### Bug Fixes\n\n- fix service mode bug (Windows)\n\n---\n\n## v1.0.1\n\n### Features\n\n- adjust default theme settings\n- reduce gpu usage of traffic graph when hidden\n- supports more remote profile response header setting\n- check remote profile data format when imported\n\n### Bug Fixes\n\n- service mode install and start issue (Windows)\n- fix launch panic (Some Windows)\n\n---\n\n## v1.0.0\n\n### Features\n\n- update clash core\n- optimize traffic graph animation\n- supports interval update profiles\n- supports service mode (Windows)\n\n### Bug Fixes\n\n- reset system proxy when exit from dock (macOS)\n- adjust clash dns config process strategy\n\n---\n\n## v0.0.29\n\n### Features\n\n- sort proxy node\n- custom proxy test url\n- logs page filter\n- connections page filter\n- default user agent for subscription\n- system tray add tun mode toggle\n- enable to change the config dir (Windows only)\n\n---\n\n## v0.0.28\n\n### Features\n\n- enable to use clash config fields (UI)\n\n### Bug Fixes\n\n- remove the character\n- fix some icon color\n\n---\n\n## v0.0.27\n\n### Features\n\n- supports custom theme color\n- tun mode setting control the final config\n\n### Bug Fixes\n\n- fix transition flickers (macOS)\n- reduce proxy page render\n\n---\n\n## v0.0.26\n\n### Features\n\n- silent start\n- profile editor\n- profile enhance mode supports more fields\n- optimize profile enhance mode strategy\n\n### Bug Fixes\n\n- fix csp restriction on macOS\n- window controllers on Linux\n\n---\n\n## v0.0.25\n\n### Features\n\n- update clash core version\n\n### Bug Fixes\n\n- app updater error\n- display window controllers on Linux\n\n### Notes\n\nIf you can't update the app properly, please consider downloading the latest version from github release.\n\n---\n\n## v0.0.24\n\n### Features\n\n- Connections page\n- add wintun.dll (Windows)\n- supports create local profile with selected file (Windows)\n- system tray enable set system proxy\n\n### Bug Fixes\n\n- open dir error\n- auto launch path (Windows)\n- fix some clash config error\n- reduce the impact of the enhanced mode\n\n---\n\n## v0.0.23\n\n### Features\n\n- i18n supports\n- Remote profile User Agent supports\n\n### Bug Fixes\n\n- clash config file case ignore\n- clash `external-controller` only port\n"
  },
  {
    "path": "backend/.gitignore",
    "content": "# Generated by Cargo\n# will have compiled files and executables\n**/target/\n"
  },
  {
    "path": "backend/Cargo.toml",
    "content": "[workspace]\nresolver = \"2\"\nmembers = [\"tauri\", \"boa_utils\", \"nyanpasu-macro\", \"nyanpasu-egui\"]\n\n[patch.crates-io]\ntray-icon = { git = \"https://github.com/tauri-apps/tray-icon.git\", rev = \"34a3442\" }\n\n[workspace.package]\nrepository = \"https://github.com/keiko233/clash-nyanpasu.git\"\nedition = \"2024\"\nlicense = \"GPL-3.0\"\nauthors = [\"zzzgydi\", \"keiko233\"]\n\n[workspace.dependencies]\nthiserror = \"2\"\ntracing = \"0.1\"\nboa_engine = { version = \"0.21\", features = [\"annex-b\"] }\nreqwest = { version = \"0.12\", default-features = false, features = [\n  \"charset\",\n  \"http2\",\n  \"system-proxy\",\n  \"json\",\n  \"stream\",\n  \"rustls-tls\",\n] }\ntokio = { version = \"1\", features = [\"full\"] }\nnyanpasu-utils = { git = \"https://github.com/libnyanpasu/nyanpasu-utils.git\", features = [\n  \"specta\",\n] }\ntest-log = { version = \"0.2.16\", features = [\"trace\"] }\ntracing-test = { git = \"https://github.com/Frando/tracing-test.git\", rev = \"e81ec65\", features = [\n  \"no-env-filter\",\n  \"pretty-log-printing\",\n] }\nfs4 = { version = \"0.13.1\", features = [\"fs-err3-tokio\", \"fs-err3\"] }\nfs-err = { version = \"3.1.2\", features = [\"tokio\"] }\n\n[profile.release]\npanic = \"unwind\"\ncodegen-units = 1\nlto = true\nopt-level = \"s\"\n"
  },
  {
    "path": "backend/Cross.toml",
    "content": "[target.aarch64-unknown-linux-gnu]\n# dockerfile = \"./manifest/docker/ubuntu-22.04-aarch64/Dockerfile\"\nimage = \"ghcr.io/libnyanpasu/builder-debian-trixie-aarch64:latest\"\n# pre-build = [\n#   \"dpkg --add-architecture $CROSS_DEB_ARCH\",\n#   \"\"\"echo \"deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ jammy main restricted universe multiverse\" | tee /etc/apt/sources.list && \\\n#     echo \"deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ jammy-updates main restricted universe multiverse\" | tee -a /etc/apt/sources.list && \\\n#     echo \"deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ jammy-security main restricted universe multiverse\" | tee -a /etc/apt/sources.list && \\\n#     echo \"deb [arch=i386,amd64] http://archive.ubuntu.com/ubuntu/ jammy main restricted universe multiverse\" | tee -a /etc/apt/sources.list && \\\n#     echo \"deb [arch=i386,amd64] http://archive.ubuntu.com/ubuntu/ jammy-updates main restricted universe multiverse\" | tee -a /etc/apt/sources.list && \\\n#     echo \"deb [arch=i386,amd64] http://archive.ubuntu.com/ubuntu/ jammy-security main restricted universe multiverse\" | tee -a /etc/apt/sources.list && \\\n#     apt-get update && apt-get -y install \\\n#     build-essential \\\n#     libgtk-3-dev:$CROSS_DEB_ARCH \\\n#     libwebkit2gtk-4.1-dev:$CROSS_DEB_ARCH \\\n#     libxdo-dev:$CROSS_DEB_ARCH \\\n#     libayatana-appindicator3-dev:$CROSS_DEB_ARCH \\\n#     librsvg2-dev:$CROSS_DEB_ARCH \\\n#     libpango1.0-dev:$CROSS_DEB_ARCH \\\n#     libcairo2-dev:$CROSS_DEB_ARCH \\\n#     libatk1.0-dev:$CROSS_DEB_ARCH \\\n#     libsoup2.4-dev:$CROSS_DEB_ARCH \\\n#     libssl-dev:$CROSS_DEB_ARCH \\\n#   \"\"\",\n# ]\n\n[target.armv7-unknown-linux-gnueabihf]\nimage = \"ghcr.io/libnyanpasu/builder-debian-trixie-armhf:latest\"\n\n[target.armv7-unknown-linux-gnueabi]\nimage = \"ghcr.io/libnyanpasu/builder-debian-trixie-armel:latest\"\n\n[target.i686-unknown-linux-gnu]\nimage = \"ghcr.io/libnyanpasu/builder-debian-trixie-i686:latest\"\n"
  },
  {
    "path": "backend/boa_utils/Cargo.toml",
    "content": "[package]\nname = \"boa_utils\"\nversion = \"0.1.0\"\nrepository.workspace = true\nedition.workspace = true\nlicense.workspace = true\nauthors.workspace = true\n\n[lib]\ndoctest = false\n\n[dependencies]\nrustc-hash = { version = \"2\", features = [\"std\"] }\nboa_engine = { workspace = true, features = [\"annex-b\"] }\nboa_gc = { version = \"0.21\" }\nboa_parser = { version = \"0.21\", features = [\"annex-b\"] }\ntokio = { workspace = true }\nnyanpasu-utils = { workspace = true }\nreqwest = { workspace = true }\nfutures-util = \"0.3\"\nfutures-concurrency = \"7\"\nsmol = \"2\"\ntracing = \"0.1\"\nurl = \"2\"\nlog = \"0.4\"\nanyhow = \"1.0\"\ninclude_url_macro = { git = \"https://github.com/libnyanpasu/include_url_macro\", rev = \"fbe47bd\" }\ninclude-compress-bytes = { git = \"https://github.com/libnyanpasu/include-compress-bytes\", rev = \"4e4f25b\" }\nphf = { version = \"0.13.1\", features = [\"macros\"] }\n\n# for cacheing\nmime = \"0.3.17\"\nasync-fs = \"2.1.2\"\n\n# for encoding/decoding\nserde = { version = \"1.0\", features = [\"derive\"] }\npostcard = { version = \"1.1.1\", features = [\"use-std\"] }\nserde_json = { version = \"1.0\", features = [\"preserve_order\"] }\nbrotli = \"8.0.2\"\n\n[dev-dependencies]\nindoc = \"2\"\ntextwrap = \"0.16\"\ntest-log = { workspace = true }\ntempfile = \"3.17\"\n"
  },
  {
    "path": "backend/boa_utils/src/console/mod.rs",
    "content": "//! Boa's implementation of JavaScript's `console` Web API object.\n//!\n//! The `console` object can be accessed from any global object.\n//!\n//! The specifics of how it works varies from browser to browser, but there is a de facto set of features that are typically provided.\n//!\n//! More information:\n//!  - [MDN documentation][mdn]\n//!  - [WHATWG `console` specification][spec]\n//!\n//! [spec]: https://console.spec.whatwg.org/\n//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/Console\n\n#[cfg(test)]\nmod tests;\n\nuse boa_engine::{\n    Context, JsArgs, JsData, JsError, JsResult, JsStr, JsString, js_str, js_string,\n    native_function::NativeFunction,\n    object::{JsObject, ObjectInitializer},\n    value::{JsValue, Numeric},\n};\nuse boa_gc::{Finalize, Trace};\nuse rustc_hash::FxHashMap;\nuse std::{cell::RefCell, collections::hash_map::Entry, rc::Rc, time::SystemTime};\n\n/// This represents the different types of log messages.\n#[derive(Debug)]\npub enum LogMessage {\n    Log(String),\n    Info(String),\n    Warn(String),\n    Error(String),\n}\n\n/// Helper function for logging messages.\nfn logger(msg: LogMessage, console_state: &Console) {\n    let indent = 2 * console_state.groups.len();\n\n    match msg {\n        LogMessage::Error(msg) => {\n            eprintln!(\"{msg:>indent$}\");\n        }\n        LogMessage::Log(msg) | LogMessage::Info(msg) | LogMessage::Warn(msg) => {\n            println!(\"{msg:>indent$}\");\n        }\n    }\n}\n\npub trait Logger {\n    type Item;\n    fn log(&mut self, msg: LogMessage, console_state: &Console);\n    fn take(&mut self) -> Vec<Self::Item>;\n}\n\npub trait LoggerBox = Logger<Item = LogMessage> + Sync + Send + 'static;\n\nstruct ConsoleLogger;\n\nimpl Logger for ConsoleLogger {\n    type Item = LogMessage;\n    fn log(&mut self, msg: LogMessage, console_state: &Console) {\n        logger(msg, console_state);\n    }\n    fn take(&mut self) -> Vec<Self::Item> {\n        vec![]\n    }\n}\n\nthread_local! {\n    static LOGGER: RefCell<Box<dyn LoggerBox>> = RefCell::new(Box::new(ConsoleLogger));\n}\n\npub fn inspect_logger<R>(f: impl FnOnce(&mut dyn LoggerBox) -> R) -> R {\n    LOGGER.with(|cell| f(cell.borrow_mut().as_mut()))\n}\n\npub fn set_logger(logger: Box<dyn LoggerBox>) {\n    LOGGER.with(|cell| {\n        *cell.borrow_mut() = logger;\n    });\n}\n\n/// This represents the `console` formatter.\nfn formatter(data: &[JsValue], context: &mut Context) -> JsResult<String> {\n    match data {\n        [] => Ok(String::new()),\n        [val] => Ok(val.to_string(context)?.to_std_string_escaped()),\n        data => {\n            let mut formatted = String::new();\n            let mut arg_index = 1;\n            let target = data\n                .get_or_undefined(0)\n                .to_string(context)?\n                .to_std_string_escaped();\n            let mut chars = target.chars();\n            while let Some(c) = chars.next() {\n                if c == '%' {\n                    let fmt = chars.next().unwrap_or('%');\n                    match fmt {\n                        /* integer */\n                        'd' | 'i' => {\n                            let arg = match data.get_or_undefined(arg_index).to_numeric(context)? {\n                                Numeric::Number(r) => (r.floor() + 0.0).to_string(),\n                                Numeric::BigInt(int) => int.to_string(),\n                            };\n                            formatted.push_str(&arg);\n                            arg_index += 1;\n                        }\n                        /* float */\n                        'f' => {\n                            let arg = data.get_or_undefined(arg_index).to_number(context)?;\n                            formatted.push_str(&format!(\"{arg:.6}\"));\n                            arg_index += 1;\n                        }\n                        /* object, FIXME: how to render this properly? */\n                        'o' | 'O' => {\n                            let arg = data.get_or_undefined(arg_index);\n                            formatted.push_str(&arg.display().to_string());\n                            arg_index += 1;\n                        }\n                        /* string */\n                        's' => {\n                            let arg = data\n                                .get_or_undefined(arg_index)\n                                .to_string(context)?\n                                .to_std_string_escaped();\n                            formatted.push_str(&arg);\n                            arg_index += 1;\n                        }\n                        '%' => formatted.push('%'),\n                        /* TODO: %c is not implemented */\n                        c => {\n                            formatted.push('%');\n                            formatted.push(c);\n                        }\n                    }\n                } else {\n                    formatted.push(c);\n                };\n            }\n\n            /* unformatted data */\n            for rest in data.iter().skip(arg_index) {\n                formatted.push_str(&format!(\n                    \" {}\",\n                    rest.to_string(context)?.to_std_string_escaped()\n                ));\n            }\n\n            Ok(formatted)\n        }\n    }\n}\n\n/// This is the internal console object state.\n#[derive(Debug, Default, Trace, Finalize, JsData)]\npub struct Console {\n    count_map: FxHashMap<JsString, u32>,\n    timer_map: FxHashMap<JsString, u128>,\n    groups: Vec<String>,\n}\n\nimpl Console {\n    /// Name of the built-in `console` property.\n    pub const NAME: JsStr<'static> = js_str!(\"console\");\n\n    /// Initializes the `console` built-in object.\n    #[allow(clippy::too_many_lines)]\n    pub fn init(context: &mut Context) -> JsObject {\n        fn console_method(\n            f: fn(&JsValue, &[JsValue], &Console, &mut Context) -> JsResult<JsValue>,\n            state: Rc<RefCell<Console>>,\n        ) -> NativeFunction {\n            // SAFETY: `Console` doesn't contain types that need tracing.\n            unsafe {\n                NativeFunction::from_closure(move |this, args, context| {\n                    f(this, args, &state.borrow(), context)\n                })\n            }\n        }\n        fn console_method_mut(\n            f: fn(&JsValue, &[JsValue], &mut Console, &mut Context) -> JsResult<JsValue>,\n            state: Rc<RefCell<Console>>,\n        ) -> NativeFunction {\n            // SAFETY: `Console` doesn't contain types that need tracing.\n            unsafe {\n                NativeFunction::from_closure(move |this, args, context| {\n                    f(this, args, &mut state.borrow_mut(), context)\n                })\n            }\n        }\n        // let _timer = Profiler::global().start_event(std::any::type_name::<Self>(), \"init\");\n\n        let state = Rc::new(RefCell::new(Self::default()));\n\n        ObjectInitializer::with_native_data(Self::default(), context)\n            .function(\n                console_method(Self::assert, state.clone()),\n                js_string!(\"assert\"),\n                0,\n            )\n            .function(\n                console_method_mut(Self::clear, state.clone()),\n                js_string!(\"clear\"),\n                0,\n            )\n            .function(\n                console_method(Self::debug, state.clone()),\n                js_string!(\"debug\"),\n                0,\n            )\n            .function(\n                console_method(Self::error, state.clone()),\n                js_string!(\"error\"),\n                0,\n            )\n            .function(\n                console_method(Self::info, state.clone()),\n                js_string!(\"info\"),\n                0,\n            )\n            .function(\n                console_method(Self::log, state.clone()),\n                js_string!(\"log\"),\n                0,\n            )\n            .function(\n                console_method(Self::trace, state.clone()),\n                js_string!(\"trace\"),\n                0,\n            )\n            .function(\n                console_method(Self::warn, state.clone()),\n                js_string!(\"warn\"),\n                0,\n            )\n            .function(\n                console_method_mut(Self::count, state.clone()),\n                js_string!(\"count\"),\n                0,\n            )\n            .function(\n                console_method_mut(Self::count_reset, state.clone()),\n                js_string!(\"countReset\"),\n                0,\n            )\n            .function(\n                console_method_mut(Self::group, state.clone()),\n                js_string!(\"group\"),\n                0,\n            )\n            .function(\n                console_method_mut(Self::group_collapsed, state.clone()),\n                js_string!(\"groupCollapsed\"),\n                0,\n            )\n            .function(\n                console_method_mut(Self::group_end, state.clone()),\n                js_string!(\"groupEnd\"),\n                0,\n            )\n            .function(\n                console_method_mut(Self::time, state.clone()),\n                js_string!(\"time\"),\n                0,\n            )\n            .function(\n                console_method(Self::time_log, state.clone()),\n                js_string!(\"timeLog\"),\n                0,\n            )\n            .function(\n                console_method_mut(Self::time_end, state.clone()),\n                js_string!(\"timeEnd\"),\n                0,\n            )\n            .function(\n                console_method(Self::dir, state.clone()),\n                js_string!(\"dir\"),\n                0,\n            )\n            .function(console_method(Self::dir, state), js_string!(\"dirxml\"), 0)\n            .build()\n    }\n\n    /// `console.assert(condition, ...data)`\n    ///\n    /// Prints a JavaScript value to the standard error if first argument evaluates to `false` or there\n    /// were no arguments.\n    ///\n    /// More information:\n    ///  - [MDN documentation][mdn]\n    ///  - [WHATWG `console` specification][spec]\n    ///\n    /// [spec]: https://console.spec.whatwg.org/#assert\n    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/assert\n    fn assert(\n        _: &JsValue,\n        args: &[JsValue],\n        console: &Self,\n        context: &mut Context,\n    ) -> JsResult<JsValue> {\n        let assertion = args.first().is_some_and(JsValue::to_boolean);\n\n        if !assertion {\n            let mut args: Vec<JsValue> = args.iter().skip(1).cloned().collect();\n            let message = js_string!(\"Assertion failed\");\n            if args.is_empty() {\n                args.push(JsValue::new(message));\n            } else if !args[0].is_string() {\n                args.insert(0, JsValue::new(message));\n            } else {\n                let value = JsString::from(args[0].display().to_string());\n                let concat = js_string!(message.as_str(), js_str!(\": \"), &value);\n                args[0] = JsValue::new(concat);\n            }\n\n            LOGGER.with(|logger| {\n                logger\n                    .borrow_mut()\n                    .log(LogMessage::Error(formatter(&args, context)?), console);\n                Ok::<_, JsError>(())\n            })?;\n        }\n\n        Ok(JsValue::undefined())\n    }\n\n    /// `console.clear()`\n    ///\n    /// Removes all groups and clears console if possible.\n    ///\n    /// More information:\n    ///  - [MDN documentation][mdn]\n    ///  - [WHATWG `console` specification][spec]\n    ///\n    /// [spec]: https://console.spec.whatwg.org/#clear\n    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/clear\n    #[allow(clippy::unnecessary_wraps)]\n    fn clear(_: &JsValue, _: &[JsValue], console: &mut Self, _: &mut Context) -> JsResult<JsValue> {\n        console.groups.clear();\n        Ok(JsValue::undefined())\n    }\n\n    /// `console.debug(...data)`\n    ///\n    /// Prints a JavaScript values with \"debug\" logLevel.\n    ///\n    /// More information:\n    ///  - [MDN documentation][mdn]\n    ///  - [WHATWG `console` specification][spec]\n    ///\n    /// [spec]: https://console.spec.whatwg.org/#debug\n    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/debug\n    fn debug(\n        _: &JsValue,\n        args: &[JsValue],\n        console: &Self,\n        context: &mut Context,\n    ) -> JsResult<JsValue> {\n        LOGGER.with(|logger| {\n            logger\n                .borrow_mut()\n                .log(LogMessage::Log(formatter(args, context)?), console);\n            Ok::<_, JsError>(())\n        })?;\n        Ok(JsValue::undefined())\n    }\n\n    /// `console.error(...data)`\n    ///\n    /// Prints a JavaScript values with \"error\" logLevel.\n    ///\n    /// More information:\n    ///  - [MDN documentation][mdn]\n    ///  - [WHATWG `console` specification][spec]\n    ///\n    /// [spec]: https://console.spec.whatwg.org/#error\n    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/error\n    fn error(\n        _: &JsValue,\n        args: &[JsValue],\n        console: &Self,\n        context: &mut Context,\n    ) -> JsResult<JsValue> {\n        LOGGER.with(|logger| {\n            logger\n                .borrow_mut()\n                .log(LogMessage::Error(formatter(args, context)?), console);\n            Ok::<_, JsError>(())\n        })?;\n        Ok(JsValue::undefined())\n    }\n\n    /// `console.info(...data)`\n    ///\n    /// Prints a JavaScript values with \"info\" logLevel.\n    ///\n    /// More information:\n    ///  - [MDN documentation][mdn]\n    ///  - [WHATWG `console` specification][spec]\n    ///\n    /// [spec]: https://console.spec.whatwg.org/#info\n    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/info\n    fn info(\n        _: &JsValue,\n        args: &[JsValue],\n        console: &Self,\n        context: &mut Context,\n    ) -> JsResult<JsValue> {\n        LOGGER.with(|logger| {\n            logger\n                .borrow_mut()\n                .log(LogMessage::Info(formatter(args, context)?), console);\n            Ok::<_, JsError>(())\n        })?;\n        Ok(JsValue::undefined())\n    }\n\n    /// `console.log(...data)`\n    ///\n    /// Prints a JavaScript values with \"log\" logLevel.\n    ///\n    /// More information:\n    ///  - [MDN documentation][mdn]\n    ///  - [WHATWG `console` specification][spec]\n    ///\n    /// [spec]: https://console.spec.whatwg.org/#log\n    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/log\n    fn log(\n        _: &JsValue,\n        args: &[JsValue],\n        console: &Self,\n        context: &mut Context,\n    ) -> JsResult<JsValue> {\n        LOGGER.with(|logger| {\n            logger\n                .borrow_mut()\n                .log(LogMessage::Log(formatter(args, context)?), console);\n            Ok::<_, JsError>(())\n        })?;\n        Ok(JsValue::undefined())\n    }\n\n    /// `console.trace(...data)`\n    ///\n    /// Prints a stack trace with \"trace\" logLevel, optionally labelled by data.\n    ///\n    /// More information:\n    ///  - [MDN documentation][mdn]\n    ///  - [WHATWG `console` specification][spec]\n    ///\n    /// [spec]: https://console.spec.whatwg.org/#trace\n    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/trace\n    fn trace(\n        _: &JsValue,\n        args: &[JsValue],\n        console: &Self,\n        context: &mut Context,\n    ) -> JsResult<JsValue> {\n        if !args.is_empty() {\n            LOGGER.with(|logger| {\n                logger\n                    .borrow_mut()\n                    .log(LogMessage::Log(formatter(args, context)?), console);\n                Ok::<_, JsError>(())\n            })?;\n        }\n\n        let stack_trace_dump = context\n            .stack_trace()\n            .map(|frame| frame.code_block().name())\n            .collect::<Vec<_>>()\n            .into_iter()\n            .map(JsString::to_std_string_escaped)\n            .collect::<Vec<_>>()\n            .join(\"\\n\");\n        LOGGER.with(|logger| {\n            logger\n                .borrow_mut()\n                .log(LogMessage::Log(stack_trace_dump), console);\n            Ok::<_, JsError>(())\n        })?;\n\n        Ok(JsValue::undefined())\n    }\n\n    /// `console.warn(...data)`\n    ///\n    /// Prints a JavaScript values with \"warn\" logLevel.\n    ///\n    /// More information:\n    ///  - [MDN documentation][mdn]\n    ///  - [WHATWG `console` specification][spec]\n    ///\n    /// [spec]: https://console.spec.whatwg.org/#warn\n    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/warn\n    fn warn(\n        _: &JsValue,\n        args: &[JsValue],\n        console: &Self,\n        context: &mut Context,\n    ) -> JsResult<JsValue> {\n        LOGGER.with(|logger| {\n            logger\n                .borrow_mut()\n                .log(LogMessage::Warn(formatter(args, context)?), console);\n            Ok::<_, JsError>(())\n        })?;\n        Ok(JsValue::undefined())\n    }\n\n    /// `console.count(label)`\n    ///\n    /// Prints number of times the function was called with that particular label.\n    ///\n    /// More information:\n    ///  - [MDN documentation][mdn]\n    ///  - [WHATWG `console` specification][spec]\n    ///\n    /// [spec]: https://console.spec.whatwg.org/#count\n    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/count\n    fn count(\n        _: &JsValue,\n        args: &[JsValue],\n        console: &mut Self,\n        context: &mut Context,\n    ) -> JsResult<JsValue> {\n        let label = match args.first() {\n            Some(value) => value.to_string(context)?,\n            None => \"default\".into(),\n        };\n\n        let msg = format!(\"count {}:\", label.to_std_string_escaped());\n        let c = console.count_map.entry(label).or_insert(0);\n        *c += 1;\n\n        let msg = format!(\"{msg} {c}\");\n\n        LOGGER.with(|logger| {\n            logger.borrow_mut().log(LogMessage::Info(msg), console);\n            Ok::<_, JsError>(())\n        })?;\n        Ok(JsValue::undefined())\n    }\n\n    /// `console.countReset(label)`\n    ///\n    /// Resets the counter for label.\n    ///\n    /// More information:\n    ///  - [MDN documentation][mdn]\n    ///  - [WHATWG `console` specification][spec]\n    ///\n    /// [spec]: https://console.spec.whatwg.org/#countreset\n    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/countReset\n    fn count_reset(\n        _: &JsValue,\n        args: &[JsValue],\n        console: &mut Self,\n        context: &mut Context,\n    ) -> JsResult<JsValue> {\n        let label = match args.first() {\n            Some(value) => value.to_string(context)?,\n            None => \"default\".into(),\n        };\n\n        console.count_map.remove(&label);\n\n        logger(\n            LogMessage::Warn(format!(\"countReset {}\", label.to_std_string_escaped())),\n            console,\n        );\n\n        Ok(JsValue::undefined())\n    }\n\n    /// Returns current system time in ms.\n    fn system_time_in_ms() -> u128 {\n        let now = SystemTime::now();\n        now.duration_since(SystemTime::UNIX_EPOCH)\n            .expect(\"negative duration\")\n            .as_millis()\n    }\n\n    /// `console.time(label)`\n    ///\n    /// Starts the timer for given label.\n    ///\n    /// More information:\n    ///  - [MDN documentation][mdn]\n    ///  - [WHATWG `console` specification][spec]\n    ///\n    /// [spec]: https://console.spec.whatwg.org/#time\n    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/time\n    fn time(\n        _: &JsValue,\n        args: &[JsValue],\n        console: &mut Self,\n        context: &mut Context,\n    ) -> JsResult<JsValue> {\n        let label = match args.first() {\n            Some(value) => value.to_string(context)?,\n            None => \"default\".into(),\n        };\n\n        if let Entry::Vacant(e) = console.timer_map.entry(label.clone()) {\n            let time = Self::system_time_in_ms();\n            e.insert(time);\n        } else {\n            logger(\n                LogMessage::Warn(format!(\n                    \"Timer '{}' already exist\",\n                    label.to_std_string_escaped()\n                )),\n                console,\n            );\n        }\n\n        Ok(JsValue::undefined())\n    }\n\n    /// `console.timeLog(label, ...data)`\n    ///\n    /// Prints elapsed time for timer with given label.\n    ///\n    /// More information:\n    ///  - [MDN documentation][mdn]\n    ///  - [WHATWG `console` specification][spec]\n    ///\n    /// [spec]: https://console.spec.whatwg.org/#timelog\n    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/timeLog\n    fn time_log(\n        _: &JsValue,\n        args: &[JsValue],\n        console: &Self,\n        context: &mut Context,\n    ) -> JsResult<JsValue> {\n        let label = match args.first() {\n            Some(value) => value.to_string(context)?,\n            None => \"default\".into(),\n        };\n\n        console.timer_map.get(&label).map_or_else(\n            || {\n                logger(\n                    LogMessage::Warn(format!(\n                        \"Timer '{}' doesn't exist\",\n                        label.to_std_string_escaped()\n                    )),\n                    console,\n                );\n            },\n            |t| {\n                let time = Self::system_time_in_ms();\n                let mut concat = format!(\"{}: {} ms\", label.to_std_string_escaped(), time - t);\n                for msg in args.iter().skip(1) {\n                    concat = concat + \" \" + &msg.display().to_string();\n                }\n                LOGGER.with(|logger| {\n                    logger.borrow_mut().log(LogMessage::Log(concat), console);\n                });\n            },\n        );\n\n        Ok(JsValue::undefined())\n    }\n\n    /// `console.timeEnd(label)`\n    ///\n    /// Removes the timer with given label.\n    ///\n    /// More information:\n    ///  - [MDN documentation][mdn]\n    ///  - [WHATWG `console` specification][spec]\n    ///\n    /// [spec]: https://console.spec.whatwg.org/#timeend\n    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/timeEnd\n    fn time_end(\n        _: &JsValue,\n        args: &[JsValue],\n        console: &mut Self,\n        context: &mut Context,\n    ) -> JsResult<JsValue> {\n        let label = match args.first() {\n            Some(value) => value.to_string(context)?,\n            None => \"default\".into(),\n        };\n\n        console.timer_map.remove(&label).map_or_else(\n            || {\n                logger(\n                    LogMessage::Warn(format!(\n                        \"Timer '{}' doesn't exist\",\n                        label.to_std_string_escaped()\n                    )),\n                    console,\n                );\n            },\n            |t| {\n                let time = Self::system_time_in_ms();\n                logger(\n                    LogMessage::Info(format!(\n                        \"{}: {} ms - timer removed\",\n                        label.to_std_string_escaped(),\n                        time - t\n                    )),\n                    console,\n                );\n            },\n        );\n\n        Ok(JsValue::undefined())\n    }\n\n    /// `console.group(...data)`\n    ///\n    /// Adds new group with name from formatted data to stack.\n    ///\n    /// More information:\n    ///  - [MDN documentation][mdn]\n    ///  - [WHATWG `console` specification][spec]\n    ///\n    /// [spec]: https://console.spec.whatwg.org/#group\n    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/group\n    fn group(\n        _: &JsValue,\n        args: &[JsValue],\n        console: &mut Self,\n        context: &mut Context,\n    ) -> JsResult<JsValue> {\n        let group_label = formatter(args, context)?;\n\n        LOGGER.with(|logger| {\n            logger\n                .borrow_mut()\n                .log(LogMessage::Info(format!(\"group: {group_label}\")), console);\n            Ok::<_, JsError>(())\n        })?;\n        console.groups.push(group_label);\n\n        Ok(JsValue::undefined())\n    }\n\n    /// `console.groupCollapsed(...data)`\n    ///\n    /// Adds new group collapsed with name from formatted data to stack.\n    ///\n    /// More information:\n    ///  - [MDN documentation][mdn]\n    ///  - [WHATWG `console` specification][spec]\n    ///\n    /// [spec]: https://console.spec.whatwg.org/#groupcollapsed\n    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/groupcollapsed_static\n    fn group_collapsed(\n        _: &JsValue,\n        args: &[JsValue],\n        console: &mut Self,\n        context: &mut Context,\n    ) -> JsResult<JsValue> {\n        Console::group(&JsValue::undefined(), args, console, context)\n    }\n\n    /// `console.groupEnd(label)`\n    ///\n    /// Removes the last group from the stack.\n    ///\n    /// More information:\n    ///  - [MDN documentation][mdn]\n    ///  - [WHATWG `console` specification][spec]\n    ///\n    /// [spec]: https://console.spec.whatwg.org/#groupend\n    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/groupEnd\n    #[allow(clippy::unnecessary_wraps)]\n    fn group_end(\n        _: &JsValue,\n        _: &[JsValue],\n        console: &mut Self,\n        _: &mut Context,\n    ) -> JsResult<JsValue> {\n        console.groups.pop();\n\n        Ok(JsValue::undefined())\n    }\n\n    /// `console.dir(item, options)`\n    ///\n    /// Prints info about item\n    ///\n    /// More information:\n    ///  - [MDN documentation][mdn]\n    ///  - [WHATWG `console` specification][spec]\n    ///\n    /// [spec]: https://console.spec.whatwg.org/#dir\n    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/dir\n    #[allow(clippy::unnecessary_wraps)]\n    fn dir(_: &JsValue, args: &[JsValue], console: &Self, _: &mut Context) -> JsResult<JsValue> {\n        logger(\n            LogMessage::Info(args.get_or_undefined(0).display_obj(true)),\n            console,\n        );\n        Ok(JsValue::undefined())\n    }\n}\n"
  },
  {
    "path": "backend/boa_utils/src/console/tests.rs",
    "content": "use super::{Console, formatter};\nuse crate::test::{TestAction, run_test_actions, run_test_actions_with};\nuse boa_engine::{Context, JsValue, js_string, property::Attribute};\nuse indoc::indoc;\n\n#[test]\nfn formatter_no_args_is_empty_string() {\n    run_test_actions([TestAction::inspect_context(|ctx| {\n        assert_eq!(formatter(&[], ctx).unwrap(), \"\");\n    })]);\n}\n\n#[test]\nfn formatter_empty_format_string_is_empty_string() {\n    run_test_actions([TestAction::inspect_context(|ctx| {\n        assert_eq!(formatter(&[JsValue::new(js_string!())], ctx).unwrap(), \"\");\n    })]);\n}\n\n#[test]\nfn formatter_format_without_args_renders_verbatim() {\n    run_test_actions([TestAction::inspect_context(|ctx| {\n        assert_eq!(\n            formatter(&[JsValue::new(js_string!(\"%d %s %% %f\"))], ctx).unwrap(),\n            \"%d %s %% %f\"\n        );\n    })]);\n}\n\n#[test]\nfn formatter_empty_format_string_concatenates_rest_of_args() {\n    run_test_actions([TestAction::inspect_context(|ctx| {\n        assert_eq!(\n            formatter(\n                &[\n                    JsValue::new(js_string!(\"\")),\n                    JsValue::new(js_string!(\"to powinno zostać\")),\n                    JsValue::new(js_string!(\"połączone\")),\n                ],\n                ctx\n            )\n            .unwrap(),\n            \" to powinno zostać połączone\"\n        );\n    })]);\n}\n\n#[test]\nfn formatter_utf_8_checks() {\n    run_test_actions([TestAction::inspect_context(|ctx| {\n        assert_eq!(\n            formatter(\n                &[\n                    JsValue::new(js_string!(\"Są takie chwile %dą %są tu%sów %привет%ź\")),\n                    JsValue::new(123),\n                    JsValue::new(1.23),\n                    JsValue::new(js_string!(\"ł\")),\n                ],\n                ctx\n            )\n            .unwrap(),\n            \"Są takie chwile 123ą 1.23ą tułów %привет%ź\"\n        );\n    })]);\n}\n\n#[test]\nfn formatter_trailing_format_leader_renders() {\n    run_test_actions([TestAction::inspect_context(|ctx| {\n        assert_eq!(\n            formatter(\n                &[\n                    JsValue::new(js_string!(\"%%%%%\")),\n                    JsValue::new(js_string!(\"|\"))\n                ],\n                ctx\n            )\n            .unwrap(),\n            \"%%% |\"\n        );\n    })]);\n}\n\n#[test]\n#[allow(clippy::approx_constant)]\nfn formatter_float_format_works() {\n    run_test_actions([TestAction::inspect_context(|ctx| {\n        assert_eq!(\n            formatter(&[JsValue::new(js_string!(\"%f\")), JsValue::new(3.1415)], ctx).unwrap(),\n            \"3.141500\"\n        );\n    })]);\n}\n\n#[test]\nfn console_log_cyclic() {\n    let mut context = Context::default();\n    let console = Console::init(&mut context);\n    context\n        .register_global_property(js_string!(Console::NAME), console, Attribute::all())\n        .unwrap();\n\n    run_test_actions_with(\n        [TestAction::run(indoc! {r#\"\n                let a = [1];\n                a[1] = a;\n                console.log(a);\n            \"#})],\n        &mut context,\n    );\n    // Should not stack overflow\n}\n"
  },
  {
    "path": "backend/boa_utils/src/lib.rs",
    "content": "#![feature(trait_alias)]\n#![feature(auto_traits)]\n#![feature(negative_impls)]\n\n//! Boa's **boa_runtime** crate contains an example runtime and basic runtime features and\n//! functionality for the `boa_engine` crate for runtime implementors.\n//!\n//! # Example: Adding Web API's Console Object\n//!\n//! 1. Add **boa_runtime** as a dependency to your project along with **boa_engine**.\n//!\n//! ```no_run\n//! use boa_engine::{js_string, property::Attribute, Context, Source};\n//! use boa_runtime::Console;\n//!\n//! // Create the context.\n//! let mut context = Context::default();\n//!\n//! // Initialize the Console object.\n//! let console = Console::init(&mut context);\n//!\n//! // Register the console as a global property to the context.\n//! context\n//!     .register_global_property(js_string!(Console::NAME), console, Attribute::all())\n//!     .expect(\"the console object shouldn't exist yet\");\n//!\n//! // JavaScript source for parsing.\n//! let js_code = \"console.log('Hello World from a JS code string!')\";\n//!\n//! // Parse the source code\n//! match context.eval(Source::from_bytes(js_code)) {\n//!     Ok(res) => {\n//!         println!(\n//!             \"{}\",\n//!             res.to_string(&mut context).unwrap().to_std_string_escaped()\n//!         );\n//!     }\n//!     Err(e) => {\n//!         // Pretty print the error\n//!         eprintln!(\"Uncaught {e}\");\n//!         # panic!(\"An error occured in boa_runtime's js_code\");\n//!     }\n//! };\n//! ```\n#![doc(\n    html_logo_url = \"https://raw.githubusercontent.com/boa-dev/boa/main/assets/logo.svg\",\n    html_favicon_url = \"https://raw.githubusercontent.com/boa-dev/boa/main/assets/logo.svg\"\n)]\n#![cfg_attr(test, allow(clippy::needless_raw_string_hashes))] // Makes strings a bit more copy-pastable\n#![cfg_attr(not(test), forbid(clippy::unwrap_used))]\n#![allow(\n    clippy::module_name_repetitions,\n    clippy::redundant_pub_crate,\n    clippy::let_unit_value\n)]\n\nmod console;\npub mod module;\n#[doc(inline)]\npub use console::Console;\npub use console::{LogMessage, Logger, LoggerBox, inspect_logger, set_logger};\n\n#[cfg(test)]\npub(crate) mod test {\n    use boa_engine::{Context, JsResult, JsValue, Source, builtins};\n    use std::borrow::Cow;\n\n    /// A test action executed in a test function.\n    #[allow(missing_debug_implementations)]\n    #[derive(Clone)]\n    pub(crate) struct TestAction(Inner);\n\n    #[derive(Clone)]\n    #[allow(dead_code)]\n    enum Inner {\n        RunHarness,\n        Run {\n            source: Cow<'static, str>,\n        },\n        InspectContext {\n            op: fn(&mut Context),\n        },\n        Assert {\n            source: Cow<'static, str>,\n        },\n        AssertEq {\n            source: Cow<'static, str>,\n            expected: JsValue,\n        },\n        AssertWithOp {\n            source: Cow<'static, str>,\n            op: fn(JsValue, &mut Context) -> bool,\n        },\n        AssertOpaqueError {\n            source: Cow<'static, str>,\n            expected: JsValue,\n        },\n        AssertNativeError {\n            source: Cow<'static, str>,\n            kind: builtins::error::ErrorKind,\n            message: &'static str,\n        },\n        AssertContext {\n            op: fn(&mut Context) -> bool,\n        },\n    }\n\n    impl TestAction {\n        /// Runs `source`, panicking if the execution throws.\n        pub(crate) fn run(source: impl Into<Cow<'static, str>>) -> Self {\n            Self(Inner::Run {\n                source: source.into(),\n            })\n        }\n\n        /// Executes `op` with the currently active context.\n        ///\n        /// Useful to make custom assertions that must be done from Rust code.\n        pub(crate) fn inspect_context(op: fn(&mut Context)) -> Self {\n            Self(Inner::InspectContext { op })\n        }\n    }\n\n    /// Executes a list of test actions on a new, default context.\n    #[track_caller]\n    pub(crate) fn run_test_actions(actions: impl IntoIterator<Item = TestAction>) {\n        let context = &mut Context::default();\n        run_test_actions_with(actions, context);\n    }\n\n    /// Executes a list of test actions on the provided context.\n    #[track_caller]\n    #[allow(clippy::too_many_lines, clippy::missing_panics_doc)]\n    pub(crate) fn run_test_actions_with(\n        actions: impl IntoIterator<Item = TestAction>,\n        context: &mut Context,\n    ) {\n        #[track_caller]\n        fn forward_val(context: &mut Context, source: &str) -> JsResult<JsValue> {\n            context.eval(Source::from_bytes(source))\n        }\n\n        #[track_caller]\n        fn fmt_test(source: &str, test: usize) -> String {\n            format!(\n                \"\\n\\nTest case {test}: \\n```\\n{}\\n```\",\n                textwrap::indent(source, \"    \")\n            )\n        }\n\n        // Some unwrapping patterns look weird because they're replaceable\n        // by simpler patterns like `unwrap_or_else` or `unwrap_err\n        let mut i = 1;\n        for action in actions.into_iter().map(|a| a.0) {\n            match action {\n                Inner::RunHarness => {\n                    // add utility functions for testing\n                    // TODO: extract to a file\n                    forward_val(\n                        context,\n                        r#\"\n                        function equals(a, b) {\n                            if (Array.isArray(a) && Array.isArray(b)) {\n                                return arrayEquals(a, b);\n                            }\n                            return a === b;\n                        }\n                        function arrayEquals(a, b) {\n                            return Array.isArray(a) &&\n                                Array.isArray(b) &&\n                                a.length === b.length &&\n                                a.every((val, index) => equals(val, b[index]));\n                        }\n                    \"#,\n                    )\n                    .expect(\"failed to evaluate test harness\");\n                }\n                Inner::Run { source } => {\n                    if let Err(e) = forward_val(context, &source) {\n                        panic!(\"{}\\nUncaught {e}\", fmt_test(&source, i));\n                    }\n                }\n                Inner::InspectContext { op } => {\n                    op(context);\n                }\n                Inner::Assert { source } => {\n                    let val = match forward_val(context, &source) {\n                        Err(e) => panic!(\"{}\\nUncaught {e}\", fmt_test(&source, i)),\n                        Ok(v) => v,\n                    };\n                    let Some(val) = val.as_boolean() else {\n                        panic!(\n                            \"{}\\nTried to assert with the non-boolean value `{}`\",\n                            fmt_test(&source, i),\n                            val.display()\n                        )\n                    };\n                    assert!(val, \"{}\", fmt_test(&source, i));\n                    i += 1;\n                }\n                Inner::AssertEq { source, expected } => {\n                    let val = match forward_val(context, &source) {\n                        Err(e) => panic!(\"{}\\nUncaught {e}\", fmt_test(&source, i)),\n                        Ok(v) => v,\n                    };\n                    assert_eq!(val, expected, \"{}\", fmt_test(&source, i));\n                    i += 1;\n                }\n                Inner::AssertWithOp { source, op } => {\n                    let val = match forward_val(context, &source) {\n                        Err(e) => panic!(\"{}\\nUncaught {e}\", fmt_test(&source, i)),\n                        Ok(v) => v,\n                    };\n                    assert!(op(val, context), \"{}\", fmt_test(&source, i));\n                    i += 1;\n                }\n                Inner::AssertOpaqueError { source, expected } => {\n                    let err = match forward_val(context, &source) {\n                        Ok(v) => panic!(\n                            \"{}\\nExpected error, got value `{}`\",\n                            fmt_test(&source, i),\n                            v.display()\n                        ),\n                        Err(e) => e,\n                    };\n                    let Some(err) = err.as_opaque() else {\n                        panic!(\n                            \"{}\\nExpected opaque error, got native error `{}`\",\n                            fmt_test(&source, i),\n                            err\n                        )\n                    };\n\n                    assert_eq!(err, &expected, \"{}\", fmt_test(&source, i));\n                    i += 1;\n                }\n                Inner::AssertNativeError {\n                    source,\n                    kind,\n                    message,\n                } => {\n                    let err = match forward_val(context, &source) {\n                        Ok(v) => panic!(\n                            \"{}\\nExpected error, got value `{}`\",\n                            fmt_test(&source, i),\n                            v.display()\n                        ),\n                        Err(e) => e,\n                    };\n                    let native = match err.try_native(context) {\n                        Ok(err) => err,\n                        Err(e) => panic!(\n                            \"{}\\nCouldn't obtain a native error: {e}\",\n                            fmt_test(&source, i)\n                        ),\n                    };\n\n                    assert_eq!(&native.kind, &kind, \"{}\", fmt_test(&source, i));\n                    assert_eq!(native.message(), message, \"{}\", fmt_test(&source, i));\n                    i += 1;\n                }\n                Inner::AssertContext { op } => {\n                    assert!(op(context), \"Test case {i}\");\n                    i += 1;\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "backend/boa_utils/src/module/builtin/utils.js",
    "content": "import dedent from 'nyan:dedent'\nimport YAML from 'nyan:yaml'\n\n/**\n * Parse template string into YAML object\n * @param {TemplateStringsArray} strings Template string array\n * @param {...any} values Template string interpolation values\n * @returns {Object} Parsed YAML object\n */\nexport function yaml(strings, ...values) {\n  const str = String.raw({ raw: strings }, ...values)\n  return YAML.parse(dedent(str))\n}\n"
  },
  {
    "path": "backend/boa_utils/src/module/builtin.rs",
    "content": "use std::{cell::RefCell, io::Read, rc::Rc};\n\nuse anyhow::Context as _;\nuse boa_engine::{Context, JsNativeError, JsResult, JsString, Module, module::ModuleLoader};\nuse boa_parser::Source;\nuse include_compress_bytes::include_bytes_brotli;\nuse include_url_macro::include_url_bytes_with_brotli;\nuse phf::phf_map;\n\npub(crate) const BUILTIN_MODULE_PREFIX: &str = \"nyan:\";\n\nstatic BUILTIN_MODULES: phf::Map<&str, &[u8]> = phf_map! {\n    // Remote resources\n    \"es-toolkit\" => include_url_bytes_with_brotli!(\"https://fastly.jsdelivr.net/npm/es-toolkit@1.39.10/+esm\"),\n    \"yaml\" => include_url_bytes_with_brotli!(\"https://fastly.jsdelivr.net/npm/yaml@2.8.1/+esm\"),\n    \"dedent\" => include_url_bytes_with_brotli!(\"https://fastly.jsdelivr.net/npm/dedent@1.7.0/+esm\"),\n    \"js-base64\" => include_url_bytes_with_brotli!(\"https://fastly.jsdelivr.net/npm/js-base64@3.7.8/+esm\"),\n\n    // Local utils,\n    \"utils\" => include_bytes_brotli!(\"./builtin/utils.js\"),\n};\n\n/// A ModuleLoader load resources from builtin static resources\npub struct BuiltinModuleLoader;\n\nimpl ModuleLoader for BuiltinModuleLoader {\n    async fn load_imported_module(\n        self: Rc<Self>,\n        _referrer: boa_engine::module::Referrer,\n        specifier: JsString,\n        context: &RefCell<&mut Context>,\n    ) -> JsResult<Module> {\n        let specifier_str = specifier.to_std_string_escaped();\n        let result: Result<_, anyhow::Error> = (|| {\n            let module_name = specifier_str\n                .strip_prefix(BUILTIN_MODULE_PREFIX)\n                .context(\"Not builtin module prefix\")?;\n            log::trace!(\"Trying to reading builtin module: {}\", module_name);\n            let module_data = BUILTIN_MODULES\n                .get(module_name)\n                .context(\"Builtin module not found\")?;\n            let mut data = Vec::with_capacity(1024 * 8);\n            {\n                let mut reader = brotli::Decompressor::new(&**module_data, 4096);\n                let mut buf = [0u8; 1024 * 8];\n                loop {\n                    match reader.read(&mut buf) {\n                        Ok(0) => break,\n                        Ok(read) => data.extend_from_slice(&buf[..read]),\n                        Err(e) if e.kind() == std::io::ErrorKind::Interrupted => {\n                            continue;\n                        }\n                        Err(err) => Err(err).context(\"failed to decode br stream\")?,\n                    }\n                }\n            }\n            Ok(data)\n        })();\n\n        let data = result.map_err(|err| {\n            log::error!(\"Failed to loading builtin module: {}\", specifier_str);\n            JsNativeError::typ().with_message(err.to_string())\n        })?;\n\n        log::trace!(\"Finishing loading builtin module: {}\", specifier_str);\n        let source = Source::from_bytes(&data);\n        Module::parse(source, None, &mut context.borrow_mut())\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use boa_engine::{JsValue, job::SimpleJobExecutor};\n\n    use super::*;\n\n    #[test_log::test]\n    fn test_builtin_module_loader() -> JsResult<()> {\n        use boa_engine::{builtins::promise::PromiseState, js_string};\n        use std::rc::Rc;\n\n        // A simple snippet that imports modules from the web instead of the file system.\n        const SRC: &str = r#\"\n            import { isEqual } from 'nyan:es-toolkit';\n            import dedent from 'nyan:dedent';\n            import YAML from 'nyan:yaml';\n            import { Base64 } from 'nyan:js-base64';\n\n            if (isEqual(1, 2)) {\n                throw new Error('Wrong isEqual implementation');\n            }\n\n            const data = dedent`\n                object:\n                    array: [\"hello\", \"world\"]\n                    key: \"value\"\n            `;\n\n            const object = YAML.parse(data).object;\n\n            let result = [\n                Base64.encode(object.array[0]),\n                Base64.encode(object.array[1]),\n            ]\n\n            export default result;\n        \"#;\n\n        let queue = Rc::new(SimpleJobExecutor::new());\n        let mut context = Context::builder()\n            .job_executor(queue)\n            // NEW: sets the context module loader to our custom loader\n            .module_loader(Rc::new(BuiltinModuleLoader))\n            .build()?;\n\n        let module = Module::parse(Source::from_bytes(SRC.as_bytes()), None, &mut context)?;\n\n        // Calling `Module::load_link_evaluate` takes care of having to define promise handlers for\n        // `Module::load` and `Module::evaluate`.\n        let promise = module.load_link_evaluate(&mut context);\n\n        // Important to call `Context::run_jobs`, or else all the futures and promises won't be\n        // pushed forward by the job queue.\n        let _ = context.run_jobs();\n\n        match promise.state() {\n            // Our job queue guarantees that all promises and futures are finished after returning\n            // from `Context::run_jobs`.\n            // Some other job queue designs only execute a \"microtick\" or a single pass through the\n            // pending promises and futures. In that case, you can pass this logic as a promise handler\n            // for `promise` instead.\n            PromiseState::Pending => panic!(\"module didn't execute!\"),\n            // All modules after successfully evaluating return `JsValue::undefined()`.\n            PromiseState::Fulfilled(v) => {\n                assert_eq!(v, JsValue::undefined())\n            }\n            PromiseState::Rejected(err) => {\n                panic!(\"{:#?}: {}\", err.display_obj(false), err.display());\n            }\n        }\n\n        let default = module\n            .namespace(&mut context)\n            .get(js_string!(\"default\"), &mut context)?;\n\n        // `default` should contain the result of our calculations.\n        let default = default\n            .as_object()\n            .ok_or_else(|| JsNativeError::typ().with_message(\"default export was not an object\"))?;\n\n        assert_eq!(\n            default.get(0, &mut context)?.as_string().ok_or_else(\n                || JsNativeError::typ().with_message(\"array element was not a string\")\n            )?,\n            js_string!(\"aGVsbG8=\")\n        );\n        assert_eq!(\n            default.get(1, &mut context)?.as_string().ok_or_else(\n                || JsNativeError::typ().with_message(\"array element was not a string\")\n            )?,\n            js_string!(\"d29ybGQ=\")\n        );\n\n        Ok(())\n    }\n\n    #[test_log::test]\n    fn test_builtin_utils() -> JsResult<()> {\n        use boa_engine::{builtins::promise::PromiseState, js_string};\n        use std::rc::Rc;\n\n        // A simple snippet that imports modules from the web instead of the file system.\n        const SRC: &str = r#\"\n            import { yaml } from 'nyan:utils';\n            import { Base64 } from 'nyan:js-base64';\n\n            const data = yaml`\n                object:\n                    array: [\"hello\", \"world\"]\n                    key: \"value\"\n            `;\n\n            const object = data.object;\n\n            let result = [\n                Base64.encode(object.array[0]),\n                Base64.encode(object.array[1]),\n            ]\n\n            export default result;\n        \"#;\n\n        let queue = Rc::new(SimpleJobExecutor::new());\n        let mut context = Context::builder()\n            .job_executor(queue)\n            // NEW: sets the context module loader to our custom loader\n            .module_loader(Rc::new(BuiltinModuleLoader))\n            .build()?;\n\n        let module = Module::parse(Source::from_bytes(SRC.as_bytes()), None, &mut context)?;\n\n        // Calling `Module::load_link_evaluate` takes care of having to define promise handlers for\n        // `Module::load` and `Module::evaluate`.\n        let promise = module.load_link_evaluate(&mut context);\n\n        // Important to call `Context::run_jobs`, or else all the futures and promises won't be\n        // pushed forward by the job queue.\n        let _ = context.run_jobs();\n\n        match promise.state() {\n            // Our job queue guarantees that all promises and futures are finished after returning\n            // from `Context::run_jobs`.\n            // Some other job queue designs only execute a \"microtick\" or a single pass through the\n            // pending promises and futures. In that case, you can pass this logic as a promise handler\n            // for `promise` instead.\n            PromiseState::Pending => panic!(\"module didn't execute!\"),\n            // All modules after successfully evaluating return `JsValue::undefined()`.\n            PromiseState::Fulfilled(v) => {\n                assert_eq!(v, JsValue::undefined())\n            }\n            PromiseState::Rejected(err) => {\n                panic!(\"{:#?}: {}\", err.display_obj(false), err.display());\n            }\n        }\n\n        let default = module\n            .namespace(&mut context)\n            .get(js_string!(\"default\"), &mut context)?;\n\n        // `default` should contain the result of our calculations.\n        let default = default\n            .as_object()\n            .ok_or_else(|| JsNativeError::typ().with_message(\"default export was not an object\"))?;\n\n        assert_eq!(\n            default.get(0, &mut context)?.as_string().ok_or_else(\n                || JsNativeError::typ().with_message(\"array element was not a string\")\n            )?,\n            js_string!(\"aGVsbG8=\")\n        );\n        assert_eq!(\n            default.get(1, &mut context)?.as_string().ok_or_else(\n                || JsNativeError::typ().with_message(\"array element was not a string\")\n            )?,\n            js_string!(\"d29ybGQ=\")\n        );\n\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "backend/boa_utils/src/module/combine.rs",
    "content": "use std::{cell::RefCell, rc::Rc};\n\nuse boa_engine::{Context, JsResult, JsString, Module, module::ModuleLoader};\nuse url::Url;\n\nuse crate::module::builtin::{BUILTIN_MODULE_PREFIX, BuiltinModuleLoader};\n\npub struct CombineModuleLoader {\n    simple: Rc<boa_engine::module::SimpleModuleLoader>,\n    http: Rc<super::http::HttpModuleLoader>,\n    builtin: Rc<super::builtin::BuiltinModuleLoader>,\n}\n\nimpl CombineModuleLoader {\n    pub fn new(\n        simple: boa_engine::module::SimpleModuleLoader,\n        http: super::http::HttpModuleLoader,\n    ) -> Self {\n        Self {\n            simple: Rc::new(simple),\n            http: Rc::new(http),\n            builtin: Rc::new(BuiltinModuleLoader),\n        }\n    }\n\n    pub fn clone_simple(&self) -> Rc<boa_engine::module::SimpleModuleLoader> {\n        self.simple.clone()\n    }\n\n    pub fn clone_http(&self) -> Rc<super::http::HttpModuleLoader> {\n        self.http.clone()\n    }\n}\n\nimpl ModuleLoader for CombineModuleLoader {\n    async fn load_imported_module(\n        self: Rc<Self>,\n        referrer: boa_engine::module::Referrer,\n        specifier: JsString,\n        context: &RefCell<&mut Context>,\n    ) -> JsResult<Module> {\n        let specifier_str = specifier.to_std_string_escaped();\n        match Url::parse(&specifier_str) {\n            Ok(url) if url.scheme() == \"http\" || url.scheme() == \"https\" => {\n                self.http\n                    .clone()\n                    .load_imported_module(referrer, specifier, context)\n                    .await\n            }\n            _ => {\n                if specifier_str.starts_with(BUILTIN_MODULE_PREFIX) {\n                    self.builtin\n                        .clone()\n                        .load_imported_module(referrer, specifier, context)\n                        .await\n                } else {\n                    self.simple\n                        .clone()\n                        .load_imported_module(referrer, specifier, context)\n                        .await\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "backend/boa_utils/src/module/http.rs",
    "content": "use std::{\n    cell::RefCell,\n    path::PathBuf,\n    rc::Rc,\n    str::FromStr,\n    time::{Duration, SystemTime},\n};\n\nuse async_fs::create_dir_all;\nuse boa_engine::{Context, JsNativeError, JsResult, JsString, Module, module::ModuleLoader};\nuse boa_parser::Source;\nuse mime::Mime;\n// Tokio sync is not runtime related\nuse tokio::sync::oneshot::channel as oneshot_channel;\nuse url::Url;\n\n// Most of the boilerplate is taken from the `futures.rs` example.\n// This file only explains what is exclusive of async module loading.\n\n#[derive(Debug, Default)]\npub struct HttpModuleLoader {\n    cache_dir: PathBuf,\n    max_age: Duration,\n}\n\n#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)]\npub struct CachedItem {\n    pub mime: String,\n    /// raw string content\n    /// We have no plan for now to support binary content,\n    /// so we just use `String` to store the content.\n    pub content: String,\n}\n\nimpl HttpModuleLoader {\n    pub fn new(cache_dir: PathBuf, max_age: Duration) -> Self {\n        Self { cache_dir, max_age }\n    }\n\n    fn mapping_cache_dir(&self, url: &url::Url) -> PathBuf {\n        let mut buf = self.cache_dir.clone();\n        let host = match url.host() {\n            Some(host) => host.to_string().replace('.', \"--\"),\n            None => \"unknown\".to_string(),\n        };\n        let port = match url.port() {\n            Some(port) => format!(\"__{port}\"),\n            None => \"\".to_string(),\n        };\n        buf.push(format!(\"{}_{}{}\", url.scheme(), host, port));\n        buf.push(url.path().replace('/', \"_\").replace(\".\", \"--\"));\n        buf\n    }\n\n    #[tracing::instrument(skip(context))]\n    fn handle_cached_item(item: CachedItem, context: &mut Context) -> JsResult<Module> {\n        let mime = Mime::from_str(item.mime.as_str()).map_err(|_| {\n            log::error!(\"failed to parse mime type `{}`\", item.mime);\n            JsNativeError::typ().with_message(\"failed to parse mime type\")\n        })?;\n\n        let source_str = match (mime.type_(), mime.subtype()) {\n            (mime::APPLICATION, mime::JAVASCRIPT) => item.content.clone(),\n            (mime::APPLICATION, mime::JSON) => {\n                format!(\"export default {};\", item.content)\n            }\n            _ => {\n                let escaped_str = serde_json::to_string(&item.content).map_err(|_| {\n                    log::error!(\"failed to serialize content.\");\n                    JsNativeError::typ().with_message(\"failed to serialize content\")\n                })?;\n                format!(\"export const text = {escaped_str};\")\n            }\n        };\n\n        // Could also add a path if needed.\n        let source = Source::from_bytes(source_str.as_bytes());\n\n        Module::parse(source, None, context)\n    }\n}\n\nimpl ModuleLoader for HttpModuleLoader {\n    async fn load_imported_module(\n        self: Rc<Self>,\n        _referrer: boa_engine::module::Referrer,\n        specifier: JsString,\n        context: &RefCell<&mut Context>,\n    ) -> JsResult<Module> {\n        let url = specifier.to_std_string_escaped();\n        let url = Url::from_str(&url).expect(\"invalid url\"); // SAFETY: `url` is a valid URL, if it's not, its caller side issue\n        let cache_path = self.mapping_cache_dir(&url);\n        let parent_dir = cache_path\n            .parent()\n            .ok_or_else(|| {\n                log::error!(\"failed to get parent directory for `{url}`\");\n                JsNativeError::typ().with_message(format!(\n                    \"failed to get cache parent directory for `{url}`; path: `{}`\",\n                    cache_path.display()\n                ))\n            })?\n            .to_path_buf();\n\n        let max_age = self.max_age;\n\n        log::debug!(\"checking cache for `{url}`...\");\n\n        let now = SystemTime::now();\n        let should_use_cached_content = match async_fs::metadata(&cache_path).await {\n            Ok(metadata)\n                if metadata\n                    .modified()\n                    .is_ok_and(|modified| modified > now - max_age) =>\n            {\n                true\n            }\n            Err(err) => {\n                // create dir if not exists\n                if err.kind() == std::io::ErrorKind::NotFound\n                    && let Err(e) = async_fs::metadata(&parent_dir).await\n                    && e.kind() == std::io::ErrorKind::NotFound\n                    && let Err(err) = create_dir_all(parent_dir).await\n                {\n                    log::error!(\n                        \"failed to create cache directory for `{url}`; path: `{}`. error: `{}`\",\n                        cache_path.display(),\n                        err\n                    );\n                }\n                false\n            }\n            _ => false,\n        };\n\n        let item: anyhow::Result<CachedItem> = if should_use_cached_content {\n            async {\n                log::debug!(\"fetching `{url}` from cache...\");\n                let item = async_fs::read(&cache_path).await?;\n                let item = postcard::from_bytes(&item)?;\n                log::debug!(\"finished fetching `{url}` from cache\");\n                Ok(item)\n            }\n            .await\n        } else {\n            log::debug!(\"fetching `{url}`...\");\n            let (tx, rx) = oneshot_channel();\n            let fetcher_url = url.clone();\n            nyanpasu_utils::runtime::spawn(async move {\n                let result = async {\n                    let response = reqwest::Client::builder()\n                        .redirect(reqwest::redirect::Policy::limited(5))\n                        .build()?\n                        .get(fetcher_url.as_str())\n                        .send()\n                        .await?;\n\n                    let mime = response\n                        .headers()\n                        .get(reqwest::header::CONTENT_TYPE)\n                        .and_then(|v| v.to_str().ok())\n                        .map(|v| v.to_string())\n                        .unwrap_or(mime::TEXT_PLAIN.to_string());\n                    let body = response.text().await?;\n\n                    log::debug!(\"finished fetching `{fetcher_url}`\");\n                    Ok(CachedItem {\n                        mime,\n                        content: body,\n                    })\n                }\n                .await;\n                let _ = tx.send(result);\n            });\n            rx.await.expect(\"should never drop oneshot tx\")\n        };\n\n        if let Ok(item) = &item {\n            match postcard::to_stdvec(&item) {\n                Ok(item) => {\n                    if let Err(err) = async_fs::write(&cache_path, &item).await {\n                        log::error!(\n                            \"failed to write cache for `{url}`; path: `{}`. error: `{}`\",\n                            cache_path.display(),\n                            err\n                        );\n                    }\n                }\n                Err(err) => {\n                    log::error!(\"failed to serialize content: {err}\");\n                }\n            }\n        }\n\n        let item = item.map_err(|err| JsNativeError::typ().with_message(err.to_string()))?;\n        Self::handle_cached_item(item, &mut context.borrow_mut())\n    }\n}\n\n#[test]\nfn test_http_module_loader() -> JsResult<()> {\n    use boa_engine::{builtins::promise::PromiseState, job::SimpleJobExecutor, js_string};\n    use std::rc::Rc;\n    let temp_dir = tempfile::tempdir().unwrap();\n    // A simple snippet that imports modules from the web instead of the file system.\n    const SRC: &str = r#\"\n        import YAML from 'https://esm.run/yaml@2.3.4';\n        import fromAsync from 'https://esm.run/array-from-async@3.0.0';\n        import { Base64 } from 'https://esm.run/js-base64@3.7.6';\n        // Test toolkit\n        import { isEqual } from 'https://esm.run/es-toolkit@1.39.10';\n        import { text } from 'https://github.com/libnyanpasu/clash-nyanpasu/raw/refs/heads/main/pnpm-workspace.yaml';\n\n        if (isEqual(1, 2)) {\n            throw new Error('Wrong isEqual implementation');\n        }\n\n        const data = `\n            object:\n                array: [\"hello\", \"world\"]\n                key: \"value\"\n        `;\n\n        const object = YAML.parse(data).object;\n\n        let result = await fromAsync([\n            Promise.resolve(Base64.encode(object.array[0])),\n            Promise.resolve(Base64.encode(object.array[1])),\n        ]);\n\n        const parsed = YAML.parse(text);\n        result.push(JSON.stringify(parsed));\n\n        export default result;\n    \"#;\n\n    let queue = Rc::new(SimpleJobExecutor::new());\n    let mut context = Context::builder()\n        .job_executor(queue)\n        // NEW: sets the context module loader to our custom loader\n        .module_loader(Rc::new(HttpModuleLoader::new(\n            temp_dir.path().to_path_buf(),\n            Duration::from_secs(10),\n        )))\n        .build()?;\n\n    let module = Module::parse(Source::from_bytes(SRC.as_bytes()), None, &mut context)?;\n\n    // Calling `Module::load_link_evaluate` takes care of having to define promise handlers for\n    // `Module::load` and `Module::evaluate`.\n    let promise = module.load_link_evaluate(&mut context);\n\n    // Important to call `Context::run_jobs`, or else all the futures and promises won't be\n    // pushed forward by the job queue.\n    let _ = context.run_jobs();\n\n    match promise.state() {\n        // Our job queue guarantees that all promises and futures are finished after returning\n        // from `Context::run_jobs`.\n        // Some other job queue designs only execute a \"microtick\" or a single pass through the\n        // pending promises and futures. In that case, you can pass this logic as a promise handler\n        // for `promise` instead.\n        PromiseState::Pending => panic!(\"module didn't execute!\"),\n        // All modules after successfully evaluating return `JsValue::undefined()`.\n        PromiseState::Fulfilled(v) => {\n            assert_eq!(v, boa_engine::JsValue::undefined())\n        }\n        PromiseState::Rejected(err) => {\n            panic!(\"{}\", err.display());\n        }\n    }\n\n    let default = module\n        .namespace(&mut context)\n        .get(js_string!(\"default\"), &mut context)?;\n\n    // `default` should contain the result of our calculations.\n    let default = default\n        .as_object()\n        .ok_or_else(|| JsNativeError::typ().with_message(\"default export was not an object\"))?;\n\n    assert_eq!(\n        default\n            .get(0, &mut context)?\n            .as_string()\n            .ok_or_else(|| JsNativeError::typ().with_message(\"array element was not a string\"))?,\n        js_string!(\"aGVsbG8=\")\n    );\n    assert_eq!(\n        default\n            .get(1, &mut context)?\n            .as_string()\n            .ok_or_else(|| JsNativeError::typ().with_message(\"array element was not a string\"))?,\n        js_string!(\"d29ybGQ=\")\n    );\n    assert!(\n        default\n            .get(2, &mut context)?\n            .as_string()\n            .ok_or_else(|| JsNativeError::typ().with_message(\"array element was not a string\"))?\n            .to_std_string_escaped()\n            .contains(\"packages\"),\n        \"YAML content should contain 'packages' field\"\n    );\n\n    Ok(())\n}\n"
  },
  {
    "path": "backend/boa_utils/src/module/mod.rs",
    "content": "#![allow(dead_code)]\npub mod builtin;\npub mod combine;\npub mod http;\n"
  },
  {
    "path": "backend/nyanpasu-egui/.gitignore",
    "content": "/target\n"
  },
  {
    "path": "backend/nyanpasu-egui/Cargo.toml",
    "content": "[package]\nname = \"nyanpasu-egui\"\nversion = \"0.1.0\"\nedition = \"2024\"\n\n[lib]\nname = \"nyanpasu_egui\"\ncrate-type = [\"staticlib\", \"cdylib\", \"rlib\"]\n\n[[bin]]\nname = \"nyanpasu-network-statistic-widget-large\"\npath = \"./src/main.rs\"\n\n[[bin]]\nname = \"nyanpasu-network-statistic-widget-small\"\npath = \"./src/small.rs\"\n\n[dependencies]\neframe = { version = \"0.33.0\" }\negui_extras = { version = \"0.33.0\", features = [\"all_loaders\"] }\nparking_lot = \"0.12\"\nimage = { version = \"0.25.6\", features = [\"jpeg\", \"png\"] }\nhumansize = \"2\"\n# for svg currentColor replacement\nresvg = \"0.45.1\"                                            # for svg rendering\nusvg = \"0.45.1\"                                             # for svg parsing\ncsscolorparser = \"0.8\"                                      # for color conversion\nipc-channel = \"0.20\"                                        # for IPC between the Widget process and the GUI process\nserde = { version = \"1\", features = [\"derive\"] }\nanyhow = \"1\"\nspecta = { version = \"=2.0.0-rc.22\", features = [\"serde\"] }\nclap = { version = \"4\", features = [\"derive\"] }\n\n[target.'cfg(target_os = \"macos\")'.dependencies]\nobjc2 = \"0.6.1\"\nobjc2-foundation = \"0.3.1\"\nobjc2-app-kit = \"0.3.1\"\n"
  },
  {
    "path": "backend/nyanpasu-egui/src/ipc.rs",
    "content": "pub use ipc_channel::ipc::IpcSender;\nuse ipc_channel::ipc::{self, IpcReceiver};\n\nuse crate::widget::network_statistic_large::LogoPreset;\n\n#[derive(Debug, Default, serde::Deserialize, serde::Serialize)]\npub struct StatisticMessage {\n    pub download_total: u64,\n    pub upload_total: u64,\n    pub download_speed: u64,\n    pub upload_speed: u64,\n}\n\n#[derive(Debug, serde::Deserialize, serde::Serialize)]\npub enum Message {\n    Stop,\n    UpdateStatistic(StatisticMessage),\n    UpdateLogo(LogoPreset),\n}\n\npub struct IPCServer {\n    oneshot_server: Option<ipc::IpcOneShotServer<IpcSender<Message>>>,\n    tx: Option<IpcSender<Message>>,\n}\n\nimpl IPCServer {\n    pub fn is_connected(&self) -> bool {\n        self.tx.is_some()\n    }\n\n    pub fn connect(&mut self) -> anyhow::Result<()> {\n        if self.oneshot_server.is_none() {\n            anyhow::bail!(\"IPC server is already initialized\");\n        }\n\n        let (_, tx) = self.oneshot_server.take().unwrap().accept()?;\n        self.tx = Some(tx);\n        Ok(())\n    }\n\n    pub fn into_tx(self) -> Option<IpcSender<Message>> {\n        self.tx\n    }\n}\n\npub fn create_ipc_server() -> anyhow::Result<(IPCServer, String)> {\n    let (oneshot_server, oneshot_server_name) = ipc::IpcOneShotServer::new()?;\n    Ok((\n        IPCServer {\n            oneshot_server: Some(oneshot_server),\n            tx: None,\n        },\n        oneshot_server_name,\n    ))\n}\n\npub(crate) fn setup_ipc_receiver(name: &str) -> anyhow::Result<IpcReceiver<Message>> {\n    let oneshot_sender: IpcSender<IpcSender<Message>> = ipc::IpcSender::connect(name.to_string())?;\n    let (tx, rx) = ipc::channel()?;\n    oneshot_sender.send(tx)?;\n    Ok(rx)\n}\n\npub(crate) fn setup_ipc_receiver_with_env() -> anyhow::Result<IpcReceiver<Message>> {\n    let name = std::env::var(\"NYANPASU_EGUI_IPC_SERVER\")?;\n    setup_ipc_receiver(&name)\n}\n"
  },
  {
    "path": "backend/nyanpasu-egui/src/lib.rs",
    "content": "#![feature(trait_alias)]\n\npub mod ipc;\nmod utils;\npub mod widget;\n"
  },
  {
    "path": "backend/nyanpasu-egui/src/main.rs",
    "content": "#![allow(dead_code)]\n#![cfg_attr(not(debug_assertions), windows_subsystem = \"windows\")] // hide console window on Windows in release\n#![allow(rustdoc::missing_crate_level_docs)] // it's an example\n\nuse eframe::egui;\nuse nyanpasu_egui::widget::NyanpasuNetworkStatisticLargeWidget;\n\nfn main() -> eframe::Result {\n    let options = eframe::NativeOptions {\n        viewport: egui::ViewportBuilder::default()\n            .with_inner_size([206.0, 60.0])\n            .with_decorations(false)\n            .with_transparent(true)\n            .with_always_on_top()\n            .with_drag_and_drop(true)\n            .with_resizable(false)\n            .with_taskbar(false),\n        ..Default::default()\n    };\n    eframe::run_native(\n        \"egui example: custom style\",\n        options,\n        Box::new(|cc| Ok(Box::new(NyanpasuNetworkStatisticLargeWidget::new(cc)))),\n    )\n}\n"
  },
  {
    "path": "backend/nyanpasu-egui/src/small.rs",
    "content": "#![allow(dead_code)]\n#![cfg_attr(not(debug_assertions), windows_subsystem = \"windows\")] // hide console window on Windows in release\n#![allow(rustdoc::missing_crate_level_docs)] // it's an example\n\nuse eframe::egui;\nuse nyanpasu_egui::widget::NyanpasuNetworkStatisticSmallWidget;\n\nfn main() -> eframe::Result {\n    let options = eframe::NativeOptions {\n        viewport: egui::ViewportBuilder::default()\n            .with_inner_size([80.0, 32.0])\n            .with_decorations(false)\n            .with_transparent(true)\n            .with_always_on_top()\n            .with_drag_and_drop(true)\n            .with_resizable(false)\n            .with_taskbar(false),\n        ..Default::default()\n    };\n    eframe::run_native(\n        \"egui example: custom style\",\n        options,\n        Box::new(|cc| Ok(Box::new(NyanpasuNetworkStatisticSmallWidget::new(cc)))),\n    )\n}\n"
  },
  {
    "path": "backend/nyanpasu-egui/src/utils/mod.rs",
    "content": "pub mod svg;\n"
  },
  {
    "path": "backend/nyanpasu-egui/src/utils/svg.rs",
    "content": "use csscolorparser::Color as CssColor;\nuse eframe::egui::ColorImage;\nuse resvg::tiny_skia::Pixmap;\nuse usvg::{Error, Options, Transform, Tree};\n\n// TODO: change hard coded replacement when https://github.com/RazrFalcon/resvg/issues/768 got resolved\npub fn parse_svg_with_current_color_replace<T: Into<CssColor>>(\n    svg: &str,\n    color: T,\n) -> Result<Tree, Error> {\n    let color: CssColor = color.into();\n    let svg = svg.replace(r#\"\"currentColor\"\"#, &format!(r#\"\"{}\"\"#, color.to_css_hex()));\n    Tree::from_str(svg.as_str(), &Options::default())\n}\n\npub fn render_svg(tree: &Tree, width: u32, height: u32) -> Result<Pixmap, Error> {\n    let mut pixmap = Pixmap::new(width, height).unwrap();\n    let original_width = tree.size().width();\n    let original_height = tree.size().height();\n    let scale_x = width as f32 / original_width;\n    let scale_y = height as f32 / original_height;\n    let transform = Transform::from_scale(scale_x, scale_y);\n    resvg::render(tree, transform, &mut pixmap.as_mut());\n    Ok(pixmap)\n}\n\npub fn render_svg_with_current_color_replace<T: Into<CssColor>>(\n    svg: &str,\n    color: T,\n    width: u32,\n    height: u32,\n) -> Result<Pixmap, Error> {\n    let tree = parse_svg_with_current_color_replace(svg, color)?;\n    render_svg(&tree, width, height)\n}\n\npub struct SvgWrapper<'a>(pub &'a Pixmap);\n\nimpl<'a> From<&'a Pixmap> for SvgWrapper<'a> {\n    fn from(pixmap: &'a Pixmap) -> Self {\n        SvgWrapper(pixmap)\n    }\n}\n\n#[allow(clippy::wrong_self_convention)]\npub trait SvgExt {\n    fn into_wrapper(&self) -> SvgWrapper<'_>;\n}\n\nimpl SvgExt for Pixmap {\n    fn into_wrapper(&self) -> SvgWrapper<'_> {\n        SvgWrapper(self)\n    }\n}\n\nimpl SvgWrapper<'_> {\n    pub fn into_egui_image(self) -> eframe::egui::ColorImage {\n        let (width, height) = (self.0.width(), self.0.height());\n        let pixels = self.0.pixels();\n        let mut image_data = Vec::with_capacity(width as usize * height as usize * 4);\n        for pixel in pixels {\n            image_data.push(pixel.red());\n            image_data.push(pixel.green());\n            image_data.push(pixel.blue());\n            image_data.push(pixel.alpha());\n        }\n\n        ColorImage::from_rgba_unmultiplied([width as usize, height as usize], &image_data)\n    }\n}\n"
  },
  {
    "path": "backend/nyanpasu-egui/src/widget/mod.rs",
    "content": "pub mod network_statistic_large;\npub mod network_statistic_small;\n\nuse std::path::PathBuf;\n\npub use network_statistic_large::NyanpasuNetworkStatisticLargeWidget;\npub use network_statistic_small::NyanpasuNetworkStatisticSmallWidget;\n\nfn get_window_state_path() -> std::io::Result<PathBuf> {\n    let env = std::env::var(\"NYANPASU_EGUI_WINDOW_STATE_PATH\").map_err(|_| {\n        std::io::Error::new(\n            std::io::ErrorKind::NotFound,\n            \"NYANPASU_EGUI_WINDOW_STATE_PATH is not set\",\n        )\n    })?;\n\n    let path = PathBuf::from(env);\n    Ok(path)\n}\n\n#[cfg(target_os = \"macos\")]\n// TODO: move this to nyanpasu-utils\nfn set_application_activation_policy() {\n    use objc2_app_kit::{NSApplication, NSApplicationActivationPolicy};\n    use objc2_foundation::MainThreadMarker;\n    use std::cell::Cell;\n    thread_local! {\n        static MARK: Cell<MainThreadMarker> = Cell::new(MainThreadMarker::new().unwrap());\n    }\n\n    let app = NSApplication::sharedApplication(MARK.get());\n    app.setActivationPolicy(NSApplicationActivationPolicy::Accessory);\n    unsafe {\n        app.activate();\n    }\n}\n\n// pub fn launch_widget<'app, T: Send + Sync + Sized, A: EframeAppCreator<'app, T>>(\n//     name: &str,\n//     opts: eframe::NativeOptions,\n//     creator: A,\n// ) -> std::io::Result<Receiver<WidgetEvent<T>>> {\n//     let (tx, rx) = mpsc::channel();\n// }\n\n#[derive(\n    Debug,\n    serde::Serialize,\n    serde::Deserialize,\n    specta::Type,\n    Copy,\n    Clone,\n    PartialEq,\n    Eq,\n    clap::ValueEnum,\n)]\n#[serde(rename_all = \"snake_case\")]\npub enum StatisticWidgetVariant {\n    Large,\n    Small,\n}\n\nimpl std::fmt::Display for StatisticWidgetVariant {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        match self {\n            StatisticWidgetVariant::Large => write!(f, \"large\"),\n            StatisticWidgetVariant::Small => write!(f, \"small\"),\n        }\n    }\n}\n\npub fn start_statistic_widget(size: StatisticWidgetVariant) -> eframe::Result {\n    match size {\n        StatisticWidgetVariant::Large => NyanpasuNetworkStatisticLargeWidget::run(),\n        StatisticWidgetVariant::Small => NyanpasuNetworkStatisticSmallWidget::run(),\n    }\n}\n"
  },
  {
    "path": "backend/nyanpasu-egui/src/widget/network_statistic_large.rs",
    "content": "#![allow(dead_code)]\nuse std::sync::{Arc, LazyLock};\n\nuse crate::{\n    ipc::Message,\n    utils::svg::{SvgExt, render_svg_with_current_color_replace},\n};\nuse eframe::{\n    egui::{\n        self, Color32, CornerRadius, Id, Image, Label, Layout, Margin, Sense, Stroke, Style,\n        TextWrapMode, TextureOptions, Theme, ThemePreference, Vec2, ViewportCommand, Visuals,\n        style::Selection,\n    },\n    epaint::CornerRadiusF32,\n};\nuse parking_lot::RwLock;\n\n// Presets\nconst STATUS_ICON_CONTAINER_WIDTH: f32 = 20.0;\nconst STATUS_ICON_WIDTH: f32 = 12.0;\nconst LOGO_CONTAINER_WIDTH: f32 = 44.0;\nconst LOGO_SIZE: Vec2 = Vec2::new(26.0, 31.0);\n\n// Themes\nconst GLOBAL_ALPHA: u8 = 128;\nconst LIGHT_MODE_BACKGROUND_COLOR: Color32 =\n    Color32::from_rgba_premultiplied(254, 247, 255, GLOBAL_ALPHA);\nconst DARK_MODE_TEXT_COLOR: Color32 = Color32::from_rgb(254, 247, 255);\nconst DARK_MODE_BACKGROUND_COLOR: Color32 =\n    Color32::from_rgba_premultiplied(29, 27, 32, GLOBAL_ALPHA);\nconst DARK_MODE_STATUS_SHEET_COLOR: Color32 =\n    Color32::from_rgba_premultiplied(73, 69, 79, GLOBAL_ALPHA);\nconst STATUS_ICON_CONTAINER_COLOR: Color32 = Color32::from_rgb(79, 55, 139);\nstatic LOGO_CONTAINER_COLOR: LazyLock<Color32> =\n    LazyLock::new(|| Color32::from_rgba_unmultiplied(79, 55, 139, GLOBAL_ALPHA));\n\n// Icons\nconst DOWNLOAD_ICON: &[u8] = include_bytes!(\"../../assets/download.svg\");\nconst UPLOAD_ICON: &[u8] = include_bytes!(\"../../assets/upload.svg\");\nconst UP_ICON: &[u8] = include_bytes!(\"../../assets/up.svg\");\nconst DOWN_ICON: &[u8] = include_bytes!(\"../../assets/down.svg\");\n\nfn setup_custom_style(ctx: &egui::Context) {\n    ctx.style_mut_of(Theme::Light, use_light_green_accent);\n    ctx.style_mut_of(Theme::Dark, use_dark_purple_accent);\n    ctx.options_mut(|opts| {\n        // set theme preference to dark, avoid system theme\n        opts.theme_preference = ThemePreference::Dark;\n    });\n}\n\nfn setup_fonts(ctx: &egui::Context) {\n    let mut fonts = egui::FontDefinitions::default();\n\n    fonts.font_data.insert(\n        \"Inter\".to_owned(),\n        Arc::new(egui::FontData::from_static(include_bytes!(\n            \"../../assets/Inter-Regular.ttf\"\n        ))),\n    );\n\n    fonts\n        .families\n        .entry(egui::FontFamily::Proportional)\n        .or_default()\n        .insert(0, \"Inter\".to_owned());\n\n    ctx.set_fonts(fonts);\n}\n\nfn use_global_styles(styles: &mut Style) {\n    for (text_style, font_id) in styles.text_styles.iter_mut() {\n        if matches!(text_style, egui::TextStyle::Body) {\n            font_id.size = 10.0;\n        }\n    }\n    styles.spacing.window_margin = Margin::same(0);\n    styles.spacing.item_spacing = Vec2::new(0.0, 0.0);\n    styles.interaction.selectable_labels = false; // disable text selection\n}\n\nfn use_light_green_accent(style: &mut Style) {\n    use_global_styles(style);\n    style.visuals.override_text_color = Some(DARK_MODE_TEXT_COLOR);\n    style.visuals.hyperlink_color = Color32::from_rgb(18, 180, 85);\n    style.visuals.text_cursor.stroke.color = Color32::from_rgb(28, 92, 48);\n    style.visuals.selection = Selection {\n        bg_fill: Color32::from_rgb(157, 218, 169),\n        stroke: Stroke::new(1.0, Color32::from_rgb(28, 92, 48)),\n    };\n}\n\nfn use_dark_purple_accent(style: &mut Style) {\n    use_global_styles(style);\n    style.visuals.override_text_color = Some(DARK_MODE_TEXT_COLOR);\n    style.visuals.hyperlink_color = Color32::from_rgb(202, 135, 227);\n    style.visuals.text_cursor.stroke.color = Color32::from_rgb(234, 208, 244);\n    style.visuals.selection = Selection {\n        bg_fill: Color32::from_rgb(105, 67, 119),\n        stroke: Stroke::new(1.0, Color32::from_rgb(234, 208, 244)),\n    };\n}\n\n#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Deserialize, serde::Serialize)]\n#[serde(rename_all = \"snake_case\")]\npub enum LogoPreset {\n    #[default]\n    Default,\n    System,\n    Tun,\n}\n\n#[derive(Debug)]\npub struct NyanpasuNetworkStatisticLargeWidgetInner {\n    // data fields\n    logo_preset: LogoPreset,\n    download_total: u64,\n    upload_total: u64,\n    download_speed: u64,\n    upload_speed: u64,\n\n    // eframe ctx\n    egui_ctx: egui::Context,\n}\n\nimpl NyanpasuNetworkStatisticLargeWidgetInner {\n    fn request_repaint(&self) {\n        self.egui_ctx.request_repaint();\n    }\n}\n\n#[derive(Debug, Clone)]\npub struct NyanpasuNetworkStatisticLargeWidget {\n    inner: Arc<RwLock<NyanpasuNetworkStatisticLargeWidgetInner>>,\n}\n\nimpl NyanpasuNetworkStatisticLargeWidget {\n    pub fn new(cc: &eframe::CreationContext<'_>) -> Self {\n        cc.egui_ctx.set_visuals(Visuals::dark());\n        setup_fonts(&cc.egui_ctx);\n        setup_custom_style(&cc.egui_ctx);\n        egui_extras::install_image_loaders(&cc.egui_ctx);\n        let rx = crate::ipc::setup_ipc_receiver_with_env().unwrap();\n        let widget = NyanpasuNetworkStatisticLargeWidget {\n            inner: Arc::new(RwLock::new(NyanpasuNetworkStatisticLargeWidgetInner {\n                egui_ctx: cc.egui_ctx.clone(),\n                logo_preset: LogoPreset::default(),\n                download_total: 0,\n                upload_total: 0,\n                download_speed: 0,\n                upload_speed: 0,\n            })),\n        };\n        let this = widget.clone();\n        std::thread::spawn(move || {\n            loop {\n                match rx.recv() {\n                    Ok(msg) => {\n                        println!(\"Received message: {msg:?}\");\n                        let _ = this.handle_message(msg);\n                    }\n                    Err(e) => {\n                        eprintln!(\"Failed to receive message: {e}\");\n                        if matches!(\n                            e,\n                            ipc_channel::ipc::IpcError::Disconnected\n                                | ipc_channel::ipc::IpcError::Io(_)\n                        ) {\n                            let _ = this.handle_message(Message::Stop);\n                            break;\n                        }\n                    }\n                }\n            }\n        });\n        widget\n    }\n\n    pub fn run() -> eframe::Result {\n        #[cfg(target_os = \"macos\")]\n        super::set_application_activation_policy();\n\n        let options = eframe::NativeOptions {\n            viewport: egui::ViewportBuilder::default()\n                .with_inner_size([206.0, 60.0])\n                .with_decorations(false)\n                .with_transparent(true)\n                .with_always_on_top()\n                .with_drag_and_drop(true)\n                .with_resizable(false)\n                .with_taskbar(false),\n            run_and_return: false,\n            // persist_window: true,\n            // persistence_path: get_window_state_path().ok(),\n            ..Default::default()\n        };\n        println!(\"Running widget...\");\n        eframe::run_native(\n            \"Nyanpasu Network Statistic Widget\",\n            options,\n            Box::new(|cc| Ok(Box::new(NyanpasuNetworkStatisticLargeWidget::new(cc)))),\n        )\n    }\n\n    pub fn handle_message(&self, msg: Message) -> anyhow::Result<()> {\n        let mut this = self.inner.write();\n        match msg {\n            Message::UpdateStatistic(statistic) => {\n                this.download_total = statistic.download_total;\n                this.upload_total = statistic.upload_total;\n                this.download_speed = statistic.download_speed;\n                this.upload_speed = statistic.upload_speed;\n                this.request_repaint();\n            }\n            Message::UpdateLogo(logo_preset) => {\n                this.logo_preset = logo_preset;\n                this.request_repaint();\n            }\n            Message::Stop => {\n                std::thread::spawn(move || {\n                    // wait for 5 seconds to ensure the widget is closed, or the app will be terminated\n                    std::thread::sleep(std::time::Duration::from_secs(5));\n                    std::process::exit(0);\n                });\n                this.egui_ctx.send_viewport_cmd(ViewportCommand::Close);\n            }\n        }\n        Ok(())\n    }\n}\n\nimpl eframe::App for NyanpasuNetworkStatisticLargeWidget {\n    fn clear_color(&self, _visuals: &egui::Visuals) -> [f32; 4] {\n        egui::Rgba::TRANSPARENT.to_array()\n    }\n\n    fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {\n        let this = self.inner.read();\n        let visuals = &ctx.style().visuals;\n\n        egui::CentralPanel::default()\n            .frame(\n                egui::Frame::NONE\n                    .corner_radius(CornerRadius::same(12))\n                    .fill(if visuals.dark_mode { DARK_MODE_BACKGROUND_COLOR } else { LIGHT_MODE_BACKGROUND_COLOR })\n                    .inner_margin(Margin::symmetric(9, 6)),\n            )\n            .show(ctx, |ui| {\n                if ui.interact(ui.max_rect(), Id::new(\"window-drag\"), Sense::drag()).dragged() {\n                    ctx.send_viewport_cmd(ViewportCommand::StartDrag);\n                }\n\n                let available_height = ui.available_height();\n                ui.horizontal_centered(|ui| {\n                    let width = ui.available_width();\n\n                    // LOGO Column\n                    ui.allocate_ui_with_layout(\n                        egui::Vec2::new(LOGO_CONTAINER_WIDTH, LOGO_CONTAINER_WIDTH),\n                        egui::Layout::centered_and_justified(egui::Direction::TopDown),\n                        |ui| {\n                            egui::Frame::NONE.fill(*LOGO_CONTAINER_COLOR).corner_radius(CornerRadiusF32::same(LOGO_CONTAINER_WIDTH / 2.0)).show(ui, |ui| {\n                                ui.centered_and_justified(|ui| {\n                                    ui.add(Image::new(eframe::egui::include_image!(\"../../assets/tray-icon.png\")).max_size(LOGO_SIZE));\n                                });\n                            });\n                        },\n                    );\n\n                    let grid_gap = 7.0;\n\n                    ui.add_space(grid_gap); // gap\n\n                    let col_width = (width - LOGO_CONTAINER_WIDTH - grid_gap * 2.0) / 2.0;\n                    let row_height = STATUS_ICON_CONTAINER_WIDTH;\n                    let vertical_padding = LOGO_CONTAINER_WIDTH - row_height * 2.0;\n                    let top_gap = (available_height - (row_height * 2.0 + vertical_padding)) / 2.0;\n\n                    // Download Column\n                    ui.allocate_ui_with_layout(egui::Vec2::new(col_width, available_height), egui::Layout::top_down(egui::Align::LEFT), |ui| {\n                        ui.add_space(top_gap);\n                        // Download Total\n                        ui.allocate_ui_with_layout(egui::Vec2::new(col_width, row_height), Layout::left_to_right(egui::Align::Center), |ui| {\n                            egui::Frame::NONE.corner_radius(CornerRadius::same(14)).fill(DARK_MODE_STATUS_SHEET_COLOR).show(ui, |ui| {\n                                ui.allocate_ui(egui::Vec2::new(STATUS_ICON_CONTAINER_WIDTH, STATUS_ICON_CONTAINER_WIDTH), |ui| {\n                                    egui::Frame::NONE\n                                        .fill(STATUS_ICON_CONTAINER_COLOR)\n                                        .corner_radius(CornerRadius::same(STATUS_ICON_WIDTH as u8))\n                                        .show(ui, |ui| {\n                                            let image = render_svg_with_current_color_replace(\n                                                unsafe { String::from_utf8_unchecked(DOWNLOAD_ICON.to_vec()) }.as_str(),\n                                                csscolorparser::parse(&DARK_MODE_TEXT_COLOR.to_hex()).unwrap(),\n                                                (STATUS_ICON_WIDTH).round() as u32,\n                                                (STATUS_ICON_WIDTH).round() as u32,\n                                            )\n                                            .unwrap()\n                                            .into_wrapper()\n                                            .into_egui_image();\n                                            let texture_handle = ui.ctx().load_texture(\"download_icon\", image, TextureOptions::default());\n                                            ui.centered_and_justified(|ui| {\n                                                ui.add(Image::from_texture(&texture_handle));\n                                            });\n                                        });\n                                });\n                                let width = ui.available_width();\n                                let height = ui.available_height();\n                                ui.allocate_ui_with_layout(egui::Vec2::new(width, height), Layout::centered_and_justified(egui::Direction::TopDown), |ui| {\n                                    ui.add(\n                                        Label::new(\n                                            humansize::format_size(this.download_total, humansize::DECIMAL))\n                                            .wrap_mode(TextWrapMode::Extend)\n                                    );\n                                });\n                            });\n                        });\n\n                        ui.add_space(vertical_padding); // gap\n\n                        // Download Speed\n                        ui.allocate_ui_with_layout(egui::Vec2::new(col_width, row_height), Layout::left_to_right(egui::Align::Center), |ui| {\n                            egui::Frame::NONE.corner_radius(CornerRadius::same(14)).fill(DARK_MODE_STATUS_SHEET_COLOR).show(ui, |ui| {\n                                ui.allocate_ui(egui::Vec2::new(STATUS_ICON_CONTAINER_WIDTH, STATUS_ICON_CONTAINER_WIDTH), |ui| {\n                                    egui::Frame::NONE\n                                        .fill(STATUS_ICON_CONTAINER_COLOR)\n                                        .corner_radius(CornerRadius::same(STATUS_ICON_WIDTH as u8))\n                                        .show(ui, |ui| {\n                                            let image = render_svg_with_current_color_replace(\n                                                unsafe { String::from_utf8_unchecked(DOWN_ICON.to_vec()) }.as_str(),\n                                                csscolorparser::parse(&DARK_MODE_TEXT_COLOR.to_hex()).unwrap(),\n                                                (STATUS_ICON_WIDTH).round() as u32,\n                                                (STATUS_ICON_WIDTH).round() as u32,\n                                            )\n                                            .unwrap()\n                                            .into_wrapper()\n                                            .into_egui_image();\n                                            let texture_handle = ui.ctx().load_texture(\"down_icon\", image, TextureOptions::default());\n                                            ui.centered_and_justified(|ui| {\n                                                ui.add(Image::from_texture(&texture_handle));\n                                            });\n                                        });\n                                });\n                                let width = ui.available_width();\n                                let height = ui.available_height();\n                                ui.allocate_ui_with_layout(egui::Vec2::new(width, height), Layout::centered_and_justified(egui::Direction::TopDown), |ui| {\n                                    ui.add(Label::new(humansize::format_size(this.download_speed, humansize::DECIMAL.suffix(\"/s\"))).wrap_mode(TextWrapMode::Extend));\n                                });\n                            });\n                        })\n                    });\n\n                    ui.add_space(grid_gap); // gap\n\n                    // Upload Column\n                    ui.allocate_ui_with_layout(egui::Vec2::new(col_width, available_height), egui::Layout::top_down(egui::Align::LEFT), |ui| {\n                        ui.add_space(top_gap);\n\n                        // Upload Total\n                        ui.allocate_ui_with_layout(egui::Vec2::new(col_width, row_height), Layout::left_to_right(egui::Align::Center), |ui| {\n                            egui::Frame::NONE.corner_radius(CornerRadius::same(14)).fill(DARK_MODE_STATUS_SHEET_COLOR).show(ui, |ui| {\n                                ui.allocate_ui(egui::Vec2::new(STATUS_ICON_CONTAINER_WIDTH, STATUS_ICON_CONTAINER_WIDTH), |ui| {\n                                    egui::Frame::NONE\n                                        .fill(STATUS_ICON_CONTAINER_COLOR)\n                                        .corner_radius(CornerRadius::same(STATUS_ICON_WIDTH as u8))\n                                        .show(ui, |ui| {\n                                            let image = render_svg_with_current_color_replace(\n                                                unsafe { String::from_utf8_unchecked(UPLOAD_ICON.to_vec()) }.as_str(),\n                                                csscolorparser::parse(&DARK_MODE_TEXT_COLOR.to_hex()).unwrap(),\n                                                (STATUS_ICON_WIDTH).round() as u32,\n                                                (STATUS_ICON_WIDTH).round() as u32,\n                                            )\n                                            .unwrap()\n                                            .into_wrapper()\n                                            .into_egui_image();\n                                            let texture_handle = ui.ctx().load_texture(\"upload_icon\", image, TextureOptions::default());\n                                            ui.centered_and_justified(|ui| {\n                                                ui.add(Image::from_texture(&texture_handle));\n                                            });\n                                        });\n                                });\n                                let width = ui.available_width();\n                                let height = ui.available_height();\n                                ui.allocate_ui_with_layout(egui::Vec2::new(width, height), Layout::centered_and_justified(egui::Direction::TopDown), |ui| {\n                                    ui.add(Label::new(humansize::format_size(this.upload_total, humansize::DECIMAL)).wrap_mode(TextWrapMode::Extend));\n                                });\n                            });\n                        });\n\n                        ui.add_space(vertical_padding); // gap\n\n                        // Upload Speed\n                        ui.allocate_ui_with_layout(egui::Vec2::new(col_width, row_height), Layout::left_to_right(egui::Align::Center), |ui| {\n                            egui::Frame::NONE.corner_radius(CornerRadius::same(14)).fill(DARK_MODE_STATUS_SHEET_COLOR).show(ui, |ui| {\n                                ui.allocate_ui(egui::Vec2::new(STATUS_ICON_CONTAINER_WIDTH, STATUS_ICON_CONTAINER_WIDTH), |ui| {\n                                    egui::Frame::NONE\n                                        .fill(STATUS_ICON_CONTAINER_COLOR)\n                                        .corner_radius(CornerRadius::same(STATUS_ICON_WIDTH as u8))\n                                        .show(ui, |ui| {\n                                            let image = render_svg_with_current_color_replace(\n                                                unsafe { String::from_utf8_unchecked(UP_ICON.to_vec()) }.as_str(),\n                                                csscolorparser::parse(&DARK_MODE_TEXT_COLOR.to_hex()).unwrap(),\n                                                (STATUS_ICON_WIDTH).round() as u32,\n                                                (STATUS_ICON_WIDTH).round() as u32,\n                                            )\n                                            .unwrap()\n                                            .into_wrapper()\n                                            .into_egui_image();\n                                            let texture_handle = ui.ctx().load_texture(\"up_icon\", image, TextureOptions::default());\n                                            ui.centered_and_justified(|ui| {\n                                                ui.add(Image::from_texture(&texture_handle));\n                                            });\n                                        });\n                                });\n                                let width = ui.available_width();\n                                let height = ui.available_height();\n                                ui.allocate_ui_with_layout(egui::Vec2::new(width, height), Layout::centered_and_justified(egui::Direction::TopDown), |ui| {\n                                    ui.add(Label::new(humansize::format_size(this.upload_speed, humansize::DECIMAL.suffix(\"/s\"))).wrap_mode(TextWrapMode::Extend));\n                                });\n                            });\n                        })\n                    });\n                });\n            });\n    }\n}\n"
  },
  {
    "path": "backend/nyanpasu-egui/src/widget/network_statistic_small.rs",
    "content": "#![allow(dead_code)]\nuse std::sync::{Arc, LazyLock};\n\nuse eframe::egui::{\n    self, Color32, CornerRadius, Id, Image, Label, Layout, Margin, RichText, Sense, Stroke, Style,\n    TextWrapMode, Theme, Vec2, ViewportCommand, WidgetText, include_image, style::Selection,\n};\nuse parking_lot::RwLock;\n\nuse crate::ipc::Message;\n\n// Presets\nconst STATUS_ICON_CONTAINER_WIDTH: f32 = 20.0;\nconst LOGO_CONTAINER_WIDTH: f32 = 44.0;\nconst LOGO_SIZE: Vec2 = Vec2::new(26.0, 31.0);\n\n// Themes\nconst GLOBAL_ALPHA: u8 = 128;\nconst LIGHT_MODE_BACKGROUND_COLOR: Color32 = Color32::from_rgb(234, 221, 255);\nconst LIGHT_MODE_TEXT_COLOR: Color32 = Color32::from_rgb(29, 27, 32);\nconst DARK_MODE_TEXT_COLOR: Color32 = Color32::from_rgb(254, 247, 255);\nconst DARK_MODE_BACKGROUND_COLOR: Color32 = Color32::from_rgb(29, 27, 32);\nconst DARK_MODE_STATUS_SHEET_COLOR: Color32 = Color32::from_rgb(73, 69, 79);\nconst STATUS_ICON_CONTAINER_COLOR: Color32 = Color32::from_rgb(79, 55, 139);\nstatic LOGO_CONTAINER_COLOR: LazyLock<Color32> =\n    LazyLock::new(|| Color32::from_rgba_unmultiplied(79, 55, 139, GLOBAL_ALPHA));\n\n// Icons\nconst UP_ICON: &[u8] = include_bytes!(\"../../assets/up.svg\");\nconst DOWN_ICON: &[u8] = include_bytes!(\"../../assets/down.svg\");\n\nfn setup_custom_style(ctx: &egui::Context) {\n    ctx.style_mut_of(Theme::Light, use_light_green_accent);\n    ctx.style_mut_of(Theme::Dark, use_dark_purple_accent);\n}\n\nfn setup_fonts(ctx: &egui::Context) {\n    let mut fonts = egui::FontDefinitions::default();\n\n    fonts.font_data.insert(\n        \"Inter\".to_owned(),\n        Arc::new(egui::FontData::from_static(include_bytes!(\n            \"../../assets/Inter-Regular.ttf\"\n        ))),\n    );\n\n    fonts\n        .families\n        .entry(egui::FontFamily::Proportional)\n        .or_default()\n        .insert(0, \"Inter\".to_owned());\n\n    ctx.set_fonts(fonts);\n}\n\nfn use_global_styles(styles: &mut Style) {\n    styles.spacing.window_margin = Margin::same(0);\n    styles.spacing.item_spacing = Vec2::new(0.0, 0.0);\n    styles.interaction.selectable_labels = false;\n}\n\nfn use_light_green_accent(style: &mut Style) {\n    use_global_styles(style);\n    style.visuals.override_text_color = Some(LIGHT_MODE_TEXT_COLOR);\n    style.visuals.hyperlink_color = Color32::from_rgb(18, 180, 85);\n    style.visuals.text_cursor.stroke.color = Color32::from_rgb(28, 92, 48);\n    style.visuals.selection = Selection {\n        bg_fill: Color32::from_rgb(157, 218, 169),\n        stroke: Stroke::new(1.0, Color32::from_rgb(28, 92, 48)),\n    };\n}\n\nfn use_dark_purple_accent(style: &mut Style) {\n    use_global_styles(style);\n    style.visuals.override_text_color = Some(DARK_MODE_TEXT_COLOR);\n    style.visuals.hyperlink_color = Color32::from_rgb(202, 135, 227);\n    style.visuals.text_cursor.stroke.color = Color32::from_rgb(234, 208, 244);\n    style.visuals.selection = Selection {\n        bg_fill: Color32::from_rgb(105, 67, 119),\n        stroke: Stroke::new(1.0, Color32::from_rgb(234, 208, 244)),\n    };\n}\n\n#[derive(Clone)]\npub struct NyanpasuNetworkStatisticSmallWidget {\n    state: Arc<RwLock<NyanpasuNetworkStatisticSmallWidgetState>>,\n}\n\nstruct NyanpasuNetworkStatisticSmallWidgetState {\n    // data fields\n    // download_total: u64,\n    // upload_total: u64,\n    download_speed: u64,\n    upload_speed: u64,\n\n    // eframe ctx\n    egui_ctx: egui::Context,\n}\n\nimpl NyanpasuNetworkStatisticSmallWidgetState {\n    fn request_repaint(&self) {\n        self.egui_ctx.request_repaint();\n    }\n}\n\nimpl NyanpasuNetworkStatisticSmallWidget {\n    pub fn new(cc: &eframe::CreationContext<'_>) -> Self {\n        setup_fonts(&cc.egui_ctx);\n        setup_custom_style(&cc.egui_ctx);\n        egui_extras::install_image_loaders(&cc.egui_ctx);\n        let rx = crate::ipc::setup_ipc_receiver_with_env().unwrap();\n        let widget = Self {\n            state: Arc::new(RwLock::new(NyanpasuNetworkStatisticSmallWidgetState {\n                egui_ctx: cc.egui_ctx.clone(),\n                download_speed: 0,\n                upload_speed: 0,\n            })),\n        };\n        let this = widget.clone();\n        std::thread::spawn(move || {\n            loop {\n                match rx.recv() {\n                    Ok(msg) => {\n                        println!(\"Received message: {msg:?}\");\n                        let _ = this.handle_message(msg);\n                    }\n                    Err(e) => {\n                        eprintln!(\"Failed to receive message: {e}\");\n                        if matches!(\n                            e,\n                            ipc_channel::ipc::IpcError::Disconnected\n                                | ipc_channel::ipc::IpcError::Io(_)\n                        ) {\n                            let _ = this.handle_message(Message::Stop);\n                            break;\n                        }\n                    }\n                }\n            }\n        });\n        widget\n    }\n\n    pub fn run() -> eframe::Result {\n        #[cfg(target_os = \"macos\")]\n        super::set_application_activation_policy();\n\n        let options = eframe::NativeOptions {\n            viewport: egui::ViewportBuilder::default()\n                .with_inner_size([80.0, 32.0])\n                .with_decorations(false)\n                .with_transparent(true)\n                .with_always_on_top()\n                .with_drag_and_drop(true)\n                .with_resizable(false)\n                .with_taskbar(false),\n            run_and_return: false,\n            // TODO: buggy feature, and should we manually save the window state\n            // persist_window: true,\n            // persistence_path: get_window_state_path().ok(),\n            ..Default::default()\n        };\n        println!(\"Running widget...\");\n        eframe::run_native(\n            \"Nyanpasu Network Statistic Widget\",\n            options,\n            Box::new(|cc| Ok(Box::new(NyanpasuNetworkStatisticSmallWidget::new(cc)))),\n        )\n    }\n\n    pub fn handle_message(&self, msg: Message) -> anyhow::Result<()> {\n        let mut this = self.state.write();\n        match msg {\n            Message::UpdateStatistic(statistic) => {\n                // this.download_total = statistic.download_total;\n                // this.upload_total = statistic.upload_total;\n                this.download_speed = statistic.download_speed;\n                this.upload_speed = statistic.upload_speed;\n                this.request_repaint();\n            }\n            Message::Stop => {\n                std::thread::spawn(move || {\n                    // wait for 5 seconds to ensure the widget is closed, or the app will be terminated\n                    std::thread::sleep(std::time::Duration::from_secs(5));\n                    std::process::exit(0);\n                });\n                this.egui_ctx.send_viewport_cmd(ViewportCommand::Close);\n            }\n            _ => {\n                eprintln!(\"Unsupported message: {msg:?}\");\n            }\n        }\n        Ok(())\n    }\n}\n\nimpl eframe::App for NyanpasuNetworkStatisticSmallWidget {\n    fn clear_color(&self, _visuals: &egui::Visuals) -> [f32; 4] {\n        egui::Rgba::TRANSPARENT.to_array()\n    }\n\n    fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {\n        let visuals = &ctx.style().visuals;\n        let this = self.state.read();\n\n        egui::CentralPanel::default()\n            .frame(\n                egui::Frame::NONE\n                    .corner_radius(CornerRadius::same(40))\n                    .fill(if visuals.dark_mode {\n                        DARK_MODE_BACKGROUND_COLOR\n                    } else {\n                        LIGHT_MODE_BACKGROUND_COLOR\n                    })\n                    .inner_margin(Margin::same(4)),\n            )\n            .show(ctx, |ui| {\n                if ui\n                    .interact(ui.max_rect(), Id::new(\"window-drag\"), Sense::drag())\n                    .dragged()\n                {\n                    ctx.send_viewport_cmd(ViewportCommand::StartDrag);\n                }\n                ui.horizontal(|ui| {\n                    ui.allocate_ui(Vec2::new(24.0, 24.0), |ui| {\n                        egui::Frame::NONE\n                            .corner_radius(CornerRadius::same(12))\n                            .fill(*LOGO_CONTAINER_COLOR)\n                            .show(ui, |ui| {\n                                ui.allocate_ui_with_layout(\n                                    Vec2::new(24.0, 24.0),\n                                    Layout::centered_and_justified(egui::Direction::TopDown),\n                                    |ui| {\n                                        ui.add(\n                                            Image::new(include_image!(\n                                                \"../../assets/tray-icon.png\"\n                                            ))\n                                            .max_size(Vec2::new(9.84, 13.78)),\n                                        )\n                                    },\n                                )\n                            });\n                    });\n\n                    ui.add_space(1.0);\n                    ui.vertical(|ui| {\n                        let width = ui.available_width();\n                        let height = ui.available_height() / 2.0;\n                        ui.allocate_ui_with_layout(\n                            Vec2::new(width, height),\n                            Layout::centered_and_justified(egui::Direction::LeftToRight),\n                            |ui| {\n                                ui.add(\n                                    Label::new(\n                                        RichText::new(humansize::format_size(\n                                            this.upload_speed,\n                                            humansize::DECIMAL.suffix(\"/s\"),\n                                        ))\n                                        .size(8.0),\n                                    )\n                                    .selectable(false)\n                                    .wrap_mode(TextWrapMode::Extend),\n                                );\n                            },\n                        );\n                        ui.allocate_ui_with_layout(\n                            Vec2::new(width, height),\n                            Layout::centered_and_justified(egui::Direction::LeftToRight),\n                            |ui| {\n                                ui.add(\n                                    Label::new(WidgetText::from(\n                                        RichText::new(humansize::format_size(\n                                            this.download_speed,\n                                            humansize::DECIMAL.suffix(\"/s\"),\n                                        ))\n                                        .size(8.0),\n                                    ))\n                                    .selectable(false)\n                                    .wrap_mode(TextWrapMode::Extend),\n                                );\n                            },\n                        );\n                    });\n                })\n            });\n    }\n}\n"
  },
  {
    "path": "backend/nyanpasu-macro/Cargo.toml",
    "content": "[package]\nname = \"nyanpasu-macro\"\nversion = \"0.1.0\"\nrepository.workspace = true\nedition.workspace = true\nlicense.workspace = true\nauthors.workspace = true\n\n[lib]\nproc-macro = true\n\n[dependencies]\nsyn = \"2\"\nquote = \"1\"\nproc-macro2 = \"1.0.86\"\n"
  },
  {
    "path": "backend/nyanpasu-macro/src/builder_update.rs",
    "content": "use proc_macro2::TokenStream;\nuse quote::{format_ident, quote};\nuse syn::{DeriveInput, Error, Ident, LitStr, Meta, Type, spanned::Spanned};\n\npub fn builder_update(input: DeriveInput) -> syn::Result<TokenStream> {\n    let name = format_ident!(\"{}\", input.ident);\n    // search #[builder_update(ty = \"T\")]\n    let mut partial_ty: Option<Ident> = None;\n    // search #[builder_update(patch_fn = \"fn_name\")]\n    let mut patch_fn: Option<Ident> = None;\n    // search #[builder_update(getter)] or #[builder_update(getter = \"get_{}\")]\n    let mut generate_getter: Option<String> = None;\n    for attr in &input.attrs {\n        if let Some(attr_meta_name) = attr.path().get_ident()\n            && attr_meta_name == \"builder_update\"\n        {\n            let meta = &attr.meta;\n            match meta {\n                Meta::List(list) => {\n                    list.parse_nested_meta(|meta| {\n                        let path = &meta.path;\n                        match path {\n                            path if path.is_ident(\"ty\") => {\n                                let value = meta.value()?;\n                                let lit_str: LitStr = value.parse()?;\n                                partial_ty = Some(lit_str.parse()?);\n                            }\n                            path if path.is_ident(\"patch_fn\") => {\n                                let value = meta.value()?;\n                                let lit_str: LitStr = value.parse()?;\n                                patch_fn = Some(lit_str.parse()?);\n                            }\n                            path if path.is_ident(\"getter\") => {\n                                match meta.value() {\n                                    Ok(value) => {\n                                        let lit_str: LitStr = value.parse()?;\n                                        generate_getter = Some(lit_str.value());\n                                    }\n                                    Err(_) => {\n                                        // it should be default getter\n                                        generate_getter = Some(\"get_{}\".to_string());\n                                    }\n                                }\n                            }\n                            _ => {\n                                return Err(\n                                    meta.error(\"Only #[builder_update(ty = \\\"T\\\")] is supported\")\n                                );\n                            }\n                        }\n                        Ok(())\n                    })?;\n                }\n                _ => {\n                    return Err(Error::new(\n                        attr.span(),\n                        \"Only #[builder_update(ty = \\\"T\\\")] is supported\",\n                    ));\n                }\n            }\n        }\n    }\n    let partial_ty = match partial_ty {\n        Some(ty) => ty,\n        None => format_ident!(\"{}Builder\", name),\n    };\n    let patch_fn = match patch_fn {\n        Some(fn_name) => fn_name,\n        None => format_ident!(\"update\"),\n    };\n\n    let mut patch_fields = quote! {};\n    let mut fields_getter = quote! {};\n\n    match input.data {\n        syn::Data::Struct(ref data) => {\n            if let syn::Fields::Named(ref fields) = data.fields {\n                for field in &fields.named {\n                    let field_name = field.ident.as_ref().unwrap();\n                    let field_type = &field.ty;\n                    let mut getter_type = wrap_type_in_option(field_type);\n\n                    // check whether the field has #[update(nest)]\n                    let mut nested = false;\n                    for attr in &field.attrs {\n                        if attr.path().is_ident(\"builder_update\")\n                            && let Meta::List(ref list) = attr.meta\n                        {\n                            list.parse_nested_meta(|meta| {\n                                let path = &meta.path;\n                                match path {\n                                    path if path.is_ident(\"nested\") => {\n                                        nested = true;\n                                    }\n                                    path if path.is_ident(\"getter_ty\") => {\n                                        let value = meta.value()?;\n                                        let lit_str: LitStr = value.parse()?;\n                                        getter_type = syn::parse_str(&lit_str.value())?;\n                                    }\n                                    _ => {\n                                        return Err(meta\n                                            .error(\"Only #[builder_update(nested)] is supported\"));\n                                    }\n                                }\n                                Ok(())\n                            })?;\n                        }\n                    }\n\n                    patch_fields.extend(if nested {\n                        quote! {\n                            self.#field_name.#patch_fn(partial.#field_name);\n                        }\n                    } else {\n                        quote! {\n                            if let Some(value) = partial.#field_name {\n                                self.#field_name = value;\n                            }\n                        }\n                    });\n\n                    if let Some(getter) = &generate_getter {\n                        let getter_name = format_ident!(\n                            \"{}\",\n                            getter.replace(\n                                \"{}\",\n                                field_name\n                                    .to_string()\n                                    .strip_prefix(\"r#\")\n                                    .unwrap_or(&field_name.to_string())\n                            )\n                        );\n                        fields_getter.extend(quote! {\n                            pub fn #getter_name(&self) -> &#getter_type {\n                                &self.#field_name\n                            }\n                        });\n                    }\n                }\n            }\n        }\n        _ => {\n            return Err(Error::new(input.span(), \"Only struct is supported\"));\n        }\n    }\n\n    let expanded = quote! {\n        impl #name {\n            pub fn #patch_fn(&mut self, partial: #partial_ty) {\n                #patch_fields\n            }\n        }\n\n        impl #partial_ty {\n            #fields_getter\n        }\n    };\n\n    Ok(expanded)\n}\n\nfn wrap_type_in_option(ty: &Type) -> Type {\n    syn::parse_quote! {\n        Option<#ty>\n    }\n}\n"
  },
  {
    "path": "backend/nyanpasu-macro/src/enum_wrapper_combined.rs",
    "content": "use proc_macro2::TokenStream;\nuse quote::{format_ident, quote};\nuse syn::{DeriveInput, Fields};\n\npub fn enum_combined_wrapper(input: DeriveInput) -> syn::Result<TokenStream> {\n    let name = &input.ident;\n    let data = &input.data;\n\n    let mut expanded = quote! {};\n    let mut ty_assert_and_as = quote! {};\n\n    match data {\n        syn::Data::Enum(e) => {\n            for variant in e.variants.iter() {\n                let variant_name = &variant.ident;\n                match &variant.fields {\n                    Fields::Unnamed(fields) => {\n                        if fields.unnamed.len() != 1 {\n                            return Err(syn::Error::new_spanned(\n                                input,\n                                \"EnumWrapperFrom only supports enums with a single field\",\n                            ));\n                        }\n                        let field = fields.unnamed.first().unwrap();\n                        let field_ty = &field.ty;\n                        expanded.extend(quote! {\n                            impl From<#field_ty> for #name {\n                                fn from(value: #field_ty) -> Self {\n                                    Self::#variant_name(value)\n                                }\n                            }\n                        });\n\n                        let is_ty = format_ident!(\"is_{}\", variant_name.to_string().to_lowercase());\n                        let as_ty = format_ident!(\"as_{}\", variant_name.to_string().to_lowercase());\n                        let as_mut_ty =\n                            format_ident!(\"as_{}_mut\", variant_name.to_string().to_lowercase());\n\n                        ty_assert_and_as.extend(quote! {\n                            pub fn #is_ty(&self) -> bool {\n                                matches!(self, Self::#variant_name(_))\n                            }\n\n                            pub fn #as_ty(&self) -> Option<&#field_ty> {\n                                if let Self::#variant_name(value) = self {\n                                    Some(value)\n                                } else {\n                                    None\n                                }\n                            }\n\n                            pub fn #as_mut_ty(&mut self) -> Option<&mut #field_ty> {\n                                if let Self::#variant_name(value) = self {\n                                    Some(value)\n                                } else {\n                                    None\n                                }\n                            }\n                        });\n                    }\n                    _ => {\n                        return Err(syn::Error::new_spanned(\n                            input,\n                            \"EnumWrapperFrom only supports unnamed fields\",\n                        ));\n                    }\n                }\n            }\n        }\n        _ => {\n            return Err(syn::Error::new_spanned(\n                input,\n                \"EnumWrapperFrom only supports enums\",\n            ));\n        }\n    }\n\n    expanded.extend(quote! {\n        impl #name {\n            #ty_assert_and_as\n        }\n    });\n\n    Ok(expanded)\n}\n"
  },
  {
    "path": "backend/nyanpasu-macro/src/lib.rs",
    "content": "use proc_macro::TokenStream;\nuse syn::{DeriveInput, parse_macro_input};\n\nmod builder_update;\nmod enum_wrapper_combined;\nmod verge_patch;\n\n#[proc_macro_derive(BuilderUpdate, attributes(builder_update))]\npub fn builder_update(input: TokenStream) -> TokenStream {\n    let input = parse_macro_input!(input as DeriveInput);\n    match builder_update::builder_update(input) {\n        Ok(token_stream) => TokenStream::from(token_stream),\n        Err(e) => TokenStream::from(e.to_compile_error()),\n    }\n}\n\n#[proc_macro_derive(VergePatch, attributes(verge))]\npub fn verge_patch(input: TokenStream) -> TokenStream {\n    let input = parse_macro_input!(input as DeriveInput);\n    match verge_patch::verge_patch(input) {\n        Ok(token_stream) => TokenStream::from(token_stream),\n        Err(e) => TokenStream::from(e.to_compile_error()),\n    }\n}\n\n#[proc_macro_derive(EnumWrapperCombined)]\npub fn enum_wrapper_from(input: TokenStream) -> TokenStream {\n    let input = parse_macro_input!(input as DeriveInput);\n    match enum_wrapper_combined::enum_combined_wrapper(input) {\n        Ok(token_stream) => TokenStream::from(token_stream),\n        Err(e) => TokenStream::from(e.to_compile_error()),\n    }\n}\n"
  },
  {
    "path": "backend/nyanpasu-macro/src/verge_patch.rs",
    "content": "use proc_macro2::TokenStream;\nuse quote::{format_ident, quote};\nuse syn::{Data, DeriveInput, Error, Ident, LitStr, Meta, Result, spanned::Spanned};\n\npub fn verge_patch(input: DeriveInput) -> Result<TokenStream> {\n    let name = &input.ident;\n    let mut patch_fn: Option<Ident> = None;\n    let mut patch_pointer: Option<Ident> = None; // default is self\n    let mut patch_type: Option<Ident> = None; // default is Self\n    for attr in &input.attrs {\n        if attr.path().is_ident(\"verge\") {\n            match &attr.meta {\n                Meta::List(list) => {\n                    list.parse_nested_meta(|meta| {\n                        match &meta.path {\n                            path if path.is_ident(\"patch_fn\") => {\n                                let value = meta.value()?;\n                                let lit_str: LitStr = value.parse()?;\n                                patch_fn = Some(lit_str.parse()?);\n                            }\n                            path if path.is_ident(\"patch_pointer\") => {\n                                let value = meta.value()?;\n                                let lit_str: LitStr = value.parse()?;\n                                patch_pointer = Some(lit_str.parse()?);\n                            }\n                            path if path.is_ident(\"patch_type\") => {\n                                let value = meta.value()?;\n                                let lit_str: LitStr = value.parse()?;\n                                patch_type = Some(lit_str.parse()?);\n                            }\n                            _ => {\n                                return Err(meta.error(\"Unknown attribute\"));\n                            }\n                        }\n                        Ok(())\n                    })?;\n                }\n                _ => {\n                    return Err(Error::new(attr.span(), \"Only #[verge(...)] is supported\"));\n                }\n            }\n        }\n    }\n\n    let patch_fn = match patch_fn {\n        Some(fn_name) => fn_name,\n        None => format_ident!(\"patch_{}\", name),\n    };\n    let patch_pointer = match patch_pointer {\n        Some(pointer) => pointer,\n        None => format_ident!(\"self\"),\n    };\n    let patch_type = match patch_type {\n        Some(ty) => ty,\n        None => format_ident!(\"{}\", name),\n    };\n\n    let mut patch_fields = quote! {};\n\n    match input.data {\n        Data::Struct(ref data) => {\n            if let syn::Fields::Named(ref fields) = data.fields {\n                for field in &fields.named {\n                    let field_name = field.ident.as_ref().unwrap();\n                    patch_fields.extend(quote! {\n                        if patch.#field_name.is_some() {\n                            #patch_pointer.#field_name = patch.#field_name;\n                        }\n                    });\n                }\n            }\n        }\n        _ => {\n            return Err(Error::new(input.span(), \"Only struct is supported\"));\n        }\n    }\n\n    let expanded = quote! {\n        impl #name {\n            pub fn #patch_fn(&mut self, patch: #patch_type) {\n                #patch_fields\n            }\n        }\n    };\n\n    Ok(expanded)\n}\n"
  },
  {
    "path": "backend/rustfmt.toml",
    "content": "max_width = 100\nhard_tabs = false\ntab_spaces = 4\nnewline_style = \"Auto\"\nuse_small_heuristics = \"Default\"\nreorder_imports = true\nreorder_modules = true\nremove_nested_parens = true\nedition = \"2024\"\nmerge_derives = true\nuse_try_shorthand = false\nuse_field_init_shorthand = false\nforce_explicit_abi = true\nimports_granularity = \"Crate\"\n"
  },
  {
    "path": "backend/tauri/.gitignore",
    "content": "# Generated by Cargo\n# will have compiled files and executables\n/target/\nWixTools\nresources\nsidecar\ntmp/\n\n!/tmp/.gitkeep\n"
  },
  {
    "path": "backend/tauri/Cargo.toml",
    "content": "[package]\nname = \"clash-nyanpasu\"\nversion = \"0.1.0\"\ndescription = \"clash verge\"\nauthors = { workspace = true }\nlicense = { workspace = true }\nrepository = { workspace = true }\ndefault-run = \"clash-nyanpasu\"\nedition = { workspace = true }\nbuild = \"build.rs\"\n\n[lib]\nname = \"clash_nyanpasu_lib\"\ncrate-type = [\"staticlib\", \"cdylib\", \"rlib\"]\ndoctest = false\n\n[build-dependencies]\ntauri-build = { version = \"2.1\", features = [] }\nserde = \"1\"\nserde_json = { version = \"1.0\", features = [\"preserve_order\"] }\nchrono = \"0.4\"\nrustc_version = \"0.4\"\nsemver = \"1.0\"\n\n[dependencies]\n# Local Dependencies\nnyanpasu-ipc = { git = \"https://github.com/libnyanpasu/nyanpasu-service.git\", features = [\n  \"client\",\n  \"specta\",\n] } # IPC bridge between the UI process and service process\nnyanpasu-macro = { path = \"../nyanpasu-macro\" }\nnyanpasu-utils = { workspace = true }\nnyanpasu-egui = { path = \"../nyanpasu-egui\" }\n\n# Common Utilities\ntokio = { workspace = true }\ntokio-util = { version = \"0.7\", features = [\"full\"] }\noneshot = \"0.1\"\nfutures = \"0.3\"\nfutures-util = \"0.3\"\nglob = \"0.3.1\"\ntimeago = \"0.6\"\nhumansize = \"2.1.3\"\nconvert_case = \"0.11.0\"\nanyhow = \"1.0\"\npretty_assertions = \"1.4.0\"\nchrono = { version = \"0.4\", features = [\"serde\"] }\ntime = { version = \"0.3\", features = [\"formatting\", \"parsing\", \"serde\"] }\nonce_cell = \"1.19.0\"\nasync-trait = \"0.1.77\"\ndyn-clone = \"1.0.16\"\nthiserror = { workspace = true }\nparking_lot = { version = \"0.12.1\" }\nfs-err = { workspace = true }                                             # for more detailed io error\nitertools = \"0.14\"                                                        # sweet iterator utilities\nrayon = \"1.10\"                                                            # for iterator parallel processing\nambassador = \"0.5.0\"                                                      # for trait delegation\nderive_builder = \"0.20\"                                                   # for builder pattern\nstrum = { version = \"0.28\", features = [\"derive\"] }                       # for enum string conversion\natomic_enum = \"0.3.0\"                                                     # for atomic enum\nenumflags2 = \"0.7\"                                                        # for enum flags\nbackon = { version = \"1.0.1\", features = [\"tokio-sleep\"] }                # for backoff retry\n\n# Data Structures\ndashmap = \"6\"\nindexmap = { version = \"2.2.3\", features = [\"serde\"] }\nbimap = \"0.6.3\"\nbumpalo = \"3.17.0\"                                     # a bump allocator for heap allocation\nrustc-hash = \"2.1\"\n\n# Terminal Utilities\nansi-str = \"0.9\"                                    # for ansi str stripped\nctrlc = \"3.4.2\"\ncolored = \"3\"\nclap = { version = \"4.5.4\", features = [\"derive\"] }\n\n# GUI Utilities\nrfd = { version = \"0.15\", default-features = false, features = [\n  \"tokio\",\n  \"gtk3\",\n  \"common-controls-v6\",\n] } # cross platform dialog\n\n# Internationalization\nrust-i18n = \"3\"\n\n# Networking Libraries\naxum = \"0.8\"\nurl = \"2\"\nmime = \"0.3\"\nreqwest = { workspace = true }\ntokio-tungstenite = \"0.29\"\nurlencoding = \"2.1\"\nport_scanner = \"0.1.5\"\nsysproxy = { git = \"https://github.com/libnyanpasu/sysproxy-rs.git\", version = \"0.3\" }\n\n# Serialization\nserde = { version = \"1.0\", features = [\"derive\"] }\nserde_json = { version = \"1.0\", features = [\"preserve_order\"] }\nserde_yaml = { version = \"0.10\", package = \"serde_yaml_ng\", branch = \"feat/specta-update\", git = \"https://github.com/libnyanpasu/serde-yaml-ng.git\", features = [\n  \"specta\",\n] }\npostcard = { version = \"1.1\", features = [\"alloc\"] }\nbytes = { version = \"1\", features = [\"serde\"] }\nsemver = \"1.0\"\n\n# Compression & Encoding\nflate2 = \"1.0\"\nzip = \"8.0.0\"\nzip-extensions = \"0.13.0\"\nbase64 = \"0.22\"\nadler = \"1.0.2\"\nhex = \"0.4\"\npercent-encoding = \"2.3.1\"\n\n# Algorithms\nuuid = \"1.7.0\"\nrand = \"0.10\"\nmd-5 = \"0.10.6\"\nsha2 = \"0.10\"\nnanoid = \"0.4.0\"\nrs-snowflake = \"0.6\"\nseahash = \"4.1\"\n\n# System Utilities\nauto-launch = { git = \"https://github.com/libnyanpasu/auto-launch.git\", version = \"0.5\" }\ndelay_timer = { version = \"0.11\", git = \"https://github.com/libnyanpasu/delay-timer.git\" } # Task scheduler with timer\ndunce = \"1.0.4\"                                                                            # for cross platform path normalization\nrunas = { git = \"https://github.com/libnyanpasu/rust-runas.git\" }\nsingle-instance = \"0.3.3\"\nwhich = \"8\"\nopen = \"5.0.1\"\nsysinfo = \"0.38\"\nnum_cpus = \"1\"\nos_pipe = \"1.2.1\"\nwhoami = \"1.5.1\"\ncamino = { version = \"1.1.9\", features = [\"serde1\"] }\n\n# IO Utilities\ndirs = \"6\"\ntempfile = \"3.9.0\"\nfs_extra = \"1.3.0\"\nnotify-debouncer-full = \"0.7.0\"\nnotify = \"8.0.0\"\n\n# Database\nredb = \"3.0.0\"\n\n# Logging & Tracing\nlog = \"0.4.20\"\ntracing = { workspace = true }\ntracing-attributes = \"0.1\"\ntracing-futures = \"0.2\"\ntracing-subscriber = { version = \"0.3\", features = [\n  \"env-filter\",\n  \"json\",\n  \"parking_lot\",\n] }\ntracing-error = \"0.2\"\ntracing-log = { version = \"0.2\" }\ntracing-appender = { version = \"0.2\", features = [\"parking_lot\"] }\ntest-log = { workspace = true }\ntracing-test = { workspace = true }\n\n# Image & Graphics\nimage = \"0.25.5\"\nfast_image_resize = \"6\"\ndisplay-info = \"0.5.0\"  # should be removed after upgrading to tauri v2\n\n# OXC (The Oxidation Compiler)\n# We use it to parse and transpile the old script profile to esm based script profile\noxc_parser = \"0.121\"\noxc_allocator = \"0.121\"\noxc_span = \"0.121\"\noxc_ast = \"0.121\"\noxc_syntax = \"0.121\"\noxc_ast_visit = \"0.121\"\n\n# Lua Integration\nmlua = { version = \"0.11\", features = [\n  \"lua54\",\n  \"async\",\n  \"serialize\",\n  \"vendored\",\n  \"error-send\",\n] }\n\n# JavaScript Integration\nboa_utils = { path = \"../boa_utils\" }                     # should be removed when boa support console customize\nboa_engine = { workspace = true, features = [\"annex-b\"] }\n\n# Tauri Dependencies\ntauri = { version = \"2.4\", features = [\n  \"tray-icon\",\n  \"image-png\",\n  \"image-ico\",\n  \"rustls-tls\",\n  \"specta\",\n] }\ntauri-plugin-deep-link = { path = \"../tauri-plugin-deep-link\", version = \"0.1.2\" } # This should be migrated to official tauri plugin\ntauri-plugin-os = \"2.2\"\ntauri-plugin-clipboard-manager = \"2.2\"\ntauri-plugin-fs = \"2.2\"\ntauri-plugin-dialog = \"2.2\"\ntauri-plugin-process = \"2.2\"\ntauri-plugin-updater = \"2.2\"\ntauri-plugin-shell = \"2.2\"\ntauri-plugin-notification = \"2.2\"\ntauri-plugin-opener = \"2.5\"\nwindow-vibrancy = { version = \"0.7.0\" }\n\n# Strong typed api binding between typescript and rust\nspecta-typescript = \"0.0.9\"\ntauri-specta = { version = \"=2.0.0-rc.21\", features = [\"derive\", \"typescript\"] }\nspecta = { version = \"=2.0.0-rc.22\", features = [\n  \"serde\",\n  \"serde_json\",\n  \"serde_yaml\",\n  \"uuid\",\n  \"url\",\n  \"indexmap\",\n  \"function\",\n] }\n\n[target.\"cfg(not(any(target_os = \\\"android\\\", target_os = \\\"ios\\\")))\".dependencies]\ntauri-plugin-global-shortcut = \"2.2.0\"\n\n[target.'cfg(target_os = \"macos\")'.dependencies]\nobjc2 = \"0.6.1\"\nobjc2-app-kit = { version = \"0.3.1\", features = [\n  \"NSApplication\",\n  \"NSResponder\",\n  \"NSRunningApplication\",\n  \"NSWindow\",\n  \"NSView\",\n] }\nobjc2-foundation = { version = \"0.3.1\", features = [\"NSGeometry\"] }\n\n[target.'cfg(unix)'.dependencies]\nnix = { version = \"0.31.0\", features = [\"user\", \"fs\"] }\n\n[target.'cfg(windows)'.dependencies]\ndeelevate = \"0.2.0\"\nwinreg = { version = \"0.55\", features = [\"transactions\"] }\nwindows-registry = \"0.5.1\"\nwindows-sys = { version = \"0.60\", features = [\n  \"Win32_System_LibraryLoader\",\n  \"Win32_System_SystemInformation\",\n  \"Win32_UI_WindowsAndMessaging\",\n  \"Win32_System_Shutdown\",\n  \"Win32_Graphics_Gdi\",\n] }\nwindows-core = \"0.61\"\nwebview2-com = \"0.38\"\n\n[features]\ndefault = [\"custom-protocol\", \"default-meta\"]\nnightly = [\"devtools\", \"deadlock-detection\"]\ncustom-protocol = [\"tauri/custom-protocol\"]\nverge-dev = []\ndefault-meta = []\ndevtools = [\"tauri/devtools\"]\ndeadlock-detection = [\"parking_lot/deadlock_detection\"]\n"
  },
  {
    "path": "backend/tauri/Info.plist",
    "content": "<!-- Add this file next to your tauri.conf.json file -->\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n  <dict>\n    <key>CFBundleURLTypes</key>\n    <array>\n      <dict>\n        <key>CFBundleURLName</key>\n        <!-- Obviously needs to be replaced with your app's bundle identifier -->\n        <string>Clash Nyanpasu</string>\n        <key>CFBundleURLSchemes</key>\n        <array>\n          <string>clash-nyanpasu</string>\n          <!-- Compatible with Clash common register handler -->\n          <string>clash</string>\n        </array>\n      </dict>\n    </array>\n  </dict>\n</plist>\n"
  },
  {
    "path": "backend/tauri/build.rs",
    "content": "use chrono::{DateTime, SecondsFormat, Utc};\nuse rustc_version::version_meta;\nuse serde::Deserialize;\nuse std::{\n    env,\n    fs::{exists, read},\n    process::Command,\n};\n#[derive(Deserialize)]\nstruct PackageJson {\n    version: String, // we only need the version\n}\n\n#[derive(Deserialize)]\nstruct GitInfo {\n    hash: String,\n    author: String,\n    time: String,\n}\n\nfn main() {\n    let version: String = if let Ok(true) = exists(\"../../package.json\") {\n        let raw = read(\"../../package.json\").unwrap();\n        let pkg_json: PackageJson = serde_json::from_slice(&raw).unwrap();\n        pkg_json.version\n    } else {\n        let raw = read(\"./tauri.conf.json\").unwrap(); // TODO: fix it when windows arm64 need it\n        let tauri_json: PackageJson = serde_json::from_slice(&raw).unwrap();\n        tauri_json.version\n    };\n    let version = semver::Version::parse(&version).unwrap();\n    let is_prerelase = !version.pre.is_empty();\n    println!(\"cargo:rustc-env=NYANPASU_VERSION={version}\");\n    // Git Information\n    let (commit_hash, commit_author, commit_date) = if let Ok(true) = exists(\"./tmp/git-info.json\")\n    {\n        let git_info = read(\"./tmp/git-info.json\").unwrap();\n        let git_info: GitInfo = serde_json::from_slice(&git_info).unwrap();\n        (git_info.hash, git_info.author, git_info.time)\n    } else {\n        let output = Command::new(\"git\")\n            .args([\n                \"show\",\n                \"--pretty=format:'%H,%cn,%cI'\",\n                \"--no-patch\",\n                \"--no-notes\",\n            ])\n            .output()\n            .expect(\"Failed to execute git command\");\n        // println!(\"{}\", String::from_utf8(output.stderr.clone()).unwrap());\n        let command_args: Vec<String> = String::from_utf8(output.stdout)\n            .unwrap()\n            .replace('\\'', \"\")\n            .split(',')\n            .map(String::from)\n            .collect();\n        (\n            command_args[0].clone(),\n            command_args[1].clone(),\n            command_args[2].clone(),\n        )\n    };\n    println!(\"cargo:rustc-env=COMMIT_HASH={commit_hash}\");\n    println!(\"cargo:rustc-env=COMMIT_AUTHOR={commit_author}\");\n    let commit_date = DateTime::parse_from_rfc3339(&commit_date)\n        .unwrap()\n        .with_timezone(&Utc)\n        .to_rfc3339_opts(SecondsFormat::Millis, true);\n    println!(\"cargo:rustc-env=COMMIT_DATE={commit_date}\");\n\n    // Build Date\n    let build_date = Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true);\n    println!(\"cargo:rustc-env=BUILD_DATE={build_date}\");\n\n    // Build Profile\n    println!(\n        \"cargo:rustc-env=BUILD_PROFILE={}\",\n        if is_prerelase {\n            \"Nightly\"\n        } else {\n            match env::var(\"PROFILE\").unwrap().as_str() {\n                \"release\" => \"Release\",\n                \"debug\" => \"Debug\",\n                _ => \"Unknown\",\n            }\n        }\n    );\n    // Build Platform\n    println!(\n        \"cargo:rustc-env=BUILD_PLATFORM={}\",\n        env::var(\"TARGET\").unwrap()\n    );\n    // Rustc Version & LLVM Version\n    let rustc_version = version_meta().unwrap();\n    println!(\n        \"cargo:rustc-env=RUSTC_VERSION={}\",\n        rustc_version.short_version_string\n    );\n    println!(\n        \"cargo:rustc-env=LLVM_VERSION={}\",\n        match rustc_version.llvm_version {\n            Some(v) => v.to_string(),\n            None => \"Unknown\".to_string(),\n        }\n    );\n    tauri_build::build()\n}\n"
  },
  {
    "path": "backend/tauri/capabilities/main.json",
    "content": "{\n  \"$schema\": \"../gen/schemas/desktop-schema.json\",\n  \"identifier\": \"main-capability\",\n  \"local\": true,\n  \"windows\": [\"main\", \"legacy\", \"editor-*\"],\n  \"permissions\": [\n    \"core:default\",\n    \"core:app:default\",\n    \"core:event:default\",\n    \"updater:default\",\n    \"updater:allow-check\",\n    \"updater:allow-download\",\n    \"updater:allow-install\",\n    \"fs:allow-read-text-file\",\n    \"fs:allow-read-file\",\n    \"fs:allow-write-file\",\n    \"fs:allow-read-dir\",\n    \"fs:allow-copy-file\",\n    \"fs:allow-mkdir\",\n    \"fs:allow-remove\",\n    \"fs:allow-rename\",\n    \"fs:allow-exists\",\n    \"core:window:default\",\n    \"core:window:allow-create\",\n    \"core:window:allow-center\",\n    \"core:window:allow-request-user-attention\",\n    \"core:window:allow-set-resizable\",\n    \"core:window:allow-set-maximizable\",\n    \"core:window:allow-set-minimizable\",\n    \"core:window:allow-set-closable\",\n    \"core:window:allow-set-title\",\n    \"core:window:allow-maximize\",\n    \"core:window:allow-toggle-maximize\",\n    \"core:window:allow-unmaximize\",\n    \"core:window:allow-minimize\",\n    \"core:window:allow-unminimize\",\n    \"core:window:allow-show\",\n    \"core:window:allow-hide\",\n    \"core:window:allow-close\",\n    \"core:window:allow-set-decorations\",\n    \"core:window:allow-set-always-on-top\",\n    \"core:window:allow-set-content-protected\",\n    \"core:window:allow-set-size\",\n    \"core:window:allow-set-min-size\",\n    \"core:window:allow-set-max-size\",\n    \"core:window:allow-set-position\",\n    \"core:window:allow-set-fullscreen\",\n    \"core:window:allow-set-focus\",\n    \"core:window:allow-set-icon\",\n    \"core:window:allow-set-skip-taskbar\",\n    \"core:window:allow-set-cursor-grab\",\n    \"core:window:allow-set-cursor-visible\",\n    \"core:window:allow-set-cursor-icon\",\n    \"core:window:allow-set-cursor-position\",\n    \"core:window:allow-set-ignore-cursor-events\",\n    \"core:window:allow-start-dragging\",\n    \"core:webview:allow-print\",\n    \"shell:allow-execute\",\n    \"shell:allow-open\",\n    \"dialog:allow-open\",\n    \"dialog:allow-save\",\n    \"dialog:allow-message\",\n    \"dialog:allow-ask\",\n    \"dialog:allow-confirm\",\n    \"notification:default\",\n    \"global-shortcut:allow-is-registered\",\n    \"global-shortcut:allow-register\",\n    \"global-shortcut:allow-register-all\",\n    \"global-shortcut:allow-unregister\",\n    \"global-shortcut:allow-unregister-all\",\n    \"os:allow-platform\",\n    \"os:allow-version\",\n    \"os:allow-os-type\",\n    \"os:allow-family\",\n    \"os:allow-arch\",\n    \"os:allow-exe-extension\",\n    \"os:allow-locale\",\n    \"os:allow-hostname\",\n    \"process:allow-restart\",\n    \"process:allow-exit\",\n    \"clipboard-manager:allow-read-text\",\n    \"clipboard-manager:allow-write-text\"\n  ]\n}\n"
  },
  {
    "path": "backend/tauri/locales/en.json",
    "content": "{\n  \"_version\": 1,\n  \"tray\": {\n    \"copy_env\": {\n      \"cmd\": \"Copy Env (CMD)\",\n      \"ps\": \"Copy Env (PS)\",\n      \"sh\": \"Copy Env (sh)\"\n    },\n    \"no_proxies\": \"No Proxies\",\n    \"select_proxies\": \"Select Proxies\",\n    \"dashboard\": \"Dashboard\",\n    \"direct_mode\": \"Direct Mode\",\n    \"global_mode\": \"Global Mode\",\n    \"more\": {\n      \"menu\": \"More\",\n      \"restart_app\": \"Restart App\",\n      \"restart_clash\": \"Restart Clash\"\n    },\n    \"open_dir\": {\n      \"menu\": \"Open Dir\",\n      \"app_config_dir\": \"Config Dir\",\n      \"app_data_dir\": \"Data Dir\",\n      \"core_dir\": \"Core Dir\",\n      \"log_dir\": \"Log Dir\"\n    },\n    \"proxy_action\": {\n      \"on\": \"On\",\n      \"off\": \"Off\"\n    },\n    \"quit\": \"Quit\",\n    \"rule_mode\": \"Rule Mode\",\n    \"script_mode\": \"Script Mode\",\n    \"system_proxy\": \"System Proxy\",\n    \"tun_mode\": \"TUN Mode\"\n  },\n  \"dialog\": {\n    \"panic\": \"Please report this issue to Github issue tracker.\",\n    \"migrate\": \"Old version config file detected. Migrate to new version or not?\\nWARNING: This will override your current config if exists\",\n    \"custom_app_dir_migrate\": \"You will set custom app dir to %{path}\\nShall we move the current app dir to the new one?\",\n    \"warning\": {\n      \"enable_tun_with_no_permission\": \"TUN mode requires admin permission or service mode, neither is enabled, TUN mode will not work properly.\"\n    },\n    \"info\": {\n      \"grant_core_permission\": \"Clash core needs admin permission to make TUN mode work properly, grant it?\\nPlease note that this operation requires password input.\"\n    }\n  },\n  \"setting\": {\n    \"connection\": {\n      \"interrupt\": {\n        \"proxy\": {\n          \"label\": \"Interrupt connections when proxy changes\"\n        },\n        \"profile\": {\n          \"label\": \"Interrupt connections when profile changes\"\n        },\n        \"mode\": {\n          \"label\": \"Interrupt connections when mode changes\"\n        }\n      }\n    }\n  },\n  \"break_when_proxy_change\": \"Interrupt connections when proxy changes\",\n  \"break_when_profile_change\": \"Interrupt connections when profile changes\",\n  \"break_when_mode_change\": \"Interrupt connections when mode changes\"\n}\n"
  },
  {
    "path": "backend/tauri/locales/ru.json",
    "content": "{\n  \"_version\": 1,\n  \"tray\": {\n    \"copy_env\": {\n      \"cmd\": \"Копировать Env (CMD)\",\n      \"ps\": \"Копировать Env (PS)\",\n      \"sh\": \"Копировать Env (sh)\"\n    },\n    \"no_proxies\": \"Без прокси\",\n    \"select_proxies\": \"Выбрать прокси\",\n    \"dashboard\": \"Панель управления\",\n    \"direct_mode\": \"Прямой режим\",\n    \"global_mode\": \"Глобальный режим\",\n    \"more\": {\n      \"menu\": \"Еще\",\n      \"restart_app\": \"Перезапустить приложение\",\n      \"restart_clash\": \"Перезапустить Clash\"\n    },\n    \"open_dir\": {\n      \"menu\": \"Открыть папку\",\n      \"app_config_dir\": \"Папка конфигурации\",\n      \"app_data_dir\": \"Папка данных\",\n      \"core_dir\": \"Папка ядра\",\n      \"log_dir\": \"Папка журналов\"\n    },\n    \"proxy_action\": {\n      \"on\": \"Включить\",\n      \"off\": \"Выключить\"\n    },\n    \"quit\": \"Выйти\",\n    \"rule_mode\": \"Режим правил\",\n    \"script_mode\": \"Режим скриптов\",\n    \"system_proxy\": \"Системный прокси\",\n    \"tun_mode\": \"Режим TUN\"\n  },\n  \"dialog\": {\n    \"panic\": \"Пожалуйста, сообщите об этой проблеме в трекере проблем Github.\",\n    \"migrate\": \"Обнаружен файл конфигурации старой версии\\\\nМигрировать на новую версию или нет?\\\\n ВНИМАНИЕ: Это перезапишет вашу текущую конфигурацию, если она существует\",\n    \"custom_app_dir_migrate\": \"Вы установите пользовательскую папку приложения в %{path}\\\\nПереместить ли текущую папку приложения в новую?\",\n    \"warning\": {\n      \"enable_tun_with_no_permission\": \"Режим TUN требует прав администратора или режима службы, ни один из которых не включен, режим TUN не будет работать должным образом.\"\n    },\n    \"info\": {\n      \"grant_core_permission\": \"Ядру Clash необходимы права администратора для корректной работы режима TUN, предоставить их?\\\\n\\\\nОбратите внимание: Эта операция требует ввода пароля.\"\n    }\n  },\n  \"setting\": {\n    \"connection\": {\n      \"interrupt\": {\n        \"proxy\": {\n          \"label\": \"Прерывать соединения при смене прокси\"\n        },\n        \"profile\": {\n          \"label\": \"Прерывать соединения при смене профиля\"\n        },\n        \"mode\": {\n          \"label\": \"Прерывать соединения при смене режима\"\n        }\n      }\n    }\n  },\n  \"break_when_proxy_change\": \"Прерывать соединения при смене прокси\",\n  \"break_when_profile_change\": \"Прерывать соединения при смене профиля\",\n  \"break_when_mode_change\": \"Прерывать соединения при смене режима\"\n}\n"
  },
  {
    "path": "backend/tauri/locales/zh-cn.json",
    "content": "{\n  \"_version\": 1,\n  \"tray\": {\n    \"copy_env\": {\n      \"cmd\": \"复制环境变量 (CMD)\",\n      \"ps\": \"复制环境变量 (PS)\",\n      \"sh\": \"复制环境变量 (SH)\"\n    },\n    \"no_proxies\": \"无代理\",\n    \"select_proxies\": \"选择代理\",\n    \"dashboard\": \"打开面板\",\n    \"direct_mode\": \"直连模式\",\n    \"global_mode\": \"全局模式\",\n    \"more\": {\n      \"menu\": \"更多\",\n      \"restart_app\": \"重启应用程序\",\n      \"restart_clash\": \"重启 Clash\"\n    },\n    \"open_dir\": {\n      \"menu\": \"打开目录\",\n      \"app_config_dir\": \"配置目录\",\n      \"app_data_dir\": \"数据目录\",\n      \"core_dir\": \"内核目录\",\n      \"log_dir\": \"日志目录\"\n    },\n    \"proxy_action\": {\n      \"on\": \"开\",\n      \"off\": \"关\"\n    },\n    \"quit\": \"退出\",\n    \"rule_mode\": \"规则模式\",\n    \"script_mode\": \"脚本模式\",\n    \"system_proxy\": \"系统代理\",\n    \"tun_mode\": \"TUN 模式\"\n  },\n  \"dialog\": {\n    \"panic\": \"请将此问题汇报到 GitHub Issues。\",\n    \"migrate\": \"检测到旧版本配置文件，是否迁移到新版本?\\n警告：此操作会覆盖掉现有配置文件！\",\n    \"custom_app_dir_migrate\": \"你将要更改应用目录至 %{path}。\\n需要将现有数据迁移到新目录吗？\",\n    \"warning\": {\n      \"enable_tun_with_no_permission\": \"TUN 模式需要授予管理员权限或启用服务模式，当前都未开启，因此 TUN 模式将无法正常工作。\"\n    },\n    \"info\": {\n      \"grant_core_permission\": \"Clash 内核需要管理员权限才能使得 TUN 模式正常工作，是否授予？\\n\\n请注意：此操作需要输入密码。\"\n    }\n  },\n  \"setting\": {\n    \"connection\": {\n      \"interrupt\": {\n        \"proxy\": {\n          \"label\": \"当代理切换时打断连接\"\n        },\n        \"profile\": {\n          \"label\": \"当配置文件切换时打断连接\"\n        },\n        \"mode\": {\n          \"label\": \"当模式切换时打断连接\"\n        }\n      }\n    }\n  },\n  \"break_when_proxy_change\": \"当代理切换时打断连接\",\n  \"break_when_profile_change\": \"当配置文件切换时打断连接\",\n  \"break_when_mode_change\": \"当模式切换时打断连接\"\n}\n"
  },
  {
    "path": "backend/tauri/locales/zh-tw.json",
    "content": "{\n  \"_version\": 1,\n  \"tray\": {\n    \"copy_env\": {\n      \"cmd\": \"複製環境變數 (CMD)\",\n      \"ps\": \"複製環境變數 (PS)\",\n      \"sh\": \"複製環境變數 (SH)\"\n    },\n    \"no_proxies\": \"無代理\",\n    \"select_proxies\": \"選取代理\",\n    \"dashboard\": \"開啟儀表盤\",\n    \"direct_mode\": \"直連模式\",\n    \"global_mode\": \"全域模式\",\n    \"more\": {\n      \"menu\": \"更多\",\n      \"restart_app\": \"重啟 App\",\n      \"restart_clash\": \"重啟 Clash\"\n    },\n    \"open_dir\": {\n      \"menu\": \"打開目錄\",\n      \"app_config_dir\": \"設定檔目錄\",\n      \"app_data_dir\": \"資料目錄\",\n      \"core_dir\": \"核心目錄\",\n      \"log_dir\": \"日誌目錄\"\n    },\n    \"proxy_action\": {\n      \"on\": \"開\",\n      \"off\": \"關\"\n    },\n    \"quit\": \"退出\",\n    \"rule_mode\": \"規則模式\",\n    \"script_mode\": \"腳本模式\",\n    \"system_proxy\": \"系統代理\",\n    \"tun_mode\": \"TUN 模式\"\n  },\n  \"dialog\": {\n    \"panic\": \"請將此問題回報至 GitHub Issues。\",\n    \"migrate\": \"檢測到舊版本設定檔，是否遷移到新版本？\\n警告：此操作會覆蓋掉現有設定檔。\",\n    \"custom_app_dir_migrate\": \"你將要更改 App 目錄至 %{path}。\\n需要將現有資料遷移到新目錄嗎？\",\n    \"warning\": {\n      \"enable_tun_with_no_permission\": \"開啟 TUN 模式需要系統管理員權限或服務模式，目前都未啟用，因此 TUN 模式將無法正常工作。\"\n    },\n    \"info\": {\n      \"grant_core_permission\": \"Clash 核心需要系統管理員權限才能使 TUN 模式正常工作，是否授予？\\n請注意：此操作需要輸入密碼。\"\n    }\n  },\n  \"setting\": {\n    \"connection\": {\n      \"interrupt\": {\n        \"proxy\": {\n          \"label\": \"當代理切換時打斷連線\"\n        },\n        \"profile\": {\n          \"label\": \"當設定檔切換時打斷連線\"\n        },\n        \"mode\": {\n          \"label\": \"當模式切換時打斷連線\"\n        }\n      }\n    }\n  },\n  \"break_when_proxy_change\": \"當代理切換時打斷連線\",\n  \"break_when_profile_change\": \"當設定檔切換時打斷連線\",\n  \"break_when_mode_change\": \"當模式切換時打斷連線\"\n}\n"
  },
  {
    "path": "backend/tauri/overrides/fixed-webview2.conf.json",
    "content": "{\n  \"$schema\": \"../../../node_modules/@tauri-apps/cli/config.schema.json\",\n  \"bundle\": {\n    \"windows\": {\n      \"webviewInstallMode\": {\n        \"type\": \"fixedRuntime\",\n        \"path\": \"SHOULD_BE_REPLACED_WITH_THE_PATH_TO_THE_FIXED_WEBVIEW\"\n      }\n    }\n  },\n  \"plugins\": {\n    \"updater\": {\n      \"endpoints\": [\n        \"https://deno.elaina.moe/updater/update-fixed-webview-proxy.json\",\n        \"https://nyanpasu.surge.sh/updater/update-fixed-webview-proxy.json\",\n        \"https://gh-proxy.com/https://github.com/libnyanpasu/clash-nyanpasu/releases/download/updater/update-fixed-webview-proxy.json\",\n        \"https://github.com/libnyanpasu/clash-nyanpasu/releases/download/updater/update-fixed-webview.json\"\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "backend/tauri/overrides/nightly.conf.json",
    "content": "{\n  \"$schema\": \"../../../node_modules/@tauri-apps/cli/config.schema.json\",\n  \"version\": \"2.0.0\",\n  \"plugins\": {\n    \"updater\": {\n      \"pubkey\": \"dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDlBMUM0NjMxREZCNDRGMjYKUldRbVQ3VGZNVVljbW43N0FlWjA4UkNrbTgxSWxSSXJQcExXNkZjUTlTQkIyYkJzL0tsSWF2d0cK\",\n      \"endpoints\": [\n        \"https://deno.elaina.moe/updater/update-nightly-proxy.json\",\n        \"https://nyanpasu.surge.sh/updater/update-nightly-proxy.json\",\n        \"https://gh-proxy.com/https://github.com/libnyanpasu/clash-nyanpasu/releases/download/updater/update-nightly-proxy.json\",\n        \"https://github.com/libnyanpasu/clash-nyanpasu/releases/download/updater/update-nightly.json\"\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "backend/tauri/src/cmds/migrate.rs",
    "content": "use clap::Args;\n\nuse crate::core::migration::{\n    MigrationAdvice, Runner,\n    units::{find_migration, get_migrations},\n};\nuse colored::Colorize;\n\n#[derive(Debug, Args)]\npub struct MigrateOpts {\n    /// force to run migration without advice\n    #[arg(long, default_value = \"false\")]\n    skip_advice: bool,\n    /// Run specific migration\n    #[arg(long)]\n    migration: Option<String>,\n    /// Run migration up to specific version\n    #[arg(long)]\n    version: Option<String>,\n    /// List all migrations\n    #[arg(long)]\n    list: bool,\n}\n\n/// A fresh install instance should have a empty config dir,\n///\n/// The `app_config_dir` would create a new dir while access it.\nfn is_fresh_install_instance() -> bool {\n    crate::utils::dirs::app_config_dir()\n        .ok()\n        .and_then(|dir| std::fs::read_dir(dir).ok())\n        .is_some_and(|entry| {\n            let dirs = entry.collect::<Vec<Result<_, _>>>();\n            dirs.is_empty()\n        })\n}\n\npub fn parse(args: &MigrateOpts) {\n    let runner = if args.skip_advice {\n        Runner::new_with_skip_advice()\n    } else {\n        Runner::default()\n    };\n    if args.list {\n        println!(\"Available migrations:\\n\");\n        let migrations = get_migrations();\n        for migration in migrations {\n            let advice = runner.advice_migration(migration.as_ref());\n            println!(\n                \"[{}] {} - {}\",\n                match &advice {\n                    MigrationAdvice::Pending => format!(\"{advice}\").yellow(),\n                    MigrationAdvice::Ignored => format!(\"{advice}\").cyan(),\n                    MigrationAdvice::Done => format!(\"{advice}\").green(),\n                },\n                migration.version(),\n                migration.name()\n            );\n        }\n        std::process::exit(0);\n    }\n\n    if args.migration.is_some() && args.version.is_some() {\n        eprintln!(\"Please specify only one of migration or version.\");\n        std::process::exit(1);\n    }\n\n    // When `Drop`, commit the changes to the migration file.\n    let runner = runner.drop_guard();\n\n    if is_fresh_install_instance() {\n        eprintln!(\"Fresh install detected, skip all migrations\");\n        return;\n    }\n\n    if args.migration.is_none() && args.version.is_none() {\n        match crate::consts::BUILD_INFO.build_profile {\n            \"Nightly\" => {\n                println!(\"Running all upcoming migrations.\");\n                runner.run_upcoming_units().unwrap();\n            }\n            _ => {\n                println!(\n                    \"No migration or version specified. Running migrations up to current version.\"\n                );\n                runner\n                    .run_units_up_to_version(&runner.current_version)\n                    .unwrap();\n            }\n        }\n    }\n\n    if let Some(migration) = args.migration.as_ref() {\n        let migration = find_migration(migration);\n        match migration {\n            Some(migration) => {\n                runner.run_migration(migration.as_ref()).unwrap();\n            }\n            None => {\n                eprintln!(\"Migration not found.\");\n                std::process::exit(1);\n            }\n        }\n    } else if let Some(version) = args.version.as_deref() {\n        let version = semver::Version::parse(version).unwrap();\n        runner.run_units_up_to_version(&version).unwrap();\n    }\n}\n\n#[cfg(target_os = \"windows\")]\npub fn migrate_home_dir_handler(target_path: &str) -> anyhow::Result<()> {\n    use crate::utils::{self, dirs};\n    use anyhow::Context;\n    use deelevate::{PrivilegeLevel, Token};\n    use std::{borrow::Cow, path::PathBuf, process::Command, str::FromStr, thread, time::Duration};\n    use sysinfo::System;\n    use tauri::utils::platform::current_exe;\n    println!(\"target path {target_path}\");\n\n    let token = Token::with_current_process()?;\n    if let PrivilegeLevel::NotPrivileged = token.privilege_level()? {\n        eprintln!(\"Please run this command as admin to prevent authority issue.\");\n        std::process::exit(1);\n    }\n\n    let current_home_dir = dirs::app_config_dir()?;\n    let target_home_dir = PathBuf::from_str(target_path)?;\n\n    // 1. waiting for app exited\n    println!(\"waiting for app exited.\");\n    let placeholder = dirs::get_single_instance_placeholder()?;\n    let mut single_instance: single_instance::SingleInstance;\n    loop {\n        single_instance = single_instance::SingleInstance::new(&placeholder)\n            .context(\"failed to create single instance\")?;\n        if single_instance.is_single() {\n            break;\n        }\n        thread::sleep(Duration::from_secs(1));\n    }\n\n    // 2. kill all related processes.\n    let related_names = [\n        \"clash-verge-service\",\n        \"clash-nyanpasu-service\", // for upcoming v1.6.x\n        \"clash-rs\",\n        \"mihomo\",\n        \"mihomo-alpha\",\n        \"clash\",\n    ];\n    let sys = System::new_all();\n    'outer: for process in sys.processes().values() {\n        let process_name = process.name().to_string_lossy(); // TODO: check if it's utf-8\n        let process_name = if let Some(name) = process_name.strip_suffix(\".exe\") {\n            Cow::Borrowed(name)\n        } else {\n            process_name\n        };\n        for name in related_names.iter() {\n            if process_name.ends_with(name) {\n                println!(\"Process found: {process_name} should be killed. killing...\");\n                if !process.kill() {\n                    eprintln!(\"failed to kill {process_name}.\")\n                }\n                continue 'outer;\n            }\n        }\n    }\n\n    // 3. do config migrate and update the registry.\n    utils::init::do_config_migration(&current_home_dir, &target_home_dir)?;\n    utils::winreg::set_app_dir(target_home_dir.as_path())?;\n    println!(\"migration finished. starting application...\");\n    drop(single_instance); // release single instance lock\n\n    let app_path = current_exe()?;\n    thread::spawn(move || {\n        #[allow(clippy::zombie_processes)]\n        Command::new(app_path).spawn().unwrap();\n    });\n    thread::sleep(Duration::from_secs(5));\n    Ok(())\n}\n\n#[cfg(not(target_os = \"windows\"))]\npub fn migrate_home_dir_handler(_target_path: &str) -> anyhow::Result<()> {\n    Ok(())\n}\n"
  },
  {
    "path": "backend/tauri/src/cmds/mod.rs",
    "content": "use std::str::FromStr;\n\nuse crate::utils;\nuse anyhow::Ok;\nuse clap::{Parser, Subcommand};\nuse migrate::MigrateOpts;\nuse nyanpasu_egui::widget::StatisticWidgetVariant;\nuse tauri::utils::platform::current_exe;\n\nmod migrate;\n\n#[derive(Parser, Debug)]\n#[command(name = \"clash-nyanpasu\", version, about, long_about = None, disable_version_flag = true)]\n/// Clash Nyanpasu is a GUI client for Clash.\npub struct Cli {\n    /// Print the version\n    #[clap(short = 'v', long, default_value = \"false\")]\n    version: bool,\n    #[command(subcommand)]\n    command: Option<Commands>,\n    #[arg(raw = true)]\n    args: Vec<String>,\n}\n\n#[derive(Subcommand, Debug)]\nenum Commands {\n    /// Migrate home directory to another path.\n    MigrateHomeDir { target_path: String },\n    /// do migration\n    Migrate(MigrateOpts),\n\n    /// Collect the environment variables.\n    Collect,\n    /// A launch bridge to resolve the delay exit issue.\n    Launch {\n        #[arg(raw = true)]\n        args: Vec<String>,\n    },\n    /// Show a panic dialog while the application is enter panic handler.\n    PanicDialog { message: String },\n    /// Launch the Widget with the specified name.\n    StatisticWidget { variant: StatisticWidgetVariant },\n}\n\nstruct DelayedExitGuard;\nimpl DelayedExitGuard {\n    pub fn new() -> Self {\n        Self\n    }\n}\nimpl Drop for DelayedExitGuard {\n    fn drop(&mut self) {\n        std::thread::sleep(std::time::Duration::from_secs(5));\n    }\n}\n\npub fn parse() -> anyhow::Result<()> {\n    let cli = Cli::parse();\n    if cli.version {\n        print_version_info();\n    }\n    if let Some(commands) = &cli.command {\n        let guard = DelayedExitGuard::new();\n        match commands {\n            Commands::Migrate(opts) => {\n                migrate::parse(opts);\n            }\n            Commands::MigrateHomeDir { target_path } => {\n                migrate::migrate_home_dir_handler(target_path).unwrap();\n            }\n            Commands::Launch { args } => {\n                let _ = utils::init::check_singleton().unwrap();\n                let appimage: Option<String> = {\n                    #[cfg(target_os = \"linux\")]\n                    {\n                        std::env::var_os(\"APPIMAGE\").map(|s| s.to_string_lossy().to_string())\n                    }\n                    #[cfg(not(target_os = \"linux\"))]\n                    None\n                };\n                let path = match appimage {\n                    Some(appimage) => std::path::PathBuf::from_str(&appimage).unwrap(),\n                    None => current_exe().unwrap(),\n                };\n                // let args = args.clone();\n                // args.extend(vec![\"--\".to_string()]);\n                #[allow(clippy::zombie_processes)]\n                std::process::Command::new(path).args(args).spawn().unwrap();\n            }\n            Commands::Collect => {\n                let envs = crate::utils::collect::collect_envs().unwrap();\n                println!(\"{envs:#?}\");\n            }\n            Commands::PanicDialog { message } => {\n                crate::utils::dialog::panic_dialog(message);\n            }\n            Commands::StatisticWidget { variant } => {\n                nyanpasu_egui::widget::start_statistic_widget(*variant)\n                    .expect(\"Failed to start statistic widget\");\n            }\n        }\n        drop(guard);\n        std::process::exit(0);\n    }\n    Ok(()) // bypass\n}\n\nfn print_version_info() {\n    use crate::consts::*;\n    use ansi_str::AnsiStr;\n    use chrono::{DateTime, Utc};\n    use colored::*;\n    use timeago::Formatter;\n    let build_info = &BUILD_INFO;\n\n    let now = Utc::now();\n    let formatter = Formatter::new();\n    let commit_time = formatter.convert_chrono(\n        DateTime::parse_from_rfc3339(build_info.commit_date).unwrap(),\n        now,\n    );\n    let commit_time_width = commit_time.len() + build_info.commit_date.len() + 3;\n    let build_time = formatter.convert_chrono(\n        DateTime::parse_from_rfc3339(build_info.build_date).unwrap(),\n        now,\n    );\n    let build_time_width = build_time.len() + build_info.build_date.len() + 3;\n    let commit_info_width = build_info.commit_hash.len() + build_info.commit_author.len() + 4;\n    let col_width = commit_info_width\n        .max(commit_time_width)\n        .max(build_time_width)\n        .max(build_info.build_platform.len())\n        .max(build_info.rustc_version.len())\n        .max(build_info.llvm_version.len())\n        + 2;\n    let header_width = col_width + 16;\n    println!(\n        \"{} v{} ({} Build)\\n\",\n        build_info.app_name,\n        build_info.pkg_version,\n        build_info.build_profile.yellow()\n    );\n    println!(\"╭{:─^width$}╮\", \" Build Information \", width = header_width);\n\n    let mut line = format!(\n        \"{} by {}\",\n        build_info.commit_hash.green(),\n        build_info.commit_author.blue()\n    );\n\n    let mut pad = col_width - line.ansi_strip().len();\n    println!(\"│{:>14}: {}{}│\", \"Commit Info\", line, \" \".repeat(pad));\n\n    line = format!(\"{} ({})\", commit_time.red(), build_info.commit_date.cyan());\n    pad = col_width - line.ansi_strip().len();\n    println!(\"│{:>14}: {}{}│\", \"Commit Time\", line, \" \".repeat(pad));\n\n    line = format!(\"{} ({})\", build_time.red(), build_info.build_date.cyan());\n    pad = col_width - line.ansi_strip().len();\n    println!(\"│{:>14}: {}{}│\", \"Build Time\", line, \" \".repeat(pad));\n\n    println!(\n        \"│{:>14}: {:<col_width$}│\",\n        \"Build Target\",\n        build_info.build_platform.bright_yellow()\n    );\n    println!(\n        \"│{:>14}: {:<col_width$}│\",\n        \"Rust Version\",\n        build_info.rustc_version.bright_yellow()\n    );\n    println!(\n        \"│{:>14}: {:<col_width$}│\",\n        \"LLVM Version\",\n        build_info.llvm_version.bright_yellow()\n    );\n    println!(\"╰{:─^width$}╯\", \"\", width = header_width);\n    std::process::exit(0);\n}\n"
  },
  {
    "path": "backend/tauri/src/config/clash/mod.rs",
    "content": "use crate::utils::{\n    dirs,\n    help::{self, get_clash_external_port},\n};\nuse anyhow::Result;\nuse log::warn;\nuse serde::{Deserialize, Serialize};\nuse serde_yaml::{Mapping, Value};\nuse std::{\n    net::{IpAddr, Ipv4Addr, SocketAddr},\n    str::FromStr,\n};\nuse tracing_attributes::instrument;\n\nuse super::Config;\n\n#[derive(Default, Debug, Clone)]\npub struct IClashTemp(pub Mapping);\n\nimpl IClashTemp {\n    pub fn new() -> Self {\n        match dirs::clash_guard_overrides_path().and_then(|path| help::read_merge_mapping(&path)) {\n            Ok(map) => Self(Self::guard(map)),\n            Err(err) => {\n                log::error!(target: \"app\", \"{err:?}\");\n                Self::template()\n            }\n        }\n    }\n\n    pub fn template() -> Self {\n        let mut map = Mapping::new();\n\n        map.insert(\"mixed-port\".into(), 7890.into());\n        map.insert(\"log-level\".into(), \"info\".into());\n        map.insert(\"allow-lan\".into(), false.into());\n        map.insert(\"mode\".into(), \"rule\".into());\n        #[cfg(debug_assertions)]\n        map.insert(\"external-controller\".into(), \"127.0.0.1:9872\".into());\n        #[cfg(not(debug_assertions))]\n        map.insert(\"external-controller\".into(), \"127.0.0.1:17650\".into());\n        map.insert(\n            \"secret\".into(),\n            uuid::Uuid::new_v4().to_string().to_lowercase().into(), // generate a uuid v4 as default secret to secure the communication between clash and the client\n        );\n        #[cfg(feature = \"default-meta\")]\n        map.insert(\"unified-delay\".into(), true.into());\n        #[cfg(feature = \"default-meta\")]\n        map.insert(\"tcp-concurrent\".into(), true.into());\n        map.insert(\"ipv6\".into(), false.into());\n\n        Self(map)\n    }\n\n    fn guard(mut config: Mapping) -> Mapping {\n        let port = Self::guard_mixed_port(&config);\n        let ctrl = Self::guard_server_ctrl(&config);\n\n        config.insert(\"mixed-port\".into(), port.into());\n        config.insert(\"external-controller\".into(), ctrl.into());\n        config\n    }\n\n    pub fn patch_config(&mut self, patch: Mapping) {\n        for (key, value) in patch.into_iter() {\n            self.0.insert(key, value);\n        }\n    }\n\n    pub fn save_config(&self) -> Result<()> {\n        help::save_yaml(\n            &dirs::clash_guard_overrides_path()?,\n            &self.0,\n            Some(\"# Generated by Clash Nyanpasu\"),\n        )\n    }\n\n    pub fn get_mixed_port(&self) -> u16 {\n        Self::guard_mixed_port(&self.0)\n    }\n\n    pub fn get_client_info(&self) -> ClashInfo {\n        let config = &self.0;\n\n        ClashInfo {\n            port: Self::guard_mixed_port(config),\n            server: Self::guard_client_ctrl(config),\n            secret: config.get(\"secret\").and_then(|value| match value {\n                Value::String(val_str) => Some(val_str.clone()),\n                Value::Bool(val_bool) => Some(val_bool.to_string()),\n                Value::Number(val_num) => Some(val_num.to_string()),\n                _ => None,\n            }),\n        }\n    }\n\n    #[allow(dead_code)]\n    pub fn get_external_controller_port(&self) -> u16 {\n        let server = self.get_client_info().server;\n        let port = server.split(':').next_back().unwrap_or(\"9090\");\n        port.parse().unwrap_or(9090)\n    }\n\n    #[instrument]\n    pub fn prepare_external_controller_port(&mut self) -> Result<()> {\n        let strategy = Config::verge()\n            .latest()\n            .get_external_controller_port_strategy();\n        let server = self.get_client_info().server;\n        let (server_ip, server_port) = server.split_once(':').unwrap_or((\"127.0.0.1\", \"9090\"));\n        let server_port = server_port.parse::<u16>().unwrap_or(9090);\n        let port = get_clash_external_port(&strategy, server_port)?;\n        if port != server_port {\n            let new_server = format!(\"{server_ip}:{port}\");\n            warn!(\"The external controller port has been changed to {new_server}\");\n            let mut map = Mapping::new();\n            map.insert(\"external-controller\".into(), new_server.into());\n            self.patch_config(map);\n        }\n        Ok(())\n    }\n\n    pub fn guard_mixed_port(config: &Mapping) -> u16 {\n        let mut port = config\n            .get(\"mixed-port\")\n            .and_then(|value| match value {\n                Value::String(val_str) => val_str.parse().ok(),\n                Value::Number(val_num) => val_num.as_u64().map(|u| u as u16),\n                _ => None,\n            })\n            .unwrap_or(7890);\n        if port == 0 {\n            port = 7890;\n        }\n        port\n    }\n\n    pub fn guard_server_ctrl(config: &Mapping) -> String {\n        config\n            .get(\"external-controller\")\n            .and_then(|value| match value.as_str() {\n                Some(val_str) => {\n                    let val_str = val_str.trim();\n\n                    let val = match val_str.starts_with(':') {\n                        true => format!(\"127.0.0.1{val_str}\"),\n                        false => val_str.to_owned(),\n                    };\n\n                    SocketAddr::from_str(val.as_str())\n                        .ok()\n                        .map(|s| s.to_string())\n                }\n                None => None,\n            })\n            .unwrap_or(\"127.0.0.1:9090\".into())\n    }\n\n    pub fn guard_client_ctrl(config: &Mapping) -> String {\n        let value = Self::guard_server_ctrl(config);\n        match SocketAddr::from_str(value.as_str()) {\n            Ok(mut socket) => {\n                if socket.ip().is_unspecified() {\n                    socket.set_ip(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)));\n                }\n                socket.to_string()\n            }\n            Err(_) => \"127.0.0.1:9090\".into(),\n        }\n    }\n\n    #[allow(unused)]\n    pub fn get_tun_device_ip(&self) -> String {\n        let config = &self.0;\n\n        let ip = config\n            .get(\"dns\")\n            .and_then(|value| match value {\n                Value::Mapping(val_map) => Some(val_map.get(\"fake-ip-range\").and_then(\n                    |fake_ip_range| match fake_ip_range {\n                        Value::String(ip_range_val) => Some(ip_range_val.replace(\"1/16\", \"2\")),\n                        _ => None,\n                    },\n                )),\n                _ => None,\n            })\n            // 默认IP\n            .unwrap_or(Some(\"198.18.0.2\".to_string()));\n\n        ip.unwrap()\n    }\n}\n\n#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq, specta::Type)]\npub struct ClashInfo {\n    /// clash core port\n    pub port: u16,\n    /// same as `external-controller`\n    pub server: String,\n    /// clash secret\n    pub secret: Option<String>,\n}\n\n#[test]\nfn test_clash_info() {\n    fn get_case<T: Into<Value>, D: Into<Value>>(mp: T, ec: D) -> ClashInfo {\n        let mut map = Mapping::new();\n        map.insert(\"mixed-port\".into(), mp.into());\n        map.insert(\"external-controller\".into(), ec.into());\n\n        IClashTemp(IClashTemp::guard(map)).get_client_info()\n    }\n\n    fn get_result<S: Into<String>>(port: u16, server: S) -> ClashInfo {\n        ClashInfo {\n            port,\n            server: server.into(),\n            secret: None,\n        }\n    }\n\n    assert_eq!(\n        IClashTemp(IClashTemp::guard(Mapping::new())).get_client_info(),\n        get_result(7890, \"127.0.0.1:9090\")\n    );\n\n    assert_eq!(get_case(\"\", \"\"), get_result(7890, \"127.0.0.1:9090\"));\n\n    assert_eq!(get_case(65537, \"\"), get_result(1, \"127.0.0.1:9090\"));\n\n    assert_eq!(\n        get_case(8888, \"127.0.0.1:8888\"),\n        get_result(8888, \"127.0.0.1:8888\")\n    );\n\n    assert_eq!(\n        get_case(8888, \"   :98888 \"),\n        get_result(8888, \"127.0.0.1:9090\")\n    );\n\n    assert_eq!(\n        get_case(8888, \"0.0.0.0:8080  \"),\n        get_result(8888, \"127.0.0.1:8080\")\n    );\n\n    assert_eq!(\n        get_case(8888, \"0.0.0.0:8080\"),\n        get_result(8888, \"127.0.0.1:8080\")\n    );\n\n    assert_eq!(\n        get_case(8888, \"[::]:8080\"),\n        get_result(8888, \"127.0.0.1:8080\")\n    );\n\n    assert_eq!(\n        get_case(8888, \"192.168.1.1:8080\"),\n        get_result(8888, \"192.168.1.1:8080\")\n    );\n\n    assert_eq!(\n        get_case(8888, \"192.168.1.1:80800\"),\n        get_result(8888, \"127.0.0.1:9090\")\n    );\n}\n\n#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]\n#[serde(rename_all = \"kebab-case\")]\npub struct IClash {\n    pub mixed_port: Option<u16>,\n    pub allow_lan: Option<bool>,\n    pub log_level: Option<String>,\n    pub ipv6: Option<bool>,\n    pub mode: Option<String>,\n    pub external_controller: Option<String>,\n    pub secret: Option<String>,\n    pub dns: Option<IClashDNS>,\n    pub tun: Option<IClashTUN>,\n    pub interface_name: Option<String>,\n}\n\n#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]\n#[serde(rename_all = \"kebab-case\")]\npub struct IClashTUN {\n    pub enable: Option<bool>,\n    pub stack: Option<String>,\n    pub auto_route: Option<bool>,\n    pub auto_detect_interface: Option<bool>,\n    pub dns_hijack: Option<Vec<String>>,\n}\n\n#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]\n#[serde(rename_all = \"kebab-case\")]\npub struct IClashDNS {\n    pub enable: Option<bool>,\n    pub listen: Option<String>,\n    pub default_nameserver: Option<Vec<String>>,\n    pub enhanced_mode: Option<String>,\n    pub fake_ip_range: Option<String>,\n    pub use_hosts: Option<bool>,\n    pub fake_ip_filter: Option<Vec<String>>,\n    pub nameserver: Option<Vec<String>>,\n    pub fallback: Option<Vec<String>>,\n    pub fallback_filter: Option<IClashFallbackFilter>,\n    pub nameserver_policy: Option<Vec<String>>,\n}\n\n#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]\n#[serde(rename_all = \"kebab-case\")]\npub struct IClashFallbackFilter {\n    pub geoip: Option<bool>,\n    pub geoip_code: Option<String>,\n    pub ipcidr: Option<Vec<String>>,\n    pub domain: Option<Vec<String>>,\n}\n"
  },
  {
    "path": "backend/tauri/src/config/core.rs",
    "content": "use super::{Draft, IClashTemp, IRuntime, IVerge, Profiles};\nuse crate::{\n    core::state::ManagedState,\n    enhance,\n    utils::{dirs, help},\n};\nuse anyhow::{Result, anyhow};\nuse nyanpasu_utils::runtime::block_on;\nuse once_cell::sync::OnceCell;\nuse std::{env::temp_dir, path::PathBuf};\n\npub const RUNTIME_CONFIG: &str = \"clash-config.yaml\";\npub const CHECK_CONFIG: &str = \"clash-config-check.yaml\";\n\npub struct Config {\n    clash_config: Draft<IClashTemp>,\n    verge_config: Draft<IVerge>,\n    profiles_config: ManagedState<Profiles>,\n    runtime_config: Draft<IRuntime>,\n}\n\nimpl Config {\n    pub fn global() -> &'static Config {\n        static CONFIG: OnceCell<Config> = OnceCell::new();\n\n        CONFIG.get_or_init(|| Config {\n            clash_config: Draft::from(IClashTemp::new()),\n            verge_config: Draft::from(IVerge::new()),\n            profiles_config: ManagedState::from(Profiles::new()),\n            runtime_config: Draft::from(IRuntime::new()),\n        })\n    }\n\n    pub fn clash() -> Draft<IClashTemp> {\n        Self::global().clash_config.clone()\n    }\n\n    pub fn verge() -> Draft<IVerge> {\n        Self::global().verge_config.clone()\n    }\n\n    pub fn profiles() -> &'static ManagedState<Profiles> {\n        &Self::global().profiles_config\n    }\n\n    pub fn runtime() -> Draft<IRuntime> {\n        Self::global().runtime_config.clone()\n    }\n\n    /// 初始化配置\n    pub fn init_config() -> Result<()> {\n        crate::log_err!(block_on(Self::generate()));\n        if let Err(err) = Self::generate_file(ConfigType::Run) {\n            log::error!(target: \"app\", \"{err:?}\");\n\n            let runtime_path = dirs::app_config_dir()?.join(RUNTIME_CONFIG);\n            // 如果不存在就将默认的clash文件拿过来\n            if !runtime_path.exists() {\n                help::save_yaml(\n                    &runtime_path,\n                    &Config::clash().latest().0,\n                    Some(\"# Clash Nyanpasu Runtime\"),\n                )?;\n            }\n        }\n        Ok(())\n    }\n\n    /// 将配置丢到对应的文件中\n    pub fn generate_file(typ: ConfigType) -> Result<PathBuf> {\n        let path = match typ {\n            ConfigType::Run => dirs::app_config_dir()?.join(RUNTIME_CONFIG),\n            ConfigType::Check => temp_dir().join(CHECK_CONFIG),\n        };\n\n        let runtime = Config::runtime();\n        let runtime = runtime.latest();\n        let config = runtime\n            .config\n            .as_ref()\n            .ok_or(anyhow!(\"failed to get runtime config\"))?;\n\n        help::save_yaml(&path, &config, Some(\"# Generated by Clash Nyanpasu\"))?;\n        Ok(path)\n    }\n\n    /// 生成配置存好\n    pub async fn generate() -> Result<()> {\n        let (config, exists_keys, postprocessing_outputs) = enhance::enhance().await;\n\n        *Config::runtime().draft() = IRuntime {\n            config: Some(config),\n            exists_keys,\n            postprocessing_output: postprocessing_outputs,\n        };\n\n        Ok(())\n    }\n}\n\n#[derive(Debug)]\npub enum ConfigType {\n    Run,\n    Check,\n}\n"
  },
  {
    "path": "backend/tauri/src/config/draft.rs",
    "content": "use super::{IClashTemp, IRuntime, IVerge};\nuse parking_lot::{MappedMutexGuard, Mutex, MutexGuard};\nuse std::sync::Arc;\n\n#[derive(Debug, Clone)]\npub struct Draft<T: Clone + ToOwned> {\n    inner: Arc<Mutex<(T, Option<T>)>>,\n}\n\nmacro_rules! draft_define {\n    ($id: ident) => {\n        impl Draft<$id> {\n            #[allow(unused)]\n            pub fn data(&self) -> MappedMutexGuard<$id> {\n                MutexGuard::map(self.inner.lock(), |guard| &mut guard.0)\n            }\n\n            pub fn latest(&self) -> MappedMutexGuard<$id> {\n                MutexGuard::map(self.inner.lock(), |inner| {\n                    if inner.1.is_none() {\n                        &mut inner.0\n                    } else {\n                        inner.1.as_mut().unwrap()\n                    }\n                })\n            }\n\n            pub fn draft(&self) -> MappedMutexGuard<$id> {\n                MutexGuard::map(self.inner.lock(), |inner| {\n                    if inner.1.is_none() {\n                        inner.1 = Some(inner.0.clone());\n                    }\n\n                    inner.1.as_mut().unwrap()\n                })\n            }\n\n            pub fn apply(&self) -> Option<$id> {\n                let mut inner = self.inner.lock();\n\n                match inner.1.take() {\n                    Some(draft) => {\n                        let old_value = inner.0.to_owned();\n                        inner.0 = draft.to_owned();\n                        Some(old_value)\n                    }\n                    None => None,\n                }\n            }\n\n            pub fn discard(&self) -> Option<$id> {\n                let mut inner = self.inner.lock();\n                inner.1.take()\n            }\n        }\n\n        impl From<$id> for Draft<$id> {\n            fn from(data: $id) -> Self {\n                Draft {\n                    inner: Arc::new(Mutex::new((data, None))),\n                }\n            }\n        }\n    };\n}\n\n// draft_define!(IClash);\ndraft_define!(IClashTemp);\ndraft_define!(IRuntime);\ndraft_define!(IVerge);\n\nimpl Draft<IClashTemp> {\n    /// Reload configuration from file\n    pub fn reload(&self) {\n        let new_config = IClashTemp::new();\n        let mut inner = self.inner.lock();\n        inner.0 = new_config;\n        inner.1 = None; // Clear any draft\n    }\n}\n\n#[test]\nfn test_draft() {\n    let verge = IVerge {\n        enable_auto_launch: Some(true),\n        enable_tun_mode: Some(false),\n        ..IVerge::default()\n    };\n\n    let draft = Draft::from(verge);\n\n    assert_eq!(draft.data().enable_auto_launch, Some(true));\n    assert_eq!(draft.data().enable_tun_mode, Some(false));\n\n    assert_eq!(draft.draft().enable_auto_launch, Some(true));\n    assert_eq!(draft.draft().enable_tun_mode, Some(false));\n\n    let mut d = draft.draft();\n    d.enable_auto_launch = Some(false);\n    d.enable_tun_mode = Some(true);\n    drop(d);\n\n    assert_eq!(draft.data().enable_auto_launch, Some(true));\n    assert_eq!(draft.data().enable_tun_mode, Some(false));\n\n    assert_eq!(draft.draft().enable_auto_launch, Some(false));\n    assert_eq!(draft.draft().enable_tun_mode, Some(true));\n\n    assert_eq!(draft.latest().enable_auto_launch, Some(false));\n    assert_eq!(draft.latest().enable_tun_mode, Some(true));\n\n    assert!(draft.apply().is_some());\n    assert!(draft.apply().is_none());\n\n    assert_eq!(draft.data().enable_auto_launch, Some(false));\n    assert_eq!(draft.data().enable_tun_mode, Some(true));\n\n    assert_eq!(draft.draft().enable_auto_launch, Some(false));\n    assert_eq!(draft.draft().enable_tun_mode, Some(true));\n\n    let mut d = draft.draft();\n    d.enable_auto_launch = Some(true);\n    drop(d);\n\n    assert_eq!(draft.data().enable_auto_launch, Some(false));\n\n    assert_eq!(draft.draft().enable_auto_launch, Some(true));\n\n    assert!(draft.discard().is_some());\n\n    assert_eq!(draft.data().enable_auto_launch, Some(false));\n\n    assert!(draft.discard().is_none());\n\n    assert_eq!(draft.draft().enable_auto_launch, Some(false));\n}\n"
  },
  {
    "path": "backend/tauri/src/config/mod.rs",
    "content": "mod clash;\nmod core;\nmod draft;\npub mod nyanpasu;\npub mod profile;\nmod runtime;\npub use self::{\n    clash::*,\n    core::*,\n    draft::*,\n    profile::{item::*, profiles::*},\n    runtime::*,\n};\n\npub use self::nyanpasu::IVerge;\n"
  },
  {
    "path": "backend/tauri/src/config/nyanpasu/clash_strategy.rs",
    "content": "use serde::{Deserialize, Serialize};\nuse specta::Type;\n\n#[derive(Default, Debug, Clone, Deserialize, Serialize, Type)]\npub struct ClashStrategy {\n    pub external_controller_port_strategy: ExternalControllerPortStrategy,\n}\n\n#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Type)]\n#[serde(rename_all = \"snake_case\")]\npub enum ExternalControllerPortStrategy {\n    Fixed,\n    Random,\n    #[default]\n    AllowFallback,\n}\n\nimpl super::IVerge {\n    pub fn get_external_controller_port_strategy(&self) -> ExternalControllerPortStrategy {\n        self.clash_strategy\n            .as_ref()\n            .unwrap_or(&ClashStrategy::default())\n            .external_controller_port_strategy\n            .to_owned()\n    }\n}\n"
  },
  {
    "path": "backend/tauri/src/config/nyanpasu/logging.rs",
    "content": "use super::IVerge;\nuse serde::{Deserialize, Serialize};\nuse strum::{Display, EnumString};\nuse tracing_subscriber::filter;\n\n#[derive(Deserialize, Serialize, Debug, Clone, specta::Type, EnumString, Display)]\n#[strum(serialize_all = \"kebab-case\")]\npub enum LoggingLevel {\n    #[serde(rename = \"silent\", alias = \"off\")]\n    Silent,\n    #[serde(rename = \"trace\", alias = \"tracing\")]\n    Trace,\n    #[serde(rename = \"debug\")]\n    Debug,\n    #[serde(rename = \"info\")]\n    Info,\n    #[serde(rename = \"warn\", alias = \"warning\")]\n    Warn,\n    #[serde(rename = \"error\")]\n    Error,\n}\n\nimpl Default for LoggingLevel {\n    #[cfg(debug_assertions)]\n    fn default() -> Self {\n        Self::Trace\n    }\n\n    #[cfg(not(debug_assertions))]\n    fn default() -> Self {\n        Self::Info\n    }\n}\n\nimpl From<LoggingLevel> for filter::LevelFilter {\n    fn from(level: LoggingLevel) -> Self {\n        match level {\n            LoggingLevel::Silent => filter::LevelFilter::OFF,\n            LoggingLevel::Trace => filter::LevelFilter::TRACE,\n            LoggingLevel::Debug => filter::LevelFilter::DEBUG,\n            LoggingLevel::Info => filter::LevelFilter::INFO,\n            LoggingLevel::Warn => filter::LevelFilter::WARN,\n            LoggingLevel::Error => filter::LevelFilter::ERROR,\n        }\n    }\n}\n\nimpl IVerge {\n    pub fn get_log_level(&self) -> LoggingLevel {\n        self.app_log_level.clone().unwrap_or_default()\n    }\n}\n"
  },
  {
    "path": "backend/tauri/src/config/nyanpasu/mod.rs",
    "content": "use crate::utils::{dirs, help};\nuse anyhow::Result;\n// use log::LevelFilter;\nuse enumflags2::bitflags;\nuse nyanpasu_macro::VergePatch;\nuse serde::{Deserialize, Serialize};\nuse specta::Type;\n\n/// Validates if a string is a valid hex color code\npub fn is_hex_color(color: &str) -> bool {\n    if color.len() != 7 || !color.starts_with('#') {\n        return false;\n    }\n\n    color[1..].chars().all(|c| c.is_ascii_hexdigit())\n}\n\nmod clash_strategy;\npub mod logging;\nmod widget;\n\npub use self::clash_strategy::{ClashStrategy, ExternalControllerPortStrategy};\npub use logging::LoggingLevel;\npub use widget::NetworkStatisticWidgetConfig;\n\n// TODO: when support sing-box, remove this struct\n#[bitflags]\n#[repr(u8)]\n#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Type)]\npub enum ClashCore {\n    #[serde(rename = \"clash\", alias = \"clash-premium\")]\n    ClashPremium = 0b0001,\n    #[serde(rename = \"clash-rs\")]\n    ClashRs,\n    #[serde(rename = \"mihomo\", alias = \"clash-meta\")]\n    Mihomo,\n    #[serde(rename = \"mihomo-alpha\")]\n    MihomoAlpha,\n    #[serde(rename = \"clash-rs-alpha\")]\n    ClashRsAlpha,\n}\n\nimpl Default for ClashCore {\n    fn default() -> Self {\n        match cfg!(feature = \"default-meta\") {\n            false => Self::ClashPremium,\n            true => Self::Mihomo,\n        }\n    }\n}\n\nimpl From<ClashCore> for String {\n    fn from(core: ClashCore) -> Self {\n        match core {\n            ClashCore::ClashPremium => \"clash\".into(),\n            ClashCore::ClashRs => \"clash-rs\".into(),\n            ClashCore::Mihomo => \"mihomo\".into(),\n            ClashCore::MihomoAlpha => \"mihomo-alpha\".into(),\n            ClashCore::ClashRsAlpha => \"clash-rs-alpha\".into(),\n        }\n    }\n}\n\nimpl std::fmt::Display for ClashCore {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        match self {\n            ClashCore::ClashPremium => write!(f, \"clash\"),\n            ClashCore::ClashRs => write!(f, \"clash-rs\"),\n            ClashCore::Mihomo => write!(f, \"mihomo\"),\n            ClashCore::MihomoAlpha => write!(f, \"mihomo-alpha\"),\n            ClashCore::ClashRsAlpha => write!(f, \"clash-rs-alpha\"),\n        }\n    }\n}\n\nimpl From<&ClashCore> for nyanpasu_utils::core::CoreType {\n    fn from(core: &ClashCore) -> Self {\n        match core {\n            ClashCore::ClashPremium => nyanpasu_utils::core::CoreType::Clash(\n                nyanpasu_utils::core::ClashCoreType::ClashPremium,\n            ),\n            ClashCore::ClashRs => nyanpasu_utils::core::CoreType::Clash(\n                nyanpasu_utils::core::ClashCoreType::ClashRust,\n            ),\n            ClashCore::Mihomo => {\n                nyanpasu_utils::core::CoreType::Clash(nyanpasu_utils::core::ClashCoreType::Mihomo)\n            }\n            ClashCore::MihomoAlpha => nyanpasu_utils::core::CoreType::Clash(\n                nyanpasu_utils::core::ClashCoreType::MihomoAlpha,\n            ),\n            ClashCore::ClashRsAlpha => nyanpasu_utils::core::CoreType::Clash(\n                nyanpasu_utils::core::ClashCoreType::ClashRustAlpha,\n            ),\n        }\n    }\n}\n\nimpl TryFrom<&nyanpasu_utils::core::CoreType> for ClashCore {\n    type Error = anyhow::Error;\n\n    fn try_from(core: &nyanpasu_utils::core::CoreType) -> Result<Self> {\n        match core {\n            nyanpasu_utils::core::CoreType::Clash(clash) => match clash {\n                nyanpasu_utils::core::ClashCoreType::ClashPremium => Ok(ClashCore::ClashPremium),\n                nyanpasu_utils::core::ClashCoreType::ClashRust => Ok(ClashCore::ClashRs),\n                nyanpasu_utils::core::ClashCoreType::ClashRustAlpha => Ok(ClashCore::ClashRsAlpha),\n                nyanpasu_utils::core::ClashCoreType::Mihomo => Ok(ClashCore::Mihomo),\n                nyanpasu_utils::core::ClashCoreType::MihomoAlpha => Ok(ClashCore::MihomoAlpha),\n            },\n            _ => Err(anyhow::anyhow!(\"unsupported core type\")),\n        }\n    }\n}\n\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Default, Type)]\n#[serde(rename_all = \"snake_case\")]\npub enum ProxiesSelectorMode {\n    Hidden,\n    #[default]\n    Normal,\n    Submenu,\n}\n\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Default, Type)]\n#[serde(rename_all = \"snake_case\")]\npub enum TunStack {\n    System,\n    #[default]\n    Gvisor,\n    Mixed,\n}\n\nimpl AsRef<str> for TunStack {\n    fn as_ref(&self) -> &str {\n        match self {\n            TunStack::System => \"system\",\n            TunStack::Gvisor => \"gvisor\",\n            TunStack::Mixed => \"mixed\",\n        }\n    }\n}\n\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Default, Type)]\n#[serde(rename_all = \"snake_case\")]\npub enum BreakWhenProxyChange {\n    #[default]\n    None,\n    Chain,\n    All,\n}\n\n/// ### `verge.yaml` schema\n#[derive(Default, Debug, Clone, Deserialize, Serialize, VergePatch, specta::Type)]\n#[verge(patch_fn = \"patch_config\")]\n// TODO: use new managedState and builder pattern instead\npub struct IVerge {\n    /// app listening port for app singleton\n    pub app_singleton_port: Option<u16>,\n\n    /// app log level\n    /// silent | error | warn | info | debug | trace\n    pub app_log_level: Option<logging::LoggingLevel>,\n\n    // i18n\n    pub language: Option<String>,\n\n    /// `light` or `dark` or `system`\n    pub theme_mode: Option<String>,\n\n    /// enable traffic graph default is true\n    pub traffic_graph: Option<bool>,\n\n    /// show memory info (only for Clash Meta)\n    pub enable_memory_usage: Option<bool>,\n\n    /// global ui framer motion effects\n    pub lighten_animation_effects: Option<bool>,\n\n    /// clash tun mode\n    pub enable_tun_mode: Option<bool>,\n\n    /// windows service mode\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub enable_service_mode: Option<bool>,\n\n    /// can the app auto startup\n    pub enable_auto_launch: Option<bool>,\n\n    /// not show the window on launch\n    pub enable_silent_start: Option<bool>,\n\n    /// set system proxy\n    pub enable_system_proxy: Option<bool>,\n\n    /// enable proxy guard\n    pub enable_proxy_guard: Option<bool>,\n\n    /// set system proxy bypass\n    pub system_proxy_bypass: Option<String>,\n\n    /// proxy guard interval\n    #[serde(alias = \"proxy_guard_duration\")]\n    pub proxy_guard_interval: Option<u64>,\n\n    /// theme setting\n    pub theme_color: Option<String>,\n\n    /// web ui list\n    pub web_ui_list: Option<Vec<String>>,\n\n    /// clash core path\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub clash_core: Option<ClashCore>,\n\n    /// hotkey map\n    /// format: {func},{key}\n    pub hotkeys: Option<Vec<String>>,\n\n    /// 切换代理时自动关闭连接 (已弃用)\n    #[deprecated(note = \"use `break_when_proxy_change` instead\")]\n    pub auto_close_connection: Option<bool>,\n\n    /// 切换代理时中断连接\n    /// None: 不中断\n    /// Chain: 仅中断使用该代理链的连接\n    /// All: 中断所有连接\n    pub break_when_proxy_change: Option<BreakWhenProxyChange>,\n\n    /// 切换配置时中断连接\n    /// true: 中断所有连接\n    /// false: 不中断连接\n    pub break_when_profile_change: Option<bool>,\n\n    /// 切换模式时中断连接\n    /// true: 中断所有连接\n    /// false: 不中断连接\n    pub break_when_mode_change: Option<bool>,\n\n    /// 默认的延迟测试连接\n    pub default_latency_test: Option<String>,\n\n    /// 支持关闭字段过滤，避免meta的新字段都被过滤掉，默认为真\n    pub enable_clash_fields: Option<bool>,\n\n    /// 是否使用内部的脚本支持，默认为真\n    pub enable_builtin_enhanced: Option<bool>,\n\n    /// proxy 页面布局 列数\n    pub proxy_layout_column: Option<i32>,\n\n    /// 日志清理\n    /// 分钟数； 0 为不清理\n    #[deprecated(note = \"use `max_log_files` instead\")]\n    pub auto_log_clean: Option<i64>,\n    /// 日记轮转时间，单位：天\n    pub max_log_files: Option<usize>,\n    /// window size and position\n    #[deprecated(note = \"use `window_size_state` instead\")]\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub window_size_position: Option<Vec<f64>>,\n\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub window_size_state: Option<WindowState>,\n\n    /// 是否启用随机端口\n    pub enable_random_port: Option<bool>,\n\n    /// verge mixed port 用于覆盖 clash 的 mixed port\n    pub verge_mixed_port: Option<u16>,\n\n    /// Check update when app launch\n    pub enable_auto_check_update: Option<bool>,\n\n    /// Clash 相关策略\n    pub clash_strategy: Option<ClashStrategy>,\n\n    /// 是否启用代理托盘选择\n    pub clash_tray_selector: Option<ProxiesSelectorMode>,\n\n    pub always_on_top: Option<bool>,\n\n    /// Tun 堆栈选择\n    /// TODO: 弃用此字段，转移到 clash config 里\n    pub tun_stack: Option<TunStack>,\n\n    /// 是否启用网络统计信息浮窗\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub network_statistic_widget: Option<NetworkStatisticWidgetConfig>,\n\n    /// PAC URL for automatic proxy configuration\n    /// This field is used to set PAC proxy without exposing it to the frontend UI\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub pac_url: Option<String>,\n\n    /// enable tray text display on Linux systems\n    /// When enabled, shows proxy and TUN mode status as text next to the tray icon\n    /// When disabled, only shows status via icon changes (prevents text display issues on Wayland)\n    pub enable_tray_text: Option<bool>,\n\n    /// Use legacy UI (original UI at \"/\" route)\n    /// When true, opens legacy window; when false, opens new main window\n    pub use_legacy_ui: Option<bool>,\n}\n\n#[derive(Default, Debug, Clone, Deserialize, Serialize, Type)]\npub struct WindowState {\n    pub width: u32,\n    pub height: u32,\n    pub x: i32,\n    pub y: i32,\n    pub maximized: bool,\n    pub fullscreen: bool,\n}\n\nimpl IVerge {\n    pub fn new() -> Self {\n        match dirs::nyanpasu_config_path().and_then(|path| help::read_yaml::<IVerge, _>(&path)) {\n            Ok(mut config) => {\n                // Validate and fix theme_color if it's invalid\n                if let Some(ref theme_color) = config.theme_color {\n                    if !theme_color.is_empty() && !is_hex_color(theme_color) {\n                        log::warn!(target: \"app\", \"Invalid theme color detected: {}, resetting to default\", theme_color);\n                        config.theme_color = None;\n                    }\n                }\n\n                Self::merge_with_template(config)\n            }\n            Err(err) => {\n                log::error!(target: \"app\", \"{err:?}\");\n                Self::template()\n            }\n        }\n    }\n\n    fn merge_with_template(mut config: IVerge) -> Self {\n        let template = Self::template();\n\n        if config.enable_auto_check_update.is_none() {\n            config.enable_auto_check_update = template.enable_auto_check_update;\n        }\n\n        if config.clash_tray_selector.is_none() {\n            config.clash_tray_selector = template.clash_tray_selector;\n        }\n\n        if config.max_log_files.is_none() {\n            config.max_log_files = template.max_log_files;\n        }\n\n        if config.lighten_animation_effects.is_none() {\n            config.lighten_animation_effects = template.lighten_animation_effects;\n        }\n\n        if config.enable_service_mode.is_none() {\n            config.enable_service_mode = template.enable_service_mode;\n        }\n\n        // Handle deprecated auto_close_connection by migrating to break_when_proxy_change\n        if config.auto_close_connection.is_some() && config.break_when_proxy_change.is_none() {\n            config.break_when_proxy_change = if config.auto_close_connection.unwrap() {\n                Some(BreakWhenProxyChange::All)\n            } else {\n                Some(BreakWhenProxyChange::None)\n            };\n        }\n\n        // Set defaults for new options if not present\n        if config.break_when_proxy_change.is_none() {\n            config.break_when_proxy_change = template.break_when_proxy_change;\n        }\n\n        if config.break_when_profile_change.is_none() {\n            config.break_when_profile_change = template.break_when_profile_change;\n        }\n\n        if config.break_when_mode_change.is_none() {\n            config.break_when_mode_change = template.break_when_mode_change;\n        }\n\n        if config.enable_tray_text.is_none() {\n            config.enable_tray_text = template.enable_tray_text;\n        }\n\n        if config.use_legacy_ui.is_none() {\n            config.use_legacy_ui = template.use_legacy_ui;\n        }\n\n        config\n    }\n\n    pub fn template() -> Self {\n        Self {\n            clash_core: Some(ClashCore::default()),\n            language: {\n                let locale = crate::utils::help::get_system_locale();\n                Some(crate::utils::help::mapping_to_i18n_key(&locale).into())\n            },\n            app_log_level: Some(logging::LoggingLevel::default()),\n            theme_mode: Some(\"system\".into()),\n            traffic_graph: Some(true),\n            enable_memory_usage: Some(true),\n            enable_auto_launch: Some(false),\n            enable_silent_start: Some(false),\n            enable_system_proxy: Some(false),\n            enable_random_port: Some(false),\n            verge_mixed_port: Some(7890),\n            enable_proxy_guard: Some(false),\n            proxy_guard_interval: Some(30),\n            // auto_close_connection: Some(true), // Deprecated, replaced by break_when_proxy_change\n            break_when_proxy_change: Some(BreakWhenProxyChange::All),\n            break_when_profile_change: Some(true),\n            break_when_mode_change: Some(true),\n            enable_builtin_enhanced: Some(true),\n            enable_clash_fields: Some(true),\n            lighten_animation_effects: Some(false),\n            // auto_log_clean: Some(60 * 24 * 7), // 7 days 自动清理日记\n            max_log_files: Some(7), // 7 days\n            enable_auto_check_update: Some(true),\n            clash_tray_selector: Some(ProxiesSelectorMode::default()),\n            enable_service_mode: Some(false),\n            always_on_top: Some(false),\n            enable_tray_text: Some(false),\n            use_legacy_ui: Some(true),\n            ..Self::default()\n        }\n    }\n\n    /// Save IVerge App Config\n    pub fn save_file(&self) -> Result<()> {\n        help::save_yaml(\n            &dirs::nyanpasu_config_path()?,\n            &self,\n            Some(\"# Clash Nyanpasu Config\"),\n        )\n    }\n}\n"
  },
  {
    "path": "backend/tauri/src/config/nyanpasu/widget.rs",
    "content": "use nyanpasu_egui::widget::StatisticWidgetVariant;\nuse serde::{Deserialize, Serialize};\nuse specta::Type;\n\n#[derive(Debug, Default, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Type)]\n#[serde(rename_all = \"snake_case\")]\n#[serde(tag = \"kind\", content = \"value\")]\npub enum NetworkStatisticWidgetConfig {\n    #[default]\n    Disabled,\n    Enabled(StatisticWidgetVariant),\n}\n"
  },
  {
    "path": "backend/tauri/src/config/profile/builder.rs",
    "content": "use crate::config::*;\n\nuse super::item::{\n    LocalProfileBuilder, MergeProfileBuilder, RemoteProfileBuilder, ScriptProfileBuilder,\n};\n\n#[derive(Debug, serde:: Serialize, serde::Deserialize, specta::Type)]\n#[serde(tag = \"type\", rename_all = \"snake_case\")]\npub enum ProfileBuilder {\n    Remote(RemoteProfileBuilder),\n    Local(LocalProfileBuilder),\n    Merge(MergeProfileBuilder),\n    Script(ScriptProfileBuilder),\n}\n\n#[derive(Debug, thiserror::Error)]\npub enum ProfileBuilderError {\n    #[error(transparent)]\n    Remote(#[from] RemoteProfileBuilderError),\n    #[error(transparent)]\n    Local(#[from] LocalProfileBuilderError),\n    #[error(transparent)]\n    Merge(#[from] MergeProfileBuilderError),\n    #[error(transparent)]\n    Script(#[from] ScriptProfileBuilderError),\n}\n\nimpl ProfileBuilder {\n    pub fn build(self) -> Result<Profile, ProfileBuilderError> {\n        let profile = match self {\n            ProfileBuilder::Remote(mut builder) => builder.build()?.into(),\n            ProfileBuilder::Local(builder) => builder.build()?.into(),\n            ProfileBuilder::Merge(builder) => builder.build()?.into(),\n            ProfileBuilder::Script(builder) => builder.build()?.into(),\n        };\n\n        Ok(profile)\n    }\n}\n"
  },
  {
    "path": "backend/tauri/src/config/profile/item/local.rs",
    "content": "use super::{\n    ProfileCleanup, ProfileFileIo, ProfileHelper, ProfileMetaGetter, ProfileMetaSetter,\n    ProfileShared, ProfileSharedBuilder, ambassador_impl_ProfileFileIo,\n    ambassador_impl_ProfileMetaGetter, ambassador_impl_ProfileMetaSetter,\n};\nuse crate::config::{\n    ProfileKindGetter,\n    profile::item_type::{ProfileItemType, ProfileUid},\n};\nuse ambassador::Delegate;\nuse derive_builder::Builder;\nuse nyanpasu_macro::BuilderUpdate;\nuse serde::{Deserialize, Serialize};\nuse std::path::PathBuf;\n\nconst PROFILE_TYPE: ProfileItemType = ProfileItemType::Local;\n\n#[derive(\n    Default, Delegate, Debug, Clone, Deserialize, Serialize, Builder, BuilderUpdate, specta::Type,\n)]\n#[builder(derive(Debug, Serialize, Deserialize, specta::Type))]\n#[builder_update(patch_fn = \"apply\")]\n#[delegate(ProfileMetaGetter, target = \"shared\")]\n#[delegate(ProfileMetaSetter, target = \"shared\")]\n#[delegate(ProfileFileIo, target = \"shared\")]\npub struct LocalProfile {\n    #[serde(flatten)]\n    #[builder(field(\n        ty = \"ProfileSharedBuilder\",\n        build = \"self.shared.build(&PROFILE_TYPE).map_err(|e| LocalProfileBuilderError::from(e.to_string()))?\"\n    ))]\n    #[builder_field_attr(serde(flatten))]\n    #[builder_update(nested)]\n    pub shared: ProfileShared,\n\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    #[builder(setter(strip_option), default)]\n    /// file symlinks\n    pub symlinks: Option<PathBuf>,\n    /// process chain\n    #[builder(default)]\n    #[serde(alias = \"chains\", default)]\n    #[builder_field_attr(serde(alias = \"chains\", default))]\n    pub chain: Vec<ProfileUid>,\n}\n\nimpl LocalProfile {\n    pub fn builder() -> LocalProfileBuilder {\n        let mut builder = LocalProfileBuilder::default();\n        let shared = ProfileShared::get_default_builder(&PROFILE_TYPE);\n        builder.shared(shared);\n        builder\n    }\n}\n\nimpl ProfileKindGetter for LocalProfile {\n    fn kind(&self) -> ProfileItemType {\n        PROFILE_TYPE\n    }\n}\n\nimpl ProfileHelper for LocalProfile {}\nimpl ProfileCleanup for LocalProfile {}\n"
  },
  {
    "path": "backend/tauri/src/config/profile/item/merge.rs",
    "content": "use crate::config::{ProfileKindGetter, profile::item_type::ProfileItemType};\n\nuse super::{\n    ProfileCleanup, ProfileFileIo, ProfileHelper, ProfileMetaGetter, ProfileMetaSetter,\n    ProfileShared, ProfileSharedBuilder, ambassador_impl_ProfileFileIo,\n    ambassador_impl_ProfileMetaGetter, ambassador_impl_ProfileMetaSetter,\n};\nuse ambassador::Delegate;\nuse derive_builder::Builder;\nuse nyanpasu_macro::BuilderUpdate;\nuse serde::{Deserialize, Serialize};\n\nconst PROFILE_TYPE: ProfileItemType = ProfileItemType::Merge;\n\n#[derive(\n    Default, Delegate, Debug, Clone, Deserialize, Serialize, Builder, BuilderUpdate, specta::Type,\n)]\n#[builder(derive(Debug, Serialize, Deserialize, specta::Type))]\n#[builder_update(patch_fn = \"apply\")]\n#[delegate(ProfileMetaGetter, target = \"shared\")]\n#[delegate(ProfileMetaSetter, target = \"shared\")]\n#[delegate(ProfileFileIo, target = \"shared\")]\npub struct MergeProfile {\n    #[serde(flatten)]\n    #[builder(field(\n        ty = \"ProfileSharedBuilder\",\n        build = \"self.shared.build(&PROFILE_TYPE).map_err(|e| MergeProfileBuilderError::from(e.to_string()))?\"\n    ))]\n    #[builder_field_attr(serde(flatten))]\n    #[builder_update(nested)]\n    pub shared: ProfileShared,\n}\n\nimpl MergeProfile {\n    pub fn builder() -> MergeProfileBuilder {\n        let mut builder = MergeProfileBuilder::default();\n        let shared = ProfileShared::get_default_builder(&PROFILE_TYPE);\n        builder.shared(shared);\n        builder\n    }\n}\n\nimpl ProfileKindGetter for MergeProfile {\n    fn kind(&self) -> ProfileItemType {\n        PROFILE_TYPE\n    }\n}\n\nimpl ProfileCleanup for MergeProfile {}\nimpl ProfileHelper for MergeProfile {}\n"
  },
  {
    "path": "backend/tauri/src/config/profile/item/mod.rs",
    "content": "#![allow(clippy::crate_in_macro_def, dead_code)]\nuse super::item_type::ProfileItemType;\nuse crate::utils::dirs;\nuse ambassador::{Delegate, delegatable_trait};\nuse anyhow::{Context, Result, bail};\nuse nyanpasu_macro::EnumWrapperCombined;\nuse std::{borrow::Borrow, fmt::Debug, fs, io::Write};\n\nmod local;\nmod merge;\npub mod prelude;\nmod remote;\nmod script;\nmod shared;\nmod utils; // private use utils\n\npub use local::*;\npub use merge::*;\npub use remote::*;\npub use script::*;\npub use shared::*;\n\n/// Profile Setter Helper\n/// It is intended to be used in the default trait implementation, so it is PRIVATE.\n/// NOTE: this just a setter for fields, NOT do any file operation.\n#[delegatable_trait]\ntrait ProfileMetaSetter {\n    fn set_uid(&mut self, uid: String);\n    fn set_name(&mut self, name: String);\n    fn set_desc(&mut self, desc: Option<String>);\n    fn set_file(&mut self, file: String);\n    fn set_updated(&mut self, updated: usize);\n}\n\n/// Some getter is provided due to `Profile` is a enum type, and could not be used directly.\n/// If access to inner data is needed, you should use the `as_xxx` or `as_mut_xxx` method to get the inner specific profile item.\n#[delegatable_trait]\n\npub trait ProfileMetaGetter {\n    fn name(&self) -> &str;\n    fn desc(&self) -> Option<&str>;\n    fn uid(&self) -> &str;\n    fn updated(&self) -> usize;\n    fn file(&self) -> &str;\n}\n\n#[delegatable_trait]\npub trait ProfileKindGetter {\n    fn kind(&self) -> ProfileItemType;\n}\n\n/// A trait that provides some common methods for profile items\n#[allow(private_bounds)]\npub trait ProfileHelper:\n    Sized + ProfileMetaSetter + ProfileMetaGetter + ProfileKindGetter + Clone\n{\n    async fn duplicate(&self) -> Result<Self> {\n        let mut duplicate_profile = self.clone();\n        let kind = duplicate_profile.kind();\n        let new_uid = utils::generate_uid(&kind);\n        let new_file = ProfileSharedBuilder::default_file_name(&kind, &new_uid);\n        let new_name = format!(\"{}-copy\", duplicate_profile.name());\n        // copy file\n        let path = dirs::profiles_path()?;\n        let new_file_path = path.join(&new_file);\n        let old_file_path = path.join(duplicate_profile.file());\n        tokio::fs::copy(&old_file_path, &new_file_path).await?;\n        // apply new uid and name\n        duplicate_profile.set_uid(new_uid);\n        duplicate_profile.set_name(new_name);\n        duplicate_profile.set_file(new_file);\n        duplicate_profile.set_updated(chrono::Local::now().timestamp() as usize);\n        Ok(duplicate_profile)\n    }\n}\n\npub trait ProfileCleanup: ProfileHelper {\n    /// remove files and set the files to empty\n    /// It should be useful when the profile is no longer needed, or pending to be deleted\n    async fn remove_file(&mut self) -> Result<()> {\n        let file = self.file();\n        let path = dirs::app_profiles_dir()?.join(file);\n        tokio::fs::remove_file(path).await?;\n        Ok(())\n    }\n}\n\n#[derive(\n    serde::Deserialize, serde::Serialize, Debug, Delegate, Clone, EnumWrapperCombined, specta::Type,\n)]\n#[delegate(ProfileMetaSetter)]\n#[delegate(ProfileMetaGetter)]\n#[delegate(ProfileKindGetter)]\n#[delegate(ProfileFileIo)]\n#[serde(tag = \"type\", rename_all = \"snake_case\")]\npub enum Profile {\n    Remote(RemoteProfile),\n    Local(LocalProfile),\n    Merge(MergeProfile),\n    Script(ScriptProfile),\n}\n// what it actually did\n// #[derive(Default, Debug, Clone, Deserialize, Serialize)]\n// pub struct PrfSelected {\n//     pub name: Option<String>,\n//     pub now: Option<String>,\n// }\n\nimpl ProfileCleanup for Profile {}\nimpl ProfileHelper for Profile {}\n\nimpl Profile {\n    pub fn file(&self) -> &str {\n        match self {\n            Profile::Remote(profile) => &profile.shared.file,\n            Profile::Local(profile) => &profile.shared.file,\n            Profile::Merge(profile) => &profile.shared.file,\n            Profile::Script(profile) => &profile.shared.file,\n        }\n    }\n\n    /// get the file data\n    pub fn read_file(&self) -> Result<String> {\n        let file = self.file();\n        let path = dirs::app_profiles_dir()?.join(file);\n        if !path.exists() {\n            bail!(\"file does not exist\");\n        }\n        fs::read_to_string(path).context(\"failed to read the file\")\n    }\n\n    /// save the file data\n    pub fn save_file<T: Borrow<String>>(&self, data: T) -> Result<()> {\n        let file = self.file();\n        let path = dirs::app_profiles_dir()?.join(file);\n        let mut file = std::fs::OpenOptions::new()\n            .write(true)\n            .truncate(true)\n            .create(true)\n            .open(path)\n            .context(\"failed to open the file\")?;\n        file.write_all(data.borrow().as_bytes())\n            .context(\"failed to save the file\")\n    }\n}\n"
  },
  {
    "path": "backend/tauri/src/config/profile/item/prelude.rs",
    "content": "#![allow(unused_imports)]\npub use super::{\n    ProfileCleanup, ProfileFileIo, ProfileHelper, ProfileKindGetter, ProfileMetaGetter,\n};\n"
  },
  {
    "path": "backend/tauri/src/config/profile/item/remote.rs",
    "content": "use super::{\n    ProfileCleanup, ProfileFileIo, ProfileHelper, ProfileMetaGetter, ProfileMetaSetter,\n    ProfileShared, ProfileSharedBuilder, ambassador_impl_ProfileFileIo,\n    ambassador_impl_ProfileMetaGetter, ambassador_impl_ProfileMetaSetter,\n};\nuse crate::{\n    config::{\n        Config, ProfileKindGetter,\n        profile::item_type::{ProfileItemType, ProfileUid},\n    },\n    utils::{config::NyanpasuReqwestProxyExt, dirs::APP_VERSION, help},\n};\nuse ambassador::Delegate;\nuse backon::Retryable;\nuse derive_builder::Builder;\nuse indexmap::IndexMap;\nuse itertools::Itertools;\nuse nyanpasu_macro::BuilderUpdate;\nuse serde::{Deserialize, Serialize};\nuse serde_yaml::Mapping;\nuse specta::Type;\nuse std::time::Duration;\nuse sysproxy::Sysproxy;\nuse url::Url;\n\nconst PROFILE_TYPE: ProfileItemType = ProfileItemType::Remote;\n\npub trait RemoteProfileSubscription {\n    async fn subscribe(&mut self, opts: Option<RemoteProfileOptionsBuilder>) -> anyhow::Result<()>;\n}\n\n#[derive(Delegate, Debug, Clone, Deserialize, Serialize, Builder, BuilderUpdate, specta::Type)]\n#[builder(derive(Serialize, Deserialize, Debug, specta::Type))]\n#[builder(build_fn(skip, error = \"RemoteProfileBuilderError\"))]\n#[builder_update(patch_fn = \"apply\")]\n#[delegate(ProfileMetaSetter, target = \"shared\")]\n#[delegate(ProfileMetaGetter, target = \"shared\")]\n#[delegate(ProfileFileIo, target = \"shared\")]\npub struct RemoteProfile {\n    #[serde(flatten)]\n    #[builder(field(\n        ty = \"ProfileSharedBuilder\",\n        build = \"self.shared.build().map_err(Into::into)?\"\n    ))]\n    #[builder_field_attr(serde(flatten))]\n    #[builder_update(nested)]\n    pub shared: ProfileShared,\n    /// subscription url\n    pub url: Url,\n    /// subscription user info\n    #[builder(default)]\n    #[serde(default)]\n    pub extra: SubscriptionInfo,\n    /// remote profile options\n    #[builder(field(\n        ty = \"RemoteProfileOptionsBuilder\",\n        build = \"self.option.build().map_err(Into::into)?\"\n    ))]\n    #[builder_update(nested)]\n    #[builder_field_attr(serde(default))]\n    #[serde(default)]\n    pub option: RemoteProfileOptions,\n    /// process chain\n    #[builder(default)]\n    #[serde(alias = \"chains\", default)]\n    #[builder_field_attr(serde(alias = \"chains\", default))]\n    pub chain: Vec<ProfileUid>,\n}\n\nimpl RemoteProfile {\n    pub fn builder() -> RemoteProfileBuilder {\n        let mut builder = RemoteProfileBuilder::default();\n        let shared = ProfileShared::get_default_builder(&PROFILE_TYPE);\n        builder.shared(shared);\n        builder\n    }\n}\n\nimpl ProfileKindGetter for RemoteProfile {\n    fn kind(&self) -> ProfileItemType {\n        PROFILE_TYPE\n    }\n}\nimpl ProfileHelper for RemoteProfile {}\nimpl ProfileCleanup for RemoteProfile {}\n\nimpl RemoteProfileSubscription for RemoteProfile {\n    #[tracing::instrument]\n    async fn subscribe(\n        &mut self,\n        partial: Option<RemoteProfileOptionsBuilder>,\n    ) -> anyhow::Result<()> {\n        let mut opts = self.option.clone();\n        if let Some(partial) = partial {\n            opts.apply(partial);\n        }\n        let subscription = subscribe_url(&self.url, &opts).await?;\n        self.extra = subscription.info;\n\n        let content = serde_yaml::to_string(&subscription.data)?;\n        self.write_file(content).await?;\n        self.set_updated(chrono::Local::now().timestamp() as usize);\n        Ok(())\n    }\n}\n\n#[derive(Debug)]\nstruct Subscription {\n    pub url: Url,\n    pub filename: Option<String>,\n    pub data: Mapping,\n    pub info: SubscriptionInfo,\n    pub opts: Option<RemoteProfileOptions>,\n}\n\n/// perform a subscription\n#[tracing::instrument]\nasync fn subscribe_url(\n    url: &Url,\n    options: &RemoteProfileOptions,\n) -> Result<Subscription, SubscribeError> {\n    let options = options.apply_default();\n    let mut builder = reqwest::ClientBuilder::new()\n        .use_rustls_tls()\n        .no_proxy()\n        .timeout(Duration::from_secs(30));\n\n    // TODO: 添加一个代理测试环节？\n    let proxy_url: Option<String> =\n        // FIXME: 解耦此部分代理地址读取\n        if options.self_proxy.unwrap_or_default() && !cfg!(test) {\n            // 使用软件自己的代理\n            let port = Config::verge()\n                .latest()\n                .verge_mixed_port\n                .unwrap_or(Config::clash().data().get_mixed_port());\n            Some(format!(\"http://127.0.0.1:{port}\"))\n        } else if options.with_proxy.unwrap() {\n            // 使用系统代理\n            if let Ok(p @ Sysproxy { enable: true, .. }) = Sysproxy::get_system_proxy() {\n                Some(format!(\"http://{}:{}\", p.host, p.port))\n            } else {\n                None\n            }\n        } else {\n            None\n        };\n    if let Some(proxy_url) = proxy_url {\n        builder = builder.swift_set_proxy(&proxy_url);\n    }\n\n    builder = builder.user_agent(options.user_agent.unwrap());\n\n    let client = builder.build().map_err(|e| SubscribeError::Network {\n        url: url.to_string(),\n        source: e,\n    })?;\n    let perform_req = || async { client.get(url.as_str()).send().await?.error_for_status() };\n    let resp = perform_req\n        .retry(backon::ExponentialBuilder::default())\n        // Only retry on network errors or server errors\n        .when(|result| {\n            !result.is_status()\n                || result.status().is_some_and(|status_code| {\n                    !matches!(\n                        status_code,\n                        reqwest::StatusCode::FORBIDDEN\n                            | reqwest::StatusCode::NOT_FOUND\n                            | reqwest::StatusCode::UNAUTHORIZED\n                    )\n                })\n        })\n        .await\n        .map_err(|e| SubscribeError::Network {\n            url: url.to_string(),\n            source: e,\n        })?;\n\n    let header = resp.headers();\n    tracing::debug!(\"headers: {:#?}\", header);\n\n    // parse the Subscription UserInfo\n    let extra = match header\n        .get(\"subscription-userinfo\")\n        .or(header.get(\"Subscription-Userinfo\"))\n    {\n        Some(value) => {\n            tracing::debug!(\"Subscription-Userinfo: {:?}\", value);\n            let sub_info = value.to_str().unwrap_or(\"\");\n\n            Some(SubscriptionInfo {\n                upload: help::parse_str(sub_info, \"upload\").unwrap_or(0),\n                download: help::parse_str(sub_info, \"download\").unwrap_or(0),\n                total: help::parse_str(sub_info, \"total\").unwrap_or(0),\n                expire: help::parse_str(sub_info, \"expire\").unwrap_or(0),\n            })\n        }\n        None => None,\n    };\n\n    // Try to parse filename from headers\n    // `Profile-Title` -> `Content-Disposition`\n    let filename = utils::parse_profile_title_header(resp.headers())\n        .or_else(|| utils::parse_filename_from_content_disposition(resp.headers()));\n\n    // parse the profile-update-interval\n    let opts = match header\n        .get(\"profile-update-interval\")\n        .or(header.get(\"Profile-Update-Interval\"))\n    {\n        Some(value) => {\n            tracing::debug!(\"profile-update-interval: {:?}\", value);\n            match value.to_str().unwrap_or(\"\").parse::<u64>() {\n                Ok(val) => Some(RemoteProfileOptions {\n                    update_interval: val * 60, // hour -> min\n                    ..RemoteProfileOptions::default()\n                }),\n                Err(_) => None,\n            }\n        }\n        None => None,\n    };\n\n    let data = resp\n        .text_with_charset(\"utf-8\")\n        .await\n        .map_err(|e| SubscribeError::Network {\n            url: url.to_string(),\n            source: e,\n        })?;\n\n    // process the charset \"UTF-8 with BOM\"\n    let data = data.trim_start_matches('\\u{feff}');\n\n    // check the data whether the valid yaml format\n    let yaml = serde_yaml::from_str::<Mapping>(data).map_err(|e| SubscribeError::Parse {\n        url: url.to_string(),\n        source: e,\n    })?;\n\n    if !yaml.contains_key(\"proxies\") && !yaml.contains_key(\"proxy-providers\") {\n        return Err(SubscribeError::ValidationFailed {\n            url: url.to_string(),\n            reason: \"profile does not contain `proxies` or `proxy-providers`\".to_string(),\n        });\n    }\n\n    Ok(Subscription {\n        url: url.clone(),\n        filename,\n        data: yaml,\n        info: extra.unwrap_or_default(),\n        opts,\n    })\n}\n\n/// subscribe multiple urls\n#[tracing::instrument]\nasync fn subscribe_urls(\n    urls: &[Url],\n    options: &RemoteProfileOptions,\n) -> Result<Vec<Subscription>, SubscribeError> {\n    if urls.is_empty() {\n        return Err(SubscribeError::ValidationFailed {\n            url: \"\".to_string(),\n            reason: \"urls should not be empty\".to_string(),\n        });\n    }\n    let futures = urls.iter().map(|url| subscribe_url(url, options));\n    let results = futures::future::join_all(futures).await;\n    let (successes, errors): (Vec<_>, Vec<_>) = results.into_iter().partition_map(|r| match r {\n        Ok(val) => itertools::Either::Left(val),\n        Err(err) => itertools::Either::Right(err),\n    });\n\n    if !errors.is_empty() {\n        return Err(SubscribeError::MultipleErrors(errors));\n    }\n\n    Ok(successes)\n}\n\n/// merge the subscriptions\n#[tracing::instrument]\nfn merge_subscription(\n    subscriptions: &[Subscription],\n) -> (Mapping, IndexMap<Url, SubscriptionInfo>) {\n    let mut data = Mapping::new();\n    let mut extra = IndexMap::new();\n    for (i, sub) in subscriptions.iter().enumerate() {\n        if i == 0 {\n            data.extend(sub.data.clone());\n        } else {\n            let proxies = data.get_mut(\"proxies\").unwrap().as_sequence_mut().unwrap();\n            let sub_proxies = sub.data.get(\"proxies\").unwrap().as_sequence().unwrap();\n            proxies.extend(sub_proxies.iter().cloned());\n        }\n        extra.insert(sub.url.clone(), sub.info);\n    }\n    (data, extra)\n}\n\n#[derive(thiserror::Error, Debug)]\npub enum SubscribeError {\n    #[error(\"network issue at {url}: {source}\")]\n    Network {\n        url: String,\n        #[source]\n        source: reqwest::Error,\n    },\n\n    #[error(\"yaml parse error at {url}: {source}\")]\n    Parse {\n        url: String,\n        #[source]\n        source: serde_yaml::Error,\n    },\n\n    #[error(\"invalid profile at {url}: {reason}\")]\n    ValidationFailed { url: String, reason: String },\n\n    #[error(\"multiple errors occurred: {0:?}\")]\n    MultipleErrors(Vec<SubscribeError>),\n}\n\n#[derive(thiserror::Error, Debug)]\npub enum RemoteProfileBuilderError {\n    #[error(\"validation error: {0}\")]\n    Validation(String),\n    #[error(\"error: {0}\")]\n    UninitializedField(#[from] derive_builder::UninitializedFieldError),\n    #[error(\"subscribe failed: {0}\")]\n    SubscribeFailed(#[from] SubscribeError),\n\n    #[error(\"io error: {0}\")]\n    Io(#[from] std::io::Error),\n}\n\nimpl RemoteProfileBuilder {\n    fn default_shared(&self) -> ProfileSharedBuilder {\n        ProfileShared::get_default_builder(&PROFILE_TYPE)\n    }\n\n    fn validate(&self) -> Result<(), RemoteProfileBuilderError> {\n        if self.url.is_none() {\n            return Err(RemoteProfileBuilderError::Validation(\n                \"url should not be null\".into(),\n            ));\n        }\n\n        Ok(())\n    }\n\n    pub async fn build_no_blocking(&mut self) -> Result<RemoteProfile, RemoteProfileBuilderError> {\n        self.validate()?;\n        if self.shared.get_uid().is_none() {\n            self.shared\n                .uid(super::utils::generate_uid(&ProfileItemType::Remote));\n        }\n        let url = self.url.take().unwrap();\n        let options = self\n            .option\n            .build()\n            .map_err(|e| RemoteProfileBuilderError::Validation(e.to_string()))?;\n        let mut subscription = subscribe_url(&url, &options).await?;\n        let extra = subscription.info;\n\n        if self.shared.get_name().is_none()\n            && let Some(filename) = subscription.filename.take()\n        {\n            self.shared.name(filename);\n        }\n        if self.option.get_update_interval().is_none() && subscription.opts.is_some() {\n            self.option\n                .update_interval(subscription.opts.take().unwrap().update_interval);\n        }\n\n        let profile = RemoteProfile {\n            shared: self\n                .shared\n                .build(&PROFILE_TYPE)\n                .map_err(|e| RemoteProfileBuilderError::Validation(e.to_string()))?,\n            url,\n            extra,\n            option: self.option.build().unwrap(),\n            chain: self.chain.take().unwrap_or_default(),\n        };\n        // write the profile to the file\n        profile\n            .shared\n            .write_file(\n                serde_yaml::to_string(&subscription.data)\n                    .map_err(|e| RemoteProfileBuilderError::Validation(e.to_string()))?,\n            )\n            .await?;\n        Ok(profile)\n    }\n\n    /// NOTE: this call will block current async runtime, so it should be called in a blocking context\n    pub fn build(&mut self) -> Result<RemoteProfile, RemoteProfileBuilderError> {\n        nyanpasu_utils::runtime::block_on_anywhere(self.build_no_blocking())\n            .map_err(|e| RemoteProfileBuilderError::Validation(e.to_string()))\n    }\n}\n\n#[derive(Default, Debug, Clone, Copy, Deserialize, Serialize, Type)]\npub struct SubscriptionInfo {\n    pub upload: usize,\n    pub download: usize,\n    pub total: usize,\n    pub expire: usize,\n}\n\n#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Builder, BuilderUpdate, Type)]\n#[builder(derive(Serialize, Deserialize, Debug, Type))]\n#[builder_update(patch_fn = \"apply\", getter)]\npub struct RemoteProfileOptions {\n    /// see issue #13\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    #[builder(default, setter(strip_option))]\n    pub user_agent: Option<String>,\n\n    /// for `remote` profile\n    /// use system proxy\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    #[builder(default, setter(strip_option))]\n    pub with_proxy: Option<bool>,\n\n    /// use self proxy\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    #[builder(default = \"Some(true)\", setter(strip_option))]\n    pub self_proxy: Option<bool>,\n\n    /// subscription update interval\n    #[builder(default = \"120\")]\n    pub update_interval: u64,\n}\n\nimpl Default for RemoteProfileOptions {\n    fn default() -> Self {\n        Self {\n            user_agent: None,\n            with_proxy: None,\n            self_proxy: Some(true),\n            update_interval: 120, // 2 hours\n        }\n    }\n}\n\nimpl RemoteProfileOptions {\n    pub fn apply_default(&self) -> Self {\n        let mut options = self.clone();\n        if options.user_agent.is_none() {\n            options.user_agent = Some(format!(\"clash-nyanpasu/v{APP_VERSION}\"));\n        }\n        if options.with_proxy.is_none() {\n            options.with_proxy = Some(false);\n        }\n        if options.self_proxy.is_none() {\n            options.self_proxy = Some(false);\n        }\n        options\n    }\n}\n\nmod utils {\n    use base64::{Engine, engine::general_purpose};\n    use reqwest::header::{self, HeaderMap};\n\n    /// parse profile title from headers\n    pub fn parse_profile_title_header(headers: &HeaderMap) -> Option<String> {\n        headers\n            .get(\"profile-title\")\n            .and_then(|v| v.to_str().ok())\n            .and_then(|v| {\n                if v.starts_with(\"base64:\") {\n                    let encoded = v.trim_start_matches(\"base64:\");\n                    general_purpose::STANDARD\n                        .decode(encoded)\n                        .ok()\n                        .and_then(|bytes| String::from_utf8(bytes).ok())\n                } else {\n                    Some(v.to_string())\n                }\n            })\n    }\n\n    pub fn parse_filename_from_content_disposition(headers: &HeaderMap) -> Option<String> {\n        let filename = crate::utils::help::parse_str::<String>(\n            headers\n                .get(header::CONTENT_DISPOSITION)\n                .and_then(|v| v.to_str().ok())\n                .unwrap_or(\"\"),\n            \"filename\",\n        )?;\n        tracing::debug!(\"Content-Disposition: {:?}\", filename);\n\n        let filename = format!(\"{filename:?}\");\n        let filename = filename.trim_matches('\"');\n        match crate::utils::help::parse_str::<String>(filename, \"filename*\") {\n            Some(filename) => {\n                let iter = percent_encoding::percent_decode(filename.as_bytes());\n                let filename = iter.decode_utf8().unwrap_or_default();\n                filename\n                    .split(\"''\")\n                    .last()\n                    .map(|s| s.trim_matches('\"').to_string())\n            }\n            None => match crate::utils::help::parse_str::<String>(filename, \"filename\") {\n                Some(filename) => {\n                    let filename = filename.trim_matches('\"');\n                    Some(filename.to_string())\n                }\n                None => None,\n            },\n        }\n    }\n}\n"
  },
  {
    "path": "backend/tauri/src/config/profile/item/script.rs",
    "content": "use super::{\n    ProfileCleanup, ProfileFileIo, ProfileHelper, ProfileMetaGetter, ProfileMetaSetter,\n    ProfileShared, ProfileSharedBuilder, ambassador_impl_ProfileFileIo,\n    ambassador_impl_ProfileMetaGetter, ambassador_impl_ProfileMetaSetter,\n};\nuse crate::{\n    config::{ProfileKindGetter, profile::item_type::ProfileItemType},\n    enhance::ScriptType,\n};\nuse ambassador::Delegate;\nuse derive_builder::Builder;\nuse nyanpasu_macro::BuilderUpdate;\nuse serde::{Deserialize, Serialize};\n\n#[derive(\n    Default, Delegate, Debug, Clone, Deserialize, Serialize, Builder, BuilderUpdate, specta::Type,\n)]\n#[builder(derive(Debug, Serialize, Deserialize, specta::Type))]\n#[builder_update(patch_fn = \"apply\")]\n#[delegate(ProfileMetaSetter, target = \"shared\")]\n#[delegate(ProfileMetaGetter, target = \"shared\")]\n#[delegate(ProfileFileIo, target = \"shared\")]\npub struct ScriptProfile {\n    #[serde(flatten)]\n    #[builder(field(ty = \"ProfileSharedBuilder\", build = \"self.build_shared()?\"))]\n    #[builder_field_attr(serde(flatten))]\n    #[builder_update(nested)]\n    pub shared: ProfileShared,\n    pub script_type: ScriptType,\n}\n\nimpl ScriptProfileBuilder {\n    fn build_shared(&self) -> Result<ProfileShared, ScriptProfileBuilderError> {\n        self.script_type\n            .ok_or(ScriptProfileBuilderError::UninitializedField(\n                \"`script_type` is missing\",\n            ))\n            .and_then(|script_type| {\n                self.shared\n                    .build(&ProfileItemType::Script(script_type))\n                    .map_err(|e| ScriptProfileBuilderError::from(e.to_string()))\n            })\n    }\n}\n\nimpl ProfileKindGetter for ScriptProfile {\n    fn kind(&self) -> ProfileItemType {\n        ProfileItemType::Script(self.script_type)\n    }\n}\n\nimpl ScriptProfile {\n    pub fn builder(script_type: &ScriptType) -> ScriptProfileBuilder {\n        let mut builder = ScriptProfileBuilder::default();\n        let shared = ProfileShared::get_default_builder(&ProfileItemType::Script(*script_type));\n        builder.script_type(*script_type);\n        builder.shared(shared);\n        builder\n    }\n}\n\nimpl ProfileHelper for ScriptProfile {}\nimpl ProfileCleanup for ScriptProfile {}\n"
  },
  {
    "path": "backend/tauri/src/config/profile/item/shared.rs",
    "content": "use std::{fmt, str::FromStr};\n\nuse ambassador::delegatable_trait;\nuse derive_builder::Builder;\nuse nyanpasu_macro::BuilderUpdate;\nuse serde::{Deserialize, Serialize, de::Visitor};\n\nuse crate::{\n    config::profile::item_type::ProfileItemType, enhance::ScriptType, utils::dirs::app_profiles_dir,\n};\n\nuse super::{ProfileMetaGetter, ProfileMetaSetter};\n\n#[delegatable_trait]\npub trait ProfileFileIo {\n    async fn read_file(&self) -> std::io::Result<String>;\n    async fn write_file(&self, content: String) -> std::io::Result<()>;\n}\n\n#[derive(Default, Debug, Clone, Deserialize, Serialize, Builder, BuilderUpdate, specta::Type)]\n#[builder(\n    derive(Debug, serde::Serialize, serde::Deserialize, specta::Type),\n    build_fn(skip)\n)]\n#[builder_update(patch_fn = \"apply\", getter)]\npub struct ProfileShared {\n    /// Profile ID\n    pub uid: String,\n\n    /// profile name\n    pub name: String,\n\n    /// profile holds the file\n    // #[serde(alias = \"file\", deserialize_with = \"deserialize_option_single_or_vec\")]\n    #[builder(default = \"self.default_files()?\")]\n    pub file: String,\n\n    /// profile description\n    #[builder(default, setter(strip_option))]\n    pub desc: Option<String>,\n\n    #[builder(default = \"chrono::Local::now().timestamp() as usize\")]\n    /// update time\n    pub updated: usize,\n}\n\nimpl ProfileShared {\n    pub fn get_default_builder(kind: &ProfileItemType) -> ProfileSharedBuilder {\n        let mut builder = ProfileShared::builder();\n        builder\n            .name(ProfileSharedBuilder::default_name(kind).to_string())\n            .uid(ProfileSharedBuilder::default_uid(kind));\n        builder = builder.apply_default_file(kind).unwrap();\n        builder\n    }\n}\n\nimpl ProfileFileIo for ProfileShared {\n    async fn read_file(&self) -> std::io::Result<String> {\n        let path = app_profiles_dir().map_err(std::io::Error::other)?;\n        let file = path.join(&self.file);\n        tokio::fs::read_to_string(file).await\n    }\n\n    async fn write_file(&self, content: String) -> std::io::Result<()> {\n        let path = app_profiles_dir().map_err(std::io::Error::other)?;\n        let file = path.join(&self.file);\n        let mut file = tokio::fs::OpenOptions::new()\n            .write(true)\n            .create(true)\n            .truncate(true)\n            .open(&file)\n            .await?;\n        tokio::io::AsyncWriteExt::write_all(&mut file, content.as_bytes()).await\n    }\n}\n\nimpl ProfileSharedBuilder {\n    fn default_uid(kind: &ProfileItemType) -> String {\n        super::utils::generate_uid(kind)\n    }\n\n    pub fn default_name(kind: &ProfileItemType) -> &'static str {\n        match kind {\n            ProfileItemType::Remote => \"Remote Profile\",\n            ProfileItemType::Local => \"Local Profile\",\n            ProfileItemType::Merge => \"Merge Profile\",\n            ProfileItemType::Script(_) => \"Script Profile\",\n        }\n    }\n\n    pub fn default_file_name(kind: &ProfileItemType, uid: &str) -> String {\n        match kind {\n            ProfileItemType::Remote => format!(\"{uid}.yaml\"),\n            ProfileItemType::Local => format!(\"{uid}.yaml\"),\n            ProfileItemType::Merge => format!(\"{uid}.yaml\"),\n            ProfileItemType::Script(ScriptType::JavaScript) => format!(\"{uid}.js\"),\n            ProfileItemType::Script(ScriptType::Lua) => format!(\"{uid}.lua\"),\n        }\n    }\n\n    pub fn apply_default_file(\n        mut self,\n        kind: &ProfileItemType,\n    ) -> Result<ProfileSharedBuilder, String> {\n        let file = match &self.uid {\n            Some(uid) => Ok(Self::default_file_name(kind, uid)),\n            None => Err(\"uid should not be null\".to_string()),\n        }?;\n        self.file = Some(file);\n        Ok(self)\n    }\n\n    pub fn is_file_none(&self) -> bool {\n        self.file.is_none()\n    }\n\n    pub fn build(\n        &self,\n        kind: &ProfileItemType,\n    ) -> Result<ProfileShared, ProfileSharedBuilderError> {\n        let mut builder = self.clone();\n        if self.uid.is_none() {\n            builder.uid = Some(Self::default_uid(kind));\n        }\n        if self.name.is_none() {\n            builder.name = Some(Self::default_name(kind).to_string());\n        }\n        if self.file.is_none() {\n            builder.file = Some(Self::default_file_name(kind, builder.uid.as_ref().unwrap()));\n        }\n\n        Ok(ProfileShared {\n            uid: builder.uid.unwrap(),\n            name: builder.name.unwrap(),\n            file: builder.file.unwrap(),\n            desc: builder.desc.clone().unwrap_or_default(),\n            updated: builder\n                .updated\n                .unwrap_or_else(|| chrono::Local::now().timestamp() as usize),\n        })\n    }\n}\n\nimpl ProfileShared {\n    pub fn builder() -> ProfileSharedBuilder {\n        ProfileSharedBuilder::default()\n    }\n}\n\nimpl ProfileMetaGetter for ProfileShared {\n    fn name(&self) -> &str {\n        &self.name\n    }\n\n    fn desc(&self) -> Option<&str> {\n        self.desc.as_deref()\n    }\n\n    fn uid(&self) -> &str {\n        &self.uid\n    }\n\n    fn updated(&self) -> usize {\n        self.updated\n    }\n\n    fn file(&self) -> &str {\n        &self.file\n    }\n}\n\nimpl ProfileMetaSetter for ProfileShared {\n    fn set_name(&mut self, name: String) {\n        self.name = name;\n    }\n\n    fn set_desc(&mut self, desc: Option<String>) {\n        self.desc = desc;\n    }\n\n    fn set_file(&mut self, file: String) {\n        self.file = file;\n    }\n\n    fn set_uid(&mut self, uid: String) {\n        self.uid = uid;\n    }\n\n    fn set_updated(&mut self, updated: usize) {\n        self.updated = updated;\n    }\n}\n\npub fn deserialize_single_or_vec<'de, D, T>(deserializer: D) -> Result<Vec<T>, D::Error>\nwhere\n    D: serde::Deserializer<'de>,\n    T: FromStr,\n    T::Err: fmt::Display,\n{\n    use serde::de::Error;\n\n    struct StringOrVec<T>(std::marker::PhantomData<T>);\n\n    impl<'de, T> Visitor<'de> for StringOrVec<T>\n    where\n        T: FromStr,\n        T::Err: fmt::Display,\n    {\n        type Value = Vec<T>;\n\n        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {\n            formatter.write_str(\"a string or a sequence of strings\")\n        }\n\n        fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>\n        where\n            E: serde::de::Error,\n        {\n            T::from_str(value).map(|v| vec![v]).map_err(E::custom)\n        }\n\n        fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>\n        where\n            A: serde::de::SeqAccess<'de>,\n        {\n            let mut vec = Vec::new();\n            while let Some(value) = seq.next_element::<String>()? {\n                let parsed_value = T::from_str(&value).map_err(A::Error::custom)?;\n                vec.push(parsed_value);\n            }\n            Ok(vec)\n        }\n    }\n\n    deserializer.deserialize_any(StringOrVec(std::marker::PhantomData))\n}\n"
  },
  {
    "path": "backend/tauri/src/config/profile/item/utils.rs",
    "content": "use crate::{config::profile::item_type::ProfileItemType, utils::help};\n\npub fn generate_uid(kind: &ProfileItemType) -> String {\n    match kind {\n        ProfileItemType::Remote => help::get_uid(\"r\"),\n        ProfileItemType::Local => help::get_uid(\"l\"),\n        ProfileItemType::Script(_) => help::get_uid(\"s\"),\n        ProfileItemType::Merge => help::get_uid(\"m\"),\n    }\n}\n"
  },
  {
    "path": "backend/tauri/src/config/profile/item_type.rs",
    "content": "use crate::enhance::ScriptType;\nuse serde::{Deserialize, Serialize};\nuse strum::EnumString;\n\n#[derive(\n    Debug, EnumString, Clone, Copy, Serialize, Deserialize, Default, PartialEq, specta::Type,\n)]\n#[strum(serialize_all = \"snake_case\")]\n#[serde(tag = \"kind\", content = \"variant\", rename_all = \"snake_case\")]\npub enum ProfileItemType {\n    #[serde(rename = \"remote\")]\n    Remote,\n    #[serde(rename = \"local\")]\n    #[default]\n    Local,\n    #[serde(rename = \"script\")]\n    Script(ScriptType),\n    #[serde(rename = \"merge\")]\n    Merge,\n}\n\npub type ProfileUid = String;\n"
  },
  {
    "path": "backend/tauri/src/config/profile/mod.rs",
    "content": "pub mod builder;\npub mod item;\npub mod item_type;\npub mod profiles;\n\npub use builder::ProfileBuilder;\nuse item::deserialize_single_or_vec;\n\n#[cfg(test)]\nmod tests;\n"
  },
  {
    "path": "backend/tauri/src/config/profile/profiles.rs",
    "content": "use super::{\n    builder::ProfileBuilder,\n    item::{Profile, prelude::*},\n    item_type::ProfileUid,\n};\nuse crate::utils::{dirs, help};\nuse anyhow::{Result, bail};\nuse derive_builder::Builder;\nuse indexmap::IndexMap;\nuse nyanpasu_macro::BuilderUpdate;\nuse rayon::prelude::*;\nuse serde::{Deserialize, Serialize};\nuse serde_yaml::Mapping;\nuse std::borrow::Borrow;\nuse tracing_attributes::instrument;\n\n/// Define the `profiles.yaml` schema\n#[derive(Debug, Clone, Deserialize, Serialize, Builder, BuilderUpdate, specta::Type)]\n#[builder(derive(Serialize, Deserialize, specta::Type))]\n#[builder_update(patch_fn = \"apply\")]\npub struct Profiles {\n    /// same as PrfConfig.current\n    #[serde(default)]\n    #[serde(deserialize_with = \"super::deserialize_single_or_vec\")]\n    pub current: Vec<ProfileUid>,\n    #[serde(default)]\n    /// same as PrfConfig.chain\n    pub chain: Vec<ProfileUid>,\n    #[serde(default)]\n    /// record valid fields for clash\n    pub valid: Vec<String>,\n    #[serde(default)]\n    /// profile list\n    pub items: Vec<Profile>,\n}\n\nimpl Default for Profiles {\n    fn default() -> Self {\n        Self {\n            current: vec![],\n            chain: vec![],\n            valid: vec![\n                \"dns\".into(),\n                \"unified-delay\".into(),\n                \"tcp-concurrent\".into(),\n            ],\n            items: vec![],\n        }\n    }\n}\n\nimpl Profiles {\n    pub fn new() -> Self {\n        match dirs::profiles_path().and_then(|path| help::read_yaml::<Self, _>(&path)) {\n            Ok(profiles) => profiles,\n            Err(err) => {\n                log::error!(target: \"app\", \"{err:?}\\n - use the default profiles\");\n                Self::default()\n            }\n        }\n    }\n\n    pub fn save_file(&self) -> Result<()> {\n        help::save_yaml(\n            &dirs::profiles_path()?,\n            self,\n            Some(\"# Profiles Config for Clash Nyanpasu\"),\n        )\n    }\n\n    pub fn get_current(&self) -> &[ProfileUid] {\n        &self.current\n    }\n\n    /// get items ref\n    pub fn get_items(&self) -> &[Profile] {\n        &self.items\n    }\n\n    /// find the item by the uid\n    pub fn get_item(&self, uid: &str) -> Result<&Profile> {\n        self.get_items()\n            .iter()\n            .find(|e| e.uid() == uid)\n            .ok_or_else(|| anyhow::anyhow!(\"failed to get the profile item \\\"uid:{uid}\\\"\"))\n    }\n\n    /// append new item\n    pub fn append_item(&mut self, item: Profile) -> Result<()> {\n        self.items.push(item);\n        self.save_file()\n    }\n\n    /// reorder items\n    pub fn reorder(&mut self, active_id: String, over_id: String) -> Result<()> {\n        let items = &mut self.items;\n        let mut old_index = None;\n        let mut new_index = None;\n\n        for (i, item) in items.iter().enumerate() {\n            if item.uid() == active_id {\n                old_index = Some(i);\n            }\n            if item.uid() == over_id {\n                new_index = Some(i);\n            }\n        }\n\n        if old_index.is_none() || new_index.is_none() {\n            return Ok(());\n        }\n        let item = items.remove(old_index.unwrap());\n        items.insert(new_index.unwrap(), item);\n        self.save_file()\n    }\n\n    /// reorder items with the full order list\n    pub fn reorder_by_list<T: Borrow<String>>(&mut self, order: &[T]) -> Result<()> {\n        let mut items = std::mem::take(&mut self.items);\n        let mut new_items = Vec::with_capacity(items.len());\n\n        for uid in order {\n            if let Some(index) = items.iter().position(|e| e.uid() == uid.borrow()) {\n                new_items.push(items.remove(index));\n            }\n        }\n\n        // Keep unmatched items to avoid accidental data loss when order is partial.\n        new_items.extend(items);\n        self.items = new_items;\n\n        self.save_file()\n    }\n\n    /// update the item value\n    #[instrument]\n    pub fn patch_item(&mut self, uid: String, patch: ProfileBuilder) -> Result<()> {\n        tracing::debug!(\"patch item: {uid} with {patch:?}\");\n\n        let item = self\n            .items\n            .iter_mut()\n            .find(|e| e.uid() == uid)\n            .ok_or(anyhow::anyhow!(\n                \"failed to find the profile item \\\"uid:{uid}\\\"\"\n            ))?;\n\n        tracing::debug!(\"patch item: {item:?}\");\n\n        match (item, patch) {\n            (Profile::Remote(item), ProfileBuilder::Remote(builder)) => item.apply(builder),\n            (Profile::Local(item), ProfileBuilder::Local(builder)) => item.apply(builder),\n            (Profile::Merge(item), ProfileBuilder::Merge(builder)) => item.apply(builder),\n            (Profile::Script(item), ProfileBuilder::Script(builder)) => item.apply(builder),\n            _ => bail!(\"profile type mismatch when patching\"),\n        };\n\n        self.save_file()\n    }\n\n    /// replace item\n    pub fn replace_item<T: Borrow<String>>(&mut self, uid: T, item: Profile) -> Result<()> {\n        let uid = uid.borrow();\n\n        let index = self.items.iter().position(|e| e.uid() == uid);\n        if let Some(index) = index {\n            unsafe {\n                *self.items.get_unchecked_mut(index) = item;\n            }\n        }\n\n        self.save_file()\n    }\n\n    /// delete item\n    /// if delete the current then return true\n    pub async fn delete_item<T: Borrow<String>>(&mut self, uid: T) -> Result<bool> {\n        let uid = uid.borrow();\n        let items = &mut self.items;\n\n        // get the index\n        let index = items.iter().position(|e| e.uid() == uid);\n        if let Some(index) = index {\n            let mut profile = items.remove(index);\n            profile.remove_file().await?;\n        }\n\n        // delete the original uid\n        let mut current = self\n            .current\n            .iter()\n            .filter(|e| e != &uid)\n            .cloned()\n            .collect::<Vec<_>>();\n        let is_current = self.current != current;\n        // 尝试激活存在的第一个配置\n        if current.is_empty() {\n            let item = items.iter().find(|e| e.is_local() || e.is_remote());\n            if let Some(item) = item {\n                current.push(item.uid().to_string());\n            }\n        }\n        self.current = current;\n\n        self.save_file()?;\n        Ok(is_current)\n    }\n\n    /// 获取current指向的配置内容\n    pub fn current_mappings(&self) -> Result<IndexMap<&str, Mapping>> {\n        let current = self\n            .items\n            .iter()\n            .filter(|e| self.current.iter().any(|uid| uid == e.uid()))\n            .collect::<Vec<_>>();\n        let (successes, failures): (Vec<(&str, Mapping)>, Vec<anyhow::Error>) = current\n            .par_iter()\n            .map(|item| {\n                let file_path = dirs::app_profiles_dir()?.join(item.file());\n                if !file_path.exists() {\n                    return Err(anyhow::anyhow!(\"failed to find the file: {:?}\", file_path));\n                }\n                help::read_merge_mapping(&file_path).map(|mapping| (item.uid(), mapping))\n            })\n            .partition_map(|item| match item {\n                Ok(item) => itertools::Either::Left(item),\n                Err(err) => itertools::Either::Right(err),\n            });\n        if !failures.is_empty() {\n            bail!(\"failed to read the file: {:#?}\", failures);\n        }\n        let map = IndexMap::from_iter(successes);\n        Ok(map)\n    }\n}\n"
  },
  {
    "path": "backend/tauri/src/config/profile/tests.rs",
    "content": "use crate::{\n    config::profile::{\n        item::{\n            LocalProfile, MergeProfile, Profile, RemoteProfile, RemoteProfileOptions,\n            ScriptProfile, SubscriptionInfo,\n        },\n        item_type::ProfileItemType,\n    },\n    enhance::ScriptType,\n};\nuse serde_yaml;\nuse tokio_util::sync::CancellationToken;\nuse url::Url;\n\nconst REMOTE_SAMPLE_DATA: &str = include_str!(\"../../../tests/sample_clash_config.yaml\");\n\nstruct Guard(CancellationToken, Option<tokio::task::JoinHandle<()>>);\n\nimpl Drop for Guard {\n    fn drop(&mut self) {\n        self.0.cancel();\n        if let Some(handle) = self.1.take() {\n            nyanpasu_utils::runtime::block_on_anywhere(handle).unwrap();\n        }\n    }\n}\n\nasync fn create_test_server() -> (Guard, url::Url) {\n    let port = port_scanner::request_open_port().unwrap();\n    let url = Url::parse(&format!(\"http://127.0.0.1:{port}\")).unwrap();\n    let token = CancellationToken::new();\n    let token_clone = token.clone();\n    let (is_ready_tx, is_ready_rx) = tokio::sync::oneshot::channel();\n    let handle = tokio::spawn(async move {\n        let listener = tokio::net::TcpListener::bind(format!(\"0.0.0.0:{port}\"))\n            .await\n            .unwrap();\n        let _ = is_ready_tx.send(());\n        let app = axum::Router::new().route(\n            \"/sample_clash_config\",\n            axum::routing::get(|| async { REMOTE_SAMPLE_DATA }),\n        );\n        axum::serve(listener, app.into_make_service())\n            .with_graceful_shutdown(async move { token.cancelled().await })\n            .await\n            .unwrap();\n    });\n    let _ = is_ready_rx.await;\n    let guard = Guard(token_clone, Some(handle));\n    (guard, url)\n}\n\n/// 测试整数类型不匹配问题\n/// 这是原始问题的核心：YAML 解析时整数类型可能不一致\n#[test]\nfn test_integer_type_mismatch_in_yaml() {\n    // 测试不同的整数表示形式\n    let yaml_with_i32 = r#\"\ntype: remote\nuid: \"test-uid-1\"\nname: \"Test Profile\"\nupdated: 1234567890\nurl: \"https://example.com/config.yaml\"\nfile: sample.yaml\n\"#;\n\n    let yaml_with_i64 = r#\"\ntype: remote\nuid: \"test-uid-2\"\nname: \"Test Profile\"\nupdated: 9999999999999\nurl: \"https://example.com/config.yaml\"\nfile: sample.yaml\n\"#;\n\n    let yaml_with_u64 = r#\"\ntype: remote\nuid: \"test-uid-3\"\nname: \"Test Profile\"\nupdated: 18446744073709551615\nurl: \"https://example.com/config.yaml\"\nfile: sample.yaml\n\"#;\n\n    // 应该都能成功解析\n    let profile1: Result<Profile, _> = serde_yaml::from_str(yaml_with_i32);\n    let profile2: Result<Profile, _> = serde_yaml::from_str(yaml_with_i64);\n    let profile3: Result<Profile, _> = serde_yaml::from_str(yaml_with_u64);\n\n    assert!(profile1.is_ok(), \"Failed to parse i32: {:?}\", profile1);\n    assert!(profile2.is_ok(), \"Failed to parse i64: {:?}\", profile2);\n    // u64 最大值可能会被转换为 usize，在 32 位系统上可能失败\n    if std::mem::size_of::<usize>() == 8 {\n        assert!(profile3.is_ok(), \"Failed to parse u64: {:?}\", profile3);\n    }\n}\n\n/// 测试 tagged enum 的正确序列化和反序列化\n#[test]\nfn test_tagged_enum_serialization() {\n    // 创建不同类型的 Profile\n    let remote_profile = Profile::Remote(RemoteProfile {\n        shared: crate::config::profile::item::ProfileShared {\n            uid: \"remote-1\".to_string(),\n            name: \"Remote Profile\".to_string(),\n            file: \"remote-1.yaml\".to_string(),\n            desc: Some(\"A remote profile\".to_string()),\n            updated: 1234567890,\n        },\n        url: Url::parse(\"https://example.com/config.yaml\").unwrap(),\n        extra: SubscriptionInfo::default(),\n        option: RemoteProfileOptions::default(),\n        chain: vec![],\n    });\n\n    let local_profile = Profile::Local(LocalProfile {\n        shared: crate::config::profile::item::ProfileShared {\n            uid: \"local-1\".to_string(),\n            name: \"Local Profile\".to_string(),\n            file: \"local-1.yaml\".to_string(),\n            desc: None,\n            updated: 1234567890,\n        },\n        symlinks: None,\n        chain: vec![],\n    });\n\n    let merge_profile = Profile::Merge(MergeProfile {\n        shared: crate::config::profile::item::ProfileShared {\n            uid: \"merge-1\".to_string(),\n            name: \"Merge Profile\".to_string(),\n            file: \"merge-1.yaml\".to_string(),\n            desc: Some(\"Merge multiple profiles\".to_string()),\n            updated: 1234567890,\n        },\n    });\n\n    let script_profile = Profile::Script(ScriptProfile {\n        shared: crate::config::profile::item::ProfileShared {\n            uid: \"script-1\".to_string(),\n            name: \"Script Profile\".to_string(),\n            file: \"script-1.js\".to_string(),\n            desc: None,\n            updated: 1234567890,\n        },\n        script_type: ScriptType::JavaScript,\n    });\n\n    // 测试序列化\n    let remote_yaml = serde_yaml::to_string(&remote_profile).unwrap();\n    let local_yaml = serde_yaml::to_string(&local_profile).unwrap();\n    let merge_yaml = serde_yaml::to_string(&merge_profile).unwrap();\n    let script_yaml = serde_yaml::to_string(&script_profile).unwrap();\n\n    println!(\"Remote YAML:\\n{}\", remote_yaml);\n    println!(\"Local YAML:\\n{}\", local_yaml);\n    println!(\"Merge YAML:\\n{}\", merge_yaml);\n    println!(\"Script YAML:\\n{}\", script_yaml);\n\n    // 验证 YAML 包含正确的 type 标签\n    assert!(remote_yaml.contains(\"type: remote\"));\n    assert!(local_yaml.contains(\"type: local\"));\n    assert!(merge_yaml.contains(\"type: merge\"));\n    assert!(script_yaml.contains(\"type: script\"));\n\n    // 测试反序列化\n    let remote_parsed: Profile = serde_yaml::from_str(&remote_yaml).unwrap();\n    let local_parsed: Profile = serde_yaml::from_str(&local_yaml).unwrap();\n    let merge_parsed: Profile = serde_yaml::from_str(&merge_yaml).unwrap();\n    let script_parsed: Profile = serde_yaml::from_str(&script_yaml).unwrap();\n\n    // 验证反序列化后的类型正确\n    assert!(matches!(remote_parsed, Profile::Remote(_)));\n    assert!(matches!(local_parsed, Profile::Local(_)));\n    assert!(matches!(merge_parsed, Profile::Merge(_)));\n    assert!(matches!(script_parsed, Profile::Script(_)));\n}\n\n#[test]\nfn test_backward_compatibility() {\n    // 测试新的脚本格式能被正确识别\n    let new_format = r#\"uid: siL1cvjnvLB6\ntype: script\nscript_type: javascript\nname: 花☁️处理\nfile: siL1cvjnvLB6.js\ndesc: ''\nupdated: 1720954186\"#;\n    serde_yaml::from_str::<Profile>(new_format).expect(\"new format should works\");\n}\n\n/// 测试 ProfileKindGetter trait\n#[test]\nfn test_profile_kind_getter() {\n    use crate::config::ProfileKindGetter;\n\n    let remote = RemoteProfile {\n        shared: Default::default(),\n        url: Url::parse(\"https://example.com\").unwrap(),\n        extra: SubscriptionInfo::default(),\n        option: RemoteProfileOptions::default(),\n        chain: vec![],\n    };\n    assert_eq!(remote.kind(), ProfileItemType::Remote);\n\n    let local = LocalProfile {\n        shared: Default::default(),\n        symlinks: None,\n        chain: vec![],\n    };\n    assert_eq!(local.kind(), ProfileItemType::Local);\n\n    let merge = MergeProfile {\n        shared: Default::default(),\n    };\n    assert_eq!(merge.kind(), ProfileItemType::Merge);\n\n    let script_js = ScriptProfile {\n        shared: Default::default(),\n        script_type: ScriptType::JavaScript,\n    };\n    assert_eq!(\n        script_js.kind(),\n        ProfileItemType::Script(ScriptType::JavaScript)\n    );\n}\n\n/// 测试 builder 的默认值设置\n#[test_log::test(tokio::test(flavor = \"multi_thread\"))]\nasync fn test_builder_defaults() {\n    let (_guard, mut url) = create_test_server().await;\n    let remote_builder = RemoteProfile::builder();\n    let local_builder = LocalProfile::builder();\n    let merge_builder = MergeProfile::builder();\n    // let script_builder = ScriptProfile::builder(&ScriptType::JavaScript);\n\n    // 构建时应该自动填充默认值\n    let mut remote_builder = remote_builder;\n    url.set_path(\"sample_clash_config\");\n    remote_builder.url(url.clone());\n    let remote = remote_builder.build().expect(\"build remote profile\");\n    assert!(!remote.shared.uid.is_empty());\n    assert!(!remote.shared.name.is_empty());\n    assert!(!remote.shared.file.is_empty());\n\n    let local = local_builder.build();\n    assert!(local.is_ok());\n    let local = local.unwrap();\n    assert!(!local.shared.uid.is_empty());\n    assert_eq!(local.shared.name, \"Local Profile\");\n\n    let merge = merge_builder.build();\n    assert!(merge.is_ok());\n    let merge = merge.unwrap();\n    assert!(!merge.shared.uid.is_empty());\n    assert_eq!(merge.shared.name, \"Merge Profile\");\n}\n\n/// 测试错误处理\n#[test]\nfn test_error_handling() {\n    // 无效的 type 值\n    let invalid_type = r#\"\ntype: invalid_type\nuid: \"test\"\nname: \"Test\"\n\"#;\n    let result: Result<Profile, _> = serde_yaml::from_str(invalid_type);\n    assert!(result.is_err());\n\n    // Script 类型但缺少 script_type\n    let missing_script_type = r#\"\ntype: script\nuid: \"script-test\"\nname: \"Script Test\"\n\"#;\n    let result: Result<Profile, _> = serde_yaml::from_str(missing_script_type);\n    // 应该使用默认的 script_type 或者失败\n    println!(\"Script without script_type result: {:?}\", result);\n\n    // Remote 类型但缺少必需的 url 字段\n    let missing_url = r#\"\ntype: remote\nuid: \"remote-test\"\nname: \"Remote Test\"\n\"#;\n    let result: Result<Profile, _> = serde_yaml::from_str(missing_url);\n    assert!(result.is_err(), \"Should fail without required url field\");\n}\n\n/// 测试大数字的处理\n#[test]\nfn test_large_numbers() {\n    let test_cases = vec![\n        (0usize, \"zero\"),\n        (1234567890usize, \"normal\"),\n        (usize::MAX, \"max\"),\n    ];\n\n    for (value, desc) in test_cases {\n        let profile = Profile::Local(LocalProfile {\n            shared: crate::config::profile::item::ProfileShared {\n                uid: format!(\"test-{}\", desc),\n                name: format!(\"Test {}\", desc),\n                file: format!(\"test-{}.yaml\", desc),\n                desc: None,\n                updated: value,\n            },\n            symlinks: None,\n            chain: vec![],\n        });\n\n        let yaml = serde_yaml::to_string(&profile).unwrap();\n        let parsed: Profile = serde_yaml::from_str(&yaml).unwrap();\n\n        if let Profile::Local(local) = parsed {\n            assert_eq!(local.shared.updated, value, \"Failed for {}\", desc);\n        } else {\n            panic!(\"Expected Local profile\");\n        }\n    }\n}\n"
  },
  {
    "path": "backend/tauri/src/config/runtime.rs",
    "content": "use serde::{Deserialize, Serialize};\nuse serde_yaml::Mapping;\n\nuse crate::enhance::PostProcessingOutput;\n\n#[derive(Default, Debug, Clone, Deserialize, Serialize, specta::Type)]\npub struct PatchRuntimeConfig {\n    #[serde(default)]\n    pub allow_lan: Option<bool>,\n    #[serde(default)]\n    pub ipv6: Option<bool>,\n    #[serde(default)]\n    pub log_level: Option<String>,\n    #[serde(default)]\n    pub mode: Option<String>,\n}\n\n#[derive(Default, Debug, Clone, Deserialize, Serialize)]\npub struct IRuntime {\n    pub config: Option<Mapping>,\n    // 记录在配置中（包括merge和script生成的）出现过的keys\n    // 这些keys不一定都生效\n    pub exists_keys: Vec<String>,\n    pub postprocessing_output: PostProcessingOutput,\n}\n\nimpl IRuntime {\n    pub fn new() -> Self {\n        Self::default()\n    }\n\n    // 这里只更改 allow-lan | ipv6 | log-level | mode\n    pub fn patch_config(&mut self, patch: Mapping) {\n        tracing::debug!(\"patching runtime config: {:?}\", patch);\n        if let Some(config) = self.config.as_mut() {\n            let patch_config: PatchRuntimeConfig =\n                serde_yaml::from_value(serde_yaml::Value::Mapping(patch.clone()))\n                    .unwrap_or_default();\n\n            [\n                (\n                    \"allow-lan\",\n                    patch_config.allow_lan.map(serde_yaml::Value::Bool),\n                ),\n                (\"ipv6\", patch_config.ipv6.map(serde_yaml::Value::Bool)),\n                (\n                    \"log-level\",\n                    patch_config.log_level.map(serde_yaml::Value::String),\n                ),\n                (\"mode\", patch_config.mode.map(serde_yaml::Value::String)),\n            ]\n            .into_iter()\n            .filter_map(|(key, value)| value.map(|v| (key.into(), v)))\n            .for_each(|(k, v)| {\n                config.insert(k, v);\n            });\n        }\n    }\n}\n"
  },
  {
    "path": "backend/tauri/src/consts.rs",
    "content": "use once_cell::sync::{Lazy, OnceCell};\nuse tauri::AppHandle;\n\npub const MAIN_WINDOW_LABEL: &str = \"main\";\npub const LEGACY_WINDOW_LABEL: &str = \"legacy\";\npub const EDITOR_WINDOW_LABEL: &str = \"editor\";\npub const APP_NAME: &str = \"Clash Nyanpasu\";\npub const APP_EDITOR_NAME: &str = \"Clash Nyanpasu - Editor\";\n\n#[derive(Debug, serde::Serialize, Clone, specta::Type)]\npub struct BuildInfo {\n    pub app_name: &'static str,\n    pub app_version: &'static str,\n    pub pkg_version: &'static str,\n    pub commit_hash: &'static str,\n    pub commit_author: &'static str,\n    pub commit_date: &'static str,\n    pub build_date: &'static str,\n    pub build_profile: &'static str,\n    pub build_platform: &'static str,\n    pub rustc_version: &'static str,\n    pub llvm_version: &'static str,\n}\n\npub static BUILD_INFO: Lazy<BuildInfo> = Lazy::new(|| BuildInfo {\n    app_name: env!(\"CARGO_PKG_NAME\"),\n    app_version: env!(\"CARGO_PKG_VERSION\"),\n    pkg_version: env!(\"NYANPASU_VERSION\"),\n    commit_hash: env!(\"COMMIT_HASH\"),\n    commit_author: env!(\"COMMIT_AUTHOR\"),\n    commit_date: env!(\"COMMIT_DATE\"),\n    build_date: env!(\"BUILD_DATE\"),\n    build_profile: env!(\"BUILD_PROFILE\"),\n    build_platform: env!(\"BUILD_PLATFORM\"),\n    rustc_version: env!(\"RUSTC_VERSION\"),\n    llvm_version: env!(\"LLVM_VERSION\"),\n});\n\npub static IS_APPIMAGE: Lazy<bool> = Lazy::new(|| std::env::var(\"APPIMAGE\").is_ok());\n\n#[cfg(target_os = \"windows\")]\npub static IS_PORTABLE: Lazy<bool> = Lazy::new(|| {\n    if cfg!(windows) {\n        let dir = crate::utils::dirs::app_install_dir().unwrap();\n        let portable_file = dir.join(\".config/PORTABLE\");\n        portable_file.exists()\n    } else {\n        false\n    }\n});\n\n/// A Tauri AppHandle copy for access from global context,\n/// maybe only access it from panic handler\nstatic APP_HANDLE: OnceCell<AppHandle> = OnceCell::new();\npub fn app_handle() -> &'static AppHandle {\n    APP_HANDLE.get().expect(\"app handle not initialized\")\n}\n\npub(super) fn setup_app_handle(app_handle: AppHandle) {\n    let _ = APP_HANDLE.set(app_handle);\n}\n"
  },
  {
    "path": "backend/tauri/src/core/clash/api.rs",
    "content": "use crate::config::Config;\nuse anyhow::{Context, Result};\nuse indexmap::IndexMap;\nuse reqwest::{Method, StatusCode, header::HeaderMap};\nuse serde::{Deserialize, Serialize};\nuse serde_yaml::Mapping;\nuse specta::Type;\nuse std::{\n    collections::HashMap,\n    fmt::{self, Display, Formatter},\n};\nuse tracing_attributes::instrument;\nuse url::Url;\n\n/// PUT /configs\n/// path 是绝对路径\n#[instrument]\npub async fn put_configs(config_path: &str) -> Result<()> {\n    let path = \"/configs\";\n\n    let mut data = HashMap::new();\n    data.insert(\"path\", config_path);\n\n    let _ = perform_request((Method::PUT, path, Data(data))).await?;\n\n    Ok(())\n}\n\n/// PATCH /configs\n#[instrument]\npub async fn patch_configs(config: &Mapping) -> Result<()> {\n    let path = \"/configs\";\n    let _ = perform_request((Method::PATCH, path, Data(config))).await?;\n    Ok(())\n}\n\n#[derive(Debug, Clone, Deserialize, Serialize, Type)]\n#[serde(rename_all = \"camelCase\")]\npub struct ProxiesRes {\n    #[serde(default)]\n    pub proxies: IndexMap<String, ProxyItem>,\n}\n\n#[derive(Debug, Clone, Deserialize, Serialize, Default, Type)]\n#[serde(rename_all = \"camelCase\")]\npub struct ProxyItemHistory {\n    pub time: String,\n    pub delay: i64,\n}\n\n#[derive(Debug, Clone, Deserialize, Serialize, Default, Type)]\n#[serde(rename_all = \"camelCase\")]\npub struct ProxyItem {\n    pub name: String,\n    pub r#type: String, // TODO: 考虑改成枚举\n    pub udp: bool,\n    pub history: Vec<ProxyItemHistory>,\n    pub all: Option<Vec<String>>,\n    pub now: Option<String>, // 当前选中的代理\n    pub provider: Option<String>,\n    pub alive: Option<bool>, // Mihomo Or Premium Only\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub xudp: Option<bool>, // Mihomo Only\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub tfo: Option<bool>, // Mihomo Only\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub icon: Option<String>, // Mihomo Only\n    #[serde(default)]\n    pub hidden: bool, // Mihomo Only\n                             // extra: {}, // Mihomo Only\n}\n\nimpl From<ProxyProviderItem> for ProxyItem {\n    fn from(item: ProxyProviderItem) -> Self {\n        let ProxyProviderItem {\n            name,\n            r#type,\n            proxies,\n            vehicle_type: _,\n            test_url: _,\n            expected_status: _,\n        } = item;\n\n        let now = proxies\n            .iter()\n            .find(|p| p.now.is_some())\n            .map(|p| p.name.clone())\n            .unwrap_or_default();\n\n        let all = proxies.iter().map(|p| p.name.clone()).collect();\n\n        Self {\n            name,\n            r#type: r#type.to_string(),\n            udp: false,\n            history: vec![],\n            all: Some(all),\n            now: Some(now),\n            provider: None,\n            alive: None,\n            xudp: None,\n            tfo: None,\n            icon: None,\n            hidden: false,\n        }\n    }\n}\n\n/// GET /proxies\n/// 获取代理列表\n#[instrument]\npub async fn get_proxies() -> Result<ProxiesRes> {\n    let path = \"/proxies\";\n    let resp: ProxiesRes = perform_request((Method::GET, path)).await?.json().await?;\n    Ok(resp)\n}\n\n/// GET /proxies/{name}\n/// 获取单个代理\n/// name: 代理名称\n/// 返回代理的配置\n///\n#[allow(dead_code)]\n#[instrument]\npub async fn get_proxy(name: String) -> Result<ProxyItem> {\n    let path = format!(\"/proxies/{name}\");\n    let resp: ProxyItem = perform_request((Method::GET, path.as_str()))\n        .await?\n        .json()\n        .await?;\n    Ok(resp)\n}\n\n/// PUT /proxies/{group}\n/// 选择代理\n/// group: 代理分组名称\n/// name: 代理名称\n#[instrument]\npub async fn update_proxy(group: &str, name: &str) -> Result<()> {\n    let path = format!(\"/proxies/{group}\");\n\n    let mut data = HashMap::new();\n    data.insert(\"name\", name);\n\n    let _ = perform_request((Method::PUT, path.as_str(), Data(data))).await?;\n    Ok(())\n}\n\n#[derive(Debug, Clone, Deserialize, Serialize, Type)]\npub enum VehicleType {\n    File,\n    #[serde(rename = \"HTTP\")]\n    Http,\n    Compatible,\n    Unknown,\n}\n\n#[derive(Debug, Clone, Deserialize, Serialize, specta::Type)]\npub enum ProviderType {\n    Proxy,\n    Rule,\n    Unknown,\n}\n\nimpl Display for ProviderType {\n    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {\n        match self {\n            ProviderType::Proxy => write!(f, \"Proxy\"),\n            ProviderType::Rule => write!(f, \"Rule\"),\n            ProviderType::Unknown => write!(f, \"Unknown\"),\n        }\n    }\n}\n\n#[derive(Debug, Clone, Deserialize, Serialize, Type)]\n#[serde(rename_all = \"camelCase\")]\npub struct ProxyProviderItem {\n    pub name: String,\n    pub r#type: ProviderType,\n    pub proxies: Vec<ProxyItem>,\n    pub vehicle_type: VehicleType,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub test_url: Option<String>, // Mihomo Only\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub expected_status: Option<String>, // Mihomo Only\n}\n\n#[derive(Debug, Clone, Deserialize, Serialize, Type)]\n#[serde(rename_all = \"camelCase\")]\npub struct ProvidersProxiesRes {\n    #[serde(default)]\n    pub providers: IndexMap<String, ProxyProviderItem>,\n}\n\n/// GET /providers/proxies\n/// 获取所有代理集合的所有代理信息\n#[instrument]\npub async fn get_providers_proxies() -> Result<ProvidersProxiesRes> {\n    let path = \"/providers/proxies\";\n    let resp: ProvidersProxiesRes = perform_request((Method::GET, path)).await?.json().await?;\n    Ok(resp)\n}\n\n/// GET /providers/proxies/:name\n/// 获取单个代理集合的所有代理信息\n/// group: 代理集合名称\n#[allow(dead_code)]\n#[instrument]\npub async fn get_providers_proxies_group(group: String) -> Result<ProxyProviderItem> {\n    let path = format!(\"/providers/proxies/{group}\");\n    let resp: ProxyProviderItem = perform_request((Method::GET, path.as_str()))\n        .await?\n        .json()\n        .await?;\n    Ok(resp)\n}\n\n/// PUT /providers/proxies/:name\n/// 更新代理集合\n/// name: 代理集合名称\n#[instrument]\npub async fn update_providers_proxies_group(name: &str) -> Result<()> {\n    let path = format!(\"/providers/proxies/{name}\");\n    let _ = perform_request((Method::PUT, path.as_str())).await?;\n    Ok(())\n}\n\n/// GET /providers/proxies/:name/healthcheck\n/// 获取代理集合的健康检查\n/// name: 代理集合名称\n#[allow(dead_code)]\n#[instrument]\npub async fn get_providers_proxies_healthcheck(name: String) -> Result<Mapping> {\n    let path = format!(\"/providers/proxies/{name}/healthcheck\");\n    let resp: Mapping = perform_request((Method::GET, path.as_str()))\n        .await?\n        .json()\n        .await?;\n    Ok(resp)\n}\n\n#[derive(Default, Debug, Clone, Deserialize, Serialize, Type)]\npub struct DelayRes {\n    delay: u64,\n}\n\n/// GET /proxies/{name}/delay\n/// 获取代理延迟\n#[instrument]\npub async fn get_proxy_delay(name: String, test_url: Option<String>) -> Result<DelayRes> {\n    let path = format!(\"/proxies/{name}/delay\");\n    let default_url = \"http://www.gstatic.com/generate_204\";\n    let test_url = test_url\n        .map(|s| if s.is_empty() { default_url.into() } else { s })\n        .unwrap_or(default_url.into());\n\n    let query = Query([(\"timeout\", \"10000\"), (\"url\", &test_url)]);\n    let resp: DelayRes = perform_request((Method::GET, path.as_str(), query))\n        .await?\n        .json()\n        .await?;\n    Ok(resp)\n}\n\n/// 根据clash info获取clash服务地址和请求头\n#[instrument]\nfn clash_client_info() -> Result<(String, HeaderMap)> {\n    let client = { Config::clash().data().get_client_info() };\n\n    let server = format!(\"http://{}\", client.server);\n\n    let mut headers = HeaderMap::new();\n    headers.insert(\"Content-Type\", \"application/json\".parse()?);\n\n    if let Some(secret) = client.secret {\n        let secret = format!(\"Bearer {secret}\").parse()?;\n        headers.insert(\"Authorization\", secret);\n    }\n\n    Ok((server, headers))\n}\n\n/// The Request Parameters\nstruct PerformRequest<D = (), Q = ()> {\n    method: reqwest::Method,\n    path: String,\n    query: Option<Q>,\n    data: Option<D>,\n}\n/// A newtype wrapper for query parameters\nstruct Query<T>(T);\n/// A newtype wrapper for request body\nstruct Data<T>(T);\n\nimpl From<(reqwest::Method, &str)> for PerformRequest<(), ()> {\n    fn from((method, path): (reqwest::Method, &str)) -> Self {\n        Self {\n            method,\n            path: path.to_string(),\n            data: None,\n            query: None,\n        }\n    }\n}\n\nimpl<T> From<(reqwest::Method, &str, Data<T>)> for PerformRequest<T, ()>\nwhere\n    T: Serialize,\n{\n    fn from((method, path, Data(data)): (reqwest::Method, &str, Data<T>)) -> Self {\n        Self {\n            method,\n            path: path.to_string(),\n            data: Some(data),\n            query: None,\n        }\n    }\n}\n\nimpl<T> From<(reqwest::Method, &str, Query<T>)> for PerformRequest<(), T>\nwhere\n    T: Serialize,\n{\n    fn from((method, path, Query(query)): (reqwest::Method, &str, Query<T>)) -> Self {\n        Self {\n            method,\n            path: path.to_string(),\n            data: None,\n            query: Some(query),\n        }\n    }\n}\n\nimpl<D, Q> From<(reqwest::Method, &str, Query<Q>, Data<D>)> for PerformRequest<D, Q>\nwhere\n    D: Serialize,\n    Q: Serialize,\n{\n    fn from(\n        (method, path, Query(query), Data(data)): (reqwest::Method, &str, Query<Q>, Data<D>),\n    ) -> Self {\n        Self {\n            method,\n            path: path.to_string(),\n            data: Some(data),\n            query: Some(query),\n        }\n    }\n}\n\n#[instrument(skip_all, fields(\n    method = tracing::field::Empty,\n    url = tracing::field::Empty,\n    query = tracing::field::Empty,\n    data = tracing::field::Empty,\n))]\nasync fn perform_request<D, Q>(param: impl Into<PerformRequest<D, Q>>) -> Result<reqwest::Response>\nwhere\n    Q: Serialize + core::fmt::Debug,\n    D: Serialize + core::fmt::Debug,\n{\n    let PerformRequest {\n        method,\n        path,\n        data,\n        query,\n    } = param.into();\n    let (host, headers) = clash_client_info().context(\"failed to get clash client info\")?;\n    let base_url = Url::parse(&host).context(\"failed to parse host\")?;\n    let opts = url::Url::options().base_url(Some(&base_url));\n    let url = opts.parse(&path).context(\"failed to parse path\")?;\n\n    let span = tracing::Span::current();\n    span.record(\"method\", tracing::field::display(&method));\n    span.record(\"url\", tracing::field::display(&url));\n    span.record(\"query\", tracing::field::debug(&query));\n    span.record(\"data\", tracing::field::debug(&data));\n\n    async {\n        let client = reqwest::ClientBuilder::new().no_proxy().build()?;\n        let mut builder = client.request(method.clone(), url.clone()).headers(headers);\n\n        if let Some(query) = &query {\n            builder = builder.query(query);\n        }\n        if let Some(data) = &data {\n            builder = builder.json(data);\n        }\n\n        let resp = builder.send().await?;\n\n        if let Err(err) = resp.error_for_status_ref() {\n            match err.status() {\n                // Try To parse error message\n                Some(StatusCode::BAD_REQUEST) => {\n                    let Ok(bytes) = resp.bytes().await else {\n                        return Err(err.into());\n                    };\n\n                    let message: serde_json::Value = match serde_json::from_slice(&bytes) {\n                        Ok(v) => v,\n                        Err(_) => {\n                            let s = String::from_utf8_lossy(&bytes);\n                            serde_json::Value::String(s.to_string())\n                        }\n                    };\n\n                    return Err(err).context(format!(\"message: {message}\"));\n                }\n                _ => return Err(err).context(\"clash api error\"),\n            }\n        }\n        Ok(resp)\n    }\n    .await\n    .inspect_err(|e| tracing::error!(method = %method, url = %url, query = ?query, data = ?data, \"failed to perform request: {:?}\", e))\n}\n\n/// 缩短clash的日志\n#[instrument]\npub fn parse_log(log: String) -> String {\n    if log.starts_with(\"time=\") && log.len() > 33 {\n        return log[33..].to_owned();\n    }\n    if log.len() > 9 {\n        return log[9..].to_owned();\n    }\n    log\n}\n\n/// 缩短clash -t的错误输出\n/// 仅适配 clash p核 8-26、clash meta 1.13.1\n#[instrument]\npub fn parse_check_output(log: String) -> String {\n    let t = log.find(\"time=\");\n    let m = log.find(\"msg=\");\n    let mr = log.rfind('\"');\n\n    if let (Some(_), Some(m), Some(mr)) = (t, m, mr) {\n        let e = match log.find(\"level=error msg=\") {\n            Some(e) => e + 17,\n            None => m + 5,\n        };\n\n        if mr > m {\n            return log[e..mr].to_owned();\n        }\n    }\n\n    let l = log.find(\"error=\");\n    let r = log.find(\"path=\").or(Some(log.len()));\n\n    if let (Some(l), Some(r)) = (l, r) {\n        return log[(l + 6)..(r - 1)].to_owned();\n    }\n\n    log\n}\n\n/// DELETE /connections\n/// Close all connections or a specific connection by ID\n#[instrument]\npub async fn delete_connections(id: Option<&str>) -> Result<()> {\n    let path = match id {\n        Some(id) => format!(\"/connections/{}\", id),\n        None => \"/connections\".to_string(),\n    };\n\n    let _ = perform_request((Method::DELETE, path.as_str())).await?;\n    Ok(())\n}\n\n#[test]\nfn test_parse_check_output() {\n    let str1 = r#\"xxxx\\n time=\"2022-11-18T20:42:58+08:00\" level=error msg=\"proxy 0: 'alpn' expected type 'string', got unconvertible type '[]interface {}'\"\"#;\n    let str2 = r#\"20:43:49 ERR [Config] configuration file test failed error=proxy 0: unsupport proxy type: hysteria path=xxx\"#;\n    let str3 = r#\"\n    \"time=\"2022-11-18T21:38:01+08:00\" level=info msg=\"Start initial configuration in progress\"\n    time=\"2022-11-18T21:38:01+08:00\" level=error msg=\"proxy 0: 'alpn' expected type 'string', got unconvertible type '[]interface {}'\"\n    configuration file xxx\\n\n    \"#;\n\n    let res1 = parse_check_output(str1.into());\n    let res2 = parse_check_output(str2.into());\n    let res3 = parse_check_output(str3.into());\n\n    println!(\"res1: {res1}\");\n    println!(\"res2: {res2}\");\n    println!(\"res3: {res3}\");\n\n    assert_eq!(res1, res3);\n}\n\n#[test]\nfn test_path() {\n    let host = \"http://127.0.0.1:9090\";\n    let path_with_prefix = \"/configs\";\n\n    let base_url = Url::parse(host).context(\"failed to parse host\").unwrap();\n    let opts = url::Url::options().base_url(Some(&base_url));\n    let url = opts\n        .parse(path_with_prefix)\n        .context(\"failed to parse path\")\n        .unwrap();\n    assert_eq!(url.to_string(), \"http://127.0.0.1:9090/configs\");\n\n    let path_without_prefix = \"configs\";\n    let url = opts\n        .parse(path_without_prefix)\n        .context(\"failed to parse path\")\n        .unwrap();\n    assert_eq!(url.to_string(), \"http://127.0.0.1:9090/configs\");\n}\n"
  },
  {
    "path": "backend/tauri/src/core/clash/core.rs",
    "content": "use super::api;\nuse crate::{\n    config::{Config, ConfigType, nyanpasu::ClashCore},\n    core::logger::Logger,\n    log_err,\n    utils::dirs,\n};\nuse anyhow::{Context, Result, bail};\nuse camino::Utf8PathBuf;\n#[cfg(target_os = \"macos\")]\nuse nyanpasu_ipc::api::network::set_dns::NetworkSetDnsReq;\nuse nyanpasu_ipc::{\n    api::{core::start::CoreStartReq, status::CoreState},\n    utils::get_current_ts,\n};\nuse nyanpasu_utils::{\n    core::{\n        CommandEvent,\n        instance::{CoreInstance, CoreInstanceBuilder},\n    },\n    runtime::{block_on, spawn},\n};\nuse once_cell::sync::OnceCell;\nuse parking_lot::Mutex;\nuse serde::{Deserialize, Serialize};\nuse specta::Type;\nuse std::{\n    borrow::Cow,\n    path::PathBuf,\n    sync::{\n        Arc,\n        atomic::{AtomicBool, AtomicI64, Ordering},\n    },\n    time::Duration,\n};\nuse tokio::time::sleep;\nuse tracing_attributes::instrument;\n\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Type)]\n#[serde(rename_all = \"snake_case\")]\npub enum RunType {\n    /// Run as child process directly\n    Normal,\n    /// Run by Nyanpasu Service via a ipc call\n    Service,\n    // TODO: Not implemented yet\n    /// Run as elevated process, if profile advice to run as elevated\n    Elevated,\n}\n\nimpl Default for RunType {\n    fn default() -> Self {\n        let enable_service = {\n            *Config::verge()\n                .latest()\n                .enable_service_mode\n                .as_ref()\n                .unwrap_or(&false)\n        };\n        if enable_service && crate::core::service::ipc::get_ipc_state().is_connected() {\n            tracing::info!(\"run core as service\");\n            RunType::Service\n        } else {\n            tracing::info!(\"run core as child process\");\n            RunType::Normal\n        }\n    }\n}\n\n#[derive(Debug)]\nenum Instance {\n    Child {\n        child: Mutex<Arc<CoreInstance>>,\n        stated_changed_at: Arc<AtomicI64>,\n        kill_flag: Arc<AtomicBool>,\n    },\n    Service {\n        config_path: PathBuf,\n        core_type: nyanpasu_utils::core::CoreType,\n    },\n}\n\nimpl Instance {\n    pub fn try_new(run_type: RunType) -> Result<Self> {\n        let core_type: nyanpasu_utils::core::CoreType = {\n            (Config::verge()\n                .latest()\n                .clash_core\n                .as_ref()\n                .unwrap_or(&ClashCore::ClashPremium))\n            .into()\n        };\n        let data_dir = camino::Utf8PathBuf::from_path_buf(dirs::app_data_dir()?)\n            .map_err(|e| anyhow::anyhow!(\"failed to convert data dir to utf8 path: {:?}\", e))?;\n        let binary = camino::Utf8PathBuf::from_path_buf(find_binary_path(&core_type)?)\n            .map_err(|e| anyhow::anyhow!(\"failed to convert binary path to utf8 path: {:?}\", e))?;\n        let config_path = camino::Utf8PathBuf::from_path_buf(Config::generate_file(\n            ConfigType::Run,\n        )?)\n        .map_err(|e| anyhow::anyhow!(\"failed to convert config path to utf8 path: {:?}\", e))?;\n        let pid_path = camino::Utf8PathBuf::from_path_buf(dirs::clash_pid_path()?)\n            .map_err(|e| anyhow::anyhow!(\"failed to convert pid path to utf8 path: {:?}\", e))?;\n        match run_type {\n            RunType::Normal => {\n                let instance = Arc::new(\n                    CoreInstanceBuilder::default()\n                        .core_type(core_type)\n                        .app_dir(data_dir)\n                        .binary_path(binary)\n                        .config_path(config_path.clone())\n                        .pid_path(pid_path)\n                        .build()?,\n                );\n                Ok(Instance::Child {\n                    child: Mutex::new(instance),\n                    kill_flag: Arc::new(AtomicBool::new(false)),\n                    stated_changed_at: Arc::new(AtomicI64::new(get_current_ts())),\n                })\n            }\n            RunType::Service => Ok(Instance::Service {\n                config_path: config_path.into(),\n                core_type,\n            }),\n            RunType::Elevated => {\n                todo!()\n            }\n        }\n    }\n\n    pub fn run_type(&self) -> RunType {\n        match self {\n            Instance::Child { .. } => RunType::Normal,\n            Instance::Service { .. } => RunType::Service,\n        }\n    }\n\n    pub async fn start(&self) -> Result<()> {\n        match self {\n            Instance::Child {\n                child,\n                kill_flag,\n                stated_changed_at,\n            } => {\n                let instance = {\n                    let child = child.lock();\n                    child.clone()\n                };\n                let (is_premium, core_type) = {\n                    let child = child.lock();\n                    (\n                        matches!(\n                            child.core_type,\n                            nyanpasu_utils::core::CoreType::Clash(\n                                nyanpasu_utils::core::ClashCoreType::ClashPremium\n                            )\n                        ),\n                        child.core_type.clone(),\n                    )\n                };\n                let (tx, mut rx) = tokio::sync::mpsc::channel::<anyhow::Result<()>>(1); // use mpsc channel just to avoid type moved error, though it never fails\n                let stated_changed_at = stated_changed_at.clone();\n                let kill_flag = kill_flag.clone();\n                // This block below is to handle the stdio from the core process\n                tokio::spawn(async move {\n                    match instance.run().await {\n                        Ok((_, mut rx)) => {\n                            kill_flag.store(false, Ordering::Release); // reset kill flag\n                            let mut err_buf: Vec<String> = Vec::with_capacity(6);\n                            loop {\n                                if let Some(event) = rx.recv().await {\n                                    match event {\n                                        CommandEvent::Stdout(line) => {\n                                            if is_premium {\n                                                let log = api::parse_log(line.clone());\n                                                log::info!(target: \"app\", \"[{core_type}]: {log}\");\n                                            } else {\n                                                log::info!(target: \"app\", \"[{core_type}]: {line}\");\n                                            }\n                                            Logger::global().set_log(line);\n                                        }\n                                        CommandEvent::Stderr(line) => {\n                                            log::error!(target: \"app\", \"[{core_type}]: {line}\");\n                                            err_buf.push(line.clone());\n                                            Logger::global().set_log(line);\n                                        }\n                                        CommandEvent::Error(e) => {\n                                            log::error!(target: \"app\", \"[{core_type}]: {e}\");\n                                            let err = anyhow::anyhow!(format!(\n                                                \"{}\\n{}\",\n                                                e,\n                                                err_buf.join(\"\\n\")\n                                            ));\n                                            Logger::global().set_log(e);\n                                            let _ = tx.send(Err(err)).await;\n                                            stated_changed_at\n                                                .store(get_current_ts(), Ordering::Relaxed);\n                                            break;\n                                        }\n                                        CommandEvent::Terminated(status) => {\n                                            log::error!(\n                                                target: \"app\",\n                                                \"core terminated with status: {status:?}\"\n                                            );\n                                            stated_changed_at\n                                                .store(get_current_ts(), Ordering::Relaxed);\n                                            if status.code != Some(0)\n                                                || !matches!(status.signal, Some(9) | Some(15))\n                                            {\n                                                let err = anyhow::anyhow!(format!(\n                                                    \"core terminated with status: {:?}\\n{}\",\n                                                    status,\n                                                    err_buf.join(\"\\n\")\n                                                ));\n                                                tracing::error!(\"{}\\n{}\", err, err_buf.join(\"\\n\"));\n                                                if tx.send(Err(err)).await.is_err()\n                                                    && !kill_flag.load(Ordering::Acquire)\n                                                {\n                                                    std::thread::spawn(move || {\n                                                        block_on(async {\n                                                            tracing::info!(\n                                                                \"Trying to recover core.\"\n                                                            );\n                                                            let _ = CoreManager::global()\n                                                                .recover_core()\n                                                                .await;\n                                                        });\n                                                    });\n                                                }\n                                            }\n                                            break;\n                                        }\n                                        CommandEvent::DelayCheckpointPass => {\n                                            tracing::debug!(\"delay checkpoint pass\");\n                                            stated_changed_at\n                                                .store(get_current_ts(), Ordering::Relaxed);\n                                            tx.send(Ok(())).await.unwrap();\n                                        }\n                                    }\n                                }\n                            }\n                        }\n                        Err(err) => {\n                            spawn(async move {\n                                tx.send(Err(err.into())).await.unwrap();\n                            });\n                        }\n                    }\n                });\n                rx.recv().await.unwrap()?;\n                Ok(())\n            }\n            Instance::Service {\n                config_path,\n                core_type,\n            } => {\n                let payload = CoreStartReq {\n                    config_file: Cow::Borrowed(config_path),\n                    core_type: Cow::Borrowed(core_type),\n                };\n                nyanpasu_ipc::client::shortcuts::Client::service_default()\n                    .start_core(&payload)\n                    .await?;\n                Ok(())\n            }\n        }\n    }\n\n    pub async fn stop(&self) -> Result<()> {\n        let state = self.state().await;\n        match self {\n            Instance::Child {\n                child,\n                stated_changed_at,\n                kill_flag,\n            } => {\n                if matches!(state.as_ref(), CoreState::Stopped(_)) {\n                    anyhow::bail!(\"core is already stopped\");\n                }\n                kill_flag.store(true, Ordering::Release);\n                let child = {\n                    let child = child.lock();\n                    child.clone()\n                };\n                child.kill().await?;\n                stated_changed_at.store(get_current_ts(), Ordering::Relaxed);\n                Ok(())\n            }\n            Instance::Service { .. } => {\n                Ok(nyanpasu_ipc::client::shortcuts::Client::service_default()\n                    .stop_core()\n                    .await?)\n            }\n        }\n    }\n\n    #[allow(dead_code)]\n    pub async fn restart(&self) -> Result<()> {\n        let state = self.state().await;\n        if matches!(state.as_ref(), CoreState::Running) {\n            self.stop().await?;\n        }\n        self.start().await\n    }\n\n    pub async fn state<'a>(&self) -> Cow<'a, CoreState> {\n        match self {\n            Instance::Child { child, .. } => {\n                let this = child.lock();\n                Cow::Borrowed(match this.state() {\n                    nyanpasu_utils::core::instance::CoreInstanceState::Running => {\n                        &CoreState::Running\n                    }\n                    nyanpasu_utils::core::instance::CoreInstanceState::Stopped => {\n                        &CoreState::Stopped(None)\n                    }\n                })\n            }\n            Instance::Service { .. } => {\n                let status = nyanpasu_ipc::client::shortcuts::Client::service_default()\n                    .status()\n                    .await\n                    .map(|info| match info.core_infos.state {\n                        nyanpasu_ipc::api::status::CoreState::Running => CoreState::Running,\n                        nyanpasu_ipc::api::status::CoreState::Stopped(_) => {\n                            CoreState::Stopped(None)\n                        }\n                    })\n                    .unwrap_or(CoreState::Stopped(None));\n                Cow::Owned(status)\n            }\n        }\n    }\n\n    /// get core state with state changed timestamp\n    pub async fn status<'a>(&self) -> (Cow<'a, CoreState>, i64) {\n        match self {\n            Instance::Child {\n                child,\n                stated_changed_at,\n                ..\n            } => {\n                let this = child.lock();\n                (\n                    Cow::Borrowed(match this.state() {\n                        nyanpasu_utils::core::instance::CoreInstanceState::Running => {\n                            &CoreState::Running\n                        }\n                        nyanpasu_utils::core::instance::CoreInstanceState::Stopped => {\n                            &CoreState::Stopped(None)\n                        }\n                    }),\n                    stated_changed_at.load(Ordering::Relaxed),\n                )\n            }\n            Instance::Service { .. } => {\n                let status = nyanpasu_ipc::client::shortcuts::Client::service_default()\n                    .status()\n                    .await;\n                match status {\n                    Ok(info) => (\n                        Cow::Owned(match info.core_infos.state {\n                            nyanpasu_ipc::api::status::CoreState::Running => CoreState::Running,\n                            nyanpasu_ipc::api::status::CoreState::Stopped(_) => {\n                                CoreState::Stopped(None)\n                            }\n                        }),\n                        info.core_infos.state_changed_at,\n                    ),\n                    Err(_) => (Cow::Owned(CoreState::Stopped(None)), 0),\n                }\n            }\n        }\n    }\n}\n\n#[derive(Debug)]\npub struct CoreManager {\n    instance: Mutex<Option<Arc<Instance>>>,\n    #[cfg(target_os = \"macos\")]\n    previous_dns: tokio::sync::Mutex<Option<Vec<std::net::IpAddr>>>,\n}\n\nimpl CoreManager {\n    pub fn global() -> &'static CoreManager {\n        static CORE_MANAGER: OnceCell<CoreManager> = OnceCell::new();\n        CORE_MANAGER.get_or_init(|| CoreManager {\n            instance: Mutex::new(None),\n            #[cfg(target_os = \"macos\")]\n            previous_dns: tokio::sync::Mutex::new(None),\n        })\n    }\n\n    pub async fn status<'a>(&self) -> (Cow<'a, CoreState>, i64, RunType) {\n        let instance = {\n            let instance = self.instance.lock();\n            instance.as_ref().cloned()\n        };\n        if let Some(instance) = instance {\n            let (state, ts) = instance.status().await;\n            (state, ts, instance.run_type())\n        } else {\n            (\n                Cow::Owned(CoreState::Stopped(None)),\n                0_i64,\n                RunType::default(),\n            )\n        }\n    }\n\n    pub fn init(&self) -> Result<()> {\n        tauri::async_runtime::spawn(async {\n            // 启动clash\n            log_err!(Self::global().run_core().await);\n        });\n\n        Ok(())\n    }\n\n    /// 检查配置是否正确\n    pub async fn check_config(&self) -> Result<()> {\n        use nyanpasu_utils::core::instance::CoreInstance;\n        let config_path = Config::generate_file(ConfigType::Check)?;\n        let config_path = Utf8PathBuf::from_path_buf(config_path)\n            .map_err(|_| anyhow::anyhow!(\"failed to convert config path to utf8 path\"))?;\n\n        let clash_core = { Config::verge().latest().clash_core };\n        let clash_core = clash_core.unwrap_or(ClashCore::ClashPremium);\n        let clash_core: nyanpasu_utils::core::CoreType = (&clash_core).into();\n\n        let app_dir = dirs::app_data_dir()?;\n        let app_dir = Utf8PathBuf::from_path_buf(app_dir)\n            .map_err(|_| anyhow::anyhow!(\"failed to convert app dir to utf8 path\"))?;\n        let binary_path = find_binary_path(&clash_core)?;\n        let binary_path = Utf8PathBuf::from_path_buf(binary_path)\n            .map_err(|_| anyhow::anyhow!(\"failed to convert binary path to utf8 path\"))?;\n        log::debug!(target: \"app\", \"check config in `{clash_core}`\");\n        CoreInstance::check_config_(&clash_core, &config_path, &binary_path, &app_dir)\n            .await\n            .context(\"failed to check config\")\n            .inspect_err(|e| log::error!(target: \"app\", \"failed to check config: {e:?}\"))?;\n\n        Ok(())\n    }\n\n    /// 启动核心\n    pub async fn run_core(&self) -> Result<()> {\n        {\n            let instance = {\n                let instance = self.instance.lock();\n                instance.as_ref().cloned()\n            };\n            if let Some(instance) = instance.as_ref()\n                && matches!(instance.state().await.as_ref(), CoreState::Running)\n            {\n                log::debug!(target: \"app\", \"core is already running, stop it first...\");\n                instance.stop().await?;\n            }\n        }\n\n        // Reload clash config from file to get latest user preferences (e.g., mode)\n        Config::clash().reload();\n        log::debug!(target: \"app\", \"reloaded clash config from file\");\n\n        // Regenerate runtime config with the reloaded settings\n        Config::generate().await?;\n\n        // 检查端口是否可用\n        Config::clash()\n            .latest()\n            .prepare_external_controller_port()?;\n        let run_type = RunType::default();\n        let instance = Arc::new(Instance::try_new(run_type)?);\n\n        #[cfg(target_os = \"macos\")]\n        {\n            let enable_tun = Config::verge().latest().enable_tun_mode.unwrap_or(false);\n            let _ = self\n                .change_default_network_dns(enable_tun)\n                .await\n                .inspect_err(|e| log::error!(target: \"app\", \"failed to set system dns: {:?}\", e));\n        }\n\n        {\n            let mut this = self.instance.lock();\n            *this = Some(instance.clone());\n        }\n        instance.start().await\n    }\n\n    /// 重启内核\n    pub async fn recover_core(&'static self) -> Result<()> {\n        // 清除原来的实例\n        {\n            let instance = {\n                let mut this = self.instance.lock();\n                this.take()\n            };\n            if let Some(instance) = instance\n                && matches!(instance.state().await.as_ref(), CoreState::Running)\n            {\n                log::debug!(target: \"app\", \"core is running, stop it first...\");\n                instance.stop().await?;\n            }\n        }\n\n        if let Err(err) = self.run_core().await {\n            log::error!(target: \"app\", \"failed to recover clash core\");\n            log::error!(target: \"app\", \"{err:?}\");\n            tokio::time::sleep(Duration::from_secs(5)).await; // sleep 5s\n            std::thread::spawn(move || {\n                block_on(async {\n                    let _ = CoreManager::global().recover_core().await;\n                })\n            });\n        }\n\n        Ok(())\n    }\n\n    /// 停止核心运行\n    pub async fn stop_core(&self) -> Result<()> {\n        #[cfg(target_os = \"macos\")]\n        let _ = self\n            .change_default_network_dns(false)\n            .await\n            .inspect_err(|e| log::error!(target: \"app\", \"failed to set system dns: {:?}\", e));\n        let instance = {\n            let instance = self.instance.lock();\n            instance.as_ref().cloned()\n        };\n        if let Some(instance) = instance.as_ref() {\n            instance.stop().await?;\n        }\n        Ok(())\n    }\n\n    /// 切换核心\n    #[instrument(skip(self))]\n    pub async fn change_core(&self, clash_core: Option<ClashCore>) -> Result<()> {\n        let clash_core = clash_core.ok_or(anyhow::anyhow!(\"clash core is null\"))?;\n\n        log::debug!(target: \"app\", \"change core to `{clash_core}`\");\n\n        Config::verge().draft().clash_core = Some(clash_core);\n\n        // 更新配置\n        Config::generate().await?;\n\n        self.check_config().await?;\n\n        // 清掉旧日志\n        Logger::global().clear_log();\n\n        match self.run_core().await {\n            Ok(_) => {\n                tracing::info!(\"change core success\");\n                Config::verge().apply();\n                Config::runtime().apply();\n                log_err!(Config::verge().latest().save_file());\n                Ok(())\n            }\n            Err(err) => {\n                tracing::error!(\"failed to change core: {err:?}\");\n                Config::verge().discard();\n                Config::runtime().discard();\n                self.run_core().await?;\n                Err(err)\n            }\n        }\n    }\n\n    /// 更新proxies那些\n    /// 如果涉及端口和外部控制则需要重启\n    pub async fn update_config(&self) -> Result<()> {\n        log::debug!(target: \"app\", \"try to update clash config\");\n\n        // 更新配置\n        Config::generate().await?;\n\n        // 检查配置是否正常\n        self.check_config().await?;\n\n        // 更新运行时配置\n        let path = Config::generate_file(ConfigType::Run)?;\n        let path = dirs::path_to_str(&path)?;\n\n        // 发送请求 发送5次\n        for i in 0..5 {\n            match api::put_configs(path).await {\n                Ok(_) => break,\n                Err(err) => {\n                    if i < 4 {\n                        log::info!(target: \"app\", \"{err:?}\");\n                    } else {\n                        bail!(err);\n                    }\n                }\n            }\n            sleep(Duration::from_millis(250)).await;\n        }\n\n        Ok(())\n    }\n\n    #[cfg(target_os = \"macos\")]\n    pub async fn change_default_network_dns(&self, enabled: bool) -> Result<()> {\n        use anyhow::Context;\n        use nyanpasu_utils::network::macos::*;\n\n        let run_type = RunType::default();\n\n        log::debug!(target: \"app\", \"try to set system dns\");\n        let default_device =\n            get_default_network_hardware_port().context(\"failed to get default network device\")?;\n        log::debug!(target: \"app\", \"current default network device: {:?}\", default_device);\n        let tun_device_ip = Config::clash()\n            .clone()\n            .latest()\n            .get_tun_device_ip()\n            .parse::<std::net::IpAddr>()\n            .context(\"failed to parse tun device ip\")?;\n        log::debug!(target: \"app\", \"current tun device ip: {:?}\", tun_device_ip);\n\n        let current_dns = get_dns(&default_device).context(\"failed to get current dns\")?;\n        log::debug!(target: \"app\", \"current dns: {:?}\", current_dns);\n        let current_dns_contains_tun_device_ip = current_dns\n            .as_ref()\n            .is_some_and(|dns| dns.contains(&tun_device_ip));\n        let mut previous_dns = self.previous_dns.lock().await;\n        let previous_dns_clone = previous_dns.clone();\n        let new_dns = match enabled {\n            true if !current_dns_contains_tun_device_ip => {\n                *previous_dns = current_dns;\n                Some(Some(vec![tun_device_ip]))\n            }\n            false if current_dns_contains_tun_device_ip => Some(previous_dns.take()),\n            _ => None,\n        };\n        if let Some(new_dns) = new_dns {\n            log::debug!(target: \"app\", \"set new dns: {:?}\", new_dns);\n            let result = match run_type {\n                RunType::Service => {\n                    nyanpasu_ipc::client::shortcuts::Client::service_default()\n                        .set_dns(&NetworkSetDnsReq {\n                            // FIXME: improve this type notation\n                            dns_servers: new_dns\n                                .as_ref()\n                                .map(|dns| dns.iter().map(Cow::Borrowed).collect()),\n                        })\n                        .await\n                        .map_err(anyhow::Error::from)\n                }\n                _ => set_dns(&default_device, new_dns).map_err(anyhow::Error::from),\n            };\n            if let Err(e) = result.context(\"failed to set system dns\") {\n                *previous_dns = previous_dns_clone;\n                return Err(e);\n            }\n        }\n        Ok(())\n    }\n}\n\n// TODO: support system path search via a config or flag\n// FIXME: move this fn to nyanpasu-utils\n/// Search the binary path of the core: Data Dir -> Sidecar Dir\npub fn find_binary_path(core_type: &nyanpasu_utils::core::CoreType) -> std::io::Result<PathBuf> {\n    let data_dir = dirs::app_data_dir()\n        .map_err(|err| std::io::Error::new(std::io::ErrorKind::NotFound, err.to_string()))?;\n    let binary_path = data_dir.join(core_type.get_executable_name());\n    if binary_path.exists() {\n        return Ok(binary_path);\n    }\n    let app_dir = dirs::app_install_dir()\n        .map_err(|err| std::io::Error::new(std::io::ErrorKind::NotFound, err.to_string()))?;\n    let binary_path = app_dir.join(core_type.get_executable_name());\n    if binary_path.exists() {\n        return Ok(binary_path);\n    }\n    Err(std::io::Error::new(\n        std::io::ErrorKind::NotFound,\n        format!(\"{} not found\", core_type.get_executable_name()),\n    ))\n}\n"
  },
  {
    "path": "backend/tauri/src/core/clash/mod.rs",
    "content": "use backon::ExponentialBuilder;\nuse once_cell::sync::Lazy;\nuse serde::{Deserialize, Serialize};\nuse specta::Type;\nuse tauri_specta::Event;\n\npub mod api;\npub mod core;\npub mod proxies;\npub mod ws;\n\npub static CLASH_API_DEFAULT_BACKOFF_STRATEGY: Lazy<ExponentialBuilder> = Lazy::new(|| {\n    ExponentialBuilder::default()\n        .with_min_delay(std::time::Duration::from_millis(50))\n        .with_max_delay(std::time::Duration::from_secs(5))\n        .with_max_times(5)\n});\n\n#[derive(Serialize, Deserialize, Debug, Clone, Type, Event)]\npub struct ClashConnectionsEvent(pub ws::ClashConnectionsConnectorEvent);\n\npub fn setup<R: tauri::Runtime, M: tauri::Manager<R>>(manager: &M) -> anyhow::Result<()> {\n    let ws_connector = ws::ClashConnectionsConnector::new();\n    manager.manage(ws_connector.clone());\n    let app_handle = manager.app_handle().clone();\n\n    tauri::async_runtime::spawn(async move {\n        // TODO: refactor it while clash core manager use tauri event dispatcher to notify the core state changed\n        {\n            tokio::time::sleep(std::time::Duration::from_secs(10)).await;\n\n            // TODO: clash-rs ws authorization is not working\n            match ws_connector.start().await {\n                Ok(_) => {\n                    tracing::info!(\n                        \"ws_connector started successfully clash-rs may be errored here.\"\n                    );\n                }\n                // TODO: wait for clash-rs to fix\n                Err(e) => {\n                    tracing::error!(\"ws_connector failed to start: {:?}\", e);\n                }\n            }\n        }\n        let mut rx = ws_connector.subscribe();\n        while let Ok(event) = rx.recv().await {\n            ClashConnectionsEvent(event).emit(&app_handle).unwrap();\n        }\n    });\n    Ok(())\n}\n"
  },
  {
    "path": "backend/tauri/src/core/clash/proxies.rs",
    "content": "/// This module is used to manage the proxies for the Tauri application.\n/// It is used to provide the unite interface between tray and frontend.\n/// TODO: add a diff algorithm to reduce the data transfer, and the rerendering of the tray menu.\nuse super::{CLASH_API_DEFAULT_BACKOFF_STRATEGY, api};\nuse adler::adler32;\nuse anyhow::Result;\nuse backon::Retryable;\nuse indexmap::IndexMap;\nuse log::warn;\nuse parking_lot::RwLock;\nuse serde::{Deserialize, Serialize};\nuse specta::Type;\nuse std::sync::{Arc, OnceLock};\nuse tokio::{sync::broadcast, try_join};\nuse tracing_attributes::instrument;\n\n#[derive(Debug, Clone, Deserialize, Serialize, Default, Type)]\n#[serde(rename_all = \"camelCase\")]\npub struct ProxyGroupItem {\n    pub name: String,\n    pub r#type: String, // TODO: 考虑改成枚举\n    pub udp: bool,\n    pub history: Vec<api::ProxyItemHistory>,\n    pub all: Vec<api::ProxyItem>,\n    pub now: Option<String>, // 当前选中的代理\n    pub provider: Option<String>,\n    pub alive: Option<bool>, // Mihomo Or Premium Only\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub xudp: Option<bool>, // Mihomo Only\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub tfo: Option<bool>, // Mihomo Only\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub icon: Option<String>, // Mihomo Only\n    #[serde(default)]\n    pub hidden: bool, // Mihomo Only\n                             // extra: {}, // Mihomo Only\n}\n\nimpl From<api::ProxyItem> for ProxyGroupItem {\n    fn from(item: api::ProxyItem) -> Self {\n        let all = vec![];\n        ProxyGroupItem {\n            name: item.name,\n            r#type: item.r#type,\n            udp: item.udp,\n            history: item.history,\n            all,\n            now: item.now,\n            provider: item.provider,\n            alive: item.alive,\n            xudp: item.xudp,\n            tfo: item.tfo,\n            icon: item.icon,\n            hidden: item.hidden,\n        }\n    }\n}\n\n#[derive(Debug, Clone, Deserialize, Serialize, Default, Type)]\n#[serde(rename_all = \"camelCase\")]\npub struct Proxies {\n    pub global: ProxyGroupItem,\n    pub direct: api::ProxyItem,\n    pub groups: Vec<ProxyGroupItem>,\n    pub records: IndexMap<String, api::ProxyItem>,\n    pub proxies: Vec<api::ProxyItem>,\n}\n\nasync fn fetch_proxies() -> Result<(api::ProxiesRes, api::ProvidersProxiesRes)> {\n    try_join!(api::get_proxies(), api::get_providers_proxies())\n}\n\nimpl Proxies {\n    #[instrument]\n    pub async fn fetch() -> Result<Self> {\n        let (inner_proxies, providers_proxies) = fetch_proxies\n            .retry(*CLASH_API_DEFAULT_BACKOFF_STRATEGY)\n            .await?;\n        let inner_proxies = inner_proxies.proxies;\n        // 1. filter out the Http or File type provider proxies\n        let providers_proxies: IndexMap<String, api::ProxyProviderItem> = {\n            let records = providers_proxies.providers;\n            records\n                .into_iter()\n                .filter(|(_k, v)| {\n                    matches!(\n                        v.vehicle_type,\n                        api::VehicleType::Http | api::VehicleType::File\n                    )\n                })\n                .collect()\n        };\n\n        // 2. mapping provider => providerProxiesItem to name => ProxyItem\n        let mut provider_map = IndexMap::<String, api::ProxyItem>::new();\n        for (provider, record) in providers_proxies.iter() {\n            let name = record.name.clone();\n            let mut record: api::ProxyItem = record.clone().into();\n            record.provider = Some(provider.clone());\n            provider_map.insert(name, record);\n        }\n        let generate_item = |name: &str| {\n            if let Some(r) = inner_proxies.get(name) {\n                r.clone()\n            } else if let Some(r) = provider_map.get(name) {\n                r.clone()\n            } else {\n                api::ProxyItem {\n                    name: name.to_string(),\n                    r#type: \"Unknown\".to_string(),\n                    udp: false,\n                    history: vec![],\n                    ..Default::default()\n                }\n            }\n        };\n\n        let global = inner_proxies.get(\"GLOBAL\");\n        let direct = inner_proxies\n            .get(\"DIRECT\")\n            .ok_or(anyhow::anyhow!(\"DIRECT is missing in /proxies\"))?\n            .clone(); // It should be always exists\n        let reject = inner_proxies\n            .get(\"REJECT\")\n            .ok_or(anyhow::anyhow!(\"REJECT is missing in /proxies\"))?\n            .clone(); // It should be always exists\n\n        // 3. generate the proxies groups\n        let groups: Vec<ProxyGroupItem> = match global {\n            Some(api::ProxyItem { all: Some(all), .. }) => {\n                let all = all.clone();\n                all.into_iter()\n                    .filter(|name| {\n                        matches!(\n                            inner_proxies.get(name),\n                            Some(api::ProxyItem { all: Some(_), .. })\n                        )\n                    })\n                    .map(|name| {\n                        let item = inner_proxies\n                            .get(&name)\n                            .unwrap_or(&api::ProxyItem::default())\n                            .clone();\n                        let item_all = item.all.clone().unwrap_or_default();\n                        let mut item: ProxyGroupItem = item.into();\n                        item.all = item_all\n                            .into_iter()\n                            .map(|name| generate_item(&name))\n                            .collect();\n                        item\n                    })\n                    .collect()\n            }\n            _ => {\n                let mut groups: Vec<ProxyGroupItem> = inner_proxies\n                    .clone()\n                    .into_values()\n                    .filter(|v| v.name == \"GLOBAL\" && v.all.is_some())\n                    .map(|v| {\n                        let all = v.all.clone().unwrap_or_default();\n                        let mut item: ProxyGroupItem = v.clone().into();\n                        item.all = all.into_iter().map(|name| generate_item(&name)).collect();\n                        item\n                    })\n                    .collect();\n                groups.sort_by(|a, b| b.name.to_lowercase().cmp(&a.name.to_lowercase()));\n                groups\n            }\n        };\n\n        // 4. generate the proxies\n        let mut proxies: Vec<api::ProxyItem> = vec![direct.clone(), reject];\n        proxies.extend(inner_proxies.clone().into_values().filter(|v| {\n            matches!(v.name.as_str(), \"DIRECT\" | \"REJECT\")\n                && (v.all.is_none() || v.all.as_ref().unwrap().is_empty())\n        }));\n\n        // 5. generate the global\n        let global: Option<ProxyGroupItem> = global.map(|v| {\n            let all = v.all.clone().unwrap_or_default();\n            let mut item: ProxyGroupItem = v.clone().into();\n            item.all = all.into_iter().map(|name| generate_item(&name)).collect();\n            item\n        });\n\n        Ok(Proxies {\n            global: global.unwrap_or_default(),\n            direct,\n            groups,\n            records: inner_proxies,\n            proxies,\n        })\n    }\n}\n\npub struct ProxiesGuard {\n    inner: Proxies,\n    checksum: Option<u32>,\n    updated_at: u64,\n    sender: broadcast::Sender<()>,\n}\n\nimpl ProxiesGuard {\n    pub fn global() -> &'static Arc<RwLock<ProxiesGuard>> {\n        static PROXIES: OnceLock<Arc<RwLock<ProxiesGuard>>> = OnceLock::new();\n        PROXIES.get_or_init(|| {\n            let (tx, _) = broadcast::channel(5); // 默认提供 5 个消费位置，提供一定的缓冲\n            Arc::new(RwLock::new(ProxiesGuard {\n                checksum: None,\n                sender: tx,\n                inner: Proxies::default(),\n                updated_at: 0,\n            }))\n        })\n    }\n\n    pub fn get_receiver(&self) -> broadcast::Receiver<()> {\n        self.sender.subscribe()\n    }\n\n    pub fn replace(&mut self, proxies: Proxies, checksum: u32) {\n        let now = chrono::Utc::now().timestamp() as u64;\n        self.inner = proxies;\n        self.checksum = Some(checksum);\n        self.updated_at = now;\n\n        if let Err(e) = self.sender.send(()) {\n            warn!(\n                target: \"clash::proxies\",\n                \"send update signal failed: {e:?}\"\n            );\n        }\n    }\n\n    // pub async fn select_proxy(&mut self, group: &str, name: &str) -> Result<()> {\n    //     api::update_proxy(group, name).await?;\n    //     self.update().await?;\n    //     Ok(())\n    // }\n\n    pub fn inner(&self) -> &Proxies {\n        &self.inner\n    }\n\n    pub fn updated_at(&self) -> u64 {\n        self.updated_at\n    }\n\n    pub fn is_updated(&self) -> bool {\n        let now = chrono::Utc::now().timestamp() as u64;\n        now - self.updated_at <= 3\n    }\n}\n\npub trait ProxiesGuardExt {\n    async fn update(&self) -> Result<()>;\n    async fn select_proxy(&self, group: &str, name: &str) -> Result<()>;\n}\n\ntype ProxiesGuardSingleton = &'static Arc<RwLock<ProxiesGuard>>;\nimpl ProxiesGuardExt for ProxiesGuardSingleton {\n    async fn update(&self) -> Result<()> {\n        let proxies = Proxies::fetch().await?;\n        let buf = serde_json::to_string(&proxies)?;\n        let checksum = adler32(buf.as_bytes())?;\n        {\n            let reader = self.read();\n            if reader.checksum == Some(checksum) {\n                return Ok(());\n            }\n        }\n        let mut writer = self.write();\n        writer.replace(proxies, checksum);\n        Ok(())\n    }\n\n    async fn select_proxy(&self, group: &str, name: &str) -> Result<()> {\n        api::update_proxy(group, name).await?;\n        self.update().await?;\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "backend/tauri/src/core/clash/ws.rs",
    "content": "use std::{\n    future::Future,\n    ops::Deref,\n    sync::{Arc, atomic::Ordering},\n};\n\nuse anyhow::Context;\nuse atomic_enum::atomic_enum;\nuse backon::Retryable;\nuse futures_util::StreamExt;\nuse parking_lot::Mutex;\nuse serde::{Deserialize, Serialize};\nuse specta::Type;\nuse tokio::{sync::mpsc::Receiver, task::JoinHandle};\nuse tokio_tungstenite::{\n    connect_async,\n    tungstenite::{client::IntoClientRequest, handshake::client::Request, protocol::Message},\n};\n\nuse crate::log_err;\n\n#[tracing::instrument]\nasync fn connect_clash_server<T: serde::de::DeserializeOwned + Send + Sync + 'static>(\n    endpoint: Request,\n) -> anyhow::Result<Receiver<T>> {\n    let (stream, _) = connect_async(endpoint).await?;\n    let (_, mut read) = stream.split();\n    let (tx, rx) = tokio::sync::mpsc::channel(32);\n    tokio::spawn(async move {\n        while let Some(msg) = read.next().await {\n            match msg {\n                Ok(Message::Text(text)) => match serde_json::from_str(&text) {\n                    Ok(data) => {\n                        let _ = tx.send(data).await;\n                    }\n                    Err(e) => {\n                        tracing::error!(\"failed to deserialize json: {}\", e);\n                    }\n                },\n                Ok(Message::Binary(bin)) => match serde_json::from_slice(&bin) {\n                    Ok(data) => {\n                        let _ = tx.send(data).await;\n                    }\n                    Err(e) => {\n                        tracing::error!(\"failed to deserialize json: {}\", e);\n                    }\n                },\n                Ok(Message::Close(_)) => {\n                    tracing::info!(\"server closed connection\");\n                    break;\n                }\n                Err(e) => {\n                    tracing::error!(\"failed to read message: {}\", e);\n                }\n                _ => {}\n            }\n        }\n    });\n    Ok(rx)\n}\n\n#[derive(Debug, Clone, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct ClashConnectionsMessage {\n    download_total: u64,\n    upload_total: u64,\n    // other fields are omitted\n}\n\n#[derive(Debug, Clone, Default, Copy, Type, Serialize, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct ClashConnectionsInfo {\n    pub download_total: u64,\n    pub upload_total: u64,\n    pub download_speed: u64,\n    pub upload_speed: u64,\n}\n\n#[derive(Debug, Clone, Type, Serialize, Deserialize)]\n#[serde(rename_all = \"snake_case\")]\n#[serde(tag = \"kind\", content = \"data\")]\npub enum ClashConnectionsConnectorEvent {\n    StateChanged(ClashConnectionsConnectorState),\n    Update(ClashConnectionsInfo),\n}\n\n#[derive(PartialEq, Eq, Type, Serialize, Deserialize)]\n#[serde(rename_all = \"snake_case\")]\n#[atomic_enum]\npub enum ClashConnectionsConnectorState {\n    Disconnected,\n    Connecting,\n    Connected,\n}\n\npub struct ClashConnectionsConnectorInner {\n    state: AtomicClashConnectionsConnectorState,\n    connection_handler: Mutex<Option<JoinHandle<()>>>,\n    broadcast_tx: tokio::sync::broadcast::Sender<ClashConnectionsConnectorEvent>,\n    info: Mutex<ClashConnectionsInfo>,\n}\n\n// TODO:\n#[derive(Clone)]\npub struct ClashConnectionsConnector {\n    inner: Arc<ClashConnectionsConnectorInner>,\n}\n\nimpl Deref for ClashConnectionsConnector {\n    type Target = ClashConnectionsConnectorInner;\n\n    fn deref(&self) -> &Self::Target {\n        &self.inner\n    }\n}\n\nimpl ClashConnectionsConnector {\n    pub fn new() -> Self {\n        Self {\n            inner: Arc::new(ClashConnectionsConnectorInner::new()),\n        }\n    }\n\n    pub fn endpoint() -> anyhow::Result<Request> {\n        let (server, secret) = {\n            let info = crate::Config::clash().data().get_client_info();\n            (info.server, info.secret)\n        };\n        let url = format!(\"ws://{server}/connections\");\n        let mut request = url\n            .into_client_request()\n            .context(\"failed to create client request\")?;\n        if let Some(secret) = secret {\n            request.headers_mut().insert(\n                \"Authorization\",\n                format!(\"Bearer {secret}\")\n                    .parse()\n                    .context(\"failed to create header value\")?,\n            );\n        }\n        Ok(request)\n    }\n\n    #[allow(clippy::manual_async_fn)]\n    // FIXME: move to async fn while rust new solver got merged\n    // ref: https://github.com/rust-lang/rust/issues/123072\n    fn start_internal(&self) -> impl Future<Output = anyhow::Result<()>> + Send + use<'_> {\n        async {\n            self.dispatch_state_changed(ClashConnectionsConnectorState::Connecting);\n            let endpoint = Self::endpoint().context(\"failed to create endpoint\")?;\n            log::debug!(\"connecting to clash connections ws server: {endpoint:?}\");\n            let mut rx = connect_clash_server::<ClashConnectionsMessage>(endpoint).await?;\n            self.dispatch_state_changed(ClashConnectionsConnectorState::Connected);\n            let this = self.clone();\n            let mut connection_handler = self.connection_handler.lock();\n            let handle = tokio::spawn(async move {\n                loop {\n                    match rx.recv().await {\n                        Some(msg) => {\n                            this.update(msg);\n                        }\n                        None => {\n                            tracing::info!(\"clash ws server closed connection, trying to restart\");\n                            // The connection was closed, let's restart the connector\n                            this.dispatch_state_changed(\n                                ClashConnectionsConnectorState::Disconnected,\n                            );\n                            tokio::spawn(async move {\n                                let restart = async || this.restart().await;\n                                log_err!(\n                                    restart\n                                        .retry(backon::ExponentialBuilder::default())\n                                        .sleep(tokio::time::sleep)\n                                        .await\n                                        .context(\"failed to restart clash connections\")\n                                );\n                            });\n                            break;\n                        }\n                    }\n                }\n            });\n            *connection_handler = Some(handle);\n            Ok(())\n        }\n    }\n\n    pub async fn start(&self) -> anyhow::Result<()> {\n        self.start_internal().await.inspect_err(|_| {\n            self.dispatch_state_changed(ClashConnectionsConnectorState::Disconnected);\n        })\n    }\n\n    pub async fn restart(&self) -> anyhow::Result<()> {\n        self.stop().await;\n        self.start().await\n    }\n}\n\nimpl ClashConnectionsConnectorInner {\n    pub fn new() -> Self {\n        Self {\n            state: AtomicClashConnectionsConnectorState::new(\n                ClashConnectionsConnectorState::Disconnected,\n            ),\n            connection_handler: Mutex::new(None),\n            broadcast_tx: tokio::sync::broadcast::channel(5).0,\n            info: Mutex::new(ClashConnectionsInfo::default()),\n        }\n    }\n\n    pub fn state(&self) -> ClashConnectionsConnectorState {\n        self.state.load(Ordering::Acquire)\n    }\n\n    fn dispatch_state_changed(&self, state: ClashConnectionsConnectorState) {\n        self.state.store(state, Ordering::Release);\n        // SAFETY: the failures only there no active receivers,\n        // so that the message will be dropped directly\n        let _ = self\n            .broadcast_tx\n            .send(ClashConnectionsConnectorEvent::StateChanged(state));\n    }\n\n    /// Subscribe to the ClashConnectionsConnectorEvent\n    pub fn subscribe(&self) -> tokio::sync::broadcast::Receiver<ClashConnectionsConnectorEvent> {\n        self.broadcast_tx.subscribe()\n    }\n\n    fn update(&self, msg: ClashConnectionsMessage) {\n        let mut info = self.info.lock();\n        let previous_download_total =\n            std::mem::replace(&mut info.download_total, msg.download_total);\n        let previous_upload_total = std::mem::replace(&mut info.upload_total, msg.upload_total);\n        info.download_speed = msg\n            .download_total\n            .checked_sub(previous_download_total)\n            .unwrap_or_default();\n        info.upload_speed = msg\n            .upload_total\n            .checked_sub(previous_upload_total)\n            .unwrap_or_default();\n\n        // SAFETY: the failures only there no active receivers,\n        // so that the message will be dropped directly\n        let _ = self\n            .broadcast_tx\n            .send(ClashConnectionsConnectorEvent::Update(*info));\n    }\n\n    pub async fn stop(&self) {\n        log::info!(\"stopping clash connections ws server\");\n        let handle = self.connection_handler.lock().take();\n        if let Some(handle) = handle {\n            handle.abort();\n            let _ = handle.await;\n        }\n        self.dispatch_state_changed(ClashConnectionsConnectorState::Disconnected);\n    }\n}\n\nimpl Drop for ClashConnectionsConnectorInner {\n    fn drop(&mut self) {\n        let cleanup = async move {\n            self.stop().await;\n        };\n        match tokio::runtime::Handle::try_current() {\n            Ok(_) => tokio::task::block_in_place(|| {\n                tauri::async_runtime::block_on(cleanup);\n            }),\n            Err(_) => {\n                tauri::async_runtime::block_on(cleanup);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "backend/tauri/src/core/connection_interruption.rs",
    "content": "use crate::{config::Config, core::clash::api};\nuse anyhow::Result;\nuse serde::{Deserialize, Serialize};\nuse specta::Type;\n\n#[derive(Debug, Clone, Deserialize, Serialize, Type)]\npub struct ConnectionInfo {\n    pub id: String,\n    pub chains: Vec<String>,\n}\n\n/// Connection interruption service that handles closing connections based on configuration settings\npub struct ConnectionInterruptionService;\n\nimpl ConnectionInterruptionService {\n    /// Interrupt connections when proxy changes\n    pub async fn on_proxy_change() -> Result<()> {\n        let config = Config::verge().data().clone();\n        let break_when = config.break_when_proxy_change.unwrap_or_default();\n\n        match break_when {\n            crate::config::nyanpasu::BreakWhenProxyChange::None => {\n                // Do nothing\n                Ok(())\n            }\n            crate::config::nyanpasu::BreakWhenProxyChange::Chain => {\n                // TODO: Implement chain-based connection interruption\n                // This would require tracking which connections use which proxy chains\n                // For now, we'll fall back to closing all connections\n                api::delete_connections(None).await\n            }\n            crate::config::nyanpasu::BreakWhenProxyChange::All => {\n                api::delete_connections(None).await\n            }\n        }\n    }\n\n    /// Interrupt connections when profile changes\n    pub async fn on_profile_change() -> Result<()> {\n        let config = Config::verge().data().clone();\n        let break_when = config.break_when_profile_change.unwrap_or_default();\n\n        if break_when {\n            api::delete_connections(None).await\n        } else {\n            // Do nothing\n            Ok(())\n        }\n    }\n\n    /// Interrupt connections when mode changes\n    pub async fn on_mode_change() -> Result<()> {\n        let config = Config::verge().data().clone();\n        let break_when = config.break_when_mode_change.unwrap_or_default();\n\n        if break_when {\n            api::delete_connections(None).await\n        } else {\n            // Do nothing\n            Ok(())\n        }\n    }\n\n    /// Interrupt all connections\n    pub async fn interrupt_all() -> Result<()> {\n        api::delete_connections(None).await\n    }\n\n    /// Interrupt connections based on proxy chain (not yet implemented)\n    pub async fn interrupt_by_chain(_chain: &[String]) -> Result<()> {\n        // TODO: Implement chain-based connection interruption\n        // This would require:\n        // 1. Getting the current connections from the Clash API\n        // 2. Filtering connections that use the specified proxy chain\n        // 3. Closing only those connections\n        // For now, we'll close all connections as a fallback\n        api::delete_connections(None).await\n    }\n}\n"
  },
  {
    "path": "backend/tauri/src/core/handle.rs",
    "content": "use super::tray::Tray;\nuse crate::log_err;\nuse anyhow::{Result, bail};\nuse once_cell::sync::OnceCell;\nuse parking_lot::Mutex;\nuse serde::{Deserialize, Serialize};\nuse std::sync::Arc;\nuse tauri::{AppHandle, Emitter, Manager, WebviewWindow, Wry};\n#[derive(Debug, Default, Clone)]\npub struct Handle {\n    pub app_handle: Arc<Mutex<Option<AppHandle>>>,\n}\n\n#[derive(Debug, Clone, Serialize, Deserialize)]\n#[serde(rename_all = \"snake_case\")]\npub enum StateChanged {\n    NyanpasuConfig,\n    ClashConfig,\n    Profiles,\n    Proxies,\n}\n\n#[derive(Debug, Clone, Serialize, Deserialize)]\n#[serde(rename_all = \"snake_case\")]\npub enum Message {\n    SetConfig(Result<(), String>),\n}\n\nconst STATE_CHANGED_URI: &str = \"nyanpasu://mutation\";\nconst NOTIFY_MESSAGE_URI: &str = \"nyanpasu://notice-message\";\n\nimpl Handle {\n    pub fn global() -> &'static Handle {\n        static HANDLE: OnceCell<Handle> = OnceCell::new();\n\n        HANDLE.get_or_init(|| Handle {\n            app_handle: Arc::new(Mutex::new(None)),\n        })\n    }\n\n    pub fn init(&self, app_handle: AppHandle) {\n        *self.app_handle.lock() = Some(app_handle);\n    }\n\n    pub fn get_window(&self) -> Option<WebviewWindow<Wry>> {\n        self.app_handle\n            .lock()\n            .as_ref()\n            .and_then(|a| a.get_webview_window(\"main\"))\n    }\n\n    pub fn refresh_clash() {\n        if let Some(window) = Self::global().get_window() {\n            log_err!(window.emit(STATE_CHANGED_URI, StateChanged::ClashConfig));\n        }\n    }\n\n    pub fn refresh_verge() {\n        if let Some(window) = Self::global().get_window() {\n            log_err!(window.emit(STATE_CHANGED_URI, StateChanged::NyanpasuConfig));\n        }\n    }\n\n    #[allow(unused)]\n    pub fn refresh_profiles() {\n        if let Some(window) = Self::global().get_window() {\n            log_err!(window.emit(STATE_CHANGED_URI, StateChanged::Profiles));\n        }\n    }\n\n    pub fn mutate_proxies() {\n        if let Some(window) = Self::global().get_window() {\n            log_err!(window.emit(STATE_CHANGED_URI, StateChanged::Proxies));\n        }\n    }\n\n    pub fn notice_message(message: &Message) {\n        if let Some(window) = Self::global().get_window() {\n            log_err!(window.emit(NOTIFY_MESSAGE_URI, message));\n        }\n    }\n\n    pub fn update_systray() -> Result<()> {\n        // let app_handle = Self::global().app_handle.lock();\n        // if app_handle.is_none() {\n        //     bail!(\"update_systray unhandled error\");\n        // }\n        // Tray::update_systray(app_handle.as_ref().unwrap())?;\n        Handle::emit(\"update_systray\", ())?;\n        Ok(())\n    }\n\n    /// update the system tray state\n    pub fn update_systray_part() -> Result<()> {\n        let app_handle = Self::global().app_handle.lock();\n        if app_handle.is_none() {\n            bail!(\"update_systray unhandled error\");\n        }\n        Tray::update_part(app_handle.as_ref().unwrap())?;\n        Ok(())\n    }\n\n    pub fn emit<S: Serialize + Clone>(event: &str, payload: S) -> Result<()> {\n        let app_handle = Self::global().app_handle.lock();\n        if app_handle.is_none() {\n            bail!(\"app_handle is not exist\");\n        }\n\n        app_handle.as_ref().unwrap().emit(event, payload)?;\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "backend/tauri/src/core/hotkey.rs",
    "content": "use crate::{config::Config, feat, log_err};\nuse anyhow::{Result, bail};\nuse once_cell::sync::OnceCell;\nuse parking_lot::Mutex;\nuse std::{collections::HashMap, sync::Arc};\nuse tauri::AppHandle;\n\nuse tauri_plugin_global_shortcut::{GlobalShortcutExt, Shortcut, ShortcutState};\n\npub struct Hotkey {\n    current: Arc<Mutex<Vec<String>>>, // 保存当前的热键设置\n\n    app_handle: Arc<Mutex<Option<AppHandle>>>,\n}\n// (hotkey, func)\ntype HotKeyOp<'a> = (&'a str, HotKeyOpType<'a>);\n\n#[derive(Debug)]\nenum HotKeyOpType<'a> {\n    #[allow(unused)]\n    Unbind(&'a str),\n    #[allow(unused)]\n    Change(&'a str, &'a str),\n    Bind(&'a str),\n}\n\nimpl Hotkey {\n    pub fn global() -> &'static Hotkey {\n        static HOTKEY: OnceCell<Hotkey> = OnceCell::new();\n\n        HOTKEY.get_or_init(|| Hotkey {\n            current: Arc::new(Mutex::new(Vec::new())),\n            app_handle: Arc::new(Mutex::new(None)),\n        })\n    }\n\n    pub fn init(&self, app_handle: AppHandle) -> Result<()> {\n        *self.app_handle.lock() = Some(app_handle);\n\n        let verge = Config::verge();\n\n        if let Some(hotkeys) = verge.latest().hotkeys.as_ref() {\n            for hotkey in hotkeys.iter() {\n                let mut iter = hotkey.split(',');\n                let func = iter.next();\n                let key = iter.next();\n\n                match (key, func) {\n                    (Some(key), Some(func)) => {\n                        log_err!(Self::check_key(key).and_then(|_| self.register(key, func)));\n                    }\n                    _ => {\n                        let key = key.unwrap_or(\"None\");\n                        let func = func.unwrap_or(\"None\");\n                        log::error!(target: \"app\", \"invalid hotkey `{key}`:`{func}`\");\n                    }\n                }\n            }\n            self.current.lock().clone_from(hotkeys);\n        }\n\n        Ok(())\n    }\n\n    /// 检查一个键是否合法\n    fn check_key(hotkey: &str) -> Result<()> {\n        // fix #287\n        // tauri的这几个方法全部有Result expect，会panic，先检测一遍避免挂了\n        if hotkey.parse::<Shortcut>().is_err() {\n            bail!(\"invalid hotkey `{hotkey}`\");\n        }\n        Ok(())\n    }\n\n    fn register(&self, hotkey: &str, func: &str) -> Result<()> {\n        let app_handle = self.app_handle.lock();\n        if app_handle.is_none() {\n            bail!(\"app handle is none\");\n        }\n        let manager = app_handle.as_ref().unwrap().global_shortcut();\n\n        if manager.is_registered(hotkey) {\n            manager.unregister(hotkey)?;\n        }\n\n        let f = match func.trim() {\n            \"open_or_close_dashboard\" => feat::toggle_dashboard,\n            \"clash_mode_rule\" => || feat::change_clash_mode(\"rule\".into()),\n            \"clash_mode_global\" => || feat::change_clash_mode(\"global\".into()),\n            \"clash_mode_direct\" => || feat::change_clash_mode(\"direct\".into()),\n            \"clash_mode_script\" => || feat::change_clash_mode(\"script\".into()),\n            \"toggle_system_proxy\" => feat::toggle_system_proxy,\n            \"enable_system_proxy\" => feat::enable_system_proxy,\n            \"disable_system_proxy\" => feat::disable_system_proxy,\n            \"toggle_tun_mode\" => feat::toggle_tun_mode,\n            \"enable_tun_mode\" => feat::enable_tun_mode,\n            \"disable_tun_mode\" => feat::disable_tun_mode,\n            _ => bail!(\"invalid function \\\"{func}\\\"\"),\n        };\n\n        manager.on_shortcut(hotkey, move |_app_handle, hotkey, ev| {\n            if let ShortcutState::Pressed = ev.state {\n                tracing::info!(\"hotkey pressed: {}\", hotkey);\n                f();\n            }\n        })?;\n\n        log::info!(target: \"app\", \"register hotkey {hotkey} {func}\");\n        Ok(())\n    }\n\n    fn unregister(&self, hotkey: &str) -> Result<()> {\n        let app_handle = self.app_handle.lock();\n        if app_handle.is_none() {\n            bail!(\"app handle is none\");\n        }\n        let manager = app_handle.as_ref().unwrap().global_shortcut();\n\n        manager.unregister(hotkey)?;\n        log::info!(target: \"app\", \"unregister hotkey {hotkey}\");\n        Ok(())\n    }\n\n    #[tracing::instrument(skip(self))]\n    pub fn update(&self, new_hotkeys: Vec<String>) -> Result<()> {\n        let mut current = self.current.lock();\n        let old_map = Self::get_map_from_vec(&current);\n        let new_map = Self::get_map_from_vec(&new_hotkeys);\n\n        let ops = Self::get_ops(old_map, new_map);\n\n        // 先检查一遍所有新的热键是不是可以用的\n        for (hotkey, op) in ops.iter() {\n            if matches!(op, HotKeyOpType::Bind(_) | HotKeyOpType::Change(_, _)) {\n                Self::check_key(hotkey)?\n            }\n        }\n\n        tracing::info!(\"hotkey update: {:?}\", ops);\n\n        for (hotkey, op) in ops.iter() {\n            match op {\n                HotKeyOpType::Unbind(_) => self.unregister(hotkey)?,\n                HotKeyOpType::Change(_, new_func) => {\n                    self.unregister(hotkey)?;\n                    self.register(hotkey, new_func)?;\n                }\n                HotKeyOpType::Bind(func) => self.register(hotkey, func)?,\n            }\n        }\n\n        *current = new_hotkeys;\n        Ok(())\n    }\n\n    fn get_map_from_vec(hotkeys: &[String]) -> HashMap<&str, &str> {\n        let mut map = HashMap::new();\n\n        hotkeys.iter().for_each(|hotkey| {\n            let mut iter = hotkey.split(',');\n            let func = iter.next();\n            let key = iter.next();\n\n            if func.is_some() && key.is_some() {\n                let func = func.unwrap().trim();\n                let key = key.unwrap().trim();\n                map.insert(key, func);\n            }\n        });\n        map\n    }\n\n    fn get_ops<'a>(\n        old_map: HashMap<&'a str, &'a str>,\n        new_map: HashMap<&'a str, &'a str>,\n    ) -> Vec<HotKeyOp<'a>> {\n        let mut list = Vec::<HotKeyOp<'a>>::new();\n        old_map.iter().for_each(|(key, func)| {\n            match new_map.get(key) {\n                Some(new_func) => {\n                    if new_func != func {\n                        list.push((*key, HotKeyOpType::Change(func, new_func)))\n                    }\n\n                    // 无变化，无需操作\n                }\n                None => {\n                    list.push((*key, HotKeyOpType::Unbind(func)));\n                }\n            }\n        });\n\n        new_map.iter().for_each(|(key, func)| {\n            if !old_map.contains_key(key) {\n                list.push((*key, HotKeyOpType::Bind(func)));\n            }\n        });\n        list\n    }\n}\n\nimpl Drop for Hotkey {\n    fn drop(&mut self) {\n        let app_handle = self.app_handle.lock();\n        if let Some(app_handle) = app_handle.as_ref() {\n            let manager = app_handle.global_shortcut();\n            if let Ok(()) = manager.unregister_all() {\n                log::info!(target: \"app\", \"unregister all hotkeys\");\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "backend/tauri/src/core/logger.rs",
    "content": "use once_cell::sync::OnceCell;\nuse parking_lot::Mutex;\nuse std::{collections::VecDeque, sync::Arc};\n\nconst LOGS_QUEUE_LEN: usize = 100;\n\npub struct Logger {\n    log_data: Arc<Mutex<VecDeque<String>>>,\n}\n\nimpl Logger {\n    pub fn global() -> &'static Logger {\n        static LOGGER: OnceCell<Logger> = OnceCell::new();\n\n        LOGGER.get_or_init(|| Logger {\n            log_data: Arc::new(Mutex::new(VecDeque::with_capacity(LOGS_QUEUE_LEN + 10))),\n        })\n    }\n\n    pub fn get_log(&self) -> VecDeque<String> {\n        self.log_data.lock().clone()\n    }\n\n    pub fn set_log(&self, text: String) {\n        let mut logs = self.log_data.lock();\n        if logs.len() > LOGS_QUEUE_LEN {\n            logs.pop_front();\n        }\n        logs.push_back(text);\n    }\n\n    pub fn clear_log(&self) {\n        let mut logs = self.log_data.lock();\n        logs.clear();\n    }\n}\n"
  },
  {
    "path": "backend/tauri/src/core/manager.rs",
    "content": "use std::borrow::Cow;\n\n/// 给clash内核的tun模式授权\n#[cfg(any(target_os = \"macos\", target_os = \"linux\"))]\npub fn grant_permission(core: &nyanpasu_utils::core::CoreType) -> anyhow::Result<()> {\n    use std::process::Command;\n\n    let path = crate::core::clash::core::find_binary_path(&core)\n        .map_err(|_| anyhow::anyhow!(\"clash core not found\"))?\n        .canonicalize()?\n        .to_string_lossy()\n        .to_string();\n\n    log::debug!(\"grant_permission path: {:?}\", path);\n\n    #[cfg(target_os = \"macos\")]\n    let output = {\n        // the path of clash /Applications/Clash Nyanpasu.app/Contents/MacOS/clash\n        // https://apple.stackexchange.com/questions/82967/problem-with-empty-spaces-when-executing-shell-commands-in-applescript\n        // let path = escape(&path);\n        let path = path.replace(' ', \"\\\\\\\\ \");\n        let shell = format!(\"chown root:admin {path}\\nchmod +sx {path}\");\n        let command = format!(r#\"do shell script \"{shell}\" with administrator privileges\"#);\n        Command::new(\"osascript\")\n            .args(vec![\"-e\", &command])\n            .output()?\n    };\n\n    #[cfg(target_os = \"linux\")]\n    let output = {\n        let path = path.replace(' ', \"\\\\ \"); // 避免路径中有空格\n        let shell = format!(\"setcap cap_net_bind_service,cap_net_admin=+ep {path}\");\n\n        let sudo = match Command::new(\"which\").arg(\"pkexec\").output() {\n            Ok(output) => {\n                if output.stdout.is_empty() {\n                    \"sudo\"\n                } else {\n                    \"pkexec\"\n                }\n            }\n            Err(_) => \"sudo\",\n        };\n\n        Command::new(sudo).arg(\"sh\").arg(\"-c\").arg(shell).output()?\n    };\n\n    if output.status.success() {\n        Ok(())\n    } else {\n        let stderr = std::str::from_utf8(&output.stderr).unwrap_or(\"\");\n        anyhow::bail!(\"{stderr}\");\n    }\n}\n\n#[allow(unused)]\npub fn escape(text: &str) -> Cow<'_, str> {\n    let bytes = text.as_bytes();\n\n    let mut owned = None;\n\n    for pos in 0..bytes.len() {\n        let special = match bytes[pos] {\n            b' ' => Some(b' '),\n            _ => None,\n        };\n        if let Some(s) = special {\n            if owned.is_none() {\n                owned = Some(bytes[0..pos].to_owned());\n            }\n            owned.as_mut().unwrap().push(b'\\\\');\n            owned.as_mut().unwrap().push(b'\\\\');\n            owned.as_mut().unwrap().push(s);\n        } else if let Some(owned) = owned.as_mut() {\n            owned.push(bytes[pos]);\n        }\n    }\n\n    if let Some(owned) = owned {\n        Cow::Owned(String::from_utf8(owned).unwrap())\n    } else {\n        Cow::Borrowed(std::str::from_utf8(bytes).unwrap())\n    }\n}\n"
  },
  {
    "path": "backend/tauri/src/core/migration/db.rs",
    "content": "use derive_builder::Builder;\nuse once_cell::sync::Lazy;\nuse semver::Version;\n/// A simple file based database for storing the migration status.\n///\nuse serde::{Deserialize, Serialize};\nuse std::{borrow::Cow, collections::HashMap, io::Write, path::PathBuf};\n\nuse super::MigrationState;\n\n/// A lockfile store the migrated version and the state.\n/// The lower version of the migration will be ignored.\nstatic MIGRATION_LOCK_FILE: Lazy<PathBuf> = Lazy::new(|| {\n    let mut path = crate::utils::dirs::app_config_dir().unwrap();\n    path.push(\"migration.lock\");\n    path\n});\n\n#[derive(Debug, Clone, Serialize, Deserialize, Builder)]\n#[builder(default)]\npub struct MigrationFile<'a> {\n    pub version: Cow<'a, Version>,\n    pub states: HashMap<Cow<'a, str>, MigrationState>,\n}\n\nimpl MigrationFileBuilder<'_> {\n    pub fn read_file(mut self) -> Self {\n        let content = std::fs::read_to_string(&*MIGRATION_LOCK_FILE).ok();\n        if let Some(content) = content {\n            let file: Option<MigrationFile> = serde_yaml::from_str(&content).ok();\n            if let Some(file) = file {\n                self.version = Some(file.version);\n                self.states = Some(file.states);\n            }\n        }\n        self\n    }\n}\n\nimpl Default for MigrationFile<'_> {\n    fn default() -> Self {\n        // since 1.6.0, we have introduced the migration system. so the last version of 1.5.x is 1.5.1.\n        let ver = Version::parse(\"1.5.1\").unwrap();\n        Self {\n            version: Cow::Owned(ver),\n            states: HashMap::new(),\n        }\n    }\n}\n\nimpl<'a> MigrationFile<'a> {\n    /// Create or Truncate the lock file and write the content.\n    pub fn write_file(&self) -> Result<(), std::io::Error> {\n        let content = serde_yaml::to_string(self).map_err(|e| {\n            log::error!(\"Failed to serialize the migration file: {e}\");\n            std::io::Error::other(e)\n        })?;\n        let mut file = std::fs::OpenOptions::new()\n            .write(true)\n            .create(true)\n            .truncate(true)\n            .open(&*MIGRATION_LOCK_FILE)?;\n        file.write(\n            \"# This file is generated by the migration system, do not edit it manually.\\n\"\n                .as_bytes(),\n        )\n        .map_err(|e| {\n            log::error!(\"Failed to write the migration file: {e}\");\n            e\n        })?;\n        file.write_all(content.as_bytes())\n    }\n\n    pub fn get_state(&self, name: &str) -> Option<MigrationState> {\n        self.states.get(name).copied()\n    }\n\n    pub fn set_state(&mut self, name: Cow<'a, str>, state: MigrationState) {\n        self.states.insert(name, state);\n    }\n}\n"
  },
  {
    "path": "backend/tauri/src/core/migration/mod.rs",
    "content": "#![allow(dead_code)]\n/// A migration mod indicates the migration of the old version to the new version.\n/// Because this runner run at the start of the app, it will use eprintln or println to print the migration log.\n///\n///\nuse dyn_clone::{DynClone, clone_trait_object};\nuse semver::Version;\nuse std::{borrow::Cow, cell::RefCell};\n\nmod db;\npub mod units;\n\n#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]\n#[serde(rename_all = \"snake_case\")]\npub enum MigrationState {\n    /// The migration is pending.\n    NotStarted,\n    /// The migration is in progress.\n    InProgress,\n    /// The migration is completed.\n    Completed,\n    /// The migration is failed.\n    Failed,\n}\n\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\npub enum MigrationAdvice {\n    /// The migration is required to run.\n    Pending,\n    /// The migration is ignored.\n    Ignored,\n    /// The migration has been run.\n    Done,\n}\n\nimpl std::fmt::Display for MigrationAdvice {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        match self {\n            MigrationAdvice::Pending => write!(f, \"Pending\"),\n            MigrationAdvice::Ignored => write!(f, \"Ignored\"),\n            MigrationAdvice::Done => write!(f, \"Done\"),\n        }\n    }\n}\n\n#[derive(Debug, Clone)]\npub enum Unit<'a, T>\nwhere\n    T: Clone + Migration<'a> + Send + Sync,\n{\n    /// A List of migrations, it should be used to wrap a list of migrations in a single version.\n    /// Although the fn signature is T generic, it should use a Vec<DynMigration> as the input.\n    Batch(Cow<'a, [T]>),\n    Single(Cow<'a, T>),\n}\n\nimpl<'a, T> From<T> for Unit<'a, T>\nwhere\n    T: Clone + Migration<'a> + Send + Sync,\n{\n    fn from(item: T) -> Self {\n        Unit::Single(Cow::Owned(item))\n    }\n}\n\nimpl<'a, T> From<&'a T> for Unit<'a, T>\nwhere\n    T: Clone + Migration<'a> + Send + Sync,\n{\n    fn from(item: &'a T) -> Self {\n        Unit::Single(Cow::Borrowed(item))\n    }\n}\n\nimpl<'a, T> From<&'a [T]> for Unit<'a, T>\nwhere\n    T: Clone + Migration<'a> + Send + Sync,\n{\n    fn from(list: &'a [T]) -> Self {\n        Unit::Batch(Cow::Borrowed(list))\n    }\n}\n\nimpl<'a, T> From<Vec<T>> for Unit<'a, T>\nwhere\n    T: Clone + Migration<'a> + Send + Sync,\n{\n    fn from(list: Vec<T>) -> Self {\n        Unit::Batch(Cow::Owned(list))\n    }\n}\n\ntype DynMigration<'a> = Box<dyn Migration<'a> + Send + Sync + 'a>;\n\npub trait Migration<'a>: DynClone {\n    /// A version field to indicate the version of the migration.\n    /// It used to compare with the current version to determine whether the migration is needed.\n    fn version(&self) -> &'a Version;\n\n    /// A name field to indicate the name of the migration.\n    fn name(&self) -> Cow<'a, str>;\n\n    fn migrate(&self) -> std::io::Result<()> {\n        unimplemented!()\n    }\n\n    fn discard(&self) -> std::io::Result<()> {\n        Ok(())\n    }\n}\n\nclone_trait_object!(Migration<'_>);\n\npub trait MigrationExt<'a>: Migration<'a>\nwhere\n    Self: Sized + 'static + Send + Sync,\n{\n    fn boxed(self) -> DynMigration<'a> {\n        Box::new(self) as DynMigration\n    }\n}\n\nimpl<'a, T> MigrationExt<'a> for T where T: Sized + 'static + Migration<'a> + Send + Sync {}\n\nimpl<'a, T> Migration<'a> for Unit<'a, T>\nwhere\n    T: Clone + Migration<'a> + Send + Sync,\n{\n    fn version(&self) -> &'a Version {\n        match self {\n            Unit::Single(item) => item.version(),\n            Unit::Batch(list) => list.first().unwrap().version(),\n        }\n    }\n\n    fn name(&self) -> Cow<'a, str> {\n        match self {\n            Unit::Single(item) => item.name(),\n            Unit::Batch(list) => Cow::Owned(format!(\n                \"{} migrations for v{}\",\n                list.len(),\n                list.first().unwrap().version()\n            )),\n        }\n    }\n\n    fn migrate(&self) -> std::io::Result<()> {\n        unimplemented!(\"Batch migrations should be handled by the runner.\")\n    }\n}\n\nimpl<'a> Migration<'a> for DynMigration<'a> {\n    fn version(&self) -> &'a Version {\n        self.as_ref().version()\n    }\n\n    fn name(&self) -> Cow<'a, str> {\n        self.as_ref().name()\n    }\n\n    fn migrate(&self) -> std::io::Result<()> {\n        self.as_ref().migrate()\n    }\n}\n\n#[derive(Debug)]\npub struct Runner<'a> {\n    pub current_version: Cow<'a, Version>,\n    skip_advice: bool,\n    store: RefCell<db::MigrationFile<'a>>,\n}\n\npub struct DropGuard<'a>(Runner<'a>);\n\nimpl<'a> std::ops::Deref for DropGuard<'a> {\n    type Target = Runner<'a>;\n\n    fn deref(&self) -> &Self::Target {\n        &self.0\n    }\n}\n\nimpl<'a> std::ops::DerefMut for DropGuard<'a> {\n    fn deref_mut(&mut self) -> &mut Self::Target {\n        &mut self.0\n    }\n}\n\nimpl Default for Runner<'_> {\n    fn default() -> Self {\n        let ver = Version::parse(crate::consts::BUILD_INFO.pkg_version).unwrap();\n        let file = db::MigrationFileBuilder::default()\n            .read_file()\n            .build()\n            .unwrap();\n        Self {\n            current_version: Cow::Owned(ver),\n            skip_advice: false,\n            store: RefCell::new(file),\n        }\n    }\n}\n\nimpl Drop for DropGuard<'_> {\n    fn drop(&mut self) {\n        let mut store = self.store.take();\n        store.version = Cow::Borrowed(&self.0.current_version);\n        store.write_file().unwrap();\n    }\n}\n\nimpl Runner<'_> {\n    pub fn new_with_skip_advice() -> Self {\n        let ver = Version::parse(crate::consts::BUILD_INFO.pkg_version).unwrap();\n        let file = db::MigrationFileBuilder::default()\n            .read_file()\n            .build()\n            .unwrap();\n        Self {\n            skip_advice: true,\n            current_version: Cow::Owned(ver),\n            store: RefCell::new(file),\n        }\n    }\n\n    pub fn advice_migration<'a, T>(&self, migration: &T) -> MigrationAdvice\n    where\n        T: Clone + Migration<'a> + Send + Sync,\n    {\n        let migration_ver = migration.version();\n        let store = self.store.borrow();\n        if migration_ver >= &store.version {\n            // Judge the migration is run or not.\n            if let Some(state) = store.states.get(&migration.name()) {\n                match state {\n                    MigrationState::Completed => MigrationAdvice::Done,\n                    MigrationState::Failed => MigrationAdvice::Pending,\n                    _ => MigrationAdvice::Ignored,\n                }\n            } else {\n                MigrationAdvice::Pending\n            }\n        } else {\n            MigrationAdvice::Ignored\n        }\n    }\n\n    pub fn advice_unit<'a, T>(&self, unit: &Unit<'a, T>) -> MigrationAdvice\n    where\n        T: Clone + Migration<'a> + Send + Sync,\n    {\n        match unit {\n            Unit::Single(item) => self.advice_migration(item.as_ref()),\n            Unit::Batch(list) => {\n                let mut advice = MigrationAdvice::Ignored;\n                for item in list.iter() {\n                    let item_advice = self.advice_migration(item);\n                    if item_advice == MigrationAdvice::Pending {\n                        advice = MigrationAdvice::Pending;\n                        break;\n                    } else if item_advice == MigrationAdvice::Done {\n                        advice = MigrationAdvice::Done;\n                    }\n                }\n                advice\n            }\n        }\n    }\n\n    pub fn run_migration<'a, T>(&self, migration: &T) -> std::io::Result<()>\n    where\n        T: Clone + Migration<'a> + Send + Sync,\n    {\n        println!(\"Running migration: {}\", migration.name());\n        let advice = self.advice_migration(migration);\n        println!(\"Advice: {advice:?}\");\n        if matches!(advice, MigrationAdvice::Ignored | MigrationAdvice::Done) {\n            return Ok(());\n        }\n        let name = migration.name();\n        let mut store = self.store.borrow_mut();\n        match migration.migrate() {\n            Ok(_) => {\n                println!(\"Migration {name} completed.\");\n                store.set_state(Cow::Owned(name.to_string()), MigrationState::Completed);\n                Ok(())\n            }\n            Err(e) => {\n                eprintln!(\"Migration {name} failed: {e}; trying to discard changes\");\n                match migration.discard() {\n                    Ok(_) => {\n                        eprintln!(\"Migration {name} discarded.\");\n                    }\n                    Err(e) => {\n                        eprintln!(\"Migration {name} discard failed: {e}\");\n                    }\n                }\n                store.set_state(Cow::Owned(name.to_string()), MigrationState::Failed);\n                Err(e)\n            }\n        }\n    }\n\n    pub fn run_unit<'a, T>(&self, unit: &Unit<'a, T>) -> std::io::Result<()>\n    where\n        T: Clone + Migration<'a> + Send + Sync,\n    {\n        println!(\"Running unit: {}\", unit.name());\n        match unit {\n            Unit::Single(item) => self.run_migration(item.as_ref()),\n            Unit::Batch(list) => {\n                for item in list.iter() {\n                    self.run_migration(item)?;\n                }\n                Ok(())\n            }\n        }\n    }\n\n    pub fn run_units_up_to_version(&self, to_ver: &Version) -> std::io::Result<()> {\n        println!(\"Running units up to version: {to_ver}\");\n        let version = {\n            let store = self.store.borrow();\n            store.version.clone()\n        };\n        let units = units::UNITS\n            .iter()\n            .filter(|(ver, _)| **ver >= &version && **ver <= to_ver);\n        for (_, unit) in units {\n            self.run_unit(unit)?;\n        }\n        Ok(())\n    }\n\n    pub fn run_upcoming_units(&self) -> std::io::Result<()> {\n        println!(\n            \"Running all upcoming units. It is supposed to run in Nightly build. If you see this message in Stable channel, report it in Github Issues Tracker please.\"\n        );\n        let version = {\n            let store = self.store.borrow();\n            store.version.clone()\n        };\n        let units = units::UNITS.iter().filter(|(ver, _)| **ver >= &version);\n\n        for (_, unit) in units {\n            self.run_unit(unit)?;\n        }\n        Ok(())\n    }\n}\n\nimpl<'a> Runner<'a> {\n    pub fn drop_guard(self) -> DropGuard<'a> {\n        DropGuard(self)\n    }\n}\n"
  },
  {
    "path": "backend/tauri/src/core/migration/units/mod.rs",
    "content": "use super::{DynMigration, Migration, Unit};\nuse once_cell::sync::Lazy;\nuse semver::Version;\nuse std::{borrow::Cow, collections::HashMap};\n\nmod unit_160;\nmod unit_200;\n\npub static UNITS: Lazy<HashMap<&'static Version, Unit<'static, DynMigration>>> = Lazy::new(|| {\n    let mut units: HashMap<&'static Version, Unit<'static, DynMigration>> = HashMap::new();\n    let unit = Unit::Batch(Cow::Borrowed(&unit_160::UNITS));\n    units.insert(unit.version(), unit);\n    let unit = Unit::Batch(Cow::Borrowed(&unit_200::UNITS));\n    units.insert(unit.version(), unit);\n    units\n});\n\npub fn find_migration(name: &str) -> Option<Cow<'static, DynMigration<'static>>> {\n    for unit in UNITS.values() {\n        match unit {\n            Unit::Batch(units) => {\n                for unit in units.iter() {\n                    if unit.name() == name {\n                        return Some(Cow::Borrowed(unit));\n                    }\n                }\n            }\n            Unit::Single(unit) => {\n                if unit.name() == name {\n                    return Some(Cow::Borrowed(unit));\n                }\n            }\n        }\n    }\n    None\n}\n\npub fn get_migrations() -> Vec<Cow<'static, DynMigration<'static>>> {\n    let mut migrations = Vec::new();\n    for unit in UNITS.values() {\n        match unit {\n            Unit::Batch(units) => {\n                for unit in units.iter() {\n                    migrations.push(Cow::Borrowed(unit));\n                }\n            }\n            Unit::Single(unit) => {\n                migrations.push(Cow::Borrowed(unit));\n            }\n        }\n    }\n    migrations\n}\n"
  },
  {
    "path": "backend/tauri/src/core/migration/units/unit_160.rs",
    "content": "use std::borrow::Cow;\n\nuse once_cell::sync::Lazy;\nuse serde_yaml::{\n    Mapping,\n    value::{Tag, TaggedValue},\n};\n\nuse crate::{\n    config::RUNTIME_CONFIG,\n    core::migration::{DynMigration, Migration, MigrationExt},\n};\n\npub static UNITS: Lazy<Vec<DynMigration>> = Lazy::new(|| {\n    vec![\n        MigrateAppHomeDir.boxed(),\n        MigrateProxiesSelectorMode.boxed(),\n        MigrateScriptProfileType.boxed(),\n    ]\n});\n\npub static VERSION: Lazy<semver::Version> = Lazy::new(|| semver::Version::parse(\"1.6.0\").unwrap());\n\n#[derive(Debug, Clone)]\npub struct MigrateAppHomeDir;\n\nimpl<'a> Migration<'a> for MigrateAppHomeDir {\n    fn name(&self) -> std::borrow::Cow<'a, str> {\n        std::borrow::Cow::Borrowed(\"Split App Home Dir to Config and Data\")\n    }\n\n    fn version(&self) -> &'a semver::Version {\n        &VERSION\n    }\n\n    // Allow deprecated because we are moving deprecated files to new locations\n    #[allow(deprecated)]\n    fn migrate(&self) -> std::io::Result<()> {\n        let home_dir = crate::utils::dirs::app_home_dir().unwrap();\n        if !home_dir.exists() {\n            println!(\"Home dir not found, skipping migration\");\n            return Ok(());\n        }\n\n        // create the app config and data dir\n        println!(\"Creating app config and data dir\");\n        let app_config_dir = crate::utils::dirs::app_config_dir().unwrap();\n        if !app_config_dir.exists() {\n            std::fs::create_dir_all(&app_config_dir)\n                .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;\n        }\n        let app_data_dir = crate::utils::dirs::app_data_dir().unwrap();\n        if !app_data_dir.exists() {\n            std::fs::create_dir_all(&app_data_dir)\n                .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;\n        }\n\n        // move the config files to the new config dir\n        let file_opts = fs_extra::file::CopyOptions::default().skip_exist(true);\n        let dir_opts = fs_extra::dir::CopyOptions::default()\n            .skip_exist(true)\n            .content_only(true);\n\n        // move clash runtime config\n        let path = home_dir.join(\"clash-verge.yaml\");\n        if path.exists() {\n            println!(\"Moving clash-verge.yaml to config dir\");\n            fs_extra::file::move_file(path, app_config_dir.join(RUNTIME_CONFIG), &file_opts)\n                .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;\n        }\n        // move clash guard overrides\n        let path = home_dir.join(\"config.yaml\");\n        if path.exists() {\n            println!(\"Moving config.yaml to config dir\");\n            fs_extra::file::move_file(\n                path,\n                crate::utils::dirs::clash_guard_overrides_path().unwrap(),\n                &file_opts,\n            )\n            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;\n        }\n        // move nyanpasu config\n        let path = home_dir.join(\"verge.yaml\");\n        if path.exists() {\n            println!(\"Moving verge.yaml to config dir\");\n            fs_extra::file::move_file(\n                path,\n                crate::utils::dirs::app_config_dir()\n                    .unwrap()\n                    .join(crate::utils::dirs::NYANPASU_CONFIG),\n                &file_opts,\n            )\n            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;\n        }\n\n        // if app config dir is not set by registry, move the files and dirs to data dir\n        if home_dir != app_config_dir {\n            // move profiles.yaml\n            let path = home_dir.join(\"profiles.yaml\");\n            if path.exists() {\n                println!(\"Moving profiles.yaml to profiles dir\");\n                fs_extra::file::move_file(path, app_config_dir.join(\"profiles.yaml\"), &file_opts)\n                    .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;\n            }\n            // move profiles dir\n            let path = home_dir.join(\"profiles\");\n            if path.exists() {\n                println!(\"Moving profiles dir to profiles dir\");\n                fs_extra::dir::move_dir(\n                    path,\n                    crate::utils::dirs::app_profiles_dir().unwrap(),\n                    &dir_opts,\n                )\n                .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;\n            }\n            // move other files and dirs to data dir\n            println!(\"Moving other files and dirs to data dir\");\n            fs_extra::dir::move_dir(home_dir, app_data_dir, &dir_opts)\n                .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;\n        }\n        println!(\"Migration completed\");\n        Ok(())\n    }\n\n    #[allow(deprecated)]\n    fn discard(&self) -> std::io::Result<()> {\n        let home_dir = crate::utils::dirs::app_home_dir().unwrap();\n        let app_config_dir = crate::utils::dirs::app_config_dir().unwrap();\n        let app_data_dir = crate::utils::dirs::app_data_dir().unwrap();\n        if !home_dir.exists() {\n            std::fs::create_dir_all(&home_dir)\n                .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;\n        }\n        let file_opts = fs_extra::file::CopyOptions::default().skip_exist(true);\n        let dir_opts = fs_extra::dir::CopyOptions::default()\n            .skip_exist(true)\n            .content_only(true);\n        if home_dir != app_config_dir {\n            // move profiles.yaml\n            let path = app_config_dir.join(\"profiles.yaml\");\n            if path.exists() {\n                println!(\"Moving profiles.yaml to home dir\");\n                fs_extra::file::move_file(path, home_dir.join(\"profiles.yaml\"), &file_opts)\n                    .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;\n            }\n            // move profiles dir\n            let path = crate::utils::dirs::app_profiles_dir().unwrap();\n            if path.exists() {\n                println!(\"Moving profiles dir to home dir\");\n                fs_extra::dir::move_dir(path, home_dir.join(\"profiles\"), &dir_opts)\n                    .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;\n            }\n            // move other files and dirs to home dir\n            println!(\"Moving other files and dirs to home dir\");\n            fs_extra::dir::move_dir(app_data_dir, &home_dir, &dir_opts)\n                .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;\n        }\n        // move nyanpasu config\n        let path = app_config_dir.join(crate::utils::dirs::NYANPASU_CONFIG);\n        if path.exists() {\n            println!(\"Moving verge.yaml to home dir\");\n            fs_extra::file::move_file(path, home_dir.join(\"verge.yaml\"), &file_opts)\n                .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;\n        }\n        // move clash guard overrides\n        let path = crate::utils::dirs::clash_guard_overrides_path().unwrap();\n        if path.exists() {\n            println!(\"Moving config.yaml to home dir\");\n            fs_extra::file::move_file(path, home_dir.join(\"config.yaml\"), &file_opts)\n                .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;\n        }\n        // move clash runtime config\n        let path = app_config_dir.join(RUNTIME_CONFIG);\n        if path.exists() {\n            println!(\"Moving clash-verge.yaml to home dir\");\n            fs_extra::file::move_file(path, home_dir.join(\"clash-verge.yaml\"), &file_opts)\n                .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;\n        }\n        println!(\"Migration discarded\");\n        Ok(())\n    }\n}\n\n#[derive(Debug, Clone)]\npub struct MigrateProxiesSelectorMode;\nimpl<'a> Migration<'a> for MigrateProxiesSelectorMode {\n    fn version(&self) -> &'a semver::Version {\n        &VERSION\n    }\n\n    fn name(&self) -> std::borrow::Cow<'a, str> {\n        Cow::Borrowed(\"Migrate Proxies Selector Mode\")\n    }\n\n    fn migrate(&self) -> std::io::Result<()> {\n        let config_path = crate::utils::dirs::nyanpasu_config_path().unwrap();\n        if !config_path.exists() {\n            println!(\"Config file not found, skipping migration\");\n            return Ok(());\n        }\n        println!(\"parse config file...\");\n        let config = std::fs::read_to_string(&config_path)\n            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;\n        let mut config: Mapping = serde_yaml::from_str(&config)\n            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;\n        let mode = config.get_mut(\"clash_tray_selector\");\n        match mode {\n            None => {\n                println!(\"clash_tray_selector not found, skipping migration\");\n                return Ok(());\n            }\n            Some(mode) => {\n                if mode.is_bool() {\n                    println!(\"detected old mode, migrating...\");\n                    let value = mode.as_bool().unwrap();\n                    let value = if value { \"normal\" } else { \"hidden\" };\n                    *mode = serde_yaml::Value::from(value);\n                    println!(\"write config file...\");\n                    let config = serde_yaml::to_string(&config)\n                        .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;\n                    std::fs::write(&config_path, config)?;\n                }\n                println!(\"Migration completed\");\n            }\n        }\n        Ok(())\n    }\n\n    fn discard(&self) -> std::io::Result<()> {\n        let config_path = crate::utils::dirs::nyanpasu_config_path().unwrap();\n        if !config_path.exists() {\n            println!(\"Config file not found, skipping migration\");\n            return Ok(());\n        }\n        println!(\"parse config file...\");\n        let config = std::fs::read_to_string(&config_path)\n            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;\n        let mut config: Mapping = serde_yaml::from_str(&config)\n            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;\n        let mode = config.get_mut(\"clash_tray_selector\");\n        match mode {\n            None => {\n                println!(\"clash_tray_selector not found, skipping migration\");\n                return Ok(());\n            }\n            Some(mode) => {\n                if mode.is_string() {\n                    println!(\"detected new mode, migrating...\");\n                    let value = mode.as_str().unwrap();\n                    let value = value == \"normal\";\n                    *mode = serde_yaml::Value::from(value);\n                    println!(\"write config file...\");\n                    let config = serde_yaml::to_string(&config)\n                        .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;\n                    std::fs::write(&config_path, config)?;\n                }\n                println!(\"Migration discarded\");\n            }\n        }\n        Ok(())\n    }\n}\n\n#[derive(Debug, Clone)]\npub struct MigrateScriptProfileType;\n\nimpl<'a> Migration<'a> for MigrateScriptProfileType {\n    fn version(&self) -> &'a semver::Version {\n        &VERSION\n    }\n\n    fn name(&self) -> Cow<'a, str> {\n        Cow::Borrowed(\"Migrate Script Profile Type\")\n    }\n\n    fn migrate(&self) -> std::io::Result<()> {\n        let profiles_path = crate::utils::dirs::profiles_path()\n            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e.to_string()))?;\n        if !profiles_path.exists() {\n            println!(\"Profiles dir not found, skipping migration\");\n            return Ok(());\n        }\n        let profiles = std::fs::read_to_string(&profiles_path)?;\n        let mut profiles: Mapping = serde_yaml::from_str(&profiles)\n            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;\n        let items = profiles\n            .get_mut(\"items\")\n            .and_then(|items| items.as_sequence_mut());\n        if let Some(items) = items {\n            for item in items {\n                if let Some(item) = item.as_mapping_mut()\n                    && item\n                        .get(\"type\")\n                        .is_some_and(|ty| ty.as_str().is_some_and(|ty| ty == \"script\"))\n                {\n                    item.insert(\n                        \"type\".into(),\n                        serde_yaml::Value::Tagged(Box::new(TaggedValue {\n                            tag: Tag::new(\"script\"),\n                            value: serde_yaml::Value::String(\"javascript\".to_string()),\n                        })),\n                    );\n                }\n            }\n            let profiles = serde_yaml::to_string(&profiles)\n                .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;\n            std::fs::write(profiles_path, profiles)?;\n        }\n\n        Ok(())\n    }\n\n    fn discard(&self) -> std::io::Result<()> {\n        let profiles_path = crate::utils::dirs::profiles_path()\n            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e.to_string()))?;\n        if !profiles_path.exists() {\n            println!(\"Profiles dir not found, skipping migration\");\n            return Ok(());\n        }\n        let profiles = std::fs::read_to_string(&profiles_path)?;\n        let mut profiles: Mapping = serde_yaml::from_str(&profiles)\n            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;\n        let items = profiles\n            .get_mut(\"items\")\n            .and_then(|items| items.as_sequence_mut());\n        if let Some(items) = items {\n            for item in items {\n                if let Some(item) = item.as_mapping_mut()\n                    && item.get(\"type\").is_some_and(|ty| {\n                        if let serde_yaml::Value::Tagged(ty) = ty {\n                            ty.tag == Tag::new(\"script\")\n                        } else {\n                            false\n                        }\n                    })\n                {\n                    item.insert(\n                        \"type\".into(),\n                        serde_yaml::Value::String(\"script\".to_string()),\n                    );\n                }\n            }\n            let profiles = serde_yaml::to_string(&profiles)\n                .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;\n            std::fs::write(profiles_path, profiles)?;\n        }\n\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "backend/tauri/src/core/migration/units/unit_200/profile_script_newtype.rs",
    "content": "use std::borrow::Cow;\n\nuse semver::Version;\nuse serde_yaml::{\n    Mapping, Value,\n    value::{Tag, TaggedValue},\n};\n\nuse crate::{core::migration::Migration, utils::help};\n\n#[derive(Debug, Clone, Copy)]\n/// 将\n/// ```yaml\n/// type: !script javascript\n/// ```\n/// 展开为\n/// ```yaml\n/// type: script\n/// script_type: javascript\n/// ```\n/// 其他不做特殊处理\npub struct MigrateProfileScriptNewtype;\n\nimpl Migration<'_> for MigrateProfileScriptNewtype {\n    fn version(&self) -> &'static Version {\n        &super::VERSION\n    }\n\n    fn name(&self) -> Cow<'static, str> {\n        Cow::Borrowed(\"MigrateProfileScriptNewtype\")\n    }\n\n    fn migrate(&self) -> std::io::Result<()> {\n        let profiles_path = crate::utils::dirs::profiles_path().map_err(std::io::Error::other)?;\n        if !profiles_path.exists() {\n            eprintln!(\"profiles dir not found, skipping migration\");\n            return Ok(());\n        }\n        eprintln!(\"Trying to read profiles files...\");\n        let profiles = std::fs::read_to_string(profiles_path.clone())?;\n        eprintln!(\"Trying to parse profiles files...\");\n        let profiles: Mapping = serde_yaml::from_str(&profiles)\n            .map_err(|e| std::io::Error::other(format!(\"failed to parse profiles: {e}\")))?;\n        eprintln!(\"Trying to migrate profiles files...\");\n        let profiles = migrate_profile_data(profiles);\n        eprintln!(\"Trying to write profiles files...\");\n        help::save_yaml(\n            &profiles_path,\n            &profiles,\n            Some(\"# Profiles Config for Clash Nyanpasu\"),\n        )\n        .map_err(std::io::Error::other)?;\n        Ok(())\n    }\n\n    fn discard(&self) -> std::io::Result<()> {\n        let profiles_path = crate::utils::dirs::profiles_path().map_err(std::io::Error::other)?;\n        if !profiles_path.exists() {\n            eprintln!(\"profiles dir not found, skipping discard\");\n            return Ok(());\n        }\n        eprintln!(\"Trying to read profiles files...\");\n        let profiles = std::fs::read_to_string(profiles_path.clone())?;\n        eprintln!(\"Trying to parse profiles files...\");\n        let profiles: Mapping = serde_yaml::from_str(&profiles)\n            .map_err(|e| std::io::Error::other(format!(\"failed to parse profiles: {e}\")))?;\n        eprintln!(\"Trying to discard profiles files...\");\n        let profiles = discard_profile_data(profiles);\n        eprintln!(\"Trying to write profiles files...\");\n        help::save_yaml(\n            &profiles_path,\n            &profiles,\n            Some(\"# Profiles Config for Clash Nyanpasu\"),\n        )\n        .map_err(std::io::Error::other)?;\n        Ok(())\n    }\n}\n\nfn migrate_profile_data(mut mapping: serde_yaml::Mapping) -> serde_yaml::Mapping {\n    // We just need to iter items\n    if let Some(items) = mapping.get_mut(\"items\")\n        && let Some(items) = items.as_sequence_mut()\n    {\n        for item in items {\n            if let Some(item) = item.as_mapping_mut()\n                && let Some(ty) = item.get(\"type\").cloned()\n                && let Value::Tagged(tag) = ty\n                && tag.tag == \"script\"\n                && let Some(script_kind) = tag.value.as_str()\n            {\n                item.insert(\n                    \"type\".into(),\n                    serde_yaml::Value::String(\"script\".to_string()),\n                );\n                item.insert(\n                    \"script_type\".into(),\n                    serde_yaml::Value::String(script_kind.to_string()),\n                );\n            }\n        }\n    }\n\n    mapping\n}\n\nfn discard_profile_data(mut mapping: serde_yaml::Mapping) -> serde_yaml::Mapping {\n    // We just need to iter items\n    if let Some(items) = mapping.get_mut(\"items\")\n        && let Some(items) = items.as_sequence_mut()\n    {\n        for item in items {\n            if let Some(item) = item.as_mapping_mut()\n                && let Some(ty) = item.get(\"type\").cloned()\n                && let Value::String(ty) = ty\n                && ty == \"script\"\n                && let Some(script_kind) = item.get(\"script_type\").cloned()\n            {\n                item.insert(\n                    \"type\".into(),\n                    serde_yaml::Value::Tagged(Box::new(TaggedValue {\n                        tag: Tag::new(\"script\"),\n                        value: script_kind,\n                    })),\n                );\n                item.remove(\"script_type\");\n            }\n        }\n    }\n\n    mapping\n}\n\n#[cfg(test)]\nmod tests {\n    use crate::config::Profiles;\n\n    use super::*;\n    use pretty_assertions::assert_str_eq;\n\n    const ORIGINAL_SAMPLE: &str = r#\"current:\n- rIWXPHuafvEM\nchain: []\nvalid:\n- dns\n- unified-delay\n- tcp-concurrent\n- tun\n- profile\nitems:\n- uid: rIWXPHuafvEM\n  type: remote\n  name: 🌸云\n  file: rIWXPHuafvEM.yaml\n  desc: null\n  updated: 1758110672\n  url: https://example.com\n  extra:\n    upload: 3641183914\n    download: 39111158992\n    total: 42946719600\n    expire: 1769123200\n  option:\n    with_proxy: false\n    self_proxy: true\n    update_interval: 1440\n  chain:\n  - siL1cvjnvLB6\n  - sxI0dHKeqSNg\n- uid: siL1cvjnvLB6\n  type: !script javascript\n  name: 花☁️处理\n  file: siL1cvjnvLB6.js\n  desc: ''\n  updated: 1720954186\n- uid: sxI0dHKeqSNg\n  type: !script javascript\n  name: 🌸☁️图标\n  file: sxI0dHKeqSNg.js\n  desc: ''\n  updated: 1722656540\n- uid: sZYZe33w7RKV\n  type: !script lua\n  name: 图标\n  file: sZYZe33w7RKV.lua\n  desc: ''\n  updated: 1724082226\n- uid: lkvV5JXfzO34\n  type: local\n  name: New Profile\n  file: lkvV5JXfzO34.yaml\n  desc: ''\n  updated: 1725587682\n  chain: []\n- uid: lJynXCoMMIUd\n  type: local\n  name: New Profile\n  file: lJynXCoMMIUd.yaml\n  desc: ''\n  updated: 1726252304\n  chain: []\n- uid: lBtaVEaMAR97\n  type: local\n  name: Test\n  file: lBtaVEaMAR97.yaml\n  desc: ''\n  updated: 1727621893\n  chain: []\n\"#;\n\n    const MIGRATED_SAMPLE: &str = r#\"current:\n- rIWXPHuafvEM\nchain: []\nvalid:\n- dns\n- unified-delay\n- tcp-concurrent\n- tun\n- profile\nitems:\n- uid: rIWXPHuafvEM\n  type: remote\n  name: 🌸云\n  file: rIWXPHuafvEM.yaml\n  desc: null\n  updated: 1758110672\n  url: https://example.com\n  extra:\n    upload: 3641183914\n    download: 39111158992\n    total: 42946719600\n    expire: 1769123200\n  option:\n    with_proxy: false\n    self_proxy: true\n    update_interval: 1440\n  chain:\n  - siL1cvjnvLB6\n  - sxI0dHKeqSNg\n- uid: siL1cvjnvLB6\n  type: script\n  name: 花☁️处理\n  file: siL1cvjnvLB6.js\n  desc: ''\n  updated: 1720954186\n  script_type: javascript\n- uid: sxI0dHKeqSNg\n  type: script\n  name: 🌸☁️图标\n  file: sxI0dHKeqSNg.js\n  desc: ''\n  updated: 1722656540\n  script_type: javascript\n- uid: sZYZe33w7RKV\n  type: script\n  name: 图标\n  file: sZYZe33w7RKV.lua\n  desc: ''\n  updated: 1724082226\n  script_type: lua\n- uid: lkvV5JXfzO34\n  type: local\n  name: New Profile\n  file: lkvV5JXfzO34.yaml\n  desc: ''\n  updated: 1725587682\n  chain: []\n- uid: lJynXCoMMIUd\n  type: local\n  name: New Profile\n  file: lJynXCoMMIUd.yaml\n  desc: ''\n  updated: 1726252304\n  chain: []\n- uid: lBtaVEaMAR97\n  type: local\n  name: Test\n  file: lBtaVEaMAR97.yaml\n  desc: ''\n  updated: 1727621893\n  chain: []\n\"#;\n\n    #[test]\n    fn test_migrate_existing_data() {\n        let original_data = serde_yaml::from_str::<serde_yaml::Mapping>(ORIGINAL_SAMPLE).unwrap();\n        let migrated_data = migrate_profile_data(original_data);\n        let output_data = serde_yaml::to_string(&migrated_data).unwrap();\n        assert_str_eq!(output_data, MIGRATED_SAMPLE);\n    }\n\n    #[test]\n    fn test_discard_existing_data() {\n        let migrated_data = serde_yaml::from_str::<serde_yaml::Mapping>(MIGRATED_SAMPLE).unwrap();\n        let original_data = discard_profile_data(migrated_data);\n        let output_data = serde_yaml::to_string(&original_data).unwrap();\n        assert_str_eq!(output_data, ORIGINAL_SAMPLE);\n    }\n\n    #[test]\n    #[ignore]\n    fn test_profile_parse_migrated_data() {\n        let profiles = serde_yaml::from_str::<Profiles>(MIGRATED_SAMPLE).unwrap();\n        eprintln!(\"{profiles:#?}\");\n    }\n}\n"
  },
  {
    "path": "backend/tauri/src/core/migration/units/unit_200.rs",
    "content": "use std::borrow::Cow;\n\nuse once_cell::sync::Lazy;\nuse semver::Version;\nuse serde_yaml::Mapping;\n\nuse crate::{\n    core::migration::{DynMigration, Migration, MigrationExt},\n    utils::dirs,\n};\n\nmod profile_script_newtype;\n\npub static UNITS: Lazy<Vec<DynMigration>> = Lazy::new(|| {\n    vec![\n        MigrateProfilesNullValue.boxed(),\n        MigrateLanguageOption.boxed(),\n        MigrateThemeSetting.boxed(),\n        profile_script_newtype::MigrateProfileScriptNewtype.boxed(),\n    ]\n});\n\npub static VERSION: Lazy<semver::Version> = Lazy::new(|| semver::Version::parse(\"2.0.0\").unwrap());\n\n#[derive(Debug, Clone)]\npub struct MigrateProfilesNullValue;\n\nimpl Migration<'_> for MigrateProfilesNullValue {\n    fn version(&self) -> &'static Version {\n        &VERSION\n    }\n\n    fn name(&self) -> Cow<'static, str> {\n        Cow::Borrowed(\"MigrateProfilesNullValue\")\n    }\n\n    fn migrate(&self) -> std::io::Result<()> {\n        let profiles_path = dirs::profiles_path().map_err(std::io::Error::other)?;\n        if !profiles_path.exists() {\n            return Ok(());\n        }\n        let profiles = std::fs::read_to_string(profiles_path.clone())?;\n        let mut profiles: Mapping = serde_yaml::from_str(&profiles)\n            .map_err(|e| std::io::Error::other(format!(\"failed to parse profiles: {e}\")))?;\n\n        profiles.iter_mut().for_each(|(key, value)| {\n            if value.is_null() {\n                println!(\"detected null value in profiles {key:?} should be migrated\");\n                *value = serde_yaml::Value::Sequence(Vec::new());\n            }\n        });\n        let file = std::fs::OpenOptions::new()\n            .write(true)\n            .truncate(true)\n            .open(profiles_path)?;\n        serde_yaml::to_writer(file, &profiles).map_err(std::io::Error::other)?;\n        Ok(())\n    }\n\n    fn discard(&self) -> std::io::Result<()> {\n        let profiles_path = dirs::profiles_path().map_err(std::io::Error::other)?;\n        if !profiles_path.exists() {\n            return Ok(());\n        }\n        let profiles = std::fs::read_to_string(profiles_path.clone())?;\n        let mut profiles: Mapping = serde_yaml::from_str(&profiles)\n            .map_err(|e| std::io::Error::other(format!(\"failed to parse profiles: {e}\")))?;\n\n        profiles.iter_mut().for_each(|(key, value)| {\n            if key.is_string() && key.as_str().unwrap() == \"chain\" && value.is_sequence() {\n                println!(\"detected sequence value in profiles {key:?} should be migrated\");\n                *value = serde_yaml::Value::Null;\n            }\n            if key.is_string() && key.as_str().unwrap() == \"current\" && value.is_sequence() {\n                println!(\"detected sequence value in profiles {key:?} should be migrated\");\n                *value = serde_yaml::Value::Null;\n            }\n        });\n        let file = std::fs::OpenOptions::new()\n            .write(true)\n            .truncate(true)\n            .open(profiles_path)?;\n        serde_yaml::to_writer(file, &profiles).map_err(std::io::Error::other)?;\n        Ok(())\n    }\n}\n\n#[derive(Debug, Clone)]\npub struct MigrateLanguageOption;\nimpl<'a> Migration<'a> for MigrateLanguageOption {\n    fn version(&self) -> &'a semver::Version {\n        &VERSION\n    }\n\n    fn name(&self) -> std::borrow::Cow<'a, str> {\n        Cow::Borrowed(\"Migrate Language Option\")\n    }\n\n    fn migrate(&self) -> std::io::Result<()> {\n        let config_path = crate::utils::dirs::nyanpasu_config_path().unwrap();\n        if !config_path.exists() {\n            println!(\"Config file not found, skipping migration\");\n            return Ok(());\n        }\n        println!(\"parse config file...\");\n        let config = std::fs::read_to_string(&config_path)\n            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;\n        let mut config: Mapping = serde_yaml::from_str(&config)\n            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;\n        let lang = config.get_mut(\"language\");\n        match lang {\n            None => {\n                println!(\"language not found, skipping migration\");\n                return Ok(());\n            }\n            Some(lang) => {\n                if lang == \"zh\" {\n                    println!(\"detected old language option, migrating...\");\n                    let _value = lang.as_str().unwrap();\n                    let value = \"zh-CN\";\n                    *lang = serde_yaml::Value::from(value);\n                    println!(\"write config file...\");\n                    let config = serde_yaml::to_string(&config)\n                        .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;\n                    std::fs::write(&config_path, config)?;\n                }\n                println!(\"Migration completed\");\n            }\n        }\n        Ok(())\n    }\n}\n\n#[derive(Debug, Clone)]\npub struct MigrateThemeSetting;\nimpl<'a> Migration<'a> for MigrateThemeSetting {\n    fn version(&self) -> &'a semver::Version {\n        &VERSION\n    }\n\n    fn name(&self) -> std::borrow::Cow<'a, str> {\n        Cow::Borrowed(\"Migrate Theme Setting\")\n    }\n\n    fn migrate(&self) -> std::io::Result<()> {\n        let config_path = crate::utils::dirs::nyanpasu_config_path().unwrap();\n        if !config_path.exists() {\n            return Ok(());\n        }\n        let raw_config = std::fs::read_to_string(&config_path)?;\n        let mut config: Mapping =\n            serde_yaml::from_str(&raw_config).map_err(std::io::Error::other)?;\n        if let Some(theme) = config.get(\"theme_setting\")\n            && !theme.is_null()\n            && let Some(theme_obj) = theme.as_mapping()\n            && let Some(color) = theme_obj.get(\"primary_color\")\n        {\n            println!(\"color: {color:?}\");\n            config.insert(\"theme_color\".into(), color.clone());\n        }\n        config.remove(\"theme_setting\");\n        let new_config = serde_yaml::to_string(&config).map_err(std::io::Error::other)?;\n        std::fs::write(&config_path, new_config)?;\n        Ok(())\n    }\n\n    fn discard(&self) -> std::io::Result<()> {\n        let config_path = crate::utils::dirs::nyanpasu_config_path().unwrap();\n        if !config_path.exists() {\n            return Ok(());\n        }\n        let raw_config = std::fs::read_to_string(&config_path)?;\n        let mut config: Mapping =\n            serde_yaml::from_str(&raw_config).map_err(std::io::Error::other)?;\n        if let Some(color) = config.get(\"theme_color\") {\n            let mut theme_obj = Mapping::new();\n            theme_obj.insert(\"primary_color\".into(), color.clone());\n            config.insert(\n                \"theme_setting\".into(),\n                serde_yaml::Value::Mapping(theme_obj),\n            );\n            config.remove(\"theme_color\");\n        }\n        let new_config = serde_yaml::to_string(&config).map_err(std::io::Error::other)?;\n        std::fs::write(&config_path, new_config)?;\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "backend/tauri/src/core/mod.rs",
    "content": "pub mod clash;\npub mod connection_interruption;\npub mod handle;\npub mod hotkey;\npub mod logger;\npub mod manager;\npub mod pac;\npub mod service;\npub mod storage;\npub mod sysopt;\npub mod tasks;\npub mod tray;\npub mod updater;\n#[cfg(windows)]\npub mod win_uwp;\npub use self::clash::core::*;\npub mod migration;\npub mod state;\npub mod state_v2;\n"
  },
  {
    "path": "backend/tauri/src/core/pac.rs",
    "content": "use crate::{config::Config, log_err};\nuse anyhow::{Context, Result};\nuse std::{path::PathBuf, time::Duration};\nuse sysproxy::Autoproxy;\nuse tokio::fs;\n\n/// PAC module for handling Proxy Auto-Configuration\npub struct PacManager;\n\n// Constants for PAC handling\nconst PAC_DOWNLOAD_TIMEOUT: u64 = 30; // seconds\nconst PAC_MAX_RETRIES: u32 = 3;\nconst PAC_RETRY_DELAY: u64 = 5; // seconds\n\nimpl PacManager {\n    /// Get PAC URL from config\n    pub fn get_pac_url() -> Option<String> {\n        Config::verge().latest().pac_url.clone()\n    }\n\n    /// Check if PAC is enabled (URL is set)\n    pub fn is_pac_enabled() -> bool {\n        Self::get_pac_url().is_some_and(|url| !url.is_empty())\n    }\n\n    /// Download PAC script from URL with retry logic\n    pub async fn download_pac_script(url: &str) -> Result<String> {\n        let client = reqwest::Client::builder()\n            .timeout(Duration::from_secs(PAC_DOWNLOAD_TIMEOUT))\n            .build()\n            .context(\"failed to build HTTP client\")?;\n\n        // Retry logic\n        let mut last_error = None;\n        for attempt in 1..=PAC_MAX_RETRIES {\n            match client.get(url).send().await {\n                Ok(response) => {\n                    if response.status().is_success() {\n                        match response.text().await {\n                            Ok(content) => return Ok(content),\n                            Err(e) => {\n                                let err =\n                                    anyhow::anyhow!(\"failed to read PAC script content: {}\", e);\n                                log::warn!(target: \"app\", \"Attempt {}/{} failed: {}\", attempt, PAC_MAX_RETRIES, err);\n                                last_error = Some(err);\n                            }\n                        }\n                    } else {\n                        let err = anyhow::anyhow!(\n                            \"failed to download PAC script, status: {}\",\n                            response.status()\n                        );\n                        log::warn!(target: \"app\", \"Attempt {}/{} failed: {}\", attempt, PAC_MAX_RETRIES, err);\n                        last_error = Some(err);\n                    }\n                }\n                Err(e) => {\n                    let err = anyhow::anyhow!(\"failed to download PAC script: {}\", e);\n                    log::warn!(target: \"app\", \"Attempt {}/{} failed: {}\", attempt, PAC_MAX_RETRIES, err);\n                    last_error = Some(err);\n                }\n            }\n\n            // Wait before retrying (except on last attempt)\n            if attempt < PAC_MAX_RETRIES {\n                tokio::time::sleep(Duration::from_secs(PAC_RETRY_DELAY)).await;\n            }\n        }\n\n        Err(last_error.unwrap_or_else(|| {\n            anyhow::anyhow!(\n                \"failed to download PAC script after {} attempts\",\n                PAC_MAX_RETRIES\n            )\n        }))\n    }\n\n    /// Save PAC script to cache directory\n    pub async fn save_pac_script(script: &str) -> Result<PathBuf> {\n        let cache_dir = crate::utils::dirs::cache_dir()?;\n        let pac_file = cache_dir.join(\"pac.js\");\n\n        fs::write(&pac_file, script)\n            .await\n            .context(\"failed to save PAC script\")?;\n\n        Ok(pac_file)\n    }\n\n    /// Basic validation of PAC script structure - check for required functions\n    pub async fn validate_pac_script(script: &str) -> Result<()> {\n        // A basic validation without using the JS engine - just check if FindProxyForURL function exists\n        if !script.contains(\"FindProxyForURL\") {\n            return Err(anyhow::anyhow!(\n                \"PAC script must contain FindProxyForURL function\"\n            ));\n        }\n\n        // Additional basic checks could be added here if needed\n        Ok(())\n    }\n\n    /// Set system proxy to use PAC URL\n    pub fn set_pac_proxy(url: &str) -> Result<()> {\n        // Check if Autoproxy is supported on this platform\n        if !Autoproxy::is_support() {\n            return Err(anyhow::anyhow!(\n                \"PAC proxy is not supported on this platform\"\n            ));\n        }\n\n        let autoproxy = Autoproxy {\n            enable: true,\n            url: url.to_string(),\n        };\n\n        autoproxy\n            .set_auto_proxy()\n            .context(\"failed to set PAC proxy\")?;\n\n        Ok(())\n    }\n\n    /// Disable PAC proxy and revert to direct proxy\n    pub fn disable_pac_proxy() -> Result<()> {\n        // Check if Autoproxy is supported on this platform\n        if !Autoproxy::is_support() {\n            log::info!(target: \"app\", \"PAC proxy is not supported on this platform, skipping disable\");\n            return Ok(());\n        }\n\n        let autoproxy = Autoproxy {\n            enable: false,\n            url: String::new(),\n        };\n\n        autoproxy\n            .set_auto_proxy()\n            .context(\"failed to disable PAC proxy\")?;\n\n        Ok(())\n    }\n\n    /// Fallback to direct proxy when PAC fails\n    pub fn fallback_to_direct_proxy() -> Result<()> {\n        log::warn!(target: \"app\", \"Falling back to direct proxy mode\");\n\n        // Check if Sysproxy is supported on this platform\n        if !sysproxy::Sysproxy::is_support() {\n            return Err(anyhow::anyhow!(\n                \"Direct proxy is not supported on this platform\"\n            ));\n        }\n\n        // Get the standard proxy settings\n        let port = Config::verge()\n            .latest()\n            .verge_mixed_port\n            .unwrap_or(Config::clash().data().get_mixed_port());\n\n        let (enable, bypass) = {\n            let verge = Config::verge();\n            let verge = verge.latest();\n            (\n                verge.enable_system_proxy.unwrap_or(false),\n                verge.system_proxy_bypass.clone(),\n            )\n        };\n\n        #[cfg(target_os = \"windows\")]\n        let default_bypass = \"localhost;127.*;192.168.*;10.*;172.16.*;172.17.*;172.18.*;172.19.*;172.20.*;172.21.*;172.22.*;172.23.*;172.24.*;172.25.*;172.26.*;172.27.*;172.28.*;172.29.*;172.30.*;172.31.*;<local>\";\n        #[cfg(target_os = \"linux\")]\n        let default_bypass = \"localhost,127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,::1\";\n        #[cfg(target_os = \"macos\")]\n        let default_bypass = \"127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,localhost,*.local,*.crashlytics.com,<local>\";\n\n        let sysproxy = sysproxy::Sysproxy {\n            enable,\n            host: String::from(\"127.0.0.1\"),\n            port,\n            bypass: bypass.unwrap_or(default_bypass.into()),\n        };\n\n        sysproxy\n            .set_system_proxy()\n            .context(\"failed to set direct proxy as fallback\")?;\n\n        log::info!(target: \"app\", \"Fallback to direct proxy successful\");\n        Ok(())\n    }\n\n    /// Update PAC configuration with error handling and fallback\n    pub async fn update_pac() -> Result<()> {\n        if !Self::is_pac_enabled() {\n            log::info!(target: \"app\", \"PAC is not enabled, skipping update\");\n            return Ok(());\n        }\n\n        // Check if Autoproxy is supported on this platform\n        if !Autoproxy::is_support() {\n            log::warn!(target: \"app\", \"PAC proxy is not supported on this platform\");\n            // Try to fallback to direct proxy\n            log_err!(Self::fallback_to_direct_proxy());\n            return Err(anyhow::anyhow!(\n                \"PAC proxy is not supported on this platform\"\n            ));\n        }\n\n        let pac_url = Self::get_pac_url().unwrap();\n        log::info!(target: \"app\", \"Updating PAC from URL: {}\", pac_url);\n\n        // Download PAC script\n        let script = match Self::download_pac_script(&pac_url).await {\n            Ok(script) => script,\n            Err(e) => {\n                log::error!(target: \"app\", \"Failed to download PAC script: {}\", e);\n                // Try to fallback to direct proxy\n                log_err!(Self::fallback_to_direct_proxy());\n                return Err(e);\n            }\n        };\n\n        // Validate PAC script\n        if let Err(e) = Self::validate_pac_script(&script).await {\n            log::error!(target: \"app\", \"PAC script validation failed: {}\", e);\n            // Try to fallback to direct proxy\n            log_err!(Self::fallback_to_direct_proxy());\n            return Err(e);\n        }\n\n        // Save PAC script to cache\n        if let Err(e) = Self::save_pac_script(&script).await {\n            log::warn!(target: \"app\", \"Failed to save PAC script to cache: {}\", e);\n            // This is not critical, continue with setting the proxy\n        }\n\n        // Set system proxy to use PAC\n        if let Err(e) = Self::set_pac_proxy(&pac_url) {\n            log::error!(target: \"app\", \"Failed to set PAC proxy: {}\", e);\n            // Try to fallback to direct proxy\n            log_err!(Self::fallback_to_direct_proxy());\n            return Err(e);\n        }\n\n        log::info!(target: \"app\", \"PAC updated successfully\");\n        Ok(())\n    }\n\n    /// Initialize PAC proxy on startup with error handling\n    pub async fn init_pac_proxy() -> Result<()> {\n        if !Self::is_pac_enabled() {\n            log::info!(target: \"app\", \"PAC is not enabled, skipping initialization\");\n            return Ok(());\n        }\n\n        log::info!(target: \"app\", \"Initializing PAC proxy\");\n        if let Err(e) = Self::update_pac().await {\n            log::error!(target: \"app\", \"Failed to initialize PAC proxy: {}\", e);\n            return Err(e);\n        }\n        Ok(())\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use tokio;\n\n    #[tokio::test]\n    async fn test_pac_download() {\n        // Test with a known good PAC URL\n        let pac_url = \"https://raw.githubusercontent.com/Slinetrac/clash-nyanpasu/main/test.pac\";\n\n        match PacManager::download_pac_script(pac_url).await {\n            Ok(script) => {\n                assert!(!script.is_empty());\n                println!(\n                    \"Downloaded PAC script: {}\",\n                    &script[..std::cmp::min(100, script.len())]\n                );\n            }\n            Err(e) => {\n                eprintln!(\"Failed to download PAC script: {}\", e);\n                // This might fail in test environment, so we won't assert failure\n            }\n        }\n    }\n\n    #[tokio::test]\n    async fn test_pac_validation() {\n        let valid_pac_script = r#\"\n            function FindProxyForURL(url, host) {\n                return \"DIRECT\";\n            }\n        \"#;\n\n        assert!(\n            PacManager::validate_pac_script(valid_pac_script)\n                .await\n                .is_ok()\n        );\n\n        let invalid_pac_script = r#\"\n            function SomeOtherFunction(url, host) {\n                // This script does not contain the required function\n                return \"PROXY proxy.example.com:8080\";\n            }\n        \"#;\n\n        assert!(\n            PacManager::validate_pac_script(invalid_pac_script)\n                .await\n                .is_err()\n        );\n    }\n\n    #[tokio::test]\n    async fn test_pac_save() {\n        let script = \"function FindProxyForURL(url, host) { return 'DIRECT'; }\";\n        match PacManager::save_pac_script(script).await {\n            Ok(path) => {\n                assert!(path.exists());\n                // Clean up\n                let _ = tokio::fs::remove_file(path).await;\n            }\n            Err(e) => {\n                eprintln!(\"Failed to save PAC script: {}\", e);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "backend/tauri/src/core/service/control.rs",
    "content": "use crate::utils::dirs::{app_config_dir, app_data_dir, app_install_dir};\nuse runas::Command as RunasCommand;\nuse std::ffi::OsString;\n\nuse super::SERVICE_PATH;\n\n#[cfg(unix)]\nuse std::os::unix::process::ExitStatusExt;\n\npub async fn get_service_install_args() -> Result<Vec<OsString>, anyhow::Error> {\n    let user = {\n        #[cfg(windows)]\n        {\n            nyanpasu_utils::os::get_current_user_sid().await?\n        }\n        #[cfg(not(windows))]\n        {\n            whoami::username()\n        }\n    };\n    let data_dir = app_data_dir()?;\n    let config_dir = app_config_dir()?;\n    let app_dir = app_install_dir()?;\n\n    #[cfg(not(windows))]\n    let args: Vec<OsString> = vec![\n        \"install\".into(),\n        \"--user\".into(),\n        user.into(),\n        \"--nyanpasu-data-dir\".into(),\n        format!(\"\\\"{}\\\"\", data_dir.to_string_lossy()).into(),\n        \"--nyanpasu-config-dir\".into(),\n        format!(\"\\\"{}\\\"\", config_dir.to_string_lossy()).into(),\n        \"--nyanpasu-app-dir\".into(),\n        format!(\"\\\"{}\\\"\", app_dir.to_string_lossy()).into(),\n    ];\n\n    #[cfg(windows)]\n    let args: Vec<OsString> = vec![\n        \"install\".into(),\n        \"--user\".into(),\n        user.into(),\n        \"--nyanpasu-data-dir\".into(),\n        data_dir.into(),\n        \"--nyanpasu-config-dir\".into(),\n        config_dir.into(),\n        \"--nyanpasu-app-dir\".into(),\n        app_dir.into(),\n    ];\n\n    Ok(args)\n}\n\npub async fn install_service() -> anyhow::Result<()> {\n    let args = get_service_install_args().await?;\n    let child = tokio::task::spawn_blocking(move || {\n        #[cfg(not(target_os = \"macos\"))]\n        {\n            RunasCommand::new(SERVICE_PATH.as_path())\n                .args(&args)\n                .gui(true)\n                .show(true)\n                .status()\n        }\n        #[cfg(target_os = \"macos\")]\n        {\n            use crate::utils::sudo::sudo;\n            let args = args.iter().map(|s| s.to_string_lossy()).collect::<Vec<_>>();\n            match sudo(SERVICE_PATH.to_string_lossy(), &args) {\n                Ok(()) => Ok(std::process::ExitStatus::from_raw(0)),\n                Err(e) => {\n                    tracing::error!(\"failed to install service: {}\", e);\n                    Err(e)\n                }\n            }\n        }\n    })\n    .await??;\n    if !child.success() {\n        anyhow::bail!(\n            \"failed to install service, exit code: {}, signal: {:?}\",\n            child.code().unwrap_or(-1),\n            {\n                #[cfg(unix)]\n                {\n                    child.signal().unwrap_or(0)\n                }\n                #[cfg(not(unix))]\n                {\n                    0\n                }\n            }\n        );\n    }\n    // Due to most platform, the service will be started automatically after installed\n    if !super::ipc::HEALTH_CHECK_RUNNING.load(std::sync::atomic::Ordering::Relaxed) {\n        super::ipc::spawn_health_check();\n    }\n    Ok(())\n}\n\npub async fn update_service() -> anyhow::Result<()> {\n    let child = tokio::task::spawn_blocking(move || {\n        const ARGS: &[&str] = &[\"update\"];\n        #[cfg(not(target_os = \"macos\"))]\n        {\n            RunasCommand::new(SERVICE_PATH.as_path())\n                .args(ARGS)\n                .gui(true)\n                .show(true)\n                .status()\n        }\n        #[cfg(target_os = \"macos\")]\n        {\n            use crate::utils::sudo::sudo;\n            match sudo(SERVICE_PATH.to_string_lossy(), ARGS) {\n                Ok(()) => Ok(std::process::ExitStatus::from_raw(0)),\n                Err(e) => {\n                    tracing::error!(\"failed to install service: {}\", e);\n                    Err(e)\n                }\n            }\n        }\n    })\n    .await??;\n    if !child.success() {\n        anyhow::bail!(\n            \"failed to update service, exit code: {}, signal: {:?}\",\n            child.code().unwrap_or(-1),\n            {\n                #[cfg(unix)]\n                {\n                    child.signal().unwrap_or(0)\n                }\n                #[cfg(not(unix))]\n                {\n                    0\n                }\n            }\n        );\n    }\n    Ok(())\n}\n\npub async fn uninstall_service() -> anyhow::Result<()> {\n    let child = tokio::task::spawn_blocking(move || {\n        const ARGS: &[&str] = &[\"uninstall\"];\n        #[cfg(not(target_os = \"macos\"))]\n        {\n            RunasCommand::new(SERVICE_PATH.as_path())\n                .args(ARGS)\n                .gui(true)\n                .show(true)\n                .status()\n        }\n        #[cfg(target_os = \"macos\")]\n        {\n            use crate::utils::sudo::sudo;\n            match sudo(SERVICE_PATH.to_string_lossy(), ARGS) {\n                Ok(()) => Ok(std::process::ExitStatus::from_raw(0)),\n                Err(e) => {\n                    tracing::error!(\"failed to install service: {}\", e);\n                    Err(e)\n                }\n            }\n        }\n    })\n    .await??;\n    if !child.success() {\n        anyhow::bail!(\n            \"failed to uninstall service, exit code: {}\",\n            child.code().unwrap()\n        );\n    }\n    let _ = super::ipc::KILL_FLAG.compare_exchange(\n        false,\n        true,\n        std::sync::atomic::Ordering::Acquire,\n        std::sync::atomic::Ordering::Relaxed,\n    );\n    Ok(())\n}\n\npub async fn start_service() -> anyhow::Result<()> {\n    let child = tokio::task::spawn_blocking(move || {\n        const ARGS: &[&str] = &[\"start\"];\n        #[cfg(not(target_os = \"macos\"))]\n        {\n            RunasCommand::new(SERVICE_PATH.as_path())\n                .args(ARGS)\n                .gui(true)\n                .show(true)\n                .status()\n        }\n        #[cfg(target_os = \"macos\")]\n        {\n            use crate::utils::sudo::sudo;\n            match sudo(SERVICE_PATH.to_string_lossy(), ARGS) {\n                Ok(()) => Ok(std::process::ExitStatus::from_raw(0)),\n                Err(e) => {\n                    tracing::error!(\"failed to install service: {}\", e);\n                    Err(e)\n                }\n            }\n        }\n    })\n    .await??;\n    if !child.success() {\n        anyhow::bail!(\n            \"failed to start service, exit code: {}, signal: {:?}\",\n            child.code().unwrap_or(-1),\n            {\n                #[cfg(unix)]\n                {\n                    child.signal().unwrap_or(0)\n                }\n                #[cfg(not(unix))]\n                {\n                    0\n                }\n            }\n        );\n    }\n    if !super::ipc::HEALTH_CHECK_RUNNING.load(std::sync::atomic::Ordering::Acquire) {\n        super::ipc::spawn_health_check();\n    }\n    Ok(())\n}\n\npub async fn stop_service() -> anyhow::Result<()> {\n    let child = tokio::task::spawn_blocking(move || {\n        const ARGS: &[&str] = &[\"stop\"];\n        #[cfg(not(target_os = \"macos\"))]\n        {\n            RunasCommand::new(SERVICE_PATH.as_path())\n                .args(ARGS)\n                .gui(true)\n                .show(true)\n                .status()\n        }\n        #[cfg(target_os = \"macos\")]\n        {\n            use crate::utils::sudo::sudo;\n            match sudo(SERVICE_PATH.to_string_lossy(), ARGS) {\n                Ok(()) => Ok(std::process::ExitStatus::from_raw(0)),\n                Err(e) => {\n                    tracing::error!(\"failed to install service: {}\", e);\n                    Err(e)\n                }\n            }\n        }\n    })\n    .await??;\n    if !child.success() {\n        anyhow::bail!(\n            \"failed to stop service, exit code: {}, signal: {:?}\",\n            child.code().unwrap_or(-1),\n            {\n                #[cfg(unix)]\n                {\n                    child.signal().unwrap_or(0)\n                }\n                #[cfg(not(unix))]\n                {\n                    0\n                }\n            }\n        );\n    }\n    let _ = super::ipc::KILL_FLAG.compare_exchange_weak(\n        false,\n        true,\n        std::sync::atomic::Ordering::Acquire,\n        std::sync::atomic::Ordering::Relaxed,\n    );\n    Ok(())\n}\n\npub async fn restart_service() -> anyhow::Result<()> {\n    let child = tokio::task::spawn_blocking(move || {\n        const ARGS: &[&str] = &[\"restart\"];\n        #[cfg(not(target_os = \"macos\"))]\n        {\n            RunasCommand::new(SERVICE_PATH.as_path())\n                .args(ARGS)\n                .gui(true)\n                .show(true)\n                .status()\n        }\n        #[cfg(target_os = \"macos\")]\n        {\n            use crate::utils::sudo::sudo;\n            match sudo(SERVICE_PATH.to_string_lossy(), ARGS) {\n                Ok(()) => Ok(std::process::ExitStatus::from_raw(0)),\n                Err(e) => {\n                    tracing::error!(\"failed to install service: {}\", e);\n                    Err(e)\n                }\n            }\n        }\n    })\n    .await??;\n    if !child.success() {\n        anyhow::bail!(\n            \"failed to restart service, exit code: {}, signal: {:?}\",\n            child.code().unwrap_or(-1),\n            {\n                #[cfg(unix)]\n                {\n                    child.signal().unwrap_or(0)\n                }\n                #[cfg(not(unix))]\n                {\n                    0\n                }\n            }\n        );\n    }\n    if !super::ipc::HEALTH_CHECK_RUNNING.load(std::sync::atomic::Ordering::Acquire) {\n        super::ipc::spawn_health_check();\n    }\n    Ok(())\n}\n\n#[tracing::instrument]\npub async fn status<'a>() -> anyhow::Result<nyanpasu_ipc::types::StatusInfo<'a>> {\n    let mut cmd = tokio::process::Command::new(SERVICE_PATH.as_path());\n    cmd.args([\"status\", \"--json\"]);\n    #[cfg(windows)]\n    cmd.creation_flags(0x08000000); // CREATE_NO_WINDOW\n    let output = cmd.output().await?;\n    if !output.status.success() {\n        anyhow::bail!(\n            \"failed to query service status, exit code: {}, signal: {:?}\",\n            output.status.code().unwrap_or(-1),\n            {\n                #[cfg(unix)]\n                {\n                    output.status.signal().unwrap_or(0)\n                }\n                #[cfg(not(unix))]\n                {\n                    0\n                }\n            }\n        );\n    }\n    let mut status = String::from_utf8(output.stdout)?;\n    tracing::trace!(\"service status: {}\", status);\n    Ok(serde_json::from_str(&mut status)?)\n}\n"
  },
  {
    "path": "backend/tauri/src/core/service/ipc.rs",
    "content": "use std::sync::atomic::{AtomicBool, Ordering};\n\nuse atomic_enum::atomic_enum;\n\nuse nyanpasu_ipc::types::ServiceStatus;\nuse nyanpasu_utils::runtime::block_on;\nuse serde::Serialize;\nuse tracing::instrument;\n\nuse crate::log_err;\n\n#[derive(PartialEq, Eq, Serialize)]\n#[serde(rename_all = \"snake_case\")]\n#[atomic_enum]\npub enum IpcState {\n    Connected,\n    Disconnected,\n}\n\nimpl IpcState {\n    pub fn is_connected(&self) -> bool {\n        *self == IpcState::Connected\n    }\n}\n\nstatic IPC_STATE: AtomicIpcState = AtomicIpcState::new(IpcState::Disconnected);\npub(super) static KILL_FLAG: AtomicBool = AtomicBool::new(false);\npub(super) static HEALTH_CHECK_RUNNING: AtomicBool = AtomicBool::new(false);\n\npub fn get_ipc_state() -> IpcState {\n    IPC_STATE.load(Ordering::Relaxed)\n}\n\npub(super) fn set_ipc_state(state: IpcState) {\n    IPC_STATE.store(state, Ordering::Relaxed);\n    on_ipc_state_changed(state);\n}\n\nfn dispatch_disconnected() {\n    if IPC_STATE\n        .compare_exchange_weak(\n            IpcState::Connected,\n            IpcState::Disconnected,\n            Ordering::SeqCst,\n            Ordering::Relaxed,\n        )\n        .is_ok()\n    {\n        on_ipc_state_changed(IpcState::Disconnected)\n    }\n}\n\nfn dispatch_connected() {\n    if IPC_STATE\n        .compare_exchange_weak(\n            IpcState::Disconnected,\n            IpcState::Connected,\n            Ordering::SeqCst,\n            Ordering::Relaxed,\n        )\n        .is_ok()\n    {\n        on_ipc_state_changed(IpcState::Connected)\n    }\n}\n\n// TODO: it might be moved to outer scope?\n#[instrument]\nfn on_ipc_state_changed(state: IpcState) {\n    tracing::info!(\"IPC state changed: {:?}\", state);\n    let enabled_service = {\n        *crate::config::Config::verge()\n            .latest()\n            .enable_service_mode\n            .as_ref()\n            .unwrap_or(&false)\n    };\n    std::thread::spawn(move || {\n        nyanpasu_utils::runtime::block_on(async move {\n            if enabled_service {\n                let (_, _, run_type) = crate::core::CoreManager::global().status().await;\n                match (state, run_type) {\n                    (IpcState::Connected, crate::core::RunType::Normal)\n                    | (IpcState::Disconnected, crate::core::RunType::Service) => {\n                        tracing::info!(\"Restarting core due to IPC state change\");\n                        log_err!(crate::core::CoreManager::global().run_core().await);\n                    }\n                    _ => {}\n                }\n            }\n        })\n    });\n}\n\npub(super) fn spawn_health_check() {\n    KILL_FLAG.store(false, Ordering::Relaxed);\n    std::thread::spawn(|| {\n        HEALTH_CHECK_RUNNING.store(true, Ordering::Release);\n        block_on(async {\n            loop {\n                if KILL_FLAG.load(Ordering::Acquire) {\n                    set_ipc_state(IpcState::Disconnected);\n                    HEALTH_CHECK_RUNNING.store(false, Ordering::Release);\n                    break;\n                }\n                health_check().await;\n                tokio::time::sleep(std::time::Duration::from_secs(5)).await;\n            }\n        })\n    });\n}\n\n#[instrument]\nasync fn health_check() {\n    match super::control::status().await {\n        Ok(info) => match info.status {\n            ServiceStatus::Running => {\n                dispatch_connected();\n            }\n            ServiceStatus::Stopped | ServiceStatus::NotInstalled => {\n                dispatch_disconnected();\n            }\n        },\n        Err(e) => {\n            tracing::error!(\"IPC health check failed: {}\", e);\n            dispatch_disconnected();\n        }\n    }\n}\n"
  },
  {
    "path": "backend/tauri/src/core/service/mod.rs",
    "content": "use std::path::PathBuf;\n\nuse nyanpasu_ipc::types::StatusInfo;\nuse once_cell::sync::Lazy;\n\nuse crate::{config::Config, utils::dirs::app_install_dir};\n\npub mod control;\npub mod ipc;\n\nconst SERVICE_NAME: &str = \"nyanpasu-service\";\nstatic SERVICE_PATH: Lazy<PathBuf> = Lazy::new(|| {\n    let app_path = app_install_dir().unwrap();\n    app_path.join(format!(\"{}{}\", SERVICE_NAME, std::env::consts::EXE_SUFFIX))\n});\n\npub async fn init_service() {\n    let enable_service = {\n        *Config::verge()\n            .latest()\n            .enable_service_mode\n            .as_ref()\n            .unwrap_or(&false)\n    };\n    if let Ok(StatusInfo {\n        status: nyanpasu_ipc::types::ServiceStatus::Running,\n        ..\n    }) = control::status().await\n        && enable_service\n    {\n        ipc::spawn_health_check();\n        while !ipc::HEALTH_CHECK_RUNNING.load(std::sync::atomic::Ordering::Acquire) {\n            tokio::time::sleep(std::time::Duration::from_millis(100)).await;\n        }\n    }\n}\n"
  },
  {
    "path": "backend/tauri/src/core/state.rs",
    "content": "#[allow(dead_code)]\nuse parking_lot::{\n    MappedRwLockReadGuard, MappedRwLockWriteGuard, RwLock,\n    lock_api::{RwLockReadGuard, RwLockWriteGuard},\n};\nuse std::{\n    ops::Deref,\n    sync::{Arc, atomic::AtomicBool},\n};\n\n/// State manager for the application\n/// It provides a way to manage the application state, draft and persist it\n/// Note: It is safe to clone the StateManager, as it is backed by an Arc\n#[derive(Clone)]\npub struct ManagedState<T>\nwhere\n    T: Clone + Sync + Send,\n{\n    inner: Arc<ManagedStateInner<T>>,\n}\n\nimpl<T> Deref for ManagedState<T>\nwhere\n    T: Clone + Sync + Send,\n{\n    type Target = ManagedStateInner<T>;\n\n    fn deref(&self) -> &Self::Target {\n        &self.inner\n    }\n}\n\nimpl<T> From<T> for ManagedState<T>\nwhere\n    T: Clone + Sync + Send,\n{\n    fn from(state: T) -> Self {\n        Self {\n            inner: Arc::new(ManagedStateInner::new(state)),\n        }\n    }\n}\n\nimpl<T> ManagedState<T>\nwhere\n    T: Clone + Sync + Send,\n{\n    /// to auto commit the state when it is dropped\n    pub fn auto_commit(&self) -> ManagedStateAutoCommit<T> {\n        ManagedStateAutoCommit(self)\n    }\n}\n\npub struct ManagedStateAutoCommit<'a, T: Clone + Send + Sync>(&'a ManagedState<T>);\n\nimpl<T> Deref for ManagedStateAutoCommit<'_, T>\nwhere\n    T: Clone + Send + Sync,\n{\n    type Target = ManagedState<T>;\n\n    fn deref(&self) -> &Self::Target {\n        self.0\n    }\n}\n\nimpl<T: Clone + Send + Sync> Drop for ManagedStateAutoCommit<'_, T> {\n    fn drop(&mut self) {\n        if self.0.is_dirty() {\n            self.0.apply();\n        }\n    }\n}\n\npub struct ManagedStateInner<T>\nwhere\n    T: Clone + Sync + Send,\n{\n    inner: RwLock<T>,\n    draft: RwLock<Option<T>>,\n    is_dirty: AtomicBool,\n}\n\nimpl<T> ManagedStateInner<T>\nwhere\n    T: Clone + Sync + Send,\n{\n    /// create a new managed state\n    pub fn new(state: T) -> Self {\n        Self {\n            inner: RwLock::new(state),\n            draft: RwLock::new(None),\n            is_dirty: AtomicBool::new(false),\n        }\n    }\n\n    /// Get the committed state\n    pub fn data(&self) -> MappedRwLockReadGuard<'_, T> {\n        RwLockReadGuard::map(self.inner.read(), |guard| guard)\n    }\n\n    /// get the current state, it will return the ManagedStateLocker for the state\n    pub fn latest(&self) -> MappedRwLockReadGuard<'_, T> {\n        if self.is_dirty() {\n            let draft = self.draft.read();\n            if draft.is_some() {\n                RwLockReadGuard::map(draft, |guard| guard.as_ref().unwrap())\n            } else {\n                let state = self.inner.read();\n                RwLockReadGuard::map(state, |guard| guard)\n            }\n        } else {\n            let state = self.inner.read();\n            RwLockReadGuard::map(state, |guard| guard)\n        }\n    }\n\n    /// whether the state is dirty, i.e. a draft is present, and not yet committed or discarded\n    pub fn is_dirty(&self) -> bool {\n        self.is_dirty.load(std::sync::atomic::Ordering::Acquire)\n    }\n\n    /// You can modify the draft state, and then commit it\n    pub fn draft(&self) -> MappedRwLockWriteGuard<'_, T> {\n        if self.is_dirty() {\n            let guard = self.draft.write();\n            if guard.is_some() {\n                return RwLockWriteGuard::map(guard, |g| g.as_mut().unwrap());\n            }\n        }\n\n        let state = self.inner.read().clone();\n        self.is_dirty\n            .store(true, std::sync::atomic::Ordering::Release);\n\n        RwLockWriteGuard::map(self.draft.write(), move |guard| {\n            *guard = Some(state);\n            guard.as_mut().unwrap()\n        })\n    }\n\n    /// commit the draft state, and make it the new state\n    pub fn apply(&self) -> Option<T> {\n        if !self.is_dirty() {\n            return None;\n        }\n\n        let mut draft = self.draft.write();\n        let mut inner = self.inner.write();\n        let old_value = inner.to_owned();\n        if let Some(draft_value) = draft.take() {\n            *inner = draft_value;\n            self.is_dirty\n                .store(false, std::sync::atomic::Ordering::Release);\n            Some(old_value)\n        } else {\n            self.is_dirty\n                .store(false, std::sync::atomic::Ordering::Release);\n            None\n        }\n    }\n\n    /// discard the draft state\n    pub fn discard(&self) -> Option<T> {\n        let v = self.draft.write().take();\n        self.is_dirty\n            .store(false, std::sync::atomic::Ordering::Release);\n        v\n    }\n}\n\nmod test {\n    #![allow(unused)]\n    use super::ManagedState;\n    use crate::config::IVerge;\n\n    #[test]\n    fn test_managed_state() {\n        let verge = IVerge {\n            enable_auto_launch: Some(true),\n            enable_tun_mode: Some(false),\n            ..IVerge::default()\n        };\n\n        let draft = ManagedState::from(verge);\n\n        assert_eq!(draft.data().enable_auto_launch, Some(true));\n        assert_eq!(draft.data().enable_tun_mode, Some(false));\n\n        assert_eq!(draft.draft().enable_auto_launch, Some(true));\n        assert_eq!(draft.draft().enable_tun_mode, Some(false));\n\n        let mut d = draft.draft();\n        d.enable_auto_launch = Some(false);\n        d.enable_tun_mode = Some(true);\n        drop(d);\n\n        assert_eq!(draft.data().enable_auto_launch, Some(true));\n        assert_eq!(draft.data().enable_tun_mode, Some(false));\n\n        assert_eq!(draft.draft().enable_auto_launch, Some(false));\n        assert_eq!(draft.draft().enable_tun_mode, Some(true));\n\n        assert_eq!(draft.latest().enable_auto_launch, Some(false));\n        assert_eq!(draft.latest().enable_tun_mode, Some(true));\n\n        assert!(draft.apply().is_some());\n        assert!(draft.apply().is_none());\n\n        assert_eq!(draft.data().enable_auto_launch, Some(false));\n        assert_eq!(draft.data().enable_tun_mode, Some(true));\n\n        assert_eq!(draft.draft().enable_auto_launch, Some(false));\n        assert_eq!(draft.draft().enable_tun_mode, Some(true));\n\n        let mut d = draft.draft();\n        d.enable_auto_launch = Some(true);\n        drop(d);\n\n        assert_eq!(draft.data().enable_auto_launch, Some(false));\n\n        assert_eq!(draft.draft().enable_auto_launch, Some(true));\n\n        assert!(draft.discard().is_some());\n\n        assert_eq!(draft.data().enable_auto_launch, Some(false));\n\n        assert!(draft.discard().is_none());\n\n        assert_eq!(draft.draft().enable_auto_launch, Some(false));\n    }\n}\n"
  },
  {
    "path": "backend/tauri/src/core/state_v2/builder.rs",
    "content": "pub trait StateSyncBuilder: Default + Clone {\n    type State: Clone + Send + Sync + 'static;\n\n    fn build(&self) -> anyhow::Result<Self::State>;\n}\n\npub trait StateAsyncBuilder: Default + Clone {\n    type State: Clone + Send + Sync + 'static;\n\n    async fn build(&self) -> anyhow::Result<Self::State>;\n}\n\nimpl<T, S> StateAsyncBuilder for S\nwhere\n    S: StateSyncBuilder<State = T>,\n    T: Clone + Send + Sync + 'static,\n{\n    type State = T;\n    async fn build(&self) -> anyhow::Result<Self::State> {\n        self.build()\n    }\n}\n"
  },
  {
    "path": "backend/tauri/src/core/state_v2/coordinator.rs",
    "content": "use super::builder::*;\n\n#[derive(thiserror::Error, Debug)]\npub enum StateChangedError {\n    #[error(\"builder validation error: {0}\")]\n    Validation(anyhow::Error),\n    #[error(\"state migrate error: {0:#?}\")]\n    Migrate(#[from] MigrateError),\n\n    #[error(\"state migrate and rollback error: migrate {0:#?}, rollback {1:#?}\")]\n    MigrateAndRollback(MigrateError, RollbackError),\n}\n\n#[derive(thiserror::Error, Debug)]\n#[error(\"state migrate error: {name}: {error:#?}\")]\npub struct MigrateError {\n    pub name: String,\n    pub error: anyhow::Error,\n}\n\n#[derive(thiserror::Error, Debug)]\n#[error(\"state rollback error: {name}: {error:#?}\")]\npub struct RollbackError {\n    pub name: String,\n    pub error: anyhow::Error,\n}\n\n#[async_trait::async_trait]\n#[allow(unused_variables)]\npub(crate) trait StateChangedSubscriber<T: Clone + Send + Sync + 'static> {\n    /// The name of the subscriber.\n    fn name(&self) -> &str;\n\n    /// Called when the state is changed, return a Error if the state change is failed.\n    ///\n    /// While state migrate is failed, the rollback will be called.\n    ///\n    /// When the prev_state is None, it means the state is not initialized.\n    async fn migrate(&self, prev_state: Option<T>, new_state: T) -> Result<(), anyhow::Error>;\n\n    /// Called when the state migrate is failed, return a Error if the state rollback is failed.\n    ///\n    /// If the migration do not affect the real system/service, you can use the default implementation,\n    /// OR you MUST implement the rollback method.\n    async fn rollback(&self, prev_state: Option<T>, new_state: T) -> Result<(), anyhow::Error> {\n        Ok(())\n    }\n}\n\n#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)]\npub enum ConcurrencyStrategy {\n    #[default]\n    Sequential,\n    Concurrent,\n    Limited(usize),\n}\n\n#[non_exhaustive]\npub struct StateCoordinator<T: Clone + Send + Sync + 'static> {\n    current_state: Option<T>,\n    subscribers: Vec<Box<dyn StateChangedSubscriber<T> + Send + Sync>>,\n    // strategy: ConcurrencyStrategy,\n}\n\nimpl<T: Clone + Send + Sync> StateCoordinator<T> {\n    pub(super) fn new() -> Self {\n        Self {\n            current_state: None,\n            subscribers: Vec::new(),\n        }\n    }\n\n    /// Add a subscriber to the state coordinator.\n    fn add_subscriber(&mut self, subscriber: Box<dyn StateChangedSubscriber<T> + Send + Sync>) {\n        self.subscribers.push(subscriber);\n    }\n\n    /// Get the current state.\n    pub fn current_state(&self) -> Option<T> {\n        self.current_state.clone()\n    }\n\n    async fn run_migration<S>(\n        subscriber: &S,\n        current_state: Option<&T>,\n        new_state: &T,\n    ) -> Result<(), StateChangedError>\n    where\n        S: StateChangedSubscriber<T> + Send + Sync + ?Sized,\n    {\n        if let Err(e) = subscriber\n            .migrate(current_state.cloned(), new_state.clone())\n            .await\n        {\n            let migrate_error = MigrateError {\n                name: subscriber.name().to_string(),\n                error: e,\n            };\n            tracing::error!(\"migrate error: {migrate_error:#?}\");\n            if let Err(e) = subscriber\n                .rollback(current_state.cloned(), new_state.clone())\n                .await\n            {\n                tracing::error!(\"rollback error: {e:#?}\");\n                return Err(StateChangedError::MigrateAndRollback(\n                    migrate_error,\n                    RollbackError {\n                        name: subscriber.name().to_string(),\n                        error: e,\n                    },\n                ));\n            }\n            return Err(StateChangedError::Migrate(migrate_error));\n        }\n        Ok(())\n    }\n\n    /// Upsert the state by a builder, it was used for a builder was patched for upsert.\n    pub async fn upsert(\n        &mut self,\n        builder: impl StateAsyncBuilder<State = T>,\n    ) -> Result<(), StateChangedError> {\n        let new_state = builder\n            .build()\n            .await\n            .map_err(StateChangedError::Validation)?;\n\n        for subscriber in self.subscribers.iter() {\n            Self::run_migration(subscriber.as_ref(), self.current_state.as_ref(), &new_state)\n                .await?;\n        }\n\n        self.current_state = Some(new_state);\n        Ok(())\n    }\n\n    /// Upsert the state directly, it used for a small StateObject, a bool value, etc.\n    pub async fn upsert_state(&mut self, state: T) -> Result<(), StateChangedError> {\n        for subscriber in self.subscribers.iter() {\n            Self::run_migration(subscriber.as_ref(), self.current_state.as_ref(), &state).await?;\n        }\n        self.current_state = Some(state);\n        Ok(())\n    }\n}\n\n#[cfg(test)]\nmod test {\n    use super::*;\n    use std::sync::{\n        Arc,\n        atomic::{AtomicBool, AtomicUsize, Ordering},\n    };\n    use tokio::sync::Mutex;\n\n    #[derive(Debug, Clone, PartialEq)]\n    struct TestState {\n        value: i32,\n        name: String,\n    }\n\n    struct MockSubscriber {\n        name: String,\n        migrate_calls: Arc<AtomicUsize>,\n        rollback_calls: Arc<AtomicUsize>,\n        should_fail_migrate: Arc<AtomicBool>,\n        should_fail_rollback: Arc<AtomicBool>,\n        migrate_history: Arc<Mutex<Vec<(Option<TestState>, TestState)>>>,\n        rollback_history: Arc<Mutex<Vec<(Option<TestState>, TestState)>>>,\n    }\n\n    impl MockSubscriber {\n        fn new(name: &str) -> Self {\n            Self {\n                name: name.to_string(),\n                migrate_calls: Arc::new(AtomicUsize::new(0)),\n                rollback_calls: Arc::new(AtomicUsize::new(0)),\n                should_fail_migrate: Arc::new(AtomicBool::new(false)),\n                should_fail_rollback: Arc::new(AtomicBool::new(false)),\n                migrate_history: Arc::new(Mutex::new(Vec::new())),\n                rollback_history: Arc::new(Mutex::new(Vec::new())),\n            }\n        }\n\n        fn set_migrate_failure(&self, should_fail: bool) {\n            self.should_fail_migrate\n                .store(should_fail, Ordering::SeqCst);\n        }\n\n        fn set_rollback_failure(&self, should_fail: bool) {\n            self.should_fail_rollback\n                .store(should_fail, Ordering::SeqCst);\n        }\n\n        async fn get_migrate_history(&self) -> Vec<(Option<TestState>, TestState)> {\n            self.migrate_history.lock().await.clone()\n        }\n\n        async fn get_rollback_history(&self) -> Vec<(Option<TestState>, TestState)> {\n            self.rollback_history.lock().await.clone()\n        }\n\n        fn get_migrate_calls(&self) -> usize {\n            self.migrate_calls.load(Ordering::SeqCst)\n        }\n\n        fn get_rollback_calls(&self) -> usize {\n            self.rollback_calls.load(Ordering::SeqCst)\n        }\n    }\n\n    #[async_trait::async_trait]\n    impl StateChangedSubscriber<TestState> for MockSubscriber {\n        fn name(&self) -> &str {\n            &self.name\n        }\n\n        async fn migrate(\n            &self,\n            prev_state: Option<TestState>,\n            new_state: TestState,\n        ) -> Result<(), anyhow::Error> {\n            self.migrate_calls.fetch_add(1, Ordering::SeqCst);\n            self.migrate_history\n                .lock()\n                .await\n                .push((prev_state.clone(), new_state.clone()));\n\n            if self.should_fail_migrate.load(Ordering::SeqCst) {\n                return Err(anyhow::anyhow!(\"Mock migrate failure\"));\n            }\n            Ok(())\n        }\n\n        async fn rollback(\n            &self,\n            prev_state: Option<TestState>,\n            new_state: TestState,\n        ) -> Result<(), anyhow::Error> {\n            self.rollback_calls.fetch_add(1, Ordering::SeqCst);\n            self.rollback_history\n                .lock()\n                .await\n                .push((prev_state.clone(), new_state.clone()));\n\n            if self.should_fail_rollback.load(Ordering::SeqCst) {\n                return Err(anyhow::anyhow!(\"Mock rollback failure\"));\n            }\n            Ok(())\n        }\n    }\n\n    #[async_trait::async_trait]\n    impl StateChangedSubscriber<TestState> for Arc<MockSubscriber> {\n        fn name(&self) -> &str {\n            self.as_ref().name()\n        }\n\n        async fn migrate(\n            &self,\n            prev_state: Option<TestState>,\n            new_state: TestState,\n        ) -> Result<(), anyhow::Error> {\n            self.as_ref().migrate(prev_state, new_state).await\n        }\n\n        async fn rollback(\n            &self,\n            prev_state: Option<TestState>,\n            new_state: TestState,\n        ) -> Result<(), anyhow::Error> {\n            self.as_ref().rollback(prev_state, new_state).await\n        }\n    }\n\n    #[derive(Default, Clone, Debug)]\n    struct TestStateBuilder {\n        state: Option<TestState>,\n        should_fail: bool,\n    }\n\n    impl TestStateBuilder {\n        fn new(state: TestState) -> Self {\n            Self {\n                state: Some(state),\n                should_fail: false,\n            }\n        }\n\n        fn failing() -> Self {\n            Self {\n                state: None,\n                should_fail: true,\n            }\n        }\n    }\n\n    impl StateSyncBuilder for TestStateBuilder {\n        type State = TestState;\n\n        fn build(&self) -> anyhow::Result<Self::State> {\n            if self.should_fail {\n                return Err(anyhow::anyhow!(\"Builder validation failed\"));\n            }\n            Ok(self.state.clone().unwrap())\n        }\n    }\n\n    #[tokio::test]\n    async fn test_new_coordinator() {\n        let coordinator: StateCoordinator<TestState> = StateCoordinator::new();\n        let current_state = coordinator.current_state.clone();\n        assert!(current_state.is_none());\n        assert_eq!(coordinator.subscribers.len(), 0);\n    }\n\n    #[tokio::test]\n    async fn test_upsert_state_success() {\n        let mut coordinator: StateCoordinator<TestState> = StateCoordinator::new();\n        let subscriber = Arc::new(MockSubscriber::new(\"test_subscriber\"));\n        coordinator.subscribers.push(Box::new(subscriber.clone())\n            as Box<dyn StateChangedSubscriber<TestState> + Send + Sync>);\n\n        let test_state = TestState {\n            value: 42,\n            name: \"test\".to_string(),\n        };\n\n        let result = coordinator.upsert_state(test_state.clone()).await;\n        assert!(result.is_ok());\n\n        // 检查状态是否更新\n        let current_state = coordinator.current_state.clone();\n        assert_eq!(current_state, Some(test_state.clone()));\n\n        // 检查订阅者是否被调用\n        assert_eq!(subscriber.get_migrate_calls(), 1);\n        assert_eq!(subscriber.get_rollback_calls(), 0);\n\n        let history = subscriber.get_migrate_history().await;\n        assert_eq!(history.len(), 1);\n        assert_eq!(history[0], (None, test_state));\n    }\n\n    #[tokio::test]\n    async fn test_upsert_with_builder_success() {\n        let mut coordinator: StateCoordinator<TestState> = StateCoordinator::new();\n        let subscriber = Arc::new(MockSubscriber::new(\"test_subscriber\"));\n        coordinator.subscribers.push(Box::new(subscriber.clone())\n            as Box<dyn StateChangedSubscriber<TestState> + Send + Sync>);\n\n        let test_state = TestState {\n            value: 100,\n            name: \"builder_test\".to_string(),\n        };\n        let builder = TestStateBuilder::new(test_state.clone());\n\n        let result = coordinator.upsert(builder).await;\n        assert!(result.is_ok());\n\n        // 检查状态是否更新\n        let current_state = coordinator.current_state.clone();\n        assert_eq!(current_state, Some(test_state.clone()));\n\n        // 检查订阅者是否被调用\n        assert_eq!(subscriber.get_migrate_calls(), 1);\n        assert_eq!(subscriber.get_rollback_calls(), 0);\n    }\n\n    #[tokio::test]\n    async fn test_upsert_builder_validation_failure() {\n        let mut coordinator: StateCoordinator<TestState> = StateCoordinator::new();\n        let builder = TestStateBuilder::failing();\n\n        let result = coordinator.upsert(builder).await;\n        assert!(result.is_err());\n\n        match result.unwrap_err() {\n            StateChangedError::Validation(_) => {}\n            _ => panic!(\"Expected validation error\"),\n        }\n\n        // 确保状态没有改变\n        let current_state = coordinator.current_state.clone();\n        assert!(current_state.is_none());\n    }\n\n    #[tokio::test]\n    async fn test_migrate_failure_with_successful_rollback() {\n        let mut coordinator: StateCoordinator<TestState> = StateCoordinator::new();\n        let subscriber = Arc::new(MockSubscriber::new(\"failing_subscriber\"));\n        subscriber.set_migrate_failure(true);\n        coordinator.subscribers.push(Box::new(subscriber.clone())\n            as Box<dyn StateChangedSubscriber<TestState> + Send + Sync>);\n\n        let test_state = TestState {\n            value: 42,\n            name: \"test\".to_string(),\n        };\n\n        let result = coordinator.upsert_state(test_state.clone()).await;\n        assert!(result.is_err());\n\n        match result.unwrap_err() {\n            StateChangedError::Migrate(migrate_error) => {\n                assert_eq!(migrate_error.name, \"failing_subscriber\");\n            }\n            _ => panic!(\"Expected migrate error\"),\n        }\n\n        // 检查调用次数\n        assert_eq!(subscriber.get_migrate_calls(), 1);\n        assert_eq!(subscriber.get_rollback_calls(), 1);\n\n        // 确保状态没有改变\n        let current_state = coordinator.current_state.clone();\n        assert!(current_state.is_none());\n    }\n\n    #[tokio::test]\n    async fn test_migrate_failure_with_rollback_failure() {\n        let mut coordinator: StateCoordinator<TestState> = StateCoordinator::new();\n        let subscriber = Arc::new(MockSubscriber::new(\"double_failing_subscriber\"));\n        subscriber.set_migrate_failure(true);\n        subscriber.set_rollback_failure(true);\n        coordinator.subscribers.push(Box::new(subscriber.clone())\n            as Box<dyn StateChangedSubscriber<TestState> + Send + Sync>);\n\n        let test_state = TestState {\n            value: 42,\n            name: \"test\".to_string(),\n        };\n\n        let result = coordinator.upsert_state(test_state).await;\n        assert!(result.is_err());\n\n        match result.unwrap_err() {\n            StateChangedError::MigrateAndRollback(migrate_error, rollback_error) => {\n                assert_eq!(migrate_error.name, \"double_failing_subscriber\");\n                assert_eq!(rollback_error.name, \"double_failing_subscriber\");\n            }\n            _ => panic!(\"Expected migrate and rollback error\"),\n        }\n\n        // 检查调用次数\n        assert_eq!(subscriber.get_migrate_calls(), 1);\n        assert_eq!(subscriber.get_rollback_calls(), 1);\n\n        // 确保状态没有改变\n        let current_state = coordinator.current_state.clone();\n        assert!(current_state.is_none());\n    }\n\n    #[tokio::test]\n    async fn test_multiple_subscribers_success() {\n        let mut coordinator: StateCoordinator<TestState> = StateCoordinator::new();\n        let subscriber1 = Arc::new(MockSubscriber::new(\"subscriber1\"));\n        let subscriber2 = Arc::new(MockSubscriber::new(\"subscriber2\"));\n        let subscriber3 = Arc::new(MockSubscriber::new(\"subscriber3\"));\n\n        coordinator.subscribers.push(Box::new(subscriber1.clone())\n            as Box<dyn StateChangedSubscriber<TestState> + Send + Sync>);\n        coordinator.subscribers.push(Box::new(subscriber2.clone())\n            as Box<dyn StateChangedSubscriber<TestState> + Send + Sync>);\n        coordinator.subscribers.push(Box::new(subscriber3.clone())\n            as Box<dyn StateChangedSubscriber<TestState> + Send + Sync>);\n\n        let test_state = TestState {\n            value: 42,\n            name: \"multi_test\".to_string(),\n        };\n\n        let result = coordinator.upsert_state(test_state.clone()).await;\n        assert!(result.is_ok());\n\n        // 检查所有订阅者都被调用\n        assert_eq!(subscriber1.get_migrate_calls(), 1);\n        assert_eq!(subscriber2.get_migrate_calls(), 1);\n        assert_eq!(subscriber3.get_migrate_calls(), 1);\n\n        // 检查没有回滚调用\n        assert_eq!(subscriber1.get_rollback_calls(), 0);\n        assert_eq!(subscriber2.get_rollback_calls(), 0);\n        assert_eq!(subscriber3.get_rollback_calls(), 0);\n\n        // 检查状态更新\n        let current_state = coordinator.current_state.clone();\n        assert_eq!(current_state, Some(test_state));\n    }\n\n    #[tokio::test]\n    async fn test_multiple_subscribers_with_one_failure() {\n        let mut coordinator: StateCoordinator<TestState> = StateCoordinator::new();\n        let subscriber1 = Arc::new(MockSubscriber::new(\"subscriber1\"));\n        let subscriber2 = Arc::new(MockSubscriber::new(\"failing_subscriber\"));\n        let subscriber3 = Arc::new(MockSubscriber::new(\"subscriber3\"));\n\n        subscriber2.set_migrate_failure(true);\n\n        coordinator.subscribers.push(Box::new(subscriber1.clone())\n            as Box<dyn StateChangedSubscriber<TestState> + Send + Sync>);\n        coordinator.subscribers.push(Box::new(subscriber2.clone())\n            as Box<dyn StateChangedSubscriber<TestState> + Send + Sync>);\n        coordinator.subscribers.push(Box::new(subscriber3.clone())\n            as Box<dyn StateChangedSubscriber<TestState> + Send + Sync>);\n\n        let test_state = TestState {\n            value: 42,\n            name: \"multi_fail_test\".to_string(),\n        };\n\n        let result = coordinator.upsert_state(test_state).await;\n        assert!(result.is_err());\n\n        // 检查调用次数 - 只有前两个订阅者被调用\n        assert_eq!(subscriber1.get_migrate_calls(), 1);\n        assert_eq!(subscriber2.get_migrate_calls(), 1);\n        assert_eq!(subscriber3.get_migrate_calls(), 0); // 第三个不应该被调用\n\n        // 检查回滚调用\n        assert_eq!(subscriber1.get_rollback_calls(), 0);\n        assert_eq!(subscriber2.get_rollback_calls(), 1);\n        assert_eq!(subscriber3.get_rollback_calls(), 0);\n\n        // 确保状态没有改变\n        let current_state = coordinator.current_state.clone();\n        assert!(current_state.is_none());\n    }\n\n    #[tokio::test]\n    async fn test_state_update_sequence() {\n        let mut coordinator: StateCoordinator<TestState> = StateCoordinator::new();\n        let subscriber = Arc::new(MockSubscriber::new(\"sequence_subscriber\"));\n        coordinator.subscribers.push(Box::new(subscriber.clone())\n            as Box<dyn StateChangedSubscriber<TestState> + Send + Sync>);\n\n        // 第一次更新\n        let state1 = TestState {\n            value: 1,\n            name: \"first\".to_string(),\n        };\n        coordinator.upsert_state(state1.clone()).await.unwrap();\n\n        // 第二次更新\n        let state2 = TestState {\n            value: 2,\n            name: \"second\".to_string(),\n        };\n        coordinator.upsert_state(state2.clone()).await.unwrap();\n\n        // 检查历史记录\n        let history = subscriber.get_migrate_history().await;\n        assert_eq!(history.len(), 2);\n        assert_eq!(history[0], (None, state1.clone()));\n        assert_eq!(history[1], (Some(state1), state2.clone()));\n\n        // 检查当前状态\n        let current_state = coordinator.current_state.clone();\n        assert_eq!(current_state, Some(state2));\n    }\n\n    #[tokio::test]\n    async fn test_error_display() {\n        let migrate_error = MigrateError {\n            name: \"test_subscriber\".to_string(),\n            error: anyhow::anyhow!(\"test error\"),\n        };\n        let error_string = format!(\"{}\", migrate_error);\n        assert!(error_string.contains(\"state migrate error: test_subscriber\"));\n\n        let rollback_error = RollbackError {\n            name: \"test_subscriber\".to_string(),\n            error: anyhow::anyhow!(\"rollback error\"),\n        };\n        let error_string = format!(\"{}\", rollback_error);\n        assert!(error_string.contains(\"state rollback error: test_subscriber\"));\n\n        let state_error = StateChangedError::Migrate(migrate_error);\n        let error_string = format!(\"{}\", state_error);\n        assert!(error_string.contains(\"state migrate error\"));\n    }\n\n    #[tokio::test]\n    async fn test_sync_builder_to_async_conversion() {\n        let mut coordinator: StateCoordinator<TestState> = StateCoordinator::new();\n        let test_state = TestState {\n            value: 123,\n            name: \"sync_to_async\".to_string(),\n        };\n        let sync_builder = TestStateBuilder::new(test_state.clone());\n\n        // 通过 StateAsyncBuilder trait 使用同步构建器\n        let result = coordinator.upsert(sync_builder).await;\n        assert!(result.is_ok());\n\n        let current_state = coordinator.current_state.clone();\n        assert_eq!(current_state, Some(test_state));\n    }\n\n    #[tokio::test]\n    async fn test_add_subscriber() {\n        let mut coordinator: StateCoordinator<TestState> = StateCoordinator::new();\n        let subscriber1 = Arc::new(MockSubscriber::new(\"subscriber1\"));\n        let subscriber2 = Arc::new(MockSubscriber::new(\"subscriber2\"));\n\n        assert_eq!(coordinator.subscribers.len(), 0);\n\n        coordinator.add_subscriber(Box::new(subscriber1.clone()));\n        assert_eq!(coordinator.subscribers.len(), 1);\n\n        coordinator.add_subscriber(Box::new(subscriber2.clone()));\n        assert_eq!(coordinator.subscribers.len(), 2);\n\n        // 测试添加的订阅者是否工作\n        let test_state = TestState {\n            value: 42,\n            name: \"add_test\".to_string(),\n        };\n\n        let result = coordinator.upsert_state(test_state.clone()).await;\n        assert!(result.is_ok());\n\n        // 检查两个订阅者都被调用\n        assert_eq!(subscriber1.get_migrate_calls(), 1);\n        assert_eq!(subscriber2.get_migrate_calls(), 1);\n    }\n\n    #[tokio::test]\n    async fn test_get_state() {\n        let mut coordinator: StateCoordinator<TestState> = StateCoordinator::new();\n\n        // 初始状态应该是 None\n        let initial_state = coordinator.current_state();\n        assert!(initial_state.is_none());\n\n        // 设置状态后应该能获取到\n        let test_state = TestState {\n            value: 100,\n            name: \"get_test\".to_string(),\n        };\n\n        coordinator.upsert_state(test_state.clone()).await.unwrap();\n        let retrieved_state = coordinator.current_state();\n        assert_eq!(retrieved_state, Some(test_state.clone()));\n\n        // 更新状态后应该获取到新状态\n        let new_state = TestState {\n            value: 200,\n            name: \"updated_test\".to_string(),\n        };\n\n        coordinator.upsert_state(new_state.clone()).await.unwrap();\n        let updated_retrieved_state = coordinator.current_state();\n        assert_eq!(updated_retrieved_state, Some(new_state));\n    }\n\n    #[tokio::test]\n    async fn test_empty_subscribers_list() {\n        let mut coordinator: StateCoordinator<TestState> = StateCoordinator::new();\n        let test_state = TestState {\n            value: 42,\n            name: \"no_subscribers\".to_string(),\n        };\n\n        // 没有订阅者时更新状态应该成功\n        let result = coordinator.upsert_state(test_state.clone()).await;\n        assert!(result.is_ok());\n\n        let current_state = coordinator.current_state();\n        assert_eq!(current_state, Some(test_state));\n    }\n}\n"
  },
  {
    "path": "backend/tauri/src/core/state_v2/manager/persistent.rs",
    "content": "use anyhow::Context;\nuse camino::Utf8PathBuf;\nuse serde::{Serialize, de::DeserializeOwned};\n\nuse crate::utils::help;\n\nuse super::*;\n\n#[derive(thiserror::Error, Debug)]\npub enum UpsertError {\n    #[error(\"state changed error: {0}\")]\n    State(StateChangedError),\n    #[error(\"write config error: {0}\")]\n    WriteConfig(anyhow::Error),\n}\n\npub struct PersistentStateManager<\n    State: Clone + Send + Sync + 'static,\n    Builder: StateAsyncBuilder<State = State> + Serialize + DeserializeOwned,\n> {\n    config_prefix: Option<String>,\n    config_path: Utf8PathBuf,\n    current_builder: Option<Builder>,\n    state_coordinator: StateCoordinator<State>,\n}\n\nimpl<State, Builder> PersistentStateManager<State, Builder>\nwhere\n    State: Clone + Send + Sync + 'static,\n    Builder: StateAsyncBuilder<State = State> + Serialize + DeserializeOwned,\n{\n    pub fn new(\n        config_prefix: Option<String>,\n        config_path: Utf8PathBuf,\n        state_coordinator: StateCoordinator<State>,\n    ) -> Self {\n        Self {\n            config_prefix,\n            config_path,\n            current_builder: None,\n            state_coordinator,\n        }\n    }\n\n    pub async fn try_load(&mut self) -> anyhow::Result<()> {\n        let config: Builder =\n            help::read_yaml(&self.config_path).context(\"failed to read the config file\")?;\n\n        self.state_coordinator.upsert(config.clone()).await?;\n\n        self.current_builder = Some(config);\n        Ok(())\n    }\n\n    pub async fn try_load_with_defaults(&mut self) -> anyhow::Result<()> {\n        let config: Builder = help::read_yaml(&self.config_path)\n            .inspect_err(|e| {\n                log::error!(target: \"app\", \"failed to read the config file: {e:?}\");\n            })\n            .unwrap_or_else(|_| Builder::default());\n\n        self.state_coordinator.upsert(config.clone()).await?;\n\n        self.current_builder = Some(config);\n        Ok(())\n    }\n\n    async fn write_config(&self, builder: Builder) -> anyhow::Result<()> {\n        help::save_yaml(&self.config_path, &builder, self.config_prefix.as_deref())?;\n        Ok(())\n    }\n\n    pub fn current_state(&self) -> Option<State> {\n        self.state_coordinator.current_state()\n    }\n\n    pub async fn upsert(&mut self, builder: Builder) -> Result<(), UpsertError> {\n        self.state_coordinator\n            .upsert(builder.clone())\n            .await\n            .map_err(UpsertError::State)?;\n        self.current_builder = Some(builder.clone());\n\n        self.write_config(builder)\n            .await\n            .map_err(UpsertError::WriteConfig)?;\n        Ok(())\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use serde::{Deserialize, Serialize};\n    use std::sync::Arc;\n    use tempfile::tempdir;\n    use tokio::fs;\n\n    // 测试用的状态结构\n    #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]\n    struct TestState {\n        name: String,\n        value: i32,\n    }\n\n    // 测试用的构建器\n    #[derive(Debug, Clone, Default, Serialize, Deserialize)]\n    struct TestBuilder {\n        name: String,\n        value: i32,\n        should_fail: bool,\n    }\n\n    impl TestBuilder {\n        fn new(name: String, value: i32) -> Self {\n            Self {\n                name,\n                value,\n                should_fail: false,\n            }\n        }\n\n        fn failing() -> Self {\n            Self {\n                name: \"\".to_string(),\n                value: 0,\n                should_fail: true,\n            }\n        }\n    }\n\n    impl StateAsyncBuilder for TestBuilder {\n        type State = TestState;\n\n        async fn build(&self) -> anyhow::Result<Self::State> {\n            if self.should_fail {\n                return Err(anyhow::anyhow!(\"构建失败\"));\n            }\n            Ok(TestState {\n                name: self.name.clone(),\n                value: self.value,\n            })\n        }\n    }\n\n    // 辅助函数：创建临时配置文件\n    async fn create_temp_config_file(\n        builder: &TestBuilder,\n    ) -> anyhow::Result<(Utf8PathBuf, tempfile::TempDir)> {\n        let temp_dir = tempdir()?;\n        let config_path = temp_dir.path().join(\"test_config.yaml\");\n        let config_path = Utf8PathBuf::from_path_buf(config_path).unwrap();\n\n        help::save_yaml(&config_path, builder, None)?;\n        Ok((config_path, temp_dir))\n    }\n\n    #[tokio::test]\n    async fn test_new_persistent_state_manager() {\n        let coordinator: StateCoordinator<TestState> = StateCoordinator::new();\n        let config_path = Utf8PathBuf::from(\"/tmp/test_config.yaml\");\n\n        let manager: PersistentStateManager<TestState, TestBuilder> = PersistentStateManager::new(\n            Some(\"# 测试配置\".to_string()),\n            config_path.clone(),\n            coordinator,\n        );\n\n        // 验证初始状态\n        assert_eq!(manager.config_prefix, Some(\"# 测试配置\".to_string()));\n        assert_eq!(manager.config_path, config_path);\n        assert!(manager.current_builder.is_none());\n        assert!(manager.current_state().is_none());\n    }\n\n    #[tokio::test]\n    async fn test_try_load_success() {\n        let builder = TestBuilder::new(\"测试\".to_string(), 42);\n        let (config_path, _temp_dir) = create_temp_config_file(&builder).await.unwrap();\n\n        let coordinator: StateCoordinator<TestState> = StateCoordinator::new();\n        let mut manager: PersistentStateManager<TestState, TestBuilder> =\n            PersistentStateManager::new(None, config_path, coordinator);\n\n        // 测试成功加载\n        let result = manager.try_load().await;\n        assert!(result.is_ok(), \"加载配置应该成功\");\n\n        // 验证状态\n        let current_state = manager.current_state();\n        assert!(current_state.is_some());\n        let state = current_state.unwrap();\n        assert_eq!(state.name, \"测试\");\n        assert_eq!(state.value, 42);\n\n        // 验证构建器\n        let current_builder = manager.current_builder.as_ref();\n        assert!(current_builder.is_some());\n        let loaded_builder: &TestBuilder = current_builder.as_ref().unwrap();\n        assert_eq!(loaded_builder.name, \"测试\");\n        assert_eq!(loaded_builder.value, 42);\n    }\n\n    #[tokio::test]\n    async fn test_try_load_file_not_exist() {\n        let config_path = Utf8PathBuf::from(\"/nonexistent/config.yaml\");\n        let coordinator: StateCoordinator<TestState> = StateCoordinator::new();\n        let mut manager: PersistentStateManager<TestState, TestBuilder> =\n            PersistentStateManager::new(None, config_path, coordinator);\n\n        // 测试文件不存在的情况\n        let result = manager.try_load().await;\n        assert!(result.is_err(), \"加载不存在的配置文件应该失败\");\n\n        let error_msg = result.unwrap_err().to_string();\n        assert!(error_msg.contains(\"failed to read the config file\"));\n    }\n\n    #[tokio::test]\n    async fn test_try_load_with_defaults_success() {\n        let builder = TestBuilder::new(\"默认测试\".to_string(), 100);\n        let (config_path, _temp_dir) = create_temp_config_file(&builder).await.unwrap();\n\n        let coordinator: StateCoordinator<TestState> = StateCoordinator::new();\n        let mut manager: PersistentStateManager<TestState, TestBuilder> =\n            PersistentStateManager::new(None, config_path, coordinator);\n\n        // 测试使用默认值加载\n        let result = manager.try_load_with_defaults().await;\n        assert!(result.is_ok(), \"使用默认值加载应该成功\");\n\n        // 验证状态\n        let current_state = manager.current_state();\n        assert!(current_state.is_some());\n        let state = current_state.unwrap();\n        assert_eq!(state.name, \"默认测试\");\n        assert_eq!(state.value, 100);\n    }\n\n    #[tokio::test]\n    async fn test_try_load_with_defaults_file_not_exist() {\n        let config_path = Utf8PathBuf::from(\"/nonexistent/config.yaml\");\n        let coordinator: StateCoordinator<TestState> = StateCoordinator::new();\n        let mut manager: PersistentStateManager<TestState, TestBuilder> =\n            PersistentStateManager::new(None, config_path, coordinator);\n\n        // 测试文件不存在时使用默认值\n        let result = manager.try_load_with_defaults().await;\n        assert!(result.is_ok(), \"文件不存在时使用默认值应该成功\");\n\n        // 验证使用了默认值\n        let current_state = manager.current_state();\n        assert!(current_state.is_some());\n        let state = current_state.unwrap();\n        assert_eq!(state.name, \"\"); // 默认值\n        assert_eq!(state.value, 0); // 默认值\n    }\n\n    #[tokio::test]\n    async fn test_upsert_success() {\n        let temp_dir = tempdir().unwrap();\n        let config_path = temp_dir.path().join(\"upsert_test.yaml\");\n        let config_path = Utf8PathBuf::from_path_buf(config_path).unwrap();\n\n        let coordinator: StateCoordinator<TestState> = StateCoordinator::new();\n        let mut manager: PersistentStateManager<TestState, TestBuilder> =\n            PersistentStateManager::new(\n                Some(\"# 更新测试\".to_string()),\n                config_path.clone(),\n                coordinator,\n            );\n\n        let builder = TestBuilder::new(\"更新测试\".to_string(), 200);\n\n        // 测试更新操作\n        let result = manager.upsert(builder.clone()).await;\n        assert!(result.is_ok(), \"更新操作应该成功\");\n\n        // 验证状态更新\n        let current_state = manager.current_state();\n        assert!(current_state.is_some());\n        let state = current_state.unwrap();\n        assert_eq!(state.name, \"更新测试\");\n        assert_eq!(state.value, 200);\n\n        // 验证构建器更新\n        let current_builder = manager.current_builder.as_ref();\n        assert!(current_builder.is_some());\n        let updated_builder = current_builder.as_ref().unwrap();\n        assert_eq!(updated_builder.name, \"更新测试\");\n        assert_eq!(updated_builder.value, 200);\n\n        // 验证配置文件已保存\n        assert!(config_path.exists(), \"配置文件应该被创建\");\n        let saved_builder: TestBuilder = help::read_yaml(&config_path).unwrap();\n        assert_eq!(saved_builder.name, \"更新测试\");\n        assert_eq!(saved_builder.value, 200);\n    }\n\n    #[tokio::test]\n    async fn test_upsert_builder_validation_error() {\n        let temp_dir = tempdir().unwrap();\n        let config_path = temp_dir.path().join(\"upsert_fail_test.yaml\");\n        let config_path = Utf8PathBuf::from_path_buf(config_path).unwrap();\n\n        let coordinator: StateCoordinator<TestState> = StateCoordinator::new();\n        let mut manager: PersistentStateManager<TestState, TestBuilder> =\n            PersistentStateManager::new(None, config_path, coordinator);\n\n        let failing_builder = TestBuilder::failing();\n\n        // 测试构建器验证失败\n        let result = manager.upsert(failing_builder).await;\n        assert!(result.is_err(), \"构建器验证失败时更新应该失败\");\n\n        match result.unwrap_err() {\n            UpsertError::State(StateChangedError::Validation(_)) => {\n                // 期望的错误类型\n            }\n            other => panic!(\n                \"期望 UpsertError::State(StateChangedError::Validation), 但得到: {:?}\",\n                other\n            ),\n        }\n\n        // 验证状态未改变\n        assert!(manager.current_state().is_none());\n        assert!(manager.current_builder.as_ref().is_none());\n    }\n\n    #[tokio::test]\n    async fn test_upsert_write_config_error() {\n        // 使用只读目录路径来触发写入错误\n        let config_path = Utf8PathBuf::from(\"/proc/version\"); // Linux 系统上的只读文件\n\n        let coordinator: StateCoordinator<TestState> = StateCoordinator::new();\n        let mut manager: PersistentStateManager<TestState, TestBuilder> =\n            PersistentStateManager::new(None, config_path, coordinator);\n\n        let builder = TestBuilder::new(\"写入失败测试\".to_string(), 300);\n\n        // 在某些系统上这可能不会失败，所以我们只测试逻辑\n        let result = manager.upsert(builder).await;\n\n        // 如果写入失败，应该得到 WriteConfig 错误\n        if result.is_err() {\n            match result.unwrap_err() {\n                UpsertError::WriteConfig(_) => {\n                    // 期望的错误类型\n                }\n                UpsertError::State(_) => {\n                    // 状态更新可能成功，但写入失败\n                }\n            }\n        }\n    }\n\n    #[tokio::test]\n    async fn test_current_state() {\n        let coordinator: StateCoordinator<TestState> = StateCoordinator::new();\n        let config_path = Utf8PathBuf::from(\"/tmp/current_state_test.yaml\");\n        let mut manager: PersistentStateManager<TestState, TestBuilder> =\n            PersistentStateManager::new(None, config_path, coordinator);\n\n        // 初始状态应该为 None\n        assert!(manager.current_state().is_none());\n\n        // 添加状态后应该能获取到\n        let builder = TestBuilder::new(\"当前状态测试\".to_string(), 400);\n        let _ = manager.upsert(builder).await;\n\n        let current_state = manager.current_state();\n        assert!(current_state.is_some());\n        let state = current_state.unwrap();\n        assert_eq!(state.name, \"当前状态测试\");\n        assert_eq!(state.value, 400);\n    }\n\n    #[tokio::test]\n    async fn test_multiple_upserts() {\n        let temp_dir = tempdir().unwrap();\n        let config_path = temp_dir.path().join(\"multiple_upserts_test.yaml\");\n        let config_path = Utf8PathBuf::from_path_buf(config_path).unwrap();\n\n        let coordinator: StateCoordinator<TestState> = StateCoordinator::new();\n        let mut manager: PersistentStateManager<TestState, TestBuilder> =\n            PersistentStateManager::new(\n                Some(\"# 多次更新测试\".to_string()),\n                config_path.clone(),\n                coordinator,\n            );\n\n        // 第一次更新\n        let builder1 = TestBuilder::new(\"第一次\".to_string(), 1);\n        let result1 = manager.upsert(builder1).await;\n        assert!(result1.is_ok());\n\n        let state1 = manager.current_state().unwrap();\n        assert_eq!(state1.name, \"第一次\");\n        assert_eq!(state1.value, 1);\n\n        // 第二次更新\n        let builder2 = TestBuilder::new(\"第二次\".to_string(), 2);\n        let result2 = manager.upsert(builder2).await;\n        assert!(result2.is_ok());\n\n        let state2 = manager.current_state().unwrap();\n        assert_eq!(state2.name, \"第二次\");\n        assert_eq!(state2.value, 2);\n\n        // 验证配置文件包含最新的值\n        let saved_builder: TestBuilder = help::read_yaml(&config_path).unwrap();\n        assert_eq!(saved_builder.name, \"第二次\");\n        assert_eq!(saved_builder.value, 2);\n    }\n\n    #[tokio::test]\n    async fn test_config_prefix_in_saved_file() {\n        let temp_dir = tempdir().unwrap();\n        let config_path = temp_dir.path().join(\"prefix_test.yaml\");\n        let config_path = Utf8PathBuf::from_path_buf(config_path).unwrap();\n\n        let coordinator: StateCoordinator<TestState> = StateCoordinator::new();\n        let prefix = \"# 这是一个测试配置文件\\n# 请勿手动修改\";\n        let mut manager: PersistentStateManager<TestState, TestBuilder> =\n            PersistentStateManager::new(Some(prefix.to_string()), config_path.clone(), coordinator);\n\n        let builder = TestBuilder::new(\"前缀测试\".to_string(), 500);\n        let result = manager.upsert(builder).await;\n        assert!(result.is_ok());\n\n        // 验证保存的文件包含前缀\n        let file_content = fs::read_to_string(&config_path).await.unwrap();\n        assert!(file_content.starts_with(\"# 这是一个测试配置文件\"));\n        assert!(file_content.contains(\"# 请勿手动修改\"));\n        assert!(file_content.contains(\"name: 前缀测试\"));\n    }\n}\n"
  },
  {
    "path": "backend/tauri/src/core/state_v2/manager/simple.rs",
    "content": "use super::*;\n\n#[repr(transparent)]\npub struct SimpleStateManager<State: Clone + Send + Sync + 'static> {\n    state_coordinator: StateCoordinator<State>,\n}\n\nimpl<State: Clone + Send + Sync + 'static> SimpleStateManager<State> {\n    pub fn new(state_coordinator: StateCoordinator<State>) -> Self {\n        Self { state_coordinator }\n    }\n\n    pub fn current_state(&self) -> Option<State> {\n        self.state_coordinator.current_state()\n    }\n\n    pub async fn upsert(&mut self, state: State) -> Result<(), StateChangedError> {\n        self.state_coordinator.upsert_state(state).await\n    }\n}\n"
  },
  {
    "path": "backend/tauri/src/core/state_v2/manager.rs",
    "content": "mod persistent;\nmod simple;\n\nuse super::{builder::*, coordinator::*};\n\npub use persistent::*;\npub use simple::*;\n"
  },
  {
    "path": "backend/tauri/src/core/state_v2/mod.rs",
    "content": "mod builder;\nmod coordinator;\nmod manager;\n\npub use coordinator::*;\n"
  },
  {
    "path": "backend/tauri/src/core/storage.rs",
    "content": "use crate::{log_err, utils::dirs};\nuse anyhow::Context;\nuse redb::{ReadableDatabase, ReadableTable, TableDefinition};\nuse serde::{Deserialize, Serialize, de::DeserializeOwned};\nuse specta::Type;\nuse std::{fs, ops::Deref, result::Result as StdResult, sync::Arc};\nuse tauri::Manager;\nuse tauri_specta::Event;\n\n#[derive(Debug, thiserror::Error)]\npub enum StorageOperationError {\n    #[error(\"failed to open database: {0}\")]\n    OpenDatabase(#[from] redb::DatabaseError),\n    #[error(\"internal redb error: {0}\")]\n    Redb(#[from] redb::Error),\n    #[error(\"internal redb table error: {0}\")]\n    RedbTable(#[from] redb::TableError),\n    #[error(\"internal redb storage error: {0}\")]\n    RedbStorage(#[from] redb::StorageError),\n    #[error(\"failed to start transaction: {0}\")]\n    RedbTransaction(#[from] redb::TransactionError),\n    #[error(\"failed to commit transaction: {0}\")]\n    RedbCommit(#[from] redb::CommitError),\n    #[error(\"failed to serialize or deserialize data: {0}\")]\n    Serialize(#[from] serde_json::Error),\n}\n\npub const NYANPASU_TABLE: TableDefinition<&[u8], &[u8]> = TableDefinition::new(\"clash-nyanpasu\");\n\ntype Result<T> = StdResult<T, StorageOperationError>;\n\n/// storage is a wrapper or called a facade for the rocksdb\n/// Maybe provide a facade for a kv storage is a good idea?\n#[derive(Clone)]\npub struct Storage {\n    inner: Arc<StorageInner>,\n}\n\nimpl Storage {\n    pub fn try_new(path: &std::path::Path) -> Result<Self> {\n        let inner = StorageInner::try_new(path)?;\n        Ok(Self {\n            inner: Arc::new(inner),\n        })\n    }\n}\n\nimpl Deref for Storage {\n    type Target = Arc<StorageInner>;\n\n    fn deref(&self) -> &Self::Target {\n        &self.inner\n    }\n}\n\npub struct StorageInner {\n    instance: redb::Database,\n    tx: tokio::sync::broadcast::Sender<(String, Option<Vec<u8>>)>,\n}\n\n/// Event emitted to all windows when a storage value changes.\n/// Event name: `storage-value-changed-event`\n#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]\npub struct StorageValueChangedEvent {\n    pub key: String,\n    /// The new JSON-encoded value, or `None` if the key was removed.\n    pub value: Option<String>,\n}\n\npub trait WebStorage {\n    fn get_item<T: DeserializeOwned>(&self, key: impl AsRef<str>) -> Result<Option<T>>;\n    fn set_item<T: Serialize>(&self, key: impl AsRef<str>, value: &T) -> Result<()>;\n    fn remove_item(&self, key: impl AsRef<str>) -> Result<()>;\n    /// Returns all key-value pairs as raw JSON strings (for debug use).\n    fn get_all(&self) -> Result<Vec<(String, String)>>;\n    /// Removes all entries from the storage (for debug use).\n    fn clear(&self) -> Result<()>;\n}\n\nimpl StorageInner {\n    fn create_and_init_database(path: &std::path::Path) -> Result<redb::Database> {\n        let db = redb::Database::create(path)?;\n        // Create table\n        let write_txn = db.begin_write()?;\n        write_txn.open_table(NYANPASU_TABLE)?;\n        write_txn.commit()?;\n        Ok(db)\n    }\n\n    pub fn try_new(path: &std::path::Path) -> Result<Self> {\n        let metadata = fs::metadata(path).ok();\n        let instance: redb::Database = if metadata.as_ref().is_some_and(|m| m.is_file()) {\n            match redb::Database::open(path) {\n                Ok(db) => db,\n                // In redb v3 upgrading point, we only store the task history, and frontend persist state,\n                // such as memorized router, which is NOT very valuable to make us keep two redb versions,\n                // intended to support upgrade database formats.\n                Err(redb::DatabaseError::UpgradeRequired(ver)) => {\n                    tracing::error!(\"database upgrade required {ver:?}, removing...\");\n                    fs::remove_file(path).unwrap();\n                    Self::create_and_init_database(path)?\n                }\n                Err(e) => return Err(e.into()),\n            }\n        } else {\n            // Remove previous rocksdb files\n            if metadata.is_some_and(|m| m.is_dir()) {\n                fs::remove_dir_all(path).unwrap();\n            }\n            Self::create_and_init_database(path)?\n        };\n        Ok(Self {\n            instance,\n            tx: tokio::sync::broadcast::channel(16).0,\n        })\n    }\n\n    pub fn get_instance(&self) -> &redb::Database {\n        &self.instance\n    }\n\n    fn notify_subscribers(&self, key: impl AsRef<str>, value: Option<&[u8]>) {\n        let key = key.as_ref().to_string();\n        let value = value.map(|v| v.to_vec());\n        let tx = self.tx.clone();\n        std::thread::spawn(move || {\n            let _ = tx.send((key, value));\n        });\n    }\n\n    fn get_rx(&self) -> tokio::sync::broadcast::Receiver<(String, Option<Vec<u8>>)> {\n        self.tx.subscribe()\n    }\n}\n\nimpl WebStorage for StorageInner {\n    fn get_item<T: DeserializeOwned>(&self, key: impl AsRef<str>) -> Result<Option<T>> {\n        let key = key.as_ref().as_bytes();\n        let db = self.get_instance();\n        let read_txn = db.begin_read()?;\n        let table = read_txn.open_table(NYANPASU_TABLE)?;\n        let result = table.get(key)?;\n        match result {\n            Some(value) => {\n                let value = value.value();\n                let value = serde_json::from_slice(value)?;\n                Ok(Some(value))\n            }\n            None => Ok(None),\n        }\n    }\n\n    fn set_item<T: Serialize>(&self, key: impl AsRef<str>, value: &T) -> Result<()> {\n        let key_str = key.as_ref();\n        let key = key_str.as_bytes();\n        let value = serde_json::to_vec(value)?;\n        let db = self.get_instance();\n        let write_txn = db.begin_write()?;\n        {\n            let mut table = write_txn.open_table(NYANPASU_TABLE)?;\n            table.insert(key, &*value)?;\n        }\n        write_txn.commit()?;\n        self.notify_subscribers(key_str, Some(&value));\n        Ok(())\n    }\n\n    fn remove_item(&self, key: impl AsRef<str>) -> Result<()> {\n        let key_str = key.as_ref();\n        let key = key_str.as_bytes();\n        let db = self.get_instance();\n        let write_txn = db.begin_write()?;\n        {\n            let mut table = write_txn.open_table(NYANPASU_TABLE)?;\n            table.remove(key)?;\n        }\n        write_txn.commit()?;\n        self.notify_subscribers(key_str, None);\n        Ok(())\n    }\n\n    fn get_all(&self) -> Result<Vec<(String, String)>> {\n        let db = self.get_instance();\n        let read_txn = db.begin_read()?;\n        let table = read_txn.open_table(NYANPASU_TABLE)?;\n        let mut result = Vec::new();\n        for entry in table.iter()? {\n            let (key, value) = entry?;\n            let key = String::from_utf8_lossy(key.value()).to_string();\n            let value = String::from_utf8_lossy(value.value()).to_string();\n            result.push((key, value));\n        }\n        Ok(result)\n    }\n\n    fn clear(&self) -> Result<()> {\n        let db = self.get_instance();\n        // Collect all keys in a read transaction first\n        let keys: Vec<Vec<u8>> = {\n            let read_txn = db.begin_read()?;\n            let table = read_txn.open_table(NYANPASU_TABLE)?;\n            let mut keys = Vec::new();\n            for entry in table.iter()? {\n                let (key, _) = entry?;\n                keys.push(key.value().to_vec());\n            }\n            keys\n        };\n        // Remove all in a write transaction\n        let write_txn = db.begin_write()?;\n        {\n            let mut table = write_txn.open_table(NYANPASU_TABLE)?;\n            for key in &keys {\n                table.remove(key.as_slice())?;\n            }\n        }\n        write_txn.commit()?;\n        Ok(())\n    }\n}\n\npub fn register_web_storage_listener(app_handle: &tauri::AppHandle) {\n    let storage = app_handle.state::<Storage>();\n    let rx = storage.get_rx();\n    let app_handle = app_handle.clone();\n    std::thread::spawn(move || {\n        nyanpasu_utils::runtime::block_on(async {\n            let mut rx = rx;\n\n            while let Ok((key, value)) = rx.recv().await {\n                let value = value.map(|v| String::from_utf8_lossy(&v).to_string());\n                let event = StorageValueChangedEvent { key, value };\n                log_err!(\n                    event.emit(&app_handle),\n                    \"failed to emit storage_value_changed event\"\n                );\n            }\n        });\n    });\n}\n\npub fn setup<R: tauri::Runtime, M: tauri::Manager<R>>(app: &M) -> anyhow::Result<()> {\n    let storage_path = dirs::storage_path().context(\"failed to get storage path\")?;\n    let storage = Storage::try_new(&storage_path)?;\n    app.manage(storage);\n    Ok(())\n}\n"
  },
  {
    "path": "backend/tauri/src/core/sysopt.rs",
    "content": "use crate::{config::Config, log_err};\nuse anyhow::{Result, anyhow};\nuse auto_launch::{AutoLaunch, AutoLaunchBuilder};\nuse once_cell::sync::OnceCell;\nuse parking_lot::Mutex;\nuse std::sync::Arc;\nuse sysproxy::Sysproxy;\nuse tauri::{async_runtime::Mutex as TokioMutex, utils::platform::current_exe};\n\n// Import PAC manager\n#[cfg(feature = \"default-meta\")]\nuse crate::core::pac::PacManager;\n\n#[cfg(target_os = \"linux\")]\nuse std::process::Command;\n\npub struct Sysopt {\n    /// current system proxy setting\n    cur_sysproxy: Arc<Mutex<Option<Sysproxy>>>,\n\n    /// record the original system proxy\n    /// recover it when exit\n    old_sysproxy: Arc<Mutex<Option<Sysproxy>>>,\n\n    /// helps to auto launch the app\n    auto_launch: Arc<Mutex<Option<AutoLaunch>>>,\n\n    /// record whether the guard async is running or not\n    guard_state: Arc<TokioMutex<bool>>,\n}\n\n#[cfg(target_os = \"windows\")]\nstatic DEFAULT_BYPASS: &str = \"localhost;127.*;192.168.*;10.*;172.16.*;172.17.*;172.18.*;172.19.*;172.20.*;172.21.*;172.22.*;172.23.*;172.24.*;172.25.*;172.26.*;172.27.*;172.28.*;172.29.*;172.30.*;172.31.*;<local>\";\n#[cfg(target_os = \"linux\")]\nstatic DEFAULT_BYPASS: &str = \"localhost,127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,::1\";\n#[cfg(target_os = \"macos\")]\nstatic DEFAULT_BYPASS: &str =\n    \"127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,localhost,*.local,*.crashlytics.com,<local>\";\n\n#[cfg(target_os = \"linux\")]\nfn detect_desktop_environment() -> String {\n    std::env::var(\"XDG_CURRENT_DESKTOP\")\n        .or_else(|_| std::env::var(\"DESKTOP_SESSION\"))\n        .unwrap_or_else(|_| \"unknown\".to_string())\n        .to_lowercase()\n}\n\n#[cfg(target_os = \"linux\")]\nfn get_autostart_requirements(desktop_env: &str) -> (bool, Vec<String>) {\n    match desktop_env {\n        \"kde\" | \"plasma\" => {\n            // KDE 可能需要特殊的桌面文件格式或权限\n            (true, vec![\"X-KDE-autostart-after=panel\".to_string()])\n        }\n        _ => (false, vec![]),\n    }\n}\n\nimpl Sysopt {\n    pub fn global() -> &'static Sysopt {\n        static SYSOPT: OnceCell<Sysopt> = OnceCell::new();\n\n        SYSOPT.get_or_init(|| Sysopt {\n            cur_sysproxy: Arc::new(Mutex::new(None)),\n            old_sysproxy: Arc::new(Mutex::new(None)),\n            auto_launch: Arc::new(Mutex::new(None)),\n            guard_state: Arc::new(TokioMutex::new(false)),\n        })\n    }\n\n    /// init the sysproxy\n    pub fn init_sysproxy(&self) -> Result<()> {\n        // Check if PAC is enabled first\n        #[cfg(feature = \"default-meta\")]\n        if PacManager::is_pac_enabled() {\n            log::info!(target: \"app\", \"Initializing PAC proxy\");\n            // For PAC, we don't set the regular system proxy\n            // Instead, we let the PAC manager handle it\n            tauri::async_runtime::spawn(async {\n                if let Err(e) = PacManager::init_pac_proxy().await {\n                    log::error!(target: \"app\", \"Failed to initialize PAC proxy: {}\", e);\n                }\n            });\n            // run the system proxy guard\n            self.guard_proxy();\n            return Ok(());\n        }\n\n        let port = Config::verge()\n            .latest()\n            .verge_mixed_port\n            .unwrap_or(Config::clash().data().get_mixed_port());\n\n        let (enable, bypass) = {\n            let verge = Config::verge();\n            let verge = verge.latest();\n            (\n                verge.enable_system_proxy.unwrap_or(false),\n                verge.system_proxy_bypass.clone(),\n            )\n        };\n\n        let current = Sysproxy {\n            enable,\n            host: String::from(\"127.0.0.1\"),\n            port,\n            bypass: bypass.unwrap_or(DEFAULT_BYPASS.into()),\n        };\n\n        if enable {\n            let old = Sysproxy::get_system_proxy().ok();\n            if let Err(e) = current.set_system_proxy() {\n                log::error!(target: \"app\", \"Failed to set system proxy: {}\", e);\n                return Err(e.into()); // Convert sysproxy::Error to anyhow::Error\n            }\n\n            *self.old_sysproxy.lock() = old;\n            *self.cur_sysproxy.lock() = Some(current);\n        }\n\n        // run the system proxy guard\n        self.guard_proxy();\n        Ok(())\n    }\n\n    /// update the system proxy\n    pub fn update_sysproxy(&self) -> Result<()> {\n        // Check if PAC is enabled first\n        #[cfg(feature = \"default-meta\")]\n        if PacManager::is_pac_enabled() {\n            log::info!(target: \"app\", \"Updating PAC proxy\");\n            // For PAC, we don't set the regular system proxy\n            // Instead, we let the PAC manager handle it\n            tauri::async_runtime::spawn(async {\n                log_err!(PacManager::update_pac().await);\n            });\n            return Ok(());\n        }\n\n        let mut cur_sysproxy = self.cur_sysproxy.lock();\n        let old_sysproxy = self.old_sysproxy.lock();\n\n        if cur_sysproxy.is_none() || old_sysproxy.is_none() {\n            drop(cur_sysproxy);\n            drop(old_sysproxy);\n            return self.init_sysproxy();\n        }\n\n        let (enable, bypass) = {\n            let verge = Config::verge();\n            let verge = verge.latest();\n            (\n                verge.enable_system_proxy.unwrap_or(false),\n                verge.system_proxy_bypass.clone(),\n            )\n        };\n        let mut sysproxy = cur_sysproxy.take().unwrap();\n\n        sysproxy.enable = enable;\n        sysproxy.bypass = bypass.unwrap_or(DEFAULT_BYPASS.into());\n\n        sysproxy.set_system_proxy()?;\n        *cur_sysproxy = Some(sysproxy);\n\n        Ok(())\n    }\n\n    /// reset the sysproxy\n    pub fn reset_sysproxy(&self) -> Result<()> {\n        // Check if PAC is enabled first\n        #[cfg(feature = \"default-meta\")]\n        if PacManager::is_pac_enabled() {\n            log::info!(target: \"app\", \"Resetting PAC proxy\");\n            // Disable PAC proxy\n            log_err!(PacManager::disable_pac_proxy());\n        }\n\n        let mut cur_sysproxy = self.cur_sysproxy.lock();\n        let mut old_sysproxy = self.old_sysproxy.lock();\n\n        let cur_sysproxy = cur_sysproxy.take();\n\n        if let Some(mut old) = old_sysproxy.take() {\n            // 如果原代理和当前代理 端口一致，就disable关闭，否则就恢复原代理设置\n            // 当前没有设置代理的时候，不确定旧设置是否和当前一致，全关了\n            let port_same = cur_sysproxy.is_none_or(|cur| old.port == cur.port);\n\n            if old.enable && port_same {\n                old.enable = false;\n                log::info!(target: \"app\", \"reset proxy by disabling the original proxy\");\n            } else {\n                log::info!(target: \"app\", \"reset proxy to the original proxy\");\n            }\n\n            old.set_system_proxy()?;\n        } else if let Some(mut cur @ Sysproxy { enable: true, .. }) = cur_sysproxy {\n            // 没有原代理，就按现在的代理设置disable即可\n            log::info!(target: \"app\", \"reset proxy by disabling the current proxy\");\n            cur.enable = false;\n            cur.set_system_proxy()?;\n        } else {\n            log::info!(target: \"app\", \"reset proxy with no action\");\n        }\n\n        Ok(())\n    }\n\n    /// init the auto launch\n    pub fn init_launch(&self) -> Result<()> {\n        let enable = { Config::verge().latest().enable_auto_launch };\n        let enable = enable.unwrap_or(false);\n\n        log::info!(target: \"app\", \"Initializing auto-launch with enable={}\", enable);\n\n        let app_exe = current_exe()?;\n        let app_exe = dunce::canonicalize(app_exe)?;\n        log::debug!(target: \"app\", \"Resolved app executable path: {:?}\", app_exe);\n\n        let app_name = app_exe\n            .file_stem()\n            .and_then(|f| f.to_str())\n            .ok_or(anyhow!(\"failed to get file stem\"))?;\n\n        let app_path = app_exe\n            .as_os_str()\n            .to_str()\n            .ok_or(anyhow!(\"failed to get app_path\"))?\n            .to_string();\n\n        log::debug!(target: \"app\", \"Initial app path: {}\", app_path);\n\n        // fix issue #26\n        #[cfg(target_os = \"windows\")]\n        let app_path = format!(\"\\\"{app_path}\\\"\");\n        #[cfg(target_os = \"windows\")]\n        log::debug!(target: \"app\", \"Windows formatted app path: {}\", app_path);\n\n        // use the /Applications/Clash Nyanpasu.app path\n        #[cfg(target_os = \"macos\")]\n        let app_path = (|| -> Option<String> {\n            let path = std::path::PathBuf::from(&app_path);\n            let path = path.parent()?.parent()?.parent()?;\n            let extension = path.extension()?.to_str()?;\n            match extension == \"app\" {\n                true => Some(path.as_os_str().to_str()?.to_string()),\n                false => None,\n            }\n        })()\n        .unwrap_or(app_path);\n        #[cfg(target_os = \"macos\")]\n        log::debug!(target: \"app\", \"macOS app path: {}\", app_path);\n\n        // fix #403\n        #[cfg(target_os = \"linux\")]\n        let app_path = {\n            use crate::core::handle::Handle;\n            use tauri::Manager;\n\n            let handle = Handle::global();\n            let appimage_path = match handle.app_handle.lock().as_ref() {\n                Some(app_handle) => {\n                    // 优先使用 Tauri 环境变量\n                    let appimage = app_handle.env().appimage;\n                    appimage.and_then(|p| p.to_str().map(|s| s.to_string()))\n                }\n                None => None,\n            };\n\n            // 备用方法：检查环境变量\n            let fallback_appimage = std::env::var(\"APPIMAGE\").ok();\n\n            let final_path = appimage_path.or(fallback_appimage).unwrap_or(app_path);\n\n            log::info!(target: \"app\", \"Using executable path for auto-launch: {}\", final_path);\n            final_path\n        };\n\n        log::info!(target: \"app\", \"Using executable path for auto-launch: {}\", app_path);\n\n        let auto = AutoLaunchBuilder::new()\n            .set_app_name(app_name)\n            .set_app_path(&app_path)\n            .build()?;\n\n        log::debug!(target: \"app\", \"AutoLaunch builder created with app_name: {}\", app_name);\n\n        // 避免在开发时将自启动关了\n        #[cfg(feature = \"verge-dev\")]\n        if !enable {\n            log::info!(target: \"app\", \"Skipping auto-launch setup in development mode\");\n            return Ok(());\n        }\n\n        #[cfg(target_os = \"macos\")]\n        {\n            if enable && !auto.is_enabled().unwrap_or(false) {\n                // 避免重复设置登录项\n                log::debug!(target: \"app\", \"macOS: Disabling existing auto-launch\");\n                let _ = auto.disable();\n                log::debug!(target: \"app\", \"macOS: Enabling auto-launch\");\n                auto.enable()?;\n            } else if !enable {\n                log::debug!(target: \"app\", \"macOS: Disabling auto-launch\");\n                let _ = auto.disable();\n            }\n        }\n\n        #[cfg(not(target_os = \"macos\"))]\n        {\n            if enable {\n                log::debug!(target: \"app\", \"Enabling auto-launch for non-macOS platform\");\n                auto.enable()?;\n            } else {\n                log::debug!(target: \"app\", \"Disabling auto-launch for non-macOS platform\");\n                let _ = auto.disable();\n            }\n        }\n\n        *self.auto_launch.lock() = Some(auto);\n\n        Ok(())\n    }\n\n    /// update the startup\n    pub fn update_launch(&self) -> Result<()> {\n        let auto_launch = self.auto_launch.lock();\n\n        if auto_launch.is_none() {\n            drop(auto_launch);\n            return self.init_launch();\n        }\n        let enable = { Config::verge().latest().enable_auto_launch };\n        let enable = enable.unwrap_or(false);\n        let auto_launch = auto_launch.as_ref().unwrap();\n\n        match enable {\n            true => auto_launch.enable()?,\n            false => log_err!(auto_launch.disable()), // 忽略关闭的错误\n        };\n\n        Ok(())\n    }\n\n    /// launch a system proxy guard\n    /// read config from file directly\n    pub fn guard_proxy(&self) {\n        use tokio::time::{Duration, sleep};\n\n        let guard_state = self.guard_state.clone();\n\n        tauri::async_runtime::spawn(async move {\n            // if it is running, exit\n            let mut state = guard_state.lock().await;\n            if *state {\n                return;\n            }\n            *state = true;\n            drop(state);\n\n            // default duration is 10s\n            let mut wait_secs = 10u64;\n\n            loop {\n                sleep(Duration::from_secs(wait_secs)).await;\n\n                let (enable, guard, guard_interval, bypass) = {\n                    let verge = Config::verge();\n                    let verge = verge.latest();\n                    (\n                        verge.enable_system_proxy.unwrap_or(false),\n                        verge.enable_proxy_guard.unwrap_or(false),\n                        verge.proxy_guard_interval.unwrap_or(10),\n                        verge.system_proxy_bypass.clone(),\n                    )\n                };\n\n                // stop loop\n                if !enable || !guard {\n                    break;\n                }\n\n                // update duration\n                wait_secs = guard_interval;\n\n                log::debug!(target: \"app\", \"try to guard the system proxy\");\n\n                let port = {\n                    Config::verge()\n                        .latest()\n                        .verge_mixed_port\n                        .unwrap_or(Config::clash().data().get_mixed_port())\n                };\n\n                let sysproxy = Sysproxy {\n                    enable: true,\n                    host: \"127.0.0.1\".into(),\n                    port,\n                    bypass: bypass.unwrap_or(DEFAULT_BYPASS.into()),\n                };\n\n                log_err!(sysproxy.set_system_proxy());\n            }\n\n            let mut state = guard_state.lock().await;\n            *state = false;\n            drop(state);\n        });\n    }\n}\n"
  },
  {
    "path": "backend/tauri/src/core/tasks/events.rs",
    "content": "use anyhow::Context;\nuse chrono::Utc;\nuse parking_lot::Mutex;\nuse serde::{Deserialize, Serialize};\n\nuse super::{\n    storage::TaskStorage,\n    task::{TaskEventID, TaskID, TaskRunResult, Timestamp},\n    utils::Result,\n};\nuse std::{collections::HashMap, sync::Arc};\npub struct TaskEvents {\n    storage: Arc<Mutex<TaskStorage>>,\n}\n\n/// TaskEventDispatcher is a dispatcher for a task event,\n/// currently, it's designed for a single thread task to dispatch event.\npub struct TaskEventDispatcher {\n    storage: Arc<Mutex<TaskStorage>>,\n    event: TaskEvent,\n}\n\nimpl TaskEvents {\n    pub fn new(storage: Arc<Mutex<TaskStorage>>) -> Self {\n        TaskEvents { storage }\n    }\n\n    pub fn new_event(&self, task_id: TaskID, event_id: TaskEventID) -> Result<TaskEventDispatcher> {\n        tracing::debug!(\"create new event: {:?} for task: {:?}\", event_id, task_id);\n        let mut dispatcher = {\n            let storage = self.storage.lock();\n            let event = TaskEvent {\n                id: event_id,\n                task_id,\n                ..TaskEvent::default()\n            };\n            storage.add_event(&event).context(\"failed to add event\")?;\n            TaskEventDispatcher::new(self.storage.clone(), event)\n        };\n        dispatcher\n            .dispatch(TaskEventState::Pending)\n            .context(\"failed to dispatch pending event\")?;\n        Ok(dispatcher)\n    }\n}\n\nimpl TaskEventDispatcher {\n    pub fn new(storage: Arc<Mutex<TaskStorage>>, event: TaskEvent) -> Self {\n        TaskEventDispatcher { storage, event }\n    }\n    pub fn dispatch(&mut self, state: TaskEventState) -> Result<()> {\n        tracing::debug!(\n            \"dispatch state: {:?} for event: {:?} of task: {:?}\",\n            state,\n            self.event.id,\n            self.event.task_id\n        );\n        self.event.dispatch(state);\n        let storage = self.storage.lock();\n        storage.update_event(&self.event)?;\n        Ok(())\n    }\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct TaskEvent {\n    pub id: TaskEventID,\n    pub task_id: TaskID,\n    pub state: TaskEventState,\n    pub timeline: HashMap<String, Timestamp>,\n    pub updated_at: Timestamp,\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub enum TaskEventState {\n    Pending, // added to the queue, alias of created\n    Running,\n    Finished(TaskRunResult),\n    Cancelled,\n}\n\nimpl TaskEventState {\n    pub fn fmt(&self) -> &'static str {\n        match self {\n            Self::Pending => \"pending\",\n            Self::Running => \"running\",\n            Self::Finished(_) => \"finished\",\n            Self::Cancelled => \"cancelled\",\n        }\n    }\n}\n\nimpl Default for TaskEvent {\n    fn default() -> Self {\n        TaskEvent {\n            id: 0,\n            task_id: 0,\n            state: TaskEventState::Pending,\n            timeline: HashMap::with_capacity(4), // 4 states\n            updated_at: Utc::now().timestamp_millis(),\n        }\n    }\n}\n\nimpl TaskEvent {\n    fn dispatch(&mut self, state: TaskEventState) {\n        let now = Utc::now().timestamp_millis();\n        self.state = state;\n        self.timeline.insert(self.state.fmt().into(), now);\n        self.updated_at = now;\n    }\n}\n"
  },
  {
    "path": "backend/tauri/src/core/tasks/executor.rs",
    "content": "use std::fmt::{self, Formatter};\n\nuse anyhow::Result;\nuse async_trait::async_trait;\nuse dyn_clone::{DynClone, clone_trait_object};\n\n/// JobExecutor is a trait for job executor\n/// It is used to define a sync job\n///\n/// For example, you can define a job to print hello.\n/// ``` rust\n/// use anyhow::Result;\n/// #[derive(Clone)]\n/// pub struct HelloJob {}\n/// impl JobExecutor for HelloJob {\n///     fn execute(&self) -> Result<()> {\n///        println!(\"hello\");\n///       Ok(())\n///    }\n/// }\n/// ```\n/// Then you can pass it to the task manager to execute it.\n///\n///\npub trait JobExecutor: DynClone {\n    fn execute(&self) -> Result<()>;\n}\n\nclone_trait_object!(JobExecutor);\n\npub type Job = Box<dyn JobExecutor + Send + Sync>;\n\n#[async_trait]\npub trait AsyncJobExecutor: DynClone {\n    async fn execute(&self) -> Result<()>;\n}\n\nclone_trait_object!(AsyncJobExecutor);\n\npub type AsyncJob = Box<dyn AsyncJobExecutor + Send + Sync>;\n\n#[derive(Clone)]\npub enum TaskExecutor {\n    Sync(Job),\n    Async(AsyncJob),\n}\n\nimpl fmt::Debug for TaskExecutor {\n    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {\n        match self {\n            Self::Sync(_) => write!(f, \"Sync\"),\n            Self::Async(_) => write!(f, \"Async\"),\n        }\n    }\n}\n\nimpl Default for TaskExecutor {\n    fn default() -> Self {\n        Self::Sync(Job::default()) // default job executor\n    }\n}\n\nimpl From<Job> for TaskExecutor {\n    fn from(job: Job) -> Self {\n        Self::Sync(job)\n    }\n}\n\nimpl From<AsyncJob> for TaskExecutor {\n    fn from(job: AsyncJob) -> Self {\n        Self::Async(job)\n    }\n}\n\n#[derive(Clone, Debug)]\nstruct DefaultJobExecutor {}\n\nimpl JobExecutor for DefaultJobExecutor {\n    fn execute(&self) -> Result<()> {\n        unimplemented!(\"not implemented\");\n    }\n}\n\n#[async_trait]\nimpl AsyncJobExecutor for DefaultJobExecutor {\n    async fn execute(&self) -> Result<()> {\n        unimplemented!(\"not implemented\");\n    }\n}\n\nimpl Default for Job {\n    fn default() -> Self {\n        Box::new(DefaultJobExecutor {})\n    }\n}\n\nimpl Default for AsyncJob {\n    fn default() -> Self {\n        Box::new(DefaultJobExecutor {})\n    }\n}\n"
  },
  {
    "path": "backend/tauri/src/core/tasks/jobs/events_rotate.rs",
    "content": "use crate::core::tasks::{\n    executor::{AsyncJobExecutor, TaskExecutor},\n    storage::TaskStorage,\n    task::TaskSchedule,\n};\nuse anyhow::Context;\nuse parking_lot::Mutex;\nuse std::sync::Arc;\n\nuse super::JobExt;\n\nconst CLEAR_EVENTS_TASK_NAME: &str = \"Task Events Rotate\";\n\n#[derive(Clone)]\npub struct EventsRotateJob {\n    task_storage: Arc<Mutex<TaskStorage>>,\n}\n\nimpl EventsRotateJob {\n    pub fn new(task_storage: Arc<Mutex<TaskStorage>>) -> Self {\n        Self { task_storage }\n    }\n}\n\n#[async_trait::async_trait]\nimpl AsyncJobExecutor for EventsRotateJob {\n    // TODO: optimize performance if we got reported that this job is slow\n    async fn execute(&self) -> anyhow::Result<()> {\n        let storage = self.task_storage.lock();\n        let task_ids = storage.list_tasks().context(\"failed to list tasks\")?;\n        for task_id in task_ids {\n            let event_ids = storage\n                .get_event_ids(task_id)\n                .context(format!(\"failed to get event ids for task {task_id}\"))?\n                .unwrap_or_default();\n            let mut events_to_remove = Vec::new();\n            let mut events = event_ids\n                .into_iter()\n                .filter_map(|id| {\n                    let event = storage.get_event(id).ok().flatten();\n                    if event.is_none() {\n                        events_to_remove.push(id);\n                    }\n                    event\n                })\n                .collect::<Vec<_>>();\n            // DESC sort events by updated_at\n            events.sort_by(|a, b| b.updated_at.cmp(&a.updated_at));\n            // keep max 10 events\n            let events = events\n                .into_iter()\n                .skip(10)\n                .map(|e| e.id)\n                .collect::<Vec<_>>();\n            events_to_remove.extend(events);\n            // remove events\n            for event_id in events_to_remove {\n                log::debug!(\"removing event {event_id} for task {task_id}\");\n                storage\n                    .remove_event(event_id, task_id)\n                    .context(format!(\"failed to remove event {event_id}\"))?;\n            }\n        }\n        Ok(())\n    }\n}\n\nimpl JobExt for EventsRotateJob {\n    fn name(&self) -> &'static str {\n        CLEAR_EVENTS_TASK_NAME\n    }\n\n    fn setup(&self) -> Option<crate::core::tasks::task::Task> {\n        Some(crate::core::tasks::task::Task {\n            name: CLEAR_EVENTS_TASK_NAME.to_string(),\n            schedule: TaskSchedule::Cron(\"@hourly\".to_string()),\n            executor: TaskExecutor::Async(Box::new(self.clone())),\n            ..Default::default()\n        })\n    }\n}\n"
  },
  {
    "path": "backend/tauri/src/core/tasks/jobs/logger.rs",
    "content": "use super::JobExt;\nuse crate::{\n    config::Config,\n    core::tasks::{\n        executor::{AsyncJobExecutor, TaskExecutor},\n        task::TaskSchedule,\n    },\n    utils::dirs,\n};\nuse anyhow::Result;\nuse async_trait::async_trait;\nuse chrono::{DateTime, Local, TimeZone};\nuse std::{\n    fs::{self, DirEntry},\n    str::FromStr,\n    time::Duration,\n};\n\nconst CLEAR_LOG_TASK_NAME: &str = \"clear_logs\";\n\n#[derive(Clone, Default)]\npub struct ClearLogsJob;\n\n/// Clear logs from the logs directory\npub fn clear_logs() -> Result<()> {\n    let log_dir = dirs::app_logs_dir()?;\n    if !log_dir.exists() {\n        return Ok(());\n    }\n\n    let minutes = {\n        let verge = Config::verge();\n        let verge = verge.data();\n        #[allow(deprecated)]\n        verge.auto_log_clean.unwrap_or(0)\n    };\n    if minutes == 0 {\n        return Ok(()); // 0 means disable\n    }\n    log::debug!(target: \"app\", \"try to delete log files, minutes: {minutes}\");\n\n    // %Y-%m-%d to NaiveDateTime\n    let parse_time_str = |s: &str| {\n        let sa: Vec<&str> = s.split('-').collect();\n        if sa.len() != 4 {\n            return Err(anyhow::anyhow!(\"invalid time str\"));\n        }\n\n        let year = i32::from_str(sa[0])?;\n        let month = u32::from_str(sa[1])?;\n        let day = u32::from_str(sa[2])?;\n        let time = chrono::NaiveDate::from_ymd_opt(year, month, day)\n            .ok_or(anyhow::anyhow!(\"invalid time str\"))?\n            .and_hms_opt(0, 0, 0)\n            .ok_or(anyhow::anyhow!(\"invalid time str\"))?;\n        Ok(time)\n    };\n\n    let process_file = |file: DirEntry| -> Result<()> {\n        let file_name = file.file_name();\n        let file_name = file_name.to_str().unwrap_or_default();\n\n        if file_name.ends_with(\".log\") {\n            let now = Local::now();\n            let created_time = parse_time_str(&file_name[0..file_name.len() - 4])?;\n            let created_time: DateTime<Local> = Local.from_local_datetime(&created_time).unwrap(); // It is safe to use `unwrap` here because we just parsed it\n\n            let duration = now.signed_duration_since(created_time);\n            if duration.num_minutes() > minutes {\n                let file_path = file.path();\n                let _ = fs::remove_file(file_path);\n                log::info!(target: \"app\", \"delete log file: {file_name}\");\n            }\n        }\n        Ok(())\n    };\n\n    for file in fs::read_dir(&log_dir)? {\n        match file {\n            Ok(file) => {\n                let _ = process_file(file);\n            }\n            Err(err) => {\n                log::error!(target: \"app\", \"read log dir error: {err:?}\");\n            }\n        }\n    }\n    Ok(())\n}\n\n#[async_trait]\nimpl AsyncJobExecutor for ClearLogsJob {\n    async fn execute(&self) -> Result<()> {\n        clear_logs()\n    }\n}\n\nimpl JobExt for ClearLogsJob {\n    fn name(&self) -> &'static str {\n        CLEAR_LOG_TASK_NAME\n    }\n\n    fn setup(&self) -> Option<crate::core::tasks::task::Task> {\n        Some(crate::core::tasks::task::Task {\n            name: CLEAR_LOG_TASK_NAME.to_string(),\n            schedule: TaskSchedule::Interval(Duration::from_secs(30 * 60)), // 30 minutes 清理一次\n            executor: TaskExecutor::Async(Box::new(self.clone())),\n            ..Default::default()\n        })\n    }\n}\n"
  },
  {
    "path": "backend/tauri/src/core/tasks/jobs/mod.rs",
    "content": "mod events_rotate;\nmod logger;\nmod profiles;\n\nuse super::{\n    task::{Task, TaskManager},\n    utils::{ConfigChangedNotifier, Result},\n};\nuse anyhow::anyhow;\nuse parking_lot::RwLock;\npub use profiles::ProfilesJobGuard;\nuse std::sync::Arc;\npub trait JobExt {\n    fn name(&self) -> &'static str;\n    fn setup(&self) -> Option<Task>; // called when the app starts or the config changed\n}\n\npub struct JobsManager {\n    jobs: Vec<Box<dyn JobExt + Send + Sync>>,\n    task_manager: Arc<RwLock<TaskManager>>,\n}\n\nimpl JobsManager {\n    pub fn new(task_manager: Arc<RwLock<TaskManager>>) -> Self {\n        Self {\n            jobs: Vec::new(),\n            task_manager,\n        }\n    }\n\n    pub fn setup(&mut self) -> anyhow::Result<()> {\n        let jobs: Vec<Box<dyn JobExt + Send + Sync>> = vec![Box::new(\n            events_rotate::EventsRotateJob::new(self.task_manager.read().get_inner_task_storage()),\n        )];\n        for job in jobs {\n            let task = job.setup();\n            if let Some(task) = task {\n                self.task_manager.write().add_task(task)?;\n            }\n            self.jobs.push(job);\n        }\n        Ok(())\n    }\n}\n\nimpl ConfigChangedNotifier for JobsManager {\n    fn notify_config_changed(&self, job_name: &str) -> Result<()> {\n        let job = self\n            .jobs\n            .iter()\n            .find(|job| job.name() == job_name)\n            .ok_or(anyhow!(\"job not exist\"))?;\n        let task = job.setup();\n        if let Some(task) = task {\n            let mut task_manager = self.task_manager.write();\n            task_manager.remove_task(task.id)?;\n            task_manager.add_task(task)?;\n        }\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "backend/tauri/src/core/tasks/jobs/profiles.rs",
    "content": "use super::super::{\n    executor::{AsyncJobExecutor, TaskExecutor},\n    task::{Task, TaskID, TaskManager, TaskSchedule},\n};\nuse crate::{\n    config::{Config, ProfileMetaGetter},\n    feat,\n};\nuse anyhow::Result;\nuse async_trait::async_trait;\nuse parking_lot::RwLock;\nuse std::{collections::HashMap, ops::Deref, sync::Arc, time::Duration};\n\nconst INITIAL_TASK_ID: TaskID = 10000000; // 留一个初始的 TaskID，避免和其他任务的 ID 冲突\n\ntype Minutes = u64;\ntype ProfileUID = String;\n\n#[derive(Clone)]\npub struct ProfileUpdater(ProfileUID);\n\nimpl ProfileUpdater {\n    #[allow(dead_code)]\n    pub fn new(profile_uid: &str) -> Self {\n        Self(profile_uid.to_string())\n    }\n}\n\n#[async_trait]\nimpl AsyncJobExecutor for ProfileUpdater {\n    async fn execute(&self) -> Result<()> {\n        log::info!(target: \"app\", \"running timer task `{}`\", self.0);\n        match feat::update_profile(self.0.clone(), None).await {\n            Ok(_) => Ok(()),\n            Err(err) => {\n                log::error!(target: \"app\", \"failed to update profile: {err:?}\");\n                Err(err)\n            }\n        }\n    }\n}\n\nenum ProfileTaskOp {\n    Add(TaskID, Minutes),\n    Remove(TaskID),\n    Update(TaskID, Minutes),\n}\n\npub struct ProfilesJob {\n    task_map: HashMap<ProfileUID, (TaskID, u64)>,\n    task_manager: Arc<RwLock<TaskManager>>,\n    // next_id: TaskID,\n}\n\npub struct ProfilesJobGuard {\n    job: Arc<RwLock<ProfilesJob>>,\n}\n\nimpl ProfilesJobGuard {\n    pub fn new(task_manager: Arc<RwLock<TaskManager>>) -> Self {\n        Self {\n            job: Arc::new(RwLock::new(ProfilesJob::new(task_manager))),\n        }\n    }\n}\n\nimpl Deref for ProfilesJobGuard {\n    type Target = Arc<RwLock<ProfilesJob>>;\n\n    fn deref(&self) -> &Self::Target {\n        &self.job\n    }\n}\n\nimpl ProfilesJob {\n    pub fn new(task_manager: Arc<RwLock<TaskManager>>) -> Self {\n        Self {\n            task_map: HashMap::new(),\n            task_manager,\n        }\n    }\n\n    /// restore timer\n    pub fn init(&mut self) -> Result<()> {\n        self.refresh();\n        let cur_timestamp = chrono::Local::now().timestamp();\n        let task_map = &self.task_map;\n\n        Config::profiles()\n            .latest()\n            .items\n            .iter()\n            .filter_map(|item| {\n                if !item.is_remote() {\n                    return None;\n                }\n                let item = item.as_remote().unwrap();\n                // mins to seconds\n                let interval = ((item.option.update_interval) as i64) * 60;\n                let updated = item.updated() as i64;\n\n                if interval > 0 && cur_timestamp - updated >= interval {\n                    Some(item)\n                } else {\n                    None\n                }\n            })\n            .for_each(|item| {\n                if let Some((task_id, _)) = task_map.get(item.uid()) {\n                    crate::log_err!(self.task_manager.write().advance_task(*task_id));\n                }\n            });\n\n        Ok(())\n    }\n\n    /// Correctly update all cron tasks\n    pub fn refresh(&mut self) {\n        let diff_map = self.diff();\n        for (uid, diff) in diff_map.into_iter() {\n            match diff {\n                ProfileTaskOp::Add(task_id, interval) => {\n                    let task = new_task(task_id, &uid, interval);\n                    crate::log_err!(self.task_manager.write().add_task(task));\n                    self.task_map.insert(uid, (task_id, interval));\n                }\n                ProfileTaskOp::Remove(task_id) => {\n                    crate::log_err!(self.task_manager.write().remove_task(task_id));\n                    self.task_map.remove(&uid);\n                }\n                ProfileTaskOp::Update(task_id, interval) => {\n                    crate::log_err!(self.task_manager.write().remove_task(task_id));\n                    let task = new_task(task_id, &uid, interval);\n                    crate::log_err!(self.task_manager.write().add_task(task));\n                    self.task_map.insert(uid, (task_id, interval));\n                }\n            }\n        }\n    }\n    // fn get_next_task_id(&mut self) -> TaskID {\n    //     let id = self.next_id;\n    //     self.next_id += 1;\n    //     id\n    // }\n\n    /// generate the diff map for refresh\n    fn diff(&self) -> HashMap<ProfileUID, ProfileTaskOp> {\n        let mut diff_map = HashMap::new();\n\n        let timer_map = &self.task_map;\n\n        let new_map = gen_map();\n\n        timer_map.iter().for_each(|(uid, (tid, val))| {\n            let new_val = new_map.get(uid).unwrap_or(&0);\n\n            if *new_val == 0 {\n                diff_map.insert(uid.clone(), ProfileTaskOp::Remove(*tid));\n            } else if new_val != val {\n                diff_map.insert(uid.clone(), ProfileTaskOp::Update(*tid, *new_val));\n            }\n        });\n\n        new_map.iter().for_each(|(uid, val)| {\n            if timer_map.get(uid).is_none() {\n                let task_id = get_task_id(uid);\n                diff_map.insert(uid.clone(), ProfileTaskOp::Add(task_id, *val));\n            }\n        });\n\n        diff_map\n    }\n}\n\n/// generate a uid -> update_interval map\nfn gen_map() -> HashMap<ProfileUID, Minutes> {\n    let mut new_map = HashMap::new();\n\n    Config::profiles()\n        .latest()\n        .get_items()\n        .iter()\n        .filter_map(|item| item.as_remote())\n        .for_each(|item| {\n            let interval = item.option.update_interval;\n            if interval > 0 {\n                new_map.insert(item.uid().to_string(), interval);\n            }\n        });\n\n    new_map\n}\n\n/// get_task_id Get a u64 task id by profile uid\nfn get_task_id(uid: &str) -> TaskID {\n    let task_id = seahash::hash(uid.as_bytes());\n    if task_id < INITIAL_TASK_ID {\n        INITIAL_TASK_ID + task_id\n    } else {\n        task_id\n    }\n}\n\nfn new_task(task_id: TaskID, profile_uid: &str, interval: Minutes) -> Task {\n    Task {\n        id: task_id,\n        name: format!(\"profile-updater-{profile_uid}\"),\n        executor: TaskExecutor::Async(Box::new(ProfileUpdater(profile_uid.to_owned().to_string()))),\n        schedule: TaskSchedule::Interval(Duration::from_secs(interval * 60)),\n        ..Task::default()\n    }\n}\n"
  },
  {
    "path": "backend/tauri/src/core/tasks/mod.rs",
    "content": "mod events;\npub mod executor;\npub mod jobs;\nmod storage;\npub mod task;\nmod utils;\n\npub fn setup<R: tauri::Runtime, M: tauri::Manager<R>>(\n    app: &M,\n    storage: super::storage::Storage,\n) -> anyhow::Result<()> {\n    use anyhow::Context;\n    use parking_lot::RwLock;\n\n    let task_storage = storage::TaskStorage::new(storage);\n    let task_manager = task::TaskManager::new(task_storage);\n    let task_manager = std::sync::Arc::new(RwLock::new(task_manager));\n\n    // job manager\n    let mut job_manager = jobs::JobsManager::new(task_manager.clone());\n    job_manager.setup().context(\"failed to setup job manager\")?;\n    let job_manager = std::sync::Arc::new(RwLock::new(job_manager));\n    app.manage(job_manager);\n    // profiles job\n    let profiles_job = jobs::ProfilesJobGuard::new(task_manager.clone());\n    {\n        let mut profiles_job = profiles_job.write();\n        profiles_job.init()?;\n    }\n    app.manage(profiles_job);\n    app.manage(task_manager);\n    Ok(())\n}\n"
  },
  {
    "path": "backend/tauri/src/core/tasks/storage.rs",
    "content": "//! store is a interface to save and restore task states\nuse super::{\n    events::TaskEvent,\n    task::{TaskEventID, TaskID, TaskManager},\n    utils::Result,\n};\nuse crate::core::{\n    storage::{NYANPASU_TABLE, Storage},\n    tasks::task::Task,\n};\nuse log::debug;\nuse redb::{ReadableDatabase, ReadableTable};\nuse std::{collections::HashSet, str};\n\npub struct TaskStorage {\n    // TODO: hold storage instance, and better concurrency safety\n    storage: Storage,\n}\n\n/// TaskStorage is a bridge between the task events and the storage\nimpl TaskStorage {\n    const TASKS_KEY: &str = \"tasks\";\n\n    pub fn new(storage: Storage) -> Self {\n        Self { storage }\n    }\n\n    /// list_tasks list all tasks, for reduce the number of read operations\n    pub fn list_tasks(&self) -> Result<Vec<TaskID>> {\n        let db = self.storage.get_instance();\n        let read_txn = db.begin_read()?;\n        let table = read_txn.open_table(NYANPASU_TABLE)?;\n        let value = table.get(Self::TASKS_KEY.as_bytes())?;\n        match value {\n            Some(value) => {\n                let tasks: Vec<TaskID> = serde_json::from_slice(value.value())?;\n                Ok(tasks)\n            }\n            None => Ok(Vec::new()),\n        }\n    }\n\n    /// add_task add a task id to the storage\n    pub fn add_task(&self, task_id: TaskID) -> Result<()> {\n        let db = self.storage.get_instance();\n        let write_txn = db.begin_write()?;\n        {\n            let mut table = write_txn.open_table(NYANPASU_TABLE)?;\n            let mut tasks = table\n                .get(Self::TASKS_KEY.as_bytes())?\n                .and_then(|val| {\n                    let tasks: HashSet<TaskID> = serde_json::from_slice(val.value()).ok()?;\n                    Some(tasks)\n                })\n                .unwrap_or_default();\n            tasks.insert(task_id);\n            let value = serde_json::to_vec(&tasks)?;\n            table.insert(Self::TASKS_KEY.as_bytes(), value.as_slice())?;\n        }\n        write_txn.commit()?;\n        Ok(())\n    }\n\n    /// remove_task remove a task id from the storage\n    pub fn remove_task(&self, _task_id: TaskID) -> Result<()> {\n        let db = self.storage.get_instance();\n        let write_txn = db.begin_write()?;\n        {\n            let mut table = write_txn.open_table(NYANPASU_TABLE)?;\n            table.remove(Self::TASKS_KEY.as_bytes())?;\n        }\n        write_txn.commit()?;\n        Ok(())\n    }\n\n    /// get_event get a task event by event id\n    pub fn get_event(&self, event_id: TaskEventID) -> Result<Option<TaskEvent>> {\n        let db = self.storage.get_instance();\n        let key = format!(\"task:event:id:{event_id}\");\n        let read_txn = db.begin_read()?;\n        let table = read_txn.open_table(NYANPASU_TABLE)?;\n        let value = table.get(key.as_bytes())?;\n        match value {\n            Some(value) => {\n                let event: TaskEvent = serde_json::from_slice(value.value())?;\n                Ok(Some(event))\n            }\n            None => Ok(None),\n        }\n    }\n\n    /// get_events get all events of a task\n    #[allow(dead_code)]\n    pub fn get_events(&self, task_id: TaskID) -> Result<Option<Vec<TaskEvent>>> {\n        let mut value = match self.get_event_ids(task_id)? {\n            Some(value) => value,\n            None => return Ok(None),\n        };\n\n        let mut events = Vec::with_capacity(value.len());\n        for event_id in value.drain(..) {\n            let event = self.get_event(event_id)?.unwrap(); // unwrap because it should be exist here, if not, it's a bug\n            events.push(event);\n        }\n        Ok(Some(events))\n    }\n\n    pub fn get_event_ids(&self, task_id: TaskID) -> Result<Option<Vec<TaskEventID>>> {\n        let db = self.storage.get_instance();\n        let key = format!(\"task:events:task_id:{task_id}\");\n        let read_txn = db.begin_read()?;\n        let table = read_txn.open_table(NYANPASU_TABLE)?;\n        let value = table.get(key.as_bytes())?;\n        let value: Vec<TaskEventID> = match value {\n            Some(value) => serde_json::from_slice(value.value())?,\n            None => return Ok(None),\n        };\n        Ok(Some(value))\n    }\n\n    /// add_event add a new event to the storage\n    pub fn add_event(&self, event: &TaskEvent) -> Result<()> {\n        let mut event_ids = (self.get_event_ids(event.task_id)?).unwrap_or_default();\n        event_ids.push(event.id);\n\n        let db = self.storage.get_instance();\n        let event_key = format!(\"task:event:id:{}\", event.id);\n        let event_ids_key = format!(\"task:events:task_id:{}\", event.task_id);\n        let event_value = serde_json::to_vec(event)?;\n        let event_ids = serde_json::to_vec(&event_ids)?;\n        let write_txn = db.begin_write()?;\n        {\n            let mut table = write_txn.open_table(NYANPASU_TABLE)?;\n            table.insert(event_key.as_bytes(), event_value.as_slice())?;\n            table.insert(event_ids_key.as_bytes(), event_ids.as_slice())?;\n        }\n        write_txn.commit()?;\n        Ok(())\n    }\n\n    /// update_event update a event in the storage\n    pub fn update_event(&self, event: &TaskEvent) -> Result<()> {\n        let db = self.storage.get_instance();\n        let event_key = format!(\"task:event:id:{}\", event.id);\n        let event_value = serde_json::to_vec(event)?;\n        let write_txn = db.begin_write()?;\n        {\n            let mut table = write_txn.open_table(NYANPASU_TABLE)?;\n            table.insert(event_key.as_bytes(), event_value.as_slice())?;\n        }\n        write_txn.commit()?;\n        Ok(())\n    }\n\n    /// remove_event remove a event from the storage\n    #[allow(dead_code)]\n    pub fn remove_event(&self, event_id: TaskEventID, task_id: TaskID) -> Result<()> {\n        let event_ids: Vec<TaskEventID> = match self.get_event_ids(task_id)? {\n            Some(value) => value.into_iter().filter(|v| v != &event_id).collect(),\n            None => return Ok(()),\n        };\n        let db = self.storage.get_instance();\n        let event_key = format!(\"task:event:id:{event_id}\");\n        let event_ids_key = format!(\"task:events:task_id:{event_id}\");\n        let write_txn = db.begin_write()?;\n        {\n            let mut table = write_txn.open_table(NYANPASU_TABLE)?;\n            table.remove(event_key.as_bytes())?;\n            if event_ids.is_empty() {\n                table.remove(event_ids_key.as_bytes())?;\n            } else {\n                let event_ids = serde_json::to_vec(&event_ids)?;\n                table.insert(event_ids_key.as_bytes(), event_ids.as_slice())?;\n            }\n        }\n        write_txn.commit()?;\n        Ok(())\n    }\n\n    /// get_instance get the raw storage instance\n    fn get_instance(&self) -> &redb::Database {\n        self.storage.get_instance()\n    }\n}\n\n// pub struct TaskGuard;\npub trait TaskGuard {\n    fn restore(&mut self) -> Result<()>;\n    fn dump(&self) -> Result<()>;\n}\n\n/// TaskGuard is a bridge between the tasks and the storage\nimpl TaskGuard for TaskManager {\n    fn restore(&mut self) -> Result<()> {\n        let tasks = {\n            let db = self.storage.lock();\n            let instance = db.get_instance();\n            let mut tasks = Vec::new();\n\n            let read_txn = instance.begin_read()?;\n            let table = read_txn.open_table(NYANPASU_TABLE)?;\n            for item in table.iter()? {\n                let (key, value) = item?;\n                let key = key.value();\n                let mut value = value.value().to_owned();\n                if key.starts_with(b\"task:id:\") {\n                    let task = serde_json::from_slice::<Task>(value.as_mut_slice())?;\n                    debug!(\n                        \"restore task: {:?} {:?}\",\n                        str::from_utf8(key).unwrap(),\n                        str::from_utf8(value.as_slice()).unwrap()\n                    );\n                    tasks.push(task);\n                }\n            }\n            tasks\n        };\n        self.restore_tasks(tasks);\n        Ok(())\n    }\n    fn dump(&self) -> Result<()> {\n        let tasks = self.list();\n        let db = self.storage.lock();\n        let instance = db.get_instance();\n        let write_txn = instance.begin_write()?;\n        {\n            let mut table = write_txn.open_table(NYANPASU_TABLE)?;\n            for task in tasks {\n                let key = format!(\"task:id:{}\", task.id);\n                let value = serde_json::to_vec(&task)?;\n                table.insert(key.as_bytes(), value.as_slice())?;\n            }\n        }\n        write_txn.commit()?;\n        Ok(())\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    #[test]\n    fn test_hashset_eq_vec() {\n        let json = r#\"\n        [1, 2, 3]\n        \"#\n        .trim();\n        let hashset: HashSet<i32> = serde_json::from_str(json).unwrap();\n        let new_json = serde_json::to_string(&hashset).unwrap();\n        println!(\"{new_json}\");\n    }\n}\n"
  },
  {
    "path": "backend/tauri/src/core/tasks/task.rs",
    "content": "use super::{\n    events::{TaskEventState, TaskEvents},\n    executor::{Job, TaskExecutor},\n    storage::TaskStorage,\n    utils::{Error, Result, TaskCreationError},\n};\nuse crate::error;\nuse chrono::Utc;\nuse delay_timer::{\n    entity::{DelayTimer, DelayTimerBuilder},\n    timer::task::TaskBuilder as TimerTaskBuilder,\n    utils::convenience::cron_expression_grammatical_candy::{CandyCronStr, CandyFrequency},\n};\nuse parking_lot::{Mutex, RwLock as RW};\nuse serde::{Deserialize, Serialize};\nuse snowflake::SnowflakeIdGenerator;\nuse std::{sync::Arc, time::Duration};\n\npub type TaskID = u64;\npub type TaskEventID = i64; // 任务事件 ID，适用于任务并发执行，区分不同的执行事件\n\n#[derive(Debug, Clone, Serialize, Deserialize, Default)]\npub enum TaskState {\n    Cancelled, // 任务已取消，不再执行\n    #[default]\n    Idle, // 空闲\n    Running(TaskEventID), // 任务执行中，存储最新执行的事件 ID\n}\n\n#[derive(Serialize, Deserialize, Debug, Clone)]\npub enum TaskRunResult {\n    Ok,\n    Err(String),\n}\n\n#[derive(Debug, Clone)]\npub enum TaskSchedule {\n    Once(Duration),     // 一次性执行\n    Interval(Duration), // 按间隔执行\n    #[allow(dead_code)]\n    Cron(String), // 按 cron 表达式执行\n}\n\nimpl Default for TaskSchedule {\n    fn default() -> Self {\n        Self::Once(Duration::from_secs(0))\n    }\n}\n\n// TODO: 如果需要的话，未来可以添加执行日记（历史记录）\n#[derive(Debug, Clone)]\npub struct TaskOptions {\n    pub maximum_parallel_runnable_num: u64, // 最大同时并发数\n}\n\nimpl Default for TaskOptions {\n    fn default() -> Self {\n        Self {\n            maximum_parallel_runnable_num: 5,\n        }\n    }\n}\n\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct Task {\n    pub id: TaskID,\n    pub name: String,\n    #[serde(skip_serializing, skip_deserializing)]\n    pub(super) schedule: TaskSchedule,\n    #[serde(skip_serializing, skip_deserializing)]\n    pub(super) state: TaskState,\n    #[serde(skip_serializing, skip_deserializing)]\n    pub(super) opts: TaskOptions,\n    pub(super) last_run: Option<(Timestamp, TaskRunResult)>,\n    pub(super) next_run: Option<Timestamp>, // timestamp\n    #[serde(skip_serializing, skip_deserializing)]\n    pub(super) executor: TaskExecutor,\n    pub created_at: Timestamp,\n}\n\nimpl Default for Task {\n    fn default() -> Self {\n        Task {\n            id: 0,\n            name: String::new(),\n            schedule: TaskSchedule::Once(Duration::from_secs(0)),\n            state: TaskState::Idle,\n            opts: TaskOptions::default(),\n            executor: TaskExecutor::Sync(Job::default()), // a unimplemented job\n            last_run: None,\n            next_run: None,\n            created_at: 0,\n        }\n    }\n}\n\npub type Timestamp = i64;\n\n// 参数校验失败\nmacro_rules! params_validated_failed {\n    ($fmt:expr) => {\n        Err(Error::ParamsValidationFailed($fmt))\n    };\n}\n\n// 检查任务输入\nmacro_rules! check_task_input {\n    ($task:ident) => {\n        if $task.name.is_empty() {\n            return params_validated_failed!(\"task name is empty\");\n        }\n\n        match &$task.schedule {\n            TaskSchedule::Once(duration) => {\n                if duration.as_secs() <= 0 {\n                    return params_validated_failed!(\"task interval must be greater than 0\");\n                }\n            }\n            TaskSchedule::Interval(duration) => {\n                if duration.as_secs() <= 0 {\n                    return params_validated_failed!(\"task interval must be greater than 0\");\n                }\n            }\n            TaskSchedule::Cron(cron) => {\n                if cron.is_empty() {\n                    return params_validated_failed!(\"task cron is empty\");\n                }\n            }\n        }\n    };\n}\n\n// 构建任务\nfn build_task<'a>(task: Task, len: usize) -> (Task, TimerTaskBuilder<'a>) {\n    let task = Task {\n        id: match task.id {\n            0 => len as u64 + 1,\n            _ => task.id,\n        },\n        created_at: match task.created_at {\n            0 => Utc::now().timestamp(),\n            _ => task.created_at,\n        },\n        ..task\n    };\n\n    let mut builder = TimerTaskBuilder::default();\n    builder.set_task_id(task.id);\n\n    match &task.schedule {\n        TaskSchedule::Cron(cron) => {\n            // NOTE: 由于 DelayTimer 的设计，因此继续使用弃用的 candy 方法\n            // NOTE: 请注意一定需要回收内存，否则会造成内存泄漏\n            let cron = cron.clone();\n            #[allow(deprecated)]\n            builder.set_frequency_by_candy(CandyFrequency::Repeated(CandyCronStr(cron)));\n        }\n        TaskSchedule::Interval(duration) => {\n            builder.set_frequency_repeated_by_seconds(duration.as_secs());\n        }\n        // 一次性延迟任务，目前设计只支持 Interval\n        // TODO: 支持即时任务？\n        TaskSchedule::Once(duration) => {\n            builder.set_frequency_once_by_seconds(duration.as_secs());\n        }\n    }\n\n    builder.set_maximum_parallel_runnable_num(task.opts.maximum_parallel_runnable_num); // 最大同时并发数\n\n    (task, builder)\n}\n\nmacro_rules! wrap_job {\n    ($exec:expr, $list:expr, $id_generator:expr, $task_id:expr, $task_events:expr) => {{\n        let event_id = $id_generator.generate();\n\n        let _ = $list.set_task_state($task_id, TaskState::Running(event_id), None);\n        let mut dispatcher = $task_events.new_event($task_id, event_id).unwrap();\n        dispatcher.dispatch(TaskEventState::Running).unwrap();\n\n        let res = $exec;\n\n        let res = match res {\n            Ok(_) => TaskRunResult::Ok,\n            Err(e) => {\n                error!(format!(\"task error: {}\", e.to_string()));\n                TaskRunResult::Err(e.to_string())\n            }\n        };\n\n        if let TaskState::Running(latest_event_id) = $list.get_task_state($task_id).unwrap() {\n            if latest_event_id == event_id {\n                let _ = $list.set_task_state($task_id, TaskState::Idle, Some(res.clone()));\n            }\n        }\n        dispatcher.dispatch(TaskEventState::Finished(res)).unwrap();\n    }};\n}\n\n// TaskList 语法糖\ntype TaskList = Arc<RW<Vec<Task>>>;\ntrait TaskListOps {\n    fn get_task_state(&self, task_id: TaskID) -> Result<TaskState>;\n    fn set_task_state(\n        &self,\n        task_id: TaskID,\n        state: TaskState,\n        result: Option<TaskRunResult>,\n    ) -> Result<()>;\n}\nimpl TaskListOps for TaskList {\n    fn get_task_state(&self, task_id: TaskID) -> Result<TaskState> {\n        let list = self.read();\n        let item = list\n            .iter()\n            .find(|t| t.id == task_id)\n            .ok_or(Error::CreateTaskFailed(TaskCreationError::NotFound))?;\n        Ok(item.state.clone())\n    }\n\n    fn set_task_state(\n        &self,\n        task_id: TaskID,\n        state: TaskState,\n        result: Option<TaskRunResult>,\n    ) -> Result<()> {\n        let mut list = self.write();\n        let item = list\n            .iter_mut()\n            .find(|t| t.id == task_id)\n            .ok_or(Error::CreateTaskFailed(TaskCreationError::NotFound))?;\n        match state {\n            TaskState::Running(event_id) => {\n                item.state = TaskState::Running(event_id);\n            }\n            TaskState::Idle => {\n                if let TaskState::Running(_) = item.state {\n                    item.last_run = Some((\n                        Utc::now().timestamp(),\n                        result.ok_or(Error::CreateTaskFailed(TaskCreationError::NotFound))?,\n                    ));\n                }\n                item.state = TaskState::Idle;\n            }\n            TaskState::Cancelled => {\n                item.state = TaskState::Cancelled;\n            }\n        }\n        Ok(())\n    }\n}\n\npub struct TaskManager {\n    /// cron manager\n    timer: Arc<Mutex<DelayTimer>>,\n    // Add a mutex to protect the concurrency of the storage\n    pub(super) storage: Arc<Mutex<TaskStorage>>,\n    task_events: Arc<TaskEvents>,\n    /// task list\n    list: TaskList,\n    restore_list: TaskList,\n    id_generator: SnowflakeIdGenerator,\n}\n\nimpl TaskManager {\n    pub fn new(storage: TaskStorage) -> Self {\n        let storage = Arc::new(Mutex::new(storage));\n        let task_events = TaskEvents::new(storage.clone());\n        Self {\n            timer: Arc::new(Mutex::new(DelayTimerBuilder::default().build())),\n            storage,\n            task_events: Arc::new(task_events),\n            restore_list: Arc::new(RW::new(Vec::new())),\n            list: Arc::new(RW::new(Vec::new())),\n            id_generator: SnowflakeIdGenerator::new(1, 1),\n        }\n    }\n\n    #[doc(hidden)]\n    /// a hidden method to get the inner task storage\n    /// just for internal jobs to use\n    pub(super) fn get_inner_task_storage(&self) -> Arc<Mutex<TaskStorage>> {\n        self.storage.clone()\n    }\n\n    pub fn restore_tasks(&mut self, tasks: Vec<Task>) {\n        let mut list = self.restore_list.write();\n        list.clear();\n        for task in tasks {\n            list.push(task);\n        }\n    }\n\n    /// add task\n    ///\n    /// # Example\n    /// ```rust\n    /// let task = Task {\n    ///    name: \"test\".to_string(),\n    ///    schedule: TaskSchedule::Once(Duration::from_secs(1)),\n    ///   ..Task::default()\n    /// };\n    /// let job = Job::default();\n    /// task_manager.add_task(task, job.into());\n    pub fn add_task(&mut self, task: Task) -> Result<()> {\n        check_task_input!(task);\n\n        let (mut task, mut builder) = {\n            let list = self.list.read();\n            build_task(task, list.len())\n        };\n        let restored_task = self.get_task_from_restored(task.id);\n        if let Some(restored_task) = restored_task\n            && restored_task.name == task.name\n        {\n            task.last_run = restored_task.last_run;\n            task.created_at = restored_task.created_at;\n        }\n\n        let task_id = task.id;\n        let id_generator = self.id_generator;\n        let list_ref = self.list.clone();\n        let executor = task.executor.clone();\n        let task_events = self.task_events.clone();\n        let timer_task = match executor {\n            TaskExecutor::Sync(job) => {\n                let body = move || {\n                    let list = list_ref.clone();\n                    let mut id_generator = id_generator;\n                    wrap_job!(job.execute(), list, id_generator, task_id, task_events);\n                };\n                builder.spawn_routine(body)\n            }\n            TaskExecutor::Async(async_job) => {\n                let body = move || {\n                    let list = list_ref.clone();\n                    let async_job = async_job.clone();\n                    let mut id_generator = id_generator;\n                    let task_events = task_events.clone();\n                    async move {\n                        wrap_job!(\n                            async_job.execute().await,\n                            list,\n                            id_generator,\n                            task_id,\n                            task_events\n                        );\n                    }\n                };\n\n                builder.spawn_async_routine(body)\n            }\n        };\n\n        {\n            builder.free(); // 在错误处理之前，先释放内存\n        }\n\n        let timer = self.timer.lock();\n        let mut list = self.list.write();\n        timer\n            .add_task(timer_task.map_err(|e| {\n                Error::new_task_error(\"failed to create a delay task instance\".to_string(), e)\n            })?)\n            .map_err(|e| {\n                Error::new_task_error(\"failed to add a task to scheduler\".to_string(), e)\n            })?;\n        let storage = self.storage.lock();\n        let task_id = task.id;\n        list.push(task);\n        storage.add_task(task_id)?;\n        Ok(())\n    }\n\n    fn get_task_from_restored(&self, task_id: TaskID) -> Option<Task> {\n        let list = self.restore_list.read();\n        list.iter().find(|t| t.id == task_id).cloned()\n    }\n\n    #[allow(dead_code)]\n    pub fn pick_task(&self, task_id: TaskID) -> Result<Task> {\n        let list = self.list.read();\n        list.iter()\n            .find(|t| t.id == task_id)\n            .cloned()\n            .ok_or(Error::CreateTaskFailed(TaskCreationError::NotFound))\n    }\n\n    #[allow(dead_code)]\n    pub fn total(&self) -> usize {\n        let list = self.list.read();\n        list.len()\n    }\n\n    // get current task list\n    // note: this method will clone the task list\n    pub fn list(&self) -> Vec<Task> {\n        let list = self.list.read();\n        list.clone()\n    }\n\n    pub fn remove_task(&mut self, task_id: TaskID) -> Result<()> {\n        let mut list = self.list.write();\n        let index = list\n            .iter()\n            .position(|t| t.id == task_id)\n            .ok_or(Error::CreateTaskFailed(TaskCreationError::NotFound))?;\n        self.timer\n            .lock()\n            .remove_task(task_id)\n            .map_err(|e| Error::new_task_error(\"failed to remove task\".to_string(), e))?;\n        list.remove(index);\n        let storage = self.storage.lock();\n        storage.remove_task(task_id)?;\n        Ok(())\n    }\n\n    pub fn advance_task(&mut self, task_id: TaskID) -> Result<()> {\n        let timer = self.timer.lock();\n        timer\n            .advance_task(task_id)\n            .map_err(|e| Error::new_task_error(\"failed to advance a task\".to_string(), e))?;\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "backend/tauri/src/core/tasks/utils.rs",
    "content": "use thiserror::Error;\n\n#[derive(Debug)]\npub enum TaskCreationError {\n    #[allow(unused)]\n    AlreadyExist,\n    NotFound,\n}\n\n#[derive(Error, Debug)]\npub enum Error {\n    #[error(\"create task failed: {0:?}\")]\n    CreateTaskFailed(TaskCreationError),\n\n    #[error(\"params validation failed: {0}\")]\n    ParamsValidationFailed(&'static str),\n\n    #[error(\"database operation failed: {0:?}\")]\n    DatabaseOperationFailed(#[from] redb::DatabaseError),\n\n    #[error(\"database transaction failed: {0:?}\")]\n    DatabaseTransactionFailed(#[from] redb::TransactionError),\n\n    #[error(\"database table operation failed: {0:?}\")]\n    DatabaseTableOperationFailed(#[from] redb::TableError),\n\n    #[error(\"database storage operation failed: {0:?}\")]\n    DatabaseStorageOperationFailed(#[from] redb::StorageError),\n\n    #[error(\"database commit operation failed: {0:?}\")]\n    DatabaseCommitOperationFailed(#[from] redb::CommitError),\n\n    #[error(\"json parse failed: {0:?}\")]\n    JsonParseFailed(#[from] serde_json::Error),\n\n    #[error(\"task issue failed: {message:?}\")]\n    InnerTask {\n        message: String,\n        #[source]\n        source: delay_timer::error::TaskError,\n    },\n\n    #[error(transparent)]\n    Other(#[from] anyhow::Error),\n}\n\npub type Result<T> = std::result::Result<T, Error>;\n\nimpl Error {\n    pub fn new_task_error(message: String, source: delay_timer::error::TaskError) -> Self {\n        Self::InnerTask { message, source }\n    }\n}\n\npub trait ConfigChangedNotifier {\n    #[allow(dead_code)]\n    fn notify_config_changed(&self, task_name: &str) -> Result<()>;\n}\n"
  },
  {
    "path": "backend/tauri/src/core/tray/icon.rs",
    "content": "use crate::utils::dirs::tray_icons_path;\nuse serde::{Deserialize, Serialize};\nuse specta::Type;\nuse std::{\n    borrow::Cow,\n    fmt::{Display, Formatter},\n    path::PathBuf,\n};\n\n#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, Type)]\n#[serde(rename_all = \"snake_case\")]\npub enum TrayIcon {\n    #[default]\n    Normal,\n    Tun,\n    SystemProxy,\n}\n\nimpl Display for TrayIcon {\n    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {\n        match self {\n            TrayIcon::Normal => write!(f, \"normal\"),\n            TrayIcon::Tun => write!(f, \"tun\"),\n            TrayIcon::SystemProxy => write!(f, \"system_proxy\"),\n        }\n    }\n}\n\nimpl From<TrayIcon> for &'static str {\n    fn from(icon: TrayIcon) -> Self {\n        match icon {\n            TrayIcon::Normal => \"normal\",\n            TrayIcon::Tun => \"tun\",\n            TrayIcon::SystemProxy => \"system_proxy\",\n        }\n    }\n}\n\nimpl From<&TrayIcon> for &'static str {\n    fn from(icon: &TrayIcon) -> Self {\n        match icon {\n            TrayIcon::Normal => \"normal\",\n            TrayIcon::Tun => \"tun\",\n            TrayIcon::SystemProxy => \"system_proxy\",\n        }\n    }\n}\n\nimpl TrayIcon {\n    pub fn raw_bytes(&self) -> &'static [u8] {\n        match self {\n            TrayIcon::Normal => include_bytes!(\"../../../icons/win-tray-icon.png\"),\n            TrayIcon::Tun => include_bytes!(\"../../../icons/win-tray-icon-blue.png\"),\n            TrayIcon::SystemProxy => include_bytes!(\"../../../icons/win-tray-icon-pink.png\"),\n        }\n    }\n\n    pub fn all_supported() -> &'static [TrayIcon] {\n        &[TrayIcon::Normal, TrayIcon::Tun, TrayIcon::SystemProxy]\n    }\n\n    pub fn as_str(&self) -> &'static str {\n        match self {\n            TrayIcon::Normal => \"normal\",\n            TrayIcon::Tun => \"tun\",\n            TrayIcon::SystemProxy => \"system_proxy\",\n        }\n    }\n}\n\n#[tracing_attributes::instrument]\npub fn get_raw_icon<'n>(mode: TrayIcon) -> Cow<'n, [u8]> {\n    match tray_icons_path(mode.as_str()) {\n        Ok(path) if path.exists() => match std::fs::read(path) {\n            Ok(bytes) => Cow::Owned(bytes),\n            Err(e) => {\n                tracing::error!(\"failed to read icon file: {:?}\", e);\n                Cow::Borrowed(mode.raw_bytes())\n            }\n        },\n        _ => Cow::Borrowed(mode.raw_bytes()),\n    }\n}\n\n#[tracing_attributes::instrument]\nfn resize_image(mode: TrayIcon, scale_factor: f64) {\n    let raw_icon: Cow<[u8]> = get_raw_icon(mode);\n    let icon = match crate::utils::help::resize_tray_image(&raw_icon, scale_factor) {\n        Ok(icon) => icon,\n        Err(e) => {\n            tracing::error!(\"failed to resize icon: {:?}\", e);\n            raw_icon.to_vec()\n        }\n    };\n    let cache_dir = crate::utils::dirs::cache_dir().unwrap().join(\"icons\");\n    if !cache_dir.exists()\n        && let Err(e) = std::fs::create_dir_all(&cache_dir)\n    {\n        tracing::error!(\"failed to create cache dir: {:?}\", e);\n    }\n    if let Err(e) = std::fs::write(cache_dir.join(format!(\"tray_{mode}.png\")), icon) {\n        tracing::error!(\"failed to write icon file: {:?}\", e);\n    }\n}\n\n// TODO: migrate to async fn\n#[tracing_attributes::instrument]\npub fn resize_images(scale_factor: f64) {\n    for item in TrayIcon::all_supported() {\n        resize_image(*item, scale_factor);\n    }\n}\n\npub fn set_icon(mode: TrayIcon, path: Option<PathBuf>) -> anyhow::Result<()> {\n    match path {\n        Some(path) => {\n            // try parse path and convert image to png\n            let image = image::open(&path)?;\n            image.save(tray_icons_path(mode.as_str())?)?;\n        }\n        None => {\n            // use default icon\n            std::fs::remove_file(tray_icons_path(mode.as_str())?)?;\n        }\n    }\n    let factor = crate::utils::help::get_max_scale_factor();\n    resize_image(mode, factor);\n    Ok(())\n}\n\npub fn on_scale_factor_changed(scale_factor: f64) {\n    resize_images(scale_factor);\n}\n\n#[allow(dead_code)]\npub fn get_icon(mode: &TrayIcon) -> Vec<u8> {\n    let cache_file = crate::utils::dirs::cache_dir()\n        .unwrap()\n        .join(\"icons\")\n        .join(format!(\"tray_{mode}.png\"));\n    match std::fs::read(&cache_file) {\n        Ok(bytes) if bytes.starts_with(&[0x89, 0x50, 0x4E, 0x47]) => {\n            tracing::info!(\"use cached icon: {:?}\", cache_file);\n            bytes\n        }\n        Err(e) => {\n            tracing::error!(\"failed to read icon file: {:?}\", e);\n            mode.raw_bytes().to_vec()\n        }\n        _ => {\n            tracing::error!(\"invalid icon file: {:?}\", cache_file);\n            mode.raw_bytes().to_vec()\n        }\n    }\n}\n"
  },
  {
    "path": "backend/tauri/src/core/tray/mod.rs",
    "content": "use std::borrow::Cow;\n\nuse crate::{\n    config::{Config, nyanpasu::ClashCore},\n    feat, ipc, log_err,\n    utils::{help, resolve},\n};\nuse anyhow::Result;\nuse once_cell::sync::Lazy;\nuse parking_lot::Mutex;\nuse rust_i18n::t;\nuse tauri::{\n    AppHandle, Manager, Runtime,\n    menu::{Menu, MenuBuilder, MenuEvent, MenuItemBuilder, SubmenuBuilder},\n    tray::{MouseButton, TrayIcon, TrayIconBuilder, TrayIconEvent},\n};\nuse tracing_attributes::instrument;\n\npub mod icon;\npub mod proxies;\npub use self::icon::on_scale_factor_changed;\nuse self::proxies::SystemTrayMenuProxiesExt;\n\n#[cfg(target_os = \"linux\")]\nuse std::sync::atomic::AtomicU16;\n\nstruct TrayState<R: Runtime> {\n    menu: Mutex<Menu<R>>,\n}\n\npub struct Tray {}\n\nstatic UPDATE_SYSTRAY_MUTEX: Lazy<parking_lot::Mutex<()>> =\n    Lazy::new(|| parking_lot::Mutex::new(()));\n\nconst TRAY_ID: &str = \"main-tray\";\n\n#[cfg(target_os = \"linux\")]\nstatic LINUX_TRAY_ID: AtomicU16 = AtomicU16::new(0);\n// #[cfg(target_os = \"linux\")]\n// fn bump_tray_id() -> Cow<'static, str> {\n//     let id = LINUX_TRAY_ID.fetch_add(1, std::sync::atomic::Ordering::Release) + 1;\n//     Cow::Owned(format!(\"{}-{}\", TRAY_ID, id))\n// }\n\n#[inline]\nfn get_tray_id<'n>() -> Cow<'n, str> {\n    #[cfg(target_os = \"linux\")]\n    {\n        let id = LINUX_TRAY_ID.load(std::sync::atomic::Ordering::Acquire);\n        Cow::Owned(format!(\"{}-{}\", TRAY_ID, id))\n    }\n    #[cfg(not(target_os = \"linux\"))]\n    {\n        Cow::Borrowed(TRAY_ID)\n    }\n}\n\n// fn dummy_print_submenu<R: Runtime>(submenu: &Submenu<R>) {\n//     for item in submenu.items().unwrap() {\n//         tracing::debug!(\"item: {:#?}\", item.id());\n//         match item {\n//             tauri::menu::MenuItemKind::MenuItem(item) => {\n//                 tracing::debug!(\n//                     \"item: {:#?}, type: MenuItem, text: {:#?}\",\n//                     item.id(),\n//                     item.text()\n//                 );\n//             }\n//             tauri::menu::MenuItemKind::Submenu(submenu) => {\n//                 tracing::debug!(\n//                     \"item: {:#?}, type: Submenu, text: {:#?}\",\n//                     submenu.id(),\n//                     submenu.text()\n//                 );\n//                 dummy_print_submenu(&submenu);\n//             }\n//             tauri::menu::MenuItemKind::Predefined(item) => {\n//                 tracing::debug!(\n//                     \"item: {:#?}, type: Predefined, text: {:#?}\",\n//                     item.id(),\n//                     item.text()\n//                 );\n//             }\n//             tauri::menu::MenuItemKind::Check(item) => {\n//                 tracing::debug!(\n//                     \"item: {:#?}, type: Check, text: {:#?}\",\n//                     item.id(),\n//                     item.text()\n//                 );\n//             }\n//             tauri::menu::MenuItemKind::Icon(item) => {\n//                 tracing::debug!(\n//                     \"item: {:#?}, type: Icon, text: {:#?}\",\n//                     item.id(),\n//                     item.text()\n//                 );\n//             }\n//         }\n//     }\n// }\n\n// fn dummy_print_menu<R: Runtime>(menu: &Menu<R>) {\n//     for item in menu.items().unwrap() {\n//         tracing::debug!(\"item: {:#?}\", item.id());\n//         match item {\n//             tauri::menu::MenuItemKind::MenuItem(item) => {\n//                 tracing::debug!(\n//                     \"item: {:#?}, type: MenuItem, text: {:#?}\",\n//                     item.id(),\n//                     item.text()\n//                 );\n//             }\n//             tauri::menu::MenuItemKind::Submenu(submenu) => {\n//                 tracing::debug!(\n//                     \"item: {:#?}, type: Submenu, text: {:#?}\",\n//                     submenu.id(),\n//                     submenu.text()\n//                 );\n//                 dummy_print_submenu(&submenu);\n//             }\n//             tauri::menu::MenuItemKind::Predefined(item) => {\n//                 tracing::debug!(\n//                     \"item: {:#?}, type: Predefined, text: {:#?}\",\n//                     item.id(),\n//                     item.text()\n//                 );\n//             }\n//             tauri::menu::MenuItemKind::Check(item) => {\n//                 tracing::debug!(\n//                     \"item: {:#?}, type: Check, text: {:#?}\",\n//                     item.id(),\n//                     item.text()\n//                 );\n//             }\n//             tauri::menu::MenuItemKind::Icon(item) => {\n//                 tracing::debug!(\n//                     \"item: {:#?}, type: Icon, text: {:#?}\",\n//                     item.id(),\n//                     item.text()\n//                 );\n//             }\n//         }\n//     }\n// }\n\nimpl Tray {\n    #[instrument(skip(app_handle))]\n    pub fn tray_menu<R: Runtime>(app_handle: &AppHandle<R>) -> Result<Menu<R>> {\n        let version = env!(\"NYANPASU_VERSION\");\n        let core = {\n            *Config::verge()\n                .latest()\n                .clash_core\n                .as_ref()\n                .unwrap_or(&ClashCore::default())\n        };\n        let mut menu = MenuBuilder::new(app_handle)\n            .text(\"open_window\", t!(\"tray.dashboard\"))\n            .setup_proxies(app_handle)? // Setup the proxies menu\n            .separator()\n            .check(\"rule_mode\", t!(\"tray.rule_mode\"))\n            .check(\"global_mode\", t!(\"tray.global_mode\"))\n            .check(\"direct_mode\", t!(\"tray.direct_mode\"));\n        if core == ClashCore::ClashPremium {\n            menu = menu.check(\"script_mode\", t!(\"tray.script_mode\"));\n        }\n        menu = menu\n            .separator()\n            .check(\"system_proxy\", t!(\"tray.system_proxy\"))\n            .check(\"tun_mode\", t!(\"tray.tun_mode\"))\n            .separator()\n            .text(\"copy_env_sh\", t!(\"tray.copy_env.sh\"))\n            .text(\"copy_env_cmd\", t!(\"tray.copy_env.cmd\"))\n            .text(\"copy_env_ps\", t!(\"tray.copy_env.ps\"))\n            .item(\n                &SubmenuBuilder::new(app_handle, t!(\"tray.open_dir.menu\"))\n                    .text(\"open_app_config_dir\", t!(\"tray.open_dir.app_config_dir\"))\n                    .text(\"open_app_data_dir\", t!(\"tray.open_dir.app_data_dir\"))\n                    .text(\"open_core_dir\", t!(\"tray.open_dir.core_dir\"))\n                    .text(\"open_logs_dir\", t!(\"tray.open_dir.log_dir\"))\n                    .build()?,\n            )\n            .item(\n                &SubmenuBuilder::new(app_handle, t!(\"tray.more.menu\"))\n                    .text(\"restart_clash\", t!(\"tray.more.restart_clash\"))\n                    .text(\"restart_app\", t!(\"tray.more.restart_app\"))\n                    .item(\n                        &MenuItemBuilder::new(format!(\"Version {version}\"))\n                            .id(\"app_version\")\n                            .enabled(false)\n                            .build(app_handle)?,\n                    )\n                    .build()?,\n            )\n            .separator()\n            .item(\n                &MenuItemBuilder::new(t!(\"tray.quit\"))\n                    .id(\"quit\")\n                    .accelerator(\"CmdOrControl+Q\")\n                    .build(app_handle)?,\n            );\n\n        Ok(menu.build()?)\n    }\n\n    #[instrument(skip(app_handle))]\n    pub fn update_systray(app_handle: &AppHandle<tauri::Wry>) -> Result<()> {\n        let _guard = UPDATE_SYSTRAY_MUTEX.lock();\n        let tray_id = get_tray_id();\n        let tray = {\n            // if cfg!(target_os = \"linux\") {\n            //     tracing::debug!(\"removing tray by id: {}\", tray_id);\n            //     let mut tray = app_handle.remove_tray_by_id(tray_id.as_ref());\n            //     tray.take(); // Drop the tray\n            //     tray_id = bump_tray_id();\n            //     tracing::debug!(\"bumped tray id to: {}\", tray_id);\n            // }\n            app_handle.tray_by_id(tray_id.as_ref())\n        };\n\n        let menu = Tray::tray_menu(app_handle)?;\n        let tray = match tray {\n            None => {\n                let mut builder = TrayIconBuilder::with_id(tray_id);\n                #[cfg(any(windows, target_os = \"linux\"))]\n                {\n                    builder = builder.icon(tauri::image::Image::from_bytes(&icon::get_icon(\n                        &icon::TrayIcon::Normal,\n                    ))?);\n                }\n                #[cfg(target_os = \"macos\")]\n                {\n                    builder = builder\n                        .icon(tauri::image::Image::from_bytes(include_bytes!(\n                            \"../../../icons/tray-icon.png\"\n                        ))?)\n                        .icon_as_template(true);\n                }\n                builder\n                    .menu(&menu)\n                    .on_menu_event(|app, event| {\n                        Tray::on_menu_item_event(app, event);\n                    })\n                    .on_tray_icon_event(|tray_icon, event| {\n                        Tray::on_system_tray_event(tray_icon, event);\n                    })\n                    .show_menu_on_left_click(false)\n                    .build(app_handle)?\n            }\n            Some(tray) => {\n                // This is a workaround for linux tray menu update. Due to the api disallow set_menu again\n                // and recreate tray icon will cause buggy tray. No icon and no menu.\n                // So this block is a dirty inheritance of the menu items from the previous tray menu.\n                if cfg!(target_os = \"linux\") {\n                    let state = app_handle.state::<TrayState<tauri::Wry>>();\n                    let previous_menu = state.menu.lock();\n                    if let Ok(items) = previous_menu.items() {\n                        tracing::debug!(\"removing previous tray menu items\");\n                        for item in items {\n                            log_err!(previous_menu.remove(&item), \"failed to remove menu item\");\n                        }\n                    }\n                    // migrate the menu items\n                    if let Ok(items) = menu.items() {\n                        tracing::debug!(\"migrating new tray menu items\");\n                        for item in items {\n                            log_err!(previous_menu.append(&item), \"failed to append menu item\");\n                        }\n                    }\n                } else {\n                    tray.set_menu(Some(menu.clone()))?;\n                }\n                tray\n            }\n        };\n        tray.set_visible(true)?;\n        {\n            match app_handle.try_state::<TrayState<tauri::Wry>>() {\n                Some(state) if cfg!(not(target_os = \"linux\")) => {\n                    tracing::debug!(\"replacing previous tray menu\");\n                    *state.menu.lock() = menu;\n                }\n                None => {\n                    tracing::debug!(\"creating new tray menu\");\n                    app_handle.manage(TrayState {\n                        menu: Mutex::new(menu),\n                    });\n                }\n                _ => {}\n            }\n        }\n        tracing::debug!(\"full update tray finished\");\n        Tray::update_part(app_handle)?;\n        Ok(())\n    }\n\n    #[instrument(skip(app_handle))]\n    pub fn update_part<R: Runtime>(app_handle: &AppHandle<R>) -> Result<()> {\n        let mode = crate::utils::config::get_current_clash_mode();\n        let core = {\n            *Config::verge()\n                .latest()\n                .clash_core\n                .as_ref()\n                .unwrap_or(&ClashCore::default())\n        };\n        let tray_id = get_tray_id();\n        tracing::debug!(\"updating tray part: {}\", tray_id);\n        let tray = app_handle\n            .tray_by_id(tray_id.as_ref())\n            .expect(\"tray not found\");\n        let state = app_handle.state::<TrayState<R>>();\n        let menu = state.menu.lock();\n\n        let _ = menu\n            .get(\"rule_mode\")\n            .and_then(|item| item.as_check_menuitem()?.set_checked(mode == \"rule\").ok());\n        let _ = menu\n            .get(\"global_mode\")\n            .and_then(|item| item.as_check_menuitem()?.set_checked(mode == \"global\").ok());\n        let _ = menu\n            .get(\"direct_mode\")\n            .and_then(|item| item.as_check_menuitem()?.set_checked(mode == \"direct\").ok());\n        if core == ClashCore::ClashPremium {\n            let _ = menu\n                .get(\"script_mode\")\n                .and_then(|item| item.as_check_menuitem()?.set_checked(mode == \"script\").ok());\n        }\n\n        #[allow(unused_variables)]\n        let (system_proxy, tun_mode, enable_tray_text) = {\n            let verge = Config::verge();\n            let verge = verge.latest();\n            (\n                *verge.enable_system_proxy.as_ref().unwrap_or(&false),\n                *verge.enable_tun_mode.as_ref().unwrap_or(&false),\n                *verge.enable_tray_text.as_ref().unwrap_or(&false),\n            )\n        };\n\n        #[cfg(any(target_os = \"windows\", target_os = \"linux\"))]\n        {\n            use icon::TrayIcon;\n\n            let mode = if tun_mode {\n                TrayIcon::Tun\n            } else if system_proxy {\n                TrayIcon::SystemProxy\n            } else {\n                TrayIcon::Normal\n            };\n            let icon = icon::get_icon(&mode);\n            let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&icon)?));\n        }\n\n        let _ = menu\n            .get(\"system_proxy\")\n            .and_then(|item| item.as_check_menuitem()?.set_checked(system_proxy).ok());\n        let _ = menu\n            .get(\"tun_mode\")\n            .and_then(|item| item.as_check_menuitem()?.set_checked(tun_mode).ok());\n\n        let switch_map = {\n            let mut map = std::collections::HashMap::new();\n            map.insert(true, t!(\"tray.proxy_action.on\"));\n            map.insert(false, t!(\"tray.proxy_action.off\"));\n            map\n        };\n\n        #[cfg(not(target_os = \"linux\"))]\n        {\n            let _ = tray.set_tooltip(Some(&format!(\n                \"{}: {}\\n{}: {}\",\n                t!(\"tray.system_proxy\"),\n                switch_map[&system_proxy],\n                t!(\"tray.tun_mode\"),\n                switch_map[&tun_mode]\n            )));\n        }\n        #[cfg(target_os = \"linux\")]\n        {\n            if enable_tray_text {\n                let _ = tray.set_title(Some(&format!(\n                    \"{}: {}\\n{}: {}\",\n                    t!(\"tray.system_proxy\"),\n                    switch_map[&system_proxy],\n                    t!(\"tray.tun_mode\"),\n                    switch_map[&tun_mode]\n                )));\n            } else {\n                let _ = tray.set_title::<&str>(None);\n            }\n        }\n\n        Ok(())\n    }\n\n    #[instrument(skip(app_handle, event))]\n    pub fn on_menu_item_event(app_handle: &AppHandle, event: MenuEvent) {\n        let id = event.id().0.as_str();\n        match id {\n            mode @ (\"rule_mode\" | \"global_mode\" | \"direct_mode\" | \"script_mode\") => {\n                let mode = &mode[0..mode.len() - 5];\n                feat::change_clash_mode(mode.into());\n            }\n\n            \"open_window\" => resolve::create_window(app_handle),\n            \"system_proxy\" => feat::toggle_system_proxy(),\n            \"tun_mode\" => feat::toggle_tun_mode(),\n            \"copy_env_sh\" => feat::copy_clash_env(app_handle, \"sh\"),\n            #[cfg(target_os = \"windows\")]\n            \"copy_env_cmd\" => feat::copy_clash_env(app_handle, \"cmd\"),\n            #[cfg(target_os = \"windows\")]\n            \"copy_env_ps\" => feat::copy_clash_env(app_handle, \"ps\"),\n            \"open_app_config_dir\" => crate::log_err!(ipc::open_app_config_dir()),\n            \"open_app_data_dir\" => crate::log_err!(ipc::open_app_data_dir()),\n            \"open_core_dir\" => crate::log_err!(ipc::open_core_dir()),\n            \"open_logs_dir\" => crate::log_err!(ipc::open_logs_dir()),\n            \"restart_clash\" => feat::restart_clash_core(),\n            \"restart_app\" => help::restart_application(app_handle),\n            \"quit\" => {\n                help::quit_application(app_handle);\n            }\n            _ => {\n                proxies::on_system_tray_event(id);\n            }\n        }\n    }\n\n    pub fn on_system_tray_event(tray_icon: &TrayIcon, event: TrayIconEvent) {\n        if let TrayIconEvent::Click {\n            button: MouseButton::Left,\n            ..\n        } = event\n        {\n            resolve::create_window(tray_icon.app_handle());\n        }\n    }\n}\n"
  },
  {
    "path": "backend/tauri/src/core/tray/proxies.rs",
    "content": "use crate::{\n    config::{Config, nyanpasu::ProxiesSelectorMode},\n    core::{\n        clash::proxies::{Proxies, ProxiesGuard, ProxiesGuardExt},\n        handle::Handle,\n    },\n};\nuse anyhow::Context;\nuse indexmap::IndexMap;\nuse tauri::{AppHandle, Manager, Runtime, menu::MenuBuilder};\nuse tracing::{debug, error, warn};\nuse tracing_attributes::instrument;\n\n#[instrument]\nasync fn loop_task() {\n    loop {\n        match ProxiesGuard::global().update().await {\n            Ok(_) => {\n                debug!(\"update proxies success\");\n            }\n            Err(e) => {\n                warn!(\"update proxies failed: {:?}\", e);\n            }\n        }\n        {\n            let guard = ProxiesGuard::global().read();\n            if guard.updated_at() == 0 {\n                error!(\"proxies not updated yet!!!!\");\n                // TODO: add a error dialog or notification, and panic?\n            }\n\n            // else {\n            //     let proxies = guard.inner();\n            //     let str = simd_json::to_string_pretty(proxies).unwrap();\n            //     debug!(target: \"tray\", \"proxies info: {:?}\", str);\n            // }\n        }\n        tokio::time::sleep(tokio::time::Duration::from_secs(10)).await; // TODO: add a config to control the interval\n    }\n}\n\ntype GroupName = String;\ntype ProxyName = String;\ntype FromProxy = ProxyName;\ntype ToProxy = ProxyName;\ntype ProxySelectAction = (GroupName, FromProxy, ToProxy);\n#[derive(PartialEq)]\nenum TrayUpdateType {\n    None,\n    Full,\n    Part(Vec<ProxySelectAction>),\n}\n\nstruct TrayProxyItem {\n    current: Option<String>,\n    all: Vec<String>,\n    r#type: String, // TODO: 转成枚举\n}\ntype TrayProxies = IndexMap<String, TrayProxyItem>;\n\n/// Convert raw proxies to tray proxies\nfn to_tray_proxies(mode: &str, raw_proxies: &Proxies) -> TrayProxies {\n    let mut tray_proxies = TrayProxies::new();\n    if matches!(mode, \"global\" | \"rule\" | \"script\") {\n        if mode == \"global\" || raw_proxies.proxies.is_empty() {\n            let global = TrayProxyItem {\n                current: raw_proxies.global.now.clone(),\n                all: raw_proxies\n                    .global\n                    .all\n                    .iter()\n                    .map(|x| x.name.to_owned())\n                    .collect(),\n                r#type: \"Selector\".to_string(),\n            };\n            tray_proxies.insert(\"global\".to_owned(), global);\n        }\n        for raw_group in raw_proxies.groups.iter() {\n            let group = TrayProxyItem {\n                current: raw_group.now.clone(),\n                all: raw_group.all.iter().map(|x| x.name.to_owned()).collect(),\n                r#type: raw_group.r#type.clone(),\n            };\n            tray_proxies.insert(raw_group.name.to_owned(), group);\n        }\n    }\n    tray_proxies\n}\n\nfn diff_proxies(old_proxies: &TrayProxies, new_proxies: &TrayProxies) -> TrayUpdateType {\n    // 1. check if the length of two map is different\n    if old_proxies.len() != new_proxies.len() {\n        return TrayUpdateType::Full;\n    }\n    // 2. check if the group matching\n    let group_matching = new_proxies\n        .keys()\n        .cloned()\n        .collect::<Vec<String>>()\n        .iter()\n        .zip(&old_proxies.keys().cloned().collect::<Vec<String>>())\n        .filter(|&(new, old)| new == old)\n        .count();\n    if group_matching != old_proxies.len() {\n        return TrayUpdateType::Full;\n    }\n    // 3. start checking the group content\n    let mut actions = Vec::new();\n    for (group, item) in new_proxies.iter() {\n        let old_item = old_proxies.get(group).unwrap(); // safe to unwrap\n\n        // check if the length of all list is different\n        if item.all.len() != old_item.all.len() {\n            return TrayUpdateType::Full;\n        }\n\n        // first diff the all list\n        let all_matching = item\n            .all\n            .iter()\n            .zip(&old_item.all)\n            .filter(|&(new, old)| new == old)\n            .count();\n        if all_matching != old_item.all.len() {\n            return TrayUpdateType::Full;\n        }\n        // then diff the current\n        if item.current != old_item.current {\n            actions.push((\n                group.clone(),\n                old_item.current.clone().unwrap(),\n                item.current.clone().unwrap(),\n            ));\n        }\n    }\n    if actions.is_empty() {\n        TrayUpdateType::None\n    } else {\n        TrayUpdateType::Part(actions)\n    }\n}\n\n#[instrument]\npub async fn proxies_updated_receiver() {\n    let (mut rx, mut tray_proxies_holder) = {\n        let guard = ProxiesGuard::global().read();\n        let proxies = guard.inner().to_owned();\n        let mode = crate::utils::config::get_current_clash_mode();\n        (\n            guard.get_receiver(),\n            to_tray_proxies(mode.as_str(), &proxies),\n        )\n    };\n\n    loop {\n        match rx.recv().await {\n            Ok(_) => {\n                debug!(\"proxies updated\");\n                if Handle::global().app_handle.lock().is_none() {\n                    warn!(\"app handle not found\");\n                    continue;\n                }\n                Handle::mutate_proxies();\n                {\n                    let is_tray_selector_enabled = Config::verge()\n                        .latest()\n                        .clash_tray_selector\n                        .unwrap_or_default()\n                        != ProxiesSelectorMode::Hidden;\n                    if !is_tray_selector_enabled {\n                        continue;\n                    }\n                }\n                // Do diff check\n                let mode = crate::utils::config::get_current_clash_mode();\n                let current_tray_proxies =\n                    to_tray_proxies(mode.as_str(), ProxiesGuard::global().read().inner());\n\n                match diff_proxies(&tray_proxies_holder, &current_tray_proxies) {\n                    TrayUpdateType::Full => {\n                        debug!(\"should do full update\");\n\n                        tray_proxies_holder = current_tray_proxies;\n                        match Handle::emit(\"update_systray\", ()) {\n                            Ok(_) => {\n                                debug!(\"update systray success\");\n                            }\n                            Err(e) => {\n                                warn!(\"update systray failed: {:?}\", e);\n                            }\n                        }\n                    }\n                    TrayUpdateType::Part(action_list) => {\n                        debug!(\"should do partial update, op list: {:?}\", action_list);\n                        tray_proxies_holder = current_tray_proxies;\n                        platform_impl::update_selected_proxies(&action_list);\n                        debug!(\"update selected proxies success\");\n                    }\n                    _ => {}\n                }\n            }\n            Err(e) => {\n                warn!(\"proxies updated receiver failed: {:?}\", e);\n            }\n        }\n    }\n}\n\npub fn setup_proxies() {\n    tauri::async_runtime::spawn(loop_task());\n    tauri::async_runtime::spawn(proxies_updated_receiver());\n}\n\nmod platform_impl {\n    use super::{GroupName, ProxyName, ProxySelectAction, TrayProxyItem};\n    use crate::{\n        config::nyanpasu::ProxiesSelectorMode,\n        core::{clash::proxies::ProxiesGuard, handle::Handle},\n    };\n    use bimap::BiMap;\n    use once_cell::sync::Lazy;\n    use parking_lot::Mutex;\n    use rust_i18n::t;\n    use std::sync::atomic::AtomicBool;\n    use tauri::{\n        AppHandle, Manager, Runtime,\n        menu::{\n            CheckMenuItemBuilder, Menu, MenuBuilder, MenuItemBuilder, MenuItemKind, Submenu,\n            SubmenuBuilder,\n        },\n    };\n    use tracing::warn;\n\n    // It store a map of proxy nodes like \"GROUP_PROXY\" -> ID\n    // TODO: use Cow<str> instead of String\n    pub(super) static ITEM_IDS: Lazy<Mutex<BiMap<(GroupName, ProxyName), usize>>> =\n        Lazy::new(|| Mutex::new(BiMap::new()));\n\n    pub fn generate_group_selector<R: Runtime>(\n        app_handle: &AppHandle<R>,\n        group_name: &str,\n        group: &TrayProxyItem,\n    ) -> anyhow::Result<Submenu<R>> {\n        let mut item_ids = ITEM_IDS.lock();\n        let mut group_menu = SubmenuBuilder::new(app_handle, group_name);\n        if group.all.is_empty() {\n            group_menu = group_menu.item(\n                &MenuItemBuilder::new(t!(\"tray.no_proxies\"))\n                    .enabled(false)\n                    .build(app_handle)?,\n            );\n            return Ok(group_menu.build()?);\n        }\n        for item in group.all.iter() {\n            let key = (group_name.to_string(), item.to_string());\n            let id = item_ids.len();\n            item_ids.insert(key, id);\n            let mut sub_item_builder = CheckMenuItemBuilder::new(item.clone())\n                .id(format!(\"proxy_node_{id}\"))\n                .checked(false);\n            if let Some(now) = group.current.clone()\n                && now == item.as_str()\n            {\n                sub_item_builder = sub_item_builder.checked(true);\n            }\n\n            if !matches!(group.r#type.as_str(), \"Selector\" | \"Fallback\") {\n                sub_item_builder = sub_item_builder.enabled(false);\n            }\n\n            group_menu = group_menu.item(&sub_item_builder.build(app_handle)?);\n        }\n        Ok(group_menu.build()?)\n    }\n\n    pub fn generate_selectors<R: Runtime>(\n        app_handle: &AppHandle<R>,\n        proxies: &super::TrayProxies,\n    ) -> anyhow::Result<Vec<MenuItemKind<R>>> {\n        let mut items = Vec::new();\n        if proxies.is_empty() {\n            items.push(MenuItemKind::MenuItem(\n                MenuItemBuilder::new(t!(\"tray.no_proxies\"))\n                    .id(\"no_proxies\")\n                    .enabled(false)\n                    .build(app_handle)?,\n            ));\n            return Ok(items);\n        }\n        {\n            let mut item_ids = ITEM_IDS.lock();\n            item_ids.clear(); // clear the item ids\n        }\n        for (group, item) in proxies.iter() {\n            let group_menu = generate_group_selector(app_handle, group, item)?;\n            items.push(MenuItemKind::Submenu(group_menu));\n        }\n        Ok(items)\n    }\n\n    pub fn setup_tray<'m, R: Runtime, M: Manager<R>>(\n        app_handle: &AppHandle<R>,\n        mut menu: MenuBuilder<'m, R, M>,\n    ) -> anyhow::Result<MenuBuilder<'m, R, M>> {\n        let selector_mode = crate::config::Config::verge()\n            .latest()\n            .clash_tray_selector\n            .unwrap_or_default();\n        menu = match selector_mode {\n            ProxiesSelectorMode::Hidden => return Ok(menu),\n            ProxiesSelectorMode::Normal => menu.separator(),\n            ProxiesSelectorMode::Submenu => menu,\n        };\n        let proxies = ProxiesGuard::global().read().inner().to_owned();\n        let mode = crate::utils::config::get_current_clash_mode();\n        let tray_proxies = super::to_tray_proxies(mode.as_str(), &proxies);\n        let items = generate_selectors::<R>(app_handle, &tray_proxies)?;\n        match selector_mode {\n            ProxiesSelectorMode::Normal => {\n                for item in items {\n                    menu = menu.item(&item);\n                }\n            }\n            ProxiesSelectorMode::Submenu => {\n                let mut submenu = SubmenuBuilder::with_id(\n                    app_handle,\n                    \"select_proxies\",\n                    t!(\"tray.select_proxies\"),\n                );\n                for item in items {\n                    submenu = submenu.item(&item);\n                }\n                menu = menu.item(&submenu.build()?);\n            }\n            _ => {}\n        }\n        Ok(menu)\n    }\n\n    static TRAY_ITEM_UPDATE_BARRIER: AtomicBool = AtomicBool::new(false);\n\n    #[tracing_attributes::instrument]\n    pub fn update_selected_proxies(actions: &[ProxySelectAction]) {\n        if TRAY_ITEM_UPDATE_BARRIER.load(std::sync::atomic::Ordering::Acquire) {\n            warn!(\"tray item update is in progress, skip this update\");\n            return;\n        }\n        let app_handle = Handle::global().app_handle.lock();\n        let tray_state = app_handle\n            .as_ref()\n            .unwrap()\n            .state::<crate::core::tray::TrayState<tauri::Wry>>();\n        TRAY_ITEM_UPDATE_BARRIER.store(true, std::sync::atomic::Ordering::Release);\n        let menu = tray_state.menu.lock();\n        // comment it just because we could not get the access to the menu item via the id\n        // If the tauri team fixes this issue, we could use the following code to update the tray item\n        // let item_ids = ITEM_IDS.lock();\n        for action in actions {\n            //     #[cfg(not(target_os = \"linux\"))]\n            //     {\n            //         tracing::debug!(\"update selected proxies: {:?}\", action);\n            //         let from_id = match item_ids.get_by_left(&(action.0.clone(), action.1.clone())) {\n            //             Some(id) => *id,\n            //             None => {\n            //                 warn!(\"from item not found: {:?}\", action);\n            //                 continue;\n            //             }\n            //         };\n            //         let from_id = format!(\"proxy_node_{}\", from_id);\n\n            //         let to_id = match item_ids.get_by_left(&(action.0.clone(), action.2.clone())) {\n            //             Some(id) => *id,\n            //             None => {\n            //                 warn!(\"to item not found: {:?}\", action);\n            //                 continue;\n            //             }\n            //         };\n            //         let to_id = format!(\"proxy_node_{}\", to_id);\n\n            //         match menu.get(&from_id) {\n            //             Some(item) => match item.kind() {\n            //                 MenuItemKind::Check(item) => {\n            //                     if item.is_checked().is_ok_and(|x| x) {\n            //                         let _ = item.set_checked(false);\n            //                     }\n            //                 }\n            //                 MenuItemKind::MenuItem(item) => {\n            //                     let _ = item.set_text(action.1.clone());\n            //                 }\n            //                 _ => {\n            //                     warn!(\"failed to deselect, item is not a check item: {}\", from_id);\n            //                 }\n            //             },\n            //             None => {\n            //                 warn!(\"failed to deselect, item not found: {}\", from_id);\n            //             }\n            //         }\n            //         match menu.get(&to_id) {\n            //             Some(item) => match item.kind() {\n            //                 MenuItemKind::Check(item) => {\n            //                     if item.is_checked().is_ok_and(|x| !x) {\n            //                         let _ = item.set_checked(true);\n            //                     }\n            //                 }\n            //                 MenuItemKind::MenuItem(item) => {\n            //                     let _ = item.set_text(action.2.clone());\n            //                 }\n            //                 _ => {\n            //                     warn!(\"failed to select, item is not a check item: {}\", to_id);\n            //                 }\n            //             },\n            //             None => {\n            //                 warn!(\"failed to select, item not found: {}\", to_id);\n            //             }\n            //         }\n            //     }\n            // }\n\n            // here is a fucking workaround for id getter\n            #[inline]\n            fn find_check_item<R: Runtime>(\n                menu: &Menu<R>,\n                group: GroupName,\n                proxy: ProxyName,\n            ) -> Option<tauri::menu::CheckMenuItem<R>> {\n                menu.items()\n                    .ok()\n                    .and_then(|items| {\n                        items.into_iter().find(|i| matches!(i, tauri::menu::MenuItemKind::Submenu(submenu) if submenu.text().is_ok_and(|text| text == group) || submenu.id() == \"select_proxies\"))\n                    })\n                    .and_then(|submenu| {\n                        let submenu = submenu.as_submenu_unchecked();\n                        if submenu.id() == \"select_proxies\" {\n                            submenu.items().ok().and_then(|items| {\n                                items.into_iter().find(|i| matches!(i, tauri::menu::MenuItemKind::Submenu(submenu) if submenu.text().is_ok_and(|text| text == group)))\n                            })\n                            .and_then(|submenu| {\n                                submenu.as_submenu_unchecked().items().ok()\n                            })\n                        } else {\n                            submenu.items().ok()\n                        }\n                    })\n                    .and_then(|items| {\n                        items.into_iter().find(|i| matches!(i, tauri::menu::MenuItemKind::Check(item) if item.text().is_ok_and(|text| text == proxy)))\n                    }).map(|item| item.as_check_menuitem_unchecked().clone())\n            }\n\n            let from_item = find_check_item(&menu, action.0.clone(), action.1.clone());\n            match from_item {\n                Some(item) => {\n                    let _ = item.set_checked(false);\n                }\n                None => {\n                    warn!(\n                        \"failed to deselect, item not found: {} {}\",\n                        action.0, action.1\n                    );\n                }\n            }\n\n            let to_item = find_check_item(&menu, action.0.clone(), action.2.clone());\n            match to_item {\n                Some(item) => {\n                    let _ = item.set_checked(true);\n                }\n                None => {\n                    warn!(\n                        \"failed to select, item not found: {} {}\",\n                        action.0, action.2\n                    );\n                }\n            }\n        }\n        TRAY_ITEM_UPDATE_BARRIER.store(false, std::sync::atomic::Ordering::Release);\n    }\n}\n\npub trait SystemTrayMenuProxiesExt<R: Runtime> {\n    fn setup_proxies(self, app_handle: &AppHandle<R>) -> anyhow::Result<Self>\n    where\n        Self: Sized;\n}\n\nimpl<R: Runtime, M: Manager<R>> SystemTrayMenuProxiesExt<R> for MenuBuilder<'_, R, M> {\n    fn setup_proxies(self, app_handle: &AppHandle<R>) -> anyhow::Result<Self> {\n        platform_impl::setup_tray(app_handle, self)\n    }\n}\n\n#[instrument]\npub fn on_system_tray_event(event: &str) {\n    if !event.starts_with(\"proxy_node_\") {\n        return; // bypass non-select event\n    }\n    let node_id = event.split('_').next_back().unwrap(); // safe to unwrap\n    let node_id = match node_id.parse::<usize>() {\n        Ok(id) => id,\n        Err(e) => {\n            error!(\"parse node id failed: {:?}\", e);\n            return;\n        }\n    };\n\n    let (group, name) = {\n        let map = platform_impl::ITEM_IDS.lock();\n        let item = map.get_by_right(&node_id);\n        match item {\n            Some((group, name)) => (group.clone(), name.clone()),\n            None => {\n                error!(\"node id not found: {}\", node_id);\n                return;\n            }\n        }\n    };\n\n    let wrapper = move || -> anyhow::Result<()> {\n        tracing::debug!(\"received select proxy event: {} {}\", group, name);\n        tauri::async_runtime::block_on(async move {\n            ProxiesGuard::global()\n                .select_proxy(&group, &name)\n                .await\n                .with_context(|| format!(\"select proxy failed, {group} {name}, cause: \"))?;\n\n            debug!(\"select proxy success: {} {}\", group, name);\n            Ok::<(), anyhow::Error>(())\n        })?;\n        Ok(())\n    };\n\n    if let Err(e) = wrapper() {\n        // TODO: add a error dialog or notification\n        error!(\"on_system_tray_event failed: {:?}\", e);\n    }\n}\n"
  },
  {
    "path": "backend/tauri/src/core/updater/instance.rs",
    "content": "use super::shared::{self, CoreTypeMeta};\nuse crate::{\n    config::nyanpasu::ClashCore,\n    core::CoreManager,\n    utils::downloader::{DownloadStatus, Downloader, DownloaderBuilder, DownloaderState},\n};\nuse anyhow::anyhow;\nuse runas::Command as RunasCommand;\nuse serde::Serialize;\nuse specta::Type;\n#[cfg(target_family = \"unix\")]\nuse std::os::unix::fs::PermissionsExt;\nuse std::sync::Arc;\nuse tempfile::TempDir;\nuse tokio::sync::Mutex;\n\n#[derive(Debug, Clone, Serialize, Default, specta::Type)]\n#[serde(rename_all = \"snake_case\")]\npub enum UpdaterState {\n    #[default]\n    Idle,\n    Downloading,\n    Decompressing,\n    Replacing,\n    Restarting,\n    Done,\n    Failed(String),\n}\n\ntype DownloaderWithDynCallback = Downloader<Box<dyn Fn(DownloaderState) + Send + Sync>>;\n\npub(super) struct Updater {\n    id: usize,\n    temp_dir: TempDir,\n    core_type: ClashCore,\n    artifact: String,\n    inner: parking_lot::RwLock<UpdaterInner>,\n    rx: Mutex<tokio::sync::mpsc::Receiver<DownloaderState>>,\n    downloader: Arc<DownloaderWithDynCallback>,\n}\n\nstruct UpdaterInner {\n    state: UpdaterState,\n}\n\n#[derive(Debug, Serialize, Type)]\npub struct UpdaterSummary {\n    pub id: usize,\n    pub state: UpdaterState,\n    pub downloader: DownloadStatus,\n}\n\npub(super) struct UpdaterBuilder {\n    client: Option<reqwest::Client>,\n    core_type: Option<ClashCore>,\n    mirror: Option<String>,\n    artifact: Option<String>,\n    tag: Option<CoreTypeMeta>,\n}\n\nimpl UpdaterBuilder {\n    pub fn new() -> Self {\n        Self {\n            client: None,\n            core_type: None,\n            mirror: None,\n            artifact: None,\n            tag: None,\n        }\n    }\n\n    pub fn set_client(mut self, client: reqwest::Client) -> Self {\n        self.client = Some(client);\n        self\n    }\n\n    pub fn set_core_type(mut self, core_type: ClashCore) -> Self {\n        self.core_type = Some(core_type);\n        self\n    }\n\n    pub fn set_artifact(mut self, artifact: String) -> Self {\n        self.artifact = Some(artifact);\n        self\n    }\n\n    pub fn set_tag(mut self, tag: CoreTypeMeta) -> Self {\n        self.tag = Some(tag);\n        self\n    }\n\n    pub fn set_mirror(mut self, mirror: String) -> Self {\n        self.mirror = Some(mirror);\n        self\n    }\n\n    pub async fn build(self) -> anyhow::Result<Updater> {\n        let client = self.client.ok_or(anyhow::anyhow!(\"client is required\"))?;\n        let core_type = self\n            .core_type\n            .ok_or(anyhow::anyhow!(\"core_type is required\"))?;\n        let artifact = self\n            .artifact\n            .ok_or(anyhow::anyhow!(\"artifact is required\"))?;\n        let tag = self.tag.ok_or(anyhow::anyhow!(\"tag is required\"))?;\n        let mirror = self.mirror.ok_or(anyhow::anyhow!(\"mirror is required\"))?;\n\n        let temp_dir = TempDir::new()?;\n        let inner = UpdaterInner {\n            state: UpdaterState::Idle,\n        };\n\n        // setup downloader\n        let download_path = shared::get_download_path(tag, &artifact);\n        let mut download_url = url::Url::parse(\"https://github.com\")?;\n        download_url.set_path(&download_path);\n        let download_url = crate::utils::candy::parse_gh_url(&mirror, download_url.as_str())?;\n        let file = tokio::fs::File::create(temp_dir.path().join(&artifact)).await?;\n        tracing::debug!(\"downloader url: {}\", download_url);\n        tracing::debug!(\"downloader file: {:?}\", file);\n        let (tx, rx) = tokio::sync::mpsc::channel::<DownloaderState>(1);\n        let callback: Box<dyn Fn(DownloaderState) + Send + Sync> = Box::new(move |state| {\n            let tx = tx.clone();\n            tokio::spawn(async move {\n                if let Err(e) = tx.send(state).await {\n                    tracing::warn!(\"failed to send downloader state: {}\", e);\n                }\n            });\n        });\n        let downloader = Arc::new(\n            DownloaderBuilder::new()\n                .set_client(client)\n                .set_url(download_url)?\n                .set_file(file)\n                .set_event_callback(callback)\n                .build()?,\n        );\n        Ok(Updater {\n            id: rand::random::<u32>() as usize,\n            temp_dir,\n            core_type,\n            inner: parking_lot::RwLock::new(inner),\n            artifact,\n            rx: Mutex::new(rx),\n            downloader,\n        })\n    }\n}\n\nimpl Updater {\n    fn dispatch_state(&self, state: UpdaterState) {\n        tracing::debug!(\"dispatching updater state: {:?}\", state);\n        let mut inner = self.inner.write();\n        inner.state = state;\n    }\n\n    async fn decompress_and_set_permission(&self) -> anyhow::Result<()> {\n        self.dispatch_state(UpdaterState::Decompressing);\n        let path = self.temp_dir.path().join(&self.artifact);\n        tracing::debug!(\"decompressing file: {:?}\", path);\n        let mut tmp_file = std::fs::File::open(path)?;\n        tracing::debug!(\"file size: {}\", tmp_file.metadata()?.len());\n        let artifact = self.artifact.clone();\n        let buff = tokio::task::spawn_blocking(move || {\n            let mut buff = Vec::<u8>::new();\n            match artifact {\n                fname if fname.ends_with(\".gz\") => {\n                    tracing::debug!(\"decompressing gz file\");\n                    let mut decoder = flate2::read::GzDecoder::new(&mut tmp_file);\n                    std::io::copy(&mut decoder, &mut buff)?;\n                }\n                fname if fname.ends_with(\".zip\") => {\n                    tracing::debug!(\"decompressing zip file\");\n                    let mut archive = zip::ZipArchive::new(tmp_file)?;\n                    let len = archive.len();\n                    for i in 0..len {\n                        let mut file = archive.by_index(i)?;\n                        let file_name = file.name();\n                        tracing::debug!(\"Filename: {}\", file.name());\n                        // TODO: 在 enum 做点魔法\n                        if file_name.contains(\"mihomo\") || file_name.contains(\"clash\") {\n                            tracing::debug!(\"extract file: {}\", file_name);\n                            tracing::debug!(\"extract file size: {}\", file.size());\n                            std::io::copy(&mut file, &mut buff)?;\n                            break;\n                        }\n                        if i == len - 1 {\n                            anyhow::bail!(\"failed to find core file in a zip archive\");\n                        }\n                    }\n                }\n                _ => {\n                    tracing::debug!(\"directly copying file\");\n                    std::io::copy(&mut tmp_file, &mut buff)?;\n                }\n            };\n            Ok::<_, anyhow::Error>(buff)\n        })\n        .await??;\n        let tmp_core = self.temp_dir.path().join(format!(\n            \"{}{}\",\n            self.core_type,\n            std::env::consts::EXE_SUFFIX\n        ));\n        tracing::debug!(\"writing core to {:?} ({} bytes)\", tmp_core, buff.len());\n        let mut core_file = tokio::fs::File::create(&tmp_core).await?;\n        tokio::io::copy(&mut buff.as_slice(), &mut core_file).await?;\n        #[cfg(target_family = \"unix\")]\n        {\n            std::fs::set_permissions(&tmp_core, std::fs::Permissions::from_mode(0o755))?;\n        }\n        Ok(())\n    }\n\n    async fn replace_core(&self) -> anyhow::Result<()> {\n        self.dispatch_state(UpdaterState::Replacing);\n        let current_core = crate::config::Config::verge()\n            .latest()\n            .clash_core\n            .unwrap_or_default();\n        tracing::debug!(\"current core: {}\", current_core);\n        if current_core == self.core_type {\n            tracing::debug!(\"stopping core to replace\");\n            CoreManager::global().stop_core().await?;\n        }\n        #[cfg(target_os = \"windows\")]\n        let target_core = format!(\"{}.exe\", self.core_type);\n        #[cfg(not(target_os = \"windows\"))]\n        let target_core = self.core_type.clone().to_string();\n        let core_dir = tauri::utils::platform::current_exe()?;\n        let core_dir = core_dir.parent().ok_or(anyhow!(\"failed to get core dir\"))?;\n        let target_core = core_dir.join(target_core);\n        tracing::debug!(\"copying core to {:?}\", target_core);\n        let tmp_core_path = self.temp_dir.path().join(format!(\n            \"{}{}\",\n            self.core_type,\n            std::env::consts::EXE_SUFFIX\n        ));\n        match tokio::fs::copy(tmp_core_path.clone(), target_core.clone()).await {\n            Ok(size) => {\n                tracing::debug!(\"copied core to {:?} ({} bytes)\", target_core, size);\n            }\n            Err(err) => {\n                tracing::warn!(\n                    \"failed to copy core: {}, trying to use elevated permission to copy and override core\",\n                    err\n                );\n                let mut target_core_str = target_core.to_str().unwrap().to_string();\n                if target_core_str.starts_with(\"\\\\\\\\?\\\\\") {\n                    target_core_str = target_core_str[4..].to_string();\n                }\n                tracing::debug!(\"tmp core path: {:?}\", tmp_core_path);\n                tracing::debug!(\"target core path: {:?}\", target_core_str);\n                // 防止 UAC 弹窗堵塞主线程\n                let status_code = tokio::task::spawn_blocking(move || {\n                    #[cfg(target_os = \"windows\")]\n                    {\n                        RunasCommand::new(\"cmd\")\n                            .args(&[\n                                \"/C\",\n                                \"copy\",\n                                \"/Y\",\n                                tmp_core_path.to_str().unwrap(),\n                                &target_core_str,\n                            ])\n                            .status()\n                    }\n                    #[cfg(not(target_os = \"windows\"))]\n                    {\n                        RunasCommand::new(\"cp\")\n                            .args(&[\"-f\", tmp_core_path.to_str().unwrap(), &target_core_str])\n                            .status()\n                    }\n                })\n                .await??;\n                if !status_code.success() {\n                    anyhow::bail!(\"failed to copy core: {}\", status_code);\n                }\n            }\n        };\n\n        if current_core == self.core_type {\n            self.dispatch_state(UpdaterState::Restarting);\n            CoreManager::global().run_core().await?;\n        }\n\n        Ok(())\n    }\n\n    pub async fn start(&self) {\n        {\n            let mut inner = self.inner.write();\n            if !matches!(inner.state, UpdaterState::Idle) {\n                return;\n            }\n            inner.state = UpdaterState::Downloading;\n        }\n        let downloader = self.downloader.clone();\n        tokio::spawn(async move {\n            if let Err(e) = downloader.start().await {\n                tracing::error!(\"failed to start downloader: {}\", e);\n            }\n        });\n        let mut rx = self.rx.lock().await;\n        loop {\n            match rx.recv().await {\n                Some(state) => match state {\n                    DownloaderState::Downloading => {\n                        tracing::debug!(\"start to download core.\");\n                        self.dispatch_state(UpdaterState::Downloading);\n                    }\n                    DownloaderState::Finished => {\n                        tracing::debug!(\"download finished and start to incoming update logic\");\n                        if let Err(e) = self.decompress_and_set_permission().await {\n                            tracing::error!(\"failed to decompress and set permission: {}\", e);\n                            self.dispatch_state(UpdaterState::Failed(e.to_string()));\n                            return;\n                        }\n                        if let Err(e) = self.replace_core().await {\n                            tracing::error!(\"failed to replace core: {}\", e);\n                            self.dispatch_state(UpdaterState::Failed(e.to_string()));\n                            return;\n                        }\n                        self.dispatch_state(UpdaterState::Done);\n                        break;\n                    }\n                    DownloaderState::Failed(e) => {\n                        tracing::error!(\"download failed: {}\", e);\n                        self.dispatch_state(UpdaterState::Failed(e));\n                        break;\n                    }\n                    _ => {\n                        tracing::debug!(\"downloader enter state: {:?}\", state);\n                    }\n                },\n                None => {\n                    tracing::error!(\"downloader channel closed\");\n                }\n            }\n        }\n    }\n\n    pub fn get_report(&self) -> UpdaterSummary {\n        UpdaterSummary {\n            id: self.id,\n            state: self.inner.read().state.clone(),\n            downloader: self.downloader.get_current_status(),\n        }\n    }\n\n    pub fn get_updater_id(&self) -> usize {\n        self.id\n    }\n}\n\nunsafe impl Send for Updater {}\n"
  },
  {
    "path": "backend/tauri/src/core/updater/mod.rs",
    "content": "use std::{\n    collections::HashMap,\n    sync::{Arc, OnceLock},\n};\n\nuse crate::{\n    config::nyanpasu::ClashCore,\n    utils::candy::{ReqwestSpeedTestExt, parse_gh_url},\n};\nuse anyhow::{Result, anyhow};\nuse dashmap::DashMap;\nuse serde::{Deserialize, Serialize};\nuse shared::{CoreTypeMeta, get_arch};\nuse specta::Type;\nuse tokio::sync::RwLock;\n\nmod instance;\nmod shared;\n\npub use instance::UpdaterSummary;\n\npub struct UpdaterManager {\n    manifest_version: ManifestVersion,\n    client: reqwest::Client,\n    mirror: Arc<parking_lot::RwLock<Option<(String, u64)>>>,\n    instances: Arc<DashMap<usize, Arc<instance::Updater>>>,\n}\n\nimpl Default for UpdaterManager {\n    fn default() -> Self {\n        Self {\n            manifest_version: ManifestVersion::default(),\n            client: crate::utils::candy::get_reqwest_client().unwrap(),\n            mirror: Arc::new(parking_lot::RwLock::new(None)),\n            instances: Arc::new(DashMap::new()),\n        }\n    }\n}\n\n#[derive(Deserialize, Serialize, Clone, Debug)]\npub struct ManifestVersion {\n    manifest_version: u64,\n    latest: ManifestVersionLatest,\n    arch_template: ArchTemplate,\n    updated_at: String,\n}\n\n// TODO: manifest v2 should be kebad-case\n#[derive(Deserialize, Serialize, Clone, Debug, Type)]\npub struct ManifestVersionLatest {\n    mihomo: String,\n    mihomo_alpha: String,\n    clash_rs: String,\n    clash_rs_alpha: String,\n    clash_premium: String,\n}\n\n#[derive(Deserialize, Serialize, Default, Clone, Debug)]\npub struct ArchTemplate {\n    mihomo: HashMap<String, String>,\n    mihomo_alpha: HashMap<String, String>,\n    clash_rs: HashMap<String, String>,\n    clash_rs_alpha: HashMap<String, String>,\n    clash_premium: HashMap<String, String>,\n}\n\nimpl Default for ManifestVersion {\n    fn default() -> Self {\n        Self {\n            manifest_version: 0,\n            latest: ManifestVersionLatest::default(),\n            arch_template: ArchTemplate::default(),\n            updated_at: \"\".to_string(),\n        }\n    }\n}\n\nimpl Default for ManifestVersionLatest {\n    fn default() -> Self {\n        Self {\n            mihomo: \"\".to_string(),\n            mihomo_alpha: \"\".to_string(),\n            clash_rs: \"\".to_string(),\n            clash_rs_alpha: \"\".to_string(),\n            clash_premium: \"\".to_string(),\n        }\n    }\n}\n\nimpl ManifestVersion {\n    pub(self) fn get_matches(&self, core_type: &ClashCore) -> Option<(String, CoreTypeMeta)> {\n        let arch = get_arch().ok()?;\n        match core_type {\n            ClashCore::ClashPremium => Some((\n                self.arch_template\n                    .clash_premium\n                    .get(arch)?\n                    .clone()\n                    .replace(\"{}\", &self.latest.clash_premium),\n                CoreTypeMeta::ClashPremium(self.latest.clash_premium.clone()),\n            )),\n            ClashCore::Mihomo => Some((\n                self.arch_template\n                    .mihomo\n                    .get(arch)?\n                    .clone()\n                    .replace(\"{}\", &self.latest.mihomo),\n                CoreTypeMeta::Mihomo(self.latest.mihomo.clone()),\n            )),\n            ClashCore::MihomoAlpha => Some((\n                self.arch_template\n                    .mihomo_alpha\n                    .get(arch)?\n                    .clone()\n                    .replace(\"{}\", &self.latest.mihomo_alpha),\n                CoreTypeMeta::MihomoAlpha,\n            )),\n            ClashCore::ClashRs => Some((\n                self.arch_template\n                    .clash_rs\n                    .get(arch)?\n                    .clone()\n                    .replace(\"{}\", &self.latest.clash_rs),\n                CoreTypeMeta::ClashRs(self.latest.clash_rs.clone()),\n            )),\n            ClashCore::ClashRsAlpha => Some((\n                self.arch_template\n                    .clash_rs_alpha\n                    .get(arch)?\n                    .clone()\n                    .replace(\"{}\", &self.latest.clash_rs_alpha),\n                CoreTypeMeta::ClashRsAlpha,\n            )),\n        }\n    }\n}\n\nimpl UpdaterManager {\n    pub fn new() -> Self {\n        Self::default()\n    }\n\n    pub fn global() -> &'static RwLock<Self> {\n        static INSTANCE: OnceLock<RwLock<UpdaterManager>> = OnceLock::new();\n        INSTANCE.get_or_init(|| RwLock::new(UpdaterManager::new()))\n    }\n\n    pub fn get_latest_versions(&self) -> ManifestVersionLatest {\n        self.manifest_version.latest.clone()\n    }\n\n    pub fn get_mirror(&self) -> Option<String> {\n        self.mirror.read().clone().map(|(mirror, _)| mirror)\n    }\n\n    async fn get_latest_version_manifest(&self, mirror: &str) -> Result<ManifestVersion> {\n        let url = parse_gh_url(\n            mirror,\n            \"/libnyanpasu/clash-nyanpasu/raw/main/manifest/version.json\",\n        )?;\n        log::debug!(\"{url}\");\n        let res = self.client.get(url).send().await?;\n        let status_code = res.status();\n        if !status_code.is_success() {\n            anyhow::bail!(\n                \"failed to get latest version manifest: response status is {}, expected 200\",\n                status_code\n            );\n        }\n        Ok(res.json::<ManifestVersion>().await?)\n    }\n\n    pub async fn fetch_latest(&mut self) -> Result<()> {\n        self.mirror_speed_test().await?;\n        let mirror = self.get_mirror().unwrap();\n        let latest = self.get_latest_version_manifest(&mirror).await?;\n        log::debug!(\"latest version: {latest:?}\");\n        self.manifest_version = latest;\n        Ok(())\n    }\n\n    // TODO: add user-spec mirror support\n    pub async fn mirror_speed_test(&self) -> Result<()> {\n        {\n            let mirror = self.mirror.read();\n            if let Some((_, timestamp)) = mirror.as_ref()\n                && chrono::Utc::now().timestamp() - (*timestamp as i64) < 3600\n            {\n                return Ok(());\n            }\n        }\n        let mirrors = crate::utils::candy::INTERNAL_MIRRORS;\n        let path = \"https://github.com/libnyanpasu/clash-nyanpasu/raw/main/manifest/version.json\";\n        let client = crate::utils::candy::get_reqwest_client()?;\n        let results = client.mirror_speed_test(mirrors, path).await?;\n        let (fastest_mirror, speed) = results.first().ok_or(anyhow!(\"no mirrors found\"))?;\n        if speed - 1.0 < 0.0001 {\n            anyhow::bail!(\"all mirrors are too slow\");\n        }\n        tracing::debug!(\"fastest mirror: {}, speed: {}\", fastest_mirror, speed);\n        {\n            let mut mirror = self.mirror.write();\n            *mirror = Some((\n                fastest_mirror.to_string(),\n                chrono::Utc::now().timestamp() as u64,\n            ));\n        }\n        Ok(())\n    }\n\n    pub async fn update_core(&mut self, core_type: &ClashCore) -> Result<usize> {\n        self.mirror_speed_test().await?;\n        let (artifact, tag) = self\n            .manifest_version\n            .get_matches(core_type)\n            .ok_or(anyhow!(\"no matches found for core type: {:?}\", core_type))?;\n        let mirror = self.get_mirror().unwrap();\n        let updater = Arc::new(\n            instance::UpdaterBuilder::new()\n                .set_client(self.client.clone())\n                .set_core_type(*core_type)\n                .set_mirror(mirror)\n                .set_artifact(artifact)\n                .set_tag(tag)\n                .build()\n                .await?,\n        );\n        let updater_ref = updater.clone();\n        let updater_id = updater.get_updater_id();\n        self.instances.insert(updater_id, updater);\n        tokio::spawn(async move {\n            updater_ref.start().await;\n        });\n        Ok(updater_id)\n    }\n\n    pub fn inspect_updater(&self, updater_id: usize) -> Option<UpdaterSummary> {\n        let updater = self.instances.get(&updater_id)?;\n        let report = updater.get_report();\n        if matches!(\n            report.state,\n            instance::UpdaterState::Done | instance::UpdaterState::Failed(_)\n        ) {\n            let map = self.instances.clone();\n            tokio::spawn(async move {\n                tokio::time::sleep(std::time::Duration::from_secs(5)).await;\n                map.remove(&updater_id);\n            });\n        }\n        Some(report)\n    }\n}\n"
  },
  {
    "path": "backend/tauri/src/core/updater/shared.rs",
    "content": "pub(super) fn get_arch() -> anyhow::Result<&'static str> {\n    let env = {\n        let arch = std::env::consts::ARCH;\n        let os = std::env::consts::OS;\n        #[cfg(all(target_arch = \"arm\", target_abi = \"eabihf\"))]\n        let arch = \"armhf\";\n        #[cfg(all(target_arch = \"arm\", target_abi = \"\"))]\n        let arch = \"armel\";\n        (arch, os)\n    };\n\n    match env {\n        (\"x86_64\", \"macos\") => Ok(\"darwin-x64\"),\n        (\"x86_64\", \"linux\") => Ok(\"linux-amd64\"),\n        (\"x86_64\", \"windows\") => Ok(\"windows-x86_64\"),\n        (\"i686\", \"windows\") => Ok(\"windows-i386\"),\n        (\"i686\", \"linux\") => Ok(\"linux-i386\"),\n        (\"armhf\", \"linux\") => Ok(\"linux-armv7hf\"),\n        (\"armel\", \"linux\") => Ok(\"linux-armv7\"),\n        (\"aarch64\", \"macos\") => Ok(\"darwin-arm64\"),\n        (\"aarch64\", \"linux\") => Ok(\"linux-aarch64\"),\n        (\"aarch64\", \"windows\") => Ok(\"windows-arm64\"),\n        _ => anyhow::bail!(\"unsupported platform\"),\n    }\n}\n\npub(super) enum CoreTypeMeta {\n    ClashPremium(String),\n    Mihomo(String),\n    MihomoAlpha,\n    ClashRs(String),\n    ClashRsAlpha,\n}\n\npub(super) fn get_download_path(core_type: CoreTypeMeta, artifact: &str) -> String {\n    match core_type {\n        CoreTypeMeta::Mihomo(tag) => {\n            format!(\"MetaCubeX/mihomo/releases/download/{tag}/{artifact}\")\n        }\n        CoreTypeMeta::MihomoAlpha => {\n            format!(\"MetaCubeX/mihomo/releases/download/Prerelease-Alpha/{artifact}\")\n        }\n        CoreTypeMeta::ClashRs(tag) => {\n            format!(\"Watfaq/clash-rs/releases/download/{tag}/{artifact}\")\n        }\n        CoreTypeMeta::ClashRsAlpha => {\n            format!(\"Watfaq/clash-rs/releases/download/latest/{artifact}\")\n        }\n        CoreTypeMeta::ClashPremium(tag) => {\n            format!(\"zhongfly/Clash-premium-backup/releases/download/{tag}/{artifact}\")\n        }\n    }\n}\n"
  },
  {
    "path": "backend/tauri/src/core/win_uwp.rs",
    "content": "#![cfg(target_os = \"windows\")]\n\nuse crate::utils::dirs;\nuse anyhow::{Result, bail};\nuse deelevate::{PrivilegeLevel, Token};\nuse runas::Command as RunasCommand;\nuse std::process::Command as StdCommand;\n\npub async fn invoke_uwptools() -> Result<()> {\n    let resource_dir = dirs::app_resources_dir()?;\n    let tool_path = resource_dir.join(\"enableLoopback.exe\");\n\n    if !tool_path.exists() {\n        bail!(\"enableLoopback exe not found\");\n    }\n\n    let token = Token::with_current_process()?;\n    let level = token.privilege_level()?;\n\n    match level {\n        PrivilegeLevel::NotPrivileged => RunasCommand::new(tool_path).status()?,\n        _ => StdCommand::new(tool_path).status()?,\n    };\n\n    Ok(())\n}\n"
  },
  {
    "path": "backend/tauri/src/enhance/advice.rs",
    "content": "#[allow(unused_imports)]\nuse crate::enhance::{Logs, LogsExt, script::runner::ProcessOutput};\nuse rust_i18n::t;\nuse serde_yaml::Mapping;\n\n// TODO: add more advice for chain\npub fn chain_advice(config: &Mapping) -> ProcessOutput {\n    #[allow(unused_mut)]\n    let mut logs = Logs::default();\n    if config.get(\"tun\").is_some_and(|val| {\n        val.is_mapping()\n            && val\n                .as_mapping()\n                .unwrap()\n                .get(\"enable\")\n                .is_some_and(|val| val.as_bool().unwrap_or(false))\n    }) {\n        let service_state = crate::core::service::ipc::get_ipc_state();\n        // show a warning dialog if the user has no permission to enable tun\n        #[cfg(windows)]\n        {\n            use deelevate::{PrivilegeLevel, Token};\n            let level = {\n                match Token::with_current_process() {\n                    Ok(token) => token\n                        .privilege_level()\n                        .unwrap_or(PrivilegeLevel::NotPrivileged),\n                    Err(_) => PrivilegeLevel::NotPrivileged,\n                }\n            };\n            if level == PrivilegeLevel::NotPrivileged && !service_state.is_connected() {\n                let msg = t!(\"dialog.warning.enable_tun_with_no_permission\");\n                logs.warn(msg.as_ref());\n                crate::utils::dialog::warning_dialog(msg.as_ref());\n            }\n        }\n        // If the core file is not granted the necessary permissions, grant it\n        #[cfg(any(target_os = \"macos\", target_os = \"linux\"))]\n        {\n            if !service_state.is_connected() {\n                let core: nyanpasu_utils::core::CoreType = {\n                    crate::config::Config::verge()\n                        .latest()\n                        .clash_core\n                        .as_ref()\n                        .unwrap_or(&crate::config::nyanpasu::ClashCore::default())\n                        .into()\n                };\n                if crate::utils::dirs::check_core_permission(&core)\n                    .inspect_err(|v| {\n                        log::error!(target: \"app\", \"clash core is not granted the necessary permissions, grant it: {v:?}\");\n                    })\n                    .is_ok_and(|v| !v && *crate::consts::IS_APPIMAGE)\n                {\n                    tracing::warn!(\"The core file is not granted the necessary permissions, grant it\");\n                    let msg = t!(\"dialog.info.grant_core_permission\");\n                    if crate::utils::dialog::ask_dialog(msg.as_ref()) {\n                        if let Err(err) = crate::core::manager::grant_permission(&core) {\n                            tracing::error!(\n                                \"Failed to grant permission to the core file: {}\",\n                                err\n                            );\n                            crate::utils::dialog::error_dialog(format!(\n                                \"failed to grant core permission:\\n{:#?}\",\n                                err\n                            ));\n                        }\n                    }\n                }\n            }\n        }\n    }\n    (Ok(Mapping::new()), logs)\n}\n"
  },
  {
    "path": "backend/tauri/src/enhance/builtin/clash_rs_comp.lua",
    "content": "-- compatible with ipv6 decrepation \nif config[\"ipv6\"] ~= nil then\n    config[\"ipv6\"] = nil\n    if config[\"dns\"] ~= nil and config[\"dns\"][\"enabled\"] == true then\n        config[\"dns\"][\"ipv6\"] = true\n    end\nend\n\n-- compatible with allow lan decrepation\nif config[\"allow_lan\"] == true then\n    config[\"allow_lan\"] = nil\n    config[\"bind_address\"] = \"0.0.0.0\"\nend\n\n-- compatible with proxies strict port type\nif config[\"proxies\"] ~= nil and type(config[\"proxies\"]) == \"table\" then\n    for _, proxy in pairs(config[\"proxies\"]) do\n        if proxy[\"port\"] ~= nil and type(proxy[\"port\"]) == \"string\" then\n            proxy[\"port\"] = tonumber(proxy[\"port\"]) or error(\"invalid port: \" .. proxy[\"port\"])\n        end\n    end\nend\n\n\nreturn config\n"
  },
  {
    "path": "backend/tauri/src/enhance/builtin/config_fixer.js",
    "content": "export default function main(params) {\n  if (typeof params['log-level'] === 'boolean') {\n    params['log-level'] = 'debug'\n  }\n  return params\n}\n"
  },
  {
    "path": "backend/tauri/src/enhance/builtin/meta_guard.js",
    "content": "export default function main(params) {\n  if (params.mode === 'script') {\n    params.mode = 'rule'\n  }\n  return params\n}\n"
  },
  {
    "path": "backend/tauri/src/enhance/builtin/meta_hy_alpn.js",
    "content": "export default function main(params) {\n  if (Array.isArray(params.proxies)) {\n    params.proxies.forEach((p, i) => {\n      if (p.type === 'hysteria' && typeof p.alpn === 'string') {\n        params.proxies[i].alpn = [p.alpn]\n      }\n    })\n  }\n  return params\n}\n"
  },
  {
    "path": "backend/tauri/src/enhance/chain.rs",
    "content": "use crate::{\n    config::{\n        Profile,\n        nyanpasu::ClashCore,\n        profile::{\n            item::prelude::*,\n            item_type::{ProfileItemType, ProfileUid},\n        },\n    },\n    utils::{dirs, help},\n};\nuse enumflags2::{BitFlag, BitFlags};\nuse indexmap::IndexMap;\nuse serde::{Deserialize, Serialize};\nuse serde_yaml::Mapping;\nuse std::fs;\nuse strum::EnumString;\n\nuse super::Logs;\n\n#[derive(Default, Debug, Clone, Serialize, Deserialize, specta::Type)]\n/// 后处理输出\npub struct PostProcessingOutput {\n    /// 局部链的输出\n    pub scopes: IndexMap<ProfileUid, IndexMap<ProfileUid, Logs>>,\n    /// 全局链的输出\n    pub global: IndexMap<ProfileUid, Logs>,\n    /// 根据配置进行的分析建议\n    pub advice: Logs,\n    // TODO: 增加 Meta 信息\n}\n\n#[derive(Debug, Clone)]\npub struct ChainItem {\n    pub uid: String,\n    pub data: ChainTypeWrapper,\n}\n\n#[derive(Debug, Clone)]\npub enum ChainTypeWrapper {\n    Merge(Mapping),\n    Script(ScriptWrapper),\n}\n\nimpl ChainTypeWrapper {\n    pub fn new_js(data: Data) -> Self {\n        Self::Script(ScriptWrapper(ScriptType::JavaScript, data))\n    }\n\n    pub fn new_lua(data: Data) -> Self {\n        Self::Script(ScriptWrapper(ScriptType::Lua, data))\n    }\n\n    pub fn new_merge(data: Mapping) -> Self {\n        Self::Merge(data)\n    }\n}\n\nimpl TryFrom<&Profile> for ChainTypeWrapper {\n    type Error = anyhow::Error;\n\n    fn try_from(item: &Profile) -> Result<Self, Self::Error> {\n        use anyhow::Context;\n        let r#type = item.kind();\n        let file = item.file();\n        let path = dirs::app_profiles_dir()\n            .context(\"profiles dir not found\")?\n            .join(file);\n\n        if !path.exists() {\n            anyhow::bail!(\"file not found: {:?}\", path);\n        }\n\n        match r#type {\n            ProfileItemType::Script(ScriptType::JavaScript) => Ok(ChainTypeWrapper::Script(\n                ScriptWrapper(ScriptType::JavaScript, fs::read_to_string(path)?),\n            )),\n            ProfileItemType::Script(ScriptType::Lua) => Ok(ChainTypeWrapper::Script(\n                ScriptWrapper(ScriptType::Lua, fs::read_to_string(path)?),\n            )),\n            ProfileItemType::Merge => Ok(ChainTypeWrapper::Merge(help::read_merge_mapping(&path)?)),\n            _ => anyhow::bail!(\"unsupported type: {:?}\", r#type),\n        }\n    }\n}\n\nimpl TryFrom<&Profile> for ChainItem {\n    type Error = anyhow::Error;\n\n    fn try_from(item: &Profile) -> Result<Self, Self::Error> {\n        let uid = item.uid().to_string();\n        let data = ChainTypeWrapper::try_from(item)?;\n        Ok(Self { uid, data })\n    }\n}\n\nimpl From<&Profile> for Option<ChainItem> {\n    fn from(item: &Profile) -> Self {\n        let uid = item.uid().to_string();\n        let data = ChainTypeWrapper::try_from(item);\n        match data {\n            Err(_) => None,\n            Ok(data) => Some(ChainItem { uid, data }),\n        }\n    }\n}\n\ntype Data = String;\n#[derive(Debug, Clone)]\npub struct ScriptWrapper(pub ScriptType, pub Data);\n\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub enum ChainType {\n    #[serde(rename = \"merge\")]\n    Merge,\n    #[serde(rename = \"script\")]\n    Script(ScriptType),\n}\n\n#[derive(\n    Debug,\n    EnumString,\n    Clone,\n    Copy,\n    Serialize,\n    Deserialize,\n    Default,\n    Eq,\n    PartialEq,\n    Hash,\n    specta::Type,\n)]\n#[strum(serialize_all = \"snake_case\")]\npub enum ScriptType {\n    #[default]\n    #[serde(rename = \"javascript\")]\n    #[strum(serialize = \"javascript\")]\n    JavaScript,\n    #[serde(rename = \"lua\")]\n    Lua,\n}\n\nimpl ChainItem {\n    /// 内建支持一些脚本\n    pub fn builtin() -> Vec<(BitFlags<ClashCore>, ChainItem)> {\n        // meta 的一些处理\n        let meta_guard = ChainItem::to_script(\n            \"verge_meta_guard\",\n            ChainTypeWrapper::new_js(include_str!(\"./builtin/meta_guard.js\").to_string()),\n        );\n\n        // meta 1.13.2 alpn string 转 数组\n        let hy_alpn = ChainItem::to_script(\n            \"verge_hy_alpn\",\n            ChainTypeWrapper::new_js(include_str!(\"./builtin/meta_hy_alpn.js\").to_string()),\n        );\n\n        // 修复配置的一些问题\n        let config_fixer = ChainItem::to_script(\n            \"config_fixer\",\n            ChainTypeWrapper::new_js(include_str!(\"./builtin/config_fixer.js\").to_string()),\n        );\n\n        // 移除或转换 Clash Rs 不支持的字段\n        let clash_rs_comp = ChainItem::to_script(\n            \"clash_rs_comp\",\n            ChainTypeWrapper::new_lua(include_str!(\"./builtin/clash_rs_comp.lua\").to_string()),\n        );\n\n        vec![\n            (ClashCore::Mihomo | ClashCore::MihomoAlpha, hy_alpn),\n            (ClashCore::Mihomo | ClashCore::MihomoAlpha, meta_guard),\n            (ClashCore::all(), config_fixer),\n            (ClashCore::ClashRs.into(), clash_rs_comp),\n        ]\n    }\n\n    pub fn to_script<U: Into<String>, D: Into<ChainTypeWrapper>>(uid: U, data: D) -> Self {\n        Self {\n            uid: uid.into(),\n            data: data.into(),\n        }\n    }\n}\n"
  },
  {
    "path": "backend/tauri/src/enhance/field.rs",
    "content": "use serde_yaml::{Mapping, Value};\nuse std::collections::HashSet;\n\npub const HANDLE_FIELDS: [&str; 9] = [\n    \"mode\",\n    \"port\",\n    \"socks-port\",\n    \"mixed-port\",\n    \"allow-lan\",\n    \"log-level\",\n    \"ipv6\",\n    \"secret\",\n    \"external-controller\",\n];\n\npub const DEFAULT_FIELDS: [&str; 5] = [\n    \"proxies\",\n    \"proxy-groups\",\n    \"proxy-providers\",\n    \"rules\",\n    \"rule-providers\",\n];\n\npub const OTHERS_FIELDS: [&str; 31] = [\n    \"dns\",\n    \"tun\",\n    \"ebpf\",\n    \"hosts\",\n    \"script\",\n    \"profile\",\n    \"payload\",\n    \"tunnels\",\n    \"auto-redir\",\n    \"experimental\",\n    \"interface-name\",\n    \"routing-mark\",\n    \"redir-port\",\n    \"tproxy-port\",\n    \"iptables\",\n    \"external-ui\",\n    \"bind-address\",\n    \"authentication\",\n    \"tls\",                       // meta\n    \"sniffer\",                   // meta\n    \"geox-url\",                  // meta\n    \"listeners\",                 // meta\n    \"sub-rules\",                 // meta\n    \"geodata-mode\",              // meta\n    \"unified-delay\",             // meta\n    \"tcp-concurrent\",            // meta\n    \"enable-process\",            // meta\n    \"find-process-mode\",         // meta\n    \"skip-auth-prefixes\",        // meta\n    \"external-controller-tls\",   // meta\n    \"global-client-fingerprint\", // meta\n];\n\npub fn use_clash_fields() -> Vec<String> {\n    DEFAULT_FIELDS\n        .into_iter()\n        .chain(HANDLE_FIELDS)\n        .chain(OTHERS_FIELDS)\n        .map(|s| s.to_string())\n        .collect()\n}\n\npub fn use_valid_fields(valid: &[String]) -> Vec<String> {\n    let others = Vec::from(OTHERS_FIELDS);\n\n    valid\n        .iter()\n        .cloned()\n        .map(|s| s.to_ascii_lowercase())\n        .filter(|s| others.contains(&s.as_str()))\n        .chain(DEFAULT_FIELDS.iter().map(|s| s.to_string()))\n        .collect()\n}\n\n/// 使用白名单过滤配置字段\npub fn use_whitelist_fields_filter(config: Mapping, filter: &[String], enable: bool) -> Mapping {\n    if !enable {\n        return config;\n    }\n\n    let mut ret = Mapping::new();\n\n    for (key, value) in config.into_iter() {\n        if let Some(key) = key.as_str()\n            && filter.contains(&key.to_string())\n        {\n            ret.insert(Value::from(key), value);\n        }\n    }\n    ret\n}\n\npub fn use_lowercase(config: Mapping) -> Mapping {\n    let mut ret = Mapping::new();\n    for (key, value) in config.into_iter() {\n        if let Some(key_str) = key.as_str() {\n            let mut key_str = String::from(key_str);\n            key_str.make_ascii_lowercase();\n            // recursive transform the key of the nested mapping\n            let value = if let Value::Mapping(value) = value {\n                Value::Mapping(use_lowercase(value))\n            } else {\n                value // TODO: maybe should handle other types, Tagged, Sequence, etc.\n            };\n            ret.insert(Value::from(key_str), value);\n        }\n    }\n    ret\n}\n\npub fn use_sort(config: Mapping, enable_filter: bool) -> Mapping {\n    let mut ret = Mapping::new();\n\n    HANDLE_FIELDS\n        .into_iter()\n        .chain(OTHERS_FIELDS)\n        .chain(DEFAULT_FIELDS)\n        .for_each(|key| {\n            let key = Value::from(key);\n            if let Some(value) = config.get(&key) {\n                ret.insert(key, value.clone());\n            }\n        });\n\n    if !enable_filter {\n        let supported_keys: HashSet<&str> = HANDLE_FIELDS\n            .into_iter()\n            .chain(OTHERS_FIELDS)\n            .chain(DEFAULT_FIELDS)\n            .collect();\n\n        let config_keys: HashSet<&str> = config.keys().filter_map(|e| e.as_str()).collect();\n\n        config_keys.difference(&supported_keys).for_each(|&key| {\n            let key = Value::from(key);\n            if let Some(value) = config.get(&key) {\n                ret.insert(key, value.clone());\n            }\n        });\n    }\n\n    ret\n}\n\npub fn use_keys(config: &Mapping) -> Vec<String> {\n    config\n        .iter()\n        .filter_map(|(key, _)| key.as_str())\n        .map(|s| {\n            let mut s = s.to_string();\n            s.make_ascii_lowercase();\n            s\n        })\n        .collect()\n}\n"
  },
  {
    "path": "backend/tauri/src/enhance/merge.rs",
    "content": "use super::{Logs, LogsExt, runner::ProcessOutput};\nuse mlua::LuaSerdeExt;\nuse serde::de::DeserializeOwned;\nuse serde_yaml::{Mapping, Value};\nuse tracing_attributes::instrument;\n\n// Override recursive, and if the value is sequence, it should be append to the end.\nfn override_recursive(config: &mut Mapping, key: &Value, data: Value) {\n    if let Some(value) = config.get_mut(key) {\n        if value.is_mapping() {\n            let value = value.as_mapping_mut().unwrap();\n            let data = data.as_mapping().unwrap();\n            for (k, v) in data.iter() {\n                override_recursive(value, k, v.clone());\n            }\n        } else {\n            tracing::trace!(\"override key: {:#?}\", key);\n            *value = data;\n        }\n    } else {\n        tracing::trace!(\"insert key: {:#?}\", key);\n        config.insert(key.clone(), data);\n    }\n}\n\n/// Key should be a.b.c to access the value\nfn find_field<'a>(config: &'a mut Value, key: &'a str) -> Option<&'a mut Value> {\n    let mut keys = key.split('.').peekable();\n    let mut value = config;\n    while let Some(k) = keys.next() {\n        if let Some(v) = match k.parse::<usize>() {\n            Ok(i) => value.get_mut(i),\n            Err(_) => value.get_mut(k),\n        } {\n            if keys.peek().is_none() {\n                return Some(v);\n            }\n            if v.is_mapping() || v.is_sequence() {\n                value = v\n            } else {\n                return None;\n            }\n        } else {\n            return None;\n        }\n    }\n    None\n}\n\nfn merge_sequence(target: &mut Value, to_merge: &Value, append: bool) {\n    if target.is_sequence() && to_merge.is_sequence() {\n        let target = target.as_sequence_mut().unwrap();\n        let to_merge = to_merge.as_sequence().unwrap();\n        if append {\n            target.extend(to_merge.clone());\n        } else {\n            target.splice(0..0, to_merge.iter().cloned());\n        }\n    }\n}\n\nfn run_expr<T: DeserializeOwned>(logs: &mut Logs, item: &Value, expr: &str) -> Option<T> {\n    let lua_runtime = match super::script::create_lua_context() {\n        Ok(lua) => lua,\n        Err(e) => {\n            logs.error(e.to_string());\n            return None;\n        }\n    };\n    let item = match lua_runtime.to_value(item) {\n        Ok(v) => v,\n        Err(e) => {\n            logs.error(format!(\"failed to convert item to lua value: {e:#?}\"));\n            return None;\n        }\n    };\n\n    if let Err(e) = lua_runtime.globals().set(\"item\", item) {\n        logs.error(e.to_string());\n        return None;\n    }\n    let res = lua_runtime.load(expr).eval::<mlua::Value>();\n    match res {\n        Ok(v) => {\n            if let Ok(v) = lua_runtime.from_value(v) {\n                Some(v)\n            } else {\n                logs.error(\"failed to convert lua value to serde value\");\n                None\n            }\n        }\n        Err(e) => {\n            logs.error(format!(\"failed to run expr: {e:#?}\"));\n            None\n        }\n    }\n}\n\nfn do_filter(logs: &mut Logs, config: &mut Value, field_str: &str, filter: &Value) {\n    let field = match find_field(config, field_str) {\n        Some(field) if !field.is_sequence() => {\n            logs.warn(format!(\"field is not sequence: {field_str:#?}\"));\n            return;\n        }\n        Some(field) => field,\n        None => {\n            logs.warn(format!(\"field not found: {field_str:#?}\"));\n            return;\n        }\n    };\n    match filter {\n        Value::Sequence(filters) => {\n            for filter in filters {\n                do_filter(logs, config, field_str, filter);\n            }\n        }\n        Value::String(filter) => {\n            let list = field.as_sequence_mut().unwrap();\n            list.retain(|item| run_expr(logs, item, filter).unwrap_or(false));\n        }\n        Value::Mapping(filter)\n            if filter.get(\"when\").is_some_and(|v| v.is_string())\n                && filter.get(\"expr\").is_some_and(|v| v.is_string()) =>\n        {\n            let when = filter.get(\"when\").unwrap().as_str().unwrap();\n            let expr = filter.get(\"expr\").unwrap().as_str().unwrap();\n            let list = field.as_sequence_mut().unwrap();\n            list.iter_mut().for_each(|item| {\n                let r#match = run_expr(logs, item, when);\n                if r#match.unwrap_or(false) {\n                    let res: Option<Value> = run_expr(logs, item, expr);\n                    if let Some(res) = res {\n                        *item = res;\n                    }\n                }\n            });\n        }\n        Value::Mapping(filter)\n            if filter.get(\"when\").is_some_and(|v| v.is_string())\n                && filter.contains_key(\"override\") =>\n        {\n            let when = filter.get(\"when\").unwrap().as_str().unwrap();\n            let r#override = filter.get(\"override\").unwrap();\n            let list = field.as_sequence_mut().unwrap();\n            list.iter_mut().for_each(|item| {\n                let r#match = run_expr(logs, item, when);\n                if r#match.unwrap_or(false) {\n                    *item = r#override.clone();\n                }\n            });\n        }\n        Value::Mapping(filter)\n            if filter.get(\"when\").is_some_and(|v| v.is_string())\n                && filter.get(\"merge\").is_some_and(|v| v.is_mapping()) =>\n        {\n            let when = filter.get(\"when\").unwrap().as_str().unwrap();\n            let merge = filter.get(\"merge\").unwrap().as_mapping().unwrap();\n            let list = field.as_sequence_mut().unwrap();\n            list.iter_mut().for_each(|item| {\n                let r#match = run_expr(logs, item, when);\n                if r#match.unwrap_or(false) {\n                    for (key, value) in merge.iter() {\n                        let item = item.as_mapping_mut().unwrap();\n                        if item.contains_key(key) {\n                            override_recursive(item, key, value.clone());\n                        } else {\n                            item.insert(key.clone(), value.clone());\n                        }\n                    }\n                }\n            });\n        }\n\n        Value::Mapping(filter)\n            if filter.get(\"when\").is_some_and(|v| v.is_string())\n                && filter.get(\"remove\").is_some_and(|v| v.is_sequence()) =>\n        {\n            let when = filter.get(\"when\").unwrap().as_str().unwrap();\n            let remove = filter.get(\"remove\").unwrap().as_sequence().unwrap();\n            let list = field.as_sequence_mut().unwrap();\n            list.iter_mut().for_each(|item| {\n                let r#match = run_expr(logs, item, when);\n                if r#match.unwrap_or(false) {\n                    remove.iter().for_each(|key| {\n                        if key.is_string() && item.is_mapping() {\n                            let key_str = key.as_str().unwrap();\n                            // 对 key_str 做一下处理，跳过最后一个元素\n                            let mut keys = key_str.split('.').collect::<Vec<_>>();\n                            let last_key = if keys.len() > 1 { keys.pop() } else { None };\n                            let key_str = keys.join(\".\");\n                            match last_key {\n                                None => {\n                                    item.as_mapping_mut().unwrap().remove(key_str);\n                                }\n                                Some(last_key) => {\n                                    let field = find_field(item, &key_str);\n                                    if let Some(field) = field {\n                                        match field {\n                                            Value::Mapping(map) => {\n                                                map.remove(last_key);\n                                            }\n                                            Value::Sequence(list)\n                                                if last_key.parse::<usize>().is_ok() =>\n                                            {\n                                                let index = last_key.parse::<usize>().unwrap();\n                                                if index < list.len() {\n                                                    list.remove(index);\n                                                }\n                                            }\n                                            _ => {\n                                                logs.info(format!(\"invalid key: {last_key:#?}\"));\n                                            }\n                                        }\n                                    }\n                                }\n                            }\n                        } else {\n                            match item {\n                                Value::Sequence(list) if key.is_i64() => {\n                                    let index = key.as_i64().unwrap();\n                                    if index >= 0 && (index as usize) < list.len() {\n                                        list.remove(index as usize);\n                                    }\n                                }\n                                _ => {\n                                    logs.info(format!(\"invalid key: {key:#?}\"));\n                                }\n                            }\n                        }\n                    });\n                }\n            });\n        }\n\n        _ => {\n            logs.warn(format!(\"invalid filter: {filter:#?}\"));\n        }\n    }\n}\n\n#[instrument(skip(merge, config))]\npub fn use_merge(merge: &Mapping, mut config: Mapping) -> ProcessOutput {\n    tracing::trace!(\"original config: {:#?}\", config);\n    tracing::trace!(\"merge: {:#?}\", merge);\n    let mut logs = Logs::new();\n    let mut map = Value::from(config);\n    for (key, value) in merge.iter() {\n        let key_str = key.as_str().unwrap_or_default().to_lowercase();\n        match key_str {\n            key_str if key_str.starts_with(\"prepend__\") || key_str.starts_with(\"prepend-\") => {\n                if !value.is_sequence() {\n                    logs.warn(format!(\"prepend value is not sequence: {key_str:#?}\"));\n                    continue;\n                }\n                let key_str = key_str.replace(\"prepend__\", \"\").replace(\"prepend-\", \"\");\n                let field = find_field(&mut map, &key_str);\n                match field {\n                    Some(field) => {\n                        if field.is_sequence() {\n                            merge_sequence(field, value, false);\n                        } else {\n                            logs.warn(format!(\"field is not sequence: {key_str:#?}\"));\n                        }\n                    }\n                    None => {\n                        logs.warn(format!(\"field not found: {key_str:#?}\"));\n                    }\n                }\n                continue;\n            }\n            key_str if key_str.starts_with(\"append__\") || key_str.starts_with(\"append-\") => {\n                if !value.is_sequence() {\n                    logs.warn(format!(\"append value is not sequence: {key_str:#?}\"));\n                    continue;\n                }\n                let key_str = key_str.replace(\"append__\", \"\").replace(\"append-\", \"\");\n                let field = find_field(&mut map, &key_str);\n                match field {\n                    Some(field) => {\n                        if field.is_sequence() {\n                            merge_sequence(field, value, true);\n                        } else {\n                            logs.warn(format!(\"field is not sequence: {key_str:#?}\"));\n                        }\n                    }\n                    None => {\n                        logs.warn(format!(\"field not found: {key_str:#?}\"));\n                    }\n                }\n                continue;\n            }\n            key_str if key_str.starts_with(\"override__\") => {\n                let key_str = key_str.replace(\"override__\", \"\");\n                let field = find_field(&mut map, &key_str);\n                match field {\n                    Some(field) => {\n                        *field = value.clone();\n                    }\n                    None => {\n                        logs.warn(format!(\"field not found: {key_str:#?}\"));\n                    }\n                }\n                continue;\n            }\n            key_str if key_str.starts_with(\"filter__\") => {\n                let key_str = key_str.replace(\"filter__\", \"\");\n                do_filter(&mut logs, &mut map, &key_str, value);\n                continue;\n            }\n            _ => {\n                override_recursive(map.as_mapping_mut().unwrap(), key, value.clone());\n            }\n        }\n    }\n    config = map.as_mapping().unwrap().clone();\n    tracing::trace!(\"merged config: {:#?}\", config);\n    (Ok(config), logs)\n}\n\nmod tests {\n    #[allow(unused_imports)]\n    use pretty_assertions::{assert_eq, assert_ne};\n\n    #[test]\n    fn test_find_field() {\n        let config = r\"\n        a:\n          b:\n            c:\n            - 111\n            - 222\n        \";\n        let mut config = serde_yaml::from_str::<super::Value>(config).unwrap();\n        eprintln!(\"{config:#?}\");\n        let field = super::find_field(&mut config, \"a.b.c\");\n        assert!(field.is_some(), \"a.b.c should be found\");\n        let field = super::find_field(&mut config, \"a.b\");\n        assert!(field.is_some(), \"a.b should be found\");\n        let field = super::find_field(&mut config, \"a.b.c.0\");\n        assert!(field.is_some(), \"a.b.c.0 should be found\");\n        let field = super::find_field(&mut config, \"a.b.c.1\");\n        assert!(field.is_some(), \"a.b.c.1 should be found\");\n        let field = super::find_field(&mut config, \"a.b.c.2\");\n        assert!(field.is_none(), \"a.b.c.2 should not be found\");\n    }\n\n    #[test]\n    fn test_merge_append() {\n        let merge = r\"\n        append-proxies:\n          - 666\n        append__proxies:\n          - 555\n        append__a.b.c:\n          - 12321\n          - 44444\n        append__nothing:\n          - nothing\n        \";\n        let config = r\"\n        proxies:\n          - 123\n        a:\n          b:\n            c:\n            - 111\n            - 222\n        \";\n        let expected = r\"\n        proxies:\n          - 123\n          - 666\n          - 555\n        a:\n          b:\n            c:\n            - 111\n            - 222\n            - 12321\n            - 44444\n        \";\n        let merge = serde_yaml::from_str::<super::Mapping>(merge).unwrap();\n        let config = serde_yaml::from_str::<super::Mapping>(config).unwrap();\n        let (result, logs) = super::use_merge(&merge, config);\n        eprintln!(\"{logs:#?}\\n\\n{result:#?}\");\n        let expected = serde_yaml::from_str::<super::Mapping>(expected).unwrap();\n        assert_eq!(logs.len(), 1); // field not found: nothing\n        assert_eq!(result.unwrap(), expected);\n    }\n\n    #[test]\n    fn test_prepend() {\n        let merge = r\"\n        prepend-proxies:\n          - 666\n        prepend__proxies:\n          - 555\n        prepend__a.b.c:\n          - 12321\n          - 44444\n        prepend__nothing:\n          - nothing\n        \";\n        let config = r\"\n        proxies:\n          - 123\n        a:\n          b:\n            c:\n            - 111\n            - 222\n        \";\n        let expected = r\"\n        proxies:\n          - 555\n          - 666\n          - 123\n        a:\n          b:\n            c:\n            - 12321\n            - 44444\n            - 111\n            - 222\n        \";\n        let merge = serde_yaml::from_str::<super::Mapping>(merge).unwrap();\n        let config = serde_yaml::from_str::<super::Mapping>(config).unwrap();\n        let (result, logs) = super::use_merge(&merge, config);\n        eprintln!(\"{logs:#?}\\n\\n{result:#?}\");\n        let expected = serde_yaml::from_str::<super::Mapping>(expected).unwrap();\n        assert_eq!(logs.len(), 1); // field not found: nothing\n        assert_eq!(result.unwrap(), expected);\n    }\n\n    #[test]\n    fn test_override() {\n        let merge = r\"\n        override__proxies:\n          - 555\n        override__a.b.c:\n          - 12321\n          - 44444\n        override__nothing:\n          - nothing\n        override__a.f.0: wow\n        \";\n        let config = r\"\n        proxies:\n          - 123\n        a:\n          b:\n            c:\n            - 111\n            - 222\n          f:\n            - 444\n        \";\n        let expected = r\"\n        proxies:\n          - 555\n        a:\n          b:\n            c:\n            - 12321\n            - 44444\n          f:\n            - wow   \n        \";\n        let merge = serde_yaml::from_str::<super::Mapping>(merge).unwrap();\n        let config = serde_yaml::from_str::<super::Mapping>(config).unwrap();\n        let (result, logs) = super::use_merge(&merge, config);\n        eprintln!(\"{logs:#?}\\n\\n{result:#?}\");\n        let expected = serde_yaml::from_str::<super::Mapping>(expected).unwrap();\n        assert_eq!(logs.len(), 1); // field not found: nothing\n        assert_eq!(result.unwrap(), expected);\n    }\n\n    #[test]\n    fn test_filter_string() {\n        let merge = r\"\n        filter__proxies: |\n          type(item) == 'table' and (item.type == 'ss' or item.type == 'hysteria2')\n        filter__wow: |\n          item == 'wow'\n        \";\n        let config = r#\"\n        wow: 123\n        proxies:\n          - 123\n          - 555\n          - name: \"hysteria2\"\n            type: hysteria2\n            server: server.com\n            port: 443\n            ports: 443-8443\n            password: yourpassword\n            up: \"30 Mbps\"\n            down: \"200 Mbps\"\n            obfs: salamander # 默认为空，如果填写则开启obfs，目前仅支持salamander\n            obfs-password: yourpassword\n\n            sni: server.com\n            skip-cert-verify: false\n            fingerprint: xxxx\n            alpn:\n              - h3\n            ca: \"./my.ca\"\n            ca-str: \"xyz\"\n          - name: \"hysteria2\"\n            type: ss\n            server: server.com\n            port: 443\n            ports: 443-8443\n            password: yourpassword\n            up: \"30 Mbps\"\n            down: \"200 Mbps\"\n            obfs: salamander # 默认为空，如果填写则开启obfs，目前仅支持salamander\n            obfs-password: yourpassword\n\n            sni: server.com\n            skip-cert-verify: false\n            fingerprint: xxxx\n            alpn:\n              - h3\n            ca: \"./my.ca\"\n            ca-str: \"xyz\"            \n        \"#;\n        let expected = r#\"\n        wow: 123\n        proxies:\n            -   name: hysteria2\n                type: hysteria2\n                server: server.com\n                port: 443\n                ports: 443-8443\n                password: yourpassword\n                up: \"30 Mbps\"\n                down: \"200 Mbps\"\n                obfs: salamander\n                obfs-password: yourpassword\n                sni: server.com\n                skip-cert-verify: false\n                fingerprint: xxxx\n                alpn:\n                - h3\n                ca: \"./my.ca\"\n                ca-str: \"xyz\"\n            -   name: hysteria2\n                type: ss\n                server: server.com\n                port: 443\n                ports: 443-8443\n                password: yourpassword\n                up: \"30 Mbps\"\n                down: \"200 Mbps\"\n                obfs: salamander\n                obfs-password: yourpassword\n                sni: server.com\n                skip-cert-verify: false\n                fingerprint: xxxx\n                alpn:\n                - h3\n                ca: \"./my.ca\"\n                ca-str: \"xyz\"\n        \"#;\n        let merge = serde_yaml::from_str::<super::Mapping>(merge).unwrap();\n        let config = serde_yaml::from_str::<super::Mapping>(config).unwrap();\n        let (result, logs) = super::use_merge(&merge, config);\n        eprintln!(\"{logs:#?}\\n\\n{result:#?}\");\n        assert!(logs.len() == 1, \"filter_wow should not work\");\n        let expected = serde_yaml::from_str::<super::Mapping>(expected).unwrap();\n        assert_eq!(result.unwrap(), expected);\n    }\n\n    #[test]\n    fn test_filter_when_and_expr() {\n        let merge = r\"\n        filter__proxies:\n          - when: |\n              type(item) == 'table' and (item.type == 'ss' or item.type == 'hysteria2')\n            expr: |\n              item\n        filter__proxy-groups:\n          - when: |\n              item.name == 'Spotify'\n            expr: |\n              item.icon = 'https://raw.githubusercontent.com/Koolson/Qure/master/IconSet/Color/Spotify.png'\n              return item\n        \";\n        let config = r#\"proxy-groups:\n- name: Spotify\n  type: select\n  proxies:\n  - Proxies\n  - DIRECT\n  - HK\n  - JP\n  - SG\n  - TW\n  - US\n- name: Steam\n  type: select\n  proxies:\n  - Proxies\n  - DIRECT\n  - HK\n  - JP\n  - SG\n  - TW\n  - US\n- name: Telegram\n  type: select\n  proxies:\n  - Proxies\n  - HK\n  - JP\n  - SG\n  - TW\n  - US\"#;\n        let expected = r#\"proxy-groups:\n- name: Spotify\n  icon: https://raw.githubusercontent.com/Koolson/Qure/master/IconSet/Color/Spotify.png\n  type: select\n  proxies:\n  - Proxies\n  - DIRECT\n  - HK\n  - JP\n  - SG\n  - TW\n  - US\n- name: Steam\n  type: select\n  proxies:\n  - Proxies\n  - DIRECT\n  - HK\n  - JP\n  - SG\n  - TW\n  - US\n- name: Telegram\n  type: select\n  proxies:\n  - Proxies\n  - HK\n  - JP\n  - SG\n  - TW\n  - US\"#;\n        let merge = serde_yaml::from_str::<super::Mapping>(merge).unwrap();\n        let config = serde_yaml::from_str::<super::Mapping>(config).unwrap();\n        let (result, logs) = super::use_merge(&merge, config);\n        eprintln!(\"{logs:#?}\\n\\n{result:#?}\");\n        assert_eq!(logs.len(), 1);\n        let expected = serde_yaml::from_str::<super::Mapping>(expected).unwrap();\n        assert_eq!(result.unwrap(), expected);\n    }\n\n    #[test]\n    fn test_filter_when_and_override() {\n        let merge = r\"\n        filter__proxies:\n          - when: |\n              type(item) == 'table' and (item.type == 'ss' or item.type == 'hysteria2')\n            override: OVERRIDDEN\n        \";\n        let config = r#\"\n        proxies:\n          - 123\n          - 555\n          - name: \"hysteria2\"\n            type: hysteria2\n            server: server.com\n            port: 443\n            ports: 443-8443\n            password: yourpassword\n            up: \"30 Mbps\"\n            down: \"200 Mbps\"\n            obfs: salamander # 默认为空，如果填写则开启obfs，目前仅支持salamander\n            obfs-password: yourpassword\n\n            sni: server.com\n            skip-cert-verify: false\n            fingerprint: xxxx\n            alpn:\n              - h3\n            ca: \"./my.ca\"\n            ca-str: \"xyz\"\n          - name: \"hysteria2\"\n            type: ss\n            server: server.com\n            port: 443\n            ports: 443-8443\n            password: yourpassword\n            up: \"30 Mbps\"\n            down: \"200 Mbps\"\n            obfs: salamander # 默认为空，如果填写则开启obfs，目前仅支持salamander\n            obfs-password: yourpassword\n\n            sni: server.com\n            skip-cert-verify: false\n            fingerprint: xxxx\n            alpn:\n              - h3\n            ca: \"./my.ca\"\n            ca-str: \"xyz\"            \n        \"#;\n        let expected = r#\"\n        proxies:\n          - 123\n          - 555\n          - OVERRIDDEN\n          - OVERRIDDEN\n        \"#;\n        let merge = serde_yaml::from_str::<super::Mapping>(merge).unwrap();\n        let config = serde_yaml::from_str::<super::Mapping>(config).unwrap();\n        let (result, logs) = super::use_merge(&merge, config);\n        eprintln!(\"{logs:#?}\\n\\n{result:#?}\");\n        assert_eq!(logs.len(), 0);\n        let expected = serde_yaml::from_str::<super::Mapping>(expected).unwrap();\n        assert_eq!(result.unwrap(), expected);\n    }\n\n    #[test]\n    fn test_filter_when_and_merge() {\n        let merge = r\"\n        filter__proxy-groups:\n          when: |\n            item.name == 'Spotify'\n          merge:\n            icon: 'https://raw.githubusercontent.com/Koolson/Qure/master/IconSet/Color/Spotify.png'\n        filter__wow:\n          when: |\n            item == 'wow'\n          merge:\n            item: 'wow'\";\n        let config = r#\"proxy-groups:\n- name: Spotify\n  type: select\n  proxies:\n  - Proxies\n  - DIRECT\n  - HK\n  - JP\n  - SG\n  - TW\n  - US\n- name: Steam\n  type: select\n  proxies:\n  - Proxies\n  - DIRECT\n  - HK\n  - JP\n  - SG\n  - TW\n  - US\n- name: Telegram\n  type: select\n  proxies:\n  - Proxies\n  - HK\n  - JP\n  - SG\n  - TW\n  - US\"#;\n        let expected = r#\"proxy-groups:\n- name: Spotify\n  type: select\n  icon: https://raw.githubusercontent.com/Koolson/Qure/master/IconSet/Color/Spotify.png\n  proxies:\n  - Proxies\n  - DIRECT\n  - HK\n  - JP\n  - SG\n  - TW\n  - US\n- name: Steam\n  type: select\n  proxies:\n  - Proxies\n  - DIRECT\n  - HK\n  - JP\n  - SG\n  - TW\n  - US\n- name: Telegram\n  type: select\n  proxies:\n  - Proxies\n  - HK\n  - JP\n  - SG\n  - TW\n  - US\"#;\n        let merge = serde_yaml::from_str::<super::Mapping>(merge).unwrap();\n        let config = serde_yaml::from_str::<super::Mapping>(config).unwrap();\n        let (result, logs) = super::use_merge(&merge, config);\n        eprintln!(\"{logs:#?}\\n\\n{result:#?}\");\n        assert_eq!(logs.len(), 1);\n        let expected = serde_yaml::from_str::<super::Mapping>(expected).unwrap();\n        assert_eq!(result.unwrap(), expected);\n    }\n\n    #[test]\n    fn test_filter_when_and_remove() {\n        let merge = r\"\n        filter__proxies:\n          when: |\n            type(item) == 'table' and (item.type == 'ss' or item.type == 'hysteria2')\n          remove:\n            - name\n            - type\n        filter__list: # note that Lua table index starts from 1\n          when: |\n            item[1] == 123\n          remove:\n            - 0\n        filter__wow:\n          when: |\n            item.flag == true\n          remove:\n            - test.1\n            - good.should_remove\n        \";\n        let config = r#\"\n        wow:\n          - test:\n               - 123\n               - 456\n            flag: true\n          - good:\n              should_remove: true\n              should_not_remove: true\n            flag: true\n        list:\n          - - 123\n            - 456\n            - 222\n          - - 123\n            - 456\n            - 222\n        proxies:\n          - 123\n          - 555\n          - name: \"hysteria2\"\n            type: hysteria2\n            server: server.com\n            port: 443\n            ports: 443-8443\n            password: yourpassword\n            up: \"30 Mbps\"\n            down: \"200 Mbps\"\n            obfs: salamander # 默认为空，如果填写则开启obfs，目前仅支持salamander\n            obfs-password: yourpassword\n\n            sni: server.com\n            skip-cert-verify: false\n            fingerprint: xxxx\n            alpn:\n              - h3\n            ca: \"./my.ca\"\n            ca-str: \"xyz\"\n          - name: \"hysteria2\"\n            type: ss\n            server: server.com\n            port: 443\n            ports: 443-8443\n            password: yourpassword\n            up: \"30 Mbps\"\n            down: \"200 Mbps\"\n            obfs: salamander # 默认为空，如果填写则开启obfs，目前仅支持salamander\n            obfs-password: yourpassword\n\n            sni: server.com\n            skip-cert-verify: false\n            fingerprint: xxxx\n            alpn:\n              - h3\n            ca: \"./my.ca\"\n            ca-str: \"xyz\"            \n        \"#;\n        let expected = r#\"\n        wow:\n          - test:\n               - 123\n            flag: true\n          - good:\n              should_not_remove: true\n            flag: true\n        list:\n          - - 456\n            - 222\n          - - 456\n            - 222\n        proxies:\n          - 123\n          - 555\n          - server: server.com\n            port: 443\n            ports: 443-8443\n            password: yourpassword\n            up: \"30 Mbps\"\n            down: \"200 Mbps\"\n            obfs: salamander\n            obfs-password: yourpassword\n            sni: server.com\n            skip-cert-verify: false\n            fingerprint: xxxx\n            alpn:\n            - h3\n            ca: \"./my.ca\"\n            ca-str: \"xyz\"\n          - server: server.com\n            port: 443\n            ports: 443-8443\n            password: yourpassword\n            up: \"30 Mbps\"\n            down: \"200 Mbps\"\n            obfs: salamander\n            obfs-password: yourpassword\n            sni: server.com\n            skip-cert-verify: false\n            fingerprint: xxxx\n            alpn:\n            - h3\n            ca: \"./my.ca\"\n            ca-str: \"xyz\"\n        \"#;\n        let merge = serde_yaml::from_str::<super::Mapping>(merge).unwrap();\n        let config = serde_yaml::from_str::<super::Mapping>(config).unwrap();\n        let (result, logs) = super::use_merge(&merge, config);\n        eprintln!(\"{logs:#?}\\n\\n{result:#?}\");\n        assert_eq!(logs.len(), 0);\n        let expected = serde_yaml::from_str::<super::Mapping>(expected).unwrap();\n        assert_eq!(result.unwrap(), expected);\n    }\n\n    #[test]\n    fn test_filter_sequence() {\n        let merge = r\"\n        filter__proxy-groups:\n          - when: |\n              item.name == 'Spotify'\n            merge:\n              icon: 'https://raw.githubusercontent.com/Koolson/Qure/master/IconSet/Color/Spotify.png'\n          - when: |\n              item.name == 'Steam'\n            merge:\n              icon: 'https://raw.githubusercontent.com/Koolson/Qure/master/IconSet/Color/Steam.png'\n          - when: |\n              item.name == 'Telegram'\n            merge:\n              icon: 'https://raw.githubusercontent.com/Koolson/Qure/master/IconSet/Color/Telegram.png'  \n              \";\n        let config = r#\"proxy-groups:\n- name: Spotify\n  type: select\n  proxies:\n  - Proxies\n  - DIRECT\n  - HK\n  - JP\n  - SG\n  - TW\n  - US\n- name: Steam\n  type: select\n  proxies:\n  - Proxies\n  - DIRECT\n  - HK\n  - JP\n  - SG\n  - TW\n  - US\n- name: Telegram\n  type: select\n  proxies:\n  - Proxies\n  - HK\n  - JP\n  - SG\n  - TW\n  - US\"#;\n        let expected = r#\"proxy-groups:\n- name: Spotify\n  type: select\n  icon: https://raw.githubusercontent.com/Koolson/Qure/master/IconSet/Color/Spotify.png\n  proxies:\n  - Proxies\n  - DIRECT\n  - HK\n  - JP\n  - SG\n  - TW\n  - US\n- name: Steam\n  type: select\n  icon: https://raw.githubusercontent.com/Koolson/Qure/master/IconSet/Color/Steam.png\n  proxies:\n  - Proxies\n  - DIRECT\n  - HK\n  - JP\n  - SG\n  - TW\n  - US\n- name: Telegram\n  type: select\n  icon: https://raw.githubusercontent.com/Koolson/Qure/master/IconSet/Color/Telegram.png\n  proxies:\n  - Proxies\n  - HK\n  - JP\n  - SG\n  - TW\n  - US\"#;\n        let merge = serde_yaml::from_str::<super::Mapping>(merge).unwrap();\n        let config = serde_yaml::from_str::<super::Mapping>(config).unwrap();\n        let (result, logs) = super::use_merge(&merge, config);\n        eprintln!(\"{logs:#?}\\n\\n{result:#?}\");\n        assert_eq!(logs.len(), 0);\n        let expected = serde_yaml::from_str::<super::Mapping>(expected).unwrap();\n        assert_eq!(result.unwrap(), expected);\n    }\n\n    #[test]\n    fn test_override_recursive() {\n        let merge = r\"\n        a:\n          b:\n            c:\n              d: 22323\n          f:\n          - wow\n          e: ttt\n        \";\n        let config = r#\"\n        a:\n          b:\n            c:\n              d: 123\n          f:\n          - 123\n          - 456 \n          t: will preserve\n        \"#;\n\n        let merge = serde_yaml::from_str::<super::Mapping>(merge).unwrap();\n        let config = serde_yaml::from_str::<super::Mapping>(config).unwrap();\n\n        let (result, logs) = super::use_merge(&merge, config);\n        eprintln!(\"{logs:#?}\\n\\n{result:#?}\");\n        assert_eq!(logs.len(), 0);\n        let expected = r#\"\n        a:\n          b:\n            c:\n              d: 22323\n          f:\n          - wow\n          t: will preserve\n          e: ttt\n        \"#;\n        let expected = serde_yaml::from_str::<super::Mapping>(expected).unwrap();\n        assert_eq!(result.unwrap(), expected);\n    }\n}\n"
  },
  {
    "path": "backend/tauri/src/enhance/mod.rs",
    "content": "mod advice;\nmod chain;\nmod field;\nmod merge;\nmod script;\nmod tun;\nmod utils;\n\npub use self::chain::ScriptType;\nuse self::{chain::*, field::*, merge::*, script::*, tun::*};\nuse crate::config::{Config, ProfileMetaGetter, nyanpasu::ClashCore};\npub use chain::PostProcessingOutput;\nuse futures::future::join_all;\nuse indexmap::IndexMap;\nuse serde_yaml::{Mapping, Value};\nuse std::collections::HashSet;\npub use utils::{Logs, LogsExt};\nuse utils::{merge_profiles, process_chain};\n\n/// Enhance mode\n/// 返回最终配置、该配置包含的键、和script执行的结果\npub async fn enhance() -> (Mapping, Vec<String>, PostProcessingOutput) {\n    // config.yaml 的配置\n    let clash_config = { Config::clash().latest().0.clone() };\n\n    let (clash_core, enable_tun, enable_builtin, enable_filter) = {\n        let verge = Config::verge();\n        let verge = verge.latest();\n        (\n            verge.clash_core,\n            verge.enable_tun_mode.unwrap_or(false),\n            verge.enable_builtin_enhanced.unwrap_or(true),\n            verge.enable_clash_fields.unwrap_or(true),\n        )\n    };\n\n    // 从profiles里拿东西\n    let (profiles, profile_chain, global_chain, valid) = {\n        let profiles = Config::profiles();\n        let profiles = profiles.latest();\n\n        let profile_chain_mapping = profiles\n            .get_current()\n            .iter()\n            .filter_map(|uid| profiles.get_item(uid).ok())\n            .map(|item| {\n                (\n                    item.uid().to_string(),\n                    match item {\n                        profile if profile.is_local() => {\n                            let profile = profile.as_local().unwrap();\n                            utils::convert_uids_to_scripts(&profiles, &profile.chain)\n                        }\n                        profile if profile.is_remote() => {\n                            let profile = profile.as_remote().unwrap();\n                            utils::convert_uids_to_scripts(&profiles, &profile.chain)\n                        }\n                        _ => vec![],\n                    },\n                )\n            })\n            .collect::<IndexMap<_, _>>();\n\n        let current_mappings = profiles\n            .current_mappings()\n            .unwrap_or_default()\n            .into_iter()\n            .map(|(k, v)| (k.to_string(), v))\n            .collect::<IndexMap<_, _>>();\n\n        let global_chain = utils::convert_uids_to_scripts(&profiles, &profiles.chain);\n\n        let valid = profiles.valid.clone();\n\n        (current_mappings, profile_chain_mapping, global_chain, valid)\n    };\n\n    let mut postprocessing_output = PostProcessingOutput::default();\n\n    let valid = use_valid_fields(&valid);\n\n    // 执行 scoped chain\n    let profiles_outputs = join_all(profiles.into_iter().map(|(uid, mapping)| async {\n        let chain = profile_chain.get(&uid).map_or(&[] as &[_], |v| v);\n        let output = process_chain(mapping, chain).await;\n        (uid, output)\n    }))\n    .await;\n\n    let mut profiles = IndexMap::new();\n    for (uid, (config, output)) in profiles_outputs {\n        postprocessing_output.scopes.insert(uid.to_string(), output);\n        profiles.insert(uid.to_string(), config);\n    }\n\n    // 合并多个配置\n    // TODO: 此步骤需要提供针对每个配置的 Meta 信息\n    // TODO: 需要支持自定义合并逻辑\n    let config = merge_profiles(profiles);\n\n    // 执行全局 chain\n    let (mut config, global_chain_output) = process_chain(config, &global_chain).await;\n    postprocessing_output.global = global_chain_output;\n\n    // 记录当前配置包含的键\n    let mut exists_keys = use_keys(&config);\n    config = use_whitelist_fields_filter(config, &valid, enable_filter);\n\n    // 合并默认的config\n    clash_config\n        .iter()\n        // only guarded fields should be overwritten\n        .filter(|(k, _)| HANDLE_FIELDS.contains(&k.as_str().unwrap_or_default()))\n        .for_each(|(key, value)| {\n            config.insert(key.to_owned(), value.clone());\n        });\n\n    let clash_fields = use_clash_fields();\n\n    // 内建脚本最后跑\n    if enable_builtin {\n        let mut script_runner = RunnerManager::new();\n        for item in ChainItem::builtin()\n            .into_iter()\n            .filter(|(s, _)| s.contains(*clash_core.as_ref().unwrap_or(&ClashCore::default())))\n            .map(|(_, c)| c)\n        {\n            log::debug!(target: \"app\", \"run builtin script {}\", item.uid);\n\n            if let ChainTypeWrapper::Script(script) = item.data {\n                let (res, _) = script_runner\n                    .process_script(&script, config.to_owned())\n                    .await;\n                match res {\n                    Ok(res_config) => {\n                        config = res_config;\n                    }\n                    Err(err) => {\n                        log::error!(target: \"app\", \"builtin script error `{err:?}`\");\n                    }\n                }\n            }\n        }\n    }\n\n    config = use_whitelist_fields_filter(config, &clash_fields, enable_filter);\n    config = use_tun(config, enable_tun);\n    config = use_include_all_proxy_groups(config);\n    config = use_cache(config);\n    config = use_sort(config, enable_filter);\n\n    let (_, logs) = advice::chain_advice(&config);\n    postprocessing_output.advice = logs;\n\n    let mut exists_set = HashSet::new();\n    exists_set.extend(exists_keys.into_iter().filter(|s| clash_fields.contains(s)));\n    exists_keys = exists_set.into_iter().collect();\n\n    (config, exists_keys, postprocessing_output)\n}\n\n/// Process proxy groups with include-all field\nfn use_include_all_proxy_groups(mut config: Mapping) -> Mapping {\n    // Collect all proxy names from proxies and proxy-providers first (before mutable borrow)\n    let mut all_proxy_names = Vec::new();\n\n    // Collect from proxies section\n    if let Some(proxies_value) = config.get(\"proxies\") {\n        if let Some(proxies_seq) = proxies_value.as_sequence() {\n            for proxy in proxies_seq {\n                if let Some(proxy_map) = proxy.as_mapping() {\n                    if let Some(name_value) = proxy_map.get(\"name\") {\n                        if let Some(name) = name_value.as_str() {\n                            all_proxy_names.push(name.to_string());\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    // Collect from proxy-providers section\n    if let Some(providers_value) = config.get(\"proxy-providers\") {\n        if let Some(providers_map) = providers_value.as_mapping() {\n            for (provider_name, _) in providers_map {\n                if let Some(name) = provider_name.as_str() {\n                    all_proxy_names.push(name.to_string());\n                }\n            }\n        }\n    }\n\n    // Check if we have proxy-groups field\n    if let Some(proxy_groups_value) = config.get_mut(\"proxy-groups\") {\n        if let Some(proxy_groups) = proxy_groups_value.as_sequence_mut() {\n            // Process each proxy group\n            for group in proxy_groups.iter_mut() {\n                if let Some(group_map) = group.as_mapping_mut() {\n                    // Check if this group has include-all: true\n                    if let Some(include_all_value) = group_map.get(\"include-all\") {\n                        if include_all_value.as_bool().unwrap_or(false) {\n                            // Check if this is the GLOBAL group or any group with include-all\n                            if let Some(name_value) = group_map.get(\"name\") {\n                                let _name = name_value.as_str().unwrap_or(\"\");\n                                // Preserve existing proxies\n                                let mut existing_proxies = Vec::new();\n                                if let Some(existing) = group_map.get(\"proxies\") {\n                                    if let Some(existing_seq) = existing.as_sequence() {\n                                        for proxy in existing_seq {\n                                            if let Some(proxy_str) = proxy.as_str() {\n                                                existing_proxies.push(proxy_str.to_string());\n                                            }\n                                        }\n                                    }\n                                }\n\n                                // Create new proxies list with all proxies\n                                let mut new_proxies = Vec::new();\n\n                                // Add all collected proxy names\n                                for proxy_name in &all_proxy_names {\n                                    new_proxies.push(Value::String(proxy_name.clone()));\n                                }\n\n                                // Add existing proxies that aren't in the all list\n                                for existing_proxy in existing_proxies {\n                                    if !all_proxy_names.contains(&existing_proxy) {\n                                        new_proxies.push(Value::String(existing_proxy));\n                                    }\n                                }\n\n                                // Update the proxies field\n                                group_map.insert(\n                                    Value::String(\"proxies\".to_string()),\n                                    Value::Sequence(new_proxies),\n                                );\n\n                                // Remove the include-all field since it's been processed\n                                group_map.remove(\"include-all\");\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n    config\n}\n\nfn use_cache(mut config: Mapping) -> Mapping {\n    if !config.contains_key(\"profile\") {\n        tracing::debug!(\"Don't detect profile, set default profile for memorized profile\");\n        let mut profile = Mapping::new();\n        profile.insert(\"store-selected\".into(), true.into());\n        // Disable fake-ip store, due to the slow speed.\n        // each dns query should indirect to the file io, which is very very slow.\n        profile.insert(\"store-fake-ip\".into(), false.into());\n        config.insert(\"profile\".into(), profile.into());\n    }\n    config\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_use_cache() {\n        let config = Mapping::new();\n        dbg!(&config);\n        let config = use_cache(config);\n        dbg!(&config);\n        assert!(config.contains_key(\"profile\"));\n\n        let mut config = Mapping::new();\n        let mut profile = Mapping::new();\n        profile.insert(\"do-not-override\".into(), true.into());\n        config.insert(\"profile\".into(), profile.into());\n        dbg!(&config);\n        let config = use_cache(config);\n        dbg!(&config);\n        assert!(config.contains_key(\"profile\"));\n        assert!(\n            config\n                .get(\"profile\")\n                .unwrap()\n                .as_mapping()\n                .unwrap()\n                .contains_key(\"do-not-override\")\n        );\n    }\n\n    #[test]\n    fn test_use_include_all_proxy_groups() {\n        let yaml = r#\"\nproxies:\n  - name: \"Proxy1\"\n    type: ss\n    server: server.com\n    port: 443\n  - name: \"Proxy2\"\n    type: vmess\n    server: server2.com\n    port: 8080\nproxy-providers:\n  provider1:\n    type: http\n    url: \"http://example.com/provider1.yaml\"\n    interval: 3600\n  provider2:\n    type: file\n    path: ./providers/provider2.yaml\nproxy-groups:\n  - name: GLOBAL\n    type: select\n    include-all: true\n    proxies:\n      - DIRECT\n  - name: Proxies\n    type: select\n    proxies:\n      - DIRECT\n\"#;\n        let config: Mapping = serde_yaml::from_str(yaml).unwrap();\n        let result = use_include_all_proxy_groups(config);\n\n        // Check that GLOBAL group now contains all proxies\n        let proxy_groups = result.get(\"proxy-groups\").unwrap().as_sequence().unwrap();\n        let global_group = proxy_groups\n            .iter()\n            .find(|group| {\n                if let Some(mapping) = group.as_mapping() {\n                    if let Some(name) = mapping.get(\"name\") {\n                        return name.as_str().unwrap() == \"GLOBAL\";\n                    }\n                }\n                false\n            })\n            .unwrap();\n\n        // Check that include-all field was removed\n        assert!(\n            global_group\n                .as_mapping()\n                .unwrap()\n                .get(\"include-all\")\n                .is_none()\n        );\n\n        let global_proxies = global_group\n            .as_mapping()\n            .unwrap()\n            .get(\"proxies\")\n            .unwrap()\n            .as_sequence()\n            .unwrap();\n        let proxy_names: Vec<&str> = global_proxies.iter().map(|p| p.as_str().unwrap()).collect();\n\n        // Should contain all proxies from the config\n        assert!(proxy_names.contains(&\"Proxy1\"));\n        assert!(proxy_names.contains(&\"Proxy2\"));\n        assert!(proxy_names.contains(&\"provider1\"));\n        assert!(proxy_names.contains(&\"provider2\"));\n        // Should still contain original proxies\n        assert!(proxy_names.contains(&\"DIRECT\"));\n    }\n}\n"
  },
  {
    "path": "backend/tauri/src/enhance/script/js.rs",
    "content": "use super::runner::{ProcessOutput, Runner, wrap_result};\nuse crate::enhance::utils::{LogSpan, Logs, LogsExt};\nuse anyhow::Context as _;\nuse async_trait::async_trait;\nuse boa_engine::{\n    Context, JsError, JsNativeError, JsValue, Source,\n    builtins::promise::PromiseState,\n    job::SimpleJobExecutor,\n    js_string,\n    module::{Module, SimpleModuleLoader},\n    property::Attribute,\n};\nuse boa_utils::{\n    Console,\n    module::{combine::CombineModuleLoader, http::HttpModuleLoader},\n};\nuse once_cell::sync::Lazy;\nuse serde_yaml::Mapping;\nuse std::{\n    cell::RefCell,\n    path::{Path, PathBuf},\n    rc::Rc,\n    time::Duration,\n};\nuse tracing_attributes::instrument;\nuse utils::wrap_script_if_not_esm;\n\nuse std::result::Result as StdResult;\n\ntype Result<T, E = JsRunnerError> = StdResult<T, E>;\n\nstatic CUSTOM_SCRIPTS_DIR: Lazy<PathBuf> = Lazy::new(|| {\n    let path = crate::utils::dirs::app_data_dir().unwrap().join(\"scripts\");\n    if !path.exists() {\n        std::fs::create_dir_all(&path).unwrap();\n    }\n    dunce::canonicalize(path).unwrap()\n});\n\n// define a JsRunnerError due to boa engine error is not Send\n#[derive(Debug, thiserror::Error)]\npub enum JsRunnerError {\n    #[error(\"JsError: {0}\")]\n    JsError(#[from] boa_engine::JsError),\n    #[error(\"JsNativeError: {0}\")]\n    JsNativeError(#[from] boa_engine::JsNativeError),\n    #[error(\"TryNativeError: {0}\")]\n    TryNativeError(#[from] boa_engine::error::TryNativeError),\n    #[error(\"IoError: {0}\")]\n    IoError(#[from] std::io::Error),\n    #[error(\"Other: {0}\")]\n    Other(String),\n}\n\npub struct BoaConsoleLogger(Logs);\nimpl boa_utils::Logger for BoaConsoleLogger {\n    type Item = boa_utils::LogMessage;\n    fn log(&mut self, msg: boa_utils::LogMessage, _: &Console) {\n        match msg {\n            boa_utils::LogMessage::Log(msg) => self.0.log(msg),\n            boa_utils::LogMessage::Info(msg) => self.0.info(msg),\n            boa_utils::LogMessage::Warn(msg) => self.0.warn(msg),\n            boa_utils::LogMessage::Error(msg) => self.0.error(msg),\n        }\n    }\n\n    #[inline]\n    fn take(&mut self) -> Vec<Self::Item> {\n        std::mem::take(&mut self.0)\n            .into_iter()\n            .map(|(span, msg)| match span {\n                LogSpan::Log => boa_utils::LogMessage::Log(msg),\n                LogSpan::Info => boa_utils::LogMessage::Info(msg),\n                LogSpan::Warn => boa_utils::LogMessage::Warn(msg),\n                LogSpan::Error => boa_utils::LogMessage::Error(msg),\n            })\n            .collect()\n    }\n}\n\nimpl BoaConsoleLogger {\n    pub fn take(&mut self) -> Logs {\n        std::mem::take(&mut self.0)\n    }\n}\n\n#[inline]\nfn take_console_logs() -> Logs {\n    let logs = boa_utils::inspect_logger(|logger| logger.take());\n    logs.into_iter()\n        .map(|msg| match msg {\n            boa_utils::LogMessage::Log(msg) => (LogSpan::Log, msg),\n            boa_utils::LogMessage::Info(msg) => (LogSpan::Info, msg),\n            boa_utils::LogMessage::Warn(msg) => (LogSpan::Warn, msg),\n            boa_utils::LogMessage::Error(msg) => (LogSpan::Error, msg),\n        })\n        .collect()\n}\n\npub struct JSRunner;\n\n// boa engine is single-thread runner so that we can not define it in runner trait directly\npub struct BoaRunner {\n    ctx: Rc<RefCell<Context>>,\n    simple_loader: Rc<SimpleModuleLoader>,\n}\n\nimpl BoaRunner {\n    pub fn try_new() -> Result<Self> {\n        let cache_dir = crate::utils::dirs::cache_dir().unwrap();\n        let loader = Rc::new(CombineModuleLoader::new(\n            SimpleModuleLoader::new(CUSTOM_SCRIPTS_DIR.as_path())?,\n            HttpModuleLoader::new(cache_dir, Duration::from_secs(60 * 60 * 24 * 30)),\n        ));\n        let simple_loader = loader.clone_simple();\n        let queue = Rc::new(SimpleJobExecutor::new());\n        let context = Context::builder()\n            .job_executor(queue)\n            .module_loader(loader.clone())\n            .build()?;\n        Ok(Self {\n            ctx: Rc::new(RefCell::new(context)),\n            simple_loader,\n        })\n    }\n\n    pub fn setup_console(&self, logger: BoaConsoleLogger) -> Result<()> {\n        let ctx = &mut self.ctx.borrow_mut();\n        // it not concurrency safe. we should move to new boa_runtime console when it is ready for custom logger\n        boa_utils::set_logger(Box::new(logger) as Box<dyn boa_utils::LoggerBox>);\n        let console = Console::init(ctx);\n        ctx.register_global_property(js_string!(Console::NAME), console, Attribute::all())?;\n        Ok(())\n    }\n\n    pub fn get_ctx(&self) -> Rc<RefCell<Context>> {\n        self.ctx.clone()\n    }\n\n    /// Parse a module to prepare for execution.\n    pub fn parse_module(&self, source: &str, name: &str) -> Result<Module> {\n        let ctx = &mut self.ctx.borrow_mut();\n        let path_name = format!(\"./{name}.mjs\");\n        let source = Source::from_reader(source.as_bytes(), Some(Path::new(&path_name)));\n        // Can also pass a `Some(realm)` if you need to execute the module in another realm.\n        let module = Module::parse(source, None, ctx)?;\n        // Don't forget to insert the parsed module into the loader itself, since the root module\n        // is not automatically inserted by the `ModuleLoader::load_imported_module` impl.\n        //\n        // Simulate as if the \"fake\" module is located in the modules root, just to ensure that\n        // the loader won't double load in case someone tries to import \"./main.mjs\".\n        self.simple_loader\n            .insert(CUSTOM_SCRIPTS_DIR.join(&path_name), module.clone());\n        Ok(module)\n    }\n\n    pub fn execute_module(&self, module: &Module) -> Result<()> {\n        let ctx = &mut self.ctx.borrow_mut();\n        let promise_result = module.load_link_evaluate(ctx);\n\n        // Very important to push forward the job queue after queueing promises.\n        let _ = ctx.run_jobs();\n\n        // Checking if the final promise didn't return an error.\n        for i in 0..20 {\n            match promise_result.state() {\n                PromiseState::Pending => {\n                    if i == 19 {\n                        return Err(JsRunnerError::Other(\"module didn't execute!\".to_string()));\n                    }\n                }\n                PromiseState::Fulfilled(v) => {\n                    assert_eq!(v, JsValue::undefined());\n                    break;\n                }\n                PromiseState::Rejected(err) => {\n                    return Err(JsError::from_opaque(err).try_native(ctx)?.into());\n                }\n            }\n            std::thread::sleep(std::time::Duration::from_millis(100));\n        }\n        Ok(())\n    }\n}\n\n#[async_trait]\nimpl Runner for JSRunner {\n    #[instrument]\n    fn try_new() -> Result<JSRunner, anyhow::Error> {\n        Ok(JSRunner)\n    }\n\n    async fn process(&self, mapping: Mapping, path: &str) -> ProcessOutput {\n        let content = wrap_result!(\n            tokio::fs::read_to_string(path)\n                .await\n                .context(\"failed to read the script file\")\n        );\n        self.process_honey(mapping, &content).await\n    }\n\n    async fn process_honey(&self, mapping: Mapping, script: &str) -> ProcessOutput {\n        let script = wrap_result!(wrap_script_if_not_esm(script));\n        let hash = crate::utils::help::get_uid(\"script\");\n        let path = CUSTOM_SCRIPTS_DIR.join(format!(\"{hash}.mjs\"));\n        wrap_result!(\n            tokio::fs::write(&path, script.as_bytes())\n                .await\n                .context(\"failed to write the script file\")\n        );\n        // boa engine is single-thread runner so that we can use it in tokio::task::spawn_blocking\n        let res = tokio::task::spawn_blocking(move || {\n            let wrapped_fn = move || {\n                let mut logger = BoaConsoleLogger(Logs::new());\n                let boa_runner = wrap_result!(BoaRunner::try_new(), logger.take());\n                wrap_result!(boa_runner.setup_console(logger), take_console_logs());\n                let config = wrap_result!(\n                    serde_json::to_string(&mapping)\n                        .map_err(|e| { std::io::Error::new(std::io::ErrorKind::InvalidData, e) }),\n                    take_console_logs()\n                );\n                let config = serde_json::to_string(&config).unwrap(); // escape the string\n                let execute_module = format!(\n                    r#\"import process from \"./{hash}.mjs\";\n        let config = JSON.parse({config});\n        export let result = JSON.stringify(await process(config));\n        \"#\n                );\n                // let process_module = wrap_result!(\n                //     boa_runner.parse_module(&script, \"process\").map_err(|e| {\n                //         logs.error(format!(\"failed to parse the process module: {:?}\", e));\n                //         e\n                //     }),\n                //     logs\n                // );\n                // wrap_result!(boa_runner.execute_module(&process_module));\n                let main_module = wrap_result!(\n                    boa_runner.parse_module(&execute_module, \"main\"),\n                    take_console_logs()\n                );\n                wrap_result!(boa_runner.execute_module(&main_module));\n                let ctx = boa_runner.get_ctx();\n                let namespace = main_module.namespace(&mut ctx.borrow_mut());\n                let result = wrap_result!(\n                    namespace.get(js_string!(\"result\"), &mut ctx.borrow_mut()),\n                    take_console_logs()\n                );\n                let result = wrap_result!(\n                    result\n                        .as_string()\n                        .ok_or_else(|| JsNativeError::typ().with_message(\"Expected string\"))\n                        .map(|str| str.to_std_string_escaped()),\n                    take_console_logs()\n                );\n                let mapping = wrap_result!(\n                    serde_json::from_str(&result)\n                        .map_err(|e| { std::io::Error::new(std::io::ErrorKind::InvalidData, e) }),\n                    take_console_logs()\n                );\n                (Ok::<Mapping, JsRunnerError>(mapping), take_console_logs())\n            };\n            let (res, logs) = wrapped_fn();\n            match res {\n                Ok(mapping) => (Ok(mapping), logs),\n                Err(e) => {\n                    tracing::error!(\"error: {:?}\", e);\n                    (Err(anyhow::anyhow!(\"{:?}\", e)), logs)\n                }\n            }\n        })\n        .await;\n        let _ = tokio::fs::remove_file(&path).await;\n        match res {\n            Ok(output) => output,\n            Err(e) => (Err(e.into()), vec![]),\n        }\n    }\n}\n\nmod utils {\n    use oxc_allocator::Allocator;\n    use oxc_ast_visit::{\n        Visit,\n        walk::{walk_function, walk_module_export_name},\n    };\n    use oxc_parser::Parser;\n    use oxc_span::{SourceType, Span};\n    use oxc_syntax::scope::ScopeFlags;\n\n    use std::borrow::Cow;\n\n    #[derive(Debug)]\n    // TODO: support fn params check and support typescript type erase\n    struct DefaultExport {\n        span: Span,\n        is_function: bool,\n    }\n\n    #[derive(Debug, Default)]\n    struct FunctionVisitor<'n> {\n        exported_name: Vec<Cow<'n, str>>,\n        declared_functions: Vec<(Cow<'n, str>, Cow<'n, Span>)>,\n        default_export: Option<DefaultExport>,\n    }\n\n    impl<'n> Visit<'n> for FunctionVisitor<'n> {\n        // Visit module exported name to confirm whether exists default export\n        fn visit_module_export_name(&mut self, it: &oxc_ast::ast::ModuleExportName<'n>) {\n            match it {\n                oxc_ast::ast::ModuleExportName::IdentifierName(id) => {\n                    self.exported_name.push(Cow::Borrowed(id.name.as_str()))\n                }\n                oxc_ast::ast::ModuleExportName::IdentifierReference(id) => {\n                    self.exported_name.push(Cow::Borrowed(id.name.as_str()))\n                }\n                oxc_ast::ast::ModuleExportName::StringLiteral(s) => {\n                    self.exported_name.push(Cow::Borrowed(s.value.as_str()))\n                }\n            }\n            walk_module_export_name(self, it);\n        }\n\n        // Visit function declaration to save the function name and span and check whether it is default export\n        fn visit_function(&mut self, it: &oxc_ast::ast::Function<'n>, flags: ScopeFlags) {\n            // eprintln!(\"function: {:#?}\", it);\n            if let Some(id) = it.id.clone() {\n                self.declared_functions\n                    .push((Cow::Borrowed(id.name.as_str()), Cow::Owned(it.span)));\n            }\n            walk_function(self, it, flags);\n        }\n\n        // Visit export default declaration to save the default export\n        fn visit_export_default_declaration(\n            &mut self,\n            it: &oxc_ast::ast::ExportDefaultDeclaration<'n>,\n        ) {\n            self.default_export = Some(DefaultExport {\n                is_function: matches!(\n                    it.declaration,\n                    oxc_ast::ast::ExportDefaultDeclarationKind::FunctionDeclaration(_)\n                ),\n                span: it.span,\n            });\n        }\n    }\n\n    /// This is a tool function to wrap the script if it is not a ESM script.\n    pub fn wrap_script_if_not_esm(script: &str) -> Result<Cow<'_, str>, anyhow::Error> {\n        let allocator = Allocator::default();\n        let source_type = SourceType::default().with_module(true);\n        let source_text = script.trim_matches(['\\t', '\\n', '\\r', ' ']);\n        let result = Parser::new(&allocator, source_text, source_type).parse();\n\n        if !result.errors.is_empty() {\n            let mut errors = String::new();\n            for error in result.errors {\n                errors.push_str(&format!(\n                    \"{:?}\\n\",\n                    error.with_source_code(source_text.to_string())\n                ));\n            }\n            return Err(anyhow::anyhow!(\"parse error: {}\", errors));\n        }\n        #[cfg(test)]\n        eprintln!(\"result: {:#?}\", result.program);\n        let mut visitor = FunctionVisitor::default();\n        visitor.visit_program(&result.program);\n        #[cfg(test)]\n        eprintln!(\"visitor: {:#?}\", visitor);\n        if visitor.default_export.is_some() {\n            return Ok(Cow::Borrowed(script));\n        }\n        // check whether `function main` exists\n        match visitor\n            .declared_functions\n            .iter()\n            .find(|(name, _)| name.contains(\"main\"))\n        {\n            Some((_, span)) => {\n                // just insert `export default` before the function\n                let mut script = script.to_string();\n                script.insert_str(span.start as usize, \"export default \");\n                Ok(Cow::Owned(script))\n            }\n            None => Err(anyhow::anyhow!(\"no default export or main function\")),\n        }\n    }\n}\n\n#[cfg(test)]\nmod test {\n    #[test]\n    fn test_wrap_script_if_not_esm() {\n        let script = r#\"function main(config) {\n            return config\n        };\"#;\n        let script = super::utils::wrap_script_if_not_esm(script).unwrap();\n        assert_eq!(\n            script,\n            \"export default function main(config) {\\n            return config\\n        };\"\n        );\n    }\n\n    #[test]\n    fn test_wrap_script_if_esm() {\n        let script =\n            \"export default function main(config) {\\n            return config\\n        };\";\n        let script = super::utils::wrap_script_if_not_esm(script).unwrap();\n        assert_eq!(\n            script,\n            \"export default function main(config) {\\n            return config\\n        };\"\n        );\n    }\n\n    #[test]\n    fn test_wrap_script_if_not_esm_sample_2() {\n        let script = r#\"// 国内DNS服务器\nconst domesticNameservers = [\n  \"https://dns.alidns.com/dns-query\", // 阿里云公共DNS\n  \"https://doh.pub/dns-query\", // 腾讯DNSPod\n  \"https://doh.360.cn/dns-query\" // 360安全DNS\n];\n// 国外DNS服务器\nconst foreignNameservers = [\n  \"https://1.1.1.1/dns-query\", // Cloudflare(主)\n  \"https://1.0.0.1/dns-query\", // Cloudflare(备)\n  \"https://208.67.222.222/dns-query\", // OpenDNS(主)\n  \"https://208.67.220.220/dns-query\", // OpenDNS(备)\n  \"https://194.242.2.2/dns-query\", // Mullvad(主)\n  \"https://194.242.2.3/dns-query\" // Mullvad(备)\n];\n        function main(config) {\n            // do something\n            return config\n        };\"#;\n        let script = super::utils::wrap_script_if_not_esm(script).unwrap();\n        assert_eq!(\n            script,\n            r#\"// 国内DNS服务器\nconst domesticNameservers = [\n  \"https://dns.alidns.com/dns-query\", // 阿里云公共DNS\n  \"https://doh.pub/dns-query\", // 腾讯DNSPod\n  \"https://doh.360.cn/dns-query\" // 360安全DNS\n];\n// 国外DNS服务器\nconst foreignNameservers = [\n  \"https://1.1.1.1/dns-query\", // Cloudflare(主)\n  \"https://1.0.0.1/dns-query\", // Cloudflare(备)\n  \"https://208.67.222.222/dns-query\", // OpenDNS(主)\n  \"https://208.67.220.220/dns-query\", // OpenDNS(备)\n  \"https://194.242.2.2/dns-query\", // Mullvad(主)\n  \"https://194.242.2.3/dns-query\" // Mullvad(备)\n];\n        export default function main(config) {\n            // do something\n            return config\n        };\"#\n        );\n    }\n\n    #[test]\n    fn test_process_honey() {\n        use super::{super::runner::Runner, JSRunner};\n        let runner = JSRunner::try_new().unwrap();\n        let mapping = serde_yaml::from_str(\n            r#\"\n        rules:\n                - RULE-SET,custom-reject,REJECT\n                - RULE-SET,custom-direct,DIRECT\n                - RULE-SET,custom-proxy,🚀\n        tun:\n            enable: false\n        dns:\n            enable: false\n        \"#,\n        )\n        .unwrap();\n        let script = r#\"\n        export default async function main(config) {\n            if (Array.isArray(config.rules)) {\n                config.rules = [...config.rules, \"MATCH,🚀\"];\n            }\n            // print(JSON.stringify(config));\n            console.log(\"Test console log\");\n            console.warn(\"Test console log\");\n            console.error(\"Test console log\");\n            config.proxies = [\"Test\"];\n            return config;\n        }\"#;\n        tokio::runtime::Builder::new_current_thread()\n            .enable_all()\n            .build()\n            .unwrap()\n            .block_on(async move {\n                let (res, logs) = runner.process_honey(mapping, script).await;\n                eprintln!(\"logs: {logs:?}\");\n                let mapping = res.unwrap();\n                assert_eq!(\n                    mapping[\"rules\"],\n                    serde_yaml::Value::Sequence(vec![\n                        serde_yaml::Value::String(\"RULE-SET,custom-reject,REJECT\".to_string()),\n                        serde_yaml::Value::String(\"RULE-SET,custom-direct,DIRECT\".to_string()),\n                        serde_yaml::Value::String(\"RULE-SET,custom-proxy,🚀\".to_string()),\n                        serde_yaml::Value::String(\"MATCH,🚀\".to_string())\n                    ])\n                );\n                assert_eq!(\n                    mapping[\"proxies\"],\n                    serde_yaml::Value::Sequence(vec![serde_yaml::Value::String(\n                        \"Test\".to_string()\n                    ),])\n                );\n                let outs = serde_json::to_string(&logs).unwrap();\n                assert_eq!(\n                    outs,\n                    r#\"[[\"log\",\"Test console log\"],[\"warn\",\"Test console log\"],[\"error\",\"Test console log\"]]\"#\n                );\n            });\n    }\n\n    #[test_log::test]\n    fn test_process_honey_with_fetch() {\n        use super::{super::runner::Runner, JSRunner};\n        let runner = JSRunner::try_new().unwrap();\n        let mapping = serde_yaml::from_str(\n            r#\"\n        rules:\n                - RULE-SET,custom-reject,REJECT\n                - RULE-SET,custom-direct,DIRECT\n                - RULE-SET,custom-proxy,🚀\n        tun:\n            enable: false\n        dns:\n            enable: false\n        \"#,\n        )\n        .unwrap();\n        let script = r#\"\n        import YAML from 'https://esm.run/yaml@2.3.4';\n        import fromAsync from 'https://esm.run/array-from-async@3.0.0';\n        import { Base64 } from 'https://esm.run/js-base64@3.7.6';\n\n\n        export default async function main(config) {\n            const data = `\n            object:\n                array: [\"hello\", \"world\"]\n                key: \"value\"\n            `;\n\n            const object = YAML.parse(data).object;\n\n            let result = await fromAsync([\n                Promise.resolve(Base64.encode(object.array[0])),\n                Promise.resolve(Base64.encode(object.array[1])),\n            ]);\n            // add result to config.rules\n            config.rules.push(`${result[0]}`);\n            config.rules.push(`${result[1]}`);\n            return config;\n        }\"#;\n        tokio::runtime::Builder::new_current_thread()\n            .enable_all()\n            .build()\n            .unwrap()\n            .block_on(async move {\n                let (res, logs) = runner.process_honey(mapping, script).await;\n                eprintln!(\"logs: {logs:?}\");\n                let mapping = res.unwrap();\n                assert_eq!(\n                    mapping[\"rules\"],\n                    serde_yaml::Value::Sequence(vec![\n                        serde_yaml::Value::String(\"RULE-SET,custom-reject,REJECT\".to_string()),\n                        serde_yaml::Value::String(\"RULE-SET,custom-direct,DIRECT\".to_string()),\n                        serde_yaml::Value::String(\"RULE-SET,custom-proxy,🚀\".to_string()),\n                        serde_yaml::Value::String(\"aGVsbG8=\".to_string()),\n                        serde_yaml::Value::String(\"d29ybGQ=\".to_string()),\n                    ])\n                );\n                let outs = serde_json::to_string(&logs).unwrap();\n                assert_eq!(outs, r#\"[]\"#);\n            });\n    }\n\n    #[test_log::test]\n    fn test_process_honey_with_builtin_modules() {\n        use super::{super::runner::Runner, JSRunner};\n        let runner = JSRunner::try_new().unwrap();\n        let mapping = serde_yaml::from_str(\n            r#\"\n        rules:\n                - RULE-SET,custom-reject,REJECT\n                - RULE-SET,custom-direct,DIRECT\n                - RULE-SET,custom-proxy,🚀\n        tun:\n            enable: false\n        dns:\n            enable: false\n        \"#,\n        )\n        .unwrap();\n        let script = r#\"\n        import { yaml } from \"nyan:utils\";\n        import { Base64 } from \"nyan:js-base64\";\n\n\n        export default async function main(config) {\n            const data = yaml`\n            object:\n                array: [\"hello\", \"world\"]\n                key: \"value\"\n            `;\n\n            const object = data.object;\n\n            let result = [\n                Base64.encode(object.array[0]),\n                Base64.encode(object.array[1]),\n            ];\n            // add result to config.rules\n            config.rules.push(`${result[0]}`);\n            config.rules.push(`${result[1]}`);\n            return config;\n        }\"#;\n        tokio::runtime::Builder::new_current_thread()\n            .enable_all()\n            .build()\n            .unwrap()\n            .block_on(async move {\n                let (res, logs) = runner.process_honey(mapping, script).await;\n                eprintln!(\"logs: {logs:?}\");\n                let mapping = res.unwrap();\n                assert_eq!(\n                    mapping[\"rules\"],\n                    serde_yaml::Value::Sequence(vec![\n                        serde_yaml::Value::String(\"RULE-SET,custom-reject,REJECT\".to_string()),\n                        serde_yaml::Value::String(\"RULE-SET,custom-direct,DIRECT\".to_string()),\n                        serde_yaml::Value::String(\"RULE-SET,custom-proxy,🚀\".to_string()),\n                        serde_yaml::Value::String(\"aGVsbG8=\".to_string()),\n                        serde_yaml::Value::String(\"d29ybGQ=\".to_string()),\n                    ])\n                );\n                let outs = serde_json::to_string(&logs).unwrap();\n                assert_eq!(outs, r#\"[]\"#);\n            });\n    }\n}\n"
  },
  {
    "path": "backend/tauri/src/enhance/script/lua/mod.rs",
    "content": "use std::sync::Arc;\n\nuse anyhow::Error;\nuse mlua::prelude::*;\nuse parking_lot::Mutex;\nuse serde_yaml::{Mapping, Value};\n\nuse crate::enhance::{Logs, LogsExt, runner::wrap_result, utils::take_logs};\n\nuse super::runner::{ProcessOutput, Runner};\n\npub fn create_lua_context() -> Result<Lua, anyhow::Error> {\n    let lua = Lua::new();\n    lua.load_std_libs(LuaStdLib::ALL_SAFE)?;\n    Ok(lua)\n}\n\nfn create_console(lua: &Lua, logger: Arc<Mutex<Option<Logs>>>) -> Result<(), anyhow::Error> {\n    let table = lua.create_table()?;\n    let logger_ = logger.clone();\n    let log = lua.create_function(move |_, msg: String| {\n        let mut logger = logger_.lock();\n        logger.as_mut().unwrap().log(msg);\n        Ok(())\n    })?;\n    let logger_ = logger.clone();\n    let info = lua.create_function(move |_, msg: String| {\n        let mut logger = logger_.lock();\n        logger.as_mut().unwrap().info(msg);\n        Ok(())\n    })?;\n    let logger_ = logger.clone();\n    let warn = lua.create_function(move |_, msg: String| {\n        let mut logger = logger_.lock();\n        logger.as_mut().unwrap().warn(msg);\n        Ok(())\n    })?;\n    let error = lua.create_function(move |_, msg: String| {\n        let mut logger = logger.lock();\n        logger.as_mut().unwrap().error(msg);\n        Ok(())\n    })?;\n    table.set(\"log\", log)?;\n    table.set(\"info\", info)?;\n    table.set(\"warn\", warn)?;\n    table.set(\"error\", error)?;\n    lua.globals().set(\"console\", table)?;\n    Ok(())\n}\n\n/// This is a workaround for mihomo's yaml config based on the index of the map.\n/// We compare the keys of the index order of the original mapping with the target mapping,\n/// and then we correct the order of the target mapping.\n/// This is a recursive call, so it will correct the order of the nested mapping.\nfn correct_original_mapping_order(target: &mut Value, original: &Value) {\n    if !target.is_mapping() && !target.is_sequence() {\n        return;\n    }\n\n    match (target, original) {\n        (Value::Mapping(target_mapping), Value::Mapping(original_mapping)) => {\n            let original_keys: Vec<_> = original_mapping.keys().collect();\n            let mut new_mapping = serde_yaml::Mapping::new();\n\n            for key in original_keys {\n                if let Some(mut value) = target_mapping.remove(key) {\n                    if let Some(original_value) = original_mapping.get(key) {\n                        correct_original_mapping_order(&mut value, original_value);\n                    }\n                    new_mapping.insert(key.clone(), value);\n                }\n            }\n\n            let remaining_keys = target_mapping.keys().cloned().collect::<Vec<_>>();\n            for key in remaining_keys {\n                if let Some(value) = target_mapping.remove(&key) {\n                    new_mapping.insert(key, value);\n                }\n            }\n\n            *target_mapping = new_mapping;\n        }\n        (Value::Sequence(target), Value::Sequence(original)) if target.len() == original.len() => {\n            for (target_value, original_value) in target.iter_mut().zip(original.iter()) {\n                // TODO: Maybe here exist a bug when the mappings was not in the same order\n                correct_original_mapping_order(target_value, original_value);\n            }\n        }\n        _ => {}\n    }\n}\n\npub struct LuaRunner;\n\n#[async_trait::async_trait]\nimpl Runner for LuaRunner {\n    fn try_new() -> Result<Self, Error> {\n        Ok(Self)\n    }\n\n    async fn process(&self, mapping: Mapping, path: &str) -> ProcessOutput {\n        let file = wrap_result!(tokio::fs::read_to_string(path).await);\n        self.process_honey(mapping, &file).await\n    }\n    // TODO: Keep the order of the dictionary structure in the configuration when processing lua. Because mihomo needs ordered dictionaries for dns policy.\n    async fn process_honey(&self, mapping: Mapping, script: &str) -> ProcessOutput {\n        let lua = wrap_result!(create_lua_context());\n        let logger = Arc::new(Mutex::new(Some(Logs::new())));\n        wrap_result!(create_console(&lua, logger.clone()), take_logs(logger));\n        let config = wrap_result!(\n            lua.to_value(&mapping)\n                .context(\"Failed to convert mapping to value\"),\n            take_logs(logger)\n        );\n        wrap_result!(\n            lua.globals()\n                .set(\"config\", config)\n                .context(\"Failed to set config\"),\n            take_logs(logger)\n        );\n        let output = wrap_result!(\n            lua.load(script)\n                .eval::<mlua::Value>()\n                .context(\"Failed to load script\"),\n            take_logs(logger)\n        );\n        if !output.is_table() {\n            return wrap_result!(\n                Err(anyhow::anyhow!(\n                    \"Script must return a table, data: {:?}\",\n                    output\n                )),\n                take_logs(logger)\n            );\n        }\n        let config: Mapping = wrap_result!(\n            lua.from_value(output)\n                .context(\"Failed to convert output to config\"),\n            take_logs(logger)\n        );\n\n        // Correct the order of the mapping\n        correct_original_mapping_order(\n            &mut Value::Mapping(config.clone()),\n            &Value::Mapping(mapping),\n        );\n\n        (Ok(config), take_logs(logger))\n    }\n}\n\nmod tests {\n    #[test]\n    fn test_process_honey() {\n        use super::*;\n        use crate::enhance::runner::Runner;\n        use serde_yaml::Mapping;\n\n        let runner = LuaRunner;\n        let mapping = r#\"\n        proxies:\n        - 123\n        - 12312\n        - asdxxx\n        shoud_remove: 123\n        \"#;\n\n        let mapping = serde_yaml::from_str::<Mapping>(mapping).unwrap();\n        let script = r#\"\n            console.log(\"Hello, world!\");\n            console.warn(\"Hello, world!\");\n            console.error(\"Hello, world!\");\n            config[\"proxies\"] = {1, 2, 3};\n            config[\"shoud_remove\"] = nil;\n            return config;\n        \"#;\n        let expected = r#\"\n        proxies:\n        - 1\n        - 2\n        - 3\n        \"#;\n\n        let (result, logs) = tokio::runtime::Runtime::new()\n            .unwrap()\n            .block_on(runner.process_honey(mapping, script));\n        eprintln!(\"{logs:?}\\n{result:?}\");\n        assert!(result.is_ok());\n        assert_eq!(logs.len(), 3);\n        let expected = serde_yaml::from_str::<Mapping>(expected).unwrap();\n        assert_eq!(expected, result.unwrap());\n    }\n\n    #[test]\n    fn test_correct_original_mapping_order() {\n        use super::*;\n\n        let mut target = serde_yaml::from_str::<Value>(\n            r#\"            ######### 锚点 start #######\nTroxyInPort: &TroxyInPort 65535\nShareInPort: &ShareInPort 65534\n# TailscaleOutPort: &TailscaleOutPort 65528\nReqableOutPort: &ReqableOutPort 9000\nDNSSocket: &DNSSocket 127.0.0.1:65533\nUISocket: &UISocket 127.0.0.1:65532\ndirect_dns: &direct_dns\n  - 114.114.114.114#直连DNS\n  - https://doh.pub/dns-query#直连DNS\n  - https://dns.alidns.com/dns-query#直连DNS\n  - system\ncn_dns: &cn_dns\n  - 114.114.114.114#中国DNS\n  - https://doh.pub/dns-query#中国DNS\n  - https://dns.alidns.com/dns-query#中国DNS\n  - system\ninternational_dns: &international_dns\n  - \"https://dns.cloudflare.com/dns-query#国际DNS\"\n  - \"https://doh.opendns.com/dns-query#国际DNS\"\n  - \"https://dns.w3ctag.org/dns-query#国际DNS\"\n  - \"https://dns.google/dns-query#国际DNS\"\nus_dns: &us_dns\n  - \"https://dns.koala.us.to/dns-query#美国\"\n  - \"https://dns.dns-53.us/dns-query#美国\" \n  - \"https://cloudflare-dns.com/dns-query#美国\"\n  - \"https://doh.opendns.com/dns-query#美国\"\n  - \"https://dns.google/dns-query#美国\"\nuk_dns: &uk_dns\n  - \"https://dns.aa.net.uk/dns-query#英国\"\n  - \"https://princez.uk/dns-query#英国\" \n  - \"https://dns.dns-53.uk/dns-query#英国\"\nde_dns: &de_dns\n  - \"https://doh.ffmuc.net/dns-query#德国\"\n  - \"https://dns.dnshome.de/dns-query#德国\"\n  - \"https://dnsforge.de/dns-query#德国\"\n  - \"https://bahopir188.dnshome.de/dns-query#德国\"\n  - \"https://dns.csa-rz.de/dns-query#德国\"\n  - \"https://dns.datenquark.de/dns-query#德国\"\n  - \"https://doh-de.blahdns.com/dns-query#德国\" \n  - \"https://dns.telekom.de/dns-query#德国\"\n  - \"https://dns.csaonline.de/dns-query#德国\"  \nfr_dns: &fr_dns\n  - \"https://ns0.fdn.fr/dns-query#法国\"\n  - \"https://qlf-doh.inria.fr/dns-query#法国\"\n  - \"https://dns.k3nny.fr/dns-query#法国\"\n  - \"https://doh.ffmuc.net/dns-query#法国\" \njp_dns: &jp_dns\n  - \"https://public.dns.iij.jp/dns-query#日本\"\n  - \"https://dns.google/dns-query#日本\"\nhk_dns: &hk_dns\n  - \"https://dns.cloudflare.com/dns-query#香港\"\n  - \"https://doh.opendns.com/dns-query#香港\"\n  - \"https://dns.w3ctag.org/dns-query#香港\"\n  - \"https://dns.google/dns-query#香港\"\nmo_dns: &mo_dns\n  - \"https://dns.cloudflare.com/dns-query#澳门\"\n  - \"https://doh.opendns.com/dns-query#澳门\"\n  - \"https://dns.w3ctag.org/dns-query#澳门\"\n  - \"https://dns.google/dns-query#澳门\"\ntw_dns: &tw_dns\n  - \"https://dns.cloudflare.com/dns-query#台湾\"\n  - \"https://doh.opendns.com/dns-query#台湾\"\n  - \"https://dns.w3ctag.org/dns-query#台湾\"\n  - \"https://dns.google/dns-query#台湾\"\nsg_dns: &sg_dns\n  - \"https://dns.cloudflare.com/dns-query#新加坡\"\n  - \"https://doh.opendns.com/dns-query#新加坡\"\n  - \"https://dns.w3ctag.org/dns-query#新加坡\"\n  - \"https://dns.google/dns-query#新加坡\"\nru_dns: &ru_dns\n  - \"https://dns.ch295.ru/dns-query#俄国\"\n  - \"https://dns.yandex.com/dns-query#俄国\"\n  - \"https://unfiltered.adguard-dns.com/dns-query#俄国\"\nin_dns: &in_dns\n  - \"https://dns.gutwe.in/dns-query#印度\"\n  - \"https://dns.brahma.world/dns-query#印度\"\nbr_dns: &br_dns\n  - \"https://adguard.frutuozo.com.br/dns-query#巴西\"\n  - \"https://dns.google/dns-query#巴西\"\nca_dns: &ca_dns\n  - \"https://dns1.dnscrypt.ca/dns-query#加拿大\"\n  - \"https://dns.cloudflare.com/dns-query#加拿大\"\nau_dns: &au_dns\n  - \"https://dns.netraptor.com.au/dns-query#澳大利亚\"\n  - \"https://dns.quad9.net/dns-query#澳大利亚\"\nit_dns: &it_dns\n  - \"https://doh.libredns.gr/dns-query#意大利\"\nnl_dns: &nl_dns\n  - \"https://doh.nl.ahadns.net/dns-query#荷兰\"\n  - \"https://dns.melvin2204.nl/dns-query#荷兰\"\n  - \"https://dns.quad9.net/dns-query#荷兰\"\nse_dns: &se_dns\n  - \"https://dns.mullvad.net/dns-query#瑞典\"\n  - \"https://resolver.sunet.se/dns-query#瑞典\"\n  - \"https://dns.haka.se/dns-query#瑞典\"\nch_dns: &ch_dns\n  - \"https://dns10.quad9.net/dns-query#瑞士\"\n  - \"https://doh.immerda.ch/dns-query#瑞士\"\n  - \"https://c.cicitt.ch/dns-query#瑞士\"\n  - \"https://dns.digitale-gesellschaft.ch/dns-query#瑞士\"\n  - \"https://doh.li/dns-query#瑞士\"\n\ndns:\n  enable: true\n  listen: *DNSSocket\n  ipv6: true\n  enhanced-mode: redir-host\n  default-nameserver: # proxy-server-nameserver,nameserver-policy,nameserver、fallback域名的解析\n    - 223.5.5.5#DNSDNS\n    - 114.114.114.114#DNSDNS\n    - 8.8.8.8#DNSDNS\n    - https://120.53.53.53/dns-query#DNSDNS\n    - https://223.5.5.5/dns-query#DNSDNS\n    - https://1.12.12.12/dns-query#DNSDNS\n    - system\n\n  proxy-server-nameserver: # 节点域名的解析\n    - https://120.53.53.53/dns-query#节点直连DNS\n    - https://223.5.5.5/dns-query#节点直连DNS\n    - https://1.1.1.1/dns-query#节点直连DNS\n    - https://dns.google/dns-query#节点直连DNS\n    - https://1.1.1.1/dns-query#节点国际DNS\n    - https://dns.google/dns-query#节点国际DNS\n\n  prefer-h3: false\n\n  direct-nameserver-follow-policy: false\n  direct-nameserver: # [动态回环出口:direct,中国:direct出站]时\n    *direct_dns\n\n  respect-rules: true # [中国非direct,其他地区,不出站]时，依据[nameserver-policy,nameserver、fallback]分类，使用不同dns\n  nameserver-policy: \n    \"rule-set:loopback_classical\": *direct_dns #动态回环出口\n    \"rule-set:firewall_classical\": rcode://success #个人文件\n    \"rule-set:international_classical\": *international_dns #个人文件\n    \"rule-set:domestic_classical\": *cn_dns #个人文件\n    \"rule-set:category-ads-all_classical\": rcode://success #广告拦截\n    \"rule-set:download_domain,bing_domain,openai_domain,github_domain,twitter_domain,instagram_domain,facebook_domain,youtube_domain,netflix_domain,spotify_domain,apple_domain,adobe_domain,telegram_domain,discord_domain,reddit_domain,biliintl_domain,bahamut_domain,ehentai_domain,pixiv_domain,steam_domain,epic_domain,microsoft_domain,google_domain\":\n      *international_dns\n  #中国\n    \"+.cn\": *cn_dns\n  #美国\n    \"+.us\": *us_dns\n  #英国\n    \"+.uk\": *uk_dns\n  #德国\n    \"+.de,+.eu\": *de_dns\n  #法国\n    \"+.fr\": *fr_dns\n  #日本\n    \"+.jp,+.nico\": *jp_dns\n  #香港\n    \"+.hk\": *hk_dns\n  #澳门\n    \"+.mo\": *mo_dns\n  #台湾\n    \"+.tw\": *tw_dns\n  #新加坡\n    \"+.sg\": *sg_dns\n  #俄罗斯\n    \"+.ru\": *ru_dns\n  #印度\n    \"+.in\": *in_dns\n  #巴西\n    \"+.br\": *br_dns\n  #加拿大\n    \"+.ca\": *ca_dns\n  #澳大利亚\n    \"+.au\": *au_dns\n  #意大利\n    \"+.it\": *it_dns\n  #荷兰\n    \"+.nl\": *nl_dns\n  #瑞士\n    \"+.ch\": *ch_dns\n  #瑞典\n    \"+.se\": *se_dns\n  #国际\n    \"rule-set:geolocation-!cn,tld-!cn\": *international_dns\n    \"rule-set:cn_domain,private_domain\": *cn_dns\"#,\n        )\n        .unwrap();\n        let mut original = serde_yaml::from_str::<Value>(\n            r#\"######### 锚点 start #######\nTroxyInPort: &TroxyInPort 65535\nShareInPort: &ShareInPort 65534\n# TailscaleOutPort: &TailscaleOutPort 65528\nReqableOutPort: &ReqableOutPort 9000\nDNSSocket: &DNSSocket 127.0.0.1:65533\nUISocket: &UISocket 127.0.0.1:65532\ndirect_dns: &direct_dns\n  - 114.114.114.114#直连DNS\n  - https://doh.pub/dns-query#直连DNS\n  - https://dns.alidns.com/dns-query#直连DNS\n  - system\ncn_dns: &cn_dns\n  - 114.114.114.114#中国DNS\n  - https://doh.pub/dns-query#中国DNS\n  - https://dns.alidns.com/dns-query#中国DNS\n  - system\ninternational_dns: &international_dns\n  - \"https://dns.cloudflare.com/dns-query#国际DNS\"\n  - \"https://doh.opendns.com/dns-query#国际DNS\"\n  - \"https://dns.w3ctag.org/dns-query#国际DNS\"\n  - \"https://dns.google/dns-query#国际DNS\"\nus_dns: &us_dns\n  - \"https://dns.koala.us.to/dns-query#美国\"\n  - \"https://dns.dns-53.us/dns-query#美国\" \n  - \"https://cloudflare-dns.com/dns-query#美国\"\n  - \"https://doh.opendns.com/dns-query#美国\"\n  - \"https://dns.google/dns-query#美国\"\nuk_dns: &uk_dns\n  - \"https://dns.aa.net.uk/dns-query#英国\"\n  - \"https://princez.uk/dns-query#英国\" \n  - \"https://dns.dns-53.uk/dns-query#英国\"\nde_dns: &de_dns\n  - \"https://doh.ffmuc.net/dns-query#德国\"\n  - \"https://dns.dnshome.de/dns-query#德国\"\n  - \"https://dnsforge.de/dns-query#德国\"\n  - \"https://bahopir188.dnshome.de/dns-query#德国\"\n  - \"https://dns.csa-rz.de/dns-query#德国\"\n  - \"https://dns.datenquark.de/dns-query#德国\"\n  - \"https://doh-de.blahdns.com/dns-query#德国\" \n  - \"https://dns.telekom.de/dns-query#德国\"\n  - \"https://dns.csaonline.de/dns-query#德国\"  \nfr_dns: &fr_dns\n  - \"https://ns0.fdn.fr/dns-query#法国\"\n  - \"https://qlf-doh.inria.fr/dns-query#法国\"\n  - \"https://dns.k3nny.fr/dns-query#法国\"\n  - \"https://doh.ffmuc.net/dns-query#法国\" \njp_dns: &jp_dns\n  - \"https://public.dns.iij.jp/dns-query#日本\"\n  - \"https://dns.google/dns-query#日本\"\nhk_dns: &hk_dns\n  - \"https://dns.cloudflare.com/dns-query#香港\"\n  - \"https://doh.opendns.com/dns-query#香港\"\n  - \"https://dns.w3ctag.org/dns-query#香港\"\n  - \"https://dns.google/dns-query#香港\"\nmo_dns: &mo_dns\n  - \"https://dns.cloudflare.com/dns-query#澳门\"\n  - \"https://doh.opendns.com/dns-query#澳门\"\n  - \"https://dns.w3ctag.org/dns-query#澳门\"\n  - \"https://dns.google/dns-query#澳门\"\ntw_dns: &tw_dns\n  - \"https://dns.cloudflare.com/dns-query#台湾\"\n  - \"https://doh.opendns.com/dns-query#台湾\"\n  - \"https://dns.w3ctag.org/dns-query#台湾\"\n  - \"https://dns.google/dns-query#台湾\"\nsg_dns: &sg_dns\n  - \"https://dns.cloudflare.com/dns-query#新加坡\"\n  - \"https://doh.opendns.com/dns-query#新加坡\"\n  - \"https://dns.w3ctag.org/dns-query#新加坡\"\n  - \"https://dns.google/dns-query#新加坡\"\nru_dns: &ru_dns\n  - \"https://dns.ch295.ru/dns-query#俄国\"\n  - \"https://dns.yandex.com/dns-query#俄国\"\n  - \"https://unfiltered.adguard-dns.com/dns-query#俄国\"\nin_dns: &in_dns\n  - \"https://dns.gutwe.in/dns-query#印度\"\n  - \"https://dns.brahma.world/dns-query#印度\"\nbr_dns: &br_dns\n  - \"https://adguard.frutuozo.com.br/dns-query#巴西\"\n  - \"https://dns.google/dns-query#巴西\"\nca_dns: &ca_dns\n  - \"https://dns1.dnscrypt.ca/dns-query#加拿大\"\n  - \"https://dns.cloudflare.com/dns-query#加拿大\"\nau_dns: &au_dns\n  - \"https://dns.netraptor.com.au/dns-query#澳大利亚\"\n  - \"https://dns.quad9.net/dns-query#澳大利亚\"\nit_dns: &it_dns\n  - \"https://doh.libredns.gr/dns-query#意大利\"\nnl_dns: &nl_dns\n  - \"https://doh.nl.ahadns.net/dns-query#荷兰\"\n  - \"https://dns.melvin2204.nl/dns-query#荷兰\"\n  - \"https://dns.quad9.net/dns-query#荷兰\"\nse_dns: &se_dns\n  - \"https://dns.mullvad.net/dns-query#瑞典\"\n  - \"https://resolver.sunet.se/dns-query#瑞典\"\n  - \"https://dns.haka.se/dns-query#瑞典\"\nch_dns: &ch_dns\n  - \"https://dns10.quad9.net/dns-query#瑞士\"\n  - \"https://doh.immerda.ch/dns-query#瑞士\"\n  - \"https://c.cicitt.ch/dns-query#瑞士\"\n  - \"https://dns.digitale-gesellschaft.ch/dns-query#瑞士\"\n  - \"https://doh.li/dns-query#瑞士\"\n\ndns:\n  enable: true\n  listen: *DNSSocket\n  ipv6: true\n  enhanced-mode: redir-host\n  default-nameserver: # proxy-server-nameserver,nameserver-policy,nameserver、fallback域名的解析\n    - 223.5.5.5#DNSDNS\n    - 114.114.114.114#DNSDNS\n    - 8.8.8.8#DNSDNS\n    - https://120.53.53.53/dns-query#DNSDNS\n    - https://223.5.5.5/dns-query#DNSDNS\n    - https://1.12.12.12/dns-query#DNSDNS\n    - system\n\n  proxy-server-nameserver: # 节点域名的解析\n    - https://120.53.53.53/dns-query#节点直连DNS\n    - https://223.5.5.5/dns-query#节点直连DNS\n    - https://1.1.1.1/dns-query#节点直连DNS\n    - https://dns.google/dns-query#节点直连DNS\n    - https://1.1.1.1/dns-query#节点国际DNS\n    - https://dns.google/dns-query#节点国际DNS\n\n  prefer-h3: false\n\n  direct-nameserver-follow-policy: false\n  direct-nameserver: # [动态回环出口:direct,中国:direct出站]时\n    *direct_dns\n\n  respect-rules: true # [中国非direct,其他地区,不出站]时，依据[nameserver-policy,nameserver、fallback]分类，使用不同dns\n  nameserver-policy: \n    \"rule-set:loopback_classical\": *direct_dns #动态回环出口\n    \"rule-set:firewall_classical\": rcode://success #个人文件\n    \"rule-set:international_classical\": *international_dns #个人文件\n    \"rule-set:domestic_classical\": *cn_dns #个人文件\n    \"rule-set:category-ads-all_classical\": rcode://success #广告拦截\n    \"rule-set:download_domain,bing_domain,openai_domain,github_domain,twitter_domain,instagram_domain,facebook_domain,youtube_domain,netflix_domain,spotify_domain,apple_domain,adobe_domain,telegram_domain,discord_domain,reddit_domain,biliintl_domain,bahamut_domain,ehentai_domain,pixiv_domain,steam_domain,epic_domain,microsoft_domain,google_domain\":\n      *international_dns\n  #中国\n    \"+.cn\": *cn_dns\n  #美国\n    \"+.us\": *us_dns\n  #英国\n    \"+.uk\": *uk_dns\n  #德国\n    \"+.de,+.eu\": *de_dns\n  #法国\n    \"+.fr\": *fr_dns\n  #日本\n    \"+.jp,+.nico\": *jp_dns\n  #香港\n    \"+.hk\": *hk_dns\n  #澳门\n    \"+.mo\": *mo_dns\n  #台湾\n    \"+.tw\": *tw_dns\n  #新加坡\n    \"+.sg\": *sg_dns\n  #俄罗斯\n    \"+.ru\": *ru_dns\n  #印度\n    \"+.in\": *in_dns\n  #巴西\n    \"+.br\": *br_dns\n  #加拿大\n    \"+.ca\": *ca_dns\n  #澳大利亚\n    \"+.au\": *au_dns\n  #意大利\n    \"+.it\": *it_dns\n  #荷兰\n    \"+.nl\": *nl_dns\n  #瑞典\n    \"+.se\": *se_dns\n  #瑞士\n    \"+.ch\": *ch_dns\n  #国际\n    \"rule-set:geolocation-!cn,tld-!cn\": *international_dns\n    \"rule-set:cn_domain,private_domain\": *cn_dns\n            \"#,\n        )\n        .unwrap();\n        original.apply_merge().unwrap();\n        target.apply_merge().unwrap();\n        correct_original_mapping_order(&mut target, &original);\n        let mut expected = serde_yaml::from_str::<Value>(\n            r#\"######### 锚点 start #######\nTroxyInPort: &TroxyInPort 65535\nShareInPort: &ShareInPort 65534\n# TailscaleOutPort: &TailscaleOutPort 65528\nReqableOutPort: &ReqableOutPort 9000\nDNSSocket: &DNSSocket 127.0.0.1:65533\nUISocket: &UISocket 127.0.0.1:65532\ndirect_dns: &direct_dns\n  - 114.114.114.114#直连DNS\n  - https://doh.pub/dns-query#直连DNS\n  - https://dns.alidns.com/dns-query#直连DNS\n  - system\ncn_dns: &cn_dns\n  - 114.114.114.114#中国DNS\n  - https://doh.pub/dns-query#中国DNS\n  - https://dns.alidns.com/dns-query#中国DNS\n  - system\ninternational_dns: &international_dns\n  - \"https://dns.cloudflare.com/dns-query#国际DNS\"\n  - \"https://doh.opendns.com/dns-query#国际DNS\"\n  - \"https://dns.w3ctag.org/dns-query#国际DNS\"\n  - \"https://dns.google/dns-query#国际DNS\"\nus_dns: &us_dns\n  - \"https://dns.koala.us.to/dns-query#美国\"\n  - \"https://dns.dns-53.us/dns-query#美国\" \n  - \"https://cloudflare-dns.com/dns-query#美国\"\n  - \"https://doh.opendns.com/dns-query#美国\"\n  - \"https://dns.google/dns-query#美国\"\nuk_dns: &uk_dns\n  - \"https://dns.aa.net.uk/dns-query#英国\"\n  - \"https://princez.uk/dns-query#英国\" \n  - \"https://dns.dns-53.uk/dns-query#英国\"\nde_dns: &de_dns\n  - \"https://doh.ffmuc.net/dns-query#德国\"\n  - \"https://dns.dnshome.de/dns-query#德国\"\n  - \"https://dnsforge.de/dns-query#德国\"\n  - \"https://bahopir188.dnshome.de/dns-query#德国\"\n  - \"https://dns.csa-rz.de/dns-query#德国\"\n  - \"https://dns.datenquark.de/dns-query#德国\"\n  - \"https://doh-de.blahdns.com/dns-query#德国\" \n  - \"https://dns.telekom.de/dns-query#德国\"\n  - \"https://dns.csaonline.de/dns-query#德国\"  \nfr_dns: &fr_dns\n  - \"https://ns0.fdn.fr/dns-query#法国\"\n  - \"https://qlf-doh.inria.fr/dns-query#法国\"\n  - \"https://dns.k3nny.fr/dns-query#法国\"\n  - \"https://doh.ffmuc.net/dns-query#法国\" \njp_dns: &jp_dns\n  - \"https://public.dns.iij.jp/dns-query#日本\"\n  - \"https://dns.google/dns-query#日本\"\nhk_dns: &hk_dns\n  - \"https://dns.cloudflare.com/dns-query#香港\"\n  - \"https://doh.opendns.com/dns-query#香港\"\n  - \"https://dns.w3ctag.org/dns-query#香港\"\n  - \"https://dns.google/dns-query#香港\"\nmo_dns: &mo_dns\n  - \"https://dns.cloudflare.com/dns-query#澳门\"\n  - \"https://doh.opendns.com/dns-query#澳门\"\n  - \"https://dns.w3ctag.org/dns-query#澳门\"\n  - \"https://dns.google/dns-query#澳门\"\ntw_dns: &tw_dns\n  - \"https://dns.cloudflare.com/dns-query#台湾\"\n  - \"https://doh.opendns.com/dns-query#台湾\"\n  - \"https://dns.w3ctag.org/dns-query#台湾\"\n  - \"https://dns.google/dns-query#台湾\"\nsg_dns: &sg_dns\n  - \"https://dns.cloudflare.com/dns-query#新加坡\"\n  - \"https://doh.opendns.com/dns-query#新加坡\"\n  - \"https://dns.w3ctag.org/dns-query#新加坡\"\n  - \"https://dns.google/dns-query#新加坡\"\nru_dns: &ru_dns\n  - \"https://dns.ch295.ru/dns-query#俄国\"\n  - \"https://dns.yandex.com/dns-query#俄国\"\n  - \"https://unfiltered.adguard-dns.com/dns-query#俄国\"\nin_dns: &in_dns\n  - \"https://dns.gutwe.in/dns-query#印度\"\n  - \"https://dns.brahma.world/dns-query#印度\"\nbr_dns: &br_dns\n  - \"https://adguard.frutuozo.com.br/dns-query#巴西\"\n  - \"https://dns.google/dns-query#巴西\"\nca_dns: &ca_dns\n  - \"https://dns1.dnscrypt.ca/dns-query#加拿大\"\n  - \"https://dns.cloudflare.com/dns-query#加拿大\"\nau_dns: &au_dns\n  - \"https://dns.netraptor.com.au/dns-query#澳大利亚\"\n  - \"https://dns.quad9.net/dns-query#澳大利亚\"\nit_dns: &it_dns\n  - \"https://doh.libredns.gr/dns-query#意大利\"\nnl_dns: &nl_dns\n  - \"https://doh.nl.ahadns.net/dns-query#荷兰\"\n  - \"https://dns.melvin2204.nl/dns-query#荷兰\"\n  - \"https://dns.quad9.net/dns-query#荷兰\"\nse_dns: &se_dns\n  - \"https://dns.mullvad.net/dns-query#瑞典\"\n  - \"https://resolver.sunet.se/dns-query#瑞典\"\n  - \"https://dns.haka.se/dns-query#瑞典\"\nch_dns: &ch_dns\n  - \"https://dns10.quad9.net/dns-query#瑞士\"\n  - \"https://doh.immerda.ch/dns-query#瑞士\"\n  - \"https://c.cicitt.ch/dns-query#瑞士\"\n  - \"https://dns.digitale-gesellschaft.ch/dns-query#瑞士\"\n  - \"https://doh.li/dns-query#瑞士\"\n\ndns:\n  enable: true\n  listen: *DNSSocket\n  ipv6: true\n  enhanced-mode: redir-host\n  default-nameserver: # proxy-server-nameserver,nameserver-policy,nameserver、fallback域名的解析\n    - 223.5.5.5#DNSDNS\n    - 114.114.114.114#DNSDNS\n    - 8.8.8.8#DNSDNS\n    - https://120.53.53.53/dns-query#DNSDNS\n    - https://223.5.5.5/dns-query#DNSDNS\n    - https://1.12.12.12/dns-query#DNSDNS\n    - system\n\n  proxy-server-nameserver: # 节点域名的解析\n    - https://120.53.53.53/dns-query#节点直连DNS\n    - https://223.5.5.5/dns-query#节点直连DNS\n    - https://1.1.1.1/dns-query#节点直连DNS\n    - https://dns.google/dns-query#节点直连DNS\n    - https://1.1.1.1/dns-query#节点国际DNS\n    - https://dns.google/dns-query#节点国际DNS\n\n  prefer-h3: false\n\n  direct-nameserver-follow-policy: false\n  direct-nameserver: # [动态回环出口:direct,中国:direct出站]时\n    *direct_dns\n\n  respect-rules: true # [中国非direct,其他地区,不出站]时，依据[nameserver-policy,nameserver、fallback]分类，使用不同dns\n  nameserver-policy: \n    \"rule-set:loopback_classical\": *direct_dns #动态回环出口\n    \"rule-set:firewall_classical\": rcode://success #个人文件\n    \"rule-set:international_classical\": *international_dns #个人文件\n    \"rule-set:domestic_classical\": *cn_dns #个人文件\n    \"rule-set:category-ads-all_classical\": rcode://success #广告拦截\n    \"rule-set:download_domain,bing_domain,openai_domain,github_domain,twitter_domain,instagram_domain,facebook_domain,youtube_domain,netflix_domain,spotify_domain,apple_domain,adobe_domain,telegram_domain,discord_domain,reddit_domain,biliintl_domain,bahamut_domain,ehentai_domain,pixiv_domain,steam_domain,epic_domain,microsoft_domain,google_domain\":\n      *international_dns\n  #中国\n    \"+.cn\": *cn_dns\n  #美国\n    \"+.us\": *us_dns\n  #英国\n    \"+.uk\": *uk_dns\n  #德国\n    \"+.de,+.eu\": *de_dns\n  #法国\n    \"+.fr\": *fr_dns\n  #日本\n    \"+.jp,+.nico\": *jp_dns\n  #香港\n    \"+.hk\": *hk_dns\n  #澳门\n    \"+.mo\": *mo_dns\n  #台湾\n    \"+.tw\": *tw_dns\n  #新加坡\n    \"+.sg\": *sg_dns\n  #俄罗斯\n    \"+.ru\": *ru_dns\n  #印度\n    \"+.in\": *in_dns\n  #巴西\n    \"+.br\": *br_dns\n  #加拿大\n    \"+.ca\": *ca_dns\n  #澳大利亚\n    \"+.au\": *au_dns\n  #意大利\n    \"+.it\": *it_dns\n  #荷兰\n    \"+.nl\": *nl_dns\n  #瑞典\n    \"+.se\": *se_dns\n  #瑞士\n    \"+.ch\": *ch_dns\n  #国际\n    \"rule-set:geolocation-!cn,tld-!cn\": *international_dns\n    \"rule-set:cn_domain,private_domain\": *cn_dns\n            \"#,\n        )\n        .unwrap();\n        expected.apply_merge().unwrap();\n        assert_eq!(expected, target);\n    }\n}\n"
  },
  {
    "path": "backend/tauri/src/enhance/script/mod.rs",
    "content": "mod js;\nmod lua;\npub use lua::create_lua_context;\npub mod runner;\npub use runner::RunnerManager;\n// TODO: add test\n// pub fn use_script(\n//     script: ScriptWrapper,\n//     config: Mapping,\n// ) -> Result<(Mapping, Vec<(String, String)>)> {\n//     match script.0 {\n//         ScriptType::JavaScript => {\n\n//         },\n//         _ => unimplemented!(\"unsupported script type\"),\n//     }\n// }\n\n// #[test]\n// fn test_script() {\n//     let script = r#\"\n//     function main(config) {\n//       if (Array.isArray(config.rules)) {\n//         config.rules = [...config.rules, \"add\"];\n//       }\n//       console.log(config);\n//       config.proxies = [\"111\"];\n//       return config;\n//     }\n//   \"#;\n\n//     let config = r#\"\n//     rules:\n//       - 111\n//       - 222\n//     tun:\n//       enable: false\n//     dns:\n//       enable: false\n//   \"#;\n\n//     let config = serde_yaml::from_str(config).unwrap();\n//     let (config, results) = process_js(script.into(), config).unwrap();\n\n//     let config_str = serde_yaml::to_string(&config).unwrap();\n\n//     println!(\"{config_str}\");\n\n//     dbg!(results);\n// }\n"
  },
  {
    "path": "backend/tauri/src/enhance/script/runner.rs",
    "content": "use anyhow::Error;\nuse async_trait::async_trait;\nuse serde_yaml::Mapping;\nuse std::collections::HashMap;\n\nuse super::{js, lua};\nuse crate::enhance::{Logs, ScriptType, ScriptWrapper};\n\n/// The output of the process function is a tuple of the mapping and the logs.\n/// Although the process fails, the logs should be returned.\npub type ProcessOutput = (Result<Mapping, anyhow::Error>, Logs);\n\n/// warp a result and return the ProcessOutput\nmacro_rules! wrap_result {\n    ($result:expr) => {\n        match $result {\n            Ok(inner) => inner,\n            Err(e) => return (Err(e.into()), Vec::new()),\n        }\n    };\n\n    ($result:expr, $logs:expr) => {\n        match $result {\n            Ok(inner) => inner,\n            Err(e) => return (Err(e.into()), $logs),\n        }\n    };\n}\n\npub(super) use wrap_result;\n\n#[async_trait]\npub trait Runner: Send + Sync {\n    fn try_new() -> Result<Self, Error>\n    where\n        Self: std::marker::Sized;\n    #[allow(dead_code)]\n    /// Process profiles by script file path\n    async fn process(&self, mapping: Mapping, path: &str) -> ProcessOutput;\n\n    /// Honey replacement - use in memory code str to load module and exec it!\n    /// It might not be implemented - due to some embeded engine is not support.\n    async fn process_honey(&self, mapping: Mapping, script: &str) -> ProcessOutput {\n        tracing::debug!(\"mapping: {:?}\\nscript:{}\", mapping, script);\n        unimplemented!()\n    }\n}\n\npub struct RunnerManager {\n    runners: HashMap<ScriptType, Box<dyn Runner>>,\n}\n\nimpl RunnerManager {\n    pub fn new() -> Self {\n        Self {\n            runners: HashMap::new(),\n        }\n    }\n    // If the script runner is not exist, it should be created.\n    pub fn get_or_init_runner(&mut self, script_type: &ScriptType) -> anyhow::Result<&dyn Runner> {\n        if !self.runners.contains_key(script_type) {\n            let runner = match script_type {\n                ScriptType::JavaScript => Box::new(js::JSRunner::try_new()?) as Box<dyn Runner>,\n                ScriptType::Lua => Box::new(lua::LuaRunner::try_new()?) as Box<dyn Runner>,\n            };\n            self.runners.insert(script_type.clone(), runner);\n        }\n        Ok(self.runners.get(script_type).unwrap().as_ref())\n    }\n\n    pub async fn process_script(\n        &mut self,\n        script: &ScriptWrapper,\n        config: Mapping,\n    ) -> ProcessOutput {\n        let runner = wrap_result!(self.get_or_init_runner(&script.0));\n        tracing::debug!(\"script: {:?}\", script);\n        runner.process_honey(config, script.1.as_str()).await\n    }\n}\n"
  },
  {
    "path": "backend/tauri/src/enhance/tun.rs",
    "content": "use serde_yaml::{Mapping, Value};\n\nuse crate::config::{\n    Config,\n    nyanpasu::{ClashCore, TunStack},\n};\n\nmacro_rules! revise {\n    ($map: expr, $key: expr, $val: expr) => {\n        let ret_key = Value::String($key.into());\n        $map.insert(ret_key, Value::from($val));\n    };\n}\n\n// if key not exists then append value\nmacro_rules! append {\n    ($map: expr, $key: expr, $val: expr) => {\n        let ret_key = Value::String($key.into());\n        if !$map.contains_key(&ret_key) {\n            $map.insert(ret_key, Value::from($val));\n        }\n    };\n}\n\n#[tracing_attributes::instrument(skip(config))]\npub fn use_tun(mut config: Mapping, enable: bool) -> Mapping {\n    let tun_key = Value::from(\"tun\");\n    let tun_val = config.get(&tun_key);\n    tracing::debug!(\"tun_val: {:?}\", tun_val);\n    if !enable && tun_val.is_none() {\n        return config;\n    }\n\n    let mut tun_val = tun_val.map_or(Mapping::new(), |val| {\n        val.as_mapping().cloned().unwrap_or(Mapping::new())\n    });\n\n    revise!(tun_val, \"enable\", enable);\n    if enable {\n        let core = {\n            *Config::verge()\n                .latest()\n                .clash_core\n                .as_ref()\n                .unwrap_or(&ClashCore::default())\n        };\n        if core == ClashCore::ClashRs {\n            append!(tun_val, \"device-id\", \"dev://utun1989\");\n            append!(tun_val, \"auto-route\", true);\n        } else {\n            let mut tun_stack = {\n                *Config::verge()\n                    .latest()\n                    .tun_stack\n                    .as_ref()\n                    .unwrap_or(&TunStack::default())\n            };\n            if core == ClashCore::ClashPremium && tun_stack == TunStack::Mixed {\n                tun_stack = TunStack::Gvisor;\n            }\n            append!(tun_val, \"stack\", AsRef::<str>::as_ref(&tun_stack));\n            append!(tun_val, \"dns-hijack\", vec![\"any:53\"]);\n            append!(tun_val, \"auto-route\", true);\n            append!(tun_val, \"auto-detect-interface\", true);\n        }\n    }\n\n    revise!(config, \"tun\", tun_val);\n\n    if enable {\n        use_dns_for_tun(config)\n    } else {\n        config\n    }\n}\n\nfn use_dns_for_tun(mut config: Mapping) -> Mapping {\n    let dns_key = Value::from(\"dns\");\n    let dns_val = config.get(&dns_key);\n\n    let mut dns_val = dns_val.map_or(Mapping::new(), |val| {\n        val.as_mapping().cloned().unwrap_or(Mapping::new())\n    });\n\n    // 开启tun将同时开启dns\n    revise!(dns_val, \"enable\", true);\n\n    append!(dns_val, \"enhanced-mode\", \"fake-ip\");\n    append!(dns_val, \"fake-ip-range\", \"198.18.0.1/16\");\n    append!(\n        dns_val,\n        \"nameserver\",\n        vec![\"114.114.114.114\", \"223.5.5.5\", \"8.8.8.8\"]\n    );\n    append!(dns_val, \"fallback\", vec![] as Vec<&str>);\n\n    #[cfg(target_os = \"windows\")]\n    append!(\n        dns_val,\n        \"fake-ip-filter\",\n        vec![\n            \"dns.msftncsi.com\",\n            \"www.msftncsi.com\",\n            \"www.msftconnecttest.com\"\n        ]\n    );\n    revise!(config, \"dns\", dns_val);\n    config\n}\n"
  },
  {
    "path": "backend/tauri/src/enhance/utils.rs",
    "content": "use indexmap::IndexMap;\nuse serde::{Deserialize, Serialize};\nuse serde_yaml::Mapping;\n\nuse crate::config::profile::{item_type::ProfileUid, profiles::Profiles};\n\nuse super::{ChainItem, ChainTypeWrapper, RunnerManager, use_merge};\nuse parking_lot::Mutex;\nuse std::{borrow::Borrow, sync::Arc};\n\npub fn convert_uids_to_scripts(profiles: &Profiles, uids: &[ProfileUid]) -> Vec<ChainItem> {\n    uids.iter()\n        .filter_map(|uid| profiles.get_item(uid).ok())\n        .filter_map(<Option<ChainItem>>::from)\n        .collect::<Vec<ChainItem>>()\n}\n\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, specta::Type)]\n#[serde(rename_all = \"lowercase\")]\npub enum LogSpan {\n    Log,\n    Info,\n    Warn,\n    Error,\n}\n\nimpl AsRef<str> for LogSpan {\n    fn as_ref(&self) -> &str {\n        match self {\n            LogSpan::Log => \"log\",\n            LogSpan::Info => \"info\",\n            LogSpan::Warn => \"warn\",\n            LogSpan::Error => \"error\",\n        }\n    }\n}\n\npub type Logs = Vec<(LogSpan, String)>;\npub trait LogsExt {\n    fn span<T: AsRef<str>>(&mut self, span: LogSpan, msg: T);\n    fn log<T: AsRef<str>>(&mut self, msg: T);\n    fn info<T: AsRef<str>>(&mut self, msg: T);\n    fn warn<T: AsRef<str>>(&mut self, msg: T);\n    fn error<T: AsRef<str>>(&mut self, msg: T);\n}\nimpl LogsExt for Logs {\n    fn span<T: AsRef<str>>(&mut self, span: LogSpan, msg: T) {\n        self.push((span, msg.as_ref().to_string()));\n    }\n    fn log<T: AsRef<str>>(&mut self, msg: T) {\n        self.span(LogSpan::Log, msg);\n    }\n    fn info<T: AsRef<str>>(&mut self, msg: T) {\n        self.span(LogSpan::Info, msg);\n    }\n    fn warn<T: AsRef<str>>(&mut self, msg: T) {\n        self.span(LogSpan::Warn, msg);\n    }\n    fn error<T: AsRef<str>>(&mut self, msg: T) {\n        self.span(LogSpan::Error, msg);\n    }\n}\n\npub fn take_logs(logs: Arc<Mutex<Option<Logs>>>) -> Logs {\n    logs.lock().take().unwrap()\n}\n\n/// 合并多个配置\n// TODO: 可能移动到其他地方\n// TODO: 增加自定义合并逻辑\n// TODO: 添加元信息\npub fn merge_profiles<T: Borrow<String>>(mappings: IndexMap<T, Mapping>) -> Mapping {\n    mappings\n        .into_iter()\n        .enumerate()\n        .fold(Mapping::new(), |mut acc, (idx, (_key, value))| {\n            // full extend the first one, others just extend proxies\n            // TODO: custom merge logic\n            // TODO: add meta info\n            if idx == 0 {\n                acc.extend(value);\n            } else {\n                let proxies = value.get(\"proxies\").unwrap().as_sequence().unwrap().clone();\n                let acc_proxies = acc.get_mut(\"proxies\").unwrap().as_sequence_mut().unwrap();\n                acc_proxies.extend(proxies);\n            }\n            acc\n        })\n}\n\n/// 处理链\npub async fn process_chain(\n    mut config: Mapping,\n    nodes: &[ChainItem],\n) -> (Mapping, IndexMap<ProfileUid, Logs>) {\n    let mut result_map = IndexMap::new();\n\n    let mut script_runner = RunnerManager::new();\n    for item in nodes.iter() {\n        match &item.data {\n            ChainTypeWrapper::Merge(merge) => {\n                let mut logs = vec![];\n                let (res, process_logs) = use_merge(merge, config.clone());\n                config = res.unwrap();\n                logs.extend(process_logs);\n                result_map.insert(item.uid.to_string(), logs);\n            }\n            ChainTypeWrapper::Script(script) => {\n                let mut logs = vec![];\n                let (res, process_logs) =\n                    script_runner.process_script(script, config.clone()).await;\n                logs.extend(process_logs);\n                // TODO: 修改日记 level 格式？\n                match res {\n                    Ok(res_config) => {\n                        config = res_config;\n                    }\n                    Err(err) => logs.error(err.to_string()),\n                }\n                // TODO: 这里添加对 field 的检查，触发 WARN 日记。此外，需要对 Merge 的结果进行检查？\n                result_map.insert(item.uid.to_string(), logs);\n            }\n        }\n    }\n\n    (config, result_map)\n}\n\n#[cfg(test)]\nmod tests {\n    use crate::enhance::chain::ChainTypeWrapper;\n\n    use super::*;\n    use serde_yaml::Value;\n\n    #[tokio::test]\n    async fn test_process_chain_order() {\n        // 准备初始配置\n        let mut initial_config = Mapping::new();\n        initial_config.insert(\n            Value::String(\"value\".to_string()),\n            Value::String(\"initial\".to_string()),\n        );\n\n        // 创建两个 ChainItem\n        let item_a = ChainItem {\n            uid: \"a\".to_string(),\n            data: ChainTypeWrapper::new_js(\n                \"function main(cfg) { cfg.value = 'a'; return cfg; }\".to_string(),\n            ),\n        };\n\n        let item_b = ChainItem {\n            uid: \"b\".to_string(),\n            data: ChainTypeWrapper::new_js(\n                \"function main(cfg) { cfg.value = cfg.value + '_b'; return cfg; }\".to_string(),\n            ),\n        };\n\n        let chain = vec![item_a, item_b];\n\n        // 执行处理链\n        let (final_config, logs) = process_chain(initial_config, &chain).await;\n\n        // 验证最终结果\n        assert_eq!(\n            final_config.get(\"value\").unwrap().as_str().unwrap(),\n            \"a_b\",\n            \"链式处理应该按顺序执行：A 将值设为 'a'，然后 B 将 'a' 修改为 'a_b'\"\n        );\n\n        // 验证日志存在\n        assert!(logs.contains_key(\"a\"), \"应该包含 A 的处理日志\");\n        assert!(logs.contains_key(\"b\"), \"应该包含 B 的处理日志\");\n    }\n}\n"
  },
  {
    "path": "backend/tauri/src/event_handler/mod.rs",
    "content": "/// This module is a tauri event based handler.\n/// Some state is good to be managed by the Tauri Manager. we should not hold the singletons in the global state in some cases.\nuse tauri::{Emitter, Listener, Manager, Runtime};\n\nmod widget;\n\npub fn mount_handlers<M, R>(_app: &mut M)\nwhere\n    M: Manager<R> + Listener<R> + Emitter<R>,\n    R: Runtime,\n{\n}\n"
  },
  {
    "path": "backend/tauri/src/event_handler/widget.rs",
    "content": "use tauri::{AppHandle, Event, Runtime};\n\npub enum WidgetInstance {\n    Small(nyanpasu_egui::widget::NyanpasuNetworkStatisticSmallWidget),\n    Large(nyanpasu_egui::widget::NyanpasuNetworkStatisticLargeWidget),\n}\n\n#[tracing::instrument(skip(_app_handle))]\npub(super) fn on_network_statistic_config_changed<R: Runtime>(\n    _app_handle: &AppHandle<R>,\n    event: Event,\n) -> anyhow::Result<()> {\n    // let config: NetworkStatisticWidgetConfig =\n    //     serde_json::from_str(event.payload()).context(\"failed to deserialize the new config\")?;\n    // match config {\n    //     NetworkStatisticWidgetConfig::Disabled => {\n    //         app_handle\n    //             .emit_all(\"network-statistic-widget:hide\")\n    //             .context(\"failed to emit the hide event\")?;\n    //     }\n    //     NetworkStatisticWidgetConfig::Large => {\n    //         app_handle\n    //             .emit_all(\"network-statistic-widget:show-large\")\n    //             .context(\"failed to emit the show-large event\")?;\n    //     }\n    //     NetworkStatisticWidgetConfig::Small => {\n    //         app_handle\n    //             .emit_all(\"network-statistic-widget:show-small\")\n    //             .context(\"failed to emit the show-small event\")?;\n    //     }\n    // }\n    Ok(())\n}\n"
  },
  {
    "path": "backend/tauri/src/feat.rs",
    "content": "//！\n//! feat mod 里的函数主要用于\n//! - hotkey 快捷键\n//! - timer 定时器\n//! - cmds 页面调用\n//!\nuse std::borrow::Borrow;\n\nuse crate::{\n    config::{\n        nyanpasu::NetworkStatisticWidgetConfig,\n        profile::{\n            builder::ProfileBuilder,\n            item::{\n                LocalProfileBuilder, MergeProfileBuilder, ProfileSharedBuilder,\n                ScriptProfileBuilder,\n            },\n        },\n        *,\n    },\n    core::{service::ipc::get_ipc_state, *},\n    log_err,\n    utils::{self, help::get_clash_external_port, resolve},\n};\nuse anyhow::{Result, bail};\nuse handle::Message;\nuse nyanpasu_ipc::api::status::CoreState;\nuse serde_yaml::{Mapping, Value};\nuse tauri::{AppHandle, Manager};\nuse tauri_plugin_clipboard_manager::ClipboardExt;\n\n// 打开面板\n#[allow(unused)]\npub fn open_dashboard() {\n    let handle = handle::Handle::global();\n    let app_handle = handle.app_handle.lock();\n    if let Some(app_handle) = app_handle.as_ref() {\n        resolve::create_window(app_handle);\n    }\n}\n\n// 关闭面板\n#[allow(unused)]\npub fn close_dashboard() {\n    let handle = handle::Handle::global();\n    let app_handle = handle.app_handle.lock();\n    if let Some(app_handle) = app_handle.as_ref() {\n        resolve::close_window(app_handle);\n    }\n}\n\n// 开关面板\npub fn toggle_dashboard() {\n    let handle = handle::Handle::global();\n    let app_handle = handle.app_handle.lock();\n    if let Some(app_handle) = app_handle.as_ref() {\n        match resolve::is_window_open(app_handle) {\n            true => resolve::close_window(app_handle),\n            false => resolve::create_window(app_handle),\n        }\n    }\n}\n\n// 重启clash\npub fn restart_clash_core() {\n    tauri::async_runtime::spawn(async {\n        match CoreManager::global().run_core().await {\n            Ok(_) => {\n                handle::Handle::refresh_clash();\n                handle::Handle::notice_message(&Message::SetConfig(Ok(())));\n            }\n            Err(err) => {\n                handle::Handle::notice_message(&Message::SetConfig(Err(format!(\"{err:?}\"))));\n                log::error!(target:\"app\", \"{err:?}\");\n            }\n        }\n    });\n}\n\n// 切换模式 rule/global/direct/script mode\npub fn change_clash_mode(mode: String) {\n    let mut mapping = Mapping::new();\n    mapping.insert(Value::from(\"mode\"), mode.clone().into());\n    let (tx, rx) = tokio::sync::oneshot::channel();\n    tauri::async_runtime::spawn(async move {\n        log::debug!(target: \"app\", \"change clash mode to {mode}\");\n\n        match clash::api::patch_configs(&mapping).await {\n            Ok(_) => {\n                // 更新配置\n                Config::clash().data().patch_config(mapping);\n\n                if Config::clash().data().save_config().is_ok() {\n                    handle::Handle::refresh_clash();\n                    log_err!(handle::Handle::update_systray_part());\n                }\n            }\n            Err(err) => log::error!(target: \"app\", \"{err:?}\"),\n        }\n        if tx.send(()).is_err() {\n            log::error!(target: \"app::change_clash_mode\", \"failed to send tx\");\n        }\n    });\n\n    // refresh proxies\n    update_proxies_buff(Some(rx));\n\n    // Interrupt connections based on configuration\n    tauri::async_runtime::spawn(async move {\n        let _ =\n            crate::core::connection_interruption::ConnectionInterruptionService::on_mode_change()\n                .await;\n    });\n}\n\n// 切换系统代理\npub fn toggle_system_proxy() {\n    let enable = Config::verge().draft().enable_system_proxy;\n    let enable = enable.unwrap_or(false);\n\n    tauri::async_runtime::spawn(async move {\n        match patch_verge(IVerge {\n            enable_system_proxy: Some(!enable),\n            ..IVerge::default()\n        })\n        .await\n        {\n            Ok(_) => handle::Handle::refresh_verge(),\n            Err(err) => log::error!(target: \"app\", \"{err:?}\"),\n        }\n    });\n}\n\n// 打开系统代理\npub fn enable_system_proxy() {\n    tauri::async_runtime::spawn(async {\n        match patch_verge(IVerge {\n            enable_system_proxy: Some(true),\n            ..IVerge::default()\n        })\n        .await\n        {\n            Ok(_) => handle::Handle::refresh_verge(),\n            Err(err) => log::error!(target: \"app\", \"{err:?}\"),\n        }\n    });\n}\n\n// 关闭系统代理\npub fn disable_system_proxy() {\n    tauri::async_runtime::spawn(async {\n        match patch_verge(IVerge {\n            enable_system_proxy: Some(false),\n            ..IVerge::default()\n        })\n        .await\n        {\n            Ok(_) => handle::Handle::refresh_verge(),\n            Err(err) => log::error!(target: \"app\", \"{err:?}\"),\n        }\n    });\n}\n\n// 切换tun模式\npub fn toggle_tun_mode() {\n    let enable = Config::verge().data().enable_tun_mode;\n    let enable = enable.unwrap_or(false);\n\n    tauri::async_runtime::spawn(async move {\n        match patch_verge(IVerge {\n            enable_tun_mode: Some(!enable),\n            ..IVerge::default()\n        })\n        .await\n        {\n            Ok(_) => handle::Handle::refresh_verge(),\n            Err(err) => log::error!(target: \"app\", \"{err:?}\"),\n        }\n    });\n}\n\n// 打开tun模式\npub fn enable_tun_mode() {\n    tauri::async_runtime::spawn(async {\n        match patch_verge(IVerge {\n            enable_tun_mode: Some(true),\n            ..IVerge::default()\n        })\n        .await\n        {\n            Ok(_) => handle::Handle::refresh_verge(),\n            Err(err) => log::error!(target: \"app\", \"{err:?}\"),\n        }\n    });\n}\n\n// 关闭tun模式\npub fn disable_tun_mode() {\n    tauri::async_runtime::spawn(async {\n        match patch_verge(IVerge {\n            enable_tun_mode: Some(false),\n            ..IVerge::default()\n        })\n        .await\n        {\n            Ok(_) => handle::Handle::refresh_verge(),\n            Err(err) => log::error!(target: \"app\", \"{err:?}\"),\n        }\n    });\n}\n\n/// 修改clash的配置\npub async fn patch_clash(patch: Mapping) -> Result<()> {\n    Config::clash().draft().patch_config(patch.clone());\n\n    let run = move || async move {\n        let mixed_port = patch.get(\"mixed-port\");\n        let enable_random_port = Config::verge().latest().enable_random_port.unwrap_or(false);\n        if mixed_port.is_some() && !enable_random_port {\n            let changed = mixed_port.unwrap()\n                != Config::verge()\n                    .latest()\n                    .verge_mixed_port\n                    .unwrap_or(Config::clash().data().get_mixed_port());\n            // 检查端口占用\n            if changed\n                && let Some(port) = mixed_port.unwrap().as_u64()\n                && !port_scanner::local_port_available(port as u16)\n            {\n                Config::clash().discard();\n                bail!(\"port already in use\");\n            }\n        };\n\n        // 检测 external-controller port 是否修改\n        if let Some(external_controller) = patch.get(\"external-controller\") {\n            let external_controller = external_controller.as_str().unwrap();\n            let changed = external_controller != Config::clash().data().get_client_info().server;\n            if changed {\n                let (_, port) = external_controller.split_once(':').unwrap();\n                let port = port.parse::<u16>()?;\n                let strategy = Config::verge()\n                    .latest()\n                    .get_external_controller_port_strategy();\n                let core_state = crate::core::CoreManager::global().status().await;\n                if matches!(core_state.0.as_ref(), &CoreState::Running)\n                    && get_clash_external_port(&strategy, port).is_err()\n                {\n                    Config::clash().discard();\n                    bail!(\"can not select fixed: current port is not available.\");\n                }\n            }\n        }\n\n        // 激活配置\n        if mixed_port.is_some()\n            || patch.get(\"secret\").is_some()\n            || patch.get(\"external-controller\").is_some()\n        {\n            Config::generate().await?;\n            CoreManager::global().run_core().await?;\n            handle::Handle::refresh_clash();\n        }\n\n        // 更新系统代理\n        if mixed_port.is_some() {\n            log_err!(sysopt::Sysopt::global().init_sysproxy());\n        }\n\n        if patch.get(\"mode\").is_some() {\n            crate::feat::update_proxies_buff(None);\n            log_err!(handle::Handle::update_systray_part());\n        }\n\n        Config::runtime().latest().patch_config(patch);\n\n        <Result<()>>::Ok(())\n    };\n    match run().await {\n        Ok(()) => {\n            Config::clash().apply();\n            Config::clash().data().save_config()?;\n            Ok(())\n        }\n        Err(err) => {\n            Config::clash().discard();\n            Err(err)\n        }\n    }\n}\n\n/// 修改verge的配置\n/// 一般都是一个个的修改\npub async fn patch_verge(patch: IVerge) -> Result<()> {\n    // Validate theme_color if it's being updated\n    if let Some(ref theme_color) = patch.theme_color {\n        if !theme_color.is_empty() && !crate::config::nyanpasu::is_hex_color(theme_color) {\n            anyhow::bail!(\"Invalid theme color: {}\", theme_color);\n        }\n    }\n\n    Config::verge().draft().patch_config(patch.clone());\n    let tun_mode = patch.enable_tun_mode;\n    let auto_launch = patch.enable_auto_launch;\n    let system_proxy = patch.enable_system_proxy;\n    let proxy_bypass = patch.system_proxy_bypass;\n    let language = patch.language;\n    let log_level = patch.app_log_level;\n    let log_max_files = patch.max_log_files;\n    let enable_tray_selector = patch.clash_tray_selector;\n    let enable_tray_text = patch.enable_tray_text;\n    let network_statistic_widget = patch.network_statistic_widget;\n    let res = || async move {\n        let service_mode = patch.enable_service_mode;\n        let ipc_state = get_ipc_state();\n        if service_mode.is_some() && ipc_state.is_connected() {\n            log::debug!(target: \"app\", \"change service mode to {}\", service_mode.unwrap());\n\n            Config::generate().await?;\n            CoreManager::global().run_core().await?;\n        }\n\n        if tun_mode.is_some() {\n            log::debug!(target: \"app\", \"toggle tun mode\");\n            #[allow(unused_mut)]\n            let mut flag = false;\n            #[cfg(any(target_os = \"macos\", target_os = \"linux\"))]\n            {\n                use crate::utils::dirs::check_core_permission;\n                let current_core = Config::verge().data().clash_core.unwrap_or_default();\n                let current_core: nyanpasu_utils::core::CoreType = (&current_core).into();\n                let service_state = crate::core::service::ipc::get_ipc_state();\n                if !service_state.is_connected() && check_core_permission(&current_core).inspect_err(|e| {\n                    log::error!(target: \"app\", \"clash core is not granted the necessary permissions, grant it: {e:?}\");\n                }).is_ok_and(|v| !v) {\n                    log::debug!(target: \"app\", \"grant core permission, and restart core\");\n                    flag = true;\n                }\n            }\n            let (state, _, _) = CoreManager::global().status().await;\n            if flag || matches!(state.as_ref(), CoreState::Stopped(_)) {\n                log::debug!(target: \"app\", \"core is stopped, restart core\");\n                Config::generate().await?;\n                CoreManager::global().run_core().await?;\n            } else {\n                log::debug!(target: \"app\", \"update core config\");\n                #[cfg(target_os = \"macos\")]\n                let _ = CoreManager::global()\n                    .change_default_network_dns(tun_mode.unwrap_or(false))\n                    .await\n                    .inspect_err(\n                        |e| log::error!(target: \"app\", \"failed to set system dns: {:?}\", e),\n                    );\n                update_core_config().await?;\n            }\n        }\n\n        if auto_launch.is_some() {\n            sysopt::Sysopt::global().update_launch()?;\n        }\n        if system_proxy.is_some() || proxy_bypass.is_some() {\n            sysopt::Sysopt::global().update_sysproxy()?;\n            sysopt::Sysopt::global().guard_proxy();\n        }\n\n        if let Some(true) = patch.enable_proxy_guard {\n            sysopt::Sysopt::global().guard_proxy();\n        }\n\n        if let Some(hotkeys) = patch.hotkeys {\n            hotkey::Hotkey::global().update(hotkeys)?;\n        }\n\n        if language.is_some() {\n            rust_i18n::set_locale(language.unwrap().as_str());\n            handle::Handle::update_systray()?;\n        } else if system_proxy.or(tun_mode).or(enable_tray_text).is_some() {\n            handle::Handle::update_systray_part()?;\n        }\n\n        if log_level.is_some() || log_max_files.is_some() {\n            utils::init::refresh_logger((log_level, log_max_files))?;\n        }\n\n        if enable_tray_selector.is_some() {\n            handle::Handle::update_systray()?;\n        }\n\n        // TODO: refactor config with changed notify\n        if let Some(network_statistic_widget) = network_statistic_widget {\n            let widget_manager =\n                crate::consts::app_handle().state::<crate::widget::WidgetManager>();\n            let is_running = widget_manager.is_running().await;\n            match network_statistic_widget {\n                NetworkStatisticWidgetConfig::Disabled => {\n                    if is_running {\n                        widget_manager.stop().await?;\n                    }\n                }\n                NetworkStatisticWidgetConfig::Enabled(variant) => {\n                    widget_manager.start(variant).await?;\n                }\n            }\n        }\n\n        <Result<()>>::Ok(())\n    };\n\n    match res().await {\n        Ok(()) => {\n            Config::verge().apply();\n            Config::verge().data().save_file()?;\n            Ok(())\n        }\n        Err(err) => {\n            Config::verge().discard();\n            Err(err)\n        }\n    }\n}\n\n/// 更新某个profile\n/// 如果更新当前配置就激活配置\npub async fn update_profile<T: Borrow<String>>(\n    uid: T,\n    opts: Option<RemoteProfileOptionsBuilder>,\n) -> Result<()> {\n    let uid = uid.borrow();\n    let profile_item = Config::profiles().latest().get_item(uid)?.clone();\n    let is_remote = profile_item.is_remote();\n\n    let should_update = if is_remote {\n        let mut item = profile_item.as_remote().unwrap().clone();\n\n        item.subscribe(opts).await?;\n        let committer = Config::profiles().auto_commit();\n        let mut profiles = committer.draft();\n        profiles.replace_item(uid, item.into())?;\n        profiles.get_current().contains(uid)\n    } else {\n        // For local profiles, we need to update the timestamp\n        let committer = Config::profiles().auto_commit();\n        let mut profiles = committer.draft();\n\n        // Create a builder to update the timestamp\n        match profile_item {\n            Profile::Local(_) => {\n                let mut shared_builder = ProfileSharedBuilder::default();\n                shared_builder.updated(chrono::Local::now().timestamp() as usize);\n                let mut builder = LocalProfileBuilder::default();\n                builder.shared(shared_builder);\n                profiles.patch_item(uid.to_string(), ProfileBuilder::Local(builder))?;\n            }\n            Profile::Merge(_) => {\n                let mut shared_builder = ProfileSharedBuilder::default();\n                shared_builder.updated(chrono::Local::now().timestamp() as usize);\n                let mut builder = MergeProfileBuilder::default();\n                builder.shared(shared_builder);\n                profiles.patch_item(uid.to_string(), ProfileBuilder::Merge(builder))?;\n            }\n            Profile::Script(_) => {\n                let mut shared_builder = ProfileSharedBuilder::default();\n                shared_builder.updated(chrono::Local::now().timestamp() as usize);\n                let mut builder = ScriptProfileBuilder::default();\n                builder.shared(shared_builder);\n                profiles.patch_item(uid.to_string(), ProfileBuilder::Script(builder))?;\n            }\n            _ => {}\n        }\n\n        profiles.get_current().contains(uid)\n    };\n\n    if should_update {\n        update_core_config().await?;\n    }\n\n    Ok(())\n}\n\n/// 更新配置\nasync fn update_core_config() -> Result<()> {\n    match CoreManager::global().update_config().await {\n        Ok(_) => {\n            handle::Handle::refresh_clash();\n            handle::Handle::notice_message(&Message::SetConfig(Ok(())));\n            Ok(())\n        }\n        Err(err) => {\n            handle::Handle::notice_message(&Message::SetConfig(Err(format!(\"{err:?}\"))));\n            Err(err)\n        }\n    }\n}\n\n/// copy env variable\npub fn copy_clash_env(app_handle: &AppHandle, option: &str) {\n    let port = { Config::verge().latest().verge_mixed_port.unwrap_or(7890) };\n    let http_proxy = format!(\"http://127.0.0.1:{port}\");\n    let socks5_proxy = format!(\"socks5://127.0.0.1:{port}\");\n\n    let sh =\n        format!(\"export https_proxy={http_proxy} http_proxy={http_proxy} all_proxy={socks5_proxy}\");\n    let cmd: String = format!(\"set http_proxy={http_proxy} \\n set https_proxy={http_proxy}\");\n    let ps: String = format!(\"$env:HTTP_PROXY=\\\"{http_proxy}\\\"; $env:HTTPS_PROXY=\\\"{http_proxy}\\\"\");\n\n    let clipboard = app_handle.clipboard();\n\n    match option {\n        \"sh\" => {\n            if let Err(e) = clipboard.write_text(sh) {\n                log::error!(target: \"app\", \"copy_clash_env failed: {e}\");\n            }\n        }\n        \"cmd\" => {\n            if let Err(e) = clipboard.write_text(cmd) {\n                log::error!(target: \"app\", \"copy_clash_env failed: {e}\");\n            }\n        }\n        \"ps\" => {\n            if let Err(e) = clipboard.write_text(ps) {\n                log::error!(target: \"app\", \"copy_clash_env failed: {e}\");\n            }\n        }\n        _ => log::error!(target: \"app\", \"copy_clash_env: Invalid option! {option}\"),\n    }\n}\n\npub fn update_proxies_buff(rx: Option<tokio::sync::oneshot::Receiver<()>>) {\n    use crate::core::clash::proxies::{ProxiesGuard, ProxiesGuardExt};\n\n    tauri::async_runtime::spawn(async move {\n        if let Some(rx) = rx\n            && let Err(e) = rx.await\n        {\n            log::error!(target: \"app::clash::proxies\", \"update proxies buff by rx failed: {e}\");\n        }\n        match ProxiesGuard::global().update().await {\n            Ok(_) => {\n                log::debug!(target: \"app::clash::proxies\", \"update proxies buff success\");\n            }\n            Err(e) => {\n                log::error!(target: \"app::clash::proxies\", \"update proxies buff failed: {e}\");\n            }\n        }\n    });\n}\n"
  },
  {
    "path": "backend/tauri/src/ipc.rs",
    "content": "use crate::{\n    config::{profile::ProfileBuilder, *},\n    core::{\n        logger::Logger, storage::Storage, tasks::jobs::ProfilesJobGuard,\n        updater::ManifestVersionLatest, *,\n    },\n    enhance::PostProcessingOutput,\n    feat,\n    utils::{candy, collect::EnvInfo, dirs, help, resolve},\n};\nuse anyhow::{Context, anyhow};\nuse chrono::Local;\nuse log::debug;\nuse nyanpasu_ipc::api::status::CoreState;\nuse profile::item_type::ProfileItemType;\nuse serde_yaml::Mapping;\nuse std::{borrow::Cow, collections::VecDeque, path::PathBuf, result::Result as StdResult};\nuse storage::{StorageOperationError, WebStorage};\nuse sysproxy::Sysproxy;\nuse tauri::{AppHandle, Manager};\nuse tray::icon::TrayIcon;\n\nuse tauri_plugin_dialog::{DialogExt, FileDialogBuilder};\n\n#[derive(Debug, thiserror::Error)]\npub enum IpcError {\n    #[error(transparent)]\n    Io(#[from] std::io::Error),\n    #[error(transparent)]\n    SerdeYaml(#[from] serde_yaml::Error),\n    #[error(transparent)]\n    SerdeJson(#[from] serde_json::Error),\n    #[error(transparent)]\n    Tauri(#[from] tauri::Error),\n    #[error(transparent)]\n    Storage(#[from] StorageOperationError),\n    #[error(transparent)]\n    Anyhow(#[from] anyhow::Error),\n    #[error(\"{0}\")]\n    Custom(String),\n}\n\nimpl From<String> for IpcError {\n    fn from(s: String) -> Self {\n        IpcError::Custom(s)\n    }\n}\n\nimpl serde::Serialize for IpcError {\n    fn serialize<S>(&self, serializer: S) -> StdResult<S::Ok, S::Error>\n    where\n        S: serde::ser::Serializer,\n    {\n        serializer.serialize_str(format!(\"{self:#?}\").as_str())\n    }\n}\n\nimpl specta::Type for IpcError {\n    fn inline(\n        type_map: &mut specta::TypeMap,\n        generics: specta::Generics,\n    ) -> specta::datatype::DataType {\n        specta::datatype::DataType::Primitive(specta::datatype::PrimitiveType::String)\n    }\n}\n\ntype Result<T = ()> = StdResult<T, IpcError>;\n\n// TODO: remove this struct use Sysproxy\n#[derive(specta::Type, serde::Serialize)]\npub struct GetSysProxyResponse {\n    // Sysproxy fields (manually defined),\n    // because specta not support serde(flatten)\n    pub enable: bool,\n    pub host: String,\n    pub port: u16,\n    pub bypass: String,\n\n    // old version compatible\n    pub server: String,\n}\n\n#[tauri::command]\n#[specta::specta]\npub fn get_profiles() -> Result<Profiles> {\n    Ok(Config::profiles().data().clone())\n}\n\n#[cfg(target_os = \"windows\")]\n#[tauri::command]\n#[specta::specta]\npub fn is_portable() -> Result<bool> {\n    Ok(crate::utils::dirs::get_portable_flag())\n}\n\n#[cfg(not(target_os = \"windows\"))]\n#[tauri::command]\n#[specta::specta]\npub fn is_portable() -> Result<bool> {\n    Ok(false)\n}\n\n#[tauri::command]\n#[specta::specta]\npub async fn enhance_profiles() -> Result {\n    CoreManager::global().update_config().await?;\n    handle::Handle::refresh_clash();\n    Ok(())\n}\n\n#[tauri::command]\n#[specta::specta]\npub async fn import_profile(url: String, option: Option<RemoteProfileOptionsBuilder>) -> Result {\n    let url = url::Url::parse(&url).context(\"failed to parse the url\")?;\n    let mut builder = crate::config::profile::item::RemoteProfileBuilder::default();\n    builder.url(url);\n    if let Some(option) = option {\n        builder.option(option.clone());\n    }\n    let profile = builder\n        .build_no_blocking()\n        .await\n        .context(\"failed to build a remote profile\")?;\n    // 根据是否为 Some(uid) 来判断是否要激活配置\n    let profile_id = {\n        if Config::profiles().draft().current.is_empty() {\n            Some(profile.uid().to_string())\n        } else {\n            None\n        }\n    };\n    {\n        let committer = Config::profiles().auto_commit();\n        (committer.draft().append_item(profile.into()))?;\n    }\n    // TODO: 使用 activate_profile 来激活配置\n    if let Some(profile_id) = profile_id {\n        let mut builder = ProfilesBuilder::default();\n        builder.current(vec![profile_id]);\n        patch_profiles_config(builder).await?;\n    }\n    Ok(())\n}\n\n/// create a new profile\n#[tauri::command]\n#[specta::specta]\npub async fn create_profile(item: ProfileBuilder, file_data: Option<String>) -> Result {\n    tracing::trace!(\"create profile: {item:?}\");\n\n    let is_remote = matches!(&item, ProfileBuilder::Remote(_));\n\n    let profile: Profile = match item {\n        ProfileBuilder::Local(builder) => builder\n            .build()\n            .context(\"failed to build local profile\")?\n            .into(),\n        ProfileBuilder::Remote(mut builder) => builder\n            .build_no_blocking()\n            .await\n            .context(\"failed to build remote profile\")?\n            .into(),\n        ProfileBuilder::Merge(builder) => builder\n            .build()\n            .context(\"failed to build merge profile\")?\n            .into(),\n        ProfileBuilder::Script(builder) => builder\n            .build()\n            .context(\"failed to build script profile\")?\n            .into(),\n    };\n\n    tracing::info!(\"created new profile: {:#?}\", profile);\n\n    // Save file data for non-remote profiles\n    if let Some(file_data) = file_data\n        && !file_data.is_empty()\n        && !is_remote\n    {\n        profile.save_file(file_data)?;\n    }\n\n    // 根据是否为 Some(uid) 来判断是否要激活配置\n    let profile_id = {\n        if (profile.is_local() || profile.is_remote())\n            && Config::profiles().draft().current.is_empty()\n        {\n            Some(profile.uid().to_string())\n        } else {\n            None\n        }\n    };\n\n    // Save the profile\n    {\n        let committer = Config::profiles().auto_commit();\n        committer.draft().append_item(profile)?;\n    };\n    // TODO: 使用 activate_profile 来激活配置\n    if let Some(profile_id) = profile_id {\n        let mut builder = ProfilesBuilder::default();\n        builder.current(vec![profile_id]);\n        patch_profiles_config(builder).await?;\n    }\n\n    Ok(())\n}\n\n#[tauri::command]\n#[specta::specta]\npub async fn reorder_profile(active_id: String, over_id: String) -> Result {\n    let committer = Config::profiles().auto_commit();\n    (committer.draft().reorder(active_id, over_id))?;\n    Ok(())\n}\n\n#[tauri::command]\n#[specta::specta]\npub fn reorder_profiles_by_list(list: Vec<String>) -> Result {\n    let committer = Config::profiles().auto_commit();\n    (committer.draft().reorder_by_list(&list))?;\n    Ok(())\n}\n\n#[tauri::command]\n#[specta::specta]\npub async fn update_profile(uid: String, option: Option<RemoteProfileOptionsBuilder>) -> Result {\n    (feat::update_profile(uid, option).await)?;\n    Ok(())\n}\n\n#[tauri::command]\n#[specta::specta]\npub async fn delete_profile(uid: String) -> Result {\n    let should_update = tokio::task::spawn_blocking(move || {\n        #[allow(clippy::let_and_return)] // a bug in clippy\n        nyanpasu_utils::runtime::block_on_current_thread(async move {\n            let committer = Config::profiles().auto_commit();\n            let x = committer.draft().delete_item(&uid).await;\n            x\n        })\n    })\n    .await\n    .context(\"failed to join the task\")?\n    .context(\"failed to delete the profile\")?;\n\n    if should_update {\n        (CoreManager::global().update_config().await)?;\n        handle::Handle::refresh_clash();\n    }\n    Ok(())\n}\n\n/// 修改profiles的\n#[tauri::command]\n#[specta::specta]\npub async fn patch_profiles_config(profiles: ProfilesBuilder) -> Result {\n    Config::profiles().draft().apply(profiles);\n\n    match CoreManager::global().update_config().await {\n        Ok(_) => {\n            handle::Handle::refresh_clash();\n            Config::profiles().apply();\n            (Config::profiles().data().save_file())?;\n\n            // Interrupt connections based on configuration\n            let _ = crate::core::connection_interruption::ConnectionInterruptionService::on_profile_change().await;\n\n            Ok(())\n        }\n        Err(err) => {\n            Config::profiles().discard();\n            log::error!(target: \"app\", \"{err:?}\");\n            Err(IpcError::from(err))\n        }\n    }\n}\n\n/// update profile by uid\n#[tauri::command]\n#[specta::specta]\npub async fn patch_profile(app_handle: AppHandle, uid: String, profile: ProfileBuilder) -> Result {\n    tracing::debug!(\"patch profile: {uid} with {profile:?}\");\n    {\n        let committer = Config::profiles().auto_commit();\n        (committer.draft().patch_item(uid.clone(), profile))?;\n    }\n    {\n        let profiles_jobs = app_handle.state::<ProfilesJobGuard>();\n        profiles_jobs.write().refresh();\n    }\n    let need_update = {\n        let profiles = Config::profiles();\n        let profiles = profiles.latest();\n        match (&profiles.chain, &profiles.current) {\n            (chains, _) if chains.contains(&uid) => true,\n            (_, current_chain) if current_chain.contains(&uid) => true,\n            (_, current_chain) => {\n                current_chain\n                    .iter()\n                    .any(|chain_uid| match profiles.get_item(chain_uid) {\n                        Ok(item) if item.is_local() => {\n                            item.as_local().unwrap().chain.contains(&uid)\n                        }\n                        Ok(item) if item.is_remote() => {\n                            item.as_remote().unwrap().chain.contains(&uid)\n                        }\n                        _ => false,\n                    })\n            }\n        }\n    };\n    if need_update {\n        match CoreManager::global().update_config().await {\n            Ok(_) => {\n                handle::Handle::refresh_clash();\n            }\n            Err(err) => {\n                log::error!(target: \"app\", \"{err:?}\");\n            }\n        }\n    }\n    Ok(())\n}\n\n#[tauri::command]\n#[specta::specta]\npub fn view_profile(app_handle: tauri::AppHandle, uid: String) -> Result {\n    let file = {\n        Config::profiles()\n            .latest()\n            .get_item(&uid)?\n            .file()\n            .to_string()\n    };\n\n    let path = (dirs::app_profiles_dir())?.join(file);\n    if !path.exists() {\n        return Err(anyhow!(\"file not exists: {:#?}\", path).into());\n    }\n\n    help::open_file(app_handle, path)?;\n    Ok(())\n}\n\n#[tauri::command]\n#[specta::specta]\npub fn read_profile_file(uid: String) -> Result<String> {\n    let profiles = Config::profiles();\n    let profiles = profiles.latest();\n    let item = (profiles.get_item(&uid))?;\n    let data = match item.kind() {\n        ProfileItemType::Local | ProfileItemType::Remote => {\n            let raw = (item.read_file())?;\n            let data = (serde_yaml::from_str::<Mapping>(&raw))?;\n            (serde_yaml::to_string(&data).context(\"failed to convert yaml to string\"))?\n        }\n        _ => (item.read_file())?,\n    };\n    Ok(data)\n}\n\n#[tauri::command]\n#[specta::specta]\npub fn save_profile_file(uid: String, file_data: Option<String>) -> Result {\n    if file_data.is_none() {\n        return Ok(());\n    }\n\n    let profiles = Config::profiles();\n    let profiles = profiles.latest();\n    let item = (profiles.get_item(&uid))?;\n    (item.save_file(file_data.unwrap()))?;\n    Ok(())\n}\n\n#[tauri::command]\n#[specta::specta]\npub fn get_clash_info() -> Result<ClashInfo> {\n    Ok(Config::clash().latest().get_client_info())\n}\n\n/// get the runtime config\n#[tauri::command]\n#[specta::specta]\npub fn get_runtime_config() -> Result<Option<serde_json::Value>> {\n    let config = Config::runtime().latest().config.clone();\n    match config {\n        Some(cfg) => {\n            let yaml_value = serde_yaml::to_value(cfg)?;\n            let json_value = serde_json::to_value(&yaml_value)?;\n            Ok(Some(json_value))\n        }\n        None => Ok(None),\n    }\n}\n\n#[tauri::command]\n#[specta::specta]\npub fn get_runtime_yaml() -> Result<String> {\n    let runtime = Config::runtime();\n    let runtime = runtime.latest();\n    let config = runtime.config.as_ref();\n    let mapping = (config\n        .ok_or(anyhow::anyhow!(\"failed to parse config to yaml file\"))\n        .and_then(|config| {\n            serde_yaml::to_string(config).context(\"failed to convert config to yaml\")\n        }))?;\n    Ok(mapping)\n}\n\n#[tauri::command]\n#[specta::specta]\npub fn get_runtime_exists() -> Result<Vec<String>> {\n    Ok(Config::runtime().latest().exists_keys.clone())\n}\n\n#[tauri::command]\n#[specta::specta]\npub fn get_postprocessing_output() -> Result<PostProcessingOutput> {\n    Ok(Config::runtime().latest().postprocessing_output.clone())\n}\n\n#[tauri::command]\n#[specta::specta]\npub async fn get_core_status<'n>() -> Result<(Cow<'n, CoreState>, i64, RunType)> {\n    Ok(CoreManager::global().status().await)\n}\n\n#[tauri::command]\n#[specta::specta]\npub async fn url_delay_test(url: &str, expected_status: u16) -> Result<Option<u64>> {\n    Ok(crate::utils::net::url_delay_test(url, expected_status).await)\n}\n\n#[tauri::command]\n#[specta::specta]\npub async fn get_ipsb_asn() -> Result<serde_json::Value> {\n    Ok(crate::utils::net::get_ipsb_asn().await?)\n}\n\n/// patch clash runtime config\n#[tauri::command]\n#[specta::specta]\n#[tracing_attributes::instrument]\npub async fn patch_clash_config(payload: PatchRuntimeConfig) -> Result {\n    tracing::debug!(\"patch_clash_config: {payload:?}\");\n\n    let mapping = match serde_yaml::to_value(&payload)? {\n        serde_yaml::Value::Mapping(m) => m,\n        _ => return Err(IpcError::Custom(\"Expected a mapping\".to_string())),\n    };\n\n    (crate::core::clash::api::patch_configs(&mapping).await)?;\n\n    if let Err(e) = feat::patch_clash(mapping).await {\n        tracing::error!(\"{e}\");\n        return Err(IpcError::from(e));\n    }\n\n    feat::update_proxies_buff(None);\n    Ok(())\n}\n\n#[tauri::command]\n#[specta::specta]\npub fn get_verge_config() -> Result<IVerge> {\n    Ok(Config::verge().data().clone())\n}\n\n#[tauri::command]\n#[specta::specta]\npub async fn patch_verge_config(payload: IVerge) -> Result {\n    (feat::patch_verge(payload).await)?;\n    Ok(())\n}\n\n#[tauri::command]\n#[specta::specta]\npub async fn change_clash_core(clash_core: Option<nyanpasu::ClashCore>) -> Result {\n    (CoreManager::global().change_core(clash_core).await)?;\n    Ok(())\n}\n\n/// restart the sidecar\n#[tauri::command]\n#[specta::specta]\npub async fn restart_sidecar() -> Result {\n    (CoreManager::global().run_core().await)?;\n    Ok(())\n}\n\n/// get the system proxy\n/// server field is the combination of host and port\n#[tauri::command]\n#[specta::specta]\npub fn get_sys_proxy() -> Result<GetSysProxyResponse> {\n    let current = (Sysproxy::get_system_proxy()).context(\"failed to get system proxy\")?;\n\n    let server = format!(\"{}:{}\", current.host, current.port);\n\n    Ok(GetSysProxyResponse {\n        enable: current.enable,\n        host: current.host,\n        port: current.port,\n        bypass: current.bypass,\n        server,\n    })\n}\n\n#[tauri::command]\n#[specta::specta]\npub fn get_clash_logs() -> Result<VecDeque<String>> {\n    Ok(Logger::global().get_log())\n}\n\n#[tauri::command]\n#[specta::specta]\npub fn open_app_config_dir() -> Result<()> {\n    let config_dir = (dirs::app_config_dir())?;\n    (crate::utils::open::that(config_dir))?;\n    Ok(())\n}\n\n#[tauri::command]\n#[specta::specta]\npub fn open_app_data_dir() -> Result<()> {\n    let data_dir = (dirs::app_data_dir())?;\n    (crate::utils::open::that(data_dir))?;\n    Ok(())\n}\n\n#[tauri::command]\n#[specta::specta]\npub fn open_core_dir() -> Result<()> {\n    let core_dir = (tauri::utils::platform::current_exe())?;\n    let core_dir = core_dir\n        .parent()\n        .ok_or(\"failed to get core dir\".to_string())?;\n    (crate::utils::open::that(core_dir))?;\n    Ok(())\n}\n\n#[tauri::command]\n#[specta::specta]\npub fn get_core_dir() -> Result<String> {\n    let core_dir = (tauri::utils::platform::current_exe())?;\n    let core_dir = core_dir\n        .parent()\n        .ok_or(\"failed to get core dir\".to_string())?;\n    let core_dir = dunce::canonicalize(core_dir)?;\n    Ok(core_dir.to_string_lossy().to_string())\n}\n\n#[tauri::command]\n#[specta::specta]\npub fn open_logs_dir() -> Result<()> {\n    let log_dir = (dirs::app_logs_dir())?;\n    (crate::utils::open::that(log_dir))?;\n    Ok(())\n}\n\n#[tauri::command]\n#[specta::specta]\npub fn open_web_url(url: String) -> Result<()> {\n    (crate::utils::open::that(url))?;\n    Ok(())\n}\n\n#[tauri::command]\n#[specta::specta]\npub async fn fetch_latest_core_versions() -> Result<ManifestVersionLatest> {\n    let mut updater = updater::UpdaterManager::global().write().await; // It is intended to block here\n    (updater.fetch_latest().await)?;\n    // TODO: result key should be kebab-case\n    Ok(updater.get_latest_versions())\n}\n\n#[tauri::command]\n#[specta::specta]\npub async fn get_core_version(\n    app_handle: AppHandle,\n    core_type: nyanpasu::ClashCore,\n) -> Result<String> {\n    match resolve::resolve_core_version(&app_handle, &core_type).await {\n        Ok(version) => Ok(version),\n        Err(err) => Err(IpcError::from(err)),\n    }\n}\n\n#[tauri::command]\n#[specta::specta]\npub async fn collect_logs(app_handle: AppHandle) -> Result {\n    let now = Local::now().format(\"%Y-%m-%d\");\n    let fname = format!(\"{now}-log\");\n    let builder = FileDialogBuilder::new(app_handle.dialog().clone());\n    builder\n        .add_filter(\"archive files\", &[\"zip\"])\n        .set_file_name(&fname)\n        .set_title(\"Save log archive\")\n        .save_file(|file_path| match file_path {\n            Some(path) if path.as_path().is_some() => {\n                debug!(\"{path:#?}\");\n                match candy::collect_logs(path.as_path().unwrap()) {\n                    Ok(_) => (),\n                    Err(err) => {\n                        log::error!(target: \"app\", \"{err:?}\");\n                    }\n                }\n            }\n            _ => (),\n        });\n    Ok(())\n}\n\n#[tauri::command]\n#[specta::specta]\npub async fn update_core(core_type: nyanpasu::ClashCore) -> Result<usize> {\n    let event_id = (updater::UpdaterManager::global()\n        .write()\n        .await\n        .update_core(&core_type)\n        .await)?;\n    Ok(event_id)\n}\n\n#[tauri::command]\n#[specta::specta]\npub async fn inspect_updater(updater_id: usize) -> Result<updater::UpdaterSummary> {\n    let updater = (updater::UpdaterManager::global()\n        .read()\n        .await\n        .inspect_updater(updater_id)\n        .ok_or(anyhow::anyhow!(\"updater is not exist\")))?;\n    Ok(updater)\n}\n\n#[tauri::command]\n#[specta::specta]\npub async fn clash_api_get_proxy_delay(\n    name: String,\n    url: Option<String>,\n) -> Result<clash::api::DelayRes> {\n    match clash::api::get_proxy_delay(name, url).await {\n        Ok(res) => Ok(res),\n        Err(err) => Err(err.into()),\n    }\n}\n\n#[tauri::command]\n#[specta::specta]\npub async fn get_proxies() -> Result<crate::core::clash::proxies::Proxies> {\n    use crate::core::clash::proxies::{ProxiesGuard, ProxiesGuardExt};\n    {\n        let guard = ProxiesGuard::global().read();\n        if guard.is_updated() {\n            return Ok(guard.inner().clone());\n        }\n    }\n    match ProxiesGuard::global().update().await {\n        Ok(_) => {\n            let proxies = ProxiesGuard::global().read().inner().clone();\n            Ok(proxies)\n        }\n        Err(err) => Err(err.into()),\n    }\n}\n\n#[tauri::command]\n#[specta::specta]\npub async fn mutate_proxies() -> Result<crate::core::clash::proxies::Proxies> {\n    use crate::core::clash::proxies::{ProxiesGuard, ProxiesGuardExt};\n    (ProxiesGuard::global().update().await)?;\n    Ok(ProxiesGuard::global().read().inner().clone())\n}\n\n#[tauri::command]\n#[specta::specta]\npub async fn select_proxy(group: String, name: String) -> Result<()> {\n    use crate::core::clash::proxies::{ProxiesGuard, ProxiesGuardExt};\n    (ProxiesGuard::global().select_proxy(&group, &name).await)?;\n\n    // Interrupt connections based on configuration\n    let _ = crate::core::connection_interruption::ConnectionInterruptionService::on_proxy_change()\n        .await;\n\n    Ok(())\n}\n\n#[tauri::command]\n#[specta::specta]\npub async fn update_proxy_provider(name: String) -> Result<()> {\n    use crate::core::clash::{\n        api,\n        proxies::{ProxiesGuard, ProxiesGuardExt},\n    };\n    (api::update_providers_proxies_group(&name).await)?;\n    (ProxiesGuard::global().update().await)?;\n    Ok(())\n}\n\n#[tauri::command]\n#[specta::specta]\npub fn collect_envs<'a>() -> Result<EnvInfo<'a>> {\n    Ok((crate::utils::collect::collect_envs())?)\n}\n\n#[tauri::command]\n#[specta::specta]\npub fn open_that(path: String) -> Result {\n    (crate::utils::open::that(path))?;\n    Ok(())\n}\n\n#[tauri::command]\n#[specta::specta]\npub fn is_appimage() -> Result<bool> {\n    Ok(*crate::consts::IS_APPIMAGE)\n}\n\n#[cfg(windows)]\n#[tauri::command]\n#[specta::specta]\npub fn get_custom_app_dir() -> Result<Option<String>> {\n    use crate::utils::winreg::get_app_dir;\n    match get_app_dir() {\n        Ok(Some(path)) => Ok(Some(path.to_string_lossy().to_string())),\n        Ok(None) => Ok(None),\n        Err(err) => Err(IpcError::from(err)),\n    }\n}\n\n#[cfg(not(windows))]\n#[tauri::command]\n#[specta::specta]\npub fn get_custom_app_dir() -> Result<Option<String>> {\n    Ok(None)\n}\n\n#[cfg(windows)]\n#[tauri::command]\n#[specta::specta]\npub async fn set_custom_app_dir(app_handle: tauri::AppHandle, path: String) -> Result {\n    use crate::utils::{self, dialog::migrate_dialog, winreg::set_app_dir};\n    use rust_i18n::t;\n    use std::path::PathBuf;\n\n    let path_str = path.clone();\n    let path = PathBuf::from(path);\n\n    // show a dialog to ask whether to migrate the data\n    let res =\n        tauri::async_runtime::spawn_blocking(move || {\n            let msg = t!(\"dialog.custom_app_dir_migrate\", path = path_str).to_string();\n\n            if migrate_dialog(&msg) {\n                let app_exe = tauri::utils::platform::current_exe()?;\n                let app_exe = dunce::canonicalize(app_exe)?.to_string_lossy().to_string();\n                std::process::Command::new(\"powershell\")\n                    .arg(\"-Command\")\n                    .arg(\n                    format!(\n                        r#\"Start-Process '{}' -ArgumentList 'migrate-home-dir','\"{}\"' -Verb runAs\"#,\n                        app_exe.as_str(),\n                        path_str.as_str()\n                    )\n                    .as_str(),\n                ).spawn().unwrap().wait()?;\n                utils::help::quit_application(&app_handle);\n            } else {\n                set_app_dir(&path)?;\n            }\n            Ok::<_, anyhow::Error>(())\n        })\n        .await;\n    ((res)?)?;\n    Ok(())\n}\n\n#[tauri::command]\n#[specta::specta]\npub fn restart_application(app_handle: tauri::AppHandle) -> Result {\n    crate::utils::help::restart_application(&app_handle);\n    Ok(())\n}\n\n#[tauri::command]\n#[specta::specta]\npub fn get_server_port() -> Result<u16> {\n    Ok(*crate::server::SERVER_PORT)\n}\n\n#[cfg(not(windows))]\n#[tauri::command]\n#[specta::specta]\npub async fn set_custom_app_dir(_path: String) -> Result {\n    Ok(())\n}\n\n#[cfg(windows)]\npub mod uwp {\n    use super::Result;\n    use crate::core::win_uwp;\n\n    #[tauri::command]\n    #[specta::specta]\n    pub async fn invoke_uwp_tool() -> Result {\n        (win_uwp::invoke_uwptools().await)?;\n        Ok(())\n    }\n}\n\n#[tauri::command]\n#[specta::specta]\npub async fn set_tray_icon(\n    app_handle: tauri::AppHandle,\n    mode: TrayIcon,\n    path: Option<PathBuf>,\n) -> Result {\n    (crate::core::tray::icon::set_icon(mode, path))?;\n    (crate::core::tray::Tray::update_part(&app_handle))?;\n    Ok(())\n}\n\n#[tauri::command]\n#[specta::specta]\npub async fn is_tray_icon_set(mode: TrayIcon) -> Result<bool> {\n    let icon_path = (crate::utils::dirs::tray_icons_path(mode.as_str()))?;\n    Ok(tokio::fs::metadata(icon_path).await.is_ok())\n}\n\npub mod service {\n    use super::Result;\n    use crate::core::service;\n\n    #[tauri::command]\n    #[specta::specta]\n    pub async fn status_service<'a>() -> Result<nyanpasu_ipc::types::StatusInfo<'a>> {\n        let res = (service::control::status().await)?;\n        Ok(res)\n    }\n\n    #[tauri::command]\n    #[specta::specta]\n    pub async fn install_service() -> Result {\n        (service::control::install_service().await)?;\n        Ok(())\n    }\n\n    #[tauri::command]\n    #[specta::specta]\n    pub async fn uninstall_service() -> Result {\n        (service::control::uninstall_service().await)?;\n        Ok(())\n    }\n\n    #[tauri::command]\n    #[specta::specta]\n    pub async fn start_service() -> Result {\n        let res = service::control::start_service().await;\n        let enabled_service = {\n            *crate::config::Config::verge()\n                .latest()\n                .enable_service_mode\n                .as_ref()\n                .unwrap_or(&false)\n        };\n        if enabled_service && let Err(e) = crate::core::CoreManager::global().run_core().await {\n            log::error!(target: \"app\", \"{e}\");\n        }\n        Ok(res?)\n    }\n\n    #[tauri::command]\n    #[specta::specta]\n    pub async fn stop_service() -> Result {\n        let res = service::control::stop_service().await;\n        let enabled_service = {\n            *crate::config::Config::verge()\n                .latest()\n                .enable_service_mode\n                .as_ref()\n                .unwrap_or(&false)\n        };\n        if enabled_service && let Err(e) = crate::core::CoreManager::global().run_core().await {\n            log::error!(target: \"app\", \"{e}\");\n        }\n        Ok(res?)\n    }\n\n    #[tauri::command]\n    #[specta::specta]\n    pub async fn restart_service() -> Result {\n        let res = service::control::restart_service().await;\n        let enabled_service = {\n            *crate::config::Config::verge()\n                .latest()\n                .enable_service_mode\n                .as_ref()\n                .unwrap_or(&false)\n        };\n        if enabled_service && let Err(e) = crate::core::CoreManager::global().run_core().await {\n            log::error!(target: \"app\", \"{e}\");\n        }\n        Ok(res?)\n    }\n}\n\n#[cfg(not(windows))]\npub mod uwp {\n    use super::*;\n\n    #[tauri::command]\n    #[specta::specta]\n    pub async fn invoke_uwp_tool() -> Result {\n        Ok(())\n    }\n}\n\n#[tauri::command]\n#[specta::specta]\npub async fn get_service_install_prompt() -> Result<String> {\n    let args = (crate::core::service::control::get_service_install_args().await)?\n        .into_iter()\n        .map(|arg| arg.to_string_lossy().to_string())\n        .collect::<Vec<_>>()\n        .join(\" \");\n    let mut prompt = format!(\"./nyanpasu-service {args}\");\n    if cfg!(not(windows)) {\n        prompt = format!(\"sudo {prompt}\");\n    }\n    Ok(prompt)\n}\n\n#[tauri::command]\n#[specta::specta]\npub fn cleanup_processes(app_handle: AppHandle) -> Result {\n    crate::utils::help::cleanup_processes(&app_handle);\n    Ok(())\n}\n\n/// Namespace prefix for all frontend-visible KV entries.\n/// Internal subsystems (e.g. task storage) use un-prefixed keys and are\n/// never exposed to the frontend through these IPC commands.\nconst WEB_STORAGE_KEY_PREFIX: &str = \"web:\";\n\nfn web_key(key: &str) -> String {\n    format!(\"{WEB_STORAGE_KEY_PREFIX}{key}\")\n}\n\n#[tauri::command]\n#[specta::specta]\npub fn get_storage_item(app_handle: AppHandle, key: String) -> Result<Option<String>> {\n    let storage = app_handle.state::<Storage>();\n    let value = (storage.get_item(&web_key(&key)))?;\n    Ok(value)\n}\n\n#[tauri::command]\n#[specta::specta]\npub fn set_storage_item(app_handle: AppHandle, key: String, value: String) -> Result {\n    let storage = app_handle.state::<Storage>();\n    (storage.set_item(&web_key(&key), &value))?;\n    Ok(())\n}\n\n#[tauri::command]\n#[specta::specta]\npub fn remove_storage_item(app_handle: AppHandle, key: String) -> Result {\n    let storage = app_handle.state::<Storage>();\n    (storage.remove_item(&web_key(&key)))?;\n    Ok(())\n}\n\n#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, specta::Type)]\npub struct StorageEntry {\n    pub key: String,\n    /// Raw JSON-encoded value string.\n    pub value: String,\n}\n\n/// Debug: returns all frontend KV entries (keys with the `web:` prefix).\n/// Internal storage entries used by other subsystems are excluded.\n#[tauri::command]\n#[specta::specta]\npub fn get_all_storage_items(app_handle: AppHandle) -> Result<Vec<StorageEntry>> {\n    let storage = app_handle.state::<Storage>();\n    let items = storage.get_all()?;\n    Ok(items\n        .into_iter()\n        .filter_map(|(raw_key, value)| {\n            raw_key\n                .strip_prefix(WEB_STORAGE_KEY_PREFIX)\n                .map(|key| StorageEntry {\n                    key: key.to_string(),\n                    value,\n                })\n        })\n        .collect())\n}\n\n/// Debug: clears all frontend KV entries (keys with the `web:` prefix).\n/// Internal storage entries used by other subsystems are left intact.\n#[tauri::command]\n#[specta::specta]\npub fn clear_storage(app_handle: AppHandle) -> Result {\n    let storage = app_handle.state::<Storage>();\n    let web_keys: Vec<String> = storage\n        .get_all()?\n        .into_iter()\n        .filter(|(k, _)| k.starts_with(WEB_STORAGE_KEY_PREFIX))\n        .map(|(k, _)| k)\n        .collect();\n    for key in web_keys {\n        storage.remove_item(&key)?;\n    }\n    Ok(())\n}\n\n#[tauri::command]\n#[specta::specta]\npub async fn get_clash_ws_connections_state(\n    app_handle: AppHandle,\n) -> Result<crate::core::clash::ws::ClashConnectionsConnectorState> {\n    let ws_connector = app_handle.state::<crate::core::clash::ws::ClashConnectionsConnector>();\n    Ok(ws_connector.state())\n}\n\n// Updater block\n\n#[derive(Default, Clone, serde::Serialize, serde::Deserialize, specta::Type)]\n// TODO: a copied from updater metadata, and should be moved a separate updater module\npub struct UpdateWrapper {\n    rid: tauri::ResourceId,\n    available: bool,\n    current_version: String,\n    version: String,\n    date: Option<String>,\n    body: Option<String>,\n    raw_json: serde_json::Value,\n}\n\n#[tauri::command]\n#[specta::specta]\npub async fn check_update(webview: tauri::Webview) -> Result<Option<UpdateWrapper>> {\n    use crate::utils::config::{get_self_proxy, get_system_proxy};\n    use std::cmp::Ordering;\n    use tauri_plugin_updater::UpdaterExt;\n\n    let build_time = time::OffsetDateTime::parse(\n        crate::consts::BUILD_INFO.build_date,\n        &time::format_description::well_known::Rfc3339,\n    )\n    .context(\"failed to parse build time\")?;\n    let mut builder = webview\n        .updater_builder()\n        .version_comparator(move |_, remote| {\n            use semver::Version;\n            let local = Version::parse(crate::consts::BUILD_INFO.pkg_version).ok();\n            log::trace!(\"[check] local: {:?}, remote: {:?}\", local, remote.version);\n            match local {\n                Some(local) => {\n                    if !local.build.is_empty() && !remote.version.build.is_empty() {\n                        // ignore build info to compare the version directly\n                        match local.cmp_precedence(&remote.version) {\n                            Ordering::Less => true,\n                            Ordering::Equal => match remote.pub_date {\n                                // prefer newer build if pub_date is available\n                                Some(pub_date) => {\n                                    local.build != remote.version.build && pub_date > build_time\n                                }\n                                None => local.build != remote.version.build,\n                            },\n                            Ordering::Greater => false,\n                        }\n                    } else {\n                        local < remote.version\n                    }\n                }\n                None => false,\n            }\n        });\n    // apply proxy\n    if let Ok(proxy) = get_self_proxy() {\n        builder = builder.proxy(proxy.parse().context(\"failed to parse proxy\")?);\n    }\n    if let Ok(Some(proxy)) = get_system_proxy() {\n        builder = builder.proxy(proxy.parse().context(\"failed to parse system proxy\")?);\n    }\n    let updater = builder.build().context(\"failed to build updater\")?;\n    let update = updater.check().await.context(\"failed to check update\")?;\n    Ok(update.map(|u| {\n        let mut wrapper = UpdateWrapper {\n            available: true,\n            current_version: u.current_version.clone(),\n            version: u.version.clone(),\n            date: u.date.and_then(|d| {\n                d.format(&time::format_description::well_known::Rfc3339)\n                    .ok()\n            }),\n            body: u.body.clone(),\n            raw_json: u.raw_json.clone(),\n            ..Default::default()\n        };\n        wrapper.rid = webview.resources_table().add(u);\n        wrapper\n    }))\n}\n\n#[tauri::command]\n#[specta::specta]\npub fn save_window_size_state(app_handle: AppHandle, label: String) -> Result<()> {\n    match label.as_str() {\n        crate::consts::MAIN_WINDOW_LABEL => {\n            resolve::save_main_window_state(&app_handle, true)?;\n        }\n        crate::consts::LEGACY_WINDOW_LABEL => {\n            resolve::save_legacy_window_state(&app_handle, true)?;\n        }\n        _ => {\n            log::warn!(\"Unknown window label: {}\", label);\n        }\n    }\n    Ok(())\n}\n\n#[tauri::command]\n#[specta::specta]\npub fn create_main_window(app_handle: AppHandle) -> Result<()> {\n    // Spawn window creation to avoid blocking\n    std::thread::spawn(move || {\n        // Small delay to let the IPC return first\n        std::thread::sleep(std::time::Duration::from_millis(10));\n        let handle_inner = app_handle.clone();\n        let _ = app_handle.run_on_main_thread(move || {\n            resolve::create_main_window(&handle_inner);\n        });\n    });\n    Ok(())\n}\n\n#[tauri::command]\n#[specta::specta]\npub fn create_legacy_window(app_handle: AppHandle) -> Result<()> {\n    // Spawn window creation to avoid blocking\n    std::thread::spawn(move || {\n        // Small delay to let the IPC return first\n        std::thread::sleep(std::time::Duration::from_millis(10));\n        let handle_inner = app_handle.clone();\n        let _ = app_handle.run_on_main_thread(move || {\n            resolve::create_legacy_window(&handle_inner);\n        });\n    });\n    Ok(())\n}\n\n#[tauri::command]\n#[specta::specta]\npub fn create_editor_window(app_handle: AppHandle, uid: String) -> Result<()> {\n    // Spawn window creation to avoid blocking\n    std::thread::spawn(move || {\n        // Small delay to let the IPC return first\n        std::thread::sleep(std::time::Duration::from_millis(10));\n        let handle_inner = app_handle.clone();\n        let _ = app_handle.run_on_main_thread(move || {\n            let _ = resolve::create_editor_window(&handle_inner, &uid);\n        });\n    });\n    Ok(())\n}\n"
  },
  {
    "path": "backend/tauri/src/lib.rs",
    "content": "#![feature(auto_traits, negative_impls, trait_alias, impl_trait_in_assoc_type)]\n#![cfg_attr(\n    all(not(debug_assertions), target_os = \"windows\"),\n    windows_subsystem = \"windows\"\n)]\n// This lint was needed by ambassador\n#![allow(clippy::duplicated_attributes)]\nmod cmds;\nmod config;\nmod consts;\nmod core;\nmod enhance;\nmod event_handler;\nmod feat;\nmod ipc;\nmod logging;\nmod server;\nmod setup;\n\n#[cfg(windows)]\nmod shutdown_hook;\nmod utils;\nmod widget;\nmod window;\n\nuse std::io;\n\nuse crate::{\n    config::Config,\n    core::handle::Handle,\n    utils::{init, resolve},\n};\nuse anyhow::Context;\nuse specta_typescript::{BigIntExportBehavior, Typescript};\nuse tauri::{Emitter, Manager};\nuse tauri_specta::{collect_commands, collect_events};\nuse utils::resolve::{is_window_opened, reset_window_open_counter};\n\nrust_i18n::i18n!(\"./locales\");\n\n#[cfg(feature = \"deadlock-detection\")]\nfn deadlock_detection() {\n    use parking_lot::deadlock;\n    use std::{thread, time::Duration};\n    use tracing::error;\n    thread::spawn(move || {\n        loop {\n            thread::sleep(Duration::from_secs(10));\n            let deadlocks = deadlock::check_deadlock();\n            if deadlocks.is_empty() {\n                continue;\n            }\n\n            error!(\"{} deadlocks detected\", deadlocks.len());\n            for (i, threads) in deadlocks.iter().enumerate() {\n                error!(\"Deadlock #{}\", i);\n                for t in threads {\n                    error!(\"Thread Id {:#?}\", t.thread_id());\n                    error!(\"{:#?}\", t.backtrace());\n                }\n            }\n        }\n    });\n}\n\n#[cfg_attr(mobile, tauri::mobile_entry_point)]\npub fn run() -> std::io::Result<()> {\n    // share the tauri async runtime to nyanpasu-utils\n    #[cfg(feature = \"deadlock-detection\")]\n    deadlock_detection();\n\n    // Should be in first place in order prevent single instance check block everything\n    // Custom scheme check\n    #[cfg(not(target_os = \"macos\"))]\n    // on macos the plugin handles this (macos doesn't use cli args for the url)\n    let custom_scheme = match std::env::args().nth(1) {\n        Some(url) => url::Url::parse(&url).ok(),\n        None => None,\n    };\n    #[cfg(target_os = \"macos\")]\n    let custom_scheme: Option<url::Url> = None;\n\n    if custom_scheme.is_none() {\n        // Parse commands\n        cmds::parse().unwrap();\n    };\n    #[cfg(feature = \"verge-dev\")]\n    tauri_plugin_deep_link::prepare(\"moe.elaina.clash.nyanpasu.dev\");\n\n    #[cfg(not(feature = \"verge-dev\"))]\n    tauri_plugin_deep_link::prepare(\"moe.elaina.clash.nyanpasu\");\n\n    // 单例检测 with robust logging\n    let single_instance_result = utils::init::check_singleton();\n    match &single_instance_result {\n        Ok(Some(_)) => {\n            tracing::info!(target: \"app\", \"Acquired single-instance lock\");\n        }\n        Ok(None) => {\n            tracing::warn!(target: \"app\", \"Another instance is running; exiting\");\n            std::process::exit(0);\n        }\n        Err(e) => {\n            tracing::error!(target: \"app\", \"Failed to check single-instance lock: {e:?}\");\n            // Policy: continue startup in best-effort mode\n        }\n    }\n    // Use system locale as default\n    let locale = {\n        let locale = utils::help::get_system_locale();\n        utils::help::mapping_to_i18n_key(&locale)\n    };\n    rust_i18n::set_locale(locale.to_lowercase().as_str());\n\n    if single_instance_result\n        .as_ref()\n        .is_ok_and(|instance| instance.is_some())\n        && let Err(e) = init::run_pending_migrations()\n    {\n        // Try to open migration log files\n        if let Ok(data_dir) = crate::utils::dirs::app_data_dir() {\n            let _ = crate::utils::open::that(data_dir.join(\"migration.log\"));\n        }\n\n        utils::dialog::panic_dialog(&format!(\n            \"Failed to finish migration event: {e}\\nYou can see the detailed information at migration.log in your local data dir.\\nYou're supposed to submit it as the attachment of new issue.\",\n        ));\n        std::process::exit(1);\n    }\n\n    crate::log_err!(init::init_config());\n\n    // Panic Hook to show a panic dialog and save logs\n    std::panic::set_hook(Box::new(move |panic_info| {\n        use std::backtrace::{Backtrace, BacktraceStatus};\n        let payload = panic_info.payload();\n\n        #[allow(clippy::manual_map)]\n        let payload = if let Some(s) = payload.downcast_ref::<&str>() {\n            Some(&**s)\n        } else if let Some(s) = payload.downcast_ref::<String>() {\n            Some(s.as_str())\n        } else {\n            None\n        };\n\n        let location = panic_info.location().map(|l| l.to_string());\n        let (backtrace, note) = {\n            let backtrace = Backtrace::force_capture();\n            let note = (backtrace.status() == BacktraceStatus::Disabled)\n                .then_some(\"run with RUST_BACKTRACE=1 environment variable to display a backtrace\");\n            (Some(backtrace), note)\n        };\n\n        tracing::error!(\n            panic.payload = payload,\n            panic.location = location,\n            panic.backtrace = backtrace.as_ref().map(tracing::field::display),\n            panic.note = note,\n            \"A panic occurred\",\n        );\n\n        // This is a workaround for the upstream issue: https://github.com/tauri-apps/tauri/issues/10546\n        if let Some(s) = payload.as_ref()\n            && s.contains(\"PostMessage failed ; is the messages queue full?\")\n        {\n            return;\n        }\n\n        // FIXME: maybe move this logic to a util function?\n        let msg = format!(\n            \"Oops, we encountered some issues and program will exit immediately.\\n\\npayload: {payload:#?}\\nlocation: {location:?}\\nbacktrace: {backtrace:#?}\\n\\n\",\n        );\n        let child = std::process::Command::new(tauri::utils::platform::current_exe().unwrap())\n            .arg(\"panic-dialog\")\n            .arg(msg.as_str())\n            .spawn();\n        // fallback to show a dialog directly\n        if child.is_err() {\n            utils::dialog::panic_dialog(msg.as_str());\n        }\n\n        match Handle::global().app_handle.lock().as_ref() {\n            Some(app_handle) => {\n                app_handle.exit(1);\n            }\n            None => {\n                log::error!(\"app handle is not initialized\");\n                std::process::exit(1);\n            }\n        }\n    }));\n\n    // setup specta\n    let specta_builder = tauri_specta::Builder::<tauri::Wry>::new()\n        .commands(collect_commands![\n            // common\n            ipc::get_sys_proxy,\n            ipc::open_app_config_dir,\n            ipc::open_app_data_dir,\n            ipc::open_logs_dir,\n            ipc::open_web_url,\n            ipc::open_core_dir,\n            // cmds::kill_sidecar,\n            ipc::restart_sidecar,\n            // clash\n            ipc::get_clash_info,\n            ipc::get_clash_logs,\n            ipc::patch_clash_config,\n            ipc::change_clash_core,\n            ipc::get_runtime_config,\n            ipc::get_runtime_yaml,\n            ipc::get_runtime_exists,\n            ipc::get_postprocessing_output,\n            ipc::clash_api_get_proxy_delay,\n            ipc::uwp::invoke_uwp_tool,\n            // updater\n            ipc::fetch_latest_core_versions,\n            ipc::update_core,\n            ipc::inspect_updater,\n            ipc::get_core_version,\n            // utils\n            ipc::collect_logs,\n            // verge\n            ipc::get_verge_config,\n            ipc::patch_verge_config,\n            // cmds::update_hotkeys,\n            // profile\n            ipc::get_profiles,\n            ipc::enhance_profiles,\n            ipc::patch_profiles_config,\n            ipc::view_profile,\n            ipc::patch_profile,\n            ipc::create_profile,\n            ipc::import_profile,\n            ipc::reorder_profile,\n            ipc::reorder_profiles_by_list,\n            ipc::update_profile,\n            ipc::delete_profile,\n            ipc::read_profile_file,\n            ipc::save_profile_file,\n            ipc::get_custom_app_dir,\n            ipc::set_custom_app_dir,\n            // service mode\n            ipc::service::status_service,\n            ipc::service::install_service,\n            ipc::service::uninstall_service,\n            ipc::service::start_service,\n            ipc::service::stop_service,\n            ipc::service::restart_service,\n            ipc::is_portable,\n            ipc::get_proxies,\n            ipc::select_proxy,\n            ipc::update_proxy_provider,\n            ipc::restart_application,\n            ipc::collect_envs,\n            ipc::get_server_port,\n            ipc::set_tray_icon,\n            ipc::is_tray_icon_set,\n            ipc::get_core_status,\n            ipc::url_delay_test,\n            ipc::get_ipsb_asn,\n            ipc::open_that,\n            ipc::is_appimage,\n            ipc::get_service_install_prompt,\n            ipc::cleanup_processes,\n            ipc::get_storage_item,\n            ipc::set_storage_item,\n            ipc::remove_storage_item,\n            ipc::get_all_storage_items,\n            ipc::clear_storage,\n            ipc::mutate_proxies,\n            ipc::get_core_dir,\n            // clash layer\n            ipc::get_clash_ws_connections_state,\n            // updater layer\n            ipc::check_update,\n            // window management\n            ipc::save_window_size_state,\n            ipc::create_main_window,\n            ipc::create_legacy_window,\n            ipc::create_editor_window,\n        ])\n        .events(collect_events![\n            core::clash::ClashConnectionsEvent,\n            window::WindowMessageEvent,\n            window::ReactAppMountedEvent,\n            core::storage::StorageValueChangedEvent\n        ]);\n\n    #[cfg(debug_assertions)]\n    {\n        const SPECTA_BINDINGS_PATH: &str = \"../../frontend/interface/src/ipc/bindings.ts\";\n\n        match specta_builder.export(\n            Typescript::default()\n                .formatter(specta_typescript::formatter::prettier)\n                .formatter(|file| {\n                    let npx_command = if cfg!(target_os = \"windows\") {\n                        \"npx.cmd\"\n                    } else {\n                        \"npx\"\n                    };\n\n                    std::process::Command::new(npx_command)\n                        .arg(\"prettier\")\n                        .arg(\"--write\")\n                        .arg(file)\n                        .output()\n                        .map(|_| ())\n                        .map_err(io::Error::other)\n                })\n                .bigint(BigIntExportBehavior::Number)\n                .header(\"/* oxlint-disable */\\n// @ts-nocheck\"),\n            SPECTA_BINDINGS_PATH,\n        ) {\n            Ok(_) => {\n                log::debug!(\"Exported typescript bindings, path: {SPECTA_BINDINGS_PATH}\");\n            }\n            Err(e) => {\n                panic!(\"Failed to export typescript bindings: {e}\");\n            }\n        };\n    }\n\n    let verge = { Config::verge().latest().language.clone().unwrap() };\n    rust_i18n::set_locale(verge.to_lowercase().as_str());\n\n    // show a dialog to print the single instance error\n    // Hold the guard until the end of the program if acquired\n    let _singleton = match single_instance_result {\n        Ok(Some(guard)) => Some(guard),\n        _ => None,\n    };\n\n    #[allow(unused_mut)]\n    let mut builder = tauri::Builder::default()\n        .invoke_handler(specta_builder.invoke_handler())\n        .plugin(tauri_plugin_os::init())\n        .plugin(tauri_plugin_shell::init())\n        .plugin(tauri_plugin_process::init())\n        .plugin(tauri_plugin_fs::init())\n        .plugin(tauri_plugin_dialog::init())\n        .plugin(tauri_plugin_clipboard_manager::init())\n        .plugin(tauri_plugin_notification::init())\n        .plugin(tauri_plugin_updater::Builder::new().build())\n        .plugin(tauri_plugin_global_shortcut::Builder::default().build())\n        .setup(move |app| {\n            specta_builder.mount_events(app);\n            setup::setup(app)\n                .context(\"Failed to setup the app\")\n                .inspect_err(|e| {\n                    tracing::error!(\"Failed to setup the app: {:#?}\", e);\n                })?;\n\n            #[cfg(target_os = \"macos\")]\n            {\n                use tauri::menu::{MenuBuilder, SubmenuBuilder};\n                let submenu = SubmenuBuilder::new(app, \"Edit\")\n                    .undo()\n                    .redo()\n                    .copy()\n                    .paste()\n                    .cut()\n                    .select_all()\n                    .close_window()\n                    .quit()\n                    .build()\n                    .unwrap();\n                let menu = MenuBuilder::new(app).item(&submenu).build().unwrap();\n                app.set_menu(menu).unwrap();\n            }\n\n            resolve::resolve_setup(app);\n\n            // setup custom scheme\n            let handle = app.handle().clone();\n            // For start new app from schema\n            #[cfg(not(target_os = \"macos\"))]\n            if let Some(url) = custom_scheme {\n                log::info!(target: \"app\", \"started with schema\");\n                resolve::create_window(&handle.clone());\n                while !is_window_opened() {\n                    log::info!(target: \"app\", \"waiting for window open\");\n                    std::thread::sleep(std::time::Duration::from_millis(100));\n                }\n                Handle::global()\n                    .app_handle\n                    .lock()\n                    .as_ref()\n                    .unwrap()\n                    .emit(\"scheme-request-received\", url.clone())\n                    .unwrap();\n            }\n            // This operation should terminate the app if app is called by custom scheme and this instance is not the primary instance\n            log_err!(tauri_plugin_deep_link::register(\n                &[\"clash-nyanpasu\", \"clash\"],\n                move |request| {\n                    log::info!(target: \"app\", \"scheme request received: {:?}\", &request);\n                    resolve::create_window(&handle.clone()); // create window if not exists\n                    while !is_window_opened() {\n                        log::info!(target: \"app\", \"waiting for window open\");\n                        std::thread::sleep(std::time::Duration::from_millis(100));\n                    }\n                    handle.emit(\"scheme-request-received\", request).unwrap();\n                }\n            ));\n            std::thread::spawn(move || {\n                nyanpasu_utils::runtime::block_on(async move {\n                    server::run(*server::SERVER_PORT)\n                        .await\n                        .expect(\"failed to start server\");\n                });\n            });\n            Ok(())\n        });\n\n    let app = builder\n        .build(tauri::generate_context!())\n        .expect(\"error while running tauri application\");\n    app.run(|app_handle, e| match e {\n        tauri::RunEvent::ExitRequested { api, code, .. } if code.is_none() => {\n            api.prevent_exit();\n        }\n        tauri::RunEvent::ExitRequested { .. } => {\n            utils::help::cleanup_processes(app_handle);\n        }\n        tauri::RunEvent::WindowEvent { label, event, .. } if label == \"main\" => match event {\n            tauri::WindowEvent::ScaleFactorChanged { scale_factor, .. } => {\n                core::tray::on_scale_factor_changed(scale_factor);\n            }\n            tauri::WindowEvent::CloseRequested { .. } => {\n                log::debug!(target: \"app\", \"window close requested\");\n                let _ = resolve::save_window_state(app_handle, true);\n                #[cfg(target_os = \"macos\")]\n                crate::utils::dock::macos::hide_dock_icon();\n            }\n            tauri::WindowEvent::Destroyed => {\n                log::debug!(target: \"app\", \"window destroyed\");\n                reset_window_open_counter();\n            }\n            tauri::WindowEvent::Moved(_) | tauri::WindowEvent::Resized(_) => {\n                log::debug!(target: \"app\", \"window moved or resized\");\n                std::thread::sleep(std::time::Duration::from_nanos(1));\n                let _ = resolve::save_window_state(app_handle, false);\n            }\n            _ => {}\n        },\n        #[cfg(target_os = \"macos\")]\n        tauri::RunEvent::Reopen { .. } => {\n            resolve::create_window(app_handle);\n        }\n        _ => {}\n    });\n\n    Ok(())\n}\n"
  },
  {
    "path": "backend/tauri/src/logging/indexer.rs",
    "content": "use std::{\n    collections::{BTreeMap, BTreeSet},\n    io::SeekFrom,\n    ops::Range,\n};\n\nuse anyhow::Context;\nuse bumpalo::Bump;\nuse camino::Utf8PathBuf;\nuse chrono::{DateTime, Local};\nuse derive_builder::Builder;\nuse itertools::Itertools;\nuse rustc_hash::FxHashMap;\nuse serde::{Deserialize, Serialize};\nuse specta::Type;\nuse tokio::io::{AsyncBufReadExt, AsyncSeekExt, BufReader};\n\n#[derive(\n    Debug, Clone, Copy, Serialize, Deserialize, Type, Hash, Eq, PartialEq, Ord, PartialOrd,\n)]\n#[serde(rename_all = \"UPPERCASE\")]\n#[allow(clippy::upper_case_acronyms)]\npub enum LoggingLevel {\n    DEBUG,\n    INFO,\n    WARN,\n    ERROR,\n    FATAL,\n}\n\n#[derive(Debug, Clone, Serialize, Deserialize, Type)]\n#[serde(rename_all = \"snake_case\")]\npub struct LogEntry {\n    /// The line number of the log entry.\n    /// For query limit, and offset.\n    pub line_number: u64,\n    /// The level of the log entry.\n    pub level: LoggingLevel,\n    /// The timestamp of the log entry.\n    pub timestamp: u64,\n    /// The target of the log entry.\n    /// eg: \"backend::logging::indexer\"\n    pub target: String,\n    /// The start position of the log entry in the file.\n    pub start_pos: usize,\n    /// The end position of the log entry in the file.\n    pub end_pos: usize,\n}\n\n#[derive(Debug, Default, Clone, Serialize, Deserialize, Type)]\nstruct CurrentPos {\n    line: u64,\n    end_pos: usize,\n}\n\n#[derive(Debug, Builder, Clone, Serialize, Deserialize, Type)]\npub struct Query {\n    #[builder(default)]\n    offset: usize,\n    #[builder(default = 100)]\n    limit: usize,\n    #[builder(default, setter(into, strip_option))]\n    level: Option<Vec<LoggingLevel>>,\n    #[builder(default, setter(into, strip_option))]\n    target: Option<Vec<String>>,\n    #[builder(default, setter(into, strip_option))]\n    timestamp: Option<Range<u64>>,\n}\n\npub type LineNumber = u64;\npub type Timestamp = u64;\n\nstruct LogIndex {\n    /// a bump allocator for heap allocation\n    arena: Bump,\n\n    /// index by line number\n    line_index: BTreeMap<LineNumber, *mut LogEntry>,\n    /// index by timestamp\n    /// in our case, the timestamp is nanoseconds, so only one item per timestamp\n    timestamp_index: BTreeMap<Timestamp, LineNumber>,\n    /// index by level\n    level_index: FxHashMap<LoggingLevel, *mut Vec<LineNumber>>,\n    /// index by target\n    target_index: FxHashMap<String, *mut Vec<LineNumber>>,\n\n    last_line_number: Option<LineNumber>,\n}\n\nimpl core::fmt::Debug for LogIndex {\n    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {\n        let lines = self\n            .line_index\n            .values()\n            .map(|v| unsafe {\n                let v = &**v;\n                v.clone()\n            })\n            .collect_vec();\n        let levels = BTreeMap::from_iter(self.level_index.iter().map(|(k, v)| {\n            (k, unsafe {\n                let v = &**v;\n                v.clone()\n            })\n        }));\n        let targets = BTreeMap::from_iter(self.target_index.iter().map(|(k, v)| {\n            (k, unsafe {\n                let v = &**v;\n                v.clone()\n            })\n        }));\n        write!(\n            f,\n            \"LogIndex {{ \n                lines: {:?}; \n                timestamp_index: {:?};\n                level_index: {:?};\n                target_index: {:?};\n                last_line_number: {:?} \n            }}\",\n            lines, self.timestamp_index, levels, targets, self.last_line_number\n        )\n    }\n}\n\nimpl LogIndex {\n    pub fn new() -> Self {\n        Self {\n            arena: Bump::new(),\n            line_index: BTreeMap::new(),\n            timestamp_index: BTreeMap::new(),\n            level_index: FxHashMap::default(),\n            target_index: FxHashMap::default(),\n            last_line_number: None,\n        }\n    }\n\n    #[inline]\n    /// add an entry to the index\n    pub fn add_entry(&mut self, entry: LogEntry) {\n        let line_number = entry.line_number;\n        let timestamp = entry.timestamp;\n        let level = entry.level;\n        let target = entry.target.clone();\n\n        let entry_ptr = self.arena.alloc(entry) as *mut LogEntry;\n        // update level index\n        {\n            let entry = self.level_index.entry(level);\n            entry\n                .and_modify(|v| {\n                    // SAFETY: we are sure that the vec_ptr is valid\n                    unsafe {\n                        let v = &mut **v;\n                        v.push(line_number);\n                    }\n                })\n                .or_insert_with(|| {\n                    let vec = self.arena.alloc(vec![line_number]);\n                    vec as *mut Vec<u64>\n                });\n        }\n        // update timestamp index\n        {\n            let entry = self.timestamp_index.entry(timestamp);\n            entry\n                .and_modify(|v| {\n                    tracing::warn!(\n                        \"duplicate timestamp: {}; previous: {}, new: {}\",\n                        timestamp,\n                        v,\n                        line_number\n                    );\n                    *v = line_number;\n                })\n                .or_insert(line_number);\n        }\n        // update target index\n        {\n            let entry = self.target_index.entry(target);\n            entry\n                .and_modify(|v| {\n                    // SAFETY: we are sure that the vec_ptr is valid\n                    unsafe {\n                        let v = &mut **v;\n                        v.push(line_number);\n                    }\n                })\n                .or_insert_with(|| {\n                    let vec = self.arena.alloc(vec![line_number]);\n                    vec as *mut Vec<u64>\n                });\n        }\n        // update line index\n        {\n            self.line_index.insert(line_number, entry_ptr);\n        }\n\n        self.last_line_number = Some(line_number);\n    }\n\n    // TODO: optimize query performance\n    pub fn query(&self, query: Query) -> Option<Vec<LogEntry>> {\n        // query by timestamp\n        let mut matching_lines: Option<Vec<LineNumber>> = None;\n        if let Some(range) = query.timestamp {\n            let mut range = self.timestamp_index.range(range);\n            let (_, start) = range.next()?;\n            let end = match range.last() {\n                Some((_, end_line)) => *end_line,\n                None => *start,\n            };\n            matching_lines = Some(Vec::from_iter(*start..=end));\n        }\n\n        // query by level\n        if let Some(levels) = query.level {\n            let mut matched_lines = BTreeSet::new();\n            for level in levels {\n                if let Some(lines) = self.level_index.get(&level) {\n                    // SAFETY: we have allocated the vec on the heap by bumpalo\n                    unsafe {\n                        let lines = &**lines;\n                        matched_lines.extend(lines.iter());\n                    }\n                }\n            }\n            matching_lines = match matching_lines {\n                Some(lines) => Some(\n                    lines\n                        .into_iter()\n                        .filter(|line| matched_lines.contains(line))\n                        .collect_vec(),\n                ),\n                None => Some(matched_lines.into_iter().collect_vec()),\n            }\n        }\n\n        // query by target\n        if let Some(targets) = query.target {\n            let mut matched_lines = BTreeSet::new();\n            for target in targets {\n                if let Some(lines) = self.target_index.get(&target) {\n                    // SAFETY: we have allocated the vec on the heap by bumpalo\n                    unsafe {\n                        let lines = &**lines;\n                        matched_lines.extend(lines.iter());\n                    }\n                }\n            }\n            matching_lines = match matching_lines {\n                Some(lines) => Some(\n                    lines\n                        .into_iter()\n                        .filter(|line| matched_lines.contains(line))\n                        .collect_vec(),\n                ),\n                None => Some(matched_lines.into_iter().collect_vec()),\n            }\n        }\n\n        let matching_lines = match matching_lines {\n            Some(lines) if lines.is_empty() => return None,\n            None => {\n                let last_line = self.last_line_number.as_ref()?;\n                Vec::from_iter(0..=*last_line)\n            }\n            Some(lines) => lines,\n        };\n\n        #[cfg(test)]\n        dbg!(&matching_lines);\n\n        let results = matching_lines\n            .into_iter()\n            .skip(query.offset)\n            .take(query.limit)\n            // SAFETY: we are sure that the line_index is valid, which is allocated by bumpalo,\n            // and the pool only be dropped when this index is dropped\n            .map(|line_number| unsafe {\n                let entry = &**self.line_index.get(&line_number).unwrap();\n                entry.clone()\n            })\n            .collect_vec();\n\n        if results.is_empty() {\n            None\n        } else {\n            Some(results)\n        }\n    }\n}\n\npub struct Indexer {\n    index: LogIndex,\n    path: Utf8PathBuf,\n    current: CurrentPos,\n}\n\n#[derive(Debug, Serialize, Deserialize)]\nstruct TracingJson {\n    level: LoggingLevel,\n    target: String,\n    timestamp: DateTime<Local>,\n}\n\nimpl Indexer {\n    pub fn new(path: Utf8PathBuf) -> Self {\n        Self {\n            index: LogIndex::new(),\n            path,\n            current: CurrentPos::default(),\n        }\n    }\n\n    fn handle_line(\n        &mut self,\n        line: &str,\n        current: &mut CurrentPos,\n        bytes_read: usize,\n    ) -> anyhow::Result<()> {\n        let tracing_json: TracingJson =\n            serde_json::from_str(line).context(\"failed to parse log line\")?;\n        let end_pos = current.end_pos + bytes_read;\n        let entry = LogEntry {\n            line_number: current.line,\n            level: tracing_json.level,\n            timestamp: tracing_json.timestamp.timestamp_millis() as u64,\n            target: tracing_json.target,\n            start_pos: current.end_pos,\n            end_pos,\n        };\n        self.index.add_entry(entry);\n        current.line += 1;\n        current.end_pos = end_pos;\n        Ok(())\n    }\n\n    pub async fn build_index(&mut self) -> anyhow::Result<()> {\n        // read file line by line\n        let mut file = tokio::fs::File::open(&self.path).await?;\n        let mut reader = BufReader::new(&mut file);\n        let mut current = CurrentPos::default();\n\n        let mut line = String::new();\n        loop {\n            let bytes_read = reader.read_line(&mut line).await?;\n            if bytes_read == 0 {\n                break;\n            }\n            self.handle_line(&line, &mut current, bytes_read)?;\n            line.clear();\n        }\n        #[cfg(test)]\n        {\n            let bytes_count = file.metadata().await?.len();\n            pretty_assertions::assert_eq!(bytes_count, current.end_pos as u64);\n        }\n        self.current = current;\n        Ok(())\n    }\n\n    pub fn query(&self, query: Query) -> Option<Vec<LogEntry>> {\n        self.index.query(query)\n    }\n\n    pub async fn on_file_change(&mut self) -> anyhow::Result<()> {\n        let mut file = tokio::fs::File::open(&self.path).await?;\n        file.seek(SeekFrom::Start(self.current.end_pos as u64))\n            .await?;\n        let mut reader = BufReader::new(file);\n        let mut current = std::mem::take(&mut self.current);\n        let mut line = String::new();\n        loop {\n            let bytes_read = reader.read_line(&mut line).await?;\n            if bytes_read == 0 {\n                break;\n            }\n            self.handle_line(&line, &mut current, bytes_read)?;\n            line.clear();\n        }\n        self.current = current;\n        Ok(())\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use camino::Utf8PathBuf;\n    use std::io::Write;\n    use tempfile::NamedTempFile;\n\n    #[test]\n    fn test_log_index() {\n        let mut index = LogIndex::new();\n        let query = QueryBuilder::default().build().unwrap();\n        let results = index.query(query);\n        assert!(results.is_none(), \"results should be empty\");\n\n        let entry = LogEntry {\n            line_number: 1,\n            level: LoggingLevel::INFO,\n            timestamp: 1740504078324,\n            target: \"test\".to_string(),\n            start_pos: 0,\n            end_pos: 0,\n        };\n        index.add_entry(entry);\n\n        let entry = LogEntry {\n            line_number: 2,\n            level: LoggingLevel::WARN,\n            timestamp: 1740417699000,\n            target: \"test\".to_string(),\n            start_pos: 0,\n            end_pos: 0,\n        };\n        index.add_entry(entry);\n\n        let entry = LogEntry {\n            line_number: 3,\n            level: LoggingLevel::ERROR,\n            timestamp: 1740417699001,\n            target: \"test\".to_string(),\n            start_pos: 0,\n            end_pos: 0,\n        };\n        index.add_entry(entry);\n\n        let entry = LogEntry {\n            line_number: 4,\n            level: LoggingLevel::INFO,\n            timestamp: 1740331299000,\n            target: \"different_target\".to_string(),\n            start_pos: 0,\n            end_pos: 0,\n        };\n        index.add_entry(entry);\n\n        dbg!(&index);\n\n        // Test offset limit\n        let query = QueryBuilder::default().offset(1).limit(1).build().unwrap();\n        let results = index.query(query).unwrap();\n        dbg!(&results);\n        assert_eq!(results.len(), 1, \"results should have 1 entries\");\n        assert_eq!(results[0].line_number, 1);\n\n        // Test filter by level\n        let query = QueryBuilder::default()\n            .level(vec![LoggingLevel::INFO])\n            .build()\n            .unwrap();\n        let results = index.query(query).unwrap();\n        dbg!(&results);\n        assert_eq!(results.len(), 2, \"results should have 2 entries\");\n        assert_eq!(results[0].line_number, 1);\n        assert_eq!(results[1].line_number, 4);\n\n        let query = QueryBuilder::default()\n            .level(vec![LoggingLevel::INFO, LoggingLevel::WARN])\n            .build()\n            .unwrap();\n        let results = index.query(query).unwrap();\n        dbg!(&results);\n        assert_eq!(results.len(), 3, \"results should have 3 entries\");\n        assert_eq!(results[0].line_number, 1);\n        assert_eq!(results[1].line_number, 2);\n        assert_eq!(results[2].line_number, 4);\n\n        // test filter by target\n        let query = QueryBuilder::default()\n            .target(vec![\"test\".to_string()])\n            .build()\n            .unwrap();\n        let results = index.query(query).unwrap();\n        dbg!(&results);\n        assert_eq!(results.len(), 3, \"results should have 3 entries\");\n        assert_eq!(results[0].line_number, 1);\n        assert_eq!(results[1].line_number, 2);\n        assert_eq!(results[2].line_number, 3);\n\n        // test filter by timestamp\n        let query = QueryBuilder::default()\n            .timestamp(1740417699000..1740504078324)\n            .build()\n            .unwrap();\n        let results = index.query(query).unwrap();\n        dbg!(&results);\n        assert_eq!(results.len(), 2, \"results should have 2 entries\");\n        assert_eq!(results[0].line_number, 2);\n        assert_eq!(results[1].line_number, 3);\n\n        // a complex query\n        let query = QueryBuilder::default()\n            .level(vec![LoggingLevel::INFO, LoggingLevel::WARN])\n            .target(vec![\"test\".to_string()])\n            .timestamp(1740417699000..1740504078324)\n            .build()\n            .unwrap();\n        let results = index.query(query).unwrap();\n        dbg!(&results);\n        assert_eq!(results.len(), 1, \"results should have 1 entries\");\n        assert_eq!(results[0].line_number, 2);\n    }\n\n    fn create_test_log_file(entries: Vec<&str>) -> anyhow::Result<(NamedTempFile, Utf8PathBuf)> {\n        let mut file = NamedTempFile::new()?;\n        for entry in entries {\n            writeln!(file, \"{entry}\")?;\n        }\n        file.flush()?;\n\n        let path = file.path().to_str().unwrap().to_string();\n        let utf8_path = Utf8PathBuf::from(path);\n\n        Ok((file, utf8_path))\n    }\n\n    fn append_to_log_file(file: &mut NamedTempFile, entries: Vec<&str>) -> anyhow::Result<()> {\n        for entry in entries {\n            writeln!(file, \"{entry}\")?;\n        }\n        file.flush()?;\n        Ok(())\n    }\n\n    fn get_sample_log_entries() -> Vec<&'static str> {\n        vec![\n            r#\"{\"level\":\"INFO\",\"target\":\"app::module1\",\"timestamp\":\"2023-02-25T10:15:30+00:00\"}\"#,\n            r#\"{\"level\":\"WARN\",\"target\":\"app::module2\",\"timestamp\":\"2023-02-25T10:16:30+00:00\"}\"#,\n            r#\"{\"level\":\"ERROR\",\"target\":\"app::module1\",\"timestamp\":\"2023-02-25T10:17:30+00:00\"}\"#,\n            r#\"{\"level\":\"DEBUG\",\"target\":\"app::module3\",\"timestamp\":\"2023-02-25T10:18:30+00:00\"}\"#,\n        ]\n    }\n\n    fn get_additional_log_entries() -> Vec<&'static str> {\n        vec![\n            r#\"{\"level\":\"INFO\",\"target\":\"app::module2\",\"timestamp\":\"2023-02-25T10:19:30+00:00\"}\"#,\n            r#\"{\"level\":\"FATAL\",\"target\":\"app::module1\",\"timestamp\":\"2023-02-25T10:20:30+00:00\"}\"#,\n        ]\n    }\n\n    #[test]\n    fn test_indexer_creation() {\n        let entries = get_sample_log_entries();\n        let (_guard, path) = create_test_log_file(entries).unwrap();\n\n        let indexer = Indexer::new(path);\n        assert!(indexer.current.line == 0, \"Initial line count should be 0\");\n        assert!(\n            indexer.current.end_pos == 0,\n            \"Initial end position should be 0\"\n        );\n    }\n\n    #[tokio::test]\n    async fn test_build_index() -> anyhow::Result<()> {\n        let entries = get_sample_log_entries();\n        let (_guard, path) = create_test_log_file(entries.clone()).unwrap();\n\n        let mut indexer = Indexer::new(path);\n        indexer.build_index().await.unwrap();\n\n        // Verify that all entries were indexed\n        assert_eq!(\n            indexer.current.line,\n            entries.len() as u64,\n            \"Line count should match number of entries\"\n        );\n\n        // Query the index to verify entries\n        let query = QueryBuilder::default().build().unwrap();\n        let results = indexer.index.query(query).unwrap();\n\n        assert_eq!(\n            results.len(),\n            entries.len(),\n            \"Query should return all indexed entries\"\n        );\n\n        // Verify specific entries by level\n        let info_query = QueryBuilder::default()\n            .level(vec![LoggingLevel::INFO])\n            .build()?;\n        let info_results = indexer.index.query(info_query).unwrap();\n        assert_eq!(info_results.len(), 1, \"Should have 1 INFO entry\");\n\n        let warn_query = QueryBuilder::default()\n            .level(vec![LoggingLevel::WARN])\n            .build()?;\n        let warn_results = indexer.index.query(warn_query).unwrap();\n        assert_eq!(warn_results.len(), 1, \"Should have 1 WARN entry\");\n\n        let error_query = QueryBuilder::default()\n            .level(vec![LoggingLevel::ERROR])\n            .build()?;\n        let error_results = indexer.index.query(error_query).unwrap();\n        assert_eq!(error_results.len(), 1, \"Should have 1 ERROR entry\");\n\n        let debug_query = QueryBuilder::default()\n            .level(vec![LoggingLevel::DEBUG])\n            .build()?;\n        let debug_results = indexer.index.query(debug_query).unwrap();\n        assert_eq!(debug_results.len(), 1, \"Should have 1 DEBUG entry\");\n\n        Ok(())\n    }\n\n    #[tokio::test]\n    async fn test_on_file_change() -> anyhow::Result<()> {\n        let initial_entries = get_sample_log_entries();\n        let (mut file, path) = create_test_log_file(initial_entries.clone()).unwrap();\n\n        // Initialize and build the initial index\n        let mut indexer = Indexer::new(path);\n        indexer.build_index().await.unwrap();\n\n        // Verify initial indexing\n        assert_eq!(\n            indexer.current.line,\n            initial_entries.len() as u64,\n            \"Line count should match initial entries\"\n        );\n\n        // Add more entries to the file\n        let additional_entries = get_additional_log_entries();\n        append_to_log_file(&mut file, additional_entries.clone()).unwrap();\n\n        // Process file changes\n        indexer.on_file_change().await?;\n\n        // Verify that all entries are now indexed\n        let total_entries = initial_entries.len() + additional_entries.len();\n        assert_eq!(\n            indexer.current.line, total_entries as u64,\n            \"Line count should match total entries\"\n        );\n\n        // Query all entries\n        let query = QueryBuilder::default().build().unwrap();\n        let results = indexer.index.query(query).unwrap();\n        assert_eq!(\n            results.len(),\n            total_entries,\n            \"Query should return all indexed entries\"\n        );\n\n        // Check for specific new entry\n        let fatal_query = QueryBuilder::default()\n            .level(vec![LoggingLevel::FATAL])\n            .build()?;\n        let fatal_results = indexer.index.query(fatal_query).unwrap();\n        assert_eq!(\n            fatal_results.len(),\n            1,\n            \"Should have 1 FATAL entry from file change\"\n        );\n\n        Ok(())\n    }\n\n    #[tokio::test]\n    async fn test_indexer_with_target_filter() -> anyhow::Result<()> {\n        let entries = get_sample_log_entries();\n        let (_guard, path) = create_test_log_file(entries).unwrap();\n\n        let mut indexer = Indexer::new(path);\n        indexer.build_index().await.unwrap();\n\n        // Query by target\n        let target_query = QueryBuilder::default()\n            .target(vec![\"app::module1\".to_string()])\n            .build()?;\n        let target_results = indexer.index.query(target_query).unwrap();\n\n        assert_eq!(\n            target_results.len(),\n            2,\n            \"Should have 2 entries for app::module1\"\n        );\n\n        // Verify the levels of the filtered results\n        let has_info = target_results.iter().any(|e| e.level == LoggingLevel::INFO);\n        let has_error = target_results\n            .iter()\n            .any(|e| e.level == LoggingLevel::ERROR);\n\n        assert!(has_info, \"app::module1 should have an INFO entry\");\n        assert!(has_error, \"app::module1 should have an ERROR entry\");\n\n        Ok(())\n    }\n\n    #[tokio::test]\n    async fn test_indexer_complex_query() -> anyhow::Result<()> {\n        let entries = get_sample_log_entries();\n        let additional_entries = get_additional_log_entries();\n        let mut all_entries = entries.clone();\n        all_entries.extend(additional_entries.clone());\n\n        let (_guard, path) = create_test_log_file(all_entries).unwrap();\n\n        let mut indexer = Indexer::new(path);\n        indexer.build_index().await.unwrap();\n\n        // Complex query with multiple filters\n        let complex_query = QueryBuilder::default()\n            .level(vec![LoggingLevel::INFO, LoggingLevel::WARN])\n            .target(vec![\"app::module2\".to_string()])\n            .build()?;\n\n        let complex_results = indexer.index.query(complex_query).unwrap();\n        assert_eq!(\n            complex_results.len(),\n            2,\n            \"Complex query should return 2 entries\"\n        );\n\n        // Verify specific entries\n        let has_info = complex_results\n            .iter()\n            .any(|e| e.level == LoggingLevel::INFO);\n        let has_warn = complex_results\n            .iter()\n            .any(|e| e.level == LoggingLevel::WARN);\n\n        assert!(\n            has_info,\n            \"Results should include INFO entry for app::module2\"\n        );\n        assert!(\n            has_warn,\n            \"Results should include WARN entry for app::module2\"\n        );\n\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "backend/tauri/src/logging/manager.rs",
    "content": "use std::{collections::HashMap, ops::Deref, sync::Arc};\n\nuse crate::logging::indexer::Indexer;\nuse anyhow::Context;\nuse camino::{Utf8Path, Utf8PathBuf};\nuse notify::EventKind;\nuse notify_debouncer_full::{\n    DebounceEventResult, DebouncedEvent, Debouncer, RecommendedCache, new_debouncer,\n    notify::{RecommendedWatcher, RecursiveMode},\n};\nuse tokio::{\n    sync::{\n        mpsc::{Receiver, UnboundedSender},\n        oneshot,\n    },\n    task::{JoinHandle, LocalSet},\n};\nuse tokio_util::sync::CancellationToken;\n\nuse super::{LogEntry, Query};\n\n#[derive(Clone)]\npub struct IndexerManager {\n    inner: Arc<IndexerRunnerGuard>,\n}\n\nimpl Deref for IndexerManager {\n    type Target = IndexerRunnerGuard;\n\n    fn deref(&self) -> &Self::Target {\n        &self.inner\n    }\n}\n\nasync fn is_log_file(path: &Utf8Path) -> anyhow::Result<bool> {\n    let metadata = tokio::fs::metadata(path).await?;\n    Ok(metadata.is_file()\n        && path\n            .file_name()\n            .is_some_and(|name| name.to_ascii_lowercase().ends_with(\".log\")))\n}\n\nimpl IndexerManager {\n    pub async fn try_new(logging_dir: Utf8PathBuf) -> anyhow::Result<Self> {\n        let inner = IndexerManagerRunner::new_and_spawn().await;\n        let manager = Self {\n            inner: Arc::new(inner),\n        };\n        manager.watch(&logging_dir).await?;\n        Ok(manager)\n    }\n}\n\n// TODO: only keep latest log file when we detect a serious memory report on it\npub struct IndexerManagerRunner {\n    map: HashMap<Utf8PathBuf, Indexer>,\n    debouncer: Option<Debouncer<RecommendedWatcher, RecommendedCache>>,\n}\n\npub enum IndexerRunnerCmd {\n    /// scan the logging directory for new log files\n    Watch(Utf8PathBuf, oneshot::Sender<anyhow::Result<()>>),\n    /// remove the indexer for the given path\n    // Unwatch(Utf8PathBuf, oneshot::Sender<anyhow::Result<()>>),\n    /// query the indexer for the given path\n    AddLogFile(Utf8PathBuf, oneshot::Sender<anyhow::Result<()>>),\n    RemoveLogFile(Utf8PathBuf, oneshot::Sender<anyhow::Result<()>>),\n    LogFileChanged(Utf8PathBuf, oneshot::Sender<anyhow::Result<()>>),\n    Query(Utf8PathBuf, Query, oneshot::Sender<Option<Vec<LogEntry>>>),\n}\n\npub struct IndexerRunnerGuard {\n    cancel_token: CancellationToken,\n    handle: JoinHandle<()>,\n    tx: tokio::sync::mpsc::UnboundedSender<IndexerRunnerCmd>,\n}\n\nimpl IndexerManagerRunner {\n    pub async fn new_and_spawn() -> IndexerRunnerGuard {\n        let cancel_token = CancellationToken::new();\n        let (handle, rx) = Self::spawn_task(cancel_token.clone());\n        let tx = rx.await.unwrap();\n\n        IndexerRunnerGuard {\n            cancel_token,\n            handle,\n            tx,\n        }\n    }\n\n    fn spawn_task(\n        cancel_token: CancellationToken,\n    ) -> (\n        JoinHandle<()>,\n        tokio::sync::oneshot::Receiver<tokio::sync::mpsc::UnboundedSender<IndexerRunnerCmd>>,\n    ) {\n        let (tx, rx) = oneshot::channel();\n        let handle = tauri::async_runtime::spawn_blocking(move || {\n            let (cmd_tx, mut cmd_rx) = tokio::sync::mpsc::unbounded_channel();\n            let mut runner = Self {\n                map: HashMap::new(),\n                debouncer: None,\n            };\n            let local = LocalSet::new();\n            let cmd_tx_clone = cmd_tx.clone();\n            local.spawn_local(async move {\n                while let Some(cmd) = cmd_rx.recv().await {\n                    runner.run_cmd(&cmd_tx_clone, cmd).await;\n                }\n            });\n            tx.send(cmd_tx).unwrap();\n            tauri::async_runtime::block_on(async {\n                tokio::select! {\n                    _ = cancel_token.cancelled() => {\n                        tracing::info!(\"cancel token triggered, shutting down\");\n                    }\n                    _ = local => {}\n                }\n            });\n        });\n\n        // unwrap the join handle\n        match handle {\n            tauri::async_runtime::JoinHandle::Tokio(handle) => (handle, rx),\n        }\n    }\n\n    async fn run_cmd(&mut self, cmd_tx: &UnboundedSender<IndexerRunnerCmd>, cmd: IndexerRunnerCmd) {\n        match cmd {\n            IndexerRunnerCmd::Watch(path, tx) => {\n                if let Err(err) = self.scan(&path).await {\n                    tx.send(Err(err)).unwrap();\n                    return;\n                }\n                let watcher = self.recommended_watcher(&path).unwrap();\n                let cmd_tx = cmd_tx.clone();\n                nyanpasu_utils::runtime::spawn(Self::spawn_watcher(watcher, cmd_tx));\n                tx.send(Ok(())).unwrap();\n            }\n            IndexerRunnerCmd::Query(path, query, tx) => {\n                let indexer = self.map.get(&path).unwrap();\n                let result = indexer.query(query);\n                tx.send(result).unwrap();\n            }\n            IndexerRunnerCmd::AddLogFile(path, tx) => {\n                let mut indexer = Indexer::new(path.clone());\n                if let Err(err) = indexer.build_index().await {\n                    tx.send(Err(err)).unwrap();\n                    return;\n                }\n                self.map.insert(path, indexer);\n                tx.send(Ok(())).unwrap();\n            }\n            IndexerRunnerCmd::RemoveLogFile(path, tx) => {\n                self.map.remove(&path);\n                tx.send(Ok(())).unwrap();\n            }\n            IndexerRunnerCmd::LogFileChanged(path, tx) => {\n                let indexer = self.map.get_mut(&path).unwrap();\n                if let Err(err) = indexer.on_file_change().await {\n                    tx.send(Err(err)).unwrap();\n                    return;\n                }\n                tx.send(Ok(())).unwrap();\n            }\n        }\n    }\n\n    async fn spawn_watcher(\n        mut watcher: Receiver<Vec<DebouncedEvent>>,\n        cmd_tx: UnboundedSender<IndexerRunnerCmd>,\n    ) {\n        while let Some(events) = watcher.recv().await {\n            for event in events {\n                let path = Utf8Path::from_path(event.paths.first().unwrap()).unwrap();\n                match event.kind {\n                    EventKind::Create(_) => {\n                        if is_log_file(path).await.is_ok_and(|ok| ok) {\n                            tracing::debug!(\"create indexer for {}\", path);\n                            let (tx, rx) = oneshot::channel();\n                            cmd_tx\n                                .send(IndexerRunnerCmd::AddLogFile(path.to_path_buf(), tx))\n                                .unwrap();\n                            match rx.await {\n                                Ok(_) => {\n                                    tracing::debug!(\"indexer for {} created\", path);\n                                }\n                                Err(_err) => {\n                                    tracing::error!(\"failed to create indexer for {}\", path);\n                                }\n                            }\n                        }\n                    }\n                    EventKind::Remove(_) => {\n                        let (tx, rx) = oneshot::channel();\n                        cmd_tx\n                            .send(IndexerRunnerCmd::RemoveLogFile(path.to_path_buf(), tx))\n                            .unwrap();\n                        match rx.await {\n                            Ok(_) => {\n                                tracing::debug!(\"indexer for {} removed\", path);\n                            }\n                            Err(_err) => {\n                                tracing::error!(\"failed to remove indexer for {}\", path);\n                            }\n                        }\n                    }\n                    EventKind::Modify(_) => {\n                        if is_log_file(path).await.is_ok_and(|ok| ok) {\n                            let (tx, rx) = oneshot::channel();\n                            cmd_tx\n                                .send(IndexerRunnerCmd::LogFileChanged(path.to_path_buf(), tx))\n                                .unwrap();\n                            match rx.await {\n                                Ok(_) => {\n                                    tracing::debug!(\"indexer for {} updated\", path);\n                                }\n                                Err(_err) => {\n                                    tracing::error!(\"failed to update indexer for {}\", path);\n                                }\n                            }\n                        }\n                    }\n                    _ => (),\n                }\n            }\n        }\n    }\n\n    #[tracing::instrument(skip(self))]\n    pub async fn scan(&mut self, logging_dir: &Utf8Path) -> anyhow::Result<()> {\n        let mut entries = tokio::fs::read_dir(logging_dir).await?;\n        while let Some(entry) = entries.next_entry().await? {\n            let path = Utf8PathBuf::from_path_buf(entry.path())\n                .map_err(|e| anyhow::anyhow!(\"failed to convert path: {:?}\", e))?;\n            if is_log_file(&path).await? {\n                tracing::debug!(\"create indexer for {}\", path);\n                let mut indexer = Indexer::new(path.clone());\n                indexer.build_index().await?;\n                self.map.insert(path, indexer);\n            }\n        }\n        Ok(())\n    }\n\n    #[tracing::instrument(skip(self))]\n    pub fn recommended_watcher(\n        &mut self,\n        logging_dir: &Utf8Path,\n    ) -> anyhow::Result<Receiver<Vec<DebouncedEvent>>> {\n        let (tx, rx) = tokio::sync::mpsc::channel(10);\n        let mut debouncer = new_debouncer(\n            std::time::Duration::from_secs(2),\n            None,\n            move |events_result: DebounceEventResult| match events_result {\n                Ok(events) => {\n                    let tx = tx.clone();\n                    nyanpasu_utils::runtime::spawn(async move {\n                        if let Err(err) = tx.send(events).await {\n                            tracing::error!(\"failed to send events to channel: {:?}\", err);\n                        }\n                    });\n                }\n                Err(errors) => {\n                    tracing::error!(\n                        \"failed to receive events from logging directory: {:?}\",\n                        errors\n                    );\n                }\n            },\n        )?;\n        debouncer\n            .watch(logging_dir, RecursiveMode::Recursive)\n            .context(\"failed to watch logging directory\")?;\n\n        self.debouncer = Some(debouncer);\n\n        Ok(rx)\n    }\n}\n\nimpl IndexerRunnerGuard {\n    pub async fn watch(&self, logging_dir: &Utf8Path) -> anyhow::Result<()> {\n        let (tx, rx) = oneshot::channel();\n        self.tx\n            .send(IndexerRunnerCmd::Watch(logging_dir.to_path_buf(), tx))\n            .context(\"failed to send watch command\")?;\n        rx.await.context(\"failed to receive watch command\")??;\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "backend/tauri/src/logging/mod.rs",
    "content": "mod indexer;\nmod manager;\n\nconst LOGGING_NS: &str = \"logging\";\nconst LOGGING_DB_PREFIX: &str = \"logging\";\n\nuse anyhow::Context;\nuse camino::Utf8PathBuf;\npub use indexer::*;\n\nuse manager::IndexerManager;\nuse tauri::Manager;\n\npub fn setup<R: tauri::Runtime, M: tauri::Manager<R>>(app: &M) -> anyhow::Result<()> {\n    let app_handle = app.app_handle().clone();\n    // FIXME: this is a background setup, so be careful use this state in ipc. If use state<T> when it is not ready, it will cause panic.\n    nyanpasu_utils::runtime::spawn(async move {\n        let logging_dir = crate::utils::dirs::app_logs_dir()\n            .context(\"failed to get app logs dir\")\n            .unwrap();\n        let logging_dir = Utf8PathBuf::from_path_buf(logging_dir).unwrap();\n        let manager = IndexerManager::try_new(logging_dir)\n            .await\n            .context(\"failed to create indexer manager\")\n            .unwrap();\n        app_handle.manage(manager);\n    });\n\n    Ok(())\n}\n"
  },
  {
    "path": "backend/tauri/src/main.rs",
    "content": "#![cfg_attr(not(debug_assertions), windows_subsystem = \"windows\")]\n\nfn main() {\n    clash_nyanpasu_lib::run().unwrap();\n}\n"
  },
  {
    "path": "backend/tauri/src/server/mod.rs",
    "content": "use anyhow::{Context, Result, anyhow};\nuse axum::{\n    Router,\n    body::Body,\n    extract::Query,\n    http::{HeaderValue, Response, StatusCode},\n    routing::get,\n};\nuse base64::{Engine, prelude::BASE64_STANDARD};\nuse bytes::Bytes;\nuse once_cell::sync::Lazy;\nuse serde::{Deserialize, Serialize};\nuse sha2::{Digest, Sha256};\nuse tokio::io::AsyncWriteExt;\nuse tracing_attributes::instrument;\nuse url::Url;\n\nuse std::{borrow::Cow, path::Path, time::Duration};\n\npub(crate) use crate::utils::candy::get_reqwest_client;\n\npub static SERVER_PORT: Lazy<u16> = Lazy::new(|| port_scanner::request_open_port().unwrap());\n\nconst CACHE_TIMEOUT: Duration = Duration::from_secs(60 * 60 * 24 * 7); // 7 days\n\n#[derive(Debug, Deserialize)]\nstruct CacheIcon {\n    /// should be encoded as base64\n    url: String,\n}\n\n#[derive(Debug, Clone, Deserialize, Serialize)]\nstruct CacheFile<'n> {\n    mime: Cow<'n, str>,\n    bytes: Bytes,\n}\n\nimpl TryFrom<CacheFile<'static>> for (HeaderValue, Bytes) {\n    type Error = anyhow::Error;\n\n    fn try_from(value: CacheFile<'static>) -> Result<Self, Self::Error> {\n        Ok((\n            value\n                .mime\n                .parse::<HeaderValue>()\n                .context(\"failed to parse mime\")?,\n            value.bytes,\n        ))\n    }\n}\n\n// TODO: use Reader instead of Vec\nasync fn read_cache_file(path: &Path) -> Result<(HeaderValue, Bytes)> {\n    let cache_file = tokio::fs::read(path).await?;\n    let cache_file: CacheFile<'static> = postcard::from_bytes(&cache_file)?;\n    cache_file.try_into()\n}\n\n// TODO: use Writer instead of Vec\nasync fn write_cache_file(path: &Path, cache_file: &CacheFile<'_>) -> Result<()> {\n    let mut file = tokio::fs::File::create(path).await?;\n    let cache_file = postcard::to_allocvec(cache_file)?;\n    file.write_all(&cache_file).await?;\n    Ok(())\n}\n\nasync fn remove_cache_file(cache_file: &Path) {\n    if let Err(e) = tokio::fs::remove_file(&cache_file).await {\n        tracing::error!(\"failed to remove cache file: {}\", e);\n    }\n}\n\nasync fn cache_icon_inner(url: &str) -> Result<(HeaderValue, Bytes)> {\n    let url = BASE64_STANDARD.decode(url)?;\n    let url = String::from_utf8_lossy(&url);\n    let url = Url::parse(&url)?;\n    // get filename\n    let hash = Sha256::digest(url.as_str().as_bytes());\n    let cache_dir = crate::utils::dirs::cache_dir()?.join(\"icons\");\n    if !cache_dir.exists() {\n        std::fs::create_dir_all(&cache_dir)?;\n    }\n    // TODO: if face performance issue, abstract a task to schedule cache file removal\n    let now = std::time::SystemTime::now();\n    let outdated_time = now\n        .checked_sub(CACHE_TIMEOUT)\n        .expect(\"cache timeout is too long\");\n    let cache_file = cache_dir.join(format!(\"{hash:x}.bin\"));\n    let meta = tokio::fs::metadata(&cache_file).await.ok();\n    match meta {\n        Some(meta) if meta.modified().is_ok_and(|t| t < outdated_time) => {\n            tracing::debug!(\"cache file is outdate, removing it\");\n            remove_cache_file(&cache_file).await;\n        }\n        Some(_) => {\n            let span = tracing::span!(tracing::Level::DEBUG, \"read_cache_file\", path = ?cache_file);\n            let _enter = span.enter();\n            match read_cache_file(&cache_file).await {\n                Ok((mime, bytes)) => return Ok((mime, bytes)),\n                Err(e) => {\n                    tracing::error!(\"failed to read cache file: {}\", e);\n                    remove_cache_file(&cache_file).await;\n                }\n            }\n        }\n        _ => (),\n    }\n    let client = get_reqwest_client()?;\n    let response = client.get(url).send().await?.error_for_status()?;\n    let mime = response\n        .headers()\n        .get(\"content-type\")\n        .ok_or(anyhow!(\"no content-type\"))?\n        .to_str()?\n        .to_string();\n\n    let bytes = response.bytes().await?;\n    let data = CacheFile {\n        mime: Cow::Owned(mime),\n        bytes,\n    };\n    if let Err(e) = write_cache_file(&cache_file, &data).await {\n        tracing::error!(\"failed to write cache file: {}\", e);\n    }\n    Ok(data\n        .try_into()\n        .expect(\"It's impossible to fail, if failed, it must a bug, or memory corruption\"))\n}\n\n#[tracing_attributes::instrument]\nasync fn cache_icon(query: Query<CacheIcon>) -> Response<Body> {\n    match cache_icon_inner(&query.url).await {\n        Ok((mime, bytes)) => {\n            let mut response = Response::new(Body::from(bytes));\n            response.headers_mut().insert(\"content-type\", mime);\n            response\n        }\n        Err(e) => {\n            tracing::error!(\"{}\", e);\n            let mut response = Response::new(Body::from(e.to_string()));\n            *response.status_mut() = StatusCode::BAD_REQUEST;\n            response\n        }\n    }\n}\n\n#[derive(Deserialize)]\nstruct TrayIconReq {\n    mode: crate::core::tray::icon::TrayIcon,\n}\n\nasync fn tray_icon(query: Query<TrayIconReq>) -> Response<Body> {\n    let mode = query.mode;\n    let icon = crate::core::tray::icon::get_raw_icon(mode);\n    let mut response = Response::new(Body::from(icon));\n    response\n        .headers_mut()\n        .insert(\"content-type\", \"image/png\".parse().unwrap());\n    response\n}\n\n#[instrument]\npub async fn run(port: u16) -> std::io::Result<()> {\n    let app = Router::new()\n        .route(\"/cache/icon\", get(cache_icon))\n        .route(\"/tray/icon\", get(tray_icon));\n    let listener = tokio::net::TcpListener::bind(format!(\"127.0.0.1:{port}\")).await?;\n    tracing::debug!(\n        \"internal http server listening on {}\",\n        listener.local_addr()?\n    );\n    axum::serve(listener, app).await\n}\n"
  },
  {
    "path": "backend/tauri/src/setup.rs",
    "content": "//! Setup logic for the app\nuse anyhow::Context;\n\npub fn setup<R: tauri::Runtime, M: tauri::Manager<R>>(app: &M) -> Result<(), anyhow::Error> {\n    let app_handle = app.app_handle().clone();\n    #[cfg(target_os = \"windows\")]\n    super::shutdown_hook::setup_shutdown_hook(move || {\n        tracing::info!(\"Shutdown hook triggered, exiting app...\");\n        app_handle.exit(0);\n    })\n    .context(\"Failed to setup the shutdown hook\")?;\n\n    // FIXME: this is a background setup, so be careful use this state in ipc.\n    // crate::logging::setup(app).context(\"Failed to setup logging\")?;\n    Ok(())\n}\n"
  },
  {
    "path": "backend/tauri/src/shutdown_hook.rs",
    "content": "//! a shutdown handler for Windows\n\nuse atomic_enum::atomic_enum;\nuse once_cell::sync::OnceCell;\nuse windows_core::{Error, w};\nuse windows_sys::Win32::{\n    Foundation::{HINSTANCE, HWND, LPARAM, WPARAM},\n    System::{\n        LibraryLoader::GetModuleHandleW,\n        Shutdown::{ShutdownBlockReasonCreate, ShutdownBlockReasonDestroy},\n    },\n    UI::WindowsAndMessaging::{\n        CreateWindowExW, DefWindowProcW, DispatchMessageW, GetMessageW, MSG, PostMessageW,\n        RegisterClassExW, TranslateMessage, WM_CLOSE, WM_ENDSESSION, WM_QUERYENDSESSION,\n        WNDCLASSEXW, WS_EX_NOACTIVATE, WS_EX_TOOLWINDOW,\n    },\n};\n\nstatic SHUTDOWN_HOOK_INSTANCE: OnceCell<std::sync::mpsc::Sender<()>> = OnceCell::new();\n\n#[atomic_enum]\n#[derive(PartialEq, Eq)]\npub enum ShutdownState {\n    Idle,\n    CleaningUp,\n    ReadyForShutdown,\n}\n\nstatic SHUTDOWN_STATE: AtomicShutdownState = AtomicShutdownState::new(ShutdownState::Idle);\n\npub fn setup_shutdown_hook(f: impl Fn() + Send + Sync + 'static) -> anyhow::Result<()> {\n    if SHUTDOWN_HOOK_INSTANCE.get().is_some() {\n        anyhow::bail!(\"Shutdown hook already set\");\n    }\n    let (initd_tx, initd_rx) = oneshot::channel();\n    let handle = std::thread::spawn(move || setup_shutdown_hook_inner(f, initd_tx));\n    if let Err(oneshot::RecvError) = initd_rx.recv() {\n        handle\n            .join()\n            .map_err(|_| anyhow::anyhow!(\"Failed to join the shutdown hook thread\"))??;\n    }\n    Ok(())\n}\n\nstruct WindowHandle {\n    hwnd: HWND,\n    h_instance: HINSTANCE,\n}\n\nimpl Drop for WindowHandle {\n    fn drop(&mut self) {\n        unsafe {\n            tracing::debug!(\"Destroying window handle...\");\n            // Post a message to the window to tell it to exit\n            PostMessageW(self.hwnd, WM_CLOSE, 0, 0);\n        }\n    }\n}\n\nunsafe extern \"system\" fn callback(hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM) -> isize {\n    let is_ready = SHUTDOWN_STATE.load(std::sync::atomic::Ordering::Relaxed);\n    match msg {\n        WM_QUERYENDSESSION | WM_ENDSESSION if is_ready == ShutdownState::Idle => {\n            tracing::info!(\"Shutdown hook triggered, received WM_QUERYENDSESSION\");\n            if let Some(tx) = SHUTDOWN_HOOK_INSTANCE.get() {\n                tracing::info!(\"Blocking shutdown for cleanup...\");\n                let reason = w!(\"Clash Nyanpasu is cleaning up...\");\n                if unsafe { ShutdownBlockReasonCreate(hwnd, reason.as_ptr()) } == 0 {\n                    let err = Error::from_win32();\n                    tracing::error!(\"Failed to create shutdown block reason: {err}\");\n                }\n                tx.send(()).unwrap();\n                while SHUTDOWN_STATE.load(std::sync::atomic::Ordering::Relaxed)\n                    != ShutdownState::ReadyForShutdown\n                {\n                    std::thread::sleep(std::time::Duration::from_millis(10));\n                }\n                tracing::info!(\"Shutdown hook is ready for shutdown\");\n                if unsafe { ShutdownBlockReasonDestroy(hwnd) } == 0 {\n                    let err = Error::from_win32();\n                    tracing::error!(\"Failed to destroy shutdown block reason: {err}\");\n                }\n            }\n            0\n        }\n        WM_QUERYENDSESSION | WM_ENDSESSION if is_ready == ShutdownState::CleaningUp => {\n            loop {\n                if SHUTDOWN_STATE.load(std::sync::atomic::Ordering::Relaxed)\n                    == ShutdownState::ReadyForShutdown\n                {\n                    break;\n                }\n                std::thread::sleep(std::time::Duration::from_millis(10));\n            }\n            0\n        }\n        _ => unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) },\n    }\n}\n\n/// Only called on tauri cleanup thread finished\npub fn set_ready_for_shutdown() {\n    SHUTDOWN_STATE.store(\n        ShutdownState::ReadyForShutdown,\n        std::sync::atomic::Ordering::Relaxed,\n    );\n}\n\nfn setup_shutdown_hook_inner(\n    f: impl Fn() + Send + Sync + 'static,\n    initd_tx: oneshot::Sender<()>,\n) -> anyhow::Result<()> {\n    let class_name = w!(\"TAURI_SHUTDOWN_HOOK\");\n\n    let (tx, rx) = std::sync::mpsc::channel::<()>();\n    std::thread::spawn(move || {\n        while let Ok(()) = rx.recv() {\n            f();\n        }\n    });\n\n    SHUTDOWN_HOOK_INSTANCE.set(tx).unwrap();\n\n    let h_instance = unsafe { GetModuleHandleW(std::ptr::null()) };\n    if h_instance.is_null() {\n        let err = Error::from_win32();\n        anyhow::bail!(\"Failed to get module handle: {err}\");\n    }\n\n    let mut window_class_ex = unsafe { std::mem::zeroed::<WNDCLASSEXW>() };\n    window_class_ex.cbSize = std::mem::size_of::<WNDCLASSEXW>() as u32;\n    window_class_ex.lpszClassName = class_name.as_ptr();\n    window_class_ex.lpfnWndProc = Some(callback);\n    window_class_ex.hInstance = h_instance;\n\n    unsafe {\n        if RegisterClassExW(&window_class_ex) == 0 {\n            let err = Error::from_win32();\n            anyhow::bail!(\"Failed to register window class: {err}\");\n        }\n    }\n\n    let window_name = w!(\"TAURI_SHUTDOWN_HOOK_WINDOW\");\n    let hidden_window = unsafe {\n        CreateWindowExW(\n            WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE,\n            class_name.as_ptr(),\n            window_name.as_ptr(),\n            0,\n            0,\n            0,\n            0,\n            0,\n            std::ptr::null_mut(),\n            std::ptr::null_mut(),\n            h_instance,\n            std::ptr::null_mut(),\n        )\n    };\n    if hidden_window.is_null() {\n        let err = Error::from_win32();\n        anyhow::bail!(\"Failed to create hidden window: {err}\");\n    }\n\n    let window_handle = WindowHandle {\n        hwnd: hidden_window,\n        h_instance,\n    };\n\n    if let Err(e) = initd_tx.send(()) {\n        anyhow::bail!(\"Failed to send initd signal: {e}\");\n    }\n\n    let mut msg = unsafe { std::mem::zeroed::<MSG>() };\n    unsafe {\n        loop {\n            let result = GetMessageW(&mut msg, window_handle.hwnd, 0, 0);\n            if result > 0 {\n                TranslateMessage(&msg);\n                DispatchMessageW(&msg);\n            } else {\n                let err = Error::from_win32();\n                tracing::error!(\n                    \"GetMessageW failed with {result}, shutdown hook thread exiting: {err}\"\n                );\n                break;\n            }\n        }\n    }\n\n    Ok(())\n}\n"
  },
  {
    "path": "backend/tauri/src/utils/candy.rs",
    "content": "use super::{config::NyanpasuReqwestProxyExt, dirs::app_logs_dir};\nuse anyhow::Result;\nuse chrono::Local;\nuse glob::glob;\nuse std::{path::Path, time::Duration};\nuse url::Url;\nuse zip::{ZipWriter, write::SimpleFileOptions};\n\npub fn collect_logs(target_path: &Path) -> Result<()> {\n    let logs_dir = app_logs_dir()?;\n    let now = Local::now().format(\"%Y-%m-%d\");\n    let globstr = format!(\"{}/*.{}.app.log\", logs_dir.to_str().unwrap(), now);\n    let mut paths = Vec::new();\n    for entry in glob(&globstr)? {\n        match entry {\n            Ok(path) => paths.push(path),\n            Err(e) => return Err(e.into()),\n        }\n    }\n    let file = std::fs::File::create(target_path)?;\n    let mut zip = ZipWriter::new(file);\n    for path in paths {\n        let file_name = path.file_name().unwrap().to_str().unwrap();\n        zip.start_file(file_name, SimpleFileOptions::default())?;\n        let mut file = std::fs::File::open(path)?;\n        std::io::copy(&mut file, &mut zip)?;\n    }\n    zip.finish()?;\n    Ok(())\n}\n\n// TODO: 添加自定义 User-Agent 等配置，说白了就是重构一下 prfitem 的那坨代码\npub fn get_reqwest_client() -> Result<reqwest::Client> {\n    let builder = reqwest::ClientBuilder::new();\n    let app_version = super::dirs::get_app_version();\n    let client = builder\n        .swift_set_nyanpasu_proxy()\n        .user_agent(format!(\"clash-nyanpasu/{app_version}\"))\n        .build()?;\n    Ok(client)\n}\n\npub const INTERNAL_MIRRORS: &[&str] = &[\n    \"https://github.com/\",\n    \"https://gh-proxy.com/\",\n    // too many restrictions, not recommended\n    // \"https://gh.idayer.com/\",\n];\n\npub fn parse_gh_url(mirror: &str, path: &str) -> Result<Url, url::ParseError> {\n    if mirror.contains(\"github.com\") && !path.starts_with('/') {\n        Url::parse(path)\n    } else {\n        let mut url = Url::parse(mirror)?;\n        url.set_path(path);\n        Ok(url)\n    }\n}\n\n#[async_trait::async_trait]\npub trait ReqwestSpeedTestExt {\n    async fn mirror_speed_test<'a>(\n        &self,\n        mirrors: &'a [&'a str],\n        path: &'a str,\n    ) -> Result<Vec<(&'a str, f64)>>;\n}\n\n#[async_trait::async_trait]\nimpl ReqwestSpeedTestExt for reqwest::Client {\n    async fn mirror_speed_test<'a>(\n        &self,\n        mirrors: &'a [&'a str],\n        path: &'a str,\n    ) -> Result<Vec<(&'a str, f64)>> {\n        let results = futures::future::join_all(mirrors.iter().map(|&mirror| {\n            let client = self;\n            async move {\n                let start = tokio::time::Instant::now();\n                // if mirror is github.com, we should use it directly\n                let url = parse_gh_url(mirror, path)?;\n                tracing::debug!(\"Testing {}\", url.as_str());\n                let _ =\n                    tokio::time::timeout(Duration::from_secs(3), client.get(url.as_str()).send())\n                        .await; // warm up\n                let result: Result<reqwest::Response, anyhow::Error> =\n                    tokio::time::timeout(Duration::from_secs(3), client.get(url).send())\n                        .await\n                        .map_err(anyhow::Error::msg)\n                        .and_then(|v| v.map_err(anyhow::Error::msg))\n                        .and_then(|v| v.error_for_status().map_err(anyhow::Error::msg));\n                match result {\n                    Ok(response) => {\n                        let content_length = response.content_length().unwrap_or(0) as f64;\n                        // should read all the response body to get the correct speed\n                        match response.bytes().await {\n                            Ok(_) => {\n                                let elapsed = start.elapsed().as_secs_f64();\n                                let speed = content_length / elapsed;\n                                Ok((mirror, speed))\n                            }\n                            Err(e) => {\n                                tracing::warn!(\"test mirror {} failed: {}\", mirror, e);\n                                Ok((mirror, 0.0))\n                            }\n                        }\n                    }\n                    Err(e) => {\n                        tracing::warn!(\"test mirror {} failed: {}\", mirror, e);\n                        Ok((mirror, 0.0))\n                    }\n                }\n            }\n        }))\n        .await;\n        let collected_result: Result<Vec<_>, anyhow::Error> = results.into_iter().collect();\n        let mut results = collected_result?;\n        results.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());\n\n        Ok(results)\n    }\n}\n\nmod test {\n    #[allow(unused_imports)]\n    use super::*;\n\n    #[tokio::test]\n    #[allow(clippy::needless_return)] // a bug in clippy\n    async fn test_mirror_speed_test() {\n        let client = reqwest::Client::builder().user_agent(\n            \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36\"\n        ).build().unwrap();\n        let results = client\n            .mirror_speed_test(\n                INTERNAL_MIRRORS,\n                \"https://raw.githubusercontent.com/simonw/github-large-file-test/master/1.5mb.txt\",\n            )\n            .await\n            .unwrap();\n        println!(\"{results:?}\");\n    }\n}\n"
  },
  {
    "path": "backend/tauri/src/utils/collect.rs",
    "content": "use std::{borrow::Cow, collections::HashMap};\n\n#[cfg(windows)]\nuse std::os::windows::process::CommandExt;\n\nuse crate::consts::{BUILD_INFO, BuildInfo};\nuse humansize::{BINARY, SizeFormatter};\nuse nyanpasu_utils::core::{ClashCoreType, CoreType};\nuse serde::Serialize;\nuse sysinfo::System;\n\n#[derive(Debug, Serialize, specta::Type)]\npub struct DeviceInfo<'a> {\n    /// Device name, such as \"Intel Core i5-8250U CPU @ 1.60GHz x 8\"\n    pub cpu: Vec<Cow<'a, str>>,\n    /// GPU name, such as \"Intel UHD Graphics 620 (Kabylake GT2)\"\n    // pub gpu: Cow<'a, str>,\n    /// Memory size in bytes\n    pub memory: Cow<'a, str>,\n}\n\n#[derive(Debug, Serialize, specta::Type)]\npub struct EnvInfo<'a> {\n    pub os: Cow<'a, str>,\n    pub arch: Cow<'a, str>,\n    pub core: CoreInfo<'a>,\n    pub device: DeviceInfo<'a>,\n    pub build_info: Cow<'a, BuildInfo>,\n    // TODO: add service info\n    // pub service_info: xxx\n}\n\npub type CoreInfo<'a> = HashMap<Cow<'a, str>, Cow<'a, str>>;\n\npub fn collect_envs<'a>() -> Result<EnvInfo<'a>, std::io::Error> {\n    let mut system = sysinfo::System::new_all();\n    system.refresh_all();\n\n    let device = DeviceInfo {\n        cpu: {\n            let mut cpus: Vec<(u64, &str, i32)> = Vec::new();\n            for cpu in system.cpus().iter() {\n                let item = cpus.iter_mut().find(|(_, name, _)| name == &cpu.brand());\n                match item {\n                    Some((_, _, count)) => *count += 1,\n                    None => cpus.push((cpu.frequency(), cpu.brand(), 1)),\n                }\n            }\n            cpus.iter()\n                .map(|(freq, name, count)| {\n                    Cow::Owned(format!(\n                        \"{} @ {:.2}GHz x {}\",\n                        name,\n                        *freq as f64 / 1000.0,\n                        count\n                    ))\n                })\n                .collect()\n        },\n        memory: Cow::Owned(SizeFormatter::new(system.total_memory(), BINARY).to_string()),\n    };\n\n    let mut core = HashMap::new();\n    for c in CoreType::get_supported_cores() {\n        let name: &str = c.as_ref();\n\n        let mut command = std::process::Command::new(\n            super::dirs::get_data_or_sidecar_path(name)\n                .map_err(|e| std::io::Error::other(e.to_string()))?,\n        );\n        command.args(if matches!(c, CoreType::Clash(ClashCoreType::ClashRust)) {\n            [\"-V\"]\n        } else {\n            [\"-v\"]\n        });\n        #[cfg(windows)]\n        let command = command.creation_flags(0x08000000);\n        let output = command.output().expect(\"failed to execute sidecar command\");\n        let stdout = String::from_utf8_lossy(&output.stdout);\n        core.insert(\n            Cow::Borrowed(name),\n            Cow::Owned(stdout.replace(\"\\n\\n\", \" \").trim().to_owned()),\n        );\n    }\n    Ok(EnvInfo {\n        os: Cow::Owned(\n            format!(\n                \"{} {}\",\n                System::long_os_version().unwrap_or(\"\".to_string()),\n                System::kernel_version().unwrap_or(\"\".to_string()),\n            )\n            .trim()\n            .to_owned(),\n        ),\n        arch: Cow::Owned(System::cpu_arch()),\n        core,\n        device,\n        build_info: Cow::Borrowed(&BUILD_INFO),\n    })\n}\n"
  },
  {
    "path": "backend/tauri/src/utils/config.rs",
    "content": "use anyhow::Result;\nuse sysproxy::Sysproxy;\n\nuse crate::config::Config;\n\npub fn get_self_proxy() -> Result<String> {\n    let port = Config::verge()\n        .latest()\n        .verge_mixed_port\n        .unwrap_or(Config::clash().data().get_mixed_port());\n\n    let proxy_scheme = format!(\"http://127.0.0.1:{port}\");\n    Ok(proxy_scheme)\n}\n\npub fn get_system_proxy() -> Result<Option<String>> {\n    let p = Sysproxy::get_system_proxy()?;\n    if p.enable {\n        let proxy_scheme = format!(\"http://{}:{}\", p.host, p.port);\n        return Ok(Some(proxy_scheme));\n    }\n\n    Ok(None)\n}\n\npub fn get_current_clash_mode() -> String {\n    Config::clash()\n        .latest()\n        .0\n        .get(\"mode\")\n        .map(|val| val.as_str().unwrap_or(\"rule\"))\n        .unwrap_or(\"rule\")\n        .to_owned()\n}\n\npub trait NyanpasuReqwestProxyExt {\n    fn swift_set_proxy(self, url: &str) -> Self;\n\n    fn swift_set_nyanpasu_proxy(self) -> Self;\n}\n\nimpl NyanpasuReqwestProxyExt for reqwest::ClientBuilder {\n    fn swift_set_proxy(self, url: &str) -> Self {\n        let mut builder = self;\n        if let Ok(proxy) = reqwest::Proxy::http(url) {\n            builder = builder.proxy(proxy);\n        }\n        if let Ok(proxy) = reqwest::Proxy::https(url) {\n            builder = builder.proxy(proxy);\n        }\n        if let Ok(proxy) = reqwest::Proxy::all(url) {\n            builder = builder.proxy(proxy);\n        }\n        builder\n    }\n\n    // TODO: 修改成按枚举配置\n    fn swift_set_nyanpasu_proxy(self) -> Self {\n        let mut builder = self;\n        if let Ok(proxy) = get_self_proxy() {\n            builder = builder.swift_set_proxy(&proxy);\n        }\n        if let Ok(Some(proxy)) = get_system_proxy() {\n            builder = builder.swift_set_proxy(&proxy);\n        }\n        builder\n    }\n}\n"
  },
  {
    "path": "backend/tauri/src/utils/dialog.rs",
    "content": "#![allow(dead_code)]\n\nuse rfd::{MessageButtons, MessageDialog, MessageDialogResult, MessageLevel};\nuse rust_i18n::t;\n\npub fn panic_dialog(msg: &str) {\n    let msg = format!(\"{}\\n\\n{}\", msg, t!(\"dialog.panic\"));\n    MessageDialog::new()\n        .set_level(MessageLevel::Error)\n        .set_title(\"Clash Nyanpasu Crash\")\n        .set_description(msg.as_str())\n        .set_buttons(MessageButtons::Ok)\n        .show();\n}\n\npub fn migrate_dialog(msg: &str) -> bool {\n    matches!(\n        MessageDialog::new()\n            .set_level(MessageLevel::Warning)\n            .set_title(\"Clash Nyanpasu Migration\")\n            .set_buttons(MessageButtons::YesNo)\n            .set_description(msg)\n            .show(),\n        MessageDialogResult::Yes\n    )\n}\n\npub fn error_dialog<T: Into<String>>(msg: T) {\n    MessageDialog::new()\n        .set_level(MessageLevel::Error)\n        .set_title(\"Clash Nyanpasu Error\")\n        .set_description(msg.into())\n        .set_buttons(MessageButtons::Ok)\n        .show();\n}\n\npub fn warning_dialog<T: Into<String>>(msg: T) {\n    MessageDialog::new()\n        .set_level(MessageLevel::Warning)\n        .set_title(\"Clash Nyanpasu Warning\")\n        .set_description(msg.into())\n        .set_buttons(MessageButtons::Ok)\n        .show();\n}\n\npub fn ask_dialog<T: Into<String>>(msg: T) -> bool {\n    matches!(\n        MessageDialog::new()\n            .set_level(MessageLevel::Info)\n            .set_title(\"Clash Nyanpasu\")\n            .set_buttons(MessageButtons::YesNo)\n            .set_description(msg.into())\n            .show(),\n        MessageDialogResult::Yes\n    )\n}\n"
  },
  {
    "path": "backend/tauri/src/utils/dirs.rs",
    "content": "use crate::{core::handle, log_err};\nuse anyhow::Result;\nuse fs_err as fs;\nuse nyanpasu_utils::dirs::{suggest_config_dir, suggest_data_dir};\nuse once_cell::sync::Lazy;\nuse std::{borrow::Cow, path::PathBuf};\nuse tauri::{Env, utils::platform::resource_dir};\n\n#[cfg(not(feature = \"verge-dev\"))]\n#[allow(unused)]\nconst PREVIOUS_APP_NAME: &str = \"clash-verge\";\n#[cfg(feature = \"verge-dev\")]\nconst PREVIOUS_APP_NAME: &str = \"clash-verge-dev\";\n#[cfg(not(feature = \"verge-dev\"))]\npub const APP_NAME: &str = \"clash-nyanpasu\";\n#[cfg(feature = \"verge-dev\")]\npub const APP_NAME: &str = \"clash-nyanpasu-dev\";\n\n/// App Dir placeholder\n/// It is used to create the config and data dir in the filesystem\n/// For windows, the style should be similar to `C:/Users/nyanapasu/AppData/Roaming/Clash Nyanpasu`\n/// For macos, it should be similar to `/Users/nyanpasu/Library/Application Support/Clash Nyanpasu`\n/// For other platforms, it should be similar to `/home/nyanpasu/.config/clash-nyanpasu`\npub static APP_DIR_PLACEHOLDER: Lazy<Cow<'static, str>> = Lazy::new(|| {\n    use convert_case::{Case, Casing};\n    if cfg!(any(target_os = \"windows\", target_os = \"macos\")) {\n        Cow::Owned(APP_NAME.to_case(Case::Title))\n    } else {\n        Cow::Borrowed(APP_NAME)\n    }\n});\n\npub const CLASH_CFG_GUARD_OVERRIDES: &str = \"clash-guard-overrides.yaml\";\npub const NYANPASU_CONFIG: &str = \"nyanpasu-config.yaml\";\npub const PROFILE_YAML: &str = \"profiles.yaml\";\npub const STORAGE_DB: &str = \"storage.db\";\n\npub static APP_VERSION: &str = env!(\"NYANPASU_VERSION\");\n\npub fn get_app_version() -> &'static str {\n    APP_VERSION\n}\n\n#[cfg(target_os = \"windows\")]\npub fn get_portable_flag() -> bool {\n    *crate::consts::IS_PORTABLE\n}\n\npub fn app_config_dir() -> Result<PathBuf> {\n    let path: Option<PathBuf> = {\n        #[cfg(target_os = \"windows\")]\n        {\n            if get_portable_flag() {\n                let app_dir = app_install_dir()?;\n                Some(app_dir.join(\".config\").join(APP_NAME))\n            } else if let Ok(Some(path)) = super::winreg::get_app_dir() {\n                Some(path)\n            } else {\n                None\n            }\n        }\n        #[cfg(not(target_os = \"windows\"))]\n        {\n            None\n        }\n    };\n\n    match path {\n        Some(path) => Ok(path),\n        None => suggest_config_dir(&APP_DIR_PLACEHOLDER)\n            .ok_or(anyhow::anyhow!(\"failed to get the app config dir\")),\n    }\n    .and_then(|dir| {\n        create_dir_all(&dir)?;\n        Ok(dir)\n    })\n}\n\npub fn app_data_dir() -> Result<PathBuf> {\n    let path: Option<PathBuf> = {\n        #[cfg(target_os = \"windows\")]\n        {\n            if get_portable_flag() {\n                let app_dir = app_install_dir()?;\n                Some(app_dir.join(\".data\").join(APP_NAME))\n            } else {\n                None\n            }\n        }\n        #[cfg(not(target_os = \"windows\"))]\n        {\n            None\n        }\n    };\n\n    match path {\n        Some(path) => Ok(path),\n        None => suggest_data_dir(&APP_DIR_PLACEHOLDER)\n            .ok_or(anyhow::anyhow!(\"failed to get the app data dir\")),\n    }\n    .and_then(|dir| {\n        create_dir_all(&dir)?;\n        Ok(dir)\n    })\n}\n\n/// get the verge app home dir\n#[deprecated(\n    since = \"1.6.0\",\n    note = \"should use self::app_config_dir or self::app_data_dir instead\"\n)]\npub fn app_home_dir() -> Result<PathBuf> {\n    if cfg!(feature = \"verge-dev\") {\n        return Ok(dirs::home_dir()\n            .ok_or(anyhow::anyhow!(\"failed to get the app home dir\"))?\n            .join(\".config\")\n            .join(APP_NAME));\n    }\n\n    #[cfg(target_os = \"windows\")]\n    {\n        use crate::utils::winreg::get_app_dir;\n        if !get_portable_flag() {\n            let reg_app_dir = get_app_dir()?;\n            if let Some(reg_app_dir) = reg_app_dir {\n                return Ok(reg_app_dir);\n            }\n            return Ok(dirs::home_dir()\n                .ok_or(anyhow::anyhow!(\"failed to get app home dir\"))?\n                .join(\".config\")\n                .join(APP_NAME));\n        }\n        Ok((app_install_dir()?).join(\".config\").join(APP_NAME))\n    }\n\n    #[cfg(not(target_os = \"windows\"))]\n    Ok(dirs::home_dir()\n        .ok_or(anyhow::anyhow!(\"failed to get the app home dir\"))?\n        .join(\".config\")\n        .join(APP_NAME))\n}\n\n/// get the resources dir\npub fn app_resources_dir() -> Result<PathBuf> {\n    let handle = handle::Handle::global();\n    let app_handle = handle.app_handle.lock();\n    if let Some(app_handle) = app_handle.as_ref() {\n        let res_dir = resource_dir(app_handle.package_info(), &Env::default())\n            .map_err(|_| anyhow::anyhow!(\"failed to get the resource dir\"))?\n            .join(\"resources\");\n        return Ok(res_dir);\n    };\n    Err(anyhow::anyhow!(\"failed to get the resource dir\"))\n}\n\n// /// Cache dir, it safe to clean up\n// pub fn cache_dir() -> Result<PathBuf> {\n//     let mut dir = dirs::cache_dir()\n//         .ok_or(anyhow::anyhow!(\"failed to get the cache dir\"))?\n//         .join(APP_DIR_PLACEHOLDER.as_ref());\n//     if cfg!(windows) {\n//         dir.push(\"cache\");\n//     }\n//     if !dir.exists() {\n//         fs::create_dir_all(&dir)?;\n//     }\n//     Ok(dir)\n// }\n\n/// App install dir, sidecars should placed here\npub fn app_install_dir() -> Result<PathBuf> {\n    let exe = tauri::utils::platform::current_exe()?;\n    let exe = dunce::canonicalize(exe)?;\n    let dir = exe\n        .parent()\n        .ok_or(anyhow::anyhow!(\"failed to get the app install dir\"))?;\n    Ok(PathBuf::from(dir))\n}\n\n/// profiles dir\npub fn app_profiles_dir() -> Result<PathBuf> {\n    let path = app_config_dir()?.join(\"profiles\");\n    static INIT: std::sync::Once = std::sync::Once::new();\n    INIT.call_once(|| {\n        log_err!(create_dir_all(&path));\n    });\n    Ok(path)\n}\n\n/// logs dir\npub fn app_logs_dir() -> Result<PathBuf> {\n    let path = app_data_dir()?.join(\"logs\");\n    static INIT: std::sync::Once = std::sync::Once::new();\n    INIT.call_once(|| {\n        log_err!(create_dir_all(&path));\n    });\n    Ok(path)\n}\n\npub fn clash_guard_overrides_path() -> Result<PathBuf> {\n    Ok(app_config_dir()?.join(CLASH_CFG_GUARD_OVERRIDES))\n}\n\npub fn nyanpasu_config_path() -> Result<PathBuf> {\n    Ok(app_config_dir()?.join(NYANPASU_CONFIG))\n}\n\npub fn profiles_path() -> Result<PathBuf> {\n    Ok(app_config_dir()?.join(PROFILE_YAML))\n}\n\npub fn storage_path() -> Result<PathBuf> {\n    Ok(app_data_dir()?.join(STORAGE_DB))\n}\n\npub fn clash_pid_path() -> Result<PathBuf> {\n    Ok(app_data_dir()?.join(\"clash.pid\"))\n}\n\npub fn cache_dir() -> Result<PathBuf> {\n    let path = app_data_dir()?.join(\"cache\");\n    static INIT: std::sync::Once = std::sync::Once::new();\n    INIT.call_once(|| {\n        log_err!(create_dir_all(&path));\n    });\n    Ok(path)\n}\n\npub fn tray_icons_path(mode: &str) -> Result<PathBuf> {\n    let icons_dir = app_config_dir()?.join(\"icons\");\n    static INIT: std::sync::Once = std::sync::Once::new();\n    INIT.call_once(|| {\n        log_err!(create_dir_all(&icons_dir));\n    });\n    Ok(icons_dir.join(format!(\"{mode}.png\")))\n}\n\n#[cfg(windows)]\n#[deprecated(since = \"1.6.0\", note = \"should use nyanpasu_utils::dirs mod instead\")]\npub fn service_log_file() -> Result<PathBuf> {\n    use chrono::Local;\n\n    let log_dir = app_logs_dir()?.join(\"service\");\n\n    let local_time = Local::now().format(\"%Y-%m-%d-%H%M\").to_string();\n    let log_file = format!(\"{local_time}.log\");\n    let log_file = log_dir.join(log_file);\n\n    let _ = std::fs::create_dir_all(&log_dir);\n\n    Ok(log_file)\n}\n\npub fn path_to_str(path: &PathBuf) -> Result<&str> {\n    let path_str = path\n        .as_os_str()\n        .to_str()\n        .ok_or(anyhow::anyhow!(\"failed to get path from {:?}\", path))?;\n    Ok(path_str)\n}\n\npub fn get_single_instance_placeholder() -> Result<String> {\n    let cfg_dir = crate::utils::dirs::app_config_dir()?;\n    #[cfg(windows)]\n    {\n        // Try to get user SID for better user isolation\n        match crate::utils::winreg::get_current_user_sid() {\n            Ok(sid) => {\n                // Use session-local namespace and include app name + user SID to ensure per-user uniqueness\n                return Ok(format!(\"Local\\\\{}-{}\", APP_NAME, sid));\n            }\n            Err(_) => {\n                // Fallback to config dir hashing if SID retrieval fails\n                use std::{\n                    collections::hash_map::DefaultHasher,\n                    hash::{Hash, Hasher},\n                };\n                let mut hasher = DefaultHasher::new();\n                cfg_dir.to_string_lossy().hash(&mut hasher);\n                let hash = hasher.finish();\n                return Ok(format!(\"Local\\\\{}-{:x}\", APP_NAME, hash));\n            }\n        }\n    }\n    #[cfg(not(windows))]\n    {\n        return Ok(cfg_dir.join(\"instance.lock\").to_string_lossy().to_string());\n    }\n}\n\nfn create_dir_all(dir: &PathBuf) -> Result<(), std::io::Error> {\n    let meta = fs::metadata(dir);\n    if let Ok(meta) = meta {\n        if !meta.is_dir() {\n            fs_err::remove_file(dir)?;\n        } else {\n            return Ok(());\n        }\n    }\n    fs_extra::dir::create_all(dir, false).map_err(|e| {\n        std::io::Error::other(format!(\"failed to create dir: {:?}, kind: {:?}\", e, e.kind))\n    })?;\n    Ok(())\n}\n\npub fn get_data_or_sidecar_path(binary_name: impl AsRef<str>) -> Result<PathBuf> {\n    let binary_name = binary_name.as_ref();\n    let data_dir = app_data_dir()?;\n    let path = data_dir.join(if cfg!(windows) && !binary_name.ends_with(\".exe\") {\n        format!(\"{binary_name}.exe\")\n    } else {\n        binary_name.to_string()\n    });\n    if path.exists() {\n        return Ok(data_dir);\n    }\n\n    let install_dir = app_install_dir()?;\n    let path = install_dir.join(if cfg!(windows) && !binary_name.ends_with(\".exe\") {\n        format!(\"{binary_name}.exe\")\n    } else {\n        binary_name.to_string()\n    });\n\n    Ok(path)\n}\n\n#[cfg(any(target_os = \"macos\", target_os = \"linux\"))]\npub fn check_core_permission(core: &nyanpasu_utils::core::CoreType) -> anyhow::Result<bool> {\n    #[cfg(target_os = \"macos\")]\n    const ROOT_GROUP: &str = \"admin\";\n    #[cfg(target_os = \"linux\")]\n    const ROOT_GROUP: &str = \"root\";\n\n    use anyhow::Context;\n    use nix::unistd::{Gid, Group as NixGroup, Uid, User};\n    use std::os::unix::fs::MetadataExt;\n\n    let core_path =\n        crate::core::clash::core::find_binary_path(core).context(\"clash core not found\")?;\n    let metadata = std::fs::metadata(&core_path).context(\"failed to get core metadata\")?;\n    let uid = metadata.uid();\n    let gid = metadata.gid();\n    let user = User::from_uid(Uid::from_raw(uid)).ok().flatten();\n    let group = NixGroup::from_gid(Gid::from_raw(gid)).ok().flatten();\n    if let (Some(user), Some(group)) = (user, group) {\n        if user.name == \"root\" && group.name == ROOT_GROUP {\n            return Ok(true);\n        }\n    }\n    Ok(false)\n}\n\nmod test {\n    #[test]\n    #[ignore]\n    fn test_dir_placeholder() {\n        let placeholder = super::APP_DIR_PLACEHOLDER.clone();\n        if cfg!(windows) {\n            assert_eq!(placeholder, \"Clash Nyanpasu\");\n        } else {\n            assert_eq!(placeholder, \"clash-nyanpasu\");\n        }\n    }\n}\n"
  },
  {
    "path": "backend/tauri/src/utils/dock.rs",
    "content": "#[cfg(target_os = \"macos\")]\npub mod macos {\n    use objc2_app_kit::{NSApplication, NSApplicationActivationPolicy};\n    use objc2_foundation::MainThreadMarker;\n    use std::cell::Cell;\n    thread_local! {\n        static MARK: Cell<MainThreadMarker> = Cell::new(MainThreadMarker::new().unwrap());\n    }\n\n    pub fn show_dock_icon() {\n        let app = NSApplication::sharedApplication(MARK.get());\n        app.setActivationPolicy(NSApplicationActivationPolicy::Regular);\n        unsafe {\n            app.activate();\n        }\n    }\n\n    pub fn hide_dock_icon() {\n        let app = NSApplication::sharedApplication(MARK.get());\n        app.setActivationPolicy(NSApplicationActivationPolicy::Accessory);\n    }\n}\n"
  },
  {
    "path": "backend/tauri/src/utils/downloader.rs",
    "content": "/// Downloader is a utility to download file with parallel requests and progress bar.\n/// TODO: use &str instead of String to avoid unnecessary allocation\n/// TODO: support no RANGE support server\n/// TODO: add dynamic increase chunks features\n///\nuse futures::StreamExt;\nuse num_cpus;\nuse parking_lot::RwLock;\nuse reqwest::{Client, IntoUrl};\nuse serde::Serialize;\nuse std::{fs::File as StdFile, io::Write, sync::Arc, time};\nuse tempfile::tempfile;\nuse thiserror::Error;\nuse tokio::{\n    fs::File,\n    io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt},\n    sync::{\n        Semaphore,\n        mpsc::{self, Sender},\n    },\n    time::sleep,\n};\nuse url::Url;\n\npub struct Downloader<F: Fn(DownloaderState)> {\n    inner: RwLock<DownloaderInner>,\n    client: Client,\n    url: Arc<Url>,\n    event_callback: Option<F>,\n}\n\nimpl<F: Fn(DownloaderState)> std::fmt::Debug for Downloader<F> {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        let inner = self.inner.read();\n        f.debug_struct(\"Downloader\")\n            .field(\"inner\", &inner)\n            .field(\"client\", &self.client)\n            .field(\"url\", &self.url)\n            .finish()\n    }\n}\n\n#[derive(Debug)]\nstruct DownloaderInner {\n    state: DownloaderState,\n    file: Option<File>,\n    total_size: u64,\n    semaphore: Arc<Semaphore>,\n    chunks: Vec<Arc<RwLock<ChunkThread>>>,\n    mode: DownloadMode,\n}\n\nimpl Default for DownloaderInner {\n    fn default() -> Self {\n        Self {\n            state: DownloaderState::default(),\n            file: None,\n            total_size: 0,\n            semaphore: Arc::new(Semaphore::new(1)),\n            chunks: Vec::new(),\n            mode: DownloadMode::default(),\n        }\n    }\n}\n\npub struct DownloaderBuilder<F: Fn(DownloaderState)> {\n    client: Option<Client>,\n    url: Option<Url>,\n    file: Option<File>,\n    event_callback: Option<F>,\n}\n\nimpl<F: Fn(DownloaderState)> DownloaderBuilder<F> {\n    pub fn new() -> Self {\n        Self {\n            client: None,\n            url: None,\n            file: None,\n            event_callback: None,\n        }\n    }\n\n    pub fn set_client(mut self, client: Client) -> Self {\n        self.client = Some(client);\n        self\n    }\n\n    pub fn set_url<U: IntoUrl>(mut self, url: U) -> Result<Self, DownloaderError> {\n        self.url = Some(url.into_url()?);\n        Ok(self)\n    }\n\n    pub fn set_file(mut self, file: File) -> Self {\n        self.file = Some(file);\n        self\n    }\n\n    pub fn set_event_callback(mut self, callback: F) -> Self {\n        self.event_callback = Some(callback);\n        self\n    }\n\n    pub fn build(self) -> Result<Downloader<F>, DownloaderError> {\n        let client = self.client.unwrap_or_default();\n        let url = self\n            .url\n            .ok_or(DownloaderError::Other(\"URL is not set\".to_string()))?;\n        let nums = num_cpus::get();\n        Ok(Downloader {\n            inner: RwLock::new(DownloaderInner {\n                file: self.file,\n                semaphore: Arc::new(Semaphore::new(nums)),\n                chunks: Vec::with_capacity(nums),\n                ..Default::default()\n            }),\n            event_callback: self.event_callback,\n            client,\n            url: Arc::new(url),\n        })\n    }\n}\n\n#[derive(Debug, Serialize, Default, Clone, specta::Type)]\n#[serde(rename_all = \"snake_case\")]\npub enum DownloaderState {\n    #[default]\n    Idle,\n    Downloading,\n    WaitingForMerge,\n    Merging,\n    Failed(String),\n    Finished,\n}\n\n#[derive(Debug, Serialize, Default)]\npub enum DownloadMode {\n    SingleThread,\n    #[default]\n    MultiThread,\n}\n\n#[derive(Debug, Serialize, specta::Type)]\npub struct DownloadStatus {\n    pub state: DownloaderState,\n    pub downloaded: u64,\n    pub total: u64,\n    pub speed: f64,\n    pub chunks: Vec<ChunkStatus>,\n    pub now: u64,\n}\n\n#[derive(Debug, Serialize, specta::Type)]\n#[allow(private_interfaces)]\npub struct ChunkStatus {\n    pub state: ChunkThreadState,\n    pub start: usize,\n    pub end: usize,\n    pub downloaded: usize,\n    pub speed: f64,\n}\n\nenum ChunkThreadEvent {\n    DecreaseSemaphore(DecreaseSemaphoreReason),\n    Finish,\n}\n\nenum DecreaseSemaphoreReason {\n    Reason(String),\n    Cause(anyhow::Error),\n}\n\n#[derive(Debug, Clone, Serialize, specta::Type)]\nenum ChunkThreadState {\n    Idle,\n    Downloading,\n    Finished,\n}\n\n#[derive(Debug)]\nstruct ChunkThread {\n    client: Client,\n    sender: Sender<ChunkThreadEvent>,\n    semaphore: Arc<Semaphore>,\n    file: StdFile,\n    url: Arc<Url>,\n    pub state: ChunkThreadState,\n    pub start: usize,\n    pub end: usize,\n    pub downloaded: usize,\n    pub speed: f64,\n}\n\n#[derive(Error, Debug)]\npub enum DownloaderError {\n    #[error(\"Failed to perform a request, reason: {0}\")]\n    RequestFailed(#[from] reqwest::Error),\n\n    #[error(\"Failed to download file, reason: {0}\")]\n    DownloadFailed(#[from] anyhow::Error),\n    #[error(\"Failed to write file\")]\n    WriteFailed(#[from] std::io::Error),\n    #[error(\"Failed to confirm file size\")]\n    ConfirmSizeFailed,\n\n    #[error(\"Other error: {0}\")]\n    Other(String),\n}\n\nimpl Serialize for DownloaderError {\n    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>\n    where\n        S: serde::Serializer,\n    {\n        format!(\"{self}\").serialize(serializer)\n    }\n}\n\n#[allow(unused)]\nimpl<F: Fn(DownloaderState)> Downloader<F> {\n    pub fn set_file(&self, file: File) {\n        let mut inner = self.inner.write();\n        inner.file = Some(file);\n    }\n\n    fn dispatch_event(&self, state: DownloaderState) {\n        {\n            let mut inner = self.inner.write();\n            inner.state = state.clone();\n        }\n        if let Some(callback) = &self.event_callback {\n            callback(state);\n        }\n    }\n\n    // get file status, get remote content size, and return server filename\n    async fn confirm_file_status(&self) -> Result<(String, u64), DownloaderError> {\n        let response = self\n            .client\n            .head(self.url.clone().as_str())\n            .send()\n            .await?\n            .error_for_status()?;\n        let headers = response.headers();\n        // TODO: fallback to single thread download\n        // TODO：考慮到相當一部分服務端不發送 ACCEPT_RANGES，因此需要動態嘗試來確認\n        if headers.get(reqwest::header::ACCEPT_RANGES).is_none() {\n            tracing::warn!(\n                \"Server  does not provide ACCEPT_RANGES header. Though dynamic confirm whether server support RANGE requests is required, we have to use multi-thread download mode directly.\"\n            )\n        }\n        if headers\n            .get(reqwest::header::ACCEPT_RANGES)\n            .is_some_and(|v| v == \"none\")\n        {\n            return Err(DownloaderError::Other(\n                \"Server does not support RANGE requests\".to_string(),\n            ));\n        }\n        let total_size = headers\n            .get(reqwest::header::CONTENT_LENGTH)\n            .and_then(|v| v.to_str().ok())\n            .and_then(|v| v.parse().ok())\n            .unwrap_or(0);\n        if total_size == 0 {\n            return Err(DownloaderError::ConfirmSizeFailed);\n        }\n        {\n            let mut inner = self.inner.write();\n            inner.total_size = total_size;\n        }\n        let filename = headers\n            .get(reqwest::header::CONTENT_DISPOSITION)\n            .and_then(|v| v.to_str().ok())\n            .and_then(|v| {\n                let parts: Vec<&str> = v.split(';').collect();\n                parts\n                    .iter()\n                    .find(|part| part.trim().starts_with(\"filename=\"))\n                    .map(|part| {\n                        part.trim()\n                            .split('=')\n                            .next_back()\n                            .unwrap()\n                            .trim_matches(['\"', ';', '\\''])\n                    })\n            })\n            .unwrap_or(self.url.path_segments().unwrap().next_back().unwrap());\n        Ok((filename.to_string(), total_size))\n    }\n\n    async fn merge_chunks(&self) -> Result<(), DownloaderError> {\n        {\n            let inner = self.inner.read();\n            if !matches!(inner.state, DownloaderState::WaitingForMerge) {\n                return Err(DownloaderError::Other(\n                    \"Download is not finished\".to_string(),\n                ));\n            }\n            if inner.file.is_none() {\n                return Err(DownloaderError::Other(\"File is not set\".to_string()));\n            }\n        }\n        self.dispatch_event(DownloaderState::Merging);\n        let mut file = {\n            let mut inner = self.inner.write();\n            inner.file.take().unwrap()\n        };\n        {\n            let chunks = self.get_cloned_chunks();\n            for part in &chunks {\n                let mut part_file = {\n                    let part = part.read();\n                    File::from_std(part.file.try_clone().unwrap())\n                };\n                part_file.seek(tokio::io::SeekFrom::Start(0)).await?;\n                let mut buffer = vec![0u8; 1024 * 1024];\n                loop {\n                    let read = part_file.read(&mut buffer).await?;\n                    if read == 0 {\n                        break;\n                    }\n                    file.write_all(&buffer[..read]).await?;\n                }\n            }\n            file.flush().await?;\n        }\n        self.dispatch_event(DownloaderState::Finished);\n        Ok(())\n    }\n\n    async fn download(&self) -> Result<(), DownloaderError> {\n        let mut total_size = {\n            let inner = self.inner.read();\n            if inner.file.is_none() {\n                return Err(DownloaderError::Other(\"File is not set\".to_string()));\n            }\n            inner.total_size\n        };\n\n        if total_size == 0 {\n            let (_, size) = self.confirm_file_status().await?;\n            total_size = size;\n        }\n\n        let mut file = {\n            let mut inner = self.inner.write();\n            inner.file.take().unwrap()\n        };\n        file.set_len(total_size).await?;\n        {\n            let mut inner = self.inner.write();\n            inner.file = Some(file);\n        }\n        let counts = {\n            let inner = self.inner.read();\n            inner.semaphore.available_permits() as u64\n        };\n        let chunk_size = total_size / counts;\n        let (tx, mut rx) = mpsc::channel(10);\n        self.dispatch_event(DownloaderState::Downloading);\n\n        for chunk in 0..counts {\n            let start = (chunk * chunk_size) as usize;\n            let end = if chunk == counts - 1 {\n                total_size as usize\n            } else {\n                start + (chunk_size as usize) - 1\n            };\n\n            let thread = {\n                let inner = self.inner.read();\n                Arc::new(RwLock::new(ChunkThread::try_new(\n                    self.client.clone(),\n                    tx.clone(),\n                    inner.semaphore.clone(),\n                    start,\n                    end,\n                    self.url.clone(),\n                )?))\n            };\n            let thread_clone = thread.clone();\n            tokio::spawn(async move {\n                thread_clone.start().await;\n            });\n            {\n                let mut inner = self.inner.write();\n                inner.chunks.push(thread);\n            }\n        }\n\n        // TODO: 根據情況嘗試恢復 semaphore 數目\n        let mut downloaded = 0;\n        let mut total_permits = counts;\n        while let Some(event) = rx.recv().await {\n            match event {\n                ChunkThreadEvent::Finish => {\n                    downloaded += 1;\n                    if downloaded == counts {\n                        {\n                            let mut inner = self.inner.write();\n                            inner.semaphore.close(); // 關閉 semaphore\n                        }\n                        self.dispatch_event(DownloaderState::WaitingForMerge);\n                        break;\n                    }\n                }\n                ChunkThreadEvent::DecreaseSemaphore(reason) => {\n                    total_permits -= 1;\n                    // 儅 semaphore 為 0 時，表示無可用下載綫程，説明文件無法下載\n                    if total_permits == 0 {\n                        let mut inner = self.inner.write();\n                        inner.semaphore.close(); // 關閉 semaphore\n                        match reason {\n                            DecreaseSemaphoreReason::Cause(e) => {\n                                return Err(DownloaderError::DownloadFailed(e));\n                            }\n                            DecreaseSemaphoreReason::Reason(e) => {\n                                return Err(DownloaderError::Other(e));\n                            }\n                        }\n                    }\n                }\n            }\n        }\n        // 合并文件\n        self.merge_chunks().await?;\n        Ok(())\n    }\n\n    pub async fn start(&self) -> Result<(), DownloaderError> {\n        let result = self.download().await;\n        match result {\n            Ok(_) => Ok(()),\n            Err(e) => {\n                self.dispatch_event(DownloaderState::Failed(format!(\"{e}\")));\n                Err(e)\n            }\n        }\n    }\n    fn get_cloned_chunks(&self) -> Vec<Arc<RwLock<ChunkThread>>> {\n        let inner = self.inner.read();\n        inner.chunks.to_vec()\n    }\n\n    fn get_total_size(&self) -> u64 {\n        let inner = self.inner.read();\n        inner.total_size\n    }\n\n    pub fn get_current_status(&self) -> DownloadStatus {\n        let mut downloaded = 0;\n        let mut speed = 0.0;\n        let inner = self.inner.read();\n        let total = inner.total_size;\n        let mut chunks = Vec::with_capacity(inner.chunks.len());\n        for chunk in &inner.chunks {\n            let chunk = chunk.read();\n            downloaded += chunk.downloaded as u64;\n            speed += chunk.speed;\n            chunks.push(ChunkStatus {\n                start: chunk.start,\n                end: chunk.end,\n                downloaded: chunk.downloaded,\n                speed: chunk.speed,\n                state: chunk.state.clone(),\n            });\n        }\n        DownloadStatus {\n            downloaded,\n            total,\n            speed,\n            state: inner.state.clone(),\n            chunks,\n            now: chrono::Utc::now().timestamp() as u64,\n        }\n    }\n}\n\n#[async_trait::async_trait]\ntrait SafeChunkThread {\n    fn dispatch_event(&self, state: ChunkThreadState);\n    async fn download_chunk(&self) -> Result<(), anyhow::Error>;\n    async fn start(&self);\n}\n\nimpl ChunkThread {\n    pub fn try_new(\n        client: Client,\n        sender: Sender<ChunkThreadEvent>,\n        semaphore: Arc<Semaphore>,\n        start: usize,\n        end: usize,\n        url: Arc<Url>,\n    ) -> std::io::Result<Self> {\n        let file = tempfile()?;\n        Ok(Self {\n            client,\n            sender,\n            semaphore,\n            state: ChunkThreadState::Idle,\n            start,\n            end,\n            file,\n            url,\n            downloaded: 0,\n            speed: 0.0,\n        })\n    }\n}\n\n#[async_trait::async_trait]\nimpl SafeChunkThread for RwLock<ChunkThread> {\n    fn dispatch_event(&self, state: ChunkThreadState) {\n        let mut thread = self.write();\n        tracing::debug!(\"ChunkThread state: {:?}\", state);\n        if matches!(state, ChunkThreadState::Finished) {\n            thread.speed = 0.0;\n        }\n        thread.state = state;\n    }\n\n    async fn download_chunk(&self) -> Result<(), anyhow::Error> {\n        {\n            let thread = self.read();\n            tracing::debug!(\"start downloading chunk: {}-{}\", thread.start, thread.end);\n        }\n        self.dispatch_event(ChunkThreadState::Downloading);\n        let response = {\n            let (client, url, start, end) = {\n                let thread = self.read();\n                let client = thread.client.clone();\n                (client, thread.url.clone(), thread.start, thread.end)\n            };\n            client\n                .get(url.as_str())\n                .header(reqwest::header::RANGE, format!(\"bytes={start}-{end}\"))\n                .send()\n                .await?\n                .error_for_status()?\n        };\n        let mut stream = response.bytes_stream();\n        let mut tick = time::Instant::now();\n        while let Some(chunk) = stream.next().await {\n            let chunk = chunk?;\n            {\n                let mut thread = self.write();\n                let elapsed = tick.elapsed().as_secs_f64();\n                // 防止除零错误和异常大的速度值\n                if elapsed > 0.0 {\n                    thread.speed = chunk.len() as f64 / elapsed;\n                } else {\n                    thread.speed = 0.0;\n                }\n                thread.file.write_all(&chunk)?;\n                thread.downloaded += chunk.len();\n                tracing::debug!(\n                    \"ChunkThread downloading chunk size: {}, current downloaded pos: {}, speed: {}\",\n                    chunk.len(),\n                    thread.downloaded,\n                    thread.speed\n                );\n            }\n            tick = time::Instant::now();\n        }\n        {\n            let mut thread = self.write();\n            thread.file.flush()?;\n        }\n        Ok(())\n    }\n\n    async fn start(&self) {\n        let mut attempts = 0;\n        let semaphore = {\n            let thread = self.read();\n            thread.semaphore.clone()\n        };\n        loop {\n            let result = {\n                let _permit = match semaphore.acquire().await {\n                    Ok(permit) => permit,\n                    Err(_) => {\n                        tracing::debug!(\"ChunkThread semaphore is released\");\n                        break; // semaphore 已經被釋放\n                    }\n                };\n                self.download_chunk().await\n            };\n            match result {\n                Ok(_) => {\n                    self.dispatch_event(ChunkThreadState::Finished);\n                    let sender = {\n                        let thread = self.read();\n                        thread.sender.clone()\n                    };\n                    sender.send(ChunkThreadEvent::Finish).await.unwrap();\n                    break;\n                }\n                Err(_) if attempts < 3 => {\n                    tracing::debug!(\"ChunkThread download failed, retrying...\");\n                    attempts += 1;\n                    self.dispatch_event(ChunkThreadState::Idle);\n                    sleep(time::Duration::from_secs(1)).await;\n                }\n                Err(e) => {\n                    tracing::debug!(\"ChunkThread download failed: {}\", e);\n                    self.dispatch_event(ChunkThreadState::Idle);\n                    let sender = {\n                        let thread = self.read();\n                        thread.sender.clone()\n                    };\n                    sender\n                        .send(ChunkThreadEvent::DecreaseSemaphore(\n                            DecreaseSemaphoreReason::Cause(e),\n                        ))\n                        .await\n                        .unwrap();\n                    semaphore.forget_permits(1); // 釋放自身的 semaphore\n                    attempts = 0;\n                }\n            }\n        }\n    }\n}\n\n#[allow(unused)]\nmod test {\n    use super::*;\n    use tokio::fs::File as TokioFile;\n\n    #[test_log::test(tokio::test)]\n    #[ignore]\n    async fn test_downloader() {\n        use md5::{Digest, Md5};\n\n        let file = TokioFile::create(\"QQ9.7.17.29225.exe\").await.unwrap();\n        let tick = time::Instant::now();\n        let on_event = |state: DownloaderState| {\n            println!(\"{state:?}\");\n            match state {\n                DownloaderState::Failed(e) => {\n                    panic!(\"{}\", e);\n                }\n                DownloaderState::Finished => {\n                    println!(\"Download finished\");\n                }\n                _ => {}\n            }\n        };\n        let client = Client::builder()\n            .user_agent(\n                \"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0\",\n            )\n            .build()\n            .unwrap();\n        let url = \"https://dldir1.qq.com/qqfile/qq/PCQQ9.7.17/QQ9.7.17.29225.exe\";\n        let head = client.head(url).send().await.unwrap();\n        let md5crc = head\n            .headers()\n            .get(\"X-COS-META-MD5\")\n            .unwrap()\n            .to_str()\n            .unwrap();\n        let mut downloader = Arc::new(\n            DownloaderBuilder::new()\n                // .set_url(\"http://hkg.download.datapacket.com/100mb.bin\")\n                .set_url(url)\n                .unwrap()\n                .set_client(client)\n                .set_file(file)\n                .set_event_callback(on_event)\n                .build()\n                .unwrap(),\n        );\n        let downloader_clone = downloader.clone();\n        let barrier = Arc::new(std::sync::Barrier::new(2));\n        let barrier_clone = barrier.clone();\n        std::thread::spawn(move || {\n            std::thread::sleep(std::time::Duration::from_millis(10));\n            loop {\n                let status = downloader_clone.get_current_status();\n                println!(\"{status:#?}\");\n                if matches!(\n                    status.state,\n                    DownloaderState::Finished | DownloaderState::Failed(_)\n                ) {\n                    println!(\"Download finished\");\n                    break;\n                }\n                std::thread::sleep(std::time::Duration::from_millis(100));\n            }\n            barrier_clone.wait();\n        });\n        tokio::time::timeout(std::time::Duration::from_secs(30), downloader.start())\n            .await\n            .unwrap()\n            .unwrap();\n        println!(\"Time elapsed: {:?}\", tick.elapsed());\n        barrier.wait();\n        drop(downloader);\n        // check file md5\n        let mut hasher = Md5::new();\n        let mut file = StdFile::open(\"QQ9.7.17.29225.exe\").unwrap();\n        let n = std::io::copy(&mut file, &mut hasher).unwrap();\n        let hash = hasher.finalize();\n        assert_eq!(hex::encode_upper(hash), md5crc.to_uppercase());\n    }\n}\n"
  },
  {
    "path": "backend/tauri/src/utils/help.rs",
    "content": "use crate::config::nyanpasu::ExternalControllerPortStrategy;\nuse anyhow::{Context, Result, anyhow, bail};\nuse display_info::DisplayInfo;\nuse fast_image_resize::{\n    FilterType, PixelType, ResizeAlg, ResizeOptions, Resizer,\n    images::{Image, ImageRef},\n};\nuse fs_err as fs;\nuse image::{ColorType, ImageEncoder, ImageReader, codecs::png::PngEncoder};\nuse nanoid::nanoid;\nuse serde::{Serialize, de::DeserializeOwned};\nuse serde_yaml::{Mapping, Value};\nuse std::{\n    io::{BufWriter, Cursor},\n    path::{Path, PathBuf},\n    str::FromStr,\n};\nuse tauri::{AppHandle, Manager, process::current_binary};\nuse tauri_plugin_shell::ShellExt;\nuse tracing::{debug, warn};\nuse tracing_attributes::instrument;\n\nuse crate::trace_err;\nuse tauri_plugin_opener::OpenerExt;\n\n/// read data from yaml as struct T\npub fn read_yaml<T: DeserializeOwned, P: AsRef<Path>>(path: P) -> Result<T> {\n    let path = path.as_ref();\n    if !path.exists() {\n        bail!(\"file not found \\\"{}\\\"\", path.display());\n    }\n\n    let yaml_str = fs::read_to_string(path)\n        .with_context(|| format!(\"failed to read the file \\\"{}\\\"\", path.display()))?;\n\n    serde_yaml::from_str::<T>(&yaml_str).with_context(|| {\n        format!(\n            \"failed to read the file with yaml format \\\"{}\\\"\",\n            path.display()\n        )\n    })\n}\n\n/// read mapping from yaml fix #165\npub fn read_merge_mapping(path: &PathBuf) -> Result<Mapping> {\n    let mut val: Value = read_yaml(path)?;\n    val.apply_merge()\n        .with_context(|| format!(\"failed to apply merge \\\"{}\\\"\", path.display()))?;\n\n    Ok(val\n        .as_mapping()\n        .ok_or(anyhow!(\n            \"failed to transform to yaml mapping \\\"{}\\\"\",\n            path.display()\n        ))?\n        .to_owned())\n}\n\n/// save the data to the file\n/// can set `prefix` string to add some comments\npub fn save_yaml<T: Serialize, P: AsRef<Path>>(\n    path: P,\n    data: &T,\n    prefix: Option<&str>,\n) -> Result<()> {\n    let path = path.as_ref();\n    let data_str = serde_yaml::to_string(data)?;\n\n    let yaml_str = match prefix {\n        Some(prefix) => format!(\"{prefix}\\n\\n{data_str}\"),\n        None => data_str,\n    };\n\n    let path_str = path.as_os_str().to_string_lossy().to_string();\n    fs::write(path, yaml_str.as_bytes())\n        .with_context(|| format!(\"failed to save file \\\"{path_str}\\\"\"))\n}\n\nconst ALPHABET: [char; 62] = [\n    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i',\n    'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B',\n    'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U',\n    'V', 'W', 'X', 'Y', 'Z',\n];\n\n/// generate the uid\npub fn get_uid(prefix: &str) -> String {\n    let id = nanoid!(11, &ALPHABET);\n    format!(\"{prefix}{id}\")\n}\n\n/// parse the string\n/// xxx=123123; => 123123\npub fn parse_str<T: FromStr>(target: &str, key: &str) -> Option<T> {\n    target.split(';').map(str::trim).find_map(|s| {\n        let mut parts = s.splitn(2, '=');\n        match (parts.next(), parts.next()) {\n            (Some(k), Some(v)) if k == key => v.parse::<T>().ok(),\n            _ => None,\n        }\n    })\n}\n\n/// open file\n/// use vscode by default\npub fn open_file(app: tauri::AppHandle, path: PathBuf) -> Result<()> {\n    #[cfg(target_os = \"macos\")]\n    let code = \"Visual Studio Code\";\n    #[cfg(windows)]\n    let code = \"code.cmd\";\n    #[cfg(all(not(windows), not(target_os = \"macos\")))]\n    let code = \"code\";\n\n    let shell = app.shell();\n\n    trace_err!(\n        match which::which(code) {\n            Ok(code_path) => {\n                log::debug!(target: \"app\", \"find VScode `{}`\", code_path.display());\n                #[cfg(not(windows))]\n                {\n                    crate::utils::open::with(path, code)\n                }\n                #[cfg(windows)]\n                {\n                    use std::ffi::OsString;\n                    let mut buf = OsString::with_capacity(path.as_os_str().len() + 2);\n                    buf.push(\"\\\"\");\n                    buf.push(path.as_os_str());\n                    buf.push(\"\\\"\");\n\n                    open::with_detached(buf, code)\n                }\n            }\n            Err(err) => {\n                log::error!(target: \"app\", \"Can't find VScode `{err:?}`\");\n                // default open\n                app.opener()\n                    .open_url(path.to_string_lossy().to_string(), None::<String>)\n                    .map_err(std::io::Error::other)\n            }\n        },\n        \"Can't open file\"\n    );\n\n    Ok(())\n}\n\npub fn get_system_locale() -> String {\n    tauri_plugin_os::locale().unwrap_or(\"en-US\".to_string())\n}\n\npub fn mapping_to_i18n_key(locale_key: &str) -> &'static str {\n    if locale_key.starts_with(\"zh-TW\") {\n        \"zh-TW\"\n    } else if locale_key.starts_with(\"zh-\") {\n        \"zh-CN\"\n    } else {\n        \"en\"\n    }\n}\n\npub fn get_clash_external_port(\n    strategy: &ExternalControllerPortStrategy,\n    port: u16,\n) -> anyhow::Result<u16> {\n    match strategy {\n        ExternalControllerPortStrategy::Fixed => {\n            if !port_scanner::local_port_available(port) {\n                bail!(\"Port {} is not available\", port);\n            }\n        }\n        ExternalControllerPortStrategy::Random | ExternalControllerPortStrategy::AllowFallback => {\n            if ExternalControllerPortStrategy::AllowFallback == *strategy\n                && port_scanner::local_port_available(port)\n            {\n                return Ok(port);\n            }\n            let new_port = port_scanner::request_open_port()\n                .ok_or_else(|| anyhow!(\"Can't find an open port\"))?;\n            return Ok(new_port);\n        }\n    }\n    Ok(port)\n}\n\npub fn resize_tray_image(img: &[u8], scale_factor: f64) -> Result<Vec<u8>> {\n    let img = ImageReader::new(Cursor::new(img))\n        .with_guessed_format()?\n        .decode()?;\n    let width = img.width();\n    let height = img.height();\n    let src_pixels = img.into_rgba8().into_raw();\n    let src_image = ImageRef::new(width, height, &src_pixels, PixelType::U8x4)\n        .context(\"failed to parse image\")?;\n\n    // Create container for data of destination image\n    let size = (32_f64 * scale_factor).round() as u32; // 32px is the base tray size as the dpi is 96\n    let dst_width = size;\n    let dst_height = size;\n    let mut dst_image = Image::new(dst_width, dst_height, src_image.pixel_type());\n\n    // Create Resizer instance and resize source image\n    // into buffer of destination image\n    let mut resizer = Resizer::new();\n    let resizer_options = ResizeOptions {\n        algorithm: ResizeAlg::Convolution(FilterType::Lanczos3),\n        ..Default::default()\n    };\n    resizer\n        .resize(&src_image, &mut dst_image, &resizer_options)\n        .context(\"failed to resize image\")?;\n\n    // Extract raw pixel data from the destination image\n    let dst_image_data = dst_image.buffer().to_vec();\n\n    // Write destination image as PNG-file\n    let mut result_buf = BufWriter::new(Vec::new());\n    PngEncoder::new(&mut result_buf).write_image(\n        &dst_image_data,\n        dst_width,\n        dst_height,\n        ColorType::Rgba8.into(),\n    )?;\n    Ok(result_buf.into_inner()?)\n}\n\n#[instrument]\npub fn get_max_scale_factor() -> f64 {\n    match DisplayInfo::all() {\n        Ok(displays) => {\n            let mut scale_factor = 0.0;\n            debug!(\"displays: {:?}\", displays);\n            for display in displays {\n                if display.scale_factor > scale_factor {\n                    scale_factor = display.scale_factor;\n                }\n            }\n            scale_factor as f64\n        }\n        Err(err) => {\n            warn!(\"failed to get display info: {:?}\", err);\n            1.0_f64\n        }\n    }\n}\n\n#[instrument(skip(app_handle))]\npub fn cleanup_processes(app_handle: &AppHandle) {\n    let _ = super::resolve::save_window_state(app_handle, true);\n    super::resolve::resolve_reset();\n    let widget_manager = app_handle.state::<crate::widget::WidgetManager>();\n    let _ = nyanpasu_utils::runtime::block_on(async {\n        if let Err(e) = widget_manager.stop().await {\n            log::error!(\"failed to stop widget manager: {e:?}\");\n        };\n        crate::core::CoreManager::global().stop_core().await\n    });\n    #[cfg(windows)]\n    crate::shutdown_hook::set_ready_for_shutdown();\n}\n\n#[instrument(skip(app_handle))]\npub fn quit_application(app_handle: &AppHandle) {\n    app_handle.exit(0);\n}\n\n#[instrument(skip(app_handle))]\npub fn restart_application(app_handle: &AppHandle) {\n    cleanup_processes(app_handle);\n    let env = app_handle.env();\n    let path = current_binary(&env).unwrap();\n    let arg = std::env::args().collect::<Vec<String>>();\n    let mut args = vec![\"launch\".to_string(), \"--\".to_string()];\n    // filter out the first arg\n    if arg.len() > 1 {\n        args.extend(arg.iter().skip(1).cloned());\n    }\n    tracing::info!(\"restart app: {:#?} with args: {:#?}\", path, args);\n    std::process::Command::new(path)\n        .args(args)\n        .spawn()\n        .expect(\"application failed to start\");\n    app_handle.exit(0);\n    std::process::exit(0);\n}\n\n#[macro_export]\nmacro_rules! error {\n    ($result: expr) => {\n        log::error!(target: \"app\", \"{:?}\", $result);\n    };\n}\n\n#[macro_export]\nmacro_rules! log_err {\n    ($result: expr) => {\n        if let Err(err) = $result {\n            log::error!(target: \"app\", \"{:#?}\", err);\n        }\n    };\n\n    ($result: expr, $label: expr) => {\n        if let Err(err) = $result {\n            log::error!(target: \"app\", \"{}: {:#?}\", $label, err);\n        }\n    };\n}\n\n#[macro_export]\nmacro_rules! dialog_err {\n    ($result: expr) => {\n        if let Err(err) = $result {\n            $crate::utils::dialog::error_dialog(format!(\"{:?}\", err));\n        }\n    };\n\n    ($result: expr, $err_str: expr) => {\n        if let Err(_) = $result {\n            $crate::utils::dialog::error_dialog($err_str.into());\n        }\n    };\n}\n\n#[macro_export]\nmacro_rules! trace_err {\n    ($result: expr, $err_str: expr) => {\n        if let Err(err) = $result {\n            log::trace!(target: \"app\", \"{}, err {:?}\", $err_str, err);\n        }\n    }\n}\n\n#[test]\nfn test_parse_value() {\n    let test_1 = \"upload=111; download=2222; total=3333; expire=444\";\n    let test_2 = \"attachment; filename=Clash.yaml\";\n\n    assert_eq!(parse_str::<usize>(test_1, \"upload\").unwrap(), 111);\n    assert_eq!(parse_str::<usize>(test_1, \"download\").unwrap(), 2222);\n    assert_eq!(parse_str::<usize>(test_1, \"total\").unwrap(), 3333);\n    assert_eq!(parse_str::<usize>(test_1, \"expire\").unwrap(), 444);\n    assert_eq!(\n        parse_str::<String>(test_2, \"filename\").unwrap(),\n        format!(\"Clash.yaml\")\n    );\n\n    assert_eq!(parse_str::<usize>(test_1, \"aaa\"), None);\n    assert_eq!(parse_str::<usize>(test_1, \"upload1\"), None);\n    assert_eq!(parse_str::<usize>(test_1, \"expire1\"), None);\n    assert_eq!(parse_str::<usize>(test_2, \"attachment\"), None);\n}\n"
  },
  {
    "path": "backend/tauri/src/utils/init/logging.rs",
    "content": "use crate::{Config, config, utils::dirs};\nuse anyhow::{Result, anyhow, bail};\nuse parking_lot::Mutex;\nuse std::{\n    fs,\n    io::IsTerminal,\n    sync::{\n        OnceLock,\n        mpsc::{self, Sender},\n    },\n    thread,\n};\nuse tracing::error;\nuse tracing_appender::{\n    non_blocking::{NonBlocking, WorkerGuard},\n    rolling::Rotation,\n};\nuse tracing_log::log_tracer;\nuse tracing_subscriber::{EnvFilter, filter, fmt, layer::SubscriberExt, reload};\n\nuse super::nyanpasu::LoggingLevel;\n\npub type ReloadSignal = (Option<config::nyanpasu::LoggingLevel>, Option<usize>);\n\nstruct Channel(Option<Sender<ReloadSignal>>);\nimpl Channel {\n    fn globals() -> &'static Mutex<Channel> {\n        static CHANNEL: OnceLock<Mutex<Channel>> = OnceLock::new();\n        CHANNEL.get_or_init(|| Mutex::new(Channel(None)))\n    }\n}\n\npub fn refresh_logger(signal: ReloadSignal) -> Result<()> {\n    let channel = Channel::globals().lock();\n    match &channel.0 {\n        Some(sender) => {\n            let _ = sender.send(signal);\n            Ok(())\n        }\n        None => bail!(\"no logger channel\"),\n    }\n}\n\nfn get_file_appender(max_files: usize) -> Result<(NonBlocking, WorkerGuard)> {\n    let log_dir = dirs::app_logs_dir().unwrap();\n    let file_appender = tracing_appender::rolling::Builder::new()\n        .filename_prefix(\"clash-nyanpasu\")\n        .filename_suffix(\"app.log\")\n        .rotation(Rotation::DAILY)\n        .max_log_files(max_files)\n        .build(log_dir)?;\n    Ok(tracing_appender::non_blocking(file_appender))\n}\n\n/// initial instance global logger\npub fn init() -> Result<()> {\n    let log_dir = dirs::app_logs_dir().unwrap();\n    if !log_dir.exists() {\n        let _ = fs::create_dir_all(&log_dir);\n    }\n    let (log_level, log_max_files) = { (LoggingLevel::Debug, 7) }; // This is intended to capture config loading errors\n    let (filter, filter_handle) = reload::Layer::new(\n        EnvFilter::builder()\n            .with_default_directive(\n                std::convert::Into::<filter::LevelFilter>::into(LoggingLevel::Warn).into(),\n            )\n            .from_env_lossy()\n            .add_directive(format!(\"nyanpasu={log_level}\").parse().unwrap())\n            .add_directive(format!(\"clash_nyanpasu={log_level}\").parse().unwrap()),\n    );\n\n    // register the logger\n    let (appender, _guard) = get_file_appender(log_max_files)?;\n    let (file_layer, file_handle) = reload::Layer::new(\n        fmt::layer()\n            .json()\n            .with_writer(appender)\n            .with_current_span(true)\n            .with_line_number(true)\n            .with_file(true),\n    );\n\n    // spawn a thread to handle the reload signal\n    thread::spawn(move || {\n        let mut _guard = _guard; // just hold here to keep the file open\n        let (sender, receiver) = mpsc::channel::<ReloadSignal>();\n        {\n            let mut channel = Channel::globals().lock();\n            channel.0 = Some(sender);\n        }\n        loop {\n            let signal = receiver.recv().unwrap();\n            if let Some(level) = signal.0 {\n                filter_handle\n                    .reload(\n                        EnvFilter::builder()\n                            .with_default_directive(\n                                std::convert::Into::<filter::LevelFilter>::into(LoggingLevel::Warn)\n                                    .into(),\n                            )\n                            .from_env_lossy()\n                            .add_directive(format!(\"nyanpasu={level}\").parse().unwrap())\n                            .add_directive(format!(\"clash_nyanpasu={level}\").parse().unwrap()),\n                    )\n                    .unwrap(); // panic if error\n            }\n\n            if let Some(max_files) = signal.1 {\n                let (appender, guard) = match get_file_appender(max_files) {\n                    Ok(x) => x,\n                    Err(e) => {\n                        error!(\"failed to create file appender: {}\", e);\n                        continue;\n                    }\n                };\n                _guard = guard;\n                if let Err(e) = file_handle.modify(|layer| *layer.writer_mut() = appender) {\n                    error!(\"failed to modify file appender: {}\", e);\n                }\n            }\n        }\n    });\n\n    // if debug build, log to stdout and stderr with all levels\n    #[cfg(debug_assertions)]\n    let terminal_layer = fmt::Layer::new()\n        .with_ansi(std::io::stdout().is_terminal())\n        .compact()\n        .with_target(false)\n        .with_file(true)\n        .with_line_number(true)\n        .with_writer(std::io::stdout);\n\n    let subscriber = tracing_subscriber::registry().with(filter).with(file_layer);\n    #[cfg(debug_assertions)]\n    let subscriber = subscriber.with(terminal_layer);\n\n    log_tracer::LogTracer::init()?;\n    tracing::subscriber::set_global_default(subscriber)\n        .map_err(|x| anyhow!(\"setup logging error: {}\", x))?;\n    // reload the log level\n    std::thread::spawn(move || {\n        let config = Config::verge();\n        let log_level = config.latest().get_log_level();\n        let log_max_files = config.latest().max_log_files;\n        let _ = refresh_logger((Some(log_level), log_max_files));\n    });\n    Ok(())\n}\n"
  },
  {
    "path": "backend/tauri/src/utils/init/mod.rs",
    "content": "use crate::{\n    config::*,\n    utils::{dirs, help},\n};\nuse anyhow::{Context, Result, anyhow};\nuse fs_extra::dir::CopyOptions;\n#[cfg(windows)]\nuse runas::Command as RunasCommand;\nuse std::{\n    fs,\n    io::{BufReader, Write},\n    path::PathBuf,\n    sync::Arc,\n};\nuse tauri::utils::platform::current_exe;\nuse tracing_attributes::instrument;\n\nmod logging;\npub use logging::refresh_logger;\n\npub fn run_pending_migrations() -> Result<()> {\n    let current_exe = current_exe()?;\n    let current_exe = dunce::canonicalize(current_exe)?;\n    let file = std::fs::OpenOptions::new()\n        .write(true)\n        .create(true)\n        .truncate(true)\n        .open(crate::utils::dirs::app_data_dir()?.join(\"migration.log\"))?;\n    let file = Arc::new(parking_lot::Mutex::new(file));\n    let (stdout_reader, stdout_writer) = os_pipe::pipe()?;\n    let (stderr_reader, stderr_writer) = os_pipe::pipe()?;\n    let errs = Arc::new(parking_lot::Mutex::new(String::new()));\n    let guard = Arc::new(parking_lot::RwLock::new(()));\n    let mut child = std::process::Command::new(current_exe)\n        .arg(\"migrate\")\n        .stderr(stderr_writer)\n        .stdout(stdout_writer)\n        .spawn()?;\n    let file_ = file.clone();\n    let guard_ = guard.clone();\n    let errs_ = errs.clone();\n    std::thread::spawn(move || {\n        let _l = guard_.read();\n        let mut reader = BufReader::new(stdout_reader);\n        let mut buf = Vec::new();\n        loop {\n            buf.clear();\n            match nyanpasu_utils::io::read_line(&mut reader, &mut buf) {\n                Ok(0) => break,\n                Ok(_) => {\n                    let mut file = file_.lock();\n                    let _ = file.write_all(&buf);\n                }\n                Err(e) => {\n                    eprintln!(\"failed to read stdout: {e:?}\");\n                    let mut errs = errs_.lock();\n                    errs.push_str(&format!(\"failed to read stdout: {e:?}\\n\"));\n                    break;\n                }\n            }\n        }\n    });\n    let errs_ = errs.clone();\n    let guard_ = guard.clone();\n    std::thread::spawn(move || {\n        let _l = guard_.read();\n        let mut reader = BufReader::new(stderr_reader);\n        let mut buf = Vec::new();\n        loop {\n            buf.clear();\n            match nyanpasu_utils::io::read_line(&mut reader, &mut buf) {\n                Ok(0) => break,\n                Ok(_) => {\n                    let mut file = file.lock();\n                    let _ = file.write_all(&buf);\n                    let mut errs = errs_.lock();\n                    errs.push_str(unsafe { std::str::from_utf8_unchecked(&buf) });\n                }\n                Err(e) => {\n                    eprintln!(\"failed to read stderr: {e:?}\");\n                    let mut errs = errs_.lock();\n                    errs.push_str(&format!(\"failed to read stderr: {e:?}\\n\"));\n                    break;\n                }\n            }\n        }\n    });\n    let result = child.wait();\n    let _l = guard.write(); // Just for waiting the thread read all the output\n    let err = errs.lock();\n    result\n        .map_err(|e| anyhow!(\"Failed to wait for child: {:?}, errs: {}\", e, err))\n        .and_then(|status| {\n            if !status.success() {\n                Err(anyhow!(\"child process failed: {:?}, err: {}\", status, err))\n            } else {\n                Ok(())\n            }\n        })\n}\n\n/// Initialize all the config files\n/// before tauri setup\npub fn init_config() -> Result<()> {\n    // Check if old config dir exist and new config dir is not exist\n    // let mut old_app_dir: Option<PathBuf> = None;\n    // let mut app_dir: Option<PathBuf> = None;\n    // crate::dialog_err!(dirs::old_app_home_dir().map(|_old_app_dir| {\n    //     old_app_dir = Some(_old_app_dir);\n    // }));\n\n    // crate::dialog_err!(dirs::app_home_dir().map(|_app_dir| {\n    //     app_dir = Some(_app_dir);\n    // }));\n\n    // if let (Some(app_dir), Some(old_app_dir)) = (app_dir, old_app_dir) {\n    //     let msg = t!(\"dialog.migrate\");\n    //     if !app_dir.exists() && old_app_dir.exists() && migrate_dialog(msg.to_string().as_str()) {\n    //         if let Err(e) = do_config_migration(&old_app_dir, &app_dir) {\n    //             super::dialog::error_dialog(format!(\"failed to do migration: {:?}\", e))\n    //         }\n    //     }\n    //     if !app_dir.exists() {\n    //         let _ = fs::create_dir_all(app_dir);\n    //     }\n    // }\n\n    // init log\n    logging::init().unwrap();\n\n    crate::log_err!(dirs::app_profiles_dir().map(|profiles_dir| {\n        if !profiles_dir.exists() {\n            let _ = fs::create_dir_all(&profiles_dir);\n        }\n    }));\n\n    crate::log_err!(dirs::clash_guard_overrides_path().map(|path| {\n        if !path.exists() {\n            help::save_yaml(\n                &path,\n                &IClashTemp::template().0,\n                Some(\"# Clash Nyanpasuasu\"),\n            )?;\n        }\n        <Result<()>>::Ok(())\n    }));\n\n    crate::log_err!(dirs::nyanpasu_config_path().map(|path| {\n        if !path.exists() {\n            help::save_yaml(&path, &IVerge::template(), Some(\"# Clash Nyanpasu\"))?;\n        }\n        <Result<()>>::Ok(())\n    }));\n\n    crate::log_err!(dirs::profiles_path().map(|path| {\n        if !path.exists() {\n            help::save_yaml(&path, &Profiles::default(), Some(\"# Clash Nyanpasu\"))?;\n        }\n        <Result<()>>::Ok(())\n    }));\n\n    Ok(())\n}\n\n/// initialize app resources\n/// after tauri setup\npub fn init_resources() -> Result<()> {\n    let app_dir = dirs::app_data_dir()?;\n    let res_dir = dirs::app_resources_dir()?;\n\n    if !app_dir.exists() {\n        let _ = fs::create_dir_all(&app_dir);\n    }\n    if !res_dir.exists() {\n        let _ = fs::create_dir_all(&res_dir);\n    }\n\n    #[cfg(target_os = \"windows\")]\n    let file_list = [\"Country.mmdb\", \"geoip.dat\", \"geosite.dat\", \"wintun.dll\"];\n    #[cfg(not(target_os = \"windows\"))]\n    let file_list = [\"Country.mmdb\", \"geoip.dat\", \"geosite.dat\"];\n\n    // copy the resource file\n    // if the source file is newer than the destination file, copy it over\n    for file in file_list.iter() {\n        let src_path = res_dir.join(file);\n        let dest_path = app_dir.join(file);\n\n        let handle_copy = || {\n            match fs::copy(&src_path, &dest_path) {\n                Ok(_) => log::debug!(target: \"app\", \"resources copied '{file}'\"),\n                Err(err) => {\n                    log::error!(target: \"app\", \"failed to copy resources '{file}', {err:?}\")\n                }\n            };\n        };\n\n        if src_path.exists() && !dest_path.exists() {\n            handle_copy();\n            continue;\n        }\n\n        let src_modified = fs::metadata(&src_path).and_then(|m| m.modified());\n        let dest_modified = fs::metadata(&dest_path).and_then(|m| m.modified());\n\n        match (src_modified, dest_modified) {\n            (Ok(src_modified), Ok(dest_modified)) => {\n                if src_modified > dest_modified {\n                    handle_copy();\n                } else {\n                    log::debug!(target: \"app\", \"skipping resource copy '{file}'\");\n                }\n            }\n            _ => {\n                log::debug!(target: \"app\", \"failed to get modified '{file}'\");\n                handle_copy();\n            }\n        };\n    }\n\n    Ok(())\n}\n\n/// initialize service resources\n/// after tauri setup\n#[instrument]\npub fn init_service() -> Result<()> {\n    use nyanpasu_utils::runtime::block_on;\n    tracing::debug!(\"init services\");\n    block_on(async move {\n        let enable_service = {\n            *Config::verge()\n                .latest()\n                .enable_service_mode\n                .as_ref()\n                .unwrap_or(&false)\n        };\n        if enable_service {\n            match crate::core::service::control::status().await {\n                Ok(status) => {\n                    tracing::info!(\n                        \"service mode is enabled and service is running, do a update check\"\n                    );\n                    if let Some(info) = status.server {\n                        let server_ver = semver::Version::parse(info.version.as_ref()).unwrap();\n                        let app_ver = semver::Version::parse(status.version.as_ref()).unwrap();\n                        if app_ver > server_ver {\n                            tracing::info!(\n                                \"client service ver is newer than exist one, do service update\"\n                            );\n                            if let Err(e) = crate::core::service::control::update_service().await {\n                                log::error!(target: \"app\", \"failed to update service: {e:?}\");\n                            }\n                        }\n                    }\n                }\n                Err(e) => {\n                    log::error!(target: \"app\", \"failed to get service status: {e:?}\");\n                }\n            }\n        }\n        crate::core::service::init_service().await;\n    });\n    Ok(())\n}\n\npub fn check_singleton() -> Result<Option<single_instance::SingleInstance>> {\n    let placeholder = super::dirs::get_single_instance_placeholder()?;\n    for i in 0..5 {\n        let instance = single_instance::SingleInstance::new(&placeholder)\n            .context(\"failed to create single instance\")?;\n        if instance.is_single() {\n            return Ok(Some(instance));\n        }\n        if i != 4 {\n            std::thread::sleep(std::time::Duration::from_secs(1));\n        }\n    }\n    Ok(None)\n}\n\npub fn do_config_migration(old_app_dir: &PathBuf, app_dir: &PathBuf) -> anyhow::Result<()> {\n    let copy_option = CopyOptions::new();\n    let copy_option = copy_option.overwrite(true);\n    let copy_option = copy_option.content_only(true);\n    if let Err(e) = fs_extra::dir::move_dir(old_app_dir, app_dir, &copy_option) {\n        match e.kind {\n            #[cfg(windows)]\n            fs_extra::error::ErrorKind::PermissionDenied => {\n                // It seems that clash-verge-service is running, so kill it.\n                let status = RunasCommand::new(\"cmd\")\n                    .args(&[\"/C\", \"taskkill\", \"/IM\", \"clash-verge-service.exe\", \"/F\"])\n                    .status()?;\n                if !status.success() {\n                    anyhow::bail!(\"failed to kill clash-verge-service.exe\")\n                }\n                fs::rename(old_app_dir, app_dir)?;\n            }\n            _ => return Err(e.into()),\n        };\n    }\n    Ok(())\n}\n"
  },
  {
    "path": "backend/tauri/src/utils/mod.rs",
    "content": "pub mod candy;\npub mod config;\npub mod dialog;\npub mod dirs;\npub mod help;\npub mod init;\npub mod resolve;\n// mod winhelp;\npub mod downloader;\n#[cfg(windows)]\npub mod winreg;\n\npub mod collect;\npub mod net;\n\npub mod open;\n\npub mod dock;\npub mod sudo;\n\n#[cfg(test)]\n#[cfg(windows)]\nmod winreg_test;\n"
  },
  {
    "path": "backend/tauri/src/utils/net.rs",
    "content": "use std::time::Duration;\n\nuse super::candy::get_reqwest_client;\n\n#[tracing_attributes::instrument]\npub async fn url_delay_test(url: &str, expected_status: u16) -> Option<u64> {\n    // heat up\n    let client = get_reqwest_client().ok()?;\n    let _ = tokio::time::timeout(Duration::from_secs(10), client.get(url).send())\n        .await\n        .ok()?\n        .ok()?;\n    let tick = tokio::time::Instant::now();\n    let response = tokio::time::timeout(Duration::from_secs(10), client.get(url).send())\n        .await\n        .ok()?\n        .ok()?;\n    if response.status().as_u16() != expected_status {\n        return None;\n    }\n    Some(tick.elapsed().as_millis() as u64)\n}\n\n#[tracing_attributes::instrument]\npub async fn get_ipsb_asn() -> anyhow::Result<serde_json::Value> {\n    let client = get_reqwest_client()?;\n    let response = client\n        .get(\"https://api.ip.sb/geoip\")\n        .send()\n        .await?\n        .error_for_status()?;\n    let data: serde_json::Value = response.json().await?;\n    Ok(data)\n}\n"
  },
  {
    "path": "backend/tauri/src/utils/open.rs",
    "content": "use std::ffi::OsStr;\n\npub fn that<T: AsRef<OsStr>>(path: T) -> std::io::Result<()> {\n    // A dirty workaround for AppImage\n    if std::env::var(\"APPIMAGE\").is_ok() {\n        std::process::Command::new(\"xdg-open\")\n            .arg(path)\n            .env_remove(\"LD_LIBRARY_PATH\")\n            .status()?;\n        Ok(())\n    } else {\n        open::that(path)\n    }\n}\n\npub fn with<T: AsRef<OsStr>>(path: T, program: &str) -> std::io::Result<()> {\n    // A dirty workaround for AppImage\n    if std::env::var(\"APPIMAGE\").is_ok() {\n        std::process::Command::new(program)\n            .arg(path)\n            .env_remove(\"LD_LIBRARY_PATH\")\n            .status()?;\n        Ok(())\n    } else {\n        open::with(path, program)\n    }\n}\n"
  },
  {
    "path": "backend/tauri/src/utils/resolve.rs",
    "content": "use crate::{\n    config::{\n        Config, IVerge,\n        nyanpasu::{ClashCore, WindowState},\n    },\n    core::{storage::Storage, tray::proxies, *},\n    log_err,\n    utils::init,\n    window::{AppWindow, ReactAppMountedEvent, WindowConfig, WindowParamsBuilder},\n};\nuse anyhow::Result;\nuse semver::Version;\nuse serde_yaml::Mapping;\nuse std::{\n    net::TcpListener,\n    sync::atomic::{AtomicU16, Ordering},\n};\nuse tauri::{App, AppHandle, Emitter, Listener, Manager, async_runtime::block_on};\nuse tauri_plugin_shell::ShellExt;\nuse tauri_specta::Event;\n\nstatic OPEN_WINDOWS_COUNTER: AtomicU16 = AtomicU16::new(0);\n\npub fn is_window_opened() -> bool {\n    OPEN_WINDOWS_COUNTER.load(Ordering::Acquire) == 0 // 0 means no window open or windows is initialized\n}\n\npub fn reset_window_open_counter() {\n    OPEN_WINDOWS_COUNTER.store(0, Ordering::Release);\n}\n\n#[cfg(target_os = \"macos\")]\nfn set_window_controls_pos(\n    window: objc2::rc::Retained<objc2_app_kit::NSWindow>,\n    x: f64,\n    y: f64,\n) -> anyhow::Result<()> {\n    use objc2_app_kit::NSWindowButton;\n    use objc2_foundation::NSRect;\n    let close = window\n        .standardWindowButton(NSWindowButton::CloseButton)\n        .ok_or(anyhow::anyhow!(\"failed to get close button\"))?;\n    let miniaturize = window\n        .standardWindowButton(NSWindowButton::MiniaturizeButton)\n        .ok_or(anyhow::anyhow!(\"failed to get miniaturize button\"))?;\n    let zoom = window\n        .standardWindowButton(NSWindowButton::ZoomButton)\n        .ok_or(anyhow::anyhow!(\"failed to get zoom button\"))?;\n\n    let title_bar_container_view = unsafe {\n        close\n            .superview()\n            .and_then(|view| view.superview())\n            .ok_or(anyhow::anyhow!(\"failed to get title bar container view\"))?\n    };\n\n    let close_rect = close.frame();\n    let button_height = close_rect.size.height;\n\n    let title_bar_frame_height = button_height + y;\n    let mut title_bar_rect = title_bar_container_view.frame();\n    title_bar_rect.size.height = title_bar_frame_height;\n    title_bar_rect.origin.y = window.frame().size.height - title_bar_frame_height;\n    unsafe {\n        title_bar_container_view.setFrame(title_bar_rect);\n    }\n\n    let space_between = miniaturize.frame().origin.x - close.frame().origin.x;\n    let window_buttons = vec![close, miniaturize, zoom];\n\n    for (i, button) in window_buttons.into_iter().enumerate() {\n        let mut rect: NSRect = button.frame();\n        rect.origin.x = x + (i as f64 * space_between);\n        unsafe {\n            button.setFrameOrigin(rect.origin);\n        }\n    }\n    Ok(())\n}\n\npub fn find_unused_port() -> Result<u16> {\n    match TcpListener::bind(\"127.0.0.1:0\") {\n        Ok(listener) => {\n            let port = listener.local_addr()?.port();\n            Ok(port)\n        }\n        Err(_) => {\n            let port = Config::verge()\n                .latest()\n                .verge_mixed_port\n                .unwrap_or(Config::clash().data().get_mixed_port());\n            log::warn!(target: \"app\", \"use default port: {port}\");\n            Ok(port)\n        }\n    }\n}\n\n/// handle something when start app\npub fn resolve_setup(app: &mut App) {\n    #[cfg(target_os = \"macos\")]\n    app.set_activation_policy(tauri::ActivationPolicy::Accessory);\n    #[cfg(target_os = \"macos\")]\n    let app_handle = app.app_handle().clone();\n    ReactAppMountedEvent::listen(app, move |_| {\n        tracing::debug!(\"Frontend React App is mounted, reset open window counter\");\n        reset_window_open_counter();\n        #[cfg(target_os = \"macos\")]\n        log_err!(app_handle.run_on_main_thread(move || {\n            crate::utils::dock::macos::show_dock_icon();\n        }));\n    });\n\n    handle::Handle::global().init(app.app_handle().clone());\n    crate::consts::setup_app_handle(app.app_handle().clone());\n\n    log_err!(init::init_resources());\n    log_err!(init::init_service());\n\n    // 处理随机端口\n    let enable_random_port = Config::verge().latest().enable_random_port.unwrap_or(false);\n\n    let mut port = Config::verge()\n        .latest()\n        .verge_mixed_port\n        .unwrap_or(Config::clash().data().get_mixed_port());\n\n    if enable_random_port {\n        port = find_unused_port().unwrap_or(\n            Config::verge()\n                .latest()\n                .verge_mixed_port\n                .unwrap_or(Config::clash().data().get_mixed_port()),\n        );\n    }\n\n    Config::verge().data().patch_config(IVerge {\n        verge_mixed_port: Some(port),\n        ..IVerge::default()\n    });\n    let _ = Config::verge().data().save_file();\n    let mut mapping = Mapping::new();\n    mapping.insert(\"mixed-port\".into(), port.into());\n    Config::clash().data().patch_config(mapping);\n    let _ = Config::clash().latest().prepare_external_controller_port();\n    let _ = Config::clash().data().save_config();\n\n    // 启动核心\n    log::trace!(\"init config\");\n    log_err!(Config::init_config());\n\n    log::trace!(\"init storage\");\n    log_err!(crate::core::storage::setup(app));\n\n    log::trace!(\"launch core\");\n    log_err!(CoreManager::global().init());\n\n    log::trace!(\"init clash connection connector\");\n    log_err!(crate::core::clash::setup(app));\n\n    log::trace!(\"init widget manager\");\n    log_err!(tauri::async_runtime::block_on(async {\n        crate::widget::setup(app, {\n            let manager = app.state::<crate::core::clash::ws::ClashConnectionsConnector>();\n            manager.subscribe()\n        })\n        .await\n    }));\n\n    #[cfg(any(windows, target_os = \"linux\"))]\n    log::trace!(\"init system tray\");\n    #[cfg(any(windows, target_os = \"linux\"))]\n    tray::icon::resize_images(crate::utils::help::get_max_scale_factor()); // generate latest cache icon by current scale factor\n    let app_handle = app.app_handle().clone();\n    app.listen(\"update_systray\", move |_| {\n        // Fix the GTK should run on main thread issue\n        let app_handle_clone = app_handle.clone();\n        log_err!(app_handle.run_on_main_thread(move || {\n            log_err!(\n                tray::Tray::update_systray(&app_handle_clone),\n                \"failed to update systray\"\n            );\n        }));\n    });\n    log_err!(app.emit(\"update_systray\", ()));\n\n    let silent_start = { Config::verge().data().enable_silent_start };\n    if !silent_start.unwrap_or(false) {\n        create_window(app.app_handle());\n    }\n\n    log_err!(sysopt::Sysopt::global().init_launch());\n    log_err!(sysopt::Sysopt::global().init_sysproxy());\n\n    log_err!(handle::Handle::update_systray_part());\n    log_err!(hotkey::Hotkey::global().init(app.app_handle().clone()));\n\n    // setup jobs\n    log::trace!(\"setup jobs\");\n    {\n        let storage = app.state::<Storage>();\n        let storage = (*storage).clone();\n        log_err!(crate::core::tasks::setup(app, storage));\n    }\n\n    // test job\n    proxies::setup_proxies();\n    crate::core::storage::register_web_storage_listener(app.app_handle());\n}\n\n/// reset system proxy\npub fn resolve_reset() {\n    log_err!(sysopt::Sysopt::global().reset_sysproxy());\n    log_err!(block_on(CoreManager::global().stop_core()));\n}\n\n/// Main window implementation (new UI)\nstruct MainWindow;\n\nimpl AppWindow for MainWindow {\n    fn label(&self) -> &str {\n        crate::consts::MAIN_WINDOW_LABEL\n    }\n\n    fn title(&self) -> &str {\n        crate::consts::APP_NAME\n    }\n\n    fn url(&self) -> &str {\n        \"/main\"\n    }\n\n    fn config(&self) -> WindowConfig {\n        WindowConfig::new()\n            .singleton(true)\n            .visible_on_create(true)\n            .default_size(800.0, 636.0)\n            .min_size(400.0, 600.0)\n            .center(true)\n    }\n\n    fn get_window_state(&self) -> Option<WindowState> {\n        Config::verge().latest().window_size_state.clone()\n    }\n\n    fn set_window_state(&self, state: Option<WindowState>) {\n        Config::verge().data().patch_config(IVerge {\n            window_size_state: state,\n            ..IVerge::default()\n        });\n    }\n}\n\n/// Editor window\nstruct EditorWindow {\n    label: String,\n}\n\nimpl EditorWindow {\n    fn new(uid: &str) -> Self {\n        Self {\n            label: format!(\"{}-{}\", crate::consts::EDITOR_WINDOW_LABEL, uid),\n        }\n    }\n}\n\nimpl AppWindow for EditorWindow {\n    fn label(&self) -> &str {\n        &self.label\n    }\n\n    fn title(&self) -> &str {\n        &crate::consts::APP_EDITOR_NAME\n    }\n\n    fn url(&self) -> &str {\n        \"/editor\"\n    }\n\n    fn config(&self) -> WindowConfig {\n        WindowConfig::new()\n            .singleton(false) // Allow multiple editor windows with different uids\n            .visible_on_create(true)\n            .default_size(800.0, 636.0)\n            .min_size(400.0, 600.0)\n            .center(true)\n    }\n\n    fn get_window_state(&self) -> Option<WindowState> {\n        // EditorWindow does not remember window state\n        None\n    }\n\n    fn set_window_state(&self, _state: Option<WindowState>) {\n        // EditorWindow does not remember window state\n    }\n}\n\n/// Legacy window implementation (original UI)\nstruct LegacyWindow;\n\nimpl AppWindow for LegacyWindow {\n    fn label(&self) -> &str {\n        crate::consts::LEGACY_WINDOW_LABEL\n    }\n\n    fn title(&self) -> &str {\n        crate::consts::APP_NAME\n    }\n\n    fn url(&self) -> &str {\n        \"/\"\n    }\n\n    fn config(&self) -> WindowConfig {\n        WindowConfig::new()\n            .singleton(true)\n            .visible_on_create(true)\n            .default_size(800.0, 636.0)\n            .min_size(400.0, 600.0)\n            .center(true)\n    }\n\n    fn get_window_state(&self) -> Option<WindowState> {\n        Config::verge().latest().window_size_state.clone()\n    }\n\n    fn set_window_state(&self, state: Option<WindowState>) {\n        Config::verge().data().patch_config(IVerge {\n            window_size_state: state,\n            ..IVerge::default()\n        });\n    }\n}\n\n/// create main window\n#[tracing_attributes::instrument(skip(app_handle))]\npub fn create_main_window(app_handle: &AppHandle) {\n    log_err!(MainWindow.create(app_handle));\n}\n\n/// close main window\npub fn close_main_window(app_handle: &AppHandle) {\n    MainWindow.close(app_handle);\n}\n\n/// is main window open\npub fn is_main_window_open(app_handle: &AppHandle) -> bool {\n    MainWindow.is_open(app_handle)\n}\n\npub fn save_main_window_state(app_handle: &AppHandle, save_to_file: bool) -> Result<()> {\n    MainWindow.save_state(app_handle, save_to_file)\n}\n\n/// create legacy window\n#[tracing_attributes::instrument(skip(app_handle))]\npub fn create_legacy_window(app_handle: &AppHandle) {\n    log_err!(LegacyWindow.create(app_handle));\n}\n\n/// close legacy window\npub fn close_legacy_window(app_handle: &AppHandle) {\n    LegacyWindow.close(app_handle);\n}\n\n/// is legacy window open\npub fn is_legacy_window_open(app_handle: &AppHandle) -> bool {\n    LegacyWindow.is_open(app_handle)\n}\n\npub fn save_legacy_window_state(app_handle: &AppHandle, save_to_file: bool) -> Result<()> {\n    LegacyWindow.save_state(app_handle, save_to_file)\n}\n\n/// Create window based on use_legacy_ui config\n/// This is the primary function to use when opening window from tray, etc.\n#[tracing_attributes::instrument(skip(app_handle))]\npub fn create_window(app_handle: &AppHandle) {\n    let use_legacy = Config::verge().latest().use_legacy_ui.unwrap_or(true);\n\n    if use_legacy {\n        create_legacy_window(app_handle);\n    } else {\n        create_main_window(app_handle);\n    }\n}\n\n/// Close the currently active window based on use_legacy_ui config\npub fn close_window(app_handle: &AppHandle) {\n    let use_legacy = Config::verge().latest().use_legacy_ui.unwrap_or(true);\n\n    if use_legacy {\n        close_legacy_window(app_handle);\n    } else {\n        close_main_window(app_handle);\n    }\n}\n\n/// Check if the configured window is open\npub fn is_window_open(app_handle: &AppHandle) -> bool {\n    let use_legacy = Config::verge().latest().use_legacy_ui.unwrap_or(true);\n\n    if use_legacy {\n        is_legacy_window_open(app_handle)\n    } else {\n        is_main_window_open(app_handle)\n    }\n}\n\n/// Save window state for the configured window type\npub fn save_window_state(app_handle: &AppHandle, save_to_file: bool) -> Result<()> {\n    let use_legacy = Config::verge().latest().use_legacy_ui.unwrap_or(true);\n\n    if use_legacy {\n        save_legacy_window_state(app_handle, save_to_file)\n    } else {\n        save_main_window_state(app_handle, save_to_file)\n    }\n}\n\n/// Create editor window with uid\n#[tracing_attributes::instrument(skip(app_handle))]\npub fn create_editor_window(app_handle: &AppHandle, uid: &str) -> Result<()> {\n    let editor_window = EditorWindow::new(uid);\n    let params = WindowParamsBuilder::new().param(\"uid\", uid).build();\n    editor_window.create_with_params(app_handle, params)?;\n    Ok(())\n}\n\n/// Close editor window by uid\npub fn close_editor_window(app_handle: &AppHandle, uid: &str) {\n    let editor_window = EditorWindow::new(uid);\n    editor_window.close_by_label(app_handle, &editor_window.label());\n}\n\n/// Check if editor window with uid is open\npub fn is_editor_window_open(app_handle: &AppHandle, uid: &str) -> bool {\n    let editor_window = EditorWindow::new(uid);\n    app_handle\n        .get_webview_window(editor_window.label())\n        .is_some()\n}\n\n/// resolve core version\n// TODO: use enum instead\npub async fn resolve_core_version(app_handle: &AppHandle, core_type: &ClashCore) -> Result<String> {\n    let shell = app_handle.shell();\n    let core = core_type.clone().to_string();\n    log::debug!(target: \"app\", \"check config in `{core}`\");\n    let cmd = match core_type {\n        ClashCore::ClashPremium | ClashCore::Mihomo | ClashCore::MihomoAlpha => {\n            shell.sidecar(core)?.args([\"-v\"])\n        }\n        ClashCore::ClashRs | ClashCore::ClashRsAlpha => shell.sidecar(core)?.args([\"-V\"]),\n    };\n    let out = cmd.output().await?;\n    if !out.status.success() {\n        return Err(anyhow::anyhow!(\"failed to get core version\"));\n    }\n    let out = String::from_utf8_lossy(&out.stdout);\n    log::trace!(target: \"app\", \"get core version: {out:?}\");\n    let out = out.trim().split(' ').collect::<Vec<&str>>();\n    for item in out {\n        log::debug!(target: \"app\", \"check item: {item}\");\n        if item.starts_with('v')\n            || item.starts_with('n')\n            || item.starts_with(\"alpha\")\n            || Version::parse(item).is_ok()\n        {\n            match core_type {\n                ClashCore::ClashRs => return Ok(format!(\"v{}\", item)),\n                _ => return Ok(item.to_string()),\n            }\n        }\n    }\n    Err(anyhow::anyhow!(\"failed to get core version\"))\n}\n"
  },
  {
    "path": "backend/tauri/src/utils/sudo.rs",
    "content": "#[cfg(target_os = \"macos\")]\nmod macos {\n    use std::{os::unix::process::ExitStatusExt, path::PathBuf};\n\n    /// use runas to run the command with bash and pipe the output to the tmp output file\n    pub fn sudo<M: AsRef<str>, T: AsRef<str>>(bin: M, args: &[T]) -> std::io::Result<()> {\n        let dir = tempfile::tempdir()?;\n        let script = dir.path().join(\"script.sh\");\n        let out = dir.path().join(\"output.txt\");\n        if !out.exists() {\n            std::fs::write(&out, String::new())?;\n        }\n        let bin = PathBuf::from(bin.as_ref());\n        let parent = bin.parent();\n        let mut script_content = String::with_capacity(1024);\n        if let Some(parent) = parent {\n            script_content.push_str(\"cd \");\n            script_content.push('\"');\n            script_content.push_str(parent.to_string_lossy().as_ref());\n            script_content.push('\"');\n            script_content.push_str(\" && ./\");\n        }\n        script_content.push_str(bin.file_name().unwrap().to_string_lossy().as_ref());\n        script_content.push(' ');\n        script_content.push_str(\n            args.iter()\n                .map(|s| s.as_ref())\n                .collect::<Vec<_>>()\n                .join(\" \")\n                .as_ref(),\n        );\n        tracing::debug!(\"prepare script: {}\", script_content);\n        std::fs::write(&script, script_content)?;\n        let status = std::process::Command::new(\"osascript\")\n            .arg(\"-e\")\n            .args([&format!(\n                r#\"do shell script \"bash {} &> {}\" with administrator privileges\"#,\n                script.to_string_lossy(),\n                out.to_string_lossy()\n            )])\n            .status();\n        match status {\n            Ok(status) if status.success() => Ok(()),\n            Ok(status) => {\n                // read the output file\n                let output = std::fs::read_to_string(out)\n                    .unwrap_or_else(|e| format!(\"failed to read output file: {}\", e));\n                Err(std::io::Error::new(\n                    std::io::ErrorKind::Other,\n                    format!(\n                        \"exit code: {:?}, signal: {:?}, output: {}\",\n                        status.code(),\n                        status.signal(),\n                        output\n                    ),\n                ))\n            }\n            Err(e) => Err(std::io::Error::new(\n                std::io::ErrorKind::Other,\n                e.to_string(),\n            )),\n        }\n    }\n}\n\n#[cfg(target_os = \"macos\")]\npub use macos::sudo;\n"
  },
  {
    "path": "backend/tauri/src/utils/winhelp.rs",
    "content": "#![cfg(target_os = \"windows\")]\n#![allow(non_snake_case)]\n#![allow(non_camel_case_types)]\n\n//!\n//! From https://github.com/tauri-apps/window-vibrancy/blob/dev/src/windows.rs\n//!\n\nuse windows_sys::Win32::{\n    Foundation::*,\n    System::{LibraryLoader::*, SystemInformation::*},\n};\n\nfn get_function_impl(library: &str, function: &str) -> Option<FARPROC> {\n    assert_eq!(library.chars().last(), Some('\\0'));\n    assert_eq!(function.chars().last(), Some('\\0'));\n\n    let module = unsafe { LoadLibraryA(library.as_ptr()) };\n    if module == 0 {\n        return None;\n    }\n    Some(unsafe { GetProcAddress(module, function.as_ptr()) })\n}\n\nmacro_rules! get_function {\n    ($lib:expr, $func:ident) => {\n        get_function_impl(concat!($lib, '\\0'), concat!(stringify!($func), '\\0')).map(|f| unsafe {\n            std::mem::transmute::<::windows_sys::Win32::Foundation::FARPROC, $func>(f)\n        })\n    };\n}\n\n/// Returns a tuple of (major, minor, buildnumber)\nfn get_windows_ver() -> Option<(u32, u32, u32)> {\n    type RtlGetVersion = unsafe extern \"system\" fn(*mut OSVERSIONINFOW) -> i32;\n    let handle = get_function!(\"ntdll.dll\", RtlGetVersion);\n    if let Some(rtl_get_version) = handle {\n        unsafe {\n            let mut vi = OSVERSIONINFOW {\n                dwOSVersionInfoSize: 0,\n                dwMajorVersion: 0,\n                dwMinorVersion: 0,\n                dwBuildNumber: 0,\n                dwPlatformId: 0,\n                szCSDVersion: [0; 128],\n            };\n\n            let status = (rtl_get_version)(&mut vi as _);\n\n            if status >= 0 {\n                Some((vi.dwMajorVersion, vi.dwMinorVersion, vi.dwBuildNumber))\n            } else {\n                None\n            }\n        }\n    } else {\n        None\n    }\n}\n\npub fn is_win11() -> bool {\n    let v = get_windows_ver().unwrap_or_default();\n    v.2 >= 22000\n}\n\n#[test]\nfn test_version() {\n    dbg!(get_windows_ver().unwrap_or_default());\n}\n"
  },
  {
    "path": "backend/tauri/src/utils/winreg.rs",
    "content": "use std::{\n    io::ErrorKind,\n    path::{Path, PathBuf},\n};\n\nuse super::dirs::APP_DIR_PLACEHOLDER;\nuse anyhow::Result;\nuse once_cell::sync::Lazy;\nuse winreg::{RegKey, enums::*};\n\nstatic SOFTWARE_KEY: Lazy<&'static str> = Lazy::new(|| {\n    let key = format!(\"Software\\\\{}\", *APP_DIR_PLACEHOLDER);\n    Box::leak(key.into_boxed_str()) // safe to leak\n});\n\npub fn get_app_dir() -> Result<Option<PathBuf>> {\n    let hcu = RegKey::predef(HKEY_CURRENT_USER);\n    let key = match hcu.open_subkey(*SOFTWARE_KEY) {\n        Ok(key) => key,\n        Err(e) => {\n            if let ErrorKind::NotFound = e.kind() {\n                return Ok(None);\n            }\n            return Err(e.into());\n        }\n    };\n    let path: String = key.get_value(\"AppDir\")?;\n    if path.is_empty() {\n        return Ok(None);\n    }\n    let path = PathBuf::from(path);\n    // Basic validation: ensure absolute path\n    if !path.is_absolute() {\n        return Ok(None);\n    }\n    Ok(Some(path))\n}\n\npub fn set_app_dir(path: &Path) -> Result<()> {\n    let hcu = RegKey::predef(HKEY_CURRENT_USER);\n    let (key, _) = hcu.create_subkey(*SOFTWARE_KEY)?;\n    let path = path.to_str().unwrap(); // safe to unwrap\n    key.set_value(\"AppDir\", &path)?;\n    Ok(())\n}\n\n/// Get current Windows user SID\n#[cfg(windows)]\npub fn get_current_user_sid() -> Result<String> {\n    use std::{os::windows::process::CommandExt, process::Command};\n\n    // Try PowerShell method first (more reliable)\n    let output = Command::new(\"powershell\")\n        .args(&[\n            \"-Command\",\n            \"[System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value\",\n        ])\n        .creation_flags(0x08000000) // CREATE_NO_WINDOW\n        .output();\n\n    if let Ok(output) = output {\n        if output.status.success() {\n            let sid = String::from_utf8_lossy(&output.stdout).trim().to_string();\n            if !sid.is_empty() {\n                return Ok(sid);\n            }\n        }\n    }\n\n    // Fallback to WMIC method\n    let output = Command::new(\"wmic\")\n        .args(&[\n            \"useraccount\",\n            \"where\",\n            \"name='%username%'\",\n            \"get\",\n            \"sid\",\n            \"/value\",\n        ])\n        .creation_flags(0x08000000) // CREATE_NO_WINDOW\n        .output();\n\n    if let Ok(output) = output {\n        if output.status.success() {\n            let result = String::from_utf8_lossy(&output.stdout);\n            for line in result.lines() {\n                if line.starts_with(\"SID=\") {\n                    let sid = line[4..].trim().to_string();\n                    if !sid.is_empty() {\n                        return Ok(sid);\n                    }\n                }\n            }\n        }\n    }\n\n    // If both methods fail, fall back to the config dir hashing approach\n    use std::{\n        collections::hash_map::DefaultHasher,\n        hash::{Hash, Hasher},\n    };\n    let cfg_dir = super::dirs::app_config_dir()?;\n    let mut hasher = DefaultHasher::new();\n    cfg_dir.to_string_lossy().hash(&mut hasher);\n    let hash = hasher.finish();\n    Ok(format!(\"{:x}\", hash))\n}\n"
  },
  {
    "path": "backend/tauri/src/utils/winreg_test.rs",
    "content": "#[cfg(test)]\nmod tests {\n    use crate::utils::{dirs::get_single_instance_placeholder, winreg::get_current_user_sid};\n\n    #[test]\n    #[cfg(windows)]\n    fn test_get_current_user_sid() {\n        let sid = get_current_user_sid();\n        assert!(sid.is_ok());\n        let sid = sid.unwrap();\n        assert!(!sid.is_empty());\n        // SID should start with \"S-\" followed by numbers\n        assert!(sid.starts_with(\"S-\"));\n        println!(\"Current user SID: {}\", sid);\n    }\n\n    #[test]\n    #[cfg(windows)]\n    fn test_get_single_instance_placeholder_with_sid() {\n        let placeholder = get_single_instance_placeholder();\n        assert!(placeholder.is_ok());\n        let placeholder = placeholder.unwrap();\n        assert!(!placeholder.is_empty());\n        // Should contain the app name\n        assert!(\n            placeholder.contains(\"clash-nyanpasu\") || placeholder.contains(\"clash-nyanpasu-dev\")\n        );\n        println!(\"Single instance placeholder: {}\", placeholder);\n    }\n}\n"
  },
  {
    "path": "backend/tauri/src/widget.rs",
    "content": "use crate::config::{Config, nyanpasu::NetworkStatisticWidgetConfig};\n\nuse super::core::clash::ws::ClashConnectionsConnectorEvent;\n\nuse anyhow::Context;\nuse nyanpasu_egui::{\n    ipc::{IpcSender, Message, StatisticMessage, create_ipc_server},\n    widget::StatisticWidgetVariant,\n};\nuse std::sync::{Arc, atomic::AtomicBool};\nuse tauri::{Manager, Runtime, utils::platform::current_exe};\nuse tokio::{\n    process::Child,\n    sync::{\n        Mutex,\n        broadcast::{Receiver as BroadcastReceiver, error::RecvError as BroadcastRecvError},\n    },\n};\n\n#[derive(Clone)]\npub struct WidgetManager {\n    instance: Arc<Mutex<Option<WidgetManagerInstance>>>,\n    listener_initd: Arc<AtomicBool>,\n}\n\nstruct WidgetManagerInstance {\n    tx: IpcSender<Message>,\n    process: Child,\n}\n\nimpl WidgetManager {\n    pub fn new() -> Self {\n        Self {\n            instance: Arc::new(Mutex::new(None)),\n            listener_initd: Arc::new(AtomicBool::new(false)),\n        }\n    }\n\n    fn register_listener(&self, mut receiver: BroadcastReceiver<ClashConnectionsConnectorEvent>) {\n        if self\n            .listener_initd\n            .load(std::sync::atomic::Ordering::Acquire)\n        {\n            return;\n        }\n        let signal = self.listener_initd.clone();\n        let this = self.clone();\n        tokio::spawn(async move {\n            loop {\n                match receiver.recv().await {\n                    Ok(event) => {\n                        if let Err(e) = this.handle_event(event).await {\n                            log::error!(\"Failed to handle event: {e}\");\n                        }\n                    }\n                    Err(e) => {\n                        log::error!(\"Error receiving event: {e}\");\n                        if BroadcastRecvError::Closed == e {\n                            signal.store(false, std::sync::atomic::Ordering::Release);\n                            break;\n                        }\n                    }\n                }\n            }\n        });\n        self.listener_initd\n            .store(true, std::sync::atomic::Ordering::Release);\n    }\n\n    async fn handle_event(&self, event: ClashConnectionsConnectorEvent) -> anyhow::Result<()> {\n        let mut instance = self.instance.clone().lock_owned().await;\n        if let ClashConnectionsConnectorEvent::Update(info) = event\n            && instance\n                .as_mut()\n                .is_some_and(|instance| instance.is_alive())\n        {\n            tokio::task::spawn_blocking(move || {\n                let instance = instance.as_ref().unwrap();\n                // we only care about the update event now\n                instance\n                    .send_message(Message::UpdateStatistic(StatisticMessage {\n                        download_total: info.download_total,\n                        upload_total: info.upload_total,\n                        download_speed: info.download_speed,\n                        upload_speed: info.upload_speed,\n                    }))\n                    .context(\"Failed to send event to widget\")?;\n                Ok::<(), anyhow::Error>(())\n            })\n            .await\n            .context(\"Failed to send event to widget\")??;\n        }\n        Ok(())\n    }\n\n    pub async fn start(&self, widget: StatisticWidgetVariant) -> anyhow::Result<()> {\n        if (self.instance.lock().await).is_some() {\n            log::info!(\"Widget already running, stopping it first...\");\n            self.stop().await.context(\"Failed to stop widget\")?;\n        }\n        let mut instance = self.instance.lock().await;\n        let current_exe = current_exe().context(\"Failed to get current executable\")?;\n        // This operation is blocking, but it internal just a system call, so I think it's okay\n        let (mut ipc_server, server_name) = create_ipc_server()?;\n        // spawn a process to run the widget\n        let variant = format!(\"{widget}\");\n        tracing::debug!(\"Spawning widget process for {}...\", variant);\n        let widget_win_state_path = crate::utils::dirs::app_data_dir()\n            .context(\"Failed to get app data dir\")?\n            .join(format!(\"widget_{variant}.state\"));\n        let mut child = tokio::process::Command::new(current_exe)\n            .arg(\"statistic-widget\")\n            .arg(variant)\n            .env(\"NYANPASU_EGUI_IPC_SERVER\", server_name)\n            .env(\"NYANPASU_EGUI_WINDOW_STATE_PATH\", widget_win_state_path)\n            .stdin(std::process::Stdio::inherit())\n            .stdout(os_pipe::dup_stdout()?)\n            .stderr(os_pipe::dup_stderr()?)\n            .spawn()\n            .context(\"Failed to spawn widget process\")?;\n        tracing::debug!(\"Waiting for widget process to start...\");\n        let tx = tokio::select! {\n            res = tokio::task::spawn_blocking(move || {\n                ipc_server\n                    .connect()\n                    .context(\"Failed to connect to widget\")?;\n                ipc_server.into_tx().context(\"Failed to get ipc sender\")\n            }) => res.context(\"Failed to get ipc sender\")??,\n            res = child.wait() => {\n                match res {\n                    Ok(status) => {\n                        return Err(anyhow::anyhow!(\"Widget process exited: {}\", status));\n                    }\n                    Err(e) => {\n                        return Err(anyhow::anyhow!(\"Failed to wait for widget process: {}\", e));\n                    }\n                }\n            }\n        };\n        instance.replace(WidgetManagerInstance { tx, process: child });\n        Ok(())\n    }\n\n    pub async fn stop(&self) -> anyhow::Result<()> {\n        let Some(mut instance) = self.instance.lock().await.take() else {\n            tracing::debug!(\"Widget instance is not exists, skipping...\");\n            return Ok(());\n        };\n        if !instance.is_alive() {\n            tracing::debug!(\"Widget instance is not alive, skipping...\");\n            return Ok(());\n        }\n        // first try to stop the process gracefully\n        let mut instance = tokio::task::spawn_blocking(move || {\n            instance\n                .send_message(Message::Stop)\n                .context(\"Failed to send stop message to widget\")?;\n            Ok::<WidgetManagerInstance, anyhow::Error>(instance)\n        })\n        .await\n        .context(\"Failed to kill widget process\")??;\n        for _ in 0..5 {\n            if instance.is_alive() {\n                tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;\n            } else {\n                return Ok(());\n            }\n        }\n        // force kill the process\n        instance\n            .process\n            .kill()\n            .await\n            .context(\"Failed to kill widget process\")?;\n        Ok(())\n    }\n\n    pub async fn is_running(&self) -> bool {\n        let mut instance = self.instance.lock().await;\n        instance\n            .as_mut()\n            .is_some_and(|instance| instance.is_alive())\n    }\n}\n\nimpl WidgetManagerInstance {\n    pub fn is_alive(&mut self) -> bool {\n        self.process.try_wait().is_ok_and(|status| status.is_none())\n    }\n\n    fn send_message(&self, message: Message) -> anyhow::Result<()> {\n        #[cfg(debug_assertions)]\n        tracing::debug!(\"Sending message to widget: {:?}\", message);\n        self.tx\n            .send(message)\n            .context(\"Failed to send message to widget\")?;\n        Ok(())\n    }\n}\n\nimpl Drop for WidgetManager {\n    fn drop(&mut self) {\n        let cleanup = async {\n            let _ = self.stop().await;\n        };\n        match tokio::runtime::Handle::try_current() {\n            Ok(_) => {\n                tokio::task::block_in_place(move || {\n                    tauri::async_runtime::block_on(cleanup);\n                });\n            }\n            Err(_) => {\n                tauri::async_runtime::block_on(cleanup);\n            }\n        }\n    }\n}\n\npub async fn setup<R: Runtime, M: Manager<R>>(\n    manager: &M,\n    ws_connections_receiver: BroadcastReceiver<ClashConnectionsConnectorEvent>,\n) -> anyhow::Result<()> {\n    let widget_manager = WidgetManager::new();\n    // TODO: use the app_handle to read initial config.\n    let option = Config::verge()\n        .data()\n        .network_statistic_widget\n        .unwrap_or_default();\n    widget_manager.register_listener(ws_connections_receiver);\n    if let NetworkStatisticWidgetConfig::Enabled(widget) = option {\n        widget_manager.start(widget).await?;\n    }\n\n    // TODO: subscribe to the config change event\n    manager.manage(widget_manager);\n    Ok(())\n}\n"
  },
  {
    "path": "backend/tauri/src/window.rs",
    "content": "//! Tauri window management mod\n//!\n//! This module provides a flexible window management system that supports:\n//! - URL parameters for windows\n//! - Multiple instances of the same window type (e.g., main, main-1, main-2)\n//! - Inter-window communication\n//! - Configurable window properties (singleton, visibility, size, etc.)\n\nuse crate::{\n    config::{Config, nyanpasu::WindowState},\n    log_err, trace_err,\n};\nuse anyhow::Result;\nuse serde::{Deserialize, Serialize};\nuse specta::Type;\nuse std::{\n    collections::HashMap,\n    sync::{\n        Mutex, OnceLock,\n        atomic::{AtomicU16, Ordering},\n    },\n};\nuse tauri::{AppHandle, Manager};\nuse tauri_specta::Event;\n\n/// Global counter for tracking open windows\nstatic OPEN_WINDOWS_COUNTER: AtomicU16 = AtomicU16::new(0);\n\n/// Global window manager instance\nstatic WINDOW_MANAGER: OnceLock<Mutex<WindowManager>> = OnceLock::new();\n/// Window configuration options\n#[derive(Debug, Clone)]\npub struct WindowConfig {\n    /// Whether only one instance of this window type is allowed\n    pub singleton: bool,\n    /// Whether the window should be visible when created\n    pub visible_on_create: bool,\n    /// Default window size (width, height)\n    pub default_size: (f64, f64),\n    /// Minimum window size (width, height)\n    pub min_size: Option<(f64, f64)>,\n    /// Maximum window size (width, height)\n    pub max_size: Option<(f64, f64)>,\n    /// Whether to center the window on creation\n    pub center: bool,\n    /// Whether the window is resizable\n    pub resizable: bool,\n    /// Whether the window should always be on top (None = use global config)\n    pub always_on_top: Option<bool>,\n    /// Whether to use decorations (None = use platform default)\n    pub decorations: Option<bool>,\n    /// Whether the window is transparent (None = use platform default)\n    pub transparent: Option<bool>,\n    /// Whether to skip taskbar\n    pub skip_taskbar: bool,\n}\n\nimpl Default for WindowConfig {\n    fn default() -> Self {\n        Self {\n            singleton: true,\n            visible_on_create: true,\n            default_size: (800.0, 636.0),\n            min_size: Some((400.0, 600.0)),\n            max_size: None,\n            center: true,\n            resizable: true,\n            always_on_top: None,\n            decorations: None,\n            transparent: None,\n            skip_taskbar: false,\n        }\n    }\n}\n\nimpl WindowConfig {\n    /// Create a new WindowConfig with default values\n    pub fn new() -> Self {\n        Self::default()\n    }\n\n    /// Set whether only one instance is allowed\n    pub fn singleton(mut self, singleton: bool) -> Self {\n        self.singleton = singleton;\n        self\n    }\n\n    /// Set whether window is visible on creation\n    pub fn visible_on_create(mut self, visible: bool) -> Self {\n        self.visible_on_create = visible;\n        self\n    }\n\n    /// Set default window size\n    pub fn default_size(mut self, width: f64, height: f64) -> Self {\n        self.default_size = (width, height);\n        self\n    }\n\n    /// Set minimum window size\n    pub fn min_size(mut self, width: f64, height: f64) -> Self {\n        self.min_size = Some((width, height));\n        self\n    }\n\n    /// Set maximum window size\n    pub fn max_size(mut self, width: f64, height: f64) -> Self {\n        self.max_size = Some((width, height));\n        self\n    }\n\n    /// Set whether to center the window\n    pub fn center(mut self, center: bool) -> Self {\n        self.center = center;\n        self\n    }\n\n    /// Set whether the window is resizable\n    pub fn resizable(mut self, resizable: bool) -> Self {\n        self.resizable = resizable;\n        self\n    }\n\n    /// Set always on top\n    pub fn always_on_top(mut self, always_on_top: bool) -> Self {\n        self.always_on_top = Some(always_on_top);\n        self\n    }\n\n    /// Set whether to skip taskbar\n    pub fn skip_taskbar(mut self, skip: bool) -> Self {\n        self.skip_taskbar = skip;\n        self\n    }\n}\n\n/// Window URL parameters\npub type WindowParams = HashMap<String, String>;\n\n/// Builder for constructing URL parameters\n#[derive(Debug, Clone, Default)]\npub struct WindowParamsBuilder {\n    params: WindowParams,\n}\n\nimpl WindowParamsBuilder {\n    pub fn new() -> Self {\n        Self::default()\n    }\n\n    /// Add a string parameter\n    pub fn param(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {\n        self.params.insert(key.into(), value.into());\n        self\n    }\n\n    /// Add a parameter if condition is true\n    pub fn param_if(\n        self,\n        condition: bool,\n        key: impl Into<String>,\n        value: impl Into<String>,\n    ) -> Self {\n        if condition {\n            self.param(key, value)\n        } else {\n            self\n        }\n    }\n\n    /// Add an optional parameter\n    pub fn param_opt(self, key: impl Into<String>, value: Option<impl Into<String>>) -> Self {\n        match value {\n            Some(v) => self.param(key, v),\n            None => self,\n        }\n    }\n\n    /// Build the parameters\n    pub fn build(self) -> Option<WindowParams> {\n        if self.params.is_empty() {\n            None\n        } else {\n            Some(self.params)\n        }\n    }\n}\n\n/// Build URL with optional parameters\npub fn build_url_with_params(base_url: &str, params: Option<&WindowParams>) -> String {\n    match params {\n        Some(params) if !params.is_empty() => {\n            let query: Vec<String> = params\n                .iter()\n                .map(|(k, v)| format!(\"{}={}\", urlencoding::encode(k), urlencoding::encode(v)))\n                .collect();\n            format!(\"{}?{}\", base_url, query.join(\"&\"))\n        }\n        _ => base_url.to_string(),\n    }\n}\n/// Window manager for tracking window instances\n#[derive(Debug, Default)]\npub struct WindowManager {\n    /// Maps base label to list of instance labels\n    instances: HashMap<String, Vec<String>>,\n}\n\nimpl WindowManager {\n    /// Get global window manager instance\n    pub fn global() -> &'static Mutex<Self> {\n        WINDOW_MANAGER.get_or_init(|| Mutex::new(Self::default()))\n    }\n\n    /// Generate a unique label for a window\n    ///\n    /// For singleton windows, returns None if an instance already exists.\n    /// For non-singleton windows, generates labels like: base, base-1, base-2, etc.\n    pub fn generate_label(&mut self, base_label: &str, singleton: bool) -> Option<String> {\n        let instances = self.instances.entry(base_label.to_string()).or_default();\n\n        if singleton && !instances.is_empty() {\n            return None; // Singleton window already exists\n        }\n\n        if instances.is_empty() {\n            instances.push(base_label.to_string());\n            return Some(base_label.to_string());\n        }\n\n        // Find the next available number\n        let mut next_num = 1;\n        loop {\n            let label = format!(\"{}-{}\", base_label, next_num);\n            if !instances.contains(&label) {\n                instances.push(label.clone());\n                return Some(label);\n            }\n            next_num += 1;\n        }\n    }\n\n    /// Remove a window instance\n    pub fn remove_instance(&mut self, label: &str) {\n        for instances in self.instances.values_mut() {\n            instances.retain(|l| l != label);\n        }\n    }\n\n    /// Get all instances for a base label\n    pub fn get_instances(&self, base_label: &str) -> Vec<String> {\n        self.instances.get(base_label).cloned().unwrap_or_default()\n    }\n\n    /// Check if a specific label exists\n    pub fn has_instance(&self, label: &str) -> bool {\n        self.instances\n            .values()\n            .any(|instances| instances.contains(&label.to_string()))\n    }\n\n    /// Get the count of instances for a base label\n    pub fn instance_count(&self, base_label: &str) -> usize {\n        self.instances.get(base_label).map(|v| v.len()).unwrap_or(0)\n    }\n}\n/// Event emitted by the frontend when the React app is mounted.\n/// Event name: `react-app-mounted-event`\n#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]\npub struct ReactAppMountedEvent;\n\n/// Message for inter-window communication\n#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]\npub struct WindowMessageEvent {\n    /// Source window label\n    pub from: String,\n    /// Target window label (use \"*\" for broadcast)\n    pub to: String,\n    /// Message type/event name\n    pub event: String,\n    /// Message payload\n    pub payload: serde_json::Value,\n}\n\nimpl WindowMessageEvent {\n    /// Create a new window message\n    pub fn new(\n        from: impl Into<String>,\n        to: impl Into<String>,\n        event: impl Into<String>,\n        payload: serde_json::Value,\n    ) -> Self {\n        Self {\n            from: from.into(),\n            to: to.into(),\n            event: event.into(),\n            payload,\n        }\n    }\n\n    /// Create a broadcast message to all windows\n    pub fn broadcast(\n        from: impl Into<String>,\n        event: impl Into<String>,\n        payload: serde_json::Value,\n    ) -> Self {\n        Self::new(from, \"*\", event, payload)\n    }\n}\n\n/// Send a message to a specific window\npub fn send_message_to_window(app_handle: &AppHandle, message: WindowMessageEvent) -> Result<()> {\n    // Verify window exists\n    let _ = app_handle\n        .get_webview_window(&message.to)\n        .ok_or_else(|| anyhow::anyhow!(\"Window '{}' not found\", message.to))?;\n\n    let target = message.to.clone();\n    message.emit_to(app_handle, target)?;\n    Ok(())\n}\n\n/// Send a message to all instances of a window type\npub fn broadcast_to_window_type(\n    app_handle: &AppHandle,\n    base_label: &str,\n    from: &str,\n    event: &str,\n    payload: serde_json::Value,\n) -> Result<()> {\n    let instances = {\n        let manager = WindowManager::global().lock().unwrap();\n        manager.get_instances(base_label)\n    };\n\n    for label in instances {\n        if app_handle.get_webview_window(&label).is_some() {\n            let message = WindowMessageEvent::new(from, &label, event, payload.clone());\n            trace_err!(\n                message.emit_to(app_handle, &label),\n                \"failed to emit message\"\n            );\n        }\n    }\n    Ok(())\n}\n\n/// Broadcast a message to all open windows\npub fn broadcast_to_all_windows(\n    app_handle: &AppHandle,\n    from: &str,\n    event: &str,\n    payload: serde_json::Value,\n) -> Result<()> {\n    WindowMessageEvent::broadcast(from, event, payload).emit(app_handle)?;\n    Ok(())\n}\n\n/// Result of window creation\n#[derive(Debug, Clone)]\npub struct WindowCreateResult {\n    /// The actual label of the created window\n    pub label: String,\n    /// Whether this was a newly created window or an existing one was shown\n    pub is_new: bool,\n}\n\nimpl WindowCreateResult {\n    fn new(label: String) -> Self {\n        Self {\n            label,\n            is_new: true,\n        }\n    }\n\n    fn existing(label: String) -> Self {\n        Self {\n            label,\n            is_new: false,\n        }\n    }\n}\n\n/// Trait for window management\npub trait AppWindow {\n    /// Get window base label (e.g., \"main\", \"editor\")\n    fn label(&self) -> &str;\n\n    /// Get window title\n    fn title(&self) -> &str;\n\n    /// Get window URL path\n    fn url(&self) -> &str;\n\n    /// Get window configuration\n    fn config(&self) -> WindowConfig {\n        WindowConfig::default()\n    }\n\n    /// Get window state from config\n    fn get_window_state(&self) -> Option<WindowState>;\n\n    /// Set window state to config\n    fn set_window_state(&self, state: Option<WindowState>);\n\n    fn reset_window_open_counter(&self) {\n        OPEN_WINDOWS_COUNTER.fetch_sub(1, Ordering::Release);\n    }\n\n    /// Create window with optional URL parameters\n    ///\n    /// Returns the label of the created (or existing) window\n    fn create_with_params(\n        &self,\n        app_handle: &AppHandle,\n        params: Option<WindowParams>,\n    ) -> Result<WindowCreateResult> {\n        let config = self.config();\n        let base_label = self.label();\n\n        // Clean up stale window records before generating label\n        // This handles cases where the window was destroyed but the record wasn't cleaned up\n        {\n            let mut manager = WindowManager::global().lock().unwrap();\n            let stale_labels: Vec<String> = manager\n                .get_instances(base_label)\n                .into_iter()\n                .filter(|label| app_handle.get_webview_window(label).is_none())\n                .collect();\n            for label in stale_labels {\n                tracing::debug!(\"cleaning up stale window record: {}\", label);\n                manager.remove_instance(&label);\n            }\n        }\n\n        // Generate unique label\n        let label = {\n            let mut manager = WindowManager::global().lock().unwrap();\n            // After cleanup above, generate_label should work correctly\n            // For singleton windows, if it returns None, the window truly exists\n            manager\n                .generate_label(base_label, config.singleton)\n                .unwrap_or_else(|| {\n                    // Singleton window already exists - try to focus it\n                    if let Some(window) = app_handle.get_webview_window(base_label) {\n                        tracing::debug!(\"{} window is already opened, try to focus it\", base_label);\n                        trace_err!(window.unminimize(), \"set win unminimize\");\n                        trace_err!(window.show(), \"set win visible\");\n                        trace_err!(window.set_focus(), \"set win focus\");\n                    }\n                    // Return early indicator - we'll handle this below\n                    String::new()\n                })\n        };\n\n        // Handle singleton window that already exists\n        if label.is_empty() {\n            return Ok(WindowCreateResult::existing(base_label.to_string()));\n        }\n\n        let always_on_top = config.always_on_top.unwrap_or_else(|| {\n            *Config::verge()\n                .latest()\n                .always_on_top\n                .as_ref()\n                .unwrap_or(&false)\n        });\n\n        // Build URL with params\n        let url = build_url_with_params(self.url(), params.as_ref());\n\n        tracing::debug!(\"create {} window (label: {})...\", base_label, label);\n\n        let mut builder = tauri::WebviewWindowBuilder::new(\n            app_handle,\n            label.clone(),\n            tauri::WebviewUrl::App(url.into()),\n        )\n        .title(self.title())\n        .fullscreen(false)\n        .always_on_top(always_on_top)\n        .resizable(config.resizable)\n        .skip_taskbar(config.skip_taskbar)\n        .disable_drag_drop_handler();\n\n        // Apply min/max size\n        if let Some((w, h)) = config.min_size {\n            builder = builder.min_inner_size(w, h);\n        }\n        if let Some((w, h)) = config.max_size {\n            builder = builder.max_inner_size(w, h);\n        }\n\n        let win_state = &self.get_window_state();\n        match win_state {\n            Some(_) => {\n                builder = builder.inner_size(800., 800.).position(0., 0.);\n            }\n            _ => {\n                let (default_width, default_height) = config.default_size;\n\n                #[cfg(target_os = \"windows\")]\n                {\n                    builder = builder.inner_size(default_width, default_height);\n                }\n\n                #[cfg(target_os = \"macos\")]\n                {\n                    // macOS has slightly different height due to title bar\n                    builder = builder.inner_size(default_width, default_height + 6.0);\n                }\n\n                #[cfg(target_os = \"linux\")]\n                {\n                    builder = builder.inner_size(default_width, default_height + 6.0);\n                }\n\n                if config.center {\n                    builder = builder.center();\n                }\n            }\n        };\n\n        #[cfg(windows)]\n        let win_res = builder\n            .decorations(false)\n            .transparent(true)\n            .visible(false)\n            .additional_browser_args(\"--enable-features=msWebView2EnableDraggableRegions --disable-features=OverscrollHistoryNavigation,msExperimentalScrolling\")\n            .build();\n\n        #[cfg(target_os = \"macos\")]\n        let win_res = {\n            let decorations = config.decorations.unwrap_or(true);\n            builder\n                .decorations(decorations)\n                .hidden_title(true)\n                .title_bar_style(tauri::TitleBarStyle::Overlay)\n                .build()\n        };\n\n        #[cfg(target_os = \"linux\")]\n        let win_res = {\n            let decorations = config.decorations.unwrap_or(true);\n            let transparent = config.transparent.unwrap_or(false);\n            builder\n                .decorations(decorations)\n                .transparent(transparent)\n                .build()\n        };\n\n        match win_res {\n            Ok(win) => {\n                use tauri::{PhysicalPosition, PhysicalSize};\n\n                if win_state.is_some() {\n                    let state = win_state.as_ref().unwrap();\n                    let _ = win.set_position(PhysicalPosition {\n                        x: state.x,\n                        y: state.y,\n                    });\n                    // Clamp restored size to min_size to prevent 0x0 windows\n                    let mut width = state.width;\n                    let mut height = state.height;\n                    if let Some((min_w, min_h)) = config.min_size {\n                        let scale_factor = win.scale_factor().unwrap_or(1.0);\n                        let min_w_physical = (min_w * scale_factor) as u32;\n                        let min_h_physical = (min_h * scale_factor) as u32;\n                        if width < min_w_physical {\n                            width = min_w_physical;\n                        }\n                        if height < min_h_physical {\n                            height = min_h_physical;\n                        }\n                    }\n                    let _ = win.set_size(PhysicalSize { width, height });\n                }\n\n                if let Some(state) = win_state {\n                    if state.maximized {\n                        trace_err!(win.maximize(), \"set win maximize\");\n                    }\n                    if state.fullscreen {\n                        trace_err!(win.set_fullscreen(true), \"set win fullscreen\");\n                    }\n                }\n                #[cfg(windows)]\n                trace_err!(win.set_shadow(true), \"set win shadow\");\n                log::trace!(\"try to calculate the monitor size\");\n                let center = (|| -> Result<bool> {\n                    let center;\n                    if let Some(state) = win_state {\n                        let monitor = win.current_monitor()?.ok_or(anyhow::anyhow!(\"\"))?;\n                        let PhysicalPosition { x, y } = *monitor.position();\n                        let PhysicalSize { width, height } = *monitor.size();\n                        let left = x;\n                        let right = x + width as i32;\n                        let top = y;\n                        let bottom = y + height as i32;\n\n                        let x = state.x;\n                        let y = state.y;\n                        let width = state.width as i32;\n                        let height = state.height as i32;\n                        center = ![\n                            (x, y),\n                            (x + width, y),\n                            (x, y + height),\n                            (x + width, y + height),\n                        ]\n                        .into_iter()\n                        .any(|(x, y)| x >= left && x < right && y >= top && y < bottom);\n                    } else {\n                        center = true;\n                    }\n                    Ok(center)\n                })();\n\n                if center.unwrap_or(true) {\n                    trace_err!(win.center(), \"set win center\");\n                }\n\n                #[cfg(debug_assertions)]\n                {\n                    if let Some(webview_window) = win.get_webview_window(&label) {\n                        webview_window.open_devtools();\n                    }\n                }\n\n                #[cfg(target_os = \"macos\")]\n                {\n                    tracing::trace!(\"setup traffic lights pos\");\n                    let mtm = objc2_foundation::MainThreadMarker::new().unwrap();\n                    crate::window::macos::setup_traffic_lights_pos(win.clone(), (18.0, 22.0), mtm);\n                }\n\n                // Register window close event to clean up WindowManager\n                let label_clone = label.clone();\n                win.on_window_event(move |event| {\n                    if let tauri::WindowEvent::Destroyed = event {\n                        tracing::debug!(\"window {} destroyed, removing from manager\", label_clone);\n                        let mut manager = WindowManager::global().lock().unwrap();\n                        manager.remove_instance(&label_clone);\n                        OPEN_WINDOWS_COUNTER.fetch_sub(1, Ordering::Release);\n                    }\n                });\n\n                OPEN_WINDOWS_COUNTER.fetch_add(1, Ordering::Release);\n                Ok(WindowCreateResult::new(label))\n            }\n            Err(err) => {\n                log::error!(target: \"app\", \"failed to create window, {err:?}\");\n                // Remove from manager on failure\n                {\n                    let mut manager = WindowManager::global().lock().unwrap();\n                    manager.remove_instance(&label);\n                }\n                if let Some(win) = app_handle.get_webview_window(&label) {\n                    // Cleanup window if failed to create, it's a workaround for tauri bug\n                    log_err!(\n                        win.destroy(),\n                        \"occur error when close window while failed to create\"\n                    );\n                }\n                Err(err.into())\n            }\n        }\n    }\n\n    /// Create window with default implementation (no params)\n    fn create(&self, app_handle: &AppHandle) -> Result<()> {\n        let result = self.create_with_params(app_handle, None)?;\n\n        // Configure webview settings asynchronously to avoid blocking\n        #[cfg(target_os = \"windows\")]\n        if result.is_new {\n            let label = result.label.clone();\n            let app_handle = app_handle.clone();\n            std::thread::spawn(move || {\n                use webview2_com::Microsoft::Web::WebView2::Win32::ICoreWebView2Settings6;\n                use windows_core::Interface;\n\n                // Wait a bit for webview to be ready\n                std::thread::sleep(std::time::Duration::from_millis(100));\n\n                if let Some(window) = app_handle.get_webview_window(&label) {\n                    let _ = window.with_webview(|webview| unsafe {\n                        if let Ok(core) = webview.controller().CoreWebView2() {\n                            if let Ok(settings) = core.Settings() {\n                                if let Ok(settings6) = settings.cast::<ICoreWebView2Settings6>() {\n                                    let _ = settings6.SetIsSwipeNavigationEnabled(false);\n                                }\n                            }\n                        }\n                    });\n                }\n            });\n        }\n\n        Ok(())\n    }\n\n    /// Close window by label\n    ///\n    /// Note: The WindowManager cleanup is handled automatically by the\n    /// on_window_event callback registered during window creation.\n    fn close_by_label(&self, app_handle: &AppHandle, label: &str) {\n        if let Some(window) = app_handle.get_webview_window(label) {\n            trace_err!(window.close(), \"close window\");\n            // WindowManager cleanup is handled by on_window_event(Destroyed)\n        }\n    }\n\n    /// Close window with default implementation (closes the base label window)\n    fn close(&self, app_handle: &AppHandle) {\n        self.close_by_label(app_handle, self.label());\n    }\n\n    /// Close all instances of this window type\n    fn close_all(&self, app_handle: &AppHandle) {\n        let instances = {\n            let manager = WindowManager::global().lock().unwrap();\n            manager.get_instances(self.label())\n        };\n        for label in instances {\n            self.close_by_label(app_handle, &label);\n        }\n    }\n\n    /// Check if the base label window is open\n    fn is_open(&self, app_handle: &AppHandle) -> bool {\n        app_handle.get_webview_window(self.label()).is_some()\n    }\n\n    /// Check if any instance of this window type is open\n    fn has_any_instance(&self, app_handle: &AppHandle) -> bool {\n        let manager = WindowManager::global().lock().unwrap();\n        let instances = manager.get_instances(self.label());\n        instances\n            .iter()\n            .any(|label| app_handle.get_webview_window(label).is_some())\n    }\n\n    /// Get all open window labels for this type\n    fn get_open_instances(&self, app_handle: &AppHandle) -> Vec<String> {\n        let manager = WindowManager::global().lock().unwrap();\n        manager\n            .get_instances(self.label())\n            .into_iter()\n            .filter(|label| app_handle.get_webview_window(label).is_some())\n            .collect()\n    }\n\n    /// Send a message to another window\n    fn send_message(\n        &self,\n        app_handle: &AppHandle,\n        to: &str,\n        event: &str,\n        payload: serde_json::Value,\n    ) -> Result<()> {\n        let message = WindowMessageEvent::new(self.label(), to, event, payload);\n        send_message_to_window(app_handle, message)\n    }\n\n    /// Broadcast a message to all instances of another window type\n    fn broadcast_to_type(\n        &self,\n        app_handle: &AppHandle,\n        target_type: &str,\n        event: &str,\n        payload: serde_json::Value,\n    ) -> Result<()> {\n        broadcast_to_window_type(app_handle, target_type, self.label(), event, payload)\n    }\n\n    /// Save window state with default implementation\n    fn save_state(&self, app_handle: &AppHandle, save_to_file: bool) -> Result<()> {\n        let win = app_handle\n            .get_webview_window(self.label())\n            .ok_or(anyhow::anyhow!(\"failed to get window\"))?;\n        let current_monitor = win.current_monitor()?;\n\n        let state = match current_monitor {\n            Some(_) => {\n                let maximized = win.is_maximized()?;\n                let fullscreen = win.is_fullscreen()?;\n                let is_minimized = win.is_minimized()?;\n                let size = win.inner_size()?;\n\n                // During system shutdown, Windows sends resize events with 0x0 dimensions.\n                // Skip saving in this case to preserve the last valid window state.\n                if size.width == 0 || size.height == 0 {\n                    if !maximized && !fullscreen && !is_minimized {\n                        tracing::debug!(\n                            \"skipping window state save: invalid size {}x{} in normal state\",\n                            size.width,\n                            size.height\n                        );\n                        return Ok(());\n                    }\n                }\n\n                let mut state = WindowState {\n                    maximized,\n                    fullscreen,\n                    ..WindowState::default()\n                };\n\n                if size.width > 0 && size.height > 0 && !state.maximized && !is_minimized {\n                    state.width = size.width;\n                    state.height = size.height;\n                }\n                let position = win.outer_position()?;\n                if !state.maximized && !is_minimized {\n                    state.x = position.x;\n                    state.y = position.y;\n                }\n                Some(state)\n            }\n            None => None,\n        };\n\n        self.set_window_state(state);\n\n        if save_to_file {\n            Config::verge().data().save_file()?;\n        }\n\n        Ok(())\n    }\n}\n\n#[cfg(target_os = \"macos\")]\npub mod macos {\n    #![allow(non_snake_case)]\n    use std::cell::RefCell;\n\n    use objc2::{\n        DeclaredClass, MainThreadOnly, define_class, msg_send, rc::Retained,\n        runtime::ProtocolObject,\n    };\n    use objc2_app_kit::{NSApplicationPresentationOptions, NSWindow, NSWindowDelegate};\n    use objc2_foundation::{MainThreadMarker, NSNotification, NSObject, NSObjectProtocol};\n    use tauri::{Emitter, Listener, Manager, Runtime, WebviewWindow, Window, WindowEvent};\n\n    #[derive(Debug, Clone, Copy)]\n    pub struct Position {\n        pub x: f64,\n        pub y: f64,\n    }\n\n    impl From<(f64, f64)> for Position {\n        fn from(value: (f64, f64)) -> Self {\n            Self {\n                x: value.0,\n                y: value.1,\n            }\n        }\n    }\n\n    impl From<Position> for (f64, f64) {\n        fn from(value: Position) -> Self {\n            (value.x, value.y)\n        }\n    }\n\n    fn set_traffic_lights_pos(\n        window: objc2::rc::Retained<objc2_app_kit::NSWindow>,\n        pos: Position,\n    ) -> anyhow::Result<()> {\n        use objc2_app_kit::NSWindowButton;\n        use objc2_foundation::NSRect;\n        let close = window\n            .standardWindowButton(NSWindowButton::CloseButton)\n            .ok_or(anyhow::anyhow!(\"failed to get close button\"))?;\n        let miniaturize = window\n            .standardWindowButton(NSWindowButton::MiniaturizeButton)\n            .ok_or(anyhow::anyhow!(\"failed to get miniaturize button\"))?;\n        let zoom = window\n            .standardWindowButton(NSWindowButton::ZoomButton)\n            .ok_or(anyhow::anyhow!(\"failed to get zoom button\"))?;\n\n        let title_bar_container_view = unsafe {\n            close\n                .superview()\n                .and_then(|view| view.superview())\n                .ok_or(anyhow::anyhow!(\"failed to get title bar container view\"))?\n        };\n\n        let close_rect = close.frame();\n        let button_height = close_rect.size.height;\n\n        let title_bar_frame_height = button_height + pos.y;\n        let mut title_bar_rect = title_bar_container_view.frame();\n        title_bar_rect.size.height = title_bar_frame_height;\n        title_bar_rect.origin.y = window.frame().size.height - title_bar_frame_height;\n        unsafe {\n            title_bar_container_view.setFrame(title_bar_rect);\n        }\n\n        let space_between = miniaturize.frame().origin.x - close.frame().origin.x;\n        let window_buttons = vec![close, miniaturize, zoom];\n\n        for (i, button) in window_buttons.into_iter().enumerate() {\n            let mut rect: NSRect = button.frame();\n            rect.origin.x = pos.x + (i as f64 * space_between);\n            unsafe {\n                button.setFrameOrigin(rect.origin);\n            }\n        }\n        Ok(())\n    }\n\n    #[derive(Debug, Clone)]\n    struct WindowState {\n        window: WebviewWindow<tauri::Wry>,\n        traffic_lights_pos: Position,\n    }\n\n    impl WindowState {\n        fn new(window: WebviewWindow<tauri::Wry>, traffic_lights_pos: Position) -> Self {\n            Self {\n                window,\n                traffic_lights_pos,\n            }\n        }\n\n        fn with_ns_window<T>(&self, func: impl FnOnce(Retained<NSWindow>) -> T) -> T {\n            let ns_window = self.window.ns_window().expect(\"window not found\");\n            let ns_window = unsafe { Retained::retain_autoreleased(ns_window as *mut NSWindow) }\n                .expect(\"failed to retain window\");\n            func(ns_window)\n        }\n\n        fn apply_traffic_lights_pos(&self) {\n            self.with_ns_window(|win| {\n                set_traffic_lights_pos(win, self.traffic_lights_pos)\n                    .expect(\"failed to set traffic lights pos\");\n            });\n        }\n    }\n\n    #[derive(Debug)]\n    struct TrafficLightsWindowDelegateIvars {\n        app_box: WindowState,\n        super_class: Retained<ProtocolObject<dyn NSWindowDelegate>>,\n    }\n\n    const WINDOW_DID_ENTER_FULL_SCREEN: &str = \"internal:://window-did-enter-full-screen\";\n    const WINDOW_WILL_ENTER_FULL_SCREEN: &str = \"internal:://window-will-enter-full-screen\";\n    const WINDOW_WILL_EXIT_FULL_SCREEN: &str = \"internal:://window-will-exit-full-screen\";\n    const WINDOW_DID_EXIT_FULL_SCREEN: &str = \"internal:://window-did-exit-full-screen\";\n\n    define_class! {\n        #[unsafe(super(NSObject))]\n        #[name = \"TrafficLightsPosWindowDelegate\"]\n        #[thread_kind = MainThreadOnly]\n        #[ivars = TrafficLightsWindowDelegateIvars]\n        struct WindowDelegate;\n\n        unsafe impl NSObjectProtocol for WindowDelegate {}\n\n        unsafe impl NSWindowDelegate for WindowDelegate {\n            #[unsafe(method(windowShouldClose:))]\n            unsafe fn windowShouldClose(&self, sender: &NSWindow) -> bool {\n                tracing::trace!(\"passthrough `windowShouldClose` to TAO layer\");\n                unsafe { self.ivars().super_class.windowShouldClose(sender) }\n            }\n\n            #[unsafe(method(windowWillClose:))]\n            unsafe fn windowWillClose(&self, notification: &NSNotification) {\n                tracing::trace!(\"passthrough `windowWillClose` to TAO layer\");\n                unsafe { self.ivars().super_class.windowWillClose(notification) }\n            }\n\n            #[unsafe(method(windowDidResize:))]\n            unsafe fn windowDidResize(&self, notification: &NSNotification) {\n                self.ivars().app_box.apply_traffic_lights_pos();\n                tracing::trace!(\"passthrough `windowDidResize` to TAO layer\");\n                unsafe { self.ivars().super_class.windowDidResize(notification) }\n            }\n\n            #[unsafe(method(windowDidMove:))]\n            unsafe fn windowDidMove(&self, notification: &NSNotification) {\n                tracing::trace!(\"passthrough `windowDidMove` to TAO layer\");\n                unsafe { self.ivars().super_class.windowDidMove(notification) }\n            }\n\n            #[unsafe(method(windowDidChangeBackingProperties:))]\n            unsafe fn windowDidChangeBackingProperties(&self, notification: &NSNotification) {\n                self.ivars().app_box.apply_traffic_lights_pos();\n                tracing::trace!(\"passthrough `windowDidChangeBackingProperties` to TAO layer\");\n                unsafe { self.ivars().super_class.windowDidChangeBackingProperties(notification) }\n            }\n\n            #[unsafe(method(windowDidBecomeKey:))]\n            unsafe fn windowDidBecomeKey(&self, notification: &NSNotification) {\n                tracing::trace!(\"passthrough `windowDidBecomeKey` to TAO layer\");\n                unsafe { self.ivars().super_class.windowDidBecomeKey(notification) }\n            }\n\n            #[unsafe(method(windowDidResignKey:))]\n            unsafe fn windowDidResignKey(&self, notification: &NSNotification) {\n                tracing::trace!(\"passthrough `windowDidResignKey` to TAO layer\");\n                unsafe { self.ivars().super_class.windowDidResignKey(notification) }\n            }\n\n            #[unsafe(method(window:willUseFullScreenPresentationOptions:))]\n            unsafe fn window_willUseFullScreenPresentationOptions(&self, window: &NSWindow, options: NSApplicationPresentationOptions) -> NSApplicationPresentationOptions {\n                tracing::trace!(\"passthrough `window_willUseFullScreenPresentationOptions` to TAO layer\");\n                unsafe { self.ivars().super_class.window_willUseFullScreenPresentationOptions(window, options) }\n            }\n\n            #[unsafe(method(windowDidEnterFullScreen:))]\n            unsafe fn windowDidEnterFullScreen(&self, notification: &NSNotification) {\n                if let Err(e) = self.ivars().app_box.window.emit(WINDOW_DID_ENTER_FULL_SCREEN, ()) {\n                    log::error!(\"failed to emit window-did-enter-full-screen event: {}\", e);\n                }\n                tracing::trace!(\"passthrough `windowDidEnterFullScreen` to TAO layer\");\n                unsafe { self.ivars().super_class.windowDidEnterFullScreen(notification) }\n            }\n\n            #[unsafe(method(windowWillEnterFullScreen:))]\n            unsafe fn windowWillEnterFullScreen(&self, notification: &NSNotification) {\n                if let Err(e) = self.ivars().app_box.window.emit(WINDOW_WILL_ENTER_FULL_SCREEN, ()) {\n                    log::error!(\"failed to emit window-will-enter-full-screen event: {}\", e);\n                }\n                unsafe { self.ivars().super_class.windowWillEnterFullScreen(notification) }\n            }\n\n            #[unsafe(method(windowWillExitFullScreen:))]\n            unsafe fn windowWillExitFullScreen(&self, notification: &NSNotification) {\n                if let Err(e) = self.ivars().app_box.window.emit(WINDOW_WILL_EXIT_FULL_SCREEN, ()) {\n                    log::error!(\"failed to emit window-will-exit-full-screen event: {}\", e);\n                }\n                tracing::trace!(\"passthrough `windowWillExitFullScreen` to TAO layer\");\n                unsafe { self.ivars().super_class.windowWillExitFullScreen(notification) }\n            }\n\n            #[unsafe(method(windowDidExitFullScreen:))]\n            unsafe fn windowDidExitFullScreen(&self, notification: &NSNotification) {\n                if let Err(e) = self.ivars().app_box.window.emit(WINDOW_DID_EXIT_FULL_SCREEN, ()) {\n                    log::error!(\"failed to emit window-did-exit-full-screen event: {}\", e);\n                }\n                self.ivars().app_box.apply_traffic_lights_pos();\n                tracing::trace!(\"passthrough `windowDidExitFullScreen` to TAO layer\");\n                unsafe { self.ivars().super_class.windowDidExitFullScreen(notification) }\n            }\n\n            #[unsafe(method(windowDidFailToEnterFullScreen:))]\n            unsafe fn windowDidFailToEnterFullScreen(&self,window: &NSWindow) {\n                tracing::trace!(\"passthrough `windowDidFailToEnterFullScreen` to TAO layer\");\n                unsafe { self.ivars().super_class.windowDidFailToEnterFullScreen(window) }\n            }\n\n        }\n    }\n\n    impl WindowDelegate {\n        pub fn new(window_state: WindowState, mtm: MainThreadMarker) -> Retained<Self> {\n            let this = Self::alloc(mtm);\n            let super_class = window_state\n                .with_ns_window(|win| unsafe { win.delegate().expect(\"failed to get delegate\") });\n            let ivars = TrafficLightsWindowDelegateIvars {\n                app_box: window_state,\n                super_class,\n            };\n            let this = this.set_ivars(ivars);\n            unsafe { msg_send![super(this), init] }\n        }\n    }\n\n    pub struct TrafficLightsWindowDelegateGuard {\n        _delegate: Retained<WindowDelegate>,\n    }\n\n    thread_local! {\n        /// This is used to keep the delegate alive until the window is destroyed\n        static TRAFFIC_LIGHTS_WINDOW_DELEGATE_GUARD: RefCell<Option<TrafficLightsWindowDelegateGuard>> = const { RefCell::new(None) };\n    }\n\n    pub fn setup_traffic_lights_pos(window: WebviewWindow, pos: (f64, f64), mtm: MainThreadMarker) {\n        let window_state = WindowState::new(window.clone(), pos.into());\n        let ns_window = window_state.with_ns_window(|win| win);\n        let window_state_clone = window_state.clone();\n        window.on_window_event(move |event| match event {\n            WindowEvent::ThemeChanged(_) => {\n                window_state_clone.apply_traffic_lights_pos();\n            }\n            WindowEvent::Destroyed => {\n                let _ = TRAFFIC_LIGHTS_WINDOW_DELEGATE_GUARD.take();\n            }\n            _ => {}\n        });\n        // first apply the traffic lights pos\n        window_state.apply_traffic_lights_pos();\n        let delegate = WindowDelegate::new(window_state, mtm);\n        let object: &ProtocolObject<dyn NSWindowDelegate> = ProtocolObject::from_ref(&*delegate);\n        ns_window.setDelegate(Some(object));\n        TRAFFIC_LIGHTS_WINDOW_DELEGATE_GUARD.replace(Some(TrafficLightsWindowDelegateGuard {\n            _delegate: delegate,\n        }));\n    }\n}\n"
  },
  {
    "path": "backend/tauri/tauri.conf.json",
    "content": "{\n  \"$schema\": \"../../node_modules/@tauri-apps/cli/config.schema.json\",\n  \"mainBinaryName\": \"Clash Nyanpasu\",\n  \"bundle\": {\n    \"active\": true,\n    \"targets\": \"all\",\n    \"windows\": {\n      \"certificateThumbprint\": null,\n      \"digestAlgorithm\": \"sha256\",\n      \"timestampUrl\": \"\",\n      \"webviewInstallMode\": {\n        \"type\": \"embedBootstrapper\"\n      },\n      \"wix\": {\n        \"language\": [\"en-US\", \"ru-RU\", \"zh-CN\", \"zh-TW\"],\n        \"template\": \"./templates/installer.wxs\",\n        \"fragmentPaths\": [\"./templates/cleanup.wxs\"]\n      },\n      \"nsis\": {\n        \"displayLanguageSelector\": true,\n        \"installerIcon\": \"icons/icon.ico\",\n        \"languages\": [\"English\", \"Russian\", \"SimpChinese\", \"TradChinese\"],\n        \"template\": \"./templates/installer.nsi\",\n        \"installMode\": \"both\"\n      }\n    },\n    \"icon\": [\n      \"icons/32x32.png\",\n      \"icons/128x128.png\",\n      \"icons/128x128@2x.png\",\n      \"icons/icon.icns\",\n      \"icons/icon.ico\"\n    ],\n    \"resources\": [\"resources\"],\n    \"externalBin\": [\n      \"sidecar/clash\",\n      \"sidecar/mihomo\",\n      \"sidecar/mihomo-alpha\",\n      \"sidecar/clash-rs\",\n      \"sidecar/clash-rs-alpha\",\n      \"sidecar/nyanpasu-service\"\n    ],\n    \"copyright\": \"© 2024-2026 Clash Nyanpasu All Rights Reserved\",\n    \"category\": \"DeveloperTool\",\n    \"shortDescription\": \"Clash Nyanpasu! (∠・ω< )⌒☆\",\n    \"longDescription\": \"Clash Nyanpasu! (∠・ω< )⌒☆\",\n    \"macOS\": {\n      \"frameworks\": [],\n      \"minimumSystemVersion\": \"12.6\",\n      \"exceptionDomain\": \"\",\n      \"signingIdentity\": null,\n      \"entitlements\": null\n    },\n    \"linux\": {\n      \"deb\": {\n        \"depends\": []\n      }\n    },\n    \"licenseFile\": \"../../LICENSE\",\n    \"createUpdaterArtifacts\": \"v1Compatible\"\n  },\n  \"build\": {\n    \"beforeBuildCommand\": \"pnpm run-p web:build generate:git-info && echo $(pwd)\",\n    \"frontendDist\": \"./tmp/dist\",\n    \"beforeDevCommand\": \"pnpm run web:dev\",\n    \"devUrl\": \"http://localhost:3000/\"\n  },\n  \"productName\": \"Clash Nyanpasu\",\n  \"version\": \"1.6.0\",\n  \"identifier\": \"moe.elaina.clash.nyanpasu\",\n  \"plugins\": {\n    \"updater\": {\n      \"pubkey\": \"dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDlBMUM0NjMxREZCNDRGMjYKUldRbVQ3VGZNVVljbW43N0FlWjA4UkNrbTgxSWxSSXJQcExXNkZjUTlTQkIyYkJzL0tsSWF2d0cK\",\n      \"endpoints\": [\n        \"https://deno.elaina.moe/updater/update-proxy.json\",\n        \"https://nyanpasu.surge.sh/updater/update-proxy.json\",\n        \"https://gh-proxy.com/https://github.com/libnyanpasu/clash-nyanpasu/releases/download/updater/update-proxy.json\",\n        \"https://github.com/libnyanpasu/clash-nyanpasu/releases/download/updater/update.json\"\n      ]\n    }\n  },\n  \"app\": {\n    \"windows\": [],\n    \"security\": {\n      \"csp\": \"script-src 'unsafe-eval' 'self'; default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self'; img-src 'self' data: asset: blob: http://localhost:* https:; connect-src ipc: http://ipc.localhost http://127.0.0.1:* http://localhost:* ws://127.0.0.1:* ws://localhost:* wss://*\",\n      \"capabilities\": [\"main-capability\"]\n    }\n  }\n}\n"
  },
  {
    "path": "backend/tauri/tauri.windows.conf.json",
    "content": "{\n  \"$schema\": \"../../node_modules/@tauri-apps/cli/config.schema.json\",\n  \"bundle\": {\n    \"targets\": [\"nsis\"]\n  }\n}\n"
  },
  {
    "path": "backend/tauri/templates/cleanup.wxs",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<Wix xmlns=\"http://schemas.microsoft.com/wix/2006/wi\"\n  xmlns:util=\"http://schemas.microsoft.com/wix/UtilExtension\">\n  <Fragment>\n    <!-- close nyanpasu processes while install-->\n    <util:CloseApplication Id=\"NyanpasuCloseApp\" Target=\"Clash Nyanpasu.exe\" CloseMessage=\"yes\" RebootPrompt=\"no\" TerminateProcess=\"1\">\n    </util:CloseApplication>\n    <util:CloseApplication Id=\"NyanpasuCloseService\" Target=\"clash-verge-service.exe\" CloseMessage=\"yes\" RebootPrompt=\"no\" TerminateProcess=\"1\">\n    </util:CloseApplication>\n    <util:CloseApplication Id=\"NyanpasuCloseMihomo\" Target=\"mihomo.exe\" CloseMessage=\"yes\" RebootPrompt=\"no\" TerminateProcess=\"1\">\n    </util:CloseApplication>\n    <util:CloseApplication Id=\"NyanpasuCloseMihomoAlpha\" Target=\"mihomo-alpha.exe\" CloseMessage=\"yes\" RebootPrompt=\"no\" TerminateProcess=\"1\">\n    </util:CloseApplication>\n    <util:CloseApplication Id=\"NyanpasuCloseService\" Target=\"clash-rs.exe\" CloseMessage=\"yes\" RebootPrompt=\"no\" TerminateProcess=\"1\">\n    </util:CloseApplication>\n    <util:CloseApplication Id=\"NyanpasuCloseService\" Target=\"clash.exe\" CloseMessage=\"yes\" RebootPrompt=\"no\" TerminateProcess=\"1\">\n    </util:CloseApplication>\n  </Fragment>\n</Wix>\n"
  },
  {
    "path": "backend/tauri/templates/installer.nsi",
    "content": "; This file is copied from https://github.com/tauri-apps/tauri/blob/tauri-v1.5/tooling/bundler/src/bundle/windows/templates/installer.nsi\n; and edit to fit the needs of the project. the latest tauri 2.x has a different base nsi script.\n\nUnicode true\n; Set the compression algorithm. Default is LZMA.\n!if \"{{compression}}\" == \"\"\n  SetCompressor /SOLID lzma\n!else\n  SetCompressor /SOLID \"{{compression}}\"\n!endif\n\n!include MUI2.nsh\n!include FileFunc.nsh\n!include x64.nsh\n!include WordFunc.nsh\n!include \"LogicLib.nsh\"\n!include \"StrFunc.nsh\"\n!include \"Win\\COM.nsh\"\n!include \"Win\\Propkey.nsh\"\n${StrCase}\n${StrLoc}\n\n!define MANUFACTURER \"{{manufacturer}}\"\n!define PRODUCTNAME \"{{product_name}}\"\n!define VERSION \"{{version}}\"\n!define VERSIONWITHBUILD \"{{version_with_build}}\"\n!define SHORTDESCRIPTION \"{{short_description}}\"\n!define INSTALLMODE \"{{install_mode}}\"\n!define LICENSE \"{{license}}\"\n!define INSTALLERICON \"{{installer_icon}}\"\n!define SIDEBARIMAGE \"{{sidebar_image}}\"\n!define HEADERIMAGE \"{{header_image}}\"\n!define MAINBINARYNAME \"{{main_binary_name}}\"\n!define MAINBINARYSRCPATH \"{{main_binary_path}}\"\n!define BUNDLEID \"{{bundle_id}}\"\n!define COPYRIGHT \"{{copyright}}\"\n!define OUTFILE \"{{out_file}}\"\n!define ARCH \"{{arch}}\"\n!define PLUGINSPATH \"{{additional_plugins_path}}\"\n!define ALLOWDOWNGRADES \"{{allow_downgrades}}\"\n!define DISPLAYLANGUAGESELECTOR \"{{display_language_selector}}\"\n!define INSTALLWEBVIEW2MODE \"{{install_webview2_mode}}\"\n!define WEBVIEW2INSTALLERARGS \"{{webview2_installer_args}}\"\n!define WEBVIEW2BOOTSTRAPPERPATH \"{{webview2_bootstrapper_path}}\"\n!define WEBVIEW2INSTALLERPATH \"{{webview2_installer_path}}\"\n!define UNINSTKEY \"Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${PRODUCTNAME}\"\n!define MANUPRODUCTKEY \"Software\\${MANUFACTURER}\\${PRODUCTNAME}\"\n!define UNINSTALLERSIGNCOMMAND \"{{uninstaller_sign_cmd}}\"\n!define ESTIMATEDSIZE \"{{estimated_size}}\"\n\nVar ProgramDataPathVar\n\nName \"${PRODUCTNAME}\"\nBrandingText \"${COPYRIGHT}\"\nOutFile \"${OUTFILE}\"\n\nVIProductVersion \"${VERSIONWITHBUILD}\"\nVIAddVersionKey \"ProductName\" \"${PRODUCTNAME}\"\nVIAddVersionKey \"FileDescription\" \"${SHORTDESCRIPTION}\"\nVIAddVersionKey \"LegalCopyright\" \"${COPYRIGHT}\"\nVIAddVersionKey \"FileVersion\" \"${VERSION}\"\nVIAddVersionKey \"ProductVersion\" \"${VERSION}\"\n\n; Plugins path, currently exists for linux only\n!if \"${PLUGINSPATH}\" != \"\"\n    !addplugindir \"${PLUGINSPATH}\"\n!endif\n\n!if \"${UNINSTALLERSIGNCOMMAND}\" != \"\"\n  !uninstfinalize '${UNINSTALLERSIGNCOMMAND}'\n!endif\n\n; Handle install mode, `perUser`, `perMachine` or `both`\n!if \"${INSTALLMODE}\" == \"perMachine\"\n  RequestExecutionLevel highest\n!endif\n\n!if \"${INSTALLMODE}\" == \"currentUser\"\n  RequestExecutionLevel user\n!endif\n\n!if \"${INSTALLMODE}\" == \"both\"\n  !define MULTIUSER_MUI\n  !define MULTIUSER_INSTALLMODE_INSTDIR \"${PRODUCTNAME}\"\n  !define MULTIUSER_INSTALLMODE_COMMANDLINE\n  !if \"${ARCH}\" == \"x64\"\n    !define MULTIUSER_USE_PROGRAMFILES64\n  !else if \"${ARCH}\" == \"arm64\"\n    !define MULTIUSER_USE_PROGRAMFILES64\n  !endif\n  !define MULTIUSER_INSTALLMODE_DEFAULT_REGISTRY_KEY \"${UNINSTKEY}\"\n  !define MULTIUSER_INSTALLMODE_DEFAULT_REGISTRY_VALUENAME \"CurrentUser\"\n  !define MULTIUSER_INSTALLMODEPAGE_SHOWUSERNAME\n  !define MULTIUSER_INSTALLMODE_FUNCTION RestorePreviousInstallLocation\n  !define MULTIUSER_EXECUTIONLEVEL Highest\n  !include MultiUser.nsh\n!endif\n\n; installer icon\n!if \"${INSTALLERICON}\" != \"\"\n  !define MUI_ICON \"${INSTALLERICON}\"\n!endif\n\n; installer sidebar image\n!if \"${SIDEBARIMAGE}\" != \"\"\n  !define MUI_WELCOMEFINISHPAGE_BITMAP \"${SIDEBARIMAGE}\"\n!endif\n\n; installer header image\n!if \"${HEADERIMAGE}\" != \"\"\n  !define MUI_HEADERIMAGE\n  !define MUI_HEADERIMAGE_BITMAP  \"${HEADERIMAGE}\"\n!endif\n\n; Define registry key to store installer language\n!define MUI_LANGDLL_REGISTRY_ROOT \"HKCU\"\n!define MUI_LANGDLL_REGISTRY_KEY \"${MANUPRODUCTKEY}\"\n!define MUI_LANGDLL_REGISTRY_VALUENAME \"Installer Language\"\n\n; Installer pages, must be ordered as they appear\n; 1. Welcome Page\n!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfPassive\n!insertmacro MUI_PAGE_WELCOME\n\n; 2. License Page (if defined)\n!if \"${LICENSE}\" != \"\"\n  !define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfPassive\n  !insertmacro MUI_PAGE_LICENSE \"${LICENSE}\"\n!endif\n\n; 3. Install mode (if it is set to `both`)\n!if \"${INSTALLMODE}\" == \"both\"\n  !define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfPassive\n  !insertmacro MULTIUSER_PAGE_INSTALLMODE\n!endif\n\n\n; 4. Custom page to ask user if he wants to reinstall/uninstall\n;    only if a previous installtion was detected\nVar ReinstallPageCheck\nPage custom PageReinstall PageLeaveReinstall\nFunction PageReinstall\n  ; Uninstall previous WiX installation if exists.\n  ;\n  ; A WiX installer stores the isntallation info in registry\n  ; using a UUID and so we have to loop through all keys under\n  ; `HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall`\n  ; and check if `DisplayName` and `Publisher` keys match ${PRODUCTNAME} and ${MANUFACTURER}\n  ;\n  ; This has a potentional issue that there maybe another installation that matches\n  ; our ${PRODUCTNAME} and ${MANUFACTURER} but wasn't installed by our WiX installer,\n  ; however, this should be fine since the user will have to confirm the uninstallation\n  ; and they can chose to abort it if doesn't make sense.\n  StrCpy $0 0\n\n  wix_loop:\n    EnumRegKey $1 HKLM \"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\" $0\n    StrCmp $1 \"\" wix_done ; Exit loop if there is no more keys to loop on\n    IntOp $0 $0 + 1\n    ReadRegStr $R0 HKLM \"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\$1\" \"DisplayName\"\n    ReadRegStr $R1 HKLM \"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\$1\" \"Publisher\"\n    StrCmp \"$R0$R1\" \"${PRODUCTNAME}${MANUFACTURER}\" 0 wix_loop\n    ReadRegStr $R0 HKLM \"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\$1\" \"UninstallString\"\n    ${StrCase} $R1 $R0 \"L\"\n    ${StrLoc} $R0 $R1 \"msiexec\" \">\"\n    StrCmp $R0 0 0 wix_done\n    StrCpy $R7 \"wix\"\n    StrCpy $R6 \"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\$1\"\n    Goto compare_version\n  wix_done:\n\n  ; Check if there is an existing installation, if not, abort the reinstall page\n  ReadRegStr $R0 SHCTX \"${UNINSTKEY}\" \"\"\n  ReadRegStr $R1 SHCTX \"${UNINSTKEY}\" \"UninstallString\"\n  ${IfThen} \"$R0$R1\" == \"\" ${|} Abort ${|}\n\n  ; Compare this installar version with the existing installation\n  ; and modify the messages presented to the user accordingly\n  compare_version:\n  StrCpy $R4 \"$(older)\"\n  ${If} $R7 == \"wix\"\n    ReadRegStr $R0 HKLM \"$R6\" \"DisplayVersion\"\n  ${Else}\n    ReadRegStr $R0 SHCTX \"${UNINSTKEY}\" \"DisplayVersion\"\n  ${EndIf}\n  ${IfThen} $R0 == \"\" ${|} StrCpy $R4 \"$(unknown)\" ${|}\n\n  nsis_tauri_utils::SemverCompare \"${VERSION}\" $R0\n  Pop $R0\n  ; Reinstalling the same version\n  ${If} $R0 == 0\n    StrCpy $R1 \"$(alreadyInstalledLong)\"\n    StrCpy $R2 \"$(addOrReinstall)\"\n    StrCpy $R3 \"$(uninstallApp)\"\n    !insertmacro MUI_HEADER_TEXT \"$(alreadyInstalled)\" \"$(chooseMaintenanceOption)\"\n    StrCpy $R5 \"2\"\n  ; Upgrading\n  ${ElseIf} $R0 == 1\n    StrCpy $R1 \"$(olderOrUnknownVersionInstalled)\"\n    StrCpy $R2 \"$(uninstallBeforeInstalling)\"\n    StrCpy $R3 \"$(dontUninstall)\"\n    !insertmacro MUI_HEADER_TEXT \"$(alreadyInstalled)\" \"$(choowHowToInstall)\"\n    StrCpy $R5 \"1\"\n  ; Downgrading\n  ${ElseIf} $R0 == -1\n    StrCpy $R1 \"$(newerVersionInstalled)\"\n    StrCpy $R2 \"$(uninstallBeforeInstalling)\"\n    !if \"${ALLOWDOWNGRADES}\" == \"true\"\n      StrCpy $R3 \"$(dontUninstall)\"\n    !else\n      StrCpy $R3 \"$(dontUninstallDowngrade)\"\n    !endif\n    !insertmacro MUI_HEADER_TEXT \"$(alreadyInstalled)\" \"$(choowHowToInstall)\"\n    StrCpy $R5 \"1\"\n  ${Else}\n    Abort\n  ${EndIf}\n\n  Call SkipIfPassive\n\n  nsDialogs::Create 1018\n  Pop $R4\n  ${IfThen} $(^RTL) == 1 ${|} nsDialogs::SetRTL $(^RTL) ${|}\n\n  ${NSD_CreateLabel} 0 0 100% 24u $R1\n  Pop $R1\n\n  ${NSD_CreateRadioButton} 30u 50u -30u 8u $R2\n  Pop $R2\n  ${NSD_OnClick} $R2 PageReinstallUpdateSelection\n\n  ${NSD_CreateRadioButton} 30u 70u -30u 8u $R3\n  Pop $R3\n  ; disable this radio button if downgrading and downgrades are disabled\n  !if \"${ALLOWDOWNGRADES}\" == \"false\"\n    ${IfThen} $R0 == -1 ${|} EnableWindow $R3 0 ${|}\n  !endif\n  ${NSD_OnClick} $R3 PageReinstallUpdateSelection\n\n  ; Check the first radio button if this the first time\n  ; we enter this page or if the second button wasn't\n  ; selected the last time we were on this page\n  ${If} $ReinstallPageCheck != 2\n    SendMessage $R2 ${BM_SETCHECK} ${BST_CHECKED} 0\n  ${Else}\n    SendMessage $R3 ${BM_SETCHECK} ${BST_CHECKED} 0\n  ${EndIf}\n\n  ${NSD_SetFocus} $R2\n  nsDialogs::Show\nFunctionEnd\nFunction PageReinstallUpdateSelection\n  ${NSD_GetState} $R2 $R1\n  ${If} $R1 == ${BST_CHECKED}\n    StrCpy $ReinstallPageCheck 1\n  ${Else}\n    StrCpy $ReinstallPageCheck 2\n  ${EndIf}\nFunctionEnd\nFunction PageLeaveReinstall\n  ${NSD_GetState} $R2 $R1\n\n  ; $R5 holds whether we are reinstalling the same version or not\n  ; $R5 == \"1\" -> different versions\n  ; $R5 == \"2\" -> same version\n  ;\n  ; $R1 holds the radio buttons state. its meaning is dependant on the context\n  StrCmp $R5 \"1\" 0 +2 ; Existing install is not the same version?\n    StrCmp $R1 \"1\" reinst_uninstall reinst_done ; $R1 == \"1\", then user chose to uninstall existing version, otherwise skip uninstalling\n  StrCmp $R1 \"1\" reinst_done ; Same version? skip uninstalling\n\n  reinst_uninstall:\n    HideWindow\n    ClearErrors\n\n    ${If} $R7 == \"wix\"\n      ReadRegStr $R1 HKLM \"$R6\" \"UninstallString\"\n      ExecWait '$R1' $0\n    ${Else}\n      ReadRegStr $4 SHCTX \"${MANUPRODUCTKEY}\" \"\"\n      ReadRegStr $R1 SHCTX \"${UNINSTKEY}\" \"UninstallString\"\n      ExecWait '$R1 /P _?=$4' $0\n    ${EndIf}\n\n    BringToFront\n\n    ${IfThen} ${Errors} ${|} StrCpy $0 2 ${|} ; ExecWait failed, set fake exit code\n\n    ${If} $0 <> 0\n    ${OrIf} ${FileExists} \"$INSTDIR\\${MAINBINARYNAME}.exe\"\n      ${If} $0 = 1 ; User aborted uninstaller?\n        StrCmp $R5 \"2\" 0 +2 ; Is the existing install the same version?\n          Quit ; ...yes, already installed, we are done\n        Abort\n      ${EndIf}\n      MessageBox MB_ICONEXCLAMATION \"$(unableToUninstall)\"\n      Abort\n    ${Else}\n      StrCpy $0 $R1 1\n      ${IfThen} $0 == '\"' ${|} StrCpy $R1 $R1 -1 1 ${|} ; Strip quotes from UninstallString\n      Delete $R1\n      RMDir $INSTDIR\n    ${EndIf}\n  reinst_done:\nFunctionEnd\n\n; 5. Choose install directoy page\n!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfPassive\n!insertmacro MUI_PAGE_DIRECTORY\n\n; 6. Start menu shortcut page\n!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfPassive\nVar AppStartMenuFolder\n!insertmacro MUI_PAGE_STARTMENU Application $AppStartMenuFolder\n\n; 7. Installation page\n!insertmacro MUI_PAGE_INSTFILES\n\n; 8. Finish page\n;\n; Don't auto jump to finish page after installation page,\n; because the installation page has useful info that can be used debug any issues with the installer.\n!define MUI_FINISHPAGE_NOAUTOCLOSE\n; Use show readme button in the finish page as a button create a desktop shortcut\n!define MUI_FINISHPAGE_SHOWREADME\n!define MUI_FINISHPAGE_SHOWREADME_TEXT \"$(createDesktop)\"\n!define MUI_FINISHPAGE_SHOWREADME_FUNCTION CreateDesktopShortcut\n; Show run app after installation.\n!define MUI_FINISHPAGE_RUN \"$INSTDIR\\${MAINBINARYNAME}.exe\"\n!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfPassive\n!insertmacro MUI_PAGE_FINISH\n\n; Uninstaller Pages\n; 1. Confirm uninstall page\nVar DeleteAppDataCheckbox\nVar DeleteAppDataCheckboxState\n!define /ifndef WS_EX_LAYOUTRTL         0x00400000\n!define MUI_PAGE_CUSTOMFUNCTION_SHOW un.ConfirmShow\nFunction un.ConfirmShow\n    FindWindow $1 \"#32770\" \"\" $HWNDPARENT ; Find inner dialog\n    ${If} $(^RTL) == 1\n      System::Call 'USER32::CreateWindowEx(i${__NSD_CheckBox_EXSTYLE}|${WS_EX_LAYOUTRTL},t\"${__NSD_CheckBox_CLASS}\",t \"$(deleteAppData)\",i${__NSD_CheckBox_STYLE},i 50,i 100,i 400, i 25,i$1,i0,i0,i0)i.s'\n    ${Else}\n      System::Call 'USER32::CreateWindowEx(i${__NSD_CheckBox_EXSTYLE},t\"${__NSD_CheckBox_CLASS}\",t \"$(deleteAppData)\",i${__NSD_CheckBox_STYLE},i 0,i 100,i 400, i 25,i$1,i0,i0,i0)i.s'\n    ${EndIf}\n    Pop $DeleteAppDataCheckbox\n    SendMessage $HWNDPARENT ${WM_GETFONT} 0 0 $1\n    SendMessage $DeleteAppDataCheckbox ${WM_SETFONT} $1 1\nFunctionEnd\n!define MUI_PAGE_CUSTOMFUNCTION_LEAVE un.ConfirmLeave\nFunction un.ConfirmLeave\n    SendMessage $DeleteAppDataCheckbox ${BM_GETCHECK} 0 0 $DeleteAppDataCheckboxState\nFunctionEnd\n!insertmacro MUI_UNPAGE_CONFIRM\n\n; 2. Uninstalling Page\n!insertmacro MUI_UNPAGE_INSTFILES\n\n;Languages\n{{#each languages}}\n!insertmacro MUI_LANGUAGE \"{{this}}\"\n{{/each}}\n!insertmacro MUI_RESERVEFILE_LANGDLL\n{{#each language_files}}\n  !include \"{{this}}\"\n{{/each}}\n\n!macro SetContext\n  !if \"${INSTALLMODE}\" == \"currentUser\"\n    SetShellVarContext current\n  !else if \"${INSTALLMODE}\" == \"perMachine\"\n    SetShellVarContext all\n  !endif\n\n  ${If} ${RunningX64}\n    !if \"${ARCH}\" == \"x64\"\n      SetRegView 64\n    !else if \"${ARCH}\" == \"arm64\"\n      SetRegView 64\n    !else\n      SetRegView 32\n    !endif\n  ${EndIf}\n!macroend\n\n!define FOLDERID_ProgramData \"{62AB5D82-FDC1-4DC3-A9DD-070D1D495D97}\"\n!macro GetProgramDataPath\n    ; 调用SHGetKnownFolderIDList获取PIDL\n    System::Call 'shell32::SHGetKnownFolderIDList(g\"${FOLDERID_ProgramData}\", i0x1000, i0, *i.r1)i.r0'\n    ${If} $0 = 0\n        ; 调用SHGetPathFromIDList将PIDL转换为路径\n        System::Call 'shell32::SHGetPathFromIDList(ir1,t.r0)'\n        StrCpy $ProgramDataPathVar $0 ; 将结果保存到变量\n        ; DetailPrint \"ProgramData Path: $ProgramDataPathVar\"\n        \n        ; 释放PIDL内存\n        System::Call 'ole32::CoTaskMemFree(ir1)'\n    ${Else}\n        DetailPrint \"Failed to get ProgramData path, error code: $0\"\n    ${EndIf}\n!macroend\n\nVar PassiveMode\nFunction .onInit\n  ${GetOptions} $CMDLINE \"/P\" $PassiveMode\n  IfErrors +2 0\n    StrCpy $PassiveMode 1\n\n  !if \"${DISPLAYLANGUAGESELECTOR}\" == \"true\"\n    !insertmacro MUI_LANGDLL_DISPLAY\n  !endif\n\n  !insertmacro SetContext\n\n  ${If} $INSTDIR == \"\"\n    ; Set default install location\n    !if \"${INSTALLMODE}\" == \"perMachine\"\n      ${If} ${RunningX64}\n        !if \"${ARCH}\" == \"x64\"\n          StrCpy $INSTDIR \"$PROGRAMFILES64\\${PRODUCTNAME}\"\n        !else if \"${ARCH}\" == \"arm64\"\n          StrCpy $INSTDIR \"$PROGRAMFILES64\\${PRODUCTNAME}\"\n        !else\n          StrCpy $INSTDIR \"$PROGRAMFILES\\${PRODUCTNAME}\"\n        !endif\n      ${Else}\n        StrCpy $INSTDIR \"$PROGRAMFILES\\${PRODUCTNAME}\"\n      ${EndIf}\n    !else if \"${INSTALLMODE}\" == \"currentUser\"\n      StrCpy $INSTDIR \"$LOCALAPPDATA\\${PRODUCTNAME}\"\n    !endif\n\n    Call RestorePreviousInstallLocation\n  ${EndIf}\n\n\n  !if \"${INSTALLMODE}\" == \"both\"\n    !insertmacro MULTIUSER_INIT\n  !endif\nFunctionEnd\n\n!macro CheckNyanpasuProcess Process ID\n  !if \"${INSTALLMODE}\" == \"currentUser\"\n    nsis_tauri_utils::FindProcessCurrentUser \"${Process}\"\n  !else\n    nsis_tauri_utils::FindProcess \"${Process}\"\n  !endif\n  Pop $R0\n  ${If} $R0 = 0\n      DetailPrint \"${Process} is running\"\n      IfSilent kill${ID} 0\n      ${IfThen} $PassiveMode != 1 ${|} MessageBox MB_OKCANCEL \"${Process} is running, ok to kill?\" IDOK kill${ID} IDCANCEL cancel${ID} ${|}\n      kill${ID}:\n        !if \"${INSTALLMODE}\" == \"currentUser\"\n          nsis_tauri_utils::KillProcessCurrentUser \"${Process}\"\n        !else\n          nsis_tauri_utils::KillProcess \"${Process}\"\n        !endif\n        Pop $R0\n        Sleep 500\n        ${If} $R0 = 0\n          Goto process_check_done${ID}\n        ${Else}\n          IfSilent silent${ID} ui${ID}\n          silent${ID}:\n            System::Call 'kernel32::AttachConsole(i -1)i.r0'\n            ${If} $0 != 0\n              System::Call 'kernel32::GetStdHandle(i -11)i.r0'\n              System::call 'kernel32::SetConsoleTextAttribute(i r0, i 0x0004)' ; set red color\n              FileWrite $0 \"${Process} is running\\n\"\n            ${EndIf}\n            Abort\n          ui${ID}:\n            Abort \"${Process} is running, failed to kill it\"\n        ${EndIf}\n      cancel${ID}:\n        Abort \"${Process} is running, aborting installation\"\n  ${EndIf}\n  process_check_done${ID}:\n!macroend\n\n!macro CheckAllNyanpasuProcesses\n  !insertmacro CheckNyanpasuProcess \"Clash Nyanpasu.exe\" \"1\"\n  !insertmacro CheckNyanpasuProcess \"clash-nyanpasu.exe\" \"2\"\n  ; !insertmacro CheckNyanpasuProcess \"clash-verge-service.exe\" \"3\"\n  !insertmacro CheckNyanpasuProcess \"clash.exe\" \"4\"\n  !insertmacro CheckNyanpasuProcess \"clash-rs.exe\" \"5\"\n  !insertmacro CheckNyanpasuProcess \"mihomo.exe\" \"6\"\n  !insertmacro CheckNyanpasuProcess \"mihomo-alpha.exe\" \"7\"\n!macroend\n\n; Section CheckProcesses\n;   !insertmacro CheckAllNyanpasuProcesses\n; SectionEnd\n\nSection EarlyChecks\n  ; Abort silent installer if downgrades is disabled\n  !if \"${ALLOWDOWNGRADES}\" == \"false\"\n  IfSilent 0 silent_downgrades_done\n    ; If downgrading\n    ${If} $R0 == -1\n      System::Call 'kernel32::AttachConsole(i -1)i.r0'\n      ${If} $0 != 0\n        System::Call 'kernel32::GetStdHandle(i -11)i.r0'\n        System::call 'kernel32::SetConsoleTextAttribute(i r0, i 0x0004)' ; set red color\n        FileWrite $0 \"$(silentDowngrades)\"\n      ${EndIf}\n      Abort\n    ${EndIf}\n  silent_downgrades_done:\n  !endif\n\nSectionEnd\n\nSection WebView2\n  ; Check if Webview2 is already installed and skip this section\n  ${If} ${RunningX64}\n    ReadRegStr $4 HKLM \"SOFTWARE\\WOW6432Node\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}\" \"pv\"\n  ${Else}\n    ReadRegStr $4 HKLM \"SOFTWARE\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}\" \"pv\"\n  ${EndIf}\n  ReadRegStr $5 HKCU \"SOFTWARE\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}\" \"pv\"\n\n  StrCmp $4 \"\" 0 webview2_done\n  StrCmp $5 \"\" 0 webview2_done\n\n  ; Webview2 install modes\n  !if \"${INSTALLWEBVIEW2MODE}\" == \"downloadBootstrapper\"\n    Delete \"$TEMP\\MicrosoftEdgeWebview2Setup.exe\"\n    DetailPrint \"$(webview2Downloading)\"\n    NSISdl::download \"https://go.microsoft.com/fwlink/p/?LinkId=2124703\" \"$TEMP\\MicrosoftEdgeWebview2Setup.exe\"\n    Pop $0\n    ${If} $0 == 0\n      DetailPrint \"$(webview2DownloadSuccess)\"\n    ${Else}\n      DetailPrint \"$(webview2DownloadError)\"\n      Abort \"$(webview2AbortError)\"\n    ${EndIf}\n    StrCpy $6 \"$TEMP\\MicrosoftEdgeWebview2Setup.exe\"\n    Goto install_webview2\n  !endif\n\n  !if \"${INSTALLWEBVIEW2MODE}\" == \"embedBootstrapper\"\n    Delete \"$TEMP\\MicrosoftEdgeWebview2Setup.exe\"\n    File \"/oname=$TEMP\\MicrosoftEdgeWebview2Setup.exe\" \"${WEBVIEW2BOOTSTRAPPERPATH}\"\n    DetailPrint \"$(installingWebview2)\"\n    StrCpy $6 \"$TEMP\\MicrosoftEdgeWebview2Setup.exe\"\n    Goto install_webview2\n  !endif\n\n  !if \"${INSTALLWEBVIEW2MODE}\" == \"offlineInstaller\"\n    Delete \"$TEMP\\MicrosoftEdgeWebView2RuntimeInstaller.exe\"\n    File \"/oname=$TEMP\\MicrosoftEdgeWebView2RuntimeInstaller.exe\" \"${WEBVIEW2INSTALLERPATH}\"\n    DetailPrint \"$(installingWebview2)\"\n    StrCpy $6 \"$TEMP\\MicrosoftEdgeWebView2RuntimeInstaller.exe\"\n    Goto install_webview2\n  !endif\n\n  Goto webview2_done\n\n  install_webview2:\n    DetailPrint \"$(installingWebview2)\"\n    ; $6 holds the path to the webview2 installer\n    ExecWait \"$6 ${WEBVIEW2INSTALLERARGS} /install\" $1\n    ${If} $1 == 0\n      DetailPrint \"$(webview2InstallSuccess)\"\n    ${Else}\n      DetailPrint \"$(webview2InstallError)\"\n      Abort \"$(webview2AbortError)\"\n    ${EndIf}\n  webview2_done:\nSectionEnd\n\n; !macro CheckIfAppIsRunning\n;   !if \"${INSTALLMODE}\" == \"currentUser\"\n;     nsis_tauri_utils::FindProcessCurrentUser \"${MAINBINARYNAME}.exe\"\n;   !else\n;     nsis_tauri_utils::FindProcess \"${MAINBINARYNAME}.exe\"\n;   !endif\n;   Pop $R0\n;   ${If} $R0 = 0\n;       IfSilent kill 0\n;       ${IfThen} $PassiveMode != 1 ${|} MessageBox MB_OKCANCEL \"$(appRunningOkKill)\" IDOK kill IDCANCEL cancel ${|}\n;       kill:\n;         !if \"${INSTALLMODE}\" == \"currentUser\"\n;           nsis_tauri_utils::KillProcessCurrentUser \"${MAINBINARYNAME}.exe\"\n;         !else\n;           nsis_tauri_utils::KillProcess \"${MAINBINARYNAME}.exe\"\n;         !endif\n;         Pop $R0\n;         Sleep 500\n;         ${If} $R0 = 0\n;           Goto app_check_done\n;         ${Else}\n;           IfSilent silent ui\n;           silent:\n;             System::Call 'kernel32::AttachConsole(i -1)i.r0'\n;             ${If} $0 != 0\n;               System::Call 'kernel32::GetStdHandle(i -11)i.r0'\n;               System::call 'kernel32::SetConsoleTextAttribute(i r0, i 0x0004)' ; set red color\n;               FileWrite $0 \"$(appRunning)$\\n\"\n;             ${EndIf}\n;             Abort\n;           ui:\n;             Abort \"$(failedToKillApp)\"\n;         ${EndIf}\n;       cancel:\n;         Abort \"$(appRunning)\"\n;   ${EndIf}\n;   app_check_done:\n; !macroend\n\n!macro StopCoreByService\n  ; 构建服务可执行文件的完整路径\n  StrCpy $1 \"$ProgramDataPathVar\\nyanpasu-service\\data\\nyanpasu-service.exe\"\n\n  ; 检查文件是否存在\n  IfFileExists \"$1\" 0 SkipStopCore\n\n  ; 文件存在，执行停止核心服务\n  nsExec::ExecToLog '\"$1\" rpc stop-core'\n  Pop $0  ; 弹出命令执行的返回值\n  ${If} $0 == \"0\"\n    DetailPrint \"Core service stopped successfully.\"\n  ${Else}\n    DetailPrint \"Core stop failed with exit code $0\"\n  ${EndIf}\n  SkipStopCore:\n    ; 如果文件不存在，打印错误\n    DetailPrint \"Nyanpasu Service is not installed, skipping stop-core\"\n!macroend\n\nSection Install\n  !insertmacro GetProgramDataPath\n  !insertmacro StopCoreByService\n  SetOutPath $INSTDIR\n  !insertmacro CheckAllNyanpasuProcesses\n  ; !insertmacro CheckIfAppIsRunning\n\n  ; Copy main executable\n  File \"${MAINBINARYSRCPATH}\"\n\n  ; Copy resources\n  {{#each resources_dirs}}\n    CreateDirectory \"$INSTDIR\\\\{{this}}\"\n  {{/each}}\n  {{#each resources}}\n    File /a \"/oname={{this.[1]}}\" \"{{@key}}\"\n  {{/each}}\n\n  ; Copy external binaries\n  {{#each binaries}}\n    File /a \"/oname={{this}}\" \"{{@key}}\"\n  {{/each}}\n\n  ; Create uninstaller\n  WriteUninstaller \"$INSTDIR\\uninstall.exe\"\n\n  ; Save $INSTDIR in registry for future installations\n  WriteRegStr SHCTX \"${MANUPRODUCTKEY}\" \"\" $INSTDIR\n\n  !if \"${INSTALLMODE}\" == \"both\"\n    ; Save install mode to be selected by default for the next installation such as updating\n    ; or when uninstalling\n    WriteRegStr SHCTX \"${UNINSTKEY}\" $MultiUser.InstallMode 1\n  !endif\n\n  ; Registry information for add/remove programs\n  WriteRegStr SHCTX \"${UNINSTKEY}\" \"DisplayName\" \"${PRODUCTNAME}\"\n  WriteRegStr SHCTX \"${UNINSTKEY}\" \"DisplayIcon\" \"$\\\"$INSTDIR\\${MAINBINARYNAME}.exe$\\\"\"\n  WriteRegStr SHCTX \"${UNINSTKEY}\" \"DisplayVersion\" \"${VERSION}\"\n  WriteRegStr SHCTX \"${UNINSTKEY}\" \"Publisher\" \"${MANUFACTURER}\"\n  WriteRegStr SHCTX \"${UNINSTKEY}\" \"InstallLocation\" \"$\\\"$INSTDIR$\\\"\"\n  WriteRegStr SHCTX \"${UNINSTKEY}\" \"UninstallString\" \"$\\\"$INSTDIR\\uninstall.exe$\\\"\"\n  WriteRegDWORD SHCTX \"${UNINSTKEY}\" \"NoModify\" \"1\"\n  WriteRegDWORD SHCTX \"${UNINSTKEY}\" \"NoRepair\" \"1\"\n  WriteRegDWORD SHCTX \"${UNINSTKEY}\" \"EstimatedSize\" \"${ESTIMATEDSIZE}\"\n\n  ; Create start menu shortcut (GUI)\n  !insertmacro MUI_STARTMENU_WRITE_BEGIN Application\n    Call CreateStartMenuShortcut\n  !insertmacro MUI_STARTMENU_WRITE_END\n\n  ; Create shortcuts for silent and passive installers, which\n  ; can be disabled by passing `/NS` flag\n  ; GUI installer has buttons for users to control creating them\n  IfSilent check_ns_flag 0\n  ${IfThen} $PassiveMode == 1 ${|} Goto check_ns_flag ${|}\n  Goto shortcuts_done\n  check_ns_flag:\n    ${GetOptions} $CMDLINE \"/NS\" $R0\n    IfErrors 0 shortcuts_done\n      Call CreateDesktopShortcut\n      Call CreateStartMenuShortcut\n  shortcuts_done:\n\n  ; Auto close this page for passive mode\n  ${IfThen} $PassiveMode == 1 ${|} SetAutoClose true ${|}\nSectionEnd\n\nFunction .onInstSuccess\n  ; Check for `/R` flag only in silent and passive installers because\n  ; GUI installer has a toggle for the user to (re)start the app\n  IfSilent check_r_flag 0\n  ${IfThen} $PassiveMode == 1 ${|} Goto check_r_flag ${|}\n  Goto run_done\n  check_r_flag:\n    ${GetOptions} $CMDLINE \"/R\" $R0\n    IfErrors run_done 0\n      Exec '\"$INSTDIR\\${MAINBINARYNAME}.exe\"'\n  run_done:\nFunctionEnd\n\nFunction un.onInit\n  !insertmacro SetContext\n\n  !if \"${INSTALLMODE}\" == \"both\"\n    !insertmacro MULTIUSER_UNINIT\n  !endif\n\n  !insertmacro MUI_UNGETLANGUAGE\nFunctionEnd\n\n!macro DeleteAppUserModelId\n  !insertmacro ComHlpr_CreateInProcInstance ${CLSID_DestinationList} ${IID_ICustomDestinationList} r1 \"\"\n  ${If} $1 P<> 0\n    ${ICustomDestinationList::DeleteList} $1 '(\"${BUNDLEID}\")'\n    ${IUnknown::Release} $1 \"\"\n  ${EndIf}\n  !insertmacro ComHlpr_CreateInProcInstance ${CLSID_ApplicationDestinations} ${IID_IApplicationDestinations} r1 \"\"\n  ${If} $1 P<> 0\n    ${IApplicationDestinations::SetAppID} $1 '(\"${BUNDLEID}\")i.r0'\n    ${If} $0 >= 0\n      ${IApplicationDestinations::RemoveAllDestinations} $1 ''\n    ${EndIf}\n    ${IUnknown::Release} $1 \"\"\n  ${EndIf}\n!macroend\n\n; From https://stackoverflow.com/a/42816728/16993372\n!macro UnpinShortcut shortcut\n  !insertmacro ComHlpr_CreateInProcInstance ${CLSID_StartMenuPin} ${IID_IStartMenuPinnedList} r0 \"\"\n  ${If} $0 P<> 0\n      System::Call 'SHELL32::SHCreateItemFromParsingName(ws, p0, g \"${IID_IShellItem}\", *p0r1)' \"${shortcut}\"\n      ${If} $1 P<> 0\n          ${IStartMenuPinnedList::RemoveFromList} $0 '(r1)'\n          ${IUnknown::Release} $1 \"\"\n      ${EndIf}\n      ${IUnknown::Release} $0 \"\"\n  ${EndIf}\n!macroend\n\n!macro StopAndRemoveServiceDirectory\n  ; 构建服务路径\n  StrCpy $1 \"$ProgramDataPathVar\\nyanpasu-service\\data\\nyanpasu-service.exe\"\n\n  ; 检查服务可执行文件是否存在\n  IfFileExists \"$1\" 0 Skip\n  nsExec::ExecToLog '\"$1\" uninstall'\n  Pop $0\n  DetailPrint \"uninstall service with exit code $0\"\n\n  ; 检查停止服务是否成功（假设0, 100, 102为成功）\n  IntCmp $0 0 RemoveDirectories UninstallServiceFailed 0\n  IntCmp $0 100 RemoveDirectories UninstallServiceFailed 0\n  IntCmp $0 102 RemoveDirectories UninstallServiceFailed UninstallServiceFailed\n\n  UninstallServiceFailed:\n    Abort \"Failed to stop the service. Aborting installation.\"\n\n  RemoveDirectories:\n    ; 如果服务成功停止，继续检查目录是否存在并删除\n    StrCpy $2 \"$ProgramDataPathVar\\nyanpasu-service\"\n    IfFileExists \"$2\\*\" 0 Skip\n    RMDir /r \"$2\"\n    DetailPrint \"Removed service directory successfully\"\n\n  Skip:\n    DetailPrint \"Service directory does not exist, skipping stop and remove service directory\"\n!macroend\n\n!macro RemoveRegs\n  ; cleanup auto start registry keys\n  DeleteRegValue HKCU \"Software\\Microsoft\\Windows\\CurrentVersion\\Run\" \"Clash Nyanpasu\"\n  DeleteRegValue HKLM \"SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Run\" \"Clash Nyanpasu\"\n  ; cleanup custom protocol handler\n  DeleteRegKey HKCU \"Software\\Classes\\clash\"\n  DeleteRegKey HKCU \"Software\\Classes\\clash-nyanpasu\"\n  DeleteRegKey HKCR \"clash\"\n  DeleteRegKey HKCR \"clash-nyanpasu\"\n!macroend\n\nSection Uninstall\n  !insertmacro GetProgramDataPath\n  !insertmacro StopAndRemoveServiceDirectory\n  !insertmacro CheckAllNyanpasuProcesses\n  !insertmacro RemoveRegs\n  ; !insertmacro CheckIfAppIsRunning\n  \n  ; Delete the app directory and its content from disk\n  ; Copy main executable\n  Delete \"$INSTDIR\\${MAINBINARYNAME}.exe\"\n\n  ; Delete resources\n  {{#each resources}}\n    Delete \"$INSTDIR\\\\{{this.[1]}}\"\n  {{/each}}\n\n  ; Delete external binaries\n  {{#each binaries}}\n    Delete \"$INSTDIR\\\\{{this}}\"\n  {{/each}}\n\n  ; Delete uninstaller\n  Delete \"$INSTDIR\\uninstall.exe\"\n\n  {{#each resources_ancestors}}\n  RMDir /REBOOTOK \"$INSTDIR\\\\{{this}}\"\n  {{/each}}\n  RMDir \"$INSTDIR\"\n\n  !insertmacro DeleteAppUserModelId\n  !insertmacro UnpinShortcut \"$SMPROGRAMS\\$AppStartMenuFolder\\${MAINBINARYNAME}.lnk\"\n  !insertmacro UnpinShortcut \"$DESKTOP\\${MAINBINARYNAME}.lnk\"\n\n  ; Remove start menu shortcut\n  !insertmacro MUI_STARTMENU_GETFOLDER Application $AppStartMenuFolder\n  Delete \"$SMPROGRAMS\\$AppStartMenuFolder\\${MAINBINARYNAME}.lnk\"\n  RMDir \"$SMPROGRAMS\\$AppStartMenuFolder\"\n\n  ; Remove desktop shortcuts\n  Delete \"$DESKTOP\\${MAINBINARYNAME}.lnk\"\n\n  ; Remove registry information for add/remove programs\n  !if \"${INSTALLMODE}\" == \"both\"\n    DeleteRegKey SHCTX \"${UNINSTKEY}\"\n  !else if \"${INSTALLMODE}\" == \"perMachine\"\n    DeleteRegKey HKLM \"${UNINSTKEY}\"\n  !else\n    DeleteRegKey HKCU \"${UNINSTKEY}\"\n  !endif\n\n  DeleteRegValue HKCU \"${MANUPRODUCTKEY}\" \"Installer Language\"\n\n  ; Delete app data\n  ${If} $DeleteAppDataCheckboxState == 1\n    SetShellVarContext current\n    RmDir /r \"$APPDATA\\${BUNDLEID}\"\n    RmDir /r \"$LOCALAPPDATA\\${BUNDLEID}\"\n    RmDir /r \"$APPDATA\\Clash Nyanpasu\"\n    RmDir /r \"$LOCALAPPDATA\\Clash Nyanpasu\"\n  ${EndIf}\n\n  ${GetOptions} $CMDLINE \"/P\" $R0\n  IfErrors +2 0\n    SetAutoClose true\nSectionEnd\n\nFunction RestorePreviousInstallLocation\n  ReadRegStr $4 SHCTX \"${MANUPRODUCTKEY}\" \"\"\n  StrCmp $4 \"\" +2 0\n    StrCpy $INSTDIR $4\nFunctionEnd\n \nFunction SkipIfPassive\n  ${IfThen} $PassiveMode == 1  ${|} Abort ${|}\nFunctionEnd\n\n!macro SetLnkAppUserModelId shortcut\n  !insertmacro ComHlpr_CreateInProcInstance ${CLSID_ShellLink} ${IID_IShellLink} r0 \"\"\n  ${If} $0 P<> 0\n    ${IUnknown::QueryInterface} $0 '(\"${IID_IPersistFile}\",.r1)'\n    ${If} $1 P<> 0\n      ${IPersistFile::Load} $1 '(\"${shortcut}\", ${STGM_READWRITE})'\n      ${IUnknown::QueryInterface} $0 '(\"${IID_IPropertyStore}\",.r2)'\n      ${If} $2 P<> 0\n        System::Call 'Oleaut32::SysAllocString(w \"${BUNDLEID}\") i.r3'\n        System::Call '*${SYSSTRUCT_PROPERTYKEY}(${PKEY_AppUserModel_ID})p.r4'\n        System::Call '*${SYSSTRUCT_PROPVARIANT}(${VT_BSTR},,&i4 $3)p.r5'\n        ${IPropertyStore::SetValue} $2 '($4,$5)'\n\n        System::Call 'Oleaut32::SysFreeString($3)'\n        System::Free $4\n        System::Free $5\n        ${IPropertyStore::Commit} $2 \"\"\n        ${IUnknown::Release} $2 \"\"\n        ${IPersistFile::Save} $1 '(\"${shortcut}\",1)'\n      ${EndIf}\n      ${IUnknown::Release} $1 \"\"\n    ${EndIf}\n    ${IUnknown::Release} $0 \"\"\n  ${EndIf}\n!macroend\n\nFunction CreateDesktopShortcut\n  CreateShortcut \"$DESKTOP\\${MAINBINARYNAME}.lnk\" \"$INSTDIR\\${MAINBINARYNAME}.exe\"\n  !insertmacro SetLnkAppUserModelId \"$DESKTOP\\${MAINBINARYNAME}.lnk\"\nFunctionEnd\n\nFunction CreateStartMenuShortcut\n  CreateDirectory \"$SMPROGRAMS\\$AppStartMenuFolder\"\n  CreateShortcut \"$SMPROGRAMS\\$AppStartMenuFolder\\${MAINBINARYNAME}.lnk\" \"$INSTDIR\\${MAINBINARYNAME}.exe\"\n  !insertmacro SetLnkAppUserModelId \"$SMPROGRAMS\\$AppStartMenuFolder\\${MAINBINARYNAME}.lnk\"\nFunctionEnd\n"
  },
  {
    "path": "backend/tauri/templates/installer.wxs",
    "content": "<?if $(sys.BUILDARCH)=\"x86\"?>\n    <?define Win64 = \"no\" ?>\n    <?define PlatformProgramFilesFolder = \"ProgramFilesFolder\" ?>\n<?elseif $(sys.BUILDARCH)=\"x64\"?>\n    <?define Win64 = \"yes\" ?>\n    <?define PlatformProgramFilesFolder = \"ProgramFiles64Folder\" ?>\n<?else?>\n    <?error Unsupported value of sys.BUILDARCH=$(sys.BUILDARCH)?>\n<?endif?>\n\n<Wix xmlns=\"http://schemas.microsoft.com/wix/2006/wi\">\n    <Product\n            Id=\"*\"\n            Name=\"{{product_name}}\"\n            UpgradeCode=\"{{upgrade_code}}\"\n            Language=\"!(loc.TauriLanguage)\"\n            Manufacturer=\"{{manufacturer}}\"\n            Version=\"{{version}}\">\n\n        <Package Id=\"*\"\n                 Keywords=\"Installer\"\n                 InstallerVersion=\"450\"\n                 Languages=\"0\"\n                 Compressed=\"yes\"\n                 InstallScope=\"perMachine\"\n                 SummaryCodepage=\"!(loc.TauriCodepage)\"/>\n\n        <!-- https://docs.microsoft.com/en-us/windows/win32/msi/reinstallmode -->\n        <!-- reinstall all files; rewrite all registry entries; reinstall all shortcuts -->\n        <Property Id=\"REINSTALLMODE\" Value=\"amus\" />\n\n        {{#if allow_downgrades}}\n            <MajorUpgrade Schedule=\"afterInstallInitialize\" AllowDowngrades=\"yes\" />\n        {{else}}\n            <MajorUpgrade Schedule=\"afterInstallInitialize\" DowngradeErrorMessage=\"!(loc.DowngradeErrorMessage)\" AllowSameVersionUpgrades=\"yes\" />\n        {{/if}}\n\n        <InstallExecuteSequence>\n            <RemoveShortcuts>Installed AND NOT UPGRADINGPRODUCTCODE</RemoveShortcuts>\n        </InstallExecuteSequence>\n\n        <Media Id=\"1\" Cabinet=\"app.cab\" EmbedCab=\"yes\" />\n\n        {{#if banner_path}}\n        <WixVariable Id=\"WixUIBannerBmp\" Value=\"{{banner_path}}\" />\n        {{/if}}\n        {{#if dialog_image_path}}\n        <WixVariable Id=\"WixUIDialogBmp\" Value=\"{{dialog_image_path}}\" />\n        {{/if}}\n        {{#if license}}\n        <WixVariable Id=\"WixUILicenseRtf\" Value=\"{{license}}\" />\n        {{/if}}\n\n        <Icon Id=\"ProductIcon\" SourceFile=\"{{icon_path}}\"/>\n        <Property Id=\"ARPPRODUCTICON\" Value=\"ProductIcon\" />\n        <Property Id=\"ARPNOREPAIR\" Value=\"yes\" Secure=\"yes\" />      <!-- Remove repair -->\n        <SetProperty Id=\"ARPNOMODIFY\" Value=\"1\" After=\"InstallValidate\" Sequence=\"execute\"/>\n\n        <!-- initialize with previous InstallDir -->\n        <Property Id=\"INSTALLDIR\">\n            <RegistrySearch Id=\"PrevInstallDirReg\" Root=\"HKCU\" Key=\"Software\\\\{{manufacturer}}\\\\{{product_name}}\" Name=\"InstallDir\" Type=\"raw\"/>\n        </Property>\n\n        <!-- launch app checkbox -->\n        <Property Id=\"WIXUI_EXITDIALOGOPTIONALCHECKBOXTEXT\" Value=\"!(loc.LaunchApp)\" />\n        <Property Id=\"WIXUI_EXITDIALOGOPTIONALCHECKBOX\" Value=\"1\"/>\n        <Property Id=\"WixShellExecTarget\" Value=\"[!Path]\" />\n        <CustomAction Id=\"LaunchApplication\" BinaryKey=\"WixCA\" DllEntry=\"WixShellExec\" Impersonate=\"yes\" />\n\n        <UI>\n            <!-- launch app checkbox -->\n            <Publish Dialog=\"ExitDialog\" Control=\"Finish\" Event=\"DoAction\" Value=\"LaunchApplication\">WIXUI_EXITDIALOGOPTIONALCHECKBOX = 1 and NOT Installed</Publish>\n\n            <Property Id=\"WIXUI_INSTALLDIR\" Value=\"INSTALLDIR\" />\n\n            {{#unless license}}\n            <!-- Skip license dialog -->\n            <Publish Dialog=\"WelcomeDlg\"\n                     Control=\"Next\"\n                     Event=\"NewDialog\"\n                     Value=\"InstallDirDlg\"\n                     Order=\"2\">1</Publish>\n            <Publish Dialog=\"InstallDirDlg\"\n                     Control=\"Back\"\n                     Event=\"NewDialog\"\n                     Value=\"WelcomeDlg\"\n                     Order=\"2\">1</Publish>\n            {{/unless}}\n        </UI>\n\n        <UIRef Id=\"WixUI_InstallDir\" />\n\n        <Directory Id=\"TARGETDIR\" Name=\"SourceDir\">\n            <Directory Id=\"DesktopFolder\" Name=\"Desktop\">\n                <Component Id=\"ApplicationShortcutDesktop\" Guid=\"*\">\n                    <Shortcut Id=\"ApplicationDesktopShortcut\" Name=\"{{product_name}}\" Description=\"Runs {{product_name}}\" Target=\"[!Path]\" WorkingDirectory=\"INSTALLDIR\" />\n                    <RemoveFolder Id=\"DesktopFolder\" On=\"uninstall\" />\n                    <RegistryValue Root=\"HKCU\" Key=\"Software\\\\{{manufacturer}}\\\\{{product_name}}\" Name=\"Desktop Shortcut\" Type=\"integer\" Value=\"1\" KeyPath=\"yes\" />\n                </Component>\n            </Directory>\n            <Directory Id=\"$(var.PlatformProgramFilesFolder)\" Name=\"PFiles\">\n                <Directory Id=\"INSTALLDIR\" Name=\"{{product_name}}\"/>\n            </Directory>\n            <Directory Id=\"ProgramMenuFolder\">\n                <Directory Id=\"ApplicationProgramsFolder\" Name=\"{{product_name}}\"/>\n            </Directory>\n        </Directory>\n\n        <DirectoryRef Id=\"INSTALLDIR\">\n            <Component Id=\"RegistryEntries\" Guid=\"*\">\n                <RegistryKey Root=\"HKCU\" Key=\"Software\\\\{{manufacturer}}\\\\{{product_name}}\">\n                    <RegistryValue Name=\"InstallDir\" Type=\"string\" Value=\"[INSTALLDIR]\" KeyPath=\"yes\" />\n                </RegistryKey>\n            </Component>\n            <Component Id=\"Path\" Guid=\"{{path_component_guid}}\" Win64=\"$(var.Win64)\">\n                <File Id=\"Path\" Source=\"{{app_exe_source}}\" KeyPath=\"yes\" Checksum=\"yes\"/>\n            </Component>\n            {{#each binaries as |bin| ~}}\n            <Component Id=\"{{ bin.id }}\" Guid=\"{{bin.guid}}\" Win64=\"$(var.Win64)\">\n                <File Id=\"Bin_{{ bin.id }}\" Source=\"{{bin.path}}\" KeyPath=\"yes\"/>\n            </Component>\n            {{/each~}}\n            {{#if enable_elevated_update_task}}\n            <Component Id=\"UpdateTask\" Guid=\"C492327D-9720-4CD5-8DB8-F09082AF44BE\" Win64=\"$(var.Win64)\">\n                <File Id=\"UpdateTask\" Source=\"update.xml\" KeyPath=\"yes\" Checksum=\"yes\"/>\n            </Component>\n            <Component Id=\"UpdateTaskInstaller\" Guid=\"011F25ED-9BE3-50A7-9E9B-3519ED2B9932\" Win64=\"$(var.Win64)\">\n                <File Id=\"UpdateTaskInstaller\" Source=\"install-task.ps1\" KeyPath=\"yes\" Checksum=\"yes\"/>\n            </Component>\n            <Component Id=\"UpdateTaskUninstaller\" Guid=\"D4F6CC3F-32DC-5FD0-95E8-782FFD7BBCE1\" Win64=\"$(var.Win64)\">\n                <File Id=\"UpdateTaskUninstaller\" Source=\"uninstall-task.ps1\" KeyPath=\"yes\" Checksum=\"yes\"/>\n            </Component>\n            {{/if}}\n            {{resources}}\n            <Component Id=\"CMP_UninstallShortcut\" Guid=\"*\">\n\n                <Shortcut Id=\"UninstallShortcut\"\n\t\t\t\t\t\t  Name=\"Uninstall {{product_name}}\"\n\t\t\t\t\t\t  Description=\"Uninstalls {{product_name}}\"\n\t\t\t\t\t\t  Target=\"[System64Folder]msiexec.exe\"\n\t\t\t\t\t\t  Arguments=\"/x [ProductCode]\" />\n\n\t\t\t\t<RemoveFolder Id=\"INSTALLDIR\"\n\t\t\t\t\t\t\t  On=\"uninstall\" />\n\n\t\t\t\t<RegistryValue Root=\"HKCU\"\n\t\t\t\t\t\t\t   Key=\"Software\\\\{{manufacturer}}\\\\{{product_name}}\"\n\t\t\t\t\t\t\t   Name=\"Uninstaller Shortcut\"\n\t\t\t\t\t\t\t   Type=\"integer\"\n\t\t\t\t\t\t\t   Value=\"1\"\n\t\t\t\t\t\t\t   KeyPath=\"yes\" />\n            </Component>\n        </DirectoryRef>\n\n        <DirectoryRef Id=\"ApplicationProgramsFolder\">\n            <Component Id=\"ApplicationShortcut\" Guid=\"*\">\n                <Shortcut Id=\"ApplicationStartMenuShortcut\"\n                    Name=\"{{product_name}}\"\n                    Description=\"Runs {{product_name}}\"\n                    Target=\"[!Path]\"\n                    Icon=\"ProductIcon\"\n                    WorkingDirectory=\"INSTALLDIR\">\n                    <ShortcutProperty Key=\"System.AppUserModel.ID\" Value=\"{{bundle_id}}\"/>\n                </Shortcut>\n                <RemoveFolder Id=\"ApplicationProgramsFolder\" On=\"uninstall\"/>\n                <RegistryValue Root=\"HKCU\" Key=\"Software\\\\{{manufacturer}}\\\\{{product_name}}\" Name=\"Start Menu Shortcut\" Type=\"integer\" Value=\"1\" KeyPath=\"yes\"/>\n           </Component>\n        </DirectoryRef>\n\n        {{#each merge_modules as |msm| ~}}\n        <DirectoryRef Id=\"TARGETDIR\">\n            <Merge Id=\"{{ msm.name }}\" SourceFile=\"{{ msm.path }}\" DiskId=\"1\" Language=\"!(loc.TauriLanguage)\" />\n        </DirectoryRef>\n\n        <Feature Id=\"{{ msm.name }}\" Title=\"{{ msm.name }}\" AllowAdvertise=\"no\" Display=\"hidden\" Level=\"1\">\n            <MergeRef Id=\"{{ msm.name }}\"/>\n        </Feature>\n        {{/each~}}\n\n        <Feature\n                Id=\"MainProgram\"\n                Title=\"Application\"\n                Description=\"!(loc.InstallAppFeature)\"\n                Level=\"1\"\n                ConfigurableDirectory=\"INSTALLDIR\"\n                AllowAdvertise=\"no\"\n                Display=\"expand\"\n                Absent=\"disallow\">\n\n            <ComponentRef Id=\"RegistryEntries\"/>\n\n            {{#each resource_file_ids as |resource_file_id| ~}}\n                <ComponentRef Id=\"{{ resource_file_id }}\"/>\n            {{/each~}}\n\n            {{#if enable_elevated_update_task}}\n                <ComponentRef Id=\"UpdateTask\" />\n                <ComponentRef Id=\"UpdateTaskInstaller\" />\n                <ComponentRef Id=\"UpdateTaskUninstaller\" />\n            {{/if}}\n\n            <Feature Id=\"ShortcutsFeature\"\n                Title=\"Shortcuts\"\n                Level=\"1\">\n                <ComponentRef Id=\"Path\"/>\n                <ComponentRef Id=\"CMP_UninstallShortcut\" />\n                <ComponentRef Id=\"ApplicationShortcut\" />\n                <ComponentRef Id=\"ApplicationShortcutDesktop\" />\n            </Feature>\n\n            <Feature\n                Id=\"Environment\"\n                Title=\"PATH Environment Variable\"\n                Description=\"!(loc.PathEnvVarFeature)\"\n                Level=\"1\"\n                Absent=\"allow\">\n            <ComponentRef Id=\"Path\"/>\n            {{#each binaries as |bin| ~}}\n            <ComponentRef Id=\"{{ bin.id }}\"/>\n            {{/each~}}\n            </Feature>\n        </Feature>\n\n        <Feature Id=\"External\" AllowAdvertise=\"no\" Absent=\"disallow\">\n            {{#each component_group_refs as |id| ~}}\n            <ComponentGroupRef Id=\"{{ id }}\"/>\n            {{/each~}}\n            {{#each component_refs as |id| ~}}\n            <ComponentRef Id=\"{{ id }}\"/>\n            {{/each~}}\n            {{#each feature_group_refs as |id| ~}}\n            <FeatureGroupRef Id=\"{{ id }}\"/>\n            {{/each~}}\n            {{#each feature_refs as |id| ~}}\n            <FeatureRef Id=\"{{ id }}\"/>\n            {{/each~}}\n            {{#each merge_refs as |id| ~}}\n            <MergeRef Id=\"{{ id }}\"/>\n            {{/each~}}\n        </Feature>\n\n        {{#if install_webview}}\n        <!-- WebView2 -->\n        <Property Id=\"WVRTINSTALLED\">\n            <RegistrySearch Id=\"WVRTInstalledSystem\" Root=\"HKLM\" Key=\"SOFTWARE\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}\" Name=\"pv\" Type=\"raw\" Win64=\"no\" />\n            <RegistrySearch Id=\"WVRTInstalledUser\" Root=\"HKCU\" Key=\"SOFTWARE\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}\" Name=\"pv\" Type=\"raw\"/>\n        </Property>\n\n        {{#if download_bootstrapper}}\n        <CustomAction Id='DownloadAndInvokeBootstrapper' Directory=\"INSTALLDIR\" Execute=\"deferred\" ExeCommand='powershell.exe -NoProfile -windowstyle hidden try [\\{] [\\[]Net.ServicePointManager[\\]]::SecurityProtocol = [\\[]Net.SecurityProtocolType[\\]]::Tls12 [\\}] catch [\\{][\\}]; Invoke-WebRequest -Uri \"https://go.microsoft.com/fwlink/p/?LinkId=2124703\" -OutFile \"$env:TEMP\\MicrosoftEdgeWebview2Setup.exe\" ; Start-Process -FilePath \"$env:TEMP\\MicrosoftEdgeWebview2Setup.exe\" -ArgumentList ({{webview_installer_args}} &apos;/install&apos;) -Wait' Return='check'/>\n        <InstallExecuteSequence>\n            <Custom Action='DownloadAndInvokeBootstrapper' Before='InstallFinalize'>\n                <![CDATA[NOT(REMOVE OR WVRTINSTALLED)]]>\n            </Custom>\n        </InstallExecuteSequence>\n        {{/if}}\n\n        <!-- Embedded webview bootstrapper mode -->\n        {{#if webview2_bootstrapper_path}}\n        <Binary Id=\"MicrosoftEdgeWebview2Setup.exe\" SourceFile=\"{{webview2_bootstrapper_path}}\"/>\n        <CustomAction Id='InvokeBootstrapper' BinaryKey='MicrosoftEdgeWebview2Setup.exe' Execute=\"deferred\" ExeCommand='{{webview_installer_args}} /install' Return='check' />\n        <InstallExecuteSequence>\n            <Custom Action='InvokeBootstrapper' Before='InstallFinalize'>\n                <![CDATA[NOT(REMOVE OR WVRTINSTALLED)]]>\n            </Custom>\n        </InstallExecuteSequence>\n        {{/if}}\n\n        <!-- Embedded offline installer -->\n        {{#if webview2_installer_path}}\n        <Binary Id=\"MicrosoftEdgeWebView2RuntimeInstaller.exe\" SourceFile=\"{{webview2_installer_path}}\"/>\n        <CustomAction Id='InvokeStandalone' BinaryKey='MicrosoftEdgeWebView2RuntimeInstaller.exe' Execute=\"deferred\" ExeCommand='{{webview_installer_args}} /install' Return='check' />\n        <InstallExecuteSequence>\n            <Custom Action='InvokeStandalone' Before='InstallFinalize'>\n                <![CDATA[NOT(REMOVE OR WVRTINSTALLED)]]>\n            </Custom>\n        </InstallExecuteSequence>\n        {{/if}}\n\n        {{/if}}\n\n        {{#if enable_elevated_update_task}}\n        <!-- Install an elevated update task within Windows Task Scheduler -->\n        <CustomAction\n            Id=\"CreateUpdateTask\"\n            Return=\"check\"\n            Directory=\"INSTALLDIR\"\n            Execute=\"commit\"\n            Impersonate=\"yes\"\n            ExeCommand=\"powershell.exe -WindowStyle hidden .\\install-task.ps1\" />\n        <InstallExecuteSequence>\n            <Custom Action='CreateUpdateTask' Before='InstallFinalize'>\n                NOT(REMOVE)\n            </Custom>\n        </InstallExecuteSequence>\n        <!-- Remove elevated update task during uninstall -->\n        <CustomAction\n            Id=\"DeleteUpdateTask\"\n            Return=\"check\"\n            Directory=\"INSTALLDIR\"\n            ExeCommand=\"powershell.exe -WindowStyle hidden .\\uninstall-task.ps1\" />\n        <InstallExecuteSequence>\n            <Custom Action=\"DeleteUpdateTask\" Before='InstallFinalize'>\n                (REMOVE = \"ALL\") AND NOT UPGRADINGPRODUCTCODE\n            </Custom>\n        </InstallExecuteSequence>\n        {{/if}}\n\n        <SetProperty Id=\"ARPINSTALLLOCATION\" Value=\"[INSTALLDIR]\" After=\"CostFinalize\"/>\n    </Product>\n</Wix>\n"
  },
  {
    "path": "backend/tauri/tests/sample_clash_config.yaml",
    "content": "# port: 7890 # HTTP(S) 代理服务器端口\n# socks-port: 7891 # SOCKS5 代理端口\nmixed-port: 10801 # HTTP(S) 和 SOCKS 代理混合端口\n# redir-port: 7892 # 透明代理端口，用于 Linux 和 MacOS\n\n# Transparent proxy server port for Linux (TProxy TCP and TProxy UDP)\n# tproxy-port: 7893\n\nallow-lan: true # 允许局域网连接\nbind-address: '*' # 绑定 IP 地址，仅作用于 allow-lan 为 true，'*'表示所有地址\nauthentication: # http,socks 入口的验证用户名，密码\n  - 'username:password'\nskip-auth-prefixes: # 设置跳过验证的 IP 段\n  - 127.0.0.1/8\n  - ::1/128\nlan-allowed-ips: # 允许连接的 IP 地址段，仅作用于 allow-lan 为 true, 默认值为 0.0.0.0/0 和::/0\n  - 0.0.0.0/0\n  - ::/0\nlan-disallowed-ips: # 禁止连接的 IP 地址段，黑名单优先级高于白名单，默认值为空\n  - 192.168.0.3/32\n\n#  find-process-mode has 3 values:always, strict, off\n#  - always, 开启，强制匹配所有进程\n#  - strict, 默认，由 mihomo 判断是否开启\n#  - off, 不匹配进程，推荐在路由器上使用此模式\nfind-process-mode: strict\n\nmode: rule\n\n#自定义 geodata url\ngeox-url:\n  geoip: 'https://fastly.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geoip.dat'\n  geosite: 'https://fastly.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geosite.dat'\n  mmdb: 'https://fastly.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geoip.metadb'\n\ngeo-auto-update: false # 是否自动更新 geodata\ngeo-update-interval: 24 # 更新间隔，单位：小时\n\n# Matcher implementation used by GeoSite, available implementations:\n# - succinct (default, same as rule-set)\n# - mph (from V2Ray, also `hybrid` in Xray)\n# geosite-matcher: succinct\n\nlog-level: debug # 日志等级 silent/error/warning/info/debug\n\nipv6: true # 开启 IPv6 总开关，关闭阻断所有 IPv6 链接和屏蔽 DNS 请求 AAAA 记录\n\ntls:\n  certificate: string # 证书 PEM 格式，或者 证书的路径\n  private-key: string # 证书对应的私钥 PEM 格式，或者私钥路径\n  # 如果填写则开启ech（可由 mihomo generate ech-keypair <明文域名> 生成）\n  # ech-key: |\n  #   -----BEGIN ECH KEYS-----\n  #   ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK\n  #   madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz\n  #   dC5jb20AAA==\n  #   -----END ECH KEYS-----\n  custom-certifactes:\n    - |\n      -----BEGIN CERTIFICATE-----\n      format/pem...\n      -----END CERTIFICATE-----\n\nexternal-controller: 0.0.0.0:9093 # RESTful API 监听地址\nexternal-controller-tls: 0.0.0.0:9443 # RESTful API HTTPS 监听地址，需要配置 tls 部分配置文件\n# secret: \"123456\" # `Authorization:Bearer ${secret}`\n\n# RESTful API CORS标头配置\nexternal-controller-cors:\n  allow-origins:\n    - '*'\n  allow-private-network: true\n\n# RESTful API Unix socket 监听地址（ windows版本大于17063也可以使用，即大于等于1803/RS4版本即可使用 ）\n# ！！！注意： 从Unix socket访问api接口不会验证secret， 如果开启请自行保证安全问题 ！！！\n# 测试方法： curl -v --unix-socket \"mihomo.sock\" http://localhost/\nexternal-controller-unix: mihomo.sock\n\n# RESTful API Windows namedpipe 监听地址\n# ！！！注意： 从Windows namedpipe访问api接口不会验证secret， 如果开启请自行保证安全问题 ！！！\nexternal-controller-pipe: \\\\.\\pipe\\mihomo\n\n# tcp-concurrent: true # TCP 并发连接所有 IP, 将使用最快握手的 TCP\n\n# 配置 WEB UI 目录，使用 http://{{external-controller}}/ui 访问\nexternal-ui: /path/to/ui/folder/\nexternal-ui-name: xd\n# 目前支持下载zip,tgz格式的压缩包\nexternal-ui-url: 'https://github.com/MetaCubeX/metacubexd/archive/refs/heads/gh-pages.zip'\n\n# 在RESTful API端口上开启DOH服务器\n# ！！！该URL不会验证secret， 如果开启请自行保证安全问题 ！！！\nexternal-doh-server: /dns-query\n\n# interface-name: en0 # 设置出口网卡\n\n# 全局 TLS 指纹，优先低于 proxy 内的 client-fingerprint\n# 可选： \"chrome\",\"firefox\",\"safari\",\"ios\",\"random\",\"none\" options.\n# Utls is currently support TLS transport in TCP/grpc/WS/HTTP for VLESS/Vmess and trojan.\nglobal-client-fingerprint: chrome\n\n#  TCP keep alive interval\n# disable-keep-alive: false #目前在android端强制为true\n# keep-alive-idle: 15\n# keep-alive-interval: 15\n\n# routing-mark:6666 # 配置 fwmark 仅用于 Linux\nexperimental:\n  # Disable quic-go GSO support. This may result in reduced performance on Linux.\n  # This is not recommended for most users.\n  # Only users encountering issues with quic-go's internal implementation should enable this,\n  # and they should disable it as soon as the issue is resolved.\n  # This field will be removed when quic-go fixes all their issues in GSO.\n  # This equivalent to the environment variable QUIC_GO_DISABLE_GSO=1.\n  #quic-go-disable-gso: true\n\n# 类似于 /etc/hosts, 仅支持配置单个 IP\nhosts:\n# '*.mihomo.dev': 127.0.0.1\n# '.dev': 127.0.0.1\n# 'alpha.mihomo.dev': '::1'\n# test.com: [1.1.1.1, 2.2.2.2]\n# home.lan: lan # lan 为特别字段，将加入本地所有网卡的地址\n# baidu.com: google.com # 只允许配置一个别名\n\nprofile: # 存储 select 选择记录\n  store-selected: false\n\n  # 持久化 fake-ip\n  store-fake-ip: true\n\n# Tun 配置\ntun:\n  enable: false\n  stack: system # gvisor/mixed\n  dns-hijack:\n    - 0.0.0.0:53 # 需要劫持的 DNS\n  # auto-detect-interface: true # 自动识别出口网卡\n  # auto-route: true # 配置路由表\n  # mtu: 9000 # 最大传输单元\n  # gso: false # 启用通用分段卸载，仅支持 Linux\n  # gso-max-size: 65536 # 通用分段卸载包的最大大小\n  auto-redirect: false # 自动配置 iptables 以重定向 TCP 连接。仅支持 Linux。带有 auto-redirect 的 auto-route 现在可以在路由器上按预期工作，无需干预。\n  # strict-route: true # 将所有连接路由到 tun 来防止泄漏，但你的设备将无法其他设备被访问\n  route-address-set: # 将指定规则集中的目标 IP CIDR 规则添加到防火墙, 不匹配的流量将绕过路由, 仅支持 Linux，且需要 nftables，`auto-route` 和 `auto-redirect` 已启用。\n    - ruleset-1\n    - ruleset-2\n  route-exclude-address-set: # 将指定规则集中的目标 IP CIDR 规则添加到防火墙, 匹配的流量将绕过路由, 仅支持 Linux，且需要 nftables，`auto-route` 和 `auto-redirect` 已启用。\n    - ruleset-3\n    - ruleset-4\n  route-address: # 启用 auto-route 时使用自定义路由而不是默认路由\n    - 0.0.0.0/1\n    - 128.0.0.0/1\n    - '::/1'\n    - '8000::/1'\n  # inet4-route-address: # 启用 auto-route 时使用自定义路由而不是默认路由（旧写法）\n  #   - 0.0.0.0/1\n  #   - 128.0.0.0/1\n  # inet6-route-address: # 启用 auto-route 时使用自定义路由而不是默认路由（旧写法）\n  #   - \"::/1\"\n  #   - \"8000::/1\"\n  # endpoint-independent-nat: false # 启用独立于端点的 NAT\n  # include-interface: # 限制被路由的接口。默认不限制，与 `exclude-interface` 冲突\n  #   - \"lan0\"\n  # exclude-interface: # 排除路由的接口，与 `include-interface` 冲突\n  #   - \"lan1\"\n  # include-uid: # UID 规则仅在 Linux 下被支持，并且需要 auto-route\n  # - 0\n  # include-uid-range: # 限制被路由的的用户范围\n  # - 1000:9999\n  # exclude-uid: # 排除路由的的用户\n  #- 1000\n  # exclude-uid-range: # 排除路由的的用户范围\n  # - 1000:9999\n\n  # Android 用户和应用规则仅在 Android 下被支持\n  # 并且需要 auto-route\n\n  # include-android-user: # 限制被路由的 Android 用户\n  # - 0\n  # - 10\n  # include-package: # 限制被路由的 Android 应用包名\n  # - com.android.chrome\n  # exclude-package: # 排除被路由的 Android 应用包名\n  # - com.android.captiveportallogin\n\n# 嗅探域名 可选配置\nsniffer:\n  enable: false\n  ## 对 redir-host 类型识别的流量进行强制嗅探\n  ## 如：Tun、Redir 和 TProxy 并 DNS 为 redir-host 皆属于\n  # force-dns-mapping: false\n  ## 对所有未获取到域名的流量进行强制嗅探\n  # parse-pure-ip: false\n  # 是否使用嗅探结果作为实际访问，默认 true\n  # 全局配置，优先级低于 sniffer.sniff 实际配置\n  override-destination: false\n  sniff: # TLS 和 QUIC 默认如果不配置 ports 默认嗅探 443\n    QUIC:\n    #  ports: [ 443 ]\n    TLS:\n    #  ports: [443, 8443]\n\n    # 默认嗅探 80\n    HTTP: # 需要嗅探的端口\n      ports: [80, 8080-8880]\n      # 可覆盖 sniffer.override-destination\n      override-destination: true\n  force-domain:\n    - +.v2ex.com\n  # skip-src-address: # 对于来源ip跳过嗅探\n  #   - 192.168.0.3/32\n  # skip-dst-address: # 对于目标ip跳过嗅探\n  #   - 192.168.0.3/32\n  ## 对嗅探结果进行跳过\n  # skip-domain:\n  #   - Mijia Cloud\n  # 需要嗅探协议\n  # 已废弃，若 sniffer.sniff 配置则此项无效\n  sniffing:\n    - tls\n    - http\n  # 强制对此域名进行嗅探\n\n  # 仅对白名单中的端口进行嗅探，默认为 443，80\n  # 已废弃，若 sniffer.sniff 配置则此项无效\n  port-whitelist:\n    - '80'\n    - '443'\n    # - 8000-9999\n\ntunnels: # one line config\n  - tcp/udp,127.0.0.1:6553,114.114.114.114:53,proxy\n  - tcp,127.0.0.1:6666,rds.mysql.com:3306,vpn\n  # full yaml config\n  - network: [tcp, udp]\n    address: 127.0.0.1:7777\n    target: target.com\n    proxy: proxy\n\n# DNS 配置\ndns:\n  cache-algorithm: arc\n  enable: false # 关闭将使用系统 DNS\n  prefer-h3: false # 是否开启 DoH 支持 HTTP/3，将并发尝试\n  listen: 0.0.0.0:53 # 开启 DNS 服务器监听\n  # ipv6: false # false 将返回 AAAA 的空结果\n  # ipv6-timeout: 300 # 单位：ms，内部双栈并发时，向上游查询 AAAA 时，等待 AAAA 的时间，默认 100ms\n  # 用于解析 nameserver，fallback 以及其他 DNS 服务器配置的，DNS 服务域名\n  # 只能使用纯 IP 地址，可使用加密 DNS\n  default-nameserver:\n    - 114.114.114.114\n    - 8.8.8.8\n    - tls://1.12.12.12:853\n    - tls://223.5.5.5:853\n    - system # append DNS server from system configuration. If not found, it would print an error log and skip.\n  enhanced-mode: fake-ip # or redir-host\n\n  fake-ip-range: 198.18.0.1/16 # fake-ip 池设置\n\n  # 配置不使用 fake-ip 的域名\n  fake-ip-filter:\n    - '*.lan'\n    - localhost.ptlogin2.qq.com\n    # fakeip-filter 为 rule-providers 中的名为 fakeip-filter 规则订阅，\n    # 且 behavior 必须为 domain/classical，当为 classical 时仅会生效域名类规则\n    - rule-set:fakeip-filter\n    # fakeip-filter 为 geosite 中名为 fakeip-filter 的分类（需要自行保证该分类存在）\n    - geosite:fakeip-filter\n  # 配置fake-ip-filter的匹配模式，默认为blacklist，即如果匹配成功不返回fake-ip\n  # 可设置为whitelist，即只有匹配成功才返回fake-ip\n  fake-ip-filter-mode: blacklist\n\n  # use-hosts: true # 查询 hosts\n\n  # 配置后面的nameserver、fallback和nameserver-policy向dns服务器的连接过程是否遵守遵守rules规则\n  # 如果为false（默认值）则这三部分的dns服务器在未特别指定的情况下会直连\n  # 如果为true，将会按照rules的规则匹配链接方式（走代理或直连），如果有特别指定则任然以指定值为准\n  # 仅当proxy-server-nameserver非空时可以开启此选项, 强烈不建议和prefer-h3一起使用\n  # 此外，这三者配置中的dns服务器如果出现域名会采用default-nameserver配置项解析，也请确保正确配置default-nameserver\n  respect-rules: false\n\n  # DNS 主要域名配置\n  # 支持 UDP，TCP，DoT，DoH，DoQ\n  # 这部分为主要 DNS 配置，影响所有直连，确保使用对大陆解析精准的 DNS\n  nameserver:\n    - 114.114.114.114 # default value\n    - 8.8.8.8 # default value\n    - tls://223.5.5.5:853 # DNS over TLS\n    - https://doh.pub/dns-query # DNS over HTTPS\n    - https://dns.alidns.com/dns-query#h3=true # 强制 HTTP/3，与 perfer-h3 无关，强制开启 DoH 的 HTTP/3 支持，若不支持将无法使用\n    - https://mozilla.cloudflare-dns.com/dns-query#DNS&h3=true # 指定策略组和使用 HTTP/3\n    - dhcp://en0 # dns from dhcp\n    - quic://dns.adguard.com:784 # DNS over QUIC\n    # - '8.8.8.8#RULES' # 效果同respect-rules，但仅对该服务器生效\n    # - '8.8.8.8#en0' # 兼容指定 DNS 出口网卡\n\n  # 当配置 fallback 时，会查询 nameserver 中返回的 IP 是否为 CN，非必要配置\n  # 当不是 CN，则使用 fallback 中的 DNS 查询结果\n  # 确保配置 fallback 时能够正常查询\n  # fallback:\n  #   - tcp://1.1.1.1\n  #   - 'tcp://1.1.1.1#ProxyGroupName' # 指定 DNS 过代理查询，ProxyGroupName 为策略组名或节点名，过代理配置优先于配置出口网卡，当找不到策略组或节点名则设置为出口网卡\n\n  # 专用于节点域名解析的 DNS 服务器，非必要配置项，如果不填则遵循nameserver-policy、nameserver和fallback的配置\n  # proxy-server-nameserver:\n  #   - https://dns.google/dns-query\n  #   - tls://one.one.one.one\n\n  # 专用于direct出口域名解析的 DNS 服务器，非必要配置项，如果不填则遵循nameserver-policy、nameserver和fallback的配置\n  # direct-nameserver:\n  #   - system://\n  # direct-nameserver-follow-policy: false # 是否遵循nameserver-policy，默认为不遵守，仅当direct-nameserver不为空时生效\n\n  # 配置 fallback 使用条件\n  # fallback-filter:\n  #   geoip: true # 配置是否使用 geoip\n  #   geoip-code: CN # 当 nameserver 域名的 IP 查询 geoip 库为 CN 时，不使用 fallback 中的 DNS 查询结果\n  #   配置强制 fallback，优先于 IP 判断，具体分类自行查看 geosite 库\n  #   geosite:\n  #     - gfw\n  #   如果不匹配 ipcidr 则使用 nameservers 中的结果\n  #   ipcidr:\n  #     - 240.0.0.0/4\n  #   domain:\n  #     - '+.google.com'\n  #     - '+.facebook.com'\n  #     - '+.youtube.com'\n\n  # 配置查询域名使用的 DNS 服务器\n  nameserver-policy:\n    #   'www.baidu.com': '114.114.114.114'\n    #   '+.internal.crop.com': '10.0.0.1'\n    'geosite:cn,private,apple':\n      - https://doh.pub/dns-query\n      - https://dns.alidns.com/dns-query\n    'geosite:category-ads-all': rcode://success\n    'www.baidu.com,+.google.cn': [223.5.5.5, https://dns.alidns.com/dns-query]\n    ## global，dns 为 rule-providers 中的名为 global 和 dns 规则订阅，\n    ## 且 behavior 必须为 domain/classical，当为 classical 时仅会生效域名类规则\n    # \"rule-set:global,dns\": 8.8.8.8\n\nproxies: # socks5\n  - name: 'socks'\n    type: socks5\n    server: server\n    port: 443\n    # username: username\n    # password: password\n    # tls: true\n    # fingerprint: xxxx\n    # skip-cert-verify: true\n    # udp: true\n    # ip-version: ipv6\n\n  # http\n  - name: 'http'\n    type: http\n    server: server\n    port: 443\n    # username: username\n    # password: password\n    # tls: true # https\n    # skip-cert-verify: true\n    # sni: custom.com\n    # fingerprint: xxxx # 同 experimental.fingerprints 使用 sha256 指纹，配置协议独立的指纹，将忽略 experimental.fingerprints\n    # ip-version: dual\n\n  # Snell\n  # Beware that there's currently no UDP support yet\n  - name: 'snell'\n    type: snell\n    server: server\n    port: 44046\n    psk: yourpsk\n    # version: 2\n    # obfs-opts:\n    # mode: http # or tls\n    # host: bing.com\n\n  # Shadowsocks\n  # cipher支持:\n  #   aes-128-gcm aes-192-gcm aes-256-gcm\n  #   aes-128-cfb aes-192-cfb aes-256-cfb\n  #   aes-128-ctr aes-192-ctr aes-256-ctr\n  #   rc4-md5 chacha20-ietf xchacha20\n  #   chacha20-ietf-poly1305 xchacha20-ietf-poly1305\n  #   2022-blake3-aes-128-gcm 2022-blake3-aes-256-gcm 2022-blake3-chacha20-poly1305\n  - name: 'ss1'\n    type: ss\n    server: server\n    port: 443\n    cipher: chacha20-ietf-poly1305\n    password: 'password'\n    # udp: true\n    # udp-over-tcp: false\n    # ip-version: ipv4 # 设置节点使用 IP 版本，可选：dual，ipv4，ipv6，ipv4-prefer，ipv6-prefer。默认使用 dual\n    # ipv4：仅使用 IPv4  ipv6：仅使用 IPv6\n    # ipv4-prefer：优先使用 IPv4 对于 TCP 会进行双栈解析，并发链接但是优先使用 IPv4 链接，\n    # UDP 则为双栈解析，获取结果中的第一个 IPv4\n    # ipv6-prefer 同 ipv4-prefer\n    # 现有协议都支持此参数，TCP 效果仅在开启 tcp-concurrent 生效\n    smux:\n      enabled: false\n      protocol: smux # smux/yamux/h2mux\n      # max-connections: 4 # Maximum connections. Conflict with max-streams.\n      # min-streams: 4 # Minimum multiplexed streams in a connection before opening a new connection. Conflict with max-streams.\n      # max-streams: 0 # Maximum multiplexed streams in a connection before opening a new connection. Conflict with max-connections and min-streams.\n      # padding: false # Enable padding. Requires sing-box server version 1.3-beta9 or later.\n      # statistic: false # 控制是否将底层连接显示在面板中，方便打断底层连接\n      # only-tcp: false # 如果设置为 true, smux 的设置将不会对 udp 生效，udp 连接会直接走底层协议\n\n  - name: 'ss2'\n    type: ss\n    server: server\n    port: 443\n    cipher: chacha20-ietf-poly1305\n    password: 'password'\n    plugin: obfs\n    plugin-opts:\n      mode: tls # or http\n      # host: bing.com\n\n  - name: 'ss3'\n    type: ss\n    server: server\n    port: 443\n    cipher: chacha20-ietf-poly1305\n    password: 'password'\n    plugin: v2ray-plugin\n    plugin-opts:\n      mode: websocket # no QUIC now\n      # tls: true # wss\n      # 可使用 openssl x509 -noout -fingerprint -sha256 -inform pem -in yourcert.pem 获取\n      # 配置指纹将实现 SSL Pining 效果\n      # fingerprint: xxxx\n      # ech-opts:\n      #   enable: true # 必须手动开启\n      #   # 如果config为空则通过dns解析，不为空则通过该值指定，格式为经过base64编码的ech参数（dig +short TYPE65 tls-ech.dev）\n      #   config: AEn+DQBFKwAgACABWIHUGj4u+PIggYXcR5JF0gYk3dCRioBW8uJq9H4mKAAIAAEAAQABAANAEnB1YmxpYy50bHMtZWNoLmRldgAA\n      # skip-cert-verify: true\n      # host: bing.com\n      # path: \"/\"\n      # mux: true\n      # headers:\n      #   custom: value\n      # v2ray-http-upgrade: false\n      # v2ray-http-upgrade-fast-open: false\n\n  - name: 'ss4-shadow-tls'\n    type: ss\n    server: server\n    port: 443\n    cipher: chacha20-ietf-poly1305\n    password: 'password'\n    plugin: shadow-tls\n    client-fingerprint: chrome\n    plugin-opts:\n      host: 'cloud.tencent.com'\n      password: 'shadow_tls_password'\n      version: 2 # support 1/2/3\n      # alpn: [\"h2\",\"http/1.1\"]\n\n  - name: 'ss5'\n    type: ss\n    server: server\n    port: 443\n    cipher: chacha20-ietf-poly1305\n    password: 'password'\n    plugin: gost-plugin\n    plugin-opts:\n      mode: websocket\n      # tls: true # wss\n      # 可使用 openssl x509 -noout -fingerprint -sha256 -inform pem -in yourcert.pem 获取\n      # 配置指纹将实现 SSL Pining 效果\n      # fingerprint: xxxx\n      # skip-cert-verify: true\n      # host: bing.com\n      # path: \"/\"\n      # mux: true\n      # headers:\n      #   custom: value\n\n  - name: 'ss-restls-tls13'\n    type: ss\n    server: [YOUR_SERVER_IP]\n    port: 443\n    cipher: chacha20-ietf-poly1305\n    password: [YOUR_SS_PASSWORD]\n    client-fingerprint:\n      chrome # One of: chrome, ios, firefox or safari\n      # 可以是 chrome, ios, firefox, safari 中的一个\n    plugin: restls\n    plugin-opts:\n      host:\n        'www.microsoft.com' # Must be a TLS 1.3 server\n        # 应当是一个 TLS 1.3 服务器\n      password: [YOUR_RESTLS_PASSWORD]\n      version-hint: 'tls13'\n      # Control your post-handshake traffic through restls-script\n      # Hide proxy behaviors like \"tls in tls\".\n      # see https://github.com/3andne/restls/blob/main/Restls-Script:%20Hide%20Your%20Proxy%20Traffic%20Behavior.md\n      # 用 restls 剧本来控制握手后的行为，隐藏\"tls in tls\"等特征\n      # 详情：https://github.com/3andne/restls/blob/main/Restls-Script:%20%E9%9A%90%E8%97%8F%E4%BD%A0%E7%9A%84%E4%BB%A3%E7%90%86%E8%A1%8C%E4%B8%BA.md\n      restls-script: '300?100<1,400~100,350~100,600~100,300~200,300~100'\n\n  - name: 'ss-restls-tls12'\n    type: ss\n    server: [YOUR_SERVER_IP]\n    port: 443\n    cipher: chacha20-ietf-poly1305\n    password: [YOUR_SS_PASSWORD]\n    client-fingerprint:\n      chrome # One of: chrome, ios, firefox or safari\n      # 可以是 chrome, ios, firefox, safari 中的一个\n    plugin: restls\n    plugin-opts:\n      host:\n        'vscode.dev' # Must be a TLS 1.2 server\n        # 应当是一个 TLS 1.2 服务器\n      password: [YOUR_RESTLS_PASSWORD]\n      version-hint: 'tls12'\n      restls-script: '1000?100<1,500~100,350~100,600~100,400~200'\n\n  # vmess\n  # cipher 支持 auto/aes-128-gcm/chacha20-poly1305/none\n  - name: 'vmess'\n    type: vmess\n    server: server\n    port: 443\n    uuid: uuid\n    alterId: 32\n    cipher: auto\n    # udp: true\n    # tls: true\n    # fingerprint: xxxx\n    # client-fingerprint: chrome    # Available: \"chrome\",\"firefox\",\"safari\",\"ios\",\"random\", currently only support TLS transport in TCP/GRPC/WS/HTTP for VLESS/Vmess and trojan.\n    # skip-cert-verify: true\n    # servername: example.com # priority over wss host\n    # network: ws\n    # ech-opts:\n    #   enable: true # 必须手动开启\n    #   # 如果config为空则通过dns解析，不为空则通过该值指定，格式为经过base64编码的ech参数（dig +short TYPE65 tls-ech.dev）\n    #   config: AEn+DQBFKwAgACABWIHUGj4u+PIggYXcR5JF0gYk3dCRioBW8uJq9H4mKAAIAAEAAQABAANAEnB1YmxpYy50bHMtZWNoLmRldgAA\n    # ws-opts:\n    # path: /path\n    # headers:\n    #   Host: v2ray.com\n    # max-early-data: 2048\n    # early-data-header-name: Sec-WebSocket-Protocol\n    # v2ray-http-upgrade: false\n    # v2ray-http-upgrade-fast-open: false\n\n  - name: 'vmess-h2'\n    type: vmess\n    server: server\n    port: 443\n    uuid: uuid\n    alterId: 32\n    cipher: auto\n    network: h2\n    tls: true\n    # fingerprint: xxxx\n    h2-opts:\n      host:\n        - http.example.com\n        - http-alt.example.com\n      path: /\n\n  - name: 'vmess-http'\n    type: vmess\n    server: server\n    port: 443\n    uuid: uuid\n    alterId: 32\n    cipher: auto\n    # udp: true\n    # network: http\n    # http-opts:\n    #   method: \"GET\"\n    #   path:\n    #     - '/'\n    #     - '/video'\n    #   headers:\n    #     Connection:\n    #       - keep-alive\n    # ip-version: ipv4 # 设置使用 IP 类型偏好，可选：ipv4，ipv6，dual，默认值：dual\n\n  - name: vmess-grpc\n    server: server\n    port: 443\n    type: vmess\n    uuid: uuid\n    alterId: 32\n    cipher: auto\n    network: grpc\n    tls: true\n    # fingerprint: xxxx\n    servername: example.com\n    # skip-cert-verify: true\n    grpc-opts:\n      grpc-service-name: 'example'\n    # ip-version: ipv4\n\n  # vless\n  - name: 'vless-tcp'\n    type: vless\n    server: server\n    port: 443\n    uuid: uuid\n    network: tcp\n    servername: example.com # AKA SNI\n    # flow: xtls-rprx-direct # xtls-rprx-origin  # enable XTLS\n    # skip-cert-verify: true\n    # fingerprint: xxxx\n    # client-fingerprint: random # Available: \"chrome\",\"firefox\",\"safari\",\"random\",\"none\"\n    # ech-opts:\n    #   enable: true # 必须手动开启\n    #   # 如果config为空则通过dns解析，不为空则通过该值指定，格式为经过base64编码的ech参数（dig +short TYPE65 tls-ech.dev）\n    #   config: AEn+DQBFKwAgACABWIHUGj4u+PIggYXcR5JF0gYk3dCRioBW8uJq9H4mKAAIAAEAAQABAANAEnB1YmxpYy50bHMtZWNoLmRldgAA\n\n  - name: 'vless-vision'\n    type: vless\n    server: server\n    port: 443\n    uuid: uuid\n    network: tcp\n    tls: true\n    udp: true\n    flow: xtls-rprx-vision\n    client-fingerprint: chrome\n    # fingerprint: xxxx\n    # skip-cert-verify: true\n\n  - name: 'vless-encryption'\n    type: vless\n    server: server\n    port: 443\n    uuid: uuid\n    network: tcp\n    # -------------------------\n    # vless encryption客户端配置：\n    # （native/xorpub 的 XTLS 可以 Splice。只使用 1-RTT 模式 / 若服务端发的 ticket 中秒数不为零则 0-RTT 复用）\n    # / 是只能选一个，后面 base64 至少一个，无限串联，使用  mihomo generate vless-x25519 和 mihomo generate vless-mlkem768 生成，替换值时需去掉括号\n    # -------------------------\n    encryption: 'mlkem768x25519plus.native/xorpub/random.1rtt/0rtt.(X25519 Password).(ML-KEM-768 Client)...'\n    tls: false #可以不开启tls\n    udp: true\n\n  - name: 'vless-reality-vision'\n    type: vless\n    server: server\n    port: 443\n    uuid: uuid\n    network: tcp\n    tls: true\n    udp: true\n    flow: xtls-rprx-vision\n    servername: www.microsoft.com # REALITY servername\n    reality-opts:\n      public-key: xxx\n      short-id: xxx # optional\n      support-x25519mlkem768: false # 如果服务端支持可手动设置为true\n    client-fingerprint: chrome # cannot be empty\n\n  - name: 'vless-reality-grpc'\n    type: vless\n    server: server\n    port: 443\n    uuid: uuid\n    network: grpc\n    tls: true\n    udp: true\n    flow:\n    # skip-cert-verify: true\n    client-fingerprint: chrome\n    servername: testingcf.jsdelivr.net\n    grpc-opts:\n      grpc-service-name: 'grpc'\n    reality-opts:\n      public-key: CrrQSjAG_YkHLwvM2M-7XkKJilgL5upBKCp0od0tLhE\n      short-id: 10f897e26c4b9478\n      support-x25519mlkem768: false # 如果服务端支持可手动设置为true\n\n  - name: 'vless-ws'\n    type: vless\n    server: server\n    port: 443\n    uuid: uuid\n    udp: true\n    tls: true\n    network: ws\n    # client-fingerprint: random # Available: \"chrome\",\"firefox\",\"safari\",\"random\",\"none\"\n    servername: example.com # priority over wss host\n    # skip-cert-verify: true\n    # fingerprint: xxxx\n    ws-opts:\n      path: '/'\n      headers:\n        Host: example.com\n      # v2ray-http-upgrade: false\n      # v2ray-http-upgrade-fast-open: false\n\n  # Trojan\n  - name: 'trojan'\n    type: trojan\n    server: server\n    port: 443\n    password: yourpsk\n    # client-fingerprint: random # Available: \"chrome\",\"firefox\",\"safari\",\"random\",\"none\"\n    # fingerprint: xxxx\n    # udp: true\n    # sni: example.com # aka server name\n    # alpn:\n    #   - h2\n    #   - http/1.1\n    # skip-cert-verify: true\n    # ss-opts: # like trojan-go's `shadowsocks` config\n    #   enabled: false\n    #   method: aes-128-gcm # aes-128-gcm/aes-256-gcm/chacha20-ietf-poly1305\n    #   password: \"example\"\n    # ech-opts:\n    #   enable: true # 必须手动开启\n    #   # 如果config为空则通过dns解析，不为空则通过该值指定，格式为经过base64编码的ech参数（dig +short TYPE65 tls-ech.dev）\n    #   config: AEn+DQBFKwAgACABWIHUGj4u+PIggYXcR5JF0gYk3dCRioBW8uJq9H4mKAAIAAEAAQABAANAEnB1YmxpYy50bHMtZWNoLmRldgAA\n\n  - name: trojan-grpc\n    server: server\n    port: 443\n    type: trojan\n    password: 'example'\n    network: grpc\n    sni: example.com\n    # skip-cert-verify: true\n    # fingerprint: xxxx\n    udp: true\n    grpc-opts:\n      grpc-service-name: 'example'\n\n  - name: trojan-ws\n    server: server\n    port: 443\n    type: trojan\n    password: 'example'\n    network: ws\n    sni: example.com\n    # skip-cert-verify: true\n    # fingerprint: xxxx\n    udp: true\n    # ws-opts:\n    #   path: /path\n    #   headers:\n    #     Host: example.com\n    #   v2ray-http-upgrade: false\n    #   v2ray-http-upgrade-fast-open: false\n\n  - name: 'trojan-xtls'\n    type: trojan\n    server: server\n    port: 443\n    password: yourpsk\n    flow: 'xtls-rprx-direct' # xtls-rprx-origin xtls-rprx-direct\n    flow-show: true\n    # udp: true\n    # sni: example.com # aka server name\n    # skip-cert-verify: true\n    # fingerprint: xxxx\n\n  #hysteria\n  - name: 'hysteria'\n    type: hysteria\n    server: server.com\n    port: 443\n    # ports: 1000,2000-3000,5000 # port 不可省略\n    auth-str: yourpassword\n    # obfs: obfs_str\n    # alpn:\n    #   - h3\n    protocol: udp # 支持 udp/wechat-video/faketcp\n    up: '30 Mbps' # 若不写单位，默认为 Mbps\n    down: '200 Mbps' # 若不写单位，默认为 Mbps\n    # sni: server.com\n    # ech-opts:\n    #   enable: true # 必须手动开启\n    #   # 如果config为空则通过dns解析，不为空则通过该值指定，格式为经过base64编码的ech参数（dig +short TYPE65 tls-ech.dev）\n    #   config: AEn+DQBFKwAgACABWIHUGj4u+PIggYXcR5JF0gYk3dCRioBW8uJq9H4mKAAIAAEAAQABAANAEnB1YmxpYy50bHMtZWNoLmRldgAA\n    # skip-cert-verify: false\n    # recv-window-conn: 12582912\n    # recv-window: 52428800\n    # ca: \"./my.ca\"\n    # ca-str: \"xyz\"\n    # disable-mtu-discovery: false\n    # fingerprint: xxxx\n    # fast-open: true # 支持 TCP 快速打开，默认为 false\n\n  #hysteria2\n  - name: 'hysteria2'\n    type: hysteria2\n    server: server.com\n    port: 443\n    # ports: 1000,2000-3000,5000 # port 不可省略\n    # hop-interval: 15\n    #  up 和 down 均不写或为 0 则使用 BBR 流控\n    # up: \"30 Mbps\" # 若不写单位，默认为 Mbps\n    # down: \"200 Mbps\" # 若不写单位，默认为 Mbps\n    password: yourpassword\n    # obfs: salamander # 默认为空，如果填写则开启 obfs，目前仅支持 salamander\n    # obfs-password: yourpassword\n    # sni: server.com\n    # ech-opts:\n    #   enable: true # 必须手动开启\n    #   # 如果config为空则通过dns解析，不为空则通过该值指定，格式为经过base64编码的ech参数（dig +short TYPE65 tls-ech.dev）\n    #   config: AEn+DQBFKwAgACABWIHUGj4u+PIggYXcR5JF0gYk3dCRioBW8uJq9H4mKAAIAAEAAQABAANAEnB1YmxpYy50bHMtZWNoLmRldgAA\n    # skip-cert-verify: false\n    # fingerprint: xxxx\n    # alpn:\n    #   - h3\n    # ca: \"./my.ca\"\n    # ca-str: \"xyz\"\n    ###quic-go特殊配置项，不要随意修改除非你知道你在干什么###\n    # initial-stream-receive-window： 8388608\n    # max-stream-receive-window： 8388608\n    # initial-connection-receive-window： 20971520\n    # max-connection-receive-window： 20971520\n\n  # wireguard\n  - name: 'wg'\n    type: wireguard\n    server: 162.159.192.1\n    port: 2480\n    ip: 172.16.0.2\n    ipv6: fd01:5ca1:ab1e:80fa:ab85:6eea:213f:f4a5\n    public-key: Cr8hWlKvtDt7nrvf+f0brNQQzabAqrjfBvas9pmowjo=\n    #    pre-shared-key: 31aIhAPwktDGpH4JDhA8GNvjFXEf/a6+UaQRyOAiyfM=\n    private-key: eCtXsJZ27+4PbhDkHnB923tkUn2Gj59wZw5wFA75MnU=\n    udp: true\n    reserved: 'U4An'\n    # 数组格式也是合法的\n    # reserved: [209,98,59]\n    # 一个出站代理的标识。当值不为空时，将使用指定的 proxy 发出连接\n    # dialer-proxy: \"ss1\"\n    # remote-dns-resolve: true # 强制 dns 远程解析，默认值为 false\n    # dns: [ 1.1.1.1, 8.8.8.8 ] # 仅在 remote-dns-resolve 为 true 时生效\n    # refresh-server-ip-interval: 60 # 重新解析server ip的间隔，单位为秒，默认值为0即仅第一次链接时解析server域名，仅应在server域名对应的IP会发生变化时启用该选项（如家宽ddns）\n    # 如果 peers 不为空，该段落中的 allowed-ips 不可为空；前面段落的 server,port,public-key,pre-shared-key 均会被忽略，但 private-key 会被保留且只能在顶层指定\n    # peers:\n    #   - server: 162.159.192.1\n    #     port: 2480\n    #     public-key: Cr8hWlKvtDt7nrvf+f0brNQQzabAqrjfBvas9pmowjo=\n    #     # pre-shared-key: 31aIhAPwktDGpH4JDhA8GNvjFXEf/a6+UaQRyOAiyfM=\n    #     allowed-ips: ['0.0.0.0/0']\n    #     reserved: [209,98,59]\n    # 如果存在则开启AmneziaWG功能\n    # amnezia-wg-option:\n    #   jc: 5\n    #   jmin: 500\n    #   jmax: 501\n    #   s1: 30\n    #   s2: 40\n    #   h1: 123456\n    #   h2: 67543\n    #   h4: 32345\n    #   h3: 123123\n    #   # AmneziaWG v1.5\n    #   i1: <b 0xf6ab3267fa><c><b 0xf6ab><t><r 10><wt 10>\n    #   i2: <b 0xf6ab3267fa><r 100>\n    #   i3: \"\"\n    #   i4: \"\"\n    #   i5: \"\"\n    #   j1: <b 0xffffffff><c><b 0xf6ab><t><r 10>\n    #   j2: <c><b 0xf6ab><t><wt 1000>\n    #   j3: <t><b 0xf6ab><c><r 10>\n    #   itime: 60\n\n  # tuic\n  - name: tuic\n    server: www.example.com\n    port: 10443\n    type: tuic\n    # tuicV4 必须填写 token（不可同时填写 uuid 和 password）\n    token: TOKEN\n    # tuicV5 必须填写 uuid 和 password（不可同时填写 token）\n    uuid: 00000000-0000-0000-0000-000000000001\n    password: PASSWORD_1\n    # ip: 127.0.0.1 # for overwriting the DNS lookup result of the server address set in option 'server'\n    # heartbeat-interval: 10000\n    # alpn: [h3]\n    disable-sni: true\n    reduce-rtt: true\n    request-timeout: 8000\n    udp-relay-mode: native # Available: \"native\", \"quic\". Default: \"native\"\n    # congestion-controller: bbr # Available: \"cubic\", \"new_reno\", \"bbr\". Default: \"cubic\"\n    # cwnd: 10 # default: 32\n    # max-udp-relay-packet-size: 1500\n    # fast-open: true\n    # skip-cert-verify: true\n    # max-open-streams: 20 # default 100, too many open streams may hurt performance\n    # sni: example.com\n    # ech-opts:\n    #   enable: true # 必须手动开启\n    #   # 如果config为空则通过dns解析，不为空则通过该值指定，格式为经过base64编码的ech参数（dig +short TYPE65 tls-ech.dev）\n    #   config: AEn+DQBFKwAgACABWIHUGj4u+PIggYXcR5JF0gYk3dCRioBW8uJq9H4mKAAIAAEAAQABAANAEnB1YmxpYy50bHMtZWNoLmRldgAA\n    #\n    # meta 和 sing-box 私有扩展，将 ss-uot 用于 udp 中继，开启此选项后 udp-relay-mode 将失效\n    # 警告，与原版 tuic 不兼容！！！\n    # udp-over-stream: false\n    # udp-over-stream-version: 1\n\n  # ShadowsocksR\n  # The supported ciphers (encryption methods): all stream ciphers in ss\n  # The supported obfses:\n  #   plain http_simple http_post\n  #   random_head tls1.2_ticket_auth tls1.2_ticket_fastauth\n  # The supported protocols:\n  #   origin auth_sha1_v4 auth_aes128_md5\n  #   auth_aes128_sha1 auth_chain_a auth_chain_b\n  - name: 'ssr'\n    type: ssr\n    server: server\n    port: 443\n    cipher: chacha20-ietf\n    password: 'password'\n    obfs: tls1.2_ticket_auth\n    protocol: auth_sha1_v4\n    # obfs-param: domain.tld\n    # protocol-param: \"#\"\n    # udp: true\n\n  - name: 'ssh-out'\n    type: ssh\n\n    server: 127.0.0.1\n    port: 22\n    username: root\n    password: password\n    privateKey: path\n\n  # mieru\n  - name: mieru\n    type: mieru\n    server: 1.2.3.4\n    port: 2999\n    # port-range: 2090-2099 #（不可同时填写 port 和 port-range）\n    transport: TCP # 只支持 TCP\n    udp: true # 支持 UDP over TCP\n    username: user\n    password: password\n    # 可以使用的值包括 MULTIPLEXING_OFF, MULTIPLEXING_LOW, MULTIPLEXING_MIDDLE, MULTIPLEXING_HIGH。其中 MULTIPLEXING_OFF 会关闭多路复用功能。默认值为 MULTIPLEXING_LOW。\n    # multiplexing: MULTIPLEXING_LOW\n    # 如果想开启 0-RTT 握手，请设置为 HANDSHAKE_NO_WAIT，否则请设置为 HANDSHAKE_STANDARD。默认值为 HANDSHAKE_STANDARD\n    # handshake-mode: HANDSHAKE_STANDARD\n\n  # anytls\n  - name: anytls\n    type: anytls\n    server: 1.2.3.4\n    port: 443\n    password: '<your password>'\n    # client-fingerprint: chrome\n    udp: true\n    # idle-session-check-interval: 30 # seconds\n    # idle-session-timeout: 30 # seconds\n    # min-idle-session: 0\n    # sni: \"example.com\"\n    # alpn:\n    #   - h2\n    #   - http/1.1\n    # skip-cert-verify: true\n\n  # dns 出站会将请求劫持到内部 dns 模块，所有请求均在内部处理\n  - name: 'dns-out'\n    type: dns\n\n  # 配置指定 interface-name 和 fwmark 的 DIRECT\n  - name: en1-direct\n    type: direct\n    interface-name: en1\n    routing-mark: 6667\nproxy-groups:\n  # 代理链，目前 relay 可以支持 udp 的只有 vmess/vless/trojan/ss/ssr/tuic\n  # wireguard 目前不支持在 relay 中使用，请使用 proxy 中的 dialer-proxy 配置项\n  # Traffic: mihomo <-> http <-> vmess <-> ss1 <-> ss2 <-> Internet\n  - name: 'relay'\n    type: relay\n    proxies:\n      - http\n      - vmess\n      - ss1\n      - ss2\n\n  # url-test 将按照 url 测试结果使用延迟最低节点\n  - name: 'auto'\n    type: url-test\n    proxies:\n      - ss1\n      - ss2\n      - vmess1\n    # tolerance: 150\n    # lazy: true\n    # expected-status: 204 # 当健康检查返回状态码与期望值不符时，认为节点不可用\n    url: 'https://cp.cloudflare.com/generate_204'\n    interval: 300\n\n  # fallback 将按照 url 测试结果按照节点顺序选择\n  - name: 'fallback-auto'\n    type: fallback\n    proxies:\n      - ss1\n      - ss2\n      - vmess1\n    url: 'https://cp.cloudflare.com/generate_204'\n    interval: 300\n\n  # load-balance 将按照算法随机选择节点\n  - name: 'load-balance'\n    type: load-balance\n    proxies:\n      - ss1\n      - ss2\n      - vmess1\n    url: 'https://cp.cloudflare.com/generate_204'\n    interval: 300\n  # strategy: consistent-hashing # 可选 round-robin 和 sticky-sessions\n\n  # select 用户自行选择节点\n  - name: Proxy\n    type: select\n    # disable-udp: true\n    proxies:\n      - ss1\n      - ss2\n      - vmess1\n      - auto\n\n  - name: UseProvider\n    type: select\n    filter: 'HK|TW' # 正则表达式，过滤 provider1 中节点名包含 HK 或 TW\n    use:\n      - provider1\n    proxies:\n      - Proxy\n      - DIRECT\n\n# Mihomo 格式的节点或支持 *ray 的分享格式\nproxy-providers:\n  provider1:\n    type: http # http 的 path 可空置，默认储存路径为 homedir 的 proxies 文件夹，文件名为 url 的 md5\n    url: 'url'\n    interval: 3600\n    path: ./provider1.yaml # 默认只允许存储在 mihomo 的 Home Dir，如果想存储到其他位置，请通过设置 SAFE_PATHS 环境变量指定额外的安全路径。该环境变量的语法同本操作系统的PATH环境变量解析规则（即Windows下以分号分割，其他系统下以冒号分割）\n    proxy: DIRECT\n    # size-limit: 10240 # 限制下载文件最大为10kb，默认为0即不限制文件大小\n    header:\n      User-Agent:\n        - 'Clash/v1.18.0'\n        - 'mihomo/1.18.3'\n      # Accept:\n      # - 'application/vnd.github.v3.raw'\n      # Authorization:\n      # - 'token 1231231'\n    health-check:\n      enable: true\n      interval: 600\n      # lazy: true\n      url: https://cp.cloudflare.com/generate_204\n      # expected-status: 204 # 当健康检查返回状态码与期望值不符时，认为节点不可用\n    override: # 覆写节点加载时的一些配置项\n      skip-cert-verify: true\n      udp: true\n      # down: \"50 Mbps\"\n      # up: \"10 Mbps\"\n      # dialer-proxy: proxy\n      # interface-name: tailscale0\n      # routing-mark: 233\n      # ip-version: ipv4-prefer\n      # additional-prefix: \"[provider1]\"\n      # additional-suffix: \"test\"\n      # # 名字替换，支持正则表达式\n      # proxy-name:\n      #   - pattern: \"test\"\n      #     target: \"TEST\"\n      #   - pattern: \"IPLC-(.*?)倍\"\n      #     target: \"iplc x $1\"\n\n  provider2:\n    type: inline\n    dialer-proxy: proxy\n    payload:\n      - name: 'ss1'\n        type: ss\n        server: server\n        port: 443\n        cipher: chacha20-ietf-poly1305\n        password: 'password'\n\n  test:\n    type: file\n    path: /test.yaml\n    health-check:\n      enable: true\n      interval: 36000\n      url: https://cp.cloudflare.com/generate_204\nrule-providers:\n  rule1:\n    behavior: classical # domain ipcidr\n    interval: 259200\n    path: /path/to/save/file.yaml # 默认只允许存储在 mihomo 的 Home Dir，如果想存储到其他位置，请通过设置 SAFE_PATHS 环境变量指定额外的安全路径。该环境变量的语法同本操作系统的PATH环境变量解析规则（即Windows下以分号分割，其他系统下以冒号分割）\n    type: http # http 的 path 可空置，默认储存路径为 homedir 的 rules 文件夹，文件名为 url 的 md5\n    url: 'url'\n    proxy: DIRECT\n    # size-limit: 10240 # 限制下载文件最大为10kb，默认为0即不限制文件大小\n  rule2:\n    behavior: classical\n    interval: 259200\n    path: /path/to/save/file.yaml\n    type: file\n  rule3:\n    # mrs类型ruleset，目前仅支持domain和ipcidr(即不支持classical），\n    #\n    # 对于behavior=domain:\n    #  - format=yaml 可以通过“mihomo convert-ruleset domain yaml XXX.yaml XXX.mrs”转换到mrs格式\n    #  - format=text 可以通过“mihomo convert-ruleset domain text XXX.text XXX.mrs”转换到mrs格式\n    #  - XXX.mrs 可以通过\"mihomo convert-ruleset domain mrs XXX.mrs XXX.text\"转换回text格式（暂不支持转换回yaml格式）\n    #\n    # 对于behavior=ipcidr:\n    #  - format=yaml 可以通过“mihomo convert-ruleset ipcidr yaml XXX.yaml XXX.mrs”转换到mrs格式\n    #  - format=text 可以通过“mihomo convert-ruleset ipcidr text XXX.text XXX.mrs”转换到mrs格式\n    #  - XXX.mrs 可以通过\"mihomo convert-ruleset ipcidr mrs XXX.mrs XXX.text\"转换回text格式（暂不支持转换回yaml格式）\n    #\n    type: http\n    url: 'url'\n    format: mrs\n    behavior: domain\n    path: /path/to/save/file.mrs\n  rule4:\n    type: inline\n    behavior: domain # classical / ipcidr\n    payload:\n      - '.blogger.com'\n      - '*.*.microsoft.com'\n      - 'books.itunes.apple.com'\n\nrules:\n  - RULE-SET,rule1,REJECT\n  - IP-ASN,1,PROXY\n  - DOMAIN-REGEX,^abc,DIRECT\n  - DOMAIN-SUFFIX,baidu.com,DIRECT\n  - DOMAIN-KEYWORD,google,ss1\n  - DOMAIN-WILDCARD,test.*.mihomo.com,ss1\n  - IP-CIDR,1.1.1.1/32,ss1\n  - IP-CIDR6,2409::/64,DIRECT\n  # 当满足条件是 TCP 或 UDP 流量时，使用名为 sub-rule-name1 的规则集\n  - SUB-RULE,(OR,((NETWORK,TCP),(NETWORK,UDP))),sub-rule-name1\n  - SUB-RULE,(AND,((NETWORK,UDP))),sub-rule-name2\n# 定义多个子规则集，规则将以分叉匹配，使用 SUB-RULE 使用\n#                                               google.com(not match)--> baidu.com(match)\n#                                                /                                ｜\n#                                               /                                 ｜\n#  https://baidu.com  --> rule1 --> rule2 --> sub-rule-name1(match tcp)          使用 DIRECT\n#\n#\n#                                              google.com(not match)--> baidu.com(not match)\n#                                                /                            ｜\n#                                               /                             ｜\n#  dns 1.1.1.1  --> rule1 --> rule2 --> sub-rule-name1(match udp)         sub-rule-name2(match udp)\n#                                                                             ｜\n#                                                                             ｜\n#                                                                 使用 REJECT <-- 1.1.1.1/32(match)\n#\n\nsub-rules:\n  sub-rule-name1:\n    - DOMAIN,google.com,ss1\n    - DOMAIN,baidu.com,DIRECT\n  sub-rule-name2:\n    - IP-CIDR,1.1.1.1/32,REJECT\n    - IP-CIDR,8.8.8.8/32,ss1\n    - DOMAIN,dns.alidns.com,REJECT\n\n# 流量入站\nlisteners:\n  - name: socks5-in-1\n    type: socks\n    port: 10808 # 支持使用ports格式，例如200,302 or 200,204,401-429,501-503\n    #listen: 0.0.0.0 # 默认监听 0.0.0.0\n    # rule: sub-rule-name1 # 默认使用 rules，如果未找到 sub-rule 则直接使用 rules\n    # proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理\n    # udp: false # 默认 true\n    # users: # 如果不填写users项，则遵从全局authentication设置，如果填写会忽略全局设置, 如想跳过该入站的验证可填写 users: []\n    #   - username: aaa\n    #     password: aaa\n    # 下面两项如果填写则开启 tls（需要同时填写）\n    # certificate: ./server.crt\n    # private-key: ./server.key\n    # 如果填写则开启ech（可由 mihomo generate ech-keypair <明文域名> 生成）\n    # ech-key: |\n    #   -----BEGIN ECH KEYS-----\n    #   ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK\n    #   madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz\n    #   dC5jb20AAA==\n    #   -----END ECH KEYS-----\n\n  - name: http-in-1\n    type: http\n    port: 10809 # 支持使用ports格式，例如200,302 or 200,204,401-429,501-503\n    listen: 0.0.0.0\n    # rule: sub-rule-name1 # 默认使用 rules，如果未找到 sub-rule 则直接使用 rules\n    # proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时，这里的 proxy 名称必须合法，否则会出错)\n    # users: # 如果不填写users项，则遵从全局authentication设置，如果填写会忽略全局设置, 如想跳过该入站的验证可填写 users: []\n    #   - username: aaa\n    #     password: aaa\n    # 下面两项如果填写则开启 tls（需要同时填写）\n    # certificate: ./server.crt\n    # private-key: ./server.key\n    # 如果填写则开启ech（可由 mihomo generate ech-keypair <明文域名> 生成）\n    # ech-key: |\n    #   -----BEGIN ECH KEYS-----\n    #   ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK\n    #   madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz\n    #   dC5jb20AAA==\n    #   -----END ECH KEYS-----\n\n  - name: mixed-in-1\n    type: mixed #  HTTP(S) 和 SOCKS 代理混合\n    port: 10810 # 支持使用ports格式，例如200,302 or 200,204,401-429,501-503\n    listen: 0.0.0.0\n    # rule: sub-rule-name1 # 默认使用 rules，如果未找到 sub-rule 则直接使用 rules\n    # proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时，这里的 proxy 名称必须合法，否则会出错)\n    # udp: false # 默认 true\n    # users: # 如果不填写users项，则遵从全局authentication设置，如果填写会忽略全局设置, 如想跳过该入站的验证可填写 users: []\n    #   - username: aaa\n    #     password: aaa\n    # 下面两项如果填写则开启 tls（需要同时填写）\n    # certificate: ./server.crt\n    # private-key: ./server.key\n    # 如果填写则开启ech（可由 mihomo generate ech-keypair <明文域名> 生成）\n    # ech-key: |\n    #   -----BEGIN ECH KEYS-----\n    #   ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK\n    #   madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz\n    #   dC5jb20AAA==\n    #   -----END ECH KEYS-----\n\n  - name: reidr-in-1\n    type: redir\n    port: 10811 # 支持使用ports格式，例如200,302 or 200,204,401-429,501-503\n    listen: 0.0.0.0\n    # rule: sub-rule-name1 # 默认使用 rules，如果未找到 sub-rule 则直接使用 rules\n    # proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时，这里的 proxy 名称必须合法，否则会出错)\n\n  - name: tproxy-in-1\n    type: tproxy\n    port: 10812 # 支持使用ports格式，例如200,302 or 200,204,401-429,501-503\n    listen: 0.0.0.0\n    # rule: sub-rule-name1 # 默认使用 rules，如果未找到 sub-rule 则直接使用 rules\n    # proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时，这里的 proxy 名称必须合法，否则会出错)\n    # udp: false # 默认 true\n\n  - name: shadowsocks-in-1\n    type: shadowsocks\n    port: 10813 # 支持使用ports格式，例如200,302 or 200,204,401-429,501-503\n    listen: 0.0.0.0\n    # rule: sub-rule-name1 # 默认使用 rules，如果未找到 sub-rule 则直接使用 rules\n    # proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时，这里的 proxy 名称必须合法，否则会出错)\n    password: vlmpIPSyHH6f4S8WVPdRIHIlzmB+GIRfoH3aNJ/t9Gg=\n    cipher: 2022-blake3-aes-256-gcm\n    # shadow-tls:\n    #   enable: false # 设置为true时开启\n    #   version: 3 # 支持v1/v2/v3\n    #   password: password # v2设置项\n    #   users: # v3设置项\n    #     - name: 1\n    #       password: password\n    #   handshake:\n    #     dest: test.com:443\n\n  - name: vmess-in-1\n    type: vmess\n    port: 10814 # 支持使用ports格式，例如200,302 or 200,204,401-429,501-503\n    listen: 0.0.0.0\n    # rule: sub-rule-name1 # 默认使用 rules，如果未找到 sub-rule 则直接使用 rules\n    # proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时，这里的 proxy 名称必须合法，否则会出错)\n    users:\n      - username: 1\n        uuid: 9d0cb9d0-964f-4ef6-897d-6c6b3ccf9e68\n        alterId: 1\n    # ws-path: \"/\" # 如果不为空则开启 websocket 传输层\n    # grpc-service-name: \"GunService\" # 如果不为空则开启 grpc 传输层\n    # 下面两项如果填写则开启 tls（需要同时填写）\n    # certificate: ./server.crt\n    # private-key: ./server.key\n    # 如果填写则开启ech（可由 mihomo generate ech-keypair <明文域名> 生成）\n    # ech-key: |\n    #   -----BEGIN ECH KEYS-----\n    #   ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK\n    #   madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz\n    #   dC5jb20AAA==\n    #   -----END ECH KEYS-----\n    # 如果填写reality-config则开启reality（注意不可与certificate和private-key同时填写）\n    # reality-config:\n    #   dest: test.com:443\n    #   private-key: jNXHt1yRo0vDuchQlIP6Z0ZvjT3KtzVI-T4E7RoLJS0 # 可由 mihomo generate reality-keypair 命令生成\n    #   short-id:\n    #     - 0123456789abcdef\n    #   server-names:\n    #     - test.com\n    #   #下列两个 limit 为选填，可对未通过验证的回落连接限速，bytesPerSec 默认为 0 即不启用\n    #   #回落限速是一种特征，不建议启用，如果您是面板/一键脚本开发者，务必让这些参数随机化\n    #   limit-fallback-upload:\n    #     after-bytes: 0 # 传输指定字节后开始限速\n    #     bytes-per-sec: 0 # 基准速率（字节/秒）\n    #     burst-bytes-per-sec: 0 # 突发速率（字节/秒），大于 bytesPerSec 时生效\n    #   limit-fallback-download:\n    #     after-bytes: 0 # 传输指定字节后开始限速\n    #     bytes-per-sec: 0 # 基准速率（字节/秒）\n    #     burst-bytes-per-sec: 0 # 突发速率（字节/秒），大于 bytesPerSec 时生效\n\n  - name: tuic-in-1\n    type: tuic\n    port: 10815 # 支持使用ports格式，例如200,302 or 200,204,401-429,501-503\n    listen: 0.0.0.0\n    # rule: sub-rule-name1 # 默认使用 rules，如果未找到 sub-rule 则直接使用 rules\n    # proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时，这里的 proxy 名称必须合法，否则会出错)\n    # token:    # tuicV4 填写（可以同时填写 users）\n    #   - TOKEN\n    # users:    # tuicV5 填写（可以同时填写 token）\n    #   00000000-0000-0000-0000-000000000000: PASSWORD_0\n    #   00000000-0000-0000-0000-000000000001: PASSWORD_1\n    #  certificate: ./server.crt\n    #  private-key: ./server.key\n    #  如果填写则开启ech（可由 mihomo generate ech-keypair <明文域名> 生成）\n    #  ech-key: |\n    #    -----BEGIN ECH KEYS-----\n    #    ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK\n    #    madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz\n    #    dC5jb20AAA==\n    #    -----END ECH KEYS-----\n    #  congestion-controller: bbr\n    #  max-idle-time: 15000\n    #  authentication-timeout: 1000\n    #  alpn:\n    #    - h3\n    #  max-udp-relay-packet-size: 1500\n\n  - name: tunnel-in-1\n    type: tunnel\n    port: 10816 # 支持使用ports格式，例如200,302 or 200,204,401-429,501-503\n    listen: 0.0.0.0\n    # rule: sub-rule-name1 # 默认使用 rules，如果未找到 sub-rule 则直接使用 rules\n    # proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时，这里的 proxy 名称必须合法，否则会出错)\n    network: [tcp, udp]\n    target: target.com\n\n  - name: vless-in-1\n    type: vless\n    port: 10817 # 支持使用ports格式，例如200,302 or 200,204,401-429,501-503\n    listen: 0.0.0.0\n    # rule: sub-rule-name1 # 默认使用 rules，如果未找到 sub-rule 则直接使用 rules\n    # proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时，这里的 proxy 名称必须合法，否则会出错)\n    users:\n      - username: 1\n        uuid: 9d0cb9d0-964f-4ef6-897d-6c6b3ccf9e68\n        flow: xtls-rprx-vision\n    # ws-path: \"/\" # 如果不为空则开启 websocket 传输层\n    # grpc-service-name: \"GunService\" # 如果不为空则开启 grpc 传输层\n    # -------------------------\n    # vless encryption服务端配置：\n    # （原生外观 / 只 XOR 公钥 / 全随机数。只允许 1-RTT 模式 / 同时允许 1-RTT 模式与 600 秒复用的 0-RTT 模式）\n    # / 是只能选一个，后面 base64 至少一个，无限串联，使用 mihomo generate vless-x25519 和 mihomo generate vless-mlkem768 生成，替换值时需去掉括号\n    # -------------------------\n    # decryption: \"mlkem768x25519plus.native/xorpub/random.1rtt/600s.(X25519 PrivateKey).(ML-KEM-768 Seed)...\"\n    # 下面两项如果填写则开启 tls（需要同时填写）\n    # certificate: ./server.crt\n    # private-key: ./server.key\n    # 如果填写则开启ech（可由 mihomo generate ech-keypair <明文域名> 生成）\n    # ech-key: |\n    #   -----BEGIN ECH KEYS-----\n    #   ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK\n    #   madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz\n    #   dC5jb20AAA==\n    #   -----END ECH KEYS-----\n    # 如果填写reality-config则开启reality（注意不可与certificate和private-key同时填写）\n    reality-config:\n      dest: test.com:443\n      private-key: jNXHt1yRo0vDuchQlIP6Z0ZvjT3KtzVI-T4E7RoLJS0 # 可由 mihomo generate reality-keypair 命令生成\n      short-id:\n        - 0123456789abcdef\n      server-names:\n        - test.com\n      #下列两个 limit 为选填，可对未通过验证的回落连接限速，bytesPerSec 默认为 0 即不启用\n      #回落限速是一种特征，不建议启用，如果您是面板/一键脚本开发者，务必让这些参数随机化\n      limit-fallback-upload:\n        after-bytes: 0 # 传输指定字节后开始限速\n        bytes-per-sec: 0 # 基准速率（字节/秒）\n        burst-bytes-per-sec: 0 # 突发速率（字节/秒），大于 bytesPerSec 时生效\n      limit-fallback-download:\n        after-bytes: 0 # 传输指定字节后开始限速\n        bytes-per-sec: 0 # 基准速率（字节/秒）\n        burst-bytes-per-sec: 0 # 突发速率（字节/秒），大于 bytesPerSec 时生效\n    ### 注意，对于vless listener, 至少需要填写 “certificate和private-key” 或 “reality-config” 或 “decryption” 的其中一项 ###\n\n  - name: anytls-in-1\n    type: anytls\n    port: 10818 # 支持使用ports格式，例如200,302 or 200,204,401-429,501-503\n    listen: 0.0.0.0\n    users:\n      username1: password1\n      username2: password2\n    # \"certificate\" and \"private-key\" are required\n    certificate: ./server.crt\n    private-key: ./server.key\n    # 如果填写则开启ech（可由 mihomo generate ech-keypair <明文域名> 生成）\n    # ech-key: |\n    #   -----BEGIN ECH KEYS-----\n    #   ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK\n    #   madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz\n    #   dC5jb20AAA==\n    #   -----END ECH KEYS-----\n    # padding-scheme: \"\" # https://github.com/anytls/anytls-go/blob/main/docs/protocol.md#cmdupdatepaddingscheme\n\n  - name: trojan-in-1\n    type: trojan\n    port: 10819 # 支持使用ports格式，例如200,302 or 200,204,401-429,501-503\n    listen: 0.0.0.0\n    # rule: sub-rule-name1 # 默认使用 rules，如果未找到 sub-rule 则直接使用 rules\n    # proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时，这里的 proxy 名称必须合法，否则会出错)\n    users:\n      - username: 1\n        password: 9d0cb9d0-964f-4ef6-897d-6c6b3ccf9e68\n    # ws-path: \"/\" # 如果不为空则开启 websocket 传输层\n    # grpc-service-name: \"GunService\" # 如果不为空则开启 grpc 传输层\n    # 下面两项如果填写则开启 tls（需要同时填写）\n    certificate: ./server.crt\n    private-key: ./server.key\n    # 如果填写则开启ech（可由 mihomo generate ech-keypair <明文域名> 生成）\n    # ech-key: |\n    #   -----BEGIN ECH KEYS-----\n    #   ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK\n    #   madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz\n    #   dC5jb20AAA==\n    #   -----END ECH KEYS-----\n    # 如果填写reality-config则开启reality（注意不可与certificate和private-key同时填写）\n    # reality-config:\n    #   dest: test.com:443\n    #   private-key: jNXHt1yRo0vDuchQlIP6Z0ZvjT3KtzVI-T4E7RoLJS0 # 可由 mihomo generate reality-keypair 命令生成\n    #   short-id:\n    #     - 0123456789abcdef\n    #   server-names:\n    #     - test.com\n    #   #下列两个 limit 为选填，可对未通过验证的回落连接限速，bytesPerSec 默认为 0 即不启用\n    #   #回落限速是一种特征，不建议启用，如果您是面板/一键脚本开发者，务必让这些参数随机化\n    #   limit-fallback-upload:\n    #     after-bytes: 0 # 传输指定字节后开始限速\n    #     bytes-per-sec: 0 # 基准速率（字节/秒）\n    #     burst-bytes-per-sec: 0 # 突发速率（字节/秒），大于 bytesPerSec 时生效\n    #   limit-fallback-download:\n    #     after-bytes: 0 # 传输指定字节后开始限速\n    #     bytes-per-sec: 0 # 基准速率（字节/秒）\n    #     burst-bytes-per-sec: 0 # 突发速率（字节/秒），大于 bytesPerSec 时生效\n    # ss-option: # like trojan-go's `shadowsocks` config\n    #   enabled: false\n    #   method: aes-128-gcm # aes-128-gcm/aes-256-gcm/chacha20-ietf-poly1305\n    #   password: \"example\"\n    ### 注意，对于trojan listener, 至少需要填写 “certificate和private-key” 或 “reality-config” 或 “ss-option” 的其中一项 ###\n\n  - name: hysteria2-in-1\n    type: hysteria2\n    port: 10820 # 支持使用ports格式，例如200,302 or 200,204,401-429,501-503\n    listen: 0.0.0.0\n    # rule: sub-rule-name1 # 默认使用 rules，如果未找到 sub-rule 则直接使用 rules\n    # proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时，这里的 proxy 名称必须合法，否则会出错)\n    users:\n      00000000-0000-0000-0000-000000000000: PASSWORD_0\n      00000000-0000-0000-0000-000000000001: PASSWORD_1\n    #  certificate: ./server.crt\n    #  private-key: ./server.key\n    #  如果填写则开启ech（可由 mihomo generate ech-keypair <明文域名> 生成）\n    #  ech-key: |\n    #    -----BEGIN ECH KEYS-----\n    #    ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK\n    #    madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz\n    #    dC5jb20AAA==\n    #    -----END ECH KEYS-----\n    ##  up 和 down 均不写或为 0 则使用 BBR 流控\n    #  up: \"30 Mbps\" # 若不写单位，默认为 Mbps\n    #  down: \"200 Mbps\" # 若不写单位，默认为 Mbps\n    #  obfs: salamander # 默认为空，如果填写则开启 obfs，目前仅支持 salamander\n    #  obfs-password: yourpassword\n    #  max-idle-time: 15000\n    #  alpn:\n    #    - h3\n    #  ignore-client-bandwidth: false\n    # HTTP3 服务器认证失败时的行为 （URL 字符串配置）,如果 masquerade 未配置，则返回 404 页\n    #  masquerade: file:///var/www # 作为文件服务器\n    #  masquerade: http://127.0.0.1:8080\t#作为反向代理\n    #  masquerade: https://127.0.0.1:8080\t#作为反向代理\n\n  # 注意，listeners中的tun仅提供给高级用户使用，普通用户应使用顶层配置中的tun\n  - name: tun-in-1\n    type: tun\n    # rule: sub-rule-name1 # 默认使用 rules，如果未找到 sub-rule 则直接使用 rules\n    # proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时，这里的 proxy 名称必须合法，否则会出错)\n    stack: system # gvisor / mixed\n    dns-hijack:\n      - 0.0.0.0:53 # 需要劫持的 DNS\n    # auto-detect-interface: false # 自动识别出口网卡\n    # auto-route: false # 配置路由表\n    # mtu: 9000 # 最大传输单元\n    inet4-address: # 必须手动设置 ipv4 地址段\n      - 198.19.0.1/30\n    inet6-address: # 必须手动设置 ipv6 地址段\n      - 'fdfe:dcba:9877::1/126'\n    # strict-route: true # 将所有连接路由到 tun 来防止泄漏，但你的设备将无法其他设备被访问\n    # inet4-route-address: # 启用 auto-route 时使用自定义路由而不是默认路由\n    # - 0.0.0.0/1\n    # - 128.0.0.0/1\n    # inet6-route-address: # 启用 auto-route 时使用自定义路由而不是默认路由\n    # - \"::/1\"\n    # - \"8000::/1\"\n    # endpoint-independent-nat: false # 启用独立于端点的 NAT\n    # include-uid: # UID 规则仅在 Linux 下被支持，并且需要 auto-route\n    # - 0\n    # include-uid-range: # 限制被路由的的用户范围\n    # - 1000:99999\n    # exclude-uid: # 排除路由的的用户\n    # - 1000\n    # exclude-uid-range: # 排除路由的的用户范围\n    # - 1000:99999\n\n    # Android 用户和应用规则仅在 Android 下被支持\n    # 并且需要 auto-route\n\n    # include-android-user: # 限制被路由的 Android 用户\n    # - 0\n    # - 10\n    # include-package: # 限制被路由的 Android 应用包名\n    # - com.android.chrome\n    # exclude-package: # 排除被路由的 Android 应用包名\n    # - com.android.captiveportallogin\n# 入口配置与 Listener 等价，传入流量将和 socks,mixed 等入口一样按照 mode 所指定的方式进行匹配处理\n# shadowsocks,vmess 入口配置（传入流量将和 socks,mixed 等入口一样按照 mode 所指定的方式进行匹配处理）\n# ss-config: ss://2022-blake3-aes-256-gcm:vlmpIPSyHH6f4S8WVPdRIHIlzmB+GIRfoH3aNJ/t9Gg=@:23456\n# vmess-config: vmess://1:9d0cb9d0-964f-4ef6-897d-6c6b3ccf9e68@:12345\n\n# tuic 服务器入口（传入流量将和 socks,mixed 等入口一样按照 mode 所指定的方式进行匹配处理）\n# tuic-server:\n#  enable: true\n#  listen: 127.0.0.1:10443\n#  token:    # tuicV4 填写（可以同时填写 users）\n#    - TOKEN\n#  users:    # tuicV5 填写（可以同时填写 token）\n#    00000000-0000-0000-0000-000000000000: PASSWORD_0\n#    00000000-0000-0000-0000-000000000001: PASSWORD_1\n#  certificate: ./server.crt\n#  private-key: ./server.key\n#  congestion-controller: bbr\n#  max-idle-time: 15000\n#  authentication-timeout: 1000\n#  alpn:\n#    - h3\n#  max-udp-relay-packet-size: 1500\n"
  },
  {
    "path": "backend/tauri-plugin-deep-link/.github/workflows/audit.yml",
    "content": "name: Audit\n\non:\n  schedule:\n    - cron: '0 0 * * *'\n  push:\n    branches:\n      - main\n    paths:\n      - '**/Cargo.lock'\n      - '**/Cargo.toml'\n  pull_request:\n    branches:\n      - main\n    paths:\n      - '**/Cargo.lock'\n      - '**/Cargo.toml'\n\njobs:\n  audit:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v6\n      - uses: actions-rs/audit-check@v1\n        with:\n          token: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": "backend/tauri-plugin-deep-link/.github/workflows/format.yml",
    "content": "name: Format\n\non:\n  push:\n    branches:\n      - main\n  pull_request:\n    branches:\n      - main\n      - dev\n\njobs:\n  format:\n    runs-on: ubuntu-latest\n    strategy:\n      fail-fast: false\n\n    steps:\n      - uses: actions/checkout@v6\n      - uses: dtolnay/rust-toolchain@stable\n        with:\n          components: rustfmt\n      - uses: Swatinem/rust-cache@v2\n      - run: cargo fmt --all -- --check\n"
  },
  {
    "path": "backend/tauri-plugin-deep-link/.github/workflows/lint.yml",
    "content": "name: Clippy\n\non:\n  push:\n    branches:\n      - main\n  pull_request:\n    branches:\n      - main\n      - dev\n\njobs:\n  clippy:\n    strategy:\n      fail-fast: false\n      matrix:\n        platform: [ubuntu-latest, windows-latest, macos-latest]\n\n    runs-on: ${{ matrix.platform }}\n\n    steps:\n      - uses: actions/checkout@v6\n      - uses: dtolnay/rust-toolchain@stable\n        with:\n          components: clippy\n      - uses: Swatinem/rust-cache@v2\n      - run: cargo clippy --all-targets --all-features -- -D warnings\n"
  },
  {
    "path": "backend/tauri-plugin-deep-link/.github/workflows/release.yml",
    "content": "name: Publish\n\non:\n  push:\n    tags:\n      - 'v*.*.*'\n\njobs:\n  release:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6\n        with:\n          fetch-depth: 0\n\n      - name: Release to crates.io\n        run: |\n          cargo login ${{ secrets.CRATES_IO }}\n          cargo publish\n\n      - name: Generate Changelog\n        uses: orhun/git-cliff-action@v4\n        id: git-cliff\n        with:\n          config: cliff.toml\n          args: -vv --latest --strip header\n\n      - name: Create GitHub Release\n        uses: softprops/action-gh-release@v2\n        with:\n          body: ${{ steps.git-cliff.outputs.content }}\n"
  },
  {
    "path": "backend/tauri-plugin-deep-link/.gitignore",
    "content": "/target\n/Cargo.lock\n"
  },
  {
    "path": "backend/tauri-plugin-deep-link/CHANGELOG.md",
    "content": "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\n## [0.1.2] - 2023-08-15\n\n**This plugin will be migrated to https://github.com/tauri-apps/plugins-workspace/.** Therefore, this will likely be the last release in this repository.\n\n### Miscellaneous Tasks\n\n- Update rust crate objc2 to 0.4.0 (#30)\n- Update rust crate objc2 to 0.4.1 (#31)\n\n## [0.1.1] - 2023-04-04\n\n### Bug Fixes\n\n- Info.plist formatting (#22)\n- Fixed inability to focus when launched from a Windows notification. (#27)\n\n### Documentation\n\n- Add env::args getter to example\n\n### Miscellaneous Tasks\n\n- Update rust crate winreg to 0.50.0 (#28)\n- Switch from dirs-next to dirs\n\n## [0.1.0] - 2023-02-27\n\n### Features\n\n- Initial release\n"
  },
  {
    "path": "backend/tauri-plugin-deep-link/Cargo.toml",
    "content": "[package]\nname = \"tauri-plugin-deep-link\"\nversion = \"0.1.2\"\nauthors = [\"FabianLars <fabianlars@fabianlars.de>\"]\ndescription = \"A Tauri plugin for deep linking support\"\nrepository = \"https://github.com/FabianLars/tauri-plugin-deep-link\"\nedition = \"2021\"\nrust-version = \"1.64\"\nlicense = \"MIT OR Apache-2.0\"\nreadme = \"README.md\"\ninclude = [\"src/**\", \"Cargo.toml\", \"LICENSE_*\"]\n\n[lib]\ndoctest = false\n\n[dependencies]\ndirs = \"6\"\nlog = \"0.4\"\nonce_cell = \"1\"\ntauri-utils = { version = \"2\" }\n\n[target.'cfg(windows)'.dependencies]\ninterprocess = { version = \"2\", features = [\"tokio\"] }\nwindows-sys = { version = \"0.60\", features = [\n  \"Win32_Foundation\",\n  \"Win32_UI_Input_KeyboardAndMouse\",\n  \"Win32_UI_WindowsAndMessaging\",\n] }\nwinreg = \"0.55.0\"\ntokio = { workspace = true }\n\n[target.'cfg(target_os = \"macos\")'.dependencies]\nobjc2 = \"0.6.1\"\n"
  },
  {
    "path": "backend/tauri-plugin-deep-link/LICENSE_APACHE-2.0",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS"
  },
  {
    "path": "backend/tauri-plugin-deep-link/LICENSE_MIT",
    "content": "MIT License\n\nCopyright (c) 2022 - Present FabianLars\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "backend/tauri-plugin-deep-link/README.md",
    "content": "# Deep link plugin for Tauri\n\n[![](https://img.shields.io/crates/v/tauri-plugin-deep-link.svg)](https://crates.io/crates/tauri-plugin-deep-link) [![](https://img.shields.io/docsrs/tauri-plugin-deep-link)](https://docs.rs/tauri-plugin-deep-link)\n\n**This plugin will be migrated to https://github.com/tauri-apps/plugins-workspace/.** `0.1.2` will be the last release in this repo.\n\n~~Temporary solution until https://github.com/tauri-apps/tauri/issues/323 lands.~~\n\nDepending on your use case, for example a `Login with Google` button, you may want to take a look at https://github.com/FabianLars/tauri-plugin-oauth instead. It uses a minimalistic localhost server for the OAuth process instead of custom uri schemes because some oauth providers, like the aforementioned Google, require this setup. Personally, I think it's easier to use too.\n\nCheck out the [`example/`](https://github.com/FabianLars/tauri-plugin-deep-link/tree/main/example) directory for a minimal example. You must copy it into an actual tauri app first!\n\n## macOS\n\nIn case you're one of the very few people that didn't know this already: macOS hates developers! Not only is that why the macOS implementation took me so long, it also means _you_ have to be a bit more careful if your app targets macOS:\n\n- Read through the methods' platform-specific notes.\n- On macOS you need to register the schemes in a `Info.plist` file at build time, the plugin can't change the schemes at runtime.\n- macOS apps are in single-instance by default so this plugin will not manually shut down secondary instances in release mode.\n  - To make development via `tauri dev` a little bit more pleasant, the plugin will work similar-ish to Linux and Windows _in debug mode_ but you will see secondary instances show on the screen for a split second and the event will trigger twice in the primary instance (one of these events will be an empty string). You still have to install a `.app` bundle you got from `tauri build --debug` for this to work!\n"
  },
  {
    "path": "backend/tauri-plugin-deep-link/cliff.toml",
    "content": "# configuration file for git-cliff\n# see https://github.com/orhun/git-cliff#configuration-file\n\n[changelog]\n# changelog header\nheader = \"\"\"\n# Changelog\\n\nAll notable changes to this project will be documented in this file.\\n\n\"\"\"\n# template for the changelog body\n# https://tera.netlify.app/docs/#introduction\nbody = \"\"\"\n{% if version %}\\\n    ## [{{ version | trim_start_matches(pat=\"v\") }}] - {{ timestamp | date(format=\"%Y-%m-%d\") }}\n{% else %}\\\n    ## [unreleased]\n{% endif %}\\\n{% for group, commits in commits | group_by(attribute=\"group\") %}\n    ### {{ group | upper_first }}\n    {% for commit in commits %}\n        - {% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message | upper_first }}\\\n    {% endfor %}\n{% endfor %}\\n\n\"\"\"\n# remove the leading and trailing whitespace from the template\ntrim = true\n# changelog footer\nfooter = \"\"\n\n[git]\n# parse the commits based on https://www.conventionalcommits.org\nconventional_commits = true\n# filter out the commits that are not conventional\nfilter_unconventional = true\n# process each line of a commit as an individual commit\nsplit_commits = false\n# regex for preprocessing the commit messages\ncommit_preprocessors = [\n  # { pattern = '\\((\\w+\\s)?#([0-9]+)\\)', replace = \"([#${2}](https://github.com/orhun/git-cliff/issues/${2}))\"}, # replace issue numbers\n\n]\n# regex for parsing and grouping commits\ncommit_parsers = [\n  { message = \"^feat\", group = \"Features\" },\n  { message = \"^fix\", group = \"Bug Fixes\" },\n  { message = \"^doc\", group = \"Documentation\" },\n  { message = \"^perf\", group = \"Performance\" },\n  { message = \"^refactor\", group = \"Refactor\" },\n  { message = \"^style\", group = \"Styling\" },\n  { message = \"^test\", group = \"Testing\" },\n  { message = \"^chore\\\\(release\\\\): [Pp]repare for\", skip = true },\n  { message = \"^chore\", group = \"Miscellaneous Tasks\" },\n  { message = \"^ci\", group = \"CI\", skip = true },\n  { body = \".*security\", group = \"Security\" },\n]\n# protect breaking changes from being skipped due to matching a skipping commit_parser\nprotect_breaking_commits = false\n# filter out the commits that are not matched by commit parsers\nfilter_commits = false\n# glob pattern for matching git tags\ntag_pattern = \"v[0-9]*\"\n# regex for skipping tags\n# skip_tags = \"v0.1.0-beta.1\"\n# regex for ignoring tags\nignore_tags = \"\"\n# sort the tags topologically\ntopo_order = false\n# sort the commits inside sections by oldest/newest order\nsort_commits = \"oldest\"\n# limit the number of commits included in the changelog.\n# limit_commits = 42\n"
  },
  {
    "path": "backend/tauri-plugin-deep-link/example/Info.plist",
    "content": "<!-- Add this file next to your tauri.conf.json file -->\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n    <dict>\n        <key>CFBundleURLTypes</key>\n        <array>\n            <dict>\n                <key>CFBundleURLName</key>\n                <!-- Obviously needs to be replaced with your app's bundle identifier -->\n                <string>de.fabianlars.deep-link-test</string>\n                <key>CFBundleURLSchemes</key>\n                <array>\n                    <!-- register the myapp:// and myscheme:// schemes -->\n                    <string>myapp</string>\n                    <string>myscheme</string>\n                </array>\n            </dict>\n        </array>\n    </dict>\n</plist>\n"
  },
  {
    "path": "backend/tauri-plugin-deep-link/example/main.rs",
    "content": "#![cfg_attr(not(debug_assertions), windows_subsystem = \"windows\")]\n\nuse tauri::Manager;\n\nfn main() {\n    // prepare() checks if it's a single instance and tries to send the args otherwise.\n    // It should always be the first line in your main function (with the exception of loggers or similar)\n    tauri_plugin_deep_link::prepare(\"de.fabianlars.deep-link-test\");\n    // It's expected to use the identifier from tauri.conf.json\n    // Unfortuenetly getting it is pretty ugly without access to sth that implements `Manager`.\n\n    tauri::Builder::default()\n    .setup(|app| {\n      // If you need macOS support this must be called in .setup() !\n      // Otherwise this could be called right after prepare() but then you don't have access to tauri APIs\n      let handle = app.handle();\n      tauri_plugin_deep_link::register(\n        \"my-scheme\",\n        move |request| {\n          dbg!(&request);\n          handle.emit_all(\"scheme-request-received\", request).unwrap();\n        },\n      )\n      .unwrap(/* If listening to the scheme is optional for your app, you don't want to unwrap here. */);\n        \n      // If you also need the url when the primary instance was started by the custom scheme, you currently have to read it yourself\n      /*\n      #[cfg(not(target_os = \"macos\"))] // on macos the plugin handles this (macos doesn't use cli args for the url)\n      if let Some(url) = std::env::args().nth(1) {\n        app.emit_all(\"scheme-request-received\", url).unwrap();\n      }\n      */\n\n      Ok(())\n    })\n    // .plugin(tauri_plugin_deep_link::init()) // consider adding a js api later\n    .run(tauri::generate_context!())\n    .expect(\"error while running tauri application\");\n}\n"
  },
  {
    "path": "backend/tauri-plugin-deep-link/renovate.json",
    "content": "{\n  \"extends\": [\"config:base\", \":semanticCommitTypeAll(chore)\"]\n}\n"
  },
  {
    "path": "backend/tauri-plugin-deep-link/src/lib.rs",
    "content": "use std::io::{ErrorKind, Result};\n\nuse once_cell::sync::OnceCell;\n\n#[cfg(target_os = \"windows\")]\n#[path = \"windows.rs\"]\nmod platform_impl;\n#[cfg(target_os = \"linux\")]\n#[path = \"linux.rs\"]\nmod platform_impl;\n#[cfg(target_os = \"macos\")]\n#[path = \"macos.rs\"]\nmod platform_impl;\n\nstatic ID: OnceCell<String> = OnceCell::new();\n\n/// This function is meant for use-cases where the default [`prepare()`] function can't be used.\n///\n/// # Errors\n/// If ID was already set this functions returns an AlreadyExists error.\npub fn set_identifier(identifier: &str) -> Result<()> {\n    ID.set(identifier.to_string())\n        .map_err(|_| ErrorKind::AlreadyExists.into())\n}\n\n// Consider adding a function to register without starting the listener.\n\n/// Registers a handler for the given scheme.\n///\n/// ## Platform-specific:\n///\n/// - **macOS**: On macOS schemes must be defined in an Info.plist file, therefore this function only calls [`listen()`] without registering the scheme. This function can only be called once on macOS.\npub fn register<F: FnMut(String) + Send + 'static>(scheme: &[&str], handler: F) -> Result<()> {\n    platform_impl::register(scheme, handler)\n}\n\n/// Starts the event listener without registering any schemes.\n///\n/// ## Platform-specific:\n///\n/// - **macOS**: This function can only be called once on macOS.\npub fn listen<F: FnMut(String) + Send + 'static>(handler: F) -> Result<()> {\n    platform_impl::listen(handler)\n}\n\n/// Unregister a previously registered scheme.\n///\n/// ## Platform-specific:\n///\n/// - **macOS**: This function has no effect on macOS.\npub fn unregister(scheme: &[&str]) -> Result<()> {\n    platform_impl::unregister(scheme)\n}\n\n/// Checks if current instance is the primary instance.\n/// Also sends the URL event data to the primary instance and stops the process afterwards.\n///\n/// ## Platform-specific:\n///\n/// - **macOS**: Only registers the identifier (only relevant in debug mode). It does not interact with the primary instance and does not exit the app.\npub fn prepare(identifier: &str) {\n    platform_impl::prepare(identifier)\n}\n"
  },
  {
    "path": "backend/tauri-plugin-deep-link/src/linux.rs",
    "content": "use std::{\n    fs::{create_dir_all, remove_file, File},\n    io::{Error, ErrorKind, Read, Result, Write},\n    os::unix::net::{UnixListener, UnixStream},\n    process::Command,\n};\n\nuse dirs::data_dir;\n\nuse crate::ID;\n\npub fn register<F: FnMut(String) + Send + 'static>(schemes: &[&str], handler: F) -> Result<()> {\n    listen(handler)?;\n\n    let mut target = data_dir()\n        .ok_or_else(|| Error::new(ErrorKind::NotFound, \"data directory not found.\"))?\n        .join(\"applications\");\n\n    create_dir_all(&target)?;\n\n    let exe = tauri_utils::platform::current_exe()?;\n\n    let file_name = format!(\n        \"{}-handler.desktop\",\n        exe.file_name()\n            .ok_or_else(|| Error::new(\n                ErrorKind::NotFound,\n                \"Couldn't get file name of curent executable.\",\n            ))?\n            .to_string_lossy()\n    );\n\n    target.push(&file_name);\n\n    let mime_types = format!(\n        \"{};\",\n        schemes\n            .iter()\n            .map(|s| format!(\"x-scheme-handler/{}\", s))\n            .collect::<Vec<String>>()\n            .join(\";\")\n    );\n\n    let mut file = File::create(&target)?;\n    file.write_all(\n        format!(\n            include_str!(\"template.desktop\"),\n            name = ID\n                .get()\n                .expect(\"Called register() before prepare()\")\n                .split('.')\n                .last()\n                .unwrap(),\n            exec = std::env::var(\"APPIMAGE\").unwrap_or_else(|_| exe.display().to_string()),\n            mime_types = mime_types\n        )\n        .as_bytes(),\n    )?;\n\n    // update-desktop-database [-q|--quiet] [-v|--verbose] [DIRECTORY...]\n    target.pop();\n\n    Command::new(\"update-desktop-database\")\n        .arg(&target)\n        .status()?;\n\n    for scheme in schemes {\n        Command::new(\"xdg-mime\")\n            .args([\n                \"default\",\n                &file_name,\n                &format!(\"x-scheme-handler/{}\", scheme),\n            ])\n            .status()?;\n    }\n\n    Ok(())\n}\n\npub fn unregister(_schemes: &[&str]) -> Result<()> {\n    let mut target =\n        data_dir().ok_or_else(|| Error::new(ErrorKind::NotFound, \"data directory not found.\"))?;\n\n    target.push(\"applications\");\n\n    target.push(format!(\n        \"{}-handler.desktop\",\n        tauri_utils::platform::current_exe()?\n            .file_name()\n            .ok_or_else(|| Error::new(\n                ErrorKind::NotFound,\n                \"Couldn't get file name of current executable.\",\n            ))?\n            .to_string_lossy()\n    ));\n\n    remove_file(&target)?;\n    target.pop();\n\n    Ok(())\n}\n\npub fn listen<F: FnMut(String) + Send + 'static>(mut handler: F) -> Result<()> {\n    std::thread::spawn(move || {\n        let addr = format!(\n            \"/tmp/{}-deep-link.sock\",\n            ID.get().expect(\"listen() called before prepare()\")\n        );\n\n        let listener = UnixListener::bind(addr).expect(\"Can't create listener\");\n\n        for stream in listener.incoming() {\n            match stream {\n                Ok(mut stream) => {\n                    let mut buffer = String::new();\n                    if let Err(io_err) = stream.read_to_string(&mut buffer) {\n                        log::error!(\"Error reading incoming connection: {}\", io_err.to_string());\n                    };\n\n                    handler(dbg!(buffer));\n                }\n                Err(err) => {\n                    log::error!(\"Incoming connection failed: {}\", err);\n                    continue;\n                }\n            }\n        }\n    });\n\n    Ok(())\n}\n\npub fn prepare(identifier: &str) {\n    let addr = format!(\"/tmp/{}-deep-link.sock\", identifier);\n\n    match UnixStream::connect(&addr) {\n        Ok(mut stream) => {\n            if let Err(io_err) =\n                stream.write_all(std::env::args().nth(1).unwrap_or_default().as_bytes())\n            {\n                log::error!(\n                    \"Error sending message to primary instance: {}\",\n                    io_err.to_string()\n                );\n            };\n            std::process::exit(0);\n        }\n        Err(err) => {\n            log::error!(\"Error creating socket listener: {}\", err.to_string());\n            if err.kind() == ErrorKind::ConnectionRefused {\n                let _ = remove_file(&addr);\n            }\n        }\n    };\n    ID.set(identifier.to_string())\n        .expect(\"prepare() called more than once with different identifiers.\");\n}\n"
  },
  {
    "path": "backend/tauri-plugin-deep-link/src/macos.rs",
    "content": "use std::{\n    fs::remove_file,\n    io::{ErrorKind, Read, Result, Write},\n    os::unix::net::{UnixListener, UnixStream},\n    sync::Mutex,\n};\n\nuse objc2::{\n    class, define_class, msg_send, msg_send_id,\n    rc::Retained,\n    runtime::{AnyObject, NSObject},\n    sel, ClassType,\n};\nuse once_cell::sync::OnceCell;\n\nuse crate::ID;\n\ntype THandler = OnceCell<Mutex<Box<dyn FnMut(String) + Send + 'static>>>;\n\n// If the Mutex turns out to be a problem, or FnMut turns out to be useless, we can remove the Mutex and turn FnMut into Fn\nstatic HANDLER: THandler = OnceCell::new();\n\npub fn register<F: FnMut(String) + Send + 'static>(_scheme: &[&str], handler: F) -> Result<()> {\n    listen(handler)?;\n\n    Ok(())\n}\n\npub fn unregister(_scheme: &[&str]) -> Result<()> {\n    Ok(())\n}\n\n// kInternetEventClass\nconst EVENT_CLASS: u32 = 0x4755524c;\n// kAEGetURL\nconst EVENT_GET_URL: u32 = 0x4755524c;\n\n// Adapted from https://github.com/mrmekon/fruitbasket/blob/aad14e400d710d1d46317c0d8c55ff742bfeaadd/src/osx.rs#L848\nfn parse_url_event(event: *mut AnyObject) -> Option<String> {\n    if event as u64 == 0u64 {\n        return None;\n    }\n    unsafe {\n        let class: u32 = msg_send![event, eventClass];\n        let id: u32 = msg_send![event, eventID];\n        if class != EVENT_CLASS || id != EVENT_GET_URL {\n            return None;\n        }\n\n        let subevent: *mut AnyObject = msg_send![event, paramDescriptorForKeyword: 0x2d2d2d2d_u32];\n        let nsstring: *mut AnyObject = msg_send![subevent, stringValue];\n        let cstr: *const i8 = msg_send![nsstring, UTF8String];\n        if !cstr.is_null() {\n            Some(std::ffi::CStr::from_ptr(cstr).to_string_lossy().to_string())\n        } else {\n            None\n        }\n    }\n}\n\ndefine_class!(\n    #[unsafe(super(NSObject))]\n    #[name = \"TauriPluginDeepLinkHandler\"]\n    struct Handler;\n\n    impl Handler {\n        #[unsafe(method(handleEvent:withReplyEvent:))]\n        fn handle_event(&self, event: *mut AnyObject, _replace: *const AnyObject) {\n            let s = parse_url_event(event).unwrap_or_default();\n            let mut cb = HANDLER.get().unwrap().lock().unwrap();\n            cb(s);\n        }\n    }\n);\n\nimpl Handler {\n    pub fn new() -> Retained<Self> {\n        let cls = Self::class();\n        unsafe { msg_send_id![msg_send_id![cls, alloc], init] }\n    }\n}\n\n#[cfg(debug_assertions)]\nfn secondary_handler(s: String) {\n    let addr = format!(\n        \"/tmp/{}-deep-link.sock\",\n        ID.get()\n            .expect(\"URL event received before prepare() was called\")\n    );\n    if let Ok(mut stream) = UnixStream::connect(addr) {\n        if let Err(io_err) = stream.write_all(s.as_bytes()) {\n            log::error!(\n                \"Error sending message to primary instance: {}\",\n                io_err.to_string()\n            );\n        };\n    }\n    std::process::exit(0);\n}\n\npub fn listen<F: FnMut(String) + Send + 'static>(handler: F) -> Result<()> {\n    #[cfg(debug_assertions)]\n    let addr = format!(\n        \"/tmp/{}-deep-link.sock\",\n        ID.get().expect(\"listen() called before prepare()\")\n    );\n\n    #[cfg(debug_assertions)]\n    if HANDLER\n        .set(match UnixStream::connect(&addr) {\n            Ok(_) => Mutex::new(Box::new(secondary_handler)),\n            Err(err) => {\n                log::error!(\"Error creating socket listener: {}\", err.to_string());\n                if err.kind() == ErrorKind::ConnectionRefused {\n                    let _ = remove_file(&addr);\n                }\n                Mutex::new(Box::new(handler))\n            }\n        })\n        .is_err()\n    {\n        return Err(std::io::Error::new(\n            ErrorKind::AlreadyExists,\n            \"Handler was already set\",\n        ));\n    }\n\n    #[cfg(not(debug_assertions))]\n    if HANDLER.set(Mutex::new(Box::new(handler))).is_err() {\n        return Err(std::io::Error::new(\n            ErrorKind::AlreadyExists,\n            \"Handler was already set\",\n        ));\n    }\n\n    unsafe {\n        let event_manager: Retained<AnyObject> =\n            msg_send_id![class!(NSAppleEventManager), sharedAppleEventManager];\n\n        let handler = Handler::new();\n        let handler_boxed = Box::into_raw(Box::new(handler));\n\n        let _: () = msg_send![&event_manager,\n            setEventHandler: &**handler_boxed\n            andSelector: sel!(handleEvent:withReplyEvent:)\n            forEventClass:EVENT_CLASS\n            andEventID:EVENT_GET_URL];\n    }\n\n    #[cfg(debug_assertions)]\n    std::thread::spawn(move || {\n        let listener = UnixListener::bind(addr).expect(\"Can't create listener\");\n\n        for stream in listener.incoming() {\n            match stream {\n                Ok(mut stream) => {\n                    let mut buffer = String::new();\n                    if let Err(io_err) = stream.read_to_string(&mut buffer) {\n                        log::error!(\"Error reading incoming connection: {}\", io_err.to_string());\n                    };\n\n                    let mut cb = HANDLER.get().unwrap().lock().unwrap();\n                    cb(buffer);\n                }\n                Err(err) => {\n                    log::error!(\"Incoming connection failed: {}\", err);\n                    continue;\n                }\n            }\n        }\n    });\n\n    Ok(())\n}\n\npub fn prepare(identifier: &str) {\n    ID.set(identifier.to_string())\n        .expect(\"prepare() called more than once with different identifiers.\");\n}\n"
  },
  {
    "path": "backend/tauri-plugin-deep-link/src/template.desktop",
    "content": "[Desktop Entry]\nType=Application\nName={name}\nExec={exec} %u\nTerminal=false\nMimeType={mime_types}\nNoDisplay=true"
  },
  {
    "path": "backend/tauri-plugin-deep-link/src/windows.rs",
    "content": "use std::{\n    path::Path,\n    sync::atomic::{AtomicU16, Ordering},\n};\n\nuse interprocess::{\n    bound_util::RefTokioAsyncRead,\n    local_socket::{\n        tokio::prelude::*,\n        traits::tokio::{Listener, Stream},\n        GenericNamespaced, ListenerNonblockingMode, ListenerOptions, Name, ToNsName,\n    },\n    os::windows::{\n        local_socket::ListenerOptionsExt, security_descriptor::SecurityDescriptor, ToWtf16,\n    },\n};\nuse std::io::Result;\nuse tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};\nuse windows_sys::Win32::UI::{\n    Input::KeyboardAndMouse::{SendInput, INPUT, INPUT_0, INPUT_KEYBOARD, KEYBDINPUT},\n    WindowsAndMessaging::{AllowSetForegroundWindow, ASFW_ANY},\n    // WindowsAndMessaging::{AllowSetForegroundWindow, ASFW_ANY},\n};\nuse winreg::{enums::HKEY_CURRENT_USER, RegKey};\n\nuse crate::ID;\n\npub fn register<F: FnMut(String) + Send + 'static>(schemes: &[&str], handler: F) -> Result<()> {\n    listen(handler)?;\n\n    for scheme in schemes {\n        let hkcu = RegKey::predef(HKEY_CURRENT_USER);\n        let base = Path::new(\"Software\").join(\"Classes\").join(scheme);\n\n        let exe = tauri_utils::platform::current_exe()?\n            .display()\n            .to_string()\n            .replace(\"\\\\\\\\?\\\\\", \"\");\n\n        let (key, _) = hkcu.create_subkey(&base)?;\n        key.set_value(\n            \"\",\n            &format!(\n                \"URL:{}\",\n                ID.get().expect(\"register() called before prepare()\")\n            ),\n        )?;\n        key.set_value(\"URL Protocol\", &\"\")?;\n\n        let (icon, _) = hkcu.create_subkey(base.join(\"DefaultIcon\"))?;\n        icon.set_value(\"\", &format!(\"\\\"{}\\\",0\", &exe))?;\n\n        let (cmd, _) = hkcu.create_subkey(base.join(\"shell\").join(\"open\").join(\"command\"))?;\n\n        cmd.set_value(\"\", &format!(\"\\\"{}\\\" \\\"%1\\\"\", &exe))?;\n    }\n\n    Ok(())\n}\n\npub fn unregister(schemes: &[&str]) -> Result<()> {\n    for scheme in schemes {\n        let hkcu = RegKey::predef(HKEY_CURRENT_USER);\n        let base = Path::new(\"Software\").join(\"Classes\").join(scheme);\n\n        hkcu.delete_subkey_all(base)?;\n    }\n\n    Ok(())\n}\n\nstatic CRASH_COUNT: AtomicU16 = AtomicU16::new(0);\n\npub fn listen<F: FnMut(String) + Send + 'static>(mut handler: F) -> Result<()> {\n    if CRASH_COUNT.load(Ordering::Acquire) > 5 {\n        panic!(\"Local socket too many crashes\");\n    }\n\n    std::thread::spawn(move || {\n        let name = ID\n            .get()\n            .expect(\"listen() called before prepare()\")\n            .as_str()\n            .to_ns_name::<GenericNamespaced>()\n            .unwrap();\n        tokio::runtime::Builder::new_current_thread()\n            .enable_all()\n            .build()\n            .expect(\"failed to create tokio runtime\")\n            .block_on(async move {\n                let sdsf = \"D:(A;;GA;;;WD)\".to_wtf_16().unwrap();\n                let sd = SecurityDescriptor::deserialize(&sdsf).expect(\"Failed to deserialize SD\");\n                let listener = ListenerOptions::new()\n                    .name(name)\n                    .nonblocking(ListenerNonblockingMode::Both)\n                    .security_descriptor(sd)\n                    .create_tokio()\n                    .expect(\"Can't create listener\");\n\n                loop {\n                    match listener.accept().await {\n                        Ok(conn) => {\n                            let (rx, mut tx) = conn.split();\n                            let mut reader = BufReader::new(rx);\n                            let mut buf = String::new();\n                            if let Err(e) = reader.read_line(&mut buf).await {\n                                log::error!(\"Error reading from connection: {e}\");\n                                continue;\n                            }\n                            buf.pop();\n                            let current_pid = std::process::id();\n                            let response = format!(\"{current_pid}\\n\");\n                            if let Err(e) = tx.write_all(response.as_bytes()).await {\n                                log::error!(\"Error writing to connection: {e}\");\n                                continue;\n                            }\n                            handler(buf);\n                        }\n                        Err(e) if e.raw_os_error() == Some(232) => {\n                            // 234 is WSAEINTR, which means the listener was closed.\n                            break;\n                        }\n                        Err(e) => {\n                            log::error!(\"Error accepting connection: {e}\");\n                        }\n                    }\n                }\n                CRASH_COUNT.fetch_add(1, Ordering::Release);\n                let _ = listen(handler);\n            });\n    });\n\n    Ok(())\n}\n\n#[inline(never)]\npub fn prepare(identifier: &str) {\n    let name: Name = identifier\n        .to_ns_name::<GenericNamespaced>()\n        .expect(\"Invalid identifier\");\n\n    tokio::runtime::Builder::new_current_thread()\n        .enable_all()\n        .build()\n        .expect(\"failed to create tokio runtime\")\n        .block_on(async move {\n            for _ in 0..3 {\n                match LocalSocketStream::connect(name.clone()).await {\n                    Ok(conn) => {\n                        // We are the secondary instance.\n                        // Prep to activate primary instance by allowing another process to take focus.\n\n                        // A workaround to allow AllowSetForegroundWindow to succeed - press a key.\n                        // This was originally used by Chromium: https://bugs.chromium.org/p/chromium/issues/detail?id=837796\n                        // dummy_keypress();\n\n                        // let primary_instance_pid = conn.peer_pid().unwrap_or(ASFW_ANY);\n                        // unsafe {\n                        //     let success = AllowSetForegroundWindow(primary_instance_pid) != 0;\n                        //     if !success {\n                        //         log::warn!(\"AllowSetForegroundWindow failed.\");\n                        //     }\n                        // }\n                        let (socket_rx, mut socket_tx) = conn.split();\n                        let mut socket_rx = socket_rx.as_tokio_async_read();\n                        let url = std::env::args().nth(1).expect(\"URL not provided\");\n                        socket_tx\n                            .write_all(url.as_bytes())\n                            .await\n                            .expect(\"Failed to write to socket\");\n                        socket_tx\n                            .write_all(b\"\\n\")\n                            .await\n                            .expect(\"Failed to write to socket\");\n                        socket_tx.flush().await.expect(\"Failed to flush socket\");\n\n                        let mut reader = BufReader::new(&mut socket_rx);\n                        let mut buf = String::new();\n                        if let Err(e) = reader.read_line(&mut buf).await {\n                            eprintln!(\"Error reading from connection: {e}\");\n                        }\n                        buf.pop();\n                        dummy_keypress();\n                        let pid = buf.parse::<u32>().unwrap_or(ASFW_ANY);\n                        unsafe {\n                            let success = AllowSetForegroundWindow(pid) != 0;\n                            if !success {\n                                eprintln!(\"AllowSetForegroundWindow failed.\");\n                            }\n                        }\n                        std::process::exit(0);\n                    }\n                    Err(e) => {\n                        eprintln!(\"Failed to connect to local socket: {e}\");\n                        std::thread::sleep(std::time::Duration::from_millis(1));\n                    }\n                };\n            }\n        });\n\n    ID.set(identifier.to_string())\n        .expect(\"prepare() called more than once with different identifiers.\");\n}\n\n/// Send a dummy keypress event so AllowSetForegroundWindow can succeed\nfn dummy_keypress() {\n    let keyboard_input_down = KEYBDINPUT {\n        wVk: 0, // This doesn't correspond to any actual keyboard key, but should still function for the workaround.\n        dwExtraInfo: 0,\n        wScan: 0,\n        time: 0,\n        dwFlags: 0,\n    };\n\n    let mut keyboard_input_up = keyboard_input_down;\n    keyboard_input_up.dwFlags = 0x0002; // KEYUP flag\n\n    let input_down_u = INPUT_0 {\n        ki: keyboard_input_down,\n    };\n    let input_up_u = INPUT_0 {\n        ki: keyboard_input_up,\n    };\n\n    let input_down = INPUT {\n        r#type: INPUT_KEYBOARD,\n        Anonymous: input_down_u,\n    };\n\n    let input_up = INPUT {\n        r#type: INPUT_KEYBOARD,\n        Anonymous: input_up_u,\n    };\n\n    let ipsize = std::mem::size_of::<INPUT>() as i32;\n    unsafe {\n        SendInput(2, [input_down, input_up].as_ptr(), ipsize);\n    };\n}\n"
  },
  {
    "path": "cliff.toml",
    "content": "# git-cliff ~ configuration file\n# https://git-cliff.org/docs/configuration\n\n[changelog]\n# changelog header\nheader = \"\"\"\n# Changelog\\n\nAll notable changes to this project will be documented in this file.\\n\n\"\"\"\n# template for the changelog body\n# https://keats.github.io/tera/docs/#introduction\nbody = \"\"\"\n{% set whitespace = \" \" %}\n{% if version %}\\\n    ## [{{ version | trim_start_matches(pat=\"v\") }}] - {{ timestamp | date(format=\"%Y-%m-%d\") }}\n{% else %}\\\n    ## [unreleased]\n{% endif %}\\\n{% for group, commits in commits | filter(attribute=\"breaking\", value=true) | group_by(attribute=\"group\") %}\n    ### {{ group | upper_first }}\n    {% for commit in commits | filter(attribute=\"scope\") | sort(attribute=\"scope\") %}\n        - **{{ commit.scope | trim_end}}:**{{ whitespace }}{{ commit.message | upper_first | trim_end }}\\\n          {% if commit.github.username %} by @{{ commit.github.username }} {% else %} by {{ commit.author.name }} {%- endif -%}\n          {% if commit.github.pr_number %} in \\\n            [#{{ commit.github.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.github.pr_number }}) \\\n    {%- endif %}\n    {% endfor %}\\\n    {% for commit in commits %}{% if not commit.scope %}\n        - {{ commit.message | upper_first | trim_end }}\\\n          {% if commit.github.username %} by @{{ commit.github.username }} {% else %} by {{ commit.author.name }} {%- endif -%}\n          {% if commit.github.pr_number %} in \\\n            [#{{ commit.github.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.github.pr_number }}) \\\n          {%- endif %}\n        {% else %}{%- endif -%}\n    {% endfor %}\n{% endfor %}\n{% for group, commits in commits | filter(attribute=\"breaking\", value=false) | group_by(attribute=\"group\") %}\n    ### {{ group | upper_first }}\n    {% for commit in commits | filter(attribute=\"scope\") | sort(attribute=\"scope\") %}\n        - **{{ commit.scope | trim_end}}:**{{ whitespace }}{{ commit.message | upper_first | trim_end }}\\\n          {% if commit.github.username %} by @{{ commit.github.username }} {% else %} by {{ commit.author.name }} {%- endif -%}\n          {% if commit.github.pr_number %} in \\\n            [#{{ commit.github.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.github.pr_number }}) \\\n    {%- endif %}\n    {% endfor %}\\\n    {% for commit in commits %}{% if not commit.scope %}\n        - {{ commit.message | upper_first | trim_end }}\\\n          {% if commit.github.username %} by @{{ commit.github.username }} {% else %} by {{ commit.author.name }} {%- endif -%}\n          {% if commit.github.pr_number %} in \\\n            [#{{ commit.github.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.github.pr_number }}) \\\n          {%- endif %}\n        {% else %}{%- endif -%}\n    {% endfor %}\n{% endfor %}\\n\n\n{%- if github -%}\n\n-----------------\n\n{% if github.contributors | filter(attribute=\"is_first_time\", value=true) | length != 0 %}\n  {% raw %}\\n{% endraw -%}\n  ## New Contributors\n{%- endif %}\\\n{% for contributor in github.contributors | filter(attribute=\"is_first_time\", value=true) %}\n  * @{{ contributor.username }} made their first contribution\n    {%- if contributor.pr_number %} in \\\n      [#{{ contributor.pr_number }}]({{ self::remote_url() }}/pull/{{ contributor.pr_number }}) \\\n    {%- endif %}\n{%- endfor -%}\n\n{% if version %}\n    {% if previous.version %}\n      **Full Changelog**: {{ self::remote_url() }}/compare/{{ previous.version }}...{{ version }}\n    {% endif %}\n{% else -%}\n  {% raw %}\\n{% endraw %}\n{% endif %}\n{% endif %}\n\n{%- macro remote_url() -%}\n  https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}\n{%- endmacro -%}\n\"\"\"\n# template for the changelog footer\nfooter = \"\"\"\"\"\"\n# remove the leading and trailing whitespace from the templates\ntrim = true\n\n[git]\n# parse the commits based on https://www.conventionalcommits.org\nconventional_commits = true\n# filter out the commits that are not conventional\nfilter_unconventional = false\n# process each line of a commit as an individual commit\nsplit_commits = false\n# regex for parsing and grouping commits\ncommit_parsers = [\n  { field = \"author.name\", pattern = \"renovate\\\\[bot\\\\]\", group = \"Renovate\", skip = true },\n  { field = \"scope\", pattern = \"manifest\", message = \"^chore\", skip = true },\n  { field = \"breaking\", pattern = \"true\", group = \"💥 Breaking Changes\" },\n  { message = \"^feat\", group = \"✨ Features\" },\n  { message = \"^fix\", group = \"🐛 Bug Fixes\" },\n  { message = \"^doc\", group = \"📚 Documentation\" },\n  { message = \"^perf\", group = \"⚡ Performance Improvements\" },\n  { message = \"^refactor\", group = \"🔨 Refactor\" },\n  { message = \"^style\", group = \"💅 Styling\" },\n  { message = \"^test\", group = \"✅ Testing\" },\n  { message = \"^chore\\\\(release\\\\): prepare for\", skip = true },\n  { message = \"^chore: bump version\", skip = true },\n  { message = \"^chore\", group = \"🧹 Miscellaneous Tasks\" },\n  { body = \".*security\", group = \"🛡️ Security\" },\n  { body = \".*\", group = \"Other (unconventional)\", skip = true },\n]\n# protect breaking changes from being skipped due to matching a skipping commit_parser\nprotect_breaking_commits = false\n# filter out the commits that are not matched by commit parsers\nfilter_commits = false\n# regex for matching git tags\ntag_pattern = \"v[0-9].*\"\n# regex for skipping tags\nskip_tags = \"v0.1.0-beta.1\"\n# regex for ignoring tags\nignore_tags = \"\"\n# sort the tags topologically\ntopo_order = false\n# sort the commits inside sections by oldest/newest order\nsort_commits = \"newest\"\n"
  },
  {
    "path": "commitlint.config.js",
    "content": "export default { extends: ['@commitlint/config-conventional'] }\n"
  },
  {
    "path": "frontend/interface/package.json",
    "content": "{\n  \"name\": \"@nyanpasu/interface\",\n  \"version\": \"2.0.0\",\n  \"main\": \"dist/src/index.js\",\n  \"types\": \"dist/src/index.d.ts\",\n  \"require\": {\n    \".\": \"dist/src/index.js\"\n  },\n  \"type\": \"module\",\n  \"scripts\": {\n    \"build\": \"tsc\"\n  },\n  \"dependencies\": {\n    \"@tanstack/react-query\": \"5.91.2\",\n    \"@tauri-apps/api\": \"2.10.1\",\n    \"ahooks\": \"3.9.6\",\n    \"dayjs\": \"1.11.20\",\n    \"lodash-es\": \"4.17.23\",\n    \"ofetch\": \"1.5.1\",\n    \"react\": \"19.2.4\",\n    \"swr\": \"2.4.1\"\n  },\n  \"devDependencies\": {\n    \"@types/lodash-es\": \"4.17.12\",\n    \"@types/react\": \"19.2.14\"\n  }\n}\n"
  },
  {
    "path": "frontend/interface/src/hooks/index.ts",
    "content": "export * from './use-kv-storage'\n"
  },
  {
    "path": "frontend/interface/src/hooks/use-kv-storage.ts",
    "content": "import { useCallback, useEffect, useRef, useState } from 'react'\nimport { commands, events } from '../ipc/bindings'\n\nconst LOCAL_CACHE_PREFIX = 'nyanpasu-kv-:'\n/** Mirrors the `WEB_STORAGE_KEY_PREFIX` constant on the backend. */\nconst WEB_KEY_PREFIX = 'web:'\n\nfunction getLocalCache<T>(key: string, defaultValue: T): T {\n  try {\n    const raw = localStorage.getItem(LOCAL_CACHE_PREFIX + btoa(key))\n\n    if (raw === null) {\n      return defaultValue\n    }\n\n    return JSON.parse(raw) as T\n  } catch {\n    return defaultValue\n  }\n}\n\nfunction setLocalCache<T>(key: string, value: T): void {\n  try {\n    localStorage.setItem(LOCAL_CACHE_PREFIX + btoa(key), JSON.stringify(value))\n  } catch {\n    // ignore quota / security errors\n  }\n}\n\nfunction removeLocalCache(key: string): void {\n  localStorage.removeItem(LOCAL_CACHE_PREFIX + btoa(key))\n}\n\nexport interface UseKvStorageOptions<T> {\n  /**\n   * Called with the raw parsed value when it is loaded from the backend.\n   * Use this to transform old data shapes into the current shape.\n   */\n  migrate?: (value: unknown) => T\n}\n\n/**\n * A `useState`-like hook backed by the Tauri/redb KV storage.\n *\n * - Reads the localStorage cache immediately so the UI has a value on first\n *   render without flickering.\n * - Fetches the authoritative value from the backend on mount; the backend\n *   always wins.\n * - Listens for `StorageValueChangedEvent` so all open windows stay in sync.\n * - Writing calls `commands.setStorageItem` and optimistically updates local\n *   state; the subsequent backend event confirms the change.\n */\nexport function useKvStorage<T>(\n  key: string,\n  defaultValue: T,\n  options?: UseKvStorageOptions<T>,\n): readonly [\n  T,\n  (value: T | ((prev: T) => T)) => Promise<void>,\n  {\n    isLoading: boolean\n  },\n] {\n  const [value, setValueState] = useState<T>(() =>\n    getLocalCache(key, defaultValue),\n  )\n  const [isLoading, setIsLoading] = useState(true)\n\n  // Stable refs to avoid stale closures\n  const defaultValueRef = useRef(defaultValue)\n\n  const valueRef = useRef(value)\n  valueRef.current = value\n\n  const migrateRef = useRef(options?.migrate)\n  migrateRef.current = options?.migrate\n\n  const applyMigrate = useCallback((raw: unknown): T => {\n    return migrateRef.current ? migrateRef.current(raw) : (raw as T)\n  }, [])\n\n  // When key changes: reset to local cache and re-fetch from backend\n  useEffect(() => {\n    setValueState(getLocalCache(key, defaultValueRef.current))\n    setIsLoading(true)\n\n    commands.getStorageItem(key).then((result) => {\n      if (result.status === 'ok') {\n        if (result.data !== null) {\n          try {\n            const parsed = JSON.parse(result.data)\n            const migrated = applyMigrate(parsed)\n            setValueState(migrated)\n            setLocalCache(key, migrated)\n          } catch {\n            // backend returned non-JSON; keep local cache\n          }\n        }\n\n        setIsLoading(false)\n      }\n    })\n  }, [key, applyMigrate])\n\n  // Listen for changes emitted from backend (any window).\n  // The backend emits the raw storage key which includes the `web:` prefix.\n  useEffect(() => {\n    const unlistenPromise = events.storageValueChangedEvent.listen((event) => {\n      if (event.payload.key !== WEB_KEY_PREFIX + key) {\n        return\n      }\n\n      if (event.payload.value === null) {\n        setValueState(defaultValueRef.current)\n        removeLocalCache(key)\n      } else {\n        try {\n          const parsed = JSON.parse(event.payload.value)\n          const migrated = applyMigrate(parsed)\n\n          setValueState(migrated)\n          setLocalCache(key, migrated)\n        } catch {\n          // ignore invalid JSON from event\n        }\n      }\n    })\n\n    return () => {\n      unlistenPromise.then((fn) => fn())\n    }\n  }, [key, applyMigrate])\n\n  const setValue = useCallback(\n    async (newValue: T | ((prev: T) => T)) => {\n      const resolved =\n        typeof newValue === 'function'\n          ? (newValue as (prev: T) => T)(valueRef.current)\n          : newValue\n\n      // Optimistic update — the backend event will also arrive and confirm\n      setValueState(resolved)\n      setLocalCache(key, resolved)\n\n      const result = await commands.setStorageItem(\n        key,\n        JSON.stringify(resolved),\n      )\n\n      if (result.status === 'error') {\n        console.error('[useKvStorage] setStorageItem failed:', result.error)\n      }\n    },\n    [key],\n  )\n\n  return [value, setValue, { isLoading }] as const\n}\n\n/**\n * Debug utilities for the backend KV store.\n * Not intended for production use — these bypass per-key subscriptions.\n */\nexport const kvStorageDebug = {\n  /** Returns all stored key-value pairs with values deserialized from JSON. */\n  async getAll(): Promise<Record<string, unknown>> {\n    const result = await commands.getAllStorageItems()\n\n    if (result.status === 'error') {\n      throw new Error(result.error)\n    }\n\n    return Object.fromEntries(\n      result.data.map(({ key, value }) => {\n        try {\n          return [key, JSON.parse(value)]\n        } catch {\n          return [key, value]\n        }\n      }),\n    )\n  },\n\n  /** Removes every entry from the backend storage. */\n  async clear(): Promise<void> {\n    const result = await commands.clearStorage()\n\n    if (result.status === 'error') {\n      throw new Error(result.error)\n    }\n  },\n}\n"
  },
  {
    "path": "frontend/interface/src/index.ts",
    "content": "export * from './hooks'\nexport * from './ipc'\nexport * from './openapi'\nexport * from './provider'\nexport * from './service'\nexport * from './template'\nexport * from './utils'\n"
  },
  {
    "path": "frontend/interface/src/ipc/bindings.ts",
    "content": "/** tauri-specta globals **/\n\nimport {\n  Channel as TAURI_CHANNEL,\n  invoke as TAURI_INVOKE,\n} from '@tauri-apps/api/core'\nimport * as TAURI_API_EVENT from '@tauri-apps/api/event'\nimport { type WebviewWindow as __WebviewWindow__ } from '@tauri-apps/api/webviewWindow'\n\n/* oxlint-disable */\n// @ts-nocheck\n// This file was generated by [tauri-specta](https://github.com/oscartbeaumont/tauri-specta). Do not edit this file manually.\n\n/** user-defined commands **/\n\nexport const commands = {\n  /**\n   * get the system proxy\n   * server field is the combination of host and port\n   */\n  async getSysProxy(): Promise<Result<GetSysProxyResponse, string>> {\n    try {\n      return { status: 'ok', data: await TAURI_INVOKE('get_sys_proxy') }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  async openAppConfigDir(): Promise<Result<null, string>> {\n    try {\n      return { status: 'ok', data: await TAURI_INVOKE('open_app_config_dir') }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  async openAppDataDir(): Promise<Result<null, string>> {\n    try {\n      return { status: 'ok', data: await TAURI_INVOKE('open_app_data_dir') }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  async openLogsDir(): Promise<Result<null, string>> {\n    try {\n      return { status: 'ok', data: await TAURI_INVOKE('open_logs_dir') }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  async openWebUrl(url: string): Promise<Result<null, string>> {\n    try {\n      return { status: 'ok', data: await TAURI_INVOKE('open_web_url', { url }) }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  async openCoreDir(): Promise<Result<null, string>> {\n    try {\n      return { status: 'ok', data: await TAURI_INVOKE('open_core_dir') }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  /**\n   * restart the sidecar\n   */\n  async restartSidecar(): Promise<Result<null, string>> {\n    try {\n      return { status: 'ok', data: await TAURI_INVOKE('restart_sidecar') }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  async getClashInfo(): Promise<Result<ClashInfo, string>> {\n    try {\n      return { status: 'ok', data: await TAURI_INVOKE('get_clash_info') }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  async getClashLogs(): Promise<Result<string[], string>> {\n    try {\n      return { status: 'ok', data: await TAURI_INVOKE('get_clash_logs') }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  /**\n   * patch clash runtime config\n   */\n  async patchClashConfig(\n    payload: PatchRuntimeConfig,\n  ): Promise<Result<null, string>> {\n    try {\n      return {\n        status: 'ok',\n        data: await TAURI_INVOKE('patch_clash_config', { payload }),\n      }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  async changeClashCore(\n    clashCore: ClashCore | null,\n  ): Promise<Result<null, string>> {\n    try {\n      return {\n        status: 'ok',\n        data: await TAURI_INVOKE('change_clash_core', { clashCore }),\n      }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  /**\n   * get the runtime config\n   */\n  async getRuntimeConfig(): Promise<Result<JsonValue | null, string>> {\n    try {\n      return { status: 'ok', data: await TAURI_INVOKE('get_runtime_config') }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  async getRuntimeYaml(): Promise<Result<string, string>> {\n    try {\n      return { status: 'ok', data: await TAURI_INVOKE('get_runtime_yaml') }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  async getRuntimeExists(): Promise<Result<string[], string>> {\n    try {\n      return { status: 'ok', data: await TAURI_INVOKE('get_runtime_exists') }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  async getPostprocessingOutput(): Promise<\n    Result<PostProcessingOutput, string>\n  > {\n    try {\n      return {\n        status: 'ok',\n        data: await TAURI_INVOKE('get_postprocessing_output'),\n      }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  async clashApiGetProxyDelay(\n    name: string,\n    url: string | null,\n  ): Promise<Result<DelayRes, string>> {\n    try {\n      return {\n        status: 'ok',\n        data: await TAURI_INVOKE('clash_api_get_proxy_delay', { name, url }),\n      }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  async invokeUwpTool(): Promise<Result<null, string>> {\n    try {\n      return { status: 'ok', data: await TAURI_INVOKE('invoke_uwp_tool') }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  async fetchLatestCoreVersions(): Promise<\n    Result<ManifestVersionLatest, string>\n  > {\n    try {\n      return {\n        status: 'ok',\n        data: await TAURI_INVOKE('fetch_latest_core_versions'),\n      }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  async updateCore(coreType: ClashCore): Promise<Result<number, string>> {\n    try {\n      return {\n        status: 'ok',\n        data: await TAURI_INVOKE('update_core', { coreType }),\n      }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  async inspectUpdater(\n    updaterId: number,\n  ): Promise<Result<UpdaterSummary, string>> {\n    try {\n      return {\n        status: 'ok',\n        data: await TAURI_INVOKE('inspect_updater', { updaterId }),\n      }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  async getCoreVersion(coreType: ClashCore): Promise<Result<string, string>> {\n    try {\n      return {\n        status: 'ok',\n        data: await TAURI_INVOKE('get_core_version', { coreType }),\n      }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  async collectLogs(): Promise<Result<null, string>> {\n    try {\n      return { status: 'ok', data: await TAURI_INVOKE('collect_logs') }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  async getVergeConfig(): Promise<Result<IVerge, string>> {\n    try {\n      return { status: 'ok', data: await TAURI_INVOKE('get_verge_config') }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  async patchVergeConfig(payload: IVerge): Promise<Result<null, string>> {\n    try {\n      return {\n        status: 'ok',\n        data: await TAURI_INVOKE('patch_verge_config', { payload }),\n      }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  async getProfiles(): Promise<Result<Profiles, string>> {\n    try {\n      return { status: 'ok', data: await TAURI_INVOKE('get_profiles') }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  async enhanceProfiles(): Promise<Result<null, string>> {\n    try {\n      return { status: 'ok', data: await TAURI_INVOKE('enhance_profiles') }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  /**\n   * 修改profiles的\n   */\n  async patchProfilesConfig(\n    profiles: ProfilesBuilder,\n  ): Promise<Result<null, string>> {\n    try {\n      return {\n        status: 'ok',\n        data: await TAURI_INVOKE('patch_profiles_config', { profiles }),\n      }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  async viewProfile(uid: string): Promise<Result<null, string>> {\n    try {\n      return { status: 'ok', data: await TAURI_INVOKE('view_profile', { uid }) }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  /**\n   * update profile by uid\n   */\n  async patchProfile(\n    uid: string,\n    profile: ProfileBuilder,\n  ): Promise<Result<null, string>> {\n    try {\n      return {\n        status: 'ok',\n        data: await TAURI_INVOKE('patch_profile', { uid, profile }),\n      }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  /**\n   * create a new profile\n   */\n  async createProfile(\n    item: ProfileBuilder,\n    fileData: string | null,\n  ): Promise<Result<null, string>> {\n    try {\n      return {\n        status: 'ok',\n        data: await TAURI_INVOKE('create_profile', { item, fileData }),\n      }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  async importProfile(\n    url: string,\n    option: RemoteProfileOptionsBuilder | null,\n  ): Promise<Result<null, string>> {\n    try {\n      return {\n        status: 'ok',\n        data: await TAURI_INVOKE('import_profile', { url, option }),\n      }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  async reorderProfile(\n    activeId: string,\n    overId: string,\n  ): Promise<Result<null, string>> {\n    try {\n      return {\n        status: 'ok',\n        data: await TAURI_INVOKE('reorder_profile', { activeId, overId }),\n      }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  async reorderProfilesByList(list: string[]): Promise<Result<null, string>> {\n    try {\n      return {\n        status: 'ok',\n        data: await TAURI_INVOKE('reorder_profiles_by_list', { list }),\n      }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  async updateProfile(\n    uid: string,\n    option: RemoteProfileOptionsBuilder | null,\n  ): Promise<Result<null, string>> {\n    try {\n      return {\n        status: 'ok',\n        data: await TAURI_INVOKE('update_profile', { uid, option }),\n      }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  async deleteProfile(uid: string): Promise<Result<null, string>> {\n    try {\n      return {\n        status: 'ok',\n        data: await TAURI_INVOKE('delete_profile', { uid }),\n      }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  async readProfileFile(uid: string): Promise<Result<string, string>> {\n    try {\n      return {\n        status: 'ok',\n        data: await TAURI_INVOKE('read_profile_file', { uid }),\n      }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  async saveProfileFile(\n    uid: string,\n    fileData: string | null,\n  ): Promise<Result<null, string>> {\n    try {\n      return {\n        status: 'ok',\n        data: await TAURI_INVOKE('save_profile_file', { uid, fileData }),\n      }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  async getCustomAppDir(): Promise<Result<string | null, string>> {\n    try {\n      return { status: 'ok', data: await TAURI_INVOKE('get_custom_app_dir') }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  async setCustomAppDir(path: string): Promise<Result<null, string>> {\n    try {\n      return {\n        status: 'ok',\n        data: await TAURI_INVOKE('set_custom_app_dir', { path }),\n      }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  async statusService(): Promise<Result<StatusInfo, string>> {\n    try {\n      return { status: 'ok', data: await TAURI_INVOKE('status_service') }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  async installService(): Promise<Result<null, string>> {\n    try {\n      return { status: 'ok', data: await TAURI_INVOKE('install_service') }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  async uninstallService(): Promise<Result<null, string>> {\n    try {\n      return { status: 'ok', data: await TAURI_INVOKE('uninstall_service') }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  async startService(): Promise<Result<null, string>> {\n    try {\n      return { status: 'ok', data: await TAURI_INVOKE('start_service') }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  async stopService(): Promise<Result<null, string>> {\n    try {\n      return { status: 'ok', data: await TAURI_INVOKE('stop_service') }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  async restartService(): Promise<Result<null, string>> {\n    try {\n      return { status: 'ok', data: await TAURI_INVOKE('restart_service') }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  async isPortable(): Promise<Result<boolean, string>> {\n    try {\n      return { status: 'ok', data: await TAURI_INVOKE('is_portable') }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  async getProxies(): Promise<Result<Proxies, string>> {\n    try {\n      return { status: 'ok', data: await TAURI_INVOKE('get_proxies') }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  async selectProxy(\n    group: string,\n    name: string,\n  ): Promise<Result<null, string>> {\n    try {\n      return {\n        status: 'ok',\n        data: await TAURI_INVOKE('select_proxy', { group, name }),\n      }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  async updateProxyProvider(name: string): Promise<Result<null, string>> {\n    try {\n      return {\n        status: 'ok',\n        data: await TAURI_INVOKE('update_proxy_provider', { name }),\n      }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  async restartApplication(): Promise<Result<null, string>> {\n    try {\n      return { status: 'ok', data: await TAURI_INVOKE('restart_application') }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  async collectEnvs(): Promise<Result<EnvInfo, string>> {\n    try {\n      return { status: 'ok', data: await TAURI_INVOKE('collect_envs') }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  async getServerPort(): Promise<Result<number, string>> {\n    try {\n      return { status: 'ok', data: await TAURI_INVOKE('get_server_port') }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  async setTrayIcon(\n    mode: TrayIcon,\n    path: string | null,\n  ): Promise<Result<null, string>> {\n    try {\n      return {\n        status: 'ok',\n        data: await TAURI_INVOKE('set_tray_icon', { mode, path }),\n      }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  async isTrayIconSet(mode: TrayIcon): Promise<Result<boolean, string>> {\n    try {\n      return {\n        status: 'ok',\n        data: await TAURI_INVOKE('is_tray_icon_set', { mode }),\n      }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  async getCoreStatus(): Promise<Result<[CoreState, number, RunType], string>> {\n    try {\n      return { status: 'ok', data: await TAURI_INVOKE('get_core_status') }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  async urlDelayTest(\n    url: string,\n    expectedStatus: number,\n  ): Promise<Result<number | null, string>> {\n    try {\n      return {\n        status: 'ok',\n        data: await TAURI_INVOKE('url_delay_test', { url, expectedStatus }),\n      }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  async getIpsbAsn(): Promise<Result<JsonValue, string>> {\n    try {\n      return { status: 'ok', data: await TAURI_INVOKE('get_ipsb_asn') }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  async openThat(path: string): Promise<Result<null, string>> {\n    try {\n      return { status: 'ok', data: await TAURI_INVOKE('open_that', { path }) }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  async isAppimage(): Promise<Result<boolean, string>> {\n    try {\n      return { status: 'ok', data: await TAURI_INVOKE('is_appimage') }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  async getServiceInstallPrompt(): Promise<Result<string, string>> {\n    try {\n      return {\n        status: 'ok',\n        data: await TAURI_INVOKE('get_service_install_prompt'),\n      }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  async cleanupProcesses(): Promise<Result<null, string>> {\n    try {\n      return { status: 'ok', data: await TAURI_INVOKE('cleanup_processes') }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  async getStorageItem(key: string): Promise<Result<string | null, string>> {\n    try {\n      return {\n        status: 'ok',\n        data: await TAURI_INVOKE('get_storage_item', { key }),\n      }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  async setStorageItem(\n    key: string,\n    value: string,\n  ): Promise<Result<null, string>> {\n    try {\n      return {\n        status: 'ok',\n        data: await TAURI_INVOKE('set_storage_item', { key, value }),\n      }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  async removeStorageItem(key: string): Promise<Result<null, string>> {\n    try {\n      return {\n        status: 'ok',\n        data: await TAURI_INVOKE('remove_storage_item', { key }),\n      }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  /**\n   * Debug: returns all frontend KV entries (keys with the `web:` prefix).\n   * Internal storage entries used by other subsystems are excluded.\n   */\n  async getAllStorageItems(): Promise<Result<StorageEntry[], string>> {\n    try {\n      return { status: 'ok', data: await TAURI_INVOKE('get_all_storage_items') }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  /**\n   * Debug: clears all frontend KV entries (keys with the `web:` prefix).\n   * Internal storage entries used by other subsystems are left intact.\n   */\n  async clearStorage(): Promise<Result<null, string>> {\n    try {\n      return { status: 'ok', data: await TAURI_INVOKE('clear_storage') }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  async mutateProxies(): Promise<Result<Proxies, string>> {\n    try {\n      return { status: 'ok', data: await TAURI_INVOKE('mutate_proxies') }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  async getCoreDir(): Promise<Result<string, string>> {\n    try {\n      return { status: 'ok', data: await TAURI_INVOKE('get_core_dir') }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  async getClashWsConnectionsState(): Promise<\n    Result<ClashConnectionsConnectorState, string>\n  > {\n    try {\n      return {\n        status: 'ok',\n        data: await TAURI_INVOKE('get_clash_ws_connections_state'),\n      }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  async checkUpdate(): Promise<Result<UpdateWrapper | null, string>> {\n    try {\n      return { status: 'ok', data: await TAURI_INVOKE('check_update') }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  async saveWindowSizeState(label: string): Promise<Result<null, string>> {\n    try {\n      return {\n        status: 'ok',\n        data: await TAURI_INVOKE('save_window_size_state', { label }),\n      }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  async createMainWindow(): Promise<Result<null, string>> {\n    try {\n      return { status: 'ok', data: await TAURI_INVOKE('create_main_window') }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  async createLegacyWindow(): Promise<Result<null, string>> {\n    try {\n      return { status: 'ok', data: await TAURI_INVOKE('create_legacy_window') }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n  async createEditorWindow(uid: string): Promise<Result<null, string>> {\n    try {\n      return {\n        status: 'ok',\n        data: await TAURI_INVOKE('create_editor_window', { uid }),\n      }\n    } catch (e) {\n      if (e instanceof Error) throw e\n      else return { status: 'error', error: e as any }\n    }\n  },\n}\n\n/** user-defined events **/\n\nexport const events = __makeEvents__<{\n  clashConnectionsEvent: ClashConnectionsEvent\n  reactAppMountedEvent: ReactAppMountedEvent\n  storageValueChangedEvent: StorageValueChangedEvent\n  windowMessageEvent: WindowMessageEvent\n}>({\n  clashConnectionsEvent: 'clash-connections-event',\n  reactAppMountedEvent: 'react-app-mounted-event',\n  storageValueChangedEvent: 'storage-value-changed-event',\n  windowMessageEvent: 'window-message-event',\n})\n\n/** user-defined constants **/\n\n/** user-defined types **/\n\nexport type BreakWhenProxyChange = 'none' | 'chain' | 'all'\nexport type BuildInfo = {\n  app_name: string\n  app_version: string\n  pkg_version: string\n  commit_hash: string\n  commit_author: string\n  commit_date: string\n  build_date: string\n  build_profile: string\n  build_platform: string\n  rustc_version: string\n  llvm_version: string\n}\nexport type ChunkStatus = {\n  state: ChunkThreadState\n  start: number\n  end: number\n  downloaded: number\n  speed: number\n}\nexport type ChunkThreadState = 'Idle' | 'Downloading' | 'Finished'\nexport type ClashConnectionsConnectorEvent =\n  | { kind: 'state_changed'; data: ClashConnectionsConnectorState }\n  | { kind: 'update'; data: ClashConnectionsInfo }\nexport type ClashConnectionsConnectorState =\n  | 'disconnected'\n  | 'connecting'\n  | 'connected'\nexport type ClashConnectionsEvent = ClashConnectionsConnectorEvent\nexport type ClashConnectionsInfo = {\n  downloadTotal: number\n  uploadTotal: number\n  downloadSpeed: number\n  uploadSpeed: number\n}\nexport type ClashCore =\n  | 'clash'\n  | 'clash-rs'\n  | 'mihomo'\n  | 'mihomo-alpha'\n  | 'clash-rs-alpha'\nexport type ClashCoreType =\n  | 'mihomo'\n  | 'mihomo-alpha'\n  | 'clash-rs'\n  | 'clash-rs-alpha'\n  | 'clash'\nexport type ClashInfo = {\n  /**\n   * clash core port\n   */\n  port: number\n  /**\n   * same as `external-controller`\n   */\n  server: string\n  /**\n   * clash secret\n   */\n  secret: string | null\n}\nexport type ClashStrategy = {\n  external_controller_port_strategy: ExternalControllerPortStrategy\n}\nexport type CoreInfos = {\n  type: CoreType | null\n  state: CoreState\n  state_changed_at: number\n  config_path: string | null\n}\nexport type CoreState = 'Running' | { Stopped: string | null }\nexport type CoreType = { clash: ClashCoreType } | 'singbox'\nexport type DelayRes = { delay: number }\nexport type DeviceInfo = {\n  /**\n   * Device name, such as \"Intel Core i5-8250U CPU @ 1.60GHz x 8\"\n   */\n  cpu: string[]\n  /**\n   * GPU name, such as \"Intel UHD Graphics 620 (Kabylake GT2)\"\n   * Memory size in bytes\n   */\n  memory: string\n}\nexport type DownloadStatus = {\n  state: DownloaderState\n  downloaded: number\n  total: number\n  speed: number\n  chunks: ChunkStatus[]\n  now: number\n}\nexport type DownloaderState =\n  | 'idle'\n  | 'downloading'\n  | 'waiting_for_merge'\n  | 'merging'\n  | { failed: string }\n  | 'finished'\nexport type EnvInfo = {\n  os: string\n  arch: string\n  core: Partial<{ [key in string]: string }>\n  device: DeviceInfo\n  build_info: BuildInfo\n}\nexport type ExternalControllerPortStrategy =\n  | 'fixed'\n  | 'random'\n  | 'allow_fallback'\nexport type GetSysProxyResponse = {\n  enable: boolean\n  host: string\n  port: number\n  bypass: string\n  server: string\n}\n/**\n * ### `verge.yaml` schema\n */\nexport type IVerge = {\n  /**\n   * app listening port for app singleton\n   */\n  app_singleton_port: number | null\n  /**\n   * app log level\n   * silent | error | warn | info | debug | trace\n   */\n  app_log_level: LoggingLevel | null\n  language: string | null\n  /**\n   * `light` or `dark` or `system`\n   */\n  theme_mode: string | null\n  /**\n   * enable traffic graph default is true\n   */\n  traffic_graph: boolean | null\n  /**\n   * show memory info (only for Clash Meta)\n   */\n  enable_memory_usage: boolean | null\n  /**\n   * global ui framer motion effects\n   */\n  lighten_animation_effects: boolean | null\n  /**\n   * clash tun mode\n   */\n  enable_tun_mode: boolean | null\n  /**\n   * windows service mode\n   */\n  enable_service_mode?: boolean | null\n  /**\n   * can the app auto startup\n   */\n  enable_auto_launch: boolean | null\n  /**\n   * not show the window on launch\n   */\n  enable_silent_start: boolean | null\n  /**\n   * set system proxy\n   */\n  enable_system_proxy: boolean | null\n  /**\n   * enable proxy guard\n   */\n  enable_proxy_guard: boolean | null\n  /**\n   * set system proxy bypass\n   */\n  system_proxy_bypass: string | null\n  /**\n   * proxy guard interval\n   */\n  proxy_guard_interval: number | null\n  /**\n   * theme setting\n   */\n  theme_color: string | null\n  /**\n   * web ui list\n   */\n  web_ui_list: string[] | null\n  /**\n   * clash core path\n   */\n  clash_core?: ClashCore | null\n  /**\n   * hotkey map\n   * format: {func},{key}\n   */\n  hotkeys: string[] | null\n  /**\n   * 切换代理时自动关闭连接 (已弃用)\n   * @deprecated use `break_when_proxy_change` instead\n   */\n  auto_close_connection: boolean | null\n  /**\n   * 切换代理时中断连接\n   * None: 不中断\n   * Chain: 仅中断使用该代理链的连接\n   * All: 中断所有连接\n   */\n  break_when_proxy_change: BreakWhenProxyChange | null\n  /**\n   * 切换配置时中断连接\n   * true: 中断所有连接\n   * false: 不中断连接\n   */\n  break_when_profile_change: boolean | null\n  /**\n   * 切换模式时中断连接\n   * true: 中断所有连接\n   * false: 不中断连接\n   */\n  break_when_mode_change: boolean | null\n  /**\n   * 默认的延迟测试连接\n   */\n  default_latency_test: string | null\n  /**\n   * 支持关闭字段过滤，避免meta的新字段都被过滤掉，默认为真\n   */\n  enable_clash_fields: boolean | null\n  /**\n   * 是否使用内部的脚本支持，默认为真\n   */\n  enable_builtin_enhanced: boolean | null\n  /**\n   * proxy 页面布局 列数\n   */\n  proxy_layout_column: number | null\n  /**\n   * 日志清理\n   * 分钟数； 0 为不清理\n   * @deprecated use `max_log_files` instead\n   */\n  auto_log_clean: number | null\n  /**\n   * 日记轮转时间，单位：天\n   */\n  max_log_files: number | null\n  /**\n   * window size and position\n   * @deprecated use `window_size_state` instead\n   */\n  window_size_position?: number[] | null\n  window_size_state?: WindowState | null\n  /**\n   * 是否启用随机端口\n   */\n  enable_random_port: boolean | null\n  /**\n   * verge mixed port 用于覆盖 clash 的 mixed port\n   */\n  verge_mixed_port: number | null\n  /**\n   * Check update when app launch\n   */\n  enable_auto_check_update: boolean | null\n  /**\n   * Clash 相关策略\n   */\n  clash_strategy: ClashStrategy | null\n  /**\n   * 是否启用代理托盘选择\n   */\n  clash_tray_selector: ProxiesSelectorMode | null\n  always_on_top: boolean | null\n  /**\n   * Tun 堆栈选择\n   * TODO: 弃用此字段，转移到 clash config 里\n   */\n  tun_stack: TunStack | null\n  /**\n   * 是否启用网络统计信息浮窗\n   */\n  network_statistic_widget?: NetworkStatisticWidgetConfig | null\n  /**\n   * PAC URL for automatic proxy configuration\n   * This field is used to set PAC proxy without exposing it to the frontend UI\n   */\n  pac_url?: string | null\n  /**\n   * enable tray text display on Linux systems\n   * When enabled, shows proxy and TUN mode status as text next to the tray icon\n   * When disabled, only shows status via icon changes (prevents text display issues on Wayland)\n   */\n  enable_tray_text: boolean | null\n  /**\n   * Use legacy UI (original UI at \"/\" route)\n   * When true, opens legacy window; when false, opens new main window\n   */\n  use_legacy_ui: boolean | null\n}\nexport type JsonValue =\n  | null\n  | boolean\n  | number\n  | string\n  | JsonValue[]\n  | Partial<{ [key in string]: JsonValue }>\nexport type LocalProfile = {\n  /**\n   * Profile ID\n   */\n  uid: string\n  /**\n   * profile name\n   */\n  name: string\n  /**\n   * profile holds the file\n   */\n  file: string\n  /**\n   * profile description\n   */\n  desc: string | null\n  /**\n   * update time\n   */\n  updated: number\n} & {\n  /**\n   * file symlinks\n   */\n  symlinks?: string | null\n  /**\n   * process chain\n   */\n  chain?: string[]\n}\n/**\n * Builder for [`LocalProfile`](struct.LocalProfile.html).\n *\n */\nexport type LocalProfileBuilder = {\n  /**\n   * Profile ID\n   */\n  uid: string | null\n  /**\n   * profile name\n   */\n  name: string | null\n  /**\n   * profile holds the file\n   */\n  file: string | null\n  /**\n   * profile description\n   */\n  desc: string | null\n  /**\n   * update time\n   */\n  updated: number | null\n} & {\n  /**\n   * file symlinks\n   */\n  symlinks: string | null\n  /**\n   * process chain\n   */\n  chain?: string[] | null\n}\nexport type LogSpan = 'log' | 'info' | 'warn' | 'error'\nexport type LoggingLevel =\n  | 'silent'\n  | 'trace'\n  | 'debug'\n  | 'info'\n  | 'warn'\n  | 'error'\nexport type ManifestVersionLatest = {\n  mihomo: string\n  mihomo_alpha: string\n  clash_rs: string\n  clash_rs_alpha: string\n  clash_premium: string\n}\nexport type MergeProfile = {\n  /**\n   * Profile ID\n   */\n  uid: string\n  /**\n   * profile name\n   */\n  name: string\n  /**\n   * profile holds the file\n   */\n  file: string\n  /**\n   * profile description\n   */\n  desc: string | null\n  /**\n   * update time\n   */\n  updated: number\n}\n/**\n * Builder for [`MergeProfile`](struct.MergeProfile.html).\n *\n */\nexport type MergeProfileBuilder = {\n  /**\n   * Profile ID\n   */\n  uid: string | null\n  /**\n   * profile name\n   */\n  name: string | null\n  /**\n   * profile holds the file\n   */\n  file: string | null\n  /**\n   * profile description\n   */\n  desc: string | null\n  /**\n   * update time\n   */\n  updated: number | null\n}\nexport type NetworkStatisticWidgetConfig =\n  | { kind: 'disabled' }\n  | { kind: 'enabled'; value: StatisticWidgetVariant }\nexport type PatchRuntimeConfig = {\n  allow_lan?: boolean | null\n  ipv6?: boolean | null\n  log_level?: string | null\n  mode?: string | null\n}\n/**\n * 后处理输出\n */\nexport type PostProcessingOutput = {\n  /**\n   * 局部链的输出\n   */\n  scopes: Partial<{\n    [key in string]: Partial<{ [key in string]: [LogSpan, string][] }>\n  }>\n  /**\n   * 全局链的输出\n   */\n  global: Partial<{ [key in string]: [LogSpan, string][] }>\n  /**\n   * 根据配置进行的分析建议\n   */\n  advice: [LogSpan, string][]\n}\nexport type Profile =\n  | ({ type: 'remote' } & RemoteProfile)\n  | ({ type: 'local' } & LocalProfile)\n  | ({ type: 'merge' } & MergeProfile)\n  | ({ type: 'script' } & ScriptProfile)\nexport type ProfileBuilder =\n  | ({ type: 'remote' } & RemoteProfileBuilder)\n  | ({ type: 'local' } & LocalProfileBuilder)\n  | ({ type: 'merge' } & MergeProfileBuilder)\n  | ({ type: 'script' } & ScriptProfileBuilder)\n/**\n * Define the `profiles.yaml` schema\n */\nexport type Profiles = {\n  /**\n   * same as PrfConfig.current\n   */\n  current?: string[]\n  /**\n   * same as PrfConfig.chain\n   */\n  chain?: string[]\n  /**\n   * record valid fields for clash\n   */\n  valid?: string[]\n  /**\n   * profile list\n   */\n  items?: Profile[]\n}\n/**\n * Builder for [`Profiles`](struct.Profiles.html).\n *\n */\nexport type ProfilesBuilder = {\n  /**\n   * same as PrfConfig.current\n   */\n  current: string[] | null\n  /**\n   * same as PrfConfig.chain\n   */\n  chain: string[] | null\n  /**\n   * record valid fields for clash\n   */\n  valid: string[] | null\n  /**\n   * profile list\n   */\n  items: Profile[] | null\n}\nexport type Proxies = {\n  global: ProxyGroupItem\n  direct: ProxyItem\n  groups: ProxyGroupItem[]\n  records: Partial<{ [key in string]: ProxyItem }>\n  proxies: ProxyItem[]\n}\nexport type ProxiesSelectorMode = 'hidden' | 'normal' | 'submenu'\nexport type ProxyGroupItem = {\n  name: string\n  type: string\n  udp: boolean\n  history: ProxyItemHistory[]\n  all: ProxyItem[]\n  now: string | null\n  provider: string | null\n  alive: boolean | null\n  xudp?: boolean | null\n  tfo?: boolean | null\n  icon?: string | null\n  hidden?: boolean\n}\nexport type ProxyItem = {\n  name: string\n  type: string\n  udp: boolean\n  history: ProxyItemHistory[]\n  all: string[] | null\n  now: string | null\n  provider: string | null\n  alive: boolean | null\n  xudp?: boolean | null\n  tfo?: boolean | null\n  icon?: string | null\n  hidden?: boolean\n}\nexport type ProxyItemHistory = { time: string; delay: number }\n/**\n * Event emitted by the frontend when the React app is mounted.\n * Event name: `react-app-mounted-event`\n */\nexport type ReactAppMountedEvent = null\nexport type RemoteProfile = {\n  /**\n   * Profile ID\n   */\n  uid: string\n  /**\n   * profile name\n   */\n  name: string\n  /**\n   * profile holds the file\n   */\n  file: string\n  /**\n   * profile description\n   */\n  desc: string | null\n  /**\n   * update time\n   */\n  updated: number\n} & {\n  /**\n   * subscription url\n   */\n  url: string\n  /**\n   * subscription user info\n   */\n  extra?: SubscriptionInfo\n  /**\n   * remote profile options\n   */\n  option?: RemoteProfileOptions\n  /**\n   * process chain\n   */\n  chain?: string[]\n}\n/**\n * Builder for [`RemoteProfile`](struct.RemoteProfile.html).\n *\n */\nexport type RemoteProfileBuilder = {\n  /**\n   * Profile ID\n   */\n  uid: string | null\n  /**\n   * profile name\n   */\n  name: string | null\n  /**\n   * profile holds the file\n   */\n  file: string | null\n  /**\n   * profile description\n   */\n  desc: string | null\n  /**\n   * update time\n   */\n  updated: number | null\n} & {\n  /**\n   * subscription url\n   */\n  url: string | null\n  /**\n   * subscription user info\n   */\n  extra: SubscriptionInfo | null\n  /**\n   * remote profile options\n   */\n  option?: RemoteProfileOptionsBuilder\n  /**\n   * process chain\n   */\n  chain?: string[] | null\n}\nexport type RemoteProfileOptions = {\n  /**\n   * see issue #13\n   */\n  user_agent?: string | null\n  /**\n   * for `remote` profile\n   * use system proxy\n   */\n  with_proxy?: boolean | null\n  /**\n   * use self proxy\n   */\n  self_proxy?: boolean | null\n  /**\n   * subscription update interval\n   */\n  update_interval: number\n}\n/**\n * Builder for [`RemoteProfileOptions`](struct.RemoteProfileOptions.html).\n *\n */\nexport type RemoteProfileOptionsBuilder = {\n  /**\n   * see issue #13\n   */\n  user_agent: string | null\n  /**\n   * for `remote` profile\n   * use system proxy\n   */\n  with_proxy: boolean | null\n  /**\n   * use self proxy\n   */\n  self_proxy: boolean | null\n  /**\n   * subscription update interval\n   */\n  update_interval: number | null\n}\nexport type RunType =\n  /**\n   * Run as child process directly\n   */\n  | 'normal'\n  /**\n   * Run by Nyanpasu Service via a ipc call\n   */\n  | 'service'\n  /**\n   * Run as elevated process, if profile advice to run as elevated\n   */\n  | 'elevated'\nexport type RuntimeInfos = {\n  service_data_dir: string\n  service_config_dir: string\n  nyanpasu_config_dir: string\n  nyanpasu_data_dir: string\n}\nexport type ScriptProfile = {\n  /**\n   * Profile ID\n   */\n  uid: string\n  /**\n   * profile name\n   */\n  name: string\n  /**\n   * profile holds the file\n   */\n  file: string\n  /**\n   * profile description\n   */\n  desc: string | null\n  /**\n   * update time\n   */\n  updated: number\n} & { script_type: ScriptType }\n/**\n * Builder for [`ScriptProfile`](struct.ScriptProfile.html).\n *\n */\nexport type ScriptProfileBuilder = {\n  /**\n   * Profile ID\n   */\n  uid: string | null\n  /**\n   * profile name\n   */\n  name: string | null\n  /**\n   * profile holds the file\n   */\n  file: string | null\n  /**\n   * profile description\n   */\n  desc: string | null\n  /**\n   * update time\n   */\n  updated: number | null\n} & { script_type: ScriptType | null }\nexport type ScriptType = 'javascript' | 'lua'\nexport type ServiceStatus = 'not_installed' | 'stopped' | 'running'\nexport type StatisticWidgetVariant = 'large' | 'small'\nexport type StatusInfo = {\n  name: string\n  version: string\n  status: ServiceStatus\n  server: StatusResBody | null\n}\nexport type StatusResBody = {\n  version: string\n  core_infos: CoreInfos\n  runtime_infos: RuntimeInfos\n}\nexport type StorageEntry = {\n  key: string\n  /**\n   * Raw JSON-encoded value string.\n   */\n  value: string\n}\n/**\n * Event emitted to all windows when a storage value changes.\n * Event name: `storage-value-changed-event`\n */\nexport type StorageValueChangedEvent = {\n  key: string\n  /**\n   * The new JSON-encoded value, or `None` if the key was removed.\n   */\n  value: string | null\n}\nexport type SubscriptionInfo = {\n  upload: number\n  download: number\n  total: number\n  expire: number\n}\nexport type TrayIcon = 'normal' | 'tun' | 'system_proxy'\nexport type TunStack = 'system' | 'gvisor' | 'mixed'\nexport type UpdateWrapper = {\n  rid: number\n  available: boolean\n  current_version: string\n  version: string\n  date: string | null\n  body: string | null\n  raw_json: JsonValue\n}\nexport type UpdaterState =\n  | 'idle'\n  | 'downloading'\n  | 'decompressing'\n  | 'replacing'\n  | 'restarting'\n  | 'done'\n  | { failed: string }\nexport type UpdaterSummary = {\n  id: number\n  state: UpdaterState\n  downloader: DownloadStatus\n}\n/**\n * Message for inter-window communication\n */\nexport type WindowMessageEvent = {\n  /**\n   * Source window label\n   */\n  from: string\n  /**\n   * Target window label (use \"*\" for broadcast)\n   */\n  to: string\n  /**\n   * Message type/event name\n   */\n  event: string\n  /**\n   * Message payload\n   */\n  payload: JsonValue\n}\nexport type WindowState = {\n  width: number\n  height: number\n  x: number\n  y: number\n  maximized: boolean\n  fullscreen: boolean\n}\n\ntype __EventObj__<T> = {\n  listen: (\n    cb: TAURI_API_EVENT.EventCallback<T>,\n  ) => ReturnType<typeof TAURI_API_EVENT.listen<T>>\n  once: (\n    cb: TAURI_API_EVENT.EventCallback<T>,\n  ) => ReturnType<typeof TAURI_API_EVENT.once<T>>\n  emit: null extends T\n    ? (payload?: T) => ReturnType<typeof TAURI_API_EVENT.emit>\n    : (payload: T) => ReturnType<typeof TAURI_API_EVENT.emit>\n}\n\nexport type Result<T, E> =\n  | { status: 'ok'; data: T }\n  | { status: 'error'; error: E }\n\nfunction __makeEvents__<T extends Record<string, any>>(\n  mappings: Record<keyof T, string>,\n) {\n  return new Proxy(\n    {} as unknown as {\n      [K in keyof T]: __EventObj__<T[K]> & {\n        (handle: __WebviewWindow__): __EventObj__<T[K]>\n      }\n    },\n    {\n      get: (_, event) => {\n        const name = mappings[event as keyof T]\n\n        return new Proxy((() => {}) as any, {\n          apply: (_, __, [window]: [__WebviewWindow__]) => ({\n            listen: (arg: any) => window.listen(name, arg),\n            once: (arg: any) => window.once(name, arg),\n            emit: (arg: any) => window.emit(name, arg),\n          }),\n          get: (_, command: keyof __EventObj__<any>) => {\n            switch (command) {\n              case 'listen':\n                return (arg: any) => TAURI_API_EVENT.listen(name, arg)\n              case 'once':\n                return (arg: any) => TAURI_API_EVENT.once(name, arg)\n              case 'emit':\n                return (arg: any) => TAURI_API_EVENT.emit(name, arg)\n            }\n          },\n        })\n      },\n    },\n  )\n}\n"
  },
  {
    "path": "frontend/interface/src/ipc/consts.ts",
    "content": "import { getSystem } from '@/utils/get-system'\n\n/**\n * Operating system, used by useUpdaterSupported hook\n */\nexport const OS = getSystem()\n\n/**\n * Nyanpasu backend event name, use tauri event api to listen this event\n */\nexport const NYANPASU_BACKEND_EVENT_NAME = 'nyanpasu://mutation'\n\n/**\n * Is appimage query key, used by useIsAppImage hook\n */\nexport const IS_APPIMAGE_QUERY_KEY = 'is-appimage'\n\n/**\n * Service prompt query key, used by useServicePrompt hook\n */\nexport const SERVICE_PROMPT_QUERY_KEY = 'service-prompt'\n\n/**\n * Core dir query key, used by useCoreDir hook\n */\nexport const CORE_DIR_QUERY_KEY = 'core-dir'\n\n/**\n * Server port query key, used by useServerPort hook\n */\nexport const SERVER_PORT_QUERY_KEY = 'server-port'\n\n/**\n * Nyanpasu setting query key, used by useSettings hook\n */\nexport const NYANPASU_SETTING_QUERY_KEY = 'settings'\n\n/**\n * Nyanpasu system proxy query key, used by useSystemProxy hook\n */\nexport const NYANPASU_SYSTEM_PROXY_QUERY_KEY = 'system-proxy'\n\n/**\n * Nyanpasu chains log query key, fn: getPostProcessingOutput\n */\nexport const NYANPASU_POST_PROCESSING_QUERY_KEY = 'post-processing'\n\n/**\n * Clash version query key, used to fetch clash version from query\n */\nexport const CLASH_VERSION_QUERY_KEY = 'clash-version'\n\n/**\n * Nyanpasu profile query key, used to fetch profiles from query\n */\nexport const RROFILES_QUERY_KEY = 'profiles'\n\n/**\n * Clash log query key, used by clash ws provider to mutate logs via clash logs ws api\n */\nexport const CLASH_LOGS_QUERY_KEY = 'clash-logs'\n\n/**\n * Clash traffic query key, used by clash ws provider to mutate memory via clash traffic ws api\n */\nexport const CLASH_TRAAFFIC_QUERY_KEY = 'clash-traffic'\n\n/**\n * Clash memory query key, used by clash ws provider to mutate memory via clash memory ws api\n */\nexport const CLASH_MEMORY_QUERY_KEY = 'clash-memory'\n\n/**\n * Clash connections query key, used by clash ws provider to mutate connections via clash connections ws api\n */\nexport const CLASH_CONNECTIONS_QUERY_KEY = 'clash-connections'\n\n/**\n * Clash config query key, used by useClashConfig hook\n */\nexport const CLASH_CONFIG_QUERY_KEY = 'clash-config'\n\n/**\n * Clash core query key, used by useClashCores hook\n */\nexport const CLASH_CORE_QUERY_KEY = 'clash-core'\n\n/**\n * Clash info query key, used by useClashInfo hook\n */\nexport const CLASH_INFO_QUERY_KEY = 'clash-info'\n\n/**\n * Clash proxies query key, used by useClashProxies hook\n */\nexport const CLASH_PROXIES_QUERY_KEY = 'clash-proxies'\n\n/**\n * Clash rules query key, used by useClashRules hook\n */\nexport const CLASH_RULES_QUERY_KEY = 'clash-rules'\n\n/**\n * Clash rules provider query key, used by useClashRulesProvider hook\n */\nexport const CLASH_RULES_PROVIDER_QUERY_KEY = 'clash-rules-provider'\n\n/**\n * Clash proxies provider query key, used by useClashProxiesProvider hook\n */\nexport const CLASH_PROXIES_PROVIDER_QUERY_KEY = 'clash-proxies-provider'\n\n/**\n * Maximum connections history length, used by clash ws provider to limit connections history length\n */\nexport const MAX_CONNECTIONS_HISTORY = 32\n\n/**\n * Maximum memory history length, used by clash ws provider to limit memory history length\n */\nexport const MAX_MEMORY_HISTORY = 32\n\n/**\n * Maximum traffic history length, used by clash ws provider to limit traffic history length\n */\nexport const MAX_TRAFFIC_HISTORY = 32\n\n/**\n * Maximum logs history length, used by clash ws provider to limit logs history length\n */\nexport const MAX_LOGS_HISTORY = 1024\n"
  },
  {
    "path": "frontend/interface/src/ipc/index.ts",
    "content": "import { commands } from './bindings'\n\nexport * from './consts'\nexport * from './use-server-port'\nexport * from './use-clash-config'\nexport * from './use-clash-connections'\nexport * from './use-clash-cores'\nexport * from './use-clash-info'\nexport * from './use-clash-logs'\nexport * from './use-clash-memory'\nexport * from './use-clash-proxies-provider'\nexport * from './use-clash-proxies'\nexport * from './use-clash-rules-provider'\nexport * from './use-clash-rules'\nexport * from './use-clash-traffic'\nexport * from './use-clash-version'\nexport * from './use-post-processing-output'\nexport * from './use-profile-content'\nexport * from './use-profile'\nexport * from './use-proxy-mode'\nexport * from './use-runtime-profile'\nexport * from './use-settings'\nexport * from './use-system-proxy'\nexport * from './use-system-service'\nexport * from './use-service-prompt'\nexport * from './use-core-dir'\nexport * from './use-platform'\n\nexport { commands, events } from './bindings'\nexport type * from './bindings'\n\n// manually added\nexport const openUWPTool = commands.invokeUwpTool\n"
  },
  {
    "path": "frontend/interface/src/ipc/use-clash-config.ts",
    "content": "import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'\nimport { ClashConfig, useClashAPI } from '../service/clash-api'\nimport { unwrapResult } from '../utils'\nimport { commands, PatchRuntimeConfig } from './bindings'\nimport { CLASH_CONFIG_QUERY_KEY } from './consts'\n\n/**\n * A hook that manages fetching and updating the Clash configuration.\n *\n * @remarks\n * This hook fetches the current Clash configuration using a query keyed by `['clash-config']`\n * and allows updates via an upsert mutation. The upsert mutation:\n * - First updates the local configuration using `setConfigs`.\n * - Then patches the remote configuration through `commands.patchClashConfig`.\n * - On success, it invalidates the `['clash-config']` query, prompting a refetch to keep the configuration up-to-date.\n *\n * @returns An object with:\n * - `query`: The result of the useQuery hook that retrieves the current configuration.\n * - `upsert`: The mutation object that can be used to update the configuration.\n *\n * @example\n * const { query, upsert } = useClashConfig();\n */\nexport const useClashConfig = () => {\n  const { configs, patchConfigs } = useClashAPI()\n\n  const queryClient = useQueryClient()\n\n  /**\n   * Retrieves the Clash configuration using a query.\n   *\n   * @remarks\n   * The query is configured with the key 'clash-config' and uses the\n   * getConfigs function as its query function. This setup ensures that:\n   * - The data is uniquely identified and cached based on the query key.\n   * - The asynchronous retrieval of configuration data is handled\n   *   via the getConfigs function.\n   *\n   * @see useQuery - For additional configuration options and usage details.\n   */\n  const query = useQuery({\n    queryKey: [CLASH_CONFIG_QUERY_KEY],\n    queryFn: configs,\n  })\n\n  /**\n   * Performs an upsert operation to update or insert the Clash configuration.\n   *\n   * This mutation function accepts a payload that extends both PatchRuntimeConfig and a partial version\n   * of Clash.Config. It first updates the local configuration via the setConfigs function, then proceeds\n   * to patch the remote configuration with commands.patchClashConfig. On a successful operation, it\n   * invalidates the 'clash-config' query to prompt refetching of the newest configuration data.\n   *\n   * @remarks\n   * Ensure that the payload conforms to both the PatchRuntimeConfig specifications and the partial structure\n   * of Clash.Config as expected by the remote configuration endpoint.\n   *\n   * @returns A Promise resolving to the updated configuration, obtained by unwrapping the result of the\n   *          commands.patchClashConfig call.\n   */\n  const upsert = useMutation({\n    mutationFn: async (payload: PatchRuntimeConfig & Partial<ClashConfig>) => {\n      await patchConfigs(payload)\n\n      return unwrapResult(await commands.patchClashConfig(payload))\n    },\n    onSuccess: () => {\n      queryClient.invalidateQueries({ queryKey: [CLASH_CONFIG_QUERY_KEY] })\n    },\n  })\n\n  return {\n    query,\n    upsert,\n  }\n}\n"
  },
  {
    "path": "frontend/interface/src/ipc/use-clash-connections.ts",
    "content": "import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'\nimport { useClashAPI } from '../service/clash-api'\nimport { CLASH_CONNECTIONS_QUERY_KEY } from './consts'\n\nexport type ClashConnection = {\n  downloadTotal: number\n  uploadTotal: number\n  memory?: number\n  connections?: ClashConnectionItem[]\n}\n\nexport type ClashConnectionItem = {\n  id: string\n  metadata: ClashConnectionMetadata\n  upload: number\n  download: number\n  start: string\n  chains: string[]\n  rule: string\n  rulePayload: string\n}\n\nexport type ClashConnectionMetadata = {\n  network: string\n  type: string\n  host: string\n  sourceIP: string\n  sourcePort: string\n  destinationPort: string\n  destinationIP?: string\n  destinationIPASN?: string\n  process?: string\n  processPath?: string\n  dnsMode?: string\n  dscp?: number\n  inboundIP?: string\n  inboundName?: string\n  inboundPort?: string\n  inboundUser?: string\n  remoteDestination?: string\n  sniffHost?: string\n  specialProxy?: string\n  specialRules?: string\n}\n\nexport const useClashConnections = () => {\n  const queryClient = useQueryClient()\n\n  const clashApi = useClashAPI()\n\n  const query = useQuery<ClashConnection[]>({\n    queryKey: [CLASH_CONNECTIONS_QUERY_KEY],\n    queryFn: () => {\n      return (\n        queryClient.getQueryData<ClashConnection[]>([\n          CLASH_CONNECTIONS_QUERY_KEY,\n        ]) || []\n      )\n    },\n    // Ensure the query is enabled and properly initialized\n    enabled: true,\n    staleTime: 0, // Data is always fresh as it comes from WebSocket\n  })\n\n  const deleteConnections = useMutation({\n    mutationFn: async (id?: string | null) => {\n      await clashApi.deleteConnections(id || undefined)\n\n      const currentData = queryClient.getQueryData([\n        CLASH_CONNECTIONS_QUERY_KEY,\n      ]) as ClashConnection[]\n\n      if (id) {\n        const lastConnections = currentData.at(-1)?.connections\n\n        if (lastConnections) {\n          const filteredConnections = lastConnections.filter(\n            (conn) => conn.id !== id,\n          )\n\n          const lastData = {\n            ...currentData.at(-1)!,\n            connections: filteredConnections,\n          }\n\n          queryClient.setQueryData(\n            [CLASH_CONNECTIONS_QUERY_KEY],\n            [...currentData.slice(0, -1), lastData],\n          )\n        }\n      } else {\n        const lastData = currentData.at(-1)\n\n        if (lastData) {\n          const { downloadTotal, uploadTotal } = lastData\n\n          queryClient.setQueryData(\n            [CLASH_CONNECTIONS_QUERY_KEY],\n            [\n              ...currentData.slice(0, -1),\n              {\n                downloadTotal,\n                uploadTotal,\n              },\n            ],\n          )\n        }\n      }\n    },\n  })\n\n  return {\n    query,\n    deleteConnections,\n  }\n}\n"
  },
  {
    "path": "frontend/interface/src/ipc/use-clash-cores.ts",
    "content": "import { kebabCase } from 'lodash-es'\nimport { unwrapResult } from '@/utils'\nimport { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'\nimport { commands, type ClashCore } from './bindings'\nimport {\n  CLASH_CORE_QUERY_KEY,\n  CLASH_VERSION_QUERY_KEY,\n  NYANPASU_SETTING_QUERY_KEY,\n} from './consts'\n\nexport const ClashCores = {\n  clash: 'Clash Premium',\n  mihomo: 'Mihomo',\n  'mihomo-alpha': 'Mihomo Alpha',\n  'clash-rs': 'Clash Rust',\n  'clash-rs-alpha': 'Clash Rust Alpha',\n} as Record<ClashCore, string>\n\nexport type ClashCoresInfo = Record<ClashCore, ClashCoresDetail>\n\nexport type ClashCoresDetail = {\n  name: string\n  currentVersion: string\n  latestVersion?: string\n}\n\nexport const useClashCores = () => {\n  const queryClient = useQueryClient()\n\n  const query = useQuery({\n    queryKey: [CLASH_CORE_QUERY_KEY],\n    queryFn: async () => {\n      return await Object.keys(ClashCores).reduce(\n        async (acc, key) => {\n          const result = await acc\n          try {\n            const currentVersion =\n              unwrapResult(await commands.getCoreVersion(key as ClashCore)) ??\n              'N/A'\n\n            result[key as ClashCore] = {\n              name: ClashCores[key as ClashCore],\n              currentVersion,\n            }\n          } catch (e) {\n            console.error('failed to fetch core version', e)\n            result[key as ClashCore] = {\n              name: ClashCores[key as ClashCore],\n              currentVersion: 'N/A',\n            }\n          }\n          return result\n        },\n        Promise.resolve({} as ClashCoresInfo),\n      )\n    },\n  })\n\n  const fetchRemote = useMutation({\n    mutationFn: async () => {\n      const results = unwrapResult(await commands.fetchLatestCoreVersions())\n\n      if (!results) {\n        return\n      }\n\n      const currentData = queryClient.getQueryData([\n        CLASH_CORE_QUERY_KEY,\n      ]) as ClashCoresInfo\n\n      if (currentData && results) {\n        const updatedData = { ...currentData }\n\n        Object.entries(results).forEach(([_key, latestVersion]) => {\n          const key = kebabCase(_key)\n\n          if (updatedData[key as ClashCore]) {\n            updatedData[key as ClashCore] = {\n              ...updatedData[key as ClashCore],\n              latestVersion,\n            }\n          }\n        })\n\n        queryClient.setQueryData([CLASH_CORE_QUERY_KEY], updatedData)\n      }\n      return results\n    },\n  })\n\n  const updateCore = useMutation({\n    mutationFn: async (core: ClashCore) => {\n      return unwrapResult(await commands.updateCore(core))\n    },\n    onSuccess: () => {\n      queryClient.invalidateQueries({ queryKey: [CLASH_CORE_QUERY_KEY] })\n    },\n  })\n\n  const upsert = useMutation({\n    mutationFn: async (core: ClashCore) => {\n      return unwrapResult(await commands.changeClashCore(core))\n    },\n    onSuccess: () => {\n      queryClient.invalidateQueries({ queryKey: [CLASH_CORE_QUERY_KEY] })\n      queryClient.invalidateQueries({ queryKey: [NYANPASU_SETTING_QUERY_KEY] })\n      queryClient.invalidateQueries({ queryKey: [CLASH_VERSION_QUERY_KEY] })\n    },\n  })\n\n  const restartSidecar = async () => {\n    return await commands.restartSidecar()\n  }\n\n  return {\n    query,\n    updateCore,\n    upsert,\n    restartSidecar,\n    fetchRemote,\n  }\n}\n"
  },
  {
    "path": "frontend/interface/src/ipc/use-clash-info.ts",
    "content": "import { unwrapResult } from '@/utils'\nimport { useQuery } from '@tanstack/react-query'\nimport { commands } from './bindings'\nimport { CLASH_INFO_QUERY_KEY } from './consts'\n\n/**\n * A hook that retrieves and returns clash information using react-query.\n *\n * This hook leverages the useQuery hook to asynchronously fetch clash information by invoking\n * the getClashInfo command. The fetched result is processed via unwrapResult before being returned\n * alongside the query's state and metadata.\n *\n * @returns An object containing the properties of the query returned by useQuery, including loading,\n * error states, and the fetched data.\n */\nexport const useClashInfo = () => {\n  const query = useQuery({\n    queryKey: [CLASH_INFO_QUERY_KEY],\n    queryFn: async () => {\n      return unwrapResult(await commands.getClashInfo())\n    },\n  })\n\n  return {\n    ...query,\n  }\n}\n"
  },
  {
    "path": "frontend/interface/src/ipc/use-clash-logs.ts",
    "content": "import { useMemoizedFn } from 'ahooks'\nimport { useClashWSContext } from '@/provider/clash-ws-provider'\nimport { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'\nimport { CLASH_LOGS_QUERY_KEY } from './consts'\n\nexport type ClashLog = {\n  type: string\n  time?: string\n  payload: string\n}\n\nexport const useClashLogs = () => {\n  const { recordLogs, setRecordLogs } = useClashWSContext()\n\n  const queryClient = useQueryClient()\n\n  const query = useQuery<ClashLog[]>({\n    queryKey: [CLASH_LOGS_QUERY_KEY],\n    queryFn: () => {\n      return queryClient.getQueryData<ClashLog[]>([CLASH_LOGS_QUERY_KEY]) || []\n    },\n  })\n\n  const clean = useMutation({\n    mutationFn: async () => {\n      await queryClient.setQueryData([CLASH_LOGS_QUERY_KEY], [])\n    },\n  })\n\n  const status = recordLogs\n\n  const enable = useMemoizedFn(() => {\n    setRecordLogs(true)\n  })\n\n  const disable = useMemoizedFn(() => {\n    setRecordLogs(false)\n  })\n\n  return {\n    query,\n    clean,\n    status,\n    enable,\n    disable,\n  }\n}\n"
  },
  {
    "path": "frontend/interface/src/ipc/use-clash-memory.ts",
    "content": "import { useQuery, useQueryClient } from '@tanstack/react-query'\nimport { CLASH_MEMORY_QUERY_KEY } from './consts'\n\nexport type ClashMemory = {\n  inuse: number\n  oslimit: number\n}\n\nexport const useClashMemory = () => {\n  const queryClient = useQueryClient()\n\n  const query = useQuery<ClashMemory[]>({\n    queryKey: [CLASH_MEMORY_QUERY_KEY],\n    queryFn: () => {\n      return (\n        queryClient.getQueryData<ClashMemory[]>([CLASH_MEMORY_QUERY_KEY]) || []\n      )\n    },\n  })\n\n  return query\n}\n"
  },
  {
    "path": "frontend/interface/src/ipc/use-clash-proxies-provider.ts",
    "content": "import { useQuery } from '@tanstack/react-query'\nimport { useClashAPI, type ClashProviderProxies } from '../service/clash-api'\nimport { CLASH_PROXIES_PROVIDER_QUERY_KEY } from './consts'\n\nexport interface ClashProxiesProviderQueryItem extends ClashProviderProxies {\n  mutate: () => Promise<void>\n}\n\nexport type ClashProxiesProviderQuery = Record<\n  string,\n  ClashProxiesProviderQueryItem\n>\n\nexport const useClashProxiesProvider = () => {\n  const { providersProxies, putProvidersProxies } = useClashAPI()\n\n  const query = useQuery({\n    queryKey: [CLASH_PROXIES_PROVIDER_QUERY_KEY],\n    queryFn: async () => {\n      const { providers } = await providersProxies()\n\n      return Object.fromEntries(\n        Object.entries(providers).map(([key, value]) => [\n          key,\n          {\n            ...value,\n            mutate: async () => {\n              await putProvidersProxies(key)\n              await query.refetch()\n            },\n          },\n        ]),\n      )\n    },\n  })\n\n  return {\n    ...query,\n  }\n}\n"
  },
  {
    "path": "frontend/interface/src/ipc/use-clash-proxies.ts",
    "content": "import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'\nimport { useClashAPI, type ClashDelayOptions } from '../service/clash-api'\nimport { unwrapResult } from '../utils'\nimport {\n  commands,\n  ProxyItemHistory,\n  type Proxies,\n  type ProxyGroupItem,\n  type ProxyItem,\n} from './bindings'\nimport { CLASH_PROXIES_QUERY_KEY } from './consts'\n\nexport type ClashProxiesQueryHelperFn = {\n  mutateDelay: (options?: ClashDelayOptions) => Promise<void>\n}\n\nexport interface ClashProxiesQueryProxyItem\n  extends ProxyItem, ClashProxiesQueryHelperFn {\n  mutateSelect: () => Promise<void>\n}\n\nexport interface ClashProxiesQueryGroupItem\n  extends ProxyGroupItem, ClashProxiesQueryHelperFn {\n  all: ClashProxiesQueryProxyItem[]\n}\n\nexport interface ClashProxiesQuery extends Proxies {\n  global: ClashProxiesQueryGroupItem\n  groups: ClashProxiesQueryGroupItem[]\n}\n\n// Create a new proxy item with updated history\nconst createUpdatedProxy = (\n  proxy: ClashProxiesQueryProxyItem,\n  { name, delay }: { name: string; delay: number },\n) => {\n  if (proxy.name !== name) return proxy\n\n  const newHistory = [\n    ...proxy.history,\n    { time: new Date().toISOString(), delay },\n  ] satisfies ProxyItemHistory[]\n\n  return { ...proxy, history: newHistory }\n}\n\nexport const useClashProxies = () => {\n  const queryClient = useQueryClient()\n\n  const { proxiesDelay, groupDelay } = useClashAPI()\n\n  const proxies = useQuery<ClashProxiesQuery | undefined>({\n    queryKey: [CLASH_PROXIES_QUERY_KEY],\n    queryFn: async () => {\n      const result = unwrapResult(await commands.getProxies())\n\n      if (!result) {\n        return\n      }\n\n      // Create helper functions to reduce code duplication\n      const createProxyWithHelpers = (\n        proxy: ProxyItem,\n        groupName: string,\n      ): ClashProxiesQueryProxyItem => ({\n        ...proxy,\n        mutateDelay: async (options?: ClashDelayOptions) => {\n          await updateProxiesDelay.mutateAsync([proxy.name, options])\n        },\n        mutateSelect: async () => {\n          await commands.selectProxy(groupName, proxy.name)\n          await proxies.refetch()\n        },\n      })\n\n      const createGroupWithHelpers = (\n        group: ProxyGroupItem,\n      ): ClashProxiesQueryGroupItem => ({\n        ...group,\n        mutateDelay: async (options?: ClashDelayOptions) => {\n          await updateGroupDelay.mutateAsync([group.name, options])\n        },\n        all: group.all.map((proxy) =>\n          createProxyWithHelpers(proxy, group.name),\n        ),\n      })\n\n      // Apply helper functions to groups and global\n      const groups = result.groups\n        .filter((g) => !g.hidden)\n        .map(createGroupWithHelpers)\n      const global = createGroupWithHelpers(result.global)\n\n      // merge the results & type validation\n      const merged = {\n        ...result,\n        groups,\n        global,\n      } satisfies ClashProxiesQuery\n\n      return merged\n    },\n  })\n\n  const getQueryData = () => {\n    return queryClient.getQueryData([CLASH_PROXIES_QUERY_KEY]) as\n      | ClashProxiesQuery\n      | undefined\n  }\n\n  const setQueryData = (data: ClashProxiesQuery) => {\n    queryClient.setQueryData([CLASH_PROXIES_QUERY_KEY], data)\n  }\n\n  const updateProxiesDelay = useMutation({\n    mutationFn: async (args: Parameters<typeof proxiesDelay>) => {\n      return {\n        name: args[0],\n        delay: (await proxiesDelay(...args)).delay,\n      }\n    },\n    onSuccess: ({ name, delay }) => {\n      const oldData = getQueryData()\n\n      if (!oldData) {\n        return\n      }\n\n      // Create new data structure with updated proxies\n      const newData = {\n        ...oldData,\n        global: {\n          ...oldData.global,\n          all: oldData.global.all.map((proxy) =>\n            createUpdatedProxy(proxy, { name, delay }),\n          ),\n        },\n        groups: oldData.groups.map((group) => ({\n          ...group,\n          all: group.all.map((proxy) =>\n            createUpdatedProxy(proxy, { name, delay }),\n          ),\n        })),\n      } satisfies ClashProxiesQuery\n\n      setQueryData(newData)\n    },\n  })\n\n  const updateGroupDelay = useMutation<\n    Awaited<ReturnType<typeof groupDelay>>,\n    unknown,\n    Parameters<typeof groupDelay>,\n    ReturnType<typeof setInterval>\n  >({\n    mutationFn: async (args: Parameters<typeof groupDelay>) => {\n      return await groupDelay(...args)\n    },\n    onMutate: () => {\n      // Start polling proxies every 0.5 seconds\n      const intervalId = setInterval(() => {\n        proxies.refetch()\n      }, 500)\n      // Return interval ID to be used in onSettled\n      return intervalId\n    },\n    onSuccess: (data) => {\n      const oldData = getQueryData()\n\n      if (!oldData) {\n        return\n      }\n\n      // Create new data structure with updated proxies\n      const newData = {\n        ...oldData,\n        global: {\n          ...oldData.global,\n          all: oldData.global.all.map((proxy) =>\n            Object.prototype.hasOwnProperty.call(data, proxy.name)\n              ? createUpdatedProxy(proxy, {\n                  name: proxy.name,\n                  delay: data[proxy.name],\n                })\n              : {\n                  ...proxy,\n                  history: [],\n                },\n          ),\n        },\n        groups: oldData.groups.map((group) => ({\n          ...group,\n          all: group.all.map((proxy) =>\n            Object.prototype.hasOwnProperty.call(data, proxy.name)\n              ? createUpdatedProxy(proxy, {\n                  name: proxy.name,\n                  delay: data[proxy.name],\n                })\n              : {\n                  ...proxy,\n                  history: [],\n                },\n          ),\n        })),\n      } satisfies ClashProxiesQuery\n\n      setQueryData(newData)\n    },\n    onSettled: (_, __, ___, context) => {\n      // Clear interval when mutation is settled (success or error)\n      if (context !== undefined) {\n        clearInterval(context)\n      }\n    },\n  })\n\n  return {\n    proxies,\n    updateProxiesDelay,\n    updateGroupDelay,\n  }\n}\n"
  },
  {
    "path": "frontend/interface/src/ipc/use-clash-rules-provider.ts",
    "content": "import { useQuery } from '@tanstack/react-query'\nimport { useClashAPI, type ClashProviderRule } from '../service/clash-api'\nimport { CLASH_RULES_PROVIDER_QUERY_KEY } from './consts'\n\nexport interface ClashRulesProviderQueryItem extends ClashProviderRule {\n  mutate: () => Promise<void>\n}\n\nexport type ClashRulesProviderQuery = Record<\n  string,\n  ClashRulesProviderQueryItem\n>\n\nexport const useClashRulesProvider = () => {\n  const { providersRules, putProvidersRules } = useClashAPI()\n\n  const query = useQuery({\n    queryKey: [CLASH_RULES_PROVIDER_QUERY_KEY],\n    queryFn: async () => {\n      const { providers } = await providersRules()\n\n      return Object.fromEntries(\n        Object.entries(providers).map(([key, value]) => [\n          key,\n          {\n            ...value,\n            mutate: async () => {\n              await putProvidersRules(key)\n              await query.refetch()\n            },\n          },\n        ]),\n      ) satisfies ClashRulesProviderQuery\n    },\n  })\n\n  return {\n    ...query,\n  }\n}\n"
  },
  {
    "path": "frontend/interface/src/ipc/use-clash-rules.ts",
    "content": "import { useQuery } from '@tanstack/react-query'\nimport { useClashAPI } from '../service/clash-api'\nimport { CLASH_RULES_QUERY_KEY } from './consts'\n\nexport const useClashRules = () => {\n  const { rules } = useClashAPI()\n\n  const query = useQuery({\n    queryKey: [CLASH_RULES_QUERY_KEY],\n    queryFn: async () => {\n      return await rules()\n    },\n  })\n\n  return {\n    ...query,\n  }\n}\n"
  },
  {
    "path": "frontend/interface/src/ipc/use-clash-traffic.ts",
    "content": "import { useQuery, useQueryClient } from '@tanstack/react-query'\nimport { CLASH_TRAAFFIC_QUERY_KEY } from './consts'\n\nexport type ClashTraffic = {\n  up: number\n  down: number\n}\n\nexport const useClashTraffic = () => {\n  const queryClient = useQueryClient()\n\n  const query = useQuery<ClashTraffic[]>({\n    queryKey: [CLASH_TRAAFFIC_QUERY_KEY],\n    queryFn: () => {\n      return (\n        queryClient.getQueryData<ClashTraffic[]>([CLASH_TRAAFFIC_QUERY_KEY]) ||\n        []\n      )\n    },\n  })\n\n  return query\n}\n"
  },
  {
    "path": "frontend/interface/src/ipc/use-clash-version.ts",
    "content": "import { useQuery } from '@tanstack/react-query'\nimport { useClashAPI } from '../service/clash-api'\nimport { CLASH_VERSION_QUERY_KEY } from './consts'\n\nexport const useClashVersion = () => {\n  const { version } = useClashAPI()\n\n  const query = useQuery({\n    queryKey: [CLASH_VERSION_QUERY_KEY],\n    queryFn: async () => {\n      return await version()\n    },\n  })\n\n  return query\n}\n"
  },
  {
    "path": "frontend/interface/src/ipc/use-clash-web-socket.ts",
    "content": "import { useWebSocket } from 'ahooks'\nimport { useCallback, useMemo } from 'react'\nimport { useClashInfo } from './use-clash-info'\n\nexport const useClashWebSocket = () => {\n  const { data: info } = useClashInfo()\n\n  const wsBaseUrl = useMemo(() => `ws://${info?.server}`, [info?.server])\n\n  const tokenParams = useMemo(\n    // must have token=, otherwise clash will return 403\n    () => `token=${encodeURIComponent(info?.secret || '')}`,\n    [info?.secret],\n  )\n\n  const resolveUrl = useCallback(\n    (path: string) => {\n      return `${wsBaseUrl}/${path}?${tokenParams}`\n    },\n    [wsBaseUrl, tokenParams],\n  )\n\n  const urls = useMemo(() => {\n    if (info) {\n      return {\n        connections: resolveUrl('connections'),\n        logs: resolveUrl('logs'),\n        traffic: resolveUrl('traffic'),\n        memory: resolveUrl('memory'),\n      }\n    }\n  }, [info, resolveUrl])\n\n  const connectionsWS = useWebSocket(urls?.connections ?? '')\n\n  const logsWS = useWebSocket(urls?.logs ?? '')\n\n  const trafficWS = useWebSocket(urls?.traffic ?? '')\n\n  const memoryWS = useWebSocket(urls?.memory ?? '')\n\n  return {\n    connectionsWS,\n    logsWS,\n    trafficWS,\n    memoryWS,\n  }\n}\n"
  },
  {
    "path": "frontend/interface/src/ipc/use-core-dir.ts",
    "content": "import { unwrapResult } from '@/utils'\nimport { useQuery } from '@tanstack/react-query'\nimport { commands } from './bindings'\nimport { CORE_DIR_QUERY_KEY } from './consts'\n\nexport const useCoreDir = () => {\n  const query = useQuery({\n    queryKey: [CORE_DIR_QUERY_KEY],\n    queryFn: async () => {\n      return unwrapResult(await commands.getCoreDir())\n    },\n  })\n\n  return {\n    ...query,\n  }\n}\n"
  },
  {
    "path": "frontend/interface/src/ipc/use-platform.ts",
    "content": "import { useEffect, useState } from 'react'\nimport { unwrapResult } from '@/utils'\nimport { useQuery } from '@tanstack/react-query'\nimport { commands } from './bindings'\nimport { IS_APPIMAGE_QUERY_KEY, OS } from './consts'\n\nexport const useIsAppImage = () => {\n  const query = useQuery({\n    queryKey: [IS_APPIMAGE_QUERY_KEY],\n    queryFn: async () => unwrapResult(await commands.isAppimage()),\n  })\n\n  return {\n    ...query,\n  }\n}\n\nexport function useUpdaterSupported() {\n  const [supported, setSupported] = useState(false)\n\n  const isAppImage = useIsAppImage()\n\n  useEffect(() => {\n    switch (OS) {\n      case 'macos':\n      case 'windows':\n        setSupported(true)\n        break\n      case 'linux':\n        setSupported(!!isAppImage.data)\n        break\n    }\n  }, [isAppImage.data])\n\n  return supported\n}\n"
  },
  {
    "path": "frontend/interface/src/ipc/use-post-processing-output.ts",
    "content": "import { unwrapResult } from '@/utils'\nimport { useQuery } from '@tanstack/react-query'\nimport { commands } from './bindings'\nimport { NYANPASU_POST_PROCESSING_QUERY_KEY } from './consts'\n\n/**\n * Custom hook for fetching post-processing output using React Query.\n * Another name is chains/script logs.\n *\n * This hook queries post-processing output data using a predefined query key\n * and fetches the data through the `commands.getPostprocessingOutput` command.\n * The result is unwrapped using the `unwrapResult` utility function.\n */\nexport const usePostProcessingOutput = () => {\n  const query = useQuery({\n    queryKey: [NYANPASU_POST_PROCESSING_QUERY_KEY],\n    queryFn: async () => {\n      return unwrapResult(await commands.getPostprocessingOutput())\n    },\n  })\n\n  return {\n    ...query,\n  }\n}\n"
  },
  {
    "path": "frontend/interface/src/ipc/use-profile-content.ts",
    "content": "import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'\nimport { unwrapResult } from '../utils'\nimport { commands } from './bindings'\n\n/**\n * A custom hook that manages profile content data fetching and updating.\n *\n * @remarks\n * This hook provides functionality to read and write profile content using React Query.\n * It includes both query and mutation capabilities for profile data management.\n *\n * @param uid - The unique identifier for the profile\n *\n * @returns An object containing:\n * - `query` - The React Query result object for fetching profile content\n * - `upsert` - Mutation object for saving/updating profile content\n *\n * @example\n * ```tsx\n * const { query, upsert } = useProfileContent(\"user123\");\n * const { data, isLoading } = query;\n *\n * // To update profile content\n * upsert.mutate(\"new profile content\");\n * ```\n */\nexport const useProfileContent = (uid: string) => {\n  const queryClient = useQueryClient()\n\n  /**\n   * A React Query hook that fetches profile content based on a user ID.\n   *\n   * @remarks\n   * This query uses the `readProfileFile` command to retrieve profile data\n   * and unwraps the result.\n   *\n   * @param uid - The user ID used to fetch the profile content\n   * @returns A React Query result object containing the profile content data,\n   * loading state, and error state\n   *\n   * @example\n   * ```tsx\n   * const { data, isLoading } = useQuery(['profileContent', userId]);\n   * ```\n   */\n  const query = useQuery({\n    queryKey: ['profile-content', uid],\n    queryFn: async () => {\n      return unwrapResult(await commands.readProfileFile(uid))\n    },\n    enabled: !!uid,\n  })\n\n  /**\n   * Mutation hook for saving and updating profile file data\n   *\n   * @remarks\n   * This mutation will invalidate the profile content query cache on success\n   *\n   * @example\n   * ```ts\n   * const { mutate } = upsert;\n   * mutate(\"profile content\");\n   * ```\n   *\n   * @returns A mutation object that handles saving profile file data\n   */\n  const upsert = useMutation({\n    mutationFn: async (fileData: string) => {\n      return unwrapResult(await commands.saveProfileFile(uid, fileData))\n    },\n    onSuccess: () => {\n      queryClient.invalidateQueries({ queryKey: ['profile-content', uid] })\n    },\n  })\n\n  return {\n    query,\n    upsert,\n  }\n}\n"
  },
  {
    "path": "frontend/interface/src/ipc/use-profile.ts",
    "content": "import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'\nimport { unwrapResult } from '../utils'\nimport {\n  commands,\n  Profile,\n  type ProfileBuilder,\n  type ProfilesBuilder,\n  type RemoteProfileOptionsBuilder,\n} from './bindings'\nimport { RROFILES_QUERY_KEY } from './consts'\n\nexport type URLImportParams = Parameters<typeof commands.importProfile>\n\nexport type ManualImportParams = Parameters<typeof commands.createProfile>\n\nexport type CreateParams =\n  | {\n      type: 'url'\n      data: {\n        url: URLImportParams[0]\n        option: URLImportParams[1]\n      }\n    }\n  | {\n      type: 'manual'\n      data: {\n        item: ManualImportParams[0]\n        fileData: ManualImportParams[1]\n      }\n    }\n\ntype ProfileHelperFn = {\n  view: () => Promise<null | undefined>\n  update: (option: RemoteProfileOptionsBuilder) => Promise<null | undefined>\n  drop: () => Promise<null | undefined>\n}\n\nexport type ProfileQueryResult = NonNullable<\n  ReturnType<typeof useProfile>['query']['data']\n>\n\nexport type ProfileQueryResultItem = Profile & Partial<ProfileHelperFn>\n/**\n * A custom hook for managing profiles with various operations including creation, updating, sorting, and deletion.\n *\n * @remarks\n * This hook provides comprehensive profile management functionality through React Query:\n * - Fetching profiles with optional helper functions\n * - Creating/importing profiles from URLs or files\n * - Updating existing profiles\n * - Reordering profiles\n * - Upserting profile configurations\n * - Deleting profiles\n *\n * Each operation automatically handles cache invalidation and refetching when successful.\n *\n * @param options - Configuration options for the hook\n * @param options.without_helper_fn - When true, disables the addition of helper functions to profile items\n *\n * @returns An object containing:\n * - query: Query result for fetching profiles\n * - create: Mutation for creating/importing profiles\n * - update: Mutation for updating existing profiles\n * - sort: Mutation for reordering profiles\n * - upsert: Mutation for upserting profile configurations\n * - drop: Mutation for deleting profiles\n *\n * @example\n * ```tsx\n * const { query, create, update, sort, upsert, drop } = useProfile();\n *\n * // Fetch profiles\n * const profiles = query.data?.items;\n *\n * // Create a new profile\n * create.mutate({ type: 'file', data: { item: newProfile, fileData: 'config' }});\n *\n * // Update a profile\n * update.mutate({ uid: 'profile-id', profile: updatedProfile });\n * ```\n */\nexport const useProfile = (options?: { without_helper_fn?: boolean }) => {\n  const queryClient = useQueryClient()\n\n  function addHelperFn(item: Profile): Profile & ProfileHelperFn {\n    return {\n      ...item,\n      view: async () => unwrapResult(await commands.viewProfile(item.uid)),\n      update: async (option: RemoteProfileOptionsBuilder) =>\n        await update.mutateAsync({ uid: item.uid, option }),\n      drop: async () => await drop.mutateAsync(item.uid),\n    }\n  }\n\n  /**\n   * Retrieves and processes a list of profiles.\n   *\n   * This query uses the `useQuery` hook to fetch profile data by invoking the `commands.getProfiles()` command.\n   * The raw result is first unwrapped using `unwrapResult`, and then each profile item is augmented with additional\n   * helper functions:\n   *\n   * - view: Invokes `commands.viewProfile` with the profile's UID.\n   * - update: Executes the update mutation by passing an object containing the UID and the new profile data.\n   * - drop: Executes the drop mutation using the profile's UID.\n   *\n   * @returns A promise resolving to an object containing the profile list along with the extended helper functions.\n   */\n  const query = useQuery({\n    queryKey: [RROFILES_QUERY_KEY],\n    queryFn: async () => {\n      const result = unwrapResult(await commands.getProfiles())\n\n      // Skip helper functions if without_helper_fn is set\n      if (options?.without_helper_fn) {\n        return result\n      }\n\n      return {\n        ...result,\n        items: result?.items?.map((item) => {\n          return addHelperFn(item)\n        }),\n      }\n    },\n  })\n\n  /**\n   * Mutation hook for creating or importing profiles\n   *\n   * @remarks\n   * This mutation handles two types of profile creation:\n   * 1. URL-based import using `importProfile` command\n   * 2. Direct creation using `createProfile` command\n   *\n   * @returns A mutation object that accepts CreateParams and handles profile creation\n   *\n   * @throws Will throw an error if the profile creation/import fails\n   *\n   * @example\n   * ```ts\n   * const { mutate } = create();\n   * // Import from URL\n   * mutate({ type: 'url', data: { url: 'https://example.com/config.yaml', option: {...} }});\n   * // Create directly\n   * mutate({ type: 'file', data: { item: {...}, fileData: '...' }});\n   * ```\n   */\n  const create = useMutation({\n    mutationFn: async ({ type, data }: CreateParams) => {\n      if (type === 'url') {\n        const { url, option } = data\n        return unwrapResult(await commands.importProfile(url, option))\n      } else {\n        const { item, fileData } = data\n        return unwrapResult(await commands.createProfile(item, fileData))\n      }\n    },\n    onSuccess: () => {\n      // Invalidate and refetch\n      queryClient.invalidateQueries({ queryKey: [RROFILES_QUERY_KEY] })\n    },\n  })\n\n  /**\n   * Mutation hook for updating a profile.\n   * Uses React Query's useMutation to handle the update operation.\n   *\n   * @param {Object} params - The parameters for the update operation\n   * @param {string} params.uid - The unique identifier of the profile to update\n   * @param {RemoteProfileOptionsBuilder} params.profile - The profile data to update\n   *\n   * @returns {UseMutationResult} A mutation result object containing the update operation status and methods\n   *\n   * @remarks\n   * On successful update, it invalidates the profiles query cache\n   */\n  const update = useMutation({\n    mutationFn: async ({\n      uid,\n      option,\n    }: {\n      uid: string\n      option: RemoteProfileOptionsBuilder\n    }) => {\n      return unwrapResult(await commands.updateProfile(uid, option))\n    },\n    onSuccess: () => {\n      queryClient.invalidateQueries({ queryKey: [RROFILES_QUERY_KEY] })\n    },\n  })\n\n  /**\n   * A mutation hook for updating a profile.\n   * Uses React Query's useMutation to handle the profile update operation.\n   *\n   * @property {Function} mutationFn - Async function that patches the profile\n   * @param {Object} params - The parameters for the mutation\n   * @param {string} params.uid - The unique identifier of the profile\n   * @param {ProfileBuilder} params.profile - The profile data to update\n   *\n   * @returns {UseMutationResult} A mutation result object containing the mutation state and functions\n   *\n   * @remarks\n   * On successful mutation, it invalidates the profiles query cache,\n   * triggering a refetch of the profiles data.\n   */\n  const patch = useMutation({\n    mutationFn: async ({\n      uid,\n      profile,\n    }: {\n      uid: string\n      profile: ProfileBuilder\n    }) => {\n      return unwrapResult(await commands.patchProfile(uid, profile))\n    },\n    onSuccess: () => {\n      // Invalidate and refetch\n      queryClient.invalidateQueries({ queryKey: [RROFILES_QUERY_KEY] })\n    },\n  })\n\n  /**\n   * Mutation hook for reordering profiles.\n   * Uses the React Query's useMutation hook to handle profile reordering operations.\n   *\n   * @remarks\n   * This mutation takes an array of profile UIDs and reorders them according to the new sequence.\n   * On successful reordering, it invalidates the 'profiles' query cache to trigger a refresh.\n   *\n   * @example\n   * ```typescript\n   * const { mutate } = sort;\n   * mutate(['uid1', 'uid2', 'uid3']);\n   * ```\n   */\n  const sort = useMutation({\n    mutationFn: async (uids: string[]) => {\n      return unwrapResult(await commands.reorderProfilesByList(uids))\n    },\n    onSuccess: () => {\n      // Invalidate and refetch\n      queryClient.invalidateQueries({ queryKey: [RROFILES_QUERY_KEY] })\n    },\n  })\n\n  /**\n   * Mutation hook for upserting profile configurations.\n   *\n   * @remarks\n   * This mutation handles the update/insert of profile configurations and invalidates\n   * the profiles query cache on success.\n   *\n   * @returns A mutation object that:\n   * - Accepts a ProfilesBuilder parameter for the mutation\n   * - Returns the unwrapped result from patchProfilesConfig command\n   * - Automatically invalidates the 'profiles' query cache on successful mutation\n   */\n  const upsert = useMutation({\n    mutationFn: async (options: Partial<ProfilesBuilder>) => {\n      return unwrapResult(\n        await commands.patchProfilesConfig(options as ProfilesBuilder),\n      )\n    },\n    onSuccess: () => {\n      queryClient.invalidateQueries({ queryKey: [RROFILES_QUERY_KEY] })\n    },\n  })\n\n  /**\n   * A mutation hook for deleting a profile.\n   *\n   * @returns {UseMutationResult} A mutation object that:\n   * - Accepts a profile UID as parameter\n   * - Deletes the profile via commands.deleteProfile\n   * - Automatically invalidates 'profiles' queries on success\n   */\n  const drop = useMutation({\n    mutationFn: async (uid: string) => {\n      return unwrapResult(await commands.deleteProfile(uid))\n    },\n    onSuccess: () => {\n      queryClient.invalidateQueries({ queryKey: [RROFILES_QUERY_KEY] })\n    },\n  })\n\n  return {\n    query,\n    create,\n    update,\n    patch,\n    sort,\n    upsert,\n    drop,\n  }\n}\n"
  },
  {
    "path": "frontend/interface/src/ipc/use-proxy-mode.ts",
    "content": "import { useMemo } from 'react'\nimport { useClashAPI } from '../service/clash-api'\nimport { useClashConfig } from './use-clash-config'\nimport { useSetting } from './use-settings'\n\n/**\n * Hook for managing proxy mode in Clash configuration\n *\n * @returns {Object} An object containing:\n * - value: Record of available proxy modes (rule, global, direct, script) with their active states\n * - upsert: Function to update the proxy mode and delete existing connections\n *\n * @remarks\n * - Script mode is only available when using Clash Premium\n * - Default mode is 'rule' if current mode is invalid or not set\n * - Changes to proxy mode will clear all existing connections\n */\nexport const useProxyMode = () => {\n  const clashConfig = useClashConfig()\n\n  const clashCore = useSetting('clash_core')\n\n  const { deleteConnections } = useClashAPI()\n\n  const value = useMemo(() => {\n    const modes: Record<'rule' | 'global' | 'direct', boolean> & {\n      script?: boolean\n    } = {\n      rule: false,\n      global: false,\n      direct: false,\n    }\n\n    // only clash premium support script mode\n    if (clashCore.value === 'clash') {\n      modes.script = false\n    }\n\n    const currentMode = clashConfig.query.data?.mode?.toLowerCase()\n\n    if (\n      currentMode &&\n      Object.prototype.hasOwnProperty.call(modes, currentMode)\n    ) {\n      modes[currentMode as keyof typeof modes] = true\n    } else {\n      modes.rule = true\n    }\n\n    return modes\n  }, [clashConfig.query.data, clashCore.value])\n\n  const upsert = async (mode: string) => {\n    await deleteConnections()\n\n    await clashConfig.upsert.mutateAsync({ mode })\n  }\n\n  return {\n    value,\n    upsert,\n  }\n}\n"
  },
  {
    "path": "frontend/interface/src/ipc/use-runtime-profile.ts",
    "content": "import { useQuery } from '@tanstack/react-query'\nimport { unwrapResult } from '../utils'\nimport { commands } from './bindings'\n\n/**\n * Custom hook for retrieving the runtime profile.\n *\n * This hook leverages the useQuery API to asynchronously retrieve and unwrap the runtime's YAML profile data\n * via the commands.getRuntimeYaml call. The resulting query object includes properties such as data, error,\n * status, and other metadata necessary to manage the loading state.\n *\n * @returns An object containing the query state and helper methods related to the runtime profile.\n */\nexport const useRuntimeProfile = () => {\n  const query = useQuery({\n    queryKey: ['runtime-profile'],\n    queryFn: async () => {\n      return unwrapResult(await commands.getRuntimeYaml())\n    },\n  })\n\n  return {\n    ...query,\n  }\n}\n"
  },
  {
    "path": "frontend/interface/src/ipc/use-server-port.ts",
    "content": "import { getServerPort } from '@/service'\nimport { useQuery } from '@tanstack/react-query'\nimport { SERVER_PORT_QUERY_KEY } from './consts'\n\nexport const useServerPort = () => {\n  const { data: serverPort } = useQuery({\n    queryKey: [SERVER_PORT_QUERY_KEY],\n    queryFn: () => getServerPort(),\n  })\n\n  return serverPort\n}\n"
  },
  {
    "path": "frontend/interface/src/ipc/use-service-prompt.ts",
    "content": "import { unwrapResult } from '@/utils'\nimport { useQuery } from '@tanstack/react-query'\nimport { commands } from './bindings'\nimport { SERVICE_PROMPT_QUERY_KEY } from './consts'\n\nexport const useServicePrompt = () => {\n  const query = useQuery({\n    queryKey: [SERVICE_PROMPT_QUERY_KEY],\n    queryFn: async () => {\n      return unwrapResult(await commands.getServiceInstallPrompt())\n    },\n  })\n\n  return {\n    ...query,\n  }\n}\n"
  },
  {
    "path": "frontend/interface/src/ipc/use-settings.ts",
    "content": "import { merge } from 'lodash-es'\nimport { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'\nimport { unwrapResult } from '../utils'\nimport { commands, type IVerge } from './bindings'\nimport { NYANPASU_SETTING_QUERY_KEY } from './consts'\n\n/**\n * Custom hook for managing Verge configuration settings using React Query.\n * Provides functionality to fetch and update settings with automatic cache invalidation.\n *\n * @returns An object containing:\n * - query: UseQueryResult for fetching settings\n *   - data: Current Verge configuration\n *   - status: Query status ('loading', 'error', 'success')\n *   - error: Error object if query fails\n * - upsert: UseMutationResult for updating settings\n *   - mutate: Function to update configuration\n *   - status: Mutation status\n *\n * @example\n * ```tsx\n * const { query, upsert } = useSettings();\n *\n * // Get current settings\n * const settings = query.data;\n *\n * // Update settings\n * upsert.mutate({ theme: 'dark' });\n * ```\n */\nexport const useSettings = () => {\n  const queryClient = useQueryClient()\n\n  /**\n   * A query hook that fetches Verge configuration settings.\n   * Uses React Query to manage the data fetching state.\n   *\n   * @returns UseQueryResult containing:\n   * - data: The unwrapped Verge configuration data\n   * - status: Current status of the query ('loading', 'error', 'success')\n   * - error: Error object if the query fails\n   * - other standard React Query properties\n   */\n  const query = useQuery({\n    queryKey: [NYANPASU_SETTING_QUERY_KEY],\n    queryFn: async () => {\n      return unwrapResult(await commands.getVergeConfig())\n    },\n  })\n\n  /**\n   * Mutation hook for updating Verge configuration settings\n   *\n   * @remarks\n   * Uses React Query's useMutation to manage state and side effects\n   *\n   * @param options - Partial configuration options to update\n   * @returns Mutation object containing mutate function and mutation state\n   *\n   * @example\n   * ```ts\n   * const { mutate } = upsert();\n   * mutate({ theme: 'dark' });\n   * ```\n   */\n  const upsert = useMutation({\n    // Partial to allow for partial updates\n    mutationFn: async (options: Partial<IVerge>) => {\n      return unwrapResult(await commands.patchVergeConfig(options as IVerge))\n    },\n    onSuccess: () => {\n      queryClient.invalidateQueries({\n        queryKey: [NYANPASU_SETTING_QUERY_KEY],\n      })\n    },\n  })\n\n  return {\n    query,\n    upsert,\n  }\n}\n\n/**\n * A custom hook that manages a specific setting from the Verge configuration.\n *\n * @template K - The key type extending keyof IVerge\n * @param key - The specific setting key to manage\n * @returns An object containing:\n * - value: The current value of the specified setting\n * - upsert: Function to update the setting value\n * - Additional merged hook status properties\n *\n * @example\n * ```typescript\n * const { value, upsert } = useSetting('theme');\n * // value contains current theme setting\n * // upsert can be used to update theme setting\n * ```\n */\nexport const useSetting = <K extends keyof IVerge>(key: K) => {\n  const {\n    query: { data, ...query },\n    upsert: update,\n  } = useSettings()\n\n  /**\n   * The value retrieved from the data object using the specified key.\n   * May be undefined if either data is undefined or the key doesn't exist in data.\n   */\n  const value = data?.[key]\n\n  /**\n   * Updates a specific setting value in the Verge configuration\n   * @param value - The new value to be set for the specified key\n   * @returns void\n   * @remarks This function will not execute if the data is not available\n   */\n  const upsert = async (value: IVerge[K]) => {\n    if (!data) {\n      return\n    }\n\n    await update.mutateAsync({ [key]: value })\n  }\n\n  return {\n    value,\n    upsert,\n    // merge hook status\n    ...merge(query, update),\n  }\n}\n"
  },
  {
    "path": "frontend/interface/src/ipc/use-system-proxy.ts",
    "content": "import { useUpdateEffect } from 'ahooks'\nimport { useQuery } from '@tanstack/react-query'\nimport { unwrapResult } from '../utils'\nimport { commands } from './bindings'\nimport { NYANPASU_SYSTEM_PROXY_QUERY_KEY } from './consts'\nimport { useSetting } from './use-settings'\n\n/**\n * Custom hook to fetch and manage the system proxy settings.\n *\n * This hook leverages the `useQuery` hook to perform an asynchronous request\n * to obtain system proxy data via `commands.getSysProxy()`. The result of the query\n * is processed with `unwrapResult` to extract the proxy information.\n *\n * @returns An object containing the query results and helper properties/methods\n *          (e.g., loading status, error, and refetch function) provided by `useQuery`.\n */\nexport const useSystemProxy = () => {\n  const query = useQuery({\n    queryKey: [NYANPASU_SYSTEM_PROXY_QUERY_KEY],\n    queryFn: async () => {\n      return unwrapResult(await commands.getSysProxy())\n    },\n    refetchInterval: 5000,\n    refetchIntervalInBackground: true,\n  })\n\n  const { value } = useSetting('enable_system_proxy')\n\n  useUpdateEffect(() => {\n    query.refetch()\n  }, [value])\n\n  return {\n    ...query,\n  }\n}\n"
  },
  {
    "path": "frontend/interface/src/ipc/use-system-service.ts",
    "content": "import { unwrapResult } from '@/utils'\nimport { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'\nimport { commands } from './bindings'\n\nexport type ServiceType = 'install' | 'uninstall' | 'start' | 'stop'\n\n/**\n * Custom hook to fetch and manage the system service status using TanStack Query.\n *\n * @returns An object containing the query result for the system service status.\n */\nexport const useSystemService = () => {\n  const queryClient = useQueryClient()\n\n  const query = useQuery({\n    queryKey: ['system-service'],\n    queryFn: async () => {\n      return unwrapResult(await commands.statusService())\n    },\n  })\n\n  const upsert = useMutation({\n    mutationFn: async (type: ServiceType) => {\n      switch (type) {\n        case 'install':\n          await commands.installService()\n          break\n\n        case 'uninstall':\n          await commands.uninstallService()\n          break\n\n        case 'start':\n          await commands.startService()\n          break\n\n        case 'stop':\n          await commands.stopService()\n          break\n      }\n    },\n    onSuccess: () => {\n      queryClient.invalidateQueries({ queryKey: ['system-service'] })\n    },\n  })\n\n  return {\n    query,\n    upsert,\n  }\n}\n"
  },
  {
    "path": "frontend/interface/src/openapi/geoip/index.ts",
    "content": "export * from './ipsb'\n"
  },
  {
    "path": "frontend/interface/src/openapi/geoip/ipsb.ts",
    "content": "import useSWR, { SWRConfiguration } from 'swr'\nimport { getIpsbASN } from '@/service'\n\nexport interface IPSBResponse {\n  organization: string\n  longitude: number\n  timezone: string\n  isp: string\n  offset: number\n  asn: number\n  asn_organization: string\n  country: string\n  ip: string\n  latitude: number\n  continent_code: string\n  country_code: string\n}\n\nexport const useIPSB = (config?: SWRConfiguration) => {\n  return useSWR('https://api.ip.sb/geoip', () => getIpsbASN(), config)\n}\n"
  },
  {
    "path": "frontend/interface/src/openapi/healthcheck/index.ts",
    "content": "import { createTiming } from './utils'\n\nexport const timing = {\n  Google: createTiming('https://www.gstatic.com/generate_204'),\n  GitHub: createTiming('https://github.com/', 200),\n  BingCN: createTiming('https://cn.bing.com/', 200),\n  Baidu: createTiming('https://www.baidu.com/', 200),\n}\n"
  },
  {
    "path": "frontend/interface/src/openapi/healthcheck/utils.ts",
    "content": "import { urlDelayTest } from '@/service'\n\nexport const timing = async (url: string, code: number) => {\n  return (await urlDelayTest(url, code)) ?? 0\n}\n\nexport const createTiming = (url: string, code: number = 204) => {\n  return () => timing(url, code)\n}\n"
  },
  {
    "path": "frontend/interface/src/openapi/index.ts",
    "content": "export * from './geoip'\nexport * from './healthcheck'\n"
  },
  {
    "path": "frontend/interface/src/provider/clash-ws-provider.tsx",
    "content": "import { useUpdateEffect } from 'ahooks'\nimport dayjs from 'dayjs'\nimport {\n  createContext,\n  useContext,\n  useState,\n  type PropsWithChildren,\n} from 'react'\nimport { useQueryClient } from '@tanstack/react-query'\nimport {\n  CLASH_CONNECTIONS_QUERY_KEY,\n  CLASH_LOGS_QUERY_KEY,\n  CLASH_MEMORY_QUERY_KEY,\n  CLASH_TRAAFFIC_QUERY_KEY,\n  MAX_CONNECTIONS_HISTORY,\n  MAX_LOGS_HISTORY,\n  MAX_MEMORY_HISTORY,\n  MAX_TRAFFIC_HISTORY,\n} from '../ipc/consts'\nimport type { ClashConnection } from '../ipc/use-clash-connections'\nimport type { ClashLog } from '../ipc/use-clash-logs'\nimport type { ClashMemory } from '../ipc/use-clash-memory'\nimport type { ClashTraffic } from '../ipc/use-clash-traffic'\nimport { useClashWebSocket } from '../ipc/use-clash-web-socket'\n\n// Utility functions for localStorage persistence\nconst createPersistedState = (key: string, defaultValue: boolean) => {\n  const getStoredValue = (): boolean => {\n    try {\n      const item = localStorage.getItem(key)\n      return item ? JSON.parse(item) : defaultValue\n    } catch {\n      return defaultValue\n    }\n  }\n\n  const setStoredValue = (value: boolean) => {\n    try {\n      localStorage.setItem(key, JSON.stringify(value))\n    } catch {\n      // Ignore storage errors\n    }\n  }\n\n  return { getStoredValue, setStoredValue }\n}\n\nconst ClashWSContext = createContext<{\n  recordLogs: boolean\n  setRecordLogs: (value: boolean) => void\n  recordTraffic: boolean\n  setRecordTraffic: (value: boolean) => void\n  recordMemory: boolean\n  setRecordMemory: (value: boolean) => void\n  recordConnections: boolean\n  setRecordConnections: (value: boolean) => void\n} | null>(null)\n\nexport const useClashWSContext = () => {\n  const context = useContext(ClashWSContext)\n\n  if (!context) {\n    throw new Error('useClashWSContext must be used in a ClashWSProvider')\n  }\n\n  return context\n}\n\nexport const ClashWSProvider = ({ children }: PropsWithChildren) => {\n  // Create persisted state handlers\n  const logsStorage = createPersistedState('clash-ws-record-logs', true)\n  const trafficStorage = createPersistedState('clash-ws-record-traffic', true)\n  const memoryStorage = createPersistedState('clash-ws-record-memory', true)\n  const connectionsStorage = createPersistedState(\n    'clash-ws-record-connections',\n    true,\n  )\n\n  // Initialize states with persisted values\n  const [recordLogs, setRecordLogsState] = useState(logsStorage.getStoredValue)\n  const [recordTraffic, setRecordTrafficState] = useState(\n    trafficStorage.getStoredValue,\n  )\n  const [recordMemory, setRecordMemoryState] = useState(\n    memoryStorage.getStoredValue,\n  )\n  const [recordConnections, setRecordConnectionsState] = useState(\n    connectionsStorage.getStoredValue,\n  )\n\n  // Wrapped setters that also persist to localStorage\n  const setRecordLogs = (value: boolean) => {\n    setRecordLogsState(value)\n    logsStorage.setStoredValue(value)\n  }\n\n  const setRecordTraffic = (value: boolean) => {\n    setRecordTrafficState(value)\n    trafficStorage.setStoredValue(value)\n  }\n\n  const setRecordMemory = (value: boolean) => {\n    setRecordMemoryState(value)\n    memoryStorage.setStoredValue(value)\n  }\n\n  const setRecordConnections = (value: boolean) => {\n    setRecordConnectionsState(value)\n    connectionsStorage.setStoredValue(value)\n  }\n\n  const { connectionsWS, memoryWS, trafficWS, logsWS } = useClashWebSocket()\n\n  const queryClient = useQueryClient()\n\n  // clash connections\n  useUpdateEffect(() => {\n    if (!recordConnections) {\n      return\n    }\n\n    const data = JSON.parse(\n      connectionsWS.latestMessage?.data,\n    ) as ClashConnection\n\n    const currentData = queryClient.getQueryData([\n      CLASH_CONNECTIONS_QUERY_KEY,\n    ]) as ClashConnection[]\n\n    const newData = [...(currentData || []), data]\n\n    if (newData.length > MAX_CONNECTIONS_HISTORY) {\n      newData.shift()\n    }\n\n    queryClient.setQueryData([CLASH_CONNECTIONS_QUERY_KEY], newData)\n  }, [connectionsWS.latestMessage])\n\n  // clash memory\n  useUpdateEffect(() => {\n    if (!recordMemory) {\n      return\n    }\n\n    const data = JSON.parse(memoryWS.latestMessage?.data) as ClashMemory\n\n    const currentData = queryClient.getQueryData([\n      CLASH_MEMORY_QUERY_KEY,\n    ]) as ClashMemory[]\n\n    const newData = [...(currentData || []), data]\n\n    if (newData.length > MAX_MEMORY_HISTORY) {\n      newData.shift()\n    }\n\n    queryClient.setQueryData([CLASH_MEMORY_QUERY_KEY], newData)\n  }, [memoryWS.latestMessage])\n\n  // clash traffic\n  useUpdateEffect(() => {\n    if (!recordTraffic) {\n      return\n    }\n\n    const data = JSON.parse(trafficWS.latestMessage?.data) as ClashTraffic\n\n    const currentData = queryClient.getQueryData([\n      CLASH_TRAAFFIC_QUERY_KEY,\n    ]) as ClashTraffic[]\n\n    const newData = [...(currentData || []), data]\n\n    if (newData.length > MAX_TRAFFIC_HISTORY) {\n      newData.shift()\n    }\n\n    queryClient.setQueryData([CLASH_TRAAFFIC_QUERY_KEY], newData)\n  }, [trafficWS.latestMessage])\n\n  // clash logs\n  useUpdateEffect(() => {\n    if (!recordLogs) {\n      return\n    }\n\n    const data = {\n      ...JSON.parse(logsWS.latestMessage?.data),\n      time: dayjs(new Date()).format('HH:mm:ss'),\n    } as ClashLog\n\n    const currentData = queryClient.getQueryData([\n      CLASH_LOGS_QUERY_KEY,\n    ]) as ClashLog[]\n\n    const newData = [...(currentData || []), data]\n\n    if (newData.length > MAX_LOGS_HISTORY) {\n      newData.shift()\n    }\n\n    queryClient.setQueryData([CLASH_LOGS_QUERY_KEY], newData)\n  }, [logsWS.latestMessage])\n\n  return (\n    <ClashWSContext.Provider\n      value={{\n        recordLogs,\n        setRecordLogs,\n        recordTraffic,\n        setRecordTraffic,\n        recordMemory,\n        setRecordMemory,\n        recordConnections,\n        setRecordConnections,\n      }}\n    >\n      {children}\n    </ClashWSContext.Provider>\n  )\n}\n"
  },
  {
    "path": "frontend/interface/src/provider/index.tsx",
    "content": "import type { PropsWithChildren } from 'react'\nimport { QueryClient, QueryClientProvider } from '@tanstack/react-query'\nimport { ClashWSProvider, useClashWSContext } from './clash-ws-provider'\nimport { MutationProvider } from './mutation-provider'\n\nconst queryClient = new QueryClient()\n\nexport const NyanpasuProvider = ({ children }: PropsWithChildren) => {\n  return (\n    <QueryClientProvider client={queryClient}>\n      <MutationProvider>\n        <ClashWSProvider>{children}</ClashWSProvider>\n      </MutationProvider>\n    </QueryClientProvider>\n  )\n}\n\nexport { useClashWSContext }\n"
  },
  {
    "path": "frontend/interface/src/provider/mutation-provider.tsx",
    "content": "import { useMount } from 'ahooks'\nimport { PropsWithChildren, useRef } from 'react'\nimport { useQueryClient } from '@tanstack/react-query'\nimport { listen, type UnlistenFn } from '@tauri-apps/api/event'\nimport {\n  CLASH_CONFIG_QUERY_KEY,\n  CLASH_INFO_QUERY_KEY,\n  CLASH_VERSION_QUERY_KEY,\n  NYANPASU_BACKEND_EVENT_NAME,\n  NYANPASU_SETTING_QUERY_KEY,\n  NYANPASU_SYSTEM_PROXY_QUERY_KEY,\n  RROFILES_QUERY_KEY,\n} from '../ipc/consts'\n\ntype EventPayload = 'nyanpasu_config' | 'clash_config' | 'proxies' | 'profiles'\n\nconst NYANPASU_CONFIG_MUTATION_KEYS = [\n  NYANPASU_SETTING_QUERY_KEY,\n  NYANPASU_SYSTEM_PROXY_QUERY_KEY,\n  // TODO: proxies hook refetch\n  // TODO: profiles hook refetch\n] as const\n\nconst CLASH_CONFIG_MUTATION_KEYS = [\n  CLASH_VERSION_QUERY_KEY,\n  CLASH_INFO_QUERY_KEY,\n  CLASH_CONFIG_QUERY_KEY,\n  RROFILES_QUERY_KEY,\n  // TODO: clash rules hook refetch\n  // TODO: clash rules providers hook refetch\n  // TODO: proxies hook refetch\n  // TODO: proxies providers hook refetch\n  // TODO: profiles hook refetch\n  // TODO: all profiles providers hook refetch, key.includes('getAllProxiesProviders')\n] as const\n\nconst PROFILES_MUTATION_KEYS = [\n  CLASH_VERSION_QUERY_KEY,\n  CLASH_INFO_QUERY_KEY,\n  // TODO: clash rules hook refetch\n  // TODO: clash rules providers hook refetch\n  // TODO: proxies hook refetch\n  // TODO: proxies providers hook refetch\n  // TODO: profiles hook refetch\n  // TODO: all profiles providers hook refetch, key.includes('getAllProxiesProviders')\n]\n\nconst PROXIES_MUTATION_KEYS = [\n  // TODO: key.includes('getProxies')\n] as const\n\nexport const MutationProvider = ({ children }: PropsWithChildren) => {\n  const unlistenFn = useRef<UnlistenFn>(null)\n\n  const queryClient = useQueryClient()\n\n  const refetchQueries = (keys: readonly string[]) => {\n    Promise.all(\n      keys.map((key) =>\n        queryClient.refetchQueries({\n          queryKey: [key],\n        }),\n      ),\n    ).catch((e) => console.error(e))\n  }\n\n  useMount(() => {\n    listen<EventPayload>(NYANPASU_BACKEND_EVENT_NAME, ({ payload }) => {\n      console.log('MutationProvider', payload)\n\n      switch (payload) {\n        case 'nyanpasu_config':\n          refetchQueries(NYANPASU_CONFIG_MUTATION_KEYS)\n          break\n        case 'clash_config':\n          refetchQueries(CLASH_CONFIG_MUTATION_KEYS)\n          break\n        case 'profiles':\n          refetchQueries(PROFILES_MUTATION_KEYS)\n          break\n        case 'proxies':\n          refetchQueries(PROXIES_MUTATION_KEYS)\n          break\n      }\n    })\n      .then((unlisten) => {\n        unlistenFn.current = unlisten\n      })\n      .catch((e) => {\n        console.error(e)\n      })\n  })\n\n  return children\n}\n"
  },
  {
    "path": "frontend/interface/src/service/clash-api.ts",
    "content": "import { ofetch } from 'ofetch'\nimport { useMemo } from 'react'\nimport type { ProxyGroupItem, SubscriptionInfo } from '../ipc/bindings'\nimport { useClashInfo } from '../ipc/use-clash-info'\n\nconst prepareServer = (server?: string) => {\n  if (server?.startsWith(':')) {\n    return `127.0.0.1${server}`\n  } else if (server && /^\\d+$/.test(server)) {\n    return `127.0.0.1:${server}`\n  } else {\n    return server\n  }\n}\n\nexport interface ClashConfig {\n  port: number\n  mode: string\n  ipv6: boolean\n  'socket-port': number\n  'allow-lan': boolean\n  'log-level': string\n  'mixed-port': number\n  'redir-port': number\n  'socks-port': number\n  'tproxy-port': number\n  'external-controller': string\n  secret: string\n}\n\nexport type ClashVersion = {\n  premium?: boolean\n  meta?: boolean\n  version: string\n}\n\nexport type ClashDelayOptions = {\n  url?: string\n  timeout?: number\n}\n\nexport type ClashProxyGroupItem = ProxyGroupItem\n\nexport type ClashProviderRule = {\n  behavior: string\n  format: string\n  name: string\n  ruleCount: number\n  type: string\n  updatedAt: string\n  vehicleType: string\n}\n\nexport type ClashProviderProxies = {\n  name: string\n  type: string\n  proxies: ClashProxyGroupItem[]\n  updatedAt?: string\n  vehicleType: string\n  subscriptionInfo?: SubscriptionInfo\n  testUrl?: string\n}\n\nexport type ClashRule = {\n  type: string\n  payload: string\n  proxy: string\n}\n\nexport const useClashAPI = () => {\n  const { data } = useClashInfo()\n\n  const request = useMemo(() => {\n    return ofetch.create({\n      baseURL: `http://${prepareServer(data?.server)}`,\n      headers: data?.secret\n        ? { Authorization: `Bearer ${data?.secret}` }\n        : undefined,\n    })\n  }, [data])\n\n  /**\n   * Fetches Clash configurations from the server.\n   */\n  const configs = async () => {\n    return await request<ClashConfig>('/configs')\n  }\n\n  /**\n   * Update basic configuration; data must be sent in the format '{\"mixed-port\": 7890}',\n   * modified as needed for the configuration items to be updated.\n   */\n  const patchConfigs = async (config: Partial<ClashConfig>) => {\n    return await request<ClashConfig>('/configs', {\n      method: 'PATCH',\n      body: config,\n    })\n  }\n\n  /**\n   * Reload basic configuration; data must be sent, and the URL must include ?force=true to enforce execution.\n   */\n  const putConfigs = async (config: Partial<ClashConfig>, force?: boolean) => {\n    const url = force ? '/configs?force=true' : '/configs'\n\n    return await request<ClashConfig>(url, {\n      method: 'PUT',\n      body: config,\n    })\n  }\n\n  const deleteConnections = async (id?: string) => {\n    const url = id ? `/connections/${id}` : '/connections'\n\n    return await request(url, {\n      method: 'DELETE',\n    })\n  }\n\n  const version = async () => {\n    return await request<ClashVersion>('/version')\n  }\n\n  const proxiesDelay = async (name: string, options?: ClashDelayOptions) => {\n    return await request<{ delay: number }>(\n      `/proxies/${encodeURIComponent(name)}/delay`,\n      {\n        params: {\n          timeout: options?.timeout || 10000,\n          url: options?.url || 'http://www.gstatic.com/generate_204',\n        },\n      },\n    )\n  }\n\n  const groupDelay = async (group: string, options?: ClashDelayOptions) => {\n    return await request<Record<string, number>>(\n      `/group/${encodeURIComponent(group)}/delay`,\n      {\n        params: {\n          timeout: options?.timeout || 10000,\n          url: options?.url || 'http://www.gstatic.com/generate_204',\n        },\n      },\n    )\n  }\n\n  const proxies = async () => {\n    return await request<{\n      proxies: ClashProxyGroupItem[]\n    }>('/proxies')\n  }\n\n  const putProxies = async ({\n    group,\n    proxy,\n  }: {\n    group: string\n    proxy: string\n  }) => {\n    return await request(`/proxies/${encodeURIComponent(group)}`, {\n      method: 'PUT',\n      body: { name: proxy },\n    })\n  }\n\n  const rules = async () => {\n    return await request<{\n      rules: ClashRule[]\n    }>('/rules')\n  }\n\n  const providersRules = async () => {\n    return await request<{ providers: Record<string, ClashProviderRule> }>(\n      '/providers/rules',\n    )\n  }\n\n  const putProvidersRules = async (name: string) => {\n    return await request(`/providers/rules/${encodeURIComponent(name)}`, {\n      method: 'PUT',\n    })\n  }\n\n  const providersProxies = async (all?: string) => {\n    const result = await request<{\n      providers: Record<string, ClashProviderProxies>\n    }>('/providers/proxies')\n\n    if (all) {\n      return result\n    }\n\n    return {\n      providers: Object.fromEntries(\n        Object.entries(result.providers).filter(([, value]) =>\n          ['http', 'file'].includes(value.vehicleType.toLowerCase()),\n        ),\n      ),\n    }\n  }\n\n  const putProvidersProxies = async (name: string) => {\n    return await request(`/providers/proxies/${encodeURIComponent(name)}`, {\n      method: 'PUT',\n    })\n  }\n\n  return {\n    configs,\n    patchConfigs,\n    putConfigs,\n    deleteConnections,\n    version,\n    proxiesDelay,\n    groupDelay,\n    proxies,\n    putProxies,\n    rules,\n    providersRules,\n    putProvidersRules,\n    providersProxies,\n    putProvidersProxies,\n  }\n}\n"
  },
  {
    "path": "frontend/interface/src/service/core.ts",
    "content": "import type { ClashCore } from '../ipc/bindings'\nimport { fetchLatestCoreVersions, getCoreVersion } from './tauri'\n\nexport interface Core {\n  name: string\n  core: ClashCore\n  version?: string\n  latest?: string\n}\n\nexport const VALID_CORE: Core[] = [\n  { name: 'Clash Premium', core: 'clash' },\n  { name: 'Mihomo', core: 'mihomo' },\n  { name: 'Mihomo Alpha', core: 'mihomo-alpha' },\n  { name: 'Clash Rust', core: 'clash-rs' },\n  { name: 'Clash Rust Alpha', core: 'clash-rs-alpha' },\n]\n\nexport const fetchCoreVersion = async () => {\n  return await Promise.all(\n    VALID_CORE.map(async (item) => {\n      try {\n        const version = await getCoreVersion(item.core)\n        return { ...item, version }\n      } catch (e) {\n        console.error('failed to fetch core version', e)\n        return { ...item, version: 'N/A' }\n      }\n    }),\n  )\n}\n\nexport const fetchLatestCore = async () => {\n  const results = await fetchLatestCoreVersions()\n\n  const cores = VALID_CORE.map((item) => {\n    const key = item.core.replace(/-/g, '_') as keyof typeof results\n\n    let latest: string\n\n    switch (item.core) {\n      case 'clash':\n        latest = `n${results['clash_premium']}`\n        break\n\n      case 'clash-rs':\n        latest = results[key].replace(/v/, '')\n        break\n\n      default:\n        latest = results[key]\n        break\n    }\n\n    return {\n      ...item,\n      latest,\n    }\n  })\n\n  return cores\n}\n\nexport enum SupportedArch {\n  // blocked by clash-rs\n  // WindowsX86 = \"windows-x86\",\n  WindowsX86_64 = 'windows-x86_64',\n  // blocked by clash-rs#212\n  // WindowsArm64 = \"windows-arm64\",\n  LinuxAarch64 = 'linux-aarch64',\n  LinuxAmd64 = 'linux-amd64',\n  DarwinArm64 = 'darwin-arm64',\n  DarwinX64 = 'darwin-x64',\n}\n\nexport enum SupportedCore {\n  Mihomo = 'mihomo',\n  MihomoAlpha = 'mihomo_alpha',\n  ClashRs = 'clash_rs',\n  ClashPremium = 'clash_premium',\n}\n\nexport type ArchMapping = { [key in SupportedArch]: string }\n\nexport interface ManifestVersion {\n  manifest_version: number\n  latest: { [K in SupportedCore]: string }\n  arch_template: { [K in SupportedCore]: ArchMapping }\n  updated_at: string // ISO 8601\n}\n"
  },
  {
    "path": "frontend/interface/src/service/index.ts",
    "content": "export * from './types'\nexport * from './tauri'\nexport * from './clash-api'\nexport * from './core'\n"
  },
  {
    "path": "frontend/interface/src/service/tauri.ts",
    "content": "// oxlint-disable typescript/no-explicit-any\nimport { IPSBResponse } from '@/openapi'\nimport { invoke } from '@tauri-apps/api/core'\nimport type {\n  ClashInfo,\n  Profile,\n  Profiles,\n  ProfilesBuilder,\n  Proxies,\n  RemoteProfileOptionsBuilder,\n} from '../ipc/bindings'\nimport { ManifestVersion } from './core'\nimport { EnvInfos, InspectUpdater, SystemProxy, VergeConfig } from './types'\n\nexport const getNyanpasuConfig = async () => {\n  return await invoke<VergeConfig>('get_verge_config')\n}\n\nexport const patchNyanpasuConfig = async (payload: VergeConfig) => {\n  return await invoke<void>('patch_verge_config', { payload })\n}\n\nexport const getClashInfo = async () => {\n  return await invoke<ClashInfo | null>('get_clash_info')\n}\n\nexport const patchClashConfig = async (payload: Partial<any>) => {\n  return await invoke<void>('patch_clash_config', { payload })\n}\n\nexport const getRuntimeExists = async () => {\n  return await invoke<string[]>('get_runtime_exists')\n}\n\nexport const getRuntimeLogs = async () => {\n  return await invoke<Record<string, [string, string][]>>('get_runtime_logs')\n}\n\nexport const createProfile = async (\n  item: Partial<Profile>,\n  fileData?: string | null,\n) => {\n  return await invoke<void>('create_profile', { item, fileData })\n}\n\nexport const updateProfile = async (\n  uid: string,\n  option?: RemoteProfileOptionsBuilder,\n) => {\n  return await invoke<void>('update_profile', { uid, option })\n}\n\nexport const deleteProfile = async (uid: string) => {\n  return await invoke<void>('delete_profile', { uid })\n}\n\nexport const viewProfile = async (uid: string) => {\n  return await invoke<void>('view_profile', { uid })\n}\n\nexport const getProfiles = async () => {\n  return await invoke<Profiles>('get_profiles')\n}\n\nexport const setProfiles = async (payload: {\n  uid: string\n  profile: Partial<Profile>\n}) => {\n  return await invoke<void>('patch_profile', payload)\n}\n\nexport const setProfilesConfig = async (profiles: ProfilesBuilder) => {\n  return await invoke<void>('patch_profiles_config', { profiles })\n}\n\nexport const readProfileFile = async (uid: string) => {\n  return await invoke<string>('read_profile_file', { uid })\n}\n\nexport const saveProfileFile = async (uid: string, fileData: string) => {\n  return await invoke<void>('save_profile_file', { uid, fileData })\n}\n\nexport const importProfile = async (\n  url: string,\n  option: RemoteProfileOptionsBuilder,\n) => {\n  return await invoke<void>('import_profile', {\n    url,\n    option,\n  })\n}\n\nexport const getCoreVersion = async (\n  coreType: Required<VergeConfig>['clash_core'],\n) => {\n  return await invoke<string>('get_core_version', { coreType })\n}\n\nexport const setClashCore = async (\n  clashCore: Required<VergeConfig>['clash_core'],\n) => {\n  return await invoke<void>('change_clash_core', { clashCore })\n}\n\nexport const restartSidecar = async () => {\n  return await invoke<void>('restart_sidecar')\n}\n\nexport const fetchLatestCoreVersions = async () => {\n  return await invoke<ManifestVersion['latest']>('fetch_latest_core_versions')\n}\n\nexport const updateCore = async (\n  coreType: Required<VergeConfig>['clash_core'],\n) => {\n  return await invoke<number>('update_core', { coreType })\n}\n\nexport const inspectUpdater = async (updaterId: number) => {\n  return await invoke<InspectUpdater>('inspect_updater', { updaterId })\n}\n\nexport const pullupUWPTool = async () => {\n  return await invoke<void>('invoke_uwp_tool')\n}\n\nexport const getSystemProxy = async () => {\n  return await invoke<SystemProxy>('get_sys_proxy')\n}\n\nexport const statusService = async () => {\n  try {\n    const result = await invoke<{\n      status: 'running' | 'stopped' | 'not_installed'\n    }>('status_service')\n    return result.status\n  } catch (e) {\n    console.error(e)\n    return 'not_installed'\n  }\n}\n\nexport const installService = async () => {\n  return await invoke<void>('install_service')\n}\n\nexport const uninstallService = async () => {\n  return await invoke<void>('uninstall_service')\n}\n\nexport const startService = async () => {\n  return await invoke<void>('start_service')\n}\n\nexport const stopService = async () => {\n  return await invoke<void>('stop_service')\n}\n\nexport const restartService = async () => {\n  return await invoke<void>('restart_service')\n}\n\nexport const openAppConfigDir = async () => {\n  return await invoke<void>('open_app_config_dir')\n}\n\nexport const openAppDataDir = async () => {\n  return await invoke<void>('open_app_data_dir')\n}\n\nexport const openCoreDir = async () => {\n  return await invoke<void>('open_core_dir')\n}\n\nexport const getCoreDir = async () => {\n  return await invoke<string>('get_core_dir')\n}\n\nexport const openLogsDir = async () => {\n  return await invoke<void>('open_logs_dir')\n}\n\nexport const collectLogs = async () => {\n  return await invoke<void>('collect_logs')\n}\n\nexport const setCustomAppDir = async (path: string) => {\n  return await invoke<void>('set_custom_app_dir', { path })\n}\n\nexport const restartApplication = async () => {\n  return await invoke<void>('restart_application')\n}\n\nexport const isPortable = async () => {\n  return await invoke<boolean>('is_portable')\n}\n\nexport const getProxies = async () => {\n  return await invoke<Proxies>('get_proxies')\n}\n\nexport const mutateProxies = async () => {\n  return await invoke<Proxies>('mutate_proxies')\n}\n\nexport const selectProxy = async (group: string, name: string) => {\n  return await invoke<void>('select_proxy', { group, name })\n}\n\nexport const updateProxyProvider = async (name: string) => {\n  return await invoke<void>('update_proxy_provider', { name })\n}\n\nexport const saveWindowSizeState = async () => {\n  return await invoke<void>('save_window_size_state')\n}\n\nexport const collectEnvs = async () => {\n  return await invoke<EnvInfos>('collect_envs')\n}\n\nexport const getRuntimeYaml = async () => {\n  return await invoke<string>('get_runtime_yaml')\n}\n\nexport const getServerPort = async () => {\n  return await invoke<number>('get_server_port')\n}\n\nexport const setTrayIcon = async (\n  mode: 'tun' | 'system_proxy' | 'normal',\n  path?: string,\n) => {\n  return await invoke<void>('set_tray_icon', { mode, path })\n}\n\nexport const isTrayIconSet = async (\n  mode: 'tun' | 'system_proxy' | 'normal',\n) => {\n  return await invoke<boolean>('is_tray_icon_set', {\n    mode,\n  })\n}\n\nexport const getCoreStatus = async () => {\n  return await invoke<\n    ['Running' | { Stopped: string | null }, number, 'normal' | 'service']\n  >('get_core_status')\n}\n\nexport const urlDelayTest = async (url: string, expectedStatus: number) => {\n  return await invoke<number | null>('url_delay_test', {\n    url,\n    expectedStatus,\n  })\n}\n\nexport const getIpsbASN = async () => invoke<IPSBResponse>('get_ipsb_asn')\n\nexport const openThat = async (path: string) => {\n  return await invoke<void>('open_that', { path })\n}\n\nexport const isAppImage = async () => {\n  return await invoke<boolean>('is_appimage')\n}\n\nexport const getServiceInstallPrompt = async () => {\n  return await invoke<string>('get_service_install_prompt')\n}\n\nexport const cleanupProcesses = async () => {\n  return await invoke<void>('cleanup_processes')\n}\n\nexport const getStorageItem = async (key: string) => {\n  return await invoke<string | null>('get_storage_item', { key })\n}\n\nexport const setStorageItem = async (key: string, value: string) => {\n  return await invoke<void>('set_storage_item', { key, value })\n}\n\nexport const removeStorageItem = async (key: string) => {\n  return await invoke<void>('remove_storage_item', { key })\n}\n\nexport const reorderProfilesByList = async (list: string[]) => {\n  return await invoke<void>('reorder_profiles_by_list', { list })\n}\n"
  },
  {
    "path": "frontend/interface/src/service/types.ts",
    "content": "export interface VergeConfig {\n  app_log_level?: 'trace' | 'debug' | 'info' | 'warn' | 'error' | string\n  language?: string\n  clash_core?:\n    | 'mihomo'\n    | 'mihomo-alpha'\n    | 'clash-rs'\n    | 'clash-rs-alpha'\n    | 'clash'\n  theme_mode?: 'light' | 'dark' | 'system'\n  theme_blur?: boolean\n  traffic_graph?: boolean\n  enable_memory_usage?: boolean\n  lighten_animation_effects?: boolean\n  enable_auto_check_update?: boolean\n  enable_tun_mode?: boolean\n  enable_auto_launch?: boolean\n  enable_service_mode?: boolean\n  enable_silent_start?: boolean\n  enable_system_proxy?: boolean\n  enable_random_port?: boolean\n  verge_mixed_port?: number\n  enable_proxy_guard?: boolean\n  proxy_guard_interval?: number\n  system_proxy_bypass?: string\n  web_ui_list?: string[]\n  hotkeys?: string[]\n  theme_setting?: {\n    primary_color?: string\n    secondary_color?: string\n    primary_text?: string\n    secondary_text?: string\n    info_color?: string\n    error_color?: string\n    warning_color?: string\n    success_color?: string\n    font_family?: string\n    css_injection?: string\n    page_transition_duration?: number\n  }\n  max_log_files?: number\n  auto_close_connection?: boolean\n  default_latency_test?: string\n  enable_clash_fields?: boolean\n  enable_builtin_enhanced?: boolean\n  proxy_layout_column?: number\n  clash_tray_selector?: 'normal' | 'hidden' | 'submenu'\n  clash_strategy?: {\n    external_controller_port_strategy: 'fixed' | 'random' | 'allow_fallback'\n  }\n  enable_tray_text?: boolean\n  tun_stack?: 'system' | 'gvisor' | 'mixed'\n  always_on_top?: boolean\n}\n\nexport interface AutoReloadConfig {\n  enabled: boolean\n  onProxyChange: boolean\n  onProfileChange: boolean\n  onModeChange: boolean\n}\n\nexport interface SystemProxy {\n  enable: boolean\n  server: string\n  bypass: string\n}\n\n// eslint-disable-next-line @typescript-eslint/no-namespace\nexport namespace Connection {\n  export interface Item {\n    id: string\n    metadata: Metadata\n    upload: number\n    download: number\n    start: string\n    chains: string[]\n    rule: string\n    rulePayload: string\n  }\n\n  export interface Metadata {\n    network: string\n    type: string\n    host: string\n    sourceIP: string\n    sourcePort: string\n    destinationPort: string\n    destinationIP?: string\n    destinationIPASN?: string\n    process?: string\n    processPath?: string\n    dnsMode?: string\n    dscp?: number\n    inboundIP?: string\n    inboundName?: string\n    inboundPort?: string\n    inboundUser?: string\n    remoteDestination?: string\n    sniffHost?: string\n    specialProxy?: string\n    specialRules?: string\n  }\n\n  export interface Response {\n    downloadTotal: number\n    uploadTotal: number\n    memory?: number\n    connections?: Item[]\n  }\n}\n\nexport interface LogMessage {\n  type: string\n  time?: string\n  payload: string\n}\n\nexport interface ProviderRules {\n  behavior: string\n  format: string\n  name: string\n  ruleCount: number\n  type: string\n  updatedAt: string\n  vehicleType: string\n}\n\nexport interface Traffic {\n  up: number\n  down: number\n}\n\nexport interface Memory {\n  inuse: number\n  oslimit: number\n}\n\nexport interface EnvInfos {\n  os: string\n  arch: string\n  core: { [key: string]: string }\n  device: {\n    cpu: Array<string>\n    memory: string\n  }\n  build_info: { [key: string]: string }\n}\n\nexport interface InspectUpdater {\n  id: number\n  state:\n    | 'idle'\n    | 'downloading'\n    | 'decompressing'\n    | 'replacing'\n    | 'restarting'\n    | 'done'\n    | { failed: string }\n  downloader: {\n    state:\n      | 'idle'\n      | 'downloading'\n      | 'waiting_for_merge'\n      | 'merging'\n      | { failed: string }\n      | 'finished'\n    downloaded: number\n    total: number\n    speed: number\n    chunks: Array<{\n      state: 'idle' | 'downloading' | 'finished'\n      start: number\n      end: number\n      downloaded: number\n      speed: number\n    }>\n    now: number\n  }\n}\n"
  },
  {
    "path": "frontend/interface/src/template/index.ts",
    "content": "// nyanpasu merge profile chain template\nconst merge = `# Clash Nyanpasu Merge Template (YAML)\n# Documentation on https://nyanpasu.elaina.moe/\n# Set the default merge strategy to recursive merge. \n# Enable the old mode with the override__ prefix. \n# Use the filter__ prefix to filter lists (removing unwanted content). \n# All prefixes should support accessing maps or lists with a.b.c syntax.\n`\n\n// nyanpasu javascript profile chain template\nconst javascript = `// Clash Nyanpasu JavaScript Template\n// Documentation on https://nyanpasu.elaina.moe/\n\n/** @type {config} */\nexport default function (profile) {\n  return profile;\n}\n`\n\n// nyanpasu lua profile chain template\nconst luascript = `-- Clash Nyanpasu Lua Script Template\n-- Documentation on https://nyanpasu.elaina.moe/\n\nreturn config;\n`\n\n// clash profile template example\nconst profile = `# Clash Nyanpasu Profile Template\n# Documentation on https://nyanpasu.elaina.moe/\n\nproxies:\n\nproxy-groups:\n\nrules:\n`\n\nexport const ProfileTemplate = {\n  merge,\n  javascript,\n  luascript,\n  profile,\n} as const\n"
  },
  {
    "path": "frontend/interface/src/utils/get-system.ts",
    "content": "type Platform =\n  | 'aix'\n  | 'android'\n  | 'darwin'\n  | 'freebsd'\n  | 'haiku'\n  | 'linux'\n  | 'openbsd'\n  | 'sunos'\n  | 'win32'\n  | 'cygwin'\n  | 'netbsd'\n\ndeclare const OS_PLATFORM: Platform | undefined\n\n// get the system os\n// according to UA\nexport function getSystem() {\n  const ua = typeof window === 'undefined' ? '' : window.navigator?.userAgent\n  const platform = typeof OS_PLATFORM !== 'undefined' ? OS_PLATFORM : 'unknown'\n\n  if (ua.includes('Mac OS X') || platform === 'darwin') return 'macos'\n\n  if (/win64|win32/i.test(ua) || platform === 'win32') return 'windows'\n\n  if (/linux/i.test(ua)) return 'linux'\n\n  return 'unknown'\n}\n"
  },
  {
    "path": "frontend/interface/src/utils/index.ts",
    "content": "import type { Result } from '../ipc/bindings'\n\nexport function unwrapResult<T, E>(res: Result<T, E>) {\n  if (res.status === 'error') {\n    throw res.error\n  }\n  return res.status === 'ok' ? res.data : undefined\n}\n\nexport * from './get-system'\nexport * from './retry'\n"
  },
  {
    "path": "frontend/interface/src/utils/retry.ts",
    "content": "/**\n * Retry a function with exponential backoff\n *\n * @param fn - The function to retry\n * @param options - Retry options\n * @returns The result of the function\n * @throws The last error encountered\n */\nexport async function retry<T>(\n  fn: () => Promise<T>,\n  options: {\n    maxRetries?: number\n    initialDelay?: number\n    maxDelay?: number\n    factor?: number\n    retryCondition?: (error: Error) => boolean\n  } = {},\n): Promise<T> {\n  const {\n    maxRetries = 3,\n    initialDelay = 200,\n    maxDelay = 5000,\n    factor = 2,\n    retryCondition = () => true,\n  } = options\n\n  let lastError: Error = new Error('Unknown error occurred')\n\n  let delay = initialDelay\n\n  for (let attempt = 0; attempt <= maxRetries; attempt++) {\n    try {\n      return await fn()\n    } catch (error) {\n      if (attempt === maxRetries || !retryCondition(error as Error)) {\n        throw error\n      }\n\n      lastError = error as Error\n\n      // Wait for the specified delay\n      await new Promise((resolve) => setTimeout(resolve, delay))\n\n      // Increase the delay for the next attempt (exponential backoff)\n      delay = Math.min(delay * factor, maxDelay)\n    }\n  }\n\n  throw lastError\n}\n"
  },
  {
    "path": "frontend/interface/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"target\": \"ESNext\",\n    \"useDefineForClassFields\": true,\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ESNext\"],\n    \"allowJs\": false,\n    \"skipLibCheck\": true,\n    \"esModuleInterop\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"strict\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"preserve\",\n    \"composite\": true,\n    \"declaration\": true,\n    \"paths\": {\n      \"@/*\": [\"./src/*\"],\n    },\n    \"outDir\": \"./dist\",\n    \"sourceMap\": true,\n  },\n  \"include\": [\"src\"],\n}\n"
  },
  {
    "path": "frontend/nyanpasu/.gitignore",
    "content": ".tanstack\n"
  },
  {
    "path": "frontend/nyanpasu/.vscode/extensions.json",
    "content": "{\n  \"recommendations\": [\"inlang.vs-code-extension\"]\n}\n"
  },
  {
    "path": "frontend/nyanpasu/auto-imports.d.ts",
    "content": "/* eslint-disable */\n/* prettier-ignore */\n// @ts-nocheck\n// noinspection JSUnusedGlobalSymbols\n// Generated by unplugin-auto-import\n// biome-ignore lint: disable\nexport {}\ndeclare global {\n\n}\n"
  },
  {
    "path": "frontend/nyanpasu/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <link\n      rel=\"shortcut icon\"\n      href=\"./assets/image/logo.ico\"\n      type=\"image/x-icon\"\n    />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title><%- title %></title>\n    <%- injectScript %>\n    <script>\n      ;(function () {\n        var _matchMedia = window.matchMedia\n        window.matchMedia = function () {\n          var v = _matchMedia.apply(null, arguments)\n          if (!v.addEventListener) {\n            v.addEventListener = function () {\n              if (arguments.length < 2 || arguments[0] !== 'change') {\n                console.error('Cannot proxy addEventListener:', arguments)\n                return\n              }\n              if (arguments.length > 2) {\n                console.warn('Proxy addEventListener:', arguments)\n              }\n              v.addListener(arguments[1])\n            }\n          }\n          return v\n        }\n      })()\n    </script>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/src/main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "frontend/nyanpasu/messages/en.json",
    "content": "{\n  \"$schema\": \"https://inlang.com/schema/inlang-message-format\",\n  \"language\": \"English\",\n  \"navbar_label_dashboard\": \"Dashboard\",\n  \"navbar_label_proxies\": \"Proxies\",\n  \"navbar_label_profiles\": \"Profiles\",\n  \"navbar_label_connections\": \"Connections\",\n  \"navbar_label_logs\": \"Logs\",\n  \"navbar_label_rules\": \"Rules\",\n  \"navbar_label_settings\": \"Settings\",\n  \"navbar_label_providers\": \"Providers\",\n  \"header_help_action_title\": \"Help\",\n  \"header_help_action_wiki\": \"Official Wiki\",\n  \"header_help_action_issues\": \"Report Issue\",\n  \"header_help_action_collect_logs\": \"Collect Logs\",\n  \"header_help_action_about\": \"About\",\n  \"header_settings_action_title\": \"Settings\",\n  \"header_settings_action_language\": \"Language\",\n  \"header_settings_action_theme_mode\": \"Theme Mode\",\n  \"header_file_action_title\": \"File\",\n  \"header_file_action_import_local_profile\": \"Import Local Profile\",\n  \"settings_system_proxy_title\": \"System Settings\",\n  \"settings_system_proxy_proxy_mode_label\": \"Proxy Mode\",\n  \"settings_system_proxy_system_proxy_label\": \"System Proxy\",\n  \"settings_system_proxy_tun_mode_label\": \"TUN Mode\",\n  \"settings_system_proxy_proxy_guard_label\": \"Proxy Guard\",\n  \"settings_system_proxy_proxy_guard_switch_label\": \"System Proxy Guard\",\n  \"settings_system_proxy_proxy_guard_switch_description\": \"After enabling, the system proxy will automatically detect the proxy settings and automatically modify them to the settings in the program\",\n  \"settings_system_proxy_proxy_guard_interval_label\": \"System Proxy Guard Interval\",\n  \"settings_system_proxy_proxy_bypass_label\": \"System Proxy Bypass\",\n  \"settings_system_proxy_current_system_proxy_label\": \"Current System Proxy\",\n  \"settings_system_proxy_service_mode_label\": \"Service Mode\",\n  \"settings_system_proxy_service_mode_description\": \"Service is used to manage the Clash core, to achieve minimum permission acquisition, only providing the necessary permissions to the core, for scenarios such as the TUN mode.\",\n  \"settings_system_proxy_service_mode_disabled_tooltip\": \"To enable service mode, make sure the Clash Nyanpasu service is installed and started\",\n  \"settings_system_proxy_system_service_ctrl_label\": \"System Service\",\n  \"settings_system_proxy_system_service_ctrl_detail\": \"Service Detail\",\n  \"settings_system_proxy_system_service_ctrl_install\": \"Install\",\n  \"settings_system_proxy_system_service_ctrl_uninstall\": \"Uninstall\",\n  \"settings_system_proxy_system_service_ctrl_failed_install\": \"Install failed\",\n  \"settings_system_proxy_system_service_ctrl_failed_uninstall\": \"Uninstall failed\",\n  \"settings_system_proxy_system_service_ctrl_prompt\": \"Service Prompt\",\n  \"settings_system_proxy_system_service_ctrl_manual_prompt\": \"Service Manual Tips\",\n  \"settings_system_proxy_system_service_ctrl_manual_operation_prompt\": \"Unable to control the service automatically. Please navigate to the core directory, run PowerShell as administrator on Windows or a terminal emulator on macOS/Linux, and execute the following commands:\",\n  \"settings_system_proxy_system_service_ctrl_start\": \"Start\",\n  \"settings_system_proxy_system_service_ctrl_stop\": \"Stop\",\n  \"settings_system_proxy_launch_label\": \"Launch Settings\",\n  \"settings_system_proxy_auto_launch_label\": \"Auto Launch\",\n  \"settings_system_proxy_silent_start_label\": \"Silent Start\",\n  \"settings_system_proxy_windows_tools_label\": \"Windows Tools\",\n  \"settings_system_proxy_uwp_tools_label\": \"UWP Loopback Tools\",\n  \"settings_system_proxy_uwp_tools_description\": \"Used to solve the problem of Windows UWP applications not being able to access the network through the local proxy\",\n  \"settings_user_interface_title\": \"User Interface\",\n  \"settings_user_interface_language_group\": \"Language Settings\",\n  \"settings_user_interface_language_label\": \"Language\",\n  \"settings_user_interface_theme_mode_group\": \"Theme Settings\",\n  \"settings_user_interface_theme_mode_label\": \"Theme Mode\",\n  \"settings_user_interface_theme_mode_light\": \"Light\",\n  \"settings_user_interface_theme_mode_dark\": \"Dark\",\n  \"settings_user_interface_theme_mode_system\": \"System\",\n  \"settings_user_interface_theme_color_label\": \"Theme Color\",\n  \"settings_user_interface_theme_color_custom\": \"Custom\",\n  \"settings_clash_settings_title\": \"Clash Settings\",\n  \"settings_clash_settings_allow_lan_label\": \"Allow LAN\",\n  \"settings_clash_settings_ipv6_label\": \"Enable IPv6\",\n  \"settings_clash_settings_tun_stack_label\": \"TUN Stack\",\n  \"settings_clash_settings_log_level_label\": \"Log Level\",\n  \"settings_clash_settings_port_label\": \"Port Settings\",\n  \"settings_clash_settings_mixed_port_label\": \"Mixed Port\",\n  \"settings_clash_settings_random_port_label\": \"Random Port\",\n  \"settings_clash_settings_random_port_enabled\": \"Random port enabled, after restart to take effect.\",\n  \"settings_clash_settings_random_port_disabled\": \"Random port disabled, after restart to take effect.\",\n  \"settings_clash_settings_external_controll_label\": \"External Controller\",\n  \"settings_clash_settings_port_strategy_label\": \"Port Strategy\",\n  \"settings_clash_settings_allow_fallback_label\": \"Allow Fallback\",\n  \"settings_clash_settings_fixed_label\": \"Fixed\",\n  \"settings_clash_settings_random_label\": \"Random\",\n  \"settings_clash_settings_core_secret_label\": \"API Secret\",\n  \"settings_clash_settings_field_filter_label\": \"Clash Field Filter\",\n  \"settings_clash_settings_field_filter_nyanpasu_control_fields\": \"Nyanpasu Control Fields\",\n  \"settings_web_ui_title\": \"Web UI\",\n  \"settings_web_ui_add_button\": \"Add new\",\n  \"settings_web_ui_empty_item\": \"No records found, try adding one\",\n  \"settings_web_ui_input_label\": \"Enter HTTP address\",\n  \"settings_web_ui_replace_with_label\": \"Replace host, port and secret with\",\n  \"settings_web_ui_preview_title\": \"Preview\",\n  \"settings_clash_core_manager_card_title\": \"Core Manager\",\n  \"settings_clash_core_manager_card_loading\": \"Executing operation...\",\n  \"settings_clash_core_manager_card_loading_error\": \"Execution failed, please check the log\",\n  \"settings_clash_core_manager_card_loading_success\": \"Execution successful\",\n  \"settings_clash_core_manager_card_restart_sidecar\": \"Restart Core\",\n  \"settings_clash_core_manager_card_restart_sidecar_error\": \"Restart core failed, please check the log\",\n  \"settings_clash_core_manager_card_restart_sidecar_success\": \"Restart core successfully\",\n  \"settings_clash_core_manager_card_fetch_remote\": \"Check Updates\",\n  \"settings_clash_core_manager_card_click_to_update\": \"Click to update\",\n  \"settings_clash_core_manager_card_decompressing\": \"Decompressing...\",\n  \"settings_clash_core_manager_card_replacing\": \"Replacing...\",\n  \"settings_clash_core_manager_card_restarting\": \"Restarting...\",\n  \"settings_clash_core_manager_card_done\": \"Done\",\n  \"settings_debug_utils_open_config_directory\": \"Open Config Directory\",\n  \"settings_debug_utils_open_data_directory\": \"Open Data Directory\",\n  \"settings_debug_utils_open_core_directory\": \"Open Core Directory\",\n  \"settings_debug_utils_open_log_directory\": \"Open Log Directory\",\n  \"settings_nyanpasu_max_log_files_label\": \"Max Log Files\",\n  \"settings_label_system\": \"System Settings\",\n  \"settings_label_system_description\": \"Proxy mode, proxy bypass, auto start, silent start and more.\",\n  \"settings_label_user_interface\": \"User Interface\",\n  \"settings_label_user_interface_description\": \"Language, theme mode, theme color and more.\",\n  \"settings_label_clash_settings\": \"Clash Settings\",\n  \"settings_label_clash_settings_description\": \"Clash configuration, log level, mixed port, random port and more.\",\n  \"settings_label_external_controll\": \"Clash External Control\",\n  \"settings_label_external_controll_description\": \"Web UI address, port strategy, API key and more.\",\n  \"settings_label_nyanpasu\": \"Nyanpasu Settings\",\n  \"settings_label_nyanpasu_description\": \"Nyanpasu specific settings\",\n  \"settings_label_debug\": \"Debug Tools\",\n  \"settings_label_debug_description\": \"Debug tools and more.\",\n  \"settings_label_about\": \"About\",\n  \"settings_label_about_description\": \"About Clash Nyanpasu\",\n  \"settings_label_about_update\": \"Check Update\",\n  \"settings_label_about_auto_check_updates\": \"Auto Check Updates\",\n  \"settings_label_about_update_to_github_releases\": \"Visit GitHub Releases\",\n  \"settings_label_about_version\": \"Version: v{version}\",\n  \"settings_label_about_update_has_new_version\": \"A new version is available\",\n  \"settings_label_about_update_no_update\": \"The current version is the latest version, no update information found\",\n  \"settings_label_about_update_to_update_button\": \"Download and Install\",\n  \"settings_label_about_update_installing\": \"Installing...\",\n  \"profile_subscription_title\": \"Subscription\",\n  \"profile_subscription_updated_at\": \"{updated} updated\",\n  \"profile_subscription_next_update_at\": \"Next update at {next} update\",\n  \"profile_subscription_expires_in\": \"{expires} expires\",\n  \"profile_subscription_update\": \"Update\",\n  \"profile_base_info_title\": \"Basic Info\",\n  \"profile_name_editor_title\": \"Edit Name\",\n  \"profile_name_label\": \"Profile Name\",\n  \"profile_update_option_edit\": \"Sub. Opts\",\n  \"profile_update_option_editor_title\": \"Edit Subscription Options\",\n  \"profile_user_agent_label\": \"User Agent (UA)\",\n  \"profile_with_proxy_label\": \"Use System Proxy\",\n  \"profile_self_proxy_label\": \"Use Clash Proxy\",\n  \"profile_update_interval_label\": \"Auto Update Interval (minutes)\",\n  \"profile_subscription_url_editor_label\": \"Edit Sub. URL\",\n  \"profile_subscription_url_label\": \"Subscription URL\",\n  \"profile_delete_title\": \"Delete Profile\",\n  \"profile_delete_description\": \"This action cannot be reverted. Are you sure you want to delete this profile?\",\n  \"profile_view_content_title\": \"Profile Content\",\n  \"profile_pending_mask_message\": \"Executing operation…\",\n  \"profile_active_title\": \"Activate Profile\",\n  \"profile_is_active_description\": \"The current profile is already active, no need to repeat the operation.\",\n  \"profile_active_title_success\": \"Profile {name} activated successfully!\",\n  \"profile_active_title_error\": \"Profile {name} activation failed, please check the profile or proxy chain is correct\",\n  \"profile_open_locally_title\": \"Open Profile File\",\n  \"profile_chain_editor_active_column\": \"Active Proxy Chain\",\n  \"profile_chain_editor_inactive_column\": \"Inactive Proxy Chain\",\n  \"profile_chain_editor_apply_message\": \"Applying proxy chain…\",\n  \"profile_quick_import_placeholder\": \"Enter URL or paste link to quickly import profile\",\n  \"profile_quick_import_success_message\": \"Profile imported successfully, please check the list\",\n  \"profile_view_details_title\": \"Profile Details\",\n  \"profile_no_more_profiles\": \"No more profiles 0.0\",\n  \"profile_import_title\": \"Import Profile\",\n  \"profile_import_remote_title\": \"Remote Profile\",\n  \"profile_import_local_title\": \"Local Profile\",\n  \"profile_empty_list_message\": \"No profiles found, please try importing or creating a profile.\",\n  \"profile_import_remote_url_label\": \"Remote Profile URL\",\n  \"profile_profile_label\": \"Profiles\",\n  \"profile_javascript_label\": \"JavaScript scripts\",\n  \"profile_lua_label\": \"Lua scripts\",\n  \"profile_merge_label\": \"Merge (YAML)\",\n  \"profile_profile_label_count\": \"{count} profiles\",\n  \"profile_form_name_label\": \"Name\",\n  \"profile_form_desc_label\": \"Description\",\n  \"profile_form_url_label\": \"Subscription URL\",\n  \"profile_form_option_label\": \"Profile Options\",\n  \"profile_form_option_user_agent_label\": \"User Agent (UA)\",\n  \"profile_form_option_with_proxy_label\": \"Use System Proxy\",\n  \"profile_form_option_self_proxy_label\": \"Use Clash Proxy\",\n  \"profile_form_option_update_interval_label\": \"Auto Update Interval (minutes)\",\n  \"profile_import_local_file_placeholder\": \"Click or drag file here to import\",\n  \"profile_import_local_file_type_label\": \"Supported files: {types}\",\n  \"profile_import_local_file_size_label\": \"File size: {size}\",\n  \"profile_import_chain_title\": \"New {type}\",\n  \"proxies_group_delay_test_title\": \"Delay Test\",\n  \"proxies_group_delay_test_pending_title\": \"Testing…\",\n  \"proxies_group_empty_message\": \"No proxy groups found, please try switching profiles\",\n  \"proxies_group_empty_button_text\": \"Switch to profiles\",\n  \"profile_is_active_label\": \"Current Profile\",\n  \"profile_remote_label\": \"Remote Profile\",\n  \"profile_local_label\": \"Local Profile\",\n  \"logs_search_placeholder\": \"Search logs (time, type, or message)...\",\n  \"logs_empty_message\": \"No logs recorded\",\n  \"logs_action_clear_log\": \"Clear Logs\",\n  \"rules_list_all_proxies\": \"All Groups\",\n  \"connections_all_connections\": \"All Connections\",\n  \"connections_search_placeholder\": \"Search connections (host, process, rule, chains...)...\",\n  \"connections_close_all_connections\": \"Close All Connections\",\n  \"connections_empty_message\": \"No connections found\",\n  \"connections_close_connection\": \"Close Connection\",\n  \"connections_view_details\": \"Details\",\n  \"providers_proxies_title\": \"Proxy Groups\",\n  \"providers_rules_title\": \"Rule Sets\",\n  \"providers_no_proxies_message\": \"Current profile does not have any proxy groups\",\n  \"providers_no_rules_message\": \"Current profile does not have any rule sets\",\n  \"providers_proxies_proxy_count_label\": \"{count} Proxies\",\n  \"providers_rules_rule_count_label\": \"{count} Rules\",\n  \"providers_info_title\": \"Resource Info\",\n  \"providers_subscription_title\": \"Subscription Info\",\n  \"providers_update_provider\": \"Update\",\n  \"editor_before_close_message\": \"You have not saved the edited content, are you sure you want to close the editor?\",\n  \"editor_validate_error_message\": \"Please fix the error before saving content\",\n  \"editor_read_only_chip\": \"Read Only\",\n  \"unit_seconds\": \"s\",\n  \"common_submit\": \"Submit\",\n  \"common_cancel\": \"Cancel\",\n  \"common_apply\": \"Apply\",\n  \"common_reset\": \"Reset\",\n  \"common_save\": \"Save\",\n  \"common_validate\": \"Validate\",\n  \"common_close\": \"Close\",\n  \"common_copy\": \"Copy\",\n  \"common_open\": \"Open\",\n  \"common_cut\": \"Cut\",\n  \"common_paste\": \"Paste\"\n}\n"
  },
  {
    "path": "frontend/nyanpasu/messages/ru.json",
    "content": "{\n  \"$schema\": \"https://inlang.com/schema/inlang-message-format\",\n  \"language\": \"Русский\",\n  \"navbar_label_dashboard\": \"Панель управления\",\n  \"navbar_label_proxies\": \"Прокси\",\n  \"navbar_label_profiles\": \"Профили\",\n  \"navbar_label_connections\": \"Соединения\",\n  \"navbar_label_logs\": \"Журналы\",\n  \"navbar_label_rules\": \"Правила\",\n  \"navbar_label_settings\": \"Настройки\",\n  \"navbar_label_providers\": \"Поставщики\",\n  \"header_help_action_title\": \"Помощь\",\n  \"header_help_action_wiki\": \"Онлайн-документация\",\n  \"header_help_action_issues\": \"Сообщить о проблеме\",\n  \"header_help_action_collect_logs\": \"Собрать журналы\",\n  \"header_help_action_about\": \"О программе\",\n  \"header_settings_action_title\": \"Настройки\",\n  \"header_settings_action_language\": \"Язык\",\n  \"header_settings_action_theme_mode\": \"Режим темы\",\n  \"header_file_action_title\": \"Файл\",\n  \"header_file_action_import_local_profile\": \"Импорт локального профиля\",\n  \"settings_system_proxy_title\": \"Системный прокси\",\n  \"settings_system_proxy_proxy_mode_label\": \"Режим прокси\",\n  \"settings_system_proxy_system_proxy_label\": \"Системный прокси\",\n  \"settings_system_proxy_tun_mode_label\": \"TUN режим\",\n  \"settings_system_proxy_proxy_guard_label\": \"Охрана прокси\",\n  \"settings_system_proxy_proxy_guard_switch_label\": \"Системный прокси\",\n  \"settings_system_proxy_proxy_guard_switch_description\": \"После включения, системный прокси будет автоматически обнаруживать настройки прокси и автоматически изменять их на настройки в программе\",\n  \"settings_system_proxy_proxy_guard_interval_label\": \"Интервал охраны прокси\",\n  \"settings_system_proxy_proxy_bypass_label\": \"Обход прокси\",\n  \"settings_system_proxy_current_system_proxy_label\": \"Текущий системный прокси\",\n  \"settings_system_proxy_service_mode_label\": \"Режим службы\",\n  \"settings_system_proxy_service_mode_description\": \"Служба используется для управления ядром, чтобы достичь минимального получения прав, только предоставляя ядру необходимые права, для сценариев, таких как режим TUN.\",\n  \"settings_system_proxy_service_mode_disabled_tooltip\": \"Чтобы включить режим службы, убедитесь, что служба Clash Nyanpasu установлена и запущена\",\n  \"settings_system_proxy_system_service_ctrl_label\": \"Системный сервис\",\n  \"settings_system_proxy_system_service_ctrl_detail\": \"Подробности сервиса\",\n  \"settings_system_proxy_system_service_ctrl_install\": \"Установить\",\n  \"settings_system_proxy_system_service_ctrl_uninstall\": \"Удалить\",\n  \"settings_system_proxy_system_service_ctrl_failed_install\": \"Установка не удалась\",\n  \"settings_system_proxy_system_service_ctrl_failed_uninstall\": \"Удаление не удалось\",\n  \"settings_system_proxy_system_service_ctrl_prompt\": \"Подсказка по сервису\",\n  \"settings_system_proxy_system_service_ctrl_manual_prompt\": \"Руководство по ручному управлению\",\n  \"settings_system_proxy_system_service_ctrl_manual_operation_prompt\": \"Не удалось автоматически управлять сервисом. Пожалуйста, перейдите в каталог ядра, запустите PowerShell как администратор на Windows или эмулятор терминала на macOS/Linux и выполните следующие команды:\",\n  \"settings_system_proxy_system_service_ctrl_start\": \"Запустить\",\n  \"settings_system_proxy_system_service_ctrl_stop\": \"Остановить\",\n  \"settings_system_proxy_launch_label\": \"Настройки запуска\",\n  \"settings_system_proxy_auto_launch_label\": \"Автозапуск\",\n  \"settings_system_proxy_silent_start_label\": \"Тихий запуск\",\n  \"settings_system_proxy_windows_tools_label\": \"Инструменты Windows\",\n  \"settings_system_proxy_uwp_tools_label\": \"Инструменты UWP Loopback\",\n  \"settings_system_proxy_uwp_tools_description\": \"Используется для решения проблемы, когда Windows UWP приложения не могут получить доступ к сети через локальный прокси\",\n  \"settings_user_interface_title\": \"Интерфейс пользователя\",\n  \"settings_user_interface_language_group\": \"Настройки языка\",\n  \"settings_user_interface_language_label\": \"Язык\",\n  \"settings_user_interface_theme_mode_group\": \"Настройки темы\",\n  \"settings_user_interface_theme_mode_label\": \"Режим темы\",\n  \"settings_user_interface_theme_mode_light\": \"Светлый\",\n  \"settings_user_interface_theme_mode_dark\": \"Темный\",\n  \"settings_user_interface_theme_mode_system\": \"Системный\",\n  \"settings_user_interface_theme_color_label\": \"Цвет темы\",\n  \"settings_user_interface_theme_color_custom\": \"Пользовательский\",\n  \"settings_clash_settings_title\": \"Настройки Clash\",\n  \"settings_clash_settings_allow_lan_label\": \"Разрешить LAN\",\n  \"settings_clash_settings_ipv6_label\": \"Включить IPv6\",\n  \"settings_clash_settings_tun_stack_label\": \"Стек TUN\",\n  \"settings_clash_settings_log_level_label\": \"Уровень журнала\",\n  \"settings_clash_settings_port_label\": \"Настройки порта\",\n  \"settings_clash_settings_mixed_port_label\": \"Смешанный порт\",\n  \"settings_clash_settings_random_port_label\": \"Случайный порт\",\n  \"settings_clash_settings_random_port_enabled\": \"Случайный порт включен, после перезапуска для вступления в силу.\",\n  \"settings_clash_settings_random_port_disabled\": \"Случайный порт отключен, после перезапуска для вступления в силу.\",\n  \"settings_clash_settings_external_controll_label\": \"Внешнее управление\",\n  \"settings_clash_settings_port_strategy_label\": \"Стратегия порта\",\n  \"settings_clash_settings_allow_fallback_label\": \"Разрешить откат\",\n  \"settings_clash_settings_fixed_label\": \"Фиксированный\",\n  \"settings_clash_settings_random_label\": \"Случайный\",\n  \"settings_clash_settings_core_secret_label\": \"API ключ\",\n  \"settings_clash_settings_field_filter_label\": \"Фильтр полей Clash\",\n  \"settings_clash_settings_field_filter_nyanpasu_control_fields\": \"Управляющие поля Nyanpasu\",\n  \"settings_web_ui_title\": \"Web UI\",\n  \"settings_web_ui_add_button\": \"Добавить\",\n  \"settings_web_ui_empty_item\": \"Не найдено записей, попробуйте добавить одну\",\n  \"settings_web_ui_input_label\": \"Введите HTTP-адрес\",\n  \"settings_web_ui_replace_with_label\": \"Заменить хост, порт и секрет на\",\n  \"settings_web_ui_preview_title\": \"Предварительный просмотр\",\n  \"settings_clash_core_manager_card_title\": \"Менеджер ядра\",\n  \"settings_clash_core_manager_card_loading\": \"Выполнение операции...\",\n  \"settings_clash_core_manager_card_loading_error\": \"Выполнение операции не удалось, пожалуйста, проверьте журнал\",\n  \"settings_clash_core_manager_card_loading_success\": \"Выполнение операции успешно\",\n  \"settings_clash_core_manager_card_restart_sidecar\": \"Перезапустить ядро\",\n  \"settings_clash_core_manager_card_restart_sidecar_error\": \"Перезапуск ядра не удалось, пожалуйста, проверьте журнал\",\n  \"settings_clash_core_manager_card_restart_sidecar_success\": \"Перезапуск ядра успешно\",\n  \"settings_clash_core_manager_card_fetch_remote\": \"Проверить обновления\",\n  \"settings_clash_core_manager_card_click_to_update\": \"Нажмите для обновления\",\n  \"settings_clash_core_manager_card_decompressing\": \"Распаковка...\",\n  \"settings_clash_core_manager_card_replacing\": \"Замена...\",\n  \"settings_clash_core_manager_card_restarting\": \"Перезапуск...\",\n  \"settings_clash_core_manager_card_done\": \"Готово\",\n  \"settings_debug_utils_open_config_directory\": \"Открыть конфигурационную директорию\",\n  \"settings_debug_utils_open_data_directory\": \"Открыть директорию данных\",\n  \"settings_debug_utils_open_core_directory\": \"Открыть директорию ядра\",\n  \"settings_debug_utils_open_log_directory\": \"Открыть директорию журналов\",\n  \"settings_nyanpasu_max_log_files_label\": \"Максимальное количество файлов журнала\",\n  \"settings_label_system\": \"Системные настройки\",\n  \"settings_label_system_description\": \"Режим прокси, обход прокси, автозапуск, старт без отображения окна и т.д.\",\n  \"settings_label_user_interface\": \"Интерфейс пользователя\",\n  \"settings_label_user_interface_description\": \"Язык, режим темы, цвет темы и т.д.\",\n  \"settings_label_clash_settings\": \"Настройки Clash\",\n  \"settings_label_clash_settings_description\": \"Конфигурация Clash, уровень логирования, смешанный порт, случайный порт и т.д.\",\n  \"settings_label_external_controll\": \"Внешнее управление Clash\",\n  \"settings_label_external_controll_description\": \"Адрес Web UI, стратегия порта, API ключ и т.д.\",\n  \"settings_label_nyanpasu\": \"Настройки Nyanpasu\",\n  \"settings_label_nyanpasu_description\": \"Специфические настройки Nyanpasu\",\n  \"settings_label_debug\": \"Отладочные инструменты\",\n  \"settings_label_debug_description\": \"Отладочные инструменты и т.д.\",\n  \"settings_label_about\": \"О программе\",\n  \"settings_label_about_description\": \"О Clash Nyanpasu\",\n  \"settings_label_about_update\": \"Проверить обновление\",\n  \"settings_label_about_auto_check_updates\": \"Автоматически проверять обновления\",\n  \"settings_label_about_update_to_github_releases\": \"Перейти на GitHub Releases\",\n  \"settings_label_about_version\": \"Версия: v{version}\",\n  \"settings_label_about_update_has_new_version\": \"Доступна новая версия\",\n  \"settings_label_about_update_no_update\": \"Текущая версия является последней версией, не найдено информации об обновлении\",\n  \"settings_label_about_update_to_update_button\": \"Скачать и установить\",\n  \"settings_label_about_update_installing\": \"Установка...\",\n  \"profile_subscription_title\": \"Информация о подписке\",\n  \"profile_subscription_updated_at\": \"{updated} обновлено\",\n  \"profile_subscription_next_update_at\": \"Следующее обновление: {next}\",\n  \"profile_subscription_expires_in\": \"{expires} истекает\",\n  \"profile_subscription_update\": \"Обновить\",\n  \"profile_base_info_title\": \"Основная информация\",\n  \"profile_name_editor_title\": \"Редактировать название\",\n  \"profile_name_label\": \"Название профиля\",\n  \"profile_update_option_edit\": \"Опции подписки\",\n  \"profile_update_option_editor_title\": \"Редактировать опции подписки\",\n  \"profile_user_agent_label\": \"Пользовательский агент (UA)\",\n  \"profile_with_proxy_label\": \"Использовать системный прокси\",\n  \"profile_self_proxy_label\": \"Использовать прокси Clash\",\n  \"profile_update_interval_label\": \"Интервал автоматического обновления (минуты)\",\n  \"profile_subscription_url_editor_label\": \"Редактировать URL подписки\",\n  \"profile_subscription_url_label\": \"URL подписки\",\n  \"profile_delete_title\": \"Удалить профиль\",\n  \"profile_delete_description\": \"Это действие не может быть отменено. Вы уверены, что хотите удалить этот профиль?\",\n  \"profile_view_content_title\": \"Содержимое профиля\",\n  \"profile_pending_mask_message\": \"Выполнение операции…\",\n  \"profile_active_title\": \"Активировать профиль\",\n  \"profile_is_active_description\": \"Текущий профиль уже активирован, повторная активация не требуется.\",\n  \"profile_active_title_success\": \"Профиль {name} успешно активирован!\",\n  \"profile_active_title_error\": \"Активация профиля {name} не удалась, пожалуйста, проверьте профиль или цепочку прокси\",\n  \"profile_open_locally_title\": \"Открыть файл профиля\",\n  \"profile_chain_editor_active_column\": \"Активный профиль\",\n  \"profile_chain_editor_inactive_column\": \"Неактивный профиль\",\n  \"profile_chain_editor_apply_message\": \"Применение цепочки прокси…\",\n  \"profile_quick_import_placeholder\": \"Введите URL или вставьте ссылку для быстрого импорта профиля\",\n  \"profile_quick_import_success_message\": \"Профиль успешно импортирован, пожалуйста, проверьте список\",\n  \"profile_view_details_title\": \"Подробности профиля\",\n  \"profile_no_more_profiles\": \"Нет больше профилей 0.0\",\n  \"profile_import_title\": \"Импорт профиля\",\n  \"profile_import_remote_title\": \"Удаленный профиль\",\n  \"profile_import_local_title\": \"Локальный профиль\",\n  \"profile_empty_list_message\": \"Не найдено ни одного профиля, пожалуйста, попробуйте импортировать или создать профиль.\",\n  \"profile_import_remote_url_label\": \"URL удаленного профиля\",\n  \"profile_profile_label\": \"Профили\",\n  \"profile_javascript_label\": \"JavaScript скрипты\",\n  \"profile_lua_label\": \"Lua скрипты\",\n  \"profile_merge_label\": \"Слияние скриптов (YAML)\",\n  \"profile_profile_label_count\": \"{count} профилей\",\n  \"profile_form_name_label\": \"Название\",\n  \"profile_form_desc_label\": \"Описание\",\n  \"profile_form_url_label\": \"URL подписки\",\n  \"profile_form_option_label\": \"Опции профиля\",\n  \"profile_form_option_user_agent_label\": \"Пользовательский агент (UA)\",\n  \"profile_form_option_with_proxy_label\": \"Использовать системный прокси\",\n  \"profile_form_option_self_proxy_label\": \"Использовать прокси Clash\",\n  \"profile_form_option_update_interval_label\": \"Интервал автоматического обновления (минуты)\",\n  \"profile_import_local_file_placeholder\": \"Нажмите или перетащите файл сюда для импорта\",\n  \"profile_import_local_file_type_label\": \"Поддерживаемые файлы: {types}\",\n  \"profile_import_local_file_size_label\": \"Размер файла: {size}\",\n  \"profile_import_chain_title\": \"Новый {type}\",\n  \"proxies_group_delay_test_title\": \"Тест задержки\",\n  \"proxies_group_delay_test_pending_title\": \"Тестирование…\",\n  \"proxies_group_empty_message\": \"Не найдено ни одной группы прокси, пожалуйста, попробуйте переключить профили\",\n  \"proxies_group_empty_button_text\": \"Перейти к профилям\",\n  \"profile_is_active_label\": \"Текущий профиль\",\n  \"profile_remote_label\": \"Удаленный профиль\",\n  \"profile_local_label\": \"Локальный профиль\",\n  \"logs_search_placeholder\": \"Поиск журналов (время, тип или сообщение)...\",\n  \"logs_empty_message\": \"Нет записей в журналах\",\n  \"logs_action_clear_log\": \"Очистить журналы\",\n  \"rules_list_all_proxies\": \"Все группы\",\n  \"connections_all_connections\": \"Все соединения\",\n  \"connections_search_placeholder\": \"Поиск соединений (хост, процесс, правило, цепочки)...\",\n  \"connections_close_all_connections\": \"Закрыть все соединения\",\n  \"connections_empty_message\": \"Не найдено ни одного соединения\",\n  \"connections_close_connection\": \"Закрыть соединение\",\n  \"connections_view_details\": \"Просмотреть детали\",\n  \"providers_proxies_title\": \"Группы прокси\",\n  \"providers_rules_title\": \"Наборы правил\",\n  \"providers_no_proxies_message\": \"Текущий профиль не имеет групп прокси\",\n  \"providers_no_rules_message\": \"Текущий профиль не имеет наборов правил\",\n  \"providers_proxies_proxy_count_label\": \"{count} прокси\",\n  \"providers_rules_rule_count_label\": \"{count} правил\",\n  \"providers_info_title\": \"Информация о ресурсах\",\n  \"providers_subscription_title\": \"Информация о подписке\",\n  \"providers_update_provider\": \"Обновить ресурсы\",\n  \"editor_before_close_message\": \"Вы не сохранили измененное содержимое, вы уверены, что хотите закрыть редактор?\",\n  \"editor_validate_error_message\": \"Пожалуйста, исправьте ошибки перед сохранением содержимого\",\n  \"editor_read_only_chip\": \"Только для чтения\",\n  \"unit_seconds\": \"сек.\",\n  \"common_submit\": \"Отправить\",\n  \"common_cancel\": \"Отменить\",\n  \"common_apply\": \"Применить\",\n  \"common_reset\": \"Сбросить\",\n  \"common_save\": \"Сохранить\",\n  \"common_validate\": \"Проверить\",\n  \"common_close\": \"Закрыть\",\n  \"common_copy\": \"Копировать\",\n  \"common_open\": \"Открыть\",\n  \"common_cut\": \"Вырезать\",\n  \"common_paste\": \"Вставить\"\n}\n"
  },
  {
    "path": "frontend/nyanpasu/messages/zh-cn.json",
    "content": "{\n  \"$schema\": \"https://inlang.com/schema/inlang-message-format\",\n  \"language\": \"简体中文\",\n  \"navbar_label_dashboard\": \"概览\",\n  \"navbar_label_proxies\": \"代理\",\n  \"navbar_label_profiles\": \"配置\",\n  \"navbar_label_connections\": \"连接\",\n  \"navbar_label_logs\": \"日志\",\n  \"navbar_label_rules\": \"规则\",\n  \"navbar_label_settings\": \"设置\",\n  \"navbar_label_providers\": \"资源\",\n  \"header_help_action_title\": \"帮助\",\n  \"header_help_action_wiki\": \"在线文档\",\n  \"header_help_action_issues\": \"报告问题\",\n  \"header_help_action_collect_logs\": \"收集日志\",\n  \"header_help_action_about\": \"关于\",\n  \"header_settings_action_title\": \"设置\",\n  \"header_settings_action_language\": \"语言\",\n  \"header_settings_action_theme_mode\": \"主题模式\",\n  \"header_file_action_title\": \"文件\",\n  \"header_file_action_import_local_profile\": \"导入本地配置\",\n  \"settings_system_proxy_title\": \"系统设置\",\n  \"settings_system_proxy_proxy_mode_label\": \"代理模式\",\n  \"settings_system_proxy_system_proxy_label\": \"系统代理\",\n  \"settings_system_proxy_tun_mode_label\": \"TUN 模式\",\n  \"settings_system_proxy_proxy_guard_label\": \"代理守卫\",\n  \"settings_system_proxy_proxy_guard_switch_label\": \"系统代理守卫\",\n  \"settings_system_proxy_proxy_guard_switch_description\": \"启用后，系统代理将自动检测代理设置，并自动修改为软件内设置\",\n  \"settings_system_proxy_proxy_guard_interval_label\": \"系统代理守卫间隔\",\n  \"settings_system_proxy_proxy_bypass_label\": \"系统代理绕过\",\n  \"settings_system_proxy_current_system_proxy_label\": \"当前系统代理\",\n  \"settings_system_proxy_service_mode_label\": \"服务模式\",\n  \"settings_system_proxy_service_mode_description\": \"服务用于管理内核，达到权限获取最小化，只给予内核必要的权限，用于例如 TUN 模式等需要特权权限的场景\",\n  \"settings_system_proxy_service_mode_disabled_tooltip\": \"要启用服务模式，请确保 Clash Nyanpasu 服务已安装并启动\",\n  \"settings_system_proxy_system_service_ctrl_label\": \"系统服务\",\n  \"settings_system_proxy_system_service_ctrl_detail\": \"服务详情\",\n  \"settings_system_proxy_system_service_ctrl_install\": \"安装\",\n  \"settings_system_proxy_system_service_ctrl_uninstall\": \"卸载\",\n  \"settings_system_proxy_system_service_ctrl_failed_install\": \"安装失败\",\n  \"settings_system_proxy_system_service_ctrl_failed_uninstall\": \"卸载失败\",\n  \"settings_system_proxy_system_service_ctrl_prompt\": \"服务提示\",\n  \"settings_system_proxy_system_service_ctrl_manual_prompt\": \"手动操作服务提示\",\n  \"settings_system_proxy_system_service_ctrl_manual_operation_prompt\": \"无法自动操作服务。请导航到内核所在目录，在 Windows 上以管理员身份打开 PowerShell 或在 macOS/Linux 上打开终端仿真器，然后执行以下命令：\",\n  \"settings_system_proxy_system_service_ctrl_start\": \"启动\",\n  \"settings_system_proxy_system_service_ctrl_stop\": \"停止\",\n  \"settings_system_proxy_launch_label\": \"启动设置\",\n  \"settings_system_proxy_auto_launch_label\": \"开机自启\",\n  \"settings_system_proxy_silent_start_label\": \"静默启动\",\n  \"settings_system_proxy_windows_tools_label\": \"Windows 工具\",\n  \"settings_system_proxy_uwp_tools_label\": \"UWP 回环工具\",\n  \"settings_system_proxy_uwp_tools_description\": \"用于解决 Windows UWP 应用无法通过本地代理访问网络的问题\",\n  \"settings_user_interface_title\": \"用户界面\",\n  \"settings_user_interface_language_group\": \"语言设置\",\n  \"settings_user_interface_language_label\": \"语言\",\n  \"settings_user_interface_theme_mode_group\": \"主题设置\",\n  \"settings_user_interface_theme_mode_label\": \"主题模式\",\n  \"settings_user_interface_theme_mode_light\": \"浅色\",\n  \"settings_user_interface_theme_mode_dark\": \"深色\",\n  \"settings_user_interface_theme_mode_system\": \"跟随系统\",\n  \"settings_user_interface_theme_color_label\": \"主题颜色\",\n  \"settings_user_interface_theme_color_custom\": \"自定义\",\n  \"settings_clash_settings_title\": \"Clash 设置\",\n  \"settings_clash_settings_allow_lan_label\": \"允许局域网连接\",\n  \"settings_clash_settings_ipv6_label\": \"启用 IPv6\",\n  \"settings_clash_settings_tun_stack_label\": \"TUN 堆栈\",\n  \"settings_clash_settings_log_level_label\": \"日志级别\",\n  \"settings_clash_settings_port_label\": \"端口设置\",\n  \"settings_clash_settings_mixed_port_label\": \"混合端口\",\n  \"settings_clash_settings_random_port_label\": \"随机端口\",\n  \"settings_clash_settings_random_port_enabled\": \"随机端口已启用，重启后生效。\",\n  \"settings_clash_settings_random_port_disabled\": \"随机端口已禁用，重启后生效。\",\n  \"settings_clash_settings_external_controll_label\": \"外部控制器监听地址\",\n  \"settings_clash_settings_port_strategy_label\": \"端口策略\",\n  \"settings_clash_settings_allow_fallback_label\": \"允许回退\",\n  \"settings_clash_settings_fixed_label\": \"固定\",\n  \"settings_clash_settings_random_label\": \"随机\",\n  \"settings_clash_settings_core_secret_label\": \"API 访问密钥\",\n  \"settings_clash_settings_field_filter_label\": \"Clash 字段过滤\",\n  \"settings_clash_settings_field_filter_nyanpasu_control_fields\": \"Nyanpasu 覆写控制字段\",\n  \"settings_web_ui_title\": \"Web UI\",\n  \"settings_web_ui_add_button\": \"添加\",\n  \"settings_web_ui_empty_item\": \"没有找到记录，尝试添加一个吧\",\n  \"settings_web_ui_input_label\": \"输入 HTTP 地址\",\n  \"settings_web_ui_replace_with_label\": \"替换主机、端口和密钥为\",\n  \"settings_web_ui_preview_title\": \"预览\",\n  \"settings_clash_core_manager_card_title\": \"核心管理\",\n  \"settings_clash_core_manager_card_loading\": \"正在执行操作...\",\n  \"settings_clash_core_manager_card_loading_error\": \"执行操作失败，请检查日志\",\n  \"settings_clash_core_manager_card_loading_success\": \"执行操作成功\",\n  \"settings_clash_core_manager_card_restart_sidecar\": \"重启核心\",\n  \"settings_clash_core_manager_card_restart_sidecar_error\": \"重启核心失败，请检查日志\",\n  \"settings_clash_core_manager_card_restart_sidecar_success\": \"重启核心成功\",\n  \"settings_clash_core_manager_card_fetch_remote\": \"检查更新\",\n  \"settings_clash_core_manager_card_click_to_update\": \"点击更新\",\n  \"settings_clash_core_manager_card_decompressing\": \"解压中...\",\n  \"settings_clash_core_manager_card_replacing\": \"替换中...\",\n  \"settings_clash_core_manager_card_restarting\": \"重启中...\",\n  \"settings_clash_core_manager_card_done\": \"完成\",\n  \"settings_debug_utils_open_config_directory\": \"打开配置路径\",\n  \"settings_debug_utils_open_data_directory\": \"打开数据路径\",\n  \"settings_debug_utils_open_core_directory\": \"打开内核路径\",\n  \"settings_debug_utils_open_log_directory\": \"打开日志路径\",\n  \"settings_nyanpasu_max_log_files_label\": \"最大日志文件数量\",\n  \"settings_label_system\": \"系统设置\",\n  \"settings_label_system_description\": \"代理模式、代理绕过、开机自启、静默启动等设置\",\n  \"settings_label_user_interface\": \"用户界面\",\n  \"settings_label_user_interface_description\": \"语言、主题模式、主题颜色等设置\",\n  \"settings_label_clash_settings\": \"Clash 设置\",\n  \"settings_label_clash_settings_description\": \"Clash 配置、日志级别、混合端口、随机端口等设置\",\n  \"settings_label_external_controll\": \"Web UI 与外部控制\",\n  \"settings_label_external_controll_description\": \"Web UI 地址、端口策略、API 密钥等设置\",\n  \"settings_label_nyanpasu\": \"Nyanpasu 配置\",\n  \"settings_label_nyanpasu_description\": \"Nyanpasu 特性配置\",\n  \"settings_label_debug\": \"调试工具\",\n  \"settings_label_debug_description\": \"调试工具配置\",\n  \"settings_label_about\": \"关于\",\n  \"settings_label_about_description\": \"关于 Clash Nyanpasu\",\n  \"settings_label_about_update\": \"检查更新\",\n  \"settings_label_about_auto_check_updates\": \"自动检查更新\",\n  \"settings_label_about_update_to_github_releases\": \"前往 GitHub 检查更新\",\n  \"settings_label_about_version\": \"版本: v{version}\",\n  \"settings_label_about_update_has_new_version\": \"有新版本可用\",\n  \"settings_label_about_update_no_update\": \"当前已经是最新版本，没有找到更新信息\",\n  \"settings_label_about_update_to_update_button\": \"下载并安装\",\n  \"settings_label_about_update_installing\": \"正在安装...\",\n  \"profile_subscription_title\": \"订阅信息\",\n  \"profile_subscription_updated_at\": \"{updated}更新\",\n  \"profile_subscription_next_update_at\": \"下次更新于 {next} 更新\",\n  \"profile_subscription_expires_in\": \"{expires}到期\",\n  \"profile_subscription_update\": \"更新\",\n  \"profile_base_info_title\": \"基本信息\",\n  \"profile_name_editor_title\": \"编辑名称\",\n  \"profile_name_label\": \"配置名称\",\n  \"profile_update_option_edit\": \"订阅选项\",\n  \"profile_update_option_editor_title\": \"编辑订阅选项\",\n  \"profile_user_agent_label\": \"用户代理 (UA)\",\n  \"profile_with_proxy_label\": \"使用系统代理\",\n  \"profile_self_proxy_label\": \"使用 Clash 代理\",\n  \"profile_update_interval_label\": \"自动更新间隔 (分钟)\",\n  \"profile_subscription_url_editor_label\": \"编辑订阅 URL\",\n  \"profile_subscription_url_label\": \"订阅 URL\",\n  \"profile_delete_title\": \"删除配置\",\n  \"profile_delete_description\": \"此操作无法撤销。确定要删除此配置吗？\",\n  \"profile_view_content_title\": \"查看配置内容\",\n  \"profile_pending_mask_message\": \"正在执行操作……\",\n  \"profile_active_title\": \"启用配置\",\n  \"profile_is_active_description\": \"当前配置已启用，无需重复操作。\",\n  \"profile_active_title_success\": \"配置 {name} 已成功启用！\",\n  \"profile_active_title_error\": \"配置 {name} 启用失败，请检查配置或代理链是否正确\",\n  \"profile_open_locally_title\": \"打开配置文件\",\n  \"profile_chain_editor_active_column\": \"激活的代理链\",\n  \"profile_chain_editor_inactive_column\": \"未激活的代理链\",\n  \"profile_chain_editor_apply_message\": \"正在应用代理链……\",\n  \"profile_quick_import_placeholder\": \"键入订阅 URL 或粘贴订阅链接以快速导入配置\",\n  \"profile_quick_import_success_message\": \"配置导入成功，请检查列表\",\n  \"profile_view_details_title\": \"配置详情\",\n  \"profile_no_more_profiles\": \"没有更多配置了 0.0\",\n  \"profile_import_title\": \"导入配置\",\n  \"profile_import_remote_title\": \"远程配置\",\n  \"profile_import_local_title\": \"本地配置\",\n  \"profile_empty_list_message\": \"没有找到任何配置，请尝试导入或创建配置。\",\n  \"profile_import_remote_url_label\": \"远程配置 URL\",\n  \"profile_profile_label\": \"代理配置\",\n  \"profile_javascript_label\": \"JavaScript 脚本\",\n  \"profile_lua_label\": \"Lua 脚本\",\n  \"profile_merge_label\": \"合并脚本 (YAML)\",\n  \"profile_profile_label_count\": \"{count} 个配置\",\n  \"profile_form_name_label\": \"名称\",\n  \"profile_form_desc_label\": \"描述\",\n  \"profile_form_url_label\": \"订阅 URL\",\n  \"profile_form_option_label\": \"配置选项\",\n  \"profile_form_option_user_agent_label\": \"用户代理（UA）\",\n  \"profile_form_option_with_proxy_label\": \"使用系统代理\",\n  \"profile_form_option_self_proxy_label\": \"使用 Clash 代理\",\n  \"profile_form_option_update_interval_label\": \"自动更新间隔（分钟）\",\n  \"profile_import_local_file_placeholder\": \"点击或拖拽文件到此处导入\",\n  \"profile_import_local_file_type_label\": \"支持 {types} 文件\",\n  \"profile_import_local_file_size_label\": \"文件大小: {size}\",\n  \"profile_import_chain_title\": \"新建 {type}\",\n  \"proxies_group_delay_test_title\": \"延迟测试\",\n  \"proxies_group_delay_test_pending_title\": \"正在测试中……\",\n  \"proxies_group_empty_message\": \"没有找到任何代理组，请尝试切换配置\",\n  \"proxies_group_empty_button_text\": \"切换到配置页面\",\n  \"profile_is_active_label\": \"当前配置\",\n  \"profile_remote_label\": \"远程配置\",\n  \"profile_local_label\": \"本地配置\",\n  \"logs_search_placeholder\": \"通过时间、类型或消息等等搜索日志\",\n  \"logs_empty_message\": \"没有任何日志记录\",\n  \"logs_action_clear_log\": \"清空日志\",\n  \"rules_list_all_proxies\": \"所有分组\",\n  \"connections_all_connections\": \"所有连接\",\n  \"connections_search_placeholder\": \"通过进程、类型、地址等信息搜索连接\",\n  \"connections_close_all_connections\": \"关闭所有连接\",\n  \"connections_empty_message\": \"没有找到任何连接\",\n  \"connections_close_connection\": \"关闭连接\",\n  \"connections_view_details\": \"查看详情\",\n  \"providers_proxies_title\": \"代理集\",\n  \"providers_rules_title\": \"规则集\",\n  \"providers_no_proxies_message\": \"当前配置没有任何代理集\",\n  \"providers_no_rules_message\": \"当前配置没有任何规则集\",\n  \"providers_proxies_proxy_count_label\": \"{count}个节点\",\n  \"providers_rules_rule_count_label\": \"{count}个规则\",\n  \"providers_info_title\": \"资源信息\",\n  \"providers_subscription_title\": \"订阅信息\",\n  \"providers_update_provider\": \"更新资源\",\n  \"editor_before_close_message\": \"你尚未保存编辑的内容，确定要关闭编辑器吗？\",\n  \"editor_validate_error_message\": \"请修复错误后再保存内容\",\n  \"editor_read_only_chip\": \"只读\",\n  \"unit_seconds\": \"秒\",\n  \"common_submit\": \"提交\",\n  \"common_cancel\": \"取消\",\n  \"common_apply\": \"应用\",\n  \"common_reset\": \"重置\",\n  \"common_save\": \"保存\",\n  \"common_validate\": \"验证\",\n  \"common_close\": \"关闭\",\n  \"common_copy\": \"复制\",\n  \"common_open\": \"打开\",\n  \"common_cut\": \"剪切\",\n  \"common_paste\": \"粘贴\"\n}\n"
  },
  {
    "path": "frontend/nyanpasu/messages/zh-tw.json",
    "content": "{\n  \"$schema\": \"https://inlang.com/schema/inlang-message-format\",\n  \"language\": \"繁體中文\",\n  \"navbar_label_dashboard\": \"概覽\",\n  \"navbar_label_proxies\": \"代理組\",\n  \"navbar_label_profiles\": \"配置\",\n  \"navbar_label_connections\": \"連接\",\n  \"navbar_label_logs\": \"日誌\",\n  \"navbar_label_rules\": \"規則\",\n  \"navbar_label_settings\": \"設置\",\n  \"navbar_label_providers\": \"資源\",\n  \"header_help_action_title\": \"幫助\",\n  \"header_help_action_wiki\": \"線上文檔\",\n  \"header_help_action_issues\": \"報告問題\",\n  \"header_help_action_collect_logs\": \"收集日誌\",\n  \"header_help_action_about\": \"關於\",\n  \"header_settings_action_title\": \"設置\",\n  \"header_settings_action_language\": \"語言\",\n  \"header_settings_action_theme_mode\": \"主題模式\",\n  \"header_file_action_title\": \"文件\",\n  \"header_file_action_import_local_profile\": \"匯入本機配置\",\n  \"settings_system_proxy_title\": \"系統設置\",\n  \"settings_system_proxy_proxy_mode_label\": \"代理模式\",\n  \"settings_system_proxy_system_proxy_label\": \"系統代理\",\n  \"settings_system_proxy_tun_mode_label\": \"TUN 模式\",\n  \"settings_system_proxy_proxy_guard_label\": \"代理守衛\",\n  \"settings_system_proxy_proxy_guard_switch_label\": \"系統代理守衛\",\n  \"settings_system_proxy_proxy_guard_switch_description\": \"啟用後，系統代理將自動偵測代理設定，並自動修改為軟體內設定\",\n  \"settings_system_proxy_proxy_guard_interval_label\": \"系統代理守衛間隔\",\n  \"settings_system_proxy_proxy_bypass_label\": \"系統代理繞過\",\n  \"settings_system_proxy_current_system_proxy_label\": \"當前系統代理\",\n  \"settings_system_proxy_service_mode_label\": \"服務模式\",\n  \"settings_system_proxy_service_mode_description\": \"服務用於管理內核，達到權限獲取最小化，只給予內核必要的權限，用於例如 TUN 模式等需要特權權限的場景\",\n  \"settings_system_proxy_service_mode_disabled_tooltip\": \"要啟用服務模式，請確保 Clash Nyanpasu 服務已安裝並啟動\",\n  \"settings_system_proxy_system_service_ctrl_label\": \"系統服務\",\n  \"settings_system_proxy_system_service_ctrl_detail\": \"服務詳情\",\n  \"settings_system_proxy_system_service_ctrl_install\": \"安裝\",\n  \"settings_system_proxy_system_service_ctrl_uninstall\": \"卸載\",\n  \"settings_system_proxy_system_service_ctrl_failed_install\": \"安裝失敗\",\n  \"settings_system_proxy_system_service_ctrl_failed_uninstall\": \"卸載失敗\",\n  \"settings_system_proxy_system_service_ctrl_prompt\": \"服務提示\",\n  \"settings_system_proxy_system_service_ctrl_manual_prompt\": \"手動操作服務提示\",\n  \"settings_system_proxy_system_service_ctrl_manual_operation_prompt\": \"無法自動操作服務。請開啟核心所在目錄，在 Windows 上以管理員身分開啟 PowerShell 或在 macOS/Linux 上開啟終端模擬器，然後執行以下指令：\",\n  \"settings_system_proxy_system_service_ctrl_start\": \"啟動\",\n  \"settings_system_proxy_system_service_ctrl_stop\": \"停止\",\n  \"settings_system_proxy_launch_label\": \"啟動設置\",\n  \"settings_system_proxy_auto_launch_label\": \"開機自啟\",\n  \"settings_system_proxy_silent_start_label\": \"靜默啟動\",\n  \"settings_system_proxy_windows_tools_label\": \"Windows 工具\",\n  \"settings_system_proxy_uwp_tools_label\": \"UWP 回環工具\",\n  \"settings_system_proxy_uwp_tools_description\": \"用於解決 Windows UWP 應用程式無法透過本機代理存取網路的問題\",\n  \"settings_user_interface_title\": \"使用者介面\",\n  \"settings_user_interface_language_group\": \"語言設置\",\n  \"settings_user_interface_language_label\": \"語言\",\n  \"settings_user_interface_theme_mode_group\": \"主題設置\",\n  \"settings_user_interface_theme_mode_label\": \"主題模式\",\n  \"settings_user_interface_theme_mode_light\": \"淺色\",\n  \"settings_user_interface_theme_mode_dark\": \"深色\",\n  \"settings_user_interface_theme_mode_system\": \"跟隨系統\",\n  \"settings_user_interface_theme_color_label\": \"主題顏色\",\n  \"settings_user_interface_theme_color_custom\": \"自訂\",\n  \"settings_clash_settings_title\": \"Clash 設置\",\n  \"settings_clash_settings_allow_lan_label\": \"允許區域網路連線\",\n  \"settings_clash_settings_ipv6_label\": \"啟用 IPv6\",\n  \"settings_clash_settings_tun_stack_label\": \"TUN 堆疊\",\n  \"settings_clash_settings_log_level_label\": \"日誌級別\",\n  \"settings_clash_settings_port_label\": \"端口設置\",\n  \"settings_clash_settings_mixed_port_label\": \"混合端口\",\n  \"settings_clash_settings_random_port_label\": \"隨機端口\",\n  \"settings_clash_settings_random_port_enabled\": \"隨機端口已啟用，重啟後生效。\",\n  \"settings_clash_settings_random_port_disabled\": \"隨機端口已禁用，重啟後生效。\",\n  \"settings_clash_settings_external_controll_label\": \"外部控制器監聽地址\",\n  \"settings_clash_settings_port_strategy_label\": \"端口策略\",\n  \"settings_clash_settings_allow_fallback_label\": \"允許回退\",\n  \"settings_clash_settings_fixed_label\": \"固定\",\n  \"settings_clash_settings_random_label\": \"隨機\",\n  \"settings_clash_settings_core_secret_label\": \"API 訪問密鑰\",\n  \"settings_clash_settings_field_filter_label\": \"Clash 字段過濾\",\n  \"settings_clash_settings_field_filter_nyanpasu_control_fields\": \"Nyanpasu 覆寫控制字段\",\n  \"settings_web_ui_title\": \"Web UI 設置\",\n  \"settings_web_ui_add_button\": \"添加\",\n  \"settings_web_ui_empty_item\": \"沒有找到記錄，嘗試添加一個吧\",\n  \"settings_web_ui_input_label\": \"輸入 HTTP 地址\",\n  \"settings_web_ui_replace_with_label\": \"替換主機、端口和密鑰為\",\n  \"settings_web_ui_preview_title\": \"預覽\",\n  \"settings_clash_core_manager_card_title\": \"核心管理\",\n  \"settings_clash_core_manager_card_loading\": \"正在執行操作...\",\n  \"settings_clash_core_manager_card_loading_error\": \"執行操作失敗，請檢查日誌\",\n  \"settings_clash_core_manager_card_loading_success\": \"執行操作成功\",\n  \"settings_clash_core_manager_card_restart_sidecar\": \"重啟核心\",\n  \"settings_clash_core_manager_card_restart_sidecar_error\": \"重啟核心失敗，請檢查日誌\",\n  \"settings_clash_core_manager_card_restart_sidecar_success\": \"重啟核心成功\",\n  \"settings_clash_core_manager_card_fetch_remote\": \"檢查更新\",\n  \"settings_clash_core_manager_card_click_to_update\": \"點擊更新\",\n  \"settings_clash_core_manager_card_decompressing\": \"解壓縮中...\",\n  \"settings_clash_core_manager_card_replacing\": \"替換中...\",\n  \"settings_clash_core_manager_card_restarting\": \"重啟中...\",\n  \"settings_clash_core_manager_card_done\": \"完成\",\n  \"settings_debug_utils_open_config_directory\": \"開啟設定路徑\",\n  \"settings_debug_utils_open_data_directory\": \"開啟資料路徑\",\n  \"settings_debug_utils_open_core_directory\": \"開啟核心路徑\",\n  \"settings_debug_utils_open_log_directory\": \"開啟日誌路徑\",\n  \"settings_nyanpasu_max_log_files_label\": \"最大日誌文件數量\",\n  \"settings_label_system\": \"系統設置\",\n  \"settings_label_system_description\": \"代理模式、代理繞過、開機自啟、靜默啟動等設定\",\n  \"settings_label_user_interface\": \"使用者介面\",\n  \"settings_label_user_interface_description\": \"語言、主題模式、主題顏色等設定\",\n  \"settings_label_clash_settings\": \"Clash 設置\",\n  \"settings_label_clash_settings_description\": \"Clash 配置、日誌級別、混合端口、隨機端口等設定\",\n  \"settings_label_external_controll\": \"Web UI 與外部控制\",\n  \"settings_label_external_controll_description\": \"Web UI 位址、端口策略、API 密鑰等設定\",\n  \"settings_label_nyanpasu\": \"Nyanpasu 設置\",\n  \"settings_label_nyanpasu_description\": \"Nyanpasu 特性設定\",\n  \"settings_label_debug\": \"調試工具\",\n  \"settings_label_debug_description\": \"調試工具設定\",\n  \"settings_label_about\": \"關於\",\n  \"settings_label_about_description\": \"關於 Clash Nyanpasu\",\n  \"settings_label_about_update\": \"檢查更新\",\n  \"settings_label_about_auto_check_updates\": \"自動檢查更新\",\n  \"settings_label_about_update_to_github_releases\": \"前往 GitHub 檢查更新\",\n  \"settings_label_about_version\": \"版本: v{version}\",\n  \"settings_label_about_update_has_new_version\": \"有新版本可用\",\n  \"settings_label_about_update_no_update\": \"當前已經是最新版本，沒有找到更新信息\",\n  \"settings_label_about_update_to_update_button\": \"下載並安裝\",\n  \"settings_label_about_update_installing\": \"正在安裝...\",\n  \"profile_subscription_title\": \"訂閱信息\",\n  \"profile_subscription_updated_at\": \"{updated}更新\",\n  \"profile_subscription_next_update_at\": \"下次更新於 {next} 更新\",\n  \"profile_subscription_expires_in\": \"{expires}到期\",\n  \"profile_subscription_update\": \"更新\",\n  \"profile_base_info_title\": \"基本資訊\",\n  \"profile_name_editor_title\": \"編輯名稱\",\n  \"profile_name_label\": \"配置名稱\",\n  \"profile_update_option_edit\": \"訂閱選項\",\n  \"profile_update_option_editor_title\": \"編輯訂閱選項\",\n  \"profile_user_agent_label\": \"用戶代理 (UA)\",\n  \"profile_with_proxy_label\": \"使用系統代理\",\n  \"profile_self_proxy_label\": \"使用 Clash 代理\",\n  \"profile_update_interval_label\": \"自動更新間隔 (分鐘)\",\n  \"profile_subscription_url_editor_label\": \"編輯訂閱 URL\",\n  \"profile_subscription_url_label\": \"訂閱 URL\",\n  \"profile_delete_title\": \"刪除配置\",\n  \"profile_delete_description\": \"此操作無法撤銷。確定要刪除此配置嗎？\",\n  \"profile_view_content_title\": \"查看配置內容\",\n  \"profile_pending_mask_message\": \"正在執行操作……\",\n  \"profile_active_title\": \"啟用配置\",\n  \"profile_is_active_description\": \"當前配置已啟用，無需重複操作。\",\n  \"profile_active_title_success\": \"配置 {name} 已成功啟用！\",\n  \"profile_active_title_error\": \"配置 {name} 啟用失敗，請檢查配置或代理鏈是否正確\",\n  \"profile_open_locally_title\": \"開啟設定檔\",\n  \"profile_chain_editor_active_column\": \"激活的代理链\",\n  \"profile_chain_editor_inactive_column\": \"未激活的代理链\",\n  \"profile_chain_editor_apply_message\": \"正在應用代理鏈……\",\n  \"profile_quick_import_placeholder\": \"輸入訂閱 URL 或貼上訂閱連結以快速匯入設定\",\n  \"profile_quick_import_success_message\": \"配置導入成功，請檢查列表\",\n  \"profile_view_details_title\": \"配置詳情\",\n  \"profile_no_more_profiles\": \"沒有更多配置了 0.0\",\n  \"profile_import_title\": \"導入配置\",\n  \"profile_import_remote_title\": \"遠程配置\",\n  \"profile_import_local_title\": \"本地配置\",\n  \"profile_empty_list_message\": \"沒有找到任何配置，請嘗試導入或創建配置。\",\n  \"profile_import_remote_url_label\": \"遠程配置 URL\",\n  \"profile_profile_label\": \"代理配置\",\n  \"profile_javascript_label\": \"JavaScript 腳本\",\n  \"profile_lua_label\": \"Lua 腳本\",\n  \"profile_merge_label\": \"合併腳本 (YAML)\",\n  \"profile_profile_label_count\": \"{count} 個配置\",\n  \"profile_form_name_label\": \"名稱\",\n  \"profile_form_desc_label\": \"描述\",\n  \"profile_form_url_label\": \"訂閱 URL\",\n  \"profile_form_option_label\": \"配置選項\",\n  \"profile_form_option_user_agent_label\": \"用戶代理 (UA)\",\n  \"profile_form_option_with_proxy_label\": \"使用系統代理\",\n  \"profile_form_option_self_proxy_label\": \"使用 Clash 代理\",\n  \"profile_form_option_update_interval_label\": \"自動更新間隔 (分鐘)\",\n  \"profile_import_local_file_placeholder\": \"點擊或拖曳檔案至此處匯入\",\n  \"profile_import_local_file_type_label\": \"支持 {types} 文件\",\n  \"profile_import_local_file_size_label\": \"文件大小: {size}\",\n  \"profile_import_chain_title\": \"新建 {type}\",\n  \"proxies_group_delay_test_title\": \"延遲測試\",\n  \"proxies_group_delay_test_pending_title\": \"正在測試中……\",\n  \"proxies_group_empty_message\": \"沒有找到任何代理組，請嘗試切換配置\",\n  \"proxies_group_empty_button_text\": \"切換到配置頁面\",\n  \"profile_is_active_label\": \"當前配置\",\n  \"profile_remote_label\": \"遠程配置\",\n  \"profile_local_label\": \"本地配置\",\n  \"logs_search_placeholder\": \"透過時間、類型或訊息等等搜尋日誌\",\n  \"logs_empty_message\": \"沒有任何日誌記錄\",\n  \"logs_action_clear_log\": \"清空日誌\",\n  \"rules_list_all_proxies\": \"所有分組\",\n  \"connections_all_connections\": \"所有連接\",\n  \"connections_search_placeholder\": \"透過進程、類型、地址等資訊搜尋連接\",\n  \"connections_close_all_connections\": \"關閉所有連接\",\n  \"connections_empty_message\": \"沒有找到任何連接\",\n  \"connections_close_connection\": \"關閉連接\",\n  \"connections_view_details\": \"查看詳情\",\n  \"providers_proxies_title\": \"代理集\",\n  \"providers_rules_title\": \"規則集\",\n  \"providers_no_proxies_message\": \"當前配置沒有任何代理集\",\n  \"providers_no_rules_message\": \"當前配置沒有任何規則集\",\n  \"providers_proxies_proxy_count_label\": \"{count}個節點\",\n  \"providers_rules_rule_count_label\": \"{count}個規則\",\n  \"providers_info_title\": \"資源信息\",\n  \"providers_subscription_title\": \"訂閱信息\",\n  \"providers_update_provider\": \"更新資源\",\n  \"editor_before_close_message\": \"你尚未儲存編輯的內容，確定要關閉編輯器嗎？\",\n  \"editor_validate_error_message\": \"請修正錯誤後再儲存內容\",\n  \"editor_read_only_chip\": \"只讀\",\n  \"unit_seconds\": \"秒\",\n  \"common_submit\": \"提交\",\n  \"common_cancel\": \"取消\",\n  \"common_apply\": \"應用\",\n  \"common_reset\": \"重置\",\n  \"common_save\": \"儲存\",\n  \"common_validate\": \"驗證\",\n  \"common_close\": \"關閉\",\n  \"common_copy\": \"複製\",\n  \"common_open\": \"開啟\",\n  \"common_cut\": \"剪下\",\n  \"common_paste\": \"貼上\"\n}\n"
  },
  {
    "path": "frontend/nyanpasu/package.json",
    "content": "{\n  \"name\": \"@nyanpasu/nyanpasu\",\n  \"version\": \"2.0.0-alpha+10a82d25\",\n  \"license\": \"GPL-3.0\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"build\": \"vite build\",\n    \"bundle:visualize\": \"vite-bundle-visualizer\",\n    \"dev\": \"vite\",\n    \"serve\": \"vite preview\"\n  },\n  \"dependencies\": {\n    \"@dnd-kit/core\": \"6.3.1\",\n    \"@dnd-kit/helpers\": \"0.3.2\",\n    \"@dnd-kit/react\": \"0.3.2\",\n    \"@dnd-kit/sortable\": \"10.0.0\",\n    \"@dnd-kit/utilities\": \"3.2.2\",\n    \"@emotion/styled\": \"11.14.1\",\n    \"@hookform/resolvers\": \"5.2.2\",\n    \"@inlang/paraglide-js\": \"2.15.0\",\n    \"@juggle/resize-observer\": \"3.4.0\",\n    \"@material/material-color-utilities\": \"0.4.0\",\n    \"@mui/icons-material\": \"7.3.9\",\n    \"@mui/lab\": \"7.0.0-beta.17\",\n    \"@mui/material\": \"7.3.9\",\n    \"@mui/x-date-pickers\": \"8.27.2\",\n    \"@nyanpasu/interface\": \"workspace:^\",\n    \"@nyanpasu/ui\": \"workspace:^\",\n    \"@paper-design/shaders-react\": \"0.0.72\",\n    \"@radix-ui/react-use-controllable-state\": \"1.2.2\",\n    \"@tailwindcss/postcss\": \"4.2.2\",\n    \"@tanstack/react-table\": \"8.21.3\",\n    \"@tanstack/react-virtual\": \"3.13.23\",\n    \"@tanstack/router-zod-adapter\": \"1.81.5\",\n    \"@tauri-apps/api\": \"2.10.1\",\n    \"@types/json-schema\": \"7.0.15\",\n    \"@uidotdev/usehooks\": \"2.4.1\",\n    \"@uiw/react-color\": \"2.9.6\",\n    \"ahooks\": \"3.9.6\",\n    \"allotment\": \"1.20.5\",\n    \"class-variance-authority\": \"0.7.1\",\n    \"country-code-emoji\": \"2.3.0\",\n    \"country-emoji\": \"1.5.6\",\n    \"dayjs\": \"1.11.20\",\n    \"framer-motion\": \"12.38.0\",\n    \"i18next\": \"25.8.20\",\n    \"jotai\": \"2.18.1\",\n    \"json-schema\": \"0.4.0\",\n    \"material-react-table\": \"3.2.1\",\n    \"monaco-editor\": \"0.55.1\",\n    \"mui-color-input\": \"7.0.0\",\n    \"radix-ui\": \"1.4.3\",\n    \"react\": \"19.2.4\",\n    \"react-dom\": \"19.2.4\",\n    \"react-error-boundary\": \"6.0.0\",\n    \"react-fast-marquee\": \"1.6.5\",\n    \"react-hook-form\": \"7.71.2\",\n    \"react-hook-form-mui\": \"8.2.0\",\n    \"react-i18next\": \"15.7.4\",\n    \"react-markdown\": \"10.1.0\",\n    \"react-split-grid\": \"1.0.4\",\n    \"react-use\": \"17.6.0\",\n    \"rxjs\": \"7.8.2\",\n    \"swr\": \"2.4.1\",\n    \"virtua\": \"0.46.6\",\n    \"vite-bundle-visualizer\": \"1.2.1\"\n  },\n  \"devDependencies\": {\n    \"@csstools/normalize.css\": \"12.1.1\",\n    \"@emotion/babel-plugin\": \"11.13.5\",\n    \"@emotion/react\": \"11.14.0\",\n    \"@iconify/json\": \"2.2.452\",\n    \"@monaco-editor/react\": \"4.7.0\",\n    \"@tanstack/react-query\": \"5.91.2\",\n    \"@tanstack/react-router\": \"1.167.5\",\n    \"@tanstack/react-router-devtools\": \"1.166.9\",\n    \"@tanstack/router-plugin\": \"1.166.14\",\n    \"@tauri-apps/plugin-clipboard-manager\": \"2.3.2\",\n    \"@tauri-apps/plugin-dialog\": \"2.6.0\",\n    \"@tauri-apps/plugin-fs\": \"2.4.5\",\n    \"@tauri-apps/plugin-notification\": \"2.3.3\",\n    \"@tauri-apps/plugin-os\": \"2.3.2\",\n    \"@tauri-apps/plugin-process\": \"2.3.1\",\n    \"@tauri-apps/plugin-shell\": \"2.3.5\",\n    \"@tauri-apps/plugin-updater\": \"2.10.0\",\n    \"@types/react\": \"19.2.14\",\n    \"@types/react-dom\": \"19.2.3\",\n    \"@types/validator\": \"13.15.10\",\n    \"@vitejs/plugin-legacy\": \"7.2.1\",\n    \"@vitejs/plugin-react\": \"5.2.0\",\n    \"@vitejs/plugin-react-swc\": \"4.3.0\",\n    \"change-case\": \"5.4.4\",\n    \"clsx\": \"2.1.1\",\n    \"core-js\": \"3.49.0\",\n    \"filesize\": \"11.0.13\",\n    \"meta-json-schema\": \"1.19.21\",\n    \"monaco-yaml\": \"5.4.1\",\n    \"nanoid\": \"5.1.7\",\n    \"sass-embedded\": \"1.98.0\",\n    \"shiki\": \"4.0.2\",\n    \"unplugin-auto-import\": \"21.0.0\",\n    \"unplugin-icons\": \"23.0.1\",\n    \"validator\": \"13.15.26\",\n    \"vite\": \"7.3.1\",\n    \"vite-plugin-html\": \"3.2.2\",\n    \"vite-plugin-sass-dts\": \"1.3.35\",\n    \"vite-plugin-svgr\": \"4.5.0\",\n    \"vite-tsconfig-paths\": \"6.1.1\",\n    \"zod\": \"4.3.6\"\n  }\n}\n"
  },
  {
    "path": "frontend/nyanpasu/postcss.config.js",
    "content": "export default {\n  plugins: {\n    '@tailwindcss/postcss': {},\n  },\n}\n"
  },
  {
    "path": "frontend/nyanpasu/project.inlang/project_id",
    "content": "hmmAR8W6ML07bAYbAQ"
  },
  {
    "path": "frontend/nyanpasu/project.inlang/settings.json",
    "content": "{\n  \"$schema\": \"https://inlang.com/schema/project-settings\",\n  \"baseLocale\": \"en\",\n  \"locales\": [\"en\", \"zh-cn\", \"zh-tw\", \"ru\"],\n  \"modules\": [\n    \"https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@4/dist/index.js\",\n    \"https://cdn.jsdelivr.net/npm/@inlang/plugin-m-function-matcher@2/dist/index.js\"\n  ],\n  \"plugin.inlang.messageFormat\": {\n    \"pathPattern\": \"./messages/{locale}.json\"\n  }\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/assets/json/clash-field.json",
    "content": "{\n  \"default\": {\n    \"proxies\": \"https://nyanpasu.elaina.moe/others/field.html#proxies\",\n    \"proxy-groups\": \"https://nyanpasu.elaina.moe/others/field.html#proxy-groups\",\n    \"proxy-providers\": \"https://nyanpasu.elaina.moe/others/field.html#proxy-providers\",\n    \"rules\": \"https://nyanpasu.elaina.moe/others/field.html#rules\",\n    \"rule-providers\": \"https://nyanpasu.elaina.moe/others/field.html#rule-providers\"\n  },\n  \"handle\": {\n    \"mode\": \"https://nyanpasu.elaina.moe/others/field.html#mode\",\n    \"port\": \"https://nyanpasu.elaina.moe/others/field.html#port\",\n    \"socks-port\": \"https://nyanpasu.elaina.moe/others/field.html#socks-port\",\n    \"mixed-port\": \"https://nyanpasu.elaina.moe/others/field.html#mixed-port\",\n    \"allow-lan\": \"https://nyanpasu.elaina.moe/others/field.html#allow-lan\",\n    \"log-level\": \"https://nyanpasu.elaina.moe/others/field.html#log-level\",\n    \"ipv6\": \"https://nyanpasu.elaina.moe/others/field.html#ipv6\",\n    \"secret\": \"https://nyanpasu.elaina.moe/others/field.html#secret\",\n    \"external-controller\": \"https://nyanpasu.elaina.moe/others/field.html#external-controller\"\n  },\n  \"other\": {\n    \"dns\": \"https://nyanpasu.elaina.moe/others/field.html#dns\",\n    \"tun\": \"https://nyanpasu.elaina.moe/others/field.html#tun\",\n    \"ebpf\": \"https://nyanpasu.elaina.moe/others/field.html#ebpf\",\n    \"hosts\": \"https://nyanpasu.elaina.moe/others/field.html#hosts\",\n    \"script\": \"https://nyanpasu.elaina.moe/others/field.html#script\",\n    \"profile\": \"https://nyanpasu.elaina.moe/others/field.html#profile\",\n    \"payload\": \"https://nyanpasu.elaina.moe/others/field.html#payload\",\n    \"tunnels\": \"https://nyanpasu.elaina.moe/others/field.html#tunnels\",\n    \"auto-redir\": \"https://nyanpasu.elaina.moe/others/field.html#auto-redir\",\n    \"experimental\": \"https://nyanpasu.elaina.moe/others/field.html#experimental\",\n    \"interface-name\": \"https://nyanpasu.elaina.moe/others/field.html#interface-name\",\n    \"routing-mark\": \"https://nyanpasu.elaina.moe/others/field.html#routing-mark\",\n    \"redir-port\": \"https://nyanpasu.elaina.moe/others/field.html#redir-port\",\n    \"tproxy-port\": \"https://nyanpasu.elaina.moe/others/field.html#tproxy-port\",\n    \"iptables\": \"https://nyanpasu.elaina.moe/others/field.html#iptables\",\n    \"external-ui\": \"https://nyanpasu.elaina.moe/others/field.html#external-ui\",\n    \"bind-address\": \"https://nyanpasu.elaina.moe/others/field.html#bind-address\",\n    \"authentication\": \"https://nyanpasu.elaina.moe/others/field.html#authentication\"\n  },\n  \"meta\": {\n    \"tls\": \"https://nyanpasu.elaina.moe/others/field.html#tls\",\n    \"sniffer\": \"https://nyanpasu.elaina.moe/others/field.html#sniffer\",\n    \"geox-url\": \"https://nyanpasu.elaina.moe/others/field.html#geox-url\",\n    \"listeners\": \"https://nyanpasu.elaina.moe/others/field.html#listeners\",\n    \"sub-rules\": \"https://nyanpasu.elaina.moe/others/field.html#sub-rules\",\n    \"geodata-mode\": \"https://nyanpasu.elaina.moe/others/field.html#geodata-mode\",\n    \"unified-delay\": \"https://nyanpasu.elaina.moe/others/field.html#unified-delay\",\n    \"tcp-concurrent\": \"https://nyanpasu.elaina.moe/others/field.html#tcp-concurrent\",\n    \"enable-process\": \"https://nyanpasu.elaina.moe/others/field.html#enable-process\",\n    \"find-process-mode\": \"https://nyanpasu.elaina.moe/others/field.html#find-process-mode\",\n    \"skip-auth-prefixes\": \"https://nyanpasu.elaina.moe/others/field.html#skip-auth-prefixes\",\n    \"external-controller-tls\": \"https://nyanpasu.elaina.moe/others/field.html#external-controller-tls\",\n    \"global-client-fingerprint\": \"https://nyanpasu.elaina.moe/others/field.html#global-client-fingerprint\"\n  }\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/assets/styles/fonts.scss",
    "content": "// 这个字体是为了解决在 Windows 系统下，微软因为政策问题，不支持显示国旗 emoji 的问题\n@font-face {\n  font-family: 'Color Emoji Flags';\n  src:\n    local('Apple Color Emoji'), local('Noto Color Emoji'),\n    url('../fonts/Twemoji.Mozilla.ttf');\n  unicode-range: U+1F1E6-1F1FF;\n}\n\n// use local emoji font for better backward compatibility\n@font-face {\n  font-family: 'Color Emoji';\n  src:\n    local('Apple Color Emoji'), local('Segoe UI Emoji'),\n    local('Segoe UI Symbol'), local('Noto Color Emoji'),\n    url('../fonts/Twemoji.Mozilla.ttf');\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/assets/styles/index.scss",
    "content": "@use './fonts.scss';\n@use './theme.scss';\n\nbody {\n  margin: 0;\n  overflow: hidden;\n  font-family:\n    system-ui,\n    -apple-system,\n    BlinkMacSystemFont,\n    Segoe UI,\n    Roboto,\n    'Helvetica Neue',\n    Arial,\n    'Noto Sans',\n    sans-serif,\n    'Color Emoji Flags',\n    'Color Emoji';\n  color: var(--color-on-surface);\n  user-select: none;\n  background-color: #f7f7f7;\n  -webkit-font-smoothing: antialiased;\n}\n\n.dark body {\n  background-color: #0b0b0b;\n}\n\n:root {\n  --primary-main: #5b5c9d;\n  --text-primary: #637381;\n  --selection-color: #f5f5f5;\n  --scroller-color: #90939980;\n  --background-color: #fff;\n  --background-color-alpha: rgb(24 103 192 / 10%);\n  --border-radius: 12px;\n}\n\n::selection {\n  color: var(--selection-color);\n  background-color: var(--primary-main);\n}\n\n*::-webkit-scrollbar {\n  width: 6px;\n  height: 6px;\n  background: transparent;\n}\n\n*::-webkit-scrollbar-thumb {\n  background-color: var(--scroller-color);\n  border-radius: 6px;\n}\n\n*::-webkit-scrollbar-track {\n  margin-block: calc(var(--border-radius) + 3px);\n}\n\n@media (prefers-color-scheme: dark) {\n  :root {\n    background-color: rgb(18 18 18 / 100%);\n  }\n}\n\n.user-none {\n  user-select: none;\n}\n\n.bg-inherit-allow-fallback {\n  background-color: var(--fallback-bg, inherit);\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/assets/styles/tailwind.css",
    "content": "/* stylelint-disable value-keyword-case */\n/* stylelint-disable at-rule-no-unknown */\n/* stylelint-disable custom-property-pattern */\n/* stylelint-disable import-notation */\n\n@import 'tailwindcss';\n\n@config '../../../tailwind.config.ts';\n\n@tailwind utilities;\n\n@theme {\n  --font-mono:\n    'Cascadia Mono', 'Fira Code', ui-monospace, SFMono-Regular, Menlo, Monaco,\n    Consolas, 'Liberation Mono', 'Courier New', '等距更纱黑体 SC', monospace,\n    'Color Emoji Flags', 'Color Emoji';\n  --custom-text-shadow:\n    0px 0px 1px rgb(0 0 0 / 20%), 0px 0px 1px rgb(1 0 5 / 10%);\n  --custom-text-shadow-sm: 1px 1px 3px rgb(36 37 47 / 25%);\n  --custom-text-shadow-md:\n    0px 1px 2px rgb(30 29 39 / 19%), 1px 2px 4px rgb(54 64 147 / 18%);\n  --custom-text-shadow-lg:\n    3px 3px 6px rgb(0 0 0 / 26%), 0 0 5px rgb(15 3 86 / 22%);\n  --custom-text-shadow-xl:\n    1px 1px 3px rgb(0 0 0 / 29%), 2px 4px 7px rgb(73 64 125 / 35%);\n  --custom-text-shadow-none: none;\n\n  /* Material Design 3 Color System */\n  --color-primary: var(--color-md-primary);\n  --color-on-primary: var(--color-md-on-primary);\n  --color-primary-container: var(--color-md-primary-container);\n  --color-on-primary-container: var(--color-md-on-primary-container);\n  --color-secondary: var(--color-md-secondary);\n  --color-on-secondary: var(--color-md-on-secondary);\n  --color-secondary-container: var(--color-md-secondary-container);\n  --color-on-secondary-container: var(--color-md-on-secondary-container);\n  --color-tertiary: var(--color-md-tertiary);\n  --color-on-tertiary: var(--color-md-on-tertiary);\n  --color-tertiary-container: var(--color-md-tertiary-container);\n  --color-on-tertiary-container: var(--color-md-on-tertiary-container);\n  --color-error: var(--color-md-error);\n  --color-on-error: var(--color-md-on-error);\n  --color-error-container: var(--color-md-error-container);\n  --color-on-error-container: var(--color-md-on-error-container);\n  --color-background: var(--color-md-background);\n  --color-on-background: var(--color-md-on-background);\n  --color-surface: var(--color-md-surface);\n  --color-on-surface: var(--color-md-on-surface);\n  --color-surface-variant: var(--color-md-surface-variant);\n  --color-on-surface-variant: var(--color-md-on-surface-variant);\n  --color-outline: var(--color-md-outline);\n  --color-outline-variant: var(--color-md-outline-variant);\n  --color-shadow: var(--color-md-shadow);\n  --color-scrim: var(--color-md-scrim);\n  --color-inverse-surface: var(--color-md-inverse-surface);\n  --color-inverse-on-surface: var(--color-md-inverse-on-surface);\n  --color-inverse-primary: var(--color-md-inverse-primary);\n\n  /* Progress Spin Animation (Circular) */\n  --animate-progress-spin: progress-spin 5332ms cubic-bezier(0.4, 0, 0.2, 1)\n    infinite both;\n  --animate-progress-spin-left: progress-spin-left 1333ms\n    cubic-bezier(0.4, 0, 0.2, 1) infinite both;\n  --animate-progress-spin-right: progress-spin-right 1333ms\n    cubic-bezier(0.4, 0, 0.2, 1) infinite both;\n\n  /* Linear Progress Animation (Indeterminate) */\n  --animate-linear-progress-primary: linear-progress-primary 2.1s\n    cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite;\n  --animate-linear-progress-secondary: linear-progress-secondary 2.1s\n    cubic-bezier(0.165, 0.84, 0.44, 1) 1.15s infinite;\n}\n\n/* Custom Mixed Colors (light) */\n@theme {\n  --color-mixed-background: color-mix(\n    in srgb,\n    var(--color-background) 95%,\n    white 5%\n  );\n}\n\n/* Custom Mixed Colors (dark) */\n.dark {\n  --color-mixed-background: color-mix(\n    in srgb,\n    var(--color-background) 50%,\n    black 50%\n  );\n}\n\n@utility text-shadow-* {\n  /* prettier-ignore */\n  text-shadow: --value(--custom-text-shadow-*);\n}\n\n@utility bg-transparent-fallback-* {\n  background-color: transparent;\n\n  --fallback-bg: --value(--color-*);\n}\n\n@layer components {\n  svg.logo-colorized #element {\n    fill: var(--color-primary);\n  }\n\n  svg.logo-colorized #bg {\n    fill: var(--color-surface);\n  }\n\n  .dark svg.logo-colorized #element {\n    fill: var(--color-on-primary);\n  }\n\n  .dark svg.logo-colorized #bg {\n    fill: var(--color-on-surface);\n  }\n}\n\n@keyframes progress-spin {\n  12.5% {\n    transform: rotate(135deg);\n  }\n\n  25% {\n    transform: rotate(270deg);\n  }\n\n  37.5% {\n    transform: rotate(405deg);\n  }\n\n  50% {\n    transform: rotate(540deg);\n  }\n\n  62.5% {\n    transform: rotate(675deg);\n  }\n\n  75% {\n    transform: rotate(810deg);\n  }\n\n  87.5% {\n    transform: rotate(945deg);\n  }\n\n  100% {\n    transform: rotate(1080deg);\n  }\n}\n\n@keyframes progress-spin-left {\n  0% {\n    transform: rotate(265deg);\n  }\n\n  50% {\n    transform: rotate(130deg);\n  }\n\n  100% {\n    transform: rotate(265deg);\n  }\n}\n\n@keyframes progress-spin-right {\n  0% {\n    transform: rotate(-265deg);\n  }\n\n  50% {\n    transform: rotate(-130deg);\n  }\n\n  100% {\n    transform: rotate(-265deg);\n  }\n}\n\n/* Material You Linear Progress Indeterminate Animation */\n@keyframes linear-progress-primary {\n  0% {\n    right: 100%;\n    left: -35%;\n  }\n\n  60% {\n    right: -90%;\n    left: 100%;\n  }\n\n  100% {\n    right: -90%;\n    left: 100%;\n  }\n}\n\n@keyframes linear-progress-secondary {\n  0% {\n    right: 100%;\n    left: -200%;\n  }\n\n  60% {\n    right: -8%;\n    left: 107%;\n  }\n\n  100% {\n    right: -8%;\n    left: 107%;\n  }\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/assets/styles/theme.scss",
    "content": "// default theme, generated by material-color-utilities\n// frontend/nyanpasu/src/components/providers/theme-provider.tsx\n\n// this is fallback theme, if custom theme not set\n:root {\n  --color-md-primary: #005db5;\n  --color-md-on-primary: #fff;\n  --color-md-primary-container: #d6e3ff;\n  --color-md-on-primary-container: #001b3d;\n  --color-md-secondary: #555f71;\n  --color-md-on-secondary: #fff;\n  --color-md-secondary-container: #d9e3f8;\n  --color-md-on-secondary-container: #121c2b;\n  --color-md-tertiary: #6f5675;\n  --color-md-on-tertiary: #fff;\n  --color-md-tertiary-container: #f9d8fe;\n  --color-md-on-tertiary-container: #28132f;\n  --color-md-error: #ba1a1a;\n  --color-md-on-error: #fff;\n  --color-md-error-container: #ffdad6;\n  --color-md-on-error-container: #410002;\n  --color-md-background: #fdfbff;\n  --color-md-on-background: #1a1b1e;\n  --color-md-surface: #fdfbff;\n  --color-md-on-surface: #1a1b1e;\n  --color-md-surface-variant: #e0e2ec;\n  --color-md-on-surface-variant: #43474e;\n  --color-md-outline: #74777f;\n  --color-md-outline-variant: #c4c6cf;\n  --color-md-shadow: #000;\n  --color-md-scrim: #000;\n  --color-md-inverse-surface: #2f3033;\n  --color-md-inverse-on-surface: #f1f0f4;\n  --color-md-inverse-primary: #a8c8ff;\n}\n\n:root.dark {\n  --color-md-primary: #a8c8ff;\n  --color-md-on-primary: #003062;\n  --color-md-primary-container: #00468b;\n  --color-md-on-primary-container: #d6e3ff;\n  --color-md-secondary: #bdc7dc;\n  --color-md-on-secondary: #273141;\n  --color-md-secondary-container: #3e4758;\n  --color-md-on-secondary-container: #d9e3f8;\n  --color-md-tertiary: #dcbce1;\n  --color-md-on-tertiary: #3e2845;\n  --color-md-tertiary-container: #563e5c;\n  --color-md-on-tertiary-container: #f9d8fe;\n  --color-md-error: #ffb4ab;\n  --color-md-on-error: #690005;\n  --color-md-error-container: #93000a;\n  --color-md-on-error-container: #ffb4ab;\n  --color-md-background: #1a1b1e;\n  --color-md-on-background: #e3e2e6;\n  --color-md-surface: #1a1b1e;\n  --color-md-on-surface: #e3e2e6;\n  --color-md-surface-variant: #43474e;\n  --color-md-on-surface-variant: #c4c6cf;\n  --color-md-outline: #8e9099;\n  --color-md-outline-variant: #43474e;\n  --color-md-shadow: #000;\n  --color-md-scrim: #000;\n  --color-md-inverse-surface: #e3e2e6;\n  --color-md-inverse-on-surface: #2f3033;\n  --color-md-inverse-primary: #005db5;\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/app/app-container.module.d.scss.ts",
    "content": "declare const classNames: {\n  readonly layout: 'layout'\n  readonly container: 'container'\n}\nexport default classNames\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/app/app-container.module.scss",
    "content": ":root {\n  --focus-border: transparent !important;\n  --separator-border: transparent !important;\n}\n\n.layout {\n  display: flex;\n  width: 100%;\n  height: 100vh;\n  overflow: hidden;\n  background-color: var(--background-color);\n\n  .container {\n    position: relative;\n    flex: 1 1 75%;\n    height: 100%;\n    background-color: var(--background-color-alpha);\n  }\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/app/app-container.module.scss.d.ts",
    "content": "declare const classNames: {\n  readonly layout: 'layout'\n  readonly container: 'container'\n}\nexport default classNames\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/app/app-container.tsx",
    "content": "import getSystem from '@/utils/get-system'\nimport { Box } from '@mui/material'\nimport Paper from '@mui/material/Paper'\nimport { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'\nimport 'allotment/dist/style.css'\nimport { useAtomValue } from 'jotai'\nimport { ReactNode, useEffect, useRef } from 'react'\nimport { atomIsDrawerOnlyIcon } from '@/store'\nimport { alpha, cn } from '@nyanpasu/ui'\nimport { useQueryClient, useSuspenseQuery } from '@tanstack/react-query'\nimport { TauriEvent, UnlistenFn } from '@tauri-apps/api/event'\nimport { LayoutControl } from '../layout/layout-control'\nimport styles from './app-container.module.scss'\nimport AppDrawer from './app-drawer'\nimport DrawerContent from './drawer-content'\n\nconst appWindow = getCurrentWebviewWindow()\n\nconst OS = getSystem()\n\nexport const AppContainer = ({\n  children,\n  isDrawer,\n}: {\n  children?: ReactNode\n  isDrawer?: boolean\n}) => {\n  const { data: isMaximized } = useSuspenseQuery({\n    queryKey: ['isMaximized'],\n    queryFn: () => appWindow.isMaximized(),\n  })\n  const queryClient = useQueryClient()\n  const unlistenRef = useRef<UnlistenFn | null>(null)\n  const onlyIcon = useAtomValue(atomIsDrawerOnlyIcon)\n\n  useEffect(() => {\n    appWindow\n      .listen(TauriEvent.WINDOW_RESIZED, () => {\n        queryClient.invalidateQueries({ queryKey: ['isMaximized'] })\n      })\n      .then((unlisten) => {\n        unlistenRef.current = unlisten\n      })\n      .catch((error) => {\n        console.error(error)\n      })\n    return () => {\n      unlistenRef.current?.()\n    }\n  }, [queryClient])\n\n  return (\n    <Paper\n      square\n      elevation={0}\n      className={styles.layout}\n      onPointerDown={(e) => {\n        if ((e.target as HTMLElement)?.dataset?.windrag) {\n          appWindow.startDragging()\n        }\n      }}\n    >\n      {isDrawer && <AppDrawer data-tauri-drag-region />}\n\n      {!isDrawer && (\n        <div className={cn(onlyIcon ? 'w-24' : 'w-64')}>\n          <DrawerContent data-tauri-drag-region onlyIcon={onlyIcon} />\n        </div>\n      )}\n\n      <div className={styles.container}>\n        {OS === 'windows' && (\n          <LayoutControl className=\"!z-top fixed top-2 right-4\" />\n        )}\n        {/* TODO: add a framer motion animation to toggle the maximized state */}\n        {OS === 'macos' && !isMaximized && (\n          <Box\n            className=\"z-top fixed top-1.5 left-3 h-7 w-[4.5rem] rounded-full\"\n            sx={(theme) => ({\n              backgroundColor: alpha(theme.vars.palette.primary.main, 0.1),\n            })}\n          />\n        )}\n\n        <div\n          className={OS === 'macos' ? 'h-[2.75rem]' : 'h-9'}\n          data-tauri-drag-region\n        />\n\n        {children}\n      </div>\n    </Paper>\n  )\n}\n\nexport default AppContainer\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/app/app-drawer.tsx",
    "content": "import { AnimatePresence, motion } from 'framer-motion'\nimport { useState } from 'react'\nimport getSystem from '@/utils/get-system'\nimport { MenuOpen } from '@mui/icons-material'\nimport { Backdrop, IconButton } from '@mui/material'\nimport type { SxProps, Theme } from '@mui/material/styles'\nimport { alpha, cn } from '@nyanpasu/ui'\nimport AnimatedLogo from '../layout/animated-logo'\nimport DrawerContent from './drawer-content'\n\nconst OS = getSystem()\n\nexport const AppDrawer = () => {\n  const [open, setOpen] = useState(false)\n\n  const DrawerTitle = () => {\n    return (\n      <div\n        className={cn(\n          'fixed z-10 flex items-center gap-2',\n          OS === 'macos' ? 'top-3 left-24' : 'top-1.5 left-4',\n        )}\n        data-tauri-drag-region\n      >\n        <IconButton\n          className=\"!size-8 !min-w-0\"\n          sx={[\n            (theme) => ({\n              backgroundColor: alpha(theme.vars.palette.primary.main, 0.1),\n              svg: { transform: 'scale(0.9)' },\n            }),\n          ]}\n          onClick={() => setOpen(true)}\n        >\n          <MenuOpen />\n        </IconButton>\n\n        <div className=\"size-5\" data-tauri-drag-region>\n          <AnimatedLogo className=\"h-full w-full\" data-tauri-drag-region />\n        </div>\n\n        <div className=\"text-lg\" data-tauri-drag-region>\n          Clash Nyanpasu\n        </div>\n      </div>\n    )\n  }\n\n  return (\n    <>\n      <DrawerTitle />\n      <Backdrop\n        className={cn('z-20', OS !== 'linux' && 'backdrop-blur-xl')}\n        sx={\n          (OS === 'linux'\n            ? {\n                backgroundColor: 'transparent',\n              }\n            : (theme) => ({\n                backgroundColor: alpha(theme.vars.palette.primary.light, 0.1),\n                ...theme.applyStyles('dark', {\n                  backgroundColor: alpha(theme.vars.palette.primary.dark, 0.1),\n                }),\n              })) as SxProps<Theme>\n        }\n        open={open}\n        onClick={() => setOpen(false)}\n      >\n        <AnimatePresence initial={false}>\n          <div className=\"h-full w-full\">\n            <motion.div\n              className=\"h-full\"\n              animate={open ? 'open' : 'closed'}\n              variants={{\n                open: {\n                  x: 0,\n                },\n                closed: {\n                  x: -240,\n                },\n              }}\n              transition={{\n                type: 'tween',\n              }}\n            >\n              <DrawerContent className=\"max-w-64\" />\n            </motion.div>\n          </div>\n        </AnimatePresence>\n      </Backdrop>\n    </>\n  )\n}\n\nexport default AppDrawer\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/app/drawer-content.tsx",
    "content": "import getSystem from '@/utils/get-system'\nimport { getRoutesWithIcon } from '@/utils/routes-utils'\nimport { Box } from '@mui/material'\nimport { cn } from '@nyanpasu/ui'\nimport AnimatedLogo from '../layout/animated-logo'\nimport RouteListItem from './modules/route-list-item'\n\nexport const DrawerContent = ({\n  className,\n  onlyIcon,\n}: {\n  className?: string\n  onlyIcon?: boolean\n}) => {\n  const routes = getRoutesWithIcon()\n\n  return (\n    <Box\n      className={cn(\n        'p-4',\n        getSystem() === 'macos' ? 'pt-14' : 'pt-8',\n        'w-full',\n        'h-full',\n        'flex',\n        'flex-col',\n        'gap-4',\n        className,\n      )}\n      sx={[\n        {\n          backgroundColor: 'var(--background-color-alpha)',\n        },\n      ]}\n      data-tauri-drag-region\n    >\n      <div className=\"mx-2 flex items-center justify-center gap-4\">\n        <div className=\"h-full max-h-28 max-w-28\" data-tauri-drag-region>\n          <AnimatedLogo className=\"h-full w-full\" data-tauri-drag-region />\n        </div>\n\n        {!onlyIcon && (\n          <div\n            className=\"mt-1 flex-1 text-lg font-bold whitespace-pre-wrap\"\n            data-tauri-drag-region\n          >\n            {'Clash\\nNyanpasu'}\n          </div>\n        )}\n      </div>\n\n      <div className=\"scrollbar-hidden flex flex-col gap-2 !overflow-x-hidden overflow-y-auto\">\n        {Object.entries(routes).map(([name, { path, icon }]) => {\n          return (\n            <RouteListItem\n              key={name}\n              name={name}\n              path={path}\n              icon={icon}\n              onlyIcon={onlyIcon}\n            />\n          )\n        })}\n      </div>\n    </Box>\n  )\n}\n\nexport default DrawerContent\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/app/locales-provider.tsx",
    "content": "import { locale } from 'dayjs'\nimport { changeLanguage } from 'i18next'\nimport { useEffect } from 'react'\nimport { useSetting } from '@nyanpasu/interface'\n\nexport const LocalesProvider = () => {\n  const { value } = useSetting('language')\n\n  useEffect(() => {\n    if (value) {\n      locale(value === 'zh' ? 'zh-cn' : value)\n\n      changeLanguage(value)\n    }\n  }, [value])\n\n  return null\n}\n\nexport default LocalesProvider\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/app/modules/route-list-item.tsx",
    "content": "import { createElement } from 'react'\nimport { useTranslation } from 'react-i18next'\nimport { languageQuirks } from '@/utils/language'\nimport { SvgIconComponent } from '@mui/icons-material'\nimport { Box, ListItemButton, ListItemIcon, Tooltip } from '@mui/material'\nimport { useSetting } from '@nyanpasu/interface'\nimport { alpha, cn } from '@nyanpasu/ui'\nimport { useLocation, useNavigate } from '@tanstack/react-router'\n\nexport const RouteListItem = ({\n  name,\n  path,\n  icon,\n  onlyIcon,\n}: {\n  name: string\n  path: string\n  icon: SvgIconComponent\n  onlyIcon?: boolean\n}) => {\n  const { t } = useTranslation()\n\n  const location = useLocation()\n\n  const match = location.pathname === path\n\n  const navigate = useNavigate()\n\n  const { value: language } = useSetting('language')\n\n  const listItemButton = (\n    <ListItemButton\n      className={cn(\n        onlyIcon ? '!mx-auto !size-16 !rounded-3xl' : '!rounded-full !pr-14',\n      )}\n      sx={[\n        (theme) => ({\n          backgroundColor: match\n            ? alpha(theme.vars.palette.primary.main, 0.3)\n            : alpha(theme.vars.palette.background.paper, 0.15),\n        }),\n        (theme) => ({\n          '&:hover': {\n            backgroundColor: match\n              ? alpha(theme.vars.palette.primary.main, 0.5)\n              : null,\n          },\n        }),\n      ]}\n      onClick={() => {\n        navigate({\n          to: path,\n        })\n      }}\n    >\n      <ListItemIcon>\n        {createElement(icon, {\n          sx: (theme) => ({\n            fill: match ? theme.vars.palette.primary.main : undefined,\n          }),\n          className: onlyIcon ? '!size-8' : undefined,\n        })}\n      </ListItemIcon>\n      {!onlyIcon && (\n        <Box\n          className={cn(\n            'w-full pt-1 pb-1 text-nowrap',\n            language && languageQuirks[language].drawer.itemClassNames,\n          )}\n          sx={(theme) => ({\n            color: match ? theme.vars.palette.primary.main : undefined,\n          })}\n        >\n          {t(`label_${name}`)}\n        </Box>\n      )}\n    </ListItemButton>\n  )\n\n  return onlyIcon ? (\n    <Tooltip title={t(`label_${name}`)}>{listItemButton}</Tooltip>\n  ) : (\n    listItemButton\n  )\n}\n\nexport default RouteListItem\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/base/base-empty.tsx",
    "content": "import { InboxRounded } from '@mui/icons-material'\nimport { Box, Typography } from '@mui/material'\nimport { alpha } from '@nyanpasu/ui'\n\ninterface Props {\n  text?: React.ReactNode\n  extra?: React.ReactNode\n}\n\nexport const BaseEmpty = (props: Props) => {\n  const { text = 'Empty', extra } = props\n\n  return (\n    <Box\n      sx={(theme) => ({\n        width: '100%',\n        height: '100%',\n        display: 'flex',\n        flexDirection: 'column',\n        alignItems: 'center',\n        justifyContent: 'center',\n        color: alpha(theme.vars.palette.text.secondary, 0.75),\n      })}\n    >\n      <InboxRounded sx={{ fontSize: '4em' }} />\n      <Typography sx={{ fontSize: '1.25em' }}>{text}</Typography>\n      {extra}\n    </Box>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/base/base-error-boundary.tsx",
    "content": "import { ReactNode } from 'react'\nimport { ErrorBoundary, FallbackProps } from 'react-error-boundary'\n\nfunction ErrorFallback({ error }: FallbackProps) {\n  return (\n    <div role=\"alert\" style={{ padding: 16 }}>\n      <h4>Something went wrong:(</h4>\n\n      <pre>{error.message}</pre>\n\n      <details title=\"Error Stack\">\n        <summary>Error Stack</summary>\n        <pre>{error.stack}</pre>\n      </details>\n    </div>\n  )\n}\n\ninterface Props {\n  children?: ReactNode\n}\n\nexport const BaseErrorBoundary = (props: Props) => {\n  return (\n    <ErrorBoundary FallbackComponent={ErrorFallback}>\n      {props.children}\n    </ErrorBoundary>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/base/base-notice.tsx",
    "content": "import { ReactNode, useState } from 'react'\nimport { createRoot } from 'react-dom/client'\nimport { CheckCircleRounded, Close, ErrorRounded } from '@mui/icons-material'\nimport { Box, IconButton, Slide, Snackbar, Typography } from '@mui/material'\n\ninterface InnerProps {\n  type: string\n  duration?: number\n  message: ReactNode\n  onClose: () => void\n}\n\nconst NoticeInner = (props: InnerProps) => {\n  const { type, message, duration = 1500, onClose } = props\n  const [visible, setVisible] = useState(true)\n\n  const onBtnClose = () => {\n    setVisible(false)\n    onClose()\n  }\n\n  // oxlint-disable-next-line typescript/no-explicit-any\n  const onAutoClose = (_e: any, reason: string) => {\n    if (reason !== 'clickaway') onBtnClose()\n  }\n\n  const msgElement =\n    type === 'info' ? (\n      message\n    ) : (\n      <Box sx={{ width: 328, display: 'flex', alignItems: 'center' }}>\n        {type === 'error' && <ErrorRounded color=\"error\" />}\n        {type === 'success' && <CheckCircleRounded color=\"success\" />}\n\n        <Typography\n          component=\"span\"\n          sx={{ ml: 1, wordWrap: 'break-word', width: 'calc(100% - 35px)' }}\n        >\n          {message}\n        </Typography>\n      </Box>\n    )\n\n  return (\n    <Snackbar\n      open={visible}\n      anchorOrigin={{ vertical: 'top', horizontal: 'right' }}\n      autoHideDuration={duration}\n      onClose={onAutoClose}\n      message={msgElement}\n      sx={{ maxWidth: 360 }}\n      TransitionComponent={(p) => <Slide {...p} direction=\"left\" />}\n      transitionDuration={200}\n      action={\n        <IconButton size=\"small\" color=\"inherit\" onClick={onBtnClose}>\n          <Close fontSize=\"inherit\" />\n        </IconButton>\n      }\n    />\n  )\n}\n\ninterface NoticeInstance {\n  (props: Omit<InnerProps, 'onClose'>): void\n\n  info(message: ReactNode, duration?: number): void\n  error(message: ReactNode, duration?: number): void\n  success(message: ReactNode, duration?: number): void\n}\n\nlet parent: HTMLDivElement = null!\n\n// @ts-expect-error 90 行动态添加了 info、error、success 属性\nexport const Notice: NoticeInstance = (props) => {\n  if (!parent) {\n    parent = document.createElement('div')\n    document.body.appendChild(parent)\n  }\n\n  const container = document.createElement('div')\n  parent.appendChild(container)\n  const root = createRoot(container)\n\n  const onUnmount = () => {\n    root.unmount()\n    if (parent) setTimeout(() => parent.removeChild(container), 500)\n  }\n\n  root.render(<NoticeInner {...props} onClose={onUnmount} />)\n}\n;(['info', 'error', 'success'] as const).forEach((type) => {\n  Notice[type] = (message, duration) => {\n    setTimeout(() => Notice({ type, message, duration }), 0)\n  }\n})\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/base/content-display.tsx",
    "content": "import { ReactNode } from 'react'\nimport { Public } from '@mui/icons-material'\nimport { cn } from '@nyanpasu/ui'\n\nexport interface ContentDisplayProps {\n  className?: string\n  message?: string\n  children?: ReactNode\n}\n\nexport const ContentDisplay = ({\n  message,\n  children,\n  className,\n}: ContentDisplayProps) => (\n  <div\n    className={cn('flex h-full w-full items-center justify-center', className)}\n  >\n    <div className=\"flex flex-col items-center gap-4\">\n      {children || (\n        <>\n          <Public className=\"!size-16\" />\n\n          <b>{message}</b>\n        </>\n      )}\n    </div>\n  </div>\n)\n\nexport default ContentDisplay\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/base/index.ts",
    "content": "export { BaseEmpty } from './base-empty'\nexport { BaseErrorBoundary } from './base-error-boundary'\nexport { Notice } from './base-notice'\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/connections/close-connections-button.tsx",
    "content": "import { useLockFn } from 'ahooks'\nimport { useTranslation } from 'react-i18next'\nimport { Close } from '@mui/icons-material'\nimport { Tooltip } from '@mui/material'\nimport { useClashConnections } from '@nyanpasu/interface'\nimport { FloatingButton } from '@nyanpasu/ui'\n\nexport const CloseConnectionsButton = () => {\n  const { t } = useTranslation()\n\n  const { deleteConnections } = useClashConnections()\n\n  const onCloseAll = useLockFn(async () => {\n    await deleteConnections.mutateAsync(undefined)\n  })\n\n  return (\n    <Tooltip title={t('Close All')}>\n      <FloatingButton onClick={onCloseAll}>\n        <Close className=\"absolute !size-8\" />\n      </FloatingButton>\n    </Tooltip>\n  )\n}\n\nexport default CloseConnectionsButton\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/connections/connection-detail-dialog.tsx",
    "content": "import { sentenceCase } from 'change-case'\nimport dayjs from 'dayjs'\nimport { filesize } from 'filesize'\nimport * as React from 'react'\nimport { useTranslation } from 'react-i18next'\nimport { Tooltip } from '@mui/material'\nimport { Connection } from '@nyanpasu/interface'\nimport { BaseDialog, BaseDialogProps, cn } from '@nyanpasu/ui'\n\nexport type ConnectionDetailDialogProps = { item?: Connection.Item } & Omit<\n  BaseDialogProps,\n  'title'\n>\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst formatValue = (key: string, value: any): React.ReactElement => {\n  if (Array.isArray(value)) {\n    return <span>{value.join(' / ')}</span>\n  }\n  key = key.toLowerCase()\n  if (key.includes('speed')) {\n    return <span>{filesize(value)}/s</span>\n  }\n\n  if (key.includes('download') || key.includes('upload')) {\n    return <span>{filesize(value)}</span>\n  }\n\n  if (key.includes('port') || key.includes('id') || key.includes('ip')) {\n    return <span>{value}</span>\n  }\n\n  const date = dayjs(value)\n\n  if (date.isValid()) {\n    return (\n      <Tooltip title={date.format('YYYY-MM-DD HH:mm:ss')}>\n        <span>{date.fromNow()}</span>\n      </Tooltip>\n    )\n  }\n\n  return value\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst Row = ({ label, value }: { label: string; value: any }) => {\n  const key = label.toLowerCase()\n  return (\n    <>\n      <div className=\"w-fit font-bold\">{sentenceCase(label)}</div>\n      <div\n        className={cn(\n          'overflow',\n          (key === 'id' ||\n            key.includes('ip') ||\n            key.includes('port') ||\n            key.includes('destination') ||\n            key.includes('path')) &&\n            'font-mono',\n        )}\n      >\n        {formatValue(key, value)}\n      </div>\n    </>\n  )\n}\n\nexport default function ConnectionDetailDialog({\n  item,\n  ...others\n}: ConnectionDetailDialogProps) {\n  const { t } = useTranslation()\n  if (!item) return null\n\n  return (\n    <BaseDialog {...others} title={t('Connection Detail')}>\n      <div className=\"grid grid-cols-[max-content_1fr] gap-x-3 gap-y-2 px-3\">\n        {Object.entries(item)\n          .filter(([key, value]) => key !== 'metadata' && !!value)\n          .map(([key, value]) => (\n            <Row key={key} label={key} value={value} />\n          ))}\n\n        <h3 className=\"col-span-2 py-1 pt-5 text-xl font-semibold\">\n          {t('Metadata')}\n        </h3>\n\n        {Object.entries(item.metadata)\n          .filter(([, value]) => !!value)\n          .map(([key, value]) => (\n            <Row key={key} label={key} value={value} />\n          ))}\n      </div>\n    </BaseDialog>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/connections/connection-page.tsx",
    "content": "import { use } from 'react'\nimport CloseConnectionsButton from './close-connections-button'\nimport { SearchTermCtx } from './connection-search-term'\nimport ConnectionsTable from './connections-table'\n\nexport default function ConnectionPage() {\n  const searchTerm = use(SearchTermCtx)\n  return (\n    <>\n      <ConnectionsTable searchTerm={searchTerm} />\n      <CloseConnectionsButton />\n    </>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/connections/connection-search-term.tsx",
    "content": "import { createContext } from 'react'\n\nexport const SearchTermCtx = createContext<string | undefined>(undefined)\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/connections/connections-column-filter.tsx",
    "content": "/* eslint-disable camelcase */\nimport { useLockFn } from 'ahooks'\nimport { snakeCase } from 'change-case'\nimport dayjs from 'dayjs'\nimport { AnimatePresence, Reorder, useDragControls } from 'framer-motion'\nimport { useAtom } from 'jotai'\nimport { type MRT_ColumnDef } from 'material-react-table'\nimport { MouseEventHandler, useCallback, useMemo, useState } from 'react'\nimport { useTranslation } from 'react-i18next'\nimport { connectionTableColumnsAtom } from '@/store'\nimport parseTraffic from '@/utils/parse-traffic'\nimport { Cancel, Menu } from '@mui/icons-material'\nimport { Checkbox, CircularProgress, IconButton } from '@mui/material'\nimport { useClashConnections } from '@nyanpasu/interface'\nimport { BaseDialog, BaseDialogProps } from '@nyanpasu/ui'\nimport { TableConnection } from './connections-table'\n\nfunction CloseConnectionButton({ id }: { id: string }) {\n  const { deleteConnections } = useClashConnections()\n\n  const closeConnect = useLockFn(async (id?: string) => {\n    await deleteConnections.mutateAsync(id)\n  })\n\n  const [loading, setLoading] = useState(false)\n\n  const onClick: MouseEventHandler<HTMLButtonElement> = useCallback(\n    (e) => {\n      e.preventDefault()\n      e.stopPropagation()\n      setLoading(true)\n      closeConnect(id).finally(() => setLoading(false))\n    },\n    [closeConnect, id],\n  )\n\n  return (\n    <div className=\"flex w-full items-center justify-center gap-2\">\n      <IconButton\n        color=\"primary\"\n        className=\"size-4\"\n        onClick={onClick}\n        disabled={loading}\n      >\n        {loading ? <CircularProgress color=\"primary\" /> : <Cancel />}\n      </IconButton>\n    </div>\n  )\n}\n\nexport const useColumns = (): Array<MRT_ColumnDef<TableConnection>> => {\n  const { t } = useTranslation()\n\n  return useMemo(\n    () =>\n      (\n        [\n          {\n            header: 'Actions',\n            size: 60,\n            enableSorting: false,\n            enableGlobalFilter: false,\n            enableResizing: false,\n            accessorFn: ({ id }) => <CloseConnectionButton id={id} />,\n          },\n          {\n            header: 'Host',\n            size: 240,\n            accessorFn: ({ metadata }) =>\n              metadata.host || metadata.destinationIP,\n          },\n          {\n            header: 'Process',\n            size: 140,\n            accessorFn: ({ metadata }) => metadata.process,\n          },\n          {\n            header: 'Downloaded',\n            size: 88,\n            accessorFn: ({ download }) => parseTraffic(download).join(' '),\n            sortingFn: (rowA, rowB) =>\n              rowA.original.download - rowB.original.download,\n          },\n          {\n            header: 'Uploaded',\n            size: 88,\n            accessorFn: ({ upload }) => parseTraffic(upload).join(' '),\n            sortingFn: (rowA, rowB) =>\n              rowA.original.upload - rowB.original.upload,\n          },\n          {\n            header: 'DL Speed',\n            size: 88,\n            accessorFn: ({ downloadSpeed }) =>\n              parseTraffic(downloadSpeed).join(' ') + '/s',\n            sortingFn: (rowA, rowB) =>\n              (rowA.original.downloadSpeed || 0) -\n              (rowB.original.downloadSpeed || 0),\n          },\n          {\n            header: 'UL Speed',\n            size: 88,\n            accessorFn: ({ uploadSpeed }) =>\n              parseTraffic(uploadSpeed).join(' ') + '/s',\n            sortingFn: (rowA, rowB) =>\n              (rowA.original.uploadSpeed || 0) -\n              (rowB.original.uploadSpeed || 0),\n          },\n          {\n            header: 'Chains',\n            size: 360,\n            accessorFn: ({ chains }) => [...chains].reverse().join(' / '),\n          },\n          {\n            header: 'Rule',\n            size: 200,\n            accessorFn: ({ rule, rulePayload }) =>\n              rulePayload ? `${rule} (${rulePayload})` : rule,\n          },\n          {\n            header: 'Time',\n            size: 120,\n            accessorFn: ({ start }) => dayjs(start).fromNow(),\n            sortingFn: (rowA, rowB) =>\n              dayjs(rowA.original.start).diff(rowB.original.start),\n          },\n          {\n            header: 'Source',\n            size: 200,\n            accessorFn: ({ metadata: { sourceIP, sourcePort } }) =>\n              `${sourceIP}:${sourcePort}`,\n          },\n          {\n            header: 'Destination IP',\n            size: 200,\n            accessorFn: ({ metadata: { destinationIP, destinationPort } }) =>\n              `${destinationIP}:${destinationPort}`,\n          },\n          {\n            header: 'Destination ASN',\n            size: 200,\n            accessorFn: ({ metadata: { destinationIPASN } }) =>\n              `${destinationIPASN}`,\n          },\n          {\n            header: 'Type',\n            size: 160,\n            accessorFn: ({ metadata }) =>\n              `${metadata.type} (${metadata.network})`,\n          },\n        ] satisfies Array<MRT_ColumnDef<TableConnection>>\n      ).map(\n        (column) =>\n          ({\n            ...column,\n            id: snakeCase(column.header),\n            header: t(column.header),\n          }) satisfies MRT_ColumnDef<TableConnection>,\n      ),\n    [t],\n  )\n}\n\nexport type ConnectionColumnFilterDialogProps = {} & Omit<\n  BaseDialogProps,\n  'title'\n>\n\nfunction ColItem({\n  column,\n  checked,\n  onChange,\n  value,\n}: {\n  column: MRT_ColumnDef<TableConnection>\n  checked: boolean\n  onChange: (e: React.ChangeEvent<HTMLInputElement>) => void\n  value: [string, boolean]\n}) {\n  const controls = useDragControls()\n  return (\n    <Reorder.Item\n      value={value}\n      dragListener={false}\n      dragControls={controls}\n      className=\"flex gap-1\"\n    >\n      <div className=\"flex-1\">\n        <Checkbox checked={checked} onChange={onChange} />\n        {column.header}\n      </div>\n      <div className=\"w-12\">\n        <IconButton onPointerDown={(e) => controls.start(e)}>\n          <Menu />\n        </IconButton>\n      </div>\n    </Reorder.Item>\n  )\n}\n\nexport default function ConnectionColumnFilterDialog(\n  props: ConnectionColumnFilterDialogProps,\n) {\n  const { t } = useTranslation()\n  const columns = useColumns()\n  const [filteredCols, setFilteredCols] = useAtom(connectionTableColumnsAtom)\n  const sortedCols = useMemo(\n    () =>\n      columns\n        .filter((o) => o.id !== 'actions')\n        .sort((a, b) => {\n          const aIndex = filteredCols.findIndex((o) => o[0] === a.id)\n          const bIndex = filteredCols.findIndex((o) => o[0] === b.id)\n          if (aIndex === -1 && bIndex === -1) {\n            return 0\n          }\n          if (aIndex === -1) {\n            return 1\n          }\n          if (bIndex === -1) {\n            return -1\n          }\n          return aIndex - bIndex\n        }),\n    [columns, filteredCols],\n  )\n\n  const latestFilteredCols = sortedCols.map((column) => [\n    column.id,\n    filteredCols.find((o) => o[0] === column.id)?.[1] ?? true,\n  ]) as Array<[string, boolean]>\n\n  return (\n    <BaseDialog title={t('Connection Columns')} {...props}>\n      <div className=\"grid grid-cols-1 gap-1\">\n        <AnimatePresence>\n          <Reorder.Group\n            values={latestFilteredCols}\n            onReorder={setFilteredCols}\n          >\n            {sortedCols.map((column, index) => (\n              <ColItem\n                key={column.id}\n                column={column}\n                checked={\n                  filteredCols.find((o) => o[0] === column.id)?.[1] ?? true\n                }\n                onChange={(e) => {\n                  console.log(e.target.checked)\n                  const newCols = [...filteredCols]\n                  newCols[index] = [newCols[index][0], e.target.checked]\n                  console.log(newCols)\n                  setFilteredCols(newCols)\n                }}\n                value={latestFilteredCols[index]}\n              />\n            ))}\n          </Reorder.Group>\n        </AnimatePresence>\n      </div>\n    </BaseDialog>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/connections/connections-table.tsx",
    "content": "/* eslint-disable camelcase */\nimport { useAtomValue } from 'jotai'\nimport { cloneDeep } from 'lodash-es'\nimport { MaterialReactTable, useMaterialReactTable } from 'material-react-table'\nimport { MRT_Localization_EN } from 'material-react-table/locales/en'\nimport { MRT_Localization_RU } from 'material-react-table/locales/ru'\nimport { MRT_Localization_ZH_HANS } from 'material-react-table/locales/zh-Hans'\nimport { MRT_Localization_ZH_HANT } from 'material-react-table/locales/zh-Hant'\nimport { lazy, useDeferredValue, useMemo, useRef, useState } from 'react'\nimport { useTranslation } from 'react-i18next'\nimport { connectionTableColumnsAtom } from '@/store'\nimport { containsSearchTerm } from '@/utils'\nimport {\n  useClashConnections,\n  type ClashConnection,\n  type ClashConnectionItem,\n} from '@nyanpasu/interface'\nimport ContentDisplay from '../base/content-display'\nimport { useColumns } from './connections-column-filter'\n\nconst ConnectionDetailDialog = lazy(() => import('./connection-detail-dialog'))\n\nexport type TableConnection = ClashConnectionItem & {\n  downloadSpeed?: number\n  uploadSpeed?: number\n}\n\nexport interface TableMessage extends Omit<ClashConnection, 'connections'> {\n  connections: TableConnection[]\n}\n\nexport const ConnectionsTable = ({ searchTerm }: { searchTerm?: string }) => {\n  const { t, i18n } = useTranslation()\n\n  const {\n    query: { data: clashConnections, isLoading },\n  } = useClashConnections()\n\n  const historyMessage = useRef<TableMessage | null>(null)\n\n  const connectionsMessage = useMemo(() => {\n    const result = clashConnections?.at(-1)\n\n    if (!result) {\n      return historyMessage.current\n    }\n\n    const updatedConnections: TableConnection[] = []\n\n    const filteredConnections = searchTerm\n      ? result.connections?.filter((connection) =>\n          containsSearchTerm(connection, searchTerm),\n        )\n      : result.connections\n\n    filteredConnections?.forEach((connection) => {\n      const previousConnection = historyMessage.current?.connections.find(\n        (history) => history.id === connection.id,\n      )\n\n      const downloadSpeed = previousConnection\n        ? connection.download - previousConnection.download\n        : 0\n\n      const uploadSpeed = previousConnection\n        ? connection.upload - previousConnection.upload\n        : 0\n\n      updatedConnections.push({\n        ...connection,\n        downloadSpeed,\n        uploadSpeed,\n      })\n    })\n\n    const data = { ...result, connections: updatedConnections }\n\n    historyMessage.current = data\n\n    return data\n  }, [clashConnections, searchTerm])\n\n  const deferredTableData = useDeferredValue(connectionsMessage?.connections)\n\n  const locale = useMemo(() => {\n    switch (i18n.language) {\n      case 'zh-CN':\n        return MRT_Localization_ZH_HANS\n      case 'zh-TW':\n        return MRT_Localization_ZH_HANT\n      case 'ru':\n        return MRT_Localization_RU\n      case 'en':\n      default:\n        return MRT_Localization_EN\n    }\n  }, [i18n.language])\n\n  const columns = useColumns()\n  const tableColsOrder = useAtomValue(connectionTableColumnsAtom)\n  const filteredColumns = useMemo(\n    () =>\n      columns\n        .filter(\n          (column) =>\n            tableColsOrder.find((o) => o[0] === column.id)?.[1] ?? true,\n        )\n        .sort((a, b) => {\n          const aIndex = tableColsOrder.findIndex((o) => o[0] === a.id)\n          const bIndex = tableColsOrder.findIndex((o) => o[0] === b.id)\n          if (aIndex === -1 && bIndex === -1) {\n            return 0\n          }\n          if (aIndex === -1) {\n            return 1\n          }\n          if (bIndex === -1) {\n            return -1\n          }\n          return aIndex - bIndex\n        }),\n    [columns, tableColsOrder],\n  )\n  const columnOrder = useMemo(\n    () => filteredColumns.map((column) => column.id) as string[],\n    [filteredColumns],\n  )\n\n  const columnVisibility = useMemo(() => {\n    return filteredColumns.reduce(\n      (acc, column) => {\n        acc[column.id as string] =\n          tableColsOrder.find((o) => o[0] === column.id)?.[1] ?? true\n        return acc\n      },\n      {} as Record<string, boolean>,\n    )\n  }, [filteredColumns, tableColsOrder])\n\n  const [connectionDetailDialogOpen, setConnectionDetailDialogOpen] =\n    useState(false)\n  const [connectioNDetailDialogItem, setConnectionDetailDialogItem] = useState<\n    ClashConnectionItem | undefined\n  >(undefined)\n\n  const table = useMaterialReactTable({\n    columns: filteredColumns,\n    data: deferredTableData ?? [],\n    initialState: {\n      density: 'compact',\n      columnPinning: {\n        left: ['actions'],\n      },\n    },\n    state: {\n      columnOrder,\n      columnVisibility,\n    },\n    defaultDisplayColumn: {\n      enableResizing: true,\n    },\n    enableTopToolbar: false,\n    enableColumnActions: false,\n    enablePagination: false,\n    enableBottomToolbar: false,\n    enableColumnResizing: true,\n    enableGlobalFilterModes: true,\n    enableColumnPinning: true,\n    muiTableContainerProps: {\n      sx: { minHeight: '100%' },\n      className: '!absolute !h-full !w-full',\n    },\n    muiTableBodyRowProps({ row }) {\n      return {\n        onClick() {\n          const id = row.original.id\n          const item = connectionsMessage?.connections.find((o) => o.id === id)\n          if (item) {\n            setConnectionDetailDialogItem(cloneDeep(item))\n            setConnectionDetailDialogOpen(true)\n          }\n        },\n      }\n    },\n    localization: locale,\n    enableRowVirtualization: true,\n    enableColumnVirtualization: true,\n    rowVirtualizerOptions: { overscan: 5 },\n    columnVirtualizerOptions: { overscan: 2 },\n  })\n\n  // Show loading state while data is being fetched\n  if (isLoading && !connectionsMessage) {\n    // Don't show a separate loading indicator here since the parent component already handles it\n    return null\n  }\n\n  return connectionsMessage?.connections.length ? (\n    <>\n      <ConnectionDetailDialog\n        item={connectioNDetailDialogItem}\n        open={connectionDetailDialogOpen}\n        onClose={() => setConnectionDetailDialogOpen(false)}\n      />\n\n      <MaterialReactTable table={table} />\n    </>\n  ) : (\n    <ContentDisplay\n      className=\"!absolute !h-full !w-full\"\n      message={t('No Connections')}\n    />\n  )\n}\n\nexport default ConnectionsTable\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/connections/connections-total.tsx",
    "content": "import { filesize } from 'filesize'\nimport { useEffect, useRef, useState } from 'react'\nimport { Download, Upload } from '@mui/icons-material'\nimport { Paper, Skeleton } from '@mui/material'\nimport type { SxProps, Theme } from '@mui/material/styles'\nimport { useClashConnections } from '@nyanpasu/interface'\nimport { darken, lighten } from '@nyanpasu/ui'\n\nexport default function ConnectionTotal() {\n  const {\n    query: { data: clashConnections, isLoading },\n  } = useClashConnections()\n\n  const latestClashConnections = clashConnections?.at(-1)\n\n  const [downloadHighlight, setDownloadHighlight] = useState(false)\n  const [uploadHighlight, setUploadHighlight] = useState(false)\n\n  const downloadHighlightTimerRef = useRef<number | null>(null)\n  const uploadHighlightTimerRef = useRef<number | null>(null)\n\n  useEffect(() => {\n    if (\n      latestClashConnections?.downloadTotal &&\n      latestClashConnections?.downloadTotal > 0\n    ) {\n      setDownloadHighlight(true)\n      if (downloadHighlightTimerRef.current) {\n        clearTimeout(downloadHighlightTimerRef.current)\n      }\n      downloadHighlightTimerRef.current = window.setTimeout(() => {\n        setDownloadHighlight(false)\n      }, 300)\n    }\n  }, [latestClashConnections?.downloadTotal])\n\n  useEffect(() => {\n    if (\n      latestClashConnections?.uploadTotal &&\n      latestClashConnections?.uploadTotal > 0\n    ) {\n      setUploadHighlight(true)\n      if (uploadHighlightTimerRef.current) {\n        clearTimeout(uploadHighlightTimerRef.current)\n      }\n      uploadHighlightTimerRef.current = window.setTimeout(() => {\n        setUploadHighlight(false)\n      }, 300)\n    }\n  }, [latestClashConnections?.uploadTotal])\n\n  // Show skeleton loading state while data is being fetched\n  if (isLoading || !latestClashConnections) {\n    return (\n      <div className=\"flex gap-2\">\n        <Paper\n          elevation={0}\n          className=\"flex min-h-8 items-center justify-center gap-1 px-2\"\n          sx={{\n            borderRadius: '1em',\n          }}\n        >\n          <Download className=\"scale-75\" />\n          <Skeleton variant=\"text\" width={60} height={20} />\n        </Paper>\n\n        <Paper\n          elevation={0}\n          className=\"flex min-h-8 items-center justify-center gap-1 px-2\"\n          sx={{\n            borderRadius: '1em',\n          }}\n        >\n          <Upload className=\"scale-75\" />\n          <Skeleton variant=\"text\" width={60} height={20} />\n        </Paper>\n      </div>\n    )\n  }\n\n  return (\n    <div className=\"flex gap-2\">\n      <Paper\n        elevation={0}\n        className=\"flex min-h-8 items-center justify-center gap-1 px-2\"\n        sx={{\n          borderRadius: '1em',\n        }}\n      >\n        <Download\n          className=\"scale-75\"\n          sx={\n            ((theme) => ({\n              color: darken(\n                theme.vars.palette.primary.main,\n                downloadHighlight ? 0.9 : 0.3,\n              ),\n              ...theme.applyStyles('dark', {\n                color: lighten(\n                  theme.vars.palette.primary.main,\n                  downloadHighlight ? 0.2 : 0.9,\n                ),\n              }),\n            })) as SxProps<Theme>\n          }\n        />{' '}\n        <span className=\"font-mono text-xs\">\n          {filesize(latestClashConnections.downloadTotal, { pad: true })}\n        </span>\n      </Paper>\n\n      <Paper\n        elevation={0}\n        className=\"flex min-h-8 items-center justify-center gap-1 px-2\"\n        sx={{\n          borderRadius: '1em',\n        }}\n      >\n        <Upload\n          className=\"scale-75\"\n          sx={\n            ((theme) => ({\n              color: darken(\n                theme.vars.palette.primary.main,\n                uploadHighlight ? 0.9 : 0.3,\n              ),\n              ...theme.applyStyles('dark', {\n                color: lighten(\n                  theme.vars.palette.primary.main,\n                  downloadHighlight ? 0.2 : 0.9,\n                ),\n              }),\n            })) as SxProps<Theme>\n          }\n        />{' '}\n        <span className=\"font-mono text-xs\">\n          {filesize(latestClashConnections.uploadTotal, { pad: true })}\n        </span>\n      </Paper>\n    </div>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/connections/header-search.tsx",
    "content": "import { useTranslation } from 'react-i18next'\nimport { FilledInputProps, TextField, TextFieldProps } from '@mui/material'\nimport { alpha } from '@nyanpasu/ui'\n\nexport const HeaderSearch = (props: TextFieldProps) => {\n  const { t } = useTranslation()\n\n  const inputProps: Partial<FilledInputProps> = {\n    sx: (theme) => ({\n      borderRadius: 7,\n      backgroundColor: alpha(theme.vars.palette.primary.main, 0.1),\n\n      '&::before': {\n        display: 'none',\n      },\n\n      '&::after': {\n        display: 'none',\n      },\n    }),\n  }\n\n  return (\n    <TextField\n      autoComplete=\"off\"\n      spellCheck=\"false\"\n      hiddenLabel\n      placeholder={t('Filter conditions')}\n      variant=\"filled\"\n      className=\"!pb-0\"\n      sx={{ input: { py: 1, fontSize: 14 } }}\n      slotProps={{\n        input: inputProps,\n      }}\n      {...props}\n    />\n  )\n}\n\nexport default HeaderSearch\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/dashboard/data-panel.tsx",
    "content": "import { useAtomValue } from 'jotai'\nimport { useMemo } from 'react'\nimport { useTranslation } from 'react-i18next'\nimport Dataline, { DatalineProps } from '@/components/dashboard/dataline'\nimport { atomIsDrawer } from '@/store'\nimport {\n  ArrowDownward,\n  ArrowUpward,\n  MemoryOutlined,\n  SettingsEthernet,\n} from '@mui/icons-material'\nimport Grid from '@mui/material/Grid'\nimport {\n  MAX_CONNECTIONS_HISTORY,\n  MAX_MEMORY_HISTORY,\n  MAX_TRAFFIC_HISTORY,\n  useClashConnections,\n  useClashMemory,\n  useClashTraffic,\n  useSetting,\n} from '@nyanpasu/interface'\n\nexport const DataPanel = ({ visible = true }: { visible?: boolean }) => {\n  const { t } = useTranslation()\n\n  const { data: clashTraffic } = useClashTraffic()\n\n  const { data: clashMemory } = useClashMemory()\n\n  const {\n    query: { data: clashConnections },\n  } = useClashConnections()\n\n  const { value } = useSetting('clash_core')\n\n  const supportMemory = value && ['mihomo', 'mihomo-alpha'].includes(value)\n\n  const padData = (data: (number | undefined)[] = [], max: number) =>\n    Array(Math.max(0, max - data.length))\n      .fill(0)\n      .concat(data.slice(-max))\n\n  const Datalines: (DatalineProps & { visible?: boolean })[] = [\n    {\n      data: padData(\n        clashTraffic?.map((item) => item.down),\n        MAX_TRAFFIC_HISTORY,\n      ),\n      icon: ArrowDownward,\n      title: t('Download Traffic'),\n      total: clashConnections?.at(-1)?.downloadTotal,\n      type: 'speed',\n      visible,\n    },\n    {\n      data: padData(\n        clashTraffic?.map((item) => item.up),\n        MAX_TRAFFIC_HISTORY,\n      ),\n      icon: ArrowUpward,\n      title: t('Upload Traffic'),\n      total: clashConnections?.at(-1)?.uploadTotal,\n      type: 'speed',\n      visible,\n    },\n    {\n      data: padData(\n        clashConnections?.map((item) => item.connections?.length ?? 0),\n        MAX_CONNECTIONS_HISTORY,\n      ),\n      icon: SettingsEthernet,\n      title: t('Active Connections'),\n      type: 'raw',\n      visible,\n    },\n  ]\n\n  if (supportMemory) {\n    Datalines.splice(2, 0, {\n      data: padData(\n        clashMemory?.map((item) => item.inuse),\n        MAX_MEMORY_HISTORY,\n      ),\n      icon: MemoryOutlined,\n      title: t('Memory'),\n      visible,\n    })\n  }\n\n  const isDrawer = useAtomValue(atomIsDrawer)\n\n  const gridLayout = useMemo(\n    () => ({\n      sm: isDrawer ? 6 : 12,\n      md: 6,\n      lg: supportMemory ? 3 : 4,\n      xl: supportMemory ? 3 : 4,\n    }),\n    [isDrawer, supportMemory],\n  )\n\n  return Datalines.map((props, index) => {\n    return (\n      <Grid key={`data-${index}`} size={gridLayout}>\n        <Dataline {...props} className=\"max-h-1/8 min-h-40\" />\n      </Grid>\n    )\n  })\n}\n\nexport default DataPanel\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/dashboard/dataline.tsx",
    "content": "import { cloneElement, FC } from 'react'\nimport { useTranslation } from 'react-i18next'\nimport parseTraffic from '@/utils/parse-traffic'\nimport { type SvgIconComponent } from '@mui/icons-material'\nimport { Paper } from '@mui/material'\nimport { cn, Sparkline } from '@nyanpasu/ui'\n\nexport interface DatalineProps {\n  className?: string\n  data: number[]\n  icon: SvgIconComponent\n  title: string\n  total?: number\n  type?: 'speed' | 'raw'\n  visible?: boolean\n}\n\nexport const Dataline: FC<DatalineProps> = ({\n  data,\n  icon,\n  title,\n  total,\n  type,\n  className,\n  visible = true,\n}) => {\n  const { t } = useTranslation()\n\n  return (\n    <Paper className={cn('relative !rounded-3xl', className)}>\n      <Sparkline\n        data={data}\n        className=\"absolute rounded-3xl\"\n        {...(visible !== undefined ? { visible } : {})}\n      />\n\n      <div className=\"absolute top-0 flex h-full flex-col justify-between gap-4 p-4\">\n        <div className=\"flex items-center gap-2\">\n          {/* @ts-expect-error icon should be cloneable */}\n          {cloneElement(icon)}\n\n          <div className=\"font-bold\">{title}</div>\n        </div>\n\n        <div className=\"text-2xl font-bold text-shadow-md\">\n          {type === 'raw' ? data.at(-1) : parseTraffic(data.at(-1)).join(' ')}\n          {type === 'speed' && '/s'}\n        </div>\n\n        <div className=\"h-5\">\n          {total !== undefined && (\n            <span className=\"text-shadow-sm\">\n              {t('Total')}: {parseTraffic(total).join(' ')}\n            </span>\n          )}\n        </div>\n      </div>\n    </Paper>\n  )\n}\n\nexport default Dataline\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/dashboard/health-panel.tsx",
    "content": "import { useInterval } from 'ahooks'\nimport { useRef, useState } from 'react'\nimport { timing } from '@nyanpasu/interface'\nimport IPASNPanel from './modules/ipasn-panel'\nimport TimingPanel from './modules/timing-panel'\n\nconst REFRESH_SECONDS = 5\n\nexport const HealthPanel = () => {\n  const [health, setHealth] = useState({\n    Google: 0,\n    GitHub: 0,\n    BingCN: 0,\n    Baidu: 0,\n  })\n\n  const healthCache = useRef({\n    Google: 0,\n    GitHub: 0,\n    BingCN: 0,\n    Baidu: 0,\n  })\n\n  const [refreshCount, setRefreshCount] = useState(0)\n\n  useInterval(async () => {\n    setHealth(healthCache.current)\n\n    setRefreshCount(refreshCount + REFRESH_SECONDS)\n\n    healthCache.current = {\n      Google: await timing.Google(),\n      GitHub: await timing.GitHub(),\n      BingCN: await timing.BingCN(),\n      Baidu: await timing.Baidu(),\n    }\n  }, 1000 * REFRESH_SECONDS)\n\n  return (\n    <>\n      <TimingPanel data={health} />\n\n      <IPASNPanel refreshCount={refreshCount} />\n    </>\n  )\n}\n\nexport default HealthPanel\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/dashboard/modules/ipasn-panel.tsx",
    "content": "import { flag as countryCodeEmoji } from 'country-emoji'\nimport { useAtomValue } from 'jotai'\nimport { useState } from 'react'\nimport { useTranslation } from 'react-i18next'\nimport { atomIsDrawer } from '@/store'\nimport { Visibility, VisibilityOff } from '@mui/icons-material'\nimport {\n  Button,\n  CircularProgress,\n  IconButton,\n  Paper,\n  Tooltip,\n} from '@mui/material'\nimport Grid from '@mui/material/Grid'\nimport { useIPSB, useSetting } from '@nyanpasu/interface'\nimport { cn } from '@nyanpasu/ui'\n\nconst IP_REFRESH_SECONDS = 180\n\nconst EmojiCounty = ({ countryCode }: { countryCode: string }) => {\n  let emoji = countryCodeEmoji(countryCode)\n\n  if (!emoji) {\n    emoji = '🇺🇳'\n  }\n\n  return (\n    <div className=\"relative text-5xl select-none\">\n      <span className=\"opacity-50 blur\">{emoji}</span>\n\n      <span className=\"absolute left-0\">{emoji}</span>\n    </div>\n  )\n}\n\nconst MAX_WIDTH = 'calc(100% - 48px - 16px)'\n\nexport const IPASNPanel = ({ refreshCount }: { refreshCount: number }) => {\n  const { t } = useTranslation()\n\n  const { data, mutate, isValidating } = useIPSB()\n\n  const handleRefreshIP = () => {\n    mutate()\n  }\n\n  const [showIPAddress, setShowIPAddress] = useState(false)\n\n  const isDrawer = useAtomValue(atomIsDrawer)\n\n  const { value } = useSetting('clash_core')\n\n  const supportMemory = value && ['mihomo', 'mihomo-alpha'].includes(value)\n\n  return (\n    <Grid\n      size={{\n        sm: isDrawer ? (supportMemory ? 6 : 12) : 12,\n        md: supportMemory ? 8 : 12,\n        lg: supportMemory ? 5 : 8,\n        xl: 3,\n      }}\n    >\n      <Paper className=\"relative flex !h-full gap-4 !rounded-3xl px-4 py-3 select-text\">\n        {data ? (\n          <>\n            {data.country_code && (\n              <EmojiCounty countryCode={data.country_code} />\n            )}\n\n            <div className=\"flex flex-col gap-1\" style={{ width: MAX_WIDTH }}>\n              <div className=\"flex items-end justify-between text-xl font-bold text-shadow-md\">\n                <div className=\"truncate\">{data.country}</div>\n\n                <Tooltip title={t('Click to Refresh Now')}>\n                  <Button\n                    className=\"!size-8 !min-w-0\"\n                    onClick={handleRefreshIP}\n                    loading={isValidating}\n                  >\n                    {!isValidating && (\n                      <CircularProgress\n                        size={16}\n                        variant=\"determinate\"\n                        value={\n                          100 -\n                          ((refreshCount % IP_REFRESH_SECONDS) /\n                            IP_REFRESH_SECONDS) *\n                            100\n                        }\n                      />\n                    )}\n                  </Button>\n                </Tooltip>\n              </div>\n\n              <div className=\"truncate\">{data.organization}</div>\n\n              <div className=\"text-sm\">AS{data.asn}</div>\n\n              <div className=\"flex items-center justify-between gap-4\">\n                <div\n                  className=\"relative font-mono\"\n                  style={{ width: MAX_WIDTH }}\n                >\n                  <span\n                    className={cn(\n                      'block truncate transition-opacity',\n                      showIPAddress ? 'opacity-100' : 'opacity-0',\n                    )}\n                  >\n                    {data.ip}\n                  </span>\n\n                  <span\n                    className={cn(\n                      'absolute top-0 left-0 block h-full w-full rounded-lg bg-slate-300 transition-opacity',\n                      showIPAddress ? 'opacity-0' : 'animate-pulse opacity-100',\n                    )}\n                  />\n                </div>\n\n                <IconButton\n                  className=\"!size-8\"\n                  color=\"primary\"\n                  onClick={() => setShowIPAddress(!showIPAddress)}\n                >\n                  {showIPAddress ? <Visibility /> : <VisibilityOff />}\n                </IconButton>\n              </div>\n            </div>\n          </>\n        ) : (\n          <>\n            <div className=\"mt-1.5 mb-2 h-9 w-12 animate-pulse rounded-lg bg-slate-700\" />\n\n            <div className=\"flex flex-1 animate-pulse flex-col gap-1\">\n              <div className=\"mt-1.5 h-6 w-20 rounded-full bg-slate-700\" />\n\n              <div className=\"mt-1 h-5 w-44 rounded-full bg-slate-700\" />\n\n              <div className=\"mt-1 h-5 w-16 rounded-full bg-slate-700\" />\n\n              <div className=\"mt-1 h-6 w-32 rounded-lg bg-slate-700\" />\n            </div>\n          </>\n        )}\n      </Paper>\n    </Grid>\n  )\n}\n\nexport default IPASNPanel\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/dashboard/modules/timing-panel.tsx",
    "content": "import { useAtomValue } from 'jotai'\nimport { useTranslation } from 'react-i18next'\nimport { useColorSxForDelay } from '@/hooks/theme'\nimport { atomIsDrawer } from '@/store'\nimport { Box, Paper } from '@mui/material'\nimport Grid from '@mui/material/Grid'\nimport { useSetting } from '@nyanpasu/interface'\n\nfunction LatencyTag({ name, value }: { name: string; value: number }) {\n  const { t } = useTranslation()\n\n  const sx = useColorSxForDelay(value)\n\n  return (\n    <div className=\"flex justify-between gap-1\">\n      <div className=\"font-bold\">{name}:</div>\n\n      <Box className=\"truncate\" sx={sx}>\n        {value ? `${value.toFixed(0)} ms` : t('Timeout')}\n      </Box>\n    </div>\n  )\n}\n\nexport const TimingPanel = ({ data }: { data: { [key: string]: number } }) => {\n  const isDrawer = useAtomValue(atomIsDrawer)\n\n  const { value } = useSetting('clash_core')\n\n  const supportMemory = value && ['mihomo', 'mihomo-alpha'].includes(value)\n\n  return (\n    <Grid\n      size={{\n        sm: isDrawer ? 6 : 12,\n        md: supportMemory ? 4 : 6,\n        lg: supportMemory ? 3 : 4,\n        xl: 3,\n      }}\n    >\n      <Paper className=\"!h-full !rounded-3xl p-4\">\n        <div className=\"flex h-full flex-col justify-between\">\n          {Object.entries(data).map(([name, value]) => (\n            <LatencyTag key={name} name={name} value={value} />\n          ))}\n        </div>\n      </Paper>\n    </Grid>\n  )\n}\n\nexport default TimingPanel\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/dashboard/proxy-shortcuts.tsx",
    "content": "import { useLockFn } from 'ahooks'\nimport { useAtomValue } from 'jotai'\nimport { useMemo } from 'react'\nimport { useTranslation } from 'react-i18next'\nimport { atomIsDrawer } from '@/store'\nimport { formatError } from '@/utils'\nimport { message } from '@/utils/notification'\nimport { NetworkPing, SettingsEthernet } from '@mui/icons-material'\nimport { Chip, Paper, type ChipProps } from '@mui/material'\nimport Grid from '@mui/material/Grid'\nimport { useClashConfig, useSetting, useSystemProxy } from '@nyanpasu/interface'\nimport { PaperSwitchButton } from '../setting/modules/system-proxy'\n\nconst TitleComp = () => {\n  const { t } = useTranslation()\n\n  const { data } = useSystemProxy()\n\n  const {\n    query: { data: clashConfigs },\n  } = useClashConfig()\n\n  const status = useMemo<{\n    label: string\n    color: ChipProps['color']\n  }>(() => {\n    if (data?.enable) {\n      const port = Number(data.server.split(':')[1])\n\n      if (port === clashConfigs?.['mixed-port']) {\n        return {\n          label: t('Successful'),\n          color: 'success',\n        }\n      } else {\n        return {\n          label: t('Occupied'),\n          color: 'warning',\n        }\n      }\n    } else {\n      return {\n        label: t('Disabled'),\n        color: 'error',\n      }\n    }\n  }, [clashConfigs, data?.enable, data?.server, t])\n\n  return (\n    <div className=\"flex items-center gap-2 px-1\">\n      <div>{t('Proxy Takeover Status')}</div>\n\n      <Chip\n        color={status.color}\n        className=\"!h-5\"\n        sx={{\n          span: {\n            padding: '0 8px',\n          },\n        }}\n        label={status.label}\n      />\n    </div>\n  )\n}\n\nexport const ProxyShortcuts = () => {\n  const { t } = useTranslation()\n\n  const isDrawer = useAtomValue(atomIsDrawer)\n\n  const systemProxy = useSetting('enable_system_proxy')\n\n  const handleSystemProxy = useLockFn(async () => {\n    try {\n      await systemProxy.upsert(!systemProxy.value)\n    } catch (error) {\n      message(\n        `Activation System Proxy failed!\\n Error: ${formatError(error)}`,\n        {\n          title: t('Error'),\n          kind: 'error',\n        },\n      )\n    }\n  })\n\n  const tunMode = useSetting('enable_tun_mode')\n\n  const handleTunMode = useLockFn(async () => {\n    try {\n      await tunMode.upsert(!tunMode.value)\n    } catch (error) {\n      message(`Activation TUN Mode failed! \\n Error: ${formatError(error)}`, {\n        title: t('Error'),\n        kind: 'error',\n      })\n    }\n  })\n\n  return (\n    <Grid\n      size={{\n        sm: isDrawer ? 6 : 12,\n        md: 6,\n        lg: 4,\n        xl: 3,\n      }}\n    >\n      <Paper className=\"flex !h-full flex-col justify-between gap-2 !rounded-3xl p-3\">\n        <TitleComp />\n\n        <div className=\"flex gap-3\">\n          <div className=\"!w-full\">\n            <PaperSwitchButton\n              checked={systemProxy.value || false}\n              onClick={handleSystemProxy}\n            >\n              <div className=\"flex flex-col gap-2\">\n                <NetworkPing />\n\n                <div>{t('System Proxy')}</div>\n              </div>\n            </PaperSwitchButton>\n          </div>\n\n          <div className=\"!w-full\">\n            <PaperSwitchButton\n              checked={tunMode.value || false}\n              onClick={handleTunMode}\n            >\n              <div className=\"flex flex-col gap-2\">\n                <SettingsEthernet />\n\n                <div>{t('TUN Mode')}</div>\n              </div>\n            </PaperSwitchButton>\n          </div>\n        </div>\n      </Paper>\n    </Grid>\n  )\n}\n\nexport default ProxyShortcuts\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/dashboard/service-shortcuts.tsx",
    "content": "import dayjs from 'dayjs'\nimport { useAtomValue } from 'jotai'\nimport { isObject } from 'lodash-es'\nimport { useMemo } from 'react'\nimport { useTranslation } from 'react-i18next'\nimport useSWR from 'swr'\nimport { atomIsDrawer } from '@/store'\nimport { Box, CircularProgress, Paper, Tooltip } from '@mui/material'\nimport Grid from '@mui/material/Grid'\nimport type { SxProps, Theme } from '@mui/material/styles'\nimport { getCoreStatus, useSystemService } from '@nyanpasu/interface'\nimport { alpha } from '@nyanpasu/ui'\n\ntype Status = {\n  label: string\n  sx: SxProps<Theme>\n}\n\nexport const ServiceShortcuts = () => {\n  const { t } = useTranslation()\n\n  const isDrawer = useAtomValue(atomIsDrawer)\n\n  const {\n    query: { data: serviceStatus },\n  } = useSystemService()\n\n  // TODO: refactor to use tanstack query\n  const coreStatusSWR = useSWR('/coreStatus', getCoreStatus, {\n    refreshInterval: 2000,\n    revalidateOnFocus: false,\n  })\n\n  const status: Status = useMemo(() => {\n    switch (serviceStatus?.status) {\n      case 'running': {\n        return {\n          label: t('running'),\n          sx: ((theme) => ({\n            backgroundColor: alpha(theme.vars.palette.success.light, 0.3),\n            ...theme.applyStyles('dark', {\n              backgroundColor: alpha(theme.vars.palette.success.dark, 0.3),\n            }),\n          })) as SxProps<Theme>,\n        }\n      }\n\n      case 'stopped': {\n        return {\n          label: t('stopped'),\n          sx: ((theme) => ({\n            backgroundColor: alpha(theme.vars.palette.error.light, 0.3),\n            ...theme.applyStyles('dark', {\n              backgroundColor: alpha(theme.vars.palette.error.dark, 0.3),\n            }),\n          })) as SxProps<Theme>,\n        }\n      }\n\n      case 'not_installed':\n      default: {\n        return {\n          label: t('not_installed'),\n          sx: ((theme) => ({\n            backgroundColor: theme.vars.palette.grey[100],\n            ...theme.applyStyles('dark', {\n              backgroundColor: theme.vars.palette.background.paper,\n            }),\n          })) as SxProps<Theme>,\n        }\n      }\n    }\n  }, [serviceStatus, t])\n\n  const coreStatus: Status = useMemo(() => {\n    const status = coreStatusSWR.data || [{ Stopped: null }, 0, 'normal']\n    if (\n      isObject(status[0]) &&\n      Object.prototype.hasOwnProperty.call(status[0], 'Stopped')\n    ) {\n      const { Stopped } = status[0]\n      return {\n        label:\n          !!Stopped && Stopped.trim()\n            ? t('stopped_reason', { reason: Stopped })\n            : t('stopped'),\n        sx: ((theme) => ({\n          backgroundColor: alpha(theme.vars.palette.success.light, 0.3),\n          ...theme.applyStyles('dark', {\n            backgroundColor: alpha(theme.vars.palette.success.dark, 0.3),\n          }),\n        })) as SxProps<Theme>,\n      }\n    }\n    return {\n      label: t('service_shortcuts.core_started_by', {\n        by: t(status[2] === 'normal' ? 'UI' : 'service'),\n      }),\n      sx: ((theme) => ({\n        backgroundColor: alpha(theme.vars.palette.success.light, 0.3),\n        ...theme.applyStyles('dark', {\n          backgroundColor: alpha(theme.vars.palette.success.dark, 0.3),\n        }),\n      })) as SxProps<Theme>,\n    }\n  }, [coreStatusSWR.data, t])\n\n  return (\n    <Grid\n      size={{\n        sm: isDrawer ? 6 : 12,\n        md: 6,\n        lg: 4,\n        xl: 3,\n      }}\n    >\n      <Paper className=\"flex !h-full flex-col justify-between gap-2 !rounded-3xl p-3\">\n        {serviceStatus ? (\n          <>\n            <div className=\"text-center font-bold\">\n              {t('service_shortcuts.title')}\n            </div>\n\n            <div className=\"flex w-full flex-col gap-2\">\n              <Box\n                className=\"flex w-full justify-center gap-[2px] rounded-2xl py-2\"\n                sx={status.sx}\n              >\n                <div>{t('service_shortcuts.service_status')}</div>\n                <div>{t(status.label)}</div>\n              </Box>\n\n              <Box\n                className=\"flex w-full justify-center gap-[2px] rounded-2xl py-2\"\n                sx={coreStatus.sx}\n              >\n                <div>{t('service_shortcuts.core_status')}</div>\n                <Tooltip\n                  title={\n                    !!coreStatusSWR.data?.[1] &&\n                    t('service_shortcuts.last_status_changed_since', {\n                      time: dayjs(coreStatusSWR.data[1]).fromNow(),\n                    })\n                  }\n                >\n                  <div>{coreStatus.label}</div>\n                </Tooltip>\n              </Box>\n            </div>\n          </>\n        ) : (\n          <div className=\"flex w-full flex-col items-center justify-center gap-2\">\n            <CircularProgress />\n\n            <div>Loading...</div>\n          </div>\n        )}\n      </Paper>\n    </Grid>\n  )\n}\n\nexport default ServiceShortcuts\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/layout/animated-logo.module.d.scss.ts",
    "content": "declare const classNames: {\n  readonly LogoSchema: 'LogoSchema'\n}\nexport default classNames\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/layout/animated-logo.module.scss",
    "content": ".LogoSchema {\n  fill: var(--primary-main);\n\n  :global(#bg) {\n    fill: var(--background-color);\n  }\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/layout/animated-logo.module.scss.d.ts",
    "content": "declare const classNames: {\n  readonly LogoSchema: 'LogoSchema'\n}\nexport default classNames\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/layout/animated-logo.tsx",
    "content": "import {\n  AnimatePresence,\n  motion,\n  type Transition,\n  type Variants,\n} from 'framer-motion'\nimport { CSSProperties } from 'react'\nimport LogoSvg from '@/assets/image/logo.svg?react'\nimport { useSetting } from '@nyanpasu/interface'\nimport { cn } from '@nyanpasu/ui'\nimport styles from './animated-logo.module.scss'\n\nconst Logo = motion.create(LogoSvg)\n\nconst transition = {\n  type: 'spring',\n  stiffness: 260,\n  damping: 20,\n} satisfies Transition\n\nconst motionVariants: { [name: string]: Variants } = {\n  default: {\n    initial: {\n      opacity: 0,\n      scale: 0.5,\n      transition,\n    },\n    animate: {\n      opacity: 1,\n      scale: 1,\n      transition,\n    },\n    exit: {\n      opacity: 0,\n      scale: 0.5,\n      transition,\n    },\n    whileHover: {\n      scale: 1.1,\n      transition,\n    },\n  },\n  none: {\n    initial: {},\n    animate: {},\n    exit: {},\n  },\n}\n\nexport default function AnimatedLogo({\n  className,\n  style,\n  disableMotion,\n}: {\n  className?: string\n  style?: CSSProperties\n  disableMotion?: boolean\n}) {\n  const { value } = useSetting('lighten_animation_effects')\n\n  const disable = disableMotion ?? value\n\n  return (\n    <AnimatePresence initial={false}>\n      <Logo\n        className={cn(styles.LogoSchema, className)}\n        variants={motionVariants[disable ? 'none' : 'default']}\n        style={style}\n        drag\n        dragConstraints={{ left: 0, right: 0, top: 0, bottom: 0 }}\n        whileDrag={{ scale: 1.15 }}\n        dragSnapToOrigin\n        dragElastic={0.1}\n      />\n    </AnimatePresence>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/layout/layout-control.tsx",
    "content": "import { useMemoizedFn } from 'ahooks'\nimport { useEffect, useRef } from 'react'\nimport {\n  CloseRounded,\n  CropSquareRounded,\n  FilterNoneRounded,\n  HorizontalRuleRounded,\n  PushPin,\n  PushPinOutlined,\n} from '@mui/icons-material'\nimport { Button, ButtonProps } from '@mui/material'\nimport { commands, useSetting } from '@nyanpasu/interface'\nimport { alpha, cn } from '@nyanpasu/ui'\nimport { useQueryClient, useSuspenseQuery } from '@tanstack/react-query'\nimport { listen, TauriEvent, UnlistenFn } from '@tauri-apps/api/event'\nimport { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'\nimport { platform as getPlatform } from '@tauri-apps/plugin-os'\n\nconst appWindow = getCurrentWebviewWindow()\n\nconst CtrlButton = (props: ButtonProps) => {\n  return (\n    <Button\n      className=\"!size-8 !min-w-0\"\n      sx={(theme) => ({\n        backgroundColor: alpha(theme.vars.palette.primary.main, 0.1),\n        svg: { transform: 'scale(0.9)' },\n      })}\n      {...props}\n    />\n  )\n}\n\nexport const LayoutControl = ({ className }: { className?: string }) => {\n  const { value: alwaysOnTop, upsert } = useSetting('always_on_top')\n\n  const { data: isMaximized } = useSuspenseQuery({\n    queryKey: ['isMaximized'],\n    queryFn: () => appWindow.isMaximized(),\n  })\n  const queryClient = useQueryClient()\n  const unlistenRef = useRef<UnlistenFn | null>(null)\n  const platform = useRef(getPlatform())\n\n  useEffect(() => {\n    listen(TauriEvent.WINDOW_RESIZED, () => {\n      queryClient.invalidateQueries({ queryKey: ['isMaximized'] })\n    })\n      .then((unlisten) => {\n        unlistenRef.current = unlisten\n      })\n      .catch((error) => {\n        console.error(error)\n      })\n  }, [queryClient])\n\n  const toggleAlwaysOnTop = useMemoizedFn(async () => {\n    await upsert(!alwaysOnTop)\n    await appWindow.setAlwaysOnTop(!alwaysOnTop)\n  })\n\n  return (\n    <div className={cn('flex gap-1', className)} data-tauri-drag-region>\n      <CtrlButton onClick={toggleAlwaysOnTop}>\n        {alwaysOnTop ? (\n          <PushPin fontSize=\"small\" style={{ transform: 'rotate(15deg)' }} />\n        ) : (\n          <PushPinOutlined\n            fontSize=\"small\"\n            style={{ transform: 'rotate(15deg)' }}\n          />\n        )}\n      </CtrlButton>\n\n      <CtrlButton onClick={() => appWindow.minimize()}>\n        <HorizontalRuleRounded fontSize=\"small\" />\n      </CtrlButton>\n\n      <CtrlButton\n        onClick={() => {\n          appWindow.toggleMaximize().then(() => {\n            queryClient.invalidateQueries({ queryKey: ['isMaximized'] })\n          })\n        }}\n      >\n        {isMaximized ? (\n          <FilterNoneRounded\n            fontSize=\"small\"\n            style={{\n              transform: 'rotate(180deg) scale(0.8)',\n            }}\n          />\n        ) : (\n          <CropSquareRounded fontSize=\"small\" />\n        )}\n      </CtrlButton>\n\n      <CtrlButton\n        onClick={() => {\n          if (platform.current === 'windows') {\n            commands.saveWindowSizeState(appWindow.label).finally(() => {\n              appWindow.close()\n            })\n          } else {\n            appWindow.close()\n          }\n        }}\n      >\n        <CloseRounded fontSize=\"small\" />\n      </CtrlButton>\n    </div>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/layout/mutation-provider.tsx",
    "content": "import { useEffect, useRef } from 'react'\nimport { useTranslation } from 'react-i18next'\nimport { useGlobalMutation } from '@/utils/mutation'\nimport { notification, NotificationType } from '@/utils/notification'\nimport { listen, UnlistenFn } from '@tauri-apps/api/event'\n\nexport default function MutationProvider() {\n  const { t } = useTranslation()\n  const unlistenFn = useRef<UnlistenFn>(null)\n  const mutate = useGlobalMutation()\n  useEffect(() => {\n    listen<'nyanpasu_config' | 'clash_config' | 'proxies' | 'profiles'>(\n      'nyanpasu://mutation',\n      ({ payload }) => {\n        switch (payload) {\n          case 'nyanpasu_config':\n            mutate((key) => {\n              if (typeof key === 'string') {\n                return (\n                  key.includes('nyanpasuConfig') ||\n                  key.includes('getProxies') ||\n                  key.includes('getProfiles')\n                )\n              }\n              return false\n            })\n            break\n          case 'clash_config':\n            mutate((key) => {\n              if (typeof key === 'string') {\n                return (\n                  key.includes('getClashRules') ||\n                  key.includes('getClashInfo') ||\n                  key.includes('getClashVersion') ||\n                  key.includes('getProxies') ||\n                  key.includes('getProfiles') ||\n                  key.includes('getRulesProviders') ||\n                  key.includes('getProxiesProviders') ||\n                  key.includes('getAllProxiesProviders')\n                )\n              }\n              return false\n            })\n            break\n          case 'proxies':\n            mutate(\n              (key) => typeof key === 'string' && key.includes('getProxies'),\n            )\n            break\n          case 'profiles':\n            mutate((key) => {\n              if (typeof key === 'string') {\n                return (\n                  key.includes('getClashRules') ||\n                  key.includes('getClashInfo') ||\n                  key.includes('getClashVersion') ||\n                  key.includes('getProxies') ||\n                  key.includes('getProfiles') ||\n                  key.includes('getRulesProviders') ||\n                  key.includes('getProxiesProviders') ||\n                  key.includes('getAllProxiesProviders')\n                )\n              }\n              return false\n            })\n            break\n        }\n      },\n    )\n      .then((unlisten) => {\n        unlistenFn.current = unlisten\n      })\n      .catch((e) => {\n        notification({\n          title: t('Error'),\n          body: e.message,\n          type: NotificationType.Error,\n        })\n      })\n    return () => {\n      unlistenFn.current?.()\n    }\n  }, [mutate, t])\n  return null\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/layout/notice-provider.tsx",
    "content": "// oxlint-disable no-unsafe-optional-chaining\nimport { useEffect, useRef } from 'react'\nimport { useTranslation } from 'react-i18next'\nimport { notification, NotificationType } from '@/utils/notification'\nimport { listen, UnlistenFn } from '@tauri-apps/api/event'\n\nexport const NoticeProvider = () => {\n  const { t } = useTranslation()\n  const unlistenFn = useRef<UnlistenFn>(null)\n  useEffect(() => {\n    listen<{\n      set_config: { ok: string } | { err: string }\n    }>('nyanpasu://notice-message', ({ payload }) => {\n      if ('ok' in payload?.set_config) {\n        notification({\n          title: t('Successful'),\n          body: 'Refresh Clash Config',\n          type: NotificationType.Success,\n        })\n      } else if ('err' in payload?.set_config) {\n        notification({\n          title: t('Error'),\n          body: payload.set_config.err,\n          type: NotificationType.Error,\n        })\n      }\n    })\n      .then((unlisten) => {\n        unlistenFn.current = unlisten\n      })\n      .catch((e) => {\n        notification({\n          title: t('Error'),\n          body: e.message,\n          type: NotificationType.Error,\n        })\n      })\n    return () => {\n      unlistenFn.current?.()\n    }\n  }, [t])\n\n  return null\n}\n\nexport default NoticeProvider\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/layout/page-transition.tsx",
    "content": "import { AnimatePresence, type Transition, type Variant } from 'framer-motion'\nimport { useSetting } from '@nyanpasu/interface'\nimport { cn } from '@nyanpasu/ui'\nimport { useMatch, useMatches } from '@tanstack/react-router'\nimport { AnimatedOutlet } from '../router/animated-outlet'\n\ntype PageVariantKey = 'initial' | 'visible' | 'hidden'\n\ntype PageVariant = {\n  [key in PageVariantKey]: Variant\n}\n\nconst commonTransition = {\n  type: 'spring',\n  bounce: 0,\n  duration: 0.35,\n} satisfies Transition\n\nexport const pageTransitionVariants: { [name: string]: PageVariant } = {\n  blur: {\n    initial: { opacity: 0, filter: 'blur(10px)' },\n    visible: { opacity: 1, filter: 'blur(0px)' },\n    hidden: { opacity: 0, filter: 'blur(10px)' },\n  },\n  slide: {\n    initial: {\n      translateY: '30%',\n      opacity: 0,\n      scale: 0.95,\n    },\n    visible: {\n      translateY: '0%',\n      opacity: 1,\n      scale: 1,\n      transition: commonTransition,\n    },\n    hidden: {\n      opacity: 0,\n      scale: 0.9,\n      transition: commonTransition,\n    },\n  },\n  transparent: {\n    initial: { opacity: 0 },\n    visible: { opacity: 1 },\n    hidden: { opacity: 0 },\n  },\n}\n\nexport default function PageTransition({ className }: { className?: string }) {\n  const { value: lightenAnimationEffects } = useSetting(\n    'lighten_animation_effects',\n  )\n\n  const matches = useMatches()\n  const match = useMatch({ strict: false })\n  const nextMatchIndex = matches.findIndex((d) => d.id === match.id) + 1\n  const nextMatch = matches[nextMatchIndex]\n\n  const variants = lightenAnimationEffects\n    ? pageTransitionVariants.transparent\n    : pageTransitionVariants.slide\n\n  return (\n    <AnimatePresence mode=\"popLayout\" initial={false}>\n      <AnimatedOutlet\n        className={cn('page-transition', className)}\n        key={nextMatch ? nextMatch.id : ''}\n        layout\n        layoutId={nextMatch.id}\n        variants={variants}\n        initial=\"initial\"\n        animate=\"visible\"\n        exit=\"hidden\"\n      />\n    </AnimatePresence>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/layout/scheme-provider.tsx",
    "content": "import { useEffect, useRef } from 'react'\nimport { useNavigate } from '@tanstack/react-router'\nimport { listen, UnlistenFn } from '@tauri-apps/api/event'\n\nexport const SchemeProvider = () => {\n  const navigate = useNavigate()\n  const unlistenRef = useRef<UnlistenFn | null>(null)\n  useEffect(() => {\n    const run = async () => {\n      unlistenRef.current = await listen('scheme-request-received', (req) => {\n        const message: string = req.payload as string\n\n        const url = new URL(message)\n        // ref: https://github.com/nodejs/node/issues/44476\n        // Support whatwg style { hostname: \"first\", pathname: \"others\" }\n        // Support firefox, chromium style { hostname: \"\", pathname: \"//path\"}\n        let pathname = (url.hostname || '') + (url.pathname || '')\n        if (pathname.endsWith('/')) {\n          pathname = pathname.slice(0, -1)\n        }\n        if (pathname.startsWith('//')) {\n          pathname = pathname.slice(2)\n        }\n        console.log('received', url, pathname)\n        switch (pathname) {\n          case 'install-config':\n          case 'subscribe-remote-profile':\n            console.log('redirect to profile page')\n            navigate({\n              to: '/profiles',\n              search: {\n                subscribeUrl: url.searchParams.get('url') || undefined,\n                subscribeName: url.searchParams.has('name')\n                  ? decodeURIComponent(url.searchParams.get('name')!)\n                  : undefined,\n                subscribeDesc: url.searchParams.has('desc')\n                  ? decodeURIComponent(url.searchParams.get('desc')!)\n                  : undefined,\n              },\n            })\n        }\n      })\n    }\n    run()\n    return () => {\n      unlistenRef.current?.()\n    }\n  }, [navigate])\n\n  return null\n}\n\nexport default SchemeProvider\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/layout/use-custom-theme.tsx",
    "content": "import { useAtomValue, useSetAtom } from 'jotai'\nimport { PropsWithChildren, useEffect, useMemo } from 'react'\nimport { themeMode as themeModeAtom } from '@/store'\nimport { alpha, darken, lighten, Theme, useColorScheme } from '@mui/material'\nimport { ThemeProvider } from '@mui/material/styles'\nimport { useSetting } from '@nyanpasu/interface'\nimport { cn, createMDYTheme } from '@nyanpasu/ui'\nimport { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'\n\nexport const DEFAULT_COLOR = '#1867c0'\n\nexport const DEFAULT_FONT_FAMILY = `\"Roboto\", \"Helvetica\", \"Arial\", sans-serif, \"Color Emoji Flags\",\" Color Emoji\"`\n\nconst appWindow = getCurrentWebviewWindow()\n\nconst applyRootStyleVar = (mode: 'light' | 'dark', theme: Theme) => {\n  const root = document.documentElement\n  const palette = theme.palette\n\n  const isLightMode = mode !== 'light'\n  root.className = cn(mode === 'dark' ? 'dark' : 'light')\n  const backgroundColor = isLightMode\n    ? darken(palette.secondary.dark, 0.95)\n    : lighten(palette.secondary.light, 0.95)\n\n  const selectionColor = isLightMode ? '#d5d5d5' : '#f5f5f5'\n  const scrollerColor = isLightMode ? '#54545480' : '#90939980'\n\n  root.style.setProperty('--background-color', backgroundColor)\n  root.style.setProperty('--selection-color', selectionColor)\n  root.style.setProperty('--scroller-color', scrollerColor)\n  root.style.setProperty('--primary-main', palette.primary.main)\n  root.style.setProperty(\n    '--background-color-alpha',\n    alpha(palette.primary.main, 0.1),\n  )\n\n  const reactRootDom = document.getElementById('root')\n  if (reactRootDom) {\n    reactRootDom.className = cn(mode === 'dark' ? 'dark' : 'light')\n  }\n}\n\nexport const CustomTheme = ({ children }: PropsWithChildren) => {\n  const themeMode = useAtomValue(themeModeAtom)\n\n  const { value: themeColor } = useSetting('theme_color')\n\n  const theme = useMemo(() => {\n    const color = themeColor || DEFAULT_COLOR\n\n    const mergedTheme = createMDYTheme(color, DEFAULT_FONT_FAMILY)\n    applyRootStyleVar(themeMode, mergedTheme)\n\n    return mergedTheme\n  }, [themeColor, themeMode])\n\n  return <ThemeProvider theme={theme}>{children}</ThemeProvider>\n}\n\nconst ThemeInner = ({ children }: PropsWithChildren) => {\n  const { value: themeMode } = useSetting('theme_mode')\n\n  const setThemeMode = useSetAtom(themeModeAtom)\n\n  const { setMode } = useColorScheme()\n\n  useEffect(() => {\n    if (themeMode === 'system') {\n      appWindow.theme().then((m) => {\n        if (m) {\n          setThemeMode(m)\n          setMode(m)\n        }\n      })\n    } else {\n      const chosenThemeMode = (themeMode as 'light' | 'dark') || 'light'\n      setThemeMode(chosenThemeMode)\n      setMode(chosenThemeMode)\n    }\n\n    const unlisten = appWindow.onThemeChanged((e) => {\n      if (themeMode === 'system') {\n        setThemeMode(e.payload)\n        setMode(e.payload)\n      }\n    })\n\n    return () => {\n      unlisten.then((fn) => fn())\n    }\n  }, [setMode, setThemeMode, themeMode])\n\n  return children\n}\n\nexport const ThemeModeProvider = ({ children }: PropsWithChildren) => {\n  return (\n    <CustomTheme>\n      <ThemeInner>{children}</ThemeInner>\n    </CustomTheme>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/logo/animated-logo.tsx",
    "content": "import { motion, useAnimationControls } from 'framer-motion'\nimport {\n  ComponentProps,\n  KeyboardEvent,\n  useCallback,\n  useEffect,\n  useRef,\n} from 'react'\nimport LogoSvg from '@/assets/image/logo.svg?react'\n\nconst FAST_SPRING = [0.22, 1, 0.36, 1] as const // fast attack, soft landing\nconst DRAMATIC_PRESENT = [0.2, 0.8, 0.2, 1] as const // dramatic entrance\nconst GENTLE_SYMMETRIC_S_CURVE = [0.45, 0.05, 0.55, 0.95] as const // gentle symmetric S-curve\n\nexport default function AnimatedLogo({\n  indeterminate,\n  ...props\n}: Omit<ComponentProps<typeof motion.div>, 'children'> & {\n  indeterminate?: boolean\n}) {\n  const logoControls = useAnimationControls()\n  const intensityRef = useRef(0)\n  const directionRef = useRef(1)\n  const resetTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)\n  const indeterminateRef = useRef(false)\n\n  useEffect(() => {\n    return () => {\n      if (resetTimeoutRef.current) {\n        clearTimeout(resetTimeoutRef.current)\n      }\n    }\n  }, [])\n\n  const startSway = useCallback(() => {\n    logoControls.start({\n      rotate: [0, 2, 0, -2, 0],\n      scale: [1, 1.02, 1, 0.98, 1],\n      transition: {\n        duration: 2.4,\n        ease: GENTLE_SYMMETRIC_S_CURVE,\n        repeat: Infinity,\n        repeatType: 'loop',\n      },\n    })\n  }, [logoControls])\n\n  // Indeterminate mode: init animation then continuous slow sway\n  useEffect(() => {\n    indeterminateRef.current = !!indeterminate\n\n    if (indeterminate) {\n      let cancelled = false\n\n      const run = async () => {\n        // Init animation: blur fade in\n        logoControls.set({\n          filter: 'blur(10px)',\n          opacity: 0,\n          scale: 2,\n        })\n        await logoControls.start({\n          filter: 'blur(0px)',\n          opacity: 1,\n          scale: 1,\n          transition: {\n            duration: 1,\n            ease: DRAMATIC_PRESENT,\n          },\n        })\n\n        if (cancelled) {\n          return\n        }\n\n        // Continuous slow sway\n        startSway()\n      }\n\n      run()\n\n      return () => {\n        cancelled = true\n      }\n    } else {\n      // Reset to idle\n      logoControls.stop()\n      logoControls.start({\n        rotate: 0,\n        scale: 1,\n        filter: 'blur(0px)',\n        opacity: 1,\n        transition: { duration: 0.4, ease: FAST_SPRING },\n      })\n    }\n  }, [indeterminate, logoControls, startSway])\n\n  const scheduleReset = useCallback(() => {\n    if (resetTimeoutRef.current) {\n      clearTimeout(resetTimeoutRef.current)\n    }\n\n    // reset intensity after 1 seconds\n    resetTimeoutRef.current = setTimeout(async () => {\n      intensityRef.current = 0\n\n      if (indeterminateRef.current) {\n        // Smoothly reset scale first, then resume sway\n        await logoControls.start({\n          scale: 1,\n          rotate: 0,\n          transition: {\n            duration: 0.4,\n            ease: FAST_SPRING,\n          },\n        })\n        if (indeterminateRef.current) {\n          startSway()\n        }\n      } else {\n        logoControls.start({\n          scale: 1,\n          transition: {\n            duration: 0.4,\n            ease: FAST_SPRING,\n          },\n        })\n      }\n    }, 1000)\n  }, [logoControls, startSway])\n\n  const triggerShake = useCallback(() => {\n    const nextIntensity = Math.min(12, intensityRef.current + 1)\n    intensityRef.current = nextIntensity\n\n    // alternate direction each click\n    const d = directionRef.current\n    directionRef.current = -d\n\n    // non-linear amplitude: ramps up slowly then accelerates\n    const amp = 4 + Math.pow(nextIntensity, 1.4) * 0.8\n\n    // calculate the scale based on the intensity\n    const scale = 1 + Math.min(0.75, Math.pow(nextIntensity, 1.3) * 0.03)\n\n    // apply the animation to logo\n    logoControls.start({\n      rotate: [-amp * d, amp * d, -amp * 0.8 * d, amp * 0.8 * d, 0],\n      scale,\n      transition: {\n        duration: 0.5,\n        ease: 'linear',\n        rotate: {\n          duration: 0.5,\n          ease: [0.1, 0.8, 0.2, 1],\n        },\n      },\n    })\n\n    scheduleReset()\n  }, [logoControls, scheduleReset])\n\n  const handleKeyDown = useCallback(\n    (event: KeyboardEvent<HTMLDivElement>) => {\n      if (event.key === 'Enter' || event.key === ' ') {\n        event.preventDefault()\n        triggerShake()\n      }\n    },\n    [triggerShake],\n  )\n\n  return (\n    <motion.div\n      data-slot=\"app-header-logo\"\n      data-tauri-drag-region\n      role=\"button\"\n      tabIndex={0}\n      onClick={triggerShake}\n      onKeyDown={handleKeyDown}\n      animate={logoControls}\n      aria-label=\"Animate logo\"\n      {...props}\n    >\n      <LogoSvg\n        className=\"logo-colorized h-full w-full\"\n        data-tauri-drag-region\n      />\n    </motion.div>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/logs/clear-log-button.tsx",
    "content": "import { useLockFn } from 'ahooks'\nimport { useTranslation } from 'react-i18next'\nimport { Close } from '@mui/icons-material'\nimport { Tooltip } from '@mui/material'\nimport { useClashLogs } from '@nyanpasu/interface'\nimport { FloatingButton } from '@nyanpasu/ui'\n\nexport const ClearLogButton = () => {\n  const { t } = useTranslation()\n\n  const { clean } = useClashLogs()\n\n  const handleClean = useLockFn(async () => {\n    await clean.mutateAsync()\n  })\n\n  return (\n    <Tooltip title={t('Clear')}>\n      <FloatingButton onClick={handleClean}>\n        <Close className=\"absolute !size-8\" />\n      </FloatingButton>\n    </Tooltip>\n  )\n}\n\nexport default ClearLogButton\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/logs/log-filter.tsx",
    "content": "import { useTranslation } from 'react-i18next'\nimport { FilledInputProps, TextField } from '@mui/material'\nimport { alpha } from '@nyanpasu/ui'\nimport { useLogContext } from './log-provider'\n\nexport const LogFilter = () => {\n  const { t } = useTranslation()\n\n  const { filterText, setFilterText } = useLogContext()\n\n  const inputProps: Partial<FilledInputProps> = {\n    sx: (theme) => ({\n      borderRadius: 7,\n      backgroundColor: alpha(theme.vars.palette.primary.main, 0.1),\n\n      fieldset: {\n        border: 'none',\n      },\n    }),\n  }\n\n  return (\n    <TextField\n      hiddenLabel\n      autoComplete=\"off\"\n      spellCheck=\"false\"\n      value={filterText}\n      placeholder={t('Filter conditions')}\n      onChange={(e) => setFilterText(e.target.value)}\n      className=\"!pb-0\"\n      sx={{ input: { py: 1, fontSize: 14 } }}\n      slotProps={{\n        input: inputProps,\n      }}\n    />\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/logs/log-item.module.scss",
    "content": ".item {\n  :global(.shiki) {\n    margin-bottom: 0;\n    background-color: transparent !important;\n\n    * {\n      font-family: var(--item-font);\n    }\n\n    span {\n      white-space: normal;\n    }\n  }\n\n  &.dark {\n    :global(.shiki) {\n      span {\n        color: var(--shiki-dark) !important;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/logs/log-item.module.scss.d.ts",
    "content": "declare const classNames: {\n  readonly item: 'item'\n  readonly shiki: 'shiki'\n  readonly dark: 'dark'\n}\nexport default classNames\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/logs/log-item.tsx",
    "content": "import { useAsyncEffect } from 'ahooks'\nimport { CSSProperties, useState } from 'react'\nimport { formatAnsi } from '@/utils/shiki'\nimport { Box, SxProps, Theme, useColorScheme } from '@mui/material'\nimport { LogMessage } from '@nyanpasu/interface'\nimport { cn } from '@nyanpasu/ui'\nimport styles from './log-item.module.scss'\n\nconst colorMapping: { [key: string]: SxProps<Theme> } = {\n  error: (theme) => ({\n    color: theme.vars.palette.error.main,\n  }),\n  warning: (theme) => ({\n    color: theme.vars.palette.warning.main,\n  }),\n  info: (theme) => ({\n    color: theme.vars.palette.info.main,\n  }),\n}\n\nexport const LogItem = ({\n  value,\n  className,\n}: {\n  value: LogMessage\n  className?: string\n}) => {\n  const [payload, setPayload] = useState(value.payload)\n\n  const { mode } = useColorScheme()\n\n  useAsyncEffect(async () => {\n    setPayload(await formatAnsi(value.payload))\n  }, [value.payload])\n\n  return (\n    <div\n      className={cn('w-full p-4 pt-2 pb-0 font-mono select-text', className)}\n    >\n      <div className=\"flex gap-2\">\n        <span className=\"font-thin\">{value.time}</span>\n\n        <Box\n          component=\"span\"\n          className=\"inline-block font-semibold uppercase\"\n          sx={colorMapping[value.type]}\n        >\n          {value.type}\n        </Box>\n      </div>\n\n      <div className=\"pb-2 text-wrap\">\n        <div\n          className={cn(styles.item, mode === 'dark' && styles.dark)}\n          style={\n            {\n              '--item-font': 'var(--font-mono)',\n            } as CSSProperties\n          }\n          dangerouslySetInnerHTML={{\n            __html: payload,\n          }}\n        />\n      </div>\n    </div>\n  )\n}\n\nexport default LogItem\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/logs/log-level.tsx",
    "content": "import { useState } from 'react'\nimport { Button, Menu, MenuItem } from '@mui/material'\nimport { alpha } from '@nyanpasu/ui'\nimport { useLogContext } from './log-provider'\n\nexport const LogLevel = () => {\n  const { logLevel, setLogLevel } = useLogContext()\n\n  const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null)\n\n  const handleClick = (value: string) => {\n    setAnchorEl(null)\n    setLogLevel(value)\n  }\n\n  const mapping: { [key: string]: string } = {\n    all: 'ALL',\n    inf: 'INFO',\n    warn: 'WARN',\n    err: 'ERROR',\n  }\n\n  return (\n    <>\n      <Button\n        size=\"small\"\n        sx={(theme) => ({\n          textTransform: 'none',\n          backgroundColor: alpha(theme.vars.palette.primary.main, 0.1),\n        })}\n        onClick={(e) => setAnchorEl(e.currentTarget)}\n      >\n        {mapping[logLevel]}\n      </Button>\n\n      <Menu\n        anchorEl={anchorEl}\n        open={Boolean(anchorEl)}\n        onClose={() => setAnchorEl(null)}\n      >\n        {Object.entries(mapping).map(([key, value], index) => {\n          return (\n            <MenuItem key={index} onClick={() => handleClick(key)}>\n              {value}\n            </MenuItem>\n          )\n        })}\n      </Menu>\n    </>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/logs/log-list.tsx",
    "content": "import { useDebounceEffect } from 'ahooks'\nimport { RefObject, useDeferredValue, useEffect, useRef } from 'react'\nimport { useTranslation } from 'react-i18next'\nimport { Virtualizer, VirtualizerHandle } from 'virtua'\nimport { cn } from '@nyanpasu/ui'\nimport ContentDisplay from '../base/content-display'\nimport LogItem from './log-item'\nimport { useLogContext } from './log-provider'\n\nexport const LogList = ({\n  scrollRef,\n}: {\n  scrollRef: RefObject<HTMLElement>\n}) => {\n  const { t } = useTranslation()\n\n  const { logs, logLevel } = useLogContext()\n\n  const virtualizerRef = useRef<VirtualizerHandle>(null)\n\n  const shouldStickToBottom = useRef(true)\n\n  const isFirstScroll = useRef(true)\n\n  useDebounceEffect(\n    () => {\n      if (shouldStickToBottom && logs?.length) {\n        virtualizerRef.current?.scrollToIndex(logs?.length - 1, {\n          align: 'end',\n          smooth: !isFirstScroll.current,\n        })\n\n        isFirstScroll.current = false\n      }\n    },\n    [logs],\n    { wait: 100 },\n  )\n\n  useEffect(() => {\n    isFirstScroll.current = true\n  }, [logLevel])\n\n  const handleScroll = (_offset: number) => {\n    const end = virtualizerRef.current?.findEndIndex() || 0\n    if (end + 1 === logs?.length) {\n      shouldStickToBottom.current = true\n    } else {\n      shouldStickToBottom.current = false\n    }\n  }\n\n  const deferredLogs = useDeferredValue(logs)\n\n  return deferredLogs?.length ? (\n    <Virtualizer\n      ref={virtualizerRef}\n      scrollRef={scrollRef}\n      onScroll={handleScroll}\n    >\n      {deferredLogs?.map((item, index) => {\n        return (\n          <LogItem\n            key={index}\n            className={cn(index !== 0 && 'border-t border-zinc-500')}\n            value={item}\n          />\n        )\n      })}\n    </Virtualizer>\n  ) : (\n    <ContentDisplay className=\"absolute\" message={t('No Logs')} />\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/logs/log-page.tsx",
    "content": "import { RefObject } from 'react'\nimport ClearLogButton from './clear-log-button'\nimport { LogList } from './log-list'\n\nexport const LogPage = ({\n  scrollRef,\n}: {\n  scrollRef: RefObject<HTMLElement>\n}) => {\n  return (\n    <>\n      <LogList scrollRef={scrollRef} />\n\n      <ClearLogButton />\n    </>\n  )\n}\n\nexport default LogPage\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/logs/log-provider.tsx",
    "content": "import {\n  createContext,\n  PropsWithChildren,\n  useContext,\n  useMemo,\n  useState,\n} from 'react'\nimport { useClashLogs, type ClashLog } from '@nyanpasu/interface'\n\nconst LogContext = createContext<{\n  logs?: ClashLog[]\n  filterText: string\n  setFilterText: (text: string) => void\n  logLevel: string\n  setLogLevel: (level: string) => void\n} | null>(null)\n\nexport const useLogContext = () => {\n  const context = useContext(LogContext)\n\n  if (!context) {\n    throw new Error('useLogContext must be used within LogProvider')\n  }\n\n  return context\n}\n\nexport const LogProvider = ({ children }: PropsWithChildren) => {\n  const [filterText, setFilterText] = useState('')\n\n  const [logLevel, setLogLevel] = useState('all')\n\n  const {\n    query: { data },\n  } = useClashLogs()\n\n  const logs = useMemo(() => {\n    return data?.filter((log) => {\n      const matchesFilter =\n        !filterText ||\n        log.payload.toLowerCase().includes(filterText.toLowerCase())\n\n      const matchesLevel = logLevel === 'all' ? true : log.type === logLevel\n\n      return matchesFilter && matchesLevel\n    })\n  }, [data, filterText, logLevel])\n\n  return (\n    <LogContext.Provider\n      value={{\n        logs,\n        filterText,\n        setFilterText,\n        logLevel,\n        setLogLevel,\n      }}\n    >\n      {children}\n    </LogContext.Provider>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/logs/log-toggle.tsx",
    "content": "import {\n  PauseCircleOutlineRounded,\n  PlayCircleOutlineRounded,\n} from '@mui/icons-material'\nimport { IconButton } from '@mui/material'\nimport { useClashLogs } from '@nyanpasu/interface'\n\nexport const LogToggle = () => {\n  const { status, disable, enable } = useClashLogs()\n\n  const handleClick = () => {\n    if (status) {\n      disable()\n    } else {\n      enable()\n    }\n  }\n\n  return (\n    <IconButton size=\"small\" color=\"inherit\" onClick={handleClick}>\n      {status ? <PauseCircleOutlineRounded /> : <PlayCircleOutlineRounded />}\n    </IconButton>\n  )\n}\n\nexport default LogToggle\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/logs/los-header.tsx",
    "content": "import { LogFilter } from './log-filter'\nimport { LogLevel } from './log-level'\nimport LogToggle from './log-toggle'\n\nexport const LogHeader = () => {\n  return (\n    <div className=\"flex gap-2\">\n      <LogToggle />\n\n      <LogLevel />\n\n      <LogFilter />\n    </div>\n  )\n}\n\nexport default LogHeader\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/profiles/modules/chain-item.tsx",
    "content": "import { Reorder } from 'framer-motion'\nimport { memo, PointerEvent, useRef, useState, useTransition } from 'react'\nimport { useTranslation } from 'react-i18next'\nimport { Menu as MenuIcon } from '@mui/icons-material'\nimport { Button, ListItemButton, Menu, MenuItem } from '@mui/material'\nimport { ProfileQueryResultItem } from '@nyanpasu/interface'\nimport { alpha, cleanDeepClickEvent } from '@nyanpasu/ui'\n\nconst longPressDelay = 200\n\ninterface Context {\n  global: boolean\n  scoped: boolean\n}\n\nexport const ChainItem = memo(function ChainItem({\n  item,\n  selected,\n  context,\n  onClick,\n  onChainEdit,\n}: {\n  item: ProfileQueryResultItem\n  selected?: boolean\n  context?: Context\n  onClick: () => Promise<void>\n  onChainEdit: () => void\n}) {\n  const { t } = useTranslation()\n\n  const [isPending, startTransition] = useTransition()\n\n  const handleClick = () => {\n    startTransition(onClick)\n  }\n\n  const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null)\n\n  const isChainIncluded = selected // Based on the 'selected' prop which indicates if the chain is active\n\n  const menuMapping = {\n    [isChainIncluded ? 'Disable' : 'Enable']: () => handleClick(),\n    'Edit Info': () => onChainEdit(),\n    'Open File': () => item.view && item.view(),\n    Delete: () => item.drop && item.drop(),\n  }\n\n  const handleMenuClick = (func: () => void) => {\n    setAnchorEl(null)\n    func()\n  }\n\n  // const controls = useDragControls();\n\n  const onLongPress = (e: PointerEvent) => {\n    cleanDeepClickEvent(e)\n    // controls.start(e);\n  }\n\n  const longPressTimerRef = useRef<number | null>(null)\n\n  return (\n    <>\n      <Reorder.Item\n        css={{\n          zIndex: 100,\n        }}\n        value={item.uid}\n        // dragListener={false}\n        // dragControls={controls}\n        onPointerDown={(e: PointerEvent) => {\n          longPressTimerRef.current = window.setTimeout(() => {\n            longPressTimerRef.current = null\n            onLongPress(e as unknown as PointerEvent)\n          }, longPressDelay)\n        }}\n        onPointerUp={(e: PointerEvent) => {\n          if (longPressTimerRef.current) {\n            clearTimeout(longPressTimerRef.current!)\n          } else {\n            cleanDeepClickEvent(e)\n            longPressTimerRef.current = null\n          }\n        }}\n      >\n        <ListItemButton\n          className=\"!mt-2 !mb-2 !flex !justify-between gap-2\"\n          sx={[\n            {\n              borderRadius: 4,\n            },\n            (theme) => ({\n              backgroundColor: selected\n                ? alpha(theme.vars.palette.primary.main, 0.3)\n                : alpha(theme.vars.palette.secondary.main, 0.1),\n            }),\n            (theme) => ({\n              '&:hover': {\n                backgroundColor: selected\n                  ? alpha(theme.vars.palette.primary.main, 0.5)\n                  : null,\n              },\n            }),\n          ]}\n          onClick={handleClick}\n          disabled={isPending}\n        >\n          <div className=\"truncate py-1\">\n            <span>{item.name}</span>\n            <div className=\"mt-1 flex gap-1\">\n              {context?.global && (\n                <span className=\"rounded bg-blue-500 px-1 py-0.5 text-xs text-white\">\n                  G\n                </span>\n              )}\n              {context?.scoped && (\n                <span className=\"rounded bg-green-500 px-1 py-0.5 text-xs text-white\">\n                  S\n                </span>\n              )}\n            </div>\n          </div>\n\n          <Button\n            size=\"small\"\n            color=\"primary\"\n            className=\"!size-8 !min-w-0\"\n            loading={isPending}\n            onClick={(e) => {\n              cleanDeepClickEvent(e)\n              setAnchorEl(e!.currentTarget as HTMLButtonElement)\n            }}\n          >\n            <MenuIcon />\n          </Button>\n        </ListItemButton>\n      </Reorder.Item>\n      <Menu\n        anchorEl={anchorEl}\n        open={Boolean(anchorEl)}\n        onClose={() => setAnchorEl(null)}\n      >\n        {Object.entries(menuMapping).map(([key, func], index) => {\n          return (\n            <MenuItem\n              key={index}\n              onClick={(e) => {\n                cleanDeepClickEvent(e)\n                handleMenuClick(func)\n              }}\n            >\n              {t(key)}\n            </MenuItem>\n          )\n        })}\n      </Menu>\n    </>\n  )\n})\n\nexport default ChainItem\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/profiles/modules/language-chip.tsx",
    "content": "import { Box } from '@mui/material'\nimport { alpha } from '@nyanpasu/ui'\n\nexport const LanguageChip = ({ lang }: { lang: string }) => {\n  return (\n    lang && (\n      <Box\n        className=\"my-auto rounded-full px-2 py-0.5 text-center text-sm font-bold\"\n        sx={(theme) => ({\n          backgroundColor: alpha(theme.vars.palette.primary.main, 0.1),\n          color: theme.vars.palette.primary.main,\n        })}\n      >\n        {lang}\n      </Box>\n    )\n  )\n}\n\nexport default LanguageChip\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/profiles/modules/side-chain.tsx",
    "content": "import { useLockFn } from 'ahooks'\nimport { Reorder } from 'framer-motion'\nimport { useAtomValue } from 'jotai'\nimport { useMemo } from 'react'\nimport { useTranslation } from 'react-i18next'\nimport { formatError } from '@/utils'\nimport { message } from '@/utils/notification'\nimport { Add } from '@mui/icons-material'\nimport { ListItemButton } from '@mui/material'\nimport { ProfileQueryResultItem, useProfile } from '@nyanpasu/interface'\nimport { alpha } from '@nyanpasu/ui'\nimport { ClashProfile, ClashProfileBuilder, filterProfiles } from '../utils'\nimport ChainItem from './chain-item'\nimport { atomChainsSelected, atomGlobalChainCurrent } from './store'\n\nexport interface SideChainProps {\n  onChainEdit: (item?: ProfileQueryResultItem) => void | Promise<void>\n}\n\nexport const SideChain = ({ onChainEdit }: SideChainProps) => {\n  const { t } = useTranslation()\n\n  const isGlobalChainCurrent = useAtomValue(atomGlobalChainCurrent)\n\n  const currentProfileUid = useAtomValue(atomChainsSelected)\n\n  const { query, upsert, patch, sort } = useProfile()\n\n  const profiles = query.data\n\n  const { clash, chain } = filterProfiles(profiles?.items)\n\n  const currentProfile = useMemo(() => {\n    return clash?.find((item) => item.uid === currentProfileUid) as ClashProfile\n  }, [clash, currentProfileUid])\n\n  // Filter chains to show only relevant ones based on global/local context\n  const filteredChains = useMemo(() => {\n    if (isGlobalChainCurrent) {\n      // When in global chain mode, show all chain profiles\n      return chain || []\n    } else {\n      // In local chain mode, show all chain profiles so user can add them to the current profile's chain\n      // This is the expected behavior for local chains - users should see all available chains to choose from\n      return chain || []\n    }\n  }, [chain, isGlobalChainCurrent])\n\n  const handleChainClick = useLockFn(async (uid: string) => {\n    const chains = isGlobalChainCurrent\n      ? (profiles?.chain ?? [])\n      : (currentProfile?.chain ?? [])\n\n    const updatedChains = chains.includes(uid)\n      ? chains.filter((chain) => chain !== uid)\n      : [...chains, uid]\n\n    try {\n      if (isGlobalChainCurrent) {\n        await upsert.mutateAsync({ chain: updatedChains })\n      } else {\n        if (!currentProfile?.uid) {\n          return\n        }\n        await patch.mutateAsync({\n          uid: currentProfile.uid,\n          profile: {\n            ...(currentProfile as ClashProfileBuilder),\n            chain: updatedChains,\n          },\n        })\n      }\n    } catch (e) {\n      message(`Apply error: ${formatError(e)}`, {\n        kind: 'error',\n        title: t('Error'),\n      })\n    }\n  })\n\n  const reorderValues = useMemo(\n    () => filteredChains?.map((item) => item.uid) || [],\n    [filteredChains],\n  )\n\n  return (\n    <div className=\"h-full overflow-auto !pr-2 !pl-2\">\n      <Reorder.Group\n        axis=\"y\"\n        values={reorderValues}\n        onReorder={(values) => {\n          const profileUids = clash?.map((item) => item.uid) || []\n          sort.mutate([...profileUids, ...values])\n        }}\n        layoutScroll\n        style={{ overflowY: 'scroll' }}\n      >\n        {filteredChains?.map((item, index) => {\n          const selected = isGlobalChainCurrent\n            ? profiles?.chain?.includes(item.uid)\n            : currentProfile?.chain?.includes(item.uid)\n\n          // Check if chain is used in global context\n          const usedInGlobal = profiles?.chain?.includes(item.uid) ?? false\n\n          // Check if chain is used in current profile context\n          const usedInCurrentProfile =\n            currentProfile?.chain?.includes(item.uid) ?? false\n\n          return (\n            <ChainItem\n              key={index}\n              item={item}\n              selected={selected}\n              context={{\n                global: usedInGlobal,\n                scoped: usedInCurrentProfile,\n              }}\n              onClick={async () => await handleChainClick(item.uid)}\n              onChainEdit={() => onChainEdit(item)}\n            />\n          )\n        })}\n      </Reorder.Group>\n\n      <ListItemButton\n        className=\"!mt-2 !mb-2 flex justify-center gap-2\"\n        sx={(theme) => ({\n          backgroundColor: alpha(theme.vars.palette.secondary.main, 0.1),\n          borderRadius: 4,\n        })}\n        onClick={() => onChainEdit()}\n      >\n        <Add color=\"primary\" />\n\n        <div className=\"py-1\">{t('New Chain')}</div>\n      </ListItemButton>\n    </div>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/profiles/modules/side-log.tsx",
    "content": "import { useAtomValue } from 'jotai'\nimport { memo, useMemo } from 'react'\nimport { useTranslation } from 'react-i18next'\nimport { VList } from 'virtua'\nimport { RamenDining, Terminal } from '@mui/icons-material'\nimport { Divider } from '@mui/material'\nimport { usePostProcessingOutput, useProfile } from '@nyanpasu/interface'\nimport { cn } from '@nyanpasu/ui'\nimport { atomChainsSelected, atomGlobalChainCurrent } from './store'\n\nconst LogListItem = memo(function LogListItem({\n  name,\n  item,\n  showDivider,\n}: {\n  name?: string\n  item?: [string, string]\n  showDivider?: boolean\n}) {\n  return (\n    <>\n      {showDivider && <Divider />}\n\n      <div className=\"w-full font-mono break-all\">\n        <span className=\"rounded-sm bg-blue-600 px-0.5\">{name}</span>\n        <span className=\"text-red-500\"> [{item?.[0]}]: </span>\n        <span>{item?.[1]}</span>\n      </div>\n    </>\n  )\n})\n\nexport interface SideLogProps {\n  className?: string\n}\n\nexport const SideLog = ({ className }: SideLogProps) => {\n  const { t } = useTranslation()\n\n  const { query } = useProfile()\n\n  const profiles = query.data?.items\n\n  const { data } = usePostProcessingOutput()\n\n  const isGlobalChainCurrent = useAtomValue(atomGlobalChainCurrent)\n\n  const currentProfileUid = useAtomValue(atomChainsSelected)\n\n  const currentLogs = useMemo(() => {\n    if (currentProfileUid) {\n      return data?.scopes[currentProfileUid]\n    }\n\n    if (isGlobalChainCurrent) {\n      return data?.scopes.global\n    }\n  }, [currentProfileUid, data, isGlobalChainCurrent])\n\n  return (\n    <div className={cn('w-full', className)}>\n      <div className=\"flex items-center justify-between p-2 pl-4\">\n        <div className=\"flex items-center gap-2\">\n          <Terminal />\n\n          <span>{t('Console')}</span>\n        </div>\n      </div>\n\n      <Divider />\n\n      <VList className=\"flex flex-col gap-2 overflow-auto p-2 select-text\">\n        {currentLogs ? (\n          Object.entries(currentLogs).map(([uid, content]) => {\n            return content?.map((item, index) => {\n              const name = profiles?.find((script) => script.uid === uid)?.name\n\n              return (\n                <LogListItem\n                  key={uid + index}\n                  name={name}\n                  item={item}\n                  showDivider={index !== 0}\n                />\n              )\n            })\n          })\n        ) : (\n          <div className=\"flex h-full min-h-48 w-full flex-col items-center justify-center\">\n            <RamenDining className=\"!size-10\" />\n            <p>{t('No Logs')}</p>\n          </div>\n        )}\n      </VList>\n    </div>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/profiles/modules/store.ts",
    "content": "import { atom } from 'jotai'\n\nexport const atomGlobalChainCurrent = atom<boolean>(false)\n\nexport const atomChainsSelected = atom<string>()\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/profiles/new-profile-button.tsx",
    "content": "import { use, useEffect, useState } from 'react'\nimport { Add } from '@mui/icons-material'\nimport { cn, FloatingButton } from '@nyanpasu/ui'\nimport { AddProfileContext, ProfileDialog } from './profile-dialog'\n\nexport const NewProfileButton = ({ className }: { className?: string }) => {\n  const addProfileCtx = use(AddProfileContext)\n  const [open, setOpen] = useState(!!addProfileCtx)\n  useEffect(() => {\n    setOpen(!!addProfileCtx)\n  }, [addProfileCtx])\n  return (\n    <>\n      <FloatingButton className={cn(className)} onClick={() => setOpen(true)}>\n        <Add className=\"absolute !size-8\" />\n      </FloatingButton>\n\n      <ProfileDialog open={open} onClose={() => setOpen(false)} />\n    </>\n  )\n}\n\nexport default NewProfileButton\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/profiles/profile-dialog.tsx",
    "content": "import { version } from '~/package.json'\nimport { useAsyncEffect } from 'ahooks'\nimport { type editor } from 'monaco-editor'\nimport {\n  createContext,\n  lazy,\n  Suspense,\n  use,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n} from 'react'\nimport {\n  Controller,\n  SelectElement,\n  TextFieldElement,\n  useForm,\n} from 'react-hook-form-mui'\nimport { useTranslation } from 'react-i18next'\nimport { useLatest } from 'react-use'\nimport { formatError } from '@/utils'\nimport { message } from '@/utils/notification'\nimport { Divider, InputAdornment } from '@mui/material'\nimport {\n  ProfileQueryResultItem,\n  ProfileTemplate,\n  RemoteProfile,\n  useProfile,\n  useProfileContent,\n} from '@nyanpasu/interface'\nimport { BaseDialog } from '@nyanpasu/ui'\nimport { LabelSwitch } from '../setting/modules/clash-field'\nimport { ReadProfile } from './read-profile'\nimport { ClashProfile, ClashProfileBuilder } from './utils'\n\nconst ProfileMonacoViewer = lazy(() => import('./profile-monaco-viewer'))\n\nexport interface ProfileDialogProps {\n  profile?: ProfileQueryResultItem\n  open: boolean\n  onClose: () => void\n}\n\nexport type AddProfileContextValue = {\n  name: string | null\n  desc: string | null\n  url: string\n}\n\nexport const AddProfileContext = createContext<AddProfileContextValue | null>(\n  null,\n)\n\nexport const ProfileDialog = ({\n  profile,\n  open,\n  onClose,\n}: ProfileDialogProps) => {\n  const { t } = useTranslation()\n\n  const { create, patch } = useProfile()\n\n  const contentFn = useProfileContent(profile?.uid ?? '')\n\n  const localProfile = useRef('')\n  const addProfileCtx = use(AddProfileContext)\n  const [localProfileMessage, setLocalProfileMessage] = useState('')\n\n  const { control, watch, handleSubmit, reset, setValue } =\n    useForm<ClashProfileBuilder>({\n      defaultValues: (profile as ClashProfile) || {\n        type: 'remote',\n        name: addProfileCtx?.name || t(`New Profile`),\n        desc: addProfileCtx?.desc || '',\n        url: addProfileCtx?.url || '',\n        option: {\n          // user_agent: \"\",\n          with_proxy: false,\n          self_proxy: false,\n        },\n      },\n    })\n\n  useEffect(() => {\n    if (addProfileCtx) {\n      setValue('url', addProfileCtx.url)\n      if (addProfileCtx.desc) setValue('desc', addProfileCtx.desc)\n      if (addProfileCtx.name) setValue('name', addProfileCtx.name)\n    }\n  }, [addProfileCtx, setValue])\n\n  const isRemote = watch('type') === 'remote'\n\n  const [isEdit, setIsEdit] = useState(!!profile)\n  useEffect(() => {\n    setIsEdit(!!profile)\n  }, [profile])\n\n  const commonProps = useMemo(\n    () => ({\n      autoComplete: 'off',\n      autoCorrect: 'off',\n      fullWidth: true,\n    }),\n    [],\n  )\n\n  const handleProfileSelected = (content: string) => {\n    localProfile.current = content\n\n    setLocalProfileMessage('')\n  }\n\n  const [editor, setEditor] = useState({\n    value: '',\n    language: 'yaml',\n  })\n\n  const latestEditor = useLatest(editor)\n\n  const editorMarks = useRef<editor.IMarker[]>([])\n\n  const editorHasError = () =>\n    editorMarks.current.length > 0 &&\n    editorMarks.current.some((m) => m.severity === 8)\n\n  const onSubmit = handleSubmit(async (form) => {\n    if (editorHasError()) {\n      message('Please fix the error before saving', {\n        kind: 'error',\n      })\n      return\n    }\n\n    const toCreate = async () => {\n      if (isRemote) {\n        const data = form as RemoteProfile\n\n        await create.mutateAsync({\n          type: 'url',\n          data: {\n            url: data.url,\n            // TODO: define backend serde(option) to move null\n            option: data.option\n              ? {\n                  ...data.option,\n                  user_agent: data.option.user_agent ?? null,\n                  with_proxy: data.option.with_proxy ?? null,\n                  self_proxy: data.option.self_proxy ?? null,\n                }\n              : null,\n          },\n        })\n      } else {\n        if (localProfile.current) {\n          await create.mutateAsync({\n            type: 'manual',\n            data: {\n              item: form,\n              fileData: localProfile.current,\n            },\n          })\n        } else {\n          await create.mutateAsync({\n            type: 'manual',\n            data: {\n              item: form,\n              fileData: ProfileTemplate.profile,\n            },\n          })\n        }\n      }\n    }\n\n    const toUpdate = async () => {\n      const value = latestEditor.current.value\n      await contentFn.upsert.mutateAsync(value)\n\n      await patch.mutateAsync({\n        uid: form.uid!,\n        profile: form,\n      })\n    }\n\n    try {\n      if (isEdit) {\n        await toUpdate()\n      } else {\n        await toCreate()\n      }\n\n      setTimeout(() => reset(), 300)\n\n      onClose()\n    } catch (err) {\n      message('Failed to save profile: \\n' + formatError(err), {\n        kind: 'error',\n      })\n      console.error(err)\n    }\n  })\n\n  const dialogProps = isEdit && {\n    contentStyle: {\n      overflow: 'hidden',\n      padding: 0,\n    },\n    full: true,\n  }\n\n  const MetaInfo = useMemo(\n    () => (\n      <div className=\"flex flex-col gap-4 pt-2 pb-2\">\n        {!isEdit && (\n          <SelectElement\n            label={t('Type')}\n            name=\"type\"\n            control={control}\n            {...commonProps}\n            size=\"small\"\n            required\n            options={[\n              {\n                id: 'remote',\n                label: t('Remote Profile'),\n              },\n              {\n                id: 'local',\n                label: t('Local Profile'),\n              },\n            ]}\n          />\n        )}\n\n        <TextFieldElement\n          label={t('Name')}\n          name=\"name\"\n          control={control}\n          size=\"small\"\n          fullWidth\n          required\n        />\n\n        <TextFieldElement\n          label={t('Descriptions')}\n          name=\"desc\"\n          control={control}\n          {...commonProps}\n          size=\"small\"\n          multiline\n        />\n\n        {isRemote && (\n          <>\n            <TextFieldElement\n              label={t('Subscription URL')}\n              name=\"url\"\n              control={control}\n              {...commonProps}\n              size=\"small\"\n              multiline\n              required\n            />\n\n            <TextFieldElement\n              label={t('User Agent')}\n              name=\"option.user_agent\"\n              control={control}\n              {...commonProps}\n              size=\"small\"\n              placeholder={`clash-nyanpasu/v${version}`}\n            />\n\n            <TextFieldElement\n              label={t('Update Interval')}\n              name=\"option.update_interval\"\n              control={control}\n              {...commonProps}\n              size=\"small\"\n              type=\"number\"\n              InputProps={{\n                inputProps: { min: 0 },\n                endAdornment: (\n                  <InputAdornment position=\"end\">{t('minutes')}</InputAdornment>\n                ),\n              }}\n            />\n\n            <Controller\n              name=\"option.with_proxy\"\n              control={control}\n              render={({ field }) => (\n                <LabelSwitch\n                  label={t('Use System Proxy')}\n                  checked={Boolean(field.value)}\n                  {...field}\n                />\n              )}\n            />\n\n            <Controller\n              name=\"option.self_proxy\"\n              control={control}\n              render={({ field }) => (\n                <LabelSwitch\n                  label={t('Use Clash Proxy')}\n                  checked={Boolean(field.value)}\n                  {...field}\n                />\n              )}\n            />\n          </>\n        )}\n        {!isRemote && !isEdit && (\n          <>\n            <ReadProfile\n              key=\"read_profile\"\n              onSelected={handleProfileSelected}\n            />\n\n            {localProfileMessage && (\n              <div className=\"ml-2 text-red-500\">{localProfileMessage}</div>\n            )}\n            <span className=\"px-2 text-xs\">\n              * {t('Choose file to import or leave it blank to create new one')}\n            </span>\n          </>\n        )}\n      </div>\n    ),\n    [commonProps, control, isEdit, isRemote, localProfileMessage, t],\n  )\n\n  useAsyncEffect(async () => {\n    if (profile) {\n      reset(profile as ClashProfileBuilder)\n    }\n\n    if (isEdit) {\n      try {\n        const value = contentFn.query.data ?? ''\n        setEditor((editor) => ({ ...editor, value }))\n      } catch (error) {\n        console.error(error)\n      }\n    }\n  }, [open])\n\n  return (\n    <BaseDialog\n      title={isEdit ? t('Edit Profile') : t('Create Profile')}\n      open={open}\n      onClose={() => onClose()}\n      onOk={onSubmit}\n      divider\n      {...dialogProps}\n    >\n      {isEdit ? (\n        <div className=\"flex h-full\">\n          <div className=\"min-w-72 overflow-auto p-4\">{MetaInfo}</div>\n\n          <Divider orientation=\"vertical\" />\n\n          <Suspense fallback={null}>\n            {open && (\n              <ProfileMonacoViewer\n                className=\"w-full\"\n                readonly={isRemote}\n                schemaType=\"clash\"\n                value={editor.value}\n                onChange={(value) =>\n                  setEditor((editor) => ({ ...editor, value }))\n                }\n                onValidate={(marks) => (editorMarks.current = marks)}\n                language={editor.language}\n              />\n            )}\n          </Suspense>\n        </div>\n      ) : (\n        MetaInfo\n      )}\n    </BaseDialog>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/profiles/profile-item.tsx",
    "content": "import { useLockFn, useMemoizedFn, useSetState } from 'ahooks'\nimport dayjs from 'dayjs'\nimport { AnimatePresence, motion } from 'framer-motion'\nimport { memo, use, useEffect, useMemo, useState } from 'react'\nimport { useTranslation } from 'react-i18next'\nimport { formatError } from '@/utils'\nimport { message } from '@/utils/notification'\nimport parseTraffic from '@/utils/parse-traffic'\nimport {\n  FiberManualRecord,\n  FilterDrama,\n  InsertDriveFile,\n  Menu as MenuIcon,\n  Terminal,\n  Update,\n} from '@mui/icons-material'\nimport {\n  Badge,\n  Button,\n  Chip,\n  LinearProgress,\n  Menu,\n  MenuItem,\n  Paper,\n  Tooltip,\n} from '@mui/material'\nimport {\n  ProfileQueryResultItem,\n  RemoteProfile,\n  RemoteProfileOptionsBuilder,\n  useClashConnections,\n  useProfile,\n} from '@nyanpasu/interface'\nimport { alpha, cleanDeepClickEvent, cn } from '@nyanpasu/ui'\nimport { ProfileDialog } from './profile-dialog'\nimport { GlobalUpdatePendingContext } from './provider'\nimport { ClashProfile } from './utils'\n\nexport interface ProfileItemProps {\n  item: ProfileQueryResultItem\n  selected?: boolean\n  maxLogLevelTriggered?: {\n    global: undefined | 'info' | 'error' | 'warn'\n    current: undefined | 'info' | 'error' | 'warn'\n  }\n  onClickChains: (item: ClashProfile) => void\n  chainsSelected?: boolean\n}\n\nexport const ProfileItem = memo(function ProfileItem({\n  item,\n  selected,\n  onClickChains,\n  chainsSelected,\n  maxLogLevelTriggered,\n}: ProfileItemProps) {\n  const { t } = useTranslation()\n\n  const { deleteConnections } = useClashConnections()\n\n  const { upsert } = useProfile()\n\n  const globalUpdatePending = use(GlobalUpdatePendingContext)\n\n  const [loading, setLoading] = useSetState({\n    update: false,\n    card: false,\n  })\n\n  const calc = () => {\n    let progress = 0\n    let total = 0\n    let used = 0\n\n    if ('extra' in item && item.extra) {\n      const { download, upload, total: t } = item.extra\n\n      total = t\n\n      used = download + upload\n\n      progress = (used / (total || 1)) * 100\n    }\n\n    return { progress, total, used }\n  }\n\n  const { progress, total, used } = calc()\n\n  const isRemote = item.type === 'remote'\n\n  const IconComponent = isRemote ? FilterDrama : InsertDriveFile\n\n  const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null)\n\n  const handleSelect = useLockFn(async () => {\n    if (selected) {\n      return\n    }\n\n    try {\n      setLoading({ card: true })\n\n      await upsert.mutateAsync({ current: [item.uid] })\n\n      await deleteConnections.mutateAsync(undefined)\n    } catch (err) {\n      // This FetchError was triggered by the `DELETE /connections` API\n      const isFetchError = err instanceof Error && err.name === 'FetchError'\n      message(\n        isFetchError\n          ? `Failed to delete connections: \\n ${err instanceof Error ? err.message : String(err)}`\n          : `Error setting profile: \\n ${err instanceof Error ? err.message : String(err)}`,\n        {\n          title: isFetchError ? t('DeleteConnectionsError') : t('Error'),\n          kind: isFetchError ? 'warning' : 'error',\n        },\n      )\n    } finally {\n      setLoading({ card: false })\n    }\n  })\n\n  const handleUpdate = useLockFn(async (proxy?: boolean) => {\n    // TODO: define backend serde(option) to move null\n    const selfOption = 'option' in item ? item.option : undefined\n\n    const options: RemoteProfileOptionsBuilder = {\n      with_proxy: false,\n      self_proxy: false,\n      update_interval: 0,\n      user_agent: null,\n      ...selfOption,\n    }\n\n    if (proxy) {\n      if (selfOption?.self_proxy) {\n        options.with_proxy = false\n        options.self_proxy = true\n      } else {\n        options.with_proxy = true\n        options.self_proxy = false\n      }\n    }\n\n    try {\n      setLoading({ update: true })\n\n      await item?.update?.(options)\n    } catch (e) {\n      message(`Update failed: \\n ${formatError(e)}`, {\n        title: t('Error'),\n        kind: 'error',\n      })\n    } finally {\n      setLoading({ update: false })\n    }\n  })\n\n  const handleDelete = useLockFn(async () => {\n    try {\n      // await deleteProfile(item.uid)\n      await item?.drop?.()\n    } catch (err) {\n      message(`Delete failed: \\n ${JSON.stringify(err)}`, {\n        title: t('Error'),\n        kind: 'error',\n      })\n    }\n  })\n\n  const menuMapping = useMemo(\n    () => ({\n      Select: () => handleSelect(),\n      'Edit Info': () => setOpen(true),\n      'Proxy Chains': () => onClickChains(item as ClashProfile),\n      'Open File': () => item?.view?.(),\n      Update: () => handleUpdate(),\n      'Update(Proxy)': () => handleUpdate(true),\n      Delete: () => handleDelete(),\n    }),\n    [handleDelete, handleSelect, handleUpdate, item, onClickChains],\n  )\n\n  const MenuComp = useMemo(() => {\n    const handleClick = (func: () => void) => {\n      setAnchorEl(null)\n      func()\n    }\n\n    return (\n      <Menu\n        anchorEl={anchorEl}\n        open={Boolean(anchorEl)}\n        onClose={() => setAnchorEl(null)}\n      >\n        {Object.entries(menuMapping).map(([key, func], index) => {\n          return (\n            <MenuItem\n              key={index}\n              onClick={(e) => {\n                cleanDeepClickEvent(e)\n                handleClick(func)\n              }}\n            >\n              {t(key)}\n            </MenuItem>\n          )\n        })}\n      </Menu>\n    )\n  }, [anchorEl, menuMapping, t])\n\n  const [open, setOpen] = useState(false)\n\n  return (\n    <>\n      <Paper\n        className=\"relative transition-all\"\n        sx={[\n          {\n            borderRadius: 6,\n          },\n          (theme) => ({\n            backgroundColor: selected\n              ? alpha(theme.vars.palette.primary.main, 0.2)\n              : null,\n          }),\n        ]}\n      >\n        <div\n          className=\"flex cursor-pointer flex-col gap-4 p-5\"\n          onClick={handleSelect}\n        >\n          <div className=\"flex items-center justify-between gap-2\">\n            <Tooltip title={(item as RemoteProfile).url}>\n              <Chip\n                className=\"!pr-2 !pl-2 font-bold\"\n                avatar={<IconComponent className=\"!size-5\" color=\"primary\" />}\n                label={isRemote ? t('Remote') : t('Local')}\n              />\n            </Tooltip>\n\n            {selected && (\n              <FiberManualRecord\n                className=\"top-0 mr-auto !size-3 animate-bounce\"\n                sx={(theme) => ({\n                  fill: theme.vars.palette.success.main,\n                })}\n              />\n            )}\n\n            <TextCarousel\n              className=\"flex h-6 w-30 items-center\"\n              nodes={[\n                !!item.updated && (\n                  <TimeSpan\n                    key=\"updated\"\n                    ts={item.updated!}\n                    k=\"Subscription Updated At\"\n                  />\n                ),\n                !!(item as RemoteProfile).extra?.expire && (\n                  <TimeSpan\n                    key=\"expire\"\n                    ts={(item as RemoteProfile).extra!.expire!}\n                    k=\"Subscription Expires In\"\n                  />\n                ),\n              ]}\n            />\n          </div>\n\n          <div>\n            <p className=\"truncate text-lg font-bold\">{item.name}</p>\n            <p className=\"truncate\">{item.desc}</p>\n          </div>\n\n          <div\n            className={cn(\n              'flex items-center justify-between gap-4',\n              !isRemote && 'invisible',\n            )}\n          >\n            <div className=\"w-full\">\n              <LinearProgress variant=\"determinate\" value={progress} />\n            </div>\n\n            <Tooltip title={`${parseTraffic(used)} / ${parseTraffic(total)}`}>\n              <div className=\"text-sm font-bold\">{progress.toFixed(2)}%</div>\n            </Tooltip>\n          </div>\n\n          <div className=\"flex justify-end gap-2\">\n            <Badge\n              variant=\"dot\"\n              color={\n                maxLogLevelTriggered?.current === 'error'\n                  ? 'error'\n                  : maxLogLevelTriggered?.current === 'warn'\n                    ? 'warning'\n                    : 'primary'\n              }\n              invisible={!selected || !maxLogLevelTriggered?.current}\n            >\n              <Button\n                className=\"!mr-auto\"\n                size=\"small\"\n                variant={chainsSelected ? 'contained' : 'outlined'}\n                startIcon={<Terminal />}\n                onClick={(e) => {\n                  cleanDeepClickEvent(e)\n                  onClickChains(item as ClashProfile)\n                }}\n              >\n                {t('Proxy Chains')}\n              </Button>\n            </Badge>\n\n            <Tooltip title={t('Update')}>\n              <Button\n                size=\"small\"\n                variant=\"outlined\"\n                className=\"!size-8 !min-w-0\"\n                onClick={(e) => {\n                  cleanDeepClickEvent(e)\n                  menuMapping.Update()\n                }}\n                loading={globalUpdatePending || loading.update}\n              >\n                <Update />\n              </Button>\n            </Tooltip>\n\n            <Tooltip title={t('Menu')}>\n              <Button\n                size=\"small\"\n                variant=\"contained\"\n                className=\"!size-8 !min-w-0\"\n                onClick={(e) => {\n                  cleanDeepClickEvent(e)\n                  setAnchorEl(e.currentTarget)\n                }}\n              >\n                <MenuIcon />\n              </Button>\n            </Tooltip>\n          </div>\n        </div>\n\n        <motion.div\n          className={cn(\n            'absolute top-0 left-0 h-full w-full',\n            'flex-col items-center justify-center gap-4',\n            'text-shadow-xl rounded-3xl font-bold backdrop-blur',\n          )}\n          initial={{ opacity: 0, display: 'none' }}\n          animate={loading.card ? 'show' : 'hidden'}\n          variants={{\n            show: { opacity: 1, display: 'flex' },\n            hidden: { opacity: 0, transitionEnd: { display: 'none' } },\n          }}\n        >\n          <LinearProgress className=\"w-40\" />\n\n          <div>{t('Applying Profile')}</div>\n        </motion.div>\n      </Paper>\n      {MenuComp}\n      <ProfileDialog\n        open={open}\n        onClose={() => setOpen(false)}\n        profile={item}\n      />\n    </>\n  )\n})\n\nfunction TimeSpan({ ts, k }: { ts: number; k: string }) {\n  const time = dayjs(ts * 1000)\n  const { t } = useTranslation()\n  return (\n    <Tooltip title={time.format('YYYY/MM/DD HH:mm:ss')}>\n      <div className=\"animate-marquee h-fit text-right text-sm font-medium whitespace-nowrap\">\n        {t(k, {\n          time: time.fromNow(),\n        })}\n      </div>\n    </Tooltip>\n  )\n}\n\nfunction TextCarousel(props: { nodes: React.ReactNode[]; className?: string }) {\n  const [index, setIndex] = useState(0)\n  const nodes = useMemo(\n    () => props.nodes.filter((item) => !!item),\n    [props.nodes],\n  )\n\n  const nextNode = useMemoizedFn(() => {\n    setIndex((i) => (i + 1) % nodes.length)\n  })\n\n  useEffect(() => {\n    if (nodes.length <= 1) {\n      return\n    }\n    const timer = setInterval(() => {\n      nextNode()\n    }, 8000)\n    return () => clearInterval(timer)\n  }, [index, nextNode, nodes.length])\n  if (nodes.length === 0) {\n    return null\n  }\n  return (\n    <div\n      className={cn('overflow-hidden', props.className)}\n      onClick={() => nextNode()}\n    >\n      <AnimatePresence mode=\"wait\">\n        {nodes.map(\n          (node, i) =>\n            i === index && (\n              <motion.div\n                className=\"h-full w-full\"\n                key={index}\n                initial={{ y: 40, opacity: 0, scale: 0.8 }}\n                animate={{ y: 0, opacity: 1, scale: 1 }}\n                exit={{ y: -40, opacity: 0, scale: 0.8 }}\n              >\n                {node}\n              </motion.div>\n            ),\n        )}\n      </AnimatePresence>\n    </div>\n  )\n}\n\nexport default ProfileItem\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/profiles/profile-monaco-diff-viewer.tsx",
    "content": "import '@/services/monaco'\nimport { DiffEditor, DiffEditorProps } from '@monaco-editor/react'\nimport { beforeEditorMount } from './profile-monaco-viewer'\n\nexport default function ProfileMonacoDiffViewer(\n  props: Omit<DiffEditorProps, 'beforeMount'>,\n) {\n  return <DiffEditor {...props} beforeMount={beforeEditorMount} />\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/profiles/profile-monaco-viewer.tsx",
    "content": "import { OS } from '@/consts'\nimport '@/services/monaco'\nimport { useAtomValue } from 'jotai'\nimport { type JSONSchema7 } from 'json-schema'\nimport nyanpasuMergeSchema from 'meta-json-schema/schemas/clash-nyanpasu-merge-json-schema.json'\nimport clashMetaSchema from 'meta-json-schema/schemas/meta-json-schema.json'\nimport { type editor } from 'monaco-editor'\nimport * as monaco from 'monaco-editor'\nimport { configureMonacoYaml } from 'monaco-yaml'\nimport { nanoid } from 'nanoid'\nimport { useCallback, useMemo, useRef } from 'react'\n// schema\nimport { themeMode } from '@/store'\nimport MonacoEditor from '@monaco-editor/react'\nimport { openThat } from '@nyanpasu/interface'\nimport { cn } from '@nyanpasu/ui'\n\nexport interface ProfileMonacoViewProps {\n  value?: string\n  onChange?: (value: string) => void\n  language?: string\n  className?: string\n  readonly?: boolean\n  schemaType?: 'clash' | 'merge'\n  onValidate?: (markers: editor.IMarker[]) => void\n}\n\nexport interface ProfileMonacoViewRef {\n  getValue: () => string | undefined\n}\n\nlet initd = false\n\nexport const beforeEditorMount = () => {\n  if (!initd) {\n    monaco.typescript.javascriptDefaults.setCompilerOptions({\n      target: monaco.typescript.ScriptTarget.ES2020,\n      allowNonTsExtensions: true,\n      allowJs: true,\n    })\n    console.log(clashMetaSchema)\n    console.log(nyanpasuMergeSchema)\n    configureMonacoYaml(monaco, {\n      validate: true,\n      enableSchemaRequest: true,\n      completion: true,\n      schemas: [\n        {\n          uri: 'http://example.com/schema-name.json',\n          fileMatch: ['**/*.clash.yaml'],\n          // @ts-expect-error JSONSchema7 as JSONSchema\n          schema: clashMetaSchema as JSONSchema7,\n        },\n        {\n          uri: 'http://example.com/schema-name.json',\n          fileMatch: ['**/*.merge.yaml'],\n          // @ts-expect-error JSONSchema7 as JSONSchema\n          schema: nyanpasuMergeSchema as JSONSchema7,\n        },\n      ],\n    })\n\n    // Register link provider for all supported languages\n    const registerLinkProvider = (language: string) => {\n      monaco.languages.registerLinkProvider(language, {\n        provideLinks: (model) => {\n          const links = []\n          // More robust URL regex pattern\n          const urlRegex = /\\b(?:https?:\\/\\/|www\\.)[^\\s<>\"']*[^<>\\s\"',.!?]/gi\n\n          for (let i = 1; i <= model.getLineCount(); i++) {\n            const line = model.getLineContent(i)\n            let match\n\n            while ((match = urlRegex.exec(line)) !== null) {\n              const url = match[0].startsWith('http')\n                ? match[0]\n                : `https://${match[0]}`\n              links.push({\n                range: new monaco.Range(\n                  i,\n                  match.index + 1,\n                  i,\n                  match.index + match[0].length + 1,\n                ),\n                url,\n              })\n            }\n          }\n\n          return {\n            links,\n            dispose: () => {},\n          }\n        },\n      })\n    }\n\n    // Register link provider for all languages we support\n    registerLinkProvider('javascript')\n    registerLinkProvider('lua')\n    registerLinkProvider('yaml')\n  }\n  initd = true\n}\n\nexport default function ProfileMonacoViewer({\n  value,\n  language,\n  readonly = false,\n  schemaType,\n  className,\n  onValidate,\n  ...others\n}: ProfileMonacoViewProps) {\n  const mode = useAtomValue(themeMode)\n\n  const path = useMemo(\n    () => `${nanoid()}.${schemaType ? `${schemaType}.` : ''}${language}`,\n    [schemaType, language],\n  )\n\n  const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null)\n\n  const onChange = useCallback(\n    (value: string | undefined) => {\n      if (value && others.onChange) {\n        others.onChange(value)\n      }\n    },\n    [others],\n  )\n\n  const handleEditorDidMount = useCallback(\n    (editor: editor.IStandaloneCodeEditor) => {\n      editorRef.current = editor\n\n      // Enable URL detection and handling\n      editor.onMouseDown((e) => {\n        const position = e.target.position\n        if (!position) return\n\n        // Get the model\n        const model = editor.getModel()\n        if (!model) return\n\n        // Get the word at the clicked position\n        const wordAtPosition = model.getWordAtPosition(position)\n        if (!wordAtPosition) return\n\n        // More comprehensive URL regex pattern\n        const urlRegex = /\\b(?:https?:\\/\\/|www\\.)[^\\s<>\"']*[^<>\\s\"',.!?]/gi\n\n        // Check if the clicked word is part of a URL\n        const lineContent = model.getLineContent(position.lineNumber)\n        let match\n\n        while ((match = urlRegex.exec(lineContent)) !== null) {\n          const urlStart = match.index + 1\n          const urlEnd = urlStart + match[0].length\n\n          // Check if the click position is within the URL\n          if (position.column >= urlStart && position.column <= urlEnd) {\n            // Only handle Ctrl+Click or Cmd+Click\n            if (e.event.ctrlKey || e.event.metaKey) {\n              // Add protocol if missing (for www. URLs)\n              const url = match[0].startsWith('http')\n                ? match[0]\n                : `https://${match[0]}`\n              openThat(url)\n              e.event.preventDefault()\n              break\n            }\n          }\n        }\n      })\n    },\n    [],\n  )\n\n  return (\n    <MonacoEditor\n      className={cn(className)}\n      value={value}\n      language={language}\n      path={path}\n      theme={mode === 'light' ? 'vs' : 'vs-dark'}\n      beforeMount={beforeEditorMount}\n      onMount={handleEditorDidMount}\n      onChange={onChange}\n      onValidate={onValidate}\n      options={{\n        readOnly: readonly,\n        mouseWheelZoom: true,\n        renderValidationDecorations: 'on',\n        tabSize: language === 'yaml' ? 2 : 4,\n        minimap: { enabled: false },\n        automaticLayout: true,\n        fontLigatures: true,\n        smoothScrolling: true,\n        fontFamily: `'Cascadia Code NF', 'Cascadia Code', Fira Code, JetBrains Mono, Roboto Mono, \"Source Code Pro\", Consolas, Menlo, Monaco, monospace, \"Courier New\", \"Apple Color Emoji\"${OS === 'windows' ? ', twemoji mozilla' : ''}`,\n        quickSuggestions: {\n          strings: true,\n          comments: true,\n          other: true,\n        },\n      }}\n    />\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/profiles/profile-side.tsx",
    "content": "import { Allotment } from 'allotment'\nimport 'allotment/dist/style.css'\nimport { useAtomValue } from 'jotai'\nimport { useMemo, useState } from 'react'\nimport { useTranslation } from 'react-i18next'\nimport { Close } from '@mui/icons-material'\nimport { IconButton } from '@mui/material'\nimport { Profile, useProfile } from '@nyanpasu/interface'\nimport { SideChain } from './modules/side-chain'\nimport { SideLog } from './modules/side-log'\nimport { atomChainsSelected, atomGlobalChainCurrent } from './modules/store'\nimport { ScriptDialog } from './script-dialog'\n\nexport interface ProfileSideProps {\n  onClose: () => void\n}\n\nexport const ProfileSide = ({ onClose }: ProfileSideProps) => {\n  const { t } = useTranslation()\n\n  const [open, setOpen] = useState(false)\n\n  const [item, setItem] = useState<Profile>()\n\n  const isGlobalChainCurrent = useAtomValue(atomGlobalChainCurrent)\n\n  const currentProfileUid = useAtomValue(atomChainsSelected)\n\n  const { query } = useProfile()\n\n  const currentProfile = useMemo(() => {\n    return query.data?.items?.find((item) => item.uid === currentProfileUid)\n  }, [query.data?.items, currentProfileUid])\n\n  const handleEditChain = async (_item?: Profile) => {\n    setItem(_item)\n    setOpen(true)\n  }\n\n  return (\n    <div className=\"absolute h-full w-full\">\n      <div className=\"flex items-start justify-between p-4 pr-2\">\n        <div>\n          <div className=\"text-xl font-bold\">\n            {isGlobalChainCurrent\n              ? t('Global Proxy Chains')\n              : t('Proxy Chains')}\n          </div>\n\n          <div className=\"truncate\">\n            {isGlobalChainCurrent ? t('All Profiles') : currentProfile?.name}\n          </div>\n        </div>\n\n        <IconButton onClick={onClose}>\n          <Close />\n        </IconButton>\n      </div>\n\n      <div style={{ height: 'calc(100% - 84px)' }}>\n        <Allotment vertical defaultSizes={[1, 0]}>\n          <Allotment.Pane snap>\n            <SideChain onChainEdit={handleEditChain} />\n          </Allotment.Pane>\n\n          <Allotment.Pane minSize={40}>\n            <SideLog className=\"h-full border-t-2\" />\n          </Allotment.Pane>\n        </Allotment>\n      </div>\n\n      <ScriptDialog\n        open={open}\n        profile={item}\n        onClose={() => {\n          setOpen(false)\n          setItem(undefined)\n        }}\n      />\n    </div>\n  )\n}\n\nexport default ProfileSide\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/profiles/provider.tsx",
    "content": "import { createContext } from 'react'\n\nexport const GlobalUpdatePendingContext = createContext<boolean>(false)\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/profiles/quick-import.tsx",
    "content": "import { useState } from 'react'\nimport { useTranslation } from 'react-i18next'\nimport { ClearRounded, ContentCopyRounded, Download } from '@mui/icons-material'\nimport {\n  CircularProgress,\n  FilledInputProps,\n  IconButton,\n  TextField,\n  Tooltip,\n} from '@mui/material'\nimport { useProfile } from '@nyanpasu/interface'\nimport { alpha } from '@nyanpasu/ui'\nimport { readText } from '@tauri-apps/plugin-clipboard-manager'\n\nexport const QuickImport = () => {\n  const { t } = useTranslation()\n\n  const [url, setUrl] = useState('')\n\n  const [loading, setLoading] = useState(false)\n\n  const { create } = useProfile()\n\n  const onCopyLink = async () => {\n    const text = await readText()\n\n    if (text) {\n      setUrl(text)\n    }\n  }\n\n  const endAdornment = () => {\n    if (loading) {\n      return <CircularProgress size={20} />\n    }\n\n    if (url) {\n      return (\n        <>\n          <Tooltip title={t('Clear')}>\n            <IconButton size=\"small\" onClick={() => setUrl('')}>\n              <ClearRounded fontSize=\"inherit\" />\n            </IconButton>\n          </Tooltip>\n\n          <Tooltip title={t('Download')}>\n            <IconButton size=\"small\" onClick={handleImport}>\n              <Download fontSize=\"inherit\" />\n            </IconButton>\n          </Tooltip>\n        </>\n      )\n    }\n\n    return (\n      <Tooltip title={t('Paste')}>\n        <IconButton size=\"small\" onClick={onCopyLink}>\n          <ContentCopyRounded fontSize=\"inherit\" />\n        </IconButton>\n      </Tooltip>\n    )\n  }\n\n  const handleImport = async () => {\n    try {\n      setLoading(true)\n\n      await create.mutateAsync({\n        type: 'url',\n        data: {\n          url,\n          option: null,\n        },\n      })\n    } finally {\n      setUrl('')\n      setLoading(false)\n    }\n  }\n\n  const inputProps: Partial<FilledInputProps> = {\n    sx: (theme) => ({\n      borderRadius: 7,\n      backgroundColor: alpha(theme.vars.palette.primary.main, 0.1),\n\n      fieldset: {\n        border: 'none',\n      },\n    }),\n    endAdornment: endAdornment(),\n  }\n\n  return (\n    <TextField\n      hiddenLabel\n      fullWidth\n      autoComplete=\"off\"\n      spellCheck=\"false\"\n      value={url}\n      placeholder={t('Profile URL')}\n      onChange={(e) => setUrl(e.target.value)}\n      onKeyDown={(e) => url !== '' && e.key === 'Enter' && handleImport()}\n      sx={{ input: { py: 1, px: 2 } }}\n      slotProps={{\n        input: inputProps,\n      }}\n    />\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/profiles/read-profile.tsx",
    "content": "import { useState } from 'react'\nimport { useTranslation } from 'react-i18next'\nimport getSystem from '@/utils/get-system'\nimport { Button } from '@mui/material'\nimport { open } from '@tauri-apps/plugin-dialog'\nimport { readTextFile } from '@tauri-apps/plugin-fs'\n\nconst isWin = getSystem() === 'windows'\n\nexport interface ReadProfileProps {\n  onSelected: (content: string) => void\n}\n\nexport const ReadProfile = ({ onSelected }: ReadProfileProps) => {\n  const { t } = useTranslation()\n\n  const [loading, setLoading] = useState(false)\n\n  const [label, setLabel] = useState('')\n\n  const handleSelectFile = async () => {\n    try {\n      setLoading(true)\n\n      const selected = await open({\n        directory: false,\n        multiple: false,\n        filters: [\n          {\n            name: t('Profile'),\n            extensions: ['yaml', 'yml'],\n          },\n        ],\n      })\n\n      // user cancelled the selection\n      if (!selected || Array.isArray(selected)) {\n        return null\n      }\n\n      onSelected(await readTextFile(selected))\n\n      if (isWin) {\n        setLabel(selected.split('\\\\').at(-1) as string)\n      } else {\n        setLabel(selected.split('/').at(-1) as string)\n      }\n    } catch (e) {\n      console.error(e)\n    } finally {\n      setLoading(false)\n    }\n  }\n\n  return (\n    <Button\n      variant=\"contained\"\n      loading={loading}\n      disabled={loading}\n      onClick={handleSelectFile}\n      color={label ? 'success' : 'primary'}\n    >\n      {label || t('Choose File')}\n    </Button>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/profiles/runtime-config-diff-dialog.tsx",
    "content": "import { useCreation } from 'ahooks'\nimport { useAtomValue } from 'jotai'\nimport { nanoid } from 'nanoid'\nimport { lazy, Suspense } from 'react'\nimport { useTranslation } from 'react-i18next'\nimport { themeMode } from '@/store'\nimport {\n  useProfile,\n  useProfileContent,\n  useRuntimeProfile,\n} from '@nyanpasu/interface'\nimport { BaseDialog, cn } from '@nyanpasu/ui'\n\nconst MonacoDiffEditor = lazy(() => import('./profile-monaco-diff-viewer'))\n\nexport type RuntimeConfigDiffDialogProps = {\n  open: boolean\n  onClose: () => void\n}\n\nexport default function RuntimeConfigDiffDialog({\n  open,\n  onClose,\n}: RuntimeConfigDiffDialogProps) {\n  const { t } = useTranslation()\n\n  const { query } = useProfile()\n\n  const currentProfileUid = query.data?.current?.[0]\n\n  const contentFn = useProfileContent(currentProfileUid || '')\n\n  // need manual refetch\n  contentFn.query.refetch()\n\n  const runtimeProfile = useRuntimeProfile()\n\n  const loaded = !contentFn.query.isLoading && !query.isLoading\n\n  const mode = useAtomValue(themeMode)\n\n  const originalModelPath = useCreation(() => `${nanoid()}.clash.yaml`, [])\n  const modifiedModelPath = useCreation(() => `${nanoid()}.runtime.yaml`, [])\n\n  if (!currentProfileUid) {\n    return null\n  }\n\n  return (\n    <BaseDialog title={t('Runtime Config')} open={open} onClose={onClose}>\n      <div className=\"xs:w-[95vw] h-full w-[80vw] px-4\">\n        <div\n          className={cn(\n            'items-center justify-between px-5 pb-2',\n            loaded ? 'flex' : 'hidden',\n          )}\n        >\n          <span className=\"text-base font-semibold\">\n            {t('Original Config')}\n          </span>\n          <span className=\"text-base font-semibold\">{t('Runtime Config')}</span>\n        </div>\n        <div className=\"h-[75vh] w-full\">\n          <Suspense fallback={null}>\n            {loaded && (\n              <MonacoDiffEditor\n                language=\"yaml\"\n                theme={mode === 'light' ? 'vs' : 'vs-dark'}\n                original={contentFn.query.data}\n                originalModelPath={originalModelPath}\n                modified={runtimeProfile.data}\n                modifiedModelPath={modifiedModelPath}\n                options={{\n                  minimap: { enabled: false },\n                  automaticLayout: true,\n                  readOnly: true,\n                }}\n              />\n            )}\n          </Suspense>\n        </div>\n      </div>\n    </BaseDialog>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/profiles/script-dialog.tsx",
    "content": "import { useAsyncEffect, useReactive } from 'ahooks'\nimport { type editor } from 'monaco-editor'\nimport { lazy, Suspense, useEffect, useRef, useState } from 'react'\nimport { SelectElement, TextFieldElement, useForm } from 'react-hook-form-mui'\nimport { useTranslation } from 'react-i18next'\nimport { message } from '@/utils/notification'\nimport { Divider } from '@mui/material'\nimport {\n  Profile,\n  ProfileTemplate,\n  useProfile,\n  useProfileContent,\n} from '@nyanpasu/interface'\nimport { BaseDialog, BaseDialogProps } from '@nyanpasu/ui'\nimport LanguageChip from './modules/language-chip'\nimport {\n  ChainProfileBuilder,\n  getLanguage,\n  ProfileType,\n  ProfileTypes,\n} from './utils'\n\nconst ProfileMonacoViewer = lazy(() => import('./profile-monaco-viewer'))\n\nconst formCommonProps = {\n  autoComplete: 'off',\n  autoCorrect: 'off',\n  fullWidth: true,\n}\n\nconst optionTypeMapping = [\n  {\n    id: 'js',\n    value: ProfileTypes.JavaScript,\n    language: 'javascript',\n    label: 'JavaScript',\n  },\n  {\n    id: 'lua',\n    value: ProfileTypes.LuaScript,\n    language: 'lua',\n    label: 'LuaScript',\n  },\n  {\n    id: 'merge',\n    value: ProfileTypes.Merge,\n    language: 'yaml',\n    label: 'Merge',\n  },\n]\n\nconst convertTypeMapping = (data: Profile) => {\n  optionTypeMapping.forEach((option) => {\n    if (option.id === data.type) {\n      data = {\n        ...data,\n        ...option,\n      }\n    }\n  })\n\n  return data\n}\n\nexport interface ScriptDialogProps extends Omit<BaseDialogProps, 'title'> {\n  open: boolean\n  onClose: () => void\n  profile?: Profile\n}\n\nexport const ScriptDialog = ({\n  open,\n  profile,\n  onClose,\n  ...props\n}: ScriptDialogProps) => {\n  const { t } = useTranslation()\n\n  const { create, patch } = useProfile()\n\n  const contentFn = useProfileContent(profile?.uid ?? '')\n\n  const form = useForm<Profile>()\n\n  const isEdit = Boolean(profile)\n\n  useEffect(() => {\n    if (isEdit) {\n      form.reset(profile)\n    } else {\n      form.reset({\n        type: 'merge',\n        name: t('New Script'),\n        desc: '',\n      })\n    }\n  }, [form, isEdit, profile, t])\n\n  const [openMonaco, setOpenMonaco] = useState(false)\n\n  const editor = useReactive<{\n    value: string\n    displayLanguage: string\n    language: string\n    rawType: ProfileType\n  }>({\n    value: ProfileTemplate.merge,\n    displayLanguage: 'YAML',\n    language: 'yaml',\n    rawType: 'merge',\n  })\n\n  const editorMarks = useRef<editor.IMarker[]>([])\n  const editorHasError = () =>\n    editorMarks.current.length > 0 &&\n    editorMarks.current.some((m) => m.severity === 8)\n\n  const onSubmit = form.handleSubmit(async (data) => {\n    if (editorHasError()) {\n      message(t('Please fix the error before submitting'), {\n        kind: 'error',\n      })\n      return\n    }\n\n    convertTypeMapping(data)\n\n    const editorValue = editor.value\n\n    if (!editorValue) {\n      return\n    }\n\n    try {\n      if (isEdit) {\n        await contentFn.upsert.mutateAsync(editorValue)\n        await patch.mutateAsync({\n          uid: data.uid,\n          profile: data as ChainProfileBuilder,\n        })\n      } else {\n        await create.mutateAsync({\n          type: 'manual',\n          data: {\n            item: data as ChainProfileBuilder,\n            fileData: editorValue,\n          },\n        })\n      }\n    } finally {\n      onClose()\n    }\n  })\n\n  useAsyncEffect(async () => {\n    if (isEdit) {\n      const result = await contentFn.query.refetch()\n\n      editor.value = result.data ?? ''\n      editor.displayLanguage = getLanguage(profile!)\n      editor.language = editor.displayLanguage.toLowerCase()\n    } else {\n      editor.value = ProfileTemplate.merge\n      editor.displayLanguage = 'YAML'\n      editor.language = editor.displayLanguage.toLowerCase()\n    }\n\n    setOpenMonaco(open)\n  }, [open])\n\n  const handleTypeChange = () => {\n    const data = form.getValues()\n\n    editor.rawType = convertTypeMapping(data).type\n\n    const lang = getLanguage(data)\n\n    if (!lang) {\n      return\n    }\n\n    editor.displayLanguage = lang\n    editor.language = editor.displayLanguage.toLowerCase()\n\n    switch (editor.language) {\n      case 'yaml': {\n        editor.value = ProfileTemplate.merge\n        break\n      }\n\n      case 'lua': {\n        editor.value = ProfileTemplate.luascript\n        break\n      }\n\n      case 'javascript': {\n        editor.value = ProfileTemplate.javascript\n        break\n      }\n    }\n  }\n\n  return (\n    <BaseDialog\n      title={\n        <div className=\"flex gap-2\" data-tauri-drag-region>\n          <span>{isEdit ? t('Edit Script') : t('New Script')}</span>\n\n          <LanguageChip\n            lang={\n              isEdit && profile ? getLanguage(profile) : editor.displayLanguage\n            }\n          />\n        </div>\n      }\n      open={open}\n      onClose={() => onClose()}\n      onOk={onSubmit}\n      divider\n      contentStyle={{\n        overflow: 'hidden',\n        padding: 0,\n      }}\n      full\n      {...props}\n    >\n      <div className=\"flex h-full\">\n        <div className=\"overflow-auto pt-4 pb-4\">\n          <div className=\"flex flex-col gap-4 pr-4 pb-4 pl-4\">\n            {!isEdit && (\n              <SelectElement\n                label={t('Type')}\n                name=\"type\"\n                control={form.control}\n                {...formCommonProps}\n                size=\"small\"\n                required\n                options={optionTypeMapping}\n                onChange={() => handleTypeChange()}\n              />\n            )}\n\n            <TextFieldElement\n              label={t('Name')}\n              name=\"name\"\n              control={form.control}\n              {...formCommonProps}\n              size=\"small\"\n              required\n            />\n\n            <TextFieldElement\n              label={t('Descriptions')}\n              name=\"desc\"\n              control={form.control}\n              {...formCommonProps}\n              size=\"small\"\n              multiline\n            />\n          </div>\n        </div>\n\n        <Divider orientation=\"vertical\" />\n\n        <Suspense fallback={null}>\n          {openMonaco && !contentFn.query.isPending && (\n            <ProfileMonacoViewer\n              className=\"w-full\"\n              value={editor.value}\n              onChange={(value) => {\n                editor.value = value\n              }}\n              language={editor.language}\n              onValidate={(marks) => {\n                editorMarks.current = marks\n              }}\n              schemaType={editor.rawType === 'merge' ? 'merge' : undefined}\n            />\n          )}\n        </Suspense>\n      </div>\n    </BaseDialog>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/profiles/utils.ts",
    "content": "import type { Profile, ProfileBuilder } from '@nyanpasu/interface'\n\n/**\n * Represents a Clash configuration profile, which can be either locally stored or fetched from a remote source.\n */\nexport type ClashProfile = Extract<Profile, { type: 'remote' | 'local' }>\nexport type ClashProfileBuilder = Extract<\n  ProfileBuilder,\n  { type: 'remote' | 'local' }\n>\n\n/**\n * Represents a Clash configuration profile that is a chain of multiple profiles.\n */\nexport type ChainProfile = Extract<Profile, { type: 'merge' | 'script' }>\nexport type ChainProfileBuilder = Extract<\n  ProfileBuilder,\n  { type: 'merge' | 'script' }\n>\n\n/**\n * Filters an array of profiles into two categories: clash and chain profiles.\n *\n * @param items - Array of Profile objects to be filtered\n * @returns An object containing two arrays:\n *          - clash: Array of profiles where type is 'remote' or 'local'\n *          - chain: Array of profiles where type is 'merge' or has a script property\n */\nexport function filterProfiles<T extends Profile>(items?: T[]) {\n  /**\n   * Filters the input array to include only items of type 'remote' or 'local'\n   * @param items - Array of items to filter\n   * @returns {Array} Filtered array containing only remote and local items\n   */\n  const clash = items?.filter(\n    (item) => item.type === 'remote' || item.type === 'local',\n  )\n\n  /**\n   * Filters an array of items to get a chain of either 'merge' type items\n   * or items with a script property in their type object.\n   *\n   * @param {Array<{ type: string | { script: 'javascript' | 'lua' } }>} items - The array of items to filter\n   * @returns {Array<{ type: string | { script: 'javascript' | 'lua' } }>} A filtered array containing only merge items or items with scripts\n   */\n  const chain = items?.filter(\n    (item) => item.type === 'merge' || item.type === 'script',\n  )\n\n  return {\n    clash,\n    chain,\n  }\n}\n\nexport type ProfileType = Profile['type']\n\nexport const ProfileTypes = {\n  JavaScript: { type: 'script', script_type: 'javascript' },\n  LuaScript: { type: 'script', script_type: 'lua' },\n  Merge: { type: 'merge' },\n} as const\n\nexport const getLanguage = (profile: Profile) => {\n  switch (profile.type) {\n    case 'script':\n      switch (profile.script_type) {\n        case 'javascript':\n          return 'JavaScript'\n        case 'lua':\n          return 'Lua'\n      }\n      break\n    case 'merge':\n    case 'local':\n    case 'remote':\n      return 'YAML'\n  }\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/providers/block-task-provider.tsx",
    "content": "import {\n  createContext,\n  PropsWithChildren,\n  useCallback,\n  useContext,\n  useRef,\n  useState,\n} from 'react'\nimport { useLockFn } from '@/hooks/use-lock-fn'\n\ntype BlockTaskStatus = 'idle' | 'pending' | 'success' | 'error'\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ninterface BlockTask<T = any> {\n  id: string\n  status: BlockTaskStatus\n  data?: T\n  error?: Error\n  startTime: number\n  endTime?: number\n}\n\ninterface BlockTaskContextType {\n  tasks: Record<string, BlockTask>\n  run: <T>(key: string, fn: (...args: unknown[]) => Promise<T>) => Promise<T>\n  getTask: (key: string) => BlockTask | undefined\n  clearTask: (key: string) => void\n}\n\nconst BlockContext = createContext<BlockTaskContextType | null>(null)\n\nexport const useBlockTaskContext = () => {\n  const context = useContext(BlockContext)\n\n  if (!context) {\n    throw new Error('useBlockContext must be used within a BlockProvider')\n  }\n\n  return context\n}\n\nexport const useBlockTask = <T, Args extends unknown[] = []>(\n  key: string,\n  fn: (...args: Args) => Promise<T>,\n) => {\n  const { run, tasks } = useBlockTaskContext()\n\n  const execute = useLockFn(async (...args: Args) => {\n    return await run(key, () => fn(...args))\n  })\n\n  return {\n    execute,\n    isPending: tasks[key]?.status === 'pending',\n    isSuccess: tasks[key]?.status === 'success',\n    isError: tasks[key]?.status === 'error',\n    data: tasks[key]?.data,\n    error: tasks[key]?.error,\n  }\n}\n\nexport const BlockTaskProvider = ({ children }: PropsWithChildren) => {\n  const [tasks, setTasks] = useState<Record<string, BlockTask>>({})\n\n  const tasksRef = useRef<Record<string, BlockTask>>({})\n\n  const run = useCallback(\n    async <T,>(key: string, fn: () => Promise<T>): Promise<T> => {\n      const task: BlockTask<T> = {\n        id: key,\n        status: 'pending',\n        startTime: Date.now(),\n      }\n\n      setTasks((prev) => ({ ...prev, [key]: task }))\n      tasksRef.current[key] = task\n\n      try {\n        const data = await fn()\n\n        const successTask: BlockTask<T> = {\n          ...task,\n          status: 'success',\n          data,\n          endTime: Date.now(),\n        }\n\n        setTasks((prev) => ({\n          ...prev,\n          [key]: successTask,\n        }))\n\n        tasksRef.current[key] = successTask\n\n        return data\n      } catch (error) {\n        const errorTask: BlockTask = {\n          ...task,\n          status: 'error',\n          error: error instanceof Error ? error : new Error(String(error)),\n          endTime: Date.now(),\n        }\n\n        setTasks((prev) => ({\n          ...prev,\n          [key]: errorTask,\n        }))\n\n        tasksRef.current[key] = errorTask\n\n        throw error\n      }\n    },\n    [],\n  )\n\n  const getTask = useCallback((key: string) => tasks[key], [tasks])\n\n  const clearTask = useCallback((key: string) => {\n    setTasks((prev) => {\n      const newTasks = { ...prev }\n      delete newTasks[key]\n      return newTasks\n    })\n\n    delete tasksRef.current[key]\n  }, [])\n\n  return (\n    <BlockContext.Provider\n      value={{\n        tasks,\n        run,\n        getTask,\n        clearTask,\n      }}\n    >\n      {children}\n    </BlockContext.Provider>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/providers/context-menu-provider.tsx",
    "content": "import ContentCopy from '~icons/material-symbols/content-copy-rounded'\nimport ContentCut from '~icons/material-symbols/content-cut-rounded'\nimport ContentPaste from '~icons/material-symbols/content-paste-rounded'\nimport {\n  Children,\n  cloneElement,\n  createContext,\n  PropsWithChildren,\n  ReactElement,\n  ReactNode,\n  Ref,\n  RefCallback,\n  RefObject,\n  useCallback,\n  useContext,\n  useEffect,\n  useRef,\n  useState,\n} from 'react'\nimport {\n  ContextMenu,\n  ContextMenuContent,\n  ContextMenuItem,\n  ContextMenuSeparator,\n  ContextMenuShortcut,\n  ContextMenuTrigger,\n} from '@/components/ui/context-menu'\nimport { m } from '@/paraglide/messages'\nimport { readText, writeText } from '@tauri-apps/plugin-clipboard-manager'\n\ntype ContextMenuRegistryValue = {\n  registerElement: (el: Element, getChildren: () => ReactNode) => void\n  unregisterElement: (el: Element) => void\n}\n\nconst ContextMenuRegistryContext =\n  createContext<ContextMenuRegistryValue | null>(null)\n\nconst useContextMenuRegistry = () => {\n  const context = useContext(ContextMenuRegistryContext)\n\n  if (!context) {\n    throw new Error(\n      'useContextMenuRegistry must be used within a ContextMenuRegistryContext',\n    )\n  }\n\n  return context\n}\n\nexport function useRegisterContextMenu<T extends Element>(\n  menuChildren: ReactNode,\n): RefCallback<T> {\n  const { registerElement, unregisterElement } = useContextMenuRegistry()\n\n  const elementRef = useRef<T | null>(null)\n\n  const childrenRef = useRef(menuChildren)\n\n  childrenRef.current = menuChildren\n\n  const getChildren = useCallback(() => childrenRef.current, [])\n\n  return useCallback(\n    (el: T | null) => {\n      if (elementRef.current) {\n        unregisterElement(elementRef.current)\n        elementRef.current = null\n      }\n\n      if (el) {\n        elementRef.current = el\n        registerElement(el, getChildren)\n      }\n    },\n    [registerElement, unregisterElement, getChildren],\n  )\n}\n\ntype RegisterContextMenuInternalCtxValue = {\n  childrenRef: RefObject<ReactNode>\n  setTriggerEl: (el: Element | null) => void\n}\n\nconst RegisterContextMenuInternalCtx =\n  createContext<RegisterContextMenuInternalCtxValue | null>(null)\n\nconst useRegisterContextMenuInternal = () => {\n  const ctx = useContext(RegisterContextMenuInternalCtx)\n\n  if (!ctx) {\n    throw new Error(\n      'RegisterContextMenuTrigger/Content must be used within RegisterContextMenu',\n    )\n  }\n\n  return ctx\n}\n\nexport function RegisterContextMenu({ children }: PropsWithChildren) {\n  const { registerElement, unregisterElement } = useContextMenuRegistry()\n\n  const triggerElRef = useRef<Element | null>(null)\n  const childrenRef = useRef<ReactNode>(null)\n\n  const getChildren = useCallback(() => childrenRef.current, [])\n\n  const setTriggerEl = useCallback(\n    (el: Element | null) => {\n      if (triggerElRef.current) {\n        unregisterElement(triggerElRef.current)\n      }\n      triggerElRef.current = el\n      if (el) {\n        registerElement(el, getChildren)\n      }\n    },\n    [registerElement, unregisterElement, getChildren],\n  )\n\n  return (\n    <RegisterContextMenuInternalCtx.Provider\n      value={{ childrenRef, setTriggerEl }}\n    >\n      {children}\n    </RegisterContextMenuInternalCtx.Provider>\n  )\n}\n\n/**\n * Attaches context-menu registration to its child element.\n *\n * - `asChild` (default `false`): wraps children in a `<span>`.\n * - `asChild={true}`: merges the registration ref directly into the single\n *   child element, preserving any existing ref on it.\n */\nexport function RegisterContextMenuTrigger({\n  children,\n  asChild = false,\n}: {\n  children: ReactElement\n  asChild?: boolean\n}) {\n  const { setTriggerEl } = useRegisterContextMenuInternal()\n\n  // For asChild: keep the child's original ref in a stable container so\n  // mergedRef doesn't have it as a dep and stays stable across renders.\n  const child = Children.only(children) as ReactElement<{\n    ref?: Ref<Element>\n  }>\n  const originalRefLatest = useRef<Ref<Element> | undefined>(child.props.ref)\n  originalRefLatest.current = child.props.ref\n\n  const mergedRef = useCallback(\n    (el: Element | null) => {\n      setTriggerEl(el)\n      const orig = originalRefLatest.current\n\n      if (typeof orig === 'function') {\n        orig(el)\n      } else if (orig != null) {\n        ;(orig as RefObject<Element | null>).current = el\n      }\n    },\n    [setTriggerEl],\n  )\n\n  if (asChild) {\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    return cloneElement(child, { ref: mergedRef } as any)\n  }\n\n  return <span ref={setTriggerEl}>{children}</span>\n}\n\nexport function RegisterContextMenuContent({ children }: PropsWithChildren) {\n  const { childrenRef } = useRegisterContextMenuInternal()\n\n  // Update the ref synchronously during render so getChildren() always returns\n  // the latest JSX when the menu opens (safe — mutating a ref, not state).\n  childrenRef.current = children\n\n  useEffect(() => {\n    // Also set in the effect body so that React StrictMode's cleanup+re-invoke\n    // cycle restores the value after the cleanup sets it to null.\n    childrenRef.current = children\n\n    return () => {\n      childrenRef.current = null\n    }\n  })\n\n  return null\n}\n\nconst isEditable = (el: Element | null): boolean => {\n  if (!el || !(el instanceof HTMLElement)) {\n    return false\n  }\n\n  const tag = el.tagName.toLowerCase()\n\n  if (tag === 'input' || tag === 'textarea') {\n    return true\n  }\n\n  if (el.isContentEditable) {\n    return true\n  }\n\n  return false\n}\n\nexport default function ContextMenuProvider({ children }: PropsWithChildren) {\n  const [hasSelection, setHasSelection] = useState(false)\n\n  const [editable, setEditable] = useState(false)\n\n  const [customChildren, setCustomChildren] = useState<ReactNode>(null)\n\n  const targetRef = useRef<Element | null>(null)\n\n  const [open, setOpen] = useState(false)\n\n  const registryRef = useRef(new Map<Element, () => ReactNode>())\n\n  const lastRightClickTargetRef = useRef<Element | null>(null)\n\n  // Capture the right-clicked element before the context menu opens.\n  // Use pointerdown (button === 2) instead of contextmenu so the target is\n  // always recorded before any contextmenu listener (including Radix's) fires.\n  useEffect(() => {\n    const handler = (e: PointerEvent) => {\n      if (e.button === 2) {\n        lastRightClickTargetRef.current = e.target as Element\n      }\n    }\n    document.addEventListener('pointerdown', handler, true)\n    return () => document.removeEventListener('pointerdown', handler, true)\n  }, [])\n\n  const registerElement = useCallback(\n    (el: Element, getChildren: () => ReactNode) => {\n      registryRef.current.set(el, getChildren)\n    },\n    [],\n  )\n\n  const unregisterElement = useCallback((el: Element) => {\n    registryRef.current.delete(el)\n  }, [])\n\n  const handleOpenChange = useCallback((nextOpen: boolean) => {\n    setOpen(nextOpen)\n\n    if (nextOpen) {\n      const selection = window.getSelection()\n      setHasSelection(!!selection && selection.toString().length > 0)\n\n      const active = document.activeElement\n      setEditable(isEditable(active))\n      targetRef.current = active\n\n      // Traverse up the DOM from the right-clicked element to find registered children.\n      let el: Element | null = lastRightClickTargetRef.current\n      let found: ReactNode = null\n      while (el) {\n        const getter = registryRef.current.get(el)\n        if (getter) {\n          found = getter()\n          break\n        }\n        el = el.parentElement\n      }\n      setCustomChildren(found)\n    }\n  }, [])\n\n  const handleCopy = useCallback(async () => {\n    const selection = window.getSelection()\n    const text = selection?.toString() ?? ''\n\n    if (!text) {\n      return\n    }\n\n    await writeText(text)\n  }, [])\n\n  const handleCut = useCallback(async () => {\n    const selection = window.getSelection()\n    const text = selection?.toString() ?? ''\n\n    if (!text || !editable) {\n      return\n    }\n\n    await writeText(text)\n\n    const el = targetRef.current\n\n    if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {\n      const start = el.selectionStart ?? 0\n      const end = el.selectionEnd ?? 0\n      const currentValue = el.value\n\n      const nativeInputValueSetter = Object.getOwnPropertyDescriptor(\n        el instanceof HTMLTextAreaElement\n          ? HTMLTextAreaElement.prototype\n          : HTMLInputElement.prototype,\n        'value',\n      )?.set\n\n      nativeInputValueSetter?.call(\n        el,\n        currentValue.slice(0, start) + currentValue.slice(end),\n      )\n\n      el.dispatchEvent(new Event('input', { bubbles: true }))\n      el.setSelectionRange(start, start)\n      return\n    }\n\n    if (el instanceof HTMLElement && el.isContentEditable && selection) {\n      selection.deleteFromDocument()\n    }\n  }, [editable])\n\n  const handlePaste = useCallback(async () => {\n    try {\n      const text = await readText()\n      const el = targetRef.current\n\n      if (el && isEditable(el)) {\n        if (\n          el instanceof HTMLInputElement ||\n          el instanceof HTMLTextAreaElement\n        ) {\n          const start = el.selectionStart ?? 0\n          const end = el.selectionEnd ?? 0\n          const currentValue = el.value\n\n          const nativeInputValueSetter = Object.getOwnPropertyDescriptor(\n            el instanceof HTMLTextAreaElement\n              ? HTMLTextAreaElement.prototype\n              : HTMLInputElement.prototype,\n            'value',\n          )?.set\n\n          nativeInputValueSetter?.call(\n            el,\n            currentValue.slice(0, start) + text + currentValue.slice(end),\n          )\n\n          el.dispatchEvent(new Event('input', { bubbles: true }))\n\n          const newPos = start + text.length\n          el.setSelectionRange(newPos, newPos)\n        } else {\n          const editableEl = el as HTMLElement\n          editableEl.focus()\n\n          const selection = window.getSelection()\n          if (!selection || selection.rangeCount === 0) {\n            return\n          }\n\n          const range = selection.getRangeAt(0)\n          range.deleteContents()\n          range.insertNode(document.createTextNode(text))\n          range.collapse(false)\n          selection.removeAllRanges()\n          selection.addRange(range)\n        }\n      }\n    } catch {\n      // Ignore clipboard read failures (e.g. permission denied).\n    }\n  }, [])\n\n  return (\n    <ContextMenuRegistryContext.Provider\n      value={{ registerElement, unregisterElement }}\n    >\n      <ContextMenu open={open} onOpenChange={handleOpenChange}>\n        <ContextMenuTrigger asChild>{children}</ContextMenuTrigger>\n\n        <ContextMenuContent>\n          {customChildren != null && (\n            <>\n              {customChildren}\n\n              <ContextMenuSeparator />\n            </>\n          )}\n\n          <ContextMenuItem\n            disabled={!hasSelection || !editable}\n            onSelect={handleCut}\n          >\n            <ContentCut className=\"size-4\" />\n            <span>{m.common_cut()}</span>\n            <ContextMenuShortcut>Ctrl+X</ContextMenuShortcut>\n          </ContextMenuItem>\n\n          <ContextMenuItem disabled={!hasSelection} onSelect={handleCopy}>\n            <ContentCopy className=\"size-4\" />\n            <span>{m.common_copy()}</span>\n            <ContextMenuShortcut>Ctrl+C</ContextMenuShortcut>\n          </ContextMenuItem>\n\n          <ContextMenuItem disabled={!editable} onSelect={handlePaste}>\n            <ContentPaste className=\"size-4\" />\n            <span>{m.common_paste()}</span>\n            <ContextMenuShortcut>Ctrl+V</ContextMenuShortcut>\n          </ContextMenuItem>\n        </ContextMenuContent>\n      </ContextMenu>\n    </ContextMenuRegistryContext.Provider>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/providers/language-provider.tsx",
    "content": "import { locale } from 'dayjs'\nimport { createContext, PropsWithChildren, useContext, useEffect } from 'react'\nimport { useLockFn } from '@/hooks/use-lock-fn'\nimport { getLocale, Locale, setLocale } from '@/paraglide/runtime'\nimport { useSetting } from '@nyanpasu/interface'\n\nconst LanguageContext = createContext<{\n  language?: Locale\n  setLanguage: (value: Locale) => Promise<void>\n} | null>(null)\n\nexport const useLanguage = () => {\n  const context = useContext(LanguageContext)\n\n  if (!context) {\n    throw new Error('useLanguage must be used within a LanguageProvider')\n  }\n\n  return context\n}\n\nexport const LanguageProvider = ({ children }: PropsWithChildren) => {\n  const language = useSetting('language')\n\n  const setLanguage = useLockFn(async (value: Locale) => {\n    await language.upsert(value)\n    setLocale(value)\n  })\n\n  // sync dayjs locale\n  useEffect(() => {\n    if (language) {\n      locale(language.value || 'en')\n    }\n  }, [language])\n\n  return (\n    <LanguageContext.Provider\n      value={{\n        language: getLocale(),\n        setLanguage,\n      }}\n    >\n      {children}\n    </LanguageContext.Provider>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/providers/nyanpasu-update-provider.tsx",
    "content": "import {\n  createContext,\n  PropsWithChildren,\n  use,\n  useEffect,\n  useState,\n} from 'react'\nimport { Action as AboutAction } from '@/pages/(main)/main/settings/about/route'\nimport {\n  commands,\n  unwrapResult,\n  useSetting,\n  useUpdaterSupported,\n} from '@nyanpasu/interface'\nimport packageJson from '@root/package.json'\nimport { useNavigate } from '@tanstack/react-router'\nimport { Update } from '@tauri-apps/plugin-updater'\nimport { useBlockTask } from './block-task-provider'\n\nconst NyanpasuUpdateContext = createContext<{\n  currentVersion: string\n  hasNewVersion: boolean\n  newVersion: Update | null\n  isChecking: boolean\n  checkNewVersion: () => Promise<Update | null>\n  isSupported: boolean\n} | null>(null)\n\nexport const useNyanpasuUpdate = () => {\n  const context = use(NyanpasuUpdateContext)\n\n  if (!context) {\n    throw new Error(\n      'useNyanpasuUpdate must be used within a NyanpasuUpdateProvider',\n    )\n  }\n\n  return context\n}\n\nexport default function NyanpasuUpdateProvider({\n  children,\n}: PropsWithChildren) {\n  const { value: enableAutoCheckUpdate } = useSetting(\n    'enable_auto_check_update',\n  )\n\n  const isSupported = useUpdaterSupported()\n\n  const [hasNewVersion, setHasNewVersion] = useState(false)\n\n  const [newVersion, setNewVersion] = useState<Update | null>(null)\n\n  const blockTask = useBlockTask('check-nyanpasu-update', async () => {\n    const metadata = unwrapResult(await commands.checkUpdate())\n\n    if (metadata) {\n      const update = new Update({\n        rid: metadata.rid,\n        currentVersion: metadata.current_version,\n        version: metadata.version,\n        rawJson: metadata.raw_json as Record<string, unknown>,\n      })\n\n      setNewVersion(update)\n\n      setHasNewVersion(true)\n\n      return update\n    }\n\n    return null\n  })\n\n  const navigate = useNavigate()\n\n  // auto check update\n  useEffect(() => {\n    if (enableAutoCheckUpdate) {\n      blockTask.execute().then((update) => {\n        // if there is a new version, navigate to the about page\n        if (update) {\n          navigate({\n            to: '/main/settings/about',\n            search: {\n              action: AboutAction.NEED_UPDATE,\n            },\n          })\n        }\n      })\n    }\n    // oxlint-disable-next-line eslint-plugin-react-hooks/exhaustive-deps\n  }, [enableAutoCheckUpdate, blockTask.execute])\n\n  return (\n    <NyanpasuUpdateContext.Provider\n      value={{\n        currentVersion: packageJson.version,\n        hasNewVersion,\n        newVersion,\n        isChecking: blockTask.isPending,\n        checkNewVersion: blockTask.execute,\n        isSupported,\n      }}\n    >\n      {children}\n    </NyanpasuUpdateContext.Provider>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/providers/proxies-provider-traffic.tsx",
    "content": "import parseTraffic from '@/utils/parse-traffic'\nimport { LinearProgress, Tooltip } from '@mui/material'\nimport { ProxiesProviderProps } from './proxies-provider'\n\nexport const ProxiesProviderTraffic = ({ provider }: ProxiesProviderProps) => {\n  const calc = () => {\n    let progress = 0\n    let total = 0\n    let used = 0\n\n    if (provider.subscriptionInfo) {\n      const { download, upload, total: t } = provider.subscriptionInfo\n\n      total = t ?? 0\n\n      used = (download ?? 0) + (upload ?? 0)\n\n      progress = (used / (total ?? 0)) * 100\n    }\n\n    return { progress, total, used }\n  }\n\n  const { progress, total, used } = calc()\n\n  return (\n    <div className=\"flex items-center justify-between gap-4\">\n      <div className=\"w-full\">\n        <LinearProgress variant=\"determinate\" value={progress} />\n      </div>\n\n      <Tooltip title={`${parseTraffic(used)} / ${parseTraffic(total)}`}>\n        <div className=\"text-sm font-bold\">{progress.toFixed(2)}%</div>\n      </Tooltip>\n    </div>\n  )\n}\n\nexport default ProxiesProviderTraffic\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/providers/proxies-provider.tsx",
    "content": "import { useLockFn } from 'ahooks'\nimport dayjs from 'dayjs'\nimport { useState } from 'react'\nimport { useTranslation } from 'react-i18next'\nimport { message } from '@/utils/notification'\nimport { Refresh } from '@mui/icons-material'\nimport { Button, Chip, Paper } from '@mui/material'\nimport { ClashProxiesProviderQueryItem } from '@nyanpasu/interface'\nimport ProxiesProviderTraffic from './proxies-provider-traffic'\n\nexport interface ProxiesProviderProps {\n  provider: ClashProxiesProviderQueryItem\n}\n\nexport const ProxiesProvider = ({ provider }: ProxiesProviderProps) => {\n  const { t } = useTranslation()\n\n  const [loading, setLoading] = useState(false)\n\n  const handleClick = useLockFn(async () => {\n    try {\n      setLoading(true)\n\n      await provider.mutate()\n    } catch (e) {\n      message(`Update ${provider.name} failed.\\n${String(e)}`, {\n        kind: 'error',\n        title: t('Error'),\n      })\n    } finally {\n      setLoading(false)\n    }\n  })\n\n  return (\n    <Paper\n      className=\"flex h-full flex-col justify-between gap-2 p-5\"\n      sx={{\n        borderRadius: 6,\n      }}\n    >\n      <div className=\"flex items-start justify-between gap-2\">\n        <div className=\"ml-1\">\n          <p className=\"truncate text-lg font-bold\">{provider.name}</p>\n\n          <p className=\"truncate text-sm\">\n            {provider.vehicleType}/{provider.type}\n          </p>\n        </div>\n\n        <div className=\"text-right text-sm\">\n          {t('Last Update', {\n            fromNow: dayjs(provider.updatedAt).fromNow(),\n          })}\n        </div>\n      </div>\n\n      {provider.subscriptionInfo && (\n        <ProxiesProviderTraffic provider={provider} />\n      )}\n\n      <div className=\"flex items-center justify-between\">\n        <Chip\n          className=\"truncate font-bold\"\n          label={t('Proxy Set proxies', {\n            rule: provider.proxies.length,\n          })}\n        />\n\n        <Button\n          loading={loading}\n          size=\"small\"\n          variant=\"contained\"\n          className=\"!size-8 !min-w-0\"\n          onClick={handleClick}\n        >\n          <Refresh />\n        </Button>\n      </div>\n    </Paper>\n  )\n}\n\nexport default ProxiesProvider\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/providers/rules-provider.tsx",
    "content": "import { useLockFn } from 'ahooks'\nimport dayjs from 'dayjs'\nimport { useState } from 'react'\nimport { useTranslation } from 'react-i18next'\nimport { message } from '@/utils/notification'\nimport { Refresh } from '@mui/icons-material'\nimport { Button, Chip, Paper } from '@mui/material'\nimport { ClashRulesProviderQueryItem } from '@nyanpasu/interface'\n\nexport interface RulesProviderProps {\n  provider: ClashRulesProviderQueryItem\n}\n\nexport default function RulesProvider({ provider }: RulesProviderProps) {\n  const { t } = useTranslation()\n\n  const [loading, setLoading] = useState(false)\n\n  const handleClick = useLockFn(async () => {\n    try {\n      setLoading(true)\n\n      await provider.mutate()\n    } catch (e) {\n      message(`Update ${provider.name} failed.\\n${String(e)}`, {\n        kind: 'error',\n        title: t('Error'),\n      })\n    } finally {\n      setLoading(false)\n    }\n  })\n\n  return (\n    <Paper\n      className=\"flex flex-col gap-2 p-5\"\n      sx={{\n        borderRadius: 6,\n      }}\n    >\n      <div className=\"flex items-start justify-between gap-2\">\n        <div className=\"ml-1\">\n          <p className=\"truncate text-lg font-bold\">{provider.name}</p>\n\n          <p className=\"truncate text-sm\">\n            {provider.vehicleType}/{provider.behavior}\n          </p>\n        </div>\n\n        <div className=\"text-right text-sm\">\n          {t('Last Update', {\n            fromNow: dayjs(provider.updatedAt).fromNow(),\n          })}\n        </div>\n      </div>\n\n      <div className=\"flex items-center justify-between\">\n        <Chip\n          className=\"truncate font-bold\"\n          label={t('Rule Set rules', {\n            rule: provider.ruleCount,\n          })}\n        />\n\n        <Button\n          loading={loading}\n          size=\"small\"\n          variant=\"contained\"\n          className=\"!size-8 !min-w-0\"\n          onClick={handleClick}\n        >\n          <Refresh />\n        </Button>\n      </div>\n    </Paper>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/providers/theme-provider.tsx",
    "content": "import { isEqual, kebabCase } from 'lodash-es'\nimport {\n  createContext,\n  PropsWithChildren,\n  useCallback,\n  useContext,\n  useEffect,\n  useMemo,\n} from 'react'\nimport { insertStyle } from '@/utils/styled'\nimport {\n  argbFromHex,\n  hexFromArgb,\n  Theme,\n  themeFromSourceColor,\n} from '@material/material-color-utilities'\nimport { useSetting } from '@nyanpasu/interface'\nimport { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'\nimport { useLocalStorage } from '@uidotdev/usehooks'\n\nconst appWindow = getCurrentWebviewWindow()\n\nexport const DEFAULT_COLOR = '#1867C0'\n\nexport enum ThemeMode {\n  LIGHT = 'light',\n  DARK = 'dark',\n  SYSTEM = 'system',\n}\n\nconst CUSTOM_THEME_KEY = 'custom-theme' as const\n\nconst THEME_PALETTE_KEY = 'theme-palette-v1' as const\nconst THEME_CSS_VARS_KEY = 'theme-css-vars-v1' as const\n\nconst generateThemeCssVars = ({ schemes }: Theme) => {\n  let lightCssVars = ':root{'\n  let darkCssVars = ':root.dark{'\n\n  Object.entries(schemes).forEach(([mode, scheme]) => {\n    let inputScheme\n\n    // Safely convert scheme to JSON if possible, otherwise use as-is\n    if (typeof scheme.toJSON === 'function') {\n      inputScheme = scheme.toJSON()\n    } else {\n      inputScheme = scheme\n    }\n\n    Object.entries(inputScheme).forEach(([key, value]) => {\n      if (mode === 'light') {\n        lightCssVars += `--color-md-${kebabCase(key)}: ${hexFromArgb(value)};`\n      } else {\n        darkCssVars += `--color-md-${kebabCase(key)}: ${hexFromArgb(value)};`\n      }\n    })\n  })\n\n  lightCssVars += '}'\n  darkCssVars += '}'\n\n  return lightCssVars + darkCssVars\n}\n\nconst changeHtmlThemeMode = (mode: Omit<ThemeMode, 'system'>) => {\n  const root = document.documentElement\n\n  if (mode === ThemeMode.DARK) {\n    root.classList.add(ThemeMode.DARK)\n  } else {\n    root.classList.remove(ThemeMode.DARK)\n  }\n\n  if (mode === ThemeMode.LIGHT) {\n    root.classList.add(ThemeMode.LIGHT)\n  } else {\n    root.classList.remove(ThemeMode.LIGHT)\n  }\n}\n\nconst getSystemThemeMode = () => {\n  return window.matchMedia('(prefers-color-scheme: dark)').matches\n    ? ThemeMode.DARK\n    : ThemeMode.LIGHT\n}\n\nconst ThemeContext = createContext<{\n  themePalette: Theme\n  themeCssVars: string\n  themeColor: string\n  setThemeColor: (color: string) => Promise<void>\n  themeMode: ThemeMode\n  currentThemeMode: Omit<ThemeMode, 'system'>\n  setThemeMode: (mode: ThemeMode) => Promise<void>\n} | null>(null)\n\nexport function useExperimentalThemeContext() {\n  const context = useContext(ThemeContext)\n\n  if (!context) {\n    throw new Error(\n      'useExperimentalThemeContext must be used within a ExperimentalThemeProvider',\n    )\n  }\n\n  return context\n}\n\nexport function ExperimentalThemeProvider({ children }: PropsWithChildren) {\n  const themeMode = useSetting('theme_mode')\n\n  const themeColor = useSetting('theme_color')\n\n  const [cachedThemePalette, setCachedThemePalette] = useLocalStorage<Theme>(\n    THEME_PALETTE_KEY,\n    themeFromSourceColor(\n      // use default color if theme color is not set\n      argbFromHex(themeColor.value || DEFAULT_COLOR),\n    ),\n  )\n\n  const [cachedThemeCssVars, setCachedThemeCssVars] = useLocalStorage<string>(\n    THEME_CSS_VARS_KEY,\n    // initialize theme css vars from cached theme palette\n    generateThemeCssVars(cachedThemePalette),\n  )\n\n  // automatically insert custom theme css vars into document head\n  useEffect(() => {\n    insertStyle(CUSTOM_THEME_KEY, cachedThemeCssVars)\n  }, [cachedThemeCssVars])\n\n  const setThemeColor = useCallback(\n    async (color: string) => {\n      if (color === themeColor.value) {\n        return\n      } else {\n        await themeColor.upsert(color)\n      }\n\n      const materialColor = themeFromSourceColor(\n        // use default color if theme color is not set\n        argbFromHex(color || DEFAULT_COLOR),\n      )\n\n      if (isEqual(materialColor, cachedThemePalette)) {\n        return\n      } else {\n        setCachedThemePalette(materialColor)\n      }\n\n      const themeCssVars = generateThemeCssVars(materialColor)\n      setCachedThemeCssVars(themeCssVars)\n    },\n    [\n      themeColor,\n      cachedThemePalette,\n      setCachedThemeCssVars,\n      setCachedThemePalette,\n    ],\n  )\n\n  // initialize theme mode on mount\n  useEffect(() => {\n    const initializeTheme = async () => {\n      if (themeMode.value === ThemeMode.SYSTEM) {\n        // Apply a synchronous system fallback first to avoid a light flash.\n        changeHtmlThemeMode(getSystemThemeMode())\n\n        const systemTheme = await appWindow.theme()\n        changeHtmlThemeMode(\n          systemTheme === ThemeMode.DARK ? ThemeMode.DARK : ThemeMode.LIGHT,\n        )\n      } else if (\n        themeMode.value === ThemeMode.LIGHT ||\n        themeMode.value === ThemeMode.DARK\n      ) {\n        changeHtmlThemeMode(themeMode.value)\n      } else {\n        // Setting value may still be loading; keep current class to avoid visual flicker.\n      }\n    }\n\n    initializeTheme()\n  }, [themeMode.value])\n\n  // listen to theme changed event and change html theme mode\n  useEffect(() => {\n    const unlisten = appWindow.onThemeChanged((e) => {\n      if (themeMode.value === ThemeMode.SYSTEM) {\n        changeHtmlThemeMode(e.payload)\n      }\n    })\n\n    return () => {\n      unlisten.then((fn) => fn())\n    }\n  }, [themeMode.value])\n\n  const setThemeMode = useCallback(\n    async (mode: ThemeMode) => {\n      // if theme mode is not system, change html theme mode\n      if (mode !== ThemeMode.SYSTEM) {\n        changeHtmlThemeMode(mode)\n      }\n\n      if (mode !== themeMode.value) {\n        await themeMode.upsert(mode)\n      }\n    },\n    [themeMode],\n  )\n\n  const currentThemeMode = useMemo<Omit<ThemeMode, 'system'>>(() => {\n    if (themeMode.value === ThemeMode.DARK) {\n      return ThemeMode.DARK\n    }\n\n    if (themeMode.value === ThemeMode.LIGHT) {\n      return ThemeMode.LIGHT\n    }\n\n    return getSystemThemeMode()\n  }, [themeMode.value])\n\n  return (\n    <ThemeContext.Provider\n      value={{\n        themePalette: cachedThemePalette,\n        themeCssVars: cachedThemeCssVars,\n        themeColor: themeColor.value || DEFAULT_COLOR,\n        setThemeColor,\n        themeMode: themeMode.value as ThemeMode,\n        currentThemeMode,\n        setThemeMode,\n      }}\n    >\n      {children}\n    </ThemeContext.Provider>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/providers/update-providers.tsx",
    "content": "import { useLockFn } from 'ahooks'\nimport { useState } from 'react'\nimport { useTranslation } from 'react-i18next'\nimport { message } from '@/utils/notification'\nimport { Refresh } from '@mui/icons-material'\nimport { Button } from '@mui/material'\nimport { useClashRulesProvider } from '@nyanpasu/interface'\n\nexport const UpdateProviders = () => {\n  const { t } = useTranslation()\n\n  const [loading, setLoading] = useState(false)\n\n  const rulesProvider = useClashRulesProvider()\n\n  const handleProviderUpdate = useLockFn(async () => {\n    if (!rulesProvider.data) {\n      message(`No Providers.`, {\n        kind: 'info',\n        title: t('Info'),\n      })\n\n      return\n    }\n\n    try {\n      setLoading(true)\n\n      await Promise.all(\n        Object.values(rulesProvider.data).map((provider) => {\n          return provider.mutate()\n        }),\n      )\n    } catch (e) {\n      message(`Update all failed.\\n${String(e)}`, {\n        kind: 'error',\n        title: t('Error'),\n      })\n    } finally {\n      setLoading(false)\n    }\n  })\n\n  return (\n    <Button\n      variant=\"contained\"\n      loading={loading}\n      startIcon={<Refresh />}\n      onClick={handleProviderUpdate}\n    >\n      {t('Update All Rules Providers')}\n    </Button>\n  )\n}\n\nexport default UpdateProviders\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/providers/update-proxies-providers.tsx",
    "content": "import { useLockFn } from 'ahooks'\nimport { useState } from 'react'\nimport { useTranslation } from 'react-i18next'\nimport { message } from '@/utils/notification'\nimport { Refresh } from '@mui/icons-material'\nimport { Button } from '@mui/material'\nimport { useClashProxiesProvider } from '@nyanpasu/interface'\n\nexport const UpdateProxiesProviders = () => {\n  const { t } = useTranslation()\n\n  const [loading, setLoading] = useState(false)\n\n  const proxiesProvider = useClashProxiesProvider()\n\n  const handleProviderUpdate = useLockFn(async () => {\n    if (!proxiesProvider.data) {\n      message(`No Providers.`, {\n        kind: 'info',\n        title: t('Info'),\n      })\n\n      return\n    }\n\n    try {\n      setLoading(true)\n\n      await Promise.all(\n        Object.entries(proxiesProvider.data).map(([_, provider]) =>\n          provider.mutate(),\n        ),\n      )\n    } catch (e) {\n      message(`Update all failed.\\n${String(e)}`, {\n        kind: 'error',\n        title: t('Error'),\n      })\n    } finally {\n      setLoading(false)\n    }\n  })\n\n  return (\n    <Button\n      variant=\"contained\"\n      loading={loading}\n      startIcon={<Refresh />}\n      onClick={handleProviderUpdate}\n    >\n      {t('Update All Proxies Providers')}\n    </Button>\n  )\n}\n\nexport default UpdateProxiesProviders\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/proxies/delay-button.tsx",
    "content": "import { useDebounceFn, useLockFn } from 'ahooks'\nimport { memo, useState } from 'react'\nimport { useTranslation } from 'react-i18next'\nimport { Bolt, Done } from '@mui/icons-material'\nimport { Button, CircularProgress, Tooltip } from '@mui/material'\nimport { alpha, cn } from '@nyanpasu/ui'\n\nexport const DelayButton = memo(function DelayButton({\n  onClick,\n}: {\n  onClick: () => Promise<void>\n}) {\n  const { t } = useTranslation()\n\n  const [loading, setLoading] = useState(false)\n\n  const [mounted, setMounted] = useState(false)\n\n  const { run: runMounted, cancel: cancelMounted } = useDebounceFn(\n    () => setMounted(false),\n    { wait: 1000 },\n  )\n\n  const handleClick = useLockFn(async () => {\n    try {\n      setLoading(true)\n      setMounted(true)\n      cancelMounted()\n\n      await onClick()\n    } finally {\n      setLoading(false)\n      runMounted()\n    }\n  })\n\n  const isSuccess = mounted && !loading\n\n  return (\n    <Tooltip title={t('Latency check')}>\n      <Button\n        className=\"!fixed right-8 bottom-8 z-10 size-16 !rounded-2xl backdrop-blur\"\n        sx={(theme) => ({\n          boxShadow: 8,\n          backgroundColor: alpha(\n            theme.vars.palette[isSuccess ? 'success' : 'primary'].main,\n            isSuccess ? 0.7 : 0.3,\n          ),\n\n          '&:hover': {\n            backgroundColor: alpha(theme.vars.palette.primary.main, 0.45),\n          },\n\n          '&.MuiButton-loading': {\n            backgroundColor: alpha(theme.vars.palette.primary.main, 0.15),\n          },\n        })}\n        onClick={handleClick}\n      >\n        <Bolt\n          className={cn(\n            '!size-8',\n            '!transition-opacity',\n            mounted ? 'opacity-0' : 'opacity-100',\n          )}\n        />\n\n        {mounted && (\n          <CircularProgress\n            size={32}\n            className={cn(\n              'transition-opacity',\n              'absolute',\n              loading ? 'opacity-100' : 'opacity-0',\n            )}\n          />\n        )}\n\n        <Done\n          color=\"success\"\n          className={cn(\n            '!size-8',\n            'absolute',\n            '!transition-opacity',\n            isSuccess ? 'opacity-100' : 'opacity-0',\n          )}\n        />\n      </Button>\n    </Tooltip>\n  )\n})\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/proxies/delay-chip.tsx",
    "content": "import { memo, useState } from 'react'\nimport { useColorSxForDelay } from '@/hooks/theme'\nimport { mergeSxProps } from '@/utils/mui-theme'\nimport { Bolt } from '@mui/icons-material'\nimport { CircularProgress } from '@mui/material'\nimport { cn } from '@nyanpasu/ui'\nimport FeatureChip from './feature-chip'\n\nexport const DelayChip = memo(function DelayChip({\n  className,\n  delay,\n  onClick,\n}: {\n  className?: string\n  delay: number\n  onClick: () => Promise<void>\n}) {\n  const [loading, setLoading] = useState(false)\n\n  const handleClick = async () => {\n    try {\n      setLoading(true)\n\n      await onClick()\n    } finally {\n      setLoading(false)\n    }\n  }\n\n  return (\n    <FeatureChip\n      className={cn(className, loading && '!visible')}\n      sx={mergeSxProps(\n        {\n          ml: 'auto',\n        },\n        useColorSxForDelay(delay),\n      )}\n      label={\n        <>\n          <span\n            className={cn(\n              'flex items-center px-[1px] transition-opacity',\n              loading ? 'opacity-0' : 'opacity-100',\n            )}\n          >\n            {delay === -1 ? (\n              <Bolt className=\"scale-[0.6]\" />\n            ) : !!delay && delay < 10000 ? (\n              `${delay} ms`\n            ) : (\n              'timeout'\n            )}\n          </span>\n\n          <CircularProgress\n            size={12}\n            className={cn(\n              'transition-opacity',\n              'absolute',\n              'animate-spin',\n              'top-0',\n              'bottom-0',\n              'left-0',\n              'right-0',\n              'm-auto',\n              loading ? 'opacity-100' : 'opacity-0',\n            )}\n          />\n        </>\n      }\n      variant=\"filled\"\n      onClick={(e) => {\n        e.preventDefault()\n        e.stopPropagation()\n        handleClick()\n      }}\n    />\n  )\n})\n\nexport default DelayChip\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/proxies/feature-chip.tsx",
    "content": "import { memo } from 'react'\nimport { mergeSxProps } from '@/utils/mui-theme'\nimport { Chip, ChipProps } from '@mui/material'\n\nexport const FeatureChip = memo(function FeatureChip(props: ChipProps) {\n  return (\n    <Chip\n      variant=\"outlined\"\n      size=\"small\"\n      {...props}\n      sx={mergeSxProps(\n        {\n          fontSize: 10,\n          height: 16,\n          padding: 0,\n          '& .MuiChip-label': {\n            padding: '0 4px',\n          },\n        },\n        props.sx,\n      )}\n    />\n  )\n})\n\nexport default FeatureChip\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/proxies/group-list.tsx",
    "content": "import { useAtom, useAtomValue } from 'jotai'\nimport { memo, RefObject, useDeferredValue, useMemo } from 'react'\nimport useSWR from 'swr'\nimport { Virtualizer } from 'virtua'\nimport { proxyGroupAtom } from '@/store'\nimport { proxiesFilterAtom } from '@/store/proxies'\nimport {\n  ListItem,\n  ListItemButton,\n  ListItemButtonProps,\n  ListItemIcon,\n  ListItemText,\n} from '@mui/material'\nimport { getServerPort, useClashProxies } from '@nyanpasu/interface'\nimport { alpha, LazyImage } from '@nyanpasu/ui'\n\nconst IconRender = memo(function IconRender({ icon }: { icon: string }) {\n  const {\n    data: serverPort,\n    isLoading,\n    error,\n  } = useSWR('/getServerPort', getServerPort)\n  const src = icon.trim().startsWith('<svg')\n    ? `data:image/svg+xml;base64,${btoa(icon)}`\n    : icon\n  const cachedUrl = useMemo(() => {\n    if (!src.startsWith('http')) {\n      return src\n    }\n    return `http://localhost:${serverPort}/cache/icon?url=${btoa(src)}`\n  }, [src, serverPort])\n  if (isLoading || error) {\n    return null\n  }\n  return (\n    <ListItemIcon>\n      <LazyImage\n        className=\"h-11 w-11\"\n        loadingClassName=\"rounded-full\"\n        src={cachedUrl}\n      />\n    </ListItemIcon>\n  )\n})\n\nexport interface GroupListProps extends ListItemButtonProps {\n  scrollRef: RefObject<HTMLElement>\n}\n\nexport const GroupList = ({\n  scrollRef,\n  ...listItemButtonProps\n}: GroupListProps) => {\n  const {\n    proxies: { data },\n  } = useClashProxies()\n\n  const [proxyGroup, setProxyGroup] = useAtom(proxyGroupAtom)\n  const proxiesFilter = useAtomValue(proxiesFilterAtom)\n  const deferredProxiesFilter = useDeferredValue(proxiesFilter)\n\n  const handleSelect = (index: number) => {\n    setProxyGroup({ selector: index })\n  }\n\n  const groups = useMemo(() => {\n    if (!data?.groups) {\n      return []\n    }\n\n    return data.groups.filter((group) => {\n      return (\n        !deferredProxiesFilter ||\n        group.name\n          .toLowerCase()\n          .includes(deferredProxiesFilter.toLowerCase()) ||\n        group.all?.some((proxy) => {\n          return proxy.name\n            .toLowerCase()\n            .includes(deferredProxiesFilter.toLowerCase())\n        }) ||\n        false\n      )\n    })\n  }, [data?.groups, deferredProxiesFilter])\n\n  return (\n    <Virtualizer scrollRef={scrollRef}>\n      {groups.map((group, index) => {\n        const selected = index === proxyGroup.selector\n\n        return (\n          <ListItem key={index} disablePadding>\n            <ListItemButton\n              selected={selected}\n              onClick={() => handleSelect(index)}\n              sx={[\n                (theme) => ({\n                  backgroundColor: selected\n                    ? `${alpha(theme.vars.palette.primary.main, 0.3)} !important`\n                    : null,\n                }),\n              ]}\n              {...listItemButtonProps}\n            >\n              {group.icon && <IconRender icon={group.icon} />}\n\n              <ListItemText\n                className=\"!truncate\"\n                primary={group.name}\n                secondary={group.now}\n              />\n            </ListItemButton>\n          </ListItem>\n        )\n      })}\n    </Virtualizer>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/proxies/index.ts",
    "content": "export * from './group-list'\nexport * from './node-list'\nexport * from './delay-button'\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/proxies/node-card.module.d.scss.ts",
    "content": "declare const classNames: {\n  readonly Card: 'Card'\n  readonly NoDelay: 'NoDelay'\n  readonly DelayChip: 'DelayChip'\n}\nexport default classNames\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/proxies/node-card.module.scss",
    "content": ".Card {\n  &.NoDelay {\n    .DelayChip {\n      visibility: hidden;\n    }\n\n    &:hover {\n      .DelayChip {\n        visibility: visible;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/proxies/node-card.module.scss.d.ts",
    "content": "declare const classNames: {\n  readonly Card: 'Card'\n  readonly NoDelay: 'NoDelay'\n  readonly DelayChip: 'DelayChip'\n}\nexport default classNames\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/proxies/node-card.tsx",
    "content": "import { useLockFn } from 'ahooks'\nimport { CSSProperties, memo, useMemo } from 'react'\nimport Box from '@mui/material/Box'\nimport type { SxProps, Theme } from '@mui/material/styles'\nimport { ClashProxiesQueryProxyItem } from '@nyanpasu/interface'\nimport { alpha, cn } from '@nyanpasu/ui'\nimport { PaperSwitchButton } from '../setting/modules/system-proxy'\nimport DelayChip from './delay-chip'\nimport FeatureChip from './feature-chip'\nimport styles from './node-card.module.scss'\nimport { filterDelay } from './utils'\n\nexport const NodeCard = memo(function NodeCard({\n  node,\n  now,\n  disabled,\n  style,\n}: {\n  node: ClashProxiesQueryProxyItem\n  now?: string | null\n  disabled?: boolean\n  style?: CSSProperties\n}) {\n  const delay = useMemo(() => filterDelay(node.history), [node.history])\n\n  const checked = node.name === now\n\n  const handleDelayClick = useLockFn(async () => {\n    await node.mutateDelay()\n  })\n\n  const handleClick = useLockFn(async () => {\n    await node.mutateSelect()\n  })\n\n  return (\n    <PaperSwitchButton\n      label={node.name}\n      checked={checked}\n      disableLoading\n      onClick={handleClick}\n      disabled={disabled}\n      style={style}\n      className={cn(styles.Card, delay === -1 && styles.NoDelay)}\n      sxPaper={\n        ((theme) => ({\n          backgroundColor: checked\n            ? alpha(theme.vars.palette.primary.main, 0.3)\n            : theme.vars.palette.grey[100],\n          ...theme.applyStyles('dark', {\n            backgroundColor: checked\n              ? alpha(theme.vars.palette.primary.main, 0.3)\n              : theme.vars.palette.grey[900],\n          }),\n        })) as SxProps<Theme>\n      }\n    >\n      <Box width=\"100%\" display=\"flex\" gap={0.5}>\n        <FeatureChip label={node.type} />\n\n        {node.udp && <FeatureChip label=\"UDP\" />}\n\n        <DelayChip\n          className={styles.DelayChip}\n          delay={delay}\n          onClick={handleDelayClick}\n        />\n      </Box>\n    </PaperSwitchButton>\n  )\n})\n\nexport default NodeCard\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/proxies/node-list.tsx",
    "content": "import { AnimatePresence, motion } from 'framer-motion'\nimport { useAtomValue } from 'jotai'\nimport {\n  forwardRef,\n  RefObject,\n  useCallback,\n  useDeferredValue,\n  useEffect,\n  useImperativeHandle,\n  useRef,\n  useState,\n} from 'react'\nimport { Virtualizer, VListHandle } from 'virtua'\nimport { proxyGroupAtom, proxyGroupSortAtom } from '@/store'\nimport { proxiesFilterAtom } from '@/store/proxies'\nimport {\n  ClashProxiesQueryProxyItem,\n  ProxyGroupItem,\n  useClashProxies,\n  useProxyMode,\n  useSetting,\n} from '@nyanpasu/interface'\nimport { cn, useBreakpointValue } from '@nyanpasu/ui'\nimport NodeCard from './node-card'\nimport { nodeSortingFn } from './utils'\n\ntype RenderClashProxy = ClashProxiesQueryProxyItem & { renderLayoutKey: string }\n\nexport interface NodeListRef {\n  scrollToCurrent: () => void\n}\n\nexport const NodeList = forwardRef(function NodeList(\n  { scrollRef }: { scrollRef: RefObject<HTMLElement> },\n  ref,\n) {\n  const {\n    proxies: { data },\n  } = useClashProxies()\n\n  const { value: proxyMode } = useProxyMode()\n\n  const proxyGroup = useAtomValue(proxyGroupAtom)\n  const proxiesFilter = useAtomValue(proxiesFilterAtom)\n  const deferredProxiesFilter = useDeferredValue(proxiesFilter)\n\n  const proxyGroupSort = useAtomValue(proxyGroupSortAtom)\n\n  const [group, setGroup] = useState<ProxyGroupItem>()\n\n  const sortGroup = useCallback(() => {\n    if (!proxyMode.global) {\n      if (proxyGroup.selector !== null) {\n        // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain\n        const selectedGroup = data?.groups[proxyGroup.selector]!\n\n        if (selectedGroup) {\n          setGroup(nodeSortingFn(selectedGroup, proxyGroupSort))\n        }\n      }\n    } else {\n      if (data?.global) {\n        setGroup(nodeSortingFn(data?.global, proxyGroupSort))\n      } else {\n        setGroup(data?.global)\n      }\n    }\n  }, [\n    proxyMode.global,\n    proxyGroup.selector,\n    data?.groups,\n    data?.global,\n    proxyGroupSort,\n  ])\n\n  useEffect(() => {\n    sortGroup()\n  }, [sortGroup])\n\n  const column = useBreakpointValue({\n    xs: 1,\n    sm: 1,\n    md: 2,\n    lg: 3,\n    xl: 4,\n  })\n\n  const [renderList, setRenderList] = useState<RenderClashProxy[][]>([])\n\n  useEffect(() => {\n    if (!group?.all) return\n\n    const nodeNames: string[] = []\n\n    let nodes = group?.all || []\n    if (!!deferredProxiesFilter && deferredProxiesFilter !== group?.name) {\n      nodes = nodes.filter((node) =>\n        node.name.toLowerCase().includes(deferredProxiesFilter.toLowerCase()),\n      )\n    }\n\n    const list = nodes.reduce<RenderClashProxy[][]>((result, value, index) => {\n      const getKey = () => {\n        const filter = nodeNames.filter((i) => i === value.name)\n\n        if (filter.length === 0) {\n          return value.name\n        } else {\n          return `${value.name}-${filter.length}`\n        }\n      }\n\n      if (index % column === 0) {\n        result.push([])\n      }\n\n      result[Math.floor(index / column)].push({\n        ...(value as ClashProxiesQueryProxyItem),\n        renderLayoutKey: getKey(),\n      })\n\n      nodeNames.push(value.name)\n\n      return result\n    }, [])\n\n    setRenderList(list)\n  }, [group?.all, group?.name, column, deferredProxiesFilter])\n\n  const { value: disableMotion } = useSetting('lighten_animation_effects')\n\n  const vListRef = useRef<VListHandle>(null)\n\n  useImperativeHandle(ref, () => ({\n    scrollToCurrent: () => {\n      const index = renderList.findIndex((node) =>\n        node.some((item) => item.name === group?.now),\n      )\n\n      vListRef.current?.scrollToIndex(index, {\n        align: 'center',\n        smooth: true,\n      })\n    },\n  }))\n\n  return (\n    <AnimatePresence initial={false} mode=\"sync\">\n      <Virtualizer ref={vListRef} scrollRef={scrollRef}>\n        {renderList?.map((node, index) => {\n          return (\n            <div\n              key={index}\n              className={cn('grid gap-2 px-2 pb-2', index === 0 && 'pt-14')}\n              style={{ gridTemplateColumns: `repeat(${column} , 1fr)` }}\n            >\n              {node.map((render) => {\n                const Card = () => (\n                  <NodeCard\n                    node={render}\n                    now={group?.now}\n                    disabled={group?.type !== 'Selector'}\n                  />\n                )\n\n                return disableMotion ? (\n                  <div key={render.name} className=\"relative overflow-hidden\">\n                    <Card />\n                  </div>\n                ) : (\n                  <motion.div\n                    key={render.name}\n                    layoutId={`node-${render.renderLayoutKey}`}\n                    className=\"relative overflow-hidden\"\n                    layout=\"position\"\n                    initial={false}\n                  >\n                    <Card />\n                  </motion.div>\n                )\n              })}\n            </div>\n          )\n        })}\n      </Virtualizer>\n    </AnimatePresence>\n  )\n})\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/proxies/proxy-group-name.tsx",
    "content": "import { AnimatePresence, motion } from 'framer-motion'\nimport { memo } from 'react'\nimport { useSetting } from '@nyanpasu/interface'\n\nexport const ProxyGroupName = memo(function ProxyGroupName({\n  name,\n}: {\n  name: string\n}) {\n  const { value: disbaleMotion } = useSetting('lighten_animation_effects')\n\n  return disbaleMotion ? (\n    <>{name}</>\n  ) : (\n    <AnimatePresence mode=\"sync\" initial={false}>\n      <motion.div\n        key={`group-name-${name}`}\n        className=\"absolute\"\n        initial={{ x: 100, opacity: 0 }}\n        animate={{ x: 0, opacity: 1 }}\n        exit={{ x: -100, opacity: 0 }}\n        transition={{\n          type: 'spring',\n          bounce: 0,\n          duration: 0.5,\n        }}\n      >\n        {name}\n      </motion.div>\n    </AnimatePresence>\n  )\n})\n\nexport default ProxyGroupName\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/proxies/scroll-current-node.tsx",
    "content": "import { useTranslation } from 'react-i18next'\nimport { Radar } from '@mui/icons-material'\nimport { Button, Tooltip } from '@mui/material'\nimport { alpha } from '@nyanpasu/ui'\n\nexport const ScrollCurrentNode = ({ onClick }: { onClick?: () => void }) => {\n  const { t } = useTranslation()\n\n  return (\n    <Tooltip title={t('Locate')}>\n      <Button\n        size=\"small\"\n        className=\"!size-8 !min-w-0\"\n        sx={(theme) => ({\n          backgroundColor: alpha(theme.vars.palette.primary.main, 0.1),\n        })}\n        onClick={onClick}\n      >\n        <Radar />\n      </Button>\n    </Tooltip>\n  )\n}\n\nexport default ScrollCurrentNode\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/proxies/sort-selector.tsx",
    "content": "import { useAtom } from 'jotai'\nimport { memo, useState } from 'react'\nimport { useTranslation } from 'react-i18next'\nimport { proxyGroupSortAtom } from '@/store'\nimport { Button, Menu, MenuItem } from '@mui/material'\nimport { alpha } from '@nyanpasu/ui'\n\nexport const SortSelector = memo(function SortSelector() {\n  const { t } = useTranslation()\n\n  const [proxyGroupSort, setProxyGroupSort] = useAtom(proxyGroupSortAtom)\n\n  type SortType = typeof proxyGroupSort\n\n  const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null)\n\n  const handleClick = (sort: SortType) => {\n    setAnchorEl(null)\n    setProxyGroupSort(sort)\n  }\n\n  const tmaps: { [key: string]: string } = {\n    default: 'Sort by default',\n    delay: 'Sort by latency',\n    name: 'Sort by name',\n  }\n\n  return (\n    <>\n      <Button\n        size=\"small\"\n        className=\"!px-2\"\n        sx={(theme) => ({\n          textTransform: 'none',\n          backgroundColor: alpha(theme.vars.palette.primary.main, 0.1),\n        })}\n        onClick={(e) => setAnchorEl(e.currentTarget)}\n      >\n        {t(tmaps[proxyGroupSort])}\n      </Button>\n\n      <Menu\n        anchorEl={anchorEl}\n        open={Boolean(anchorEl)}\n        onClose={() => setAnchorEl(null)}\n      >\n        {Object.entries(tmaps).map(([key, value], index) => {\n          return (\n            <MenuItem key={index} onClick={() => handleClick(key as SortType)}>\n              {t(value)}\n            </MenuItem>\n          )\n        })}\n      </Menu>\n    </>\n  )\n})\n\nexport default SortSelector\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/proxies/utils.ts",
    "content": "import type { ProxyGroupItem, ProxyItemHistory } from '@nyanpasu/interface'\n\nexport const filterDelay = (history?: ProxyItemHistory[]): number => {\n  if (!history || history.length === 0) {\n    return -1\n  } else {\n    return history[history.length - 1].delay\n  }\n}\n\nexport enum SortType {\n  Default = 'default',\n  Delay = 'delay',\n  Name = 'name',\n}\n\nexport const nodeSortingFn = (\n  selectedGroup: ProxyGroupItem,\n  type: SortType,\n) => {\n  let sortedList = selectedGroup.all?.slice()\n\n  switch (type) {\n    case SortType.Delay: {\n      sortedList = sortedList?.sort((a, b) => {\n        const delayA = filterDelay(a.history)\n        const delayB = filterDelay(b.history)\n\n        if (delayA === -1 || delayA === -2) return 1\n        if (delayB === -1 || delayB === -2) return -1\n\n        if (delayA === 0) return 1\n        if (delayB === 0) return -1\n\n        return delayA - delayB\n      })\n\n      break\n    }\n\n    case SortType.Name: {\n      sortedList = sortedList?.sort((a, b) => a.name.localeCompare(b.name))\n\n      break\n    }\n  }\n\n  return {\n    ...selectedGroup,\n    all: sortedList,\n  }\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/router/animated-outlet.tsx",
    "content": "import { AnimatePresence, motion, useIsPresent, Variants } from 'framer-motion'\nimport { ComponentProps, useRef } from 'react'\nimport {\n  Outlet,\n  RouterContextProvider,\n  useMatch,\n  useMatches,\n  useRouter,\n  useRouterState,\n} from '@tanstack/react-router'\n\ntype TransitionDirection = 1 | -1\n\nconst directionalSlideVariants = {\n  forward: {\n    initial: {\n      translateX: '30%',\n      opacity: 0,\n    },\n    visible: {\n      translateX: '0%',\n      opacity: 1,\n    },\n    hidden: {\n      translateX: '-30%',\n      opacity: 0,\n    },\n  },\n  backward: {\n    initial: {\n      translateX: '-30%',\n      opacity: 0,\n    },\n    visible: {\n      translateX: '0%',\n      opacity: 1,\n    },\n    hidden: {\n      translateX: '30%',\n      opacity: 0,\n    },\n  },\n} satisfies Record<'forward' | 'backward', Variants>\n\nfunction getDirectionalVariant(direction: TransitionDirection) {\n  return direction === 1\n    ? directionalSlideVariants.forward\n    : directionalSlideVariants.backward\n}\n\nexport function AnimatedOutlet({\n  ref,\n  ...props\n}: ComponentProps<typeof motion.div>) {\n  const isPresent = useIsPresent()\n\n  const matches = useMatches()\n  const prevMatches = useRef(matches)\n\n  const router = useRouter()\n\n  // Frozen router for the exit animation, created once when isPresent becomes false\n  const frozenRouterRef = useRef<typeof router | null>(null)\n\n  let renderedRouter = router\n\n  if (isPresent) {\n    prevMatches.current = matches\n    frozenRouterRef.current = null\n  } else {\n    if (!frozenRouterRef.current) {\n      // Build patched matches: old route data (prevMatches) but new match IDs\n      const patched = [\n        ...matches.map((m, i) => ({\n          ...(prevMatches.current[i] || m),\n          id: m.id,\n        })),\n        ...prevMatches.current.slice(matches.length),\n      ]\n\n      // Snapshot of router state with old route's matches\n      const patchedState = { ...router.__store.state, matches: patched }\n\n      // Create a fake store that always returns the frozen patched state.\n      // Object.create delegates everything else (subscribe, atom, etc.) to the real\n      // store via the prototype chain, so subscriptions still work — but the snapshot\n      // always returns patchedState, which never changes, so there are no re-renders.\n      const fakeStore = Object.create(router.__store)\n      Object.defineProperty(fakeStore, 'get', {\n        value: () => patchedState,\n        configurable: true,\n      })\n      Object.defineProperty(fakeStore, 'state', {\n        get: () => patchedState,\n        configurable: true,\n      })\n\n      // Create a fake router that delegates everything to the real router except __store\n      const fakeRouter = Object.create(router)\n      Object.defineProperty(fakeRouter, '__store', {\n        value: fakeStore,\n        configurable: true,\n      })\n\n      frozenRouterRef.current = fakeRouter\n    }\n\n    // force type safety\n    renderedRouter = frozenRouterRef.current!\n  }\n\n  return (\n    <motion.div ref={ref} {...props}>\n      <RouterContextProvider router={renderedRouter}>\n        <Outlet />\n      </RouterContextProvider>\n    </motion.div>\n  )\n}\n\nexport function AnimatedOutletPreset(props: ComponentProps<typeof motion.div>) {\n  const matches = useMatches()\n  const match = useMatch({ strict: false })\n  const pathname = useRouterState({\n    select: (state) => state.location.pathname,\n  })\n  const nextMatchIndex = matches.findIndex((d) => d.id === match.id) + 1\n  const nextMatch = matches[nextMatchIndex]\n\n  const id = nextMatch ? nextMatch.id : ''\n  const prevPathRef = useRef(pathname)\n  const directionRef = useRef<TransitionDirection>(1)\n\n  if (prevPathRef.current !== pathname) {\n    const prevPath = prevPathRef.current\n    const nextPath = pathname\n\n    if (nextPath.startsWith(`${prevPath}/`)) {\n      directionRef.current = 1\n    } else if (prevPath.startsWith(`${nextPath}/`)) {\n      directionRef.current = -1\n    } else {\n      // Non-ancestor navigation (including sibling routes) uses forward animation.\n      directionRef.current = 1\n    }\n\n    prevPathRef.current = pathname\n  }\n\n  const direction = directionRef.current\n  const selectedVariants = getDirectionalVariant(direction)\n\n  return (\n    <AnimatePresence mode=\"popLayout\" initial={false} custom={direction}>\n      <AnimatedOutlet\n        key={id}\n        custom={direction}\n        layout=\"position\"\n        initial=\"initial\"\n        animate=\"visible\"\n        exit=\"hidden\"\n        variants={{\n          initial: (customDirection: TransitionDirection) =>\n            getDirectionalVariant(customDirection).initial,\n          visible: selectedVariants.visible,\n          hidden: (customDirection: TransitionDirection) =>\n            getDirectionalVariant(customDirection).hidden,\n        }}\n        transition={{\n          type: 'spring',\n          bounce: 0.1,\n          duration: 0.35,\n        }}\n        {...props}\n      />\n    </AnimatePresence>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/rules/modules/store.ts",
    "content": "import { atom } from 'jotai'\nimport { RefObject } from 'react'\nimport { ClashRule } from '@nyanpasu/interface'\n\nexport const atomRulePage = atom<{\n  data?: ClashRule[]\n  scrollRef?: RefObject<HTMLElement>\n}>()\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/rules/rule-item.tsx",
    "content": "import { Box, SxProps, Theme } from '@mui/material'\nimport { ClashRule } from '@nyanpasu/interface'\n\ninterface Props {\n  index: number\n  value: ClashRule\n}\n\nconst COLOR = [\n  (theme) => ({\n    color: theme.vars.palette.primary.main,\n  }),\n  (theme) => ({\n    color: theme.vars.palette.secondary.main,\n  }),\n  (theme) => ({\n    color: theme.vars.palette.info.main,\n  }),\n  (theme) => ({\n    color: theme.vars.palette.warning.main,\n  }),\n  (theme) => ({\n    color: theme.vars.palette.success.main,\n  }),\n] satisfies SxProps<Theme>[]\n\nconst RuleItem = ({ index, value }: Props) => {\n  const parseColorSx: (text: string) => SxProps<Theme> = (text) => {\n    const TYPE = {\n      reject: ['REJECT', 'REJECT-DROP'],\n      direct: ['DIRECT'],\n    }\n\n    if (TYPE.reject.includes(text))\n      return (theme) => ({ color: theme.vars.palette.error.main })\n\n    if (TYPE.direct.includes(text))\n      return (theme) => ({ color: theme.vars.palette.text.primary })\n\n    let sum = 0\n\n    for (let i = 0; i < text.length; i++) {\n      sum += text.charCodeAt(i)\n    }\n\n    return COLOR[sum % COLOR.length]\n  }\n\n  return (\n    <div className=\"flex p-2 pr-7 pl-7 select-text\">\n      <Box\n        sx={(theme) => ({ color: theme.vars.palette.text.secondary })}\n        className=\"min-w-14\"\n      >\n        {index + 1}\n      </Box>\n\n      <div className=\"flex flex-col gap-1\">\n        <Box sx={(theme) => ({ color: theme.vars.palette.text.primary })}>\n          {value.payload || '-'}\n        </Box>\n\n        <div className=\"flex gap-8\">\n          <div className=\"min-w-40 text-sm\">{value.type}</div>\n\n          <Box className=\"text-s text-sm\" sx={parseColorSx(value.proxy)}>\n            {value.proxy}\n          </Box>\n        </div>\n      </div>\n    </div>\n  )\n}\n\nexport default RuleItem\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/rules/rule-page.tsx",
    "content": "import { useAtomValue } from 'jotai'\nimport { useTranslation } from 'react-i18next'\nimport { Virtualizer } from 'virtua'\nimport ContentDisplay from '../base/content-display'\nimport { atomRulePage } from './modules/store'\nimport RuleItem from './rule-item'\n\nexport const RulePage = () => {\n  const { t } = useTranslation()\n\n  const rule = useAtomValue(atomRulePage)\n\n  return rule?.data?.length ? (\n    <Virtualizer scrollRef={rule?.scrollRef}>\n      {rule.data.map((item, index) => {\n        return <RuleItem key={index} index={index} value={item} />\n      })}\n    </Virtualizer>\n  ) : (\n    <ContentDisplay className=\"absolute\" message={t('No Rules')} />\n  )\n}\n\nexport default RulePage\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/setting/modules/clash-core.tsx",
    "content": "import { motion } from 'framer-motion'\nimport { isObject } from 'lodash-es'\nimport { useState } from 'react'\nimport { useTranslation } from 'react-i18next'\nimport ClashRs from '@/assets/image/core/clash-rs.png'\nimport ClashMeta from '@/assets/image/core/clash.meta.png'\nimport Clash from '@/assets/image/core/clash.png'\nimport { formatError } from '@/utils'\nimport { message } from '@/utils/notification'\nimport parseTraffic from '@/utils/parse-traffic'\nimport FiberManualRecord from '@mui/icons-material/FiberManualRecord'\nimport Update from '@mui/icons-material/Update'\nimport { Box, Button } from '@mui/material'\nimport ListItem from '@mui/material/ListItem'\nimport ListItemButton from '@mui/material/ListItemButton'\nimport Tooltip from '@mui/material/Tooltip'\nimport {\n  ClashCore,\n  ClashCoresDetail,\n  InspectUpdater,\n  inspectUpdater,\n  useClashCores,\n} from '@nyanpasu/interface'\nimport { alpha, cleanDeepClickEvent, cn } from '@nyanpasu/ui'\n\nexport const getImage = (core: ClashCore) => {\n  switch (core) {\n    case 'mihomo':\n    case 'mihomo-alpha': {\n      return ClashMeta\n    }\n\n    case 'clash-rs':\n    case 'clash-rs-alpha': {\n      return ClashRs\n    }\n\n    default: {\n      return Clash\n    }\n  }\n}\n\nconst calcProgress = (data?: InspectUpdater) => {\n  return (\n    (Number(data?.downloader?.downloaded) / Number(data?.downloader?.total)) *\n    100\n  )\n}\n\nconst CardProgress = ({\n  data,\n  show,\n}: {\n  data?: InspectUpdater\n  show?: boolean\n}) => {\n  const parsedState = () => {\n    if (data?.downloader?.state) {\n      return 'waiting'\n    } else if (isObject(data?.downloader.state)) {\n      return data?.downloader.state.failed\n    } else {\n      return data?.downloader.state\n    }\n  }\n\n  return (\n    <Box\n      component={motion.div}\n      className={cn(\n        'absolute top-0 left-0 z-10 h-full w-full rounded-2xl backdrop-blur',\n        'flex flex-col items-center justify-center gap-2',\n      )}\n      sx={(theme) => ({\n        backgroundColor: alpha(theme.vars.palette.primary.main, 0.3),\n      })}\n      animate={show ? 'open' : 'closed'}\n      initial={{ opacity: 0 }}\n      variants={{\n        open: {\n          opacity: 1,\n          display: 'flex',\n        },\n        closed: {\n          opacity: 0,\n          transitionEnd: {\n            display: 'none',\n          },\n        },\n      }}\n    >\n      <Box\n        className=\"absolute left-0 h-full rounded-2xl transition-all\"\n        sx={(theme) => ({\n          backgroundColor: alpha(theme.vars.palette.primary.main, 0.3),\n          width: `${calcProgress(data) < 10 ? 10 : calcProgress(data)}%`,\n        })}\n      />\n\n      <div className=\"truncate capitalize\">{parsedState()}</div>\n\n      <div className=\"truncate\">\n        {calcProgress(data).toFixed(0)}%{''}\n        <span>({parseTraffic(data?.downloader.speed || 0)}/s)</span>\n      </div>\n    </Box>\n  )\n}\n\nexport interface ClashCoreItemProps {\n  selected: boolean\n  data: ClashCoresDetail\n  core: ClashCore\n  onClick: (core: ClashCore) => void\n}\n\n/**\n * @example\n * <ClashCoreItem\n    data={core}\n    selected={selected}\n    onClick={() => changeClashCore(item.core)}\n  />\n *\n * `Design for Clash Core used.`\n *\n * @author keiko233 <i@elaina.moe>\n * @copyright LibNyanpasu org. 2024\n */\nexport const ClashCoreItem = ({\n  selected,\n  data,\n  core,\n  onClick,\n}: ClashCoreItemProps) => {\n  const { t } = useTranslation()\n\n  const { query, updateCore } = useClashCores()\n\n  const haveNewVersion = data.latestVersion\n    ? data.latestVersion !== data.currentVersion\n    : false\n\n  const [downloadState, setDownloadState] = useState(false)\n\n  const [updater, setUpdater] = useState<InspectUpdater>()\n\n  const handleUpdateCore = async () => {\n    try {\n      setDownloadState(true)\n\n      const updaterId = await updateCore.mutateAsync(core)\n\n      if (!updaterId) {\n        throw new Error('Failed to update')\n      }\n\n      await new Promise<void>((resolve, reject) => {\n        const interval = setInterval(async () => {\n          const result = await inspectUpdater(updaterId)\n\n          setUpdater(result)\n\n          if (\n            isObject(result.downloader.state) &&\n            Object.prototype.hasOwnProperty.call(\n              result.downloader.state,\n              'failed',\n            )\n          ) {\n            reject(result.downloader.state.failed)\n            clearInterval(interval)\n          }\n\n          if (result.state === 'done') {\n            resolve()\n            clearInterval(interval)\n          }\n        }, 100)\n      })\n\n      await query.refetch()\n\n      message(t('Successfully updated the core', { core: `${data.name}` }), {\n        kind: 'info',\n        title: t('Successful'),\n      })\n    } catch (e) {\n      message(t('Failed to update', { error: `${formatError(e)}` }), {\n        kind: 'error',\n        title: t('Error'),\n      })\n    } finally {\n      setDownloadState(false)\n    }\n  }\n\n  return (\n    <ListItem sx={{ pl: 0, pr: 0 }}>\n      <ListItemButton\n        className=\"!relative !p-0\"\n        sx={(theme) => ({\n          borderRadius: '16px',\n          backgroundColor: alpha(theme.vars.palette.background.paper, 0.3),\n\n          '&.Mui-selected': {\n            backgroundColor: alpha(theme.vars.palette.primary.main, 0.3),\n          },\n        })}\n        selected={selected}\n        onClick={() => {\n          if (!downloadState) {\n            onClick(core)\n          }\n        }}\n      >\n        <CardProgress data={updater} show={downloadState} />\n\n        <div className=\"flex w-full items-center gap-2 p-4\">\n          <img style={{ width: '64px' }} src={getImage(core)} />\n\n          <div className=\"flex-1\">\n            <div className=\"truncate font-bold\">\n              {data.name}\n\n              {haveNewVersion && (\n                <FiberManualRecord\n                  sx={(theme) => ({\n                    height: 10,\n                    fill: theme.vars.palette.success.main,\n                  })}\n                />\n              )}\n            </div>\n\n            <div className=\"truncate text-sm\">{data.currentVersion}</div>\n\n            {haveNewVersion && (\n              <div className=\"truncate text-sm\">New: {data.latestVersion}</div>\n            )}\n          </div>\n\n          {haveNewVersion && (\n            <Tooltip title={t('Update Core')}>\n              <Button\n                variant=\"text\"\n                className=\"!size-8 !min-w-0\"\n                loading={downloadState}\n                onClick={(e) => {\n                  cleanDeepClickEvent(e)\n                  handleUpdateCore()\n                }}\n              >\n                <Update />\n              </Button>\n            </Tooltip>\n          )}\n        </div>\n      </ListItemButton>\n    </ListItem>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/setting/modules/clash-field.tsx",
    "content": "import { ChangeEvent, useState } from 'react'\nimport Marquee from 'react-fast-marquee'\nimport ArrowForwardIos from '@mui/icons-material/ArrowForwardIos'\nimport OpenInNewRounded from '@mui/icons-material/OpenInNewRounded'\nimport Box from '@mui/material/Box'\nimport ButtonBase, { ButtonBaseProps } from '@mui/material/ButtonBase'\nimport Grid from '@mui/material/Grid'\nimport IconButton from '@mui/material/IconButton'\nimport Paper from '@mui/material/Paper'\nimport { SwitchProps } from '@mui/material/Switch'\nimport Tooltip from '@mui/material/Tooltip'\nimport Typography from '@mui/material/Typography'\nimport { openThat } from '@nyanpasu/interface'\nimport { alpha, LoadingSwitch } from '@nyanpasu/ui'\n\nexport interface LabelSwitchProps extends SwitchProps {\n  label: string\n  url?: string\n  onChange?: (\n    event: ChangeEvent<HTMLInputElement>,\n    checked: boolean,\n  ) => Promise<void> | void\n}\n\n/**\n * @example\n * <LabelSwitch\n    label={label}\n    url={url}\n    checked={true}\n    onChange={(key) => console.log(key)}\n  />\n * `Design for Clash Filed use.`\n *\n * @author keiko233 <i@elaina.moe>\n * @copyright LibNyanpasu org. 2024\n */\nexport const LabelSwitch = ({\n  label,\n  url,\n  onChange,\n  ...props\n}: LabelSwitchProps) => {\n  const [loading, setLoading] = useState(false)\n\n  const handleChange = async (\n    event: ChangeEvent<HTMLInputElement>,\n    checked: boolean,\n  ) => {\n    if (onChange) {\n      try {\n        setLoading(true)\n\n        await onChange(event, checked)\n      } finally {\n        setLoading(false)\n      }\n    }\n  }\n\n  return (\n    <Paper\n      sx={(theme) => ({\n        display: 'flex',\n        alignItems: 'center',\n        justifyContent: 'space-between',\n        padding: 2,\n        borderRadius: 6,\n        backgroundColor: alpha(theme.vars.palette.primary.main, 0.1),\n      })}\n      elevation={0}\n    >\n      <Box display=\"flex\" alignItems=\"center\" gap={1}>\n        <Typography noWrap>{label}</Typography>\n\n        {url && (\n          <Tooltip title=\"What this field?\">\n            <IconButton size=\"small\" onClick={() => openThat(url)}>\n              <OpenInNewRounded sx={{ width: 16, height: 16 }} />\n            </IconButton>\n          </Tooltip>\n        )}\n      </Box>\n\n      {/* <Switch {...props} /> */}\n      <LoadingSwitch loading={loading} onChange={handleChange} {...props} />\n    </Paper>\n  )\n}\n\nexport interface ClashFieldItemProps extends ButtonBaseProps {\n  label: string\n  fields: string[]\n}\n\n/**\n * @example\n * <ClashFieldItem\n    label={label}\n    fields={string[]}\n    onClick={() => console.log(\"open\")}\n  />\n\n * `Design for Clash Filed use.`\n *\n * @author keiko233 <i@elaina.moe>\n * @copyright LibNyanpasu org. 2024\n */\nexport const ClashFieldItem = ({\n  label,\n  fields,\n  ...props\n}: ClashFieldItemProps) => {\n  return (\n    <Grid\n      size={{\n        xs: 6,\n        xl: 3,\n      }}\n    >\n      <Paper\n        elevation={0}\n        sx={(theme) => ({\n          borderRadius: 6,\n          backgroundColor: alpha(theme.vars.palette.primary.main, 0.1),\n        })}\n      >\n        <ButtonBase\n          sx={{\n            borderRadius: 6,\n            width: '100%',\n            textAlign: 'start',\n            padding: 2,\n            display: 'flex',\n            alignItems: 'center',\n            justifyContent: 'space-between',\n          }}\n          {...props}\n        >\n          <Box width=\"calc(100% - 8px)\">\n            <Typography\n              sx={{\n                textTransform: 'capitalize',\n                fontWeight: 700,\n              }}\n            >\n              {label}\n            </Typography>\n\n            <Marquee speed={36}>\n              <Box display=\"flex\" gap={1} sx={{ paddingRight: 16 }}>\n                <span>Enabled: </span>\n\n                {fields.map((item, index) => {\n                  return <span key={index}>{item}</span>\n                })}\n              </Box>\n            </Marquee>\n          </Box>\n\n          <ArrowForwardIos sx={{ width: 16, height: 16 }} />\n        </ButtonBase>\n      </Paper>\n    </Grid>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/setting/modules/clash-web.tsx",
    "content": "import { ReactElement, ReactNode } from 'react'\nimport Marquee from 'react-fast-marquee'\nimport DeleteRounded from '@mui/icons-material/DeleteRounded'\nimport EditRounded from '@mui/icons-material/EditRounded'\nimport OpenInNewRounded from '@mui/icons-material/OpenInNewRounded'\nimport Box from '@mui/material/Box'\nimport Chip from '@mui/material/Chip'\nimport IconButton from '@mui/material/IconButton'\nimport Paper, { PaperProps } from '@mui/material/Paper'\nimport { styled } from '@mui/material/styles'\nimport Typography from '@mui/material/Typography'\nimport { openThat } from '@nyanpasu/interface'\nimport { alpha } from '@nyanpasu/ui'\n\n/**\n * @example\n * renderChip(\"http://localhost?server=%host\", labels)\n *\n * @returns { (string | JSX.Element)[] }\n * (string | JSX.Element)[]\n *\n * `replace key string to Mui Chip.`\n *\n * @author keiko233 <i@elaina.moe>\n * @copyright LibNyanpasu org. 2024\n */\nexport const renderChip = (\n  string: string,\n  labels: {\n    [label: string]: string | number | undefined | null\n  },\n): (string | ReactElement)[] => {\n  return string.split(/(%[^&?]+)/).map((part, index) => {\n    if (part.startsWith('%')) {\n      const label = labels[part.replace('%', '')]\n\n      // TODO: may should return part string\n      if (!label) {\n        return ''\n      }\n\n      return (\n        <Chip\n          sx={{\n            '& .MuiChip-label': {\n              pl: 0.5,\n              pr: 0.5,\n            },\n          }}\n          key={index}\n          size=\"small\"\n          label={label}\n        />\n      )\n    } else {\n      return part\n    }\n  })\n}\n\n/**\n * @example\n * extractServer(\"127.0.0.1:7789\")\n *\n * @returns { { host: string; port: number } }\n * { host: \"127.0.0.1\"; port: 7789 }\n *\n * @author keiko233 <i@elaina.moe>\n * @copyright LibNyanpasu org. 2024\n */\nexport const extractServer = (\n  string?: string,\n): { host: string; port: number } => {\n  if (!string) {\n    // fallback default values\n    return { host: '127.0.0.1', port: 7890 }\n  } else {\n    const [host, port] = string.split(':')\n\n    return { host, port: Number(port) }\n  }\n}\n\n/**\n * @example\n * openWebUrl(\"http://localhost?server=%host\", labels)\n *\n * @returns { void }\n * void\n *\n * `open clash external web url with browser.`\n *\n * @author keiko233 <i@elaina.moe>\n * @copyright LibNyanpasu org. 2024\n */\nexport const openWebUrl = (\n  string: string,\n  labels: {\n    [label: string]: string | number | undefined | null\n  },\n): void => {\n  let url = ''\n\n  for (const key in labels) {\n    const regex = new RegExp(`%${key}`, 'g')\n\n    url = string.replace(regex, labels[key] as string)\n  }\n\n  openThat(url)\n}\n\n/**\n * @example\n * <Item>\n *  <Child />\n * </Item>\n *\n * `Material You list Item. Extend MuiPaper.`\n *\n * @author keiko233 <i@elaina.moe>\n * @copyright LibNyanpasu org. 2024\n */\nexport const Item = styled(Paper)<PaperProps>(({ theme }) => ({\n  backgroundColor: alpha(theme.vars.palette.primary.main, 0.1),\n  padding: 16,\n  borderRadius: 16,\n  display: 'flex',\n  flexDirection: 'column',\n  gap: 8,\n})) as typeof Paper\n\nexport interface ClashWebItemProps {\n  label: ReactNode\n  onOpen: () => void\n  onDelete: () => void\n  onEdit: () => void\n}\n\n/**\n * @example\n * <ClashWebItem\n    label={renderChip(item, labels)}\n    onOpen={() => openWebUrl(item, labels)}\n    onEdit={() => {\n      setEditString(item);\n      setOpen(true);\n    }}\n    onDelete={() => {}}\n  />\n  \n * `Clash Web UI list Item.`\n *\n * @author keiko233 <i@elaina.moe>\n * @copyright LibNyanpasu org. 2024\n */\nexport const ClashWebItem = ({\n  label,\n  onOpen,\n  onDelete,\n  onEdit,\n}: ClashWebItemProps) => {\n  return (\n    <Item>\n      <Marquee>\n        <Typography variant=\"subtitle1\" sx={{ marginRight: 16 }}>\n          {label}\n        </Typography>\n      </Marquee>\n\n      <Box display=\"flex\" justifyContent=\"end\" alignItems=\"center\" gap={1}>\n        <IconButton onClick={onOpen}>\n          <OpenInNewRounded />\n        </IconButton>\n\n        <IconButton onClick={onEdit}>\n          <EditRounded />\n        </IconButton>\n\n        <IconButton onClick={onDelete}>\n          <DeleteRounded />\n        </IconButton>\n      </Box>\n    </Item>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/setting/modules/hotkey-dialog.tsx",
    "content": "import { useLockFn } from 'ahooks'\nimport { useCallback, useEffect, useState } from 'react'\nimport { useTranslation } from 'react-i18next'\nimport { formatError } from '@/utils'\nimport { message } from '@/utils/notification'\nimport { Typography } from '@mui/material'\nimport { useSetting } from '@nyanpasu/interface'\nimport { BaseDialog, BaseDialogProps } from '@nyanpasu/ui'\nimport HotkeyInput from './hotkey-input'\n\nexport type HotkeyDialogProps = Omit<BaseDialogProps, 'title'>\n\nconst HOTKEY_FUNC = [\n  'open_or_close_dashboard',\n  'clash_mode_rule',\n  'clash_mode_global',\n  'clash_mode_direct',\n  'clash_mode_script',\n  'toggle_system_proxy',\n  // \"enable_system_proxy\",\n  // \"disable_system_proxy\",\n  'toggle_tun_mode',\n  // \"enable_tun_mode\",\n  // \"disable_tun_mode\",\n] as const\n\ntype AllowedHotkeyFunc = (typeof HOTKEY_FUNC)[number]\n\ntype Key = string\n\ntype HotKeyErrorMessages = {\n  [K in AllowedHotkeyFunc]: string | null\n}\n\ntype HotKeyLoading = {\n  [K in AllowedHotkeyFunc]: boolean\n}\n\ntype HotkeyMap = { [K in AllowedHotkeyFunc]: Key[] }\n\nexport default function HotkeyDialog({\n  open,\n  onClose,\n  children,\n  ...rest\n}: HotkeyDialogProps) {\n  const { t } = useTranslation()\n\n  // 检查是否有快捷键重复\n  const [duplicateItems, setDuplicateItems] = useState<string[]>([])\n\n  const { value, upsert } = useSetting('hotkeys')\n\n  const [hotkeyMap, setHotkeyMap] = useState<HotkeyMap>({} as HotkeyMap)\n\n  useEffect(() => {\n    if (open && Object.keys(hotkeyMap).length === 0) {\n      const map = {} as typeof hotkeyMap\n      value?.forEach((text) => {\n        const [func, key] = text.split(',').map((i) => i.trim())\n        if (!func || !key) return\n        map[func as AllowedHotkeyFunc] = key\n          .split('+')\n          .map((e) => e.trim())\n          .map((k) => (k === 'PLUS' ? '+' : k))\n      })\n      setHotkeyMap(map)\n      setDuplicateItems([])\n    }\n  }, [hotkeyMap, open, value])\n\n  const [errorMessages, setErrorMessages] = useState<HotKeyErrorMessages>(\n    HOTKEY_FUNC.reduce(\n      (acc, cur) => ({ ...acc, [cur]: null }),\n      {} as HotKeyErrorMessages,\n    ),\n  )\n\n  const [loading, setLoading] = useState<HotKeyLoading>(\n    HOTKEY_FUNC.reduce(\n      (acc, cur) => ({ ...acc, [cur]: false }),\n      {} as HotKeyLoading,\n    ),\n  )\n\n  const saveState = useLockFn(\n    async (func: AllowedHotkeyFunc, hotkeyMap: HotkeyMap) => {\n      const hotkeys = Object.entries(hotkeyMap)\n        .map(([func, keys]) => {\n          if (!func || !keys?.length) return ''\n\n          const key = keys\n            .map((k) => k.trim())\n            .filter(Boolean)\n            .map((k) => (k === '+' ? 'PLUS' : k))\n            .join('+')\n\n          if (!key) return ''\n          return `${func},${key}`\n        })\n        .filter(Boolean)\n\n      try {\n        await upsert(hotkeys)\n      } catch (err: unknown) {\n        setErrorMessages((prev) => ({\n          ...prev,\n          [func]: formatError(err),\n        }))\n        await message(formatError(err), {\n          kind: 'error',\n        })\n      }\n    },\n  )\n\n  const onBlurCb = useCallback(\n    (e: React.FocusEvent<HTMLInputElement>, func: string) => {\n      const keys = Object.values(hotkeyMap).flat().filter(Boolean)\n      const set = new Set(keys)\n      if (keys.length !== set.size) {\n        setDuplicateItems([...duplicateItems, func])\n        return\n      } else {\n        setDuplicateItems(duplicateItems.filter((e) => e !== func))\n      }\n\n      setLoading((prev) => ({ ...prev, [func]: true }))\n\n      saveState(func as AllowedHotkeyFunc, hotkeyMap)\n        .catch(() => {\n          setDuplicateItems([...duplicateItems, func])\n        })\n        .finally(() => {\n          setLoading((prev) => ({ ...prev, [func]: false }))\n        })\n    },\n    [duplicateItems, hotkeyMap, saveState],\n  )\n\n  return (\n    <BaseDialog\n      title={t('Hotkey Setting')}\n      open={open}\n      onClose={onClose}\n      {...rest}\n    >\n      {children}\n      <div className=\"grid-1 grid gap-3\">\n        {HOTKEY_FUNC.map((func) => (\n          <div className=\"flex items-center justify-between px-2\" key={func}>\n            <Typography>{t(func)}</Typography>\n            <HotkeyInput\n              func={func}\n              isDuplicate={\n                duplicateItems.includes(func) || !!errorMessages[func]\n              }\n              onBlurCb={onBlurCb}\n              loading={loading[func]}\n              value={hotkeyMap[func] ?? []}\n              onValueChange={(v) =>\n                setHotkeyMap((prev) => ({ ...prev, [func]: v }))\n              }\n            />\n          </div>\n        ))}\n      </div>\n    </BaseDialog>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/setting/modules/hotkey-input.module.d.scss.ts",
    "content": "declare const classNames: {\n  readonly wrapper: 'wrapper'\n  readonly input: 'input'\n  readonly items: 'items'\n}\nexport default classNames\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/setting/modules/hotkey-input.module.scss",
    "content": ".wrapper {\n  .input {\n    &:hover {\n      + .items {\n        border-color: var(--input-hover-border-color);\n      }\n    }\n\n    &:focus {\n      + .items {\n        border-color: var(--input-focus-border-color);\n        border-width: 2px;\n      }\n    }\n  }\n\n  .items {\n    border-color: var(--border-color);\n  }\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/setting/modules/hotkey-input.module.scss.d.ts",
    "content": "declare const classNames: {\n  readonly wrapper: 'wrapper'\n  readonly input: 'input'\n  readonly items: 'items'\n}\nexport default classNames\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/setting/modules/hotkey-input.tsx",
    "content": "import { parseHotkey } from '@/utils/parse-hotkey'\nimport { Dangerous, DeleteRounded } from '@mui/icons-material'\nimport { CircularProgress, IconButton, useTheme } from '@mui/material'\nimport type {} from '@mui/material/themeCssVarsAugmentation'\nimport { CSSProperties, useEffect, useRef, useState } from 'react'\nimport { alpha, cn, Kbd } from '@nyanpasu/ui'\nimport styles from './hotkey-input.module.scss'\n\nexport interface Props extends React.HTMLAttributes<HTMLInputElement> {\n  isDuplicate?: boolean\n  value?: string[]\n  onValueChange?: (value: string[]) => void\n  func: string\n  onBlurCb?: (e: React.FocusEvent<HTMLInputElement>, func: string) => void\n  loading?: boolean\n}\n\nexport default function HotkeyInput({\n  isDuplicate = false,\n  value,\n  func,\n  onValueChange,\n  onBlurCb,\n  // native\n  className,\n  loading,\n  ...rest\n}: Props) {\n  const theme = useTheme()\n\n  const changeRef = useRef<string[]>([])\n  const [keys, setKeys] = useState(value || [])\n  const [isClearing, setIsClearing] = useState(false)\n\n  useEffect(() => {\n    if (isClearing) {\n      onBlurCb?.({} as React.FocusEvent<HTMLInputElement>, func)\n      setIsClearing(false)\n    }\n  }, [func, isClearing, onBlurCb])\n\n  return (\n    <div className=\"flex items-center gap-2\">\n      <div className={cn('relative min-h-[36px] w-[165px]', styles.wrapper)}>\n        <input\n          className={cn(\n            'absolute top-0 left-0 z-[1] h-full w-full opacity-0',\n            styles.input,\n            className,\n          )}\n          onKeyUp={() => {\n            const ret = changeRef.current.slice()\n            if (ret.length) {\n              onValueChange?.(ret)\n              changeRef.current = []\n            }\n          }}\n          onKeyDown={(e) => {\n            const evt = e.nativeEvent\n            e.preventDefault()\n            e.stopPropagation()\n            const key = parseHotkey(evt.key)\n            if (key === 'UNIDENTIFIED') return\n\n            changeRef.current = [...new Set([...changeRef.current, key])]\n            setKeys(changeRef.current)\n          }}\n          onBlur={(e) => {\n            onBlurCb?.(e, func)\n          }}\n          {...rest}\n        />\n        <div\n          className={cn(\n            'box-border flex h-full min-h-[36px] w-full flex-wrap items-center rounded border border-solid px-1 py-1 last:mr-0',\n            styles.items,\n          )}\n          style={\n            {\n              '--border-color': isDuplicate\n                ? theme.vars.palette.error.main\n                : alpha(theme.vars.palette.text.secondary, 0.15),\n              '--input-focus-border-color': alpha(\n                theme.vars.palette.primary.main,\n                0.75,\n              ),\n              '--input-hover-border-color': `rgba(${theme.vars.palette.common.background} / 0.23)`,\n            } as CSSProperties\n          }\n        >\n          {keys.map((key) => (\n            <Kbd className=\"scale-75\" key={key}>\n              {key}\n            </Kbd>\n          ))}\n          {loading && (\n            <CircularProgress className=\"absolute right-2\" size={13} />\n          )}\n          {isDuplicate && (\n            <Dangerous\n              className=\"absolute right-2 text-base\"\n              sx={[\n                (theme) => ({\n                  color: theme.vars.palette.error.main,\n                }),\n              ]}\n            />\n          )}\n        </div>\n      </div>\n\n      <IconButton\n        size=\"small\"\n        title=\"Delete\"\n        color=\"inherit\"\n        onClick={() => {\n          onValueChange?.([])\n          setKeys([])\n          setIsClearing(true)\n        }}\n      >\n        <DeleteRounded fontSize=\"inherit\" />\n      </IconButton>\n    </div>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/setting/modules/index.ts",
    "content": "export * from './clash-web'\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/setting/modules/nyanpasu-path.tsx",
    "content": "import { memo, ReactNode } from 'react'\nimport { mergeSxProps } from '@/utils/mui-theme'\nimport {\n  ButtonBase,\n  ButtonBaseProps,\n  Paper,\n  SxProps,\n  Theme,\n  Typography,\n} from '@mui/material'\nimport { alpha } from '@nyanpasu/ui'\n\nexport interface PaperButtonProps extends ButtonBaseProps {\n  label?: string\n  children?: ReactNode\n  sxPaper?: SxProps<Theme>\n  sxButton?: SxProps<Theme>\n}\n\nexport const PaperButton = memo(function PaperButton({\n  label,\n  children,\n  sxPaper,\n  sxButton,\n  ...props\n}: PaperButtonProps) {\n  return (\n    <Paper\n      elevation={0}\n      sx={mergeSxProps(\n        (theme: Theme) => ({\n          borderRadius: 6,\n          backgroundColor: alpha(theme.vars.palette.primary.main, 0.1),\n        }),\n        sxPaper,\n      )}\n    >\n      <ButtonBase\n        sx={mergeSxProps(\n          {\n            borderRadius: 6,\n            width: '100%',\n            textAlign: 'start',\n            padding: 2,\n            display: 'flex',\n            alignItems: 'center',\n            justifyContent: 'space-between',\n\n            '&.Mui-disabled': {\n              pointerEvents: 'auto',\n              cursor: 'not-allowed',\n            },\n          },\n          sxButton,\n        )}\n        {...props}\n      >\n        {label && (\n          <Typography\n            noWrap\n            component=\"p\"\n            width=\"100%\"\n            sx={{\n              fontWeight: 700,\n              textOverflow: 'ellipsis',\n              overflow: 'hidden',\n            }}\n          >\n            {label}\n          </Typography>\n        )}\n\n        {children}\n      </ButtonBase>\n    </Paper>\n  )\n})\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/setting/modules/service-manual-prompt-dialog.module.d.scss.ts",
    "content": "declare const classNames: {\n  readonly prompt: 'prompt'\n  readonly shiki: 'shiki'\n  readonly dark: 'dark'\n}\nexport default classNames\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/setting/modules/service-manual-prompt-dialog.module.scss",
    "content": ".prompt {\n  :global(.shiki) {\n    width: 100%;\n    padding: 16px;\n    overflow-x: auto;\n    user-select: text;\n    border-radius: 4px;\n  }\n}\n\n.dark {\n  &.prompt {\n    :global(.shiki),\n    :global(.shiki span) {\n      /* Optional, if you also want font styles */\n      font-style: var(--shiki-dark-font-style) !important;\n      font-weight: var(--shiki-dark-font-weight) !important;\n      color: var(--shiki-dark) !important;\n      text-decoration: var(--shiki-dark-text-decoration) !important;\n      background-color: var(--shiki-dark-bg) !important;\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/setting/modules/service-manual-prompt-dialog.module.scss.d.ts",
    "content": "declare const classNames: {\n  readonly prompt: 'prompt'\n  readonly shiki: 'shiki'\n  readonly dark: 'dark'\n}\nexport default classNames\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/setting/modules/service-manual-prompt-dialog.tsx",
    "content": "import { useAsyncEffect } from 'ahooks'\nimport { useAtom, useSetAtom } from 'jotai'\nimport { useCallback, useMemo, useState } from 'react'\nimport { useTranslation } from 'react-i18next'\nimport useSWR from 'swr'\nimport { OS } from '@/consts'\nimport { serviceManualPromptDialogAtom } from '@/store/service'\nimport { notification } from '@/utils/notification'\nimport { getShikiSingleton } from '@/utils/shiki'\nimport ContentPasteIcon from '@mui/icons-material/ContentPaste'\nimport { IconButton, Tooltip } from '@mui/material'\nimport { useColorScheme } from '@mui/material/styles'\nimport { getCoreDir, getServiceInstallPrompt } from '@nyanpasu/interface'\nimport { BaseDialog, BaseDialogProps, cn } from '@nyanpasu/ui'\nimport styles from './service-manual-prompt-dialog.module.scss'\n\ntype CopyToClipboardButtonProps = {\n  onClick: () => void\n}\n\nfunction CopyToClipboardButton({ onClick }: CopyToClipboardButtonProps) {\n  const { t } = useTranslation()\n  return (\n    <Tooltip\n      title={t('Copy to clipboard')}\n      placement=\"top\"\n      slotProps={{\n        popper: {\n          modifiers: [\n            {\n              name: 'offset',\n              options: {\n                offset: [0, -8],\n              },\n            },\n          ],\n        },\n      }}\n    >\n      <IconButton\n        size=\"small\"\n        className=\"!absolute top-1 right-1\"\n        onClick={onClick}\n      >\n        <ContentPasteIcon fontSize=\"small\" color=\"primary\" />\n      </IconButton>\n    </Tooltip>\n  )\n}\n\nexport type ServerManualPromptDialogProps = Omit<BaseDialogProps, 'title'> & {\n  operation: 'uninstall' | 'install' | 'start' | 'stop' | null\n}\n\n// TODO: maybe support more commands prompt?\nexport default function ServerManualPromptDialog({\n  open,\n  onClose,\n  operation,\n  ...props\n}: ServerManualPromptDialogProps) {\n  const { t } = useTranslation()\n  const { mode } = useColorScheme()\n  const { data: serviceInstallPrompt, error } = useSWR(\n    operation === 'install' ? '/service_install_prompt' : null,\n    getServiceInstallPrompt,\n  )\n  const { data: coreDir } = useSWR('/core_dir', () => getCoreDir())\n  const commands = useMemo(() => {\n    if (operation === 'install' && serviceInstallPrompt) {\n      return `cd \"${coreDir}\"\\n${serviceInstallPrompt}`\n    } else if (operation) {\n      return `cd \"${coreDir}\"\\n${OS !== 'windows' ? 'sudo ' : ''}./nyanpasu-service ${operation}`\n    }\n    return ''\n  }, [operation, serviceInstallPrompt, coreDir])\n  const [codes, setCodes] = useState<string | null>(null)\n\n  useAsyncEffect(async () => {\n    const shiki = await getShikiSingleton()\n    const code = await shiki.codeToHtml(commands, {\n      lang: 'shell',\n      themes: {\n        dark: 'nord',\n        light: 'min-light',\n      },\n    })\n    setCodes(code)\n  }, [serviceInstallPrompt, operation, coreDir, commands])\n\n  const handleCopyToClipboard = useCallback(() => {\n    if (commands) {\n      const item = new ClipboardItem({\n        'text/plain': new Blob([commands], { type: 'text/plain' }),\n      })\n      navigator.clipboard\n        .write([item])\n        .then(() => {\n          console.log('copied')\n          notification({\n            title: `Clash Nyanpasu - ${t('Service Manual Tips')}`,\n            body: t('Copied to clipboard'),\n          })\n        })\n        .catch((error) => {\n          console.error(error)\n          notification({\n            title: `Clash Nyanpasu - ${t('Service Manual Tips')}`,\n            body: t('Failed to copy to clipboard'),\n          })\n        })\n    }\n  }, [commands, t])\n\n  return (\n    <BaseDialog\n      title={t('Service Manual Tips')}\n      open={open}\n      onClose={onClose}\n      {...props}\n    >\n      <div className=\"grid gap-3\">\n        <p>\n          {t('Unable to operation the service automatically', {\n            operation: t(`${operation}`),\n          })}\n        </p>\n        {error && <p className=\"text-red-500\">{error.message}</p>}\n        {!!codes && (\n          <div className=\"relative\">\n            <div\n              className={cn(\n                'rounded-sm md:max-w-[80vw] lg:max-w-[60vw] xl:max-w-[50vw]',\n                mode === 'dark' && styles.dark,\n                styles.prompt,\n              )}\n              dangerouslySetInnerHTML={{\n                __html: codes,\n              }}\n            />\n            <CopyToClipboardButton onClick={handleCopyToClipboard} />\n          </div>\n        )}\n      </div>\n    </BaseDialog>\n  )\n}\n\nexport function ServerManualPromptDialogWrapper() {\n  const [prompt, setPrompt] = useAtom(serviceManualPromptDialogAtom)\n  return (\n    <ServerManualPromptDialog\n      open={!!prompt}\n      onClose={() => setPrompt(null)}\n      operation={prompt}\n    />\n  )\n}\n\nexport function useServerManualPromptDialog() {\n  const setPrompt = useSetAtom(serviceManualPromptDialogAtom)\n  return {\n    show: (prompt: 'install' | 'uninstall' | 'stop' | 'start') =>\n      setPrompt(prompt),\n    close: () => setPrompt(null),\n  }\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/setting/modules/system-proxy.tsx",
    "content": "import { useControllableValue } from 'ahooks'\nimport { memo, ReactNode } from 'react'\nimport { mergeSxProps } from '@/utils/mui-theme'\nimport { CircularProgress } from '@mui/material'\nimport type { SxProps, Theme } from '@mui/material/styles'\nimport { alpha } from '@nyanpasu/ui'\nimport { PaperButton, PaperButtonProps } from './nyanpasu-path'\n\nexport interface PaperSwitchButtonProps extends PaperButtonProps {\n  label?: string\n  checked: boolean\n  loading?: boolean\n  disableLoading?: boolean\n  children?: ReactNode\n  onClick?: () => Promise<void> | void\n  sxPaper?: SxProps<Theme>\n}\n\nexport const PaperSwitchButton = memo(function PaperSwitchButton({\n  label,\n  checked,\n  loading,\n  disableLoading,\n  children,\n  onClick,\n  sxPaper,\n  ...props\n}: PaperSwitchButtonProps) {\n  const [pending, setPending] = useControllableValue<boolean>(\n    { loading },\n    {\n      defaultValue: false,\n    },\n  )\n\n  const handleClick = async () => {\n    if (onClick) {\n      if (disableLoading) {\n        return onClick()\n      }\n\n      setPending(true)\n      await onClick()\n      setPending(false)\n    }\n  }\n\n  return (\n    <PaperButton\n      label={label}\n      sxPaper={mergeSxProps(\n        ((theme) => ({\n          backgroundColor: checked\n            ? alpha(theme.vars.palette.primary.main, 0.1)\n            : theme.vars.palette.grey[100],\n          ...theme.applyStyles('dark', {\n            backgroundColor: checked\n              ? alpha(theme.vars.palette.primary.main, 0.1)\n              : theme.vars.palette.common.black,\n          }),\n        })) as SxProps<Theme>,\n        sxPaper,\n      )}\n      sxButton={{\n        flexDirection: 'column',\n        alignItems: 'start',\n        gap: 0.5,\n      }}\n      onClick={handleClick}\n      {...props}\n    >\n      {pending === true && (\n        <CircularProgress\n          sx={{\n            position: 'absolute',\n            bottom: 'calc(50% - 12px)',\n            right: 12,\n          }}\n          color=\"inherit\"\n          size={24}\n        />\n      )}\n\n      {children}\n    </PaperButton>\n  )\n})\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/setting/modules/tray-icon-dialog.tsx",
    "content": "import { useMemoizedFn } from 'ahooks'\nimport { useState, useTransition } from 'react'\nimport { useTranslation } from 'react-i18next'\nimport useSWR from 'swr'\nimport { formatError, sleep } from '@/utils'\nimport { message } from '@/utils/notification'\nimport { Button } from '@mui/material'\nimport {\n  getServerPort,\n  isTrayIconSet,\n  setTrayIcon as setTrayIconCall,\n} from '@nyanpasu/interface'\nimport { BaseDialog, BaseDialogProps } from '@nyanpasu/ui'\nimport { open } from '@tauri-apps/plugin-dialog'\n\nfunction TrayIconItem({ mode }: { mode: 'system_proxy' | 'tun' | 'normal' }) {\n  const { t } = useTranslation()\n  const [ts, setTs] = useState(Date.now())\n  const {\n    data: isSetTrayIcon,\n    isLoading,\n    mutate,\n  } = useSWR('/isSetTrayIcon?mode=' + mode, () => isTrayIconSet(mode), {\n    revalidateOnFocus: true,\n  })\n  const { data: serverPort } = useSWR('/getServerPort', getServerPort)\n  const src = `http://localhost:${serverPort}/tray/icon?mode=${mode}&ts=${ts}`\n  const [loading, startTransition] = useTransition()\n  const selectImage = async () => {\n    const selected = await open({\n      directory: false,\n      multiple: false,\n      filters: [\n        { name: 'Images', extensions: ['png', 'jpg', 'jpeg', 'bmp', 'ico'] },\n      ],\n    })\n    if (Array.isArray(selected)) {\n      throw new Error('Not Support')\n    } else if (selected === null) {\n      return null\n    } else {\n      return selected\n    }\n  }\n\n  const setTrayIcon = useMemoizedFn((reset?: boolean) => {\n    startTransition(async () => {\n      try {\n        const selected = reset ? undefined : await selectImage()\n        if (selected === null) {\n          return\n        }\n        return await setTrayIconCall(mode, selected)\n      } catch (e) {\n        message(formatError(e), {\n          kind: 'error',\n        })\n      } finally {\n        setTs(Date.now())\n        await sleep(2000)\n        await mutate()\n      }\n    })\n  })\n\n  return (\n    <div className=\"flex items-center justify-between\">\n      <div className=\"flex items-center gap-3\">\n        <img className=\"h-14 w-14\" src={src} draggable={false} loading=\"lazy\" />\n        <span className=\"text-base font-semibold\">{t(mode)}</span>\n      </div>\n      <span>\n        {isSetTrayIcon ? (\n          <div className=\"flex gap-3\">\n            <Button\n              variant=\"contained\"\n              loading={isLoading || loading}\n              disabled={loading || isLoading}\n              onClick={() => setTrayIcon()}\n            >\n              {t('Edit')}\n            </Button>\n            <Button\n              variant=\"contained\"\n              loading={isLoading || loading}\n              disabled={loading || isLoading}\n              onClick={() => setTrayIcon(true)}\n            >\n              {t('Reset')}\n            </Button>\n          </div>\n        ) : (\n          <Button\n            variant=\"contained\"\n            loading={isLoading || loading}\n            disabled={loading || isLoading}\n            onClick={() => setTrayIcon()}\n          >\n            {t('Set')}\n          </Button>\n        )}\n      </span>\n    </div>\n  )\n}\n\nexport type TrayIconDialogProps = Omit<BaseDialogProps, 'title'>\n\nexport default function TrayIconDialog({\n  open,\n  onClose,\n  ...props\n}: TrayIconDialogProps) {\n  const { t } = useTranslation()\n  return (\n    <BaseDialog\n      title={t('Tray Icons')}\n      open={open}\n      onClose={onClose}\n      {...props}\n    >\n      <div className=\"grid gap-3\">\n        <TrayIconItem mode=\"normal\" />\n        <TrayIconItem mode=\"tun\" />\n        <TrayIconItem mode=\"system_proxy\" />\n      </div>\n    </BaseDialog>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/setting/setting-clash-base.tsx",
    "content": "import { useMemo } from 'react'\nimport { useTranslation } from 'react-i18next'\nimport { useCoreType } from '@/hooks/use-store'\nimport { formatError } from '@/utils'\nimport getSystem from '@/utils/get-system'\nimport { message } from '@/utils/notification'\nimport { Button, List, ListItem, ListItemText } from '@mui/material'\nimport {\n  openUWPTool,\n  useClashConfig,\n  useRuntimeProfile,\n  useSetting,\n  type TunStack as TunStackType,\n} from '@nyanpasu/interface'\nimport { BaseCard, MenuItem, SwitchItem } from '@nyanpasu/ui'\n\nconst isWIN = getSystem() === 'windows'\n\nconst AllowLan = () => {\n  const { t } = useTranslation()\n\n  const { query, upsert } = useClashConfig()\n\n  const value = useMemo(() => query.data?.['allow-lan'], [query.data])\n\n  return (\n    <SwitchItem\n      label={t('Allow LAN')}\n      checked={value}\n      onChange={async () => {\n        await upsert.mutateAsync({\n          'allow-lan': !value,\n        })\n      }}\n    />\n  )\n}\n\nconst IPv6 = () => {\n  const { t } = useTranslation()\n\n  const { query, upsert } = useClashConfig()\n\n  const value = useMemo(() => query.data?.['ipv6'], [query.data])\n\n  return (\n    <SwitchItem\n      label={t('IPv6')}\n      checked={value}\n      onChange={async () => {\n        await upsert.mutateAsync({\n          ipv6: !value,\n        })\n      }}\n    />\n  )\n}\n\nconst TunStack = () => {\n  const { t } = useTranslation()\n\n  const [coreType] = useCoreType()\n\n  const { value, upsert: upsertTunStack } = useSetting('tun_stack')\n\n  const { value: enableTun, upsert: upsertTun } = useSetting('enable_tun_mode')\n\n  const runtimeProfile = useRuntimeProfile()\n\n  const tunStackOptions = useMemo(() => {\n    const options: {\n      [key: string]: string\n    } = {\n      system: 'System',\n      gvisor: 'gVisor',\n      mixed: 'Mixed',\n    }\n\n    // clash not support mixed\n    if (coreType === 'clash') {\n      delete options.mixed\n    }\n    return options\n  }, [coreType])\n\n  const selected = useMemo(() => {\n    const stack = value || 'gvisor'\n    return stack in tunStackOptions ? stack : 'gvisor'\n  }, [tunStackOptions, value])\n\n  return (\n    <MenuItem\n      label={t('TUN Stack')}\n      options={tunStackOptions}\n      selected={selected}\n      onSelected={async (value) => {\n        try {\n          await upsertTunStack(value as TunStackType)\n\n          if (enableTun) {\n            // just to reload clash config\n            await upsertTun(true)\n          }\n\n          // need manual mutate to refetch runtime profile\n          await runtimeProfile.refetch()\n        } catch (error) {\n          message(`Change Tun Stack failed ! \\n Error: ${formatError(error)}`, {\n            title: t('Error'),\n            kind: 'error',\n          })\n        }\n      }}\n    />\n  )\n}\n\nconst LogLevel = () => {\n  const { t } = useTranslation()\n\n  const { query, upsert } = useClashConfig()\n\n  const options = {\n    debug: 'Debug',\n    info: 'Info',\n    warning: 'Warn',\n    error: 'Error',\n    silent: 'Silent',\n  }\n\n  const value = useMemo(() => query.data?.['log-level'], [query.data])\n\n  return (\n    <MenuItem\n      label={t('Log Level')}\n      options={options}\n      selected={value ?? 'debug'}\n      onSelected={async (value) => {\n        await upsert.mutateAsync({\n          'log-level': value as string,\n        })\n      }}\n    />\n  )\n}\n\nconst UWPTool = () => {\n  const { t } = useTranslation()\n\n  const handleClick = async () => {\n    try {\n      await openUWPTool()\n    } catch (e) {\n      message(`Failed to Open UWP Tools.\\n${JSON.stringify(e)}`, {\n        title: t('Error'),\n        kind: 'error',\n      })\n    }\n  }\n\n  return (\n    <ListItem sx={{ pl: 0, pr: 0 }}>\n      <ListItemText primary={t('Open UWP Tool')} />\n\n      <Button variant=\"contained\" onClick={handleClick}>\n        {t('Open')}\n      </Button>\n    </ListItem>\n  )\n}\n\nexport const SettingClashBase = () => {\n  const { t } = useTranslation()\n\n  const [coreType] = useCoreType()\n\n  return (\n    <BaseCard label={t('Clash Setting')}>\n      <List disablePadding>\n        <AllowLan />\n\n        <IPv6 />\n\n        {coreType !== 'clash-rs' && <TunStack />}\n\n        <LogLevel />\n\n        {isWIN && <UWPTool />}\n      </List>\n    </BaseCard>\n  )\n}\n\nexport default SettingClashBase\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/setting/setting-clash-core.tsx",
    "content": "import { useLockFn, useReactive } from 'ahooks'\nimport { motion } from 'framer-motion'\nimport { useMemo, useState } from 'react'\nimport { useTranslation } from 'react-i18next'\nimport { OS } from '@/consts'\nimport { formatError } from '@/utils'\nimport { message } from '@/utils/notification'\nimport { Box, Button, List, ListItem } from '@mui/material'\nimport {\n  ClashCore,\n  ClashCores,\n  useClashConnections,\n  useClashCores,\n  useClashVersion,\n  useSetting,\n} from '@nyanpasu/interface'\nimport { BaseCard, ExpandMore, LoadingButton } from '@nyanpasu/ui'\nimport { ClashCoreItem } from './modules/clash-core'\n\nexport const SettingClashCore = () => {\n  const { t } = useTranslation()\n\n  const loading = useReactive({\n    mask: false,\n  })\n\n  const [expand, setExpand] = useState(false)\n\n  const { value: currentCore } = useSetting('clash_core')\n\n  const {\n    query: clashCores,\n    upsert: switchCore,\n    restartSidecar,\n    fetchRemote,\n  } = useClashCores()\n\n  const { data: clashVersion } = useClashVersion()\n\n  const { deleteConnections } = useClashConnections()\n\n  const version = useMemo(() => {\n    return clashVersion?.premium\n      ? `${clashVersion.version} Premium`\n      : clashVersion?.meta\n        ? `${clashVersion.version} Meta`\n        : clashVersion?.version || '-'\n  }, [clashVersion])\n\n  const changeClashCore = useLockFn(async (core: ClashCore) => {\n    try {\n      loading.mask = true\n      try {\n        await deleteConnections.mutateAsync(undefined)\n      } catch (e) {\n        console.error(e)\n      }\n\n      await switchCore.mutateAsync(core)\n\n      message(\n        t('Successfully switched to the clash core', {\n          core: ClashCores[core],\n        }),\n        {\n          kind: 'info',\n          title: t('Successful'),\n        },\n      )\n    } catch (e) {\n      message(\n        t('Failed to switch. You could see the details in the log', {\n          error: `${e instanceof Error ? e.message : String(e)}`,\n        }),\n        {\n          kind: 'error',\n          title: t('Error'),\n        },\n      )\n    } finally {\n      loading.mask = false\n    }\n  })\n\n  const handleRestart = async () => {\n    try {\n      await restartSidecar()\n\n      message(t('Successfully restarted the core'), {\n        kind: 'info',\n        title: t('Successful'),\n      })\n    } catch (e) {\n      message(\n        t('Failed to restart. You could see the details in the log') +\n          formatError(e),\n        {\n          kind: 'error',\n          title: t('Error'),\n        },\n      )\n    }\n  }\n\n  const handleCheckUpdates = async () => {\n    try {\n      await fetchRemote.mutateAsync()\n    } catch (e) {\n      message(\n        t('Failed to fetch. Please check your network connection') +\n          '\\n' +\n          formatError(e),\n        {\n          kind: 'error',\n          title: t('Error'),\n        },\n      )\n    }\n  }\n\n  return (\n    <BaseCard\n      label={t('Clash Core')}\n      loading={loading.mask}\n      labelChildren={<span>{version}</span>}\n    >\n      <List disablePadding>\n        {clashCores.data &&\n          Object.entries(clashCores.data).map(([core, item]) => {\n            const show = expand || core === currentCore\n\n            return (\n              <motion.div\n                key={item.name}\n                animate={show ? 'open' : 'closed'}\n                variants={{\n                  open: {\n                    height: 'auto',\n                    opacity: 1,\n                    scale: 1,\n                  },\n                  closed: {\n                    height: 0,\n                    opacity: 0,\n                    scale: 0.7,\n                  },\n                }}\n                transition={{\n                  type: 'spring',\n                  bounce: 0,\n                  duration: 0.35,\n                }}\n              >\n                <ClashCoreItem\n                  data={item}\n                  core={core as ClashCore}\n                  selected={core === currentCore}\n                  onClick={() => changeClashCore(core as ClashCore)}\n                />\n              </motion.div>\n            )\n          })}\n\n        <ListItem\n          sx={{\n            pl: 0,\n            pr: 0,\n            alignItems: 'center',\n            justifyContent: 'space-between',\n          }}\n        >\n          <Box display=\"flex\" gap={1}>\n            <Button variant=\"outlined\" onClick={handleRestart}>\n              {t('Restart')}\n            </Button>\n\n            {/** TODO: Support Linux when Manifest v2 released */}\n            {OS !== 'linux' && (\n              <LoadingButton\n                variant=\"contained\"\n                loading={fetchRemote.isPending}\n                onClick={handleCheckUpdates}\n              >\n                {t('Check Updates')}\n              </LoadingButton>\n            )}\n          </Box>\n\n          <ExpandMore expand={expand} onClick={() => setExpand(!expand)} />\n        </ListItem>\n      </List>\n    </BaseCard>\n  )\n}\n\nexport default SettingClashCore\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/setting/setting-clash-external.tsx",
    "content": "import { useEffect, useMemo, useState } from 'react'\nimport { useTranslation } from 'react-i18next'\nimport { sleep } from '@/utils'\nimport Done from '@mui/icons-material/Done'\nimport { Button, List, ListItem, ListItemText, TextField } from '@mui/material'\nimport {\n  ExternalControllerPortStrategy,\n  useClashConfig,\n  useClashInfo,\n  useRuntimeProfile,\n  useSetting,\n} from '@nyanpasu/interface'\nimport { BaseCard, Expand, MenuItem, TextItemProps } from '@nyanpasu/ui'\n\nconst TextItem = ({\n  value,\n  label,\n  onApply,\n  applyLabel,\n  placeholder,\n}: TextItemProps) => {\n  const { t } = useTranslation()\n\n  const [textString, setTextString] = useState(value)\n\n  useEffect(() => {\n    setTextString(value)\n  }, [value])\n\n  return (\n    <>\n      <ListItem sx={{ pl: 0, pr: 0 }}>\n        <ListItemText primary={label} />\n\n        <TextField\n          value={textString}\n          onChange={(e) => setTextString(e.target.value)}\n          placeholder={placeholder}\n          size=\"small\"\n          variant=\"outlined\"\n          sx={{ width: 160 }}\n          inputProps={{\n            'aria-autocomplete': 'none',\n          }}\n        />\n      </ListItem>\n\n      <Expand open={textString !== value}>\n        <div className=\"flex justify-end\">\n          <Button\n            variant=\"contained\"\n            startIcon={<Done />}\n            onClick={() => onApply(textString)}\n          >\n            {applyLabel ?? t('Apply')}\n          </Button>\n        </div>\n      </Expand>\n    </>\n  )\n}\n\nconst ExternalController = () => {\n  const { t } = useTranslation()\n\n  const { data, refetch } = useClashInfo()\n\n  const { upsert } = useClashConfig()\n\n  const runtimeProfile = useRuntimeProfile()\n\n  return (\n    <TextItem\n      label={t('External Controller')}\n      value={data?.server || ''}\n      onApply={async (value) => {\n        await upsert.mutateAsync({ 'external-controller': value })\n        await refetch()\n\n        // Wait for the server to apply\n        await sleep(300)\n        await runtimeProfile.refetch()\n      }}\n    />\n  )\n}\n\nconst PortStrategy = () => {\n  const { t } = useTranslation()\n\n  const portStrategyOptions = {\n    allow_fallback: t('Allow Fallback'),\n    fixed: t('Fixed'),\n    random: t('Random'),\n  }\n\n  const { value, upsert } = useSetting('clash_strategy')\n\n  const selected = useMemo(\n    () => value?.external_controller_port_strategy || 'allow_fallback',\n    [value],\n  )\n\n  return (\n    <MenuItem\n      label={t('Port Strategy')}\n      options={portStrategyOptions}\n      selected={selected}\n      onSelected={async (value) => {\n        await upsert({\n          external_controller_port_strategy:\n            value as ExternalControllerPortStrategy,\n        })\n      }}\n      selectSx={{ width: 160 }}\n    />\n  )\n}\n\nconst CoreSecret = () => {\n  const { t } = useTranslation()\n\n  const { data, refetch } = useClashInfo()\n\n  const { upsert } = useClashConfig()\n\n  return (\n    <TextItem\n      label={t('Core Secret')}\n      value={data?.secret || ''}\n      onApply={async (value) => {\n        await upsert.mutateAsync({ secret: value })\n        await refetch()\n      }}\n    />\n  )\n}\n\nexport const SettingClashExternal = () => {\n  const { t } = useTranslation()\n\n  return (\n    <BaseCard label={t('Clash External Controll')}>\n      <List disablePadding>\n        <ExternalController />\n\n        <PortStrategy />\n\n        <CoreSecret />\n      </List>\n    </BaseCard>\n  )\n}\n\nexport default SettingClashExternal\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/setting/setting-clash-field.tsx",
    "content": "import { useMemo, useState } from 'react'\nimport { useTranslation } from 'react-i18next'\nimport CLASH_FIELD from '@/assets/json/clash-field.json'\nimport { Box, Typography } from '@mui/material'\nimport Grid from '@mui/material/Grid'\nimport { useProfile, useSetting } from '@nyanpasu/interface'\nimport { BaseCard, BaseDialog } from '@nyanpasu/ui'\nimport { ClashFieldItem, LabelSwitch } from './modules/clash-field'\n\nconst FieldsControl = ({\n  label,\n  fields,\n  enabledFields,\n  onChange,\n}: {\n  label: string\n  fields: { [key: string]: string }\n  enabledFields?: string[]\n  onChange?: (key: string) => void\n}) => {\n  const [open, setOpen] = useState(false)\n\n  // Nyanpasu Control Fields object key\n  const disabled = label === 'default' || label === 'handle'\n\n  const showFields: string[] = disabled\n    ? Object.entries(fields).map(([key]) => key)\n    : (enabledFields as string[])\n\n  const Item = () => {\n    return Object.entries(fields).map(([fKey, fValue], fIndex) => {\n      const checked = enabledFields?.includes(fKey)\n\n      return (\n        <LabelSwitch\n          key={fIndex}\n          label={fKey}\n          url={fValue}\n          disabled={disabled}\n          checked={disabled ? true : checked}\n          onChange={onChange ? () => onChange(fKey) : undefined}\n        />\n      )\n    })\n  }\n\n  return (\n    <>\n      <ClashFieldItem\n        label={label}\n        fields={showFields}\n        onClick={() => setOpen(true)}\n      />\n\n      <BaseDialog\n        title={label}\n        open={open}\n        close=\"Close\"\n        onClose={() => setOpen(false)}\n        divider\n        contentStyle={{ overflow: 'auto' }}\n      >\n        <Box display=\"flex\" flexDirection=\"column\" gap={1}>\n          {disabled && <Typography>Clash Nyanpasu Control Fields.</Typography>}\n\n          <Item />\n        </Box>\n      </BaseDialog>\n    </>\n  )\n}\n\nconst ClashFieldSwitch = () => {\n  const { t } = useTranslation()\n\n  const { value, upsert } = useSetting('enable_clash_fields')\n\n  return (\n    <LabelSwitch\n      label={t('Enable Clash Fields Filter')}\n      checked={Boolean(value)}\n      onChange={() => upsert(!value)}\n    />\n  )\n}\n\nexport const SettingClashField = () => {\n  const { t } = useTranslation()\n\n  const { query, upsert } = useProfile()\n\n  const mergeFields = useMemo(\n    () => [\n      ...Object.keys(CLASH_FIELD.default),\n      ...Object.keys(CLASH_FIELD.handle),\n      ...(query.data?.valid ?? []),\n    ],\n    [query.data],\n  )\n\n  const filteredField = (fields: { [key: string]: string }): string[] => {\n    const usedObjects = []\n\n    for (const key in fields) {\n      if (\n        Object.prototype.hasOwnProperty.call(fields, key) &&\n        mergeFields.includes(key)\n      ) {\n        usedObjects.push(key)\n      }\n    }\n\n    return usedObjects\n  }\n\n  const updateFiled = async (key: string) => {\n    const getFields = (): string[] => {\n      const valid = query.data?.valid ?? []\n\n      if (valid.includes(key)) {\n        return valid.filter((item) => item !== key)\n      } else {\n        valid.push(key)\n\n        return valid\n      }\n    }\n\n    await upsert.mutateAsync({ valid: getFields() })\n  }\n\n  return (\n    <BaseCard label={t('Clash Field')}>\n      <Box sx={{ pt: 1, pb: 2 }}>\n        <ClashFieldSwitch />\n      </Box>\n\n      <Grid container spacing={2}>\n        {Object.entries(CLASH_FIELD).map(([key, value], index) => {\n          const filtered = filteredField(value)\n\n          return (\n            <FieldsControl\n              key={index}\n              label={key}\n              fields={value}\n              enabledFields={filtered}\n              onChange={updateFiled}\n            />\n          )\n        })}\n      </Grid>\n    </BaseCard>\n  )\n}\n\nexport default SettingClashField\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/setting/setting-clash-port.tsx",
    "content": "import { useMemo } from 'react'\nimport { useTranslation } from 'react-i18next'\nimport { message } from '@/utils/notification'\nimport { List } from '@mui/material'\nimport { useClashConfig, useSetting } from '@nyanpasu/interface'\nimport { BaseCard, NumberItem, SwitchItem } from '@nyanpasu/ui'\n\nconst ClashPort = () => {\n  const { t } = useTranslation()\n\n  const { value, upsert } = useSetting('verge_mixed_port')\n\n  const { query, upsert: upsertClash } = useClashConfig()\n\n  const port = useMemo(() => {\n    return query.data?.['mixed-port'] || value || 7890\n  }, [query.data, value])\n\n  return (\n    <NumberItem\n      label={t('Mixed Port')}\n      value={port}\n      checkEvent={(input) => input > 65535 || input < 1}\n      checkLabel=\"Port must be between 1 and 65535.\"\n      onApply={async (value) => {\n        await upsertClash.mutateAsync({ 'mixed-port': value })\n        await upsert(value)\n      }}\n    />\n  )\n}\n\nconst RandomPort = () => {\n  const { t } = useTranslation()\n\n  const { value, upsert } = useSetting('enable_random_port')\n\n  const handleRandomPort = async () => {\n    try {\n      await upsert(!value)\n    } catch (e) {\n      message(JSON.stringify(e), {\n        title: t('Error'),\n        kind: 'error',\n      })\n    } finally {\n      message(t('After restart to take effect'), {\n        title: t('Successful'),\n        kind: 'info',\n      })\n    }\n  }\n\n  return (\n    <SwitchItem\n      label={t('Random Port')}\n      checked={value || false}\n      onChange={handleRandomPort}\n    />\n  )\n}\n\nexport const SettingClashPort = () => {\n  const { t } = useTranslation()\n\n  return (\n    <BaseCard label={t('Clash Port')}>\n      <List disablePadding>\n        <ClashPort />\n\n        <RandomPort />\n      </List>\n    </BaseCard>\n  )\n}\n\nexport default SettingClashPort\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/setting/setting-clash-web.tsx",
    "content": "import { useLockFn } from 'ahooks'\nimport { useMemo, useState } from 'react'\nimport { useTranslation } from 'react-i18next'\nimport AddIcon from '@mui/icons-material/Add'\nimport {\n  Box,\n  Chip,\n  Divider,\n  IconButton,\n  TextField,\n  Tooltip,\n  Typography,\n} from '@mui/material'\nimport Grid from '@mui/material/Grid'\nimport { useClashInfo, useSetting } from '@nyanpasu/interface'\nimport { BaseCard, BaseDialog, Expand } from '@nyanpasu/ui'\nimport { ClashWebItem, extractServer, openWebUrl, renderChip } from './modules'\n\nconst AddRecordButton = ({ onClick }: { onClick: () => void }) => {\n  const { t } = useTranslation()\n\n  return (\n    <Tooltip title={t('New Item')}>\n      <IconButton onClick={onClick}>\n        <AddIcon />\n      </IconButton>\n    </Tooltip>\n  )\n}\n\nexport const SettingClashWeb = () => {\n  const { t } = useTranslation()\n\n  const { value, upsert } = useSetting('web_ui_list')\n\n  const { data } = useClashInfo()\n\n  const labels = useMemo(() => {\n    const { host, port } = extractServer(data?.server)\n\n    return {\n      host,\n      port,\n      secret: data?.secret,\n    }\n  }, [data])\n\n  const [open, setOpen] = useState(false)\n\n  const [editString, setEditString] = useState('')\n\n  const [editIndex, setEditIndex] = useState<number | null>(null)\n\n  const deleteItem = useLockFn(async (index: number) => {\n    await upsert(\n      value ? value.slice(0, index).concat(value.slice(index + 1)) : null,\n    )\n  })\n\n  const updateItem = useLockFn(async () => {\n    const list = [...(value || [])]\n\n    if (!list) return\n\n    if (editIndex !== null) {\n      list[editIndex] = editString\n    } else {\n      list.push(editString)\n    }\n\n    await upsert(list)\n  })\n\n  return (\n    <>\n      <BaseCard\n        label={t('Web UI')}\n        labelChildren={\n          <AddRecordButton\n            onClick={() => {\n              setEditString('')\n              setEditIndex(null)\n              setOpen(true)\n            }}\n          />\n        }\n      >\n        {value && (\n          <Grid container sx={{ mt: 1 }} spacing={2}>\n            {value.map((item, index) => {\n              return (\n                <Grid\n                  key={index}\n                  size={{\n                    xs: 12,\n                    xl: 6,\n                  }}\n                >\n                  <ClashWebItem\n                    label={renderChip(item, labels)}\n                    onOpen={() => openWebUrl(item, labels)}\n                    onEdit={() => {\n                      setEditIndex(index)\n                      setEditString(item)\n                      setOpen(true)\n                    }}\n                    onDelete={() => deleteItem(index)}\n                  />\n                </Grid>\n              )\n            })}\n          </Grid>\n        )}\n      </BaseCard>\n\n      <BaseDialog\n        title={editIndex != null ? t('Edit Item') : t('New Item')}\n        open={open}\n        onClose={() => {\n          setOpen(false)\n          setEditIndex(null)\n        }}\n        onOk={() => {\n          updateItem()\n          setOpen(false)\n          setEditIndex(null)\n          setEditString('')\n        }}\n        ok={t('Ok')}\n        close={t('Close')}\n        contentStyle={{ overflow: editString ? 'auto' : 'hidden' }}\n        divider\n      >\n        <Box display=\"flex\" flexDirection=\"column\" gap={1}>\n          <Typography variant=\"h5\">{t('Input')}</Typography>\n\n          <TextField\n            sx={{ width: '100%' }}\n            value={editString}\n            variant=\"outlined\"\n            multiline\n            placeholder={t(`Support %host %port, and %secret`)}\n            onChange={(e) => setEditString(e.target.value)}\n          />\n\n          <Typography sx={{ userSelect: 'text' }}>\n            {t('Replace host, port, and secret with')}\n          </Typography>\n\n          <Box display=\"flex\" gap={1}>\n            {Object.entries(labels).map(([key], index) => {\n              return <Chip key={index} size=\"small\" label={`%${key}`} />\n            })}\n          </Box>\n\n          <Expand open={Boolean(editString) || false}>\n            <Box display=\"flex\" flexDirection=\"column\" gap={1}>\n              <Divider sx={{ mt: 1, mb: 1 }} />\n\n              <Typography variant=\"h5\">{t('Result')}</Typography>\n\n              <Typography sx={{ userSelect: 'text' }} component=\"div\">\n                {renderChip(editString, labels)}\n              </Typography>\n            </Box>\n          </Expand>\n        </Box>\n      </BaseDialog>\n    </>\n  )\n}\n\nexport default SettingClashWeb\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/setting/setting-nyanpasu-auto-reload.tsx",
    "content": "// oxlint-disable typescript/no-explicit-any\nimport { useTranslation } from 'react-i18next'\nimport { useSetting } from '@nyanpasu/interface'\nimport { SwitchItem } from '@nyanpasu/ui'\n\n// 定义各语言的翻译文本\nconst translations = {\n  'zh-CN': {\n    proxy: '当代理切换时打断连接',\n    profile: '当配置文件切换时打断连接',\n    mode: '当模式切换时打断连接',\n  },\n  'zh-TW': {\n    proxy: '當代理切換時打斷連線',\n    profile: '當設定檔切換時打斷連線',\n    mode: '當模式切換時打斷連線',\n  },\n  ru: {\n    proxy: 'Прерывать соединения при смене прокси',\n    profile: 'Прерывать соединения при смене профиля',\n    mode: 'Прерывать соединения при смене режима',\n  },\n  en: {\n    proxy: 'Interrupt connections when proxy changes',\n    profile: 'Interrupt connections when profile changes',\n    mode: 'Interrupt connections when mode changes',\n  },\n  // 默认使用英文\n  default: {\n    proxy: 'Interrupt connections when proxy changes',\n    profile: 'Interrupt connections when profile changes',\n    mode: 'Interrupt connections when mode changes',\n  },\n}\n\nconst BreakWhenProxyChangeSetting = () => {\n  const { i18n } = useTranslation()\n  const currentLang = i18n.language\n\n  // 获取当前语言的翻译，如果找不到则使用默认英文\n  const currentTranslations =\n    translations[currentLang as keyof typeof translations] ||\n    translations.default\n\n  const { value, upsert } = useSetting('break_when_proxy_change' as any)\n\n  return (\n    <SwitchItem\n      label={currentTranslations.proxy}\n      checked={value !== 'none'}\n      onChange={() => {\n        if (value === 'none') {\n          upsert('all' as any)\n        } else {\n          upsert('none' as any)\n        }\n      }}\n    />\n  )\n}\n\nconst BreakWhenProfileChangeSetting = () => {\n  const { i18n } = useTranslation()\n  const currentLang = i18n.language\n\n  // 获取当前语言的翻译，如果找不到则使用默认英文\n  const currentTranslations =\n    translations[currentLang as keyof typeof translations] ||\n    translations.default\n\n  const { value, upsert } = useSetting('break_when_profile_change' as any)\n\n  return (\n    <SwitchItem\n      label={currentTranslations.profile}\n      checked={value === true}\n      onChange={() => {\n        if (value === true) {\n          upsert(false as any)\n        } else {\n          upsert(true as any)\n        }\n      }}\n    />\n  )\n}\n\nconst BreakWhenModeChangeSetting = () => {\n  const { i18n } = useTranslation()\n  const currentLang = i18n.language\n\n  // 获取当前语言的翻译，如果找不到则使用默认英文\n  const currentTranslations =\n    translations[currentLang as keyof typeof translations] ||\n    translations.default\n\n  const { value, upsert } = useSetting('break_when_mode_change' as any)\n\n  return (\n    <SwitchItem\n      label={currentTranslations.mode}\n      checked={value === true}\n      onChange={() => {\n        if (value === true) {\n          upsert(false as any)\n        } else {\n          upsert(true as any)\n        }\n      }}\n    />\n  )\n}\n\nexport {\n  BreakWhenProxyChangeSetting,\n  BreakWhenProfileChangeSetting,\n  BreakWhenModeChangeSetting,\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/setting/setting-nyanpasu-misc.tsx",
    "content": "import { useTranslation } from 'react-i18next'\nimport { List } from '@mui/material'\nimport {\n  LoggingLevel,\n  ProxiesSelectorMode,\n  useSetting,\n  type NetworkStatisticWidgetConfig,\n} from '@nyanpasu/interface'\nimport { BaseCard, MenuItem, SwitchItem, TextItem } from '@nyanpasu/ui'\nimport {\n  BreakWhenModeChangeSetting,\n  BreakWhenProfileChangeSetting,\n  BreakWhenProxyChangeSetting,\n} from './setting-nyanpasu-auto-reload'\n\nconst EnableBuiltinEnhanced = () => {\n  const { t } = useTranslation()\n\n  const { value, upsert } = useSetting('enable_builtin_enhanced')\n\n  return (\n    <SwitchItem\n      label={t('Enable Built-in Enhanced')}\n      checked={Boolean(value)}\n      onChange={() => upsert(!value)}\n    />\n  )\n}\n\nconst LightenAnimationEffects = () => {\n  const { t } = useTranslation()\n\n  const { value, upsert } = useSetting('lighten_animation_effects')\n\n  return (\n    <SwitchItem\n      label={t('Lighten Up Animation Effects')}\n      checked={Boolean(value)}\n      onChange={() => upsert(!value)}\n    />\n  )\n}\n\nconst AppLogLevel = () => {\n  const { t } = useTranslation()\n\n  const { value, upsert } = useSetting('app_log_level')\n\n  const logOptions = {\n    trace: 'Trace',\n    debug: 'Debug',\n    info: 'Info',\n    warn: 'Warn',\n    error: 'Error',\n    silent: 'Silent',\n  }\n\n  return (\n    <MenuItem\n      label={t('App Log Level')}\n      options={logOptions}\n      selected={value || 'info'}\n      onSelected={(value) => upsert(value as LoggingLevel)}\n    />\n  )\n}\n\nconst TrayProxiesSelector = () => {\n  const { t } = useTranslation()\n\n  const { value, upsert } = useSetting('clash_tray_selector')\n\n  const trayProxiesSelectorMode = {\n    normal: t('Normal'),\n    hidden: t('Hidden'),\n    submenu: t('Submenu'),\n  }\n\n  return (\n    <MenuItem\n      label={t('Tray Proxies Selector')}\n      options={trayProxiesSelectorMode}\n      selected={value || 'normal'}\n      onSelected={(value) => upsert(value as ProxiesSelectorMode)}\n    />\n  )\n}\n\nconst NetworkWidgetVariant = () => {\n  const { t } = useTranslation()\n\n  const { value, upsert } = useSetting('network_statistic_widget')\n\n  const options = {\n    disabled: t('Disabled'),\n    small: 'Small',\n    large: 'Large',\n  }\n\n  const mapping: { [key: string]: NetworkStatisticWidgetConfig } = {\n    disabled: {\n      kind: 'disabled',\n    },\n    small: {\n      kind: 'enabled',\n      value: 'small',\n    },\n    large: {\n      kind: 'enabled',\n      value: 'large',\n    },\n  }\n\n  return (\n    <MenuItem\n      label={t('Network Statistic Widget')}\n      options={options}\n      selected={\n        Object.entries(mapping).find(([_, config]) =>\n          config.kind === 'disabled'\n            ? value?.kind === 'disabled'\n            : value?.kind === 'enabled' && config.value === value.value,\n        )?.[0] || 'disabled'\n      }\n      onSelected={(val) => upsert(mapping[val as string])}\n    />\n  )\n}\n\nconst DefaultLatencyTest = () => {\n  const { t } = useTranslation()\n\n  const { value, upsert } = useSetting('default_latency_test')\n\n  return (\n    <TextItem\n      label={t('Default Latency Test')}\n      placeholder=\"http://www.gstatic.com/generate_204\"\n      value={value || ''}\n      onApply={(value) => upsert(value)}\n    />\n  )\n}\n\nexport const SettingNyanpasuMisc = () => {\n  const { t } = useTranslation()\n\n  return (\n    <BaseCard label={t('Nyanpasu Setting')}>\n      <List disablePadding>\n        <AppLogLevel />\n\n        <TrayProxiesSelector />\n\n        <NetworkWidgetVariant />\n\n        <BreakWhenProxyChangeSetting />\n\n        <BreakWhenProfileChangeSetting />\n\n        <BreakWhenModeChangeSetting />\n\n        <EnableBuiltinEnhanced />\n\n        <LightenAnimationEffects />\n\n        <DefaultLatencyTest />\n      </List>\n    </BaseCard>\n  )\n}\n\nexport default SettingNyanpasuMisc\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/setting/setting-nyanpasu-path.tsx",
    "content": "import { useLockFn } from 'ahooks'\nimport { useTranslation } from 'react-i18next'\nimport { OS } from '@/consts'\nimport { sleep } from '@/utils'\nimport { message } from '@/utils/notification'\nimport Grid from '@mui/material/Grid'\nimport {\n  collectLogs,\n  openAppConfigDir,\n  openAppDataDir,\n  openCoreDir,\n  openLogsDir,\n  restartApplication,\n  setCustomAppDir,\n} from '@nyanpasu/interface'\nimport { BaseCard } from '@nyanpasu/ui'\nimport { open } from '@tauri-apps/plugin-dialog'\nimport { PaperButton } from './modules/nyanpasu-path'\n\nexport const SettingNyanpasuPath = () => {\n  const { t } = useTranslation()\n\n  const migrateAppPath = useLockFn(async () => {\n    try {\n      // TODO: use current app dir as defaultPath\n      const selected = await open({\n        directory: true,\n        multiple: false,\n      })\n\n      // user cancelled the selection\n      if (!selected) {\n        return\n      }\n\n      if (Array.isArray(selected)) {\n        message(t('Multiple directories are not supported'), {\n          title: t('Error'),\n          kind: 'error',\n        })\n\n        return\n      }\n\n      await setCustomAppDir(selected)\n\n      message(t('Successfully changed the app directory'), {\n        title: t('Successful'),\n        kind: 'error',\n      })\n\n      await sleep(1000)\n\n      await restartApplication()\n    } catch (e) {\n      message(t('Failed to migrate', { error: `${JSON.stringify(e)}` }), {\n        title: t('Error'),\n        kind: 'error',\n      })\n    }\n  })\n\n  const gridLists = [\n    { label: t('Open Config Dir'), onClick: openAppConfigDir },\n    { label: t('Open Data Dir'), onClick: openAppDataDir },\n    OS === 'windows' && {\n      label: t('Migrate Config Dir'),\n      onClick: migrateAppPath,\n    },\n    { label: t('Open Core Dir'), onClick: openCoreDir },\n    { label: t('Open Log Dir'), onClick: openLogsDir },\n    { label: t('Collect Logs'), onClick: collectLogs },\n  ].filter((x) => !!x)\n\n  return (\n    <BaseCard label={t('Path Config')}>\n      <Grid container alignItems=\"stretch\" spacing={2}>\n        {gridLists.map(({ label, onClick }) => (\n          <Grid\n            key={label}\n            size={{\n              xs: 6,\n              xl: 3,\n            }}\n          >\n            <PaperButton\n              label={label}\n              onClick={onClick}\n              sxPaper={{ height: '100%' }}\n              sxButton={{ height: '100%' }}\n            />\n          </Grid>\n        ))}\n      </Grid>\n    </BaseCard>\n  )\n}\n\nexport default SettingNyanpasuPath\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/setting/setting-nyanpasu-tasks.tsx",
    "content": "import { useTranslation } from 'react-i18next'\nimport { List } from '@mui/material'\nimport { useSetting } from '@nyanpasu/interface'\nimport { BaseCard, NumberItem } from '@nyanpasu/ui'\n\nexport const SettingNyanpasuTasks = () => {\n  const { t } = useTranslation()\n\n  const { value, upsert } = useSetting('max_log_files')\n\n  return (\n    <BaseCard label={t('Tasks')}>\n      <List disablePadding>\n        <NumberItem\n          value={value || 0}\n          label={t('Max Log Files')}\n          checkEvent={(value) => value <= 0}\n          checkLabel=\"Value must larger than 0.\"\n          onApply={(v) => upsert(v)}\n        />\n      </List>\n    </BaseCard>\n  )\n}\n\nexport default SettingNyanpasuTasks\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/setting/setting-nyanpasu-ui.tsx",
    "content": "import { useAtom } from 'jotai'\nimport { MuiColorInput } from 'mui-color-input'\nimport { useEffect, useState } from 'react'\nimport { useTranslation } from 'react-i18next'\nimport { isHexColor } from 'validator'\nimport { useLockFn } from '@/hooks/use-lock-fn'\nimport { atomIsDrawerOnlyIcon } from '@/store'\nimport { languageOptions } from '@/utils/language'\nimport Done from '@mui/icons-material/Done'\nimport { Button, List, ListItem, ListItemText } from '@mui/material'\nimport { commands, useSetting } from '@nyanpasu/interface'\nimport { BaseCard, Expand, MenuItem, SwitchItem } from '@nyanpasu/ui'\nimport { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'\nimport { DEFAULT_COLOR } from '../layout/use-custom-theme'\n\nconst currentWindow = getCurrentWebviewWindow()\n\nconst commonSx = {\n  width: 128,\n}\n\nconst LanguageSwitch = () => {\n  const { t } = useTranslation()\n\n  const language = useSetting('language')\n\n  return (\n    <MenuItem\n      label={t('Language')}\n      selectSx={commonSx}\n      options={languageOptions}\n      selected={language.value || 'en'}\n      onSelected={(value) => language.upsert(value as string)}\n    />\n  )\n}\n\nconst ThemeSwitch = () => {\n  const { t } = useTranslation()\n\n  const themeOptions = {\n    dark: t('theme.dark'),\n    light: t('theme.light'),\n    system: t('theme.system'),\n  }\n\n  const themeMode = useSetting('theme_mode')\n\n  return (\n    <MenuItem\n      label={t('Theme Mode')}\n      selectSx={commonSx}\n      options={themeOptions}\n      selected={themeMode.value || 'system'}\n      onSelected={(value) => themeMode.upsert(value as string)}\n    />\n  )\n}\n\nconst ThemeColor = () => {\n  const { t } = useTranslation()\n\n  const theme = useSetting('theme_color')\n\n  const [value, setValue] = useState(theme.value ?? DEFAULT_COLOR)\n\n  useEffect(() => {\n    setValue(theme.value ?? DEFAULT_COLOR)\n  }, [theme.value])\n\n  return (\n    <>\n      <ListItem sx={{ pl: 0, pr: 0 }}>\n        <ListItemText primary={t('Theme Setting')} />\n\n        <MuiColorInput\n          size=\"small\"\n          sx={commonSx}\n          value={value ?? DEFAULT_COLOR}\n          isAlphaHidden\n          format=\"hex\"\n          onBlur={() => {\n            if (!isHexColor(value ?? DEFAULT_COLOR)) {\n              setValue(theme.value ?? DEFAULT_COLOR)\n            }\n          }}\n          onChange={(color: string) => setValue(color)}\n        />\n      </ListItem>\n\n      <Expand open={(theme.value || DEFAULT_COLOR) !== value}>\n        <div className=\"flex justify-end\">\n          <Button\n            variant=\"contained\"\n            startIcon={<Done />}\n            onClick={() => {\n              if (isHexColor(value)) {\n                theme.upsert(value)\n              } else {\n                // 如果输入的不是有效的十六进制颜色，则恢复为之前的值\n                setValue(theme.value ?? DEFAULT_COLOR)\n              }\n            }}\n          >\n            {t('Apply')}\n          </Button>\n        </div>\n      </Expand>\n    </>\n  )\n}\n\nconst ExperimentalSwitch = () => {\n  const { upsert } = useSetting('use_legacy_ui')\n\n  const handleClick = useLockFn(async () => {\n    await upsert(false)\n    await commands.createMainWindow()\n    await currentWindow.close()\n  })\n\n  return (\n    <ListItem sx={{ pl: 0, pr: 0 }}>\n      <ListItemText primary=\"Switch to Experimental UI\" />\n\n      <Button variant=\"contained\" onClick={handleClick}>\n        Continue\n      </Button>\n    </ListItem>\n  )\n}\n\nexport const SettingNyanpasuUI = () => {\n  const { t } = useTranslation()\n\n  const [onlyIcon, setOnlyIcon] = useAtom(atomIsDrawerOnlyIcon)\n\n  return (\n    <BaseCard label={t('User Interface')}>\n      <List disablePadding>\n        <LanguageSwitch />\n\n        <ThemeSwitch />\n\n        <ThemeColor />\n\n        <SwitchItem\n          label={t('Icon Navigation Bar')}\n          checked={onlyIcon}\n          onChange={() => setOnlyIcon(!onlyIcon)}\n        />\n\n        <ExperimentalSwitch />\n      </List>\n    </BaseCard>\n  )\n}\n\nexport default SettingNyanpasuUI\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/setting/setting-nyanpasu-version.tsx",
    "content": "import { useLockFn } from 'ahooks'\nimport { useSetAtom } from 'jotai'\nimport { useState } from 'react'\nimport { useTranslation } from 'react-i18next'\nimport LogoSvg from '@/assets/image/logo.svg?react'\nimport { checkUpdate, useUpdaterPlatformSupported } from '@/hooks/use-updater'\nimport { UpdaterInstanceAtom } from '@/store/updater'\nimport { formatError } from '@/utils'\nimport { message } from '@/utils/notification'\nimport { Box, Button, List, ListItem, Paper, Typography } from '@mui/material'\nimport { useSetting } from '@nyanpasu/interface'\nimport { alpha, BaseCard } from '@nyanpasu/ui'\nimport { version } from '@root/package.json'\nimport { LabelSwitch } from './modules/clash-field'\n\nconst AutoCheckUpdate = () => {\n  const { t } = useTranslation()\n\n  const { value, upsert } = useSetting('enable_auto_check_update')\n\n  return (\n    <LabelSwitch\n      label={t('Auto Check Updates')}\n      checked={value ?? true}\n      onChange={() => upsert(!value)}\n    />\n  )\n}\n\nexport const SettingNyanpasuVersion = () => {\n  const { t } = useTranslation()\n\n  const [loading, setLoading] = useState(false)\n\n  const setUpdaterInstance = useSetAtom(UpdaterInstanceAtom)\n  const isPlatformSupported = useUpdaterPlatformSupported()\n  const onCheckUpdate = useLockFn(async () => {\n    try {\n      setLoading(true)\n\n      const update = await checkUpdate()\n\n      if (!update) {\n        message(t('No update available.'), {\n          title: t('Info'),\n          kind: 'info',\n        })\n      } else {\n        setUpdaterInstance(update || null)\n      }\n    } catch (e) {\n      message(\n        `Update check failed. Please verify your network connection.\\n\\n${formatError(e)}`,\n        {\n          title: t('Error'),\n          kind: 'error',\n        },\n      )\n    } finally {\n      setLoading(false)\n    }\n  })\n\n  return (\n    <BaseCard label={t('Nyanpasu Version')}>\n      <List disablePadding>\n        <ListItem sx={{ pl: 0, pr: 0 }}>\n          <Paper\n            elevation={0}\n            sx={(theme) => ({\n              mt: 1,\n              padding: 2,\n              backgroundColor: alpha(theme.vars.palette.primary.main, 0.1),\n              borderRadius: 6,\n              width: '100%',\n            })}\n          >\n            <Box\n              display=\"flex\"\n              flexDirection=\"column\"\n              alignItems=\"center\"\n              gap={2}\n            >\n              <LogoSvg className=\"h-32 w-32\" />\n\n              <Typography fontWeight={700} noWrap>\n                {'Clash Nyanpasu~(∠・ω< )⌒☆'}&nbsp;\n              </Typography>\n\n              <Typography>\n                <b>Version: </b>v{version}\n              </Typography>\n            </Box>\n          </Paper>\n        </ListItem>\n\n        {isPlatformSupported && (\n          <>\n            <div className=\"mt-1 mb-1\">\n              <AutoCheckUpdate />\n            </div>\n            <ListItem sx={{ pl: 0, pr: 0 }}>\n              <Button\n                variant=\"contained\"\n                size=\"large\"\n                loading={loading}\n                onClick={onCheckUpdate}\n                sx={{ width: '100%' }}\n              >\n                {t('Check for Updates')}\n              </Button>\n            </ListItem>\n          </>\n        )}\n      </List>\n    </BaseCard>\n  )\n}\n\nexport default SettingNyanpasuVersion\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/setting/setting-page.tsx",
    "content": "import { useAtomValue } from 'jotai'\nimport { useWindowSize } from 'react-use'\nimport { useIsAppImage } from '@/hooks/use-consts'\nimport { atomIsDrawerOnlyIcon } from '@/store'\nimport Masonry from '@mui/lab/Masonry'\nimport SettingClashBase from './setting-clash-base'\nimport SettingClashCore from './setting-clash-core'\nimport SettingClashExternal from './setting-clash-external'\nimport SettingClashField from './setting-clash-field'\nimport SettingClashPort from './setting-clash-port'\nimport SettingClashWeb from './setting-clash-web'\nimport SettingNyanpasuMisc from './setting-nyanpasu-misc'\nimport SettingNyanpasuPath from './setting-nyanpasu-path'\nimport SettingNyanpasuTasks from './setting-nyanpasu-tasks'\nimport SettingNyanpasuUI from './setting-nyanpasu-ui'\nimport SettingNyanpasuVersion from './setting-nyanpasu-version'\nimport SettingSystemBehavior from './setting-system-behavior'\nimport SettingSystemProxy from './setting-system-proxy'\nimport SettingSystemService from './setting-system-service'\n\nexport const SettingPage = () => {\n  const isAppImage = useIsAppImage()\n\n  const isDrawerOnlyIcon = useAtomValue(atomIsDrawerOnlyIcon)\n\n  const { width } = useWindowSize()\n\n  return (\n    <Masonry\n      className=\"w-full\"\n      columns={{\n        xs: 1,\n        sm: 1,\n        md: isDrawerOnlyIcon ? 2 : width > 1000 ? 2 : 1,\n        lg: 2,\n        xl: 2,\n      }}\n      spacing={3}\n      sequential\n    >\n      <SettingSystemProxy />\n\n      <SettingNyanpasuUI />\n\n      <SettingClashBase />\n\n      <SettingClashPort />\n\n      <SettingClashExternal />\n\n      <SettingClashWeb />\n\n      <SettingClashField />\n\n      <SettingClashCore />\n\n      <SettingSystemBehavior />\n\n      {!isAppImage.data && <SettingSystemService />}\n\n      <SettingNyanpasuTasks />\n\n      <SettingNyanpasuMisc />\n\n      <SettingNyanpasuPath />\n\n      <SettingNyanpasuVersion />\n    </Masonry>\n  )\n}\n\nexport default SettingPage\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/setting/setting-system-behavior.tsx",
    "content": "import { useTranslation } from 'react-i18next'\nimport Grid from '@mui/material/Grid'\nimport { useSetting } from '@nyanpasu/interface'\nimport { BaseCard } from '@nyanpasu/ui'\nimport { PaperSwitchButton } from './modules/system-proxy'\n\nexport const SettingSystemBehavior = () => {\n  const { t } = useTranslation()\n\n  const autoLaunch = useSetting('enable_auto_launch')\n\n  const silentStart = useSetting('enable_silent_start')\n\n  return (\n    <BaseCard label={t('Initiating Behavior')}>\n      <Grid container spacing={2}>\n        <Grid size={{ xs: 6 }}>\n          <PaperSwitchButton\n            label={t('Auto Start')}\n            checked={autoLaunch.value || false}\n            onClick={() => autoLaunch.upsert(!autoLaunch.value)}\n          />\n        </Grid>\n\n        <Grid\n          size={{\n            xs: 6,\n          }}\n        >\n          <PaperSwitchButton\n            label={t('Silent Start')}\n            checked={silentStart.value || false}\n            onClick={() => silentStart.upsert(!silentStart.value)}\n          />\n        </Grid>\n      </Grid>\n    </BaseCard>\n  )\n}\n\nexport default SettingSystemBehavior\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/setting/setting-system-proxy.tsx",
    "content": "import { useLockFn } from 'ahooks'\nimport { useState } from 'react'\nimport { useTranslation } from 'react-i18next'\nimport { formatError } from '@/utils'\nimport { message } from '@/utils/notification'\nimport { InputAdornment, List, ListItem } from '@mui/material'\nimport Grid from '@mui/material/Grid'\nimport { useSetting, useSystemProxy } from '@nyanpasu/interface'\nimport {\n  BaseCard,\n  Expand,\n  ExpandMore,\n  NumberItem,\n  SwitchItem,\n  TextItem,\n} from '@nyanpasu/ui'\nimport { PaperSwitchButton } from './modules/system-proxy'\n\nconst TunModeButton = () => {\n  const { t } = useTranslation()\n\n  const tunMode = useSetting('enable_tun_mode')\n\n  const handleTunMode = useLockFn(async () => {\n    try {\n      await tunMode.upsert(!tunMode.value)\n    } catch (error) {\n      message(`Activation TUN Mode failed! \\n Error: ${formatError(error)}`, {\n        title: t('Error'),\n        kind: 'error',\n      })\n    }\n  })\n\n  return (\n    <PaperSwitchButton\n      label={t('TUN Mode')}\n      checked={Boolean(tunMode.value)}\n      onClick={handleTunMode}\n    />\n  )\n}\n\nconst SystemProxyButton = () => {\n  const { t } = useTranslation()\n\n  const systemProxy = useSetting('enable_system_proxy')\n\n  const handleSystemProxy = useLockFn(async () => {\n    try {\n      await systemProxy.upsert(!systemProxy.value)\n    } catch (error) {\n      message(\n        `Activation System Proxy failed!\\n Error: ${formatError(error)}`,\n        {\n          title: t('Error'),\n          kind: 'error',\n        },\n      )\n    }\n  })\n\n  return (\n    <PaperSwitchButton\n      label={t('System Proxy')}\n      checked={Boolean(systemProxy.value)}\n      onClick={handleSystemProxy}\n    />\n  )\n}\n\nconst ProxyGuardSwitch = () => {\n  const { t } = useTranslation()\n\n  const proxyGuard = useSetting('enable_proxy_guard')\n\n  const handleProxyGuard = useLockFn(async () => {\n    try {\n      await proxyGuard.upsert(!proxyGuard.value)\n    } catch (error) {\n      message(`Activation Proxy Guard failed!\\n Error: ${formatError(error)}`, {\n        title: t('Error'),\n        kind: 'error',\n      })\n    }\n  })\n\n  return (\n    <SwitchItem\n      label={t('Proxy Guard')}\n      checked={Boolean(proxyGuard.value)}\n      onClick={handleProxyGuard}\n    />\n  )\n}\n\nconst ProxyGuardInterval = () => {\n  const { t } = useTranslation()\n\n  const proxyGuardInterval = useSetting('proxy_guard_interval')\n\n  return (\n    <NumberItem\n      label={t('Guard Interval')}\n      value={proxyGuardInterval.value || 0}\n      checkEvent={(input) => input <= 0}\n      checkLabel={t('The interval must be greater than 0 second')}\n      onApply={(value) => {\n        proxyGuardInterval.upsert(value)\n      }}\n      textFieldProps={{\n        inputProps: {\n          'aria-autocomplete': 'none',\n        },\n        InputProps: {\n          endAdornment: (\n            <InputAdornment position=\"end\">{t('seconds')}</InputAdornment>\n          ),\n        },\n      }}\n    />\n  )\n}\n\nconst DEFAULT_BYPASS =\n  'localhost;127.;192.168.;10.;172.16.;172.17.;172.18.;172.19.;172.20.;172.21.;172.22.;172.23.;172.24.;172.25.;172.26.;172.27.;172.28.;172.29.;172.30.;172.31.*'\n\nconst SystemProxyBypass = () => {\n  const { t } = useTranslation()\n\n  const systemProxyBypass = useSetting('system_proxy_bypass')\n\n  return (\n    <TextItem\n      label={t('Proxy Bypass')}\n      value={systemProxyBypass.data || ''}\n      onApply={(value) => {\n        if (!value || value.trim() === '') {\n          // 输入为空 → 重置为默认规则\n          systemProxyBypass.upsert(DEFAULT_BYPASS)\n        } else {\n          // 正常写入用户配置\n          systemProxyBypass.upsert(value)\n        }\n      }}\n    />\n  )\n}\n\nconst CurrentSystemProxy = () => {\n  const { t } = useTranslation()\n\n  const { data } = useSystemProxy()\n\n  return (\n    <ListItem\n      className=\"!w-full !flex-col !items-start select-text\"\n      sx={{ pl: 0, pr: 0 }}\n    >\n      <div className=\"text-base leading-10\">{t('Current System Proxy')}</div>\n\n      {Object.entries(data ?? []).map(([key, value], index) => {\n        return (\n          <div key={index} className=\"flex w-full leading-8\">\n            <div className=\"w-28 capitalize\">{key}:</div>\n\n            <div className=\"text-warp flex-1 break-all\">{String(value)}</div>\n          </div>\n        )\n      })}\n    </ListItem>\n  )\n}\n\nexport const SettingSystemProxy = () => {\n  const { t } = useTranslation()\n\n  const [expand, setExpand] = useState(false)\n\n  return (\n    <BaseCard\n      label={t('System Setting')}\n      labelChildren={\n        <ExpandMore expand={expand} onClick={() => setExpand(!expand)} />\n      }\n    >\n      <Grid container spacing={2}>\n        <Grid size={{ xs: 6 }}>\n          <TunModeButton />\n        </Grid>\n\n        <Grid size={{ xs: 6 }}>\n          <SystemProxyButton />\n        </Grid>\n      </Grid>\n\n      <Expand open={expand}>\n        <List disablePadding sx={{ pt: 1 }}>\n          <ProxyGuardSwitch />\n\n          <ProxyGuardInterval />\n\n          <SystemProxyBypass />\n\n          <CurrentSystemProxy />\n        </List>\n      </Expand>\n    </BaseCard>\n  )\n}\n\nexport default SettingSystemProxy\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/setting/setting-system-service.tsx",
    "content": "import { useMemoizedFn } from 'ahooks'\nimport { useTransition } from 'react'\nimport { useTranslation } from 'react-i18next'\nimport { formatError } from '@/utils'\nimport { message } from '@/utils/notification'\nimport { Button, List, ListItem, ListItemText, Typography } from '@mui/material'\nimport {\n  restartSidecar,\n  useSetting,\n  useSystemService,\n} from '@nyanpasu/interface'\nimport { BaseCard, SwitchItem } from '@nyanpasu/ui'\nimport {\n  ServerManualPromptDialogWrapper,\n  useServerManualPromptDialog,\n} from './modules/service-manual-prompt-dialog'\n\nexport const SettingSystemService = () => {\n  const { t } = useTranslation()\n\n  const { query, upsert } = useSystemService()\n\n  const getInstallButtonString = () => {\n    switch (query.data?.status) {\n      case 'running':\n      case 'stopped': {\n        return t('uninstall')\n      }\n\n      case 'not_installed': {\n        return t('install')\n      }\n    }\n  }\n  const getControlButtonString = () => {\n    switch (query.data?.status) {\n      case 'running': {\n        return t('stop')\n      }\n\n      case 'stopped': {\n        return t('start')\n      }\n    }\n  }\n\n  const isDisabled = query.data?.status === 'not_installed'\n\n  const promptDialog = useServerManualPromptDialog()\n\n  const [installOrUninstallPending, startInstallOrUninstall] = useTransition()\n  const handleInstallClick = useMemoizedFn(() => {\n    startInstallOrUninstall(async () => {\n      try {\n        switch (query.data?.status) {\n          case 'running':\n          case 'stopped':\n            await upsert.mutateAsync('uninstall')\n            break\n\n          case 'not_installed':\n            await upsert.mutateAsync('install')\n            break\n\n          default:\n            break\n        }\n        await restartSidecar()\n      } catch (e) {\n        const errorMessage = `${\n          query.data?.status === 'not_installed'\n            ? t('Failed to install')\n            : t('Failed to uninstall')\n        }: ${formatError(e)}`\n\n        message(errorMessage, {\n          kind: 'error',\n          title: t('Error'),\n        })\n        // If the installation fails, prompt the user to manually install the service\n        promptDialog.show(\n          query.data?.status === 'not_installed' ? 'install' : 'uninstall',\n        )\n      }\n    })\n  })\n\n  const [serviceControlPending, startServiceControl] = useTransition()\n  const handleControlClick = useMemoizedFn(() => {\n    startServiceControl(async () => {\n      try {\n        switch (query.data?.status) {\n          case 'running':\n            await upsert.mutateAsync('stop')\n            break\n\n          case 'stopped':\n            await upsert.mutateAsync('start')\n            break\n\n          default:\n            break\n        }\n        await restartSidecar()\n      } catch (e) {\n        const errorMessage =\n          query.data?.status === 'running'\n            ? `Stop failed: ${formatError(e)}`\n            : `Start failed: ${formatError(e)}`\n\n        message(errorMessage, {\n          kind: 'error',\n          title: t('Error'),\n        })\n        // If start failed show a prompt to user to start the service manually\n        promptDialog.show(query.data?.status === 'running' ? 'stop' : 'start')\n      }\n    })\n  })\n\n  const serviceMode = useSetting('enable_service_mode')\n\n  return (\n    <BaseCard label={t('System Service')}>\n      <ServerManualPromptDialogWrapper />\n      <List disablePadding>\n        <SwitchItem\n          label={t('Service Mode')}\n          disabled={isDisabled}\n          checked={serviceMode.value || false}\n          onChange={() => serviceMode.upsert(!serviceMode.value)}\n        />\n\n        {isDisabled && (\n          <ListItem sx={{ pl: 0, pr: 0 }}>\n            <Typography>\n              {t(\n                'Information: To enable service mode, make sure the Clash Nyanpasu service is installed and started',\n              )}\n            </Typography>\n          </ListItem>\n        )}\n\n        <ListItem sx={{ pl: 0, pr: 0 }}>\n          <ListItemText\n            primary={t('Current Status', {\n              status: t(`${query.data?.status}`),\n            })}\n          />\n          <div className=\"flex gap-2\">\n            {!isDisabled && (\n              <Button\n                variant=\"contained\"\n                onClick={handleControlClick}\n                loading={serviceControlPending}\n                disabled={installOrUninstallPending || serviceControlPending}\n              >\n                {getControlButtonString()}\n              </Button>\n            )}\n\n            <Button\n              variant=\"contained\"\n              onClick={handleInstallClick}\n              loading={installOrUninstallPending}\n              disabled={installOrUninstallPending || serviceControlPending}\n            >\n              {getInstallButtonString()}\n            </Button>\n\n            {import.meta.env.DEV && (\n              <Button\n                variant=\"contained\"\n                onClick={() => promptDialog.show('install')}\n              >\n                {t('Prompt')}\n              </Button>\n            )}\n          </div>\n        </ListItem>\n      </List>\n    </BaseCard>\n  )\n}\n\nexport default SettingSystemService\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/settings/system-proxy.tsx",
    "content": "import NetworkPing from '~icons/material-symbols/network-ping-rounded'\nimport SettingsEthernet from '~icons/material-symbols/settings-ethernet-rounded'\nimport { useBlockTask } from '@/components/providers/block-task-provider'\nimport { Button, ButtonProps } from '@/components/ui/button'\nimport { CircularProgress } from '@/components/ui/progress'\nimport { m } from '@/paraglide/messages'\nimport { useSetting } from '@nyanpasu/interface'\nimport { cn } from '@nyanpasu/ui'\n\nconst ProxyButton = ({\n  className,\n  isActive,\n  loading,\n  children,\n  ...props\n}: ButtonProps & {\n  isActive?: boolean\n}) => {\n  return (\n    <Button\n      className={cn(\n        'group h-16 rounded-3xl font-bold',\n        'flex items-center justify-between gap-2',\n        'data-[active=false]:bg-white dark:data-[active=false]:bg-black',\n        className,\n      )}\n      data-active={String(Boolean(isActive))}\n      data-loading={String(Boolean(loading))}\n      disabled={loading}\n      variant=\"fab\"\n      {...props}\n    >\n      <div className=\"flex items-center gap-3 [&_svg]:size-7\">{children}</div>\n\n      {loading && (\n        <CircularProgress\n          className={cn(\n            'size-6 transition-opacity',\n            'group-data-[loading=false]:opacity-0 group-data-[loading=true]:opacity-100',\n          )}\n          indeterminate\n        />\n      )}\n    </Button>\n  )\n}\n\nexport const SystemProxyButton = (\n  props: Omit<ButtonProps, 'children' | 'loading'>,\n) => {\n  const systemProxy = useSetting('enable_system_proxy')\n\n  const { execute, isPending } = useBlockTask('system-proxy', async () => {\n    await systemProxy.upsert(!systemProxy.value)\n  })\n\n  return (\n    <ProxyButton\n      {...props}\n      loading={isPending}\n      onClick={execute}\n      isActive={Boolean(systemProxy.value)}\n    >\n      <NetworkPing />\n      <span>{m.settings_system_proxy_system_proxy_label()}</span>\n    </ProxyButton>\n  )\n}\n\nexport const TunModeButton = (\n  props: Omit<ButtonProps, 'children' | 'loading'>,\n) => {\n  const tunMode = useSetting('enable_tun_mode')\n\n  const { execute, isPending } = useBlockTask('tun-mode', async () => {\n    await tunMode.upsert(!tunMode.value)\n  })\n\n  return (\n    <ProxyButton\n      {...props}\n      loading={isPending}\n      onClick={execute}\n      isActive={Boolean(tunMode.value)}\n    >\n      <SettingsEthernet />\n      <span>{m.settings_system_proxy_tun_mode_label()}</span>\n    </ProxyButton>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/ui/animated-item.tsx",
    "content": "import { motion } from 'framer-motion'\nimport { ComponentProps } from 'react'\nimport { cn } from '@nyanpasu/ui'\n\nexport function AnimatedItem({\n  className,\n  ...props\n}: ComponentProps<typeof motion.div>) {\n  return (\n    <motion.div\n      className={cn('overflow-hidden', className)}\n      initial={{\n        height: 0,\n        opacity: 0,\n      }}\n      animate={{\n        height: 'auto',\n        opacity: 1,\n      }}\n      exit={{\n        height: 0,\n        opacity: 0,\n      }}\n      transition={{\n        height: {\n          duration: 0.2,\n          ease: 'easeInOut',\n        },\n        opacity: {\n          duration: 0.15,\n        },\n      }}\n      {...props}\n    />\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/ui/border-beam.tsx",
    "content": "import { motion, MotionStyle, Transition } from 'framer-motion'\nimport { ComponentProps } from 'react'\nimport { cn } from '@nyanpasu/ui'\n\nexport default function BorderBeam({\n  className,\n  size = 50,\n  delay = 0,\n  duration = 6,\n  transition,\n  style,\n  reverse = false,\n  initialOffset = 0,\n}: ComponentProps<typeof motion.div> & {\n  size?: number\n  duration?: number\n  delay?: number\n  transition?: Transition\n  className?: string\n  reverse?: boolean\n  initialOffset?: number\n}) {\n  return (\n    <div\n      className={cn(\n        'pointer-events-none absolute inset-0 rounded-[inherit]',\n        'border-2 border-transparent',\n        'mask-[linear-gradient(transparent,transparent),linear-gradient(#000,#000)]',\n        'mask-intersect [mask-clip:padding-box,border-box]',\n      )}\n    >\n      <motion.div\n        className={cn(\n          'absolute aspect-square border-2 border-transparent',\n          'from-primary-main via-primary-container bg-linear-to-l to-transparent',\n          className,\n        )}\n        style={\n          {\n            width: size,\n            offsetPath: `rect(0 auto auto 0 round ${size}px)`,\n            ...style,\n          } as MotionStyle\n        }\n        initial={{ offsetDistance: `${initialOffset}%` }}\n        animate={{\n          offsetDistance: reverse\n            ? [`${100 - initialOffset}%`, `${-initialOffset}%`]\n            : [`${initialOffset}%`, `${100 + initialOffset}%`],\n        }}\n        transition={{\n          repeat: Infinity,\n          ease: 'linear',\n          duration,\n          delay: -delay,\n          ...transition,\n        }}\n      />\n    </div>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/ui/button.tsx",
    "content": "import { cva, type VariantProps } from 'class-variance-authority'\nimport { AnimatePresence, motion } from 'framer-motion'\nimport { Slot } from 'radix-ui'\nimport { lazy, Suspense, useCallback } from 'react'\nimport { chains } from '@/utils/chain'\nimport { cn } from '@nyanpasu/ui'\nimport { CircularProgress } from './progress'\nimport { useRipple } from './ripple'\n\nexport const buttonVariants = cva(\n  [\n    'cursor-pointer select-none',\n    'focus:outline-hidden',\n    'relative overflow-hidden',\n    'h-10 text-sm font-medium',\n    'rounded-full',\n    'transition-[background-color,color,shadow,filter]',\n  ],\n  {\n    variants: {\n      variant: {\n        basic: [\n          'px-4',\n          'text-primary dark:text-primary',\n          'bg-transparent-fallback-surface dark:bg-transparent-fallback-surface-variant',\n          'hover:bg-primary-container dark:hover:bg-surface-variant',\n        ],\n        raised: [\n          'px-6',\n          'text-primary dark:text-on-surface',\n          'shadow-xs hover:shadow-sm focus:shadow-sm',\n          'bg-surface',\n          'hover:bg-surface-variant',\n        ],\n        stroked: [\n          'px-6',\n          'text-primary',\n          'border border-primary',\n          'bg-transparent-fallback-surface dark:bg-transparent-fallback-surface-variant',\n          'hover:bg-primary-container dark:hover:bg-surface-variant',\n        ],\n        flat: [\n          'px-6',\n          'text-surface dark:text-on-surface',\n          'bg-primary dark:bg-primary-container',\n          'dark:hover:bg-on-primary',\n        ],\n        fab: [\n          'px-4 h-14',\n          'rounded-2xl',\n          'shadow-sm',\n          'text-on-primary-container dark:text-on-primary-container',\n          'bg-primary-container dark:bg-on-primary',\n          'hover:shadow-md',\n          'hover:brightness-95 dark:hover:brightness-105',\n        ],\n      },\n      disabled: {\n        true: 'cursor-not-allowed shadow-none hover:shadow-none focus:shadow-none',\n        false: '',\n      },\n      icon: {\n        true: 'p-0 grid place-content-center',\n        false: 'min-w-16',\n      },\n    },\n    compoundVariants: [\n      {\n        variant: 'basic',\n        disabled: true,\n        className: 'text-zinc-900/40 hover:bg-transparent',\n      },\n      {\n        variant: 'raised',\n        disabled: true,\n        className: 'bg-gray-900/20 text-zinc-900/40 hover:bg-gray-900/20',\n      },\n      {\n        variant: 'stroked',\n        disabled: true,\n        className: 'text-zinc-900/40 hover:bg-transparent border-zinc-300',\n      },\n      {\n        variant: 'flat',\n        disabled: true,\n        className: 'bg-gray-900/20 text-gray-900/40 hover:bg-primary',\n      },\n      {\n        variant: 'fab',\n        disabled: true,\n        className:\n          'bg-gray-900/20 text-gray-900/40 hover:brightness-100 hover:shadow-container-xl',\n      },\n      {\n        icon: true,\n        className: 'w-10',\n      },\n      {\n        variant: 'fab',\n        icon: true,\n        className: 'w-14',\n      },\n    ],\n    defaultVariants: {\n      variant: 'basic',\n      disabled: false,\n      icon: false,\n    },\n  },\n)\n\nexport type ButtonVariantsProps = VariantProps<typeof buttonVariants>\n\nconst LazyRipple = lazy(() =>\n  import('./ripple').then((mod) => ({ default: mod.Ripple })),\n)\n\nexport interface ButtonProps\n  extends\n    Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'disabled'>,\n    ButtonVariantsProps {\n  asChild?: boolean\n  loading?: boolean\n}\n\nexport const Button = ({\n  loading,\n  asChild,\n  variant,\n  disabled,\n  icon,\n  className,\n  children,\n  onClick,\n  ...props\n}: ButtonProps) => {\n  const Comp = asChild ? Slot.Root : 'button'\n\n  const ripple = useRipple()\n\n  const handleClick = disabled ? undefined : chains(onClick, ripple.onClick)\n\n  const handleClear = useCallback(\n    (key: React.Key) => {\n      ripple.onClear(key)\n    },\n    [ripple],\n  )\n\n  return (\n    <Comp\n      className={cn(\n        buttonVariants({\n          variant,\n          disabled,\n          icon,\n        }),\n        className,\n      )}\n      onClick={handleClick}\n      data-loading={String(Boolean(loading))}\n      {...props}\n    >\n      <Slot.Slottable>{children}</Slot.Slottable>\n\n      <AnimatePresence initial={false}>\n        {loading && (\n          <motion.span\n            className={cn(\n              'absolute inset-0 z-10 flex h-full w-full cursor-wait items-center justify-center',\n              'bg-inherit-allow-fallback',\n            )}\n            data-slot=\"button-loading\"\n            initial={{ opacity: 0 }}\n            animate={{ opacity: 1 }}\n            exit={{ opacity: 0 }}\n          >\n            <CircularProgress className=\"size-3/5\" indeterminate />\n          </motion.span>\n        )}\n      </AnimatePresence>\n\n      <Suspense>\n        {ripple && !loading && !disabled && (\n          <LazyRipple ripples={ripple.ripples} onClear={handleClear} />\n        )}\n      </Suspense>\n    </Comp>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/ui/card.tsx",
    "content": "import { cva, type VariantProps } from 'class-variance-authority'\nimport { Slot } from 'radix-ui'\nimport { createContext, HTMLAttributes, useContext } from 'react'\nimport { cn } from '@nyanpasu/ui'\n\nexport const cardVariants = cva('rounded-3xl text-on-surface overflow-hidden', {\n  variants: {\n    variant: {\n      basic: ['shadow-sm', 'bg-surface dark:bg-surface'],\n      raised: ['shadow-sm', 'bg-primary-container dark:bg-on-primary'],\n      outline: [\n        'bg-surface dark:bg-surface',\n        'border border-outline-variant dark:border-outline-variant',\n      ],\n    },\n  },\n  defaultVariants: {\n    variant: 'basic',\n  },\n})\n\nexport type CardVariantsProps = VariantProps<typeof cardVariants>\n\nexport const cardContentVariants = cva(['flex flex-col gap-4 p-4'])\n\nexport type CardContentVariantsProps = VariantProps<typeof cardContentVariants>\n\nexport const cardHeaderVariants = cva(\n  ['flex items-center gap-4 text-xl', 'px-4'],\n  {\n    variants: {\n      variant: {\n        basic: 'border-surface-variant dark:border-surface-variant',\n        raised: 'border-inverse-primary dark:border-primary-container',\n        outline: 'border-outline-variant dark:border-outline-variant',\n      },\n      divider: {\n        true: 'border-b py-4 ',\n        false: 'pt-4',\n      },\n    },\n    defaultVariants: {\n      divider: false,\n      variant: 'basic',\n    },\n  },\n)\n\nexport type CardHeaderVariantsProps = VariantProps<typeof cardHeaderVariants>\n\nexport const cardFooterVariants = cva(\n  ['flex flex-row-reverse items-center gap-4', 'px-2'],\n  {\n    variants: {\n      variant: {\n        basic: 'border-surface-variant dark:border-surface-variant',\n        raised: 'border-inverse-primary dark:border-primary-container',\n        outline: 'border-outline-variant dark:border-outline-variant',\n      },\n      divider: {\n        true: 'border-t py-2',\n        false: 'pb-2',\n      },\n    },\n    defaultVariants: {\n      divider: false,\n      variant: 'basic',\n    },\n  },\n)\n\nexport type CardFooterVariantsProps = VariantProps<typeof cardFooterVariants>\n\ntype CardContextType = {\n  variant: CardVariantsProps['variant']\n  divider: CardHeaderVariantsProps['divider'] &\n    CardFooterVariantsProps['divider']\n}\n\nconst CardContext = createContext<CardContextType | null>(null)\n\nconst useCardContext = () => {\n  const context = useContext(CardContext)\n\n  if (!context) {\n    throw new Error('useCardContext must be used within a CardProvider')\n  }\n\n  return context\n}\n\nexport interface CardProps\n  extends\n    HTMLAttributes<HTMLDivElement>,\n    CardVariantsProps,\n    Partial<CardContextType> {\n  asChild?: boolean\n}\n\nexport const Card = ({\n  variant,\n  divider,\n  asChild,\n  className,\n  ...props\n}: CardProps) => {\n  const Comp = asChild ? Slot.Root : 'div'\n\n  return (\n    <CardContext.Provider\n      value={{\n        variant,\n        divider,\n      }}\n    >\n      <Comp\n        className={cn(\n          cardVariants({\n            variant,\n          }),\n          className,\n        )}\n        {...props}\n      />\n    </CardContext.Provider>\n  )\n}\n\nexport type CardContentProps = HTMLAttributes<HTMLDivElement> &\n  CardContentVariantsProps & {\n    asChild?: boolean\n  }\n\nexport const CardContent = ({\n  className,\n  asChild,\n  ...props\n}: CardContentProps) => {\n  const Comp = asChild ? Slot.Root : 'div'\n\n  return <Comp className={cn(cardContentVariants(), className)} {...props} />\n}\n\nexport type CardHeaderProps = HTMLAttributes<HTMLDivElement> &\n  CardHeaderVariantsProps & {\n    asChild?: boolean\n  }\n\nexport const CardHeader = ({\n  divider,\n  variant,\n  className,\n  ...props\n}: CardHeaderProps) => {\n  const context = useCardContext()\n\n  return (\n    <div\n      className={cn(\n        cardHeaderVariants({\n          divider: context?.divider ?? divider,\n          variant: context?.variant ?? variant,\n        }),\n        className,\n      )}\n      {...props}\n    />\n  )\n}\n\nexport interface CardFooterProps\n  extends HTMLAttributes<HTMLDivElement>, CardFooterVariantsProps {}\n\nexport const CardFooter = ({\n  divider,\n  variant,\n  className,\n  ...props\n}: CardFooterProps) => {\n  const context = useCardContext()\n\n  return (\n    <div\n      className={cn(\n        cardFooterVariants({\n          divider: context?.divider ?? divider,\n          variant: context?.variant ?? variant,\n        }),\n        className,\n      )}\n      {...props}\n    />\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/ui/circle.tsx",
    "content": "import { ComponentProps } from 'react'\nimport { cn } from '@nyanpasu/ui'\n\nconst BASE_STROKE_WIDTH = 10\nconst BASE_SIZE = 100\n\nconst getCircleRefence = (value: number) => {\n  const radius = (BASE_SIZE - BASE_STROKE_WIDTH) / 2\n  const strokeDasharray = 2 * Math.PI * radius\n  const strokeDashoffset = (strokeDasharray * (100 - value)) / 100\n\n  return {\n    radius,\n    strokeDasharray,\n    strokeDashoffset,\n  }\n}\n\nexport function Circle({\n  value,\n  className,\n  style,\n  ...props\n}: ComponentProps<'circle'> & {\n  value: number\n}) {\n  const { strokeDasharray, strokeDashoffset } = getCircleRefence(value)\n\n  return (\n    <circle\n      className={cn('fill-transparent stroke-current stroke-[10%]', className)}\n      cx=\"50%\"\n      cy=\"50%\"\n      r=\"45\"\n      style={{\n        strokeDasharray: `${strokeDasharray}px`,\n        strokeDashoffset: `${strokeDashoffset}px`,\n        ...style,\n      }}\n      {...props}\n    />\n  )\n}\n\nexport function CircleSVG({ className, ...props }: ComponentProps<'svg'>) {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      className={cn(\n        'absolute top-0 left-0 h-full w-full stroke-current',\n        className,\n      )}\n      focusable=\"false\"\n      viewBox={`0 0 ${BASE_SIZE} ${BASE_SIZE}`}\n      {...props}\n    />\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/ui/context-menu.tsx",
    "content": "import ArrowRight from '~icons/material-symbols/arrow-right-rounded'\nimport Check from '~icons/material-symbols/check-rounded'\nimport { AnimatePresence, motion } from 'framer-motion'\nimport { ContextMenu as ContextMenuPrimitive } from 'radix-ui'\nimport { ComponentProps, createContext, useContext } from 'react'\nimport { cn } from '@nyanpasu/ui'\nimport { useControllableState } from '@radix-ui/react-use-controllable-state'\n\nconst MotionContent = ({\n  children,\n  className,\n  style,\n  ...props\n}: ComponentProps<typeof motion.div>) => {\n  return (\n    <motion.div\n      className={cn(\n        'relative z-50 w-full overflow-auto rounded-md',\n        'dark:text-on-surface',\n        'bg-mixed-background/30',\n        'backdrop-blur-xl',\n        'dark:shadow-inverse-on-surface/50 shadow-inverse-surface/30 shadow-sm',\n        'border-outline-variant/50 dark:border-outline-variant/50 border',\n        className,\n      )}\n      style={{\n        ...style,\n        maxHeight: 'var(--radix-context-menu-content-available-height)',\n        transformOrigin:\n          'var(--radix-context-menu-content-transform-origin, top left)',\n      }}\n      initial={{\n        opacity: 0,\n        scale: 0.9,\n      }}\n      animate={{\n        opacity: 1,\n        scale: 1,\n      }}\n      exit={{\n        opacity: 0,\n      }}\n      transition={{\n        type: 'spring',\n        bounce: 0,\n        duration: 0.35,\n      }}\n      {...props}\n    >\n      {children}\n    </motion.div>\n  )\n}\n\nconst ContextMenuContext = createContext<{\n  open: boolean\n} | null>(null)\n\nconst useContextMenuContext = () => {\n  const context = useContext(ContextMenuContext)\n\n  if (context === null) {\n    throw new Error(\n      'ContextMenu compound components cannot be rendered outside the ContextMenu component',\n    )\n  }\n\n  return context\n}\n\nexport const ContextMenu = ({\n  open: inputOpen,\n  onOpenChange,\n  ...props\n}: ComponentProps<typeof ContextMenuPrimitive.Root> & {\n  open?: boolean\n}) => {\n  const [open, setOpen] = useControllableState({\n    prop: inputOpen,\n    defaultProp: false,\n    onChange: onOpenChange,\n  })\n\n  return (\n    <ContextMenuContext.Provider value={{ open }}>\n      <ContextMenuPrimitive.Root {...props} onOpenChange={setOpen} />\n    </ContextMenuContext.Provider>\n  )\n}\n\nexport const ContextMenuTrigger = ContextMenuPrimitive.Trigger\n\nexport const ContextMenuGroup = ContextMenuPrimitive.Group\n\nexport const ContextMenuPortal = ContextMenuPrimitive.Portal\n\nconst ContextMenuSubContext = createContext<{\n  open: boolean\n} | null>(null)\n\nconst useContextMenuSubContext = () => {\n  const context = useContext(ContextMenuSubContext)\n\n  if (context === null) {\n    throw new Error(\n      'ContextMenuSub compound components cannot be rendered outside the ContextMenuSub component',\n    )\n  }\n\n  return context\n}\n\nexport const ContextMenuSub = ({\n  open: inputOpen,\n  defaultOpen,\n  onOpenChange,\n  children,\n  ...props\n}: ComponentProps<typeof ContextMenuPrimitive.Sub>) => {\n  const [open, setOpen] = useControllableState({\n    prop: inputOpen,\n    defaultProp: defaultOpen ?? false,\n    onChange: onOpenChange,\n  })\n\n  return (\n    <ContextMenuSubContext.Provider value={{ open }}>\n      <ContextMenuPrimitive.Sub {...props} open={open} onOpenChange={setOpen}>\n        {children}\n      </ContextMenuPrimitive.Sub>\n    </ContextMenuSubContext.Provider>\n  )\n}\n\nexport function ContextMenuSubTrigger({\n  children,\n  className,\n  ...props\n}: ComponentProps<typeof ContextMenuPrimitive.SubTrigger>) {\n  return (\n    <ContextMenuPrimitive.SubTrigger\n      className={cn(\n        'flex h-9 cursor-default items-center justify-between gap-2 px-3 outline-hidden',\n        'cursor-pointer',\n        'hover:bg-surface-variant',\n        'dark:hover:bg-surface-variant',\n        'data-[state=open]:bg-surface-variant/30',\n        'dark:data-[state=open]:bg-surface-variant/30',\n        className,\n      )}\n      {...props}\n    >\n      {children}\n\n      <ArrowRight className=\"text-outline-variant dark:text-outline size-5\" />\n    </ContextMenuPrimitive.SubTrigger>\n  )\n}\n\nexport function ContextMenuSubContent({\n  children,\n  className,\n  ...props\n}: ComponentProps<typeof ContextMenuPrimitive.SubContent>) {\n  const { open } = useContextMenuSubContext()\n\n  return (\n    <AnimatePresence initial={false}>\n      {open && (\n        <ContextMenuPortal forceMount>\n          <ContextMenuPrimitive.SubContent {...props} asChild>\n            <MotionContent className={className}>{children}</MotionContent>\n          </ContextMenuPrimitive.SubContent>\n        </ContextMenuPortal>\n      )}\n    </AnimatePresence>\n  )\n}\n\nexport const ContextMenuContent = ({\n  children,\n  className,\n  ...props\n}: ComponentProps<typeof ContextMenuPrimitive.Content>) => {\n  const { open } = useContextMenuContext()\n\n  return (\n    <AnimatePresence initial={false}>\n      {open && (\n        <ContextMenuPrimitive.Portal forceMount>\n          <ContextMenuPrimitive.Content {...props} asChild>\n            <MotionContent\n              className={cn('min-w-40', className)}\n              onContextMenu={(e) => {\n                e.preventDefault()\n              }}\n            >\n              {children}\n            </MotionContent>\n          </ContextMenuPrimitive.Content>\n        </ContextMenuPrimitive.Portal>\n      )}\n    </AnimatePresence>\n  )\n}\n\nexport const ContextMenuItem = ({\n  className,\n  ...props\n}: ComponentProps<typeof ContextMenuPrimitive.Item>) => {\n  return (\n    <ContextMenuPrimitive.Item\n      data-disabled={String(props.disabled)}\n      className={cn(\n        'flex h-9 cursor-default items-center gap-2 px-3 text-sm outline-hidden',\n        'cursor-pointer',\n        'data-[disabled=false]:hover:bg-surface-variant/70',\n        'data-[disabled=false]:dark:hover:bg-surface-variant/50',\n        'data-[disabled=true]:text-on-surface/50',\n        'data-[disabled=true]:dark:text-on-surface/50',\n        'data-[disabled=true]:cursor-default',\n        className,\n      )}\n      {...props}\n    />\n  )\n}\n\nexport const ContextMenuCheckboxItem = ({\n  children,\n  className,\n  ...props\n}: ComponentProps<typeof ContextMenuPrimitive.CheckboxItem>) => {\n  return (\n    <ContextMenuPrimitive.CheckboxItem\n      className={cn(\n        'flex h-9 cursor-default items-center justify-between gap-2 px-3 text-sm outline-hidden',\n        'cursor-pointer',\n        'hover:bg-surface-variant',\n        'dark:hover:bg-surface-variant',\n        'data-[state=checked]:bg-primary-container dark:data-[state=checked]:bg-on-primary',\n        className,\n      )}\n      {...props}\n    >\n      {children}\n\n      <ContextMenuPrimitive.ItemIndicator>\n        <Check className=\"text-primary size-5\" />\n      </ContextMenuPrimitive.ItemIndicator>\n    </ContextMenuPrimitive.CheckboxItem>\n  )\n}\n\nexport const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup\n\nexport const ContextMenuRadioItem = ({\n  children,\n  className,\n  ...props\n}: ComponentProps<typeof ContextMenuPrimitive.RadioItem>) => {\n  return (\n    <ContextMenuPrimitive.RadioItem\n      className={cn(\n        'flex h-9 cursor-default items-center justify-between gap-2 px-3 text-sm outline-hidden',\n        'cursor-pointer',\n        'hover:bg-surface-variant',\n        'dark:hover:bg-surface-variant',\n        'data-[state=checked]:bg-primary-container dark:data-[state=checked]:bg-on-primary',\n        className,\n      )}\n      {...props}\n    >\n      {children}\n\n      <ContextMenuPrimitive.ItemIndicator>\n        <Check className=\"text-primary size-5\" />\n      </ContextMenuPrimitive.ItemIndicator>\n    </ContextMenuPrimitive.RadioItem>\n  )\n}\n\nexport const ContextMenuLabel = ({\n  className,\n  ...props\n}: ComponentProps<typeof ContextMenuPrimitive.Label>) => {\n  return (\n    <ContextMenuPrimitive.Label\n      className={cn(\n        'text-outline-variant flex h-9 cursor-default items-center gap-2 px-3 text-xs font-medium outline-hidden',\n        className,\n      )}\n      {...props}\n    />\n  )\n}\n\nexport const ContextMenuSeparator = ({\n  className,\n  ...props\n}: ComponentProps<typeof ContextMenuPrimitive.Separator>) => {\n  return (\n    <ContextMenuPrimitive.Separator\n      className={cn('bg-outline-variant/50 h-px', className)}\n      {...props}\n    />\n  )\n}\n\nexport const ContextMenuShortcut = ({\n  className,\n  ...props\n}: ComponentProps<'span'>) => {\n  return (\n    <span\n      className={cn(\n        'text-outline-variant ml-auto text-xs tracking-widest',\n        className,\n      )}\n      {...props}\n    />\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/ui/dropdown-menu.tsx",
    "content": "import ArrowRight from '~icons/material-symbols/arrow-right-rounded'\nimport Check from '~icons/material-symbols/check-rounded'\nimport RadioChecked from '~icons/material-symbols/radio-button-checked'\nimport Radio from '~icons/material-symbols/radio-button-unchecked'\nimport { AnimatePresence, motion } from 'framer-motion'\nimport { DropdownMenu as DropdownMenuPrimitive } from 'radix-ui'\nimport { ComponentProps, createContext, useContext } from 'react'\nimport { cn } from '@nyanpasu/ui'\nimport { useControllableState } from '@radix-ui/react-use-controllable-state'\n\nconst MotionContent = ({\n  children,\n  className,\n  ...props\n}: ComponentProps<typeof motion.div>) => {\n  return (\n    <motion.div\n      className={cn(\n        'relative z-50 w-full overflow-auto rounded',\n        'dark:text-on-surface',\n        'bg-inverse-on-surface dark:bg-surface',\n        'shadow shadow-zinc-300 dark:shadow-zinc-900',\n        className,\n      )}\n      style={{\n        maxHeight: 'var(--radix-popper-available-height)',\n      }}\n      initial={{\n        opacity: 0,\n        scaleY: 0.9,\n        transformOrigin: 'top',\n      }}\n      animate={{\n        opacity: 1,\n        scaleY: 1,\n        transformOrigin: 'top',\n      }}\n      exit={{\n        opacity: 0,\n        scaleY: 0.9,\n        transformOrigin: 'top',\n      }}\n      transition={{\n        type: 'spring',\n        bounce: 0,\n        duration: 0.35,\n      }}\n      {...props}\n    >\n      {children}\n    </motion.div>\n  )\n}\n\nconst DropdownMenuContext = createContext<{\n  open: boolean\n} | null>(null)\n\nconst useDropdownMenuContext = () => {\n  const context = useContext(DropdownMenuContext)\n\n  if (context === null) {\n    throw new Error(\n      'DropdownMenu compound components cannot be rendered outside the DropdownMenu component',\n    )\n  }\n\n  return context\n}\n\nexport const DropdownMenu = ({\n  open: inputOpen,\n  defaultOpen,\n  onOpenChange,\n  ...props\n}: ComponentProps<typeof DropdownMenuPrimitive.Root>) => {\n  const [open, setOpen] = useControllableState({\n    prop: inputOpen,\n    defaultProp: defaultOpen ?? false,\n    onChange: onOpenChange,\n  })\n\n  return (\n    <DropdownMenuContext.Provider value={{ open }}>\n      <DropdownMenuPrimitive.Root\n        {...props}\n        open={open}\n        onOpenChange={setOpen}\n      />\n    </DropdownMenuContext.Provider>\n  )\n}\n\nexport const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger\n\nexport const DropdownMenuGroup = DropdownMenuPrimitive.Group\n\nexport const DropdownMenuPortal = DropdownMenuPrimitive.Portal\n\nconst DropdownMenuSubContext = createContext<{\n  open: boolean\n} | null>(null)\n\nconst useDropdownMenuSubContext = () => {\n  const context = useContext(DropdownMenuSubContext)\n\n  if (context === null) {\n    throw new Error(\n      'DropdownMenuSub compound components cannot be rendered outside the DropdownMenuSub component',\n    )\n  }\n\n  return context\n}\n\nexport const DropdownMenuSub = ({\n  open: inputOpen,\n  defaultOpen,\n  onOpenChange,\n  children,\n  ...props\n}: ComponentProps<typeof DropdownMenuPrimitive.Sub>) => {\n  const [open, setOpen] = useControllableState({\n    prop: inputOpen,\n    defaultProp: defaultOpen ?? false,\n    onChange: onOpenChange,\n  })\n\n  return (\n    <DropdownMenuSubContext.Provider\n      value={{\n        open,\n      }}\n    >\n      <DropdownMenuPrimitive.Sub {...props} open={open} onOpenChange={setOpen}>\n        {children}\n      </DropdownMenuPrimitive.Sub>\n    </DropdownMenuSubContext.Provider>\n  )\n}\n\nexport function DropdownMenuSubTrigger({\n  children,\n  className,\n  ...props\n}: ComponentProps<typeof DropdownMenuPrimitive.SubTrigger>) {\n  return (\n    <DropdownMenuPrimitive.SubTrigger\n      className={cn(\n        'flex h-12 cursor-default items-center justify-between gap-2 p-4 pr-2 outline-hidden',\n        'cursor-pointer',\n        'hover:bg-surface-variant',\n        'dark:hover:bg-surface-variant',\n        'data-[state=open]:bg-surface-variant/30',\n        'dark:data-[state=open]:bg-surface-variant/30',\n        className,\n      )}\n      {...props}\n    >\n      {children}\n\n      <ArrowRight className=\"text-outline-variant dark:text-outline size-6\" />\n    </DropdownMenuPrimitive.SubTrigger>\n  )\n}\n\nexport function DropdownMenuSubContent({\n  children,\n  className,\n  ...props\n}: ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {\n  const { open } = useDropdownMenuSubContext()\n\n  return (\n    <AnimatePresence initial={false}>\n      {open && (\n        <DropdownMenuPortal forceMount>\n          <DropdownMenuPrimitive.SubContent {...props} asChild>\n            <MotionContent className={className}>{children}</MotionContent>\n          </DropdownMenuPrimitive.SubContent>\n        </DropdownMenuPortal>\n      )}\n    </AnimatePresence>\n  )\n}\n\nconst DropdownMenuRadioGroupContext = createContext<{\n  value: string | null\n}>({ value: null })\n\nconst useDropdownMenuRadioGroupContext = () => {\n  const context = useContext(DropdownMenuRadioGroupContext)\n\n  if (context === undefined) {\n    throw new Error(\n      'DropdownMenuRadioGroup compound components cannot be rendered outside the DropdownMenuRadioGroup component',\n    )\n  }\n\n  return context\n}\n\nexport const DropdownMenuRadioGroup = ({\n  value: inputValue,\n  defaultValue,\n  onValueChange,\n  ...props\n}: ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) => {\n  const [value, setValue] = useControllableState({\n    prop: inputValue,\n    defaultProp: String(defaultValue),\n    onChange: onValueChange,\n  })\n\n  return (\n    <DropdownMenuRadioGroupContext.Provider value={{ value }}>\n      <DropdownMenuPrimitive.RadioGroup\n        {...props}\n        value={value}\n        onValueChange={setValue}\n      />\n    </DropdownMenuRadioGroupContext.Provider>\n  )\n}\n\nexport const DropdownMenuContent = ({\n  children,\n  className,\n  ...props\n}: ComponentProps<typeof DropdownMenuPrimitive.Content>) => {\n  const { open } = useDropdownMenuContext()\n\n  return (\n    <AnimatePresence initial={false}>\n      {open && (\n        <DropdownMenuPrimitive.Portal forceMount>\n          <DropdownMenuPrimitive.Content {...props} asChild>\n            <MotionContent className={className}>{children}</MotionContent>\n          </DropdownMenuPrimitive.Content>\n        </DropdownMenuPrimitive.Portal>\n      )}\n    </AnimatePresence>\n  )\n}\n\nexport const DropdownMenuItem = ({\n  className,\n  ...props\n}: ComponentProps<typeof DropdownMenuPrimitive.Item>) => {\n  return (\n    <DropdownMenuPrimitive.Item\n      className={cn(\n        'flex h-12 cursor-default items-center justify-between gap-2 p-4 outline-hidden',\n        'cursor-pointer',\n        'hover:bg-surface-variant',\n        'dark:hover:bg-surface-variant',\n        className,\n      )}\n      {...props}\n    />\n  )\n}\n\nexport const DropdownMenuCheckboxItem = ({\n  children,\n  className,\n  ...props\n}: ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) => {\n  return (\n    <DropdownMenuPrimitive.CheckboxItem\n      className={cn(\n        'flex h-12 cursor-default items-center justify-between gap-2 p-4 outline-hidden',\n        'cursor-pointer',\n        'hover:bg-surface-variant',\n        'dark:hover:bg-surface-variant',\n        'data-[state=checked]:bg-primary-container dark:data-[state=checked]:bg-on-primary',\n        className,\n      )}\n      {...props}\n    >\n      {children}\n\n      <DropdownMenuPrimitive.ItemIndicator>\n        <Check className=\"text-primary\" />\n      </DropdownMenuPrimitive.ItemIndicator>\n    </DropdownMenuPrimitive.CheckboxItem>\n  )\n}\n\nexport const DropdownMenuRadioItem = ({\n  value,\n  children,\n  className,\n  ...props\n}: ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) => {\n  const context = useDropdownMenuRadioGroupContext()\n\n  const selected = context.value === value\n\n  return (\n    <DropdownMenuPrimitive.RadioItem\n      className={cn(\n        'flex h-12 cursor-default items-center justify-between gap-2 p-4 outline-hidden',\n        'cursor-pointer',\n        'hover:bg-surface-variant',\n        'dark:hover:bg-surface-variant',\n        'data-[state=checked]:bg-primary-container dark:data-[state=checked]:bg-on-primary',\n        className,\n      )}\n      value={value}\n      {...props}\n    >\n      <DropdownMenuPrimitive.ItemIndicator>\n        <RadioChecked className=\"text-primary\" />\n      </DropdownMenuPrimitive.ItemIndicator>\n\n      {!selected && (\n        <span>\n          <Radio className=\"text-outline-variant dark:text-outline\" />\n        </span>\n      )}\n\n      <div className=\"flex-1\">{children}</div>\n    </DropdownMenuPrimitive.RadioItem>\n  )\n}\n\nexport const DropdownMenuLabel = ({\n  className,\n  ...props\n}: ComponentProps<typeof DropdownMenuPrimitive.Label>) => {\n  return (\n    <DropdownMenuPrimitive.Label\n      className={cn(\n        'flex h-12 cursor-default items-center justify-between gap-2 p-4 outline-hidden',\n        className,\n      )}\n      {...props}\n    />\n  )\n}\n\nexport const DropdownMenuSeparator = ({\n  className,\n  ...props\n}: ComponentProps<typeof DropdownMenuPrimitive.Separator>) => {\n  return (\n    <DropdownMenuPrimitive.Separator\n      className={cn('bg-outline-variant/50 h-px', className)}\n      {...props}\n    />\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/ui/file-drop-zone.tsx",
    "content": "import { cva, type VariantProps } from 'class-variance-authority'\nimport {\n  ChangeEvent,\n  ComponentProps,\n  createContext,\n  DragEvent,\n  RefObject,\n  useContext,\n  useEffect,\n  useRef,\n  useState,\n} from 'react'\nimport getSystem from '@/utils/get-system'\nimport { cn } from '@nyanpasu/ui'\nimport { readTextFile } from '@tauri-apps/plugin-fs'\n\nconst isWin = getSystem() === 'windows'\n\nconst FileDropZoneContext = createContext<{\n  isDragging: boolean\n  isLoading: boolean\n  fileName: string | null\n  accept: string[]\n  disabled: boolean\n  fileInputRef: RefObject<HTMLInputElement | null>\n  handleClick: () => void\n} | null>(null)\n\nconst useFileDropZoneContext = () => {\n  const context = useContext(FileDropZoneContext)\n\n  if (!context) {\n    throw new Error('FileDropZone components must be used within FileDropZone')\n  }\n\n  return context\n}\n\nexport const fileDropZoneVariants = cva(\n  [\n    'relative flex min-h-24 flex-col items-center justify-center gap-2',\n    'rounded-md border border-dashed p-4',\n    'transition-colors duration-200',\n    'cursor-pointer',\n    'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary',\n  ],\n  {\n    variants: {\n      variant: {\n        default: [\n          'border-outline-variant',\n          'bg-transparent',\n          'hover:border-primary/50',\n          'hover:bg-surface-variant/30',\n        ],\n        outline: [\n          'border-outline-variant',\n          'bg-surface dark:bg-surface',\n          'hover:border-primary/50',\n          'hover:bg-surface-variant/30',\n        ],\n      },\n      isDragging: {\n        true: 'border-primary bg-primary-container/20',\n        false: '',\n      },\n      disabled: {\n        true: 'cursor-not-allowed opacity-50',\n        false: '',\n      },\n    },\n    compoundVariants: [\n      {\n        disabled: true,\n        className: 'hover:border-outline-variant hover:bg-transparent',\n      },\n    ],\n    defaultVariants: {\n      variant: 'default',\n      isDragging: false,\n      disabled: false,\n    },\n  },\n)\n\nexport type FileDropZoneVariants = VariantProps<typeof fileDropZoneVariants>\n\nexport interface FileDropZoneProps\n  extends\n    Omit<\n      ComponentProps<'div'>,\n      | 'onChange'\n      | 'onDragEnter'\n      | 'onDragLeave'\n      | 'onDragOver'\n      | 'onDrop'\n      | 'onClick'\n    >,\n    FileDropZoneVariants {\n  value?: string | null\n  onChange?: (filePath: string) => void\n  onFileRead?: (content: string) => void\n  accept: string[]\n  disabled?: boolean\n}\n\nexport function FileDropZone({\n  value,\n  onChange,\n  onFileRead,\n  accept,\n  className,\n  disabled = false,\n  variant,\n  children,\n  ...props\n}: FileDropZoneProps) {\n  const [isDragging, setIsDragging] = useState(false)\n\n  const [isLoading, setIsLoading] = useState(false)\n\n  const [fileName, setFileName] = useState<string | null>(\n    value\n      ? ((isWin ? value.split('\\\\').at(-1) : value.split('/').at(-1)) ?? null)\n      : null,\n  )\n  const fileInputRef = useRef<HTMLInputElement>(null)\n\n  // Update fileName when value changes\n  useEffect(() => {\n    if (value) {\n      const name = isWin ? value.split('\\\\').at(-1) : value.split('/').at(-1)\n      setFileName(name || null)\n    } else {\n      setFileName(null)\n    }\n  }, [value])\n\n  const handleFile = async (filePath: string, file?: File) => {\n    if (disabled) return\n\n    try {\n      setIsLoading(true)\n\n      let content: string\n\n      // If file object is provided (from drag & drop), use FileReader\n      // Otherwise, use Tauri's readTextFile API\n      if (file) {\n        content = await new Promise<string>((resolve, reject) => {\n          const reader = new FileReader()\n          reader.onload = (e) => {\n            resolve(e.target?.result as string)\n          }\n          reader.onerror = reject\n          reader.readAsText(file)\n        })\n      } else {\n        content = await readTextFile(filePath)\n      }\n\n      // Read file content if callback is provided\n      if (onFileRead) {\n        onFileRead(content)\n      }\n\n      // Update file path\n      onChange?.(filePath)\n\n      // Extract file name\n      const name =\n        file?.name ||\n        (isWin ? filePath.split('\\\\').at(-1) : filePath.split('/').at(-1))\n      setFileName(name || null)\n    } catch (error) {\n      console.error('Failed to read file:', error)\n    } finally {\n      setIsLoading(false)\n    }\n  }\n\n  const handleDragEnter = (e: DragEvent<HTMLDivElement>) => {\n    if (disabled) return\n    e.preventDefault()\n    e.stopPropagation()\n    setIsDragging(true)\n  }\n\n  const handleDragLeave = (e: DragEvent<HTMLDivElement>) => {\n    if (disabled) return\n    e.preventDefault()\n    e.stopPropagation()\n    setIsDragging(false)\n  }\n\n  const handleDragOver = (e: DragEvent<HTMLDivElement>) => {\n    if (disabled) return\n    e.preventDefault()\n    e.stopPropagation()\n  }\n\n  const handleDrop = async (e: DragEvent<HTMLDivElement>) => {\n    if (disabled) return\n    e.preventDefault()\n    e.stopPropagation()\n    setIsDragging(false)\n\n    const files = e.dataTransfer.files\n    if (files.length === 0) return\n\n    const file = files[0]\n\n    // Check file extension\n    const fileExt = file.name\n      .toLowerCase()\n      .substring(file.name.lastIndexOf('.'))\n    if (!accept.some((ext) => fileExt === ext.toLowerCase())) {\n      console.error('File type not accepted')\n      return\n    }\n\n    // In Tauri, try to get file path from the file object\n    // If not available, use FileReader API\n    const filePath = (file as File & { path?: string }).path as\n      | string\n      | undefined\n\n    if (filePath) {\n      // File path is available (Tauri native drag & drop)\n      await handleFile(filePath, file)\n    } else {\n      // Fallback: use file name as identifier and read content via FileReader\n      // Note: In this case, we use the file name as the path identifier\n      await handleFile(file.name, file)\n    }\n  }\n\n  const handleClick = () => {\n    if (disabled || isLoading) {\n      return\n    }\n\n    fileInputRef.current?.click()\n  }\n\n  const handleFileInputChange = async (e: ChangeEvent<HTMLInputElement>) => {\n    const files = e.target.files\n    if (!files || files.length === 0) return\n\n    const file = files[0]\n\n    // In Tauri, file input may have path property\n    const filePath = (file as File & { path?: string }).path as\n      | string\n      | undefined\n\n    if (filePath) {\n      // File path is available (Tauri file dialog)\n      await handleFile(filePath)\n    } else {\n      // Fallback: use file name and read via FileReader\n      await handleFile(file.name, file)\n    }\n\n    // Reset input\n    if (fileInputRef.current) {\n      fileInputRef.current.value = ''\n    }\n  }\n\n  return (\n    <FileDropZoneContext.Provider\n      value={{\n        isDragging,\n        isLoading,\n        fileName,\n        accept,\n        disabled,\n        fileInputRef,\n        handleClick,\n      }}\n    >\n      <div\n        className={cn(\n          fileDropZoneVariants({\n            variant,\n            isDragging,\n            disabled,\n          }),\n          className,\n        )}\n        data-slot=\"file-drop-zone\"\n        onDragEnter={handleDragEnter}\n        onDragLeave={handleDragLeave}\n        onDragOver={handleDragOver}\n        onDrop={handleDrop}\n        onClick={handleClick}\n        {...props}\n      >\n        <input\n          data-slot=\"file-drop-zone-input\"\n          ref={fileInputRef}\n          type=\"file\"\n          accept={accept.join(',')}\n          className=\"hidden\"\n          onChange={handleFileInputChange}\n          disabled={disabled}\n        />\n\n        {children}\n\n        {/* {isLoading ? (\n          <FileDropZoneLoading />\n        ) : fileName ? (\n          (fileSelected?.(fileName) ?? (\n            <FileDropZoneFileSelected name={fileName} />\n          ))\n        ) : (\n          <FileDropZonePlaceholder accept={accept} />\n        )} */}\n      </div>\n    </FileDropZoneContext.Provider>\n  )\n}\n\nexport function FileDropZoneLoading(props: ComponentProps<'div'>) {\n  const { isLoading } = useFileDropZoneContext()\n\n  if (!isLoading) {\n    return null\n  }\n\n  return <div data-slot=\"file-drop-zone-loading\" {...props} />\n}\n\nexport function FileDropZonePlaceholder(props: ComponentProps<'div'>) {\n  const { isLoading, fileName } = useFileDropZoneContext()\n\n  if (isLoading || fileName) {\n    return null\n  }\n\n  return <div data-slot=\"file-drop-zone-placeholder\" {...props} />\n}\n\nexport function FileDropZoneFileSelected(props: ComponentProps<'div'>) {\n  const { fileName } = useFileDropZoneContext()\n\n  if (!fileName) {\n    return null\n  }\n\n  return <div data-slot=\"file-drop-zone-file-selected\" {...props} />\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/ui/highlight-text.tsx",
    "content": "export default function HighlightText({\n  searchText,\n  className,\n  children,\n}: {\n  searchText: string\n  className?: string\n  children: string\n}) {\n  if (!searchText.trim()) {\n    return <span className={className}>{children}</span>\n  }\n\n  const parts: { text: string; isHighlight: boolean }[] = []\n  const searchLower = searchText.toLowerCase()\n  const textLower = children.toLowerCase()\n\n  let lastIndex = 0\n  let index = textLower.indexOf(searchLower, lastIndex)\n\n  while (index !== -1) {\n    // Add text before match\n    if (index > lastIndex) {\n      parts.push({\n        text: children.slice(lastIndex, index),\n        isHighlight: false,\n      })\n    }\n\n    // Add matched text\n    parts.push({\n      text: children.slice(index, index + searchText.length),\n      isHighlight: true,\n    })\n\n    lastIndex = index + searchText.length\n    index = textLower.indexOf(searchLower, lastIndex)\n  }\n\n  // Add remaining text\n  if (lastIndex < children.length) {\n    parts.push({\n      text: children.slice(lastIndex),\n      isHighlight: false,\n    })\n  }\n\n  return (\n    <span className={className}>\n      {parts.map((part, index) =>\n        part.isHighlight ? (\n          <mark\n            key={index}\n            className=\"rounded bg-yellow-400 px-0.5 text-black dark:bg-yellow-500\"\n          >\n            {part.text}\n          </mark>\n        ) : (\n          <span key={index}>{part.text}</span>\n        ),\n      )}\n    </span>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/ui/image.tsx",
    "content": "import { ComponentProps, useMemo } from 'react'\nimport { useServerPort } from '@nyanpasu/interface'\nimport { LazyImage } from '@nyanpasu/ui'\n\nexport default function Image({\n  icon,\n  ...porps\n}: Omit<ComponentProps<typeof LazyImage>, 'src'> & {\n  icon: string\n}) {\n  const serverPort = useServerPort()\n\n  const src = icon.trim().startsWith('<svg')\n    ? `data:image/svg+xml;base64,${btoa(icon)}`\n    : icon\n\n  const cachedUrl = useMemo(() => {\n    if (!src.startsWith('http')) {\n      return src\n    }\n\n    return `http://localhost:${serverPort}/cache/icon?url=${btoa(src)}`\n  }, [src, serverPort])\n\n  return <LazyImage src={cachedUrl} {...porps} />\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/ui/input.tsx",
    "content": "import { useCreation } from 'ahooks'\nimport { cva, type VariantProps } from 'class-variance-authority'\nimport {\n  ChangeEvent,\n  ComponentProps,\n  createContext,\n  isValidElement,\n  useContext,\n  useEffect,\n  useState,\n} from 'react'\nimport { cn } from '@nyanpasu/ui'\n\nexport const inputContainerVariants = cva(\n  [\n    'group relative box-border inline-flex w-full flex-auto items-baseline',\n    'cursor-pointer',\n    'px-4 py-4 outline-hidden',\n    // TODO: size variants, fix this\n    'flex items-center justify-between h-14',\n    'dark:text-on-surface',\n  ],\n  {\n    variants: {\n      variant: {\n        filled: 'rounded-t bg-surface-variant/30 dark:bg-surface',\n        // outlined use inputLabelFieldsetVariants\n        outlined: '',\n      },\n    },\n    defaultVariants: {\n      variant: 'filled',\n    },\n  },\n)\n\nexport type InputContainerVariants = VariantProps<typeof inputContainerVariants>\n\nexport const inputVariants = cva(\n  [\n    'peer',\n    'w-full border-none p-0',\n    'bg-transparent placeholder-transparent outline-hidden',\n    'transition-[margin] duration-200',\n  ],\n  {\n    variants: {\n      variant: {\n        filled: '',\n        outlined: '',\n      },\n      haveValue: {\n        true: '',\n        false: '',\n      },\n      haveLabel: {\n        true: '',\n        false: '',\n      },\n    },\n    compoundVariants: [\n      {\n        variant: 'filled',\n        haveValue: true,\n        haveLabel: true,\n        className: 'mt-3!',\n      },\n    ],\n    defaultVariants: {\n      variant: 'filled',\n      haveValue: false,\n      haveLabel: false,\n    },\n  },\n)\n\nexport type InputVariants = VariantProps<typeof inputVariants>\n\nexport const inputLabelVariants = cva(\n  [\n    'absolute',\n    'left-4 top-4',\n    'pointer-events-none',\n    'text-base select-none',\n    // TODO: only transition position, not text color\n    'transition-all duration-200',\n  ],\n  {\n    variants: {\n      variant: {\n        filled: [\n          'group-data-[state=open]:top-2 group-data-[state=open]:dark:text-surface',\n          'group-data-[state=open]:text-xs group-data-[state=open]:text-primary',\n        ],\n        outlined: [\n          'group-data-[state=open]:-top-2',\n          'group-data-[state=open]:text-sm',\n          'group-data-[state=open]:text-primary',\n\n          'dark:group-data-[state=open]:text-inverse-primary',\n          'dark:group-data-[state=closed]:text-primary-container',\n\n          // \"before:absolute before:inset-0 before:content-['']\",\n          // \"before:-z-10 before:-mx-1\",\n          // \"before:bg-transparent \",\n          // \"before:inline-block\",\n        ],\n      },\n      focus: {\n        true: '',\n        false: '',\n      },\n    },\n    compoundVariants: [\n      {\n        variant: 'filled',\n        focus: true,\n        className: 'top-2 text-xs',\n      },\n      {\n        variant: 'outlined',\n        focus: true,\n        className: '-top-2 text-sm',\n      },\n    ],\n    defaultVariants: {\n      variant: 'filled',\n      focus: false,\n    },\n  },\n)\n\nexport type InputLabelVariants = VariantProps<typeof inputLabelVariants>\n\nexport const inputLineVariants = cva('', {\n  variants: {\n    variant: {\n      filled: [\n        'absolute inset-x-0 bottom-0 w-full border-b border-b-outline-variant',\n        'transition-all duration-200',\n        // pseudo elements be overlay parent element, will not affect the box size\n        'after:absolute after:inset-x-0 after:bottom-0 after:z-10',\n        \"after:scale-x-0 after:border-b-2 after:opacity-0 after:content-['']\",\n        'after:transition-all after:duration-200',\n        'after:border-primary dark:after:border-on-primary-container',\n        // sync parent group state, state from radix-ui\n        'group-data-[state=open]:border-b-0',\n        'group-data-[state=open]:after:scale-x-100',\n        'group-data-[state=open]:after:opacity-100',\n        'peer-focus:border-b-0',\n        'peer-focus:after:scale-x-100',\n        'peer-focus:after:opacity-100',\n      ],\n      // hidden line for outlined variant\n      outlined: 'hidden',\n    },\n  },\n  defaultVariants: {\n    variant: 'filled',\n  },\n})\n\nexport type InputLineVariants = VariantProps<typeof inputLineVariants>\n\nexport const inputLabelFieldsetVariants = cva('pointer-events-none', {\n  variants: {\n    variant: {\n      // only for outlined variant\n      filled: 'hidden',\n      outlined: [\n        'absolute inset-0 text-left',\n        'rounded transition-all duration-200',\n        // may open border width will be 1.5, idk\n        'group-data-[state=closed]:border',\n        'group-data-[state=open]:border-2',\n        'peer-not-focus:border',\n        'peer-focus:border-2',\n        // different material web border color, i think this looks better\n        'group-data-[state=closed]:border-outline-variant',\n        'group-data-[state=open]:border-primary',\n        'peer-not-focus:border-primary-container',\n        'peer-focus:border-primary',\n        // dark must be prefixed\n        'dark:group-data-[state=closed]:border-outline-variant',\n        'dark:group-data-[state=open]:border-primary-container',\n        'dark:peer-not-focus:border-outline-variant',\n        'dark:peer-focus:border-primary-container',\n      ],\n    },\n  },\n  defaultVariants: {\n    variant: 'filled',\n  },\n})\n\nexport type InputLabelFieldsetVariants = VariantProps<\n  typeof inputLabelFieldsetVariants\n>\n\nexport const inputLabelLegendVariants = cva('', {\n  variants: {\n    variant: {\n      // only for outlined variant\n      filled: 'hidden',\n      outlined: 'invisible ml-2 px-2 text-sm h-0',\n    },\n    haveValue: {\n      true: '',\n      false: '',\n    },\n  },\n  compoundVariants: [\n    {\n      variant: 'outlined',\n      haveValue: false,\n      className: ['group-data-[state=closed]:hidden', 'group-not-focus:hidden'],\n    },\n  ],\n  defaultVariants: {\n    variant: 'filled',\n    haveValue: false,\n  },\n})\n\nexport type InputLabelLegendVariants = VariantProps<\n  typeof inputLabelLegendVariants\n>\n\ntype InputContextType = {\n  haveLabel?: boolean\n  haveValue?: boolean\n} & InputContainerVariants\n\nconst InputContext = createContext<InputContextType | null>(null)\n\nconst useInputContext = () => {\n  const context = useContext(InputContext)\n\n  if (!context) {\n    throw new Error('InputContext is undefined')\n  }\n\n  return context\n}\n\nexport const InputContainer = ({\n  className,\n  ...props\n}: ComponentProps<'div'>) => {\n  const { variant } = useInputContext()\n\n  return (\n    <div\n      className={cn(\n        inputContainerVariants({\n          variant,\n        }),\n        className,\n      )}\n      {...props}\n    />\n  )\n}\n\nexport const InputLine = ({ className, ...props }: ComponentProps<'input'>) => {\n  const { variant } = useInputContext()\n\n  return (\n    <div\n      className={cn(\n        inputLineVariants({\n          variant,\n        }),\n        className,\n      )}\n      {...props}\n    />\n  )\n}\n\nexport type InputProps = ComponentProps<'input'> & {\n  label?: string\n} & InputContainerVariants\n\nexport const Input = ({\n  variant,\n  className,\n  label,\n  children,\n  onChange,\n  ...props\n}: InputProps) => {\n  const [haveValue, setHaveValue] = useState(false)\n\n  const haveLabel = useCreation(() => {\n    if (label) {\n      return true\n    }\n\n    if (isValidElement(children)) {\n      if (typeof children.type !== 'string') {\n        if ('displayName' in children.type) {\n          if (children.type.displayName === InputLabel.displayName) {\n            return true\n          }\n        }\n      }\n    }\n\n    return false\n  }, [])\n\n  useEffect(() => {\n    if (props.value || props.defaultValue) {\n      setHaveValue(true)\n    } else {\n      setHaveValue(false)\n    }\n  }, [props.value, props.defaultValue])\n\n  const handleChange = (event: ChangeEvent<HTMLInputElement>) => {\n    setHaveValue(event.target.value.length > 0)\n    onChange?.(event)\n  }\n\n  useEffect(() => {\n    setHaveValue(Boolean(props.value || props.defaultValue))\n  }, [props.value, props.defaultValue])\n\n  return (\n    <InputContext.Provider\n      value={{\n        haveLabel,\n        haveValue,\n        variant,\n      }}\n    >\n      <InputContainer>\n        <input\n          className={cn(\n            inputVariants({\n              variant,\n              haveValue,\n              haveLabel,\n            }),\n            className,\n          )}\n          autoComplete=\"off\"\n          autoCapitalize=\"off\"\n          autoCorrect=\"off\"\n          spellCheck={false}\n          onChange={handleChange}\n          {...props}\n        />\n\n        {label && (\n          <>\n            <fieldset\n              className={cn(\n                inputLabelFieldsetVariants({\n                  variant,\n                }),\n              )}\n            >\n              <legend\n                className={cn(\n                  inputLabelLegendVariants({\n                    variant,\n                    haveValue,\n                  }),\n                )}\n              >\n                {label}\n              </legend>\n            </fieldset>\n\n            <InputLabel>{label}</InputLabel>\n          </>\n        )}\n\n        {children}\n\n        <InputLine />\n      </InputContainer>\n    </InputContext.Provider>\n  )\n}\n\nInput.displayName = 'Input'\n\nexport const InputLabel = ({\n  className,\n  ...props\n}: ComponentProps<'label'>) => {\n  const { haveValue, variant } = useInputContext()\n\n  return (\n    <label\n      className={cn(\n        inputLabelVariants({\n          variant,\n          focus: haveValue,\n        }),\n        className,\n      )}\n      {...props}\n    />\n  )\n}\n\nInputLabel.displayName = 'InputLabel'\n\nexport type NumericInputProps = Omit<\n  ComponentProps<'input'>,\n  'onChange' | 'value' | 'defaultValue' | 'type'\n> & {\n  value?: number | null\n  defaultValue?: number | null\n  onChange?: (value: number | null) => void\n  label?: string\n  min?: number\n  max?: number\n  decimalScale?: number\n  allowNegative?: boolean\n} & InputContainerVariants\n\nexport const NumericInput = ({\n  label,\n  variant,\n  className,\n  onChange,\n  value,\n  defaultValue,\n  min,\n  max,\n  decimalScale,\n  allowNegative = true,\n  ...props\n}: NumericInputProps) => {\n  const [inputValue, setInputValue] = useState<string>(() => {\n    const initialValue = value ?? defaultValue\n    return initialValue != null ? String(initialValue) : ''\n  })\n\n  useEffect(() => {\n    if (value != null) {\n      setInputValue(String(value))\n    }\n  }, [value])\n\n  const validateAndFormatValue = (numValue: number): number => {\n    let validated = numValue\n\n    if (!allowNegative && validated < 0) {\n      validated = 0\n    }\n\n    if (min != null && validated < min) {\n      validated = min\n    }\n\n    if (max != null && validated > max) {\n      validated = max\n    }\n\n    if (decimalScale != null) {\n      validated = Number(validated.toFixed(decimalScale))\n    }\n\n    return validated\n  }\n\n  const handleChange = (e: ChangeEvent<HTMLInputElement>) => {\n    const rawValue = e.target.value\n\n    // Allow empty string\n    if (rawValue === '') {\n      setInputValue('')\n      onChange?.(null)\n      return\n    }\n\n    // Allow minus sign for negative numbers\n    if (rawValue === '-' && allowNegative) {\n      setInputValue('-')\n      return\n    }\n\n    // Allow decimal point\n    if (rawValue.endsWith('.') || rawValue.endsWith(',')) {\n      setInputValue(rawValue)\n      return\n    }\n\n    const numValue = Number(rawValue)\n\n    // Check if it's a valid number\n    if (!isNaN(numValue)) {\n      setInputValue(rawValue)\n\n      // Only validate and callback when it's a complete number\n      if (!rawValue.endsWith('.') && !rawValue.endsWith(',')) {\n        const validated = validateAndFormatValue(numValue)\n        onChange?.(validated)\n      }\n    }\n  }\n\n  const handleBlur = () => {\n    if (inputValue === '' || inputValue === '-') {\n      setInputValue('')\n      onChange?.(null)\n      return\n    }\n\n    const numValue = Number(inputValue)\n    if (!isNaN(numValue)) {\n      const validated = validateAndFormatValue(numValue)\n      setInputValue(String(validated))\n      onChange?.(validated)\n    }\n  }\n\n  return (\n    <Input\n      label={label}\n      variant={variant}\n      className={className}\n      value={inputValue}\n      onChange={handleChange}\n      onBlur={handleBlur}\n      type=\"text\"\n      inputMode=\"decimal\"\n      {...props}\n    />\n  )\n}\n\nNumericInput.displayName = 'NumericInput'\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/ui/modal.tsx",
    "content": "import { AnimatePresence, motion } from 'framer-motion'\nimport { Dialog as DialogPrimitive, Slot } from 'radix-ui'\nimport { ComponentProps, createContext, useContext, useId } from 'react'\nimport { cn } from '@nyanpasu/ui'\nimport { useControllableState } from '@radix-ui/react-use-controllable-state'\nimport { Button, type ButtonProps } from './button'\n\nexport const ModalPortal = DialogPrimitive.Portal\n\nexport const ModalTitle = DialogPrimitive.Title\n\nexport const ModalDescription = DialogPrimitive.Description\n\nconst ModalContext = createContext<{\n  open?: boolean\n  layoutId?: string\n}>({})\n\nconst useModalContext = () => {\n  const context = useContext(ModalContext)\n\n  if (context === undefined) {\n    throw new Error(\n      'Modal compound components cannot be rendered outside the Modal component',\n    )\n  }\n\n  return context\n}\n\nexport function ModalTrigger({\n  className,\n  children,\n  asChild,\n  ...props\n}: ComponentProps<typeof DialogPrimitive.Trigger>) {\n  const { layoutId } = useModalContext()\n\n  const Comp = asChild ? Slot.Root : 'button'\n\n  return (\n    <DialogPrimitive.Trigger\n      {...props}\n      asChild\n      data-slot=\"modal-trigger\"\n      data-layout-id={layoutId}\n    >\n      <Comp className={cn('relative', className)}>\n        <Slot.Slottable>{children}</Slot.Slottable>\n\n        <div\n          className=\"@container-[size] absolute inset-0 -z-10 flex items-center justify-center\"\n          data-slot=\"modal-trigger-placeholder-container\"\n        >\n          <motion.div\n            className=\"size-full\"\n            style={{\n              maxWidth: 'min(100%, calc(4 * 100cqh))',\n              maxHeight: 'min(100%, calc(4 * 100cqw))',\n            }}\n            data-slot=\"modal-trigger-placeholder\"\n            layout\n            layoutId={layoutId}\n          />\n        </div>\n      </Comp>\n    </DialogPrimitive.Trigger>\n  )\n}\n\nexport function ModalClose({\n  children,\n  ...props\n}: ComponentProps<typeof DialogPrimitive.Close> &\n  (ComponentProps<typeof DialogPrimitive.Close>['asChild'] extends true\n    ? object\n    : ButtonProps)) {\n  return (\n    <DialogPrimitive.Close {...props} asChild>\n      {props.asChild ? children : <Button>{children}</Button>}\n    </DialogPrimitive.Close>\n  )\n}\n\nexport function ModalOverlay({\n  className,\n  ...props\n}: ComponentProps<typeof DialogPrimitive.Overlay>) {\n  return (\n    <DialogPrimitive.Overlay {...props} asChild>\n      <motion.div\n        className={cn(\n          'fixed inset-0 z-50',\n          'backdrop-blur-lg',\n          'bg-on-primary-container/10 dark:bg-on-primary/5',\n          className,\n        )}\n        initial={{ opacity: 0 }}\n        animate={{ opacity: 1 }}\n        exit={{ opacity: 0 }}\n      />\n    </DialogPrimitive.Overlay>\n  )\n}\n\nexport function ModalContent({\n  className,\n  children,\n  ...props\n}: ComponentProps<typeof DialogPrimitive.Content>) {\n  const { open, layoutId } = useModalContext()\n\n  return (\n    <AnimatePresence initial={false}>\n      {open && (\n        <ModalPortal forceMount>\n          <ModalOverlay />\n\n          <div\n            className={cn(\n              'fixed inset-0 z-50 grid place-items-center',\n              className,\n            )}\n          >\n            <DialogPrimitive.Content\n              {...props}\n              aria-describedby={undefined}\n              data-slot=\"modal-content\"\n              data-layout-id={layoutId}\n              asChild\n            >\n              <motion.div\n                layout\n                layoutId={layoutId}\n                initial={{\n                  opacity: 0,\n                  scale: 0.95,\n                }}\n                animate={{\n                  opacity: 1,\n                  scale: 1,\n                }}\n                exit={{\n                  opacity: 0,\n                  scale: 0.95,\n                }}\n              >\n                {children}\n              </motion.div>\n            </DialogPrimitive.Content>\n          </div>\n        </ModalPortal>\n      )}\n    </AnimatePresence>\n  )\n}\n\nexport function Modal({\n  open: inputOpen,\n  defaultOpen,\n  onOpenChange,\n  ...props\n}: ComponentProps<typeof DialogPrimitive.Root>) {\n  const layoutId = useId()\n\n  const [open, setOpen] = useControllableState({\n    prop: inputOpen,\n    defaultProp: defaultOpen ?? false,\n    onChange: onOpenChange,\n  })\n\n  return (\n    <ModalContext.Provider\n      value={{\n        open,\n        layoutId,\n      }}\n    >\n      <DialogPrimitive.Root open={open} onOpenChange={setOpen} {...props} />\n    </ModalContext.Provider>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/ui/progress.tsx",
    "content": "import { ComponentProps } from 'react'\nimport { cn } from '@nyanpasu/ui'\nimport { Circle, CircleSVG } from './circle'\n\nconst HalfCircle = (props: Omit<ComponentProps<typeof Circle>, 'value'>) => {\n  return <Circle value={50} {...props} />\n}\n\nconst HalfCircleSVG = ({\n  className,\n  ...props\n}: ComponentProps<typeof CircleSVG>) => {\n  return <CircleSVG className={cn('w-[200%]', className)} {...props} />\n}\n\nconst HalfCircleContainer = ({\n  className,\n  ...props\n}: ComponentProps<'div'>) => {\n  return (\n    <div\n      className={cn(\n        'relative h-full w-1/2 shrink-0 overflow-hidden',\n        className,\n      )}\n      {...props}\n    />\n  )\n}\n\nexport function CircularProgress({\n  value,\n  indeterminate,\n  className,\n  children,\n  ...props\n}: ComponentProps<'div'> & {\n  indeterminate?: boolean\n  value?: number\n}) {\n  return (\n    <div\n      className={cn('relative size-12 overflow-hidden', className)}\n      data-slot=\"circular-progress\"\n      {...props}\n    >\n      {indeterminate ? (\n        <div\n          className=\"absolute inset-0 animate-spin\"\n          data-slot=\"circular-progress-indeterminate\"\n        >\n          <div\n            className=\"animate-progress-spin absolute inset-0 flex\"\n            data-slot=\"circular-progress-indeterminate-inner\"\n          >\n            {/* left */}\n            <HalfCircleContainer data-slot=\"circular-progress-indeterminate-left\">\n              <HalfCircleSVG className=\"animate-progress-spin-left\">\n                <HalfCircle data-slot=\"circular-progress-indeterminate-left-circle\" />\n              </HalfCircleSVG>\n            </HalfCircleContainer>\n\n            {/* right */}\n            <HalfCircleContainer data-slot=\"circular-progress-indeterminate-right\">\n              <HalfCircleSVG className=\"animate-progress-spin-right -left-full\">\n                <HalfCircle data-slot=\"circular-progress-indeterminate-right-circle\" />\n              </HalfCircleSVG>\n            </HalfCircleContainer>\n          </div>\n        </div>\n      ) : (\n        <div\n          className=\"absolute h-full w-full -rotate-90\"\n          data-slot=\"circular-progress-determinate\"\n        >\n          <CircleSVG data-slot=\"circular-progress-determinate-svg\">\n            <Circle\n              data-slot=\"circular-progress-determinate-circle\"\n              className=\"transition-all\"\n              value={value ?? 100}\n            />\n          </CircleSVG>\n        </div>\n      )}\n\n      {children && (\n        <div className=\"absolute inset-0 flex items-center justify-center text-xs\">\n          {children}\n        </div>\n      )}\n    </div>\n  )\n}\n\nexport function LinearProgress({\n  value,\n  indeterminate,\n  className,\n  ...props\n}: ComponentProps<'div'> & {\n  indeterminate?: boolean\n  value?: number\n}) {\n  const clampedValue = Math.min(100, Math.max(0, value ?? 0))\n\n  return (\n    <div\n      className={cn(\n        'bg-secondary-container relative h-3 w-full overflow-hidden rounded-full',\n        className,\n      )}\n      role=\"progressbar\"\n      aria-valuenow={indeterminate ? undefined : clampedValue}\n      aria-valuemin={0}\n      aria-valuemax={100}\n      data-slot=\"linear-progress\"\n      {...props}\n    >\n      {indeterminate ? (\n        <>\n          {/* Primary indicator - moves from left to right */}\n          <div\n            className=\"animate-linear-progress-primary bg-primary absolute inset-y-0 left-0 rounded-full\"\n            data-slot=\"linear-progress-indeterminate-primary\"\n          />\n\n          {/* Secondary indicator - follows with different timing */}\n          <div\n            className=\"animate-linear-progress-secondary bg-primary absolute inset-y-0 left-0 rounded-full\"\n            data-slot=\"linear-progress-indeterminate-secondary\"\n          />\n        </>\n      ) : (\n        <div\n          className=\"bg-primary absolute inset-y-0 left-0 rounded-full transition-[width] duration-300 ease-out\"\n          style={{\n            width: `${clampedValue}%`,\n          }}\n          data-slot=\"linear-progress-indicator\"\n        />\n      )}\n    </div>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/ui/ripple.tsx",
    "content": "import {\n  AnimatePresence,\n  clamp,\n  domAnimation,\n  LazyMotion,\n  motion,\n} from 'framer-motion'\nimport { Key, MouseEvent, useCallback, useState } from 'react'\n\nexport type RippleConfig = {\n  key: Key\n  x: number\n  y: number\n  size: number\n}\n\nexport interface RippleProps {\n  ripples: RippleConfig[]\n  color?: string\n  onClear: (key: Key) => void\n}\n\nexport const Ripple = ({ ripples, color, onClear }: RippleProps) => {\n  return ripples.map((ripple) => {\n    const duration = clamp(\n      ripple.size > 100 ? 0.6 : 0.4,\n      0.01 * ripple.size,\n      0.3,\n    )\n\n    return (\n      <LazyMotion key={ripple.key} features={domAnimation}>\n        <AnimatePresence mode=\"popLayout\">\n          <motion.span\n            className=\"pointer-events-none absolute inset-0 z-0 origin-center overflow-hidden rounded-full\"\n            data-slot=\"ripple-item\"\n            initial={{ transform: 'scale(0)', opacity: 0.4 }}\n            animate={{ transform: 'scale(2)', opacity: 0 }}\n            exit={{ opacity: 0 }}\n            transition={{ duration }}\n            style={{\n              backgroundColor: color ?? 'currentColor',\n              top: ripple.y,\n              left: ripple.x,\n              width: `${ripple.size}px`,\n              height: `${ripple.size}px`,\n            }}\n            onAnimationComplete={() => {\n              onClear(ripple.key)\n            }}\n          />\n        </AnimatePresence>\n      </LazyMotion>\n    )\n  })\n}\n\nexport const useRipple = () => {\n  const [ripples, setRipples] = useState<RippleConfig[]>([])\n\n  const onClick = useCallback((e: MouseEvent) => {\n    const target = e.currentTarget\n\n    const size = Math.max(target.clientWidth, target.clientHeight)\n    const rect = target.getBoundingClientRect()\n\n    setRipples((prev) => [\n      ...prev,\n      {\n        key: new Date().getTime(),\n        size,\n        x: e.clientX - rect.left - size / 2,\n        y: e.clientY - rect.top - size / 2,\n      },\n    ])\n  }, [])\n\n  const onClear = useCallback((key: Key) => {\n    setRipples((prev) => prev.filter((ripple) => ripple.key !== key))\n  }, [])\n\n  return { ripples, onClick, onClear }\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/ui/scroll-area.tsx",
    "content": "import { ScrollArea as ScrollAreaPrimitive } from 'radix-ui'\nimport * as React from 'react'\nimport { createContext, useContext, useRef, useState } from 'react'\nimport { cn } from '@nyanpasu/ui'\n\ninterface ScrollAreaContextValue {\n  isScrolling: boolean\n  isTop: boolean\n  isBottom: boolean\n  scrollDirection: 'up' | 'down' | 'left' | 'right' | 'none'\n  offset: {\n    top: number\n    bottom: number\n    left: number\n    right: number\n  }\n  viewportRef: React.RefObject<HTMLDivElement | null>\n}\n\nconst ScrollAreaContext = createContext<ScrollAreaContextValue | null>(null)\n\nexport function useScrollArea() {\n  const context = useContext(ScrollAreaContext)\n\n  if (!context) {\n    throw new Error('useScrollArea must be used within a ScrollArea component')\n  }\n\n  return context\n}\n\nfunction useScrollTracking(threshold = 50) {\n  const [isScrolling, setIsScrolling] = useState(false)\n  const [isTop, setIsTop] = useState(true)\n  const [isBottom, setIsBottom] = useState(false)\n  const [scrollDirection, setScrollDirection] = useState<\n    'up' | 'down' | 'left' | 'right' | 'none'\n  >('none')\n\n  const [offset, setOffset] = useState({\n    top: 0,\n    left: 0,\n    right: 0,\n    bottom: 0,\n  })\n\n  const lastScrollTop = useRef(0)\n  const lastScrollLeft = useRef(0)\n\n  const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)\n\n  const handleScroll = (e: React.UIEvent<HTMLDivElement>) => {\n    const target = e.currentTarget as HTMLElement\n    const {\n      scrollTop,\n      scrollLeft,\n      scrollWidth,\n      clientWidth,\n      scrollHeight,\n      clientHeight,\n    } = target\n\n    if (timeoutRef.current) {\n      clearTimeout(timeoutRef.current)\n    }\n\n    setIsScrolling(true)\n\n    setIsTop(scrollTop === 0)\n\n    setOffset({\n      top: scrollTop,\n      left: scrollLeft,\n      right: scrollWidth - clientWidth,\n      bottom: scrollHeight - clientHeight,\n    })\n\n    // check if is at bottom, allow a small threshold\n    const isAtBottom = scrollHeight - scrollTop - clientHeight < threshold\n    setIsBottom(isAtBottom)\n\n    const deltaY = scrollTop - lastScrollTop.current\n    const deltaX = scrollLeft - lastScrollLeft.current\n\n    // Determine primary scroll direction\n    if (Math.abs(deltaY) > Math.abs(deltaX)) {\n      if (deltaY > 0) {\n        setScrollDirection('down')\n      } else if (deltaY < 0) {\n        setScrollDirection('up')\n      }\n    } else if (Math.abs(deltaX) > Math.abs(deltaY)) {\n      if (deltaX > 0) {\n        setScrollDirection('right')\n      } else if (deltaX < 0) {\n        setScrollDirection('left')\n      }\n    }\n\n    lastScrollTop.current = scrollTop\n    lastScrollLeft.current = scrollLeft\n\n    timeoutRef.current = setTimeout(() => {\n      if (timeoutRef.current) {\n        clearTimeout(timeoutRef.current)\n      }\n\n      setIsScrolling(false)\n    }, threshold)\n  }\n\n  return {\n    isTop,\n    isBottom,\n    scrollDirection,\n    handleScroll,\n    isScrolling,\n    offset,\n  }\n}\n\nexport function Viewport({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<typeof ScrollAreaPrimitive.Viewport>) {\n  return (\n    <ScrollAreaPrimitive.Viewport\n      data-slot=\"scroll-area-viewport\"\n      className={cn(\n        'size-full rounded-[inherit] transition-[color,box-shadow] outline-none',\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </ScrollAreaPrimitive.Viewport>\n  )\n}\n\nexport const Corner = ScrollAreaPrimitive.Corner\n\nexport const Root = ScrollAreaPrimitive.Root\n\nexport function ScrollArea({\n  className,\n  children,\n  scrollbars = 'vertical',\n  ...props\n}: React.ComponentProps<typeof ScrollAreaPrimitive.Root> & {\n  scrollbars?: 'vertical' | 'horizontal' | 'both'\n}) {\n  const viewportRef = useRef<HTMLDivElement>(null)\n\n  const {\n    isTop,\n    isBottom,\n    scrollDirection,\n    handleScroll,\n    isScrolling,\n    offset,\n  } = useScrollTracking()\n\n  return (\n    <ScrollAreaContext.Provider\n      value={{\n        isScrolling,\n        isTop,\n        isBottom,\n        scrollDirection,\n        viewportRef,\n        offset,\n      }}\n    >\n      <Root\n        data-slot=\"scroll-area\"\n        type=\"scroll\"\n        scrollHideDelay={600}\n        className={cn('relative', className)}\n        data-top={String(isTop)}\n        data-scroll-direction={scrollDirection}\n        {...props}\n      >\n        <Viewport ref={viewportRef} onScroll={handleScroll}>\n          {children}\n        </Viewport>\n\n        {(scrollbars === 'vertical' || scrollbars === 'both') && (\n          <ScrollBar orientation=\"vertical\" />\n        )}\n        {(scrollbars === 'horizontal' || scrollbars === 'both') && (\n          <ScrollBar orientation=\"horizontal\" />\n        )}\n        <Corner />\n      </Root>\n    </ScrollAreaContext.Provider>\n  )\n}\n\nexport function ScrollBar({\n  className,\n  orientation = 'vertical',\n  ...props\n}: React.ComponentProps<typeof ScrollAreaPrimitive.Scrollbar>) {\n  return (\n    <ScrollAreaPrimitive.ScrollAreaScrollbar\n      data-slot=\"scroll-area-scrollbar\"\n      orientation={orientation}\n      className={cn(\n        'z-50 flex touch-none p-px select-none',\n        'transition-opacity duration-300 ease-out',\n        'data-[state=hidden]:opacity-0 data-[state=visible]:opacity-100',\n        orientation === 'vertical' &&\n          'h-full w-3.5 border-l border-l-transparent px-0.75 py-1.5',\n        orientation === 'horizontal' &&\n          'h-3.5 flex-col border-t border-t-transparent px-1.5 py-0.75',\n        className,\n      )}\n      {...props}\n    >\n      <ScrollAreaPrimitive.ScrollAreaThumb\n        data-slot=\"scroll-area-thumb\"\n        className=\"bg-surface-variant relative flex-1 rounded-full\"\n      />\n    </ScrollAreaPrimitive.ScrollAreaScrollbar>\n  )\n}\n\nexport function AppContentScrollArea({\n  className,\n  children,\n  scrollbars = 'vertical',\n  ...props\n}: React.ComponentProps<typeof ScrollAreaPrimitive.Root> & {\n  scrollbars?: 'vertical' | 'horizontal' | 'both'\n}) {\n  const viewportRef = useRef<HTMLDivElement>(null)\n\n  const {\n    isTop,\n    isBottom,\n    scrollDirection,\n    handleScroll,\n    isScrolling,\n    offset,\n  } = useScrollTracking()\n\n  return (\n    <ScrollAreaContext.Provider\n      value={{\n        isScrolling,\n        isTop,\n        isBottom,\n        scrollDirection,\n        viewportRef,\n        offset,\n      }}\n    >\n      <Root\n        className={cn(\n          'relative',\n          'flex flex-1 flex-col',\n          'max-w-screen min-w-0',\n          'max-h-[calc(100vh-40px-64px)]',\n          'min-h-[calc(100vh-40px-64px)]',\n          'sm:max-h-[calc(100vh-40px-48px)]',\n          'sm:min-h-[calc(100vh-40px-48px)]',\n          className,\n        )}\n        data-slot=\"app-content-scroll-area\"\n        type=\"scroll\"\n        scrollHideDelay={600}\n        data-scrolling={String(isScrolling)}\n        data-top={String(isTop)}\n        data-bottom={String(isBottom)}\n        data-scroll-direction={scrollDirection}\n        {...props}\n      >\n        <Viewport\n          // className={cn('[&>div]:min-h-[calc(100vh-40px-64px)]', className)}\n          ref={viewportRef}\n          onScroll={handleScroll}\n        >\n          {children}\n        </Viewport>\n\n        {(scrollbars === 'vertical' || scrollbars === 'both') && (\n          <ScrollBar orientation=\"vertical\" />\n        )}\n        {(scrollbars === 'horizontal' || scrollbars === 'both') && (\n          <ScrollBar orientation=\"horizontal\" />\n        )}\n        <Corner />\n      </Root>\n    </ScrollAreaContext.Provider>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/ui/select.tsx",
    "content": "import ArrowDropDown from '~icons/material-symbols/arrow-drop-down-rounded'\nimport Check from '~icons/material-symbols/check-rounded'\nimport { cva, type VariantProps } from 'class-variance-authority'\nimport { AnimatePresence, motion } from 'framer-motion'\nimport { Select as SelectPrimitive } from 'radix-ui'\nimport {\n  ComponentProps,\n  createContext,\n  useCallback,\n  useContext,\n  useEffect,\n  useState,\n} from 'react'\nimport { chains } from '@/utils/chain'\nimport { cn } from '@nyanpasu/ui'\nimport { useControllableState } from '@radix-ui/react-use-controllable-state'\n\nexport const selectTriggerVariants = cva(\n  [\n    'group relative box-border inline-flex w-full flex-auto items-baseline',\n    'cursor-pointer',\n    'px-4 py-4 outline-hidden',\n    // TODO: size variants, fix this\n    'flex items-center justify-between h-14',\n    'dark:text-on-surface',\n  ],\n  {\n    variants: {\n      variant: {\n        filled: 'rounded-t bg-surface-variant/30 dark:bg-surface',\n        // outlined use selectValuePlaceholderFieldsetVariants\n        outlined: '',\n      },\n    },\n    defaultVariants: {\n      variant: 'filled',\n    },\n  },\n)\n\nexport type SelectTriggerVariants = VariantProps<typeof selectTriggerVariants>\n\nexport const selectLineVariants = cva('', {\n  variants: {\n    variant: {\n      filled: [\n        'absolute inset-x-0 bottom-0 w-full border-b border-on-primary-container',\n        'transition-all duration-200',\n\n        // pseudo elements be overlay parent element, will not affect the box size\n        'after:absolute after:inset-x-0 after:bottom-0 after:z-10',\n        \"after:scale-x-0 after:border-b-2 after:opacity-0 after:content-['']\",\n        'after:transition-all after:duration-200',\n        'after:border-primary dark:after:border-on-primary-container',\n\n        // sync parent group state, state from radix-ui\n        'group-data-[state=open]:border-b-0',\n        'group-data-[state=open]:after:scale-x-100',\n        'group-data-[state=open]:after:opacity-100',\n        'peer-focus:border-b-0',\n        'peer-focus:after:scale-x-100',\n        'peer-focus:after:opacity-100',\n      ],\n      // hidden line for outlined variant\n      outlined: 'hidden',\n    },\n  },\n  defaultVariants: {\n    variant: 'filled',\n  },\n})\n\nexport type SelectLineVariants = VariantProps<typeof selectLineVariants>\n\nexport const selectValueVariants = cva(\n  'pointer-events-none transition-[margin] duration-200',\n  {\n    variants: {\n      variant: {\n        filled: '',\n        outlined: '',\n      },\n      haveValue: {\n        true: '',\n        false: '',\n      },\n    },\n    compoundVariants: [\n      {\n        variant: 'filled',\n        haveValue: true,\n        className: 'mt-3!',\n      },\n    ],\n    defaultVariants: {\n      variant: 'filled',\n      haveValue: false,\n    },\n  },\n)\n\nexport type SelectValueVariants = VariantProps<typeof selectValueVariants>\n\nexport const selectValuePlaceholderVariants = cva(\n  [\n    'absolute',\n    'left-4 top-4',\n    'pointer-events-none',\n    'text-base select-none',\n    // TODO: only transition position, not text color\n    'transition-all duration-200',\n  ],\n  {\n    variants: {\n      variant: {\n        filled: [\n          'group-data-[state=open]:top-2',\n          'group-data-[state=open]:text-xs group-data-[state=open]:text-primary',\n        ],\n        outlined: [\n          'group-data-[state=open]:-top-2',\n          'group-data-[state=open]:text-sm',\n          'group-data-[state=open]:text-primary',\n\n          'dark:group-data-[state=open]:text-inverse-primary',\n          'dark:group-data-[state=closed]:text-on-primary-container',\n        ],\n      },\n      focus: {\n        true: '',\n        false: '',\n      },\n    },\n    compoundVariants: [\n      {\n        variant: 'filled',\n        focus: true,\n        className: 'top-2 text-xs',\n      },\n      {\n        variant: 'outlined',\n        focus: true,\n        className: '-top-2 text-sm',\n      },\n    ],\n    defaultVariants: {\n      variant: 'filled',\n      focus: false,\n    },\n  },\n)\n\nexport type SelectValuePlaceholderVariants = VariantProps<\n  typeof selectValuePlaceholderVariants\n>\n\nexport const selectValuePlaceholderFieldsetVariants = cva(\n  'pointer-events-none',\n  {\n    variants: {\n      variant: {\n        // only for outlined variant\n        filled: 'hidden',\n        outlined: [\n          'absolute inset-0 text-left',\n          'rounded transition-all duration-200',\n          // may open border width will be 1.5, idk\n          'group-data-[state=closed]:border',\n          'group-data-[state=open]:border-2',\n          'peer-not-focus:border',\n          'peer-focus:border-2',\n          // different material web border color, i think this looks better\n          'group-data-[state=closed]:border-outline-variant',\n          'group-data-[state=open]:border-primary',\n          'peer-not-focus:border-primary-container',\n          'peer-focus:border-primary',\n          // dark must be prefixed\n          'dark:group-data-[state=closed]:border-outline-variant',\n          'dark:group-data-[state=open]:border-primary-container',\n          'dark:peer-not-focus:border-outline-variant',\n          'dark:peer-focus:border-primary-container',\n        ],\n      },\n    },\n    defaultVariants: {\n      variant: 'filled',\n    },\n  },\n)\n\nexport type SelectValuePlaceholderFieldsetVariants = VariantProps<\n  typeof selectValuePlaceholderFieldsetVariants\n>\n\nexport const selectValuePlaceholderLegendVariants = cva('', {\n  variants: {\n    variant: {\n      // only for outlined variant\n      filled: 'hidden',\n      outlined: 'invisible ml-2 px-2 text-sm h-0',\n    },\n    haveValue: {\n      true: '',\n      false: '',\n    },\n  },\n  compoundVariants: [\n    {\n      variant: 'outlined',\n      haveValue: false,\n      className: 'group-data-[state=closed]:hidden group-not-focus:hidden',\n    },\n  ],\n  defaultVariants: {\n    variant: 'filled',\n    haveValue: false,\n  },\n})\n\nexport type SelectValuePlaceholderLegendVariants = VariantProps<\n  typeof selectValuePlaceholderLegendVariants\n>\n\nexport const selectContentVariants = cva(\n  [\n    'relative w-full overflow-auto rounded shadow-container z-50',\n    'bg-inverse-on-surface dark:bg-surface',\n    'dark:text-on-surface',\n  ],\n  {\n    variants: {\n      variant: {\n        filled: 'rounded-t-none',\n        outlined: '',\n      },\n    },\n    defaultVariants: {\n      variant: 'filled',\n    },\n  },\n)\n\nexport type SelectContentVariants = VariantProps<typeof selectContentVariants>\n\ntype SelectContextType = {\n  haveValue?: boolean\n  open?: boolean\n} & SelectTriggerVariants\n\nconst SelectContext = createContext<SelectContextType | null>(null)\n\nconst useSelectContext = () => {\n  const context = useContext(SelectContext)\n\n  if (!context) {\n    throw new Error('useSelectContext must be used within a SelectProvider')\n  }\n\n  return context\n}\n\nexport const SelectLine = ({ className, ...props }: ComponentProps<'div'>) => {\n  const { variant } = useSelectContext()\n\n  return (\n    <div\n      className={cn(\n        selectLineVariants({\n          variant,\n        }),\n        className,\n      )}\n      {...props}\n    />\n  )\n}\n\nexport const Select = ({\n  onValueChange,\n  variant,\n  open: inputOpen,\n  defaultOpen,\n  onOpenChange,\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.Root> &\n  SelectTriggerVariants) => {\n  const [open, setOpen] = useControllableState({\n    prop: inputOpen,\n    defaultProp: defaultOpen ?? false,\n    onChange: onOpenChange,\n  })\n\n  const [haveValue, setHaveValue] = useState(\n    Boolean(props.value || props.defaultValue),\n  )\n\n  const handleOnChange = useCallback((value?: string) => {\n    setHaveValue(Boolean(value))\n  }, [])\n\n  useEffect(() => {\n    setHaveValue(Boolean(props.value || props.defaultValue))\n  }, [props.value, props.defaultValue])\n\n  return (\n    <SelectContext.Provider\n      value={{\n        open,\n        haveValue,\n        variant,\n      }}\n    >\n      <SelectPrimitive.Root\n        open={open}\n        onOpenChange={setOpen}\n        onValueChange={chains(handleOnChange, onValueChange)}\n        {...props}\n      />\n    </SelectContext.Provider>\n  )\n}\n\nexport type SelectProps = ComponentProps<typeof Select>\n\nexport const SelectValue = ({\n  className,\n  placeholder,\n  ...props\n}: ComponentProps<typeof SelectPrimitive.Value>) => {\n  const { haveValue, open, variant } = useSelectContext()\n\n  return (\n    <>\n      <div\n        className={cn(\n          selectValueVariants({\n            variant,\n            haveValue,\n          }),\n          className,\n        )}\n      >\n        <SelectPrimitive.Value {...props} />\n      </div>\n\n      <fieldset\n        className={cn(\n          selectValuePlaceholderFieldsetVariants({\n            variant,\n          }),\n        )}\n      >\n        <legend\n          className={cn(\n            selectValuePlaceholderLegendVariants({\n              variant,\n              haveValue: haveValue || open,\n            }),\n          )}\n        >\n          {placeholder}\n        </legend>\n      </fieldset>\n\n      <div\n        className={cn(\n          selectValuePlaceholderVariants({\n            variant,\n            focus: haveValue || open,\n          }),\n        )}\n      >\n        {placeholder}\n      </div>\n    </>\n  )\n}\n\nexport const SelectGroup = (\n  props: ComponentProps<typeof SelectPrimitive.Group>,\n) => {\n  return <SelectPrimitive.Group {...props} />\n}\n\nexport const SelectLabel = ({\n  className,\n  ...props\n}: ComponentProps<typeof SelectPrimitive.Label>) => {\n  return (\n    <SelectPrimitive.Label\n      className={cn(\n        'flex h-12 cursor-default items-center justify-between gap-2 p-4 outline-hidden',\n        'text-primary dark:text-inverse-primary',\n        className,\n      )}\n      {...props}\n    />\n  )\n}\n\nexport const SelectTrigger = ({\n  className,\n  children,\n  ...props\n}: ComponentProps<typeof SelectPrimitive.Trigger>) => {\n  const { variant } = useSelectContext()\n\n  return (\n    <SelectPrimitive.Trigger\n      className={cn(\n        selectTriggerVariants({\n          variant,\n        }),\n        className,\n      )}\n      {...props}\n    >\n      {children}\n\n      <SelectLine />\n\n      <SelectIcon />\n    </SelectPrimitive.Trigger>\n  )\n}\n\nexport const SelectIcon = ({\n  asChild,\n  children,\n  className,\n  ...props\n}: ComponentProps<typeof SelectPrimitive.Icon>) => {\n  return (\n    <SelectPrimitive.Icon\n      className={cn('absolute right-4', className)}\n      asChild\n      {...props}\n    >\n      {asChild ? children : <ArrowDropDown />}\n    </SelectPrimitive.Icon>\n  )\n}\n\nexport const SelectContent = ({\n  className,\n  children,\n  ...props\n}: ComponentProps<typeof SelectPrimitive.Content>) => {\n  const { open, variant } = useSelectContext()\n\n  return (\n    <AnimatePresence initial={false}>\n      {open && (\n        <SelectPrimitive.Portal>\n          <SelectPrimitive.Content {...props} position=\"popper\" asChild>\n            <motion.div\n              className={cn(\n                selectContentVariants({\n                  variant,\n                }),\n                className,\n              )}\n              style={{\n                width: 'var(--radix-popper-anchor-width)',\n                maxHeight: 'var(--radix-popper-available-height)',\n              }}\n              initial={{ opacity: 0, scaleY: 0.9, transformOrigin: 'top' }}\n              animate={{ opacity: 1, scaleY: 1, transformOrigin: 'top' }}\n              exit={{ opacity: 0, scaleY: 0.9, transformOrigin: 'top' }}\n              transition={{\n                type: 'spring',\n                bounce: 0,\n                duration: 0.35,\n              }}\n            >\n              <SelectPrimitive.Viewport>{children}</SelectPrimitive.Viewport>\n            </motion.div>\n          </SelectPrimitive.Content>\n        </SelectPrimitive.Portal>\n      )}\n    </AnimatePresence>\n  )\n}\n\nexport const SelectItem = ({\n  className,\n  children,\n  ...props\n}: ComponentProps<typeof SelectPrimitive.Item>) => {\n  return (\n    <SelectPrimitive.Item\n      className={cn(\n        'flex h-12 cursor-default items-center justify-between gap-2 p-4 outline-hidden',\n        'cursor-pointer',\n        'hover:bg-surface-variant data-[state=checked]:bg-primary-container',\n        'dark:hover:bg-surface-variant dark:data-[state=checked]:bg-primary-container',\n        className,\n      )}\n      {...props}\n    >\n      <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>\n\n      <SelectPrimitive.ItemIndicator>\n        <Check className=\"text-primary\" />\n      </SelectPrimitive.ItemIndicator>\n    </SelectPrimitive.Item>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/ui/separator.tsx",
    "content": "import { Separator as SeparatorPrimitive } from 'radix-ui'\nimport { ComponentProps } from 'react'\nimport { cn } from '@nyanpasu/ui'\n\nexport function Separator({\n  className,\n  orientation = 'horizontal',\n  decorative = true,\n  ...props\n}: ComponentProps<typeof SeparatorPrimitive.Root>) {\n  return (\n    <SeparatorPrimitive.Root\n      data-slot=\"separator\"\n      decorative={decorative}\n      orientation={orientation}\n      className={cn(\n        'bg-outline-variant/50 shrink-0',\n        'data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full',\n        'data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px',\n        className,\n      )}\n      {...props}\n    />\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/ui/sidebar.tsx",
    "content": "import { ComponentProps, createContext, useContext } from 'react'\nimport useIsMobile from '@/hooks/use-is-moblie'\nimport { cn } from '@nyanpasu/ui'\nimport { AppContentScrollArea } from './scroll-area'\n\nconst SidebarContext = createContext<{\n  isHiddenSide: boolean\n} | null>(null)\n\nexport const useSidebarContext = () => {\n  const context = useContext(SidebarContext)\n\n  if (!context) {\n    throw new Error(\n      'useSidebarContext must be used within a SidebarContext.Provider',\n    )\n  }\n\n  return context\n}\n\nexport function Sidebar({ className, ...props }: ComponentProps<'div'>) {\n  const isMobile = useIsMobile()\n\n  return (\n    <SidebarContext.Provider\n      value={{\n        isHiddenSide: isMobile,\n      }}\n    >\n      <div\n        className={cn('flex', className)}\n        data-slot=\"sidebar-container\"\n        {...props}\n      />\n    </SidebarContext.Provider>\n  )\n}\n\nexport function SidebarContent({\n  className,\n  ...props\n}: ComponentProps<typeof AppContentScrollArea>) {\n  const { isHiddenSide } = useSidebarContext()\n\n  if (isHiddenSide) {\n    return null\n  }\n\n  return (\n    <AppContentScrollArea\n      className={cn('z-50 max-w-96 min-w-64', className)}\n      data-slot=\"sidebar-scroll-area\"\n      {...props}\n    />\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/ui/slider-sidebar.tsx",
    "content": "import MenuOpenRounded from '~icons/material-symbols/menu-open-rounded'\nimport { motion } from 'framer-motion'\nimport { merge } from 'lodash-es'\nimport {\n  createContext,\n  use,\n  type ComponentProps,\n  type PropsWithChildren,\n} from 'react'\nimport { cn } from '@nyanpasu/ui'\nimport { useControllableState } from '@radix-ui/react-use-controllable-state'\nimport { Button } from './button'\n\nconst DEFAULT_SIDEBAR_WIDTH = {\n  open: 280,\n  closed: 48 + 8 * 2,\n}\n\nconst SidebarContext = createContext<{\n  open: boolean\n  setOpen: (isOpen: boolean) => void\n} | null>(null)\n\nexport const useSidebar = () => {\n  const context = use(SidebarContext)\n\n  if (!context) {\n    throw new Error('useSidebar must be used within a SidebarProvider')\n  }\n\n  return context\n}\n\nexport function SidebarProvider({\n  open: inputOpen,\n  onOpenChange,\n  defaultOpen,\n  children,\n}: PropsWithChildren & {\n  open?: boolean\n  onOpenChange?: (isOpen: boolean) => void\n  defaultOpen?: boolean\n}) {\n  const [open, setOpen] = useControllableState({\n    prop: inputOpen,\n    defaultProp: defaultOpen ?? false,\n    onChange: onOpenChange,\n  })\n\n  return (\n    <SidebarContext.Provider\n      value={{\n        open,\n        setOpen,\n      }}\n    >\n      {children}\n    </SidebarContext.Provider>\n  )\n}\n\nexport function Sidebar({\n  className,\n  animate,\n  transition,\n  width = DEFAULT_SIDEBAR_WIDTH,\n  ...props\n}: ComponentProps<typeof motion.aside> & {\n  width?: {\n    open?: number\n    closed?: number\n  }\n}) {\n  const { open } = useSidebar()\n\n  return (\n    <>\n      <div\n        className=\"h-full md:hidden\"\n        data-slot=\"sidebar-placeholder\"\n        style={{\n          width: width.closed,\n        }}\n      />\n\n      <motion.aside\n        className={cn(\n          'bg-mixed-background absolute h-full md:static',\n          className,\n        )}\n        initial={false}\n        animate={merge(\n          {\n            width: open ? width.open : width.closed,\n          },\n          animate,\n        )}\n        transition={{\n          type: 'spring',\n          stiffness: 300,\n          damping: 30,\n          ...transition,\n        }}\n        {...props}\n      />\n    </>\n  )\n}\n\nexport function SidebarLabelItem({\n  className,\n  animate,\n  transition,\n  ...props\n}: ComponentProps<typeof motion.span>) {\n  const { open } = useSidebar()\n\n  return (\n    <motion.span\n      data-open={String(open)}\n      className={cn('overflow-hidden whitespace-nowrap', className)}\n      initial={false}\n      animate={merge(\n        {\n          width: open ? '100%' : 0,\n        },\n        animate,\n      )}\n      transition={{\n        duration: 0.2,\n        ease: 'easeOut',\n        ...transition,\n      }}\n      {...props}\n    />\n  )\n}\n\nexport const SidebarToggleButton = () => {\n  const { open, setOpen } = useSidebar()\n\n  return (\n    <Button\n      className=\"flex size-12 min-w-0 items-center gap-2 rounded-2xl px-3 text-left\"\n      variant=\"raised\"\n      onClick={() => setOpen(!open)}\n    >\n      <MenuOpenRounded className=\"size-6 shrink-0\" />\n    </Button>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/ui/slider.tsx",
    "content": "import { clamp, motion, Transition } from 'framer-motion'\nimport { ComponentProps } from 'react'\nimport { cn } from '@nyanpasu/ui'\nimport { useControllableState } from '@radix-ui/react-use-controllable-state'\n\nconst EDGE_OFFSET_PX = 16\nconst PADDING_PX = 8\n\nexport function Slider({\n  className,\n  defaultValue,\n  value,\n  min = 0,\n  max = 100,\n  disabled,\n  step = 1,\n  onValueChange,\n  onValueCommit,\n  onMouseUp,\n  onTouchEnd,\n  onKeyUp,\n  onBlur,\n  ...props\n}: Omit<\n  ComponentProps<'input'>,\n  'type' | 'value' | 'defaultValue' | 'min' | 'max' | 'onChange'\n> & {\n  value?: number\n  defaultValue?: number\n  min?: number\n  max?: number\n  onValueChange?: (value: number) => void\n  onValueCommit?: (value: number) => void\n}) {\n  const controlledValue =\n    typeof value === 'number' ? clamp(min, max, value) : undefined\n\n  const defaultSliderValue = clamp(min, max, defaultValue ?? min)\n\n  const [rawValue, setRawValue] = useControllableState<number>({\n    prop: controlledValue,\n    defaultProp: defaultSliderValue,\n    onChange: (nextValue) => {\n      onValueChange?.(clamp(min, max, nextValue))\n    },\n  })\n\n  const currentValue = clamp(min, max, rawValue ?? min)\n\n  const percentage =\n    max === min ? 0 : ((currentValue - min) / (max - min)) * 100\n\n  const ratio = percentage / 100\n\n  const thumbOffsetPx = EDGE_OFFSET_PX + PADDING_PX\n  const thumbLeft = `calc(${thumbOffsetPx}px + (100% - ${thumbOffsetPx * 2}px) * ${ratio})`\n  const rangeWidth = `calc(${thumbLeft} - ${PADDING_PX}px)`\n  const trackWidth = `calc(100% - ${thumbLeft} - ${PADDING_PX}px)`\n\n  const motionTransition: Transition = disabled\n    ? { duration: 0 }\n    : { type: 'spring' as const, stiffness: 380, damping: 35, mass: 0.2 }\n\n  const handleValueChange: ComponentProps<'input'>['onChange'] = (event) => {\n    const nextValue = clamp(min, max, Number(event.target.value))\n    setRawValue(nextValue)\n  }\n\n  const commitValue = () => {\n    onValueCommit?.(currentValue)\n  }\n\n  return (\n    <div\n      data-slot=\"slider\"\n      data-disabled={String(disabled)}\n      className={cn(\n        'relative flex w-full touch-none items-center justify-between select-none',\n        'h-4 data-[disabled=true]:opacity-50',\n        className,\n      )}\n    >\n      <motion.span\n        data-slot=\"slider-range\"\n        className={cn(\n          'bg-primary absolute inset-y-0 left-0 select-none',\n          'rounded-l-full rounded-r-sm',\n        )}\n        animate={{\n          width: rangeWidth,\n          borderRadius: '12px 4px 4px 12px',\n        }}\n        transition={motionTransition}\n      />\n\n      <motion.span\n        data-slot=\"slider-track\"\n        className={cn(\n          'bg-surface-variant absolute inset-y-0 right-0 select-none',\n        )}\n        animate={{\n          width: trackWidth,\n          borderRadius: '4px 12px 12px 4px',\n        }}\n        transition={motionTransition}\n      />\n\n      <motion.span\n        data-slot=\"slider-thumb\"\n        className={cn(\n          'bg-primary pointer-events-none absolute top-1/2 h-10 w-1.5 -translate-x-1/2 -translate-y-1/2 rounded-full',\n          'transition-[color,box-shadow] select-none',\n        )}\n        animate={{\n          left: thumbLeft,\n        }}\n        transition={motionTransition}\n      />\n\n      <input\n        type=\"range\"\n        min={min}\n        max={max}\n        step={step}\n        value={currentValue}\n        disabled={disabled}\n        onChange={handleValueChange}\n        onMouseUp={(event) => {\n          commitValue()\n          onMouseUp?.(event)\n        }}\n        onTouchEnd={(event) => {\n          commitValue()\n          onTouchEnd?.(event)\n        }}\n        onKeyUp={(event) => {\n          commitValue()\n          onKeyUp?.(event)\n        }}\n        onBlur={(event) => {\n          commitValue()\n          onBlur?.(event)\n        }}\n        className=\"absolute inset-0 h-full w-full cursor-pointer appearance-none bg-transparent opacity-0 disabled:cursor-not-allowed\"\n        {...props}\n      />\n    </div>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/ui/switch.tsx",
    "content": "import { Switch as SwitchPrimitives } from 'radix-ui'\nimport React, { ComponentProps } from 'react'\nimport { cn } from '@nyanpasu/ui'\nimport { CircularProgress } from './progress'\n\nexport const Switch = ({\n  className,\n  loading,\n  ...props\n}: React.ComponentProps<typeof SwitchPrimitives.Root> & {\n  loading?: boolean\n}) => {\n  return (\n    <SwitchPrimitives.Root\n      className={cn(\n        'peer',\n        'inline-flex h-8 w-14 shrink-0 cursor-pointer items-center rounded-full',\n        'focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none',\n        'disabled:cursor-not-allowed disabled:opacity-50',\n        'border-2 transition-colors',\n        'data-[state=unchecked]:border-on-surface/10 data-[state=checked]:border-transparent',\n        'data-[state=unchecked]:border-transparent dark:data-[state=checked]:border-transparent',\n        'data-[state=checked]:bg-primary data-[state=unchecked]:bg-surface-variant',\n        'dark:data-[state=checked]:bg-primary-container dark:data-[state=unchecked]:bg-on-surface-variant/30',\n        className,\n      )}\n      {...props}\n    >\n      <SwitchPrimitives.Thumb\n        className={cn(\n          'group',\n          'pointer-events-none block',\n          'rounded-full shadow-lg ring-0 transition-all duration-200 ease-in-out',\n          'data-[state=checked]:bg-surface data-[state=unchecked]:bg-on-surface/80',\n          'dark:data-[state=checked]:bg-inverse-surface dark:data-[state=unchecked]:bg-inverse-surface',\n          'data-[state=checked]:size-6 data-[state=unchecked]:size-4',\n          'data-[state=checked]:translate-x-6.5 data-[state=unchecked]:translate-x-1.5',\n        )}\n      >\n        {loading && (\n          <span\n            className=\"grid h-full w-full place-items-center\"\n            data-slot=\"switch-thumb-loading\"\n          >\n            <CircularProgress\n              className=\"text-surface group-data-[state=checked]:size-4 group-data-[state=unchecked]:size-2.5\"\n              data-slot=\"switch-thumb-loading-circular-progress\"\n              indeterminate\n            />\n          </span>\n        )}\n      </SwitchPrimitives.Thumb>\n    </SwitchPrimitives.Root>\n  )\n}\n\nexport function SwitchItem({\n  children,\n  className,\n  ...props\n}: ComponentProps<typeof Switch>) {\n  return (\n    <div\n      className={cn(\n        'flex h-16 w-full items-center justify-between gap-2',\n        'bg-surface-variant/30 dark:bg-surface-variant/10',\n        'rounded-xl',\n        'p-4',\n        className,\n      )}\n    >\n      {children}\n\n      <Switch {...props} />\n    </div>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/ui/text-marquee.tsx",
    "content": "import { motion, useAnimationControls } from 'framer-motion'\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport { sleep } from '@/utils'\nimport { cn } from '@nyanpasu/ui'\n\nexport default function TextMarquee({\n  children,\n  className,\n  speed = 30,\n  gap = 32,\n  pauseDuration = 1,\n  // pauseOnHover = true,\n}: {\n  children: React.ReactNode\n  className?: string\n  speed?: number\n  gap?: number\n  pauseDuration?: number\n  // pauseOnHover?: boolean\n}) {\n  const containerRef = useRef<HTMLDivElement>(null)\n\n  const textRef = useRef<HTMLDivElement>(null)\n\n  const [shouldAnimate, setShouldAnimate] = useState(false)\n\n  const [textWidth, setTextWidth] = useState(0)\n\n  const controls = useAnimationControls()\n\n  const isHoveredRef = useRef(false)\n\n  // Check if text overflows container\n  const checkOverflow = useCallback(() => {\n    if (!containerRef.current || !textRef.current) {\n      return\n    }\n\n    const container = containerRef.current\n    const text = textRef.current\n\n    const containerW = container.offsetWidth\n    const textW = text.scrollWidth\n\n    setTextWidth(textW)\n    setShouldAnimate(textW > containerW)\n  }, [])\n\n  // Observe container size changes\n  useEffect(() => {\n    checkOverflow()\n\n    const resizeObserver = new ResizeObserver(() => {\n      checkOverflow()\n    })\n\n    if (containerRef.current) {\n      resizeObserver.observe(containerRef.current)\n    }\n\n    return () => {\n      resizeObserver.disconnect()\n    }\n  }, [checkOverflow, children])\n\n  // Animate when shouldAnimate changes\n  useEffect(() => {\n    if (!shouldAnimate) {\n      controls.set({ x: 0 })\n      return\n    }\n\n    const totalDistance = textWidth + gap\n    const animDuration = totalDistance / speed\n\n    const cancelledRef = { current: false }\n\n    const runAnimationLoop = async () => {\n      // Wait at start position\n      await sleep(pauseDuration * 1000)\n\n      if (cancelledRef.current) {\n        return\n      }\n\n      // Check if hovered, wait and retry\n      if (isHoveredRef.current) {\n        await sleep(100)\n\n        if (!cancelledRef.current) {\n          runAnimationLoop()\n        }\n\n        return\n      }\n\n      // Animate to end\n      await controls.start({\n        x: -totalDistance,\n        transition: {\n          duration: animDuration,\n          ease: 'linear',\n        },\n      })\n\n      if (cancelledRef.current) {\n        return\n      }\n\n      // Reset to start position instantly and loop\n      controls.set({ x: 0 })\n\n      if (!cancelledRef.current) {\n        runAnimationLoop()\n      }\n    }\n\n    runAnimationLoop()\n\n    return () => {\n      cancelledRef.current = true\n      controls.stop()\n    }\n  }, [shouldAnimate, textWidth, gap, speed, pauseDuration, controls])\n\n  // const handleMouseEnter = () => {\n  //   if (!pauseOnHover) {\n  //     return\n  //   }\n\n  //   isHoveredRef.current = true\n  //   controls.stop()\n  // }\n\n  // const handleMouseLeave = () => {\n  //   if (!pauseOnHover || !shouldAnimate) {\n  //     return\n  //   }\n\n  //   isHoveredRef.current = false\n\n  //   resumeAnimation()\n  // }\n\n  // const resumeAnimation = () => {\n  //   const totalDistance = textWidth + gap\n\n  //   // Resume animation\n  //   const marqueeContent = containerRef.current?.querySelector<HTMLDivElement>(\n  //     '[data-marquee-content]',\n  //   )\n\n  //   if (marqueeContent) {\n  //     const transform = window.getComputedStyle(marqueeContent).transform\n  //     const matrix = new DOMMatrix(transform)\n  //     const currentPosition = matrix.m41\n\n  //     const remainingDistance = -totalDistance - currentPosition\n  //     const remainingDuration = Math.abs(remainingDistance) / speed\n\n  //     controls.start({\n  //       x: -totalDistance,\n  //       transition: {\n  //         duration: remainingDuration,\n  //         ease: 'linear',\n  //       },\n  //     })\n  //   }\n  // }\n\n  return (\n    <div\n      ref={containerRef}\n      className={cn('overflow-hidden', className)}\n      data-slot=\"text-marquee\"\n    >\n      {shouldAnimate ? (\n        <motion.div\n          className=\"flex whitespace-nowrap\"\n          animate={controls}\n          data-slot=\"text-marquee-content\"\n        >\n          <span\n            ref={textRef}\n            data-slot=\"text-marquee-content-item\"\n            data-index=\"0\"\n          >\n            {children}\n          </span>\n\n          <span\n            style={{\n              paddingLeft: gap,\n            }}\n            data-slot=\"text-marquee-content-item\"\n            data-index=\"1\"\n          >\n            {children}\n          </span>\n        </motion.div>\n      ) : (\n        <div\n          ref={textRef}\n          className=\"truncate\"\n          data-slot=\"text-marquee-content\"\n        >\n          {children}\n        </div>\n      )}\n    </div>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/ui/tooltip.tsx",
    "content": "import { Tooltip as TooltipPrimitive } from 'radix-ui'\nimport * as React from 'react'\nimport { cn } from '@nyanpasu/ui'\n\nexport function TooltipProvider({\n  delayDuration = 0,\n  ...props\n}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {\n  return (\n    <TooltipPrimitive.Provider\n      data-slot=\"tooltip-provider\"\n      delayDuration={delayDuration}\n      {...props}\n    />\n  )\n}\n\nexport function Tooltip({\n  ...props\n}: React.ComponentProps<typeof TooltipPrimitive.Root>) {\n  return (\n    <TooltipProvider>\n      <TooltipPrimitive.Root data-slot=\"tooltip\" {...props} />\n    </TooltipProvider>\n  )\n}\n\nexport function TooltipTrigger({\n  ...props\n}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {\n  return <TooltipPrimitive.Trigger data-slot=\"tooltip-trigger\" {...props} />\n}\n\nexport function TooltipContent({\n  className,\n  sideOffset = 0,\n  children,\n  disableArrow = false,\n  ...props\n}: React.ComponentProps<typeof TooltipPrimitive.Content> & {\n  disableArrow?: boolean\n}) {\n  return (\n    <TooltipPrimitive.Portal>\n      <TooltipPrimitive.Content\n        data-slot=\"tooltip-content\"\n        sideOffset={sideOffset}\n        className={cn(\n          'bg-surface-variant text-on-surface',\n          'z-50 w-fit min-w-12 text-center',\n          'rounded-full px-3 py-1.5 text-xs text-balance',\n          'shadow-outline/30 dark:shadow-surface-variant/20 shadow-sm',\n          className,\n        )}\n        {...props}\n      >\n        {children}\n\n        {!disableArrow && (\n          <TooltipPrimitive.Arrow\n            className={cn(\n              'fill-surface-variant z-50',\n              'h-2.5 w-4 translate-y-[-6px] rounded-xl',\n            )}\n          />\n        )}\n      </TooltipPrimitive.Content>\n    </TooltipPrimitive.Portal>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/updater/updater-dialog-wrapper.tsx",
    "content": "import { useAtom } from 'jotai'\nimport { lazy, Suspense, useState } from 'react'\nimport { UpdaterInstanceAtom } from '@/store/updater'\n\nconst UpdaterDialog = lazy(() => import('./updater-dialog'))\n\nexport const UpdaterDialogWrapper = () => {\n  const [open, setOpen] = useState(true)\n  const [manifest, setManifest] = useAtom(UpdaterInstanceAtom)\n  if (!manifest) return null\n  return (\n    <Suspense fallback={null}>\n      <UpdaterDialog\n        open={open}\n        onClose={() => {\n          setOpen(false)\n          setManifest(null)\n        }}\n        update={manifest}\n      />\n    </Suspense>\n  )\n}\n\nexport default UpdaterDialogWrapper\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/updater/updater-dialog.module.scss",
    "content": "@reference \"tailwindcss\";\n\n.UpdaterDialog {\n  .MarkdownContent {\n    h1 {\n      @apply pb-2 text-3xl font-bold;\n    }\n\n    h2 {\n      @apply pb-2 text-2xl font-bold;\n    }\n\n    h3 {\n      @apply pb-2 text-xl font-bold;\n    }\n\n    h4 {\n      @apply text-lg font-bold;\n    }\n\n    h5 {\n      @apply text-base font-bold;\n    }\n\n    h6 {\n      @apply text-sm font-bold;\n    }\n\n    p,\n    li {\n      @apply text-base;\n    }\n\n    a {\n      @apply text-blue-500 underline underline-offset-2;\n    }\n\n    ul {\n      @apply list-inside list-disc pb-4;\n    }\n\n    ol {\n      @apply list-inside list-decimal;\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/updater/updater-dialog.module.scss.d.ts",
    "content": "declare const classNames: {\n  readonly UpdaterDialog: 'UpdaterDialog'\n  readonly MarkdownContent: 'MarkdownContent'\n}\nexport default classNames\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/updater/updater-dialog.tsx",
    "content": "import { useLockFn } from 'ahooks'\nimport dayjs from 'dayjs'\nimport { useSetAtom } from 'jotai'\nimport { lazy, Suspense, useCallback, useState, useTransition } from 'react'\nimport { useTranslation } from 'react-i18next'\nimport { IS_NIGHTLY } from '@/consts'\nimport { UpdaterIgnoredAtom } from '@/store/updater'\nimport { formatError } from '@/utils'\nimport { message } from '@/utils/notification'\nimport { Button, LinearProgress } from '@mui/material'\nimport { cleanupProcesses, openThat } from '@nyanpasu/interface'\nimport { BaseDialog, BaseDialogProps, cn } from '@nyanpasu/ui'\nimport { relaunch } from '@tauri-apps/plugin-process'\nimport { DownloadEvent, type Update } from '@tauri-apps/plugin-updater'\nimport styles from './updater-dialog.module.scss'\n\nconst Markdown = lazy(() => import('react-markdown'))\n\nexport interface UpdaterDialogProps extends Omit<BaseDialogProps, 'title'> {\n  update: Update\n}\n\nexport default function UpdaterDialog({\n  open,\n  update,\n  onClose,\n  ...others\n}: UpdaterDialogProps) {\n  const { t } = useTranslation()\n  const setUpdaterIgnore = useSetAtom(UpdaterIgnoredAtom)\n  const [contentLength, setContentLength] = useState(0)\n  const [contentDownloaded, setContentDownloaded] = useState(0)\n  const [pending, startPending] = useTransition()\n  const progress =\n    contentDownloaded && contentLength\n      ? (contentDownloaded / contentLength) * 100\n      : 0\n  const date =\n    update.date ||\n    (typeof update.rawJson.pub_date === 'string'\n      ? update.rawJson.pub_date\n      : undefined)\n\n  console.info(date)\n\n  const onDownloadEvent = useCallback((e: DownloadEvent) => {\n    switch (e.event) {\n      case 'Started':\n        setContentLength(e.data.contentLength || 0)\n        break\n      case 'Progress':\n        setContentDownloaded((prev) => prev + e.data.chunkLength)\n        break\n    }\n  }, [])\n\n  const handleUpdate = useLockFn(async () => {\n    startPending(async () => {\n      try {\n        // Install the update. This will also restart the app on Windows!\n        await update.download(onDownloadEvent)\n        await cleanupProcesses()\n        // cleanup and stop core\n        await update.install()\n        // On macOS and Linux you will need to restart the app manually.\n        // You could use this step to display another confirmation dialog.\n        await relaunch()\n      } catch (e) {\n        console.error(e)\n        message(formatError(e), { kind: 'error', title: t('Error') })\n      }\n    })\n  })\n\n  const releasesPageUrl = IS_NIGHTLY\n    ? `https://github.com/libnyanpasu/clash-nyanpasu/releases/tag/pre-release`\n    : `https://github.com/libnyanpasu/clash-nyanpasu/releases/tag/v${update.version}`\n\n  return (\n    <BaseDialog\n      {...others}\n      title={t('updater.title')}\n      open={open}\n      onClose={() => {\n        setUpdaterIgnore(update.version) // TODO: control this behavior\n        onClose?.()\n      }}\n      onOk={handleUpdate}\n      loading={pending}\n      close={t('updater.close')}\n      ok={t('updater.update')}\n      divider\n    >\n      <div\n        className={cn(\n          'xs:min-w-[90vw] sm:min-w-[55vw] md:min-w-[33.3vw]',\n          styles.UpdaterDialog,\n        )}\n      >\n        <div className=\"flex items-center justify-between px-2 py-2\">\n          <div className=\"flex gap-3\">\n            <span className=\"text-xl font-bold\">{update.version}</span>\n            <span className=\"contents text-xs text-slate-500\">\n              {date\n                ? dayjs(date).format('YYYY-MM-DD HH:mm:ss')\n                : 'Invalid date'}\n            </span>\n          </div>\n          <Button\n            variant=\"contained\"\n            size=\"small\"\n            onClick={() => {\n              openThat(releasesPageUrl)\n            }}\n          >\n            {t('updater.go')}\n          </Button>\n        </div>\n        <div\n          className={cn('h-[50vh] overflow-y-auto p-4', styles.MarkdownContent)}\n        >\n          <Suspense fallback={<div>{t('loading')}</div>}>\n            <Markdown\n              components={{\n                a(props) {\n                  const { children, node, ...rest } = props\n                  return (\n                    <a\n                      {...rest}\n                      onClick={(e) => {\n                        e.preventDefault()\n                        e.stopPropagation()\n                        if (typeof node?.properties.href === 'string') {\n                          openThat(node.properties.href)\n                        }\n                      }}\n                    >\n                      {children}\n                    </a>\n                  )\n                },\n              }}\n            >\n              {update.body || 'New version available.'}\n            </Markdown>\n          </Suspense>\n        </div>\n        {pending && (\n          <div className=\"mt-2 flex items-center gap-2\">\n            <LinearProgress\n              className=\"flex-1\"\n              variant=\"determinate\"\n              value={progress}\n            />\n            <span className=\"text-xs text-slate-500\">\n              {progress.toFixed(2)}%\n            </span>\n          </div>\n        )}\n      </div>\n    </BaseDialog>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/window/window-control.tsx",
    "content": "import CloseRounded from '~icons/material-symbols/close-rounded'\nimport Crop54Outline from '~icons/material-symbols/crop-5-4-outline'\nimport FilterNoneRounded from '~icons/material-symbols/filter-none-outline-rounded'\nimport HorizontalRuleRounded from '~icons/material-symbols/horizontal-rule-rounded'\nimport PushPin from '~icons/material-symbols/push-pin'\nimport PushPinOutline from '~icons/material-symbols/push-pin-outline'\nimport { AnimatePresence, motion } from 'framer-motion'\nimport { ComponentProps, useCallback } from 'react'\nimport { Button, ButtonProps } from '@/components/ui/button'\nimport useWindowMaximized from '@/hooks/use-window-maximized'\nimport { useSetting } from '@nyanpasu/interface'\nimport { cn } from '@nyanpasu/ui'\nimport { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'\n\nconst appWindow = getCurrentWebviewWindow()\n\nconst CtrlButton = ({ className, ...props }: ButtonProps) => {\n  return (\n    <Button\n      className={cn(\n        'hover:bg-primary-container dark:hover:bg-on-primary size-8',\n        className,\n      )}\n      icon\n      {...props}\n    />\n  )\n}\n\nconst AlwaysOnTopButton = () => {\n  const { value: alwaysOnTop, upsert: upsertAlwaysOnTop } =\n    useSetting('always_on_top')\n\n  const handleToggleAlwaysOnTop = useCallback(async () => {\n    await upsertAlwaysOnTop(!alwaysOnTop)\n    await appWindow.setAlwaysOnTop(!alwaysOnTop)\n  }, [alwaysOnTop, upsertAlwaysOnTop])\n\n  return (\n    <CtrlButton\n      onClick={handleToggleAlwaysOnTop}\n      data-slot=\"window-control-always-on-top-button\"\n    >\n      <AnimatePresence mode=\"wait\">\n        <motion.span\n          key={alwaysOnTop ? 'pinned' : 'unpinned'}\n          className=\"flex items-center justify-center\"\n          initial={{ opacity: 0, scale: 0.7 }}\n          animate={{ opacity: 1, scale: 1 }}\n          exit={{ opacity: 0, rotate: 35, scale: 0.8 }}\n          transition={{ duration: 0.2 }}\n        >\n          {alwaysOnTop ? (\n            <PushPin className=\"size-5 rotate-15\" />\n          ) : (\n            <PushPinOutline className=\"size-5 rotate-15\" />\n          )}\n        </motion.span>\n      </AnimatePresence>\n    </CtrlButton>\n  )\n}\n\nconst MinimizeButton = () => {\n  const handleMinimize = useCallback(async () => {\n    await appWindow.minimize()\n  }, [])\n\n  return (\n    <CtrlButton\n      onClick={handleMinimize}\n      data-slot=\"window-control-minimize-button\"\n    >\n      <HorizontalRuleRounded className=\"size-5\" />\n    </CtrlButton>\n  )\n}\n\nconst MaximizeButton = () => {\n  const { isMaximized, toggleMaximize } = useWindowMaximized()\n\n  return (\n    <CtrlButton\n      onClick={toggleMaximize}\n      data-slot=\"window-control-maximize-button\"\n    >\n      {isMaximized ? (\n        <FilterNoneRounded className=\"size-4.5 rotate-180\" />\n      ) : (\n        <Crop54Outline className=\"size-4.5\" />\n      )}\n    </CtrlButton>\n  )\n}\n\nconst CloseButton = ({\n  beforeClose,\n}: {\n  beforeClose?: () => Promise<boolean>\n}) => {\n  const handleClose = useCallback(async () => {\n    if (beforeClose) {\n      const result = await beforeClose()\n\n      if (!result) {\n        return\n      }\n    }\n\n    await appWindow.close()\n  }, [beforeClose])\n\n  return (\n    <CtrlButton onClick={handleClose} data-slot=\"window-control-close-button\">\n      <CloseRounded className=\"size-5.5\" />\n    </CtrlButton>\n  )\n}\n\nexport default function WindowControl({\n  className,\n  hiddenAlwaysOnTop,\n  beforeClose,\n}: ComponentProps<'div'> & {\n  hiddenAlwaysOnTop?: boolean\n  beforeClose?: ComponentProps<typeof CloseButton>['beforeClose']\n}) {\n  return (\n    <div\n      className={cn('flex gap-1', className)}\n      data-slot=\"window-control\"\n      data-tauri-drag-region\n    >\n      {!hiddenAlwaysOnTop && <AlwaysOnTopButton />}\n\n      <MinimizeButton />\n\n      <MaximizeButton />\n\n      <CloseButton beforeClose={beforeClose} />\n    </div>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/window/window-header.tsx",
    "content": "import { ComponentProps } from 'react'\nimport { cn } from '@nyanpasu/ui'\n\nexport default function WindowHeader({\n  className,\n  ...props\n}: ComponentProps<'div'>) {\n  return (\n    <div\n      className={cn(\n        'dark:bg-primary-container bg-inverse-primary flex h-10 w-full',\n        className,\n      )}\n      data-slot=\"app-header\"\n      data-tauri-drag-region\n      {...props}\n    />\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/components/window/window-title.tsx",
    "content": "import { ComponentProps } from 'react'\nimport AnimatedLogo from '@/components/logo/animated-logo'\nimport { cn } from '@nyanpasu/ui'\n\nexport default function WindowTitle({\n  children,\n  className,\n  ...props\n}: ComponentProps<'div'>) {\n  return (\n    <div\n      className={cn('flex items-center gap-2', className)}\n      data-slot=\"app-header-logo-container\"\n      data-tauri-drag-region\n      {...props}\n    >\n      <AnimatedLogo className=\"size-5\" />\n\n      {children}\n    </div>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/consts.ts",
    "content": "/* eslint-disable */\n// @ts-nocheck\n\nimport { getSystem } from '@nyanpasu/ui'\n\nexport const OS = getSystem()\n\nexport const isWindows = OS === 'windows'\n\nexport const isMacOS = OS === 'macos'\n\nexport const isLinux = OS === 'linux'\n\nexport const IS_NIGHTLY = window.__IS_NIGHTLY__ === true\n"
  },
  {
    "path": "frontend/nyanpasu/src/hooks/theme.ts",
    "content": "import { SxProps, Theme } from '@mui/material'\n\nconst delayThresholds = [\n  {\n    max: -1,\n    sx: (theme: Theme) => ({ color: theme.vars.palette.text.primary }),\n  },\n  {\n    max: 0,\n    sx: (theme: Theme) => ({ color: theme.vars.palette.text.secondary }),\n  },\n  {\n    max: 1,\n    sx: (theme: Theme) => ({ color: theme.vars.palette.text.secondary }),\n  },\n  {\n    max: 500,\n    sx: (theme: Theme) => ({ color: theme.vars.palette.success.main }),\n  },\n  {\n    max: 2000,\n    sx: (theme: Theme) => ({ color: theme.vars.palette.warning.main }),\n  },\n  {\n    max: Infinity,\n    sx: (theme: Theme) => ({ color: theme.vars.palette.error.main }),\n  },\n]\n\nexport const useColorSxForDelay = (delay: number): SxProps<Theme> => {\n  if (delay === -1) {\n    return delayThresholds[0].sx\n  }\n\n  return (\n    delayThresholds.find((threshold) => delay <= threshold.max)?.sx ||\n    delayThresholds[delayThresholds.length - 1].sx\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/hooks/use-consts.ts",
    "content": "import useSWR, { SWRConfiguration } from 'swr'\nimport { isAppImage } from '@nyanpasu/interface'\n\nexport const useIsAppImage = (config?: Partial<SWRConfiguration>) => {\n  return useSWR<boolean>('/api/is_appimage', isAppImage, {\n    ...config,\n    revalidateOnFocus: false,\n    revalidateOnReconnect: false,\n    refreshInterval: 0,\n  })\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/hooks/use-core-icon.ts",
    "content": "import ClashRs from '@/assets/image/core/clash-rs.png'\nimport ClashMeta from '@/assets/image/core/clash.meta.png'\nimport Clash from '@/assets/image/core/clash.png'\nimport { ClashCore } from '@nyanpasu/interface'\n\nexport default function useCoreIcon(core?: ClashCore | null) {\n  switch (core) {\n    case 'clash':\n      return Clash\n    case 'clash-rs':\n    case 'clash-rs-alpha':\n      return ClashRs\n    case 'mihomo':\n    case 'mihomo-alpha':\n      return ClashMeta\n    // sync from backend\n    default:\n      return ClashMeta\n  }\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/hooks/use-current-core-icon.ts",
    "content": "import { useSetting } from '@nyanpasu/interface'\nimport useCoreIcon from './use-core-icon'\n\nexport default function useCurrentCoreIcon() {\n  const { value: currentCore } = useSetting('clash_core')\n\n  return useCoreIcon(currentCore)\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/hooks/use-element-breakpoints.ts",
    "content": "import { RefObject, useEffect, useState } from 'react'\n\nexport const useElementBreakpoints = (\n  element: RefObject<HTMLElement>,\n  breakpoints: { [key: string]: number },\n  defaultBreakpoint: string,\n) => {\n  const [breakpoint, setBreakpoint] = useState<string | null>(defaultBreakpoint)\n\n  useEffect(() => {\n    let observer: ResizeObserver | null = null\n    if (element.current) {\n      observer = new ResizeObserver(() => {\n        const { width } = element.current.getBoundingClientRect()\n        const breakpoint = Object.entries(breakpoints).find(\n          ([, value]) => width >= value,\n        )?.[0]\n        if (breakpoint) {\n          setBreakpoint(breakpoint)\n        }\n      })\n      observer.observe(element.current)\n    }\n    return () => observer?.disconnect()\n  }, [element, breakpoints])\n\n  return breakpoint\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/hooks/use-is-moblie.tsx",
    "content": "import { useBreakpoint } from '@nyanpasu/ui'\n\nexport default function useIsMobile() {\n  const breakpoint = useBreakpoint()\n\n  const isMobile = breakpoint === 'sm' || breakpoint === 'xs'\n\n  return isMobile\n}\n\nexport function useIsMobileOrTablet() {\n  const breakpoint = useBreakpoint()\n\n  const isMobileOrTablet =\n    breakpoint === 'sm' || breakpoint === 'xs' || breakpoint === 'md'\n\n  return isMobileOrTablet\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/hooks/use-lock-fn.ts",
    "content": "/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { useCallback, useRef } from 'react'\n\nexport type LockFn<P extends any[] = any[], T = any> = (\n  ...args: P\n) => Promise<T>\n\n/**\n * Hook similar to ahooks useLockFn - prevents concurrent execution of async functions\n * When the function is executing, subsequent calls will be ignored until the function completes\n */\nexport function useLockFn<P extends any[] = any[], T = any>(\n  fn: LockFn<P, T>,\n): LockFn<P, T> {\n  const lockRef = useRef(false)\n  const fnRef = useRef(fn)\n\n  // Update ref on each render to ensure we have the latest fn\n  fnRef.current = fn\n\n  return useCallback(async (...args: P): Promise<T> => {\n    if (lockRef.current) {\n      // return Promise.reject(new Error(\"Function is locked\"));\n      console.warn(`Function is locked: ${fnRef.current.name}`)\n      return undefined as T\n    }\n\n    lockRef.current = true\n\n    try {\n      const result = await fnRef.current(...args)\n      return result\n    } finally {\n      lockRef.current = false\n    }\n  }, [])\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/hooks/use-store.ts",
    "content": "import { useAtom } from 'jotai'\nimport { useEffect } from 'react'\nimport { dispatchStorageValueChanged } from '@/services/storage'\nimport { coreTypeAtom } from '@/store/clash'\nimport { useSetting } from '@nyanpasu/interface'\nimport { listen, UnlistenFn } from '@tauri-apps/api/event'\n\nexport function useCoreType() {\n  const [coreType, setCoreType] = useAtom(coreTypeAtom)\n\n  const { upsert } = useSetting('clash_core')\n\n  const setter = (value: typeof coreType) => {\n    setCoreType(value)\n    upsert(value)\n  }\n  return [coreType, setter] as const\n}\n\nexport function useNyanpasuStorageSubscribers() {\n  useEffect(() => {\n    let unlisten: UnlistenFn | null = null\n    listen<[string, string | null]>('storage_value_changed', (event) => {\n      const [key, value] = event.payload\n      dispatchStorageValueChanged(\n        key,\n        typeof value === 'string' ? JSON.parse(value) : value,\n      )\n    }).then((fn) => {\n      unlisten = fn\n    })\n    return () => {\n      if (unlisten) {\n        unlisten()\n      }\n    }\n  })\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/hooks/use-updater.ts",
    "content": "import { useAtomValue, useSetAtom } from 'jotai'\nimport { useEffect, useState } from 'react'\nimport { OS } from '@/consts'\nimport { UpdaterIgnoredAtom, UpdaterInstanceAtom } from '@/store/updater'\nimport { commands, unwrapResult, useSetting } from '@nyanpasu/interface'\nimport { Update } from '@tauri-apps/plugin-updater'\nimport { useIsAppImage } from './use-consts'\n\nexport function useUpdaterPlatformSupported() {\n  const [supported, setSupported] = useState(false)\n  const isAppImage = useIsAppImage()\n  useEffect(() => {\n    switch (OS) {\n      case 'macos':\n      case 'windows':\n        setSupported(true)\n        break\n      case 'linux':\n        setSupported(!!isAppImage.data)\n        break\n    }\n  }, [isAppImage.data])\n  return supported\n}\n\nexport async function checkUpdate() {\n  const metadata = unwrapResult(await commands.checkUpdate())\n  if (metadata) {\n    return new Update({\n      rid: metadata.rid,\n      currentVersion: metadata.current_version,\n      version: metadata.version,\n      rawJson: metadata.raw_json as Record<string, unknown>,\n    })\n  }\n  return null\n}\n\nexport function useUpdater() {\n  const { value: enableAutoCheckUpdate } = useSetting(\n    'enable_auto_check_update',\n  )\n  const updaterIgnored = useAtomValue(UpdaterIgnoredAtom)\n  const setUpdaterInstance = useSetAtom(UpdaterInstanceAtom)\n  const isPlatformSupported = useUpdaterPlatformSupported()\n\n  useEffect(() => {\n    const run = async () => {\n      if (enableAutoCheckUpdate && isPlatformSupported) {\n        const updater = await checkUpdate()\n        if (updater && updaterIgnored !== updater.version) {\n          setUpdaterInstance(updater)\n        }\n      }\n    }\n    run().catch(console.error)\n  }, [\n    isPlatformSupported,\n    enableAutoCheckUpdate,\n    setUpdaterInstance,\n    updaterIgnored,\n  ])\n}\n\nexport const UpdaterProvider = () => {\n  useUpdater()\n\n  return null\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/hooks/use-visibility.ts",
    "content": "import { useEffect, useState } from 'react'\n\nexport const useVisibility = () => {\n  const [visible, setVisible] = useState(true)\n\n  useEffect(() => {\n    const handleVisibilityChange = () => {\n      setVisible(document.visibilityState === 'visible')\n    }\n\n    const handleFocus = () => setVisible(true)\n    const handleClick = () => setVisible(true)\n\n    handleVisibilityChange()\n    document.addEventListener('focus', handleFocus)\n    document.addEventListener('pointerdown', handleClick)\n    document.addEventListener('visibilitychange', handleVisibilityChange)\n\n    return () => {\n      document.removeEventListener('focus', handleFocus)\n      document.removeEventListener('pointerdown', handleClick)\n      document.removeEventListener('visibilitychange', handleVisibilityChange)\n    }\n  }, [])\n\n  return visible\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/hooks/use-window-maximized.ts",
    "content": "import { useCallback, useEffect, useRef } from 'react'\nimport { isMacOS } from '@/consts'\nimport { useSuspenseQuery } from '@tanstack/react-query'\nimport { listen, TauriEvent, UnlistenFn } from '@tauri-apps/api/event'\nimport { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'\n\nconst appWindow = getCurrentWebviewWindow()\n\nconst IS_MAXIMIZED_QUERY_KEY = 'isMaximized'\n\nexport default function useWindowMaximized() {\n  const unlistenRef = useRef<UnlistenFn | null>(null)\n\n  const query = useSuspenseQuery({\n    queryKey: [IS_MAXIMIZED_QUERY_KEY],\n    queryFn: async () => {\n      // why maximized on macOS is fullscreen?\n      if (isMacOS) {\n        return await appWindow.isFullscreen()\n      }\n\n      return await appWindow.isMaximized()\n    },\n  })\n\n  const handleToggleMaximize = useCallback(async () => {\n    await appWindow.toggleMaximize()\n    await query.refetch()\n  }, [query])\n\n  useEffect(() => {\n    listen(TauriEvent.WINDOW_RESIZED, async () => {\n      await query.refetch()\n    })\n      .then((unlisten) => {\n        unlistenRef.current = unlisten\n      })\n      .catch((error) => {\n        console.error(error)\n      })\n  }, [query])\n\n  return {\n    isMaximized: query.data,\n    toggleMaximize: handleToggleMaximize,\n    ...query,\n  }\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/locales/en.json",
    "content": "{\n  \"seconds\": \"s\",\n  \"minutes\": \"min(s)\",\n  \"Ok\": \"OK\",\n  \"Close\": \"Close\",\n  \"label_dashboard\": \"Dashboard\",\n  \"label_proxies\": \"Proxies\",\n  \"label_profiles\": \"Profiles\",\n  \"label_connections\": \"Connections\",\n  \"label_logs\": \"Logs\",\n  \"label_rules\": \"Rules\",\n  \"label_settings\": \"Settings\",\n  \"label_providers\": \"Providers\",\n  \"Connections\": \"Connections\",\n  \"Upload Traffic\": \"Upload\",\n  \"Download Traffic\": \"Download\",\n  \"Total\": \"Total\",\n  \"Memory\": \"Memory\",\n  \"Active Connections\": \"Active Connections\",\n  \"Timeout\": \"Timeout\",\n  \"Click to Refresh Now\": \"Click to Refresh Now\",\n  \"No Proxies\": \"No Proxies\",\n  \"Direct Mode\": \"Direct Mode\",\n  \"Rules\": \"Rules\",\n  \"No Rules\": \"No Rules\",\n  \"Logs\": \"Logs\",\n  \"No Logs\": \"No Logs\",\n  \"Clear\": \"Clear\",\n  \"Proxies\": \"Proxies\",\n  \"Proxy Groups\": \"Proxy Groups\",\n  \"rule\": \"rule\",\n  \"global\": \"global\",\n  \"direct\": \"direct\",\n  \"script\": \"script\",\n  \"Dashboard\": \"Dashboard\",\n  \"Profiles\": \"Profiles\",\n  \"Profile URL\": \"Profile URL\",\n  \"Download\": \"Download\",\n  \"Paste\": \"Paste\",\n  \"Import\": \"Import\",\n  \"New\": \"New\",\n  \"Edit Profile\": \"Edit Profile\",\n  \"Create Profile\": \"Create Profile\",\n  \"New Profile\": \"New Profile\",\n  \"Choose File\": \"Choose File\",\n  \"Profile\": \"Profile\",\n  \"Close All\": \"Close All\",\n  \"Menu\": \"Menu\",\n  \"Select\": \"Select\",\n  \"Applying Profile\": \"Applying Profile...\",\n  \"Edit Info\": \"Edit Info\",\n  \"Proxy Chains\": \"Chains\",\n  \"Global Proxy Chains\": \"Global Chains\",\n  \"New Chain\": \"New Chain\",\n  \"New Script\": \"New Script\",\n  \"Edit Script\": \"Edit Script\",\n  \"Please fix the error before submitting\": \"Please fix the error before submitting.\",\n  \"Open\": \"Open\",\n  \"Open File\": \"Open File\",\n  \"Update\": \"Update\",\n  \"Update(Proxy)\": \"Update(Proxy)\",\n  \"Delete\": \"Delete\",\n  \"Enable\": \"Enable\",\n  \"Disable\": \"Disable\",\n  \"Refresh\": \"Refresh\",\n  \"To Top\": \"To Top\",\n  \"To End\": \"To End\",\n  \"Update All Profiles\": \"Update All Profiles\",\n  \"View Runtime Config\": \"View Runtime Config\",\n  \"Reactivate Profiles\": \"Reactivate Profiles\",\n  \"Locate\": \"Locate\",\n  \"Latency check\": \"Latency check\",\n  \"Sort by default\": \"Sort by default\",\n  \"Sort by latency\": \"Sort by latency\",\n  \"Sort by name\": \"Sort by name\",\n  \"Latency check URL\": \"Latency check URL\",\n  \"Proxy detail\": \"Proxy detail\",\n  \"Filter\": \"Filter\",\n  \"Filter conditions\": \"Filter conditions\",\n  \"Refresh profiles\": \"Refresh profiles\",\n  \"Connection Columns\": \"Connection Columns\",\n  \"Actions\": \"Actions\",\n  \"Host\": \"Host\",\n  \"Process\": \"Process\",\n  \"Downloaded\": \"Downloaded\",\n  \"Uploaded\": \"Uploaded\",\n  \"DL Speed\": \"DL Speed\",\n  \"UL Speed\": \"UL Speed\",\n  \"Chains\": \"Chains\",\n  \"Rule\": \"Rule\",\n  \"Time\": \"Time\",\n  \"Source\": \"Source\",\n  \"Destination IP\": \"Destination IP\",\n  \"Destination ASN\": \"Destination ASN\",\n  \"Type\": \"Type\",\n  \"Connection Detail\": \"Connection Detail\",\n  \"Metadata\": \"Metadata\",\n  \"Remote\": \"Remote\",\n  \"Local\": \"Local\",\n  \"Remote Profile\": \"Remote Profile\",\n  \"Local Profile\": \"Local Profile\",\n  \"Name\": \"Name\",\n  \"Descriptions\": \"Descriptions\",\n  \"Subscription URL\": \"Subscription URL\",\n  \"User Agent\": \"User Agent\",\n  \"Update Interval\": \"Update Interval\",\n  \"Use System Proxy\": \"Use System Proxy\",\n  \"Use Clash Proxy\": \"Use Clash Proxy\",\n  \"No Connections\": \"No Connections\",\n  \"Tray Icons\": \"Tray Icons\",\n  \"Set\": \"Set\",\n  \"Edit\": \"Edit\",\n  \"Reset\": \"Reset\",\n  \"Hotkeys\": \"Hotkeys\",\n  \"Feedback\": \"Feedback\",\n  \"Settings\": \"Settings\",\n  \"Clash Setting\": \"Clash Setting\",\n  \"System Setting\": \"System Setting\",\n  \"Nyanpasu Setting\": \"Nyanpasu Setting\",\n  \"Allow LAN\": \"Allow LAN\",\n  \"IPv6\": \"IPv6\",\n  \"TUN Stack\": \"TUN Stack\",\n  \"Log Level\": \"Log Level\",\n  \"Clash Port\": \"Clash Port\",\n  \"Mixed Port\": \"Mixed Port\",\n  \"Random Port\": \"Random Port\",\n  \"After restart to take effect\": \"After restart to take effect.\",\n  \"Clash External Controll\": \"Clash External Controll\",\n  \"External Controller\": \"External Controller\",\n  \"Port Strategy\": \"Port Strategy\",\n  \"Allow Fallback\": \"Allow Fallback\",\n  \"Fixed\": \"Fixed\",\n  \"Random\": \"Random\",\n  \"Core Secret\": \"Core Secret\",\n  \"Clash Core\": \"Clash Core\",\n  \"TUN Mode\": \"TUN Mode\",\n  \"System Service\": \"System Service\",\n  \"Service Mode\": \"Service Mode\",\n  \"Initiating Behavior\": \"Initiating Behavior\",\n  \"Auto Start\": \"Auto Start\",\n  \"Silent Start\": \"Silent Start\",\n  \"System Proxy\": \"System Proxy\",\n  \"Open UWP Tool\": \"Open UWP Tool\",\n  \"System Proxy Setting\": \"System Proxy Setting\",\n  \"Proxy Guard\": \"Proxy Guard\",\n  \"Guard Interval\": \"Guard Interval\",\n  \"The interval must be greater than 0 second\": \"The interval must be greater than 0 second.\",\n  \"Proxy Bypass\": \"Proxy Bypass\",\n  \"Toggle\": \"Toggle\",\n  \"Current System Proxy\": \"Current System Proxy\",\n  \"User Interface\": \"User Interface\",\n  \"Theme Mode\": \"Theme Mode\",\n  \"Theme Blur\": \"Theme Blur\",\n  \"Theme Setting\": \"Theme Setting\",\n  \"Icon Navigation Bar\": \"Icon Navigation Bar\",\n  \"Network Statistic Widget\": \"Network Statistic Widget\",\n  \"Network Statistic Widget Variant\": \"Widget Variant\",\n  \"Layout Setting\": \"Layout Setting\",\n  \"Miscellaneous\": \"Miscellaneous\",\n  \"Hotkey Setting\": \"Hotkey Setting\",\n  \"Traffic Graph\": \"Traffic Graph\",\n  \"Memory Usage\": \"Memory Usage\",\n  \"Page Transition Animation\": \"Page Transition Animation\",\n  \"Page Transition Animation Slide\": \"Slide\",\n  \"Page Transition Animation Blur\": \"Blur\",\n  \"Page Transition Animation Transparent\": \"Transparent\",\n  \"Page Transition Animation None\": \"None\",\n  \"Language\": \"Language\",\n  \"Path Config\": \"Path Config\",\n  \"Migrate Config Dir\": \"Migrate Config Dir\",\n  \"Open Config Dir\": \"Open Config Dir\",\n  \"Open Data Dir\": \"Open Data Dir\",\n  \"Open Core Dir\": \"Open Core Dir\",\n  \"Open Log Dir\": \"Open Log Dir\",\n  \"Auto Check Updates\": \"Auto Check Updates\",\n  \"Check for Updates\": \"Check for Updates\",\n  \"Nyanpasu Version\": \"Nyanpasu Version\",\n  \"theme.light\": \"Light\",\n  \"theme.dark\": \"Dark\",\n  \"theme.system\": \"System\",\n  \"Clash Field\": \"Clash Field\",\n  \"Original Config\": \"Original Config\",\n  \"Runtime Config\": \"Runtime Config\",\n  \"Console\": \"Console\",\n  \"ReadOnly\": \"ReadOnly\",\n  \"Check Updates\": \"Check Updates\",\n  \"Restart\": \"Restart\",\n  \"Update Core\": \"Update Core\",\n  \"Tasks\": \"Tasks\",\n  \"Auto Log Clean\": \"Auto Log Clean\",\n  \"Never Clean\": \"Never\",\n  \"Retain 3 Days\": \"Retain 3 Days\",\n  \"Retain 7 Days\": \"Retain 7 Days\",\n  \"Retain 30 Days\": \"Retain 30 Days\",\n  \"Retain 90 Days\": \"Retain 90 Days\",\n  \"Max Log Files\": \"Max Log Files\",\n  \"Collect Logs\": \"Collect Logs\",\n  \"Back\": \"Back\",\n  \"Save\": \"Save\",\n  \"Cancel\": \"Cancel\",\n  \"Default\": \"Default\",\n  \"Download Speed\": \"Download Speed\",\n  \"Upload Speed\": \"Upload Speed\",\n  \"open_or_close_dashboard\": \"Open/Close Dashboard\",\n  \"clash_mode_rule\": \"Rule Mode\",\n  \"clash_mode_global\": \"Global Mode\",\n  \"clash_mode_direct\": \"Direct Mode\",\n  \"clash_mode_script\": \"Script Mode\",\n  \"toggle_system_proxy\": \"Toggle System Proxy\",\n  \"enable_system_proxy\": \"Enable System Proxy\",\n  \"disable_system_proxy\": \"Disable System Proxy\",\n  \"toggle_tun_mode\": \"Toggle TUN Mode\",\n  \"enable_tun_mode\": \"Enable TUN Mode\",\n  \"disable_tun_mode\": \"Disable TUN Mode\",\n  \"App Log Level\": \"App Log Level\",\n  \"Auto Close Connections\": \"Auto Close Connections\",\n  \"Enable Clash Fields Filter\": \"Enable Clash Fields Filter\",\n  \"Enable Built-in Enhanced\": \"Enable Built-in Enhanced\",\n  \"Proxy Layout Column\": \"Proxy Layout Column\",\n  \"Default Latency Test\": \"Default Latency Test\",\n  \"Error\": \"Error\",\n  \"Successful\": \"Successful\",\n  \"Occupied\": \"Occupied\",\n  \"Disabled\": \"Disabled\",\n  \"Providers\": \"Providers\",\n  \"Proxies Providers\": \"Proxies Providers\",\n  \"Rules Providers\": \"Rules Providers\",\n  \"Update All Rules Providers\": \"Update All Rules Providers\",\n  \"Rule Set rules\": \"{{rule}} rules\",\n  \"Last Update\": \"Last Updated: {{fromNow}}\",\n  \"Successfully Updated Rules Providers\": \"Successfully Updated Rules Providers\",\n  \"Portable Update Error\": \"Portable Update is not supported. Please download the latest version from the official website.\",\n  \"Tray Proxies Selector\": \"Tray Proxies Selector\",\n  \"Hidden\": \"Hidden\",\n  \"Normal\": \"Normal\",\n  \"Submenu\": \"Submenu\",\n  \"Proxy Set proxies\": \"{{rule}} proxies\",\n  \"Update All Proxies Providers\": \"Update All Proxies Providers\",\n  \"Lighten Up Animation Effects\": \"Lighten Up Animation Effects\",\n  \"Subscription\": \"Subscription\",\n  \"FetchError\": \"Failed to fetch {{content}} due to network issue. Please check your network connection and try again later.\",\n  \"tun\": \"TUN Mode\",\n  \"normal\": \"Normal\",\n  \"system_proxy\": \"System Proxy\",\n  \"Proxy Takeover Status\": \"Proxy Takeover Status\",\n  \"Subscription Expires In\": \"Expires {{time}}\",\n  \"Subscription Updated At\": \"Updated at {{time}}\",\n  \"Choose file to import or leave it blank to create new one\": \"Choose a file to import or leave it blank to create new one.\",\n  \"updater\": {\n    \"title\": \"New version available\",\n    \"close\": \"Ignore\",\n    \"update\": \"Update Now\",\n    \"go\": \"View on GitHub\"\n  },\n  \"not_installed\": \"Not Installed\",\n  \"stopped\": \"Stopped\",\n  \"running\": \"Running\",\n  \"stopped_reason\": \"Stopped, Reason: {{reason}}\",\n  \"Current Status\": \"Current Status: {{status}}\",\n  \"Information: To enable service mode, make sure the Clash Nyanpasu service is installed and started\": \"Information: To enable service mode, make sure the Clash Nyanpasu service is installed and started.\",\n  \"Prompt\": \"Prompt\",\n  \"install\": \"install\",\n  \"uninstall\": \"uninstall\",\n  \"start\": \"start\",\n  \"stop\": \"stop\",\n  \"Failed to install\": \"Failed to install\",\n  \"Failed to uninstall\": \"Failed to uninstall\",\n  \"service_shortcuts\": {\n    \"title\": \"Service Shortcuts\",\n    \"core_status\": \"Core Status: \",\n    \"service_status\": \"Service Status: \",\n    \"core_started_by\": \"Started by {{by}}\",\n    \"last_status_changed_since\": \"Last status changed: {{time}}\"\n  },\n  \"service\": \"Service\",\n  \"UI\": \"UI\",\n  \"Service Manual Tips\": \"Service Manual Tips\",\n  \"Unable to operation the service automatically\": \"Unable to {{operation}} the service automatically. Please navigate to the core directory, run PowerShell as administrator on Windows or a terminal emulator on macOS/Linux, and execute the following commands:\",\n  \"Copy to clipboard\": \"Copy to clipboard\",\n  \"Copied to clipboard\": \"Copied to clipboard~\",\n  \"Failed to copy to clipboard\": \"Failed to copy to clipboard!\",\n  \"Successfully switched to the clash core\": \"Successfully switched to the {{core}} core.\",\n  \"Failed to switch. You could see the details in the log\": \"Failed to switch. You could see the details in the log. \\nError: {{error}}\",\n  \"Successfully restarted the core\": \"Successfully restarted the core.\",\n  \"Failed to restart. You could see the details in the log\": \"Failed to restart. You could see the details in the log.\\n\\nError:\",\n  \"Failed to fetch. Please check your network connection\": \"Failed to fetch. Please check your network connection.\",\n  \"Successfully updated the core\": \"Successfully updated the {{core}} core.\",\n  \"Failed to update\": \"Failed to update. {{error}}\",\n  \"Multiple directories are not supported\": \"Multiple directories are not supported.\",\n  \"Successfully changed the app directory\": \"Successfully changed the app directory.\",\n  \"Failed to migrate\": \"Failed to migrate. {{error}}\",\n  \"Web UI\": \"Web UI\",\n  \"New Item\": \"New Item\",\n  \"Edit Item\": \"Edit Item\",\n  \"Input\": \"Input\",\n  \"Support %host %port, and %secret\": \"Support %host, %port, and %secret\",\n  \"Replace host, port, and secret with\": \"Replace host, port, and secret with:\",\n  \"Result\": \"Result\"\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/locales/ru.json",
    "content": "{\n  \"seconds\": \"секунды\",\n  \"minutes\": \"минуты\",\n  \"Ok\": \"OK\",\n  \"Close\": \"Закрыть\",\n  \"label_dashboard\": \"Панель управления\",\n  \"label_proxies\": \"Прокси\",\n  \"label_profiles\": \"Профили\",\n  \"label_connections\": \"Соединения\",\n  \"label_logs\": \"Журналы\",\n  \"label_rules\": \"Правила\",\n  \"label_settings\": \"Настройки\",\n  \"label_providers\": \"Поставщики\",\n  \"Connections\": \"Соединения\",\n  \"Upload Traffic\": \"Отправка\",\n  \"Download Traffic\": \"Загрузка\",\n  \"Total\": \"Всего\",\n  \"Memory\": \"Память\",\n  \"Active Connections\": \"Активные соединения\",\n  \"Timeout\": \"Тайм-аут\",\n  \"Click to Refresh Now\": \"Нажмите для обновления\",\n  \"No Proxies\": \"Без прокси\",\n  \"Direct Mode\": \"Прямой режим\",\n  \"Rules\": \"Правила\",\n  \"No Rules\": \"Нет правил\",\n  \"Logs\": \"Журналы\",\n  \"No Logs\": \"Нет журналов\",\n  \"Clear\": \"Очистить\",\n  \"Proxies\": \"Прокси\",\n  \"Proxy Groups\": \"Группы прокси\",\n  \"rule\": \"правило\",\n  \"global\": \"глобальный\",\n  \"direct\": \"прямой\",\n  \"script\": \"скрипт\",\n  \"Dashboard\": \"Панель управления\",\n  \"Profiles\": \"Профили\",\n  \"Profile URL\": \"URL профиля\",\n  \"Download\": \"Загрузить\",\n  \"Paste\": \"Вставить\",\n  \"Import\": \"Импортировать\",\n  \"New\": \"Новый\",\n  \"Edit Profile\": \"Редактировать профиль\",\n  \"Create Profile\": \"Создать профиль\",\n  \"New Profile\": \"Новый профиль\",\n  \"Choose File\": \"Выбрать файл\",\n  \"Profile\": \"Профиль\",\n  \"Close All\": \"Закрыть все\",\n  \"Menu\": \"Меню\",\n  \"Select\": \"Выбрать\",\n  \"Applying Profile\": \"Применение профиля...\",\n  \"Edit Info\": \"Редактировать информацию\",\n  \"Proxy Chains\": \"Цепочки прокси\",\n  \"Global Proxy Chains\": \"Глобальные цепочки прокси\",\n  \"New Chain\": \"Новая цепочка\",\n  \"New Script\": \"Новый скрипт\",\n  \"Edit Script\": \"Редактировать скрипт\",\n  \"Please fix the error before submitting\": \"Исправьте ошибку перед отправкой.\",\n  \"Open\": \"Открыть\",\n  \"Open File\": \"Открыть файл\",\n  \"Update\": \"Обновить\",\n  \"Update(Proxy)\": \"Обновить(Прокси)\",\n  \"Delete\": \"Удалить\",\n  \"Enable\": \"Включить\",\n  \"Disable\": \"Отключить\",\n  \"Refresh\": \"Обновить\",\n  \"To Top\": \"На верх\",\n  \"To End\": \"В конец\",\n  \"Update All Profiles\": \"Обновить все профили\",\n  \"View Runtime Config\": \"Посмотреть конфигурацию времени выполнения\",\n  \"Reactivate Profiles\": \"Реактивировать профили\",\n  \"Locate\": \"Найти\",\n  \"Latency check\": \"Проверка задержки\",\n  \"Sort by default\": \"Сортировать по умолчанию\",\n  \"Sort by latency\": \"Сортировать по задержке\",\n  \"Sort by name\": \"Сортировать по имени\",\n  \"Latency check URL\": \"URL проверки задержки\",\n  \"Proxy detail\": \"Детали прокси\",\n  \"Filter\": \"Фильтр\",\n  \"Filter conditions\": \"Условия фильтрации\",\n  \"Refresh profiles\": \"Обновить профили\",\n  \"Connection Columns\": \"Колонки соединения\",\n  \"Actions\": \"Действия\",\n  \"Host\": \"Хост\",\n  \"Process\": \"Процесс\",\n  \"Downloaded\": \"Загружено\",\n  \"Uploaded\": \"Отправлено\",\n  \"DL Speed\": \"Скорость загрузки\",\n  \"UL Speed\": \"Скорость отправки\",\n  \"Chains\": \"Цепочки\",\n  \"Rule\": \"Правило\",\n  \"Time\": \"Время\",\n  \"Source\": \"Источник\",\n  \"Destination IP\": \"IP назначения\",\n  \"Destination ASN\": \"ASN назначения\",\n  \"Type\": \"Тип\",\n  \"Connection Detail\": \"Детали соединения\",\n  \"Metadata\": \"Метаданные\",\n  \"Remote\": \"Удаленный\",\n  \"Local\": \"Локальный\",\n  \"Remote Profile\": \"Удаленный профиль\",\n  \"Local Profile\": \"Локальный профиль\",\n  \"Name\": \"Имя\",\n  \"Descriptions\": \"Описания\",\n  \"Subscription URL\": \"URL подписки\",\n  \"User Agent\": \"Пользовательский агент\",\n  \"Update Interval\": \"Интервал обновления\",\n  \"Use System Proxy\": \"Использовать системный прокси\",\n  \"Use Clash Proxy\": \"Использовать прокси Clash\",\n  \"No Connections\": \"Нет соединений\",\n  \"Tray Icons\": \"Иконки в трее\",\n  \"Set\": \"Установить\",\n  \"Edit\": \"Редактировать\",\n  \"Reset\": \"Сбросить\",\n  \"Hotkeys\": \"Горячие клавиши\",\n  \"Feedback\": \"Обратная связь\",\n  \"Settings\": \"Настройки\",\n  \"Clash Setting\": \"Настройки Clash\",\n  \"System Setting\": \"Системные настройки\",\n  \"Nyanpasu Setting\": \"Настройки Nyanpasu\",\n  \"Allow LAN\": \"Разрешить LAN\",\n  \"IPv6\": \"IPv6\",\n  \"TUN Stack\": \"Стек TUN\",\n  \"Log Level\": \"Уровень логирования\",\n  \"Clash Port\": \"Порт Clash\",\n  \"Mixed Port\": \"Смешанный порт\",\n  \"Random Port\": \"Случайный порт\",\n  \"After restart to take effect\": \"Вступит в силу после перезагрузки.\",\n  \"Clash External Controll\": \"Внешнее управление Clash\",\n  \"External Controller\": \"Внешний контроллер\",\n  \"Port Strategy\": \"Стратегия порта\",\n  \"Allow Fallback\": \"Разрешить откат\",\n  \"Fixed\": \"Фиксированный\",\n  \"Random\": \"Случайный\",\n  \"Core Secret\": \"Секрет ядра\",\n  \"Clash Core\": \"Ядро Clash\",\n  \"TUN Mode\": \"Режим TUN\",\n  \"System Service\": \"Системная служба\",\n  \"Service Mode\": \"Режим службы\",\n  \"Initiating Behavior\": \"Инициирующее поведение\",\n  \"Auto Start\": \"Автозапуск\",\n  \"Silent Start\": \"Тихий запуск\",\n  \"System Proxy\": \"Системный прокси\",\n  \"Open UWP Tool\": \"Открыть инструмент UWP\",\n  \"System Proxy Setting\": \"Настройка системного прокси\",\n  \"Proxy Guard\": \"Охрана прокси\",\n  \"Guard Interval\": \"Интервал охраны\",\n  \"The interval must be greater than 0 second\": \"Интервал должен быть больше 0 секунд.\",\n  \"Proxy Bypass\": \"Обход прокси\",\n  \"Toggle\": \"Переключить\",\n  \"Current System Proxy\": \"Текущий системный прокси\",\n  \"User Interface\": \"Пользовательский интерфейс\",\n  \"Theme Mode\": \"Тематический режим\",\n  \"Theme Blur\": \"Размытие темы\",\n  \"Theme Setting\": \"Настройка темы\",\n  \"Icon Navigation Bar\": \"Панель навигации иконок\",\n  \"Network Statistic Widget\": \"Виджет статистики сети\",\n  \"Network Statistic Widget Variant\": \"Вариант виджета\",\n  \"Layout Setting\": \"Настройка макета\",\n  \"Miscellaneous\": \"Разное\",\n  \"Hotkey Setting\": \"Настройка горячих клавиш\",\n  \"Traffic Graph\": \"График трафика\",\n  \"Memory Usage\": \"Использование памяти\",\n  \"Page Transition Animation\": \"Анимация перехода страницы\",\n  \"Page Transition Animation Slide\": \"Слайд\",\n  \"Page Transition Animation Blur\": \"Размытие\",\n  \"Page Transition Animation Transparent\": \"Прозрачность\",\n  \"Page Transition Animation None\": \"Без анимации\",\n  \"Language\": \"Язык\",\n  \"Path Config\": \"Конфигурация пути\",\n  \"Migrate Config Dir\": \"Перенести каталог конфигурации\",\n  \"Open Config Dir\": \"Открыть каталог конфигурации\",\n  \"Open Data Dir\": \"Открыть каталог данных\",\n  \"Open Core Dir\": \"Открыть каталог ядра\",\n  \"Open Log Dir\": \"Открыть каталог журналов\",\n  \"Auto Check Updates\": \"Автоматическая проверка обновлений\",\n  \"Check for Updates\": \"Проверить обновления\",\n  \"Nyanpasu Version\": \"Версия Nyanpasu\",\n  \"theme.light\": \"Светлая\",\n  \"theme.dark\": \"Темная\",\n  \"theme.system\": \"Системная\",\n  \"Clash Field\": \"Поле Clash\",\n  \"Original Config\": \"Исходная конфигурация\",\n  \"Runtime Config\": \"Конфигурация времени выполнения\",\n  \"Console\": \"Консоль\",\n  \"ReadOnly\": \"Только чтение\",\n  \"Check Updates\": \"Проверить обновления\",\n  \"Restart\": \"Перезапустить\",\n  \"Update Core\": \"Обновить ядро\",\n  \"Tasks\": \"Задачи\",\n  \"Auto Log Clean\": \"Автоматическая очистка журналов\",\n  \"Never Clean\": \"Никогда не очищать\",\n  \"Retain 3 Days\": \"Хранить 3 дня\",\n  \"Retain 7 Days\": \"Хранить 7 дней\",\n  \"Retain 30 Days\": \"Хранить 30 дней\",\n  \"Retain 90 Days\": \"Хранить 90 дней\",\n  \"Max Log Files\": \"Максимум файлов журнала\",\n  \"Collect Logs\": \"Собрать журналы\",\n  \"Back\": \"Назад\",\n  \"Save\": \"Сохранить\",\n  \"Cancel\": \"Отмена\",\n  \"Default\": \"По умолчанию\",\n  \"Download Speed\": \"Скорость загрузки\",\n  \"Upload Speed\": \"Скорость отправки\",\n  \"open_or_close_dashboard\": \"Открыть/Закрыть панель управления\",\n  \"clash_mode_rule\": \"Режим правил\",\n  \"clash_mode_global\": \"Глобальный режим\",\n  \"clash_mode_direct\": \"Прямой режим\",\n  \"clash_mode_script\": \"Режим скрипта\",\n  \"toggle_system_proxy\": \"Переключить системный прокси\",\n  \"enable_system_proxy\": \"Включить системный прокси\",\n  \"disable_system_proxy\": \"Отключить системный прокси\",\n  \"toggle_tun_mode\": \"Переключить режим TUN\",\n  \"enable_tun_mode\": \"Включить режим TUN\",\n  \"disable_tun_mode\": \"Отключить режим TUN\",\n  \"App Log Level\": \"Уровень журнала приложения\",\n  \"Auto Close Connections\": \"Автоматическое закрытие соединений\",\n  \"Enable Clash Fields Filter\": \"Включить фильтр полей Clash\",\n  \"Enable Built-in Enhanced\": \"Включить встроенное улучшение\",\n  \"Proxy Layout Column\": \"Столбец макета прокси\",\n  \"Default Latency Test\": \"Тест задержки по умолчанию\",\n  \"Error\": \"Ошибка\",\n  \"Successful\": \"Успешно\",\n  \"Occupied\": \"Занято\",\n  \"Disabled\": \"Отключено\",\n  \"Providers\": \"Поставщики\",\n  \"Proxies Providers\": \"Поставщики прокси\",\n  \"Rules Providers\": \"Поставщики правил\",\n  \"Update All Rules Providers\": \"Обновить всех поставщиков правил\",\n  \"Rule Set rules\": \"{{rule}} правил\",\n  \"Last Update\": \"Последнее обновление: {{fromNow}}\",\n  \"Successfully Updated Rules Providers\": \"Успешно обновлены поставщики правил\",\n  \"Portable Update Error\": \"Портативное обновление не поддерживается. Пожалуйста, загрузите последнюю версию с официального сайта.\",\n  \"Tray Proxies Selector\": \"Селектор прокси в трее\",\n  \"Hidden\": \"Скрыто\",\n  \"Normal\": \"Нормальный\",\n  \"Submenu\": \"Подменю\",\n  \"Proxy Set proxies\": \"{{rule}} прокси\",\n  \"Update All Proxies Providers\": \"Обновить всех поставщиков прокси\",\n  \"Lighten Up Animation Effects\": \"Осветлить анимационные эффекты\",\n  \"Subscription\": \"Подписка\",\n  \"FetchError\": \"Не удалось получить {{content}} из-за проблемы с сетью. Пожалуйста, проверьте ваше сетевое соединение и попробуйте снова позже.\",\n  \"tun\": \"Режим TUN\",\n  \"normal\": \"Нормальный\",\n  \"system_proxy\": \"Системный прокси\",\n  \"Proxy Takeover Status\": \"Статус захвата прокси\",\n  \"Subscription Expires In\": \"Истекает {{time}}\",\n  \"Subscription Updated At\": \"Обновлено в {{time}}\",\n  \"Choose file to import or leave it blank to create new one\": \"Выберите файл для импорта или оставьте пустым, чтобы создать новый.\",\n  \"updater\": {\n    \"title\": \"Доступна новая версия\",\n    \"close\": \"Игнорировать\",\n    \"update\": \"Обновить сейчас\",\n    \"go\": \"Посмотреть на GitHub\"\n  },\n  \"not_installed\": \"Не установлено\",\n  \"stopped\": \"Остановлено\",\n  \"running\": \"Работает\",\n  \"stopped_reason\": \"Остановлено, Причина: {{reason}}\",\n  \"Current Status\": \"Текущее состояние: {{status}}\",\n  \"Information: To enable service mode, make sure the Clash Nyanpasu service is installed and started\": \"Информация: Чтобы включить режим службы, убедитесь, что служба Clash Nyanpasu установлена и запущена.\",\n  \"Prompt\": \"Подсказка\",\n  \"install\": \"установить\",\n  \"uninstall\": \"удалить\",\n  \"start\": \"запустить\",\n  \"stop\": \"остановить\",\n  \"Failed to install\": \"Не удалось установить\",\n  \"Failed to uninstall\": \"Не удалось удалить\",\n  \"service_shortcuts\": {\n    \"title\": \"Ярлыки службы\",\n    \"core_status\": \"Статус ядра: \",\n    \"service_status\": \"Статус службы: \",\n    \"core_started_by\": \"Запущено {{by}}\",\n    \"last_status_changed_since\": \"Последнее изменение статуса: {{time}}\"\n  },\n  \"service\": \"Служба\",\n  \"UI\": \"Пользовательский интерфейс\",\n  \"Service Manual Tips\": \"Руководство по обслуживанию\",\n  \"Unable to operation the service automatically\": \"Не удается автоматически {{operation}} службу. Пожалуйста, перейдите в каталог ядра, запустите PowerShell как администратор на Windows или эмулятор терминала на macOS/Linux и выполните следующие команды:\",\n  \"Copy to clipboard\": \"Копировать в буфер обмена\",\n  \"Copied to clipboard\": \"Копируется в буфер обмена~\",\n  \"Failed to copy to clipboard\": \"Не удалось копировать в буфер обмена!\",\n  \"Successfully switched to the clash core\": \"Успешно переключено на ядро {{core}}.\",\n  \"Failed to switch. You could see the details in the log\": \"Не удалось переключить. Вы можете увидеть детали в журнале. \\nОшибка: {{error}}\",\n  \"Successfully restarted the core\": \"Ядро успешно перезапущено.\",\n  \"Failed to restart. You could see the details in the log\": \"Не удалось перезапустить. Вы можете увидеть детали в журнале.\\n\\nОшибка:\",\n  \"Failed to fetch. Please check your network connection\": \"Не удалось получить. Пожалуйста, проверьте ваше сетевое соединение.\",\n  \"Successfully updated the core\": \"Ядро успешно обновлено {{core}}.\",\n  \"Failed to update\": \"Не удалось обновить. {{error}}\",\n  \"Multiple directories are not supported\": \"Несколько каталогов не поддерживаются.\",\n  \"Successfully changed the app directory\": \"Каталог приложения успешно изменен.\",\n  \"Failed to migrate\": \"Не удалось мигрировать. {{error}}\",\n  \"Web UI\": \"Веб-интерфейс\",\n  \"New Item\": \"Новый элемент\",\n  \"Edit Item\": \"Редактировать элемент\",\n  \"Input\": \"Ввод\",\n  \"Support %host %port, and %secret\": \"Поддерживает %host, %port и %secret\",\n  \"Replace host, port, and secret with\": \"Замените хост, порт и секрет на:\",\n  \"Result\": \"Результат\"\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/locales/zh-CN.json",
    "content": "{\n  \"seconds\": \"秒\",\n  \"minutes\": \"分钟\",\n  \"Ok\": \"确定\",\n  \"Close\": \"关闭\",\n  \"label_dashboard\": \"概览\",\n  \"label_proxies\": \"代理组\",\n  \"label_profiles\": \"配置\",\n  \"label_connections\": \"连接\",\n  \"label_logs\": \"日志\",\n  \"label_rules\": \"规则\",\n  \"label_settings\": \"设置\",\n  \"label_providers\": \"资源\",\n  \"Connections\": \"连接\",\n  \"Upload Traffic\": \"上传流量\",\n  \"Download Traffic\": \"下载流量\",\n  \"Total\": \"总量\",\n  \"Memory\": \"内存占用\",\n  \"Active Connections\": \"活动连接\",\n  \"Timeout\": \"超时\",\n  \"Click to Refresh Now\": \"点击立即刷新\",\n  \"No Proxies\": \"无代理\",\n  \"Direct Mode\": \"直连模式\",\n  \"Rules\": \"规则\",\n  \"No Rules\": \"无规则\",\n  \"Logs\": \"日志\",\n  \"No Logs\": \"无日志\",\n  \"Clear\": \"清除\",\n  \"Proxies\": \"代理\",\n  \"Proxy Groups\": \"代理组\",\n  \"rule\": \"规则\",\n  \"global\": \"全局\",\n  \"direct\": \"直连\",\n  \"script\": \"脚本\",\n  \"Dashboard\": \"概览\",\n  \"Profiles\": \"配置\",\n  \"Profile URL\": \"配置文件链接\",\n  \"Download\": \"下载\",\n  \"Paste\": \"粘贴\",\n  \"Import\": \"导入\",\n  \"New\": \"新建\",\n  \"Edit Profile\": \"编辑配置\",\n  \"Create Profile\": \"新建配置\",\n  \"New Profile\": \"新配置\",\n  \"Choose File\": \"选择文件\",\n  \"Profile\": \"配置文件\",\n  \"Close All\": \"关闭全部\",\n  \"Menu\": \"菜单\",\n  \"Select\": \"使用\",\n  \"Applying Profile\": \"正在应用配置……\",\n  \"Edit Info\": \"编辑信息\",\n  \"Proxy Chains\": \"局部链\",\n  \"Global Proxy Chains\": \"全局链\",\n  \"New Chain\": \"添加新链\",\n  \"New Script\": \"新脚本\",\n  \"Edit Script\": \"编辑脚本\",\n  \"Please fix the error before submitting\": \"请在提交前修正错误。\",\n  \"Open\": \"打开\",\n  \"Open File\": \"打开文件\",\n  \"Update\": \"更新\",\n  \"Update(Proxy)\": \"更新（使用代理）\",\n  \"Delete\": \"删除\",\n  \"Enable\": \"启用\",\n  \"Disable\": \"禁用\",\n  \"Refresh\": \"刷新\",\n  \"To Top\": \"移到最前\",\n  \"To End\": \"移到末尾\",\n  \"Update All Profiles\": \"更新所有配置\",\n  \"View Runtime Config\": \"查看运行配置\",\n  \"Reactivate Profiles\": \"重新激活配置\",\n  \"Locate\": \"当前使用节点\",\n  \"Latency check\": \"延迟测试\",\n  \"Sort by default\": \"默认排序\",\n  \"Sort by latency\": \"按延迟排序\",\n  \"Sort by name\": \"按名称排序\",\n  \"Latency check URL\": \"延迟测试链接\",\n  \"Proxy detail\": \"展示节点细节\",\n  \"Filter\": \"过滤节点\",\n  \"Filter conditions\": \"过滤条件\",\n  \"Refresh profiles\": \"刷新配置\",\n  \"Connection Columns\": \"连接表格列\",\n  \"Actions\": \"操作\",\n  \"Host\": \"主机\",\n  \"Process\": \"进程\",\n  \"Downloaded\": \"下载量\",\n  \"Uploaded\": \"上传量\",\n  \"DL Speed\": \"下载速度\",\n  \"UL Speed\": \"上传速度\",\n  \"Chains\": \"链路\",\n  \"Rule\": \"规则\",\n  \"Time\": \"连接时间\",\n  \"Source\": \"源地址\",\n  \"Destination IP\": \"目标地址\",\n  \"Destination ASN\": \"目标 ASN\",\n  \"Type\": \"类型\",\n  \"Connection Detail\": \"连接详情\",\n  \"Metadata\": \"元信息\",\n  \"Remote\": \"远程\",\n  \"Local\": \"本地\",\n  \"Remote Profile\": \"远程配置\",\n  \"Local Profile\": \"本地配置\",\n  \"Name\": \"名称\",\n  \"Descriptions\": \"描述\",\n  \"Subscription URL\": \"订阅链接\",\n  \"User Agent\": \"用户代理\",\n  \"Update Interval\": \"更新间隔\",\n  \"Use System Proxy\": \"使用系统代理更新\",\n  \"Use Clash Proxy\": \"使用 Clash 代理更新\",\n  \"No Connections\": \"无连接\",\n  \"Tray Icons\": \"托盘图标\",\n  \"Set\": \"设置\",\n  \"Edit\": \"编辑\",\n  \"Reset\": \"重置\",\n  \"Hotkeys\": \"快捷键\",\n  \"Feedback\": \"反馈\",\n  \"Settings\": \"设置\",\n  \"Clash Setting\": \"Clash 设置\",\n  \"System Setting\": \"系统设置\",\n  \"Nyanpasu Setting\": \"Nyanpasu 设置\",\n  \"Allow LAN\": \"局域网连接\",\n  \"IPv6\": \"IPv6\",\n  \"TUN Stack\": \"TUN 堆栈\",\n  \"Log Level\": \"日志等级\",\n  \"Clash Port\": \"Clash 端口\",\n  \"Mixed Port\": \"端口设置\",\n  \"Random Port\": \"随机端口\",\n  \"After restart to take effect\": \"重启后生效。\",\n  \"Clash External Controll\": \"Clash 外部控制\",\n  \"External Controller\": \"外部控制器监听地址\",\n  \"Port Strategy\": \"端口策略\",\n  \"Allow Fallback\": \"允许后备\",\n  \"Fixed\": \"固定\",\n  \"Random\": \"随机\",\n  \"Core Secret\": \"API 访问密钥\",\n  \"Clash Core\": \"Clash 内核\",\n  \"TUN Mode\": \"TUN 模式\",\n  \"System Service\": \"系统服务\",\n  \"Service Mode\": \"服务模式\",\n  \"Initiating Behavior\": \"启动行为\",\n  \"Auto Start\": \"开机自启\",\n  \"Silent Start\": \"静默启动\",\n  \"System Proxy\": \"系统代理\",\n  \"Open UWP Tool\": \"UWP 工具\",\n  \"System Proxy Setting\": \"系统代理设置\",\n  \"Proxy Guard\": \"系统代理守卫\",\n  \"Guard Interval\": \"代理守卫间隔\",\n  \"The interval must be greater than 0 second\": \"间隔时间必须大于 0 秒。\",\n  \"Proxy Bypass\": \"代理绕过\",\n  \"Toggle\": \"切换\",\n  \"Current System Proxy\": \"当前系统代理\",\n  \"User Interface\": \"用户界面\",\n  \"Theme Mode\": \"主题模式\",\n  \"Theme Blur\": \"背景模糊\",\n  \"Theme Setting\": \"主题设置\",\n  \"Icon Navigation Bar\": \"图标导航栏\",\n  \"Network Statistic Widget\": \"网络悬浮窗\",\n  \"Network Statistic Widget Variant\": \"网络悬浮窗样式\",\n  \"Layout Setting\": \"界面设置\",\n  \"Miscellaneous\": \"杂项设置\",\n  \"Hotkey Setting\": \"快捷键设置\",\n  \"Traffic Graph\": \"流量图显\",\n  \"Memory Usage\": \"内存使用\",\n  \"Page Transition Animation\": \"页面切换动画\",\n  \"Page Transition Animation Slide\": \"滑动\",\n  \"Page Transition Animation Blur\": \"模糊\",\n  \"Page Transition Animation Transparent\": \"透明\",\n  \"Page Transition Animation None\": \"无\",\n  \"Language\": \"语言设置\",\n  \"Path Config\": \"目录配置\",\n  \"Migrate Config Dir\": \"迁移配置目录\",\n  \"Open Config Dir\": \"配置目录\",\n  \"Open Data Dir\": \"数据目录\",\n  \"Open Core Dir\": \"内核目录\",\n  \"Open Log Dir\": \"日志目录\",\n  \"Auto Check Updates\": \"自动检查更新\",\n  \"Check for Updates\": \"检查更新\",\n  \"Nyanpasu Version\": \"Nyanpasu 版本\",\n  \"theme.light\": \"浅色\",\n  \"theme.dark\": \"深色\",\n  \"theme.system\": \"跟随系统\",\n  \"Clash Field\": \"Clash 字段\",\n  \"Original Config\": \"原始配置\",\n  \"Runtime Config\": \"运行配置\",\n  \"Console\": \"控制台\",\n  \"ReadOnly\": \"只读\",\n  \"Check Updates\": \"检查更新\",\n  \"Restart\": \"重启内核\",\n  \"Update Core\": \"更新内核\",\n  \"Tasks\": \"定期任务\",\n  \"Auto Log Clean\": \"自动清理日志\",\n  \"Never Clean\": \"不清理\",\n  \"Retain 3 Days\": \"保留 3 天\",\n  \"Retain 7 Days\": \"保留 7 天\",\n  \"Retain 30 Days\": \"保留 30 天\",\n  \"Retain 90 Days\": \"保留 90 天\",\n  \"Max Log Files\": \"最大日志文件数\",\n  \"Collect Logs\": \"收集日志\",\n  \"Back\": \"返回\",\n  \"Save\": \"保存\",\n  \"Cancel\": \"取消\",\n  \"Default\": \"默认\",\n  \"Download Speed\": \"下载速度\",\n  \"Upload Speed\": \"上传速度\",\n  \"open_or_close_dashboard\": \"打开/关闭面板\",\n  \"clash_mode_rule\": \"规则模式\",\n  \"clash_mode_global\": \"全局模式\",\n  \"clash_mode_direct\": \"直连模式\",\n  \"clash_mode_script\": \"脚本模式\",\n  \"toggle_system_proxy\": \"切换系统代理\",\n  \"enable_system_proxy\": \"开启系统代理\",\n  \"disable_system_proxy\": \"关闭系统代理\",\n  \"toggle_tun_mode\": \"切换 TUN 模式\",\n  \"enable_tun_mode\": \"开启 TUN 模式\",\n  \"disable_tun_mode\": \"关闭 TUN 模式\",\n  \"App Log Level\": \"应用程序日志等级\",\n  \"Auto Close Connections\": \"自动关闭连接\",\n  \"Enable Clash Fields Filter\": \"开启 Clash 字段过滤\",\n  \"Enable Built-in Enhanced\": \"开启内建增强功能\",\n  \"Proxy Layout Column\": \"代理页布局列数\",\n  \"Default Latency Test\": \"默认测试链接\",\n  \"Error\": \"错误\",\n  \"Successful\": \"成功\",\n  \"Occupied\": \"被占用\",\n  \"Disabled\": \"已禁用\",\n  \"Providers\": \"资源\",\n  \"Proxies Providers\": \"代理集\",\n  \"Rules Providers\": \"规则集\",\n  \"Update All Rules Providers\": \"全部更新\",\n  \"Rule Set rules\": \"{{rule}} 条规则\",\n  \"Last Update\": \"{{fromNow}}更新\",\n  \"Successfully Updated Rules Providers\": \"更新规则集成功\",\n  \"Portable Update Error\": \"便携版无法自动更新，请到 GitHub 下载最新版本\",\n  \"Tray Proxies Selector\": \"托盘代理选择\",\n  \"Hidden\": \"隐藏\",\n  \"Normal\": \"开启\",\n  \"Submenu\": \"子菜单\",\n  \"Proxy Set proxies\": \"{{rule}} 个节点\",\n  \"Update All Proxies Providers\": \"全部更新\",\n  \"Lighten Up Animation Effects\": \"减轻动画效果\",\n  \"Subscription\": \"订阅\",\n  \"FetchError\": \"由于网络问题，无法获取{{content}}内容。请检查网络连接或稍后再试。\",\n  \"tun\": \"TUN 模式\",\n  \"normal\": \"默认\",\n  \"system_proxy\": \"系统代理\",\n  \"Proxy Takeover Status\": \"代理接管状态\",\n  \"Subscription Expires In\": \"{{time}}到期\",\n  \"Subscription Updated At\": \"{{time}}更新\",\n  \"Choose file to import or leave it blank to create new one\": \"选择文件导入，或留空以新建配置。\",\n  \"updater\": {\n    \"title\": \"发现新版本\",\n    \"close\": \"忽略\",\n    \"update\": \"立即更新\",\n    \"go\": \"查看发布页\"\n  },\n  \"not_installed\": \"未安装\",\n  \"stopped\": \"已停止\",\n  \"running\": \"运行中\",\n  \"stopped_reason\": \"已停止，原因：{{reason}}\",\n  \"Current Status\": \"当前状态：{{status}}\",\n  \"Information: To enable service mode, make sure the Clash Nyanpasu service is installed and started\": \"提示信息：如需启用服务模式，请确保 Clash Nyanpasu 服务已安装并启动。\",\n  \"Prompt\": \"提示\",\n  \"install\": \"安装\",\n  \"uninstall\": \"卸载\",\n  \"start\": \"启动\",\n  \"stop\": \"停止\",\n  \"Failed to install\": \"安装失败\",\n  \"Failed to uninstall\": \"卸载失败\",\n  \"service_shortcuts\": {\n    \"title\": \"内核信息\",\n    \"core_status\": \"内核状态：\",\n    \"service_status\": \"服务状态：\",\n    \"core_started_by\": \"通过{{by}}启动\",\n    \"last_status_changed_since\": \"上次状态变更：{{time}}\"\n  },\n  \"service\": \"服务\",\n  \"UI\": \"用户界面\",\n  \"Service Manual Tips\": \"有关服务的提示\",\n  \"Unable to operation the service automatically\": \"无法自动{{operation}}服务。请导航到内核所在目录，在 Windows 上以管理员身份打开 PowerShell 或在 macOS/Linux 上打开终端仿真器，然后执行以下命令：\",\n  \"Successfully switched to the clash core\": \"成功切换至 {{core}} 内核。\",\n  \"Copy to clipboard\": \"复制到剪贴板\",\n  \"Copied to clipboard\": \"已复制到剪贴板~\",\n  \"Failed to copy to clipboard\": \"复制失败！\",\n  \"Failed to switch. You could see the details in the log\": \"切换失败，可以在日志中查看详细信息。\\n错误：{{error}}\",\n  \"Successfully restarted the core\": \"重启内核成功。\",\n  \"Failed to restart. You could see the details in the log\": \"重启失败，详细信息请检查日志。\\n\\n错误：\",\n  \"Failed to fetch. Please check your network connection\": \"获取更新失败，请检查你的互联网连接。\",\n  \"Successfully updated the core\": \"更新 {{core}} 内核成功。\",\n  \"Failed to update\": \"更新失败。{{error}}\",\n  \"Multiple directories are not supported\": \"不支持多个目录。\",\n  \"Successfully changed the app directory\": \"应用程序目录更改成功。\",\n  \"Failed to migrate\": \"迁移失败。{{error}}\",\n  \"Web UI\": \"Web UI\",\n  \"New Item\": \"添加新项目\",\n  \"Edit Item\": \"编辑项目\",\n  \"Input\": \"输入\",\n  \"Support %host %port, and %secret\": \"支持使用 %host、%port 和 %secret\",\n  \"Replace host, port, and secret with\": \"使用以下字段表示主机、端口和访问密钥：\",\n  \"Result\": \"结果\"\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/locales/zh-TW.json",
    "content": "{\n  \"seconds\": \"秒\",\n  \"minutes\": \"分鐘\",\n  \"Ok\": \"確定\",\n  \"Close\": \"關閉\",\n  \"label_dashboard\": \"儀表盤\",\n  \"label_proxies\": \"代理組\",\n  \"label_profiles\": \"代理設定檔\",\n  \"label_connections\": \"連線\",\n  \"label_logs\": \"日誌\",\n  \"label_rules\": \"規則\",\n  \"label_settings\": \"設定\",\n  \"label_providers\": \"資源\",\n  \"Connections\": \"連線\",\n  \"Upload Traffic\": \"上傳流量\",\n  \"Download Traffic\": \"下載流量\",\n  \"Total\": \"總量\",\n  \"Memory\": \"記憶體占用\",\n  \"Active Connections\": \"活動連線\",\n  \"Timeout\": \"逾時\",\n  \"Click to Refresh Now\": \"點擊立即重新整理\",\n  \"No Proxies\": \"無代理\",\n  \"Direct Mode\": \"直連模式\",\n  \"Rules\": \"規則\",\n  \"No Rules\": \"無規則\",\n  \"Logs\": \"日誌\",\n  \"No Logs\": \"無日誌\",\n  \"Clear\": \"清除\",\n  \"Proxies\": \"代理\",\n  \"Proxy Groups\": \"代理組\",\n  \"rule\": \"規則\",\n  \"global\": \"全域\",\n  \"direct\": \"直連\",\n  \"script\": \"腳本\",\n  \"Dashboard\": \"儀表盤\",\n  \"Profiles\": \"代理設定檔\",\n  \"Profile URL\": \"設定檔連結\",\n  \"Download\": \"下載\",\n  \"Paste\": \"貼上\",\n  \"Import\": \"匯入\",\n  \"New\": \"建立\",\n  \"Edit Profile\": \"編輯設定檔\",\n  \"Create Profile\": \"建立設定檔\",\n  \"New Profile\": \"新設定檔\",\n  \"Choose File\": \"選取檔案\",\n  \"Profile\": \"代理設定檔\",\n  \"Close All\": \"關閉全部\",\n  \"Menu\": \"選單\",\n  \"Select\": \"使用\",\n  \"Applying Profile\": \"正在套用設定檔……\",\n  \"Edit Info\": \"編輯資訊\",\n  \"Proxy Chains\": \"局部鏈\",\n  \"Global Proxy Chains\": \"全域鏈\",\n  \"New Chain\": \"添加新鏈\",\n  \"New Script\": \"新腳本\",\n  \"Edit Script\": \"編輯腳本\",\n  \"Please fix the error before submitting\": \"請在提交前修正錯誤。\",\n  \"Open\": \"開啟\",\n  \"Open File\": \"開啟檔案\",\n  \"Update\": \"更新\",\n  \"Update(Proxy)\": \"更新（使用代理）\",\n  \"Delete\": \"刪除\",\n  \"Enable\": \"啟用\",\n  \"Disable\": \"停用\",\n  \"Refresh\": \"重新整理\",\n  \"To Top\": \"移到最前\",\n  \"To End\": \"移到末尾\",\n  \"Update All Profiles\": \"更新所有代理設定檔\",\n  \"View Runtime Config\": \"查看實際執行設定\",\n  \"Reactivate Profiles\": \"重新啟用設定檔\",\n  \"Locate\": \"目前使用節點\",\n  \"Latency check\": \"延遲測試\",\n  \"Sort by default\": \"預設排序\",\n  \"Sort by latency\": \"按延遲排序\",\n  \"Sort by name\": \"按名稱排序\",\n  \"Latency check URL\": \"延遲測試連結\",\n  \"Proxy detail\": \"展示節點詳情\",\n  \"Filter\": \"過濾節點\",\n  \"Filter conditions\": \"過濾條件\",\n  \"Refresh profiles\": \"重新整理設定檔\",\n  \"Connection Columns\": \"連接表格列\",\n  \"Actions\": \"操作\",\n  \"Host\": \"主機\",\n  \"Process\": \"處理程式\",\n  \"Downloaded\": \"下載量\",\n  \"Uploaded\": \"上傳量\",\n  \"DL Speed\": \"下載速度\",\n  \"UL Speed\": \"上傳速度\",\n  \"Chains\": \"鏈路\",\n  \"Rule\": \"規則\",\n  \"Time\": \"連線時間\",\n  \"Source\": \"源位址\",\n  \"Destination IP\": \"目標位址\",\n  \"Destination ASN\": \"目標 ASN\",\n  \"Type\": \"類型\",\n  \"Connection Detail\": \"連線詳情\",\n  \"Metadata\": \"原始資訊\",\n  \"Remote\": \"遠端\",\n  \"Local\": \"本機\",\n  \"Remote Profile\": \"遠端設定\",\n  \"Local Profile\": \"本機設定\",\n  \"Name\": \"名稱\",\n  \"Descriptions\": \"描述\",\n  \"Subscription URL\": \"更新連結\",\n  \"User Agent\": \"使用者代理 (UA)\",\n  \"Update Interval\": \"更新間隔\",\n  \"Use System Proxy\": \"使用系統代理更新\",\n  \"Use Clash Proxy\": \"使用 Clash 代理更新\",\n  \"No Connections\": \"無連線\",\n  \"Tray Icons\": \"系統匣圖示\",\n  \"Set\": \"設定\",\n  \"Edit\": \"編輯\",\n  \"Reset\": \"重設\",\n  \"Hotkeys\": \"快速鍵\",\n  \"Feedback\": \"回報問題\",\n  \"Settings\": \"設定\",\n  \"Clash Setting\": \"Clash 設定\",\n  \"System Setting\": \"系統設定\",\n  \"Nyanpasu Setting\": \"Nyanpasu 設定\",\n  \"Allow LAN\": \"區域網路連線\",\n  \"IPv6\": \"IPv6\",\n  \"TUN Stack\": \"TUN 堆疊\",\n  \"Log Level\": \"日誌等級\",\n  \"Clash Port\": \"Clash 埠\",\n  \"Mixed Port\": \"埠設定\",\n  \"Random Port\": \"隨機埠\",\n  \"After restart to take effect\": \"重啟後生效。\",\n  \"Clash External Controll\": \"Clash 外部控制\",\n  \"External Controller\": \"外部控制器監聽位址\",\n  \"Port Strategy\": \"埠策略\",\n  \"Allow Fallback\": \"允許 fallback\",\n  \"Fixed\": \"固定\",\n  \"Random\": \"隨機\",\n  \"Core Secret\": \"API 訪問金鑰\",\n  \"Clash Core\": \"Clash 核心\",\n  \"TUN Mode\": \"TUN 模式\",\n  \"System Service\": \"系統服務\",\n  \"Service Mode\": \"服務模式\",\n  \"Initiating Behavior\": \"啟動行為\",\n  \"Auto Start\": \"開機啟動\",\n  \"Silent Start\": \"靜默啟動\",\n  \"System Proxy\": \"系統代理\",\n  \"Open UWP Tool\": \"UWP 工具\",\n  \"System Proxy Setting\": \"系統代理設定\",\n  \"Proxy Guard\": \"系統代理守衛\",\n  \"Guard Interval\": \"代理守衛間隔\",\n  \"The interval must be greater than 0 second\": \"間隔時間必須大於 0 秒。\",\n  \"Proxy Bypass\": \"代理繞過\",\n  \"Toggle\": \"切換\",\n  \"Current System Proxy\": \"目前系統代理\",\n  \"User Interface\": \"使用者介面\",\n  \"Theme Mode\": \"主題模式\",\n  \"Theme Blur\": \"背景模糊\",\n  \"Theme Setting\": \"主題設定\",\n  \"Icon Navigation Bar\": \"圖示導航欄\",\n  \"Network Statistic Widget\": \"網路懸浮窗\",\n  \"Network Statistic Widget Variant\": \"網路懸浮窗樣式\",\n  \"Layout Setting\": \"介面設定\",\n  \"Miscellaneous\": \"雜項設定\",\n  \"Hotkey Setting\": \"快速鍵設定\",\n  \"Traffic Graph\": \"流量圖顯\",\n  \"Memory Usage\": \"記憶體使用\",\n  \"Page Transition Animation\": \"頁面切換動畫\",\n  \"Page Transition Animation Slide\": \"滑動\",\n  \"Page Transition Animation Blur\": \"模糊\",\n  \"Page Transition Animation Transparent\": \"透明\",\n  \"Page Transition Animation None\": \"無\",\n  \"Language\": \"語言設定\",\n  \"Path Config\": \"目錄配置\",\n  \"Migrate Config Dir\": \"遷移配置目錄\",\n  \"Open Config Dir\": \"配置目錄\",\n  \"Open Data Dir\": \"資料目錄\",\n  \"Open Core Dir\": \"核心目錄\",\n  \"Open Log Dir\": \"日誌目錄\",\n  \"Auto Check Updates\": \"自動檢查更新\",\n  \"Check for Updates\": \"檢查更新\",\n  \"Nyanpasu Version\": \"Nyanpasu 版本\",\n  \"theme.light\": \"淺色\",\n  \"theme.dark\": \"深色\",\n  \"theme.system\": \"跟隨系統\",\n  \"Clash Field\": \"Clash 欄位\",\n  \"Original Config\": \"原始配置\",\n  \"Runtime Config\": \"執行配置\",\n  \"Console\": \"控制台\",\n  \"ReadOnly\": \"唯讀\",\n  \"Check Updates\": \"檢查更新\",\n  \"Restart\": \"重啟核心\",\n  \"Update Core\": \"更新核心\",\n  \"Tasks\": \"定期工作\",\n  \"Auto Log Clean\": \"自動清除日誌\",\n  \"Never Clean\": \"不清除\",\n  \"Retain 3 Days\": \"保留 3 天\",\n  \"Retain 7 Days\": \"保留 7 天\",\n  \"Retain 30 Days\": \"保留 30 天\",\n  \"Retain 90 Days\": \"保留 90 天\",\n  \"Max Log Files\": \"最大日誌檔案數\",\n  \"Collect Logs\": \"收集日誌\",\n  \"Back\": \"返回\",\n  \"Save\": \"儲存\",\n  \"Cancel\": \"取消\",\n  \"Default\": \"預設\",\n  \"Download Speed\": \"下載速度\",\n  \"Upload Speed\": \"上傳速度\",\n  \"open_or_close_dashboard\": \"打開/關閉儀表盤\",\n  \"clash_mode_rule\": \"規則模式\",\n  \"clash_mode_global\": \"全域模式\",\n  \"clash_mode_direct\": \"直連模式\",\n  \"clash_mode_script\": \"腳本模式\",\n  \"toggle_system_proxy\": \"切換系統代理\",\n  \"enable_system_proxy\": \"開啟系統代理\",\n  \"disable_system_proxy\": \"關閉系統代理\",\n  \"toggle_tun_mode\": \"切換 TUN 模式\",\n  \"enable_tun_mode\": \"開啟 TUN 模式\",\n  \"disable_tun_mode\": \"關閉 TUN 模式\",\n  \"App Log Level\": \"App 日誌等級\",\n  \"Auto Close Connections\": \"自動結束連線\",\n  \"Enable Clash Fields Filter\": \"開啟 Clash 欄位過濾\",\n  \"Enable Built-in Enhanced\": \"開啟內建增強功能\",\n  \"Proxy Layout Column\": \"代理頁布局列數\",\n  \"Default Latency Test\": \"預設測試連結\",\n  \"Error\": \"錯誤\",\n  \"Successful\": \"成功\",\n  \"Occupied\": \"被占用\",\n  \"Disabled\": \"已停用\",\n  \"Providers\": \"資源\",\n  \"Proxies Providers\": \"代理集\",\n  \"Rules Providers\": \"規則集\",\n  \"Update All Rules Providers\": \"全部更新\",\n  \"Rule Set rules\": \"{{rule}} 條規則\",\n  \"Last Update\": \"{{fromNow}}更新\",\n  \"Successfully Updated Rules Providers\": \"更新規則集成功\",\n  \"Portable Update Error\": \"便攜式軟體無法自動更新，請到 GitHub 下載最新版本\",\n  \"Tray Proxies Selector\": \"從系統匣選取代理\",\n  \"Hidden\": \"隱藏\",\n  \"Normal\": \"開啟\",\n  \"Submenu\": \"子選單\",\n  \"Proxy Set proxies\": \"{{rule}} 個節點\",\n  \"Update All Proxies Providers\": \"全部更新\",\n  \"Lighten Up Animation Effects\": \"減輕動畫效果\",\n  \"Subscription\": \"訂閱\",\n  \"FetchError\": \"由於網路問題，無法獲取{{content}}內容。請檢查網路連線或稍後再試。\",\n  \"tun\": \"TUN 模式\",\n  \"normal\": \"預設\",\n  \"system_proxy\": \"系統代理\",\n  \"Proxy Takeover Status\": \"代理接管狀態\",\n  \"Subscription Expires In\": \"{{time}}過期\",\n  \"Subscription Updated At\": \"{{time}}更新\",\n  \"Choose file to import or leave it blank to create new one\": \"選取檔案匯入，或留空以建立新檔。\",\n  \"updater\": {\n    \"title\": \"發現新版本\",\n    \"close\": \"忽略\",\n    \"update\": \"立即更新\",\n    \"go\": \"查看發布頁\"\n  },\n  \"not_installed\": \"未安裝\",\n  \"stopped\": \"已停止\",\n  \"running\": \"執行中\",\n  \"stopped_reason\": \"已停止，原因：{{reason}}\",\n  \"Current Status\": \"目前狀態：{{status}}\",\n  \"Information: To enable service mode, make sure the Clash Nyanpasu service is installed and started\": \"提示資訊：如需啟用服務模式，請確保 Clash Nyanpasu 服務已安裝並啟動。\",\n  \"Prompt\": \"提示\",\n  \"install\": \"安裝\",\n  \"uninstall\": \"移除\",\n  \"start\": \"啟動\",\n  \"stop\": \"停止\",\n  \"Failed to install\": \"安裝失敗\",\n  \"Failed to uninstall\": \"移除失敗\",\n  \"service_shortcuts\": {\n    \"title\": \"核心資訊\",\n    \"core_status\": \"核心狀態：\",\n    \"service_status\": \"服務狀態：\",\n    \"core_started_by\": \"透過{{by}}啟動\",\n    \"last_status_changed_since\": \"上次狀態變更：{{time}}\"\n  },\n  \"service\": \"服務\",\n  \"UI\": \"使用者介面\",\n  \"Service Manual Tips\": \"有關服務的提示\",\n  \"Unable to operation the service automatically\": \"無法自動{{operation}}服務。請開啟核心所在目錄，在 Windows 上以管理員身分開啟 PowerShell 或在 macOS/Linux 上開啟終端模擬器，然後執行以下指令：\",\n  \"Copy to clipboard\": \"複製到剪貼簿\",\n  \"Copied to clipboard\": \"已複製到剪貼簿~\",\n  \"Failed to copy to clipboard\": \"複製失敗！\",\n  \"Successfully switched to the clash core\": \"成功切換至「{{core}}」核心。\",\n  \"Failed to switch. You could see the details in the log\": \"切換失敗，可以在日誌中查看詳細資訊。\\n錯誤：{{error}}\",\n  \"Successfully restarted the core\": \"成功重啟核心。\",\n  \"Failed to restart. You could see the details in the log\": \"重啟失敗，詳細資訊請檢查日誌。\\n\\n錯誤：\",\n  \"Failed to fetch. Please check your network connection\": \"獲取更新失敗，請檢查你的網路連線。\",\n  \"Successfully updated the core\": \"成功更新「{{core}}」核心。\",\n  \"Failed to update\": \"更新失敗。{{error}}\",\n  \"Multiple directories are not supported\": \"不支援多個目錄。\",\n  \"Successfully changed the app directory\": \"App 目錄更改成功。\",\n  \"Failed to migrate\": \"遷移失敗。{{error}}\",\n  \"Web UI\": \"Web UI\",\n  \"New Item\": \"添加新項目\",\n  \"Edit Item\": \"編輯項目\",\n  \"Input\": \"輸入\",\n  \"Support %host %port, and %secret\": \"可以使用 %host、%port 和 %secret\",\n  \"Replace host, port, and secret with\": \"使用以下欄位表示主機、埠和訪問金鑰：\",\n  \"Result\": \"結果\"\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/main.tsx",
    "content": "/// <reference types=\"vite/client\" />\n/// <reference types=\"vite-plugin-svgr/client\" />\nimport React from 'react'\nimport { createRoot } from 'react-dom/client'\nimport { ResizeObserver } from '@juggle/resize-observer'\n// Styles\nimport '@csstools/normalize.css/normalize.css'\nimport '@csstools/normalize.css/opinionated.css'\nimport { createRouter, RouterProvider } from '@tanstack/react-router'\nimport './assets/styles/index.scss'\nimport './assets/styles/tailwind.css'\nimport { routeTree } from './route-tree.gen'\nimport './services/i18n'\n// manually import language utils, inject paraglide custom strategy\nimport '@/utils/language'\n\nif (!window.ResizeObserver) {\n  window.ResizeObserver = ResizeObserver\n}\n\nwindow.addEventListener('error', (event) => {\n  console.error(event)\n})\n\n// prepare dark mode class on root element before React hydration to avoid FOUC\ndocument.documentElement.classList.toggle(\n  'dark',\n  window.matchMedia('(prefers-color-scheme: dark)').matches,\n)\n\n// Set up a Router instance\nconst router = createRouter({\n  routeTree,\n  defaultPreload: 'intent',\n})\n\n// Register things for typesafety\ndeclare module '@tanstack/react-router' {\n  interface Register {\n    router: typeof router\n  }\n}\n\nconst container = document.getElementById('root')!\n\ncreateRoot(container).render(\n  <React.StrictMode>\n    <RouterProvider router={router} />\n  </React.StrictMode>,\n)\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(editor)/editor/_modules/chip.tsx",
    "content": "import { ComponentProps } from 'react'\nimport { cn } from '@nyanpasu/ui'\n\nexport default function Chip({ className, ...props }: ComponentProps<'span'>) {\n  return (\n    <span\n      className={cn(\n        'bg-primary-container rounded-full px-3 py-0.5 text-sm font-bold',\n        className,\n      )}\n      {...props}\n    />\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(editor)/editor/_modules/header.tsx",
    "content": "import { ComponentProps } from 'react'\nimport WindowControl from '@/components/window/window-control'\nimport WindowHeader from '@/components/window/window-header'\nimport WindowTitle from '@/components/window/window-title'\n\nconst APP_NAME = 'Clash Nyanpasu - Editor'\n\nconst Title = () => {\n  return (\n    <WindowTitle>\n      <div\n        className=\"text-on-surface text-base font-bold text-nowrap\"\n        data-slot=\"app-header-logo-name\"\n        data-tauri-drag-region\n      >\n        {APP_NAME}\n      </div>\n    </WindowTitle>\n  )\n}\n\nexport default function Header({\n  beforeClose,\n}: {\n  beforeClose?: ComponentProps<typeof WindowControl>['beforeClose']\n}) {\n  return (\n    <WindowHeader\n      className=\"shrink-0 items-center justify-between px-3\"\n      data-slot=\"window-control\"\n    >\n      <div className=\"flex items-center gap-2\" data-tauri-drag-region>\n        <Title />\n      </div>\n\n      <WindowControl hiddenAlwaysOnTop beforeClose={beforeClose} />\n    </WindowHeader>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(editor)/editor/_modules/hooks.tsx",
    "content": "import { nanoid } from 'nanoid'\nimport { useMemo } from 'react'\nimport { useProfile, type Profile } from '@nyanpasu/interface'\n\ntype CurrentProfileData = Profile & {\n  language: string\n  extension: string\n  readOnly: boolean\n  virtualPath: string\n}\n\nexport function useCurrentProfile(uid: string): {\n  data: CurrentProfileData | undefined\n} & Omit<ReturnType<typeof useProfile>['query'], 'data'> {\n  const profiles = useProfile()\n\n  const currentProfile = useMemo(() => {\n    const item = profiles.query.data?.items?.find((item) => item.uid === uid)\n\n    if (item) {\n      let language = 'yaml'\n      let extension = 'yaml'\n      let readOnly = false\n      let schemaType\n\n      if (item.type === 'remote') {\n        readOnly = true\n      }\n\n      if (item.type === 'remote' || item.type === 'local') {\n        schemaType = 'clash'\n      }\n\n      if (item.type === 'merge') {\n        schemaType = 'merge'\n      }\n\n      if (item.type === 'script') {\n        if (item.script_type === 'javascript') {\n          language = 'javascript'\n          extension = 'js'\n        }\n\n        if (item.script_type === 'lua') {\n          language = 'lua'\n          extension = 'lua'\n        }\n      }\n\n      return {\n        ...item,\n        language,\n        extension,\n        readOnly,\n        virtualPath: `${nanoid()}${schemaType ? `.${schemaType}` : ''}.${language}`,\n      }\n    }\n  }, [profiles.query.data, uid])\n\n  return {\n    ...profiles.query,\n    data: currentProfile,\n  }\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(editor)/editor/_modules/loading-skeleton.tsx",
    "content": "import { CircularProgress } from '@/components/ui/progress'\n\nexport default function LoadingSkeleton() {\n  return (\n    <div className=\"grid flex-1 place-items-center\">\n      <CircularProgress className=\"size-12\" indeterminate />\n    </div>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(editor)/editor/_modules/utils.tsx",
    "content": "import nyanpasuMergeSchema from 'meta-json-schema/schemas/clash-nyanpasu-merge-json-schema.json'\nimport clashMetaSchema from 'meta-json-schema/schemas/meta-json-schema.json'\nimport * as monaco from 'monaco-editor'\nimport { configureMonacoYaml } from 'monaco-yaml'\nimport { OS } from '@/consts'\n\nexport const MONACO_FONT_FAMILY =\n  '\"Cascadia Code NF\",' +\n  '\"Cascadia Code\",' +\n  'Fira Code,' +\n  'JetBrains Mono,' +\n  'Roboto Mono,' +\n  '\"Source Code Pro\",' +\n  'Consolas,' +\n  'Menlo,' +\n  'Monaco,' +\n  'monospace,' +\n  `${OS === 'windows' ? 'twemoji mozilla' : ''}`\n\nlet initd = false\n\nexport const beforeEditorMount = () => {\n  if (initd) {\n    return\n  }\n\n  monaco.typescript.javascriptDefaults.setCompilerOptions({\n    target: monaco.typescript.ScriptTarget.ES2020,\n    allowNonTsExtensions: true,\n    allowJs: true,\n  })\n\n  // console.log(clashMetaSchema)\n  // console.log(nyanpasuMergeSchema)\n\n  // configure yaml schema\n  configureMonacoYaml(monaco, {\n    validate: true,\n    enableSchemaRequest: true,\n    completion: true,\n    schemas: [\n      {\n        uri: 'http://example.com/schema-name.json',\n        fileMatch: ['**/*.clash.yaml'],\n        // @ts-expect-error JSONSchema7 as JSONSchema\n        schema: clashMetaSchema as JSONSchema7,\n      },\n      {\n        uri: 'http://example.com/schema-name.json',\n        fileMatch: ['**/*.merge.yaml'],\n        // @ts-expect-error JSONSchema7 as JSONSchema\n        schema: nyanpasuMergeSchema as JSONSchema7,\n      },\n    ],\n  })\n\n  // Register link provider for all supported languages\n  const registerLinkProvider = (language: string) => {\n    monaco.languages.registerLinkProvider(language, {\n      provideLinks: (model) => {\n        const links = []\n        // More robust URL regex pattern\n        const urlRegex = /\\b(?:https?:\\/\\/|www\\.)[^\\s<>\"']*[^<>\\s\"',.!?]/gi\n\n        for (let i = 1; i <= model.getLineCount(); i++) {\n          const line = model.getLineContent(i)\n          let match\n\n          while ((match = urlRegex.exec(line)) !== null) {\n            const url = match[0].startsWith('http')\n              ? match[0]\n              : `https://${match[0]}`\n            links.push({\n              range: new monaco.Range(\n                i,\n                match.index + 1,\n                i,\n                match.index + match[0].length + 1,\n              ),\n              url,\n            })\n          }\n        }\n\n        return {\n          links,\n          dispose: () => {},\n        }\n      },\n    })\n  }\n\n  // Register link provider for all languages we support\n  registerLinkProvider('javascript')\n  registerLinkProvider('lua')\n  registerLinkProvider('yaml')\n\n  initd = true\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(editor)/editor/index.tsx",
    "content": "import { isEqual } from 'lodash-es'\nimport { editor } from 'monaco-editor'\nimport { ComponentProps, useCallback, useEffect, useRef, useState } from 'react'\nimport { z } from 'zod'\nimport { useBlockTask } from '@/components/providers/block-task-provider'\nimport { useExperimentalThemeContext } from '@/components/providers/theme-provider'\nimport { Button } from '@/components/ui/button'\nimport { useLockFn } from '@/hooks/use-lock-fn'\nimport { m } from '@/paraglide/messages'\nimport MonacoEditor from '@monaco-editor/react'\nimport { openThat, useProfileContent } from '@nyanpasu/interface'\nimport { cn } from '@nyanpasu/ui'\nimport { createFileRoute } from '@tanstack/react-router'\nimport { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'\nimport { ask, message } from '@tauri-apps/plugin-dialog'\nimport Chip from './_modules/chip'\nimport Header from './_modules/header'\nimport { useCurrentProfile } from './_modules/hooks'\nimport LoadingSkeleton from './_modules/loading-skeleton'\nimport { beforeEditorMount, MONACO_FONT_FAMILY } from './_modules/utils'\n\nconst currentWindow = getCurrentWebviewWindow()\n\nexport const Route = createFileRoute('/(editor)/editor/')({\n  component: RouteComponent,\n  validateSearch: z.object({\n    uid: z.string(),\n  }),\n})\n\nconst ActionButton = ({\n  className,\n  ...props\n}: ComponentProps<typeof Button>) => {\n  return <Button className={cn('h-8 min-w-0 px-3', className)} {...props} />\n}\n\nfunction RouteComponent() {\n  const { themeMode } = useExperimentalThemeContext()\n\n  const { uid } = Route.useSearch()\n\n  const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null)\n\n  const editorMarks = useRef<editor.IMarker[]>([])\n\n  const currentProfile = useCurrentProfile(uid)\n\n  const content = useProfileContent(uid)\n\n  const [editorValue, setEditorValue] = useState<string>()\n\n  // sync editor value with content\n  useEffect(() => {\n    if (content.query.data) {\n      setEditorValue(content.query.data)\n    }\n  }, [content.query.data])\n\n  const blockTask = useBlockTask(`save-profile-content-${uid}`, async () => {\n    if (!editorValue) {\n      return\n    }\n\n    const editorHasError =\n      editorMarks.current.length > 0 &&\n      editorMarks.current.some((m) => m.severity === 8)\n\n    if (editorHasError) {\n      message(m.editor_validate_error_message(), {\n        kind: 'error',\n      })\n\n      return false\n    }\n\n    await content.upsert.mutateAsync(editorValue)\n\n    await currentWindow.close()\n  })\n\n  const handleSave = useLockFn(blockTask.execute)\n\n  const handleBeforeClose = useLockFn(async () => {\n    const isDirty = !isEqual(editorValue, content.query.data)\n\n    if (isDirty) {\n      const result = await ask(m.editor_before_close_message(), {\n        kind: 'warning',\n      })\n\n      if (!result) {\n        return false\n      }\n    }\n\n    return true\n  })\n\n  const handleCancel = useLockFn(async () => {\n    const result = await handleBeforeClose()\n\n    if (!result) {\n      return\n    }\n\n    await currentWindow.close()\n  })\n\n  const handleReset = useLockFn(async () => {\n    setEditorValue(content.query.data)\n  })\n\n  const handleEditorDidMount = useCallback(\n    (editor: editor.IStandaloneCodeEditor) => {\n      editorRef.current = editor\n\n      // Enable URL detection and handling\n      editor.onMouseDown((e) => {\n        const position = e.target.position\n        if (!position) return\n\n        // Get the model\n        const model = editor.getModel()\n        if (!model) return\n\n        // Get the word at the clicked position\n        const wordAtPosition = model.getWordAtPosition(position)\n        if (!wordAtPosition) return\n\n        // More comprehensive URL regex pattern\n        const urlRegex = /\\b(?:https?:\\/\\/|www\\.)[^\\s<>\"']*[^<>\\s\"',.!?]/gi\n\n        // Check if the clicked word is part of a URL\n        const lineContent = model.getLineContent(position.lineNumber)\n        let match\n\n        while ((match = urlRegex.exec(lineContent)) !== null) {\n          const urlStart = match.index + 1\n          const urlEnd = urlStart + match[0].length\n\n          // Check if the click position is within the URL\n          if (position.column >= urlStart && position.column <= urlEnd) {\n            // Only handle Ctrl+Click or Cmd+Click\n            if (e.event.ctrlKey || e.event.metaKey) {\n              // Add protocol if missing (for www. URLs)\n              const url = match[0].startsWith('http')\n                ? match[0]\n                : `https://${match[0]}`\n              openThat(url)\n              e.event.preventDefault()\n              break\n            }\n          }\n        }\n      })\n    },\n    [],\n  )\n\n  return (\n    <>\n      <Header beforeClose={handleBeforeClose} />\n\n      {content.query.isLoading || currentProfile.isLoading ? (\n        <LoadingSkeleton />\n      ) : (\n        <>\n          <div\n            className={cn(\n              'dark:bg-on-primary bg-primary-container flex shrink-0 items-center gap-2 px-3',\n              'h-12',\n            )}\n            data-slot=\"editor-header-actions\"\n          >\n            <div\n              className=\"text-sm font-medium\"\n              data-slot=\"editor-header-title\"\n            >\n              {currentProfile.data?.name}.{currentProfile.data?.extension}\n            </div>\n\n            {currentProfile.data?.readOnly && (\n              <Chip>{m.editor_read_only_chip()}</Chip>\n            )}\n          </div>\n\n          <div className=\"min-h-0 flex-1\" data-slot=\"editor-content\">\n            <MonacoEditor\n              className=\"h-full w-full\"\n              value={content.query.data}\n              language={currentProfile.data?.language}\n              path={currentProfile.data?.virtualPath}\n              theme={themeMode === 'light' ? 'vs' : 'vs-dark'}\n              beforeMount={beforeEditorMount}\n              onMount={handleEditorDidMount}\n              onChange={setEditorValue}\n              onValidate={(marks) => {\n                editorMarks.current = marks\n              }}\n              loading={<LoadingSkeleton />}\n              options={{\n                readOnly: currentProfile.data?.readOnly,\n                mouseWheelZoom: true,\n                renderValidationDecorations: 'on',\n                tabSize: currentProfile.data?.language === 'yaml' ? 2 : 4,\n                minimap: { enabled: false },\n                automaticLayout: true,\n                fontLigatures: true,\n                smoothScrolling: true,\n                fontFamily: MONACO_FONT_FAMILY,\n                quickSuggestions: {\n                  strings: true,\n                  comments: true,\n                  other: true,\n                },\n              }}\n            />\n          </div>\n\n          <div\n            className=\"bg-background flex h-12 shrink-0 items-center gap-2 px-3\"\n            data-slot=\"editor-footer-actions\"\n          >\n            <ActionButton onClick={handleReset}>\n              {m.common_reset()}\n            </ActionButton>\n\n            <div className=\"flex-1\" />\n\n            <ActionButton onClick={handleCancel}>\n              {m.common_cancel()}\n            </ActionButton>\n\n            <ActionButton\n              className=\"px-5\"\n              variant=\"flat\"\n              loading={blockTask.isPending}\n              onClick={handleSave}\n            >\n              {m.common_save()}\n            </ActionButton>\n          </div>\n        </>\n      )}\n    </>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(editor)/editor/route.tsx",
    "content": "import { cn } from '@nyanpasu/ui'\nimport { createFileRoute, Outlet } from '@tanstack/react-router'\nimport '@/services/monaco'\n\nexport const Route = createFileRoute('/(editor)/editor')({\n  component: RouteComponent,\n})\n\nfunction RouteComponent() {\n  return (\n    <div\n      className={cn('flex h-dvh flex-col', 'bg-background/30')}\n      data-slot=\"editor-container\"\n      onContextMenu={(e) => {\n        e.preventDefault()\n      }}\n    >\n      <Outlet />\n    </div>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(legacy)/connections.tsx",
    "content": "import { useThrottle } from 'ahooks'\nimport { lazy, Suspense, useDeferredValue, useEffect, useState } from 'react'\nimport { useTranslation } from 'react-i18next'\nimport { SearchTermCtx } from '@/components/connections/connection-search-term'\nimport HeaderSearch from '@/components/connections/header-search'\nimport { FilterAlt } from '@mui/icons-material'\nimport { Box, CircularProgress, IconButton } from '@mui/material'\nimport { BasePage } from '@nyanpasu/ui'\nimport { createFileRoute, useBlocker } from '@tanstack/react-router'\n\nconst Component = lazy(() => import('@/components/connections/connection-page'))\n\nconst ColumnFilterDialog = lazy(\n  () => import('@/components/connections/connections-column-filter'),\n)\n\nconst ConnectionTotal = lazy(\n  () => import('@/components/connections/connections-total'),\n)\n\nexport const Route = createFileRoute('/(legacy)/connections')({\n  component: Connections,\n})\n\nfunction Connections() {\n  const { t } = useTranslation()\n\n  const [openColumnFilter, setOpenColumnFilter] = useState(false)\n\n  const [searchTerm, setSearchTerm] = useState<string>()\n  const throttledSearchTerm = useThrottle(searchTerm, { wait: 150 })\n\n  const [mountTable, setMountTable] = useState(true)\n  const deferredMountTable = useDeferredValue(mountTable)\n  const { proceed } = useBlocker({\n    shouldBlockFn: () => {\n      setMountTable(false)\n      return !mountTable\n    },\n    withResolver: true,\n  })\n\n  useEffect(() => {\n    if (!deferredMountTable) {\n      proceed?.()\n    }\n  }, [proceed, deferredMountTable])\n\n  // Loading fallback component\n  const LoadingFallback = () => (\n    <Box\n      sx={{\n        display: 'flex',\n        justifyContent: 'center',\n        alignItems: 'center',\n        height: '100%',\n        minHeight: 200,\n      }}\n    >\n      <CircularProgress />\n    </Box>\n  )\n\n  return (\n    <SearchTermCtx.Provider value={throttledSearchTerm}>\n      <BasePage\n        title={t('Connections')}\n        full\n        header={\n          <div className=\"flex max-h-96 w-full flex-1 items-center justify-between gap-2 pl-5\">\n            <Suspense fallback={null}>\n              <ConnectionTotal />\n            </Suspense>\n            <div className=\"flex items-center gap-1\">\n              <Suspense fallback={null}>\n                <ColumnFilterDialog\n                  open={openColumnFilter}\n                  onClose={() => setOpenColumnFilter(false)}\n                />\n              </Suspense>\n              <HeaderSearch\n                value={searchTerm}\n                onChange={(e) => setSearchTerm(e.target.value)}\n              />\n              <IconButton onClick={() => setOpenColumnFilter(true)}>\n                <FilterAlt />\n              </IconButton>\n            </div>\n          </div>\n        }\n      >\n        <Suspense fallback={<LoadingFallback />}>\n          {mountTable && <Component />}\n        </Suspense>\n      </BasePage>\n    </SearchTermCtx.Provider>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(legacy)/dashboard.tsx",
    "content": "import { useTranslation } from 'react-i18next'\nimport DataPanel from '@/components/dashboard/data-panel'\nimport HealthPanel from '@/components/dashboard/health-panel'\nimport ProxyShortcuts from '@/components/dashboard/proxy-shortcuts'\nimport ServiceShortcuts from '@/components/dashboard/service-shortcuts'\nimport { useVisibility } from '@/hooks/use-visibility'\nimport Grid from '@mui/material/Grid'\nimport { useClashWSContext } from '@nyanpasu/interface'\nimport { BasePage } from '@nyanpasu/ui'\nimport { createFileRoute } from '@tanstack/react-router'\n\nexport const Route = createFileRoute('/(legacy)/dashboard')({\n  component: Dashboard,\n})\n\nfunction Dashboard() {\n  const { t } = useTranslation()\n  const visible = useVisibility()\n  const { setRecordTraffic } = useClashWSContext()\n\n  // When the page is not visible, reduce the traffic data update frequency\n  // to prevent performance issues when the page is restored\n  setRecordTraffic(visible)\n\n  return (\n    <BasePage title={t('Dashboard')}>\n      <Grid container spacing={2}>\n        <DataPanel visible={visible} />\n\n        <HealthPanel />\n\n        <ProxyShortcuts />\n\n        <ServiceShortcuts />\n      </Grid>\n    </BasePage>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(legacy)/index.tsx",
    "content": "import { createFileRoute, Navigate } from '@tanstack/react-router'\n\nexport const Route = createFileRoute('/(legacy)/')({\n  component: RouteComponent,\n})\n\nfunction RouteComponent() {\n  // const memorizedNavigate = useAtomValue(memorizedRoutePathAtom)\n\n  return <Navigate to=\"/dashboard\" />\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(legacy)/logs.tsx",
    "content": "import { lazy, RefObject, useRef } from 'react'\nimport { useTranslation } from 'react-i18next'\nimport { LogProvider } from '@/components/logs/log-provider'\nimport LogHeader from '@/components/logs/los-header'\nimport { BasePage } from '@nyanpasu/ui'\nimport { createFileRoute } from '@tanstack/react-router'\n\nexport const Route = createFileRoute('/(legacy)/logs')({\n  component: LogPage,\n})\n\nfunction LogPage() {\n  const { t } = useTranslation()\n\n  const viewportRef = useRef<HTMLDivElement>(null)\n\n  const Component = lazy(() => import('@/components/logs/log-page'))\n\n  return (\n    <LogProvider>\n      <BasePage\n        full\n        title={t('Logs')}\n        header={<LogHeader />}\n        viewportRef={viewportRef}\n      >\n        <Component scrollRef={viewportRef as RefObject<HTMLElement>} />\n      </BasePage>\n    </LogProvider>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(legacy)/profiles.tsx",
    "content": "import MdiTextBoxCheckOutline from '~icons/mdi/text-box-check-outline'\nimport { useLockFn } from 'ahooks'\nimport { AnimatePresence, motion } from 'framer-motion'\nimport { useAtom } from 'jotai'\nimport { useMemo, useState, useTransition } from 'react'\nimport { useTranslation } from 'react-i18next'\nimport { useWindowSize } from 'react-use'\nimport { z } from 'zod'\nimport {\n  atomChainsSelected,\n  atomGlobalChainCurrent,\n} from '@/components/profiles/modules/store'\nimport NewProfileButton from '@/components/profiles/new-profile-button'\nimport {\n  AddProfileContext,\n  AddProfileContextValue,\n} from '@/components/profiles/profile-dialog'\nimport ProfileItem from '@/components/profiles/profile-item'\nimport ProfileSide from '@/components/profiles/profile-side'\nimport { GlobalUpdatePendingContext } from '@/components/profiles/provider'\nimport { QuickImport } from '@/components/profiles/quick-import'\nimport RuntimeConfigDiffDialog from '@/components/profiles/runtime-config-diff-dialog'\nimport { ClashProfile, filterProfiles } from '@/components/profiles/utils'\nimport { formatError } from '@/utils'\nimport { message } from '@/utils/notification'\nimport { Public, Update } from '@mui/icons-material'\nimport { Badge, Button, CircularProgress, IconButton } from '@mui/material'\nimport Grid from '@mui/material/Grid'\nimport type { SxProps, Theme } from '@mui/material/styles'\nimport {\n  RemoteProfileOptionsBuilder,\n  useProfile,\n  type RemoteProfile,\n} from '@nyanpasu/interface'\nimport { FloatingButton, SidePage } from '@nyanpasu/ui'\nimport { createFileRoute, useLocation } from '@tanstack/react-router'\nimport { zodSearchValidator } from '@tanstack/router-zod-adapter'\n\nconst profileSearchParams = z.object({\n  subscribeName: z.string().optional(),\n  subscribeUrl: z.url().optional(),\n  subscribeDesc: z.string().optional(),\n})\n\nexport const Route = createFileRoute('/(legacy)/profiles')({\n  validateSearch: zodSearchValidator(profileSearchParams),\n  component: ProfilePage,\n})\n\nfunction ProfilePage() {\n  const { t } = useTranslation()\n\n  const { query, update } = useProfile()\n\n  const profiles = useMemo(() => {\n    return filterProfiles(query.data?.items)\n  }, [query.data?.items])\n\n  // TODO: fix me\n  // const maxLogLevelTriggered = useMemo(() => {\n  //   const currentProfileChains =\n  //     (\n  //       query.data?.items?.find(\n  //         // TODO: 支持多 Profile\n  //         (item) => query.data?.current?.[0] === item.uid,\n  //         // TODO: fix any type\n  //       ) as any\n  //     )?.chain || []\n  //   return Object.entries(getRuntimeLogs.data || {}).reduce(\n  //     (acc, [key, value]) => {\n  //       const accKey = currentProfileChains.includes(key) ? 'current' : 'global'\n  //       if (acc[accKey] === 'error') {\n  //         return acc\n  //       }\n  //       for (const log of value) {\n  //         switch (log[0]) {\n  //           case 'error':\n  //             return { ...acc, [accKey]: 'error' }\n  //           case 'warn':\n  //             acc = { ...acc, [accKey]: 'warn' }\n  //             break\n  //           case 'info':\n  //             if (acc[accKey] !== 'warn') {\n  //               acc = { ...acc, [accKey]: 'info' }\n  //             }\n  //             break\n  //         }\n  //       }\n  //       return acc\n  //     },\n  //     {} as {\n  //       global: undefined | 'info' | 'error' | 'warn'\n  //       current: undefined | 'info' | 'error' | 'warn'\n  //     },\n  //   )\n  // }, [query.data, getRuntimeLogs.data])\n\n  const [globalChain, setGlobalChain] = useAtom(atomGlobalChainCurrent)\n\n  const [chainsSelected, setChainsSelected] = useAtom(atomChainsSelected)\n\n  const handleGlobalChainClick = () => {\n    setChainsSelected(undefined)\n    setGlobalChain(!globalChain)\n  }\n\n  const onClickChains = (profile: ClashProfile) => {\n    setGlobalChain(false)\n\n    if (chainsSelected === profile.uid) {\n      setChainsSelected(undefined)\n    } else {\n      setChainsSelected(profile.uid)\n    }\n  }\n\n  const handleSideClose = () => {\n    setChainsSelected(undefined)\n    setGlobalChain(false)\n  }\n\n  const [runtimeConfigViewerOpen, setRuntimeConfigViewerOpen] = useState(false)\n  const location = useLocation()\n  const addProfileCtxValue = useMemo(() => {\n    if (!location.search || !location.search.subscribeUrl) {\n      return null\n    }\n    return {\n      name: location.search.subscribeName!,\n      desc: location.search.subscribeDesc!,\n      url: location.search.subscribeUrl,\n    } satisfies AddProfileContextValue\n  }, [location.search])\n\n  const hasSide = globalChain || chainsSelected\n\n  const { width } = useWindowSize()\n\n  const [globalUpdatePending, startGlobalUpdate] = useTransition()\n  const handleGlobalProfileUpdate = useLockFn(async () => {\n    startGlobalUpdate(async () => {\n      const remoteProfiles =\n        (profiles.clash?.filter(\n          (item) => item.type === 'remote',\n        ) as RemoteProfile[]) || []\n\n      const updates: Array<Promise<void>> = []\n\n      for (const profile of remoteProfiles) {\n        const option = {\n          with_proxy: false,\n          self_proxy: false,\n          update_interval: 0,\n          user_agent: profile.option?.user_agent ?? null,\n          ...profile.option,\n        } satisfies RemoteProfileOptionsBuilder\n\n        const result = await update.mutateAsync({\n          uid: profile.uid,\n          option,\n        })\n        updates.push(Promise.resolve(result || undefined))\n      }\n      try {\n        await Promise.all(updates)\n      } catch (e) {\n        message(`failed to update profiles: \\n${formatError(e)}`, {\n          kind: 'error',\n        })\n      }\n    })\n  })\n\n  return (\n    <SidePage\n      title={t('Profiles')}\n      flexReverse\n      header={\n        <div className=\"flex items-center gap-2\">\n          <RuntimeConfigDiffDialog\n            open={runtimeConfigViewerOpen}\n            onClose={() => setRuntimeConfigViewerOpen(false)}\n          />\n          <IconButton\n            className=\"h-10 w-10\"\n            color=\"inherit\"\n            title={t('Runtime Config')}\n            onClick={() => {\n              setRuntimeConfigViewerOpen(true)\n            }}\n          >\n            <MdiTextBoxCheckOutline\n            // style={{\n            //   color: theme.vars.palette.text.primary,\n            // }}\n            />\n          </IconButton>\n          <Badge\n            variant=\"dot\"\n            // color={\n            //   maxLogLevelTriggered.global === 'error'\n            //     ? 'error'\n            //     : maxLogLevelTriggered.global === 'warn'\n            //       ? 'warning'\n            //       : 'primary'\n            // }\n            // invisible={!maxLogLevelTriggered.global}\n          >\n            <Button\n              size=\"small\"\n              variant={globalChain ? 'contained' : 'outlined'}\n              onClick={handleGlobalChainClick}\n              startIcon={<Public />}\n            >\n              {t('Global Proxy Chains')}\n            </Button>\n          </Badge>\n        </div>\n      }\n      side={hasSide && <ProfileSide onClose={handleSideClose} />}\n    >\n      <AnimatePresence initial={false} mode=\"sync\">\n        <GlobalUpdatePendingContext.Provider value={globalUpdatePending}>\n          <div className=\"flex flex-col gap-4 p-6\">\n            <QuickImport />\n\n            {profiles && (\n              <Grid container spacing={2}>\n                {profiles.clash?.map((item) => (\n                  <Grid\n                    key={item.uid}\n                    size={{\n                      xs: 12,\n                      sm: 12,\n                      md: hasSide && width <= 1000 ? 12 : 6,\n                      lg: 4,\n                      xl: 3,\n                    }}\n                  >\n                    <motion.div\n                      key={item.uid}\n                      layoutId={`profile-${item.uid}`}\n                      layout=\"position\"\n                      initial={false}\n                    >\n                      <ProfileItem\n                        item={item}\n                        onClickChains={onClickChains}\n                        selected={query.data?.current?.includes(item.uid)}\n                        // maxLogLevelTriggered={maxLogLevelTriggered}\n                        chainsSelected={chainsSelected === item.uid}\n                      />\n                    </motion.div>\n                  </Grid>\n                ))}\n              </Grid>\n            )}\n          </div>\n        </GlobalUpdatePendingContext.Provider>\n      </AnimatePresence>\n\n      <AddProfileContext.Provider value={addProfileCtxValue}>\n        <div className=\"!fixed right-8 bottom-8\">\n          <FloatingButton\n            className=\"!relative -top-15 -right-13.5 flex size-11 !min-w-fit\"\n            sx={\n              ((theme) => ({\n                backgroundColor: theme.vars.palette.grey[200],\n                boxShadow: 4,\n                '&:hover': {\n                  backgroundColor: theme.vars.palette.grey[300],\n                },\n                ...theme.applyStyles('dark', {\n                  backgroundColor: theme.vars.palette.grey[800],\n                  '&:hover': {\n                    backgroundColor: theme.vars.palette.grey[700],\n                  },\n                }),\n              })) as SxProps<Theme>\n            }\n            onClick={handleGlobalProfileUpdate}\n          >\n            {globalUpdatePending ? <CircularProgress size={22} /> : <Update />}\n          </FloatingButton>\n          <NewProfileButton className=\"!static\" />\n        </div>\n      </AddProfileContext.Provider>\n    </SidePage>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(legacy)/providers.tsx",
    "content": "import { useTranslation } from 'react-i18next'\nimport ProxiesProvider from '@/components/providers/proxies-provider'\nimport RulesProvider from '@/components/providers/rules-provider'\nimport UpdateProviders from '@/components/providers/update-providers'\nimport UpdateProxiesProviders from '@/components/providers/update-proxies-providers'\nimport { Chip } from '@mui/material'\nimport Grid from '@mui/material/Grid'\nimport {\n  useClashProxiesProvider,\n  useClashRulesProvider,\n} from '@nyanpasu/interface'\nimport { BasePage } from '@nyanpasu/ui'\nimport { createFileRoute } from '@tanstack/react-router'\n\nexport const Route = createFileRoute('/(legacy)/providers')({\n  component: ProvidersPage,\n})\n\nfunction ProvidersPage() {\n  const { t } = useTranslation()\n\n  const proxiesProvider = useClashProxiesProvider()\n\n  const rulesProvider = useClashRulesProvider()\n\n  return (\n    <BasePage title={t('Providers')}>\n      <div className=\"flex flex-col gap-4\">\n        <div className=\"flex items-center justify-between\">\n          <Chip\n            className=\"!h-10 truncate !rounded-full !p-2 !text-lg font-bold\"\n            label={`${t(`Proxies Providers`)} (${Object.entries(proxiesProvider.data ?? {}).length})`}\n          />\n\n          <UpdateProxiesProviders />\n        </div>\n\n        {proxiesProvider.data && (\n          <Grid container spacing={2}>\n            {Object.entries(proxiesProvider.data).map(([name, provider]) => (\n              <Grid\n                key={name}\n                className=\"w-full\"\n                size={{\n                  sm: 12,\n                  md: 6,\n                  lg: 4,\n                  xl: 3,\n                }}\n              >\n                <ProxiesProvider provider={provider} />\n              </Grid>\n            ))}\n          </Grid>\n        )}\n\n        <div className=\"flex items-center justify-between\">\n          <Chip\n            className=\"!h-10 truncate !rounded-full !p-2 !text-lg font-bold\"\n            label={`${t(`Rules Providers`)} (${Object.entries(rulesProvider.data ?? {}).length})`}\n          />\n\n          <UpdateProviders />\n        </div>\n\n        {rulesProvider.data && (\n          <Grid container spacing={2}>\n            {Object.entries(rulesProvider.data).map(([name, provider]) => (\n              <Grid\n                key={name}\n                size={{\n                  sm: 12,\n                  md: 6,\n                  lg: 4,\n                  xl: 3,\n                }}\n              >\n                <RulesProvider provider={provider} />\n              </Grid>\n            ))}\n          </Grid>\n        )}\n      </div>\n    </BasePage>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(legacy)/proxies.tsx",
    "content": "import { useLockFn } from 'ahooks'\nimport { useAtom } from 'jotai'\nimport { RefObject, useEffect, useMemo, useRef, useState } from 'react'\nimport { useTranslation } from 'react-i18next'\nimport ContentDisplay from '@/components/base/content-display'\nimport {\n  DelayButton,\n  GroupList,\n  NodeList,\n  NodeListRef,\n} from '@/components/proxies'\nimport ProxyGroupName from '@/components/proxies/proxy-group-name'\nimport ScrollCurrentNode from '@/components/proxies/scroll-current-node'\nimport SortSelector from '@/components/proxies/sort-selector'\nimport { proxyGroupAtom } from '@/store'\nimport { proxiesFilterAtom } from '@/store/proxies'\nimport { Check } from '@mui/icons-material'\nimport { TextField, ToggleButton, ToggleButtonGroup } from '@mui/material'\nimport {\n  ProxyGroupItem,\n  useClashProxies,\n  useProxyMode,\n} from '@nyanpasu/interface'\nimport { alpha, cn, SidePage } from '@nyanpasu/ui'\nimport { createFileRoute } from '@tanstack/react-router'\n\nexport const Route = createFileRoute('/(legacy)/proxies')({\n  component: ProxyPage,\n})\n\nfunction SideBar() {\n  const [proxiesFilter, setProxiesFilter] = useAtom(proxiesFilterAtom)\n  const { t } = useTranslation()\n\n  return (\n    <TextField\n      hiddenLabel\n      fullWidth\n      autoComplete=\"off\"\n      spellCheck=\"false\"\n      placeholder={t('Filter conditions')}\n      className=\"!pb-0\"\n      sx={{ input: { py: 1.2, fontSize: 14 } }}\n      value={proxiesFilter || ''}\n      onChange={(e) =>\n        setProxiesFilter(!e.target.value.trim().length ? null : e.target.value)\n      }\n      slotProps={{\n        input: {\n          sx: (theme) => ({\n            borderRadius: 7,\n            backgroundColor: alpha(theme.vars.palette.primary.main, 0.1),\n\n            fieldset: {\n              border: 'none',\n            },\n          }),\n        },\n      }}\n    />\n  )\n}\n\nfunction ProxyPage() {\n  const { t } = useTranslation()\n\n  const { value: proxyMode, upsert } = useProxyMode()\n\n  const {\n    proxies: { data },\n  } = useClashProxies()\n\n  const [proxyGroup] = useAtom(proxyGroupAtom)\n\n  const [group, setGroup] = useState<ProxyGroupItem>()\n\n  useEffect(() => {\n    if (proxyMode.global) {\n      setGroup(data?.global)\n    } else if (proxyMode.direct) {\n      setGroup(data?.direct ? { ...data.direct, all: [] } : undefined)\n    } else {\n      if (proxyGroup.selector !== null) {\n        setGroup(data?.groups[proxyGroup.selector])\n      }\n    }\n  }, [\n    proxyGroup.selector,\n    data?.groups,\n    data?.global,\n    proxyMode.global,\n    proxyMode.direct,\n    data?.direct,\n  ])\n\n  const handleDelayClick = async () => {\n    if (proxyMode.global) {\n      await data?.global.mutateDelay()\n    } else {\n      if (proxyGroup.selector !== null) {\n        await data?.groups[proxyGroup.selector].mutateDelay()\n      }\n    }\n  }\n\n  const hasProxies = Boolean(data?.groups.length)\n\n  const nodeListRef = useRef<NodeListRef>(null)\n\n  const handleSwitch = useLockFn(async (key: string) => {\n    await upsert(key)\n  })\n\n  const Header = useMemo(() => {\n    return (\n      <div className=\"flex items-center gap-1\">\n        <ToggleButtonGroup\n          color=\"primary\"\n          size=\"small\"\n          exclusive\n          onChange={(_, newValue) => handleSwitch(newValue)}\n        >\n          {Object.entries(proxyMode).map(([key, enabled], index) => (\n            <ToggleButton\n              key={key}\n              className={cn(\n                'flex justify-center gap-0.5 !px-3',\n                index === 0 && '!rounded-l-full',\n                index === Object.entries(proxyMode).length - 1 &&\n                  '!rounded-r-full',\n              )}\n              value={key}\n              selected={enabled}\n            >\n              {enabled && <Check className=\"mr-[0.1rem] -ml-2 scale-75\" />}\n              {t(key)}\n            </ToggleButton>\n          ))}\n        </ToggleButtonGroup>\n      </div>\n    )\n  }, [handleSwitch, proxyMode, t])\n\n  const leftViewportRef = useRef<HTMLDivElement>(null)\n\n  const rightViewportRef = useRef<HTMLDivElement>(null)\n\n  return (\n    <SidePage\n      title={t('Proxy Groups')}\n      leftViewportRef={leftViewportRef}\n      rightViewportRef={rightViewportRef}\n      header={Header}\n      sideBar={<SideBar />}\n      side={\n        hasProxies &&\n        proxyMode.rule && (\n          <GroupList scrollRef={leftViewportRef as RefObject<HTMLElement>} />\n        )\n      }\n      portalRightRoot={\n        hasProxies &&\n        !proxyMode.direct && (\n          <div\n            className={cn(\n              'absolute z-10 flex w-full items-center justify-between px-4 py-2 backdrop-blur',\n              'bg-gray-200/30 dark:bg-gray-900/30',\n              '!rounded-t-2xl',\n            )}\n          >\n            <div className=\"flex items-center gap-4\">\n              {group?.name && <ProxyGroupName name={group?.name} />}\n            </div>\n\n            <div className=\"flex gap-2\">\n              <ScrollCurrentNode\n                onClick={() => {\n                  nodeListRef.current?.scrollToCurrent()\n                }}\n              />\n\n              <SortSelector />\n            </div>\n          </div>\n        )\n      }\n    >\n      {!proxyMode.direct ? (\n        hasProxies ? (\n          <>\n            <NodeList\n              ref={nodeListRef}\n              scrollRef={rightViewportRef as RefObject<HTMLElement>}\n            />\n\n            <DelayButton onClick={handleDelayClick} />\n          </>\n        ) : (\n          <ContentDisplay className=\"absolute\" message={t('No Proxies')} />\n        )\n      ) : (\n        <ContentDisplay className=\"absolute\" message={t('Direct Mode')} />\n      )}\n    </SidePage>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(legacy)/route.tsx",
    "content": "import AppContainer from '@/components/app/app-container'\nimport LocalesProvider from '@/components/app/locales-provider'\nimport MutationProvider from '@/components/layout/mutation-provider'\nimport NoticeProvider from '@/components/layout/notice-provider'\nimport PageTransition from '@/components/layout/page-transition'\nimport SchemeProvider from '@/components/layout/scheme-provider'\nimport { ThemeModeProvider } from '@/components/layout/use-custom-theme'\nimport UpdaterDialog from '@/components/updater/updater-dialog-wrapper'\nimport { UpdaterProvider } from '@/hooks/use-updater'\nimport { FileRouteTypes } from '@/route-tree.gen'\nimport { atomIsDrawer, memorizedRoutePathAtom } from '@/store'\nimport { CssBaseline, StyledEngineProvider } from '@mui/material'\nimport { useSettings } from '@nyanpasu/interface'\nimport { cn, useBreakpoint } from '@nyanpasu/ui'\nimport { createFileRoute, useLocation } from '@tanstack/react-router'\nimport 'dayjs/locale/ru'\nimport 'dayjs/locale/zh-cn'\nimport 'dayjs/locale/zh-tw'\nimport { useAtom, useSetAtom } from 'jotai'\nimport { PropsWithChildren, useEffect } from 'react'\nimport { SWRConfig } from 'swr'\n\nexport const Route = createFileRoute('/(legacy)')({\n  component: Layout,\n})\n\nconst QueryLoaderProvider = ({ children }: PropsWithChildren) => {\n  const {\n    query: { isLoading },\n  } = useSettings()\n\n  return isLoading ? null : children\n}\n\nfunction Layout() {\n  const breakpoint = useBreakpoint()\n\n  const setMemorizedPath = useSetAtom(memorizedRoutePathAtom)\n\n  const pathname = useLocation({\n    select: (location) => location.pathname,\n  })\n\n  useEffect(() => {\n    if (pathname !== '/') {\n      setMemorizedPath(pathname as FileRouteTypes['to'])\n    }\n  }, [pathname, setMemorizedPath])\n\n  const [isDrawer, setIsDrawer] = useAtom(atomIsDrawer)\n\n  useEffect(() => {\n    setIsDrawer(breakpoint === 'sm' || breakpoint === 'xs')\n  }, [breakpoint, setIsDrawer])\n\n  return (\n    <StyledEngineProvider injectFirst>\n      <ThemeModeProvider>\n        <CssBaseline />\n\n        <SWRConfig\n          value={{\n            errorRetryCount: 5,\n            revalidateOnMount: true,\n            revalidateOnFocus: true,\n            refreshInterval: 5000,\n          }}\n        >\n          <QueryLoaderProvider>\n            <LocalesProvider />\n            <MutationProvider />\n            <NoticeProvider />\n            <SchemeProvider />\n            <UpdaterDialog />\n            <UpdaterProvider />\n            <AppContainer isDrawer={isDrawer}>\n              <PageTransition\n                className={cn('absolute inset-4 top-10', !isDrawer && 'left-0')}\n              />\n            </AppContainer>\n          </QueryLoaderProvider>\n        </SWRConfig>\n      </ThemeModeProvider>\n    </StyledEngineProvider>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(legacy)/rules.tsx",
    "content": "import { useDebounceEffect } from 'ahooks'\nimport { useSetAtom } from 'jotai'\nimport { lazy, RefObject, useRef, useState } from 'react'\nimport { useTranslation } from 'react-i18next'\nimport { atomRulePage } from '@/components/rules/modules/store'\nimport { FilledInputProps, TextField } from '@mui/material'\nimport { useClashRules } from '@nyanpasu/interface'\nimport { alpha, BasePage } from '@nyanpasu/ui'\nimport { createFileRoute } from '@tanstack/react-router'\n\nexport const Route = createFileRoute('/(legacy)/rules')({\n  component: RulesPage,\n})\n\nfunction RulesPage() {\n  const { t } = useTranslation()\n\n  const { data } = useClashRules()\n\n  const [filterText, setFilterText] = useState('')\n\n  const setRule = useSetAtom(atomRulePage)\n\n  const viewportRef = useRef<HTMLDivElement>(null)\n\n  useDebounceEffect(\n    () => {\n      setRule({\n        data: data?.rules.filter((each) => each.payload.includes(filterText)),\n        scrollRef: viewportRef as RefObject<HTMLElement>,\n      })\n    },\n    [data, viewportRef.current, filterText],\n    { wait: 150 },\n  )\n\n  const inputProps: Partial<FilledInputProps> = {\n    sx: (theme) => ({\n      borderRadius: 7,\n      backgroundColor: alpha(theme.vars.palette.primary.main, 0.1),\n\n      fieldset: {\n        border: 'none',\n      },\n    }),\n  }\n\n  const Component = lazy(() => import('@/components/rules/rule-page'))\n\n  return (\n    <BasePage\n      full\n      title={t('Rules')}\n      header={\n        <TextField\n          hiddenLabel\n          autoComplete=\"off\"\n          spellCheck=\"false\"\n          value={filterText}\n          placeholder={t('Filter conditions')}\n          onChange={(e) => setFilterText(e.target.value)}\n          className=\"!pb-0\"\n          sx={{ input: { py: 1, fontSize: 14 } }}\n          slotProps={{\n            input: inputProps,\n          }}\n        />\n      }\n      viewportRef={viewportRef}\n    >\n      <Component />\n    </BasePage>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(legacy)/settings.tsx",
    "content": "import MdiTrayFull from '~icons/mdi/tray-full'\nimport { useLockFn } from 'ahooks'\nimport React, { lazy } from 'react'\nimport { useTranslation } from 'react-i18next'\nimport HotkeyDialog from '@/components/setting/modules/hotkey-dialog'\nimport TrayIconDialog from '@/components/setting/modules/tray-icon-dialog'\nimport { formatEnvInfos } from '@/utils'\nimport { Feedback, GitHub, Keyboard } from '@mui/icons-material'\nimport { IconButton } from '@mui/material'\nimport { commands, openThat } from '@nyanpasu/interface'\nimport { BasePage } from '@nyanpasu/ui'\nimport { createFileRoute } from '@tanstack/react-router'\n\nexport const Route = createFileRoute('/(legacy)/settings')({\n  component: SettingPage,\n})\n\nfunction SettingPage() {\n  const { t } = useTranslation()\n\n  const Component = lazy(() => import('@/components/setting/setting-page'))\n\n  const GithubIcon = () => {\n    const toGithubRepo = useLockFn(() => {\n      return openThat('https://github.com/libnyanpasu/clash-nyanpasu')\n    })\n\n    return (\n      <IconButton\n        color=\"inherit\"\n        title=\"@libnyanpasu/clash-nyanpasu\"\n        onClick={toGithubRepo}\n      >\n        <GitHub fontSize=\"inherit\" />\n      </IconButton>\n    )\n  }\n\n  const FeedbackIcon = () => {\n    const toFeedback = useLockFn(async () => {\n      const envs = await commands.collectEnvs()\n\n      if (envs.status !== 'ok') {\n        return\n      }\n\n      const formattedEnv = encodeURIComponent(\n        formatEnvInfos(envs.data)\n          .split('\\n')\n          .map((v) => `> ${v}`)\n          .join('\\n'),\n      )\n      return openThat(\n        'https://github.com/libnyanpasu/clash-nyanpasu/issues/new?assignees=&labels=T%3A+Bug%2CS%3A+Untriaged&projects=&template=bug_report.yaml&env_infos=' +\n          formattedEnv,\n      )\n    })\n    return (\n      <IconButton color=\"inherit\" title={t('Feedback')} onClick={toFeedback}>\n        <Feedback fontSize=\"inherit\" />\n      </IconButton>\n    )\n  }\n\n  // FIXME: it should move to a proper place\n  const HotkeyButton = () => {\n    const [open, setOpen] = React.useState(false)\n    return (\n      <>\n        <HotkeyDialog open={open} onClose={() => setOpen(false)} />\n        <IconButton\n          color=\"inherit\"\n          title={t('Hotkeys')}\n          onClick={() => setOpen(true)}\n        >\n          <Keyboard fontSize=\"inherit\" />\n        </IconButton>\n      </>\n    )\n  }\n\n  // FIXME: it should move to a proper place\n  const TrayIconButton = () => {\n    const [open, setOpen] = React.useState(false)\n    return (\n      <>\n        <TrayIconDialog open={open} onClose={() => setOpen(false)} />\n        <IconButton\n          color=\"inherit\"\n          title={t('Tray Icons')}\n          onClick={() => setOpen(true)}\n        >\n          <MdiTrayFull fontSize=\"inherit\" />\n        </IconButton>\n      </>\n    )\n  }\n\n  return (\n    <BasePage\n      title={t('Settings')}\n      header={\n        <div className=\"flex gap-1\">\n          <TrayIconButton />\n          <HotkeyButton />\n          <FeedbackIcon />\n          <GithubIcon />\n        </div>\n      }\n      sectionStyle={{\n        paddingRight: 0,\n      }}\n    >\n      <Component />\n    </BasePage>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/_modules/header-file-action.tsx",
    "content": "import { PropsWithChildren } from 'react'\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n} from '@/components/ui/dropdown-menu'\nimport { m } from '@/paraglide/messages'\nimport { Link } from '@tanstack/react-router'\nimport { ProfileType } from '../main/profiles/_modules/consts'\nimport { Action } from '../main/profiles/$type/index'\n\nexport default function HeaderFileAction({ children }: PropsWithChildren) {\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger asChild>{children}</DropdownMenuTrigger>\n\n      <DropdownMenuContent>\n        <DropdownMenuItem asChild>\n          <Link\n            to=\"/main/profiles/$type\"\n            params={{\n              type: ProfileType.Profile,\n            }}\n            search={{\n              action: Action.ImportLocalProfile,\n            }}\n          >\n            {m.header_file_action_import_local_profile()}\n          </Link>\n        </DropdownMenuItem>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/_modules/header-help-action.tsx",
    "content": "import { PropsWithChildren } from 'react'\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n} from '@/components/ui/dropdown-menu'\nimport { useLockFn } from '@/hooks/use-lock-fn'\nimport { m } from '@/paraglide/messages'\nimport { formatEnvInfos } from '@/utils'\nimport { commands } from '@nyanpasu/interface'\nimport { Link } from '@tanstack/react-router'\n\nconst WikiItem = () => {\n  const handleClick = useLockFn(async () => {\n    await commands.openThat('https://nyanpasu.elaina.moe')\n  })\n\n  return (\n    <DropdownMenuItem onClick={handleClick}>\n      {m.header_help_action_wiki()}\n    </DropdownMenuItem>\n  )\n}\n\nconst IssuesItem = () => {\n  const handleClick = useLockFn(async () => {\n    const envs = await commands.collectEnvs()\n\n    if (envs.status !== 'ok') {\n      return\n    }\n\n    const formattedEnv = encodeURIComponent(\n      formatEnvInfos(envs.data)\n        .split('\\n')\n        .map((v) => `> ${v}`)\n        .join('\\n'),\n    )\n\n    const params = new URLSearchParams({\n      assignees: '',\n      labels: 'T%3A+Bug%2CS%3A+Untriaged',\n      projects: '',\n      template: 'bug_report.yaml',\n    })\n\n    return commands.openThat(\n      'https://github.com/libnyanpasu/clash-nyanpasu/issues/new?' +\n        params.toString() +\n        // envs can't be serialized\n        '&env_infos=' +\n        formattedEnv,\n    )\n  })\n\n  return (\n    <DropdownMenuItem onClick={handleClick}>\n      {m.header_help_action_issues()}\n    </DropdownMenuItem>\n  )\n}\n\nconst CollectLogItem = () => {\n  const handleClick = useLockFn(async () => {\n    await commands.collectLogs()\n  })\n\n  return (\n    <DropdownMenuItem onClick={handleClick}>\n      {m.header_help_action_collect_logs()}\n    </DropdownMenuItem>\n  )\n}\n\nexport default function HeaderHelpAction({ children }: PropsWithChildren) {\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger asChild>{children}</DropdownMenuTrigger>\n\n      <DropdownMenuContent>\n        <WikiItem />\n\n        <IssuesItem />\n\n        <CollectLogItem />\n\n        <DropdownMenuItem asChild>\n          <Link to=\"/main/settings/about\">{m.header_help_action_about()}</Link>\n        </DropdownMenuItem>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/_modules/header-menu.tsx",
    "content": "import { ComponentProps } from 'react'\nimport { Button, ButtonProps } from '@/components/ui/button'\nimport { m } from '@/paraglide/messages'\nimport { cn } from '@nyanpasu/ui'\nimport HeaderFileAction from './header-file-action'\nimport HeaderHelpAction from './header-help-action'\nimport HeaderSettingsAction from './header-settings-action'\n\nconst MenuButton = ({ className, ...props }: ButtonProps) => {\n  return (\n    <Button\n      className={cn(\n        'hover:bg-primary-container dark:hover:bg-on-primary h-8 min-w-0 px-3',\n        'data-[state=open]:bg-primary-container dark:data-[state=open]:bg-on-primary',\n        className,\n      )}\n      {...props}\n    />\n  )\n}\n\n// TODO: implement menu items\nexport default function HeaderMenu({\n  className,\n  ...props\n}: ComponentProps<'div'>) {\n  return (\n    <div\n      className={cn('flex items-center gap-0.5', className)}\n      {...props}\n      data-tauri-drag-region\n    >\n      <HeaderFileAction>\n        <MenuButton>{m.header_file_action_title()}</MenuButton>\n      </HeaderFileAction>\n\n      <HeaderSettingsAction>\n        <MenuButton>{m.header_settings_action_title()}</MenuButton>\n      </HeaderSettingsAction>\n\n      <HeaderHelpAction>\n        <MenuButton>{m.header_help_action_title()}</MenuButton>\n      </HeaderHelpAction>\n    </div>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/_modules/header-settings-action.tsx",
    "content": "import { PropsWithChildren } from 'react'\nimport { useLanguage } from '@/components/providers/language-provider'\nimport {\n  ThemeMode,\n  useExperimentalThemeContext,\n} from '@/components/providers/theme-provider'\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuRadioGroup,\n  DropdownMenuRadioItem,\n  DropdownMenuSub,\n  DropdownMenuSubContent,\n  DropdownMenuSubTrigger,\n  DropdownMenuTrigger,\n} from '@/components/ui/dropdown-menu'\nimport { m } from '@/paraglide/messages'\nimport { Locale, locales } from '@/paraglide/runtime'\n\nconst LanguageSelector = () => {\n  const { language, setLanguage } = useLanguage()\n\n  const handleLanguageChange = (value: string) => {\n    setLanguage(value as Locale)\n  }\n\n  return (\n    <DropdownMenuSub>\n      <DropdownMenuSubTrigger>\n        {m.header_settings_action_language()}\n      </DropdownMenuSubTrigger>\n\n      <DropdownMenuSubContent>\n        <DropdownMenuRadioGroup\n          value={language}\n          onValueChange={handleLanguageChange}\n        >\n          {Object.entries(locales).map(([key, value]) => (\n            <DropdownMenuRadioItem key={key} value={value}>\n              {m.language(key, { locale: value })}\n            </DropdownMenuRadioItem>\n          ))}\n        </DropdownMenuRadioGroup>\n      </DropdownMenuSubContent>\n    </DropdownMenuSub>\n  )\n}\n\nconst ThemeModeSelector = () => {\n  const { themeMode, setThemeMode } = useExperimentalThemeContext()\n\n  const handleThemeModeChange = (value: string) => {\n    setThemeMode(value as ThemeMode)\n  }\n\n  const messages = {\n    [ThemeMode.LIGHT]: m.settings_user_interface_theme_mode_light(),\n    [ThemeMode.DARK]: m.settings_user_interface_theme_mode_dark(),\n    [ThemeMode.SYSTEM]: m.settings_user_interface_theme_mode_system(),\n  } satisfies Record<ThemeMode, string>\n\n  return (\n    <DropdownMenuSub>\n      <DropdownMenuSubTrigger>\n        {m.header_settings_action_theme_mode()}\n      </DropdownMenuSubTrigger>\n\n      <DropdownMenuSubContent>\n        <DropdownMenuRadioGroup\n          value={themeMode}\n          onValueChange={handleThemeModeChange}\n        >\n          {Object.entries(messages).map(([key, value]) => (\n            <DropdownMenuRadioItem key={key} value={key}>\n              {value}\n            </DropdownMenuRadioItem>\n          ))}\n        </DropdownMenuRadioGroup>\n      </DropdownMenuSubContent>\n    </DropdownMenuSub>\n  )\n}\n\nexport default function HeaderSettingsAction({ children }: PropsWithChildren) {\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger asChild>{children}</DropdownMenuTrigger>\n\n      <DropdownMenuContent>\n        <LanguageSelector />\n\n        <ThemeModeSelector />\n      </DropdownMenuContent>\n    </DropdownMenu>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/_modules/header.tsx",
    "content": "import { ComponentProps } from 'react'\nimport WindowControl from '@/components/window/window-control'\nimport WindowHeader from '@/components/window/window-header'\nimport WindowTitle from '@/components/window/window-title'\nimport { isMacOS } from '@/consts'\nimport useWindowMaximized from '@/hooks/use-window-maximized'\nimport { cn } from '@nyanpasu/ui'\nimport HeaderMenu from './header-menu'\n\nconst APP_NAME = 'Clash Nyanpasu'\n\nconst Title = () => {\n  return (\n    <WindowTitle>\n      <div\n        className=\"text-on-surface text-base font-bold text-nowrap\"\n        data-slot=\"app-header-logo-name\"\n        data-tauri-drag-region\n      >\n        {APP_NAME}\n      </div>\n    </WindowTitle>\n  )\n}\n\nexport function DefaultHeader({ className, ...props }: ComponentProps<'div'>) {\n  return (\n    <WindowHeader\n      className={cn('items-center justify-between px-3', className)}\n      data-slot=\"app-header\"\n      {...props}\n    >\n      <div className=\"flex items-center gap-2\" data-tauri-drag-region>\n        <Title />\n        <HeaderMenu className=\"hidden md:flex\" />\n      </div>\n\n      <WindowControl />\n    </WindowHeader>\n  )\n}\n\nexport function MacOSHeader({ className, ...props }: ComponentProps<'div'>) {\n  const { isMaximized } = useWindowMaximized()\n\n  return (\n    <WindowHeader\n      className={cn('items-center justify-center px-3', className)}\n      data-slot=\"app-header\"\n      {...props}\n    >\n      <div\n        className={cn(\n          'absolute left-22 hidden items-center md:flex',\n          isMaximized ? 'left-2' : 'left-22',\n        )}\n        data-tauri-drag-region\n      >\n        <HeaderMenu />\n      </div>\n\n      <Title />\n    </WindowHeader>\n  )\n}\n\nexport default function Header({ className, ...props }: ComponentProps<'div'>) {\n  return isMacOS ? (\n    <MacOSHeader className={className} {...props} />\n  ) : (\n    <DefaultHeader className={className} {...props} />\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/_modules/navbar.tsx",
    "content": "import Apps from '~icons/material-symbols/apps'\nimport DashboardRounded from '~icons/material-symbols/dashboard-rounded'\nimport DesignServicesRounded from '~icons/material-symbols/design-services-rounded'\nimport GridViewOutlineRounded from '~icons/material-symbols/grid-view-outline-rounded'\nimport Public from '~icons/material-symbols/public'\nimport SettingsEthernetRounded from '~icons/material-symbols/settings-ethernet-rounded'\nimport SettingsRounded from '~icons/material-symbols/settings-rounded'\nimport TerminalRounded from '~icons/material-symbols/terminal-rounded'\nimport { ComponentProps, ReactNode, useMemo } from 'react'\nimport { Button } from '@/components/ui/button'\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipTrigger,\n} from '@/components/ui/tooltip'\nimport useIsMobile from '@/hooks/use-is-moblie'\nimport { m } from '@/paraglide/messages'\nimport { useClashProxies } from '@nyanpasu/interface'\nimport { cn } from '@nyanpasu/ui'\nimport { Link, useLocation } from '@tanstack/react-router'\n\nconst NavbarButton = ({\n  icon,\n  label,\n  to,\n  mobileTo,\n  ...props\n}: Omit<ComponentProps<typeof Link>, 'children'> & {\n  icon: ReactNode\n  label: string\n  mobileTo?: ComponentProps<typeof Link>['to']\n}) => {\n  const location = useLocation()\n\n  const isMobile = useIsMobile()\n\n  const finalTo = isMobile && mobileTo ? mobileTo : to\n\n  const isActive = Boolean(\n    mobileTo\n      ? location.pathname.startsWith(mobileTo)\n      : to && location.pathname.startsWith(to),\n  )\n\n  return (\n    <Tooltip>\n      <TooltipTrigger asChild>\n        <Button\n          className={cn(\n            'flex items-center justify-center gap-1',\n            'lg:w-fit lg:px-3',\n            'sm:h-8!',\n            'hover:bg-primary-container dark:hover:bg-primary-container min-w-0',\n            'dark:data-[active=true]:bg-primary-container! data-[active=true]:bg-inverse-primary!',\n          )}\n          data-active={String(Boolean(isActive))}\n          asChild\n        >\n          <Link {...props} to={finalTo}>\n            <span className=\"size-5\" data-slot=\"navbar-button-icon\">\n              {icon}\n            </span>\n\n            <span className=\"hidden lg:block\" data-slot=\"navbar-button-label\">\n              {label}\n            </span>\n          </Link>\n        </Button>\n      </TooltipTrigger>\n\n      <TooltipContent\n        side=\"bottom\"\n        sideOffset={-4}\n        className=\"hidden sm:block md:hidden\"\n      >\n        {label}\n      </TooltipContent>\n    </Tooltip>\n  )\n}\n\nconst ProxiesGroupButton = () => {\n  const isMobile = useIsMobile()\n\n  const {\n    proxies: { data: proxies },\n  } = useClashProxies()\n\n  const fristGroup = useMemo(() => {\n    return proxies?.groups[0]?.name\n  }, [proxies])\n\n  if (isMobile || !fristGroup) {\n    return (\n      <NavbarButton\n        to=\"/main/proxies\"\n        icon={<Public className=\"size-5\" />}\n        label={m.navbar_label_proxies()}\n      />\n    )\n  }\n\n  return (\n    <NavbarButton\n      to=\"/main/proxies/group/$name\"\n      mobileTo=\"/main/proxies\"\n      params={{ name: fristGroup } as never}\n      icon={<Public className=\"size-5\" />}\n      label={m.navbar_label_proxies()}\n    />\n  )\n}\n\nexport default function Navbar({ className, ...props }: ComponentProps<'div'>) {\n  return (\n    <div\n      className={cn(\n        'dark:bg-on-primary bg-primary-container flex items-center px-3',\n        'h-16 sm:h-12',\n        'justify-between sm:justify-start',\n        'gap-2 lg:gap-1',\n        className,\n      )}\n      data-slot=\"app-navbar\"\n      {...props}\n    >\n      <NavbarButton\n        to=\"/main/dashboard\"\n        icon={<DashboardRounded className=\"size-5\" />}\n        label={m.navbar_label_dashboard()}\n      />\n\n      <ProxiesGroupButton />\n\n      <NavbarButton\n        to=\"/main/profiles/profile\"\n        mobileTo=\"/main/profiles\"\n        icon={<GridViewOutlineRounded className=\"size-5\" />}\n        label={m.navbar_label_profiles()}\n      />\n\n      <NavbarButton\n        to=\"/main/connections\"\n        icon={<SettingsEthernetRounded className=\"size-5\" />}\n        label={m.navbar_label_connections()}\n      />\n\n      <NavbarButton\n        to=\"/main/rules\"\n        icon={<DesignServicesRounded className=\"size-5\" />}\n        label={m.navbar_label_rules()}\n      />\n\n      <NavbarButton\n        to=\"/main/logs\"\n        icon={<TerminalRounded className=\"size-5\" />}\n        label={m.navbar_label_logs()}\n      />\n\n      <NavbarButton\n        to=\"/main/settings/system\"\n        mobileTo=\"/main/settings\"\n        icon={<SettingsRounded className=\"size-5\" />}\n        label={m.navbar_label_settings()}\n      />\n\n      <NavbarButton\n        to=\"/main/providers\"\n        icon={<Apps className=\"size-5\" />}\n        label={m.navbar_label_providers()}\n      />\n    </div>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/connections/_modules/table-row.tsx",
    "content": "import ChatInfoRounded from '~icons/material-symbols/chat-info-rounded'\nimport CloseRounded from '~icons/material-symbols/close-rounded'\nimport { sentenceCase } from 'change-case'\nimport dayjs from 'dayjs'\nimport { filesize } from 'filesize'\nimport { ComponentProps, useState } from 'react'\nimport {\n  RegisterContextMenu,\n  RegisterContextMenuContent,\n  RegisterContextMenuTrigger,\n} from '@/components/providers/context-menu-provider'\nimport { Button } from '@/components/ui/button'\nimport { Card, CardContent, CardFooter, CardHeader } from '@/components/ui/card'\nimport { ContextMenuItem } from '@/components/ui/context-menu'\nimport {\n  Modal,\n  ModalClose,\n  ModalContent,\n  ModalTitle,\n} from '@/components/ui/modal'\nimport { ScrollArea } from '@/components/ui/scroll-area'\nimport { useLockFn } from '@/hooks/use-lock-fn'\nimport { m } from '@/paraglide/messages'\nimport { useClashConnections } from '@nyanpasu/interface'\nimport { cn } from '@nyanpasu/ui'\nimport { ConnectionRow } from '..'\n\n// Keys added by ConnectionRow that should not be rendered in the dialog\nconst INTERNAL_KEYS = new Set(['closed', 'downloadSpeed', 'uploadSpeed'])\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction formatValue(key: string, value: any): React.ReactNode {\n  if (Array.isArray(value)) {\n    return <span>{value.join(' / ')}</span>\n  }\n\n  const k = key.toLowerCase()\n\n  if (k.includes('speed')) {\n    return <span>{filesize(value)}/s</span>\n  }\n\n  if (k.includes('download') || k.includes('upload')) {\n    return <span>{filesize(value)}</span>\n  }\n\n  if (k.includes('port') || k === 'id' || k.includes('ip')) {\n    return <span>{value}</span>\n  }\n\n  const date = dayjs(value)\n\n  if (date.isValid() && typeof value === 'string' && value.includes('T')) {\n    return (\n      <span title={date.format('YYYY-MM-DD HH:mm:ss')}>{date.fromNow()}</span>\n    )\n  }\n\n  return <span>{String(value)}</span>\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction RowRender({ label, value }: { label: string; value: any }) {\n  const key = label.toLowerCase()\n\n  return (\n    <>\n      <div className=\"w-fit text-sm font-semibold\">{sentenceCase(label)}</div>\n      <div\n        className={cn(\n          'text-sm break-all',\n          (key === 'id' ||\n            key.includes('ip') ||\n            key.includes('port') ||\n            key.includes('destination') ||\n            key.includes('path')) &&\n            'font-mono',\n        )}\n      >\n        {formatValue(key, value)}\n      </div>\n    </>\n  )\n}\n\nexport default function TableRow({\n  data,\n  onDoubleClick,\n  ...props\n}: ComponentProps<'tr'> & {\n  data: ConnectionRow\n}) {\n  const { deleteConnections } = useClashConnections()\n\n  const [open, setOpen] = useState(false)\n\n  const handleCloseConnection = useLockFn(async () => {\n    // frist close the dialog to avoid showing stale data when the deletion is slow\n    if (open) {\n      setOpen(false)\n    }\n\n    await deleteConnections.mutateAsync(data.id)\n  })\n\n  return (\n    <>\n      <RegisterContextMenu>\n        <RegisterContextMenuTrigger asChild>\n          <tr\n            onDoubleClick={(e) => {\n              onDoubleClick?.(e)\n              setOpen(true)\n            }}\n            {...props}\n          />\n        </RegisterContextMenuTrigger>\n\n        <RegisterContextMenuContent>\n          <ContextMenuItem onSelect={() => setOpen(true)}>\n            <ChatInfoRounded className=\"size-4\" />\n            <span>{m.connections_view_details()}</span>\n          </ContextMenuItem>\n\n          <ContextMenuItem onSelect={() => handleCloseConnection()}>\n            <CloseRounded className=\"size-4\" />\n            <span>{m.connections_close_connection()}</span>\n          </ContextMenuItem>\n        </RegisterContextMenuContent>\n      </RegisterContextMenu>\n\n      <Modal open={open} onOpenChange={setOpen}>\n        <ModalContent>\n          <Card divider className=\"flex max-w-[80vw] min-w-96 flex-col\">\n            <CardHeader>\n              <ModalTitle>{m.connections_view_details()}</ModalTitle>\n            </CardHeader>\n\n            <CardContent asChild className=\"p-0\">\n              <ScrollArea className=\"max-h-[70vh] select-text\">\n                <div className=\"grid grid-cols-[max-content_1fr] gap-x-4 gap-y-2 p-4\">\n                  {Object.entries(data)\n                    .filter(\n                      ([key, value]) =>\n                        key !== 'metadata' &&\n                        !INTERNAL_KEYS.has(key) &&\n                        value !== undefined &&\n                        value !== null &&\n                        value !== '',\n                    )\n                    .map(([key, value]) => (\n                      <RowRender key={key} label={key} value={value} />\n                    ))}\n\n                  <h3 className=\"col-span-2 pt-4 pb-1 text-base font-semibold\">\n                    Metadata\n                  </h3>\n\n                  {Object.entries(data.metadata)\n                    .filter(\n                      ([, value]) =>\n                        value !== undefined && value !== null && value !== '',\n                    )\n                    .map(([key, value]) => (\n                      <RowRender key={key} label={key} value={value} />\n                    ))}\n                </div>\n              </ScrollArea>\n            </CardContent>\n\n            <CardFooter className=\"gap-2\">\n              <ModalClose variant=\"flat\">{m.common_close()}</ModalClose>\n\n              <Button onClick={handleCloseConnection}>\n                {m.connections_close_connection()}\n              </Button>\n            </CardFooter>\n          </Card>\n        </ModalContent>\n      </Modal>\n    </>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/connections/index.tsx",
    "content": "import BoxOutlineRounded from '~icons/material-symbols/box-outline-rounded'\nimport CloseRounded from '~icons/material-symbols/close-rounded'\nimport dayjs from 'dayjs'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport {\n  RegisterContextMenu,\n  RegisterContextMenuContent,\n  RegisterContextMenuTrigger,\n} from '@/components/providers/context-menu-provider'\nimport { Button } from '@/components/ui/button'\nimport { ContextMenuItem } from '@/components/ui/context-menu'\nimport HighlightText from '@/components/ui/highlight-text'\nimport { ScrollArea, useScrollArea } from '@/components/ui/scroll-area'\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipTrigger,\n} from '@/components/ui/tooltip'\nimport { useLockFn } from '@/hooks/use-lock-fn'\nimport { m } from '@/paraglide/messages'\nimport { containsSearchTerm } from '@/utils'\nimport parseTraffic from '@/utils/parse-traffic'\nimport { ClashConnectionItem, useClashConnections } from '@nyanpasu/interface'\nimport { cn } from '@nyanpasu/ui'\nimport { createFileRoute } from '@tanstack/react-router'\nimport {\n  ColumnDef,\n  ColumnSizingState,\n  flexRender,\n  getCoreRowModel,\n  getSortedRowModel,\n  useReactTable,\n  type Updater,\n} from '@tanstack/react-table'\nimport { useVirtualizer } from '@tanstack/react-virtual'\nimport { useLocalStorage } from '@uidotdev/usehooks'\nimport TableRow from './_modules/table-row'\nimport { Route as IndexRoute } from './route'\n\nexport type ConnectionRow = ClashConnectionItem & {\n  closed: boolean\n  downloadSpeed: number\n  uploadSpeed: number\n}\n\nconst COLUMN_SIZING_STORAGE_KEY = 'connections-column-sizing-v2'\n\nexport const Route = createFileRoute('/(main)/main/connections/')({\n  component: RouteComponent,\n})\n\nconst Viewer = ({ search }: { search: string }) => {\n  const { proxy } = IndexRoute.useSearch()\n\n  const [columnSizing, setColumnSizing] = useLocalStorage<ColumnSizingState>(\n    COLUMN_SIZING_STORAGE_KEY,\n    {},\n  )\n\n  const {\n    query: { data: clashConnections },\n  } = useClashConnections()\n\n  const { viewportRef } = useScrollArea()\n\n  const data = useMemo<ConnectionRow[]>(() => {\n    const allSnapshots = clashConnections ?? []\n\n    const latestConnections = allSnapshots.at(-1)?.connections ?? []\n    const prevConnections = allSnapshots.at(-2)?.connections ?? []\n\n    const prevMap = new Map(prevConnections.map((c) => [c.id, c]))\n\n    const all = latestConnections\n      .filter((conn) => (proxy ? conn.chains?.includes(proxy) : true))\n      .map((conn) => {\n        const prev = prevMap.get(conn.id)\n        return {\n          ...conn,\n          closed: false,\n          downloadSpeed: prev ? conn.download - prev.download : 0,\n          uploadSpeed: prev ? conn.upload - prev.upload : 0,\n        }\n      })\n      .filter((c) => (search ? containsSearchTerm(c, search) : true))\n\n    return all\n  }, [clashConnections, search, proxy])\n\n  const handleColumnSizingChange = useCallback(\n    (updater: Updater<ColumnSizingState>) => {\n      setColumnSizing((prev) => {\n        return typeof updater === 'function' ? updater(prev) : updater\n      })\n    },\n    // oxlint-disable-next-line eslint-plugin-react-hooks/exhaustive-deps\n    [],\n  )\n\n  const columns = useMemo(\n    () =>\n      [\n        {\n          header: 'Host',\n          accessorFn: ({ metadata }) => metadata.host || metadata.destinationIP,\n          size: 320,\n          cell: (info) => (\n            <HighlightText searchText={search}>\n              {info.row.original.metadata.host ||\n                info.row.original.metadata.destinationIP ||\n                ''}\n            </HighlightText>\n          ),\n        },\n        {\n          header: 'Chains',\n          accessorFn: ({ chains }) => [...chains].reverse().join(' / '),\n          size: 360,\n          cell: (info) => (\n            <HighlightText searchText={search}>\n              {[...info.row.original.chains].reverse().join(' / ') || ''}\n            </HighlightText>\n          ),\n        },\n\n        {\n          header: 'Downloaded',\n          accessorFn: ({ download }) => parseTraffic(download).join(' '),\n          sortingFn: (rowA, rowB) =>\n            rowA.original.download - rowB.original.download,\n          size: 120,\n          cell: (info) => (\n            <HighlightText searchText={search}>\n              {parseTraffic(info.row.original.download).join(' ')}\n            </HighlightText>\n          ),\n        },\n        {\n          header: 'Uploaded',\n          accessorFn: ({ upload }) => parseTraffic(upload).join(' '),\n          sortingFn: (rowA, rowB) =>\n            rowA.original.upload - rowB.original.upload,\n          size: 120,\n          cell: (info) => (\n            <span>{parseTraffic(info.row.original.upload).join(' ')}</span>\n          ),\n        },\n        {\n          header: 'DL Speed',\n          accessorFn: ({ downloadSpeed }) =>\n            parseTraffic(downloadSpeed).join(' ') + '/s',\n          sortingFn: (rowA, rowB) =>\n            rowA.original.downloadSpeed - rowB.original.downloadSpeed,\n          size: 120,\n          cell: (info) => (\n            <span>\n              {parseTraffic(info.row.original.downloadSpeed).join(' ')}/s\n            </span>\n          ),\n        },\n        {\n          header: 'UL Speed',\n          accessorFn: ({ uploadSpeed }) =>\n            parseTraffic(uploadSpeed).join(' ') + '/s',\n          sortingFn: (rowA, rowB) =>\n            rowA.original.uploadSpeed - rowB.original.uploadSpeed,\n          size: 120,\n          cell: (info) => (\n            <span>\n              {parseTraffic(info.row.original.uploadSpeed).join(' ')}/s\n            </span>\n          ),\n        },\n        {\n          header: 'Process',\n          accessorFn: ({ metadata }) => metadata.process,\n          size: 160,\n          cell: (info) => (\n            <HighlightText searchText={search}>\n              {info.row.original.metadata.process || ''}\n            </HighlightText>\n          ),\n        },\n        {\n          header: 'Rule',\n          accessorFn: ({ rule, rulePayload }) =>\n            rulePayload ? `${rule} (${rulePayload})` : rule,\n          size: 200,\n          cell: (info) => (\n            <HighlightText searchText={search}>\n              {info.row.original.rulePayload\n                ? `${info.row.original.rule} (${info.row.original.rulePayload})`\n                : info.row.original.rule || ''}\n            </HighlightText>\n          ),\n        },\n        {\n          header: 'Time',\n          accessorFn: ({ start }) => dayjs(start).fromNow(),\n          sortingFn: (rowA, rowB) =>\n            dayjs(rowA.original.start).diff(rowB.original.start),\n          size: 120,\n          cell: (info) => (\n            <span\n              title={dayjs(info.row.original.start).format(\n                'YYYY-MM-DD HH:mm:ss',\n              )}\n            >\n              {dayjs(info.row.original.start).fromNow()}\n            </span>\n          ),\n        },\n        {\n          header: 'Source',\n          accessorFn: ({ metadata: { sourceIP, sourcePort } }) =>\n            `${sourceIP}:${sourcePort}`,\n          size: 160,\n          cell: (info) => (\n            <HighlightText searchText={search}>\n              {`${info.row.original.metadata.sourceIP}:${info.row.original.metadata.sourcePort}`}\n            </HighlightText>\n          ),\n        },\n        {\n          header: 'Destination IP',\n          accessorFn: ({ metadata: { destinationIP, destinationPort } }) =>\n            `${destinationIP}:${destinationPort}`,\n          size: 160,\n          cell: (info) => (\n            <HighlightText searchText={search}>\n              {`${info.row.original.metadata.destinationIP || ''}:${info.row.original.metadata.destinationPort || ''}`}\n            </HighlightText>\n          ),\n        },\n        {\n          header: 'Type',\n          accessorFn: ({ metadata }) =>\n            `${metadata.type} (${metadata.network})`,\n          size: 120,\n          cell: (info) => (\n            <HighlightText searchText={search}>\n              {`${info.row.original.metadata.type} (${info.row.original.metadata.network})`}\n            </HighlightText>\n          ),\n        },\n      ] satisfies Array<ColumnDef<ConnectionRow>>,\n    [search],\n  )\n\n  const table = useReactTable({\n    data,\n    columns,\n    state: {\n      columnSizing,\n    },\n    onColumnSizingChange: handleColumnSizingChange,\n    getCoreRowModel: getCoreRowModel(),\n    getSortedRowModel: getSortedRowModel(),\n    enableColumnResizing: true,\n    columnResizeMode: 'onChange',\n  })\n\n  const { rows } = table.getRowModel()\n\n  const rowVirtualizer = useVirtualizer({\n    count: rows.length,\n    getScrollElement: () => viewportRef.current,\n    estimateSize: () => 40,\n    overscan: 10,\n    measureElement: (element) => element?.getBoundingClientRect().height,\n  })\n\n  const virtualItems = rowVirtualizer.getVirtualItems()\n\n  const [viewportWidth, setViewportWidth] = useState(0)\n\n  useEffect(() => {\n    const viewport = viewportRef.current\n\n    if (!viewport) {\n      return\n    }\n\n    const updateWidth = () => {\n      setViewportWidth(viewport.clientWidth)\n    }\n\n    updateWidth()\n\n    const observer = new ResizeObserver(updateWidth)\n    observer.observe(viewport)\n\n    return () => {\n      observer.disconnect()\n    }\n  }, [viewportRef])\n\n  const visibleColumnCount = table.getVisibleLeafColumns().length\n  const tableBaseWidth = table.getTotalSize()\n  const extraWidthPerColumn =\n    visibleColumnCount > 0 && viewportWidth > tableBaseWidth\n      ? (viewportWidth - tableBaseWidth) / visibleColumnCount\n      : 0\n  const tableRenderWidth = Math.max(tableBaseWidth, viewportWidth)\n\n  if (rows.length === 0) {\n    return (\n      <div\n        className=\"absolute inset-0 flex flex-col items-center justify-center gap-4\"\n        data-slot=\"connections-no-connections\"\n      >\n        <BoxOutlineRounded className=\"text-surface-variant size-16\" />\n\n        <p\n          className=\"text-surface-variant text-sm\"\n          data-slot=\"connections-no-connections-message\"\n        >\n          {m.connections_empty_message()}\n        </p>\n      </div>\n    )\n  }\n\n  return (\n    <div\n      className=\"mx-auto min-h-full\"\n      data-slot=\"connections-virtual-container\"\n      style={{\n        height: `${rowVirtualizer.getTotalSize()}px`,\n      }}\n    >\n      <table\n        className=\"divide-outline-variant w-full table-fixed border-separate border-spacing-0\"\n        data-slot=\"connections-virtual-table\"\n        style={{ width: tableRenderWidth }}\n      >\n        <thead className=\"bg-mixed-background sticky top-0 z-20 h-10\">\n          {table.getHeaderGroups().map((headerGroup) => (\n            <tr key={headerGroup.id}>\n              {headerGroup.headers.map((header) => (\n                <th\n                  key={header.id}\n                  colSpan={header.colSpan}\n                  className=\"border-outline-variant relative border-b whitespace-nowrap\"\n                  style={{ width: header.getSize() + extraWidthPerColumn }}\n                >\n                  {header.isPlaceholder ? null : (\n                    <div\n                      className={cn(\n                        'truncate px-3 text-left align-middle text-sm font-bold select-none',\n                        header.column.getCanSort() &&\n                          'hover:text-primary cursor-pointer',\n                      )}\n                      onClick={header.column.getToggleSortingHandler()}\n                    >\n                      {flexRender(\n                        header.column.columnDef.header,\n                        header.getContext(),\n                      )}\n                      {header.column.getIsSorted() === 'asc' && ' ↑'}\n                      {header.column.getIsSorted() === 'desc' && ' ↓'}\n                    </div>\n                  )}\n                  {header.column.getCanResize() && (\n                    <div\n                      onMouseDown={header.getResizeHandler()}\n                      onTouchStart={header.getResizeHandler()}\n                      className={cn(\n                        'absolute top-0 right-0 h-full w-1 cursor-col-resize touch-none select-none',\n                        'hover:bg-primary/40 bg-transparent',\n                        header.column.getIsResizing() && 'bg-primary/60',\n                      )}\n                    />\n                  )}\n                </th>\n              ))}\n            </tr>\n          ))}\n        </thead>\n\n        <tbody className=\"select-text\" data-slot=\"connections-virtual-tbody\">\n          {virtualItems.map((virtualRow, index) => {\n            const row = rows[virtualRow.index]\n\n            if (!row) {\n              return null\n            }\n\n            const offset = virtualRow.start - index * virtualRow.size\n\n            return (\n              <TableRow\n                key={row.id}\n                data-index={virtualRow.index}\n                ref={(node) => rowVirtualizer.measureElement(node)}\n                className={cn(\n                  'transition-colors',\n                  'hover:bg-primary/5 active:bg-primary/10',\n                  row.original.closed && 'opacity-40',\n                )}\n                style={{\n                  height: `${virtualRow.size}px`,\n                  transform: `translateY(${offset}px)`,\n                }}\n                data={row.original}\n              >\n                {row.getVisibleCells().map(({ column, id, getContext }) => (\n                  <td\n                    key={id}\n                    className=\"border-outline-variant/30 max-w-0 truncate border-b px-3 text-sm\"\n                    style={{ width: column.getSize() + extraWidthPerColumn }}\n                  >\n                    {flexRender(column.columnDef.cell, getContext())}\n                  </td>\n                ))}\n              </TableRow>\n            )\n          })}\n        </tbody>\n      </table>\n    </div>\n  )\n}\n\nfunction RouteComponent() {\n  const [search, setSearch] = useState('')\n\n  const { deleteConnections } = useClashConnections()\n\n  const handleCloseAllConnections = useLockFn(async () => {\n    await deleteConnections.mutateAsync(null)\n  })\n\n  return (\n    <div className=\"divide-outline-variant flex h-full min-h-0 flex-1 flex-col divide-y overflow-hidden\">\n      <RegisterContextMenu>\n        <RegisterContextMenuTrigger asChild>\n          <ScrollArea className=\"min-h-0 flex-1\" scrollbars=\"both\" type=\"hover\">\n            <Viewer search={search} />\n          </ScrollArea>\n        </RegisterContextMenuTrigger>\n\n        <RegisterContextMenuContent>\n          <ContextMenuItem onSelect={() => handleCloseAllConnections()}>\n            <CloseRounded className=\"size-4\" />\n            <span>{m.connections_close_all_connections()}</span>\n          </ContextMenuItem>\n        </RegisterContextMenuContent>\n      </RegisterContextMenu>\n\n      <div\n        className=\"bg-mixed-background flex h-16 shrink-0 items-center gap-3 px-4\"\n        data-slot=\"connections-toolbar\"\n      >\n        <input\n          type=\"text\"\n          className={cn(\n            'bg-surface-variant dark:bg-surface-variant/30',\n            'h-10 min-w-0 flex-1 rounded-full px-4 text-sm outline-none',\n          )}\n          placeholder={m.connections_search_placeholder()}\n          value={search}\n          onChange={(e) => setSearch(e.target.value)}\n        />\n\n        <Tooltip>\n          <TooltipTrigger asChild>\n            <Button onClick={handleCloseAllConnections} icon>\n              <CloseRounded />\n            </Button>\n          </TooltipTrigger>\n\n          <TooltipContent>\n            {m.connections_close_all_connections()}\n          </TooltipContent>\n        </Tooltip>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/connections/route.tsx",
    "content": "import ListRounded from '~icons/material-symbols/lists-rounded'\nimport { ComponentProps, PropsWithChildren, ReactNode, useMemo } from 'react'\nimport z from 'zod'\nimport { Button } from '@/components/ui/button'\nimport { ScrollArea } from '@/components/ui/scroll-area'\nimport {\n  Sidebar,\n  SidebarLabelItem,\n  SidebarProvider,\n  SidebarToggleButton,\n  useSidebar,\n} from '@/components/ui/slider-sidebar'\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipTrigger,\n} from '@/components/ui/tooltip'\nimport { useIsMobileOrTablet } from '@/hooks/use-is-moblie'\nimport { m } from '@/paraglide/messages'\nimport { useClashRules } from '@nyanpasu/interface'\nimport { cn } from '@nyanpasu/ui'\nimport { createFileRoute, Link, Outlet } from '@tanstack/react-router'\nimport ProxyIcon from '../rules/_modules/proxy-icon'\n\nexport const Route = createFileRoute('/(main)/main/connections')({\n  component: RouteComponent,\n  validateSearch: z.object({\n    proxy: z.string().optional().nullable(),\n  }),\n})\n\nconst SidebarContent = ({ className, ...props }: ComponentProps<'div'>) => {\n  return <div className={cn('p-2', className)} {...props} />\n}\n\nconst Item = ({\n  item,\n  children,\n  icon,\n}: PropsWithChildren<{\n  item?: string\n  icon?: ReactNode\n}>) => {\n  const { proxy } = Route.useSearch()\n\n  const { open, setOpen } = useSidebar()\n\n  const isMobileOrTablet = useIsMobileOrTablet()\n\n  const handleClick = () => {\n    if (isMobileOrTablet) {\n      setOpen(false)\n    }\n  }\n\n  return (\n    <Tooltip open={open ? false : undefined}>\n      <TooltipTrigger asChild>\n        <Button\n          variant=\"fab\"\n          data-active={String(item === proxy)}\n          className={cn(\n            'h-12 min-w-0 px-3',\n            'flex items-center gap-2',\n            'data-[active=true]:bg-surface-variant/50',\n            'data-[active=false]:bg-transparent',\n            'data-[active=false]:shadow-none',\n            'data-[active=false]:hover:shadow-none',\n            'data-[active=false]:hover:bg-surface-variant/30',\n          )}\n          onClick={handleClick}\n          asChild\n        >\n          <Link\n            to=\".\"\n            search={{\n              proxy: item,\n            }}\n          >\n            <div className=\"text-md grid size-6 shrink-0 place-content-center\">\n              {icon}\n            </div>\n\n            <SidebarLabelItem>{children}</SidebarLabelItem>\n          </Link>\n        </Button>\n      </TooltipTrigger>\n\n      <TooltipContent side=\"right\">\n        <p>{children}</p>\n      </TooltipContent>\n    </Tooltip>\n  )\n}\n\nconst ProxySelector = () => {\n  const { data } = useClashRules()\n\n  const allProxy = useMemo(() => {\n    const proxies =\n      data?.rules\n        .map((rule) => rule.proxy)\n        .filter((proxy): proxy is string => !!proxy) ?? []\n\n    return [...new Set(proxies)]\n  }, [data])\n\n  return (\n    <SidebarContent className=\"flex flex-col gap-2\">\n      <Item icon={<ListRounded />}>{m.connections_all_connections()}</Item>\n\n      {allProxy.map((item) => (\n        <Item key={item} item={item} icon={<ProxyIcon groupName={item} />}>\n          {item}\n        </Item>\n      ))}\n    </SidebarContent>\n  )\n}\n\nfunction RouteComponent() {\n  return (\n    <SidebarProvider defaultOpen={false}>\n      <div\n        className={cn(\n          'divide-outline-variant relative flex h-full min-h-0 w-full divide-x overflow-hidden',\n        )}\n      >\n        <Sidebar className=\"divide-outline-variant z-10 flex flex-col divide-y\">\n          <ScrollArea className=\"min-h-0 w-full flex-1 [&>div>div]:block!\">\n            <ProxySelector />\n          </ScrollArea>\n\n          <SidebarContent className=\"flex h-16 justify-end\">\n            <SidebarToggleButton />\n          </SidebarContent>\n        </Sidebar>\n\n        <Outlet />\n      </div>\n    </SidebarProvider>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/dashboard/route.tsx",
    "content": "import { Button } from '@/components/ui/button'\nimport { AppContentScrollArea } from '@/components/ui/scroll-area'\nimport { createFileRoute } from '@tanstack/react-router'\n\nexport const Route = createFileRoute('/(main)/main/dashboard')({\n  component: RouteComponent,\n})\n\nfunction RouteComponent() {\n  return (\n    <AppContentScrollArea>\n      <div className=\"h-dvh\">\n        <p>Hello \"/(main)/main/dashboard\"!</p>\n\n        <Button>Click me</Button>\n      </div>\n    </AppContentScrollArea>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/index.tsx",
    "content": "import { createFileRoute, Navigate } from '@tanstack/react-router'\n\nexport const Route = createFileRoute('/(main)/main/')({\n  component: RouteComponent,\n})\n\nfunction RouteComponent() {\n  return <Navigate to=\"/main/dashboard\" />\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/logs/_modules/consts.ts",
    "content": "export enum LogLevel {\n  Debug = 'debug',\n  Info = 'info',\n  Warning = 'warning',\n  Error = 'error',\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/logs/_modules/log-level-badge.tsx",
    "content": "import { ComponentProps } from 'react'\nimport HighlightText from '@/components/ui/highlight-text'\nimport { cn } from '@nyanpasu/ui'\n\nexport default function LogLevelBadge({\n  className,\n  searchText = '',\n  children,\n  ...props\n}: ComponentProps<'div'> & { children: string; searchText?: string }) {\n  const childrenLower = children?.toLowerCase()\n\n  return (\n    <div\n      className={cn(\n        'inline-block rounded-full px-2 py-1 font-semibold uppercase',\n        childrenLower === 'info' && 'text-blue-500',\n        childrenLower === 'warn' && 'text-yellow-500',\n        childrenLower === 'error' && 'text-red-500',\n        className,\n      )}\n      {...props}\n    >\n      <HighlightText searchText={searchText}>{children}</HighlightText>\n    </div>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/logs/index.tsx",
    "content": "import BoxOutlineRounded from '~icons/material-symbols/box-outline-rounded'\nimport DeleteForeverOutlineRounded from '~icons/material-symbols/delete-forever-outline-rounded'\nimport { useEffect, useMemo, useState } from 'react'\nimport {\n  RegisterContextMenu,\n  RegisterContextMenuContent,\n  RegisterContextMenuTrigger,\n} from '@/components/providers/context-menu-provider'\nimport { ContextMenuItem } from '@/components/ui/context-menu'\nimport HighlightText from '@/components/ui/highlight-text'\nimport { ScrollArea, useScrollArea } from '@/components/ui/scroll-area'\nimport { useLockFn } from '@/hooks/use-lock-fn'\nimport { m } from '@/paraglide/messages'\nimport { useClashLogs } from '@nyanpasu/interface'\nimport { cn } from '@nyanpasu/ui'\nimport { createFileRoute } from '@tanstack/react-router'\nimport { useVirtualizer } from '@tanstack/react-virtual'\nimport LogLevelBadge from './_modules/log-level-badge'\nimport { Route as IndexRoute } from './route'\n\nexport const Route = createFileRoute('/(main)/main/logs/')({\n  component: RouteComponent,\n})\n\nconst Viewer = ({ search }: { search: string }) => {\n  const { level } = IndexRoute.useSearch()\n\n  const {\n    query: { data: logs },\n  } = useClashLogs()\n\n  const filteredLogs = useMemo(() => {\n    if (!logs) {\n      return []\n    }\n\n    if (!level) {\n      return logs\n    }\n\n    return logs.filter((log) => log.type.toLowerCase() === level)\n  }, [logs, level])\n\n  const { isBottom, viewportRef } = useScrollArea()\n\n  const rowVirtualizer = useVirtualizer({\n    count: filteredLogs.length,\n    getScrollElement: () => viewportRef.current,\n    estimateSize: () => 60,\n    overscan: 5,\n    measureElement: (element) => element?.getBoundingClientRect().height,\n  })\n\n  const virtualItems = rowVirtualizer.getVirtualItems()\n\n  useEffect(() => {\n    if (isBottom && filteredLogs.length > 0) {\n      rowVirtualizer.scrollToIndex(filteredLogs.length - 1, {\n        align: 'end',\n        behavior: 'smooth',\n      })\n    }\n  }, [filteredLogs, isBottom, rowVirtualizer])\n\n  if (filteredLogs.length === 0) {\n    return (\n      <div\n        className=\"absolute inset-0 flex flex-col items-center justify-center gap-4\"\n        data-slot=\"logs-no-logs\"\n      >\n        <BoxOutlineRounded className=\"text-surface-variant size-16\" />\n\n        <p\n          className=\"text-surface-variant text-sm\"\n          data-slot=\"logs-no-logs-message\"\n        >\n          {m.logs_empty_message()}\n        </p>\n      </div>\n    )\n  }\n\n  return (\n    <div\n      className={cn(\n        'relative mx-4 flex flex-col',\n        'divide-outline-variant divide-y',\n      )}\n      data-slot=\"logs-virtual-list\"\n      style={{\n        height: `${rowVirtualizer.getTotalSize()}px`,\n      }}\n    >\n      {virtualItems.map((virtualItem) => {\n        const log = filteredLogs[virtualItem.index]\n\n        if (!log) {\n          return null\n        }\n\n        return (\n          <div\n            key={virtualItem.key}\n            ref={rowVirtualizer.measureElement}\n            data-index={virtualItem.index}\n            data-slot=\"logs-virtual-item\"\n            className={cn(\n              'absolute top-0 left-0 w-full select-text',\n              'font-mono break-all',\n              'flex flex-col py-2',\n            )}\n            style={{\n              transform: `translateY(${virtualItem.start}px)`,\n            }}\n          >\n            <div className=\"flex items-center gap-1\">\n              <HighlightText searchText={search}>\n                {log.time || ''}\n              </HighlightText>\n\n              <LogLevelBadge searchText={search}>{log.type}</LogLevelBadge>\n            </div>\n\n            <div className=\"font-normal text-wrap\">\n              <HighlightText searchText={search}>\n                {log.payload || ''}\n              </HighlightText>\n            </div>\n          </div>\n        )\n      })}\n    </div>\n  )\n}\n\nfunction RouteComponent() {\n  const [search, setSearch] = useState('')\n\n  const {\n    query: { data: logs },\n    clean,\n  } = useClashLogs()\n\n  const handleClearLogs = useLockFn(async () => {\n    await clean.mutateAsync()\n  })\n\n  return (\n    <div className=\"divide-outline-variant flex h-full min-h-0 flex-1 flex-col divide-y overflow-hidden\">\n      <RegisterContextMenu>\n        <RegisterContextMenuTrigger asChild>\n          <ScrollArea className=\"min-h-0 flex-1\">\n            <Viewer search={search} />\n          </ScrollArea>\n        </RegisterContextMenuTrigger>\n\n        <RegisterContextMenuContent>\n          <ContextMenuItem\n            disabled={logs?.length === 0}\n            onClick={handleClearLogs}\n          >\n            <DeleteForeverOutlineRounded className=\"size-4\" />\n            <span>{m.logs_action_clear_log()}</span>\n          </ContextMenuItem>\n        </RegisterContextMenuContent>\n      </RegisterContextMenu>\n\n      <div\n        className=\"bg-mixed-background flex h-16 shrink-0 items-center px-4\"\n        data-slot=\"logs-search\"\n      >\n        <input\n          type=\"text\"\n          className={cn(\n            'bg-surface-variant dark:bg-surface-variant/30',\n            'h-10 w-full rounded-full px-4 pr-10 text-sm outline-none',\n          )}\n          data-slot=\"logs-search-input-field\"\n          placeholder={m.logs_search_placeholder()}\n          value={search}\n          onChange={(e) => setSearch(e.target.value)}\n        />\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/logs/route.tsx",
    "content": "import { ComponentProps, PropsWithChildren } from 'react'\nimport { z } from 'zod'\nimport { Button } from '@/components/ui/button'\nimport {\n  Sidebar,\n  SidebarLabelItem,\n  SidebarProvider,\n  SidebarToggleButton,\n  useSidebar,\n} from '@/components/ui/slider-sidebar'\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipTrigger,\n} from '@/components/ui/tooltip'\nimport { useIsMobileOrTablet } from '@/hooks/use-is-moblie'\nimport { cn } from '@nyanpasu/ui'\nimport { createFileRoute, Link, Outlet } from '@tanstack/react-router'\nimport { LogLevel } from './_modules/consts'\n\nexport const Route = createFileRoute('/(main)/main/logs')({\n  component: RouteComponent,\n  validateSearch: z.object({\n    level: z.enum(LogLevel).nullable().optional(),\n  }),\n})\n\nconst LogLevelIcon = {\n  [LogLevel.Debug]: () => '🐛',\n  [LogLevel.Info]: () => 'ℹ️',\n  [LogLevel.Warning]: () => '⚠️',\n  [LogLevel.Error]: () => '❌',\n} satisfies Record<LogLevel, React.FC>\n\nconst SidebarContent = ({ className, ...props }: ComponentProps<'div'>) => {\n  return <div className={cn('p-2', className)} {...props} />\n}\n\nconst LogLevelButton = ({\n  level: inputLevel,\n  children,\n}: PropsWithChildren<{ level?: LogLevel }>) => {\n  const { level } = Route.useSearch()\n\n  const Icon = inputLevel ? LogLevelIcon[inputLevel] : () => '📋'\n\n  const { open, setOpen } = useSidebar()\n\n  const isMobileOrTablet = useIsMobileOrTablet()\n\n  const handleClick = () => {\n    if (isMobileOrTablet) {\n      setOpen(false)\n    }\n  }\n\n  return (\n    <Tooltip open={open ? false : undefined}>\n      <TooltipTrigger asChild>\n        <Button\n          variant=\"fab\"\n          data-active={String(inputLevel === level)}\n          className={cn(\n            'h-12 min-w-0 px-3',\n            'flex items-center gap-2',\n            'data-[active=true]:bg-surface-variant/50',\n            'data-[active=false]:bg-transparent',\n            'data-[active=false]:shadow-none',\n            'data-[active=false]:hover:shadow-none',\n            'data-[active=false]:hover:bg-surface-variant/30',\n          )}\n          onClick={handleClick}\n          asChild\n        >\n          <Link\n            to=\".\"\n            search={{\n              level: inputLevel,\n            }}\n          >\n            <div className=\"text-md grid size-6 shrink-0 place-content-center\">\n              <Icon />\n            </div>\n\n            <SidebarLabelItem className=\"capitalize\">\n              {children}\n            </SidebarLabelItem>\n          </Link>\n        </Button>\n      </TooltipTrigger>\n\n      <TooltipContent side=\"right\">\n        <p className=\"capitalize\">{children}</p>\n      </TooltipContent>\n    </Tooltip>\n  )\n}\n\nfunction RouteComponent() {\n  return (\n    <SidebarProvider defaultOpen={false}>\n      <div\n        className={cn(\n          'divide-outline-variant relative flex h-full min-h-0 w-full divide-x overflow-hidden',\n        )}\n      >\n        <Sidebar className=\"divide-outline-variant z-10 flex flex-col divide-y\">\n          <SidebarContent className=\"flex flex-1 flex-col gap-2\">\n            <LogLevelButton>All</LogLevelButton>\n\n            {Object.values(LogLevel).map((item) => (\n              <LogLevelButton key={item} level={item}>\n                {item}\n              </LogLevelButton>\n            ))}\n          </SidebarContent>\n\n          <SidebarContent className=\"flex h-16 justify-end\">\n            <SidebarToggleButton />\n          </SidebarContent>\n        </Sidebar>\n\n        <Outlet />\n      </div>\n    </SidebarProvider>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/profiles/$type/_modules/chain-profile-import.tsx",
    "content": "import NoteStackAddRounded from '~icons/material-symbols/note-stack-add-rounded'\nimport dayjs from 'dayjs'\nimport { AnimatePresence } from 'framer-motion'\nimport { useState } from 'react'\nimport { Controller, useForm } from 'react-hook-form'\nimport z from 'zod'\nimport { useBlockTask } from '@/components/providers/block-task-provider'\nimport { Button } from '@/components/ui/button'\nimport { Card, CardContent, CardFooter, CardHeader } from '@/components/ui/card'\nimport { Input } from '@/components/ui/input'\nimport {\n  Modal,\n  ModalContent,\n  ModalTitle,\n  ModalTrigger,\n} from '@/components/ui/modal'\nimport { ScrollArea } from '@/components/ui/scroll-area'\nimport { useLockFn } from '@/hooks/use-lock-fn'\nimport { m } from '@/paraglide/messages'\nimport { formatError } from '@/utils'\nimport { message } from '@/utils/notification'\nimport { zodResolver } from '@hookform/resolvers/zod'\nimport {\n  MergeProfileBuilder,\n  ProfileTemplate,\n  ScriptProfileBuilder,\n  useProfile,\n} from '@nyanpasu/interface'\nimport {\n  PROFILE_TYPE_NAMES,\n  PROFILE_TYPES,\n  ProfileType as RawProfileType,\n} from '../../_modules/consts'\nimport AnimatedErrorItem from '../../_modules/error-item'\nimport { Route as IndexRoute } from '../index'\n\nconst formSchema = z.object({\n  type: z.enum(['merge', 'script']),\n  uid: z.string().nullable(),\n  name: z.string().nullable(),\n  file: z.string().nullable(),\n  desc: z.string().nullable(),\n  updated: z.number().nullable(),\n  script_type: z.literal('javascript').or(z.literal('lua')).nullable(),\n}) satisfies z.ZodType<MergeProfileBuilder | ScriptProfileBuilder>\n\nconst getDefaultValues = (rawType: RawProfileType) => {\n  // get the first type of the raw type\n  // FIXME: better error handling\n  const finallyType = PROFILE_TYPES[rawType][0]\n\n  // check if the type is script\n  const typeValidation = formSchema.shape.type.safeParse(finallyType.type)\n  if (!typeValidation.success) {\n    throw new Error(typeValidation.error.message)\n  }\n\n  // check if script_type is valid\n  const scriptTypeValue =\n    'script_type' in finallyType ? finallyType.script_type : null\n  const scriptTypeValidation =\n    formSchema.shape.script_type.safeParse(scriptTypeValue)\n  if (!scriptTypeValidation.success) {\n    throw new Error(scriptTypeValidation.error.message)\n  }\n\n  return {\n    type: typeValidation.data,\n    uid: null,\n    name: `${PROFILE_TYPE_NAMES[rawType]} - ${dayjs().format('YYYY-MM-DD HH:mm:ss')}`,\n    file: null,\n    desc: null,\n    updated: null,\n    script_type: scriptTypeValidation.data,\n  } satisfies z.infer<typeof formSchema>\n}\n\nexport default function ChainProfileImport() {\n  const [open, setOpen] = useState(false)\n\n  const { type } = IndexRoute.useParams()\n\n  const { create } = useProfile()\n\n  const form = useForm<z.infer<typeof formSchema>>({\n    resolver: zodResolver(formSchema),\n    defaultValues: getDefaultValues(type as RawProfileType),\n  })\n\n  const blockTask = useBlockTask(\n    `create-chain-profile`,\n    form.handleSubmit(async (data) => {\n      try {\n        await create.mutateAsync({\n          type: 'manual',\n          data: {\n            item: data,\n            // TODO: when content editor is implemented, use the content editor value instead of the template\n            fileData: ProfileTemplate[type as keyof typeof ProfileTemplate],\n          },\n        })\n        handleToggle(false)\n      } catch (error) {\n        message(`Create failed: \\n ${formatError(error)}`, {\n          title: 'Error',\n          kind: 'error',\n        })\n      }\n    }),\n  )\n\n  const handleToggle = (value: boolean) => {\n    if (blockTask.isPending) {\n      return\n    }\n\n    setOpen(value)\n\n    if (value) {\n      form.reset(getDefaultValues(type as RawProfileType))\n    }\n  }\n\n  const handleSubmit = useLockFn(blockTask.execute)\n\n  return (\n    <Modal open={open} onOpenChange={handleToggle}>\n      <ModalTrigger asChild>\n        <Button variant=\"fab\" icon>\n          <NoteStackAddRounded className=\"size-6\" />\n        </Button>\n      </ModalTrigger>\n\n      <ModalContent>\n        <Card className=\"w-96\">\n          <CardHeader>\n            <ModalTitle>\n              {m.profile_import_chain_title({\n                type: PROFILE_TYPE_NAMES[type as RawProfileType],\n              })}\n            </ModalTitle>\n          </CardHeader>\n\n          <CardContent asChild>\n            <ScrollArea className=\"max-h-[calc(100vh-200px)]\">\n              <div className=\"space-y-4 pt-2\">\n                <Controller\n                  control={form.control}\n                  name=\"name\"\n                  render={({ field }) => (\n                    <div className=\"space-y-2\">\n                      <Input\n                        variant=\"outlined\"\n                        label={m.profile_form_name_label()}\n                        {...field}\n                        value={field.value ?? ''}\n                      />\n\n                      <AnimatePresence>\n                        {form.formState.errors.name && (\n                          <AnimatedErrorItem className=\"text-error\">\n                            {form.formState.errors.name?.message}\n                          </AnimatedErrorItem>\n                        )}\n                      </AnimatePresence>\n                    </div>\n                  )}\n                />\n\n                <Controller\n                  control={form.control}\n                  name=\"desc\"\n                  render={({ field }) => (\n                    <div className=\"space-y-2\">\n                      <Input\n                        variant=\"outlined\"\n                        label={m.profile_form_desc_label()}\n                        {...field}\n                        value={field.value ?? ''}\n                      />\n\n                      <AnimatePresence>\n                        {form.formState.errors.desc && (\n                          <AnimatedErrorItem className=\"text-error\">\n                            {form.formState.errors.desc?.message}\n                          </AnimatedErrorItem>\n                        )}\n                      </AnimatePresence>\n                    </div>\n                  )}\n                />\n\n                {/* TODO: edit content before submit */}\n              </div>\n            </ScrollArea>\n          </CardContent>\n\n          <CardFooter className=\"gap-1\">\n            <Button onClick={handleSubmit} loading={blockTask.isPending}>\n              {m.common_submit()}\n            </Button>\n\n            <Button onClick={() => handleToggle(false)}>\n              {m.common_cancel()}\n            </Button>\n          </CardFooter>\n        </Card>\n      </ModalContent>\n    </Modal>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/profiles/$type/_modules/import-button.tsx",
    "content": "import CloudDownloadRounded from '~icons/material-symbols/cloud-download-rounded'\nimport FileOpenRounded from '~icons/material-symbols/file-open-rounded'\nimport NoteStackAddRounded from '~icons/material-symbols/note-stack-add-rounded'\nimport { AnimatePresence } from 'framer-motion'\nimport { ComponentProps, useEffect, useState } from 'react'\nimport { Button } from '@/components/ui/button'\nimport { useScrollArea } from '@/components/ui/scroll-area'\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipTrigger,\n} from '@/components/ui/tooltip'\nimport { m } from '@/paraglide/messages'\nimport { cn } from '@nyanpasu/ui'\nimport { ProfileType } from '../../_modules/consts'\nimport { Action, Route as IndexRoute } from '../index'\nimport ChainProfileImport from './chain-profile-import'\nimport LocalProfileButton from './local-profile-button'\nimport RemoteProfileButton from './remote-profile-button'\n\nconst SelectButton = ({\n  className,\n  label,\n  ...props\n}: ComponentProps<typeof Button> & {\n  label?: string\n}) => {\n  return (\n    <Tooltip>\n      <TooltipTrigger asChild>\n        <Button\n          className={cn(\n            'flex size-10 items-center justify-center gap-2',\n            'bg-primary-container dark:bg-surface-variant/30',\n            className,\n          )}\n          variant=\"fab\"\n          icon\n          {...props}\n        />\n      </TooltipTrigger>\n\n      {label && (\n        <TooltipContent side=\"left\">\n          <span>{label}</span>\n        </TooltipContent>\n      )}\n    </Tooltip>\n  )\n}\n\nconst ProxyProfileImport = () => {\n  const { isScrolling } = useScrollArea()\n\n  const { action } = IndexRoute.useSearch()\n\n  const [open, setOpen] = useState(false)\n\n  useEffect(() => {\n    // for animation duration to open the modal\n    if (action === Action.ImportLocalProfile) {\n      setOpen(true)\n    }\n  }, [action])\n\n  const handleToggle = () => {\n    setOpen(!open)\n  }\n\n  // close the modal when scrolling\n  useEffect(() => {\n    if (isScrolling && open) {\n      setOpen(false)\n    }\n  }, [isScrolling, open])\n\n  return (\n    <div className=\"relative\">\n      <Button className=\"z-10\" variant=\"fab\" icon onClick={handleToggle}>\n        <NoteStackAddRounded className=\"size-6\" />\n      </Button>\n\n      <AnimatePresence initial={false}>\n        <div\n          className={cn(\n            'absolute flex w-full flex-col items-center gap-4',\n            'top-0 scale-0 opacity-0 transition-[top,opacity,scale] duration-300 ease-in-out',\n            open && '-top-28 scale-100 opacity-100',\n          )}\n        >\n          <RemoteProfileButton>\n            <SelectButton label={m.profile_import_remote_title()}>\n              <CloudDownloadRounded />\n            </SelectButton>\n          </RemoteProfileButton>\n\n          <LocalProfileButton>\n            <SelectButton label={m.profile_import_local_title()}>\n              <FileOpenRounded />\n            </SelectButton>\n          </LocalProfileButton>\n        </div>\n      </AnimatePresence>\n    </div>\n  )\n}\n\nexport default function ImportButton() {\n  const { type } = IndexRoute.useParams()\n\n  const isProxy = type === ProfileType.Profile\n\n  return (\n    <div\n      className={cn(\n        'absolute',\n        'right-4 transition-[top] duration-500',\n        'top-[calc(100vh-40px-64px-72px)]',\n        'sm:top-[calc(100vh-40px-48px-72px)]',\n        'group-data-[scroll-direction=down]/profiles-content:top-full',\n      )}\n    >\n      {isProxy ? <ProxyProfileImport /> : <ChainProfileImport />}\n    </div>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/profiles/$type/_modules/local-profile-button.tsx",
    "content": "import UploadFileRounded from '~icons/material-symbols/upload-file-rounded'\nimport dayjs from 'dayjs'\nimport { filesize } from 'filesize'\nimport { AnimatePresence } from 'framer-motion'\nimport { PropsWithChildren, useEffect, useState } from 'react'\nimport { Controller, useForm } from 'react-hook-form'\nimport z from 'zod'\nimport { useBlockTask } from '@/components/providers/block-task-provider'\nimport { Button } from '@/components/ui/button'\nimport { Card, CardContent, CardFooter, CardHeader } from '@/components/ui/card'\nimport {\n  FileDropZone,\n  FileDropZoneFileSelected,\n  FileDropZoneLoading,\n  FileDropZonePlaceholder,\n} from '@/components/ui/file-drop-zone'\nimport { Input } from '@/components/ui/input'\nimport {\n  Modal,\n  ModalContent,\n  ModalTitle,\n  ModalTrigger,\n} from '@/components/ui/modal'\nimport { CircularProgress } from '@/components/ui/progress'\nimport { ScrollArea } from '@/components/ui/scroll-area'\nimport { useLockFn } from '@/hooks/use-lock-fn'\nimport { m } from '@/paraglide/messages'\nimport { formatError } from '@/utils'\nimport { message } from '@/utils/notification'\nimport { zodResolver } from '@hookform/resolvers/zod'\nimport {\n  LocalProfileBuilder,\n  ProfileTemplate,\n  useProfile,\n} from '@nyanpasu/interface'\nimport { useLocation } from '@tanstack/react-router'\nimport AnimatedErrorItem from '../../_modules/error-item'\nimport { Action, Route as IndexRoute } from '../index'\n\nconst formSchema = z.object({\n  uid: z.string().nullable(),\n  name: z.string().nullable(),\n  file: z.string().nullable(),\n  desc: z.string().nullable(),\n  updated: z.number().nullable(),\n  symlinks: z.string().nullable(),\n  chain: z.array(z.string()).nullable().optional(),\n}) satisfies z.ZodType<LocalProfileBuilder>\n\nconst acceptFiles = ['.yaml', '.yml']\n\nconst getDefaultValues = () => {\n  return {\n    uid: null,\n    name: `${m.profile_import_local_title()} - ${dayjs().format('YYYY-MM-DD HH:mm:ss')}`,\n    file: null,\n    desc: null,\n    updated: null,\n    symlinks: null,\n    chain: null,\n  } satisfies z.infer<typeof formSchema>\n}\n\nexport default function LocalProfileButton({ children }: PropsWithChildren) {\n  const { action } = IndexRoute.useSearch()\n\n  const navigate = IndexRoute.useNavigate()\n\n  const { pathname } = useLocation()\n\n  const { create } = useProfile()\n\n  const [open, setOpen] = useState(false)\n\n  const [profileContent, setProfileContent] = useState<string | null>(null)\n\n  useEffect(() => {\n    if (action === Action.ImportLocalProfile) {\n      // if the current path is the index page, open the modal immediately\n      if (pathname === '/main/profiles/$type') {\n        setOpen(true)\n        return\n      }\n\n      // else, wait animation duration to open the modal\n      const timeout = setTimeout(() => {\n        setOpen(true)\n      }, 150)\n\n      return () => {\n        clearTimeout(timeout)\n      }\n    }\n    // oxlint-disable-next-line eslint-plugin-react-hooks/exhaustive-deps\n  }, [action])\n\n  const form = useForm<z.infer<typeof formSchema>>({\n    resolver: zodResolver(formSchema),\n    defaultValues: getDefaultValues(),\n  })\n\n  const blockTask = useBlockTask(\n    `create-local-profile`,\n    form.handleSubmit(async (data) => {\n      try {\n        await create.mutateAsync({\n          type: 'manual',\n          data: {\n            item: {\n              type: 'local',\n              ...data,\n            },\n            fileData: profileContent || ProfileTemplate.profile,\n          },\n        })\n\n        handleToggle(false)\n      } catch (error) {\n        message(`Create failed: \\n ${formatError(error)}`, {\n          title: 'Error',\n          kind: 'error',\n        })\n      }\n    }),\n  )\n\n  const handleToggle = (value: boolean) => {\n    if (blockTask.isPending) {\n      return\n    }\n\n    setOpen(value)\n\n    navigate({\n      search: {\n        action: null,\n      },\n    })\n\n    if (value) {\n      form.reset(getDefaultValues())\n    }\n  }\n\n  const handleSubmit = useLockFn(blockTask.execute)\n\n  return (\n    <Modal open={open} onOpenChange={handleToggle}>\n      <ModalTrigger asChild>{children}</ModalTrigger>\n\n      <ModalContent>\n        <Card className=\"w-96\">\n          <CardHeader>\n            <ModalTitle>{m.profile_import_local_title()}</ModalTitle>\n          </CardHeader>\n\n          <CardContent asChild>\n            <ScrollArea className=\"max-h-[calc(100vh-200px)]\">\n              <div className=\"space-y-4 pt-2\">\n                <Controller\n                  control={form.control}\n                  name=\"name\"\n                  render={({ field }) => (\n                    <div className=\"space-y-2\">\n                      <Input\n                        variant=\"outlined\"\n                        label={m.profile_form_name_label()}\n                        {...field}\n                        value={field.value ?? ''}\n                      />\n\n                      <AnimatePresence>\n                        {form.formState.errors.name && (\n                          <AnimatedErrorItem className=\"text-error\">\n                            {form.formState.errors.name?.message}\n                          </AnimatedErrorItem>\n                        )}\n                      </AnimatePresence>\n                    </div>\n                  )}\n                />\n\n                <Controller\n                  control={form.control}\n                  name=\"desc\"\n                  render={({ field }) => (\n                    <div className=\"space-y-2\">\n                      <Input\n                        variant=\"outlined\"\n                        label={m.profile_form_desc_label()}\n                        {...field}\n                        value={field.value ?? ''}\n                      />\n\n                      <AnimatePresence>\n                        {form.formState.errors.desc && (\n                          <AnimatedErrorItem className=\"text-error\">\n                            {form.formState.errors.desc?.message}\n                          </AnimatedErrorItem>\n                        )}\n                      </AnimatePresence>\n                    </div>\n                  )}\n                />\n\n                <Controller\n                  control={form.control}\n                  name=\"file\"\n                  render={({ field }) => (\n                    <FileDropZone\n                      accept={acceptFiles}\n                      value={field.value}\n                      onChange={(name) => {\n                        form.setValue('desc', name)\n                      }}\n                      onFileRead={(value) => {\n                        setProfileContent(value)\n                      }}\n                      disabled={blockTask.isPending}\n                    >\n                      <FileDropZonePlaceholder className=\"flex flex-col items-center justify-center gap-2\">\n                        <UploadFileRounded className=\"text-on-surface-variant size-8\" />\n\n                        <span className=\"text-on-surface-variant text-sm\">\n                          {m.profile_import_local_file_placeholder()}\n                        </span>\n\n                        <span className=\"text-on-surface-variant text-xs\">\n                          {m.profile_import_local_file_type_label({\n                            types: acceptFiles.join(', '),\n                          })}\n                        </span>\n                      </FileDropZonePlaceholder>\n\n                      <FileDropZoneLoading>\n                        <CircularProgress className=\"size-8\" indeterminate />\n                      </FileDropZoneLoading>\n\n                      <FileDropZoneFileSelected className=\"flex flex-col items-center justify-center gap-2\">\n                        <UploadFileRounded className=\"text-primary size-8\" />\n\n                        <div className=\"text-on-surface max-w-full truncate text-sm font-medium\">\n                          {m.profile_import_local_file_size_label({\n                            size: filesize(profileContent?.length ?? 0),\n                          })}\n                        </div>\n                      </FileDropZoneFileSelected>\n                    </FileDropZone>\n                  )}\n                />\n              </div>\n            </ScrollArea>\n          </CardContent>\n\n          <CardFooter className=\"gap-1\">\n            <Button onClick={handleSubmit} loading={blockTask.isPending}>\n              {m.common_submit()}\n            </Button>\n\n            <Button onClick={() => handleToggle(false)}>\n              {m.common_cancel()}\n            </Button>\n          </CardFooter>\n        </Card>\n      </ModalContent>\n    </Modal>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/profiles/$type/_modules/profiles-header.tsx",
    "content": "import ArrowBackIosNewRounded from '~icons/material-symbols/arrow-back-ios-new-rounded'\nimport { Button } from '@/components/ui/button'\nimport useIsMobile from '@/hooks/use-is-moblie'\nimport { m } from '@/paraglide/messages'\nimport { cn } from '@nyanpasu/ui'\nimport { Link } from '@tanstack/react-router'\nimport { ProfileType } from '../../_modules/consts'\nimport ProfileQuickImport from '../../_modules/profile-quick-import'\nimport { Route as IndexRoute } from '../index'\n\nconst BackButton = () => {\n  return (\n    <Button icon className=\"flex items-center justify-center md:hidden\" asChild>\n      <Link to=\"/main/profiles\">\n        <ArrowBackIosNewRounded className=\"size-4\" />\n      </Link>\n    </Button>\n  )\n}\n\nexport default function ProfilesHeader() {\n  const { type } = IndexRoute.useParams()\n\n  const isMobile = useIsMobile()\n\n  const isProfileType = type === ProfileType.Profile\n\n  const messages = {\n    [ProfileType.Profile]: m.profile_profile_label(),\n    [ProfileType.JavaScript]: m.profile_javascript_label(),\n    [ProfileType.Lua]: m.profile_lua_label(),\n    [ProfileType.Merge]: m.profile_merge_label(),\n  } satisfies Record<ProfileType, string>\n\n  return (\n    <div\n      className={cn(\n        'flex items-center gap-2 p-4',\n        'sticky top-0 z-50',\n        'bg-mixed-background',\n      )}\n    >\n      {isMobile && <BackButton />}\n\n      {isProfileType ? (\n        <ProfileQuickImport />\n      ) : (\n        <p className=\"text-lg font-bold\">{messages[type as ProfileType]}</p>\n      )}\n    </div>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/profiles/$type/_modules/profiles-list.tsx",
    "content": "import DeleteForeverOutlineRounded from '~icons/material-symbols/delete-forever-outline-rounded'\nimport DragClickRounded from '~icons/material-symbols/drag-click-rounded'\nimport { AnimatePresence, motion } from 'framer-motion'\nimport { isEqual } from 'lodash-es'\nimport { ComponentProps, useRef } from 'react'\nimport {\n  RegisterContextMenu,\n  RegisterContextMenuContent,\n  RegisterContextMenuTrigger,\n} from '@/components/providers/context-menu-provider'\nimport { useExperimentalThemeContext } from '@/components/providers/theme-provider'\nimport { Button } from '@/components/ui/button'\nimport { Card, CardContent, CardFooter, CardHeader } from '@/components/ui/card'\nimport { ContextMenuItem } from '@/components/ui/context-menu'\nimport { LinearProgress } from '@/components/ui/progress'\nimport TextMarquee from '@/components/ui/text-marquee'\nimport { m } from '@/paraglide/messages'\nimport { move } from '@dnd-kit/helpers'\nimport { DragDropProvider } from '@dnd-kit/react'\nimport { useSortable } from '@dnd-kit/react/sortable'\nimport { hexFromArgb } from '@material/material-color-utilities'\nimport { Profile, useProfile } from '@nyanpasu/interface'\nimport { cn } from '@nyanpasu/ui'\nimport { MeshGradient } from '@paper-design/shaders-react'\nimport { Link } from '@tanstack/react-router'\nimport { useActiveProfile } from '../detail/_modules/active-button'\nimport { useDeleteProfile } from '../detail/_modules/delete-profile'\nimport { Route as IndexRoute } from '../index'\nimport { categoryProfiles, isProxyProfile } from './utils'\n\nconst Chip = ({ children, className, ...props }: ComponentProps<'span'>) => {\n  return (\n    <span\n      className={cn(\n        'bg-primary-container rounded-full px-3 py-1 text-xs font-bold whitespace-nowrap',\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </span>\n  )\n}\n\nconst GridViewProfile = ({\n  profile,\n  index,\n}: {\n  profile: Profile\n  index: number\n}) => {\n  const { type } = IndexRoute.useParams()\n\n  const activeProfile = useActiveProfile(profile)\n  const deleteProfile = useDeleteProfile(profile)\n\n  const isPending = activeProfile.isPending || deleteProfile.isPending\n\n  const isRemote = profile.type === 'remote'\n\n  const { themePalette } = useExperimentalThemeContext()\n\n  const cardRef = useRef<HTMLDivElement>(null)\n\n  const { isDragging: _isDragging } = useSortable({\n    id: profile.uid,\n    index,\n    element: cardRef.current,\n  })\n\n  return (\n    <RegisterContextMenu>\n      <RegisterContextMenuTrigger asChild>\n        <Card\n          data-slot=\"profile-card\"\n          className=\"relative flex flex-col justify-between\"\n          asChild\n        >\n          <div ref={cardRef}>\n            <AnimatePresence initial={false}>\n              {isPending && (\n                <motion.div\n                  data-slot=\"profile-card-mask\"\n                  initial={{ opacity: 0 }}\n                  animate={{ opacity: 1 }}\n                  exit={{ opacity: 0 }}\n                  className={cn(\n                    'bg-primary/10 absolute inset-0 z-50 backdrop-blur-3xl',\n                    'flex flex-col items-center justify-center gap-2',\n                  )}\n                >\n                  <LinearProgress className=\"w-2/3 max-w-60\" indeterminate />\n\n                  <p className=\"text-on-surface-variant text-xs\">\n                    {m.profile_pending_mask_message()}\n                  </p>\n                </motion.div>\n              )}\n            </AnimatePresence>\n\n            {activeProfile.isActive && (\n              <MeshGradient\n                className=\"absolute inset-0 size-full opacity-30\"\n                colors={Object.values(themePalette.schemes.light).map((color) =>\n                  hexFromArgb(color),\n                )}\n                distortion={0.5}\n                swirl={0.1}\n                grainMixer={0}\n                grainOverlay={0}\n                speed={1 / 3}\n              />\n            )}\n\n            <CardHeader\n              className=\"flex items-center justify-between gap-2\"\n              data-slot=\"profile-card-title\"\n            >\n              <TextMarquee className=\"z-10 min-w-0 flex-1\">\n                {profile.name}\n              </TextMarquee>\n\n              {activeProfile.isActive && (\n                <Chip className=\"shrink-0\">{m.profile_is_active_label()}</Chip>\n              )}\n            </CardHeader>\n\n            <CardContent>\n              <div className=\"z-10\" data-slot=\"profile-card-type\">\n                {isRemote ? (\n                  <Chip>{m.profile_remote_label()}</Chip>\n                ) : (\n                  <Chip>{m.profile_local_label()}</Chip>\n                )}\n              </div>\n            </CardContent>\n\n            <CardFooter>\n              <Button className=\"flex items-center justify-center\" asChild>\n                <Link\n                  to=\"/main/profiles/$type/detail/$uid\"\n                  params={{\n                    type,\n                    uid: profile.uid,\n                  }}\n                >\n                  {m.profile_view_details_title()}\n                </Link>\n              </Button>\n            </CardFooter>\n          </div>\n        </Card>\n      </RegisterContextMenuTrigger>\n\n      <RegisterContextMenuContent>\n        {isProxyProfile(profile) && (\n          <ContextMenuItem\n            disabled={isPending}\n            onClick={activeProfile.handleClick}\n          >\n            <DragClickRounded className=\"size-4\" />\n            <span>{m.profile_active_title()}</span>\n          </ContextMenuItem>\n        )}\n\n        <ContextMenuItem\n          disabled={isPending}\n          onClick={deleteProfile.handleClick}\n        >\n          <DeleteForeverOutlineRounded className=\"size-4\" />\n          <span>{m.profile_delete_title()}</span>\n        </ContextMenuItem>\n      </RegisterContextMenuContent>\n    </RegisterContextMenu>\n  )\n}\n\nconst EmptyList = () => {\n  return (\n    <div\n      className={cn(\n        'mb-4 flex h-16 items-center justify-center text-center text-sm',\n        'text-on-surface-variant',\n        'dark:text-on-surface-variant-dark',\n        'min-h-[calc(100vh-40px-64px-80px)]',\n      )}\n    >\n      {m.profile_empty_list_message()}\n    </div>\n  )\n}\n\nconst NoMoreProfiles = () => {\n  return (\n    <div className=\"mb-4 flex h-16 items-center justify-center text-center text-sm text-gray-500\">\n      {m.profile_no_more_profiles()}\n    </div>\n  )\n}\n\nexport default function ProfilesList({\n  className,\n  ...props\n}: Omit<ComponentProps<'div'>, 'children'>) {\n  const { type } = IndexRoute.useParams()\n\n  const {\n    query: { data: profiles },\n    sort,\n  } = useProfile()\n\n  // Filter by allowed types, fallback to no filtering if not found\n  const categorizedProfiles = profiles?.items\n    ? categoryProfiles(profiles.items)\n    : null\n\n  // If no profiles are found, show the empty list message\n  if (\n    !categorizedProfiles?.profile ||\n    categorizedProfiles.profile.length === 0\n  ) {\n    return <EmptyList />\n  }\n\n  const filteredProfiles =\n    categorizedProfiles[type as keyof typeof categorizedProfiles]\n\n  return (\n    <>\n      <div\n        className={cn(\n          'flex flex-col gap-4',\n          'min-h-[calc(100vh-40px-64px)]',\n          'sm:min-h-[calc(100vh-40px-48px)]',\n        )}\n      >\n        <DragDropProvider\n          onDragEnd={(event) => {\n            const currentUids = filteredProfiles.map((profile) => profile.uid)\n\n            const nextUids = move(currentUids, event)\n\n            if (isEqual(currentUids, nextUids)) {\n              return\n            }\n\n            sort.mutate(nextUids)\n          }}\n        >\n          <div\n            className={cn(\n              'grid gap-2',\n              'md:grid-cols-2',\n              'lg:grid-cols-3',\n              'dxl:grid-cols-4',\n              className,\n            )}\n            data-slot=\"profiles-navigate\"\n            {...props}\n          >\n            {filteredProfiles.map((profile, index) => (\n              <GridViewProfile\n                key={profile.uid}\n                profile={profile}\n                index={index}\n              />\n            ))}\n          </div>\n        </DragDropProvider>\n\n        <div className=\"flex-1\" />\n      </div>\n\n      <NoMoreProfiles />\n    </>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/profiles/$type/_modules/remote-profile-button.tsx",
    "content": "import { useLockFn } from 'ahooks'\nimport dayjs from 'dayjs'\nimport { AnimatePresence } from 'framer-motion'\nimport { PropsWithChildren, useState } from 'react'\nimport { Controller, useForm } from 'react-hook-form'\nimport z from 'zod'\nimport { useBlockTask } from '@/components/providers/block-task-provider'\nimport { Button } from '@/components/ui/button'\nimport { Card, CardContent, CardFooter, CardHeader } from '@/components/ui/card'\nimport { Input, NumericInput } from '@/components/ui/input'\nimport {\n  Modal,\n  ModalContent,\n  ModalTitle,\n  ModalTrigger,\n} from '@/components/ui/modal'\nimport { ScrollArea } from '@/components/ui/scroll-area'\nimport { SwitchItem } from '@/components/ui/switch'\nimport { m } from '@/paraglide/messages'\nimport { formatError } from '@/utils'\nimport { message } from '@/utils/notification'\nimport { zodResolver } from '@hookform/resolvers/zod'\nimport { RemoteProfileBuilder, useProfile } from '@nyanpasu/interface'\nimport AnimatedErrorItem from '../../_modules/error-item'\n\nconst formSchema = z.object({\n  type: z.literal('remote'),\n  uid: z.string().nullable(),\n  name: z.string().nullable(),\n  file: z.string().nullable(),\n  desc: z.string().nullable(),\n  updated: z.number().nullable(),\n  url: z.httpUrl(),\n  extra: z\n    .object({\n      upload: z.number(),\n      download: z.number(),\n      total: z.number(),\n      expire: z.number(),\n    })\n    .nullable(),\n  option: z\n    .object({\n      user_agent: z.string().nullable(),\n      with_proxy: z.boolean(),\n      self_proxy: z.boolean(),\n      update_interval: z.number().nullable(),\n    })\n    .optional(),\n  chain: z.array(z.string()).nullable().optional(),\n}) satisfies z.ZodType<RemoteProfileBuilder>\n\nconst getDefaultValues = () => {\n  return {\n    type: 'remote',\n    uid: null,\n    name: `${m.profile_import_remote_title()} - ${dayjs().format('YYYY-MM-DD HH:mm:ss')}`,\n    file: null,\n    desc: null,\n    updated: null,\n    url: '',\n    extra: null,\n    option: {\n      with_proxy: false,\n      self_proxy: false,\n      update_interval: 1440,\n      user_agent: null,\n    },\n    chain: null,\n  } satisfies z.infer<typeof formSchema>\n}\n\nexport default function RemoteProfileButton({ children }: PropsWithChildren) {\n  const { create } = useProfile()\n\n  const [open, setOpen] = useState(false)\n\n  const form = useForm<z.infer<typeof formSchema>>({\n    resolver: zodResolver(formSchema),\n    defaultValues: getDefaultValues(),\n  })\n\n  const blockTask = useBlockTask(\n    `create-remote-profile`,\n    form.handleSubmit(async (data) => {\n      try {\n        await create.mutateAsync({\n          type: 'manual',\n          data: {\n            item: data,\n            fileData: null,\n          },\n        })\n\n        handleToggle(false)\n      } catch (error) {\n        message(`Create failed: \\n ${formatError(error)}`, {\n          title: 'Error',\n          kind: 'error',\n        })\n      }\n    }),\n  )\n\n  const handleToggle = (value: boolean) => {\n    if (blockTask.isPending) {\n      return\n    }\n\n    setOpen(value)\n\n    if (value) {\n      form.reset(getDefaultValues())\n    }\n  }\n\n  const handleSubmit = useLockFn(blockTask.execute)\n\n  return (\n    <Modal open={open} onOpenChange={handleToggle}>\n      <ModalTrigger asChild>{children}</ModalTrigger>\n\n      <ModalContent>\n        <Card className=\"w-96\">\n          <CardHeader>\n            <ModalTitle>{m.profile_import_remote_title()}</ModalTitle>\n          </CardHeader>\n\n          <CardContent asChild>\n            <ScrollArea className=\"max-h-[calc(100vh-200px)]\">\n              <div className=\"space-y-4 pt-2\">\n                <Controller\n                  control={form.control}\n                  name=\"name\"\n                  render={({ field }) => (\n                    <div className=\"space-y-2\">\n                      <Input\n                        variant=\"outlined\"\n                        label={m.profile_form_name_label()}\n                        {...field}\n                        value={field.value ?? ''}\n                      />\n\n                      <AnimatePresence>\n                        {form.formState.errors.name && (\n                          <AnimatedErrorItem className=\"text-error\">\n                            {form.formState.errors.name?.message}\n                          </AnimatedErrorItem>\n                        )}\n                      </AnimatePresence>\n                    </div>\n                  )}\n                />\n\n                <Controller\n                  control={form.control}\n                  name=\"desc\"\n                  render={({ field }) => (\n                    <div className=\"space-y-2\">\n                      <Input\n                        variant=\"outlined\"\n                        label={m.profile_form_desc_label()}\n                        {...field}\n                        value={field.value ?? ''}\n                      />\n\n                      <AnimatePresence>\n                        {form.formState.errors.desc && (\n                          <AnimatedErrorItem className=\"text-error\">\n                            {form.formState.errors.desc?.message}\n                          </AnimatedErrorItem>\n                        )}\n                      </AnimatePresence>\n                    </div>\n                  )}\n                />\n\n                <Controller\n                  control={form.control}\n                  name=\"url\"\n                  render={({ field }) => (\n                    <div className=\"space-y-2\">\n                      <Input\n                        variant=\"outlined\"\n                        label={m.profile_form_url_label()}\n                        {...field}\n                        value={field.value ?? ''}\n                      />\n\n                      <AnimatePresence>\n                        {form.formState.errors.url && (\n                          <AnimatedErrorItem className=\"text-error\">\n                            {form.formState.errors.url?.message}\n                          </AnimatedErrorItem>\n                        )}\n                      </AnimatePresence>\n                    </div>\n                  )}\n                />\n\n                <Controller\n                  control={form.control}\n                  name=\"option.user_agent\"\n                  render={({ field }) => (\n                    <div className=\"space-y-2\">\n                      <Input\n                        variant=\"outlined\"\n                        label={m.profile_form_option_user_agent_label()}\n                        {...field}\n                        value={field.value ?? ''}\n                      />\n\n                      <AnimatePresence>\n                        {form.formState.errors.option?.user_agent && (\n                          <AnimatedErrorItem className=\"text-error\">\n                            {form.formState.errors.option?.user_agent?.message}\n                          </AnimatedErrorItem>\n                        )}\n                      </AnimatePresence>\n                    </div>\n                  )}\n                />\n\n                <Controller\n                  control={form.control}\n                  name=\"option.update_interval\"\n                  render={({ field }) => (\n                    <div className=\"space-y-2\">\n                      <NumericInput\n                        variant=\"outlined\"\n                        label={m.profile_form_option_update_interval_label()}\n                        min={0}\n                        step={1}\n                        {...field}\n                      />\n\n                      <AnimatePresence>\n                        {form.formState.errors.option?.update_interval && (\n                          <AnimatedErrorItem className=\"text-error\">\n                            {\n                              form.formState.errors.option?.update_interval\n                                .message\n                            }\n                          </AnimatedErrorItem>\n                        )}\n                      </AnimatePresence>\n                    </div>\n                  )}\n                />\n\n                <Controller\n                  control={form.control}\n                  name=\"option.with_proxy\"\n                  render={({ field }) => (\n                    <SwitchItem\n                      checked={field.value}\n                      onCheckedChange={(checked) => field.onChange(checked)}\n                    >\n                      <span>{m.profile_with_proxy_label()}</span>\n                    </SwitchItem>\n                  )}\n                />\n\n                <Controller\n                  control={form.control}\n                  name=\"option.self_proxy\"\n                  render={({ field }) => (\n                    <SwitchItem\n                      checked={field.value}\n                      onCheckedChange={(checked) => field.onChange(checked)}\n                    >\n                      <span>{m.profile_self_proxy_label()}</span>\n                    </SwitchItem>\n                  )}\n                />\n              </div>\n            </ScrollArea>\n          </CardContent>\n\n          <CardFooter className=\"gap-1\">\n            <Button onClick={handleSubmit} loading={blockTask.isPending}>\n              {m.common_submit()}\n            </Button>\n\n            <Button onClick={() => handleToggle(false)}>\n              {m.common_cancel()}\n            </Button>\n          </CardFooter>\n        </Card>\n      </ModalContent>\n    </Modal>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/profiles/$type/_modules/utils.ts",
    "content": "import { findKey } from 'lodash-es'\nimport { Profile } from '@nyanpasu/interface'\nimport { PROFILE_TYPES, ProfileType } from '../../_modules/consts'\n\nexport type CategoryProfiles = {\n  [ProfileType.Profile]: Array<Extract<Profile, { type: 'local' | 'remote' }>>\n  [ProfileType.JavaScript]: Array<\n    Extract<Profile, { type: 'script'; script_type: 'javascript' }>\n  >\n  [ProfileType.Lua]: Array<\n    Extract<Profile, { type: 'script'; script_type: 'lua' }>\n  >\n  [ProfileType.Merge]: Array<Extract<Profile, { type: 'merge' }>>\n}\n\nexport const isProxyProfile = (\n  profile: Profile,\n): profile is CategoryProfiles[ProfileType.Profile][number] =>\n  profile.type === 'local' || profile.type === 'remote'\n\nexport const isJavaScriptProfile = (\n  profile: Profile,\n): profile is CategoryProfiles[ProfileType.JavaScript][number] =>\n  profile.type === 'script' && profile.script_type === 'javascript'\n\nexport const isLuaProfile = (\n  profile: Profile,\n): profile is CategoryProfiles[ProfileType.Lua][number] =>\n  profile.type === 'script' && profile.script_type === 'lua'\n\nexport const isMergeProfile = (\n  profile: Profile,\n): profile is CategoryProfiles[ProfileType.Merge][number] =>\n  profile.type === 'merge'\n\nexport const categoryProfiles = (profiles: Profile[]): CategoryProfiles => {\n  const initialCategorized: CategoryProfiles = {\n    [ProfileType.Profile]: [],\n    [ProfileType.JavaScript]: [],\n    [ProfileType.Lua]: [],\n    [ProfileType.Merge]: [],\n  }\n\n  return profiles.reduce((categorized, profile) => {\n    const matchedProfileType = findKey(PROFILE_TYPES, (allowedTypes) =>\n      allowedTypes.some((allowedType) => {\n        if (allowedType.type !== profile.type) {\n          return false\n        }\n\n        if (\n          'script_type' in allowedType &&\n          allowedType.script_type !== undefined\n        ) {\n          return (\n            profile.type === 'script' &&\n            profile.script_type === allowedType.script_type\n          )\n        }\n\n        return true\n      }),\n    ) as ProfileType | undefined\n\n    if (!matchedProfileType) {\n      return categorized\n    }\n\n    switch (matchedProfileType) {\n      case ProfileType.Profile:\n        if (isProxyProfile(profile)) {\n          categorized[ProfileType.Profile].push(profile)\n        }\n        break\n\n      case ProfileType.JavaScript:\n        if (isJavaScriptProfile(profile)) {\n          categorized[ProfileType.JavaScript].push(profile)\n        }\n        break\n\n      case ProfileType.Lua:\n        if (isLuaProfile(profile)) {\n          categorized[ProfileType.Lua].push(profile)\n        }\n        break\n\n      case ProfileType.Merge:\n        if (isMergeProfile(profile)) {\n          categorized[ProfileType.Merge].push(profile)\n        }\n        break\n    }\n\n    return categorized\n  }, initialCategorized)\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/profiles/$type/detail/$uid.tsx",
    "content": "import EditSquareOutlineRounded from '~icons/material-symbols/edit-square-outline-rounded'\nimport { Button } from '@/components/ui/button'\nimport TextMarquee from '@/components/ui/text-marquee'\nimport { useProfile } from '@nyanpasu/interface'\nimport { createFileRoute } from '@tanstack/react-router'\nimport { isProxyProfile } from '../_modules/utils'\nimport ActionCard from './_modules/action-card'\nimport ChianEditorCard from './_modules/chian-editor-card'\nimport DetialHeader from './_modules/detial-header'\nimport ProfileNameEditor from './_modules/profile-name-editor'\nimport { SubscriptionCard } from './_modules/subscription-card'\n\nexport const Route = createFileRoute('/(main)/main/profiles/$type/detail/$uid')(\n  {\n    component: RouteComponent,\n  },\n)\n\nfunction RouteComponent() {\n  const { uid } = Route.useParams()\n\n  const { query } = useProfile()\n\n  const currentProfile = query.data?.items?.find((item) => item.uid === uid)\n\n  // TODO: better error handling\n  if (!currentProfile) {\n    return null\n  }\n\n  const isRemoteProfile = currentProfile.type === 'remote'\n\n  return (\n    <>\n      <DetialHeader>\n        <TextMarquee className=\"w-0 min-w-0 flex-1 text-lg font-bold\">\n          {currentProfile.name}\n        </TextMarquee>\n\n        <ProfileNameEditor profile={currentProfile} asChild>\n          <Button icon className=\"shrink-0\">\n            <EditSquareOutlineRounded className=\"size-4\" />\n          </Button>\n        </ProfileNameEditor>\n      </DetialHeader>\n\n      <div className=\"grid grid-cols-2 gap-4 p-4 md:grid-cols-4\">\n        {isRemoteProfile && <SubscriptionCard profile={currentProfile} />}\n\n        <ActionCard profile={currentProfile} />\n\n        {isProxyProfile(currentProfile) && (\n          <ChianEditorCard profile={currentProfile} />\n        )}\n      </div>\n    </>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/profiles/$type/detail/_modules/action-card.tsx",
    "content": "import DeleteForeverOutlineRounded from '~icons/material-symbols/delete-forever-outline-rounded'\nimport DragClickRounded from '~icons/material-symbols/drag-click-rounded'\nimport EditSquareOutlineRounded from '~icons/material-symbols/edit-square-outline-rounded'\nimport FileOpenOutlineRounded from '~icons/material-symbols/file-open-outline-rounded'\nimport { ComponentProps } from 'react'\nimport { Button } from '@/components/ui/button'\nimport { m } from '@/paraglide/messages'\nimport { Profile } from '@nyanpasu/interface'\nimport { cn } from '@nyanpasu/ui'\nimport ActiveButton from './active-button'\nimport DeleteProfile from './delete-profile'\nimport OpenLocally from './open-locally'\nimport ProfileNameEditor from './profile-name-editor'\nimport SubscriptionUrlEditor from './subscription-url-editor'\nimport ViewContent from './view-content'\n\nconst ActionCardButton = ({\n  className,\n  ...props\n}: ComponentProps<typeof Button>) => {\n  return (\n    <Button\n      variant=\"basic\"\n      className={cn(\n        'flex h-14 items-center gap-3 truncate rounded-2xl text-base font-semibold',\n        'bg-primary-container dark:bg-surface-variant/30',\n        className,\n      )}\n      {...props}\n    />\n  )\n}\n\nexport default function ActionCard({ profile }: { profile: Profile }) {\n  const isScript = !(profile.type === 'local' || profile.type === 'remote')\n\n  return (\n    <div className=\"col-span-2 grid grid-cols-2 gap-4\">\n      <ProfileNameEditor profile={profile} asChild>\n        <ActionCardButton>\n          <span className=\"size-4\">\n            <EditSquareOutlineRounded />\n          </span>\n\n          <span className=\"truncate\">{m.profile_name_editor_title()}</span>\n        </ActionCardButton>\n      </ProfileNameEditor>\n\n      {profile.type === 'remote' && (\n        <SubscriptionUrlEditor profile={profile} asChild>\n          <ActionCardButton>\n            <span className=\"size-4\">\n              <EditSquareOutlineRounded />\n            </span>\n\n            <span className=\"truncate\">\n              {m.profile_subscription_url_editor_label()}\n            </span>\n          </ActionCardButton>\n        </SubscriptionUrlEditor>\n      )}\n\n      {!isScript && (\n        <ActionCardButton asChild>\n          <ActiveButton profile={profile}>\n            <span className=\"size-4\">\n              <DragClickRounded />\n            </span>\n\n            <span className=\"truncate\">{m.profile_active_title()}</span>\n          </ActiveButton>\n        </ActionCardButton>\n      )}\n\n      <ActionCardButton asChild>\n        <DeleteProfile profile={profile}>\n          <span className=\"size-4\">\n            <DeleteForeverOutlineRounded />\n          </span>\n\n          <span className=\"truncate\">{m.profile_delete_title()}</span>\n        </DeleteProfile>\n      </ActionCardButton>\n\n      <ActionCardButton asChild>\n        <ViewContent profile={profile}>\n          <span className=\"size-4\">\n            <FileOpenOutlineRounded />\n          </span>\n\n          <span className=\"truncate\">{m.profile_view_content_title()}</span>\n        </ViewContent>\n      </ActionCardButton>\n\n      <ActionCardButton asChild>\n        <OpenLocally profile={profile}>\n          <span className=\"size-4\">\n            <FileOpenOutlineRounded />\n          </span>\n\n          <span className=\"truncate\">{m.profile_open_locally_title()}</span>\n        </OpenLocally>\n      </ActionCardButton>\n    </div>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/profiles/$type/detail/_modules/active-button.tsx",
    "content": "import { ComponentProps } from 'react'\nimport { useBlockTask } from '@/components/providers/block-task-provider'\nimport { Button } from '@/components/ui/button'\nimport { useLockFn } from '@/hooks/use-lock-fn'\nimport { m } from '@/paraglide/messages'\nimport { formatError } from '@/utils'\nimport { message } from '@/utils/notification'\nimport { Profile, useClashConnections, useProfile } from '@nyanpasu/interface'\nimport { cn } from '@nyanpasu/ui'\n\nexport const useActiveProfile = (profile: Profile) => {\n  const {\n    query: { data },\n    upsert,\n  } = useProfile()\n\n  const isActive = data?.current?.find((uid) => uid === profile.uid)\n\n  const { deleteConnections } = useClashConnections()\n\n  const blockTask = useBlockTask(`active-profile-${profile.uid}`, async () => {\n    try {\n      await upsert.mutateAsync({ current: [profile.uid] })\n\n      await deleteConnections.mutateAsync(null)\n\n      message(m.profile_active_title_success({ name: profile.name }), {\n        title: m.profile_active_title(),\n        kind: 'info',\n      })\n    } catch (err) {\n      // This FetchError was triggered by the `DELETE /connections` API\n      const isFetchError = err instanceof Error && err.name === 'FetchError'\n\n      message(\n        isFetchError\n          ? `Failed to delete connections: \\n ${formatError(err)}`\n          : `${m.profile_active_title_error({\n              name: profile.name,\n            })} \\n ${formatError(err)}`,\n        {\n          title: 'Error',\n          kind: isFetchError ? 'warning' : 'error',\n        },\n      )\n    }\n  })\n\n  const handleClick = useLockFn(async () => {\n    if (isActive) {\n      message(m.profile_is_active_description(), {\n        title: m.profile_active_title(),\n        kind: 'info',\n      })\n\n      return\n    }\n\n    await blockTask.execute()\n  })\n\n  return {\n    isActive,\n    handleClick,\n    isPending: blockTask.isPending,\n  }\n}\n\nexport default function ActiveButton({\n  profile,\n  className,\n  ...props\n}: Omit<ComponentProps<typeof Button>, 'loading' | 'onClick'> & {\n  profile: Profile\n}) {\n  const { isActive, handleClick, isPending } = useActiveProfile(profile)\n\n  return (\n    <Button\n      {...props}\n      className={cn(\n        'transition-colors',\n        className,\n        isActive && [\n          'bg-green-500/30 text-green-900 hover:bg-green-500/50',\n          'dark:bg-green-900/50 dark:text-green-600 dark:hover:bg-green-900/60',\n        ],\n      )}\n      onClick={handleClick}\n      loading={isPending}\n    />\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/profiles/$type/detail/_modules/chian-editor-card.tsx",
    "content": "import { AnimatePresence, motion } from 'framer-motion'\nimport { useEffect, useMemo, useState } from 'react'\nimport { useBlockTask } from '@/components/providers/block-task-provider'\nimport { AnimatedItem } from '@/components/ui/animated-item'\nimport { Button } from '@/components/ui/button'\nimport { Card, CardContent, CardFooter, CardHeader } from '@/components/ui/card'\nimport { CircularProgress } from '@/components/ui/progress'\nimport TextMarquee from '@/components/ui/text-marquee'\nimport { useLockFn } from '@/hooks/use-lock-fn'\nimport { m } from '@/paraglide/messages'\nimport { move } from '@dnd-kit/helpers'\nimport { DragDropProvider, useDroppable } from '@dnd-kit/react'\nimport { useSortable } from '@dnd-kit/react/sortable'\nimport {\n  LocalProfile,\n  ProfileBuilder,\n  RemoteProfile,\n  useProfile,\n} from '@nyanpasu/interface'\nimport { cn } from '@nyanpasu/ui'\nimport { categoryProfiles, CategoryProfiles } from '../../_modules/utils'\nimport { ProfileType } from '../../../_modules/consts'\n\ntype ScriptOrMergeProfile = CategoryProfiles[\n  | ProfileType.JavaScript\n  | ProfileType.Lua\n  | ProfileType.Merge][number]\n\nenum ColumnType {\n  Active = 'active',\n  Inactive = 'inactive',\n}\n\nconst COLUMN_TYPES = [ColumnType.Active, ColumnType.Inactive] as const\n\nconst CHAIN_EDITOR_SORTABLE_GROUP = 'chain-editor-sortable'\n\nconst Item = ({\n  profile,\n  index,\n}: {\n  profile: ScriptOrMergeProfile\n  index: number\n}) => {\n  const { ref } = useSortable({\n    id: profile.uid,\n    index,\n    group: CHAIN_EDITOR_SORTABLE_GROUP,\n    type: 'item',\n    accept: ['item'],\n  })\n\n  return (\n    <Button\n      className=\"bg-secondary-container/30 h-14 w-full rounded-2xl text-left\"\n      variant=\"raised\"\n      asChild\n    >\n      <button ref={ref}>\n        <TextMarquee className=\"pointer-events-none\">\n          {profile.name}\n        </TextMarquee>\n      </button>\n    </Button>\n  )\n}\n\nconst Column = ({\n  profiles,\n  type,\n}: {\n  profiles: ScriptOrMergeProfile[]\n  type: ColumnType\n}) => {\n  const { ref } = useDroppable({\n    id: type,\n    type: 'column',\n    accept: ['item'],\n  })\n\n  const message = {\n    [ColumnType.Active]: m.profile_chain_editor_active_column(),\n    [ColumnType.Inactive]: m.profile_chain_editor_inactive_column(),\n  }\n\n  return (\n    <Card variant=\"outline\" asChild>\n      <div ref={ref}>\n        <CardHeader>{message[type]}</CardHeader>\n\n        <CardContent>\n          {profiles.map((profile, index) => (\n            <Item key={profile.uid} profile={profile} index={index} />\n          ))}\n        </CardContent>\n      </div>\n    </Card>\n  )\n}\n\nexport default function ChianEditorCard({\n  profile,\n}: {\n  profile: LocalProfile | RemoteProfile\n}) {\n  const {\n    query: { data: profiles },\n    patch,\n  } = useProfile()\n\n  const categorizedProfiles = useMemo(() => {\n    if (!profiles?.items) {\n      return null\n    }\n\n    return categoryProfiles(profiles.items)\n  }, [profiles?.items])\n\n  const scriptProfiles = useMemo<ScriptOrMergeProfile[]>(() => {\n    if (!categorizedProfiles) {\n      return []\n    }\n\n    return [\n      ...categorizedProfiles[ProfileType.JavaScript],\n      ...categorizedProfiles[ProfileType.Lua],\n      ...categorizedProfiles[ProfileType.Merge],\n    ]\n  }, [categorizedProfiles])\n\n  const scriptProfileUids = useMemo(() => {\n    return scriptProfiles.map((item) => item.uid)\n  }, [scriptProfiles])\n\n  const [chainsUids, setChainsUids] = useState<Record<ColumnType, string[]>>({\n    [ColumnType.Active]: [],\n    [ColumnType.Inactive]: [],\n  })\n\n  const hasSameOrder = (left: string[], right: string[]) => {\n    if (left.length !== right.length) {\n      return false\n    }\n\n    return left.every((item, index) => item === right[index])\n  }\n\n  const setChainsUidsIfChanged = (\n    updater: (\n      prev: Record<ColumnType, string[]>,\n    ) => Record<ColumnType, string[]>,\n  ) => {\n    setChainsUids((prev) => {\n      const next = updater(prev)\n      const unchanged = COLUMN_TYPES.every((type) =>\n        hasSameOrder(prev[type], next[type]),\n      )\n\n      return unchanged ? prev : next\n    })\n  }\n\n  const chains = useMemo(() => {\n    const active = chainsUids[ColumnType.Active]\n      .map((uid) => scriptProfiles.find((item) => item.uid === uid))\n      .filter((item): item is ScriptOrMergeProfile => Boolean(item))\n\n    const inactive = chainsUids[ColumnType.Inactive]\n      .map((uid) => scriptProfiles.find((item) => item.uid === uid))\n      .filter((item): item is ScriptOrMergeProfile => Boolean(item))\n\n    return {\n      [ColumnType.Active]: active,\n      [ColumnType.Inactive]: inactive,\n    }\n  }, [chainsUids, scriptProfiles])\n\n  // sync chains with profile.chain and scriptProfiles\n  useEffect(() => {\n    const activeSet = new Set(profile.chain ?? [])\n    const nextActive = scriptProfileUids.filter((uid) => activeSet.has(uid))\n    const nextInactive = scriptProfileUids.filter((uid) => !activeSet.has(uid))\n\n    setChainsUids((prev) => {\n      const activeUnchanged = hasSameOrder(prev[ColumnType.Active], nextActive)\n\n      const inactiveUnchanged = hasSameOrder(\n        prev[ColumnType.Inactive],\n        nextInactive,\n      )\n\n      if (activeUnchanged && inactiveUnchanged) {\n        return prev\n      }\n\n      return {\n        [ColumnType.Active]: nextActive,\n        [ColumnType.Inactive]: nextInactive,\n      }\n    })\n  }, [scriptProfileUids, profile.chain])\n\n  const isChanged = useMemo(() => {\n    const activeSet = new Set(profile.chain ?? [])\n    const baselineActive = scriptProfileUids.filter((uid) => activeSet.has(uid))\n    const baselineInactive = scriptProfileUids.filter(\n      (uid) => !activeSet.has(uid),\n    )\n\n    return (\n      !hasSameOrder(chainsUids[ColumnType.Active], baselineActive) ||\n      !hasSameOrder(chainsUids[ColumnType.Inactive], baselineInactive)\n    )\n  }, [chainsUids, profile.chain, scriptProfileUids])\n\n  const blockTask = useBlockTask(`update-chain-${profile.uid}`, async () => {\n    try {\n      await patch.mutateAsync({\n        uid: profile.uid,\n        profile: {\n          ...(profile as ProfileBuilder),\n          chain: chainsUids[ColumnType.Active],\n        } as ProfileBuilder,\n      })\n    } catch {\n      //\n    }\n  })\n\n  const handleApply = useLockFn(blockTask.execute)\n\n  const loadingMessage = m.profile_chain_editor_apply_message()\n\n  return (\n    <Card className=\"relative col-span-2 md:col-span-4\">\n      <AnimatePresence initial={false}>\n        {blockTask.isPending && (\n          <motion.div\n            data-slot=\"core-manager-card-mask\"\n            initial={{ opacity: 0 }}\n            animate={{ opacity: 1 }}\n            exit={{ opacity: 0 }}\n            className={cn(\n              'bg-primary/10 absolute inset-0 z-50 backdrop-blur-3xl',\n              'flex flex-col items-center justify-center gap-4',\n            )}\n          >\n            <CircularProgress className=\"size-12\" indeterminate />\n\n            <p>{loadingMessage}</p>\n          </motion.div>\n        )}\n      </AnimatePresence>\n\n      <CardContent className=\"grid sm:grid-cols-2\">\n        <DragDropProvider\n          onDragEnd={(event) => {\n            setChainsUidsIfChanged((prev) => move(prev, event))\n          }}\n        >\n          <Column profiles={chains.active} type={ColumnType.Active} />\n\n          <Column profiles={chains.inactive} type={ColumnType.Inactive} />\n        </DragDropProvider>\n      </CardContent>\n\n      <AnimatePresence>\n        {isChanged && (\n          <AnimatedItem>\n            <CardFooter className=\"gap-1\">\n              <Button className=\"flex items-center gap-2\" onClick={handleApply}>\n                {m.common_apply()}\n              </Button>\n            </CardFooter>\n          </AnimatedItem>\n        )}\n      </AnimatePresence>\n    </Card>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/profiles/$type/detail/_modules/delete-profile.tsx",
    "content": "import { ComponentProps } from 'react'\nimport { useBlockTask } from '@/components/providers/block-task-provider'\nimport { Button } from '@/components/ui/button'\nimport { useLockFn } from '@/hooks/use-lock-fn'\nimport { m } from '@/paraglide/messages'\nimport { formatError } from '@/utils'\nimport { message } from '@/utils/notification'\nimport { Profile, useProfile } from '@nyanpasu/interface'\nimport { useNavigate } from '@tanstack/react-router'\nimport { ask } from '@tauri-apps/plugin-dialog'\nimport { Route as IndexRoute } from '../$uid'\n\nexport const useDeleteProfile = (\n  profile: Profile,\n  options?: {\n    onSuccess?: () => void | Promise<void>\n  },\n) => {\n  const { drop } = useProfile()\n\n  const blockTask = useBlockTask(`delete-profile-${profile.uid}`, async () => {\n    try {\n      await drop.mutateAsync(profile.uid)\n      await options?.onSuccess?.()\n    } catch (error) {\n      message(`Delete failed: \\n ${formatError(error)}`, {\n        title: 'Error',\n        kind: 'error',\n      })\n    }\n  })\n\n  const handleClick = useLockFn(async () => {\n    const answer = await ask(m.profile_delete_description(), {\n      title: m.profile_delete_title(),\n      kind: 'warning',\n    })\n\n    // user cancelled the deletion\n    if (!answer) {\n      return\n    }\n\n    await blockTask.execute()\n  })\n\n  return {\n    handleClick,\n    isPending: blockTask.isPending,\n  }\n}\n\nexport default function DeleteProfile({\n  profile,\n  ...props\n}: Omit<ComponentProps<typeof Button>, 'loading' | 'onClick'> & {\n  profile: Profile\n}) {\n  const { type } = IndexRoute.useParams()\n\n  const navigate = useNavigate()\n\n  const { handleClick, isPending } = useDeleteProfile(profile, {\n    onSuccess: async () => {\n      await navigate({\n        to: `/main/profiles/$type`,\n        params: { type },\n      })\n    },\n  })\n\n  return <Button {...props} onClick={handleClick} loading={isPending} />\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/profiles/$type/detail/_modules/detial-header.tsx",
    "content": "import ArrowBackIosNewRounded from '~icons/material-symbols/arrow-back-ios-new-rounded'\nimport { ComponentProps } from 'react'\nimport { Button } from '@/components/ui/button'\nimport { cn } from '@nyanpasu/ui'\nimport { Link } from '@tanstack/react-router'\nimport { Route as IndexRoute } from '../$uid'\n\nconst BackButton = () => {\n  const { type } = IndexRoute.useParams()\n\n  return (\n    <Button icon className=\"flex items-center justify-center\" asChild>\n      <Link\n        to=\"/main/profiles/$type\"\n        params={{\n          type,\n        }}\n      >\n        <ArrowBackIosNewRounded className=\"size-4\" />\n      </Link>\n    </Button>\n  )\n}\n\nexport default function DetialHeader({\n  children,\n  className,\n  ...props\n}: ComponentProps<'div'>) {\n  return (\n    <div\n      className={cn(\n        'sticky top-0 z-10',\n        'bg-mixed-background',\n        'flex items-center gap-4',\n        'h-16 px-4',\n        className,\n      )}\n      {...props}\n    >\n      <BackButton />\n\n      {children}\n    </div>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/profiles/$type/detail/_modules/open-locally.tsx",
    "content": "import { ComponentProps } from 'react'\nimport { Button } from '@/components/ui/button'\nimport { useLockFn } from '@/hooks/use-lock-fn'\nimport { commands, Profile, unwrapResult } from '@nyanpasu/interface'\n\nexport default function OpenLocally({\n  profile,\n  ...props\n}: Omit<ComponentProps<typeof Button>, 'onClick'> & {\n  profile: Profile\n}) {\n  const handleClick = useLockFn(async () => {\n    unwrapResult(await commands.viewProfile(profile.uid))\n  })\n\n  return <Button {...props} onClick={handleClick} />\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/profiles/$type/detail/_modules/profile-name-editor.tsx",
    "content": "import { AnimatePresence } from 'framer-motion'\nimport { ComponentProps, useState } from 'react'\nimport { Controller, useForm } from 'react-hook-form'\nimport { z } from 'zod'\nimport { useBlockTask } from '@/components/providers/block-task-provider'\nimport { Button } from '@/components/ui/button'\nimport { Card, CardContent, CardFooter, CardHeader } from '@/components/ui/card'\nimport { Input } from '@/components/ui/input'\nimport {\n  Modal,\n  ModalContent,\n  ModalTitle,\n  ModalTrigger,\n} from '@/components/ui/modal'\nimport { useLockFn } from '@/hooks/use-lock-fn'\nimport { m } from '@/paraglide/messages'\nimport { formatError } from '@/utils'\nimport { message } from '@/utils/notification'\nimport { zodResolver } from '@hookform/resolvers/zod'\nimport { Profile, ProfileBuilder, useProfile } from '@nyanpasu/interface'\nimport AnimatedErrorItem from '../../../_modules/error-item'\n\nconst formSchema = z.object({\n  name: z.string().min(1),\n})\n\nexport default function ProfileNameEditor({\n  profile,\n  ...props\n}: ComponentProps<typeof ModalTrigger> & {\n  profile: Profile\n}) {\n  const { patch } = useProfile()\n\n  const [open, setOpen] = useState(false)\n\n  const form = useForm<z.infer<typeof formSchema>>({\n    resolver: zodResolver(formSchema),\n    defaultValues: {\n      name: profile.name,\n    },\n  })\n\n  const handleClose = () => {\n    setOpen(false)\n    // get latest name\n    form.reset({\n      name: profile.name,\n    })\n  }\n\n  const blockTask = useBlockTask(\n    `update-profile-name-${profile.uid}`,\n    form.handleSubmit(\n      async ({ name }) => {\n        try {\n          await patch.mutateAsync({\n            uid: profile.uid,\n            profile: {\n              ...profile,\n              name,\n            } as ProfileBuilder,\n          })\n\n          handleClose()\n        } catch (error) {\n          message(`Update failed: \\n ${formatError(error)}`, {\n            title: 'Error',\n            kind: 'error',\n          })\n        }\n      },\n      (error) => {\n        console.error(error)\n        message(formatError(error.name?.message ?? ''), {\n          title: 'Error',\n          kind: 'error',\n        })\n      },\n    ),\n  )\n\n  const handleSubmit = useLockFn(blockTask.execute)\n\n  return (\n    <Modal open={open} onOpenChange={setOpen}>\n      <ModalTrigger {...props} />\n\n      <ModalContent>\n        <Card className=\"w-96\">\n          <CardHeader>\n            <ModalTitle>{m.profile_name_editor_title()}</ModalTitle>\n          </CardHeader>\n\n          <CardContent>\n            <Controller\n              control={form.control}\n              name=\"name\"\n              render={({ field }) => (\n                <div className=\"space-y-2\">\n                  <Input\n                    label={m.profile_name_label()}\n                    variant=\"outlined\"\n                    {...field}\n                  />\n\n                  <AnimatePresence>\n                    {form.formState.errors.name && (\n                      <AnimatedErrorItem className=\"text-error\">\n                        {form.formState.errors.name?.message}\n                      </AnimatedErrorItem>\n                    )}\n                  </AnimatePresence>\n                </div>\n              )}\n            />\n          </CardContent>\n\n          <CardFooter className=\"gap-1\">\n            <Button onClick={handleSubmit} loading={blockTask.isPending}>\n              {m.common_save()}\n            </Button>\n\n            <Button onClick={handleClose}>{m.common_cancel()}</Button>\n          </CardFooter>\n        </Card>\n      </ModalContent>\n    </Modal>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/profiles/$type/detail/_modules/subscription-card.tsx",
    "content": "import RefreshRounded from '~icons/material-symbols/refresh-rounded'\nimport RuleSettingsRounded from '~icons/material-symbols/rule-settings-rounded'\nimport dayjs from 'dayjs'\nimport { filesize } from 'filesize'\nimport { useMemo } from 'react'\nimport { useBlockTask } from '@/components/providers/block-task-provider'\nimport { Button } from '@/components/ui/button'\nimport { Card, CardContent, CardFooter, CardHeader } from '@/components/ui/card'\nimport { LinearProgress } from '@/components/ui/progress'\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipTrigger,\n} from '@/components/ui/tooltip'\nimport { useLockFn } from '@/hooks/use-lock-fn'\nimport { m } from '@/paraglide/messages'\nimport { formatError } from '@/utils'\nimport { message } from '@/utils/notification'\nimport {\n  RemoteProfile,\n  RemoteProfileOptionsBuilder,\n  useProfile,\n} from '@nyanpasu/interface'\nimport UpdateOptionEditor from './update-option-editor'\n\nexport const SubscriptionCard = ({ profile }: { profile: RemoteProfile }) => {\n  const { update } = useProfile()\n\n  const { progress, total, used } = useMemo(() => {\n    let progress = 0\n    let total = 0\n    let used = 0\n\n    if (\n      profile !== undefined &&\n      'extra' in profile &&\n      profile.extra !== undefined\n    ) {\n      const { download, upload, total: t } = profile.extra\n\n      total = t\n\n      used = download + upload\n\n      progress = (used / (total || 1)) * 100\n    }\n\n    return { progress, total, used }\n  }, [profile])\n\n  const blockTask = useBlockTask(\n    `update-remote-profile-${profile.uid}`,\n    async () => {\n      // TODO: define backend serde(option) to move null\n      const selfOption = 'option' in profile ? profile.option : undefined\n\n      const options: RemoteProfileOptionsBuilder = {\n        with_proxy: false,\n        self_proxy: false,\n        update_interval: 0,\n        user_agent: null,\n        ...selfOption,\n      }\n\n      // if (proxy) {\n      //   if (selfOption?.self_proxy) {\n      //     options.with_proxy = false\n      //     options.self_proxy = true\n      //   } else {\n      //     options.with_proxy = true\n      //     options.self_proxy = false\n      //   }\n      // }\n\n      try {\n        await update.mutateAsync({\n          uid: profile.uid,\n          option: options,\n        })\n      } catch (e) {\n        message(`Update failed: \\n ${formatError(e)}`, {\n          title: 'Error',\n          kind: 'error',\n        })\n      }\n    },\n  )\n\n  const handleRefreshClick = useLockFn(async () => {\n    await blockTask.execute()\n  })\n\n  return (\n    <Card className=\"col-span-2\">\n      <CardHeader>{m.profile_subscription_title()}</CardHeader>\n\n      <CardContent>\n        <div className=\"flex items-center justify-between\">\n          <div className=\"text-sm font-bold\">{progress.toFixed(2)}%</div>\n\n          <div className=\"text-sm font-bold\">\n            {filesize(used)} / {filesize(total)}\n          </div>\n        </div>\n\n        <LinearProgress value={progress} />\n\n        <div className=\"flex items-center justify-between gap-2 text-sm font-bold\">\n          <Tooltip>\n            <TooltipTrigger>\n              {m.profile_subscription_updated_at({\n                updated: dayjs(profile.updated * 1000).fromNow(),\n              })}\n            </TooltipTrigger>\n\n            {profile.option?.update_interval && (\n              <TooltipContent side=\"bottom\">\n                {m.profile_subscription_next_update_at({\n                  next: dayjs(\n                    profile.updated * 1000 +\n                      profile.option.update_interval * 1000 * 60,\n                  ).format('YYYY-MM-DD HH:mm:ss'),\n                })}\n              </TooltipContent>\n            )}\n          </Tooltip>\n\n          {profile.extra?.expire && (\n            <span>\n              {m.profile_subscription_expires_in({\n                expires: dayjs(profile.extra?.expire * 1000).fromNow(),\n              })}\n            </span>\n          )}\n        </div>\n      </CardContent>\n\n      <CardFooter className=\"gap-1\">\n        <Button\n          className=\"flex items-center gap-2\"\n          onClick={handleRefreshClick}\n          loading={blockTask.isPending}\n        >\n          <RefreshRounded />\n          <span>{m.profile_subscription_update()}</span>\n        </Button>\n\n        <UpdateOptionEditor profile={profile} asChild>\n          <Button className=\"flex items-center gap-2\">\n            <RuleSettingsRounded />\n            <span>{m.profile_update_option_edit()}</span>\n          </Button>\n        </UpdateOptionEditor>\n      </CardFooter>\n    </Card>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/profiles/$type/detail/_modules/subscription-url-editor.tsx",
    "content": "import { AnimatePresence } from 'framer-motion'\nimport { ComponentProps, useState } from 'react'\nimport { Controller, useForm } from 'react-hook-form'\nimport { z } from 'zod'\nimport { useBlockTask } from '@/components/providers/block-task-provider'\nimport { Button } from '@/components/ui/button'\nimport { Card, CardContent, CardFooter, CardHeader } from '@/components/ui/card'\nimport { Input } from '@/components/ui/input'\nimport {\n  Modal,\n  ModalContent,\n  ModalTitle,\n  ModalTrigger,\n} from '@/components/ui/modal'\nimport { useLockFn } from '@/hooks/use-lock-fn'\nimport { m } from '@/paraglide/messages'\nimport { formatError } from '@/utils'\nimport { message } from '@/utils/notification'\nimport { zodResolver } from '@hookform/resolvers/zod'\nimport { ProfileBuilder, RemoteProfile, useProfile } from '@nyanpasu/interface'\nimport AnimatedErrorItem from '../../../_modules/error-item'\n\nconst formSchema = z.object({\n  url: z.httpUrl(),\n})\n\nexport default function SubscriptionUrlEditor({\n  profile,\n  ...props\n}: ComponentProps<typeof ModalTrigger> & {\n  profile: RemoteProfile\n}) {\n  const { patch } = useProfile()\n\n  const [open, setOpen] = useState(false)\n\n  const form = useForm<z.infer<typeof formSchema>>({\n    resolver: zodResolver(formSchema),\n    defaultValues: {\n      url: profile.url,\n    },\n  })\n\n  const handleClose = () => {\n    setOpen(false)\n    // get latest name\n    form.reset({\n      url: profile.url,\n    })\n  }\n\n  const blockTask = useBlockTask(\n    `update-remote-profile-url-${profile.uid}`,\n    form.handleSubmit(\n      async ({ url }) => {\n        try {\n          await patch.mutateAsync({\n            uid: profile.uid,\n            profile: {\n              ...profile,\n              url,\n            } as ProfileBuilder,\n          })\n\n          handleClose()\n        } catch (error) {\n          message(`Update failed: \\n ${formatError(error)}`, {\n            title: 'Error',\n            kind: 'error',\n          })\n        }\n      },\n      (error) => {\n        console.error(error)\n        message(formatError(error.url?.message ?? ''), {\n          title: 'Error',\n          kind: 'error',\n        })\n      },\n    ),\n  )\n\n  const handleSubmit = useLockFn(blockTask.execute)\n\n  return (\n    <Modal open={open} onOpenChange={setOpen}>\n      <ModalTrigger {...props} />\n\n      <ModalContent>\n        <Card className=\"w-96\">\n          <CardHeader>\n            <ModalTitle>{m.profile_subscription_url_editor_label()}</ModalTitle>\n          </CardHeader>\n\n          <CardContent>\n            <Controller\n              control={form.control}\n              name=\"url\"\n              render={({ field }) => (\n                <div className=\"space-y-2\">\n                  <Input\n                    label={m.profile_subscription_url_label()}\n                    variant=\"outlined\"\n                    {...field}\n                  />\n\n                  <AnimatePresence>\n                    {form.formState.errors.url && (\n                      <AnimatedErrorItem className=\"text-error\">\n                        {form.formState.errors.url?.message}\n                      </AnimatedErrorItem>\n                    )}\n                  </AnimatePresence>\n                </div>\n              )}\n            />\n          </CardContent>\n\n          <CardFooter className=\"gap-1\">\n            <Button onClick={handleSubmit} loading={blockTask.isPending}>\n              {m.common_save()}\n            </Button>\n\n            <Button onClick={handleClose}>{m.common_cancel()}</Button>\n          </CardFooter>\n        </Card>\n      </ModalContent>\n    </Modal>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/profiles/$type/detail/_modules/update-option-editor.tsx",
    "content": "import { AnimatePresence } from 'framer-motion'\nimport { ComponentProps, useCallback, useEffect, useState } from 'react'\nimport { Controller, useForm } from 'react-hook-form'\nimport { z } from 'zod'\nimport { useBlockTask } from '@/components/providers/block-task-provider'\nimport { Button } from '@/components/ui/button'\nimport { Card, CardContent, CardFooter, CardHeader } from '@/components/ui/card'\nimport { Input, NumericInput } from '@/components/ui/input'\nimport {\n  Modal,\n  ModalContent,\n  ModalTitle,\n  ModalTrigger,\n} from '@/components/ui/modal'\nimport { SwitchItem } from '@/components/ui/switch'\nimport { useLockFn } from '@/hooks/use-lock-fn'\nimport { m } from '@/paraglide/messages'\nimport { formatError } from '@/utils'\nimport { message } from '@/utils/notification'\nimport { zodResolver } from '@hookform/resolvers/zod'\nimport { ProfileBuilder, RemoteProfile, useProfile } from '@nyanpasu/interface'\nimport AnimatedErrorItem from '../../../_modules/error-item'\n\nconst formSchema = z.object({\n  user_agent: z.string().optional(),\n  with_proxy: z.boolean().optional(),\n  self_proxy: z.boolean().optional(),\n  update_interval: z.number().optional(),\n})\n\nexport default function UpdateOptionEditor({\n  profile,\n  ...props\n}: ComponentProps<typeof ModalTrigger> & {\n  profile: RemoteProfile\n}) {\n  const { patch } = useProfile()\n\n  const [open, setOpen] = useState(false)\n\n  const getDefaultValues = useCallback(() => {\n    return {\n      user_agent: profile.option?.user_agent ?? '',\n      with_proxy: profile.option?.with_proxy ?? false,\n      self_proxy: profile.option?.self_proxy ?? false,\n      update_interval: profile.option?.update_interval ?? 0,\n    }\n  }, [profile.option])\n\n  const form = useForm<z.infer<typeof formSchema>>({\n    resolver: zodResolver(formSchema),\n    defaultValues: getDefaultValues(),\n  })\n\n  // sync profile option to form\n  useEffect(() => {\n    form.reset(getDefaultValues())\n  }, [form, getDefaultValues])\n\n  const handleClose = () => {\n    setOpen(false)\n    form.reset(getDefaultValues())\n  }\n\n  const blockTask = useBlockTask(\n    `update-remote-profile-${profile.uid}`,\n    form.handleSubmit(\n      async (data) => {\n        try {\n          await patch.mutateAsync({\n            uid: profile.uid,\n            profile: {\n              ...profile,\n              option: {\n                ...profile.option,\n                ...data,\n              },\n            } as ProfileBuilder,\n          })\n          handleClose()\n        } catch (error) {\n          message(`Update failed: \\n ${formatError(error)}`, {\n            title: 'Error',\n            kind: 'error',\n          })\n        }\n      },\n      (error) => {\n        console.error(error)\n        message(formatError(error), {\n          title: 'Error',\n          kind: 'error',\n        })\n      },\n    ),\n  )\n\n  const handleSubmit = useLockFn(blockTask.execute)\n\n  return (\n    <Modal open={open} onOpenChange={setOpen}>\n      <ModalTrigger {...props} />\n\n      <ModalContent>\n        <Card className=\"w-96\">\n          <CardHeader>\n            <ModalTitle>{m.profile_update_option_editor_title()}</ModalTitle>\n          </CardHeader>\n\n          <CardContent>\n            <Controller\n              control={form.control}\n              name=\"user_agent\"\n              render={({ field }) => (\n                <div className=\"mt-2 flex items-center gap-2\">\n                  <Input\n                    label={m.profile_user_agent_label()}\n                    variant=\"outlined\"\n                    {...field}\n                  />\n\n                  <AnimatePresence>\n                    {form.formState.errors.user_agent && (\n                      <AnimatedErrorItem className=\"text-error\">\n                        {form.formState.errors.user_agent.message}\n                      </AnimatedErrorItem>\n                    )}\n                  </AnimatePresence>\n                </div>\n              )}\n            />\n\n            <Controller\n              control={form.control}\n              name=\"update_interval\"\n              render={({ field }) => (\n                <div className=\"mt-2 flex items-center gap-2\">\n                  <NumericInput\n                    label={m.profile_update_interval_label()}\n                    variant=\"outlined\"\n                    min={0}\n                    step={1}\n                    {...field}\n                  />\n\n                  <AnimatePresence>\n                    {form.formState.errors.update_interval && (\n                      <AnimatedErrorItem className=\"text-error\">\n                        {form.formState.errors.update_interval.message}\n                      </AnimatedErrorItem>\n                    )}\n                  </AnimatePresence>\n                </div>\n              )}\n            />\n\n            <Controller\n              control={form.control}\n              name=\"with_proxy\"\n              render={({ field }) => (\n                <SwitchItem\n                  checked={field.value}\n                  onCheckedChange={(checked) => field.onChange(checked)}\n                >\n                  <span>{m.profile_with_proxy_label()}</span>\n                </SwitchItem>\n              )}\n            />\n\n            <Controller\n              control={form.control}\n              name=\"self_proxy\"\n              render={({ field }) => (\n                <SwitchItem\n                  checked={field.value}\n                  onCheckedChange={(checked) => field.onChange(checked)}\n                >\n                  <span>{m.profile_self_proxy_label()}</span>\n                </SwitchItem>\n              )}\n            />\n          </CardContent>\n\n          <CardFooter className=\"gap-1\">\n            <Button onClick={handleSubmit} loading={blockTask.isPending}>\n              {m.common_save()}\n            </Button>\n\n            <Button onClick={handleClose}>{m.common_cancel()}</Button>\n          </CardFooter>\n        </Card>\n      </ModalContent>\n    </Modal>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/profiles/$type/detail/_modules/view-content.tsx",
    "content": "import { ComponentProps } from 'react'\nimport { Button } from '@/components/ui/button'\nimport { useLockFn } from '@/hooks/use-lock-fn'\nimport { commands, Profile } from '@nyanpasu/interface'\n\nexport default function ViewContent({\n  profile,\n  ...props\n}: Omit<ComponentProps<typeof Button>, 'loading' | 'onClick'> & {\n  profile: Profile\n}) {\n  const handleClick = useLockFn(async () => {\n    await commands.createEditorWindow(profile.uid)\n  })\n\n  return <Button {...props} onClick={handleClick} />\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/profiles/$type/index.tsx",
    "content": "import z from 'zod'\nimport { createFileRoute } from '@tanstack/react-router'\nimport ImportButton from './_modules/import-button'\nimport ProfilesHeader from './_modules/profiles-header'\nimport ProfilesList from './_modules/profiles-list'\n\nexport enum Action {\n  ImportLocalProfile,\n}\n\nexport const Route = createFileRoute('/(main)/main/profiles/$type/')({\n  component: RouteComponent,\n  validateSearch: z.object({\n    action: z.enum(Action).optional().nullable(),\n  }),\n})\n\nfunction RouteComponent() {\n  return (\n    <>\n      <ProfilesHeader />\n\n      <ProfilesList className=\"p-4 pt-0\" />\n\n      <ImportButton />\n    </>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/profiles/_modules/consts.ts",
    "content": "import { m } from '@/paraglide/messages'\nimport { Profile, ScriptType } from '@nyanpasu/interface'\n\nexport enum ListType {\n  Grid = 'grid',\n  List = 'list',\n}\n\nexport enum ProfileType {\n  Profile = 'profile',\n  JavaScript = 'javascript',\n  Lua = 'lua',\n  Merge = 'merge',\n}\n\nexport const PROFILE_TYPE_NAMES = {\n  [ProfileType.Profile]: m.profile_profile_label(),\n  [ProfileType.JavaScript]: m.profile_javascript_label(),\n  [ProfileType.Lua]: m.profile_lua_label(),\n  [ProfileType.Merge]: m.profile_merge_label(),\n} satisfies Record<ProfileType, string>\n\nexport const PROFILE_TYPES = {\n  [ProfileType.Profile]: [{ type: 'remote' }, { type: 'local' }],\n  [ProfileType.JavaScript]: [{ type: 'script', script_type: 'javascript' }],\n  [ProfileType.Lua]: [{ type: 'script', script_type: 'lua' }],\n  [ProfileType.Merge]: [{ type: 'merge' }],\n} satisfies Record<\n  ProfileType,\n  Array<{\n    type: Profile['type']\n    script_type?: ScriptType\n  }>\n>\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/profiles/_modules/error-item.tsx",
    "content": "import { motion } from 'framer-motion'\nimport { ComponentProps } from 'react'\nimport { cn } from '@nyanpasu/ui'\n\nexport default function AnimatedErrorItem({\n  className,\n  ...props\n}: ComponentProps<typeof motion.div>) {\n  return (\n    <motion.div\n      className={cn('overflow-hidden', className)}\n      initial={{\n        height: 0,\n        opacity: 0,\n      }}\n      animate={{\n        height: 'auto',\n        opacity: 1,\n      }}\n      exit={{\n        height: 0,\n        opacity: 0,\n      }}\n      transition={{\n        height: {\n          duration: 0.2,\n          ease: 'easeInOut',\n        },\n        opacity: {\n          duration: 0.15,\n        },\n      }}\n      {...props}\n    />\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/profiles/_modules/profile-quick-import.tsx",
    "content": "import CloseSmallOutlineRounded from '~icons/material-symbols/close-small-outline-rounded'\nimport DownloadRounded from '~icons/material-symbols/download-rounded'\nimport LinkRounded from '~icons/material-symbols/link-rounded'\nimport { useState } from 'react'\nimport { Controller, useForm } from 'react-hook-form'\nimport { z } from 'zod'\nimport { Button } from '@/components/ui/button'\nimport { m } from '@/paraglide/messages'\nimport { message } from '@/utils/notification'\nimport { zodResolver } from '@hookform/resolvers/zod'\nimport { useProfile } from '@nyanpasu/interface'\nimport { cn } from '@nyanpasu/ui'\n\nconst formSchema = z.object({\n  url: z.httpUrl(),\n})\n\nexport default function ProfileQuickImport() {\n  const { create } = useProfile()\n\n  const form = useForm<z.infer<typeof formSchema>>({\n    resolver: zodResolver(formSchema),\n    defaultValues: {\n      url: '',\n    },\n  })\n\n  const handleClear = () => {\n    form.reset({ url: '' })\n  }\n\n  const hasValue = form.watch('url')\n\n  const [loading, setLoading] = useState(false)\n\n  const handleSubmit = form.handleSubmit(\n    async (data) => {\n      try {\n        setLoading(true)\n\n        await create.mutateAsync({\n          type: 'url',\n          data: {\n            url: data.url,\n            option: null,\n          },\n        })\n\n        handleClear()\n\n        message(m.profile_quick_import_success_message(), {\n          title: 'Import profile',\n          kind: 'info',\n        })\n      } catch (error) {\n        console.error(error)\n      } finally {\n        setLoading(false)\n      }\n    },\n    (errors) => {\n      console.error(errors)\n      message(errors.url?.message ?? '', {\n        title: 'Import profile failed',\n        kind: 'error',\n      })\n    },\n  )\n\n  return (\n    <form\n      className={cn(\n        'relative flex flex-1 items-center gap-1',\n        'bg-surface h-10 w-full rounded-full pr-1 pl-3',\n        'shadow-outline/30 dark:shadow-surface-variant/10 shadow',\n      )}\n      onSubmit={handleSubmit}\n    >\n      <LinkRounded className=\"size-6\" />\n\n      <Controller\n        control={form.control}\n        name=\"url\"\n        render={({ field }) => (\n          <input\n            className=\"h-full flex-1 px-1 text-sm outline-hidden\"\n            type=\"text\"\n            placeholder={m.profile_quick_import_placeholder()}\n            autoComplete=\"off\"\n            autoCapitalize=\"off\"\n            autoCorrect=\"off\"\n            spellCheck={false}\n            {...field}\n          />\n        )}\n      />\n\n      {hasValue && (\n        <>\n          {!loading && (\n            <Button icon className=\"size-8\" onClick={handleClear} type=\"button\">\n              <CloseSmallOutlineRounded className=\"size-6\" />\n            </Button>\n          )}\n\n          <Button icon className=\"size-8\" type=\"submit\" loading={loading}>\n            <DownloadRounded className=\"size-6\" />\n          </Button>\n        </>\n      )}\n    </form>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/profiles/_modules/profiles-navigate.tsx",
    "content": "import DescriptionOutlineRounded from '~icons/material-symbols/description-outline-rounded'\nimport JavascriptRounded from '~icons/material-symbols/javascript-rounded'\nimport LuaIcon from '~icons/mdi/language-lua'\nimport ChipLine from '~icons/mingcute/chip-line'\nimport YamlIcon from '~icons/nonicons/yaml-16'\nimport ScriptIcon from '~icons/streamline-plump/script-2-remix'\nimport { mapValues } from 'lodash-es'\nimport { ComponentProps, PropsWithChildren, ReactNode, useMemo } from 'react'\nimport { Button } from '@/components/ui/button'\nimport { Separator } from '@/components/ui/separator'\nimport { m } from '@/paraglide/messages'\nimport { useProfile } from '@nyanpasu/interface'\nimport { cn } from '@nyanpasu/ui'\nimport { Link, useMatchRoute } from '@tanstack/react-router'\nimport { PROFILE_TYPES, ProfileType } from './consts'\n\nconst LinkButton = ({\n  href,\n  exact = false,\n  children,\n}: PropsWithChildren<{ href: string; exact?: boolean }>) => {\n  const matchRoute = useMatchRoute()\n\n  const isActive = !!matchRoute({\n    to: href,\n    fuzzy: !exact,\n  })\n\n  return (\n    <Button variant=\"fab\" data-active={String(isActive)} asChild>\n      <Link\n        className={cn(\n          'h-14',\n          'flex items-center gap-2',\n          'data-[active=true]:bg-surface-variant/80',\n          'data-[active=false]:bg-transparent',\n          'data-[active=false]:shadow-none',\n          'data-[active=false]:hover:shadow-none',\n          'data-[active=false]:hover:bg-surface-variant/30',\n        )}\n        to={href}\n      >\n        {children}\n      </Link>\n    </Button>\n  )\n}\n\nconst ROUTES = {\n  [ProfileType.Profile]: {\n    label: m.profile_profile_label(),\n    href: '/main/profiles/profile',\n    icon: () => (\n      <div className=\"relative\">\n        <DescriptionOutlineRounded className=\"size-8\" />\n\n        <ChipLine className=\"absolute -right-0.5 bottom-0 size-4 rotate-12 rounded bg-gray-300 p-0.5 dark:bg-gray-500\" />\n      </div>\n    ),\n  },\n  [ProfileType.JavaScript]: {\n    label: m.profile_javascript_label(),\n    href: '/main/profiles/javascript',\n    icon: () => (\n      <div className=\"relative\">\n        <ScriptIcon className=\"size-8\" />\n\n        <JavascriptRounded className=\"absolute -right-0.5 bottom-0 size-4 rotate-12 rounded bg-amber-400 dark:bg-amber-700\" />\n      </div>\n    ),\n  },\n  [ProfileType.Lua]: {\n    label: m.profile_lua_label(),\n    href: '/main/profiles/lua',\n    icon: () => (\n      <div className=\"relative\">\n        <ScriptIcon className=\"size-8\" />\n\n        <LuaIcon className=\"absolute -right-0.5 bottom-0 size-4 rotate-12 rounded bg-blue-300 p-0.5 dark:bg-blue-700\" />\n      </div>\n    ),\n  },\n  [ProfileType.Merge]: {\n    label: m.profile_merge_label(),\n    href: '/main/profiles/merge',\n    icon: () => (\n      <div className=\"relative\">\n        <ScriptIcon className=\"size-8\" />\n\n        <YamlIcon className=\"absolute -right-0.5 bottom-0 size-4 rotate-12 rounded bg-orange-400 p-0.75 dark:bg-orange-700\" />\n      </div>\n    ),\n  },\n} satisfies Record<\n  ProfileType,\n  {\n    label: string\n    href: string\n    icon: () => ReactNode\n  }\n>\n\nexport default function ProfilesNavigate({\n  className,\n  ...props\n}: Omit<ComponentProps<'div'>, 'children'>) {\n  const {\n    query: { data: profiles },\n  } = useProfile()\n\n  const counts = useMemo<Record<ProfileType, number>>(\n    () =>\n      mapValues(\n        PROFILE_TYPES,\n        (conditions) =>\n          (profiles?.items ?? []).filter((profile) =>\n            conditions.some(\n              (condition) =>\n                profile.type === condition.type &&\n                (!('script_type' in condition) ||\n                  ('script_type' in profile &&\n                    profile.script_type === condition.script_type)),\n            ),\n          ).length,\n      ),\n    [profiles?.items],\n  )\n\n  return (\n    <div className={cn('flex flex-col gap-2', className)} {...props}>\n      {Object.entries(ROUTES).map(([profileType, route]) => (\n        <LinkButton key={route.href} href={route.href}>\n          <div className=\"size-8\">{route.icon()}</div>\n\n          <div className=\"text-sm font-medium\">\n            <p>{route.label}</p>\n\n            <p className=\"text-xs text-zinc-500\">\n              {m.profile_profile_label_count({\n                count: counts[profileType as ProfileType] ?? 0,\n              })}\n            </p>\n          </div>\n        </LinkButton>\n      ))}\n\n      <Separator />\n\n      <LinkButton href=\"/main/profiles/inspect\">Profile Inspect</LinkButton>\n    </div>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/profiles/index.tsx",
    "content": "import { AppContentScrollArea } from '@/components/ui/scroll-area'\nimport useIsMobile from '@/hooks/use-is-moblie'\nimport { createFileRoute } from '@tanstack/react-router'\nimport ProfilesNavigate from './_modules/profiles-navigate'\n\nexport const Route = createFileRoute('/(main)/main/profiles/')({\n  component: RouteComponent,\n})\n\nfunction RouteComponent() {\n  const isMobile = useIsMobile()\n\n  if (!isMobile) {\n    return null\n  }\n\n  return (\n    <AppContentScrollArea\n      className=\"bg-surface-variant/10\"\n      data-slot=\"profiles-sidebar-scroll-area\"\n    >\n      <ProfilesNavigate className=\"p-4\" />\n    </AppContentScrollArea>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/profiles/inspect/route.tsx",
    "content": "import { createFileRoute } from '@tanstack/react-router'\n\nexport const Route = createFileRoute('/(main)/main/profiles/inspect')({\n  component: RouteComponent,\n})\n\nfunction RouteComponent() {\n  return <div>Hello \"/(main)/main/profiles/inspect\"!</div>\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/profiles/route.tsx",
    "content": "import { AnimatedOutletPreset } from '@/components/router/animated-outlet'\nimport { AppContentScrollArea } from '@/components/ui/scroll-area'\nimport { Sidebar, SidebarContent } from '@/components/ui/sidebar'\nimport { cn } from '@nyanpasu/ui'\nimport { createFileRoute } from '@tanstack/react-router'\nimport ProfilesNavigate from './_modules/profiles-navigate'\n\nexport const Route = createFileRoute('/(main)/main/profiles')({\n  component: RouteComponent,\n})\n\nfunction RouteComponent() {\n  return (\n    <Sidebar data-slot=\"profiles-container\">\n      <SidebarContent\n        className=\"bg-surface-variant/10\"\n        data-slot=\"profiles-sidebar-scroll-area\"\n      >\n        <ProfilesNavigate className=\"p-2\" />\n      </SidebarContent>\n\n      <AppContentScrollArea\n        className={cn(\n          'group/profiles-content flex-[3_1_auto]',\n          'overflow-clip',\n        )}\n        data-slot=\"profiles-content-scroll-area\"\n      >\n        <div\n          className={cn(\n            'container mx-auto w-full max-w-7xl',\n            'min-h-[calc(100vh-40px-64px)]',\n            'sm:min-h-[calc(100vh-40px-48px)]',\n          )}\n          data-slot=\"profiles-content\"\n        >\n          <AnimatedOutletPreset />\n        </div>\n      </AppContentScrollArea>\n    </Sidebar>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/providers/_modules/providers-title.tsx",
    "content": "import ArrowBackIosNewRounded from '~icons/material-symbols/arrow-back-ios-new-rounded'\nimport { AnimatePresence, motion } from 'framer-motion'\nimport { ComponentProps, useId } from 'react'\nimport { Button } from '@/components/ui/button'\nimport { useScrollArea } from '@/components/ui/scroll-area'\nimport { cn } from '@nyanpasu/ui'\nimport { Link } from '@tanstack/react-router'\n\nconst BackButton = () => {\n  return (\n    <Button\n      icon\n      variant=\"raised\"\n      className=\"flex items-center justify-center\"\n      asChild\n    >\n      <Link to=\"/main/providers\">\n        <ArrowBackIosNewRounded className=\"size-4\" />\n      </Link>\n    </Button>\n  )\n}\n\nconst Title = (props: ComponentProps<typeof motion.div>) => {\n  return (\n    <motion.div\n      layout\n      transition={{\n        layout: {\n          duration: 0.5,\n          ease: [0.32, 0.72, 0, 1],\n        },\n        opacity: {\n          duration: 0.16,\n        },\n      }}\n      {...props}\n    />\n  )\n}\n\nexport function ProvidersTitle({\n  className,\n  children,\n  ...props\n}: ComponentProps<'div'>) {\n  const { offset } = useScrollArea()\n\n  const id = useId()\n\n  const showTopTitle = offset.top > 40\n\n  return (\n    <>\n      <div\n        className={cn(\n          'group sticky top-0 z-10',\n          'bg-mixed-background',\n          'flex items-center gap-4',\n          'h-16 px-4',\n          className,\n        )}\n        data-show-title={showTopTitle}\n        data-slot=\"providers-title\"\n        {...props}\n      >\n        <BackButton />\n\n        <AnimatePresence initial={false}>\n          {showTopTitle && (\n            <Title\n              key=\"providers-title-top\"\n              layoutId={id}\n              className=\"text-xl font-bold\"\n            >\n              {children}\n            </Title>\n          )}\n        </AnimatePresence>\n      </div>\n\n      <div\n        className=\"group flex h-24 px-6 pt-10 pb-4\"\n        data-slot=\"providers-title\"\n        data-show-title={!showTopTitle}\n      >\n        <AnimatePresence initial={false}>\n          {!showTopTitle && (\n            <Title\n              key=\"providers-title-main\"\n              layoutId={id}\n              className=\"text-3xl font-bold\"\n            >\n              {children}\n            </Title>\n          )}\n        </AnimatePresence>\n      </div>\n    </>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/providers/_modules/use-proxies-provider-update.tsx",
    "content": "import { useBlockTask } from '@/components/providers/block-task-provider'\nimport { formatError } from '@/utils'\nimport { message } from '@/utils/notification'\nimport { ClashProxiesProviderQueryItem } from '@nyanpasu/interface'\n\nexport const useProxiesProviderUpdate = (\n  data: ClashProxiesProviderQueryItem,\n) => {\n  const blockTask = useBlockTask(\n    `update-proxies-provider-${data.name}`,\n    async () => {\n      try {\n        await data.mutate()\n      } catch (error) {\n        console.error('Failed to update proxies provider', error)\n        message(`Update provider failed: \\n ${formatError(error)}`, {\n          title: 'Error',\n          kind: 'error',\n        })\n      }\n    },\n  )\n\n  return blockTask\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/providers/_modules/use-proxies-subscription.tsx",
    "content": "import { useMemo } from 'react'\nimport { ClashProviderProxies } from '@nyanpasu/interface'\n\nexport const useProxiesSubscription = (data: ClashProviderProxies) => {\n  return useMemo(() => {\n    let progress = 0\n    let total = 0\n    let used = 0\n\n    const hasSubscriptionInfo =\n      'subscriptionInfo' in data && data.subscriptionInfo !== undefined\n\n    if (hasSubscriptionInfo) {\n      const subscriptionInfo = data.subscriptionInfo as Record<\n        string,\n        number | undefined\n      >\n\n      const download =\n        subscriptionInfo.download ?? subscriptionInfo.Download ?? 0\n      const upload = subscriptionInfo.upload ?? subscriptionInfo.Upload ?? 0\n      const t = subscriptionInfo.total ?? subscriptionInfo.Total ?? 0\n\n      total = t\n\n      used = download + upload\n\n      progress = (used / (total || 1)) * 100\n    }\n\n    return {\n      progress,\n      total,\n      used,\n      hasSubscriptionInfo,\n    }\n  }, [data])\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/providers/_modules/use-rules-provider-update.tsx",
    "content": "import { useBlockTask } from '@/components/providers/block-task-provider'\nimport { formatError } from '@/utils'\nimport { message } from '@/utils/notification'\nimport { ClashRulesProviderQueryItem } from '@nyanpasu/interface'\n\nexport const useRulesProviderUpdate = (data: ClashRulesProviderQueryItem) => {\n  const blockTask = useBlockTask(\n    `update-rules-provider-${data.name}`,\n    async () => {\n      try {\n        await data.mutate()\n      } catch (error) {\n        console.error('Failed to update rules provider', error)\n        message(`Update provider failed: \\n ${formatError(error)}`, {\n          title: 'Error',\n          kind: 'error',\n        })\n      }\n    },\n  )\n\n  return blockTask\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/providers/index.tsx",
    "content": "import AllInboxRounded from '~icons/material-symbols/all-inbox-outline-rounded'\nimport RefreshRounded from '~icons/material-symbols/refresh-rounded'\nimport dayjs from 'dayjs'\nimport { filesize } from 'filesize'\nimport { ComponentProps, PropsWithChildren } from 'react'\nimport { useBlockTask } from '@/components/providers/block-task-provider'\nimport { Button } from '@/components/ui/button'\nimport { Card, CardContent } from '@/components/ui/card'\nimport { LinearProgress } from '@/components/ui/progress'\nimport TextMarquee from '@/components/ui/text-marquee'\nimport { useLockFn } from '@/hooks/use-lock-fn'\nimport { m } from '@/paraglide/messages'\nimport { formatError } from '@/utils'\nimport { message } from '@/utils/notification'\nimport {\n  ClashProxiesProviderQueryItem,\n  ClashRulesProviderQueryItem,\n  useClashProxiesProvider,\n  useClashRulesProvider,\n} from '@nyanpasu/interface'\nimport { cn } from '@nyanpasu/ui'\nimport { createFileRoute, Link } from '@tanstack/react-router'\nimport { useProxiesProviderUpdate } from './_modules/use-proxies-provider-update'\nimport { useProxiesSubscription } from './_modules/use-proxies-subscription'\nimport { useRulesProviderUpdate } from './_modules/use-rules-provider-update'\n\nexport const Route = createFileRoute('/(main)/main/providers/')({\n  component: RouteComponent,\n})\n\nconst NavigateButton = ({\n  className,\n  ...props\n}: ComponentProps<typeof Button>) => {\n  return (\n    <Button\n      variant=\"fab\"\n      className={cn(\n        'flex h-auto w-full flex-col justify-center gap-1 p-3 text-left',\n        'bg-on-background/3!',\n        'dark:bg-surface!',\n        'shadow-none',\n        'hover:shadow-none',\n        'hover:bg-surface-variant/30',\n        className,\n      )}\n      asChild\n      {...props}\n    />\n  )\n}\n\nconst Group = ({ children }: PropsWithChildren) => {\n  return (\n    <div className=\"flex flex-col gap-1\" data-slot=\"providers-group\">\n      {children}\n    </div>\n  )\n}\n\nconst GroupTitle = ({ children }: PropsWithChildren) => {\n  return (\n    <div\n      className={cn(\n        'sticky top-0 z-10 pl-1 text-lg font-semibold',\n        'text-secondary bg-mixed-background flex h-16 items-center justify-between',\n      )}\n      data-slot=\"providers-group-title\"\n    >\n      {children}\n    </div>\n  )\n}\n\nconst GroupContent = ({ children }: PropsWithChildren) => {\n  return (\n    <div\n      className=\"grid grid-cols-2 gap-2 sm:grid-cols-3 md:grid-cols-4\"\n      data-slot=\"providers-group-content\"\n    >\n      {children}\n    </div>\n  )\n}\n\nconst Empty = ({ children }: PropsWithChildren) => {\n  return (\n    <Card variant=\"outline\">\n      <CardContent className=\"min-h-40 items-center justify-center text-sm\">\n        <AllInboxRounded className=\"size-10\" />\n\n        {children}\n      </CardContent>\n    </Card>\n  )\n}\n\nconst Proxies = ({ data }: { data: ClashProxiesProviderQueryItem }) => {\n  const { progress, total, used, hasSubscriptionInfo } =\n    useProxiesSubscription(data)\n\n  const blockTask = useProxiesProviderUpdate(data)\n\n  const handleClick = useLockFn(blockTask.execute)\n\n  return (\n    <NavigateButton className=\"flex flex-col gap-2\">\n      <Link\n        to=\"/main/providers/proxies/$key\"\n        params={{\n          key: data.name,\n        }}\n      >\n        <div className=\"flex items-center justify-between gap-2\">\n          <TextMarquee className=\"text-sm font-medium\">{data.name}</TextMarquee>\n\n          <div className=\"text-xs text-nowrap text-zinc-700 dark:text-zinc-300\">\n            {dayjs(data.updatedAt).fromNow()}\n          </div>\n        </div>\n\n        <div className=\"text-xs text-zinc-500\">\n          {data.vehicleType}/{data.type}\n        </div>\n\n        <div className=\"flex flex-1 flex-col gap-2 text-xs text-zinc-500\">\n          {hasSubscriptionInfo && (\n            <>\n              <LinearProgress value={progress} />\n\n              <TextMarquee>\n                <div className=\"flex items-center justify-between gap-2 text-xs font-bold\">\n                  <div>{progress.toFixed(2)}%</div>\n\n                  <div>\n                    {filesize(used)} / {filesize(total)}\n                  </div>\n                </div>\n              </TextMarquee>\n            </>\n          )}\n        </div>\n\n        <div className=\"flex items-center justify-between\">\n          <div className=\"bg-surface-variant text-secondary rounded-full px-2 py-1 text-[10px]\">\n            {m.providers_proxies_proxy_count_label({\n              count: data.proxies.length,\n            })}\n          </div>\n\n          <Button\n            className=\"size-6\"\n            icon\n            loading={blockTask.isPending}\n            onClick={(e) => {\n              e.preventDefault()\n              e.stopPropagation()\n              handleClick()\n            }}\n          >\n            <RefreshRounded />\n          </Button>\n        </div>\n      </Link>\n    </NavigateButton>\n  )\n}\n\nconst Rules = ({ data }: { data: ClashRulesProviderQueryItem }) => {\n  const blockTask = useRulesProviderUpdate(data)\n\n  const handleClick = useLockFn(blockTask.execute)\n\n  return (\n    <NavigateButton className=\"flex flex-col gap-2\">\n      <Link\n        to=\"/main/providers/rules/$key\"\n        params={{\n          key: data.name,\n        }}\n      >\n        <div className=\"flex items-center justify-between gap-2\">\n          <TextMarquee className=\"text-sm font-medium\">{data.name}</TextMarquee>\n\n          <div className=\"text-xs text-nowrap text-zinc-700 dark:text-zinc-300\">\n            {dayjs(data.updatedAt).fromNow()}\n          </div>\n        </div>\n\n        <div className=\"text-xs text-zinc-500\">\n          {data.vehicleType}/{data.type}\n        </div>\n\n        <div className=\"flex items-center justify-between\">\n          <div className=\"bg-surface-variant text-secondary rounded-full px-2 py-1 text-[10px]\">\n            {m.providers_rules_rule_count_label({\n              count: data.ruleCount,\n            })}\n          </div>\n\n          <Button\n            className=\"size-6\"\n            icon\n            loading={blockTask.isPending}\n            onClick={(e) => {\n              e.preventDefault()\n              e.stopPropagation()\n              handleClick()\n            }}\n          >\n            <RefreshRounded />\n          </Button>\n        </div>\n      </Link>\n    </NavigateButton>\n  )\n}\n\nfunction RouteComponent() {\n  const proxiesProvider = useClashProxiesProvider()\n\n  const proxies = proxiesProvider.data\n    ? Object.entries(proxiesProvider.data)\n    : null\n\n  const proxiesBlockTask = useBlockTask('update-proxies-provider', async () => {\n    if (!proxies) {\n      return\n    }\n\n    try {\n      await Promise.all(proxies.map(([_, data]) => data.mutate()))\n    } catch (error) {\n      console.error('Failed to update proxies provider', error)\n      message(`Update provider failed: \\n ${formatError(error)}`, {\n        title: 'Error',\n        kind: 'error',\n      })\n    }\n  })\n\n  const handleUpdateProxies = useLockFn(proxiesBlockTask.execute)\n\n  const rulesProvider = useClashRulesProvider()\n\n  const rules = rulesProvider.data ? Object.entries(rulesProvider.data) : null\n\n  const rulesBlockTask = useBlockTask('update-rules-provider', async () => {\n    if (!rules) {\n      return\n    }\n\n    try {\n      await Promise.all(rules.map(([_, data]) => data.mutate()))\n    } catch (error) {\n      console.error('Failed to update rules provider', error)\n      message(`Update provider failed: \\n ${formatError(error)}`, {\n        title: 'Error',\n        kind: 'error',\n      })\n    }\n  })\n\n  const handleUpdateRules = useLockFn(rulesBlockTask.execute)\n\n  return (\n    <div className=\"flex flex-col gap-4 p-4 pt-0\">\n      <Group>\n        <GroupTitle>\n          <span>{m.providers_proxies_title()}</span>\n\n          <Button\n            icon\n            onClick={handleUpdateProxies}\n            loading={proxiesBlockTask.isPending}\n          >\n            <RefreshRounded />\n          </Button>\n        </GroupTitle>\n\n        {proxies && proxies.length ? (\n          <GroupContent>\n            {proxies.map(([key, data]) => (\n              <Proxies key={key} data={data} />\n            ))}\n          </GroupContent>\n        ) : (\n          <Empty>\n            <p>{m.providers_no_proxies_message()}</p>\n          </Empty>\n        )}\n      </Group>\n\n      <Group>\n        <GroupTitle>\n          <span>{m.providers_rules_title()}</span>\n\n          <Button\n            icon\n            onClick={handleUpdateRules}\n            loading={rulesBlockTask.isPending}\n          >\n            <RefreshRounded />\n          </Button>\n        </GroupTitle>\n\n        {rules && rules.length ? (\n          <GroupContent>\n            {rules.map(([key, data]) => (\n              <Rules key={key} data={data} />\n            ))}\n          </GroupContent>\n        ) : (\n          <Empty>\n            <p>{m.providers_no_rules_message()}</p>\n          </Empty>\n        )}\n      </Group>\n    </div>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/providers/proxies/$key.tsx",
    "content": "import { useClashProxiesProvider } from '@nyanpasu/interface'\nimport { createFileRoute } from '@tanstack/react-router'\nimport { ProvidersTitle } from '../_modules/providers-title'\nimport { InfoCard } from './_modules/info-card'\nimport { SubscriptionCard } from './_modules/subscription-card'\n\nexport const Route = createFileRoute('/(main)/main/providers/proxies/$key')({\n  component: RouteComponent,\n})\n\nfunction RouteComponent() {\n  const { key } = Route.useParams()\n\n  const proxiesProvider = useClashProxiesProvider()\n\n  const currentProxy = proxiesProvider.data?.[key]\n\n  if (!currentProxy) {\n    return null\n  }\n\n  return (\n    <>\n      <ProvidersTitle>{key}</ProvidersTitle>\n\n      <div className=\"grid grid-cols-2 gap-4 p-4 md:grid-cols-4\">\n        <SubscriptionCard data={currentProxy} />\n\n        <InfoCard data={currentProxy} />\n      </div>\n    </>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/providers/proxies/_modules/info-card.tsx",
    "content": "import RefreshRounded from '~icons/material-symbols/refresh-rounded'\nimport dayjs from 'dayjs'\nimport { Button } from '@/components/ui/button'\nimport { Card, CardContent, CardFooter, CardHeader } from '@/components/ui/card'\nimport { useLockFn } from '@/hooks/use-lock-fn'\nimport { m } from '@/paraglide/messages'\nimport { ClashProxiesProviderQueryItem } from '@nyanpasu/interface'\nimport { useProxiesProviderUpdate } from '../../_modules/use-proxies-provider-update'\n\nexport const InfoCard = ({ data }: { data: ClashProxiesProviderQueryItem }) => {\n  const blockTask = useProxiesProviderUpdate(data)\n\n  const handleRefreshClick = useLockFn(async () => {\n    await blockTask.execute()\n  })\n\n  return (\n    <Card className=\"col-span-2 flex flex-col justify-between\">\n      <CardHeader>{m.providers_info_title()}</CardHeader>\n\n      <CardContent>\n        <div className=\"flex items-center justify-between px-1\">\n          <div className=\"text-secondary text-sm\">\n            {m.providers_proxies_proxy_count_label({\n              count: data.proxies.length,\n            })}\n          </div>\n\n          <div className=\"text-sm text-zinc-500\">\n            {data.vehicleType}/{data.type}\n          </div>\n        </div>\n      </CardContent>\n\n      <CardFooter>\n        <Button\n          className=\"flex items-center gap-2\"\n          onClick={handleRefreshClick}\n          loading={blockTask.isPending}\n        >\n          <RefreshRounded />\n          <span>{m.providers_update_provider()}</span>\n        </Button>\n\n        <div className=\"flex-1\" />\n\n        <div className=\"hover:bg-surface-variant text-secondary rounded-full px-3 py-2 text-xs font-semibold\">\n          {m.profile_subscription_updated_at({\n            updated: dayjs(data.updatedAt).fromNow(),\n          })}\n        </div>\n      </CardFooter>\n    </Card>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/providers/proxies/_modules/subscription-card.tsx",
    "content": "import { filesize } from 'filesize'\nimport { Card, CardContent, CardHeader } from '@/components/ui/card'\nimport { LinearProgress } from '@/components/ui/progress'\nimport { m } from '@/paraglide/messages'\nimport { ClashProxiesProviderQueryItem } from '@nyanpasu/interface'\nimport { useProxiesSubscription } from '../../_modules/use-proxies-subscription'\n\nexport const SubscriptionCard = ({\n  data,\n}: {\n  data: ClashProxiesProviderQueryItem\n}) => {\n  const { progress, total, used, hasSubscriptionInfo } =\n    useProxiesSubscription(data)\n\n  if (!hasSubscriptionInfo) {\n    return null\n  }\n\n  return (\n    <Card className=\"col-span-2 flex flex-col justify-between\">\n      <CardHeader>{m.providers_subscription_title()}</CardHeader>\n\n      <CardContent>\n        <LinearProgress value={progress} />\n\n        <div className=\"flex items-center justify-between pb-2\">\n          <div className=\"text-sm font-bold\">{progress.toFixed(2)}%</div>\n\n          <div className=\"text-sm font-bold\">\n            {filesize(used)} / {filesize(total)}\n          </div>\n        </div>\n      </CardContent>\n    </Card>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/providers/route.tsx",
    "content": "import { motion } from 'framer-motion'\nimport { ComponentProps } from 'react'\nimport { AnimatedOutletPreset } from '@/components/router/animated-outlet'\nimport { Button } from '@/components/ui/button'\nimport { AppContentScrollArea } from '@/components/ui/scroll-area'\nimport { Separator } from '@/components/ui/separator'\nimport { Sidebar, SidebarContent } from '@/components/ui/sidebar'\nimport TextMarquee from '@/components/ui/text-marquee'\nimport useIsMobile from '@/hooks/use-is-moblie'\nimport {\n  useClashProxiesProvider,\n  useClashRulesProvider,\n} from '@nyanpasu/interface'\nimport { cn } from '@nyanpasu/ui'\nimport { createFileRoute, Link, useLocation } from '@tanstack/react-router'\n\nexport const Route = createFileRoute('/(main)/main/providers')({\n  component: RouteComponent,\n})\n\nconst NavigateButton = ({\n  className,\n  ...props\n}: ComponentProps<typeof Button>) => {\n  return (\n    <Button\n      className={cn(\n        'h-16 w-full rounded-2xl',\n        'flex flex-col items-start justify-center gap-2',\n        'data-[active=true]:bg-surface-variant/80',\n        'data-[active=false]:bg-transparent',\n        'data-[active=false]:shadow-none',\n        'data-[active=false]:hover:shadow-none',\n        'data-[active=false]:hover:bg-surface-variant/30',\n        className,\n      )}\n      asChild\n      {...props}\n    />\n  )\n}\n\nconst SidebarNavigate = () => {\n  const proxiesProvider = useClashProxiesProvider()\n\n  const proxies = proxiesProvider.data\n    ? Object.entries(proxiesProvider.data)\n    : null\n\n  const rulesProvider = useClashRulesProvider()\n\n  const rules = rulesProvider.data ? Object.entries(rulesProvider.data) : null\n\n  const { pathname } = useLocation()\n\n  return (\n    <>\n      {proxies && proxies.length ? (\n        <>\n          <div className=\"flex flex-col gap-2 p-2\">\n            {proxies.map(([key, data]) => (\n              <NavigateButton\n                key={key}\n                data-active={String(pathname.endsWith(`/proxies/${key}`))}\n              >\n                <Link\n                  to=\"/main/providers/proxies/$key\"\n                  params={{\n                    key,\n                  }}\n                >\n                  <div className=\"text-sm font-medium\">{data.name}</div>\n\n                  <TextMarquee className=\"text-xs text-zinc-500\">\n                    {data.type}\n                  </TextMarquee>\n                </Link>\n              </NavigateButton>\n            ))}\n          </div>\n\n          <Separator />\n        </>\n      ) : null}\n\n      {rules && rules.length ? (\n        <div className=\"flex flex-col gap-2 p-2\">\n          {rules.map(([key, data]) => (\n            <NavigateButton\n              key={key}\n              data-active={String(pathname.endsWith(`/rules/${key}`))}\n            >\n              <Link\n                to=\"/main/providers/rules/$key\"\n                params={{\n                  key,\n                }}\n              >\n                <div className=\"text-sm font-medium\">{data.name}</div>\n\n                <TextMarquee className=\"text-xs text-zinc-500\">\n                  {data.type}\n                </TextMarquee>\n              </Link>\n            </NavigateButton>\n          ))}\n        </div>\n      ) : null}\n    </>\n  )\n}\n\nfunction RouteComponent() {\n  const { pathname } = useLocation()\n\n  const isCurrent = pathname === Route.fullPath\n\n  const isMobile = useIsMobile()\n\n  return (\n    <Sidebar data-slot=\"providers-container\">\n      {!isCurrent && !isMobile && (\n        <motion.div\n          animate={{\n            opacity: 1,\n            x: 0,\n          }}\n          initial={{\n            opacity: 0,\n            x: -24,\n          }}\n          transition={{\n            duration: 0.28,\n            ease: [0.22, 1, 0.36, 1],\n          }}\n        >\n          <SidebarContent\n            className=\"bg-surface-variant/10 [&>div>div]:block!\"\n            data-slot=\"providers-sidebar-scroll-area\"\n          >\n            <SidebarNavigate />\n          </SidebarContent>\n        </motion.div>\n      )}\n\n      <AppContentScrollArea\n        className={cn(\n          'group/providers-content flex-[3_1_auto]',\n          'overflow-clip',\n        )}\n        data-slot=\"providers-content-scroll-area\"\n      >\n        <div\n          className={cn(\n            'container mx-auto w-full max-w-7xl',\n            'min-h-[calc(100vh-40px-64px)]',\n            'sm:min-h-[calc(100vh-40px-48px)]',\n          )}\n          data-slot=\"providers-content\"\n        >\n          <AnimatedOutletPreset />\n        </div>\n      </AppContentScrollArea>\n    </Sidebar>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/providers/rules/$key.tsx",
    "content": "import { useClashRulesProvider } from '@nyanpasu/interface'\nimport { createFileRoute } from '@tanstack/react-router'\nimport { ProvidersTitle } from '../_modules/providers-title'\nimport { InfoCard } from './_modules/info-card'\n\nexport const Route = createFileRoute('/(main)/main/providers/rules/$key')({\n  component: RouteComponent,\n})\n\nfunction RouteComponent() {\n  const { key } = Route.useParams()\n\n  const rulesProvider = useClashRulesProvider()\n\n  const currentRule = rulesProvider.data?.[key]\n\n  if (!currentRule) {\n    return null\n  }\n\n  return (\n    <>\n      <ProvidersTitle>{key}</ProvidersTitle>\n\n      <div className=\"grid grid-cols-2 gap-4 p-4 md:grid-cols-4\">\n        <InfoCard data={currentRule} />\n      </div>\n    </>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/providers/rules/_modules/info-card.tsx",
    "content": "import RefreshRounded from '~icons/material-symbols/refresh-rounded'\nimport dayjs from 'dayjs'\nimport { Button } from '@/components/ui/button'\nimport { Card, CardContent, CardFooter, CardHeader } from '@/components/ui/card'\nimport { useLockFn } from '@/hooks/use-lock-fn'\nimport { m } from '@/paraglide/messages'\nimport { ClashRulesProviderQueryItem } from '@nyanpasu/interface'\nimport { useRulesProviderUpdate } from '../../_modules/use-rules-provider-update'\n\nexport const InfoCard = ({ data }: { data: ClashRulesProviderQueryItem }) => {\n  const blockTask = useRulesProviderUpdate(data)\n\n  const handleRefreshClick = useLockFn(async () => {\n    await blockTask.execute()\n  })\n\n  return (\n    <Card className=\"col-span-2 flex flex-col justify-between\">\n      <CardHeader>{m.providers_info_title()}</CardHeader>\n\n      <CardContent>\n        <div className=\"flex items-center justify-between px-1\">\n          <div className=\"text-secondary text-sm\">\n            {m.providers_rules_rule_count_label({\n              count: data.ruleCount,\n            })}\n          </div>\n\n          <div className=\"text-sm text-zinc-500\">\n            {data.vehicleType}/{data.type}\n          </div>\n        </div>\n      </CardContent>\n\n      <CardFooter>\n        <Button\n          className=\"flex items-center gap-2\"\n          onClick={handleRefreshClick}\n          loading={blockTask.isPending}\n        >\n          <RefreshRounded />\n          <span>{m.providers_update_provider()}</span>\n        </Button>\n\n        <div className=\"flex-1\" />\n\n        <div className=\"hover:bg-surface-variant text-secondary rounded-full px-3 py-2 text-xs font-semibold\">\n          {m.profile_subscription_updated_at({\n            updated: dayjs(data.updatedAt).fromNow(),\n          })}\n        </div>\n      </CardFooter>\n    </Card>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/proxies/_modules/hooks.ts",
    "content": "import { useMemo } from 'react'\nimport {\n  ClashProxiesQueryGroupItem,\n  useClashConnections,\n} from '@nyanpasu/interface'\n\nexport function useCurrentGroupConnection(\n  currentGroup?: ClashProxiesQueryGroupItem,\n) {\n  const {\n    query: { data: clashConnections },\n  } = useClashConnections()\n\n  return useMemo(() => {\n    if (!currentGroup?.name) {\n      return\n    }\n\n    return clashConnections\n      ?.at(-1)\n      ?.connections?.find((connection) =>\n        connection.chains.includes(currentGroup?.name),\n      )\n  }, [clashConnections, currentGroup?.name])\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/proxies/_modules/proxies-navigate.tsx",
    "content": "import { Button } from '@/components/ui/button'\nimport Image from '@/components/ui/image'\nimport { useClashProxies } from '@nyanpasu/interface'\nimport { cn } from '@nyanpasu/ui'\nimport { Link, useLocation } from '@tanstack/react-router'\n\nexport default function ProxiesNavigate() {\n  const {\n    proxies: { data: proxies },\n  } = useClashProxies()\n\n  const location = useLocation()\n\n  return (\n    <div className=\"flex flex-col gap-2 p-2\">\n      {proxies?.groups.map((group) => (\n        <Button\n          key={group.name}\n          variant=\"fab\"\n          data-active={String(\n            location.pathname.endsWith(`/group/${group.name}`),\n          )}\n          asChild\n        >\n          <Link\n            className={cn(\n              'h-16',\n              'flex items-center gap-2',\n              'data-[active=true]:bg-surface-variant/80',\n              'data-[active=false]:bg-transparent',\n              'data-[active=false]:shadow-none',\n              'data-[active=false]:hover:shadow-none',\n              'data-[active=false]:hover:bg-surface-variant/30',\n            )}\n            to=\"/main/proxies/group/$name\"\n            params={{\n              name: group.name,\n            }}\n          >\n            <div className=\"flex items-center gap-2.5\">\n              {group.icon && (\n                <div className=\"size-8\">\n                  <Image\n                    icon={group.icon}\n                    className=\"size-8\"\n                    loadingClassName=\"rounded-full\"\n                  />\n                </div>\n              )}\n\n              <div className=\"flex flex-col gap-1\">\n                <div className=\"text-sm font-medium\">{group.name}</div>\n                <div className=\"text-xs text-zinc-500\">\n                  {group.now || group.type}\n                </div>\n              </div>\n            </div>\n          </Link>\n        </Button>\n      ))}\n    </div>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/proxies/group/$name.tsx",
    "content": "import ArrowDownwardAltRounded from '~icons/material-symbols/arrow-downward-alt-rounded'\nimport ArrowUpwardAltRounded from '~icons/material-symbols/arrow-upward-alt-rounded'\nimport Radar from '~icons/material-symbols/radar'\nimport { filesize } from 'filesize'\nimport { useCallback } from 'react'\nimport { Button } from '@/components/ui/button'\nimport { useScrollArea } from '@/components/ui/scroll-area'\nimport { useClashProxies } from '@nyanpasu/interface'\nimport { useContainerBreakpointValue } from '@nyanpasu/ui'\nimport { createFileRoute } from '@tanstack/react-router'\nimport { useVirtualizer } from '@tanstack/react-virtual'\nimport { useCurrentGroupConnection } from '../_modules/hooks'\nimport DelayTestButton from './_modules/delay-test-button'\nimport GroupHeader from './_modules/group-header'\nimport ProxyNodeButton from './_modules/proxy-node-button'\n\nexport const Route = createFileRoute('/(main)/main/proxies/group/$name')({\n  component: RouteComponent,\n})\n\nfunction RouteComponent() {\n  const { name: proxyGroupName } = Route.useParams()\n\n  const {\n    proxies: { data: proxies },\n  } = useClashProxies()\n\n  const currentGroup = proxies?.groups.find(\n    (group) => group.name === proxyGroupName,\n  )\n\n  const { viewportRef } = useScrollArea()\n\n  // define the number of lanes based on the container breakpoint\n  const lanes = useContainerBreakpointValue(\n    viewportRef,\n    {\n      xs: 2,\n      sm: 3,\n      md: 4,\n      lg: 5,\n      xl: 6,\n    },\n    4,\n  )\n\n  const virtualizer = useVirtualizer({\n    count: currentGroup?.all?.length || 0,\n    getScrollElement: () => viewportRef.current,\n    estimateSize: () => 60,\n    overscan: 5,\n    lanes,\n    measureElement: (element) => element?.getBoundingClientRect().height,\n  })\n\n  const virtualItems = virtualizer.getVirtualItems()\n\n  const handleScrollToCurrentNode = useCallback(() => {\n    const index = currentGroup?.all?.findIndex(\n      (proxy) => proxy.name === currentGroup?.now,\n    )\n\n    // unwarp undefined index\n    if (index !== undefined) {\n      virtualizer.scrollToIndex(index, {\n        align: 'center',\n        behavior: 'smooth',\n      })\n    }\n  }, [currentGroup?.all, currentGroup?.now, virtualizer])\n\n  const currentGroupConnection = useCurrentGroupConnection(currentGroup)\n\n  return (\n    <>\n      <GroupHeader>\n        <div className=\"flex items-center gap-2\">\n          <div>{currentGroup?.name}</div>\n\n          <div className=\"flex items-center\">\n            <ArrowDownwardAltRounded className=\"size-6\" />\n\n            <span className=\"text-sm\">\n              {filesize(currentGroupConnection?.download ?? 0)}/s\n            </span>\n          </div>\n\n          <div className=\"flex items-center\">\n            <ArrowUpwardAltRounded className=\"size-6\" />\n\n            <span className=\"text-sm\">\n              {filesize(currentGroupConnection?.upload ?? 0)}/s\n            </span>\n          </div>\n        </div>\n\n        <div className=\"flex-1\" />\n\n        <Button icon className=\"size-8\" onClick={handleScrollToCurrentNode}>\n          <Radar className=\"size-4\" />\n        </Button>\n      </GroupHeader>\n\n      <div\n        className=\"relative m-2\"\n        data-slot=\"proxies-virtual-list\"\n        style={{\n          width: 'calc(100% - 16px)',\n          height: `${virtualizer.getTotalSize()}px`,\n        }}\n      >\n        {virtualItems.map((virtualItem) => {\n          const proxy = currentGroup?.all?.[virtualItem.index]\n\n          if (!proxy) {\n            return null\n          }\n\n          return (\n            <div\n              key={virtualItem.index}\n              ref={virtualizer.measureElement}\n              className=\"group absolute top-0 left-0 p-1\"\n              style={{\n                transform: `translateY(${virtualItem.start}px)`,\n                width: `${100 / lanes}%`,\n                left: `${virtualItem.lane * (100 / lanes)}%`,\n              }}\n              data-index={virtualItem.index}\n              data-slot=\"proxies-virtual-item\"\n              data-active={String(proxy.name === currentGroup?.now)}\n            >\n              <ProxyNodeButton proxy={proxy} />\n            </div>\n          )\n        })}\n      </div>\n\n      <DelayTestButton />\n    </>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/proxies/group/_modules/delay-test-button.tsx",
    "content": "import BoltRounded from '~icons/material-symbols/bolt-rounded'\nimport { useState } from 'react'\nimport { useBlockTask } from '@/components/providers/block-task-provider'\nimport { Button } from '@/components/ui/button'\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipTrigger,\n} from '@/components/ui/tooltip'\nimport { useLockFn } from '@/hooks/use-lock-fn'\nimport { m } from '@/paraglide/messages'\nimport { sleep } from '@/utils'\nimport { useClashProxies } from '@nyanpasu/interface'\nimport { cn } from '@nyanpasu/ui'\nimport { Route as NameRoute } from '../$name'\n\nexport default function DelayTestButton() {\n  const { name } = NameRoute.useParams()\n\n  const { updateGroupDelay } = useClashProxies()\n\n  const [isSuccess, setIsSuccess] = useState(false)\n\n  const blockTask = useBlockTask(`delay-group-test-${name}`, async () => {\n    await updateGroupDelay.mutateAsync([name])\n  })\n\n  const handleClick = useLockFn(async () => {\n    await blockTask.execute()\n\n    // success effect\n    setIsSuccess(true)\n    await sleep(1000)\n    setIsSuccess(false)\n  })\n\n  return (\n    <div\n      data-success={String(isSuccess)}\n      data-loading={String(blockTask.isPending)}\n      className={cn(\n        'absolute',\n        'right-4 transition-[top] duration-500',\n        'top-[calc(100vh-40px-64px-72px)]',\n        'sm:top-[calc(100vh-40px-48px-72px)]',\n        'data-[loading=false]:data-[success=false]:group-data-[scroll-direction=down]/proxies-content:top-full',\n      )}\n    >\n      <Tooltip>\n        <TooltipTrigger asChild>\n          <Button\n            data-success={String(isSuccess)}\n            data-loading={String(blockTask.isPending)}\n            className={cn(\n              \"**:data-[slot='circular-progress']:size-6\",\n              'transition-colors',\n              'backdrop-blur',\n              'data-[loading=false]:bg-primary-container/35',\n              'data-[loading=false]:dark:bg-on-primary/35',\n              'data-[success=true]:bg-green-500/30',\n              'data-[success=true]:dark:bg-green-700/50',\n            )}\n            variant=\"fab\"\n            icon\n            loading={blockTask.isPending}\n            onClick={handleClick}\n          >\n            <BoltRounded className=\"size-6\" />\n          </Button>\n        </TooltipTrigger>\n\n        <TooltipContent>\n          <span>\n            {blockTask.isPending\n              ? m.proxies_group_delay_test_pending_title()\n              : m.proxies_group_delay_test_title()}\n          </span>\n        </TooltipContent>\n      </Tooltip>\n    </div>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/proxies/group/_modules/group-header.tsx",
    "content": "import ArrowBackIosNewRounded from '~icons/material-symbols/arrow-back-ios-new-rounded'\nimport { ComponentProps } from 'react'\nimport { Button } from '@/components/ui/button'\nimport { cn } from '@nyanpasu/ui'\nimport { Link } from '@tanstack/react-router'\n\nconst BackButton = () => {\n  return (\n    <Button icon className=\"flex items-center justify-center md:hidden\" asChild>\n      <Link to=\"/main/proxies\">\n        <ArrowBackIosNewRounded className=\"size-4\" />\n      </Link>\n    </Button>\n  )\n}\n\nexport default function GroupHeader({\n  children,\n  className,\n  ...props\n}: ComponentProps<'div'>) {\n  return (\n    <div\n      className={cn(\n        'sticky top-0 z-10 transition-[padding] duration-500',\n        'bg-mixed-background',\n        'flex items-center gap-1',\n        'py-2 pr-4 pl-2 md:py-4 md:pl-4',\n        'group-data-[scroll-direction=down]/proxies-content:pr-6',\n        'group-data-[scroll-direction=down]/proxies-content:pl-3',\n        'group-data-[scroll-direction=down]/proxies-content:md:pl-6',\n        className,\n      )}\n      {...props}\n    >\n      <BackButton />\n\n      {children}\n    </div>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/proxies/group/_modules/proxy-node-button.tsx",
    "content": "import FlashOnRounded from '~icons/material-symbols/flash-on-rounded'\nimport { ComponentProps, MouseEvent, useMemo } from 'react'\nimport { useBlockTask } from '@/components/providers/block-task-provider'\nimport { Button } from '@/components/ui/button'\nimport { useLockFn } from '@/hooks/use-lock-fn'\nimport { ClashProxiesQueryProxyItem } from '@nyanpasu/interface'\nimport { cn } from '@nyanpasu/ui'\n\nexport default function ProxyNodeButton({\n  proxy,\n  ...props\n}: Omit<ComponentProps<typeof Button>, 'onClick' | 'children'> & {\n  proxy: ClashProxiesQueryProxyItem\n}) {\n  const handleSelectProxy = useLockFn(async () => {\n    await proxy.mutateSelect()\n  })\n\n  const delayTask = useBlockTask(\n    `proxy-delay-check-${proxy.name.toLowerCase()}`,\n    async () => {\n      await proxy.mutateDelay()\n    },\n  )\n\n  const handleDelayClick = useLockFn(\n    async (e: MouseEvent<HTMLButtonElement>) => {\n      e.preventDefault()\n      e.stopPropagation()\n\n      await delayTask.execute()\n    },\n  )\n\n  const currentDelay = useMemo(() => {\n    if (!proxy.history || proxy.history.length === 0) {\n      return -1\n    } else {\n      return proxy.history[proxy.history.length - 1].delay\n    }\n  }, [proxy.history])\n\n  return (\n    <Button\n      variant=\"fab\"\n      className={cn(\n        'flex w-full flex-col justify-center gap-1 px-2 text-left',\n        'group-data-[active=true]:bg-primary-container/75',\n        'dark:group-data-[active=true]:bg-surface-variant/50',\n        'group-data-[active=false]:bg-on-background/3',\n        'dark:group-data-[active=false]:bg-surface/30',\n        'group-data-[active=false]:shadow-none',\n        'group-data-[active=false]:hover:shadow-none',\n        'group-data-[active=false]:hover:bg-surface-variant/30',\n      )}\n      onClick={handleSelectProxy}\n      {...props}\n    >\n      <div className=\"flex items-center gap-2 px-2\">\n        <div className=\"truncate text-sm font-medium\">{proxy.name}</div>\n      </div>\n\n      <div className=\"flex items-center gap-2\">\n        <div className=\"flex-1\" />\n\n        <Button\n          className=\"grid h-4 min-w-10 place-content-center px-2 text-center\"\n          variant=\"raised\"\n          onClick={handleDelayClick}\n          loading={delayTask.isPending}\n          asChild\n        >\n          {currentDelay > 0 ? (\n            <span\n              className={cn(\n                'text-[10px]',\n                currentDelay > 0 && 'text-green-500!',\n                currentDelay > 100 && 'text-yellow-500!',\n                currentDelay > 300 && 'text-orange-500!',\n                currentDelay > 500 && 'text-red-500!',\n              )}\n            >\n              {currentDelay} ms\n            </span>\n          ) : (\n            <span>\n              <FlashOnRounded className=\"py-1\" />\n            </span>\n          )}\n        </Button>\n      </div>\n    </Button>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/proxies/index.tsx",
    "content": "import { AppContentScrollArea } from '@/components/ui/scroll-area'\nimport useIsMobile from '@/hooks/use-is-moblie'\nimport { createFileRoute } from '@tanstack/react-router'\nimport ProxiesNavigate from './_modules/proxies-navigate'\n\nexport const Route = createFileRoute('/(main)/main/proxies/')({\n  component: RouteComponent,\n})\n\nfunction RouteComponent() {\n  const isMobile = useIsMobile()\n\n  if (!isMobile) {\n    return null\n  }\n\n  return (\n    <AppContentScrollArea\n      className=\"bg-surface-variant/10\"\n      data-slot=\"proxies-sidebar-scroll-area\"\n    >\n      <ProxiesNavigate />\n    </AppContentScrollArea>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/proxies/route.tsx",
    "content": "import BoxOutlineRounded from '~icons/material-symbols/box-outline-rounded'\nimport { z } from 'zod'\nimport { AnimatedOutletPreset } from '@/components/router/animated-outlet'\nimport { Button } from '@/components/ui/button'\nimport { AppContentScrollArea } from '@/components/ui/scroll-area'\nimport { Sidebar, SidebarContent } from '@/components/ui/sidebar'\nimport { m } from '@/paraglide/messages'\nimport { useClashProxies } from '@nyanpasu/interface'\nimport { cn } from '@nyanpasu/ui'\nimport { createFileRoute, Link } from '@tanstack/react-router'\nimport { zodSearchValidator } from '@tanstack/router-zod-adapter'\nimport { ProfileType } from '../profiles/_modules/consts'\nimport ProxiesNavigate from './_modules/proxies-navigate'\n\nconst searchSchema = z.object({\n  searchQuery: z.string().optional().nullable(),\n})\n\nexport const Route = createFileRoute('/(main)/main/proxies')({\n  component: RouteComponent,\n  validateSearch: zodSearchValidator(searchSchema),\n})\n\nconst NoProxies = () => {\n  return (\n    <div\n      className=\"absolute inset-0 flex flex-col items-center justify-center gap-4\"\n      data-slot=\"proxies-no-proxies\"\n    >\n      <BoxOutlineRounded className=\"text-surface-variant size-16\" />\n\n      <p\n        className=\"text-surface-variant text-sm\"\n        data-slot=\"proxies-no-proxies-message\"\n      >\n        {m.proxies_group_empty_message()}\n      </p>\n\n      <Button variant=\"raised\" data-slot=\"proxies-no-proxies-button\" asChild>\n        <Link\n          className=\"flex items-center gap-2\"\n          to=\"/main/profiles/$type\"\n          params={{\n            type: ProfileType.Profile,\n          }}\n        >\n          {m.proxies_group_empty_button_text()}\n        </Link>\n      </Button>\n    </div>\n  )\n}\n\nfunction RouteComponent() {\n  const {\n    proxies: { data: proxies },\n  } = useClashProxies()\n\n  const isNoProxies = !proxies?.groups?.length || proxies?.groups?.length === 0\n\n  return (\n    <Sidebar data-slot=\"proxies-container\">\n      {!isNoProxies && (\n        <SidebarContent\n          className=\"bg-surface-variant/10\"\n          data-slot=\"proxies-sidebar-scroll-area\"\n        >\n          <ProxiesNavigate />\n        </SidebarContent>\n      )}\n\n      <AppContentScrollArea\n        className={cn('group/proxies-content flex-[3_1_auto]', 'overflow-clip')}\n        data-slot=\"proxies-content-scroll-area\"\n      >\n        {!isNoProxies ? (\n          <div\n            className={cn(\n              'container mx-auto w-full min-w-full',\n              'min-h-[calc(100vh-40px-64px)]',\n              'sm:min-h-[calc(100vh-40px-48px)]',\n            )}\n            data-slot=\"proxies-content\"\n          >\n            <AnimatedOutletPreset />\n          </div>\n        ) : (\n          <NoProxies />\n        )}\n      </AppContentScrollArea>\n    </Sidebar>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/rules/_modules/proxy-icon.tsx",
    "content": "import { useMemo } from 'react'\nimport Image from '@/components/ui/image'\nimport { useClashProxies } from '@nyanpasu/interface'\nimport { cn } from '@nyanpasu/ui'\n\nexport default function ProxyIcon({ groupName }: { groupName: string }) {\n  const {\n    proxies: { data: proxies },\n  } = useClashProxies()\n\n  const icon = useMemo(() => {\n    const proxyInfo = proxies?.groups.find((p) => p.name === groupName)\n\n    return proxyInfo?.icon\n  }, [groupName, proxies])\n\n  return icon ? (\n    <Image className=\"size-6\" loadingClassName=\"rounded-full\" icon={icon} />\n  ) : (\n    <div\n      className={cn(\n        'bg-surface text-secondary grid size-6 place-content-center rounded-full text-[10px]',\n      )}\n    >\n      {groupName?.toLocaleUpperCase().slice(0, 2)}\n    </div>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/rules/index.tsx",
    "content": "import { useMemo, useState } from 'react'\nimport HighlightText from '@/components/ui/highlight-text'\nimport { ScrollArea, useScrollArea } from '@/components/ui/scroll-area'\nimport { useClashRules } from '@nyanpasu/interface'\nimport { cn } from '@nyanpasu/ui'\nimport { createFileRoute } from '@tanstack/react-router'\nimport {\n  flexRender,\n  getCoreRowModel,\n  getSortedRowModel,\n  useReactTable,\n} from '@tanstack/react-table'\nimport { useVirtualizer } from '@tanstack/react-virtual'\nimport { Route as IndexRoute } from './route'\n\nexport const Route = createFileRoute('/(main)/main/rules/')({\n  component: RouteComponent,\n})\n\nconst Viewer = ({ search }: { search: string }) => {\n  const { data } = useClashRules()\n\n  const { proxy } = IndexRoute.useSearch()\n\n  const { viewportRef } = useScrollArea()\n\n  const filteredRules = useMemo(() => {\n    const rules = data?.rules ?? []\n\n    const proxyFilteredRules = proxy\n      ? rules.filter((rule) => rule.proxy === proxy)\n      : rules\n\n    if (!search.trim()) {\n      return proxyFilteredRules\n    }\n\n    const searchLower = search.toLowerCase()\n\n    return proxyFilteredRules.filter((rule) => {\n      return (\n        rule.type?.toLowerCase().includes(searchLower) ||\n        rule.payload?.toLowerCase().includes(searchLower) ||\n        rule.proxy?.toLowerCase().includes(searchLower)\n      )\n    })\n  }, [data?.rules, proxy, search])\n\n  const rowVirtualizer = useVirtualizer({\n    count: filteredRules.length,\n    getScrollElement: () => viewportRef.current,\n    estimateSize: () => 48,\n    overscan: 10,\n    measureElement: (element) => element?.getBoundingClientRect().height,\n  })\n\n  const virtualItems = rowVirtualizer.getVirtualItems()\n\n  const table = useReactTable({\n    data: filteredRules,\n    columns: [\n      {\n        accessorKey: 'Index',\n        header: 'Index',\n        cell: (info) => info.row.index + 1,\n      },\n      {\n        accessorKey: 'type',\n        header: 'Type',\n        cell: (info) => (\n          <HighlightText searchText={search}>\n            {info.row.original.type || ''}\n          </HighlightText>\n        ),\n      },\n      {\n        accessorKey: 'payload',\n        header: 'Payload',\n        cell: (info) => (\n          <HighlightText searchText={search}>\n            {info.row.original.payload || ''}\n          </HighlightText>\n        ),\n      },\n      {\n        accessorKey: 'proxy',\n        header: 'Proxy',\n        cell: (info) => (\n          <HighlightText searchText={search}>\n            {info.row.original.proxy || ''}\n          </HighlightText>\n        ),\n      },\n    ],\n    // state: {\n    //   sorting,\n    // },\n    // onSortingChange: setSorting,\n    getCoreRowModel: getCoreRowModel(),\n    getSortedRowModel: getSortedRowModel(),\n    debugTable: true,\n  })\n\n  const { rows } = table.getRowModel()\n\n  return (\n    <div\n      className=\"mx-auto max-w-7xl px-8\"\n      data-slot=\"rules-virtual-container\"\n      style={{ height: `${rowVirtualizer.getTotalSize()}px` }}\n    >\n      <table\n        className=\"w-full min-w-208 table-fixed\"\n        data-slot=\"rules-virtual-table\"\n      >\n        <colgroup>\n          <col className=\"w-20\" />\n          <col className=\"w-40\" />\n          <col />\n          <col className=\"w-40\" />\n        </colgroup>\n\n        <tbody className=\"select-text\" data-slot=\"rules-virtual-tbody\">\n          {virtualItems.map((virtualRow, index) => {\n            const row = rows[virtualRow.index]\n\n            const offset = virtualRow.start - index * virtualRow.size\n\n            return (\n              <tr\n                key={row.id}\n                data-index={virtualRow.index}\n                data-slot=\"rules-virtual-tr\"\n                style={{\n                  height: `${virtualRow.size}px`,\n                  transform: `translateY(${offset}px)`,\n                }}\n              >\n                {row.getVisibleCells().map(({ column, id, getContext }) => (\n                  <td key={id} data-slot=\"rules-virtual-td\">\n                    {flexRender(column.columnDef.cell, getContext())}\n                  </td>\n                ))}\n              </tr>\n            )\n          })}\n        </tbody>\n      </table>\n    </div>\n  )\n}\n\nfunction RouteComponent() {\n  const [search, setSearch] = useState('')\n\n  return (\n    <div className=\"divide-outline-variant flex h-full min-h-0 flex-1 flex-col divide-y overflow-hidden\">\n      <ScrollArea className=\"min-h-0 flex-1\" scrollbars=\"both\" type=\"hover\">\n        <Viewer search={search} />\n      </ScrollArea>\n\n      <div\n        className=\"bg-mixed-background flex h-16 shrink-0 items-center px-4\"\n        data-slot=\"rules-search\"\n      >\n        <input\n          type=\"text\"\n          className={cn(\n            'bg-surface-variant dark:bg-surface-variant/30',\n            'h-10 w-full rounded-full px-4 pr-10 text-sm outline-none',\n          )}\n          data-slot=\"rules-search-input-field\"\n          placeholder=\"Search rules (type, payload, or proxy)...\"\n          value={search}\n          onChange={(e) => setSearch(e.target.value)}\n        />\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/rules/route.tsx",
    "content": "import ListRounded from '~icons/material-symbols/lists-rounded'\nimport { ComponentProps, PropsWithChildren, ReactNode, useMemo } from 'react'\nimport z from 'zod'\nimport { Button } from '@/components/ui/button'\nimport { ScrollArea } from '@/components/ui/scroll-area'\nimport {\n  Sidebar,\n  SidebarLabelItem,\n  SidebarProvider,\n  SidebarToggleButton,\n  useSidebar,\n} from '@/components/ui/slider-sidebar'\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipTrigger,\n} from '@/components/ui/tooltip'\nimport { useIsMobileOrTablet } from '@/hooks/use-is-moblie'\nimport { m } from '@/paraglide/messages'\nimport { useClashRules } from '@nyanpasu/interface'\nimport { cn } from '@nyanpasu/ui'\nimport { createFileRoute, Link, Outlet } from '@tanstack/react-router'\nimport ProxyIcon from './_modules/proxy-icon'\n\nexport const Route = createFileRoute('/(main)/main/rules')({\n  component: RouteComponent,\n  validateSearch: z.object({\n    proxy: z.string().optional().nullable(),\n  }),\n})\n\nconst SidebarContent = ({ className, ...props }: ComponentProps<'div'>) => {\n  return <div className={cn('p-2', className)} {...props} />\n}\n\nconst Item = ({\n  item,\n  children,\n  icon,\n}: PropsWithChildren<{\n  item?: string\n  icon?: ReactNode\n}>) => {\n  const { proxy } = Route.useSearch()\n\n  const { open, setOpen } = useSidebar()\n\n  const isMobileOrTablet = useIsMobileOrTablet()\n\n  const handleClick = () => {\n    if (isMobileOrTablet) {\n      setOpen(false)\n    }\n  }\n\n  return (\n    <Tooltip open={open ? false : undefined}>\n      <TooltipTrigger asChild>\n        <Button\n          variant=\"fab\"\n          data-active={String(item === proxy)}\n          className={cn(\n            'h-12 min-w-0 px-3',\n            'flex items-center gap-2',\n            'data-[active=true]:bg-surface-variant/50',\n            'data-[active=false]:bg-transparent',\n            'data-[active=false]:shadow-none',\n            'data-[active=false]:hover:shadow-none',\n            'data-[active=false]:hover:bg-surface-variant/30',\n          )}\n          onClick={handleClick}\n          asChild\n        >\n          <Link\n            to=\".\"\n            search={{\n              proxy: item,\n            }}\n          >\n            <div className=\"text-md grid size-6 shrink-0 place-content-center\">\n              {icon}\n            </div>\n\n            <SidebarLabelItem>{children}</SidebarLabelItem>\n          </Link>\n        </Button>\n      </TooltipTrigger>\n\n      <TooltipContent side=\"right\">\n        <p>{children}</p>\n      </TooltipContent>\n    </Tooltip>\n  )\n}\n\nconst ProxySelector = () => {\n  const { data } = useClashRules()\n\n  const allProxy = useMemo(() => {\n    const proxies =\n      data?.rules\n        .map((rule) => rule.proxy)\n        .filter((proxy): proxy is string => !!proxy) ?? []\n\n    return [...new Set(proxies)]\n  }, [data])\n\n  return (\n    <SidebarContent className=\"flex flex-col gap-2\">\n      <Item icon={<ListRounded />}>{m.rules_list_all_proxies()}</Item>\n\n      {allProxy.map((item) => (\n        <Item key={item} item={item} icon={<ProxyIcon groupName={item} />}>\n          {item}\n        </Item>\n      ))}\n    </SidebarContent>\n  )\n}\n\nfunction RouteComponent() {\n  return (\n    <SidebarProvider defaultOpen={false}>\n      <div\n        className={cn(\n          'divide-outline-variant relative flex h-full min-h-0 w-full divide-x overflow-hidden',\n        )}\n      >\n        <Sidebar className=\"divide-outline-variant z-10 flex h-full min-h-0 flex-col divide-y\">\n          <ScrollArea className=\"min-h-0 w-full flex-1 [&>div>div]:block!\">\n            <ProxySelector />\n          </ScrollArea>\n\n          <SidebarContent className=\"flex h-16 justify-end\">\n            <SidebarToggleButton />\n          </SidebarContent>\n        </Sidebar>\n\n        <Outlet />\n      </div>\n    </SidebarProvider>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/settings/_modules/settings-card.tsx",
    "content": "import { motion } from 'framer-motion'\nimport { ComponentProps } from 'react'\nimport { AnimatedItem } from '@/components/ui/animated-item'\nimport { Card, CardContent, CardFooter, CardHeader } from '@/components/ui/card'\nimport { cn } from '@nyanpasu/ui'\n\nexport function SettingsLabel({ className, ...props }: ComponentProps<'div'>) {\n  return (\n    <div\n      className={cn('text-on-primary-container px-3 py-3 text-sm', className)}\n      data-slot=\"settings-label\"\n      {...props}\n    />\n  )\n}\n\nexport function SettingsGroup({ className, ...props }: ComponentProps<'div'>) {\n  return (\n    <div\n      className={cn(\n        'flex flex-col gap-1 *:transition-[border-radius]',\n        '[&>*:first-child:not(:only-child)]:rounded-b-sm',\n        '[&>*:last-child:not(:only-child)]:rounded-t-sm',\n        '[&>*:not(:first-child):not(:last-child)]:rounded-sm',\n        className,\n      )}\n      data-slot=\"settings-group\"\n      {...props}\n    />\n  )\n}\n\nexport function SettingsCard({\n  className,\n  ...props\n}: ComponentProps<typeof Card>) {\n  return <Card className={cn(className)} data-slot=\"settings-card\" {...props} />\n}\n\nexport function SettingsCardHeader({\n  className,\n  ...props\n}: ComponentProps<typeof CardHeader>) {\n  return (\n    <CardHeader\n      className={cn('px-5 pt-6', className)}\n      data-slot=\"settings-card-header\"\n      {...props}\n    />\n  )\n}\n\nexport function SettingsCardFooter({\n  className,\n  ...props\n}: ComponentProps<typeof CardFooter>) {\n  return (\n    <CardFooter\n      className={cn('px-3 pb-3', className)}\n      data-slot=\"settings-card-footer\"\n      {...props}\n    />\n  )\n}\n\nexport function SettingsCardContent({\n  className,\n  ...props\n}: ComponentProps<typeof CardContent>) {\n  return (\n    <CardContent\n      className={cn('gap-6 px-5 py-6', className)}\n      data-slot=\"settings-card-content\"\n      {...props}\n    />\n  )\n}\n\nexport function ItemContainer({ className, ...props }: ComponentProps<'div'>) {\n  return (\n    <div\n      className={cn('flex items-center justify-between gap-4', className)}\n      data-slot=\"settings-card-content-item-container\"\n      {...props}\n    />\n  )\n}\n\nexport function ItemLabel({ className, ...props }: ComponentProps<'div'>) {\n  return (\n    <div\n      className={cn('flex flex-col gap-0.5', className)}\n      data-slot=\"settings-card-content-item-label\"\n      {...props}\n    />\n  )\n}\n\nexport function ItemLabelText({\n  className,\n  ...props\n}: ComponentProps<typeof CardContent>) {\n  return (\n    <p\n      className={cn('text-base font-medium', className)}\n      data-slot=\"settings-card-content-item-label-text\"\n      {...props}\n    />\n  )\n}\n\nexport function ItemLabelDescription({\n  className,\n  ...props\n}: ComponentProps<'p'>) {\n  return (\n    <p\n      className={cn('text-on-surface-variant text-sm', className)}\n      data-slot=\"settings-card-content-item-label-description\"\n      {...props}\n    />\n  )\n}\n\nexport const SettingsCardAnimatedItem = AnimatedItem\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/settings/_modules/settings-navigate.tsx",
    "content": "import DisplayExternalInput from '~icons/material-symbols/display-external-input-rounded'\nimport FrameBugOutlineRounded from '~icons/material-symbols/frame-bug-outline-rounded'\nimport SettingsBoltRounded from '~icons/material-symbols/settings-b-roll-rounded'\nimport SettingsEthernet from '~icons/material-symbols/settings-ethernet-rounded'\nimport SettingsRounded from '~icons/material-symbols/settings-rounded'\nimport ViewQuilt from '~icons/material-symbols/view-quilt-rounded'\nimport { ComponentProps, ReactNode } from 'react'\nimport LogoSvg from '@/assets/image/logo.svg?react'\nimport { Button } from '@/components/ui/button'\nimport TextMarquee from '@/components/ui/text-marquee'\nimport useCurrentCoreIcon from '@/hooks/use-current-core-icon'\nimport { m } from '@/paraglide/messages'\nimport { cn } from '@nyanpasu/ui'\nimport { Link, useLocation } from '@tanstack/react-router'\n\nconst NyanpasuLogo = () => {\n  return (\n    <LogoSvg className=\"[&_#element]:fill-primary [&_#bg]:fill-surface size-8\" />\n  )\n}\n\nconst CurrentCoreIcon = ({\n  className,\n  ...props\n}: Omit<ComponentProps<'img'>, 'src'>) => {\n  const currentCoreIconUrl = useCurrentCoreIcon()\n\n  return (\n    <img\n      src={currentCoreIconUrl}\n      className={cn('size-full', className)}\n      {...props}\n    />\n  )\n}\n\nconst NavigateButton = ({\n  icon,\n  label,\n  description,\n  className,\n  ...props\n}: ComponentProps<typeof Link> & {\n  icon: ReactNode\n  label: string\n  description: string\n}) => {\n  const location = useLocation()\n\n  const isActive = location.pathname === props.to\n\n  return (\n    <Button\n      variant=\"fab\"\n      data-active={String(isActive)}\n      className={cn(\n        'h-16',\n        'flex items-center gap-2',\n        'data-[active=true]:bg-surface-variant/80',\n        'data-[active=false]:bg-transparent',\n        'data-[active=false]:shadow-none',\n        'data-[active=false]:hover:shadow-none',\n        'data-[active=false]:hover:bg-surface-variant/30',\n        className,\n      )}\n      asChild\n    >\n      <Link {...props}>\n        <div className=\"flex max-w-full items-center gap-3\">\n          <div className=\"size-8\">{icon}</div>\n\n          <div className=\"flex min-w-0 flex-1 flex-col gap-1\">\n            <div className=\"text-sm font-medium\">{label}</div>\n\n            <TextMarquee className=\"text-xs text-zinc-500\">\n              {description}\n            </TextMarquee>\n          </div>\n        </div>\n      </Link>\n    </Button>\n  )\n}\n\nconst SystemButton = () => {\n  return (\n    <NavigateButton\n      icon={<SettingsEthernet className=\"size-8\" />}\n      label={m.settings_label_system()}\n      description={m.settings_label_system_description()}\n      to=\"/main/settings/system\"\n    />\n  )\n}\n\nconst UserInterfaceButton = () => {\n  return (\n    <NavigateButton\n      icon={<ViewQuilt className=\"size-8\" />}\n      label={m.settings_label_user_interface()}\n      description={m.settings_label_user_interface_description()}\n      to=\"/main/settings/user-interface\"\n    />\n  )\n}\n\nconst ClashButton = () => {\n  return (\n    <NavigateButton\n      icon={<SettingsBoltRounded className=\"size-8\" />}\n      label={m.settings_label_clash_settings()}\n      description={m.settings_label_clash_settings_description()}\n      to=\"/main/settings/clash\"\n    />\n  )\n}\n\nconst ExternalControllButton = () => {\n  return (\n    <NavigateButton\n      icon={\n        <div className=\"relative size-8\">\n          <CurrentCoreIcon className=\"size-7.5\" />\n\n          <div\n            className={cn(\n              'absolute -right-1 -bottom-1 size-4 p-0.5',\n              'text-primary bg-surface-variant rounded-full shadow-sm',\n            )}\n          >\n            <DisplayExternalInput className=\"size-3\" />\n          </div>\n        </div>\n      }\n      label={m.settings_label_external_controll()}\n      description={m.settings_label_external_controll_description()}\n      to=\"/main/settings/web-ui\"\n    />\n  )\n}\n\nconst NyanpasuButton = () => {\n  return (\n    <NavigateButton\n      icon={\n        <div className=\"relative size-8\">\n          <NyanpasuLogo />\n\n          <div\n            className={cn(\n              'absolute -right-1 -bottom-1 size-4 p-0.5',\n              'text-primary bg-surface-variant rounded-full shadow-sm',\n            )}\n          >\n            <SettingsRounded className=\"text-primary size-3\" />\n          </div>\n        </div>\n      }\n      label={m.settings_label_nyanpasu()}\n      description={m.settings_label_nyanpasu_description()}\n      to=\"/main/settings/nyanpasu\"\n    />\n  )\n}\n\nconst DebugButton = () => {\n  return (\n    <NavigateButton\n      icon={<FrameBugOutlineRounded className=\"size-8\" />}\n      label={m.settings_label_debug()}\n      description={m.settings_label_debug_description()}\n      to=\"/main/settings/debug\"\n    />\n  )\n}\n\nconst AboutButton = () => {\n  return (\n    <NavigateButton\n      icon={<NyanpasuLogo />}\n      label={m.settings_label_about()}\n      description={m.settings_label_about_description()}\n      to=\"/main/settings/about\"\n    />\n  )\n}\n\nexport default function SettingsNavigate() {\n  return (\n    <div className=\"flex flex-col gap-2 p-2\">\n      <SystemButton />\n\n      <UserInterfaceButton />\n\n      <ClashButton />\n\n      <ExternalControllButton />\n\n      <NyanpasuButton />\n\n      <DebugButton />\n\n      <AboutButton />\n    </div>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/settings/_modules/settings-title.tsx",
    "content": "import ArrowBackIosNewRounded from '~icons/material-symbols/arrow-back-ios-new-rounded'\nimport { AnimatePresence, motion } from 'framer-motion'\nimport { ComponentProps, useId } from 'react'\nimport { Button } from '@/components/ui/button'\nimport { useScrollArea } from '@/components/ui/scroll-area'\nimport { cn } from '@nyanpasu/ui'\nimport { Link } from '@tanstack/react-router'\n\nconst BackButton = () => {\n  return (\n    <Button\n      icon\n      variant=\"raised\"\n      className=\"flex items-center justify-center md:hidden\"\n      asChild\n    >\n      <Link to=\"/main/settings\">\n        <ArrowBackIosNewRounded className=\"size-4\" />\n      </Link>\n    </Button>\n  )\n}\n\nconst Title = (props: ComponentProps<typeof motion.p>) => {\n  return (\n    <motion.p\n      layout\n      transition={{\n        layout: {\n          duration: 0.5,\n          ease: [0.32, 0.72, 0, 1],\n        },\n        opacity: {\n          duration: 0.16,\n        },\n      }}\n      {...props}\n    />\n  )\n}\n\nexport function SettingsTitle({\n  className,\n  children,\n  ...props\n}: ComponentProps<'div'>) {\n  const { offset } = useScrollArea()\n\n  const id = useId()\n\n  const showTopTitle = offset.top > 40\n\n  return (\n    <>\n      <div\n        className={cn(\n          'group sticky top-0 z-10',\n          'bg-mixed-background',\n          'flex items-center gap-4',\n          'h-16 px-4 md:px-6',\n          className,\n        )}\n        data-show-title={showTopTitle}\n        data-slot=\"settings-title\"\n        {...props}\n      >\n        <BackButton />\n\n        <AnimatePresence initial={false}>\n          {showTopTitle && (\n            <Title\n              key=\"settings-title-top\"\n              layoutId={id}\n              className=\"text-xl font-bold\"\n            >\n              {children}\n            </Title>\n          )}\n        </AnimatePresence>\n      </div>\n\n      <div\n        className=\"group flex h-24 px-6 pt-10 pb-4\"\n        data-slot=\"settings-title\"\n        data-show-title={!showTopTitle}\n      >\n        <AnimatePresence initial={false}>\n          {!showTopTitle && (\n            <Title\n              key=\"settings-title-main\"\n              layoutId={id}\n              className=\"text-3xl font-bold\"\n            >\n              {children}\n            </Title>\n          )}\n        </AnimatePresence>\n      </div>\n    </>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/settings/about/_modules/nyanpasu-version.tsx",
    "content": "import { PropsWithChildren, useEffect, useState } from 'react'\nimport Markdown from 'react-markdown'\nimport AnimatedLogo from '@/components/logo/animated-logo'\nimport { useNyanpasuUpdate } from '@/components/providers/nyanpasu-update-provider'\nimport { Button } from '@/components/ui/button'\nimport { Card, CardContent, CardFooter, CardHeader } from '@/components/ui/card'\nimport {\n  Modal,\n  ModalClose,\n  ModalContent,\n  ModalTitle,\n  ModalTrigger,\n} from '@/components/ui/modal'\nimport { LinearProgress } from '@/components/ui/progress'\nimport { ScrollArea } from '@/components/ui/scroll-area'\nimport { SwitchItem } from '@/components/ui/switch'\nimport { useLockFn } from '@/hooks/use-lock-fn'\nimport {\n  Action as AboutAction,\n  Route as AboutRoute,\n} from '@/pages/(main)/main/settings/about/route'\nimport { m } from '@/paraglide/messages'\nimport { formatError } from '@/utils'\nimport { message } from '@/utils/notification'\nimport { commands, useSetting } from '@nyanpasu/interface'\nimport { relaunch } from '@tauri-apps/plugin-process'\nimport {\n  SettingsCard,\n  SettingsCardContent,\n  SettingsCardFooter,\n} from '../../_modules/settings-card'\n\nconst TITLE = 'Clash Nyanpasu~(∠・ω< )⌒☆'\n\nconst GITHUB_RELEASES_URL =\n  'https://github.com/libnyanpasu/clash-nyanpasu/releases'\n\nconst AutoCheckUpdate = () => {\n  const { value, upsert, isPending } = useSetting('enable_auto_check_update')\n\n  return (\n    <SwitchItem\n      className=\"rounded-[20px]\"\n      checked={value ?? true}\n      onCheckedChange={(checked) => upsert(checked)}\n      loading={isPending}\n    >\n      <p className=\"truncate\">{m.settings_label_about_auto_check_updates()}</p>\n    </SwitchItem>\n  )\n}\n\nconst NewVersionModal = ({ children }: PropsWithChildren) => {\n  const { action } = AboutRoute.useSearch()\n\n  const { newVersion } = useNyanpasuUpdate()\n\n  const [isInstalling, setIsInstalling] = useState(false)\n\n  const [contentLength, setContentLength] = useState(0)\n  const [contentDownloaded, setContentDownloaded] = useState(0)\n\n  const progress =\n    contentDownloaded && contentLength\n      ? (contentDownloaded / contentLength) * 100\n      : 0\n\n  const [open, setOpen] = useState(false)\n\n  useEffect(() => {\n    // for animation duration to open the modal\n    if (action === AboutAction.NEED_UPDATE) {\n      setOpen(true)\n    }\n  }, [action])\n\n  const handleOpenChange = (open: boolean) => {\n    if (isInstalling) {\n      return\n    }\n\n    setOpen(open)\n  }\n\n  // const newVersionReleasesPageUrl = IS_NIGHTLY\n  //   ? `https://github.com/libnyanpasu/clash-nyanpasu/releases/tag/pre-release`\n  //   : `https://github.com/libnyanpasu/clash-nyanpasu/releases/tag/v${newVersion?.version}`\n\n  const handleUpdate = useLockFn(async () => {\n    if (!newVersion) {\n      return\n    }\n\n    try {\n      setIsInstalling(true)\n\n      // Install the update. This will also restart the app on Windows!\n      await newVersion.download((e) => {\n        switch (e.event) {\n          case 'Started':\n            setContentLength(e.data.contentLength || 0)\n            break\n          case 'Progress':\n            setContentDownloaded((prev) => prev + e.data.chunkLength)\n            break\n        }\n      })\n\n      await commands.cleanupProcesses()\n      // cleanup and stop core\n      await newVersion.install()\n      // On macOS and Linux you will need to restart the app manually.\n      // You could use this step to display another confirmation dialog.\n      await relaunch()\n    } catch (e) {\n      console.error(e)\n      message(formatError(e), {\n        kind: 'error',\n        title: 'Error',\n      })\n    } finally {\n      setIsInstalling(false)\n    }\n  })\n\n  return (\n    <Modal open={open} onOpenChange={handleOpenChange}>\n      <ModalTrigger asChild>{children}</ModalTrigger>\n\n      <ModalContent>\n        <Card className=\"max-w-3xl min-w-96\">\n          <CardHeader>\n            <ModalTitle>\n              {m.settings_label_about_update_has_new_version()}\n            </ModalTitle>\n          </CardHeader>\n\n          <CardContent asChild>\n            <ScrollArea className=\"max-h-[calc(100vh-200px)]\">\n              {isInstalling ? (\n                <div className=\"flex flex-col gap-2\">\n                  <div className=\"flex items-center gap-2\">\n                    {m.settings_label_about_update_installing()}\n\n                    <span className=\"text-xs text-slate-500\">\n                      {progress.toFixed(2)}%\n                    </span>\n                  </div>\n\n                  <LinearProgress className=\"w-full\" value={progress} />\n                </div>\n              ) : (\n                <Markdown\n                  components={{\n                    a(props) {\n                      const { children, node, ...rest } = props\n\n                      return (\n                        <a\n                          {...rest}\n                          onClick={(e) => {\n                            e.preventDefault()\n                            e.stopPropagation()\n\n                            if (typeof node?.properties.href === 'string') {\n                              commands.openThat(node.properties.href)\n                            }\n                          }}\n                        >\n                          {children}\n                        </a>\n                      )\n                    },\n                  }}\n                >\n                  {newVersion?.body || 'New version available.'}\n                </Markdown>\n              )}\n            </ScrollArea>\n          </CardContent>\n\n          <CardFooter className=\"gap-2\">\n            <Button\n              variant=\"flat\"\n              loading={isInstalling}\n              onClick={handleUpdate}\n            >\n              {m.settings_label_about_update_to_update_button()}\n            </Button>\n\n            {!isInstalling && <ModalClose>{m.common_close()}</ModalClose>}\n          </CardFooter>\n        </Card>\n      </ModalContent>\n    </Modal>\n  )\n}\n\nexport default function NyanpasuVersion() {\n  const {\n    currentVersion,\n    hasNewVersion,\n    isChecking,\n    checkNewVersion,\n    isSupported,\n  } = useNyanpasuUpdate()\n\n  const handleUpdateToGithubReleases = useLockFn(\n    async () => await commands.openThat(GITHUB_RELEASES_URL),\n  )\n\n  const handleCheckNewVersion = useLockFn(async () => {\n    const update = await checkNewVersion()\n\n    if (update) {\n      message(m.settings_label_about_update_has_new_version(), {\n        kind: 'info',\n        title: m.settings_label_about_update(),\n      })\n    } else {\n      message(m.settings_label_about_update_no_update(), {\n        kind: 'info',\n        title: m.settings_label_about_update(),\n      })\n    }\n  })\n\n  return (\n    <SettingsCard className=\"space-y-2\">\n      <SettingsCardContent className=\"items-center gap-4\">\n        <div className=\"p-4\">\n          <AnimatedLogo className=\"size-32\" indeterminate />\n        </div>\n\n        <div className=\"truncate text-base font-bold\">{TITLE}</div>\n\n        <div className=\"text-sm font-semibold\">\n          {m.settings_label_about_version({\n            version: currentVersion,\n          })}\n        </div>\n      </SettingsCardContent>\n\n      {isSupported ? (\n        <SettingsCardFooter className=\"flex-col gap-2\">\n          <AutoCheckUpdate />\n\n          {hasNewVersion ? (\n            <NewVersionModal>\n              <Button variant=\"flat\" className=\"w-full\">\n                {m.settings_label_about_update_has_new_version()}\n              </Button>\n            </NewVersionModal>\n          ) : (\n            <Button\n              variant=\"flat\"\n              className=\"w-full\"\n              onClick={handleCheckNewVersion}\n              loading={isChecking}\n            >\n              {m.settings_label_about_update()}\n            </Button>\n          )}\n        </SettingsCardFooter>\n      ) : (\n        <SettingsCardFooter>\n          <Button\n            variant=\"flat\"\n            className=\"w-full\"\n            onClick={handleUpdateToGithubReleases}\n          >\n            {m.settings_label_about_update_to_github_releases()}\n          </Button>\n        </SettingsCardFooter>\n      )}\n    </SettingsCard>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/settings/about/route.tsx",
    "content": "import z from 'zod'\nimport { m } from '@/paraglide/messages'\nimport { createFileRoute } from '@tanstack/react-router'\nimport { SettingsTitle } from '../_modules/settings-title'\nimport NyanpasuVersion from './_modules/nyanpasu-version'\n\nexport enum Action {\n  NEED_UPDATE = 'need-update',\n}\n\nexport const Route = createFileRoute('/(main)/main/settings/about')({\n  component: RouteComponent,\n  validateSearch: z.object({\n    action: z.enum(Action).optional().nullable(),\n  }),\n})\n\nfunction RouteComponent() {\n  return (\n    <>\n      <SettingsTitle>{m.settings_label_about()}</SettingsTitle>\n\n      <div className=\"space-y-4 px-4 pb-4\">\n        <div className=\"grid gap-2 sm:grid-cols-2\">\n          <NyanpasuVersion />\n        </div>\n      </div>\n    </>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/settings/clash/_modules/allow-lan-switch.tsx",
    "content": "import { useMemo } from 'react'\nimport { Switch } from '@/components/ui/switch'\nimport { useLockFn } from '@/hooks/use-lock-fn'\nimport { m } from '@/paraglide/messages'\nimport { formatError } from '@/utils'\nimport { message } from '@/utils/notification'\nimport { useClashConfig } from '@nyanpasu/interface'\nimport {\n  ItemContainer,\n  ItemLabel,\n  ItemLabelText,\n} from '../../_modules/settings-card'\n\nexport default function AllowLanSwitch() {\n  const { query, upsert } = useClashConfig()\n\n  const value = useMemo(() => query.data?.['allow-lan'], [query.data])\n\n  const handleAllowLan = useLockFn(async (input: boolean) => {\n    try {\n      await upsert.mutateAsync({\n        'allow-lan': input,\n      })\n    } catch (error) {\n      message(`Activation Allow LAN failed!\\n Error: ${formatError(error)}`, {\n        title: 'Error',\n        kind: 'error',\n      })\n    }\n  })\n\n  return (\n    <ItemContainer data-slot=\"allow-lan-switch-container\">\n      <ItemLabel>\n        <ItemLabelText>\n          {m.settings_clash_settings_allow_lan_label()}\n        </ItemLabelText>\n      </ItemLabel>\n\n      <Switch\n        checked={Boolean(value)}\n        onCheckedChange={handleAllowLan}\n        loading={upsert.isPending}\n      />\n    </ItemContainer>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/settings/clash/_modules/core-manager-card.tsx",
    "content": "import ArrowRightAltRounded from '~icons/material-symbols/arrow-right-alt-rounded'\nimport DeployedCodeUpdateOutlineRounded from '~icons/material-symbols/deployed-code-update-outline-rounded'\nimport RestartAltRounded from '~icons/material-symbols/restart-alt-rounded'\nimport { filesize } from 'filesize'\nimport { AnimatePresence, motion } from 'framer-motion'\nimport { isObject } from 'lodash-es'\nimport { useMemo, useState } from 'react'\nimport { useBlockTask } from '@/components/providers/block-task-provider'\nimport { Button } from '@/components/ui/button'\nimport { CircularProgress } from '@/components/ui/progress'\nimport TextMarquee from '@/components/ui/text-marquee'\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipTrigger,\n} from '@/components/ui/tooltip'\nimport useCoreIcon from '@/hooks/use-core-icon'\nimport { useLockFn } from '@/hooks/use-lock-fn'\nimport { m } from '@/paraglide/messages'\nimport { formatError } from '@/utils'\nimport { message } from '@/utils/notification'\nimport {\n  ClashCore,\n  ClashCoresDetail,\n  InspectUpdater,\n  inspectUpdater,\n  useClashConnections,\n  useClashCores,\n  useSetting,\n} from '@nyanpasu/interface'\nimport { cn } from '@nyanpasu/ui'\nimport {\n  SettingsCard,\n  SettingsCardContent,\n  SettingsCardFooter,\n  SettingsCardHeader,\n} from '../../_modules/settings-card'\n\nfunction useCoreUpdateTask(\n  core?: ClashCore | null,\n  item?: ClashCoresDetail | null,\n) {\n  const { query, updateCore } = useClashCores()\n\n  const [updater, setUpdater] = useState<InspectUpdater>()\n\n  const task = useBlockTask(`core-manager-update-${core}`, async () => {\n    try {\n      const updaterId = await updateCore.mutateAsync(core!)\n\n      if (!updaterId) {\n        throw new Error('Failed to update')\n      }\n\n      await new Promise<void>((resolve, reject) => {\n        const interval = setInterval(async () => {\n          const result = await inspectUpdater(updaterId)\n\n          setUpdater(result)\n\n          if (\n            isObject(result.downloader.state) &&\n            Object.prototype.hasOwnProperty.call(\n              result.downloader.state,\n              'failed',\n            )\n          ) {\n            reject(result.downloader.state.failed)\n            clearInterval(interval)\n          }\n\n          if (result.state === 'done') {\n            resolve()\n            clearInterval(interval)\n          }\n        }, 100)\n      })\n\n      await query.refetch()\n\n      message(\n        `Successfully updated the core ${item?.name} to ${item?.latestVersion}`,\n        {\n          kind: 'info',\n          title: 'Successful',\n        },\n      )\n    } catch (e) {\n      console.error(e)\n      message(formatError(e), {\n        kind: 'error',\n        title: 'Error',\n      })\n    }\n  })\n\n  const progress = useMemo(() => {\n    if (!updater || !task.isPending) {\n      return 0\n    }\n\n    const { downloaded, total } = updater.downloader\n\n    if (total <= 0) {\n      return 0\n    }\n    return Math.min((downloaded / total) * 100, 100)\n  }, [updater, task.isPending])\n\n  const stateLabel = useMemo(() => {\n    if (!updater || !task.isPending) {\n      return null\n    }\n\n    const state = updater.state\n\n    if (state === 'downloading') {\n      const { downloaded, total, speed } = updater.downloader\n      return `${filesize(downloaded)} / ${filesize(total)} · ${filesize(speed)}/s`\n    }\n\n    if (state === 'decompressing') {\n      return m.settings_clash_core_manager_card_decompressing()\n    }\n\n    if (state === 'replacing') {\n      return m.settings_clash_core_manager_card_replacing()\n    }\n\n    if (state === 'restarting') {\n      return m.settings_clash_core_manager_card_restarting()\n    }\n\n    if (state === 'done') {\n      return m.settings_clash_core_manager_card_done()\n    }\n\n    return null\n  }, [updater, task.isPending])\n\n  return { task, progress, stateLabel }\n}\n\nconst UpdateProgressBar = ({\n  isPending,\n  progress,\n}: {\n  isPending: boolean\n  progress: number\n}) => {\n  if (!isPending) {\n    return null\n  }\n\n  return (\n    <motion.div\n      className=\"bg-primary/10 absolute inset-0 origin-left\"\n      initial={{ scaleX: 0 }}\n      animate={{ scaleX: progress / 100 }}\n      transition={{ duration: 0.3, ease: 'easeOut' }}\n    />\n  )\n}\n\nconst CoreItem = ({\n  core,\n  item,\n  onClick,\n}: {\n  core: ClashCore\n  item: ClashCoresDetail\n  onClick: (core: ClashCore) => void\n}) => {\n  const { value: currentCore } = useSetting('clash_core')\n\n  const icon = useCoreIcon(core)\n\n  const isSelected = core === currentCore\n\n  const haveNewVersion = item.latestVersion\n    ? item.latestVersion !== item.currentVersion\n    : false\n\n  const {\n    task: updateCoreTask,\n    progress,\n    stateLabel: updaterStateLabel,\n  } = useCoreUpdateTask(core, item)\n\n  return (\n    <Button\n      variant={isSelected ? 'raised' : 'basic'}\n      data-selected={isSelected}\n      data-downloading={updateCoreTask.isPending}\n      className={cn(\n        'relative h-auto w-full min-w-0 overflow-hidden rounded-2xl p-2 text-left',\n        'flex items-center gap-2',\n      )}\n      onClick={() => {\n        if (updateCoreTask.isPending) {\n          return\n        }\n\n        onClick(core)\n      }}\n    >\n      <UpdateProgressBar\n        isPending={updateCoreTask.isPending}\n        progress={progress}\n      />\n\n      <div className=\"relative size-12\">\n        <img src={icon} alt={item.name} />\n      </div>\n\n      <div className=\"relative flex min-w-0 flex-1 flex-col gap-1\">\n        <TextMarquee>{item.name}</TextMarquee>\n\n        <TextMarquee className=\"text-sm\">\n          {updateCoreTask.isPending && updaterStateLabel ? (\n            <span className=\"text-emerald-700\">{updaterStateLabel}</span>\n          ) : haveNewVersion ? (\n            <p className=\"flex items-center gap-1\">\n              <span>{item.currentVersion}</span>\n              <ArrowRightAltRounded />\n              <span className=\"text-emerald-700\">{item.latestVersion}</span>\n            </p>\n          ) : (\n            item.currentVersion\n          )}\n        </TextMarquee>\n      </div>\n\n      {haveNewVersion && (\n        <div className=\"m-2\">\n          <Tooltip>\n            <TooltipTrigger asChild>\n              <Button\n                className=\"size-8\"\n                variant=\"stroked\"\n                icon\n                onClick={(e) => {\n                  e.preventDefault()\n                  e.stopPropagation()\n                  updateCoreTask.execute()\n                }}\n                loading={updateCoreTask.isPending}\n                asChild\n              >\n                <span>\n                  <DeployedCodeUpdateOutlineRounded />\n                </span>\n              </Button>\n            </TooltipTrigger>\n\n            <TooltipContent>\n              {m.settings_clash_core_manager_card_click_to_update()}\n            </TooltipContent>\n          </Tooltip>\n        </div>\n      )}\n    </Button>\n  )\n}\n\nexport default function CoreManagerCard() {\n  const {\n    query: clashCores,\n    upsert: switchCore,\n    restartSidecar,\n    fetchRemote,\n  } = useClashCores()\n\n  const { deleteConnections } = useClashConnections()\n\n  const { value: currentCoreKey } = useSetting('clash_core')\n\n  const currentCoreIcon = useCoreIcon(currentCoreKey)\n\n  const currentCore = currentCoreKey && clashCores.data?.[currentCoreKey]\n\n  const switchCoreTask = useBlockTask(\n    'core-manager-switch',\n    async (core: ClashCore) => {\n      try {\n        await deleteConnections.mutateAsync(null)\n\n        await switchCore.mutateAsync(core)\n\n        message(m.settings_clash_core_manager_card_loading_success(), {\n          kind: 'info',\n          title: 'Successful',\n        })\n      } catch (e) {\n        console.error(e)\n        message(\n          `${m.settings_clash_core_manager_card_loading_error()} \\n${formatError(e)}`,\n          {\n            kind: 'error',\n            title: 'Error',\n          },\n        )\n      }\n    },\n  )\n\n  const restartSidecarTask = useBlockTask(\n    'core-manager-restart-sidecar',\n    async () => {\n      try {\n        await restartSidecar()\n\n        message(m.settings_clash_core_manager_card_restart_sidecar_success(), {\n          kind: 'info',\n          title: 'Successful',\n        })\n      } catch (e) {\n        console.error(e)\n        message(\n          `${m.settings_clash_core_manager_card_restart_sidecar_error()} \\n${formatError(e)}`,\n          {\n            kind: 'error',\n            title: 'Error',\n          },\n        )\n      }\n    },\n  )\n\n  const handleFetchRemote = useLockFn(async () => {\n    try {\n      await fetchRemote.mutateAsync()\n    } catch (e) {\n      console.error(e)\n      message(formatError(e), {\n        kind: 'error',\n        title: 'Error',\n      })\n    }\n  })\n\n  const isLoading =\n    clashCores.isPending ||\n    switchCoreTask.isPending ||\n    restartSidecarTask.isPending\n\n  const loadingMessage = m.settings_clash_core_manager_card_loading()\n\n  const haveNewVersion = currentCore?.latestVersion\n    ? currentCore.latestVersion !== currentCore.currentVersion\n    : false\n\n  const currentCoreUpdate = useCoreUpdateTask(currentCoreKey, currentCore)\n\n  return (\n    <SettingsCard data-slot=\"core-manager-card\" className=\"relative\">\n      <AnimatePresence initial={false}>\n        {isLoading && (\n          <motion.div\n            data-slot=\"core-manager-card-mask\"\n            initial={{ opacity: 0 }}\n            animate={{ opacity: 1 }}\n            exit={{ opacity: 0 }}\n            className={cn(\n              'bg-primary/10 absolute inset-0 z-50 backdrop-blur-3xl',\n              'flex flex-col items-center justify-center gap-4',\n            )}\n          >\n            <CircularProgress className=\"size-12\" indeterminate />\n\n            <p>{loadingMessage}</p>\n          </motion.div>\n        )}\n      </AnimatePresence>\n\n      <SettingsCardContent data-slot=\"core-manager-card-content\">\n        <div\n          className={cn(\n            'relative flex items-center gap-3 overflow-hidden rounded-2xl p-4',\n            'bg-surface-variant',\n          )}\n        >\n          <UpdateProgressBar\n            isPending={currentCoreUpdate.task.isPending}\n            progress={currentCoreUpdate.progress}\n          />\n\n          <div className=\"relative size-12\">\n            <img\n              src={currentCoreIcon}\n              alt={currentCore?.name}\n              className=\"size-full\"\n            />\n          </div>\n\n          <div className=\"relative flex-1\">\n            <p className=\"font-medium\">{currentCore?.name}</p>\n\n            <p className=\"flex items-center gap-1 text-sm\">\n              {currentCoreUpdate.task.isPending &&\n              currentCoreUpdate.stateLabel ? (\n                <span className=\"text-emerald-700\">\n                  {currentCoreUpdate.stateLabel}\n                </span>\n              ) : haveNewVersion ? (\n                <>\n                  <span>{currentCore?.currentVersion}</span>\n                  <ArrowRightAltRounded />\n                  <span className=\"text-emerald-700\">\n                    {currentCore?.latestVersion}\n                  </span>\n                </>\n              ) : (\n                currentCore?.currentVersion\n              )}\n            </p>\n          </div>\n\n          <div className=\"relative mr-2 flex items-center gap-3\">\n            {haveNewVersion && (\n              <Tooltip>\n                <TooltipTrigger asChild>\n                  <Button\n                    variant=\"stroked\"\n                    icon\n                    onClick={() => currentCoreUpdate.task.execute()}\n                    loading={currentCoreUpdate.task.isPending}\n                  >\n                    <DeployedCodeUpdateOutlineRounded className=\"size-5\" />\n                  </Button>\n                </TooltipTrigger>\n\n                <TooltipContent>\n                  {m.settings_clash_core_manager_card_click_to_update()}\n                </TooltipContent>\n              </Tooltip>\n            )}\n\n            <Tooltip>\n              <TooltipTrigger asChild>\n                <Button\n                  icon\n                  variant=\"stroked\"\n                  onClick={() => restartSidecarTask.execute()}\n                >\n                  <RestartAltRounded className=\"size-5\" />\n                </Button>\n              </TooltipTrigger>\n\n              <TooltipContent>\n                {m.settings_clash_core_manager_card_restart_sidecar()}\n              </TooltipContent>\n            </Tooltip>\n          </div>\n        </div>\n\n        <div className=\"grid grid-cols-1 gap-2 md:grid-cols-2\">\n          {Object.entries(clashCores.data ?? {}).map(([core, item]) => {\n            if (core === currentCoreKey) {\n              return null\n            }\n\n            return (\n              <CoreItem\n                key={item.name}\n                core={core as ClashCore}\n                item={item}\n                onClick={() => switchCoreTask.execute(core as ClashCore)}\n              />\n            )\n          })}\n        </div>\n      </SettingsCardContent>\n\n      <SettingsCardFooter className=\"gap-2\">\n        <Button\n          variant=\"flat\"\n          onClick={handleFetchRemote}\n          loading={fetchRemote.isPending}\n        >\n          {m.settings_clash_core_manager_card_fetch_remote()}\n        </Button>\n      </SettingsCardFooter>\n    </SettingsCard>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/settings/clash/_modules/field-filter-card.tsx",
    "content": "import ArrowForwardIosRounded from '~icons/material-symbols/arrow-forward-ios-rounded'\nimport OpenInNewRounded from '~icons/material-symbols/open-in-new-rounded'\nimport { PropsWithChildren, useMemo } from 'react'\nimport CLASH_FIELD from '@/assets/json/clash-field.json'\nimport { useBlockTask } from '@/components/providers/block-task-provider'\nimport { Button } from '@/components/ui/button'\nimport { Card, CardContent, CardFooter, CardHeader } from '@/components/ui/card'\nimport {\n  Modal,\n  ModalClose,\n  ModalContent,\n  ModalTitle,\n  ModalTrigger,\n} from '@/components/ui/modal'\nimport { ScrollArea } from '@/components/ui/scroll-area'\nimport TextMarquee from '@/components/ui/text-marquee'\nimport { useLockFn } from '@/hooks/use-lock-fn'\nimport { m } from '@/paraglide/messages'\nimport { commands, useProfile } from '@nyanpasu/interface'\nimport { cn } from '@nyanpasu/ui'\n\ntype Item = {\n  url?: string\n  enabled: boolean\n}\n\nconst OpenLinkButton = ({ data }: { data: Item }) => {\n  const handleOpen = useLockFn(async () => {\n    if (!data.url) {\n      return\n    }\n\n    await commands.openThat(data.url)\n  })\n\n  return (\n    <Button\n      variant=\"stroked\"\n      className=\"size-6\"\n      icon\n      onClick={(e) => {\n        e.stopPropagation()\n        e.preventDefault()\n        handleOpen()\n      }}\n      asChild\n    >\n      <span>\n        <OpenInNewRounded className=\"size-3\" />\n      </span>\n    </Button>\n  )\n}\n\nconst FieldButton = ({\n  data,\n  disabled,\n  label,\n}: {\n  data: Item\n  disabled: boolean\n  label: string\n}) => {\n  const { query, upsert } = useProfile()\n\n  const blockTask = useBlockTask(`update-clash-field-${label}`, async () => {\n    let valid = query.data?.valid ?? []\n\n    if (data.enabled) {\n      valid = valid.filter((item) => item !== label)\n    } else {\n      valid.push(label)\n    }\n\n    await upsert.mutateAsync({ valid })\n  })\n\n  return (\n    <Button\n      data-enabled={String(data.enabled)}\n      className={cn(\n        'flex h-12 items-center justify-between gap-2 rounded-2xl pr-3',\n        'data-[enabled=true]:bg-primary-container',\n        'data-[enabled=true]:dark:bg-surface-variant',\n        'data-[enabled=false]:bg-primary-container/50',\n        'data-[enabled=false]:dark:bg-surface-variant/10',\n      )}\n      disabled={disabled}\n      onClick={() => blockTask.execute()}\n      loading={blockTask.isPending}\n    >\n      <TextMarquee className=\"w-full min-w-0 text-left text-sm\">\n        {label}\n      </TextMarquee>\n\n      <div>\n        <OpenLinkButton data={data} />\n      </div>\n    </Button>\n  )\n}\n\nconst ItemButton = ({\n  items,\n  children,\n}: PropsWithChildren<{\n  items: Record<string, Item>\n}>) => {\n  // Nyanpasu Control Fields object key\n  const isNyanpasuControlField = ['default', 'handle'].includes(\n    children as string,\n  )\n\n  const enableFields = Object.keys(items).filter((key) => items[key].enabled)\n\n  return (\n    <Modal>\n      <ModalTrigger asChild>\n        <Button\n          className={cn(\n            'relative h-20 w-full min-w-0 rounded-3xl',\n            'bg-primary-container dark:bg-surface-variant/30',\n            'flex flex-col items-start justify-center gap-0.5 pr-8',\n          )}\n        >\n          <div className=\"text-base font-bold capitalize\">{children}</div>\n\n          <TextMarquee className=\"w-full min-w-0 text-left text-sm\">\n            <p className=\"space-x-1\">\n              <span>Enabled:</span>\n\n              {enableFields.map((field) => {\n                return <span key={field}>{field}</span>\n              })}\n            </p>\n          </TextMarquee>\n\n          <ArrowForwardIosRounded className=\"absolute top-1/2 right-2 size-5 -translate-y-1/2\" />\n        </Button>\n      </ModalTrigger>\n\n      <ModalContent>\n        <Card className=\"w-96\">\n          <CardHeader>\n            <ModalTitle className=\"capitalize\">{children}</ModalTitle>\n\n            {isNyanpasuControlField && (\n              <div className=\"text-on-surface-variant text-sm\">\n                {m.settings_clash_settings_field_filter_nyanpasu_control_fields()}\n              </div>\n            )}\n          </CardHeader>\n\n          <CardContent asChild>\n            <ScrollArea className=\"max-h-[calc(100vh-200px)]\">\n              <div className=\"grid grid-cols-2 gap-2\">\n                {Object.entries(items).map(([item, data]) => {\n                  return (\n                    <FieldButton\n                      key={item}\n                      data={data}\n                      disabled={isNyanpasuControlField}\n                      label={item}\n                    />\n                  )\n                })}\n              </div>\n            </ScrollArea>\n          </CardContent>\n\n          <CardFooter>\n            <ModalClose>{m.common_close()}</ModalClose>\n          </CardFooter>\n        </Card>\n      </ModalContent>\n    </Modal>\n  )\n}\n\nexport default function FieldFilterCard() {\n  const { query } = useProfile()\n\n  const mergeFields = useMemo(\n    () => [\n      ...Object.keys(CLASH_FIELD.default),\n      ...Object.keys(CLASH_FIELD.handle),\n      ...(query.data?.valid ?? []),\n    ],\n    [query.data],\n  )\n\n  const filteredField = (fields: Record<string, string>) => {\n    const usedObjects: Record<string, Item> = {}\n\n    for (const key in fields) {\n      if (Object.prototype.hasOwnProperty.call(fields, key)) {\n        usedObjects[key] = {\n          url: fields[key],\n          enabled: mergeFields.includes(key),\n        }\n      }\n    }\n\n    return usedObjects\n  }\n\n  return (\n    <div className=\"grid grid-cols-2 gap-2 lg:grid-cols-4\">\n      {Object.entries(CLASH_FIELD).map(([key, value], index) => {\n        const filtered = filteredField(value)\n\n        return (\n          <ItemButton key={index} items={filtered}>\n            {key}\n          </ItemButton>\n        )\n      })}\n    </div>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/settings/clash/_modules/field-filter-switch.tsx",
    "content": "import { Switch } from '@/components/ui/switch'\nimport { useLockFn } from '@/hooks/use-lock-fn'\nimport { m } from '@/paraglide/messages'\nimport { formatError } from '@/utils'\nimport { message } from '@/utils/notification'\nimport { useSetting } from '@nyanpasu/interface'\nimport {\n  ItemContainer,\n  ItemLabel,\n  ItemLabelText,\n} from '../../_modules/settings-card'\n\nexport default function FieldFilterButton() {\n  const { value, upsert } = useSetting('enable_clash_fields')\n\n  const handleFieldFilter = useLockFn(async (input: boolean) => {\n    try {\n      await upsert(input)\n    } catch (error) {\n      message(\n        `Activation Field Filter failed!\\n Error: ${formatError(error)}`,\n        {\n          title: 'Error',\n          kind: 'error',\n        },\n      )\n    }\n  })\n\n  return (\n    <ItemContainer data-slot=\"field-filter-switch-container\">\n      <ItemLabel>\n        <ItemLabelText>\n          {m.settings_clash_settings_field_filter_label()}\n        </ItemLabelText>\n      </ItemLabel>\n\n      <Switch checked={Boolean(value)} onCheckedChange={handleFieldFilter} />\n    </ItemContainer>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/settings/clash/_modules/ipv6-switch.tsx",
    "content": "import { useMemo } from 'react'\nimport { Switch } from '@/components/ui/switch'\nimport { useLockFn } from '@/hooks/use-lock-fn'\nimport { m } from '@/paraglide/messages'\nimport { formatError } from '@/utils'\nimport { message } from '@/utils/notification'\nimport { useClashConfig } from '@nyanpasu/interface'\nimport {\n  ItemContainer,\n  ItemLabel,\n  ItemLabelText,\n} from '../../_modules/settings-card'\n\nexport default function IPv6Switch() {\n  const { query, upsert } = useClashConfig()\n\n  const value = useMemo(() => query.data?.['ipv6'], [query.data])\n\n  const handleIPv6 = useLockFn(async (input: boolean) => {\n    try {\n      await upsert.mutateAsync({\n        ipv6: input,\n      })\n    } catch (error) {\n      message(`Activation IPv6 failed!\\n Error: ${formatError(error)}`, {\n        title: 'Error',\n        kind: 'error',\n      })\n    }\n  })\n\n  return (\n    <ItemContainer data-slot=\"ipv6-switch-container\">\n      <ItemLabel>\n        <ItemLabelText>{m.settings_clash_settings_ipv6_label()}</ItemLabelText>\n      </ItemLabel>\n\n      <Switch\n        checked={Boolean(value)}\n        onCheckedChange={handleIPv6}\n        loading={upsert.isPending}\n      />\n    </ItemContainer>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/settings/clash/_modules/log-level-selector.tsx",
    "content": "import { useCallback, useMemo } from 'react'\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from '@/components/ui/select'\nimport { m } from '@/paraglide/messages'\nimport { useClashConfig } from '@nyanpasu/interface'\n\nconst LOG_LEVEL_OPTIONS = {\n  debug: 'Debug',\n  info: 'Info',\n  warning: 'Warn',\n  error: 'Error',\n  silent: 'Silent',\n} as const\n\nexport default function LogLevelSelector() {\n  const { query, upsert } = useClashConfig()\n\n  const value = useMemo(\n    () => query.data?.['log-level'] as keyof typeof LOG_LEVEL_OPTIONS,\n    [query.data],\n  )\n\n  const handleLogLevelChange = useCallback(\n    async (value: string) => {\n      await upsert.mutateAsync({\n        'log-level': value as string,\n      })\n    },\n    [upsert],\n  )\n\n  return (\n    <Select\n      variant=\"outlined\"\n      value={value}\n      onValueChange={handleLogLevelChange}\n    >\n      <SelectTrigger>\n        <SelectValue placeholder={m.settings_clash_settings_log_level_label()}>\n          {value ? LOG_LEVEL_OPTIONS[value] : null}\n        </SelectValue>\n      </SelectTrigger>\n\n      <SelectContent>\n        {Object.entries(LOG_LEVEL_OPTIONS).map(([key, value]) => (\n          <SelectItem key={key} value={key}>\n            {value}\n          </SelectItem>\n        ))}\n      </SelectContent>\n    </Select>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/settings/clash/_modules/mixed-port-config.tsx",
    "content": "import { AnimatePresence } from 'framer-motion'\nimport { useCallback, useEffect, useMemo } from 'react'\nimport { Controller, useForm } from 'react-hook-form'\nimport { z } from 'zod'\nimport { Button } from '@/components/ui/button'\nimport { NumericInput } from '@/components/ui/input'\nimport { m } from '@/paraglide/messages'\nimport { formatError } from '@/utils'\nimport { message } from '@/utils/notification'\nimport { zodResolver } from '@hookform/resolvers/zod'\nimport { useClashConfig, useSetting } from '@nyanpasu/interface'\nimport { SettingsCardAnimatedItem } from '../../_modules/settings-card'\n\nconst DEFAULT_MIXED_PORT = 7890\n\nconst formSchema = z.object({\n  mixedPort: z.number().min(1).max(65535),\n})\n\nexport default function MixedPortConfig() {\n  const mixedPort = useSetting('verge_mixed_port')\n\n  const clashConfig = useClashConfig()\n\n  const form = useForm<z.infer<typeof formSchema>>({\n    resolver: zodResolver(formSchema),\n    defaultValues: {\n      mixedPort: DEFAULT_MIXED_PORT,\n    },\n  })\n\n  // get current mixed port from clash config or verge setting\n  const currentMixedPort = useMemo(() => {\n    return (\n      clashConfig.query.data?.['mixed-port'] ||\n      mixedPort.value ||\n      DEFAULT_MIXED_PORT\n    )\n  }, [clashConfig.query.data, mixedPort.value])\n\n  // sync current mixed port to form\n  useEffect(() => {\n    form.setValue('mixedPort', currentMixedPort)\n  }, [currentMixedPort, form])\n\n  const handleSubmit = form.handleSubmit(async (data) => {\n    try {\n      await clashConfig.upsert.mutateAsync({\n        'mixed-port': data.mixedPort,\n      })\n      await mixedPort.upsert(data.mixedPort)\n\n      form.reset({\n        mixedPort: data.mixedPort,\n      })\n    } catch (error) {\n      message(formatError(error), {\n        title: 'Error',\n        kind: 'error',\n      })\n    }\n  })\n\n  const handleReset = useCallback(() => {\n    form.reset({\n      mixedPort: currentMixedPort,\n    })\n  }, [form, currentMixedPort])\n\n  return (\n    <form className=\"flex flex-col gap-2\" onSubmit={handleSubmit}>\n      <Controller\n        name=\"mixedPort\"\n        control={form.control}\n        render={({ field, fieldState }) => {\n          const handleChange = (value: number | null) => {\n            field.onChange(value)\n          }\n\n          return (\n            <>\n              <NumericInput\n                variant=\"outlined\"\n                label={m.settings_clash_settings_mixed_port_label()}\n                value={field.value}\n                onChange={handleChange}\n                allowNegative={false}\n                decimalScale={0}\n              />\n\n              <AnimatePresence>\n                {fieldState.error && (\n                  <SettingsCardAnimatedItem className=\"text-error\">\n                    {fieldState.error.message}\n                  </SettingsCardAnimatedItem>\n                )}\n              </AnimatePresence>\n            </>\n          )\n        }}\n      />\n\n      <AnimatePresence initial={false}>\n        {form.formState.isDirty && (\n          <SettingsCardAnimatedItem>\n            <div className=\"flex justify-end gap-2 pt-1\">\n              <Button type=\"button\" onClick={handleReset}>\n                {m.common_reset()}\n              </Button>\n\n              <Button\n                variant=\"raised\"\n                onClick={handleSubmit}\n                loading={form.formState.isSubmitting}\n              >\n                {m.common_apply()}\n              </Button>\n            </div>\n          </SettingsCardAnimatedItem>\n        )}\n      </AnimatePresence>\n    </form>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/settings/clash/_modules/random-port-switch.tsx",
    "content": "import { Switch } from '@/components/ui/switch'\nimport { m } from '@/paraglide/messages'\nimport { formatError } from '@/utils'\nimport { message } from '@/utils/notification'\nimport { useSetting } from '@nyanpasu/interface'\nimport {\n  ItemContainer,\n  ItemLabel,\n  ItemLabelText,\n} from '../../_modules/settings-card'\n\nexport default function RandomPortSwitch() {\n  const enableRandomPort = useSetting('enable_random_port')\n\n  const handleRandomPort = async () => {\n    try {\n      await enableRandomPort.upsert(!enableRandomPort.value)\n    } catch (e) {\n      message(formatError(e), {\n        title: 'Error',\n        kind: 'error',\n      })\n    } finally {\n      message(\n        enableRandomPort.value\n          ? m.settings_clash_settings_random_port_disabled()\n          : m.settings_clash_settings_random_port_enabled(),\n        {\n          title: 'Successful',\n          kind: 'info',\n        },\n      )\n    }\n  }\n\n  return (\n    <ItemContainer data-slot=\"auto-launch-switch-container\">\n      <ItemLabel>\n        <ItemLabelText>\n          {m.settings_clash_settings_random_port_label()}\n        </ItemLabelText>\n      </ItemLabel>\n\n      <Switch\n        checked={Boolean(enableRandomPort.value)}\n        onCheckedChange={handleRandomPort}\n        loading={enableRandomPort.isPending}\n      />\n    </ItemContainer>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/settings/clash/_modules/tun-stack-selector.tsx",
    "content": "import { useCallback, useMemo } from 'react'\nimport {\n  Select,\n  SelectContent,\n  SelectGroup,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from '@/components/ui/select'\nimport { useCoreType } from '@/hooks/use-store'\nimport { m } from '@/paraglide/messages'\nimport { formatError } from '@/utils'\nimport { message } from '@/utils/notification'\nimport { TunStack, useRuntimeProfile, useSetting } from '@nyanpasu/interface'\n\nexport default function TunStackSelector() {\n  const [coreType] = useCoreType()\n\n  const tunStack = useSetting('tun_stack')\n\n  const enableTunMode = useSetting('enable_tun_mode')\n\n  const runtimeProfile = useRuntimeProfile()\n\n  const tunStackOptions = useMemo(() => {\n    const options: {\n      [key: string]: string\n    } = {\n      system: 'System',\n      gvisor: 'gVisor',\n      mixed: 'Mixed',\n    }\n\n    // clash not support mixed\n    if (coreType === 'clash') {\n      delete options.mixed\n    }\n    return options\n  }, [coreType])\n\n  const currentTunStack = useMemo(() => {\n    const stack = tunStack.value || 'gvisor'\n    return stack in tunStackOptions ? stack : 'gvisor'\n  }, [tunStackOptions, tunStack.value])\n\n  const handleTunStackChange = useCallback(\n    async (value: string) => {\n      try {\n        await tunStack.upsert(value as TunStack)\n\n        if (enableTunMode.value) {\n          // just to reload clash config\n          await enableTunMode.upsert(true)\n        }\n\n        // need manual mutate to refetch runtime profile\n        await runtimeProfile.refetch()\n      } catch (error) {\n        message(`Change Tun Stack failed ! \\n Error: ${formatError(error)}`, {\n          title: 'Error',\n          kind: 'error',\n        })\n      }\n    },\n    [tunStack, enableTunMode, runtimeProfile],\n  )\n\n  return (\n    <Select\n      variant=\"outlined\"\n      value={currentTunStack}\n      onValueChange={handleTunStackChange}\n    >\n      <SelectTrigger>\n        <SelectValue placeholder={m.settings_clash_settings_tun_stack_label()}>\n          {tunStackOptions[currentTunStack]}\n        </SelectValue>\n      </SelectTrigger>\n\n      <SelectContent>\n        <SelectGroup>\n          {Object.entries(tunStackOptions).map(([key, value]) => (\n            <SelectItem key={key} value={key}>\n              {value}\n            </SelectItem>\n          ))}\n        </SelectGroup>\n      </SelectContent>\n    </Select>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/settings/clash/route.tsx",
    "content": "import { m } from '@/paraglide/messages'\nimport { createFileRoute } from '@tanstack/react-router'\nimport {\n  SettingsCard,\n  SettingsCardContent,\n  SettingsGroup,\n  SettingsLabel,\n} from '../_modules/settings-card'\nimport { SettingsTitle } from '../_modules/settings-title'\nimport AllowLanSwitch from './_modules/allow-lan-switch'\nimport CoreManagerCard from './_modules/core-manager-card'\nimport FieldFilterCard from './_modules/field-filter-card'\nimport FieldFilterSwitch from './_modules/field-filter-switch'\nimport IPv6Switch from './_modules/ipv6-switch'\nimport LogLevelSelector from './_modules/log-level-selector'\nimport MixedPortConfig from './_modules/mixed-port-config'\nimport RandomPortSwitch from './_modules/random-port-switch'\nimport TunStackSelector from './_modules/tun-stack-selector'\n\nexport const Route = createFileRoute('/(main)/main/settings/clash')({\n  component: RouteComponent,\n})\n\nconst PatchSettings = () => {\n  return (\n    <div data-slot=\"patch-settings-container\">\n      <SettingsLabel>{m.settings_clash_settings_title()}</SettingsLabel>\n\n      <SettingsGroup>\n        <SettingsCard>\n          <SettingsCardContent>\n            <AllowLanSwitch />\n          </SettingsCardContent>\n        </SettingsCard>\n\n        <SettingsCard>\n          <SettingsCardContent>\n            <IPv6Switch />\n          </SettingsCardContent>\n        </SettingsCard>\n\n        <SettingsCard>\n          <SettingsCardContent>\n            <TunStackSelector />\n\n            <LogLevelSelector />\n          </SettingsCardContent>\n        </SettingsCard>\n      </SettingsGroup>\n    </div>\n  )\n}\n\nconst PortSettings = () => {\n  return (\n    <div data-slot=\"port-settings-container\">\n      <SettingsLabel>{m.settings_clash_settings_port_label()}</SettingsLabel>\n\n      <SettingsGroup>\n        <SettingsCard>\n          <SettingsCardContent>\n            <MixedPortConfig />\n          </SettingsCardContent>\n        </SettingsCard>\n\n        <SettingsCard>\n          <SettingsCardContent>\n            <RandomPortSwitch />\n          </SettingsCardContent>\n        </SettingsCard>\n      </SettingsGroup>\n    </div>\n  )\n}\n\nconst CoreManagerSettings = () => {\n  return (\n    <div data-slot=\"core-manager-settings-container\">\n      <SettingsLabel>\n        {m.settings_clash_core_manager_card_title()}\n      </SettingsLabel>\n\n      <SettingsGroup>\n        <CoreManagerCard />\n      </SettingsGroup>\n    </div>\n  )\n}\n\nconst FieldFilterSettings = () => {\n  return (\n    <div data-slot=\"field-filter-settings-container\">\n      <SettingsLabel>\n        {m.settings_clash_settings_field_filter_label()}\n      </SettingsLabel>\n\n      <div className=\"space-y-2\">\n        <SettingsCard>\n          <SettingsCardContent>\n            <FieldFilterSwitch />\n          </SettingsCardContent>\n        </SettingsCard>\n\n        <FieldFilterCard />\n      </div>\n    </div>\n  )\n}\n\nfunction RouteComponent() {\n  return (\n    <>\n      <SettingsTitle>{m.settings_clash_settings_title()}</SettingsTitle>\n\n      <div className=\"space-y-4 px-4 pb-4\">\n        <PatchSettings />\n\n        <PortSettings />\n\n        <CoreManagerSettings />\n\n        <FieldFilterSettings />\n      </div>\n    </>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/settings/debug/_modules/advance-tools-switch.tsx",
    "content": "import { Switch } from '@/components/ui/switch'\nimport {\n  ItemContainer,\n  ItemLabel,\n  ItemLabelText,\n} from '../../_modules/settings-card'\nimport { useDebugContext } from './debug-provider'\n\nexport default function AdvanceToolsSwitch() {\n  const { advanceTools, setAdvanceTools } = useDebugContext()\n\n  return (\n    <ItemContainer data-slot=\"allow-lan-switch-container\">\n      <ItemLabel>\n        <ItemLabelText>Advance Tools</ItemLabelText>\n      </ItemLabel>\n\n      <Switch\n        checked={Boolean(advanceTools)}\n        onCheckedChange={setAdvanceTools}\n      />\n    </ItemContainer>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/settings/debug/_modules/block-task-viewer.tsx",
    "content": "import { useBlockTaskContext } from '@/components/providers/block-task-provider'\nimport { Button } from '@/components/ui/button'\nimport { Card, CardContent, CardFooter, CardHeader } from '@/components/ui/card'\nimport {\n  Modal,\n  ModalClose,\n  ModalContent,\n  ModalTitle,\n  ModalTrigger,\n} from '@/components/ui/modal'\nimport { useLockFn } from '@/hooks/use-lock-fn'\nimport { m } from '@/paraglide/messages'\nimport {\n  SettingsCard,\n  SettingsCardAnimatedItem,\n  SettingsCardContent,\n  SettingsCardFooter,\n  SettingsCardHeader,\n} from '../../_modules/settings-card'\n\nexport default function BlockTaskViewer() {\n  const { tasks, clearTask } = useBlockTaskContext()\n\n  const handleClearAllTask = useLockFn(async () => {\n    Object.keys(tasks).forEach((key) => {\n      clearTask(key)\n    })\n  })\n\n  return (\n    <SettingsCard asChild>\n      <SettingsCardAnimatedItem>\n        <SettingsCardHeader>Block Task Viewer</SettingsCardHeader>\n\n        <SettingsCardContent>\n          {Object.entries(tasks).map(([key, task]) => (\n            <div key={key} className=\"flex items-center gap-2\">\n              <div className=\"flex-1\">Label: {key}</div>\n\n              <div>Status: {task.status}</div>\n\n              <Modal>\n                <ModalTrigger asChild>\n                  <Button variant=\"stroked\" className=\"h-8 min-w-0 px-3\">\n                    Detial\n                  </Button>\n                </ModalTrigger>\n\n                <ModalContent>\n                  <Card className=\"min-w-96\">\n                    <CardHeader>\n                      <ModalTitle>Task Detail</ModalTitle>\n                    </CardHeader>\n\n                    <CardContent>\n                      <pre className=\"overflow-auto font-mono select-text\">\n                        {JSON.stringify(task, null, 2)}\n                      </pre>\n                    </CardContent>\n\n                    <CardFooter className=\"gap-2\">\n                      <ModalClose>{m.common_close()}</ModalClose>\n\n                      <Button onClick={() => clearTask(key)}>Clear Task</Button>\n                    </CardFooter>\n                  </Card>\n                </ModalContent>\n              </Modal>\n            </div>\n          ))}\n        </SettingsCardContent>\n\n        <SettingsCardFooter>\n          <Button onClick={handleClearAllTask}>Clear All Task</Button>\n        </SettingsCardFooter>\n      </SettingsCardAnimatedItem>\n    </SettingsCard>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/settings/debug/_modules/debug-provider.tsx",
    "content": "import { createContext, PropsWithChildren, use, useState } from 'react'\n\nconst isDev = import.meta.env.DEV\n\nconst DebugContext = createContext<{\n  advanceTools: boolean\n  setAdvanceTools: (value: boolean) => void\n} | null>(null)\n\nexport const useDebugContext = () => {\n  const context = use(DebugContext)\n\n  if (!context) {\n    throw new Error('useDebugContext must be used within a DebugProvider')\n  }\n\n  return context\n}\n\nexport default function DebugProvider({ children }: PropsWithChildren) {\n  const [advanceTools, setAdvanceTools] = useState(isDev)\n\n  return (\n    <DebugContext.Provider\n      value={{\n        advanceTools,\n        setAdvanceTools,\n      }}\n    >\n      {children}\n    </DebugContext.Provider>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/settings/debug/_modules/kv-storage.tsx",
    "content": "import { Button } from '@/components/ui/button'\nimport { Card, CardContent, CardFooter, CardHeader } from '@/components/ui/card'\nimport {\n  Modal,\n  ModalClose,\n  ModalContent,\n  ModalTitle,\n  ModalTrigger,\n} from '@/components/ui/modal'\nimport { useLockFn } from '@/hooks/use-lock-fn'\nimport { m } from '@/paraglide/messages'\nimport { commands, unwrapResult } from '@nyanpasu/interface'\nimport { useQuery } from '@tanstack/react-query'\nimport {\n  SettingsCard,\n  SettingsCardAnimatedItem,\n  SettingsCardContent,\n  SettingsCardFooter,\n  SettingsCardHeader,\n} from '../../_modules/settings-card'\n\nexport default function KVStorage() {\n  const query = useQuery({\n    queryKey: ['kv-storage'],\n    queryFn: async () => {\n      const result = await commands.getAllStorageItems()\n\n      return unwrapResult(result)\n    },\n  })\n\n  const handleClearAllTask = useLockFn(async () => {\n    await commands.clearStorage()\n    await query.refetch()\n  })\n\n  return (\n    <SettingsCard asChild>\n      <SettingsCardAnimatedItem>\n        <SettingsCardHeader>KV Storage</SettingsCardHeader>\n\n        <SettingsCardContent>\n          <div className=\"flex items-center gap-1 select-text\">\n            <span className=\"font-medium\">Total Items:</span>\n\n            <span>{query.isLoading ? 'Loading...' : query.data?.length}</span>\n          </div>\n\n          {query.data &&\n            query.data.map((storage) => (\n              <div key={storage.key} className=\"flex items-center gap-2\">\n                <div className=\"flex-1\">Key: {storage.key}</div>\n\n                <Modal>\n                  <ModalTrigger asChild>\n                    <Button variant=\"stroked\" className=\"h-8 min-w-0 px-3\">\n                      Detail\n                    </Button>\n                  </ModalTrigger>\n\n                  <ModalContent>\n                    <Card className=\"min-w-96\">\n                      <CardHeader>\n                        <ModalTitle>Storage Detail</ModalTitle>\n                      </CardHeader>\n\n                      <CardContent>\n                        <pre className=\"overflow-auto font-mono select-text\">\n                          {JSON.stringify(storage, null, 2)}\n                        </pre>\n                      </CardContent>\n\n                      <CardFooter className=\"gap-2\">\n                        <ModalClose>{m.common_close()}</ModalClose>\n                      </CardFooter>\n                    </Card>\n                  </ModalContent>\n                </Modal>\n              </div>\n            ))}\n        </SettingsCardContent>\n\n        <SettingsCardFooter>\n          <Button onClick={handleClearAllTask}>Clear All Web Storage</Button>\n        </SettingsCardFooter>\n      </SettingsCardAnimatedItem>\n    </SettingsCard>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/settings/debug/_modules/path-utils-card.tsx",
    "content": "import { Button, ButtonProps } from '@/components/ui/button'\nimport TextMarquee from '@/components/ui/text-marquee'\nimport { useLockFn } from '@/hooks/use-lock-fn'\nimport { m } from '@/paraglide/messages'\nimport { commands } from '@nyanpasu/interface'\nimport { cn } from '@nyanpasu/ui'\n\nconst PathButton = ({\n  className,\n  children,\n  ...props\n}: Omit<ButtonProps, 'variant'>) => {\n  return (\n    <Button\n      variant=\"raised\"\n      className={cn(\n        'h-18 w-full rounded-3xl px-5 text-left font-bold',\n        className,\n      )}\n      {...props}\n    >\n      <TextMarquee>{children}</TextMarquee>\n    </Button>\n  )\n}\n\nexport default function PathUtilsCard() {\n  const handleOpenConfigDirectory = useLockFn(async () => {\n    await commands.openAppConfigDir()\n  })\n\n  const handleOpenDataDirectory = useLockFn(async () => {\n    await commands.openAppDataDir()\n  })\n\n  const handleOpenCoreDirectory = useLockFn(async () => {\n    await commands.openCoreDir()\n  })\n\n  const handleOpenLogDirectory = useLockFn(async () => {\n    await commands.openLogsDir()\n  })\n\n  return (\n    <div className=\"grid grid-cols-2 gap-2 md:grid-cols-4\">\n      <PathButton onClick={handleOpenConfigDirectory}>\n        {m.settings_debug_utils_open_config_directory()}\n      </PathButton>\n\n      <PathButton onClick={handleOpenDataDirectory}>\n        {m.settings_debug_utils_open_data_directory()}\n      </PathButton>\n\n      <PathButton onClick={handleOpenCoreDirectory}>\n        {m.settings_debug_utils_open_core_directory()}\n      </PathButton>\n\n      <PathButton onClick={handleOpenLogDirectory}>\n        {m.settings_debug_utils_open_log_directory()}\n      </PathButton>\n    </div>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/settings/debug/_modules/window-debug.tsx",
    "content": "import { Button } from '@/components/ui/button'\nimport { useLockFn } from '@/hooks/use-lock-fn'\nimport { commands } from '@nyanpasu/interface'\nimport { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'\nimport {\n  SettingsCard,\n  SettingsCardAnimatedItem,\n  SettingsCardContent,\n  SettingsCardHeader,\n} from '../../_modules/settings-card'\n\nconst currentWindow = getCurrentWebviewWindow()\n\nexport default function WindowDebug() {\n  const handleCreateLegacyWindow = useLockFn(async () => {\n    await commands.createLegacyWindow()\n  })\n\n  const handleCreateEditorWindow = useLockFn(async () => {\n    await commands.createEditorWindow('test')\n  })\n\n  return (\n    <SettingsCard asChild>\n      <SettingsCardAnimatedItem>\n        <SettingsCardHeader>Window Debug Utils</SettingsCardHeader>\n\n        <SettingsCardContent>\n          <div className=\"flex items-center gap-1 select-text\">\n            <span>Current Window Label:</span>\n            <span className=\"font-mono font-bold\">{currentWindow.label}</span>\n          </div>\n\n          <div className=\"flex items-center gap-2\">\n            <Button variant=\"flat\" onClick={handleCreateLegacyWindow}>\n              Create Legacy Window\n            </Button>\n\n            <Button variant=\"flat\" onClick={handleCreateEditorWindow}>\n              Create Test Editor Window\n            </Button>\n          </div>\n        </SettingsCardContent>\n      </SettingsCardAnimatedItem>\n    </SettingsCard>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/settings/debug/index.tsx",
    "content": "import { AnimatePresence } from 'framer-motion'\nimport { m } from '@/paraglide/messages'\nimport { createFileRoute } from '@tanstack/react-router'\nimport {\n  SettingsCard,\n  SettingsCardContent,\n  SettingsGroup,\n  SettingsLabel,\n} from '../_modules/settings-card'\nimport { SettingsTitle } from '../_modules/settings-title'\nimport AdvanceToolsSwitch from './_modules/advance-tools-switch'\nimport BlockTaskViewer from './_modules/block-task-viewer'\nimport { useDebugContext } from './_modules/debug-provider'\nimport KVStorage from './_modules/kv-storage'\nimport PathUtilsCard from './_modules/path-utils-card'\nimport WindowDebug from './_modules/window-debug'\n\nexport const Route = createFileRoute('/(main)/main/settings/debug/')({\n  component: RouteComponent,\n})\n\nconst PathUtilsSettings = () => {\n  return (\n    <div data-slot=\"debug-settings-container\">\n      <SettingsLabel>{m.settings_label_debug()}</SettingsLabel>\n\n      <PathUtilsCard />\n    </div>\n  )\n}\n\nconst AdvanceToolsSettings = () => {\n  const { advanceTools } = useDebugContext()\n\n  return (\n    <div data-slot=\"debug-settings-container\">\n      <SettingsLabel>Advance Tools</SettingsLabel>\n\n      <SettingsGroup>\n        <SettingsCard>\n          <SettingsCardContent>\n            <AdvanceToolsSwitch />\n          </SettingsCardContent>\n        </SettingsCard>\n\n        <AnimatePresence initial={false}>\n          {advanceTools && (\n            <>\n              <WindowDebug />\n\n              <BlockTaskViewer />\n\n              <KVStorage />\n            </>\n          )}\n        </AnimatePresence>\n      </SettingsGroup>\n    </div>\n  )\n}\n\nfunction RouteComponent() {\n  return (\n    <>\n      <SettingsTitle>{m.settings_label_debug()}</SettingsTitle>\n\n      <div className=\"space-y-4 px-4 pb-4\">\n        <PathUtilsSettings />\n\n        <AdvanceToolsSettings />\n      </div>\n    </>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/settings/debug/route.tsx",
    "content": "import { createFileRoute, Outlet } from '@tanstack/react-router'\nimport DebugProvider from './_modules/debug-provider'\n\nexport const Route = createFileRoute('/(main)/main/settings/debug')({\n  component: RouteComponent,\n})\n\nfunction RouteComponent() {\n  return (\n    <DebugProvider>\n      <Outlet />\n    </DebugProvider>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/settings/index.tsx",
    "content": "import { AppContentScrollArea } from '@/components/ui/scroll-area'\nimport useIsMobile from '@/hooks/use-is-moblie'\nimport { cn } from '@nyanpasu/ui'\nimport { createFileRoute } from '@tanstack/react-router'\nimport SettingsNavigate from './_modules/settings-navigate'\n\nexport const Route = createFileRoute('/(main)/main/settings/')({\n  component: RouteComponent,\n})\n\nfunction RouteComponent() {\n  const isMobile = useIsMobile()\n\n  if (!isMobile) {\n    return null\n  }\n\n  return (\n    <AppContentScrollArea\n      className={cn('bg-surface z-50 w-full [&>div>div]:block!')}\n      data-slot=\"settings-sidebar-scroll-area\"\n    >\n      <SettingsNavigate />\n    </AppContentScrollArea>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/settings/nyanpasu/_modules/log-file-config.tsx",
    "content": "import { useEffect, useState } from 'react'\nimport { Slider } from '@/components/ui/slider'\nimport { m } from '@/paraglide/messages'\nimport { useSetting } from '@nyanpasu/interface'\nimport { SettingsCard, SettingsCardContent } from '../../_modules/settings-card'\n\nconst MAX_LOG_FILES = 7\n\nexport default function LogFileConfig() {\n  const { value, upsert } = useSetting('max_log_files')\n\n  const committedValue = value ?? 1\n\n  const [cachedValue, setCachedValue] = useState(committedValue)\n\n  // sync the cached value with the committed value\n  useEffect(() => {\n    setCachedValue(committedValue)\n  }, [committedValue])\n\n  return (\n    <SettingsCard data-slot=\"log-file-config-card\">\n      <SettingsCardContent\n        data-slot=\"log-file-config-card-content\"\n        className=\"gap-4\"\n      >\n        <div className=\"flex items-center justify-between\">\n          <span>{m.settings_nyanpasu_max_log_files_label()}</span>\n\n          <span>{cachedValue}</span>\n        </div>\n\n        <Slider\n          value={cachedValue}\n          min={1}\n          max={MAX_LOG_FILES}\n          step={1}\n          onValueChange={(value) => {\n            setCachedValue(value)\n          }}\n          onValueCommit={(value) => {\n            if (value !== committedValue) {\n              upsert(value)\n            }\n          }}\n        />\n      </SettingsCardContent>\n    </SettingsCard>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/settings/nyanpasu/route.tsx",
    "content": "import { m } from '@/paraglide/messages'\nimport { createFileRoute } from '@tanstack/react-router'\nimport { SettingsLabel } from '../_modules/settings-card'\nimport { SettingsTitle } from '../_modules/settings-title'\nimport LogFileConfig from './_modules/log-file-config'\n\nexport const Route = createFileRoute('/(main)/main/settings/nyanpasu')({\n  component: RouteComponent,\n})\n\nconst AppSettings = () => {\n  return (\n    <div data-slot=\"app-settings-container\">\n      <SettingsLabel>{m.settings_label_nyanpasu()}</SettingsLabel>\n\n      <LogFileConfig />\n    </div>\n  )\n}\n\nfunction RouteComponent() {\n  return (\n    <>\n      <SettingsTitle>{m.settings_label_nyanpasu()}</SettingsTitle>\n\n      <div className=\"space-y-4 px-4 pb-4\">\n        <AppSettings />\n      </div>\n    </>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/settings/route.tsx",
    "content": "import { AnimatedOutletPreset } from '@/components/router/animated-outlet'\nimport { AppContentScrollArea } from '@/components/ui/scroll-area'\nimport { Sidebar, SidebarContent } from '@/components/ui/sidebar'\nimport { cn } from '@nyanpasu/ui'\nimport { createFileRoute } from '@tanstack/react-router'\nimport SettingsNavigate from './_modules/settings-navigate'\n\nexport const Route = createFileRoute('/(main)/main/settings')({\n  component: RouteComponent,\n})\n\nfunction RouteComponent() {\n  return (\n    <Sidebar data-slot=\"settings-container\">\n      <SidebarContent\n        className=\"bg-surface-variant/10 [&>div>div]:block!\"\n        data-slot=\"settings-sidebar-scroll-area\"\n      >\n        <SettingsNavigate />\n      </SidebarContent>\n\n      <AppContentScrollArea\n        className={cn(\n          'group/settings-content flex-[3_1_auto]',\n          'overflow-clip',\n        )}\n        data-slot=\"settings-content-scroll-area\"\n      >\n        <div\n          className={cn(\n            'container mx-auto w-full max-w-7xl',\n            'min-h-[calc(100vh-40px-64px)]',\n            'sm:min-h-[calc(100vh-40px-48px)]',\n          )}\n          data-slot=\"settings-content\"\n        >\n          <AnimatedOutletPreset />\n        </div>\n      </AppContentScrollArea>\n    </Sidebar>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/settings/system/_modules/auto-launch-switch.tsx",
    "content": "import { Switch } from '@/components/ui/switch'\nimport { useLockFn } from '@/hooks/use-lock-fn'\nimport { m } from '@/paraglide/messages'\nimport { formatError } from '@/utils'\nimport { message } from '@/utils/notification'\nimport { useSetting } from '@nyanpasu/interface'\nimport {\n  ItemContainer,\n  ItemLabel,\n  ItemLabelText,\n} from '../../_modules/settings-card'\n\nexport default function AutoLaunchSwitch() {\n  const autoLaunch = useSetting('enable_auto_launch')\n\n  const handleAutoLaunch = useLockFn(async () => {\n    try {\n      await autoLaunch.upsert(!autoLaunch.value)\n    } catch (error) {\n      message(`Activation Auto Launch failed!\\n Error: ${formatError(error)}`, {\n        title: 'Error',\n        kind: 'error',\n      })\n    }\n  })\n\n  return (\n    <ItemContainer data-slot=\"auto-launch-switch-container\">\n      <ItemLabel>\n        <ItemLabelText>\n          {m.settings_system_proxy_auto_launch_label()}\n        </ItemLabelText>\n      </ItemLabel>\n\n      <Switch\n        checked={Boolean(autoLaunch.value)}\n        onCheckedChange={handleAutoLaunch}\n        loading={autoLaunch.isPending}\n      />\n    </ItemContainer>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/settings/system/_modules/current-system-proxy.tsx",
    "content": "import { Card, CardContent } from '@/components/ui/card'\nimport { m } from '@/paraglide/messages'\nimport { useSystemProxy } from '@nyanpasu/interface'\n\nexport default function CurrentSystemProxy() {\n  const { data } = useSystemProxy()\n\n  return (\n    <div\n      data-slot=\"current-system-proxy-container\"\n      className=\"flex flex-col gap-0.5 select-text\"\n    >\n      {Object.entries(data ?? []).map(([key, value], index) => {\n        return (\n          <div key={index} className=\"flex w-full leading-8\">\n            <div className=\"w-28 capitalize\">{key}:</div>\n\n            <div className=\"text-warp flex-1 break-all\">{String(value)}</div>\n          </div>\n        )\n      })}\n    </div>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/settings/system/_modules/proxy-bypass-config.tsx",
    "content": "import { AnimatePresence } from 'framer-motion'\nimport { ChangeEvent, useCallback, useEffect } from 'react'\nimport { Controller, useForm } from 'react-hook-form'\nimport { z } from 'zod'\nimport { Button } from '@/components/ui/button'\nimport { Input } from '@/components/ui/input'\nimport { m } from '@/paraglide/messages'\nimport { formatError } from '@/utils'\nimport { message } from '@/utils/notification'\nimport { zodResolver } from '@hookform/resolvers/zod'\nimport { useSetting } from '@nyanpasu/interface'\nimport { SettingsCardAnimatedItem } from '../../_modules/settings-card'\n\nconst DEFAULT_BYPASS =\n  'localhost;127.;192.168.;10.;' +\n  '172.16.;172.17.;172.18.;172.19.;172.20.;172.21.;172.22.;172.23.;' +\n  '172.24.;172.25.;172.26.;172.27.;172.28.;172.29.;172.30.;172.31.*'\n\nconst formSchema = z.object({\n  systemProxyBypass: z.string().nullable().optional(),\n})\n\nexport default function ProxyBypassConfig() {\n  const systemProxyBypass = useSetting('system_proxy_bypass')\n\n  const form = useForm<z.infer<typeof formSchema>>({\n    resolver: zodResolver(formSchema),\n    defaultValues: {\n      systemProxyBypass: systemProxyBypass.value,\n    },\n  })\n\n  useEffect(() => {\n    form.setValue('systemProxyBypass', systemProxyBypass.value)\n  }, [systemProxyBypass.value, form])\n\n  const handleSubmit = form.handleSubmit(async (data) => {\n    try {\n      await systemProxyBypass.upsert(data.systemProxyBypass || DEFAULT_BYPASS)\n\n      form.reset({\n        systemProxyBypass: data.systemProxyBypass || DEFAULT_BYPASS,\n      })\n    } catch (error) {\n      message(formatError(error), {\n        title: 'Error',\n        kind: 'error',\n      })\n    }\n  })\n\n  const handleReset = useCallback(() => {\n    form.reset({\n      systemProxyBypass: systemProxyBypass.value,\n    })\n  }, [systemProxyBypass.value, form])\n\n  return (\n    <form className=\"flex flex-col gap-2\" onSubmit={handleSubmit}>\n      <Controller\n        control={form.control}\n        name=\"systemProxyBypass\"\n        render={({ field }) => {\n          const handleChange = (event: ChangeEvent<HTMLInputElement>) => {\n            field.onChange(event.target.value)\n          }\n\n          return (\n            <>\n              <Input\n                variant=\"outlined\"\n                label={m.settings_system_proxy_proxy_bypass_label()}\n                value={field.value ?? ''}\n                onChange={handleChange}\n              />\n\n              {form.formState.errors.systemProxyBypass && (\n                <SettingsCardAnimatedItem className=\"text-error\">\n                  {form.formState.errors.systemProxyBypass.message}\n                </SettingsCardAnimatedItem>\n              )}\n            </>\n          )\n        }}\n      />\n\n      <AnimatePresence initial={false}>\n        {form.formState.isDirty && (\n          <SettingsCardAnimatedItem>\n            <div className=\"flex justify-end gap-2 pt-1\">\n              <Button type=\"button\" onClick={handleReset}>\n                {m.common_reset()}\n              </Button>\n\n              <Button\n                variant=\"raised\"\n                onClick={handleSubmit}\n                loading={form.formState.isSubmitting}\n              >\n                {m.common_apply()}\n              </Button>\n            </div>\n          </SettingsCardAnimatedItem>\n        )}\n      </AnimatePresence>\n    </form>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/settings/system/_modules/proxy-guard-config.tsx",
    "content": "import { AnimatePresence } from 'framer-motion'\nimport { isNumber } from 'lodash-es'\nimport { useCallback, useEffect } from 'react'\nimport { Controller, useForm } from 'react-hook-form'\nimport { z } from 'zod'\nimport { Button } from '@/components/ui/button'\nimport { NumericInput } from '@/components/ui/input'\nimport { m } from '@/paraglide/messages'\nimport { formatError } from '@/utils'\nimport { message } from '@/utils/notification'\nimport { zodResolver } from '@hookform/resolvers/zod'\nimport { useSetting } from '@nyanpasu/interface'\nimport { SettingsCardAnimatedItem } from '../../_modules/settings-card'\n\nconst formSchema = z.object({\n  proxyGuardInterval: z.number().min(1),\n})\n\nexport default function ProxyGuardConfig() {\n  const proxyGuardInterval = useSetting('proxy_guard_interval')\n\n  const form = useForm<z.infer<typeof formSchema>>({\n    resolver: zodResolver(formSchema),\n    defaultValues: {\n      proxyGuardInterval: proxyGuardInterval.value || 1,\n    },\n  })\n\n  useEffect(() => {\n    if (isNumber(proxyGuardInterval.value)) {\n      form.setValue('proxyGuardInterval', proxyGuardInterval.value)\n    }\n  }, [proxyGuardInterval.value, form])\n\n  const handleSubmit = form.handleSubmit(async (data) => {\n    try {\n      await proxyGuardInterval.upsert(data.proxyGuardInterval)\n\n      form.reset({\n        proxyGuardInterval: data.proxyGuardInterval,\n      })\n    } catch (error) {\n      message(formatError(error), {\n        title: 'Error',\n        kind: 'error',\n      })\n    }\n  })\n\n  const handleReset = useCallback(() => {\n    form.reset({\n      proxyGuardInterval: proxyGuardInterval.value || 1,\n    })\n  }, [proxyGuardInterval.value, form])\n\n  return (\n    <form className=\"flex flex-col gap-2\" onSubmit={handleSubmit}>\n      <Controller\n        name=\"proxyGuardInterval\"\n        control={form.control}\n        render={({ field }) => {\n          const handleChange = (value: number | null) => {\n            field.onChange(value)\n          }\n\n          return (\n            <>\n              <NumericInput\n                variant=\"outlined\"\n                label={m.settings_system_proxy_proxy_guard_interval_label()}\n                value={field.value || 0}\n                onChange={handleChange}\n              />\n\n              <AnimatePresence initial={false}>\n                {form.formState.errors.proxyGuardInterval && (\n                  <SettingsCardAnimatedItem className=\"text-error\">\n                    {form.formState.errors.proxyGuardInterval.message}\n                  </SettingsCardAnimatedItem>\n                )}\n              </AnimatePresence>\n            </>\n          )\n        }}\n      />\n\n      <AnimatePresence initial={false}>\n        {form.formState.isDirty && (\n          <SettingsCardAnimatedItem>\n            <div className=\"flex justify-end gap-2 pt-1\">\n              <Button type=\"button\" onClick={handleReset}>\n                {m.common_reset()}\n              </Button>\n\n              <Button\n                variant=\"raised\"\n                onClick={handleSubmit}\n                loading={form.formState.isSubmitting}\n              >\n                {m.common_apply()}\n              </Button>\n            </div>\n          </SettingsCardAnimatedItem>\n        )}\n      </AnimatePresence>\n    </form>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/settings/system/_modules/proxy-guard-switch.tsx",
    "content": "import { Switch } from '@/components/ui/switch'\nimport { useLockFn } from '@/hooks/use-lock-fn'\nimport { m } from '@/paraglide/messages'\nimport { formatError } from '@/utils'\nimport { message } from '@/utils/notification'\nimport { useSetting } from '@nyanpasu/interface'\nimport {\n  ItemContainer,\n  ItemLabel,\n  ItemLabelDescription,\n  ItemLabelText,\n} from '../../_modules/settings-card'\n\nexport default function ProxyGuardSwitch() {\n  const proxyGuard = useSetting('enable_proxy_guard')\n\n  const handleProxyGuard = useLockFn(async () => {\n    try {\n      await proxyGuard.upsert(!proxyGuard.value)\n    } catch (error) {\n      message(`Activation Proxy Guard failed!\\n Error: ${formatError(error)}`, {\n        title: 'Error',\n        kind: 'error',\n      })\n    }\n  })\n\n  return (\n    <ItemContainer data-slot=\"proxy-guard-switch-container\">\n      <ItemLabel>\n        <ItemLabelText>\n          {m.settings_system_proxy_proxy_guard_switch_label()}\n        </ItemLabelText>\n\n        <ItemLabelDescription>\n          {m.settings_system_proxy_proxy_guard_switch_description()}\n        </ItemLabelDescription>\n      </ItemLabel>\n\n      <Switch\n        checked={Boolean(proxyGuard.value)}\n        onCheckedChange={handleProxyGuard}\n        loading={proxyGuard.isPending}\n      />\n    </ItemContainer>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/settings/system/_modules/slient-launch-switch.tsx",
    "content": "import { Switch } from '@/components/ui/switch'\nimport { useLockFn } from '@/hooks/use-lock-fn'\nimport { m } from '@/paraglide/messages'\nimport { formatError } from '@/utils'\nimport { message } from '@/utils/notification'\nimport { useSetting } from '@nyanpasu/interface'\nimport {\n  ItemContainer,\n  ItemLabel,\n  ItemLabelText,\n} from '../../_modules/settings-card'\n\nexport default function SilentLaunchSwitch() {\n  const silentStart = useSetting('enable_silent_start')\n\n  const handleSilentStart = useLockFn(async () => {\n    try {\n      await silentStart.upsert(!silentStart.value)\n    } catch (error) {\n      message(\n        `Activation Silent Start failed!\\n Error: ${formatError(error)}`,\n        {\n          title: 'Error',\n          kind: 'error',\n        },\n      )\n    }\n  })\n\n  return (\n    <ItemContainer data-slot=\"silent-launch-switch-container\">\n      <ItemLabel>\n        <ItemLabelText>\n          {m.settings_system_proxy_silent_start_label()}\n        </ItemLabelText>\n      </ItemLabel>\n\n      <Switch\n        checked={Boolean(silentStart.value)}\n        onCheckedChange={handleSilentStart}\n        loading={silentStart.isPending}\n      />\n    </ItemContainer>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/settings/system/_modules/system-service-ctrl.tsx",
    "content": "import { startCase } from 'lodash-es'\nimport { useEffect, useMemo, useState } from 'react'\nimport { Button } from '@/components/ui/button'\nimport { Card, CardContent, CardFooter, CardHeader } from '@/components/ui/card'\nimport {\n  Modal,\n  ModalClose,\n  ModalContent,\n  ModalTitle,\n  ModalTrigger,\n} from '@/components/ui/modal'\nimport { OS } from '@/consts'\nimport { useLockFn } from '@/hooks/use-lock-fn'\nimport { m } from '@/paraglide/messages'\nimport { formatError } from '@/utils'\nimport { message } from '@/utils/notification'\nimport { getShikiSingleton } from '@/utils/shiki'\nimport {\n  commands,\n  useCoreDir,\n  useServicePrompt,\n  useSystemService,\n} from '@nyanpasu/interface'\nimport { cn } from '@nyanpasu/ui'\nimport { writeText } from '@tauri-apps/plugin-clipboard-manager'\nimport {\n  SettingsCard,\n  SettingsCardContent,\n  SettingsCardFooter,\n} from '../../_modules/settings-card'\n\nconst SystemServiceCtrlItem = ({\n  name,\n  value,\n}: {\n  name: string\n  value?: string\n}) => {\n  return (\n    <div className=\"flex w-full leading-8\" data-slot=\"system-service-ctrl-item\">\n      <div\n        className=\"w-32 capitalize\"\n        data-slot=\"system-service-ctrl-item-name\"\n      >\n        {name}:\n      </div>\n\n      <div\n        className=\"text-warp flex-1 break-all\"\n        data-slot=\"system-service-ctrl-item-value\"\n      >\n        {value ?? '-'}\n      </div>\n    </div>\n  )\n}\n\nconst ServiceDetailButton = () => {\n  const { query } = useSystemService()\n\n  return (\n    <Modal>\n      <ModalTrigger asChild>\n        <Button data-slot=\"system-service-detail-button\">\n          {m.settings_system_proxy_system_service_ctrl_detail()}\n        </Button>\n      </ModalTrigger>\n\n      <ModalContent>\n        <Card className=\"w-96\">\n          <CardHeader>\n            <ModalTitle>\n              {m.settings_system_proxy_system_service_ctrl_detail()}\n            </ModalTitle>\n          </CardHeader>\n\n          <CardContent>\n            <pre className=\"overflow-auto font-mono select-text\">\n              {JSON.stringify(query.data, null, 2)}\n            </pre>\n          </CardContent>\n\n          <CardFooter>\n            <ModalClose>{m.common_close()}</ModalClose>\n          </CardFooter>\n        </Card>\n      </ModalContent>\n    </Modal>\n  )\n}\n\nconst ServiceInstallButton = () => {\n  const { upsert } = useSystemService()\n\n  const handleInstallClick = useLockFn(async () => {\n    try {\n      await upsert.mutateAsync('install')\n      await commands.restartSidecar()\n    } catch (e) {\n      const errorMessage = `${m.settings_system_proxy_system_service_ctrl_failed_install()}: ${formatError(e)}`\n\n      message(errorMessage, {\n        kind: 'error',\n      })\n      // // If the installation fails, prompt the user to manually install the service\n      // promptDialog.show(\n      //   query.data?.status === 'not_installed' ? 'install' : 'uninstall',\n      // )\n    }\n  })\n\n  return (\n    <Button\n      variant=\"flat\"\n      onClick={handleInstallClick}\n      loading={upsert.isPending}\n    >\n      {m.settings_system_proxy_system_service_ctrl_install()}\n    </Button>\n  )\n}\n\nconst ServiceUninstallButton = () => {\n  const { upsert } = useSystemService()\n\n  const handleUninstallClick = useLockFn(async () => {\n    await upsert.mutateAsync('uninstall')\n  })\n\n  return (\n    <Button onClick={handleUninstallClick} loading={upsert.isPending}>\n      {m.settings_system_proxy_system_service_ctrl_uninstall()}\n    </Button>\n  )\n}\n// {\n//   operation: 'uninstall' | 'install' | 'start' | 'stop' | null\n// }\nconst ServicePromptButton = () => {\n  const {\n    query: { data: systemService },\n  } = useSystemService()\n\n  const { data: serviceInstallPrompt } = useServicePrompt()\n\n  const { data: coreDir } = useCoreDir()\n\n  const [codes, setCodes] = useState<string | null>(null)\n\n  const userOperationCommands = useMemo(() => {\n    if (systemService?.status === 'not_installed' && serviceInstallPrompt) {\n      return `cd \"${coreDir}\"\\n${serviceInstallPrompt}`\n    } else if (systemService?.status) {\n      const operation = systemService?.status === 'running' ? 'stop' : 'start'\n\n      return `cd \"${coreDir}\"\\n${OS !== 'windows' ? 'sudo ' : ''}./nyanpasu-service ${operation}`\n    }\n    return ''\n  }, [systemService?.status, serviceInstallPrompt, coreDir])\n\n  useEffect(() => {\n    const handleGenerateCodes = async () => {\n      const shiki = await getShikiSingleton()\n      const code = shiki.codeToHtml(userOperationCommands, {\n        lang: 'shell',\n        themes: {\n          dark: 'nord',\n          light: 'min-light',\n        },\n      })\n\n      setCodes(code)\n    }\n\n    handleGenerateCodes()\n  }, [userOperationCommands])\n\n  const handleCopyToClipboard = useLockFn(async () => {\n    if (!userOperationCommands) {\n      return\n    }\n\n    await writeText(userOperationCommands)\n  })\n\n  return (\n    <Modal>\n      <ModalTrigger asChild>\n        <Button variant=\"flat\">\n          {m.settings_system_proxy_system_service_ctrl_prompt()}\n        </Button>\n      </ModalTrigger>\n\n      <ModalContent>\n        <Card className=\"max-w-3xl min-w-96\">\n          <CardHeader>\n            <ModalTitle>\n              {m.settings_system_proxy_system_service_ctrl_manual_prompt()}\n            </ModalTitle>\n          </CardHeader>\n\n          <CardContent>\n            <p className=\"leading-6\">\n              {m.settings_system_proxy_system_service_ctrl_manual_operation_prompt()}\n            </p>\n\n            {codes && (\n              <div\n                className={cn(\n                  'overflow-clip rounded select-text',\n                  '[&>pre]:overflow-auto [&>pre]:p-2',\n                  '[&>pre]:bg-surface-variant! dark:[&>pre]:bg-black!',\n                )}\n                dangerouslySetInnerHTML={{\n                  __html: codes,\n                }}\n              />\n            )}\n          </CardContent>\n\n          <CardFooter className=\"gap-2\">\n            <Button variant=\"flat\" onClick={handleCopyToClipboard}>\n              {m.common_copy()}\n            </Button>\n\n            <ModalClose>{m.common_close()}</ModalClose>\n          </CardFooter>\n        </Card>\n      </ModalContent>\n    </Modal>\n  )\n}\n\nconst ServiceControlButtons = () => {\n  const { query, upsert } = useSystemService()\n\n  const handleToggleClick = useLockFn(async () => {\n    await upsert.mutateAsync(\n      query.data?.status === 'running' ? 'stop' : 'start',\n    )\n  })\n\n  return (\n    <Button\n      variant=\"flat\"\n      onClick={handleToggleClick}\n      loading={upsert.isPending}\n    >\n      {query.data?.status === 'running'\n        ? m.settings_system_proxy_system_service_ctrl_stop()\n        : m.settings_system_proxy_system_service_ctrl_start()}\n    </Button>\n  )\n}\n\nexport default function SystemServiceCtrl() {\n  const { query } = useSystemService()\n\n  const isInstalled = query.data?.status !== 'not_installed'\n\n  return (\n    <SettingsCard>\n      <SettingsCardContent className=\"gap-2 py-4\">\n        <SystemServiceCtrlItem name=\"Service Name\" value={query.data?.name} />\n\n        <SystemServiceCtrlItem\n          name=\"Server Version\"\n          value={query.data?.server?.version}\n        />\n\n        <SystemServiceCtrlItem\n          name=\"Service Status\"\n          value={startCase(query.data?.status)}\n        />\n      </SettingsCardContent>\n\n      <SettingsCardFooter className=\"flex-wrap-reverse gap-2\">\n        {isInstalled ? (\n          <>\n            <ServiceControlButtons />\n\n            <ServiceUninstallButton />\n          </>\n        ) : (\n          <ServiceInstallButton />\n        )}\n\n        <ServiceDetailButton />\n\n        <div className=\"flex-1\" />\n\n        <ServicePromptButton />\n      </SettingsCardFooter>\n    </SettingsCard>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/settings/system/_modules/system-service-switch.tsx",
    "content": "import { Switch } from '@/components/ui/switch'\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipTrigger,\n} from '@/components/ui/tooltip'\nimport { useLockFn } from '@/hooks/use-lock-fn'\nimport { m } from '@/paraglide/messages'\nimport { formatError } from '@/utils'\nimport { message } from '@/utils/notification'\nimport { useSetting, useSystemService } from '@nyanpasu/interface'\nimport {\n  ItemContainer,\n  ItemLabel,\n  ItemLabelDescription,\n  ItemLabelText,\n} from '../../_modules/settings-card'\n\nexport default function SystemServiceSwitch() {\n  const serviceMode = useSetting('enable_service_mode')\n\n  const { query } = useSystemService()\n\n  const disabled = query.data?.status === 'not_installed'\n\n  const handleServiceMode = useLockFn(async () => {\n    try {\n      await serviceMode.upsert(!serviceMode.value)\n    } catch (error) {\n      message(\n        `Activation Service Mode failed!\\n Error: ${formatError(error)}`,\n        {\n          title: 'Error',\n          kind: 'error',\n        },\n      )\n    }\n  })\n\n  return (\n    <ItemContainer data-slot=\"system-service-switch-container\">\n      <ItemLabel>\n        <ItemLabelText>\n          {m.settings_system_proxy_service_mode_label()}\n        </ItemLabelText>\n\n        <ItemLabelDescription>\n          {m.settings_system_proxy_service_mode_description()}\n        </ItemLabelDescription>\n      </ItemLabel>\n\n      <Tooltip>\n        <TooltipTrigger asChild>\n          <div data-slot=\"system-service-switch-trigger-wrapper\">\n            <Switch\n              checked={Boolean(serviceMode.value)}\n              onCheckedChange={handleServiceMode}\n              loading={serviceMode.isPending}\n              disabled={disabled}\n            />\n          </div>\n        </TooltipTrigger>\n\n        {disabled && (\n          <TooltipContent>\n            <span>\n              {m.settings_system_proxy_service_mode_disabled_tooltip()}\n            </span>\n          </TooltipContent>\n        )}\n      </Tooltip>\n    </ItemContainer>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/settings/system/_modules/uwp-tools-button.tsx",
    "content": "import ArrowForwardIosRounded from '~icons/material-symbols/arrow-forward-ios-rounded'\nimport { Button } from '@/components/ui/button'\nimport { useLockFn } from '@/hooks/use-lock-fn'\nimport { m } from '@/paraglide/messages'\nimport { commands } from '@nyanpasu/interface'\nimport {\n  ItemContainer,\n  ItemLabel,\n  ItemLabelDescription,\n  ItemLabelText,\n  SettingsCard,\n  SettingsCardContent,\n} from '../../_modules/settings-card'\n\nexport default function UwpToolsButton() {\n  const handleOpenUwpTools = useLockFn(async () => {\n    await commands.invokeUwpTool()\n  })\n\n  return (\n    <SettingsCard data-slot=\"uwp-tools-button-card\">\n      <SettingsCardContent asChild>\n        <Button\n          className=\"text-on-surface! h-auto w-full rounded-none px-5 text-left text-base\"\n          onClick={handleOpenUwpTools}\n        >\n          <ItemContainer>\n            <ItemLabel>\n              <ItemLabelText>\n                {m.settings_system_proxy_uwp_tools_label()}\n              </ItemLabelText>\n\n              <ItemLabelDescription>\n                {m.settings_system_proxy_uwp_tools_description()}\n              </ItemLabelDescription>\n            </ItemLabel>\n\n            <div>\n              <ArrowForwardIosRounded />\n            </div>\n          </ItemContainer>\n        </Button>\n      </SettingsCardContent>\n    </SettingsCard>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/settings/system/route.tsx",
    "content": "import { AnimatePresence } from 'framer-motion'\nimport {\n  SystemProxyButton,\n  TunModeButton,\n} from '@/components/settings/system-proxy'\nimport { isWindows } from '@/consts'\nimport { m } from '@/paraglide/messages'\nimport { useSetting } from '@nyanpasu/interface'\nimport { createFileRoute } from '@tanstack/react-router'\nimport {\n  SettingsCard,\n  SettingsCardAnimatedItem,\n  SettingsCardContent,\n  SettingsGroup,\n  SettingsLabel,\n} from '../_modules/settings-card'\nimport { SettingsTitle } from '../_modules/settings-title'\nimport AutoLaunchSwitch from './_modules/auto-launch-switch'\nimport CurrentSystemProxy from './_modules/current-system-proxy'\nimport ProxyBypassConfig from './_modules/proxy-bypass-config'\nimport ProxyGuardConfig from './_modules/proxy-guard-config'\nimport ProxyGuardSwitch from './_modules/proxy-guard-switch'\nimport SilentLaunchSwitch from './_modules/slient-launch-switch'\nimport SystemServiceCtrl from './_modules/system-service-ctrl'\nimport SystemServiceSwitch from './_modules/system-service-switch'\nimport UwpToolsButton from './_modules/uwp-tools-button'\n\nexport const Route = createFileRoute('/(main)/main/settings/system')({\n  component: RouteComponent,\n})\n\nconst ProxyMode = () => {\n  return (\n    <div data-slot=\"proxy-mode-container\">\n      <SettingsLabel>\n        {m.settings_system_proxy_proxy_mode_label()}\n      </SettingsLabel>\n\n      <SettingsGroup>\n        <div className=\"grid grid-cols-2 gap-2\">\n          <SystemProxyButton />\n\n          <TunModeButton />\n        </div>\n      </SettingsGroup>\n    </div>\n  )\n}\n\nconst ProxyGuard = () => {\n  const { value } = useSetting('enable_proxy_guard')\n\n  return (\n    <div data-slot=\"proxy-guard-container\">\n      <SettingsLabel>\n        {m.settings_system_proxy_proxy_guard_label()}\n      </SettingsLabel>\n\n      <SettingsGroup>\n        <SettingsCard>\n          <SettingsCardContent>\n            <ProxyGuardSwitch />\n          </SettingsCardContent>\n        </SettingsCard>\n\n        <AnimatePresence initial={false}>\n          {value && (\n            <SettingsCard asChild>\n              <SettingsCardAnimatedItem>\n                <SettingsCardContent>\n                  <ProxyGuardConfig />\n\n                  <ProxyBypassConfig />\n                </SettingsCardContent>\n              </SettingsCardAnimatedItem>\n            </SettingsCard>\n          )}\n        </AnimatePresence>\n      </SettingsGroup>\n    </div>\n  )\n}\n\nconst CurrentProxy = () => {\n  return (\n    <div data-slot=\"current-system-proxy-container\">\n      <SettingsLabel>\n        {m.settings_system_proxy_current_system_proxy_label()}\n      </SettingsLabel>\n\n      <SettingsGroup>\n        <SettingsCard>\n          <SettingsCardContent className=\"py-4\">\n            <CurrentSystemProxy />\n          </SettingsCardContent>\n        </SettingsCard>\n      </SettingsGroup>\n    </div>\n  )\n}\n\nconst SystemService = () => {\n  return (\n    <div data-slot=\"system-service-container\">\n      <SettingsLabel>\n        {m.settings_system_proxy_system_service_ctrl_label()}\n      </SettingsLabel>\n\n      <SettingsGroup>\n        <SettingsCard>\n          <SettingsCardContent>\n            <SystemServiceSwitch />\n          </SettingsCardContent>\n        </SettingsCard>\n\n        <SystemServiceCtrl />\n      </SettingsGroup>\n    </div>\n  )\n}\n\nconst SystemLaunch = () => {\n  return (\n    <div data-slot=\"system-launch-container\">\n      <SettingsLabel>{m.settings_system_proxy_launch_label()}</SettingsLabel>\n\n      <SettingsGroup>\n        <SettingsCard>\n          <SettingsCardContent>\n            <AutoLaunchSwitch />\n          </SettingsCardContent>\n        </SettingsCard>\n\n        <SettingsCard>\n          <SettingsCardContent>\n            <SilentLaunchSwitch />\n          </SettingsCardContent>\n        </SettingsCard>\n      </SettingsGroup>\n    </div>\n  )\n}\n\nconst WindowsTools = () => {\n  return (\n    <div data-slot=\"windows-tools-container\">\n      <SettingsLabel>\n        {m.settings_system_proxy_windows_tools_label()}\n      </SettingsLabel>\n\n      <SettingsGroup>\n        <UwpToolsButton />\n      </SettingsGroup>\n    </div>\n  )\n}\n\nfunction RouteComponent() {\n  return (\n    <>\n      <SettingsTitle>{m.settings_label_system()}</SettingsTitle>\n\n      <div className=\"space-y-4 px-4 pb-4\">\n        <ProxyMode />\n\n        <ProxyGuard />\n\n        <CurrentProxy />\n\n        <SystemService />\n\n        <SystemLaunch />\n\n        {isWindows && <WindowsTools />}\n      </div>\n    </>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/settings/user-interface/_modules/language-selector.tsx",
    "content": "import { useLanguage } from '@/components/providers/language-provider'\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from '@/components/ui/select'\nimport { m } from '@/paraglide/messages'\nimport { Locale, locales } from '@/paraglide/runtime'\n\nexport default function LanguageSelector() {\n  const { language, setLanguage } = useLanguage()\n\n  const handleLanguageChange = (value: string) => {\n    setLanguage(value as Locale)\n  }\n\n  return (\n    <Select\n      variant=\"outlined\"\n      value={language}\n      onValueChange={handleLanguageChange}\n    >\n      <SelectTrigger>\n        <SelectValue placeholder={m.settings_user_interface_language_label()}>\n          {language ? m.language(language, { locale: language }) : null}\n        </SelectValue>\n      </SelectTrigger>\n\n      <SelectContent>\n        {Object.entries(locales).map(([key, value]) => (\n          <SelectItem key={key} value={value}>\n            {m.language(key, { locale: value })}\n          </SelectItem>\n        ))}\n      </SelectContent>\n    </Select>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/settings/user-interface/_modules/switch-legacy.tsx",
    "content": "import { Button } from '@/components/ui/button'\nimport { Card, CardContent, CardFooter, CardHeader } from '@/components/ui/card'\nimport {\n  Modal,\n  ModalClose,\n  ModalContent,\n  ModalTitle,\n  ModalTrigger,\n} from '@/components/ui/modal'\nimport { useLockFn } from '@/hooks/use-lock-fn'\nimport { commands, useSetting } from '@nyanpasu/interface'\nimport { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'\nimport { SettingsCard, SettingsCardContent } from '../../_modules/settings-card'\n\nconst currentWindow = getCurrentWebviewWindow()\n\nexport default function SwitchLegacy() {\n  const { upsert } = useSetting('use_legacy_ui')\n\n  const handleClick = useLockFn(async () => {\n    await upsert(true)\n    await commands.createLegacyWindow()\n    await currentWindow.close()\n  })\n\n  return (\n    <SettingsCard data-slot=\"switch-legacy-card\">\n      <SettingsCardContent\n        className=\"flex items-center justify-between px-2\"\n        data-slot=\"switch-legacy-card-content\"\n      >\n        <Card className=\"w-full space-y-4\">\n          <CardHeader>Switch to Legacy UI</CardHeader>\n\n          <CardFooter>\n            <Modal>\n              <ModalTrigger asChild>\n                <Button variant=\"stroked\">Open</Button>\n              </ModalTrigger>\n\n              <ModalContent>\n                <Card className=\"w-96\">\n                  <CardHeader>\n                    <ModalTitle>\n                      Are you sure you want to switch to Legacy UI?\n                    </ModalTitle>\n                  </CardHeader>\n\n                  <CardContent>\n                    <p>\n                      Switching to Legacy UI will revert the UI to the original\n                      design.\n                    </p>\n                  </CardContent>\n\n                  <CardFooter className=\"gap-2\">\n                    <Button variant=\"flat\" onClick={handleClick}>\n                      Continue\n                    </Button>\n\n                    <ModalClose asChild>\n                      <Button>Cancel</Button>\n                    </ModalClose>\n                  </CardFooter>\n                </Card>\n              </ModalContent>\n            </Modal>\n          </CardFooter>\n        </Card>\n      </SettingsCardContent>\n    </SettingsCard>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/settings/user-interface/_modules/theme-color-config.tsx",
    "content": "import Check from '~icons/material-symbols/check-rounded'\nimport { useCallback, useState } from 'react'\nimport {\n  DEFAULT_COLOR,\n  useExperimentalThemeContext,\n} from '@/components/providers/theme-provider'\nimport { Button } from '@/components/ui/button'\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuTrigger,\n} from '@/components/ui/dropdown-menu'\nimport { m } from '@/paraglide/messages'\nimport { Wheel } from '@uiw/react-color'\nimport {\n  SettingsCard,\n  SettingsCardContent,\n  SettingsCardFooter,\n  SettingsCardHeader,\n} from '../../_modules/settings-card'\n\nconst PERSETS = [\n  DEFAULT_COLOR,\n  '#9e1e67',\n  '#3d009e',\n  '#00089e',\n  '#066b9e',\n  '#9e5a00',\n] as const\n\nexport default function ThemeColorConfig() {\n  const { themeColor, setThemeColor } = useExperimentalThemeContext()\n\n  const [open, setOpen] = useState(false)\n\n  const [cachedThemeColor, setCachedThemeColor] = useState(themeColor)\n\n  const handleSubmit = useCallback(async () => {\n    setOpen(false)\n    await setThemeColor(cachedThemeColor)\n  }, [cachedThemeColor, setThemeColor])\n\n  return (\n    <SettingsCard>\n      <SettingsCardHeader>\n        {m.settings_user_interface_theme_color_label()}\n      </SettingsCardHeader>\n\n      <SettingsCardContent>\n        <div className=\"flex flex-wrap gap-2\">\n          {PERSETS.map((color) => (\n            <Button\n              key={color}\n              className=\"flex items-center gap-2 px-4\"\n              variant={themeColor === color ? 'flat' : 'stroked'}\n              onClick={() => setThemeColor(color)}\n            >\n              <span\n                className=\"outline-surface-variant size-4 rounded outline\"\n                style={{ backgroundColor: color }}\n              />\n\n              <span>{color.toLocaleUpperCase()}</span>\n            </Button>\n          ))}\n        </div>\n      </SettingsCardContent>\n\n      <SettingsCardFooter>\n        <DropdownMenu open={open} onOpenChange={setOpen}>\n          <DropdownMenuTrigger asChild>\n            <Button className=\"flex items-center gap-2 px-4\" variant=\"flat\">\n              <span\n                className=\"outline-surface-variant size-4 rounded outline\"\n                style={{\n                  backgroundColor: themeColor,\n                }}\n              />\n\n              <span>\n                {PERSETS.includes(themeColor as (typeof PERSETS)[number])\n                  ? m.settings_user_interface_theme_color_custom()\n                  : themeColor.toLocaleUpperCase()}\n              </span>\n            </Button>\n          </DropdownMenuTrigger>\n\n          <DropdownMenuContent className=\"flex flex-col gap-4 rounded-2xl p-4\">\n            <Wheel\n              data-slot=\"theme-color-config-colorful\"\n              color={cachedThemeColor}\n              onChange={(color) => {\n                setCachedThemeColor(color.hex)\n              }}\n            />\n\n            <Button\n              className=\"flex items-center justify-center gap-2\"\n              variant=\"flat\"\n              onClick={handleSubmit}\n            >\n              <Check className=\"size-5\" />\n              <span>{m.common_submit()}</span>\n            </Button>\n          </DropdownMenuContent>\n        </DropdownMenu>\n      </SettingsCardFooter>\n    </SettingsCard>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/settings/user-interface/_modules/theme-mode-selector.tsx",
    "content": "import {\n  ThemeMode,\n  useExperimentalThemeContext,\n} from '@/components/providers/theme-provider'\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from '@/components/ui/select'\nimport { m } from '@/paraglide/messages'\n\nexport default function ThemeModeSelector() {\n  const { themeMode, setThemeMode } = useExperimentalThemeContext()\n\n  const handleThemeModeChange = (value: string) => {\n    setThemeMode(value as ThemeMode)\n  }\n\n  const messages = {\n    [ThemeMode.LIGHT]: m.settings_user_interface_theme_mode_light(),\n    [ThemeMode.DARK]: m.settings_user_interface_theme_mode_dark(),\n    [ThemeMode.SYSTEM]: m.settings_user_interface_theme_mode_system(),\n  } satisfies Record<ThemeMode, string>\n\n  return (\n    <Select\n      variant=\"outlined\"\n      value={themeMode}\n      onValueChange={handleThemeModeChange}\n    >\n      <SelectTrigger>\n        <SelectValue placeholder={m.settings_user_interface_theme_mode_label()}>\n          {themeMode ? messages[themeMode] : null}\n        </SelectValue>\n      </SelectTrigger>\n\n      <SelectContent>\n        {Object.entries(messages).map(([key, value]) => (\n          <SelectItem key={key} value={key}>\n            {value}\n          </SelectItem>\n        ))}\n      </SelectContent>\n    </Select>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/settings/user-interface/route.tsx",
    "content": "import { m } from '@/paraglide/messages'\nimport { createFileRoute } from '@tanstack/react-router'\nimport {\n  SettingsCard,\n  SettingsCardContent,\n  SettingsGroup,\n  SettingsLabel,\n} from '../_modules/settings-card'\nimport { SettingsTitle } from '../_modules/settings-title'\nimport LanguageSelector from './_modules/language-selector'\nimport SwitchLegacy from './_modules/switch-legacy'\nimport ThemeColorConfig from './_modules/theme-color-config'\nimport ThemeModeSelector from './_modules/theme-mode-selector'\n\nexport const Route = createFileRoute('/(main)/main/settings/user-interface')({\n  component: RouteComponent,\n  head: () => ({\n    meta: [\n      {\n        title: m.settings_user_interface_title(),\n      },\n    ],\n  }),\n})\n\nconst LanguageSettings = () => {\n  return (\n    <div data-slot=\"language-settings-container\">\n      <SettingsLabel>\n        {m.settings_user_interface_language_group()}\n      </SettingsLabel>\n\n      <SettingsGroup>\n        <SettingsCard>\n          <SettingsCardContent>\n            <LanguageSelector />\n          </SettingsCardContent>\n        </SettingsCard>\n      </SettingsGroup>\n    </div>\n  )\n}\n\nconst ThemeModeSettings = () => {\n  return (\n    <div data-slot=\"theme-mode-settings-container\">\n      <SettingsLabel>\n        {m.settings_user_interface_theme_mode_group()}\n      </SettingsLabel>\n\n      <SettingsGroup>\n        <SettingsCard>\n          <SettingsCardContent>\n            <ThemeModeSelector />\n          </SettingsCardContent>\n        </SettingsCard>\n\n        <ThemeColorConfig />\n      </SettingsGroup>\n    </div>\n  )\n}\n\nfunction RouteComponent() {\n  return (\n    <>\n      <SettingsTitle>{m.settings_user_interface_title()}</SettingsTitle>\n\n      <div className=\"space-y-4 px-4 pb-4\">\n        <LanguageSettings />\n\n        <ThemeModeSettings />\n      </div>\n\n      {/* <SwitchLegacy /> */}\n    </>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/settings/web-ui/_modules/core-secret-config.tsx",
    "content": "import { AnimatePresence } from 'framer-motion'\nimport { ChangeEvent, useCallback, useEffect } from 'react'\nimport { Controller, useForm } from 'react-hook-form'\nimport { z } from 'zod'\nimport { Button } from '@/components/ui/button'\nimport { Input } from '@/components/ui/input'\nimport { m } from '@/paraglide/messages'\nimport { formatError, sleep } from '@/utils'\nimport { message } from '@/utils/notification'\nimport { zodResolver } from '@hookform/resolvers/zod'\nimport {\n  useClashConfig,\n  useClashInfo,\n  useRuntimeProfile,\n} from '@nyanpasu/interface'\nimport { SettingsCardAnimatedItem } from '../../_modules/settings-card'\n\nconst formSchema = z.object({\n  coreSecret: z.string(),\n})\n\nexport default function CoreSecretConfig() {\n  const { data, refetch } = useClashInfo()\n\n  const { upsert } = useClashConfig()\n\n  const runtimeProfile = useRuntimeProfile()\n\n  const form = useForm<z.infer<typeof formSchema>>({\n    resolver: zodResolver(formSchema),\n    defaultValues: {\n      coreSecret: data?.secret || '',\n    },\n  })\n\n  useEffect(() => {\n    form.reset({\n      coreSecret: data?.secret || '',\n    })\n  }, [data?.secret, form])\n\n  const handleSubmit = form.handleSubmit(\n    async (data) => {\n      await upsert.mutateAsync({\n        secret: data.coreSecret,\n      })\n      await refetch()\n\n      // Wait for the server to apply\n      await sleep(300)\n      await runtimeProfile.refetch()\n    },\n    (error) => {\n      message(formatError(error), {\n        title: 'Error',\n        kind: 'error',\n      })\n    },\n  )\n\n  const handleReset = useCallback(() => {\n    form.reset({\n      coreSecret: data?.secret || '',\n    })\n  }, [form, data?.secret])\n\n  return (\n    <form className=\"flex flex-col gap-2\" onSubmit={handleSubmit}>\n      <Controller\n        control={form.control}\n        name=\"coreSecret\"\n        render={({ field }) => {\n          const handleChange = (event: ChangeEvent<HTMLInputElement>) => {\n            field.onChange(event.target.value)\n          }\n\n          return (\n            <>\n              <Input\n                variant=\"outlined\"\n                label={m.settings_clash_settings_core_secret_label()}\n                value={field.value ?? ''}\n                onChange={handleChange}\n              />\n\n              {form.formState.errors.coreSecret && (\n                <SettingsCardAnimatedItem className=\"text-error\">\n                  {form.formState.errors.coreSecret.message}\n                </SettingsCardAnimatedItem>\n              )}\n            </>\n          )\n        }}\n      />\n\n      <AnimatePresence initial={false}>\n        {form.formState.isDirty && (\n          <SettingsCardAnimatedItem>\n            <div className=\"flex justify-end gap-2 pt-1\">\n              <Button type=\"button\" onClick={handleReset}>\n                {m.common_reset()}\n              </Button>\n\n              <Button\n                variant=\"raised\"\n                onClick={handleSubmit}\n                loading={form.formState.isSubmitting}\n              >\n                {m.common_apply()}\n              </Button>\n            </div>\n          </SettingsCardAnimatedItem>\n        )}\n      </AnimatePresence>\n    </form>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/settings/web-ui/_modules/external-controller-config.tsx",
    "content": "import { AnimatePresence } from 'framer-motion'\nimport { ChangeEvent, useCallback, useEffect } from 'react'\nimport { Controller, useForm } from 'react-hook-form'\nimport { z } from 'zod'\nimport { Button } from '@/components/ui/button'\nimport { Input } from '@/components/ui/input'\nimport { m } from '@/paraglide/messages'\nimport { formatError, sleep } from '@/utils'\nimport { message } from '@/utils/notification'\nimport { zodResolver } from '@hookform/resolvers/zod'\nimport {\n  useClashConfig,\n  useClashInfo,\n  useRuntimeProfile,\n} from '@nyanpasu/interface'\nimport { SettingsCardAnimatedItem } from '../../_modules/settings-card'\n\nconst formSchema = z.object({\n  externalController: z.string(),\n})\n\nexport default function ExternalControllerConfig() {\n  const { data, refetch } = useClashInfo()\n\n  const { upsert } = useClashConfig()\n\n  const runtimeProfile = useRuntimeProfile()\n\n  const form = useForm<z.infer<typeof formSchema>>({\n    resolver: zodResolver(formSchema),\n    defaultValues: {\n      externalController: data?.server || '',\n    },\n  })\n\n  useEffect(() => {\n    form.reset({\n      externalController: data?.server || '',\n    })\n  }, [data?.server, form])\n\n  const handleSubmit = form.handleSubmit(\n    async (data) => {\n      await upsert.mutateAsync({\n        'external-controller': data.externalController,\n      })\n      await refetch()\n\n      // Wait for the server to apply\n      await sleep(300)\n      await runtimeProfile.refetch()\n    },\n    (error) => {\n      message(formatError(error), {\n        title: 'Error',\n        kind: 'error',\n      })\n    },\n  )\n\n  const handleReset = useCallback(() => {\n    form.reset({\n      externalController: data?.server || '',\n    })\n  }, [form, data?.server])\n\n  return (\n    <form className=\"flex flex-col gap-2\" onSubmit={handleSubmit}>\n      <Controller\n        control={form.control}\n        name=\"externalController\"\n        render={({ field }) => {\n          const handleChange = (event: ChangeEvent<HTMLInputElement>) => {\n            field.onChange(event.target.value)\n          }\n\n          return (\n            <>\n              <Input\n                variant=\"outlined\"\n                label={m.settings_clash_settings_external_controll_label()}\n                value={field.value ?? ''}\n                onChange={handleChange}\n              />\n\n              {form.formState.errors.externalController && (\n                <SettingsCardAnimatedItem className=\"text-error\">\n                  {form.formState.errors.externalController.message}\n                </SettingsCardAnimatedItem>\n              )}\n            </>\n          )\n        }}\n      />\n\n      <AnimatePresence initial={false}>\n        {form.formState.isDirty && (\n          <SettingsCardAnimatedItem>\n            <div className=\"flex justify-end gap-2 pt-1\">\n              <Button type=\"button\" onClick={handleReset}>\n                {m.common_reset()}\n              </Button>\n\n              <Button\n                variant=\"raised\"\n                onClick={handleSubmit}\n                loading={form.formState.isSubmitting}\n              >\n                {m.common_apply()}\n              </Button>\n            </div>\n          </SettingsCardAnimatedItem>\n        )}\n      </AnimatePresence>\n    </form>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/settings/web-ui/_modules/port-strategy-selector.tsx",
    "content": "import {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from '@/components/ui/select'\nimport { m } from '@/paraglide/messages'\nimport { ExternalControllerPortStrategy, useSetting } from '@nyanpasu/interface'\n\nexport default function PortStrategySelector() {\n  const { value, upsert } = useSetting('clash_strategy')\n\n  const messages = {\n    allow_fallback: m.settings_clash_settings_allow_fallback_label(),\n    fixed: m.settings_clash_settings_fixed_label(),\n    random: m.settings_clash_settings_random_label(),\n  } as Record<ExternalControllerPortStrategy, string>\n\n  const handlePortStrategyChange = async (\n    value: ExternalControllerPortStrategy,\n  ) => {\n    await upsert({\n      external_controller_port_strategy: value,\n    })\n  }\n\n  return (\n    <Select\n      variant=\"outlined\"\n      value={value?.external_controller_port_strategy || 'allow_fallback'}\n      onValueChange={handlePortStrategyChange}\n    >\n      <SelectTrigger>\n        <SelectValue\n          placeholder={m.settings_clash_settings_port_strategy_label()}\n        >\n          {\n            messages[\n              value?.external_controller_port_strategy || 'allow_fallback'\n            ]\n          }\n        </SelectValue>\n      </SelectTrigger>\n\n      <SelectContent>\n        {Object.entries(messages).map(([key, message]) => (\n          <SelectItem key={key} value={key}>\n            {message}\n          </SelectItem>\n        ))}\n      </SelectContent>\n    </Select>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/settings/web-ui/_modules/web-ui.tsx",
    "content": "import AddIcon from '~icons/material-symbols/add-rounded'\nimport AllInboxRounded from '~icons/material-symbols/all-inbox-outline-rounded'\nimport DeleteRounded from '~icons/material-symbols/delete-rounded'\nimport EditSquareRounded from '~icons/material-symbols/edit-square-rounded'\nimport OpenInNewRounded from '~icons/material-symbols/open-in-new-rounded'\nimport { AnimatePresence, motion } from 'framer-motion'\nimport {\n  ChangeEvent,\n  PropsWithChildren,\n  useEffect,\n  useMemo,\n  useState,\n} from 'react'\nimport { Controller, useForm } from 'react-hook-form'\nimport z from 'zod'\nimport { Button } from '@/components/ui/button'\nimport { Card, CardContent, CardFooter, CardHeader } from '@/components/ui/card'\nimport { Input } from '@/components/ui/input'\nimport {\n  Modal,\n  ModalClose,\n  ModalContent,\n  ModalTitle,\n  ModalTrigger,\n} from '@/components/ui/modal'\nimport TextMarquee from '@/components/ui/text-marquee'\nimport { useLockFn } from '@/hooks/use-lock-fn'\nimport { m } from '@/paraglide/messages'\nimport { formatError } from '@/utils'\nimport { message } from '@/utils/notification'\nimport { zodResolver } from '@hookform/resolvers/zod'\nimport { commands, useClashInfo, useSetting } from '@nyanpasu/interface'\nimport {\n  SettingsCard,\n  SettingsCardAnimatedItem,\n  SettingsCardContent,\n} from '../../_modules/settings-card'\n\nconst useUrlLabels = () => {\n  const { data } = useClashInfo()\n\n  return useMemo(() => {\n    let host = '127.0.0.1'\n    let port = 7890\n\n    if (data?.server) {\n      const [h, p] = data.server.split(':')\n\n      host = h\n      port = Number(p)\n    }\n\n    return {\n      host,\n      port,\n      secret: data?.secret,\n    }\n  }, [data])\n}\n\nconst useFormattedUrl = (url: string) => {\n  const labels = useUrlLabels()\n\n  return useMemo(() => {\n    let result = url\n\n    for (const key of Object.keys(labels) as Array<keyof typeof labels>) {\n      const regex = new RegExp(`%${key}`, 'g')\n\n      result = result.replace(regex, String(labels[key] ?? ''))\n    }\n\n    return result\n  }, [url, labels])\n}\n\nconst PreviewItem = ({ url }: { url: string }) => {\n  const formattedUrl = useFormattedUrl(url)\n\n  return (\n    <motion.div\n      className=\"outline-outline-variant overflow-hidden rounded-2xl p-3 outline\"\n      initial={{\n        height: 0,\n        opacity: 0,\n      }}\n      animate={{\n        height: 'auto',\n        opacity: 1,\n      }}\n      exit={{\n        height: 0,\n        opacity: 0,\n      }}\n      transition={{\n        height: {\n          duration: 0.2,\n          ease: 'easeInOut',\n        },\n        opacity: {\n          duration: 0.15,\n        },\n      }}\n    >\n      <div>{m.settings_web_ui_preview_title()}</div>\n      <TextMarquee className=\"w-full\">{formattedUrl}</TextMarquee>\n    </motion.div>\n  )\n}\n\nconst formSchema = z.object({\n  url: z.httpUrl(),\n})\n\nconst EditItemButton = ({\n  defaultUrl,\n  children,\n}: PropsWithChildren<{ defaultUrl?: string }>) => {\n  const [open, setOpen] = useState(false)\n\n  const { value, upsert } = useSetting('web_ui_list')\n\n  const labels = useUrlLabels()\n\n  const form = useForm<z.infer<typeof formSchema>>({\n    resolver: zodResolver(formSchema),\n    defaultValues: {\n      url: defaultUrl,\n    },\n  })\n\n  // sync default url to form\n  useEffect(() => {\n    form.reset({\n      url: defaultUrl,\n    })\n  }, [defaultUrl, form])\n\n  const handleOpenChange = (open: boolean) => {\n    if (!open) {\n      form.reset({\n        url: defaultUrl,\n      })\n    }\n\n    setOpen(open)\n  }\n\n  const urlValue = form.watch('url')\n\n  const handleSubmit = form.handleSubmit(\n    async (data) => {\n      try {\n        await upsert([...(value || []), data.url])\n        handleOpenChange(false)\n      } catch (error) {\n        message(formatError(error), {\n          title: 'Error',\n          kind: 'error',\n        })\n      }\n    },\n    (error) => {\n      message(formatError(error), {\n        title: 'Error',\n        kind: 'error',\n      })\n    },\n  )\n\n  return (\n    <Modal open={open} onOpenChange={handleOpenChange}>\n      <ModalTrigger asChild>{children}</ModalTrigger>\n\n      <ModalContent>\n        <Card className=\"w-96\">\n          <CardHeader>\n            <ModalTitle>{m.settings_web_ui_add_button()}</ModalTitle>\n          </CardHeader>\n\n          <CardContent>\n            <Controller\n              control={form.control}\n              name=\"url\"\n              render={({ field }) => {\n                const handleChange = (event: ChangeEvent<HTMLInputElement>) => {\n                  field.onChange(event.target.value)\n                }\n\n                return (\n                  <>\n                    <Input\n                      variant=\"outlined\"\n                      label={m.settings_web_ui_input_label()}\n                      value={field.value ?? ''}\n                      onChange={handleChange}\n                    />\n\n                    {form.formState.errors.url && (\n                      <SettingsCardAnimatedItem className=\"text-error\">\n                        {form.formState.errors.url.message}\n                      </SettingsCardAnimatedItem>\n                    )}\n                  </>\n                )\n              }}\n            />\n\n            <p className=\"flex flex-wrap items-center gap-1 text-sm select-text\">\n              <span>{m.settings_web_ui_replace_with_label()}</span>\n\n              {Object.entries(labels).map(([key], index) => {\n                return (\n                  <span\n                    key={index}\n                    className=\"bg-on-primary rounded-full px-2 py-0.5\"\n                  >\n                    %{key}\n                  </span>\n                )\n              })}\n            </p>\n\n            <AnimatePresence>\n              {urlValue && <PreviewItem url={urlValue} />}\n            </AnimatePresence>\n          </CardContent>\n\n          <CardFooter className=\"gap-2\">\n            <Button variant=\"flat\" onClick={handleSubmit}>\n              {m.common_submit()}\n            </Button>\n\n            <ModalClose>{m.common_cancel()}</ModalClose>\n          </CardFooter>\n        </Card>\n      </ModalContent>\n    </Modal>\n  )\n}\n\nconst WebUIItem = ({ url }: { url: string }) => {\n  const formattedUrl = useFormattedUrl(url)\n\n  const handleOpen = useLockFn(async () => {\n    await commands.openWebUrl(formattedUrl)\n  })\n\n  const { value, upsert } = useSetting('web_ui_list')\n\n  const handleDelete = useLockFn(async () => {\n    await upsert(value?.filter((item) => item !== url) || [])\n  })\n\n  return (\n    <Card className=\"w-full min-w-0 space-y-4 overflow-hidden\">\n      <CardHeader className=\"flex w-full min-w-0 flex-row\">\n        <TextMarquee className=\"relative w-0 min-w-0 flex-1 text-base\">\n          {formattedUrl}\n        </TextMarquee>\n      </CardHeader>\n\n      <CardFooter className=\"gap-1\">\n        <Button variant=\"flat\" icon onClick={handleOpen}>\n          <OpenInNewRounded className=\"size-5\" />\n        </Button>\n\n        <EditItemButton defaultUrl={url}>\n          <Button icon>\n            <EditSquareRounded className=\"size-5\" />\n          </Button>\n        </EditItemButton>\n\n        <Button icon onClick={handleDelete}>\n          <DeleteRounded className=\"size-5\" />\n        </Button>\n      </CardFooter>\n    </Card>\n  )\n}\n\nconst EmptyItem = () => {\n  return (\n    <Card variant=\"outline\">\n      <CardContent className=\"min-h-40 items-center justify-center\">\n        <AllInboxRounded className=\"size-10\" />\n\n        <p>{m.settings_web_ui_empty_item()}</p>\n      </CardContent>\n    </Card>\n  )\n}\n\nexport default function WebUI() {\n  const { value } = useSetting('web_ui_list')\n\n  return (\n    <div className=\"space-y-3\">\n      <SettingsCard data-slot=\"web-ui-card\">\n        <SettingsCardContent data-slot=\"web-ui-card-content\">\n          {value && value.length > 0 ? (\n            value.map((item, index) => <WebUIItem key={index} url={item} />)\n          ) : (\n            <EmptyItem />\n          )}\n        </SettingsCardContent>\n      </SettingsCard>\n\n      <div className=\"flex justify-end\">\n        <EditItemButton>\n          <Button\n            className=\"flex items-center justify-center gap-1 px-4\"\n            variant=\"raised\"\n          >\n            <AddIcon className=\"size-6\" />\n            <span>{m.settings_web_ui_add_button()}</span>\n          </Button>\n        </EditItemButton>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/main/settings/web-ui/route.tsx",
    "content": "import { m } from '@/paraglide/messages'\nimport { createFileRoute } from '@tanstack/react-router'\nimport {\n  SettingsCard,\n  SettingsCardContent,\n  SettingsGroup,\n  SettingsLabel,\n} from '../_modules/settings-card'\nimport { SettingsTitle } from '../_modules/settings-title'\nimport CoreSecretConfig from './_modules/core-secret-config'\nimport ExternalControllerConfig from './_modules/external-controller-config'\nimport PortStrategySelector from './_modules/port-strategy-selector'\nimport WebUI from './_modules/web-ui'\n\nexport const Route = createFileRoute('/(main)/main/settings/web-ui')({\n  component: RouteComponent,\n})\n\nconst ExternalController = () => {\n  return (\n    <div data-slot=\"theme-mode-settings-container\">\n      <SettingsLabel>{m.settings_label_external_controll()}</SettingsLabel>\n\n      <SettingsGroup>\n        <SettingsCard>\n          <SettingsCardContent>\n            <ExternalControllerConfig />\n          </SettingsCardContent>\n        </SettingsCard>\n\n        <SettingsCard>\n          <SettingsCardContent>\n            <PortStrategySelector />\n          </SettingsCardContent>\n        </SettingsCard>\n\n        <SettingsCard>\n          <SettingsCardContent>\n            <CoreSecretConfig />\n          </SettingsCardContent>\n        </SettingsCard>\n      </SettingsGroup>\n    </div>\n  )\n}\n\nconst WebUISettings = () => {\n  return (\n    <div data-slot=\"theme-mode-settings-container\">\n      <SettingsLabel>{m.settings_web_ui_title()}</SettingsLabel>\n\n      <WebUI />\n    </div>\n  )\n}\n\nfunction RouteComponent() {\n  return (\n    <>\n      <SettingsTitle>{m.settings_label_external_controll()}</SettingsTitle>\n\n      <div className=\"space-y-4 px-4 pb-4\">\n        <ExternalController />\n\n        <WebUISettings />\n      </div>\n    </>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/(main)/route.tsx",
    "content": "import ContextMenuProvider from '@/components/providers/context-menu-provider'\nimport NyanpasuUpdateProvider from '@/components/providers/nyanpasu-update-provider'\nimport { AnimatedOutletPreset } from '@/components/router/animated-outlet'\nimport { cn } from '@nyanpasu/ui'\nimport packageJson from '@root/package.json'\nimport { createFileRoute } from '@tanstack/react-router'\nimport Header from './_modules/header'\nimport Navbar from './_modules/navbar'\n\nexport const Route = createFileRoute('/(main)')({\n  component: RouteComponent,\n})\n\nconst AppContent = () => {\n  return (\n    <AnimatedOutletPreset\n      className={cn(\n        'h-[calc(100vh-40px-64px)]',\n        'sm:h-[calc(100vh-40px-48px)]',\n        'overflow-hidden',\n      )}\n      data-slot=\"app-content\"\n    />\n  )\n}\n\nfunction RouteComponent() {\n  return (\n    <NyanpasuUpdateProvider>\n      <ContextMenuProvider>\n        <div\n          className={cn(\n            'flex max-h-dvh min-h-dvh flex-col',\n            'bg-mixed-background',\n          )}\n          data-slot=\"app-root\"\n          data-app-version={packageJson.version}\n        >\n          <Header />\n\n          <div\n            className=\"flex flex-1 flex-col sm:flex-col-reverse\"\n            data-slot=\"app-content-container\"\n          >\n            <AppContent />\n\n            <Navbar />\n          </div>\n        </div>\n      </ContextMenuProvider>\n    </NyanpasuUpdateProvider>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/-__root.module.scss",
    "content": ".oops {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n}\n\n.dark {\n  color: bisque;\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/-__root.module.scss.d.ts",
    "content": "declare const classNames: {\n  readonly oops: 'oops'\n  readonly dark: 'dark'\n}\nexport default classNames\n"
  },
  {
    "path": "frontend/nyanpasu/src/pages/__root.tsx",
    "content": "import { useMount } from 'ahooks'\nimport dayjs from 'dayjs'\nimport { useNyanpasuStorageSubscribers } from '@/hooks/use-store'\nimport { cn } from '@nyanpasu/ui'\nimport {\n  createRootRoute,\n  ErrorComponentProps,\n  Outlet,\n} from '@tanstack/react-router'\nimport { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'\nimport 'dayjs/locale/ru'\nimport 'dayjs/locale/zh-cn'\nimport 'dayjs/locale/zh-tw'\nimport customParseFormat from 'dayjs/plugin/customParseFormat'\nimport relativeTime from 'dayjs/plugin/relativeTime'\nimport { lazy } from 'react'\nimport { BlockTaskProvider } from '@/components/providers/block-task-provider'\nimport { LanguageProvider } from '@/components/providers/language-provider'\nimport { ExperimentalThemeProvider } from '@/components/providers/theme-provider'\nimport { events, NyanpasuProvider } from '@nyanpasu/interface'\n\ndayjs.extend(relativeTime)\ndayjs.extend(customParseFormat)\n\nconst appWindow = getCurrentWebviewWindow()\n\nexport const Catch = ({ error }: ErrorComponentProps) => {\n  return (\n    <div className={cn('h-dvh bg-black text-white', 'flex flex-col gap-4 p-4')}>\n      <div\n        className=\"fixed top-0 left-0 z-10 h-6 w-full\"\n        data-tauri-drag-region\n      />\n\n      <h1 data-tauri-drag-region>Oops!</h1>\n\n      <p>Something went wrong... Caught in error boundary.</p>\n\n      <pre className=\"overflow-x-auto font-mono whitespace-pre-wrap select-text\">\n        {error.message}\n        {error.stack}\n      </pre>\n\n      <div className=\"flex items-center gap-2\">\n        <button\n          className=\"cursor-pointer bg-zinc-900 px-3 py-2 text-zinc-100\"\n          onClick={() => window.location.reload()}\n        >\n          Reload Resource\n        </button>\n\n        <button\n          className=\"cursor-pointer bg-zinc-900 px-3 py-2 text-zinc-100\"\n          onClick={() => appWindow.close()}\n        >\n          Close Window\n        </button>\n      </div>\n    </div>\n  )\n}\n\nexport const Pending = () => <div>Loading from _root...</div>\n\nconst TanStackRouterDevtools = import.meta.env.PROD\n  ? () => null // Render nothing in production\n  : lazy(() =>\n      // Lazy load in development\n      import('@tanstack/react-router-devtools').then((res) => ({\n        default: res.TanStackRouterDevtools,\n        // For Embedded Mode\n        // default: res.TanStackRouterDevtoolsPanel\n      })),\n    )\n\nexport const Route = createRootRoute({\n  component: App,\n  errorComponent: Catch,\n  pendingComponent: Pending,\n})\n\nexport default function App() {\n  useNyanpasuStorageSubscribers()\n\n  useMount(() => {\n    Promise.all([\n      appWindow.show(),\n      appWindow.unminimize(),\n      appWindow.setFocus(),\n    ]).finally(() => {\n      events.reactAppMountedEvent.emit(null)\n    })\n  })\n\n  return (\n    <NyanpasuProvider>\n      <BlockTaskProvider>\n        <LanguageProvider>\n          <ExperimentalThemeProvider>\n            <Outlet />\n          </ExperimentalThemeProvider>\n\n          <TanStackRouterDevtools />\n        </LanguageProvider>\n      </BlockTaskProvider>\n    </NyanpasuProvider>\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/route-tree.gen.ts",
    "content": "/* eslint-disable */\n\n// @ts-nocheck\n\n// noinspection JSUnusedGlobalSymbols\n\n// This file was automatically generated by TanStack Router.\n// You should NOT make any changes in this file as it will be overwritten.\n// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.\n\nimport { Route as rootRouteImport } from './pages/__root'\nimport { Route as mainRouteRouteImport } from './pages/(main)/route'\nimport { Route as legacyRouteRouteImport } from './pages/(legacy)/route'\nimport { Route as legacyIndexRouteImport } from './pages/(legacy)/index'\nimport { Route as legacySettingsRouteImport } from './pages/(legacy)/settings'\nimport { Route as legacyRulesRouteImport } from './pages/(legacy)/rules'\nimport { Route as legacyProxiesRouteImport } from './pages/(legacy)/proxies'\nimport { Route as legacyProvidersRouteImport } from './pages/(legacy)/providers'\nimport { Route as legacyProfilesRouteImport } from './pages/(legacy)/profiles'\nimport { Route as legacyLogsRouteImport } from './pages/(legacy)/logs'\nimport { Route as legacyDashboardRouteImport } from './pages/(legacy)/dashboard'\nimport { Route as legacyConnectionsRouteImport } from './pages/(legacy)/connections'\nimport { Route as editorEditorRouteRouteImport } from './pages/(editor)/editor/route'\nimport { Route as mainMainIndexRouteImport } from './pages/(main)/main/index'\nimport { Route as editorEditorIndexRouteImport } from './pages/(editor)/editor/index'\nimport { Route as mainMainSettingsRouteRouteImport } from './pages/(main)/main/settings/route'\nimport { Route as mainMainRulesRouteRouteImport } from './pages/(main)/main/rules/route'\nimport { Route as mainMainProxiesRouteRouteImport } from './pages/(main)/main/proxies/route'\nimport { Route as mainMainProvidersRouteRouteImport } from './pages/(main)/main/providers/route'\nimport { Route as mainMainProfilesRouteRouteImport } from './pages/(main)/main/profiles/route'\nimport { Route as mainMainLogsRouteRouteImport } from './pages/(main)/main/logs/route'\nimport { Route as mainMainDashboardRouteRouteImport } from './pages/(main)/main/dashboard/route'\nimport { Route as mainMainConnectionsRouteRouteImport } from './pages/(main)/main/connections/route'\nimport { Route as mainMainSettingsIndexRouteImport } from './pages/(main)/main/settings/index'\nimport { Route as mainMainRulesIndexRouteImport } from './pages/(main)/main/rules/index'\nimport { Route as mainMainProxiesIndexRouteImport } from './pages/(main)/main/proxies/index'\nimport { Route as mainMainProvidersIndexRouteImport } from './pages/(main)/main/providers/index'\nimport { Route as mainMainProfilesIndexRouteImport } from './pages/(main)/main/profiles/index'\nimport { Route as mainMainLogsIndexRouteImport } from './pages/(main)/main/logs/index'\nimport { Route as mainMainConnectionsIndexRouteImport } from './pages/(main)/main/connections/index'\nimport { Route as mainMainSettingsWebUiRouteRouteImport } from './pages/(main)/main/settings/web-ui/route'\nimport { Route as mainMainSettingsUserInterfaceRouteRouteImport } from './pages/(main)/main/settings/user-interface/route'\nimport { Route as mainMainSettingsSystemRouteRouteImport } from './pages/(main)/main/settings/system/route'\nimport { Route as mainMainSettingsNyanpasuRouteRouteImport } from './pages/(main)/main/settings/nyanpasu/route'\nimport { Route as mainMainSettingsDebugRouteRouteImport } from './pages/(main)/main/settings/debug/route'\nimport { Route as mainMainSettingsClashRouteRouteImport } from './pages/(main)/main/settings/clash/route'\nimport { Route as mainMainSettingsAboutRouteRouteImport } from './pages/(main)/main/settings/about/route'\nimport { Route as mainMainProfilesInspectRouteRouteImport } from './pages/(main)/main/profiles/inspect/route'\nimport { Route as mainMainSettingsDebugIndexRouteImport } from './pages/(main)/main/settings/debug/index'\nimport { Route as mainMainProfilesTypeIndexRouteImport } from './pages/(main)/main/profiles/$type/index'\nimport { Route as mainMainProxiesGroupNameRouteImport } from './pages/(main)/main/proxies/group/$name'\nimport { Route as mainMainProvidersRulesKeyRouteImport } from './pages/(main)/main/providers/rules/$key'\nimport { Route as mainMainProvidersProxiesKeyRouteImport } from './pages/(main)/main/providers/proxies/$key'\nimport { Route as mainMainProfilesTypeDetailUidRouteImport } from './pages/(main)/main/profiles/$type/detail/$uid'\n\nconst mainRouteRoute = mainRouteRouteImport.update({\n  id: '/(main)',\n  getParentRoute: () => rootRouteImport,\n} as any)\nconst legacyRouteRoute = legacyRouteRouteImport.update({\n  id: '/(legacy)',\n  getParentRoute: () => rootRouteImport,\n} as any)\nconst legacyIndexRoute = legacyIndexRouteImport.update({\n  id: '/',\n  path: '/',\n  getParentRoute: () => legacyRouteRoute,\n} as any)\nconst legacySettingsRoute = legacySettingsRouteImport.update({\n  id: '/settings',\n  path: '/settings',\n  getParentRoute: () => legacyRouteRoute,\n} as any)\nconst legacyRulesRoute = legacyRulesRouteImport.update({\n  id: '/rules',\n  path: '/rules',\n  getParentRoute: () => legacyRouteRoute,\n} as any)\nconst legacyProxiesRoute = legacyProxiesRouteImport.update({\n  id: '/proxies',\n  path: '/proxies',\n  getParentRoute: () => legacyRouteRoute,\n} as any)\nconst legacyProvidersRoute = legacyProvidersRouteImport.update({\n  id: '/providers',\n  path: '/providers',\n  getParentRoute: () => legacyRouteRoute,\n} as any)\nconst legacyProfilesRoute = legacyProfilesRouteImport.update({\n  id: '/profiles',\n  path: '/profiles',\n  getParentRoute: () => legacyRouteRoute,\n} as any)\nconst legacyLogsRoute = legacyLogsRouteImport.update({\n  id: '/logs',\n  path: '/logs',\n  getParentRoute: () => legacyRouteRoute,\n} as any)\nconst legacyDashboardRoute = legacyDashboardRouteImport.update({\n  id: '/dashboard',\n  path: '/dashboard',\n  getParentRoute: () => legacyRouteRoute,\n} as any)\nconst legacyConnectionsRoute = legacyConnectionsRouteImport.update({\n  id: '/connections',\n  path: '/connections',\n  getParentRoute: () => legacyRouteRoute,\n} as any)\nconst editorEditorRouteRoute = editorEditorRouteRouteImport.update({\n  id: '/(editor)/editor',\n  path: '/editor',\n  getParentRoute: () => rootRouteImport,\n} as any)\nconst mainMainIndexRoute = mainMainIndexRouteImport.update({\n  id: '/main/',\n  path: '/main/',\n  getParentRoute: () => mainRouteRoute,\n} as any)\nconst editorEditorIndexRoute = editorEditorIndexRouteImport.update({\n  id: '/',\n  path: '/',\n  getParentRoute: () => editorEditorRouteRoute,\n} as any)\nconst mainMainSettingsRouteRoute = mainMainSettingsRouteRouteImport.update({\n  id: '/main/settings',\n  path: '/main/settings',\n  getParentRoute: () => mainRouteRoute,\n} as any)\nconst mainMainRulesRouteRoute = mainMainRulesRouteRouteImport.update({\n  id: '/main/rules',\n  path: '/main/rules',\n  getParentRoute: () => mainRouteRoute,\n} as any)\nconst mainMainProxiesRouteRoute = mainMainProxiesRouteRouteImport.update({\n  id: '/main/proxies',\n  path: '/main/proxies',\n  getParentRoute: () => mainRouteRoute,\n} as any)\nconst mainMainProvidersRouteRoute = mainMainProvidersRouteRouteImport.update({\n  id: '/main/providers',\n  path: '/main/providers',\n  getParentRoute: () => mainRouteRoute,\n} as any)\nconst mainMainProfilesRouteRoute = mainMainProfilesRouteRouteImport.update({\n  id: '/main/profiles',\n  path: '/main/profiles',\n  getParentRoute: () => mainRouteRoute,\n} as any)\nconst mainMainLogsRouteRoute = mainMainLogsRouteRouteImport.update({\n  id: '/main/logs',\n  path: '/main/logs',\n  getParentRoute: () => mainRouteRoute,\n} as any)\nconst mainMainDashboardRouteRoute = mainMainDashboardRouteRouteImport.update({\n  id: '/main/dashboard',\n  path: '/main/dashboard',\n  getParentRoute: () => mainRouteRoute,\n} as any)\nconst mainMainConnectionsRouteRoute =\n  mainMainConnectionsRouteRouteImport.update({\n    id: '/main/connections',\n    path: '/main/connections',\n    getParentRoute: () => mainRouteRoute,\n  } as any)\nconst mainMainSettingsIndexRoute = mainMainSettingsIndexRouteImport.update({\n  id: '/',\n  path: '/',\n  getParentRoute: () => mainMainSettingsRouteRoute,\n} as any)\nconst mainMainRulesIndexRoute = mainMainRulesIndexRouteImport.update({\n  id: '/',\n  path: '/',\n  getParentRoute: () => mainMainRulesRouteRoute,\n} as any)\nconst mainMainProxiesIndexRoute = mainMainProxiesIndexRouteImport.update({\n  id: '/',\n  path: '/',\n  getParentRoute: () => mainMainProxiesRouteRoute,\n} as any)\nconst mainMainProvidersIndexRoute = mainMainProvidersIndexRouteImport.update({\n  id: '/',\n  path: '/',\n  getParentRoute: () => mainMainProvidersRouteRoute,\n} as any)\nconst mainMainProfilesIndexRoute = mainMainProfilesIndexRouteImport.update({\n  id: '/',\n  path: '/',\n  getParentRoute: () => mainMainProfilesRouteRoute,\n} as any)\nconst mainMainLogsIndexRoute = mainMainLogsIndexRouteImport.update({\n  id: '/',\n  path: '/',\n  getParentRoute: () => mainMainLogsRouteRoute,\n} as any)\nconst mainMainConnectionsIndexRoute =\n  mainMainConnectionsIndexRouteImport.update({\n    id: '/',\n    path: '/',\n    getParentRoute: () => mainMainConnectionsRouteRoute,\n  } as any)\nconst mainMainSettingsWebUiRouteRoute =\n  mainMainSettingsWebUiRouteRouteImport.update({\n    id: '/web-ui',\n    path: '/web-ui',\n    getParentRoute: () => mainMainSettingsRouteRoute,\n  } as any)\nconst mainMainSettingsUserInterfaceRouteRoute =\n  mainMainSettingsUserInterfaceRouteRouteImport.update({\n    id: '/user-interface',\n    path: '/user-interface',\n    getParentRoute: () => mainMainSettingsRouteRoute,\n  } as any)\nconst mainMainSettingsSystemRouteRoute =\n  mainMainSettingsSystemRouteRouteImport.update({\n    id: '/system',\n    path: '/system',\n    getParentRoute: () => mainMainSettingsRouteRoute,\n  } as any)\nconst mainMainSettingsNyanpasuRouteRoute =\n  mainMainSettingsNyanpasuRouteRouteImport.update({\n    id: '/nyanpasu',\n    path: '/nyanpasu',\n    getParentRoute: () => mainMainSettingsRouteRoute,\n  } as any)\nconst mainMainSettingsDebugRouteRoute =\n  mainMainSettingsDebugRouteRouteImport.update({\n    id: '/debug',\n    path: '/debug',\n    getParentRoute: () => mainMainSettingsRouteRoute,\n  } as any)\nconst mainMainSettingsClashRouteRoute =\n  mainMainSettingsClashRouteRouteImport.update({\n    id: '/clash',\n    path: '/clash',\n    getParentRoute: () => mainMainSettingsRouteRoute,\n  } as any)\nconst mainMainSettingsAboutRouteRoute =\n  mainMainSettingsAboutRouteRouteImport.update({\n    id: '/about',\n    path: '/about',\n    getParentRoute: () => mainMainSettingsRouteRoute,\n  } as any)\nconst mainMainProfilesInspectRouteRoute =\n  mainMainProfilesInspectRouteRouteImport.update({\n    id: '/inspect',\n    path: '/inspect',\n    getParentRoute: () => mainMainProfilesRouteRoute,\n  } as any)\nconst mainMainSettingsDebugIndexRoute =\n  mainMainSettingsDebugIndexRouteImport.update({\n    id: '/',\n    path: '/',\n    getParentRoute: () => mainMainSettingsDebugRouteRoute,\n  } as any)\nconst mainMainProfilesTypeIndexRoute =\n  mainMainProfilesTypeIndexRouteImport.update({\n    id: '/$type/',\n    path: '/$type/',\n    getParentRoute: () => mainMainProfilesRouteRoute,\n  } as any)\nconst mainMainProxiesGroupNameRoute =\n  mainMainProxiesGroupNameRouteImport.update({\n    id: '/group/$name',\n    path: '/group/$name',\n    getParentRoute: () => mainMainProxiesRouteRoute,\n  } as any)\nconst mainMainProvidersRulesKeyRoute =\n  mainMainProvidersRulesKeyRouteImport.update({\n    id: '/rules/$key',\n    path: '/rules/$key',\n    getParentRoute: () => mainMainProvidersRouteRoute,\n  } as any)\nconst mainMainProvidersProxiesKeyRoute =\n  mainMainProvidersProxiesKeyRouteImport.update({\n    id: '/proxies/$key',\n    path: '/proxies/$key',\n    getParentRoute: () => mainMainProvidersRouteRoute,\n  } as any)\nconst mainMainProfilesTypeDetailUidRoute =\n  mainMainProfilesTypeDetailUidRouteImport.update({\n    id: '/$type/detail/$uid',\n    path: '/$type/detail/$uid',\n    getParentRoute: () => mainMainProfilesRouteRoute,\n  } as any)\n\nexport interface FileRoutesByFullPath {\n  '/editor': typeof editorEditorRouteRouteWithChildren\n  '/connections': typeof legacyConnectionsRoute\n  '/dashboard': typeof legacyDashboardRoute\n  '/logs': typeof legacyLogsRoute\n  '/profiles': typeof legacyProfilesRoute\n  '/providers': typeof legacyProvidersRoute\n  '/proxies': typeof legacyProxiesRoute\n  '/rules': typeof legacyRulesRoute\n  '/settings': typeof legacySettingsRoute\n  '/': typeof legacyIndexRoute\n  '/main/connections': typeof mainMainConnectionsRouteRouteWithChildren\n  '/main/dashboard': typeof mainMainDashboardRouteRoute\n  '/main/logs': typeof mainMainLogsRouteRouteWithChildren\n  '/main/profiles': typeof mainMainProfilesRouteRouteWithChildren\n  '/main/providers': typeof mainMainProvidersRouteRouteWithChildren\n  '/main/proxies': typeof mainMainProxiesRouteRouteWithChildren\n  '/main/rules': typeof mainMainRulesRouteRouteWithChildren\n  '/main/settings': typeof mainMainSettingsRouteRouteWithChildren\n  '/editor/': typeof editorEditorIndexRoute\n  '/main/': typeof mainMainIndexRoute\n  '/main/profiles/inspect': typeof mainMainProfilesInspectRouteRoute\n  '/main/settings/about': typeof mainMainSettingsAboutRouteRoute\n  '/main/settings/clash': typeof mainMainSettingsClashRouteRoute\n  '/main/settings/debug': typeof mainMainSettingsDebugRouteRouteWithChildren\n  '/main/settings/nyanpasu': typeof mainMainSettingsNyanpasuRouteRoute\n  '/main/settings/system': typeof mainMainSettingsSystemRouteRoute\n  '/main/settings/user-interface': typeof mainMainSettingsUserInterfaceRouteRoute\n  '/main/settings/web-ui': typeof mainMainSettingsWebUiRouteRoute\n  '/main/connections/': typeof mainMainConnectionsIndexRoute\n  '/main/logs/': typeof mainMainLogsIndexRoute\n  '/main/profiles/': typeof mainMainProfilesIndexRoute\n  '/main/providers/': typeof mainMainProvidersIndexRoute\n  '/main/proxies/': typeof mainMainProxiesIndexRoute\n  '/main/rules/': typeof mainMainRulesIndexRoute\n  '/main/settings/': typeof mainMainSettingsIndexRoute\n  '/main/providers/proxies/$key': typeof mainMainProvidersProxiesKeyRoute\n  '/main/providers/rules/$key': typeof mainMainProvidersRulesKeyRoute\n  '/main/proxies/group/$name': typeof mainMainProxiesGroupNameRoute\n  '/main/profiles/$type/': typeof mainMainProfilesTypeIndexRoute\n  '/main/settings/debug/': typeof mainMainSettingsDebugIndexRoute\n  '/main/profiles/$type/detail/$uid': typeof mainMainProfilesTypeDetailUidRoute\n}\nexport interface FileRoutesByTo {\n  '/connections': typeof legacyConnectionsRoute\n  '/dashboard': typeof legacyDashboardRoute\n  '/logs': typeof legacyLogsRoute\n  '/profiles': typeof legacyProfilesRoute\n  '/providers': typeof legacyProvidersRoute\n  '/proxies': typeof legacyProxiesRoute\n  '/rules': typeof legacyRulesRoute\n  '/settings': typeof legacySettingsRoute\n  '/': typeof legacyIndexRoute\n  '/main/dashboard': typeof mainMainDashboardRouteRoute\n  '/editor': typeof editorEditorIndexRoute\n  '/main': typeof mainMainIndexRoute\n  '/main/profiles/inspect': typeof mainMainProfilesInspectRouteRoute\n  '/main/settings/about': typeof mainMainSettingsAboutRouteRoute\n  '/main/settings/clash': typeof mainMainSettingsClashRouteRoute\n  '/main/settings/nyanpasu': typeof mainMainSettingsNyanpasuRouteRoute\n  '/main/settings/system': typeof mainMainSettingsSystemRouteRoute\n  '/main/settings/user-interface': typeof mainMainSettingsUserInterfaceRouteRoute\n  '/main/settings/web-ui': typeof mainMainSettingsWebUiRouteRoute\n  '/main/connections': typeof mainMainConnectionsIndexRoute\n  '/main/logs': typeof mainMainLogsIndexRoute\n  '/main/profiles': typeof mainMainProfilesIndexRoute\n  '/main/providers': typeof mainMainProvidersIndexRoute\n  '/main/proxies': typeof mainMainProxiesIndexRoute\n  '/main/rules': typeof mainMainRulesIndexRoute\n  '/main/settings': typeof mainMainSettingsIndexRoute\n  '/main/providers/proxies/$key': typeof mainMainProvidersProxiesKeyRoute\n  '/main/providers/rules/$key': typeof mainMainProvidersRulesKeyRoute\n  '/main/proxies/group/$name': typeof mainMainProxiesGroupNameRoute\n  '/main/profiles/$type': typeof mainMainProfilesTypeIndexRoute\n  '/main/settings/debug': typeof mainMainSettingsDebugIndexRoute\n  '/main/profiles/$type/detail/$uid': typeof mainMainProfilesTypeDetailUidRoute\n}\nexport interface FileRoutesById {\n  __root__: typeof rootRouteImport\n  '/(legacy)': typeof legacyRouteRouteWithChildren\n  '/(main)': typeof mainRouteRouteWithChildren\n  '/(editor)/editor': typeof editorEditorRouteRouteWithChildren\n  '/(legacy)/connections': typeof legacyConnectionsRoute\n  '/(legacy)/dashboard': typeof legacyDashboardRoute\n  '/(legacy)/logs': typeof legacyLogsRoute\n  '/(legacy)/profiles': typeof legacyProfilesRoute\n  '/(legacy)/providers': typeof legacyProvidersRoute\n  '/(legacy)/proxies': typeof legacyProxiesRoute\n  '/(legacy)/rules': typeof legacyRulesRoute\n  '/(legacy)/settings': typeof legacySettingsRoute\n  '/(legacy)/': typeof legacyIndexRoute\n  '/(main)/main/connections': typeof mainMainConnectionsRouteRouteWithChildren\n  '/(main)/main/dashboard': typeof mainMainDashboardRouteRoute\n  '/(main)/main/logs': typeof mainMainLogsRouteRouteWithChildren\n  '/(main)/main/profiles': typeof mainMainProfilesRouteRouteWithChildren\n  '/(main)/main/providers': typeof mainMainProvidersRouteRouteWithChildren\n  '/(main)/main/proxies': typeof mainMainProxiesRouteRouteWithChildren\n  '/(main)/main/rules': typeof mainMainRulesRouteRouteWithChildren\n  '/(main)/main/settings': typeof mainMainSettingsRouteRouteWithChildren\n  '/(editor)/editor/': typeof editorEditorIndexRoute\n  '/(main)/main/': typeof mainMainIndexRoute\n  '/(main)/main/profiles/inspect': typeof mainMainProfilesInspectRouteRoute\n  '/(main)/main/settings/about': typeof mainMainSettingsAboutRouteRoute\n  '/(main)/main/settings/clash': typeof mainMainSettingsClashRouteRoute\n  '/(main)/main/settings/debug': typeof mainMainSettingsDebugRouteRouteWithChildren\n  '/(main)/main/settings/nyanpasu': typeof mainMainSettingsNyanpasuRouteRoute\n  '/(main)/main/settings/system': typeof mainMainSettingsSystemRouteRoute\n  '/(main)/main/settings/user-interface': typeof mainMainSettingsUserInterfaceRouteRoute\n  '/(main)/main/settings/web-ui': typeof mainMainSettingsWebUiRouteRoute\n  '/(main)/main/connections/': typeof mainMainConnectionsIndexRoute\n  '/(main)/main/logs/': typeof mainMainLogsIndexRoute\n  '/(main)/main/profiles/': typeof mainMainProfilesIndexRoute\n  '/(main)/main/providers/': typeof mainMainProvidersIndexRoute\n  '/(main)/main/proxies/': typeof mainMainProxiesIndexRoute\n  '/(main)/main/rules/': typeof mainMainRulesIndexRoute\n  '/(main)/main/settings/': typeof mainMainSettingsIndexRoute\n  '/(main)/main/providers/proxies/$key': typeof mainMainProvidersProxiesKeyRoute\n  '/(main)/main/providers/rules/$key': typeof mainMainProvidersRulesKeyRoute\n  '/(main)/main/proxies/group/$name': typeof mainMainProxiesGroupNameRoute\n  '/(main)/main/profiles/$type/': typeof mainMainProfilesTypeIndexRoute\n  '/(main)/main/settings/debug/': typeof mainMainSettingsDebugIndexRoute\n  '/(main)/main/profiles/$type/detail/$uid': typeof mainMainProfilesTypeDetailUidRoute\n}\nexport interface FileRouteTypes {\n  fileRoutesByFullPath: FileRoutesByFullPath\n  fullPaths:\n    | '/editor'\n    | '/connections'\n    | '/dashboard'\n    | '/logs'\n    | '/profiles'\n    | '/providers'\n    | '/proxies'\n    | '/rules'\n    | '/settings'\n    | '/'\n    | '/main/connections'\n    | '/main/dashboard'\n    | '/main/logs'\n    | '/main/profiles'\n    | '/main/providers'\n    | '/main/proxies'\n    | '/main/rules'\n    | '/main/settings'\n    | '/editor/'\n    | '/main/'\n    | '/main/profiles/inspect'\n    | '/main/settings/about'\n    | '/main/settings/clash'\n    | '/main/settings/debug'\n    | '/main/settings/nyanpasu'\n    | '/main/settings/system'\n    | '/main/settings/user-interface'\n    | '/main/settings/web-ui'\n    | '/main/connections/'\n    | '/main/logs/'\n    | '/main/profiles/'\n    | '/main/providers/'\n    | '/main/proxies/'\n    | '/main/rules/'\n    | '/main/settings/'\n    | '/main/providers/proxies/$key'\n    | '/main/providers/rules/$key'\n    | '/main/proxies/group/$name'\n    | '/main/profiles/$type/'\n    | '/main/settings/debug/'\n    | '/main/profiles/$type/detail/$uid'\n  fileRoutesByTo: FileRoutesByTo\n  to:\n    | '/connections'\n    | '/dashboard'\n    | '/logs'\n    | '/profiles'\n    | '/providers'\n    | '/proxies'\n    | '/rules'\n    | '/settings'\n    | '/'\n    | '/main/dashboard'\n    | '/editor'\n    | '/main'\n    | '/main/profiles/inspect'\n    | '/main/settings/about'\n    | '/main/settings/clash'\n    | '/main/settings/nyanpasu'\n    | '/main/settings/system'\n    | '/main/settings/user-interface'\n    | '/main/settings/web-ui'\n    | '/main/connections'\n    | '/main/logs'\n    | '/main/profiles'\n    | '/main/providers'\n    | '/main/proxies'\n    | '/main/rules'\n    | '/main/settings'\n    | '/main/providers/proxies/$key'\n    | '/main/providers/rules/$key'\n    | '/main/proxies/group/$name'\n    | '/main/profiles/$type'\n    | '/main/settings/debug'\n    | '/main/profiles/$type/detail/$uid'\n  id:\n    | '__root__'\n    | '/(legacy)'\n    | '/(main)'\n    | '/(editor)/editor'\n    | '/(legacy)/connections'\n    | '/(legacy)/dashboard'\n    | '/(legacy)/logs'\n    | '/(legacy)/profiles'\n    | '/(legacy)/providers'\n    | '/(legacy)/proxies'\n    | '/(legacy)/rules'\n    | '/(legacy)/settings'\n    | '/(legacy)/'\n    | '/(main)/main/connections'\n    | '/(main)/main/dashboard'\n    | '/(main)/main/logs'\n    | '/(main)/main/profiles'\n    | '/(main)/main/providers'\n    | '/(main)/main/proxies'\n    | '/(main)/main/rules'\n    | '/(main)/main/settings'\n    | '/(editor)/editor/'\n    | '/(main)/main/'\n    | '/(main)/main/profiles/inspect'\n    | '/(main)/main/settings/about'\n    | '/(main)/main/settings/clash'\n    | '/(main)/main/settings/debug'\n    | '/(main)/main/settings/nyanpasu'\n    | '/(main)/main/settings/system'\n    | '/(main)/main/settings/user-interface'\n    | '/(main)/main/settings/web-ui'\n    | '/(main)/main/connections/'\n    | '/(main)/main/logs/'\n    | '/(main)/main/profiles/'\n    | '/(main)/main/providers/'\n    | '/(main)/main/proxies/'\n    | '/(main)/main/rules/'\n    | '/(main)/main/settings/'\n    | '/(main)/main/providers/proxies/$key'\n    | '/(main)/main/providers/rules/$key'\n    | '/(main)/main/proxies/group/$name'\n    | '/(main)/main/profiles/$type/'\n    | '/(main)/main/settings/debug/'\n    | '/(main)/main/profiles/$type/detail/$uid'\n  fileRoutesById: FileRoutesById\n}\nexport interface RootRouteChildren {\n  legacyRouteRoute: typeof legacyRouteRouteWithChildren\n  mainRouteRoute: typeof mainRouteRouteWithChildren\n  editorEditorRouteRoute: typeof editorEditorRouteRouteWithChildren\n}\n\ndeclare module '@tanstack/react-router' {\n  interface FileRoutesByPath {\n    '/(main)': {\n      id: '/(main)'\n      path: ''\n      fullPath: ''\n      preLoaderRoute: typeof mainRouteRouteImport\n      parentRoute: typeof rootRouteImport\n    }\n    '/(legacy)': {\n      id: '/(legacy)'\n      path: ''\n      fullPath: ''\n      preLoaderRoute: typeof legacyRouteRouteImport\n      parentRoute: typeof rootRouteImport\n    }\n    '/(legacy)/': {\n      id: '/(legacy)/'\n      path: '/'\n      fullPath: '/'\n      preLoaderRoute: typeof legacyIndexRouteImport\n      parentRoute: typeof legacyRouteRoute\n    }\n    '/(legacy)/settings': {\n      id: '/(legacy)/settings'\n      path: '/settings'\n      fullPath: '/settings'\n      preLoaderRoute: typeof legacySettingsRouteImport\n      parentRoute: typeof legacyRouteRoute\n    }\n    '/(legacy)/rules': {\n      id: '/(legacy)/rules'\n      path: '/rules'\n      fullPath: '/rules'\n      preLoaderRoute: typeof legacyRulesRouteImport\n      parentRoute: typeof legacyRouteRoute\n    }\n    '/(legacy)/proxies': {\n      id: '/(legacy)/proxies'\n      path: '/proxies'\n      fullPath: '/proxies'\n      preLoaderRoute: typeof legacyProxiesRouteImport\n      parentRoute: typeof legacyRouteRoute\n    }\n    '/(legacy)/providers': {\n      id: '/(legacy)/providers'\n      path: '/providers'\n      fullPath: '/providers'\n      preLoaderRoute: typeof legacyProvidersRouteImport\n      parentRoute: typeof legacyRouteRoute\n    }\n    '/(legacy)/profiles': {\n      id: '/(legacy)/profiles'\n      path: '/profiles'\n      fullPath: '/profiles'\n      preLoaderRoute: typeof legacyProfilesRouteImport\n      parentRoute: typeof legacyRouteRoute\n    }\n    '/(legacy)/logs': {\n      id: '/(legacy)/logs'\n      path: '/logs'\n      fullPath: '/logs'\n      preLoaderRoute: typeof legacyLogsRouteImport\n      parentRoute: typeof legacyRouteRoute\n    }\n    '/(legacy)/dashboard': {\n      id: '/(legacy)/dashboard'\n      path: '/dashboard'\n      fullPath: '/dashboard'\n      preLoaderRoute: typeof legacyDashboardRouteImport\n      parentRoute: typeof legacyRouteRoute\n    }\n    '/(legacy)/connections': {\n      id: '/(legacy)/connections'\n      path: '/connections'\n      fullPath: '/connections'\n      preLoaderRoute: typeof legacyConnectionsRouteImport\n      parentRoute: typeof legacyRouteRoute\n    }\n    '/(editor)/editor': {\n      id: '/(editor)/editor'\n      path: '/editor'\n      fullPath: '/editor'\n      preLoaderRoute: typeof editorEditorRouteRouteImport\n      parentRoute: typeof rootRouteImport\n    }\n    '/(main)/main/': {\n      id: '/(main)/main/'\n      path: '/main'\n      fullPath: '/main/'\n      preLoaderRoute: typeof mainMainIndexRouteImport\n      parentRoute: typeof mainRouteRoute\n    }\n    '/(editor)/editor/': {\n      id: '/(editor)/editor/'\n      path: '/'\n      fullPath: '/editor/'\n      preLoaderRoute: typeof editorEditorIndexRouteImport\n      parentRoute: typeof editorEditorRouteRoute\n    }\n    '/(main)/main/settings': {\n      id: '/(main)/main/settings'\n      path: '/main/settings'\n      fullPath: '/main/settings'\n      preLoaderRoute: typeof mainMainSettingsRouteRouteImport\n      parentRoute: typeof mainRouteRoute\n    }\n    '/(main)/main/rules': {\n      id: '/(main)/main/rules'\n      path: '/main/rules'\n      fullPath: '/main/rules'\n      preLoaderRoute: typeof mainMainRulesRouteRouteImport\n      parentRoute: typeof mainRouteRoute\n    }\n    '/(main)/main/proxies': {\n      id: '/(main)/main/proxies'\n      path: '/main/proxies'\n      fullPath: '/main/proxies'\n      preLoaderRoute: typeof mainMainProxiesRouteRouteImport\n      parentRoute: typeof mainRouteRoute\n    }\n    '/(main)/main/providers': {\n      id: '/(main)/main/providers'\n      path: '/main/providers'\n      fullPath: '/main/providers'\n      preLoaderRoute: typeof mainMainProvidersRouteRouteImport\n      parentRoute: typeof mainRouteRoute\n    }\n    '/(main)/main/profiles': {\n      id: '/(main)/main/profiles'\n      path: '/main/profiles'\n      fullPath: '/main/profiles'\n      preLoaderRoute: typeof mainMainProfilesRouteRouteImport\n      parentRoute: typeof mainRouteRoute\n    }\n    '/(main)/main/logs': {\n      id: '/(main)/main/logs'\n      path: '/main/logs'\n      fullPath: '/main/logs'\n      preLoaderRoute: typeof mainMainLogsRouteRouteImport\n      parentRoute: typeof mainRouteRoute\n    }\n    '/(main)/main/dashboard': {\n      id: '/(main)/main/dashboard'\n      path: '/main/dashboard'\n      fullPath: '/main/dashboard'\n      preLoaderRoute: typeof mainMainDashboardRouteRouteImport\n      parentRoute: typeof mainRouteRoute\n    }\n    '/(main)/main/connections': {\n      id: '/(main)/main/connections'\n      path: '/main/connections'\n      fullPath: '/main/connections'\n      preLoaderRoute: typeof mainMainConnectionsRouteRouteImport\n      parentRoute: typeof mainRouteRoute\n    }\n    '/(main)/main/settings/': {\n      id: '/(main)/main/settings/'\n      path: '/'\n      fullPath: '/main/settings/'\n      preLoaderRoute: typeof mainMainSettingsIndexRouteImport\n      parentRoute: typeof mainMainSettingsRouteRoute\n    }\n    '/(main)/main/rules/': {\n      id: '/(main)/main/rules/'\n      path: '/'\n      fullPath: '/main/rules/'\n      preLoaderRoute: typeof mainMainRulesIndexRouteImport\n      parentRoute: typeof mainMainRulesRouteRoute\n    }\n    '/(main)/main/proxies/': {\n      id: '/(main)/main/proxies/'\n      path: '/'\n      fullPath: '/main/proxies/'\n      preLoaderRoute: typeof mainMainProxiesIndexRouteImport\n      parentRoute: typeof mainMainProxiesRouteRoute\n    }\n    '/(main)/main/providers/': {\n      id: '/(main)/main/providers/'\n      path: '/'\n      fullPath: '/main/providers/'\n      preLoaderRoute: typeof mainMainProvidersIndexRouteImport\n      parentRoute: typeof mainMainProvidersRouteRoute\n    }\n    '/(main)/main/profiles/': {\n      id: '/(main)/main/profiles/'\n      path: '/'\n      fullPath: '/main/profiles/'\n      preLoaderRoute: typeof mainMainProfilesIndexRouteImport\n      parentRoute: typeof mainMainProfilesRouteRoute\n    }\n    '/(main)/main/logs/': {\n      id: '/(main)/main/logs/'\n      path: '/'\n      fullPath: '/main/logs/'\n      preLoaderRoute: typeof mainMainLogsIndexRouteImport\n      parentRoute: typeof mainMainLogsRouteRoute\n    }\n    '/(main)/main/connections/': {\n      id: '/(main)/main/connections/'\n      path: '/'\n      fullPath: '/main/connections/'\n      preLoaderRoute: typeof mainMainConnectionsIndexRouteImport\n      parentRoute: typeof mainMainConnectionsRouteRoute\n    }\n    '/(main)/main/settings/web-ui': {\n      id: '/(main)/main/settings/web-ui'\n      path: '/web-ui'\n      fullPath: '/main/settings/web-ui'\n      preLoaderRoute: typeof mainMainSettingsWebUiRouteRouteImport\n      parentRoute: typeof mainMainSettingsRouteRoute\n    }\n    '/(main)/main/settings/user-interface': {\n      id: '/(main)/main/settings/user-interface'\n      path: '/user-interface'\n      fullPath: '/main/settings/user-interface'\n      preLoaderRoute: typeof mainMainSettingsUserInterfaceRouteRouteImport\n      parentRoute: typeof mainMainSettingsRouteRoute\n    }\n    '/(main)/main/settings/system': {\n      id: '/(main)/main/settings/system'\n      path: '/system'\n      fullPath: '/main/settings/system'\n      preLoaderRoute: typeof mainMainSettingsSystemRouteRouteImport\n      parentRoute: typeof mainMainSettingsRouteRoute\n    }\n    '/(main)/main/settings/nyanpasu': {\n      id: '/(main)/main/settings/nyanpasu'\n      path: '/nyanpasu'\n      fullPath: '/main/settings/nyanpasu'\n      preLoaderRoute: typeof mainMainSettingsNyanpasuRouteRouteImport\n      parentRoute: typeof mainMainSettingsRouteRoute\n    }\n    '/(main)/main/settings/debug': {\n      id: '/(main)/main/settings/debug'\n      path: '/debug'\n      fullPath: '/main/settings/debug'\n      preLoaderRoute: typeof mainMainSettingsDebugRouteRouteImport\n      parentRoute: typeof mainMainSettingsRouteRoute\n    }\n    '/(main)/main/settings/clash': {\n      id: '/(main)/main/settings/clash'\n      path: '/clash'\n      fullPath: '/main/settings/clash'\n      preLoaderRoute: typeof mainMainSettingsClashRouteRouteImport\n      parentRoute: typeof mainMainSettingsRouteRoute\n    }\n    '/(main)/main/settings/about': {\n      id: '/(main)/main/settings/about'\n      path: '/about'\n      fullPath: '/main/settings/about'\n      preLoaderRoute: typeof mainMainSettingsAboutRouteRouteImport\n      parentRoute: typeof mainMainSettingsRouteRoute\n    }\n    '/(main)/main/profiles/inspect': {\n      id: '/(main)/main/profiles/inspect'\n      path: '/inspect'\n      fullPath: '/main/profiles/inspect'\n      preLoaderRoute: typeof mainMainProfilesInspectRouteRouteImport\n      parentRoute: typeof mainMainProfilesRouteRoute\n    }\n    '/(main)/main/settings/debug/': {\n      id: '/(main)/main/settings/debug/'\n      path: '/'\n      fullPath: '/main/settings/debug/'\n      preLoaderRoute: typeof mainMainSettingsDebugIndexRouteImport\n      parentRoute: typeof mainMainSettingsDebugRouteRoute\n    }\n    '/(main)/main/profiles/$type/': {\n      id: '/(main)/main/profiles/$type/'\n      path: '/$type'\n      fullPath: '/main/profiles/$type/'\n      preLoaderRoute: typeof mainMainProfilesTypeIndexRouteImport\n      parentRoute: typeof mainMainProfilesRouteRoute\n    }\n    '/(main)/main/proxies/group/$name': {\n      id: '/(main)/main/proxies/group/$name'\n      path: '/group/$name'\n      fullPath: '/main/proxies/group/$name'\n      preLoaderRoute: typeof mainMainProxiesGroupNameRouteImport\n      parentRoute: typeof mainMainProxiesRouteRoute\n    }\n    '/(main)/main/providers/rules/$key': {\n      id: '/(main)/main/providers/rules/$key'\n      path: '/rules/$key'\n      fullPath: '/main/providers/rules/$key'\n      preLoaderRoute: typeof mainMainProvidersRulesKeyRouteImport\n      parentRoute: typeof mainMainProvidersRouteRoute\n    }\n    '/(main)/main/providers/proxies/$key': {\n      id: '/(main)/main/providers/proxies/$key'\n      path: '/proxies/$key'\n      fullPath: '/main/providers/proxies/$key'\n      preLoaderRoute: typeof mainMainProvidersProxiesKeyRouteImport\n      parentRoute: typeof mainMainProvidersRouteRoute\n    }\n    '/(main)/main/profiles/$type/detail/$uid': {\n      id: '/(main)/main/profiles/$type/detail/$uid'\n      path: '/$type/detail/$uid'\n      fullPath: '/main/profiles/$type/detail/$uid'\n      preLoaderRoute: typeof mainMainProfilesTypeDetailUidRouteImport\n      parentRoute: typeof mainMainProfilesRouteRoute\n    }\n  }\n}\n\ninterface legacyRouteRouteChildren {\n  legacyConnectionsRoute: typeof legacyConnectionsRoute\n  legacyDashboardRoute: typeof legacyDashboardRoute\n  legacyLogsRoute: typeof legacyLogsRoute\n  legacyProfilesRoute: typeof legacyProfilesRoute\n  legacyProvidersRoute: typeof legacyProvidersRoute\n  legacyProxiesRoute: typeof legacyProxiesRoute\n  legacyRulesRoute: typeof legacyRulesRoute\n  legacySettingsRoute: typeof legacySettingsRoute\n  legacyIndexRoute: typeof legacyIndexRoute\n}\n\nconst legacyRouteRouteChildren: legacyRouteRouteChildren = {\n  legacyConnectionsRoute: legacyConnectionsRoute,\n  legacyDashboardRoute: legacyDashboardRoute,\n  legacyLogsRoute: legacyLogsRoute,\n  legacyProfilesRoute: legacyProfilesRoute,\n  legacyProvidersRoute: legacyProvidersRoute,\n  legacyProxiesRoute: legacyProxiesRoute,\n  legacyRulesRoute: legacyRulesRoute,\n  legacySettingsRoute: legacySettingsRoute,\n  legacyIndexRoute: legacyIndexRoute,\n}\n\nconst legacyRouteRouteWithChildren = legacyRouteRoute._addFileChildren(\n  legacyRouteRouteChildren,\n)\n\ninterface mainMainConnectionsRouteRouteChildren {\n  mainMainConnectionsIndexRoute: typeof mainMainConnectionsIndexRoute\n}\n\nconst mainMainConnectionsRouteRouteChildren: mainMainConnectionsRouteRouteChildren =\n  {\n    mainMainConnectionsIndexRoute: mainMainConnectionsIndexRoute,\n  }\n\nconst mainMainConnectionsRouteRouteWithChildren =\n  mainMainConnectionsRouteRoute._addFileChildren(\n    mainMainConnectionsRouteRouteChildren,\n  )\n\ninterface mainMainLogsRouteRouteChildren {\n  mainMainLogsIndexRoute: typeof mainMainLogsIndexRoute\n}\n\nconst mainMainLogsRouteRouteChildren: mainMainLogsRouteRouteChildren = {\n  mainMainLogsIndexRoute: mainMainLogsIndexRoute,\n}\n\nconst mainMainLogsRouteRouteWithChildren =\n  mainMainLogsRouteRoute._addFileChildren(mainMainLogsRouteRouteChildren)\n\ninterface mainMainProfilesRouteRouteChildren {\n  mainMainProfilesInspectRouteRoute: typeof mainMainProfilesInspectRouteRoute\n  mainMainProfilesIndexRoute: typeof mainMainProfilesIndexRoute\n  mainMainProfilesTypeIndexRoute: typeof mainMainProfilesTypeIndexRoute\n  mainMainProfilesTypeDetailUidRoute: typeof mainMainProfilesTypeDetailUidRoute\n}\n\nconst mainMainProfilesRouteRouteChildren: mainMainProfilesRouteRouteChildren = {\n  mainMainProfilesInspectRouteRoute: mainMainProfilesInspectRouteRoute,\n  mainMainProfilesIndexRoute: mainMainProfilesIndexRoute,\n  mainMainProfilesTypeIndexRoute: mainMainProfilesTypeIndexRoute,\n  mainMainProfilesTypeDetailUidRoute: mainMainProfilesTypeDetailUidRoute,\n}\n\nconst mainMainProfilesRouteRouteWithChildren =\n  mainMainProfilesRouteRoute._addFileChildren(\n    mainMainProfilesRouteRouteChildren,\n  )\n\ninterface mainMainProvidersRouteRouteChildren {\n  mainMainProvidersIndexRoute: typeof mainMainProvidersIndexRoute\n  mainMainProvidersProxiesKeyRoute: typeof mainMainProvidersProxiesKeyRoute\n  mainMainProvidersRulesKeyRoute: typeof mainMainProvidersRulesKeyRoute\n}\n\nconst mainMainProvidersRouteRouteChildren: mainMainProvidersRouteRouteChildren =\n  {\n    mainMainProvidersIndexRoute: mainMainProvidersIndexRoute,\n    mainMainProvidersProxiesKeyRoute: mainMainProvidersProxiesKeyRoute,\n    mainMainProvidersRulesKeyRoute: mainMainProvidersRulesKeyRoute,\n  }\n\nconst mainMainProvidersRouteRouteWithChildren =\n  mainMainProvidersRouteRoute._addFileChildren(\n    mainMainProvidersRouteRouteChildren,\n  )\n\ninterface mainMainProxiesRouteRouteChildren {\n  mainMainProxiesIndexRoute: typeof mainMainProxiesIndexRoute\n  mainMainProxiesGroupNameRoute: typeof mainMainProxiesGroupNameRoute\n}\n\nconst mainMainProxiesRouteRouteChildren: mainMainProxiesRouteRouteChildren = {\n  mainMainProxiesIndexRoute: mainMainProxiesIndexRoute,\n  mainMainProxiesGroupNameRoute: mainMainProxiesGroupNameRoute,\n}\n\nconst mainMainProxiesRouteRouteWithChildren =\n  mainMainProxiesRouteRoute._addFileChildren(mainMainProxiesRouteRouteChildren)\n\ninterface mainMainRulesRouteRouteChildren {\n  mainMainRulesIndexRoute: typeof mainMainRulesIndexRoute\n}\n\nconst mainMainRulesRouteRouteChildren: mainMainRulesRouteRouteChildren = {\n  mainMainRulesIndexRoute: mainMainRulesIndexRoute,\n}\n\nconst mainMainRulesRouteRouteWithChildren =\n  mainMainRulesRouteRoute._addFileChildren(mainMainRulesRouteRouteChildren)\n\ninterface mainMainSettingsDebugRouteRouteChildren {\n  mainMainSettingsDebugIndexRoute: typeof mainMainSettingsDebugIndexRoute\n}\n\nconst mainMainSettingsDebugRouteRouteChildren: mainMainSettingsDebugRouteRouteChildren =\n  {\n    mainMainSettingsDebugIndexRoute: mainMainSettingsDebugIndexRoute,\n  }\n\nconst mainMainSettingsDebugRouteRouteWithChildren =\n  mainMainSettingsDebugRouteRoute._addFileChildren(\n    mainMainSettingsDebugRouteRouteChildren,\n  )\n\ninterface mainMainSettingsRouteRouteChildren {\n  mainMainSettingsAboutRouteRoute: typeof mainMainSettingsAboutRouteRoute\n  mainMainSettingsClashRouteRoute: typeof mainMainSettingsClashRouteRoute\n  mainMainSettingsDebugRouteRoute: typeof mainMainSettingsDebugRouteRouteWithChildren\n  mainMainSettingsNyanpasuRouteRoute: typeof mainMainSettingsNyanpasuRouteRoute\n  mainMainSettingsSystemRouteRoute: typeof mainMainSettingsSystemRouteRoute\n  mainMainSettingsUserInterfaceRouteRoute: typeof mainMainSettingsUserInterfaceRouteRoute\n  mainMainSettingsWebUiRouteRoute: typeof mainMainSettingsWebUiRouteRoute\n  mainMainSettingsIndexRoute: typeof mainMainSettingsIndexRoute\n}\n\nconst mainMainSettingsRouteRouteChildren: mainMainSettingsRouteRouteChildren = {\n  mainMainSettingsAboutRouteRoute: mainMainSettingsAboutRouteRoute,\n  mainMainSettingsClashRouteRoute: mainMainSettingsClashRouteRoute,\n  mainMainSettingsDebugRouteRoute: mainMainSettingsDebugRouteRouteWithChildren,\n  mainMainSettingsNyanpasuRouteRoute: mainMainSettingsNyanpasuRouteRoute,\n  mainMainSettingsSystemRouteRoute: mainMainSettingsSystemRouteRoute,\n  mainMainSettingsUserInterfaceRouteRoute:\n    mainMainSettingsUserInterfaceRouteRoute,\n  mainMainSettingsWebUiRouteRoute: mainMainSettingsWebUiRouteRoute,\n  mainMainSettingsIndexRoute: mainMainSettingsIndexRoute,\n}\n\nconst mainMainSettingsRouteRouteWithChildren =\n  mainMainSettingsRouteRoute._addFileChildren(\n    mainMainSettingsRouteRouteChildren,\n  )\n\ninterface mainRouteRouteChildren {\n  mainMainConnectionsRouteRoute: typeof mainMainConnectionsRouteRouteWithChildren\n  mainMainDashboardRouteRoute: typeof mainMainDashboardRouteRoute\n  mainMainLogsRouteRoute: typeof mainMainLogsRouteRouteWithChildren\n  mainMainProfilesRouteRoute: typeof mainMainProfilesRouteRouteWithChildren\n  mainMainProvidersRouteRoute: typeof mainMainProvidersRouteRouteWithChildren\n  mainMainProxiesRouteRoute: typeof mainMainProxiesRouteRouteWithChildren\n  mainMainRulesRouteRoute: typeof mainMainRulesRouteRouteWithChildren\n  mainMainSettingsRouteRoute: typeof mainMainSettingsRouteRouteWithChildren\n  mainMainIndexRoute: typeof mainMainIndexRoute\n}\n\nconst mainRouteRouteChildren: mainRouteRouteChildren = {\n  mainMainConnectionsRouteRoute: mainMainConnectionsRouteRouteWithChildren,\n  mainMainDashboardRouteRoute: mainMainDashboardRouteRoute,\n  mainMainLogsRouteRoute: mainMainLogsRouteRouteWithChildren,\n  mainMainProfilesRouteRoute: mainMainProfilesRouteRouteWithChildren,\n  mainMainProvidersRouteRoute: mainMainProvidersRouteRouteWithChildren,\n  mainMainProxiesRouteRoute: mainMainProxiesRouteRouteWithChildren,\n  mainMainRulesRouteRoute: mainMainRulesRouteRouteWithChildren,\n  mainMainSettingsRouteRoute: mainMainSettingsRouteRouteWithChildren,\n  mainMainIndexRoute: mainMainIndexRoute,\n}\n\nconst mainRouteRouteWithChildren = mainRouteRoute._addFileChildren(\n  mainRouteRouteChildren,\n)\n\ninterface editorEditorRouteRouteChildren {\n  editorEditorIndexRoute: typeof editorEditorIndexRoute\n}\n\nconst editorEditorRouteRouteChildren: editorEditorRouteRouteChildren = {\n  editorEditorIndexRoute: editorEditorIndexRoute,\n}\n\nconst editorEditorRouteRouteWithChildren =\n  editorEditorRouteRoute._addFileChildren(editorEditorRouteRouteChildren)\n\nconst rootRouteChildren: RootRouteChildren = {\n  legacyRouteRoute: legacyRouteRouteWithChildren,\n  mainRouteRoute: mainRouteRouteWithChildren,\n  editorEditorRouteRoute: editorEditorRouteRouteWithChildren,\n}\nexport const routeTree = rootRouteImport\n  ._addFileChildren(rootRouteChildren)\n  ._addFileTypes<FileRouteTypes>()\n"
  },
  {
    "path": "frontend/nyanpasu/src/services/i18n.ts",
    "content": "import i18n from 'i18next'\nimport { initReactI18next } from 'react-i18next'\nimport en from '../locales/en.json'\nimport ru from '../locales/ru.json'\nimport zhCN from '../locales/zh-CN.json'\nimport zhTW from '../locales/zh-TW.json'\n\nconst resources = {\n  en: { translation: en },\n  ru: { translation: ru },\n  'zh-CN': { translation: zhCN },\n  'zh-TW': { translation: zhTW },\n}\n\ni18n.use(initReactI18next).init({\n  resources,\n  lng: 'en',\n  interpolation: {\n    escapeValue: false,\n  },\n})\n"
  },
  {
    "path": "frontend/nyanpasu/src/services/monaco.ts",
    "content": "/* eslint-disable new-cap */\n// features\n// langs\nimport 'monaco-editor/esm/vs/basic-languages/javascript/javascript.contribution.js'\nimport 'monaco-editor/esm/vs/basic-languages/lua/lua.contribution.js'\nimport 'monaco-editor/esm/vs/basic-languages/yaml/yaml.contribution.js'\nimport 'monaco-editor/esm/vs/editor/editor.all.js'\nimport 'monaco-editor/esm/vs/editor/contrib/links/browser/links.js'\n// language services\nimport * as monaco from 'monaco-editor'\nimport 'monaco-editor/esm/vs/language/typescript/monaco.contribution.js'\nimport editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'\nimport jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'\nimport tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker'\n// workers\nimport yamlWorker from '@/utils/monaco-yaml.worker?worker'\n// others\nimport { loader } from '@monaco-editor/react'\n\nself.MonacoEnvironment = {\n  getWorker(_, label) {\n    switch (label) {\n      case 'json':\n        return new jsonWorker()\n      case 'typescript':\n      case 'javascript':\n        return new tsWorker()\n      case 'yaml':\n        return new yamlWorker()\n      default:\n        return new editorWorker()\n    }\n  },\n}\n\nloader.config({ monaco })\n\nloader\n  .init()\n  .then(() => {\n    console.log('Monaco is ready')\n  })\n  .catch((error) => {\n    console.error('Monaco initialization failed', error)\n  })\n"
  },
  {
    "path": "frontend/nyanpasu/src/services/storage.ts",
    "content": "import { createJSONStorage } from 'jotai/utils'\nimport { type AsyncStringStorage } from 'jotai/vanilla/utils/atomWithStorage'\nimport {\n  getStorageItem,\n  removeStorageItem,\n  setStorageItem,\n} from '@nyanpasu/interface'\n\nconst subscribers: Map<\n  string,\n  Set<(newValue: string | null) => void>\n> = new Map()\n\nexport function dispatchStorageValueChanged(\n  key: string,\n  newValue: string | null,\n) {\n  if (subscribers.has(key)) {\n    const set = subscribers.get(key)\n    set!.forEach((callback) => {\n      callback(newValue)\n    })\n  }\n}\n\nexport const NyanpasuStorage = {\n  getItem(key) {\n    return getStorageItem(key)\n  },\n  setItem(key, newValue) {\n    return setStorageItem(key, newValue)\n  },\n  removeItem(key) {\n    return removeStorageItem(key)\n  },\n  subscribe(key, callback) {\n    if (!subscribers.has(key)) {\n      subscribers.set(key, new Set())\n    }\n    const set = subscribers.get(key)\n    set!.add(callback)\n    return () => {\n      if (subscribers.has(key)) {\n        const set = subscribers.get(key)\n        set!.delete(callback)\n        if (set!.size === 0) {\n          subscribers.delete(key)\n        }\n      }\n    }\n  },\n} satisfies AsyncStringStorage\n\nexport const NyanpasuJSONStorage = createJSONStorage(() => NyanpasuStorage)\n"
  },
  {
    "path": "frontend/nyanpasu/src/services/types.d.ts",
    "content": "type Platform =\n  | 'aix'\n  | 'android'\n  | 'darwin'\n  | 'freebsd'\n  | 'haiku'\n  | 'linux'\n  | 'openbsd'\n  | 'sunos'\n  | 'win32'\n  | 'cygwin'\n  | 'netbsd'\n\n/**\n * defines in `vite.config.ts`\n */\ndeclare const WIN_PORTABLE: boolean\ndeclare const OS_PLATFORM: Platform\n"
  },
  {
    "path": "frontend/nyanpasu/src/store/clash.ts",
    "content": "import { atom } from 'jotai'\nimport type { VergeConfig } from '@nyanpasu/interface'\n\nexport const coreTypeAtom =\n  atom<NonNullable<VergeConfig['clash_core']>>('mihomo')\n"
  },
  {
    "path": "frontend/nyanpasu/src/store/index.ts",
    "content": "import { atom } from 'jotai'\nimport { atomWithStorage, createJSONStorage } from 'jotai/utils'\nimport { SortType } from '@/components/proxies/utils'\nimport { FileRouteTypes } from '@/route-tree.gen'\nimport { NyanpasuStorage } from '@/services/storage'\n\nconst atomWithLocalStorage = <T>(key: string, initialValue: T) => {\n  const getInitialValue = (): T => {\n    const item = localStorage.getItem(key)\n\n    return item ? JSON.parse(item) : initialValue\n  }\n\n  const baseAtom = atom<T>(getInitialValue())\n\n  const derivedAtom = atom(\n    (get) => get(baseAtom),\n    (get, set, update: T | ((prev: T) => T)) => {\n      const nextValue =\n        typeof update === 'function'\n          ? (update as (prev: T) => T)(get(baseAtom))\n          : update\n\n      set(baseAtom, nextValue)\n\n      localStorage.setItem(key, JSON.stringify(nextValue))\n    },\n  )\n\n  return derivedAtom\n}\n\nexport const memorizedRoutePathAtom = atomWithStorage<\n  FileRouteTypes['to'] | null\n>('memorizedRoutePathAtom', null, undefined, {\n  getOnInit: true,\n})\n\nexport const proxyGroupAtom = atomWithLocalStorage<{\n  selector: number | null\n}>('proxyGroupAtom', {\n  selector: 0,\n})\n\nexport const proxyGroupSortAtom = atomWithLocalStorage<SortType>(\n  'proxyGroupSortAtom',\n  SortType.Default,\n)\n\nexport const themeMode = atomWithLocalStorage<'light' | 'dark'>(\n  'themeMode',\n  'light',\n)\n\nexport const atomIsDrawer = atom<boolean>()\n\nexport const atomIsDrawerOnlyIcon = atomWithStorage<boolean>(\n  'atomIsDrawerOnlyIcon',\n  true,\n)\n\n// save the state of each profile item loading\nexport const atomLoadingCache = atom<Record<string, boolean>>({})\n\n// save update state\nexport const atomUpdateState = atom<boolean>(false)\n\ninterface IConnectionSetting {\n  layout: 'table' | 'list'\n}\n\nexport const atomConnectionSetting = atom<IConnectionSetting>({\n  layout: 'table',\n})\n\n// TODO: generate default columns based on COLUMNS\nexport const connectionTableColumnsAtom = atomWithStorage<\n  Array<[string, boolean]>\n>(\n  'connections_table_columns',\n  [\n    'host',\n    'process',\n    'downloaded',\n    'uploaded',\n    'dl_speed',\n    'ul_speed',\n    'chains',\n    'rule',\n    'time',\n    'source',\n    'destination_ip',\n    'destination_asn',\n    'type',\n  ].map((key) => [key, true]),\n  createJSONStorage(() => NyanpasuStorage),\n)\n\n// export const themeSchemeAtom = atom<MDYTheme[\"schemes\"] | null>(null);\n"
  },
  {
    "path": "frontend/nyanpasu/src/store/proxies.ts",
    "content": "import { atomWithStorage } from 'jotai/utils'\n\nexport const proxiesFilterAtom = atomWithStorage<string | null>(\n  'proxiesFilterAtom',\n  null,\n)\n"
  },
  {
    "path": "frontend/nyanpasu/src/store/service.ts",
    "content": "import { atom } from 'jotai'\n\nexport const serviceManualPromptDialogAtom = atom<\n  'install' | 'uninstall' | 'start' | 'stop' | null\n>(null)\n"
  },
  {
    "path": "frontend/nyanpasu/src/store/updater.ts",
    "content": "import { atom } from 'jotai'\nimport { atomWithStorage } from 'jotai/utils'\nimport { type Update } from '@tauri-apps/plugin-updater'\n\nexport const UpdaterIgnoredAtom = atomWithStorage(\n  'updaterIgnored',\n  null as string | null,\n)\n\nexport const UpdaterInstanceAtom = atom<Update | null>(null)\n"
  },
  {
    "path": "frontend/nyanpasu/src/utils/chain.ts",
    "content": "export function chains<T>(\n  ...handlers: Array<((event: T) => void) | undefined>\n) {\n  return (event: T) => {\n    handlers.forEach((handler) => {\n      if (handler) {\n        handler(event)\n      }\n    })\n  }\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/utils/get-system.ts",
    "content": "import { getSystem } from '@nyanpasu/ui'\n\nexport default getSystem\n"
  },
  {
    "path": "frontend/nyanpasu/src/utils/ignore-case.ts",
    "content": "// oxlint-disable typescript/no-explicit-any\n// Deep copy and change all keys to lowercase\ntype TData = Record<string, any>\n\nexport default function ignoreCase(data: TData): TData {\n  if (!data) return {}\n\n  const newData = {} as TData\n\n  Object.entries(data).forEach(([key, value]) => {\n    newData[key.toLowerCase()] = JSON.parse(JSON.stringify(value))\n  })\n\n  return newData\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/utils/index.ts",
    "content": "// oxlint-disable typescript/no-explicit-any\nimport { includes, isArray, isObject, isString, some } from 'lodash-es'\nimport { EnvInfo } from '@nyanpasu/interface'\n\n/**\n * classNames filter out falsy values and join the rest with a space\n * @param classes - array of classes\n * @returns string of classes\n */\nexport function classNames(...classes: any[]) {\n  return classes.filter(Boolean).join(' ')\n}\n\nexport async function sleep(ms: number) {\n  return new Promise((resolve) => setTimeout(resolve, ms))\n}\n\nexport const containsSearchTerm = (obj: any, term: string): boolean => {\n  if (!obj || !term) return false\n\n  if (isString(obj)) {\n    return includes(obj.toLowerCase(), term.toLowerCase())\n  }\n\n  if (isObject(obj) || isArray(obj)) {\n    return some(obj, (value: any) => containsSearchTerm(value, term))\n  }\n\n  return false\n}\n\nexport function formatError(err: unknown): string {\n  return `Error: ${err instanceof Error ? err.message : String(err)}`\n}\n\nexport function formatEnvInfos(envs: EnvInfo) {\n  let result = '----------- System -----------\\n'\n  result += `OS: ${envs.os}\\n`\n  result += `Arch: ${envs.arch}\\n`\n  result += `----------- Device -----------\\n`\n  for (const cpu of envs.device.cpu) {\n    result += `CPU: ${cpu}\\n`\n  }\n  result += `Memory: ${envs.device.memory}\\n`\n  result += `----------- Core -----------\\n`\n  for (const key in envs.core) {\n    result += `${key}: \\`${envs.core[key]}\\`\\n`\n  }\n  result += `----------- Build Info -----------\\n`\n  for (const k of Object.keys(envs.build_info) as string[]) {\n    const key = k\n      .split('_')\n      .map((v: string) => v.charAt(0).toUpperCase() + v.slice(1))\n      .join(' ')\n    // Fix linter error: explicitly type k as keyof typeof envs.build_info\n    result += `${key}: ${envs.build_info[k as keyof typeof envs.build_info]}\\n`\n  }\n\n  return result\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/utils/language.ts",
    "content": "import { defineCustomClientStrategy, locales } from '@/paraglide/runtime'\n\nexport const languageOptions = {\n  en: 'English',\n  ru: 'Русский',\n  'zh-CN': '简体中文',\n  'zh-TW': '繁體中文',\n}\n\nexport const languageQuirks: {\n  [key: string]: {\n    drawer: {\n      minWidth: number\n      itemClassNames?: string\n    }\n  }\n} = {\n  en: {\n    drawer: {\n      minWidth: 240,\n    },\n  },\n  ru: {\n    drawer: {\n      minWidth: 240,\n    },\n  },\n  'zh-CN': {\n    drawer: {\n      minWidth: 180,\n    },\n  },\n  'zh-TW': {\n    drawer: {\n      minWidth: 180,\n    },\n  },\n}\n\nexport type Language = (typeof locales)[number]\n\nexport const LANGUAGE_STORAGE_KEY = 'paraglide-language-cache'\n\nexport const DEFAULT_LANGUAGE = 'en'\n\n// encode the language storage key to avoid special characters\nconst CACHED_LANGUAGE_STORAGE_KEY = btoa(LANGUAGE_STORAGE_KEY)\n\nexport const setCachedLanguage = (locale: Language) => {\n  localStorage.setItem(CACHED_LANGUAGE_STORAGE_KEY, locale)\n}\n\nexport const getCachedLanguage = () => {\n  const value = localStorage.getItem(CACHED_LANGUAGE_STORAGE_KEY)\n\n  return value && locales.includes(value as Language)\n    ? (value as Language)\n    : DEFAULT_LANGUAGE\n}\n\ndefineCustomClientStrategy('custom-extension', {\n  getLocale: () => {\n    return getCachedLanguage()\n  },\n  setLocale: (locale) => {\n    setCachedLanguage(locale as Language)\n  },\n})\n"
  },
  {
    "path": "frontend/nyanpasu/src/utils/monaco-yaml.worker.ts",
    "content": "// This file just to fix https://github.com/remcohaszing/monaco-yaml?tab=readme-ov-file#why-doesnt-it-work-with-vite\n\nimport 'monaco-yaml/yaml.worker.js'\n"
  },
  {
    "path": "frontend/nyanpasu/src/utils/mui-theme.ts",
    "content": "// From https://github.com/RobinTail/merge-sx\n\nimport type { SxProps } from '@mui/material'\n\ntype PureSx<T extends object> = Exclude<SxProps<T>, ReadonlyArray<unknown>>\ntype SxAsArray<T extends object> = Array<PureSx<T>>\n\nexport const mergeSxProps = <T extends object>(\n  ...styles: (SxProps<T> | false | undefined)[]\n): SxProps<T> => {\n  const capacitor: SxAsArray<T> = []\n  for (const sx of styles) {\n    if (sx) {\n      if (Array.isArray(sx)) {\n        for (const sub of sx as SxAsArray<T>) {\n          capacitor.push(sub)\n        }\n      } else {\n        capacitor.push(sx as PureSx<T>)\n      }\n    }\n  }\n  return capacitor\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/utils/mutation.ts",
    "content": "import { useCallback } from 'react'\nimport { cache, mutate } from 'swr/_internal'\n\nexport const useGlobalMutation = () => {\n  return useCallback((swrKey, ...args) => {\n    const matcher = typeof swrKey === 'function' ? swrKey : undefined\n\n    if (matcher) {\n      // eslint-disable-next-line @typescript-eslint/no-explicit-any\n      const keys = Array.from(cache.keys()).filter(matcher as any)\n      keys.forEach((key) => mutate(key, ...args))\n    } else {\n      mutate(swrKey, ...args)\n    }\n  }, []) as typeof mutate\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/utils/notification.ts",
    "content": "import { Notice } from '@/components/base'\nimport { isPortable } from '@nyanpasu/interface'\nimport {\n  MessageDialogOptions,\n  message as tauriMessage,\n} from '@tauri-apps/plugin-dialog'\nimport {\n  isPermissionGranted,\n  Options,\n  requestPermission,\n  sendNotification,\n} from '@tauri-apps/plugin-notification'\n\nlet permissionGranted: boolean | null = null\nlet portable: boolean | null = null\n\nconst checkPermission = async () => {\n  if (permissionGranted == null) {\n    permissionGranted = await isPermissionGranted()\n  }\n  if (!permissionGranted) {\n    const permission = await requestPermission()\n    permissionGranted = permission === 'granted'\n  }\n  return permissionGranted\n}\n\nexport type NotificationOptions = {\n  title: string\n  body?: string\n  type?: NotificationType\n}\n\nexport enum NotificationType {\n  Success = 'success',\n  Info = 'info',\n  // Warn = \"warn\",\n  Error = 'error',\n}\n\nexport const notification = async ({\n  title,\n  body,\n  type = NotificationType.Info,\n}: NotificationOptions) => {\n  if (!title) {\n    throw new Error('missing message argument!')\n  }\n  if (portable === null) {\n    portable = await isPortable()\n  }\n  const permissionGranted = portable || (await checkPermission())\n  if (portable || !permissionGranted) {\n    // fallback to mui notification\n    Notice[type](`${title} ${body ? `: ${body}` : ''}`)\n    // throw new Error(\"notification permission not granted!\");\n    return\n  }\n  const options: Options = {\n    title,\n  }\n  if (body) options.body = body\n  sendNotification(options)\n}\n\nexport const message = async (\n  value: string,\n  options?: string | MessageDialogOptions | undefined,\n) => {\n  if (typeof options === 'object') {\n    await tauriMessage(value, {\n      ...options,\n      title: options.title\n        ? `Clash Nyanpasu - ${options.title}`\n        : 'Clash Nyanpasu',\n    })\n  } else {\n    await tauriMessage(value, options)\n  }\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/utils/parse-hotkey.ts",
    "content": "const KEY_MAP: Record<string, string> = {\n  '\"': \"'\",\n  ':': ';',\n  '?': '/',\n  '>': '.',\n  '<': ',',\n  '{': '[',\n  '}': ']',\n  '|': '\\\\',\n  '!': '1',\n  '@': '2',\n  '#': '3',\n  $: '4',\n  '%': '5',\n  '^': '6',\n  '&': '7',\n  '*': '8',\n  '(': '9',\n  ')': '0',\n  '~': '`',\n}\n\nexport const parseHotkey = (key: string) => {\n  let temp = key.toUpperCase()\n\n  if (temp.startsWith('ARROW')) {\n    temp = temp.slice(5)\n  } else if (temp.startsWith('DIGIT')) {\n    temp = temp.slice(5)\n  } else if (temp.startsWith('KEY')) {\n    temp = temp.slice(3)\n  } else if (temp.endsWith('LEFT')) {\n    temp = temp.slice(0, -4)\n  } else if (temp.endsWith('RIGHT')) {\n    temp = temp.slice(0, -5)\n  }\n\n  switch (temp) {\n    case 'CONTROL':\n      return 'CTRL'\n    case 'META':\n      return 'CMD'\n    case ' ':\n      return 'SPACE'\n    default:\n      return KEY_MAP[temp] || temp\n  }\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/utils/parse-traffic.ts",
    "content": "const UNITS = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']\n\nconst parseTraffic = (num?: string | number) => {\n  if (typeof num !== 'number') {\n    const tmp = Number(num)\n    if (isNaN(tmp)) return ['NaN', '']\n    num = tmp\n  }\n\n  // 处理负数或零的情况\n  if (num <= 0) return ['0', 'B']\n\n  // 使用 Math.log 而不是 Math.log2 来提高精度\n  const exp = Math.min(\n    Math.floor(Math.log(num) / Math.log(1024)),\n    UNITS.length - 1,\n  )\n  const dat = num / Math.pow(1024, exp)\n\n  // 对于非常小的数字，确保至少显示一位小数\n  let ret: string\n  if (dat < 1) {\n    ret = dat.toPrecision(2)\n  } else if (dat < 10) {\n    ret = dat.toPrecision(3)\n  } else {\n    ret = dat >= 1000 ? dat.toFixed(0) : dat.toPrecision(3)\n  }\n\n  const unit = UNITS[exp]\n\n  return [ret, unit]\n}\n\nexport default parseTraffic\n"
  },
  {
    "path": "frontend/nyanpasu/src/utils/routes-utils.ts",
    "content": "import {\n  Apps,\n  Dashboard,\n  DesignServices,\n  GridView,\n  Public,\n  Settings,\n  SettingsEthernet,\n  SvgIconComponent,\n  Terminal,\n} from '@mui/icons-material'\n\nconst routes: { [key: string]: SvgIconComponent } = {\n  dashboard: Dashboard,\n  proxies: Public,\n  profiles: GridView,\n  connections: SettingsEthernet,\n  rules: DesignServices,\n  logs: Terminal,\n  settings: Settings,\n  providers: Apps,\n}\n\nexport const getRoutes = () => {\n  return Object.keys(routes).reduce(\n    (acc, key) => {\n      acc[key] = `/${key}`\n      return acc\n    },\n    {} as { [key: string]: string },\n  )\n}\n\nexport const getRoutesWithIcon = () => {\n  return Object.keys(routes).reduce(\n    (acc, key) => {\n      acc[key] = {\n        path: `/${key}`,\n        icon: routes[key],\n      }\n      return acc\n    },\n    {} as {\n      [key: string]: { path: string; icon: SvgIconComponent }\n    },\n  )\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/utils/shiki.ts",
    "content": "import type { Highlighter } from 'shiki'\nimport { getSingletonHighlighterCore } from 'shiki/core'\nimport { createOnigurumaEngine } from 'shiki/engine/oniguruma'\nimport minLight from 'shiki/themes/min-light.mjs'\nimport nord from 'shiki/themes/nord.mjs'\nimport getWasm from 'shiki/wasm'\n\nlet shiki: Highlighter | null = null\n\nexport async function getShikiSingleton() {\n  if (!shiki) {\n    shiki = (await getSingletonHighlighterCore({\n      engine: createOnigurumaEngine(getWasm),\n      themes: [nord, minLight],\n      langs: [() => import('shiki/langs/shell.mjs')],\n    })) as Highlighter\n  }\n  return shiki\n}\n\nexport async function formatAnsi(str: string) {\n  const instance = await getShikiSingleton()\n  return instance.codeToHtml(str, {\n    lang: 'ansi',\n    themes: {\n      dark: 'nord',\n      light: 'min-light',\n    },\n  })\n}\n"
  },
  {
    "path": "frontend/nyanpasu/src/utils/styled.ts",
    "content": "export function insertStyle(id: string, style: string) {\n  removeStyle(id)\n\n  const waitInsertStyle = document.createElement('style')\n  waitInsertStyle.id = id\n  waitInsertStyle.innerHTML = style\n  document.head.appendChild(waitInsertStyle)\n\n  return waitInsertStyle\n}\n\nexport function removeStyle(id: string) {\n  const originalElement = document.getElementById(id)\n\n  if (originalElement) {\n    document.head.removeChild(originalElement)\n  }\n}\n"
  },
  {
    "path": "frontend/nyanpasu/tailwind.config.ts",
    "content": "import type { Config } from 'tailwindcss'\nimport createPlugin from 'tailwindcss/plugin'\nimport { MUI_BREAKPOINTS } from '@nyanpasu/ui/src/materialYou/themeConsts.mjs'\n\nconst getMUIScreen = () => {\n  const breakpoints = MUI_BREAKPOINTS.values as Record<string, number>\n\n  const result = {} as Record<string, string>\n\n  for (const key in breakpoints) {\n    if (Object.prototype.hasOwnProperty.call(breakpoints, key)) {\n      result[key] = `${breakpoints[key]}px`\n    }\n  }\n\n  return result\n}\n\n/** @type {import('tailwindcss').Config} */\nmodule.exports = {\n  content: ['./src/**/*.{tsx,ts}', '../ui/**/*.{tsx,ts}'],\n  darkMode: 'selector',\n  theme: {\n    extend: {\n      maxHeight: {\n        '1/8': 'calc(100vh / 8)',\n      },\n      zIndex: {\n        top: 100000,\n      },\n      animation: {\n        marquee: 'marquee 4s linear infinite',\n      },\n      keyframes: {\n        marquee: {\n          '0%': { transform: 'translateX(100%)' },\n          '100%': { transform: 'translateX(-100%)' },\n        },\n      },\n      colors: {\n        scroller: 'var(--scroller-color)',\n        container: 'var(--background-color)',\n      },\n    },\n    screen: getMUIScreen(),\n  },\n  plugins: [\n    createPlugin(({ addBase }) => {\n      addBase({\n        '.scrollbar-hidden::-webkit-scrollbar': {\n          width: '0px',\n        },\n      })\n    }),\n  ],\n} satisfies Config\n"
  },
  {
    "path": "frontend/nyanpasu/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"target\": \"ESNext\",\n    \"useDefineForClassFields\": true,\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ESNext\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"esModuleInterop\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"allowArbitraryExtensions\": true,\n    \"strict\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"preserve\",\n    \"composite\": true,\n    \"paths\": {\n      \"@root/*\": [\"../../*\"],\n      \"@/*\": [\"./src/*\"],\n      \"~/*\": [\"./*\"],\n    },\n    \"jsxImportSource\": \"@emotion/react\",\n    \"types\": [\"unplugin-icons/types/react\"],\n  },\n  \"include\": [\"src\", \"./auto-imports.d.ts\"],\n  \"references\": [\n    { \"path\": \"../interface\" },\n    { \"path\": \"../ui\" },\n    { \"path\": \"./tsconfig.node.json\" },\n  ],\n}\n"
  },
  {
    "path": "frontend/nyanpasu/tsconfig.node.json",
    "content": "{\n  \"include\": [\"vite.config.*\", \"tailwind.config.ts\"],\n  \"compilerOptions\": {\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"esModuleInterop\": true,\n    \"strict\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"preserve\",\n    \"composite\": true,\n    \"types\": [\"node\", \"vite/client\"],\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"allowSyntheticDefaultImports\": true\n  }\n}\n"
  },
  {
    "path": "frontend/nyanpasu/tsr.config.json",
    "content": "{\n  \"routeFileIgnorePrefix\": \"-\",\n  \"routesDirectory\": \"./src/pages\",\n  \"autoCodeSplitting\": true\n}\n"
  },
  {
    "path": "frontend/nyanpasu/vite.config.ts",
    "content": "import path from 'node:path'\nimport { NodePackageImporter } from 'sass-embedded'\nimport AutoImport from 'unplugin-auto-import/vite'\nimport IconsResolver from 'unplugin-icons/resolver'\nimport Icons from 'unplugin-icons/vite'\nimport { defineConfig, UserConfig } from 'vite'\nimport { createHtmlPlugin } from 'vite-plugin-html'\nimport sassDts from 'vite-plugin-sass-dts'\nimport svgr from 'vite-plugin-svgr'\nimport tsconfigPaths from 'vite-tsconfig-paths'\nimport { paraglideVitePlugin } from '@inlang/paraglide-js'\n// import tailwindPlugin from '@tailwindcss/vite'\n// import react from \"@vitejs/plugin-react\";\nimport { tanstackRouter } from '@tanstack/router-plugin/vite'\nimport legacy from '@vitejs/plugin-legacy'\nimport react from '@vitejs/plugin-react-swc'\n\nconst IS_NIGHTLY = process.env.NIGHTLY?.toLowerCase() === 'true'\n\nconst builtinVars = () => {\n  return {\n    name: 'built-in-vars',\n    transformIndexHtml(html: string) {\n      return html.replace(\n        /<\\/head>/,\n        `<script>window.__IS_NIGHTLY__ = ${IS_NIGHTLY ? 'true' : 'false'}</script></head>`,\n      )\n    },\n  }\n}\n\n// https://vitejs.dev/config/\nexport default defineConfig(({ command, mode }) => {\n  const isDev = command === 'serve'\n\n  const config = {\n    // root: \"/\",\n    clearScreen: false,\n    server: {\n      port: 3000,\n      watch: {\n        ignored: ['**/*.scss.d.ts'],\n      },\n    },\n    css: {\n      preprocessorOptions: {\n        scss: {\n          api: 'modern-compiler',\n          // @ts-expect-error fucking vite why embedded their own sass types definition????\n          importer: [\n            new NodePackageImporter(),\n            // TODO: fix this when vite-sass-dts support it, or fix it when we use `@alias`\n            // (...args: string[]) => {\n            //   if (args[0] !== '@/styles') {\n            //     return\n            //   }\n\n            //   return {\n            //     file: `${path.resolve(__dirname, './src/assets/styles')}`,\n            //   }\n            // },\n          ],\n        },\n      },\n    },\n    plugins: [\n      // tailwindPlugin(),\n      tsconfigPaths(),\n      legacy({\n        renderLegacyChunks: false,\n        modernTargets: ['edge>=109', 'safari>=13'],\n        modernPolyfills: true,\n        additionalModernPolyfills: [\n          'core-js/modules/es.object.has-own.js',\n          'core-js/modules/web.structured-clone.js',\n          'core-js/modules/es.array.at.js',\n        ],\n      }),\n      createHtmlPlugin({\n        inject: {\n          data: {\n            title: 'Clash Nyanpasu',\n            injectScript:\n              mode === 'development'\n                ? '<script src=\"https://unpkg.com/react-scan/dist/auto.global.js\"></script>'\n                : '',\n          },\n        },\n      }),\n      builtinVars(),\n      tanstackRouter({\n        target: 'react',\n        autoCodeSplitting: true,\n        routesDirectory: `src/pages`,\n        generatedRouteTree: `src/route-tree.gen.ts`,\n        routeFileIgnorePattern: '_modules',\n      }),\n      svgr(),\n      react({\n        // babel: {\n        //   plugins: [\"@emotion/babel-plugin\"],\n        // },\n      }),\n      AutoImport({\n        resolvers: [\n          IconsResolver({\n            prefix: 'Icon',\n            extension: 'jsx',\n          }),\n        ],\n      }),\n      Icons({\n        compiler: 'jsx', // or 'solid'\n      }),\n      sassDts({ esmExport: true }),\n      paraglideVitePlugin({\n        project: './project.inlang',\n        outdir: './src/paraglide',\n        strategy: ['custom-extension'],\n      }),\n    ],\n    resolve: {\n      alias: {\n        '@repo': path.resolve('../../'),\n        '@nyanpasu/ui': path.resolve('../ui/src'),\n        '@nyanpasu/interface': path.resolve('../interface/src'),\n      },\n    },\n    optimizeDeps: {\n      entries: ['./src/main.tsx'],\n      include: ['@emotion/styled'],\n    },\n    esbuild: {\n      drop: isDev ? undefined : ['debugger'],\n      pure: isDev || IS_NIGHTLY ? [] : ['console.log'],\n    },\n    build: {\n      outDir: '../../backend/tauri/tmp/dist',\n      rollupOptions: {\n        output: {\n          manualChunks: {\n            jsonWorker: [`monaco-editor/esm/vs/language/json/json.worker`],\n            tsWorker: [`monaco-editor/esm/vs/language/typescript/ts.worker`],\n            editorWorker: [`monaco-editor/esm/vs/editor/editor.worker`],\n            yamlWorker: [`monaco-yaml/yaml.worker`],\n          },\n        },\n      },\n      emptyOutDir: true,\n      sourcemap: isDev || IS_NIGHTLY ? 'inline' : false,\n    },\n    define: {\n      OS_PLATFORM: `\"${process.platform}\"`,\n      WIN_PORTABLE: !!process.env.VITE_WIN_PORTABLE,\n    },\n    html: {},\n  } satisfies UserConfig\n  // fucking vite why embedded their own sass types definition????\n  // oxlint-disable-next-line typescript/no-explicit-any\n  return config as any as UserConfig\n})\n"
  },
  {
    "path": "frontend/ui/package.json",
    "content": "{\n  \"name\": \"@nyanpasu/ui\",\n  \"version\": \"2.0.0\",\n  \"type\": \"module\",\n  \"main\": \"./src/index.ts\",\n  \"files\": [\n    \"dist\",\n    \"src\"\n  ],\n  \"scripts\": {\n    \"build\": \"vite build --sourcemap\"\n  },\n  \"dependencies\": {\n    \"@material/material-color-utilities\": \"0.4.0\",\n    \"@mui/icons-material\": \"7.3.9\",\n    \"@mui/lab\": \"7.0.0-beta.17\",\n    \"@mui/material\": \"7.3.9\",\n    \"@radix-ui/react-portal\": \"1.1.10\",\n    \"@radix-ui/react-scroll-area\": \"1.2.10\",\n    \"@tauri-apps/api\": \"2.10.1\",\n    \"@types/d3\": \"7.4.3\",\n    \"@types/react\": \"19.2.14\",\n    \"@vitejs/plugin-react\": \"5.2.0\",\n    \"ahooks\": \"3.9.6\",\n    \"d3\": \"7.9.0\",\n    \"framer-motion\": \"12.38.0\",\n    \"react\": \"19.2.4\",\n    \"react-dom\": \"19.2.4\",\n    \"react-error-boundary\": \"6.0.0\",\n    \"react-i18next\": \"15.7.4\",\n    \"react-use\": \"17.6.0\",\n    \"tailwindcss\": \"4.2.2\",\n    \"vite\": \"7.3.1\",\n    \"vite-tsconfig-paths\": \"6.1.1\"\n  },\n  \"devDependencies\": {\n    \"@emotion/react\": \"11.14.0\",\n    \"@types/d3-interpolate-path\": \"2.0.3\",\n    \"clsx\": \"2.1.1\",\n    \"d3-interpolate-path\": \"2.3.0\",\n    \"sass-embedded\": \"1.98.0\",\n    \"tailwind-merge\": \"3.5.0\",\n    \"typescript-plugin-css-modules\": \"5.2.0\",\n    \"vite-plugin-dts\": \"4.5.4\"\n  }\n}\n"
  },
  {
    "path": "frontend/ui/src/chart/index.ts",
    "content": "export * from './sparkline'\n"
  },
  {
    "path": "frontend/ui/src/chart/sparkline.tsx",
    "content": "// oxlint-disable typescript/no-explicit-any\nimport * as d3 from 'd3'\nimport { interpolatePath } from 'd3-interpolate-path'\nimport { CSSProperties, FC, useEffect, useMemo, useRef } from 'react'\nimport { alpha, useColorScheme, useTheme } from '@mui/material'\n\ninterface SparklineProps {\n  data: number[]\n  className?: string\n  style?: CSSProperties\n  visible?: boolean\n}\n\nexport const Sparkline: FC<SparklineProps> = ({\n  data,\n  className,\n  style,\n  visible = true,\n}) => {\n  const theme = useTheme()\n  const { mode } = useColorScheme()\n\n  const lineColor = useMemo(\n    () =>\n      mode === 'dark'\n        ? alpha(theme.colorSchemes.light!.palette.primary.main, 0.7)\n        : alpha(theme.colorSchemes.dark!.palette.primary.main, 0.7),\n    [mode, theme],\n  )\n\n  const areaColor = useMemo(\n    () =>\n      mode === 'dark'\n        ? alpha(theme.colorSchemes.light!.palette.primary.main, 0.1)\n        : alpha(theme.colorSchemes.dark!.palette.primary.main, 0.1),\n    [mode, theme],\n  )\n\n  const svgRef = useRef<SVGSVGElement | null>(null)\n\n  useEffect(() => {\n    if (!svgRef.current) return\n\n    const svg = d3.select(svgRef.current)\n\n    const { width, height } = svg.node()?.getBoundingClientRect() ?? {\n      width: 0,\n      height: 0,\n    }\n\n    const maxHeight = () => {\n      const dataRange = d3.max(data)! - d3.min(data)!\n\n      if (dataRange / d3.max(data)! < 0.1) {\n        return height * 0.65\n      }\n\n      if (d3.max(data)) {\n        return height * 0.35\n      } else {\n        return height\n      }\n    }\n\n    const xScale = d3\n      .scaleLinear()\n      .domain([0, data.length - 1])\n      .range([0, width])\n\n    const yScale = d3\n      .scaleLinear()\n      .domain([0, d3.max(data) ?? 0])\n      .range([height, maxHeight()])\n\n    const line = d3\n      .line<number>()\n      .x((d, i) => xScale(i))\n      .y((d) => yScale(d))\n      .curve(d3.curveCatmullRom.alpha(0.5))\n\n    const area = d3\n      .area<number>()\n      .x((d, i) => xScale(i))\n      .y0(height)\n      .y1((d) => yScale(d))\n      .curve(d3.curveCatmullRom.alpha(0.5))\n\n    svg.selectAll('*').remove()\n\n    svg\n      .append('path')\n      .datum(data)\n      .attr('class', 'area')\n      .attr('fill', areaColor)\n      .attr('d', area)\n\n    svg\n      .append('path')\n      .datum(data)\n      .attr('class', 'line')\n      .attr('fill', 'none')\n      .attr('stroke', lineColor)\n      .attr('stroke-width', 2)\n      .attr('d', line)\n\n    const updateChart = () => {\n      // Skip animation if component is not visible to prevent performance issues\n      if (!visible) {\n        // Update without animation\n        svg.select('.area').datum(data).attr('d', area)\n        svg.select('.line').datum(data).attr('d', line)\n        return\n      }\n\n      xScale.domain([0, data.length - 1])\n      yScale.domain([0, d3.max(data) ?? 0])\n\n      const t = svg.transition().duration(750).ease(d3.easeCubic)\n      svg\n        .select('.area')\n        .datum(data)\n        .transition(t as any)\n        .attrTween('d', function (d) {\n          const previous = d3.select(this).attr('d')\n          const current = area(d)\n          return interpolatePath(previous, current as string)\n        })\n\n      svg\n        .select('.line')\n        .datum(data)\n        .transition(t as any)\n        .attrTween('d', function (d) {\n          const previous = d3.select(this).attr('d')\n          const current = line(d)\n          return interpolatePath(previous, current as string)\n        })\n    }\n\n    updateChart()\n  }, [data, lineColor, areaColor, visible])\n\n  return (\n    <svg\n      className={className}\n      ref={svgRef}\n      style={{ width: '100%', height: '100%', ...style }}\n    />\n  )\n}\n"
  },
  {
    "path": "frontend/ui/src/hooks/get-system.ts",
    "content": "type Platform =\n  | 'aix'\n  | 'android'\n  | 'darwin'\n  | 'freebsd'\n  | 'haiku'\n  | 'linux'\n  | 'openbsd'\n  | 'sunos'\n  | 'win32'\n  | 'cygwin'\n  | 'netbsd'\n\ndeclare const OS_PLATFORM: Platform | undefined\n\n// get the system os\n// according to UA\nexport function getSystem() {\n  const ua = typeof window === 'undefined' ? '' : window.navigator?.userAgent\n  const platform = typeof OS_PLATFORM !== 'undefined' ? OS_PLATFORM : 'unknown'\n\n  if (ua.includes('Mac OS X') || platform === 'darwin') return 'macos'\n\n  if (/win64|win32/i.test(ua) || platform === 'win32') return 'windows'\n\n  if (/linux/i.test(ua)) return 'linux'\n\n  return 'unknown'\n}\n"
  },
  {
    "path": "frontend/ui/src/hooks/index.ts",
    "content": "export * from './use-breakpoint'\nexport * from './use-click-position'\nexport * from './get-system'\n"
  },
  {
    "path": "frontend/ui/src/hooks/use-breakpoint.ts",
    "content": "import { useAsyncEffect } from 'ahooks'\nimport { RefObject, useEffect, useMemo, useState } from 'react'\nimport { createBreakpoint } from 'react-use'\nimport { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'\nimport { MUI_BREAKPOINTS } from '../materialYou/themeConsts.mjs'\n\nexport type Breakpoint = 'xs' | 'sm' | 'md' | 'lg' | 'xl'\n\nconst breakpointsOrder: Breakpoint[] = ['xs', 'sm', 'md', 'lg', 'xl']\n\nconst BREAKPOINT_VALUES = MUI_BREAKPOINTS.values as Record<Breakpoint, number>\n\nexport const useBreakpoint = createBreakpoint(\n  BREAKPOINT_VALUES,\n) as () => Breakpoint\n\ntype BreakpointEffectCallback = (currentBreakpoint: Breakpoint) => void\n\nexport const useBreakpointEffect = (callback: BreakpointEffectCallback) => {\n  const currentBreakpoint = useBreakpoint()\n\n  useEffect(() => {\n    callback(currentBreakpoint)\n  }, [currentBreakpoint, callback])\n}\n\ntype BreakpointValues<T> = Partial<Record<Breakpoint, T>>\n\nexport const useBreakpointValue = <T>(\n  values: BreakpointValues<T>,\n  defaultValue?: T,\n): T => {\n  const currentBreakpoint = useBreakpoint()\n  const calculateValue = (): T => {\n    const value = values[currentBreakpoint]\n\n    if (value !== undefined) {\n      return value as T\n    }\n\n    const currentIndex = breakpointsOrder.indexOf(currentBreakpoint)\n\n    for (let i = currentIndex; i >= 0; i--) {\n      const fallbackValue = values[breakpointsOrder[i]]\n\n      if (fallbackValue !== undefined) {\n        return fallbackValue as T\n      }\n    }\n\n    return defaultValue ?? (values[breakpointsOrder[0]] as T)\n  }\n\n  const [result, setResult] = useState<T>(calculateValue)\n\n  useAsyncEffect(async () => {\n    const appWindow = getCurrentWebviewWindow()\n    if (!(await appWindow.isMinimized())) {\n      if (result !== calculateValue) {\n        setResult(calculateValue)\n      }\n    }\n  }, [currentBreakpoint, values, defaultValue])\n\n  return result\n}\n\nconst getBreakpointFromWidth = (width: number): Breakpoint => {\n  for (let i = breakpointsOrder.length - 1; i >= 0; i--) {\n    const bp = breakpointsOrder[i]\n    if (width >= BREAKPOINT_VALUES[bp]) {\n      return bp\n    }\n  }\n  return 'xs'\n}\n\nexport const useContainerBreakpoint = (\n  containerRef: RefObject<HTMLElement | null>,\n): Breakpoint => {\n  const [breakpoint, setBreakpoint] = useState<Breakpoint>(() => {\n    if (containerRef.current) {\n      return getBreakpointFromWidth(containerRef.current.offsetWidth)\n    }\n\n    return 'md'\n  })\n\n  useEffect(() => {\n    const element = containerRef.current\n\n    if (!element) {\n      return\n    }\n\n    const resizeObserver = new ResizeObserver((entries) => {\n      for (const entry of entries) {\n        const width = entry.contentRect.width\n        const newBreakpoint = getBreakpointFromWidth(width)\n        setBreakpoint(newBreakpoint)\n      }\n    })\n\n    resizeObserver.observe(element)\n\n    return () => {\n      resizeObserver.disconnect()\n    }\n  }, [containerRef])\n\n  return breakpoint\n}\n\nexport const useContainerBreakpointValue = <T>(\n  containerRef: RefObject<HTMLElement | null>,\n  values: BreakpointValues<T>,\n  defaultValue?: T,\n): T => {\n  const currentBreakpoint = useContainerBreakpoint(containerRef)\n\n  const memoizedValue = useMemo(() => {\n    const value = values[currentBreakpoint]\n\n    if (value !== undefined) {\n      return value as T\n    }\n\n    const currentIndex = breakpointsOrder.indexOf(currentBreakpoint)\n\n    for (let i = currentIndex; i >= 0; i--) {\n      const fallbackValue = values[breakpointsOrder[i]]\n\n      if (fallbackValue !== undefined) {\n        return fallbackValue as T\n      }\n    }\n\n    return defaultValue ?? (values[breakpointsOrder[0]] as T)\n  }, [currentBreakpoint, values, defaultValue])\n\n  return memoizedValue\n}\n"
  },
  {
    "path": "frontend/ui/src/hooks/use-click-position.ts",
    "content": "import { useSessionStorageState } from 'ahooks'\nimport { useLayoutEffect } from 'react'\n\nexport interface MousePosition {\n  x: number\n  y: number\n}\n\nexport const useClickPosition = () => {\n  const [mousePosition, setMousePosition] = useSessionStorageState<\n    MousePosition | undefined\n  >('use-click-position', {\n    defaultValue: {\n      x: 0,\n      y: 0,\n    },\n  })\n\n  useLayoutEffect(() => {\n    const updateMousePosition = (ev: MouseEvent) => {\n      setMousePosition({\n        x: ev.clientX,\n        y: ev.clientY,\n      })\n    }\n\n    document.addEventListener('click', updateMousePosition, true)\n\n    return () => {\n      document.removeEventListener('click', updateMousePosition, true)\n    }\n  }, [setMousePosition])\n\n  return mousePosition\n}\n"
  },
  {
    "path": "frontend/ui/src/index.ts",
    "content": "if (typeof window === 'undefined') {\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  global.window = {} as any\n}\n\nexport * from './chart'\nexport * from './hooks'\nexport * from './materialYou'\nexport * from './utils'\n"
  },
  {
    "path": "frontend/ui/src/materialYou/components/baseCard/index.tsx",
    "content": "import { AnimatePresence, motion } from 'framer-motion'\nimport { ReactNode } from 'react'\nimport { cn } from '@/utils'\nimport {\n  Box,\n  Card,\n  CardContent,\n  CircularProgress,\n  Typography,\n} from '@mui/material'\nimport style from './style.module.scss'\n\nexport const BaseCard = ({\n  label,\n  labelChildren,\n  loading,\n  children,\n}: {\n  label?: string\n  labelChildren?: ReactNode\n  loading?: boolean\n  children?: ReactNode\n}) => {\n  return (\n    <Card style={{ position: 'relative' }}>\n      <CardContent>\n        {label && (\n          <Box\n            display=\"flex\"\n            justifyContent=\"space-between\"\n            alignItems=\"center\"\n            sx={{ pb: 1 }}\n          >\n            <Typography variant=\"h5\" component=\"div\">\n              {label}\n            </Typography>\n\n            {labelChildren}\n          </Box>\n        )}\n\n        {children}\n      </CardContent>\n\n      <AnimatePresence initial={false}>\n        {loading && (\n          <motion.div\n            className={cn(style.LoadingMask, 'bg-zinc-100/10')}\n            initial={{\n              opacity: 0,\n            }}\n            animate={{\n              opacity: 1,\n            }}\n            exit={{\n              opacity: 0,\n            }}\n          >\n            <CircularProgress />\n          </motion.div>\n        )}\n      </AnimatePresence>\n    </Card>\n  )\n}\n"
  },
  {
    "path": "frontend/ui/src/materialYou/components/baseCard/style.module.d.scss.ts",
    "content": "declare const classNames: {\n  readonly LoadingMask: 'LoadingMask'\n}\nexport default classNames\n"
  },
  {
    "path": "frontend/ui/src/materialYou/components/baseCard/style.module.scss",
    "content": ".LoadingMask {\n  position: absolute;\n  top: 0;\n  left: 0;\n  z-index: 1;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  width: 100%;\n  height: 100%;\n  border-radius: 24px;\n  backdrop-filter: blur(4px);\n}\n"
  },
  {
    "path": "frontend/ui/src/materialYou/components/baseCard/style.module.scss.d.ts",
    "content": "declare const classNames: {\n  readonly LoadingMask: 'LoadingMask'\n}\nexport default classNames\n"
  },
  {
    "path": "frontend/ui/src/materialYou/components/baseDialog/index.tsx",
    "content": "import { useLockFn } from 'ahooks'\nimport useDebounceFn from 'ahooks/lib/useDebounceFn'\nimport { AnimatePresence, motion } from 'framer-motion'\nimport {\n  CSSProperties,\n  ReactNode,\n  useEffect,\n  useLayoutEffect,\n  useState,\n} from 'react'\nimport { useTranslation } from 'react-i18next'\nimport { getSystem, useClickPosition } from '@/hooks'\nimport { alpha, cn } from '@/utils'\nimport { Box, Button, Divider } from '@mui/material'\nimport { useColorScheme } from '@mui/material/styles'\nimport * as Portal from '@radix-ui/react-portal'\n\nconst OS = getSystem()\n\nexport interface BaseDialogProps {\n  title: ReactNode\n  open: boolean\n  close?: string\n  ok?: string\n  disabledOk?: boolean\n  contentStyle?: CSSProperties\n  children?: ReactNode\n  loading?: boolean\n  full?: boolean\n  onOk?: () => void | Promise<void>\n  onClose?: () => void\n  divider?: boolean\n}\n\nexport const BaseDialog = ({\n  title,\n  open,\n  close,\n  onClose,\n  children,\n  contentStyle,\n  disabledOk,\n  loading,\n  full,\n  onOk,\n  ok,\n  divider,\n}: BaseDialogProps) => {\n  const { t } = useTranslation()\n\n  const { mode } = useColorScheme()\n\n  const [mounted, setMounted] = useState(false)\n\n  const [offset, setOffset] = useState({\n    x: 0,\n    y: 0,\n  })\n\n  const [okLoading, setOkLoading] = useState(false)\n  const [closeLoading, setCloseLoading] = useState(false)\n\n  const { run: runMounted, cancel: cancelMounted } = useDebounceFn(\n    () => setMounted(false),\n    { wait: 300 },\n  )\n\n  const clickPosition = useClickPosition()\n\n  const getClickPosition = () => clickPosition\n\n  useLayoutEffect(() => {\n    if (open) {\n      setOffset({\n        x: getClickPosition()?.x ?? 0,\n        y: getClickPosition()?.y ?? 0,\n      })\n    }\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [open])\n\n  const handleClose = useLockFn(async () => {\n    if (onClose) {\n      if (onClose.constructor.name === 'AsyncFunction') {\n        try {\n          setCloseLoading(true)\n\n          await onClose()\n        } finally {\n          setCloseLoading(false)\n        }\n      } else {\n        onClose()\n      }\n    }\n    runMounted()\n  })\n\n  const handleOk = useLockFn(async () => {\n    if (!onOk) return\n\n    if (onOk.constructor.name === 'AsyncFunction') {\n      try {\n        setOkLoading(true)\n\n        await onOk()\n      } finally {\n        setOkLoading(false)\n      }\n    } else {\n      onOk()\n    }\n  })\n\n  useEffect(() => {\n    if (open) {\n      setMounted(true)\n      cancelMounted()\n    } else {\n      handleClose()\n    }\n  }, [cancelMounted, handleClose, open])\n\n  return (\n    <AnimatePresence initial={false}>\n      {mounted && (\n        <Portal.Root className=\"fixed top-0 left-0 z-50 h-dvh w-full\">\n          {!full && (\n            <Box\n              component={motion.div}\n              className={cn(\n                'fixed inset-0 z-50',\n                OS === 'linux' ? 'bg-black/50' : 'backdrop-blur-xl',\n              )}\n              sx={[\n                OS === 'linux' ||\n                  ((theme) => ({\n                    backgroundColor: alpha(\n                      theme.vars.palette.primary.main,\n                      0.1,\n                    ),\n                  })),\n              ]}\n              animate={open ? 'open' : 'closed'}\n              initial={{ opacity: 0 }}\n              variants={{\n                open: { opacity: 1 },\n                closed: { opacity: 0 },\n              }}\n              onClick={handleClose}\n            />\n          )}\n\n          <Box\n            component={motion.div}\n            className={cn(\n              'fixed top-[50%] left-[50%] z-50',\n              full ? 'h-dvh w-full' : 'min-w-96 rounded-3xl shadow',\n              mode === 'dark' ? 'text-white shadow-zinc-900' : 'text-black',\n            )}\n            sx={(theme) => ({\n              backgroundColor: theme.vars.palette.background.default,\n            })}\n            animate={open ? 'open' : 'closed'}\n            initial={{\n              opacity: 0.3,\n              scale: 0,\n              x: offset.x - window.innerWidth / 2,\n              y: offset.y - window.innerHeight / 2,\n              translateX: '-50%',\n              translateY: '-50%',\n            }}\n            variants={{\n              open: {\n                opacity: 1,\n                scale: 1,\n                x: 0,\n                y: 0,\n              },\n              closed: {\n                opacity: 0.3,\n                scale: 0,\n                x: offset.x - window.innerWidth / 2,\n                y: offset.y - window.innerHeight / 2,\n              },\n            }}\n            transition={{\n              type: 'spring',\n              bounce: 0,\n              duration: 0.35,\n            }}\n          >\n            <div\n              className={cn(\n                'text-xl',\n                !full ? 'm-4' : OS === 'macos' ? 'ml-20 p-3.5' : 'm-2 ml-6',\n              )}\n              data-tauri-drag-region={full}\n            >\n              {title}\n            </div>\n\n            {divider && <Divider />}\n\n            <div\n              className={cn(\n                'relative overflow-x-hidden overflow-y-auto p-4',\n                full && 'h-full px-6',\n              )}\n              style={{\n                maxHeight: full\n                  ? `calc(100vh - ${OS === 'macos' ? 114 : 100}px)`\n                  : 'calc(100vh - 200px)',\n                ...contentStyle,\n              }}\n            >\n              {children}\n            </div>\n\n            {divider && <Divider />}\n\n            <div className={cn('m-2 flex justify-end gap-2', full && 'mx-6')}>\n              {onClose && (\n                <Button\n                  disabled={loading || closeLoading}\n                  loading={closeLoading || loading}\n                  variant=\"outlined\"\n                  onClick={handleClose}\n                >\n                  {close || t('Close')}\n                </Button>\n              )}\n\n              {onOk && (\n                <Button\n                  disabled={loading || disabledOk}\n                  loading={okLoading || loading}\n                  variant=\"contained\"\n                  onClick={handleOk}\n                >\n                  {ok || t('Ok')}\n                </Button>\n              )}\n            </div>\n          </Box>\n        </Portal.Root>\n      )}\n    </AnimatePresence>\n  )\n}\n"
  },
  {
    "path": "frontend/ui/src/materialYou/components/basePage/baseErrorBoundary.tsx",
    "content": "import { ReactNode } from 'react'\nimport { ErrorBoundary, FallbackProps } from 'react-error-boundary'\n\nfunction ErrorFallback({ error }: FallbackProps) {\n  console.log(error)\n\n  return (\n    <div role=\"alert\" style={{ padding: 16 }}>\n      <h4>Something went wrong:(</h4>\n\n      <pre>{error.message}</pre>\n\n      <details title=\"Error Stack\">\n        <summary>Error Stack</summary>\n        <pre>{error.stack}</pre>\n      </details>\n    </div>\n  )\n}\n\ninterface Props {\n  children?: ReactNode\n}\n\nexport const BaseErrorBoundary = (props: Props) => {\n  return (\n    <ErrorBoundary FallbackComponent={ErrorFallback}>\n      {props.children}\n    </ErrorBoundary>\n  )\n}\n"
  },
  {
    "path": "frontend/ui/src/materialYou/components/basePage/header.tsx",
    "content": "import { FC, memo, ReactNode } from 'react'\n\nexport const Header: FC<{ title?: ReactNode; header?: ReactNode }> = memo(\n  function Header({\n    title,\n    header,\n  }: {\n    title?: ReactNode\n    header?: ReactNode\n  }) {\n    return (\n      <header className=\"pl-2 select-none\" data-tauri-drag-region>\n        <h1 className=\"mb-1 !text-4xl/1 font-medium\" data-tauri-drag-region>\n          {title}\n        </h1>\n\n        {header}\n      </header>\n    )\n  },\n)\n\nexport default Header\n"
  },
  {
    "path": "frontend/ui/src/materialYou/components/basePage/index.tsx",
    "content": "import { motion } from 'framer-motion'\nimport { CSSProperties, FC, ReactNode, Ref, Suspense } from 'react'\nimport { cn } from '@/utils'\nimport * as ScrollArea from '@radix-ui/react-scroll-area'\nimport { BaseErrorBoundary } from './baseErrorBoundary'\nimport Header from './header'\nimport './style.scss'\n\ninterface BasePageProps {\n  title?: ReactNode\n  header?: ReactNode\n  contentStyle?: CSSProperties\n  sectionStyle?: CSSProperties\n  full?: boolean\n  viewportRef?: Ref<HTMLDivElement>\n  children?: ReactNode\n}\n\nexport const BasePage: FC<BasePageProps> = ({\n  title,\n  header,\n  contentStyle,\n  sectionStyle,\n  full,\n  viewportRef,\n  children,\n}) => {\n  return (\n    <BaseErrorBoundary>\n      <div className=\"MDYBasePage\" data-tauri-drag-region>\n        <Header title={title} header={header} />\n\n        <ScrollArea.Root\n          className=\"MDYBasePage-container relative h-full w-full overflow-hidden rounded-3xl\"\n          style={contentStyle}\n        >\n          <ScrollArea.Viewport\n            className={cn(\n              'relative h-full w-full [&>div]:!block',\n              full ?? 'p-6',\n            )}\n            ref={viewportRef}\n            style={sectionStyle}\n          >\n            <Suspense>\n              <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }}>\n                {children}\n              </motion.div>\n            </Suspense>\n          </ScrollArea.Viewport>\n\n          <ScrollArea.Scrollbar\n            className=\"flex touch-none py-6 pr-1.5 select-none\"\n            orientation=\"vertical\"\n          >\n            <ScrollArea.Thumb className=\"ScrollArea-Thumb relative flex !w-1.5 flex-1 rounded-full\" />\n          </ScrollArea.Scrollbar>\n\n          {/* <ScrollArea.Scrollbar\n            className=\"ScrollAreaScrollbar\"\n            orientation=\"horizontal\"\n          >\n            <ScrollArea.Thumb className=\"ScrollAreaThumb\" />\n          </ScrollArea.Scrollbar> */}\n          <ScrollArea.Corner className=\"ScrollAreaCorner\" />\n        </ScrollArea.Root>\n      </div>\n    </BaseErrorBoundary>\n  )\n}\n\nexport const ScrollAreaViewport = ScrollArea.Viewport\n"
  },
  {
    "path": "frontend/ui/src/materialYou/components/basePage/style.scss",
    "content": ".MDYBasePage {\n  display: flex;\n  flex-direction: column;\n  width: 100%;\n  height: 100%;\n\n  > header {\n    box-sizing: border-box;\n    display: flex;\n    align-items: end;\n    justify-content: space-between;\n    width: 100%;\n    height: 72px;\n    padding-bottom: 12px;\n    margin: 0 auto;\n  }\n\n  .MDYBasePage-container {\n    background-color: var(--background-color);\n\n    .ScrollArea-Thumb {\n      background-color: var(--scroller-color);\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/ui/src/materialYou/components/expand/index.tsx",
    "content": "import { motion } from 'framer-motion'\nimport { ReactNode } from 'react'\n\n/**\n * @example\n * <Expand open={true}></Expand>\n *\n * @returns {React.JSX.Element}\n * React.JSX.Element\n *\n * `With motion support.`\n *\n * @author keiko233 <i@elaina.moe>\n * @copyright LibNyanpasu org. 2024\n */\nexport const Expand = ({\n  open,\n  children,\n}: {\n  open: boolean\n  children: ReactNode\n}): React.JSX.Element => {\n  return (\n    <motion.div\n      initial={false}\n      animate={open ? 'open' : 'closed'}\n      variants={{\n        open: { opacity: 1, height: 'auto' },\n        closed: { opacity: 0, height: 0 },\n      }}\n    >\n      {children}\n    </motion.div>\n  )\n}\n"
  },
  {
    "path": "frontend/ui/src/materialYou/components/expandMore/index.tsx",
    "content": "import ExpandMoreIcon from '@mui/icons-material/ExpandMore'\nimport IconButton, { IconButtonProps } from '@mui/material/IconButton'\nimport { useTheme } from '@mui/material/styles'\n\ninterface ExpandMoreProps extends IconButtonProps {\n  expand: boolean\n  reverse?: boolean\n}\n\n/**\n * @example\n * <ExpandMore expand={expand} onClick={() => setExpand(!expand)} />\n *\n * `Built-in a small arrow icon.`\n *\n * @author keiko233 <i@elaina.moe>\n * @copyright LibNyanpasu org. 2024\n */\nexport const ExpandMore = ({ expand, reverse, ...props }: ExpandMoreProps) => {\n  const { transitions } = useTheme()\n\n  return (\n    <IconButton {...props}>\n      <ExpandMoreIcon\n        sx={{\n          transform: !expand\n            ? reverse\n              ? 'rotate(180deg)'\n              : 'rotate(0deg)'\n            : reverse\n              ? 'rotate(0deg)'\n              : 'rotate(180deg)',\n          marginLeft: 'auto',\n          transition: transitions.create('transform', {\n            duration: transitions.duration.shortest,\n          }),\n        }}\n      />\n    </IconButton>\n  )\n}\n"
  },
  {
    "path": "frontend/ui/src/materialYou/components/floatingButton/index.tsx",
    "content": "import { ReactNode } from 'react'\nimport { alpha, cn } from '@/utils'\nimport { Button, ButtonProps } from '@mui/material'\n\nexport interface FloatingButtonProps extends ButtonProps {\n  children: ReactNode\n  className?: string\n}\n\nexport const FloatingButton = ({\n  children,\n  className,\n  ...props\n}: FloatingButtonProps) => {\n  return (\n    <Button\n      className={cn(\n        `right-8 bottom-8 z-10 size-16 !rounded-2xl backdrop-blur`,\n        className,\n      )}\n      sx={(theme) => ({\n        position: 'fixed',\n        boxShadow: 8,\n        backgroundColor: alpha(theme.vars.palette.primary.main, 0.3),\n\n        '&:hover': {\n          backgroundColor: alpha(theme.vars.palette.primary.main, 0.45),\n        },\n      })}\n      {...props}\n    >\n      {children}\n    </Button>\n  )\n}\n"
  },
  {
    "path": "frontend/ui/src/materialYou/components/index.ts",
    "content": "export * from './baseCard'\nexport * from './baseDialog'\nexport * from './basePage'\nexport * from './expand'\nexport * from './expandMore'\nexport * from './floatingButton'\nexport * from './item'\nexport * from './kbd'\nexport * from './lazyImage'\nexport * from './loadingButton'\nexport * from './loadingSwitch'\nexport * from './sidePage'\n"
  },
  {
    "path": "frontend/ui/src/materialYou/components/item/baseItem.tsx",
    "content": "import { FC, memo, ReactNode } from 'react'\nimport { SxProps } from '@mui/material'\nimport ListItem from '@mui/material/ListItem'\nimport ListItemText from '@mui/material/ListItemText'\n\nexport interface BaseItemProps {\n  title: ReactNode\n  children: ReactNode\n  sxItem?: SxProps\n  sxItemText?: SxProps\n}\n\nexport const BaseItem: FC<BaseItemProps> = memo(function BaseItem({\n  title,\n  children,\n  sxItem,\n  sxItemText,\n}: BaseItemProps) {\n  return (\n    <ListItem sx={{ pl: 0, pr: 0, ...sxItem }}>\n      <ListItemText primary={title} sx={sxItemText} />\n\n      {children}\n    </ListItem>\n  )\n})\n"
  },
  {
    "path": "frontend/ui/src/materialYou/components/item/index.ts",
    "content": "export * from './switchItem'\nexport * from './menuItem'\nexport * from './numberItem'\nexport * from './textItem'\n"
  },
  {
    "path": "frontend/ui/src/materialYou/components/item/menuItem.tsx",
    "content": "import { MenuItem as MuiMenuItem, Select, SxProps } from '@mui/material'\nimport { BaseItem } from './baseItem'\n\ntype OptionValue = string | number | boolean\n\nexport interface MenuItemProps {\n  label: string\n  options: Record<string, OptionValue>\n  selected: OptionValue\n  onSelected: (value: OptionValue) => void\n  selectSx?: SxProps\n  disabled?: boolean\n}\n\n/**\n * @example\n * <MenuItem\n    label={t(\"Log Level\")}\n    options={options}\n    selected={selected}\n    onSelected={(value) => {\n      console.log(value);\n    }}\n    selectSx={{ width: 100 }}\n  />\n *\n * @returns {React.JSX.Element}\n * React.JSX.Element\n *\n * `MenuItem extends MuiMenuItem. Support options api.`\n *\n * @author keiko233 <i@elaina.moe>\n * @copyright LibNyanpasu org. 2024\n */\nexport const MenuItem = ({\n  label,\n  options,\n  selected,\n  onSelected,\n  selectSx,\n  disabled,\n}: MenuItemProps) => {\n  return (\n    <BaseItem title={label}>\n      <Select\n        size=\"small\"\n        value={selected}\n        inputProps={{ 'aria-label': 'Without label' }}\n        onChange={(e) => {\n          onSelected(e.target.value)\n        }}\n        sx={{ width: 104, ...selectSx }}\n        disabled={disabled}\n      >\n        {Object.entries(options).map(([key, value]) => (\n          <MuiMenuItem\n            key={key}\n            value={key}\n            disabled={key === selected}\n            selected={key === selected}\n          >\n            {value}\n          </MuiMenuItem>\n        ))}\n      </Select>\n    </BaseItem>\n  )\n}\n\nexport default MenuItem\n"
  },
  {
    "path": "frontend/ui/src/materialYou/components/item/numberItem.tsx",
    "content": "import { ChangeEvent, useMemo, useState } from 'react'\nimport { useTranslation } from 'react-i18next'\nimport Done from '@mui/icons-material/Done'\nimport {\n  Box,\n  Button,\n  Divider,\n  TextField,\n  TextFieldProps,\n  Typography,\n} from '@mui/material'\nimport { Expand } from '../expand'\nimport { BaseItem } from './baseItem'\n\nexport interface NumberItemProps {\n  label: string\n  value: number\n  checkEvent: (input: number) => boolean\n  checkLabel: string\n  onApply: (input: number) => void\n  divider?: boolean\n  textFieldProps?: TextFieldProps\n}\n\n/**\n * @example\n * <NumberItem\n    label={t(\"Mixed Port\")}\n    value={port}\n    checkEvent={(input) => input > 65535 || input < 1}\n    checkLabel=\"Port must be between 1 and 65535.\"\n    onApply={(value) => {\n      setConfigs({ \"mixed-port\": value });\n    }}\n    />\n *\n * @returns {React.JSX.Element}\n * React.JSX.Element\n *\n * `NumberItem most use for port label.`\n *\n * @author keiko233 <i@elaina.moe>\n * @copyright LibNyanpasu org. 2024\n */\nexport const NumberItem = ({\n  label,\n  value,\n  checkEvent,\n  checkLabel,\n  onApply,\n  divider,\n  textFieldProps,\n}: NumberItemProps) => {\n  const { t } = useTranslation()\n\n  const [changed, setChanged] = useState(false)\n\n  const [input, setInput] = useState<number | null>(null)\n\n  const applyCheck = useMemo(\n    () => checkEvent(input as number),\n    [checkEvent, input],\n  )\n\n  return (\n    <>\n      <BaseItem title={label}>\n        <TextField\n          value={input !== null ? input : value}\n          size=\"small\"\n          variant=\"outlined\"\n          sx={{ width: 80 }}\n          inputProps={{ 'aria-autocomplete': 'none' }}\n          onChange={(e: ChangeEvent<HTMLInputElement>) => {\n            setInput(Number(e.target.value))\n            setChanged(true)\n          }}\n          {...textFieldProps}\n        />\n      </BaseItem>\n\n      <Expand open={changed}>\n        <Box\n          sx={{ pb: 1 }}\n          display=\"flex\"\n          justifyContent=\"space-between\"\n          alignItems=\"center\"\n        >\n          <span>\n            {applyCheck && (\n              <Typography variant=\"body2\" color=\"error\">\n                {checkLabel}\n              </Typography>\n            )}\n          </span>\n\n          <Button\n            variant=\"contained\"\n            startIcon={<Done />}\n            disabled={applyCheck}\n            onClick={() => {\n              onApply(input as number)\n              setChanged(false)\n            }}\n          >\n            {t('Apply')}\n          </Button>\n        </Box>\n\n        {divider && <Divider />}\n      </Expand>\n    </>\n  )\n}\n"
  },
  {
    "path": "frontend/ui/src/materialYou/components/item/switchItem.tsx",
    "content": "import { ChangeEvent, useState } from 'react'\nimport { SwitchProps } from '@mui/material'\nimport LoadingSwitch from '../loadingSwitch'\nimport { BaseItem } from './baseItem'\n\ninterface Props extends SwitchProps {\n  label: string\n  onChange?: (\n    event: ChangeEvent<HTMLInputElement>,\n    checked: boolean,\n  ) => Promise<void> | void\n}\n\nexport const SwitchItem = ({ label, onChange, ...switchProps }: Props) => {\n  const [loading, setLoading] = useState(false)\n\n  const handleChange = async (\n    event: ChangeEvent<HTMLInputElement>,\n    checked: boolean,\n  ) => {\n    if (onChange) {\n      try {\n        setLoading(true)\n\n        await onChange(event, checked)\n      } finally {\n        setLoading(false)\n      }\n    }\n  }\n\n  return (\n    <BaseItem title={label}>\n      <LoadingSwitch\n        loading={loading}\n        onChange={handleChange}\n        {...switchProps}\n      />\n    </BaseItem>\n  )\n}\n"
  },
  {
    "path": "frontend/ui/src/materialYou/components/item/textItem.tsx",
    "content": "import { useState } from 'react'\nimport { useTranslation } from 'react-i18next'\nimport Done from '@mui/icons-material/Done'\nimport Box from '@mui/material/Box'\nimport Button from '@mui/material/Button'\nimport ListItem from '@mui/material/ListItem'\nimport TextField from '@mui/material/TextField'\nimport { Expand } from '../expand'\n\nexport interface TextItemProps {\n  value: string\n  label: string\n  onApply: (value: string) => void\n  applyLabel?: string\n  placeholder?: string\n}\n\nexport const TextItem = ({\n  value,\n  label,\n  onApply,\n  applyLabel,\n  placeholder,\n}: TextItemProps) => {\n  const { t } = useTranslation()\n\n  const [textString, setTextString] = useState(value)\n\n  return (\n    <>\n      <ListItem sx={{ pl: 0, pr: 0 }}>\n        <TextField\n          value={textString}\n          label={label}\n          variant=\"outlined\"\n          sx={{ width: '100%' }}\n          multiline\n          onChange={(e) => setTextString(e.target.value)}\n          placeholder={placeholder}\n        />\n      </ListItem>\n\n      <Expand open={textString !== value}>\n        <Box sx={{ pb: 1 }} display=\"flex\" justifyContent=\"end\">\n          <Button\n            variant=\"contained\"\n            startIcon={<Done />}\n            onClick={() => onApply(textString)}\n          >\n            {applyLabel ?? t('Apply')}\n          </Button>\n        </Box>\n      </Expand>\n    </>\n  )\n}\n"
  },
  {
    "path": "frontend/ui/src/materialYou/components/kbd/index.module.d.scss.ts",
    "content": "declare const classNames: {\n  readonly kbd: 'kbd'\n  readonly dark: 'dark'\n}\nexport default classNames\n"
  },
  {
    "path": "frontend/ui/src/materialYou/components/kbd/index.module.scss",
    "content": ".kbd {\n  padding-right: 0.4em;\n  padding-left: 0.4em;\n  font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace;\n  font-weight: bold;\n  white-space: nowrap;\n  background-color: #edf2f7;\n  border-color: #e2e8f0;\n  border-style: solid;\n  border-width: 1px;\n  border-bottom-width: 3px;\n  border-radius: 0.375rem;\n\n  &.dark {\n    background-color: rgb(255 255 255 / 6%);\n    border-color: rgb(255 255 255 / 16%);\n  }\n}\n"
  },
  {
    "path": "frontend/ui/src/materialYou/components/kbd/index.module.scss.d.ts",
    "content": "declare const classNames: {\n  readonly kbd: 'kbd'\n  readonly dark: 'dark'\n}\nexport default classNames\n"
  },
  {
    "path": "frontend/ui/src/materialYou/components/kbd/index.tsx",
    "content": "import { cn } from '@/utils'\nimport { useColorScheme } from '@mui/material'\nimport styles from './index.module.scss'\n\nexport type Props = React.DetailedHTMLProps<\n  React.HTMLAttributes<HTMLElement>,\n  HTMLElement\n>\n\nexport function Kbd({ className, children, ...rest }: Props) {\n  const { mode } = useColorScheme()\n  return (\n    <kbd\n      className={cn(styles.kbd, mode === 'dark' && styles.dark, className)}\n      {...rest}\n    >\n      {children}\n    </kbd>\n  )\n}\n"
  },
  {
    "path": "frontend/ui/src/materialYou/components/lazyImage/index.tsx",
    "content": "import { useState } from 'react'\nimport { cn } from '@/utils'\n\nexport interface LazyImageProps extends React.ImgHTMLAttributes<HTMLImageElement> {\n  loadingClassName?: string\n}\nexport function LazyImage({\n  className,\n  loadingClassName,\n  ...others\n}: LazyImageProps) {\n  const [loading, setLoading] = useState(true)\n\n  return (\n    <>\n      <div\n        className={cn(\n          'inline-block animate-pulse bg-slate-200 ring-1 ring-slate-200 dark:bg-slate-700 dark:ring-slate-700',\n          className,\n          loadingClassName,\n          loading ? 'inline-block' : 'hidden',\n        )}\n      />\n      <img\n        {...others}\n        onLoad={() => setLoading(false)}\n        className={cn(className, loading ? 'hidden' : 'inline-block')}\n      />\n    </>\n  )\n}\n"
  },
  {
    "path": "frontend/ui/src/materialYou/components/loadingButton/index.tsx",
    "content": "import { useControllableValue } from 'ahooks'\nimport { MouseEventHandler } from 'react'\nimport {\n  Button as MuiButton,\n  ButtonProps as MuiButtonProps,\n} from '@mui/material'\n\nexport interface LoadingButtonProps extends Omit<MuiButtonProps, 'onClick'> {\n  onClick?: MouseEventHandler<HTMLButtonElement>\n}\n\nexport const LoadingButton = ({\n  loading,\n  onClick,\n  ...props\n}: LoadingButtonProps) => {\n  const [pending, setPending] = useControllableValue<boolean>(\n    { loading },\n    {\n      defaultValue: false,\n    },\n  )\n\n  const handleClick: MouseEventHandler<HTMLButtonElement> = async (e) => {\n    if (onClick) {\n      setPending(true)\n      try {\n        await onClick(e)\n      } catch (error) {\n        console.error(error)\n      } finally {\n        setPending(false)\n      }\n    }\n  }\n\n  return <MuiButton {...props} onClick={handleClick} loading={pending} />\n}\n"
  },
  {
    "path": "frontend/ui/src/materialYou/components/loadingSwitch/index.tsx",
    "content": "import CircularProgress from '@mui/material/CircularProgress'\nimport Switch, { SwitchProps } from '@mui/material/Switch'\nimport style from './style.module.scss'\n\ninterface LoadingSwitchProps extends SwitchProps {\n  loading?: boolean\n}\n\n/**\n * @example\n * <LoadingSwitch\n    loading={loading} \n    onChange={handleChange} \n    {...switchProps} \n  />\n*\n * `Support loading status.`\n *\n * @author keiko233 <i@elaina.moe>\n * @copyright LibNyanpasu org. 2024\n */\nexport const LoadingSwitch = ({\n  loading,\n  checked,\n  disabled,\n  ...props\n}: LoadingSwitchProps) => {\n  return (\n    <div className={style['MDYSwitch-container']}>\n      {loading && (\n        <CircularProgress\n          className={\n            checked ? style['CircularProgress-checked'] : style.CircularProgress\n          }\n          aria-labelledby={props.id}\n          color=\"inherit\"\n          size={16}\n        />\n      )}\n      <Switch disabled={loading || disabled} checked={checked} {...props} />\n    </div>\n  )\n}\n\nexport default LoadingSwitch\n"
  },
  {
    "path": "frontend/ui/src/materialYou/components/loadingSwitch/style.module.d.scss.ts",
    "content": "declare const classNames: {\n  readonly 'MDYSwitch-container': 'MDYSwitch-container'\n  readonly CircularProgress: 'CircularProgress'\n  readonly 'CircularProgress-checked': 'CircularProgress-checked'\n}\nexport default classNames\n"
  },
  {
    "path": "frontend/ui/src/materialYou/components/loadingSwitch/style.module.scss",
    "content": ".MDYSwitch-container {\n  position: relative;\n\n  .CircularProgress {\n    position: absolute;\n    top: 8px;\n    left: 8px;\n    z-index: 1;\n    cursor: not-allowed;\n  }\n\n  .CircularProgress-checked {\n    position: absolute;\n    top: 8px;\n    right: 7px;\n    z-index: 1;\n    cursor: not-allowed;\n  }\n}\n"
  },
  {
    "path": "frontend/ui/src/materialYou/components/loadingSwitch/style.module.scss.d.ts",
    "content": "declare const classNames: {\n  readonly 'MDYSwitch-container': 'MDYSwitch-container'\n  readonly CircularProgress: 'CircularProgress'\n  readonly 'CircularProgress-checked': 'CircularProgress-checked'\n}\nexport default classNames\n"
  },
  {
    "path": "frontend/ui/src/materialYou/components/sidePage/index.tsx",
    "content": "import { motion } from 'framer-motion'\nimport { FC, ReactNode, Ref } from 'react'\nimport { cn } from '@/utils'\nimport * as ScrollArea from '@radix-ui/react-scroll-area'\nimport { BaseErrorBoundary } from '../basePage/baseErrorBoundary'\nimport Header from '../basePage/header'\nimport style from './style.module.scss'\n\ninterface Props {\n  title?: ReactNode\n  header?: ReactNode\n  children?: ReactNode\n  sideBar?: ReactNode\n  side?: ReactNode\n  sideClassName?: string\n  portalRightRoot?: ReactNode\n  noChildrenScroll?: boolean\n  flexReverse?: boolean\n  leftViewportRef?: Ref<HTMLDivElement>\n  rightViewportRef?: Ref<HTMLDivElement>\n}\n\nexport const SidePage: FC<Props> = ({\n  title,\n  header,\n  children,\n  sideBar,\n  side,\n  sideClassName,\n  portalRightRoot,\n  flexReverse,\n  leftViewportRef,\n  rightViewportRef,\n}) => {\n  const sideBarStyle = {\n    height: sideBar ? 'calc(100% - 56px)' : undefined,\n  }\n\n  return (\n    <BaseErrorBoundary>\n      <div className={style['MDYSidePage-Main']} data-tauri-drag-region>\n        <Header title={title} header={header} />\n\n        <div className={style['MDYSidePage-Container']}>\n          <div\n            className={cn(\n              'flex h-full w-full',\n              flexReverse && 'flex-row-reverse',\n            )}\n          >\n            <ScrollArea.Root asChild>\n              <motion.div\n                className=\"w-1/3\"\n                initial={false}\n                animate={side ? 'open' : 'closed'}\n                variants={{\n                  open: {\n                    opacity: 1,\n                    maxWidth: '348px',\n                    minWidth: '192px',\n                    display: 'block',\n                    marginLeft: flexReverse ? '16px' : undefined,\n                    marginRight: flexReverse ? undefined : '16px',\n                  },\n                  closed: {\n                    opacity: 0.5,\n                    maxWidth: 0,\n                    marginLeft: '0px',\n                    marginRight: '0px',\n                    transitionEnd: {\n                      display: 'none',\n                    },\n                  },\n                }}\n              >\n                {sideBar && <div className=\"mb-4 h-10\">{sideBar}</div>}\n\n                <ScrollArea.Viewport\n                  className={cn(\n                    style['Container-common'],\n                    'relative w-full [&>div]:!block',\n                    sideClassName,\n                  )}\n                  style={sideBarStyle}\n                  ref={leftViewportRef}\n                >\n                  {side}\n                </ScrollArea.Viewport>\n\n                <ScrollArea.Scrollbar\n                  className={cn(\n                    'flex touch-none py-6 pr-1.5 select-none',\n                    sideBar && '!top-14',\n                  )}\n                  orientation=\"vertical\"\n                  style={sideBarStyle}\n                >\n                  <ScrollArea.Thumb\n                    className={cn(\n                      style['ScrollArea-Thumb'],\n                      'relative flex !w-1.5 flex-1 rounded-full',\n                    )}\n                  />\n                </ScrollArea.Scrollbar>\n\n                <ScrollArea.Corner className=\"ScrollAreaCorner\" />\n              </motion.div>\n            </ScrollArea.Root>\n\n            <ScrollArea.Root\n              className={cn(style['Container-common'], 'w-full')}\n            >\n              {portalRightRoot}\n\n              <ScrollArea.Viewport\n                className={cn('relative h-full w-full [&>div]:!block')}\n                ref={rightViewportRef}\n              >\n                {children}\n              </ScrollArea.Viewport>\n\n              <ScrollArea.Scrollbar\n                className=\"flex touch-none py-6 pr-1.5 select-none\"\n                orientation=\"vertical\"\n              >\n                <ScrollArea.Thumb className=\"!bg-scroller relative flex !w-1.5 flex-1 rounded-full\" />\n              </ScrollArea.Scrollbar>\n\n              <ScrollArea.Corner className=\"ScrollAreaCorner\" />\n            </ScrollArea.Root>\n          </div>\n        </div>\n      </div>\n    </BaseErrorBoundary>\n  )\n}\n"
  },
  {
    "path": "frontend/ui/src/materialYou/components/sidePage/style.module.d.scss.ts",
    "content": "declare const classNames: {\n  readonly 'MDYSidePage-Main': 'MDYSidePage-Main'\n  readonly 'MDYSidePage-Container': 'MDYSidePage-Container'\n  readonly 'Container-common': 'Container-common'\n  readonly 'ScrollArea-Thumb': 'ScrollArea-Thumb'\n}\nexport default classNames\n"
  },
  {
    "path": "frontend/ui/src/materialYou/components/sidePage/style.module.scss",
    "content": "@reference \"tailwindcss\";\n\n.MDYSidePage-Main {\n  display: flex;\n  flex-direction: column;\n  width: 100%;\n  height: 100%;\n\n  > header {\n    box-sizing: border-box;\n    display: flex;\n    flex: 0 0 64px;\n    align-items: center;\n    justify-content: space-between;\n    width: 100%;\n    margin: 0 auto;\n  }\n\n  .MDYSidePage-Container {\n    height: 100%;\n    overflow: hidden;\n  }\n}\n\n.Container-common {\n  @apply relative h-full overflow-hidden rounded-3xl;\n\n  background-color: var(--background-color);\n}\n\n.ScrollArea-Thumb {\n  background-color: var(--scroller-color);\n}\n"
  },
  {
    "path": "frontend/ui/src/materialYou/components/sidePage/style.module.scss.d.ts",
    "content": "declare const classNames: {\n  readonly 'MDYSidePage-Main': 'MDYSidePage-Main'\n  readonly 'MDYSidePage-Container': 'MDYSidePage-Container'\n  readonly 'Container-common': 'Container-common'\n  readonly 'ScrollArea-Thumb': 'ScrollArea-Thumb'\n}\nexport default classNames\n"
  },
  {
    "path": "frontend/ui/src/materialYou/createTheme.ts",
    "content": "import { RecursivePartial } from '@/utils'\nimport {\n  argbFromHex,\n  hexFromArgb,\n  themeFromSourceColor,\n} from '@material/material-color-utilities'\nimport { createTheme, Palette } from '@mui/material/styles'\nimport {\n  MuiButton,\n  MuiCard,\n  MuiCardContent,\n  MuiDialog,\n  MuiDialogActions,\n  MuiDialogContent,\n  MuiDialogTitle,\n  MuiLinearProgress,\n  MuiMenu,\n  MuiPaper,\n  MuiSwitch,\n  MuiToggleButtonGroup,\n} from './themeComponents'\nimport { MUI_BREAKPOINTS } from './themeConsts.mjs'\n\nexport const createMDYTheme = (color: string, fontFamily?: string) => {\n  const materialColor = themeFromSourceColor(argbFromHex(color))\n\n  const generatePalette = (mode: 'light' | 'dark') => {\n    return {\n      primary: {\n        main: hexFromArgb(materialColor.schemes[mode].primary),\n      },\n      secondary: {\n        main: hexFromArgb(materialColor.schemes[mode].secondary),\n      },\n      error: {\n        main: hexFromArgb(materialColor.schemes[mode].error),\n      },\n      text: {\n        primary: hexFromArgb(materialColor.schemes[mode].onPrimaryContainer),\n        secondary: hexFromArgb(\n          materialColor.schemes[mode].onSecondaryContainer,\n        ),\n      },\n    } satisfies RecursivePartial<Palette>\n  }\n  const colorSchemes = {\n    light: {\n      palette: generatePalette('light'),\n    },\n    dark: {\n      palette: generatePalette('dark'),\n    },\n  }\n  console.log(colorSchemes)\n  const theme = createTheme(\n    {\n      cssVariables: {\n        colorSchemeSelector: 'class',\n      },\n      colorSchemes: {\n        light: true,\n        dark: true,\n      },\n      typography: {\n        fontFamily,\n      },\n      components: {\n        MuiButton,\n        MuiToggleButtonGroup,\n        MuiCard,\n        MuiCardContent,\n        MuiDialog,\n        MuiDialogActions,\n        MuiDialogContent,\n        MuiDialogTitle,\n        MuiLinearProgress,\n        MuiMenu,\n        MuiPaper,\n        MuiSwitch,\n      },\n      breakpoints: MUI_BREAKPOINTS,\n    },\n    {\n      colorSchemes,\n    },\n  )\n\n  return theme\n}\n"
  },
  {
    "path": "frontend/ui/src/materialYou/index.ts",
    "content": "export * from './createTheme'\nexport * from './components'\n"
  },
  {
    "path": "frontend/ui/src/materialYou/themeComponents/MuiButton.ts",
    "content": "import { Theme } from '@mui/material'\nimport { Components } from '@mui/material/styles'\n\nexport const MuiButton: Components<Theme>['MuiButton'] = {\n  styleOverrides: {\n    root: {\n      borderRadius: '48px',\n    },\n  },\n}\n"
  },
  {
    "path": "frontend/ui/src/materialYou/themeComponents/MuiCard.ts",
    "content": "import { Theme } from '@mui/material'\nimport { Components } from '@mui/material/styles'\n\nexport const MuiCard: Components<Theme>['MuiCard'] = {\n  defaultProps: {\n    sx: {\n      borderRadius: 6,\n      elevation: 0,\n    },\n  },\n}\n"
  },
  {
    "path": "frontend/ui/src/materialYou/themeComponents/MuiCardContent.ts",
    "content": "import { Theme } from '@mui/material'\nimport { Components } from '@mui/material/styles'\n\nexport const MuiCardContent: Components<Theme>['MuiCardContent'] = {\n  defaultProps: {\n    sx: {\n      padding: 3,\n    },\n  },\n}\n"
  },
  {
    "path": "frontend/ui/src/materialYou/themeComponents/MuiDialog.ts",
    "content": "import { Theme } from '@mui/material'\nimport { Components } from '@mui/material/styles'\n\nexport const MuiDialog: Components<Theme>['MuiDialog'] = {\n  styleOverrides: {\n    paper: {\n      borderRadius: 24,\n    },\n  },\n}\n"
  },
  {
    "path": "frontend/ui/src/materialYou/themeComponents/MuiDialogActions.ts",
    "content": "import { Theme } from '@mui/material'\nimport { Components } from '@mui/material/styles'\n\nexport const MuiDialogActions: Components<Theme>['MuiDialogActions'] = {\n  styleOverrides: {\n    root: {\n      padding: 24,\n    },\n  },\n}\n"
  },
  {
    "path": "frontend/ui/src/materialYou/themeComponents/MuiDialogContent.ts",
    "content": "import { Theme } from '@mui/material'\nimport { Components } from '@mui/material/styles'\n\nexport const MuiDialogContent: Components<Theme>['MuiDialogContent'] = {\n  styleOverrides: {\n    root: {\n      padding: '0 24px',\n    },\n  },\n}\n"
  },
  {
    "path": "frontend/ui/src/materialYou/themeComponents/MuiDialogTitle.ts",
    "content": "import { Theme } from '@mui/material'\nimport { Components } from '@mui/material/styles'\n\nexport const MuiDialogTitle: Components<Theme>['MuiDialogTitle'] = {\n  styleOverrides: {\n    root: {\n      padding: 24,\n    },\n  },\n}\n"
  },
  {
    "path": "frontend/ui/src/materialYou/themeComponents/MuiLinearProgress.ts",
    "content": "import { Theme } from '@mui/material'\nimport { Components } from '@mui/material/styles'\n\nexport const MuiLinearProgress: Components<Theme>['MuiLinearProgress'] = {\n  styleOverrides: {\n    root: {\n      height: '8px',\n      borderRadius: '8px',\n    },\n    bar: {\n      borderRadius: '8px',\n    },\n  },\n}\n"
  },
  {
    "path": "frontend/ui/src/materialYou/themeComponents/MuiMenu.ts",
    "content": "import { Theme } from '@mui/material'\nimport { Components } from '@mui/material/styles'\n\nexport const MuiMenu: Components<Theme>['MuiMenu'] = {\n  styleOverrides: {\n    paper: ({ theme }) => ({\n      boxShadow: `${theme.shadows[8]} !important`,\n    }),\n  },\n}\n"
  },
  {
    "path": "frontend/ui/src/materialYou/themeComponents/MuiPaper.ts",
    "content": "import { Theme } from '@mui/material'\nimport { Components } from '@mui/material/styles'\n\nexport const MuiPaper: Components<Theme>['MuiPaper'] = {\n  styleOverrides: {\n    root: () => ({\n      boxShadow: 'none',\n    }),\n  },\n}\n"
  },
  {
    "path": "frontend/ui/src/materialYou/themeComponents/MuiSwitch.ts",
    "content": "import { Theme } from '@mui/material'\nimport { Components } from '@mui/material/styles'\nimport type {} from '@mui/material/themeCssVarsAugmentation'\n\nexport const MuiSwitch: Components<Theme>['MuiSwitch'] = {\n  styleOverrides: {\n    root: ({ theme }) => ({\n      padding: 0,\n      margin: 0,\n\n      '& .Mui-checked': {\n        '& .MuiSwitch-thumb': {\n          color: theme.vars.palette.grey.A100,\n        },\n      },\n\n      '&:has(.Mui-checked) .MuiSwitch-track::before': {\n        opacity: 0,\n      },\n\n      '&:has(.Mui-disabled) .MuiSwitch-track': {\n        opacity: '0.5 !important',\n        cursor: 'not-allowed',\n      },\n\n      variants: [\n        {\n          props: {\n            size: 'medium',\n          },\n          style: {\n            height: 32,\n\n            '& .MuiSwitch-switchBase': {\n              padding: '6px',\n            },\n\n            '& .MuiSwitch-thumb': {\n              width: 14,\n              height: 14,\n              margin: 3,\n            },\n\n            '& .Mui-checked': {\n              '&.MuiSwitch-switchBase': {\n                marginLeft: '6px',\n              },\n\n              '& .MuiSwitch-thumb': {\n                width: 24,\n                height: 24,\n                margin: -2,\n              },\n            },\n          },\n        },\n        {\n          props: {\n            size: 'small',\n          },\n          style: {\n            height: 24,\n\n            '& .MuiSwitch-switchBase': {\n              padding: '3px',\n            },\n\n            '& .MuiSwitch-thumb': {\n              width: 12,\n              height: 12,\n              margin: 3,\n            },\n\n            '& .Mui-checked': {\n              '&.MuiSwitch-switchBase': {\n                marginLeft: '1px',\n              },\n\n              '& .MuiSwitch-thumb': {\n                width: 17,\n                height: 17,\n                margin: 0,\n              },\n            },\n          },\n        },\n      ],\n    }),\n\n    track: ({ theme }) => ({\n      borderRadius: '48px',\n      backgroundColor: theme.vars.palette.grey.A200,\n      opacity: `1 !important`,\n\n      ...theme.applyStyles('dark', {\n        backgroundColor: theme.vars.palette.grey.A700,\n        opacity: `0.7 !important`,\n      }),\n\n      '&::before': {\n        content: '\"\"',\n        border: `solid 2px ${theme.vars.palette.grey.A700}`,\n        width: '100%',\n        height: '100%',\n        opacity: 1,\n        position: 'absolute',\n        borderRadius: 'inherit',\n        boxSizing: 'border-box',\n        transitionProperty: 'opacity, background-color',\n        transitionTimingFunction: 'linear',\n        transitionDuration: '100ms',\n      },\n    }),\n\n    thumb: ({ theme }) => ({\n      boxShadow: 'none',\n      color: theme.vars.palette.grey.A700,\n\n      ...theme.applyStyles('dark', {\n        backgroundColor: theme.vars.palette.grey.A200,\n      }),\n    }),\n  },\n}\n"
  },
  {
    "path": "frontend/ui/src/materialYou/themeComponents/MuiToggleButtonGroup.ts",
    "content": "import { alpha, darken } from '@/utils/color-mix'\nimport { Theme } from '@mui/material'\nimport { Components } from '@mui/material/styles'\n\nexport const MuiToggleButtonGroup: Components<Theme>['MuiToggleButtonGroup'] = {\n  styleOverrides: {\n    grouped: ({ theme }) =>\n      theme.unstable_sx({\n        fontWeight: 700,\n        height: '2.5em',\n        padding: '0 1.25em',\n        border: `1px solid ${darken(theme.vars.palette.primary.main, 0.09)}`,\n        color: darken(theme.vars.palette.primary.main, 0.2),\n\n        '&.MuiButton-contained.MuiButton-colorPrimary': {\n          boxShadow: 'none',\n          border: `1px solid ${theme.vars.palette.primary.mainChannel}`,\n          backgroundColor: alpha(theme.vars.palette.primary.main, 0.2),\n          color: theme.vars.palette.primary.main,\n          '&::before': {\n            content: 'none',\n          },\n          '&:hover': {\n            backgroundColor: alpha(theme.vars.palette.primary.main, 0.3),\n          },\n        },\n      }),\n    firstButton: ({ theme }) =>\n      theme.unstable_sx({\n        borderTopLeftRadius: 48,\n        borderBottomLeftRadius: 48,\n\n        '&.MuiButton-sizeSmall': {\n          paddingLeft: '1.5em',\n        },\n\n        '&.MuiButton-sizeMedium': {\n          paddingLeft: '20px',\n        },\n\n        '&.MuiButton-sizeLarge': {\n          paddingLeft: '26px',\n        },\n      }),\n    lastButton: ({ theme }) =>\n      theme.unstable_sx({\n        borderTopRightRadius: 48,\n        borderBottomRightRadius: 48,\n\n        '&.MuiButton-sizeSmall': {\n          paddingRight: '1.5em',\n        },\n\n        '&.MuiButton-sizeMedium': {\n          paddingRight: '20px',\n        },\n\n        '&.MuiButton-sizeLarge': {\n          paddingRight: '26px',\n        },\n      }),\n  },\n} satisfies Components<Theme>['MuiToggleButtonGroup']\n"
  },
  {
    "path": "frontend/ui/src/materialYou/themeComponents/index.ts",
    "content": "export * from './MuiButton'\nexport * from './MuiToggleButtonGroup'\nexport * from './MuiPaper'\nexport * from './MuiCard'\nexport * from './MuiCardContent'\nexport * from './MuiSwitch'\nexport * from './MuiDialog'\nexport * from './MuiDialogActions'\nexport * from './MuiDialogContent'\nexport * from './MuiDialogTitle'\nexport * from './MuiLinearProgress'\nexport * from './MuiMenu'\n"
  },
  {
    "path": "frontend/ui/src/materialYou/themeConsts.mjs",
    "content": "/** @type {import(\"@mui/material/styles\").BreakpointsOptions} */\nexport const MUI_BREAKPOINTS = {\n  values: {\n    xs: 0,\n    sm: 400,\n    md: 800,\n    lg: 1200,\n    xl: 1600,\n  },\n}\n"
  },
  {
    "path": "frontend/ui/src/utils/cn.ts",
    "content": "import clsx, { type ClassValue } from 'clsx'\nimport { twMerge } from 'tailwind-merge'\n\nexport const cn = (...classes: ClassValue[]) => twMerge(clsx(...classes))\n"
  },
  {
    "path": "frontend/ui/src/utils/color-mix.ts",
    "content": "export const alpha = (color: string, alpha: number) => {\n  return `color-mix(in srgb, ${color} ${(alpha * 100).toFixed(2)}%, transparent ${((1 - alpha) * 100).toFixed(2)}%)`\n}\n\nexport const lighten = (color: string, alpha: number) => {\n  return `color-mix(in lch, ${color} ${((1 - alpha) * 100).toFixed(2)}%, white ${(alpha * 100).toFixed(2)}%)`\n}\n\nexport const darken = (color: string, alpha: number) => {\n  return `color-mix(in lch, ${color} ${((1 - alpha) * 100).toFixed(2)}%, black ${(alpha * 100).toFixed(2)}%)`\n}\n"
  },
  {
    "path": "frontend/ui/src/utils/event.ts",
    "content": "export const cleanDeepClickEvent = (\n  e: Pick<MouseEvent, 'preventDefault' | 'stopPropagation'>,\n) => {\n  e.preventDefault()\n  e.stopPropagation()\n}\n"
  },
  {
    "path": "frontend/ui/src/utils/index.ts",
    "content": "export { cn } from './cn'\nexport * from './event'\nexport * from './ts-helper'\nexport * from './color-mix'\n"
  },
  {
    "path": "frontend/ui/src/utils/ts-helper.ts",
    "content": "export type RecursivePartial<T> = {\n  [P in keyof T]?: T[P] extends (infer U)[]\n    ? RecursivePartial<U>[]\n    : T[P] extends object | undefined\n      ? RecursivePartial<T[P]>\n      : T[P]\n}\n"
  },
  {
    "path": "frontend/ui/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"target\": \"ESNext\",\n    \"useDefineForClassFields\": true,\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ESNext\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"esModuleInterop\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"allowArbitraryExtensions\": true,\n    \"strict\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"preserve\",\n    \"jsxFactory\": \"React.createElement\",\n    \"jsxFragmentFactory\": \"React.Fragment\",\n    \"declaration\": true,\n    \"composite\": true,\n    \"paths\": {\n      \"@/*\": [\"./src/*\"],\n    },\n    \"plugins\": [{ \"name\": \"typescript-plugin-css-modules\" }],\n    \"outDir\": \"./dist\",\n  },\n  \"include\": [\"vite.config.ts\", \"src/\"],\n}\n"
  },
  {
    "path": "frontend/ui/vite.config.ts",
    "content": "import { defineConfig } from 'vite'\nimport dts from 'vite-plugin-dts'\nimport tsconfigPaths from 'vite-tsconfig-paths'\nimport react from '@vitejs/plugin-react'\n\nconst needSourceMap = process.argv.includes('--sourcemap')\n\nexport default defineConfig({\n  plugins: [\n    dts({\n      // rollupTypes: true,\n      copyDtsFiles: true,\n      staticImport: true,\n      insertTypesEntry: true,\n      compilerOptions: {\n        // sourceMap: needSourceMap,\n        declarationMap: needSourceMap,\n      },\n    }),\n    react(),\n    tsconfigPaths(),\n  ],\n  build: {\n    lib: {\n      entry: 'src/index.ts',\n      fileName: 'index',\n      formats: ['es'],\n    },\n    sourcemap: needSourceMap,\n    rollupOptions: {\n      external: ['react', 'react-dom', '@tauri-apps/api'],\n      output: {\n        globals: {\n          react: 'React',\n          'react-dom': 'ReactDOM',\n          OS_PLATFORM: 'OS_PLATFORM',\n        },\n      },\n    },\n  },\n})\n"
  },
  {
    "path": "knip.config.ts",
    "content": "import { KnipConfig } from 'knip'\n\nexport default {\n  entry: [\n    'frontend/nyanpasu/src/main.tsx',\n    'frontend/nyanpasu/src/pages/**/*.tsx',\n    'scripts/*.{js,ts}',\n  ],\n  project: ['frontend/**/*.{ts,js,jsx,tsx}', 'scripts/**/*.{js,ts}'],\n} satisfies KnipConfig\n"
  },
  {
    "path": "manifest/site/index.html",
    "content": "<html>\n  <title>Clash Nyanpasu Manifest Site</title>\n\n  <body>\n    <h1>Clash Nyanpasu Manifest Site</h1>\n    <p>This is the manifest site for Clash Nyanpasu.</p>\n    <p>\n      <a href=\"/updater/update-proxy.json\"\n        >Stable updater channel with Github Proxy</a\n      >\n    </p>\n    <p>\n      <a href=\"/updater/update.json\">Stable updater channel</a>\n    </p>\n    <p>\n      <a href=\"/updater/update-fixed-webview-proxy.json\"\n        >Stable updater channel with fixed Webview and Github Proxy, only for\n        Windows</a\n      >\n    </p>\n    <p>\n      <a href=\"/updater/update-fixed-webview.json\"\n        >Stable updater channel with fixed Webview, only for Windows</a\n      >\n    </p>\n    <p>\n      <a href=\"/updater/update-nightly-proxy.json\"\n        >Nightly updater channel with Github Proxy</a\n      >\n    </p>\n    <p>\n      <a href=\"/updater/update-nightly.json\">Nightly updater channel</a>\n    </p>\n    <p>\n      <a href=\"/updater/update-nightly-fixed-webview.json\"\n        >Nightly updater channel with fixed Webview, only for Windows</a\n      >\n    </p>\n    <p>\n      <a href=\"/updater/update-nightly-fixed-webview-proxy.json\"\n        >Nightly updater channel with fixed Webview and Github Proxy, only for\n        Windows</a\n      >\n    </p>\n  </body>\n</html>\n"
  },
  {
    "path": "manifest/site/updater/.gitkeep",
    "content": ""
  },
  {
    "path": "manifest/version.json",
    "content": "{\n  \"manifest_version\": 1,\n  \"latest\": {\n    \"mihomo\": \"v1.19.21\",\n    \"mihomo_alpha\": \"alpha-dd4eb63\",\n    \"clash_rs\": \"v0.9.6\",\n    \"clash_premium\": \"2023-09-05-gdcc8d87\",\n    \"clash_rs_alpha\": \"0.9.6-alpha+sha.b17ba0a\"\n  },\n  \"arch_template\": {\n    \"mihomo\": {\n      \"windows-i386\": \"mihomo-windows-386-{}.zip\",\n      \"windows-x86_64\": \"mihomo-windows-amd64-v1-{}.zip\",\n      \"windows-arm64\": \"mihomo-windows-arm64-{}.zip\",\n      \"linux-aarch64\": \"mihomo-linux-arm64-{}.gz\",\n      \"linux-amd64\": \"mihomo-linux-amd64-v1-{}.gz\",\n      \"linux-i386\": \"mihomo-linux-386-{}.gz\",\n      \"darwin-arm64\": \"mihomo-darwin-arm64-{}.gz\",\n      \"darwin-x64\": \"mihomo-darwin-amd64-v1-{}.gz\",\n      \"linux-armv7\": \"mihomo-linux-armv5-{}.gz\",\n      \"linux-armv7hf\": \"mihomo-linux-armv7-{}.gz\"\n    },\n    \"mihomo_alpha\": {\n      \"windows-i386\": \"mihomo-windows-386-{}.zip\",\n      \"windows-x86_64\": \"mihomo-windows-amd64-v1-{}.zip\",\n      \"windows-arm64\": \"mihomo-windows-arm64-{}.zip\",\n      \"linux-aarch64\": \"mihomo-linux-arm64-{}.gz\",\n      \"linux-amd64\": \"mihomo-linux-amd64-v1-{}.gz\",\n      \"linux-i386\": \"mihomo-linux-386-{}.gz\",\n      \"darwin-arm64\": \"mihomo-darwin-arm64-{}.gz\",\n      \"darwin-x64\": \"mihomo-darwin-amd64-v1-{}.gz\",\n      \"linux-armv7\": \"mihomo-linux-armv5-{}.gz\",\n      \"linux-armv7hf\": \"mihomo-linux-armv7-{}.gz\"\n    },\n    \"clash_rs\": {\n      \"windows-i386\": \"clash-rs-i686-pc-windows-msvc-static-crt.exe\",\n      \"windows-x86_64\": \"clash-rs-x86_64-pc-windows-msvc.exe\",\n      \"windows-arm64\": \"clash-rs-aarch64-pc-windows-msvc.exe\",\n      \"linux-aarch64\": \"clash-rs-aarch64-unknown-linux-gnu\",\n      \"linux-amd64\": \"clash-rs-x86_64-unknown-linux-gnu-static-crt\",\n      \"linux-i386\": \"clash-rs-i686-unknown-linux-gnu\",\n      \"darwin-arm64\": \"clash-rs-aarch64-apple-darwin\",\n      \"darwin-x64\": \"clash-rs-x86_64-apple-darwin\",\n      \"linux-armv7\": \"clash-rs-armv7-unknown-linux-gnueabi\",\n      \"linux-armv7hf\": \"clash-rs-armv7-unknown-linux-gnueabihf\"\n    },\n    \"clash_premium\": {\n      \"windows-i386\": \"clash-windows-386-n{}.zip\",\n      \"windows-x86_64\": \"clash-windows-amd64-n{}.zip\",\n      \"windows-arm64\": \"clash-windows-arm64-n{}.zip\",\n      \"linux-aarch64\": \"clash-linux-arm64-n{}.gz\",\n      \"linux-amd64\": \"clash-linux-amd64-n{}.gz\",\n      \"linux-i386\": \"clash-linux-386-n{}.gz\",\n      \"darwin-arm64\": \"clash-darwin-arm64-n{}.gz\",\n      \"darwin-x64\": \"clash-darwin-amd64-n{}.gz\",\n      \"linux-armv7\": \"clash-linux-armv5-n{}.gz\",\n      \"linux-armv7hf\": \"clash-linux-armv7-n{}.gz\"\n    },\n    \"clash_rs_alpha\": {\n      \"windows-i386\": \"clash-rs-i686-pc-windows-msvc-static-crt.exe\",\n      \"windows-x86_64\": \"clash-rs-x86_64-pc-windows-msvc.exe\",\n      \"windows-arm64\": \"clash-rs-aarch64-pc-windows-msvc.exe\",\n      \"linux-aarch64\": \"clash-rs-aarch64-unknown-linux-gnu\",\n      \"linux-amd64\": \"clash-rs-x86_64-unknown-linux-gnu-static-crt\",\n      \"linux-i386\": \"clash-rs-i686-unknown-linux-gnu\",\n      \"darwin-arm64\": \"clash-rs-aarch64-apple-darwin\",\n      \"darwin-x64\": \"clash-rs-x86_64-apple-darwin\",\n      \"linux-armv7\": \"clash-rs-armv7-unknown-linux-gnueabi\",\n      \"linux-armv7hf\": \"clash-rs-armv7-unknown-linux-gnueabihf\"\n    }\n  },\n  \"updated_at\": \"2026-03-19T22:22:44.922Z\"\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"@nyanpasu/monorepo\",\n  \"version\": \"2.0.0\",\n  \"repository\": \"https://github.com/libnyanpasu/clash-nyanpasu.git\",\n  \"license\": \"GPL-3.0\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"run-p tauri:dev\",\n    \"dev:diff\": \"run-p tauri:diff\",\n    \"build\": \"tauri build\",\n    \"build:debug\": \"tauri build -f verge-dev deadlock-detection -d -c \\\"{ \\\\\\\"tauri\\\\\\\" : { \\\\\\\"updater\\\\\\\": { \\\\\\\"active\\\\\\\": false } }} \\\"\",\n    \"build:nightly\": \"tauri build -f nightly -c ./backend/tauri/tauri.nightly.conf.json\",\n    \"tauri\": \"tauri\",\n    \"tauri:dev\": \"tauri dev -c ./backend/tauri/tauri.conf.json\",\n    \"tauri:diff\": \"tauri dev -f verge-dev deadlock-detection -c ./backend/tauri/tauri.conf.json\",\n    \"tauri:preview\": \"pnpm prepare:preview && tauri dev -f verge-dev deadlock-detection -c ./backend/tauri/tauri.preview.conf.json\",\n    \"web:dev\": \"pnpm --filter=@nyanpasu/nyanpasu dev\",\n    \"web:build\": \"pnpm --filter=@nyanpasu/nyanpasu build\",\n    \"web:serve\": \"pnpm --filter=@nyanpasu/nyanpasu preview\",\n    \"web:visualize\": \"pnpm --filter=@nyanpasu/nyanpasu bundle:visualize\",\n    \"lint\": \"run-s lint:*\",\n    \"lint:prettier\": \"prettier --check .\",\n    \"lint:oxlint\": \"oxlint .\",\n    \"lint:styles\": \"stylelint --cache --allow-empty-input \\\"**/*.{css,scss}\\\"\",\n    \"lint:ts\": \"run-s lint:ts:*\",\n    \"lint:ts:scripts\": \"tsc --noEmit --project ./scripts/tsconfig.json\",\n    \"lint:ts:ui\": \"tsc --noEmit --project ./frontend/ui/tsconfig.json\",\n    \"lint:ts:interface\": \"tsc --noEmit --project ./frontend/interface/tsconfig.json\",\n    \"lint:ts:nyanpasu\": \"tsc --noEmit --project ./frontend/nyanpasu/tsconfig.json\",\n    \"lint:clippy\": \"cargo clippy --manifest-path ./backend/Cargo.toml --all-targets --all-features\",\n    \"lint:rustfmt\": \"cargo fmt --manifest-path ./backend/Cargo.toml --all -- --check\",\n    \"knip\": \"knip\",\n    \"test\": \"run-p test:*\",\n    \"test:backend\": \"cargo test --manifest-path ./backend/Cargo.toml --all-features\",\n    \"fmt\": \"run-p fmt:*\",\n    \"fmt:backend\": \"cargo fmt --manifest-path ./backend/Cargo.toml --all\",\n    \"fmt:prettier\": \"prettier --write .\",\n    \"fmt:oxlint\": \"oxlint --fix .\",\n    \"updater\": \"tsx scripts/updater.ts\",\n    \"updater:nightly\": \"tsx scripts/updater-nightly.ts\",\n    \"publish\": \"tsx scripts/publish.ts\",\n    \"portable\": \"tsx scripts/portable.ts\",\n    \"upload:osx-aarch64\": \"tsx scripts/osx-aarch64-upload.ts\",\n    \"generate:git-info\": \"tsx scripts/generate-git-info.ts\",\n    \"generate:manifest\": \"run-p generate:manifest:*\",\n    \"generate:manifest:latest-version\": \"deno run -A scripts/deno/generate-latest-version.ts\",\n    \"prepare\": \"husky\",\n    \"prepare:nightly\": \"tsx scripts/prepare-nightly.ts\",\n    \"prepare:release\": \"tsx scripts/prepare-release.ts\",\n    \"prepare:preview\": \"tsx scripts/prepare-preview.ts\",\n    \"prepare:check\": \"deno run -A scripts/deno/check.ts\"\n  },\n  \"dependencies\": {\n    \"@prettier/plugin-oxc\": \"0.1.3\",\n    \"husky\": \"9.1.7\",\n    \"lodash-es\": \"4.17.23\"\n  },\n  \"devDependencies\": {\n    \"@commitlint/cli\": \"20.5.0\",\n    \"@commitlint/config-conventional\": \"20.5.0\",\n    \"@ianvs/prettier-plugin-sort-imports\": \"4.7.1\",\n    \"@tauri-apps/cli\": \"2.10.1\",\n    \"@types/fs-extra\": \"11.0.4\",\n    \"@types/lodash-es\": \"4.17.12\",\n    \"@types/node\": \"24.11.0\",\n    \"autoprefixer\": \"10.4.27\",\n    \"conventional-changelog-conventionalcommits\": \"9.3.0\",\n    \"cross-env\": \"10.1.0\",\n    \"dedent\": \"1.7.2\",\n    \"globals\": \"17.4.0\",\n    \"knip\": \"5.88.1\",\n    \"lint-staged\": \"16.4.0\",\n    \"npm-run-all2\": \"8.0.4\",\n    \"oxlint\": \"1.56.0\",\n    \"postcss\": \"8.5.8\",\n    \"postcss-html\": \"1.8.1\",\n    \"postcss-import\": \"16.1.1\",\n    \"postcss-scss\": \"4.0.9\",\n    \"prettier\": \"3.8.1\",\n    \"prettier-plugin-ember-template-tag\": \"2.1.3\",\n    \"prettier-plugin-tailwindcss\": \"0.7.2\",\n    \"prettier-plugin-toml\": \"2.0.6\",\n    \"stylelint\": \"17.5.0\",\n    \"stylelint-config-html\": \"1.1.0\",\n    \"stylelint-config-recess-order\": \"7.7.0\",\n    \"stylelint-config-standard\": \"40.0.0\",\n    \"stylelint-declaration-block-no-ignored-properties\": \"3.0.0\",\n    \"stylelint-order\": \"8.1.1\",\n    \"stylelint-scss\": \"7.0.0\",\n    \"tailwindcss\": \"4.2.2\",\n    \"tsx\": \"4.21.0\",\n    \"typescript\": \"5.9.3\"\n  },\n  \"packageManager\": \"pnpm@10.32.1\",\n  \"engines\": {\n    \"node\": \"24.14.0\"\n  },\n  \"pnpm\": {\n    \"overrides\": {\n      \"vite-plugin-monaco-editor\": \"npm:vite-plugin-monaco-editor-new@1.1.3\",\n      \"material-react-table\": \"npm:@greenhat616/material-react-table@4.0.0\"\n    },\n    \"onlyBuiltDependencies\": [\n      \"@swc/core\",\n      \"@tailwindcss/oxide\",\n      \"core-js\",\n      \"esbuild\",\n      \"meta-json-schema\",\n      \"oxc-resolver\"\n    ]\n  }\n}\n"
  },
  {
    "path": "pnpm-workspace.yaml",
    "content": "packages:\n  - 'frontend/*'\n  - 'scripts'\n"
  },
  {
    "path": "renovate.json",
    "content": "{\n  \"$schema\": \"https://docs.renovatebot.com/renovate-schema.json\",\n  \"extends\": [\n    \"config:recommended\",\n    \"default:automergeMinor\",\n    \"default:prHourlyLimitNone\",\n    \"default:preserveSemverRanges\",\n    \"default:rebaseStalePrs\",\n    \"group:monorepos\"\n  ],\n  \"packageRules\": [\n    {\n      \"matchManagers\": [\"npm\"],\n      \"rangeStrategy\": \"pin\"\n    },\n    {\n      \"matchManagers\": [\"cargo\"],\n      \"rangeStrategy\": \"update-lockfile\",\n      \"platformAutomerge\": false\n    },\n    {\n      \"groupName\": \"Oxc packages\",\n      \"matchPackageNames\": [\"/oxc/\"]\n    },\n    {\n      \"groupName\": \"Bundler packages\",\n      \"matchPackageNames\": [\"/vite/\", \"/unplugin/\"]\n    },\n    {\n      \"groupName\": \"Typescript packages\",\n      \"matchPackageNames\": [\"/@types/\", \"/ts-/\", \"/tsx/\", \"/typescript/\"]\n    },\n    {\n      \"groupName\": \"Lint packages\",\n      \"matchPackageNames\": [\n        \"/eslint/\",\n        \"/prettier/\",\n        \"/commitlint/\",\n        \"/stylelint/\",\n        \"/husky/\",\n        \"/lint-staged/\"\n      ]\n    },\n    {\n      \"groupName\": \"Tauri packages\",\n      \"matchPackageNames\": [\"/tauri/\"]\n    },\n    {\n      \"groupName\": \"Windows packages\",\n      \"matchPackageNames\": [\"/windows/\", \"/webview2-com/\", \"winreg\"]\n    },\n    {\n      \"groupName\": \"Object-C packages\",\n      \"matchPackageNames\": [\"/objc2/\"]\n    },\n    {\n      \"groupName\": \"egui packages\",\n      \"matchPackageNames\": [\"/egui/\", \"/eframe/\"]\n    },\n    {\n      \"groupName\": \"Testing packages\",\n      \"matchPackageNames\": [\"/vitest/\", \"/cypress/\", \"/wdio/\"]\n    }\n  ],\n  \"prConcurrentLimit\": 30\n}\n"
  },
  {
    "path": "rust-toolchain.toml",
    "content": "[toolchain]\nchannel = \"nightly\"\n"
  },
  {
    "path": "scripts/.gitignore",
    "content": "!.vscode/settings.json\n!.vscode/\n"
  },
  {
    "path": "scripts/.vscode/settings.json",
    "content": "{\n  \"deno.enable\": true,\n  \"deno.enablePaths\": [\"./deno\"]\n}\n"
  },
  {
    "path": "scripts/deno/README.md",
    "content": "# Deno scripts\n\nWhen we migrated all the scripts to Deno, let's move them to outer directory.\n"
  },
  {
    "path": "scripts/deno/build-cache.ts",
    "content": "import { parseArgs } from 'jsr:@std/cli@1/parse-args'\nimport { exists } from 'jsr:@std/fs'\nimport * as path from 'jsr:@std/path'\nimport {\n  downloadCache,\n  listCacheKeys,\n  uploadCache,\n} from './utils/cache-client.ts'\nimport { consola } from './utils/logger.ts'\n\n// --- config ---\n\nconst WORKSPACE_ROOT = path.join(import.meta.dirname!, '../..')\nconst TARGET_DIR = path.join(WORKSPACE_ROOT, 'backend/target')\nconst CARGO_LOCK_PATH = path.join(WORKSPACE_ROOT, 'backend/Cargo.lock')\n\nconst TAR_EXCLUDE_PATTERNS = [\n  'bundle',\n  '*.exe',\n  '*.dmg',\n  '*.deb',\n  '*.rpm',\n  '*.AppImage',\n  '*.msi',\n  '*.nsis',\n]\n\n// --- helpers ---\n\nfunction requireEnv(name: string): string {\n  const value = Deno.env.get(name)\n  if (!value) {\n    consola.fatal(`${name} is required`)\n    Deno.exit(1)\n  }\n  return value\n}\n\nasync function computeCargoLockHash(): Promise<string> {\n  const content = await Deno.readFile(CARGO_LOCK_PATH)\n  const hashBuffer = await crypto.subtle.digest('SHA-256', content)\n  const hashArray = Array.from(new Uint8Array(hashBuffer))\n  const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('')\n  return hashHex.substring(0, 16)\n}\n\nfunction getCacheKey(os: string, arch: string, hash: string): string {\n  return `nyanpasu-${os}-${arch}-${hash}`\n}\n\nfunction getFallbackPrefix(os: string, arch: string): string {\n  return `nyanpasu-${os}-${arch}-`\n}\n\nasync function createTarball(tarballPath: string): Promise<void> {\n  consola.info(`creating tarball from ${TARGET_DIR}...`)\n\n  const excludeArgs = TAR_EXCLUDE_PATTERNS.flatMap((p) => ['--exclude', p])\n\n  const cmd = new Deno.Command('tar', {\n    args: [\n      '--zstd',\n      '-cf',\n      tarballPath,\n      ...excludeArgs,\n      '-C',\n      path.dirname(TARGET_DIR),\n      path.basename(TARGET_DIR),\n    ],\n    stdout: 'inherit',\n    stderr: 'inherit',\n  })\n\n  const { code } = await cmd.output()\n  if (code !== 0) {\n    throw new Error(`tar creation failed with exit code ${code}`)\n  }\n\n  const stat = await Deno.stat(tarballPath)\n  consola.success(`tarball created: ${tarballPath} (${stat.size} bytes)`)\n}\n\nasync function extractTarball(tarballPath: string): Promise<void> {\n  consola.info(`extracting tarball to ${path.dirname(TARGET_DIR)}...`)\n\n  const cmd = new Deno.Command('tar', {\n    args: ['--zstd', '-xf', tarballPath, '-C', path.dirname(TARGET_DIR)],\n    stdout: 'inherit',\n    stderr: 'inherit',\n  })\n\n  const { code } = await cmd.output()\n  if (code !== 0) {\n    throw new Error(`tar extraction failed with exit code ${code}`)\n  }\n\n  consola.success('tarball extracted successfully')\n}\n\n// --- commands ---\n\nasync function save(os: string, arch: string): Promise<void> {\n  const token = requireEnv('FILE_SERVER_TOKEN')\n\n  if (!(await exists(TARGET_DIR))) {\n    consola.warn(`target directory does not exist: ${TARGET_DIR}`)\n    return\n  }\n\n  const hash = await computeCargoLockHash()\n  const key = getCacheKey(os, arch, hash)\n  const tarballPath = path.join(Deno.makeTempDirSync(), `${key}.tar.zst`)\n\n  try {\n    await createTarball(tarballPath)\n    await uploadCache(key, tarballPath, token)\n    consola.success(`cache saved with key: ${key}`)\n  } finally {\n    try {\n      await Deno.remove(tarballPath)\n    } catch {\n      // ignore cleanup errors\n    }\n  }\n}\n\nasync function restore(os: string, arch: string): Promise<void> {\n  const token = requireEnv('FILE_SERVER_TOKEN')\n\n  const hash = await computeCargoLockHash()\n  const key = getCacheKey(os, arch, hash)\n  const tarballPath = path.join(Deno.makeTempDirSync(), `${key}.tar.zst`)\n\n  try {\n    // try exact match first\n    let hit = await downloadCache(key, tarballPath, token)\n\n    if (!hit) {\n      // fallback: find most recent cache with matching prefix\n      const prefix = getFallbackPrefix(os, arch)\n      const keys = await listCacheKeys(prefix, token)\n\n      if (keys.length > 0) {\n        const fallbackKey = keys[0] // server returns sorted by update time desc\n        consola.info(`using fallback cache key: ${fallbackKey}`)\n        hit = await downloadCache(fallbackKey, tarballPath, token)\n      }\n    }\n\n    if (!hit) {\n      consola.warn('no cache found, build will run from scratch')\n      return\n    }\n\n    await extractTarball(tarballPath)\n    consola.success('build cache restored successfully')\n  } finally {\n    try {\n      await Deno.remove(tarballPath)\n    } catch {\n      // ignore cleanup errors\n    }\n  }\n}\n\n// --- main ---\n\nfunction main(): Promise<void> {\n  const args = parseArgs(Deno.args, {\n    string: ['os', 'arch'],\n  })\n\n  const subcommand = args._[0] as string | undefined\n  const os = args.os\n  const arch = args.arch\n\n  if (!subcommand || !['save', 'restore'].includes(subcommand)) {\n    consola.error(\n      'usage: build-cache.ts <save|restore> --os <os> --arch <arch>',\n    )\n    Deno.exit(1)\n  }\n\n  if (!os || !arch) {\n    consola.error('--os and --arch are required')\n    Deno.exit(1)\n  }\n\n  if (subcommand === 'save') {\n    return save(os, arch)\n  } else {\n    return restore(os, arch)\n  }\n}\n\nmain().catch((error) => {\n  consola.fatal(error)\n  Deno.exit(1)\n})\n"
  },
  {
    "path": "scripts/deno/check.ts",
    "content": "// @ts-types=\"npm:@types/adm-zip\"\nimport { parseArgs } from 'jsr:@std/cli@1/parse-args'\nimport { ensureDir, exists } from 'jsr:@std/fs'\nimport * as path from 'jsr:@std/path'\nimport AdmZip from 'npm:adm-zip'\n// @ts-types=\"npm:@types/figlet\"\nimport figlet from 'npm:figlet'\nimport { colorize, consola } from './utils/logger.ts'\n\n// === Types ===\n\ninterface BinInfo {\n  name: string\n  targetFile: string\n  exeFile: string\n  tmpFile: string\n  downloadURL: string\n}\n\ntype SupportedArch =\n  | 'windows-i386'\n  | 'windows-x86_64'\n  | 'windows-arm64'\n  | 'linux-aarch64'\n  | 'linux-amd64'\n  | 'linux-i386'\n  | 'linux-armv7'\n  | 'linux-armv7hf'\n  | 'darwin-arm64'\n  | 'darwin-x64'\n\ntype ArchMapping = Record<SupportedArch, string>\n\ninterface VersionManifest {\n  manifest_version: number\n  latest: {\n    mihomo: string\n    mihomo_alpha: string\n    clash_rs: string\n    clash_premium: string\n    clash_rs_alpha: string\n  }\n  arch_template: {\n    mihomo: ArchMapping\n    mihomo_alpha: ArchMapping\n    clash_rs: ArchMapping\n    clash_premium: ArchMapping\n    clash_rs_alpha: ArchMapping\n  }\n  updated_at: string\n}\n\ninterface ClashManifest {\n  URL_PREFIX: string\n  BACKUP_URL_PREFIX?: string\n  BACKUP_LATEST_DATE?: string\n  VERSION?: string\n  VERSION_URL?: string\n  ARCH_MAPPING: ArchMapping\n}\n\n// === Constants ===\n\nconst WORKSPACE_ROOT = path.join(import.meta.dirname!, '../..')\nconst TAURI_APP_DIR = path.join(WORKSPACE_ROOT, 'backend/tauri')\nconst TEMP_DIR = path.join(WORKSPACE_ROOT, 'node_modules/.verge')\n\n// === CLI Args ===\n\nconst args = parseArgs(Deno.args, {\n  boolean: ['force'],\n  string: ['arch', 'sidecar-host'],\n})\n\nconst FORCE = args.force\nconst ARCH_OVERRIDE = args.arch\n\n// === Platform detection ===\n\n// Deno.build.os: 'windows' | 'darwin' | 'linux' | ...\n// Map to Node-style for arch table compatibility\nconst platform = Deno.build.os === 'windows' ? 'win32' : Deno.build.os\n\n// Deno.build.arch: 'x86_64' | 'aarch64'\n// Map to Node-style for arch table compatibility\nconst DENO_ARCH_TO_NODE: Record<string, string> = {\n  x86_64: 'x64',\n  aarch64: 'arm64',\n}\nconst arch =\n  ARCH_OVERRIDE ?? DENO_ARCH_TO_NODE[Deno.build.arch] ?? Deno.build.arch\n\n// === Sidecar Host ===\n\nlet SIDECAR_HOST = args['sidecar-host']\nif (!SIDECAR_HOST) {\n  const cmd = new Deno.Command('rustc', { args: ['-vV'], stdout: 'piped' })\n  const { stdout } = await cmd.output()\n  const text = new TextDecoder().decode(stdout)\n  SIDECAR_HOST = text.match(/host: (.+)/)?.[1]?.trim()\n}\n\nif (!SIDECAR_HOST) {\n  consola.fatal(colorize`{red.bold SIDECAR_HOST} not found`)\n  Deno.exit(1)\n}\n\nconsola.debug(colorize`sidecar-host {yellow ${SIDECAR_HOST}}`)\nconsola.debug(colorize`platform {yellow ${platform}}`)\nconsola.debug(colorize`arch {yellow ${arch}}`)\n\n// === Arch Mapping ===\n\nfunction mapArch(platform: string, arch: string): SupportedArch {\n  const mapping: Partial<Record<string, SupportedArch>> = {\n    'darwin-x64': 'darwin-x64',\n    'darwin-arm64': 'darwin-arm64',\n    'win32-x64': 'windows-x86_64',\n    'win32-ia32': 'windows-i386',\n    'win32-arm64': 'windows-arm64',\n    'linux-x64': 'linux-amd64',\n    'linux-ia32': 'linux-i386',\n    'linux-arm': 'linux-armv7hf',\n    'linux-arm64': 'linux-aarch64',\n    'linux-armel': 'linux-armv7',\n  }\n  const result = mapping[`${platform}-${arch}`]\n  if (!result) {\n    throw new Error(`Unsupported platform/architecture: ${platform}-${arch}`)\n  }\n  return result\n}\n\n// === Version Manifest ===\n\nconst versionManifest = JSON.parse(\n  await Deno.readTextFile(path.join(WORKSPACE_ROOT, 'manifest/version.json')),\n) as VersionManifest\n\nconst CLASH_MANIFEST: ClashManifest = {\n  URL_PREFIX: 'https://github.com/Dreamacro/clash/releases/download/premium/',\n  BACKUP_URL_PREFIX:\n    'https://github.com/zhongfly/Clash-premium-backup/releases/download/',\n  BACKUP_LATEST_DATE: versionManifest.latest.clash_premium,\n  VERSION: versionManifest.latest.clash_premium,\n  ARCH_MAPPING: versionManifest.arch_template.clash_premium as ArchMapping,\n}\n\nconst CLASH_META_MANIFEST: ClashManifest = {\n  URL_PREFIX: `https://github.com/MetaCubeX/mihomo/releases/download/${versionManifest.latest.mihomo}`,\n  VERSION: versionManifest.latest.mihomo,\n  ARCH_MAPPING: versionManifest.arch_template.mihomo as ArchMapping,\n}\n\nconst CLASH_META_ALPHA_MANIFEST: ClashManifest = {\n  VERSION_URL:\n    'https://github.com/MetaCubeX/mihomo/releases/download/Prerelease-Alpha/version.txt',\n  URL_PREFIX:\n    'https://github.com/MetaCubeX/mihomo/releases/download/Prerelease-Alpha',\n  ARCH_MAPPING: versionManifest.arch_template.mihomo_alpha as ArchMapping,\n}\n\nconst CLASH_RS_MANIFEST: ClashManifest = {\n  URL_PREFIX: 'https://github.com/Watfaq/clash-rs/releases/download/',\n  VERSION: versionManifest.latest.clash_rs,\n  ARCH_MAPPING: versionManifest.arch_template.clash_rs as ArchMapping,\n}\n\nconst CLASH_RS_ALPHA_MANIFEST: ClashManifest = {\n  VERSION_URL:\n    'https://github.com/Watfaq/clash-rs/releases/download/latest/version.txt',\n  URL_PREFIX: 'https://github.com/Watfaq/clash-rs/releases/download/latest',\n  ARCH_MAPPING: versionManifest.arch_template.clash_rs_alpha as ArchMapping,\n}\n\n// === Download ===\n\nasync function downloadFile(url: string, filePath: string): Promise<void> {\n  consola.debug(colorize`downloading {gray \"${url.split('/').at(-1)}\"}`)\n\n  const response = await fetch(url, {\n    method: 'GET',\n    headers: {\n      'Content-Type': 'application/octet-stream',\n      'User-Agent':\n        'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:131.0) Gecko/20100101 Firefox/131.0',\n    },\n  })\n\n  if (!response.ok) {\n    throw new Error(\n      `download failed: ${response.statusText} (${response.status})`,\n    )\n  }\n\n  const buffer = await response.arrayBuffer()\n  await Deno.writeFile(filePath, new Uint8Array(buffer))\n}\n\n// === Extract Helpers ===\n\nasync function extractZip(\n  zipPath: string,\n  destDir: string,\n  name: string,\n): Promise<string> {\n  const zip = new AdmZip(zipPath)\n  const baseName = name\n    .split('-')\n    .filter((o: string) => o !== 'alpha')\n    .join('-')\n  let entryName: string | undefined\n\n  for (const entry of zip.getEntries()) {\n    consola.debug(colorize`\"{green ${name}}\" entry name ${entry.entryName}`)\n    if (\n      (entry.entryName.includes(name) && entry.entryName.endsWith('.exe')) ||\n      (entry.entryName.includes(baseName) && entry.entryName.endsWith('.exe'))\n    ) {\n      entryName = entry.entryName\n    }\n  }\n\n  zip.extractAllTo(destDir, true)\n\n  if (!entryName) throw new Error('cannot find exe file in zip')\n\n  return path.join(destDir, entryName)\n}\n\nasync function extractTarGz(\n  tarPath: string,\n  destDir: string,\n  name: string,\n): Promise<void> {\n  const cmd = new Deno.Command('tar', {\n    args: ['-xzf', tarPath, '-C', destDir],\n    stdout: 'piped',\n    stderr: 'piped',\n  })\n  const { code, stderr } = await cmd.output()\n  if (code !== 0) {\n    throw new Error(\n      `tar extraction failed: ${new TextDecoder().decode(stderr)}`,\n    )\n  }\n}\n\nasync function gunzipFile(\n  inputPath: string,\n  outputPath: string,\n): Promise<void> {\n  const input = await Deno.open(inputPath, { read: true })\n  const output = await Deno.open(outputPath, { write: true, create: true })\n  await input.readable\n    .pipeThrough(new DecompressionStream('gzip'))\n    .pipeTo(output.writable)\n}\n\n// === Resource Resolution ===\n\nasync function resolveResource(\n  binInfo: { file: string; downloadURL: string },\n  options?: { force?: boolean },\n): Promise<void> {\n  const { file, downloadURL } = binInfo\n  const resDir = path.join(TAURI_APP_DIR, 'resources')\n  const targetPath = path.join(resDir, file)\n\n  if (!options?.force && (await exists(targetPath))) return\n\n  await ensureDir(resDir)\n  await downloadFile(downloadURL, targetPath)\n\n  consola.success(colorize`resolve {green ${file}} finished`)\n}\n\nasync function resolveSidecar(\n  binInfo: BinInfo | Promise<BinInfo>,\n  options?: { force?: boolean },\n): Promise<void> {\n  const { name, targetFile, tmpFile, exeFile, downloadURL } = await binInfo\n\n  const sidecarDir = path.join(TAURI_APP_DIR, 'sidecar')\n  const sidecarPath = path.join(sidecarDir, targetFile)\n\n  await ensureDir(sidecarDir)\n\n  if (!options?.force && (await exists(sidecarPath))) return\n\n  const tempDir = path.join(TEMP_DIR, name)\n  const tempFile = path.join(tempDir, tmpFile)\n  const tempExe = path.join(tempDir, exeFile)\n\n  await ensureDir(tempDir)\n\n  try {\n    if (!(await exists(tempFile))) {\n      await downloadFile(downloadURL, tempFile)\n    }\n\n    if (tmpFile.endsWith('.zip')) {\n      const extractedExe = await extractZip(tempFile, tempDir, name)\n      await Deno.rename(extractedExe, tempExe)\n      await Deno.rename(tempExe, sidecarPath)\n    } else if (tmpFile.endsWith('.tar.gz')) {\n      await extractTarGz(tempFile, tempDir, name)\n      await Deno.rename(tempExe, sidecarPath)\n    } else if (tmpFile.endsWith('.gz')) {\n      await gunzipFile(tempFile, sidecarPath)\n      await Deno.chmod(sidecarPath, 0o755)\n    } else {\n      await Deno.rename(tempFile, sidecarPath)\n      if (platform !== 'win32') {\n        await Deno.chmod(sidecarPath, 0o755)\n      }\n    }\n\n    consola.success(colorize`resolve {green ${name}} finished`)\n  } catch (err) {\n    try {\n      await Deno.remove(sidecarPath)\n    } catch {\n      // ignore\n    }\n    throw err\n  } finally {\n    try {\n      await Deno.remove(tempDir, { recursive: true })\n    } catch {\n      // ignore\n    }\n  }\n}\n\n// === Binary Info Functions ===\n\nfunction getClashBackupInfo(): BinInfo {\n  const { ARCH_MAPPING, BACKUP_URL_PREFIX, BACKUP_LATEST_DATE } = CLASH_MANIFEST\n  const archLabel = mapArch(platform, arch)\n  const name = ARCH_MAPPING[archLabel].replace('{}', BACKUP_LATEST_DATE!)\n  const isWin = platform === 'win32'\n  return {\n    name: 'clash',\n    targetFile: `clash-${SIDECAR_HOST}${isWin ? '.exe' : ''}`,\n    exeFile: `${name}${isWin ? '.exe' : ''}`,\n    tmpFile: name,\n    downloadURL: `${BACKUP_URL_PREFIX}${BACKUP_LATEST_DATE}/${name}`,\n  }\n}\n\nfunction getClashMetaInfo(): BinInfo {\n  const { ARCH_MAPPING, URL_PREFIX, VERSION } = CLASH_META_MANIFEST\n  const archLabel = mapArch(platform, arch)\n  const name = ARCH_MAPPING[archLabel].replace('{}', VERSION!)\n  const isWin = platform === 'win32'\n  return {\n    name: 'mihomo',\n    targetFile: `mihomo-${SIDECAR_HOST}${isWin ? '.exe' : ''}`,\n    exeFile: `${name}${isWin ? '.exe' : ''}`,\n    tmpFile: name,\n    downloadURL: `${URL_PREFIX}/${name}`,\n  }\n}\n\nasync function getClashMetaAlphaInfo(): Promise<BinInfo> {\n  const { ARCH_MAPPING, URL_PREFIX, VERSION_URL } = CLASH_META_ALPHA_MANIFEST\n  const resp = await fetch(VERSION_URL!)\n  const version = (await resp.text()).trim()\n  consola.debug(`mihomo-alpha version: ${version}`)\n  const archLabel = mapArch(platform, arch)\n  const name = ARCH_MAPPING[archLabel].replace('{}', version)\n  const isWin = platform === 'win32'\n  return {\n    name: 'mihomo-alpha',\n    targetFile: `mihomo-alpha-${SIDECAR_HOST}${isWin ? '.exe' : ''}`,\n    exeFile: `${name}${isWin ? '.exe' : ''}`,\n    tmpFile: name,\n    downloadURL: `${URL_PREFIX}/${name}`,\n  }\n}\n\nfunction getClashRustInfo(): BinInfo {\n  const { ARCH_MAPPING, URL_PREFIX, VERSION } = CLASH_RS_MANIFEST\n  const archLabel = mapArch(platform, arch)\n  const name = ARCH_MAPPING[archLabel].replace('{}', VERSION!)\n  const isWin = platform === 'win32'\n  return {\n    name: 'clash-rs',\n    targetFile: `clash-rs-${SIDECAR_HOST}${isWin ? '.exe' : ''}`,\n    exeFile: name,\n    tmpFile: name,\n    downloadURL: `${URL_PREFIX}${VERSION}/${name}`,\n  }\n}\n\nasync function getClashRustAlphaInfo(): Promise<BinInfo> {\n  const { ARCH_MAPPING, VERSION_URL, URL_PREFIX } = CLASH_RS_ALPHA_MANIFEST\n\n  const resp = await fetch(VERSION_URL!)\n  const version = (await resp.text()).trim()\n  consola.debug(`clash-rs-alpha version: ${version}`)\n  const archLabel = mapArch(platform, arch)\n  const name = ARCH_MAPPING[archLabel].replace('{}', version)\n  const isWin = platform === 'win32'\n  return {\n    name: 'clash-rs-alpha',\n    targetFile: `clash-rs-alpha-${SIDECAR_HOST}${isWin ? '.exe' : ''}`,\n    exeFile: name,\n    tmpFile: name,\n    downloadURL: `${URL_PREFIX}/${name}`,\n  }\n}\n\nasync function getNyanpasuServiceInfo(): Promise<BinInfo> {\n  const SERVICE_REPO = 'libnyanpasu/nyanpasu-service'\n  const isWin = SIDECAR_HOST!.includes('windows')\n  const urlExt = isWin ? 'zip' : 'tar.gz'\n\n  const response = await fetch(\n    `https://github.com/${SERVICE_REPO}/releases/latest`,\n    { method: 'GET', redirect: 'manual' },\n  )\n  const location = response.headers.get('location')\n  if (!location) throw new Error('Cannot find location from response header')\n  const version = location.split('/').pop()\n  if (!version) throw new Error('Cannot find tag from location')\n  consola.debug(`nyanpasu-service version: ${version}`)\n\n  const name = 'nyanpasu-service'\n  return {\n    name,\n    targetFile: `${name}-${SIDECAR_HOST}${isWin ? '.exe' : ''}`,\n    exeFile: `${name}${isWin ? '.exe' : ''}`,\n    tmpFile: `${name}-${SIDECAR_HOST}.${urlExt}`,\n    downloadURL: `https://github.com/${SERVICE_REPO}/releases/download/${version}/${name}-${SIDECAR_HOST}.${urlExt}`,\n  }\n}\n\nasync function resolveWintun(): Promise<void> {\n  if (platform !== 'win32') return\n\n  const wintunArchMap: Record<string, string> = {\n    x64: 'amd64',\n    ia32: 'x86',\n    arm: 'arm',\n    arm64: 'arm64',\n  }\n  const wintunArch = wintunArchMap[arch]\n  if (!wintunArch) throw new Error(`unsupported arch ${arch}`)\n\n  const url = 'https://www.wintun.net/builds/wintun-0.14.1.zip'\n  const expectedHash =\n    '07c256185d6ee3652e09fa55c0b673e2624b565e02c4b9091c79ca7d2f24ef51'\n  const tempDir = path.join(TEMP_DIR, 'wintun')\n  const tempZip = path.join(tempDir, 'wintun.zip')\n  const targetPath = path.join(TAURI_APP_DIR, 'resources', 'wintun.dll')\n\n  if (!FORCE && (await exists(targetPath))) return\n\n  await ensureDir(tempDir)\n\n  if (!(await exists(tempZip))) {\n    await downloadFile(url, tempZip)\n  }\n\n  // verify SHA-256\n  const fileData = await Deno.readFile(tempZip)\n  const hashBuffer = await crypto.subtle.digest('SHA-256', fileData)\n  const hashHex = Array.from(new Uint8Array(hashBuffer))\n    .map((b) => b.toString(16).padStart(2, '0'))\n    .join('')\n  if (hashHex !== expectedHash) {\n    throw new Error(`wintun hash not match ${hashHex}`)\n  }\n\n  // extract\n  const zip = new AdmZip(tempZip)\n  zip.extractAllTo(tempDir, true)\n\n  // recursively find wintun.dll for the target arch\n  function findDlls(dir: string): string[] {\n    const results: string[] = []\n    for (const entry of Deno.readDirSync(dir)) {\n      const fullPath = path.join(dir, entry.name)\n      if (entry.isDirectory) {\n        results.push(...findDlls(fullPath))\n      } else if (entry.name === 'wintun.dll' && fullPath.includes(wintunArch)) {\n        results.push(fullPath)\n      }\n    }\n    return results\n  }\n\n  const dlls = findDlls(tempDir)\n  const dll = dlls[0]\n  if (!dll) throw new Error(`wintun not found for arch ${wintunArch}`)\n\n  await ensureDir(path.dirname(targetPath))\n  await Deno.copyFile(dll, targetPath)\n  await Deno.remove(tempDir, { recursive: true })\n\n  consola.success(colorize`resolve {green wintun.dll} finished`)\n}\n\n// === Task Runner ===\n\nconst tasks: Array<{\n  name: string\n  func: () => Promise<void>\n  retry: number\n  winOnly?: boolean\n}> = [\n  {\n    name: 'clash',\n    func: () =>\n      resolveSidecar(getClashBackupInfo(), {\n        force: FORCE,\n      }),\n    retry: 5,\n  },\n  {\n    name: 'mihomo',\n    func: () => resolveSidecar(getClashMetaInfo(), { force: FORCE }),\n    retry: 5,\n  },\n  {\n    name: 'mihomo-alpha',\n    func: () => resolveSidecar(getClashMetaAlphaInfo(), { force: FORCE }),\n    retry: 5,\n  },\n  {\n    name: 'clash-rs',\n    func: () => resolveSidecar(getClashRustInfo(), { force: FORCE }),\n    retry: 5,\n  },\n  {\n    name: 'clash-rs-alpha',\n    func: () => resolveSidecar(getClashRustAlphaInfo(), { force: FORCE }),\n    retry: 5,\n  },\n  { name: 'wintun', func: () => resolveWintun(), retry: 5, winOnly: true },\n  {\n    name: 'nyanpasu-service',\n    func: () => resolveSidecar(getNyanpasuServiceInfo(), { force: FORCE }),\n    retry: 5,\n  },\n  {\n    name: 'mmdb',\n    func: () =>\n      resolveResource(\n        {\n          file: 'Country.mmdb',\n          downloadURL:\n            'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/country.mmdb',\n        },\n        { force: FORCE },\n      ),\n    retry: 5,\n  },\n  {\n    name: 'geoip',\n    func: () =>\n      resolveResource(\n        {\n          file: 'geoip.dat',\n          downloadURL:\n            'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.dat',\n        },\n        { force: FORCE },\n      ),\n    retry: 5,\n  },\n  {\n    name: 'geosite',\n    func: () =>\n      resolveResource(\n        {\n          file: 'geosite.dat',\n          downloadURL:\n            'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat',\n        },\n        { force: FORCE },\n      ),\n    retry: 5,\n  },\n  {\n    name: 'enableLoopback',\n    func: () =>\n      resolveResource(\n        {\n          file: 'enableLoopback.exe',\n          downloadURL:\n            'https://github.com/Kuingsmile/uwp-tool/releases/download/latest/enableLoopback.exe',\n        },\n        { force: FORCE },\n      ),\n    retry: 5,\n    winOnly: true,\n  },\n]\n\nasync function runTask(): Promise<void> {\n  const task = tasks.shift()\n  if (!task) return\n  if (task.winOnly && platform !== 'win32') return runTask()\n\n  for (let i = 0; i < task.retry; i++) {\n    try {\n      await task.func()\n      break\n    } catch (err) {\n      consola.warn(`task::${task.name} try ${i} ==`, err)\n      if (i === task.retry - 1) {\n        consola.fatal(`task::${task.name} failed`, err)\n        Deno.exit(1)\n      }\n    }\n  }\n\n  return runTask()\n}\n\n// === Main ===\n\nconsola.start('start check and download resources...')\n\nconst concurrency = Math.ceil(navigator.hardwareConcurrency / 2) || 2\nconst jobs = Array.from({ length: concurrency }, () => runTask())\n\nawait Promise.all(jobs)\n\nconsole.log(figlet.textSync('Clash Nyanpasu', { whitespaceBreak: true }))\nconsola.success('all resources download finished\\n')\nconsola.log('  next command:\\n')\nconsola.log('    pnpm dev - development')\nconsola.log('    pnpm dev:diff - deadlock development (recommend)')\n"
  },
  {
    "path": "scripts/deno/deno.jsonc",
    "content": "{\n  \"tasks\": {\n    \"check\": {\n      \"description\": \"Check and download required sidecar binaries and resources\",\n      \"command\": \"deno run -A check.ts\",\n    },\n    \"upload-macos-updater\": {\n      \"description\": \"Upload macOS updater to GitHub Releases\",\n      \"command\": \"deno run -A upload-macos-updater.ts\",\n    },\n    \"telegram-notify\": {\n      \"description\": \"Send Telegram notification for releases and nightly builds\",\n      \"command\": \"deno run -A telegram-notify.ts\",\n    },\n    \"build-cache\": {\n      \"description\": \"Save/restore build cache to file server\",\n      \"command\": \"deno run -A build-cache.ts\",\n    },\n  },\n}\n"
  },
  {
    "path": "scripts/deno/generate-latest-version.ts",
    "content": "import { ensureDir } from 'jsr:@std/fs'\nimport * as path from 'jsr:@std/path'\nimport {\n  resolveClashPremium,\n  resolveClashRs,\n  resolveClashRsAlpha,\n  resolveMihomo,\n  resolveMihomoAlpha,\n  type ArchMapping,\n} from './manifest.ts'\nimport { colorize, consola } from './utils/logger.ts'\n\n// === Constants ===\n\nconst WORKSPACE_ROOT = path.join(import.meta.dirname!, '../..')\nconst MANIFEST_DIR = path.join(WORKSPACE_ROOT, 'manifest')\nconst MANIFEST_VERSION_PATH = path.join(MANIFEST_DIR, 'version.json')\nconst MANIFEST_VERSION = 1\n\n// === Types ===\n\ntype SupportedCore =\n  | 'mihomo'\n  | 'mihomo_alpha'\n  | 'clash_rs'\n  | 'clash_rs_alpha'\n  | 'clash_premium'\n\ninterface ManifestVersion {\n  manifest_version: number\n  latest: Record<SupportedCore, string>\n  arch_template: Record<SupportedCore, ArchMapping>\n  updated_at?: string\n}\n\n// === Main ===\n\nconst resolvers = [\n  resolveMihomo,\n  resolveMihomoAlpha,\n  resolveClashRs,\n  resolveClashPremium,\n  resolveClashRsAlpha,\n]\n\nconsola.start(colorize`{cyan Resolving} latest versions`)\n\nconst results = await Promise.all(resolvers.map((r) => r()))\n\nconsola.success('Resolved latest versions')\nconsola.start('Generating manifest')\n\nconst manifest: ManifestVersion = {\n  manifest_version: MANIFEST_VERSION,\n  latest: {} as Record<SupportedCore, string>,\n  arch_template: {} as Record<SupportedCore, ArchMapping>,\n}\n\nfor (const result of results) {\n  manifest.latest[result.name as SupportedCore] = result.version\n  manifest.arch_template[result.name as SupportedCore] = result.archMapping\n}\n\nawait ensureDir(MANIFEST_DIR)\n\n// If no changes, skip writing manifest\nlet previousManifest: Partial<ManifestVersion> = {}\ntry {\n  previousManifest = JSON.parse(await Deno.readTextFile(MANIFEST_VERSION_PATH))\n  delete previousManifest.updated_at\n} catch {\n  // file may not exist yet\n}\n\nif (JSON.stringify(previousManifest) === JSON.stringify(manifest)) {\n  consola.success('No changes, skip writing manifest')\n  Deno.exit(0)\n}\n\nmanifest.updated_at = new Date().toISOString()\n\nconsola.success('Generated manifest')\n\nawait Deno.writeTextFile(\n  MANIFEST_VERSION_PATH,\n  JSON.stringify(manifest, null, 2) + '\\n',\n)\n\nconsola.success('Manifest written')\n"
  },
  {
    "path": "scripts/deno/manifest.ts",
    "content": "import { consola } from './utils/logger.ts'\n\n// === Types ===\n\nexport type SupportedArch =\n  | 'windows-i386'\n  | 'windows-x86_64'\n  | 'windows-arm64'\n  | 'linux-aarch64'\n  | 'linux-amd64'\n  | 'linux-i386'\n  | 'linux-armv7'\n  | 'linux-armv7hf'\n  | 'darwin-arm64'\n  | 'darwin-x64'\n\nexport type ArchMapping = Record<SupportedArch, string>\n\nexport type LatestVersionResolver = Promise<{\n  name: string\n  version: string\n  archMapping: ArchMapping\n}>\n\n// === GitHub API helpers ===\n\nconst GITHUB_API_HEADERS = {\n  Accept: 'application/vnd.github+json',\n  'User-Agent': 'clash-nyanpasu',\n}\n\nasync function githubFetch<T>(url: string): Promise<T> {\n  const resp = await fetch(url, { headers: GITHUB_API_HEADERS })\n  if (!resp.ok) {\n    throw new Error(\n      `GitHub API error: ${resp.statusText} (${resp.status}) — ${url}`,\n    )\n  }\n  return resp.json() as Promise<T>\n}\n\nasync function getLatestRelease(owner: string, repo: string): Promise<string> {\n  const data = await githubFetch<{ tag_name: string }>(\n    `https://api.github.com/repos/${owner}/${repo}/releases/latest`,\n  )\n  return data.tag_name\n}\n\n// === Resolvers ===\n\nexport const resolveMihomo = async (): LatestVersionResolver => {\n  const version = await getLatestRelease('MetaCubeX', 'mihomo')\n  consola.debug(`mihomo latest release: ${version}`)\n\n  const archMapping: ArchMapping = {\n    'windows-i386': 'mihomo-windows-386-{}.zip',\n    'windows-x86_64': 'mihomo-windows-amd64-v1-{}.zip',\n    'windows-arm64': 'mihomo-windows-arm64-{}.zip',\n    'linux-aarch64': 'mihomo-linux-arm64-{}.gz',\n    'linux-amd64': 'mihomo-linux-amd64-v1-{}.gz',\n    'linux-i386': 'mihomo-linux-386-{}.gz',\n    'darwin-arm64': 'mihomo-darwin-arm64-{}.gz',\n    'darwin-x64': 'mihomo-darwin-amd64-v1-{}.gz',\n    'linux-armv7': 'mihomo-linux-armv5-{}.gz',\n    'linux-armv7hf': 'mihomo-linux-armv7-{}.gz',\n  }\n\n  return { name: 'mihomo', version, archMapping }\n}\n\nexport const resolveMihomoAlpha = async (): LatestVersionResolver => {\n  const resp = await fetch(\n    'https://github.com/MetaCubeX/mihomo/releases/download/Prerelease-Alpha/version.txt',\n  )\n  const alphaReleaseHash = (await resp.text()).trim()\n  consola.debug(`mihomo alpha release: ${alphaReleaseHash}`)\n\n  const archMapping: ArchMapping = {\n    'windows-i386': 'mihomo-windows-386-{}.zip',\n    'windows-x86_64': 'mihomo-windows-amd64-v1-{}.zip',\n    'windows-arm64': 'mihomo-windows-arm64-{}.zip',\n    'linux-aarch64': 'mihomo-linux-arm64-{}.gz',\n    'linux-amd64': 'mihomo-linux-amd64-v1-{}.gz',\n    'linux-i386': 'mihomo-linux-386-{}.gz',\n    'darwin-arm64': 'mihomo-darwin-arm64-{}.gz',\n    'darwin-x64': 'mihomo-darwin-amd64-v1-{}.gz',\n    'linux-armv7': 'mihomo-linux-armv5-{}.gz',\n    'linux-armv7hf': 'mihomo-linux-armv7-{}.gz',\n  }\n\n  return { name: 'mihomo_alpha', version: alphaReleaseHash, archMapping }\n}\n\nexport const resolveClashRs = async (): LatestVersionResolver => {\n  const version = await getLatestRelease('Watfaq', 'clash-rs')\n  consola.debug(`clash-rs latest release: ${version}`)\n\n  const archMapping: ArchMapping = {\n    'windows-i386': 'clash-rs-i686-pc-windows-msvc-static-crt.exe',\n    'windows-x86_64': 'clash-rs-x86_64-pc-windows-msvc.exe',\n    'windows-arm64': 'clash-rs-aarch64-pc-windows-msvc.exe',\n    'linux-aarch64': 'clash-rs-aarch64-unknown-linux-gnu',\n    'linux-amd64': 'clash-rs-x86_64-unknown-linux-gnu-static-crt',\n    'linux-i386': 'clash-rs-i686-unknown-linux-gnu',\n    'darwin-arm64': 'clash-rs-aarch64-apple-darwin',\n    'darwin-x64': 'clash-rs-x86_64-apple-darwin',\n    'linux-armv7': 'clash-rs-armv7-unknown-linux-gnueabi',\n    'linux-armv7hf': 'clash-rs-armv7-unknown-linux-gnueabihf',\n  }\n\n  return { name: 'clash_rs', version, archMapping }\n}\n\nexport const resolveClashRsAlpha = async (): LatestVersionResolver => {\n  // Fetch commit SHA for the \"latest\" pre-release tag and the stable base version in parallel\n  const [ref, stableTag] = await Promise.all([\n    githubFetch<{ object: { type: string; sha: string; url: string } }>(\n      'https://api.github.com/repos/Watfaq/clash-rs/git/ref/tags/latest',\n    ),\n    getLatestRelease('Watfaq', 'clash-rs'),\n  ])\n\n  // Dereference annotated tags to get the underlying commit SHA\n  let commitSha = ref.object.sha\n  if (ref.object.type === 'tag') {\n    const tagObj = await githubFetch<{ object: { sha: string } }>(\n      ref.object.url,\n    )\n    commitSha = tagObj.object.sha\n  }\n\n  const shortSha = commitSha.substring(0, 7)\n  const baseVersion = stableTag.replace(/^v/, '')\n  const alphaVersion = `${baseVersion}-alpha+sha.${shortSha}`\n  consola.debug(`clash-rs alpha latest release: ${alphaVersion}`)\n\n  const archMapping: ArchMapping = {\n    'windows-i386': 'clash-rs-i686-pc-windows-msvc-static-crt.exe',\n    'windows-x86_64': 'clash-rs-x86_64-pc-windows-msvc.exe',\n    'windows-arm64': 'clash-rs-aarch64-pc-windows-msvc.exe',\n    'linux-aarch64': 'clash-rs-aarch64-unknown-linux-gnu',\n    'linux-amd64': 'clash-rs-x86_64-unknown-linux-gnu-static-crt',\n    'linux-i386': 'clash-rs-i686-unknown-linux-gnu',\n    'darwin-arm64': 'clash-rs-aarch64-apple-darwin',\n    'darwin-x64': 'clash-rs-x86_64-apple-darwin',\n    'linux-armv7': 'clash-rs-armv7-unknown-linux-gnueabi',\n    'linux-armv7hf': 'clash-rs-armv7-unknown-linux-gnueabihf',\n  }\n\n  return { name: 'clash_rs_alpha', version: alphaVersion, archMapping }\n}\n\nexport const resolveClashPremium = async (): LatestVersionResolver => {\n  const version = await getLatestRelease('zhongfly', 'Clash-premium-backup')\n  consola.debug(`clash-premium latest release: ${version}`)\n\n  const archMapping: ArchMapping = {\n    'windows-i386': 'clash-windows-386-n{}.zip',\n    'windows-x86_64': 'clash-windows-amd64-n{}.zip',\n    'windows-arm64': 'clash-windows-arm64-n{}.zip',\n    'linux-aarch64': 'clash-linux-arm64-n{}.gz',\n    'linux-amd64': 'clash-linux-amd64-n{}.gz',\n    'linux-i386': 'clash-linux-386-n{}.gz',\n    'darwin-arm64': 'clash-darwin-arm64-n{}.gz',\n    'darwin-x64': 'clash-darwin-amd64-n{}.gz',\n    'linux-armv7': 'clash-linux-armv5-n{}.gz',\n    'linux-armv7hf': 'clash-linux-armv7-n{}.gz',\n  }\n\n  return { name: 'clash_premium', version, archMapping }\n}\n"
  },
  {
    "path": "scripts/deno/telegram-notify.ts",
    "content": "import { retry } from 'jsr:@std/async@1/retry'\nimport { format as formatBytes } from 'jsr:@std/fmt@1/bytes'\nimport { ensureDir, exists } from 'jsr:@std/fs'\nimport * as path from 'jsr:@std/path'\nimport { Bot } from 'npm:grammy'\nimport {\n  UPLOAD_CONCURRENCY,\n  uploadAllFiles,\n  UploadResult,\n} from './utils/file-server.ts'\nimport { consola } from './utils/logger.ts'\n\n// --- env helpers ---\n\nfunction requireEnv(name: string): string {\n  const value = Deno.env.get(name)\n  if (!value) {\n    consola.fatal(`${name} is required`)\n    Deno.exit(1)\n  }\n  return value\n}\n\nconst nightlyBuild = Deno.args.includes('--nightly')\nconst fromLocal = Deno.args.includes('--from-local')\n\nconst TELEGRAM_TOKEN = requireEnv('TELEGRAM_TOKEN')\nconst TELEGRAM_TO = requireEnv('TELEGRAM_TO')\nconst TELEGRAM_TO_NIGHTLY = requireEnv('TELEGRAM_TO_NIGHTLY')\nconst GITHUB_TOKEN = requireEnv('GITHUB_TOKEN')\nconst FILE_SERVER_TOKEN = fromLocal ? '' : requireEnv('FILE_SERVER_TOKEN')\nconst WORKFLOW_RUN_ID = Deno.env.get('WORKFLOW_RUN_ID')\nconst UPLOAD_RESULTS_DIR = Deno.env.get('UPLOAD_RESULTS_DIR') || '.'\n\n// --- constants ---\n\nconst WORKSPACE_ROOT = path.join(import.meta.dirname!, '../..')\nconst TEMP_DIR = path.join(WORKSPACE_ROOT, 'node_modules/.verge')\n\nconst repoInfo = { owner: 'libnyanpasu', repo: 'clash-nyanpasu' } as const\n\nconst resourceFormats = [\n  '.exe',\n  'portable.zip',\n  '.rpm',\n  '.deb',\n  '.dmg',\n  '.AppImage',\n]\n\n// --- helpers ---\n\nfunction isValidFormat(fileName: string): boolean {\n  return resourceFormats.some((fmt) => fileName.endsWith(fmt))\n}\n\nfunction getFileSize(filePath: string): string {\n  const stat = Deno.statSync(filePath)\n  return formatBytes(stat.size ?? 0)\n}\n\nfunction getGitShortHash(): string {\n  const cmd = new Deno.Command('git', {\n    args: ['rev-parse', '--short', 'HEAD'],\n    stdout: 'piped',\n  })\n  const { stdout } = cmd.outputSync()\n  return new TextDecoder().decode(stdout).trim()\n}\n\nasync function getVersion(): Promise<string> {\n  const pkgPath = path.join(WORKSPACE_ROOT, 'package.json')\n  const pkg = JSON.parse(await Deno.readTextFile(pkgPath))\n  return pkg.version as string\n}\n\nasync function downloadFile(url: string, destPath: string): Promise<void> {\n  consola.debug(`download \"${url}\" to \"${destPath}\"`)\n\n  const response = await fetch(url, {\n    method: 'GET',\n    headers: {\n      'Content-Type': 'application/octet-stream',\n      'User-Agent':\n        'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:131.0) Gecko/20100101 Firefox/131.0',\n    },\n  })\n\n  if (!response.ok) {\n    throw new Error(`download failed: ${response.statusText}`)\n  }\n\n  const buffer = new Uint8Array(await response.arrayBuffer())\n  await Deno.writeFile(destPath, buffer)\n\n  consola.success(`download finished \"${url.split('/').at(-1)}\"`)\n}\n\ninterface GitHubAsset {\n  name: string\n  browser_download_url: string\n}\n\ninterface GitHubRelease {\n  assets: GitHubAsset[]\n}\n\nasync function fetchRelease(): Promise<GitHubRelease> {\n  const { owner, repo } = repoInfo\n  const url = nightlyBuild\n    ? `https://api.github.com/repos/${owner}/${repo}/releases/tags/pre-release`\n    : `https://api.github.com/repos/${owner}/${repo}/releases/latest`\n\n  const resp = await fetch(url, {\n    headers: {\n      Accept: 'application/vnd.github+json',\n      Authorization: `Bearer ${GITHUB_TOKEN}`,\n      'X-GitHub-Api-Version': '2022-11-28',\n    },\n  })\n\n  if (!resp.ok) {\n    throw new Error(`GitHub API error: ${resp.status} ${resp.statusText}`)\n  }\n\n  return (await resp.json()) as GitHubRelease\n}\n\nasync function readLocalUploadResults(dir: string): Promise<UploadResult[]> {\n  const results: UploadResult[] = []\n  try {\n    for await (const entry of Deno.readDir(dir)) {\n      if (entry.isDirectory) {\n        const jsonPath = path.join(dir, entry.name, 'upload-results.json')\n        try {\n          const content = await Deno.readTextFile(jsonPath)\n          const parsed = JSON.parse(content) as UploadResult[]\n          results.push(...parsed)\n          consola.success(`Loaded ${parsed.length} results from ${entry.name}`)\n        } catch {\n          // No upload-results.json in this subdirectory, skip\n        }\n      }\n    }\n  } catch (err) {\n    consola.warn(`Could not read directory ${dir}: ${err}`)\n  }\n  return results\n}\n\n// --- platform grouping ---\n\ninterface PlatformGroup {\n  label: string\n  filter: (filePath: string) => boolean\n}\n\nconst platformGroups: PlatformGroup[] = [\n  {\n    label: 'Windows',\n    filter: (item) =>\n      !item.includes('fixed-webview') &&\n      (item.endsWith('.exe') || item.endsWith('portable.zip')),\n  },\n  {\n    label: 'macOS',\n    filter: (item) => item.endsWith('.dmg'),\n  },\n  {\n    label: 'Linux',\n    filter: (item) =>\n      (item.endsWith('.rpm') ||\n        item.endsWith('.deb') ||\n        item.endsWith('.AppImage')) &&\n      !item.includes('armel') &&\n      !item.includes('armhf'),\n  },\n  {\n    label: 'Linux (armv7)',\n    filter: (item) =>\n      (item.endsWith('.rpm') ||\n        item.endsWith('.deb') ||\n        item.endsWith('.AppImage')) &&\n      (item.includes('armel') || item.includes('armhf')),\n  },\n]\n\n// --- main ---\n\nasync function main() {\n  const bot = new Bot(TELEGRAM_TOKEN)\n\n  const GIT_SHORT_HASH = getGitShortHash()\n  const version = await getVersion()\n\n  let uploadResults: UploadResult[]\n\n  if (fromLocal) {\n    consola.info(\n      `Reading upload results from local directory: ${UPLOAD_RESULTS_DIR}`,\n    )\n    uploadResults = await readLocalUploadResults(UPLOAD_RESULTS_DIR)\n    consola.success(`Loaded ${uploadResults.length} total upload results`)\n  } else {\n    const release = await fetchRelease()\n    const resourceMapping: string[] = []\n    const downloadTasks: Promise<void>[] = []\n\n    for (const asset of release.assets) {\n      if (isValidFormat(asset.name)) {\n        const dest = path.join(TEMP_DIR, asset.name)\n        resourceMapping.push(dest)\n        downloadTasks.push(\n          retry(() => downloadFile(asset.browser_download_url, dest), {\n            maxAttempts: 5,\n          }),\n        )\n      }\n    }\n\n    try {\n      await ensureDir(TEMP_DIR)\n      await Promise.all(downloadTasks)\n    } catch (error) {\n      consola.error(error)\n      throw new Error('Error during download tasks')\n    }\n\n    for (const item of resourceMapping) {\n      consola.log(\n        `found ${item}, size: ${getFileSize(item)}`,\n        await exists(item),\n      )\n    }\n\n    const buildType = nightlyBuild ? 'nightly' : 'release'\n    const folderPath = `${buildType}/${GIT_SHORT_HASH}`\n\n    consola.start(\n      `Uploading ${resourceMapping.length} files to file server (concurrency: ${UPLOAD_CONCURRENCY}, folder: ${folderPath})...`,\n    )\n\n    uploadResults = await uploadAllFiles(\n      resourceMapping,\n      FILE_SERVER_TOKEN,\n      folderPath,\n    )\n\n    consola.success(`Uploaded ${uploadResults.length} files to file server`)\n  }\n\n  // build message with grouped download links\n  const lines: string[] = []\n\n  if (!nightlyBuild) {\n    lines.push(\n      `**Clash Nyanpasu ${version} Released!**`,\n      '',\n      'GitHub Release:',\n      `https://github.com/libnyanpasu/clash-nyanpasu/releases/tag/v${version}`,\n    )\n  } else {\n    lines.push(\n      `**Clash Nyanpasu Nightly Build \\`${GIT_SHORT_HASH}\\`**`,\n      '',\n      '⚠️ Could be unstable, use at your own risk.',\n    )\n    if (WORKFLOW_RUN_ID) {\n      lines.push(\n        '',\n        'GitHub Actions:',\n        `https://github.com/libnyanpasu/clash-nyanpasu/actions/runs/${WORKFLOW_RUN_ID}`,\n      )\n    }\n  }\n\n  lines.push('', '--- Download Links ---')\n\n  for (const group of platformGroups) {\n    const groupFiles = uploadResults.filter((r) => group.filter(r.fileName))\n    if (groupFiles.length === 0) continue\n\n    lines.push('', `**${group.label}:**`)\n    for (const file of groupFiles) {\n      lines.push(`- [${file.fileName}](${file.downloadUrl})`)\n    }\n  }\n\n  const messageText = lines.join('\\n')\n  const chatId = nightlyBuild ? TELEGRAM_TO_NIGHTLY : TELEGRAM_TO\n\n  await bot.api.sendMessage(chatId, messageText, { parse_mode: 'Markdown' })\n  consola.success('Sent telegram notification')\n}\n\nmain().catch((error) => {\n  consola.fatal(error)\n  Deno.exit(1)\n})\n"
  },
  {
    "path": "scripts/deno/upload-build-artifacts.ts",
    "content": "import * as path from 'jsr:@std/path'\nimport { globby } from 'npm:globby'\nimport { uploadAllFiles } from './utils/file-server.ts'\nimport { consola } from './utils/logger.ts'\n\nfunction requireEnv(name: string): string {\n  const value = Deno.env.get(name)\n  if (!value) {\n    consola.fatal(`${name} is required`)\n    Deno.exit(1)\n  }\n  return value\n}\n\nconst FILE_SERVER_TOKEN = requireEnv('FILE_SERVER_TOKEN')\nconst FOLDER_PATH = requireEnv('FOLDER_PATH')\n\nconst patterns = Deno.args\nif (patterns.length === 0) {\n  consola.fatal('No file patterns provided as arguments')\n  Deno.exit(1)\n}\n\nconst WORKSPACE_ROOT = path.join(import.meta.dirname!, '../..')\n\nconsola.info(`Searching for files matching: ${patterns.join(', ')}`)\nconst files = await globby(patterns, { cwd: WORKSPACE_ROOT, absolute: true })\n\nconsola.info(`Found ${files.length} files:`)\nfor (const f of files) {\n  consola.info(`  ${path.basename(f)}`)\n}\n\nconst results =\n  files.length > 0\n    ? await uploadAllFiles(files, FILE_SERVER_TOKEN, FOLDER_PATH)\n    : []\n\nconst outputPath = path.join(WORKSPACE_ROOT, 'upload-results.json')\nawait Deno.writeTextFile(outputPath, JSON.stringify(results, null, 2))\nconsola.success(\n  `Upload complete. ${results.length} files uploaded. Results written to ${outputPath}`,\n)\n"
  },
  {
    "path": "scripts/deno/upload-macos-updater.ts",
    "content": "import * as path from 'jsr:@std/path'\nimport { globby } from 'npm:globby'\nimport { consola } from './utils/logger.ts'\n\nconst WORKSPACE_ROOT = path.join(import.meta.dirname!, '../..')\nconsola.info(`WORKSPACE_ROOT: ${WORKSPACE_ROOT}`)\n\nconst TARGET_ARCH = Deno.env.get('TARGET_ARCH') || Deno.build.arch\n\nconst BACKEND_BUILD_DIR = path.join(WORKSPACE_ROOT, 'backend/target')\n\nconst files = await globby(['**/*.tar.gz', '**/*.sig', '**/*.dmg'], {\n  cwd: BACKEND_BUILD_DIR,\n})\n\nfor (let file of files) {\n  file = path.join(BACKEND_BUILD_DIR, file)\n  const p = path.parse(file)\n  consola.info(`Found file: ${p.base}`)\n  if (p.base.includes('.app')) {\n    const components = p.base.split('.')\n    const newName =\n      components[0] + `.${TARGET_ARCH}.${components.slice(1).join('.')}`\n    const newPath = path.join(p.dir, newName)\n    consola.info(`Renaming ${file} to ${newPath}`)\n    await Deno.rename(file, newPath)\n  }\n}\n\nconsola.success('Files renamed successfully')\n"
  },
  {
    "path": "scripts/deno/utils/cache-client.ts",
    "content": "import { format as formatBytes } from 'jsr:@std/fmt@1/bytes'\nimport { writeAll } from 'jsr:@std/io@0.225/write-all'\nimport { CHUNK_MULTIPLIER, performChunkedUpload } from './file-server.ts'\nimport { consola } from './logger.ts'\n\nconst CACHE_BASE_URL = 'https://file-server.elaina.moe/cache'\n\n// --- cache chunked upload types ---\n\ninterface CacheInitResponse {\n  uploadId: string\n  key: string\n  fileSize: number\n  chunkSize: number\n  expiresAt: number\n}\n\ninterface CacheChunkResponse {\n  done: boolean\n  nextExpectedRanges?: string[]\n  key?: string\n  size?: number\n}\n\n// --- cache chunked upload functions ---\n\nasync function initCacheUploadSession(\n  key: string,\n  fileSize: number,\n  token: string,\n): Promise<CacheInitResponse> {\n  const resp = await fetch(`${CACHE_BASE_URL}/init`, {\n    method: 'POST',\n    headers: {\n      Authorization: `Bearer ${token}`,\n      'Content-Type': 'application/json',\n    },\n    body: JSON.stringify({\n      key,\n      fileSize,\n      chunkMultiplier: CHUNK_MULTIPLIER,\n    }),\n  })\n\n  if (!resp.ok) {\n    const body = await resp.text()\n    throw new Error(\n      `cache upload init failed: ${resp.status} ${resp.statusText} - ${body}`,\n    )\n  }\n\n  return (await resp.json()) as CacheInitResponse\n}\n\nasync function uploadCacheChunk(\n  uploadId: string,\n  chunk: Uint8Array,\n  start: number,\n  end: number,\n  total: number,\n  token: string,\n): Promise<CacheChunkResponse> {\n  const resp = await fetch(`${CACHE_BASE_URL}/chunk`, {\n    method: 'POST',\n    headers: {\n      Authorization: `Bearer ${token}`,\n      'x-upload-id': uploadId,\n      'Content-Range': `bytes ${start}-${end}/${total}`,\n      'Content-Type': 'application/octet-stream',\n    },\n    body: chunk as unknown as BodyInit,\n  })\n\n  if (!resp.ok) {\n    const body = await resp.text()\n    throw new Error(\n      `cache chunk upload failed: ${resp.status} ${resp.statusText} - ${body}`,\n    )\n  }\n\n  return (await resp.json()) as CacheChunkResponse\n}\n\nexport async function uploadCache(\n  key: string,\n  filePath: string,\n  token: string,\n): Promise<void> {\n  const stat = await Deno.stat(filePath)\n  const fileSize = stat.size\n\n  consola.info(\n    `uploading cache \"${key}\" (${formatBytes(fileSize)}) via chunked upload...`,\n  )\n\n  const { uploadId, chunkSize } = await initCacheUploadSession(\n    key,\n    fileSize,\n    token,\n  )\n\n  await performChunkedUpload({\n    filePath,\n    fileSize,\n    uploadId,\n    chunkSize,\n    label: `cache \"${key}\"`,\n    uploadChunkFn: (chunk, start, end, total) =>\n      uploadCacheChunk(uploadId, chunk, start, end, total, token),\n  })\n\n  consola.success(`cache \"${key}\" uploaded successfully`)\n}\n\nexport async function downloadCache(\n  key: string,\n  destPath: string,\n  token: string,\n): Promise<boolean> {\n  consola.info(`downloading cache \"${key}\"...`)\n\n  const resp = await fetch(`${CACHE_BASE_URL}/${encodeURIComponent(key)}`, {\n    method: 'GET',\n    headers: {\n      'x-authorization': token,\n    },\n  })\n\n  if (resp.status === 404) {\n    consola.info(`cache miss for \"${key}\"`)\n    return false\n  }\n\n  if (!resp.ok) {\n    const body = await resp.text()\n    throw new Error(\n      `cache download failed: ${resp.status} ${resp.statusText} - ${body}`,\n    )\n  }\n\n  const totalSize = Number(resp.headers.get('Content-Length')) || 0\n  const reader = resp.body!.getReader()\n  const dest = await Deno.open(destPath, {\n    write: true,\n    create: true,\n    truncate: true,\n  })\n\n  try {\n    let received = 0\n    let lastLogTime = Date.now()\n    let lastLogReceived = 0\n\n    while (true) {\n      const { done, value } = await reader.read()\n      if (done) break\n      await writeAll(dest, value)\n      received += value.byteLength\n\n      const now = Date.now()\n      const elapsed = now - lastLogTime\n      if (elapsed >= 1000) {\n        const speed = ((received - lastLogReceived) / elapsed) * 1000\n        lastLogTime = now\n        lastLogReceived = received\n        if (totalSize > 0) {\n          const pct = Math.floor((received / totalSize) * 100)\n          consola.info(\n            `  cache \"${key}\": ${formatBytes(received)}/${formatBytes(totalSize)} (${pct}%) ${formatBytes(speed)}/s`,\n          )\n        } else {\n          consola.info(\n            `  cache \"${key}\": ${formatBytes(received)} downloaded ${formatBytes(speed)}/s`,\n          )\n        }\n      }\n    }\n  } catch {\n    try {\n      dest.close()\n    } catch {\n      // already closed\n    }\n    throw new Error(`failed to write cache to \"${destPath}\"`)\n  }\n\n  dest.close()\n  consola.success(`cache \"${key}\" downloaded to \"${destPath}\"`)\n  return true\n}\n\nexport async function listCacheKeys(\n  prefix: string,\n  token: string,\n): Promise<string[]> {\n  consola.debug(`listing cache keys with prefix \"${prefix}\"...`)\n\n  const resp = await fetch(\n    `${CACHE_BASE_URL}?prefix=${encodeURIComponent(prefix)}`,\n    {\n      method: 'GET',\n      headers: {\n        'x-authorization': token,\n      },\n    },\n  )\n\n  if (!resp.ok) {\n    const body = await resp.text()\n    throw new Error(\n      `cache list failed: ${resp.status} ${resp.statusText} - ${body}`,\n    )\n  }\n\n  const keys = (await resp.json()) as string[]\n  consola.debug(`found ${keys.length} cache keys matching prefix \"${prefix}\"`)\n  return keys\n}\n"
  },
  {
    "path": "scripts/deno/utils/file-server.ts",
    "content": "import { retry } from 'jsr:@std/async@1/retry'\nimport { format as formatBytes } from 'jsr:@std/fmt@1/bytes'\nimport * as path from 'jsr:@std/path'\nimport { consola } from './logger.ts'\n\n// --- constants ---\n\nexport const FILE_SERVER_UPLOAD_URL = 'https://file-server.elaina.moe/upload'\nexport const FILE_SERVER_BIN_URL = 'https://file-server.elaina.moe/bin'\n\nexport const UPLOAD_CONCURRENCY = 3\nexport const CHUNK_RETRY_ATTEMPTS = 5\nexport const CHUNK_MULTIPLIER = 32\n\n// --- types ---\n\nexport interface UploadResult {\n  fileName: string\n  downloadUrl: string\n}\n\nexport interface InitResponse {\n  uploadId: string\n  chunkSize: number\n}\n\nexport interface ChunkResponse {\n  done: boolean\n  file?: { id: string }\n}\n\nexport interface ChunkedUploadOptions<T> {\n  filePath: string\n  fileSize: number\n  uploadId: string\n  chunkSize: number\n  label: string\n  uploadChunkFn: (\n    chunk: Uint8Array,\n    start: number,\n    end: number,\n    total: number,\n  ) => Promise<T & { done: boolean }>\n}\n\n// --- upload functions ---\n\nexport async function initUploadSession(\n  fileName: string,\n  fileSize: number,\n  mimeType: string | null,\n  token: string,\n  folderPath?: string,\n): Promise<InitResponse> {\n  const body: Record<string, unknown> = {\n    filename: fileName,\n    fileSize,\n    mimeType,\n    chunkMultiplier: CHUNK_MULTIPLIER,\n  }\n  if (folderPath) {\n    body.folderPath = folderPath\n  }\n\n  const resp = await fetch(`${FILE_SERVER_UPLOAD_URL}/init`, {\n    method: 'POST',\n    headers: {\n      'x-authorization': token,\n      'Content-Type': 'application/json',\n    },\n    body: JSON.stringify(body),\n  })\n\n  if (!resp.ok) {\n    const body = await resp.text()\n    throw new Error(\n      `upload init failed: ${resp.status} ${resp.statusText} - ${body}`,\n    )\n  }\n\n  return (await resp.json()) as InitResponse\n}\n\nexport async function uploadChunk(\n  uploadId: string,\n  chunk: Uint8Array,\n  start: number,\n  end: number,\n  total: number,\n  token: string,\n): Promise<ChunkResponse> {\n  const resp = await fetch(`${FILE_SERVER_UPLOAD_URL}/chunk`, {\n    method: 'POST',\n    headers: {\n      'x-authorization': token,\n      'x-upload-id': uploadId,\n      'Content-Range': `bytes ${start}-${end}/${total}`,\n      'Content-Type': 'application/octet-stream',\n    },\n    body: chunk as unknown as BodyInit,\n  })\n\n  if (!resp.ok) {\n    const body = await resp.text()\n    throw new Error(\n      `chunk upload failed: ${resp.status} ${resp.statusText} - ${body}`,\n    )\n  }\n\n  return (await resp.json()) as ChunkResponse\n}\n\nexport async function performChunkedUpload<T>(\n  options: ChunkedUploadOptions<T>,\n): Promise<T & { done: boolean }> {\n  const { filePath, fileSize, uploadId, chunkSize, label, uploadChunkFn } =\n    options\n\n  consola.debug(\n    `upload session created: uploadId=${uploadId}, chunkSize=${formatBytes(chunkSize)}`,\n  )\n\n  const file = await Deno.open(filePath, { read: true })\n  try {\n    let start = 0\n    let chunkIndex = 0\n    const totalChunks = Math.ceil(fileSize / chunkSize)\n    let lastLogTime = Date.now()\n    let lastLogUploaded = 0\n\n    while (start < fileSize) {\n      const endExclusive = Math.min(start + chunkSize, fileSize)\n      const size = endExclusive - start\n      const buf = new Uint8Array(size)\n      await file.seek(start, Deno.SeekMode.Start)\n      let bytesRead = 0\n      while (bytesRead < size) {\n        const n = await file.read(buf.subarray(bytesRead))\n        if (n === null) break\n        bytesRead += n\n      }\n\n      const end = endExclusive - 1\n      chunkIndex++\n\n      const data = await retry(\n        () => uploadChunkFn(buf.subarray(0, bytesRead), start, end, fileSize),\n        { maxAttempts: CHUNK_RETRY_ATTEMPTS },\n      )\n\n      const now = Date.now()\n      const elapsed = now - lastLogTime\n      if (elapsed >= 1000 || data.done) {\n        const speed = ((endExclusive - lastLogUploaded) / elapsed) * 1000\n        lastLogTime = now\n        lastLogUploaded = endExclusive\n        const pct = Math.floor((endExclusive / fileSize) * 100)\n        consola.info(\n          `  ${label} ${chunkIndex}/${totalChunks}: ${formatBytes(endExclusive)}/${formatBytes(fileSize)} (${pct}%) ${formatBytes(speed)}/s`,\n        )\n      }\n\n      if (data.done) {\n        return data\n      }\n\n      start = endExclusive\n    }\n  } finally {\n    file.close()\n  }\n\n  throw new Error(`Upload of ${label} ended unexpectedly without done=true`)\n}\n\nexport async function uploadToFileServer(\n  filePath: string,\n  token: string,\n  folderPath?: string,\n): Promise<UploadResult> {\n  const fileName = path.basename(filePath)\n  const stat = await Deno.stat(filePath)\n  const fileSize = stat.size\n\n  consola.info(\n    `uploading ${fileName} (${formatBytes(fileSize)}) to file server${folderPath ? ` [folder: ${folderPath}]` : ''}...`,\n  )\n\n  const { uploadId, chunkSize } = await initUploadSession(\n    fileName,\n    fileSize,\n    null,\n    token,\n    folderPath,\n  )\n\n  const data = await performChunkedUpload({\n    filePath,\n    fileSize,\n    uploadId,\n    chunkSize,\n    label: fileName,\n    uploadChunkFn: (chunk, start, end, total) =>\n      uploadChunk(uploadId, chunk, start, end, total, token),\n  })\n\n  const downloadUrl = `${FILE_SERVER_BIN_URL}/${data.file!.id}`\n  consola.success(`uploaded ${fileName} -> ${downloadUrl}`)\n  return { fileName, downloadUrl }\n}\n\nexport async function uploadAllFiles(\n  filePaths: string[],\n  token: string,\n  folderPath?: string,\n): Promise<UploadResult[]> {\n  const results: UploadResult[] = []\n  const queue = [...filePaths]\n  const inFlight: Promise<void>[] = []\n\n  async function processNext(): Promise<void> {\n    while (queue.length > 0) {\n      const filePath = queue.shift()!\n      const result = await retry(\n        () => uploadToFileServer(filePath, token, folderPath),\n        {\n          maxAttempts: CHUNK_RETRY_ATTEMPTS,\n        },\n      )\n      results.push(result)\n    }\n  }\n\n  const workers = Math.min(UPLOAD_CONCURRENCY, filePaths.length)\n  for (let i = 0; i < workers; i++) {\n    inFlight.push(processNext())\n  }\n  await Promise.all(inFlight)\n\n  return results\n}\n"
  },
  {
    "path": "scripts/deno/utils/logger.ts",
    "content": "import { createColorize } from 'npm:colorize-template'\nimport { createConsola } from 'npm:consola'\nimport pc from 'npm:picocolors'\n\nconst logLevelStr = Deno.env.get('LOG_LEVEL')\n\nexport const consola = createConsola({\n  level: logLevelStr ? Number.parseInt(logLevelStr) : 5,\n  fancy: true,\n  formatOptions: {\n    colors: true,\n    compact: false,\n    date: true,\n  },\n})\n\nexport const colorize = createColorize({\n  ...pc,\n  success: pc.green,\n  error: pc.red,\n})\n"
  },
  {
    "path": "scripts/generate-git-info.ts",
    "content": "import { execSync } from 'node:child_process'\nimport fs from 'fs-extra'\nimport { GIT_SUMMARY_INFO_PATH, TAURI_APP_TEMP_DIR } from './utils/env'\nimport { consola } from './utils/logger'\n\nasync function main() {\n  const [hash, author, time] = execSync(\n    \"git show --pretty=format:'%H,%cn,%cI' --no-patch --no-notes\",\n    {\n      cwd: process.cwd(),\n    },\n  )\n    .toString()\n    .replace(/'/g, '')\n    .split(',')\n\n  const summary = {\n    hash,\n    author,\n    time,\n  }\n  consola.info(summary)\n  if (!(await fs.exists(TAURI_APP_TEMP_DIR))) {\n    await fs.mkdir(TAURI_APP_TEMP_DIR)\n  }\n\n  await fs.writeJSON(GIT_SUMMARY_INFO_PATH, summary, { spaces: 2 })\n  consola.success('Git summary info generated')\n}\n\nmain().catch(consola.error)\n"
  },
  {
    "path": "scripts/generate-latest-version.ts",
    "content": "import fs from 'fs-extra'\nimport { ManifestVersion, SupportedCore } from './types/index'\nimport { MANIFEST_DIR, MANIFEST_VERSION_PATH } from './utils/env'\nimport { consola } from './utils/logger'\nimport {\n  resolveClashPremium,\n  resolveClashRs,\n  resolveClashRsAlpha,\n  resolveMihomo,\n  resolveMihomoAlpha,\n} from './utils/manifest'\n\nconst MANIFEST_VERSION = 1\n\nexport async function generateLatestVersion() {\n  const resolvers = [\n    resolveMihomo,\n    resolveMihomoAlpha,\n    resolveClashRs,\n    resolveClashPremium,\n    resolveClashRsAlpha,\n  ]\n\n  consola.start('Resolving latest versions')\n\n  const results = await Promise.all(resolvers.map((r) => r()))\n\n  consola.success('Resolved latest versions')\n\n  consola.start('Generating manifest')\n\n  const manifest: ManifestVersion = {\n    manifest_version: MANIFEST_VERSION,\n    latest: {},\n    arch_template: {},\n  } as ManifestVersion\n\n  for (const result of results) {\n    manifest.latest[result.name as SupportedCore] = result.version\n    manifest.arch_template[result.name as SupportedCore] = result.archMapping\n  }\n\n  await fs.ensureDir(MANIFEST_DIR)\n  // If no changes, skip writing manifest\n  const previousManifest = (await fs.readJSON(MANIFEST_VERSION_PATH)) || {}\n\n  delete previousManifest.updated_at\n\n  if (JSON.stringify(previousManifest) === JSON.stringify(manifest)) {\n    consola.success('No changes, skip writing manifest')\n    return\n  }\n\n  manifest.updated_at = new Date().toISOString()\n\n  consola.success('Generated manifest')\n\n  await fs.writeJSON(MANIFEST_VERSION_PATH, manifest, { spaces: 2 })\n\n  consola.success('Manifest written')\n}\n\ngenerateLatestVersion()\n"
  },
  {
    "path": "scripts/manifest/clash-meta.ts",
    "content": "import { ClashManifest } from 'types'\nimport versionManifest from '../../manifest/version.json'\n\nexport const CLASH_META_MANIFEST: ClashManifest = {\n  URL_PREFIX: `https://github.com/MetaCubeX/mihomo/releases/download/${versionManifest.latest.mihomo}`,\n  VERSION: versionManifest.latest.mihomo,\n  ARCH_MAPPING: versionManifest.arch_template.mihomo,\n}\n\nexport const CLASH_META_ALPHA_MANIFEST: ClashManifest = {\n  VERSION_URL:\n    'https://github.com/MetaCubeX/mihomo/releases/download/Prerelease-Alpha/version.txt',\n  URL_PREFIX:\n    'https://github.com/MetaCubeX/mihomo/releases/download/Prerelease-Alpha',\n  VERSION: versionManifest.latest.mihomo_alpha,\n  ARCH_MAPPING: versionManifest.arch_template.mihomo_alpha,\n}\n"
  },
  {
    "path": "scripts/manifest/clash-premium.ts",
    "content": "import { ClashManifest } from 'types'\nimport versionManifest from '../../manifest/version.json'\n\nexport const CLASH_MANIFEST: ClashManifest = {\n  URL_PREFIX: 'https://github.com/Dreamacro/clash/releases/download/premium/',\n  LATEST_DATE: '2023.08.17',\n  STORAGE_PREFIX: 'https://release.dreamacro.workers.dev/',\n  BACKUP_URL_PREFIX:\n    'https://github.com/zhongfly/Clash-premium-backup/releases/download/',\n  BACKUP_LATEST_DATE: versionManifest.latest.clash_premium,\n  VERSION: versionManifest.latest.clash_premium,\n  ARCH_MAPPING: versionManifest.arch_template.clash_premium,\n}\n"
  },
  {
    "path": "scripts/manifest/clash-rs.ts",
    "content": "import { ClashManifest } from 'types'\nimport versionManifest from '../../manifest/version.json'\n\nexport const CLASH_RS_MANIFEST: ClashManifest = {\n  URL_PREFIX: 'https://github.com/Watfaq/clash-rs/releases/download/',\n  VERSION: versionManifest.latest.clash_rs,\n  ARCH_MAPPING: versionManifest.arch_template.clash_rs,\n}\n\nexport const CLASH_RS_ALPHA_MANIFEST: ClashManifest = {\n  VERSION_URL:\n    'https://github.com/Watfaq/clash-rs/releases/download/latest/version.txt',\n  URL_PREFIX: 'https://github.com/Watfaq/clash-rs/releases/download/latest',\n  VERSION: versionManifest.latest.clash_rs_alpha,\n  ARCH_MAPPING: versionManifest.arch_template.clash_rs_alpha,\n}\n"
  },
  {
    "path": "scripts/manifest/index.ts",
    "content": "import { CLASH_META_ALPHA_MANIFEST, CLASH_META_MANIFEST } from './clash-meta'\nimport { CLASH_MANIFEST } from './clash-premium'\nimport { CLASH_RS_MANIFEST } from './clash-rs'\n\nexport const clashManifest = {\n  premium: CLASH_MANIFEST,\n  meta: CLASH_META_MANIFEST,\n  metaAlpha: CLASH_META_ALPHA_MANIFEST,\n  rs: CLASH_RS_MANIFEST,\n}\n"
  },
  {
    "path": "scripts/osx-aarch64-upload.ts",
    "content": "/**\n * Build and upload assets\n * for macOS(aarch)\n */\nimport path from 'node:path'\nimport fs from 'fs-extra'\nimport { context, getOctokit } from '@actions/github'\nimport pkgJson from '../package.json'\nimport { consola } from './utils/logger'\n\nasync function resolve() {\n  if (!process.env.GITHUB_TOKEN) {\n    throw new Error('GITHUB_TOKEN is required')\n  }\n  if (!process.env.TAURI_SIGNING_PRIVATE_KEY) {\n    throw new Error('TAURI_SIGNING_PRIVATE_KEY is required')\n  }\n  if (!process.env.TAURI_SIGNING_PRIVATE_KEY_PASSWORD) {\n    throw new Error('TAURI_SIGNING_PRIVATE_KEY_PASSWORD is required')\n  }\n\n  const { version } = pkgJson\n\n  const tag = process.env.TAG_NAME || `v${version}`\n\n  consola.info(`Upload to tag ${tag}`)\n\n  const cwd = process.cwd()\n  const bundlePath = path.join(\n    'backend/target/aarch64-apple-darwin/release/bundle',\n  )\n  const join = (p: string) => path.join(bundlePath, p)\n\n  const appPathList = [\n    join('macos/Clash Nyanpasu.aarch64.app.tar.gz'),\n    join('macos/Clash Nyanpasu.aarch64.app.tar.gz.sig'),\n  ]\n\n  for (const appPath of appPathList) {\n    if (fs.pathExistsSync(appPath)) {\n      fs.removeSync(appPath)\n    }\n  }\n\n  fs.copyFileSync(join('macos/Clash Nyanpasu.app.tar.gz'), appPathList[0])\n  fs.copyFileSync(join('macos/Clash Nyanpasu.app.tar.gz.sig'), appPathList[1])\n\n  const options = { owner: context.repo.owner, repo: context.repo.repo }\n  const github = getOctokit(process.env.GITHUB_TOKEN)\n\n  const { data: release } = await github.rest.repos.getReleaseByTag({\n    ...options,\n    tag,\n  })\n\n  if (!release.id) throw new Error('failed to find the release')\n\n  await uploadAssets(release.id, [\n    join(`dmg/Clash Nyanpasu_${version}_aarch64.dmg`),\n    ...appPathList,\n  ])\n}\n\n// From tauri-apps/tauri-action\n// https://github.com/tauri-apps/tauri-action/blob/dev/packages/action/src/upload-release-assets.ts\nasync function uploadAssets(releaseId: number, assets: string[]) {\n  const GITHUB_TOKEN = process.env.GITHUB_TOKEN\n  if (!GITHUB_TOKEN) {\n    throw new Error('GITHUB_TOKEN is required')\n  }\n  const github = getOctokit(GITHUB_TOKEN)\n\n  // Determine content-length for header to upload asset\n  const contentLength = (filePath: string) => fs.statSync(filePath).size\n\n  for (const assetPath of assets) {\n    const headers = {\n      'content-type': 'application/zip',\n      'content-length': contentLength(assetPath),\n    }\n\n    const ext = path.extname(assetPath)\n    const filename = path.basename(assetPath).replace(ext, '')\n    const assetName = path.dirname(assetPath).includes(`target${path.sep}debug`)\n      ? `${filename}-debug${ext}`\n      : `${filename}${ext}`\n\n    consola.start(`Uploading ${assetName}...`)\n\n    try {\n      await github.rest.repos.uploadReleaseAsset({\n        headers,\n        name: assetName,\n        // https://github.com/tauri-apps/tauri-action/pull/45\n        // @ts-expect-error error TS2322: Type 'Buffer' is not assignable to type 'string'.\n        data: fs.readFileSync(assetPath),\n        owner: context.repo.owner,\n        repo: context.repo.repo,\n        release_id: releaseId,\n      })\n      consola.success(`Uploaded ${assetName}`)\n    } catch (error) {\n      throw new Error(\n        `Failed to upload release asset: ${error instanceof Error ? error.message : error}`,\n      )\n    }\n  }\n}\n\nresolve()\n"
  },
  {
    "path": "scripts/package.json",
    "content": "{\n  \"name\": \"@nyanpasu/scripts\",\n  \"type\": \"module\",\n  \"version\": \"2.0.0\",\n  \"dependencies\": {\n    \"@actions/github\": \"6.0.1\",\n    \"@types/figlet\": \"1.7.0\",\n    \"@types/semver\": \"7.7.1\",\n    \"figlet\": \"1.11.0\",\n    \"filesize\": \"11.0.13\",\n    \"p-retry\": \"7.1.1\",\n    \"semver\": \"7.7.4\",\n    \"zod\": \"4.3.6\"\n  },\n  \"devDependencies\": {\n    \"@octokit/types\": \"16.0.0\",\n    \"@types/adm-zip\": \"0.5.7\",\n    \"@types/yargs\": \"17.0.35\",\n    \"adm-zip\": \"0.5.16\",\n    \"colorize-template\": \"1.0.0\",\n    \"consola\": \"3.4.2\",\n    \"fs-extra\": \"11.3.4\",\n    \"octokit\": \"5.0.5\",\n    \"picocolors\": \"1.1.1\",\n    \"tar\": \"7.5.12\",\n    \"telegram\": \"2.26.22\",\n    \"undici\": \"7.24.5\",\n    \"yargs\": \"18.0.0\"\n  }\n}\n"
  },
  {
    "path": "scripts/portable.ts",
    "content": "import path from 'node:path'\nimport AdmZip from 'adm-zip'\nimport fs from 'fs-extra'\nimport { context, getOctokit } from '@actions/github'\nimport packageJson from '../package.json'\nimport { TAURI_APP_DIR } from './utils/env'\nimport { colorize, consola } from './utils/logger'\n\nconst RUST_ARCH = process.env.RUST_ARCH || 'x86_64'\nconst fixedWebview = process.argv.includes('--fixed-webview')\n\n/// Script for ci\n/// 打包绿色版/便携版 (only Windows)\nasync function resolvePortable() {\n  if (process.platform !== 'win32') return\n\n  const buildDir = path.join(\n    RUST_ARCH === 'x86_64'\n      ? 'backend/target/release'\n      : `backend/target/${RUST_ARCH}-pc-windows-msvc/release`,\n  )\n\n  const configDir = path.join(buildDir, '.config')\n\n  if (!(await fs.pathExists(buildDir))) {\n    throw new Error('could not found the release dir')\n  }\n\n  await fs.ensureDir(configDir)\n  await fs.createFile(path.join(configDir, 'PORTABLE'))\n\n  const zip = new AdmZip()\n  let mainEntryPath = path.join(buildDir, 'Clash Nyanpasu.exe')\n  if (!(await fs.pathExists(mainEntryPath))) {\n    mainEntryPath = path.join(buildDir, 'clash-nyanpasu.exe')\n  }\n  zip.addLocalFile(mainEntryPath)\n  zip.addLocalFile(path.join(buildDir, 'clash.exe'))\n  zip.addLocalFile(path.join(buildDir, 'mihomo.exe'))\n  zip.addLocalFile(path.join(buildDir, 'mihomo-alpha.exe'))\n  zip.addLocalFile(path.join(buildDir, 'nyanpasu-service.exe'))\n  zip.addLocalFile(path.join(buildDir, 'clash-rs.exe'))\n  zip.addLocalFile(path.join(buildDir, 'clash-rs-alpha.exe'))\n  zip.addLocalFolder(path.join(buildDir, 'resources'), 'resources')\n\n  if (fixedWebview) {\n    const webviewPath = (await fs.readdir(TAURI_APP_DIR)).find((file) =>\n      file.includes('WebView2'),\n    )\n    if (!webviewPath) {\n      throw new Error('WebView2 runtime not found')\n    }\n    zip.addLocalFolder(\n      path.join(TAURI_APP_DIR, webviewPath),\n      path.basename(webviewPath),\n    )\n  }\n\n  zip.addLocalFolder(configDir, '.config')\n\n  const { version } = packageJson\n\n  const zipFile = `Clash.Nyanpasu_${version}_${RUST_ARCH}${fixedWebview ? '_fixed-webview' : ''}_portable.zip`\n  zip.writeZip(zipFile)\n\n  consola.success('create portable zip successfully')\n\n  // push release assets\n  if (process.env.GITHUB_TOKEN === undefined) {\n    throw new Error('GITHUB_TOKEN is required')\n  }\n\n  const options = { owner: context.repo.owner, repo: context.repo.repo }\n  const github = getOctokit(process.env.GITHUB_TOKEN)\n\n  consola.info('upload to ', process.env.TAG_NAME || `v${version}`)\n\n  const { data: release } = await github.rest.repos.getReleaseByTag({\n    ...options,\n    tag: process.env.TAG_NAME || `v${version}`,\n  })\n\n  consola.debug(colorize`releaseName: {green ${release.name}}`)\n\n  await github.rest.repos.uploadReleaseAsset({\n    ...options,\n    release_id: release.id,\n    name: zipFile,\n    // @ts-expect-error data is Buffer should work fine\n    data: zip.toBuffer(),\n  })\n}\n\nresolvePortable().catch((err) => {\n  consola.error(err)\n  process.exit(1)\n})\n"
  },
  {
    "path": "scripts/prepare-nightly.ts",
    "content": "import { execSync } from 'child_process'\nimport path from 'node:path'\nimport fs from 'fs-extra'\nimport { merge } from 'lodash-es'\nimport {\n  cwd,\n  TAURI_APP_DIR,\n  TAURI_FIXED_WEBVIEW2_CONFIG_OVERRIDE_PATH,\n} from './utils/env'\nimport { consola } from './utils/logger'\n\nconst TAURI_DEV_APP_CONF_PATH = path.join(\n  TAURI_APP_DIR,\n  'tauri.nightly.conf.json',\n)\nconst TAURI_APP_CONF = path.join(TAURI_APP_DIR, 'tauri.conf.json')\nconst TAURI_DEV_APP_OVERRIDES_PATH = path.join(\n  TAURI_APP_DIR,\n  'overrides/nightly.conf.json',\n)\nconst ROOT_PACKAGE_JSON_PATH = path.join(cwd, 'package.json')\nconst NYANPASU_PACKAGE_JSON_PATH = path.join(\n  cwd,\n  'frontend/nyanpasu/package.json',\n)\n// blocked by https://github.com/tauri-apps/tauri/issues/8447\n// const WXS_PATH = path.join(TAURI_APP_DIR, \"templates\", \"nightly.wxs\");\n\nconst isNSIS = process.argv.includes('--nsis') // only build nsis\nconst isMSI = process.argv.includes('--msi') // only build msi\nconst fixedWebview = process.argv.includes('--fixed-webview')\nconst disableUpdater = process.argv.includes('--disable-updater')\n\nasync function main() {\n  consola.debug('Read config...')\n  const tauriAppConf = await fs.readJSON(TAURI_APP_CONF)\n  const tauriAppOverrides = await fs.readJSON(TAURI_DEV_APP_OVERRIDES_PATH)\n  let tauriConf = merge(tauriAppConf, tauriAppOverrides)\n  const packageJson = await fs.readJSON(NYANPASU_PACKAGE_JSON_PATH)\n  const rootPackageJson = await fs.readJSON(ROOT_PACKAGE_JSON_PATH)\n  // const wxsFile = await fs.readFile(WXS_PATH, \"utf-8\");\n  if (fixedWebview) {\n    const fixedWebview2Config = await fs.readJSON(\n      TAURI_FIXED_WEBVIEW2_CONFIG_OVERRIDE_PATH,\n    )\n    const webviewPath = (await fs.readdir(TAURI_APP_DIR)).find((file) =>\n      file.includes('WebView2'),\n    )\n    if (!webviewPath) {\n      throw new Error('WebView2 runtime not found')\n    }\n    tauriConf = merge(tauriConf, fixedWebview2Config)\n    delete tauriConf.bundle.windows.webviewInstallMode.silent\n    tauriConf.bundle.windows.webviewInstallMode.path = `./${path.basename(webviewPath)}`\n    tauriConf.plugins.updater.endpoints =\n      tauriConf.plugins.updater.endpoints.map((o: string) =>\n        o.replace('update-', 'update-nightly-'),\n      )\n  }\n\n  if (isNSIS) {\n    tauriConf.bundle.targets = ['nsis']\n  }\n\n  if (disableUpdater) {\n    tauriConf.bundle.createUpdaterArtifacts = false\n  }\n\n  consola.debug('Get current git short hash')\n  const GIT_SHORT_HASH = execSync('git rev-parse --short HEAD')\n    .toString()\n    .trim()\n  consola.debug(`Current git short hash: ${GIT_SHORT_HASH}`)\n\n  const version = `${tauriConf.version}-alpha+${GIT_SHORT_HASH}`\n  // blocked by https://github.com/tauri-apps/tauri/issues/8447\n  // 1. update wxs version\n  // consola.debug(\"Write raw version to wxs\");\n  // const modifiedWxsFile = wxsFile.replace(\n  //   \"{{version}}\",\n  //   tauriConf.package.version,\n  // );\n  // await fs.writeFile(WXS_PATH, modifiedWxsFile);\n  // consola.debug(\"wxs updated\");\n  // 2. update tauri version\n  consola.debug('Write tauri version to tauri.nightly.conf.json')\n  if (!isNSIS && !isMSI) tauriConf.version = version\n  await fs.writeJSON(TAURI_DEV_APP_CONF_PATH, tauriConf, { spaces: 2 })\n  consola.debug('tauri.nightly.conf.json updated')\n  // 3. update package version\n  consola.debug('Write tauri version to package.json')\n  packageJson.version = version\n  await fs.writeJSON(NYANPASU_PACKAGE_JSON_PATH, packageJson, { spaces: 2 })\n  rootPackageJson.version = version\n  await fs.writeJSON(ROOT_PACKAGE_JSON_PATH, rootPackageJson, { spaces: 2 })\n  consola.debug('package.json updated')\n}\n\nmain()\n"
  },
  {
    "path": "scripts/prepare-preview.ts",
    "content": "import path from 'path'\nimport fs from 'fs-extra'\nimport { TAURI_APP_DIR } from './utils/env'\nimport { consola } from './utils/logger'\n\nconst TAURI_APP_CONF = path.join(TAURI_APP_DIR, 'tauri.conf.json')\n\nconst TAURI_PREVIEW_APP_CONF_PATH = path.join(\n  TAURI_APP_DIR,\n  'tauri.preview.conf.json',\n)\n\nconst main = async () => {\n  consola.debug('Read config...')\n\n  const tauriAppConf = await fs.readJSON(TAURI_APP_CONF)\n\n  tauriAppConf.build.devPath = tauriAppConf.build.distDir\n  tauriAppConf.build.beforeDevCommand = tauriAppConf.build.beforeBuildCommand\n\n  consola.debug('Write config...')\n\n  await fs.writeJSON(TAURI_PREVIEW_APP_CONF_PATH, tauriAppConf, {\n    spaces: 2,\n  })\n}\n\nmain()\n"
  },
  {
    "path": "scripts/prepare-release.ts",
    "content": "import path from 'node:path'\nimport fs from 'fs-extra'\nimport { merge } from 'lodash-es'\nimport {\n  cwd,\n  TAURI_APP_DIR,\n  TAURI_FIXED_WEBVIEW2_CONFIG_OVERRIDE_PATH,\n} from './utils/env'\nimport { consola } from './utils/logger'\n\nconst TAURI_APP_CONF = path.join(TAURI_APP_DIR, 'tauri.conf.json')\n// TODO: define overrides\n// const TAURI_DEV_APP_OVERRIDES_PATH = path.join(\n//   TAURI_APP_DIR,\n//   \"overrides/nightly.conf.json\",\n// );\nconst PACKAGE_JSON_PATH = path.join(cwd, 'package.json')\n// blocked by https://github.com/tauri-apps/tauri/issues/8447\n// const WXS_PATH = path.join(TAURI_APP_DIR, \"templates\", \"nightly.wxs\");\n\nconst isNSIS = process.argv.includes('--nsis') // only build nsis\nconst fixedWebview = process.argv.includes('--fixed-webview')\n\nasync function main() {\n  consola.debug('Read config...')\n  const tauriAppConf = await fs.readJSON(TAURI_APP_CONF)\n  // const tauriAppOverrides = await fs.readJSON(TAURI_DEV_APP_OVERRIDES_PATH);\n  // const tauriConf = merge(tauriAppConf, tauriAppOverrides);\n  let tauriConf = tauriAppConf\n  // const wxsFile = await fs.readFile(WXS_PATH, \"utf-8\");\n\n  // if (isNSIS) {\n  //   tauriConf.tauri.bundle.targets = [\"nsis\", \"updater\"];\n  // }\n\n  if (fixedWebview) {\n    const fixedWebview2Config = await fs.readJSON(\n      TAURI_FIXED_WEBVIEW2_CONFIG_OVERRIDE_PATH,\n    )\n    const webviewPath = (await fs.readdir(TAURI_APP_DIR)).find((file) =>\n      file.includes('WebView2'),\n    )\n    if (!webviewPath) {\n      throw new Error('WebView2 runtime not found')\n    }\n    tauriConf = merge(tauriConf, fixedWebview2Config)\n    delete tauriConf.bundle.windows.webviewInstallMode.silent\n    tauriConf.bundle.windows.webviewInstallMode.path = `./${path.basename(webviewPath)}`\n  }\n\n  consola.debug('Write tauri version to tauri.conf.json')\n  await fs.writeJSON(TAURI_APP_CONF, tauriConf, { spaces: 2 })\n  consola.debug('tauri.conf.json updated')\n\n  consola.debug('package.json updated')\n}\n\nmain()\n"
  },
  {
    "path": "scripts/publish.ts",
    "content": "import path from 'node:path'\nimport fs from 'fs-extra'\nimport packageJson from '../package.json'\nimport { cwd, TAURI_APP_DIR } from './utils/env'\n\nconst MONO_REPO_PATHS = [\n  path.join(cwd, 'frontend/nyanpasu'),\n  path.join(cwd, 'frontend/ui'),\n  path.join(cwd, 'frontend/interface'),\n  path.join(cwd, 'scripts'),\n]\n\n// import { consola } from \"./utils/logger\";\n\nconst TAURI_APP_CONF_PATH = path.join(TAURI_APP_DIR, 'tauri.conf.json')\nconst TAURI_NIGHTLY_APP_CONF_PATH = path.join(\n  TAURI_APP_DIR,\n  'overrides/nightly.conf.json',\n)\nconst PACKAGE_JSON_PATH = path.join(cwd, 'package.json')\n\n// publish\nasync function resolvePublish() {\n  const flag = process.argv[2] ?? 'patch'\n  const tauriJson = await fs.readJSON(TAURI_APP_CONF_PATH)\n  const tauriNightlyJson = await fs.readJSON(TAURI_NIGHTLY_APP_CONF_PATH)\n\n  let [a, b, c] = packageJson.version.split('.').map(Number)\n\n  if (flag === 'major') {\n    a += 1\n    b = 0\n    c = 0\n  } else if (flag === 'minor') {\n    b += 1\n    c = 0\n  } else if (flag === 'patch') {\n    c += 1\n  } else throw new Error(`invalid flag \"${flag}\"`)\n\n  const nextVersion = `${a}.${b}.${c}`\n  const nextNightlyVersion = `${a}.${b}.${c + 1}`\n  packageJson.version = nextVersion\n  tauriJson.version = nextVersion\n  tauriNightlyJson.version = nextNightlyVersion\n\n  // 发布更新前先写更新日志\n  // const nextTag = `v${nextVersion}`;\n  // await resolveUpdateLog(nextTag);\n\n  await fs.writeJSON(PACKAGE_JSON_PATH, packageJson, {\n    spaces: 2,\n  })\n  await fs.writeJSON(TAURI_APP_CONF_PATH, tauriJson, {\n    spaces: 2,\n  })\n  await fs.writeJSON(TAURI_NIGHTLY_APP_CONF_PATH, tauriNightlyJson, {\n    spaces: 2,\n  })\n\n  // overrides mono repo package.json\n  for (const monoRepoPath of MONO_REPO_PATHS) {\n    const monoRepoPackageJsonPath = path.join(monoRepoPath, 'package.json')\n    const monoRepoPackageJson = await fs.readJSON(monoRepoPackageJsonPath)\n    monoRepoPackageJson.version = nextVersion\n    await fs.writeJSON(monoRepoPackageJsonPath, monoRepoPackageJson, {\n      spaces: 2,\n    })\n  }\n\n  // execSync(\"git add ./package.json\");\n  // execSync(`git add ${TAURI_APP_CONF_PATH}`);\n  // execSync(`git commit -m \"v${nextVersion}\"`);\n  // execSync(`git tag -a v${nextVersion} -m \"v${nextVersion}\"`);\n  // execSync(`git push`);\n  // execSync(`git push origin v${nextVersion}`);\n  // consola.success(`Publish Successfully...`);\n  console.log(nextVersion)\n}\n\nresolvePublish()\n"
  },
  {
    "path": "scripts/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"baseUrl\": \"./\",\n    \"target\": \"ESNext\",\n    \"useDefineForClassFields\": true,\n    \"lib\": [\"ESNext\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"esModuleInterop\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"strict\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Node\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"composite\": true,\n  },\n  \"include\": [\"./\"],\n  \"exclude\": [\"deno\"],\n}\n"
  },
  {
    "path": "scripts/types/index.ts",
    "content": "import { ArchMapping } from 'utils/manifest'\n\nexport interface ClashManifest {\n  URL_PREFIX: string\n  LATEST_DATE?: string\n  STORAGE_PREFIX?: string\n  BACKUP_URL_PREFIX?: string\n  BACKUP_LATEST_DATE?: string\n  VERSION?: string\n  VERSION_URL?: string\n  ARCH_MAPPING: ArchMapping\n}\n\nexport interface BinInfo {\n  name: string\n  targetFile: string\n  exeFile: string\n  tmpFile: string\n  downloadURL: string\n}\n\nexport enum SupportedArch {\n  WindowsX86_32 = 'windows-i386',\n  WindowsX86_64 = 'windows-x86_64',\n  WindowsArm64 = 'windows-arm64',\n  LinuxAarch64 = 'linux-aarch64',\n  LinuxAmd64 = 'linux-amd64',\n  LinuxI386 = 'linux-i386',\n  LinuxArmv7 = 'linux-armv7',\n  LinuxArmv7hf = 'linux-armv7hf',\n  DarwinArm64 = 'darwin-arm64',\n  DarwinX64 = 'darwin-x64',\n}\n\nexport enum SupportedCore {\n  Mihomo = 'mihomo',\n  MihomoAlpha = 'mihomo_alpha',\n  ClashRs = 'clash_rs',\n  ClashRsAlpha = 'clash_rs_alpha',\n  ClashPremium = 'clash_premium',\n}\n\nexport interface ManifestVersion {\n  manifest_version: number\n  latest: { [K in SupportedCore]: string }\n  arch_template: { [K in SupportedCore]: ArchMapping }\n  updated_at: string // ISO 8601\n}\n"
  },
  {
    "path": "scripts/updatelog.ts",
    "content": "import path from 'path'\nimport fs from 'fs-extra'\nimport { cwd } from './utils/env'\n\nconst UPDATE_LOG = 'UPDATELOG.md'\n\n// parse the UPDATELOG.md\nexport async function resolveUpdateLog(tag: string) {\n  const reTitle = /^## v[\\d.]+/\n  const reEnd = /^---/\n\n  const file = path.join(cwd, UPDATE_LOG)\n\n  if (!(await fs.pathExists(file))) {\n    throw new Error('could not found UPDATELOG.md')\n  }\n\n  const data = await fs.readFile(file).then((d) => d.toString('utf8'))\n\n  const map = {} as Record<string, string[]>\n  let p = ''\n\n  data.split('\\n').forEach((line) => {\n    if (reTitle.test(line)) {\n      p = line.slice(3).trim()\n      if (!map[p]) {\n        map[p] = []\n      } else {\n        throw new Error(`Tag ${p} dup`)\n      }\n    } else if (reEnd.test(line)) {\n      p = ''\n    } else if (p) {\n      map[p].push(line)\n    }\n  })\n\n  if (!map[tag]) {\n    throw new Error(`could not found \"${tag}\" in UPDATELOG.md`)\n  }\n\n  return map[tag].join('\\n').trim()\n}\n"
  },
  {
    "path": "scripts/updater-nightly.ts",
    "content": "import { execSync } from 'child_process'\nimport fs from 'fs/promises'\nimport path from 'path'\nimport { camelCase, upperFirst } from 'lodash-es'\nimport semver from 'semver'\nimport { fetch } from 'undici'\nimport yargs from 'yargs'\nimport { hideBin } from 'yargs/helpers'\nimport { z } from 'zod'\nimport { context, getOctokit } from '@actions/github'\nimport tauriNightly from '../backend/tauri/overrides/nightly.conf.json'\nimport { getGithubUrl, getProxyAgent } from './utils'\nimport { colorize, consola } from './utils/logger'\n\nconst UPDATE_TAG_NAME = 'updater'\nconst UPDATE_JSON_FILE = 'update-nightly.json'\nconst UPDATE_JSON_PROXY = 'update-nightly-proxy.json'\nconst UPDATE_FIXED_WEBVIEW_FILE = 'update-nightly-fixed-webview.json'\nconst UPDATE_FIXED_WEBVIEW_PROXY = 'update-nightly-fixed-webview-proxy.json'\n\nconst argv = yargs(hideBin(process.argv))\n  .option('fixed-webview', {\n    type: 'boolean',\n    default: false,\n  })\n  .option('cache-path', {\n    type: 'string',\n    requiresArg: false,\n  })\n  .help()\n  .parseSync()\n\n/// generate update.json\n/// upload to update tag's release asset\nasync function resolveUpdater() {\n  if (process.env.GITHUB_TOKEN === undefined) {\n    throw new Error('GITHUB_TOKEN is required')\n  }\n  consola.start('start to generate updater files')\n  const options = {\n    owner: context.repo.owner,\n    repo: context.repo.repo,\n  }\n  const github = getOctokit(process.env.GITHUB_TOKEN)\n\n  consola.debug('resolve latest pre-release files...')\n  // latest pre-release tag\n  const { data: latestPreRelease } = await github.rest.repos.getReleaseByTag({\n    ...options,\n    tag: 'pre-release',\n  })\n  let shortHash = ''\n  const latestContent = latestPreRelease.assets.find(\n    (o) => o.name === 'latest.json',\n  )\n  // trying to get the short hash from the latest.json\n  if (latestContent) {\n    const schema = z.object({\n      version: z.string().min(1),\n    })\n    const latest = schema.parse(\n      await fetch(latestContent.browser_download_url, {\n        dispatcher: getProxyAgent(),\n      }).then((res) => res.json()),\n    )\n\n    const version = semver.parse(latest.version)\n    if (version && version.build.length > 0) {\n      console.log(version)\n      shortHash = version.build[0]\n    }\n  }\n\n  if (!shortHash) {\n    shortHash = await execSync(`git rev-parse --short pre-release`)\n      .toString()\n      .replace('\\n', '')\n      .replace('\\r', '')\n      .slice(0, 7)\n  }\n\n  consola.info(`latest pre-release short hash: ${shortHash}`)\n  const updateData = {\n    name: `v${tauriNightly.version}-alpha+${shortHash}`,\n    notes: 'Nightly build. Full changes see commit history.',\n    pub_date: new Date().toISOString(),\n    platforms: {\n      win64: { signature: '', url: '' }, // compatible with older formats\n      linux: { signature: '', url: '' }, // compatible with older formats\n      darwin: { signature: '', url: '' }, // compatible with older formats\n      'darwin-aarch64': { signature: '', url: '' },\n      'darwin-intel': { signature: '', url: '' },\n      'darwin-x86_64': { signature: '', url: '' },\n      'linux-x86_64': { signature: '', url: '' },\n      // \"linux-aarch64\": { signature: \"\", url: \"\" },\n      // \"linux-armv7\": { signature: \"\", url: \"\" },\n      'windows-x86_64': { signature: '', url: '' },\n      'windows-i686': { signature: '', url: '' },\n      'windows-aarch64': { signature: '', url: '' },\n    },\n  }\n\n  const promises = latestPreRelease.assets.map(async (asset) => {\n    const { name, browser_download_url: browserDownloadUrl } = asset\n\n    function isMatch(name: string, extension: string, arch: string) {\n      return (\n        name.endsWith(extension) &&\n        name.includes(arch) &&\n        (argv.fixedWebview\n          ? name.includes('fixed-webview')\n          : !name.includes('fixed-webview'))\n      )\n    }\n\n    // win64 url\n    if (isMatch(name, '.nsis.zip', 'x64')) {\n      updateData.platforms.win64.url = browserDownloadUrl\n      updateData.platforms['windows-x86_64'].url = browserDownloadUrl\n    }\n    // win64 signature\n    if (isMatch(name, '.nsis.zip.sig', 'x64')) {\n      const sig = await getSignature(browserDownloadUrl)\n      updateData.platforms.win64.signature = sig\n      updateData.platforms['windows-x86_64'].signature = sig\n    }\n\n    // win32 url\n    if (isMatch(name, '.nsis.zip', 'x86')) {\n      updateData.platforms['windows-i686'].url = browserDownloadUrl\n    }\n    // win32 signature\n    if (isMatch(name, '.nsis.zip.sig', 'x86')) {\n      const sig = await getSignature(browserDownloadUrl)\n      updateData.platforms['windows-i686'].signature = sig\n    }\n\n    // win arm64 url\n    if (isMatch(name, '.nsis.zip', 'arm64')) {\n      updateData.platforms['windows-aarch64'].url = browserDownloadUrl\n    }\n    // win arm64 signature\n    if (isMatch(name, '.nsis.zip.sig', 'arm64')) {\n      const sig = await getSignature(browserDownloadUrl)\n      updateData.platforms['windows-aarch64'].signature = sig\n    }\n\n    // darwin url (intel)\n    if (name.endsWith('.app.tar.gz') && !name.includes('aarch')) {\n      updateData.platforms.darwin.url = browserDownloadUrl\n      updateData.platforms['darwin-intel'].url = browserDownloadUrl\n      updateData.platforms['darwin-x86_64'].url = browserDownloadUrl\n    }\n    // darwin signature (intel)\n    if (name.endsWith('.app.tar.gz.sig') && !name.includes('aarch')) {\n      const sig = await getSignature(browserDownloadUrl)\n      updateData.platforms.darwin.signature = sig\n      updateData.platforms['darwin-intel'].signature = sig\n      updateData.platforms['darwin-x86_64'].signature = sig\n    }\n\n    // darwin url (aarch)\n    if (name.endsWith('aarch64.app.tar.gz')) {\n      updateData.platforms['darwin-aarch64'].url = browserDownloadUrl\n    }\n    // darwin signature (aarch)\n    if (name.endsWith('aarch64.app.tar.gz.sig')) {\n      const sig = await getSignature(browserDownloadUrl)\n      updateData.platforms['darwin-aarch64'].signature = sig\n    }\n\n    // linux url\n    if (name.endsWith('.AppImage.tar.gz')) {\n      updateData.platforms.linux.url = browserDownloadUrl\n      updateData.platforms['linux-x86_64'].url = browserDownloadUrl\n    }\n    // linux signature\n    if (name.endsWith('.AppImage.tar.gz.sig')) {\n      const sig = await getSignature(browserDownloadUrl)\n      updateData.platforms.linux.signature = sig\n      updateData.platforms['linux-x86_64'].signature = sig\n    }\n  })\n\n  await Promise.allSettled(promises)\n  consola.info(updateData)\n\n  consola.debug('generate updater metadata...')\n  // maybe should test the signature as well\n  // delete the null field\n  Object.entries(updateData.platforms).forEach(([key, value]) => {\n    if (!value.url) {\n      throw new Error(`failed to parse release for \"${key}\"`)\n    }\n  })\n\n  // 生成一个代理github的更新文件\n  // 使用 https://hub.fastgit.xyz/ 做github资源的加速\n  const updateDataNew = JSON.parse(\n    JSON.stringify(updateData),\n  ) as typeof updateData\n\n  Object.entries(updateDataNew.platforms).forEach(([key, value]) => {\n    if (value.url) {\n      updateDataNew.platforms[key as keyof typeof updateData.platforms].url =\n        getGithubUrl(value.url)\n    } else {\n      consola.error(`updateDataNew.platforms.${key} is null`)\n    }\n  })\n\n  // update the update.json\n  consola.debug('update updater files...')\n  let updateRelease\n  try {\n    const { data } = await github.rest.repos.getReleaseByTag({\n      ...options,\n      tag: UPDATE_TAG_NAME,\n    })\n    updateRelease = data\n  } catch (err) {\n    consola.error(err)\n    consola.error('failed to get release by tag, create one')\n    const { data } = await github.rest.repos.createRelease({\n      ...options,\n      tag_name: UPDATE_TAG_NAME,\n      name: upperFirst(camelCase(UPDATE_TAG_NAME)),\n      body: 'files for programs to check for updates',\n      prerelease: true,\n    })\n    updateRelease = data\n  }\n\n  // delete the old assets\n  for (const asset of updateRelease.assets) {\n    if (\n      argv.fixedWebview\n        ? asset.name === UPDATE_FIXED_WEBVIEW_FILE\n        : asset.name === UPDATE_JSON_FILE\n    ) {\n      await github.rest.repos.deleteReleaseAsset({\n        ...options,\n        asset_id: asset.id,\n      })\n    }\n\n    if (\n      argv.fixedWebview\n        ? asset.name === UPDATE_FIXED_WEBVIEW_PROXY\n        : asset.name === UPDATE_JSON_PROXY\n    ) {\n      await github.rest.repos\n        .deleteReleaseAsset({ ...options, asset_id: asset.id })\n        .catch((err) => {\n          consola.error(err)\n        }) // do not break the pipeline\n    }\n  }\n\n  // upload new assets\n  await github.rest.repos.uploadReleaseAsset({\n    ...options,\n    release_id: updateRelease.id,\n    name: argv.fixedWebview ? UPDATE_FIXED_WEBVIEW_FILE : UPDATE_JSON_FILE,\n    data: JSON.stringify(updateData, null, 2),\n  })\n\n  // cache the files if cache path is provided\n  await saveToCache(\n    argv.fixedWebview ? UPDATE_FIXED_WEBVIEW_FILE : UPDATE_JSON_FILE,\n    JSON.stringify(updateData, null, 2),\n  )\n\n  await github.rest.repos.uploadReleaseAsset({\n    ...options,\n    release_id: updateRelease.id,\n    name: argv.fixedWebview ? UPDATE_FIXED_WEBVIEW_PROXY : UPDATE_JSON_PROXY,\n    data: JSON.stringify(updateDataNew, null, 2),\n  })\n\n  // cache the proxy file if cache path is provided\n  await saveToCache(\n    argv.fixedWebview ? UPDATE_FIXED_WEBVIEW_PROXY : UPDATE_JSON_PROXY,\n    JSON.stringify(updateDataNew, null, 2),\n  )\n\n  consola.success('updater files updated')\n}\n\nasync function saveToCache(fileName: string, content: string) {\n  if (!argv.cachePath) return\n  try {\n    await fs.mkdir(argv.cachePath, { recursive: true })\n    const filePath = path.join(argv.cachePath, fileName)\n    await fs.writeFile(filePath, content, 'utf-8')\n    consola.success(colorize`cached file saved to: {gray.bold ${filePath}}`)\n  } catch (err) {\n    throw new Error(`Failed to save cache file: ${err}`)\n  }\n}\n\n// get the signature file content\nasync function getSignature(url: string) {\n  const response = await fetch(url, {\n    method: 'GET',\n    headers: { 'Content-Type': 'application/octet-stream' },\n    dispatcher: getProxyAgent(),\n  })\n\n  return response.text()\n}\n\nresolveUpdater().catch((err) => {\n  consola.fatal(err)\n  process.exit(1)\n})\n"
  },
  {
    "path": "scripts/updater.ts",
    "content": "import fs from 'fs/promises'\nimport path from 'path'\nimport { fetch } from 'undici'\nimport yargs from 'yargs'\nimport { hideBin } from 'yargs/helpers'\nimport { context, getOctokit } from '@actions/github'\nimport { resolveUpdateLog } from './updatelog'\nimport { getGithubUrl, getProxyAgent } from './utils'\nimport { colorize, consola } from './utils/logger'\n\nconst UPDATE_TAG_NAME = 'updater'\nconst UPDATE_JSON_FILE = 'update.json'\nconst UPDATE_JSON_PROXY = 'update-proxy.json'\nconst UPDATE_FIXED_WEBVIEW_FILE = 'update-fixed-webview.json'\nconst UPDATE_FIXED_WEBVIEW_PROXY = 'update-fixed-webview-proxy.json'\nconst UPDATE_RELEASE_BODY = process.env.RELEASE_BODY || ''\n\nconst argv = yargs(hideBin(process.argv))\n  .option('fixed-webview', {\n    type: 'boolean',\n    default: false,\n  })\n  .option('cache-path', {\n    type: 'string',\n    requiresArg: false,\n  })\n  .help()\n  .parseSync()\n\n/// generate update.json\n/// upload to update tag's release asset\nasync function resolveUpdater() {\n  if (process.env.GITHUB_TOKEN === undefined) {\n    throw new Error('GITHUB_TOKEN is required')\n  }\n\n  const options = { owner: context.repo.owner, repo: context.repo.repo }\n  const github = getOctokit(process.env.GITHUB_TOKEN)\n\n  const { data: tags } = await github.rest.repos.listTags({\n    ...options,\n    per_page: 10,\n    page: 1,\n  })\n\n  // get the latest publish tag\n  const tag = tags.find((t) => t.name.startsWith('v'))\n  if (!tag) throw new Error('could not found the latest tag')\n  consola.debug(colorize`latest tag: {gray.bold ${tag.name}}`)\n\n  const { data: latestRelease } = await github.rest.repos.getReleaseByTag({\n    ...options,\n    tag: tag.name,\n  })\n\n  let updateLog: string | null = null\n  try {\n    updateLog = await resolveUpdateLog(tag.name)\n  } catch (err) {\n    consola.error(err)\n  }\n\n  const updateData = {\n    name: tag.name,\n    notes: UPDATE_RELEASE_BODY || updateLog || latestRelease.body,\n    pub_date: new Date().toISOString(),\n    platforms: {\n      win64: { signature: '', url: '' }, // compatible with older formats\n      linux: { signature: '', url: '' }, // compatible with older formats\n      darwin: { signature: '', url: '' }, // compatible with older formats\n      'darwin-aarch64': { signature: '', url: '' },\n      'darwin-intel': { signature: '', url: '' },\n      'darwin-x86_64': { signature: '', url: '' },\n      'linux-x86_64': { signature: '', url: '' },\n      // \"linux-aarch64\": { signature: \"\", url: \"\" },\n      // \"linux-armv7\": { signature: \"\", url: \"\" },\n      'windows-x86_64': { signature: '', url: '' },\n      'windows-i686': { signature: '', url: '' },\n      'windows-aarch64': { signature: '', url: '' },\n    },\n  }\n\n  const promises = latestRelease.assets.map(async (asset) => {\n    const { name, browser_download_url: browserDownloadUrl } = asset\n\n    function isMatch(name: string, extension: string, arch: string) {\n      return (\n        name.endsWith(extension) &&\n        name.includes(arch) &&\n        (argv.fixedWebview\n          ? name.includes('fixed-webview')\n          : !name.includes('fixed-webview'))\n      )\n    }\n\n    // win64 url\n    if (isMatch(name, '.nsis.zip', 'x64')) {\n      updateData.platforms.win64.url = browserDownloadUrl\n      updateData.platforms['windows-x86_64'].url = browserDownloadUrl\n    }\n    // win64 signature\n    if (isMatch(name, '.nsis.zip.sig', 'x64')) {\n      const sig = await getSignature(browserDownloadUrl)\n      updateData.platforms.win64.signature = sig\n      updateData.platforms['windows-x86_64'].signature = sig\n    }\n\n    // win32 url\n    if (isMatch(name, '.nsis.zip', 'x86')) {\n      updateData.platforms['windows-i686'].url = browserDownloadUrl\n    }\n    // win32 signature\n    if (isMatch(name, '.nsis.zip.sig', 'x86')) {\n      const sig = await getSignature(browserDownloadUrl)\n      updateData.platforms['windows-i686'].signature = sig\n    }\n\n    // win arm64 url\n    if (isMatch(name, '.nsis.zip', 'arm64')) {\n      updateData.platforms['windows-aarch64'].url = browserDownloadUrl\n    }\n    // win arm64 signature\n    if (isMatch(name, '.nsis.zip.sig', 'arm64')) {\n      const sig = await getSignature(browserDownloadUrl)\n      updateData.platforms['windows-aarch64'].signature = sig\n    }\n\n    // darwin url (intel)\n    if (name.endsWith('.app.tar.gz') && !name.includes('aarch')) {\n      updateData.platforms.darwin.url = browserDownloadUrl\n      updateData.platforms['darwin-intel'].url = browserDownloadUrl\n      updateData.platforms['darwin-x86_64'].url = browserDownloadUrl\n    }\n    // darwin signature (intel)\n    if (name.endsWith('.app.tar.gz.sig') && !name.includes('aarch')) {\n      const sig = await getSignature(browserDownloadUrl)\n      updateData.platforms.darwin.signature = sig\n      updateData.platforms['darwin-intel'].signature = sig\n      updateData.platforms['darwin-x86_64'].signature = sig\n    }\n\n    // darwin url (aarch)\n    if (name.endsWith('aarch64.app.tar.gz')) {\n      updateData.platforms['darwin-aarch64'].url = browserDownloadUrl\n    }\n    // darwin signature (aarch)\n    if (name.endsWith('aarch64.app.tar.gz.sig')) {\n      const sig = await getSignature(browserDownloadUrl)\n      updateData.platforms['darwin-aarch64'].signature = sig\n    }\n\n    // linux url\n    if (name.endsWith('.AppImage.tar.gz')) {\n      updateData.platforms.linux.url = browserDownloadUrl\n      updateData.platforms['linux-x86_64'].url = browserDownloadUrl\n    }\n    // linux signature\n    if (name.endsWith('.AppImage.tar.gz.sig')) {\n      const sig = await getSignature(browserDownloadUrl)\n      updateData.platforms.linux.signature = sig\n      updateData.platforms['linux-x86_64'].signature = sig\n    }\n  })\n\n  await Promise.allSettled(promises)\n  consola.info(updateData)\n\n  // maybe should test the signature as well\n  // delete the null field\n  Object.entries(updateData.platforms).forEach(([key, value]) => {\n    if (!value.url) {\n      consola.error(`failed to parse release for \"${key}\"`)\n      delete updateData.platforms[key as keyof typeof updateData.platforms]\n    }\n  })\n\n  // 生成一个代理github的更新文件\n  // 使用 https://hub.fastgit.xyz/ 做github资源的加速\n  const updateDataNew = JSON.parse(\n    JSON.stringify(updateData),\n  ) as typeof updateData\n\n  Object.entries(updateDataNew.platforms).forEach(([key, value]) => {\n    if (value.url) {\n      updateDataNew.platforms[key as keyof typeof updateData.platforms].url =\n        getGithubUrl(value.url)\n    } else {\n      consola.error(`updateDataNew.platforms.${key} is null`)\n    }\n  })\n\n  // update the update.json\n  const { data: updateRelease } = await github.rest.repos.getReleaseByTag({\n    ...options,\n    tag: UPDATE_TAG_NAME,\n  })\n\n  // delete the old assets\n  for (const asset of updateRelease.assets) {\n    if (\n      argv.fixedWebview\n        ? asset.name === UPDATE_FIXED_WEBVIEW_FILE\n        : asset.name === UPDATE_JSON_FILE\n    ) {\n      await github.rest.repos.deleteReleaseAsset({\n        ...options,\n        asset_id: asset.id,\n      })\n    }\n\n    if (\n      argv.fixedWebview\n        ? asset.name === UPDATE_FIXED_WEBVIEW_PROXY\n        : asset.name === UPDATE_JSON_PROXY\n    ) {\n      await github.rest.repos\n        .deleteReleaseAsset({ ...options, asset_id: asset.id })\n        .catch((err) => {\n          consola.error(err)\n        }) // do not break the pipeline\n    }\n  }\n\n  // upload new assets\n  await github.rest.repos.uploadReleaseAsset({\n    ...options,\n    release_id: updateRelease.id,\n    name: argv.fixedWebview ? UPDATE_FIXED_WEBVIEW_FILE : UPDATE_JSON_FILE,\n    data: JSON.stringify(updateData, null, 2),\n  })\n\n  // cache the files if cache path is provided\n  await saveToCache(\n    argv.fixedWebview ? UPDATE_FIXED_WEBVIEW_FILE : UPDATE_JSON_FILE,\n    JSON.stringify(updateData, null, 2),\n  )\n\n  await github.rest.repos.uploadReleaseAsset({\n    ...options,\n    release_id: updateRelease.id,\n    name: argv.fixedWebview ? UPDATE_FIXED_WEBVIEW_PROXY : UPDATE_JSON_PROXY,\n    data: JSON.stringify(updateDataNew, null, 2),\n  })\n\n  // cache the proxy file if cache path is provided\n  await saveToCache(\n    argv.fixedWebview ? UPDATE_FIXED_WEBVIEW_PROXY : UPDATE_JSON_PROXY,\n    JSON.stringify(updateDataNew, null, 2),\n  )\n}\n\nasync function saveToCache(fileName: string, content: string) {\n  if (!argv.cachePath) return\n\n  try {\n    await fs.mkdir(argv.cachePath, { recursive: true })\n    const filePath = path.join(argv.cachePath, fileName)\n    await fs.writeFile(filePath, content, 'utf-8')\n    consola.success(colorize`cached file saved to: {gray.bold ${filePath}}`)\n  } catch (err) {\n    consola.error(`Failed to save cache file: ${err}`)\n  }\n}\n\n// get the signature file content\nasync function getSignature(url: string) {\n  const response = await fetch(url, {\n    method: 'GET',\n    headers: { 'Content-Type': 'application/octet-stream' },\n    dispatcher: getProxyAgent(),\n  })\n\n  return response.text()\n}\n\nresolveUpdater().catch((err) => {\n  consola.error(err)\n})\n"
  },
  {
    "path": "scripts/utils/arch-check.ts",
    "content": "import { colorize, consola } from './logger'\n\nexport const archCheck = (platform: string, arch: string) => {\n  consola.debug(colorize`platform {yellow ${platform}}`)\n\n  consola.debug(colorize`arch {yellow ${arch}}`)\n}\n"
  },
  {
    "path": "scripts/utils/consts.ts",
    "content": "import { execSync } from 'child_process'\n\nexport const SIDECAR_HOST: string | undefined = process.argv.includes(\n  '--sidecar-host',\n)\n  ? process.argv[process.argv.indexOf('--sidecar-host') + 1]\n  : execSync('rustc -vV')\n      .toString()\n      ?.match(/(?<=host: ).+(?=\\s*)/g)?.[0]\n"
  },
  {
    "path": "scripts/utils/download.ts",
    "content": "import { execSync } from 'child_process'\nimport path from 'path'\nimport zlib from 'zlib'\nimport AdmZip from 'adm-zip'\nimport fs from 'fs-extra'\nimport * as tar from 'tar'\nimport { BinInfo } from 'types'\nimport { fetch, type RequestInit } from 'undici'\nimport { getProxyAgent } from './'\nimport { TAURI_APP_DIR, TEMP_DIR } from './env'\nimport { colorize, consola } from './logger'\n\n/**\n * download sidecar and rename\n */\nexport const downloadFile = async (url: string, path: string) => {\n  const options: Partial<RequestInit> = {}\n\n  const httpProxy = getProxyAgent()\n\n  if (httpProxy) {\n    options.dispatcher = httpProxy\n  }\n\n  consola.debug(colorize`download {gray \"${url}\"} to {gray \"${path}\"}`)\n\n  const response = await fetch(url, {\n    ...options,\n    method: 'GET',\n    headers: {\n      'Content-Type': 'application/octet-stream',\n      'User-Agent':\n        'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:131.0) Gecko/20100101 Firefox/131.0',\n    },\n  })\n\n  // check status code\n  if (response.status !== 200) {\n    throw new Error(`download failed: ${response.statusText}`)\n  }\n\n  const buffer = await response.arrayBuffer()\n\n  await fs.writeFile(path, new Uint8Array(buffer))\n\n  consola.success(colorize`download finished {gray \"${url.split('/').at(-1)}\"}`)\n}\n\nexport const resolveSidecar = async (\n  binInfo: PromiseLike<BinInfo> | BinInfo,\n  platform: string,\n  option?: { force?: boolean },\n) => {\n  const { name, targetFile, tmpFile, exeFile, downloadURL } = await binInfo\n\n  consola.debug(colorize`resolve {cyan ${name}}...`)\n\n  const sidecarDir = path.join(TAURI_APP_DIR, 'sidecar')\n\n  const sidecarPath = path.join(sidecarDir, targetFile)\n\n  await fs.mkdirp(sidecarDir)\n\n  if (!option?.force && (await fs.pathExists(sidecarPath))) return\n\n  const tempDir = path.join(TEMP_DIR, name)\n\n  const tempFile = path.join(tempDir, tmpFile)\n\n  const tempExe = path.join(tempDir, exeFile)\n\n  await fs.mkdirp(tempDir)\n\n  try {\n    if (!(await fs.pathExists(tempFile))) {\n      await downloadFile(downloadURL, tempFile)\n    }\n    if (tmpFile.endsWith('.zip')) {\n      const zip = new AdmZip(tempFile)\n\n      let entryName\n      zip.getEntries().forEach((entry) => {\n        consola.debug(colorize`\"{green ${name}}\" entry name ${entry.entryName}`)\n        if (\n          (entry.entryName.includes(name) &&\n            entry.entryName.endsWith('.exe')) ||\n          (entry.entryName.includes(\n            name\n              .split('-')\n              .filter((o) => o !== 'alpha')\n              .join('-'),\n          ) &&\n            entry.entryName.endsWith('.exe'))\n        ) {\n          entryName = entry.entryName\n        }\n      })\n\n      zip.extractAllTo(tempDir, true)\n\n      if (!entryName) {\n        throw new Error('cannot find exe file in zip')\n      }\n\n      await fs.rename(path.join(tempDir, entryName), tempExe)\n\n      await fs.rename(tempExe, sidecarPath)\n\n      consola.debug(colorize`{green \"${name}\"} unzip finished`)\n    } else if (tmpFile.endsWith('.tar.gz')) {\n      // decompress and untar the file\n      await tar.x({\n        file: tempFile,\n        cwd: tempDir,\n      })\n      await fs.rename(tempExe, sidecarPath)\n      consola.debug(colorize`{green \"${name}\"} untar finished`)\n    } else if (tmpFile.endsWith('.gz')) {\n      // gz\n      const readStream = fs.createReadStream(tempFile)\n\n      const writeStream = fs.createWriteStream(sidecarPath)\n\n      await new Promise<void>((resolve, reject) => {\n        const onError = (error: Error) => {\n          consola.error(colorize`\"${name}\" gz failed:`, error)\n          reject(error)\n        }\n        readStream\n          .pipe(zlib.createGunzip().on('error', onError))\n          .pipe(writeStream)\n          .on('finish', () => {\n            consola.debug(colorize`{green \"${name}\"} gunzip finished`)\n\n            execSync(`chmod 755 ${sidecarPath}`)\n\n            consola.debug(colorize`{green \"${name}\"}chmod binary finished`)\n\n            resolve()\n          })\n          .on('error', onError)\n      })\n    } else {\n      // Common Files\n      await fs.rename(tempFile, sidecarPath)\n\n      consola.info(colorize`{green \"${name}\"} rename finished`)\n\n      if (platform !== 'win32') {\n        execSync(`chmod 755 ${sidecarPath}`)\n\n        consola.info(colorize`{green \"${name}\"} chmod binary finished`)\n      }\n    }\n    consola.success(colorize`resolve {green ${name}} finished`)\n  } catch (err) {\n    // 需要删除文件\n    await fs.remove(sidecarPath)\n\n    throw err\n  } finally {\n    // delete temp dir\n    await fs.remove(tempDir)\n  }\n}\n"
  },
  {
    "path": "scripts/utils/env.ts",
    "content": "import path from 'path'\n\nexport const cwd = process.cwd()\nexport const TAURI_APP_DIR = path.join(cwd, 'backend/tauri')\nexport const TAURI_FIXED_WEBVIEW2_CONFIG_OVERRIDE_PATH = path.join(\n  TAURI_APP_DIR,\n  'overrides/fixed-webview2.conf.json',\n)\nexport const MANIFEST_DIR = path.join(cwd, 'manifest')\nexport const GITHUB_PROXY = 'https://gh-proxy.com/'\nexport const GITHUB_TOKEN = process.env.GITHUB_TOKEN\nexport const TEMP_DIR = path.join(cwd, 'node_modules/.verge')\nexport const MANIFEST_VERSION_PATH = path.join(MANIFEST_DIR, 'version.json')\nexport const TAURI_APP_TEMP_DIR = path.join(TAURI_APP_DIR, 'tmp')\nexport const GIT_SUMMARY_INFO_PATH = path.join(\n  TAURI_APP_TEMP_DIR,\n  'git-info.json',\n)\n"
  },
  {
    "path": "scripts/utils/index.ts",
    "content": "import figlet from 'figlet'\nimport { filesize } from 'filesize'\nimport fs from 'fs-extra'\nimport { ProxyAgent } from 'undici'\nimport { GITHUB_PROXY } from './env'\n\nexport const getGithubUrl = (url: string) => {\n  return new URL(url.replace(/^https?:\\/\\//g, ''), GITHUB_PROXY).toString()\n}\n\nexport const getFileSize = (path: string): string => {\n  const stat = fs.statSync(path)\n  return filesize(stat.size)\n}\n\nexport const array2text = (\n  array: string[],\n  type: 'newline' | 'space' = 'newline',\n): string => {\n  let result = ''\n\n  const getSplit = () => {\n    if (type === 'newline') {\n      return '\\n'\n    } else if (type === 'space') {\n      return ' '\n    }\n  }\n\n  array.forEach((value, index) => {\n    if (index === array.length - 1) {\n      result += value\n    } else {\n      result += value + getSplit()\n    }\n  })\n\n  return result\n}\n\nexport const printNyanpasu = () => {\n  const ascii = figlet.textSync('Clash Nyanpasu', {\n    whitespaceBreak: true,\n  })\n\n  console.log(ascii)\n}\n\nexport const HTTP_PROXY =\n  process.env.HTTP_PROXY ||\n  process.env.http_proxy ||\n  process.env.HTTPS_PROXY ||\n  process.env.https_proxy\n\nexport function getProxyAgent() {\n  if (HTTP_PROXY) {\n    return new ProxyAgent(HTTP_PROXY)\n  }\n\n  return undefined\n}\n"
  },
  {
    "path": "scripts/utils/logger.ts",
    "content": "import { createColorize } from 'colorize-template'\nimport { createConsola } from 'consola'\nimport pc from 'picocolors'\n\nexport const consola = createConsola({\n  level: process.env.LOG_LEVEL ? Number.parseInt(process.env.LOG_LEVEL) : 5,\n  fancy: true,\n  formatOptions: {\n    colors: true,\n    compact: false,\n    date: true,\n  },\n})\n\nexport const colorize = createColorize({\n  ...pc,\n  success: pc.green,\n  error: pc.red,\n})\n"
  },
  {
    "path": "scripts/utils/manifest.ts",
    "content": "import { fetch } from 'undici'\nimport { SupportedArch } from '../types/index'\nimport { getProxyAgent } from './'\nimport { consola } from './logger'\nimport { applyProxy, octokit } from './octokit'\n\nexport type ArchMapping = { [key in SupportedArch]: string }\n\nexport type NodeArch = NodeJS.Architecture | 'armel'\n\n// resolvers block\nexport type LatestVersionResolver = Promise<{\n  name: string\n  version: string\n  archMapping: ArchMapping\n}>\n\nexport const resolveMihomo = async (): LatestVersionResolver => {\n  const latestRelease = await octokit.rest.repos.getLatestRelease(\n    applyProxy({\n      owner: 'MetaCubeX',\n      repo: 'mihomo',\n    }),\n  )\n\n  consola.debug(`mihomo latest release: ${latestRelease.data.tag_name}`)\n\n  const archMapping: ArchMapping = {\n    [SupportedArch.WindowsX86_32]: 'mihomo-windows-386-{}.zip',\n    [SupportedArch.WindowsX86_64]: 'mihomo-windows-amd64-v1-{}.zip',\n    [SupportedArch.WindowsArm64]: 'mihomo-windows-arm64-{}.zip',\n    [SupportedArch.LinuxAarch64]: 'mihomo-linux-arm64-{}.gz',\n    [SupportedArch.LinuxAmd64]: 'mihomo-linux-amd64-v1-{}.gz',\n    [SupportedArch.LinuxI386]: 'mihomo-linux-386-{}.gz',\n    [SupportedArch.DarwinArm64]: 'mihomo-darwin-arm64-{}.gz',\n    [SupportedArch.DarwinX64]: 'mihomo-darwin-amd64-v1-{}.gz',\n    [SupportedArch.LinuxArmv7]: 'mihomo-linux-armv5-{}.gz',\n    [SupportedArch.LinuxArmv7hf]: 'mihomo-linux-armv7-{}.gz',\n  } satisfies ArchMapping\n\n  return {\n    name: 'mihomo',\n    version: latestRelease.data.tag_name,\n    archMapping,\n  }\n}\n\nexport const resolveMihomoAlpha = async (): LatestVersionResolver => {\n  const resp = await fetch(\n    'https://github.com/MetaCubeX/mihomo/releases/download/Prerelease-Alpha/version.txt',\n    { dispatcher: getProxyAgent() },\n  )\n\n  const alphaReleaseHash = (await resp.text()).trim()\n\n  consola.debug(`mihomo alpha release: ${alphaReleaseHash}`)\n\n  const archMapping: ArchMapping = {\n    [SupportedArch.WindowsX86_32]: 'mihomo-windows-386-{}.zip',\n    [SupportedArch.WindowsX86_64]: 'mihomo-windows-amd64-v1-{}.zip',\n    [SupportedArch.WindowsArm64]: 'mihomo-windows-arm64-{}.zip',\n    [SupportedArch.LinuxAarch64]: 'mihomo-linux-arm64-{}.gz',\n    [SupportedArch.LinuxAmd64]: 'mihomo-linux-amd64-v1-{}.gz',\n    [SupportedArch.LinuxI386]: 'mihomo-linux-386-{}.gz',\n    [SupportedArch.DarwinArm64]: 'mihomo-darwin-arm64-{}.gz',\n    [SupportedArch.DarwinX64]: 'mihomo-darwin-amd64-v1-{}.gz',\n    [SupportedArch.LinuxArmv7]: 'mihomo-linux-armv5-{}.gz',\n    [SupportedArch.LinuxArmv7hf]: 'mihomo-linux-armv7-{}.gz',\n  } satisfies ArchMapping\n\n  return {\n    name: 'mihomo_alpha',\n    version: alphaReleaseHash,\n    archMapping,\n  }\n}\n\nexport const resolveClashRs = async (): LatestVersionResolver => {\n  const latestRelease = await octokit.rest.repos.getLatestRelease(\n    applyProxy({\n      owner: 'Watfaq',\n      repo: 'clash-rs',\n    }),\n  )\n\n  consola.debug(`clash-rs latest release: ${latestRelease.data.tag_name}`)\n\n  const archMapping: ArchMapping = {\n    [SupportedArch.WindowsX86_32]: 'clash-i686-pc-windows-msvc-static-crt.exe',\n    [SupportedArch.WindowsX86_64]: 'clash-x86_64-pc-windows-msvc.exe',\n    [SupportedArch.WindowsArm64]: 'clash-aarch64-pc-windows-msvc.exe',\n    [SupportedArch.LinuxAarch64]: 'clash-aarch64-unknown-linux-gnu',\n    [SupportedArch.LinuxAmd64]: 'clash-x86_64-unknown-linux-gnu-static-crt',\n    [SupportedArch.LinuxI386]: 'clash-i686-unknown-linux-gnu',\n    [SupportedArch.DarwinArm64]: 'clash-aarch64-apple-darwin',\n    [SupportedArch.DarwinX64]: 'clash-x86_64-apple-darwin',\n    [SupportedArch.LinuxArmv7]: 'clash-armv7-unknown-linux-gnueabi',\n    [SupportedArch.LinuxArmv7hf]: 'clash-armv7-unknown-linux-gnueabihf',\n  } satisfies ArchMapping\n\n  return {\n    name: 'clash_rs',\n    version: latestRelease.data.tag_name,\n    archMapping,\n  }\n}\n\nexport const resolveClashRsAlpha = async (): LatestVersionResolver => {\n  const resp = await fetch(\n    'https://github.com/Watfaq/clash-rs/releases/download/latest/version.txt',\n    { dispatcher: getProxyAgent() },\n  )\n\n  const alphaVersion = resp.ok\n    ? (await resp.text()).trim().split(' ').pop()!\n    : 'latest'\n\n  consola.debug(`clash-rs alpha latest release: ${alphaVersion}`)\n\n  const archMapping: ArchMapping = {\n    [SupportedArch.WindowsX86_32]:\n      'clash-rs-i686-pc-windows-msvc-static-crt.exe',\n    [SupportedArch.WindowsX86_64]: 'clash-rs-x86_64-pc-windows-msvc.exe',\n    [SupportedArch.WindowsArm64]: 'clash-rs-aarch64-pc-windows-msvc.exe',\n    [SupportedArch.LinuxAarch64]: 'clash-rs-aarch64-unknown-linux-gnu',\n    [SupportedArch.LinuxAmd64]: 'clash-rs-x86_64-unknown-linux-gnu-static-crt',\n    [SupportedArch.LinuxI386]: 'clash-rs-i686-unknown-linux-gnu',\n    [SupportedArch.DarwinArm64]: 'clash-rs-aarch64-apple-darwin',\n    [SupportedArch.DarwinX64]: 'clash-rs-x86_64-apple-darwin',\n    [SupportedArch.LinuxArmv7]: 'clash-rs-armv7-unknown-linux-gnueabi',\n    [SupportedArch.LinuxArmv7hf]: 'clash-rs-armv7-unknown-linux-gnueabihf',\n  } satisfies ArchMapping\n\n  return {\n    name: 'clash_rs_alpha',\n    version: alphaVersion,\n    archMapping,\n  }\n}\n\nexport const resolveClashPremium = async (): LatestVersionResolver => {\n  const latestRelease = await octokit.rest.repos.getLatestRelease(\n    applyProxy({\n      owner: 'zhongfly',\n      repo: 'Clash-premium-backup',\n    }),\n  )\n\n  consola.debug(`clash-premium latest release: ${latestRelease.data.tag_name}`)\n\n  const archMapping: ArchMapping = {\n    [SupportedArch.WindowsX86_32]: 'clash-windows-386-n{}.zip',\n    [SupportedArch.WindowsX86_64]: 'clash-windows-amd64-n{}.zip',\n    [SupportedArch.WindowsArm64]: 'clash-windows-arm64-n{}.zip',\n    [SupportedArch.LinuxAarch64]: 'clash-linux-arm64-n{}.gz',\n    [SupportedArch.LinuxAmd64]: 'clash-linux-amd64-n{}.gz',\n    [SupportedArch.LinuxI386]: 'clash-linux-386-n{}.gz',\n    [SupportedArch.DarwinArm64]: 'clash-darwin-arm64-n{}.gz',\n    [SupportedArch.DarwinX64]: 'clash-darwin-amd64-n{}.gz',\n    [SupportedArch.LinuxArmv7]: 'clash-linux-armv5-n{}.gz',\n    [SupportedArch.LinuxArmv7hf]: 'clash-linux-armv7-n{}.gz',\n  } satisfies ArchMapping\n\n  return {\n    name: 'clash_premium',\n    version: latestRelease.data.tag_name,\n    archMapping,\n  }\n}\n"
  },
  {
    "path": "scripts/utils/octokit.ts",
    "content": "import { Octokit } from 'octokit'\nimport { ProxyAgent, fetch as undiciFetch } from 'undici'\nimport { HTTP_PROXY } from './'\n\nconst BASE_OPTIONS = {\n  owner: 'libnyanpasu',\n  repo: 'clash-nyanpasu',\n}\n\nexport const fetcher = (\n  url: string,\n  options: Parameters<typeof undiciFetch>[1] = {},\n) => {\n  return undiciFetch(url, {\n    ...options,\n    dispatcher: HTTP_PROXY ? new ProxyAgent(HTTP_PROXY) : undefined,\n  })\n}\n\nexport const octokit = new Octokit(applyProxy(BASE_OPTIONS))\n\nexport function applyProxy(opts: ConstructorParameters<typeof Octokit>[0]) {\n  return {\n    ...opts,\n    request: {\n      fetch: fetcher,\n    },\n    auth: process.env.GITHUB_TOKEN || process.env.GH_TOKEN || undefined,\n  } satisfies ConstructorParameters<typeof Octokit>[0]\n}\n"
  },
  {
    "path": "scripts/utils/resolve.ts",
    "content": "import crypto from 'node:crypto'\nimport path from 'path'\nimport AdmZip from 'adm-zip'\nimport fs from 'fs-extra'\nimport { BinInfo } from '../types'\nimport { downloadFile, resolveSidecar } from './download'\nimport { TAURI_APP_DIR, TEMP_DIR } from './env'\nimport { colorize, consola } from './logger'\nimport { NodeArch } from './manifest'\nimport {\n  getClashBackupInfo,\n  getClashMetaAlphaInfo,\n  getClashMetaInfo,\n  getClashRustAlphaInfo,\n  getClashRustInfo,\n  getNyanpasuServiceInfo,\n} from './resource'\n\n/**\n * download the file to the resources dir\n */\nexport const resolveResource = async (\n  binInfo: { file: string; downloadURL: string },\n  options?: { force?: boolean },\n) => {\n  const { file, downloadURL } = binInfo\n\n  const resDir = path.join(TAURI_APP_DIR, 'resources')\n\n  const targetPath = path.join(resDir, file)\n\n  if (!options?.force && (await fs.pathExists(targetPath))) return\n\n  await fs.mkdirp(resDir)\n\n  await downloadFile(downloadURL, targetPath)\n\n  consola.success(colorize`resolve {green ${file}} finished`)\n}\n\nexport class Resolve {\n  private infoOption: {\n    platform: NodeJS.Platform\n    arch: NodeArch\n    sidecarHost: string\n  }\n\n  constructor(\n    private readonly options: {\n      force?: boolean\n      platform: NodeJS.Platform\n      arch: NodeArch\n      sidecarHost: string\n    },\n  ) {\n    this.infoOption = {\n      platform: this.options.platform,\n      arch: this.options.arch,\n      sidecarHost: this.options.sidecarHost,\n    }\n  }\n\n  /**\n   * only Windows\n   * get the wintun.dll (not required)\n   */\n  public async wintun() {\n    const { platform } = process\n    let arch: string = this.options.arch || 'x64'\n    if (platform !== 'win32') return\n\n    switch (arch) {\n      case 'x64':\n        arch = 'amd64'\n        break\n      case 'ia32':\n        arch = 'x86'\n        break\n      case 'arm':\n        arch = 'arm'\n        break\n      case 'arm64':\n        arch = 'arm64'\n        break\n      default:\n        throw new Error(`unsupported arch ${arch}`)\n    }\n\n    const url = 'https://www.wintun.net/builds/wintun-0.14.1.zip'\n    const hash =\n      '07c256185d6ee3652e09fa55c0b673e2624b565e02c4b9091c79ca7d2f24ef51'\n\n    const tempDir = path.join(TEMP_DIR, 'wintun')\n\n    const tempZip = path.join(tempDir, 'wintun.zip')\n\n    // const wintunPath = path.join(tempDir, \"wintun/bin/amd64/wintun.dll\");\n\n    const targetPath = path.join(TAURI_APP_DIR, 'resources', 'wintun.dll')\n\n    if (!this.options?.force && (await fs.pathExists(targetPath))) return\n\n    await fs.mkdirp(tempDir)\n\n    if (!(await fs.pathExists(tempZip))) {\n      await downloadFile(url, tempZip)\n    }\n\n    // check hash\n    const hashBuffer = await fs.readFile(tempZip)\n    const sha256 = crypto.createHash('sha256')\n    sha256.update(hashBuffer)\n    const hashValue = sha256.digest('hex')\n    if (hashValue !== hash) {\n      throw new Error(`wintun. hash not match ${hashValue}`)\n    }\n\n    // unzip\n    const zip = new AdmZip(tempZip)\n\n    zip.extractAllTo(tempDir, true)\n\n    // recursive list path for debug use\n    const files = (await fs.readdir(tempDir, { recursive: true })).filter(\n      (file) => file.includes('wintun.dll'),\n    )\n    consola.debug(colorize`{green wintun} founded dlls: ${files}`)\n\n    const file = files.find((file) => file.includes(arch))\n    if (!file) {\n      throw new Error(`wintun. not found arch ${arch}`)\n    }\n\n    const wintunPath = path.join(tempDir, file.toString())\n\n    if (!(await fs.pathExists(wintunPath))) {\n      throw new Error(`path not found \"${wintunPath}\"`)\n    }\n    // prepare resource dir\n    await fs.mkdirp(path.dirname(targetPath))\n    await fs.copyFile(wintunPath, targetPath)\n\n    await fs.remove(tempDir)\n\n    consola.success(colorize`resolve {green wintun.dll} finished`)\n  }\n\n  public async service() {\n    return await this.sidecar(getNyanpasuServiceInfo(this.infoOption))\n  }\n\n  public mmdb() {\n    return resolveResource({\n      file: 'Country.mmdb',\n      downloadURL: `https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/country.mmdb`,\n    })\n  }\n\n  public geosite() {\n    return resolveResource({\n      file: 'geosite.dat',\n      downloadURL: `https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat`,\n    })\n  }\n\n  public geoip() {\n    return resolveResource({\n      file: 'geoip.dat',\n      downloadURL: `https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.dat`,\n    })\n  }\n\n  public enableLoopback() {\n    return resolveResource({\n      file: 'enableLoopback.exe',\n      downloadURL: `https://github.com/Kuingsmile/uwp-tool/releases/download/latest/enableLoopback.exe`,\n    })\n  }\n\n  private sidecar(binInfo: BinInfo | PromiseLike<BinInfo>) {\n    return resolveSidecar(binInfo, this.options.platform, {\n      force: this.options.force,\n    })\n  }\n\n  public async clash() {\n    return await this.sidecar(getClashBackupInfo(this.infoOption))\n  }\n\n  public async clashMeta() {\n    return await this.sidecar(getClashMetaInfo(this.infoOption))\n  }\n\n  public async clashMetaAlpha() {\n    return await this.sidecar(getClashMetaAlphaInfo(this.infoOption))\n  }\n\n  public async clashRust() {\n    return await this.sidecar(getClashRustInfo(this.infoOption))\n  }\n\n  public async clashRustAlpha() {\n    return await this.sidecar(getClashRustAlphaInfo(this.infoOption))\n  }\n}\n"
  },
  {
    "path": "scripts/utils/resource.ts",
    "content": "// import { ArchMapping } from 'utils/manifest';\nimport { fetch, type RequestInit } from 'undici'\nimport {\n  CLASH_META_ALPHA_MANIFEST,\n  CLASH_META_MANIFEST,\n} from '../manifest/clash-meta'\nimport { CLASH_MANIFEST } from '../manifest/clash-premium'\nimport {\n  CLASH_RS_ALPHA_MANIFEST,\n  CLASH_RS_MANIFEST,\n} from '../manifest/clash-rs'\nimport { BinInfo, SupportedArch } from '../types'\nimport { getProxyAgent } from './'\nimport { SIDECAR_HOST } from './consts'\nimport { consola } from './logger'\n\nconst SERVICE_REPO = 'libnyanpasu/nyanpasu-service'\n\ntype NodeArch = NodeJS.Architecture | 'armel'\n\nfunction mappingArch(platform: NodeJS.Platform, arch: NodeArch): SupportedArch {\n  const label = `${platform}-${arch}`\n  switch (label) {\n    case 'darwin-x64':\n      return SupportedArch.DarwinX64\n    case 'darwin-arm64':\n      return SupportedArch.DarwinArm64\n    case 'win32-x64':\n      return SupportedArch.WindowsX86_64\n    case 'win32-ia32':\n      return SupportedArch.WindowsX86_32\n    case 'win32-arm64':\n      return SupportedArch.WindowsArm64\n    case 'linux-x64':\n      return SupportedArch.LinuxAmd64\n    case 'linux-ia32':\n      return SupportedArch.LinuxI386\n    case 'linux-arm':\n      return SupportedArch.LinuxArmv7hf\n    case 'linux-arm64':\n      return SupportedArch.LinuxAarch64\n    case 'linux-armel':\n      return SupportedArch.LinuxArmv7\n    default:\n      throw new Error('Unsupported platform/architecture: ' + label)\n  }\n}\n\nexport const getClashInfo = ({\n  platform,\n  arch,\n  sidecarHost,\n}: {\n  platform: NodeJS.Platform\n  arch: NodeArch\n  sidecarHost?: string\n}): BinInfo => {\n  const { ARCH_MAPPING, URL_PREFIX, LATEST_DATE } = CLASH_MANIFEST\n  const archLabel = mappingArch(platform, arch)\n  const name = ARCH_MAPPING[archLabel].replace('{}', LATEST_DATE as string)\n\n  const isWin = platform === 'win32'\n\n  const downloadURL = `${URL_PREFIX}${name}`\n\n  const exeFile = `${name}${isWin ? '.exe' : ''}`\n\n  const tmpFile = `${name}`\n\n  const targetFile = `clash-${sidecarHost}${isWin ? '.exe' : ''}`\n\n  return {\n    name: 'clash',\n    targetFile,\n    exeFile,\n    tmpFile,\n    downloadURL,\n  }\n}\n\nexport const getClashBackupInfo = ({\n  platform,\n  arch,\n  sidecarHost,\n}: {\n  platform: NodeJS.Platform\n  arch: NodeArch\n  sidecarHost?: string\n}): BinInfo => {\n  const { ARCH_MAPPING, BACKUP_URL_PREFIX, BACKUP_LATEST_DATE } = CLASH_MANIFEST\n\n  const archLabel = mappingArch(platform, arch)\n  const name = ARCH_MAPPING[archLabel].replace(\n    '{}',\n    BACKUP_LATEST_DATE as string,\n  )\n  const isWin = platform === 'win32'\n\n  const downloadURL = `${BACKUP_URL_PREFIX}${BACKUP_LATEST_DATE}/${name}`\n\n  const exeFile = `${name}${isWin ? '.exe' : ''}`\n\n  const tmpFile = `${name}`\n\n  const targetFile = `clash-${sidecarHost}${isWin ? '.exe' : ''}`\n\n  return {\n    name: 'clash',\n    targetFile,\n    exeFile,\n    tmpFile,\n    downloadURL,\n  }\n}\n\nexport const getClashMetaInfo = ({\n  platform,\n  arch,\n  sidecarHost,\n}: {\n  platform: NodeJS.Platform\n  arch: NodeArch\n  sidecarHost?: string\n}): BinInfo => {\n  const { ARCH_MAPPING, URL_PREFIX, VERSION } = CLASH_META_MANIFEST\n  const archLabel = mappingArch(platform, arch)\n\n  const name = ARCH_MAPPING[archLabel].replace('{}', VERSION as string)\n\n  const isWin = platform === 'win32'\n\n  const downloadURL = `${URL_PREFIX}/${name}`\n\n  const exeFile = `${name}${isWin ? '.exe' : ''}`\n\n  const tmpFile = `${name}`\n\n  const targetFile = `mihomo-${sidecarHost}${isWin ? '.exe' : ''}`\n\n  return {\n    name: 'mihomo',\n    targetFile,\n    exeFile,\n    tmpFile,\n    downloadURL,\n  }\n}\n\nexport const getClashMetaAlphaInfo = async ({\n  platform,\n  arch,\n  sidecarHost,\n}: {\n  platform: NodeJS.Platform\n  arch: NodeArch\n  sidecarHost?: string\n}): Promise<BinInfo> => {\n  const { ARCH_MAPPING, URL_PREFIX } = CLASH_META_ALPHA_MANIFEST\n  const version = await getMetaAlphaLatestVersion()\n  const archLabel = mappingArch(platform as NodeJS.Platform, arch as NodeArch)\n  const name = ARCH_MAPPING[archLabel].replace('{}', version)\n  const isWin = platform === 'win32'\n  const downloadURL = `${URL_PREFIX}/${name}`\n\n  const exeFile = `${name}${isWin ? '.exe' : ''}`\n\n  const tmpFile = `${name}`\n\n  const targetFile = `mihomo-alpha-${sidecarHost}${isWin ? '.exe' : ''}`\n\n  return {\n    name: 'mihomo-alpha',\n    targetFile,\n    exeFile,\n    tmpFile,\n    downloadURL,\n  }\n}\n\nexport const getClashRustInfo = ({\n  platform,\n  arch,\n  sidecarHost,\n}: {\n  platform: string\n  arch: string\n  sidecarHost?: string\n}): BinInfo => {\n  const { ARCH_MAPPING, URL_PREFIX, VERSION } = CLASH_RS_MANIFEST\n\n  const archLabel = mappingArch(platform as NodeJS.Platform, arch as NodeArch)\n  const name = ARCH_MAPPING[archLabel].replace('{}', VERSION as string)\n\n  const isWin = platform === 'win32'\n\n  const exeFile = `${name}`\n\n  const downloadURL = `${URL_PREFIX}${VERSION}/${name}`\n\n  const tmpFile = `${name}`\n\n  const targetFile = `clash-rs-${sidecarHost}${isWin ? '.exe' : ''}`\n\n  return {\n    name: 'clash-rs',\n    targetFile,\n    exeFile,\n    tmpFile,\n    downloadURL,\n  }\n}\n\nexport const getClashRsAlphaLatestVersion = async () => {\n  const { VERSION_URL } = CLASH_RS_ALPHA_MANIFEST\n\n  try {\n    const opts = {} as Partial<RequestInit>\n\n    const httpProxy = getProxyAgent()\n\n    if (httpProxy) {\n      opts.dispatcher = httpProxy\n    }\n\n    const response = await fetch(VERSION_URL!, {\n      method: 'GET',\n      ...opts,\n    })\n\n    const v = (await response.text()).trim().split(' ').pop()!\n\n    consola.info(`Clash Rs Alpha latest release version: ${v}`)\n\n    return v.trim()\n  } catch (error) {\n    console.error('Error fetching latest release version:', error)\n\n    process.exit(1)\n  }\n}\n\nexport const getClashRustAlphaInfo = async ({\n  platform,\n  arch,\n  sidecarHost,\n}: {\n  platform: string\n  arch: string\n  sidecarHost?: string\n}): Promise<BinInfo> => {\n  const { ARCH_MAPPING, URL_PREFIX } = CLASH_RS_ALPHA_MANIFEST\n  const version = await getClashRsAlphaLatestVersion()\n  const archLabel = mappingArch(platform as NodeJS.Platform, arch as NodeArch)\n  const name = ARCH_MAPPING[archLabel].replace('{}', version as string)\n\n  const isWin = platform === 'win32'\n\n  const exeFile = `${name}`\n\n  const downloadURL = `${URL_PREFIX}/${name}`\n\n  const tmpFile = `${name}`\n\n  const targetFile = `clash-rs-alpha-${sidecarHost}${isWin ? '.exe' : ''}`\n\n  return {\n    name: 'clash-rs-alpha',\n    targetFile,\n    exeFile,\n    tmpFile,\n    downloadURL,\n  }\n}\n\nexport const getMetaAlphaLatestVersion = async () => {\n  const { VERSION_URL } = CLASH_META_ALPHA_MANIFEST\n\n  try {\n    const opts = {} as Partial<RequestInit>\n\n    const httpProxy = getProxyAgent()\n\n    if (httpProxy) {\n      opts.dispatcher = httpProxy\n    }\n\n    const response = await fetch(VERSION_URL!, {\n      method: 'GET',\n      ...opts,\n    })\n\n    const v = await response.text()\n\n    consola.info(`Mihomo Alpha latest release version: ${v}`)\n\n    return v.trim()\n  } catch (error) {\n    console.error('Error fetching latest release version:', error)\n\n    process.exit(1)\n  }\n}\n\nexport const getNyanpasuServiceLatestVersion = async () => {\n  try {\n    const opts = {} as Partial<RequestInit>\n\n    const httpProxy = getProxyAgent()\n    if (httpProxy) {\n      opts.dispatcher = httpProxy\n    }\n\n    const url = new URL('https://github.com')\n    url.pathname = `/${SERVICE_REPO}/releases/latest`\n    const response = await fetch(url, {\n      method: 'GET',\n      redirect: 'manual',\n      ...opts,\n    })\n\n    const location = response.headers.get('location')\n    if (!location) {\n      throw new Error('Cannot find location from the response header')\n    }\n    const tag = location.split('/').pop()\n    if (!tag) {\n      throw new Error('Cannot find tag from the location')\n    }\n    consola.info(`Nyanpasu Service latest release version: ${tag}`)\n    return tag.trim()\n  } catch (error) {\n    console.error('Error fetching latest release version:', error)\n    process.exit(1)\n  }\n}\n\nexport const getNyanpasuServiceInfo = async ({\n  sidecarHost,\n}: {\n  sidecarHost: string\n}): Promise<BinInfo> => {\n  const name = `nyanpasu-service`\n  const isWin = SIDECAR_HOST?.includes('windows')\n  const urlExt = isWin ? 'zip' : 'tar.gz'\n  // first we had to get the latest tag\n  const version = await getNyanpasuServiceLatestVersion()\n  const downloadURL = `https://github.com/${SERVICE_REPO}/releases/download/${version}/${name}-${sidecarHost}.${urlExt}`\n  const exeFile = `${name}${isWin ? '.exe' : ''}`\n  const tmpFile = `${name}-${sidecarHost}.${urlExt}`\n  const targetFile = `nyanpasu-service-${sidecarHost}${isWin ? '.exe' : ''}`\n  return {\n    name: 'nyanpasu-service',\n    targetFile,\n    exeFile,\n    tmpFile,\n    downloadURL,\n  }\n}\n"
  },
  {
    "path": "scripts/utils/shell.ts",
    "content": "import { execSync } from 'child_process'\n\nexport const GIT_SHORT_HASH = execSync('git rev-parse --short HEAD')\n  .toString()\n  .trim()\n"
  },
  {
    "path": "scripts/utils/telegram.ts",
    "content": "import { TelegramClient } from 'telegram'\nimport { StringSession } from 'telegram/sessions'\n\nif (!process.env.TELEGRAM_API_ID) {\n  throw new Error('TELEGRAM_API_ID is required')\n}\n\nconst TELEGRAM_API_ID = Number(process.env.TELEGRAM_API_ID)\n\nif (!process.env.TELEGRAM_API_HASH) {\n  throw new Error('TELEGRAM_API_ID is required')\n}\n\nconst TELEGRAM_API_HASH = process.env.TELEGRAM_API_HASH\n\nexport const client = new TelegramClient(\n  new StringSession(''),\n  TELEGRAM_API_ID,\n  TELEGRAM_API_HASH,\n  {\n    connectionRetries: 5,\n  },\n)\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"skipLibCheck\": true,\n    \"esModuleInterop\": true,\n    \"composite\": true,\n    \"paths\": {\n      \"@nyanpasu/ui/*\": [\"./frontend/ui/*\"],\n      \"@nyanpasu/nyanpasu/*\": [\"./frontend/nyanpasu/*\"],\n      \"@nyanpasu/interface/*\": [\"./frontend/interface/*\"],\n    },\n  },\n  \"references\": [\n    { \"path\": \"./frontend/ui\" },\n    { \"path\": \"./frontend/nyanpasu\" },\n    { \"path\": \"./frontend/interface\" },\n    { \"path\": \"./scripts\" },\n  ],\n}\n"
  }
]