[
  {
    "path": ".github/ISSUE_TEMPLATE/bug.yaml",
    "content": "name: 🐛 Bug report | 错误报告 | BUG報告\ndescription: Create a bug report to help us improve | 创建bug报告以帮助我们改进 | 改善を支援するためのレポートを作成する\ntitle: '[Bug] '\nlabels: ['bug']\nbody:\n  - type: checkboxes\n    id: checks\n    attributes:\n      label: Please carefully review each item in the checklist below | 请认真检查以下清单中的每一项 | 以下のチェックリストの各項目を注意深く確認してください\n      options:\n        - label: Searched and didn't find a similar issue | 已经搜索过，没有发现类似issue | 類似の問題が見つかりませんでした\n        - label: Searched documentation and didn't find relevant content | 已经搜索过文档，没有发现相关内容 | ドキュメントを検索して関連する内容が見つかりませんでした\n        - label: Tried with the latest version and the issue still exists | 已经尝试使用过最新版，问题依旧存在 | 最新バージョンを試しましたが問題は解消されませんでした\n  - type: input\n    id: app-version\n    attributes:\n      label: Software Version | 软件版本 | ソフトウェアバージョン\n      placeholder: '1.1.4'\n    validations:\n      required: true\n  - type: dropdown\n    id: system-type\n    attributes:\n      label: Operating System | 操作系统 | オペレーティングシステム\n      options:\n        - Windows x64\n        - Windows arm64\n        - macOS x64 (Intel)\n        - macOS arm64 (M1,M2...)\n        - Ubuntu x64\n        - Debian x64\n        - Arch Linux x64\n        - Other Linux x64\n    validations:\n      required: true\n  - type: input\n    id: system-version\n    attributes:\n      label: System Version | 系统版本 | システムバージョン\n    validations:\n      required: true\n  - type: textarea\n    id: description\n    attributes:\n      label: Describe the bug | 描述错误 | BUGの説明\n      description: |\n        A clear and concise description of what the bug is\n        描述错误的详细信息\n        バグの内容を明確かつ簡潔に説明してください\n    validations:\n      required: true\n  - type: textarea\n    id: reproduce-steps\n    attributes:\n      label: To reproduce | 复现步骤 | 再現方法\n      description: Steps to reproduce the behavior | 复现行为的步骤 | 不具合の再現手順\n      value: |\n        1. Go to '...'\n        2. Click on '....'\n        3. See error\n    validations:\n      required: true\n  - type: textarea\n    id: log\n    attributes:\n      label: Error log | 报错日志 | ログ\n      description: your error log | 您的错误日志 | エラーログ\n      value: |\n        ```\n        your error log\n        ```\n    validations:\n      required: true\n  - type: textarea\n    id: other\n    attributes:\n      label: Additional context | 附加内容 | 追加コンテキスト\n      description: |\n        Add any other context and screenshots to help explain your problem\n        添加任何其他上下文和截图，以帮助解释您的问题\n        問題を説明するために他の文脈やスクリーンショットを追加してください\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature.yaml",
    "content": "name: 🚀 Feature request | 功能请求 | フィーチャーリクエスト\ndescription: Suggest an idea for this project ｜ 为项目提供一个创意建议 ｜ このプロジェクトにアイデアを提案する\ntitle: '[FEATURE] '\nlabels: ['enhancement']\nbody:\n  - type: checkboxes\n    id: checks\n    attributes:\n      label: Please carefully review each item in the checklist below | 请认真检查以下清单中的每一项 | 以下のチェックリストの各項目を注意深く確認してください\n      options:\n        - label: Searched and didn't find a similar issue | 已经搜索过，没有发现类似issue | 類似の問題が見つかりませんでした\n  - type: textarea\n    id: is-related\n    attributes:\n      label: Is your feature request related to a problem? | 你的feature请求是否与一个问题有关？ | あなたのfeatureリクエストは質問に関連していますか？\n      description: |\n        A clear and concise description of what the problem is\n        请清楚而简明地描述问题是什么\n        問題が何であるかを明確かつ簡潔に説明してください\n    validations:\n      required: true\n  - type: textarea\n    id: detail\n    attributes:\n      label: Detail | 详细描述 | 詳細な説明\n      description: |\n        A clear and concise description of what you want to happen\n        请清楚而简明地描述您想要实现的内容\n        実現したい内容を明確かつ簡潔に説明してください\n    validations:\n      required: true\n  - type: textarea\n    id: log\n    attributes:\n      label: Additional context | 附加内容 | 追加コンテキスト\n      description: |\n        Add any other context or screenshots about the feature request here\n        在这里添加任何其他上下文或截图，以帮助解释您的功能请求\n        その他の文脈やスクリーンショットを追加して、機能リクエストについて説明してください\n"
  },
  {
    "path": ".github/workflows/CI-build.yml",
    "content": "name: CI-build\n\non:\n  push:\n    branches:\n      - main\n    paths-ignore:\n      - '**.md'\n      - LICENSE\n  pull_request:\n    paths-ignore:\n      - '**.md'\n      - LICENSE\n  workflow_dispatch:\n\njobs:\n  windows:\n    strategy:\n      matrix:\n        os-version: ['x64', 'arm64']\n\n    runs-on: windows-latest\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          submodules: recursive\n\n      - uses: actions/setup-node@v4\n        with:\n          node-version: 22\n\n      - uses: pnpm/action-setup@v4\n        with:\n          version: 10\n\n      - name: build\n        run: |\n          pnpm install\n          pnpm fetchcore\n          pnpm run build:win-${{ matrix.os-version }}\n        env:\n          GH_TOKEN: ${{ secrets.GH_TOKEN }}\n\n      - name: zip-unpacked-x64\n        if: matrix.os-version == 'x64'\n        run: |\n          cd .\\dist\\win-unpacked\n          7z a -r Final2x-windows-${{ matrix.os-version }}-unpacked.7z *\n\n      - name: zip-unpacked-arm64\n        if: matrix.os-version == 'arm64'\n        run: |\n          cd .\\dist\\win-arm64-unpacked\n          7z a -r Final2x-windows-${{ matrix.os-version }}-unpacked.7z *\n\n      - name: upload-unpacked-x64\n        if: matrix.os-version == 'x64'\n        uses: actions/upload-artifact@v4\n        with:\n          name: Final2x-windows-${{ matrix.os-version }}-unpacked\n          path: dist/win-unpacked/*.7z\n\n      - name: upload-unpacked-arm64\n        if: matrix.os-version == 'arm64'\n        uses: actions/upload-artifact@v4\n        with:\n          name: Final2x-windows-${{ matrix.os-version }}-unpacked\n          path: dist/win-arm64-unpacked/*.7z\n\n  macos:\n    strategy:\n      matrix:\n        os-version: ['arm64']\n\n    runs-on: macos-14\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          submodules: recursive\n\n      - uses: actions/setup-node@v4\n        with:\n          node-version: 22\n\n      - uses: pnpm/action-setup@v4\n        with:\n          version: 10\n\n      - name: build\n        run: |\n          pnpm install\n          pnpm fetchcore\n          pnpm run build:mac-${{ matrix.os-version }}\n        env:\n          ARCH: ${{ matrix.os-version }}\n          GH_TOKEN: ${{ secrets.GH_TOKEN }}\n\n      - name: zip-unpacked-arm64\n        if: matrix.os-version == 'arm64'\n        run: |\n          cd ./dist/mac-arm64\n          7z a -r Final2x-macos-${{ matrix.os-version }}-unpacked.7z *\n\n      - name: upload-dmg\n        uses: actions/upload-artifact@v4\n        with:\n          name: Final2x-macos-${{ matrix.os-version }}-dmg\n          path: dist/*.dmg\n\n      - name: upload-unpacked-arm64\n        if: matrix.os-version == 'arm64'\n        uses: actions/upload-artifact@v4\n        with:\n          name: Final2x-macos-${{ matrix.os-version }}-unpacked\n          path: dist/mac-arm64/*.7z\n\n  linux-pip:\n    strategy:\n      matrix:\n        os-version: ['x64']\n\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          submodules: recursive\n\n      - uses: actions/setup-node@v4\n        with:\n          node-version: 22\n\n      - uses: pnpm/action-setup@v4\n        with:\n          version: 10\n\n      - name: build\n        run: |\n          pnpm install\n          pnpm run build:linux-${{ matrix.os-version }}\n        env:\n          SKIP_DOWNLOAD_CORE: true\n          GH_TOKEN: ${{ secrets.GH_TOKEN }}\n\n      - name: zip-unpacked\n        run: |\n          cd ./dist/linux-unpacked\n          7z a -r Final2x-linux-pip-${{ matrix.os-version }}-unpacked.7z *\n\n      - name: upload-snap\n        uses: actions/upload-artifact@v4\n        with:\n          name: Final2x-linux-pip-${{ matrix.os-version }}-snap\n          path: dist/*.snap\n\n      - name: upload-AppImage\n        uses: actions/upload-artifact@v4\n        with:\n          name: Final2x-linux-pip-${{ matrix.os-version }}-AppImage\n          path: dist/*.AppImage\n\n      - name: upload-deb\n        uses: actions/upload-artifact@v4\n        with:\n          name: Final2x-linux-pip-${{ matrix.os-version }}-deb\n          path: dist/*.deb\n\n      - name: upload-unpacked\n        uses: actions/upload-artifact@v4\n        with:\n          name: Final2x-linux-pip-${{ matrix.os-version }}-unpacked\n          path: dist/linux-unpacked/*.7z\n"
  },
  {
    "path": ".github/workflows/CI-test.yml",
    "content": "name: CI-test\n\non:\n  push:\n    branches:\n      - main\n    paths-ignore:\n      - '**.md'\n      - LICENSE\n  pull_request:\n    paths-ignore:\n      - '**.md'\n      - LICENSE\n  workflow_dispatch:\n\njobs:\n  test:\n    strategy:\n      matrix:\n        os-version: ['ubuntu-latest']\n\n    runs-on: ${{ matrix.os-version }}\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          submodules: recursive\n\n      - uses: actions/setup-node@v4\n        with:\n          node-version: 22\n\n      - uses: pnpm/action-setup@v4\n        with:\n          version: 10\n\n      - name: Test\n        run: |\n          pnpm install\n          pnpm run lint\n          pnpm run typecheck\n          pnpm run test\n        env:\n          SKIP_DOWNLOAD_CORE: true\n          GH_TOKEN: ${{ secrets.GH_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/Release.yml",
    "content": "name: Release\n\non:\n  workflow_dispatch:\n  push:\n    tags:\n      - 'v*'\n\njobs:\n  windows:\n    strategy:\n      matrix:\n        os-version: ['x64', 'arm64']\n\n    runs-on: windows-latest\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          submodules: recursive\n\n      - uses: actions/setup-node@v4\n        with:\n          node-version: 22\n\n      - uses: pnpm/action-setup@v4\n        with:\n          version: 10\n\n      - name: build\n        run: |\n          pnpm install\n          pnpm fetchcore\n          pnpm run build:win-${{ matrix.os-version }}\n        env:\n          GH_TOKEN: ${{ secrets.GH_TOKEN }}\n\n      - name: zip-unpacked-x64\n        if: matrix.os-version == 'x64'\n        run: |\n          cd .\\dist\\win-unpacked\n          7z a -r Final2x-windows-${{ matrix.os-version }}-unpacked.7z *\n\n      - name: zip-unpacked-arm64\n        if: matrix.os-version == 'arm64'\n        run: |\n          cd .\\dist\\win-arm64-unpacked\n          7z a -r Final2x-windows-${{ matrix.os-version }}-unpacked.7z *\n\n      - name: upload-unpacked-x64\n        if: matrix.os-version == 'x64'\n        uses: actions/upload-artifact@v4\n        with:\n          name: Final2x-windows-${{ matrix.os-version }}-unpacked\n          path: dist/win-unpacked/*.7z\n\n      - name: upload-unpacked-arm64\n        if: matrix.os-version == 'arm64'\n        uses: actions/upload-artifact@v4\n        with:\n          name: Final2x-windows-${{ matrix.os-version }}-unpacked\n          path: dist/win-arm64-unpacked/*.7z\n\n  macos:\n    strategy:\n      matrix:\n        os-version: ['arm64']\n\n    runs-on: macos-14\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          submodules: recursive\n\n      - uses: actions/setup-node@v4\n        with:\n          node-version: 22\n\n      - uses: pnpm/action-setup@v4\n        with:\n          version: 10\n\n      - name: build\n        run: |\n          pnpm install\n          pnpm fetchcore\n          pnpm run build:mac-${{ matrix.os-version }}\n        env:\n          ARCH: ${{ matrix.os-version }}\n          GH_TOKEN: ${{ secrets.GH_TOKEN }}\n\n      - name: rename\n        run: |\n          cd ./dist\n          mv *.dmg Final2x-macos-${{ matrix.os-version }}-dmg.dmg\n\n      - name: zip-unpacked-arm64\n        if: matrix.os-version == 'arm64'\n        run: |\n          cd ./dist/mac-arm64\n          7z a -r Final2x-macos-${{ matrix.os-version }}-unpacked.7z *\n\n      - name: upload-dmg\n        uses: actions/upload-artifact@v4\n        with:\n          name: Final2x-macos-${{ matrix.os-version }}-dmg\n          path: dist/*.dmg\n\n      - name: upload-unpacked-arm64\n        if: matrix.os-version == 'arm64'\n        uses: actions/upload-artifact@v4\n        with:\n          name: Final2x-macos-${{ matrix.os-version }}-unpacked\n          path: dist/mac-arm64/*.7z\n\n  linux-pip:\n    strategy:\n      matrix:\n        os-version: ['x64']\n\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          submodules: recursive\n\n      - uses: actions/setup-node@v4\n        with:\n          node-version: 22\n\n      - uses: pnpm/action-setup@v4\n        with:\n          version: 10\n\n      - name: build\n        run: |\n          pnpm install\n          pnpm run build:linux-${{ matrix.os-version }}\n        env:\n          SKIP_DOWNLOAD_CORE: true\n          GH_TOKEN: ${{ secrets.GH_TOKEN }}\n\n      - name: zip-unpacked\n        run: |\n          cd ./dist/linux-unpacked\n          7z a -r Final2x-linux-pip-${{ matrix.os-version }}-unpacked.7z *\n\n      - name: rename\n        run: |\n          cd ./dist\n          mv *.snap Final2x-linux-pip-${{ matrix.os-version }}-snap.snap\n          mv *.AppImage Final2x-linux-pip-${{ matrix.os-version }}-AppImage.AppImage\n          mv *.deb Final2x-linux-pip-${{ matrix.os-version }}-deb.deb\n\n      - name: upload-snap\n        uses: actions/upload-artifact@v4\n        with:\n          name: Final2x-linux-pip-${{ matrix.os-version }}-snap\n          path: dist/*.snap\n\n      - name: upload-AppImage\n        uses: actions/upload-artifact@v4\n        with:\n          name: Final2x-linux-pip-${{ matrix.os-version }}-AppImage\n          path: dist/*.AppImage\n\n      - name: upload-deb\n        uses: actions/upload-artifact@v4\n        with:\n          name: Final2x-linux-pip-${{ matrix.os-version }}-deb\n          path: dist/*.deb\n\n      - name: upload-unpacked\n        uses: actions/upload-artifact@v4\n        with:\n          name: Final2x-linux-pip-${{ matrix.os-version }}-unpacked\n          path: dist/linux-unpacked/*.7z\n\n  github:\n    needs: [windows, macos, linux-pip]\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/download-artifact@v4\n        with:\n          path: asset\n\n      - name: Flatten asset directory\n        run: |\n          tree asset\n          mkdir dist\n          find asset -type f -print0 | xargs -0 -I{} cp \"{}\" dist/\n          cd dist && ls -l\n\n      - name: Create Release and Upload Release Asset\n        uses: softprops/action-gh-release@v2\n        with:\n          files: dist/*\n"
  },
  {
    "path": ".github/workflows/issue-helper.yml",
    "content": "name: issue-helper\n\non:\n  issues:\n    types: [opened, reopened, edited]\n\njobs:\n  check-inactive:\n    runs-on: ubuntu-latest\n    steps:\n      - name: close-issues\n        uses: actions-cool/issues-helper@v2\n        with:\n          actions: 'close-issues'\n          token: ${{ secrets.GH_TOKEN }}\n          inactive-day: 100\n          body: |\n            Hello! your issue has been closed because it has been inactive for a long time.\n\n            你好，你的 issue 因为长时间不活跃而被自动关闭。\n\n            こんにちは、お問い合わせは長期間活動がないため、閉じられました。\n\n  check-title:\n    runs-on: ubuntu-latest\n    if: github.event.issue.title == '[BUG] ' || github.event.issue.title == '[FEATURE] ' || (contains(github.event.issue.title, '[BUG]') == false && contains(github.event.issue.title, '[FEATURE]') == false)\n    steps:\n      - name: close issue\n        uses: actions-cool/issues-helper@v3\n        with:\n          actions: 'create-comment, add-labels, close-issue'\n          token: ${{ secrets.GH_TOKEN }}\n          issue-number: ${{ github.event.issue.number }}\n          labels: 'Invalid'\n          body: |\n            Hello @${{ github.event.issue.user.login }}, your issue has been closed because the title does not conform to our specification.\n\n            你好 @${{ github.event.issue.user.login }}，为了能够进行高效沟通，我们对 issue 有一定的格式要求，你的 issue 因为标题不符合规范而被自动关闭。\n\n            こんにちは、@${{ github.event.issue.user.login }}さん、タイトルが仕様に準拠していないため、ご提案いただいた問題はクローズされました。\n"
  },
  {
    "path": ".github/workflows/issue-translator.yml",
    "content": "name: 'issue-translator'\non:\n  issue_comment:\n    types: [created]\n  issues:\n    types: [opened]\n\njobs:\n  translate:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: usthe/issues-translate-action@v2.7\n        with:\n          CUSTOM_BOT_NOTE: Bot detected the issue body's language is not English, translate it automatically.\n"
  },
  {
    "path": ".gitignore",
    "content": "node_modules\ndist\nout\n*.log*\n*.DS_Store\n/resources/Final2x-core/\n/outputs/\n/.idea\n/coverage/\n"
  },
  {
    "path": ".npmrc",
    "content": "shamefully-hoist=true"
  },
  {
    "path": "LICENSE",
    "content": "BSD 3-Clause License\n\nCopyright (c) 2023, Tohrusky\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n1. Redistributions of source code must retain the above copyright notice, this\n   list of conditions and the following disclaimer.\n\n2. Redistributions in binary form must reproduce the above copyright notice,\n   this list of conditions and the following disclaimer in the documentation\n   and/or other materials provided with the distribution.\n\n3. Neither the name of the copyright holder nor the names of its\n   contributors may be used to endorse or promote products derived from\n   this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "README.md",
    "content": "# Final2x\n\n<div align=\"center\">\n<img src=\"./resources/icon.png\" width=\"30%\"/>\n</div>\n\n![MacOS](https://img.shields.io/badge/Support-MacOS-blue?logo=Apple&style=flat-square)\n![Windows](https://img.shields.io/badge/Support-Windows-blue?logo=Windows&style=flat-square)\n![Linux](https://img.shields.io/badge/Support-Linux-blue?logo=Linux&style=flat-square)\n[![CI-test](https://github.com/EutropicAI/Final2x/actions/workflows/CI-test.yml/badge.svg)](https://github.com/EutropicAI/Final2x/actions/workflows/CI-test.yml)\n[![CI-build](https://github.com/EutropicAI/Final2x/actions/workflows/CI-build.yml/badge.svg)](https://github.com/EutropicAI/Final2x/actions/workflows/CI-build.yml)\n[![Release](https://github.com/EutropicAI/Final2x/actions/workflows/Release.yml/badge.svg)](https://github.com/EutropicAI/Final2x/actions/workflows/Release.yml)\n![Download](https://img.shields.io/github/downloads/EutropicAI/Final2x/total)\n![GitHub](https://img.shields.io/github/license/EutropicAI/Final2x)\n\nA cross-platform image super-resolution tool.\n\n- News🎉: Final2x v4.0.0 is now available! It uses the [cccv](https://github.com/EutropicAI/cccv) backend, supporting custom models and more. See [custom model demo](https://github.com/EutropicAI/cccv_demo_remote_model).\n- News🎉: Final2x v3.0.0 is now available, support Nvidia 50 series GPUs now!\n\n### Screenshots\n\n<div align=center>\n<img width=\"40%\" alt=\"image\" src=\"https://github.com/user-attachments/assets/37f6d444-766b-4c28-b64a-018f78ae1f35\" />\n<img width=\"40%\" alt=\"image\" src=\"https://github.com/user-attachments/assets/c6a278c0-bf11-46a7-9dcc-e5fe97ccc71c\" />\n</div>\n\n### Installation\n\n##### [Download the latest release from here.](https://github.com/EutropicAI/Final2x/releases)\n\n#### Windows\n\nYou can also use a package manager like winget or scoop to install and upgrade. Please note that the versions available through package managers may not always be the latest.\n\n#### MacOS\n\n```bash\nsudo spctl --master-disable\n# Disable Gatekeeper, then allow applications downloaded from anywhere in System Preferences > Security & Privacy > General\nxattr -cr /Applications/Final2x.app\n```\n\nIn first time, you need to run the command above in terminal to allow the app to run.\n\n#### Linux\n\nFor Linux User, you need to install the dependencies first.\n\nMake sure you have Python >= 3.9 and PyTorch >= 2.0 installed\n\n```bash\npip install Final2x-core\nFinal2x-core -h # check if the installation is successful\napt install -y libomp5 xdg-utils\n```\n\n### Reference\n\nThe following references were referenced in the development of this project:\n\n- [Final2x-core](https://github.com/EutropicAI/Final2x-core)\n- [naive-ui](https://github.com/tusen-ai/naive-ui)\n- [electron-vite](https://github.com/alex8088/electron-vite)\n\n### License\n\nThis project is licensed under the BSD 3-Clause - see\nthe [LICENSE file](./LICENSE) for details.\n\n### Acknowledgements\n\nFeel free to reach out to the project maintainers with any questions or concerns~\n\n<a href=\"https://star-history.com/#EutropicAI/Final2x&Date\">\n  <picture>\n    <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://api.star-history.com/svg?repos=EutropicAI/Final2x&type=Date&theme=dark\" />\n    <source media=\"(prefers-color-scheme: light)\" srcset=\"https://api.star-history.com/svg?repos=EutropicAI/Final2x&type=Date\" />\n    <img alt=\"Star History Chart\" src=\"https://api.star-history.com/svg?repos=EutropicAI/Final2x&type=Date\" />\n  </picture>\n</a>\n"
  },
  {
    "path": "README_i18n/README_zh.md",
    "content": "# Final2x\n"
  },
  {
    "path": "build/entitlements.mac.plist",
    "content": "<?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>com.apple.security.cs.allow-jit</key>\n    <true/>\n    <key>com.apple.security.cs.allow-unsigned-executable-memory</key>\n    <true/>\n    <key>com.apple.security.cs.allow-dyld-environment-variables</key>\n    <true/>\n  </dict>\n</plist>\n"
  },
  {
    "path": "build/notarize.js",
    "content": "module.exports = async (context) => {\n  const { notarize } = require('@electron/notarize')\n\n  if (process.platform !== 'darwin')\n    return\n\n  console.log('aftersign hook triggered, start to notarize app.')\n\n  if (!process.env.CI) {\n    console.log(`skipping notarizing, not in CI.`)\n    return\n  }\n\n  if (!('APPLE_ID' in process.env && 'APPLE_ID_PASS' in process.env)) {\n    console.warn('skipping notarizing, APPLE_ID and APPLE_ID_PASS env variables must be set.')\n    return\n  }\n\n  const appId = 'com.final2x.app'\n\n  const { appOutDir } = context\n\n  const appName = context.packager.appInfo.productFilename\n\n  try {\n    await notarize({\n      appBundleId: appId,\n      appPath: `${appOutDir}/${appName}.app`,\n      appleId: process.env.APPLE_ID,\n      appleIdPassword: process.env.APPLEIDPASS,\n    })\n  }\n  catch (error) {\n    console.error(error)\n  }\n\n  console.log(`done notarizing ${appId}.`)\n}\n"
  },
  {
    "path": "electron-builder.yml",
    "content": "appId: com.final2x.app\nproductName: Final2x\ndirectories:\n  buildResources: build\n\nicon: resources/icon.png\n\nfiles:\n  - '!**/.vscode/*'\n  - '!src/*'\n  - '!electron.vite.config.{js,ts,mjs,cjs}'\n  - '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}'\n  - '!{.env,.env.*,.npmrc,pnpm-lock.yaml}'\n  - '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}'\n  - '!resources/Final2x-core/**'\n\nasarUnpack:\n  - resources/*.png\n  - resources/*.svg\n  - resources/*.ico\n\nextraResources:\n  - from: resources/Final2x-core\n    to: Final2x-core\n\nafterSign: build/notarize.js\n\nwin:\n  executableName: Final2x\n\nnsis:\n  artifactName: ${name}-${version}-setup.${ext}\n  shortcutName: ${productName}\n  uninstallDisplayName: ${productName}\n  createDesktopShortcut: always\n\nmac:\n  entitlementsInherit: build/entitlements.mac.plist\n  extendInfo:\n    - NSCameraUsageDescription: Application requests access to the device's camera.\n    - NSMicrophoneUsageDescription: Application requests access to the device's microphone.\n    - NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.\n    - NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.\n\ndmg:\n  artifactName: ${name}-${version}.${ext}\n  background: build/macosDMGbg.jpeg\n  window:\n    x: 100\n    y: 100\n    width: 480\n    height: 500\n\nlinux:\n  target:\n    - AppImage\n    - snap\n    - deb\n  maintainer: Tohrusky\n  category: Utility\n\nappImage:\n  artifactName: ${name}-${version}.${ext}\n\nnpmRebuild: false\n"
  },
  {
    "path": "electron.vite.config.ts",
    "content": "import { resolve } from 'node:path'\nimport vue from '@vitejs/plugin-vue'\nimport { defineConfig, externalizeDepsPlugin } from 'electron-vite'\n\nexport default defineConfig({\n  main: {\n    resolve: {\n      alias: {\n        '@main': resolve('src/main'),\n        '@shared': resolve('src/shared'),\n      },\n    },\n    plugins: [externalizeDepsPlugin()],\n  },\n  preload: {\n    plugins: [externalizeDepsPlugin()],\n  },\n  renderer: {\n    resolve: {\n      alias: {\n        '@renderer': resolve('src/renderer/src'),\n        '@shared': resolve('src/shared'),\n      },\n    },\n    plugins: [vue()],\n  },\n})\n"
  },
  {
    "path": "eslint.config.js",
    "content": "import antfu from '@antfu/eslint-config'\n\nexport default antfu(\n  {\n    ignores: [\n      'dist',\n      'out',\n      'node_modules',\n      'build/*.js',\n      'resources/*.js',\n    ],\n    rules: {\n      'no-console': 'off',\n    },\n  },\n  {\n    files: ['**/*.md'],\n    rules: {\n      'style/no-trailing-spaces': 'off',\n    },\n  },\n  {\n    files: ['**/*.yaml', '**/*.yml'],\n    rules: {\n      'yaml/plain-scalar': 'off',\n    },\n  },\n  {\n    files: ['**/*.ts'],\n    rules: {\n      '@typescript-eslint/ban-ts-comment': ['error', { 'ts-ignore': 'allow-with-description' }],\n      '@typescript-eslint/explicit-function-return-type': 'error',\n      '@typescript-eslint/explicit-module-boundary-types': 'off',\n      '@typescript-eslint/no-empty-function': ['error', { allow: ['arrowFunctions'] }],\n      '@typescript-eslint/no-explicit-any': ['off'],\n      '@typescript-eslint/no-non-null-assertion': 'off',\n      '@typescript-eslint/no-var-requires': 'off',\n      '@typescript-eslint/no-inferrable-types': 'off',\n      'node/prefer-global/process': 'off',\n    },\n  },\n  {\n    files: ['**/*.vue'],\n    rules: {\n      '@typescript-eslint/ban-ts-comment': ['error', { 'ts-ignore': 'allow-with-description' }],\n      '@typescript-eslint/explicit-function-return-type': 'error',\n      '@typescript-eslint/explicit-module-boundary-types': 'off',\n      '@typescript-eslint/no-empty-function': ['error', { allow: ['arrowFunctions'] }],\n      '@typescript-eslint/no-explicit-any': ['off'],\n      '@typescript-eslint/no-non-null-assertion': 'off',\n      '@typescript-eslint/no-var-requires': 'off',\n      '@typescript-eslint/no-inferrable-types': 'off',\n      'vue/require-default-prop': 'off',\n      'vue/multi-word-component-names': 'off',\n    },\n  },\n)\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"Final2x\",\n  \"productName\": \"Final2x\",\n  \"version\": \"4.0.0\",\n  \"description\": \"A cross-platform image super-resolution tool.\",\n  \"author\": \"Tohrusky\",\n  \"homepage\": \"https://github.com/EutropicAI/Final2x\",\n  \"main\": \"./out/main/index.js\",\n  \"engines\": {\n    \"node\": \">=18\",\n    \"pnpm\": \">=8\"\n  },\n  \"scripts\": {\n    \"dev\": \"electron-vite dev\",\n    \"test\": \"vitest run --coverage\",\n    \"lint\": \"eslint . --fix\",\n    \"typecheck:node\": \"tsc --noEmit -p tsconfig.node.json --composite false\",\n    \"typecheck:web\": \"vue-tsc --noEmit -p tsconfig.web.json --composite false\",\n    \"typecheck\": \"pnpm run typecheck:node && npm run typecheck:web\",\n    \"start\": \"electron-vite preview\",\n    \"build\": \"electron-vite build\",\n    \"postinstall\": \"electron-builder install-app-deps\",\n    \"fetchcore\": \"node ./resources/download-core.js\",\n    \"build:mac-arm64\": \"pnpm run build && electron-builder --mac --arm64 --publish=never\",\n    \"build:mac-x64\": \"pnpm run build && electron-builder --mac --x64 --publish=never\",\n    \"build:win-arm64\": \"pnpm run build && electron-builder --win --arm64 --dir --publish=never\",\n    \"build:win-x64\": \"pnpm run build && electron-builder --win --x64 --dir --publish=never\",\n    \"build:linux-x64\": \"pnpm run build && electron-builder --linux --x64 --publish=never\",\n    \"build:linux-arm64\": \"pnpm run build && electron-builder --linux --arm64 --publish=never\"\n  },\n  \"dependencies\": {\n    \"@intlify/unplugin-vue-i18n\": \"^6.0.8\",\n    \"@vicons/antd\": \"^0.13.0\",\n    \"@vicons/ionicons5\": \"^0.13.0\",\n    \"naive-ui\": \"^2.43.1\",\n    \"pinia\": \"^3.0.3\",\n    \"pinia-plugin-persistedstate\": \"^4.5.0\",\n    \"sass\": \"^1.93.2\",\n    \"systeminformation\": \"^5.30.8\",\n    \"tree-kill\": \"^1.2.2\",\n    \"vfonts\": \"^0.0.3\",\n    \"vue\": \"^3.5.22\",\n    \"vue-i18n\": \"^11.1.12\",\n    \"vue-router\": \"^4.5.1\"\n  },\n  \"devDependencies\": {\n    \"@antfu/eslint-config\": \"^5.4.1\",\n    \"@electron-toolkit/preload\": \"^3.0.2\",\n    \"@electron-toolkit/tsconfig\": \"^1.0.1\",\n    \"@electron-toolkit/utils\": \"^4.0.0\",\n    \"@electron/notarize\": \"^2.5.0\",\n    \"@vitejs/plugin-vue\": \"^5.2.4\",\n    \"@vitest/coverage-v8\": \"^3.2.4\",\n    \"@vue/test-utils\": \"^2.4.6\",\n    \"electron\": \"^27.3.11\",\n    \"electron-builder\": \"^26.0.12\",\n    \"electron-vite\": \"^4.0.1\",\n    \"eslint\": \"^9.37.0\",\n    \"extract-zip\": \"^2.0.1\",\n    \"jsdom\": \"^26.1.0\",\n    \"node-fetch\": \"^3.3.2\",\n    \"typescript\": \"^5.9.3\",\n    \"vite\": \"^7.1.11\",\n    \"vite-tsconfig-paths\": \"^5.1.4\",\n    \"vitest\": \"^3.2.4\",\n    \"vue-tsc\": \"^3.1.0\"\n  },\n  \"pnpm\": {\n    \"onlyBuiltDependencies\": [\n      \"electron\"\n    ],\n    \"overrides\": {\n      \"@parcel/watcher\": \"npm:empty-npm-package@1.0.0\"\n    }\n  }\n}\n"
  },
  {
    "path": "resources/download-core.js",
    "content": "// download Final2x-core from https://github.com/EutropicAI/Final2x-core/releases\n// and put it in resources folder\n\nconst fetch = (...args) => import('node-fetch').then(({ default: fetch }) => fetch(...args))\nconst child_process = require('node:child_process')\nconst fs = require('node:fs')\n\nconst path = require('node:path')\n\nconst coreDict = {\n  'macos-arm64':\n    'https://github.com/EutropicAI/Final2x-core/releases/download/v4.0.0/Final2x-core-macos-arm64.7z',\n  'windows-x64':\n    'https://github.com/EutropicAI/Final2x-core/releases/download/v4.0.0/Final2x-core-windows-x64.7z',\n}\n\nconsole.log('-'.repeat(50))\n\n// 判断当前平台\nconst PLATFORM = process.env.PLATFORM || process.platform\n// 判断当前平台架构\nconst ARCH = process.env.ARCH || process.arch\nconsole.log(`Platform: ${PLATFORM}`, `| Arch: ${ARCH}`)\nif (process.env.SKIP_DOWNLOAD_CORE) {\n  console.log('Skip download Final2x-core by env SKIP_DOWNLOAD_CORE')\n  process.exit(0)\n}\n\nasync function downloadAndUnzip(url, targetPath) {\n  const zipFileName = path.basename(url)\n  const zipFilePath = path.join(targetPath, zipFileName)\n\n  const res = await fetch(url)\n  const dest = fs.createWriteStream(zipFilePath)\n\n  dest.on('finish', () => {\n    console.log(`Download ${zipFileName} success!`)\n    // 解压缩文件, 命令行调用 7z\n    const Final2xCorePath = path.join(targetPath, 'Final2x-core')\n    const unzipCmd = `7z x ${zipFilePath} -o${Final2xCorePath}`\n    console.log(`Unzip command: ${unzipCmd}`)\n    // 使用异步方式执行解压命令\n    child_process.exec(unzipCmd, (error) => {\n      if (error) {\n        console.error(`Unzip error: ${error}`)\n        return\n      }\n      console.log(`Unzip ${zipFileName} success!`)\n      // 删除压缩文件\n      fs.unlinkSync(zipFilePath)\n      console.log(`Delete ${zipFileName} success!`)\n    })\n  })\n\n  res.body.pipe(dest)\n}\n\nasync function downloadAndUnzipCore(platform) {\n  const url = coreDict[platform]\n  if (!url) {\n    console.error('Invalid platform')\n    return\n  }\n\n  const targetPath = path.join(__dirname)\n  console.log(`Target path: ${targetPath}`)\n\n  if (fs.existsSync(path.join(targetPath, 'Final2x-core'))) {\n    console.log('Final2x-core already exists, skip download!')\n    return\n  }\n\n  if (!fs.existsSync(targetPath)) {\n    fs.mkdirSync(targetPath, { recursive: true })\n  }\n\n  await downloadAndUnzip(url, targetPath)\n}\n\n// 选择要下载的平台\nlet platformToDownload = ''\nif (PLATFORM === 'darwin') {\n  platformToDownload = ARCH === 'arm64' ? 'macos-arm64' : 'macos-x64'\n}\nelse if (PLATFORM === 'linux') {\n  console.error('Skip download Final2x-core for linux! Please use pip to install Final2x-core')\n  process.exit(0)\n}\nelse if (PLATFORM === 'win32') {\n  platformToDownload = 'windows-x64'\n}\nelse {\n  console.error('Unsupported platform!')\n  process.exit(1)\n}\n\nconsole.log(`Downloading Final2x-core for ${platformToDownload}...`)\n// 执行下载和解压\ndownloadAndUnzipCore(platformToDownload)\n  .then()\n  .catch((err) => {\n    console.error(err)\n  })\n"
  },
  {
    "path": "src/main/getCorePath.ts",
    "content": "import { spawnSync } from 'node:child_process'\nimport path from 'node:path'\nimport { app } from 'electron'\n\nconst FINAL2X_CORE_NAME = 'Final2x-core'\nconst FINAL2X_CORE_PATH = 'Final2x-core/Final2x-core'\n\n/**\n * 获取 Final2x-core 的路径\n * dev模式下，存放在项目根目录下的 resources\n * 在 electron-builder 中配置 extraResources，ASAR 打包时将它放入 app.asar 同级目录\n * @returns {string} Final2x-core 的路径\n */\nexport function getCorePath(): string {\n  if (!checkPipPackage()) {\n    if (process.env.NODE_ENV === 'development') {\n      return path.join(app.getAppPath(), 'resources', FINAL2X_CORE_PATH)\n    }\n    else {\n      return path.join(app.getAppPath(), '..', FINAL2X_CORE_PATH)\n    }\n  }\n  else {\n    return FINAL2X_CORE_NAME\n  }\n}\n\nexport function checkPipPackage(): boolean {\n  const command = `${FINAL2X_CORE_NAME} -h`\n\n  const result = spawnSync(command, { shell: true })\n\n  return result.status === 0\n}\n"
  },
  {
    "path": "src/main/index.ts",
    "content": "import { join } from 'node:path'\nimport { electronApp, is, optimizer } from '@electron-toolkit/utils'\nimport { IpcChannelInvoke, IpcChannelSend } from '@shared/const/ipc'\nimport { app, BrowserWindow, ipcMain, Menu, nativeImage, shell, Tray } from 'electron'\nimport appIcon from '../../resources/icon.png?asset'\nimport trayIcon from '../../resources/tray.png?asset'\nimport { openDirectory } from './openDirectory'\nimport { killCommand, runCommand } from './runCommand'\n\nfunction createWindow(): void {\n  // Create the browser window.\n  const mainWindow = new BrowserWindow({\n    width: 670,\n    height: 470,\n    maxWidth: 870,\n    minWidth: 670,\n    maxHeight: 670,\n    minHeight: 470,\n    frame: false,\n    show: false,\n    autoHideMenuBar: true,\n    icon: nativeImage.createFromPath(appIcon),\n    webPreferences: {\n      preload: join(__dirname, '../preload/index.js'),\n      sandbox: false,\n    },\n  })\n\n  if (process.platform === 'darwin') {\n    app.dock.setIcon(nativeImage.createFromPath(appIcon))\n  }\n\n  // Ipc events\n  ipcMain.on(IpcChannelSend.EXECUTE_COMMAND, runCommand)\n\n  ipcMain.on(IpcChannelSend.KILL_COMMAND, killCommand)\n\n  ipcMain.handle(IpcChannelInvoke.OPEN_DIRECTORY_DIALOG, openDirectory)\n\n  ipcMain.on(IpcChannelSend.MINIMIZE, () => {\n    mainWindow.minimize()\n  })\n\n  ipcMain.on(IpcChannelSend.MAXIMIZE, () => {\n    if (mainWindow.isMaximized()) {\n      mainWindow.restore()\n    }\n    else {\n      mainWindow.maximize()\n    }\n  })\n\n  ipcMain.on(IpcChannelSend.CLOSE, () => {\n    if (process.platform !== 'darwin') {\n      app.quit()\n    }\n    else {\n      app.hide()\n    }\n  })\n\n  // mainWindow\n  mainWindow.on('ready-to-show', () => {\n    mainWindow.show()\n  })\n\n  mainWindow.webContents.setWindowOpenHandler((details) => {\n    shell.openExternal(details.url)\n    return { action: 'deny' }\n  })\n\n  // HMR for renderer base on electron-vite cli.\n  // Load the remote URL for development or the local html file for production.\n  if (is.dev && process.env.ELECTRON_RENDERER_URL) {\n    mainWindow.loadURL(process.env.ELECTRON_RENDERER_URL)\n    mainWindow.webContents.openDevTools()\n  }\n  else {\n    mainWindow.loadFile(join(__dirname, '../renderer/index.html'))\n  }\n}\n\nlet tray\nfunction setTray(): void {\n  const Image = nativeImage.createFromPath(trayIcon)\n  Image.setTemplateImage(true)\n  tray = new Tray(Image)\n\n  const contextMenu = Menu.buildFromTemplate([\n    {\n      label: 'Open',\n      click: (): void => {\n        // On macOS it's common to re-create a window in the app when the\n        // dock icon is clicked and there are no other windows open.\n        if (BrowserWindow.getAllWindows().length === 0) {\n          createWindow()\n        }\n        else {\n          BrowserWindow.getAllWindows()[0].show()\n        }\n      },\n    },\n    {\n      label: 'Exit',\n      click: (): void => {\n        app.quit()\n      },\n    },\n  ])\n\n  tray.setToolTip('Final2x')\n  tray.setContextMenu(contextMenu)\n}\n\n// disable hardware acceleration for Compatibility for windows\napp.disableHardwareAcceleration()\n\n// This method will be called when Electron has finished\n// initialization and is ready to create browser windows.\n// Some APIs can only be used after this event occurs.\napp.whenReady().then(() => {\n  // Set app user model id for windows\n  electronApp.setAppUserModelId('com.final2x.app')\n\n  // Default open or close DevTools by F12 in development\n  // and ignore CommandOrControl + R in production.\n  // see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils\n  app.on('browser-window-created', (_, window) => {\n    optimizer.watchWindowShortcuts(window)\n  })\n\n  setTray()\n  createWindow()\n\n  app.on('activate', () => {\n    // On macOS it's common to re-create a window in the app when the\n    // dock icon is clicked and there are no other windows open.\n    if (BrowserWindow.getAllWindows().length === 0)\n      createWindow()\n  })\n})\n\n// Quit when all windows are closed, except on macOS. There, it's common\n// for applications and their menu bar to stay active until the user quits\n// explicitly with Cmd + Q.\napp.on('window-all-closed', () => {\n  if (process.platform !== 'darwin') {\n    app.quit()\n  }\n})\n\n// In this file you can include the rest of your app's specific main process\n// code. You can also put them in separate files and require them here.\n\nlet isQuitting = false\napp.on('before-quit', async (event) => {\n  if (isQuitting) {\n    console.log('Quitting...')\n    return\n  }\n  console.log('Killing child process before quitting...')\n  event.preventDefault()\n  isQuitting = true\n  await killCommand()\n  app.quit()\n})\n"
  },
  {
    "path": "src/main/openDirectory.ts",
    "content": "import { dialog } from 'electron'\n\n/**\n * @description Open a directory or file/multiple files\n * @param _ Unused parameter, can be used for context in future\n * @param p The properties of the dialog\n */\nexport async function openDirectory(_, p: Array<'openFile' | 'openDirectory' | 'multiSelections'>): Promise<Array<string>> {\n  try {\n    const { canceled, filePaths } = await dialog.showOpenDialog({ properties: p })\n    return canceled ? [] : filePaths\n  }\n  catch (error) {\n    console.error('Error opening directory dialog:', error)\n    return []\n  }\n}\n"
  },
  {
    "path": "src/main/runCommand.ts",
    "content": "import type { Final2xCoreConfig } from '@shared/type/core'\nimport type { IpcMainEvent } from 'electron'\nimport type { ChildProcessWithoutNullStreams } from 'node:child_process'\nimport { spawn } from 'node:child_process'\nimport { once } from 'node:events'\nimport { IpcChannelOn } from '@shared/const/ipc'\nimport kill from 'tree-kill'\nimport { getCorePath } from './getCorePath'\n\nlet child: ChildProcessWithoutNullStreams | null = null\n\nexport async function runCommand(event: IpcMainEvent, coreConfig: Final2xCoreConfig): Promise<void> {\n  let config_json = JSON.stringify(coreConfig.config)\n  // eslint-disable-next-line node/prefer-global/buffer\n  config_json = Buffer.from(config_json, 'utf8').toString('base64')\n\n  const resourceUrl = getCorePath()\n\n  let command = `\"${resourceUrl}\" -b ${config_json}`\n\n  if (!coreConfig.options.open_output_folder) {\n    command += ' -n'\n  }\n\n  console.log(command)\n\n  child = spawn(command, { shell: true })\n\n  child.stdout.on('data', (data) => {\n    event.sender.send(IpcChannelOn.COMMAND_STDOUT, data.toString())\n  })\n\n  child.stderr.on('data', (data) => {\n    event.sender.send(IpcChannelOn.COMMAND_STDERR, data.toString())\n  })\n\n  const [code] = await once(child, 'close')\n  event.sender.send(IpcChannelOn.COMMAND_CLOSE, code)\n  console.log(`Child process exited with code: ${code}`)\n\n  child = null\n}\n\nexport async function killCommand(): Promise<void> {\n  if (!child || !child.pid) {\n    console.error('Could not find child process, nothing to kill.')\n    return\n  }\n  const pid = child.pid\n\n  console.log(`Kill child process with pid: ${pid}`)\n\n  await new Promise<void>((resolve) => {\n    kill(pid, (err) => {\n      if (err) {\n        console.error(`Failed to kill process: ${err.message}`)\n      }\n      else {\n        console.log('Process killed successfully')\n      }\n      if (child && child.pid === pid) {\n        child = null\n      }\n      resolve()\n    })\n  })\n}\n"
  },
  {
    "path": "src/preload/index.d.ts",
    "content": "import type { ElectronAPI } from '@electron-toolkit/preload'\n\ndeclare global {\n  interface Window {\n    electron: ElectronAPI\n    api: unknown\n  }\n}\n"
  },
  {
    "path": "src/preload/index.ts",
    "content": "import { electronAPI } from '@electron-toolkit/preload'\nimport { contextBridge } from 'electron'\n\n// Custom APIs for renderer\nconst api = {}\n\n// Use `contextBridge` APIs to expose Electron APIs to\n// renderer only if context isolation is enabled, otherwise\n// just add to the DOM global.\nif (process.contextIsolated) {\n  try {\n    contextBridge.exposeInMainWorld('electron', electronAPI)\n    contextBridge.exposeInMainWorld('api', api)\n  }\n  catch (error) {\n    console.error(error)\n  }\n}\nelse {\n  // @ts-ignore (define in dts)\n  window.electron = electronAPI\n  // @ts-ignore (define in dts)\n  window.api = api\n}\n"
  },
  {
    "path": "src/renderer/index.html",
    "content": "<!doctype html>\n<html>\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title>Final2x</title>\n    <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->\n    <!--    <meta-->\n    <!--      http-equiv=\"Content-Security-Policy\"-->\n    <!--      content=\"default-src 'self' file:  data:; img-src * 'self' data: https:; script-src 'self'; style-src 'self' 'unsafe-inline'\"-->\n    <!--    />-->\n  </head>\n\n  <body>\n    <div id=\"app\"></div>\n    <script type=\"module\" src=\"./src/main.ts\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "src/renderer/src/App.vue",
    "content": "<script lang=\"ts\" setup>\nimport { NConfigProvider, NDialogProvider, NGlobalStyle, NNotificationProvider } from 'naive-ui'\nimport { storeToRefs } from 'pinia'\nimport { onMounted, watch } from 'vue'\nimport { useI18n } from 'vue-i18n'\nimport { RouterView } from 'vue-router'\nimport BottomNavigation from './components/bottomNavigation.vue'\nimport MyDarkMode from './components/MyDarkMode.vue'\nimport MyProgress from './components/MyProgress.vue'\nimport TrafficLightsButtons from './components/TrafficLightsButtons.vue'\nimport { useGlobalSettingsStore } from './store/globalSettingsStore'\nimport { getLanguage } from './utils'\n\nconst { locale } = useI18n()\nconst { langsNum, naiveTheme, globalcolor } = storeToRefs(useGlobalSettingsStore())\n\nwatch(langsNum, () => {\n  // 切换语言\n  locale.value = getLanguage(langsNum.value).lang\n  console.log('locale: ', locale.value)\n})\n\nonMounted(async () => {\n  if (langsNum.value !== 114514) {\n    // 当语言不是跟随环境时，设置语言\n    locale.value = getLanguage(langsNum.value).lang\n  }\n})\n\nconst themeOverrides = {\n  Select: {\n    peers: {\n      InternalSelectMenu: {\n        height: '200px',\n      },\n    },\n  },\n}\n</script>\n\n<template>\n  <NConfigProvider :theme=\"naiveTheme\" :theme-overrides=\"themeOverrides\">\n    <NGlobalStyle />\n    <NNotificationProvider class=\"n-config-provider\" placement=\"top\">\n      <NDialogProvider>\n        <div class=\"background\">\n          <MyDarkMode />\n          <TrafficLightsButtons />\n          <MyProgress />\n          <div class=\"view\">\n            <RouterView v-slot=\"{ Component }\">\n              <transition mode=\"out-in\" name=\"custom-fade\">\n                <keep-alive>\n                  <component :is=\"Component\" />\n                </keep-alive>\n              </transition>\n            </RouterView>\n          </div>\n          <BottomNavigation />\n        </div>\n      </NDialogProvider>\n    </NNotificationProvider>\n  </NConfigProvider>\n</template>\n\n<style lang=\"scss\" scoped>\n.custom-fade-enter-active {\n  transition: all 0.2s ease-out;\n}\n\n.custom-fade-leave-active {\n  transition: all 0.3s cubic-bezier(1, 0.5, 0.8, 1);\n}\n\n.custom-fade-enter-from,\n.custom-fade-leave-to {\n  opacity: 0;\n}\n\n$global-color: v-bind(globalcolor);\n$buttom-bottom: 8px;\n\n::-webkit-scrollbar {\n  display: none;\n}\n\n.n-config-provider {\n  width: 100vw;\n  height: 100vh;\n}\n\n.background {\n  box-sizing: border-box;\n  width: 100%;\n  height: 100%;\n  background-color: $global-color;\n  transition: all 300ms ease-in-out;\n  //padding-top: 30px;\n  display: flex;\n  flex-direction: column;\n\n  .view {\n    overflow: scroll;\n    flex: 1;\n  }\n}\n\n.fade-enter-active {\n  transition: opacity 0.6s ease-in-out;\n}\n\n.fade-enter-from,\n.fade-leave-to {\n  opacity: 0;\n}\n</style>\n"
  },
  {
    "path": "src/renderer/src/components/MyDarkMode.vue",
    "content": "<script lang=\"ts\" setup>\nimport { storeToRefs } from 'pinia'\nimport { useGlobalSettingsStore } from '../store/globalSettingsStore'\nimport NaiveDarkMode from './NaiveDarkMode.vue'\n\nconst { darkMode, globalcolor, naiveTheme } = storeToRefs(useGlobalSettingsStore())\n</script>\n\n<template>\n  <div>\n    <NaiveDarkMode\n      v-model:color=\"globalcolor\"\n      v-model:naivetheme=\"naiveTheme\"\n      :dark-mode=\"darkMode\"\n      design-dark=\"#101015\"\n      design-light=\"#fffafa\"\n      :fade-layer=\"0\"\n      class=\"naive-dark-mode\"\n    />\n  </div>\n</template>\n\n<style lang=\"scss\" scoped></style>\n"
  },
  {
    "path": "src/renderer/src/components/MyExternalLink.vue",
    "content": "<script lang=\"ts\" setup>\nimport { FilmOutline } from '@vicons/ionicons5'\n\nclass openWebsite {\n  static async FinalRip(): Promise<void> {\n    window.open('https://github.com/EutropicAI/FinalRip', '_blank')\n  }\n\n  static async VSET(): Promise<void> {\n    window.open('https://github.com/EutropicAI/VSET', '_blank')\n  }\n}\n</script>\n\n<template>\n  <div class=\"MyExternalLink\">\n    <n-space>\n      <n-button style=\"font-size: 36px\" text @click=\"openWebsite.VSET\">\n        <n-icon>\n          <FilmOutline />\n        </n-icon>\n      </n-button>\n    </n-space>\n  </div>\n</template>\n\n<style lang=\"scss\" scoped>\n.custom-fade-enter-active {\n  transition: all 2s ease-out;\n}\n\n.custom-fade-leave-active {\n  transition: all 2s cubic-bezier(1, 0.5, 0.8, 1);\n}\n\n.custom-fade-enter-from,\n.custom-fade-leave-to {\n  opacity: 0;\n}\n</style>\n"
  },
  {
    "path": "src/renderer/src/components/MyProgress.vue",
    "content": "<script lang=\"ts\" setup>\nimport { IpcChannelOn, IpcChannelSend } from '@shared/const/ipc'\nimport { useDialog, useNotification } from 'naive-ui'\nimport { storeToRefs } from 'pinia'\nimport { nextTick, onMounted, ref, watchEffect } from 'vue'\nimport { useI18n } from 'vue-i18n'\nimport { useGlobalSettingsStore } from '../store/globalSettingsStore'\nimport { getFinal2xCoreConfig } from '../utils/getFinal2xCoreConfig'\nimport IOPath from '../utils/IOPath'\n\nconst { t } = useI18n()\nconst notification = useNotification()\nconst dialog = useDialog()\nconst {\n  CommandLOG,\n  logInstRef,\n  StartCommandLock,\n  SrSuccess,\n  ProgressPercentage,\n} = storeToRefs(useGlobalSettingsStore())\n\nconst showLOG = ref(false)\n\nonMounted(() => {\n  window.electron.ipcRenderer.on(\n    IpcChannelOn.COMMAND_STDOUT,\n    (_, data) => {\n      handleCommandLOG(data)\n    },\n  )\n  window.electron.ipcRenderer.on(\n    IpcChannelOn.COMMAND_STDERR,\n    (_, data) => {\n      handleCommandLOG(data)\n    },\n  )\n  window.electron.ipcRenderer.on(\n    IpcChannelOn.COMMAND_CLOSE,\n    (_, data) => {\n      handleCommandLOG(`CLOSE CODE:${data}`)\n      StartCommandLock.value = false\n\n      if (!SrSuccess.value) {\n        MyProgressDialogs.SrFailed()\n      }\n      else {\n        IOPath.clearALL()\n      }\n    },\n  )\n  watchEffect(() => {\n    if (CommandLOG.value) {\n      nextTick(() => {\n        logInstRef.value?.scrollTo({ position: 'bottom', silent: true })\n      })\n    }\n  })\n})\n\nfunction handleCommandLOG(log: string): void {\n  CommandLOG.value += log\n\n  const skipImageRegex = /______Skip_Image______:(.+)/\n  const processingRegex = /Processing------\\[ ([\\d.]+)% /\n  const srSuccessRegex = /______SR_COMPLETED______/\n\n  const skipImageMatch = log.match(skipImageRegex)\n  const processingMatch = log.match(processingRegex)\n  const srSuccessMatch = log.match(srSuccessRegex)\n\n  if (skipImageMatch) {\n    const imagePath = skipImageMatch[1]\n    MyProgressNotifications.SkipImage(imagePath)\n  }\n\n  if (processingMatch) {\n    ProgressPercentage.value = Number.parseFloat(processingMatch[1])\n  }\n\n  if (srSuccessMatch) {\n    SrSuccess.value = true\n  }\n}\n\nclass MyProgressNotifications {\n  static StartSR(): void {\n    notification.success({\n      title: t('MyProgress.text0'),\n      duration: 1500,\n    })\n  }\n\n  static SRprocessing(): void {\n    notification.warning({\n      title: t('MyProgress.text1'),\n      duration: 1500,\n    })\n  }\n\n  static SRListEmpty(): void {\n    notification.warning({\n      title: t('MyProgress.text2'),\n      content: t('MyProgress.text3'),\n      duration: 1500,\n      keepAliveOnHover: true,\n    })\n  }\n\n  static TerminateSR(): void {\n    notification.error({\n      title: t('MyProgress.text4'),\n      duration: 1500,\n      keepAliveOnHover: true,\n    })\n  }\n\n  static SkipImage(imagePath: string): void {\n    notification.warning({\n      title: t('MyProgress.text5'),\n      content: imagePath,\n      duration: 2000,\n      keepAliveOnHover: true,\n    })\n  }\n}\n\nclass MyProgressDialogs {\n  static SrFailed(): void {\n    dialog.error({\n      title: t('MyProgress.text9'),\n      content: t('MyProgress.text10'),\n    })\n  }\n}\n\nfunction StartSR(): void {\n  if (StartCommandLock.value) {\n    MyProgressNotifications.SRprocessing()\n    return\n  }\n\n  if (IOPath.isEmpty()) {\n    MyProgressNotifications.SRListEmpty()\n    return\n  }\n\n  StartCommandLock.value = true // START LOCK\n  SrSuccess.value = false // RESET SR SUCCESS\n\n  MyProgressNotifications.StartSR()\n\n  // get Final2x-core config\n  const final2xCoreConfig = getFinal2xCoreConfig()\n  CommandLOG.value += `\\n${JSON.stringify(final2xCoreConfig)}\\n`\n\n  window.electron.ipcRenderer.send(IpcChannelSend.EXECUTE_COMMAND, final2xCoreConfig)\n}\n\nfunction TerminateSR(): void {\n  window.electron.ipcRenderer.send(IpcChannelSend.KILL_COMMAND)\n  MyProgressNotifications.TerminateSR()\n}\n</script>\n\n<template>\n  <div>\n    <div class=\"control\">\n      <n-progress\n        :percentage=\"ProgressPercentage\"\n        color=\"green\"\n        :height=\"34\"\n        indicator-placement=\"inside\"\n        processing\n        type=\"line\"\n      />\n      <n-button round secondary strong type=\"success\" @click=\"StartSR\">\n        {{ t('MyProgress.text6') }}\n      </n-button>\n\n      <n-button round secondary strong type=\"error\" @click=\"TerminateSR\">\n        {{ t('MyProgress.text7') }}\n      </n-button>\n\n      <n-button round secondary strong type=\"warning\" @click=\"showLOG = !showLOG\">\n        {{ t('MyProgress.text8') }}\n      </n-button>\n    </div>\n\n    <n-drawer v-model:show=\"showLOG\" height=\"385\" placement=\"top\">\n      <n-drawer-content :native-scrollbar=\"false\" title=\"\">\n        <br>\n        <n-card hoverable size=\"small\" title=\"Log\">\n          <n-log ref=\"logInstRef\" :log=\"CommandLOG\" trim />\n        </n-card>\n      </n-drawer-content>\n    </n-drawer>\n\n    <n-divider class=\"n-divider\" />\n  </div>\n</template>\n\n<style lang=\"scss\">\n.control {\n  box-sizing: border-box;\n  width: 100%;\n  padding: 30px 40px 0 40px;\n  display: flex;\n  justify-content: space-between;\n\n  > div {\n    margin: 0 5px;\n  }\n\n  > button {\n    margin: 0 5px;\n  }\n}\n\n.progress {\n  margin-left: -30px;\n  margin-top: 10px;\n}\n\n.ButtonSpace {\n  margin-right: 40px;\n  margin-top: 30px;\n}\n\n.n-divider {\n  margin: 10px 0 0 0 !important;\n}\n</style>\n"
  },
  {
    "path": "src/renderer/src/components/MySetting.vue",
    "content": "<script lang=\"ts\" setup>\nimport { HomeOutlined, SettingOutlined, TranslationOutlined } from '@vicons/antd'\nimport { ContrastSharp, MoonOutline, SunnyOutline } from '@vicons/ionicons5'\nimport { storeToRefs } from 'pinia'\nimport router from '../router'\nimport { useGlobalSettingsStore } from '../store/globalSettingsStore'\nimport { clickDebounce } from '../utils'\nimport { switchLanguage } from '../utils/switchLanguage'\n\nconst { darkMode, changeRoute } = storeToRefs(useGlobalSettingsStore())\n\nfunction handleRoute(): void {\n  if (changeRoute.value === false) {\n    changeRoute.value = true\n    router.push('/Final2xSettings')\n  }\n  else {\n    changeRoute.value = false\n    router.push('/')\n  }\n}\n\nconst handleDarkMode = clickDebounce((): void => {\n  // const darkmodeList : Array<NaiveDarkModeType> = ['system', 'light', 'dark']\n  if (darkMode.value === 'system') {\n    darkMode.value = 'light'\n  }\n  else if (darkMode.value === 'light') {\n    darkMode.value = 'dark'\n  }\n  else {\n    darkMode.value = 'system'\n  }\n})\n</script>\n\n<template>\n  <div>\n    <n-space class=\"main-buttons\">\n      <n-button style=\"font-size: 36px\" text @click=\"handleRoute\">\n        <n-icon>\n          <div v-if=\"changeRoute === false\">\n            <SettingOutlined />\n          </div>\n          <div v-else>\n            <HomeOutlined />\n          </div>\n        </n-icon>\n      </n-button>\n\n      <n-button style=\"font-size: 36px\" text @click=\"switchLanguage\">\n        <n-icon>\n          <TranslationOutlined />\n        </n-icon>\n      </n-button>\n\n      <n-button style=\"font-size: 36px\" text @click=\"handleDarkMode\">\n        <n-icon>\n          <div v-if=\"darkMode === 'light'\">\n            <SunnyOutline />\n          </div>\n          <div v-else-if=\"darkMode === 'dark'\">\n            <MoonOutline />\n          </div>\n          <div v-else>\n            <ContrastSharp />\n          </div>\n        </n-icon>\n      </n-button>\n    </n-space>\n  </div>\n</template>\n\n<style lang=\"scss\" scoped>\n$buttom-bottom: 8px;\n.main-buttons {\n  width: 180px;\n}\n</style>\n"
  },
  {
    "path": "src/renderer/src/components/NaiveDarkMode.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { PropType, Ref } from 'vue'\nimport { darkTheme, useOsTheme } from 'naive-ui'\nimport { nextTick, onBeforeMount, onMounted, ref, watch } from 'vue'\n\nexport type NaiveDarkModeType = undefined | 'light' | 'dark' | 'system'\n\n// 不想用 CSS Transition，可以用 JS 实现捏\n\n// -----------------------------------------------------------------------------\n// Props and Emits\n// -----------------------------------------------------------------------------\n\nconst props = defineProps({\n  darkMode: {\n    type: String as PropType<NaiveDarkModeType>,\n    default: () => 'system',\n  },\n  designDark: {\n    type: String,\n    default: () => '#000000',\n  },\n  designLight: {\n    type: String,\n    default: () => '#ffffff',\n  },\n  fadeLayer: {\n    type: Number,\n    default: () => 25,\n  },\n  color: {\n    type: String,\n    default: () => '#ffffff',\n  },\n  naivetheme: {\n    type: Object,\n    default: () => undefined,\n  },\n})\n\nconst emits = defineEmits(['update:color', 'update:naivetheme'])\n\n// -----------------------------------------------------------------------------\n// Refs\n// -----------------------------------------------------------------------------\n\nconst osThemeRef = useOsTheme()\n\nconst DarkMode: Ref<NaiveDarkModeType> = ref(undefined)\nconst globalcolor = ref('')\nconst DarkTheme: Ref<boolean | undefined> = ref(undefined)\nconst DesignDarkColor = ref('#000000')\nconst DesignLightColor = ref('#ffffff')\nconst FadeLayer = ref(25)\n\n// v-model 传入的 color\nwatch(\n  () => globalcolor.value,\n  (value) => {\n    emits('update:color', value)\n  },\n)\n\n// v-model 传入的 naivetheme\nwatch(\n  () => DarkTheme.value,\n  (value) => {\n    emits('update:naivetheme', value ? darkTheme : undefined)\n  },\n)\n\nonBeforeMount(() => {\n  // 传入\n  DarkMode.value = props.darkMode\n  DesignDarkColor.value = props.designDark\n  DesignLightColor.value = props.designLight\n  // set designLightColor to globalcolor, update:color\n  globalcolor.value = props.designLight\n  FadeLayer.value = props.fadeLayer\n  // console.log('onBeforeMount  DarkMode.value', DarkMode.value)\n})\n\n// 监听 props.darkMode 的变化\nwatch(\n  () => props.darkMode,\n  (value) => {\n    DarkMode.value = value\n  },\n)\n\n// 监听 props.designDark 的变化\nwatch(\n  () => props.designDark,\n  (value) => {\n    if (globalcolor.value === DesignDarkColor.value) {\n      globalcolor.value = value\n    }\n    DesignDarkColor.value = value\n  },\n)\n\n// 监听 props.designLight 的变化\nwatch(\n  () => props.designLight,\n  (value) => {\n    if (globalcolor.value === DesignLightColor.value) {\n      globalcolor.value = value\n    }\n    DesignLightColor.value = value\n  },\n)\n\n// 监听 props.fadeLayer 的变化\nwatch(\n  () => props.fadeLayer,\n  (value) => {\n    FadeLayer.value = value\n  },\n)\n\n/**\n * @description update DarkTheme.value and update:naivetheme\n * @param mode 'dark' or 'light' or 'system'\n */\nfunction handleDarkModeChange(mode: NaiveDarkModeType): void {\n  // console.log('handleDarkModeChange  DarkTheme.value', DarkTheme.value)\n  // console.log('handleDarkModeChange  mode', mode)\n  if (mode === 'system' || mode === undefined) {\n    DarkTheme.value = osThemeRef.value === 'dark'\n  }\n  else {\n    DarkTheme.value = mode === 'dark'\n  }\n}\n\nonMounted(() => {\n  // console.log('onMounted  DarkMode.value', DarkMode.value)\n  handleDarkModeChange(DarkMode.value)\n})\n\nwatch(DarkMode, (value) => {\n  // console.log('watch DarkMode  ', value)\n  handleDarkModeChange(value)\n})\n\n// 检测系统主题，修改 DarkTheme.value\nwatch(osThemeRef, (value) => {\n  // console.log('watch  osThemeRef', value)\n  if (DarkMode.value === 'system' || DarkMode.value === undefined) {\n    DarkTheme.value = value === 'dark'\n  }\n})\n\n// 检测 DarkTheme.value，修改 CSS 样式\nwatch(DarkTheme, (value) => {\n  // console.log('watch DarkTheme  ', value)\n  if (value) {\n    if (isCSSLight()) {\n      switchCSSStyle('dark')\n    }\n  }\n  else {\n    if (isCSSDark()) {\n      switchCSSStyle('light')\n    }\n  }\n})\n\n// -----------------------------------------------------------------------------\n// Functions\n// -----------------------------------------------------------------------------\n\n/**\n * @description Interpolate two colors by a given factor\n */\nfunction interpolateColor(color1: string, color2: string, factor: number): string {\n  if (factor === 0)\n    return color1\n  if (factor === 1)\n    return color2\n\n  const c1 = hexToRgb(color1)\n  const c2 = hexToRgb(color2)\n\n  const r = Math.round(interpolate(c1.r, c2.r, factor))\n  const g = Math.round(interpolate(c1.g, c2.g, factor))\n  const b = Math.round(interpolate(c1.b, c2.b, factor))\n\n  return `rgb(${r}, ${g}, ${b})`\n}\n\nfunction interpolate(start: number, end: number, factor: number): number {\n  return start + (end - start) * factor\n}\n\nfunction hexToRgb(hex: string): { r: number, g: number, b: number } {\n  const r = Number.parseInt(hex.slice(1, 3), 16)\n  const g = Number.parseInt(hex.slice(3, 5), 16)\n  const b = Number.parseInt(hex.slice(5, 7), 16)\n  return { r, g, b }\n}\n\n/**\n * @description Smooth transition to dark mode\n * @param mode 'dark' or 'light' or 'system'\n */\nfunction switchCSSStyle(mode: NaiveDarkModeType): void {\n  if (mode === 'system') {\n    const osThemeRef = useOsTheme()\n    mode = osThemeRef.value === 'dark' ? 'dark' : 'light'\n  }\n  const targetColor = mode === 'dark' ? DesignDarkColor.value : DesignLightColor.value\n  const initialColor = mode === 'dark' ? DesignLightColor.value : DesignDarkColor.value\n  const layer = Math.ceil(FadeLayer.value)\n\n  if (layer < 1) {\n    globalcolor.value = targetColor\n    return\n  }\n\n  for (let i = 1; i <= layer; i++) {\n    setTimeout(() => {\n      nextTick(() => {\n        globalcolor.value = interpolateColor(initialColor, targetColor, i / layer)\n      })\n    }, layer * i)\n  }\n}\n\n/**\n * @description Check if the current CSS theme is dark\n */\nfunction isCSSDark(): boolean {\n  return globalcolor.value === DesignDarkColor.value\n}\n\n/**\n * @description Check if the current CSS theme is light\n */\nfunction isCSSLight(): boolean {\n  return globalcolor.value === DesignLightColor.value\n}\n</script>\n\n<template>\n  <div />\n</template>\n\n<style lang=\"scss\"></style>\n"
  },
  {
    "path": "src/renderer/src/components/TrafficLightsButtons.vue",
    "content": "<script setup lang=\"ts\">\nimport { IpcChannelSend } from '@shared/const/ipc'\nimport { onMounted, onUnmounted, ref } from 'vue'\n\nconst isFocus = ref(true)\n\nfunction handleFocus(): void {\n  isFocus.value = true\n}\n\nfunction handleBlur(): void {\n  isFocus.value = false\n}\n\nonMounted(() => {\n  window.addEventListener('focus', handleFocus)\n  window.addEventListener('blur', handleBlur)\n})\n\nonUnmounted(() => {\n  window.removeEventListener('focus', handleFocus)\n  window.removeEventListener('blur', handleBlur)\n})\n\nfunction handleClose(): void {\n  window.electron.ipcRenderer.send(IpcChannelSend.CLOSE)\n}\n\nfunction handleMinimize(): void {\n  window.electron.ipcRenderer.send(IpcChannelSend.MINIMIZE)\n}\n\nfunction handleMaximize(): void {\n  window.electron.ipcRenderer.send(IpcChannelSend.MAXIMIZE)\n}\n</script>\n\n<template>\n  <div class=\"container\">\n    <div class=\"drag-area\" />\n    <div v-if=\"!isFocus\">\n      <div class=\"example\">\n        <div class=\"traffic-lights\">\n          <button\n            id=\"close\"\n            class=\"traffic-light traffic-light-close\"\n            @click=\"handleClose\"\n          />\n          <button\n            id=\"minimize\"\n            class=\"traffic-light traffic-light-minimize\"\n            @click=\"handleMinimize\"\n          />\n          <button\n            id=\"maximize\"\n            class=\"traffic-light traffic-light-maximize\"\n            @click=\"handleMaximize\"\n          />\n        </div>\n      </div>\n    </div>\n    <div v-else>\n      <div class=\"example focus\">\n        <div class=\"traffic-lights\">\n          <button\n            id=\"close\"\n            class=\"traffic-light traffic-light-close\"\n            @click=\"handleClose\"\n          />\n          <button\n            id=\"minimize\"\n            class=\"traffic-light traffic-light-minimize\"\n            @click=\"handleMinimize\"\n          />\n          <button\n            id=\"maximize\"\n            class=\"traffic-light traffic-light-maximize\"\n            @click=\"handleMaximize\"\n          />\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<style lang=\"scss\" scoped>\n$close-red: #ff6159;\n$close-red-active: #bf4942;\n$close-red-icon: #4d0000;\n$close-red-icon-active: #190000;\n\n$minimize-yellow: #ffbd2e;\n$minimize-yellow-active: #bf8e22;\n$minimize-yellow-icon: #995700;\n$minimize-yellow-icon-active: #592800;\n\n$maximize-green: #28c941;\n$maximize-green-active: #1d9730;\n$maximize-green-icon: #006500;\n$maximize-green-icon-active: #003200;\n\n$disabled-gray: #ddd;\n\n.traffic-lights {\n  // position: absolute;\n  top: 1px;\n  left: 8px;\n\n  .focus &,\n  &:hover,\n  &:active {\n    > .traffic-light-close {\n      background-color: $close-red;\n\n      &:active:hover {\n        background-color: $close-red-active;\n      }\n    }\n    > .traffic-light-minimize {\n      background-color: $minimize-yellow;\n\n      &:active:hover {\n        background-color: $minimize-yellow-active;\n      }\n    }\n    > .traffic-light-maximize {\n      background-color: $maximize-green;\n\n      &:active:hover {\n        background-color: $maximize-green-active;\n      }\n    }\n  }\n\n  > .traffic-light {\n    &:before,\n    &:after {\n      visibility: hidden;\n    }\n  }\n\n  &:hover,\n  &:active {\n    > .traffic-light {\n      &:before,\n      &:after {\n        visibility: visible;\n      }\n    }\n  }\n}\n\n.traffic-light {\n  border-radius: 100%;\n  padding: 0;\n  height: 12px;\n  width: 12px;\n  border: 1px solid rgba(0, 0, 0, 0.06);\n  box-sizing: border-box;\n  margin-right: 3.5px;\n  background-color: $disabled-gray;\n  position: relative;\n  outline: none;\n\n  &:before,\n  &:after {\n    content: '';\n    position: absolute;\n    border-radius: 1px;\n    left: 0;\n    top: 0;\n    right: 0;\n    bottom: 0;\n    margin: auto;\n  }\n\n  &-close {\n    &:before,\n    &:after {\n      background-color: $close-red-icon;\n      width: 8px;\n      height: 1px;\n    }\n    &:before {\n      transform: rotate(45deg); // translate(-0.5px, -0.5px);\n    }\n    &:after {\n      transform: rotate(-45deg); // translate(0.5px, -0.5px);\n    }\n    &:active:hover:before,\n    &:active:hover:after {\n      background-color: $close-red-icon-active;\n    }\n  }\n\n  &-minimize {\n    &:before {\n      background-color: $minimize-yellow-icon;\n      width: 8px;\n      height: 1px;\n      //transform: translateY(-0.5px);\n    }\n    &:active:hover:before {\n      background-color: $minimize-yellow-icon-active;\n    }\n  }\n\n  &-maximize {\n    &:before {\n      background-color: $maximize-green-icon;\n      width: 6px;\n      height: 6px;\n    }\n    &:after {\n      background-color: $maximize-green;\n      width: 10px;\n      height: 2px;\n      transform: rotate(45deg);\n    }\n    &:active:hover:before {\n      background-color: $maximize-green-icon-active;\n    }\n    &:active:hover:after {\n      background-color: $maximize-green-active;\n    }\n  }\n}\n\n// Example Styles\nbody {\n  font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;\n  font-weight: 100;\n}\n\nh1,\nh2 {\n  font-weight: 100;\n}\n\nh2 {\n  margin: 0 0 10px;\n}\n\n.example {\n  margin: 0 0 30px;\n}\n\n.container {\n  position: fixed;\n  height: 30px;\n  width: 50px;\n  left: 10px;\n}\n\n.drag-area {\n  -webkit-app-region: drag;\n  position: fixed;\n  height: 30px;\n  width: 100%;\n  left: 60px;\n}\n</style>\n"
  },
  {
    "path": "src/renderer/src/components/bottomNavigation.vue",
    "content": "<script lang=\"ts\" setup>\nimport MyExternalLink from './MyExternalLink.vue'\nimport MySetting from './MySetting.vue'\n</script>\n\n<template>\n  <div class=\"position\">\n    <n-divider class=\"n-divider\" />\n    <n-space class=\"n-space\" justify=\"space-between\">\n      <MySetting />\n      <MyExternalLink />\n    </n-space>\n  </div>\n</template>\n\n<style lang=\"scss\" scoped>\n.position {\n  box-sizing: border-box;\n  width: 100%;\n  height: 60px;\n  justify-content: flex-end;\n}\n\n.n-space {\n  box-sizing: border-box;\n  margin: 10px 20px 0 20px;\n}\n\n.n-divider {\n  margin: 0 !important;\n}\n</style>\n"
  },
  {
    "path": "src/renderer/src/env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n\ndeclare module '*.vue' {\n  import type { DefineComponent } from 'vue'\n\n  const component: DefineComponent<object, object, any>\n  export default component\n}\n"
  },
  {
    "path": "src/renderer/src/locales/en.ts",
    "content": "export const en = {\n  MyProgress: {\n    text0: 'Processing started',\n    text1: 'Processing in progress',\n    text2: 'Please add an image',\n    text3: 'Image list is empty',\n    text4: 'Processing terminated',\n    text5: 'Processing failed, skipping',\n    text6: 'START',\n    text7: 'TERMINATE',\n    text8: 'LOG',\n    text9: 'Processing failed',\n    text10: 'Please click on the log to view the error message',\n  },\n  Final2xHome: {\n    text0: 'Removal successful',\n    text1: 'Click or drag and drop images or folders here to upload',\n  },\n  Final2xSettings: {\n    text10: 'Device',\n    text11: 'Model',\n    text15: 'Custom Scale',\n    text16: 'Default',\n    text17: 'Output Folder',\n    text18: 'Proxy',\n    text19: 'Format',\n    text20: 'Tile Process',\n  },\n}\n"
  },
  {
    "path": "src/renderer/src/locales/fr.ts",
    "content": "export const fr = {\n  MyProgress: {\n    text0: 'Traitement commencé',\n    text1: 'Traitement en cours',\n    text2: 'Veuillez ajouter une image',\n    text3: 'La liste d\\'images est vide',\n    text4: 'Traitement terminé',\n    text5: 'Échec du traitement, passage à la suite',\n    text6: 'DÉMARRER',\n    text7: 'ARRÊTER',\n    text8: 'JOURNAL',\n    text9: 'Échec du traitement',\n    text10: 'Veuillez cliquer sur le journal pour voir le message d\\'erreur',\n  },\n  Final2xHome: {\n    text0: 'Suppression réussie',\n    text1: 'Cliquez ou faites glisser les images ou dossiers ici pour les téléverser',\n  },\n  Final2xSettings: {\n    text10: 'Périph.',\n    text11: 'Modèle',\n    text15: 'Échelle (num.)',\n    text16: 'Par déf.',\n    text17: 'Dossier de sortie',\n    text18: 'Proxy',\n    text19: 'Format',\n    text20: 'Tile Process',\n  },\n}\n"
  },
  {
    "path": "src/renderer/src/locales/ja.ts",
    "content": "export const ja = {\n  MyProgress: {\n    text0: '処理を開始します',\n    text1: '処理中です',\n    text2: '画像を追加してください',\n    text3: '画像リストは空です',\n    text4: '処理が中断されました',\n    text5: '処理に失敗しました。スキップします',\n    text6: '開始',\n    text7: '中止',\n    text8: 'ログ',\n    text9: '処理失敗',\n    text10: 'エラーメッセージを確認するには、ログをクリックしてください',\n  },\n  Final2xHome: {\n    text0: '削除が成功しました',\n    text1: '画像やフォルダをここにクリックまたはドラッグ＆ドロップしてアップロードしてください',\n  },\n  Final2xSettings: {\n    text10: 'デバイス',\n    text11: 'モデル',\n    text15: 'Custom Scale',\n    text16: 'Default',\n    text17: '出力フォルダ',\n    text18: 'プロキシ',\n    text19: 'Format',\n    text20: 'Tile Process',\n  },\n}\n"
  },
  {
    "path": "src/renderer/src/locales/zh.ts",
    "content": "export const zh = {\n  MyProgress: {\n    text0: '开始处理',\n    text1: '处理中',\n    text2: '请添加图片',\n    text3: '图片列表为空',\n    text4: '已终止处理',\n    text5: '处理失败，跳过',\n    text6: '开始',\n    text7: '终止',\n    text8: '日志',\n    text9: '处理失败',\n    text10: '请点击日志查看错误信息',\n  },\n  Final2xHome: {\n    text0: '移除成功',\n    text1: '点击或拖拽图片或文件夹到此处上传',\n  },\n  Final2xSettings: {\n    text10: '设备',\n    text11: '模型',\n    text15: '自定义倍率',\n    text16: '默认',\n    text17: '输出文件夹',\n    text18: '下载代理',\n    text19: '保存格式',\n    text20: '启用切块处理',\n  },\n}\n"
  },
  {
    "path": "src/renderer/src/main.ts",
    "content": "import {\n  // create naive ui\n  create,\n  // component\n  NButton,\n  NCard,\n  NDivider,\n  NDrawer,\n  NDrawerContent,\n  NIcon,\n  NImage,\n  NInput,\n  NInputNumber,\n  NLog,\n  NPopover,\n  NProgress,\n  NSelect,\n  NSpace,\n  NSwitch,\n  NText,\n  NUpload,\n  NUploadDragger,\n} from 'naive-ui'\nimport { createPinia } from 'pinia'\nimport { createPersistedState } from 'pinia-plugin-persistedstate'\nimport { createApp } from 'vue'\nimport App from './App.vue'\nimport i18n from './plugins/i18n'\nimport router from './router'\n// 通用字体\nimport 'vfonts/OpenSans.css'\n\nconst naive = create({\n  components: [\n    NButton,\n    NDivider,\n    NSpace,\n    NIcon,\n    NImage,\n    NCard,\n    NDrawer,\n    NDrawerContent,\n    NLog,\n    NProgress,\n    NText,\n    NUpload,\n    NUploadDragger,\n    NInput,\n    NInputNumber,\n    NPopover,\n    NSelect,\n    NSwitch,\n  ],\n})\n\nconst pinia = createPinia()\npinia.use(createPersistedState({\n  storage: localStorage,\n}))\n\ncreateApp(App).use(naive).use(i18n).use(pinia).use(router).mount('#app')\n"
  },
  {
    "path": "src/renderer/src/plugins/i18n.ts",
    "content": "import { createI18n } from 'vue-i18n'\nimport { en } from '../locales/en'\nimport { fr } from '../locales/fr'\nimport { ja } from '../locales/ja'\nimport { zh } from '../locales/zh'\n\n// -----------------------------------------------------------------------------\n// to add a new language, add the language file to the locales folder and add the language id to the LANG_LIST array\n// and \"import { xx } from '../locales/xx'\" at the top of this file\n// and add the language to the messages object below\nexport const LANG_LIST: string[] = ['en', 'zh', 'ja', 'fr']\n// -----------------------------------------------------------------------------\n\nconst i18n = createI18n({\n  legacy: false,\n  fallbackLocale: 'en',\n  globalInjection: true, // 全局注册$t方法\n  messages: {\n    en,\n    zh,\n    ja,\n    fr,\n  },\n})\n\nexport default i18n\n"
  },
  {
    "path": "src/renderer/src/public/index.html",
    "content": "<!doctype html>\n<html lang=\"\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" />\n    <meta name=\"viewport\" content=\"width=device-width,initial-scale=1.0\" />\n    <link rel=\"icon\" href=\"./favicon.ico\" />\n    <title><%= htmlWebpackPlugin.options.title %></title>\n  </head>\n  <body>\n    <noscript>\n      <strong\n        >We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without\n        JavaScript enabled. Please enable it to continue.</strong\n      >\n    </noscript>\n    <div id=\"app\"></div>\n    <!-- built files will be auto injected -->\n  </body>\n</html>\n"
  },
  {
    "path": "src/renderer/src/public/robots.txt",
    "content": "User-agent: *\nDisallow:\n"
  },
  {
    "path": "src/renderer/src/router/index.ts",
    "content": "import { createRouter, createWebHashHistory } from 'vue-router'\nimport Final2xHome from '../views/Final2xHome.vue'\nimport Final2xSettings from '../views/Final2xSettings.vue'\n\nexport default createRouter({\n  history: createWebHashHistory(),\n  routes: [\n    {\n      path: '/',\n      redirect: '/Final2xHome',\n    },\n    {\n      path: '/Final2xHome',\n      name: 'Final2xHome',\n      component: Final2xHome,\n    },\n    {\n      path: '/Final2xSettings',\n      name: 'Final2xSettings',\n      component: Final2xSettings,\n    },\n  ],\n})\n"
  },
  {
    "path": "src/renderer/src/store/SRSettingsStore.ts",
    "content": "import type { Ref } from 'vue'\nimport { defineStore } from 'pinia'\nimport { ref } from 'vue'\n\nexport const useSRSettingsStore = defineStore(\n  'SRSettings',\n  () => {\n    const selectedSRModel = ref('RealESRGAN_RealESRGAN_x2plus_2x.pth')\n    const selectedTorchDevice = ref('auto')\n    const ghProxy: Ref<string | null> = ref(null)\n    const targetScale: Ref<number | null> = ref(null)\n    const useTile: Ref<boolean> = ref(true)\n    const saveFormat: Ref<string> = ref('.png')\n\n    return {\n      selectedSRModel,\n      selectedTorchDevice,\n      ghProxy,\n      targetScale,\n      useTile,\n      saveFormat,\n    }\n  },\n  {\n    persist: true,\n  },\n)\n"
  },
  {
    "path": "src/renderer/src/store/globalSettingsStore.ts",
    "content": "import type { LogInst } from 'naive-ui'\nimport type { Ref } from 'vue'\nimport type { NaiveDarkModeType } from '../components/NaiveDarkMode.vue'\nimport { defineStore } from 'pinia'\nimport { ref } from 'vue'\n\nexport const useGlobalSettingsStore = defineStore(\n  'GlobalSettings',\n  () => {\n    const darkMode: Ref<NaiveDarkModeType> = ref('system')\n    const globalcolor = ref('#fffafa')\n    const naiveTheme: Ref<any> = ref(undefined)\n\n    const changeRoute = ref(false)\n\n    const langsNum = ref(114514)\n\n    const ProgressPercentage = ref(0)\n    const CommandLOG = ref('')\n    const logInstRef = ref<LogInst | null>(null)\n    const StartCommandLock = ref(false)\n    const SrSuccess = ref(false)\n\n    const openOutputFolder = ref(true)\n\n    return {\n      darkMode,\n      globalcolor,\n      naiveTheme,\n      changeRoute,\n      langsNum,\n      ProgressPercentage,\n      CommandLOG,\n      StartCommandLock,\n      SrSuccess,\n      logInstRef,\n      openOutputFolder,\n    }\n  },\n  {\n    persist: {\n      pick: [\n        'langsNum',\n        'darkMode',\n        'naiveTheme',\n        'globalcolor',\n        'openOutputFolder',\n      ],\n    },\n  },\n)\n"
  },
  {
    "path": "src/renderer/src/store/ioPathStore.ts",
    "content": "import type { UploadFileInfo } from 'naive-ui'\nimport { defineStore } from 'pinia'\nimport { ref } from 'vue'\n\nexport const useIOPathStore = defineStore(\n  'IOPath',\n  () => {\n    const inputpathMap = ref<Map<string, string>>(new Map())\n    const inputFileList = ref<UploadFileInfo[]>([])\n\n    const outputpath = ref<string>('')\n    const outputpathLock = ref<boolean>(false)\n\n    return {\n      inputpathMap,\n      inputFileList,\n      outputpath,\n      outputpathLock,\n    }\n  },\n  {\n    persist: {\n      pick: ['outputpath', 'outputpathLock'],\n    },\n  },\n)\n"
  },
  {
    "path": "src/renderer/src/utils/IOPath.ts",
    "content": "import { storeToRefs } from 'pinia'\nimport { useIOPathStore } from '../store/ioPathStore'\nimport PathFormat from '../utils/pathFormat'\n\nexport default class IOPath {\n  /**\n   * @description Add a new inputpath to inputpathMap\n   * @param id inputpath id\n   * @param path inputpath\n   */\n  static add(id: string, path: string): void {\n    const { inputpathMap } = storeToRefs(useIOPathStore())\n    if (path !== '') {\n      inputpathMap.value.set(id, path)\n    }\n  }\n\n  /**\n   * @description Delete an inputpath from inputpathMap by id\n   * @param id inputpath id\n   */\n  static delete(id: string): void {\n    const { inputpathMap } = storeToRefs(useIOPathStore())\n    inputpathMap.value.delete(id)\n  }\n\n  /**\n   * @description 检查 id 是否存在，因为 naive-ui 生成的 id 长度较短，所以这里只检查 inputpathMap 即可\n   * @param id inputpath id\n   */\n  static checkID(id: string): boolean {\n    const { inputpathMap } = storeToRefs(useIOPathStore())\n    return inputpathMap.value.get(id) !== undefined\n  }\n\n  /**\n   * @description Get an inputpath from inputpathMap by id\n   * @param id inputpath id\n   */\n  static getByID(id: string): string {\n    const { inputpathMap } = storeToRefs(useIOPathStore())\n    return inputpathMap.value.get(id) || ''\n  }\n\n  /**\n   * @description Get all inputpath from inputpathMap\n   * @returns inputpathMap with string\n   */\n  static getAllPath(): string {\n    const { inputpathMap } = storeToRefs(useIOPathStore())\n    // return inputpath key and value with string\n    let inputpath = ''\n    inputpathMap.value.forEach((value, key) => {\n      inputpath += `${key} : ${value}\\n`\n    })\n    return inputpath\n  }\n\n  /**\n   * @description Get all inputpath from inputpathMap\n   * @returns inputpathMap string list\n   */\n  static getList(): string[] {\n    const { inputpathMap } = storeToRefs(useIOPathStore())\n    // return inputpath value with String List\n    return Array.from(inputpathMap.value.values())\n  }\n\n  /**\n   * @description check inputpathMap is empty\n   */\n  static isEmpty(): boolean {\n    const { inputpathMap } = storeToRefs(useIOPathStore())\n    return inputpathMap.value.size === 0\n  }\n\n  /**\n   * @description Get all inputpath from inputpathMap\n   * @returns inputpathMap with string\n   */\n  static show(): string {\n    const inputpathList = this.getList()\n    console.log('inputpathList: ', inputpathList)\n    let inputpathListString = ''\n    for (const i in inputpathList) {\n      inputpathListString += `${inputpathList[i]}\\n`\n    }\n    return inputpathListString\n  }\n\n  /**\n   * @description Set outputpath by manual, and lock outputpath\n   */\n  static setoutputpathManual(path: string): void {\n    const { outputpath, outputpathLock } = storeToRefs(useIOPathStore())\n    if (path !== '') {\n      outputpath.value = path\n      outputpathLock.value = true\n      console.log('outputpath SET SUCCESS!')\n    }\n  }\n\n  /**\n   * @description Set outputpath if outputpathLock is false or outputpath is invalid\n   */\n  static setoutputpath(path: string): void {\n    const { outputpath, outputpathLock } = storeToRefs(useIOPathStore())\n    // if outputpathLock is false or outputpath is empty, set outputpath\n    if (path !== '' && (outputpathLock.value === false || !PathFormat.checkPath(outputpath.value))) {\n      outputpath.value = path\n    }\n    else {\n      console.log('outputpath Lock!')\n    }\n  }\n\n  /**\n   * @description get outputpath\n   */\n  static getoutputpath(): string {\n    const { outputpath } = storeToRefs(useIOPathStore())\n    return outputpath.value\n  }\n\n  /**\n   * @description clear all inputpath\n   */\n  static clearALL(): void {\n    const { inputpathMap, inputFileList } = storeToRefs(useIOPathStore())\n    inputpathMap.value.clear()\n    inputFileList.value = []\n  }\n}\n"
  },
  {
    "path": "src/renderer/src/utils/SROptions.ts",
    "content": "import type { Ref } from 'vue'\n\nimport { ref } from 'vue'\n\nexport const torchDeviceList: Ref<any[]> = ref([\n  { value: 'auto', label: 'Auto' },\n  { value: 'cuda', label: 'CUDA' },\n  { value: 'mps', label: 'MPS' },\n  { value: 'cpu', label: 'CPU' },\n])\n\nexport const saveFormatList: Ref<any[]> = ref([\n  { value: '.png', label: 'PNG' },\n  { value: '.jpg', label: 'JPG' },\n  { value: '.webp', label: 'WebP' },\n  { value: '.tiff', label: 'TIFF' },\n])\n"
  },
  {
    "path": "src/renderer/src/utils/getFinal2xCoreConfig.ts",
    "content": "import type { Final2xCoreConfig } from '@shared/type/core'\nimport { useGlobalSettingsStore } from '@renderer/store/globalSettingsStore'\nimport { storeToRefs } from 'pinia'\nimport { useSRSettingsStore } from '../store/SRSettingsStore'\nimport PathFormat from '../utils/pathFormat'\nimport IOPath from './IOPath'\n\n/**\n * @description: 返回输出路径，如果输出路径不合法，则从第一个输入路径构造一个合法输出路径\n */\nfunction getOutPutPATH(): string {\n  if (!PathFormat.checkPath(IOPath.getoutputpath())) {\n    const inputPATHList = IOPath.getList()\n    const pathFormat = new PathFormat()\n    pathFormat.setRootPath(inputPATHList[0])\n    IOPath.setoutputpath(pathFormat.getRootPath())\n  }\n  return IOPath.getoutputpath()\n}\n\n/**\n * @description: 返回最终的json字符串配置文件\n */\nexport function getFinal2xCoreConfig(): Final2xCoreConfig {\n  const { selectedSRModel, ghProxy, targetScale, selectedTorchDevice, useTile, saveFormat } = storeToRefs(useSRSettingsStore())\n  const { openOutputFolder } = storeToRefs(useGlobalSettingsStore())\n\n  const inputPATHList = IOPath.getList()\n  const outputPATH = getOutPutPATH()\n\n  let _gh_proxy: string | null\n  if (ghProxy.value === '') {\n    _gh_proxy = null\n  }\n  else {\n    _gh_proxy = ghProxy.value\n  }\n\n  return {\n    config: {\n      pretrained_model_name: selectedSRModel.value,\n      device: selectedTorchDevice.value,\n      gh_proxy: _gh_proxy,\n      target_scale: targetScale.value,\n      output_path: outputPATH,\n      input_path: inputPATHList,\n      use_tile: useTile.value,\n      save_format: saveFormat.value,\n    },\n    options: {\n      open_output_folder: openOutputFolder.value,\n    },\n  }\n}\n"
  },
  {
    "path": "src/renderer/src/utils/index.ts",
    "content": "import { LANG_LIST } from '../plugins/i18n'\n\nclass Utils {\n  /**\n   * @description 返回语言，和语言数量\n   * @param id 语言id 0-> en, 1-> zh, 2-> ja, 3-> fr\n   */\n  static getLanguage(id: number): { lang: string, numLang: number } {\n    const langs = LANG_LIST\n    return {\n      lang: langs[id],\n      numLang: langs.length,\n    }\n  }\n\n  /**\n   * @description 等待一段时间\n   * @param timeout 等待时间，单位毫秒\n   */\n  static sleep(timeout: number): Promise<void> {\n    return new Promise(resolve => setTimeout(resolve, timeout))\n  }\n\n  /**\n   * @description Deep, Dark, Fantasy? 真·深度睡眠\n   * @param miliseconds 等待时间，单位毫秒\n   */\n  static DeepDeepSleep(miliseconds: number): void {\n    const currentTime = new Date().getTime()\n    while (currentTime + miliseconds >= new Date().getTime()) {\n      /* empty */\n    }\n  }\n\n  /**\n   * @description 生成超长随机字符串\n   */\n  static getRandString(): string {\n    return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15)\n  }\n\n  /**\n   * @description: 防抖函数装饰器，防止用户频繁点击\n   * @param fn  需要防抖的函数\n   * @param delay 防抖的时间间隔，默认500ms\n   */\n  static clickDebounce(\n    fn: (...args: any[]) => void,\n    delay: number = 500,\n  ): (...args: any[]) => void {\n    let timer: NodeJS.Timeout | null = null\n    let immediate = true\n\n    return (...args: any[]) => {\n      if (timer) {\n        clearTimeout(timer)\n      }\n\n      if (immediate) {\n        fn(...args)\n        immediate = false\n      }\n\n      timer = setTimeout(() => {\n        immediate = true\n      }, delay)\n    }\n  }\n}\n\nexport const { getLanguage, sleep, DeepDeepSleep, getRandString, clickDebounce } = Utils\n"
  },
  {
    "path": "src/renderer/src/utils/modelOptions.ts",
    "content": "/* prettier-ignore */\n/* tslint:disable */\n/* This file is automatically generated by Final2x-core */\n/* Do not modify this file manually */\n// -----------------------------------------------------------------------------\n\n/**\n * @description: all SR models provided by ccrestoration\n */\nexport const modelOptions: any[] = [\n  { label: 'RealESRGAN_RealESRGAN_x4plus_4x', value: 'RealESRGAN_RealESRGAN_x4plus_4x.pth' },\n  { label: 'RealESRGAN_RealESRGAN_x4plus_anime_6B_4x', value: 'RealESRGAN_RealESRGAN_x4plus_anime_6B_4x.pth' },\n  { label: 'RealESRGAN_RealESRGAN_x2plus_2x', value: 'RealESRGAN_RealESRGAN_x2plus_2x.pth' },\n  { label: 'RealESRGAN_realesr_animevideov3_4x', value: 'RealESRGAN_realesr_animevideov3_4x.pth' },\n  { label: 'RealESRGAN_AnimeJaNai_HD_V3_Compact_2x', value: 'RealESRGAN_AnimeJaNai_HD_V3_Compact_2x.pth' },\n  { label: 'RealESRGAN_AniScale_2_Compact_2x', value: 'RealESRGAN_AniScale_2_Compact_2x.pth' },\n  { label: 'RealESRGAN_Ani4Kv2_Compact_2x', value: 'RealESRGAN_Ani4Kv2_Compact_2x.pth' },\n  { label: 'RealESRGAN_APISR_RRDB_GAN_generator_2x', value: 'RealESRGAN_APISR_RRDB_GAN_generator_2x.pth' },\n  { label: 'RealESRGAN_APISR_RRDB_GAN_generator_4x', value: 'RealESRGAN_APISR_RRDB_GAN_generator_4x.pth' },\n  { label: 'DAT_S_2x', value: 'DAT_S_2x.pth' },\n  { label: 'DAT_S_3x', value: 'DAT_S_3x.pth' },\n  { label: 'DAT_S_4x', value: 'DAT_S_4x.pth' },\n  { label: 'DAT_2x', value: 'DAT_2x.pth' },\n  { label: 'DAT_3x', value: 'DAT_3x.pth' },\n  { label: 'DAT_4x', value: 'DAT_4x.pth' },\n  { label: 'DAT_2_2x', value: 'DAT_2_2x.pth' },\n  { label: 'DAT_2_3x', value: 'DAT_2_3x.pth' },\n  { label: 'DAT_2_4x', value: 'DAT_2_4x.pth' },\n  { label: 'DAT_light_2x', value: 'DAT_light_2x.pth' },\n  { label: 'DAT_light_3x', value: 'DAT_light_3x.pth' },\n  { label: 'DAT_light_4x', value: 'DAT_light_4x.pth' },\n  { label: 'DAT_APISR_GAN_generator_4x', value: 'DAT_APISR_GAN_generator_4x.pth' },\n  { label: 'HAT_S_2x', value: 'HAT_S_2x.pth' },\n  { label: 'HAT_S_3x', value: 'HAT_S_3x.pth' },\n  { label: 'HAT_S_4x', value: 'HAT_S_4x.pth' },\n  { label: 'HAT_2x', value: 'HAT_2x.pth' },\n  { label: 'HAT_3x', value: 'HAT_3x.pth' },\n  { label: 'HAT_4x', value: 'HAT_4x.pth' },\n  { label: 'HAT_Real_GAN_sharper_4x', value: 'HAT_Real_GAN_sharper_4x.pth' },\n  { label: 'HAT_Real_GAN_4x', value: 'HAT_Real_GAN_4x.pth' },\n  { label: 'HAT_ImageNet_pretrain_2x', value: 'HAT_ImageNet_pretrain_2x.pth' },\n  { label: 'HAT_ImageNet_pretrain_3x', value: 'HAT_ImageNet_pretrain_3x.pth' },\n  { label: 'HAT_ImageNet_pretrain_4x', value: 'HAT_ImageNet_pretrain_4x.pth' },\n  { label: 'HAT_L_ImageNet_pretrain_2x', value: 'HAT_L_ImageNet_pretrain_2x.pth' },\n  { label: 'HAT_L_ImageNet_pretrain_3x', value: 'HAT_L_ImageNet_pretrain_3x.pth' },\n  { label: 'HAT_L_ImageNet_pretrain_4x', value: 'HAT_L_ImageNet_pretrain_4x.pth' },\n  { label: 'RealCUGAN_Conservative_2x', value: 'RealCUGAN_Conservative_2x.pth' },\n  { label: 'RealCUGAN_Denoise1x_2x', value: 'RealCUGAN_Denoise1x_2x.pth' },\n  { label: 'RealCUGAN_Denoise2x_2x', value: 'RealCUGAN_Denoise2x_2x.pth' },\n  { label: 'RealCUGAN_Denoise3x_2x', value: 'RealCUGAN_Denoise3x_2x.pth' },\n  { label: 'RealCUGAN_No_Denoise_2x', value: 'RealCUGAN_No_Denoise_2x.pth' },\n  { label: 'RealCUGAN_Conservative_3x', value: 'RealCUGAN_Conservative_3x.pth' },\n  { label: 'RealCUGAN_Denoise3x_3x', value: 'RealCUGAN_Denoise3x_3x.pth' },\n  { label: 'RealCUGAN_No_Denoise_3x', value: 'RealCUGAN_No_Denoise_3x.pth' },\n  { label: 'RealCUGAN_Conservative_4x', value: 'RealCUGAN_Conservative_4x.pth' },\n  { label: 'RealCUGAN_Denoise3x_4x', value: 'RealCUGAN_Denoise3x_4x.pth' },\n  { label: 'RealCUGAN_No_Denoise_4x', value: 'RealCUGAN_No_Denoise_4x.pth' },\n  { label: 'RealCUGAN_Pro_Conservative_2x', value: 'RealCUGAN_Pro_Conservative_2x.pth' },\n  { label: 'RealCUGAN_Pro_Denoise3x_2x', value: 'RealCUGAN_Pro_Denoise3x_2x.pth' },\n  { label: 'RealCUGAN_Pro_No_Denoise_2x', value: 'RealCUGAN_Pro_No_Denoise_2x.pth' },\n  { label: 'RealCUGAN_Pro_Conservative_3x', value: 'RealCUGAN_Pro_Conservative_3x.pth' },\n  { label: 'RealCUGAN_Pro_Denoise3x_3x', value: 'RealCUGAN_Pro_Denoise3x_3x.pth' },\n  { label: 'RealCUGAN_Pro_No_Denoise_3x', value: 'RealCUGAN_Pro_No_Denoise_3x.pth' },\n  { label: 'EDSR_Mx2_f64b16_DIV2K_official_2x', value: 'EDSR_Mx2_f64b16_DIV2K_official_2x.pth' },\n  { label: 'EDSR_Mx3_f64b16_DIV2K_official_3x', value: 'EDSR_Mx3_f64b16_DIV2K_official_3x.pth' },\n  { label: 'EDSR_Mx4_f64b16_DIV2K_official_4x', value: 'EDSR_Mx4_f64b16_DIV2K_official_4x.pth' },\n  { label: 'SwinIR_classicalSR_DF2K_s64w8_SwinIR_M_2x', value: 'SwinIR_classicalSR_DF2K_s64w8_SwinIR_M_2x.pth' },\n  { label: 'SwinIR_lightweightSR_DIV2K_s64w8_SwinIR_S_2x', value: 'SwinIR_lightweightSR_DIV2K_s64w8_SwinIR_S_2x.pth' },\n  { label: 'SwinIR_realSR_BSRGAN_DFOWMFC_s64w8_SwinIR_L_GAN_4x', value: 'SwinIR_realSR_BSRGAN_DFOWMFC_s64w8_SwinIR_L_GAN_4x.pth' },\n  { label: 'SwinIR_realSR_BSRGAN_DFO_s64w8_SwinIR_M_GAN_2x', value: 'SwinIR_realSR_BSRGAN_DFO_s64w8_SwinIR_M_GAN_2x.pth' },\n  { label: 'SwinIR_realSR_BSRGAN_DFO_s64w8_SwinIR_M_GAN_4x', value: 'SwinIR_realSR_BSRGAN_DFO_s64w8_SwinIR_M_GAN_4x.pth' },\n  { label: 'SwinIR_Bubble_AnimeScale_SwinIR_Small_v1_2x', value: 'SwinIR_Bubble_AnimeScale_SwinIR_Small_v1_2x.pth' },\n  { label: 'SCUNet_color_50_1x', value: 'SCUNet_color_50_1x.pth' },\n  { label: 'SCUNet_color_real_psnr_1x', value: 'SCUNet_color_real_psnr_1x.pth' },\n  { label: 'SCUNet_color_real_gan_1x', value: 'SCUNet_color_real_gan_1x.pth' },\n  { label: 'SRCNN_2x', value: 'SRCNN_2x.pth' },\n  { label: 'SRCNN_3x', value: 'SRCNN_3x.pth' },\n  { label: 'SRCNN_4x', value: 'SRCNN_4x.pth' },\n]\n"
  },
  {
    "path": "src/renderer/src/utils/pathFormat.ts",
    "content": "class PathFormat {\n  private rootpath: string\n\n  constructor() {\n    this.rootpath = ''\n  }\n\n  /**\n   * @description 设置本次上传的根目录\n   */\n  setRootPath(path: string): void {\n    const segments = path.split(/[/\\\\]/)\n    if (segments.length > 1) {\n      segments.pop()\n      this.rootpath = segments.join(path.startsWith('/') ? '/' : '\\\\')\n    }\n  }\n\n  /**\n   * @description 返回本次上传的根目录\n   */\n  getRootPath(): string {\n    return this.rootpath\n  }\n\n  /**\n   * @description 相对于本次上传的根目录，返回拼接后的真实路径\n   */\n  getNewPath(path: string): string {\n    const segments = path.split(/[/\\\\]/)\n    return this.rootpath + segments.join(this.rootpath.startsWith('/') ? '/' : '\\\\')\n  }\n\n  /**\n   * @description 检查路径格式是否正确\n   */\n  static checkPath(path: string): boolean {\n    return path.startsWith('/') || path.includes('\\\\')\n  }\n\n  /**\n   * @description 返回文件名\n   */\n  static getFileName(path: string): string {\n    const segments = path.split(/[/\\\\]/)\n    return segments[segments.length - 1]\n  }\n}\n\nexport default PathFormat\n"
  },
  {
    "path": "src/renderer/src/utils/switchLanguage.ts",
    "content": "import { storeToRefs } from 'pinia'\nimport { getLanguage } from '.'\nimport { useGlobalSettingsStore } from '../store/globalSettingsStore'\n\n/**\n * @description 切换语言，第一次切换到中文\n */\nexport function switchLanguage(): void {\n  const { langsNum } = storeToRefs(useGlobalSettingsStore())\n  if (langsNum.value === 114514) {\n    langsNum.value = 1\n  }\n  else {\n    langsNum.value = (langsNum.value + 1) % getLanguage(0).numLang\n  }\n}\n"
  },
  {
    "path": "src/renderer/src/views/Final2xHome.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { UploadFileInfo } from 'naive-ui'\nimport { IpcChannelInvoke } from '@shared/const/ipc'\nimport { FileImageOutlined } from '@vicons/antd'\nimport { useNotification } from 'naive-ui'\nimport { storeToRefs } from 'pinia'\nimport { onMounted } from 'vue'\nimport { useI18n } from 'vue-i18n'\nimport { useIOPathStore } from '../store/ioPathStore'\nimport { getRandString } from '../utils'\nimport IOPath from '../utils/IOPath'\nimport PathFormat from '../utils/pathFormat'\n\nconst { t } = useI18n()\nconst notification = useNotification()\nconst useIOPath = useIOPathStore()\nconst { inputFileList } = storeToRefs(useIOPath)\n\nconst pathFormat = new PathFormat()\n\nclass Final2xHomeNotifications {\n  static handleremove(s: string): void {\n    notification.success({\n      title: t('Final2xHome.text0'),\n      content: s,\n      duration: 1000,\n    })\n  }\n}\n\nfunction handleClickUpload(): void {\n  const handleSelected = (_, path): void => {\n    if (path !== undefined) {\n      path.forEach((p: string) => {\n        // 生成随机id\n        let pathid = getRandString()\n        while (IOPath.checkID(pathid)) {\n          pathid = getRandString()\n        }\n        // console.log(pathid)\n        // 插入 inputpathMap\n        IOPath.add(pathid, p)\n        // 插入 inputFileList\n        inputFileList.value.push({\n          fullPath: p,\n          id: pathid,\n          name: PathFormat.getFileName(p),\n          percentage: 0,\n          status: 'pending',\n          thumbnailUrl: null,\n          type: 'image',\n          url: null,\n        })\n      })\n    }\n  }\n\n  window.electron.ipcRenderer.invoke(IpcChannelInvoke.OPEN_DIRECTORY_DIALOG, ['openFile', 'multiSelections'])\n    .then((path) => {\n      handleSelected(null, path)\n    })\n    .catch((error) => {\n      console.error('Error selecting file:', error)\n    })\n}\n\nonMounted(() => {\n  const dragWrapper = document.getElementById('file_drag')\n  dragWrapper?.addEventListener('drop', (e) => {\n    // 阻止默认行为\n    e.preventDefault()\n    // 获取文件列表\n    const files = e.dataTransfer?.files\n\n    if (files && files.length > 0) {\n      const path = files[0].path // Get file path, the path is absolute path, electron can use directly\n      console.log(path)\n      pathFormat.setRootPath(path)\n      console.log(pathFormat.getRootPath())\n      IOPath.setoutputpath(pathFormat.getRootPath())\n    }\n  })\n  // 阻止拖拽结束事件默认行为\n  dragWrapper?.addEventListener('dragover', (e) => {\n    e.preventDefault()\n  })\n})\n\nfunction handleUploadChange(data: { fileList: UploadFileInfo[] }): void {\n  // console.log(data.fileList)\n  // console.log(inputFileList.value)\n  inputFileList.value = data.fileList\n}\n\nfunction handleBeforeUpload(options: { file: UploadFileInfo }): UploadFileInfo {\n  // console.log(pathFormat.getNewPath(options.file.fullPath))\n  IOPath.add(options.file.id, pathFormat.getNewPath(String(options.file.fullPath)))\n  return options.file\n}\n\nfunction handleRemove(options: { file: UploadFileInfo, fileList: Array<UploadFileInfo> }): boolean {\n  // console.log(ioPATH.show())\n  // console.log(options.file.id)\n  Final2xHomeNotifications.handleremove(IOPath.getByID(options.file.id))\n  IOPath.delete(options.file.id)\n  return true\n}\n</script>\n\n<template>\n  <div id=\"file_drag\" class=\"for_file_drag\" @click.prevent>\n    <n-upload\n      v-model:file-list=\"inputFileList\"\n      multiple\n      directory-dnd\n      class=\"n-upload\"\n      @remove=\"handleRemove\"\n      @before-upload=\"handleBeforeUpload\"\n      @change=\"handleUploadChange\"\n    >\n      <n-upload-dragger class=\"file-drag-zone\" @click=\"handleClickUpload\">\n        <div class=\"file-drag-zone-logo-text\">\n          <div style=\"margin-bottom: 12px\">\n            <n-icon size=\"48\" depth=\"3.0\">\n              <FileImageOutlined />\n            </n-icon>\n          </div>\n          <n-text style=\"font-size: 16px\">\n            {{ t('Final2xHome.text1') }}\n          </n-text>\n        </div>\n      </n-upload-dragger>\n    </n-upload>\n  </div>\n</template>\n\n<style lang=\"scss\" scoped>\n.for_file_drag {\n  width: 100%;\n  height: 100%;\n  padding: 0 12%;\n  box-sizing: border-box;\n  overflow: scroll;\n  overflow-x: hidden;\n  display: flex;\n  flex-direction: column;\n  justify-content: center;\n  .file-drag-zone-logo-text {\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    justify-content: center;\n  }\n}\n\n.n-upload {\n  display: flex;\n  flex-direction: column;\n}\n\n.n-upload :deep .n-upload-file-list {\n  max-height: calc(100vh - 370px);\n  overflow-y: auto;\n  overflow-x: hidden;\n  &::-webkit-scrollbar {\n    width: 6px;\n    height: 6px;\n  }\n  &::-webkit-scrollbar-track {\n    border-radius: 3px;\n    background: rgba(0, 0, 0, 0.06);\n    -webkit-box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.08);\n  }\n  &::-webkit-scrollbar-thumb {\n    border-radius: 3px;\n    background: rgba(0, 0, 0, 0.12);\n    -webkit-box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.2);\n  }\n}\n</style>\n"
  },
  {
    "path": "src/renderer/src/views/Final2xSettings.vue",
    "content": "<script lang=\"ts\" setup>\nimport { IpcChannelInvoke } from '@shared/const/ipc'\nimport { storeToRefs } from 'pinia'\nimport { useI18n } from 'vue-i18n'\nimport { useGlobalSettingsStore } from '../store/globalSettingsStore'\nimport { useIOPathStore } from '../store/ioPathStore'\nimport { useSRSettingsStore } from '../store/SRSettingsStore'\nimport IOPath from '../utils/IOPath'\nimport { modelOptions } from '../utils/modelOptions'\nimport { saveFormatList, torchDeviceList } from '../utils/SROptions'\n\nconst { openOutputFolder }\n  = storeToRefs(useGlobalSettingsStore())\nconst { selectedSRModel, ghProxy, targetScale, selectedTorchDevice, useTile, saveFormat } = storeToRefs(useSRSettingsStore())\nconst { outputpath } = storeToRefs(useIOPathStore())\nconst { t } = useI18n()\n\nfunction getPath(): void {\n  const handleSelected = (_, path): void => {\n    if (path[0] !== undefined) {\n      // console.log(ioPath.getoutputpath())\n      IOPath.setoutputpathManual(path[0])\n    }\n  }\n\n  window.electron.ipcRenderer.invoke(IpcChannelInvoke.OPEN_DIRECTORY_DIALOG, ['openDirectory'])\n    .then((path) => {\n      handleSelected(null, path)\n    })\n    .catch((error) => {\n      console.error('Error selecting directory:', error)\n    })\n}\n</script>\n\n<template>\n  <n-card :bordered=\"false\" class=\"settings-card\">\n    <n-space class=\"vertical\" vertical justify=\"center\">\n      <n-space>\n        <n-button dashed type=\"success\" style=\"width: 80px\">\n          {{ t('Final2xSettings.text11') }}\n        </n-button>\n        <n-select\n          v-model:value=\"selectedSRModel\"\n          :options=\"modelOptions\"\n          filterable\n          tag\n          clearable\n          style=\"width: 465px\"\n        />\n      </n-space>\n\n      <n-space>\n        <n-button dashed type=\"success\" style=\"width: 80px\">\n          {{ t('Final2xSettings.text10') }}\n        </n-button>\n\n        <n-select\n          v-model:value=\"selectedTorchDevice\"\n          :options=\"torchDeviceList\"\n          style=\"width: 150px\"\n        />\n\n        <n-button dashed type=\"success\" style=\"width: 120px\">\n          {{ t('Final2xSettings.text15') }}\n        </n-button>\n\n        <n-input-number\n          v-model:value=\"targetScale\"\n          :max=\"99999999\"\n          :min=\"0\"\n          :step=\"0.2\"\n          :placeholder=\"t('Final2xSettings.text16')\"\n          style=\"width: 171px\"\n        />\n      </n-space>\n\n      <n-space>\n        <n-button dashed type=\"success\" style=\"width: 80px\">\n          {{ t('Final2xSettings.text19') }}\n        </n-button>\n\n        <n-select\n          v-model:value=\"saveFormat\"\n          :options=\"saveFormatList\"\n          style=\"width: 150px\"\n        />\n\n        <n-button dashed type=\"success\" style=\"width: 120px\">\n          {{ t('Final2xSettings.text20') }}\n        </n-button>\n\n        <n-switch v-model:value=\"useTile\" size=\"large\" style=\"height: 35px; width: 76px\">\n          <template #checked>\n            ON\n          </template>\n          <template #unchecked>\n            OFF\n          </template>\n        </n-switch>\n      </n-space>\n\n      <n-space>\n        <n-button dashed type=\"success\" style=\"width: 80px\">\n          {{ t('Final2xSettings.text18') }}\n        </n-button>\n\n        <n-input\n          v-model:value=\"ghProxy\"\n          placeholder=\"Github Proxy, Example: https://github.abskoop.workers.dev/\"\n          style=\"width: 465px\"\n        />\n      </n-space>\n\n      <n-space>\n        <n-button round type=\"success\" style=\"height: 35px; width: 150px\" @click=\"getPath\">\n          {{ t('Final2xSettings.text17') }}\n        </n-button>\n\n        <n-switch v-model:value=\"openOutputFolder\" size=\"large\" style=\"height: 35px; width: 76px\">\n          <template #checked>\n            OPEN\n          </template>\n        </n-switch>\n\n        <n-input v-model:value=\"outputpath\" :placeholder=\"outputpath\" round style=\"width: 308px\" />\n      </n-space>\n    </n-space>\n  </n-card>\n</template>\n\n<style lang=\"scss\" scoped>\n.settings-card {\n  width: fit-content;\n  margin: 0 auto;\n  height: 100%;\n  // transparent\n  background-color: rgba(255, 255, 255, 0);\n\n  .vertical {\n    height: 100%;\n\n    > div {\n      margin-bottom: 20px;\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "src/shared/const/ipc.ts",
    "content": "/**\n * 渲染进程 → 主进程（invoke/handle）\n */\nexport enum IpcChannelInvoke {\n  OPEN_DIRECTORY_DIALOG = 'ipc:open-directory-dialog',\n}\n\n/**\n * 渲染进程 → 主进程（send/on，单向）\n */\nexport enum IpcChannelSend {\n  EXECUTE_COMMAND = 'ipc:send:execute-command',\n  KILL_COMMAND = 'ipc:send:kill-command',\n  MINIMIZE = 'ipc:send:minimize',\n  MAXIMIZE = 'ipc:send:maximize',\n  CLOSE = 'ipc:send:close',\n}\n\n/**\n * 主进程 → 渲染进程（send/on，主进程主动 emit）\n */\nexport enum IpcChannelOn {\n  COMMAND_STDOUT = 'ipc:on:command-stdout',\n  COMMAND_STDERR = 'ipc:on:command-stderr',\n  COMMAND_CLOSE = 'ipc:on:command-close-code',\n}\n"
  },
  {
    "path": "src/shared/type/core.ts",
    "content": "export interface Final2xCoreConfig {\n  config: {\n    pretrained_model_name: string\n    device: string\n    gh_proxy: string | null\n    target_scale: number | null\n    output_path: string\n    input_path: string[]\n    use_tile: boolean\n    save_format: string\n  }\n  options: {\n    open_output_folder: boolean\n  }\n}\n"
  },
  {
    "path": "test/node/getCorePath.test.ts",
    "content": "import { checkPipPackage } from '@main/getCorePath'\nimport { describe, expect, it } from 'vitest'\n\ndescribe('getFinal2xCorePath', () => {\n  it('checkPipPackage should return false when the pip package is not available', () => {\n    expect(checkPipPackage()).toEqual(false)\n  })\n})\n"
  },
  {
    "path": "test/web/IOPath.test.ts",
    "content": "import { useIOPathStore } from '@renderer/store/ioPathStore'\nimport IOPath from '@renderer/utils/IOPath'\nimport { createPinia, setActivePinia, storeToRefs } from 'pinia'\nimport { beforeEach, describe, expect, it } from 'vitest'\n\ndescribe('ioPath', () => {\n  beforeEach(() => {\n    setActivePinia(createPinia())\n  })\n\n  it('test_IOPath', () => {\n    const { outputpath } = storeToRefs(useIOPathStore())\n    // checkID\n    expect(IOPath.checkID('114514')).toBe(false)\n    // test inputpath\n    IOPath.add('114514', 'test')\n    // checkID\n    expect(IOPath.checkID('114514')).toBe(true)\n    expect(IOPath.getByID('114514')).toBe('test')\n    IOPath.add('114514', 'test2')\n    expect(IOPath.getByID('114514')).toBe('test2')\n\n    expect(IOPath.getList()).toEqual(['test2'])\n\n    expect(IOPath.getAllPath()).toEqual('114514 : test2\\n')\n    expect(IOPath.show()).toEqual('test2\\n')\n\n    IOPath.delete('114514')\n    expect(IOPath.getByID('114514')).toBe('')\n\n    expect(IOPath.isEmpty()).toBe(true)\n\n    // test outputpath\n    IOPath.setoutputpath('/test')\n    expect(IOPath.getoutputpath()).toBe('/test')\n    IOPath.setoutputpathManual('/test2')\n    expect(IOPath.getoutputpath()).toBe('/test2')\n    IOPath.setoutputpath('')\n    expect(IOPath.getoutputpath()).toBe('/test2')\n    outputpath.value = '' // 模拟用户手动清除outputpath\n    IOPath.setoutputpath('/testWhenEmpty')\n    expect(IOPath.getoutputpath()).toBe('/testWhenEmpty')\n    IOPath.setoutputpathManual('/test2')\n\n    // clear ALL\n    IOPath.add('114514', 'test')\n    IOPath.clearALL()\n    expect(IOPath.getList()).toEqual([])\n    expect(IOPath.isEmpty()).toBe(true)\n    expect(IOPath.getoutputpath()).toBe('/test2')\n  })\n})\n"
  },
  {
    "path": "test/web/index.test.ts",
    "content": "import { clickDebounce, DeepDeepSleep, getRandString, sleep } from '@renderer/utils'\nimport { describe, expect, it, vi } from 'vitest'\n\ndescribe('utils', () => {\n  it('sleep', async () => {\n    const start = new Date().getTime()\n    await sleep(1010)\n    const end = new Date().getTime()\n    expect(end - start).toBeGreaterThanOrEqual(1000)\n  })\n\n  it('deepDeepSleep', () => {\n    const start = new Date().getTime()\n    DeepDeepSleep(1010)\n    const end = new Date().getTime()\n    expect(end - start).toBeGreaterThanOrEqual(1000)\n  })\n\n  it('getRandString', () => {\n    expect(getRandString())\n  })\n\n  it('clickDebounce', async () => {\n    const fn = (): void => console.log('click')\n    // spy on fn to check if it's called\n    const spy = vi.spyOn(console, 'log')\n    // call fn 3 times\n    const debouncedFn = clickDebounce(fn, 1000)\n    debouncedFn()\n    debouncedFn()\n    debouncedFn()\n    // check if fn is called only once\n    expect(spy).toHaveBeenCalledTimes(1)\n    // await new Promise((resolve) => setTimeout(resolve, 1000));\n    await sleep(1000)\n    debouncedFn()\n    expect(spy).toHaveBeenCalledTimes(2)\n  })\n})\n"
  },
  {
    "path": "test/web/pathFormat.test.ts",
    "content": "import PathFormat from '@renderer/utils/pathFormat'\nimport { describe, expect, it } from 'vitest'\n\ndescribe('pathFormat', () => {\n  it('test_unix', () => {\n    const pathFormat = new PathFormat()\n    pathFormat.setRootPath('/Users/test/Downloads/unix')\n    const check: Array<string> = [pathFormat.getRootPath(), pathFormat.getNewPath('/unix/test.txt')]\n    expect(check).toStrictEqual(['/Users/test/Downloads', '/Users/test/Downloads/unix/test.txt'])\n  })\n\n  it('test_win', () => {\n    const pathFormat = new PathFormat()\n    pathFormat.setRootPath('C:\\\\Users\\\\test\\\\Downloads\\\\win')\n    const check: Array<string> = [pathFormat.getRootPath(), pathFormat.getNewPath('/win/test.txt')]\n    expect(check).toStrictEqual([\n      'C:\\\\Users\\\\test\\\\Downloads',\n      'C:\\\\Users\\\\test\\\\Downloads\\\\win\\\\test.txt',\n    ])\n  })\n\n  it('check_path', () => {\n    const check: Array<boolean> = [\n      PathFormat.checkPath('/Users/test/Downloads/unix'),\n      PathFormat.checkPath('C:\\\\Users\\\\test\\\\Downloads\\\\win'),\n      PathFormat.checkPath('C:Users/test/Downloads/unix/test.txt'),\n      PathFormat.checkPath('Users/test/Downloads/unix/test.txt'),\n    ]\n    expect(check).toStrictEqual([true, true, false, false])\n  })\n\n  it('get_file_name', () => {\n    expect(PathFormat.getFileName('/Users/test/Downloads/unix/114514.txt')).toBe('114514.txt')\n    expect(PathFormat.getFileName('C:\\\\Users\\\\test\\\\Downloads\\\\win\\\\genshin.png')).toBe(\n      'genshin.png',\n    )\n  })\n})\n"
  },
  {
    "path": "test/web/switchLanguage.test.ts",
    "content": "import { useGlobalSettingsStore } from '@renderer/store/globalSettingsStore'\nimport { getLanguage } from '@renderer/utils'\nimport { switchLanguage } from '@renderer/utils/switchLanguage'\nimport { createPinia, setActivePinia, storeToRefs } from 'pinia'\nimport { beforeEach, describe, expect, it } from 'vitest'\n\ndescribe('switchLanguage', () => {\n  beforeEach(() => {\n    // 创建一个新 pinia，并使其处于激活状态\n    setActivePinia(createPinia())\n  })\n\n  it('test_switchLanguage', () => {\n    const { langsNum } = storeToRefs(useGlobalSettingsStore())\n    switchLanguage()\n    expect(langsNum.value).toBe(1) // 第一次后应该是 'zh'\n    langsNum.value = 0 // 手动设置为 'en'\n    // 断言语言切换是否正确\n    expect(langsNum.value).toBe(0) // 初始语言是 'en', 所以切换一次后应该是 'zh', 切换两次后应该是 'ja', 切换三次后应该是 'en'\n    const numLang = getLanguage(0).numLang\n    for (let i = 0; i < 30; i++) {\n      expect(langsNum.value).toBe(i % numLang)\n      switchLanguage()\n    }\n  })\n})\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"types\": [\n      \"vitest\",\n      \"vitest/globals\"\n    ]\n  },\n  \"references\": [{ \"path\": \"./tsconfig.node.json\" }, { \"path\": \"./tsconfig.web.json\" }],\n  \"files\": []\n}\n"
  },
  {
    "path": "tsconfig.node.json",
    "content": "{\n  \"extends\": \"@electron-toolkit/tsconfig/tsconfig.node.json\",\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@main/*\": [\n        \"src/main/*\"\n      ],\n      \"@shared/*\": [\n        \"src/shared/*\"\n      ]\n    },\n    \"types\": [\"electron-vite/node\"]\n  },\n  \"include\": [\n    \"electron.vite.config.*\",\n    \"src/main/**/*\",\n    \"src/preload/**/*\",\n    \"src/preload/*.d.ts\",\n    \"src/shared/**/*\",\n    \"test/node/**/*\"\n  ]\n}\n"
  },
  {
    "path": "tsconfig.web.json",
    "content": "{\n  \"extends\": \"@electron-toolkit/tsconfig/tsconfig.web.json\",\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@renderer/*\": [\n        \"src/renderer/src/*\"\n      ],\n      \"@shared/*\": [\n        \"src/shared/*\"\n      ]\n    }\n  },\n  \"include\": [\n    \"src/renderer/src/env.d.ts\",\n    \"src/renderer/src/locales/*.ts\",\n    \"src/renderer/src/**/*\",\n    \"src/renderer/src/**/*.vue\",\n    \"src/preload/*.d.ts\",\n    \"src/shared/**/*\",\n    \"test/web/**/*\"\n  ]\n}\n"
  },
  {
    "path": "vitest.config.ts",
    "content": "import tsconfigPaths from 'vite-tsconfig-paths'\nimport { defineConfig } from 'vitest/config'\n\nexport default defineConfig({\n  test: {\n    projects: [\n      {\n        plugins: [tsconfigPaths()],\n        test: {\n          name: 'web',\n          root: 'test/web',\n          include: ['**/*.test.ts'],\n          environment: 'jsdom',\n        },\n      },\n      {\n        plugins: [tsconfigPaths()],\n        test: {\n          name: 'node',\n          root: 'test/node',\n          include: ['**/*.test.ts'],\n          environment: 'node',\n        },\n      },\n    ],\n  },\n})\n"
  }
]