Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
331f7a25d2 | ||
|
|
642b2ec17f | ||
|
|
41aab89669 | ||
|
|
f6a57215c2 | ||
|
|
38549e098e | ||
|
|
286270a819 | ||
|
|
0f9c91b59a | ||
|
|
3753375bed | ||
|
|
dab2ba2dc5 | ||
|
|
f86cb7aa7e | ||
|
|
0cf9f5e6c2 | ||
|
|
f9715f91f7 | ||
|
|
98e51d443e | ||
|
|
c8985f1a85 | ||
|
|
5bd83bfcda | ||
|
|
36f30571f3 | ||
|
|
f202925333 | ||
|
|
2e6f71d962 | ||
|
|
ffcb527adc | ||
|
|
12ca55a372 |
1
.gitignore
vendored
@@ -173,3 +173,4 @@ cython_debug/
|
||||
nuitka-crash-report.xml
|
||||
build.bat
|
||||
log.txt
|
||||
result.csv
|
||||
236
FAQ-en.md
Normal file
@@ -0,0 +1,236 @@
|
||||
<div align="center" style="margin-top: 20px; margin-bottom: 20px;">
|
||||
<img src="./introduction_imgs/main.png" alt="FRAISEMOE Logo" />
|
||||
<h2 style="margin: 10px 0 5px 0; font-weight: bold; color: #e75480;">🍓 FRAISEMOE NEKOPARA Addons Installer NEXT🍓</h2>
|
||||
<p style="font-size: 1.1em; color: #555;">An application for installing patches for the Nekopara series games.</p>
|
||||
|
||||
<p>
|
||||
<a href="./FAQ.md">简体中文</a> |
|
||||
<a href="./FAQ-en.md">English</a>
|
||||
</p>
|
||||
<blockquote style="color: #c00; font-weight: bold; border-left: 4px solid #e75480; background: #fff0f5; padding: 10px;">
|
||||
The English version is not updated in real-time! Please check the Simplified Chinese version for more updates! Thank you for your support!
|
||||
</blockquote>
|
||||
<blockquote style="color: #c00; font-weight: bold; border-left: 4px solid #e75480; background: #fff0f5; padding: 10px;">
|
||||
Please strictly follow all the rules in the <a href="https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/blob/master/FAQ.md">User Guide</a>. The developers are not responsible for any violations.<br>
|
||||
This tool is for educational and communication purposes only. Do not use it for commercial purposes.
|
||||
</blockquote>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## 🎮 Applicable Games:
|
||||
|
||||
- **NEKOPARA Vol. 1**
|
||||
- **NEKOPARA Vol. 2**
|
||||
- **NEKOPARA Vol. 3**
|
||||
- **NEKOPARA Vol. 4**
|
||||
- **NEKOPARA After**
|
||||
|
||||
---
|
||||
|
||||
⛔ **Special Reminder:**
|
||||
|
||||
> **1. Patches cannot be installed for NEKOPARA Vol. 0 & NEKOPARA Extra ❗**
|
||||
|
||||
> **2. Patches will not be installed for games you do not own ❗**
|
||||
|
||||
> **3. This tool is only for installing patches, not for installing games ❗ It only runs on Windows x64 systems ❗**
|
||||
|
||||
> **4. The tool requires administrator privileges to run ❗
|
||||
> Reason: To prevent installation issues caused by the game running, the tool will get game process information to close the game before starting ❗**
|
||||
|
||||
> **5. Before using this tool, you need to understand the basics of patch installation:**
|
||||
>
|
||||
> **5-1. Why install the patch? What's in the patch?**
|
||||
>
|
||||
> **5-2. Based on the documentation, why do errors occur? Or why does the installation fail?**
|
||||
>
|
||||
> **5-3. After a successful installation, how to check the patch settings to confirm it was installed correctly?**
|
||||
>
|
||||
> ***If you are completely unfamiliar with the above, please do not use this tool or watch the tutorial video. If you have already downloaded the tool, it is recommended to move it to the Recycle Bin and delete it.***
|
||||
|
||||
> **6. Make sure you are using the latest version of the application (please regularly check for updates on the mirror site or GitHub and download them) ❗**
|
||||
|
||||
---
|
||||
|
||||
## **🔄 Usage/Flow:**
|
||||
|
||||
1. Download "FRAISEMOE Addons Installer NEXT.exe" from the repository.
|
||||
2. **Close any running games from the ["Applicable Games"](#-applicable-games) list.**
|
||||
3. If the application asks for administrator privileges, grant them. **If administrator privileges cannot be obtained, the application will not run and will exit automatically.**
|
||||
4. If the application asks to close a running game, select "Yes". **If the running game cannot be closed, the application will not run and will exit automatically.**
|
||||
5. After launching the application, select "Start Install" and choose the **parent directory of the game directory.**<br />
|
||||
|
||||
> **Important Note 1** ❓ What is the "parent directory of the game directory"? How to get it?
|
||||
> Taking Steam as an example, find the "Library" tab at the top, then find a [patchable game](#-applicable-games) in the game list on the left;<br />Right-click the game in the list, select "Manage" -> "Browse local files" to get the game directory. Then, click the "←" button in the address bar. If you see the [game's folder, e.g., "NEKOPARA Vol. 1"](#-applicable-games) in the file explorer, then this is the parent directory of the game directory. Select and copy the full path from the address bar.
|
||||
|
||||
> **Important Note 2** ❓ How to use this tool for games from third-party installers?
|
||||
> Since the installation path for third-party games is not fixed, please copy the parent directory path of the game directory yourself, similar to the method for Steam.
|
||||
|
||||
> Example (for illustration only, do not copy directly):
|
||||
> Game folder: C: (drive letter may vary)\Steam\steamapps\common\NEKOPARA Vol. 1
|
||||
> Parent directory of game directory: C: (drive letter may vary)\Steam\steamapps\common
|
||||
|
||||
6. In the folder selection dialog, **paste the copied path into the address bar** and click "Select Folder".<br />**(Note whether the text next to the "Select Folder" button is the last folder name in the path. If not, you may need to re-select).**
|
||||
|
||||
> √ Correct Example (for illustration only, do not copy directly):
|
||||
> Path entered in the address bar above: C: (drive letter may vary)\Steam\steamapps\common
|
||||
> Folder name below: common
|
||||
|
||||
7. After selecting the folder, you may encounter the following situations:
|
||||
<table>
|
||||
<tr>
|
||||
<td><h5>Status</h5></td>
|
||||
<td><h5>Action</h5></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Game exists, but patch is not installed</td>
|
||||
<td>Proceeds directly to download task</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Game exists,<br />but patch is installed from another source or patch file is corrupted</td>
|
||||
<td>Asks whether to reinstall the patch from this tool. If the patch from another source is usable, you can choose not to reinstall</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Game does not exist</td>
|
||||
<td>Skips the patch installation step</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Game exists,<br />but the corresponding patch version cannot be installed with this tool</td>
|
||||
<td>Repeat the previous installation steps</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
8. Confirm the final installation result, then select "Exit".
|
||||
9. Enter the game and check if there are more options in "Settings". Or if you have previously entered the extra story and it appears in the "EXTRA" option, it means the patch was installed successfully. If none of the above is observed, repeat the installation steps.
|
||||
|
||||
---
|
||||
|
||||
## 🔰 Software Features:
|
||||
|
||||
- Detects installed patch files for ["Applicable Games"](#-applicable-games) and compares their [Hash (SHA-256)](#-hashsha-256-checksums) to verify integrity. If normal, it skips installation for that version;<br />If patches from other sources are used or patch files are corrupted, it asks whether to reinstall. If you choose to reinstall, it will automatically delete old patch files, download the patch package, and reinstall it.
|
||||
- Detects all unpatched versions and performs installation tasks.
|
||||
|
||||
---
|
||||
|
||||
## ❓ FAQ & User Guide
|
||||
|
||||
---
|
||||
|
||||
<h4><u>【Important】Why did the download fail?</u></h4>
|
||||
|
||||
1. Please check if the "final folder" in the address bar of the "Folder Selector" matches the "Folder Name" below (above the "Select Folder" button). If they do not match, it will not work correctly. If this is not the issue, proceed to the next step.
|
||||
2. Please check if the selected folder contains the [game folder](#-usageflow) (refer to step 5 in the usage flow). If the game folder does not exist, it will not work correctly. If this is not the issue, proceed to the next step.
|
||||
3. Please check if your network environment is normal and the connection is stable. If this is not the issue, proceed to the next step.
|
||||
4. If the installation result is displayed directly (i.e., the installation step was skipped), it means the path is incorrect and the game could not be identified. Please check the path and try again. <b>If you are using a non-Steam version, please find the relevant resources to install it yourself.</b>
|
||||
5. Please go to [GitHub](https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT) or the [domestic mirror site blog](https://blog.ovofish.com/posts/c54d3755.html) to <b>check if you are using the latest version. The application will not work correctly if it is not the latest version.</b>
|
||||
|
||||
---
|
||||
|
||||
<h4><u>【Important】Encountered an error and need to report it? How to submit a bug report?</u></h4>
|
||||
|
||||
1. First, please rule out if it is a local network problem.
|
||||
2. Second, please enable debug mode, run the program again, and <b>save a screenshot of the error along with the log.txt file from the same directory.</b>
|
||||
3. Finally, <b>please [submit an Issue on GitHub](https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/issues).</b>
|
||||

|
||||
|
||||
---
|
||||
|
||||
<h4><u>During Use</u></h4>
|
||||
|
||||
- The application is open but loading slowly.
|
||||
- Please wait patiently for a while; the program is not unresponsive.
|
||||
- Do not open the application multiple times during loading to avoid unnecessary issues.
|
||||
|
||||
<h4><u>"Application is already running" / "In use" prompt when opening</u></h4>
|
||||
|
||||
- This is caused by opening the application too frequently, causing Task Manager to fail to refresh. Please manually open Task Manager, find "FRAISEMOE Addons Installer NEXT", end its process, and then restart.
|
||||
|
||||
<h4><u>1. Download Errors</u></h4>
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td><h5>Common Error Types</h5></td>
|
||||
<td><h5>Error Information</h5></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Contains "403" / "Access denied by server"</td>
|
||||
<td>Access denied by the server. Check if you are using a network proxy (VPN), reset the network proxy (or exit the VPN program), then "Restart the application" and try again.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Contains "port=443" / "An existing connection was forcibly closed by the remote host"</td>
|
||||
<td>Download interrupted. After other tasks have verified file integrity / download tasks are complete, use "Start Install" again and select the previously entered "parent directory of the game" to install.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Contains other messages</td>
|
||||
<td>1. Mostly user network status is abnormal. Check and fix your network status before trying again.<br />2. In some cases, it may be a server failure. Please report the problem via GitHub Issues.</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
<h4><u>2. Problems During Download and Installation</u></h4>
|
||||
<table>
|
||||
<tr>
|
||||
<td><h5>Common Problem Types</h5></td>
|
||||
<td><h5>Solution</h5></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Download progress is slow or seems to have stalled</td>
|
||||
<td>If the progress has stalled but no error is reported, wait a moment.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Window flickers when verifying file integrity</td>
|
||||
<td>This is normal, no action is needed.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>The download progress window pops up and is covered by the hash check window / the window close button turns red</td>
|
||||
<td>Some patch files are large, and calculating the hash value takes longer. Please wait a moment. If the wait is too long, you can manually click the main window/download progress pop-up/hash check window to refresh the status.</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h4><u>3. Cannot exit the program during download</u></h4>
|
||||
|
||||
- To ensure the effectiveness of the patch, do not exit the program during download and installation. The user is responsible for any negative consequences of violating this notice.
|
||||
|
||||
<h4><u>4. Forcibly terminating the program during download</u></h4>
|
||||
|
||||
- This may cause patch file corruption. Restarting the application will automatically overwrite downloaded files. The user is responsible for any negative consequences of violating this notice.
|
||||
|
||||
<h4><u>5. Network speed significantly decreases after multiple downloads and installations, despite a normal network status</u></h4>
|
||||
|
||||
- To ensure server stability and resource security, download sources are divided into domestic and international. To guarantee download quality for more users, domestic sources have a download limit. Tasks exceeding the limit are forwarded to international sources for download.
|
||||
|
||||
<h4><u>6. Found an identical/similar repository/application outside of this repository</u></h4>
|
||||
|
||||
- It may be modified by other developers or use patch files from unknown sources. Do not download/use such repositories/applications.
|
||||
|
||||
<h4><u>7. User obtained this application through non-free means before using it</u></h4>
|
||||
|
||||
- This application is free and open-source. If you obtained it through a paid channel, please request a refund immediately and take action to protect your rights.
|
||||
|
||||
---
|
||||
|
||||
## 💫 HASH(SHA-256) Checksums
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td><h5>Game Patch</h5></td>
|
||||
<td><h5>SHA-256 (Hash creation date: 2024/07-2024-08)</h5></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Vol.1</td>
|
||||
<td>04b48b231a7f34431431e5027fcc7b27affaa951b8169c541709156acf754f3e</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Vol.2</td>
|
||||
<td>b9c00a2b113a1e768bf78400e4f9075ceb7b35349cdeca09be62eb014f0d4b42</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Vol.3</td>
|
||||
<td>2ce7b223c84592e1ebc3b72079dee1e5e8d064ade15723328a64dee58833b9d5</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Vol.4</td>
|
||||
<td>4a4a9ae5a75a18aacbe3ab0774d7f93f99c046afe3a777ee0363e8932b90f36a</td>
|
||||
</tr>
|
||||
</table>
|
||||
28
FAQ.md
@@ -2,19 +2,10 @@
|
||||
<img src="./introduction_imgs/main.png" alt="FRAISEMOE Logo" />
|
||||
<h2 style="margin: 10px 0 5px 0; font-weight: bold; color: #e75480;">🍓 FRAISEMOE NEKOPARA Addons Installer NEXT🍓</h2>
|
||||
<p style="font-size: 1.1em; color: #555;">一个为 Nekopara 系列游戏安装补丁的应用。</p>
|
||||
|
||||
<p>
|
||||
<a href="https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/" style="margin-right: 15px;">
|
||||
<img src="https://img.shields.io/github/stars/Yanam1Anna/FRAISEMOE-Addons-Installer?style=social" alt="GitHub stars" />
|
||||
GitHub
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1hn9UYwE6p/" style="margin-right: 15px;">
|
||||
<img src="https://img.shields.io/badge/Bilibili-视频讲解-00A1D6?logo=bilibili&logoColor=white" alt="Bilibili" />
|
||||
Bilibili
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
<a href="https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/blob/master/FAQ.md">中文</a> |
|
||||
<a href="https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/blob/master/FAQ-en.md">English</a>
|
||||
<a href="./FAQ.md">中文</a> |
|
||||
<a href="./FAQ-en.md">English</a>
|
||||
</p>
|
||||
<blockquote style="color: #c00; font-weight: bold; border-left: 4px solid #e75480; background: #fff0f5; padding: 10px;">
|
||||
请严格遵守 <a href="https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/blob/master/FAQ.md">使用须知文档</a> 的所有条例,如有违反,全体开发人员不承担任何责任。<br>
|
||||
@@ -133,13 +124,12 @@
|
||||
|
||||
---
|
||||
|
||||
<h4><u>【重要】为什么开发者无视我的问题?如何提交错误报告?</u></h4>
|
||||
<h4><u>【重要】遇到错误需要反馈?如何提交错误报告?</u></h4>
|
||||
|
||||
1. 首先,每个人都会有没空的时候,请耐心等待回复或问题处理。
|
||||
2. 其次,文档和视频中已详细介绍了使用方法和常见问题解决方式,请检查你遇到的问题,或相似类别的问题是否存在于文档中,如果存在,一般不回复处理。
|
||||
3. 最后,如果遇到了未提及的问题,<b>请勿在视频站内或博客站内以评论,私信等方式报告你的问题,请到[GitHub中提交Issues](https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/issues)。</b>
|
||||
4. 提交问题报告时,<b>请附上下载报错窗口的报错信息,而不是安装最终结果显示,</b>安装结果显示是给用户看的,不是给开发者看的。
|
||||

|
||||
1. 首先,请排除是否计算机本机网络问题
|
||||
2. 其次,请打开debug模式,再次运行程序,<b>将报错截图与同目录下的log.txt文件一并保存。</b>
|
||||
3. 最后,<b>请到[GitHub中提交Issues](https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/issues)。</b>
|
||||

|
||||
|
||||
---
|
||||
|
||||
@@ -151,7 +141,7 @@
|
||||
|
||||
<h4><u>打开应用时提示应用正在运行 / 被占用的情况</u></h4>
|
||||
|
||||
- 由于开启应用动作过于频繁,造成任务管理器刷新失败,请手动进入任务管理器中找到"FRAISEMOE-Addons-Installer",结束其程序进程后再次重启。
|
||||
- 由于开启应用动作过于频繁,造成任务管理器刷新失败,请手动进入任务管理器中找到"FRAISEMOE Addons Installer NEXT",结束其程序进程后再次重启。
|
||||
|
||||
<h4><u>1. 下载报错</u></h4>
|
||||
|
||||
|
||||
93
README-en.md
Normal file
@@ -0,0 +1,93 @@
|
||||
# 🍓FRAISEMOE-Addons-Installer-NEXT🍓
|
||||
|
||||
```
|
||||
🔊 Note: This repository's documentation updates have stabilized. If there are any missing parts, please promptly raise an issue. Thank you all for your support!
|
||||
The English version is not updated in real-time! Please check the Simplified Chinese version for more updates! Thank you for your support!
|
||||
```
|
||||
|
||||
<!-- PROJECT SHIELDS -->
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT">
|
||||
<img src="./introduction_imgs/main.png" alt="Logo">
|
||||
</a>
|
||||
<br />
|
||||
<br />
|
||||
If you find this tool helpful, please give it a Star⭐~
|
||||
<br />
|
||||
<br />
|
||||
⚠️ This is an unofficial tool and does not represent any official stance. ⚠️
|
||||
<br />
|
||||
<br />
|
||||
⚠️ This NEXT version is currently under active development, and stability is not guaranteed. ⚠️
|
||||
<br />
|
||||
<br />
|
||||
<a href="https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/issues">Report a Bug</a>
|
||||
·
|
||||
<a href="https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/issues">Request a Feature</a>
|
||||
·
|
||||
<a href="https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/blob/master/FAQ.md">【Must Read Before Use】User Guide</a>
|
||||
<br />
|
||||
</p>
|
||||
|
||||
<!-- LANGUAGE -->
|
||||
<p align="center">
|
||||
<a href="README.md">简体中文</a> |
|
||||
<a href="README-en.md">English</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## 📕 Table of Contents
|
||||
|
||||
- [Getting Started](#getting-started)
|
||||
- [Installation](#installation)
|
||||
- [Usage Steps](#usage-steps)
|
||||
- [Versioning](#versioning)
|
||||
- [Authors](#authors)
|
||||
- [Important Notes](#important-notes)
|
||||
- [Special Thanks](#special-thanks)
|
||||
- [License](#license)
|
||||
|
||||
---
|
||||
|
||||
## Getting Started
|
||||
|
||||
### 📥 Installation
|
||||
|
||||
Please download the latest version of the application from the [Releases Page](https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/releases).
|
||||
|
||||

|
||||
|
||||
### ❗ Usage Steps
|
||||
|
||||
1. **Important**: Please be sure to read the [User Guide](https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/blob/master/FAQ.md) before use.
|
||||
2. **Detailed Tutorial**: Refer to the [Video Tutorial](https://www.bilibili.com/video/BV1hn9UYwE6p/).
|
||||
|
||||
### ⭕ Versioning
|
||||
|
||||
This project uses Git for version control. You can view the currently available versions on the [Releases Page](https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/releases).
|
||||
|
||||
---
|
||||
|
||||
## 💡 Important Notes
|
||||
|
||||
1. **Do not use modified applications**: The authors and developers are not responsible for any personal loss resulting from the use of applications from unknown or modified sources.
|
||||
2. **Follow all rules**: Please strictly adhere to the rules in the [User Guide](https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/blob/master/FAQ.md) and this document. The authors and developers are not liable for any violations.
|
||||
3. **Free and Open Source**: This application is free and open-source. If you obtained it through a paid channel, please request a refund immediately and take action to protect your rights.
|
||||
|
||||
---
|
||||
|
||||
## 👨💻 Authors
|
||||
|
||||
- [ouyangqiqi](https://github.com/hyb-oyqq): Current maintainer of this repository.
|
||||
|
||||
## 🎉 Special Thanks
|
||||
- [Yanam1Anna](https://github.com/Yanam1Anna): The original author of this project, who provided extensive code and resources.
|
||||
- [HTony03](https://github.com/HTony03): Provided support for refactoring, logic optimization, and feature implementation for parts of the original source code.
|
||||
- [钨鸮](https://github.com/ABSIDIA): Provided support for cloud resource storage.
|
||||
- [XIU2/CloudflareSpeedTest](https://github.com/XIU2/CloudflareSpeedTest): Provided core support for the IP optimization feature of this project.
|
||||
|
||||
## 📖 License
|
||||
|
||||
This application is licensed under the [GPL-3.0](https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/blob/master/LICENSE) license. Please see the [LICENSE](https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/blob/master/LICENSE) file for more information.
|
||||
10
README.md
@@ -1,7 +1,7 @@
|
||||
# 🍓FRAISEMOE-Addons-Installer-NEXT🍓
|
||||
|
||||
```
|
||||
🔊 注意:本库仍然努力更新中,大部分文档不可用,敬请谅解。
|
||||
🔊 注意:本库文档更新已趋于稳定,如有遗漏部分请及时提出issue,感谢各位支持!
|
||||
```
|
||||
|
||||
<!-- PROJECT SHIELDS -->
|
||||
@@ -32,7 +32,7 @@
|
||||
<!-- LANGUAGE -->
|
||||
<p align="center">
|
||||
<a href="https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT">中文</a> |
|
||||
<a href="#">English</a>
|
||||
<a href="README-en.md">English</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
@@ -55,7 +55,7 @@
|
||||
|
||||
请从 [应用发布页面](https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/releases) 下载最新版本的应用程序。
|
||||
|
||||

|
||||

|
||||
|
||||
### ❗ 使用步骤
|
||||
|
||||
@@ -89,9 +89,9 @@
|
||||
|
||||
## 🎉 特别鸣谢
|
||||
- [Yanam1Anna](https://github.com/Yanam1Anna): 本项目的原作者,提供了大量代码和资源。
|
||||
- [HTony03](https://github.com/HTony03):对于本项目部分源码的重构、逻辑优化和功能实现提供了大力支持。
|
||||
|
||||
- [HTony03](https://github.com/HTony03):对于原项目部分源码的重构、逻辑优化和功能实现提供了支持。
|
||||
- [钨鸮](https://github.com/ABSIDIA):对于云端资源存储提供了支持。
|
||||
- [XIU2/CloudflareSpeedTest](https://github.com/XIU2/CloudflareSpeedTest):为本项目提供了 IP 优选功能的核心支持。
|
||||
|
||||
## 📖 协议
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 2.8 MiB After Width: | Height: | Size: 1.3 MiB |
BIN
source/IMG/BG/title_bg1.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
source/IMG/BG/title_bg2.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
source/IMG/BTN/Button.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
source/IMG/ICO/cloudflare_logo_icon.ico
Normal file
|
After Width: | Height: | Size: 66 KiB |
@@ -1,169 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
################################################################################
|
||||
## Form generated from reading UI file 'install.ui'
|
||||
##
|
||||
## Created by: Qt User Interface Compiler version 6.9.1
|
||||
##
|
||||
## WARNING! All changes made in this file will be lost when recompiling UI file!
|
||||
################################################################################
|
||||
from pic_data import img_data
|
||||
from PySide6.QtGui import QPixmap
|
||||
import base64
|
||||
from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
|
||||
QMetaObject, QObject, QPoint, QRect,
|
||||
QSize, QTime, QUrl, Qt)
|
||||
from PySide6.QtGui import (QAction, QBrush, QColor, QConicalGradient,
|
||||
QCursor, QFont, QFontDatabase, QGradient,
|
||||
QIcon, QImage, QKeySequence, QLinearGradient,
|
||||
QPainter, QPalette, QPixmap, QRadialGradient,
|
||||
QTransform)
|
||||
from PySide6.QtWidgets import (QApplication, QLabel, QMainWindow, QMenu,
|
||||
QMenuBar, QPushButton, QSizePolicy, QWidget)
|
||||
def load_base64_image(base64_str):
|
||||
pixmap = QPixmap()
|
||||
pixmap.loadFromData(base64.b64decode(base64_str))
|
||||
return pixmap
|
||||
|
||||
|
||||
class Ui_MainWindows(object):
|
||||
def setupUi(self, MainWindows):
|
||||
if not MainWindows.objectName():
|
||||
MainWindows.setObjectName(u"MainWindows")
|
||||
MainWindows.setEnabled(True)
|
||||
MainWindows.resize(1024, 576)
|
||||
MainWindows.setMinimumSize(QSize(1024, 576))
|
||||
MainWindows.setMaximumSize(QSize(1024, 576))
|
||||
MainWindows.setMouseTracking(False)
|
||||
MainWindows.setTabletTracking(False)
|
||||
MainWindows.setAcceptDrops(True)
|
||||
MainWindows.setAutoFillBackground(True)
|
||||
MainWindows.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
|
||||
MainWindows.setAnimated(True)
|
||||
MainWindows.setDocumentMode(False)
|
||||
MainWindows.setDockNestingEnabled(False)
|
||||
self.action_2 = QAction(MainWindows)
|
||||
self.action_2.setObjectName(u"action_2")
|
||||
self.centralwidget = QWidget(MainWindows)
|
||||
self.centralwidget.setObjectName(u"centralwidget")
|
||||
self.centralwidget.setAutoFillBackground(True)
|
||||
self.loadbg = QLabel(self.centralwidget)
|
||||
self.loadbg.setObjectName(u"loadbg")
|
||||
self.loadbg.setGeometry(QRect(0, 0, 1031, 561))
|
||||
self.loadbg.setPixmap(load_base64_image(img_data["loadbg"]))
|
||||
self.loadbg.setScaledContents(True)
|
||||
self.vol1bg = QLabel(self.centralwidget)
|
||||
self.vol1bg.setObjectName(u"vol1bg")
|
||||
self.vol1bg.setGeometry(QRect(0, 120, 93, 64))
|
||||
self.vol1bg.setPixmap(load_base64_image(img_data["vol1"]))
|
||||
self.vol1bg.setScaledContents(True)
|
||||
self.vol2bg = QLabel(self.centralwidget)
|
||||
self.vol2bg.setObjectName(u"vol2bg")
|
||||
self.vol2bg.setGeometry(QRect(0, 180, 93, 64))
|
||||
self.vol2bg.setPixmap(load_base64_image(img_data["vol2"]))
|
||||
self.vol2bg.setScaledContents(True)
|
||||
self.vol3bg = QLabel(self.centralwidget)
|
||||
self.vol3bg.setObjectName(u"vol3bg")
|
||||
self.vol3bg.setGeometry(QRect(0, 240, 93, 64))
|
||||
self.vol3bg.setPixmap(load_base64_image(img_data["vol3"]))
|
||||
self.vol3bg.setScaledContents(True)
|
||||
self.vol4bg = QLabel(self.centralwidget)
|
||||
self.vol4bg.setObjectName(u"vol4bg")
|
||||
self.vol4bg.setGeometry(QRect(0, 300, 93, 64))
|
||||
self.vol4bg.setPixmap(load_base64_image(img_data["vol4"]))
|
||||
self.vol4bg.setScaledContents(True)
|
||||
self.afterbg = QLabel(self.centralwidget)
|
||||
self.afterbg.setObjectName(u"afterbg")
|
||||
self.afterbg.setGeometry(QRect(0, 360, 93, 64))
|
||||
self.afterbg.setPixmap(load_base64_image(img_data["after"]))
|
||||
self.afterbg.setScaledContents(True)
|
||||
self.Mainbg = QLabel(self.centralwidget)
|
||||
self.Mainbg.setObjectName(u"Mainbg")
|
||||
self.Mainbg.setGeometry(QRect(0, 0, 1031, 561))
|
||||
self.Mainbg.setPixmap(load_base64_image(img_data["Mainbg"]))
|
||||
self.Mainbg.setScaledContents(True)
|
||||
self.start_install_btn = QPushButton(self.centralwidget)
|
||||
self.start_install_btn.setObjectName(u"start_install_btn")
|
||||
self.start_install_btn.setEnabled(True)
|
||||
self.start_install_btn.setGeometry(QRect(780, 250, 191, 91))
|
||||
self.start_install_btn.setAutoFillBackground(False)
|
||||
start_install_icon = QIcon()
|
||||
start_install_pixmap = load_base64_image(img_data["start_install_btn"])
|
||||
if not start_install_pixmap.isNull():
|
||||
start_install_icon.addPixmap(start_install_pixmap)
|
||||
self.start_install_btn.setIcon(start_install_icon)
|
||||
self.start_install_btn.setIcon(start_install_icon)
|
||||
self.start_install_btn.setIconSize(QSize(189, 110))
|
||||
self.start_install_btn.setCheckable(False)
|
||||
self.start_install_btn.setAutoRepeat(False)
|
||||
self.start_install_btn.setAutoDefault(False)
|
||||
self.start_install_btn.setFlat(True)
|
||||
self.exit_btn = QPushButton(self.centralwidget)
|
||||
self.exit_btn.setObjectName(u"exit_btn")
|
||||
self.exit_btn.setEnabled(True)
|
||||
self.exit_btn.setGeometry(QRect(780, 340, 191, 91))
|
||||
self.exit_btn.setAutoFillBackground(False)
|
||||
exit_icon = QIcon()
|
||||
exit_pixmap = load_base64_image(img_data["exit_btn"])
|
||||
if not exit_pixmap.isNull():
|
||||
exit_icon.addPixmap(exit_pixmap)
|
||||
self.exit_btn.setIcon(exit_icon)
|
||||
self.exit_btn.setIcon(exit_icon)
|
||||
self.exit_btn.setIconSize(QSize(189, 110))
|
||||
self.exit_btn.setCheckable(False)
|
||||
self.exit_btn.setFlat(True)
|
||||
self.menubg = QLabel(self.centralwidget)
|
||||
self.menubg.setObjectName(u"menubg")
|
||||
self.menubg.setGeometry(QRect(710, 0, 321, 561))
|
||||
self.menubg.setPixmap(load_base64_image(img_data["menubg"]))
|
||||
self.menubg.setScaledContents(True)
|
||||
MainWindows.setCentralWidget(self.centralwidget)
|
||||
self.loadbg.raise_()
|
||||
self.vol1bg.raise_()
|
||||
self.vol2bg.raise_()
|
||||
self.vol3bg.raise_()
|
||||
self.vol4bg.raise_()
|
||||
self.afterbg.raise_()
|
||||
self.Mainbg.raise_()
|
||||
self.menubg.raise_()
|
||||
self.start_install_btn.raise_()
|
||||
self.exit_btn.raise_()
|
||||
self.menubar = QMenuBar(MainWindows)
|
||||
self.menubar.setObjectName(u"menubar")
|
||||
self.menubar.setGeometry(QRect(0, 0, 1024, 21))
|
||||
self.menu = QMenu(self.menubar)
|
||||
self.menu.setObjectName(u"menu")
|
||||
self.menu_2 = QMenu(self.menubar)
|
||||
self.menu_2.setObjectName(u"menu_2")
|
||||
MainWindows.setMenuBar(self.menubar)
|
||||
|
||||
self.menubar.addAction(self.menu.menuAction())
|
||||
self.menubar.addAction(self.menu_2.menuAction())
|
||||
self.menu.addSeparator()
|
||||
self.menu.addAction(self.action_2)
|
||||
|
||||
self.retranslateUi(MainWindows)
|
||||
|
||||
QMetaObject.connectSlotsByName(MainWindows)
|
||||
# setupUi
|
||||
|
||||
def retranslateUi(self, MainWindows):
|
||||
MainWindows.setWindowTitle(QCoreApplication.translate("MainWindows", u" UI Test", None))
|
||||
self.action_2.setText(QCoreApplication.translate("MainWindows", u"\u68c0\u67e5\u66f4\u65b0(\u672a\u5b8c\u6210)", None))
|
||||
self.loadbg.setText("")
|
||||
self.vol1bg.setText("")
|
||||
self.vol2bg.setText("")
|
||||
self.vol3bg.setText("")
|
||||
self.vol4bg.setText("")
|
||||
self.afterbg.setText("")
|
||||
self.Mainbg.setText("")
|
||||
#if QT_CONFIG(accessibility)
|
||||
self.start_install_btn.setAccessibleDescription("")
|
||||
#endif // QT_CONFIG(accessibility)
|
||||
self.start_install_btn.setText("")
|
||||
self.exit_btn.setText("")
|
||||
self.menubg.setText("")
|
||||
self.menu.setTitle(QCoreApplication.translate("MainWindows", u"\u8bbe\u7f6e", None))
|
||||
self.menu_2.setTitle(QCoreApplication.translate("MainWindows", u"\u5e2e\u52a9", None))
|
||||
# retranslateUi
|
||||
|
||||
@@ -1,158 +0,0 @@
|
||||
from PySide6.QtCore import (QPropertyAnimation, QParallelAnimationGroup,
|
||||
QPoint, QEasingCurve, QTimer)
|
||||
from PySide6.QtWidgets import QGraphicsOpacityEffect
|
||||
|
||||
class MultiStageAnimations:
|
||||
def __init__(self, ui):
|
||||
self.ui = ui
|
||||
# 获取画布尺寸
|
||||
self.canvas_width = ui.centralwidget.width()
|
||||
self.canvas_height = ui.centralwidget.height()
|
||||
|
||||
# 动画时序配置
|
||||
self.animation_config = {
|
||||
"logo": {
|
||||
"delay_after": 2800
|
||||
},
|
||||
"mainbg": {
|
||||
"delay_after": 500
|
||||
}
|
||||
}
|
||||
|
||||
# 第一阶段:Logo动画配置
|
||||
self.logo_widgets = [
|
||||
{"widget": ui.vol1bg, "delay": 0, "duration": 500, "end_pos": QPoint(0, 120)},
|
||||
{"widget": ui.vol2bg, "delay": 80, "duration": 500, "end_pos": QPoint(0, 180)},
|
||||
{"widget": ui.vol3bg, "delay": 160, "duration": 500, "end_pos": QPoint(0, 240)},
|
||||
{"widget": ui.vol4bg, "delay": 240, "duration": 500, "end_pos": QPoint(0, 300)},
|
||||
{"widget": ui.afterbg, "delay": 320, "duration": 500, "end_pos": QPoint(0, 360)}
|
||||
]
|
||||
|
||||
# 第二阶段:菜单元素
|
||||
self.menu_widgets = [
|
||||
{"widget": ui.menubg, "end_pos": QPoint(710, 0), "duration": 600},
|
||||
{"widget": ui.start_install_btn, "end_pos": QPoint(780, 250), "duration": 600},
|
||||
{"widget": ui.exit_btn, "end_pos": QPoint(780, 340), "duration": 600}
|
||||
]
|
||||
|
||||
self.animations = []
|
||||
self.timers = []
|
||||
def initialize(self):
|
||||
"""初始化所有组件状态"""
|
||||
# 设置Mainbg初始状态
|
||||
effect = QGraphicsOpacityEffect(self.ui.Mainbg)
|
||||
effect.setOpacity(0)
|
||||
self.ui.Mainbg.setGraphicsEffect(effect)
|
||||
|
||||
# 初始化Logo位置(移到左侧外)
|
||||
for item in self.logo_widgets:
|
||||
widget = item["widget"]
|
||||
effect = QGraphicsOpacityEffect(widget)
|
||||
effect.setOpacity(0)
|
||||
widget.setGraphicsEffect(effect)
|
||||
widget.move(-widget.width(), item["end_pos"].y())
|
||||
widget.show()
|
||||
print("初始化支持栏动画")
|
||||
|
||||
# 初始化菜单元素(底部外)
|
||||
for item in self.menu_widgets:
|
||||
widget = item["widget"]
|
||||
effect = QGraphicsOpacityEffect(widget)
|
||||
effect.setOpacity(0)
|
||||
widget.setGraphicsEffect(effect)
|
||||
widget.move(widget.x(), self.canvas_height + 100)
|
||||
widget.show()
|
||||
|
||||
def start_logo_animations(self):
|
||||
"""启动Logo动画序列"""
|
||||
for item in self.logo_widgets:
|
||||
timer = QTimer()
|
||||
timer.setSingleShot(True)
|
||||
timer.timeout.connect(
|
||||
lambda w=item["widget"], d=item["duration"], pos=item["end_pos"]:
|
||||
self.animate_logo(w, pos, d)
|
||||
)
|
||||
timer.start(item["delay"])
|
||||
self.timers.append(timer)
|
||||
|
||||
def animate_logo(self, widget, end_pos, duration):
|
||||
"""执行单个Logo动画"""
|
||||
anim_group = QParallelAnimationGroup()
|
||||
|
||||
# 位置动画
|
||||
pos_anim = QPropertyAnimation(widget, b"pos")
|
||||
pos_anim.setDuration(duration)
|
||||
pos_anim.setStartValue(QPoint(-widget.width(), end_pos.y()))
|
||||
pos_anim.setEndValue(end_pos)
|
||||
pos_anim.setEasingCurve(QEasingCurve.Type.OutBack)
|
||||
|
||||
# 透明度动画
|
||||
opacity_anim = QPropertyAnimation(widget.graphicsEffect(), b"opacity")
|
||||
opacity_anim.setDuration(duration)
|
||||
opacity_anim.setStartValue(0)
|
||||
opacity_anim.setEndValue(1)
|
||||
|
||||
anim_group.addAnimation(pos_anim)
|
||||
anim_group.addAnimation(opacity_anim)
|
||||
|
||||
# 最后一个Logo动画完成后添加延迟
|
||||
if widget == self.logo_widgets[-1]["widget"]:
|
||||
anim_group.finished.connect(
|
||||
lambda: QTimer.singleShot(
|
||||
self.animation_config["logo"]["delay_after"],
|
||||
self.start_mainbg_animation
|
||||
)
|
||||
)
|
||||
|
||||
anim_group.start()
|
||||
self.animations.append(anim_group)
|
||||
|
||||
def start_mainbg_animation(self):
|
||||
"""启动主背景淡入动画(带延迟)"""
|
||||
main_anim = QPropertyAnimation(self.ui.Mainbg.graphicsEffect(), b"opacity")
|
||||
main_anim.setDuration(800)
|
||||
main_anim.setStartValue(0)
|
||||
main_anim.setEndValue(1)
|
||||
main_anim.finished.connect(
|
||||
lambda: QTimer.singleShot(
|
||||
self.animation_config["mainbg"]["delay_after"],
|
||||
self.start_menu_animations
|
||||
)
|
||||
)
|
||||
main_anim.start()
|
||||
self.animations.append(main_anim)
|
||||
def start_menu_animations(self):
|
||||
"""启动菜单动画(从下往上)"""
|
||||
for item in self.menu_widgets:
|
||||
anim_group = QParallelAnimationGroup()
|
||||
|
||||
# 位置动画(从下往上)
|
||||
pos_anim = QPropertyAnimation(item["widget"], b"pos")
|
||||
pos_anim.setDuration(item["duration"])
|
||||
pos_anim.setStartValue(QPoint(item["end_pos"].x(), self.canvas_height + 100))
|
||||
pos_anim.setEndValue(item["end_pos"])
|
||||
pos_anim.setEasingCurve(QEasingCurve.Type.OutBack)
|
||||
|
||||
# 透明度动画
|
||||
opacity_anim = QPropertyAnimation(item["widget"].graphicsEffect(), b"opacity")
|
||||
opacity_anim.setDuration(item["duration"])
|
||||
opacity_anim.setStartValue(0)
|
||||
opacity_anim.setEndValue(1)
|
||||
|
||||
anim_group.addAnimation(pos_anim)
|
||||
anim_group.addAnimation(opacity_anim)
|
||||
anim_group.start()
|
||||
self.animations.append(anim_group)
|
||||
def start_animations(self):
|
||||
"""启动完整动画序列"""
|
||||
self.clear_animations()
|
||||
self.start_logo_animations()
|
||||
|
||||
def clear_animations(self):
|
||||
"""清理所有动画资源"""
|
||||
for timer in self.timers:
|
||||
timer.stop()
|
||||
for anim in self.animations:
|
||||
anim.stop()
|
||||
self.timers.clear()
|
||||
self.animations.clear()
|
||||
11
source/core/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from .animations import MultiStageAnimations
|
||||
from .ui_manager import UIManager
|
||||
from .download_manager import DownloadManager
|
||||
from .debug_manager import DebugManager
|
||||
|
||||
__all__ = [
|
||||
'MultiStageAnimations',
|
||||
'UIManager',
|
||||
'DownloadManager',
|
||||
'DebugManager'
|
||||
]
|
||||
349
source/core/animations.py
Normal file
@@ -0,0 +1,349 @@
|
||||
import sys
|
||||
from PySide6.QtCore import (QObject, QPropertyAnimation, QParallelAnimationGroup,
|
||||
QPoint, QEasingCurve, QTimer, Signal, QRect)
|
||||
from PySide6.QtWidgets import QGraphicsOpacityEffect, QPushButton
|
||||
from PySide6.QtGui import QColor
|
||||
|
||||
class MultiStageAnimations(QObject):
|
||||
animation_finished = Signal()
|
||||
def __init__(self, ui, parent=None):
|
||||
super().__init__(parent)
|
||||
self.ui = ui
|
||||
self.parent = parent # 保存父窗口引用以获取当前尺寸
|
||||
|
||||
# 获取画布尺寸 - 动态从父窗口获取
|
||||
if parent:
|
||||
self.canvas_width = parent.width()
|
||||
self.canvas_height = parent.height()
|
||||
else:
|
||||
# 默认尺寸
|
||||
self.canvas_width = 1280
|
||||
self.canvas_height = 720
|
||||
|
||||
# 动画时序配置
|
||||
self.animation_config = {
|
||||
"logo": {
|
||||
"delay_after": 2800
|
||||
},
|
||||
"mainbg": {
|
||||
"delay_after": 500
|
||||
},
|
||||
"button_click": {
|
||||
"scale_duration": 100,
|
||||
"scale_min": 0.95,
|
||||
"scale_max": 1.0
|
||||
}
|
||||
}
|
||||
|
||||
# 第一阶段:Logo动画配置,根据新布局调整Y坐标
|
||||
self.logo_widgets = [
|
||||
{"widget": ui.vol1bg, "delay": 0, "duration": 500, "end_pos": QPoint(0, 150)},
|
||||
{"widget": ui.vol2bg, "delay": 80, "duration": 500, "end_pos": QPoint(0, 210)},
|
||||
{"widget": ui.vol3bg, "delay": 160, "duration": 500, "end_pos": QPoint(0, 270)},
|
||||
{"widget": ui.vol4bg, "delay": 240, "duration": 500, "end_pos": QPoint(0, 330)},
|
||||
{"widget": ui.afterbg, "delay": 320, "duration": 500, "end_pos": QPoint(0, 390)}
|
||||
]
|
||||
|
||||
# 第二阶段:菜单元素,位置会在开始动画时动态计算
|
||||
self.menu_widgets = [
|
||||
# 移除菜单背景动画
|
||||
# {"widget": ui.menubg, "end_pos": QPoint(720, 55), "duration": 600},
|
||||
{"widget": ui.button_container, "end_pos": None, "duration": 600},
|
||||
{"widget": ui.uninstall_container, "end_pos": None, "duration": 600}, # 添加卸载补丁按钮
|
||||
{"widget": ui.exit_container, "end_pos": None, "duration": 600}
|
||||
]
|
||||
|
||||
self.animations = []
|
||||
self.timers = []
|
||||
|
||||
# 设置按钮点击动画
|
||||
self.setup_button_click_animations()
|
||||
|
||||
def setup_button_click_animations(self):
|
||||
"""设置按钮点击动画"""
|
||||
# 为开始安装按钮添加点击动画
|
||||
self.ui.start_install_btn.pressed.connect(
|
||||
lambda: self.start_button_click_animation(self.ui.button_container)
|
||||
)
|
||||
self.ui.start_install_btn.released.connect(
|
||||
lambda: self.end_button_click_animation(self.ui.button_container)
|
||||
)
|
||||
|
||||
# 为卸载补丁按钮添加点击动画
|
||||
self.ui.uninstall_btn.pressed.connect(
|
||||
lambda: self.start_button_click_animation(self.ui.uninstall_container)
|
||||
)
|
||||
self.ui.uninstall_btn.released.connect(
|
||||
lambda: self.end_button_click_animation(self.ui.uninstall_container)
|
||||
)
|
||||
|
||||
# 为退出按钮添加点击动画
|
||||
self.ui.exit_btn.pressed.connect(
|
||||
lambda: self.start_button_click_animation(self.ui.exit_container)
|
||||
)
|
||||
self.ui.exit_btn.released.connect(
|
||||
lambda: self.end_button_click_animation(self.ui.exit_container)
|
||||
)
|
||||
|
||||
def start_button_click_animation(self, button_container):
|
||||
"""开始按钮点击动画"""
|
||||
# 创建缩放动画
|
||||
scale_anim = QPropertyAnimation(button_container.children()[0], b"geometry") # 只对按钮背景应用动画
|
||||
scale_anim.setDuration(self.animation_config["button_click"]["scale_duration"])
|
||||
|
||||
# 获取当前几何形状
|
||||
current_geometry = button_container.children()[0].geometry()
|
||||
|
||||
# 计算缩放后的几何形状(保持中心点不变)
|
||||
scale_factor = self.animation_config["button_click"]["scale_min"]
|
||||
width_diff = current_geometry.width() * (1 - scale_factor) / 2
|
||||
height_diff = current_geometry.height() * (1 - scale_factor) / 2
|
||||
|
||||
new_geometry = QRect(
|
||||
current_geometry.x() + width_diff,
|
||||
current_geometry.y() + height_diff,
|
||||
current_geometry.width() * scale_factor,
|
||||
current_geometry.height() * scale_factor
|
||||
)
|
||||
|
||||
scale_anim.setEndValue(new_geometry)
|
||||
scale_anim.setEasingCurve(QEasingCurve.Type.OutQuad)
|
||||
|
||||
# 启动动画
|
||||
scale_anim.start()
|
||||
self.animations.append(scale_anim)
|
||||
|
||||
# 对文本标签也应用同样的动画
|
||||
text_anim = QPropertyAnimation(button_container.children()[1], b"geometry")
|
||||
text_anim.setDuration(self.animation_config["button_click"]["scale_duration"])
|
||||
text_geometry = button_container.children()[1].geometry()
|
||||
|
||||
new_text_geometry = QRect(
|
||||
text_geometry.x() + width_diff,
|
||||
text_geometry.y() + height_diff,
|
||||
text_geometry.width() * scale_factor,
|
||||
text_geometry.height() * scale_factor
|
||||
)
|
||||
|
||||
text_anim.setEndValue(new_text_geometry)
|
||||
text_anim.setEasingCurve(QEasingCurve.Type.OutQuad)
|
||||
text_anim.start()
|
||||
self.animations.append(text_anim)
|
||||
|
||||
def end_button_click_animation(self, button_container):
|
||||
"""结束按钮点击动画,恢复正常外观"""
|
||||
# 创建恢复动画 - 对背景
|
||||
scale_anim = QPropertyAnimation(button_container.children()[0], b"geometry")
|
||||
scale_anim.setDuration(self.animation_config["button_click"]["scale_duration"])
|
||||
|
||||
# 恢复到原始大小 (10,10,191,91)
|
||||
original_geometry = QRect(10, 10, 191, 91)
|
||||
scale_anim.setEndValue(original_geometry)
|
||||
scale_anim.setEasingCurve(QEasingCurve.Type.OutElastic)
|
||||
|
||||
# 启动动画
|
||||
scale_anim.start()
|
||||
self.animations.append(scale_anim)
|
||||
|
||||
# 恢复文本标签
|
||||
text_anim = QPropertyAnimation(button_container.children()[1], b"geometry")
|
||||
text_anim.setDuration(self.animation_config["button_click"]["scale_duration"])
|
||||
|
||||
# 恢复文本到原始大小 (10,7,191,91)
|
||||
text_anim.setEndValue(QRect(10, 7, 191, 91))
|
||||
text_anim.setEasingCurve(QEasingCurve.Type.OutElastic)
|
||||
text_anim.start()
|
||||
self.animations.append(text_anim)
|
||||
|
||||
def initialize(self):
|
||||
"""初始化所有组件状态"""
|
||||
# 更新画布尺寸
|
||||
if self.parent:
|
||||
self.canvas_width = self.parent.width()
|
||||
self.canvas_height = self.parent.height()
|
||||
|
||||
# 设置Mainbg初始状态
|
||||
effect = QGraphicsOpacityEffect(self.ui.Mainbg)
|
||||
effect.setOpacity(0)
|
||||
self.ui.Mainbg.setGraphicsEffect(effect)
|
||||
|
||||
# 初始化Logo位置(移到左侧外)
|
||||
for item in self.logo_widgets:
|
||||
widget = item["widget"]
|
||||
effect = QGraphicsOpacityEffect(widget)
|
||||
effect.setOpacity(0)
|
||||
widget.setGraphicsEffect(effect)
|
||||
widget.move(-widget.width(), item["end_pos"].y())
|
||||
widget.show()
|
||||
print("初始化支持栏动画")
|
||||
|
||||
# 初始化菜单元素(底部外)
|
||||
for item in self.menu_widgets:
|
||||
widget = item["widget"]
|
||||
effect = QGraphicsOpacityEffect(widget)
|
||||
effect.setOpacity(0)
|
||||
widget.setGraphicsEffect(effect)
|
||||
widget.move(widget.x(), self.canvas_height + 100)
|
||||
widget.show()
|
||||
|
||||
def start_logo_animations(self):
|
||||
"""启动Logo动画序列"""
|
||||
for item in self.logo_widgets:
|
||||
timer = QTimer()
|
||||
timer.setSingleShot(True)
|
||||
timer.timeout.connect(
|
||||
lambda w=item["widget"], d=item["duration"], pos=item["end_pos"]:
|
||||
self.animate_logo(w, pos, d)
|
||||
)
|
||||
timer.start(item["delay"])
|
||||
self.timers.append(timer)
|
||||
|
||||
def animate_logo(self, widget, end_pos, duration):
|
||||
"""执行单个Logo动画"""
|
||||
anim_group = QParallelAnimationGroup()
|
||||
|
||||
# 位置动画
|
||||
pos_anim = QPropertyAnimation(widget, b"pos")
|
||||
pos_anim.setDuration(duration)
|
||||
pos_anim.setStartValue(QPoint(-widget.width(), end_pos.y()))
|
||||
pos_anim.setEndValue(end_pos)
|
||||
pos_anim.setEasingCurve(QEasingCurve.Type.OutBack)
|
||||
|
||||
# 透明度动画
|
||||
opacity_anim = QPropertyAnimation(widget.graphicsEffect(), b"opacity")
|
||||
opacity_anim.setDuration(duration)
|
||||
opacity_anim.setStartValue(0)
|
||||
opacity_anim.setEndValue(1)
|
||||
|
||||
anim_group.addAnimation(pos_anim)
|
||||
anim_group.addAnimation(opacity_anim)
|
||||
|
||||
# 最后一个Logo动画完成后添加延迟
|
||||
if widget == self.logo_widgets[-1]["widget"]:
|
||||
anim_group.finished.connect(
|
||||
lambda: QTimer.singleShot(
|
||||
self.animation_config["logo"]["delay_after"],
|
||||
self.start_mainbg_animation
|
||||
)
|
||||
)
|
||||
|
||||
anim_group.start()
|
||||
self.animations.append(anim_group)
|
||||
|
||||
def start_mainbg_animation(self):
|
||||
"""启动主背景淡入动画(带延迟)"""
|
||||
main_anim = QPropertyAnimation(self.ui.Mainbg.graphicsEffect(), b"opacity")
|
||||
main_anim.setDuration(800)
|
||||
main_anim.setStartValue(0)
|
||||
main_anim.setEndValue(1)
|
||||
main_anim.finished.connect(
|
||||
lambda: QTimer.singleShot(
|
||||
self.animation_config["mainbg"]["delay_after"],
|
||||
self.start_menu_animations
|
||||
)
|
||||
)
|
||||
main_anim.start()
|
||||
self.animations.append(main_anim)
|
||||
def start_menu_animations(self):
|
||||
"""启动菜单动画(从下往上)"""
|
||||
# 更新按钮最终位置
|
||||
self._update_button_positions()
|
||||
|
||||
# 跟踪最后一个动画,用于连接finished信号
|
||||
last_anim = None
|
||||
|
||||
for item in self.menu_widgets:
|
||||
anim_group = QParallelAnimationGroup()
|
||||
|
||||
# 位置动画(从下往上)
|
||||
pos_anim = QPropertyAnimation(item["widget"], b"pos")
|
||||
pos_anim.setDuration(item["duration"])
|
||||
pos_anim.setStartValue(QPoint(item["end_pos"].x(), self.canvas_height + 100))
|
||||
pos_anim.setEndValue(item["end_pos"])
|
||||
pos_anim.setEasingCurve(QEasingCurve.Type.OutBack)
|
||||
|
||||
# 透明度动画
|
||||
opacity_anim = QPropertyAnimation(item["widget"].graphicsEffect(), b"opacity")
|
||||
opacity_anim.setDuration(item["duration"])
|
||||
opacity_anim.setStartValue(0)
|
||||
opacity_anim.setEndValue(1)
|
||||
|
||||
anim_group.addAnimation(pos_anim)
|
||||
anim_group.addAnimation(opacity_anim)
|
||||
|
||||
# 记录最后一个按钮的动画
|
||||
if item["widget"] == self.ui.exit_container:
|
||||
last_anim = anim_group
|
||||
|
||||
anim_group.start()
|
||||
self.animations.append(anim_group)
|
||||
|
||||
# 在最后一个动画完成时发出信号
|
||||
if last_anim:
|
||||
last_anim.finished.connect(self.animation_finished.emit)
|
||||
|
||||
def _update_button_positions(self):
|
||||
"""更新按钮最终位置"""
|
||||
# 根据当前窗口大小动态计算按钮位置
|
||||
if self.parent:
|
||||
width = self.parent.width()
|
||||
height = self.parent.height()
|
||||
|
||||
# 计算按钮位置
|
||||
right_margin = 20 # 减小右边距,使按钮更靠右
|
||||
|
||||
# 开始安装按钮
|
||||
if hasattr(self.ui, 'button_container'):
|
||||
btn_width = self.ui.button_container.width()
|
||||
x_pos = width - btn_width - right_margin
|
||||
y_pos = int((height - 65) * 0.28) - 10 # 与resizeEvent中保持一致
|
||||
|
||||
# 更新动画目标位置
|
||||
for item in self.menu_widgets:
|
||||
if item["widget"] == self.ui.button_container:
|
||||
item["end_pos"] = QPoint(x_pos, y_pos)
|
||||
|
||||
# 卸载补丁按钮
|
||||
if hasattr(self.ui, 'uninstall_container'):
|
||||
btn_width = self.ui.uninstall_container.width()
|
||||
x_pos = width - btn_width - right_margin
|
||||
y_pos = int((height - 65) * 0.46) - 10 # 与resizeEvent中保持一致
|
||||
|
||||
# 更新动画目标位置
|
||||
for item in self.menu_widgets:
|
||||
if item["widget"] == self.ui.uninstall_container:
|
||||
item["end_pos"] = QPoint(x_pos, y_pos)
|
||||
|
||||
# 退出按钮
|
||||
if hasattr(self.ui, 'exit_container'):
|
||||
btn_width = self.ui.exit_container.width()
|
||||
x_pos = width - btn_width - right_margin
|
||||
y_pos = int((height - 65) * 0.64) - 10 # 与resizeEvent中保持一致
|
||||
|
||||
# 更新动画目标位置
|
||||
for item in self.menu_widgets:
|
||||
if item["widget"] == self.ui.exit_container:
|
||||
item["end_pos"] = QPoint(x_pos, y_pos)
|
||||
else:
|
||||
# 默认位置
|
||||
for item in self.menu_widgets:
|
||||
if item["widget"] == self.ui.button_container:
|
||||
item["end_pos"] = QPoint(1050, 200)
|
||||
elif item["widget"] == self.ui.uninstall_container:
|
||||
item["end_pos"] = QPoint(1050, 310)
|
||||
elif item["widget"] == self.ui.exit_container:
|
||||
item["end_pos"] = QPoint(1050, 420)
|
||||
|
||||
def start_animations(self):
|
||||
"""启动完整动画序列"""
|
||||
self.clear_animations()
|
||||
self.start_logo_animations()
|
||||
|
||||
def clear_animations(self):
|
||||
"""清理所有动画资源"""
|
||||
for timer in self.timers:
|
||||
timer.stop()
|
||||
for anim in self.animations:
|
||||
anim.stop()
|
||||
self.timers.clear()
|
||||
self.animations.clear()
|
||||
57
source/core/debug_manager.py
Normal file
@@ -0,0 +1,57 @@
|
||||
import os
|
||||
import sys
|
||||
from PySide6 import QtWidgets
|
||||
from data.config import LOG_FILE
|
||||
from utils import Logger
|
||||
|
||||
class DebugManager:
|
||||
def __init__(self, main_window):
|
||||
"""初始化调试管理器
|
||||
|
||||
Args:
|
||||
main_window: 主窗口实例
|
||||
"""
|
||||
self.main_window = main_window
|
||||
self.logger = None
|
||||
self.original_stdout = None
|
||||
self.original_stderr = None
|
||||
|
||||
def toggle_debug_mode(self, checked):
|
||||
"""切换调试模式
|
||||
|
||||
Args:
|
||||
checked: 是否启用调试模式
|
||||
"""
|
||||
self.main_window.config["debug_mode"] = checked
|
||||
self.main_window.save_config(self.main_window.config)
|
||||
if checked:
|
||||
self.start_logging()
|
||||
else:
|
||||
self.stop_logging()
|
||||
|
||||
def start_logging(self):
|
||||
"""启动日志记录"""
|
||||
if self.logger is None:
|
||||
try:
|
||||
if os.path.exists(LOG_FILE):
|
||||
os.remove(LOG_FILE)
|
||||
# 保存原始的 stdout 和 stderr
|
||||
self.original_stdout = sys.stdout
|
||||
self.original_stderr = sys.stderr
|
||||
# 创建 Logger 实例
|
||||
self.logger = Logger(LOG_FILE, self.original_stdout)
|
||||
sys.stdout = self.logger
|
||||
sys.stderr = self.logger
|
||||
print("--- Debug mode enabled ---")
|
||||
except (IOError, OSError) as e:
|
||||
QtWidgets.QMessageBox.critical(self.main_window, "错误", f"无法创建日志文件: {e}")
|
||||
self.logger = None
|
||||
|
||||
def stop_logging(self):
|
||||
"""停止日志记录"""
|
||||
if self.logger:
|
||||
print("--- Debug mode disabled ---")
|
||||
sys.stdout = self.original_stdout
|
||||
sys.stderr = self.original_stderr
|
||||
self.logger.close()
|
||||
self.logger = None
|
||||
725
source/core/download_manager.py
Normal file
@@ -0,0 +1,725 @@
|
||||
import os
|
||||
import requests
|
||||
import json
|
||||
from collections import deque
|
||||
from urllib.parse import urlparse
|
||||
import re # Added for recursive search
|
||||
|
||||
from PySide6 import QtWidgets, QtCore
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtGui import QIcon, QPixmap
|
||||
|
||||
from utils import msgbox_frame, HostsManager, resource_path
|
||||
from data.config import APP_NAME, PLUGIN, GAME_INFO, UA, CONFIG_URL
|
||||
from workers import IpOptimizerThread
|
||||
|
||||
class DownloadManager:
|
||||
def __init__(self, main_window):
|
||||
"""初始化下载管理器
|
||||
|
||||
Args:
|
||||
main_window: 主窗口实例,用于访问UI和状态
|
||||
"""
|
||||
self.main_window = main_window
|
||||
self.selected_folder = ""
|
||||
self.download_queue = deque()
|
||||
self.current_download_thread = None
|
||||
self.hosts_manager = HostsManager()
|
||||
self.optimized_ip = None
|
||||
self.optimization_done = False # 标记是否已执行过优选
|
||||
self.optimizing_msg_box = None
|
||||
|
||||
def file_dialog(self):
|
||||
"""显示文件夹选择对话框,选择游戏安装目录"""
|
||||
self.selected_folder = QtWidgets.QFileDialog.getExistingDirectory(
|
||||
self.main_window, f"选择游戏所在【上级目录】 {APP_NAME}"
|
||||
)
|
||||
if not self.selected_folder:
|
||||
QtWidgets.QMessageBox.warning(
|
||||
self.main_window, f"通知 - {APP_NAME}", "\n未选择任何目录,请重新选择\n"
|
||||
)
|
||||
return
|
||||
self.download_action()
|
||||
|
||||
def get_install_paths(self):
|
||||
"""获取所有游戏版本的安装路径"""
|
||||
# 使用改进的目录识别功能
|
||||
game_dirs = self.main_window.identify_game_directories_improved(self.selected_folder)
|
||||
install_paths = {}
|
||||
|
||||
debug_mode = self.is_debug_mode()
|
||||
|
||||
for game, info in GAME_INFO.items():
|
||||
if game in game_dirs:
|
||||
# 如果找到了游戏目录,使用它
|
||||
game_dir = game_dirs[game]
|
||||
install_path = os.path.join(game_dir, os.path.basename(info["install_path"]))
|
||||
install_paths[game] = install_path
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 使用识别到的游戏目录 {game}: {game_dir}")
|
||||
print(f"DEBUG: 安装路径设置为: {install_path}")
|
||||
else:
|
||||
# 回退到原始路径计算方式
|
||||
install_path = os.path.join(self.selected_folder, info["install_path"])
|
||||
install_paths[game] = install_path
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 未识别到游戏目录 {game}, 使用默认路径: {install_path}")
|
||||
|
||||
return install_paths
|
||||
|
||||
def is_debug_mode(self):
|
||||
"""检查是否处于调试模式"""
|
||||
if hasattr(self.main_window, 'ui_manager') and self.main_window.ui_manager:
|
||||
if hasattr(self.main_window.ui_manager, 'debug_action') and self.main_window.ui_manager.debug_action:
|
||||
return self.main_window.ui_manager.debug_action.isChecked()
|
||||
return False
|
||||
|
||||
def get_download_url(self) -> dict:
|
||||
"""获取所有游戏版本的下载链接
|
||||
|
||||
Returns:
|
||||
dict: 包含游戏版本和下载URL的字典
|
||||
"""
|
||||
try:
|
||||
if self.main_window.cloud_config:
|
||||
if self.is_debug_mode():
|
||||
print("--- Using pre-fetched cloud config ---")
|
||||
config_data = self.main_window.cloud_config
|
||||
else:
|
||||
# 如果没有预加载的配置,则同步获取
|
||||
headers = {"User-Agent": UA}
|
||||
response = requests.get(CONFIG_URL, headers=headers, timeout=10)
|
||||
response.raise_for_status()
|
||||
config_data = response.json()
|
||||
|
||||
if not config_data:
|
||||
raise ValueError("未能获取或解析配置数据")
|
||||
|
||||
if self.is_debug_mode():
|
||||
print(f"DEBUG: Parsed JSON data: {json.dumps(config_data, indent=2)}")
|
||||
|
||||
# 统一处理URL提取,确保返回扁平化的字典
|
||||
urls = {}
|
||||
for i in range(4):
|
||||
key = f"vol.{i+1}.data"
|
||||
if key in config_data and "url" in config_data[key]:
|
||||
urls[f"vol{i+1}"] = config_data[key]["url"]
|
||||
|
||||
if "after.data" in config_data and "url" in config_data["after.data"]:
|
||||
urls["after"] = config_data["after.data"]["url"]
|
||||
|
||||
# 检查是否成功提取了所有URL
|
||||
if len(urls) != 5:
|
||||
missing_keys_map = {
|
||||
f"vol{i+1}": f"vol.{i+1}.data" for i in range(4)
|
||||
}
|
||||
missing_keys_map["after"] = "after.data"
|
||||
|
||||
extracted_keys = set(urls.keys())
|
||||
all_keys = set(missing_keys_map.keys())
|
||||
missing_simple_keys = all_keys - extracted_keys
|
||||
|
||||
missing_original_keys = [missing_keys_map[k] for k in missing_simple_keys]
|
||||
raise ValueError(f"配置文件缺少必要的键: {', '.join(missing_original_keys)}")
|
||||
|
||||
if self.is_debug_mode():
|
||||
print(f"DEBUG: Extracted URLs: {urls}")
|
||||
print("--- Finished getting download URL successfully ---")
|
||||
return urls
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
status_code = e.response.status_code if e.response is not None else "未知"
|
||||
try:
|
||||
error_response = e.response.json() if e.response else {}
|
||||
json_title = error_response.get("title", "无错误类型")
|
||||
json_message = error_response.get("message", "无附加错误信息")
|
||||
except (ValueError, AttributeError):
|
||||
json_title = "配置文件异常,无法解析错误类型"
|
||||
json_message = "配置文件异常,无法解析错误信息"
|
||||
|
||||
if self.is_debug_mode():
|
||||
print(f"ERROR: Failed to get download config due to RequestException: {e}")
|
||||
|
||||
QtWidgets.QMessageBox.critical(
|
||||
self.main_window,
|
||||
f"错误 - {APP_NAME}",
|
||||
f"\n下载配置获取失败\n\n【HTTP状态】:{status_code}\n【错误类型】:{json_title}\n【错误信息】:{json_message}\n",
|
||||
)
|
||||
return {}
|
||||
except ValueError as e:
|
||||
if self.is_debug_mode():
|
||||
print(f"ERROR: Failed to parse download config due to ValueError: {e}")
|
||||
|
||||
QtWidgets.QMessageBox.critical(
|
||||
self.main_window,
|
||||
f"错误 - {APP_NAME}",
|
||||
f"\n配置文件格式异常\n\n【错误信息】:{e}\n",
|
||||
)
|
||||
return {}
|
||||
|
||||
def download_action(self):
|
||||
"""开始下载流程"""
|
||||
# 禁用开始安装按钮
|
||||
self.main_window.set_start_button_enabled(False)
|
||||
|
||||
# 清空下载历史记录
|
||||
self.main_window.download_queue_history = []
|
||||
|
||||
# 使用改进的目录识别功能
|
||||
game_dirs = self.main_window.identify_game_directories_improved(self.selected_folder)
|
||||
|
||||
debug_mode = self.is_debug_mode()
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 开始下载流程, 识别到 {len(game_dirs)} 个游戏目录")
|
||||
|
||||
# 检查是否找到任何游戏目录
|
||||
if not game_dirs:
|
||||
if debug_mode:
|
||||
print("DEBUG: 未识别到任何游戏目录,设置目录未找到错误")
|
||||
# 设置特定的错误类型,以便在按钮点击处理中区分处理
|
||||
self.main_window.last_error_message = "directory_not_found"
|
||||
QtWidgets.QMessageBox.warning(
|
||||
self.main_window,
|
||||
f"目录错误 - {APP_NAME}",
|
||||
"\n未能识别到任何游戏目录。\n\n请确认您选择的是游戏的上级目录,并且该目录中包含NEKOPARA系列游戏文件夹。\n"
|
||||
)
|
||||
return
|
||||
|
||||
# 显示哈希检查窗口
|
||||
self.main_window.hash_msg_box = self.main_window.hash_manager.hash_pop_window(check_type="pre")
|
||||
|
||||
# 执行预检查
|
||||
install_paths = self.get_install_paths()
|
||||
|
||||
self.main_window.hash_thread = self.main_window.create_hash_thread("pre", install_paths)
|
||||
self.main_window.hash_thread.pre_finished.connect(self.on_pre_hash_finished)
|
||||
self.main_window.hash_thread.start()
|
||||
|
||||
def on_pre_hash_finished(self, updated_status):
|
||||
"""哈希预检查完成后的处理
|
||||
|
||||
Args:
|
||||
updated_status: 更新后的安装状态
|
||||
"""
|
||||
self.main_window.installed_status = updated_status
|
||||
if self.main_window.hash_msg_box and self.main_window.hash_msg_box.isVisible():
|
||||
self.main_window.hash_msg_box.accept()
|
||||
self.main_window.hash_msg_box = None
|
||||
|
||||
# 获取下载配置
|
||||
config = self.get_download_url()
|
||||
if not config:
|
||||
QtWidgets.QMessageBox.critical(
|
||||
self.main_window, f"错误 - {APP_NAME}", "\n网络状态异常或服务器故障,请重试\n"
|
||||
)
|
||||
# 重新启用开始安装按钮
|
||||
self.main_window.set_start_button_enabled(True)
|
||||
return
|
||||
|
||||
# 填充下载队列
|
||||
self._fill_download_queue(config)
|
||||
|
||||
# 如果没有需要下载的内容,直接进行最终校验
|
||||
if not self.download_queue:
|
||||
self.main_window.after_hash_compare()
|
||||
return
|
||||
|
||||
# 只有当有需要下载内容时才询问是否使用Cloudflare加速
|
||||
# 询问用户是否使用Cloudflare加速
|
||||
msg_box = QtWidgets.QMessageBox(self.main_window)
|
||||
msg_box.setWindowTitle(f"下载优化 - {APP_NAME}")
|
||||
msg_box.setText("是否愿意通过Cloudflare加速来优化下载速度?\n\n这将临时修改系统的hosts文件,并需要管理员权限。\n如您的杀毒软件提醒有软件正在修改hosts文件,请注意放行。")
|
||||
|
||||
# 设置Cloudflare图标
|
||||
cf_icon_path = resource_path("IMG/ICO/cloudflare_logo_icon.ico")
|
||||
if os.path.exists(cf_icon_path):
|
||||
cf_pixmap = QPixmap(cf_icon_path)
|
||||
if not cf_pixmap.isNull():
|
||||
msg_box.setWindowIcon(QIcon(cf_pixmap))
|
||||
msg_box.setIconPixmap(cf_pixmap.scaled(64, 64, Qt.AspectRatioMode.KeepAspectRatio,
|
||||
Qt.TransformationMode.SmoothTransformation))
|
||||
else:
|
||||
msg_box.setIcon(QtWidgets.QMessageBox.Icon.Question)
|
||||
|
||||
yes_button = msg_box.addButton("是,开启加速", QtWidgets.QMessageBox.ButtonRole.YesRole)
|
||||
no_button = msg_box.addButton("否,直接下载", QtWidgets.QMessageBox.ButtonRole.NoRole)
|
||||
|
||||
msg_box.exec()
|
||||
|
||||
use_optimization = msg_box.clickedButton() == yes_button
|
||||
|
||||
if use_optimization and not self.optimization_done:
|
||||
first_url = self.download_queue[0][0]
|
||||
self._start_ip_optimization(first_url)
|
||||
else:
|
||||
# 如果用户选择不优化,或已经优化过,直接开始下载
|
||||
self.next_download_task()
|
||||
|
||||
def _fill_download_queue(self, config):
|
||||
"""填充下载队列
|
||||
|
||||
Args:
|
||||
config: 包含下载URL的配置字典
|
||||
"""
|
||||
# 清空现有队列
|
||||
self.download_queue.clear()
|
||||
|
||||
# 创建下载历史记录列表,用于跟踪本次安装的游戏
|
||||
if not hasattr(self.main_window, 'download_queue_history'):
|
||||
self.main_window.download_queue_history = []
|
||||
|
||||
# 获取所有识别到的游戏目录
|
||||
game_dirs = self.main_window.identify_game_directories_improved(self.selected_folder)
|
||||
|
||||
debug_mode = self.is_debug_mode()
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 填充下载队列, 识别到的游戏目录: {game_dirs}")
|
||||
|
||||
# 添加nekopara 1-4
|
||||
for i in range(1, 5):
|
||||
game_version = f"NEKOPARA Vol.{i}"
|
||||
if not self.main_window.installed_status.get(game_version, False):
|
||||
url = config.get(f"vol{i}")
|
||||
if not url: continue
|
||||
|
||||
# 确定游戏文件夹路径
|
||||
if game_version in game_dirs:
|
||||
game_folder = game_dirs[game_version]
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 使用识别到的游戏目录 {game_version}: {game_folder}")
|
||||
else:
|
||||
# 回退到传统方式
|
||||
game_folder = os.path.join(self.selected_folder, f"NEKOPARA Vol. {i}")
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 使用默认游戏目录 {game_version}: {game_folder}")
|
||||
|
||||
_7z_path = os.path.join(PLUGIN, f"vol.{i}.7z")
|
||||
plugin_path = os.path.join(PLUGIN, GAME_INFO[game_version]["plugin_path"])
|
||||
self.download_queue.append((url, game_folder, game_version, _7z_path, plugin_path))
|
||||
# 记录到下载历史
|
||||
self.main_window.download_queue_history.append(game_version)
|
||||
|
||||
# 添加nekopara after
|
||||
game_version = "NEKOPARA After"
|
||||
if not self.main_window.installed_status.get(game_version, False):
|
||||
url = config.get("after")
|
||||
if url:
|
||||
# 确定After的游戏文件夹路径
|
||||
if game_version in game_dirs:
|
||||
game_folder = game_dirs[game_version]
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 使用识别到的游戏目录 {game_version}: {game_folder}")
|
||||
else:
|
||||
game_folder = os.path.join(self.selected_folder, "NEKOPARA After")
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 使用默认游戏目录 {game_version}: {game_folder}")
|
||||
|
||||
_7z_path = os.path.join(PLUGIN, "after.7z")
|
||||
plugin_path = os.path.join(PLUGIN, GAME_INFO[game_version]["plugin_path"])
|
||||
self.download_queue.append((url, game_folder, game_version, _7z_path, plugin_path))
|
||||
# 记录到下载历史
|
||||
self.main_window.download_queue_history.append(game_version)
|
||||
|
||||
def _start_ip_optimization(self, url):
|
||||
"""开始IP优化过程
|
||||
|
||||
Args:
|
||||
url: 用于优化的URL
|
||||
"""
|
||||
# 禁用退出按钮
|
||||
self.main_window.ui.exit_btn.setEnabled(False)
|
||||
|
||||
# 使用Cloudflare图标创建消息框
|
||||
|
||||
self.optimizing_msg_box = msgbox_frame(
|
||||
f"通知 - {APP_NAME}",
|
||||
"\n正在优选Cloudflare IP,请稍候...\n\n这可能需要5-10分钟,请耐心等待喵~"
|
||||
)
|
||||
# 设置Cloudflare图标
|
||||
cf_icon_path = resource_path("IMG/ICO/cloudflare_logo_icon.ico")
|
||||
if os.path.exists(cf_icon_path):
|
||||
cf_pixmap = QPixmap(cf_icon_path)
|
||||
if not cf_pixmap.isNull():
|
||||
self.optimizing_msg_box.setWindowIcon(QIcon(cf_pixmap))
|
||||
self.optimizing_msg_box.setIconPixmap(cf_pixmap.scaled(64, 64, Qt.AspectRatioMode.KeepAspectRatio,
|
||||
Qt.TransformationMode.SmoothTransformation))
|
||||
|
||||
# 我们不再提供"跳过"按钮
|
||||
self.optimizing_msg_box.setStandardButtons(QtWidgets.QMessageBox.StandardButton.NoButton)
|
||||
self.optimizing_msg_box.setWindowModality(Qt.WindowModality.ApplicationModal)
|
||||
self.optimizing_msg_box.open()
|
||||
|
||||
# 创建并启动优化线程
|
||||
self.ip_optimizer_thread = IpOptimizerThread(url)
|
||||
self.ip_optimizer_thread.finished.connect(self.on_optimization_finished)
|
||||
self.ip_optimizer_thread.start()
|
||||
|
||||
def on_optimization_finished(self, ip):
|
||||
"""IP优化完成后的处理
|
||||
|
||||
Args:
|
||||
ip: 优选的IP地址,如果失败则为空字符串
|
||||
"""
|
||||
self.optimized_ip = ip
|
||||
self.optimization_done = True
|
||||
|
||||
# 关闭提示框
|
||||
if hasattr(self, 'optimizing_msg_box') and self.optimizing_msg_box:
|
||||
if self.optimizing_msg_box.isVisible():
|
||||
self.optimizing_msg_box.accept()
|
||||
self.optimizing_msg_box = None
|
||||
|
||||
# 显示优选结果
|
||||
if not ip:
|
||||
msg_box = QtWidgets.QMessageBox(self.main_window)
|
||||
msg_box.setWindowTitle(f"优选失败 - {APP_NAME}")
|
||||
msg_box.setText("\n未能找到合适的Cloudflare IP,将使用默认网络进行下载。\n\n10秒后自动继续...")
|
||||
msg_box.setIcon(QtWidgets.QMessageBox.Icon.Warning)
|
||||
ok_button = msg_box.addButton("确定 (10)", QtWidgets.QMessageBox.ButtonRole.AcceptRole)
|
||||
|
||||
# 创建计时器实现倒计时
|
||||
countdown = 10
|
||||
timer = QtCore.QTimer(self.main_window)
|
||||
|
||||
def update_countdown():
|
||||
nonlocal countdown
|
||||
countdown -= 1
|
||||
ok_button.setText(f"确定 ({countdown})")
|
||||
if countdown <= 0:
|
||||
timer.stop()
|
||||
if msg_box.isVisible():
|
||||
msg_box.accept()
|
||||
|
||||
timer.timeout.connect(update_countdown)
|
||||
timer.start(1000) # 每秒更新一次
|
||||
|
||||
# 显示对话框,但不阻塞主线程
|
||||
msg_box.open()
|
||||
|
||||
# 连接关闭信号以停止计时器
|
||||
msg_box.finished.connect(timer.stop)
|
||||
else:
|
||||
# 应用优选IP到hosts文件
|
||||
if self.download_queue:
|
||||
first_url = self.download_queue[0][0]
|
||||
hostname = urlparse(first_url).hostname
|
||||
|
||||
# 先清理可能存在的旧记录
|
||||
self.hosts_manager.clean_hostname_entries(hostname)
|
||||
|
||||
if self.hosts_manager.apply_ip(hostname, ip):
|
||||
msg_box = QtWidgets.QMessageBox(self.main_window)
|
||||
msg_box.setWindowTitle(f"成功 - {APP_NAME}")
|
||||
msg_box.setText(f"\n已将优选IP ({ip}) 应用到hosts文件。\n\n10秒后自动继续...")
|
||||
msg_box.setIcon(QtWidgets.QMessageBox.Icon.Information)
|
||||
ok_button = msg_box.addButton("确定 (10)", QtWidgets.QMessageBox.ButtonRole.AcceptRole)
|
||||
|
||||
# 创建计时器实现倒计时
|
||||
countdown = 10
|
||||
timer = QtCore.QTimer(self.main_window)
|
||||
|
||||
def update_countdown():
|
||||
nonlocal countdown
|
||||
countdown -= 1
|
||||
ok_button.setText(f"确定 ({countdown})")
|
||||
if countdown <= 0:
|
||||
timer.stop()
|
||||
if msg_box.isVisible():
|
||||
msg_box.accept()
|
||||
|
||||
timer.timeout.connect(update_countdown)
|
||||
timer.start(1000) # 每秒更新一次
|
||||
|
||||
# 显示对话框,但不阻塞主线程
|
||||
msg_box.open()
|
||||
|
||||
# 连接关闭信号以停止计时器
|
||||
msg_box.finished.connect(timer.stop)
|
||||
else:
|
||||
QtWidgets.QMessageBox.critical(
|
||||
self.main_window,
|
||||
f"错误 - {APP_NAME}",
|
||||
"\n修改hosts文件失败,请检查程序是否以管理员权限运行。\n"
|
||||
)
|
||||
|
||||
# 计时器结束或用户点击确定时,继续下载
|
||||
QtCore.QTimer.singleShot(10000, self.next_download_task)
|
||||
|
||||
def next_download_task(self):
|
||||
"""处理下载队列中的下一个任务"""
|
||||
if not self.download_queue:
|
||||
self.main_window.after_hash_compare()
|
||||
return
|
||||
|
||||
# 检查下载线程是否仍在运行,以避免在手动停止后立即开始下一个任务
|
||||
if self.current_download_thread and self.current_download_thread.isRunning():
|
||||
return
|
||||
|
||||
# 获取下一个下载任务并开始
|
||||
url, game_folder, game_version, _7z_path, plugin_path = self.download_queue.popleft()
|
||||
self.download_setting(url, game_folder, game_version, _7z_path, plugin_path)
|
||||
|
||||
def download_setting(self, url, game_folder, game_version, _7z_path, plugin_path):
|
||||
"""准备下载特定游戏版本
|
||||
|
||||
Args:
|
||||
url: 下载URL
|
||||
game_folder: 游戏文件夹路径
|
||||
game_version: 游戏版本名称
|
||||
_7z_path: 7z文件保存路径
|
||||
plugin_path: 插件路径
|
||||
"""
|
||||
# 使用改进的目录识别获取安装路径
|
||||
install_paths = self.get_install_paths()
|
||||
|
||||
debug_mode = self.is_debug_mode()
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 准备下载游戏 {game_version}")
|
||||
print(f"DEBUG: 游戏文件夹: {game_folder}")
|
||||
|
||||
# 获取游戏可执行文件路径
|
||||
game_dirs = self.main_window.identify_game_directories_improved(self.selected_folder)
|
||||
game_exe_exists = False
|
||||
|
||||
if game_version in game_dirs:
|
||||
game_dir = game_dirs[game_version]
|
||||
# 游戏目录已经通过可执行文件验证了,可以直接认为存在
|
||||
game_exe_exists = True
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 游戏目录已验证: {game_dir}")
|
||||
print(f"DEBUG: 游戏可执行文件存在: {game_exe_exists}")
|
||||
else:
|
||||
# 回退到传统方法检查游戏是否存在
|
||||
# 尝试多种可能的文件名格式
|
||||
expected_exe = GAME_INFO[game_version]["exe"]
|
||||
traditional_folder = os.path.join(
|
||||
self.selected_folder,
|
||||
GAME_INFO[game_version]["install_path"].split("/")[0]
|
||||
)
|
||||
|
||||
# 定义多种可能的可执行文件变体
|
||||
exe_variants = [
|
||||
expected_exe, # 标准文件名
|
||||
expected_exe + ".nocrack", # Steam加密版本
|
||||
expected_exe.replace(".exe", ""), # 无扩展名版本
|
||||
expected_exe.replace("NEKOPARA", "nekopara").lower(), # 全小写变体
|
||||
expected_exe.lower(), # 小写变体
|
||||
expected_exe.lower() + ".nocrack", # 小写变体的Steam加密版本
|
||||
]
|
||||
|
||||
# 对于Vol.3可能有特殊名称
|
||||
if "Vol.3" in game_version:
|
||||
# 增加可能的卷3特定的变体
|
||||
exe_variants.extend([
|
||||
"NEKOPARAVol3.exe",
|
||||
"NEKOPARAVol3.exe.nocrack",
|
||||
"nekoparavol3.exe",
|
||||
"nekoparavol3.exe.nocrack",
|
||||
"nekopara_vol3.exe",
|
||||
"nekopara_vol3.exe.nocrack",
|
||||
"vol3.exe",
|
||||
"vol3.exe.nocrack"
|
||||
])
|
||||
|
||||
# 检查所有可能的文件名
|
||||
for exe_variant in exe_variants:
|
||||
exe_path = os.path.join(traditional_folder, exe_variant)
|
||||
if os.path.exists(exe_path):
|
||||
game_exe_exists = True
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 找到游戏可执行文件: {exe_path}")
|
||||
break
|
||||
|
||||
# 如果仍未找到,尝试递归搜索
|
||||
if not game_exe_exists and os.path.exists(traditional_folder):
|
||||
# 提取卷号或检查是否是After
|
||||
vol_match = re.search(r"Vol\.(\d+)", game_version)
|
||||
vol_num = None
|
||||
if vol_match:
|
||||
vol_num = vol_match.group(1)
|
||||
|
||||
is_after = "After" in game_version
|
||||
|
||||
# 遍历游戏目录及其子目录
|
||||
for root, dirs, files in os.walk(traditional_folder):
|
||||
for file in files:
|
||||
file_lower = file.lower()
|
||||
if file.endswith('.exe') or file.endswith('.exe.nocrack'):
|
||||
# 检查文件名中是否包含卷号或关键词
|
||||
if ((vol_num and (f"vol{vol_num}" in file_lower or
|
||||
f"vol.{vol_num}" in file_lower or
|
||||
f"vol {vol_num}" in file_lower)) or
|
||||
(is_after and "after" in file_lower)):
|
||||
game_exe_exists = True
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 通过递归搜索找到游戏可执行文件: {os.path.join(root, file)}")
|
||||
break
|
||||
if game_exe_exists:
|
||||
break
|
||||
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 使用传统方法检查游戏目录: {traditional_folder}")
|
||||
print(f"DEBUG: 游戏可执行文件存在: {game_exe_exists}")
|
||||
|
||||
# 检查游戏是否已安装
|
||||
if (
|
||||
not game_exe_exists
|
||||
or self.main_window.installed_status[game_version]
|
||||
):
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 跳过下载游戏 {game_version}")
|
||||
print(f"DEBUG: 游戏存在: {game_exe_exists}")
|
||||
print(f"DEBUG: 已安装补丁: {self.main_window.installed_status[game_version]}")
|
||||
self.main_window.installed_status[game_version] = False if not game_exe_exists else True
|
||||
self.next_download_task()
|
||||
return
|
||||
|
||||
# 创建进度窗口并开始下载
|
||||
self.main_window.progress_window = self.main_window.create_progress_window()
|
||||
self.start_download(url, _7z_path, game_version, game_folder, plugin_path)
|
||||
|
||||
def start_download(self, url, _7z_path, game_version, game_folder, plugin_path):
|
||||
"""启动下载线程
|
||||
|
||||
Args:
|
||||
url: 下载URL
|
||||
_7z_path: 7z文件保存路径
|
||||
game_version: 游戏版本名称
|
||||
game_folder: 游戏文件夹路径
|
||||
plugin_path: 插件路径
|
||||
"""
|
||||
# 禁用退出按钮
|
||||
self.main_window.ui.exit_btn.setEnabled(False)
|
||||
|
||||
if self.optimized_ip:
|
||||
print(f"已为 {game_version} 获取到优选IP: {self.optimized_ip}")
|
||||
else:
|
||||
print(f"未能为 {game_version} 获取优选IP,将使用默认线路。")
|
||||
|
||||
# 创建并连接下载线程
|
||||
self.current_download_thread = self.main_window.create_download_thread(url, _7z_path, game_version)
|
||||
self.current_download_thread.progress.connect(self.main_window.progress_window.update_progress)
|
||||
self.current_download_thread.finished.connect(
|
||||
lambda success, error: self.on_download_finished(
|
||||
success,
|
||||
error,
|
||||
url,
|
||||
game_folder,
|
||||
game_version,
|
||||
_7z_path,
|
||||
plugin_path,
|
||||
)
|
||||
)
|
||||
|
||||
# 连接停止按钮
|
||||
self.main_window.progress_window.stop_button.clicked.connect(self.current_download_thread.stop)
|
||||
|
||||
# 启动线程和显示进度窗口
|
||||
self.current_download_thread.start()
|
||||
self.main_window.progress_window.exec()
|
||||
|
||||
def on_download_finished(self, success, error, url, game_folder, game_version, _7z_path, plugin_path):
|
||||
"""下载完成后的处理
|
||||
|
||||
Args:
|
||||
success: 是否下载成功
|
||||
error: 错误信息
|
||||
url: 下载URL
|
||||
game_folder: 游戏文件夹路径
|
||||
game_version: 游戏版本名称
|
||||
_7z_path: 7z文件保存路径
|
||||
plugin_path: 插件路径
|
||||
"""
|
||||
# 关闭进度窗口
|
||||
if self.main_window.progress_window.isVisible():
|
||||
self.main_window.progress_window.reject()
|
||||
|
||||
# 处理下载失败
|
||||
if not success:
|
||||
print(f"--- Download Failed: {game_version} ---")
|
||||
print(error)
|
||||
print("------------------------------------")
|
||||
msg_box = QtWidgets.QMessageBox(self.main_window)
|
||||
msg_box.setWindowTitle(f"下载失败 - {APP_NAME}")
|
||||
msg_box.setText(f"\n文件获取失败: {game_version}\n错误: {error}\n\n是否重试?")
|
||||
|
||||
retry_button = msg_box.addButton("重试", QtWidgets.QMessageBox.ButtonRole.YesRole)
|
||||
next_button = msg_box.addButton("下一个", QtWidgets.QMessageBox.ButtonRole.NoRole)
|
||||
end_button = msg_box.addButton("结束", QtWidgets.QMessageBox.ButtonRole.RejectRole)
|
||||
|
||||
msg_box.exec()
|
||||
clicked_button = msg_box.clickedButton()
|
||||
|
||||
# 处理用户选择
|
||||
if clicked_button == retry_button:
|
||||
self.download_setting(url, game_folder, game_version, _7z_path, plugin_path)
|
||||
elif clicked_button == next_button:
|
||||
self.next_download_task()
|
||||
else:
|
||||
self.on_download_stopped()
|
||||
return
|
||||
|
||||
# 下载成功,开始解压缩
|
||||
self.main_window.hash_msg_box = self.main_window.hash_manager.hash_pop_window(check_type="extraction")
|
||||
|
||||
# 创建并启动解压线程
|
||||
self.main_window.extraction_thread = self.main_window.create_extraction_thread(
|
||||
_7z_path, game_folder, plugin_path, game_version
|
||||
)
|
||||
self.main_window.extraction_thread.finished.connect(self.on_extraction_finished)
|
||||
self.main_window.extraction_thread.start()
|
||||
|
||||
def on_extraction_finished(self, success, error_message, game_version):
|
||||
"""解压完成后的处理
|
||||
|
||||
Args:
|
||||
success: 是否解压成功
|
||||
error_message: 错误信息
|
||||
game_version: 游戏版本
|
||||
"""
|
||||
# 关闭哈希检查窗口
|
||||
if self.main_window.hash_msg_box and self.main_window.hash_msg_box.isVisible():
|
||||
self.main_window.hash_msg_box.close()
|
||||
|
||||
# 处理解压结果
|
||||
if not success:
|
||||
QtWidgets.QMessageBox.critical(self.main_window, f"错误 - {APP_NAME}", error_message)
|
||||
self.main_window.installed_status[game_version] = False
|
||||
else:
|
||||
self.main_window.installed_status[game_version] = True
|
||||
|
||||
# 继续下一个下载任务
|
||||
self.next_download_task()
|
||||
|
||||
def on_download_stopped(self):
|
||||
"""当用户点击停止按钮或选择结束时调用的函数"""
|
||||
# 停止IP优化线程
|
||||
if hasattr(self, 'ip_optimizer_thread') and self.ip_optimizer_thread and self.ip_optimizer_thread.isRunning():
|
||||
self.ip_optimizer_thread.stop()
|
||||
self.ip_optimizer_thread.wait()
|
||||
if hasattr(self, 'optimizing_msg_box') and self.optimizing_msg_box:
|
||||
if self.optimizing_msg_box.isVisible():
|
||||
self.optimizing_msg_box.accept()
|
||||
self.optimizing_msg_box = None
|
||||
|
||||
# 停止当前可能仍在运行的下载线程
|
||||
if self.current_download_thread and self.current_download_thread.isRunning():
|
||||
self.current_download_thread.stop()
|
||||
self.current_download_thread.wait() # 等待线程完全终止
|
||||
|
||||
# 清空下载队列,因为用户决定停止
|
||||
self.download_queue.clear()
|
||||
|
||||
# 确保进度窗口已关闭
|
||||
if hasattr(self.main_window, 'progress_window') and self.main_window.progress_window.isVisible():
|
||||
self.main_window.progress_window.reject()
|
||||
|
||||
# 可以在这里决定是否立即进行哈希比较或显示结果
|
||||
print("下载已全部停止。")
|
||||
self.main_window.setEnabled(True) # 恢复主窗口交互
|
||||
|
||||
# 重新启用退出按钮和开始安装按钮
|
||||
self.main_window.ui.exit_btn.setEnabled(True)
|
||||
self.main_window.set_start_button_enabled(True)
|
||||
|
||||
self.main_window.show_result()
|
||||
111
source/core/ui_manager.py
Normal file
@@ -0,0 +1,111 @@
|
||||
from PySide6.QtGui import QIcon, QAction, QFont
|
||||
from PySide6.QtWidgets import QMessageBox, QMainWindow
|
||||
from PySide6.QtCore import Qt
|
||||
import webbrowser
|
||||
|
||||
from utils import load_base64_image, msgbox_frame
|
||||
from data.config import APP_NAME, APP_VERSION
|
||||
|
||||
class UIManager:
|
||||
def __init__(self, main_window):
|
||||
"""初始化UI管理器
|
||||
|
||||
Args:
|
||||
main_window: 主窗口实例,用于设置UI元素
|
||||
"""
|
||||
self.main_window = main_window
|
||||
# 使用getattr获取ui属性,如果不存在则为None
|
||||
self.ui = getattr(main_window, 'ui', None)
|
||||
self.debug_action = None
|
||||
|
||||
def setup_ui(self):
|
||||
"""设置UI元素,包括窗口图标、标题和菜单"""
|
||||
# 设置窗口图标
|
||||
import os
|
||||
from utils import resource_path
|
||||
icon_path = resource_path(os.path.join("IMG", "ICO", "icon.png"))
|
||||
if os.path.exists(icon_path):
|
||||
self.main_window.setWindowIcon(QIcon(icon_path))
|
||||
|
||||
# 设置窗口标题
|
||||
self.main_window.setWindowTitle(f"{APP_NAME} v{APP_VERSION}")
|
||||
|
||||
# 设置菜单
|
||||
self._setup_help_menu()
|
||||
self._setup_settings_menu()
|
||||
|
||||
def _setup_help_menu(self):
|
||||
"""设置"帮助"菜单"""
|
||||
if not self.ui or not hasattr(self.ui, 'menu_2'):
|
||||
return
|
||||
|
||||
# 创建菜单项
|
||||
project_home_action = QAction("项目主页", self.main_window)
|
||||
project_home_action.triggered.connect(self.open_project_home_page)
|
||||
|
||||
about_action = QAction("关于", self.main_window)
|
||||
about_action.triggered.connect(self.show_about_dialog)
|
||||
|
||||
# 添加到菜单
|
||||
self.ui.menu_2.addAction(project_home_action)
|
||||
self.ui.menu_2.addAction(about_action)
|
||||
|
||||
# 连接按钮点击事件,如果使用按钮式菜单
|
||||
if hasattr(self.ui, 'help_btn'):
|
||||
# 按钮已经连接到显示菜单,不需要额外处理
|
||||
pass
|
||||
|
||||
def _setup_settings_menu(self):
|
||||
"""设置"设置"菜单"""
|
||||
if not self.ui or not hasattr(self.ui, 'menu'):
|
||||
return
|
||||
|
||||
# 创建菜单项
|
||||
self.debug_action = QAction("Debug模式", self.main_window, checkable=True)
|
||||
|
||||
# 安全地获取config属性
|
||||
config = getattr(self.main_window, 'config', {})
|
||||
debug_mode = False
|
||||
if isinstance(config, dict):
|
||||
debug_mode = config.get("debug_mode", False)
|
||||
|
||||
self.debug_action.setChecked(debug_mode)
|
||||
|
||||
# 安全地连接toggle_debug_mode方法
|
||||
if hasattr(self.main_window, 'toggle_debug_mode'):
|
||||
self.debug_action.triggered.connect(self.main_window.toggle_debug_mode)
|
||||
|
||||
# 添加到菜单
|
||||
self.ui.menu.addAction(self.debug_action)
|
||||
|
||||
# 为未来功能预留的"切换下载源"按钮
|
||||
self.switch_source_action = QAction("切换下载源", self.main_window)
|
||||
self.switch_source_action.setEnabled(False) # 暂时禁用
|
||||
self.ui.menu.addAction(self.switch_source_action)
|
||||
|
||||
# 添加分隔符
|
||||
self.ui.menu.addSeparator()
|
||||
|
||||
# 连接按钮点击事件,如果使用按钮式菜单
|
||||
if hasattr(self.ui, 'settings_btn'):
|
||||
# 按钮已经连接到显示菜单,不需要额外处理
|
||||
pass
|
||||
|
||||
def open_project_home_page(self):
|
||||
"""打开项目主页"""
|
||||
webbrowser.open("https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT")
|
||||
|
||||
def show_about_dialog(self):
|
||||
"""显示关于对话框"""
|
||||
about_text = f"""
|
||||
<p><b>{APP_NAME} v{APP_VERSION}</b></p>
|
||||
<p>原作: <a href="https://github.com/Yanam1Anna">Yanam1Anna</a></p>
|
||||
<p>此应用根据 <a href="https://github.com/hyb-oyqq/FRAISEMOE2-Installer/blob/master/LICENSE">GPL-3.0 许可证</a> 授权。</p>
|
||||
"""
|
||||
msg_box = msgbox_frame(
|
||||
f"关于 - {APP_NAME}",
|
||||
about_text,
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
msg_box.setTextFormat(Qt.TextFormat.RichText) # 使用Qt.TextFormat
|
||||
msg_box.exec()
|
||||
@@ -3,7 +3,7 @@ import base64
|
||||
|
||||
# 配置信息
|
||||
app_data = {
|
||||
"APP_VERSION": "1.1.0",
|
||||
"APP_VERSION": "1.1.3",
|
||||
"APP_NAME": "FRAISEMOE Addons Installer NEXT",
|
||||
"TEMP": "TEMP",
|
||||
"CACHE": "FRAISEMOE",
|
||||
BIN
source/fonts/SmileySans-Oblique.ttf
Normal file
BIN
source/icon.ico
|
Before Width: | Height: | Size: 264 KiB |
@@ -1,333 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>MainWindows</class>
|
||||
<widget class="QMainWindow" name="MainWindows">
|
||||
<property name="enabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>1024</width>
|
||||
<height>576</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>1024</width>
|
||||
<height>576</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>1024</width>
|
||||
<height>576</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="mouseTracking">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="tabletTracking">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="acceptDrops">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string> UI Test</string>
|
||||
</property>
|
||||
<property name="autoFillBackground">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="toolButtonStyle">
|
||||
<enum>Qt::ToolButtonStyle::ToolButtonIconOnly</enum>
|
||||
</property>
|
||||
<property name="animated">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="documentMode">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="dockNestingEnabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<widget class="QWidget" name="centralwidget">
|
||||
<property name="autoFillBackground">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<widget class="QLabel" name="loadbg">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>1031</width>
|
||||
<height>561</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="pixmap">
|
||||
<pixmap>IMG/BG/bg2.jpg</pixmap>
|
||||
</property>
|
||||
<property name="scaledContents">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QLabel" name="vol1bg">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>120</y>
|
||||
<width>93</width>
|
||||
<height>64</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="pixmap">
|
||||
<pixmap>IMG/LOGO/vo01_logo.png</pixmap>
|
||||
</property>
|
||||
<property name="scaledContents">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QLabel" name="vol2bg">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>180</y>
|
||||
<width>93</width>
|
||||
<height>64</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="pixmap">
|
||||
<pixmap>IMG/LOGO/vo02_logo.png</pixmap>
|
||||
</property>
|
||||
<property name="scaledContents">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QLabel" name="vol3bg">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>240</y>
|
||||
<width>93</width>
|
||||
<height>64</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="pixmap">
|
||||
<pixmap>IMG/LOGO/vo03_logo.png</pixmap>
|
||||
</property>
|
||||
<property name="scaledContents">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QLabel" name="vol4bg">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>300</y>
|
||||
<width>93</width>
|
||||
<height>64</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="pixmap">
|
||||
<pixmap>IMG/LOGO/vo04_logo.png</pixmap>
|
||||
</property>
|
||||
<property name="scaledContents">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QLabel" name="afterbg">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>360</y>
|
||||
<width>93</width>
|
||||
<height>64</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="pixmap">
|
||||
<pixmap>IMG/LOGO/voaf_logo.png</pixmap>
|
||||
</property>
|
||||
<property name="scaledContents">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QLabel" name="Mainbg">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>1031</width>
|
||||
<height>561</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="pixmap">
|
||||
<pixmap>IMG/BG/bg3.jpg</pixmap>
|
||||
</property>
|
||||
<property name="scaledContents">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QPushButton" name="start_install_btn">
|
||||
<property name="enabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>780</x>
|
||||
<y>250</y>
|
||||
<width>191</width>
|
||||
<height>91</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="accessibleDescription">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="autoFillBackground">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset>
|
||||
<normaloff>IMG/BTN/start_install.bmp</normaloff>IMG/BTN/start_install.bmp</iconset>
|
||||
</property>
|
||||
<property name="iconSize">
|
||||
<size>
|
||||
<width>189</width>
|
||||
<height>110</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="checkable">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="autoRepeat">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="autoDefault">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="flat">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QPushButton" name="exit_btn">
|
||||
<property name="enabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>780</x>
|
||||
<y>340</y>
|
||||
<width>191</width>
|
||||
<height>91</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="autoFillBackground">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset>
|
||||
<normaloff>IMG/BTN/exit.bmp</normaloff>IMG/BTN/exit.bmp</iconset>
|
||||
</property>
|
||||
<property name="iconSize">
|
||||
<size>
|
||||
<width>189</width>
|
||||
<height>110</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="checkable">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="flat">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QLabel" name="menubg">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>710</x>
|
||||
<y>0</y>
|
||||
<width>321</width>
|
||||
<height>561</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="pixmap">
|
||||
<pixmap>IMG/BG/menubg.jpg</pixmap>
|
||||
</property>
|
||||
<property name="scaledContents">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
<zorder>loadbg</zorder>
|
||||
<zorder>vol1bg</zorder>
|
||||
<zorder>vol2bg</zorder>
|
||||
<zorder>vol3bg</zorder>
|
||||
<zorder>vol4bg</zorder>
|
||||
<zorder>afterbg</zorder>
|
||||
<zorder>Mainbg</zorder>
|
||||
<zorder>menubg</zorder>
|
||||
<zorder>start_install_btn</zorder>
|
||||
<zorder>exit_btn</zorder>
|
||||
</widget>
|
||||
<widget class="QMenuBar" name="menubar">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>1024</width>
|
||||
<height>21</height>
|
||||
</rect>
|
||||
</property>
|
||||
<widget class="QMenu" name="menu">
|
||||
<property name="title">
|
||||
<string>设置</string>
|
||||
</property>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="action_2"/>
|
||||
</widget>
|
||||
<widget class="QMenu" name="menu_2">
|
||||
<property name="title">
|
||||
<string>关于</string>
|
||||
</property>
|
||||
</widget>
|
||||
<addaction name="menu"/>
|
||||
<addaction name="menu_2"/>
|
||||
</widget>
|
||||
<action name="action_2">
|
||||
<property name="text">
|
||||
<string>update - sd</string>
|
||||
</property>
|
||||
</action>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -1,19 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Form</class>
|
||||
<widget class="QWidget" name="Form">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>480</width>
|
||||
<height>270</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
510
source/ui/Ui_install.py
Normal file
@@ -0,0 +1,510 @@
|
||||
from PySide6.QtGui import QPixmap
|
||||
import base64
|
||||
from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
|
||||
QMetaObject, QObject, QPoint, QRect,
|
||||
QSize, QTime, QUrl, Qt)
|
||||
from PySide6.QtGui import (QAction, QBrush, QColor, QConicalGradient,
|
||||
QCursor, QFont, QFontDatabase, QGradient,
|
||||
QIcon, QImage, QKeySequence, QLinearGradient,
|
||||
QPainter, QPalette, QPixmap, QRadialGradient,
|
||||
QTransform, QPainterPath, QRegion)
|
||||
from PySide6.QtWidgets import (QApplication, QLabel, QMainWindow, QMenu,
|
||||
QMenuBar, QPushButton, QSizePolicy, QWidget, QHBoxLayout)
|
||||
import os
|
||||
|
||||
# 导入配置常量
|
||||
from data.config import APP_NAME, APP_VERSION
|
||||
from utils import load_image_from_file
|
||||
|
||||
class Ui_MainWindows(object):
|
||||
def setupUi(self, MainWindows):
|
||||
if not MainWindows.objectName():
|
||||
MainWindows.setObjectName(u"MainWindows")
|
||||
MainWindows.setEnabled(True)
|
||||
# 调整窗口默认大小为1280x720以匹配背景图片
|
||||
MainWindows.resize(1280, 720)
|
||||
# 锁定窗口比例为16:9,确保不会变形,同时限制最大尺寸不超过背景图片
|
||||
MainWindows.setMinimumSize(QSize(1024, 576)) # 16:9最小尺寸
|
||||
MainWindows.setMaximumSize(QSize(1280, 720)) # 将最大尺寸限制为1280x720
|
||||
# 设置固定纵横比
|
||||
self.aspect_ratio = 16/9
|
||||
MainWindows.setMouseTracking(False)
|
||||
MainWindows.setTabletTracking(False)
|
||||
MainWindows.setAcceptDrops(True)
|
||||
MainWindows.setAutoFillBackground(False)
|
||||
MainWindows.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
|
||||
MainWindows.setAnimated(True)
|
||||
MainWindows.setDocumentMode(False)
|
||||
MainWindows.setDockNestingEnabled(False)
|
||||
|
||||
# 加载自定义字体
|
||||
font_id = QFontDatabase.addApplicationFont(os.path.join(os.path.dirname(os.path.dirname(__file__)), "fonts", "SmileySans-Oblique.ttf"))
|
||||
font_family = QFontDatabase.applicationFontFamilies(font_id)[0] if font_id != -1 else "Arial"
|
||||
self.custom_font = QFont(font_family, 16) # 创建字体对象,大小为16
|
||||
self.custom_font.setWeight(QFont.Weight.Medium) # 设置为中等粗细,不要太粗
|
||||
|
||||
self.centralwidget = QWidget(MainWindows)
|
||||
self.centralwidget.setObjectName(u"centralwidget")
|
||||
self.centralwidget.setAutoFillBackground(False) # 修改为False以支持透明背景
|
||||
self.centralwidget.setStyleSheet("""
|
||||
QWidget#centralwidget {
|
||||
background-color: transparent;
|
||||
}
|
||||
""")
|
||||
|
||||
# 圆角背景容器
|
||||
self.main_container = QWidget(self.centralwidget)
|
||||
self.main_container.setObjectName(u"main_container")
|
||||
self.main_container.setGeometry(QRect(0, 0, 1280, 720))
|
||||
self.main_container.setStyleSheet("""
|
||||
QWidget#main_container {
|
||||
background-color: #E96948;
|
||||
border-radius: 20px;
|
||||
border: 1px solid #E96948;
|
||||
}
|
||||
""")
|
||||
|
||||
# 内容容器 - 用于限制内容在圆角范围内
|
||||
self.content_container = QWidget(self.main_container)
|
||||
self.content_container.setObjectName(u"content_container")
|
||||
self.content_container.setGeometry(QRect(0, 0, 1280, 720))
|
||||
self.content_container.setStyleSheet("""
|
||||
QWidget#content_container {
|
||||
background-color: transparent;
|
||||
border-radius: 20px;
|
||||
}
|
||||
""")
|
||||
|
||||
# 添加圆角裁剪,确保内容在圆角范围内
|
||||
rect = QRect(0, 0, 1280, 720)
|
||||
path = QPainterPath()
|
||||
path.addRoundedRect(rect, 20, 20)
|
||||
region = QRegion(path.toFillPolygon().toPolygon())
|
||||
self.content_container.setMask(region)
|
||||
|
||||
# 标题栏
|
||||
self.title_bar = QWidget(self.content_container)
|
||||
self.title_bar.setObjectName(u"title_bar")
|
||||
self.title_bar.setGeometry(QRect(0, 0, 1280, 35)) # 减小高度从40到35
|
||||
self.title_bar.setStyleSheet("""
|
||||
QWidget#title_bar {
|
||||
background-color: #E96948;
|
||||
border-top-left-radius: 20px;
|
||||
border-top-right-radius: 20px;
|
||||
border-bottom: 1px solid #F47A5B;
|
||||
}
|
||||
""")
|
||||
|
||||
# 标题栏布局
|
||||
self.title_layout = QHBoxLayout(self.title_bar)
|
||||
self.title_layout.setSpacing(10)
|
||||
self.title_layout.setContentsMargins(10, 0, 10, 0)
|
||||
|
||||
# 添加最小化和关闭按钮到标题栏
|
||||
self.minimize_btn = QPushButton(self.title_bar)
|
||||
self.minimize_btn.setObjectName(u"minimize_btn")
|
||||
self.minimize_btn.setMinimumSize(QSize(24, 24))
|
||||
self.minimize_btn.setMaximumSize(QSize(24, 24))
|
||||
self.minimize_btn.setCursor(QCursor(Qt.CursorShape.PointingHandCursor))
|
||||
self.minimize_btn.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #FFC107;
|
||||
border-radius: 12px;
|
||||
border: none;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #FFD54F;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #FFA000;
|
||||
}
|
||||
""")
|
||||
self.minimize_btn.setText("—")
|
||||
self.minimize_btn.setFont(QFont(font_family, 10))
|
||||
|
||||
self.close_btn = QPushButton(self.title_bar)
|
||||
self.close_btn.setObjectName(u"close_btn")
|
||||
self.close_btn.setMinimumSize(QSize(24, 24))
|
||||
self.close_btn.setMaximumSize(QSize(24, 24))
|
||||
self.close_btn.setCursor(QCursor(Qt.CursorShape.PointingHandCursor))
|
||||
self.close_btn.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #F44336;
|
||||
border-radius: 12px;
|
||||
border: none;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #EF5350;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #D32F2F;
|
||||
}
|
||||
""")
|
||||
self.close_btn.setText("×")
|
||||
self.close_btn.setFont(QFont(font_family, 14))
|
||||
|
||||
# 标题文本
|
||||
self.title_label = QLabel(self.title_bar)
|
||||
self.title_label.setObjectName(u"title_label")
|
||||
# 直接使用APP_NAME并添加版本号
|
||||
self.title_label.setText(f"{APP_NAME} v{APP_VERSION}")
|
||||
title_font = QFont(font_family, 14) # 减小字体从16到14
|
||||
title_font.setBold(True)
|
||||
self.title_label.setFont(title_font)
|
||||
self.title_label.setStyleSheet("color: #333333; padding-left: 10px;")
|
||||
self.title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
# 添加按钮到标题栏布局
|
||||
self.title_layout.addWidget(self.title_label)
|
||||
self.title_layout.addStretch(1)
|
||||
self.title_layout.addWidget(self.minimize_btn)
|
||||
self.title_layout.addSpacing(5)
|
||||
self.title_layout.addWidget(self.close_btn)
|
||||
|
||||
# 修改菜单区域 - 确保足够宽以容纳更多菜单项
|
||||
self.menu_area = QWidget(self.content_container)
|
||||
self.menu_area.setObjectName(u"menu_area")
|
||||
self.menu_area.setGeometry(QRect(0, 35, 1280, 30)) # 调整位置从40到35,高度从35到30
|
||||
self.menu_area.setStyleSheet("""
|
||||
QWidget#menu_area {
|
||||
background-color: #E96948;
|
||||
}
|
||||
""")
|
||||
|
||||
# 不再使用菜单栏,改用普通按钮
|
||||
# 创建菜单按钮字体
|
||||
menu_font = QFont(font_family, 14) # 进一步减小字体大小到14
|
||||
menu_font.setBold(True)
|
||||
|
||||
# 设置按钮
|
||||
self.settings_btn = QPushButton("设置", self.menu_area)
|
||||
self.settings_btn.setObjectName(u"settings_btn")
|
||||
self.settings_btn.setGeometry(QRect(20, 1, 80, 28)) # 调整高度和Y位置
|
||||
self.settings_btn.setFont(menu_font)
|
||||
self.settings_btn.setCursor(QCursor(Qt.CursorShape.PointingHandCursor))
|
||||
self.settings_btn.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: transparent;
|
||||
color: white;
|
||||
border: none;
|
||||
text-align: left;
|
||||
padding-left: 10px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #F47A5B;
|
||||
border-radius: 4px;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #D25A3C;
|
||||
border-radius: 4px;
|
||||
}
|
||||
""")
|
||||
|
||||
# 帮助按钮
|
||||
self.help_btn = QPushButton("帮助", self.menu_area)
|
||||
self.help_btn.setObjectName(u"help_btn")
|
||||
self.help_btn.setGeometry(QRect(120, 1, 80, 28)) # 调整高度和Y位置
|
||||
self.help_btn.setFont(menu_font)
|
||||
self.help_btn.setCursor(QCursor(Qt.CursorShape.PointingHandCursor))
|
||||
self.help_btn.setStyleSheet(self.settings_btn.styleSheet())
|
||||
|
||||
# 将原来的菜单项移到全局,方便访问
|
||||
self.menu = QMenu(self.content_container)
|
||||
self.menu.setObjectName(u"menu")
|
||||
self.menu.setTitle("设置")
|
||||
self.menu.setFont(menu_font)
|
||||
self.menu.setStyleSheet("""
|
||||
QMenu {
|
||||
background-color: #E96948;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
border: 1px solid #F47A5B;
|
||||
padding: 8px;
|
||||
border-radius: 6px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
QMenu::item {
|
||||
padding: 6px 20px 6px 15px;
|
||||
background-color: transparent;
|
||||
min-width: 120px;
|
||||
color: white;
|
||||
}
|
||||
QMenu::item:selected {
|
||||
background-color: #F47A5B;
|
||||
border-radius: 4px;
|
||||
}
|
||||
QMenu::separator {
|
||||
height: 1px;
|
||||
background-color: #F47A5B;
|
||||
margin: 5px 15px;
|
||||
}
|
||||
""")
|
||||
|
||||
self.menu_2 = QMenu(self.content_container)
|
||||
self.menu_2.setObjectName(u"menu_2")
|
||||
self.menu_2.setTitle("帮助")
|
||||
self.menu_2.setFont(menu_font)
|
||||
self.menu_2.setStyleSheet(self.menu.styleSheet())
|
||||
|
||||
# 连接按钮点击事件到显示对应菜单
|
||||
self.settings_btn.clicked.connect(lambda: self.show_menu(self.menu, self.settings_btn))
|
||||
self.help_btn.clicked.connect(lambda: self.show_menu(self.menu_2, self.help_btn))
|
||||
|
||||
# 预留位置给未来可能的第三个按钮
|
||||
# 第三个按钮可以这样添加:
|
||||
# self.third_btn = QPushButton("第三项", self.menu_area)
|
||||
# self.third_btn.setObjectName(u"third_btn")
|
||||
# self.third_btn.setGeometry(QRect(320, 0, 120, 35))
|
||||
# self.third_btn.setFont(menu_font)
|
||||
# self.third_btn.setCursor(QCursor(Qt.CursorShape.PointingHandCursor))
|
||||
# self.third_btn.setStyleSheet(self.settings_btn.styleSheet())
|
||||
# self.third_btn.clicked.connect(lambda: self.show_menu(self.menu_3, self.third_btn))
|
||||
|
||||
# 内容子容器
|
||||
self.inner_content = QWidget(self.content_container)
|
||||
self.inner_content.setObjectName(u"inner_content")
|
||||
# 确保宽度足够大,保证右侧元素完全显示
|
||||
self.inner_content.setGeometry(QRect(0, 65, 1280, 655)) # 调整Y位置从75到65,高度从645到655
|
||||
self.inner_content.setStyleSheet("""
|
||||
QWidget#inner_content {
|
||||
background-color: transparent;
|
||||
border-bottom-left-radius: 20px;
|
||||
border-bottom-right-radius: 20px;
|
||||
}
|
||||
""")
|
||||
|
||||
# 添加底部圆角裁剪
|
||||
inner_rect = QRect(0, 0, 1280, 665)
|
||||
inner_path = QPainterPath()
|
||||
inner_path.addRoundedRect(inner_rect, 20, 20)
|
||||
inner_region = QRegion(inner_path.toFillPolygon().toPolygon())
|
||||
self.inner_content.setMask(inner_region)
|
||||
|
||||
# 在主容器中添加背景和内容元素
|
||||
# 修改loadbg使用title_bg1.png作为整个背景
|
||||
# 原来的loadbg保持不变
|
||||
self.loadbg = QLabel(self.inner_content)
|
||||
self.loadbg.setObjectName(u"loadbg")
|
||||
self.loadbg.setGeometry(QRect(0, 0, 1280, 655))
|
||||
# 加载背景图并允许拉伸
|
||||
bg_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "IMG", "BG", "bg1.jpg")
|
||||
bg_pixmap = QPixmap(bg_path)
|
||||
self.loadbg.setPixmap(bg_pixmap)
|
||||
self.loadbg.setScaledContents(True)
|
||||
|
||||
self.vol1bg = QLabel(self.inner_content)
|
||||
self.vol1bg.setObjectName(u"vol1bg")
|
||||
self.vol1bg.setGeometry(QRect(0, 150, 93, 64))
|
||||
# 直接加载图片文件
|
||||
vol1_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "IMG", "LOGO", "vo01_logo.png")
|
||||
self.vol1bg.setPixmap(QPixmap(vol1_path))
|
||||
self.vol1bg.setScaledContents(True)
|
||||
|
||||
self.vol2bg = QLabel(self.inner_content)
|
||||
self.vol2bg.setObjectName(u"vol2bg")
|
||||
self.vol2bg.setGeometry(QRect(0, 210, 93, 64))
|
||||
# 直接加载图片文件
|
||||
vol2_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "IMG", "LOGO", "vo02_logo.png")
|
||||
self.vol2bg.setPixmap(QPixmap(vol2_path))
|
||||
self.vol2bg.setScaledContents(True)
|
||||
|
||||
self.vol3bg = QLabel(self.inner_content)
|
||||
self.vol3bg.setObjectName(u"vol3bg")
|
||||
self.vol3bg.setGeometry(QRect(0, 270, 93, 64))
|
||||
# 直接加载图片文件
|
||||
vol3_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "IMG", "LOGO", "vo03_logo.png")
|
||||
self.vol3bg.setPixmap(QPixmap(vol3_path))
|
||||
self.vol3bg.setScaledContents(True)
|
||||
|
||||
self.vol4bg = QLabel(self.inner_content)
|
||||
self.vol4bg.setObjectName(u"vol4bg")
|
||||
self.vol4bg.setGeometry(QRect(0, 330, 93, 64))
|
||||
# 直接加载图片文件
|
||||
vol4_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "IMG", "LOGO", "vo04_logo.png")
|
||||
self.vol4bg.setPixmap(QPixmap(vol4_path))
|
||||
self.vol4bg.setScaledContents(True)
|
||||
|
||||
self.afterbg = QLabel(self.inner_content)
|
||||
self.afterbg.setObjectName(u"afterbg")
|
||||
self.afterbg.setGeometry(QRect(0, 390, 93, 64))
|
||||
# 直接加载图片文件
|
||||
after_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "IMG", "LOGO", "voaf_logo.png")
|
||||
self.afterbg.setPixmap(QPixmap(after_path))
|
||||
self.afterbg.setScaledContents(True)
|
||||
|
||||
# 修复Mainbg位置并使用title_bg1.png作为背景图片
|
||||
self.Mainbg = QLabel(self.inner_content)
|
||||
self.Mainbg.setObjectName(u"Mainbg")
|
||||
self.Mainbg.setGeometry(QRect(0, 0, 1280, 655))
|
||||
# 允许拉伸以填满整个区域
|
||||
main_bg_pixmap = load_image_from_file(os.path.join(os.path.dirname(os.path.dirname(__file__)), "IMG", "BG", "title_bg1.png"))
|
||||
|
||||
# 如果加载的图片不是空的,则设置,并允许拉伸填满
|
||||
if not main_bg_pixmap.isNull():
|
||||
self.Mainbg.setPixmap(main_bg_pixmap)
|
||||
self.Mainbg.setScaledContents(True)
|
||||
self.Mainbg.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
# 使用新的按钮图片
|
||||
button_pixmap = load_image_from_file(os.path.join(os.path.dirname(os.path.dirname(__file__)), "IMG", "BTN", "Button.png"))
|
||||
|
||||
# 创建文本标签布局的按钮
|
||||
# 开始安装按钮 - 基于背景图片和标签组合
|
||||
# 调整开始安装按钮的位置
|
||||
self.button_container = QWidget(self.inner_content)
|
||||
self.button_container.setObjectName(u"start_install_container")
|
||||
self.button_container.setGeometry(QRect(1050, 200, 211, 111)) # 调整Y坐标,上移至200
|
||||
# 不要隐藏容器,让动画系统来控制它的可见性和位置
|
||||
|
||||
# 使用原来的按钮背景图片
|
||||
self.start_install_bg = QLabel(self.button_container)
|
||||
self.start_install_bg.setObjectName(u"start_install_bg")
|
||||
self.start_install_bg.setGeometry(QRect(10, 10, 191, 91)) # 居中放置在扩大的容器中
|
||||
self.start_install_bg.setPixmap(button_pixmap)
|
||||
self.start_install_bg.setScaledContents(True)
|
||||
|
||||
self.start_install_text = QLabel(self.button_container)
|
||||
self.start_install_text.setObjectName(u"start_install_text")
|
||||
self.start_install_text.setGeometry(QRect(10, 7, 191, 91)) # 居中放置在扩大的容器中
|
||||
self.start_install_text.setText("开始安装")
|
||||
self.start_install_text.setFont(self.custom_font)
|
||||
self.start_install_text.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.start_install_text.setStyleSheet("letter-spacing: 1px;")
|
||||
|
||||
# 点击区域透明按钮
|
||||
self.start_install_btn = QPushButton(self.button_container)
|
||||
self.start_install_btn.setObjectName(u"start_install_btn")
|
||||
self.start_install_btn.setGeometry(QRect(10, 10, 191, 91)) # 居中放置在扩大的容器中
|
||||
self.start_install_btn.setCursor(QCursor(Qt.CursorShape.PointingHandCursor)) # 设置鼠标悬停时为手形光标
|
||||
self.start_install_btn.setFlat(True)
|
||||
self.start_install_btn.raise_() # 确保按钮在最上层
|
||||
self.start_install_btn.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
}
|
||||
""")
|
||||
|
||||
# 添加卸载补丁按钮 - 新增
|
||||
self.uninstall_container = QWidget(self.inner_content)
|
||||
self.uninstall_container.setObjectName(u"uninstall_container")
|
||||
self.uninstall_container.setGeometry(QRect(1050, 310, 211, 111)) # 调整Y坐标,位于310位置
|
||||
|
||||
# 使用相同的按钮背景图片
|
||||
self.uninstall_bg = QLabel(self.uninstall_container)
|
||||
self.uninstall_bg.setObjectName(u"uninstall_bg")
|
||||
self.uninstall_bg.setGeometry(QRect(10, 10, 191, 91)) # 居中放置在扩大的容器中
|
||||
self.uninstall_bg.setPixmap(button_pixmap)
|
||||
self.uninstall_bg.setScaledContents(True)
|
||||
|
||||
self.uninstall_text = QLabel(self.uninstall_container)
|
||||
self.uninstall_text.setObjectName(u"uninstall_text")
|
||||
self.uninstall_text.setGeometry(QRect(10, 7, 191, 91)) # 居中放置在扩大的容器中
|
||||
self.uninstall_text.setText("卸载补丁")
|
||||
self.uninstall_text.setFont(self.custom_font)
|
||||
self.uninstall_text.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.uninstall_text.setStyleSheet("letter-spacing: 1px;")
|
||||
|
||||
# 点击区域透明按钮
|
||||
self.uninstall_btn = QPushButton(self.uninstall_container)
|
||||
self.uninstall_btn.setObjectName(u"uninstall_btn")
|
||||
self.uninstall_btn.setGeometry(QRect(10, 10, 191, 91)) # 居中放置在扩大的容器中
|
||||
self.uninstall_btn.setCursor(QCursor(Qt.CursorShape.PointingHandCursor)) # 设置鼠标悬停时为手形光标
|
||||
self.uninstall_btn.setFlat(True)
|
||||
self.uninstall_btn.raise_() # 确保按钮在最上层
|
||||
self.uninstall_btn.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
}
|
||||
""")
|
||||
|
||||
# 退出按钮 - 基于背景图片和标签组合,调整位置
|
||||
self.exit_container = QWidget(self.inner_content)
|
||||
self.exit_container.setObjectName(u"exit_container")
|
||||
self.exit_container.setGeometry(QRect(1050, 420, 211, 111)) # 调整Y坐标,下移至420
|
||||
# 不要隐藏容器,让动画系统来控制它的可见性和位置
|
||||
|
||||
# 使用原来的按钮背景图片
|
||||
self.exit_bg = QLabel(self.exit_container)
|
||||
self.exit_bg.setObjectName(u"exit_bg")
|
||||
self.exit_bg.setGeometry(QRect(10, 10, 191, 91)) # 居中放置在扩大的容器中
|
||||
self.exit_bg.setPixmap(button_pixmap)
|
||||
self.exit_bg.setScaledContents(True)
|
||||
|
||||
self.exit_text = QLabel(self.exit_container)
|
||||
self.exit_text.setObjectName(u"exit_text")
|
||||
self.exit_text.setGeometry(QRect(10, 7, 191, 91)) # 居中放置在扩大的容器中
|
||||
self.exit_text.setText("退出程序")
|
||||
self.exit_text.setFont(self.custom_font)
|
||||
self.exit_text.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.exit_text.setStyleSheet("letter-spacing: 1px;")
|
||||
|
||||
# 点击区域透明按钮
|
||||
self.exit_btn = QPushButton(self.exit_container)
|
||||
self.exit_btn.setObjectName(u"exit_btn")
|
||||
self.exit_btn.setGeometry(QRect(10, 10, 191, 91)) # 居中放置在扩大的容器中
|
||||
self.exit_btn.setCursor(QCursor(Qt.CursorShape.PointingHandCursor)) # 设置鼠标悬停时为手形光标
|
||||
self.exit_btn.setFlat(True)
|
||||
self.exit_btn.raise_() # 确保按钮在最上层
|
||||
self.exit_btn.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
}
|
||||
""")
|
||||
|
||||
MainWindows.setCentralWidget(self.centralwidget)
|
||||
|
||||
# 调整层级顺序
|
||||
self.loadbg.raise_()
|
||||
self.vol1bg.raise_()
|
||||
self.vol2bg.raise_()
|
||||
self.vol3bg.raise_()
|
||||
self.vol4bg.raise_()
|
||||
self.afterbg.raise_()
|
||||
self.Mainbg.raise_()
|
||||
self.button_container.raise_()
|
||||
self.uninstall_container.raise_() # 添加新按钮到层级顺序
|
||||
self.exit_container.raise_()
|
||||
self.menu_area.raise_() # 确保菜单区域在背景之上
|
||||
# self.menubar.raise_() # 不再需要菜单栏
|
||||
self.settings_btn.raise_() # 确保设置按钮在上层
|
||||
self.help_btn.raise_() # 确保帮助按钮在上层
|
||||
self.title_bar.raise_() # 确保标题栏在最上层
|
||||
|
||||
self.retranslateUi(MainWindows)
|
||||
|
||||
QMetaObject.connectSlotsByName(MainWindows)
|
||||
# setupUi
|
||||
|
||||
def retranslateUi(self, MainWindows):
|
||||
MainWindows.setWindowTitle(QCoreApplication.translate("MainWindows", f"{APP_NAME} v{APP_VERSION}", None))
|
||||
self.loadbg.setText("")
|
||||
self.vol1bg.setText("")
|
||||
self.vol2bg.setText("")
|
||||
self.vol3bg.setText("")
|
||||
self.vol4bg.setText("")
|
||||
self.afterbg.setText("")
|
||||
self.Mainbg.setText("")
|
||||
#if QT_CONFIG(accessibility)
|
||||
self.start_install_btn.setAccessibleDescription("")
|
||||
#endif // QT_CONFIG(accessibility)
|
||||
self.menu.setTitle(QCoreApplication.translate("MainWindows", u"设置", None))
|
||||
self.menu_2.setTitle(QCoreApplication.translate("MainWindows", u"帮助", None))
|
||||
# retranslateUi
|
||||
|
||||
def show_menu(self, menu, button):
|
||||
"""显示菜单
|
||||
|
||||
Args:
|
||||
menu: 要显示的菜单
|
||||
button: 触发菜单的按钮
|
||||
"""
|
||||
# 计算菜单显示位置
|
||||
pos = button.mapToGlobal(button.rect().bottomLeft())
|
||||
menu.exec(pos)
|
||||
|
||||
20
source/utils/__init__.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from .logger import Logger
|
||||
from .helpers import (
|
||||
load_base64_image, HashManager, AdminPrivileges, msgbox_frame,
|
||||
load_config, save_config, HostsManager, censor_url, resource_path,
|
||||
load_image_from_file
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
'Logger',
|
||||
'load_base64_image',
|
||||
'load_image_from_file',
|
||||
'HashManager',
|
||||
'AdminPrivileges',
|
||||
'msgbox_frame',
|
||||
'load_config',
|
||||
'save_config',
|
||||
'HostsManager',
|
||||
'censor_url',
|
||||
'resource_path'
|
||||
]
|
||||
@@ -9,17 +9,26 @@ import psutil
|
||||
from PySide6 import QtCore, QtWidgets
|
||||
import re
|
||||
from PySide6.QtGui import QIcon, QPixmap
|
||||
from pic_data import img_data
|
||||
from config import APP_NAME, CONFIG_FILE
|
||||
from data.config import APP_NAME, CONFIG_FILE
|
||||
|
||||
def resource_path(relative_path):
|
||||
"""获取资源的绝对路径,适用于开发环境和PyInstaller打包环境"""
|
||||
"""获取资源的绝对路径,适用于开发环境和Nuitka打包环境"""
|
||||
if getattr(sys, 'frozen', False):
|
||||
# PyInstaller创建的临时文件夹,并将路径存储在_MEIPASS中
|
||||
base_path = getattr(sys, '_MEIPASS', os.path.dirname(sys.executable))
|
||||
# Nuitka/PyInstaller创建的临时文件夹,并将路径存储在_MEIPASS中或与可执行文件同目录
|
||||
if hasattr(sys, '_MEIPASS'):
|
||||
base_path = sys._MEIPASS
|
||||
else:
|
||||
base_path = os.path.dirname(sys.executable)
|
||||
else:
|
||||
# 在开发环境中运行
|
||||
base_path = os.path.dirname(os.path.abspath(__file__))
|
||||
base_path = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||
|
||||
# 处理特殊的可执行文件和数据文件路径
|
||||
if relative_path in ("aria2c.exe", "cfst.exe"):
|
||||
return os.path.join(base_path, 'bin', relative_path)
|
||||
elif relative_path in ("ip.txt", "ipv6.txt"):
|
||||
return os.path.join(base_path, 'data', relative_path)
|
||||
|
||||
return os.path.join(base_path, relative_path)
|
||||
|
||||
def load_base64_image(base64_str):
|
||||
@@ -27,14 +36,28 @@ def load_base64_image(base64_str):
|
||||
pixmap.loadFromData(base64.b64decode(base64_str))
|
||||
return pixmap
|
||||
|
||||
def load_image_from_file(file_path):
|
||||
"""加载图像文件到QPixmap
|
||||
|
||||
Args:
|
||||
file_path: 图像文件路径
|
||||
|
||||
Returns:
|
||||
QPixmap: 加载的图像
|
||||
"""
|
||||
if os.path.exists(file_path):
|
||||
return QPixmap(file_path)
|
||||
return QPixmap()
|
||||
|
||||
def msgbox_frame(title, text, buttons=QtWidgets.QMessageBox.StandardButton.NoButton):
|
||||
msg_box = QtWidgets.QMessageBox()
|
||||
msg_box.setWindowTitle(title)
|
||||
msg_box.setWindowModality(QtCore.Qt.WindowModality.WindowModal)
|
||||
|
||||
icon_data = img_data.get("icon")
|
||||
if icon_data:
|
||||
pixmap = load_base64_image(icon_data)
|
||||
# 直接加载图标文件
|
||||
icon_path = resource_path(os.path.join("IMG", "ICO", "icon.png"))
|
||||
if os.path.exists(icon_path):
|
||||
pixmap = QPixmap(icon_path)
|
||||
if not pixmap.isNull():
|
||||
msg_box.setWindowIcon(QIcon(pixmap))
|
||||
msg_box.setIconPixmap(pixmap.scaled(64, 64, QtCore.Qt.AspectRatioMode.KeepAspectRatio, QtCore.Qt.TransformationMode.SmoothTransformation))
|
||||
@@ -89,8 +112,25 @@ class HashManager:
|
||||
print(f"Error calculating hash for {file_path}: {e}")
|
||||
return results
|
||||
|
||||
def hash_pop_window(self):
|
||||
msg_box = msgbox_frame(f"通知 - {APP_NAME}", "\n正在检验文件状态...\n")
|
||||
def hash_pop_window(self, check_type="default"):
|
||||
"""显示文件检验窗口
|
||||
|
||||
Args:
|
||||
check_type: 检查类型,可以是 'pre'(预检查), 'after'(后检查), 'extraction'(解压后检查)
|
||||
|
||||
Returns:
|
||||
QMessageBox: 消息框实例
|
||||
"""
|
||||
message = "\n正在检验文件状态...\n"
|
||||
|
||||
if check_type == "pre":
|
||||
message = "\n正在检查游戏文件以确定需要安装的补丁...\n"
|
||||
elif check_type == "after":
|
||||
message = "\n正在检验本地文件完整性...\n"
|
||||
elif check_type == "extraction":
|
||||
message = "\n正在验证下载的解压文件完整性...\n"
|
||||
|
||||
msg_box = msgbox_frame(f"通知 - {APP_NAME}", message)
|
||||
msg_box.open()
|
||||
QtWidgets.QApplication.processEvents()
|
||||
return msg_box
|
||||
@@ -148,6 +188,7 @@ class AdminPrivileges:
|
||||
"nekopara_vol1.exe",
|
||||
"nekopara_vol2.exe",
|
||||
"NEKOPARAvol3.exe",
|
||||
"NEKOPARAvol3.exe.nocrack",
|
||||
"nekopara_vol4.exe",
|
||||
"nekopara_after.exe",
|
||||
]
|
||||
@@ -190,33 +231,40 @@ class AdminPrivileges:
|
||||
|
||||
def check_and_terminate_processes(self):
|
||||
for proc in psutil.process_iter(["pid", "name"]):
|
||||
if proc.info["name"] in self.required_exes:
|
||||
msg_box = msgbox_frame(
|
||||
f"进程检测 - {APP_NAME}",
|
||||
f"\n检测到游戏正在运行: {proc.info['name']} \n\n是否终止?\n",
|
||||
QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No,
|
||||
)
|
||||
reply = msg_box.exec()
|
||||
if reply == QtWidgets.QMessageBox.StandardButton.Yes:
|
||||
try:
|
||||
proc.terminate()
|
||||
proc.wait(timeout=3)
|
||||
except psutil.AccessDenied:
|
||||
proc_name = proc.info["name"].lower() if proc.info["name"] else ""
|
||||
|
||||
# 检查进程名是否匹配任何需要终止的游戏进程
|
||||
for exe in self.required_exes:
|
||||
if exe.lower() == proc_name:
|
||||
# 获取不带.nocrack的游戏名称用于显示
|
||||
display_name = exe.replace(".nocrack", "")
|
||||
|
||||
msg_box = msgbox_frame(
|
||||
f"进程检测 - {APP_NAME}",
|
||||
f"\n检测到游戏正在运行: {display_name} \n\n是否终止?\n",
|
||||
QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No,
|
||||
)
|
||||
reply = msg_box.exec()
|
||||
if reply == QtWidgets.QMessageBox.StandardButton.Yes:
|
||||
try:
|
||||
proc.terminate()
|
||||
proc.wait(timeout=3)
|
||||
except psutil.AccessDenied:
|
||||
msg_box = msgbox_frame(
|
||||
f"错误 - {APP_NAME}",
|
||||
f"\n无法关闭游戏: {display_name} \n\n请手动关闭后重启应用\n",
|
||||
QtWidgets.QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
msg_box.exec()
|
||||
sys.exit(1)
|
||||
else:
|
||||
msg_box = msgbox_frame(
|
||||
f"错误 - {APP_NAME}",
|
||||
f"\n无法关闭游戏: {proc.info['name']} \n\n请手动关闭后重启应用\n",
|
||||
f"进程检测 - {APP_NAME}",
|
||||
f"\n未关闭的游戏: {display_name} \n\n请手动关闭后重启应用\n",
|
||||
QtWidgets.QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
msg_box.exec()
|
||||
sys.exit(1)
|
||||
else:
|
||||
msg_box = msgbox_frame(
|
||||
f"进程检测 - {APP_NAME}",
|
||||
f"\n未关闭的游戏: {proc.info['name']} \n\n请手动关闭后重启应用\n",
|
||||
QtWidgets.QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
msg_box.exec()
|
||||
sys.exit(1)
|
||||
|
||||
class HostsManager:
|
||||
def __init__(self):
|
||||
@@ -224,6 +272,7 @@ class HostsManager:
|
||||
self.backup_path = os.path.join(os.path.dirname(self.hosts_path), f'hosts.bak.{APP_NAME}')
|
||||
self.original_content = None
|
||||
self.modified = False
|
||||
self.modified_hostnames = set() # 跟踪被修改的主机名
|
||||
|
||||
def backup(self):
|
||||
if not AdminPrivileges().is_admin():
|
||||
@@ -241,6 +290,48 @@ class HostsManager:
|
||||
msg_box = msgbox_frame(f"错误 - {APP_NAME}", f"\n无法备份hosts文件,请检查权限。\n\n【错误信息】:{e}\n", QtWidgets.QMessageBox.StandardButton.Ok)
|
||||
msg_box.exec()
|
||||
return False
|
||||
|
||||
def clean_hostname_entries(self, hostname):
|
||||
"""清理hosts文件中指定域名的所有记录
|
||||
|
||||
Args:
|
||||
hostname: 要清理的域名
|
||||
|
||||
Returns:
|
||||
bool: 清理是否成功
|
||||
"""
|
||||
if not self.original_content:
|
||||
if not self.backup():
|
||||
return False
|
||||
|
||||
# 确保original_content不为None
|
||||
if not self.original_content:
|
||||
print("无法读取hosts文件内容,操作中止。")
|
||||
return False
|
||||
|
||||
if not AdminPrivileges().is_admin():
|
||||
print("需要管理员权限来修改hosts文件。")
|
||||
return False
|
||||
|
||||
try:
|
||||
lines = self.original_content.splitlines()
|
||||
new_lines = [line for line in lines if hostname not in line]
|
||||
|
||||
# 如果没有变化,不需要写入
|
||||
if len(new_lines) == len(lines):
|
||||
print(f"Hosts文件中没有找到 {hostname} 的记录")
|
||||
return True
|
||||
|
||||
with open(self.hosts_path, 'w', encoding='utf-8') as f:
|
||||
f.write('\n'.join(new_lines))
|
||||
|
||||
# 更新原始内容
|
||||
self.original_content = '\n'.join(new_lines)
|
||||
print(f"已从hosts文件中清理 {hostname} 的记录")
|
||||
return True
|
||||
except IOError as e:
|
||||
print(f"清理hosts文件失败: {e}")
|
||||
return False
|
||||
|
||||
def apply_ip(self, hostname, ip_address):
|
||||
if not self.original_content:
|
||||
@@ -256,17 +347,23 @@ class HostsManager:
|
||||
return False
|
||||
|
||||
try:
|
||||
lines = self.original_content.splitlines()
|
||||
new_lines = [line for line in lines if not (hostname in line and line.strip().startswith(ip_address))]
|
||||
# 首先清理已有的同域名记录
|
||||
self.clean_hostname_entries(hostname)
|
||||
|
||||
# 然后添加新记录
|
||||
lines = self.original_content.splitlines()
|
||||
new_entry = f"{ip_address}\t{hostname}"
|
||||
new_lines.append(f"\n# Added by {APP_NAME}")
|
||||
new_lines.append(new_entry)
|
||||
lines.append(f"\n# Added by {APP_NAME}")
|
||||
lines.append(new_entry)
|
||||
|
||||
with open(self.hosts_path, 'w', encoding='utf-8') as f:
|
||||
f.write('\n'.join(new_lines))
|
||||
f.write('\n'.join(lines))
|
||||
|
||||
# 更新原始内容
|
||||
self.original_content = '\n'.join(lines)
|
||||
self.modified = True
|
||||
# 记录被修改的主机名,用于最终清理
|
||||
self.modified_hostnames.add(hostname)
|
||||
print(f"Hosts文件已更新: {new_entry}")
|
||||
return True
|
||||
except IOError as e:
|
||||
@@ -275,6 +372,55 @@ class HostsManager:
|
||||
msg_box.exec()
|
||||
return False
|
||||
|
||||
def check_and_clean_all_entries(self):
|
||||
"""检查并清理所有由本应用程序添加的hosts记录
|
||||
|
||||
Returns:
|
||||
bool: 清理是否成功
|
||||
"""
|
||||
if not AdminPrivileges().is_admin():
|
||||
print("需要管理员权限来检查和清理hosts文件。")
|
||||
return False
|
||||
|
||||
try:
|
||||
# 读取当前hosts文件内容
|
||||
with open(self.hosts_path, 'r', encoding='utf-8') as f:
|
||||
current_content = f.read()
|
||||
|
||||
lines = current_content.splitlines()
|
||||
new_lines = []
|
||||
skip_next = False
|
||||
|
||||
for line in lines:
|
||||
# 如果上一行是我们的注释标记,跳过当前行
|
||||
if skip_next:
|
||||
skip_next = False
|
||||
continue
|
||||
|
||||
# 检查是否是我们添加的注释行
|
||||
if f"# Added by {APP_NAME}" in line:
|
||||
skip_next = True # 跳过下一行(实际的hosts记录)
|
||||
continue
|
||||
|
||||
# 保留其他所有行
|
||||
new_lines.append(line)
|
||||
|
||||
# 检查是否有变化
|
||||
if len(new_lines) == len(lines):
|
||||
print("Hosts文件中没有找到由本应用添加的记录")
|
||||
return True
|
||||
|
||||
# 写回清理后的内容
|
||||
with open(self.hosts_path, 'w', encoding='utf-8') as f:
|
||||
f.write('\n'.join(new_lines))
|
||||
|
||||
print(f"已清理所有由 {APP_NAME} 添加的hosts记录")
|
||||
return True
|
||||
|
||||
except IOError as e:
|
||||
print(f"检查和清理hosts文件失败: {e}")
|
||||
return False
|
||||
|
||||
def restore(self):
|
||||
if not self.modified:
|
||||
if os.path.exists(self.backup_path):
|
||||
@@ -282,6 +428,8 @@ class HostsManager:
|
||||
os.remove(self.backup_path)
|
||||
except OSError:
|
||||
pass
|
||||
# 即使没有修改过,也检查一次是否有残留
|
||||
self.check_and_clean_all_entries()
|
||||
return True
|
||||
|
||||
if not AdminPrivileges().is_admin():
|
||||
@@ -299,6 +447,8 @@ class HostsManager:
|
||||
os.remove(self.backup_path)
|
||||
except OSError:
|
||||
pass
|
||||
# 恢复后再检查一次是否有残留
|
||||
self.check_and_clean_all_entries()
|
||||
return True
|
||||
except IOError as e:
|
||||
print(f"从内存恢复hosts文件失败: {e}")
|
||||
@@ -309,6 +459,8 @@ class HostsManager:
|
||||
def restore_from_backup_file(self):
|
||||
if not os.path.exists(self.backup_path):
|
||||
print("未找到hosts备份文件,无法恢复。")
|
||||
# 即使没有备份文件,也尝试清理可能的残留
|
||||
self.check_and_clean_all_entries()
|
||||
return False
|
||||
try:
|
||||
with open(self.backup_path, 'r', encoding='utf-8') as bf:
|
||||
@@ -318,11 +470,15 @@ class HostsManager:
|
||||
os.remove(self.backup_path)
|
||||
self.modified = False
|
||||
print("Hosts文件已从备份文件恢复。")
|
||||
# 恢复后再检查一次是否有残留
|
||||
self.check_and_clean_all_entries()
|
||||
return True
|
||||
except (IOError, OSError) as e:
|
||||
print(f"从备份文件恢复hosts失败: {e}")
|
||||
msg_box = msgbox_frame(f"警告 - {APP_NAME}", f"\n自动恢复hosts文件失败,请手动从 {self.backup_path} 恢复。\n\n【错误信息】:{e}\n", QtWidgets.QMessageBox.StandardButton.Ok)
|
||||
msg_box.exec()
|
||||
# 尽管恢复失败,仍然尝试清理可能的残留
|
||||
self.check_and_clean_all_entries()
|
||||
return False
|
||||
|
||||
def censor_url(text):
|
||||
19
source/utils/logger.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from .helpers import censor_url
|
||||
|
||||
class Logger:
|
||||
def __init__(self, filename, stream):
|
||||
self.terminal = stream
|
||||
self.log = open(filename, "w", encoding="utf-8")
|
||||
|
||||
def write(self, message):
|
||||
censored_message = censor_url(message)
|
||||
self.terminal.write(censored_message)
|
||||
self.log.write(censored_message)
|
||||
self.flush()
|
||||
|
||||
def flush(self):
|
||||
self.terminal.flush()
|
||||
self.log.flush()
|
||||
|
||||
def close(self):
|
||||
self.log.close()
|
||||
14
source/workers/__init__.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from .hash_thread import HashThread
|
||||
from .extraction_thread import ExtractionThread
|
||||
from .config_fetch_thread import ConfigFetchThread
|
||||
from .ip_optimizer import IpOptimizerThread
|
||||
from .download import DownloadThread, ProgressWindow
|
||||
|
||||
__all__ = [
|
||||
'IpOptimizerThread',
|
||||
'HashThread',
|
||||
'ExtractionThread',
|
||||
'ConfigFetchThread',
|
||||
'DownloadThread',
|
||||
'ProgressWindow'
|
||||
]
|
||||
65
source/workers/config_fetch_thread.py
Normal file
@@ -0,0 +1,65 @@
|
||||
import json
|
||||
import requests
|
||||
import webbrowser
|
||||
from PySide6.QtCore import QThread, Signal
|
||||
from PySide6.QtWidgets import QMessageBox
|
||||
import sys
|
||||
|
||||
class ConfigFetchThread(QThread):
|
||||
finished = Signal(object, str) # data, error_message
|
||||
|
||||
def __init__(self, url, headers, debug_mode=False, parent=None):
|
||||
super().__init__(parent)
|
||||
self.url = url
|
||||
self.headers = headers
|
||||
self.debug_mode = debug_mode
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
if self.debug_mode:
|
||||
print("--- Starting to fetch cloud config ---")
|
||||
print(f"DEBUG: Requesting URL: {self.url}")
|
||||
print(f"DEBUG: Using Headers: {self.headers}")
|
||||
|
||||
response = requests.get(self.url, headers=self.headers, timeout=10)
|
||||
|
||||
if self.debug_mode:
|
||||
print(f"DEBUG: Response Status Code: {response.status_code}")
|
||||
print(f"DEBUG: Response Headers: {response.headers}")
|
||||
print(f"DEBUG: Response Text: {response.text}")
|
||||
|
||||
response.raise_for_status()
|
||||
|
||||
# 首先,总是尝试解析JSON
|
||||
config_data = response.json()
|
||||
|
||||
# 检查是否是要求更新的错误信息 - 使用Unicode编码的更新提示文本
|
||||
update_required_msg = "\u8bf7\u4f7f\u7528\u6700\u65b0\u7248\u672c\u7684FraiseMoe2-Next\u8fdb\u884c\u4e0b\u8f7d"
|
||||
if isinstance(config_data, str) and config_data == update_required_msg:
|
||||
self.finished.emit(None, "update_required")
|
||||
return
|
||||
elif isinstance(config_data, dict) and config_data.get("message") == update_required_msg:
|
||||
self.finished.emit(None, "update_required")
|
||||
return
|
||||
|
||||
# 检查是否是有效的配置文件
|
||||
required_keys = [f"vol.{i+1}.data" for i in range(4)] + ["after.data"]
|
||||
missing_keys = [key for key in required_keys if key not in config_data]
|
||||
if missing_keys:
|
||||
self.finished.emit(None, f"missing_keys:{','.join(missing_keys)}")
|
||||
return
|
||||
|
||||
self.finished.emit(config_data, "")
|
||||
except requests.exceptions.RequestException as e:
|
||||
error_msg = "访问云端配置失败,请检查网络状况或稍后再试。"
|
||||
if self.debug_mode:
|
||||
error_msg += f"\n详细错误: {e}"
|
||||
self.finished.emit(None, error_msg)
|
||||
except (ValueError, json.JSONDecodeError) as e:
|
||||
error_msg = "访问云端配置失败,请检查网络状况或稍后再试。"
|
||||
if self.debug_mode:
|
||||
error_msg += f"\nJSON解析失败: {e}"
|
||||
self.finished.emit(None, error_msg)
|
||||
finally:
|
||||
if self.debug_mode:
|
||||
print("--- Finished fetching cloud config ---")
|
||||
@@ -7,7 +7,7 @@ from PySide6 import QtCore, QtWidgets
|
||||
from PySide6.QtCore import (Qt, Signal, QThread, QTimer)
|
||||
from PySide6.QtWidgets import (QLabel, QProgressBar, QVBoxLayout, QDialog)
|
||||
from utils import resource_path
|
||||
from config import APP_NAME, UA
|
||||
from data.config import APP_NAME, UA
|
||||
|
||||
# 下载线程类
|
||||
class DownloadThread(QThread):
|
||||
@@ -182,5 +182,4 @@ class ProgressWindow(QDialog):
|
||||
|
||||
def closeEvent(self, event):
|
||||
# 覆盖默认的关闭事件,防止用户通过其他方式关闭窗口
|
||||
# 如果需要,可以在这里添加逻辑,例如询问用户是否要停止下载
|
||||
event.ignore()
|
||||
31
source/workers/extraction_thread.py
Normal file
@@ -0,0 +1,31 @@
|
||||
import os
|
||||
import shutil
|
||||
import py7zr
|
||||
from PySide6.QtCore import QThread, Signal
|
||||
from data.config import PLUGIN, GAME_INFO
|
||||
|
||||
class ExtractionThread(QThread):
|
||||
finished = Signal(bool, str, str) # success, error_message, game_version
|
||||
|
||||
def __init__(self, _7z_path, game_folder, plugin_path, game_version, parent=None):
|
||||
super().__init__(parent)
|
||||
self._7z_path = _7z_path
|
||||
self.game_folder = game_folder
|
||||
self.plugin_path = plugin_path
|
||||
self.game_version = game_version
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
with py7zr.SevenZipFile(self._7z_path, mode="r") as archive:
|
||||
archive.extractall(path=PLUGIN)
|
||||
|
||||
os.makedirs(self.game_folder, exist_ok=True)
|
||||
shutil.copy(self.plugin_path, self.game_folder)
|
||||
|
||||
if self.game_version == "NEKOPARA After":
|
||||
sig_path = os.path.join(PLUGIN, GAME_INFO[self.game_version]["sig_path"])
|
||||
shutil.copy(sig_path, self.game_folder)
|
||||
|
||||
self.finished.emit(True, "", self.game_version)
|
||||
except (py7zr.Bad7zFile, FileNotFoundError, Exception) as e:
|
||||
self.finished.emit(False, f"\n文件操作失败,请重试\n\n【错误信息】:{e}\n", self.game_version)
|
||||
28
source/workers/hash_thread.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from PySide6.QtCore import QThread, Signal
|
||||
from utils import HashManager
|
||||
from data.config import BLOCK_SIZE
|
||||
|
||||
class HashThread(QThread):
|
||||
pre_finished = Signal(dict)
|
||||
after_finished = Signal(dict)
|
||||
|
||||
def __init__(self, mode, install_paths, plugin_hash, installed_status, parent=None):
|
||||
super().__init__(parent)
|
||||
self.mode = mode
|
||||
self.install_paths = install_paths
|
||||
self.plugin_hash = plugin_hash
|
||||
self.installed_status = installed_status
|
||||
# 每个线程都应该有自己的HashManager实例
|
||||
self.hash_manager = HashManager(BLOCK_SIZE)
|
||||
|
||||
def run(self):
|
||||
if self.mode == "pre":
|
||||
updated_status = self.hash_manager.cfg_pre_hash_compare(
|
||||
self.install_paths, self.plugin_hash, self.installed_status
|
||||
)
|
||||
self.pre_finished.emit(updated_status)
|
||||
elif self.mode == "after":
|
||||
result = self.hash_manager.cfg_after_hash_compare(
|
||||
self.install_paths, self.plugin_hash, self.installed_status
|
||||
)
|
||||
self.after_finished.emit(result)
|
||||
@@ -5,6 +5,7 @@ import sys
|
||||
import time
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from PySide6.QtCore import QThread, Signal
|
||||
from utils import resource_path
|
||||
|
||||
class IpOptimizer:
|
||||
@@ -28,13 +29,16 @@ class IpOptimizer:
|
||||
return None
|
||||
|
||||
ip_txt_path = resource_path("ip.txt")
|
||||
|
||||
# 正确的参数设置,根据cfst帮助文档
|
||||
command = [
|
||||
cst_path,
|
||||
"-p", "1",
|
||||
"-o", "",
|
||||
"-url", url,
|
||||
"-f", ip_txt_path,
|
||||
"-dd",
|
||||
"-n", "500", # 延迟测速线程数 (默认200)
|
||||
"-p", "1", # 显示结果数量 (默认10个)
|
||||
"-url", url, # 指定测速地址
|
||||
"-f", ip_txt_path, # IP文件
|
||||
"-dd", # 禁用下载测速,按延迟排序
|
||||
"-o","" # 不写入结果文件
|
||||
]
|
||||
|
||||
creation_flags = subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0
|
||||
@@ -53,17 +57,13 @@ class IpOptimizer:
|
||||
bufsize=0
|
||||
)
|
||||
|
||||
# 立即向 stdin 发送换行符,以便程序在 Windows 下正常退出
|
||||
if self.process.stdin:
|
||||
try:
|
||||
self.process.stdin.write('\n')
|
||||
self.process.stdin.flush()
|
||||
except:
|
||||
pass
|
||||
finally:
|
||||
self.process.stdin.close()
|
||||
|
||||
ip_pattern = re.compile(r'^\s*([\d\.]+)\s+\d+\s+\d+\s+[\d\.]+%?\s+[\d\.]+\s+[\d\.]+\s+.*$')
|
||||
# 更新正则表达式以匹配cfst输出中的IP格式
|
||||
# 匹配格式: IP地址在行首,后面跟着一些数字和文本
|
||||
ip_pattern = re.compile(r'^(\d+\.\d+\.\d+\.\d+)\s+.*')
|
||||
|
||||
# 标记是否已经找到结果表头和完成标记
|
||||
found_header = False
|
||||
found_completion = False
|
||||
|
||||
stdout = self.process.stdout
|
||||
if not stdout:
|
||||
@@ -72,7 +72,7 @@ class IpOptimizer:
|
||||
|
||||
optimal_ip = None
|
||||
timeout_counter = 0
|
||||
max_timeout = 60
|
||||
max_timeout = 300 # 增加超时时间到5分钟
|
||||
|
||||
while True:
|
||||
if self.process.poll() is not None:
|
||||
@@ -97,16 +97,45 @@ class IpOptimizer:
|
||||
cleaned_line = line.strip()
|
||||
if cleaned_line:
|
||||
print(cleaned_line)
|
||||
match = ip_pattern.match(cleaned_line)
|
||||
if match:
|
||||
optimal_ip = match.group(1)
|
||||
print(f"找到最优 IP: {optimal_ip}, 正在终止测速进程...")
|
||||
break
|
||||
|
||||
# 检测结果表头
|
||||
if "IP 地址" in cleaned_line and "平均延迟" in cleaned_line:
|
||||
print("检测到IP结果表头,准备获取IP地址...")
|
||||
found_header = True
|
||||
continue
|
||||
|
||||
# 检测完成标记
|
||||
if "完整测速结果已写入" in cleaned_line or "按下 回车键 或 Ctrl+C 退出" in cleaned_line:
|
||||
print("检测到测速完成信息")
|
||||
found_completion = True
|
||||
|
||||
# 如果已经找到了IP,可以退出了
|
||||
if optimal_ip:
|
||||
break
|
||||
|
||||
# 已找到表头后,尝试匹配IP地址行
|
||||
if found_header:
|
||||
match = ip_pattern.search(cleaned_line)
|
||||
if match and not optimal_ip: # 只保存第一个匹配的IP(最优IP)
|
||||
optimal_ip = match.group(1)
|
||||
print(f"找到最优 IP: {optimal_ip}")
|
||||
# 找到最优IP后立即退出循环,不等待完成标记
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
print(f"读取输出时发生错误: {e}")
|
||||
break
|
||||
|
||||
# 确保完全读取输出后再发送退出信号
|
||||
if self.process and self.process.poll() is None:
|
||||
try:
|
||||
if self.process.stdin and not self.process.stdin.closed:
|
||||
print("发送退出信号...")
|
||||
self.process.stdin.write('\n')
|
||||
self.process.stdin.flush()
|
||||
except:
|
||||
pass
|
||||
|
||||
self.stop()
|
||||
|
||||
print("--- CloudflareSpeedTest 执行结束 ---")
|
||||
@@ -135,6 +164,24 @@ class IpOptimizer:
|
||||
self.process.wait()
|
||||
print("CloudflareSpeedTest 进程已终止。")
|
||||
|
||||
|
||||
class IpOptimizerThread(QThread):
|
||||
"""用于在后台线程中运行IP优化的类"""
|
||||
finished = Signal(str)
|
||||
|
||||
def __init__(self, url, parent=None):
|
||||
super().__init__(parent)
|
||||
self.url = url
|
||||
self.optimizer = IpOptimizer()
|
||||
|
||||
def run(self):
|
||||
optimal_ip = self.optimizer.get_optimal_ip(self.url)
|
||||
self.finished.emit(optimal_ip if optimal_ip else "")
|
||||
|
||||
def stop(self):
|
||||
self.optimizer.stop()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 用于直接测试此模块
|
||||
test_url = "https://speed.cloudflare.com/__down?during=download&bytes=104857600"
|
||||
@@ -143,4 +190,4 @@ if __name__ == '__main__':
|
||||
if ip:
|
||||
print(f"为 {test_url} 找到的最优 IP 是: {ip}")
|
||||
else:
|
||||
print(f"未能为 {test_url} 找到最优 IP。")
|
||||
print(f"未能为 {test_url} 找到最优 IP。")
|
||||