├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── nightly-build.yml │ └── release-build.yml ├── LICENSE ├── README.md ├── README_zh.md ├── app ├── config.py ├── globals.py ├── resource │ ├── dark │ │ ├── demo.qss │ │ ├── link_card.qss │ │ ├── logo.png │ │ └── setting_interface.qss │ ├── i18n │ │ ├── DDNetToolBox.main.en_US.ts │ │ ├── DDNetToolBox.main.zh_CN.ts │ │ ├── DDNetToolBox.view.en_US.ts │ │ └── DDNetToolBox.view.zh_CN.ts │ ├── light │ │ ├── demo.qss │ │ ├── link_card.qss │ │ ├── logo.png │ │ └── setting_interface.qss │ ├── logo.ico │ └── logo.png ├── utils │ ├── __init__.py │ ├── config_directory.py │ ├── draw_tee.py │ ├── image_alpha_check.py │ ├── network.py │ ├── player_name.py │ └── points_rank.py └── view │ ├── cfg_interface.py │ ├── home_interface.py │ ├── main_interface.py │ ├── player_point_interface.py │ ├── resource_download_interface.py │ ├── resource_interface.py │ ├── server_list_interface.py │ ├── server_list_preview_interface.py │ └── setting_interface.py ├── images ├── en │ ├── cfg.png │ ├── home.png │ ├── home_tab.png │ ├── points.png │ ├── resouces.png │ ├── server_list.png │ └── settings.png └── zh │ ├── cfg.png │ ├── home.png │ ├── home_tab.png │ ├── points.png │ ├── resouces.png │ ├── server_list.png │ └── settings.png ├── main.py └── requirements.txt /.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 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. windows, macos] 28 | - Browser [e.g. chrome, firefox] 29 | - DDNetToolBox Version [e.g. v1.1.5] 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/nightly-build.yml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: 3 | branches: 4 | - main 5 | 6 | env: 7 | PYTHON_VERSION: '3.11' 8 | 9 | jobs: 10 | build: 11 | strategy: 12 | matrix: 13 | os: [ubuntu-latest, windows-latest, macos-latest] 14 | runs-on: ${{ matrix.os }} 15 | environment: dev 16 | steps: 17 | - name: '签出仓库' 18 | uses: actions/checkout@v4 19 | 20 | - name: 初始化 Python ${{ env.PYTHON_VERSION }} 环境 21 | uses: actions/setup-python@v4 22 | with: 23 | python-version: ${{ env.PYTHON_VERSION }} 24 | 25 | - name: '使用 pip 安装依赖' 26 | shell: bash 27 | run: | 28 | python -m pip install --upgrade pip 29 | pip install -r requirements.txt 30 | pip install imageio 31 | 32 | - name: 构建可执行文件 33 | uses: Nuitka/Nuitka-Action@v1.1 34 | with: 35 | nuitka-version: main 36 | script-name: main.py 37 | onefile: true 38 | windows-icon-from-ico: app/resource/logo.ico 39 | enable-plugins: pyqt5 40 | include-data-dir: | 41 | ./app=app 42 | disable-console: true 43 | output-file: DDNetToolBox 44 | output-dir: build 45 | mingw64: true 46 | macos-create-app-bundle: true 47 | 48 | - name: 上传文件 49 | uses: actions/upload-artifact@v4 50 | with: 51 | name: DDNetToolBox-${{ runner.os }}-dev${{ matrix.os == 'windows-latest' && '.exe' || '' }} 52 | path: build/DDNetToolBox${{ matrix.os == 'windows-latest' && '.exe' || '' }} 53 | compression-level: 0 -------------------------------------------------------------------------------- /.github/workflows/release-build.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | tags: 4 | - "v*.*.*" 5 | 6 | env: 7 | PYTHON_VERSION: '3.11' 8 | 9 | name: release-build 10 | jobs: 11 | build: 12 | strategy: 13 | matrix: 14 | os: [ubuntu-latest, windows-latest, macos-latest] 15 | runs-on: ${{ matrix.os }} 16 | steps: 17 | - name: '签出仓库' 18 | uses: actions/checkout@v4 19 | 20 | - name: 初始化 Python ${{ env.PYTHON_VERSION }} 环境 21 | uses: actions/setup-python@v4 22 | with: 23 | python-version: ${{ env.PYTHON_VERSION }} 24 | 25 | - name: '使用 pip 安装依赖' 26 | shell: bash 27 | run: | 28 | python -m pip install --upgrade pip 29 | pip install -r requirements.txt 30 | pip install imageio 31 | 32 | - name: 构建可执行文件 33 | uses: Nuitka/Nuitka-Action@v1.1 34 | with: 35 | nuitka-version: main 36 | script-name: main.py 37 | onefile: true 38 | windows-icon-from-ico: app/resource/logo.ico 39 | enable-plugins: pyqt5 40 | include-data-dir: | 41 | ./app=app 42 | disable-console: true 43 | output-file: DDNetToolBox 44 | output-dir: build 45 | mingw64: true 46 | macos-create-app-bundle: true 47 | 48 | - name: '上传发行' 49 | uses: svenstaro/upload-release-action@v2 50 | with: 51 | repo_token: ${{ secrets.GITHUB_TOKEN }} 52 | tag: ${{ github.ref_name }} 53 | file: build/DDNetToolBox${{ matrix.os == 'windows-latest' && '.exe' || '' }} 54 | asset_name: DDNetToolBox-${{ runner.os }}-${{ github.ref_name }}${{ matrix.os == 'windows-latest' && '.exe' || '' }} 55 | prerelease: true 56 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. 10 | 11 | "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. 12 | 13 | "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. 14 | 15 | "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. 16 | 17 | "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. 18 | 19 | "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. 20 | 21 | "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). 22 | 23 | "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. 24 | 25 | "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." 26 | 27 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 28 | 29 | 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 30 | 31 | 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 32 | 33 | 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: 34 | 35 | (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and 36 | 37 | (b) You must cause any modified files to carry prominent notices stating that You changed the files; and 38 | 39 | (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and 40 | 41 | (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. 42 | 43 | You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 44 | 45 | 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 46 | 47 | 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 48 | 49 | 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 50 | 51 | 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 52 | 53 | 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. 54 | 55 | END OF TERMS AND CONDITIONS 56 | 57 | APPENDIX: How to apply the Apache License to your work. 58 | 59 | To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. 60 | 61 | Copyright 2024 XCWQW233 62 | 63 | Licensed under the Apache License, Version 2.0 (the "License"); 64 | you may not use this file except in compliance with the License. 65 | You may obtain a copy of the License at 66 | 67 | http://www.apache.org/licenses/LICENSE-2.0 68 | 69 | Unless required by applicable law or agreed to in writing, software 70 | distributed under the License is distributed on an "AS IS" BASIS, 71 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 72 | See the License for the specific language governing permissions and 73 | limitations under the License. 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | dTb 3 |

4 | 5 |
6 | 7 | # DDNetToolBox 8 | 9 | A toolbox for [DDRaceNetwork](https://ddnet.org/) 10 | 11 |
12 | 13 |

14 | 15 | python 16 | 17 |

18 | 19 |

20 | English | 简体中文 21 |

22 | 23 | > If you have any suggestions or problems during use, please raise an Issue 24 | 25 | ### Tips: 26 | - If your `browser` or `antivirus software` flags the file as a virus, but you downloaded the executable from the `GitHub release` page, you can safely allow it. All executables in the `release` section are built and uploaded via `GitHub Actions`, ensuring they are secure and trustworthy. 27 | 28 | ### Get Started: 29 | 30 | - Download the version of the corresponding platform from Release, and double-click to open it after downloading. 31 | 32 | ### Special thanks to the following projects and contributors: 33 | 34 | - [PyQt-Fluent-Widgets](https://github.com/zhiyiYo/PyQt-Fluent-Widgets) - A fluent design widgets library based on C++ Qt/PyQt/PySide. 35 | - [ddnet-discordbot](https://github.com/ddnet/ddnet-discordbot) - DDNetDiscordBOT,Part of the source code of this project is taken from this project. 36 | - [REALYNUnU](https://github.com/REALYNUnU) - Created a logo for this project 37 | 38 | ### Software Screenshots 39 | 40 | ![front_page](images/en/home.png) 41 | ![sidebar](images/en/home_tab.png) 42 | ![points](images/en/points.png) 43 | ![cfg_management](images/en/cfg.png) 44 | ![resource_management](images/en/resouces.png) 45 | ![server_list_management](images/en/server_list.png) 46 | ![setting](images/en/settings.png) 47 | 48 | ### Contributors 49 | 50 | 51 | 52 | 53 | 54 | ### Star Trend Chart 55 | 56 | 57 | 63 | 69 | Star History Chart 73 | -------------------------------------------------------------------------------- /README_zh.md: -------------------------------------------------------------------------------- 1 |

2 | dTb 3 |

4 | 5 |
6 | 7 | # DDNetToolBox 8 | 9 | 一个适用于 [DDRaceNetwork](https://ddnet.org/) 的工具箱 10 | 11 |
12 | 13 |

14 | 15 | python 16 | 17 |

18 | 19 |

20 | English | 简体中文 21 |

