The response has been limited to 50k tokens of the smallest files in the repo. You can remove this limitation by removing the max tokens filter.
├── .github
    ├── ISSUE_TEMPLATE
    │   └── bug_report.md
    └── workflows
    │   ├── ci-build.yaml
    │   ├── codeql.yml
    │   ├── manual-build-console.yml
    │   ├── manual-build.yml
    │   ├── release-notify.yml
    │   └── update-game-data.yml
├── .gitignore
├── LICENSE
├── api
    ├── __init__.py
    ├── cheats_api.py
    ├── common_api.py
    ├── common_response.py
    ├── ryujinx_api.py
    ├── save_manager_api.py
    ├── suyu_api.py
    ├── updater_api.py
    └── yuzu_api.py
├── build_tools
    ├── upx
    │   ├── COPYING
    │   ├── LICENSE
    │   ├── NEWS
    │   ├── README
    │   ├── THANKS.txt
    │   ├── upx-doc.html
    │   ├── upx-doc.txt
    │   ├── upx.1
    │   └── upx.exe
    └── zip_files.py
├── changelog.md
├── config.py
├── doc
    ├── assets
    │   └── yuzu_user_id.jpg
    └── dev.md
├── exception
    ├── common_exception.py
    ├── download_exception.py
    └── install_exception.py
├── frontend
    ├── .browserslistrc
    ├── .editorconfig
    ├── .env.development
    ├── .env.production
    ├── .eslintrc-auto-import.json
    ├── .gitignore
    ├── README.md
    ├── bun.lock
    ├── env.d.ts
    ├── eslint.config.js
    ├── index.html
    ├── package.json
    ├── public
    │   └── favicon.ico
    ├── src
    │   ├── App.vue
    │   ├── assets
    │   │   ├── icon.png
    │   │   ├── ryujinx.webp
    │   │   ├── suyu.png
    │   │   ├── telegram.webp
    │   │   ├── telegram_black.webp
    │   │   └── yuzu.webp
    │   ├── auto-imports.d.ts
    │   ├── components.d.ts
    │   ├── components
    │   │   ├── ChangeLogDialog.vue
    │   │   ├── ConsoleDialog.vue
    │   │   ├── DialogTitle.vue
    │   │   ├── FaqGroup.vue
    │   │   ├── MarkdownContentBox.vue
    │   │   ├── NewVersionDialog.vue
    │   │   ├── OtherLinkItem.vue
    │   │   ├── README.md
    │   │   ├── SimplePage.vue
    │   │   ├── SpeedDial.vue
    │   │   ├── YuzuSaveCommonPart.vue
    │   │   └── YuzuSaveRestoreTab.vue
    │   ├── layouts
    │   │   ├── AppBar.vue
    │   │   ├── AppDrawer.vue
    │   │   ├── README.md
    │   │   ├── View.vue
    │   │   └── default.vue
    │   ├── main.ts
    │   ├── pages
    │   │   ├── about.vue
    │   │   ├── faq.vue
    │   │   ├── index.vue
    │   │   ├── keys.vue
    │   │   ├── otherLinks.vue
    │   │   ├── ryujinx.vue
    │   │   ├── settings.vue
    │   │   ├── suyu.vue
    │   │   ├── yuzu.vue
    │   │   ├── yuzuCheatsManagement.vue
    │   │   └── yuzuSaveManagement.vue
    │   ├── plugins
    │   │   ├── README.md
    │   │   ├── index.ts
    │   │   ├── mitt.ts
    │   │   └── vuetify.ts
    │   ├── router
    │   │   └── index.ts
    │   ├── stores
    │   │   ├── ConfigStore.ts
    │   │   ├── ConsoleDialogStore.ts
    │   │   ├── README.md
    │   │   ├── YuzuSaveStore.ts
    │   │   ├── app.ts
    │   │   └── index.ts
    │   ├── styles
    │   │   ├── README.md
    │   │   └── settings.scss
    │   ├── typed-router.d.ts
    │   ├── types
    │   │   ├── DefaultConfig.ts
    │   │   └── index.ts
    │   └── utils
    │   │   ├── common.ts
    │   │   └── markdown.ts
    ├── tsconfig.app.json
    ├── tsconfig.json
    ├── tsconfig.node.json
    └── vite.config.mts
├── game_data.json
├── hooks
    └── hook-api.py
├── main.py
├── main_devnull.py
├── module
    ├── aria2c.exe
    ├── cheats.py
    ├── common.py
    ├── dialogs.py
    ├── downloader.py
    ├── external
    │   └── bat_scripts.py
    ├── firmware.py
    ├── hosts.py
    ├── msg_notifier.py
    ├── network.py
    ├── nsz_wrapper.py
    ├── ryujinx.py
    ├── save_manager.py
    ├── sentry.py
    ├── suyu.py
    ├── updater.py
    └── yuzu.py
├── package.bat
├── package_all.bat
├── pyproject.toml
├── readme.md
├── repository
    ├── domain
    │   └── release_info.py
    ├── my_info.py
    ├── ryujinx.py
    ├── suyu.py
    └── yuzu.py
├── requirements.txt
├── send_release_notify.py
├── storage.py
├── ui.py
├── ui_webview.py
├── update_game_data.py
├── update_requirements.bat
├── utils
    ├── admin.py
    ├── common.py
    ├── doh.py
    ├── hardware.py
    ├── package.py
    ├── string_util.py
    └── webview2.py
└── uv.lock


/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
 1 | ---
 2 | name: Bug report
 3 | about: Create a report to help us improve
 4 | title: ''
 5 | labels: ''
 6 | assignees: ''
 7 | 
 8 | ---
 9 | 
10 | 请在描述问题的同时上传 ns-emu-tools.log 这个文件。
11 | 如果问题与 aria2 下载有关,请将 aria2.log 这个文件也带上。
12 | (将文件拖拽至输入框内即可)
13 | 


--------------------------------------------------------------------------------
/.github/workflows/ci-build.yaml:
--------------------------------------------------------------------------------
 1 | name: CI build
 2 | 
 3 | on:
 4 |   workflow_dispatch:
 5 |   push:
 6 |     branches:
 7 |       - main
 8 |     tags:
 9 |       - '*'
10 | 
11 | jobs:
12 |   build:
13 |     runs-on: ${{ matrix.os }}
14 |     strategy:
15 |       fail-fast: false
16 |       matrix:
17 |         os: ['windows-latest']
18 | 
19 |     steps:
20 |     - uses: actions/checkout@v4
21 |     - uses: actions/setup-python@v5
22 |       with:
23 |         python-version: '3.12'
24 |     - uses: actions/setup-node@v4
25 |       with:
26 |         node-version: 20
27 |     - uses: oven-sh/setup-bun@v2
28 |     - run: |
29 |         cd .\frontend
30 |         bun install
31 |         bun run build
32 |     - run: |
33 |         pip install uv
34 |         uv sync
35 |         .\package_all.bat
36 | #        pip install pywebview
37 | #        pyinstaller --noconfirm --onefile --console --icon "./web/favicon.ico" --add-data "./module/*.exe;./module/" --add-data "./web;web/" --additional-hooks-dir=".\\hooks" "./ui_webview.py" --name "NsEmuTools-webview"
38 |     - uses: actions/upload-artifact@v4
39 |       with:
40 |         path: dist/NsEmuTools.exe
41 |         name: NsEmuTools
42 |     - uses: actions/upload-artifact@v4
43 |       with:
44 |         path: dist/NsEmuTools-console.exe
45 |         name: NsEmuTools-console
46 |     - uses: actions/upload-artifact@v4
47 |       with:
48 |         path: dist/NsEmuTools
49 |         name: NsEmuTools-dir
50 |     - name: Release
51 |       uses: softprops/action-gh-release@v2.2.2
52 |       if: startsWith(github.ref, 'refs/tags/')
53 |       with:
54 |         draft: true
55 |         files: |
56 |           dist/*.exe
57 |           dist/*.7z
58 | 


--------------------------------------------------------------------------------
/.github/workflows/codeql.yml:
--------------------------------------------------------------------------------
 1 | # For most projects, this workflow file will not need changing; you simply need
 2 | # to commit it to your repository.
 3 | #
 4 | # You may wish to alter this file to override the set of languages analyzed,
 5 | # or to provide custom queries or build logic.
 6 | #
 7 | # ******** NOTE ********
 8 | # We have attempted to detect the languages in your repository. Please check
 9 | # the `language` matrix defined below to confirm you have the correct set of
10 | # supported CodeQL languages.
11 | #
12 | name: "CodeQL"
13 | 
14 | on:
15 |   push:
16 |     branches: [ "main" ]
17 |   pull_request:
18 |     # The branches below must be a subset of the branches above
19 |     branches: [ "main" ]
20 |   schedule:
21 |     - cron: '29 21 * * 0'
22 | 
23 | jobs:
24 |   analyze:
25 |     name: Analyze
26 |     runs-on: ubuntu-latest
27 |     permissions:
28 |       actions: read
29 |       contents: read
30 |       security-events: write
31 | 
32 |     strategy:
33 |       fail-fast: false
34 |       matrix:
35 |         language: [ 'javascript', 'python' ]
36 |         # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
37 |         # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
38 | 
39 |     steps:
40 |     - name: Checkout repository
41 |       uses: actions/checkout@v3
42 | 
43 |     # Initializes the CodeQL tools for scanning.
44 |     - name: Initialize CodeQL
45 |       uses: github/codeql-action/init@v2
46 |       with:
47 |         languages: ${{ matrix.language }}
48 |         # If you wish to specify custom queries, you can do so here or in a config file.
49 |         # By default, queries listed here will override any specified in a config file.
50 |         # Prefix the list here with "+" to use these queries and those in the config file.
51 | 
52 |         # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
53 |         # queries: security-extended,security-and-quality
54 | 
55 | 
56 |     # Autobuild attempts to build any compiled languages  (C/C++, C#, Go, or Java).
57 |     # If this step fails, then you should remove it and run the build manually (see below)
58 |     - name: Autobuild
59 |       uses: github/codeql-action/autobuild@v2
60 | 
61 |     # ℹ️ Command-line programs to run using the OS shell.
62 |     # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
63 | 
64 |     #   If the Autobuild fails above, remove it and uncomment the following three lines.
65 |     #   modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
66 | 
67 |     # - run: |
68 |     #   echo "Run, Build Application using script"
69 |     #   ./location_of_script_within_repo/buildscript.sh
70 | 
71 |     - name: Perform CodeQL Analysis
72 |       uses: github/codeql-action/analyze@v2
73 |       with:
74 |         category: "/language:${{matrix.language}}"
75 | 


--------------------------------------------------------------------------------
/.github/workflows/manual-build-console.yml:
--------------------------------------------------------------------------------
 1 | name: Manual build console
 2 | 
 3 | on:
 4 |   workflow_dispatch:
 5 | 
 6 | jobs:
 7 |   build:
 8 |     runs-on: ${{ matrix.os }}
 9 |     strategy:
10 |       fail-fast: false
11 |       matrix:
12 |         os: ['windows-latest']
13 | 
14 |     steps:
15 |     - uses: actions/checkout@v4
16 |     - uses: actions/setup-python@v5
17 |       with:
18 |         python-version: '3.12'
19 |     - uses: actions/setup-node@v4
20 |       with:
21 |         node-version: 20
22 |     - uses: oven-sh/setup-bun@v2
23 |     - run: |
24 |         cd .\frontend
25 |         bun install
26 |         bun run build
27 |     - run: |
28 |         pip install uv
29 |         uv sync
30 |         uv run pyinstaller --noconfirm --onefile --console --icon "./web/favicon.ico" --add-data "./module/*.exe;./module/" --add-data "./web;web/"  "./main.py" --additional-hooks-dir=".\\hooks" --name "NsEmuTools-console"
31 |     - uses: actions/upload-artifact@v4
32 |       with:
33 |         path: dist/*
34 |         name: NsEmuTools-console
35 | 


--------------------------------------------------------------------------------
/.github/workflows/manual-build.yml:
--------------------------------------------------------------------------------
 1 | name: Manual build
 2 | 
 3 | on:
 4 |   workflow_dispatch:
 5 | 
 6 | jobs:
 7 |   build:
 8 |     runs-on: ${{ matrix.os }}
 9 |     strategy:
10 |       fail-fast: false
11 |       matrix:
12 |         os: ['windows-latest']
13 | 
14 |     steps:
15 |     - uses: actions/checkout@v4
16 |     - uses: actions/setup-python@v5
17 |       with:
18 |         python-version: '3.12'
19 |     - uses: actions/setup-node@v4
20 |       with:
21 |         node-version: 20
22 |     - uses: oven-sh/setup-bun@v2
23 |     - run: |
24 |         cd .\frontend
25 |         bun install
26 |         bun run build
27 |     - run: |
28 |         pip install uv
29 |         uv sync
30 |         uv run pyinstaller --noconfirm --onefile --windowed --icon "./web/favicon.ico" --add-data "./module/*.exe;./module/" --add-data "./web;web/" --additional-hooks-dir=".\\hooks" "./main_devnull.py" --name "NsEmuTools"
31 |     - uses: actions/upload-artifact@v4
32 |       with:
33 |         path: dist/*
34 |         name: NsEmuTools
35 | 


--------------------------------------------------------------------------------
/.github/workflows/release-notify.yml:
--------------------------------------------------------------------------------
 1 | name: Release notify
 2 | 
 3 | on:
 4 |   workflow_dispatch:
 5 |   release:
 6 |     types: [released]
 7 | 
 8 | jobs:
 9 |   build:
10 | 
11 |     runs-on: ubuntu-latest
12 | 
13 |     steps:
14 |       - uses: actions/checkout@v3
15 |       - uses: actions/setup-python@v4
16 |         with:
17 |           python-version: '3.10'
18 |       - name: Install dependencies
19 |         run: pip install requests
20 |       - name: run send_release_notify.py
21 |         env:
22 |           TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }}
23 |           TG_SEND_TO: ${{ secrets.TELEGRAM_TO }}
24 |         run: |
25 |           python send_release_notify.py


--------------------------------------------------------------------------------
/.github/workflows/update-game-data.yml:
--------------------------------------------------------------------------------
 1 | name: Update game data
 2 | 
 3 | on:
 4 |   workflow_dispatch:
 5 |   schedule:
 6 |     - cron: '0 0 * * 0,3'
 7 | 
 8 | jobs:
 9 |   build:
10 |     runs-on: ubuntu-latest
11 |     steps:
12 |     - uses: actions/checkout@v3
13 |     - name: Setup Python
14 |       uses: actions/setup-python@v4
15 |       with:
16 |         python-version: '3.10'
17 |         architecture: x64
18 |     - name: Install dependencies
19 |       run: pip install requests requests_cache
20 |     - name: run update_game_data.py
21 |       run: |
22 |         python update_game_data.py
23 |     - name: Commit changes
24 |       uses: EndBug/add-and-commit@v9
25 |       with:
26 |         author_name: github-actions
27 |         author_email: 41898282+github-actions[bot]@users.noreply.github.com
28 |         message: 'game data auto update'
29 |         add: '.'
30 | 


--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
  1 | ### JetBrains template
  2 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
  3 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
  4 | .idea/
  5 | # User-specific stuff
  6 | .idea/**/workspace.xml
  7 | .idea/**/tasks.xml
  8 | .idea/**/usage.statistics.xml
  9 | .idea/**/dictionaries
 10 | .idea/**/shelf
 11 | 
 12 | # Generated files
 13 | .idea/**/contentModel.xml
 14 | 
 15 | # Sensitive or high-churn files
 16 | .idea/**/dataSources/
 17 | .idea/**/dataSources.ids
 18 | .idea/**/dataSources.local.xml
 19 | .idea/**/sqlDataSources.xml
 20 | .idea/**/dynamic.xml
 21 | .idea/**/uiDesigner.xml
 22 | .idea/**/dbnavigator.xml
 23 | 
 24 | # Gradle
 25 | .idea/**/gradle.xml
 26 | .idea/**/libraries
 27 | 
 28 | # Gradle and Maven with auto-import
 29 | # When using Gradle or Maven with auto-import, you should exclude module files,
 30 | # since they will be recreated, and may cause churn.  Uncomment if using
 31 | # auto-import.
 32 | # .idea/artifacts
 33 | # .idea/compiler.xml
 34 | # .idea/jarRepositories.xml
 35 | # .idea/modules.xml
 36 | # .idea/*.iml
 37 | # .idea/modules
 38 | # *.iml
 39 | # *.ipr
 40 | 
 41 | # CMake
 42 | cmake-build-*/
 43 | 
 44 | # Mongo Explorer plugin
 45 | .idea/**/mongoSettings.xml
 46 | 
 47 | # File-based project format
 48 | *.iws
 49 | 
 50 | # IntelliJ
 51 | out/
 52 | 
 53 | # mpeltonen/sbt-idea plugin
 54 | .idea_modules/
 55 | 
 56 | # JIRA plugin
 57 | atlassian-ide-plugin.xml
 58 | 
 59 | # Cursive Clojure plugin
 60 | .idea/replstate.xml
 61 | 
 62 | # Crashlytics plugin (for Android Studio and IntelliJ)
 63 | com_crashlytics_export_strings.xml
 64 | crashlytics.properties
 65 | crashlytics-build.properties
 66 | fabric.properties
 67 | 
 68 | # Editor-based Rest Client
 69 | .idea/httpRequests
 70 | 
 71 | # Android studio 3.1+ serialized cache file
 72 | .idea/caches/build_file_checksums.ser
 73 | 
 74 | ### Python template
 75 | # Byte-compiled / optimized / DLL files
 76 | __pycache__/
 77 | *.py[cod]
 78 | *$py.class
 79 | 
 80 | # C extensions
 81 | *.so
 82 | 
 83 | # Distribution / packaging
 84 | .Python
 85 | build/
 86 | develop-eggs/
 87 | dist/
 88 | downloads/
 89 | eggs/
 90 | .eggs/
 91 | lib/
 92 | lib64/
 93 | parts/
 94 | sdist/
 95 | var/
 96 | wheels/
 97 | share/python-wheels/
 98 | *.egg-info/
 99 | .installed.cfg
100 | *.egg
101 | MANIFEST
102 | 
103 | # PyInstaller
104 | #  Usually these files are written by a python script from a template
105 | #  before PyInstaller builds the exe, so as to inject date/other infos into it.
106 | *.manifest
107 | *.spec
108 | 
109 | # Installer logs
110 | pip-log.txt
111 | pip-delete-this-directory.txt
112 | 
113 | # Unit test / coverage reports
114 | htmlcov/
115 | .tox/
116 | .nox/
117 | .coverage
118 | .coverage.*
119 | .cache
120 | nosetests.xml
121 | coverage.xml
122 | *.cover
123 | *.py,cover
124 | .hypothesis/
125 | .pytest_cache/
126 | cover/
127 | 
128 | # Translations
129 | *.mo
130 | *.pot
131 | 
132 | # Django stuff:
133 | *.log
134 | local_settings.py
135 | db.sqlite3
136 | db.sqlite3-journal
137 | 
138 | # Flask stuff:
139 | instance/
140 | .webassets-cache
141 | 
142 | # Scrapy stuff:
143 | .scrapy
144 | 
145 | # Sphinx documentation
146 | docs/_build/
147 | 
148 | # PyBuilder
149 | .pybuilder/
150 | target/
151 | 
152 | # Jupyter Notebook
153 | .ipynb_checkpoints
154 | 
155 | # IPython
156 | profile_default/
157 | ipython_config.py
158 | 
159 | # pyenv
160 | #   For a library or package, you might want to ignore these files since the code is
161 | #   intended to run in multiple environments; otherwise, check them in:
162 | # .python-version
163 | 
164 | # pipenv
165 | #   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
166 | #   However, in case of collaboration, if having platform-specific dependencies or dependencies
167 | #   having no cross-platform support, pipenv may install dependencies that don't work, or not
168 | #   install all needed dependencies.
169 | #Pipfile.lock
170 | 
171 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
172 | __pypackages__/
173 | 
174 | # Celery stuff
175 | celerybeat-schedule
176 | celerybeat.pid
177 | 
178 | # SageMath parsed files
179 | *.sage.py
180 | 
181 | # Environments
182 | .env
183 | .venv
184 | env/
185 | venv/
186 | ENV/
187 | env.bak/
188 | venv.bak/
189 | 
190 | # Spyder project settings
191 | .spyderproject
192 | .spyproject
193 | 
194 | # Rope project settings
195 | .ropeproject
196 | 
197 | # mkdocs documentation
198 | /site
199 | 
200 | # mypy
201 | .mypy_cache/
202 | .dmypy.json
203 | dmypy.json
204 | 
205 | # Pyre type checker
206 | .pyre/
207 | 
208 | # pytype static type analyzer
209 | .pytype/
210 | 
211 | # Cython debug symbols
212 | cython_debug/
213 | 
214 | config.json
215 | yuzu-config.json
216 | download/
217 | output/
218 | web/
219 | http_cache.sqlite
220 | **/CloudflareSpeedTest/
221 | storage.json
222 | 


--------------------------------------------------------------------------------
/api/__init__.py:
--------------------------------------------------------------------------------
1 | import os
2 | import pkgutil
3 | from importlib import import_module
4 | 
5 | __all__ = list(module for _, module, _ in pkgutil.iter_modules([os.path.dirname(__file__)]))
6 | 
7 | for m in __all__:
8 |     import_module(f'.{m}', 'api')
9 | 


--------------------------------------------------------------------------------
/api/cheats_api.py:
--------------------------------------------------------------------------------
 1 | from typing import List
 2 | from config import config
 3 | from api.common_response import *
 4 | 
 5 | import eel
 6 | 
 7 | 
 8 | @eel.expose
 9 | def scan_all_cheats_folder():
10 |     from module.cheats import scan_all_cheats_folder
11 |     from module.yuzu import get_yuzu_load_path
12 |     try:
13 |         return success_response(scan_all_cheats_folder(get_yuzu_load_path()))
14 |     except Exception as e:
15 |         return exception_response(e)
16 | 
17 | 
18 | @eel.expose
19 | def list_all_cheat_files_from_folder(folder_path: str):
20 |     from module.cheats import list_all_cheat_files_from_folder
21 |     try:
22 |         return success_response(list_all_cheat_files_from_folder(folder_path))
23 |     except Exception as e:
24 |         return exception_response(e)
25 | 
26 | 
27 | @eel.expose
28 | def load_cheat_chunk_info(cheat_file_path: str):
29 |     from module.cheats import load_cheat_chunk_info
30 |     try:
31 |         return success_response(load_cheat_chunk_info(cheat_file_path))
32 |     except Exception as e:
33 |         return exception_response(e)
34 | 
35 | 
36 | @eel.expose
37 | def update_current_cheats(enable_titles: List[str], cheat_file_path: str):
38 |     from module.cheats import update_current_cheats
39 |     try:
40 |         return success_response(update_current_cheats(enable_titles, cheat_file_path))
41 |     except Exception as e:
42 |         return exception_response(e)
43 | 
44 | 
45 | @eel.expose
46 | def open_cheat_mod_folder(folder_path: str):
47 |     from module.cheats import open_cheat_mod_folder
48 |     try:
49 |         return success_response(open_cheat_mod_folder(folder_path))
50 |     except Exception as e:
51 |         return exception_response(e)
52 | 
53 | 
54 | @eel.expose
55 | def get_game_data():
56 |     from module.cheats import get_game_data
57 |     try:
58 |         return success_response(get_game_data())
59 |     except Exception as e:
60 |         return exception_response(e)
61 | 


--------------------------------------------------------------------------------
/api/common_api.py:
--------------------------------------------------------------------------------
  1 | import os
  2 | from typing import Dict
  3 | 
  4 | import eel
  5 | from api.common_response import *
  6 | from config import current_version, shared
  7 | import logging
  8 | import time
  9 | from module.firmware import get_firmware_infos
 10 | 
 11 | logger = logging.getLogger(__name__)
 12 | 
 13 | 
 14 | @eel.expose
 15 | def get_available_firmware_infos():
 16 |     try:
 17 |         return success_response(get_firmware_infos())
 18 |     except Exception as e:
 19 |         return exception_response(e)
 20 | 
 21 | 
 22 | @generic_api
 23 | def get_available_firmware_sources():
 24 |     from module.firmware import get_available_firmware_sources
 25 |     return get_available_firmware_sources()
 26 | 
 27 | 
 28 | @eel.expose
 29 | def get_current_version():
 30 |     shared['ui_init_time'] = time.time()
 31 |     return success_response(current_version)
 32 | 
 33 | 
 34 | @eel.expose
 35 | def update_last_open_emu_page(page):
 36 |     from config import update_last_open_emu_page
 37 |     update_last_open_emu_page(page)
 38 | 
 39 | 
 40 | @eel.expose
 41 | def update_dark_state(dark):
 42 |     from config import update_dark_state
 43 |     update_dark_state(dark)
 44 | 
 45 | 
 46 | @eel.expose
 47 | def detect_firmware_version(emu_type: str):
 48 |     from module.firmware import detect_firmware_version
 49 |     try:
 50 |         detect_firmware_version(emu_type)
 51 |         return success_response()
 52 |     except Exception as e:
 53 |         return exception_response(e)
 54 | 
 55 | 
 56 | @eel.expose
 57 | def get_config():
 58 |     from config import config
 59 |     return success_response(config.to_dict())
 60 | 
 61 | 
 62 | @eel.expose
 63 | def open_url_in_default_browser(url):
 64 |     import webbrowser
 65 |     webbrowser.open(url, new=0, autoraise=True)
 66 | 
 67 | 
 68 | @eel.expose
 69 | def update_setting(setting: Dict[str, object]):
 70 |     from config import config, update_setting
 71 |     update_setting(setting)
 72 |     from module.network import session, get_durable_cache_session, get_proxies
 73 |     session.proxies.update(get_proxies())
 74 |     get_durable_cache_session().proxies.update(get_proxies())
 75 |     return success_response(config.to_dict())
 76 | 
 77 | 
 78 | @eel.expose
 79 | def get_net_release_info_by_tag(tag: str):
 80 |     from repository.my_info import get_release_info_by_tag
 81 |     try:
 82 |         return success_response(get_release_info_by_tag(tag))
 83 |     except Exception as e:
 84 |         return exception_response(e)
 85 | 
 86 | 
 87 | @eel.expose
 88 | def stop_download():
 89 |     from module.downloader import stop_download
 90 |     try:
 91 |         return success_response(stop_download())
 92 |     except Exception as e:
 93 |         return exception_response(e)
 94 | 
 95 | 
 96 | @eel.expose
 97 | def pause_download():
 98 |     from module.downloader import pause_download
 99 |     try:
100 |         return success_response(pause_download())
101 |     except Exception as e:
102 |         return exception_response(e)
103 | 
104 | 
105 | @eel.expose
106 | def load_history_path(emu_type: str):
107 |     from storage import storage
108 |     from config import config
109 |     emu_type = emu_type.lower()
110 |     if emu_type == 'yuzu':
111 |         return success_response(list(_merge_to_set(storage.yuzu_history.keys(), config.yuzu.yuzu_path)))
112 |     elif emu_type == 'suyu':
113 |         return success_response(list(_merge_to_set(storage.suyu_history.keys(), config.suyu.path)))
114 |     else:
115 |         return success_response(list(_merge_to_set(storage.ryujinx_history.keys(), config.ryujinx.path)))
116 | 
117 | 
118 | @eel.expose
119 | def delete_history_path(emu_type: str, path_to_delete: str):
120 |     from storage import delete_history_path
121 |     delete_history_path(emu_type, path_to_delete)
122 |     return success_response()
123 | 
124 | 
125 | @eel.expose
126 | def get_github_mirrors():
127 |     from module.network import get_github_mirrors
128 |     try:
129 |         return success_response(get_github_mirrors())
130 |     except Exception as e:
131 |         return exception_response(e)
132 | 
133 | 
134 | @eel.expose
135 | def update_window_size(width: int, height: int):
136 |     from config import dump_config, config, shared
137 |     if shared['mode'] == 'webview':
138 |         from ui_webview import get_window_size
139 |         width, height = get_window_size()
140 |     if width == config.setting.ui.width and height == config.setting.ui.height:
141 |         return success_response()
142 |     config.setting.ui.width = width
143 |     config.setting.ui.height = height
144 |     logger.info(f'saving window size: {(config.setting.ui.width, config.setting.ui.height)}')
145 |     dump_config()
146 |     return success_response()
147 | 
148 | 
149 | @generic_api
150 | def get_storage():
151 |     from storage import storage
152 |     return storage.to_dict()
153 | 
154 | 
155 | @generic_api
156 | def delete_path(path: str):
157 |     from module.common import delete_path
158 |     return delete_path(path)
159 | 
160 | 
161 | def _merge_to_set(*cols):
162 |     from collections.abc import Iterable
163 |     s = set()
164 |     for c in cols:
165 |         if isinstance(c, Iterable) and not isinstance(c, str):
166 |             for i in c:
167 |                 s.add(i)
168 |         else:
169 |             s.add(c)
170 |     return s
171 | 


--------------------------------------------------------------------------------
/api/common_response.py:
--------------------------------------------------------------------------------
  1 | import logging
  2 | from module.msg_notifier import send_notify
  3 | from exception.common_exception import *
  4 | from exception.download_exception import *
  5 | from exception.install_exception import *
  6 | from requests.exceptions import ConnectionError
  7 | import eel
  8 | import orjson
  9 | 
 10 | 
 11 | logger = logging.getLogger(__name__)
 12 | 
 13 | 
 14 | def success_response(data=None, msg=None):
 15 |     if data:
 16 |         data = orjson.loads(orjson.dumps(data))
 17 |     return {'code': 0, 'data': data, 'msg': msg}
 18 | 
 19 | 
 20 | def exception_response(ex):
 21 |     import traceback
 22 |     if type(ex) in exception_handler_map:
 23 |         return exception_handler_map[type(ex)](ex)
 24 |     logger.error(ex, exc_info=True)
 25 |     traceback_str = "".join(traceback.format_exception(ex))
 26 |     send_notify(f'出现异常, {traceback_str}')
 27 |     return error_response(999, str(ex))
 28 | 
 29 | 
 30 | def version_not_found_handler(ex: VersionNotFoundException):
 31 |     logger.info(f'{str(ex)}')
 32 |     send_notify(f'无法获取 {ex.branch} 分支的 [{ex.target_version}] 版本信息')
 33 |     return error_response(404, str(ex))
 34 | 
 35 | 
 36 | def md5_not_found_handler(ex: Md5NotMatchException):
 37 |     logger.info(f'{str(ex)}')
 38 |     send_notify(f'固件文件 md5 不匹配, 请重新下载')
 39 |     return error_response(501, str(ex))
 40 | 
 41 | 
 42 | def download_interrupted_handler(ex: DownloadInterrupted):
 43 |     logger.info(f'{str(ex)}')
 44 |     send_notify(f'下载任务被终止')
 45 |     return error_response(601, str(ex))
 46 | 
 47 | 
 48 | def download_paused_handler(ex: DownloadPaused):
 49 |     logger.info(f'{str(ex)}')
 50 |     send_notify(f'下载任务被暂停')
 51 |     return error_response(602, str(ex))
 52 | 
 53 | 
 54 | def download_not_completed_handler(ex: DownloadNotCompleted):
 55 |     logger.info(f'{str(ex)}')
 56 |     send_notify(f'下载任务 [{ex.name}] 未完成, 状态: {ex.status}')
 57 |     return error_response(603, str(ex))
 58 | 
 59 | 
 60 | def fail_to_copy_files_handler(ex: FailToCopyFiles):
 61 |     logger.exception(ex.raw_exception)
 62 |     send_notify(f'{ex.msg}, 这可能是由于相关文件被占用或者没有相关目录的写入权限造成的')
 63 |     send_notify(f'请检查相关程序是否已经关闭, 或者重启一下系统试试')
 64 |     return error_response(701, str(ex))
 65 | 
 66 | 
 67 | def ignored_exception_handler(ex):
 68 |     logger.info(f'{str(ex)}')
 69 |     return error_response(801, str(ex))
 70 | 
 71 | 
 72 | def connection_error_handler(ex):
 73 |     import traceback
 74 |     traceback_str = "".join([s for s in traceback.format_exception(ex) if s.strip() != ''])
 75 |     logger.info(f'{str(ex)}\n{traceback_str}')
 76 |     send_notify(f'出现异常, {traceback_str}')
 77 |     return error_response(999, str(ex))
 78 | 
 79 | 
 80 | exception_handler_map = {
 81 |     VersionNotFoundException: version_not_found_handler,
 82 |     Md5NotMatchException: md5_not_found_handler,
 83 |     DownloadInterrupted: download_interrupted_handler,
 84 |     DownloadPaused: download_paused_handler,
 85 |     DownloadNotCompleted: download_not_completed_handler,
 86 |     FailToCopyFiles: fail_to_copy_files_handler,
 87 |     IgnoredException: ignored_exception_handler,
 88 |     ConnectionError: connection_error_handler,
 89 | }
 90 | 
 91 | 
 92 | def generic_api(func):
 93 |     def wrapper(*args, **kw):
 94 |         try:
 95 |             return success_response(func(*args, **kw))
 96 |         except Exception as e:
 97 |             return exception_response(e)
 98 |     eel._expose(func.__name__, wrapper)
 99 |     return wrapper
100 | 
101 | 
102 | def error_response(code, msg):
103 |     return {'code': code, 'msg': msg}
104 | 
105 | 
106 | __all__ = ['success_response', 'exception_response', 'error_response', 'generic_api']
107 | 


