Repository: EutropicAI/Final2x Branch: main Commit: 0b590138d0bb Files: 68 Total size: 101.0 KB Directory structure: gitextract_eypaha0i/ ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug.yaml │ │ ├── config.yml │ │ └── feature.yaml │ └── workflows/ │ ├── CI-build.yml │ ├── CI-test.yml │ ├── Release.yml │ ├── issue-helper.yml │ └── issue-translator.yml ├── .gitignore ├── .npmrc ├── LICENSE ├── README.md ├── README_i18n/ │ └── README_zh.md ├── build/ │ ├── entitlements.mac.plist │ └── notarize.js ├── electron-builder.yml ├── electron.vite.config.ts ├── eslint.config.js ├── package.json ├── resources/ │ └── download-core.js ├── src/ │ ├── main/ │ │ ├── getCorePath.ts │ │ ├── index.ts │ │ ├── openDirectory.ts │ │ └── runCommand.ts │ ├── preload/ │ │ ├── index.d.ts │ │ └── index.ts │ ├── renderer/ │ │ ├── index.html │ │ └── src/ │ │ ├── App.vue │ │ ├── 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/ │ │ │ ├── index.html │ │ │ └── robots.txt │ │ ├── router/ │ │ │ └── index.ts │ │ ├── store/ │ │ │ ├── SRSettingsStore.ts │ │ │ ├── globalSettingsStore.ts │ │ │ └── ioPathStore.ts │ │ ├── utils/ │ │ │ ├── IOPath.ts │ │ │ ├── SROptions.ts │ │ │ ├── getFinal2xCoreConfig.ts │ │ │ ├── index.ts │ │ │ ├── modelOptions.ts │ │ │ ├── pathFormat.ts │ │ │ └── switchLanguage.ts │ │ └── views/ │ │ ├── Final2xHome.vue │ │ └── Final2xSettings.vue │ └── shared/ │ ├── const/ │ │ └── ipc.ts │ └── type/ │ └── core.ts ├── test/ │ ├── node/ │ │ └── getCorePath.test.ts │ └── web/ │ ├── IOPath.test.ts │ ├── index.test.ts │ ├── pathFormat.test.ts │ └── switchLanguage.test.ts ├── tsconfig.json ├── tsconfig.node.json ├── tsconfig.web.json └── vitest.config.ts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE/bug.yaml ================================================ name: 🐛 Bug report | 错误报告 | BUG報告 description: Create a bug report to help us improve | 创建bug报告以帮助我们改进 | 改善を支援するためのレポートを作成する title: '[Bug] ' labels: ['bug'] body: - type: checkboxes id: checks attributes: label: Please carefully review each item in the checklist below | 请认真检查以下清单中的每一项 | 以下のチェックリストの各項目を注意深く確認してください options: - label: Searched and didn't find a similar issue | 已经搜索过,没有发现类似issue | 類似の問題が見つかりませんでした - label: Searched documentation and didn't find relevant content | 已经搜索过文档,没有发现相关内容 | ドキュメントを検索して関連する内容が見つかりませんでした - label: Tried with the latest version and the issue still exists | 已经尝试使用过最新版,问题依旧存在 | 最新バージョンを試しましたが問題は解消されませんでした - type: input id: app-version attributes: label: Software Version | 软件版本 | ソフトウェアバージョン placeholder: '1.1.4' validations: required: true - type: dropdown id: system-type attributes: label: Operating System | 操作系统 | オペレーティングシステム options: - Windows x64 - Windows arm64 - macOS x64 (Intel) - macOS arm64 (M1,M2...) - Ubuntu x64 - Debian x64 - Arch Linux x64 - Other Linux x64 validations: required: true - type: input id: system-version attributes: label: System Version | 系统版本 | システムバージョン validations: required: true - type: textarea id: description attributes: label: Describe the bug | 描述错误 | BUGの説明 description: | A clear and concise description of what the bug is 描述错误的详细信息 バグの内容を明確かつ簡潔に説明してください validations: required: true - type: textarea id: reproduce-steps attributes: label: To reproduce | 复现步骤 | 再現方法 description: Steps to reproduce the behavior | 复现行为的步骤 | 不具合の再現手順 value: | 1. Go to '...' 2. Click on '....' 3. See error validations: required: true - type: textarea id: log attributes: label: Error log | 报错日志 | ログ description: your error log | 您的错误日志 | エラーログ value: | ``` your error log ``` validations: required: true - type: textarea id: other attributes: label: Additional context | 附加内容 | 追加コンテキスト description: | Add any other context and screenshots to help explain your problem 添加任何其他上下文和截图,以帮助解释您的问题 問題を説明するために他の文脈やスクリーンショットを追加してください ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false ================================================ FILE: .github/ISSUE_TEMPLATE/feature.yaml ================================================ name: 🚀 Feature request | 功能请求 | フィーチャーリクエスト description: Suggest an idea for this project | 为项目提供一个创意建议 | このプロジェクトにアイデアを提案する title: '[FEATURE] ' labels: ['enhancement'] body: - type: checkboxes id: checks attributes: label: Please carefully review each item in the checklist below | 请认真检查以下清单中的每一项 | 以下のチェックリストの各項目を注意深く確認してください options: - label: Searched and didn't find a similar issue | 已经搜索过,没有发现类似issue | 類似の問題が見つかりませんでした - type: textarea id: is-related attributes: label: Is your feature request related to a problem? | 你的feature请求是否与一个问题有关? | あなたのfeatureリクエストは質問に関連していますか? description: | A clear and concise description of what the problem is 请清楚而简明地描述问题是什么 問題が何であるかを明確かつ簡潔に説明してください validations: required: true - type: textarea id: detail attributes: label: Detail | 详细描述 | 詳細な説明 description: | A clear and concise description of what you want to happen 请清楚而简明地描述您想要实现的内容 実現したい内容を明確かつ簡潔に説明してください validations: required: true - type: textarea id: log attributes: label: Additional context | 附加内容 | 追加コンテキスト description: | Add any other context or screenshots about the feature request here 在这里添加任何其他上下文或截图,以帮助解释您的功能请求 その他の文脈やスクリーンショットを追加して、機能リクエストについて説明してください ================================================ FILE: .github/workflows/CI-build.yml ================================================ name: CI-build on: push: branches: - main paths-ignore: - '**.md' - LICENSE pull_request: paths-ignore: - '**.md' - LICENSE workflow_dispatch: jobs: windows: strategy: matrix: os-version: ['x64', 'arm64'] runs-on: windows-latest steps: - uses: actions/checkout@v4 with: submodules: recursive - uses: actions/setup-node@v4 with: node-version: 22 - uses: pnpm/action-setup@v4 with: version: 10 - name: build run: | pnpm install pnpm fetchcore pnpm run build:win-${{ matrix.os-version }} env: GH_TOKEN: ${{ secrets.GH_TOKEN }} - name: zip-unpacked-x64 if: matrix.os-version == 'x64' run: | cd .\dist\win-unpacked 7z a -r Final2x-windows-${{ matrix.os-version }}-unpacked.7z * - name: zip-unpacked-arm64 if: matrix.os-version == 'arm64' run: | cd .\dist\win-arm64-unpacked 7z a -r Final2x-windows-${{ matrix.os-version }}-unpacked.7z * - name: upload-unpacked-x64 if: matrix.os-version == 'x64' uses: actions/upload-artifact@v4 with: name: Final2x-windows-${{ matrix.os-version }}-unpacked path: dist/win-unpacked/*.7z - name: upload-unpacked-arm64 if: matrix.os-version == 'arm64' uses: actions/upload-artifact@v4 with: name: Final2x-windows-${{ matrix.os-version }}-unpacked path: dist/win-arm64-unpacked/*.7z macos: strategy: matrix: os-version: ['arm64'] runs-on: macos-14 steps: - uses: actions/checkout@v4 with: submodules: recursive - uses: actions/setup-node@v4 with: node-version: 22 - uses: pnpm/action-setup@v4 with: version: 10 - name: build run: | pnpm install pnpm fetchcore pnpm run build:mac-${{ matrix.os-version }} env: ARCH: ${{ matrix.os-version }} GH_TOKEN: ${{ secrets.GH_TOKEN }} - name: zip-unpacked-arm64 if: matrix.os-version == 'arm64' run: | cd ./dist/mac-arm64 7z a -r Final2x-macos-${{ matrix.os-version }}-unpacked.7z * - name: upload-dmg uses: actions/upload-artifact@v4 with: name: Final2x-macos-${{ matrix.os-version }}-dmg path: dist/*.dmg - name: upload-unpacked-arm64 if: matrix.os-version == 'arm64' uses: actions/upload-artifact@v4 with: name: Final2x-macos-${{ matrix.os-version }}-unpacked path: dist/mac-arm64/*.7z linux-pip: strategy: matrix: os-version: ['x64'] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: submodules: recursive - uses: actions/setup-node@v4 with: node-version: 22 - uses: pnpm/action-setup@v4 with: version: 10 - name: build run: | pnpm install pnpm run build:linux-${{ matrix.os-version }} env: SKIP_DOWNLOAD_CORE: true GH_TOKEN: ${{ secrets.GH_TOKEN }} - name: zip-unpacked run: | cd ./dist/linux-unpacked 7z a -r Final2x-linux-pip-${{ matrix.os-version }}-unpacked.7z * - name: upload-snap uses: actions/upload-artifact@v4 with: name: Final2x-linux-pip-${{ matrix.os-version }}-snap path: dist/*.snap - name: upload-AppImage uses: actions/upload-artifact@v4 with: name: Final2x-linux-pip-${{ matrix.os-version }}-AppImage path: dist/*.AppImage - name: upload-deb uses: actions/upload-artifact@v4 with: name: Final2x-linux-pip-${{ matrix.os-version }}-deb path: dist/*.deb - name: upload-unpacked uses: actions/upload-artifact@v4 with: name: Final2x-linux-pip-${{ matrix.os-version }}-unpacked path: dist/linux-unpacked/*.7z ================================================ FILE: .github/workflows/CI-test.yml ================================================ name: CI-test on: push: branches: - main paths-ignore: - '**.md' - LICENSE pull_request: paths-ignore: - '**.md' - LICENSE workflow_dispatch: jobs: test: strategy: matrix: os-version: ['ubuntu-latest'] runs-on: ${{ matrix.os-version }} steps: - uses: actions/checkout@v4 with: submodules: recursive - uses: actions/setup-node@v4 with: node-version: 22 - uses: pnpm/action-setup@v4 with: version: 10 - name: Test run: | pnpm install pnpm run lint pnpm run typecheck pnpm run test env: SKIP_DOWNLOAD_CORE: true GH_TOKEN: ${{ secrets.GH_TOKEN }} ================================================ FILE: .github/workflows/Release.yml ================================================ name: Release on: workflow_dispatch: push: tags: - 'v*' jobs: windows: strategy: matrix: os-version: ['x64', 'arm64'] runs-on: windows-latest steps: - uses: actions/checkout@v4 with: submodules: recursive - uses: actions/setup-node@v4 with: node-version: 22 - uses: pnpm/action-setup@v4 with: version: 10 - name: build run: | pnpm install pnpm fetchcore pnpm run build:win-${{ matrix.os-version }} env: GH_TOKEN: ${{ secrets.GH_TOKEN }} - name: zip-unpacked-x64 if: matrix.os-version == 'x64' run: | cd .\dist\win-unpacked 7z a -r Final2x-windows-${{ matrix.os-version }}-unpacked.7z * - name: zip-unpacked-arm64 if: matrix.os-version == 'arm64' run: | cd .\dist\win-arm64-unpacked 7z a -r Final2x-windows-${{ matrix.os-version }}-unpacked.7z * - name: upload-unpacked-x64 if: matrix.os-version == 'x64' uses: actions/upload-artifact@v4 with: name: Final2x-windows-${{ matrix.os-version }}-unpacked path: dist/win-unpacked/*.7z - name: upload-unpacked-arm64 if: matrix.os-version == 'arm64' uses: actions/upload-artifact@v4 with: name: Final2x-windows-${{ matrix.os-version }}-unpacked path: dist/win-arm64-unpacked/*.7z macos: strategy: matrix: os-version: ['arm64'] runs-on: macos-14 steps: - uses: actions/checkout@v4 with: submodules: recursive - uses: actions/setup-node@v4 with: node-version: 22 - uses: pnpm/action-setup@v4 with: version: 10 - name: build run: | pnpm install pnpm fetchcore pnpm run build:mac-${{ matrix.os-version }} env: ARCH: ${{ matrix.os-version }} GH_TOKEN: ${{ secrets.GH_TOKEN }} - name: rename run: | cd ./dist mv *.dmg Final2x-macos-${{ matrix.os-version }}-dmg.dmg - name: zip-unpacked-arm64 if: matrix.os-version == 'arm64' run: | cd ./dist/mac-arm64 7z a -r Final2x-macos-${{ matrix.os-version }}-unpacked.7z * - name: upload-dmg uses: actions/upload-artifact@v4 with: name: Final2x-macos-${{ matrix.os-version }}-dmg path: dist/*.dmg - name: upload-unpacked-arm64 if: matrix.os-version == 'arm64' uses: actions/upload-artifact@v4 with: name: Final2x-macos-${{ matrix.os-version }}-unpacked path: dist/mac-arm64/*.7z linux-pip: strategy: matrix: os-version: ['x64'] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: submodules: recursive - uses: actions/setup-node@v4 with: node-version: 22 - uses: pnpm/action-setup@v4 with: version: 10 - name: build run: | pnpm install pnpm run build:linux-${{ matrix.os-version }} env: SKIP_DOWNLOAD_CORE: true GH_TOKEN: ${{ secrets.GH_TOKEN }} - name: zip-unpacked run: | cd ./dist/linux-unpacked 7z a -r Final2x-linux-pip-${{ matrix.os-version }}-unpacked.7z * - name: rename run: | cd ./dist mv *.snap Final2x-linux-pip-${{ matrix.os-version }}-snap.snap mv *.AppImage Final2x-linux-pip-${{ matrix.os-version }}-AppImage.AppImage mv *.deb Final2x-linux-pip-${{ matrix.os-version }}-deb.deb - name: upload-snap uses: actions/upload-artifact@v4 with: name: Final2x-linux-pip-${{ matrix.os-version }}-snap path: dist/*.snap - name: upload-AppImage uses: actions/upload-artifact@v4 with: name: Final2x-linux-pip-${{ matrix.os-version }}-AppImage path: dist/*.AppImage - name: upload-deb uses: actions/upload-artifact@v4 with: name: Final2x-linux-pip-${{ matrix.os-version }}-deb path: dist/*.deb - name: upload-unpacked uses: actions/upload-artifact@v4 with: name: Final2x-linux-pip-${{ matrix.os-version }}-unpacked path: dist/linux-unpacked/*.7z github: needs: [windows, macos, linux-pip] runs-on: ubuntu-latest steps: - uses: actions/download-artifact@v4 with: path: asset - name: Flatten asset directory run: | tree asset mkdir dist find asset -type f -print0 | xargs -0 -I{} cp "{}" dist/ cd dist && ls -l - name: Create Release and Upload Release Asset uses: softprops/action-gh-release@v2 with: files: dist/* ================================================ FILE: .github/workflows/issue-helper.yml ================================================ name: issue-helper on: issues: types: [opened, reopened, edited] jobs: check-inactive: runs-on: ubuntu-latest steps: - name: close-issues uses: actions-cool/issues-helper@v2 with: actions: 'close-issues' token: ${{ secrets.GH_TOKEN }} inactive-day: 100 body: | Hello! your issue has been closed because it has been inactive for a long time. 你好,你的 issue 因为长时间不活跃而被自动关闭。 こんにちは、お問い合わせは長期間活動がないため、閉じられました。 check-title: runs-on: ubuntu-latest 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) steps: - name: close issue uses: actions-cool/issues-helper@v3 with: actions: 'create-comment, add-labels, close-issue' token: ${{ secrets.GH_TOKEN }} issue-number: ${{ github.event.issue.number }} labels: 'Invalid' body: | Hello @${{ github.event.issue.user.login }}, your issue has been closed because the title does not conform to our specification. 你好 @${{ github.event.issue.user.login }},为了能够进行高效沟通,我们对 issue 有一定的格式要求,你的 issue 因为标题不符合规范而被自动关闭。 こんにちは、@${{ github.event.issue.user.login }}さん、タイトルが仕様に準拠していないため、ご提案いただいた問題はクローズされました。 ================================================ FILE: .github/workflows/issue-translator.yml ================================================ name: 'issue-translator' on: issue_comment: types: [created] issues: types: [opened] jobs: translate: runs-on: ubuntu-latest steps: - uses: usthe/issues-translate-action@v2.7 with: CUSTOM_BOT_NOTE: Bot detected the issue body's language is not English, translate it automatically. ================================================ FILE: .gitignore ================================================ node_modules dist out *.log* *.DS_Store /resources/Final2x-core/ /outputs/ /.idea /coverage/ ================================================ FILE: .npmrc ================================================ shamefully-hoist=true ================================================ FILE: LICENSE ================================================ BSD 3-Clause License Copyright (c) 2023, Tohrusky Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: README.md ================================================ # Final2x
![MacOS](https://img.shields.io/badge/Support-MacOS-blue?logo=Apple&style=flat-square) ![Windows](https://img.shields.io/badge/Support-Windows-blue?logo=Windows&style=flat-square) ![Linux](https://img.shields.io/badge/Support-Linux-blue?logo=Linux&style=flat-square) [![CI-test](https://github.com/EutropicAI/Final2x/actions/workflows/CI-test.yml/badge.svg)](https://github.com/EutropicAI/Final2x/actions/workflows/CI-test.yml) [![CI-build](https://github.com/EutropicAI/Final2x/actions/workflows/CI-build.yml/badge.svg)](https://github.com/EutropicAI/Final2x/actions/workflows/CI-build.yml) [![Release](https://github.com/EutropicAI/Final2x/actions/workflows/Release.yml/badge.svg)](https://github.com/EutropicAI/Final2x/actions/workflows/Release.yml) ![Download](https://img.shields.io/github/downloads/EutropicAI/Final2x/total) ![GitHub](https://img.shields.io/github/license/EutropicAI/Final2x) A cross-platform image super-resolution tool. - News🎉: Final2x v4.0.0 is now available! It uses the [cccv](https://github.com/EutropicAI/cccv) backend, supporting custom models and more. See [custom model demo](https://github.com/EutropicAI/cccv_demo_remote_model). - News🎉: Final2x v3.0.0 is now available, support Nvidia 50 series GPUs now! ### Screenshots
image image
### Installation ##### [Download the latest release from here.](https://github.com/EutropicAI/Final2x/releases) #### Windows You can also use a package manager like winget or scoop to install and upgrade. Please note that the versions available through package managers may not always be the latest. #### MacOS ```bash sudo spctl --master-disable # Disable Gatekeeper, then allow applications downloaded from anywhere in System Preferences > Security & Privacy > General xattr -cr /Applications/Final2x.app ``` In first time, you need to run the command above in terminal to allow the app to run. #### Linux For Linux User, you need to install the dependencies first. Make sure you have Python >= 3.9 and PyTorch >= 2.0 installed ```bash pip install Final2x-core Final2x-core -h # check if the installation is successful apt install -y libomp5 xdg-utils ``` ### Reference The following references were referenced in the development of this project: - [Final2x-core](https://github.com/EutropicAI/Final2x-core) - [naive-ui](https://github.com/tusen-ai/naive-ui) - [electron-vite](https://github.com/alex8088/electron-vite) ### License This project is licensed under the BSD 3-Clause - see the [LICENSE file](./LICENSE) for details. ### Acknowledgements Feel free to reach out to the project maintainers with any questions or concerns~ Star History Chart ================================================ FILE: README_i18n/README_zh.md ================================================ # Final2x ================================================ FILE: build/entitlements.mac.plist ================================================ com.apple.security.cs.allow-jit com.apple.security.cs.allow-unsigned-executable-memory com.apple.security.cs.allow-dyld-environment-variables ================================================ FILE: build/notarize.js ================================================ module.exports = async (context) => { const { notarize } = require('@electron/notarize') if (process.platform !== 'darwin') return console.log('aftersign hook triggered, start to notarize app.') if (!process.env.CI) { console.log(`skipping notarizing, not in CI.`) return } if (!('APPLE_ID' in process.env && 'APPLE_ID_PASS' in process.env)) { console.warn('skipping notarizing, APPLE_ID and APPLE_ID_PASS env variables must be set.') return } const appId = 'com.final2x.app' const { appOutDir } = context const appName = context.packager.appInfo.productFilename try { await notarize({ appBundleId: appId, appPath: `${appOutDir}/${appName}.app`, appleId: process.env.APPLE_ID, appleIdPassword: process.env.APPLEIDPASS, }) } catch (error) { console.error(error) } console.log(`done notarizing ${appId}.`) } ================================================ FILE: electron-builder.yml ================================================ appId: com.final2x.app productName: Final2x directories: buildResources: build icon: resources/icon.png files: - '!**/.vscode/*' - '!src/*' - '!electron.vite.config.{js,ts,mjs,cjs}' - '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}' - '!{.env,.env.*,.npmrc,pnpm-lock.yaml}' - '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}' - '!resources/Final2x-core/**' asarUnpack: - resources/*.png - resources/*.svg - resources/*.ico extraResources: - from: resources/Final2x-core to: Final2x-core afterSign: build/notarize.js win: executableName: Final2x nsis: artifactName: ${name}-${version}-setup.${ext} shortcutName: ${productName} uninstallDisplayName: ${productName} createDesktopShortcut: always mac: entitlementsInherit: build/entitlements.mac.plist extendInfo: - NSCameraUsageDescription: Application requests access to the device's camera. - NSMicrophoneUsageDescription: Application requests access to the device's microphone. - NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder. - NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder. dmg: artifactName: ${name}-${version}.${ext} background: build/macosDMGbg.jpeg window: x: 100 y: 100 width: 480 height: 500 linux: target: - AppImage - snap - deb maintainer: Tohrusky category: Utility appImage: artifactName: ${name}-${version}.${ext} npmRebuild: false ================================================ FILE: electron.vite.config.ts ================================================ import { resolve } from 'node:path' import vue from '@vitejs/plugin-vue' import { defineConfig, externalizeDepsPlugin } from 'electron-vite' export default defineConfig({ main: { resolve: { alias: { '@main': resolve('src/main'), '@shared': resolve('src/shared'), }, }, plugins: [externalizeDepsPlugin()], }, preload: { plugins: [externalizeDepsPlugin()], }, renderer: { resolve: { alias: { '@renderer': resolve('src/renderer/src'), '@shared': resolve('src/shared'), }, }, plugins: [vue()], }, }) ================================================ FILE: eslint.config.js ================================================ import antfu from '@antfu/eslint-config' export default antfu( { ignores: [ 'dist', 'out', 'node_modules', 'build/*.js', 'resources/*.js', ], rules: { 'no-console': 'off', }, }, { files: ['**/*.md'], rules: { 'style/no-trailing-spaces': 'off', }, }, { files: ['**/*.yaml', '**/*.yml'], rules: { 'yaml/plain-scalar': 'off', }, }, { files: ['**/*.ts'], rules: { '@typescript-eslint/ban-ts-comment': ['error', { 'ts-ignore': 'allow-with-description' }], '@typescript-eslint/explicit-function-return-type': 'error', '@typescript-eslint/explicit-module-boundary-types': 'off', '@typescript-eslint/no-empty-function': ['error', { allow: ['arrowFunctions'] }], '@typescript-eslint/no-explicit-any': ['off'], '@typescript-eslint/no-non-null-assertion': 'off', '@typescript-eslint/no-var-requires': 'off', '@typescript-eslint/no-inferrable-types': 'off', 'node/prefer-global/process': 'off', }, }, { files: ['**/*.vue'], rules: { '@typescript-eslint/ban-ts-comment': ['error', { 'ts-ignore': 'allow-with-description' }], '@typescript-eslint/explicit-function-return-type': 'error', '@typescript-eslint/explicit-module-boundary-types': 'off', '@typescript-eslint/no-empty-function': ['error', { allow: ['arrowFunctions'] }], '@typescript-eslint/no-explicit-any': ['off'], '@typescript-eslint/no-non-null-assertion': 'off', '@typescript-eslint/no-var-requires': 'off', '@typescript-eslint/no-inferrable-types': 'off', 'vue/require-default-prop': 'off', 'vue/multi-word-component-names': 'off', }, }, ) ================================================ FILE: package.json ================================================ { "name": "Final2x", "productName": "Final2x", "version": "4.0.0", "description": "A cross-platform image super-resolution tool.", "author": "Tohrusky", "homepage": "https://github.com/EutropicAI/Final2x", "main": "./out/main/index.js", "engines": { "node": ">=18", "pnpm": ">=8" }, "scripts": { "dev": "electron-vite dev", "test": "vitest run --coverage", "lint": "eslint . --fix", "typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false", "typecheck:web": "vue-tsc --noEmit -p tsconfig.web.json --composite false", "typecheck": "pnpm run typecheck:node && npm run typecheck:web", "start": "electron-vite preview", "build": "electron-vite build", "postinstall": "electron-builder install-app-deps", "fetchcore": "node ./resources/download-core.js", "build:mac-arm64": "pnpm run build && electron-builder --mac --arm64 --publish=never", "build:mac-x64": "pnpm run build && electron-builder --mac --x64 --publish=never", "build:win-arm64": "pnpm run build && electron-builder --win --arm64 --dir --publish=never", "build:win-x64": "pnpm run build && electron-builder --win --x64 --dir --publish=never", "build:linux-x64": "pnpm run build && electron-builder --linux --x64 --publish=never", "build:linux-arm64": "pnpm run build && electron-builder --linux --arm64 --publish=never" }, "dependencies": { "@intlify/unplugin-vue-i18n": "^6.0.8", "@vicons/antd": "^0.13.0", "@vicons/ionicons5": "^0.13.0", "naive-ui": "^2.43.1", "pinia": "^3.0.3", "pinia-plugin-persistedstate": "^4.5.0", "sass": "^1.93.2", "systeminformation": "^5.30.8", "tree-kill": "^1.2.2", "vfonts": "^0.0.3", "vue": "^3.5.22", "vue-i18n": "^11.1.12", "vue-router": "^4.5.1" }, "devDependencies": { "@antfu/eslint-config": "^5.4.1", "@electron-toolkit/preload": "^3.0.2", "@electron-toolkit/tsconfig": "^1.0.1", "@electron-toolkit/utils": "^4.0.0", "@electron/notarize": "^2.5.0", "@vitejs/plugin-vue": "^5.2.4", "@vitest/coverage-v8": "^3.2.4", "@vue/test-utils": "^2.4.6", "electron": "^27.3.11", "electron-builder": "^26.0.12", "electron-vite": "^4.0.1", "eslint": "^9.37.0", "extract-zip": "^2.0.1", "jsdom": "^26.1.0", "node-fetch": "^3.3.2", "typescript": "^5.9.3", "vite": "^7.1.11", "vite-tsconfig-paths": "^5.1.4", "vitest": "^3.2.4", "vue-tsc": "^3.1.0" }, "pnpm": { "onlyBuiltDependencies": [ "electron" ], "overrides": { "@parcel/watcher": "npm:empty-npm-package@1.0.0" } } } ================================================ FILE: resources/download-core.js ================================================ // download Final2x-core from https://github.com/EutropicAI/Final2x-core/releases // and put it in resources folder const fetch = (...args) => import('node-fetch').then(({ default: fetch }) => fetch(...args)) const child_process = require('node:child_process') const fs = require('node:fs') const path = require('node:path') const coreDict = { 'macos-arm64': 'https://github.com/EutropicAI/Final2x-core/releases/download/v4.0.0/Final2x-core-macos-arm64.7z', 'windows-x64': 'https://github.com/EutropicAI/Final2x-core/releases/download/v4.0.0/Final2x-core-windows-x64.7z', } console.log('-'.repeat(50)) // 判断当前平台 const PLATFORM = process.env.PLATFORM || process.platform // 判断当前平台架构 const ARCH = process.env.ARCH || process.arch console.log(`Platform: ${PLATFORM}`, `| Arch: ${ARCH}`) if (process.env.SKIP_DOWNLOAD_CORE) { console.log('Skip download Final2x-core by env SKIP_DOWNLOAD_CORE') process.exit(0) } async function downloadAndUnzip(url, targetPath) { const zipFileName = path.basename(url) const zipFilePath = path.join(targetPath, zipFileName) const res = await fetch(url) const dest = fs.createWriteStream(zipFilePath) dest.on('finish', () => { console.log(`Download ${zipFileName} success!`) // 解压缩文件, 命令行调用 7z const Final2xCorePath = path.join(targetPath, 'Final2x-core') const unzipCmd = `7z x ${zipFilePath} -o${Final2xCorePath}` console.log(`Unzip command: ${unzipCmd}`) // 使用异步方式执行解压命令 child_process.exec(unzipCmd, (error) => { if (error) { console.error(`Unzip error: ${error}`) return } console.log(`Unzip ${zipFileName} success!`) // 删除压缩文件 fs.unlinkSync(zipFilePath) console.log(`Delete ${zipFileName} success!`) }) }) res.body.pipe(dest) } async function downloadAndUnzipCore(platform) { const url = coreDict[platform] if (!url) { console.error('Invalid platform') return } const targetPath = path.join(__dirname) console.log(`Target path: ${targetPath}`) if (fs.existsSync(path.join(targetPath, 'Final2x-core'))) { console.log('Final2x-core already exists, skip download!') return } if (!fs.existsSync(targetPath)) { fs.mkdirSync(targetPath, { recursive: true }) } await downloadAndUnzip(url, targetPath) } // 选择要下载的平台 let platformToDownload = '' if (PLATFORM === 'darwin') { platformToDownload = ARCH === 'arm64' ? 'macos-arm64' : 'macos-x64' } else if (PLATFORM === 'linux') { console.error('Skip download Final2x-core for linux! Please use pip to install Final2x-core') process.exit(0) } else if (PLATFORM === 'win32') { platformToDownload = 'windows-x64' } else { console.error('Unsupported platform!') process.exit(1) } console.log(`Downloading Final2x-core for ${platformToDownload}...`) // 执行下载和解压 downloadAndUnzipCore(platformToDownload) .then() .catch((err) => { console.error(err) }) ================================================ FILE: src/main/getCorePath.ts ================================================ import { spawnSync } from 'node:child_process' import path from 'node:path' import { app } from 'electron' const FINAL2X_CORE_NAME = 'Final2x-core' const FINAL2X_CORE_PATH = 'Final2x-core/Final2x-core' /** * 获取 Final2x-core 的路径 * dev模式下,存放在项目根目录下的 resources * 在 electron-builder 中配置 extraResources,ASAR 打包时将它放入 app.asar 同级目录 * @returns {string} Final2x-core 的路径 */ export function getCorePath(): string { if (!checkPipPackage()) { if (process.env.NODE_ENV === 'development') { return path.join(app.getAppPath(), 'resources', FINAL2X_CORE_PATH) } else { return path.join(app.getAppPath(), '..', FINAL2X_CORE_PATH) } } else { return FINAL2X_CORE_NAME } } export function checkPipPackage(): boolean { const command = `${FINAL2X_CORE_NAME} -h` const result = spawnSync(command, { shell: true }) return result.status === 0 } ================================================ FILE: src/main/index.ts ================================================ import { join } from 'node:path' import { electronApp, is, optimizer } from '@electron-toolkit/utils' import { IpcChannelInvoke, IpcChannelSend } from '@shared/const/ipc' import { app, BrowserWindow, ipcMain, Menu, nativeImage, shell, Tray } from 'electron' import appIcon from '../../resources/icon.png?asset' import trayIcon from '../../resources/tray.png?asset' import { openDirectory } from './openDirectory' import { killCommand, runCommand } from './runCommand' function createWindow(): void { // Create the browser window. const mainWindow = new BrowserWindow({ width: 670, height: 470, maxWidth: 870, minWidth: 670, maxHeight: 670, minHeight: 470, frame: false, show: false, autoHideMenuBar: true, icon: nativeImage.createFromPath(appIcon), webPreferences: { preload: join(__dirname, '../preload/index.js'), sandbox: false, }, }) if (process.platform === 'darwin') { app.dock.setIcon(nativeImage.createFromPath(appIcon)) } // Ipc events ipcMain.on(IpcChannelSend.EXECUTE_COMMAND, runCommand) ipcMain.on(IpcChannelSend.KILL_COMMAND, killCommand) ipcMain.handle(IpcChannelInvoke.OPEN_DIRECTORY_DIALOG, openDirectory) ipcMain.on(IpcChannelSend.MINIMIZE, () => { mainWindow.minimize() }) ipcMain.on(IpcChannelSend.MAXIMIZE, () => { if (mainWindow.isMaximized()) { mainWindow.restore() } else { mainWindow.maximize() } }) ipcMain.on(IpcChannelSend.CLOSE, () => { if (process.platform !== 'darwin') { app.quit() } else { app.hide() } }) // mainWindow mainWindow.on('ready-to-show', () => { mainWindow.show() }) mainWindow.webContents.setWindowOpenHandler((details) => { shell.openExternal(details.url) return { action: 'deny' } }) // HMR for renderer base on electron-vite cli. // Load the remote URL for development or the local html file for production. if (is.dev && process.env.ELECTRON_RENDERER_URL) { mainWindow.loadURL(process.env.ELECTRON_RENDERER_URL) mainWindow.webContents.openDevTools() } else { mainWindow.loadFile(join(__dirname, '../renderer/index.html')) } } let tray function setTray(): void { const Image = nativeImage.createFromPath(trayIcon) Image.setTemplateImage(true) tray = new Tray(Image) const contextMenu = Menu.buildFromTemplate([ { label: 'Open', click: (): void => { // On macOS it's common to re-create a window in the app when the // dock icon is clicked and there are no other windows open. if (BrowserWindow.getAllWindows().length === 0) { createWindow() } else { BrowserWindow.getAllWindows()[0].show() } }, }, { label: 'Exit', click: (): void => { app.quit() }, }, ]) tray.setToolTip('Final2x') tray.setContextMenu(contextMenu) } // disable hardware acceleration for Compatibility for windows app.disableHardwareAcceleration() // This method will be called when Electron has finished // initialization and is ready to create browser windows. // Some APIs can only be used after this event occurs. app.whenReady().then(() => { // Set app user model id for windows electronApp.setAppUserModelId('com.final2x.app') // Default open or close DevTools by F12 in development // and ignore CommandOrControl + R in production. // see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils app.on('browser-window-created', (_, window) => { optimizer.watchWindowShortcuts(window) }) setTray() createWindow() app.on('activate', () => { // On macOS it's common to re-create a window in the app when the // dock icon is clicked and there are no other windows open. if (BrowserWindow.getAllWindows().length === 0) createWindow() }) }) // Quit when all windows are closed, except on macOS. There, it's common // for applications and their menu bar to stay active until the user quits // explicitly with Cmd + Q. app.on('window-all-closed', () => { if (process.platform !== 'darwin') { app.quit() } }) // In this file you can include the rest of your app's specific main process // code. You can also put them in separate files and require them here. let isQuitting = false app.on('before-quit', async (event) => { if (isQuitting) { console.log('Quitting...') return } console.log('Killing child process before quitting...') event.preventDefault() isQuitting = true await killCommand() app.quit() }) ================================================ FILE: src/main/openDirectory.ts ================================================ import { dialog } from 'electron' /** * @description Open a directory or file/multiple files * @param _ Unused parameter, can be used for context in future * @param p The properties of the dialog */ export async function openDirectory(_, p: Array<'openFile' | 'openDirectory' | 'multiSelections'>): Promise> { try { const { canceled, filePaths } = await dialog.showOpenDialog({ properties: p }) return canceled ? [] : filePaths } catch (error) { console.error('Error opening directory dialog:', error) return [] } } ================================================ FILE: src/main/runCommand.ts ================================================ import type { Final2xCoreConfig } from '@shared/type/core' import type { IpcMainEvent } from 'electron' import type { ChildProcessWithoutNullStreams } from 'node:child_process' import { spawn } from 'node:child_process' import { once } from 'node:events' import { IpcChannelOn } from '@shared/const/ipc' import kill from 'tree-kill' import { getCorePath } from './getCorePath' let child: ChildProcessWithoutNullStreams | null = null export async function runCommand(event: IpcMainEvent, coreConfig: Final2xCoreConfig): Promise { let config_json = JSON.stringify(coreConfig.config) // eslint-disable-next-line node/prefer-global/buffer config_json = Buffer.from(config_json, 'utf8').toString('base64') const resourceUrl = getCorePath() let command = `"${resourceUrl}" -b ${config_json}` if (!coreConfig.options.open_output_folder) { command += ' -n' } console.log(command) child = spawn(command, { shell: true }) child.stdout.on('data', (data) => { event.sender.send(IpcChannelOn.COMMAND_STDOUT, data.toString()) }) child.stderr.on('data', (data) => { event.sender.send(IpcChannelOn.COMMAND_STDERR, data.toString()) }) const [code] = await once(child, 'close') event.sender.send(IpcChannelOn.COMMAND_CLOSE, code) console.log(`Child process exited with code: ${code}`) child = null } export async function killCommand(): Promise { if (!child || !child.pid) { console.error('Could not find child process, nothing to kill.') return } const pid = child.pid console.log(`Kill child process with pid: ${pid}`) await new Promise((resolve) => { kill(pid, (err) => { if (err) { console.error(`Failed to kill process: ${err.message}`) } else { console.log('Process killed successfully') } if (child && child.pid === pid) { child = null } resolve() }) }) } ================================================ FILE: src/preload/index.d.ts ================================================ import type { ElectronAPI } from '@electron-toolkit/preload' declare global { interface Window { electron: ElectronAPI api: unknown } } ================================================ FILE: src/preload/index.ts ================================================ import { electronAPI } from '@electron-toolkit/preload' import { contextBridge } from 'electron' // Custom APIs for renderer const api = {} // Use `contextBridge` APIs to expose Electron APIs to // renderer only if context isolation is enabled, otherwise // just add to the DOM global. if (process.contextIsolated) { try { contextBridge.exposeInMainWorld('electron', electronAPI) contextBridge.exposeInMainWorld('api', api) } catch (error) { console.error(error) } } else { // @ts-ignore (define in dts) window.electron = electronAPI // @ts-ignore (define in dts) window.api = api } ================================================ FILE: src/renderer/index.html ================================================ Final2x
================================================ FILE: src/renderer/src/App.vue ================================================ ================================================ FILE: src/renderer/src/components/MyDarkMode.vue ================================================ ================================================ FILE: src/renderer/src/components/MyExternalLink.vue ================================================ ================================================ FILE: src/renderer/src/components/MyProgress.vue ================================================ ================================================ FILE: src/renderer/src/components/MySetting.vue ================================================ ================================================ FILE: src/renderer/src/components/NaiveDarkMode.vue ================================================ ================================================ FILE: src/renderer/src/components/TrafficLightsButtons.vue ================================================ ================================================ FILE: src/renderer/src/components/bottomNavigation.vue ================================================ ================================================ FILE: src/renderer/src/env.d.ts ================================================ /// declare module '*.vue' { import type { DefineComponent } from 'vue' const component: DefineComponent export default component } ================================================ FILE: src/renderer/src/locales/en.ts ================================================ export const en = { MyProgress: { text0: 'Processing started', text1: 'Processing in progress', text2: 'Please add an image', text3: 'Image list is empty', text4: 'Processing terminated', text5: 'Processing failed, skipping', text6: 'START', text7: 'TERMINATE', text8: 'LOG', text9: 'Processing failed', text10: 'Please click on the log to view the error message', }, Final2xHome: { text0: 'Removal successful', text1: 'Click or drag and drop images or folders here to upload', }, Final2xSettings: { text10: 'Device', text11: 'Model', text15: 'Custom Scale', text16: 'Default', text17: 'Output Folder', text18: 'Proxy', text19: 'Format', text20: 'Tile Process', }, } ================================================ FILE: src/renderer/src/locales/fr.ts ================================================ export const fr = { MyProgress: { text0: 'Traitement commencé', text1: 'Traitement en cours', text2: 'Veuillez ajouter une image', text3: 'La liste d\'images est vide', text4: 'Traitement terminé', text5: 'Échec du traitement, passage à la suite', text6: 'DÉMARRER', text7: 'ARRÊTER', text8: 'JOURNAL', text9: 'Échec du traitement', text10: 'Veuillez cliquer sur le journal pour voir le message d\'erreur', }, Final2xHome: { text0: 'Suppression réussie', text1: 'Cliquez ou faites glisser les images ou dossiers ici pour les téléverser', }, Final2xSettings: { text10: 'Périph.', text11: 'Modèle', text15: 'Échelle (num.)', text16: 'Par déf.', text17: 'Dossier de sortie', text18: 'Proxy', text19: 'Format', text20: 'Tile Process', }, } ================================================ FILE: src/renderer/src/locales/ja.ts ================================================ export const ja = { MyProgress: { text0: '処理を開始します', text1: '処理中です', text2: '画像を追加してください', text3: '画像リストは空です', text4: '処理が中断されました', text5: '処理に失敗しました。スキップします', text6: '開始', text7: '中止', text8: 'ログ', text9: '処理失敗', text10: 'エラーメッセージを確認するには、ログをクリックしてください', }, Final2xHome: { text0: '削除が成功しました', text1: '画像やフォルダをここにクリックまたはドラッグ&ドロップしてアップロードしてください', }, Final2xSettings: { text10: 'デバイス', text11: 'モデル', text15: 'Custom Scale', text16: 'Default', text17: '出力フォルダ', text18: 'プロキシ', text19: 'Format', text20: 'Tile Process', }, } ================================================ FILE: src/renderer/src/locales/zh.ts ================================================ export const zh = { MyProgress: { text0: '开始处理', text1: '处理中', text2: '请添加图片', text3: '图片列表为空', text4: '已终止处理', text5: '处理失败,跳过', text6: '开始', text7: '终止', text8: '日志', text9: '处理失败', text10: '请点击日志查看错误信息', }, Final2xHome: { text0: '移除成功', text1: '点击或拖拽图片或文件夹到此处上传', }, Final2xSettings: { text10: '设备', text11: '模型', text15: '自定义倍率', text16: '默认', text17: '输出文件夹', text18: '下载代理', text19: '保存格式', text20: '启用切块处理', }, } ================================================ FILE: src/renderer/src/main.ts ================================================ import { // create naive ui create, // component NButton, NCard, NDivider, NDrawer, NDrawerContent, NIcon, NImage, NInput, NInputNumber, NLog, NPopover, NProgress, NSelect, NSpace, NSwitch, NText, NUpload, NUploadDragger, } from 'naive-ui' import { createPinia } from 'pinia' import { createPersistedState } from 'pinia-plugin-persistedstate' import { createApp } from 'vue' import App from './App.vue' import i18n from './plugins/i18n' import router from './router' // 通用字体 import 'vfonts/OpenSans.css' const naive = create({ components: [ NButton, NDivider, NSpace, NIcon, NImage, NCard, NDrawer, NDrawerContent, NLog, NProgress, NText, NUpload, NUploadDragger, NInput, NInputNumber, NPopover, NSelect, NSwitch, ], }) const pinia = createPinia() pinia.use(createPersistedState({ storage: localStorage, })) createApp(App).use(naive).use(i18n).use(pinia).use(router).mount('#app') ================================================ FILE: src/renderer/src/plugins/i18n.ts ================================================ import { createI18n } from 'vue-i18n' import { en } from '../locales/en' import { fr } from '../locales/fr' import { ja } from '../locales/ja' import { zh } from '../locales/zh' // ----------------------------------------------------------------------------- // to add a new language, add the language file to the locales folder and add the language id to the LANG_LIST array // and "import { xx } from '../locales/xx'" at the top of this file // and add the language to the messages object below export const LANG_LIST: string[] = ['en', 'zh', 'ja', 'fr'] // ----------------------------------------------------------------------------- const i18n = createI18n({ legacy: false, fallbackLocale: 'en', globalInjection: true, // 全局注册$t方法 messages: { en, zh, ja, fr, }, }) export default i18n ================================================ FILE: src/renderer/src/public/index.html ================================================ <%= htmlWebpackPlugin.options.title %>
================================================ FILE: src/renderer/src/public/robots.txt ================================================ User-agent: * Disallow: ================================================ FILE: src/renderer/src/router/index.ts ================================================ import { createRouter, createWebHashHistory } from 'vue-router' import Final2xHome from '../views/Final2xHome.vue' import Final2xSettings from '../views/Final2xSettings.vue' export default createRouter({ history: createWebHashHistory(), routes: [ { path: '/', redirect: '/Final2xHome', }, { path: '/Final2xHome', name: 'Final2xHome', component: Final2xHome, }, { path: '/Final2xSettings', name: 'Final2xSettings', component: Final2xSettings, }, ], }) ================================================ FILE: src/renderer/src/store/SRSettingsStore.ts ================================================ import type { Ref } from 'vue' import { defineStore } from 'pinia' import { ref } from 'vue' export const useSRSettingsStore = defineStore( 'SRSettings', () => { const selectedSRModel = ref('RealESRGAN_RealESRGAN_x2plus_2x.pth') const selectedTorchDevice = ref('auto') const ghProxy: Ref = ref(null) const targetScale: Ref = ref(null) const useTile: Ref = ref(true) const saveFormat: Ref = ref('.png') return { selectedSRModel, selectedTorchDevice, ghProxy, targetScale, useTile, saveFormat, } }, { persist: true, }, ) ================================================ FILE: src/renderer/src/store/globalSettingsStore.ts ================================================ import type { LogInst } from 'naive-ui' import type { Ref } from 'vue' import type { NaiveDarkModeType } from '../components/NaiveDarkMode.vue' import { defineStore } from 'pinia' import { ref } from 'vue' export const useGlobalSettingsStore = defineStore( 'GlobalSettings', () => { const darkMode: Ref = ref('system') const globalcolor = ref('#fffafa') const naiveTheme: Ref = ref(undefined) const changeRoute = ref(false) const langsNum = ref(114514) const ProgressPercentage = ref(0) const CommandLOG = ref('') const logInstRef = ref(null) const StartCommandLock = ref(false) const SrSuccess = ref(false) const openOutputFolder = ref(true) return { darkMode, globalcolor, naiveTheme, changeRoute, langsNum, ProgressPercentage, CommandLOG, StartCommandLock, SrSuccess, logInstRef, openOutputFolder, } }, { persist: { pick: [ 'langsNum', 'darkMode', 'naiveTheme', 'globalcolor', 'openOutputFolder', ], }, }, ) ================================================ FILE: src/renderer/src/store/ioPathStore.ts ================================================ import type { UploadFileInfo } from 'naive-ui' import { defineStore } from 'pinia' import { ref } from 'vue' export const useIOPathStore = defineStore( 'IOPath', () => { const inputpathMap = ref>(new Map()) const inputFileList = ref([]) const outputpath = ref('') const outputpathLock = ref(false) return { inputpathMap, inputFileList, outputpath, outputpathLock, } }, { persist: { pick: ['outputpath', 'outputpathLock'], }, }, ) ================================================ FILE: src/renderer/src/utils/IOPath.ts ================================================ import { storeToRefs } from 'pinia' import { useIOPathStore } from '../store/ioPathStore' import PathFormat from '../utils/pathFormat' export default class IOPath { /** * @description Add a new inputpath to inputpathMap * @param id inputpath id * @param path inputpath */ static add(id: string, path: string): void { const { inputpathMap } = storeToRefs(useIOPathStore()) if (path !== '') { inputpathMap.value.set(id, path) } } /** * @description Delete an inputpath from inputpathMap by id * @param id inputpath id */ static delete(id: string): void { const { inputpathMap } = storeToRefs(useIOPathStore()) inputpathMap.value.delete(id) } /** * @description 检查 id 是否存在,因为 naive-ui 生成的 id 长度较短,所以这里只检查 inputpathMap 即可 * @param id inputpath id */ static checkID(id: string): boolean { const { inputpathMap } = storeToRefs(useIOPathStore()) return inputpathMap.value.get(id) !== undefined } /** * @description Get an inputpath from inputpathMap by id * @param id inputpath id */ static getByID(id: string): string { const { inputpathMap } = storeToRefs(useIOPathStore()) return inputpathMap.value.get(id) || '' } /** * @description Get all inputpath from inputpathMap * @returns inputpathMap with string */ static getAllPath(): string { const { inputpathMap } = storeToRefs(useIOPathStore()) // return inputpath key and value with string let inputpath = '' inputpathMap.value.forEach((value, key) => { inputpath += `${key} : ${value}\n` }) return inputpath } /** * @description Get all inputpath from inputpathMap * @returns inputpathMap string list */ static getList(): string[] { const { inputpathMap } = storeToRefs(useIOPathStore()) // return inputpath value with String List return Array.from(inputpathMap.value.values()) } /** * @description check inputpathMap is empty */ static isEmpty(): boolean { const { inputpathMap } = storeToRefs(useIOPathStore()) return inputpathMap.value.size === 0 } /** * @description Get all inputpath from inputpathMap * @returns inputpathMap with string */ static show(): string { const inputpathList = this.getList() console.log('inputpathList: ', inputpathList) let inputpathListString = '' for (const i in inputpathList) { inputpathListString += `${inputpathList[i]}\n` } return inputpathListString } /** * @description Set outputpath by manual, and lock outputpath */ static setoutputpathManual(path: string): void { const { outputpath, outputpathLock } = storeToRefs(useIOPathStore()) if (path !== '') { outputpath.value = path outputpathLock.value = true console.log('outputpath SET SUCCESS!') } } /** * @description Set outputpath if outputpathLock is false or outputpath is invalid */ static setoutputpath(path: string): void { const { outputpath, outputpathLock } = storeToRefs(useIOPathStore()) // if outputpathLock is false or outputpath is empty, set outputpath if (path !== '' && (outputpathLock.value === false || !PathFormat.checkPath(outputpath.value))) { outputpath.value = path } else { console.log('outputpath Lock!') } } /** * @description get outputpath */ static getoutputpath(): string { const { outputpath } = storeToRefs(useIOPathStore()) return outputpath.value } /** * @description clear all inputpath */ static clearALL(): void { const { inputpathMap, inputFileList } = storeToRefs(useIOPathStore()) inputpathMap.value.clear() inputFileList.value = [] } } ================================================ FILE: src/renderer/src/utils/SROptions.ts ================================================ import type { Ref } from 'vue' import { ref } from 'vue' export const torchDeviceList: Ref = ref([ { value: 'auto', label: 'Auto' }, { value: 'cuda', label: 'CUDA' }, { value: 'mps', label: 'MPS' }, { value: 'cpu', label: 'CPU' }, ]) export const saveFormatList: Ref = ref([ { value: '.png', label: 'PNG' }, { value: '.jpg', label: 'JPG' }, { value: '.webp', label: 'WebP' }, { value: '.tiff', label: 'TIFF' }, ]) ================================================ FILE: src/renderer/src/utils/getFinal2xCoreConfig.ts ================================================ import type { Final2xCoreConfig } from '@shared/type/core' import { useGlobalSettingsStore } from '@renderer/store/globalSettingsStore' import { storeToRefs } from 'pinia' import { useSRSettingsStore } from '../store/SRSettingsStore' import PathFormat from '../utils/pathFormat' import IOPath from './IOPath' /** * @description: 返回输出路径,如果输出路径不合法,则从第一个输入路径构造一个合法输出路径 */ function getOutPutPATH(): string { if (!PathFormat.checkPath(IOPath.getoutputpath())) { const inputPATHList = IOPath.getList() const pathFormat = new PathFormat() pathFormat.setRootPath(inputPATHList[0]) IOPath.setoutputpath(pathFormat.getRootPath()) } return IOPath.getoutputpath() } /** * @description: 返回最终的json字符串配置文件 */ export function getFinal2xCoreConfig(): Final2xCoreConfig { const { selectedSRModel, ghProxy, targetScale, selectedTorchDevice, useTile, saveFormat } = storeToRefs(useSRSettingsStore()) const { openOutputFolder } = storeToRefs(useGlobalSettingsStore()) const inputPATHList = IOPath.getList() const outputPATH = getOutPutPATH() let _gh_proxy: string | null if (ghProxy.value === '') { _gh_proxy = null } else { _gh_proxy = ghProxy.value } return { config: { pretrained_model_name: selectedSRModel.value, device: selectedTorchDevice.value, gh_proxy: _gh_proxy, target_scale: targetScale.value, output_path: outputPATH, input_path: inputPATHList, use_tile: useTile.value, save_format: saveFormat.value, }, options: { open_output_folder: openOutputFolder.value, }, } } ================================================ FILE: src/renderer/src/utils/index.ts ================================================ import { LANG_LIST } from '../plugins/i18n' class Utils { /** * @description 返回语言,和语言数量 * @param id 语言id 0-> en, 1-> zh, 2-> ja, 3-> fr */ static getLanguage(id: number): { lang: string, numLang: number } { const langs = LANG_LIST return { lang: langs[id], numLang: langs.length, } } /** * @description 等待一段时间 * @param timeout 等待时间,单位毫秒 */ static sleep(timeout: number): Promise { return new Promise(resolve => setTimeout(resolve, timeout)) } /** * @description Deep, Dark, Fantasy? 真·深度睡眠 * @param miliseconds 等待时间,单位毫秒 */ static DeepDeepSleep(miliseconds: number): void { const currentTime = new Date().getTime() while (currentTime + miliseconds >= new Date().getTime()) { /* empty */ } } /** * @description 生成超长随机字符串 */ static getRandString(): string { return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15) } /** * @description: 防抖函数装饰器,防止用户频繁点击 * @param fn 需要防抖的函数 * @param delay 防抖的时间间隔,默认500ms */ static clickDebounce( fn: (...args: any[]) => void, delay: number = 500, ): (...args: any[]) => void { let timer: NodeJS.Timeout | null = null let immediate = true return (...args: any[]) => { if (timer) { clearTimeout(timer) } if (immediate) { fn(...args) immediate = false } timer = setTimeout(() => { immediate = true }, delay) } } } export const { getLanguage, sleep, DeepDeepSleep, getRandString, clickDebounce } = Utils ================================================ FILE: src/renderer/src/utils/modelOptions.ts ================================================ /* prettier-ignore */ /* tslint:disable */ /* This file is automatically generated by Final2x-core */ /* Do not modify this file manually */ // ----------------------------------------------------------------------------- /** * @description: all SR models provided by ccrestoration */ export const modelOptions: any[] = [ { label: 'RealESRGAN_RealESRGAN_x4plus_4x', value: 'RealESRGAN_RealESRGAN_x4plus_4x.pth' }, { label: 'RealESRGAN_RealESRGAN_x4plus_anime_6B_4x', value: 'RealESRGAN_RealESRGAN_x4plus_anime_6B_4x.pth' }, { label: 'RealESRGAN_RealESRGAN_x2plus_2x', value: 'RealESRGAN_RealESRGAN_x2plus_2x.pth' }, { label: 'RealESRGAN_realesr_animevideov3_4x', value: 'RealESRGAN_realesr_animevideov3_4x.pth' }, { label: 'RealESRGAN_AnimeJaNai_HD_V3_Compact_2x', value: 'RealESRGAN_AnimeJaNai_HD_V3_Compact_2x.pth' }, { label: 'RealESRGAN_AniScale_2_Compact_2x', value: 'RealESRGAN_AniScale_2_Compact_2x.pth' }, { label: 'RealESRGAN_Ani4Kv2_Compact_2x', value: 'RealESRGAN_Ani4Kv2_Compact_2x.pth' }, { label: 'RealESRGAN_APISR_RRDB_GAN_generator_2x', value: 'RealESRGAN_APISR_RRDB_GAN_generator_2x.pth' }, { label: 'RealESRGAN_APISR_RRDB_GAN_generator_4x', value: 'RealESRGAN_APISR_RRDB_GAN_generator_4x.pth' }, { label: 'DAT_S_2x', value: 'DAT_S_2x.pth' }, { label: 'DAT_S_3x', value: 'DAT_S_3x.pth' }, { label: 'DAT_S_4x', value: 'DAT_S_4x.pth' }, { label: 'DAT_2x', value: 'DAT_2x.pth' }, { label: 'DAT_3x', value: 'DAT_3x.pth' }, { label: 'DAT_4x', value: 'DAT_4x.pth' }, { label: 'DAT_2_2x', value: 'DAT_2_2x.pth' }, { label: 'DAT_2_3x', value: 'DAT_2_3x.pth' }, { label: 'DAT_2_4x', value: 'DAT_2_4x.pth' }, { label: 'DAT_light_2x', value: 'DAT_light_2x.pth' }, { label: 'DAT_light_3x', value: 'DAT_light_3x.pth' }, { label: 'DAT_light_4x', value: 'DAT_light_4x.pth' }, { label: 'DAT_APISR_GAN_generator_4x', value: 'DAT_APISR_GAN_generator_4x.pth' }, { label: 'HAT_S_2x', value: 'HAT_S_2x.pth' }, { label: 'HAT_S_3x', value: 'HAT_S_3x.pth' }, { label: 'HAT_S_4x', value: 'HAT_S_4x.pth' }, { label: 'HAT_2x', value: 'HAT_2x.pth' }, { label: 'HAT_3x', value: 'HAT_3x.pth' }, { label: 'HAT_4x', value: 'HAT_4x.pth' }, { label: 'HAT_Real_GAN_sharper_4x', value: 'HAT_Real_GAN_sharper_4x.pth' }, { label: 'HAT_Real_GAN_4x', value: 'HAT_Real_GAN_4x.pth' }, { label: 'HAT_ImageNet_pretrain_2x', value: 'HAT_ImageNet_pretrain_2x.pth' }, { label: 'HAT_ImageNet_pretrain_3x', value: 'HAT_ImageNet_pretrain_3x.pth' }, { label: 'HAT_ImageNet_pretrain_4x', value: 'HAT_ImageNet_pretrain_4x.pth' }, { label: 'HAT_L_ImageNet_pretrain_2x', value: 'HAT_L_ImageNet_pretrain_2x.pth' }, { label: 'HAT_L_ImageNet_pretrain_3x', value: 'HAT_L_ImageNet_pretrain_3x.pth' }, { label: 'HAT_L_ImageNet_pretrain_4x', value: 'HAT_L_ImageNet_pretrain_4x.pth' }, { label: 'RealCUGAN_Conservative_2x', value: 'RealCUGAN_Conservative_2x.pth' }, { label: 'RealCUGAN_Denoise1x_2x', value: 'RealCUGAN_Denoise1x_2x.pth' }, { label: 'RealCUGAN_Denoise2x_2x', value: 'RealCUGAN_Denoise2x_2x.pth' }, { label: 'RealCUGAN_Denoise3x_2x', value: 'RealCUGAN_Denoise3x_2x.pth' }, { label: 'RealCUGAN_No_Denoise_2x', value: 'RealCUGAN_No_Denoise_2x.pth' }, { label: 'RealCUGAN_Conservative_3x', value: 'RealCUGAN_Conservative_3x.pth' }, { label: 'RealCUGAN_Denoise3x_3x', value: 'RealCUGAN_Denoise3x_3x.pth' }, { label: 'RealCUGAN_No_Denoise_3x', value: 'RealCUGAN_No_Denoise_3x.pth' }, { label: 'RealCUGAN_Conservative_4x', value: 'RealCUGAN_Conservative_4x.pth' }, { label: 'RealCUGAN_Denoise3x_4x', value: 'RealCUGAN_Denoise3x_4x.pth' }, { label: 'RealCUGAN_No_Denoise_4x', value: 'RealCUGAN_No_Denoise_4x.pth' }, { label: 'RealCUGAN_Pro_Conservative_2x', value: 'RealCUGAN_Pro_Conservative_2x.pth' }, { label: 'RealCUGAN_Pro_Denoise3x_2x', value: 'RealCUGAN_Pro_Denoise3x_2x.pth' }, { label: 'RealCUGAN_Pro_No_Denoise_2x', value: 'RealCUGAN_Pro_No_Denoise_2x.pth' }, { label: 'RealCUGAN_Pro_Conservative_3x', value: 'RealCUGAN_Pro_Conservative_3x.pth' }, { label: 'RealCUGAN_Pro_Denoise3x_3x', value: 'RealCUGAN_Pro_Denoise3x_3x.pth' }, { label: 'RealCUGAN_Pro_No_Denoise_3x', value: 'RealCUGAN_Pro_No_Denoise_3x.pth' }, { label: 'EDSR_Mx2_f64b16_DIV2K_official_2x', value: 'EDSR_Mx2_f64b16_DIV2K_official_2x.pth' }, { label: 'EDSR_Mx3_f64b16_DIV2K_official_3x', value: 'EDSR_Mx3_f64b16_DIV2K_official_3x.pth' }, { label: 'EDSR_Mx4_f64b16_DIV2K_official_4x', value: 'EDSR_Mx4_f64b16_DIV2K_official_4x.pth' }, { label: 'SwinIR_classicalSR_DF2K_s64w8_SwinIR_M_2x', value: 'SwinIR_classicalSR_DF2K_s64w8_SwinIR_M_2x.pth' }, { label: 'SwinIR_lightweightSR_DIV2K_s64w8_SwinIR_S_2x', value: 'SwinIR_lightweightSR_DIV2K_s64w8_SwinIR_S_2x.pth' }, { label: 'SwinIR_realSR_BSRGAN_DFOWMFC_s64w8_SwinIR_L_GAN_4x', value: 'SwinIR_realSR_BSRGAN_DFOWMFC_s64w8_SwinIR_L_GAN_4x.pth' }, { label: 'SwinIR_realSR_BSRGAN_DFO_s64w8_SwinIR_M_GAN_2x', value: 'SwinIR_realSR_BSRGAN_DFO_s64w8_SwinIR_M_GAN_2x.pth' }, { label: 'SwinIR_realSR_BSRGAN_DFO_s64w8_SwinIR_M_GAN_4x', value: 'SwinIR_realSR_BSRGAN_DFO_s64w8_SwinIR_M_GAN_4x.pth' }, { label: 'SwinIR_Bubble_AnimeScale_SwinIR_Small_v1_2x', value: 'SwinIR_Bubble_AnimeScale_SwinIR_Small_v1_2x.pth' }, { label: 'SCUNet_color_50_1x', value: 'SCUNet_color_50_1x.pth' }, { label: 'SCUNet_color_real_psnr_1x', value: 'SCUNet_color_real_psnr_1x.pth' }, { label: 'SCUNet_color_real_gan_1x', value: 'SCUNet_color_real_gan_1x.pth' }, { label: 'SRCNN_2x', value: 'SRCNN_2x.pth' }, { label: 'SRCNN_3x', value: 'SRCNN_3x.pth' }, { label: 'SRCNN_4x', value: 'SRCNN_4x.pth' }, ] ================================================ FILE: src/renderer/src/utils/pathFormat.ts ================================================ class PathFormat { private rootpath: string constructor() { this.rootpath = '' } /** * @description 设置本次上传的根目录 */ setRootPath(path: string): void { const segments = path.split(/[/\\]/) if (segments.length > 1) { segments.pop() this.rootpath = segments.join(path.startsWith('/') ? '/' : '\\') } } /** * @description 返回本次上传的根目录 */ getRootPath(): string { return this.rootpath } /** * @description 相对于本次上传的根目录,返回拼接后的真实路径 */ getNewPath(path: string): string { const segments = path.split(/[/\\]/) return this.rootpath + segments.join(this.rootpath.startsWith('/') ? '/' : '\\') } /** * @description 检查路径格式是否正确 */ static checkPath(path: string): boolean { return path.startsWith('/') || path.includes('\\') } /** * @description 返回文件名 */ static getFileName(path: string): string { const segments = path.split(/[/\\]/) return segments[segments.length - 1] } } export default PathFormat ================================================ FILE: src/renderer/src/utils/switchLanguage.ts ================================================ import { storeToRefs } from 'pinia' import { getLanguage } from '.' import { useGlobalSettingsStore } from '../store/globalSettingsStore' /** * @description 切换语言,第一次切换到中文 */ export function switchLanguage(): void { const { langsNum } = storeToRefs(useGlobalSettingsStore()) if (langsNum.value === 114514) { langsNum.value = 1 } else { langsNum.value = (langsNum.value + 1) % getLanguage(0).numLang } } ================================================ FILE: src/renderer/src/views/Final2xHome.vue ================================================ ================================================ FILE: src/renderer/src/views/Final2xSettings.vue ================================================ ================================================ FILE: src/shared/const/ipc.ts ================================================ /** * 渲染进程 → 主进程(invoke/handle) */ export enum IpcChannelInvoke { OPEN_DIRECTORY_DIALOG = 'ipc:open-directory-dialog', } /** * 渲染进程 → 主进程(send/on,单向) */ export enum IpcChannelSend { EXECUTE_COMMAND = 'ipc:send:execute-command', KILL_COMMAND = 'ipc:send:kill-command', MINIMIZE = 'ipc:send:minimize', MAXIMIZE = 'ipc:send:maximize', CLOSE = 'ipc:send:close', } /** * 主进程 → 渲染进程(send/on,主进程主动 emit) */ export enum IpcChannelOn { COMMAND_STDOUT = 'ipc:on:command-stdout', COMMAND_STDERR = 'ipc:on:command-stderr', COMMAND_CLOSE = 'ipc:on:command-close-code', } ================================================ FILE: src/shared/type/core.ts ================================================ export interface Final2xCoreConfig { config: { pretrained_model_name: string device: string gh_proxy: string | null target_scale: number | null output_path: string input_path: string[] use_tile: boolean save_format: string } options: { open_output_folder: boolean } } ================================================ FILE: test/node/getCorePath.test.ts ================================================ import { checkPipPackage } from '@main/getCorePath' import { describe, expect, it } from 'vitest' describe('getFinal2xCorePath', () => { it('checkPipPackage should return false when the pip package is not available', () => { expect(checkPipPackage()).toEqual(false) }) }) ================================================ FILE: test/web/IOPath.test.ts ================================================ import { useIOPathStore } from '@renderer/store/ioPathStore' import IOPath from '@renderer/utils/IOPath' import { createPinia, setActivePinia, storeToRefs } from 'pinia' import { beforeEach, describe, expect, it } from 'vitest' describe('ioPath', () => { beforeEach(() => { setActivePinia(createPinia()) }) it('test_IOPath', () => { const { outputpath } = storeToRefs(useIOPathStore()) // checkID expect(IOPath.checkID('114514')).toBe(false) // test inputpath IOPath.add('114514', 'test') // checkID expect(IOPath.checkID('114514')).toBe(true) expect(IOPath.getByID('114514')).toBe('test') IOPath.add('114514', 'test2') expect(IOPath.getByID('114514')).toBe('test2') expect(IOPath.getList()).toEqual(['test2']) expect(IOPath.getAllPath()).toEqual('114514 : test2\n') expect(IOPath.show()).toEqual('test2\n') IOPath.delete('114514') expect(IOPath.getByID('114514')).toBe('') expect(IOPath.isEmpty()).toBe(true) // test outputpath IOPath.setoutputpath('/test') expect(IOPath.getoutputpath()).toBe('/test') IOPath.setoutputpathManual('/test2') expect(IOPath.getoutputpath()).toBe('/test2') IOPath.setoutputpath('') expect(IOPath.getoutputpath()).toBe('/test2') outputpath.value = '' // 模拟用户手动清除outputpath IOPath.setoutputpath('/testWhenEmpty') expect(IOPath.getoutputpath()).toBe('/testWhenEmpty') IOPath.setoutputpathManual('/test2') // clear ALL IOPath.add('114514', 'test') IOPath.clearALL() expect(IOPath.getList()).toEqual([]) expect(IOPath.isEmpty()).toBe(true) expect(IOPath.getoutputpath()).toBe('/test2') }) }) ================================================ FILE: test/web/index.test.ts ================================================ import { clickDebounce, DeepDeepSleep, getRandString, sleep } from '@renderer/utils' import { describe, expect, it, vi } from 'vitest' describe('utils', () => { it('sleep', async () => { const start = new Date().getTime() await sleep(1010) const end = new Date().getTime() expect(end - start).toBeGreaterThanOrEqual(1000) }) it('deepDeepSleep', () => { const start = new Date().getTime() DeepDeepSleep(1010) const end = new Date().getTime() expect(end - start).toBeGreaterThanOrEqual(1000) }) it('getRandString', () => { expect(getRandString()) }) it('clickDebounce', async () => { const fn = (): void => console.log('click') // spy on fn to check if it's called const spy = vi.spyOn(console, 'log') // call fn 3 times const debouncedFn = clickDebounce(fn, 1000) debouncedFn() debouncedFn() debouncedFn() // check if fn is called only once expect(spy).toHaveBeenCalledTimes(1) // await new Promise((resolve) => setTimeout(resolve, 1000)); await sleep(1000) debouncedFn() expect(spy).toHaveBeenCalledTimes(2) }) }) ================================================ FILE: test/web/pathFormat.test.ts ================================================ import PathFormat from '@renderer/utils/pathFormat' import { describe, expect, it } from 'vitest' describe('pathFormat', () => { it('test_unix', () => { const pathFormat = new PathFormat() pathFormat.setRootPath('/Users/test/Downloads/unix') const check: Array = [pathFormat.getRootPath(), pathFormat.getNewPath('/unix/test.txt')] expect(check).toStrictEqual(['/Users/test/Downloads', '/Users/test/Downloads/unix/test.txt']) }) it('test_win', () => { const pathFormat = new PathFormat() pathFormat.setRootPath('C:\\Users\\test\\Downloads\\win') const check: Array = [pathFormat.getRootPath(), pathFormat.getNewPath('/win/test.txt')] expect(check).toStrictEqual([ 'C:\\Users\\test\\Downloads', 'C:\\Users\\test\\Downloads\\win\\test.txt', ]) }) it('check_path', () => { const check: Array = [ PathFormat.checkPath('/Users/test/Downloads/unix'), PathFormat.checkPath('C:\\Users\\test\\Downloads\\win'), PathFormat.checkPath('C:Users/test/Downloads/unix/test.txt'), PathFormat.checkPath('Users/test/Downloads/unix/test.txt'), ] expect(check).toStrictEqual([true, true, false, false]) }) it('get_file_name', () => { expect(PathFormat.getFileName('/Users/test/Downloads/unix/114514.txt')).toBe('114514.txt') expect(PathFormat.getFileName('C:\\Users\\test\\Downloads\\win\\genshin.png')).toBe( 'genshin.png', ) }) }) ================================================ FILE: test/web/switchLanguage.test.ts ================================================ import { useGlobalSettingsStore } from '@renderer/store/globalSettingsStore' import { getLanguage } from '@renderer/utils' import { switchLanguage } from '@renderer/utils/switchLanguage' import { createPinia, setActivePinia, storeToRefs } from 'pinia' import { beforeEach, describe, expect, it } from 'vitest' describe('switchLanguage', () => { beforeEach(() => { // 创建一个新 pinia,并使其处于激活状态 setActivePinia(createPinia()) }) it('test_switchLanguage', () => { const { langsNum } = storeToRefs(useGlobalSettingsStore()) switchLanguage() expect(langsNum.value).toBe(1) // 第一次后应该是 'zh' langsNum.value = 0 // 手动设置为 'en' // 断言语言切换是否正确 expect(langsNum.value).toBe(0) // 初始语言是 'en', 所以切换一次后应该是 'zh', 切换两次后应该是 'ja', 切换三次后应该是 'en' const numLang = getLanguage(0).numLang for (let i = 0; i < 30; i++) { expect(langsNum.value).toBe(i % numLang) switchLanguage() } }) }) ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "types": [ "vitest", "vitest/globals" ] }, "references": [{ "path": "./tsconfig.node.json" }, { "path": "./tsconfig.web.json" }], "files": [] } ================================================ FILE: tsconfig.node.json ================================================ { "extends": "@electron-toolkit/tsconfig/tsconfig.node.json", "compilerOptions": { "composite": true, "baseUrl": ".", "paths": { "@main/*": [ "src/main/*" ], "@shared/*": [ "src/shared/*" ] }, "types": ["electron-vite/node"] }, "include": [ "electron.vite.config.*", "src/main/**/*", "src/preload/**/*", "src/preload/*.d.ts", "src/shared/**/*", "test/node/**/*" ] } ================================================ FILE: tsconfig.web.json ================================================ { "extends": "@electron-toolkit/tsconfig/tsconfig.web.json", "compilerOptions": { "composite": true, "baseUrl": ".", "paths": { "@renderer/*": [ "src/renderer/src/*" ], "@shared/*": [ "src/shared/*" ] } }, "include": [ "src/renderer/src/env.d.ts", "src/renderer/src/locales/*.ts", "src/renderer/src/**/*", "src/renderer/src/**/*.vue", "src/preload/*.d.ts", "src/shared/**/*", "test/web/**/*" ] } ================================================ FILE: vitest.config.ts ================================================ import tsconfigPaths from 'vite-tsconfig-paths' import { defineConfig } from 'vitest/config' export default defineConfig({ test: { projects: [ { plugins: [tsconfigPaths()], test: { name: 'web', root: 'test/web', include: ['**/*.test.ts'], environment: 'jsdom', }, }, { plugins: [tsconfigPaths()], test: { name: 'node', root: 'test/node', include: ['**/*.test.ts'], environment: 'node', }, }, ], }, })