├── .editorconfig ├── .eslintignore ├── .eslintrc.cjs ├── .github ├── ISSUE_TEMPLATE │ ├── bug.yaml │ ├── config.yml │ └── feature.yaml ├── dependabot.yml └── workflows │ ├── CI-build.yml │ ├── CI-test.yml │ ├── Release.yml │ ├── issue-helper.yml │ └── issue-translator.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.yaml ├── LICENSE ├── README.md ├── README_i18n └── README_zh.md ├── build ├── entitlements.mac.plist ├── icon.icns ├── icon.ico ├── icon.png ├── macosDMGbg.jpeg └── notarize.js ├── electron-builder.yml ├── electron.vite.config.ts ├── package.json ├── pnpm-lock.yaml ├── resources ├── download-core.js ├── icon.png ├── taryTemplate.png ├── tray.png └── trayTemplate@2x.png ├── src ├── main │ ├── RunCommand.ts │ ├── index.ts │ └── openDirectory.ts ├── preload │ ├── index.d.ts │ └── index.ts └── renderer │ ├── index.html │ └── src │ ├── App.vue │ ├── assets │ ├── final2xlogo.png │ └── final2xlogoDarkMode.png │ ├── components │ ├── MyDarkMode.vue │ ├── MyExternalLink.vue │ ├── MyProgress.vue │ ├── MySetting.vue │ ├── NaiveDarkMode.vue │ ├── TrafficLightsButtons.vue │ └── bottomNavigation.vue │ ├── env.d.ts │ ├── locales │ ├── en.ts │ ├── fr.ts │ ├── ja.ts │ └── zh.ts │ ├── main.ts │ ├── plugins │ └── i18n.ts │ ├── public │ ├── favicon.ico │ ├── img │ │ └── icons │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── android-chrome-maskable-192x192.png │ │ │ ├── android-chrome-maskable-512x512.png │ │ │ ├── apple-touch-icon-120x120.png │ │ │ ├── apple-touch-icon-152x152.png │ │ │ ├── apple-touch-icon-180x180.png │ │ │ ├── apple-touch-icon-60x60.png │ │ │ ├── apple-touch-icon-76x76.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── msapplication-icon-144x144.png │ │ │ ├── mstile-150x150.png │ │ │ └── safari-pinned-tab.svg │ ├── index.html │ └── robots.txt │ ├── router │ └── index.ts │ ├── store │ ├── SRSettingsStore.ts │ ├── globalSettingsStore.ts │ └── ioPathStore.ts │ ├── utils │ ├── IOPath.ts │ ├── ModelOptions.ts │ ├── getFinal2xconfig.ts │ ├── index.ts │ ├── pathFormat.ts │ └── switchLanguage.ts │ └── views │ ├── Final2xHome.vue │ └── Final2xSettings.vue ├── test └── utils │ ├── IOPath.test.ts │ ├── getFinal2xconfig.test.ts │ ├── index.test.ts │ ├── pathFormat.test.ts │ └── switchLanguage.test.ts ├── tsconfig.json ├── tsconfig.node.json ├── tsconfig.web.json └── vitest.config.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | out 4 | .gitignore 5 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | require('@rushstack/eslint-patch/modern-module-resolution') 3 | 4 | module.exports = { 5 | root: true, 6 | env: { 7 | browser: true, 8 | commonjs: true, 9 | es6: true, 10 | node: true, 11 | 'vue/setup-compiler-macros': true 12 | }, 13 | extends: [ 14 | 'plugin:vue/vue3-recommended', 15 | 'eslint:recommended', 16 | '@vue/eslint-config-typescript/recommended', 17 | '@vue/eslint-config-prettier' 18 | ], 19 | // rules for typescript 20 | rules: { 21 | '@typescript-eslint/ban-ts-comment': ['error', { 'ts-ignore': 'allow-with-description' }], 22 | '@typescript-eslint/explicit-function-return-type': 'error', 23 | '@typescript-eslint/explicit-module-boundary-types': 'off', 24 | '@typescript-eslint/no-empty-function': ['error', { allow: ['arrowFunctions'] }], 25 | '@typescript-eslint/no-explicit-any': ['off'], 26 | '@typescript-eslint/no-non-null-assertion': 'off', 27 | '@typescript-eslint/no-var-requires': 'off', 28 | '@typescript-eslint/no-inferrable-types': 'off', 29 | 'vue/require-default-prop': 'off', 30 | 'vue/multi-word-component-names': 'off' 31 | }, 32 | overrides: [ 33 | { 34 | files: ['*.js'], 35 | rules: { 36 | '@typescript-eslint/explicit-function-return-type': 'off' 37 | } 38 | } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.yaml: -------------------------------------------------------------------------------- 1 | name: 🐛 Bug report | 错误报告 | BUG報告 2 | description: Create a bug report to help us improve | 创建bug报告以帮助我们改进 | 改善を支援するためのレポートを作成する 3 | title: '[Bug] ' 4 | labels: ['bug'] 5 | body: 6 | - type: checkboxes 7 | id: checks 8 | attributes: 9 | label: Please carefully review each item in the checklist below | 请认真检查以下清单中的每一项 | 以下のチェックリストの各項目を注意深く確認してください 10 | options: 11 | - label: Searched and didn't find a similar issue | 已经搜索过,没有发现类似issue | 類似の問題が見つかりませんでした 12 | - label: Searched documentation and didn't find relevant content | 已经搜索过文档,没有发现相关内容 | ドキュメントを検索して関連する内容が見つかりませんでした 13 | - label: Tried with the latest version and the issue still exists | 已经尝试使用过最新版,问题依旧存在 | 最新バージョンを試しましたが問題は解消されませんでした 14 | - type: input 15 | id: app-version 16 | attributes: 17 | label: Software Version | 软件版本 | ソフトウェアバージョン 18 | placeholder: '1.1.4' 19 | validations: 20 | required: true 21 | - type: dropdown 22 | id: system-type 23 | attributes: 24 | label: Operating System | 操作系统 | オペレーティングシステム 25 | options: 26 | - Windows x64 27 | - Windows arm64 28 | - macOS x64 (Intel) 29 | - macOS arm64 (M1,M2...) 30 | - Ubuntu x64 31 | - Debian x64 32 | - Arch Linux x64 33 | - Other Linux x64 34 | validations: 35 | required: true 36 | - type: input 37 | id: system-version 38 | attributes: 39 | label: System Version | 系统版本 | システムバージョン 40 | validations: 41 | required: true 42 | - type: textarea 43 | id: description 44 | attributes: 45 | label: Describe the bug | 描述错误 | BUGの説明 46 | description: | 47 | A clear and concise description of what the bug is 48 | 描述错误的详细信息 49 | バグの内容を明確かつ簡潔に説明してください 50 | validations: 51 | required: true 52 | - type: textarea 53 | id: reproduce-steps 54 | attributes: 55 | label: To reproduce | 复现步骤 | 再現方法 56 | description: Steps to reproduce the behavior | 复现行为的步骤 | 不具合の再現手順 57 | value: | 58 | 1. Go to '...' 59 | 2. Click on '....' 60 | 3. See error 61 | validations: 62 | required: true 63 | - type: textarea 64 | id: log 65 | attributes: 66 | label: Error log | 报错日志 | ログ 67 | description: your error log | 您的错误日志 | エラーログ 68 | value: | 69 | ``` 70 | your error log 71 | ``` 72 | validations: 73 | required: true 74 | - type: textarea 75 | id: other 76 | attributes: 77 | label: Additional context | 附加内容 | 追加コンテキスト 78 | description: | 79 | Add any other context and screenshots to help explain your problem 80 | 添加任何其他上下文和截图,以帮助解释您的问题 81 | 問題を説明するために他の文脈やスクリーンショットを追加してください 82 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.yaml: -------------------------------------------------------------------------------- 1 | name: 🚀 Feature request | 功能请求 | フィーチャーリクエスト 2 | description: Suggest an idea for this project | 为项目提供一个创意建议 | このプロジェクトにアイデアを提案する 3 | title: '[FEATURE] ' 4 | labels: ['enhancement'] 5 | body: 6 | - type: checkboxes 7 | id: checks 8 | attributes: 9 | label: Please carefully review each item in the checklist below | 请认真检查以下清单中的每一项 | 以下のチェックリストの各項目を注意深く確認してください 10 | options: 11 | - label: Searched and didn't find a similar issue | 已经搜索过,没有发现类似issue | 類似の問題が見つかりませんでした 12 | - type: textarea 13 | id: is-related 14 | attributes: 15 | label: Is your feature request related to a problem? | 你的feature请求是否与一个问题有关? | あなたのfeatureリクエストは質問に関連していますか? 16 | description: | 17 | A clear and concise description of what the problem is 18 | 请清楚而简明地描述问题是什么 19 | 問題が何であるかを明確かつ簡潔に説明してください 20 | validations: 21 | required: true 22 | - type: textarea 23 | id: detail 24 | attributes: 25 | label: Detail | 详细描述 | 詳細な説明 26 | description: | 27 | A clear and concise description of what you want to happen 28 | 请清楚而简明地描述您想要实现的内容 29 | 実現したい内容を明確かつ簡潔に説明してください 30 | validations: 31 | required: true 32 | - type: textarea 33 | id: log 34 | attributes: 35 | label: Additional context | 附加内容 | 追加コンテキスト 36 | description: | 37 | Add any other context or screenshots about the feature request here 38 | 在这里添加任何其他上下文或截图,以帮助解释您的功能请求 39 | その他の文脈やスクリーンショットを追加して、機能リクエストについて説明してください 40 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: 'npm' # See documentation for possible values 9 | directory: '/' # Location of package manifests 10 | schedule: 11 | interval: 'weekly' 12 | open-pull-requests-limit: 100 13 | -------------------------------------------------------------------------------- /.github/workflows/CI-build.yml: -------------------------------------------------------------------------------- 1 | name: CI-build 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths-ignore: 8 | - '**.md' 9 | - LICENSE 10 | pull_request: 11 | paths-ignore: 12 | - '**.md' 13 | - LICENSE 14 | workflow_dispatch: 15 | 16 | jobs: 17 | windows: 18 | strategy: 19 | matrix: 20 | os-version: ['x64', 'arm64'] 21 | 22 | runs-on: windows-latest 23 | steps: 24 | - uses: actions/checkout@v3 25 | with: 26 | submodules: recursive 27 | 28 | - uses: actions/setup-node@v3 29 | with: 30 | node-version: 20 31 | 32 | - uses: pnpm/action-setup@v2 33 | with: 34 | version: 8 35 | 36 | - name: build 37 | run: | 38 | pnpm install 39 | pnpm run build:win-${{ matrix.os-version }} 40 | env: 41 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 42 | 43 | - name: zip-unpacked-x64 44 | if: matrix.os-version == 'x64' 45 | run: | 46 | cd .\dist\win-unpacked 47 | 7z a -r Final2x-windows-${{ matrix.os-version }}-unpacked.7z * 48 | 49 | - name: zip-unpacked-arm64 50 | if: matrix.os-version == 'arm64' 51 | run: | 52 | cd .\dist\win-arm64-unpacked 53 | 7z a -r Final2x-windows-${{ matrix.os-version }}-unpacked.7z * 54 | 55 | - name: upload-setup 56 | uses: actions/upload-artifact@v3 57 | with: 58 | name: Final2x-windows-${{ matrix.os-version }}-setup 59 | path: dist/*.exe 60 | 61 | - name: upload-unpacked-x64 62 | if: matrix.os-version == 'x64' 63 | uses: actions/upload-artifact@v3 64 | with: 65 | name: Final2x-windows-${{ matrix.os-version }}-unpacked 66 | path: dist/win-unpacked/*.7z 67 | 68 | - name: upload-unpacked-arm64 69 | if: matrix.os-version == 'arm64' 70 | uses: actions/upload-artifact@v3 71 | with: 72 | name: Final2x-windows-${{ matrix.os-version }}-unpacked 73 | path: dist/win-arm64-unpacked/*.7z 74 | 75 | macos: 76 | strategy: 77 | matrix: 78 | os-version: ['x64', 'arm64'] 79 | 80 | runs-on: macos-14 81 | steps: 82 | - uses: actions/checkout@v3 83 | with: 84 | submodules: recursive 85 | 86 | - uses: actions/setup-node@v3 87 | with: 88 | node-version: 20 89 | 90 | - uses: pnpm/action-setup@v2 91 | with: 92 | version: 8 93 | 94 | - name: build 95 | run: | 96 | pnpm install 97 | pnpm run build:mac-${{ matrix.os-version }} 98 | env: 99 | ARCH: ${{ matrix.os-version }} 100 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 101 | 102 | - name: zip-unpacked-x64 103 | if: matrix.os-version == 'x64' 104 | run: | 105 | cd ./dist/mac 106 | 7z a -r Final2x-macos-${{ matrix.os-version }}-unpacked.7z * 107 | 108 | - name: zip-unpacked-arm64 109 | if: matrix.os-version == 'arm64' 110 | run: | 111 | cd ./dist/mac-arm64 112 | 7z a -r Final2x-macos-${{ matrix.os-version }}-unpacked.7z * 113 | 114 | - name: upload-dmg 115 | uses: actions/upload-artifact@v3 116 | with: 117 | name: Final2x-macos-${{ matrix.os-version }}-dmg 118 | path: dist/*.dmg 119 | 120 | - name: upload-unpacked-x64 121 | if: matrix.os-version == 'x64' 122 | uses: actions/upload-artifact@v3 123 | with: 124 | name: Final2x-macos-${{ matrix.os-version }}-unpacked 125 | path: dist/mac/*.7z 126 | 127 | - name: upload-unpacked-arm64 128 | if: matrix.os-version == 'arm64' 129 | uses: actions/upload-artifact@v3 130 | with: 131 | name: Final2x-macos-${{ matrix.os-version }}-unpacked 132 | path: dist/mac-arm64/*.7z 133 | 134 | linux-pip: 135 | strategy: 136 | matrix: 137 | os-version: ['x64'] 138 | 139 | runs-on: ubuntu-20.04 140 | steps: 141 | - uses: actions/checkout@v3 142 | with: 143 | submodules: recursive 144 | 145 | - uses: actions/setup-node@v3 146 | with: 147 | node-version: 20 148 | 149 | - uses: pnpm/action-setup@v2 150 | with: 151 | version: 8 152 | 153 | - name: build 154 | run: | 155 | pnpm install 156 | pnpm run build:linux-${{ matrix.os-version }} 157 | env: 158 | SKIP_DOWNLOAD_CORE: true 159 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 160 | 161 | - name: zip-unpacked 162 | run: | 163 | cd ./dist/linux-unpacked 164 | 7z a -r Final2x-linux-pip-${{ matrix.os-version }}-unpacked.7z * 165 | 166 | - name: upload-snap 167 | uses: actions/upload-artifact@v3 168 | with: 169 | name: Final2x-linux-pip-${{ matrix.os-version }}-snap 170 | path: dist/*.snap 171 | 172 | - name: upload-AppImage 173 | uses: actions/upload-artifact@v3 174 | with: 175 | name: Final2x-linux-pip-${{ matrix.os-version }}-AppImage 176 | path: dist/*.AppImage 177 | 178 | - name: upload-deb 179 | uses: actions/upload-artifact@v3 180 | with: 181 | name: Final2x-linux-pip-${{ matrix.os-version }}-deb 182 | path: dist/*.deb 183 | 184 | - name: upload-unpacked 185 | uses: actions/upload-artifact@v3 186 | with: 187 | name: Final2x-linux-pip-${{ matrix.os-version }}-unpacked 188 | path: dist/linux-unpacked/*.7z 189 | -------------------------------------------------------------------------------- /.github/workflows/CI-test.yml: -------------------------------------------------------------------------------- 1 | name: CI-test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths-ignore: 8 | - '**.md' 9 | - LICENSE 10 | pull_request: 11 | paths-ignore: 12 | - '**.md' 13 | - LICENSE 14 | workflow_dispatch: 15 | 16 | jobs: 17 | test: 18 | strategy: 19 | matrix: 20 | os-version: ['macos-latest', 'windows-latest', 'ubuntu-20.04'] 21 | 22 | runs-on: ${{ matrix.os-version }} 23 | steps: 24 | - uses: actions/checkout@v3 25 | with: 26 | submodules: recursive 27 | 28 | - uses: actions/setup-node@v3 29 | with: 30 | node-version: 20 31 | 32 | - uses: pnpm/action-setup@v2 33 | with: 34 | version: 8 35 | 36 | - name: Test 37 | run: | 38 | pnpm install 39 | pnpm run lint 40 | pnpm run typecheck 41 | pnpm run test-cov 42 | env: 43 | SKIP_DOWNLOAD_CORE: true 44 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 45 | -------------------------------------------------------------------------------- /.github/workflows/Release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | setup: 8 | runs-on: ubuntu-latest 9 | outputs: 10 | DATE: ${{ steps.get_date.outputs.DATE }} 11 | steps: 12 | - name: Get current date 13 | id: get_date 14 | run: echo "DATE=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT 15 | 16 | windows: 17 | strategy: 18 | matrix: 19 | os-version: ['x64', 'arm64'] 20 | 21 | runs-on: windows-latest 22 | steps: 23 | - uses: actions/checkout@v3 24 | with: 25 | submodules: recursive 26 | 27 | - uses: actions/setup-node@v3 28 | with: 29 | node-version: 20 30 | 31 | - uses: pnpm/action-setup@v2 32 | with: 33 | version: 8 34 | 35 | - name: build 36 | run: | 37 | pnpm install 38 | pnpm run build:win-${{ matrix.os-version }} 39 | env: 40 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 41 | 42 | - name: zip-unpacked-x64 43 | if: matrix.os-version == 'x64' 44 | run: | 45 | cd .\dist\win-unpacked 46 | 7z a -r Final2x-windows-${{ matrix.os-version }}-unpacked.7z * 47 | 48 | - name: zip-unpacked-arm64 49 | if: matrix.os-version == 'arm64' 50 | run: | 51 | cd .\dist\win-arm64-unpacked 52 | 7z a -r Final2x-windows-${{ matrix.os-version }}-unpacked.7z * 53 | 54 | - name: rename 55 | run: | 56 | cd .\dist 57 | ren *.exe Final2x-windows-${{ matrix.os-version }}-setup.exe 58 | shell: cmd 59 | 60 | - name: upload-setup 61 | uses: actions/upload-artifact@v3 62 | with: 63 | path: dist/*.exe 64 | 65 | - name: upload-unpacked-x64 66 | if: matrix.os-version == 'x64' 67 | uses: actions/upload-artifact@v3 68 | with: 69 | path: dist/win-unpacked/*.7z 70 | 71 | - name: upload-unpacked-arm64 72 | if: matrix.os-version == 'arm64' 73 | uses: actions/upload-artifact@v3 74 | with: 75 | path: dist/win-arm64-unpacked/*.7z 76 | 77 | macos: 78 | strategy: 79 | matrix: 80 | os-version: ['x64', 'arm64'] 81 | 82 | runs-on: macos-14 83 | steps: 84 | - uses: actions/checkout@v3 85 | with: 86 | submodules: recursive 87 | 88 | - uses: actions/setup-node@v3 89 | with: 90 | node-version: 20 91 | 92 | - uses: pnpm/action-setup@v2 93 | with: 94 | version: 8 95 | 96 | - name: build 97 | run: | 98 | pnpm install 99 | pnpm run build:mac-${{ matrix.os-version }} 100 | env: 101 | ARCH: ${{ matrix.os-version }} 102 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 103 | 104 | - name: rename 105 | run: | 106 | cd ./dist 107 | mv *.dmg Final2x-macos-${{ matrix.os-version }}-dmg.dmg 108 | 109 | - name: zip-unpacked-x64 110 | if: matrix.os-version == 'x64' 111 | run: | 112 | cd ./dist/mac 113 | 7z a -r Final2x-macos-${{ matrix.os-version }}-unpacked.7z * 114 | 115 | - name: zip-unpacked-arm64 116 | if: matrix.os-version == 'arm64' 117 | run: | 118 | cd ./dist/mac-arm64 119 | 7z a -r Final2x-macos-${{ matrix.os-version }}-unpacked.7z * 120 | 121 | - name: upload-dmg 122 | uses: actions/upload-artifact@v3 123 | with: 124 | path: dist/*.dmg 125 | 126 | - name: upload-unpacked-x64 127 | if: matrix.os-version == 'x64' 128 | uses: actions/upload-artifact@v3 129 | with: 130 | path: dist/mac/*.7z 131 | 132 | - name: upload-unpacked-arm64 133 | if: matrix.os-version == 'arm64' 134 | uses: actions/upload-artifact@v3 135 | with: 136 | path: dist/mac-arm64/*.7z 137 | 138 | linux-pip: 139 | strategy: 140 | matrix: 141 | os-version: ['x64'] 142 | 143 | runs-on: ubuntu-20.04 144 | steps: 145 | - uses: actions/checkout@v3 146 | with: 147 | submodules: recursive 148 | 149 | - uses: actions/setup-node@v3 150 | with: 151 | node-version: 20 152 | 153 | - uses: pnpm/action-setup@v2 154 | with: 155 | version: 8 156 | 157 | - name: build 158 | run: | 159 | pnpm install 160 | pnpm run build:linux-${{ matrix.os-version }} 161 | env: 162 | SKIP_DOWNLOAD_CORE: true 163 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 164 | 165 | - name: zip-unpacked 166 | run: | 167 | cd ./dist/linux-unpacked 168 | 7z a -r Final2x-linux-pip-${{ matrix.os-version }}-unpacked.7z * 169 | 170 | - name: rename 171 | run: | 172 | cd ./dist 173 | mv *.snap Final2x-linux-pip-${{ matrix.os-version }}-snap.snap 174 | mv *.AppImage Final2x-linux-pip-${{ matrix.os-version }}-AppImage.AppImage 175 | mv *.deb Final2x-linux-pip-${{ matrix.os-version }}-deb.deb 176 | 177 | - name: upload-snap 178 | uses: actions/upload-artifact@v3 179 | with: 180 | path: dist/*.snap 181 | 182 | - name: upload-AppImage 183 | uses: actions/upload-artifact@v3 184 | with: 185 | path: dist/*.AppImage 186 | 187 | - name: upload-deb 188 | uses: actions/upload-artifact@v3 189 | with: 190 | path: dist/*.deb 191 | 192 | - name: upload-unpacked 193 | uses: actions/upload-artifact@v3 194 | with: 195 | path: dist/linux-unpacked/*.7z 196 | 197 | Release: 198 | needs: [setup, windows, macos, linux-pip] 199 | runs-on: ubuntu-latest 200 | steps: 201 | - uses: actions/download-artifact@v3 202 | with: 203 | path: asset 204 | 205 | - name: dist 206 | run: | 207 | mkdir dist 208 | cp asset/artifact/* dist 209 | cd dist && ls 210 | 211 | - name: Create Release and Upload Release Asset 212 | uses: softprops/action-gh-release@v1 213 | with: 214 | name: Release ${{ needs.setup.outputs.DATE }} 215 | tag_name: ${{ needs.setup.outputs.DATE }} 216 | body: Auto Release. 217 | draft: false 218 | prerelease: false 219 | files: dist/* 220 | -------------------------------------------------------------------------------- /.github/workflows/issue-helper.yml: -------------------------------------------------------------------------------- 1 | name: issue-helper 2 | 3 | on: 4 | issues: 5 | types: [opened, reopened, edited] 6 | 7 | jobs: 8 | check-inactive: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: close-issues 12 | uses: actions-cool/issues-helper@v2 13 | with: 14 | actions: 'close-issues' 15 | token: ${{ secrets.GH_TOKEN }} 16 | inactive-day: 100 17 | body: | 18 | Hello! your issue has been closed because it has been inactive for a long time. 19 | 20 | 你好,你的 issue 因为长时间不活跃而被自动关闭。 21 | 22 | こんにちは、お問い合わせは長期間活動がないため、閉じられました。 23 | 24 | check-title: 25 | runs-on: ubuntu-latest 26 | 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) 27 | steps: 28 | - name: close issue 29 | uses: actions-cool/issues-helper@v3 30 | with: 31 | actions: 'create-comment, add-labels, close-issue' 32 | token: ${{ secrets.GH_TOKEN }} 33 | issue-number: ${{ github.event.issue.number }} 34 | labels: 'Invalid' 35 | body: | 36 | Hello @${{ github.event.issue.user.login }}, your issue has been closed because the title does not conform to our specification. 37 | 38 | 你好 @${{ github.event.issue.user.login }},为了能够进行高效沟通,我们对 issue 有一定的格式要求,你的 issue 因为标题不符合规范而被自动关闭。 39 | 40 | こんにちは、@${{ github.event.issue.user.login }}さん、タイトルが仕様に準拠していないため、ご提案いただいた問題はクローズされました。 41 | -------------------------------------------------------------------------------- /.github/workflows/issue-translator.yml: -------------------------------------------------------------------------------- 1 | name: 'issue-translator' 2 | on: 3 | issue_comment: 4 | types: [created] 5 | issues: 6 | types: [opened] 7 | 8 | jobs: 9 | translate: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: usthe/issues-translate-action@v2.7 13 | with: 14 | CUSTOM_BOT_NOTE: Bot detected the issue body's language is not English, translate it automatically. 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | out 4 | *.log* 5 | *.DS_Store 6 | /resources/Final2x-core/ 7 | /outputs/ 8 | /.idea 9 | /coverage/ 10 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | out 2 | dist 3 | pnpm-lock.yaml 4 | LICENSE.md 5 | tsconfig.json 6 | tsconfig.*.json 7 | -------------------------------------------------------------------------------- /.prettierrc.yaml: -------------------------------------------------------------------------------- 1 | singleQuote: true 2 | semi: false 3 | printWidth: 100 4 | trailingComma: none 5 | endOfLine: crlf 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2023, Tohrusky 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Socialify Image 6 | 7 | 8 | 9 | ![MacOS x64](https://img.shields.io/badge/Support-MacOS%20x64-blue?logo=Apple&style=flat-square) 10 | ![MacOS arm64](https://img.shields.io/badge/Support-MacOS%20arm64-blue?logo=Apple&style=flat-square) 11 | ![Windows x64](https://img.shields.io/badge/Support-Windows%20x64-blue?logo=Windows&style=flat-square) 12 | ![Windows arm64](https://img.shields.io/badge/Support-Windows%20arm64-blue?logo=Windows&style=flat-square) 13 | ![Linux x64](https://img.shields.io/badge/Support-Linux%20x64-blue?logo=Linux&style=flat-square) 14 | [![CI-test](https://github.com/Tohrusky/Final2x/actions/workflows/CI-test.yml/badge.svg)](https://github.com/Tohrusky/Final2x/actions/workflows/CI-test.yml) 15 | [![CI-build](https://github.com/Tohrusky/Final2x/actions/workflows/CI-build.yml/badge.svg)](https://github.com/Tohrusky/Final2x/actions/workflows/CI-build.yml) 16 | [![Release](https://github.com/Tohrusky/Final2x/actions/workflows/Release.yml/badge.svg)](https://github.com/Tohrusky/Final2x/actions/workflows/Release.yml) 17 | ![Download](https://img.shields.io/github/downloads/Tohrusky/Final2x/total) 18 | ![GitHub](https://img.shields.io/github/license/Tohrusky/Final2x) 19 | 20 | This is a powerful tool that allows for image super-resolution to arbitrary sizes using [multiple models](./src/renderer/src/utils/ModelOptions.ts), designed to enhance the resolution and quality of images, making them clearer and more detailed. 21 | 22 | - News🎉: We are thrilled to announce the release of Final2x v2.0.0, which marks a major milestone as we transition to utilizing [ccrestoration](https://github.com/TensoRaws/ccrestoration) (PyTorch) for our algorithm implementation. 23 | - News🎉: Want to enhance your video? Try [FinalRip](https://github.com/TensoRaws/FinalRip)! 24 | 25 | ### Comparison 26 | 27 |
28 | 29 |
30 |
use Final2x to perform 4x super-resolution on a 256x256 Hutao RGBA image
31 | 32 | ## Screenshots 33 | 34 |
35 | 36 | 37 |
38 | 39 | ### Installation 40 | 41 | ##### [Download the latest release from here.](https://github.com/Tohrusky/Final2x/releases) 42 | 43 | #### Windows 44 | 45 | Just Run! Furthermore, you can use package mananger to install and upgrade. 46 | 47 | ##### winget 48 | 49 | ```bash 50 | winget install Final2x 51 | ``` 52 | 53 | #### MacOS 54 | 55 | ```bash 56 | sudo spctl --master-disable 57 | # Disable Gatekeeper, then allow applications downloaded from anywhere in System Preferences > Security & Privacy > General 58 | xattr -cr /Applications/Final2x.app 59 | ``` 60 | 61 | In first time, you need to run the command above in terminal to allow the app to run. 62 | 63 | #### Linux 64 | 65 | For Linux User, you need to install the dependencies first. 66 | 67 | Make sure you have Python >= 3.9 and PyTorch >= 1.13 installed 68 | 69 | ```bash 70 | pip install Final2x-core 71 | Final2x-core -h # check if the installation is successful 72 | apt install -y libomp5 xdg-utils 73 | ``` 74 | 75 | ### Reference 76 | 77 | The following references were referenced in the development of this project: 78 | 79 | - [Final2x-core](https://github.com/Tohrusky/Final2x-core) 80 | - [ccrestoration](https://github.com/TensoRaws/ccrestoration) 81 | - [PyTorch](https://github.com/pytorch/pytorch) 82 | - [ncnn](https://github.com/Tencent/ncnn) 83 | - [naive-ui](https://github.com/tusen-ai/naive-ui) 84 | - [electron-vite](https://github.com/alex8088/electron-vite) 85 | 86 | ### License 87 | 88 | This project is licensed under the BSD 3-Clause - see 89 | the [LICENSE file](https://github.com/Tohrusky/Final2x/blob/main/LICENSE) for details. 90 | 91 | ### Acknowledgements 92 | 93 | Feel free to reach out to the project maintainers with any questions or concerns~ 94 | 95 | 96 | 97 | 98 | 99 | Star History Chart 100 | 101 | 102 | -------------------------------------------------------------------------------- /README_i18n/README_zh.md: -------------------------------------------------------------------------------- 1 | # Final2x 2 | -------------------------------------------------------------------------------- /build/entitlements.mac.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.cs.allow-jit 6 | 7 | com.apple.security.cs.allow-unsigned-executable-memory 8 | 9 | com.apple.security.cs.allow-dyld-environment-variables 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /build/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tohrusky/Final2x/8a115e4ce298f44b9773bf8d810259993309ef23/build/icon.icns -------------------------------------------------------------------------------- /build/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tohrusky/Final2x/8a115e4ce298f44b9773bf8d810259993309ef23/build/icon.ico -------------------------------------------------------------------------------- /build/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tohrusky/Final2x/8a115e4ce298f44b9773bf8d810259993309ef23/build/icon.png -------------------------------------------------------------------------------- /build/macosDMGbg.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tohrusky/Final2x/8a115e4ce298f44b9773bf8d810259993309ef23/build/macosDMGbg.jpeg -------------------------------------------------------------------------------- /build/notarize.js: -------------------------------------------------------------------------------- 1 | const { notarize } = require('@electron/notarize') 2 | 3 | module.exports = async (context) => { 4 | if (process.platform !== 'darwin') return 5 | 6 | console.log('aftersign hook triggered, start to notarize app.') 7 | 8 | if (!process.env.CI) { 9 | console.log(`skipping notarizing, not in CI.`) 10 | return 11 | } 12 | 13 | if (!('APPLE_ID' in process.env && 'APPLE_ID_PASS' in process.env)) { 14 | console.warn('skipping notarizing, APPLE_ID and APPLE_ID_PASS env variables must be set.') 15 | return 16 | } 17 | 18 | const appId = 'com.electron.app' 19 | 20 | const { appOutDir } = context 21 | 22 | const appName = context.packager.appInfo.productFilename 23 | 24 | try { 25 | await notarize({ 26 | appBundleId: appId, 27 | appPath: `${appOutDir}/${appName}.app`, 28 | appleId: process.env.APPLE_ID, 29 | appleIdPassword: process.env.APPLEIDPASS 30 | }) 31 | } catch (error) { 32 | console.error(error) 33 | } 34 | 35 | console.log(`done notarizing ${appId}.`) 36 | } 37 | -------------------------------------------------------------------------------- /electron-builder.yml: -------------------------------------------------------------------------------- 1 | appId: com.electron.app 2 | productName: Final2x 3 | directories: 4 | buildResources: build 5 | files: 6 | - '!**/.vscode/*' 7 | - '!src/*' 8 | - '!electron.vite.config.{js,ts,mjs,cjs}' 9 | - '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}' 10 | - '!{.env,.env.*,.npmrc,pnpm-lock.yaml}' 11 | - '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}' 12 | 13 | asar: false 14 | 15 | afterSign: build/notarize.js 16 | 17 | win: 18 | executableName: Final2x 19 | 20 | nsis: 21 | artifactName: ${name}-${version}-setup.${ext} 22 | shortcutName: ${productName} 23 | uninstallDisplayName: ${productName} 24 | createDesktopShortcut: always 25 | 26 | mac: 27 | entitlementsInherit: build/entitlements.mac.plist 28 | extendInfo: 29 | - NSCameraUsageDescription: Application requests access to the device's camera. 30 | - NSMicrophoneUsageDescription: Application requests access to the device's microphone. 31 | - NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder. 32 | - NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder. 33 | 34 | dmg: 35 | artifactName: ${name}-${version}.${ext} 36 | background: build/macosDMGbg.jpeg 37 | window: 38 | x: 100 39 | y: 100 40 | width: 480 41 | height: 500 42 | 43 | linux: 44 | target: 45 | - AppImage 46 | - snap 47 | - deb 48 | maintainer: electronjs.org 49 | category: Utility 50 | appImage: 51 | artifactName: ${name}-${version}.${ext} 52 | npmRebuild: false 53 | -------------------------------------------------------------------------------- /electron.vite.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path' 2 | import { defineConfig, externalizeDepsPlugin } from 'electron-vite' 3 | import vue from '@vitejs/plugin-vue' 4 | 5 | export default defineConfig({ 6 | main: { 7 | plugins: [externalizeDepsPlugin()] 8 | }, 9 | preload: { 10 | plugins: [externalizeDepsPlugin()] 11 | }, 12 | renderer: { 13 | resolve: { 14 | alias: { 15 | '@renderer': resolve('src/renderer/src') 16 | } 17 | }, 18 | plugins: [vue()] 19 | } 20 | }) 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Final2x", 3 | "productName": "Final2x", 4 | "version": "2.1.0", 5 | "description": "A cross-platform image super-resolution tool.", 6 | "main": "./out/main/index.js", 7 | "author": "Tohrusky", 8 | "homepage": "https://github.com/Tohrusky/Final2x", 9 | "scripts": { 10 | "dev": "electron-vite dev", 11 | "test": "vitest", 12 | "test-cov": "vitest run --coverage", 13 | "lint": "prettier --write . && eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts,.vue --fix", 14 | "typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false", 15 | "typecheck:web": "vue-tsc --noEmit -p tsconfig.web.json --composite false", 16 | "typecheck": "pnpm run typecheck:node && npm run typecheck:web", 17 | "start": "electron-vite preview", 18 | "build": "electron-vite build", 19 | "postinstall": "electron-builder install-app-deps && node ./resources/download-core.js", 20 | "build:mac-arm64": "pnpm run build && electron-builder --mac --arm64", 21 | "build:mac-x64": "pnpm run build && electron-builder --mac --x64", 22 | "build:win-arm64": "pnpm run build && electron-builder --win --arm64", 23 | "build:win-x64": "pnpm run build && electron-builder --win --x64", 24 | "build:linux-x64": "pnpm run build && electron-builder --linux --x64", 25 | "build:linux-arm64": "pnpm run build && electron-builder --linux --arm64" 26 | }, 27 | "engines": { 28 | "node": ">=18.0.0" 29 | }, 30 | "dependencies": { 31 | "@intlify/unplugin-vue-i18n": "^4.0.0", 32 | "@vicons/antd": "^0.12.0", 33 | "@vicons/ionicons5": "^0.12.0", 34 | "core-js": "^3.39.0", 35 | "naive-ui": "^2.40.1", 36 | "pinia": "^2.2.6", 37 | "pinia-plugin-persistedstate": "^3.2.3", 38 | "sass": "^1.80.6", 39 | "systeminformation": "^5.23.5", 40 | "vfonts": "^0.0.3", 41 | "vue": "^3.5.12", 42 | "vue-i18n": "^9.14.1", 43 | "vue-router": "^4.4.5" 44 | }, 45 | "devDependencies": { 46 | "@electron-toolkit/preload": "^3.0.1", 47 | "@electron-toolkit/tsconfig": "^1.0.1", 48 | "@electron-toolkit/utils": "^3.0.0", 49 | "@electron/notarize": "^2.5.0", 50 | "@rushstack/eslint-patch": "^1.10.4", 51 | "@types/node": "20.14.9", 52 | "@vitejs/plugin-vue": "^5.1.4", 53 | "@vitest/coverage-v8": "^1.6.0", 54 | "@vue/eslint-config-prettier": "^9.0.0", 55 | "@vue/eslint-config-typescript": "^13.0.0", 56 | "@vue/test-utils": "^2.4.6", 57 | "electron": "^27.3.11", 58 | "electron-builder": "^23.6.0", 59 | "electron-vite": "^2.3.0", 60 | "eslint": "^8.57.1", 61 | "eslint-plugin-vue": "^9.30.0", 62 | "extract-zip": "^2.0.1", 63 | "jsdom": "^24.1.3", 64 | "node-fetch": "^3.3.2", 65 | "prettier": "^3.3.3", 66 | "typescript": "^5.6.3", 67 | "vite": "^5.4.10", 68 | "vitest": "^1.6.0", 69 | "vue-tsc": "^2.1.10" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /resources/download-core.js: -------------------------------------------------------------------------------- 1 | // download Final2x-core from https://github.com/Final2x/Final2x-core/releases 2 | // and put it in resources folder 3 | 4 | const fetch = (...args) => import('node-fetch').then(({ default: fetch }) => fetch(...args)) 5 | const fs = require('fs') 6 | const path = require('path') 7 | 8 | const child_process = require('child_process') 9 | 10 | const coreDict = { 11 | 'macos-arm64': 12 | 'https://github.com/Tohrusky/Final2x-core/releases/download/2024-12-14/Final2x-core-macos-arm64.7z', 13 | 'macos-x64': 14 | 'https://github.com/Tohrusky/Final2x-core/releases/download/2024-12-14/Final2x-core-macos-x64.7z', 15 | 'windows-x64': 16 | 'https://github.com/Tohrusky/Final2x-core/releases/download/2024-12-14/Final2x-core-windows-latest.7z' 17 | } 18 | 19 | console.log('-'.repeat(50)) 20 | 21 | // 判断当前平台 22 | const PLATFORM = process.env.PLATFORM || process.platform 23 | // 判断当前平台架构 24 | const ARCH = process.env.ARCH || process.arch 25 | console.log(`Platform: ${PLATFORM}`, `| Arch: ${ARCH}`) 26 | if (process.env.SKIP_DOWNLOAD_CORE) { 27 | console.log('Skip download Final2x-core by env SKIP_DOWNLOAD_CORE') 28 | process.exit(0) 29 | } 30 | 31 | async function downloadAndUnzip(url, targetPath) { 32 | const zipFileName = path.basename(url) 33 | const zipFilePath = path.join(targetPath, zipFileName) 34 | 35 | const res = await fetch(url) 36 | const dest = fs.createWriteStream(zipFilePath) 37 | 38 | dest.on('finish', () => { 39 | console.log(`Download ${zipFileName} success!`) 40 | // 解压缩文件, 命令行调用 7z 41 | const Final2xCorePath = path.join(targetPath, 'Final2x-core') 42 | const unzipCmd = `7z x ${zipFilePath} -o${Final2xCorePath}` 43 | console.log(`Unzip command: ${unzipCmd}`) 44 | // 使用异步方式执行解压命令 45 | child_process.exec(unzipCmd, (error) => { 46 | if (error) { 47 | console.error(`Unzip error: ${error}`) 48 | return 49 | } 50 | console.log(`Unzip ${zipFileName} success!`) 51 | // 删除压缩文件 52 | fs.unlinkSync(zipFilePath) 53 | console.log(`Delete ${zipFileName} success!`) 54 | }) 55 | }) 56 | 57 | res.body.pipe(dest) 58 | } 59 | 60 | async function downloadAndUnzipCore(platform) { 61 | const url = coreDict[platform] 62 | if (!url) { 63 | console.error('Invalid platform') 64 | return 65 | } 66 | 67 | const targetPath = path.join(__dirname) 68 | console.log(`Target path: ${targetPath}`) 69 | 70 | if (fs.existsSync(path.join(targetPath, 'Final2x-core'))) { 71 | console.log('Final2x-core already exists, skip download!') 72 | return 73 | } 74 | 75 | if (!fs.existsSync(targetPath)) { 76 | fs.mkdirSync(targetPath, { recursive: true }) 77 | } 78 | 79 | await downloadAndUnzip(url, targetPath) 80 | } 81 | 82 | // 选择要下载的平台 83 | let platformToDownload = '' 84 | if (PLATFORM === 'darwin') { 85 | platformToDownload = ARCH === 'arm64' ? 'macos-arm64' : 'macos-x64' 86 | } else if (PLATFORM === 'linux') { 87 | console.error('Skip download Final2x-core for linux! Please use pip to install Final2x-core') 88 | process.exit(0) 89 | } else if (PLATFORM === 'win32') { 90 | platformToDownload = 'windows-x64' 91 | } else { 92 | console.error('Unsupported platform!') 93 | process.exit(1) 94 | } 95 | 96 | console.log(`Downloading Final2x-core for ${platformToDownload}...`) 97 | // 执行下载和解压 98 | downloadAndUnzipCore(platformToDownload) 99 | .then() 100 | .catch((err) => { 101 | console.error(err) 102 | }) 103 | -------------------------------------------------------------------------------- /resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tohrusky/Final2x/8a115e4ce298f44b9773bf8d810259993309ef23/resources/icon.png -------------------------------------------------------------------------------- /resources/taryTemplate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tohrusky/Final2x/8a115e4ce298f44b9773bf8d810259993309ef23/resources/taryTemplate.png -------------------------------------------------------------------------------- /resources/tray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tohrusky/Final2x/8a115e4ce298f44b9773bf8d810259993309ef23/resources/tray.png -------------------------------------------------------------------------------- /resources/trayTemplate@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tohrusky/Final2x/8a115e4ce298f44b9773bf8d810259993309ef23/resources/trayTemplate@2x.png -------------------------------------------------------------------------------- /src/main/RunCommand.ts: -------------------------------------------------------------------------------- 1 | import { spawn, spawnSync } from 'child_process' 2 | import path from 'path' 3 | import { app } from 'electron' 4 | 5 | let child 6 | 7 | export async function RunCommand( 8 | event, 9 | config_json: string, 10 | openOutputFolder: boolean 11 | ): Promise { 12 | let resourceUrl: string 13 | 14 | // ---- 还是直接传base64吧 15 | // config_json = JSON.stringify(config_json) // 转义转义 16 | // ---- 17 | config_json = Buffer.from(config_json, 'utf8').toString('base64') 18 | 19 | if (!CheckPipPackage()) { 20 | if (process.env.NODE_ENV === 'development') { 21 | resourceUrl = path.join(app.getAppPath(), '/resources/Final2x-core/Final2x-core') 22 | } else { 23 | resourceUrl = path.join(app.getAppPath(), '/resources/Final2x-core/Final2x-core') 24 | } 25 | } else { 26 | resourceUrl = 'Final2x-core' 27 | } 28 | 29 | let command = `"${resourceUrl}" -b ${config_json}` 30 | 31 | if (!openOutputFolder) { 32 | command += ' -n' 33 | } 34 | 35 | console.log(command) 36 | 37 | child = spawn(command, { shell: true }) 38 | 39 | child.stdout.on('data', (data) => { 40 | event.sender.send('command-stdout', data.toString()) 41 | }) 42 | 43 | child.stderr.on('data', (data) => { 44 | event.sender.send('command-stderr', data.toString()) 45 | }) 46 | 47 | child.on('close', (code) => { 48 | event.sender.send('command-close-code', code) 49 | }) 50 | } 51 | 52 | export async function KillCommand(): Promise { 53 | if (child) { 54 | child.kill() 55 | } 56 | } 57 | 58 | function CheckPipPackage(): boolean { 59 | const command = `Final2x-core -h` 60 | 61 | const result = spawnSync(command, { shell: true, encoding: 'utf-8' }) 62 | 63 | return result.status === 0 64 | } 65 | -------------------------------------------------------------------------------- /src/main/index.ts: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow, ipcMain, Menu, nativeImage, shell, Tray } from 'electron' 2 | import { join } from 'path' 3 | import { electronApp, is, optimizer } from '@electron-toolkit/utils' 4 | import { KillCommand, RunCommand } from './RunCommand' 5 | import { openDirectory } from './openDirectory' 6 | 7 | const icon = join(__dirname, '../../resources/icon.png') 8 | const trayIcon = join(__dirname, '../../resources/tray.png') 9 | 10 | function createWindow(): void { 11 | // Create the browser window. 12 | const mainWindow = new BrowserWindow({ 13 | width: 670, 14 | height: 470, 15 | maxWidth: 870, 16 | minWidth: 670, 17 | maxHeight: 670, 18 | minHeight: 470, 19 | frame: false, 20 | show: false, 21 | autoHideMenuBar: true, 22 | icon: nativeImage.createFromPath(icon), 23 | webPreferences: { 24 | preload: join(__dirname, '../preload/index.js'), 25 | sandbox: false 26 | } 27 | }) 28 | 29 | if (process.platform === 'darwin') { 30 | app.dock.setIcon(nativeImage.createFromPath(icon)) 31 | } 32 | 33 | ipcMain.on('execute-command', RunCommand) 34 | 35 | ipcMain.on('kill-command', KillCommand) 36 | 37 | ipcMain.on('open-directory-dialog', openDirectory) 38 | 39 | ipcMain.on('minimize', () => { 40 | mainWindow.minimize() 41 | }) 42 | 43 | ipcMain.on('maximize', () => { 44 | if (mainWindow.isMaximized()) { 45 | mainWindow.restore() 46 | } else { 47 | mainWindow.maximize() 48 | } 49 | }) 50 | 51 | ipcMain.on('close', () => { 52 | if (process.platform !== 'darwin') { 53 | app.quit() 54 | } else { 55 | app.hide() 56 | } 57 | }) 58 | 59 | mainWindow.on('ready-to-show', () => { 60 | mainWindow.show() 61 | }) 62 | 63 | mainWindow.webContents.setWindowOpenHandler((details) => { 64 | shell.openExternal(details.url) 65 | return { action: 'deny' } 66 | }) 67 | 68 | // HMR for renderer base on electron-vite cli. 69 | // Load the remote URL for development or the local html file for production. 70 | if (is.dev && process.env['ELECTRON_RENDERER_URL']) { 71 | mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL']) 72 | mainWindow.webContents.openDevTools() 73 | } else { 74 | mainWindow.loadFile(join(__dirname, '../renderer/index.html')) 75 | } 76 | } 77 | 78 | let tray 79 | function setTray(): void { 80 | const Image = nativeImage.createFromPath(trayIcon) 81 | Image.setTemplateImage(true) 82 | tray = new Tray(Image) 83 | 84 | const contextMenu = Menu.buildFromTemplate([ 85 | { 86 | label: 'Open', 87 | click: (): void => { 88 | // On macOS it's common to re-create a window in the app when the 89 | // dock icon is clicked and there are no other windows open. 90 | if (BrowserWindow.getAllWindows().length === 0) { 91 | createWindow() 92 | } else { 93 | BrowserWindow.getAllWindows()[0].show() 94 | } 95 | } 96 | }, 97 | { 98 | label: 'Exit', 99 | click: (): void => { 100 | app.quit() 101 | } 102 | } 103 | ]) 104 | 105 | tray.setToolTip('Final2x') 106 | tray.setContextMenu(contextMenu) 107 | } 108 | 109 | // This method will be called when Electron has finished 110 | // initialization and is ready to create browser windows. 111 | // Some APIs can only be used after this event occurs. 112 | app.whenReady().then(() => { 113 | // Set app user model id for windows 114 | electronApp.setAppUserModelId('com.electron') 115 | 116 | // Default open or close DevTools by F12 in development 117 | // and ignore CommandOrControl + R in production. 118 | // see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils 119 | app.on('browser-window-created', (_, window) => { 120 | optimizer.watchWindowShortcuts(window) 121 | }) 122 | 123 | setTray() 124 | createWindow() 125 | 126 | app.on('activate', function () { 127 | // On macOS it's common to re-create a window in the app when the 128 | // dock icon is clicked and there are no other windows open. 129 | if (BrowserWindow.getAllWindows().length === 0) createWindow() 130 | }) 131 | }) 132 | 133 | // Quit when all windows are closed, except on macOS. There, it's common 134 | // for applications and their menu bar to stay active until the user quits 135 | // explicitly with Cmd + Q. 136 | app.on('window-all-closed', () => { 137 | if (process.platform !== 'darwin') { 138 | app.quit() 139 | } 140 | }) 141 | 142 | // In this file you can include the rest of your app"s specific main process 143 | // code. You can also put them in separate files and require them here. 144 | -------------------------------------------------------------------------------- /src/main/openDirectory.ts: -------------------------------------------------------------------------------- 1 | import { dialog } from 'electron' 2 | 3 | /** 4 | * @description Open a directory or file/multiple files 5 | * @param event The event that triggered the function 6 | * @param p The properties of the dialog 7 | */ 8 | export function openDirectory(event, p: Array): void { 9 | dialog 10 | .showOpenDialog({ 11 | properties: p 12 | }) 13 | .then((result) => { 14 | console.log(result) 15 | event.sender.send('selectedItem', result.filePaths) 16 | }) 17 | .catch((err) => { 18 | console.log(err) 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /src/preload/index.d.ts: -------------------------------------------------------------------------------- 1 | import { ElectronAPI } from '@electron-toolkit/preload' 2 | 3 | declare global { 4 | interface Window { 5 | electron: ElectronAPI 6 | api: unknown 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/preload/index.ts: -------------------------------------------------------------------------------- 1 | import { contextBridge } from 'electron' 2 | import { electronAPI } from '@electron-toolkit/preload' 3 | 4 | // Custom APIs for renderer 5 | const api = {} 6 | 7 | // Use `contextBridge` APIs to expose Electron APIs to 8 | // renderer only if context isolation is enabled, otherwise 9 | // just add to the DOM global. 10 | if (process.contextIsolated) { 11 | try { 12 | contextBridge.exposeInMainWorld('electron', electronAPI) 13 | contextBridge.exposeInMainWorld('api', api) 14 | } catch (error) { 15 | console.error(error) 16 | } 17 | } else { 18 | // @ts-ignore (define in dts) 19 | window.electron = electronAPI 20 | // @ts-ignore (define in dts) 21 | window.api = api 22 | } 23 | -------------------------------------------------------------------------------- /src/renderer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Final2x 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/renderer/src/App.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 65 | 66 | 117 | -------------------------------------------------------------------------------- /src/renderer/src/assets/final2xlogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tohrusky/Final2x/8a115e4ce298f44b9773bf8d810259993309ef23/src/renderer/src/assets/final2xlogo.png -------------------------------------------------------------------------------- /src/renderer/src/assets/final2xlogoDarkMode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tohrusky/Final2x/8a115e4ce298f44b9773bf8d810259993309ef23/src/renderer/src/assets/final2xlogoDarkMode.png -------------------------------------------------------------------------------- /src/renderer/src/components/MyDarkMode.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/renderer/src/components/MyExternalLink.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 24 | 25 | 44 | -------------------------------------------------------------------------------- /src/renderer/src/components/MyProgress.vue: -------------------------------------------------------------------------------- 1 | 169 | 170 | 206 | 207 | 238 | -------------------------------------------------------------------------------- /src/renderer/src/components/MySetting.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 70 | 71 | 77 | -------------------------------------------------------------------------------- /src/renderer/src/components/NaiveDarkMode.vue: -------------------------------------------------------------------------------- 1 | 237 | 238 | 241 | 242 | 243 | -------------------------------------------------------------------------------- /src/renderer/src/components/TrafficLightsButtons.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 89 | 90 | 267 | -------------------------------------------------------------------------------- /src/renderer/src/components/bottomNavigation.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 15 | 16 | 33 | -------------------------------------------------------------------------------- /src/renderer/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.vue' { 4 | import type { DefineComponent } from 'vue' 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types 6 | const component: DefineComponent<{}, {}, any> 7 | export default component 8 | } 9 | -------------------------------------------------------------------------------- /src/renderer/src/locales/en.ts: -------------------------------------------------------------------------------- 1 | export const en = { 2 | MyProgress: { 3 | text0: 'Processing started', 4 | text1: 'Processing in progress', 5 | text2: 'Please add an image', 6 | text3: 'Image list is empty', 7 | text4: 'Processing terminated', 8 | text5: 'Processing failed, skipping', 9 | text6: 'START', 10 | text7: 'TERMINATE', 11 | text8: 'LOG', 12 | text9: 'Processing failed', 13 | text10: 'Please click on the log to view the error message' 14 | }, 15 | Final2xHome: { 16 | text0: 'Removal successful', 17 | text1: 'Click or drag and drop images or folders here to upload' 18 | }, 19 | Final2xSettings: { 20 | text10: 'Device', 21 | text11: 'Model', 22 | text15: 'Custom Scale', 23 | text16: 'Default', 24 | text17: 'Output Folder', 25 | text18: 'Proxy' 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/renderer/src/locales/fr.ts: -------------------------------------------------------------------------------- 1 | export const fr = { 2 | MyProgress: { 3 | text0: 'Traitement commencé', 4 | text1: 'Traitement en cours', 5 | text2: 'Veuillez ajouter une image', 6 | text3: "La liste d'images est vide", 7 | text4: 'Traitement terminé', 8 | text5: 'Échec du traitement, passage à la suite', 9 | text6: 'DÉMARRER', 10 | text7: 'ARRÊTER', 11 | text8: 'JOURNAL', 12 | text9: 'Échec du traitement', 13 | text10: "Veuillez cliquer sur le journal pour voir le message d'erreur" 14 | }, 15 | Final2xHome: { 16 | text0: 'Suppression réussie', 17 | text1: 'Cliquez ou faites glisser les images ou dossiers ici pour les téléverser' 18 | }, 19 | Final2xSettings: { 20 | text10: 'Périph.', 21 | text11: 'Modèle', 22 | text15: 'Échelle (num.)', 23 | text16: 'Par déf.', 24 | text17: 'Dossier de sortie', 25 | text18: 'Proxy' 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/renderer/src/locales/ja.ts: -------------------------------------------------------------------------------- 1 | export const ja = { 2 | MyProgress: { 3 | text0: '処理を開始します', 4 | text1: '処理中です', 5 | text2: '画像を追加してください', 6 | text3: '画像リストは空です', 7 | text4: '処理が中断されました', 8 | text5: '処理に失敗しました。スキップします', 9 | text6: '開始', 10 | text7: '中止', 11 | text8: 'ログ', 12 | text9: '処理失敗', 13 | text10: 'エラーメッセージを確認するには、ログをクリックしてください' 14 | }, 15 | Final2xHome: { 16 | text0: '削除が成功しました', 17 | text1: '画像やフォルダをここにクリックまたはドラッグ&ドロップしてアップロードしてください' 18 | }, 19 | Final2xSettings: { 20 | text10: 'デバイス', 21 | text11: 'モデル', 22 | text15: 'Custom Scale', 23 | text16: 'Default', 24 | text17: '出力フォルダ', 25 | text18: 'プロキシ' 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/renderer/src/locales/zh.ts: -------------------------------------------------------------------------------- 1 | export const zh = { 2 | MyProgress: { 3 | text0: '开始处理', 4 | text1: '处理中', 5 | text2: '请添加图片', 6 | text3: '图片列表为空', 7 | text4: '已终止处理', 8 | text5: '处理失败,跳过', 9 | text6: '开始', 10 | text7: '终止', 11 | text8: '日志', 12 | text9: '处理失败', 13 | text10: '请点击日志查看错误信息' 14 | }, 15 | Final2xHome: { 16 | text0: '移除成功', 17 | text1: '点击或拖拽图片或文件夹到此处上传' 18 | }, 19 | Final2xSettings: { 20 | text10: '设备', 21 | text11: '模型', 22 | text15: '自定义倍率', 23 | text16: '默认', 24 | text17: '输出文件夹', 25 | text18: '下载代理' 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/renderer/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | import i18n from './plugins/i18n' 5 | import { createPinia } from 'pinia' 6 | import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' 7 | // 通用字体 8 | import 'vfonts/OpenSans.css' 9 | import { 10 | // create naive ui 11 | create, 12 | // component 13 | NButton, 14 | NDivider, 15 | NSpace, 16 | NIcon, 17 | NImage, 18 | NCard, 19 | NDrawer, 20 | NDrawerContent, 21 | NLog, 22 | NProgress, 23 | NText, 24 | NUpload, 25 | NUploadDragger, 26 | NInput, 27 | NInputNumber, 28 | NPopover, 29 | NSelect, 30 | NSwitch 31 | } from 'naive-ui' 32 | 33 | const naive = create({ 34 | components: [ 35 | NButton, 36 | NDivider, 37 | NSpace, 38 | NIcon, 39 | NImage, 40 | NCard, 41 | NDrawer, 42 | NDrawerContent, 43 | NLog, 44 | NProgress, 45 | NText, 46 | NUpload, 47 | NUploadDragger, 48 | NInput, 49 | NInputNumber, 50 | NPopover, 51 | NSelect, 52 | NSwitch 53 | ] 54 | }) 55 | 56 | const pinia = createPinia() 57 | pinia.use(piniaPluginPersistedstate) 58 | 59 | createApp(App).use(naive).use(i18n).use(pinia).use(router).mount('#app') 60 | -------------------------------------------------------------------------------- /src/renderer/src/plugins/i18n.ts: -------------------------------------------------------------------------------- 1 | import { createI18n } from 'vue-i18n' 2 | import { en } from '../locales/en' 3 | import { zh } from '../locales/zh' 4 | import { ja } from '../locales/ja' 5 | import { fr } from '../locales/fr' 6 | 7 | // ----------------------------------------------------------------------------- 8 | // to add a new language, add the language file to the locales folder and add the language id to the LANG_LIST array 9 | // and "import { xx } from '../locales/xx'" at the top of this file 10 | // and add the language to the messages object below 11 | export const LANG_LIST: string[] = ['en', 'zh', 'ja', 'fr'] 12 | // ----------------------------------------------------------------------------- 13 | 14 | const i18n = createI18n({ 15 | legacy: false, 16 | fallbackLocale: 'en', 17 | globalInjection: true, // 全局注册$t方法 18 | messages: { 19 | en, 20 | zh, 21 | ja, 22 | fr 23 | } 24 | }) 25 | 26 | export default i18n 27 | -------------------------------------------------------------------------------- /src/renderer/src/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tohrusky/Final2x/8a115e4ce298f44b9773bf8d810259993309ef23/src/renderer/src/public/favicon.ico -------------------------------------------------------------------------------- /src/renderer/src/public/img/icons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tohrusky/Final2x/8a115e4ce298f44b9773bf8d810259993309ef23/src/renderer/src/public/img/icons/android-chrome-192x192.png -------------------------------------------------------------------------------- /src/renderer/src/public/img/icons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tohrusky/Final2x/8a115e4ce298f44b9773bf8d810259993309ef23/src/renderer/src/public/img/icons/android-chrome-512x512.png -------------------------------------------------------------------------------- /src/renderer/src/public/img/icons/android-chrome-maskable-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tohrusky/Final2x/8a115e4ce298f44b9773bf8d810259993309ef23/src/renderer/src/public/img/icons/android-chrome-maskable-192x192.png -------------------------------------------------------------------------------- /src/renderer/src/public/img/icons/android-chrome-maskable-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tohrusky/Final2x/8a115e4ce298f44b9773bf8d810259993309ef23/src/renderer/src/public/img/icons/android-chrome-maskable-512x512.png -------------------------------------------------------------------------------- /src/renderer/src/public/img/icons/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tohrusky/Final2x/8a115e4ce298f44b9773bf8d810259993309ef23/src/renderer/src/public/img/icons/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /src/renderer/src/public/img/icons/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tohrusky/Final2x/8a115e4ce298f44b9773bf8d810259993309ef23/src/renderer/src/public/img/icons/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /src/renderer/src/public/img/icons/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tohrusky/Final2x/8a115e4ce298f44b9773bf8d810259993309ef23/src/renderer/src/public/img/icons/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /src/renderer/src/public/img/icons/apple-touch-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tohrusky/Final2x/8a115e4ce298f44b9773bf8d810259993309ef23/src/renderer/src/public/img/icons/apple-touch-icon-60x60.png -------------------------------------------------------------------------------- /src/renderer/src/public/img/icons/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tohrusky/Final2x/8a115e4ce298f44b9773bf8d810259993309ef23/src/renderer/src/public/img/icons/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /src/renderer/src/public/img/icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tohrusky/Final2x/8a115e4ce298f44b9773bf8d810259993309ef23/src/renderer/src/public/img/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /src/renderer/src/public/img/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tohrusky/Final2x/8a115e4ce298f44b9773bf8d810259993309ef23/src/renderer/src/public/img/icons/favicon-16x16.png -------------------------------------------------------------------------------- /src/renderer/src/public/img/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tohrusky/Final2x/8a115e4ce298f44b9773bf8d810259993309ef23/src/renderer/src/public/img/icons/favicon-32x32.png -------------------------------------------------------------------------------- /src/renderer/src/public/img/icons/msapplication-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tohrusky/Final2x/8a115e4ce298f44b9773bf8d810259993309ef23/src/renderer/src/public/img/icons/msapplication-icon-144x144.png -------------------------------------------------------------------------------- /src/renderer/src/public/img/icons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tohrusky/Final2x/8a115e4ce298f44b9773bf8d810259993309ef23/src/renderer/src/public/img/icons/mstile-150x150.png -------------------------------------------------------------------------------- /src/renderer/src/public/img/icons/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/renderer/src/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/renderer/src/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /src/renderer/src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHashHistory } from 'vue-router' 2 | import Final2xHome from '../views/Final2xHome.vue' 3 | import Final2xSettings from '../views/Final2xSettings.vue' 4 | 5 | export default createRouter({ 6 | history: createWebHashHistory(), 7 | routes: [ 8 | { 9 | path: '/', 10 | redirect: '/Final2xHome' 11 | }, 12 | { 13 | path: '/Final2xHome', 14 | name: 'Final2xHome', 15 | component: Final2xHome 16 | }, 17 | { 18 | path: '/Final2xSettings', 19 | name: 'Final2xSettings', 20 | component: Final2xSettings 21 | } 22 | ] 23 | }) 24 | -------------------------------------------------------------------------------- /src/renderer/src/store/SRSettingsStore.ts: -------------------------------------------------------------------------------- 1 | import { ref, Ref } from 'vue' 2 | import { defineStore } from 'pinia' 3 | 4 | export const useSRSettingsStore = defineStore( 5 | 'SRSettings', 6 | () => { 7 | const selectedSRModel = ref('RealESRGAN_RealESRGAN_x2plus_2x.pth') 8 | const ghProxy: Ref = ref(null) 9 | const targetScale: Ref = ref(null) 10 | 11 | return { 12 | selectedSRModel, 13 | ghProxy, 14 | targetScale 15 | } 16 | }, 17 | { 18 | persist: { 19 | storage: localStorage, 20 | paths: ['selectedSRModel', 'ghProxy', 'targetScale'] 21 | } 22 | } 23 | ) 24 | -------------------------------------------------------------------------------- /src/renderer/src/store/globalSettingsStore.ts: -------------------------------------------------------------------------------- 1 | import { ref, Ref } from 'vue' 2 | import { defineStore } from 'pinia' 3 | import { LogInst } from 'naive-ui' 4 | import type { NaiveDarkModeType } from '../components/NaiveDarkMode.vue' 5 | 6 | export const useGlobalSettingsStore = defineStore( 7 | 'GlobalSettings', 8 | () => { 9 | const darkMode: Ref = ref('system') 10 | const globalcolor = ref('#fffafa') 11 | const naiveTheme: Ref = ref(undefined) 12 | 13 | const changeRoute = ref(false) 14 | 15 | const langsNum = ref(114514) 16 | 17 | const selectedTorchDevice = ref('auto') 18 | const torchDeviceList: Ref = ref([ 19 | { value: 'auto', label: 'Auto' }, 20 | { value: 'cuda', label: 'CUDA' }, 21 | { value: 'mps', label: 'MPS' }, 22 | { value: 'cpu', label: 'CPU' } 23 | ]) 24 | 25 | const ProgressPercentage = ref(0) 26 | const CommandLOG = ref('') 27 | const logInstRef = ref(null) 28 | const StartCommandLock = ref(false) 29 | const SrSuccess = ref(false) 30 | 31 | const openOutputFolder = ref(true) 32 | 33 | return { 34 | darkMode, 35 | globalcolor, 36 | naiveTheme, 37 | changeRoute, 38 | langsNum, 39 | selectedTorchDevice, 40 | torchDeviceList, 41 | ProgressPercentage, 42 | CommandLOG, 43 | StartCommandLock, 44 | SrSuccess, 45 | logInstRef, 46 | openOutputFolder 47 | } 48 | }, 49 | { 50 | persist: { 51 | storage: localStorage, 52 | paths: [ 53 | 'langsNum', 54 | 'selectedTorchDevice', 55 | 'darkMode', 56 | 'naiveTheme', 57 | 'globalcolor', 58 | 'openOutputFolder' 59 | ] 60 | } 61 | } 62 | ) 63 | -------------------------------------------------------------------------------- /src/renderer/src/store/ioPathStore.ts: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | import { defineStore } from 'pinia' 3 | import { UploadFileInfo } from 'naive-ui' 4 | 5 | export const useIOPathStore = defineStore( 6 | 'IOPath', 7 | () => { 8 | const inputpathMap = ref>(new Map()) 9 | const outputpath = ref('') 10 | const outputpathLock = ref(false) 11 | 12 | const inputFileList = ref([]) 13 | 14 | return { 15 | inputpathMap, 16 | outputpath, 17 | outputpathLock, 18 | inputFileList 19 | } 20 | }, 21 | { 22 | persist: { 23 | storage: localStorage, 24 | paths: ['outputpath', 'outputpathLock'] 25 | } 26 | } 27 | ) 28 | -------------------------------------------------------------------------------- /src/renderer/src/utils/IOPath.ts: -------------------------------------------------------------------------------- 1 | import { storeToRefs } from 'pinia' 2 | import { useIOPathStore } from '../store/ioPathStore' 3 | import PathFormat from '../utils/pathFormat' 4 | 5 | class ioPATH { 6 | /** 7 | * @description Add a new inputpath to inputpathMap 8 | * @param id inputpath id 9 | * @param path inputpath 10 | */ 11 | static add(id: string, path: string): void { 12 | const { inputpathMap } = storeToRefs(useIOPathStore()) 13 | if (path != '') { 14 | inputpathMap.value.set(id, path) 15 | } 16 | } 17 | 18 | /** 19 | * @description Delete an inputpath from inputpathMap by id 20 | * @param id inputpath id 21 | */ 22 | static delete(id: string): void { 23 | const { inputpathMap } = storeToRefs(useIOPathStore()) 24 | inputpathMap.value.delete(id) 25 | } 26 | 27 | /** 28 | * @description 检查 id 是否存在,因为 naive-ui 生成的 id 长度较短,所以这里只检查 inputpathMap 即可 29 | * @param id inputpath id 30 | */ 31 | static checkID(id: string): boolean { 32 | const { inputpathMap } = storeToRefs(useIOPathStore()) 33 | return inputpathMap.value.get(id) != undefined 34 | } 35 | 36 | /** 37 | * @description Get an inputpath from inputpathMap by id 38 | * @param id inputpath id 39 | */ 40 | static getByID(id: string): string { 41 | const { inputpathMap } = storeToRefs(useIOPathStore()) 42 | return inputpathMap.value.get(id) || '' 43 | } 44 | 45 | /** 46 | * @description Get all inputpath from inputpathMap 47 | * @returns inputpathMap with string 48 | */ 49 | static getAllPath(): string { 50 | const { inputpathMap } = storeToRefs(useIOPathStore()) 51 | // return inputpath key and value with string 52 | let inputpath = '' 53 | inputpathMap.value.forEach((value, key) => { 54 | inputpath += key + ' : ' + value + '\n' 55 | }) 56 | return inputpath 57 | } 58 | 59 | /** 60 | * @description Get all inputpath from inputpathMap 61 | * @returns inputpathMap string list 62 | */ 63 | static getList(): string[] { 64 | const { inputpathMap } = storeToRefs(useIOPathStore()) 65 | // return inputpath value with String List 66 | return Array.from(inputpathMap.value.values()) 67 | } 68 | 69 | /** 70 | * @description check inputpathMap is empty 71 | */ 72 | static isEmpty(): boolean { 73 | const { inputpathMap } = storeToRefs(useIOPathStore()) 74 | return inputpathMap.value.size == 0 75 | } 76 | 77 | /** 78 | * @description Get all inputpath from inputpathMap 79 | * @returns inputpathMap with string 80 | */ 81 | static show(): string { 82 | const inputpathList = ioPATH.getList() 83 | console.log('inputpathList: ', inputpathList) 84 | let inputpathListString = '' 85 | for (const i in inputpathList) { 86 | inputpathListString += inputpathList[i] + '\n' 87 | } 88 | return inputpathListString 89 | } 90 | 91 | /** 92 | * @description Set outputpath by manual, and lock outputpath 93 | */ 94 | static setoutputpathManual(path: string): void { 95 | const { outputpath, outputpathLock } = storeToRefs(useIOPathStore()) 96 | if (path != '') { 97 | outputpath.value = path 98 | outputpathLock.value = true 99 | console.log('outputpath SET SUCCESS!') 100 | } 101 | } 102 | 103 | /** 104 | * @description Set outputpath if outputpathLock is false or outputpath is invalid 105 | */ 106 | static setoutputpath(path: string): void { 107 | const { outputpath, outputpathLock } = storeToRefs(useIOPathStore()) 108 | // if outputpathLock is false or outputpath is empty, set outputpath 109 | if (path != '' && (outputpathLock.value == false || !PathFormat.checkPath(outputpath.value))) { 110 | outputpath.value = path 111 | } else { 112 | console.log('outputpath Lock!') 113 | } 114 | } 115 | 116 | /** 117 | * @description get outputpath 118 | */ 119 | static getoutputpath(): string { 120 | const { outputpath } = storeToRefs(useIOPathStore()) 121 | return outputpath.value 122 | } 123 | 124 | /** 125 | * @description clear all inputpath 126 | */ 127 | static clearALL(): void { 128 | const { inputpathMap, inputFileList } = storeToRefs(useIOPathStore()) 129 | inputpathMap.value.clear() 130 | inputFileList.value = [] 131 | } 132 | } 133 | 134 | export default ioPATH 135 | -------------------------------------------------------------------------------- /src/renderer/src/utils/ModelOptions.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | /* tslint:disable */ 4 | /* This file is automatically generated by Final2x-core */ 5 | /* Do not modify this file manually */ 6 | // ----------------------------------------------------------------------------- 7 | 8 | /** 9 | * @description: all SR models provided by ccrestoration 10 | */ 11 | export const modelOptions: any[] = [ 12 | { label: 'RealESRGAN_RealESRGAN_x4plus_4x', value: 'RealESRGAN_RealESRGAN_x4plus_4x.pth' }, 13 | { label: 'RealESRGAN_RealESRGAN_x4plus_anime_6B_4x', value: 'RealESRGAN_RealESRGAN_x4plus_anime_6B_4x.pth' }, 14 | { label: 'RealESRGAN_RealESRGAN_x2plus_2x', value: 'RealESRGAN_RealESRGAN_x2plus_2x.pth' }, 15 | { label: 'RealESRGAN_realesr_animevideov3_4x', value: 'RealESRGAN_realesr_animevideov3_4x.pth' }, 16 | { label: 'RealESRGAN_AnimeJaNai_HD_V3_Compact_2x', value: 'RealESRGAN_AnimeJaNai_HD_V3_Compact_2x.pth' }, 17 | { label: 'RealESRGAN_AniScale_2_Compact_2x', value: 'RealESRGAN_AniScale_2_Compact_2x.pth' }, 18 | { label: 'RealESRGAN_Ani4Kv2_Compact_2x', value: 'RealESRGAN_Ani4Kv2_Compact_2x.pth' }, 19 | { label: 'RealESRGAN_APISR_RRDB_GAN_generator_2x', value: 'RealESRGAN_APISR_RRDB_GAN_generator_2x.pth' }, 20 | { label: 'RealESRGAN_APISR_RRDB_GAN_generator_4x', value: 'RealESRGAN_APISR_RRDB_GAN_generator_4x.pth' }, 21 | { label: 'DAT_S_2x', value: 'DAT_S_2x.pth' }, 22 | { label: 'DAT_S_3x', value: 'DAT_S_3x.pth' }, 23 | { label: 'DAT_S_4x', value: 'DAT_S_4x.pth' }, 24 | { label: 'DAT_2x', value: 'DAT_2x.pth' }, 25 | { label: 'DAT_3x', value: 'DAT_3x.pth' }, 26 | { label: 'DAT_4x', value: 'DAT_4x.pth' }, 27 | { label: 'DAT_2_2x', value: 'DAT_2_2x.pth' }, 28 | { label: 'DAT_2_3x', value: 'DAT_2_3x.pth' }, 29 | { label: 'DAT_2_4x', value: 'DAT_2_4x.pth' }, 30 | { label: 'DAT_light_2x', value: 'DAT_light_2x.pth' }, 31 | { label: 'DAT_light_3x', value: 'DAT_light_3x.pth' }, 32 | { label: 'DAT_light_4x', value: 'DAT_light_4x.pth' }, 33 | { label: 'DAT_APISR_GAN_generator_4x', value: 'DAT_APISR_GAN_generator_4x.pth' }, 34 | { label: 'HAT_S_2x', value: 'HAT_S_2x.pth' }, 35 | { label: 'HAT_S_3x', value: 'HAT_S_3x.pth' }, 36 | { label: 'HAT_S_4x', value: 'HAT_S_4x.pth' }, 37 | { label: 'HAT_2x', value: 'HAT_2x.pth' }, 38 | { label: 'HAT_3x', value: 'HAT_3x.pth' }, 39 | { label: 'HAT_4x', value: 'HAT_4x.pth' }, 40 | { label: 'HAT_Real_GAN_sharper_4x', value: 'HAT_Real_GAN_sharper_4x.pth' }, 41 | { label: 'HAT_Real_GAN_4x', value: 'HAT_Real_GAN_4x.pth' }, 42 | { label: 'HAT_ImageNet_pretrain_2x', value: 'HAT_ImageNet_pretrain_2x.pth' }, 43 | { label: 'HAT_ImageNet_pretrain_3x', value: 'HAT_ImageNet_pretrain_3x.pth' }, 44 | { label: 'HAT_ImageNet_pretrain_4x', value: 'HAT_ImageNet_pretrain_4x.pth' }, 45 | { label: 'HAT_L_ImageNet_pretrain_2x', value: 'HAT_L_ImageNet_pretrain_2x.pth' }, 46 | { label: 'HAT_L_ImageNet_pretrain_3x', value: 'HAT_L_ImageNet_pretrain_3x.pth' }, 47 | { label: 'HAT_L_ImageNet_pretrain_4x', value: 'HAT_L_ImageNet_pretrain_4x.pth' }, 48 | { label: 'RealCUGAN_Conservative_2x', value: 'RealCUGAN_Conservative_2x.pth' }, 49 | { label: 'RealCUGAN_Denoise1x_2x', value: 'RealCUGAN_Denoise1x_2x.pth' }, 50 | { label: 'RealCUGAN_Denoise2x_2x', value: 'RealCUGAN_Denoise2x_2x.pth' }, 51 | { label: 'RealCUGAN_Denoise3x_2x', value: 'RealCUGAN_Denoise3x_2x.pth' }, 52 | { label: 'RealCUGAN_No_Denoise_2x', value: 'RealCUGAN_No_Denoise_2x.pth' }, 53 | { label: 'RealCUGAN_Conservative_3x', value: 'RealCUGAN_Conservative_3x.pth' }, 54 | { label: 'RealCUGAN_Denoise3x_3x', value: 'RealCUGAN_Denoise3x_3x.pth' }, 55 | { label: 'RealCUGAN_No_Denoise_3x', value: 'RealCUGAN_No_Denoise_3x.pth' }, 56 | { label: 'RealCUGAN_Conservative_4x', value: 'RealCUGAN_Conservative_4x.pth' }, 57 | { label: 'RealCUGAN_Denoise3x_4x', value: 'RealCUGAN_Denoise3x_4x.pth' }, 58 | { label: 'RealCUGAN_No_Denoise_4x', value: 'RealCUGAN_No_Denoise_4x.pth' }, 59 | { label: 'RealCUGAN_Pro_Conservative_2x', value: 'RealCUGAN_Pro_Conservative_2x.pth' }, 60 | { label: 'RealCUGAN_Pro_Denoise3x_2x', value: 'RealCUGAN_Pro_Denoise3x_2x.pth' }, 61 | { label: 'RealCUGAN_Pro_No_Denoise_2x', value: 'RealCUGAN_Pro_No_Denoise_2x.pth' }, 62 | { label: 'RealCUGAN_Pro_Conservative_3x', value: 'RealCUGAN_Pro_Conservative_3x.pth' }, 63 | { label: 'RealCUGAN_Pro_Denoise3x_3x', value: 'RealCUGAN_Pro_Denoise3x_3x.pth' }, 64 | { label: 'RealCUGAN_Pro_No_Denoise_3x', value: 'RealCUGAN_Pro_No_Denoise_3x.pth' }, 65 | { label: 'EDSR_Mx2_f64b16_DIV2K_official_2x', value: 'EDSR_Mx2_f64b16_DIV2K_official_2x.pth' }, 66 | { label: 'EDSR_Mx3_f64b16_DIV2K_official_3x', value: 'EDSR_Mx3_f64b16_DIV2K_official_3x.pth' }, 67 | { label: 'EDSR_Mx4_f64b16_DIV2K_official_4x', value: 'EDSR_Mx4_f64b16_DIV2K_official_4x.pth' }, 68 | { label: 'SwinIR_classicalSR_DF2K_s64w8_SwinIR_M_2x', value: 'SwinIR_classicalSR_DF2K_s64w8_SwinIR_M_2x.pth' }, 69 | { label: 'SwinIR_lightweightSR_DIV2K_s64w8_SwinIR_S_2x', value: 'SwinIR_lightweightSR_DIV2K_s64w8_SwinIR_S_2x.pth' }, 70 | { label: 'SwinIR_realSR_BSRGAN_DFOWMFC_s64w8_SwinIR_L_GAN_4x', value: 'SwinIR_realSR_BSRGAN_DFOWMFC_s64w8_SwinIR_L_GAN_4x.pth' }, 71 | { label: 'SwinIR_realSR_BSRGAN_DFO_s64w8_SwinIR_M_GAN_2x', value: 'SwinIR_realSR_BSRGAN_DFO_s64w8_SwinIR_M_GAN_2x.pth' }, 72 | { label: 'SwinIR_realSR_BSRGAN_DFO_s64w8_SwinIR_M_GAN_4x', value: 'SwinIR_realSR_BSRGAN_DFO_s64w8_SwinIR_M_GAN_4x.pth' }, 73 | { label: 'SwinIR_Bubble_AnimeScale_SwinIR_Small_v1_2x', value: 'SwinIR_Bubble_AnimeScale_SwinIR_Small_v1_2x.pth' }, 74 | { label: 'SCUNet_color_50_1x', value: 'SCUNet_color_50_1x.pth' }, 75 | { label: 'SCUNet_color_real_psnr_1x', value: 'SCUNet_color_real_psnr_1x.pth' }, 76 | { label: 'SCUNet_color_real_gan_1x', value: 'SCUNet_color_real_gan_1x.pth' }, 77 | { label: 'SRCNN_2x', value: 'SRCNN_2x.pth' }, 78 | { label: 'SRCNN_3x', value: 'SRCNN_3x.pth' }, 79 | { label: 'SRCNN_4x', value: 'SRCNN_4x.pth' }, 80 | ] 81 | -------------------------------------------------------------------------------- /src/renderer/src/utils/getFinal2xconfig.ts: -------------------------------------------------------------------------------- 1 | import { storeToRefs } from 'pinia' 2 | import { useSRSettingsStore } from '../store/SRSettingsStore' 3 | import { useGlobalSettingsStore } from '../store/globalSettingsStore' 4 | import ioPath from '../utils/IOPath' 5 | import PathFormat from '../utils/pathFormat' 6 | 7 | /** 8 | * @description: 返回输出路径,如果输出路径不合法,则从第一个输入路径构造一个合法输出路径 9 | */ 10 | function getOutPutPATH(): string { 11 | if (!PathFormat.checkPath(ioPath.getoutputpath())) { 12 | const inputPATHList = ioPath.getList() 13 | const pathFormat = new PathFormat() 14 | pathFormat.setRootPath(inputPATHList[0]) 15 | ioPath.setoutputpath(pathFormat.getRootPath()) 16 | } 17 | return ioPath.getoutputpath() 18 | } 19 | 20 | /** 21 | * @description: 返回最终的json字符串配置文件 22 | */ 23 | export const getFinal2xconfig = (): string => { 24 | const { selectedSRModel, ghProxy, targetScale } = storeToRefs(useSRSettingsStore()) 25 | const { selectedTorchDevice } = storeToRefs(useGlobalSettingsStore()) 26 | 27 | const inputPATHList = ioPath.getList() 28 | const outputPATH = getOutPutPATH() 29 | 30 | let _gh_proxy: string | null 31 | if (ghProxy.value === '') { 32 | _gh_proxy = null 33 | } else { 34 | _gh_proxy = ghProxy.value 35 | } 36 | 37 | return JSON.stringify({ 38 | pretrained_model_name: selectedSRModel.value, 39 | device: selectedTorchDevice.value, 40 | gh_proxy: _gh_proxy, 41 | target_scale: targetScale.value, 42 | output_path: outputPATH, 43 | input_path: inputPATHList 44 | }) 45 | } 46 | -------------------------------------------------------------------------------- /src/renderer/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { LANG_LIST } from '../plugins/i18n' 2 | 3 | class Utils { 4 | /** 5 | * @description 返回语言,和语言数量 6 | * @param id 语言id 0-> en, 1-> zh, 2-> ja, 3-> fr 7 | */ 8 | static getLanguage(id: number): { lang: string; numLang: number } { 9 | const langs = LANG_LIST 10 | return { 11 | lang: langs[id], 12 | numLang: langs.length 13 | } 14 | } 15 | 16 | /** 17 | * @description 等待一段时间 18 | * @param timeout 等待时间,单位毫秒 19 | */ 20 | static sleep(timeout: number): Promise { 21 | return new Promise((resolve) => setTimeout(resolve, timeout)) 22 | } 23 | 24 | /** 25 | * @description Deep, Dark, Fantasy? 真·深度睡眠 26 | * @param miliseconds 等待时间,单位毫秒 27 | */ 28 | static DeepDeepSleep(miliseconds: number): void { 29 | const currentTime = new Date().getTime() 30 | while (currentTime + miliseconds >= new Date().getTime()) { 31 | /* empty */ 32 | } 33 | } 34 | 35 | /** 36 | * @description 生成超长随机字符串 37 | */ 38 | static getRandString(): string { 39 | return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15) 40 | } 41 | 42 | /** 43 | * @description: 防抖函数装饰器,防止用户频繁点击 44 | * @param fn 需要防抖的函数 45 | * @param delay 防抖的时间间隔,默认500ms 46 | */ 47 | static clickDebounce( 48 | fn: (...args: any[]) => void, 49 | delay: number = 500 50 | ): (...args: any[]) => void { 51 | let timer: NodeJS.Timeout | null = null 52 | let immediate = true 53 | 54 | return (...args: any[]) => { 55 | if (timer) { 56 | clearTimeout(timer) 57 | } 58 | 59 | if (immediate) { 60 | fn(...args) 61 | immediate = false 62 | } 63 | 64 | timer = setTimeout(() => { 65 | immediate = true 66 | }, delay) 67 | } 68 | } 69 | } 70 | 71 | export const { getLanguage, sleep, DeepDeepSleep, getRandString, clickDebounce } = Utils 72 | -------------------------------------------------------------------------------- /src/renderer/src/utils/pathFormat.ts: -------------------------------------------------------------------------------- 1 | class PathFormat { 2 | private rootpath: string 3 | 4 | constructor() { 5 | this.rootpath = '' 6 | } 7 | 8 | /** 9 | * @description 设置本次上传的根目录 10 | */ 11 | setRootPath(path: string): void { 12 | const segments = path.split(/[/\\]/) 13 | if (segments.length > 1) { 14 | segments.pop() 15 | this.rootpath = segments.join(path.startsWith('/') ? '/' : '\\') 16 | } 17 | } 18 | 19 | /** 20 | * @description 返回本次上传的根目录 21 | */ 22 | getRootPath(): string { 23 | return this.rootpath 24 | } 25 | 26 | /** 27 | * @description 相对于本次上传的根目录,返回拼接后的真实路径 28 | */ 29 | getNewPath(path: string): string { 30 | const segments = path.split(/[/\\]/) 31 | return this.rootpath + segments.join(this.rootpath.startsWith('/') ? '/' : '\\') 32 | } 33 | 34 | /** 35 | * @description 检查路径格式是否正确 36 | */ 37 | static checkPath(path: string): boolean { 38 | return path.startsWith('/') || path.includes('\\') 39 | } 40 | 41 | /** 42 | * @description 返回文件名 43 | */ 44 | static getFileName(path: string): string { 45 | const segments = path.split(/[/\\]/) 46 | return segments[segments.length - 1] 47 | } 48 | } 49 | 50 | export default PathFormat 51 | -------------------------------------------------------------------------------- /src/renderer/src/utils/switchLanguage.ts: -------------------------------------------------------------------------------- 1 | import { storeToRefs } from 'pinia' 2 | import { useGlobalSettingsStore } from '../store/globalSettingsStore' 3 | import { getLanguage } from './index' 4 | 5 | /** 6 | * @description 切换语言,第一次切换到中文 7 | */ 8 | export function switchLanguage(): void { 9 | const { langsNum } = storeToRefs(useGlobalSettingsStore()) 10 | if (langsNum.value === 114514) { 11 | langsNum.value = 1 12 | } else { 13 | langsNum.value = (langsNum.value + 1) % getLanguage(0).numLang 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/renderer/src/views/Final2xHome.vue: -------------------------------------------------------------------------------- 1 | 113 | 114 | 138 | 139 | 183 | -------------------------------------------------------------------------------- /src/renderer/src/views/Final2xSettings.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 99 | 100 | 117 | -------------------------------------------------------------------------------- /test/utils/IOPath.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @vitest-environment jsdom 3 | */ 4 | 5 | import ioPath from '../../src/renderer/src/utils/IOPath' 6 | import { beforeEach, describe, expect, it } from 'vitest' 7 | import { createPinia, setActivePinia, storeToRefs } from 'pinia' 8 | import { useIOPathStore } from '../../src/renderer/src/store/ioPathStore' 9 | 10 | describe('IOPath', () => { 11 | beforeEach(() => { 12 | setActivePinia(createPinia()) 13 | }) 14 | 15 | it('test_ioPath', () => { 16 | const { outputpath } = storeToRefs(useIOPathStore()) 17 | // checkID 18 | expect(ioPath.checkID('114514')).toBe(false) 19 | // test inputpath 20 | ioPath.add('114514', 'test') 21 | // checkID 22 | expect(ioPath.checkID('114514')).toBe(true) 23 | expect(ioPath.getByID('114514')).toBe('test') 24 | ioPath.add('114514', 'test2') 25 | expect(ioPath.getByID('114514')).toBe('test2') 26 | 27 | expect(ioPath.getList()).toEqual(['test2']) 28 | 29 | expect(ioPath.getAllPath()).toEqual('114514 : test2\n') 30 | expect(ioPath.show()).toEqual('test2\n') 31 | 32 | ioPath.delete('114514') 33 | expect(ioPath.getByID('114514')).toBe('') 34 | 35 | expect(ioPath.isEmpty()).toBe(true) 36 | 37 | // test outputpath 38 | ioPath.setoutputpath('/test') 39 | expect(ioPath.getoutputpath()).toBe('/test') 40 | ioPath.setoutputpathManual('/test2') 41 | expect(ioPath.getoutputpath()).toBe('/test2') 42 | ioPath.setoutputpath('') 43 | expect(ioPath.getoutputpath()).toBe('/test2') 44 | outputpath.value = '' // 模拟用户手动清除outputpath 45 | ioPath.setoutputpath('/testWhenEmpty') 46 | expect(ioPath.getoutputpath()).toBe('/testWhenEmpty') 47 | ioPath.setoutputpathManual('/test2') 48 | 49 | // clear ALL 50 | ioPath.add('114514', 'test') 51 | ioPath.clearALL() 52 | expect(ioPath.getList()).toEqual([]) 53 | expect(ioPath.isEmpty()).toBe(true) 54 | expect(ioPath.getoutputpath()).toBe('/test2') 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /test/utils/getFinal2xconfig.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @vitest-environment jsdom 3 | */ 4 | 5 | import { getFinal2xconfig } from '../../src/renderer/src/utils/getFinal2xconfig' 6 | import ioPath from '../../src/renderer/src/utils/IOPath' 7 | import { beforeEach, describe, expect, it } from 'vitest' 8 | import { createPinia, setActivePinia } from 'pinia' 9 | 10 | describe('getFinal2xconfig', () => { 11 | beforeEach(() => { 12 | // 创建一个新 pinia,并使其处于激活状态 13 | setActivePinia(createPinia()) 14 | }) 15 | 16 | it('test_getFinal2xconfig', () => { 17 | ioPath.add('114514', '/Users/Downloads/114514') 18 | expect(getFinal2xconfig()).toEqual( 19 | '{"pretrained_model_name":"RealESRGAN_RealESRGAN_x2plus_2x.pth","device":"auto","gh_proxy":null,"target_scale":null,"output_path":"/Users/Downloads","input_path":["/Users/Downloads/114514"]}' 20 | ) 21 | ioPath.setoutputpath('/test') 22 | expect(getFinal2xconfig()).toEqual( 23 | '{"pretrained_model_name":"RealESRGAN_RealESRGAN_x2plus_2x.pth","device":"auto","gh_proxy":null,"target_scale":null,"output_path":"/test","input_path":["/Users/Downloads/114514"]}' 24 | ) 25 | ioPath.add('114514', '/test1') 26 | expect(getFinal2xconfig()).toEqual( 27 | '{"pretrained_model_name":"RealESRGAN_RealESRGAN_x2plus_2x.pth","device":"auto","gh_proxy":null,"target_scale":null,"output_path":"/test","input_path":["/test1"]}' 28 | ) 29 | ioPath.add('1919810', '/test2') 30 | expect(getFinal2xconfig()).toEqual( 31 | '{"pretrained_model_name":"RealESRGAN_RealESRGAN_x2plus_2x.pth","device":"auto","gh_proxy":null,"target_scale":null,"output_path":"/test","input_path":["/test1","/test2"]}' 32 | ) 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /test/utils/index.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @vitest-environment jsdom 3 | */ 4 | 5 | import { sleep, DeepDeepSleep, getRandString, clickDebounce } from '../../src/renderer/src/utils' 6 | import { describe, expect, it, vi } from 'vitest' 7 | 8 | describe('Utils', () => { 9 | it('sleep', async () => { 10 | const start = new Date().getTime() 11 | await sleep(1010) 12 | const end = new Date().getTime() 13 | expect(end - start).toBeGreaterThanOrEqual(1000) 14 | }) 15 | 16 | it('DeepDeepSleep', () => { 17 | const start = new Date().getTime() 18 | DeepDeepSleep(1010) 19 | const end = new Date().getTime() 20 | expect(end - start).toBeGreaterThanOrEqual(1000) 21 | }) 22 | 23 | it('getRandString', () => { 24 | expect(getRandString()) 25 | }) 26 | 27 | it('clickDebounce', async () => { 28 | const fn = (): void => console.log('click') 29 | // spy on fn to check if it's called 30 | const spy = vi.spyOn(console, 'log') 31 | // call fn 3 times 32 | const debouncedFn = clickDebounce(fn, 1000) 33 | debouncedFn() 34 | debouncedFn() 35 | debouncedFn() 36 | // check if fn is called only once 37 | expect(spy).toHaveBeenCalledTimes(1) 38 | // await new Promise((resolve) => setTimeout(resolve, 1000)); 39 | await sleep(1000) 40 | debouncedFn() 41 | expect(spy).toHaveBeenCalledTimes(2) 42 | }) 43 | }) 44 | -------------------------------------------------------------------------------- /test/utils/pathFormat.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest' 2 | import PathFormat from '../../src/renderer/src/utils/pathFormat' 3 | 4 | describe('PathFormat', () => { 5 | it('test_unix', () => { 6 | const pathFormat = new PathFormat() 7 | pathFormat.setRootPath('/Users/test/Downloads/unix') 8 | const check: Array = [pathFormat.getRootPath(), pathFormat.getNewPath('/unix/test.txt')] 9 | expect(check).toStrictEqual(['/Users/test/Downloads', '/Users/test/Downloads/unix/test.txt']) 10 | }) 11 | 12 | it('test_win', () => { 13 | const pathFormat = new PathFormat() 14 | pathFormat.setRootPath('C:\\Users\\test\\Downloads\\win') 15 | const check: Array = [pathFormat.getRootPath(), pathFormat.getNewPath('/win/test.txt')] 16 | expect(check).toStrictEqual([ 17 | 'C:\\Users\\test\\Downloads', 18 | 'C:\\Users\\test\\Downloads\\win\\test.txt' 19 | ]) 20 | }) 21 | 22 | it('check_path', () => { 23 | const check: Array = [ 24 | PathFormat.checkPath('/Users/test/Downloads/unix'), 25 | PathFormat.checkPath('C:\\Users\\test\\Downloads\\win'), 26 | PathFormat.checkPath('C:Users/test/Downloads/unix/test.txt'), 27 | PathFormat.checkPath('Users/test/Downloads/unix/test.txt') 28 | ] 29 | expect(check).toStrictEqual([true, true, false, false]) 30 | }) 31 | 32 | it('get_file_name', () => { 33 | expect(PathFormat.getFileName('/Users/test/Downloads/unix/114514.txt')).toBe('114514.txt') 34 | expect(PathFormat.getFileName('C:\\Users\\test\\Downloads\\win\\genshin.png')).toBe( 35 | 'genshin.png' 36 | ) 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /test/utils/switchLanguage.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @vitest-environment jsdom 3 | */ 4 | 5 | import { useGlobalSettingsStore } from '../../src/renderer/src/store/globalSettingsStore' 6 | import { switchLanguage } from '../../src/renderer/src/utils/switchLanguage' 7 | import { getLanguage } from '../../src/renderer/src/utils' 8 | import { beforeEach, describe, expect, it } from 'vitest' 9 | import { setActivePinia, createPinia, storeToRefs } from 'pinia' 10 | 11 | describe('switchLanguage', () => { 12 | beforeEach(() => { 13 | // 创建一个新 pinia,并使其处于激活状态 14 | setActivePinia(createPinia()) 15 | }) 16 | 17 | it('test_switchLanguage', () => { 18 | const { langsNum } = storeToRefs(useGlobalSettingsStore()) 19 | switchLanguage() 20 | expect(langsNum.value).toBe(1) // 第一次后应该是 'zh' 21 | langsNum.value = 0 // 手动设置为 'en' 22 | // 断言语言切换是否正确 23 | expect(langsNum.value).toBe(0) // 初始语言是 'en', 所以切换一次后应该是 'zh', 切换两次后应该是 'ja', 切换三次后应该是 'en' 24 | const numLang = getLanguage(0).numLang 25 | for (let i = 0; i < 30; i++) { 26 | expect(langsNum.value).toBe(i % numLang) 27 | switchLanguage() 28 | } 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [{ "path": "./tsconfig.node.json" }, { "path": "./tsconfig.web.json" }], 4 | "compilerOptions": { 5 | "types": [ 6 | "vitest", 7 | "vitest/globals" 8 | ] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@electron-toolkit/tsconfig/tsconfig.node.json", 3 | "include": ["electron.vite.config.*", "src/main/*", "src/preload/*"], 4 | "compilerOptions": { 5 | "composite": true, 6 | "types": ["electron-vite/node"] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.web.json: -------------------------------------------------------------------------------- 1 | // tsconfig.web.json 2 | { 3 | "extends": "@electron-toolkit/tsconfig/tsconfig.web.json", 4 | "include": [ 5 | "src/renderer/src/env.d.ts", 6 | "src/renderer/src/locales/*.ts", 7 | "src/renderer/src/**/*", 8 | "src/renderer/src/**/*.vue", 9 | "src/preload/*.d.ts" 10 | ], 11 | "compilerOptions": { 12 | "composite": true, 13 | "baseUrl": ".", 14 | "paths": { 15 | "@renderer/*": [ 16 | "src/renderer/src/*" 17 | ] 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | export default defineConfig({ 3 | test: { 4 | include: ['test/**/*.test.ts'] 5 | } 6 | }) 7 | --------------------------------------------------------------------------------