--------------------------------------------------------------------------------
/api/ryujinx_api.py:
--------------------------------------------------------------------------------
  1 | import eel
  2 | from api.common_response import success_response, exception_response, error_response
  3 | from repository.ryujinx import get_all_ryujinx_release_infos
  4 | from config import config
  5 | import logging
  6 | 
  7 | 
  8 | logger = logging.getLogger(__name__)
  9 | 
 10 | 
 11 | @eel.expose
 12 | def open_ryujinx_keys_folder():
 13 |     from module.ryujinx import open_ryujinx_keys_folder
 14 |     open_ryujinx_keys_folder()
 15 |     return success_response()
 16 | 
 17 | 
 18 | @eel.expose
 19 | def get_ryujinx_config():
 20 |     return config.ryujinx.to_dict()
 21 | 
 22 | 
 23 | @eel.expose
 24 | def ask_and_update_ryujinx_path():
 25 |     from module.dialogs import ask_folder
 26 |     folder = ask_folder()
 27 |     logger.info(f'select folder: {folder}')
 28 |     if folder:
 29 |         from module.ryujinx import update_ryujinx_path
 30 |         update_ryujinx_path(folder)
 31 |         return success_response(msg=f'修改 ryujinx 目录至 {folder}')
 32 |     else:
 33 |         return error_response(100, '修改已取消')
 34 | 
 35 | 
 36 | @eel.expose
 37 | def update_ryujinx_path(folder: str):
 38 |     from module.ryujinx import update_ryujinx_path
 39 |     update_ryujinx_path(folder)
 40 |     return success_response(msg=f'修改 ryujinx 目录至 {folder}')
 41 | 
 42 | 
 43 | @eel.expose
 44 | def get_ryujinx_release_infos():
 45 |     try:
 46 |         return success_response(get_all_ryujinx_release_infos(config.ryujinx.branch))
 47 |     except Exception as e:
 48 |         return exception_response(e)
 49 | 
 50 | 
 51 | @eel.expose
 52 | def detect_ryujinx_version():
 53 |     try:
 54 |         from module.ryujinx import detect_ryujinx_version
 55 |         return success_response(detect_ryujinx_version())
 56 |     except Exception as e:
 57 |         return exception_response(e)
 58 | 
 59 | 
 60 | @eel.expose
 61 | def start_ryujinx():
 62 |     from module.ryujinx import start_ryujinx
 63 |     try:
 64 |         start_ryujinx()
 65 |         return success_response()
 66 |     except Exception as e:
 67 |         return exception_response(e)
 68 | 
 69 | 
 70 | @eel.expose
 71 | def install_ryujinx(version, branch):
 72 |     if not version or version == '':
 73 |         return {'msg': f'无效的版本 {version}'}
 74 |     from module.ryujinx import install_ryujinx_by_version
 75 |     try:
 76 |         return success_response(msg=install_ryujinx_by_version(version, branch))
 77 |     except Exception as e:
 78 |         return exception_response(e)
 79 | 
 80 | 
 81 | @eel.expose
 82 | def install_ryujinx_firmware(version):
 83 |     if not version or version == '':
 84 |         return {'msg': f'无效的版本 {version}'}
 85 |     from module.ryujinx import install_firmware_to_ryujinx
 86 |     try:
 87 |         return success_response(msg=install_firmware_to_ryujinx(version))
 88 |     except Exception as e:
 89 |         return exception_response(e)
 90 | 
 91 | 
 92 | @eel.expose
 93 | def switch_ryujinx_branch(branch: str):
 94 |     from config import dump_config
 95 |     if branch not in {'mainline', 'canary'}:
 96 |         return error_response(-1, f'Invalidate branch: {branch}')
 97 |     target_branch = branch
 98 |     logger.info(f'switch ryujinx branch to {target_branch}')
 99 |     config.ryujinx.branch = target_branch
100 |     dump_config()
101 |     return success_response(config.ryujinx.to_dict())
102 | 
103 | 
104 | @eel.expose
105 | def load_ryujinx_change_log():
106 |     from repository.ryujinx import load_ryujinx_change_log
107 |     try:
108 |         return success_response(load_ryujinx_change_log(config.ryujinx.branch))
109 |     except Exception as e:
110 |         return exception_response(e)
111 | 
112 | 


--------------------------------------------------------------------------------
/api/save_manager_api.py:
--------------------------------------------------------------------------------
 1 | from config import config
 2 | from api.common_response import *
 3 | 
 4 | 
 5 | @generic_api
 6 | def get_users_in_save():
 7 |     from module.save_manager import get_users_in_save
 8 |     return get_users_in_save()
 9 | 
10 | 
11 | @generic_api
12 | def list_all_games_by_user_folder(folder: str):
13 |     from module.save_manager import list_all_games_by_user_folder
14 |     return list_all_games_by_user_folder(folder)
15 | 
16 | 
17 | @generic_api
18 | def ask_and_update_yuzu_save_backup_folder():
19 |     from module.save_manager import ask_and_update_yuzu_save_backup_folder
20 |     return ask_and_update_yuzu_save_backup_folder()
21 | 
22 | 
23 | @generic_api
24 | def backup_yuzu_save_folder(folder: str):
25 |     from module.save_manager import backup_folder
26 |     return backup_folder(folder)
27 | 
28 | 
29 | @generic_api
30 | def open_yuzu_save_backup_folder():
31 |     from module.save_manager import open_yuzu_save_backup_folder
32 |     return open_yuzu_save_backup_folder()
33 | 
34 | 
35 | @generic_api
36 | def list_all_yuzu_backups():
37 |     from module.save_manager import list_all_yuzu_backups
38 |     return list_all_yuzu_backups()
39 | 
40 | 
41 | @generic_api
42 | def restore_yuzu_save_from_backup(user_folder_name: str, backup_path: str):
43 |     from module.save_manager import restore_yuzu_save_from_backup
44 |     return restore_yuzu_save_from_backup(user_folder_name, backup_path)
45 | 


--------------------------------------------------------------------------------
/api/suyu_api.py:
--------------------------------------------------------------------------------
 1 | import eel
 2 | from api.common_response import success_response, exception_response, error_response
 3 | from config import config, dump_config
 4 | import logging
 5 | 
 6 | logger = logging.getLogger(__name__)
 7 | 
 8 | 
 9 | @eel.expose
10 | def open_suyu_keys_folder():
11 |     from module.suyu import open_suyu_keys_folder
12 |     open_suyu_keys_folder()
13 |     return success_response()
14 | 
15 | 
16 | @eel.expose
17 | def get_suyu_config():
18 |     return config.suyu.to_dict()
19 | 
20 | 
21 | @eel.expose
22 | def ask_and_update_suyu_path():
23 |     from module.dialogs import ask_folder
24 |     folder = ask_folder()
25 |     logger.info(f'select folder: {folder}')
26 |     if folder:
27 |         from module.suyu import update_suyu_path
28 |         update_suyu_path(folder)
29 |         return success_response(msg=f'修改 suyu 目录至 {folder}')
30 |     else:
31 |         return error_response(100, '修改已取消')
32 | 
33 | 
34 | @eel.expose
35 | def update_suyu_path(folder: str):
36 |     from module.suyu import update_suyu_path
37 |     update_suyu_path(folder)
38 |     return success_response(msg=f'修改 suyu 目录至 {folder}')
39 | 
40 | 
41 | # @eel.expose
42 | # def detect_suyu_version():
43 | #     try:
44 | #         from module.suyu import detect_suyu_version
45 | #         return success_response(detect_suyu_version())
46 | #     except Exception as e:
47 | #         return exception_response(e)
48 | 
49 | 
50 | @eel.expose
51 | def start_suyu():
52 |     from module.suyu import start_suyu
53 |     try:
54 |         start_suyu()
55 |         return success_response()
56 |     except Exception as e:
57 |         return exception_response(e)
58 | 
59 | 
60 | @eel.expose
61 | def install_suyu(version, branch):
62 |     if not version or version == '':
63 |         return error_response(404, f'无效的版本 {version}')
64 |     from module.suyu import install_suyu
65 |     try:
66 |         return success_response(msg=install_suyu(version))
67 |     except Exception as e:
68 |         return exception_response(e)
69 | 
70 | 
71 | @eel.expose
72 | def install_suyu_firmware(version):
73 |     if not version or version == '':
74 |         return error_response(404, f'无效的版本 {version}')
75 |     from module.suyu import install_firmware_to_suyu
76 |     try:
77 |         return success_response(msg=install_firmware_to_suyu(version))
78 |     except Exception as e:
79 |         return exception_response(e)
80 | 
81 | 
82 | @eel.expose
83 | def get_all_suyu_release_versions():
84 |     from repository.suyu import get_all_suyu_release_versions
85 |     try:
86 |         return success_response(get_all_suyu_release_versions())
87 |     except Exception as e:
88 |         return exception_response(e)
89 | 
90 | 
91 | # @eel.expose
92 | # def get_suyu_commit_logs():
93 | #     from module.suyu import get_suyu_commit_logs
94 | #     try:
95 | #         return success_response(get_suyu_commit_logs())
96 | #     except Exception as e:
97 | #         return exception_response(e)
98 | 


--------------------------------------------------------------------------------
/api/updater_api.py:
--------------------------------------------------------------------------------
 1 | from config import config
 2 | from api.common_response import *
 3 | 
 4 | import eel
 5 | 
 6 | 
 7 | @eel.expose
 8 | def check_update():
 9 |     from module.updater import check_update
10 |     has_update, latest_version = check_update()
11 |     return success_response(has_update, latest_version)
12 | 
13 | 
14 | @eel.expose
15 | def download_net_by_tag(tag: str):
16 |     from module.updater import download_net_by_tag
17 |     try:
18 |         return success_response(download_net_by_tag(tag))
19 |     except Exception as e:
20 |         return exception_response(e)
21 | 
22 | 
23 | @eel.expose
24 | def update_net_by_tag(tag: str):
25 |     from module.updater import update_self_by_tag
26 |     try:
27 |         return success_response(update_self_by_tag(tag))
28 |     except Exception as e:
29 |         return exception_response(e)
30 | 
31 | 
32 | @eel.expose
33 | def load_change_log():
34 |     from repository.my_info import load_change_log
35 |     try:
36 |         return success_response(load_change_log())
37 |     except Exception as e:
38 |         exception_response(e)
39 | 


--------------------------------------------------------------------------------
/api/yuzu_api.py:
--------------------------------------------------------------------------------
  1 | import eel
  2 | from api.common_response import success_response, exception_response, error_response
  3 | from repository.yuzu import get_all_yuzu_release_infos
  4 | from config import config, dump_config
  5 | import logging
  6 | 
  7 | logger = logging.getLogger(__name__)
  8 | 
  9 | 
 10 | @eel.expose
 11 | def open_yuzu_keys_folder():
 12 |     from module.yuzu import open_yuzu_keys_folder
 13 |     open_yuzu_keys_folder()
 14 |     return success_response()
 15 | 
 16 | 
 17 | @eel.expose
 18 | def get_yuzu_config():
 19 |     return config.yuzu.to_dict()
 20 | 
 21 | 
 22 | @eel.expose
 23 | def ask_and_update_yuzu_path():
 24 |     from module.dialogs import ask_folder
 25 |     folder = ask_folder()
 26 |     logger.info(f'select folder: {folder}')
 27 |     if folder:
 28 |         from module.yuzu import update_yuzu_path
 29 |         update_yuzu_path(folder)
 30 |         return success_response(msg=f'修改 yuzu 目录至 {folder}')
 31 |     else:
 32 |         return error_response(100, '修改已取消')
 33 | 
 34 | 
 35 | @eel.expose
 36 | def update_yuzu_path(folder: str):
 37 |     from module.yuzu import update_yuzu_path
 38 |     update_yuzu_path(folder)
 39 |     return success_response(msg=f'修改 yuzu 目录至 {folder}')
 40 | 
 41 | 
 42 | @eel.expose
 43 | def detect_yuzu_version():
 44 |     try:
 45 |         from module.yuzu import detect_yuzu_version
 46 |         return success_response(detect_yuzu_version())
 47 |     except Exception as e:
 48 |         return exception_response(e)
 49 | 
 50 | 
 51 | @eel.expose
 52 | def start_yuzu():
 53 |     from module.yuzu import start_yuzu
 54 |     try:
 55 |         start_yuzu()
 56 |         return success_response()
 57 |     except Exception as e:
 58 |         return exception_response(e)
 59 | 
 60 | 
 61 | @eel.expose
 62 | def install_yuzu(version, branch):
 63 |     if not version or version == '':
 64 |         return error_response(404, f'无效的版本 {version}')
 65 |     from module.yuzu import install_yuzu
 66 |     try:
 67 |         return success_response(msg=install_yuzu(version, branch))
 68 |     except Exception as e:
 69 |         return exception_response(e)
 70 | 
 71 | 
 72 | @eel.expose
 73 | def install_yuzu_firmware(version):
 74 |     if not version or version == '':
 75 |         return error_response(404, f'无效的版本 {version}')
 76 |     from module.yuzu import install_firmware_to_yuzu
 77 |     try:
 78 |         return success_response(msg=install_firmware_to_yuzu(version))
 79 |     except Exception as e:
 80 |         return exception_response(e)
 81 | 
 82 | 
 83 | @eel.expose
 84 | def switch_yuzu_branch():
 85 |     if config.yuzu.branch == 'ea':
 86 |         target_branch = 'mainline'
 87 |     else:
 88 |         target_branch = 'ea'
 89 |     logger.info(f'switch yuzu branch to {target_branch}')
 90 |     config.yuzu.branch = target_branch
 91 |     dump_config()
 92 |     return config.yuzu.to_dict()
 93 | 
 94 | 
 95 | @eel.expose
 96 | def get_all_yuzu_release_versions():
 97 |     from repository.yuzu import get_all_yuzu_release_versions
 98 |     try:
 99 |         return success_response(get_all_yuzu_release_versions(config.yuzu.branch))
100 |     except Exception as e:
101 |         return exception_response(e)
102 | 
103 | 
104 | @eel.expose
105 | def get_yuzu_commit_logs():
106 |     from module.yuzu import get_yuzu_commit_logs
107 |     try:
108 |         return success_response(get_yuzu_commit_logs())
109 |     except Exception as e:
110 |         return exception_response(e)
111 | 


