├── .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 ├── 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 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 | 9 | 10 | logger = logging.getLogger(__name__) 11 | 12 | 13 | def success_response(data=None, msg=None): 14 | return {'code': 0, 'data': data, 'msg': msg} 15 | 16 | 17 | def exception_response(ex): 18 | import traceback 19 | if type(ex) in exception_handler_map: 20 | return exception_handler_map[type(ex)](ex) 21 | logger.error(ex, exc_info=True) 22 | traceback_str = "".join(traceback.format_exception(ex)) 23 | send_notify(f'出现异常, {traceback_str}') 24 | return error_response(999, str(ex)) 25 | 26 | 27 | def version_not_found_handler(ex: VersionNotFoundException): 28 | logger.info(f'{str(ex)}') 29 | send_notify(f'无法获取 {ex.branch} 分支的 [{ex.target_version}] 版本信息') 30 | return error_response(404, str(ex)) 31 | 32 | 33 | def md5_not_found_handler(ex: Md5NotMatchException): 34 | logger.info(f'{str(ex)}') 35 | send_notify(f'固件文件 md5 不匹配, 请重新下载') 36 | return error_response(501, str(ex)) 37 | 38 | 39 | def download_interrupted_handler(ex: DownloadInterrupted): 40 | logger.info(f'{str(ex)}') 41 | send_notify(f'下载任务被终止') 42 | return error_response(601, str(ex)) 43 | 44 | 45 | def download_paused_handler(ex: DownloadPaused): 46 | logger.info(f'{str(ex)}') 47 | send_notify(f'下载任务被暂停') 48 | return error_response(602, str(ex)) 49 | 50 | 51 | def download_not_completed_handler(ex: DownloadNotCompleted): 52 | logger.info(f'{str(ex)}') 53 | send_notify(f'下载任务 [{ex.name}] 未完成, 状态: {ex.status}') 54 | return error_response(603, str(ex)) 55 | 56 | 57 | def fail_to_copy_files_handler(ex: FailToCopyFiles): 58 | logger.exception(ex.raw_exception) 59 | send_notify(f'{ex.msg}, 这可能是由于相关文件被占用或者没有相关目录的写入权限造成的') 60 | send_notify(f'请检查相关程序是否已经关闭, 或者重启一下系统试试') 61 | return error_response(701, str(ex)) 62 | 63 | 64 | def ignored_exception_handler(ex): 65 | logger.info(f'{str(ex)}') 66 | return error_response(801, str(ex)) 67 | 68 | 69 | def connection_error_handler(ex): 70 | import traceback 71 | traceback_str = "".join([s for s in traceback.format_exception(ex) if s.strip() != '']) 72 | logger.info(f'{str(ex)}\n{traceback_str}') 73 | send_notify(f'出现异常, {traceback_str}') 74 | return error_response(999, str(ex)) 75 | 76 | 77 | exception_handler_map = { 78 | VersionNotFoundException: version_not_found_handler, 79 | Md5NotMatchException: md5_not_found_handler, 80 | DownloadInterrupted: download_interrupted_handler, 81 | DownloadPaused: download_paused_handler, 82 | DownloadNotCompleted: download_not_completed_handler, 83 | FailToCopyFiles: fail_to_copy_files_handler, 84 | IgnoredException: ignored_exception_handler, 85 | ConnectionError: connection_error_handler, 86 | } 87 | 88 | 89 | def generic_api(func): 90 | def wrapper(*args, **kw): 91 | try: 92 | return success_response(func(*args, **kw)) 93 | except Exception as e: 94 | return exception_response(e) 95 | eel._expose(func.__name__, wrapper) 96 | return wrapper 97 | 98 | 99 | def error_response(code, msg): 100 | return {'code': code, 'msg': msg} 101 | 102 | 103 | __all__ = ['success_response', 'exception_response', 'error_response', 'generic_api'] 104 | -------------------------------------------------------------------------------- /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 | 113 | 114 | John F. Reiser 115 | 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 29 | for severals ideas for the Linux version 30 | Andi Kleen and Jamie Lokier 31 | for the /proc/self/fd/X and other Linux suggestions 32 | Andreas Muegge 33 | for the Win32 GUI 34 | Atli Mar Gudmundsson 35 | for several comments on the win32/pe stub 36 | Charles W. Sandmann 37 | for the idea with the stubless decompressor in djgpp2/coff 38 | Ice 39 | for debugging the PE headersize problem down 40 | Joergen Ibsen and d'b 41 | for the relocation & address optimization ideas 42 | John S. Fine 43 | for the new version of the dos/exe decompressor 44 | Kornel Pal 45 | for the EFI support 46 | Lukundoo 47 | for beta testing 48 | Michael Devore 49 | for initial dos/exe device driver support 50 | Oleg V. Volkov 51 | for various FreeBSD specific information 52 | The Owl & G-RoM 53 | for the --compress-icons fix 54 | Ralph Roth 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/58aa58f29e48ed5745a3db956f99dc5bef621884/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 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## 0.5.4 4 | - 更新 Ryujinx Canary 版的 repo 地址 (#121) 5 | - 修复更新脚本的问题 6 | 7 | ## 0.5.3 8 | - 更新 Ryujinx 正式版的新 repo 地址 9 | - 恢复 Ryujinx 变更日志展示功能 10 | - 移除 Cloudflare 节点选优功能 11 | 12 | ## 0.5.2 13 | - 添加对 Ryubing/Ryujinx 的支持 (#117) 14 | - 更新 GitHub 下载源 15 | 16 | ## 0.5.1 17 | 18 | - Ryujinx 项目已被关闭 (#113) 19 | - 禁用 Ryujinx 版本更新检查 20 | 21 | ## 0.5.0 22 | 23 | - 禁用 yuzu 版本更新检查 (#108) 24 | 25 | ## 0.4.9 26 | 27 | - 适配 suyu 的新包名 (#107) 28 | - 更新 GitHub 下载源 29 | 30 | ## 0.4.8 31 | 32 | - 修复无法获取固件列表的问题 (#105) 33 | - 修复固件路径存在非英文字符时无法检测固件版本的问题 (#105) 34 | - 修复无法获取程序列表时会报错的问题 (#104) 35 | - 添加 suyu 管理页面 (#103) 36 | 37 | ## 0.4.7 38 | 39 | - yuzu is dead, RIP (#97) 40 | - 修复 Ryujinx 分支的展示问题 (#96) 41 | 42 | ## 0.4.6 43 | 44 | - 调整固件名称展示逻辑 45 | - 修复手动设置的代理无法请求 api 时使用的问题 46 | - 当已安装的 msvc 版本较低时提示更新 (#92) 47 | - 添加自动将 yuzu.exe 重命名为 cemu.exe 的选项 (#89) 48 | - 替换已经失效的 GitHub Mirror 49 | 50 | ## 0.4.5 51 | 52 | - 修复某些情况下更改 UI 启动模式后无法启动的问题 (#86) 53 | - 更新 aria2 至 1.37.0 54 | - 替换已经失效的 GitHub Mirror 55 | 56 | ## 0.4.4 57 | 58 | - 修复了一些 UI 问题 59 | - 新增 [THZoria/NX_Firmware](https://github.com/THZoria/NX_Firmware) 固件源 (#83) 60 | 61 | ## 0.4.3 62 | 63 | - 替换已经失效的 GitHub Mirror 64 | - 不再使用 archive.org 作为固件源 65 | - 更新 Python 至 3.11 66 | - 更新 Vue 至 Vue3 67 | 68 | ## 0.4.2 69 | 70 | - 安装模拟器时不再提前检查目录是否被占用 (#69)(#70) 71 | - 回退 PyInstaller 至 5.10.1 版本 72 | - 延长固件信息接口的超时时间 73 | - 移除失效的 GitHub 镜像 74 | 75 | ## 0.4.1 76 | 77 | - 添加固件安装前的提示 78 | - 安装模拟器时不再自动关闭启动中的模拟器 (目录被其它程序占用时中止安装) 79 | - 新增 yuzu 存档备份功能 (#57) 80 | - 修复当系统缩放不为 100% 时,保存的窗口大小不正确的问题 (#58) 81 | - 修复当系统设置为 IPv6 优先时无法打开页面的问题 & 文档更新. by @RivenNero 82 | 83 | ## 0.4.0 84 | 85 | - 当 webview 模式长时间未能启动后自动切换至浏览器模式 (#47)(#48) 86 | - 记录窗口大小 (#50) 87 | - 修复检测不到 yuzu/ryujinx 窗口的问题 (#51) Thanks @RonaldinhoL 88 | - 允许手动设置 HTTP 代理服务器 (#52) 89 | - pywebview 回退至 4.0.2 90 | 91 | ## 0.3.9 92 | 93 | - 自动更新时确保之前的进程已经被关闭 (#41) 94 | - 安装固件时展示具体安装的路径 (#42) 95 | - 金手指管理中添加全选/反选的按钮 (#42) 96 | - 路径选择中添加删除按钮 (#43) 97 | - aria2 启动失败时尝试重新启动 (#44) 98 | - 当 Yuzu/Ryujinx 的安装包无法正常解压时自动删除下载的文件,并提示重新下载 (#45) 99 | - 移除/更新已经失效的链接 100 | 101 | ## 0.3.8 102 | 103 | - 添加展示 yuzu 最近提交记录的按钮 104 | - Edge 浏览器模式启动失败时自动回退至默认浏览器 105 | - 重构 GitHub 下载源相关代码 & 移除失效的下载源 106 | 107 | ## 0.3.7 108 | 109 | - 修复 Ryujinx 固件安装失败时会删除原有固件的问题 (#36) 110 | - 一些样式调整 111 | 112 | ## 0.3.6 113 | 114 | - 对于一些不经常发生变更的数据使用持久化缓存 115 | - 更新 ffhome 的链接 116 | - 一些样式与描述调整 117 | - 在列表中显示金手指文件里面以 {} 包裹的内容 118 | 119 | ## 0.3.5 120 | 121 | - 打开金手指管理界面时异步加载游戏信息 122 | - DoH 查询时复用已建立的连接 123 | - 一些 bug 修复及错误信息调整 124 | 125 | ## 0.3.4 126 | 127 | - 调整 Edge 浏览器的检测逻辑 128 | - 调整 requests cache 的缓存后端 129 | - 重构金手指文件的解析逻辑 130 | - 修复非 UTF-8 编码的金手指文件打开报错的问题 131 | 132 | ## 0.3.3 133 | 134 | - 修复当文件已存在时下载报错的问题 135 | - 修复 Ryujinx 在正式版/AVA分支切换后由于版本相同导致无法开始下载的问题 136 | 137 | ps. 由于现在的 Ryujinx 正式版还不支持中文,因此从 AVA / LDN 分支切换过去后,可能会出现因为配置文件冲突导致模拟器打不开的问题。如果您需要使用不同分支的模拟器,建议新建文件夹分开存放。 138 | 139 | ## 0.3.2 140 | 141 | - 在安装过程中阻止关闭控制台对话框, 以免出现奇怪的问题 142 | - 调整下载逻辑, 允许在下载过程中暂停或中断下载任务 143 | - 更新 requests-cache/pyinstaller 等一些依赖的版本 144 | 145 | ## 0.3.1 146 | 147 | - 调整浏览器的兼容范围, 修复某些老版本浏览器白屏的问题 148 | - 最低需要支持 es6 的浏览器, 一般在 2016 年之后发布的浏览器都支持 149 | - 修复固件 md5 校验失败时没有自动删除相应 zip 包的问题 150 | - 修复非简体中文的系统环境中自动更新失败的问题 151 | - 调整 Ryujinx 的安装逻辑: 安装时只移除 Ryujinx*.exe 文件, 其余文件使用覆盖模式 152 | - 更新 CloudflareSpeedTest 版本至 v2.2.2 , 并修复了一些问题 153 | 154 | ## 0.3.0 155 | 156 | - 在 api 请求发生超时错误时进行重试 157 | - 当 IPv6 启用时 DoH 尝试查询 AAAA 记录 158 | - 安装固件时对下载文件的 md5 进行校验 159 | - 修复某些情况下 aria2 进程没有正常关闭的问题 160 | - 更正检测固件版本时固件文件解密失败的错误文本 161 | - 修复某些代理软件错误配置 localhost 代理导致无法调用 aria2 api 的问题 162 | 163 | ## 0.2.9 164 | 165 | - 添加新 GitHub 下载源 nuaa.cf, 并更新在其它 GitHub 下载源中使用的 UA 166 | - 更正尝试下载一个不存在的 Ryujinx 版本时所展示的文本 167 | - 集成 sentry sdk 收集异常信息 (可通过 `--no-sentry` 启动参数将其禁用) 168 | - 使 DNS 缓存遵循返回的 ttl 设定 169 | - 当 yuzu/ryujinx/固件 版本检测失败时, 将记录中的版本号重置为 `未知` 170 | 171 | ps. 目前 Ryujinx LDN 只能下载 3.0.1 及之后的版本。如果需要更久之前的版本,请前往 Ryujinx 官网下载。 172 | 173 | ## 0.2.8 174 | 175 | - 调整 ui 启动逻辑 176 | - 启动后自动创建 `切换 UI 启动模式.bat` 用于切换启动模式 177 | - 添加启动参数 `--switch-mode` 用于切换启动模式 178 | 179 | ## 0.2.7 180 | 181 | - 优化 CloudflareST 授权流程,仅在写入 hosts 时请求管理员权限 182 | - 修复在 windowed 打包方式下 CloudflareST 控制台显示不正常的问题 183 | - 访问 api 时默认启用 DNS over HTTPS (可在设置中关闭) 184 | - 指定 aria2 使用 Aliyun / DNSPod 的 DNS 服务器 185 | - 修复因路径大小写原因误删 Ryujinx 的 portable 文件夹的问题 (#23) 186 | - 合并 webview 进入 main.py 187 | 188 | ## 0.2.6 189 | 190 | - 新增试验性功能: Cloudflare 节点选优 191 | - 修复 yuzu mod 文件夹路径获取错误的问题 (#19) 192 | - 新增模拟器路径的历史记录 (#20) 193 | 194 | ## 0.2.5 195 | 196 | - webview 版本增加运行前环境检测,并自动下载缺失的组件 197 | - 替换不安全的 Unicode decode 方式 198 | - 新增配置项: 在启动 aria2 前自动删除旧的日志 199 | - 更新 UA 标识 200 | - 添加 `其它资源` 页面 201 | 202 | ps. 现在的 webview 版本应该可以在没安装过 Microsoft Edge WebView2 的系统中运行了. 203 | 如果您之前遇到过 webview 版本打不开的问题, 可以试试这个版本, 还有问题的话可以在 issue 中反馈. 204 | 205 | ## 0.2.4 206 | 207 | - 新增对 Ryujinx LDN 版本的支持 (#5) 208 | - 当 eel websocket 断开后在界面提示重启程序 (#16) 209 | - nodejs 版本更新至 18, 更新前端相关依赖的版本 210 | 211 | ## 0.2.3 212 | 213 | - 新增自动更新功能 (建议使用 webview 版本) 214 | - 当直连 GitHub api 出现问题时尝试使用 CDN 进行重试 215 | - `设置` 页面中新增开关 aria2 ipv6 的选项 216 | - `About` 页面中新增查看 更新日志 的按钮 217 | - `Ryujinx` 页面中新增查看 更新日志 的按钮 218 | - 更新缓存配置, 根据 HTTP 响应中的 Cache Control 进行缓存 219 | 220 | ### 关于 webview 版本 221 | 222 | 由于 js/css 语法的兼容性问题, 一些浏览器上可能无法正确展示页面, 所以这里提供一个使用 webview 打包的版本。 223 | 224 | 这个版本不依赖于用户环境中的浏览器, 而是使用 [Microsoft Edge WebView2](https://developer.microsoft.com/en-us/microsoft-edge/webview2/) 225 | 打开界面, 这个组件已经预置在较新版本的系统当中(通过 Windows Update 推送), 因此这些系统无需进行额外下载。如果你的系统中没有这个组件, 226 | 可以从 [这里](https://developer.microsoft.com/zh-cn/microsoft-edge/webview2/#download-section) 下载。 227 | 228 | 此外, 由于浏览器的安全限制, 程序无法主动关闭打开的浏览器页面, 因此只有 webview 版本能在更新时自动关闭打开的窗口, 229 | 其余版本则需要手动关闭之前打开的页面。 230 | 231 | ## 0.2.2-fix 232 | 233 | - 修复未能正确转义 Yuzu 配置中的 Unicode 字符的问题 (#11) 234 | 235 | ## 0.2.2 236 | 237 | - 修复无法识别 Yuzu 自定义 nand/load 目录的问题 (#9) 238 | - 保存选择的主题 (#10) 239 | - 修复金手指文件使用大写后缀名时无法识别的问题 240 | 241 | ## 0.2.1 242 | 243 | - 更新 Edge 的检测机制,在无法检测到 Edge 时将尝试使用默认浏览器启动 244 | - 添加命令行启动参数,支持选择启动的浏览器 (chrome, edge, user default) 245 | - 例如强制使用默认浏览器启动 `NsEmuTools.exe -m "user default"` 246 | - 添加 常见问题 页面 247 | - 设置中添加更多的 GitHub 下载源选项 248 | - 更换游戏数据源 249 | - 修复 Yuzu 路径有特殊字符时无法检测版本的问题 250 | - 设置中添加选项,允许保留下载的文件 (#4) 251 | 252 | ## 0.2.0 253 | 254 | - 新增 Yuzu 金手指管理功能 255 | - 调整 aria2p 连接参数以修复某些情况下 aria2 接口调用失败的问题 256 | - 修复含有特殊字符路径时命令行无法执行的问题 257 | - 在修改模拟器目录时展示警告信息 258 | 259 | ## 0.1.9 260 | 261 | - aria2 禁用 ipv6 262 | - 新增网络设置相关选项 263 | - 添加 requests-cache 用于本地缓存 api 结果 264 | 265 | ## 0.1.8 266 | 267 | - 修复 windowed 打包方式无法正常启动 Edge 浏览器的问题 268 | 269 | ## 0.1.7 270 | 271 | - 基于 Vuetify 构建的新 UI 272 | - 添加 msvc 的代理源 273 | - 修复 Ryujinx 切换分支后由于版本相同导致无法开始下载的问题 274 | - 调整浏览器默认使用顺序: Chrome > Edge > User Default 275 | -------------------------------------------------------------------------------- /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.4' 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/58aa58f29e48ed5745a3db956f99dc5bef621884/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 | /// 2 | /// 3 | /// 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 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 18 | NS EMU TOOLS 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 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/58aa58f29e48ed5745a3db956f99dc5bef621884/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/src/App.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 46 | 47 | 79 | -------------------------------------------------------------------------------- /frontend/src/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/triwinds/ns-emu-tools/58aa58f29e48ed5745a3db956f99dc5bef621884/frontend/src/assets/icon.png -------------------------------------------------------------------------------- /frontend/src/assets/ryujinx.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/triwinds/ns-emu-tools/58aa58f29e48ed5745a3db956f99dc5bef621884/frontend/src/assets/ryujinx.webp -------------------------------------------------------------------------------- /frontend/src/assets/suyu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/triwinds/ns-emu-tools/58aa58f29e48ed5745a3db956f99dc5bef621884/frontend/src/assets/suyu.png -------------------------------------------------------------------------------- /frontend/src/assets/telegram.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/triwinds/ns-emu-tools/58aa58f29e48ed5745a3db956f99dc5bef621884/frontend/src/assets/telegram.webp -------------------------------------------------------------------------------- /frontend/src/assets/telegram_black.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/triwinds/ns-emu-tools/58aa58f29e48ed5745a3db956f99dc5bef621884/frontend/src/assets/telegram_black.webp -------------------------------------------------------------------------------- /frontend/src/assets/yuzu.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/triwinds/ns-emu-tools/58aa58f29e48ed5745a3db956f99dc5bef621884/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 | 36 | 37 | 43 | 44 | 49 | -------------------------------------------------------------------------------- /frontend/src/components/ConsoleDialog.vue: -------------------------------------------------------------------------------- 1 | 51 | 52 | 91 | 92 | 127 | -------------------------------------------------------------------------------- /frontend/src/components/DialogTitle.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | 11 | 14 | -------------------------------------------------------------------------------- /frontend/src/components/FaqGroup.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 20 | 21 | 24 | -------------------------------------------------------------------------------- /frontend/src/components/MarkdownContentBox.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 41 | 42 | 47 | -------------------------------------------------------------------------------- /frontend/src/components/NewVersionDialog.vue: -------------------------------------------------------------------------------- 1 | 68 | 69 | 132 | 133 | 136 | -------------------------------------------------------------------------------- /frontend/src/components/OtherLinkItem.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 26 | 27 | 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 | 17 | 18 | 21 | ``` 22 | 23 | When your template is rendered, the component's import will automatically be inlined, which renders to this: 24 | 25 | ```vue 26 | 31 | 32 | 35 | ``` 36 | -------------------------------------------------------------------------------- /frontend/src/components/SimplePage.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 19 | 20 | 23 | -------------------------------------------------------------------------------- /frontend/src/components/SpeedDial.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 38 | 39 | 50 | -------------------------------------------------------------------------------- /frontend/src/components/YuzuSaveCommonPart.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 54 | 55 | 58 | -------------------------------------------------------------------------------- /frontend/src/components/YuzuSaveRestoreTab.vue: -------------------------------------------------------------------------------- 1 | 61 | 62 | 144 | 145 | 148 | -------------------------------------------------------------------------------- /frontend/src/layouts/AppBar.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 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 | 8 | 9 | 12 | -------------------------------------------------------------------------------- /frontend/src/layouts/default.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 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 | 76 | 77 | 113 | 114 | 141 | -------------------------------------------------------------------------------- /frontend/src/pages/faq.vue: -------------------------------------------------------------------------------- 1 | 132 | 133 | 150 | 151 | 158 | -------------------------------------------------------------------------------- /frontend/src/pages/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 7 | -------------------------------------------------------------------------------- /frontend/src/pages/keys.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 64 | 65 | 75 | -------------------------------------------------------------------------------- /frontend/src/pages/otherLinks.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 75 | 76 | 79 | -------------------------------------------------------------------------------- /frontend/src/pages/yuzuSaveManagement.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 131 | 132 | 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, Record>, 22 | '/about': RouteRecordInfo<'/about', '/about', Record, Record>, 23 | '/faq': RouteRecordInfo<'/faq', '/faq', Record, Record>, 24 | '/keys': RouteRecordInfo<'/keys', '/keys', Record, Record>, 25 | '/otherLinks': RouteRecordInfo<'/otherLinks', '/otherLinks', Record, Record>, 26 | '/ryujinx': RouteRecordInfo<'/ryujinx', '/ryujinx', Record, Record>, 27 | '/settings': RouteRecordInfo<'/settings', '/settings', Record, Record>, 28 | '/suyu': RouteRecordInfo<'/suyu', '/suyu', Record, Record>, 29 | '/yuzu': RouteRecordInfo<'/yuzu', '/yuzu', Record, Record>, 30 | '/yuzuCheatsManagement': RouteRecordInfo<'/yuzuCheatsManagement', '/yuzuCheatsManagement', Record, Record>, 31 | '/yuzuSaveManagement': RouteRecordInfo<'/yuzuSaveManagement', '/yuzuSaveManagement', Record, Record>, 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/58aa58f29e48ed5745a3db956f99dc5bef621884/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/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 "" ( 17 | echo 备份原文件至 ".bak" 18 | move /Y "" ".bak" 19 | ) 20 | if exist "_internal" ( 21 | move /Y "_internal" "_internal_bak" 22 | timeout /t 1 /nobreak 23 | ) 24 | if not exist "" ( 25 | echo 无法找到更新文件 "" 26 | pause 27 | ) else ( 28 | echo 复制文件中 29 | robocopy "" . /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" "" 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('', str(Path(sys.argv[0]).absolute()))\ 114 | .replace('', str(upgrade_files_folder))\ 115 | .replace('', 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 | ] 29 | 30 | [dependency-groups] 31 | dev = ["pyinstaller>=6.0.0,<7"] 32 | 33 | [tool.uv] 34 | 35 | [tool.uv.sources] 36 | nsz = { git = "https://github.com/triwinds/nsz" } 37 | 38 | [tool.hatch.build.targets.sdist] 39 | include = ["module"] 40 | 41 | [tool.hatch.build.targets.wheel] 42 | include = ["module"] 43 | 44 | [build-system] 45 | requires = ["hatchling"] 46 | build-backend = "hatchling.build" 47 | -------------------------------------------------------------------------------- /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 |
31 | NsEmuTools.exe 和 NsEmuTools-console.exe 有什么区别? 32 | NsEmuTools.exe 和 NsEmuTools-console.exe 在实际的功能上并没有任何差异, 33 | 其主要的差别在于 console 会在启动的时候多一个命令行窗口,这也许可以解决某些杀毒软件的误报问题, 34 | 详情见 #2. 35 |
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/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 | 3 | 4 | # They move them codes to https://git.ryujinx.app/ryubing/ryujinx 5 | # the api of releases https://git.ryujinx.app/api/v4/projects/1/releases (using GitLab) 6 | 7 | 8 | def get_all_ryujinx_release_infos(branch='mainline'): 9 | if branch == 'canary': 10 | return get_all_canary_ryujinx_release_infos() 11 | return request_github_api('https://api.github.com/repos/Ryubing/Stable-Releases/releases') 12 | 13 | 14 | def get_all_canary_ryujinx_release_infos(): 15 | return request_github_api('https://api.github.com/repos/iurehg8uetgyh8ui5e/cr/releases') 16 | 17 | 18 | def get_latest_ryujinx_release_info(): 19 | return get_all_ryujinx_release_infos()[0] 20 | 21 | 22 | def get_ryujinx_release_info_by_version(version, branch='mainline'): 23 | if branch == 'canary': 24 | return get_canary_ryujinx_release_info_by_version(version) 25 | return request_github_api(f'https://api.github.com/repos/Ryubing/Stable-Releases/releases/tags/{version}') 26 | 27 | 28 | def get_canary_ryujinx_release_info_by_version(version): 29 | return request_github_api(f'https://api.github.com/repos/iurehg8uetgyh8ui5e/cr/releases/tags/{version}') 30 | 31 | 32 | def load_ryujinx_change_log(branch: str): 33 | if branch == 'canary': 34 | resp = request_github_api('https://api.github.com/repos/iurehg8uetgyh8ui5e/cr/releases') 35 | else: 36 | resp = request_github_api('https://api.github.com/repos/Ryubing/Stable-Releases/releases') 37 | return resp[0].get('body') 38 | -------------------------------------------------------------------------------- /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})$') 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\d+)\s+model\s+(?P\d+)" 54 | r"\s+stepping\s+(?P\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 | --------------------------------------------------------------------------------