├── .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 |
3 |
4 |
5 |
6 |
7 | # DDNetToolBox
8 |
9 | A toolbox for [DDRaceNetwork](https://ddnet.org/)
10 |
11 |
12 |
13 |
14 |
15 |
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 | 
41 | 
42 | 
43 | 
44 | 
45 | 
46 | 
47 |
48 | ### Contributors
49 |
50 |
51 |
52 |
53 |
54 | ### Star Trend Chart
55 |
56 |
57 |
63 |
69 |
73 |
--------------------------------------------------------------------------------
/README_zh.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | # DDNetToolBox
8 |
9 | 一个适用于 [DDRaceNetwork](https://ddnet.org/) 的工具箱
10 |
11 |
12 |
13 |
14 |
15 |
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 | 
42 | 
43 | 
44 | 
45 | 
46 | 
47 | 
48 |
49 | ### 贡献者
50 |
51 |
52 |
53 |
54 |
55 | ### Star 趋势图
56 |
57 |
58 |
64 |
70 |
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]
--------------------------------------------------------------------------------