--------------------------------------------------------------------------------
/build_tools/upx/LICENSE:
--------------------------------------------------------------------------------
  1 | -----BEGIN PGP SIGNED MESSAGE-----
  2 | 
  3 | 
  4 |                  ooooo     ooo ooooooooo.   ooooooo  ooooo
  5 |                  `888'     `8' `888   `Y88.  `8888    d8'
  6 |                   888       8   888   .d88'    Y888..8P
  7 |                   888       8   888ooo88P'      `8888'
  8 |                   888       8   888            .8PY888.
  9 |                   `88.    .8'   888           d8'  `888b
 10 |                     `YbodP'    o888o        o888o  o88888o
 11 | 
 12 | 
 13 |                     The Ultimate Packer for eXecutables
 14 |           Copyright (c) 1996-2000 Markus Oberhumer & Laszlo Molnar
 15 |                http://wildsau.idv.uni-linz.ac.at/mfx/upx.html
 16 |                           http://www.nexus.hu/upx
 17 |                             http://upx.tsx.org
 18 | 
 19 | 
 20 | PLEASE CAREFULLY READ THIS LICENSE AGREEMENT, ESPECIALLY IF YOU PLAN
 21 | TO MODIFY THE UPX SOURCE CODE OR USE A MODIFIED UPX VERSION.
 22 | 
 23 | 
 24 | ABSTRACT
 25 | ========
 26 | 
 27 |    UPX and UCL are copyrighted software distributed under the terms
 28 |    of the GNU General Public License (hereinafter the "GPL").
 29 | 
 30 |    The stub which is imbedded in each UPX compressed program is part
 31 |    of UPX and UCL, and contains code that is under our copyright. The
 32 |    terms of the GNU General Public License still apply as compressing
 33 |    a program is a special form of linking with our stub.
 34 | 
 35 |    As a special exception we grant the free usage of UPX for all
 36 |    executables, including commercial programs.
 37 |    See below for details and restrictions.
 38 | 
 39 | 
 40 | COPYRIGHT
 41 | =========
 42 | 
 43 |    UPX and UCL are copyrighted software. All rights remain with the authors.
 44 | 
 45 |    UPX is Copyright (C) 1996-2000 Markus Franz Xaver Johannes Oberhumer
 46 |    UPX is Copyright (C) 1996-2000 Laszlo Molnar
 47 | 
 48 |    UCL is Copyright (C) 1996-2000 Markus Franz Xaver Johannes Oberhumer
 49 | 
 50 | 
 51 | GNU GENERAL PUBLIC LICENSE
 52 | ==========================
 53 | 
 54 |    UPX and the UCL library are free software; you can redistribute them
 55 |    and/or modify them under the terms of the GNU General Public License as
 56 |    published by the Free Software Foundation; either version 2 of
 57 |    the License, or (at your option) any later version.
 58 | 
 59 |    UPX and UCL are distributed in the hope that they will be useful,
 60 |    but WITHOUT ANY WARRANTY; without even the implied warranty of
 61 |    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 62 |    GNU General Public License for more details.
 63 | 
 64 |    You should have received a copy of the GNU General Public License
 65 |    along with this program; see the file COPYING.
 66 | 
 67 | 
 68 | SPECIAL EXCEPTION FOR COMPRESSED EXECUTABLES
 69 | ============================================
 70 | 
 71 |    The stub which is imbedded in each UPX compressed program is part
 72 |    of UPX and UCL, and contains code that is under our copyright. The
 73 |    terms of the GNU General Public License still apply as compressing
 74 |    a program is a special form of linking with our stub.
 75 | 
 76 |    Hereby Markus F.X.J. Oberhumer and Laszlo Molnar grant you special
 77 |    permission to freely use and distribute all UPX compressed programs
 78 |    (including commercial ones), subject to the following restrictions:
 79 | 
 80 |    1. You must compress your program with a completely unmodified UPX
 81 |       version; either with our precompiled version, or (at your option)
 82 |       with a self compiled version of the unmodified UPX sources as
 83 |       distributed by us.
 84 |    2. This also implies that the UPX stub must be completely unmodfied, i.e.
 85 |       the stub imbedded in your compressed program must be byte-identical
 86 |       to the stub that is produced by the official unmodified UPX version.
 87 |    3. The decompressor and any other code from the stub must exclusively get
 88 |       used by the unmodified UPX stub for decompressing your program at
 89 |       program startup. No portion of the stub may get read, copied,
 90 |       called or otherwise get used or accessed by your program.
 91 | 
 92 | 
 93 | ANNOTATIONS
 94 | ===========
 95 | 
 96 |   - You can use a modified UPX version or modified UPX stub only for
 97 |     programs that are compatible with the GNU General Public License.
 98 | 
 99 |   - We grant you special permission to freely use and distribute all UPX
100 |     compressed programs. But any modification of the UPX stub (such as,
101 |     but not limited to, removing our copyright string or making your
102 |     program non-decompressible) will immediately revoke your right to
103 |     use and distribute a UPX compressed program.
104 | 
105 |   - UPX is not a software protection tool; by requiring that you use
106 |     the unmodified UPX version for your proprietary programs we
107 |     make sure that any user can decompress your program. This protects
108 |     both you and your users as nobody can hide malicious code -
109 |     any program that cannot be decompressed is highly suspicious
110 |     by definition.
111 | 
112 |   - You can integrate all or part of UPX and UCL into projects that
113 |     are compatible with the GNU GPL, but obviously you cannot grant
114 |     any special exceptions beyond the GPL for our code in your project.
115 | 
116 |   - We want to actively support manufacturers of virus scanners and
117 |     similar security software. Please contact us if you would like to
118 |     incorporate parts of UPX or UCL into such a product.
119 | 
120 | 
121 | 
122 | Markus F.X.J. Oberhumer                   Laszlo Molnar
123 | markus.oberhumer@jk.uni-linz.ac.at        ml1050@cdata.tvnet.hu
124 | 
125 | Linz, Austria, 25 Feb 2000
126 | 
127 | 
128 | 
129 | -----BEGIN PGP SIGNATURE-----
130 | Version: 2.6.3ia
131 | Charset: noconv
132 | 
133 | iQCVAwUBOLaLS210fyLu8beJAQFYVAP/ShzENWKLTvedLCjZbDcwaBEHfUVcrGMI
134 | wE7frMkbWT2zmkdv9hW90WmjMhOBu7yhUplvN8BKOtLiolEnZmLCYu8AGCwr5wBf
135 | dfLoClxnzfTtgQv5axF1awp4RwCUH3hf4cDrOVqmAsWXKPHtm4hx96jF6L4oHhjx
136 | OO03+ojZdO8=
137 | =CS52
138 | -----END PGP SIGNATURE-----
139 | 


--------------------------------------------------------------------------------
/build_tools/upx/README:
--------------------------------------------------------------------------------
  1 |                  ooooo     ooo  ooooooooo.  ooooooo  ooooo
  2 |                  `888'     `8'  `888   `Y88. `8888    d8'
  3 |                   888       8    888   .d88'   Y888..8P
  4 |                   888       8    888ooo88P'     `8888'
  5 |                   888       8    888           .8PY888.
  6 |                   `88.    .8'    888          d8'  `888b
  7 |                     `YbodP'     o888o       o888o  o88888o
  8 | 
  9 | 
 10 |                     The Ultimate Packer for eXecutables
 11 |    Copyright (c) 1996-2024 Markus Oberhumer, Laszlo Molnar & John Reiser
 12 |                            https://upx.github.io
 13 | 
 14 | 
 15 | 
 16 | WELCOME
 17 | =======
 18 | 
 19 | Welcome to UPX !
 20 | 
 21 | UPX is a free, secure, portable, extendable, high-performance
 22 | executable packer for several executable formats.
 23 | 
 24 | 
 25 | INTRODUCTION
 26 | ============
 27 | 
 28 | UPX is an advanced executable file compressor. UPX will typically
 29 | reduce the file size of programs and DLLs by around 50%-70%, thus
 30 | reducing disk space, network load times, download times and
 31 | other distribution and storage costs.
 32 | 
 33 | Programs and libraries compressed by UPX are completely self-contained
 34 | and run exactly as before, with no runtime or memory penalty for most
 35 | of the supported formats.
 36 | 
 37 | UPX supports a number of different executable formats, including
 38 | Windows programs and DLLs, and Linux executables.
 39 | 
 40 | UPX is free software distributed under the term of the GNU General
 41 | Public License. Full source code is available.
 42 | 
 43 | UPX may be distributed and used freely, even with commercial applications.
 44 | See the UPX License Agreements for details.
 45 | 
 46 | 
 47 | SECURITY CONTEXT
 48 | ================
 49 | 
 50 | IMPORTANT NOTE: UPX inherits the security context of any files it handles.
 51 | 
 52 | This means that packing, unpacking, or even testing or listing a file requires
 53 | the same security considerations as actually executing the file.
 54 | 
 55 | Use UPX on trusted files only!
 56 | 
 57 | 
 58 | SHORT DOCUMENTATION
 59 | ===================
 60 | 
 61 | 'upx program.exe' will compress a program or DLL. For best compression
 62 | results try 'upx --best program.exe' or 'upx --brute program.exe'.
 63 | 
 64 | Please see the file UPX.DOC for the full documentation. The files
 65 | NEWS and BUGS also contain various tidbits of information.
 66 | 
 67 | 
 68 | THE FUTURE
 69 | ==========
 70 | 
 71 |   - Stay up-to-date with ongoing OS and executable format changes
 72 | 
 73 |   - RISC-V 64 for Linux
 74 | 
 75 |   - ARM64 for Windows (help wanted)
 76 | 
 77 |   - We will *NOT* add any sort of protection and/or encryption.
 78 |     This only gives people a false feeling of security because
 79 |     all "protectors" can be broken by definition.
 80 | 
 81 |   - Fix all remaining bugs - please report any issues
 82 |     https://github.com/upx/upx/issues
 83 | 
 84 | 
 85 | COPYRIGHT
 86 | =========
 87 | 
 88 | Copyright (C) 1996-2024 Markus Franz Xaver Johannes Oberhumer
 89 | Copyright (C) 1996-2024 Laszlo Molnar
 90 | Copyright (C) 2000-2024 John F. Reiser
 91 | 
 92 | UPX is distributed with full source code under the terms of the
 93 | GNU General Public License v2+; either under the pure GPLv2+ (see
 94 | the file COPYING), or (at your option) under the GPLv+2 with special
 95 | exceptions and restrictions granting the free usage for all binaries
 96 | including commercial programs (see the file LICENSE).
 97 | 
 98 | This program is distributed in the hope that it will be useful,
 99 | but WITHOUT ANY WARRANTY; without even the implied warranty of
100 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
101 | 
102 | You should have received a copy of the UPX License Agreements along
103 | with this program; see the files COPYING and LICENSE. If not,
104 | visit the UPX home page.
105 | 
106 | 
107 | Share and enjoy,
108 | Markus & Laszlo & John
109 | 
110 | 
111 |    Markus F.X.J. Oberhumer              Laszlo Molnar
112 |    <markus@oberhumer.com>               <ezerotven+github@gmail.com>
113 | 
114 |    John F. Reiser
115 |    <jreiser@BitWagon.com>
116 | 
117 | 
118 | [ The term UPX is a shorthand for the Ultimate Packer for eXecutables
119 |   and holds no connection with potential owners of registered trademarks
120 |   or other rights. ]
121 | 


--------------------------------------------------------------------------------
/build_tools/upx/THANKS.txt:
--------------------------------------------------------------------------------
 1 |                  ooooo     ooo  ooooooooo.  ooooooo  ooooo
 2 |                  `888'     `8'  `888   `Y88. `8888    d8'
 3 |                   888       8    888   .d88'   Y888..8P
 4 |                   888       8    888ooo88P'     `8888'
 5 |                   888       8    888           .8PY888.
 6 |                   `88.    .8'    888          d8'  `888b
 7 |                     `YbodP'     o888o       o888o  o88888o
 8 | 
 9 | 
10 |                     The Ultimate Packer for eXecutables
11 |    Copyright (c) 1996-2024 Markus Oberhumer, Laszlo Molnar & John Reiser
12 |                            https://upx.github.io
13 | 
14 | 
15 | .___..        .
16 |   |  |_  _.._ ;_/ __
17 |   |  [ )(_][ )| \_)
18 | --------------------
19 | 
20 | UPX would not be what it is today without the invaluable help of
21 | everybody who was kind enough to spend time testing it, using it
22 | in applications and reporting bugs.
23 | 
24 | The following people made especially gracious contributions of their
25 | time and energy in helping to track down bugs, add new features, and
26 | generally assist in the UPX maintainership process:
27 | 
28 | Adam Ierymenko <api@one.net>
29 |   for severals ideas for the Linux version
30 | Andi Kleen <ak@muc.de> and Jamie Lokier <nospam@cern.ch>
31 |   for the /proc/self/fd/X and other Linux suggestions
32 | Andreas Muegge <andreas.muegge@gmx.de>
33 |   for the Win32 GUI
34 | Atli Mar Gudmundsson <agudmundsson@symantec.com>
35 |   for several comments on the win32/pe stub
36 | Charles W. Sandmann <sandmann@clio.rice.edu>
37 |   for the idea with the stubless decompressor in djgpp2/coff
38 | Ice
39 |   for debugging the PE headersize problem down
40 | Joergen Ibsen <jibz@hotmail.com> and d'b
41 |   for the relocation & address optimization ideas
42 | John S. Fine <johnfine@erols.com>
43 |   for the new version of the dos/exe decompressor
44 | Kornel Pal
45 |   for the EFI support
46 | Lukundoo <Lukundoo@softhome.net>
47 |   for beta testing
48 | Michael Devore
49 |   for initial dos/exe device driver support
50 | Oleg V. Volkov <rover@lglobus.ru>
51 |   for various FreeBSD specific information
52 | The Owl & G-RoM
53 |   for the --compress-icons fix
54 | Ralph Roth <RalphRoth@gmx.net>
55 |   for reporting several bugs
56 | Salvador Eduardo Tropea
57 |   for beta testing
58 | Stefan Widmann
59 |   for the win32/pe TLS callback support
60 | The WINE project (https://www.winehq.com/)
61 |   for lots of useful information found in their PE loader sources
62 | Natascha
63 | 


--------------------------------------------------------------------------------
/build_tools/upx/upx.exe:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/triwinds/ns-emu-tools/ab6a1a301ad524e85b1b5f2796f382bba384e915/build_tools/upx/upx.exe


--------------------------------------------------------------------------------
/build_tools/zip_files.py:
--------------------------------------------------------------------------------
 1 | from utils.package import compress_folder
 2 | from pathlib import Path
 3 | 
 4 | 
 5 | DIST_FOLDER = Path(__file__).parent.parent / 'dist'
 6 | 
 7 | 
 8 | if __name__ == '__main__':
 9 |     compress_folder(DIST_FOLDER.joinpath('NsEmuTools'), DIST_FOLDER.joinpath('NsEmuTools-dir.7z'))
10 | 


--------------------------------------------------------------------------------
/config.py:
--------------------------------------------------------------------------------
  1 | import json
  2 | import os
  3 | from dataclasses import dataclass, field
  4 | from typing import Optional, Dict
  5 | from pathlib import Path
  6 | from dataclasses_json import dataclass_json, Undefined
  7 | import logging
  8 | from logging.handlers import RotatingFileHandler
  9 | 
 10 | 
 11 | current_version = '0.5.5'
 12 | user_agent = f'ns-emu-tools/{current_version}'
 13 | 
 14 | 
 15 | console = logging.StreamHandler()
 16 | console.setLevel(logging.DEBUG)
 17 | # logging.getLogger("requests").setLevel(logging.WARNING)
 18 | logging.getLogger("urllib3").setLevel(logging.WARNING)
 19 | logging.getLogger("httpx").setLevel(logging.WARNING)
 20 | logging.getLogger("httpcore").setLevel(logging.WARNING)
 21 | # logging.getLogger("geventwebsocket.handler").setLevel(logging.WARNING)
 22 | logging.basicConfig(
 23 |     level=logging.DEBUG,
 24 |     format='%(asctime)s.%(msecs)03d|%(levelname)s|%(name)s|%(filename)s:%(lineno)s|%(funcName)s|%(message)s',
 25 |     datefmt='%Y-%m-%d %H:%M:%S',
 26 |     handlers=[RotatingFileHandler('ns-emu-tools.log', encoding='utf-8', maxBytes=10 * 1024 * 1024, backupCount=10),
 27 |               console]
 28 | )
 29 | logger = logging.getLogger(__name__)
 30 | config_path = Path('config.json')
 31 | config = None
 32 | shared = {}
 33 | 
 34 | 
 35 | def log_versions():
 36 |     import platform
 37 |     logger.info(f'system version: {platform.platform()}')
 38 |     logger.info(f'current version: {current_version}')
 39 | 
 40 | 
 41 | log_versions()
 42 | 
 43 | 
 44 | @dataclass_json
 45 | @dataclass
 46 | class YuzuConfig:
 47 |     yuzu_path: Optional[str] = 'D:\\Yuzu'
 48 |     yuzu_version: Optional[str] = None
 49 |     yuzu_firmware: Optional[str] = None
 50 |     branch: Optional[str] = 'ea'
 51 | 
 52 | 
 53 | @dataclass_json
 54 | @dataclass
 55 | class RyujinxConfig:
 56 |     path: Optional[str] = 'D:\\Ryujinx'
 57 |     version: Optional[str] = None
 58 |     firmware: Optional[str] = None
 59 |     branch: Optional[str] = 'mainline'
 60 | 
 61 | 
 62 | @dataclass_json
 63 | @dataclass
 64 | class SuyuConfig:
 65 |     path: Optional[str] = 'D:\\Suyu'
 66 |     version: Optional[str] = None
 67 |     firmware: Optional[str] = None
 68 |     branch: Optional[str] = 'dev'
 69 | 
 70 | 
 71 | @dataclass_json
 72 | @dataclass
 73 | class NetworkSetting:
 74 |     firmwareDownloadSource: Optional[str] = 'github'
 75 |     githubApiMode: Optional[str] = 'direct'
 76 |     githubDownloadMirror: Optional[str] = 'cloudflare_load_balance'
 77 |     useDoh: Optional[bool] = True
 78 |     proxy: Optional[str] = 'system'
 79 | 
 80 | 
 81 | @dataclass_json
 82 | @dataclass
 83 | class DownloadSetting:
 84 |     autoDeleteAfterInstall: Optional[bool] = True
 85 |     disableAria2Ipv6: Optional[bool] = True
 86 |     removeOldAria2LogFile: Optional[bool] = True
 87 |     verifyFirmwareMd5: Optional[bool] = True
 88 | 
 89 | 
 90 | @dataclass_json
 91 | @dataclass
 92 | class UiSetting:
 93 |     lastOpenEmuPage: Optional[str] = 'ryujinx'
 94 |     dark: Optional[bool] = True
 95 |     mode: Optional[str] = 'auto'
 96 |     width: int = 1300
 97 |     height: int = 850
 98 | 
 99 | 
100 | @dataclass_json
101 | @dataclass
102 | class OtherSetting:
103 |     rename_yuzu_to_cemu: Optional[bool] = False
104 | 
105 | 
106 | @dataclass_json(undefined=Undefined.EXCLUDE)
107 | @dataclass
108 | class CommonSetting:
109 |     ui: UiSetting = field(default_factory=UiSetting)
110 |     network: NetworkSetting = field(default_factory=NetworkSetting)
111 |     download: DownloadSetting = field(default_factory=DownloadSetting)
112 |     other: OtherSetting = field(default_factory=OtherSetting)
113 | 
114 | 
115 | @dataclass_json(undefined=Undefined.EXCLUDE)
116 | @dataclass
117 | class Config:
118 |     yuzu: YuzuConfig = field(default_factory=YuzuConfig)
119 |     ryujinx: RyujinxConfig = field(default_factory=RyujinxConfig)
120 |     suyu: SuyuConfig = field(default_factory=SuyuConfig)
121 |     setting: CommonSetting = field(default_factory=CommonSetting)
122 | 
123 | 
124 | if os.path.exists(config_path):
125 |     with open(config_path, 'r', encoding='utf-8') as f:
126 |         config = Config.from_dict(json.load(f))
127 |         config.yuzu.yuzu_path = str(Path(config.yuzu.yuzu_path).absolute())
128 |         config.ryujinx.path = str(Path(config.ryujinx.path).absolute())
129 | if not config:
130 |     config = Config()
131 | config.yuzu.branch = 'ea'
132 | 
133 | 
134 | def dump_config():
135 |     logger.info(f'saving config to {config_path.absolute()}')
136 |     with open(config_path, 'w', encoding='utf-8') as f:
137 |         f.write(config.to_json(ensure_ascii=False, indent=2))
138 | 
139 | 
140 | def update_last_open_emu_page(page: str):
141 |     if page == 'ryujinx':
142 |         config.setting.ui.lastOpenEmuPage = 'ryujinx'
143 |     elif page == 'suyu':
144 |         config.setting.ui.lastOpenEmuPage = 'suyu'
145 |     else:
146 |         config.setting.ui.lastOpenEmuPage = 'yuzu'
147 |     logger.info(f'update lastOpenEmuPage to {config.setting.ui.lastOpenEmuPage}')
148 |     dump_config()
149 | 
150 | 
151 | def update_dark_state(dark: bool):
152 |     if dark is None:
153 |         dark = True
154 |     config.setting.ui.dark = dark
155 |     logger.info(f'update dark to {config.setting.ui.dark}')
156 |     dump_config()
157 | 
158 | 
159 | def update_setting(setting: Dict[str, object]):
160 |     logger.info(f'updating settings: {setting}')
161 |     config.setting = CommonSetting.from_dict(setting)
162 |     dump_config()
163 | 
164 | 
165 | __all__ = ['config', 'dump_config', 'YuzuConfig', 'current_version', 'RyujinxConfig', 'update_dark_state',
166 |            'update_last_open_emu_page', 'update_setting', 'user_agent', 'shared']
167 | 


--------------------------------------------------------------------------------
/doc/assets/yuzu_user_id.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/triwinds/ns-emu-tools/ab6a1a301ad524e85b1b5f2796f382bba384e915/doc/assets/yuzu_user_id.jpg


--------------------------------------------------------------------------------
/doc/dev.md:
--------------------------------------------------------------------------------
 1 | # Dev
 2 | 
 3 | ## 开发环境需求
 4 | 
 5 | - Python 3.12
 6 | - Node.js 20
 7 | 
 8 | ## 运行环境准备
 9 | 
10 | ### Step 1 构建前端 package
11 | 
12 | ```shell
13 | cd frontend
14 | bun install
15 | bun build
16 | ```
17 | 
18 | ### Step 2 安装 Python 依赖
19 | 
20 | ```shell
21 | # 通过 uv 安装 (推荐)
22 | uv sync
23 | 
24 | # 通过 pip 安装
25 | python -m venv venv
26 | venv\Scripts\activate
27 | pip install -r requirements.txt
28 | ```
29 | 
30 | ## 本地运行
31 | 
32 | ```shell
33 | poetry run python main.py
34 | ```
35 | 
36 | ## 开发与调试
37 | 
38 | ### 调试前端页面
39 | 
40 | 先启动后端服务
41 | ```shell
42 | uv run python ui.py
43 | ```
44 | 
45 | 然后另起一个终端启动 dev server
46 | ```shell
47 | cd frontend
48 | bun dev
49 | ```
50 | 
51 | ### 调试 Python 代码
52 | 
53 | 由于 gevent 会与 pydebugger 冲突,因此不建议在 eel 启动时调试 Python 代码。
54 | 
55 | 可以直接使用 pycharm 或 vscode 等 IDE 在 py 文件中通过 main 方法进行调试。
56 | 
57 | 


--------------------------------------------------------------------------------
/exception/common_exception.py:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 | class VersionNotFoundException(Exception):
 4 |     msg: str = ''
 5 |     target_version: str = ''
 6 |     branch: str = ''
 7 |     emu_type: str = ''
 8 | 
 9 |     def __init__(self, target_version, branch, emu_type):
10 |         self.target_version = target_version
11 |         self.branch = branch
12 |         self.emu_type = emu_type
13 |         self.msg = f'Fail to get release info of version [{target_version}] on branch [{branch}]'
14 |         super().__init__(self.msg)
15 | 
16 | 
17 | class Md5NotMatchException(Exception):
18 |     def __init__(self):
19 |         super().__init__('MD5 not match')
20 | 
21 | 
22 | class IgnoredException(RuntimeError):
23 |     def __init__(self, msg=''):
24 |         super().__init__(msg)
25 | 


--------------------------------------------------------------------------------
/exception/download_exception.py:
--------------------------------------------------------------------------------
 1 | 
 2 | class DownloadInterrupted(Exception):
 3 |     def __init__(self):
 4 |         super().__init__('Download has been interrupted')
 5 | 
 6 | 
 7 | class DownloadPaused(Exception):
 8 |     def __init__(self):
 9 |         super().__init__('Download has been paused')
10 | 
11 | 
12 | class DownloadNotCompleted(Exception):
13 |     status: str
14 |     name: str
15 | 
16 |     def __init__(self, name, status):
17 |         self.name = name
18 |         self.status = status
19 |         super().__init__(f'Download task [{name}] is not completed, status: {status}')
20 | 


--------------------------------------------------------------------------------
/exception/install_exception.py:
--------------------------------------------------------------------------------
1 | class FailToCopyFiles(Exception):
2 |     raw_exception: Exception
3 |     msg: str
4 | 
5 |     def __init__(self, raw_exception: Exception, msg):
6 |         self.raw_exception = raw_exception
7 |         self.msg = msg
8 |         super().__init__(msg)
9 | 


--------------------------------------------------------------------------------
/frontend/.browserslistrc:
--------------------------------------------------------------------------------
1 | > 1%
2 | last 2 versions
3 | not dead
4 | not ie 11
5 | 


--------------------------------------------------------------------------------
/frontend/.editorconfig:
--------------------------------------------------------------------------------
1 | [*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue}]
2 | charset = utf-8
3 | indent_size = 2
4 | indent_style = space
5 | insert_final_newline = true
6 | trim_trailing_whitespace = true
7 | 


--------------------------------------------------------------------------------
/frontend/.env.development:
--------------------------------------------------------------------------------
1 | VITE_EEL_BASE_URI=http://127.0.0.1:8888
2 | 


--------------------------------------------------------------------------------
/frontend/.env.production:
--------------------------------------------------------------------------------
1 | VITE_EEL_BASE_URI=.


--------------------------------------------------------------------------------
/frontend/.eslintrc-auto-import.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "globals": {
 3 |     "Component": true,
 4 |     "ComponentPublicInstance": true,
 5 |     "ComputedRef": true,
 6 |     "EffectScope": true,
 7 |     "ExtractDefaultPropTypes": true,
 8 |     "ExtractPropTypes": true,
 9 |     "ExtractPublicPropTypes": true,
10 |     "InjectionKey": true,
11 |     "PropType": true,
12 |     "Ref": true,
13 |     "VNode": true,
14 |     "WritableComputedRef": true,
15 |     "computed": true,
16 |     "createApp": true,
17 |     "customRef": true,
18 |     "defineAsyncComponent": true,
19 |     "defineComponent": true,
20 |     "effectScope": true,
21 |     "getCurrentInstance": true,
22 |     "getCurrentScope": true,
23 |     "h": true,
24 |     "inject": true,
25 |     "isProxy": true,
26 |     "isReactive": true,
27 |     "isReadonly": true,
28 |     "isRef": true,
29 |     "markRaw": true,
30 |     "nextTick": true,
31 |     "onActivated": true,
32 |     "onBeforeMount": true,
33 |     "onBeforeUnmount": true,
34 |     "onBeforeUpdate": true,
35 |     "onDeactivated": true,
36 |     "onErrorCaptured": true,
37 |     "onMounted": true,
38 |     "onRenderTracked": true,
39 |     "onRenderTriggered": true,
40 |     "onScopeDispose": true,
41 |     "onServerPrefetch": true,
42 |     "onUnmounted": true,
43 |     "onUpdated": true,
44 |     "provide": true,
45 |     "reactive": true,
46 |     "readonly": true,
47 |     "ref": true,
48 |     "resolveComponent": true,
49 |     "shallowReactive": true,
50 |     "shallowReadonly": true,
51 |     "shallowRef": true,
52 |     "toRaw": true,
53 |     "toRef": true,
54 |     "toRefs": true,
55 |     "toValue": true,
56 |     "triggerRef": true,
57 |     "unref": true,
58 |     "useAttrs": true,
59 |     "useCssModule": true,
60 |     "useCssVars": true,
61 |     "useRoute": true,
62 |     "useRouter": true,
63 |     "useSlots": true,
64 |     "watch": true,
65 |     "watchEffect": true,
66 |     "watchPostEffect": true,
67 |     "watchSyncEffect": true,
68 |     "DirectiveBinding": true,
69 |     "MaybeRef": true,
70 |     "MaybeRefOrGetter": true,
71 |     "onWatcherCleanup": true,
72 |     "useId": true,
73 |     "useModel": true,
74 |     "useTemplateRef": true
75 |   }
76 | }
77 | 


--------------------------------------------------------------------------------
/frontend/.gitignore:
--------------------------------------------------------------------------------
 1 | .DS_Store
 2 | node_modules
 3 | /dist
 4 | 
 5 | # local env files
 6 | .env.local
 7 | .env.*.local
 8 | 
 9 | # Log files
10 | npm-debug.log*
11 | yarn-debug.log*
12 | yarn-error.log*
13 | pnpm-debug.log*
14 | 
15 | # Editor directories and files
16 | .idea
17 | .vscode
18 | *.suo
19 | *.ntvs*
20 | *.njsproj
21 | *.sln
22 | *.sw?
23 | 


--------------------------------------------------------------------------------
/frontend/README.md:
--------------------------------------------------------------------------------
 1 | # Vuetify (Default)
 2 | 
 3 | This is the official scaffolding tool for Vuetify, designed to give you a head start in building your new Vuetify application. It sets up a base template with all the necessary configurations and standard directory structure, enabling you to begin development without the hassle of setting up the project from scratch.
 4 | 
 5 | ## ❗️ Important Links
 6 | 
 7 | - 📄 [Docs](https://vuetifyjs.com/)
 8 | - 🚨 [Issues](https://issues.vuetifyjs.com/)
 9 | - 🏬 [Store](https://store.vuetifyjs.com/)
10 | - 🎮 [Playground](https://play.vuetifyjs.com/)
11 | - 💬 [Discord](https://community.vuetifyjs.com)
12 | 
13 | ## 💿 Install
14 | 
15 | Set up your project using your preferred package manager. Use the corresponding command to install the dependencies:
16 | 
17 | | Package Manager                                                | Command        |
18 | |---------------------------------------------------------------|----------------|
19 | | [yarn](https://yarnpkg.com/getting-started)                   | `yarn install` |
20 | | [npm](https://docs.npmjs.com/cli/v7/commands/npm-install)     | `npm install`  |
21 | | [pnpm](https://pnpm.io/installation)                          | `pnpm install` |
22 | | [bun](https://bun.sh/#getting-started)                        | `bun install`  |
23 | 
24 | After completing the installation, your environment is ready for Vuetify development.
25 | 
26 | ## ✨ Features
27 | 
28 | - 🖼️ **Optimized Front-End Stack**: Leverage the latest Vue 3 and Vuetify 3 for a modern, reactive UI development experience. [Vue 3](https://v3.vuejs.org/) | [Vuetify 3](https://vuetifyjs.com/en/)
29 | - 🗃️ **State Management**: Integrated with [Pinia](https://pinia.vuejs.org/), the intuitive, modular state management solution for Vue.
30 | - 🚦 **Routing and Layouts**: Utilizes Vue Router for SPA navigation and vite-plugin-vue-layouts for organizing Vue file layouts. [Vue Router](https://router.vuejs.org/) | [vite-plugin-vue-layouts](https://github.com/JohnCampionJr/vite-plugin-vue-layouts)
31 | - 💻 **Enhanced Development Experience**: Benefit from TypeScript's static type checking and the ESLint plugin suite for Vue, ensuring code quality and consistency. [TypeScript](https://www.typescriptlang.org/) | [ESLint Plugin Vue](https://eslint.vuejs.org/)
32 | - ⚡ **Next-Gen Tooling**: Powered by Vite, experience fast cold starts and instant HMR (Hot Module Replacement). [Vite](https://vitejs.dev/)
33 | - 🧩 **Automated Component Importing**: Streamline your workflow with unplugin-vue-components, automatically importing components as you use them. [unplugin-vue-components](https://github.com/antfu/unplugin-vue-components)
34 | - 🛠️ **Strongly-Typed Vue**: Use vue-tsc for type-checking your Vue components, and enjoy a robust development experience. [vue-tsc](https://github.com/johnsoncodehk/volar/tree/master/packages/vue-tsc)
35 | 
36 | These features are curated to provide a seamless development experience from setup to deployment, ensuring that your Vuetify application is both powerful and maintainable.
37 | 
38 | ## 💡 Usage
39 | 
40 | This section covers how to start the development server and build your project for production.
41 | 
42 | ### Starting the Development Server
43 | 
44 | To start the development server with hot-reload, run the following command. The server will be accessible at [http://localhost:3000](http://localhost:3000):
45 | 
46 | ```bash
47 | yarn dev
48 | ```
49 | 
50 | (Repeat for npm, pnpm, and bun with respective commands.)
51 | 
52 | > Add NODE_OPTIONS='--no-warnings' to suppress the JSON import warnings that happen as part of the Vuetify import mapping. If you are on Node [v21.3.0](https://nodejs.org/en/blog/release/v21.3.0) or higher, you can change this to NODE_OPTIONS='--disable-warning=5401'. If you don't mind the warning, you can remove this from your package.json dev script.
53 | 
54 | ### Building for Production
55 | 
56 | To build your project for production, use:
57 | 
58 | ```bash
59 | yarn build
60 | ```
61 | 
62 | (Repeat for npm, pnpm, and bun with respective commands.)
63 | 
64 | Once the build process is completed, your application will be ready for deployment in a production environment.
65 | 
66 | ## 💪 Support Vuetify Development
67 | 
68 | This project is built with [Vuetify](https://vuetifyjs.com/en/), a UI Library with a comprehensive collection of Vue components. Vuetify is an MIT licensed Open Source project that has been made possible due to the generous contributions by our [sponsors and backers](https://vuetifyjs.com/introduction/sponsors-and-backers/). If you are interested in supporting this project, please consider:
69 | 
70 | - [Requesting Enterprise Support](https://support.vuetifyjs.com/)
71 | - [Sponsoring John on Github](https://github.com/users/johnleider/sponsorship)
72 | - [Sponsoring Kael on Github](https://github.com/users/kaelwd/sponsorship)
73 | - [Supporting the team on Open Collective](https://opencollective.com/vuetify)
74 | - [Becoming a sponsor on Patreon](https://www.patreon.com/vuetify)
75 | - [Becoming a subscriber on Tidelift](https://tidelift.com/subscription/npm/vuetify)
76 | - [Making a one-time donation with Paypal](https://paypal.me/vuetify)
77 | 
78 | ## 📑 License
79 | [MIT](http://opensource.org/licenses/MIT)
80 | 
81 | Copyright (c) 2016-present Vuetify, LLC
82 | 


--------------------------------------------------------------------------------
/frontend/env.d.ts:
--------------------------------------------------------------------------------
1 | /// <reference types="vite/client" />
2 | /// <reference types="unplugin-vue-router/client" />
3 | /// <reference types="vite-plugin-vue-layouts/client" />
4 | 


--------------------------------------------------------------------------------
/frontend/eslint.config.js:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * .eslint.js
 3 |  *
 4 |  * ESLint configuration file.
 5 |  */
 6 | 
 7 | import pluginVue from 'eslint-plugin-vue'
 8 | import vueTsEslintConfig from '@vue/eslint-config-typescript'
 9 | 
10 | export default [
11 |   {
12 |     name: 'app/files-to-lint',
13 |     files: ['**/*.{ts,mts,tsx,vue}'],
14 |   },
15 | 
16 |   {
17 |     name: 'app/files-to-ignore',
18 |     ignores: ['**/dist/**', '**/dist-ssr/**', '**/coverage/**'],
19 |   },
20 | 
21 |   ...pluginVue.configs['flat/recommended'],
22 |   ...vueTsEslintConfig(),
23 | 
24 |   {
25 |     rules: {
26 |       '@typescript-eslint/no-unused-expressions': [
27 |         'error',
28 |         {
29 |           allowShortCircuit: true,
30 |           allowTernary: true,
31 |         },
32 |       ],
33 |       'vue/multi-word-component-names': 'off',
34 |     }
35 |   }
36 | ]
37 | 


--------------------------------------------------------------------------------
/frontend/index.html:
--------------------------------------------------------------------------------
 1 | <!DOCTYPE html>
 2 | <html style="height: 100%; width: 100%; overflow: hidden;">
 3 | 
 4 | <head>
 5 |   <meta charset="UTF-8" />
 6 |   <link rel="icon" href="/favicon.ico" />
 7 |   <meta name="viewport" content="width=device-width, initial-scale=1.0" />
 8 |   <script type=text/javascript src="%VITE_EEL_BASE_URI%/eel.js"></script>
 9 |     <script>
10 |         if ('%VITE_EEL_BASE_URI%' !== '.') {
11 |             window.eel.set_host('%VITE_EEL_BASE_URI%')
12 |         }
13 |         window.eel.expose(appendConsoleMessage)
14 |         function appendConsoleMessage(msg) {
15 |             window.$bus.emit('APPEND_CONSOLE_MESSAGE', msg)
16 |         }
17 |     </script>
18 |     <title>NS EMU TOOLS</title>
19 | </head>
20 | 
21 | <body>
22 |   <div id="app"></div>
23 |   <script type="module" src="/src/main.ts"></script>
24 | </body>
25 | 
26 | </html>
27 | 


--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "name": "frontend",
 3 |   "private": true,
 4 |   "type": "module",
 5 |   "version": "0.0.0",
 6 |   "scripts": {
 7 |     "dev": "vite",
 8 |     "build": "run-p type-check \"build-only {@}\" --",
 9 |     "preview": "vite preview",
10 |     "build-only": "vite build",
11 |     "type-check": "vue-tsc --build --force",
12 |     "lint": "eslint . --fix"
13 |   },
14 |   "dependencies": {
15 |     "@fontsource-variable/jetbrains-mono": "^5.2.5",
16 |     "@fontsource/roboto": "^5.2.5",
17 |     "core-js": "^3.37.1",
18 |     "isomorphic-dompurify": "^2.22.0",
19 |     "marked": "^15.0.7",
20 |     "mitt": "^3.0.1",
21 |     "roboto-fontface": "*",
22 |     "vue": "^3.4.31",
23 |     "vuetify": "^3.6.14"
24 |   },
25 |   "devDependencies": {
26 |     "@eslint/js": "^9.14.0",
27 |     "@mdi/js": "^7.4.47",
28 |     "@tsconfig/node22": "^22.0.0",
29 |     "@types/node": "^22.9.0",
30 |     "@vitejs/plugin-vue": "^5.1.4",
31 |     "@vue/eslint-config-typescript": "^14.1.3",
32 |     "@vue/tsconfig": "^0.5.1",
33 |     "eslint": "^9.14.0",
34 |     "eslint-plugin-vue": "^9.30.0",
35 |     "npm-run-all2": "^7.0.1",
36 |     "pinia": "^2.1.7",
37 |     "sass": "1.77.8",
38 |     "sass-embedded": "^1.77.8",
39 |     "typescript": "~5.6.3",
40 |     "unplugin-auto-import": "^0.17.6",
41 |     "unplugin-fonts": "^1.1.1",
42 |     "unplugin-vue-components": "^0.27.2",
43 |     "unplugin-vue-router": "^0.10.0",
44 |     "vite": "^5.4.10",
45 |     "vite-plugin-vue-layouts": "^0.11.0",
46 |     "vite-plugin-vuetify": "^2.0.3",
47 |     "vue-router": "^4.4.0",
48 |     "vue-tsc": "^2.1.10"
49 |   }
50 | }
51 | 


--------------------------------------------------------------------------------
/frontend/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/triwinds/ns-emu-tools/ab6a1a301ad524e85b1b5f2796f382bba384e915/frontend/public/favicon.ico


--------------------------------------------------------------------------------
/frontend/src/App.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <router-view />
 3 | </template>
 4 | 
 5 | <script lang="ts" setup>
 6 | import {onMounted} from "vue";
 7 | import {useConsoleDialogStore} from "@/stores/ConsoleDialogStore";
 8 | 
 9 | const cds = useConsoleDialogStore()
10 | let pendingWriteSize = false
11 | 
12 | onMounted(() => {
13 |   if (import.meta.env.NODE_ENV !== 'development') {
14 |     console.log('setupWebsocketConnectivityCheck')
15 |     setupWebsocketConnectivityCheck()
16 |   }
17 |   window.addEventListener('resize', rememberWindowSize);
18 | })
19 | 
20 | function rememberWindowSize() {
21 |   if (!pendingWriteSize) {
22 |     pendingWriteSize = true
23 |     setTimeout(() => {
24 |       pendingWriteSize = false
25 |       window.eel.update_window_size(window.outerWidth, window.outerHeight)()
26 |     }, 1000)
27 |   }
28 | }
29 | 
30 | function setupWebsocketConnectivityCheck() {
31 |   setInterval(() => {
32 |     try {
33 |       let ws = window.eel._websocket
34 |       if (ws.readyState === ws.CLOSED || ws.readyState === ws.CLOSING) {
35 |         cds.cleanAndShowConsoleDialog()
36 |         cds.appendConsoleMessage('程序后端连接出错, 请关闭当前页面并重启程序以解决这个问题。')
37 |       }
38 |     } catch (e) {
39 |       console.log(e)
40 |       cds.cleanAndShowConsoleDialog()
41 |       cds.appendConsoleMessage('程序后端连接出错, 请关闭当前页面并重启程序以解决这个问题。')
42 |     }
43 |   }, 5000)
44 | }
45 | </script>
46 | 
47 | <style>
48 | html ::-webkit-scrollbar {
49 |   width: 0 ;
50 |   height: 0 ;
51 | }
52 | div::-webkit-resizer, div::-webkit-scrollbar-thumb {
53 |   background: #aaa;
54 |   border-radius: 3px;
55 | }
56 | 
57 | div::-webkit-scrollbar {
58 |   width: 5px !important;
59 |   height: 5px !important;
60 | }
61 | 
62 | div::-webkit-scrollbar-corner, div ::-webkit-scrollbar-track {
63 |   background: transparent !important;
64 | }
65 | 
66 | div::-webkit-resizer, div ::-webkit-scrollbar-thumb {
67 |   background: #aaa;
68 |   border-radius: 3px;
69 | }
70 | 
71 | div::-webkit-scrollbar-corner, div ::-webkit-scrollbar-track {
72 |   background: transparent !important;
73 | }
74 | 
75 | a {
76 |   cursor: pointer;
77 | }
78 | </style>
79 | 


--------------------------------------------------------------------------------
/frontend/src/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/triwinds/ns-emu-tools/ab6a1a301ad524e85b1b5f2796f382bba384e915/frontend/src/assets/icon.png


--------------------------------------------------------------------------------
/frontend/src/assets/ryujinx.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/triwinds/ns-emu-tools/ab6a1a301ad524e85b1b5f2796f382bba384e915/frontend/src/assets/ryujinx.webp


--------------------------------------------------------------------------------
/frontend/src/assets/suyu.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/triwinds/ns-emu-tools/ab6a1a301ad524e85b1b5f2796f382bba384e915/frontend/src/assets/suyu.png


--------------------------------------------------------------------------------
/frontend/src/assets/telegram.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/triwinds/ns-emu-tools/ab6a1a301ad524e85b1b5f2796f382bba384e915/frontend/src/assets/telegram.webp


--------------------------------------------------------------------------------
/frontend/src/assets/telegram_black.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/triwinds/ns-emu-tools/ab6a1a301ad524e85b1b5f2796f382bba384e915/frontend/src/assets/telegram_black.webp


--------------------------------------------------------------------------------
/frontend/src/assets/yuzu.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/triwinds/ns-emu-tools/ab6a1a301ad524e85b1b5f2796f382bba384e915/frontend/src/assets/yuzu.webp


--------------------------------------------------------------------------------
/frontend/src/components.d.ts:
--------------------------------------------------------------------------------
 1 | /* eslint-disable */
 2 | // @ts-nocheck
 3 | // Generated by unplugin-vue-components
 4 | // Read more: https://github.com/vuejs/core/pull/3399
 5 | export {}
 6 | 
 7 | /* prettier-ignore */
 8 | declare module 'vue' {
 9 |   export interface GlobalComponents {
10 |     ChangeLogDialog: typeof import('./components/ChangeLogDialog.vue')['default']
11 |     ConsoleDialog: typeof import('./components/ConsoleDialog.vue')['default']
12 |     DialogTitle: typeof import('./components/DialogTitle.vue')['default']
13 |     FaqGroup: typeof import('./components/FaqGroup.vue')['default']
14 |     MarkdownContentBox: typeof import('./components/MarkdownContentBox.vue')['default']
15 |     NewVersionDialog: typeof import('./components/NewVersionDialog.vue')['default']
16 |     OtherLinkItem: typeof import('./components/OtherLinkItem.vue')['default']
17 |     RouterLink: typeof import('vue-router')['RouterLink']
18 |     RouterView: typeof import('vue-router')['RouterView']
19 |     SimplePage: typeof import('./components/SimplePage.vue')['default']
20 |     SpeedDial: typeof import('./components/SpeedDial.vue')['default']
21 |     YuzuSaveCommonPart: typeof import('./components/YuzuSaveCommonPart.vue')['default']
22 |     YuzuSaveRestoreTab: typeof import('./components/YuzuSaveRestoreTab.vue')['default']
23 |   }
24 | }
25 | 


--------------------------------------------------------------------------------
/frontend/src/components/ChangeLogDialog.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 | <v-dialog
 3 |     v-model="dialog"
 4 |     max-width="900"
 5 |   >
 6 |     <template v-slot:activator="{ props }">
 7 |       <slot name="activator" v-bind:props="props">
 8 | 
 9 |       </slot>
10 |     </template>
11 | 
12 |     <v-card>
13 |       <dialog-title>
14 |         更新日志
15 |       </dialog-title>
16 | 
17 |       <div class="change-log-content" style=" padding: 15px; overflow-y: auto; max-height: 50vh">
18 |         <slot name="content"></slot>
19 |       </div>
20 | 
21 |       <v-divider></v-divider>
22 | 
23 |       <v-card-actions>
24 |         <v-spacer></v-spacer>
25 |         <v-btn
26 |           color="primary"
27 |           variant="text"
28 |           @click="dialog = false"
29 |         >
30 |           关闭
31 |         </v-btn>
32 |       </v-card-actions>
33 |     </v-card>
34 |   </v-dialog>
35 | </template>
36 | 
37 | <script setup lang="ts">
38 | import {ref} from "vue";
39 | import DialogTitle from "@/components/DialogTitle.vue";
40 | 
41 | let dialog = ref(false)
42 | </script>
43 | 
44 | <style>
45 | .change-log-content li {
46 |   margin-left: 30px !important;
47 | }
48 | </style>
49 | 


--------------------------------------------------------------------------------
/frontend/src/components/ConsoleDialog.vue:
--------------------------------------------------------------------------------
  1 | <template>
  2 |   <div class="text-center">
  3 |     <v-dialog
  4 |         v-model="consoleDialogStore.dialogFlag"
  5 |         max-width="900"
  6 |         :persistent="consoleDialogStore.persistentConsoleDialog"
  7 |     >
  8 | 
  9 |       <v-card>
 10 |         <dialog-title>
 11 |           控制台日志
 12 |         </dialog-title>
 13 | 
 14 |         <div style="padding-left: 10px; padding-right: 10px; padding-top: 10px;" class="flex-grow-0">
 15 |           <textarea id="consoleBox" :value="logText" readonly rows="12"></textarea>
 16 |         </div>
 17 | 
 18 |         <v-divider></v-divider>
 19 | 
 20 |         <v-card-actions>
 21 |           <v-spacer></v-spacer>
 22 |           <v-btn
 23 |               color="primary"
 24 |               variant="text"
 25 |               @click="pauseDownload"
 26 |               v-if="consoleDialogStore.persistentConsoleDialog"
 27 |           >
 28 |             暂停下载任务
 29 |           </v-btn>
 30 |           <v-btn
 31 |               color="primary"
 32 |               variant="text"
 33 |               @click="stopDownload"
 34 |               v-if="consoleDialogStore.persistentConsoleDialog"
 35 |           >
 36 |             中断并删除下载任务
 37 |           </v-btn>
 38 |           <v-btn
 39 |               color="primary"
 40 |               variant="text"
 41 |               @click="closeDialog"
 42 |               :disabled="consoleDialogStore.persistentConsoleDialog"
 43 |           >
 44 |             关闭
 45 |           </v-btn>
 46 |         </v-card-actions>
 47 |       </v-card>
 48 |     </v-dialog>
 49 |   </div>
 50 | </template>
 51 | 
 52 | <script lang="ts" setup>
 53 | import {useConsoleDialogStore} from "@/stores/ConsoleDialogStore";
 54 | import type {CommonResponse} from "@/types";
 55 | import {computed, nextTick, onUpdated} from "vue";
 56 | import DialogTitle from "@/components/DialogTitle.vue";
 57 | 
 58 | const consoleDialogStore = useConsoleDialogStore()
 59 | 
 60 | function closeDialog() {
 61 |   consoleDialogStore.dialogFlag = false
 62 | }
 63 | function stopDownload() {
 64 |   window.eel.stop_download()((resp: CommonResponse) => {
 65 |     console.log(resp)
 66 |   })
 67 | }
 68 | function pauseDownload() {
 69 |   window.eel.pause_download()((resp: CommonResponse) => {
 70 |     console.log(resp)
 71 |   })
 72 | }
 73 | 
 74 | let logText = computed(() => {
 75 |   let text = ''
 76 |   for (let line of consoleDialogStore.consoleMessages) {
 77 |     text += line + '\n'
 78 |   }
 79 |   return text
 80 | })
 81 | 
 82 | onUpdated(() => {
 83 |   nextTick(() => {
 84 |     let consoleBox = document.getElementById("consoleBox")
 85 |     if (consoleBox) {
 86 |       consoleBox.scrollTop = consoleBox.scrollHeight
 87 |     }
 88 |   })
 89 | })
 90 | </script>
 91 | 
 92 | <style scoped>
 93 | #consoleBox {
 94 |   background-color: #000;
 95 |   width: 100%;
 96 |   color: white;
 97 |   overflow-x: scroll;
 98 |   overflow-y: scroll;
 99 |   resize: none;
100 |   padding: 10px;
101 |   font-family: 'JetBrains Mono Variable',sans-serif !important;
102 | }
103 | 
104 | #consoleBox::-webkit-resizer, #consoleBox::-webkit-scrollbar-thumb {
105 |   background: #aaa;
106 |   border-radius: 3px;
107 | }
108 | 
109 | #consoleBox::-webkit-scrollbar {
110 |   width: 5px !important;
111 |   height: 5px !important;
112 | }
113 | 
114 | #consoleBox::-webkit-scrollbar-corner, #consoleBox ::-webkit-scrollbar-track {
115 |   background: transparent !important;
116 | }
117 | 
118 | #consoleBox::-webkit-resizer, #consoleBox ::-webkit-scrollbar-thumb {
119 |   background: #aaa;
120 |   border-radius: 3px;
121 | }
122 | 
123 | #consoleBox::-webkit-scrollbar-corner, #consoleBox ::-webkit-scrollbar-track {
124 |   background: transparent !important;
125 | }
126 | </style>
127 | 


--------------------------------------------------------------------------------
/frontend/src/components/DialogTitle.vue:
--------------------------------------------------------------------------------
 1 | <script setup lang="ts">
 2 | 
 3 | </script>
 4 | 
 5 | <template>
 6 | <v-card-title class="text-h5 bg-primary text-white" style="height: 64px; padding-top: 18px">
 7 |   <slot></slot>
 8 | </v-card-title>
 9 | </template>
10 | 
11 | <style scoped>
12 | 
13 | </style>
14 | 


--------------------------------------------------------------------------------
/frontend/src/components/FaqGroup.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <v-list-group>
 3 |     <template v-slot:activator="{props}">
 4 |       <v-list-item v-bind="props" title="">
 5 |         <span class="text-info" style="font-size: 22px">
 6 |         <slot name="title"></slot>
 7 |         </span>
 8 |       </v-list-item>
 9 |     </template>
10 |     <v-list-item title="">
11 |       <slot name="content"></slot>
12 |     </v-list-item>
13 | 
14 |   </v-list-group>
15 | </template>
16 | 
17 | <script setup lang="ts">
18 | 
19 | </script>
20 | 
21 | <style scoped>
22 | 
23 | </style>
24 | 


--------------------------------------------------------------------------------
/frontend/src/components/MarkdownContentBox.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <div style="padding: 15px;">
 3 |     <div id="text-box" v-html="mdHtml">
 4 | 
 5 |     </div>
 6 |   </div>
 7 | </template>
 8 | 
 9 | <script setup lang="ts">
10 | import {nextTick, onMounted, onUpdated, ref} from "vue";
11 | import md from "@/utils/markdown";
12 | import {openUrlWithDefaultBrowser} from "@/utils/common";
13 | 
14 | let mdHtml = ref('')
15 | const props = defineProps(['content'])
16 | 
17 | onMounted(() => {
18 |   mdHtml.value = md.parse(props.content)
19 | })
20 | 
21 | onUpdated(() => {
22 |   nextTick(() => {
23 |     let aTags = document.getElementById("text-box")!.getElementsByTagName('a')
24 |       for (let aTag of aTags) {
25 |         let url = aTag.href
26 |         aTag.href = 'javascript:;'
27 |         aTag.onclick = () => {
28 |           openUrlWithDefaultBrowser(url)
29 |         }
30 |       }
31 |       let infoTags = [] as HTMLElement[]
32 |       infoTags.push(...document.getElementById("text-box")!.getElementsByTagName('strong'))
33 |       infoTags.push(...document.getElementById("text-box")!.getElementsByTagName('code'))
34 |       for (let infoTag of infoTags) {
35 |         infoTag.classList.add("text-info")
36 |         infoTag.style.fontFamily = "'JetBrains Mono Variable', sans-serif"
37 |       }
38 |   })
39 | })
40 | </script>
41 | 
42 | <style >
43 | #text-box details > p {
44 |   margin-left: 30px;
45 | }
46 | </style>
47 | 


--------------------------------------------------------------------------------
/frontend/src/components/NewVersionDialog.vue:
--------------------------------------------------------------------------------
  1 | <template>
  2 | <div class="text-center">
  3 |     <v-dialog
  4 |       v-model="dialog"
  5 |       width="850"
  6 |     >
  7 |       <v-card>
  8 |         <v-card-title class="text-h5 bg-primary text-white">
  9 |           {{ configStore.hasNewVersion ? '更新日志' : '版本检测' }}
 10 |         </v-card-title>
 11 | 
 12 |         <div style="padding: 15px;">
 13 |           <p class="text-h6 text--primary" v-show="!configStore.hasNewVersion">当前版本已经是最新版本</p>
 14 |           <div v-show="configStore.hasNewVersion" >
 15 | <!--            <p class="text-h6 text&#45;&#45;primary">[{{newVersion}}] 更新内容:</p>-->
 16 |             <div v-html="releaseDescriptionHtml" class="text--primary"
 17 |                  style="max-height: 300px; overflow-y: auto"></div>
 18 |           </div>
 19 |         </div>
 20 | 
 21 |         <v-divider></v-divider>
 22 | 
 23 |         <v-card-actions v-show="!configStore.hasNewVersion">
 24 |           <v-spacer></v-spacer>
 25 |           <v-btn
 26 |             color="primary"
 27 |             variant="text"
 28 |             @click="dialog = false"
 29 |           >
 30 |             OK
 31 |           </v-btn>
 32 |         </v-card-actions>
 33 |         <v-card-actions v-show="configStore.hasNewVersion">
 34 |           <v-spacer></v-spacer>
 35 |           <v-btn
 36 |             color="primary"
 37 |             variant="text"
 38 |             @click="updateNET"
 39 |           >
 40 |             自动更新
 41 |           </v-btn>
 42 |           <v-btn
 43 |             color="primary"
 44 |             variant="text"
 45 |             @click="downloadNET"
 46 |           >
 47 |             下载最新版本
 48 |           </v-btn>
 49 |           <v-btn
 50 |             color="primary"
 51 |             variant="text"
 52 |             @click="openReleasePage"
 53 |           >
 54 |             前往发布页
 55 |           </v-btn>
 56 |           <v-btn
 57 |             color="primary"
 58 |             variant="text"
 59 |             @click="dialog = false"
 60 |           >
 61 |             取消
 62 |           </v-btn>
 63 |         </v-card-actions>
 64 |       </v-card>
 65 |     </v-dialog>
 66 |   </div>
 67 | </template>
 68 | 
 69 | <script setup lang="ts">
 70 | import {onMounted, ref} from "vue";
 71 | import {useConfigStore} from "@/stores/ConfigStore";
 72 | import {openUrlWithDefaultBrowser} from "@/utils/common";
 73 | import md from "@/utils/markdown";
 74 | import {useConsoleDialogStore} from "@/stores/ConsoleDialogStore";
 75 | import type {CommonResponse} from "@/types";
 76 | import {useEmitter} from "@/plugins/mitt";
 77 | 
 78 | let dialog = ref(false)
 79 | const configStore = useConfigStore()
 80 | let newVersion = ref('')
 81 | let releaseDescriptionHtml = ref('')
 82 | const cds = useConsoleDialogStore()
 83 | const emitter = useEmitter()
 84 | 
 85 | onMounted(() => {
 86 |   emitter.on('showNewVersionDialog', showNewVersionDialog)
 87 | })
 88 | 
 89 | function showNewVersionDialog(info: any) {
 90 |   dialog.value = true
 91 |   newVersion.value = info.latestVersion
 92 |   if (configStore.hasNewVersion) {
 93 |     loadReleaseDescription()
 94 |   }
 95 | }
 96 | 
 97 | function openReleasePage() {
 98 |   dialog.value = false
 99 |   if (configStore.hasNewVersion) {
100 |     openUrlWithDefaultBrowser('https://github.com/triwinds/ns-emu-tools/releases');
101 |   }
102 | }
103 | 
104 | function loadReleaseDescription() {
105 |   window.eel.load_change_log()((resp: CommonResponse) => {
106 |     if (resp.code === 0) {
107 |       let rawMd = resp.data.replace('# Change Log\n\n', '')
108 |       releaseDescriptionHtml.value = md.parse(rawMd)
109 |     } else {
110 |       releaseDescriptionHtml.value = '<p>加载失败</p>'
111 |     }
112 |   })
113 | }
114 | 
115 | function downloadNET() {
116 |   cds.cleanAndShowConsoleDialog()
117 |   window.eel.download_net_by_tag(newVersion.value)((resp: CommonResponse) => {
118 |     if (resp.code === 0) {
119 |       cds.appendConsoleMessage('NET 下载完成')
120 |     } else {
121 |       cds.appendConsoleMessage(resp.msg)
122 |       cds.appendConsoleMessage('NET 下载失败')
123 |     }
124 |   })
125 | }
126 | 
127 | async function updateNET() {
128 |   cds.cleanAndShowConsoleDialog()
129 |   window.eel.update_net_by_tag(newVersion.value)
130 | }
131 | </script>
132 | 
133 | <style scoped>
134 | 
135 | </style>
136 | 


--------------------------------------------------------------------------------
/frontend/src/components/OtherLinkItem.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <div>
 3 |     <v-card
 4 |         elevation="4"
 5 |         rounded="shaped"
 6 |     >
 7 |       <v-card-title>
 8 |         <slot name="title"></slot>
 9 |       </v-card-title>
10 |       <v-card-subtitle>
11 |         <slot name="subtitle"></slot>
12 |       </v-card-subtitle>
13 |       <v-card-text class="text-body-1">
14 |         <slot name="description"></slot>
15 |       </v-card-text>
16 |       <v-card-actions>
17 |         <slot name="actions"></slot>
18 |       </v-card-actions>
19 |     </v-card>
20 |   </div>
21 | </template>
22 | 
23 | <script setup lang="ts">
24 | 
25 | </script>
26 | 
27 | <style scoped>
28 | 
29 | </style>
30 | 


--------------------------------------------------------------------------------
/frontend/src/components/README.md:
--------------------------------------------------------------------------------
 1 | # Components
 2 | 
 3 | Vue template files in this folder are automatically imported.
 4 | 
 5 | ## 🚀 Usage
 6 | 
 7 | Importing is handled by [unplugin-vue-components](https://github.com/unplugin/unplugin-vue-components). This plugin automatically imports `.vue` files created in the `src/components` directory, and registers them as global components. This means that you can use any component in your application without having to manually import it.
 8 | 
 9 | The following example assumes a component located at `src/components/MyComponent.vue`:
10 | 
11 | ```vue
12 | <template>
13 |   <div>
14 |     <MyComponent />
15 |   </div>
16 | </template>
17 | 
18 | <script lang="ts" setup>
19 |   //
20 | </script>
21 | ```
22 | 
23 | When your template is rendered, the component's import will automatically be inlined, which renders to this:
24 | 
25 | ```vue
26 | <template>
27 |   <div>
28 |     <MyComponent />
29 |   </div>
30 | </template>
31 | 
32 | <script lang="ts" setup>
33 |   import MyComponent from '@/components/MyComponent.vue'
34 | </script>
35 | ```
36 | 


--------------------------------------------------------------------------------
/frontend/src/components/SimplePage.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <v-container>
 3 |     <v-row>
 4 |       <v-spacer v-show="display.mdAndUp.value"></v-spacer>
 5 |       <v-col md="11" lg="9" xl="8" style="padding-bottom: 0">
 6 |         <slot></slot>
 7 |       </v-col>
 8 |       <v-spacer v-show="display.mdAndUp.value"></v-spacer>
 9 |     </v-row>
10 |   </v-container>
11 | </template>
12 | 
13 | <script lang="ts" setup>
14 | import type { DisplayInstance } from 'vuetify'
15 | import {useDisplay} from "vuetify";
16 | 
17 | const display = useDisplay() as DisplayInstance
18 | </script>
19 | 
20 | <style scoped>
21 | 
22 | </style>
23 | 


--------------------------------------------------------------------------------
/frontend/src/components/SpeedDial.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <div id="speed-dial" class="text-center">
 3 |     <v-menu open-delay="0" close-delay="100"
 4 |             open-on-hover
 5 |             transition="slide-y-reverse-transition">
 6 |       <template v-slot:activator="{ props }">
 7 |         <v-btn
 8 |             color="primary"
 9 |             v-bind="props"
10 |             :icon="mdiCreation"
11 |         />
12 |       </template>
13 |       <div class="btn-item">
14 |         <v-btn
15 |             color="grey-darken-3"
16 |             :icon="mdiGithub"
17 |             @click="openUrlWithDefaultBrowser('https://github.com/triwinds/ns-emu-tools')"
18 |         />
19 |       </div>
20 |       <div class="btn-item">
21 |         <v-btn
22 |             color="blue-darken-1"
23 |             :icon="mdiGithub"
24 |             @click="openUrlWithDefaultBrowser('https://t.me/+mxI34BRClLUwZDcx')"
25 |         >
26 |           <v-img src="@/assets/telegram.webp" height="22" width="22"></v-img>
27 |         </v-btn>
28 |       </div>
29 | 
30 |     </v-menu>
31 |   </div>
32 | </template>
33 | 
34 | <script setup lang="ts">
35 | import {mdiCreation, mdiGithub} from '@mdi/js'
36 | import {openUrlWithDefaultBrowser} from "@/utils/common";
37 | </script>
38 | 
39 | <style scoped>
40 | #speed-dial {
41 |   position: absolute;
42 |   right: 24px;
43 |   bottom: 24px;
44 | }
45 | 
46 | .btn-item {
47 |   padding-bottom: 12px;
48 | }
49 | </style>
50 | 


--------------------------------------------------------------------------------
/frontend/src/components/YuzuSaveCommonPart.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <v-container>
 3 |     <v-row>
 4 |       <v-col cols="7">
 5 |         <v-text-field label="备份文件存放的文件夹" variant="underlined" hide-details readonly v-model="yuzuSaveStore.yuzuSaveBackupPath"></v-text-field>
 6 |       </v-col>
 7 |       <v-col cols="2">
 8 |         <v-btn color="info" min-width="110" size="large" variant="outlined" @click="askAndUpdateYuzuBackupPath">选择文件夹</v-btn>
 9 |       </v-col>
10 |       <v-col cols="2">
11 |         <v-btn color="success" min-width="110" size="large" variant="outlined" @click="openYuzuBackupFolder">打开文件夹</v-btn>
12 |       </v-col>
13 |     </v-row>
14 |     <v-row>
15 |       <v-col>
16 |         <v-autocomplete v-model="yuzuSaveStore.selectedUser" :items="yuzuSaveStore.userList" label="选择模拟器用户 ID"
17 |                         hint="模拟器的用户 ID 可以在菜单 模拟->设置->系统->配置 中查看" persistent-hint
18 |                         item-title="user_id" item-value="folder" variant="underlined"
19 |                         style="margin-bottom: 20px"></v-autocomplete>
20 |       </v-col>
21 |     </v-row>
22 | 
23 |   </v-container>
24 | </template>
25 | 
26 | <script lang="ts" setup>
27 | import {onMounted} from "vue";
28 | import type {CommonResponse} from "@/types";
29 | import {useYuzuSaveStore} from "@/stores/YuzuSaveStore";
30 | 
31 | const yuzuSaveStore = useYuzuSaveStore()
32 | onMounted(() => {
33 |   window.eel.get_users_in_save()((resp: CommonResponse) => {
34 |     if (resp.code === 0) {
35 |       yuzuSaveStore.userList = resp.data
36 |     }
37 |   })
38 |   loadYuzuSaveBackupPath()
39 | })
40 | async function askAndUpdateYuzuBackupPath() {
41 |   await window.eel.ask_and_update_yuzu_save_backup_folder()()
42 |   await loadYuzuSaveBackupPath()
43 | }
44 | 
45 | async function loadYuzuSaveBackupPath() {
46 |   let resp = await window.eel.get_storage()()
47 |   yuzuSaveStore.yuzuSaveBackupPath = resp.data.yuzu_save_backup_path
48 | }
49 | 
50 | function openYuzuBackupFolder() {
51 |   window.eel.open_yuzu_save_backup_folder()()
52 | }
53 | </script>
54 | 
55 | <style scoped>
56 | 
57 | </style>
58 | 


--------------------------------------------------------------------------------
/frontend/src/components/YuzuSaveRestoreTab.vue:
--------------------------------------------------------------------------------
  1 | <template>
  2 |   <v-card variant="flat" style="min-height: 400px">
  3 |     <YuzuSaveCommonPart/>
  4 |     <v-card-text v-if="backupList.length === 0" class="text-center text-h2">无备份存档</v-card-text>
  5 |     <div v-else>
  6 |       <span class="text-h5 text-primary" style="padding-left: 15px">备份存档</span>
  7 |       <v-virtual-scroll :items="backupList" :height="backupItemBoxHeight" item-height="106">
  8 |         <template v-slot:default="{ item }">
  9 |           <v-list-item three-line>
 10 |             <v-list-item-title class="text-info">{{ concatGameName(item) }}</v-list-item-title>
 11 |             <v-list-item-subtitle>
 12 |               <v-icon size="20">{{ mdiClockTimeNineOutline }}</v-icon>
 13 |               备份时间: {{ new Date(item.bak_time).toLocaleString() }}
 14 |             </v-list-item-subtitle>
 15 |             <v-list-item-subtitle>
 16 |               <v-icon size="20">{{ mdiFileDocumentOutline }}</v-icon>
 17 |               文件名: {{ item.filename }}
 18 |             </v-list-item-subtitle>
 19 |             <template v-slot:append>
 20 |               <div>
 21 |                 <v-btn variant="outlined" color="warning" @click="restoreBackup(item.path)">还原备份</v-btn>
 22 |                 <br/>
 23 |                 <v-btn variant="outlined" color="error" @click="deletePath(item.path)">删除备份</v-btn>
 24 |               </div>
 25 |             </template>
 26 |           </v-list-item>
 27 |           <v-divider/>
 28 |         </template>
 29 |       </v-virtual-scroll>
 30 |     </div>
 31 |     <v-dialog v-model="restoreWaringDialog" max-width="600">
 32 |       <v-card>
 33 |         <v-card-title class="primary text--white text-h5">提示</v-card-title>
 34 |         <v-divider/>
 35 |         <v-card-text class="text--primary text-body-1" style="padding-top: 15px">
 36 |           还原时会先清空相应游戏的存档文件夹,然后再将备份的文件放入。<br/>是否继续还原存档?
 37 |         </v-card-text>
 38 |         <v-divider></v-divider>
 39 | 
 40 |         <v-card-actions>
 41 |           <v-spacer></v-spacer>
 42 |           <v-btn
 43 |               color="primary"
 44 |               variant="text"
 45 |               @click="restoreWaringDialog = false"
 46 |           >
 47 |             取消还原
 48 |           </v-btn>
 49 |           <v-btn
 50 |               color="primary"
 51 |               variant="text"
 52 |               @click="summitRestoreRequest"
 53 |           >
 54 |             执行还原
 55 |           </v-btn>
 56 |         </v-card-actions>
 57 |       </v-card>
 58 |     </v-dialog>
 59 |   </v-card>
 60 | </template>
 61 | 
 62 | <script setup lang="ts">
 63 | import {mdiClockTimeNineOutline, mdiFileDocumentOutline} from '@mdi/js';
 64 | import {onMounted, onUnmounted, ref} from "vue";
 65 | import {useAppStore} from "@/stores/app";
 66 | import {useYuzuSaveStore} from "@/stores/YuzuSaveStore";
 67 | import {useConsoleDialogStore} from "@/stores/ConsoleDialogStore";
 68 | import type {CommonResponse, YuzuSaveBackupListItem} from "@/types";
 69 | import {useEmitter} from "@/plugins/mitt";
 70 | import YuzuSaveCommonPart from "@/components/YuzuSaveCommonPart.vue";
 71 | 
 72 | let backupList = ref<YuzuSaveBackupListItem[]>([])
 73 | let backupItemBoxHeight = ref(window.innerHeight - 420)
 74 | let restoreWaringDialog = ref(false)
 75 | let restoreBackupFile = ref('')
 76 | const appStore = useAppStore()
 77 | const yuzuSaveStore = useYuzuSaveStore()
 78 | const cds = useConsoleDialogStore()
 79 | const emitter = useEmitter()
 80 | 
 81 | onMounted(() => {
 82 |   loadAllYuzuBackups()
 83 |   window.addEventListener('resize', updateBackupItemBoxHeight);
 84 |   emitter.on('yuzuSave:tabChange', (tab) => {
 85 |     loadAllYuzuBackups()
 86 |   })
 87 | })
 88 | 
 89 | onUnmounted(() => {
 90 |   window.removeEventListener('resize', updateBackupItemBoxHeight)
 91 |   emitter.off('yuzuSave:tabChange')
 92 | })
 93 | 
 94 | function loadAllYuzuBackups() {
 95 |   window.eel.list_all_yuzu_backups()((resp: CommonResponse) => {
 96 |     backupList.value = resp.data
 97 |     updateBackupItemBoxHeight()
 98 |     appStore.loadGameData().then(gameData => {
 99 |       let nl = []
100 |       for (let item of backupList.value) {
101 |         item.game_name = gameData[item.title_id]
102 |         nl.push(item)
103 |       }
104 |       backupList.value = nl;
105 |     })
106 |   })
107 | }
108 | 
109 | function updateBackupItemBoxHeight() {
110 |   let maxHeight = window.innerHeight - 420
111 |   backupItemBoxHeight.value = Math.min(backupList.value.length * 106, maxHeight)
112 | }
113 | 
114 | function restoreBackup(filepath: string) {
115 |   if (yuzuSaveStore.selectedUser === '') {
116 |     cds.cleanAndShowConsoleDialog()
117 |     cds.appendConsoleMessage('请先选择一个模拟器用户')
118 |     return
119 |   }
120 |   restoreBackupFile.value = filepath
121 |   restoreWaringDialog.value = true
122 | }
123 | 
124 | function summitRestoreRequest() {
125 |   restoreWaringDialog.value = false
126 |   cds.cleanAndShowConsoleDialog()
127 |   window.eel.restore_yuzu_save_from_backup(yuzuSaveStore.selectedUser, restoreBackupFile.value)()
128 | }
129 | 
130 | function deletePath(path: string) {
131 |   window.eel.delete_path(path)((resp: CommonResponse) => {
132 |     if (resp.code === 0) {
133 |       loadAllYuzuBackups()
134 |     }
135 |   })
136 | }
137 | 
138 | function concatGameName(item: YuzuSaveBackupListItem) {
139 |   let gameName = item.game_name ? item.game_name : appStore.gameDataInited ?
140 |       '未知游戏 - ' + item.title_id : '游戏信息加载中...'
141 |   return gameName
142 | }
143 | </script>
144 | 
145 | <style scoped>
146 | 
147 | </style>
148 | 


--------------------------------------------------------------------------------
/frontend/src/layouts/AppBar.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <v-app-bar flat color="primary">
 3 |     <v-app-bar-nav-icon class="white--text" @click="triggerDrawer"></v-app-bar-nav-icon>
 4 |     <v-app-bar-title>
 5 |       NS EMU TOOLS
 6 |     </v-app-bar-title>
 7 |     <v-spacer/>
 8 |     <v-btn class="float-right" icon @click="showConsoleDialog">
 9 |       <v-icon color="white" :icon="mdiConsole"></v-icon>
10 |     </v-btn>
11 |     <v-btn class="float-right" icon @click="switchDarkLight">
12 |       <v-icon color="white" :icon="mdiBrightness6"></v-icon>
13 |     </v-btn>
14 |   </v-app-bar>
15 | </template>
16 | 
17 | <script lang="ts" setup>
18 | import {useEmitter} from "@/plugins/mitt";
19 | import {mdiBrightness6, mdiConsole} from '@mdi/js'
20 | import {useTheme} from "vuetify";
21 | import {useConsoleDialogStore} from "@/stores/ConsoleDialogStore";
22 | 
23 | const emitter = useEmitter()
24 | const theme = useTheme()
25 | const cds = useConsoleDialogStore()
26 | 
27 | function triggerDrawer() {
28 |   emitter.emit('triggerDrawer')
29 | }
30 | function showConsoleDialog() {
31 |   cds.showConsoleDialog()
32 | }
33 | 
34 | function switchDarkLight() {
35 |   theme.global.name.value = theme.global.name.value === 'dark' ? 'light' : 'dark'
36 |   window.eel.update_dark_state(theme.global.name.value === 'dark')()
37 | }
38 | </script>
39 | 


--------------------------------------------------------------------------------
/frontend/src/layouts/README.md:
--------------------------------------------------------------------------------
1 | # Layouts
2 | 
3 | Layouts are reusable components that wrap around pages. They are used to provide a consistent look and feel across multiple pages.
4 | 
5 | Full documentation for this feature can be found in the Official [vite-plugin-vue-layouts](https://github.com/JohnCampionJr/vite-plugin-vue-layouts) repository.
6 | 


--------------------------------------------------------------------------------
/frontend/src/layouts/View.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <v-main>
 3 |     <div style="max-height: calc(100vh - 64px); width: 100%; overflow: auto;">
 4 |       <router-view />
 5 |     </div>
 6 |   </v-main>
 7 | </template>
 8 | 
 9 | <script lang="ts" setup>
10 |   //
11 | </script>
12 | 


--------------------------------------------------------------------------------
/frontend/src/layouts/default.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <v-app>
 3 |     <AppDrawer/>
 4 |     <default-bar/>
 5 | 
 6 |     <default-view/>
 7 |     <ConsoleDialog/>
 8 |     <NewVersionDialog/>
 9 |     <SpeedDial v-if="display.mdAndUp.value"/>
10 |   </v-app>
11 | </template>
12 | 
13 | <script lang="ts" setup>
14 | import DefaultBar from './AppBar.vue'
15 | import DefaultView from './View.vue'
16 | import AppDrawer from "./AppDrawer.vue";
17 | import ConsoleDialog from "@/components/ConsoleDialog.vue";
18 | import NewVersionDialog from "@/components/NewVersionDialog.vue";
19 | import SpeedDial from "@/components/SpeedDial.vue";
20 | import {useDisplay} from "vuetify";
21 | 
22 | const display = useDisplay()
23 | 
24 | </script>
25 | 


--------------------------------------------------------------------------------
/frontend/src/main.ts:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * main.ts
 3 |  *
 4 |  * Bootstraps Vuetify and other plugins then mounts the App`
 5 |  */
 6 | 
 7 | // Components
 8 | import App from './App.vue'
 9 | 
