Full Code of EutropicAI/Final2x for AI

main 0b590138d0bb cached
68 files
101.0 KB
31.3k tokens
48 symbols
1 requests
Download .txt
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>

![MacOS](https://img.shields.io/badge/Support-MacOS-blue?logo=Apple&style=flat-square)
![Windows](https://img.shields.io/badge/Support-Windows-blue?logo=Windows&style=flat-square)
![Linux](https://img.shields.io/badge/Support-Linux-blue?logo=Linux&style=flat-square)
[![CI-test](https://github.com/EutropicAI/Final2x/actions/workflows/CI-test.yml/badge.svg)](https://github.com/EutropicAI/Final2x/actions/workflows/CI-test.yml)
[![CI-build](https://github.com/EutropicAI/Final2x/actions/workflows/CI-build.yml/badge.svg)](https://github.com/EutropicAI/Final2x/actions/workflows/CI-build.yml)
[![Release](https://github.com/EutropicAI/Final2x/actions/workflows/Release.yml/badge.svg)](https://github.com/EutropicAI/Final2x/actions/workflows/Release.yml)
![Download](https://img.shields.io/github/downloads/EutropicAI/Final2x/total)
![GitHub](https://img.shields.io/github/license/EutropicAI/Final2x)

A cross-platform image super-resolution tool.

- News🎉: Final2x v4.0.0 is now available! It uses the [cccv](https://github.com/EutropicAI/cccv) backend, supporting custom models and more. See [custom model demo](https://github.com/EutropicAI/cccv_demo_remote_model).
- News🎉: Final2x v3.0.0 is now available, support Nvidia 50 series GPUs now!

### Screenshots

<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',
        },
      },
    ],
  },
})
Download .txt
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
Download .txt
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![MacOS](https://img.shields.io/ba"
  },
  {
    "path": "README_i18n/README_zh.md",
    "chars": 10,
    "preview": "# Final2x\n"
  },
  {
    "path": "build/entitlements.mac.plist",
    "chars": 415,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "build/notarize.js",
    "chars": 901,
    "preview": "module.exports = async (context) => {\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.

Copied to clipboard!