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
<div align="center">
<img src="./resources/icon.png" width="30%"/>
</div>



[](https://github.com/EutropicAI/Final2x/actions/workflows/CI-test.yml)
[](https://github.com/EutropicAI/Final2x/actions/workflows/CI-build.yml)
[](https://github.com/EutropicAI/Final2x/actions/workflows/Release.yml)


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
<div align=center>
<img width="40%" alt="image" src="https://github.com/user-attachments/assets/37f6d444-766b-4c28-b64a-018f78ae1f35" />
<img width="40%" alt="image" src="https://github.com/user-attachments/assets/c6a278c0-bf11-46a7-9dcc-e5fe97ccc71c" />
</div>
### 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~
<a href="https://star-history.com/#EutropicAI/Final2x&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=EutropicAI/Final2x&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=EutropicAI/Final2x&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=EutropicAI/Final2x&type=Date" />
</picture>
</a>
================================================
FILE: README_i18n/README_zh.md
================================================
# Final2x
================================================
FILE: build/entitlements.mac.plist
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<true/>
</dict>
</plist>
================================================
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<Array<string>> {
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<void> {
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<void> {
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<void>((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
================================================
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<title>Final2x</title>
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<!-- <meta-->
<!-- http-equiv="Content-Security-Policy"-->
<!-- content="default-src 'self' file: data:; img-src * 'self' data: https:; script-src 'self'; style-src 'self' 'unsafe-inline'"-->
<!-- />-->
</head>
<body>
<div id="app"></div>
<script type="module" src="./src/main.ts"></script>
</body>
</html>
================================================
FILE: src/renderer/src/App.vue
================================================
<script lang="ts" setup>
import { NConfigProvider, NDialogProvider, NGlobalStyle, NNotificationProvider } from 'naive-ui'
import { storeToRefs } from 'pinia'
import { onMounted, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { RouterView } from 'vue-router'
import BottomNavigation from './components/bottomNavigation.vue'
import MyDarkMode from './components/MyDarkMode.vue'
import MyProgress from './components/MyProgress.vue'
import TrafficLightsButtons from './components/TrafficLightsButtons.vue'
import { useGlobalSettingsStore } from './store/globalSettingsStore'
import { getLanguage } from './utils'
const { locale } = useI18n()
const { langsNum, naiveTheme, globalcolor } = storeToRefs(useGlobalSettingsStore())
watch(langsNum, () => {
// 切换语言
locale.value = getLanguage(langsNum.value).lang
console.log('locale: ', locale.value)
})
onMounted(async () => {
if (langsNum.value !== 114514) {
// 当语言不是跟随环境时,设置语言
locale.value = getLanguage(langsNum.value).lang
}
})
const themeOverrides = {
Select: {
peers: {
InternalSelectMenu: {
height: '200px',
},
},
},
}
</script>
<template>
<NConfigProvider :theme="naiveTheme" :theme-overrides="themeOverrides">
<NGlobalStyle />
<NNotificationProvider class="n-config-provider" placement="top">
<NDialogProvider>
<div class="background">
<MyDarkMode />
<TrafficLightsButtons />
<MyProgress />
<div class="view">
<RouterView v-slot="{ Component }">
<transition mode="out-in" name="custom-fade">
<keep-alive>
<component :is="Component" />
</keep-alive>
</transition>
</RouterView>
</div>
<BottomNavigation />
</div>
</NDialogProvider>
</NNotificationProvider>
</NConfigProvider>
</template>
<style lang="scss" scoped>
.custom-fade-enter-active {
transition: all 0.2s ease-out;
}
.custom-fade-leave-active {
transition: all 0.3s cubic-bezier(1, 0.5, 0.8, 1);
}
.custom-fade-enter-from,
.custom-fade-leave-to {
opacity: 0;
}
$global-color: v-bind(globalcolor);
$buttom-bottom: 8px;
::-webkit-scrollbar {
display: none;
}
.n-config-provider {
width: 100vw;
height: 100vh;
}
.background {
box-sizing: border-box;
width: 100%;
height: 100%;
background-color: $global-color;
transition: all 300ms ease-in-out;
//padding-top: 30px;
display: flex;
flex-direction: column;
.view {
overflow: scroll;
flex: 1;
}
}
.fade-enter-active {
transition: opacity 0.6s ease-in-out;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>
================================================
FILE: src/renderer/src/components/MyDarkMode.vue
================================================
<script lang="ts" setup>
import { storeToRefs } from 'pinia'
import { useGlobalSettingsStore } from '../store/globalSettingsStore'
import NaiveDarkMode from './NaiveDarkMode.vue'
const { darkMode, globalcolor, naiveTheme } = storeToRefs(useGlobalSettingsStore())
</script>
<template>
<div>
<NaiveDarkMode
v-model:color="globalcolor"
v-model:naivetheme="naiveTheme"
:dark-mode="darkMode"
design-dark="#101015"
design-light="#fffafa"
:fade-layer="0"
class="naive-dark-mode"
/>
</div>
</template>
<style lang="scss" scoped></style>
================================================
FILE: src/renderer/src/components/MyExternalLink.vue
================================================
<script lang="ts" setup>
import { FilmOutline } from '@vicons/ionicons5'
class openWebsite {
static async FinalRip(): Promise<void> {
window.open('https://github.com/EutropicAI/FinalRip', '_blank')
}
static async VSET(): Promise<void> {
window.open('https://github.com/EutropicAI/VSET', '_blank')
}
}
</script>
<template>
<div class="MyExternalLink">
<n-space>
<n-button style="font-size: 36px" text @click="openWebsite.VSET">
<n-icon>
<FilmOutline />
</n-icon>
</n-button>
</n-space>
</div>
</template>
<style lang="scss" scoped>
.custom-fade-enter-active {
transition: all 2s ease-out;
}
.custom-fade-leave-active {
transition: all 2s cubic-bezier(1, 0.5, 0.8, 1);
}
.custom-fade-enter-from,
.custom-fade-leave-to {
opacity: 0;
}
</style>
================================================
FILE: src/renderer/src/components/MyProgress.vue
================================================
<script lang="ts" setup>
import { IpcChannelOn, IpcChannelSend } from '@shared/const/ipc'
import { useDialog, useNotification } from 'naive-ui'
import { storeToRefs } from 'pinia'
import { nextTick, onMounted, ref, watchEffect } from 'vue'
import { useI18n } from 'vue-i18n'
import { useGlobalSettingsStore } from '../store/globalSettingsStore'
import { getFinal2xCoreConfig } from '../utils/getFinal2xCoreConfig'
import IOPath from '../utils/IOPath'
const { t } = useI18n()
const notification = useNotification()
const dialog = useDialog()
const {
CommandLOG,
logInstRef,
StartCommandLock,
SrSuccess,
ProgressPercentage,
} = storeToRefs(useGlobalSettingsStore())
const showLOG = ref(false)
onMounted(() => {
window.electron.ipcRenderer.on(
IpcChannelOn.COMMAND_STDOUT,
(_, data) => {
handleCommandLOG(data)
},
)
window.electron.ipcRenderer.on(
IpcChannelOn.COMMAND_STDERR,
(_, data) => {
handleCommandLOG(data)
},
)
window.electron.ipcRenderer.on(
IpcChannelOn.COMMAND_CLOSE,
(_, data) => {
handleCommandLOG(`CLOSE CODE:${data}`)
StartCommandLock.value = false
if (!SrSuccess.value) {
MyProgressDialogs.SrFailed()
}
else {
IOPath.clearALL()
}
},
)
watchEffect(() => {
if (CommandLOG.value) {
nextTick(() => {
logInstRef.value?.scrollTo({ position: 'bottom', silent: true })
})
}
})
})
function handleCommandLOG(log: string): void {
CommandLOG.value += log
const skipImageRegex = /______Skip_Image______:(.+)/
const processingRegex = /Processing------\[ ([\d.]+)% /
const srSuccessRegex = /______SR_COMPLETED______/
const skipImageMatch = log.match(skipImageRegex)
const processingMatch = log.match(processingRegex)
const srSuccessMatch = log.match(srSuccessRegex)
if (skipImageMatch) {
const imagePath = skipImageMatch[1]
MyProgressNotifications.SkipImage(imagePath)
}
if (processingMatch) {
ProgressPercentage.value = Number.parseFloat(processingMatch[1])
}
if (srSuccessMatch) {
SrSuccess.value = true
}
}
class MyProgressNotifications {
static StartSR(): void {
notification.success({
title: t('MyProgress.text0'),
duration: 1500,
})
}
static SRprocessing(): void {
notification.warning({
title: t('MyProgress.text1'),
duration: 1500,
})
}
static SRListEmpty(): void {
notification.warning({
title: t('MyProgress.text2'),
content: t('MyProgress.text3'),
duration: 1500,
keepAliveOnHover: true,
})
}
static TerminateSR(): void {
notification.error({
title: t('MyProgress.text4'),
duration: 1500,
keepAliveOnHover: true,
})
}
static SkipImage(imagePath: string): void {
notification.warning({
title: t('MyProgress.text5'),
content: imagePath,
duration: 2000,
keepAliveOnHover: true,
})
}
}
class MyProgressDialogs {
static SrFailed(): void {
dialog.error({
title: t('MyProgress.text9'),
content: t('MyProgress.text10'),
})
}
}
function StartSR(): void {
if (StartCommandLock.value) {
MyProgressNotifications.SRprocessing()
return
}
if (IOPath.isEmpty()) {
MyProgressNotifications.SRListEmpty()
return
}
StartCommandLock.value = true // START LOCK
SrSuccess.value = false // RESET SR SUCCESS
MyProgressNotifications.StartSR()
// get Final2x-core config
const final2xCoreConfig = getFinal2xCoreConfig()
CommandLOG.value += `\n${JSON.stringify(final2xCoreConfig)}\n`
window.electron.ipcRenderer.send(IpcChannelSend.EXECUTE_COMMAND, final2xCoreConfig)
}
function TerminateSR(): void {
window.electron.ipcRenderer.send(IpcChannelSend.KILL_COMMAND)
MyProgressNotifications.TerminateSR()
}
</script>
<template>
<div>
<div class="control">
<n-progress
:percentage="ProgressPercentage"
color="green"
:height="34"
indicator-placement="inside"
processing
type="line"
/>
<n-button round secondary strong type="success" @click="StartSR">
{{ t('MyProgress.text6') }}
</n-button>
<n-button round secondary strong type="error" @click="TerminateSR">
{{ t('MyProgress.text7') }}
</n-button>
<n-button round secondary strong type="warning" @click="showLOG = !showLOG">
{{ t('MyProgress.text8') }}
</n-button>
</div>
<n-drawer v-model:show="showLOG" height="385" placement="top">
<n-drawer-content :native-scrollbar="false" title="">
<br>
<n-card hoverable size="small" title="Log">
<n-log ref="logInstRef" :log="CommandLOG" trim />
</n-card>
</n-drawer-content>
</n-drawer>
<n-divider class="n-divider" />
</div>
</template>
<style lang="scss">
.control {
box-sizing: border-box;
width: 100%;
padding: 30px 40px 0 40px;
display: flex;
justify-content: space-between;
> div {
margin: 0 5px;
}
> button {
margin: 0 5px;
}
}
.progress {
margin-left: -30px;
margin-top: 10px;
}
.ButtonSpace {
margin-right: 40px;
margin-top: 30px;
}
.n-divider {
margin: 10px 0 0 0 !important;
}
</style>
================================================
FILE: src/renderer/src/components/MySetting.vue
================================================
<script lang="ts" setup>
import { HomeOutlined, SettingOutlined, TranslationOutlined } from '@vicons/antd'
import { ContrastSharp, MoonOutline, SunnyOutline } from '@vicons/ionicons5'
import { storeToRefs } from 'pinia'
import router from '../router'
import { useGlobalSettingsStore } from '../store/globalSettingsStore'
import { clickDebounce } from '../utils'
import { switchLanguage } from '../utils/switchLanguage'
const { darkMode, changeRoute } = storeToRefs(useGlobalSettingsStore())
function handleRoute(): void {
if (changeRoute.value === false) {
changeRoute.value = true
router.push('/Final2xSettings')
}
else {
changeRoute.value = false
router.push('/')
}
}
const handleDarkMode = clickDebounce((): void => {
// const darkmodeList : Array<NaiveDarkModeType> = ['system', 'light', 'dark']
if (darkMode.value === 'system') {
darkMode.value = 'light'
}
else if (darkMode.value === 'light') {
darkMode.value = 'dark'
}
else {
darkMode.value = 'system'
}
})
</script>
<template>
<div>
<n-space class="main-buttons">
<n-button style="font-size: 36px" text @click="handleRoute">
<n-icon>
<div v-if="changeRoute === false">
<SettingOutlined />
</div>
<div v-else>
<HomeOutlined />
</div>
</n-icon>
</n-button>
<n-button style="font-size: 36px" text @click="switchLanguage">
<n-icon>
<TranslationOutlined />
</n-icon>
</n-button>
<n-button style="font-size: 36px" text @click="handleDarkMode">
<n-icon>
<div v-if="darkMode === 'light'">
<SunnyOutline />
</div>
<div v-else-if="darkMode === 'dark'">
<MoonOutline />
</div>
<div v-else>
<ContrastSharp />
</div>
</n-icon>
</n-button>
</n-space>
</div>
</template>
<style lang="scss" scoped>
$buttom-bottom: 8px;
.main-buttons {
width: 180px;
}
</style>
================================================
FILE: src/renderer/src/components/NaiveDarkMode.vue
================================================
<script lang="ts" setup>
import type { PropType, Ref } from 'vue'
import { darkTheme, useOsTheme } from 'naive-ui'
import { nextTick, onBeforeMount, onMounted, ref, watch } from 'vue'
export type NaiveDarkModeType = undefined | 'light' | 'dark' | 'system'
// 不想用 CSS Transition,可以用 JS 实现捏
// -----------------------------------------------------------------------------
// Props and Emits
// -----------------------------------------------------------------------------
const props = defineProps({
darkMode: {
type: String as PropType<NaiveDarkModeType>,
default: () => 'system',
},
designDark: {
type: String,
default: () => '#000000',
},
designLight: {
type: String,
default: () => '#ffffff',
},
fadeLayer: {
type: Number,
default: () => 25,
},
color: {
type: String,
default: () => '#ffffff',
},
naivetheme: {
type: Object,
default: () => undefined,
},
})
const emits = defineEmits(['update:color', 'update:naivetheme'])
// -----------------------------------------------------------------------------
// Refs
// -----------------------------------------------------------------------------
const osThemeRef = useOsTheme()
const DarkMode: Ref<NaiveDarkModeType> = ref(undefined)
const globalcolor = ref('')
const DarkTheme: Ref<boolean | undefined> = ref(undefined)
const DesignDarkColor = ref('#000000')
const DesignLightColor = ref('#ffffff')
const FadeLayer = ref(25)
// v-model 传入的 color
watch(
() => globalcolor.value,
(value) => {
emits('update:color', value)
},
)
// v-model 传入的 naivetheme
watch(
() => DarkTheme.value,
(value) => {
emits('update:naivetheme', value ? darkTheme : undefined)
},
)
onBeforeMount(() => {
// 传入
DarkMode.value = props.darkMode
DesignDarkColor.value = props.designDark
DesignLightColor.value = props.designLight
// set designLightColor to globalcolor, update:color
globalcolor.value = props.designLight
FadeLayer.value = props.fadeLayer
// console.log('onBeforeMount DarkMode.value', DarkMode.value)
})
// 监听 props.darkMode 的变化
watch(
() => props.darkMode,
(value) => {
DarkMode.value = value
},
)
// 监听 props.designDark 的变化
watch(
() => props.designDark,
(value) => {
if (globalcolor.value === DesignDarkColor.value) {
globalcolor.value = value
}
DesignDarkColor.value = value
},
)
// 监听 props.designLight 的变化
watch(
() => props.designLight,
(value) => {
if (globalcolor.value === DesignLightColor.value) {
globalcolor.value = value
}
DesignLightColor.value = value
},
)
// 监听 props.fadeLayer 的变化
watch(
() => props.fadeLayer,
(value) => {
FadeLayer.value = value
},
)
/**
* @description update DarkTheme.value and update:naivetheme
* @param mode 'dark' or 'light' or 'system'
*/
function handleDarkModeChange(mode: NaiveDarkModeType): void {
// console.log('handleDarkModeChange DarkTheme.value', DarkTheme.value)
// console.log('handleDarkModeChange mode', mode)
if (mode === 'system' || mode === undefined) {
DarkTheme.value = osThemeRef.value === 'dark'
}
else {
DarkTheme.value = mode === 'dark'
}
}
onMounted(() => {
// console.log('onMounted DarkMode.value', DarkMode.value)
handleDarkModeChange(DarkMode.value)
})
watch(DarkMode, (value) => {
// console.log('watch DarkMode ', value)
handleDarkModeChange(value)
})
// 检测系统主题,修改 DarkTheme.value
watch(osThemeRef, (value) => {
// console.log('watch osThemeRef', value)
if (DarkMode.value === 'system' || DarkMode.value === undefined) {
DarkTheme.value = value === 'dark'
}
})
// 检测 DarkTheme.value,修改 CSS 样式
watch(DarkTheme, (value) => {
// console.log('watch DarkTheme ', value)
if (value) {
if (isCSSLight()) {
switchCSSStyle('dark')
}
}
else {
if (isCSSDark()) {
switchCSSStyle('light')
}
}
})
// -----------------------------------------------------------------------------
// Functions
// -----------------------------------------------------------------------------
/**
* @description Interpolate two colors by a given factor
*/
function interpolateColor(color1: string, color2: string, factor: number): string {
if (factor === 0)
return color1
if (factor === 1)
return color2
const c1 = hexToRgb(color1)
const c2 = hexToRgb(color2)
const r = Math.round(interpolate(c1.r, c2.r, factor))
const g = Math.round(interpolate(c1.g, c2.g, factor))
const b = Math.round(interpolate(c1.b, c2.b, factor))
return `rgb(${r}, ${g}, ${b})`
}
function interpolate(start: number, end: number, factor: number): number {
return start + (end - start) * factor
}
function hexToRgb(hex: string): { r: number, g: number, b: number } {
const r = Number.parseInt(hex.slice(1, 3), 16)
const g = Number.parseInt(hex.slice(3, 5), 16)
const b = Number.parseInt(hex.slice(5, 7), 16)
return { r, g, b }
}
/**
* @description Smooth transition to dark mode
* @param mode 'dark' or 'light' or 'system'
*/
function switchCSSStyle(mode: NaiveDarkModeType): void {
if (mode === 'system') {
const osThemeRef = useOsTheme()
mode = osThemeRef.value === 'dark' ? 'dark' : 'light'
}
const targetColor = mode === 'dark' ? DesignDarkColor.value : DesignLightColor.value
const initialColor = mode === 'dark' ? DesignLightColor.value : DesignDarkColor.value
const layer = Math.ceil(FadeLayer.value)
if (layer < 1) {
globalcolor.value = targetColor
return
}
for (let i = 1; i <= layer; i++) {
setTimeout(() => {
nextTick(() => {
globalcolor.value = interpolateColor(initialColor, targetColor, i / layer)
})
}, layer * i)
}
}
/**
* @description Check if the current CSS theme is dark
*/
function isCSSDark(): boolean {
return globalcolor.value === DesignDarkColor.value
}
/**
* @description Check if the current CSS theme is light
*/
function isCSSLight(): boolean {
return globalcolor.value === DesignLightColor.value
}
</script>
<template>
<div />
</template>
<style lang="scss"></style>
================================================
FILE: src/renderer/src/components/TrafficLightsButtons.vue
================================================
<script setup lang="ts">
import { IpcChannelSend } from '@shared/const/ipc'
import { onMounted, onUnmounted, ref } from 'vue'
const isFocus = ref(true)
function handleFocus(): void {
isFocus.value = true
}
function handleBlur(): void {
isFocus.value = false
}
onMounted(() => {
window.addEventListener('focus', handleFocus)
window.addEventListener('blur', handleBlur)
})
onUnmounted(() => {
window.removeEventListener('focus', handleFocus)
window.removeEventListener('blur', handleBlur)
})
function handleClose(): void {
window.electron.ipcRenderer.send(IpcChannelSend.CLOSE)
}
function handleMinimize(): void {
window.electron.ipcRenderer.send(IpcChannelSend.MINIMIZE)
}
function handleMaximize(): void {
window.electron.ipcRenderer.send(IpcChannelSend.MAXIMIZE)
}
</script>
<template>
<div class="container">
<div class="drag-area" />
<div v-if="!isFocus">
<div class="example">
<div class="traffic-lights">
<button
id="close"
class="traffic-light traffic-light-close"
@click="handleClose"
/>
<button
id="minimize"
class="traffic-light traffic-light-minimize"
@click="handleMinimize"
/>
<button
id="maximize"
class="traffic-light traffic-light-maximize"
@click="handleMaximize"
/>
</div>
</div>
</div>
<div v-else>
<div class="example focus">
<div class="traffic-lights">
<button
id="close"
class="traffic-light traffic-light-close"
@click="handleClose"
/>
<button
id="minimize"
class="traffic-light traffic-light-minimize"
@click="handleMinimize"
/>
<button
id="maximize"
class="traffic-light traffic-light-maximize"
@click="handleMaximize"
/>
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
$close-red: #ff6159;
$close-red-active: #bf4942;
$close-red-icon: #4d0000;
$close-red-icon-active: #190000;
$minimize-yellow: #ffbd2e;
$minimize-yellow-active: #bf8e22;
$minimize-yellow-icon: #995700;
$minimize-yellow-icon-active: #592800;
$maximize-green: #28c941;
$maximize-green-active: #1d9730;
$maximize-green-icon: #006500;
$maximize-green-icon-active: #003200;
$disabled-gray: #ddd;
.traffic-lights {
// position: absolute;
top: 1px;
left: 8px;
.focus &,
&:hover,
&:active {
> .traffic-light-close {
background-color: $close-red;
&:active:hover {
background-color: $close-red-active;
}
}
> .traffic-light-minimize {
background-color: $minimize-yellow;
&:active:hover {
background-color: $minimize-yellow-active;
}
}
> .traffic-light-maximize {
background-color: $maximize-green;
&:active:hover {
background-color: $maximize-green-active;
}
}
}
> .traffic-light {
&:before,
&:after {
visibility: hidden;
}
}
&:hover,
&:active {
> .traffic-light {
&:before,
&:after {
visibility: visible;
}
}
}
}
.traffic-light {
border-radius: 100%;
padding: 0;
height: 12px;
width: 12px;
border: 1px solid rgba(0, 0, 0, 0.06);
box-sizing: border-box;
margin-right: 3.5px;
background-color: $disabled-gray;
position: relative;
outline: none;
&:before,
&:after {
content: '';
position: absolute;
border-radius: 1px;
left: 0;
top: 0;
right: 0;
bottom: 0;
margin: auto;
}
&-close {
&:before,
&:after {
background-color: $close-red-icon;
width: 8px;
height: 1px;
}
&:before {
transform: rotate(45deg); // translate(-0.5px, -0.5px);
}
&:after {
transform: rotate(-45deg); // translate(0.5px, -0.5px);
}
&:active:hover:before,
&:active:hover:after {
background-color: $close-red-icon-active;
}
}
&-minimize {
&:before {
background-color: $minimize-yellow-icon;
width: 8px;
height: 1px;
//transform: translateY(-0.5px);
}
&:active:hover:before {
background-color: $minimize-yellow-icon-active;
}
}
&-maximize {
&:before {
background-color: $maximize-green-icon;
width: 6px;
height: 6px;
}
&:after {
background-color: $maximize-green;
width: 10px;
height: 2px;
transform: rotate(45deg);
}
&:active:hover:before {
background-color: $maximize-green-icon-active;
}
&:active:hover:after {
background-color: $maximize-green-active;
}
}
}
// Example Styles
body {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
font-weight: 100;
}
h1,
h2 {
font-weight: 100;
}
h2 {
margin: 0 0 10px;
}
.example {
margin: 0 0 30px;
}
.container {
position: fixed;
height: 30px;
width: 50px;
left: 10px;
}
.drag-area {
-webkit-app-region: drag;
position: fixed;
height: 30px;
width: 100%;
left: 60px;
}
</style>
================================================
FILE: src/renderer/src/components/bottomNavigation.vue
================================================
<script lang="ts" setup>
import MyExternalLink from './MyExternalLink.vue'
import MySetting from './MySetting.vue'
</script>
<template>
<div class="position">
<n-divider class="n-divider" />
<n-space class="n-space" justify="space-between">
<MySetting />
<MyExternalLink />
</n-space>
</div>
</template>
<style lang="scss" scoped>
.position {
box-sizing: border-box;
width: 100%;
height: 60px;
justify-content: flex-end;
}
.n-space {
box-sizing: border-box;
margin: 10px 20px 0 20px;
}
.n-divider {
margin: 0 !important;
}
</style>
================================================
FILE: src/renderer/src/env.d.ts
================================================
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<object, object, any>
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
================================================
<!doctype html>
<html lang="">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<link rel="icon" href="./favicon.ico" />
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong
>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without
JavaScript enabled. Please enable it to continue.</strong
>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
================================================
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<string | null> = ref(null)
const targetScale: Ref<number | null> = ref(null)
const useTile: Ref<boolean> = ref(true)
const saveFormat: Ref<string> = 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<NaiveDarkModeType> = ref('system')
const globalcolor = ref('#fffafa')
const naiveTheme: Ref<any> = ref(undefined)
const changeRoute = ref(false)
const langsNum = ref(114514)
const ProgressPercentage = ref(0)
const CommandLOG = ref('')
const logInstRef = ref<LogInst | null>(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<Map<string, string>>(new Map())
const inputFileList = ref<UploadFileInfo[]>([])
const outputpath = ref<string>('')
const outputpathLock = ref<boolean>(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<any[]> = ref([
{ value: 'auto', label: 'Auto' },
{ value: 'cuda', label: 'CUDA' },
{ value: 'mps', label: 'MPS' },
{ value: 'cpu', label: 'CPU' },
])
export const saveFormatList: Ref<any[]> = 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<void> {
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
================================================
<script lang="ts" setup>
import type { UploadFileInfo } from 'naive-ui'
import { IpcChannelInvoke } from '@shared/const/ipc'
import { FileImageOutlined } from '@vicons/antd'
import { useNotification } from 'naive-ui'
import { storeToRefs } from 'pinia'
import { onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { useIOPathStore } from '../store/ioPathStore'
import { getRandString } from '../utils'
import IOPath from '../utils/IOPath'
import PathFormat from '../utils/pathFormat'
const { t } = useI18n()
const notification = useNotification()
const useIOPath = useIOPathStore()
const { inputFileList } = storeToRefs(useIOPath)
const pathFormat = new PathFormat()
class Final2xHomeNotifications {
static handleremove(s: string): void {
notification.success({
title: t('Final2xHome.text0'),
content: s,
duration: 1000,
})
}
}
function handleClickUpload(): void {
const handleSelected = (_, path): void => {
if (path !== undefined) {
path.forEach((p: string) => {
// 生成随机id
let pathid = getRandString()
while (IOPath.checkID(pathid)) {
pathid = getRandString()
}
// console.log(pathid)
// 插入 inputpathMap
IOPath.add(pathid, p)
// 插入 inputFileList
inputFileList.value.push({
fullPath: p,
id: pathid,
name: PathFormat.getFileName(p),
percentage: 0,
status: 'pending',
thumbnailUrl: null,
type: 'image',
url: null,
})
})
}
}
window.electron.ipcRenderer.invoke(IpcChannelInvoke.OPEN_DIRECTORY_DIALOG, ['openFile', 'multiSelections'])
.then((path) => {
handleSelected(null, path)
})
.catch((error) => {
console.error('Error selecting file:', error)
})
}
onMounted(() => {
const dragWrapper = document.getElementById('file_drag')
dragWrapper?.addEventListener('drop', (e) => {
// 阻止默认行为
e.preventDefault()
// 获取文件列表
const files = e.dataTransfer?.files
if (files && files.length > 0) {
const path = files[0].path // Get file path, the path is absolute path, electron can use directly
console.log(path)
pathFormat.setRootPath(path)
console.log(pathFormat.getRootPath())
IOPath.setoutputpath(pathFormat.getRootPath())
}
})
// 阻止拖拽结束事件默认行为
dragWrapper?.addEventListener('dragover', (e) => {
e.preventDefault()
})
})
function handleUploadChange(data: { fileList: UploadFileInfo[] }): void {
// console.log(data.fileList)
// console.log(inputFileList.value)
inputFileList.value = data.fileList
}
function handleBeforeUpload(options: { file: UploadFileInfo }): UploadFileInfo {
// console.log(pathFormat.getNewPath(options.file.fullPath))
IOPath.add(options.file.id, pathFormat.getNewPath(String(options.file.fullPath)))
return options.file
}
function handleRemove(options: { file: UploadFileInfo, fileList: Array<UploadFileInfo> }): boolean {
// console.log(ioPATH.show())
// console.log(options.file.id)
Final2xHomeNotifications.handleremove(IOPath.getByID(options.file.id))
IOPath.delete(options.file.id)
return true
}
</script>
<template>
<div id="file_drag" class="for_file_drag" @click.prevent>
<n-upload
v-model:file-list="inputFileList"
multiple
directory-dnd
class="n-upload"
@remove="handleRemove"
@before-upload="handleBeforeUpload"
@change="handleUploadChange"
>
<n-upload-dragger class="file-drag-zone" @click="handleClickUpload">
<div class="file-drag-zone-logo-text">
<div style="margin-bottom: 12px">
<n-icon size="48" depth="3.0">
<FileImageOutlined />
</n-icon>
</div>
<n-text style="font-size: 16px">
{{ t('Final2xHome.text1') }}
</n-text>
</div>
</n-upload-dragger>
</n-upload>
</div>
</template>
<style lang="scss" scoped>
.for_file_drag {
width: 100%;
height: 100%;
padding: 0 12%;
box-sizing: border-box;
overflow: scroll;
overflow-x: hidden;
display: flex;
flex-direction: column;
justify-content: center;
.file-drag-zone-logo-text {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
}
.n-upload {
display: flex;
flex-direction: column;
}
.n-upload :deep .n-upload-file-list {
max-height: calc(100vh - 370px);
overflow-y: auto;
overflow-x: hidden;
&::-webkit-scrollbar {
width: 6px;
height: 6px;
}
&::-webkit-scrollbar-track {
border-radius: 3px;
background: rgba(0, 0, 0, 0.06);
-webkit-box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.08);
}
&::-webkit-scrollbar-thumb {
border-radius: 3px;
background: rgba(0, 0, 0, 0.12);
-webkit-box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.2);
}
}
</style>
================================================
FILE: src/renderer/src/views/Final2xSettings.vue
================================================
<script lang="ts" setup>
import { IpcChannelInvoke } from '@shared/const/ipc'
import { storeToRefs } from 'pinia'
import { useI18n } from 'vue-i18n'
import { useGlobalSettingsStore } from '../store/globalSettingsStore'
import { useIOPathStore } from '../store/ioPathStore'
import { useSRSettingsStore } from '../store/SRSettingsStore'
import IOPath from '../utils/IOPath'
import { modelOptions } from '../utils/modelOptions'
import { saveFormatList, torchDeviceList } from '../utils/SROptions'
const { openOutputFolder }
= storeToRefs(useGlobalSettingsStore())
const { selectedSRModel, ghProxy, targetScale, selectedTorchDevice, useTile, saveFormat } = storeToRefs(useSRSettingsStore())
const { outputpath } = storeToRefs(useIOPathStore())
const { t } = useI18n()
function getPath(): void {
const handleSelected = (_, path): void => {
if (path[0] !== undefined) {
// console.log(ioPath.getoutputpath())
IOPath.setoutputpathManual(path[0])
}
}
window.electron.ipcRenderer.invoke(IpcChannelInvoke.OPEN_DIRECTORY_DIALOG, ['openDirectory'])
.then((path) => {
handleSelected(null, path)
})
.catch((error) => {
console.error('Error selecting directory:', error)
})
}
</script>
<template>
<n-card :bordered="false" class="settings-card">
<n-space class="vertical" vertical justify="center">
<n-space>
<n-button dashed type="success" style="width: 80px">
{{ t('Final2xSettings.text11') }}
</n-button>
<n-select
v-model:value="selectedSRModel"
:options="modelOptions"
filterable
tag
clearable
style="width: 465px"
/>
</n-space>
<n-space>
<n-button dashed type="success" style="width: 80px">
{{ t('Final2xSettings.text10') }}
</n-button>
<n-select
v-model:value="selectedTorchDevice"
:options="torchDeviceList"
style="width: 150px"
/>
<n-button dashed type="success" style="width: 120px">
{{ t('Final2xSettings.text15') }}
</n-button>
<n-input-number
v-model:value="targetScale"
:max="99999999"
:min="0"
:step="0.2"
:placeholder="t('Final2xSettings.text16')"
style="width: 171px"
/>
</n-space>
<n-space>
<n-button dashed type="success" style="width: 80px">
{{ t('Final2xSettings.text19') }}
</n-button>
<n-select
v-model:value="saveFormat"
:options="saveFormatList"
style="width: 150px"
/>
<n-button dashed type="success" style="width: 120px">
{{ t('Final2xSettings.text20') }}
</n-button>
<n-switch v-model:value="useTile" size="large" style="height: 35px; width: 76px">
<template #checked>
ON
</template>
<template #unchecked>
OFF
</template>
</n-switch>
</n-space>
<n-space>
<n-button dashed type="success" style="width: 80px">
{{ t('Final2xSettings.text18') }}
</n-button>
<n-input
v-model:value="ghProxy"
placeholder="Github Proxy, Example: https://github.abskoop.workers.dev/"
style="width: 465px"
/>
</n-space>
<n-space>
<n-button round type="success" style="height: 35px; width: 150px" @click="getPath">
{{ t('Final2xSettings.text17') }}
</n-button>
<n-switch v-model:value="openOutputFolder" size="large" style="height: 35px; width: 76px">
<template #checked>
OPEN
</template>
</n-switch>
<n-input v-model:value="outputpath" :placeholder="outputpath" round style="width: 308px" />
</n-space>
</n-space>
</n-card>
</template>
<style lang="scss" scoped>
.settings-card {
width: fit-content;
margin: 0 auto;
height: 100%;
// transparent
background-color: rgba(255, 255, 255, 0);
.vertical {
height: 100%;
> div {
margin-bottom: 20px;
}
}
}
</style>
================================================
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<string> = [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<string> = [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<boolean> = [
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',
},
},
],
},
})
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
SYMBOL INDEX (48 symbols across 14 files)
FILE: resources/download-core.js
constant PLATFORM (line 20) | const PLATFORM = process.env.PLATFORM || process.platform
constant ARCH (line 22) | const ARCH = process.env.ARCH || process.arch
function downloadAndUnzip (line 29) | async function downloadAndUnzip(url, targetPath) {
function downloadAndUnzipCore (line 58) | async function downloadAndUnzipCore(platform) {
FILE: src/main/getCorePath.ts
constant FINAL2X_CORE_NAME (line 5) | const FINAL2X_CORE_NAME = 'Final2x-core'
constant FINAL2X_CORE_PATH (line 6) | const FINAL2X_CORE_PATH = 'Final2x-core/Final2x-core'
function getCorePath (line 14) | function getCorePath(): string {
function checkPipPackage (line 28) | function checkPipPackage(): boolean {
FILE: src/main/index.ts
function createWindow (line 10) | function createWindow(): void {
function setTray (line 84) | function setTray(): void {
FILE: src/main/openDirectory.ts
function openDirectory (line 8) | async function openDirectory(_, p: Array<'openFile' | 'openDirectory' | ...
FILE: src/main/runCommand.ts
function runCommand (line 12) | async function runCommand(event: IpcMainEvent, coreConfig: Final2xCoreCo...
function killCommand (line 44) | async function killCommand(): Promise<void> {
FILE: src/preload/index.d.ts
type Window (line 4) | interface Window {
FILE: src/renderer/src/plugins/i18n.ts
constant LANG_LIST (line 11) | const LANG_LIST: string[] = ['en', 'zh', 'ja', 'fr']
FILE: src/renderer/src/utils/IOPath.ts
class IOPath (line 5) | class IOPath {
method add (line 11) | static add(id: string, path: string): void {
method delete (line 22) | static delete(id: string): void {
method checkID (line 31) | static checkID(id: string): boolean {
method getByID (line 40) | static getByID(id: string): string {
method getAllPath (line 49) | static getAllPath(): string {
method getList (line 63) | static getList(): string[] {
method isEmpty (line 72) | static isEmpty(): boolean {
method show (line 81) | static show(): string {
method setoutputpathManual (line 94) | static setoutputpathManual(path: string): void {
method setoutputpath (line 106) | static setoutputpath(path: string): void {
method getoutputpath (line 120) | static getoutputpath(): string {
method clearALL (line 128) | static clearALL(): void {
FILE: src/renderer/src/utils/getFinal2xCoreConfig.ts
function getOutPutPATH (line 11) | function getOutPutPATH(): string {
function getFinal2xCoreConfig (line 24) | function getFinal2xCoreConfig(): Final2xCoreConfig {
FILE: src/renderer/src/utils/index.ts
class Utils (line 3) | class Utils {
method getLanguage (line 8) | static getLanguage(id: number): { lang: string, numLang: number } {
method sleep (line 20) | static sleep(timeout: number): Promise<void> {
method DeepDeepSleep (line 28) | static DeepDeepSleep(miliseconds: number): void {
method getRandString (line 38) | static getRandString(): string {
method clickDebounce (line 47) | static clickDebounce(
FILE: src/renderer/src/utils/pathFormat.ts
class PathFormat (line 1) | class PathFormat {
method constructor (line 4) | constructor() {
method setRootPath (line 11) | setRootPath(path: string): void {
method getRootPath (line 22) | getRootPath(): string {
method getNewPath (line 29) | getNewPath(path: string): string {
method checkPath (line 37) | static checkPath(path: string): boolean {
method getFileName (line 44) | static getFileName(path: string): string {
FILE: src/renderer/src/utils/switchLanguage.ts
function switchLanguage (line 8) | function switchLanguage(): void {
FILE: src/shared/const/ipc.ts
type IpcChannelInvoke (line 4) | enum IpcChannelInvoke {
type IpcChannelSend (line 11) | enum IpcChannelSend {
type IpcChannelOn (line 22) | enum IpcChannelOn {
FILE: src/shared/type/core.ts
type Final2xCoreConfig (line 1) | interface Final2xCoreConfig {
Condensed preview — 68 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (112K chars).
[
{
"path": ".github/ISSUE_TEMPLATE/bug.yaml",
"chars": 2422,
"preview": "name: 🐛 Bug report | 错误报告 | BUG報告\ndescription: Create a bug report to help us improve | 创建bug报告以帮助我们改进 | 改善を支援するためのレポートを"
},
{
"path": ".github/ISSUE_TEMPLATE/config.yml",
"chars": 28,
"preview": "blank_issues_enabled: false\n"
},
{
"path": ".github/ISSUE_TEMPLATE/feature.yaml",
"chars": 1353,
"preview": "name: 🚀 Feature request | 功能请求 | フィーチャーリクエスト\ndescription: Suggest an idea for this project | 为项目提供一个创意建议 | このプロジェクトにアイデア"
},
{
"path": ".github/workflows/CI-build.yml",
"chars": 4196,
"preview": "name: CI-build\n\non:\n push:\n branches:\n - main\n paths-ignore:\n - '**.md'\n - LICENSE\n pull_request:"
},
{
"path": ".github/workflows/CI-test.yml",
"chars": 779,
"preview": "name: CI-test\n\non:\n push:\n branches:\n - main\n paths-ignore:\n - '**.md'\n - LICENSE\n pull_request:\n"
},
{
"path": ".github/workflows/Release.yml",
"chars": 4978,
"preview": "name: Release\n\non:\n workflow_dispatch:\n push:\n tags:\n - 'v*'\n\njobs:\n windows:\n strategy:\n matrix:\n "
},
{
"path": ".github/workflows/issue-helper.yml",
"chars": 1420,
"preview": "name: issue-helper\n\non:\n issues:\n types: [opened, reopened, edited]\n\njobs:\n check-inactive:\n runs-on: ubuntu-lat"
},
{
"path": ".github/workflows/issue-translator.yml",
"chars": 328,
"preview": "name: 'issue-translator'\non:\n issue_comment:\n types: [created]\n issues:\n types: [opened]\n\njobs:\n translate:\n "
},
{
"path": ".gitignore",
"chars": 93,
"preview": "node_modules\ndist\nout\n*.log*\n*.DS_Store\n/resources/Final2x-core/\n/outputs/\n/.idea\n/coverage/\n"
},
{
"path": ".npmrc",
"chars": 21,
"preview": "shamefully-hoist=true"
},
{
"path": "LICENSE",
"chars": 1495,
"preview": "BSD 3-Clause License\n\nCopyright (c) 2023, Tohrusky\n\nRedistribution and use in source and binary forms, with or without\nm"
},
{
"path": "README.md",
"chars": 3435,
"preview": "# Final2x\n\n<div align=\"center\">\n<img src=\"./resources/icon.png\" width=\"30%\"/>\n</div>\n\n => {\n const { notarize } = require('@electron/notarize')\n\n if (process.platform !== '"
},
{
"path": "electron-builder.yml",
"chars": 1569,
"preview": "appId: com.final2x.app\nproductName: Final2x\ndirectories:\n buildResources: build\n\nicon: resources/icon.png\n\nfiles:\n - '"
},
{
"path": "electron.vite.config.ts",
"chars": 593,
"preview": "import { resolve } from 'node:path'\nimport vue from '@vitejs/plugin-vue'\nimport { defineConfig, externalizeDepsPlugin } "
},
{
"path": "eslint.config.js",
"chars": 1738,
"preview": "import antfu from '@antfu/eslint-config'\n\nexport default antfu(\n {\n ignores: [\n 'dist',\n 'out',\n 'nod"
},
{
"path": "package.json",
"chars": 2636,
"preview": "{\n \"name\": \"Final2x\",\n \"productName\": \"Final2x\",\n \"version\": \"4.0.0\",\n \"description\": \"A cross-platform image super-"
},
{
"path": "resources/download-core.js",
"chars": 2908,
"preview": "// download Final2x-core from https://github.com/EutropicAI/Final2x-core/releases\n// and put it in resources folder\n\ncon"
},
{
"path": "src/main/getCorePath.ts",
"chars": 876,
"preview": "import { spawnSync } from 'node:child_process'\nimport path from 'node:path'\nimport { app } from 'electron'\n\nconst FINAL2"
},
{
"path": "src/main/index.ts",
"chars": 4576,
"preview": "import { join } from 'node:path'\nimport { electronApp, is, optimizer } from '@electron-toolkit/utils'\nimport { IpcChanne"
},
{
"path": "src/main/openDirectory.ts",
"chars": 559,
"preview": "import { dialog } from 'electron'\n\n/**\n * @description Open a directory or file/multiple files\n * @param _ Unused parame"
},
{
"path": "src/main/runCommand.ts",
"chars": 1917,
"preview": "import type { Final2xCoreConfig } from '@shared/type/core'\nimport type { IpcMainEvent } from 'electron'\nimport type { Ch"
},
{
"path": "src/preload/index.d.ts",
"chars": 149,
"preview": "import type { ElectronAPI } from '@electron-toolkit/preload'\n\ndeclare global {\n interface Window {\n electron: Electr"
},
{
"path": "src/preload/index.ts",
"chars": 613,
"preview": "import { electronAPI } from '@electron-toolkit/preload'\nimport { contextBridge } from 'electron'\n\n// Custom APIs for ren"
},
{
"path": "src/renderer/index.html",
"chars": 510,
"preview": "<!doctype html>\n<html>\n <head>\n <meta charset=\"UTF-8\" />\n <title>Final2x</title>\n <!-- https://developer.mozil"
},
{
"path": "src/renderer/src/App.vue",
"chars": 2696,
"preview": "<script lang=\"ts\" setup>\nimport { NConfigProvider, NDialogProvider, NGlobalStyle, NNotificationProvider } from 'naive-ui"
},
{
"path": "src/renderer/src/components/MyDarkMode.vue",
"chars": 586,
"preview": "<script lang=\"ts\" setup>\nimport { storeToRefs } from 'pinia'\nimport { useGlobalSettingsStore } from '../store/globalSett"
},
{
"path": "src/renderer/src/components/MyExternalLink.vue",
"chars": 819,
"preview": "<script lang=\"ts\" setup>\nimport { FilmOutline } from '@vicons/ionicons5'\n\nclass openWebsite {\n static async FinalRip():"
},
{
"path": "src/renderer/src/components/MyProgress.vue",
"chars": 5239,
"preview": "<script lang=\"ts\" setup>\nimport { IpcChannelOn, IpcChannelSend } from '@shared/const/ipc'\nimport { useDialog, useNotific"
},
{
"path": "src/renderer/src/components/MySetting.vue",
"chars": 2029,
"preview": "<script lang=\"ts\" setup>\nimport { HomeOutlined, SettingOutlined, TranslationOutlined } from '@vicons/antd'\nimport { Cont"
},
{
"path": "src/renderer/src/components/NaiveDarkMode.vue",
"chars": 6051,
"preview": "<script lang=\"ts\" setup>\nimport type { PropType, Ref } from 'vue'\nimport { darkTheme, useOsTheme } from 'naive-ui'\nimpor"
},
{
"path": "src/renderer/src/components/TrafficLightsButtons.vue",
"chars": 5146,
"preview": "<script setup lang=\"ts\">\nimport { IpcChannelSend } from '@shared/const/ipc'\nimport { onMounted, onUnmounted, ref } from "
},
{
"path": "src/renderer/src/components/bottomNavigation.vue",
"chars": 578,
"preview": "<script lang=\"ts\" setup>\nimport MyExternalLink from './MyExternalLink.vue'\nimport MySetting from './MySetting.vue'\n</scr"
},
{
"path": "src/renderer/src/env.d.ts",
"chars": 195,
"preview": "/// <reference types=\"vite/client\" />\n\ndeclare module '*.vue' {\n import type { DefineComponent } from 'vue'\n\n const co"
},
{
"path": "src/renderer/src/locales/en.ts",
"chars": 763,
"preview": "export const en = {\n MyProgress: {\n text0: 'Processing started',\n text1: 'Processing in progress',\n text2: 'Pl"
},
{
"path": "src/renderer/src/locales/fr.ts",
"chars": 832,
"preview": "export const fr = {\n MyProgress: {\n text0: 'Traitement commencé',\n text1: 'Traitement en cours',\n text2: 'Veui"
},
{
"path": "src/renderer/src/locales/ja.ts",
"chars": 618,
"preview": "export const ja = {\n MyProgress: {\n text0: '処理を開始します',\n text1: '処理中です',\n text2: '画像を追加してください',\n text3: '画像リ"
},
{
"path": "src/renderer/src/locales/zh.ts",
"chars": 516,
"preview": "export const zh = {\n MyProgress: {\n text0: '开始处理',\n text1: '处理中',\n text2: '请添加图片',\n text3: '图片列表为空',\n te"
},
{
"path": "src/renderer/src/main.ts",
"chars": 1004,
"preview": "import {\n // create naive ui\n create,\n // component\n NButton,\n NCard,\n NDivider,\n NDrawer,\n NDrawerContent,\n NI"
},
{
"path": "src/renderer/src/plugins/i18n.ts",
"chars": 819,
"preview": "import { createI18n } from 'vue-i18n'\nimport { en } from '../locales/en'\nimport { fr } from '../locales/fr'\nimport { ja "
},
{
"path": "src/renderer/src/public/index.html",
"chars": 630,
"preview": "<!doctype html>\n<html lang=\"\">\n <head>\n <meta charset=\"utf-8\" />\n <meta http-equiv=\"X-UA-Compatible\" content=\"IE="
},
{
"path": "src/renderer/src/public/robots.txt",
"chars": 24,
"preview": "User-agent: *\nDisallow:\n"
},
{
"path": "src/renderer/src/router/index.ts",
"chars": 530,
"preview": "import { createRouter, createWebHashHistory } from 'vue-router'\nimport Final2xHome from '../views/Final2xHome.vue'\nimpor"
},
{
"path": "src/renderer/src/store/SRSettingsStore.ts",
"chars": 650,
"preview": "import type { Ref } from 'vue'\nimport { defineStore } from 'pinia'\nimport { ref } from 'vue'\n\nexport const useSRSettings"
},
{
"path": "src/renderer/src/store/globalSettingsStore.ts",
"chars": 1145,
"preview": "import type { LogInst } from 'naive-ui'\nimport type { Ref } from 'vue'\nimport type { NaiveDarkModeType } from '../compon"
},
{
"path": "src/renderer/src/store/ioPathStore.ts",
"chars": 560,
"preview": "import type { UploadFileInfo } from 'naive-ui'\nimport { defineStore } from 'pinia'\nimport { ref } from 'vue'\n\nexport con"
},
{
"path": "src/renderer/src/utils/IOPath.ts",
"chars": 3708,
"preview": "import { storeToRefs } from 'pinia'\nimport { useIOPathStore } from '../store/ioPathStore'\nimport PathFormat from '../uti"
},
{
"path": "src/renderer/src/utils/SROptions.ts",
"chars": 447,
"preview": "import type { Ref } from 'vue'\n\nimport { ref } from 'vue'\n\nexport const torchDeviceList: Ref<any[]> = ref([\n { value: '"
},
{
"path": "src/renderer/src/utils/getFinal2xCoreConfig.ts",
"chars": 1590,
"preview": "import type { Final2xCoreConfig } from '@shared/type/core'\nimport { useGlobalSettingsStore } from '@renderer/store/globa"
},
{
"path": "src/renderer/src/utils/index.ts",
"chars": 1607,
"preview": "import { LANG_LIST } from '../plugins/i18n'\n\nclass Utils {\n /**\n * @description 返回语言,和语言数量\n * @param id 语言id 0-> en"
},
{
"path": "src/renderer/src/utils/modelOptions.ts",
"chars": 5597,
"preview": "/* prettier-ignore */\n/* tslint:disable */\n/* This file is automatically generated by Final2x-core */\n/* Do not modify t"
},
{
"path": "src/renderer/src/utils/pathFormat.ts",
"chars": 1004,
"preview": "class PathFormat {\n private rootpath: string\n\n constructor() {\n this.rootpath = ''\n }\n\n /**\n * @description 设置本"
},
{
"path": "src/renderer/src/utils/switchLanguage.ts",
"chars": 423,
"preview": "import { storeToRefs } from 'pinia'\nimport { getLanguage } from '.'\nimport { useGlobalSettingsStore } from '../store/glo"
},
{
"path": "src/renderer/src/views/Final2xHome.vue",
"chars": 4877,
"preview": "<script lang=\"ts\" setup>\nimport type { UploadFileInfo } from 'naive-ui'\nimport { IpcChannelInvoke } from '@shared/const/"
},
{
"path": "src/renderer/src/views/Final2xSettings.vue",
"chars": 4133,
"preview": "<script lang=\"ts\" setup>\nimport { IpcChannelInvoke } from '@shared/const/ipc'\nimport { storeToRefs } from 'pinia'\nimport"
},
{
"path": "src/shared/const/ipc.ts",
"chars": 584,
"preview": "/**\n * 渲染进程 → 主进程(invoke/handle)\n */\nexport enum IpcChannelInvoke {\n OPEN_DIRECTORY_DIALOG = 'ipc:open-directory-dialog"
},
{
"path": "src/shared/type/core.ts",
"chars": 312,
"preview": "export interface Final2xCoreConfig {\n config: {\n pretrained_model_name: string\n device: string\n gh_proxy: stri"
},
{
"path": "test/node/getCorePath.test.ts",
"chars": 281,
"preview": "import { checkPipPackage } from '@main/getCorePath'\nimport { describe, expect, it } from 'vitest'\n\ndescribe('getFinal2xC"
},
{
"path": "test/web/IOPath.test.ts",
"chars": 1669,
"preview": "import { useIOPathStore } from '@renderer/store/ioPathStore'\nimport IOPath from '@renderer/utils/IOPath'\nimport { create"
},
{
"path": "test/web/index.test.ts",
"chars": 1126,
"preview": "import { clickDebounce, DeepDeepSleep, getRandString, sleep } from '@renderer/utils'\nimport { describe, expect, it, vi }"
},
{
"path": "test/web/pathFormat.test.ts",
"chars": 1458,
"preview": "import PathFormat from '@renderer/utils/pathFormat'\nimport { describe, expect, it } from 'vitest'\n\ndescribe('pathFormat'"
},
{
"path": "test/web/switchLanguage.test.ts",
"chars": 920,
"preview": "import { useGlobalSettingsStore } from '@renderer/store/globalSettingsStore'\nimport { getLanguage } from '@renderer/util"
},
{
"path": "tsconfig.json",
"chars": 195,
"preview": "{\n \"compilerOptions\": {\n \"types\": [\n \"vitest\",\n \"vitest/globals\"\n ]\n },\n \"references\": [{ \"path\": \"./"
},
{
"path": "tsconfig.node.json",
"chars": 460,
"preview": "{\n \"extends\": \"@electron-toolkit/tsconfig/tsconfig.node.json\",\n \"compilerOptions\": {\n \"composite\": true,\n \"baseU"
},
{
"path": "tsconfig.web.json",
"chars": 490,
"preview": "{\n \"extends\": \"@electron-toolkit/tsconfig/tsconfig.web.json\",\n \"compilerOptions\": {\n \"composite\": true,\n \"baseUr"
},
{
"path": "vitest.config.ts",
"chars": 566,
"preview": "import tsconfigPaths from 'vite-tsconfig-paths'\nimport { defineConfig } from 'vitest/config'\n\nexport default defineConfi"
}
]
About this extraction
This page contains the full source code of the EutropicAI/Final2x GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 68 files (101.0 KB), approximately 31.3k tokens, and a symbol index with 48 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.