10 | // Composables
11 | import { createApp } from 'vue'
12 | 
13 | // Plugins
14 | import { registerPlugins } from '@/plugins'
15 | import {emitter, useEmitter} from "@/plugins/mitt";
16 | import {useConsoleDialogStore} from "@/stores/ConsoleDialogStore";
17 | 
18 | const app = createApp(App)
19 | 
20 | registerPlugins(app)
21 | 
22 | app.mount('#app')
23 | 
24 | declare global {
25 |     interface Window {
26 |         $vm: typeof app;
27 |         eel: any;
28 |         $bus: typeof emitter;
29 |     }
30 | }
31 | 
32 | window.$vm = app
33 | window.$bus = useEmitter()
34 | const cds = useConsoleDialogStore()
35 | window.$bus.on('APPEND_CONSOLE_MESSAGE', (msg) => {
36 |     cds.appendConsoleMessage(msg as string)
37 | })
38 | cds.appendConsoleMessage('启动时间: ' + new Date())
39 | 


--------------------------------------------------------------------------------
/frontend/src/pages/about.vue:
--------------------------------------------------------------------------------
  1 | <template>
  2 |   <SimplePage>
  3 |     <v-card>
  4 |       <v-card-title class="text-h4 text-primary">Ns Emu Tools</v-card-title>
  5 |       <v-divider></v-divider>
  6 |       <v-spacer></v-spacer>
  7 |       <v-card-text class="text-h6">
  8 |         一个用于安装/更新 NS 模拟器的工具
  9 |       </v-card-text>
 10 |       <v-card-text class="text-h6">
 11 |         当前版本:v{{ configStore.currentVersion }}
 12 |         <v-btn color="primary" variant="outlined" @click="configStore.checkUpdate(true)">
 13 |           检测新版本
 14 |         </v-btn>
 15 |         <ChangeLogDialog>
 16 |           <template v-slot:activator="{props}">
 17 |             <v-btn color="info" v-bind="props" @click="loadChangeLog"
 18 |                    variant="outlined" style="margin-left: 10px">
 19 |               更新日志
 20 |             </v-btn>
 21 |           </template>
 22 |           <template v-slot:content>
 23 |             <div v-html="changeLogHtml"></div>
 24 |           </template>
 25 |         </ChangeLogDialog>
 26 |         <v-btn color="success" variant="outlined" style="margin-left: 10px"
 27 |                @click="openUrlWithDefaultBrowser('https://github.com/triwinds/ns-emu-tools/blob/main/LICENSE')">
 28 |           License
 29 |         </v-btn>
 30 |       </v-card-text>
 31 |       <v-card-text class="text-h6 text--primary">
 32 |         <div class="info-block">
 33 |           <p class="text-h5 text-accent" style="padding-bottom: 5px">项目地址</p>
 34 |           <div class="line-group">
 35 |           <div class="line-item-icon"><v-icon size="24">{{ mdiGithub }}</v-icon></div>
 36 |           <div class="line-item">GitHub:<a class="text-error"
 37 |                     @click="openUrlWithDefaultBrowser('https://github.com/triwinds/ns-emu-tools')">
 38 | 
 39 |           triwinds/ns-emu-tools</a></div>
 40 |           </div>
 41 |           <span>如果您觉得这个软件好用, 可以在 GitHub 上点个 star</span><br>
 42 |           <span>这是对我最大的鼓励。</span>
 43 |         </div>
 44 |         <div class="info-block">
 45 |           <p class="text-h5 text-success">讨论组</p>
 46 |           <div class="line-group">
 47 |             <div class="line-item" style="">
 48 |               <v-img src="@/assets/telegram.webp" height="20" width="20"
 49 |                      v-show="theme.global.name.value === 'dark'"></v-img>
 50 |               <v-img src="@/assets/telegram_black.webp" height="20" width="20"
 51 |                      v-show="theme.global.name.value !== 'dark'"></v-img>
 52 |             </div>
 53 |             <div class="line-item">Telegram:
 54 |               <a class="text-secondary"
 55 |                  @click="openUrlWithDefaultBrowser('https://t.me/+mxI34BRClLUwZDcx')">Telegram 讨论组</a></div>
 56 |           </div>
 57 |         </div>
 58 |         <div class="info-block">
 59 |           <p class="text-h5 text-warning">Credits</p>
 60 |           <div class="line-group" v-for="(item, index) in credits" :key="index">
 61 |             <div class="line-item-icon">
 62 |               <v-icon size="24">{{ mdiGithub }}</v-icon>
 63 |             </div>
 64 |             <div class="line-item">
 65 |               <a class="text-secondary"
 66 |                  @click="openUrlWithDefaultBrowser(item.link)">{{ item.name }}</a>
 67 |               - {{ item.description }}
 68 |             </div>
 69 |           </div>
 70 |         </div>
 71 | 
 72 |       </v-card-text>
 73 |     </v-card>
 74 |   </SimplePage>
 75 | </template>
 76 | 
 77 | <script setup lang="ts">
 78 | import {mdiGithub} from "@mdi/js";
 79 | import SimplePage from "@/components/SimplePage.vue";
 80 | import ChangeLogDialog from "@/components/ChangeLogDialog.vue";
 81 | import {openUrlWithDefaultBrowser} from "@/utils/common";
 82 | import {useTheme} from "vuetify";
 83 | import {ref} from "vue";
 84 | import type {CommonResponse} from "@/types";
 85 | import md from "@/utils/markdown";
 86 | import {useConfigStore} from "@/stores/ConfigStore";
 87 | 
 88 | const theme = useTheme()
 89 | const configStore = useConfigStore()
 90 | let credits = [
 91 |   {name: 'Yuzu', link: 'https://github.com/yuzu-emu/yuzu', description: 'Yuzu 模拟器'},
 92 |   {name: 'Ryujinx', link: 'https://ryujinx.app/', description: 'Ryujinx 模拟器'},
 93 |   {name: 'Suyu', link: 'https://git.suyu.dev/suyu/suyu', description: 'Suyu 模拟器'},
 94 |   {name: 'hactool', link: 'https://github.com/SciresM/hactool', description: 'NS 固件解析'},
 95 |   {name: 'nsz', link: 'https://github.com/nicoboss/nsz', description: 'NS 固件解析'},
 96 |   {name: 'aria2', link: 'https://github.com/aria2/aria2', description: 'aria2 下载器'},
 97 |   {name: 'Github 镜像源', link: 'https://github.com/XIU2/UserScript/blob/master/GithubEnhanced-High-Speed-Download.user.js', description: '来自 X.I.U 大佬的 Github 增强脚本'},
 98 |   {name: 'pineappleEA', link: 'https://github.com/pineappleEA/pineapple-src', description: 'Yuzu EA 版本来源'},
 99 |   {name: 'THZoria/NX_Firmware', link: 'https://github.com/THZoria/NX_Firmware', description: 'NS 固件来源'},
100 |   {name: 'darthsternie.net', link: 'https://darthsternie.net/switch-firmwares/', description: 'NS 固件来源'},
101 | ]
102 | let changeLogHtml = ref('<p>加载中...</p>')
103 | function loadChangeLog() {
104 |   window.eel.load_change_log()((resp: CommonResponse) => {
105 |     if (resp.code === 0) {
106 |       changeLogHtml.value = md.parse(resp.data)
107 |     } else {
108 |       changeLogHtml.value = '<p>加载失败。</p>'
109 |     }
110 |   })
111 | }
112 | </script>
113 | 
114 | <style scoped>
115 | .info-block {
116 |   margin-bottom: 20px;
117 | }
118 | 
119 | .line-group {
120 |   width: 100%;
121 |   /*overflow-x: auto;*/
122 |   overflow: hidden;
123 |   margin-top: 5px;
124 | }
125 | 
126 | .line-item {
127 |   float: left;
128 |   margin-right: 10px;
129 |   margin-top: 2px;
130 |   height: 24px;
131 | }
132 | 
133 | .line-item-icon {
134 |   float: left;
135 |   margin-right: 10px;
136 | }
137 | span {
138 |   line-height: 30px;
139 | }
140 | </style>
141 | 