22 | 23 | > 有任何建议或使用中出现了问题,请发起Issues或加入QQ群内反馈:818266207 24 | 25 | ### 小贴士: 26 | - 如果您的`浏览器`或`杀毒软件`提示该文件为病毒,但您是从`GitHub release`页面下载的可执行文件,请放心同意。`release`中的所有可执行文件均由`GitHub Action`自动构建和推送,安全可靠。 27 | 28 | ### 开始使用: 29 | 30 | - 从 Release 中下载对应平台的版本,下载后双击打开即可 31 | - 如果下载速度缓慢可加入QQ从群文件内下载 32 | 33 | ### 特别感谢以下项目和贡献者: 34 | 35 | - [PyQt-Fluent-Widgets](https://github.com/zhiyiYo/PyQt-Fluent-Widgets) - 基于 C++ Qt/PyQt/PySide 的流畅设计小部件库。 36 | - [ddnet-discordbot](https://github.com/ddnet/ddnet-discordbot) - DDNetDiscordBOT,本项目部分源码取自该项目 37 | - [REALYNUnU](https://github.com/REALYNUnU) - 为本项目制作了logo 38 | 39 | ### 软件截图 40 | 41 | ![首页](images/zh/home.png) 42 | ![侧栏](images/zh/home_tab.png) 43 | ![查分](images/zh/points.png) 44 | ![cfg管理](images/zh/cfg.png) 45 | ![资源管理](images/zh/resouces.png) 46 | ![服务器列表管理](images/zh/server_list.png) 47 | ![设置](images/zh/settings.png) 48 | 49 | ### 贡献者 50 | 51 | 52 | 53 | 54 | 55 | ### Star 趋势图 56 | 57 | 58 | 64 | 70 | Star History Chart 74 | -------------------------------------------------------------------------------- /app/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | from enum import Enum 3 | 4 | import platformdirs 5 | from PyQt5.QtCore import QLocale 6 | from qfluentwidgets import (qconfig, QConfig, ConfigItem, Theme, OptionsConfigItem, 7 | OptionsValidator, EnumSerializer, FolderValidator, BoolValidator, ColorConfigItem, 8 | ConfigSerializer) 9 | 10 | from app.utils.config_directory import get_ddnet_directory 11 | 12 | base_path = os.path.dirname(__file__) 13 | config_path = platformdirs.user_config_dir(appname="DDNetToolBox", appauthor="XCWQW233") 14 | 15 | 16 | class Language(Enum): 17 | """ Language enumeration """ 18 | 19 | CHINESE_SIMPLIFIED = QLocale(QLocale.Chinese, QLocale.China) 20 | ENGLISH = QLocale(QLocale.English) 21 | AUTO = QLocale() 22 | 23 | 24 | class LanguageSerializer(ConfigSerializer): 25 | """ Language serializer """ 26 | 27 | def serialize(self, language): 28 | return language.value.name() if language != Language.AUTO else "Auto" 29 | 30 | def deserialize(self, value: str): 31 | return Language(QLocale(value)) if value != "Auto" else Language.AUTO 32 | 33 | 34 | 35 | class Config(QConfig): 36 | themeMode = OptionsConfigItem("个性化", "ThemeMode", Theme.AUTO, OptionsValidator(Theme), 37 | EnumSerializer(Theme)) 38 | dpiScale = OptionsConfigItem( 39 | "个性化", "DpiScale", "Auto", OptionsValidator([1, 1.25, 1.5, 1.75, 2, "Auto"]), restart=True) 40 | themeColor = ColorConfigItem("个性化", "ThemeColor", "#009faa") 41 | language = OptionsConfigItem("个性化", "Language", Language.AUTO, OptionsValidator(Language), LanguageSerializer(), restart=True) 42 | DDNetFolder = ConfigItem("DDNet", "DDN1etFolder", get_ddnet_directory(), FolderValidator()) 43 | DDNetCheckUpdate = ConfigItem("DDNet", "DDNetCheckUpdate", True, BoolValidator()) 44 | DDNetAssetsCursor = ConfigItem("DDNet", "DDNetAssetsCursor", None) 45 | 46 | 47 | cfg = Config() 48 | qconfig.load(config_path + '/app/config/config.json', cfg) 49 | -------------------------------------------------------------------------------- /app/globals.py: -------------------------------------------------------------------------------- 1 | from app.config import cfg 2 | 3 | 4 | class GlobalsVal: 5 | ddnet_setting_config = {} 6 | ddnet_info = None 7 | server_list_file = False 8 | DDNetToolBoxVersion = "v1.1.6" 9 | ddnet_folder = cfg.get(cfg.DDNetFolder) 10 | ddnet_folder_status = False 11 | main_window = None 12 | teedata_build_id = None 13 | -------------------------------------------------------------------------------- /app/resource/dark/demo.qss: -------------------------------------------------------------------------------- 1 | MinimizeButton { 2 | qproperty-normalColor: white; 3 | qproperty-normalBackgroundColor: transparent; 4 | qproperty-hoverColor: white; 5 | qproperty-hoverBackgroundColor: rgba(255, 255, 255, 26); 6 | qproperty-pressedColor: white; 7 | qproperty-pressedBackgroundColor: rgba(255, 255, 255, 51) 8 | } 9 | 10 | 11 | MaximizeButton { 12 | qproperty-normalColor: white; 13 | qproperty-normalBackgroundColor: transparent; 14 | qproperty-hoverColor: white; 15 | qproperty-hoverBackgroundColor: rgba(255, 255, 255, 26); 16 | qproperty-pressedColor: white; 17 | qproperty-pressedBackgroundColor: rgba(255, 255, 255, 51) 18 | } 19 | 20 | CloseButton { 21 | qproperty-normalColor: white; 22 | qproperty-normalBackgroundColor: transparent; 23 | } 24 | 25 | StandardTitleBar > QLabel { 26 | color: white; 27 | } -------------------------------------------------------------------------------- /app/resource/dark/link_card.qss: -------------------------------------------------------------------------------- 1 | LinkCard { 2 | border: 1px solid rgb(46, 46, 46); 3 | border-radius: 10px; 4 | background-color: rgba(39, 39, 39, 0.95); 5 | } 6 | 7 | LinkCard:hover { 8 | background-color: rgba(39, 39, 39, 0.93); 9 | border: 1px solid rgb(66, 66, 66); 10 | } 11 | 12 | #titleLabel { 13 | font: 18px 'Segoe UI', 'Microsoft YaHei', 'PingFang SC'; 14 | color: white; 15 | } 16 | 17 | #contentLabel { 18 | font: 12px 'Segoe UI', 'Microsoft YaHei', 'PingFang SC'; 19 | color: rgb(208, 208, 208); 20 | } 21 | 22 | LinkCardView { 23 | background-color: transparent; 24 | border: none; 25 | } 26 | 27 | #view { 28 | background-color: transparent; 29 | } -------------------------------------------------------------------------------- /app/resource/dark/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XCWQW1/DDNetToolBox/876d56cc669e7546a1aef5d38f9feb190cd91706/app/resource/dark/logo.png -------------------------------------------------------------------------------- /app/resource/dark/setting_interface.qss: -------------------------------------------------------------------------------- 1 | SettingInterface, #scrollWidget { 2 | background-color: rgb(39, 39, 39); 3 | } 4 | 5 | QScrollArea { 6 | border: none; 7 | background-color: rgb(39, 39, 39); 8 | } 9 | 10 | 11 | /* 标签 */ 12 | QLabel#settingLabel { 13 | font: 33px 'Microsoft YaHei Light'; 14 | background-color: transparent; 15 | color: white; 16 | } 17 | 18 | -------------------------------------------------------------------------------- /app/resource/i18n/DDNetToolBox.main.en_US.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFGInterface 6 | 7 | 8 | 我们的程序无法自动找到DDNet配置目录 9 | 请手动到设置中指定DDNet配置目录 10 | Our program cannot automatically find the DDNet configuration directory. 11 | Please manually specify the DDNet configuration directory in the settings. 12 | 13 | 14 | 15 | CFG管理 16 | CFG Management 17 | 18 | 19 | 20 | 错误 21 | ERROR 22 | 23 | 24 | 25 | 您没有选择任何文件 26 | You have not selected any files 27 | 28 | 29 | 30 | 文件 {} 复制失败 31 | 原因:{} 32 | File {} copy failed 33 | Reason: {} 34 | 35 | 36 | 37 | 文件复制已完成 38 | 共复制了 {} 个文件,{} 个文件被覆盖,{} 个文件失败 39 | File copying completed 40 | A total of {} files were copied, {} files were overwritten, and {} files failed 41 | 42 | 43 | 44 | 警告 45 | WARNING 46 | 47 | 48 | 49 | 您没有选择任何东西 50 | You have not selected anything 51 | 52 | 53 | 54 | 此操作将会从磁盘中永久删除下列文件,不可恢复: 55 | {} 56 | This operation will permanently delete the following files from disk and cannot be recovered: 57 | {} 58 | 59 | 60 | 61 | 成功 62 | SUCCESS 63 | 64 | 65 | 66 | 共删除 {} 个文件,{} 个文件删除失败 67 | A total of {} files were deleted, and {} files failed to be deleted. 68 | 69 | 70 | 71 | 已重新加载本地资源 72 | Local resources have been reloaded 73 | 74 | 75 | 76 | 添加 77 | Add 78 | 79 | 80 | 81 | 删除 82 | Delete 83 | 84 | 85 | 86 | 刷新 87 | Refresh 88 | 89 | 90 | 91 | CFGSelectMessageBox 92 | 93 | 94 | 选择CFG文件 95 | Select CFG file 96 | 97 | 98 | 99 | 拖拽文件到此处或点击选择文件 100 | Drag and drop files here or click to select files 101 | 102 | 103 | 104 | DDNetFolderCrash 105 | 106 | 107 | 我们的程序无法自动找到DDNet配置目录 108 | 请手动到设置中指定DDNet配置目录 109 | Our program cannot automatically find the DDNet configuration directory. 110 | Please manually specify the DDNet configuration directory in the settings. 111 | 112 | 113 | 114 | FileSelectMessageBox 115 | 116 | 117 | 选择文件 118 | Select File 119 | 120 | 121 | 122 | 拖拽文件到此处或点击选择文件 123 | Drag and drop files here or click to select files 124 | 125 | 126 | 127 | 确认 128 | Confirm 129 | 130 | 131 | 132 | 取消 133 | Cancel 134 | 135 | 136 | 137 | HomeInterface 138 | 139 | 140 | DDNet 版本检测 141 | DDNet version detection 142 | 143 | 144 | 145 | 无法连接到DDNet官网 146 | Unable to connect to DDNet official website 147 | 148 | 149 | 150 | 您当前的DDNet版本为 {} 最新版本为 {} 请及时更新 151 | Your current DDNet version is {} The latest version is {} Please update in time 152 | 153 | 154 | 155 | MainWindow 156 | 157 | 158 | 首页 159 | Home 160 | 161 | 162 | 163 | CFG管理 164 | CFG Management 165 | 166 | 167 | 168 | 材质管理 169 | Material Management 170 | 171 | 172 | 173 | 服务器列表管理 174 | Server List Management 175 | 176 | 177 | 178 | 设置 179 | Settings 180 | 181 | 182 | 183 | 玩家分数查询 184 | Player score query 185 | 186 | 187 | 188 | 警告 189 | WARNING 190 | 191 | 192 | 193 | DDNet配置文件目录配置错误,部分功能将被禁用 194 | 请于设置中修改后重启本程序 195 | 请勿设置为DDNet游戏目录 196 | DDNet configuration file directory configuration error, some functions will be disabled 197 | Please modify in settings and restart this program 198 | Do not set as DDNet game directory 199 | 200 | 201 | 202 | MapStatus 203 | 204 | 205 | 地图 206 | Map 207 | 208 | 209 | 210 | 分数 211 | Points 212 | 213 | 214 | 215 | 队伍排名 216 | Team Rank 217 | 218 | 219 | 220 | 全球排名 221 | Rank 222 | 223 | 224 | 225 | 用时 226 | Time 227 | 228 | 229 | 230 | 通关次数 231 | Finishes 232 | 233 | 234 | 235 | 首次完成于 236 | First Finish 237 | 238 | 239 | 240 | 分数 (共 {} 点) 241 | Points ({} total) 242 | 243 | 244 | 245 | 未排名 246 | Unranked 247 | 248 | 249 | 250 | 第 {} 名,共 {} 分 251 | {}. with {} points 252 | 253 | 254 | 255 | 地图 (共 {} 张) 256 | Maps ({} total) 257 | 258 | 259 | 260 | 已完成 {} 张,剩余 {} 张未完成 261 | {} completed, {} unfinished 262 | 263 | 264 | 265 | PlayerPointInterface 266 | 267 | 268 | 填写要查询的玩家名称 269 | Enter the name of the player to be queried 270 | 271 | 272 | 273 | 填写要比较的玩家名称 274 | Enter the name of the player to compare 275 | 276 | 277 | 278 | ResourceCard 279 | 280 | 281 | 启用 282 | Enable 283 | 284 | 285 | 286 | 禁用 287 | Disable 288 | 289 | 290 | 291 | ResourceInterface 292 | 293 | 294 | 我们的程序无法自动找到DDNet配置目录 295 | 请手动到设置中指定DDNet配置目录 296 | Our program cannot automatically find the DDNet configuration directory. 297 | Please manually specify the DDNet configuration directory in the settings. 298 | 299 | 300 | 301 | 材质管理 302 | Material Management 303 | 304 | 305 | 306 | 皮肤 307 | SKINS 308 | 309 | 310 | 311 | 贴图 312 | GAMESKINS 313 | 314 | 315 | 316 | 表情 317 | EMOTICONS 318 | 319 | 320 | 321 | 光标 322 | CURSORS 323 | 324 | 325 | 326 | 粒子 327 | PARTICLES 328 | 329 | 330 | 331 | 实体层 332 | ENTITIES 333 | 334 | 335 | 336 | 错误 337 | ERROR 338 | 339 | 340 | 341 | 您没有选择任何文件 342 | You have not selected any files 343 | 344 | 345 | 346 | 文件 {} 复制失败 347 | 原因:{} 348 | File {} copy failed 349 | Reason: {} 350 | 351 | 352 | 353 | 文件复制已完成 354 | 共复制了 {} 个文件,{} 个文件被覆盖,{} 个文件失败 355 | File copying completed 356 | A total of {} files were copied, {} files were overwritten, and {} files failed 357 | 358 | 359 | 360 | 警告 361 | WARNING 362 | 363 | 364 | 365 | 您没有选择任何东西 366 | You have not selected anything 367 | 368 | 369 | 370 | 此操作将会从磁盘中永久删除下列文件,不可恢复: 371 | {} 372 | This operation will permanently delete the following files from disk and cannot be recovered: 373 | {} 374 | 375 | 376 | 377 | 成功 378 | SUCCESS 379 | 380 | 381 | 382 | 共删除 {} 个文件,{} 个文件删除失败 383 | A total of {} files were deleted, and {} files failed to be deleted. 384 | 385 | 386 | 387 | 已重新加载本地资源 388 | Local resources have been reloaded 389 | 390 | 391 | 392 | 添加 393 | Add 394 | 395 | 396 | 397 | 删除 398 | Delete 399 | 400 | 401 | 402 | 刷新 403 | Refresh 404 | 405 | 406 | 407 | ServerListInterface 408 | 409 | 410 | 我们的程序无法自动找到DDNet配置目录 411 | 请手动到设置中指定DDNet配置目录 412 | Our program cannot automatically find the DDNet configuration directory. 413 | Please manually specify the DDNet configuration directory in the settings. 414 | 415 | 416 | 417 | 服务器列表管理 418 | Server List Management 419 | 420 | 421 | 422 | 双击我进行编辑 423 | Double click me to edit 424 | 425 | 426 | 427 | 警告 428 | WARNING 429 | 430 | 431 | 432 | 您没有选择任何东西 433 | You have not selected anything 434 | 435 | 436 | 437 | 检测到空行,是否删除 438 | Detected empty line, whether to delete 439 | 440 | 441 | 442 | 成功 443 | WARNING 444 | 445 | 446 | 447 | 已剔除当前空行 448 | The current empty line has been removed 449 | 450 | 451 | 452 | 已保留当前空行 453 | The current empty line has been retained 454 | 455 | 456 | 457 | 列表内容为空,是否继续写入 458 | The list is empty, do you want to continue writing? 459 | 460 | 461 | 462 | 服务器列表已保存 463 | Server list saved 464 | 465 | 466 | 467 | 已重新加载本地资源 468 | Local resources have been reloaded 469 | 470 | 471 | 472 | 该操作将会覆盖现有服务器列表中的所有内容 473 | This operation will overwrite all existing server lists. 474 | 475 | 476 | 477 | 已重置服务器列表 478 | Server list reset 479 | 480 | 481 | 482 | 已加速服务器列表,重启游戏生效 483 | The server list has been accelerated, restart the game to take effect 484 | 485 | 486 | 487 | 添加 488 | Add 489 | 490 | 491 | 492 | 删除 493 | Delete 494 | 495 | 496 | 497 | 保存 498 | Save 499 | 500 | 501 | 502 | 刷新 503 | Refresh 504 | 505 | 506 | 507 | 重置 508 | Reset 509 | 510 | 511 | 512 | 一键加速 513 | One-click acceleration 514 | 515 | 516 | 517 | SettingInterface 518 | 519 | 520 | DDNet 521 | DDNet 522 | 523 | 524 | 525 | 更改目录 526 | Change Directory 527 | 528 | 529 | 530 | DDNet配置目录 531 | DDNet Configuration Directory 532 | 533 | 534 | 535 | 检测DDNet版本更新 536 | Check DDNet version update 537 | 538 | 539 | 540 | 在启动工具箱的时候检测DDNet客户端版本是否为最新 541 | Check if the DDNet client version is the latest when starting the toolbox 542 | 543 | 544 | 545 | 个性化 546 | Personalization 547 | 548 | 549 | 550 | 应用主题 551 | App Theme 552 | 553 | 554 | 555 | 调整你的应用外观 556 | Adjust your app's appearance 557 | 558 | 559 | 560 | 浅色 561 | Light 562 | 563 | 564 | 565 | 深色 566 | Dark 567 | 568 | 569 | 570 | 跟随系统设置 571 | Follow system default 572 | 573 | 574 | 575 | 主题颜色 576 | Theme Colors 577 | 578 | 579 | 580 | 更改应用程序的主题颜色 581 | Change the application theme color 582 | 583 | 584 | 585 | 缩放大小 586 | Scaling 587 | 588 | 589 | 590 | 更改小部件和字体的大小 591 | Change the size of widgets and fonts 592 | 593 | 594 | 595 | 跟随系统默认 596 | Follow system default 597 | 598 | 599 | 600 | 语言 601 | Language 602 | 603 | 604 | 605 | 更改首选语言 606 | Change the preferred language 607 | 608 | 609 | 610 | 其他 611 | Other 612 | 613 | 614 | 615 | 检查更新 616 | Check for updates 617 | 618 | 619 | 620 | 关于 621 | About 622 | 623 | 624 | 625 | 当前工具箱版本:{},logo 由 燃斯(Realyn//UnU) 绘制 626 | Current toolbox version: {}, logo drawn by Ran Si (Realyn//UnU) 627 | 628 | 629 | 630 | 您当前的DDNetToolBox版本为 {} 最新版本为 {} 请及时更新 631 | Your current DDNetToolBox version is {} The latest version is {} Please update in time 632 | 633 | 634 | 635 | 您的DDNetToolBox为最新版 636 | Your DDNetToolBox is the latest version 637 | 638 | 639 | 640 | 正在检查更新中... 641 | Checking for updates... 642 | 643 | 644 | 645 | 成功 646 | SUCCESS 647 | 648 | 649 | 650 | 重启以应用更改 651 | Configuration takes effect after restart 652 | 653 | 654 | 655 | 无法访问到github 656 | Unable to access github 657 | 658 | 659 | 660 | 打开目录 661 | Open Directory 662 | 663 | 664 | 665 | 工具箱配置目录 666 | DDNetToolbox Configuration Directory 667 | 668 | 669 | 670 | 打开工具箱配置文件所在目录 671 | Open the directory where the DDNetToolBox configuration file is located 672 | 673 | 674 | 675 | 识别到的DDNet配置文件夹为:{} 676 | The identified DDNet configuration folder is: {} 677 | 678 | 679 | 680 | 错误 681 | ERROR 682 | 683 | 684 | 685 | 没有找到DDNet配置文件夹 686 | DDNet configuration folder not found 687 | 688 | 689 | 690 | 自动寻找 691 | Automatic search 692 | 693 | 694 | 695 | TEECard 696 | 697 | 698 | 全球排名:加载中... 699 | 游戏分数:加载中... 700 | 游玩时长:加载中... 701 | 最后完成:加载中... 702 | 入坑时间:加载中... 703 | Global Ranking: Loading... 704 | Game Score: Loading... 705 | Playtime: Loading... 706 | Last Completed: Loading... 707 | Join Date: Loading... 708 | 709 | 710 | 711 | 全球排名:NO.{} 712 | 游戏分数:{}/{} 分 713 | 游玩时长:{} 小时 714 | 最后完成:{} 715 | 入坑时间:{} 716 | Rank: No. {} 717 | Points: {}/{} Points 718 | Playtime: {} Hours 719 | Last Completed: {} 720 | Join Date: {} 721 | 722 | 723 | 724 | 单击刷新数据 725 | Click Refresh Data 726 | 727 | 728 | 729 | 全球排名:NO.数据获取失败 730 | 游戏分数:数据获取失败 分 731 | 游玩时长:数据获取失败 小时 732 | 最后完成:数据获取失败 733 | 入坑时间:数据获取失败 734 | Rank: No. Data Retrieval Failed 735 | Points: Data Retrieval Failed Points 736 | Playtime: Data Retrieval Failed Hours 737 | Last Completed: Data Retrieval Failed 738 | Join Date: Data Retrieval Failed 739 | 740 | 741 | 742 | 全球排名:NO.查无此人 743 | 游戏分数:查无此人 分 744 | 游玩时长:查无此人 小时 745 | 最后完成:查无此人 746 | 入坑时间:查无此人 747 | Rank: No. No such person 748 | Points: No such person Points 749 | Playtime: No such person Hours 750 | Last Completed: No such person 751 | Join Date: No such person 752 | 753 | 754 | 755 | TEEInfo 756 | 757 | 758 | Novice 简单 759 | Novice 760 | 761 | 762 | 763 | Moderate 中阶 764 | Moderate 765 | 766 | 767 | 768 | Brutal 高阶 769 | Brutal 770 | 771 | 772 | 773 | Insane 疯狂 774 | Insane 775 | 776 | 777 | 778 | Dummy 分身 779 | Dummy 780 | 781 | 782 | 783 | DDmaX.Easy 古典.简单 784 | DDmaX.Easy 785 | 786 | 787 | 788 | DDmaX.Next 古典.中阶 789 | DDmaX.Next 790 | 791 | 792 | 793 | DDmaX.Pro 古典.高阶 794 | DDmaX.Pro 795 | 796 | 797 | 798 | DDmaX.Nut 古典.坚果 799 | DDmaX.Nut 800 | 801 | 802 | 803 | Oldschool 传统 804 | Oldschool 805 | 806 | 807 | 808 | Solo 单人 809 | Solo 810 | 811 | 812 | 813 | Race 竞速 814 | Race 815 | 816 | 817 | 818 | Fun 娱乐 819 | Fun 820 | 821 | 822 | 823 | 搜点什么... 824 | Search for something... 825 | 826 | 827 | 828 | TEEInfoList 829 | 830 | 831 | 本体 832 | Player 833 | 834 | 835 | 836 | 分身 837 | Dummy 838 | 839 | 840 | 841 | TEERankCard 842 | 843 | 844 | 单击刷新数据 845 | Click Refresh Data 846 | 847 | 848 | 849 | 全球排名:加载中... 850 | 游戏分数:加载中... 851 | 游玩时长:加载中... 852 | 最后完成:加载中... 853 | 入坑时间:加载中... 854 | Global Ranking: Loading... 855 | Game Score: Loading... 856 | Playtime: Loading... 857 | Last Completed: Loading... 858 | Join Date: Loading... 859 | 860 | 861 | 862 | 全球排名:NO.数据获取失败 863 | 游戏分数:数据获取失败 分 864 | 游玩时长:数据获取失败 小时 865 | 最后完成:数据获取失败 866 | 入坑时间:数据获取失败 867 | Rank: No. Data Retrieval Failed 868 | Points: Data Retrieval Failed Points 869 | Playtime: Data Retrieval Failed Hours 870 | Last Completed: Data Retrieval Failed 871 | Join Date: Data Retrieval Failed 872 | 873 | 874 | 875 | 全球排名:NO.{} 876 | 游戏分数:{}/{} 分 877 | 游玩时长:{} 小时 878 | 最后完成:{} 879 | 入坑时间:{} 880 | Rank: No. {} 881 | Points: {}/{} Points 882 | Playtime: {} Hours 883 | Last Completed: {} 884 | Join Date: {} 885 | 886 | 887 | 888 | 全球排名:NaN 889 | 游戏分数:NaN 890 | 游玩时长:NaN 891 | 最后完成:NaN 892 | 入坑时间:NaN 893 | Rank: No. NaN 894 | Points: NaN Points 895 | Playtime: NaN Hours 896 | Last Completed: NaN 897 | Join Date: NaN 898 | 899 | 900 | 901 | 全球排名:NO.查无此人 902 | 游戏分数:查无此人 分 903 | 游玩时长:查无此人 小时 904 | 最后完成:查无此人 905 | 入坑时间:查无此人 906 | Rank: No. No such person 907 | Points: No such person Points 908 | Playtime: No such person Hours 909 | Last Completed: No such person 910 | Join Date: No such person 911 | 912 | 913 | 914 | -------------------------------------------------------------------------------- /app/resource/i18n/DDNetToolBox.main.zh_CN.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/resource/i18n/DDNetToolBox.view.en_US.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFGInterface 6 | 7 | 8 | 我们的程序无法自动找到DDNet配置目录 9 | 请手动到设置中指定DDNet配置目录 10 | Our program cannot automatically find the DDNet configuration directory. 11 | Please manually specify the DDNet configuration directory in the settings. 12 | 13 | 14 | 15 | CFG管理 16 | CFG Management 17 | 18 | 19 | 20 | 错误 21 | ERROR 22 | 23 | 24 | 25 | 您没有选择任何文件 26 | You have not selected any files 27 | 28 | 29 | 30 | 文件 {} 复制失败 31 | 原因:{} 32 | File {} copy failed 33 | Reason: {} 34 | 35 | 36 | 37 | 文件复制已完成 38 | 共复制了 {} 个文件,{} 个文件被覆盖,{} 个文件失败 39 | File copying completed 40 | A total of {} files were copied, {} files were overwritten, and {} files failed 41 | 42 | 43 | 44 | 警告 45 | WARNING 46 | 47 | 48 | 49 | 您没有选择任何东西 50 | You have not selected anything 51 | 52 | 53 | 54 | 此操作将会从磁盘中永久删除下列文件,不可恢复: 55 | {} 56 | This operation will permanently delete the following files from disk and cannot be recovered: 57 | {} 58 | 59 | 60 | 61 | 成功 62 | SUCCESS 63 | 64 | 65 | 66 | 共删除 {} 个文件,{} 个文件删除失败 67 | A total of {} files were deleted, and {} files failed to be deleted. 68 | 69 | 70 | 71 | 已重新加载本地资源 72 | Local resources have been reloaded 73 | 74 | 75 | 76 | 添加 77 | Add 78 | 79 | 80 | 81 | 删除 82 | Delete 83 | 84 | 85 | 86 | 刷新 87 | Refresh 88 | 89 | 90 | 91 | CFGSelectMessageBox 92 | 93 | 94 | 选择CFG文件 95 | Select CFG file 96 | 97 | 98 | 99 | 拖拽文件到此处或点击选择文件 100 | Drag and drop files here or click to select files 101 | 102 | 103 | 104 | FileSelectMessageBox 105 | 106 | 107 | 选择文件 108 | Select File 109 | 110 | 111 | 112 | 拖拽文件到此处或点击选择文件 113 | Drag and drop files here or click to select files 114 | 115 | 116 | 117 | 确认 118 | Confirm 119 | 120 | 121 | 122 | 取消 123 | Cancel 124 | 125 | 126 | 127 | HomeInterface 128 | 129 | 130 | DDNet 版本检测 131 | DDNet version detection 132 | 133 | 134 | 135 | 无法连接到DDNet官网 136 | Unable to connect to DDNet official website 137 | 138 | 139 | 140 | 您当前的DDNet版本为 {} 最新版本为 {} 请及时更新 141 | Your current DDNet version is {} The latest version is {} Please update in time 142 | 143 | 144 | 145 | MainWindow 146 | 147 | 148 | 首页 149 | Home 150 | 151 | 152 | 153 | CFG管理 154 | CFG Management 155 | 156 | 157 | 158 | 材质管理 159 | Material Management 160 | 161 | 162 | 163 | 服务器列表管理 164 | Server List Management 165 | 166 | 167 | 168 | 设置 169 | Settings 170 | 171 | 172 | 173 | ResourceCard 174 | 175 | 176 | 启用 177 | Enable 178 | 179 | 180 | 181 | 禁用 182 | Disable 183 | 184 | 185 | 186 | ResourceInterface 187 | 188 | 189 | 我们的程序无法自动找到DDNet配置目录 190 | 请手动到设置中指定DDNet配置目录 191 | Our program cannot automatically find the DDNet configuration directory. 192 | Please manually specify the DDNet configuration directory in the settings. 193 | 194 | 195 | 196 | 材质管理 197 | Material Management 198 | 199 | 200 | 201 | 皮肤 202 | SKINS 203 | 204 | 205 | 206 | 贴图 207 | GAMESKINS 208 | 209 | 210 | 211 | 表情 212 | EMOTICONS 213 | 214 | 215 | 216 | 光标 217 | CURSORS 218 | 219 | 220 | 221 | 粒子 222 | PARTICLES 223 | 224 | 225 | 226 | 实体层 227 | ENTITIES 228 | 229 | 230 | 231 | 错误 232 | ERROR 233 | 234 | 235 | 236 | 您没有选择任何文件 237 | You have not selected any files 238 | 239 | 240 | 241 | 文件 {} 复制失败 242 | 原因:{} 243 | File {} copy failed 244 | Reason: {} 245 | 246 | 247 | 248 | 文件复制已完成 249 | 共复制了 {} 个文件,{} 个文件被覆盖,{} 个文件失败 250 | File copying completed 251 | A total of {} files were copied, {} files were overwritten, and {} files failed 252 | 253 | 254 | 255 | 警告 256 | WARNING 257 | 258 | 259 | 260 | 您没有选择任何东西 261 | You have not selected anything 262 | 263 | 264 | 265 | 此操作将会从磁盘中永久删除下列文件,不可恢复: 266 | {} 267 | This operation will permanently delete the following files from disk and cannot be recovered: 268 | {} 269 | 270 | 271 | 272 | 成功 273 | SUCCESS 274 | 275 | 276 | 277 | 共删除 {} 个文件,{} 个文件删除失败 278 | A total of {} files were deleted, and {} files failed to be deleted. 279 | 280 | 281 | 282 | 已重新加载本地资源 283 | Local resources have been reloaded 284 | 285 | 286 | 287 | 添加 288 | Add 289 | 290 | 291 | 292 | 删除 293 | Delete 294 | 295 | 296 | 297 | 刷新 298 | Refresh 299 | 300 | 301 | 302 | ServerListInterface 303 | 304 | 305 | 我们的程序无法自动找到DDNet配置目录 306 | 请手动到设置中指定DDNet配置目录 307 | Our program cannot automatically find the DDNet configuration directory. 308 | Please manually specify the DDNet configuration directory in the settings. 309 | 310 | 311 | 312 | 服务器列表管理 313 | Server List Management 314 | 315 | 316 | 317 | 双击我进行编辑 318 | Double click me to edit 319 | 320 | 321 | 322 | 警告 323 | WARNING 324 | 325 | 326 | 327 | 您没有选择任何东西 328 | You have not selected anything 329 | 330 | 331 | 332 | 检测到空行,是否删除 333 | Detected empty line, whether to delete 334 | 335 | 336 | 337 | 成功 338 | WARNING 339 | 340 | 341 | 342 | 已剔除当前空行 343 | The current empty line has been removed 344 | 345 | 346 | 347 | 已保留当前空行 348 | The current empty line has been retained 349 | 350 | 351 | 352 | 列表内容为空,是否继续写入 353 | The list is empty, do you want to continue writing? 354 | 355 | 356 | 357 | 服务器列表已保存 358 | Server list saved 359 | 360 | 361 | 362 | 已重新加载本地资源 363 | Local resources have been reloaded 364 | 365 | 366 | 367 | 该操作将会覆盖现有服务器列表中的所有内容 368 | This operation will overwrite all existing server lists. 369 | 370 | 371 | 372 | 已重置服务器列表 373 | Server list reset 374 | 375 | 376 | 377 | 已加速服务器列表,重启游戏生效 378 | The server list has been accelerated, restart the game to take effect 379 | 380 | 381 | 382 | 添加 383 | Add 384 | 385 | 386 | 387 | 删除 388 | Delete 389 | 390 | 391 | 392 | 保存 393 | Save 394 | 395 | 396 | 397 | 刷新 398 | Refresh 399 | 400 | 401 | 402 | 重置 403 | Reset 404 | 405 | 406 | 407 | 一键加速 408 | One-click acceleration 409 | 410 | 411 | 412 | SettingInterface 413 | 414 | 415 | DDNet 416 | DDNet 417 | 418 | 419 | 420 | 更改目录 421 | Change Directory 422 | 423 | 424 | 425 | DDNet配置目录 426 | DDNet Configuration Directory 427 | 428 | 429 | 430 | 检测DDNet版本更新 431 | Check DDNet version update 432 | 433 | 434 | 435 | 在启动工具箱的时候检测DDNet客户端版本是否为最新 436 | Check if the DDNet client version is the latest when starting the toolbox 437 | 438 | 439 | 440 | 个性化 441 | Personalization 442 | 443 | 444 | 445 | 应用主题 446 | App Theme 447 | 448 | 449 | 450 | 调整你的应用外观 451 | Adjust your app's appearance 452 | 453 | 454 | 455 | 浅色 456 | Light 457 | 458 | 459 | 460 | 深色 461 | Dark 462 | 463 | 464 | 465 | 跟随系统设置 466 | Follow system default 467 | 468 | 469 | 470 | 主题颜色 471 | Theme Colors 472 | 473 | 474 | 475 | 更改应用程序的主题颜色 476 | Change the application theme color 477 | 478 | 479 | 480 | 缩放大小 481 | Scaling 482 | 483 | 484 | 485 | 更改小部件和字体的大小 486 | Change the size of widgets and fonts 487 | 488 | 489 | 490 | 跟随系统默认 491 | Follow system default 492 | 493 | 494 | 495 | 语言 496 | Language 497 | 498 | 499 | 500 | 更改首选语言 501 | Change the preferred language 502 | 503 | 504 | 505 | 其他 506 | Other 507 | 508 | 509 | 510 | 检查更新 511 | Check for updates 512 | 513 | 514 | 515 | 关于 516 | About 517 | 518 | 519 | 520 | 当前工具箱版本:{},logo 由 燃斯(Realyn//UnU) 绘制 521 | Current toolbox version: {}, logo drawn by Ran Si (Realyn//UnU) 522 | 523 | 524 | 525 | 您当前的DDNetToolBox版本为 {} 最新版本为 {} 请及时更新 526 | Your current DDNetToolBox version is {} The latest version is {} Please update in time 527 | 528 | 529 | 530 | 您的DDNetToolBox为最新版 531 | Your DDNetToolBox is the latest version 532 | 533 | 534 | 535 | 正在检查更新中... 536 | Checking for updates... 537 | 538 | 539 | 540 | 成功 541 | SUCCESS 542 | 543 | 544 | 545 | 重启以应用更改 546 | Configuration takes effect after restart 547 | 548 | 549 | 550 | 无法访问到github 551 | Unable to access github 552 | 553 | 554 | 555 | TEECard 556 | 557 | 558 | 全球排名:加载中... 559 | 游戏分数:加载中... 560 | 游玩时长:加载中... 561 | 最后完成:加载中... 562 | 入坑时间:加载中... 563 | Global Ranking: Loading... 564 | Game Score: Loading... 565 | Playtime: Loading... 566 | Last Completed: Loading... 567 | Join Date: Loading... 568 | 569 | 570 | 571 | 全球排名:NO.数据获取失败 572 | 游戏分数:数据获取失败/数据获取失败 分 573 | 游玩时长:数据获取失败 小时 574 | 最后完成:数据获取失败 575 | 入坑时间:数据获取失败 576 | Global Ranking: NO. Data Retrieval Failed 577 | Game Score: Data Retrieval Failed / Data Retrieval Failed Points 578 | Playtime: Data Retrieval Failed 579 | Hours Last Completed: Data Retrieval Failed 580 | Join Date: Data Retrieval Failed 581 | 582 | 583 | 584 | 全球排名:NO.{} 585 | 游戏分数:{}/{} 分 586 | 游玩时长:{} 小时 587 | 最后完成:{} 588 | 入坑时间:{} 589 | Global Ranking: NO. {} 590 | Game Score: {}/{} Points 591 | Playtime: {} Hours 592 | Last Completed: {} 593 | Join Date: {} 594 | 595 | 596 | 597 | 单击刷新数据 598 | Click Refresh Data 599 | 600 | 601 | 602 | -------------------------------------------------------------------------------- /app/resource/i18n/DDNetToolBox.view.zh_CN.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/resource/light/demo.qss: -------------------------------------------------------------------------------- 1 | MinimizeButton { 2 | qproperty-normalColor: black; 3 | qproperty-normalBackgroundColor: transparent; 4 | qproperty-hoverColor: black; 5 | qproperty-hoverBackgroundColor: rgba(0, 0, 0, 26); 6 | qproperty-pressedColor: black; 7 | qproperty-pressedBackgroundColor: rgba(0, 0, 0, 51) 8 | } 9 | 10 | 11 | MaximizeButton { 12 | qproperty-normalColor: black; 13 | qproperty-normalBackgroundColor: transparent; 14 | qproperty-hoverColor: black; 15 | qproperty-hoverBackgroundColor: rgba(0, 0, 0, 26); 16 | qproperty-pressedColor: black; 17 | qproperty-pressedBackgroundColor: rgba(0, 0, 0, 51) 18 | } 19 | 20 | CloseButton { 21 | qproperty-normalColor: black; 22 | qproperty-normalBackgroundColor: transparent; 23 | } -------------------------------------------------------------------------------- /app/resource/light/link_card.qss: -------------------------------------------------------------------------------- 1 | LinkCard { 2 | border: 1px solid rgb(234, 234, 234); 3 | border-radius: 10px; 4 | background-color: rgba(249, 249, 249, 0.95); 5 | } 6 | 7 | LinkCard:hover { 8 | background-color: rgba(249, 249, 249, 0.93); 9 | border: 1px solid rgb(220, 220, 220); 10 | } 11 | 12 | #titleLabel { 13 | font: 18px 'Segoe UI', 'Microsoft YaHei', 'PingFang SC'; 14 | color: black; 15 | } 16 | 17 | #contentLabel { 18 | font: 12px 'Segoe UI', 'Microsoft YaHei', 'PingFang SC'; 19 | color: rgb(93, 93, 93); 20 | } 21 | 22 | LinkCardView { 23 | background-color: transparent; 24 | border: none; 25 | } 26 | 27 | #view { 28 | background-color: transparent; 29 | } -------------------------------------------------------------------------------- /app/resource/light/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XCWQW1/DDNetToolBox/876d56cc669e7546a1aef5d38f9feb190cd91706/app/resource/light/logo.png -------------------------------------------------------------------------------- /app/resource/light/setting_interface.qss: -------------------------------------------------------------------------------- 1 | SettingInterface, #scrollWidget { 2 | background-color: rgb(249, 249, 249); 3 | } 4 | 5 | QScrollArea { 6 | background-color: rgb(249, 249, 249); 7 | border: none; 8 | } 9 | 10 | 11 | /* 标签 */ 12 | QLabel#settingLabel { 13 | font: 33px 'Microsoft YaHei Light'; 14 | background-color: transparent; 15 | } 16 | 17 | -------------------------------------------------------------------------------- /app/resource/logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XCWQW1/DDNetToolBox/876d56cc669e7546a1aef5d38f9feb190cd91706/app/resource/logo.ico -------------------------------------------------------------------------------- /app/resource/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XCWQW1/DDNetToolBox/876d56cc669e7546a1aef5d38f9feb190cd91706/app/resource/logo.png -------------------------------------------------------------------------------- /app/utils/__init__.py: -------------------------------------------------------------------------------- 1 | import mimetypes 2 | 3 | 4 | def is_image(file_path): 5 | mime_type, _ = mimetypes.guess_type(file_path) 6 | return mime_type and mime_type.startswith('image/') 7 | -------------------------------------------------------------------------------- /app/utils/config_directory.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | 5 | def get_ddnet_directory(): 6 | system = sys.platform 7 | directory = None 8 | 9 | if system.lower().startswith('win'): 10 | appdata = os.getenv('APPDATA', '') 11 | if os.path.isdir(os.path.join(appdata, 'DDNet')): 12 | directory = os.path.join(appdata, "DDNet") 13 | else: 14 | directory = os.path.join(appdata, "Teeworlds") 15 | elif system.lower().startswith('darwin'): 16 | if os.path.isdir(os.path.join(os.getenv('HOME'), 'Library/Application Support/DDNet')): 17 | directory = os.path.join(os.getenv("HOME"), "Library/Application Support/DDNet") 18 | else: 19 | directory = os.path.join(os.getenv("HOME"), "Library/Application Support/Teeworlds") 20 | else: 21 | data_home = os.getenv('XDG_DATA_HOME', os.path.join(os.getenv('HOME'), '.local/share')) 22 | if os.path.isdir(os.path.join(data_home, 'ddnet')): 23 | directory = os.path.join(data_home, "ddnet") 24 | else: 25 | directory = os.path.join(os.getenv("HOME"), ".teeworlds") 26 | 27 | if not os.path.isdir(directory): 28 | return './' 29 | 30 | if directory is None: 31 | return "./" 32 | else: 33 | return directory 34 | -------------------------------------------------------------------------------- /app/utils/draw_tee.py: -------------------------------------------------------------------------------- 1 | import io 2 | 3 | from PIL import ImageOps, Image 4 | from PyQt5.QtGui import QImage, QPixmap 5 | 6 | 7 | def crop_and_generate_image(img): 8 | """ 9 | 来自DDNetDiscordBot 10 | https://github.com/ddnet/ddnet-discordbot/blob/master/cogs/skindb.py#L115 11 | """ 12 | image = img 13 | 14 | image_body_shadow = image.crop((96, 0, 192, 96)) 15 | image_feet_shadow_back = image.crop((192, 64, 255, 96)) 16 | image_feet_shadow_front = image.crop((192, 64, 255, 96)) 17 | image_body = image.crop((0, 0, 96, 96)) 18 | image_feet_front = image.crop((192, 32, 255, 64)) 19 | image_feet_back = image.crop((192, 32, 255, 64)) 20 | 21 | # default eyes 22 | image_default_left_eye = image.crop((64, 96, 96, 128)) 23 | image_default_right_eye = image.crop((64, 96, 96, 128)) 24 | 25 | # evil eyes 26 | image_evil_l_eye = image.crop((96, 96, 128, 128)) 27 | image_evil_r_eye = image.crop((96, 96, 128, 128)) 28 | 29 | # hurt eyes 30 | image_hurt_l_eye = image.crop((128, 96, 160, 128)) 31 | image_hurt_r_eye = image.crop((128, 96, 160, 128)) 32 | 33 | # happy eyes 34 | image_happy_l_eye = image.crop((160, 96, 192, 128)) 35 | image_happy_r_eye = image.crop((160, 96, 192, 128)) 36 | 37 | # surprised eyes 38 | image_surprised_l_eye = image.crop((224, 96, 255, 128)) 39 | image_surprised_r_eye = image.crop((224, 96, 255, 128)) 40 | 41 | def resize_image(image, scale): 42 | width, height = image.size 43 | new_width = int(width * scale) 44 | new_height = int(height * scale) 45 | return image.resize((new_width, new_height)) 46 | 47 | image_body_resized = resize_image(image_body, 0.66) 48 | image_body_shadow_resized = resize_image(image_body_shadow, 0.66) 49 | 50 | image_left_eye = resize_image(image_default_left_eye, 0.8) 51 | image_right_eye = resize_image(image_default_right_eye, 0.8) 52 | image_right_eye_flipped = ImageOps.mirror(image_right_eye) 53 | 54 | image_evil_l_eye = resize_image(image_evil_l_eye, 0.8) 55 | image_evil_r_eye = resize_image(image_evil_r_eye, 0.8) 56 | image_evil_r_eye_flipped = ImageOps.mirror(image_evil_r_eye) 57 | 58 | image_hurt_l_eye = resize_image(image_hurt_l_eye, 0.8) 59 | image_hurt_r_eye = resize_image(image_hurt_r_eye, 0.8) 60 | image_hurt_r_eye_flipped = ImageOps.mirror(image_hurt_r_eye) 61 | 62 | image_happy_l_eye = resize_image(image_happy_l_eye, 0.8) 63 | image_happy_r_eye = resize_image(image_happy_r_eye, 0.8) 64 | image_happy_r_eye_flipped = ImageOps.mirror(image_happy_r_eye) 65 | 66 | image_surprised_l_eye = resize_image(image_surprised_l_eye, 0.8) 67 | image_surprised_r_eye = resize_image(image_surprised_r_eye, 0.8) 68 | image_surprised_r_eye_flipped = ImageOps.mirror(image_surprised_r_eye) 69 | 70 | def create_tee_image(image_left_eye, image_right_eye_flipped): 71 | tee = Image.new("RGBA", (96, 64), (0, 0, 0, 0)) 72 | 73 | tee.paste(image_body_shadow_resized, (16, 0)) 74 | tee.paste(image_feet_shadow_back.convert("RGB"), (8, 30), image_feet_shadow_back) 75 | tee.paste(image_feet_shadow_front.convert("RGB"), (24, 30), image_feet_shadow_front) 76 | tee.paste(image_feet_back.convert("RGB"), (8, 30), image_feet_back) 77 | tee.paste(image_body_resized.convert("RGB"), (16, 0), image_body_resized) 78 | tee.paste(image_feet_front.convert("RGB"), (24, 30), image_feet_front) 79 | 80 | tee.paste(image_left_eye.convert("RGB"), (39, 18), image_left_eye) 81 | tee.paste(image_right_eye_flipped.convert("RGB"), (47, 18), image_right_eye_flipped) 82 | 83 | return tee 84 | 85 | tee_images = { 86 | 'default': create_tee_image(image_left_eye, image_right_eye_flipped), 87 | 'evil': create_tee_image(image_evil_l_eye, image_evil_r_eye_flipped), 88 | 'hurt': create_tee_image(image_hurt_l_eye, image_hurt_r_eye_flipped), 89 | 'happy': create_tee_image(image_happy_l_eye, image_happy_r_eye_flipped), 90 | 'surprised': create_tee_image(image_surprised_l_eye, image_surprised_r_eye_flipped) 91 | } 92 | return tee_images 93 | 94 | 95 | def draw_tee(file: str) -> QImage: 96 | """绘制TEE""" 97 | image = Image.open(file) 98 | width, height = image.size 99 | 100 | # 检查图像是否为256x128,如果不是则进行缩放 101 | if width != 256 or height != 128: 102 | canvas = Image.new('RGBA', (256, 128)) 103 | image = image.resize((256, 128)) 104 | canvas.paste(image, (0, 0)) 105 | image = canvas 106 | 107 | try: 108 | processed_images = crop_and_generate_image(image) 109 | except: 110 | return QImage() 111 | 112 | final_image = Image.new('RGBA', (96, 96), ) 113 | final_image.paste(processed_images['default'], (0, 0)) 114 | 115 | byte_io = io.BytesIO() 116 | final_image.save(byte_io, format='PNG') 117 | byte_data = byte_io.getvalue() 118 | 119 | qimage = QImage.fromData(byte_data) 120 | 121 | return QPixmap.fromImage(qimage) -------------------------------------------------------------------------------- /app/utils/image_alpha_check.py: -------------------------------------------------------------------------------- 1 | from PIL import Image 2 | 3 | def has_alpha_channel(image_path: str) -> bool: 4 | try: 5 | with Image.open(image_path) as img: 6 | return img.mode in ("RGBA", "LA") 7 | except: 8 | return False -------------------------------------------------------------------------------- /app/utils/network.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from PyQt5.QtCore import QThread, pyqtSignal 3 | from PyQt5.QtGui import QPixmap 4 | 5 | 6 | class ImageLoader(QThread): 7 | finished = pyqtSignal(QPixmap) 8 | 9 | def __init__(self, url): 10 | super().__init__() 11 | self.url = url 12 | 13 | def run(self): 14 | try: 15 | response = requests.get(url=self.url) 16 | image_data = response.content 17 | 18 | pixmap = QPixmap() 19 | pixmap.loadFromData(image_data) 20 | except: 21 | pixmap = QPixmap() 22 | self.finished.emit(pixmap) 23 | 24 | 25 | class JsonLoader(QThread): 26 | finished = pyqtSignal(dict) 27 | 28 | def __init__(self, url): 29 | super().__init__() 30 | self.url = url 31 | 32 | def run(self): 33 | try: 34 | response = requests.get(url=self.url).json() 35 | except: 36 | response = {} 37 | 38 | self.finished.emit(response) 39 | 40 | 41 | class HTMLoader(QThread): 42 | finished = pyqtSignal(str) 43 | 44 | def __init__(self, url): 45 | super().__init__() 46 | self.url = url 47 | 48 | def run(self): 49 | try: 50 | response = requests.get(url=self.url).text 51 | except: 52 | response = '' 53 | 54 | self.finished.emit(response) 55 | -------------------------------------------------------------------------------- /app/utils/player_name.py: -------------------------------------------------------------------------------- 1 | from app.globals import GlobalsVal 2 | 3 | def name_length_limit(name): 4 | while len(name.encode('utf-8')) > 15: 5 | name = name[:-1] 6 | return name 7 | 8 | 9 | def get_player_name(): 10 | player_name = GlobalsVal.ddnet_setting_config.get("player_name", None) 11 | if player_name is None: 12 | return GlobalsVal.ddnet_setting_config.get("steam_name", "nameless tee") 13 | else: 14 | return player_name 15 | 16 | def get_dummy_name(): 17 | dummy_name = GlobalsVal.ddnet_setting_config.get("dummy_name", None) 18 | if dummy_name is None: 19 | return name_length_limit("[D] " + GlobalsVal.ddnet_setting_config.get("steam_name", "nameless tee")) 20 | else: 21 | return dummy_name -------------------------------------------------------------------------------- /app/utils/points_rank.py: -------------------------------------------------------------------------------- 1 | def exponential_cdf(x): 2 | return 1 - 2 ** -x 3 | 4 | def points_rank(current_points: int, total_points: int, online_time: int, global_rank: int): 5 | grade_thresholds = { 6 | "S+": 1.00, 7 | "S": 0.95, 8 | "A+": 0.90, 9 | "A": 0.85, 10 | "A-": 0.80, 11 | "B+": 0.75, 12 | "B": 0.70, 13 | "B-": 0.65, 14 | "C+": 0.60, 15 | "C": 0.55, 16 | "C-": 0.50, 17 | "D+": 0.45, 18 | "D": 0.40, 19 | "D-": 0.35, 20 | "E+": 0.30, 21 | "E": 0.25, 22 | "E-": 0.20, 23 | "F+": 0.15, 24 | "F": 0.10, 25 | "F-": 0.05, 26 | "G": 0.00 27 | } 28 | 29 | POINTS_WEIGHT = 4 30 | ONLINE_TIME_MEDIAN = 500 31 | ONLINE_TIME_WEIGHT = 2 32 | GLOBAL_RANK_MEDIAN = 1000 33 | GLOBAL_RANK_WEIGHT = 3 34 | 35 | TOTAL_WEIGHT = POINTS_WEIGHT + ONLINE_TIME_WEIGHT + GLOBAL_RANK_WEIGHT 36 | 37 | score_percentage = (POINTS_WEIGHT * (current_points / total_points) + 38 | ONLINE_TIME_WEIGHT * exponential_cdf(online_time / ONLINE_TIME_MEDIAN) + 39 | GLOBAL_RANK_WEIGHT * (1 - exponential_cdf(global_rank / GLOBAL_RANK_MEDIAN))) / TOTAL_WEIGHT 40 | 41 | for grade, threshold in sorted(grade_thresholds.items(), key=lambda x: -x[1]): 42 | if score_percentage >= threshold: 43 | return grade 44 | 45 | return "G" 46 | -------------------------------------------------------------------------------- /app/view/cfg_interface.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | from functools import partial 4 | 5 | from PyQt5.QtCore import Qt 6 | from PyQt5.QtWidgets import QWidget, QHBoxLayout, QTableWidgetItem, QVBoxLayout, QHeaderView, QLabel, QFileDialog 7 | from qfluentwidgets import TableWidget, CommandBar, Action, FluentIcon, InfoBar, InfoBarPosition, MessageBox, \ 8 | TitleLabel, MessageBoxBase, SubtitleLabel, Dialog, setFont, SmoothMode 9 | 10 | from app.config import cfg 11 | from app.globals import GlobalsVal 12 | 13 | 14 | class CFGSelectMessageBox(MessageBoxBase): 15 | """ Custom message box """ 16 | def __init__(self, parent=None): 17 | super().__init__(parent) 18 | 19 | self.selected_files = None 20 | self.titleLabel = SubtitleLabel(self.tr('选择CFG文件')) 21 | self.label = QLabel(self.tr("拖拽文件到此处或点击选择文件"), self) 22 | self.label.setAlignment(Qt.AlignCenter) 23 | self.label.setStyleSheet("QLabel { border: 2px dashed #aaa; }") 24 | 25 | self.setAcceptDrops(True) 26 | 27 | self.viewLayout.addWidget(self.titleLabel) 28 | self.viewLayout.addWidget(self.label) 29 | 30 | self.label.setMinimumWidth(300) 31 | self.label.setMinimumHeight(100) 32 | 33 | self.label.mousePressEvent = self.select_file 34 | 35 | def dragEnterEvent(self, event): 36 | if event.mimeData().hasUrls(): 37 | event.acceptProposedAction() 38 | 39 | def dropEvent(self, event): 40 | files = [] 41 | for url in event.mimeData().urls(): 42 | files.append(url.toLocalFile()) 43 | 44 | self.selected_files = files 45 | self.label.setText("\n".join(files)) 46 | 47 | def select_file(self, event): 48 | options = QFileDialog.Options() 49 | options |= QFileDialog.ReadOnly 50 | files, _ = QFileDialog.getOpenFileNames(self, self.tr("选择CFG文件"), "", "CFG Files (*.cfg);;All Files (*)", 51 | options=options) 52 | 53 | if files: 54 | self.selected_files = files 55 | self.label.setText("\n".join(files)) 56 | 57 | def get_selected_files(self): 58 | return self.selected_files 59 | 60 | 61 | class CFGInterface(QWidget): 62 | def __init__(self, parent=None): 63 | super().__init__(parent=parent) 64 | self.setObjectName("CFGInterface") 65 | 66 | if not GlobalsVal.ddnet_folder_status: 67 | self.label = SubtitleLabel(self.tr("我们的程序无法自动找到DDNet配置目录\n请手动到设置中指定DDNet配置目录"), self) 68 | self.hBoxLayout = QHBoxLayout(self) 69 | 70 | setFont(self.label, 24) 71 | self.label.setAlignment(Qt.AlignCenter) 72 | self.hBoxLayout.addWidget(self.label, 1, Qt.AlignCenter) 73 | return 74 | 75 | self.vBoxLayout = QVBoxLayout(self) 76 | self.commandBar = CommandBar(self) 77 | self.table = TableWidget(self) 78 | 79 | self.vBoxLayout.addWidget(TitleLabel(self.tr('CFG管理'), self)) 80 | self.setLayout(self.vBoxLayout) 81 | 82 | self.commandBar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) 83 | 84 | self.addButton(FluentIcon.ADD, self.tr('添加'), '添加'), 85 | self.addButton(FluentIcon.DELETE, self.tr('删除'), '删除'), 86 | self.addButton(FluentIcon.SYNC, self.tr('刷新'), '刷新'), 87 | 88 | self.table.scrollDelagate.verticalSmoothScroll.setSmoothMode(SmoothMode.NO_SMOOTH) 89 | self.table.setBorderRadius(5) 90 | self.table.setWordWrap(False) 91 | self.table.setColumnCount(2) 92 | 93 | cfg_list = [file for file in os.listdir(GlobalsVal.ddnet_folder) if file.endswith('.cfg')] 94 | self.table.setRowCount(len(cfg_list)) 95 | 96 | for i, server_link in enumerate(cfg_list, start=0): 97 | self.table.setItem(i, 0, QTableWidgetItem(server_link)) 98 | """ 99 | if server_link.endswith("disable.cfg"): 100 | self.table.setItem(i, 0, QTableWidgetItem(server_link)) 101 | self.table.setItem(i, 1, QTableWidgetItem("禁用")) 102 | else: 103 | self.table.setItem(i, 0, QTableWidgetItem(server_link)) 104 | self.table.setItem(i, 1, QTableWidgetItem("启用")) 105 | """ 106 | 107 | self.table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) 108 | self.table.verticalHeader().hide() 109 | self.table.horizontalHeader().hide() 110 | 111 | self.vBoxLayout.addWidget(self.commandBar) 112 | self.vBoxLayout.addWidget(self.table) 113 | 114 | def addButton(self, icon, text, text_type): 115 | action = Action(icon, text, self) 116 | action.triggered.connect(partial(self.Button_clicked, text_type)) 117 | self.commandBar.addAction(action) 118 | 119 | def Button_clicked(self, text): 120 | if text == "添加": 121 | w = CFGSelectMessageBox(self) 122 | if w.exec(): 123 | files = w.get_selected_files() 124 | if files is None: 125 | InfoBar.error( 126 | title=self.tr('错误'), 127 | content=self.tr("您没有选择任何文件"), 128 | orient=Qt.Horizontal, 129 | isClosable=True, 130 | position=InfoBarPosition.BOTTOM_RIGHT, 131 | duration=2000, 132 | parent=GlobalsVal.main_window 133 | ) 134 | else: 135 | errors = 0 136 | cover = 0 137 | for i in files: 138 | if i.split("/")[-1] in [file for file in os.listdir(GlobalsVal.ddnet_folder) if file.endswith('.cfg')]: 139 | cover += 1 140 | 141 | try: 142 | shutil.copy(i, GlobalsVal.ddnet_folder) 143 | except Exception as e: 144 | InfoBar.error( 145 | title=self.tr('错误'), 146 | content=self.tr("文件 {} 复制失败\n原因:{}").format(i, e), 147 | orient=Qt.Horizontal, 148 | isClosable=True, 149 | position=InfoBarPosition.BOTTOM_RIGHT, 150 | duration=-1, 151 | parent=GlobalsVal.main_window 152 | ) 153 | errors += 1 154 | 155 | InfoBar.success( 156 | title='成功', 157 | content=self.tr("文件复制已完成\n共复制了 {} 个文件,{} 个文件被覆盖,{} 个文件失败").format(len(files), cover, errors), 158 | orient=Qt.Horizontal, 159 | isClosable=True, 160 | position=InfoBarPosition.BOTTOM_RIGHT, 161 | duration=2000, 162 | parent=GlobalsVal.main_window 163 | ) 164 | self.Button_clicked("刷新") 165 | elif text == "删除": 166 | selected_items = self.table.selectedItems() 167 | if selected_items == []: 168 | InfoBar.warning( 169 | title=self.tr('警告'), 170 | content=self.tr("您没有选择任何东西"), 171 | orient=Qt.Horizontal, 172 | isClosable=True, 173 | position=InfoBarPosition.BOTTOM_RIGHT, 174 | duration=2000, 175 | parent=GlobalsVal.main_window 176 | ) 177 | return 178 | 179 | rows_to_delete = {} 180 | for i in selected_items: 181 | if i.text() in ["启用", "禁用"]: 182 | continue 183 | rows_to_delete[i.row()] = i.text() 184 | 185 | delete_text = "" 186 | for i in list(set(i.text() for i in selected_items)): 187 | if i in ["启用", "禁用"]: 188 | continue 189 | delete_text += f"{i}\n" 190 | 191 | w = MessageBox(self.tr("警告"), self.tr("此操作将会从磁盘中永久删除下列文件,不可恢复:\n{}").format(delete_text), self) 192 | delete = 0 193 | if w.exec(): 194 | for i, a in enumerate(rows_to_delete): 195 | self.table.removeRow(a) 196 | os.remove(f"{GlobalsVal.ddnet_folder}/{rows_to_delete[a]}") 197 | delete += 1 198 | 199 | InfoBar.warning( 200 | title=self.tr('成功'), 201 | content=self.tr("共删除 {} 个文件,{} 个文件删除失败").format(delete, len(rows_to_delete) - delete), 202 | orient=Qt.Horizontal, 203 | isClosable=True, 204 | position=InfoBarPosition.BOTTOM_RIGHT, 205 | duration=2000, 206 | parent=GlobalsVal.main_window 207 | ) 208 | 209 | elif text == "刷新": 210 | self.table.clear() 211 | self.table.setRowCount(0) 212 | self.table.clearSelection() 213 | 214 | cfg_list = [file for file in os.listdir(GlobalsVal.ddnet_folder) if file.endswith('.cfg')] 215 | self.table.setRowCount(len(cfg_list)) 216 | 217 | for i, server_link in enumerate(cfg_list, start=0): 218 | self.table.setItem(i, 0, QTableWidgetItem(server_link)) 219 | """ 220 | if server_link.endswith("disable.cfg"): 221 | self.table.setItem(i, 0, QTableWidgetItem(server_link)) 222 | self.table.setItem(i, 1, QTableWidgetItem("禁用")) 223 | else: 224 | self.table.setItem(i, 0, QTableWidgetItem(server_link)) 225 | self.table.setItem(i, 1, QTableWidgetItem("启用")) 226 | """ 227 | 228 | InfoBar.success( 229 | title=self.tr('成功'), 230 | content=self.tr("已重新加载本地资源"), 231 | orient=Qt.Horizontal, 232 | isClosable=True, 233 | position=InfoBarPosition.BOTTOM_RIGHT, 234 | duration=2000, 235 | parent=GlobalsVal.main_window 236 | ) 237 | 238 | 239 | 240 | -------------------------------------------------------------------------------- /app/view/home_interface.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import requests 3 | 4 | from app.globals import GlobalsVal 5 | from app.config import cfg, base_path 6 | from PyQt5.QtCore import Qt, QThread, pyqtSignal 7 | from PyQt5.QtWidgets import QVBoxLayout, QWidget, QHBoxLayout, QSpacerItem, QSizePolicy, QLabel, \ 8 | QStackedWidget, QTableWidgetItem, QHeaderView, QAbstractItemView, QFrame, QTableWidget 9 | from qfluentwidgets import ImageLabel, CardWidget, SubtitleLabel, BodyLabel, HeaderCardWidget, InfoBar, InfoBarPosition, \ 10 | CaptionLabel, SingleDirectionScrollArea, ToolTipFilter, ToolTipPosition, Pivot, TableWidget, SmoothMode, \ 11 | ComboBox, StrongBodyLabel, SearchLineEdit 12 | 13 | from app.utils.network import ImageLoader 14 | from app.utils.player_name import get_player_name, get_dummy_name 15 | 16 | 17 | class TEEDataLoader(QThread): 18 | finished = pyqtSignal(dict) 19 | 20 | def __init__(self, name): 21 | super().__init__() 22 | self.name = name 23 | 24 | def run(self): 25 | try: 26 | response = requests.get('https://ddnet.org/players/?json2={}'.format(self.name)) 27 | if response.json() == {}: 28 | self.finished.emit({"error": "NoData"}) 29 | else: 30 | self.finished.emit(response.json()) 31 | except: 32 | self.finished.emit({"error": "InternetError"}) 33 | 34 | 35 | class CheckUpdate(QThread): 36 | finished = pyqtSignal(list) 37 | 38 | def __init__(self, parent=None): 39 | super().__init__(parent) 40 | 41 | def run(self): 42 | if GlobalsVal.ddnet_info is None: 43 | self.finished.emit([]) 44 | return 45 | try: 46 | response = requests.get("https://update.ddnet.org/update.json").json() 47 | self.finished.emit(response) 48 | except: 49 | self.finished.emit([]) 50 | 51 | 52 | class TEECard(CardWidget): 53 | ref_status = True 54 | 55 | def __init__(self, name: str, tee_info_ready=None, parent=None): 56 | super().__init__(parent) 57 | self.tee_info_ready = tee_info_ready 58 | 59 | self.setToolTip(self.tr('单击刷新数据')) 60 | self.setToolTipDuration(1000) 61 | self.installEventFilter(ToolTipFilter(self, showDelay=300, position=ToolTipPosition.BOTTOM)) 62 | 63 | self.name = name 64 | self.hBoxLayout = QHBoxLayout() 65 | self.vBoxLayout = QVBoxLayout() 66 | 67 | self.iconWidget = ImageLabel(base_path + '/resource/logo.png', self) 68 | self.iconWidget.scaledToHeight(120) 69 | 70 | self.setLayout(self.hBoxLayout) 71 | self.hBoxLayout.setContentsMargins(0, 15, 0, 0) 72 | 73 | self.hBoxLayout.addWidget(self.iconWidget, 0, Qt.AlignLeft) 74 | 75 | self.labels = [ 76 | SubtitleLabel(name, self), 77 | BodyLabel(self.tr('全球排名:加载中...\n' 78 | '游戏分数:加载中...\n' 79 | '游玩时长:加载中...\n' 80 | '最后完成:加载中...\n' 81 | '入坑时间:加载中...'), self), 82 | ] 83 | 84 | for label in self.labels: 85 | self.vBoxLayout.addWidget(label, 0, Qt.AlignLeft | Qt.AlignTop) 86 | 87 | self.spacer = QSpacerItem(0, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) 88 | self.vBoxLayout.addItem(self.spacer) 89 | 90 | self.hBoxLayout.addLayout(self.vBoxLayout) 91 | 92 | self.image_loader = ImageLoader('https://xc.null.red:8043/api/ddnet/draw_player_skin?name={}'.format(name)) 93 | self.image_loader.finished.connect(self.on_image_loaded) 94 | self.image_loader.start() 95 | 96 | self.data_loader = TEEDataLoader(name) 97 | self.data_loader.finished.connect(self.on_data_loaded) 98 | self.data_loader.start() 99 | 100 | self.clicked.connect(self.__on_clicked) 101 | 102 | def __on_clicked(self): 103 | if self.ref_status: 104 | return 105 | else: 106 | self.ref_status = True 107 | 108 | self.labels[1].setText(self.tr('全球排名:加载中...\n' 109 | '游戏分数:加载中...\n' 110 | '游玩时长:加载中...\n' 111 | '最后完成:加载中...\n' 112 | '入坑时间:加载中...')) 113 | 114 | self.image_loader = ImageLoader('https://xc.null.red:8043/api/ddnet/draw_player_skin?name={}'.format(self.name)) 115 | self.image_loader.finished.connect(self.on_image_loaded) 116 | self.image_loader.start() 117 | 118 | self.data_loader = TEEDataLoader(self.name) 119 | self.data_loader.finished.connect(self.on_data_loaded) 120 | self.data_loader.start() 121 | 122 | def on_image_loaded(self, pixmap): 123 | self.iconWidget.setPixmap(pixmap) 124 | self.iconWidget.scaledToHeight(120) 125 | 126 | def on_data_loaded(self, json_data: dict): 127 | if self.tee_info_ready is not None: 128 | self.tee_info_ready.emit(json_data) 129 | 130 | if 'error' in json_data: 131 | if json_data['error'] == "NoData": 132 | self.labels[1].setText(self.tr('全球排名:NO.查无此人\n' 133 | '游戏分数:查无此人 分\n' 134 | '游玩时长:查无此人 小时\n' 135 | '最后完成:查无此人\n' 136 | '入坑时间:查无此人')) 137 | else: 138 | self.labels[1].setText(self.tr('全球排名:NO.数据获取失败\n' 139 | '游戏分数:数据获取失败 分\n' 140 | '游玩时长:数据获取失败 小时\n' 141 | '最后完成:数据获取失败\n' 142 | '入坑时间:数据获取失败')) 143 | return 144 | use_time = 0 145 | for time in json_data['activity']: 146 | use_time = use_time + time['hours_played'] 147 | 148 | self.labels[1].setText(self.tr('全球排名:NO.{}\n' 149 | '游戏分数:{}/{} 分\n' 150 | '游玩时长:{} 小时\n' 151 | '最后完成:{}\n' 152 | '入坑时间:{}').format(json_data["points"]["rank"], json_data["points"]["points"], 153 | json_data["points"]["total"], use_time, 154 | json_data["last_finishes"][0]["map"], 155 | datetime.datetime.fromtimestamp(json_data["first_finish"]["timestamp"]))) 156 | 157 | self.ref_status = False 158 | 159 | 160 | class CreateTitleContent(QFrame): 161 | def __init__(self, title, content, parent=None): 162 | super().__init__(parent) 163 | self.vBoxLayout = QVBoxLayout(self) 164 | 165 | self.title_label = StrongBodyLabel(title) 166 | self.content_label = CaptionLabel(content) 167 | 168 | self.vBoxLayout.addWidget(self.title_label) 169 | self.vBoxLayout.addWidget(self.content_label) 170 | 171 | 172 | class MapStatus(QWidget): 173 | tee_data = pyqtSignal(dict) 174 | 175 | def __init__(self, parent=None): 176 | super().__init__(parent) 177 | self.setMinimumHeight(200) 178 | 179 | self.vBoxLayout = QVBoxLayout(self) 180 | self.hBoxLayout = QHBoxLayout() 181 | 182 | self.table = TableWidget() 183 | self.pointsWidget = CreateTitleContent(self.tr("分数 (共 {} 点)").format('NaN'), self.tr("第 {} 名,共 {} 分").format('NaN', 'NaN')) 184 | self.mapsWidget = CreateTitleContent(self.tr("地图 (共 {} 张)").format('NaN'), self.tr("已完成 {} 张,剩余 {} 张未完成").format('NaN', 'NaN')) 185 | self.teamRankWidget = CreateTitleContent(self.tr("队伍排名"), "NaN") 186 | self.rankWidget = CreateTitleContent(self.tr("全球排名"), "NaN") 187 | 188 | self.table.scrollDelagate.verticalSmoothScroll.setSmoothMode(SmoothMode.NO_SMOOTH) 189 | self.table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents) 190 | self.table.horizontalHeader().setStretchLastSection(True) 191 | self.table.setBorderRadius(5) 192 | self.table.setWordWrap(False) 193 | self.table.setColumnCount(7) 194 | self.table.setSelectionMode(QAbstractItemView.NoSelection) 195 | self.table.verticalHeader().hide() 196 | self.table.setEditTriggers(QTableWidget.NoEditTriggers) 197 | self.table.setSortingEnabled(True) 198 | self.table.setHorizontalHeaderLabels([ 199 | self.tr("地图"), 200 | self.tr("分数"), 201 | self.tr("队伍排名"), 202 | self.tr("全球排名"), 203 | self.tr("用时"), 204 | self.tr("通关次数"), 205 | self.tr("首次完成于") 206 | ]) 207 | self.vBoxLayout.setContentsMargins(0, 0, 0, 0) 208 | 209 | self.hBoxLayout.addWidget(self.pointsWidget) 210 | self.hBoxLayout.addWidget(self.mapsWidget) 211 | self.hBoxLayout.addWidget(self.teamRankWidget) 212 | self.hBoxLayout.addWidget(self.rankWidget) 213 | self.vBoxLayout.addLayout(self.hBoxLayout) 214 | self.vBoxLayout.addWidget(self.table) 215 | 216 | self.tee_data.connect(self.__on_data_loader) 217 | 218 | def search(self, text=None): 219 | if text is None: 220 | for i in range(self.table.rowCount()): 221 | self.table.setRowHidden(i, False) 222 | else: 223 | for i in range(self.table.rowCount()): 224 | match = False 225 | for j in range(self.table.columnCount()): 226 | item = self.table.item(i, j) 227 | if text.lower() in item.text().lower(): 228 | match = True 229 | break 230 | self.table.setRowHidden(i, not match) 231 | 232 | def __on_data_loader(self, data): 233 | map_count = len(data['maps']) 234 | 235 | self.table.setRowCount(0) 236 | 237 | self.pointsWidget.title_label.setText(self.tr("分数 (共 {} 点)").format(data['points']['total'])) 238 | if data['points']['rank'] is None: 239 | self.pointsWidget.content_label.setText(self.tr("未排名")) 240 | else: 241 | self.pointsWidget.content_label.setText(self.tr("第 {} 名,共 {} 分").format(data['points'].get('rank', '0'), data['points'].get('points', '0'))) 242 | 243 | self.mapsWidget.title_label.setText(self.tr("地图 (共 {} 张)").format(map_count)) 244 | 245 | self.team_rank = data.get('team_rank', {}).get('rank', self.tr("未排名")) 246 | self.teamRankWidget.content_label.setText(self.tr("未排名") if self.team_rank is None else str(self.team_rank)) 247 | 248 | self.rank_text = data.get('rank', {}).get('rank', self.tr("未排名")) 249 | self.rankWidget.content_label.setText(self.tr("未排名") if self.rank_text is None else str(self.rank_text)) 250 | 251 | data = data['maps'] 252 | finish_map = 0 253 | 254 | for map_name in data: 255 | current_row = self.table.rowCount() 256 | self.table.insertRow(current_row) 257 | 258 | first_finish = data[map_name].get('first_finish', None) 259 | if first_finish is not None: 260 | first_finish = datetime.datetime.fromtimestamp(first_finish) 261 | finish_map += 1 262 | else: 263 | first_finish = '' 264 | 265 | team_rank = data[map_name].get('team_rank', None) 266 | if team_rank is not None: 267 | team_rank = team_rank 268 | else: 269 | team_rank = '' 270 | 271 | finish_time = data[map_name].get('time', None) 272 | if finish_time is not None: 273 | finish_time = datetime.timedelta(seconds=int(finish_time)) 274 | else: 275 | finish_time = '' 276 | 277 | add_item = [ 278 | map_name, 279 | data[map_name].get('points', ''), 280 | team_rank, 281 | data[map_name].get('rank', ''), 282 | finish_time, 283 | data[map_name]['finishes'], 284 | first_finish 285 | ] 286 | 287 | for column, value in enumerate(add_item): 288 | item = QTableWidgetItem(str(value)) 289 | self.table.setItem(current_row, column, item) 290 | 291 | self.mapsWidget.content_label.setText(self.tr("已完成 {} 张,剩余 {} 张未完成").format(finish_map, map_count - finish_map)) 292 | 293 | class TEEInfo(QWidget): 294 | tee_data = pyqtSignal(dict) 295 | 296 | def __init__(self, parent=None): 297 | super().__init__(parent) 298 | self.vBoxLayout = QVBoxLayout(self) 299 | self.hBoxLayout = QHBoxLayout() 300 | self.comboBox = ComboBox() 301 | self.stackedWidget = QStackedWidget() 302 | self.searchLine = SearchLineEdit() 303 | 304 | self.stackedWidget.setContentsMargins(0, 0, 0, 0) 305 | 306 | self.NoviceWidget = MapStatus() 307 | self.ModerateWidget = MapStatus() 308 | self.BrutalWidget = MapStatus() 309 | self.InsaneWidget = MapStatus() 310 | self.DummyWidget = MapStatus() 311 | self.DDmaXEasyWidget = MapStatus() 312 | self.DDmaXNextWidget = MapStatus() 313 | self.DDmaXProWidget = MapStatus() 314 | self.DDmaXNutWidget = MapStatus() 315 | self.OldschoolWidget = MapStatus() 316 | self.SoloWidget = MapStatus() 317 | self.RaceWidget = MapStatus() 318 | self.FunWidget = MapStatus() 319 | 320 | self.addSubInterface(self.NoviceWidget, self.tr("Novice 简单")) 321 | self.addSubInterface(self.ModerateWidget, self.tr("Moderate 中阶")) 322 | self.addSubInterface(self.BrutalWidget, self.tr("Brutal 高阶")) 323 | self.addSubInterface(self.InsaneWidget, self.tr("Insane 疯狂")) 324 | self.addSubInterface(self.DummyWidget, self.tr("Dummy 分身")) 325 | self.addSubInterface(self.DDmaXEasyWidget, self.tr("DDmaX.Easy 古典.简单")) 326 | self.addSubInterface(self.DDmaXNextWidget, self.tr("DDmaX.Next 古典.中阶")) 327 | self.addSubInterface(self.DDmaXProWidget, self.tr("DDmaX.Pro 古典.高阶")) 328 | self.addSubInterface(self.DDmaXNutWidget, self.tr("DDmaX.Nut 古典.坚果")) 329 | self.addSubInterface(self.OldschoolWidget, self.tr("Oldschool 传统")) 330 | self.addSubInterface(self.SoloWidget, self.tr("Solo 单人")) 331 | self.addSubInterface(self.RaceWidget, self.tr("Race 竞速")) 332 | self.addSubInterface(self.FunWidget, self.tr("Fun 娱乐")) 333 | 334 | self.stackedWidget.setCurrentWidget(self.NoviceWidget) 335 | self.comboBox.currentIndexChanged.connect(lambda k: self.stackedWidget.setCurrentIndex(k)) 336 | self.searchLine.setPlaceholderText(self.tr("搜点什么...")) 337 | 338 | self.hBoxLayout.addWidget(self.comboBox) 339 | self.hBoxLayout.addWidget(self.searchLine) 340 | self.vBoxLayout.addLayout(self.hBoxLayout) 341 | self.vBoxLayout.addWidget(self.stackedWidget) 342 | 343 | self.tee_data.connect(self.__on_data_loader) 344 | self.searchLine.returnPressed.connect(self.searchLine.search) 345 | self.searchLine.searchSignal.connect(lambda text=None:self.stackedWidget.currentWidget().search(text)) 346 | self.searchLine.clearSignal.connect(lambda text=None:self.stackedWidget.currentWidget().search(text)) 347 | 348 | def addSubInterface(self, widget: QLabel, text): 349 | self.stackedWidget.addWidget(widget) 350 | self.comboBox.addItem(text) 351 | 352 | def __on_data_loader(self, data): 353 | if data == {} or not "types" in data: 354 | return 355 | 356 | self.NoviceWidget.tee_data.emit(data['types']['Novice']) 357 | self.ModerateWidget.tee_data.emit(data['types']['Moderate']) 358 | self.BrutalWidget.tee_data.emit(data['types']['Brutal']) 359 | self.InsaneWidget.tee_data.emit(data['types']['Insane']) 360 | self.DummyWidget.tee_data.emit(data['types']['Dummy']) 361 | self.DDmaXEasyWidget.tee_data.emit(data['types']['DDmaX.Easy']) 362 | self.DDmaXNextWidget.tee_data.emit(data['types']['DDmaX.Next']) 363 | self.DDmaXProWidget.tee_data.emit(data['types']['DDmaX.Pro']) 364 | self.DDmaXNutWidget.tee_data.emit(data['types']['DDmaX.Nut']) 365 | self.OldschoolWidget.tee_data.emit(data['types']['Oldschool']) 366 | self.SoloWidget.tee_data.emit(data['types']['Solo']) 367 | self.RaceWidget.tee_data.emit(data['types']['Race']) 368 | self.FunWidget.tee_data.emit(data['types']['Fun']) 369 | 370 | 371 | class TEEInfoList(HeaderCardWidget): 372 | title_player_name = pyqtSignal(dict) 373 | title_dummy_name = pyqtSignal(dict) 374 | 375 | def __init__(self, on_data: bool=False, parent=None): 376 | super().__init__(parent) 377 | self.viewLayout.setContentsMargins(0, 0, 0, 0) 378 | 379 | self.headerLabel.deleteLater() 380 | self.headerLabel = Pivot(self) 381 | self.headerLabel.setStyleSheet("font-size: 15px;") 382 | 383 | self.containerWidget = QWidget() 384 | self.vBoxLayout = QVBoxLayout(self.containerWidget) 385 | self.vBoxLayout.setContentsMargins(0, 0, 0, 0) 386 | 387 | self.scrollArea = SingleDirectionScrollArea() 388 | self.scrollArea.setWidgetResizable(True) 389 | self.scrollArea.setWidget(self.containerWidget) 390 | self.scrollArea.enableTransparentBackground() 391 | 392 | self.viewLayout.addWidget(self.scrollArea) 393 | 394 | self.stackedWidget = QStackedWidget(self) 395 | self.stackedWidget.setContentsMargins(0, 0, 0, 0) 396 | 397 | self.homePlayerInterface = TEEInfo(self) 398 | self.homeDummyInterface = TEEInfo(self) 399 | 400 | if on_data: 401 | self.addSubInterface(self.homePlayerInterface, 'homePlayerInterface', "NaN") 402 | self.addSubInterface(self.homeDummyInterface, 'homeDummyInterface', "NaN") 403 | else: 404 | self.addSubInterface(self.homePlayerInterface, 'homePlayerInterface', self.tr('本体')) 405 | self.addSubInterface(self.homeDummyInterface, 'homeDummyInterface', self.tr('分身')) 406 | 407 | self.stackedWidget.setCurrentWidget(self.homePlayerInterface) 408 | self.headerLabel.setCurrentItem(self.homePlayerInterface.objectName()) 409 | self.headerLabel.currentItemChanged.connect(lambda k: self.stackedWidget.setCurrentWidget(self.findChild(QWidget, k))) 410 | self.vBoxLayout.addWidget(self.stackedWidget) 411 | 412 | self.title_player_name.connect(self.__changePlayerTitle) 413 | self.title_dummy_name.connect(self.__changeDummyTitle) 414 | 415 | def __changePlayerTitle(self, data): 416 | self.headerLabel.items['homePlayerInterface'].setText(data.get("player", "NaN")) 417 | self.homePlayerInterface.tee_data.emit(data) 418 | 419 | def __changeDummyTitle(self, data): 420 | self.headerLabel.items['homeDummyInterface'].setText(data.get("player", "NaN")) 421 | self.homeDummyInterface.tee_data.emit(data) 422 | 423 | def addSubInterface(self, widget: QLabel, objectName, text): 424 | widget.setObjectName(objectName) 425 | self.stackedWidget.addWidget(widget) 426 | self.headerLabel.addItem(routeKey=objectName, text=text) 427 | 428 | 429 | class HomeInterface(QWidget): 430 | def __init__(self, parent=None): 431 | super().__init__(parent=parent) 432 | self.setObjectName("HomeInterface") 433 | 434 | self.vBoxLayout = QVBoxLayout(self) 435 | self.hBoxLayout = QHBoxLayout() 436 | 437 | self.teeinfolist = TEEInfoList() 438 | 439 | # Add Layout&widget 440 | self.vBoxLayout.addLayout(self.hBoxLayout, Qt.AlignTop) 441 | self.TEECARD(get_player_name(), 442 | get_dummy_name()) 443 | self.vBoxLayout.addWidget(self.teeinfolist, Qt.AlignCenter) 444 | 445 | if cfg.get(cfg.DDNetCheckUpdate): 446 | self.check_update = CheckUpdate() 447 | self.check_update.finished.connect(self.on_check_update_loaded) 448 | self.check_update.start() 449 | 450 | def on_check_update_loaded(self, json_data: list): 451 | if json_data == []: 452 | InfoBar.warning( 453 | title=self.tr('DDNet 版本检测'), 454 | content=self.tr("无法连接到DDNet官网"), 455 | orient=Qt.Horizontal, 456 | isClosable=True, 457 | position=InfoBarPosition.BOTTOM_RIGHT, 458 | duration=-1, 459 | parent=GlobalsVal.main_window 460 | ) 461 | return 462 | if GlobalsVal.ddnet_info['version'] != json_data[0]["version"]: 463 | InfoBar.warning( 464 | title=self.tr('DDNet 版本检测'), 465 | content=self.tr("您当前的DDNet版本为 {} 最新版本为 {} 请及时更新").format( 466 | GlobalsVal.ddnet_info['version'], json_data[0]["version"]), 467 | orient=Qt.Horizontal, 468 | isClosable=True, 469 | position=InfoBarPosition.BOTTOM_RIGHT, 470 | duration=-1, 471 | parent=GlobalsVal.main_window 472 | ) 473 | 474 | def TEECARD(self, player_name: str, dummy_name: str): 475 | for i in reversed(range(self.hBoxLayout.count())): 476 | widget = self.hBoxLayout.itemAt(i).widget() 477 | self.hBoxLayout.removeWidget(widget) 478 | widget.deleteLater() 479 | self.hBoxLayout.addWidget(TEECard(player_name, self.teeinfolist.homePlayerInterface.tee_data), alignment=Qt.AlignTop) 480 | self.hBoxLayout.addWidget(TEECard(dummy_name, self.teeinfolist.homeDummyInterface.tee_data), alignment=Qt.AlignTop) -------------------------------------------------------------------------------- /app/view/main_interface.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import re 4 | 5 | from PyQt5.QtCore import pyqtSignal, Qt, QSize 6 | from PyQt5.QtGui import QColor, QIcon 7 | from PyQt5.QtWidgets import QWidget, QHBoxLayout, QApplication 8 | from qfluentwidgets import Theme, qconfig, NavigationItemPosition, FluentWindow, SubtitleLabel, setFont, InfoBar, \ 9 | InfoBarPosition, SplashScreen 10 | from qfluentwidgets import FluentIcon as FIF 11 | 12 | from app.config import cfg, base_path, config_path 13 | from app.globals import GlobalsVal 14 | from app.utils.player_name import get_player_name 15 | from app.view.home_interface import HomeInterface 16 | from app.view.player_point_interface import PlayerPointInterface 17 | from app.view.cfg_interface import CFGInterface 18 | from app.view.resource_download_interface import ResourceDownloadInterface 19 | from app.view.resource_interface import ResourceInterface 20 | from app.view.server_list_interface import ServerListInterface 21 | from app.view.setting_interface import SettingInterface 22 | 23 | 24 | class DDNetFolderCrash(QWidget): 25 | def __init__(self, parent=None): 26 | super().__init__(parent) 27 | self.label = SubtitleLabel(self.tr("我们的程序无法自动找到DDNet配置目录\n请手动到设置中指定DDNet配置目录"), 28 | self) 29 | self.hBoxLayout = QHBoxLayout(self) 30 | 31 | setFont(self.label, 24) 32 | self.label.setAlignment(Qt.AlignCenter) 33 | self.hBoxLayout.addWidget(self.label, 1, Qt.AlignCenter) 34 | 35 | 36 | class MainWindow(FluentWindow): 37 | """ 主界面 """ 38 | themeChane = pyqtSignal(Theme) 39 | def __init__(self): 40 | super().__init__() 41 | 42 | self.initWindow() 43 | 44 | self.file_list = GlobalsVal.ddnet_folder 45 | 46 | # 加载配置文件 47 | self.load_config_files() 48 | 49 | # 初始化子界面 50 | self.homeInterface = HomeInterface(self) 51 | self.PlayerPointInterface = PlayerPointInterface(self) 52 | self.CFGInterface = CFGInterface(self) 53 | self.ResourceInterface = ResourceInterface(self) 54 | self.ResourceDownloadInterface = ResourceDownloadInterface(self) 55 | self.ServerListMirrorInterface = ServerListInterface(self) 56 | # self.ServerListPreviewInterface = None 57 | self.settingInterface = SettingInterface(self.themeChane, self) 58 | 59 | self.initNavigation() 60 | self.themeChane.connect(self.__theme_change) 61 | self.splashScreen.finish() 62 | 63 | 64 | def load_config_files(self): 65 | """加载配置文件""" 66 | if all(elem in os.listdir(self.file_list) for elem in ['assets', 'settings_ddnet.cfg']): 67 | GlobalsVal.ddnet_folder_status = True 68 | else: 69 | InfoBar.warning( 70 | title=self.tr('警告'), 71 | content=self.tr("DDNet配置文件目录配置错误,部分功能将被禁用\n请于设置中修改后重启本程序\n请勿设置为DDNet游戏目录"), 72 | orient=Qt.Horizontal, 73 | isClosable=True, 74 | position=InfoBarPosition.BOTTOM_RIGHT, 75 | duration=-1, 76 | parent=GlobalsVal.main_window 77 | ) 78 | 79 | settings_file = os.path.join(self.file_list, "settings_ddnet.cfg") 80 | if os.path.isfile(settings_file): 81 | self.load_settings_ddnet_cfg(settings_file) 82 | 83 | json_file = os.path.join(self.file_list, "ddnet-info.json") 84 | if os.path.isfile(json_file): 85 | try: 86 | with open(json_file, encoding='utf-8') as f: 87 | GlobalsVal.ddnet_info = json.loads(f.read()) 88 | except: 89 | InfoBar.warning( 90 | title=self.tr('警告'), 91 | content=self.tr("没有在DDNet配置文件目录下找到ddnet-info.json文件,游戏版本更新检测将无法工作"), 92 | orient=Qt.Horizontal, 93 | isClosable=True, 94 | position=InfoBarPosition.BOTTOM_RIGHT, 95 | duration=-1, 96 | parent=GlobalsVal.main_window 97 | ) 98 | 99 | server_list_file = os.path.join(self.file_list, "ddnet-serverlist-urls.cfg") 100 | GlobalsVal.server_list_file = os.path.isfile(server_list_file) 101 | 102 | if not os.path.isfile(f"{config_path}/app/config/config.json"): 103 | if get_player_name() == "Realyn//UnU": 104 | cfg.set(cfg.themeColor, QColor("#af251a")) 105 | 106 | def load_settings_ddnet_cfg(self, file_path): 107 | """加载并解析 settings_ddnet.cfg 文件""" 108 | with open(file_path, encoding='utf-8') as f: 109 | lines = f.read().strip().split('\n') 110 | for line in lines: 111 | if line.strip(): 112 | parts = re.split(r'\s+', line, maxsplit=1) 113 | if len(parts) == 2: 114 | key, value = parts 115 | value = self.parse_value(value) 116 | 117 | if key in GlobalsVal.ddnet_setting_config: 118 | if not isinstance(GlobalsVal.ddnet_setting_config[key], list): 119 | GlobalsVal.ddnet_setting_config[key] = [GlobalsVal.ddnet_setting_config[key]] 120 | GlobalsVal.ddnet_setting_config[key].append(value) 121 | else: 122 | GlobalsVal.ddnet_setting_config[key] = value 123 | 124 | @staticmethod 125 | def parse_value(value): 126 | """解析配置文件中的值""" 127 | if ',' in value: 128 | return [v.strip(' "') for v in re.split(r',', value)] 129 | return MainWindow.remove_quotes(value) 130 | 131 | @staticmethod 132 | def remove_quotes(text): 133 | """替换一些文本方便解析""" 134 | if text.startswith('"') and text.endswith('"'): 135 | text = text[1:-1] 136 | if '" "' in text: 137 | return re.split(r'" "', text) 138 | return text 139 | 140 | def initNavigation(self): 141 | """初始化子页面""" 142 | self.addSubInterface(self.homeInterface, FIF.HOME, self.tr('首页')) 143 | self.addSubInterface(self.PlayerPointInterface, FIF.SEARCH, self.tr('玩家分数查询')) 144 | self.addSubInterface(self.CFGInterface, FIF.APPLICATION, self.tr('CFG管理')) 145 | self.addSubInterface(self.ResourceInterface, FIF.EMOJI_TAB_SYMBOLS, self.tr('材质管理')) 146 | self.addSubInterface(self.ServerListMirrorInterface, FIF.LIBRARY, self.tr('服务器列表管理')) 147 | # self.addSubInterface(self.ServerListPreviewInterface, FIF.LIBRARY, self.tr('服务器列表预览')) 148 | # self.addSubInterface(self.ResourceDownloadInterface, FIF.DOWNLOAD, self.tr('材质下载')) 149 | 150 | self.addSubInterface(self.settingInterface, FIF.SETTING, self.tr('设置'), NavigationItemPosition.BOTTOM) 151 | 152 | def initWindow(self): 153 | self.resize(820, 600) 154 | theme = cfg.get(cfg.themeMode) 155 | theme = qconfig.theme if theme == Theme.AUTO else theme 156 | self.setWindowIcon(QIcon(base_path + f'/resource/{theme.value.lower()}/logo.png')) 157 | self.setWindowTitle('DDNetToolBox') 158 | self.setMicaEffectEnabled(False) # 关闭win11的云母特效 159 | 160 | # 显示加载窗口 161 | self.splashScreen = SplashScreen(QIcon(base_path + f'/resource/logo.ico'), self) 162 | self.splashScreen.setIconSize(QSize(106, 106)) 163 | self.splashScreen.raise_() 164 | 165 | # 居中显示 166 | desktop = QApplication.desktop().availableGeometry() 167 | w, h = desktop.width(), desktop.height() 168 | self.move(w//2 - self.width()//2, h//2 - self.height()//2) 169 | self.show() 170 | QApplication.processEvents() 171 | 172 | def __theme_change(self, theme: Theme): 173 | theme = qconfig.theme if theme == Theme.AUTO else theme 174 | self.setWindowIcon(QIcon(base_path + f'/resource/{theme.value.lower()}/logo.png')) 175 | -------------------------------------------------------------------------------- /app/view/player_point_interface.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from PyQt5.QtCore import Qt, pyqtSignal 4 | from PyQt5.QtGui import QFont 5 | from PyQt5.QtWidgets import QWidget, QHBoxLayout, QVBoxLayout, QSpacerItem, QSizePolicy, QStackedWidget, QLabel 6 | from qfluentwidgets import CardWidget, ToolTipFilter, ToolTipPosition, SubtitleLabel, BodyLabel, ProgressRing, \ 7 | SearchLineEdit, ImageLabel, HeaderCardWidget, Pivot, SingleDirectionScrollArea 8 | from sspicon import SECPKG_ATTR_NATIVE_NAMES 9 | 10 | from app.utils.points_rank import points_rank 11 | from app.view.home_interface import TEEDataLoader, TEEInfo, TEEInfoList 12 | 13 | 14 | class ByteLimitedSearchLineEdit(SearchLineEdit): 15 | def __init__(self, max_bytes, *args, **kwargs): 16 | super().__init__(*args, **kwargs) 17 | self.max_bytes = max_bytes 18 | self.textChanged.connect(self.limit_text) 19 | 20 | def limit_text(self): 21 | text = self.text() 22 | while len(text.encode('utf-8')) > self.max_bytes: 23 | text = text[:-1] 24 | self.setText(text) 25 | 26 | 27 | class TEERankCard(CardWidget): 28 | ref_status = True 29 | 30 | def __init__(self, tee_info_ready=None, parent=None): 31 | super().__init__(parent) 32 | self.setMaximumSize(16777215, 180) 33 | self.tee_info_ready = tee_info_ready 34 | 35 | self.setToolTip(self.tr('单击刷新数据')) 36 | self.setToolTipDuration(1000) 37 | self.installEventFilter(ToolTipFilter(self, showDelay=300, position=ToolTipPosition.BOTTOM)) 38 | 39 | self.name = "NaN" 40 | self.hBoxLayout = QHBoxLayout(self) 41 | self.vBoxLayout = QVBoxLayout() 42 | self.teeRankRing = ProgressRing() 43 | self.labels = [ 44 | SubtitleLabel(self.name, self), 45 | BodyLabel(self.tr('全球排名:NaN\n' 46 | '游戏分数:NaN\n' 47 | '游玩时长:NaN\n' 48 | '最后完成:NaN\n' 49 | '入坑时间:NaN'), self), 50 | ] 51 | 52 | self.teeRankRing.setTextVisible(True) 53 | self.teeRankRing.setFormat("NaN") 54 | self.teeRankRing.setFont(QFont(None, 35)) 55 | self.teeRankRing.setStrokeWidth(10) 56 | self.hBoxLayout.setContentsMargins(15, 15, 15, 15) 57 | 58 | self.hBoxLayout.addLayout(self.vBoxLayout) 59 | for label in self.labels: 60 | self.vBoxLayout.addWidget(label, 0, Qt.AlignLeft | Qt.AlignTop) 61 | self.hBoxLayout.addWidget(self.teeRankRing) 62 | 63 | self.clicked.connect(self.__on_clicked) 64 | 65 | def on_data(self, name): 66 | self.name = name 67 | self.labels[0].setText(name) 68 | self.ref_status = False 69 | self.__on_clicked() 70 | 71 | 72 | def __on_clicked(self): 73 | if self.ref_status: 74 | return 75 | else: 76 | self.ref_status = True 77 | 78 | self.teeRankRing.setRange(0, 100) 79 | self.teeRankRing.setValue(0) 80 | self.teeRankRing.setFormat("NaN") 81 | self.tee_info_ready.emit({"player": self.name}) 82 | self.labels[1].setText(self.tr('全球排名:加载中...\n' 83 | '游戏分数:加载中...\n' 84 | '游玩时长:加载中...\n' 85 | '最后完成:加载中...\n' 86 | '入坑时间:加载中...')) 87 | 88 | # self.image_loader = ImageLoader('https://xc.null.red:8043/api/ddnet/draw_player_skin?name={}'.format(self.name)) 89 | # self.image_loader.finished.connect(self.on_image_loaded) 90 | # self.image_loader.start() 91 | 92 | self.data_loader = TEEDataLoader(self.name) 93 | self.data_loader.finished.connect(self.on_data_loaded) 94 | self.data_loader.start() 95 | 96 | def on_image_loaded(self, pixmap): 97 | self.iconWidget.setPixmap(pixmap) 98 | self.iconWidget.scaledToHeight(120) 99 | 100 | def on_data_loaded(self, json_data: dict): 101 | if self.tee_info_ready is not None: 102 | json_data['player'] = self.name 103 | self.tee_info_ready.emit(json_data) 104 | 105 | if 'error' in json_data: 106 | if json_data['error'] == "NoData": 107 | self.labels[1].setText(self.tr('全球排名:NO.查无此人\n' 108 | '游戏分数:查无此人 分\n' 109 | '游玩时长:查无此人 小时\n' 110 | '最后完成:查无此人\n' 111 | '入坑时间:查无此人')) 112 | else: 113 | self.labels[1].setText(self.tr('全球排名:NO.数据获取失败\n' 114 | '游戏分数:数据获取失败 分\n' 115 | '游玩时长:数据获取失败 小时\n' 116 | '最后完成:数据获取失败\n' 117 | '入坑时间:数据获取失败')) 118 | self.ref_status = False 119 | return 120 | use_time = 0 121 | for time in json_data['activity']: 122 | use_time = use_time + time['hours_played'] 123 | 124 | self.teeRankRing.setRange(0, json_data["points"]["total"]) 125 | self.teeRankRing.setValue(json_data["points"]["points"]) 126 | self.teeRankRing.setFormat(points_rank(json_data["points"]["points"], json_data["points"]["total"], use_time, json_data["points"]["rank"])) 127 | self.labels[1].setText(self.tr('全球排名:NO.{}\n' 128 | '游戏分数:{}/{} 分\n' 129 | '游玩时长:{} 小时\n' 130 | '最后完成:{}\n' 131 | '入坑时间:{}').format(json_data["points"]["rank"], json_data["points"]["points"], 132 | json_data["points"]["total"], use_time, 133 | json_data["last_finishes"][0]["map"], 134 | datetime.datetime.fromtimestamp(json_data["first_finish"]["timestamp"]))) 135 | 136 | self.ref_status = False 137 | 138 | 139 | class PlayerPointInterface(QWidget): 140 | def __init__(self, parent=None): 141 | super().__init__(parent=parent) 142 | self.setObjectName('PlayerPointInterface') 143 | 144 | self.vBoxLayout = QVBoxLayout(self) 145 | self.teeinfolist = TEEInfoList(on_data=True) 146 | self.searchHBoxLayout = QHBoxLayout() 147 | self.teeHBoxLayout = QHBoxLayout() 148 | self.teeRankCard = TEERankCard(self.teeinfolist.title_player_name) 149 | self.searchLine = ByteLimitedSearchLineEdit(15) 150 | self.compareTeeRankCard = TEERankCard(self.teeinfolist.title_dummy_name) 151 | self.compareSearchLine = ByteLimitedSearchLineEdit(15) 152 | 153 | self.searchLine.setPlaceholderText(self.tr("填写要查询的玩家名称")) 154 | self.searchLine.setMaxLength(15) 155 | self.compareSearchLine.setPlaceholderText(self.tr("填写要比较的玩家名称")) 156 | 157 | self.searchHBoxLayout.addWidget(self.searchLine) 158 | self.searchHBoxLayout.addWidget(self.compareSearchLine) 159 | self.teeHBoxLayout.addWidget(self.teeRankCard, 0, Qt.AlignTop) 160 | self.teeHBoxLayout.addWidget(self.compareTeeRankCard, 0, Qt.AlignTop) 161 | self.vBoxLayout.addLayout(self.searchHBoxLayout) 162 | self.vBoxLayout.addLayout(self.teeHBoxLayout) 163 | self.vBoxLayout.addWidget(self.teeinfolist) 164 | 165 | self.searchLine.returnPressed.connect(self.searchLine.search) 166 | self.searchLine.searchSignal.connect(self.teeRankCard.on_data) 167 | self.compareSearchLine.returnPressed.connect(self.compareSearchLine.search) 168 | self.compareSearchLine.searchSignal.connect(self.compareTeeRankCard.on_data) -------------------------------------------------------------------------------- /app/view/resource_download_interface.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import shutil 4 | from functools import partial 5 | 6 | from PyQt5.QtCore import Qt, pyqtSignal, QTimer 7 | from PyQt5.QtGui import QFontMetrics, QPainter, QBrush, QPainterPath, QPixmap 8 | from PyQt5.QtWidgets import QWidget, QVBoxLayout, QStackedWidget, QLabel, QFileDialog, QHBoxLayout 9 | from qfluentwidgets import CommandBar, Action, FluentIcon, InfoBar, InfoBarPosition, Pivot, TitleLabel, CardWidget, \ 10 | ImageLabel, CaptionLabel, FlowLayout, SingleDirectionScrollArea, MessageBoxBase, SubtitleLabel, MessageBox, \ 11 | SearchLineEdit, TogglePushButton, ToolTipFilter, ToolTipPosition, setFont, IndeterminateProgressRing, InfoBadge, \ 12 | InfoBadgePosition 13 | from win32comext.mapi.mapitags import PR_DELTAX 14 | 15 | from app.config import cfg, base_path, config_path 16 | from app.globals import GlobalsVal 17 | from app.utils.draw_tee import draw_tee 18 | from app.utils.network import JsonLoader, ImageLoader, HTMLoader 19 | 20 | select_list = { 21 | "skins": {}, 22 | "game": {}, 23 | "emoticons": {}, 24 | "cursor": {}, 25 | "particles": {}, 26 | "entities": {} 27 | } 28 | button_select = None 29 | 30 | 31 | class ResourceCard(CardWidget): 32 | selected = False 33 | 34 | def __init__(self, data, card_type, parent=None): 35 | super().__init__(parent) 36 | global button_select 37 | 38 | self.card_type = card_type 39 | self.data = data 40 | self.file = data['name'] 41 | self.setFixedSize(135, 120) 42 | 43 | if self.card_type == "skins": 44 | self.image_load = ImageLoader(f"https://teedata.net/api/skin/render/name/{data['name']}?emotion=default_eye") 45 | self.spinner = IndeterminateProgressRing() 46 | else: 47 | self.image_load = ImageLoader(f"https://teedata.net/databasev2{data['file_path']}") 48 | self.spinner = IndeterminateProgressRing() 49 | 50 | self.image_load.finished.connect(self.__on_image_load) 51 | self.image_load.start() 52 | 53 | self.label = CaptionLabel(self) 54 | self.label.setText(self.get_elided_text(self.label, self.data['name'])) 55 | self.label.setToolTip(self.data['name']) 56 | self.label.setToolTipDuration(1000) 57 | self.label.installEventFilter(ToolTipFilter(self.label, showDelay=300, position=ToolTipPosition.BOTTOM)) 58 | 59 | self.vBoxLayout = QVBoxLayout(self) 60 | self.vBoxLayout.addWidget(self.spinner, 0, Qt.AlignCenter) 61 | 62 | self.vBoxLayout.addWidget(self.label, 0, Qt.AlignCenter) 63 | 64 | self.clicked.connect(self.__on_clicked) 65 | 66 | def __on_image_load(self, pixmap: QPixmap): 67 | self.iconWidget = ImageLabel(pixmap) 68 | 69 | self.vBoxLayout.replaceWidget(self.spinner, self.iconWidget) 70 | self.spinner.deleteLater() 71 | 72 | if self.card_type == "skins": 73 | self.iconWidget.scaledToHeight(110) 74 | else: 75 | if self.card_type == "entities": 76 | self.iconWidget.scaledToHeight(100) 77 | else: 78 | self.iconWidget.scaledToHeight(60) 79 | 80 | self.iconWidget.stackUnder(self.label) 81 | 82 | def __on_clicked(self): 83 | self.set_selected(not self.selected) 84 | 85 | def set_selected(self, selected): 86 | self.selected = selected 87 | if self.selected: 88 | select_list[self.card_type][self.file] = self 89 | else: 90 | del select_list[self.card_type][self.file] 91 | self.update() 92 | 93 | def paintEvent(self, event): 94 | super().paintEvent(event) 95 | if self.selected: 96 | painter = QPainter(self) 97 | painter.setRenderHint(QPainter.Antialiasing) 98 | 99 | rect = self.rect() 100 | path = QPainterPath() 101 | path.addRoundedRect(rect.x(), rect.y(), rect.width(), rect.height(), 5, 5) 102 | 103 | painter.setBrush(QBrush(cfg.get(cfg.themeColor))) 104 | painter.setPen(Qt.NoPen) 105 | painter.drawPath(path) 106 | 107 | def get_elided_text(self, label, text): 108 | # 省略文本 109 | metrics = QFontMetrics(label.font()) 110 | available_width = label.width() 111 | 112 | elided_text = metrics.elidedText(text, Qt.ElideRight, available_width) 113 | return elided_text 114 | 115 | 116 | class ResourceList(SingleDirectionScrollArea): 117 | refresh_resource = pyqtSignal() 118 | data_ready = pyqtSignal() 119 | batch_size = 1 120 | current_index = 0 121 | 122 | def __init__(self, list_type, parent=None): 123 | super().__init__(parent) 124 | self.list_type = list_type 125 | if self.list_type == "cursors" and not os.path.exists(f"{config_path}/app/ddnet_assets/cursor"): 126 | os.mkdir(f"{config_path}/app/ddnet_assets") 127 | os.mkdir(f"{config_path}/app/ddnet_assets/cursor") 128 | 129 | if self.list_type == "skins": 130 | self.file_path = f"{GlobalsVal.ddnet_folder}/{self.list_type}" 131 | elif self.list_type == "cursors": 132 | self.file_path = f"{config_path}/app/ddnet_assets/cursor" 133 | else: 134 | self.file_path = f"{GlobalsVal.ddnet_folder}/assets/{self.list_type}" 135 | 136 | self.containerWidget = QWidget() 137 | self.containerWidget.setStyleSheet("background: transparent;") 138 | self.fBoxLayout = FlowLayout(self.containerWidget) 139 | 140 | self.setContentsMargins(11, 11, 11, 11) 141 | self.setWidgetResizable(True) 142 | self.enableTransparentBackground() 143 | self.setWidget(self.containerWidget) 144 | 145 | self.refresh_resource.connect(self.__refresh) 146 | self.data_ready.connect(self.__data_ready) 147 | 148 | def load_next_batch(self): 149 | end_index = min(self.current_index + self.batch_size, len(self.teedata_list)) 150 | for i in range(self.current_index, end_index): 151 | self.fBoxLayout.addWidget(ResourceCard(self.teedata_list[i], self.list_type)) 152 | self.current_index = end_index 153 | 154 | if self.current_index < len(self.teedata_list): 155 | QTimer.singleShot(0, self.load_next_batch) 156 | 157 | def __refresh(self): 158 | for i in reversed(range(self.fBoxLayout.count())): 159 | widget = self.fBoxLayout.itemAt(i).widget() 160 | if widget: 161 | self.fBoxLayout.removeWidget(widget) 162 | widget.deleteLater() 163 | 164 | self.file_list = os.listdir(self.file_path) 165 | self.current_index = 0 166 | 167 | QTimer.singleShot(0, self.load_next_batch) 168 | 169 | def __data_ready(self, data=None): 170 | if data is None: 171 | self.teedata = JsonLoader(f"https://teedata.net/_next/data/{GlobalsVal.teedata_build_id}/{self.list_type}.json") 172 | self.teedata.finished.connect(self.__data_ready) 173 | self.teedata.start() 174 | return 175 | 176 | if self.list_type == 'skins': 177 | self.teedata_list = data['pageProps']['skins']['items'] 178 | else: 179 | self.teedata_list = data['pageProps']['assets']['items'] 180 | 181 | QTimer.singleShot(0, self.load_next_batch) 182 | # print(data) 183 | 184 | 185 | class ResourceDownloadInterface(QWidget): 186 | def __init__(self, parent=None): 187 | super().__init__(parent) 188 | self.setObjectName("ResourceDownloadInterface") 189 | 190 | if not GlobalsVal.ddnet_folder_status: 191 | self.label = SubtitleLabel("我们的程序无法自动找到DDNet配置目录\n请手动到设置中指定DDNet配置目录", self) 192 | self.hBoxLayout = QHBoxLayout(self) 193 | 194 | setFont(self.label, 24) 195 | self.label.setAlignment(Qt.AlignCenter) 196 | self.hBoxLayout.addWidget(self.label, 1, Qt.AlignCenter) 197 | return 198 | 199 | self.pivot = Pivot(self) 200 | self.stackedWidget = QStackedWidget(self) 201 | self.vBoxLayout = QVBoxLayout(self) 202 | 203 | self.hBoxLayout = QHBoxLayout() 204 | self.hBoxLayout.addWidget(TitleLabel('材质下载', self)) 205 | self.hBoxLayout.addWidget(CaptionLabel("数据取自 teedata.net"), 0, Qt.AlignRight | Qt.AlignTop) 206 | 207 | self.search_edit = SearchLineEdit() 208 | self.search_edit.setPlaceholderText('搜点什么...') 209 | 210 | self.commandBar = CommandBar(self) 211 | self.commandBar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) 212 | 213 | self.addButton(FluentIcon.DOWNLOAD, '下载'), 214 | self.addButton(FluentIcon.SYNC, '刷新'), 215 | 216 | self.TeedataSkinsInterface = ResourceList('skins', self) 217 | self.TeedataGameSkinsInterface = ResourceList('gameskins', self) 218 | self.TeedataEmoticonsInterface = ResourceList('emoticons', self) 219 | self.TeedataCursorsInterface = ResourceList('cursors', self) # gui_cursor.png 220 | self.TeedataParticlesInterface = ResourceList('particles', self) 221 | self.TeedataEntitiesInterface = ResourceList('entities', self) 222 | 223 | self.addSubInterface(self.TeedataSkinsInterface, 'TeedataSkinsInterface', '皮肤') 224 | self.addSubInterface(self.TeedataGameSkinsInterface, 'TeedataGameSkinsInterface', '贴图') 225 | self.addSubInterface(self.TeedataEmoticonsInterface, 'TeedataEmoticonsInterface', '表情') 226 | self.addSubInterface(self.TeedataCursorsInterface, 'TeedataCursorsInterface', '光标') 227 | self.addSubInterface(self.TeedataParticlesInterface, 'TeedataParticlesInterface', '粒子') 228 | self.addSubInterface(self.TeedataEntitiesInterface, 'TeedataEntitiesInterface', '实体层') 229 | 230 | self.headBoxLayout = QHBoxLayout() 231 | self.headBoxLayout.addWidget(self.pivot, 0, Qt.AlignLeft) 232 | self.headBoxLayout.addWidget(self.commandBar) 233 | 234 | self.vBoxLayout.addLayout(self.hBoxLayout) 235 | self.vBoxLayout.addWidget(self.search_edit) 236 | self.vBoxLayout.addLayout(self.headBoxLayout) 237 | self.vBoxLayout.addWidget(self.stackedWidget) 238 | 239 | self.stackedWidget.setCurrentWidget(self.TeedataSkinsInterface) 240 | self.pivot.setCurrentItem(self.TeedataSkinsInterface.objectName()) 241 | self.pivot.currentItemChanged.connect(lambda k: self.stackedWidget.setCurrentWidget(self.findChild(QWidget, k))) 242 | 243 | self.teedata_build_id = HTMLoader("https://teedata.net/") 244 | self.teedata_build_id.finished.connect(self.__teedata_build_id_finished) 245 | 246 | def showEvent(self, event): 247 | super().showEvent(event) 248 | self.teedata_build_id.start() 249 | 250 | 251 | def __teedata_build_id_finished(self, data): 252 | match = re.search(r'"buildId":"(.*?)"', data) 253 | 254 | if match: 255 | GlobalsVal.teedata_build_id = match.group(1) 256 | self.__teedata_load_data() 257 | 258 | def __teedata_load_data(self): 259 | self.TeedataSkinsInterface.data_ready.emit() 260 | self.TeedataGameSkinsInterface.data_ready.emit() 261 | self.TeedataEmoticonsInterface.data_ready.emit() 262 | self.TeedataCursorsInterface.data_ready.emit() 263 | self.TeedataParticlesInterface.data_ready.emit() 264 | self.TeedataEntitiesInterface.data_ready.emit() 265 | 266 | def addSubInterface(self, widget: QLabel, objectName, text): 267 | widget.setObjectName(objectName) 268 | self.stackedWidget.addWidget(widget) 269 | self.pivot.addItem(routeKey=objectName, text=text) 270 | 271 | def addButton(self, icon, text): 272 | action = Action(icon, text, self) 273 | action.triggered.connect(partial(self.Button_clicked, text)) 274 | self.commandBar.addAction(action) 275 | 276 | def Button_clicked(self, text): 277 | current_item = self.pivot.currentItem().text() 278 | 279 | if text == "下载": 280 | pass 281 | elif text == "刷新": 282 | InfoBar.success( 283 | title='成功', 284 | content="已重新加载本地资源", 285 | orient=Qt.Horizontal, 286 | isClosable=True, 287 | position=InfoBarPosition.BOTTOM_RIGHT, 288 | duration=2000, 289 | parent=GlobalsVal.main_window 290 | ) 291 | 292 | def get_resource_pivot(self, text): 293 | if text == "皮肤": 294 | return self.TeedataSkinsInterface 295 | elif text == "贴图": 296 | return self.TeedataGameSkinsInterface 297 | elif text == "表情": 298 | return self.TeedataEmoticonsInterface 299 | elif text == "光标": 300 | return self.TeedataCursorsInterface 301 | elif text == "粒子": 302 | return self.TeedataParticlesInterface 303 | elif text == "实体层": 304 | return self.TeedataEntitiesInterface 305 | 306 | @staticmethod 307 | def get_resource_pivot_type(text): 308 | if text == "皮肤": 309 | text = "skins" 310 | elif text == "贴图": 311 | text = "game" 312 | elif text == "表情": 313 | text = "emoticons" 314 | elif text == "光标": 315 | text = "cursor" 316 | elif text == "粒子": 317 | text = "particles" 318 | elif text == "实体层": 319 | text = "entities" 320 | 321 | return text 322 | 323 | @staticmethod 324 | def get_resource_url(text): 325 | if text == "皮肤": 326 | text = "skins" 327 | elif text == "贴图": 328 | text = "game" 329 | elif text == "表情": 330 | text = "emoticons" 331 | elif text == "光标": 332 | text = "cursor" 333 | elif text == "粒子": 334 | text = "particles" 335 | elif text == "实体层": 336 | text = "entities" 337 | 338 | if text == "cursor" and not os.path.exists(f"{config_path}/app/ddnet_assets/cursor"): 339 | os.mkdir(f"{config_path}/app/ddnet_assets") 340 | os.mkdir(f"{config_path}/app/ddnet_assets/cursor") 341 | 342 | if text == "skins": 343 | file_path = f"{GlobalsVal.ddnet_folder}/{text}" 344 | elif text == "cursor": 345 | file_path = f"{config_path}/app/ddnet_assets/cursor" 346 | else: 347 | file_path = f"{GlobalsVal.ddnet_folder}/assets/{text}" 348 | 349 | return file_path 350 | -------------------------------------------------------------------------------- /app/view/resource_interface.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | from functools import partial 4 | 5 | from PyQt5.QtCore import Qt, pyqtSignal, QTimer 6 | from PyQt5.QtGui import QFontMetrics, QPainter, QBrush, QPainterPath 7 | from PyQt5.QtWidgets import QWidget, QVBoxLayout, QStackedWidget, QLabel, QFileDialog, QHBoxLayout 8 | from qfluentwidgets import CommandBar, Action, FluentIcon, InfoBar, InfoBarPosition, Pivot, TitleLabel, CardWidget, \ 9 | ImageLabel, CaptionLabel, FlowLayout, SingleDirectionScrollArea, MessageBoxBase, SubtitleLabel, MessageBox, \ 10 | TogglePushButton, ToolTipFilter, ToolTipPosition, setFont 11 | 12 | from app.config import cfg, config_path 13 | from app.globals import GlobalsVal 14 | from app.utils import is_image 15 | from app.utils.draw_tee import draw_tee 16 | # from app.utils.image_alpha_check import has_alpha_channel 17 | 18 | select_list = { 19 | "skins": {}, 20 | "game": {}, 21 | "emoticons": {}, 22 | "cursor": {}, 23 | "particles": {}, 24 | "entities": {} 25 | } 26 | button_select = None 27 | 28 | 29 | class FileSelectMessageBox(MessageBoxBase): 30 | def __init__(self, parent=None): 31 | super().__init__(parent) 32 | self.selected_files = None 33 | self.titleLabel = SubtitleLabel(self.tr('选择文件')) 34 | self.label = QLabel(self.tr("拖拽文件到此处或点击选择文件"), self) 35 | self.label.setAlignment(Qt.AlignCenter) 36 | self.label.setStyleSheet("QLabel { border: 2px dashed #aaa; }") 37 | self.yesButton.setText(self.tr("确认")) 38 | self.cancelButton.setText(self.tr("取消")) 39 | 40 | self.setAcceptDrops(True) 41 | 42 | self.viewLayout.addWidget(self.titleLabel) 43 | self.viewLayout.addWidget(self.label) 44 | 45 | self.label.setMinimumWidth(300) 46 | self.label.setMinimumHeight(100) 47 | 48 | self.label.mousePressEvent = self.select_file 49 | 50 | def dragEnterEvent(self, event): 51 | if event.mimeData().hasUrls(): 52 | event.acceptProposedAction() 53 | 54 | def dropEvent(self, event): 55 | files = [] 56 | for url in event.mimeData().urls(): 57 | files.append(url.toLocalFile()) 58 | 59 | self.selected_files = files 60 | self.label.setText("\n".join(files)) 61 | 62 | def select_file(self, event): 63 | options = QFileDialog.Options() 64 | options |= QFileDialog.ReadOnly 65 | files, _ = QFileDialog.getOpenFileNames(self, self.tr("选择文件"), "", "All Files (*)", 66 | options=options) 67 | 68 | if files: 69 | self.selected_files = files 70 | self.label.setText("\n".join(files)) 71 | 72 | def get_selected_files(self): 73 | return self.selected_files 74 | 75 | 76 | class ResourceCard(CardWidget): 77 | selected = False 78 | 79 | def __init__(self, file, card_type, parent=None): 80 | super().__init__(parent) 81 | global button_select 82 | 83 | self.card_type = card_type 84 | self.file = file 85 | self.setFixedSize(135, 120) 86 | 87 | if self.card_type == "skins": 88 | self.iconWidget = ImageLabel(draw_tee(self.file), self) 89 | self.iconWidget.scaledToHeight(110) 90 | else: 91 | self.iconWidget = ImageLabel(self.file, self) 92 | if self.card_type == "entities": 93 | self.iconWidget.scaledToHeight(100) 94 | else: 95 | self.iconWidget.scaledToHeight(60) 96 | 97 | self.label = CaptionLabel(self) 98 | self.label.setText(self.get_elided_text(self.label, os.path.basename(self.file)[:-4])) 99 | self.label.setToolTip(os.path.basename(self.file)[:-4]) 100 | self.label.setToolTipDuration(1000) 101 | self.label.installEventFilter(ToolTipFilter(self.label, showDelay=300, position=ToolTipPosition.BOTTOM)) 102 | 103 | self.vBoxLayout = QVBoxLayout(self) 104 | self.vBoxLayout.addWidget(self.iconWidget, 0, Qt.AlignCenter) 105 | 106 | if self.card_type == "cursor": 107 | self.button = TogglePushButton(self.tr("启用"), self) 108 | self.button.clicked.connect(self.__button_clicked) 109 | self.vBoxLayout.addWidget(self.button, 0, Qt.AlignCenter) 110 | 111 | self.select_cursor = cfg.get(cfg.DDNetAssetsCursor) 112 | if self.select_cursor is not None: 113 | if file == self.select_cursor: 114 | self.button.setText(self.tr('禁用')) 115 | button_select = self.button 116 | button_select.setChecked(True) 117 | 118 | self.vBoxLayout.addWidget(self.label, 0, Qt.AlignCenter) 119 | 120 | self.clicked.connect(self.__on_clicked) 121 | 122 | def __button_clicked(self, checked): # gui_cursor.png 123 | global button_select 124 | if button_select is not None and button_select != self.button: 125 | button_select.setChecked(False) 126 | button_select.setText(self.tr('启用')) 127 | 128 | ddnet_folder = GlobalsVal.ddnet_folder 129 | 130 | if checked: 131 | self.button.setText(self.tr('禁用')) 132 | button_select = self.button 133 | cfg.set(cfg.DDNetAssetsCursor, self.file) 134 | 135 | shutil.copy(self.file, f"{ddnet_folder}/gui_cursor.png") 136 | else: 137 | self.button.setText(self.tr('启用')) 138 | cfg.set(cfg.DDNetAssetsCursor, f"{ddnet_folder}/gui_cursor.png") 139 | os.remove(f"{ddnet_folder}/gui_cursor.png") 140 | 141 | def __on_clicked(self): 142 | self.set_selected(not self.selected) 143 | 144 | def set_selected(self, selected): 145 | self.selected = selected 146 | if self.selected: 147 | select_list[self.card_type][self.file] = self 148 | else: 149 | del select_list[self.card_type][self.file] 150 | self.update() 151 | 152 | def paintEvent(self, event): 153 | super().paintEvent(event) 154 | if self.selected: 155 | painter = QPainter(self) 156 | painter.setRenderHint(QPainter.Antialiasing) 157 | 158 | rect = self.rect() 159 | path = QPainterPath() 160 | path.addRoundedRect(rect.x(), rect.y(), rect.width(), rect.height(), 5, 5) 161 | 162 | painter.setBrush(QBrush(cfg.get(cfg.themeColor))) 163 | painter.setPen(Qt.NoPen) 164 | painter.drawPath(path) 165 | 166 | def get_elided_text(self, label, text): 167 | # 省略文本 168 | metrics = QFontMetrics(label.font()) 169 | available_width = label.width() 170 | 171 | elided_text = metrics.elidedText(text, Qt.ElideRight, available_width) 172 | return elided_text 173 | 174 | 175 | class ResourceList(SingleDirectionScrollArea): 176 | refresh_resource = pyqtSignal() 177 | file_list = [] 178 | 179 | def __init__(self, list_type, parent=None): 180 | super().__init__(parent) 181 | self.list_type = list_type 182 | if self.list_type == "cursor" and not os.path.exists(f"{config_path}/app/ddnet_assets/cursor"): 183 | os.mkdir(f"{config_path}/app/ddnet_assets") 184 | os.mkdir(f"{config_path}/app/ddnet_assets/cursor") 185 | 186 | if self.list_type == "skins": 187 | self.file_path = [f"{GlobalsVal.ddnet_folder}/{self.list_type}", f"{GlobalsVal.ddnet_folder}/downloadedskins"] 188 | elif self.list_type == "cursor": 189 | self.file_path = [f"{config_path}/app/ddnet_assets/cursor"] 190 | else: 191 | self.file_path = [f"{GlobalsVal.ddnet_folder}/assets/{self.list_type}"] 192 | 193 | self.containerWidget = QWidget() 194 | self.containerWidget.setStyleSheet("background: transparent;") 195 | self.fBoxLayout = FlowLayout(self.containerWidget) 196 | self.setContentsMargins(11, 11, 11, 11) 197 | 198 | self.setWidgetResizable(True) 199 | self.enableTransparentBackground() 200 | self.setWidget(self.containerWidget) 201 | 202 | for i in self.file_path: 203 | if os.path.exists(i): 204 | self.file_list = self.file_list + [os.path.join(i, file_name) for file_name in os.listdir(i)] 205 | 206 | self.batch_size = 1 207 | self.current_index = 0 208 | 209 | QTimer.singleShot(0, self.load_next_batch) 210 | 211 | self.refresh_resource.connect(self.__refresh) 212 | 213 | def load_next_batch(self): 214 | end_index = min(self.current_index + self.batch_size, len(self.file_list)) 215 | for i in range(self.current_index, end_index): 216 | resource_path = self.file_list[i] 217 | file_extension = os.path.splitext(resource_path)[1].lower() 218 | 219 | if not is_image(resource_path) or file_extension != '.png': # 非PNG格式或图片文件不渲染 220 | continue 221 | 222 | # if not is_image(resource_path) or has_alpha_channel(resource_path): 223 | # continue 224 | 225 | self.fBoxLayout.addWidget(ResourceCard(resource_path, self.list_type)) 226 | self.current_index = end_index 227 | 228 | if self.current_index < len(self.file_list): 229 | QTimer.singleShot(0, self.load_next_batch) 230 | 231 | def __refresh(self): 232 | for i in reversed(range(self.fBoxLayout.count())): 233 | widget = self.fBoxLayout.itemAt(i).widget() 234 | if widget: 235 | self.fBoxLayout.removeWidget(widget) 236 | widget.deleteLater() 237 | 238 | self.file_list = [] 239 | for i in self.file_path: 240 | if os.path.exists(i): 241 | self.file_list = self.file_list + [os.path.join(i, file_name) for file_name in os.listdir(i)] 242 | 243 | self.current_index = 0 244 | 245 | QTimer.singleShot(0, self.load_next_batch) 246 | 247 | 248 | class ResourceInterface(QWidget): 249 | def __init__(self, parent=None): 250 | super().__init__(parent) 251 | self.setObjectName("ResourceInterface") 252 | 253 | if not GlobalsVal.ddnet_folder_status: 254 | self.label = SubtitleLabel(self.tr("我们的程序无法自动找到DDNet配置目录\n请手动到设置中指定DDNet配置目录"), self) 255 | self.hBoxLayout = QHBoxLayout(self) 256 | 257 | setFont(self.label, 24) 258 | self.label.setAlignment(Qt.AlignCenter) 259 | self.hBoxLayout.addWidget(self.label, 1, Qt.AlignCenter) 260 | return 261 | 262 | self.pivot = Pivot(self) 263 | self.stackedWidget = QStackedWidget(self) 264 | self.vBoxLayout = QVBoxLayout(self) 265 | self.vBoxLayout.addWidget(TitleLabel(self.tr('材质管理'), self)) 266 | 267 | self.commandBar = CommandBar(self) 268 | self.commandBar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) 269 | 270 | self.addButton(FluentIcon.ADD, self.tr('添加'), '添加'), 271 | self.addButton(FluentIcon.DELETE, self.tr('删除'), '删除'), 272 | self.addButton(FluentIcon.SYNC, self.tr('刷新'), '刷新'), 273 | 274 | self.TeedataSkinsInterface = ResourceList('skins', self) 275 | self.TeedataGameSkinsInterface = ResourceList('game', self) 276 | self.TeedataEmoticonsInterface = ResourceList('emoticons', self) 277 | self.TeedataCursorsInterface = ResourceList('cursor', self) 278 | self.TeedataParticlesInterface = ResourceList('particles', self) 279 | self.TeedataEntitiesInterface = ResourceList('entities', self) 280 | 281 | self.addSubInterface(self.TeedataSkinsInterface, 'TeedataSkinsInterface', self.tr('皮肤')) 282 | self.addSubInterface(self.TeedataGameSkinsInterface, 'TeedataGameSkinsInterface', self.tr('贴图')) 283 | self.addSubInterface(self.TeedataEmoticonsInterface, 'TeedataEmoticonsInterface', self.tr('表情')) 284 | self.addSubInterface(self.TeedataCursorsInterface, 'TeedataCursorsInterface', self.tr('光标')) 285 | self.addSubInterface(self.TeedataParticlesInterface, 'TeedataParticlesInterface', self.tr('粒子')) 286 | self.addSubInterface(self.TeedataEntitiesInterface, 'TeedataEntitiesInterface', self.tr('实体层')) 287 | 288 | self.vBoxLayout.addWidget(self.pivot, 0, Qt.AlignLeft) 289 | self.vBoxLayout.addWidget(self.commandBar) 290 | self.vBoxLayout.addWidget(self.stackedWidget) 291 | 292 | self.stackedWidget.setCurrentWidget(self.TeedataSkinsInterface) 293 | self.pivot.setCurrentItem(self.TeedataSkinsInterface.objectName()) 294 | self.pivot.currentItemChanged.connect( 295 | lambda k: self.stackedWidget.setCurrentWidget(self.findChild(QWidget, k))) 296 | 297 | def addSubInterface(self, widget: QLabel, objectName, text): 298 | widget.setObjectName(objectName) 299 | self.stackedWidget.addWidget(widget) 300 | self.pivot.addItem(routeKey=objectName, text=text) 301 | 302 | def addButton(self, icon, text, text_type): 303 | action = Action(icon, text, self) 304 | action.triggered.connect(partial(self.Button_clicked, text_type)) 305 | self.commandBar.addAction(action) 306 | 307 | def Button_clicked(self, text): 308 | global button_select 309 | current_item = self.pivot.currentItem().text() 310 | 311 | if text == "添加": 312 | w = FileSelectMessageBox(self) 313 | if w.exec(): 314 | files = w.get_selected_files() 315 | if files is None: 316 | InfoBar.error( 317 | title=self.tr('错误'), 318 | content=self.tr("您没有选择任何文件"), 319 | orient=Qt.Horizontal, 320 | isClosable=True, 321 | position=InfoBarPosition.BOTTOM_RIGHT, 322 | duration=2000, 323 | parent=GlobalsVal.main_window 324 | ) 325 | else: 326 | errors = 0 327 | cover = 0 328 | for i in files: 329 | if i.split("/")[-1] in [file for file in os.listdir(self.get_resource_url(current_item))]: 330 | cover += 1 331 | 332 | try: 333 | shutil.copy(i, self.get_resource_url(current_item)) 334 | except Exception as e: 335 | InfoBar.error( 336 | title=self.tr('错误'), 337 | content=self.tr("文件 {} 复制失败\n原因:{}").format(i, e), 338 | orient=Qt.Horizontal, 339 | isClosable=True, 340 | position=InfoBarPosition.BOTTOM_RIGHT, 341 | duration=-1, 342 | parent=GlobalsVal.main_window 343 | ) 344 | errors += 1 345 | 346 | InfoBar.success( 347 | title='成功', 348 | content=self.tr("文件复制已完成\n共复制了 {} 个文件,{} 个文件被覆盖,{} 个文件失败").format(len(files), cover, errors), 349 | orient=Qt.Horizontal, 350 | isClosable=True, 351 | position=InfoBarPosition.BOTTOM_RIGHT, 352 | duration=2000, 353 | parent=GlobalsVal.main_window 354 | ) 355 | self.Button_clicked("刷新") 356 | 357 | elif text == "删除": 358 | selected_items = select_list[self.get_resource_pivot_type(current_item)] 359 | if not selected_items: 360 | InfoBar.warning( 361 | title=self.tr('警告'), 362 | content=self.tr("您没有选择任何东西"), 363 | orient=Qt.Horizontal, 364 | isClosable=True, 365 | position=InfoBarPosition.BOTTOM_RIGHT, 366 | duration=2000, 367 | parent=GlobalsVal.main_window 368 | ) 369 | return 370 | 371 | delete_file = "" 372 | for i in selected_items: 373 | delete_file += f"{i}\n" 374 | 375 | w = MessageBox(self.tr("警告"), self.tr("此操作将会从磁盘中永久删除下列文件,不可恢复:\n{}").format(delete_file), self) 376 | delete = 0 377 | if w.exec(): 378 | for i in selected_items: 379 | try: 380 | os.remove(i) 381 | delete += 1 382 | except: 383 | pass 384 | 385 | select_list[self.get_resource_pivot_type(current_item)] = {} 386 | 387 | InfoBar.warning( 388 | title=self.tr('成功'), 389 | content=self.tr("共删除 {} 个文件,{} 个文件删除失败").format(delete, len(selected_items) - delete), 390 | orient=Qt.Horizontal, 391 | isClosable=True, 392 | position=InfoBarPosition.BOTTOM_RIGHT, 393 | duration=2000, 394 | parent=GlobalsVal.main_window 395 | ) 396 | 397 | self.Button_clicked("刷新") 398 | 399 | elif text == "刷新": 400 | button_select = None 401 | self.get_resource_pivot(current_item).refresh_resource.emit() 402 | select_list[self.get_resource_pivot_type(current_item)] = {} 403 | 404 | InfoBar.success( 405 | title=self.tr('成功'), 406 | content=self.tr("已重新加载本地资源"), 407 | orient=Qt.Horizontal, 408 | isClosable=True, 409 | position=InfoBarPosition.BOTTOM_RIGHT, 410 | duration=2000, 411 | parent=GlobalsVal.main_window 412 | ) 413 | 414 | def get_resource_pivot(self, text): 415 | if text == "皮肤": 416 | return self.TeedataSkinsInterface 417 | elif text == "贴图": 418 | return self.TeedataGameSkinsInterface 419 | elif text == "表情": 420 | return self.TeedataEmoticonsInterface 421 | elif text == "光标": 422 | return self.TeedataCursorsInterface 423 | elif text == "粒子": 424 | return self.TeedataParticlesInterface 425 | elif text == "实体层": 426 | return self.TeedataEntitiesInterface 427 | 428 | @staticmethod 429 | def get_resource_pivot_type(text): 430 | if text == "皮肤": 431 | text = "skins" 432 | elif text == "贴图": 433 | text = "game" 434 | elif text == "表情": 435 | text = "emoticons" 436 | elif text == "光标": 437 | text = "cursor" 438 | elif text == "粒子": 439 | text = "particles" 440 | elif text == "实体层": 441 | text = "entities" 442 | 443 | return text 444 | 445 | @staticmethod 446 | def get_resource_url(text): 447 | if text == "皮肤": 448 | text = "skins" 449 | elif text == "贴图": 450 | text = "game" 451 | elif text == "表情": 452 | text = "emoticons" 453 | elif text == "光标": 454 | text = "cursor" 455 | elif text == "粒子": 456 | text = "particles" 457 | elif text == "实体层": 458 | text = "entities" 459 | 460 | if text == "cursor" and not os.path.exists(f"{config_path}/app/ddnet_assets/cursor"): 461 | os.mkdir(f"{config_path}/app/ddnet_assets") 462 | os.mkdir(f"{config_path}/app/ddnet_assets/cursor") 463 | 464 | if text == "skins": 465 | file_path = f"{GlobalsVal.ddnet_folder}/{text}" 466 | elif text == "cursor": 467 | file_path = f"{config_path}/app/ddnet_assets/cursor" 468 | else: 469 | file_path = f"{GlobalsVal.ddnet_folder}/assets/{text}" 470 | 471 | return file_path 472 | -------------------------------------------------------------------------------- /app/view/server_list_interface.py: -------------------------------------------------------------------------------- 1 | import os 2 | from functools import partial 3 | 4 | from PyQt5.QtCore import Qt 5 | from PyQt5.QtWidgets import QWidget, QHBoxLayout, QTableWidgetItem, QVBoxLayout, QHeaderView, QLabel 6 | from qfluentwidgets import TableWidget, CommandBar, Action, FluentIcon, InfoBar, InfoBarPosition, MessageBox, \ 7 | TitleLabel, SubtitleLabel, setFont, SmoothMode 8 | 9 | from app.config import cfg 10 | from app.globals import GlobalsVal 11 | 12 | 13 | class ServerListInterface(QWidget): 14 | def __init__(self, parent=None): 15 | super().__init__(parent=parent) 16 | self.setObjectName("ServerListInterface") 17 | 18 | if not GlobalsVal.ddnet_folder_status: 19 | self.label = SubtitleLabel(self.tr("我们的程序无法自动找到DDNet配置目录\n请手动到设置中指定DDNet配置目录"), self) 20 | self.hBoxLayout = QHBoxLayout(self) 21 | 22 | setFont(self.label, 24) 23 | self.label.setAlignment(Qt.AlignCenter) 24 | self.hBoxLayout.addWidget(self.label, 1, Qt.AlignCenter) 25 | return 26 | 27 | self.vBoxLayout = QVBoxLayout(self) 28 | self.vBoxLayout.addWidget(TitleLabel(self.tr('服务器列表管理'), self)) 29 | self.setLayout(self.vBoxLayout) 30 | 31 | self.commandBar = CommandBar() 32 | self.commandBar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) 33 | 34 | self.addButton(FluentIcon.ADD, self.tr('添加'), '添加'), 35 | self.addButton(FluentIcon.DELETE, self.tr('删除'), '删除'), 36 | self.addButton(FluentIcon.SAVE, self.tr('保存'), '保存'), 37 | self.addButton(FluentIcon.SYNC, self.tr('刷新'), '刷新'), 38 | self.addButton(FluentIcon.UPDATE, self.tr('重置'), '重置'), 39 | self.addButton(FluentIcon.SPEED_HIGH, self.tr('一键加速'), '一键加速'), 40 | 41 | self.table = TableWidget(self) 42 | self.table.scrollDelagate.verticalSmoothScroll.setSmoothMode(SmoothMode.NO_SMOOTH) 43 | self.table.setBorderRadius(5) 44 | self.table.setWordWrap(False) 45 | self.table.setColumnCount(1) 46 | 47 | server_list = self.get_server_list() 48 | self.table.setRowCount(len(server_list)) 49 | 50 | for i, server_link in enumerate(server_list, start=0): 51 | self.table.setItem(i, 0, QTableWidgetItem(server_link)) 52 | 53 | self.table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) 54 | self.table.verticalHeader().hide() 55 | self.table.horizontalHeader().hide() 56 | 57 | self.vBoxLayout.addWidget(self.commandBar) 58 | self.vBoxLayout.addWidget(self.table) 59 | 60 | def get_server_list(self): 61 | if GlobalsVal.server_list_file: 62 | with open(f'{GlobalsVal.ddnet_folder}/ddnet-serverlist-urls.cfg', encoding='utf-8') as f: 63 | return f.read().split('\n') 64 | else: 65 | return ['https://master1.ddnet.org/ddnet/15/servers.json', 'https://master2.ddnet.org/ddnet/15/servers.json', 'https://master3.ddnet.org/ddnet/15/servers.json', 'https://master4.ddnet.org/ddnet/15/servers.json'] 66 | 67 | def addButton(self, icon, text, text_type): 68 | action = Action(icon, self.tr(text), self) 69 | action.triggered.connect(partial(self.Button_clicked, text_type)) 70 | self.commandBar.addAction(action) 71 | 72 | def Button_clicked(self, text): 73 | if text == "添加": 74 | row_position = self.table.rowCount() 75 | self.table.insertRow(row_position) 76 | 77 | item = QTableWidgetItem(self.tr("双击我进行编辑")) 78 | self.table.setItem(row_position, 0, item) 79 | elif text == "删除": 80 | selected_items = self.table.selectedItems() 81 | if selected_items == []: 82 | InfoBar.warning( 83 | title=self.tr('警告'), 84 | content=self.tr("您没有选择任何东西"), 85 | orient=Qt.Horizontal, 86 | isClosable=True, 87 | position=InfoBarPosition.BOTTOM_RIGHT, 88 | duration=2000, 89 | parent=GlobalsVal.main_window 90 | ) 91 | return 92 | 93 | for item in selected_items: 94 | self.table.removeRow(item.row()) 95 | elif text == "保存": 96 | save_txt = "" 97 | for i in range(self.table.rowCount()): 98 | selected_items = self.table.item(i, 0).text() 99 | if selected_items == "": 100 | w = MessageBox(self.tr("警告"), self.tr("检测到空行,是否删除"), self) 101 | if w.exec(): 102 | InfoBar.success( 103 | title=self.tr('成功'), 104 | content=self.tr("已剔除当前空行"), 105 | orient=Qt.Horizontal, 106 | isClosable=True, 107 | position=InfoBarPosition.BOTTOM_RIGHT, 108 | duration=2000, 109 | parent=GlobalsVal.main_window 110 | ) 111 | continue 112 | else: 113 | InfoBar.warning( 114 | title=self.tr('警告'), 115 | content=self.tr("已保留当前空行"), 116 | orient=Qt.Horizontal, 117 | isClosable=True, 118 | position=InfoBarPosition.BOTTOM_RIGHT, 119 | duration=2000, 120 | parent=GlobalsVal.main_window 121 | ) 122 | save_txt += f"{selected_items}\n" 123 | 124 | save_txt = save_txt.rstrip('\n') 125 | 126 | if save_txt == "": 127 | w = MessageBox(self.tr("警告"), self.tr("列表内容为空,是否继续写入"), self) 128 | if not w.exec(): 129 | return 130 | 131 | with open(f'{GlobalsVal.ddnet_folder}/ddnet-serverlist-urls.cfg', 'w', encoding='utf-8') as f: 132 | f.write(save_txt) 133 | 134 | InfoBar.success( 135 | title=self.tr('成功'), 136 | content=self.tr("服务器列表已保存"), 137 | orient=Qt.Horizontal, 138 | isClosable=True, 139 | position=InfoBarPosition.BOTTOM_RIGHT, 140 | duration=2000, 141 | parent=GlobalsVal.main_window 142 | ) 143 | self.Button_clicked('刷新') 144 | elif text == "刷新": 145 | self.table.clear() 146 | self.table.setRowCount(0) 147 | self.table.clearSelection() 148 | 149 | server_list = self.get_server_list() 150 | self.table.setRowCount(len(server_list)) 151 | 152 | for i, server_link in enumerate(server_list, start=0): 153 | self.table.setItem(i, 0, QTableWidgetItem(server_link)) 154 | 155 | InfoBar.success( 156 | title=self.tr('成功'), 157 | content=self.tr("已重新加载本地资源"), 158 | orient=Qt.Horizontal, 159 | isClosable=True, 160 | position=InfoBarPosition.BOTTOM_RIGHT, 161 | duration=2000, 162 | parent=GlobalsVal.main_window 163 | ) 164 | elif text == "重置": 165 | w = MessageBox(self.tr("警告"), self.tr("该操作将会覆盖现有服务器列表中的所有内容"), self) 166 | if w.exec(): 167 | with open(f'{GlobalsVal.ddnet_folder}/ddnet-serverlist-urls.cfg', 'w', encoding='utf-8') as f: 168 | f.write('https://master1.ddnet.org/ddnet/15/servers.json\nhttps://master2.ddnet.org/ddnet/15/servers.json\nhttps://master3.ddnet.org/ddnet/15/servers.json\nhttps://master4.ddnet.org/ddnet/15/servers.json') 169 | 170 | self.Button_clicked('刷新') 171 | 172 | InfoBar.success( 173 | title=self.tr('成功'), 174 | content=self.tr("已重置服务器列表"), 175 | orient=Qt.Horizontal, 176 | isClosable=True, 177 | position=InfoBarPosition.BOTTOM_RIGHT, 178 | duration=2000, 179 | parent=GlobalsVal.main_window 180 | ) 181 | elif text == "一键加速": 182 | w = MessageBox(self.tr("警告"), self.tr("该操作将会覆盖现有服务器列表中的所有内容"), self) 183 | if w.exec(): 184 | with open(f'{GlobalsVal.ddnet_folder}/ddnet-serverlist-urls.cfg', 'w', encoding='utf-8') as f: 185 | f.write('https://master1.ddnet.org/ddnet/15/servers.json\nhttps://master2.ddnet.org/ddnet/15/servers.json\nhttps://master3.ddnet.org/ddnet/15/servers.json\nhttps://master4.ddnet.org/ddnet/15/servers.json\nhttps://xc.null.red:8043/api/ddnet/get_ddnet_server_list\nhttps://midnight-1312303898.cos.ap-nanjing.myqcloud.com/server-list.json') 186 | 187 | self.Button_clicked('刷新') 188 | 189 | InfoBar.success( 190 | title=self.tr('成功'), 191 | content=self.tr("已加速服务器列表,重启游戏生效"), 192 | orient=Qt.Horizontal, 193 | isClosable=True, 194 | position=InfoBarPosition.BOTTOM_RIGHT, 195 | duration=2000, 196 | parent=GlobalsVal.main_window 197 | ) 198 | -------------------------------------------------------------------------------- /app/view/server_list_preview_interface.py: -------------------------------------------------------------------------------- 1 | import os 2 | from functools import partial 3 | 4 | from PyQt5.QtCore import Qt 5 | from PyQt5.QtWidgets import QWidget, QTableWidgetItem, QVBoxLayout, QHeaderView, QHBoxLayout 6 | from qfluentwidgets import TableWidget, CommandBar, Action, FluentIcon, InfoBar, InfoBarPosition, MessageBox, \ 7 | TitleLabel, SubtitleLabel, setFont 8 | 9 | from app.config import cfg 10 | from app.globals import GlobalsVal 11 | 12 | 13 | class ServerListPreviewInterface(QWidget): 14 | def __init__(self, parent=None): 15 | super().__init__(parent=parent) 16 | self.setObjectName("ServerListPreviewInterface") 17 | 18 | if not GlobalsVal.ddnet_folder_status: 19 | self.label = SubtitleLabel("我们的程序无法自动找到DDNet配置目录\n请手动到设置中指定DDNet配置目录", self) 20 | self.hBoxLayout = QHBoxLayout(self) 21 | 22 | setFont(self.label, 24) 23 | self.label.setAlignment(Qt.AlignCenter) 24 | self.hBoxLayout.addWidget(self.label, 1, Qt.AlignCenter) 25 | return 26 | 27 | self.vBoxLayout = QVBoxLayout(self) 28 | self.vBoxLayout.addWidget(TitleLabel('服务器列表管理', self)) 29 | self.setLayout(self.vBoxLayout) 30 | 31 | self.commandBar = CommandBar() 32 | self.commandBar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) 33 | 34 | self.addButton(FluentIcon.ADD, '添加'), 35 | self.addButton(FluentIcon.DELETE, '删除'), 36 | self.addButton(FluentIcon.SAVE, '保存'), 37 | self.addButton(FluentIcon.SYNC, '刷新'), 38 | self.addButton(FluentIcon.UPDATE, '重置'), 39 | 40 | self.table = TableWidget(self) 41 | self.table.setBorderVisible(True) 42 | self.table.setBorderRadius(5) 43 | self.table.setWordWrap(False) 44 | self.table.setColumnCount(1) 45 | 46 | server_list = self.get_server_list() 47 | self.table.setRowCount(len(server_list)) 48 | 49 | for i, server_link in enumerate(server_list, start=0): 50 | self.table.setItem(i, 0, QTableWidgetItem(server_link)) 51 | 52 | self.table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) 53 | self.table.verticalHeader().hide() 54 | self.table.horizontalHeader().hide() 55 | 56 | self.vBoxLayout.addWidget(self.commandBar) 57 | self.vBoxLayout.addWidget(self.table) 58 | 59 | def get_server_list(self): 60 | if GlobalsVal.server_list_file: 61 | with open(f'{GlobalsVal.ddnet_folder}/ddnet-serverlist-urls.cfg', encoding='utf-8') as f: 62 | return f.read().split('\n') 63 | else: 64 | return ['https://master1.ddnet.org/ddnet/15/servers.json', 'https://master2.ddnet.org/ddnet/15/servers.json', 'https://master3.ddnet.org/ddnet/15/servers.json', 'https://master4.ddnet.org/ddnet/15/servers.json'] 65 | 66 | def addButton(self, icon, text): 67 | action = Action(icon, text, self) 68 | action.triggered.connect(partial(self.Button_clicked, text)) 69 | self.commandBar.addAction(action) 70 | 71 | def Button_clicked(self, text): 72 | if text == "添加": 73 | row_position = self.table.rowCount() 74 | self.table.insertRow(row_position) 75 | 76 | item = QTableWidgetItem("双击我进行编辑") 77 | self.table.setItem(row_position, 0, item) 78 | elif text == "删除": 79 | selected_items = self.table.selectedItems() 80 | if selected_items == []: 81 | InfoBar.warning( 82 | title='警告', 83 | content="您没有选择任何东西", 84 | orient=Qt.Horizontal, 85 | isClosable=True, 86 | position=InfoBarPosition.BOTTOM_RIGHT, 87 | duration=2000, 88 | parent=GlobalsVal.main_window 89 | ) 90 | return 91 | 92 | for item in selected_items: 93 | self.table.removeRow(item.row()) 94 | elif text == "保存": 95 | save_txt = "" 96 | for i in range(self.table.rowCount()): 97 | selected_items = self.table.item(i, 0).text() 98 | if selected_items == "": 99 | w = MessageBox("警告", "检测到空行,是否删除", self) 100 | if w.exec(): 101 | InfoBar.success( 102 | title='成功', 103 | content="已剔除当前空行", 104 | orient=Qt.Horizontal, 105 | isClosable=True, 106 | position=InfoBarPosition.BOTTOM_RIGHT, 107 | duration=2000, 108 | parent=GlobalsVal.main_window 109 | ) 110 | continue 111 | else: 112 | InfoBar.warning( 113 | title='警告', 114 | content="已保留当前空行", 115 | orient=Qt.Horizontal, 116 | isClosable=True, 117 | position=InfoBarPosition.BOTTOM_RIGHT, 118 | duration=2000, 119 | parent=GlobalsVal.main_window 120 | ) 121 | save_txt += f"{selected_items}\n" 122 | 123 | save_txt = save_txt.rstrip('\n') 124 | 125 | if save_txt == "": 126 | w = MessageBox("警告", "列表内容为空,是否继续写入", self) 127 | if not w.exec(): 128 | return 129 | 130 | with open(f'{GlobalsVal.ddnet_folder}/ddnet-serverlist-urls.cfg', 'w', encoding='utf-8') as f: 131 | f.write(save_txt) 132 | 133 | InfoBar.success( 134 | title='成功', 135 | content="服务器列表已保存", 136 | orient=Qt.Horizontal, 137 | isClosable=True, 138 | position=InfoBarPosition.BOTTOM_RIGHT, 139 | duration=2000, 140 | parent=GlobalsVal.main_window 141 | ) 142 | self.Button_clicked('刷新') 143 | elif text == "刷新": 144 | self.table.clear() 145 | self.table.setRowCount(0) 146 | self.table.clearSelection() 147 | 148 | server_list = self.get_server_list() 149 | self.table.setRowCount(len(server_list)) 150 | 151 | for i, server_link in enumerate(server_list, start=0): 152 | self.table.setItem(i, 0, QTableWidgetItem(server_link)) 153 | 154 | InfoBar.success( 155 | title='成功', 156 | content="已重新加载本地资源", 157 | orient=Qt.Horizontal, 158 | isClosable=True, 159 | position=InfoBarPosition.BOTTOM_RIGHT, 160 | duration=2000, 161 | parent=GlobalsVal.main_window 162 | ) 163 | elif text == "重置": 164 | w = MessageBox("警告", "该操作将会覆盖本地文件中的所有内容", self) 165 | if w.exec(): 166 | with open(f'{GlobalsVal.ddnet_folder}/ddnet-serverlist-urls.cfg', 'w', encoding='utf-8') as f: 167 | f.write('https://master1.ddnet.org/ddnet/15/servers.json\nhttps://master2.ddnet.org/ddnet/15/servers.json\nhttps://master3.ddnet.org/ddnet/15/servers.json\nhttps://master4.ddnet.org/ddnet/15/servers.json') 168 | 169 | self.table.clear() 170 | self.table.setRowCount(0) 171 | self.table.clearSelection() 172 | 173 | server_list = self.get_server_list() 174 | self.table.setRowCount(len(server_list)) 175 | 176 | for i, server_link in enumerate(server_list, start=0): 177 | self.table.setItem(i, 0, QTableWidgetItem(server_link)) 178 | 179 | InfoBar.success( 180 | title='成功', 181 | content="已重置服务器列表", 182 | orient=Qt.Horizontal, 183 | isClosable=True, 184 | position=InfoBarPosition.BOTTOM_RIGHT, 185 | duration=2000, 186 | parent=GlobalsVal.main_window 187 | ) 188 | -------------------------------------------------------------------------------- /app/view/setting_interface.py: -------------------------------------------------------------------------------- 1 | import os 2 | import platform 3 | import subprocess 4 | 5 | from PyQt5.QtCore import Qt, pyqtSignal, QUrl 6 | from PyQt5.QtGui import QDesktopServices 7 | from qfluentwidgets import (OptionsSettingCard, ScrollArea, ExpandLayout, FluentIcon, SettingCardGroup, setTheme, 8 | InfoBar, isDarkTheme, Theme, PushSettingCard, SwitchSettingCard, PrimaryPushSettingCard, 9 | CustomColorSettingCard, setThemeColor, InfoBarPosition, 10 | ComboBoxSettingCard, PushButton, InfoBarIcon) 11 | from PyQt5.QtWidgets import QWidget, QFileDialog, QPushButton, QHBoxLayout 12 | from app.config import cfg, base_path, config_path 13 | from app.globals import GlobalsVal 14 | from app.utils.config_directory import get_ddnet_directory 15 | from app.utils.network import JsonLoader 16 | 17 | 18 | class SettingInterface(ScrollArea): 19 | """ Setting interface """ 20 | 21 | ddnetFolderChanged = pyqtSignal(list) 22 | 23 | def __init__(self, themeChane, parent=None): 24 | super().__init__(parent=parent) 25 | self.themeChane = themeChane 26 | 27 | self.scrollWidget = QWidget() 28 | self.expandLayout = ExpandLayout(self.scrollWidget) 29 | 30 | # DDNet 31 | self.DDNetGroup = SettingCardGroup(self.tr("DDNet"), self.scrollWidget) 32 | self.DDNetFolder = PushSettingCard( 33 | self.tr('更改目录'), 34 | FluentIcon.DOWNLOAD, 35 | self.tr("DDNet配置目录"), 36 | cfg.get(cfg.DDNetFolder), 37 | self.DDNetGroup 38 | ) 39 | self.DDNetFolderButton = QPushButton(self.tr('自动寻找'), self.DDNetFolder) 40 | self.DDNetFolder.hBoxLayout.addWidget(self.DDNetFolderButton, 0, Qt.AlignRight) 41 | self.DDNetFolder.hBoxLayout.addSpacing(16) 42 | 43 | self.DDNetCheckUpdate = SwitchSettingCard( 44 | FluentIcon.UPDATE, 45 | self.tr("检测DDNet版本更新"), 46 | self.tr("在启动工具箱的时候检测DDNet客户端版本是否为最新"), 47 | configItem=cfg.DDNetCheckUpdate, 48 | parent=self.DDNetGroup 49 | ) 50 | 51 | # personal 52 | self.personalGroup = SettingCardGroup(self.tr('个性化'), self.scrollWidget) 53 | self.themeCard = OptionsSettingCard( 54 | cfg.themeMode, 55 | FluentIcon.BRUSH, 56 | self.tr('应用主题'), 57 | self.tr("调整你的应用外观"), 58 | texts=[ 59 | self.tr('浅色'), self.tr('深色'), 60 | self.tr('跟随系统设置') 61 | ], 62 | parent=self.personalGroup 63 | ) 64 | 65 | self.themeColorCard = CustomColorSettingCard( 66 | cfg.themeColor, 67 | FluentIcon.PALETTE, 68 | self.tr('主题颜色'), 69 | self.tr('更改应用程序的主题颜色'), 70 | self.personalGroup 71 | ) 72 | 73 | self.zoomCard = OptionsSettingCard( 74 | cfg.dpiScale, 75 | FluentIcon.ZOOM, 76 | self.tr("缩放大小"), 77 | self.tr("更改小部件和字体的大小"), 78 | texts=[ 79 | "100%", "125%", "150%", "175%", "200%", 80 | self.tr("跟随系统默认") 81 | ], 82 | parent=self.personalGroup 83 | ) 84 | 85 | self.languageCard = ComboBoxSettingCard( 86 | cfg.language, 87 | FluentIcon.LANGUAGE, 88 | self.tr('语言'), 89 | self.tr('更改首选语言'), 90 | texts=['简体中文', 'English', self.tr('跟随系统默认')], 91 | parent=self.personalGroup 92 | ) 93 | 94 | self.otherGroup = SettingCardGroup(self.tr('其他'), self.scrollWidget) 95 | self.checkUpdate = PrimaryPushSettingCard( 96 | text=self.tr("检查更新"), 97 | icon=FluentIcon.INFO, 98 | title=self.tr("关于"), 99 | content=self.tr("当前工具箱版本:{},logo 由 燃斯(Realyn//UnU) 绘制").format(GlobalsVal.DDNetToolBoxVersion) 100 | ) 101 | self.checkUpdate.clicked.connect(self.__check_update) 102 | 103 | self.openConfigFolder = PrimaryPushSettingCard( 104 | text=self.tr("打开目录"), 105 | icon=FluentIcon.FOLDER, 106 | title=self.tr("工具箱配置目录"), 107 | content=self.tr("打开工具箱配置文件所在目录") 108 | ) 109 | self.openConfigFolder.clicked.connect(lambda: self.open_folder(config_path)) 110 | 111 | self.__initWidget() 112 | 113 | @staticmethod 114 | def open_folder(directory_path): 115 | if platform.system() == "Windows": 116 | os.startfile(directory_path) 117 | elif platform.system() == "Darwin": 118 | subprocess.run(["open", directory_path]) 119 | else: 120 | subprocess.run(["xdg-open", directory_path]) 121 | 122 | 123 | def __check_update(self, data=None, on_load: bool=False): 124 | if data is not None: 125 | self.checkUpdate.button.setEnabled(True) 126 | if data == {}: 127 | InfoBar.error( 128 | title=self.tr('检查更新'), 129 | content=self.tr("无法访问到github"), 130 | orient=Qt.Horizontal, 131 | isClosable=True, 132 | position=InfoBarPosition.BOTTOM_RIGHT, 133 | duration=-1, 134 | parent=GlobalsVal.main_window 135 | ) 136 | return 137 | 138 | if GlobalsVal.DDNetToolBoxVersion != data['tag_name']: 139 | self.updateInfoBar = InfoBar( 140 | icon=InfoBarIcon.WARNING, 141 | title=self.tr('检查更新'), 142 | content=self.tr("您当前的DDNetToolBox版本为 {} 最新版本为 {}").format(GlobalsVal.DDNetToolBoxVersion, data['tag_name']), 143 | orient=Qt.HorPattern, 144 | isClosable=True, 145 | position=InfoBarPosition.BOTTOM_RIGHT, 146 | duration=-1, 147 | parent=GlobalsVal.main_window 148 | ) 149 | self.updateButton = PushButton('现在更新') 150 | self.closeButton = PushButton('以后再说') 151 | self.updateLayout = QHBoxLayout() 152 | 153 | self.updateButton.clicked.connect(lambda: QDesktopServices.openUrl(QUrl(data['html_url']))) 154 | self.closeButton.clicked.connect(lambda: self.updateInfoBar.close()) 155 | 156 | self.updateLayout.addWidget(self.updateButton) 157 | self.updateLayout.addWidget(self.closeButton) 158 | self.updateInfoBar.widgetLayout.addLayout(self.updateLayout) 159 | self.updateInfoBar.show() 160 | else: 161 | InfoBar.success( 162 | title=self.tr('检查更新'), 163 | content=self.tr("您的DDNetToolBox为最新版"), 164 | orient=Qt.Horizontal, 165 | isClosable=True, 166 | position=InfoBarPosition.BOTTOM_RIGHT, 167 | duration=2000, 168 | parent=GlobalsVal.main_window 169 | ) 170 | return 171 | 172 | if not on_load: 173 | self.checkUpdate.button.setEnabled(False) 174 | InfoBar.info( 175 | title=self.tr('检查更新'), 176 | content=self.tr("正在检查更新中..."), 177 | orient=Qt.Horizontal, 178 | isClosable=True, 179 | position=InfoBarPosition.BOTTOM_RIGHT, 180 | duration=2000, 181 | parent=GlobalsVal.main_window 182 | ) 183 | 184 | self.latest_release = JsonLoader('https://api.github.com/repos/XCWQW1/DDNetToolBox/releases/latest') 185 | self.latest_release.finished.connect(self.__check_update) 186 | self.latest_release.start() 187 | 188 | def __initWidget(self): 189 | self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) 190 | self.setWidget(self.scrollWidget) 191 | self.setWidgetResizable(True) 192 | self.setObjectName("SettingInterface") 193 | 194 | self.__setQss() 195 | self.__check_update(on_load=True) 196 | 197 | # initialize layout 198 | self.__initLayout() 199 | self.__connectSignalToSlot() 200 | 201 | def __setQss(self): 202 | """ set style sheet """ 203 | self.scrollWidget.setObjectName('scrollWidget') 204 | 205 | theme = 'dark' if isDarkTheme() else 'light' 206 | with open(base_path + f'/resource/{theme}/setting_interface.qss', encoding='utf-8') as f: 207 | self.setStyleSheet(f.read()) 208 | 209 | def __initLayout(self): 210 | self.DDNetGroup.addSettingCard(self.DDNetFolder) 211 | self.DDNetGroup.addSettingCard(self.DDNetCheckUpdate) 212 | self.personalGroup.addSettingCard(self.themeCard) 213 | self.personalGroup.addSettingCard(self.themeColorCard) 214 | self.personalGroup.addSettingCard(self.zoomCard) 215 | self.personalGroup.addSettingCard(self.languageCard) 216 | self.otherGroup.addSettingCard(self.checkUpdate) 217 | self.otherGroup.addSettingCard(self.openConfigFolder) 218 | 219 | self.expandLayout.addWidget(self.DDNetGroup) 220 | self.expandLayout.addWidget(self.personalGroup) 221 | self.expandLayout.addWidget(self.otherGroup) 222 | 223 | def __showRestartTooltip(self): 224 | """ show restart tooltip """ 225 | InfoBar.success( 226 | self.tr('成功'), 227 | self.tr('重启以应用更改'), 228 | duration=1500, 229 | position=InfoBarPosition.BOTTOM_RIGHT, 230 | parent=GlobalsVal.main_window 231 | ) 232 | 233 | def __onThemeChanged(self, theme: Theme): 234 | """ theme changed slot """ 235 | self.themeChane.emit(theme) 236 | setTheme(theme) 237 | self.__setQss() 238 | 239 | def __onDDNetFolderChanged(self): 240 | for i in os.listdir(cfg.get(cfg.DDNetFolder)): 241 | if i == "settings_ddnet.cfg": 242 | with open(f'{cfg.get(cfg.DDNetFolder)}/settings_ddnet.cfg', encoding='utf-8') as f: 243 | for i in f.read().split('\n'): 244 | i = i.split(' ', 1) 245 | if len(i) == 2: 246 | GlobalsVal.ddnet_setting_config[i[0]] = i[1].strip('\'"') 247 | 248 | def __onDDNetFolderCardClicked(self): 249 | """ download folder card clicked slot """ 250 | folder = QFileDialog.getExistingDirectory( 251 | self, self.tr("更改目录"), "./") 252 | if not folder or cfg.get(cfg.DDNetFolder) == folder: 253 | return 254 | 255 | cfg.set(cfg.DDNetFolder, folder) 256 | self.DDNetFolder.setContent(folder) 257 | self.__showRestartTooltip() 258 | 259 | def __connectSignalToSlot(self): 260 | """ connect signal to slot """ 261 | cfg.appRestartSig.connect(self.__showRestartTooltip) 262 | cfg.themeChanged.connect(self.__onThemeChanged) 263 | 264 | self.DDNetFolderButton.clicked.connect(self.__FindDDNetFolder) 265 | self.DDNetFolder.clicked.connect(self.__onDDNetFolderCardClicked) 266 | self.DDNetFolder.clicked.connect(self.__onDDNetFolderChanged) 267 | self.themeCard.optionChanged.connect(lambda ci: setTheme(cfg.get(ci))) 268 | self.themeColorCard.colorChanged.connect(setThemeColor) 269 | 270 | 271 | def __FindDDNetFolder(self): 272 | folder = get_ddnet_directory() 273 | if folder != "./": 274 | cfg.set(cfg.DDNetFolder, folder) 275 | self.DDNetFolder.contentLabel.setText(folder) 276 | InfoBar.success( 277 | self.tr('成功'), 278 | self.tr('识别到的DDNet配置文件夹为:{}').format(folder), 279 | duration=1500, 280 | parent=self, 281 | position=InfoBarPosition.BOTTOM_RIGHT, 282 | ) 283 | self.__showRestartTooltip() 284 | else: 285 | InfoBar.error( 286 | self.tr('错误'), 287 | self.tr('没有找到DDNet配置文件夹'), 288 | duration=1500, 289 | position=InfoBarPosition.BOTTOM_RIGHT, 290 | parent=self 291 | ) 292 | -------------------------------------------------------------------------------- /images/en/cfg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XCWQW1/DDNetToolBox/876d56cc669e7546a1aef5d38f9feb190cd91706/images/en/cfg.png -------------------------------------------------------------------------------- /images/en/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XCWQW1/DDNetToolBox/876d56cc669e7546a1aef5d38f9feb190cd91706/images/en/home.png -------------------------------------------------------------------------------- /images/en/home_tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XCWQW1/DDNetToolBox/876d56cc669e7546a1aef5d38f9feb190cd91706/images/en/home_tab.png -------------------------------------------------------------------------------- /images/en/points.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XCWQW1/DDNetToolBox/876d56cc669e7546a1aef5d38f9feb190cd91706/images/en/points.png -------------------------------------------------------------------------------- /images/en/resouces.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XCWQW1/DDNetToolBox/876d56cc669e7546a1aef5d38f9feb190cd91706/images/en/resouces.png -------------------------------------------------------------------------------- /images/en/server_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XCWQW1/DDNetToolBox/876d56cc669e7546a1aef5d38f9feb190cd91706/images/en/server_list.png -------------------------------------------------------------------------------- /images/en/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XCWQW1/DDNetToolBox/876d56cc669e7546a1aef5d38f9feb190cd91706/images/en/settings.png -------------------------------------------------------------------------------- /images/zh/cfg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XCWQW1/DDNetToolBox/876d56cc669e7546a1aef5d38f9feb190cd91706/images/zh/cfg.png -------------------------------------------------------------------------------- /images/zh/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XCWQW1/DDNetToolBox/876d56cc669e7546a1aef5d38f9feb190cd91706/images/zh/home.png -------------------------------------------------------------------------------- /images/zh/home_tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XCWQW1/DDNetToolBox/876d56cc669e7546a1aef5d38f9feb190cd91706/images/zh/home_tab.png -------------------------------------------------------------------------------- /images/zh/points.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XCWQW1/DDNetToolBox/876d56cc669e7546a1aef5d38f9feb190cd91706/images/zh/points.png -------------------------------------------------------------------------------- /images/zh/resouces.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XCWQW1/DDNetToolBox/876d56cc669e7546a1aef5d38f9feb190cd91706/images/zh/resouces.png -------------------------------------------------------------------------------- /images/zh/server_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XCWQW1/DDNetToolBox/876d56cc669e7546a1aef5d38f9feb190cd91706/images/zh/server_list.png -------------------------------------------------------------------------------- /images/zh/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XCWQW1/DDNetToolBox/876d56cc669e7546a1aef5d38f9feb190cd91706/images/zh/settings.png -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import time 4 | import traceback 5 | 6 | from qfluentwidgets import FluentTranslator 7 | 8 | from app.config import cfg, base_path, config_path 9 | from PyQt5.QtCore import Qt, QTranslator 10 | from PyQt5.QtWidgets import QApplication, QWidget, QDialog, QTextEdit, QVBoxLayout, QHBoxLayout, QPushButton 11 | 12 | from app.view.main_interface import MainWindow 13 | from app.globals import GlobalsVal 14 | 15 | 16 | class CrashApp(QWidget): 17 | def __init__(self, exc_type, exc_value, exc_traceback): 18 | super().__init__() 19 | 20 | traceback.print_exception(exc_type, exc_value, exc_traceback) 21 | error_message = ''.join(traceback.format_exception(exc_type, exc_value, exc_traceback)) 22 | 23 | dialog = QDialog() 24 | dialog.setWindowTitle(self.tr("程序崩溃")) 25 | 26 | layout = QVBoxLayout(dialog) 27 | 28 | text_edit = QTextEdit() 29 | text_edit.setReadOnly(True) 30 | text_edit.setText(error_message) 31 | layout.addWidget(text_edit) 32 | 33 | button_layout = QHBoxLayout() 34 | 35 | copy_button = QPushButton(self.tr("复制日志")) 36 | copy_button.clicked.connect(lambda: QApplication.clipboard().setText(error_message)) 37 | button_layout.addWidget(copy_button) 38 | 39 | ok_button = QPushButton(self.tr("确定并关闭")) 40 | ok_button.clicked.connect(dialog.accept) 41 | button_layout.addWidget(ok_button) 42 | 43 | layout.addLayout(button_layout) 44 | dialog.exec_() 45 | sys.exit(1) 46 | 47 | 48 | def init_window(): 49 | # 初始化目录 50 | if not os.path.exists(config_path): 51 | os.makedirs(os.path.join(config_path, "app"), exist_ok=True) 52 | 53 | # 启用DPI 54 | dpi_scale = cfg.get(cfg.dpiScale) 55 | if dpi_scale == "Auto": 56 | QApplication.setHighDpiScaleFactorRoundingPolicy(Qt.HighDpiScaleFactorRoundingPolicy.PassThrough) 57 | QApplication.setAttribute(Qt.AA_EnableHighDpiScaling) 58 | else: 59 | os.environ["QT_ENABLE_HIGHDPI_SCALING"] = "0" 60 | os.environ["QT_SCALE_FACTOR"] = str(dpi_scale) 61 | 62 | QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps) 63 | 64 | app = QApplication(sys.argv) 65 | # app.setQuitOnLastWindowClosed(False) 66 | 67 | locale = cfg.get(cfg.language).value 68 | 69 | fluentTranslator = FluentTranslator(locale) 70 | Translator = QTranslator() 71 | CrashTranslator = QTranslator() 72 | 73 | Translator.load(locale, "DDNetToolBox.view", ".", f"{base_path}/resource/i18n") 74 | CrashTranslator.load(locale, "DDNetToolBox.main", ".", f"{base_path}/resource/i18n") 75 | 76 | app.installTranslator(fluentTranslator) 77 | app.installTranslator(Translator) 78 | app.installTranslator(CrashTranslator) 79 | 80 | GlobalsVal.main_window = MainWindow() 81 | GlobalsVal.main_window.show() 82 | 83 | app.exec_() 84 | 85 | 86 | if __name__ == '__main__': 87 | # 崩溃回溯 88 | sys.excepthook = CrashApp 89 | sys.exit(init_window()) 90 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | PyQt5 2 | PyQt5-Frameless-Window 3 | PyQt5-Qt5 4 | PyQt5_sip 5 | requests 6 | nuitka 7 | Pillow 8 | platformdirs 9 | PyQt-Fluent-Widgets[full] --------------------------------------------------------------------------------