--------------------------------------------------------------------------------
/frontend/src/pages/faq.vue:
--------------------------------------------------------------------------------
  1 | <template>
  2 |   <SimplePage>
  3 |     <v-card id="faq-card" style="padding-bottom: 10px">
  4 |       <v-card-title>
  5 |         <span class="text-h4 text-primary">常见问题</span>
  6 |       </v-card-title>
  7 |       <v-divider></v-divider>
  8 |       <v-list>
  9 |         <FaqGroup>
 10 |           <template v-slot:title>第一次使用应该安装哪些组件</template>
 11 |           <template v-slot:content>
 12 |             <p>1. 安装最新的显卡驱动,这能减少模拟器运行时发生的很多问题。</p>
 13 |             <p>2. 安装 Yuzu/Ryujinx 模拟器(如果检测到缺少 msvc 运行库的话会自动运行 msvc 的安装程序,msvc
 14 |               装完后记得重启一下)</p>
 15 |             <p>3. 安装固件并配置相应的密钥文件</p>
 16 |             <p>这些东西装完之后模拟器就安装好了。</p>
 17 |           </template>
 18 |         </FaqGroup>
 19 | 
 20 |         <FaqGroup>
 21 |           <template v-slot:title>Yuzu/Ryujinx 最新版本信息加载失败</template>
 22 |           <template v-slot:content>
 23 |             <p>yuzu/ryujinx 的 release 数据需要从 GitHub api 中获取,而接口调用失败时就会出现这个问题。</p>
 24 |             <p><b>这个问题可能由以下两种原因产生:</b></p>
 25 |             <p>1.某些运营商的屏蔽了 api.github.com 这个地址,因此无法连接至 GitHub.</p>
 26 |             <p>2.如果你使用的是共享的 ip (比如用了某些公用的梯子或某些运营商的公共出口),可能是当前 ip 使用的频率达到
 27 |               GitHub 的使用上限(大概每小时 60 次).</p>
 28 |             <p><b>解决方法:</b></p>
 29 |             <p>1. 在设置中将 GitHub api 改为 "使用 CDN";如果还是不行,那说明 CDN 也被屏蔽了,挂个梯子直连吧。</p>
 30 |             <p>2. 如果是用梯子的话可以尝试换个节点, 或者等一段时间, GitHub 会自动解除封禁(最多只封 1h)</p>
 31 |           </template>
 32 |         </FaqGroup>
 33 | 
 34 |         <FaqGroup>
 35 |           <template v-slot:title>下载时出错,download 文件夹下没有任何东西</template>
 36 |           <template v-slot:content>
 37 |             <p>特殊地区/时期 Cloudflare
 38 |               服务器会因为某些原因被屏蔽,可以在设置中切换下载源,如果还是不行就需要使用代理软件了。</p>
 39 |             <p>或者前往下一小节中提到的 整合贴 中下载整合包。</p>
 40 |           </template>
 41 |         </FaqGroup>
 42 | 
 43 |         <FaqGroup>
 44 |           <template v-slot:title>下载速度很慢</template>
 45 |           <template v-slot:content>
 46 |             <p><b>如果设置中的下载源用的是 "直连":</b></p>
 47 |             <p>请直接和你的宽带运营商或者梯子提供者反馈下载速度不达标的问题。</p>
 48 |             <p><b>如果设置中的下载源用的是 CDN:</b></p>
 49 |             <p>
 50 |               由于 Cloudflare CDN 服务器不在国内,因此下载速度取决于你的宽带运营商所能提供的国际带宽,
 51 |               某些运营商为了节约运营成本,提供的国际线路不太好,带宽容量也比较少,所以在高峰期会出现丢包和卡顿。
 52 |             </p>
 53 |             <p>一劳永逸的解决办法当然是用好点的运营商以及一个好点的梯子
 54 |               <del>(加钱,世界触手可及.jpg)</del>
 55 |             </p>
 56 |             <p>如果你的动手能力比较强,可以看看这个由 XIU2 大佬分享的 <a
 57 |                 @click="openUrlWithDefaultBrowser('https://github.com/XIU2/CloudflareSpeedTest/discussions/71')">
 58 |               加速 Cloudflare CDN 访问</a> 的办法。</p>
 59 |             <p>或者直接在贴吧的
 60 |               <a @click="openUrlWithDefaultBrowser('https://tieba.baidu.com/p/7665223775')">整合贴 1</a> 或
 61 |               <a @click="openUrlWithDefaultBrowser('https://tieba.baidu.com/p/7799545671')">整合贴 2</a>
 62 |               中下载整合包。
 63 |             </p>
 64 |           </template>
 65 |         </FaqGroup>
 66 | 
 67 |         <FaqGroup>
 68 |           <template v-slot:title>如何为 aria2 配置代理</template>
 69 |           <template v-slot:content>
 70 |             <p>目前程序可以检测系统代理,并在进行下载时自动为 aria2 配置代理。</p>
 71 |             <p>当前使用的系统代理可以在右键 开始菜单图标 - 设置 - 网络和 Internet - 代理 - 代理服务器 中查看。</p>
 72 |             <p>v2rayN 的 "自动配置系统代理" 以及 Clash for windows 的 "System Proxy" 都可以正确配置系统代理。</p>
 73 |             <p>其它代理工具请自行摸索。</p>
 74 |           </template>
 75 |         </FaqGroup>
 76 | 
 77 |         <FaqGroup>
 78 |           <template v-slot:title>点击安装后出现 “当前的 xxx 就是 [yyy], 跳过安装.”</template>
 79 |           <template v-slot:content>
 80 |             <p>这是由于记录中的版本和你选择的版本一致,为了避免重复下载/安装,这里会跳过安装过程。</p>
 81 |             <p>如果你确认你选择的版本没有安装过,那可能是因为你用的是别人的配置文件。</p>
 82 |             <p>可以删除目录下的 config.json 文件,然后重启程序(这会重置你的设置及记录).</p>
 83 |             <p>这个时候点安装就应该正常了。</p>
 84 |           </template>
 85 |         </FaqGroup>
 86 | 
 87 |         <FaqGroup>
 88 |           <template v-slot:title>关于模拟器版本检测</template>
 89 |           <template v-slot:content>
 90 |             <p>
 91 |               模拟器版本检测的原理是启动模拟器,然后根据窗口标题确定正在使用的模拟器是什么版本,再将检测到的版本和分支信息保存到程序的记录中。</p>
 92 |             <p>ps.这个功能仅用于更新记录的版本号,不影响模拟器及程序的使用。</p>
 93 |           </template>
 94 |         </FaqGroup>
 95 | 
 96 |         <FaqGroup>
 97 |           <template v-slot:title>关于固件版本检测</template>
 98 |           <template v-slot:content>
 99 |             <p>
100 |               固件版本检测的原理是使用配置的密钥去解密固件文件,从而获取版本号。因此,这个功能需要你正确的配置了固件和相应的密钥后才能使用</p>
101 |             <p>ps.这个功能仅用于更新记录的版本号,不影响程序的使用。</p>
102 |           </template>
103 |         </FaqGroup>
104 | 
105 |         <FaqGroup>
106 |           <template v-slot:title>游戏与模拟器的兼容问题</template>
107 |           <template v-slot:content>
108 |             <p>
109 |               不同游戏对不同版本的模拟器兼容程度不一样,新版本模拟器对老游戏的支持不一定好,可以在贴吧里面找找别人是用什么版本
110 |               的模拟器和固件通关的,依此来决定你应该使用什么版本。
111 |             </p>
112 |             <p>
113 |               对于新游戏,模拟器会渐渐完善相关的支持,可以等一段时间后更新试试。
114 |             </p>
115 |           </template>
116 |         </FaqGroup>
117 | 
118 |         <FaqGroup>
119 |           <template v-slot:title>其它问题反馈</template>
120 |           <template v-slot:content>
121 |             <p>如果你遇到的问题不属于上面的任何一个,可以在
122 |               <a
123 |                  @click="openUrlWithDefaultBrowser('https://github.com/triwinds/ns-emu-tools/issues')">GitHub Issues</a>
124 |               中提交问题反馈,记得带上程序目录下的两个 log 文件,这将有助于排查你遇到的问题。
125 |             </p>
126 |           </template>
127 |         </FaqGroup>
128 |       </v-list>
129 |     </v-card>
130 |   </SimplePage>
131 | </template>
132 | 
133 | <script setup lang="ts">
134 | import SimplePage from "@/components/SimplePage.vue";
135 | import FaqGroup from "@/components/FaqGroup.vue";
136 | import {openUrlWithDefaultBrowser} from "@/utils/common";
137 | import {onMounted} from "vue";
138 | 
139 | onMounted(() => {
140 |   let aLinks = document.getElementById('faq-card')?.getElementsByTagName('a')
141 |   if (aLinks) {
142 |     for (let aLink of aLinks) {
143 |       aLink.classList.add('text-primary')
144 |       aLink.style.cursor = 'pointer'
145 |     }
146 |   }
147 | })
148 | 
149 | </script>
150 | 
151 | <style scoped>
152 | p {
153 |   font-size: 18px;
154 |   line-height: 30px !important;
155 |   margin-bottom: 16px !important;
156 | }
157 | </style>
158 | 


--------------------------------------------------------------------------------
/frontend/src/pages/index.vue:
--------------------------------------------------------------------------------
1 | <template>
2 |   <div></div>
3 | </template>
4 | 
5 | <script lang="ts">
6 | </script>
7 | 


--------------------------------------------------------------------------------
/frontend/src/pages/keys.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <SimplePage>
 3 |     <v-card>
 4 |       <v-container id="main-container">
 5 |         <v-row>
 6 |           <v-col>
 7 |             <p class="text-h3 text-primary">
 8 |               密钥管理
 9 |             </p>
10 |           </v-col>
11 |         </v-row>
12 |         <v-divider style="margin-bottom: 15px"></v-divider>
13 | 
14 |         <div class="text-h6">
15 |           <p>由于版权原因, 本项目不再提供 keys 自动安装功能。</p>
16 |           <p>请用户自行放置 keys 文件至相应文件夹。</p>
17 |           <div class="text-h4 text-accent">获取密钥</div>
18 |           <v-divider style="margin-bottom: 15px"></v-divider>
19 |           <p>请善用搜索引擎...</p>
20 |           <div id="root">
21 |             <div class="text-h4 text-warning">添加 keys 到模拟器</div>
22 |             <v-divider style="margin-bottom: 15px"></v-divider>
23 |             <p>请将上面获取到的 <b>prod.keys</b> 放进相应模拟器的文件夹
24 |             </p>
25 |             <p><b>注意:</b> 你使用的 keys 版本号应该大于等于当前使用的固件版本号</p>
26 |             <v-row style="padding-top: 25px">
27 |               <v-col>
28 |                 <v-btn color="info" variant="outlined" @click="openYuzuKeyFolder" block>打开 Yuzu keys 文件夹</v-btn>
29 |               </v-col>
30 |               <v-col>
31 |                 <v-btn color="info" variant="outlined" @click="openSuyuKeyFolder" block>打开 Suzu keys 文件夹</v-btn>
32 |               </v-col>
33 |               <v-col>
34 |                 <v-btn color="info" variant="outlined" @click="openRyujinxKeyFolder" block>打开 Ryujinx keys 文件夹
35 |                 </v-btn>
36 |               </v-col>
37 |             </v-row>
38 |           </div>
39 |         </div>
40 |       </v-container>
41 |     </v-card>
42 |   </SimplePage>
43 | </template>
44 | 
45 | <script setup lang="ts">
46 | import SimplePage from "@/components/SimplePage.vue";
47 | import {openUrlWithDefaultBrowser} from "@/utils/common";
48 | 
49 | function openYuzuKeyFolder() {
50 |   window.eel.open_yuzu_keys_folder()(() => {
51 |   })
52 | }
53 | 
54 | function openSuyuKeyFolder() {
55 |   window.eel.open_suyu_keys_folder()(() => {
56 |   })
57 | }
58 | 
59 | function openRyujinxKeyFolder() {
60 |   window.eel.open_ryujinx_keys_folder()(() => {
61 |   })
62 | }
63 | </script>
64 | 
65 | <style scoped>
66 | .text-h3 {
67 |   padding-bottom: 10px;
68 | }
69 | 
70 | .text-h4 {
71 |   padding-top: 25px;
72 |   padding-bottom: 10px;
73 | }
74 | </style>
75 | 


--------------------------------------------------------------------------------
/frontend/src/pages/otherLinks.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <SimplePage>
 3 |     <OtherLinkItem v-for="item in linkItems" :key="item.link" style="margin-bottom: 10px">
 4 |       <template v-slot:title>
 5 |         <v-icon style="margin-right: 5px">{{
 6 |             item.link.startsWith('https://github.com') ? mdiGithub : mdiWeb
 7 |           }}
 8 |         </v-icon>
 9 |         <span class="text-warning">{{ item.title }}</span>
10 |       </template>
11 |       <template v-slot:description>{{ item.description }}</template>
12 |       <template v-slot:actions>
13 |         <v-btn variant="text" color="info" @click="openUrlWithDefaultBrowser(item.link)">
14 |           打开链接
15 |         </v-btn>
16 |         <div v-if="item.subLinks">
17 |           <v-btn v-for="subLink in item.subLinks" variant="text" color="info"
18 |                  :key="subLink.link" @click="openUrlWithDefaultBrowser(subLink.link)">
19 |             {{ subLink.name }}
20 |           </v-btn>
21 |         </div>
22 |       </template>
23 |     </OtherLinkItem>
24 |   </SimplePage>
25 | </template>
26 | 
27 | <script setup lang="ts">
28 | 
29 | import {mdiGithub, mdiWeb} from '@mdi/js';
30 | import {openUrlWithDefaultBrowser} from "@/utils/common";
31 | import SimplePage from "@/components/SimplePage.vue";
32 | import OtherLinkItem from "@/components/OtherLinkItem.vue";
33 | 
34 | let linkItems = [
35 |   {
36 |     link: 'https://github.com/dezem/SAK',
37 |     title: 'Switch Army Knife (SAK)',
38 |     description: '一个用于转换 XCI / NSP / NSZ 格式的工具',
39 |   }, {
40 |     link: 'https://github.com/BeyondDimension/SteamTools',
41 |     title: 'Watt Toolkit (原 steam++)',
42 |     description: '一个开源跨平台的多功能 Steam 工具箱, 可以本地代理 Steam 社区、 Github 、谷歌验证码等国内难以访问的网页',
43 |     subLinks: [
44 |       {
45 |         name: '官网',
46 |         link: 'https://steampp.net/'
47 |       }
48 |     ]
49 |   }, {
50 |     link: 'https://github.com/theboy181/switch-ptchtxt-mods',
51 |     title: 'theboy181 的 MOD 合集',
52 |     description: '由 theboy181 整理的 MOD 合集',
53 |     subLinks: [
54 |       {
55 |         name: 'Discord',
56 |         link: 'https://discord.gg/B56sXVVRmC'
57 |       }
58 |     ]
59 |   }, {
60 |     link: 'https://github.com/ChanseyIsTheBest/NX-60FPS-RES-GFX-Cheats/blob/main/GAMES.md',
61 |     title: '图形类金手指合集',
62 |     description: '由 ChanseyIsTheBest 整理的金手指合集',
63 |   }, {
64 |     link: 'https://www.cheatslips.com/',
65 |     title: '作弊类金手指合集',
66 |     description: '由 cheatslips 整理的金手指合集',
67 |   }, {
68 |     link: 'http://www.ffhome.com/works/1814.html',
69 |     title: 'FFHOME Nintendo Switch Game Manager',
70 |     description: '软件全称 FFHOME Nintendo Switch Game Manager,它是一款整理(处理)你拥有的NSP格式、XCI格式游戏文件的一款小工具,包括文件信息查看、批量更名和文件处理等功能。',
71 |   },
72 | ]
73 | 
74 | </script>
75 | 
76 | <style scoped>
77 | 
78 | </style>
79 | 


--------------------------------------------------------------------------------
/frontend/src/pages/yuzuSaveManagement.vue:
--------------------------------------------------------------------------------
  1 | <template>
  2 | <SimplePage>
  3 |     <v-card>
  4 |       <v-card-title class="text-primary text-h4" style="margin-bottom: 10px; margin-top: 10px;">Yuzu 存档管理</v-card-title>
  5 |       <v-divider></v-divider>
  6 |       <div class="d-flex flex-row">
  7 |         <v-tabs v-model="tab" direction="vertical" color="primary">
  8 |         <v-tab value="backup">
  9 |           <v-icon>{{ mdiContentSaveAll }}</v-icon>
 10 |           <span style="padding-left: 15px">备份</span>
 11 |         </v-tab>
 12 |         <v-tab value="restore">
 13 |           <v-icon>{{ mdiBackupRestore }}</v-icon>
 14 |           <span style="padding-left: 15px">还原</span>
 15 |         </v-tab>
 16 |           </v-tabs>
 17 | 
 18 |         <v-window v-model="tab" style="width: 100%; height: 100%;">
 19 |           <v-window-item key="backup" value="backup">
 20 |             <v-card variant="flat">
 21 |               <YuzuSaveCommonPart/>
 22 |               <MarkdownContentBox v-if="!yuzuSaveStore.selectedUser || yuzuSaveStore.selectedUser === ''" :content="guide"/>
 23 |               <v-container v-else>
 24 |                 <v-row>
 25 |                   <v-col>
 26 |                     <v-autocomplete v-model="selectedGameFolder" hide-details variant="underlined"
 27 |                                     :items="gameList" label="选择需要进行备份的游戏"
 28 |                                     :item-title="concatGameName" item-value="folder"/>
 29 |                   </v-col>
 30 |                 </v-row>
 31 |                 <v-row>
 32 |                   <v-col>
 33 |                     <v-btn color="success" variant="outlined" block @click="doBackup"
 34 |                            :disabled="selectedGameFolder === ''">创建备份</v-btn>
 35 |                   </v-col>
 36 |                 </v-row>
 37 |               </v-container>
 38 |             </v-card>
 39 |           </v-window-item>
 40 | 
 41 |           <v-window-item key="restore" value="restore">
 42 |             <YuzuSaveRestoreTab/>
 43 |           </v-window-item>
 44 |         </v-window>
 45 |       </div>
 46 |     </v-card>
 47 |   </SimplePage>
 48 | </template>
 49 | 
 50 | <script setup lang="ts">
 51 | import SimplePage from "@/components/SimplePage.vue";
 52 | import {ref, watch} from "vue";
 53 | import {mdiContentSaveAll, mdiBackupRestore} from '@mdi/js';
 54 | import {useYuzuSaveStore} from "@/stores/YuzuSaveStore";
 55 | import MarkdownContentBox from "@/components/MarkdownContentBox.vue";
 56 | import YuzuSaveCommonPart from "@/components/YuzuSaveCommonPart.vue";
 57 | import type {CommonResponse, SaveGameInfo} from "@/types";
 58 | import {useAppStore} from "@/stores/app";
 59 | import {useConsoleDialogStore} from "@/stores/ConsoleDialogStore";
 60 | import YuzuSaveRestoreTab from "@/components/YuzuSaveRestoreTab.vue";
 61 | import {useEmitter} from "@/plugins/mitt";
 62 | 
 63 | let tab = ref('')
 64 | const guide = `
 65 | ## 使用说明
 66 | 
 67 | Yuzu 模拟器在保存存档时会根据用户 id 选择不同的文件夹,因此需要先确认你正在使用的用户 id.
 68 | 
 69 | 模拟器的用户 ID 可以在菜单 模拟->设置->系统->配置 中查看
 70 | [参考截图](https://cdn.jsdelivr.net/gh/triwinds/ns-emu-tools@main/doc/assets/yuzu_user_id.jpg).
 71 | 
 72 | 点击备份会将选择的存档文件夹打包成一个 7z 压缩包放到指定的目录, 你也可以选择手动解压还原。
 73 | `
 74 | let selectedGameFolder = ref('')
 75 | let gameList = ref<SaveGameInfo[]>([])
 76 | const yuzuSaveStore = useYuzuSaveStore()
 77 | const appStore = useAppStore()
 78 | const cds = useConsoleDialogStore()
 79 | const emitter = useEmitter()
 80 | let lastUser: string = ''
 81 | 
 82 | yuzuSaveStore.$subscribe((mutation, state) => {
 83 |   if (state.selectedUser && lastUser != state.selectedUser) {
 84 |     reloadGameList()
 85 |     lastUser = state.selectedUser
 86 |   }
 87 | })
 88 | 
 89 | watch(tab, () => {
 90 |   emitter.emit('yuzuSave:tabChange', tab)
 91 | })
 92 | 
 93 | function reloadGameList() {
 94 |   window.eel.list_all_games_by_user_folder(yuzuSaveStore.selectedUser)((resp: CommonResponse) => {
 95 |     if (resp.code === 0) {
 96 |       gameList.value = resp.data
 97 |       if (appStore.gameDataInited) {
 98 |         enrichGameList()
 99 |       } else {
100 |         appStore.loadGameData().then(() => {
101 |           enrichGameList()
102 |         })
103 |       }
104 |       if (gameList.value.length > 0) {
105 |         selectedGameFolder.value = gameList.value[0].folder
106 |       }
107 |     }
108 |   })
109 | }
110 | 
111 | function enrichGameList() {
112 |   let nl = []
113 |   for (let item of gameList.value) {
114 |     item.game_name = appStore.gameData[item.title_id]
115 |     nl.push(item)
116 |   }
117 |   gameList.value = nl;
118 | }
119 | 
120 | function concatGameName(item: SaveGameInfo) {
121 |   let gameName = item.game_name ? item.game_name : appStore.gameDataInited ? '未知游戏' : '游戏信息加载中...'
122 |   return `[${item.title_id}] ${gameName}`
123 | }
124 | 
125 | function doBackup() {
126 |   cds.cleanAndShowConsoleDialog()
127 |   window.eel.backup_yuzu_save_folder(selectedGameFolder.value)()
128 | }
129 | 
130 | </script>
131 | 
132 | <style scoped>
133 | 
134 | </style>
135 | 


--------------------------------------------------------------------------------
/frontend/src/plugins/README.md:
--------------------------------------------------------------------------------
1 | # Plugins
2 | 
3 | Plugins are a way to extend the functionality of your Vue application. Use this folder for registering plugins that you want to use globally.
4 | 


--------------------------------------------------------------------------------
/frontend/src/plugins/index.ts:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * plugins/index.ts
 3 |  *
 4 |  * Automatically included in `./src/main.ts`
 5 |  */
 6 | 
 7 | // Plugins
 8 | import vuetify from './vuetify'
 9 | import pinia from '../stores'
10 | import router from '../router'
11 | 
12 | // Types
13 | import type { App } from 'vue'
14 | 
15 | export function registerPlugins (app: App) {
16 |   app
17 |     .use(vuetify)
18 |     .use(router)
19 |     .use(pinia)
20 | }
21 | 


--------------------------------------------------------------------------------
/frontend/src/plugins/mitt.ts:
--------------------------------------------------------------------------------
1 | // Composables
2 | import mitt from 'mitt'
3 | 
4 | export const emitter = mitt()
5 | 
6 | export function useEmitter() {
7 |   return emitter
8 | }
9 | 


--------------------------------------------------------------------------------
/frontend/src/plugins/vuetify.ts:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * plugins/vuetify.ts
 3 |  *
 4 |  * Framework documentation: https://vuetifyjs.com`
 5 |  */
 6 | 
 7 | // Styles
 8 | import 'vuetify/styles'
 9 | import '@fontsource-variable/jetbrains-mono';
10 | 
11 | // Composables
12 | import { createVuetify } from 'vuetify'
13 | import { aliases, mdi } from 'vuetify/iconsets/mdi-svg'
14 | 
15 | // https://vuetifyjs.com/en/introduction/why-vuetify/#feature-guides
16 | export default createVuetify({
17 |   icons: {
18 |     defaultSet: 'mdi',
19 |     aliases,
20 |     sets: {
21 |       mdi,
22 |     },
23 |   },
24 |   defaults: {
25 |     VCardTitle: {
26 |       style: 'padding-top: 16px; padding-bottom: 16px;'
27 |     }
28 |   },
29 |   theme: {
30 |     defaultTheme: 'dark',
31 |     themes: {
32 |       dark: {
33 |         colors: {
34 |           primary: '#009688',
35 |           secondary: '#89ddff',
36 |           accent: '#c792ea',
37 |           error: '#ff5370',
38 |           warning: '#ffcb6b',
39 |           info: '#89ddff',
40 |           success: '#c3e88d',
41 |           background: '#263238'
42 |         }
43 |       },
44 |       light: {
45 |         colors: {
46 |           primary: '#3A66D1',
47 |           secondary: '#2AA298',
48 |           accent: '#6F42C1',
49 |           error: '#d25252',
50 |           warning: '#E36209',
51 |           info: '#2AA298',
52 |           success: '#22863A',
53 |           background: '#F7F8FA'
54 |         }
55 |       },
56 |     },
57 |   },
58 | })
59 | 


--------------------------------------------------------------------------------
/frontend/src/router/index.ts:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * router/index.ts
 3 |  *
 4 |  * Automatic routes for `./src/pages/*.vue`
 5 |  */
 6 | 
 7 | // Composables
 8 | import { createRouter, createWebHashHistory } from 'vue-router/auto'
 9 | import { setupLayouts } from 'virtual:generated-layouts'
10 | import { routes } from 'vue-router/auto-routes'
11 | 
12 | const router = createRouter({
13 |   history: createWebHashHistory(import.meta.env.BASE_URL),
14 |   routes: setupLayouts(routes),
15 | })
16 | 
17 | // Workaround for https://github.com/vitejs/vite/issues/11804
18 | router.onError((err, to) => {
19 |   if (err?.message?.includes?.('Failed to fetch dynamically imported module')) {
20 |     if (!localStorage.getItem('vuetify:dynamic-reload')) {
21 |       console.log('Reloading page to fix dynamic import error')
22 |       localStorage.setItem('vuetify:dynamic-reload', 'true')
23 |       location.assign(to.fullPath)
24 |     } else {
25 |       console.error('Dynamic import error, reloading page did not fix it', err)
26 |     }
27 |   } else {
28 |     console.error(err)
29 |   }
30 | })
31 | 
32 | router.isReady().then(() => {
33 |   localStorage.removeItem('vuetify:dynamic-reload')
34 | })
35 | 
36 | export default router
37 | 


--------------------------------------------------------------------------------
/frontend/src/stores/ConfigStore.ts:
--------------------------------------------------------------------------------
 1 | // Utilities
 2 | import { defineStore } from 'pinia'
 3 | import type {AppConfig, CommonResponse} from "@/types";
 4 | 
 5 | export const useConfigStore = defineStore('config', {
 6 |   state: () => ({
 7 |     config: {
 8 |       yuzu: {},
 9 |       suyu: {},
10 |       ryujinx: {},
11 |       setting: {}
12 |     } as AppConfig,
13 |     currentVersion: '',
14 |     hasNewVersion: false,
15 |   }),
16 |   actions: {
17 |     async reloadConfig() {
18 |       const resp = await window.eel.get_config()()
19 |       if (resp.code === 0) {
20 |         this.config = resp.data
21 |       }
22 |     },
23 |     initCurrentVersion() {
24 |       window.eel.get_current_version()((resp: CommonResponse) => {
25 |         if (resp['code'] === 0) {
26 |           this.currentVersion = resp.data
27 |         } else {
28 |           this.currentVersion = '未知'
29 |         }
30 |       })
31 |     },
32 |     checkUpdate(forceShowDialog: boolean) {
33 |       window.eel.check_update()((data: CommonResponse) => {
34 |         if (data['code'] === 0 && data['data']) {
35 |             this.hasNewVersion = true
36 |         }
37 |         if (forceShowDialog || this.hasNewVersion) {
38 |             window.$bus.emit('showNewVersionDialog',
39 |                 {hasNewVersion: this.hasNewVersion, latestVersion: data['msg']})
40 |         }
41 |       })
42 |     },
43 |   },
44 |   getters: {
45 |     yuzuConfig(state) {
46 |       return state.config.yuzu
47 |     }
48 |   }
49 | })
50 | 


--------------------------------------------------------------------------------
/frontend/src/stores/ConsoleDialogStore.ts:
--------------------------------------------------------------------------------
 1 | // Utilities
 2 | import {defineStore} from 'pinia'
 3 | 
 4 | export const useConsoleDialogStore = defineStore('consoleDialog', {
 5 |     state: () => ({
 6 |         dialogFlag: false,
 7 |         persistentConsoleDialog: false,
 8 |         consoleMessages: [] as string[],
 9 |         newLine: true
10 |     }),
11 |     actions: {
12 |         cleanAndShowConsoleDialog() {
13 |             this.dialogFlag = true
14 |             this.consoleMessages = []
15 |         },
16 |         showConsoleDialog() {
17 |             this.dialogFlag = true
18 |         },
19 |         appendConsoleMessage(message: string) {
20 |             if (!message) {
21 |                 return
22 |             }
23 |             const splits = message.split('\n')
24 |             for (const value of splits) {
25 |                 if (value.length < 1) {
26 |                     continue
27 |                 }
28 |                 if (value && value.startsWith('^')) {
29 |                     if (this.newLine) {
30 |                         this.consoleMessages.push(value)
31 |                         this.newLine = false
32 |                     } else {
33 |                         this.consoleMessages[this.consoleMessages.length - 1] = value.substring(1)
34 |                     }
35 |                 } else {
36 |                     this.consoleMessages.push(value)
37 |                     if (!this.newLine) {
38 |                         this.newLine = true
39 |                     }
40 |                 }
41 |             }
42 |         },
43 |         cleanMessages() {
44 |             this.consoleMessages = []
45 |         }
46 |     }
47 | })
48 | 


--------------------------------------------------------------------------------
/frontend/src/stores/README.md:
--------------------------------------------------------------------------------
1 | # Store
2 | 
3 | Pinia stores are used to store reactive state and expose actions to mutate it.
4 | 
5 | Full documentation for this feature can be found in the Official [Pinia](https://pinia.esm.dev/) repository.
6 | 


--------------------------------------------------------------------------------
/frontend/src/stores/YuzuSaveStore.ts:
--------------------------------------------------------------------------------
 1 | // Utilities
 2 | import { defineStore } from 'pinia'
 3 | import type {CommonResponse, YuzuSaveUserListItem} from "@/types";
 4 | 
 5 | 
 6 | 
 7 | export const useYuzuSaveStore = defineStore('yuzuSave', {
 8 |   state: () => ({
 9 |     userList: [] as YuzuSaveUserListItem[],
10 |     selectedUser: '',
11 |     yuzuSaveBackupPath: ''
12 |   }),
13 |   actions: {
14 | 
15 |   }
16 | })
17 | 


--------------------------------------------------------------------------------
/frontend/src/stores/app.ts:
--------------------------------------------------------------------------------
 1 | // Utilities
 2 | import { defineStore } from 'pinia'
 3 | import type {CheatGameInfo, CommonResponse} from "@/types";
 4 | import {useConsoleDialogStore} from "@/stores/ConsoleDialogStore";
 5 | 
 6 | const cds = useConsoleDialogStore()
 7 | 
 8 | export const useAppStore = defineStore('app', {
 9 |   state: () => ({
10 |     targetFirmwareVersion: null,
11 |     availableFirmwareInfos: [],
12 |     gameData: {} as {[key: string]: string}
13 |   }),
14 |   getters: {
15 |     gameDataInited(state) {
16 |         return Object.keys(state.gameData).length !== 0
17 |     }
18 |   },
19 |   actions: {
20 |     updateAvailableFirmwareInfos() {
21 |         this.targetFirmwareVersion = null
22 |         window.eel.get_available_firmware_infos()((data: CommonResponse) => {
23 |             if (data['code'] === 0) {
24 |               const infos = data['data']
25 |               this.availableFirmwareInfos = infos
26 |               this.targetFirmwareVersion = infos[0]['version']
27 |             } else {
28 |                 cds.showConsoleDialog()
29 |                 cds.appendConsoleMessage('固件信息加载异常.')
30 |             }
31 |         })
32 |     },
33 |     async loadGameData() {
34 |       if (this.gameDataInited && !('unknown' in this.gameData)) {
35 |           return this.gameData
36 |       }
37 |       const resp = await window.eel.get_game_data()()
38 |       const gameData = resp.code === 0 ? resp.data : {'unknown': 'unknown'}
39 |       this.gameData = gameData
40 |       return gameData
41 |     }
42 |   }
43 | })
44 | 


--------------------------------------------------------------------------------
/frontend/src/stores/index.ts:
--------------------------------------------------------------------------------
1 | // Utilities
2 | import { createPinia } from 'pinia'
3 | 
4 | export default createPinia()
5 | 


--------------------------------------------------------------------------------
/frontend/src/styles/README.md:
--------------------------------------------------------------------------------
1 | # Styles
2 | 
3 | This directory is for configuring the styles of the application.
4 | 


--------------------------------------------------------------------------------
/frontend/src/styles/settings.scss:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * src/styles/settings.scss
 3 |  *
 4 |  * Configures SASS variables and Vuetify overwrites
 5 |  */
 6 | 
 7 | // https://vuetifyjs.com/features/sass-variables/`
 8 | // @use 'vuetify/settings' with (
 9 | //   $color-pack: false
10 | // );
11 | 


--------------------------------------------------------------------------------
/frontend/src/typed-router.d.ts:
--------------------------------------------------------------------------------
 1 | /* eslint-disable */
 2 | /* prettier-ignore */
 3 | // @ts-nocheck
 4 | // Generated by unplugin-vue-router. ‼️ DO NOT MODIFY THIS FILE ‼️
 5 | // It's recommended to commit this file.
 6 | // Make sure to add this file to your tsconfig.json file as an "includes" or "files" entry.
 7 | 
 8 | declare module 'vue-router/auto-routes' {
 9 |   import type {
10 |     RouteRecordInfo,
11 |     ParamValue,
12 |     ParamValueOneOrMore,
13 |     ParamValueZeroOrMore,
14 |     ParamValueZeroOrOne,
15 |   } from 'vue-router'
16 | 
17 |   /**
18 |    * Route name map generated by unplugin-vue-router
19 |    */
20 |   export interface RouteNamedMap {
21 |     '/': RouteRecordInfo<'/', '/', Record<never, never>, Record<never, never>>,
22 |     '/about': RouteRecordInfo<'/about', '/about', Record<never, never>, Record<never, never>>,
23 |     '/faq': RouteRecordInfo<'/faq', '/faq', Record<never, never>, Record<never, never>>,
24 |     '/keys': RouteRecordInfo<'/keys', '/keys', Record<never, never>, Record<never, never>>,
25 |     '/otherLinks': RouteRecordInfo<'/otherLinks', '/otherLinks', Record<never, never>, Record<never, never>>,
26 |     '/ryujinx': RouteRecordInfo<'/ryujinx', '/ryujinx', Record<never, never>, Record<never, never>>,
27 |     '/settings': RouteRecordInfo<'/settings', '/settings', Record<never, never>, Record<never, never>>,
28 |     '/suyu': RouteRecordInfo<'/suyu', '/suyu', Record<never, never>, Record<never, never>>,
29 |     '/yuzu': RouteRecordInfo<'/yuzu', '/yuzu', Record<never, never>, Record<never, never>>,
30 |     '/yuzuCheatsManagement': RouteRecordInfo<'/yuzuCheatsManagement', '/yuzuCheatsManagement', Record<never, never>, Record<never, never>>,
31 |     '/yuzuSaveManagement': RouteRecordInfo<'/yuzuSaveManagement', '/yuzuSaveManagement', Record<never, never>, Record<never, never>>,
32 |   }
33 | }
34 | 


--------------------------------------------------------------------------------
/frontend/src/types/DefaultConfig.ts:
--------------------------------------------------------------------------------
 1 | import type {AppConfig} from "@/types";
 2 | 
 3 | export const defaultConfig: AppConfig = {
 4 |   "yuzu": {
 5 |     "yuzu_path": "D:\\Yuzu",
 6 |     "yuzu_version": "",
 7 |     "yuzu_firmware": "",
 8 |     "branch": "ea"
 9 |   },
10 |   "suyu": {
11 |     "path": "D:\\Suyu",
12 |     "version": "",
13 |     "firmware": "",
14 |     "branch": "dev"
15 |   },
16 |   "ryujinx": {
17 |     "path": "D:\\Ryujinx",
18 |     "version": "",
19 |     "firmware": "",
20 |     "branch": "ava"
21 |   },
22 |   "setting": {
23 |     "ui": {
24 |       "lastOpenEmuPage": "",
25 |       "dark": true,
26 |       "mode": "auto",
27 |       "width": 1300,
28 |       "height": 850
29 |     },
30 |     "network": {
31 |       "firmwareDownloadSource": "github",
32 |       "githubApiMode": "direct",
33 |       "githubDownloadMirror": "cloudflare_load_balance",
34 |       "useDoh": true,
35 |       "proxy": "system"
36 |     },
37 |     "download": {
38 |       "autoDeleteAfterInstall": true,
39 |       "disableAria2Ipv6": true,
40 |       "removeOldAria2LogFile": true,
41 |       "verifyFirmwareMd5": true
42 |     },
43 |     "other": {
44 |       "rename_yuzu_to_cemu": false
45 |     }
46 |   }
47 | }
48 | 


--------------------------------------------------------------------------------
/frontend/src/types/index.ts:
--------------------------------------------------------------------------------
  1 | // Composables
  2 | 
  3 | export interface AppConfig {
  4 |   yuzu: YuzuConfig
  5 |   suyu: SuyuConfig
  6 |   ryujinx: RyujinxConfig
  7 |   setting: Setting
  8 | }
  9 | 
 10 | export interface YuzuConfig {
 11 |   yuzu_path: string
 12 |   yuzu_version: string
 13 |   yuzu_firmware: string
 14 |   branch: string
 15 | }
 16 | 
 17 | export interface SuyuConfig {
 18 |   path: string
 19 |   version: string
 20 |   firmware: string
 21 |   branch: string
 22 | }
 23 | 
 24 | export interface RyujinxConfig {
 25 |   path: string
 26 |   version: string
 27 |   firmware: string
 28 |   branch: string
 29 | }
 30 | 
 31 | export interface Setting {
 32 |   ui: UiSetting
 33 |   network: NetworkSetting
 34 |   download: DownloadSetting
 35 |   other: OtherSetting
 36 | }
 37 | 
 38 | export interface OtherSetting {
 39 |   rename_yuzu_to_cemu: boolean
 40 | }
 41 | 
 42 | export interface UiSetting {
 43 |   lastOpenEmuPage: string
 44 |   dark: boolean
 45 |   mode: string
 46 |   width: number
 47 |   height: number
 48 | }
 49 | 
 50 | export interface NetworkSetting {
 51 |   firmwareDownloadSource: string
 52 |   githubApiMode: string
 53 |   githubDownloadMirror: string
 54 |   useDoh: boolean
 55 |   proxy: string
 56 | }
 57 | 
 58 | export interface DownloadSetting {
 59 |   autoDeleteAfterInstall: boolean
 60 |   disableAria2Ipv6: boolean
 61 |   removeOldAria2LogFile: boolean
 62 |   verifyFirmwareMd5: boolean
 63 | }
 64 | 
 65 | export interface CommonResponse {
 66 |   code: number,
 67 |   msg: string,
 68 |   data: any
 69 | }
 70 | 
 71 | export interface CheatGameInfo {
 72 |   game_id: string,
 73 |   game_name: string,
 74 |   cheats_path: string,
 75 | }
 76 | 
 77 | export interface CheatItem {
 78 |   enable: boolean,
 79 |   title: string,
 80 | }
 81 | 
 82 | export interface CheatFileInfo {
 83 |   path: string,
 84 |   name: string,
 85 | }
 86 | 
 87 | export interface NameValueItem {
 88 |   name: string,
 89 |   value: string,
 90 | }
 91 | 
 92 | export interface YuzuSaveUserListItem {
 93 |   user_id: string,
 94 |   folder: string
 95 | }
 96 | 
 97 | export interface YuzuSaveBackupListItem {
 98 |   game_name: string,
 99 |   title_id: string,
100 |   bak_time: number,
101 |   filename: string,
102 |   path: string,
103 | }
104 | 
105 | export interface SaveGameInfo {
106 |   title_id: string,
107 |   game_name: string,
108 |   folder: string,
109 | }
110 | 


--------------------------------------------------------------------------------
/frontend/src/utils/common.ts:
--------------------------------------------------------------------------------
 1 | // Utilities
 2 | 
 3 | 
 4 | import {useAppStore} from "@/stores/app";
 5 | 
 6 | const appStore = useAppStore()
 7 | 
 8 | export function openUrlWithDefaultBrowser(url: string) {
 9 |     window.eel.open_url_in_default_browser(url)()
10 | }
11 | 
12 | export async function loadGameData() {
13 |     if (appStore.gameDataInited && !('unknown' in appStore.gameData)) {
14 |         return appStore.gameData
15 |     }
16 |     const resp = await window.eel.get_game_data()()
17 |     const gameData = resp.code === 0 ? resp.data : {'unknown': 'unknown'}
18 |     appStore.gameData = gameData
19 |     return gameData
20 | }
21 | 


--------------------------------------------------------------------------------
/frontend/src/utils/markdown.ts:
--------------------------------------------------------------------------------
 1 | import DOMPurify from "isomorphic-dompurify";
 2 | import { marked } from 'marked';
 3 | 
 4 | export default {
 5 |   parse(markdown: string) {
 6 |     return DOMPurify.sanitize(marked.parse(markdown) as string);
 7 |   },
 8 | }
 9 | 
10 | 


--------------------------------------------------------------------------------
/frontend/tsconfig.app.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "extends": "@vue/tsconfig/tsconfig.dom.json",
 3 |   "include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
 4 |   "exclude": ["src/**/__tests__/*"],
 5 |   "compilerOptions": {
 6 |     "composite": true,
 7 |     "types": ["vuetify"],
 8 |     "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
 9 | 
10 |     "baseUrl": ".",
11 |     "paths": {
12 |       "@/*": ["./src/*"]
13 |     }
14 |   }
15 | }
16 | 


--------------------------------------------------------------------------------
/frontend/tsconfig.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "files": [],
 3 |   "references": [
 4 |     {
 5 |       "path": "./tsconfig.node.json"
 6 |     },
 7 |     {
 8 |       "path": "./tsconfig.app.json"
 9 |     }
10 |   ]
11 | }
12 | 


--------------------------------------------------------------------------------
/frontend/tsconfig.node.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "extends": "@tsconfig/node22/tsconfig.json",
 3 |   "include": [
 4 |     "vite.config.*",
 5 |     "vitest.config.*",
 6 |     "cypress.config.*",
 7 |     "nightwatch.conf.*",
 8 |     "playwright.config.*"
 9 |   ],
10 |   "compilerOptions": {
11 |     "composite": true,
12 |     "noEmit": true,
13 |     "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
14 | 
15 |     "module": "ESNext",
16 |     "moduleResolution": "Bundler",
17 |     "types": ["node"]
18 |   }
19 | }
20 | 


--------------------------------------------------------------------------------
/frontend/vite.config.mts:
--------------------------------------------------------------------------------
 1 | // Plugins
 2 | import AutoImport from 'unplugin-auto-import/vite'
 3 | import Components from 'unplugin-vue-components/vite'
 4 | import Fonts from 'unplugin-fonts/vite'
 5 | import Layouts from 'vite-plugin-vue-layouts'
 6 | import Vue from '@vitejs/plugin-vue'
 7 | import VueRouter from 'unplugin-vue-router/vite'
 8 | import Vuetify, { transformAssetUrls } from 'vite-plugin-vuetify'
 9 | 
10 | // Utilities
11 | import { defineConfig } from 'vite'
12 | import { fileURLToPath, URL } from 'node:url'
13 | 
14 | // https://vitejs.dev/config/
15 | export default defineConfig({
16 |   plugins: [
17 |     VueRouter({
18 |       dts: 'src/typed-router.d.ts',
19 |     }),
20 |     Layouts(),
21 |     AutoImport({
22 |       imports: [
23 |         'vue',
24 |         {
25 |           'vue-router/auto': ['useRoute', 'useRouter'],
26 |         }
27 |       ],
28 |       dts: 'src/auto-imports.d.ts',
29 |       eslintrc: {
30 |         enabled: true,
31 |       },
32 |       vueTemplate: true,
33 |     }),
34 |     Components({
35 |       dts: 'src/components.d.ts',
36 |     }),
37 |     Vue({
38 |       template: { transformAssetUrls },
39 |     }),
40 |     // https://github.com/vuetifyjs/vuetify-loader/tree/master/packages/vite-plugin#readme
41 |     Vuetify({
42 |       autoImport: true,
43 |       styles: {
44 |         configFile: 'src/styles/settings.scss',
45 |       },
46 |     }),
47 |     Fonts({
48 |       google: {
49 |         families: [ {
50 |           name: 'Roboto',
51 |           styles: 'wght@100;300;400;500;700;900',
52 |         }],
53 |       },
54 |     }),
55 |   ],
56 |   define: { 'process.env': {} },
57 |   resolve: {
58 |     alias: {
59 |       '@': fileURLToPath(new URL('./src', import.meta.url)),
60 |     },
61 |     extensions: [
62 |       '.js',
63 |       '.json',
64 |       '.jsx',
65 |       '.mjs',
66 |       '.ts',
67 |       '.tsx',
68 |       '.vue',
69 |     ],
70 |   },
71 |   server: {
72 |     port: 3000,
73 |   },
74 |   build: {
75 |     outDir: '../web',
76 |     emptyOutDir: true
77 |   },
78 |   css: {
79 |     preprocessorOptions: {
80 |       sass: {
81 |         api: 'modern-compiler',
82 |       },
83 |     },
84 |   },
85 | })
86 | 


--------------------------------------------------------------------------------
/hooks/hook-api.py:
--------------------------------------------------------------------------------
1 | from api import __all__
2 | 
3 | 
4 | hiddenimports = []
5 | for m in __all__:
6 |     hiddenimports.append(f'api.{m}')
7 | 


--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------
 1 | import argparse
 2 | import logging
 3 | import gevent.monkey
 4 | 
 5 | gevent.monkey.patch_ssl()
 6 | gevent.monkey.patch_socket()
 7 | 
 8 | import sys
 9 | from config import config, dump_config
10 | from utils.webview2 import can_use_webview
11 | 
12 | logger = logging.getLogger(__name__)
13 | 
14 | 
15 | def start_ui(mode=None):
16 |     import ui
17 |     ui.main(mode=mode)
18 |     return 0
19 | 
20 | 
21 | def start_webview_ui():
22 |     import ui_webview
23 |     ui_webview.main()
24 |     return 0
25 | 
26 | 
27 | def try_start_webview():
28 |     try:
29 |         return start_webview_ui()
30 |     except Exception as e:
31 |         logger.error('Error occur in start_webview_ui', e)
32 |         return fallback_to_browser()
33 | 
34 | 
35 | def fallback_to_browser():
36 |     config.setting.ui.mode = 'browser'
37 |     dump_config()
38 |     return start_ui(None)
39 | 
40 | 
41 | def create_parser():
42 |     parser = argparse.ArgumentParser()
43 |     parser.add_argument(
44 |         "-m",
45 |         "--mode",
46 |         choices=['webview', 'browser', 'chrome', 'edge', 'user default'],
47 |         help="指定 ui 启动方式",
48 |     )
49 |     parser.add_argument(
50 |         "--switch-mode",
51 |         choices=['auto', 'webview', 'browser', 'chrome', 'edge', 'user default'],
52 |         help="切换 ui 启动方式",
53 |     )
54 |     parser.add_argument(
55 |         "--no-sentry",
56 |         action='store_true',
57 |         help="禁用 sentry",
58 |     )
59 |     return parser
60 | 
61 | 
62 | def main():
63 |     parser = create_parser()
64 |     args = parser.parse_args()
65 |     logger.info(f'args: {args}')
66 | 
67 |     if args.switch_mode is not None:
68 |         logger.info(f'switch mode: {args.switch_mode}')
69 |         config.setting.ui.mode = args.switch_mode
70 |         dump_config()
71 |         return 0
72 | 
73 |     from module.external.bat_scripts import create_scripts
74 |     create_scripts()
75 | 
76 |     if not args.no_sentry:
77 |         from module.sentry import init_sentry
78 |         init_sentry()
79 | 
80 |     ui_mode = args.mode or config.setting.ui.mode
81 |     logger.info(f'ui mode: {ui_mode}')
82 |     if ui_mode is None or ui_mode == 'auto':
83 |         ui_mode = 'webview' if can_use_webview() else 'browser'
84 |     if ui_mode == 'browser':
85 |         return start_ui(None)
86 |     elif ui_mode == 'webview':
87 |         return try_start_webview()
88 |     else:
89 |         return start_ui(ui_mode)
90 | 
91 | 
92 | if __name__ == '__main__':
93 |     sys.exit(main())
94 | 


--------------------------------------------------------------------------------
/main_devnull.py:
--------------------------------------------------------------------------------
 1 | import os
 2 | import sys
 3 | 
 4 | f = open(os.devnull, 'w')
 5 | sys.stdout = f
 6 | sys.stderr = f
 7 | 
 8 | 
 9 | if __name__ == '__main__':
10 |     from main import main
11 |     main()
12 | 


--------------------------------------------------------------------------------
/module/aria2c.exe:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/triwinds/ns-emu-tools/ab6a1a301ad524e85b1b5f2796f382bba384e915/module/aria2c.exe


--------------------------------------------------------------------------------
/module/common.py:
--------------------------------------------------------------------------------
 1 | import logging
 2 | import os
 3 | import subprocess
 4 | from pathlib import Path
 5 | from module.msg_notifier import send_notify
 6 | from module.network import get_finial_url
 7 | 
 8 | logger = logging.getLogger(__name__)
 9 | 
10 | 
11 | def check_and_install_msvc():
12 |     windir = Path(os.environ['windir'])
13 |     if windir.joinpath(r'System32\msvcp140_atomic_wait.dll').exists():
14 |         from utils.common import find_installed_software, is_newer_version
15 |         software_list = find_installed_software(r'Microsoft Visual C\+\+ .+ Redistributable')
16 |         if not software_list:
17 |             logger.info(f'msvc already installed, but version not found in registry.')
18 |             return
19 |         logger.debug(f'Installed msvc: {software_list}')
20 |         if not any(is_newer_version('14.38', s['version']) for s in software_list):
21 |             logger.info(f'show update msvc notification.')
22 |             send_notify('如果在启动模拟器时提示 [无法定位程序输入点],可以试试更新你的 msvc')
23 |             send_notify('下载链接:https://aka.ms/vs/17/release/VC_redist.x64.exe')
24 |         return
25 |     from module.downloader import download
26 |     send_notify('开始下载 msvc 安装包...')
27 |     logger.info('downloading msvc installer...')
28 |     download_info = download(get_finial_url('https://aka.ms/vs/17/release/VC_redist.x64.exe'))
29 |     install_file = download_info.files[0]
30 |     send_notify('安装 msvc...')
31 |     logger.info('install msvc...')
32 |     process = subprocess.Popen([install_file.path])
33 |     # process.wait()
34 | 
35 | 
36 | def delete_path(path: str):
37 |     import shutil
38 |     path = Path(path)
39 |     logger.info(f'delete_path: {str(path)}')
40 |     if not path.exists():
41 |         send_notify(f'{str(path)} 不存在')
42 |         return
43 |     if path.is_dir():
44 |         logging.info(f'delete folder: {str(path)}')
45 |         send_notify(f'正在删除 {str(path)} 目录...')
46 |         shutil.rmtree(path, ignore_errors=True)
47 |     elif path.is_file():
48 |         logging.info(f'delete file: {str(path)}')
49 |         send_notify(f'正在删除 {str(path)} 文件...')
50 |         os.remove(path)
51 |     logger.info(f'delete_path done: {str(path)}')
52 |     send_notify(f'{str(path)} 删除完成')
53 | 
54 | 
55 | if __name__ == '__main__':
56 |     # infos = get_firmware_infos()
57 |     # for info in infos:
58 |     #     print(info)
59 |     # check_and_install_msvc()
60 |     print(check_update())
61 | 


--------------------------------------------------------------------------------
/module/dialogs.py:
--------------------------------------------------------------------------------
 1 | # Copy from https://github.com/brentvollebregt/auto-py-to-exe/blob/master/auto_py_to_exe/dialogs.py
 2 | import platform
 3 | import sys
 4 | try:
 5 |     from tkinter import Tk
 6 | except ImportError:
 7 |     try:
 8 |         from Tkinter import Tk
 9 |     except ImportError:
10 |         # If no versions of tkinter exist (most likely linux) provide a message
11 |         if sys.version_info.major < 3:
12 |             print("Error: Tkinter not found")
13 |             print('For linux, you can install Tkinter by executing: "sudo apt-get install python-tk"')
14 |             sys.exit(1)
15 |         else:
16 |             print("Error: tkinter not found")
17 |             print('For linux, you can install tkinter by executing: "sudo apt-get install python3-tk"')
18 |             sys.exit(1)
19 | try:
20 |     from tkinter.filedialog import askopenfilename, askdirectory, askopenfilenames, asksaveasfilename
21 | except ImportError:
22 |     from tkFileDialog import askopenfilename, askdirectory, askopenfilenames, asksaveasfilename
23 | 
24 | 
25 | def ask_file(file_type):
26 |     """ Ask the user to select a file """
27 |     root = Tk()
28 |     root.withdraw()
29 |     root.wm_attributes('-topmost', 1)
30 |     if (file_type is None) or (platform.system() == "Darwin"):
31 |         file_path = askopenfilename(parent=root)
32 |     else:
33 |         if file_type == 'python':
34 |             file_types = [('Python files', '*.py;*.pyw'), ('All files', '*')]
35 |         elif file_type == 'icon':
36 |             file_types = [('Icon files', '*.ico'), ('All files', '*')]
37 |         elif file_type == 'json':
38 |             file_types = [('JSON Files', '*.json'), ('All files', '*')]
39 |         else:
40 |             file_types = [('All files', '*')]
41 |         file_path = askopenfilename(parent=root, filetypes=file_types)
42 |     root.update()
43 | 
44 |     # bool(file_path) will help filter our the negative cases; an empty string or an empty tuple
45 |     return file_path if bool(file_path) else None
46 | 
47 | 
48 | def ask_files():
49 |     """ Ask the user to select one or more files """
50 |     root = Tk()
51 |     root.withdraw()
52 |     root.wm_attributes('-topmost', 1)
53 |     file_paths = askopenfilenames(parent=root)
54 |     root.update()
55 | 
56 |     return file_paths if bool(file_paths) else None
57 | 
58 | 
59 | def ask_folder():
60 |     """ Ask the user to select a folder """
61 |     root = Tk()
62 |     root.withdraw()
63 |     root.wm_attributes('-topmost', 1)
64 |     folder = askdirectory(parent=root)
65 |     root.update()
66 | 
67 |     return folder if bool(folder) else None
68 | 
69 | 
70 | def ask_file_save_location(file_type):
71 |     """ Ask the user where to save a file """
72 |     root = Tk()
73 |     root.withdraw()
74 |     root.wm_attributes('-topmost', 1)
75 | 
76 |     if (file_type is None) or (platform.system() == "Darwin"):
77 |         file_path = asksaveasfilename(parent=root)
78 |     else:
79 |         if file_type == 'json':
80 |             file_types = [('JSON Files', '*.json'), ('All files', '*')]
81 |         else:
82 |             file_types = [('All files', '*')]
83 |         file_path = asksaveasfilename(parent=root, filetypes=file_types)
84 |     root.update()
85 | 
86 |     if bool(file_path):
87 |         if file_type == 'json':
88 |             return file_path if file_path.endswith('.json') else file_path + '.json'
89 |         else:
90 |             return file_path
91 |     else:
92 |         return None
93 | 


--------------------------------------------------------------------------------
/module/external/bat_scripts.py:
--------------------------------------------------------------------------------
 1 | from pathlib import Path
 2 | 
 3 | 
 4 | switch_ui_path = Path('切换 UI 启动模式.bat')
 5 | switch_ui_template = """@echo off
 6 | chcp>nul 2>nul 65001
 7 | cd>nul 2>nul /D %~dp0
 8 | set net_path=""
 9 | if exist "NsEmuTools.exe" (
10 |     set net_path="NsEmuTools.exe"
11 | )
12 | if exist "NsEmuTools-console.exe" (
13 |     set net_path="NsEmuTools-console.exe"
14 | )
15 | if %net_path% == "" (
16 |     echo 无法在当前目录找到 NsEmuTools 可执行文件, 请将 bat 脚本与 exe 放置在同一目录下。
17 |     pause
18 | )
19 | echo %net_path%
20 | echo "切换 UI 启动模式"
21 | echo "0: 自动选择"
22 | echo "1: 通过 webview 启动"
23 | echo "2: 通过浏览器启动(自动查找浏览器)"
24 | echo "3: 通过 chrome 浏览器启动"
25 | echo "4: 通过 edge 浏览器启动"
26 | echo "5: 通过默认浏览器启动"
27 | set uc="0"
28 | set mode="auto"
29 | :GET_INPUT
30 | set /p uc=选择启动模式(输入数字):
31 | 2>NUL CALL :CASE_%uc%
32 | IF ERRORLEVEL 1 CALL :CASE_ERROR
33 | :CASE_0
34 |   set mode="auto"
35 |   GOTO END_CASE
36 | :CASE_1
37 |   set mode="webview"
38 |   GOTO END_CASE
39 | :CASE_2
40 |   set mode="browser"
41 |   GOTO END_CASE
42 | :CASE_3
43 |   set mode="chrome"
44 |   GOTO END_CASE
45 | :CASE_4
46 |   set mode="edge"
47 |   GOTO END_CASE
48 | :CASE_5
49 |   set mode="user default"
50 |   GOTO END_CASE
51 | :CASE_ERROR
52 |   echo "无法识别的模式,请重新输入"
53 |   GOTO GET_INPUT
54 | :END_CASE
55 |   call %net_path% --switch-mode %mode%
56 |   IF %ERRORLEVEL% == 0 ( 
57 |     echo "变更已保存,将于下次启动时生效。" 
58 |   ) else (
59 |     echo "发生未知错误。" 
60 |   )
61 |   pause
62 |   exit
63 | """
64 | 
65 | 
66 | def create_scripts():
67 |     with open(switch_ui_path, 'w', encoding='utf-8') as f:
68 |         f.write(switch_ui_template)
69 | 


--------------------------------------------------------------------------------
/module/msg_notifier.py:
--------------------------------------------------------------------------------
 1 | 
 2 | def dummy_notifier(msg):
 3 |     pass
 4 | 
 5 | 
 6 | def eel_notifier(msg):
 7 |     import eel
 8 |     eel.updateTopBarMsg(msg)
 9 | 
10 | 
11 | def eel_console_notifier(msg):
12 |     import eel
13 |     eel.appendConsoleMessage(msg)
14 | 
15 | 
16 | notifier = dummy_notifier
17 | 
18 | 
19 | def update_notifier(mode):
20 |     global notifier
21 |     if mode == 'eel':
22 |         notifier = eel_notifier
23 |     elif mode == 'eel-console':
24 |         notifier = eel_console_notifier
25 |     else:
26 |         notifier = dummy_notifier
27 | 
28 | 
29 | def send_notify(msg):
30 |     notifier(msg)
31 | 


--------------------------------------------------------------------------------
/module/nsz_wrapper.py:
--------------------------------------------------------------------------------
 1 | from logging import getLogger
 2 | from pathlib import Path
 3 | 
 4 | from exception.common_exception import IgnoredException
 5 | 
 6 | logger = getLogger(__name__)
 7 | 
 8 | 
 9 | def reload_key(key_path):
10 |     try:
11 |         from nsz.nut.Keys import load
12 |         load(key_path)
13 |     except:
14 |         raise IgnoredException("Failed to load key")
15 | 
16 | 
17 | def parse_nca_header(nca_path):
18 |     if isinstance(nca_path, Path):
19 |         nca_path = str(nca_path)
20 |     from nsz.Fs.Nca import Nca
21 |     nca = Nca()
22 |     try:
23 |         nca.open(nca_path)
24 |         return nca.header
25 |     finally:
26 |         nca.close()
27 | 
28 | 
29 | def read_firmware_version_from_nca(nca_path):
30 |     if isinstance(nca_path, Path):
31 |         nca_path = str(nca_path)
32 |     from nsz.Fs.Nca import Nca
33 |     nca = Nca()
34 |     try:
35 |         nca.open(nca_path)
36 |         if not nca.sectionFilesystems:
37 |             logger.info('No filesystem section found in nca.')
38 |             return None
39 |         data: bytearray = nca.sectionFilesystems[0].read()
40 |         idx = data.index(b'NX\x00\x00\x00\x00') + 0x60
41 |         # print(data[idx:])
42 |         version = data[idx:idx + 0x10].replace(b'\x00', b'').decode()
43 |         return version
44 |     finally:
45 |         nca.close()
46 | 


--------------------------------------------------------------------------------
/module/save_manager.py:
--------------------------------------------------------------------------------
  1 | import logging
  2 | import shutil
  3 | import time
  4 | 
  5 | from module.yuzu import get_yuzu_nand_path
  6 | from module.cheats import get_game_data, game_id_re
  7 | from module.msg_notifier import send_notify
  8 | from pathlib import Path
  9 | from storage import storage, dump_storage
 10 | from exception.common_exception import IgnoredException
 11 | from utils.package import compress_folder, uncompress, is_7zfile
 12 | from utils.common import is_path_in_use
 13 | 
 14 | 
 15 | logger = logging.getLogger(__name__)
 16 | 
 17 | 
 18 | def get_yuzu_save_path():
 19 |     # https://github.com/yuzu-emu/yuzu/blob/e264ab4ad0137224559ac0bacc68b905db65f5a8/src/core/file_sys/savedata_factory.cpp#L197
 20 |     return get_yuzu_nand_path().joinpath('user/save/0000000000000000')
 21 | 
 22 | 
 23 | def _get_all_user_ids():
 24 |     return [folder.name for folder in get_yuzu_save_path().glob('*') if len(folder.name) == 32]
 25 | 
 26 | 
 27 | def get_users_in_save():
 28 |     return [{'user_id': convert_to_uuid(uid), 'folder': uid} for uid in _get_all_user_ids()]
 29 | 
 30 | 
 31 | def convert_to_uuid(user_id: str):
 32 |     tmp = ''
 33 |     for i in range(16):
 34 |         tmp = user_id[i*2:i*2+2] + tmp
 35 |     return f'{tmp[:8]}-{tmp[8:12]}-{tmp[12:16]}-{tmp[16:20]}-{tmp[20:]}'.lower()
 36 | 
 37 | 
 38 | def list_all_games_by_user_folder(user_folder_name: str):
 39 |     user_save_folder = get_yuzu_save_path().joinpath(user_folder_name)
 40 |     res = []
 41 |     for folder in user_save_folder.glob('*'):
 42 |         if game_id_re.match(folder.name):
 43 |             res.append({
 44 |                 'title_id': folder.name,
 45 |                 'folder': str(folder.absolute())
 46 |             })
 47 |     return res
 48 | 
 49 | 
 50 | def backup_folder(folder_path: str):
 51 |     yuzu_save_backup_path = Path(storage.yuzu_save_backup_path)
 52 |     if not yuzu_save_backup_path.exists():
 53 |         yuzu_save_backup_path.mkdir(parents=True, exist_ok=True)
 54 |     folder_path = Path(folder_path)
 55 |     if is_path_in_use(folder_path):
 56 |         logger.info(f'{folder_path} is in use.')
 57 |         raise IgnoredException(f'{folder_path} 目录正在使用中,跳过备份。')
 58 |     backup_filename = f'yuzu_{folder_path.name}_{int(time.time())}.7z'
 59 |     backup_filepath = yuzu_save_backup_path.joinpath(backup_filename)
 60 |     logger.info(f'backup folder [{str(folder_path)}] to {str(backup_filepath)}')
 61 |     send_notify(f'正在备份文件夹 [{str(folder_path)}] 至 {str(backup_filepath)}')
 62 |     compress_folder(folder_path, backup_filepath)
 63 |     logger.info(f'{str(backup_filepath)} backup finished, size: {sizeof_fmt(backup_filepath.stat().st_size)}.')
 64 |     send_notify(f'{str(backup_filepath)} 备份完成, size: {sizeof_fmt(backup_filepath.stat().st_size)}')
 65 | 
 66 | 
 67 | def sizeof_fmt(num, suffix="B"):
 68 |     for unit in ["", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi"]:
 69 |         if abs(num) < 1024.0:
 70 |             return f"{num:3.1f}{unit}{suffix}"
 71 |         num /= 1024.0
 72 |     return f"{num:.1f}Yi{suffix}"
 73 | 
 74 | 
 75 | def ask_and_update_yuzu_save_backup_folder():
 76 |     from module.dialogs import ask_folder
 77 |     folder = ask_folder()
 78 |     logger.info(f'select yuzu_save_backup_folder: {folder}, current: {storage.yuzu_save_backup_path}')
 79 |     if not folder:
 80 |         send_notify('未选择文件夹, 取消变更.')
 81 |         return
 82 |     new_path = Path(folder).absolute()
 83 |     if Path(storage.yuzu_save_backup_path).absolute() == new_path:
 84 |         send_notify('文件夹未发生变动, 取消变更.')
 85 |         return
 86 |     storage.yuzu_save_backup_path = str(new_path.absolute())
 87 |     dump_storage()
 88 |     logger.info(f'new yuzu_save_backup_path: {storage.yuzu_save_backup_path}')
 89 |     send_notify(f'yuzu 存档备份文件夹更改为: {storage.yuzu_save_backup_path}')
 90 | 
 91 | 
 92 | def open_yuzu_save_backup_folder():
 93 |     import subprocess
 94 |     path = Path(storage.yuzu_save_backup_path)
 95 |     path.mkdir(parents=True, exist_ok=True)
 96 |     logger.info(f'open explorer on path {path}')
 97 |     subprocess.Popen(f'explorer "{str(path.absolute())}"')
 98 | 
 99 | 
100 | def parse_backup_info(file: Path):
101 |     res = {'filename': file.name, 'path': str(file.absolute())}
102 |     if file.name.startswith('yuzu_') and file.name.endswith('.7z'):
103 |         s = file.name[5:-3]
104 |         title_id, bak_time = s.split('_')
105 |         res['title_id'] = title_id
106 |         res['bak_time'] = int(bak_time) * 1000
107 |     return res
108 | 
109 | 
110 | def list_all_yuzu_backups():
111 |     path = Path(storage.yuzu_save_backup_path)
112 |     res = []
113 |     if not path.exists():
114 |         return res
115 |     for file in path.glob('yuzu_*.7z'):
116 |         res.append(parse_backup_info(file))
117 |     return sorted(res, key=lambda x: x['bak_time'], reverse=True)
118 | 
119 | 
120 | def restore_yuzu_save_from_backup(user_folder_name: str, backup_path: str):
121 |     backup_path = Path(backup_path)
122 |     if not is_7zfile(backup_path):
123 |         logger.info(f'{str(backup_path)} seems not a 7z file.')
124 |         send_notify(f'{str(backup_path)} 看起来不是一个完整的 7z 文件,跳过还原.')
125 |         return
126 |     backup_info = parse_backup_info(backup_path)
127 |     logger.info(f'backup_info: {backup_info}')
128 |     user_save_path = get_yuzu_save_path().joinpath(user_folder_name)
129 |     target_game_save_path = user_save_path.joinpath(backup_info['title_id'])
130 |     if is_path_in_use(target_game_save_path):
131 |         logger.info(f'{str(target_game_save_path)} is in use, skip restore.')
132 |         send_notify(f'{str(target_game_save_path)} 目录正在使用中,跳过还原.')
133 |         return
134 |     logger.info(f'removing path: {str(target_game_save_path)}')
135 |     send_notify(f'正在清空目录 {str(target_game_save_path)}')
136 |     shutil.rmtree(target_game_save_path, ignore_errors=True)
137 |     logger.info(f'uncompress to {str(target_game_save_path)}')
138 |     send_notify(f'正在解压备份至 {str(user_save_path)}')
139 |     uncompress(backup_path, user_save_path, False, '备份')
140 |     logger.info(f'{str(backup_path)} restore done.')
141 |     send_notify(f'{backup_path.name} 还原完成')
142 | 
143 | 
144 | if __name__ == '__main__':
145 |     from pprint import pprint
146 |     # print(get_all_user_ids())
147 |     # print(convert_to_uuid('97A1DAE861CD445AB9645267B3AB99BE'))
148 |     # print(get_users_in_save())
149 |     # pprint(list_all_games_by_user_folder('97A1DAE861CD445AB9645267B3AB99BE'))
150 |     # storage.yuzu_save_backup_path = 'R:/'
151 |     # backup_folder('D:\\Yuzu\\user\\nand\\user\\save\\0000000000000000\\97A1DAE861CD445AB9645267B3AB99BE\\0100F3400332C000')
152 |     # pprint(list_all_yuzu_backups())
153 |     restore_yuzu_save_from_backup('97A1DAE861CD445AB9645267B3AB99BE',
154 |                                   'D:\\yuzu_save_backup\\yuzu_0100F2C0115B6000_1685114415.7z')
155 | 


--------------------------------------------------------------------------------
/module/sentry.py:
--------------------------------------------------------------------------------
 1 | import sentry_sdk
 2 | from config import current_version
 3 | 
 4 | 
 5 | def sampler(sample_data):
 6 |     if 'wsgi_environ' in sample_data and sample_data['wsgi_environ']['PATH_INFO'] == '/eel.js':
 7 |         return 0.1
 8 |     return 0
 9 | 
10 | 
11 | def init_sentry():
12 |     sentry_sdk.init(
13 |         dsn="https://022fb678c5bc4859b6052fc409506f23@o527477.ingest.sentry.io/4504689953472512",
14 |         auto_session_tracking=False,
15 |         traces_sampler=sampler,
16 |         release=f'ns-emu-tools@{current_version}'
17 |     )
18 |     sentry_sdk.set_user({'ip_address': '{{auto}}'})
19 | 


--------------------------------------------------------------------------------
/module/updater.py:
--------------------------------------------------------------------------------
  1 | from module.downloader import download, download_path
  2 | from module.msg_notifier import send_notify
  3 | import sys
  4 | from pathlib import Path
  5 | import subprocess
  6 | import logging
  7 | from config import current_version
  8 | 
  9 | 
 10 | logger = logging.getLogger(__name__)
 11 | script_template = """@echo off
 12 | chcp>nul 2>nul 65001
 13 | echo 开始准备更新
 14 | timeout /t 3 /nobreak
 15 | taskkill /F /IM NsEmuTools* >nul 2>nul
 16 | if exist "<old_exe>" (
 17 |   echo 备份原文件至 "<old_exe>.bak"
 18 |   move /Y "<old_exe>" "<old_exe>.bak"
 19 | )
 20 | if exist "_internal" (
 21 |   move /Y "_internal" "_internal_bak"
 22 |   timeout /t 1 /nobreak
 23 | )
 24 | if not exist "<upgrade_files_folder>" (
 25 |   echo 无法找到更新文件 "<upgrade_files_folder>"
 26 |   pause
 27 | ) else (
 28 |   echo 复制文件中
 29 |   robocopy "<upgrade_files_folder>" . /MOVE /E /NFL /NDL /NC
 30 |   if exist "download/upgrade_files" (
 31 |     timeout /t 1 /nobreak
 32 |     rmdir /s /q "download/upgrade_files"
 33 |   )
 34 |   echo 启动程序
 35 |   start /b "NsEmuTools" "<target_place>"
 36 | )
 37 | DEL "%~f0"
 38 | """
 39 | 
 40 | 
 41 | def _parse_version(version_str):
 42 |     qualifier = 'zzzzzzzzzzzz'
 43 |     sp = version_str.split('-')
 44 |     if len(sp) == 2:
 45 |         version, qualifier = sp
 46 |     else:
 47 |         version = version_str
 48 |     version = version.strip()
 49 |     major, minor, incr = version.split('.')
 50 |     return int(major), int(minor), int(incr), qualifier
 51 | 
 52 | 
 53 | def check_update(prerelease=False):
 54 |     from repository.my_info import get_all_release
 55 |     cur_ver_group = _parse_version(current_version)
 56 |     release_infos = get_all_release()
 57 |     remote_version = None
 58 |     if prerelease:
 59 |         remote_version = release_infos[0]['tag_name']
 60 |     else:
 61 |         for ri in release_infos:
 62 |             if not ri['prerelease']:
 63 |                 remote_version = ri['tag_name']
 64 |                 break
 65 |     if not remote_version:
 66 |         remote_version = release_infos[0]['tag_name']
 67 |     remote_ver_group = _parse_version(remote_version)
 68 |     return cur_ver_group < remote_ver_group, remote_version
 69 | 
 70 | 
 71 | def download_net_by_tag(tag: str):
 72 |     from repository.my_info import get_release_info_by_tag
 73 |     from module.network import get_github_download_url
 74 |     release_info = get_release_info_by_tag(tag)
 75 |     logger.info(f'start download NET release by tag: {tag}, release name: {release_info.get("name")}')
 76 |     execute_path = Path(sys.argv[0])
 77 |     logger.info(f'execute_path: {execute_path}')
 78 |     asset_map = {asset['name']: asset for asset in release_info['assets']}
 79 |     target_asset = asset_map.get('NsEmuTools-dir.7z')
 80 |     if not target_asset:
 81 |         target_asset = asset_map.get(execute_path.name, asset_map.get('NsEmuTools.exe'))
 82 |     target_file_name = target_asset["name"]
 83 |     logger.info(f'target_file_name: {target_file_name}')
 84 |     logger.info(f'start download {target_file_name}, version: [{tag}]')
 85 |     send_notify(f'开始下载 {target_file_name}, 版本: [{tag}]')
 86 |     upgrade_files_path = download_path.joinpath('upgrade_files')
 87 |     info = download(get_github_download_url(target_asset['browser_download_url']),
 88 |                     save_dir=str(upgrade_files_path.absolute()),
 89 |                     options={'allow-overwrite': 'true'})
 90 |     filepath = info.files[0].path.absolute()
 91 |     logger.info(f'{target_file_name} of [{tag}] downloaded to {filepath}')
 92 |     send_notify(f'{target_file_name} 版本: [{tag}] 已下载至')
 93 |     send_notify(f'{filepath}')
 94 |     return filepath
 95 | 
 96 | 
 97 | def update_self_by_tag(tag: str):
 98 |     # upgrade_files_path = download_path.joinpath('upgrade_files')
 99 |     # upgrade_file_path = upgrade_files_path.joinpath('NsEmuTools.7z')
100 |     upgrade_file_path = download_net_by_tag(tag)
101 |     upgrade_files_folder = upgrade_file_path.parent
102 |     if not upgrade_file_path:
103 |         logger.error(f'something wrong in downloading.')
104 |         send_notify(f'下载时出现问题, 更新已取消.')
105 |         return
106 |     if upgrade_file_path.name.endswith('.7z'):
107 |         from utils.package import uncompress
108 |         uncompress(upgrade_file_path, upgrade_file_path.parent)
109 |         upgrade_file_path.unlink()
110 |         upgrade_files_folder = upgrade_file_path.parent.joinpath('NsEmuTools')
111 |     target_path = Path('NsEmuTools.exe') if Path('NsEmuTools.exe').exists() else Path('NsEmuTools-console.exe')
112 |     script = script_template\
113 |         .replace('<old_exe>', str(Path(sys.argv[0]).absolute()))\
114 |         .replace('<upgrade_files_folder>', str(upgrade_files_folder))\
115 |         .replace('<target_place>', str(target_path.absolute()))
116 |     logger.info(f'creating update script')
117 |     with open('update.bat', 'w', encoding='utf-8') as f:
118 |         f.write(script)
119 |     script_path = Path(sys.argv[0]).parent.joinpath('update.bat').absolute()
120 |     logger.info(f'execute script')
121 |     subprocess.Popen(f'start cmd /c "{script_path}"', shell=True)
122 |     try:
123 |         from ui_webview import close_all_windows
124 |         close_all_windows()
125 |     except:
126 |         pass
127 |     send_notify(f'由于浏览器的安全限制,程序无法主动关闭当前窗口。因此请手动关闭当前窗口。')
128 |     send_notify(f'webview 版本可以避免这个问题,如果你的系统版本比较新,可以尝试使用一下 webview 版本。')
129 |     logger.info(f'exit')
130 |     sys.exit()
131 | 
132 | 
133 | if __name__ == '__main__':
134 |     # print(check_update())
135 |     print(_parse_version('0.0.1') < _parse_version('0.0.2'))
136 |     print(_parse_version('0.0.1-beta1') < _parse_version('0.0.1'))
137 |     print(_parse_version('0.0.1-beta1') < _parse_version('0.0.1-beta2'))
138 |     print(_parse_version('0.0.1-alpha1') < _parse_version('0.0.1-beta2'))
139 | 


--------------------------------------------------------------------------------
/package.bat:
--------------------------------------------------------------------------------
 1 | @echo off
 2 | chcp>nul 2>nul 65001
 3 | :path
 4 | cd>nul 2>nul /D %~dp0
 5 | rem call venv\Scripts\activate.bat
 6 | rem pyinstaller --noconfirm --onefile --windowed --icon "./web/favicon.ico" --add-data "./module/*.exe;./module/" --add-data "./web;web/"  "./main_devnull.py" --additional-hooks-dir=".\\hooks" --name "NsEmuTools"
 7 | rem poetry run pyinstaller --noconfirm --onefile --console --upx-dir "./build_tools/upx/" --icon "./web/favicon.ico" --add-data "./module/*.exe;./module/" --add-data "./web;web/"  "./main.py" --additional-hooks-dir=".\\hooks" --name "NsEmuTools-console"
 8 | uv run pyinstaller --noconfirm --windowed --icon "./web/favicon.ico" --add-data "./module/*.exe;./module/" --add-data "./web;web/"  "./main_devnull.py" --additional-hooks-dir=".\\hooks" --name "NsEmuTools"
 9 | pause
10 | 


--------------------------------------------------------------------------------
/package_all.bat:
--------------------------------------------------------------------------------
 1 | @echo off
 2 | chcp>nul 2>nul 65001
 3 | :path
 4 | cd>nul 2>nul /D %~dp0
 5 | rem call venv\Scripts\activate.bat
 6 | uv run pyinstaller --noconfirm --onefile --windowed --icon "./web/favicon.ico" --add-data "./module/*.exe;./module/" --add-data "./web;web/"  "./main_devnull.py" --additional-hooks-dir=".\\hooks" --name "NsEmuTools"
 7 | uv run pyinstaller --noconfirm --onefile --console --icon "./web/favicon.ico" --add-data "./module/*.exe;./module/" --add-data "./web;web/"  "./main.py" --additional-hooks-dir=".\\hooks" --name "NsEmuTools-console"
 8 | uv run pyinstaller --noconfirm --windowed --icon "./web/favicon.ico" --add-data "./module/*.exe;./module/" --add-data "./web;web/"  "./main_devnull.py" --additional-hooks-dir=".\\hooks" --name "NsEmuTools"
 9 | uv run python build_tools/zip_files.py
10 | pause
11 | 


--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
 1 | [project]
 2 | name = "ns-emu-tools"
 3 | version = "0.4.2"
 4 | description = ""
 5 | authors = [{ name = "triwinds", email = "triwinds@users.noreply.github.com" }]
 6 | requires-python = ">=3.11"
 7 | readme = "README.md"
 8 | dependencies = [
 9 |     "tqdm>=4.66.1,<5",
10 |     "aria2p>=0.11.3,<0.12",
11 |     "requests>=2.31.0,<3",
12 |     "py7zr>=0.20.6,<0.21",
13 |     "beautifulsoup4>=4.12.2,<5",
14 |     "eel>=0.16.0,<0.17",
15 |     "pywin32>=306,<307",
16 |     "psutil>=5.9.5,<6",
17 |     "gevent>=23.7.0,<24",
18 |     "httplib2>=0.22.0,<0.23",
19 |     "requests-cache>=1.1.0,<2",
20 |     "dnspython[doh]>=2.4.2,<3",
21 |     "sentry-sdk>=1.29.2,<2",
22 |     "xmltodict>=0.13.0,<0.14",
23 |     "chardet>=5.2.0,<6",
24 |     "pywebview>=4.2.2,<5",
25 |     "dataclasses-json>=0.5.14,<0.6",
26 |     "pyinstaller>=6.3.0,<7",
27 |     "nsz",
28 |     "orjson>=3.10.18",
29 | ]
30 | 
31 | [dependency-groups]
32 | dev = ["pyinstaller>=6.0.0,<7"]
33 | 
34 | [tool.uv]
35 | 
36 | [tool.uv.sources]
37 | nsz = { git = "https://github.com/triwinds/nsz" }
38 | 
39 | [tool.hatch.build.targets.sdist]
40 | include = ["module"]
41 | 
42 | [tool.hatch.build.targets.wheel]
43 | include = ["module"]
44 | 
45 | [build-system]
46 | requires = ["hatchling"]
47 | build-backend = "hatchling.build"
48 | 


--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
 1 | # Ns Emu Tools
 2 | 
 3 | 一个用于安装/更新 NS 模拟器的工具
 4 | 
 5 | ![GitHub release (latest by date)](https://img.shields.io/github/v/release/triwinds/ns-emu-tools?style=for-the-badge)
 6 | ![GitHub last commit](https://img.shields.io/github/last-commit/triwinds/ns-emu-tools?style=for-the-badge)
 7 | ![GitHub all releases](https://img.shields.io/github/downloads/triwinds/ns-emu-tools/total?style=for-the-badge)
 8 | ![GitHub Repo stars](https://img.shields.io/github/stars/triwinds/ns-emu-tools?style=for-the-badge)
 9 | ![GitHub](https://img.shields.io/github/license/triwinds/ns-emu-tools?style=for-the-badge)
10 | 
11 | ## Features
12 | 
13 |  - ~~支持安装 Yuzu EA/正式版模拟器~~ 
14 |  - ~~支持 Yuzu 版本检测及更新~~ (yuzu 目前已经停止开发)
15 |  - 支持安装 Ryubing/Ryujinx 正式/Canary 版模拟器
16 |  - 支持 Ryujinx 版本检测及更新
17 |  - 自动检测并安装 msvc 运行库
18 |  - 支持安装及更新 NS 固件至模拟器
19 |  - 支持固件版本检测 (感谢 [a709560839](https://tieba.baidu.com/home/main?id=tb.1.f9804802.YmDokXJSRkAJB0xF8XfaCQ&fr=pb) 提供的思路)
20 |  - 管理模拟器密钥
21 |  - Yuzu 金手指管理
22 |  - aria2 多线程下载
23 | 
24 | ## 使用方法
25 | 
26 | ### 一、使用预构建的版本运行
27 | 
28 | 从 [GitHub 发布页(稳定版本)](https://github.com/triwinds/ns-emu-tools/releases) 或 
29 | [CI 自动构建](https://github.com/triwinds/ns-emu-tools/actions/workflows/ci-build.yaml) 下载 exe 文件,然后双击运行即可。
30 | <details>
31 | <summary>NsEmuTools.exe 和 NsEmuTools-console.exe 有什么区别?</summary>
32 | NsEmuTools.exe 和 NsEmuTools-console.exe 在实际的功能上并没有任何差异,
33 | 其主要的差别在于 console 会在启动的时候多一个命令行窗口,这也许可以解决某些杀毒软件的误报问题,
34 | 详情见 <a href="https://github.com/triwinds/ns-emu-tools/issues/2">#2</a>.
35 | </details>
36 | 
37 | 
38 | ### 二、使用源码运行
39 | 
40 | 参见 [开发文档](doc/dev.md)
41 | 
42 | 
43 | ## 讨论组
44 | 
45 | Telegram: [Telegram 讨论组](https://t.me/+mxI34BRClLUwZDcx)
46 | 
47 | 
48 | ## License
49 | 
50 | 本项目的发布受 [AGPL-3.0](https://github.com/triwinds/ns-emu-tools/blob/main/LICENSE) 许可认证。
51 | 
52 | ## 启动参数
53 | 
54 | ```
55 | usage: NsEmuTools-console.exe [-h] [-m {webview,browser,chrome,edge,user default}]
56 |                               [--switch-mode {auto,webview,browser,chrome,edge,user default}]
57 | 
58 | options:
59 |   -h, --help            show this help message and exit
60 |   -m {webview,browser,chrome,edge,user default}, --mode {webview,browser,chrome,edge,user default}
61 |                         指定 ui 启动方式
62 |   --switch-mode {auto,webview,browser,chrome,edge,user default}
63 |                         切换 ui 启动方式
64 | ```
65 | 
66 | ## Credits
67 | 
68 |  - ~~[Yuzu](https://github.com/yuzu-emu/yuzu) - Yuzu 模拟器~~
69 |  - [Ryubing/Ryujinx](https://ryujinx.app/) - Ryujinx 模拟器
70 |  - [Suyu](https://git.suyu.dev/suyu/suyu) - Suyu 模拟器
71 |  - ~~[hactool](https://github.com/SciresM/hactool) - NS 固件解析~~
72 |  - [nsz](https://github.com/nicoboss/nsz) - NS 固件解析
73 |  - [aria2](https://github.com/aria2/aria2) - aria2 下载器
74 |  - [Github 镜像源](https://github.com/XIU2/UserScript/blob/master/GithubEnhanced-High-Speed-Download.user.js) - 来自 X.I.U 大佬的 Github 增强脚本
75 |  - [pineappleEA](https://github.com/pineappleEA/pineapple-src) - Yuzu EA 版本来源
76 |  - [THZoria/NX_Firmware](https://github.com/THZoria/NX_Firmware) - NS 固件来源
77 |  - [darthsternie.net](https://darthsternie.net/switch-firmwares/) - NS 固件来源
78 | 


--------------------------------------------------------------------------------
/repository/domain/release_info.py:
--------------------------------------------------------------------------------
 1 | from dataclasses import dataclass
 2 | from typing import List, Dict
 3 | 
 4 | 
 5 | @dataclass
 6 | class ReleaseAsset:
 7 |     name: str
 8 |     download_url: str
 9 | 
10 |     def __init__(self, name, download_url):
11 |         self.name = name
12 |         self.download_url = download_url
13 | 
14 | 
15 | @dataclass
16 | class ReleaseInfo:
17 |     name: str
18 |     tag_name: str
19 |     description: str
20 |     assets: List[ReleaseAsset]
21 | 
22 | 
23 | def from_gitlab_api(release_info):
24 |     assets = []
25 |     for asset in release_info['assets']['links']:
26 |         assets.append(ReleaseAsset(asset['name'], asset['url']))
27 |     return ReleaseInfo(
28 |         name=release_info['name'],
29 |         tag_name=release_info['tag_name'],
30 |         description=release_info['description'],
31 |         assets=assets
32 |     )
33 | 
34 | def from_github_api(release_info):
35 |     assets = []
36 |     for asset in release_info['assets']:
37 |         assets.append(ReleaseAsset(asset['name'], asset['browser_download_url']))
38 |     return ReleaseInfo(
39 |         name=release_info['name'],
40 |         tag_name=release_info['tag_name'],
41 |         description=release_info['body'],
42 |         assets=assets
43 |     )
44 | 


--------------------------------------------------------------------------------
/repository/my_info.py:
--------------------------------------------------------------------------------
 1 | from module.network import request_github_api, session, get_finial_url
 2 | 
 3 | 
 4 | def get_all_release():
 5 |     with session.cache_disabled():
 6 |         return request_github_api('https://api.github.com/repos/triwinds/ns-emu-tools/releases')
 7 | 
 8 | 
 9 | def get_latest_release(prerelease=False):
10 |     with session.cache_disabled():
11 |         data = get_all_release()
12 |         release_list = data if prerelease else [i for i in data if i['prerelease'] is False]
13 |         return release_list[0]
14 | 
15 | 
16 | def get_release_info_by_tag(tag: str):
17 |     return request_github_api(f'https://api.github.com/repos/triwinds/ns-emu-tools/releases/tags/{tag}')
18 | 
19 | 
20 | def load_change_log():
21 |     resp = session.get(get_finial_url('https://raw.githubusercontent.com/triwinds/ns-emu-tools/main/changelog.md'))
22 |     return resp.text
23 | 


--------------------------------------------------------------------------------
/repository/ryujinx.py:
--------------------------------------------------------------------------------
 1 | from module.network import request_github_api, session, get_finial_url
 2 | from repository.domain.release_info import *
 3 | 
 4 | 
 5 | # They move them codes to https://git.ryujinx.app/ryubing/ryujinx
 6 | # the api of releases https://git.ryujinx.app/api/v4/projects/1/releases  (using GitLab)
 7 | # Gitlab Canary releases: https://git.ryujinx.app/api/v4/projects/68/releases
 8 | 
 9 | 
10 | def get_all_ryujinx_release_infos(branch='mainline')-> List[ReleaseInfo]:
11 |     if branch == 'canary':
12 |         return get_all_canary_ryujinx_release_infos()
13 |     # return request_github_api('https://api.github.com/repos/Ryubing/Stable-Releases/releases')
14 |     resp = session.get('https://git.ryujinx.app/api/v4/projects/1/releases').json()
15 |     res =  [from_gitlab_api(item) for item in resp]
16 |     return res
17 | 
18 | 
19 | def get_all_canary_ryujinx_release_infos() -> List[ReleaseInfo]:
20 |     resp = session.get('https://git.ryujinx.app/api/v4/projects/68/releases').json()
21 |     return [from_gitlab_api(item) for item in resp]
22 | 
23 | 
24 | def get_latest_ryujinx_release_info() -> ReleaseInfo:
25 |     return get_all_ryujinx_release_infos()[0]
26 | 
27 | 
28 | def get_ryujinx_release_info_by_version(version, branch='mainline') -> ReleaseInfo:
29 |     if branch == 'canary':
30 |         return get_canary_ryujinx_release_info_by_version(version)
31 | #     get from gitlab
32 |     return from_gitlab_api(session.get(f'https://git.ryujinx.app/api/v4/projects/1/releases/{version}').json())
33 | 
34 | 
35 | 
36 | def get_canary_ryujinx_release_info_by_version(version) -> ReleaseInfo:
37 |     return from_gitlab_api(session.get(f'https://git.ryujinx.app/api/v4/projects/68/releases/{version}').json())
38 | 
39 | 
40 | def load_ryujinx_change_log(branch: str) -> str:
41 |     infos = get_all_ryujinx_release_infos(branch)
42 |     return infos[0].description
43 | 


--------------------------------------------------------------------------------
/repository/suyu.py:
--------------------------------------------------------------------------------
 1 | from module.network import session, get_finial_url
 2 | 
 3 | 
 4 | # Api doc: https://git.suyu.dev/api/swagger
 5 | 
 6 | def load_suyu_releases():
 7 |     resp = session.get(get_finial_url('https://git.suyu.dev/api/v1/repos/suyu/suyu/releases'))
 8 |     return resp.json()
 9 | 
10 | 
11 | def get_release_by_tag_name(tag_name: str):
12 |     resp = session.get(get_finial_url(f'https://git.suyu.dev/api/v1/repos/suyu/suyu/releases/tags/{tag_name}'))
13 |     return resp.json()
14 | 
15 | 
16 | def get_all_suyu_release_versions():
17 |     res = []
18 |     data = load_suyu_releases()
19 |     for item in data:
20 |         res.append(item['tag_name'])
21 |     return res
22 | 


--------------------------------------------------------------------------------
/repository/yuzu.py:
--------------------------------------------------------------------------------
 1 | from module.network import request_github_api
 2 | 
 3 | 
 4 | def get_all_yuzu_release_infos():
 5 |     data = request_github_api('https://api.github.com/repos/pineappleEA/pineapple-src/releases')
 6 |     res = [item for item in data if item['author']['login'] == 'pineappleEA']
 7 |     return res
 8 | 
 9 | 
10 | def get_all_yuzu_release_versions(branch: str):
11 |     res = []
12 |     if branch.lower() == 'mainline':
13 |         data = request_github_api('https://api.github.com/repos/yuzu-emu/yuzu-mainline/releases')
14 |         for item in data:
15 |             res.append(item['tag_name'][11:])
16 |     else:
17 |         data = request_github_api('https://api.github.com/repos/pineappleEA/pineapple-src/releases')
18 |         for item in data:
19 |             if item['author']['login'] == 'pineappleEA':
20 |                 res.append(item['tag_name'][3:])
21 |     return res
22 | 
23 | 
24 | def get_latest_yuzu_release_info():
25 |     return get_all_yuzu_release_infos()[0]
26 | 
27 | 
28 | def get_yuzu_release_info_by_version(version, branch='ea'):
29 |     if branch.lower() == 'mainline':
30 |         url = f'https://api.github.com/repos/yuzu-emu/yuzu-mainline/releases/tags/mainline-0-{version}'
31 |     else:
32 |         url = f'https://api.github.com/repos/pineappleEA/pineapple-src/releases/tags/EA-{version}'
33 |     return request_github_api(url)
34 | 
35 | 
36 | if __name__ == '__main__':
37 |     print(get_all_yuzu_release_versions('mainline'))
38 | 


--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
 1 | anyio==3.7.1 ; python_version >= "3.10" and python_version < "3.12"
 2 | appdirs==1.4.4 ; python_version >= "3.10" and python_version < "3.12"
 3 | aria2p==0.11.3 ; python_version >= "3.10" and python_version < "3.12"
 4 | attrs==23.1.0 ; python_version >= "3.10" and python_version < "3.12"
 5 | beautifulsoup4==4.12.2 ; python_version >= "3.10" and python_version < "3.12"
 6 | bottle-websocket==0.2.9 ; python_version >= "3.10" and python_version < "3.12"
 7 | bottle==0.12.25 ; python_version >= "3.10" and python_version < "3.12"
 8 | brotli==1.0.9 ; python_version >= "3.10" and python_version < "3.12" and platform_python_implementation == "CPython"
 9 | brotlicffi==1.0.9.2 ; python_version >= "3.10" and python_version < "3.12" and platform_python_implementation == "PyPy"
10 | cattrs==23.1.2 ; python_version >= "3.10" and python_version < "3.12"
11 | certifi==2023.7.22 ; python_version >= "3.10" and python_version < "3.12"
12 | cffi==1.15.1 ; python_version >= "3.10" and python_version < "3.12" and (sys_platform == "win32" or platform_python_implementation == "PyPy")
13 | chardet==5.2.0 ; python_version >= "3.10" and python_version < "3.12"
14 | charset-normalizer==3.2.0 ; python_version >= "3.10" and python_version < "3.12"
15 | clr-loader==0.2.6 ; python_version >= "3.10" and python_version < "3.12" and sys_platform == "win32"
16 | colorama==0.4.6 ; python_version >= "3.10" and python_version < "3.12" and (platform_system == "Windows" or sys_platform == "win32")
17 | dataclasses-json==0.5.14 ; python_version >= "3.10" and python_version < "3.12"
18 | dnspython[doh]==2.4.2 ; python_version >= "3.10" and python_version < "3.12"
19 | eel==0.16.0 ; python_version >= "3.10" and python_version < "3.12"
20 | exceptiongroup==1.1.3 ; python_version >= "3.10" and python_version < "3.11"
21 | future==0.18.3 ; python_version >= "3.10" and python_version < "3.12"
22 | gevent-websocket==0.10.1 ; python_version >= "3.10" and python_version < "3.12"
23 | gevent==23.7.0 ; python_version >= "3.10" and python_version < "3.12"
24 | greenlet==2.0.2 ; python_version >= "3.10" and platform_python_implementation == "CPython" and python_version < "3.12"
25 | h11==0.14.0 ; python_version >= "3.10" and python_version < "3.12"
26 | h2==4.1.0 ; python_version >= "3.10" and python_version < "3.12"
27 | hpack==4.0.0 ; python_version >= "3.10" and python_version < "3.12"
28 | httpcore==0.17.3 ; python_version >= "3.10" and python_version < "3.12"
29 | httplib2==0.22.0 ; python_version >= "3.10" and python_version < "3.12"
30 | httpx==0.24.1 ; python_version >= "3.10" and python_version < "3.12"
31 | hyperframe==6.0.1 ; python_version >= "3.10" and python_version < "3.12"
32 | idna==3.4 ; python_version >= "3.10" and python_version < "3.12"
33 | inflate64==0.3.1 ; python_version >= "3.10" and python_version < "3.12"
34 | loguru==0.7.0 ; python_version >= "3.10" and python_version < "3.12"
35 | marshmallow==3.20.1 ; python_version >= "3.10" and python_version < "3.12"
36 | multivolumefile==0.2.3 ; python_version >= "3.10" and python_version < "3.12"
37 | mypy-extensions==1.0.0 ; python_version >= "3.10" and python_version < "3.12"
38 | packaging==23.1 ; python_version >= "3.10" and python_version < "3.12"
39 | platformdirs==3.10.0 ; python_version >= "3.10" and python_version < "3.12"
40 | proxy-tools==0.1.0 ; python_version >= "3.10" and python_version < "3.12"
41 | psutil==5.9.5 ; python_version >= "3.10" and python_version < "3.12"
42 | py7zr==0.20.6 ; python_version >= "3.10" and python_version < "3.12"
43 | pybcj==1.0.1 ; python_version >= "3.10" and python_version < "3.12"
44 | pycparser==2.21 ; python_version >= "3.10" and python_version < "3.12" and (sys_platform == "win32" or platform_python_implementation == "PyPy")
45 | pycryptodomex==3.18.0 ; python_version >= "3.10" and python_version < "3.12"
46 | pyobjc-core==9.2 ; python_version >= "3.10" and python_version < "3.12" and sys_platform == "darwin"
47 | pyobjc-framework-cocoa==9.2 ; python_version >= "3.10" and python_version < "3.12" and sys_platform == "darwin"
48 | pyobjc-framework-security==9.2 ; python_version >= "3.10" and python_version < "3.12" and sys_platform == "darwin"
49 | pyobjc-framework-webkit==9.2 ; python_version >= "3.10" and python_version < "3.12" and sys_platform == "darwin"
50 | pyparsing==3.1.1 ; python_version >= "3.10" and python_version < "3.12"
51 | pyppmd==1.0.0 ; python_version >= "3.10" and python_version < "3.12"
52 | pythonnet==3.0.1 ; python_version >= "3.10" and python_version < "3.12" and sys_platform == "win32"
53 | pywebview==4.2.2 ; python_version >= "3.10" and python_version < "3.12"
54 | pywin32==306 ; python_version >= "3.10" and python_version < "3.12"
55 | pyzstd==0.15.9 ; python_version >= "3.10" and python_version < "3.12"
56 | qtpy==2.3.1 ; python_version >= "3.10" and python_version < "3.12" and sys_platform == "openbsd6"
57 | requests-cache==1.1.0 ; python_version >= "3.10" and python_version < "3.12"
58 | requests==2.31.0 ; python_version >= "3.10" and python_version < "3.12"
59 | sentry-sdk==1.29.2 ; python_version >= "3.10" and python_version < "3.12"
60 | setuptools==68.1.0 ; python_version >= "3.10" and python_version < "3.12"
61 | six==1.16.0 ; python_version >= "3.10" and python_version < "3.12"
62 | sniffio==1.3.0 ; python_version >= "3.10" and python_version < "3.12"
63 | soupsieve==2.4.1 ; python_version >= "3.10" and python_version < "3.12"
64 | texttable==1.6.7 ; python_version >= "3.10" and python_version < "3.12"
65 | toml==0.10.2 ; python_version >= "3.10" and python_version < "3.12"
66 | tqdm==4.66.1 ; python_version >= "3.10" and python_version < "3.12"
67 | typing-extensions==4.7.1 ; python_version >= "3.10" and python_version < "3.12"
68 | typing-inspect==0.9.0 ; python_version >= "3.10" and python_version < "3.12"
69 | url-normalize==1.4.3 ; python_version >= "3.10" and python_version < "3.12"
70 | urllib3==2.0.4 ; python_version >= "3.10" and python_version < "3.12"
71 | websocket-client==1.6.1 ; python_version >= "3.10" and python_version < "3.12"
72 | whichcraft==0.6.1 ; python_version >= "3.10" and python_version < "3.12"
73 | win32-setctime==1.1.0 ; python_version >= "3.10" and python_version < "3.12" and sys_platform == "win32"
74 | xmltodict==0.13.0 ; python_version >= "3.10" and python_version < "3.12"
75 | zope-event==5.0 ; python_version >= "3.10" and python_version < "3.12"
76 | zope-interface==6.0 ; python_version >= "3.10" and python_version < "3.12"
77 | 


--------------------------------------------------------------------------------
/send_release_notify.py:
--------------------------------------------------------------------------------
 1 | import requests
 2 | import os
 3 | 
 4 | 
 5 | bot_token = os.environ['TELEGRAM_TOKEN']
 6 | send_to = os.environ['TG_SEND_TO']
 7 | 
 8 | message_template = """New release of  v%s
 9 | 
10 | %s
11 | 
12 | Release page: [%s](%s)
13 | """
14 | 
15 | message_template2 = """New release of  v%s
16 | 
17 | ```
18 | %s
19 | ```
20 | 
21 | Release page: [%s](%s)
22 | """
23 | 
24 | 
25 | def send_message(msg: str):
26 |     data = {'text': msg, 'chat_id': send_to, 'parse_mode': 'markdown'}
27 |     resp = requests.post(f'https://api.telegram.org/bot{bot_token}/sendMessage', json=data)
28 |     data = resp.json()
29 |     # print(data)
30 |     if not data.get('ok'):
31 |         print(data)
32 |         raise RuntimeError(data.get('description'))
33 | 
34 | 
35 | def get_all_release():
36 |     return requests.get('https://api.github.com/repos/triwinds/ns-emu-tools/releases').json()
37 | 
38 | 
39 | def get_latest_release(prerelease=False):
40 |     data = get_all_release()
41 |     release_list = data if prerelease else [i for i in data if i['prerelease'] is False]
42 |     return release_list[0]
43 | 
44 | 
45 | def main():
46 |     release_info = get_latest_release(True)
47 |     print(release_info)
48 |     message = message_template % (
49 |         release_info['tag_name'],
50 |         release_info['body'],
51 |         release_info['tag_name'],
52 |         release_info['html_url']
53 |     )
54 |     try:
55 |         send_message(message)
56 |         return
57 |     except:
58 |         pass
59 |     message = message_template2 % (
60 |         release_info['tag_name'],
61 |         release_info['body'],
62 |         release_info['tag_name'],
63 |         release_info['html_url']
64 |     )
65 |     send_message(message)
66 | 
67 | 
68 | if __name__ == '__main__':
69 |     main()
70 | 


--------------------------------------------------------------------------------
/storage.py:
--------------------------------------------------------------------------------
 1 | from dataclasses import dataclass, field
 2 | import json
 3 | import os
 4 | from pathlib import Path
 5 | from typing import Dict
 6 | 
 7 | from dataclasses_json import dataclass_json, Undefined
 8 | from config import config, YuzuConfig, RyujinxConfig, SuyuConfig
 9 | import logging
10 | 
11 | 
12 | logger = logging.getLogger(__name__)
13 | storage_path = Path('storage.json')
14 | storage = None
15 | 
16 | 
17 | @dataclass_json(undefined=Undefined.EXCLUDE)
18 | @dataclass
19 | class Storage:
20 |     yuzu_history: Dict[str, YuzuConfig] = field(default_factory=dict)
21 |     suyu_history: Dict[str, SuyuConfig] = field(default_factory=dict)
22 |     ryujinx_history: Dict[str, RyujinxConfig] = field(default_factory=dict)
23 |     yuzu_save_backup_path: str = str(Path(r'D:\\yuzu_save_backup'))
24 | 
25 | 
26 | def dump_storage():
27 |     logger.info(f'saving storage to {storage_path.absolute()}')
28 |     with open(storage_path, 'w', encoding='utf-8') as f:
29 |         f.write(storage.to_json(ensure_ascii=False, indent=2))
30 | 
31 | 
32 | if os.path.exists(storage_path):
33 |     with open(storage_path, 'r', encoding='utf-8') as f:
34 |         storage = Storage.from_dict(json.load(f))
35 | if not storage:
36 |     storage = Storage()
37 |     dump_storage()
38 | 
39 | 
40 | def add_yuzu_history(yuzu_config: YuzuConfig, dump=True):
41 |     yuzu_path = Path(yuzu_config.yuzu_path)
42 |     storage.yuzu_history[str(yuzu_path.absolute())] = yuzu_config
43 |     if dump:
44 |         dump_storage()
45 | 
46 | 
47 | def add_ryujinx_history(ryujinx_config: RyujinxConfig, dump=True):
48 |     ryujinx_path = Path(ryujinx_config.path)
49 |     storage.ryujinx_history[str(ryujinx_path.absolute())] = ryujinx_config
50 |     if dump:
51 |         dump_storage()
52 | 
53 | 
54 | def add_suyu_history(suyu_config: SuyuConfig, dump=True):
55 |     suyu_path = Path(suyu_config.path)
56 |     storage.suyu_history[str(suyu_path.absolute())] = suyu_config
57 |     if dump:
58 |         dump_storage()
59 | 
60 | 
61 | def delete_history_path(emu_type: str, path_to_delete: str):
62 |     if emu_type == 'yuzu':
63 |         history = storage.yuzu_history
64 |     elif emu_type == 'suyu':
65 |         history = storage.suyu_history
66 |     else:
67 |         history = storage.ryujinx_history
68 |     abs_path = str(Path(path_to_delete).absolute())
69 |     if abs_path in history:
70 |         del history[abs_path]
71 |         logger.info(f'{emu_type} path {abs_path} deleted.')
72 |         dump_storage()
73 | 
74 | 
75 | if __name__ == '__main__':
76 |     print(storage)
77 | 


--------------------------------------------------------------------------------
/ui.py:
--------------------------------------------------------------------------------
  1 | import logging
  2 | from typing import Optional
  3 | import eel
  4 | from config import config, dump_config, shared
  5 | 
  6 | logger = logging.getLogger(__name__)
  7 | 
  8 | 
  9 | def can_use_chrome():
 10 |     """ Identify if Chrome is available for Eel to use """
 11 |     import os
 12 |     from eel import chrome
 13 |     chrome_instance_path = chrome.find_path()
 14 |     return chrome_instance_path is not None and os.path.exists(chrome_instance_path)
 15 | 
 16 | 
 17 | def can_use_edge():
 18 |     try:
 19 |         import winreg
 20 |         key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, r'Software\Microsoft\Edge\BLBeacon', 0, winreg.KEY_READ)
 21 |         with key:
 22 |             version: str = winreg.QueryValueEx(key, 'version')[0]
 23 |             logger.info(f'Edge version: {version}')
 24 |             return int(version.split('.')[0]) > 70 and _find_edge_win() is not None
 25 |     except:
 26 |         return False
 27 | 
 28 | 
 29 | def _find_edge_win() -> Optional[str]:
 30 |     import winreg as reg
 31 |     import os
 32 |     reg_path = r'SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\msedge.exe'
 33 |     for install_type in reg.HKEY_CURRENT_USER, reg.HKEY_LOCAL_MACHINE:
 34 |         try:
 35 |             reg_key = reg.OpenKey(install_type, reg_path, 0, reg.KEY_READ)
 36 |             edge_path = reg.QueryValue(reg_key, None)
 37 |             reg_key.Close()
 38 |             if not os.path.isfile(edge_path):
 39 |                 continue
 40 |         except WindowsError:
 41 |             edge_path = None
 42 |         else:
 43 |             break
 44 |     return edge_path
 45 | 
 46 | 
 47 | def start_edge_in_app_mode(page, port, size=(1280, 720)):
 48 |     if port == 0:
 49 |         from module.network import get_available_port
 50 |         port = get_available_port()
 51 |     url = f'http://127.0.0.1:{port}/{page}'
 52 |     import subprocess
 53 |     try:
 54 |         subprocess.Popen(f'"{_find_edge_win()}" --app={url}',
 55 |                          stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL, shell=True)
 56 |     except Exception as e:
 57 |         logger.info(f'Fail to start Edge with full path, fallback with "start" command, exception: {str(e)}')
 58 |         subprocess.Popen(f'start msedge --app={url}',
 59 |                          stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL, shell=True)
 60 |     eel.start(url, port=port, mode=False, size=size)
 61 | 
 62 | 
 63 | def import_api_modules():
 64 |     import api
 65 | 
 66 | 
 67 | def main(port=0, mode=None, dev=False):
 68 |     import_api_modules()
 69 |     logger.info('eel init starting...')
 70 |     # eel.init('vue/public') if dev else eel.init("web")
 71 |     eel.init("web")
 72 |     shutdown_delay = 114514 if dev else 1
 73 |     logger.info('eel init finished.')
 74 |     from module.msg_notifier import update_notifier
 75 |     default_page = f''
 76 |     update_notifier('eel-console')
 77 |     if mode is None:
 78 |         if can_use_chrome():
 79 |             mode = 'chrome'
 80 |         elif can_use_edge():
 81 |             mode = 'edge'
 82 |         else:
 83 |             mode = 'user default'
 84 |     size = (config.setting.ui.width, config.setting.ui.height)
 85 |     logger.info(f'browser mode: {mode}, size: {size}')
 86 |     if port == 0:
 87 |         from module.network import get_available_port
 88 |         port = get_available_port()
 89 |         logger.info(f'starting eel at port: {port}')
 90 |     if mode == 'edge':
 91 |         try:
 92 |             shared['mode'] = mode
 93 |             start_edge_in_app_mode(default_page, port, size)
 94 |         except Exception as e:
 95 |             logger.info(f'Fail to start with Edge, fallback to default browser, exception: {str(e)}')
 96 |             mode = 'user default'
 97 |             config.setting.ui.mode = mode
 98 |             dump_config()
 99 |             shared['mode'] = mode
100 |             eel.start(default_page, port=port, size=size, mode=mode, shutdown_delay=shutdown_delay)
101 |     else:
102 |         shared['mode'] = mode
103 |         eel.start(default_page, port=port, size=size, mode=mode, shutdown_delay=shutdown_delay)
104 | 
105 | 
106 | if __name__ == '__main__':
107 |     import gevent.monkey
108 | 
109 |     gevent.monkey.patch_ssl()
110 |     gevent.monkey.patch_socket()
111 |     main(8888, False, True)
112 | 


--------------------------------------------------------------------------------
/ui_webview.py:
--------------------------------------------------------------------------------
 1 | import logging
 2 | import os
 3 | 
 4 | import eel
 5 | import webview
 6 | from utils.webview2 import ensure_runtime_components
 7 | from config import config, shared, dump_config
 8 | from threading import Timer
 9 | 
10 | logger = logging.getLogger(__name__)
11 | default_page = f''
12 | port = 0
13 | 
14 | 
15 | def import_api_modules():
16 |     import api
17 | 
18 | 
19 | def check_webview_status():
20 |     if 'ui_init_time' in shared:
21 |         return
22 |     config.setting.ui.mode = 'browser'
23 |     dump_config()
24 |     from tkinter import messagebox
25 |     messagebox.showerror('未检测到活动的会话', '未能检测到活动的会话,这可能是由于当前系统的 webview2 组件存在问题造成的,'
26 |                                       '已将 ui 切换至浏览器模式,请重新启动程序.')
27 | 
28 | 
29 | def maximize_window():
30 |     if os.name != 'nt':
31 |         return
32 |     from webview.platforms.winforms import WinForms, Func, Type, BrowserView
33 | 
34 |     def _maximize():
35 |         BrowserView.instances['master'].WindowState = WinForms.FormWindowState.Maximized
36 |     BrowserView.instances['master'].Invoke(Func[Type](_maximize))
37 | 
38 | 
39 | def get_window_size():
40 |     return webview.windows[0].width, webview.windows[0].height
41 | 
42 | 
43 | def post_start(fullscreen):
44 |     Timer(10.0, check_webview_status).start()
45 |     if fullscreen:
46 |         Timer(0.5, maximize_window).start()
47 |     shared['mode'] = 'webview'
48 |     eel.start(default_page, port=port, mode=False)
49 | 
50 | 
51 | def close_all_windows():
52 |     if webview.windows:
53 |         logger.info('Closing all windows...')
54 |         for win in webview.windows:
55 |             win.destroy()
56 | 
57 | 
58 | def main():
59 |     if ensure_runtime_components():
60 |         return
61 |     global port
62 |     import_api_modules()
63 |     logger.info('eel init starting...')
64 |     eel.init('vue/public') if port else eel.init("web")
65 |     logger.info('eel init finished.')
66 |     from module.msg_notifier import update_notifier
67 |     update_notifier('eel-console')
68 |     if port == 0:
69 |         from module.network import get_available_port
70 |         port = get_available_port()
71 |     url = f'http://127.0.0.1:{port}/{default_page}'
72 |     logger.info(f'start webview with url: {url}')
73 |     width, height = config.setting.ui.width, config.setting.ui.height
74 |     sw, sh = webview.screens[0].width, webview.screens[0].height
75 |     fullscreen = sw - width < 3 and height / sh > 0.9
76 |     logger.info(f'window size: {(width, height)}, fullscreen: {fullscreen}')
77 |     webview.create_window('NS EMU TOOLS', url, width=width, height=height, text_select=True)
78 |     webview.start(func=post_start, args=[fullscreen])
79 | 
80 | 
81 | if __name__ == '__main__':
82 |     import gevent.monkey
83 | 
84 |     gevent.monkey.patch_ssl()
85 |     gevent.monkey.patch_socket()
86 |     main()
87 | 


--------------------------------------------------------------------------------
/update_game_data.py:
--------------------------------------------------------------------------------
 1 | import os
 2 | 
 3 | import requests
 4 | import requests_cache
 5 | import re
 6 | import json
 7 | 
 8 | 
 9 | # requests_cache.install_cache('ryujinx_issue', expire_after=114514)
10 | game_re = re.compile(r'^(.*?) - ([\da-zA-Z]{16})
#39;)
11 | gh_token = os.environ.get('gh_token')
12 | headers = {
13 |     'Authorization': f'Bearer {gh_token}'
14 | } if gh_token else {}
15 | 
16 | 
17 | def update_with_page(game_data, page):
18 |     resp = requests.get(f'https://api.github.com/repos/Ryujinx/Ryujinx-Games-List/issues?page={page}',
19 |                         headers=headers)
20 |     # print(resp.headers)
21 |     print(f'handle page {page}')
22 |     issues = resp.json()
23 |     print(f'issues size: {len(issues)}')
24 |     if not issues:
25 |         return False
26 |     for issue in issues:
27 |         title = issue['title']
28 |         groups = game_re.findall(title)
29 |         if not groups:
30 |             continue
31 |         title, game_id = groups[0]
32 |         game_data[game_id] = title
33 |     return True
34 | 
35 | 
36 | def update_all():
37 |     page = 0
38 |     game_data = {}
39 |     while True:
40 |         page += 1
41 |         if not update_with_page(game_data, page):
42 |             break
43 |     if game_data:
44 |         with open('game_data.json', 'w', encoding='utf-8') as f:
45 |             json.dump(game_data, f, ensure_ascii=False, indent=2)
46 | 
47 | 
48 | def update_latest():
49 |     import os
50 |     if not os.path.exists('game_data.json'):
51 |         game_data = {}
52 |     else:
53 |         with open('game_data.json', 'r', encoding='utf-8') as f:
54 |             game_data = json.load(f)
55 |     update_with_page(game_data, 1)
56 |     with open('game_data.json', 'w', encoding='utf-8') as f:
57 |         json.dump(game_data, f, ensure_ascii=False, indent=2)
58 | 
59 | 
60 | if __name__ == '__main__':
61 |     update_latest()
62 | 


--------------------------------------------------------------------------------
/update_requirements.bat:
--------------------------------------------------------------------------------
1 | poetry export --without-hashes --format=requirements.txt > requirements.txt
2 | 


--------------------------------------------------------------------------------
/utils/admin.py:
--------------------------------------------------------------------------------
 1 | import logging
 2 | 
 3 | 
 4 | logger = logging.getLogger(__name__)
 5 | 
 6 | 
 7 | def run_with_admin_privilege(executable, argument_line):
 8 |     import ctypes
 9 |     ret = ctypes.windll.shell32.ShellExecuteW(None, u"runas", executable, argument_line, None, 1)
10 |     logger.info(f'run_with_admin_privilege ret code: {ret}')
11 |     return ret
12 | 
13 | 
14 | def check_is_admin():
15 |     import ctypes
16 |     import os
17 |     try:
18 |         return os.getuid() == 0
19 |     except AttributeError:
20 |         return ctypes.windll.shell32.IsUserAnAdmin() != 0
21 | 
22 | 
23 | if __name__ == '__main__':
24 |     run_with_admin_privilege('cmd', r'/c copy D:\py\ns-emu-tools\test.json D:\py\ns-emu-tools\test2.json')
25 | 
26 | 


--------------------------------------------------------------------------------
/utils/common.py:
--------------------------------------------------------------------------------
  1 | import re
  2 | import time
  3 | from pathlib import Path
  4 | from module.msg_notifier import send_notify
  5 | import logging
  6 | 
  7 | 
  8 | logger = logging.getLogger(__name__)
  9 | path_unicode_re = re.compile(r'\\x([\da-f]{4})')
 10 | 
 11 | 
 12 | def callback(hwnd, strings):
 13 |     from win32 import win32gui
 14 |     window_title = win32gui.GetWindowText(hwnd)
 15 |     # left, top, right, bottom = win32gui.GetWindowRect(hwnd)
 16 |     if window_title:
 17 |         strings.append(window_title)
 18 |     return True
 19 | 
 20 | 
 21 | def get_all_window_name():
 22 |     from win32 import win32gui
 23 |     win_list = []  # list of strings containing win handles and window titles
 24 |     win32gui.EnumWindows(callback, win_list)  # populate list
 25 |     return win_list
 26 | 
 27 | 
 28 | def decode_yuzu_path(raw_path_in_config: str):
 29 |     # raw_path_in_config = raw_path_in_config.replace("'", "\'")
 30 |     raw_path_in_config = path_unicode_re.sub(r'\\u\1', raw_path_in_config)
 31 |     # return eval(f"'{raw_path_in_config}'")
 32 |     return raw_path_in_config.encode().decode("unicode-escape")
 33 | 
 34 | 
 35 | def find_all_instances(process_name: str, exe_path: Path = None):
 36 |     import psutil
 37 |     result = []
 38 |     for p in psutil.process_iter():
 39 |         if p.name().startswith(process_name):
 40 |             if exe_path is not None:
 41 |                 process_path = Path(p.exe()).parent.absolute()
 42 |                 if exe_path.absolute() != process_path:
 43 |                     continue
 44 |             result.append(p)
 45 |     return result
 46 | 
 47 | 
 48 | def kill_all_instances(process_name: str, exe_path: Path = None):
 49 |     processes = find_all_instances(process_name, exe_path)
 50 |     if processes:
 51 |         for p in processes:
 52 |             send_notify(f'关闭进程 {p.name()} [{p.pid}]')
 53 |             p.kill()
 54 |         time.sleep(1)
 55 | 
 56 | 
 57 | def is_path_in_use(file_path):
 58 |     # Only works under windows
 59 |     if isinstance(file_path, Path):
 60 |         path = file_path
 61 |     else:
 62 |         path = Path(file_path)
 63 |     if not path.exists():
 64 |         return False
 65 |     try:
 66 |         path.rename(path)
 67 |     except PermissionError:
 68 |         return True
 69 |     else:
 70 |         return False
 71 | 
 72 | 
 73 | def get_installed_software():
 74 |     import winreg
 75 | 
 76 |     def foo(hive, flag):
 77 |         aReg = winreg.ConnectRegistry(None, hive)
 78 |         aKey = winreg.OpenKey(aReg, r"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall",
 79 |                               0, winreg.KEY_READ | flag)
 80 |         count_subkey = winreg.QueryInfoKey(aKey)[0]
 81 |         software_list = []
 82 |         for i in range(count_subkey):
 83 |             software = {}
 84 |             try:
 85 |                 asubkey_name = winreg.EnumKey(aKey, i)
 86 |                 asubkey = winreg.OpenKey(aKey, asubkey_name)
 87 |                 software['name'] = winreg.QueryValueEx(asubkey, "DisplayName")[0]
 88 | 
 89 |                 try:
 90 |                     software['version'] = winreg.QueryValueEx(asubkey, "DisplayVersion")[0]
 91 |                 except EnvironmentError:
 92 |                     software['version'] = 'undefined'
 93 |                 try:
 94 |                     software['publisher'] = winreg.QueryValueEx(asubkey, "Publisher")[0]
 95 |                 except EnvironmentError:
 96 |                     software['publisher'] = 'undefined'
 97 |                 software_list.append(software)
 98 |             except EnvironmentError:
 99 |                 continue
100 | 
101 |         return software_list
102 | 
103 |     try:
104 |         sl = (foo(winreg.HKEY_LOCAL_MACHINE, winreg.KEY_WOW64_32KEY) +
105 |               foo(winreg.HKEY_LOCAL_MACHINE, winreg.KEY_WOW64_64KEY) +
106 |               foo(winreg.HKEY_CURRENT_USER, 0))
107 |         return sl
108 |     except Exception as e:
109 |         logger.info('Exception occurred in get_software_list, exception is: {}'.format(e))
110 |         return []
111 | 
112 | 
113 | def find_installed_software(name_pattern: str):
114 |     import re
115 |     software_list = get_installed_software()
116 |     pattern = re.compile(name_pattern)
117 |     final_list = [s for s in software_list if pattern.search(s['name']) is not None]
118 |     return final_list
119 | 
120 | 
121 | def is_newer_version(min_version, current_version):
122 |     cur_range = current_version.split(".")
123 |     min_range = min_version.split(".")
124 |     for index in range(len(cur_range)):
125 |         if len(min_range) > index:
126 |             try:
127 |                 return int(cur_range[index]) >= int(min_range[index])
128 |             except:
129 |                 return False
130 |     return False
131 | 
132 | 
133 | if __name__ == '__main__':
134 |     from pprint import pp
135 |     # from config import config
136 |     # print(is_path_in_use(config.yuzu.yuzu_path))
137 |     pp(get_installed_software())
138 |     test_l = find_installed_software(r'Microsoft Visual C\+\+ .+ Redistributable')
139 |     print(any(is_newer_version('14.34', s['version']) for s in test_l))
140 | 


--------------------------------------------------------------------------------
/utils/hardware.py:
--------------------------------------------------------------------------------
 1 | import subprocess
 2 | import logging
 3 | 
 4 | 
 5 | logger = logging.getLogger(__name__)
 6 | 
 7 | 
 8 | def get_gpu_info():
 9 |     # https://github.com/SummaLabs/DLS/blob/master/app/backend/env/hardware.py
10 |     gpu_info = []
11 |     try:
12 |         command = "nvidia-smi --query-gpu=index,name,uuid,memory.total,memory.free," \
13 |                   "memory.used,count,utilization.gpu,utilization.memory --format=csv"
14 |         output = execute_command(command)
15 |         lines = output.splitlines()
16 |         lines.pop(0)
17 |         for line in lines:
18 |             tokens = line.split(", ")
19 |             if len(tokens) > 6:
20 |                 gpu_info.append({'id': tokens[0], 'name': tokens[1], 'mem': tokens[3], 'cores': tokens[6],
21 |                                  'mem_free': tokens[4], 'mem_used': tokens[5],
22 |                                  'util_gpu': tokens[7], 'util_mem': tokens[8]})
23 |     except OSError:
24 |         logger.info("GPU device is not available")
25 | 
26 |     return gpu_info
27 | 
28 | 
29 | def execute_command(cmd):
30 |     process = subprocess.Popen(cmd, stdout=subprocess.PIPE)
31 |     return process.communicate()[0].decode()
32 | 
33 | 
34 | def get_cpu_info():
35 |     import platform
36 |     return platform.processor()
37 | 
38 | 
39 | def get_win32_cpu_info():
40 |     # https://github.com/pydata/numexpr/blob/master/numexpr/cpuinfo.py
41 |     import re
42 |     import sys
43 |     from utils.string_util import auto_decode
44 |     pkey = r"HARDWARE\DESCRIPTION\System\CentralProcessor"
45 |     try:
46 |         import _winreg
47 |     except ImportError:  # Python 3
48 |         import winreg as _winreg
49 |     info = []
50 |     try:
51 |         # XXX: Bad style to use so long `try:...except:...`. Fix it!
52 | 
53 |         prgx = re.compile(r"family\s+(?P<FML>\d+)\s+model\s+(?P<MDL>\d+)"
54 |                           r"\s+stepping\s+(?P<STP>\d+)", re.IGNORECASE)
55 |         chnd = _winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE, pkey)
56 |         pnum = 0
57 |         while 1:
58 |             try:
59 |                 proc = _winreg.EnumKey(chnd, pnum)
60 |             except _winreg.error:
61 |                 break
62 |             else:
63 |                 pnum += 1
64 |                 info.append({"Processor": proc})
65 |                 phnd = _winreg.OpenKey(chnd, proc)
66 |                 pidx = 0
67 |                 while True:
68 |                     try:
69 |                         name, value, vtpe = _winreg.EnumValue(phnd, pidx)
70 |                     except _winreg.error:
71 |                         break
72 |                     else:
73 |                         pidx = pidx + 1
74 | 
75 |                         if isinstance(value, bytes):
76 |                             value = value.rstrip(b'\0')
77 |                             value = auto_decode(value)
78 |                         info[-1][name] = str(value).strip()
79 |                         if name == "Identifier":
80 |                             srch = prgx.search(value)
81 |                             if srch:
82 |                                 info[-1]["Family"] = int(srch.group("FML"))
83 |                                 info[-1]["Model"] = int(srch.group("MDL"))
84 |                                 info[-1]["Stepping"] = int(srch.group("STP"))
85 |     except:
86 |         logger.info('Fail to get cpu info.')
87 |     return info
88 | 
89 | 
90 | if __name__ == '__main__':
91 |     # print(get_gpu_info())
92 |     print(get_win32_cpu_info())
93 | 


--------------------------------------------------------------------------------
/utils/package.py:
--------------------------------------------------------------------------------
 1 | from pathlib import Path
 2 | import logging
 3 | 
 4 | import py7zr
 5 | 
 6 | from exception.common_exception import IgnoredException
 7 | from module.msg_notifier import send_notify
 8 | import os
 9 | 
10 | 
11 | logger = logging.getLogger(__name__)
12 | 
13 | 
14 | def uncompress(filepath: Path, target_path, delete_on_error=True,
15 |                exception_msg='当前下载的文件看起来不太正常,请重新下载试试'):
16 |     if isinstance(target_path, str):
17 |         target_path = Path(target_path)
18 |     try:
19 |         if filepath.name.lower().endswith(".zip"):
20 |             import zipfile
21 |             with zipfile.ZipFile(filepath, 'r') as zf:
22 |                 zf.extractall(str(target_path.absolute()))
23 |         elif filepath.name.lower().endswith(".7z"):
24 |             import py7zr
25 |             with py7zr.SevenZipFile(filepath) as zf:
26 |                 zf.extractall(str(target_path.absolute()))
27 |         elif filepath.name.lower().endswith(".tar.xz"):
28 |             import tarfile
29 |             with tarfile.open(filepath, 'r') as tf:
30 |                 tf.extractall(str(target_path.absolute()))
31 |     except Exception as e:
32 |         logger.error(f'Fail to uncompress file: {filepath}', exc_info=True)
33 |         if delete_on_error:
34 |             send_notify(f'文件解压失败,正在删除异常的文件 [{filepath}]')
35 |             os.remove(filepath)
36 |         raise IgnoredException(exception_msg)
37 | 
38 | 
39 | def compress_folder(folder_path: Path, save_path):
40 |     import py7zr
41 |     if isinstance(save_path, str):
42 |         save_path = Path(save_path)
43 |     directory = str(folder_path.absolute())
44 |     rootdir = os.path.basename(directory)
45 |     try:
46 |         logger.info(f'compress {folder_path} to {save_path}')
47 |         zf: py7zr.SevenZipFile
48 |         with py7zr.SevenZipFile(save_path, 'w') as zf:
49 |             for dirpath, dirnames, filenames in os.walk(directory):
50 |                 for filename in filenames:
51 |                     # Write the file named filename to the archive,
52 |                     # giving it the archive name 'arcname'.
53 |                     filepath = os.path.join(dirpath, filename)
54 |                     parentpath = os.path.relpath(filepath, directory)
55 |                     arcname = os.path.join(rootdir, parentpath)
56 |                     zf.write(filepath, arcname)
57 |     except Exception as e:
58 |         logger.error(f'Fail to compress file {folder_path} to {save_path}', exc_info=True)
59 |         raise IgnoredException(f'备份失败, {str(e)}')
60 | 
61 | 
62 | def is_7zfile(filepath: Path):
63 |     return py7zr.is_7zfile(filepath)
64 | 


--------------------------------------------------------------------------------
/utils/string_util.py:
--------------------------------------------------------------------------------
1 | import chardet
2 | 
3 | 
4 | def auto_decode(input_bytes: bytes):
5 |     det_res = chardet.detect(input_bytes)
6 |     if det_res['encoding']:
7 |         return input_bytes.decode(det_res['encoding'])
8 |     return input_bytes.decode()
9 | 


--------------------------------------------------------------------------------
/utils/webview2.py:
--------------------------------------------------------------------------------
  1 | # modify from https://github.com/r0x0r/pywebview/blob/master/webview/platforms/winforms.py
  2 | 
  3 | import winreg
  4 | import logging
  5 | import os
  6 | 
  7 | logger = logging.getLogger(__name__)
  8 | 
  9 | 
 10 | def get_dot_net_version():
 11 |     net_key, version = None, None
 12 |     try:
 13 |         net_key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r'SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full')
 14 |         version, _ = winreg.QueryValueEx(net_key, 'Release')
 15 |     finally:
 16 |         if net_key:
 17 |             winreg.CloseKey(net_key)
 18 |     return version
 19 | 
 20 | 
 21 | def is_chromium(verbose=False):
 22 |     from utils.common import is_newer_version
 23 | 
 24 |     def edge_build(key_type, key, description=''):
 25 |         try:
 26 |             windows_key = None
 27 |             if key_type == 'HKEY_CURRENT_USER':
 28 |                 path = rf'Microsoft\EdgeUpdate\Clients\{key}'
 29 |             else:
 30 |                 path = rf'WOW6432Node\Microsoft\EdgeUpdate\Clients\{key}'
 31 |             with winreg.OpenKey(getattr(winreg, key_type), rf'SOFTWARE\{path}') as windows_key:
 32 |                 build, _ = winreg.QueryValueEx(windows_key, 'pv')
 33 |                 return str(build)
 34 |         except Exception as e:
 35 |             pass
 36 |         return '0'
 37 | 
 38 |     try:
 39 |         build_versions = [
 40 |             # runtime
 41 |             {'key': '{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}', 'description': 'Microsoft Edge WebView2 Runtime'},
 42 |             # beta
 43 |             {'key': '{2CD8A007-E189-409D-A2C8-9AF4EF3C72AA}', 'description': 'Microsoft Edge WebView2 Beta'},
 44 |             # dev
 45 |             {'key': '{0D50BFEC-CD6A-4F9A-964C-C7416E3ACB10}', 'description': 'Microsoft Edge WebView2 Developer'},
 46 |             # canary
 47 |             {'key': '{65C35B14-6C1D-4122-AC46-7148CC9D6497}', 'description': 'Microsoft Edge WebView2 Canary'},
 48 |         ]
 49 | 
 50 |         for item in build_versions:
 51 |             for key_type in ('HKEY_CURRENT_USER', 'HKEY_LOCAL_MACHINE'):
 52 |                 build = edge_build(key_type, item['key'], item['description'])
 53 |                 if is_newer_version('105.0.0.0', build):
 54 |                     if verbose:
 55 |                         logger.info(f'webview2 version: {build}, description: {item["description"]}')
 56 |                     return True
 57 | 
 58 |     except Exception as e:
 59 |         logger.exception(e)
 60 |     return False
 61 | 
 62 | 
 63 | def ensure_runtime_components():
 64 |     flag = False
 65 |     version = get_dot_net_version()
 66 |     logger.info(f'dot net version: {version}')
 67 |     if version < 394802:  # .NET 4.6.2
 68 |         install_dot_net()
 69 |         flag = True
 70 |     if not is_chromium(verbose=True):
 71 |         install_webview2()
 72 |         flag = True
 73 |     if flag:
 74 |         show_msgbox('重启程序', '组件安装完成后, 请重新启动程序.', 0)
 75 |     return flag
 76 | 
 77 | 
 78 | def can_use_webview():
 79 |     version = get_dot_net_version()
 80 |     return version >= 394802 and is_chromium()
 81 | 
 82 | 
 83 | def show_msgbox(title, content, style):
 84 |     import ctypes  # An included library with Python install.
 85 |     #  Styles:
 86 |     #  0 : OK
 87 |     #  1 : OK | Cancel
 88 |     #  2 : Abort | Retry | Ignore
 89 |     #  3 : Yes | No | Cancel
 90 |     #  4 : Yes | No
 91 |     #  5 : Retry | Cancel
 92 |     #  6 : Cancel | Try Again | Continue
 93 |     return ctypes.windll.user32.MessageBoxW(0, content, title, style)
 94 | 
 95 | 
 96 | def get_download_file_name(resp):
 97 |     if 'Content-Disposition' in resp.headers:
 98 |         import cgi
 99 |         value, params = cgi.parse_header(resp.headers['Content-Disposition'])
100 |         return params['filename']
101 |     if resp.url.find('/'):
102 |         return resp.url.rsplit('/', 1)[1]
103 |     return 'index'
104 | 
105 | 
106 | def download_file(url):
107 |     import requests
108 |     resp = requests.get(url)
109 |     local_filename = get_download_file_name(resp)
110 |     with open(local_filename, 'wb') as f:
111 |         f.write(resp.content)
112 |     logger.info(f'[{local_filename}] download success.')
113 |     return local_filename
114 | 
115 | 
116 | def install_dot_net():
117 |     ret = show_msgbox("运行组件缺失", "缺失 .NET Framework 组件, 是否下载安装?", 4)
118 |     if ret == 7:
119 |         raise RuntimeError('缺失 .NET Framework 组件')
120 |     fn = download_file('https://go.microsoft.com/fwlink/?LinkId=2203304')
121 |     logger.info('installing .NET Framework ...')
122 |     os.system(fn)
123 |     logger.info('removing .NET Framework installer.')
124 |     os.remove(fn)
125 | 
126 | 
127 | def install_webview2():
128 |     ret = show_msgbox("运行组件缺失", "缺失 Microsoft Edge WebView2 组件, 是否下载安装?", 4)
129 |     if ret == 7:
130 |         raise RuntimeError('缺失 Microsoft Edge WebView2 组件')
131 |     fn = download_file('https://go.microsoft.com/fwlink/p/?LinkId=2124703')
132 |     logger.info('installing webview2...')
133 |     os.system(fn)
134 |     logger.info('removing webview2 installer.')
135 |     os.remove(fn)
136 | 
137 | 
138 | if __name__ == '__main__':
139 |     import config
140 | 
141 |     # check_runtime_components()
142 |     install_webview2()
143 | 


--------------------------------------------------------------------------------