Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19da86c808 | ||
|
|
96d20c6a5b | ||
|
|
0d33d5610a | ||
|
|
1c749079a2 | ||
|
|
291c471b9e | ||
|
|
6399382242 | ||
|
|
3fc74555cb | ||
|
|
5c06802f65 | ||
|
|
a93991ca9d | ||
|
|
c5b9f1746a | ||
|
|
c941c03446 | ||
|
|
5ad4062346 | ||
|
|
cbfe0d7ff6 | ||
|
|
c837370470 | ||
|
|
db9736cc4e | ||
|
|
a411461f63 | ||
|
|
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 10/11 64-bit systems (other platforms or versions have not been tested) ❗**
|
||||
|
||||
> **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>
|
||||
30
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>
|
||||
@@ -40,7 +31,7 @@
|
||||
|
||||
> **2. 尚未拥有的游戏将不会进行补丁安装 ❗**
|
||||
|
||||
> **3. 本工具仅适用于补丁安装,不适用于安装游戏 ❗ 且仅限于在 Windows x64 系统上运行 ❗**
|
||||
> **3. 本工具仅适用于补丁安装,不适用于安装游戏 ❗ 且仅限于在 Windows 10/11 64位系统上运行(其他平台或版本未经测试)❗**
|
||||
|
||||
> **4. 工具需要使用管理员权限运行 ❗
|
||||
> 原因:为了防止用户在没有关闭正在运行的游戏而影响本工具的安装效果,启用应用前会获取游戏进程信息从而关闭游戏 ❗**
|
||||
@@ -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>
|
||||
|
||||
|
||||
91
PRIVACY.md
Normal file
@@ -0,0 +1,91 @@
|
||||
# FRAISEMOE Addons Installer NEXT 隐私政策
|
||||
|
||||
## 1. 引言
|
||||
|
||||
本隐私政策旨在说明 FRAISEMOE Addons Installer NEXT(以下简称"本应用")在使用过程中如何收集、使用和保护您的个人信息。我们致力于保护您的隐私,并确保您了解我们如何处理您的数据。
|
||||
|
||||
## 2. 收集的信息
|
||||
|
||||
本应用在运行过程中可能会收集或处理以下信息:
|
||||
|
||||
### 2.1 系统信息
|
||||
- 程序版本号:用于检查更新和兼容性
|
||||
|
||||
### 2.2 网络相关信息
|
||||
- **IP 地址、ISP 及地理位置**: 应用启动时,为获取云端配置,您的 IP 地址会被服务器记录。服务器可能会根据您的 IP 地址推断您的互联网服务提供商(ISP)和地理位置,这些信息仅用于用户数量、区域分布的统计和软件使用情况分析。当您使用 Cloudflare 加速功能时,您的 IP 地址也会被用于节点优选。
|
||||
- **下载统计信息**:用于监控下载进度和速度
|
||||
- **IPv6 连接测试**:应用会访问 testipv6.cn(test-ipv6.com 的中国大陆镜像网站)以判断软件是否支持 IPv6 连接
|
||||
- **IPv6 地址获取**:应用在测试 IPv6 功能时会请求 ipw.cn 获取您的公网 IPv6 地址,仅用于显示和连接测试目的
|
||||
|
||||
### 2.3 文件信息
|
||||
- 游戏安装路径:用于识别已安装的游戏和安装补丁
|
||||
- 文件哈希值:用于验证文件完整性
|
||||
|
||||
## 3. 信息使用
|
||||
|
||||
我们收集的信息仅用于以下目的:
|
||||
|
||||
### 3.1 功能实现
|
||||
- 游戏目录识别:识别已安装的游戏版本
|
||||
- 文件完整性验证:确保下载文件的完整性和安全性
|
||||
- 下载加速:通过 Cloudflare 优化下载速度
|
||||
|
||||
### 3.2 服务改进
|
||||
- **应用更新**:检查应用版本并推送更新。
|
||||
- **使用情况分析**:通过统计IP地址、ISP和地理位置等信息,分析用户下载次数与软件使用情况,以帮助我们改进服务。您的所有信息都仅用于软件使用统计,不会用于其他特殊目的。
|
||||
- **错误报告**:收集错误信息以改进应用体验。
|
||||
|
||||
## 4. 数据存储
|
||||
|
||||
### 4.1 本地存储
|
||||
- 配置文件:保存在系统临时文件夹的 FRAISEMOE 子目录下
|
||||
- 临时下载文件:保存在系统临时文件夹中
|
||||
- 日志文件:记录程序运行日志
|
||||
|
||||
### 4.2 网络传输
|
||||
所有网络请求均使用安全的 HTTPS 协议进行传输。
|
||||
|
||||
## 5. 修改系统文件
|
||||
|
||||
### 5.1 hosts 文件修改
|
||||
- 当您选择使用 Cloudflare 加速功能时,本应用会临时修改系统 hosts 文件
|
||||
- 修改前会自动创建备份(位于 %SystemRoot%\System32\drivers\etc\hosts.bak.FRAISEMOE Addons Installer NEXT)
|
||||
- 程序退出时会自动恢复原始 hosts 文件
|
||||
|
||||
## 6. 第三方服务
|
||||
|
||||
本应用使用以下第三方服务:
|
||||
|
||||
### 6.1 Cloudflare
|
||||
- 本应用使用第三方开源项目 [CloudflareSpeedTest (CFST)](https://github.com/XIU2/CloudflareSpeedTest/) 为您提供 Cloudflare 加速功能。该优选服务由 CFST 项目提供,本项目及作者不负责其功能的实际维护。
|
||||
- 启用此功能时,CFST 将向 Cloudflare 的所有节点发送请求以测试延迟,此过程不可避免地会将您的 IP 地址提交至 Cloudflare。我们建议您遵循并查阅 Cloudflare 的相关用户协议和隐私政策。
|
||||
|
||||
### 6.2 云端配置服务
|
||||
- 本应用启动时会从云端服务器获取配置信息(如下载链接等)。在此过程中,服务器会获取并统计您的IP地址、地理位置及ISP等信息,以用于软件使用情况分析。
|
||||
- 为确保通信安全和服务的稳定性,云端服务器设置了严格的 User-Agent 校验,仅允许本应用内置的特定 User-Agent 发出请求。非本应用指定的 User-Agent 将无法访问服务。
|
||||
|
||||
## 7. 用户控制
|
||||
|
||||
您对以下功能有完全的控制权:
|
||||
|
||||
- 选择是否使用 Cloudflare 加速功能(需修改 hosts 文件)
|
||||
- 选择安装目录和需要安装的游戏版本
|
||||
- 选择是否终止可能冲突的进程
|
||||
|
||||
## 8. 数据安全
|
||||
|
||||
我们采取以下措施保护您的数据:
|
||||
|
||||
- 本地配置文件不包含敏感个人信息
|
||||
- 网络请求使用安全的 HTTPS 协议
|
||||
- hosts 文件修改会在程序退出时自动恢复
|
||||
|
||||
## 9. 联系我们
|
||||
|
||||
如果您对本隐私政策有任何疑问或建议,请通过 GitHub 项目页面联系我们。
|
||||
|
||||
## 10. 政策更新
|
||||
|
||||
本隐私政策可能会根据应用功能的变化而更新。请定期查看最新版本。
|
||||
|
||||
最后更新日期:2025年8月4日
|
||||
95
README-en.md
Normal file
@@ -0,0 +1,95 @@
|
||||
# 🍓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.
|
||||
4. **System Compatibility**: This application has been tested and works with Windows 10/11 64-bit systems. Other platforms or versions have not been tested.
|
||||
|
||||
---
|
||||
|
||||
## 👨💻 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.
|
||||
- [hosxy/aria2-fast](https://github.com/hosxy/aria2-fast): Provided a modified version of aria2c for improved download speed and performance.
|
||||
|
||||
## 📖 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.
|
||||
18
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) 下载最新版本的应用程序。
|
||||
|
||||

|
||||

|
||||
|
||||
### ❗ 使用步骤
|
||||
|
||||
@@ -73,6 +73,7 @@
|
||||
1. 请勿使用经过二次修改的应用:若使用未知来源或修改后的应用导致个人利益受损,作者和开发人员不承担任何责任。
|
||||
2. 请遵循所有规则:请严格遵守 [使用须知文档](https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/blob/master/FAQ.md) 和本文档中的规则,如有违反,作者和开发人员不承担责任。
|
||||
3. 免费开源:本应用免费、开源,如有通过非免费途径获取,请立即向来源申请退款并积极维权。
|
||||
4. 系统兼容性:本应用已实测可兼容Windows 10/11 64位系统,其他平台或版本未经测试。
|
||||
|
||||
---
|
||||
|
||||
@@ -80,18 +81,13 @@
|
||||
|
||||
- [ouyangqiqi](https://github.com/hyb-oyqq): 本仓库现维护者
|
||||
|
||||
## 💡 注意事项
|
||||
|
||||
1. 请勿使用经过二次修改的应用:若使用未知来源或修改后的应用导致个人利益受损,作者和开发人员不承担任何责任。
|
||||
2. 请遵循所有规则:请严格遵守 [使用须知文档](https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/blob/master/FAQ.md) 和本文档中的规则,如有违反,作者和开发人员不承担责任。
|
||||
3. 免费开源:本应用免费、开源,如有通过非免费途径获取,请立即向来源申请退款并积极维权。
|
||||
|
||||
|
||||
## 🎉 特别鸣谢
|
||||
- [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 优选功能的核心支持。
|
||||
- [hosxy/aria2-fast](https://github.com/hosxy/aria2-fast):提供了修改版aria2c,提高了下载速度和性能。
|
||||
|
||||
## 📖 协议
|
||||
|
||||
|
||||
|
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,9 +1,35 @@
|
||||
import sys
|
||||
from PySide6.QtWidgets import QApplication
|
||||
from PySide6.QtWidgets import QApplication, QMessageBox
|
||||
from main_window import MainWindow
|
||||
from core.privacy_manager import PrivacyManager
|
||||
from utils.logger import setup_logger
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 初始化日志
|
||||
logger = setup_logger("main")
|
||||
logger.info("应用启动")
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
# 初始化隐私协议管理器
|
||||
try:
|
||||
privacy_manager = PrivacyManager()
|
||||
except Exception as e:
|
||||
logger.error(f"初始化隐私协议管理器失败: {e}")
|
||||
QMessageBox.critical(
|
||||
None,
|
||||
"隐私协议加载错误",
|
||||
f"无法加载隐私协议管理器,程序将退出。\n\n错误信息:{e}"
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
# 显示隐私协议对话框
|
||||
if not privacy_manager.show_privacy_dialog():
|
||||
logger.info("用户未同意隐私协议,程序退出")
|
||||
sys.exit(0) # 如果用户不同意隐私协议,退出程序
|
||||
|
||||
# 用户已同意隐私协议,继续启动程序
|
||||
logger.info("隐私协议已同意,启动主程序")
|
||||
window = MainWindow()
|
||||
window.show()
|
||||
sys.exit(app.exec())
|
||||
@@ -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()
|
||||
27
source/core/__init__.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from .animations import MultiStageAnimations
|
||||
from .ui_manager import UIManager
|
||||
from .download_manager import DownloadManager
|
||||
from .debug_manager import DebugManager
|
||||
from .window_manager import WindowManager
|
||||
from .game_detector import GameDetector
|
||||
from .patch_manager import PatchManager
|
||||
from .config_manager import ConfigManager
|
||||
from .privacy_manager import PrivacyManager
|
||||
from .cloudflare_optimizer import CloudflareOptimizer
|
||||
from .download_task_manager import DownloadTaskManager
|
||||
from .extraction_handler import ExtractionHandler
|
||||
|
||||
__all__ = [
|
||||
'MultiStageAnimations',
|
||||
'UIManager',
|
||||
'DownloadManager',
|
||||
'DebugManager',
|
||||
'WindowManager',
|
||||
'GameDetector',
|
||||
'PatchManager',
|
||||
'ConfigManager',
|
||||
'PrivacyManager',
|
||||
'CloudflareOptimizer',
|
||||
'DownloadTaskManager',
|
||||
'ExtractionHandler'
|
||||
]
|
||||
360
source/core/animations.py
Normal file
@@ -0,0 +1,360 @@
|
||||
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()
|
||||
|
||||
# 禁用所有按钮,直到动画完成
|
||||
self.ui.start_install_btn.setEnabled(False)
|
||||
self.ui.uninstall_btn.setEnabled(False)
|
||||
self.ui.exit_btn.setEnabled(False)
|
||||
|
||||
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.ui.start_install_btn.setEnabled(False)
|
||||
self.ui.uninstall_btn.setEnabled(False)
|
||||
self.ui.exit_btn.setEnabled(False)
|
||||
|
||||
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()
|
||||
401
source/core/cloudflare_optimizer.py
Normal file
@@ -0,0 +1,401 @@
|
||||
import os
|
||||
from urllib.parse import urlparse
|
||||
from PySide6 import QtWidgets
|
||||
from PySide6.QtCore import Qt, QTimer
|
||||
from PySide6.QtGui import QIcon, QPixmap
|
||||
|
||||
from utils import msgbox_frame, resource_path
|
||||
from workers import IpOptimizerThread
|
||||
|
||||
|
||||
class CloudflareOptimizer:
|
||||
"""Cloudflare IP优化器,负责处理IP优化和Cloudflare加速相关功能"""
|
||||
|
||||
def __init__(self, main_window, hosts_manager):
|
||||
"""初始化Cloudflare优化器
|
||||
|
||||
Args:
|
||||
main_window: 主窗口实例,用于访问UI和状态
|
||||
hosts_manager: Hosts文件管理器实例
|
||||
"""
|
||||
self.main_window = main_window
|
||||
self.hosts_manager = hosts_manager
|
||||
self.optimized_ip = None
|
||||
self.optimized_ipv6 = None
|
||||
self.optimization_done = False # 标记是否已执行过优选
|
||||
self.countdown_finished = False # 标记倒计时是否结束
|
||||
self.optimizing_msg_box = None
|
||||
self.optimization_cancelled = False
|
||||
self.ip_optimizer_thread = None
|
||||
self.ipv6_optimizer_thread = None
|
||||
|
||||
def is_optimization_done(self):
|
||||
"""检查是否已完成优化
|
||||
|
||||
Returns:
|
||||
bool: 是否已完成优化
|
||||
"""
|
||||
return self.optimization_done
|
||||
|
||||
def is_countdown_finished(self):
|
||||
"""检查倒计时是否已完成
|
||||
|
||||
Returns:
|
||||
bool: 倒计时是否已完成
|
||||
"""
|
||||
return self.countdown_finished
|
||||
|
||||
def get_optimized_ip(self):
|
||||
"""获取优选的IP地址
|
||||
|
||||
Returns:
|
||||
str: 优选的IP地址,如果未优选则为None
|
||||
"""
|
||||
return self.optimized_ip
|
||||
|
||||
def get_optimized_ipv6(self):
|
||||
"""获取优选的IPv6地址
|
||||
|
||||
Returns:
|
||||
str: 优选的IPv6地址,如果未优选则为None
|
||||
"""
|
||||
return self.optimized_ipv6
|
||||
|
||||
def start_ip_optimization(self, url):
|
||||
"""开始IP优化过程
|
||||
|
||||
Args:
|
||||
url: 用于优化的URL
|
||||
"""
|
||||
# 创建取消状态标记
|
||||
self.optimization_cancelled = False
|
||||
self.countdown_finished = False
|
||||
|
||||
# 检查是否启用了IPv6
|
||||
use_ipv6 = False
|
||||
if hasattr(self.main_window, 'config'):
|
||||
use_ipv6 = self.main_window.config.get("ipv6_enabled", False)
|
||||
|
||||
# 如果启用了IPv6,显示警告消息
|
||||
if use_ipv6:
|
||||
ipv6_warning = QtWidgets.QMessageBox(self.main_window)
|
||||
ipv6_warning.setWindowTitle(f"IPv6优选警告 - {self.main_window.APP_NAME}")
|
||||
ipv6_warning.setText("\nIPv6优选比IPv4耗时更长且感知不强(预计耗时10分钟以上),不建议使用。\n\n确定要同时执行IPv6优选吗?\n")
|
||||
ipv6_warning.setIcon(QtWidgets.QMessageBox.Icon.Warning)
|
||||
|
||||
# 设置图标
|
||||
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():
|
||||
ipv6_warning.setWindowIcon(QIcon(pixmap))
|
||||
|
||||
yes_button = ipv6_warning.addButton("是", QtWidgets.QMessageBox.ButtonRole.YesRole)
|
||||
no_button = ipv6_warning.addButton("否,仅使用IPv4", QtWidgets.QMessageBox.ButtonRole.NoRole)
|
||||
cancel_button = ipv6_warning.addButton("取消优选", QtWidgets.QMessageBox.ButtonRole.RejectRole)
|
||||
|
||||
ipv6_warning.setDefaultButton(no_button)
|
||||
ipv6_warning.exec()
|
||||
|
||||
if ipv6_warning.clickedButton() == cancel_button:
|
||||
# 用户取消了优选
|
||||
self.optimization_cancelled = True
|
||||
return
|
||||
|
||||
# 根据用户选择调整IPv6设置
|
||||
if ipv6_warning.clickedButton() == no_button:
|
||||
use_ipv6 = False
|
||||
# 临时覆盖配置(不保存到文件)
|
||||
if hasattr(self.main_window, 'config'):
|
||||
self.main_window.config["ipv6_enabled"] = False
|
||||
|
||||
# 准备提示信息
|
||||
optimization_msg = "\n正在优选Cloudflare IP,请稍候...\n\n"
|
||||
if use_ipv6:
|
||||
optimization_msg += "已启用IPv6支持,同时进行IPv4和IPv6优选。\n这可能需要10分钟以上,请耐心等待喵~\n"
|
||||
else:
|
||||
optimization_msg += "这可能需要5-10分钟,请耐心等待喵~\n"
|
||||
|
||||
# 使用Cloudflare图标创建消息框
|
||||
self.optimizing_msg_box = msgbox_frame(
|
||||
f"通知 - {self.main_window.APP_NAME}",
|
||||
optimization_msg
|
||||
)
|
||||
# 设置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.Cancel)
|
||||
self.optimizing_msg_box.buttonClicked.connect(self._on_optimization_dialog_clicked)
|
||||
self.optimizing_msg_box.setWindowModality(Qt.WindowModality.ApplicationModal)
|
||||
|
||||
# 创建并启动优化线程
|
||||
self.ip_optimizer_thread = IpOptimizerThread(url)
|
||||
self.ip_optimizer_thread.finished.connect(self.on_ipv4_optimization_finished)
|
||||
|
||||
# 如果启用IPv6,同时启动IPv6优化线程
|
||||
if use_ipv6:
|
||||
print("IPv6已启用,将同时优选IPv6地址")
|
||||
self.ipv6_optimizer_thread = IpOptimizerThread(url, use_ipv6=True)
|
||||
self.ipv6_optimizer_thread.finished.connect(self.on_ipv6_optimization_finished)
|
||||
self.ipv6_optimizer_thread.start()
|
||||
|
||||
# 启动IPv4优化线程
|
||||
self.ip_optimizer_thread.start()
|
||||
|
||||
# 显示消息框(非模态,不阻塞)
|
||||
self.optimizing_msg_box.open()
|
||||
|
||||
def _on_optimization_dialog_clicked(self, button):
|
||||
"""处理优化对话框按钮点击
|
||||
|
||||
Args:
|
||||
button: 被点击的按钮
|
||||
"""
|
||||
if button.text() == "Cancel": # 如果是取消按钮
|
||||
# 标记已取消
|
||||
self.optimization_cancelled = True
|
||||
|
||||
# 停止优化线程
|
||||
if hasattr(self, 'ip_optimizer_thread') and self.ip_optimizer_thread and self.ip_optimizer_thread.isRunning():
|
||||
self.ip_optimizer_thread.stop()
|
||||
|
||||
if hasattr(self, 'ipv6_optimizer_thread') and self.ipv6_optimizer_thread and self.ipv6_optimizer_thread.isRunning():
|
||||
self.ipv6_optimizer_thread.stop()
|
||||
|
||||
# 恢复主窗口状态
|
||||
self.main_window.setEnabled(True)
|
||||
self.main_window.ui.start_install_text.setText("开始安装")
|
||||
|
||||
# 显示取消消息
|
||||
QtWidgets.QMessageBox.information(
|
||||
self.main_window,
|
||||
f"已取消 - {self.main_window.APP_NAME}",
|
||||
"\n已取消IP优选和安装过程。\n"
|
||||
)
|
||||
|
||||
def on_ipv4_optimization_finished(self, ip):
|
||||
"""IPv4优化完成后的处理
|
||||
|
||||
Args:
|
||||
ip: 优选的IP地址,如果失败则为空字符串
|
||||
"""
|
||||
# 如果已经取消,则不继续处理
|
||||
if hasattr(self, 'optimization_cancelled') and self.optimization_cancelled:
|
||||
return
|
||||
|
||||
self.optimized_ip = ip
|
||||
print(f"IPv4优选完成,结果: {ip if ip else '未找到合适的IP'}")
|
||||
|
||||
# 检查是否还有IPv6优化正在运行
|
||||
if hasattr(self, 'ipv6_optimizer_thread') and self.ipv6_optimizer_thread and self.ipv6_optimizer_thread.isRunning():
|
||||
print("等待IPv6优选完成...")
|
||||
return
|
||||
|
||||
# 所有优选都已完成,继续处理
|
||||
self.optimization_done = True
|
||||
self.countdown_finished = False # 确保倒计时标志重置
|
||||
|
||||
# 关闭提示框
|
||||
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
|
||||
|
||||
# 处理优选结果
|
||||
self._process_optimization_results()
|
||||
|
||||
def on_ipv6_optimization_finished(self, ipv6):
|
||||
"""IPv6优化完成后的处理
|
||||
|
||||
Args:
|
||||
ipv6: 优选的IPv6地址,如果失败则为空字符串
|
||||
"""
|
||||
# 如果已经取消,则不继续处理
|
||||
if hasattr(self, 'optimization_cancelled') and self.optimization_cancelled:
|
||||
return
|
||||
|
||||
self.optimized_ipv6 = ipv6
|
||||
print(f"IPv6优选完成,结果: {ipv6 if ipv6 else '未找到合适的IPv6'}")
|
||||
|
||||
# 检查IPv4优化是否已完成
|
||||
if hasattr(self, 'ip_optimizer_thread') and self.ip_optimizer_thread and self.ip_optimizer_thread.isRunning():
|
||||
print("等待IPv4优选完成...")
|
||||
return
|
||||
|
||||
# 所有优选都已完成,继续处理
|
||||
self.optimization_done = True
|
||||
self.countdown_finished = False # 确保倒计时标志重置
|
||||
|
||||
# 关闭提示框
|
||||
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
|
||||
|
||||
# 处理优选结果
|
||||
self._process_optimization_results()
|
||||
|
||||
def _process_optimization_results(self):
|
||||
"""处理优选的IP结果,显示相应提示"""
|
||||
use_ipv6 = False
|
||||
if hasattr(self.main_window, 'config'):
|
||||
use_ipv6 = self.main_window.config.get("ipv6_enabled", False)
|
||||
|
||||
# 判断优选结果
|
||||
ipv4_success = bool(self.optimized_ip)
|
||||
ipv6_success = bool(self.optimized_ipv6) if use_ipv6 else False
|
||||
|
||||
# 临时启用窗口以显示对话框
|
||||
self.main_window.setEnabled(True)
|
||||
|
||||
hostname = urlparse(self.main_window.current_url).hostname if hasattr(self.main_window, 'current_url') else None
|
||||
|
||||
if not ipv4_success and (not use_ipv6 or not ipv6_success):
|
||||
# 两种IP都没有优选成功
|
||||
msg_box = QtWidgets.QMessageBox(self.main_window)
|
||||
msg_box.setWindowTitle(f"优选失败 - {self.main_window.APP_NAME}")
|
||||
|
||||
fail_message = "\n未能找到合适的Cloudflare "
|
||||
if use_ipv6:
|
||||
fail_message += "IPv4和IPv6地址"
|
||||
else:
|
||||
fail_message += "IP地址"
|
||||
|
||||
fail_message += ",将使用默认网络进行下载。\n\n10秒后自动继续..."
|
||||
|
||||
msg_box.setText(fail_message)
|
||||
msg_box.setIcon(QtWidgets.QMessageBox.Icon.Warning)
|
||||
ok_button = msg_box.addButton("确定 (10)", QtWidgets.QMessageBox.ButtonRole.AcceptRole)
|
||||
cancel_button = msg_box.addButton("取消安装", QtWidgets.QMessageBox.ButtonRole.RejectRole)
|
||||
|
||||
# 创建计时器实现倒计时
|
||||
countdown = 10
|
||||
timer = 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) # 每秒更新一次
|
||||
|
||||
# 显示对话框并等待用户响应
|
||||
result = msg_box.exec()
|
||||
|
||||
# 停止计时器
|
||||
timer.stop()
|
||||
|
||||
# 如果用户点击了取消安装
|
||||
if msg_box.clickedButton() == cancel_button:
|
||||
# 恢复主窗口状态
|
||||
self.main_window.setEnabled(True)
|
||||
self.main_window.ui.start_install_text.setText("开始安装")
|
||||
return False
|
||||
|
||||
# 用户点击了继续,重新禁用主窗口
|
||||
self.main_window.setEnabled(False)
|
||||
# 标记倒计时已完成
|
||||
self.countdown_finished = True
|
||||
return True
|
||||
else:
|
||||
# 至少有一种IP优选成功
|
||||
success_message = ""
|
||||
if ipv4_success:
|
||||
success_message += f"IPv4: {self.optimized_ip}\n"
|
||||
|
||||
if ipv6_success:
|
||||
success_message += f"IPv6: {self.optimized_ipv6}\n"
|
||||
|
||||
if hostname:
|
||||
# 先清理可能存在的旧记录(只清理一次)
|
||||
self.hosts_manager.clean_hostname_entries(hostname)
|
||||
|
||||
success = False
|
||||
|
||||
# 应用优选IP到hosts文件
|
||||
if ipv4_success:
|
||||
success = self.hosts_manager.apply_ip(hostname, self.optimized_ip, clean=False) or success
|
||||
|
||||
# 如果启用IPv6并且找到了IPv6地址,也应用到hosts
|
||||
if ipv6_success:
|
||||
success = self.hosts_manager.apply_ip(hostname, self.optimized_ipv6, clean=False) or success
|
||||
|
||||
if success:
|
||||
msg_box = QtWidgets.QMessageBox(self.main_window)
|
||||
msg_box.setWindowTitle(f"成功 - {self.main_window.APP_NAME}")
|
||||
msg_box.setText(f"\n已将优选IP应用到hosts文件:\n{success_message}\n10秒后自动继续...")
|
||||
msg_box.setIcon(QtWidgets.QMessageBox.Icon.Information)
|
||||
ok_button = msg_box.addButton("确定 (10)", QtWidgets.QMessageBox.ButtonRole.AcceptRole)
|
||||
cancel_button = msg_box.addButton("取消安装", QtWidgets.QMessageBox.ButtonRole.RejectRole)
|
||||
|
||||
# 创建计时器实现倒计时
|
||||
countdown = 10
|
||||
timer = 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) # 每秒更新一次
|
||||
|
||||
# 显示对话框并等待用户响应
|
||||
result = msg_box.exec()
|
||||
|
||||
# 停止计时器
|
||||
timer.stop()
|
||||
|
||||
# 如果用户点击了取消安装
|
||||
if msg_box.clickedButton() == cancel_button:
|
||||
# 恢复主窗口状态
|
||||
self.main_window.setEnabled(True)
|
||||
self.main_window.ui.start_install_text.setText("开始安装")
|
||||
return False
|
||||
else:
|
||||
QtWidgets.QMessageBox.critical(
|
||||
self.main_window,
|
||||
f"错误 - {self.main_window.APP_NAME}",
|
||||
"\n修改hosts文件失败,请检查程序是否以管理员权限运行。\n"
|
||||
)
|
||||
# 恢复主窗口状态
|
||||
self.main_window.ui.start_install_text.setText("开始安装")
|
||||
return False
|
||||
|
||||
# 用户点击了继续,重新禁用主窗口
|
||||
self.main_window.setEnabled(False)
|
||||
# 标记倒计时已完成
|
||||
self.countdown_finished = True
|
||||
|
||||
return True
|
||||
|
||||
def stop_optimization(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, 'ipv6_optimizer_thread') and self.ipv6_optimizer_thread and self.ipv6_optimizer_thread.isRunning():
|
||||
self.ipv6_optimizer_thread.stop()
|
||||
self.ipv6_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
|
||||
169
source/core/config_manager.py
Normal file
@@ -0,0 +1,169 @@
|
||||
import json
|
||||
import webbrowser
|
||||
from PySide6.QtWidgets import QMessageBox
|
||||
|
||||
from utils import load_config, save_config, msgbox_frame
|
||||
|
||||
class ConfigManager:
|
||||
"""配置管理器,用于处理配置的加载、保存和获取云端配置"""
|
||||
|
||||
def __init__(self, app_name, config_url, ua, debug_manager=None):
|
||||
"""初始化配置管理器
|
||||
|
||||
Args:
|
||||
app_name: 应用程序名称,用于显示消息框标题
|
||||
config_url: 云端配置URL
|
||||
ua: User-Agent字符串
|
||||
debug_manager: 调试管理器实例,用于输出调试信息
|
||||
"""
|
||||
self.app_name = app_name
|
||||
self.config_url = config_url
|
||||
self.ua = ua
|
||||
self.debug_manager = debug_manager
|
||||
self.cloud_config = None
|
||||
self.config_valid = False
|
||||
self.last_error_message = ""
|
||||
|
||||
def _is_debug_mode(self):
|
||||
"""检查是否处于调试模式
|
||||
|
||||
Returns:
|
||||
bool: 是否处于调试模式
|
||||
"""
|
||||
if hasattr(self.debug_manager, 'ui_manager') and hasattr(self.debug_manager.ui_manager, 'debug_action'):
|
||||
return self.debug_manager.ui_manager.debug_action.isChecked()
|
||||
return False
|
||||
|
||||
def load_config(self):
|
||||
"""加载本地配置
|
||||
|
||||
Returns:
|
||||
dict: 加载的配置
|
||||
"""
|
||||
return load_config()
|
||||
|
||||
def save_config(self, config):
|
||||
"""保存配置
|
||||
|
||||
Args:
|
||||
config: 要保存的配置
|
||||
"""
|
||||
save_config(config)
|
||||
|
||||
def fetch_cloud_config(self, config_fetch_thread_class, callback=None):
|
||||
"""获取云端配置
|
||||
|
||||
Args:
|
||||
config_fetch_thread_class: 用于获取云端配置的线程类
|
||||
callback: 获取完成后的回调函数,接受两个参数(data, error_message)
|
||||
"""
|
||||
headers = {"User-Agent": self.ua}
|
||||
debug_mode = self._is_debug_mode()
|
||||
self.config_fetch_thread = config_fetch_thread_class(self.config_url, headers, debug_mode)
|
||||
|
||||
# 如果提供了回调,使用它;否则使用内部的on_config_fetched方法
|
||||
if callback:
|
||||
self.config_fetch_thread.finished.connect(callback)
|
||||
else:
|
||||
self.config_fetch_thread.finished.connect(self.on_config_fetched)
|
||||
|
||||
self.config_fetch_thread.start()
|
||||
|
||||
def on_config_fetched(self, data, error_message):
|
||||
"""云端配置获取完成的回调处理
|
||||
|
||||
Args:
|
||||
data: 获取到的配置数据
|
||||
error_message: 错误信息,如果有
|
||||
"""
|
||||
debug_mode = self._is_debug_mode()
|
||||
|
||||
if error_message:
|
||||
# 标记配置无效
|
||||
self.config_valid = False
|
||||
|
||||
# 记录错误信息,用于按钮点击时显示
|
||||
if error_message == "update_required":
|
||||
self.last_error_message = "update_required"
|
||||
msg_box = msgbox_frame(
|
||||
f"更新提示 - {self.app_name}",
|
||||
"\n当前版本过低,请及时更新。\n",
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
msg_box.exec()
|
||||
# 在浏览器中打开项目主页
|
||||
webbrowser.open("https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/")
|
||||
# 版本过低,应当显示"无法安装"
|
||||
return {"action": "disable_button", "then": "exit"}
|
||||
|
||||
elif "missing_keys" in error_message:
|
||||
self.last_error_message = "missing_keys"
|
||||
missing_versions = error_message.split(":")[1]
|
||||
msg_box = msgbox_frame(
|
||||
f"配置缺失 - {self.app_name}",
|
||||
f'\n云端缺失下载链接,可能云服务器正在维护,不影响其他版本下载。\n当前缺失版本:"{missing_versions}"\n',
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
msg_box.exec()
|
||||
# 对于部分缺失,仍然允许使用,因为可能只影响部分游戏版本
|
||||
self.config_valid = True
|
||||
return {"action": "enable_button"}
|
||||
else:
|
||||
# 设置网络错误标记
|
||||
self.last_error_message = "network_error"
|
||||
|
||||
# 显示通用错误消息,只在debug模式下显示详细错误
|
||||
error_msg = "访问云端配置失败,请检查网络状况或稍后再试。"
|
||||
if debug_mode and "详细错误:" in error_message:
|
||||
msg_box = msgbox_frame(
|
||||
f"错误 - {self.app_name}",
|
||||
f"\n{error_message}\n",
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
else:
|
||||
msg_box = msgbox_frame(
|
||||
f"错误 - {self.app_name}",
|
||||
f"\n{error_msg}\n",
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
msg_box.exec()
|
||||
|
||||
# 网络错误时应当显示"无法安装"
|
||||
return {"action": "disable_button"}
|
||||
else:
|
||||
self.cloud_config = data
|
||||
# 标记配置有效
|
||||
self.config_valid = True
|
||||
# 清除错误信息
|
||||
self.last_error_message = ""
|
||||
|
||||
if debug_mode:
|
||||
print("--- Cloud config fetched successfully ---")
|
||||
print(json.dumps(data, indent=2))
|
||||
|
||||
# 获取配置成功,允许安装
|
||||
return {"action": "enable_button"}
|
||||
|
||||
def is_config_valid(self):
|
||||
"""检查配置是否有效
|
||||
|
||||
Returns:
|
||||
bool: 配置是否有效
|
||||
"""
|
||||
return self.config_valid
|
||||
|
||||
def get_cloud_config(self):
|
||||
"""获取云端配置
|
||||
|
||||
Returns:
|
||||
dict: 云端配置
|
||||
"""
|
||||
return self.cloud_config
|
||||
|
||||
def get_last_error(self):
|
||||
"""获取最后一次错误信息
|
||||
|
||||
Returns:
|
||||
str: 错误信息
|
||||
"""
|
||||
return self.last_error_message
|
||||
82
source/core/debug_manager.py
Normal file
@@ -0,0 +1,82 @@
|
||||
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
|
||||
self.ui_manager = None # 添加ui_manager属性
|
||||
|
||||
def set_ui_manager(self, ui_manager):
|
||||
"""设置UI管理器引用
|
||||
|
||||
Args:
|
||||
ui_manager: UI管理器实例
|
||||
"""
|
||||
self.ui_manager = ui_manager
|
||||
|
||||
def _is_debug_mode(self):
|
||||
"""检查是否处于调试模式
|
||||
|
||||
Returns:
|
||||
bool: 是否处于调试模式
|
||||
"""
|
||||
if hasattr(self, 'ui_manager') and hasattr(self.ui_manager, 'debug_action'):
|
||||
return self.ui_manager.debug_action.isChecked()
|
||||
return False
|
||||
|
||||
def toggle_debug_mode(self, checked):
|
||||
"""切换调试模式
|
||||
|
||||
Args:
|
||||
checked: 是否启用调试模式
|
||||
"""
|
||||
print(f"Toggle debug mode: {checked}")
|
||||
self.main_window.config["debug_mode"] = checked
|
||||
self.main_window.save_config(self.main_window.config)
|
||||
|
||||
# 更新打开log文件按钮状态
|
||||
if hasattr(self, 'ui_manager') and hasattr(self.ui_manager, 'open_log_action'):
|
||||
self.ui_manager.open_log_action.setEnabled(checked)
|
||||
|
||||
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
|
||||
642
source/core/download_manager.py
Normal file
@@ -0,0 +1,642 @@
|
||||
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, QFont
|
||||
|
||||
from utils import msgbox_frame, HostsManager, resource_path
|
||||
from data.config import APP_NAME, PLUGIN, GAME_INFO, UA, CONFIG_URL, DOWNLOAD_THREADS, DEFAULT_DOWNLOAD_THREAD_LEVEL
|
||||
from workers import IpOptimizerThread
|
||||
from core.cloudflare_optimizer import CloudflareOptimizer
|
||||
from core.download_task_manager import DownloadTaskManager
|
||||
from core.extraction_handler import ExtractionHandler
|
||||
|
||||
class DownloadManager:
|
||||
def __init__(self, main_window):
|
||||
"""初始化下载管理器
|
||||
|
||||
Args:
|
||||
main_window: 主窗口实例,用于访问UI和状态
|
||||
"""
|
||||
self.main_window = main_window
|
||||
self.main_window.APP_NAME = APP_NAME # 为了让子模块能够访问APP_NAME
|
||||
self.selected_folder = ""
|
||||
self.download_queue = deque()
|
||||
self.current_download_thread = None
|
||||
self.hosts_manager = HostsManager()
|
||||
|
||||
# 添加下载线程级别
|
||||
self.download_thread_level = DEFAULT_DOWNLOAD_THREAD_LEVEL
|
||||
|
||||
# 初始化子模块
|
||||
self.cloudflare_optimizer = CloudflareOptimizer(main_window, self.hosts_manager)
|
||||
self.download_task_manager = DownloadTaskManager(main_window, self.download_thread_level)
|
||||
self.extraction_handler = ExtractionHandler(main_window)
|
||||
|
||||
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.main_window.ui.start_install_text.setText("正在安装")
|
||||
|
||||
# 禁用整个主窗口,防止用户操作
|
||||
self.main_window.setEnabled(False)
|
||||
|
||||
self.download_action()
|
||||
|
||||
def get_install_paths(self):
|
||||
"""获取所有游戏版本的安装路径"""
|
||||
# 使用改进的目录识别功能
|
||||
game_dirs = self.main_window.game_detector.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}")
|
||||
|
||||
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):
|
||||
"""开始下载流程"""
|
||||
# 主窗口在file_dialog中已被禁用
|
||||
|
||||
# 清空下载历史记录
|
||||
self.main_window.download_queue_history = []
|
||||
|
||||
# 使用改进的目录识别功能
|
||||
game_dirs = self.main_window.game_detector.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"
|
||||
)
|
||||
# 恢复主窗口
|
||||
self.main_window.setEnabled(True)
|
||||
self.main_window.ui.start_install_text.setText("开始安装")
|
||||
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)
|
||||
# 使用lambda连接,传递game_dirs参数
|
||||
self.main_window.hash_thread.pre_finished.connect(
|
||||
lambda updated_status: self.on_pre_hash_finished_with_dirs(updated_status, game_dirs)
|
||||
)
|
||||
self.main_window.hash_thread.start()
|
||||
|
||||
def on_pre_hash_finished_with_dirs(self, updated_status, game_dirs):
|
||||
"""优化的哈希预检查完成处理,带有游戏目录信息
|
||||
|
||||
Args:
|
||||
updated_status: 更新后的安装状态
|
||||
game_dirs: 识别到的游戏目录
|
||||
"""
|
||||
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
|
||||
|
||||
debug_mode = self.is_debug_mode()
|
||||
|
||||
# 临时启用窗口以显示选择对话框
|
||||
self.main_window.setEnabled(True)
|
||||
|
||||
# 获取可安装的游戏版本列表(尚未安装补丁的版本)
|
||||
installable_games = []
|
||||
already_installed_games = []
|
||||
for game_version, game_dir in game_dirs.items():
|
||||
if self.main_window.installed_status.get(game_version, False):
|
||||
if debug_mode:
|
||||
print(f"DEBUG: {game_version} 已安装补丁,不需要再次安装")
|
||||
already_installed_games.append(game_version)
|
||||
else:
|
||||
if debug_mode:
|
||||
print(f"DEBUG: {game_version} 未安装补丁,可以安装")
|
||||
installable_games.append(game_version)
|
||||
|
||||
# 显示状态消息
|
||||
status_message = ""
|
||||
if already_installed_games:
|
||||
status_message += f"已安装补丁的游戏:\n{chr(10).join(already_installed_games)}\n\n"
|
||||
|
||||
if not installable_games:
|
||||
# 如果没有可安装的游戏
|
||||
QtWidgets.QMessageBox.information(
|
||||
self.main_window,
|
||||
f"信息 - {APP_NAME}",
|
||||
f"\n所有检测到的游戏都已安装补丁。\n\n{status_message}"
|
||||
)
|
||||
# 恢复主窗口
|
||||
self.main_window.setEnabled(True)
|
||||
self.main_window.ui.start_install_text.setText("开始安装")
|
||||
return
|
||||
|
||||
# 如果有可安装的游戏版本,让用户选择
|
||||
from PySide6.QtWidgets import QInputDialog, QListWidget, QVBoxLayout, QDialog, QLabel, QPushButton, QAbstractItemView, QHBoxLayout
|
||||
|
||||
# 创建自定义选择对话框
|
||||
dialog = QDialog(self.main_window)
|
||||
dialog.setWindowTitle("选择要安装的游戏")
|
||||
dialog.resize(400, 300)
|
||||
|
||||
layout = QVBoxLayout(dialog)
|
||||
|
||||
# 先显示已安装补丁的游戏
|
||||
if already_installed_games:
|
||||
already_installed_label = QLabel("已安装补丁的游戏:", dialog)
|
||||
already_installed_label.setFont(QFont(already_installed_label.font().family(), already_installed_label.font().pointSize(), QFont.Bold))
|
||||
layout.addWidget(already_installed_label)
|
||||
|
||||
already_installed_list = QLabel(chr(10).join(already_installed_games), dialog)
|
||||
layout.addWidget(already_installed_list)
|
||||
|
||||
# 添加一些间距
|
||||
layout.addSpacing(10)
|
||||
|
||||
# 添加"请选择你需要安装补丁的游戏"的标签
|
||||
info_label = QLabel("请选择你需要安装补丁的游戏:", dialog)
|
||||
info_label.setFont(QFont(info_label.font().family(), info_label.font().pointSize(), QFont.Bold))
|
||||
layout.addWidget(info_label)
|
||||
|
||||
# 添加列表控件
|
||||
list_widget = QListWidget(dialog)
|
||||
list_widget.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) # 允许多选
|
||||
for game in installable_games:
|
||||
list_widget.addItem(game)
|
||||
layout.addWidget(list_widget)
|
||||
|
||||
# 添加全选按钮
|
||||
select_all_btn = QPushButton("全选", dialog)
|
||||
select_all_btn.clicked.connect(lambda: list_widget.selectAll())
|
||||
layout.addWidget(select_all_btn)
|
||||
|
||||
# 添加确定和取消按钮
|
||||
buttons_layout = QHBoxLayout()
|
||||
ok_button = QPushButton("确定", dialog)
|
||||
cancel_button = QPushButton("取消", dialog)
|
||||
buttons_layout.addWidget(ok_button)
|
||||
buttons_layout.addWidget(cancel_button)
|
||||
layout.addLayout(buttons_layout)
|
||||
|
||||
# 连接按钮事件
|
||||
ok_button.clicked.connect(dialog.accept)
|
||||
cancel_button.clicked.connect(dialog.reject)
|
||||
|
||||
# 显示对话框并等待用户选择
|
||||
result = dialog.exec()
|
||||
|
||||
if result != QDialog.DialogCode.Accepted or list_widget.selectedItems() == []:
|
||||
# 用户取消或未选择任何游戏
|
||||
self.main_window.setEnabled(True)
|
||||
self.main_window.ui.start_install_text.setText("开始安装")
|
||||
return
|
||||
|
||||
# 获取用户选择的游戏
|
||||
selected_games = [item.text() for item in list_widget.selectedItems()]
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 用户选择了以下游戏进行安装: {selected_games}")
|
||||
|
||||
# 过滤game_dirs,只保留选中的游戏
|
||||
selected_game_dirs = {game: game_dirs[game] for game in selected_games if game in game_dirs}
|
||||
|
||||
# 重新禁用窗口
|
||||
self.main_window.setEnabled(False)
|
||||
|
||||
# 获取下载配置
|
||||
config = self.get_download_url()
|
||||
if not config:
|
||||
QtWidgets.QMessageBox.critical(
|
||||
self.main_window, f"错误 - {APP_NAME}", "\n网络状态异常或服务器故障,请重试\n"
|
||||
)
|
||||
# 网络故障时,恢复主窗口
|
||||
self.main_window.setEnabled(True)
|
||||
self.main_window.ui.start_install_text.setText("开始安装")
|
||||
return
|
||||
|
||||
# 填充下载队列,传入选定的游戏目录
|
||||
self._fill_download_queue(config, selected_game_dirs)
|
||||
|
||||
# 如果没有需要下载的内容,直接进行最终校验
|
||||
if not self.download_queue:
|
||||
self.main_window.after_hash_compare()
|
||||
return
|
||||
|
||||
# 询问是否使用Cloudflare加速
|
||||
self._show_cloudflare_option()
|
||||
|
||||
def _fill_download_queue(self, config, game_dirs):
|
||||
"""填充下载队列
|
||||
|
||||
Args:
|
||||
config: 包含下载URL的配置字典
|
||||
game_dirs: 包含游戏文件夹路径的字典
|
||||
"""
|
||||
# 清空现有队列
|
||||
self.download_queue.clear()
|
||||
|
||||
# 创建下载历史记录列表,用于跟踪本次安装的游戏
|
||||
if not hasattr(self.main_window, 'download_queue_history'):
|
||||
self.main_window.download_queue_history = []
|
||||
|
||||
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}"
|
||||
# 只处理game_dirs中包含的游戏版本(如果用户选择了特定版本)
|
||||
if game_version in game_dirs and not self.main_window.installed_status.get(game_version, False):
|
||||
url = config.get(f"vol{i}")
|
||||
if not url: continue
|
||||
|
||||
# 获取识别到的游戏文件夹路径
|
||||
game_folder = game_dirs[game_version]
|
||||
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"
|
||||
# 只处理game_dirs中包含的游戏版本(如果用户选择了特定版本)
|
||||
if game_version in game_dirs and not self.main_window.installed_status.get(game_version, False):
|
||||
url = config.get("after")
|
||||
if url:
|
||||
# 获取识别到的游戏文件夹路径
|
||||
game_folder = game_dirs[game_version]
|
||||
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 _show_cloudflare_option(self):
|
||||
"""显示Cloudflare加速选择对话框"""
|
||||
# 临时启用窗口以显示对话框
|
||||
self.main_window.setEnabled(True)
|
||||
|
||||
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)
|
||||
cancel_button = msg_box.addButton("取消安装", QtWidgets.QMessageBox.ButtonRole.RejectRole)
|
||||
|
||||
msg_box.exec()
|
||||
|
||||
clicked_button = msg_box.clickedButton()
|
||||
if clicked_button == cancel_button:
|
||||
# 用户取消了安装,保持主窗口启用
|
||||
self.main_window.setEnabled(True)
|
||||
self.main_window.ui.start_install_text.setText("开始安装")
|
||||
self.download_queue.clear() # 清空下载队列
|
||||
return
|
||||
|
||||
# 用户点击了继续按钮,重新禁用主窗口
|
||||
self.main_window.setEnabled(False)
|
||||
|
||||
use_optimization = clicked_button == yes_button
|
||||
|
||||
if use_optimization and not self.cloudflare_optimizer.is_optimization_done():
|
||||
first_url = self.download_queue[0][0]
|
||||
# 保存当前URL供CloudflareOptimizer使用
|
||||
self.main_window.current_url = first_url
|
||||
# 使用CloudflareOptimizer进行IP优化
|
||||
self.cloudflare_optimizer.start_ip_optimization(first_url)
|
||||
# 等待CloudflareOptimizer的回调
|
||||
# on_optimization_finished会被调用,然后决定是否继续
|
||||
QtCore.QTimer.singleShot(100, self.check_optimization_status)
|
||||
else:
|
||||
# 如果用户选择不优化,或已经优化过,直接开始下载
|
||||
self.next_download_task()
|
||||
|
||||
def check_optimization_status(self):
|
||||
"""检查IP优化状态并继续下载流程"""
|
||||
# 必须同时满足:优化已完成且倒计时已结束
|
||||
if self.cloudflare_optimizer.is_optimization_done() and self.cloudflare_optimizer.is_countdown_finished():
|
||||
self.next_download_task()
|
||||
else:
|
||||
# 否则,继续等待100ms后再次检查
|
||||
QtCore.QTimer.singleShot(100, self.check_optimization_status)
|
||||
|
||||
def next_download_task(self):
|
||||
"""处理下载队列中的下一个任务"""
|
||||
if not self.download_queue:
|
||||
self.main_window.after_hash_compare()
|
||||
return
|
||||
|
||||
# 检查下载线程是否仍在运行,以避免在手动停止后立即开始下一个任务
|
||||
if self.download_task_manager.current_download_thread and self.download_task_manager.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_folder是从已验证的game_dirs中获取的
|
||||
game_exe_exists = True
|
||||
|
||||
# 检查游戏是否已安装
|
||||
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()
|
||||
|
||||
# 从CloudflareOptimizer获取已优选的IP
|
||||
self.optimized_ip = self.cloudflare_optimizer.get_optimized_ip()
|
||||
if self.optimized_ip:
|
||||
print(f"已为 {game_version} 获取到优选IP: {self.optimized_ip}")
|
||||
else:
|
||||
print(f"未能为 {game_version} 获取优选IP,将使用默认线路。")
|
||||
|
||||
# 使用DownloadTaskManager开始下载
|
||||
self.download_task_manager.start_download(url, _7z_path, game_version, game_folder, plugin_path)
|
||||
|
||||
# 连接到主窗口中的下载完成处理函数
|
||||
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 and self.main_window.progress_window.isVisible():
|
||||
self.main_window.progress_window.reject()
|
||||
self.main_window.progress_window = None
|
||||
|
||||
# 处理下载失败
|
||||
if not success:
|
||||
print(f"--- Download Failed: {game_version} ---")
|
||||
print(error)
|
||||
print("------------------------------------")
|
||||
|
||||
# 临时启用窗口以显示对话框
|
||||
self.main_window.setEnabled(True)
|
||||
|
||||
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.main_window.setEnabled(False)
|
||||
self.download_setting(url, game_folder, game_version, _7z_path, plugin_path)
|
||||
elif clicked_button == next_button:
|
||||
# 继续下一个,重新禁用窗口
|
||||
self.main_window.setEnabled(False)
|
||||
self.next_download_task()
|
||||
else:
|
||||
# 结束,保持窗口启用
|
||||
self.on_download_stopped()
|
||||
return
|
||||
|
||||
# 下载成功,使用ExtractionHandler开始解压缩
|
||||
self.extraction_handler.start_extraction(_7z_path, game_folder, plugin_path, game_version)
|
||||
# extraction_handler的回调会处理下一步操作
|
||||
|
||||
def on_extraction_finished(self, continue_download):
|
||||
"""解压完成后的回调,决定是否继续下载队列
|
||||
|
||||
Args:
|
||||
continue_download: 是否继续下载队列中的下一个任务
|
||||
"""
|
||||
if continue_download:
|
||||
# 继续下一个下载任务
|
||||
self.next_download_task()
|
||||
else:
|
||||
# 清空剩余队列并显示结果
|
||||
self.download_queue.clear()
|
||||
self.main_window.show_result()
|
||||
|
||||
def on_download_stopped(self):
|
||||
"""当用户点击停止按钮或选择结束时调用的函数"""
|
||||
# 停止IP优化线程
|
||||
self.cloudflare_optimizer.stop_optimization()
|
||||
|
||||
# 停止当前可能仍在运行的下载线程
|
||||
self.download_task_manager.stop_download()
|
||||
|
||||
# 清空下载队列,因为用户决定停止
|
||||
self.download_queue.clear()
|
||||
|
||||
# 确保进度窗口已关闭
|
||||
if hasattr(self.main_window, 'progress_window') and self.main_window.progress_window:
|
||||
if self.main_window.progress_window.isVisible():
|
||||
self.main_window.progress_window.reject()
|
||||
self.main_window.progress_window = None
|
||||
|
||||
# 退出应用程序
|
||||
print("下载已全部停止。")
|
||||
|
||||
# 恢复主窗口状态
|
||||
self.main_window.setEnabled(True)
|
||||
self.main_window.ui.start_install_text.setText("开始安装")
|
||||
|
||||
# 显示取消安装的消息
|
||||
QtWidgets.QMessageBox.information(
|
||||
self.main_window,
|
||||
f"已取消 - {APP_NAME}",
|
||||
"\n已成功取消安装进程。\n"
|
||||
)
|
||||
|
||||
# 以下方法委托给DownloadTaskManager
|
||||
def get_download_thread_count(self):
|
||||
"""获取当前下载线程设置对应的线程数"""
|
||||
return self.download_task_manager.get_download_thread_count()
|
||||
|
||||
def set_download_thread_level(self, level):
|
||||
"""设置下载线程级别"""
|
||||
return self.download_task_manager.set_download_thread_level(level)
|
||||
|
||||
def show_download_thread_settings(self):
|
||||
"""显示下载线程设置对话框"""
|
||||
return self.download_task_manager.show_download_thread_settings()
|
||||
221
source/core/download_task_manager.py
Normal file
@@ -0,0 +1,221 @@
|
||||
from PySide6 import QtWidgets
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtWidgets import QDialog, QVBoxLayout, QRadioButton, QPushButton, QLabel, QButtonGroup, QHBoxLayout
|
||||
from PySide6.QtGui import QFont
|
||||
|
||||
from data.config import DOWNLOAD_THREADS
|
||||
|
||||
|
||||
class DownloadTaskManager:
|
||||
"""下载任务管理器,负责管理下载任务和线程设置"""
|
||||
|
||||
def __init__(self, main_window, download_thread_level="medium"):
|
||||
"""初始化下载任务管理器
|
||||
|
||||
Args:
|
||||
main_window: 主窗口实例,用于访问UI和状态
|
||||
download_thread_level: 下载线程级别,默认为"medium"
|
||||
"""
|
||||
self.main_window = main_window
|
||||
self.APP_NAME = main_window.APP_NAME if hasattr(main_window, 'APP_NAME') else ""
|
||||
self.current_download_thread = None
|
||||
self.download_thread_level = download_thread_level
|
||||
|
||||
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: 插件路径
|
||||
"""
|
||||
# 按钮在file_dialog中已设置为禁用状态
|
||||
|
||||
# 创建并连接下载线程
|
||||
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.main_window.download_manager.on_download_finished(
|
||||
success,
|
||||
error,
|
||||
url,
|
||||
game_folder,
|
||||
game_version,
|
||||
_7z_path,
|
||||
plugin_path,
|
||||
)
|
||||
)
|
||||
|
||||
# 连接停止按钮到download_manager的on_download_stopped方法
|
||||
self.main_window.progress_window.stop_button.clicked.connect(self.main_window.download_manager.on_download_stopped)
|
||||
|
||||
# 连接暂停/恢复按钮
|
||||
self.main_window.progress_window.pause_resume_button.clicked.connect(self.toggle_download_pause)
|
||||
|
||||
# 启动线程和显示进度窗口
|
||||
self.current_download_thread.start()
|
||||
self.main_window.progress_window.exec()
|
||||
|
||||
def toggle_download_pause(self):
|
||||
"""切换下载的暂停/恢复状态"""
|
||||
if not self.current_download_thread:
|
||||
return
|
||||
|
||||
# 获取当前暂停状态
|
||||
is_paused = self.current_download_thread.is_paused()
|
||||
|
||||
if is_paused:
|
||||
# 如果已暂停,则恢复下载
|
||||
success = self.current_download_thread.resume()
|
||||
if success:
|
||||
self.main_window.progress_window.update_pause_button_state(False)
|
||||
else:
|
||||
# 如果未暂停,则暂停下载
|
||||
success = self.current_download_thread.pause()
|
||||
if success:
|
||||
self.main_window.progress_window.update_pause_button_state(True)
|
||||
|
||||
def get_download_thread_count(self):
|
||||
"""获取当前下载线程设置对应的线程数
|
||||
|
||||
Returns:
|
||||
int: 下载线程数
|
||||
"""
|
||||
# 获取当前线程级别对应的线程数
|
||||
thread_count = DOWNLOAD_THREADS.get(self.download_thread_level, DOWNLOAD_THREADS["medium"])
|
||||
return thread_count
|
||||
|
||||
def set_download_thread_level(self, level):
|
||||
"""设置下载线程级别
|
||||
|
||||
Args:
|
||||
level: 线程级别 (low, medium, high, extreme, insane)
|
||||
|
||||
Returns:
|
||||
bool: 设置是否成功
|
||||
"""
|
||||
if level in DOWNLOAD_THREADS:
|
||||
old_level = self.download_thread_level
|
||||
self.download_thread_level = level
|
||||
|
||||
# 只有非极端级别才保存到配置
|
||||
if level not in ["extreme", "insane"]:
|
||||
if hasattr(self.main_window, 'config'):
|
||||
self.main_window.config["download_thread_level"] = level
|
||||
self.main_window.save_config(self.main_window.config)
|
||||
|
||||
return True
|
||||
return False
|
||||
|
||||
def show_download_thread_settings(self):
|
||||
"""显示下载线程设置对话框"""
|
||||
# 创建对话框
|
||||
dialog = QDialog(self.main_window)
|
||||
dialog.setWindowTitle(f"下载线程设置 - {self.APP_NAME}")
|
||||
dialog.setMinimumWidth(350)
|
||||
|
||||
layout = QVBoxLayout(dialog)
|
||||
|
||||
# 添加说明标签
|
||||
info_label = QLabel("选择下载线程数量(更多线程通常可以提高下载速度):", dialog)
|
||||
info_label.setWordWrap(True)
|
||||
layout.addWidget(info_label)
|
||||
|
||||
# 创建按钮组
|
||||
button_group = QButtonGroup(dialog)
|
||||
|
||||
# 添加线程选项
|
||||
thread_options = {
|
||||
"low": f"低速 - {DOWNLOAD_THREADS['low']}线程(慢慢来,不着急)",
|
||||
"medium": f"中速 - {DOWNLOAD_THREADS['medium']}线程(快人半步)",
|
||||
"high": f"高速 - {DOWNLOAD_THREADS['high']}线程(默认,推荐配置)",
|
||||
"extreme": f"极速 - {DOWNLOAD_THREADS['extreme']}线程(如果你对你的网和电脑很自信的话)",
|
||||
"insane": f"狂暴 - {DOWNLOAD_THREADS['insane']}线程(看看是带宽和性能先榨干还是牛牛先榨干)"
|
||||
}
|
||||
|
||||
radio_buttons = {}
|
||||
|
||||
for level, text in thread_options.items():
|
||||
radio = QRadioButton(text, dialog)
|
||||
|
||||
# 选中当前使用的线程级别
|
||||
if level == self.download_thread_level:
|
||||
radio.setChecked(True)
|
||||
|
||||
button_group.addButton(radio)
|
||||
layout.addWidget(radio)
|
||||
radio_buttons[level] = radio
|
||||
|
||||
layout.addSpacing(10)
|
||||
|
||||
# 添加按钮区域
|
||||
btn_layout = QHBoxLayout()
|
||||
|
||||
ok_button = QPushButton("确定", dialog)
|
||||
cancel_button = QPushButton("取消", dialog)
|
||||
|
||||
btn_layout.addWidget(ok_button)
|
||||
btn_layout.addWidget(cancel_button)
|
||||
|
||||
layout.addLayout(btn_layout)
|
||||
|
||||
# 连接按钮事件
|
||||
ok_button.clicked.connect(dialog.accept)
|
||||
cancel_button.clicked.connect(dialog.reject)
|
||||
|
||||
# 显示对话框
|
||||
result = dialog.exec()
|
||||
|
||||
# 处理结果
|
||||
if result == QDialog.DialogCode.Accepted:
|
||||
# 获取用户选择的线程级别
|
||||
selected_level = None
|
||||
for level, radio in radio_buttons.items():
|
||||
if radio.isChecked():
|
||||
selected_level = level
|
||||
break
|
||||
|
||||
if selected_level:
|
||||
# 为极速和狂暴模式显示警告
|
||||
if selected_level in ["extreme", "insane"]:
|
||||
warning_result = QtWidgets.QMessageBox.warning(
|
||||
self.main_window,
|
||||
f"高风险警告 - {self.APP_NAME}",
|
||||
"警告!过高的线程数可能导致CPU负载过高或其他恶性问题!\n你确定要这么做吗?",
|
||||
QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No,
|
||||
QtWidgets.QMessageBox.StandardButton.No
|
||||
)
|
||||
|
||||
if warning_result != QtWidgets.QMessageBox.StandardButton.Yes:
|
||||
return False
|
||||
|
||||
success = self.set_download_thread_level(selected_level)
|
||||
|
||||
if success:
|
||||
# 显示设置成功消息
|
||||
thread_count = DOWNLOAD_THREADS[selected_level]
|
||||
message = f"\n已成功设置下载线程为: {thread_count}线程\n"
|
||||
|
||||
# 对于极速和狂暴模式,添加仅本次生效的提示
|
||||
if selected_level in ["extreme", "insane"]:
|
||||
message += "\n注意:极速/狂暴模式仅本次生效。软件重启后将恢复默认设置。\n"
|
||||
|
||||
QtWidgets.QMessageBox.information(
|
||||
self.main_window,
|
||||
f"设置成功 - {self.APP_NAME}",
|
||||
message
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def stop_download(self):
|
||||
"""停止当前下载线程"""
|
||||
if self.current_download_thread and self.current_download_thread.isRunning():
|
||||
self.current_download_thread.stop()
|
||||
self.current_download_thread.wait() # 等待线程完全终止
|
||||
return True
|
||||
return False
|
||||
81
source/core/extraction_handler.py
Normal file
@@ -0,0 +1,81 @@
|
||||
import os
|
||||
from PySide6 import QtWidgets
|
||||
from PySide6.QtWidgets import QMessageBox
|
||||
|
||||
|
||||
class ExtractionHandler:
|
||||
"""解压处理器,负责管理解压任务和结果处理"""
|
||||
|
||||
def __init__(self, main_window):
|
||||
"""初始化解压处理器
|
||||
|
||||
Args:
|
||||
main_window: 主窗口实例,用于访问UI和状态
|
||||
"""
|
||||
self.main_window = main_window
|
||||
self.APP_NAME = main_window.APP_NAME if hasattr(main_window, 'APP_NAME') else ""
|
||||
|
||||
def start_extraction(self, _7z_path, game_folder, plugin_path, game_version):
|
||||
"""开始解压任务
|
||||
|
||||
Args:
|
||||
_7z_path: 7z文件路径
|
||||
game_folder: 游戏文件夹路径
|
||||
plugin_path: 插件路径
|
||||
game_version: 游戏版本名称
|
||||
"""
|
||||
# 显示解压中的消息窗口
|
||||
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()
|
||||
self.main_window.hash_msg_box = None
|
||||
|
||||
# 处理解压结果
|
||||
if not success:
|
||||
# 临时启用窗口以显示错误消息
|
||||
self.main_window.setEnabled(True)
|
||||
|
||||
QtWidgets.QMessageBox.critical(self.main_window, f"错误 - {self.APP_NAME}", error_message)
|
||||
self.main_window.installed_status[game_version] = False
|
||||
|
||||
# 询问用户是否继续其他游戏的安装
|
||||
reply = QtWidgets.QMessageBox.question(
|
||||
self.main_window,
|
||||
f"继续安装? - {self.APP_NAME}",
|
||||
f"\n{game_version} 的补丁安装失败。\n\n是否继续安装其他游戏的补丁?\n",
|
||||
QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No,
|
||||
QtWidgets.QMessageBox.StandardButton.Yes
|
||||
)
|
||||
|
||||
if reply == QtWidgets.QMessageBox.StandardButton.Yes:
|
||||
# 继续下一个,重新禁用窗口
|
||||
self.main_window.setEnabled(False)
|
||||
# 通知DownloadManager继续下一个下载任务
|
||||
self.main_window.download_manager.on_extraction_finished(True)
|
||||
else:
|
||||
# 用户选择停止,保持窗口启用状态
|
||||
self.main_window.ui.start_install_text.setText("开始安装")
|
||||
# 通知DownloadManager停止下载队列
|
||||
self.main_window.download_manager.on_extraction_finished(False)
|
||||
else:
|
||||
# 更新安装状态
|
||||
self.main_window.installed_status[game_version] = True
|
||||
# 通知DownloadManager继续下一个下载任务
|
||||
self.main_window.download_manager.on_extraction_finished(True)
|
||||
311
source/core/game_detector.py
Normal file
@@ -0,0 +1,311 @@
|
||||
import os
|
||||
import re
|
||||
|
||||
class GameDetector:
|
||||
"""游戏检测器,用于识别游戏目录和版本"""
|
||||
|
||||
def __init__(self, game_info, debug_manager=None):
|
||||
"""初始化游戏检测器
|
||||
|
||||
Args:
|
||||
game_info: 游戏信息字典,包含各版本的安装路径和可执行文件名
|
||||
debug_manager: 调试管理器实例,用于输出调试信息
|
||||
"""
|
||||
self.game_info = game_info
|
||||
self.debug_manager = debug_manager
|
||||
|
||||
def _is_debug_mode(self):
|
||||
"""检查是否处于调试模式
|
||||
|
||||
Returns:
|
||||
bool: 是否处于调试模式
|
||||
"""
|
||||
if hasattr(self.debug_manager, 'ui_manager') and hasattr(self.debug_manager.ui_manager, 'debug_action'):
|
||||
return self.debug_manager.ui_manager.debug_action.isChecked()
|
||||
return False
|
||||
|
||||
def identify_game_version(self, game_dir):
|
||||
"""识别游戏版本
|
||||
|
||||
Args:
|
||||
game_dir: 游戏目录路径
|
||||
|
||||
Returns:
|
||||
str: 游戏版本名称,如果不是有效的游戏目录则返回None
|
||||
"""
|
||||
debug_mode = self._is_debug_mode()
|
||||
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 尝试识别游戏版本: {game_dir}")
|
||||
|
||||
# 先通过目录名称进行初步推测(这将作为递归搜索的提示)
|
||||
dir_name = os.path.basename(game_dir).lower()
|
||||
potential_version = None
|
||||
vol_num = None
|
||||
|
||||
# 提取卷号或判断是否是After
|
||||
if "vol" in dir_name or "vol." in dir_name:
|
||||
vol_match = re.search(r"vol(?:\.|\s*)?(\d+)", dir_name)
|
||||
if vol_match:
|
||||
vol_num = vol_match.group(1)
|
||||
potential_version = f"NEKOPARA Vol.{vol_num}"
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 从目录名推测游戏版本: {potential_version}, 卷号: {vol_num}")
|
||||
elif "after" in dir_name:
|
||||
potential_version = "NEKOPARA After"
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 从目录名推测游戏版本: NEKOPARA After")
|
||||
|
||||
# 检查是否为NEKOPARA游戏目录
|
||||
# 通过检查游戏可执行文件来识别游戏版本
|
||||
for game_version, info in self.game_info.items():
|
||||
# 尝试多种可能的可执行文件名变体
|
||||
exe_variants = [
|
||||
info["exe"], # 标准文件名
|
||||
info["exe"] + ".nocrack", # Steam加密版本
|
||||
info["exe"].replace(".exe", ""), # 无扩展名版本
|
||||
info["exe"].replace("NEKOPARA", "nekopara").lower(), # 全小写变体
|
||||
info["exe"].lower(), # 小写变体
|
||||
info["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(game_dir, exe_variant)
|
||||
if os.path.exists(exe_path):
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 通过可执行文件确认游戏版本: {game_version}, 文件: {exe_variant}")
|
||||
return game_version
|
||||
|
||||
# 如果没有直接匹配,尝试递归搜索
|
||||
if potential_version:
|
||||
# 从预测的版本中获取卷号或确认是否是After
|
||||
is_after = "After" in potential_version
|
||||
if not vol_num and not is_after:
|
||||
vol_match = re.search(r"Vol\.(\d+)", potential_version)
|
||||
if vol_match:
|
||||
vol_num = vol_match.group(1)
|
||||
|
||||
# 递归搜索可执行文件
|
||||
for root, dirs, files in os.walk(game_dir):
|
||||
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)):
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 通过递归搜索确认游戏版本: {potential_version}, 文件: {file}")
|
||||
return potential_version
|
||||
|
||||
# 如果仍然没有找到,基于目录名的推测返回结果
|
||||
if potential_version:
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 基于目录名返回推测的游戏版本: {potential_version}")
|
||||
return potential_version
|
||||
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 无法识别游戏版本: {game_dir}")
|
||||
|
||||
return None
|
||||
|
||||
def identify_game_directories_improved(self, selected_folder):
|
||||
"""改进的游戏目录识别,支持大小写不敏感和特殊字符处理
|
||||
|
||||
Args:
|
||||
selected_folder: 选择的上级目录
|
||||
|
||||
Returns:
|
||||
dict: 游戏版本到游戏目录的映射
|
||||
"""
|
||||
debug_mode = self._is_debug_mode()
|
||||
|
||||
if debug_mode:
|
||||
print(f"--- 开始识别目录: {selected_folder} ---")
|
||||
|
||||
game_paths = {}
|
||||
|
||||
# 获取上级目录中的所有文件夹
|
||||
try:
|
||||
all_dirs = [d for d in os.listdir(selected_folder) if os.path.isdir(os.path.join(selected_folder, d))]
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 找到以下子目录: {all_dirs}")
|
||||
except Exception as e:
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 无法读取目录 {selected_folder}: {str(e)}")
|
||||
return {}
|
||||
|
||||
for game, info in self.game_info.items():
|
||||
expected_dir = info["install_path"].split("/")[0] # 例如 "NEKOPARA Vol. 1"
|
||||
expected_exe = info["exe"] # 标准可执行文件名
|
||||
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 搜索游戏 {game}, 预期目录: {expected_dir}, 预期可执行文件: {expected_exe}")
|
||||
|
||||
# 尝试不同的匹配方法
|
||||
found_dir = None
|
||||
|
||||
# 1. 精确匹配
|
||||
if expected_dir in all_dirs:
|
||||
found_dir = expected_dir
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 精确匹配成功: {expected_dir}")
|
||||
|
||||
# 2. 大小写不敏感匹配
|
||||
if not found_dir:
|
||||
for dir_name in all_dirs:
|
||||
if expected_dir.lower() == dir_name.lower():
|
||||
found_dir = dir_name
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 大小写不敏感匹配成功: {dir_name}")
|
||||
break
|
||||
|
||||
# 3. 更模糊的匹配(允许特殊字符差异)
|
||||
if not found_dir:
|
||||
# 准备用于模糊匹配的正则表达式模式
|
||||
# 替换空格为可选空格或连字符,替换点为可选点
|
||||
pattern_text = expected_dir.replace(" ", "[ -]?").replace(".", "\\.?")
|
||||
pattern = re.compile(f"^{pattern_text}$", re.IGNORECASE)
|
||||
|
||||
for dir_name in all_dirs:
|
||||
if pattern.match(dir_name):
|
||||
found_dir = dir_name
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 模糊匹配成功: {dir_name} 匹配模式 {pattern_text}")
|
||||
break
|
||||
|
||||
# 4. 如果还是没找到,尝试更宽松的匹配
|
||||
if not found_dir:
|
||||
vol_match = re.search(r"vol(?:\.|\s*)?(\d+)", expected_dir, re.IGNORECASE)
|
||||
vol_num = None
|
||||
if vol_match:
|
||||
vol_num = vol_match.group(1)
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 提取卷号: {vol_num}")
|
||||
|
||||
is_after = "after" in expected_dir.lower()
|
||||
|
||||
for dir_name in all_dirs:
|
||||
dir_lower = dir_name.lower()
|
||||
|
||||
# 对于After特殊处理
|
||||
if is_after and "after" in dir_lower:
|
||||
found_dir = dir_name
|
||||
if debug_mode:
|
||||
print(f"DEBUG: After特殊匹配成功: {dir_name}")
|
||||
break
|
||||
|
||||
# 对于Vol特殊处理
|
||||
if vol_num:
|
||||
# 查找目录名中的卷号
|
||||
dir_vol_match = re.search(r"vol(?:\.|\s*)?(\d+)", dir_lower)
|
||||
if dir_vol_match and dir_vol_match.group(1) == vol_num:
|
||||
found_dir = dir_name
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 卷号匹配成功: {dir_name} 卷号 {vol_num}")
|
||||
break
|
||||
|
||||
# 如果找到匹配的目录,验证exe文件是否存在
|
||||
if found_dir:
|
||||
potential_path = os.path.join(selected_folder, found_dir)
|
||||
|
||||
# 尝试多种可能的可执行文件名变体
|
||||
# 包括Steam加密版本和其他可能的变体
|
||||
exe_variants = [
|
||||
expected_exe, # 标准文件名
|
||||
expected_exe + ".nocrack", # Steam加密版本
|
||||
expected_exe.replace(".exe", ""),# 无扩展名版本
|
||||
# Vol.3的特殊变体,因为它的文件名可能不一样
|
||||
expected_exe.replace("NEKOPARA", "nekopara").lower(), # 全小写变体
|
||||
expected_exe.lower(), # 小写变体
|
||||
expected_exe.lower() + ".nocrack", # 小写变体的Steam加密版本
|
||||
]
|
||||
|
||||
# 对于Vol.3可能有特殊名称
|
||||
if "Vol.3" in game:
|
||||
# 增加可能的卷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"
|
||||
])
|
||||
|
||||
exe_exists = False
|
||||
found_exe = None
|
||||
|
||||
# 尝试所有可能的变体
|
||||
for exe_variant in exe_variants:
|
||||
exe_path = os.path.join(potential_path, exe_variant)
|
||||
if os.path.exists(exe_path):
|
||||
exe_exists = True
|
||||
found_exe = exe_variant
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 验证成功,找到游戏可执行文件: {exe_variant}")
|
||||
break
|
||||
|
||||
# 如果没有直接找到,尝试递归搜索当前目录下的所有可执行文件
|
||||
if not exe_exists:
|
||||
# 遍历当前目录下的所有文件和文件夹
|
||||
for root, dirs, files in os.walk(potential_path):
|
||||
for file in files:
|
||||
file_lower = file.lower()
|
||||
# 检查是否是游戏可执行文件(根据关键字)
|
||||
if file.endswith('.exe') or file.endswith('.exe.nocrack'):
|
||||
# 检查文件名中是否包含卷号或关键词
|
||||
if "Vol." in game:
|
||||
vol_match = re.search(r"Vol\.(\d+)", game)
|
||||
if vol_match:
|
||||
vol_num = vol_match.group(1)
|
||||
if (f"vol{vol_num}" in file_lower or
|
||||
f"vol.{vol_num}" in file_lower or
|
||||
f"vol {vol_num}" in file_lower):
|
||||
exe_path = os.path.join(root, file)
|
||||
exe_exists = True
|
||||
found_exe = os.path.relpath(exe_path, potential_path)
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 通过递归搜索找到游戏可执行文件: {found_exe}")
|
||||
break
|
||||
elif "After" in game and "after" in file_lower:
|
||||
exe_path = os.path.join(root, file)
|
||||
exe_exists = True
|
||||
found_exe = os.path.relpath(exe_path, potential_path)
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 通过递归搜索找到After游戏可执行文件: {found_exe}")
|
||||
break
|
||||
if exe_exists:
|
||||
break
|
||||
|
||||
# 如果找到了可执行文件,将该目录添加到游戏目录列表
|
||||
if exe_exists:
|
||||
game_paths[game] = potential_path
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 验证成功,将 {potential_path} 添加为 {game} 的目录")
|
||||
else:
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 未找到任何可执行文件变体,游戏 {game} 在 {potential_path} 未找到")
|
||||
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 最终识别的游戏目录: {game_paths}")
|
||||
print(f"--- 目录识别结束 ---")
|
||||
|
||||
return game_paths
|
||||
345
source/core/ipv6_manager.py
Normal file
@@ -0,0 +1,345 @@
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import subprocess
|
||||
import urllib.request
|
||||
import ssl
|
||||
import threading
|
||||
from PySide6.QtCore import QObject, Signal
|
||||
from PySide6.QtWidgets import QDialog, QVBoxLayout, QLabel, QPushButton, QTextEdit, QProgressBar, QMessageBox
|
||||
|
||||
from data.config import APP_NAME
|
||||
from utils import msgbox_frame
|
||||
|
||||
|
||||
class IPv6Manager:
|
||||
"""管理IPv6相关功能的类"""
|
||||
|
||||
def __init__(self, main_window):
|
||||
"""初始化IPv6管理器
|
||||
|
||||
Args:
|
||||
main_window: 主窗口实例,用于显示对话框和访问配置
|
||||
"""
|
||||
self.main_window = main_window
|
||||
self.config = getattr(main_window, 'config', {})
|
||||
|
||||
def check_ipv6_availability(self):
|
||||
"""检查IPv6是否可用
|
||||
|
||||
通过访问IPv6专用图片URL测试IPv6连接
|
||||
|
||||
Returns:
|
||||
bool: IPv6是否可用
|
||||
"""
|
||||
import urllib.request
|
||||
import time
|
||||
|
||||
print("开始检测IPv6可用性...")
|
||||
|
||||
try:
|
||||
# 获取IPv6测试请求
|
||||
ipv6_test_url, req, context = self._get_ipv6_test_request()
|
||||
|
||||
# 设置3秒超时,避免长时间等待
|
||||
start_time = time.time()
|
||||
with urllib.request.urlopen(req, timeout=3, context=context) as response:
|
||||
# 读取图片数据
|
||||
image_data = response.read()
|
||||
|
||||
# 检查是否成功
|
||||
if response.status == 200 and len(image_data) > 0:
|
||||
elapsed = time.time() - start_time
|
||||
print(f"IPv6测试成功! 用时: {elapsed:.2f}秒")
|
||||
return True
|
||||
else:
|
||||
print(f"IPv6测试失败: 状态码 {response.status}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"IPv6测试失败: {e}")
|
||||
return False
|
||||
|
||||
def _get_ipv6_test_request(self):
|
||||
"""获取IPv6测试请求
|
||||
|
||||
Returns:
|
||||
tuple: (测试URL, 请求对象, SSL上下文)
|
||||
"""
|
||||
import urllib.request
|
||||
import ssl
|
||||
|
||||
# IPv6测试URL - 这是一个只能通过IPv6访问的资源
|
||||
ipv6_test_url = "https://ipv6.testipv6.cn/images-nc/knob_green.png?&testdomain=www.test-ipv6.com&testname=sites"
|
||||
|
||||
# 创建SSL上下文
|
||||
context = ssl._create_unverified_context()
|
||||
|
||||
# 创建请求并添加常见的HTTP头
|
||||
req = urllib.request.Request(ipv6_test_url)
|
||||
req.add_header('User-Agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)')
|
||||
req.add_header('Accept', 'image/webp,image/apng,image/*,*/*;q=0.8')
|
||||
|
||||
return ipv6_test_url, req, context
|
||||
|
||||
def get_ipv6_address(self):
|
||||
"""获取公网IPv6地址
|
||||
|
||||
Returns:
|
||||
str: IPv6地址,如果失败则返回None
|
||||
"""
|
||||
try:
|
||||
# 使用curl命令获取IPv6地址
|
||||
process = subprocess.Popen(
|
||||
["curl", "-6", "6.ipw.cn"],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
encoding='utf-8',
|
||||
errors='replace',
|
||||
creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0
|
||||
)
|
||||
|
||||
# 设置超时
|
||||
timeout = 5 # 5秒超时
|
||||
start_time = time.time()
|
||||
while process.poll() is None and (time.time() - start_time) < timeout:
|
||||
time.sleep(0.1)
|
||||
|
||||
# 如果进程仍在运行,则强制终止
|
||||
if process.poll() is None:
|
||||
process.terminate()
|
||||
print("获取IPv6地址超时")
|
||||
return None
|
||||
|
||||
stdout, stderr = process.communicate()
|
||||
|
||||
if process.returncode == 0 and stdout.strip():
|
||||
ipv6_address = stdout.strip()
|
||||
print(f"获取到IPv6地址: {ipv6_address}")
|
||||
return ipv6_address
|
||||
else:
|
||||
print("未能获取到IPv6地址")
|
||||
if stderr:
|
||||
print(f"错误信息: {stderr}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
print(f"获取IPv6地址失败: {e}")
|
||||
return None
|
||||
|
||||
def show_ipv6_details(self):
|
||||
"""显示IPv6连接详情"""
|
||||
class SignalEmitter(QObject):
|
||||
update_signal = Signal(str)
|
||||
complete_signal = Signal(bool, float)
|
||||
|
||||
# 创建对话框
|
||||
dialog = QDialog(self.main_window)
|
||||
dialog.setWindowTitle(f"IPv6连接测试 - {APP_NAME}")
|
||||
dialog.resize(500, 300)
|
||||
|
||||
# 创建布局
|
||||
layout = QVBoxLayout(dialog)
|
||||
|
||||
# 创建状态标签
|
||||
status_label = QLabel("正在测试IPv6连接...", dialog)
|
||||
layout.addWidget(status_label)
|
||||
|
||||
# 创建进度条
|
||||
progress = QProgressBar(dialog)
|
||||
progress.setRange(0, 0) # 不确定进度
|
||||
layout.addWidget(progress)
|
||||
|
||||
# 创建结果文本框
|
||||
result_text = QTextEdit(dialog)
|
||||
result_text.setReadOnly(True)
|
||||
layout.addWidget(result_text)
|
||||
|
||||
# 创建关闭按钮
|
||||
close_button = QPushButton("关闭", dialog)
|
||||
close_button.clicked.connect(dialog.accept)
|
||||
close_button.setEnabled(False) # 测试完成前禁用
|
||||
layout.addWidget(close_button)
|
||||
|
||||
# 信号发射器
|
||||
signal_emitter = SignalEmitter()
|
||||
|
||||
# 连接信号
|
||||
signal_emitter.update_signal.connect(
|
||||
lambda text: result_text.append(text)
|
||||
)
|
||||
|
||||
def on_test_complete(success, elapsed_time):
|
||||
# 停止进度条动画
|
||||
progress.setRange(0, 100)
|
||||
progress.setValue(100 if success else 0)
|
||||
|
||||
# 更新状态
|
||||
if success:
|
||||
status_label.setText(f"IPv6连接测试完成: 可用 (用时: {elapsed_time:.2f}秒)")
|
||||
else:
|
||||
status_label.setText("IPv6连接测试完成: 不可用")
|
||||
|
||||
# 启用关闭按钮
|
||||
close_button.setEnabled(True)
|
||||
|
||||
signal_emitter.complete_signal.connect(on_test_complete)
|
||||
|
||||
# 测试函数
|
||||
def test_ipv6():
|
||||
try:
|
||||
signal_emitter.update_signal.emit("正在测试IPv6连接,请稍候...")
|
||||
|
||||
# 先进行标准的IPv6连接测试
|
||||
signal_emitter.update_signal.emit("正在进行标准IPv6连接测试...")
|
||||
|
||||
# 使用IPv6测试URL
|
||||
ipv6_test_url, req, context = self._get_ipv6_test_request()
|
||||
ipv6_connected = False
|
||||
ipv6_test_elapsed_time = 0
|
||||
|
||||
try:
|
||||
# 设置5秒超时
|
||||
start_time = time.time()
|
||||
signal_emitter.update_signal.emit(f"开始连接: {ipv6_test_url}")
|
||||
|
||||
# 尝试下载图片
|
||||
with urllib.request.urlopen(req, timeout=5, context=context) as response:
|
||||
image_data = response.read()
|
||||
|
||||
# 计算耗时
|
||||
elapsed_time = time.time() - start_time
|
||||
ipv6_test_elapsed_time = elapsed_time
|
||||
|
||||
# 检查是否成功
|
||||
if response.status == 200 and len(image_data) > 0:
|
||||
ipv6_connected = True
|
||||
signal_emitter.update_signal.emit(f"✓ 成功! 已下载 {len(image_data)} 字节")
|
||||
signal_emitter.update_signal.emit(f"✓ 响应时间: {elapsed_time:.2f}秒")
|
||||
else:
|
||||
signal_emitter.update_signal.emit(f"✗ 失败: 状态码 {response.status}")
|
||||
signal_emitter.update_signal.emit("\n结论: 您的网络不支持IPv6连接 ✗")
|
||||
signal_emitter.complete_signal.emit(False, 0)
|
||||
return
|
||||
|
||||
except Exception as e:
|
||||
signal_emitter.update_signal.emit(f"✗ 连接失败: {e}")
|
||||
signal_emitter.update_signal.emit("\n结论: 您的网络不支持IPv6连接 ✗")
|
||||
signal_emitter.complete_signal.emit(False, 0)
|
||||
return
|
||||
|
||||
# 如果IPv6连接测试成功,再尝试获取公网IPv6地址
|
||||
if ipv6_connected:
|
||||
signal_emitter.update_signal.emit("\n正在获取您的公网IPv6地址...")
|
||||
|
||||
try:
|
||||
# 使用curl命令获取IPv6地址
|
||||
process = subprocess.Popen(
|
||||
["curl", "-6", "6.ipw.cn"],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
encoding='utf-8',
|
||||
errors='replace',
|
||||
creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0
|
||||
)
|
||||
|
||||
# 设置超时
|
||||
timeout = 5 # 5秒超时
|
||||
start_time = time.time()
|
||||
while process.poll() is None and (time.time() - start_time) < timeout:
|
||||
time.sleep(0.1)
|
||||
|
||||
# 如果进程仍在运行,则强制终止
|
||||
if process.poll() is None:
|
||||
process.terminate()
|
||||
signal_emitter.update_signal.emit("✗ 获取IPv6地址超时")
|
||||
else:
|
||||
stdout, stderr = process.communicate()
|
||||
|
||||
if process.returncode == 0 and stdout.strip():
|
||||
ipv6_address = stdout.strip()
|
||||
signal_emitter.update_signal.emit(f"✓ 获取到的IPv6地址: {ipv6_address}")
|
||||
else:
|
||||
signal_emitter.update_signal.emit("✗ 未能获取到IPv6地址")
|
||||
if stderr:
|
||||
signal_emitter.update_signal.emit(f"错误信息: {stderr}")
|
||||
|
||||
except Exception as e:
|
||||
signal_emitter.update_signal.emit(f"✗ 获取IPv6地址失败: {e}")
|
||||
|
||||
# 输出最终结论
|
||||
signal_emitter.update_signal.emit("\n结论: 您的网络支持IPv6连接 ✓")
|
||||
signal_emitter.complete_signal.emit(True, ipv6_test_elapsed_time)
|
||||
return
|
||||
|
||||
except Exception as e:
|
||||
signal_emitter.update_signal.emit(f"测试过程中出错: {e}")
|
||||
signal_emitter.complete_signal.emit(False, 0)
|
||||
|
||||
# 启动测试线程
|
||||
threading.Thread(target=test_ipv6, daemon=True).start()
|
||||
|
||||
# 显示对话框
|
||||
dialog.exec()
|
||||
|
||||
def toggle_ipv6_support(self, enabled):
|
||||
"""切换IPv6支持
|
||||
|
||||
Args:
|
||||
enabled: 是否启用IPv6支持
|
||||
"""
|
||||
print(f"Toggle IPv6 support: {enabled}")
|
||||
|
||||
# 如果用户尝试启用IPv6,检查系统是否支持IPv6并发出警告
|
||||
if enabled:
|
||||
# 先显示警告提示
|
||||
warning_msg_box = self._create_message_box(
|
||||
"警告",
|
||||
"\n目前IPv6支持功能仍在测试阶段,可能会发生意料之外的bug!\n\n您确定需要启用吗?\n",
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
|
||||
)
|
||||
response = warning_msg_box.exec()
|
||||
|
||||
# 如果用户选择不启用,直接返回
|
||||
if response != QMessageBox.StandardButton.Yes:
|
||||
return False
|
||||
|
||||
# 用户确认启用后,继续检查IPv6可用性
|
||||
ipv6_available = self.check_ipv6_availability()
|
||||
|
||||
if not ipv6_available:
|
||||
msg_box = self._create_message_box("错误", "\n未检测到可用的IPv6连接,无法启用IPv6支持。\n\n请确保您的网络环境支持IPv6且已正确配置。\n")
|
||||
msg_box.exec()
|
||||
return False
|
||||
|
||||
# 保存设置到配置
|
||||
if self.config is not None:
|
||||
self.config["ipv6_enabled"] = enabled
|
||||
# 直接使用utils.save_config保存配置
|
||||
from utils import save_config
|
||||
save_config(self.config)
|
||||
|
||||
# 显示设置已保存的消息
|
||||
status = "启用" if enabled else "禁用"
|
||||
msg_box = self._create_message_box("IPv6设置", f"\nIPv6支持已{status}。新的设置将在下一次下载时生效。\n")
|
||||
msg_box.exec()
|
||||
return True
|
||||
|
||||
def _create_message_box(self, title, message, buttons=QMessageBox.StandardButton.Ok):
|
||||
"""创建统一风格的消息框
|
||||
|
||||
Args:
|
||||
title: 消息框标题
|
||||
message: 消息内容
|
||||
buttons: 按钮类型,默认为确定按钮
|
||||
|
||||
Returns:
|
||||
QMessageBox: 配置好的消息框实例
|
||||
"""
|
||||
msg_box = msgbox_frame(
|
||||
f"{title} - {APP_NAME}",
|
||||
message,
|
||||
buttons,
|
||||
)
|
||||
return msg_box
|
||||
391
source/core/patch_manager.py
Normal file
@@ -0,0 +1,391 @@
|
||||
import os
|
||||
import shutil
|
||||
from PySide6.QtWidgets import QMessageBox
|
||||
|
||||
class PatchManager:
|
||||
"""补丁管理器,用于处理补丁的安装和卸载"""
|
||||
|
||||
def __init__(self, app_name, game_info, debug_manager=None):
|
||||
"""初始化补丁管理器
|
||||
|
||||
Args:
|
||||
app_name: 应用程序名称,用于显示消息框标题
|
||||
game_info: 游戏信息字典,包含各版本的安装路径和可执行文件名
|
||||
debug_manager: 调试管理器实例,用于输出调试信息
|
||||
"""
|
||||
self.app_name = app_name
|
||||
self.game_info = game_info
|
||||
self.debug_manager = debug_manager
|
||||
self.installed_status = {} # 游戏版本的安装状态
|
||||
|
||||
def _is_debug_mode(self):
|
||||
"""检查是否处于调试模式
|
||||
|
||||
Returns:
|
||||
bool: 是否处于调试模式
|
||||
"""
|
||||
if hasattr(self.debug_manager, 'ui_manager') and hasattr(self.debug_manager.ui_manager, 'debug_action'):
|
||||
return self.debug_manager.ui_manager.debug_action.isChecked()
|
||||
return False
|
||||
|
||||
def initialize_status(self):
|
||||
"""初始化所有游戏版本的安装状态"""
|
||||
self.installed_status = {f"NEKOPARA Vol.{i}": False for i in range(1, 5)}
|
||||
self.installed_status["NEKOPARA After"] = False
|
||||
|
||||
def update_status(self, game_version, is_installed):
|
||||
"""更新游戏版本的安装状态
|
||||
|
||||
Args:
|
||||
game_version: 游戏版本
|
||||
is_installed: 是否已安装
|
||||
"""
|
||||
self.installed_status[game_version] = is_installed
|
||||
|
||||
def get_status(self, game_version=None):
|
||||
"""获取游戏版本的安装状态
|
||||
|
||||
Args:
|
||||
game_version: 游戏版本,如果为None则返回所有状态
|
||||
|
||||
Returns:
|
||||
bool或dict: 指定版本的安装状态或所有版本的安装状态
|
||||
"""
|
||||
if game_version:
|
||||
return self.installed_status.get(game_version, False)
|
||||
return self.installed_status
|
||||
|
||||
def uninstall_patch(self, game_dir, game_version, silent=False):
|
||||
"""卸载补丁
|
||||
|
||||
Args:
|
||||
game_dir: 游戏目录路径
|
||||
game_version: 游戏版本
|
||||
silent: 是否静默模式(不显示弹窗)
|
||||
|
||||
Returns:
|
||||
bool: 卸载成功返回True,失败返回False
|
||||
dict: 在silent=True时,返回包含卸载结果信息的字典
|
||||
"""
|
||||
debug_mode = self._is_debug_mode()
|
||||
|
||||
if game_version not in self.game_info:
|
||||
if not silent:
|
||||
QMessageBox.critical(
|
||||
None,
|
||||
f"错误 - {self.app_name}",
|
||||
f"\n无法识别游戏版本: {game_version}\n",
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
return False if not silent else {"success": False, "message": f"无法识别游戏版本: {game_version}", "files_removed": 0}
|
||||
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 开始卸载 {game_version} 补丁,目录: {game_dir}")
|
||||
|
||||
try:
|
||||
files_removed = 0
|
||||
|
||||
# 获取可能的补丁文件路径
|
||||
install_path_base = os.path.basename(self.game_info[game_version]["install_path"])
|
||||
patch_file_path = os.path.join(game_dir, install_path_base)
|
||||
|
||||
# 尝试查找补丁文件,支持不同大小写
|
||||
patch_files_to_check = [
|
||||
patch_file_path,
|
||||
patch_file_path.lower(),
|
||||
patch_file_path.upper(),
|
||||
patch_file_path.replace("_", ""),
|
||||
patch_file_path.replace("_", "-"),
|
||||
]
|
||||
|
||||
# 查找并删除补丁文件
|
||||
patch_file_found = False
|
||||
for patch_path in patch_files_to_check:
|
||||
if os.path.exists(patch_path):
|
||||
patch_file_found = True
|
||||
os.remove(patch_path)
|
||||
files_removed += 1
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 已删除补丁文件: {patch_path}")
|
||||
|
||||
if not patch_file_found and debug_mode:
|
||||
print(f"DEBUG: 未找到补丁文件,检查了以下路径: {patch_files_to_check}")
|
||||
|
||||
# 检查是否有额外的签名文件 (.sig)
|
||||
if game_version == "NEKOPARA After":
|
||||
for patch_path in patch_files_to_check:
|
||||
sig_file_path = f"{patch_path}.sig"
|
||||
if os.path.exists(sig_file_path):
|
||||
os.remove(sig_file_path)
|
||||
files_removed += 1
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 已删除签名文件: {sig_file_path}")
|
||||
|
||||
# 删除patch文件夹
|
||||
patch_folders_to_check = [
|
||||
os.path.join(game_dir, "patch"),
|
||||
os.path.join(game_dir, "Patch"),
|
||||
os.path.join(game_dir, "PATCH"),
|
||||
]
|
||||
|
||||
for patch_folder in patch_folders_to_check:
|
||||
if os.path.exists(patch_folder):
|
||||
shutil.rmtree(patch_folder)
|
||||
files_removed += 1
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 已删除补丁文件夹: {patch_folder}")
|
||||
|
||||
# 删除game/patch文件夹
|
||||
game_folders = ["game", "Game", "GAME"]
|
||||
patch_folders = ["patch", "Patch", "PATCH"]
|
||||
|
||||
for game_folder in game_folders:
|
||||
for patch_folder in patch_folders:
|
||||
game_patch_folder = os.path.join(game_dir, game_folder, patch_folder)
|
||||
if os.path.exists(game_patch_folder):
|
||||
shutil.rmtree(game_patch_folder)
|
||||
files_removed += 1
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 已删除game/patch文件夹: {game_patch_folder}")
|
||||
|
||||
# 删除配置文件
|
||||
config_files = ["config.json", "Config.json", "CONFIG.JSON"]
|
||||
script_files = ["scripts.json", "Scripts.json", "SCRIPTS.JSON"]
|
||||
|
||||
for game_folder in game_folders:
|
||||
game_path = os.path.join(game_dir, game_folder)
|
||||
if os.path.exists(game_path):
|
||||
# 删除配置文件
|
||||
for config_file in config_files:
|
||||
config_path = os.path.join(game_path, config_file)
|
||||
if os.path.exists(config_path):
|
||||
os.remove(config_path)
|
||||
files_removed += 1
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 已删除配置文件: {config_path}")
|
||||
|
||||
# 删除脚本文件
|
||||
for script_file in script_files:
|
||||
script_path = os.path.join(game_path, script_file)
|
||||
if os.path.exists(script_path):
|
||||
os.remove(script_path)
|
||||
files_removed += 1
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 已删除脚本文件: {script_path}")
|
||||
|
||||
# 更新安装状态
|
||||
self.installed_status[game_version] = False
|
||||
|
||||
# 在非静默模式且非批量卸载模式下显示卸载成功消息
|
||||
if not silent and game_version != "all":
|
||||
# 显示卸载成功消息
|
||||
if files_removed > 0:
|
||||
QMessageBox.information(
|
||||
None,
|
||||
f"卸载完成 - {self.app_name}",
|
||||
f"\n{game_version} 补丁卸载成功!\n共删除 {files_removed} 个文件/文件夹。\n",
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
else:
|
||||
QMessageBox.warning(
|
||||
None,
|
||||
f"警告 - {self.app_name}",
|
||||
f"\n未找到 {game_version} 的补丁文件,可能未安装补丁或已被移除。\n",
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
|
||||
# 卸载成功
|
||||
if silent:
|
||||
return {"success": True, "message": f"{game_version} 补丁卸载成功", "files_removed": files_removed}
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
# 在非静默模式且非批量卸载模式下显示卸载失败消息
|
||||
if not silent and game_version != "all":
|
||||
# 显示卸载失败消息
|
||||
error_message = f"\n卸载 {game_version} 补丁时出错:\n\n{str(e)}\n"
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 卸载错误 - {str(e)}")
|
||||
|
||||
QMessageBox.critical(
|
||||
None,
|
||||
f"卸载失败 - {self.app_name}",
|
||||
error_message,
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
|
||||
# 卸载失败
|
||||
if silent:
|
||||
return {"success": False, "message": f"卸载 {game_version} 补丁时出错: {str(e)}", "files_removed": 0}
|
||||
return False
|
||||
|
||||
def batch_uninstall_patches(self, game_dirs):
|
||||
"""批量卸载多个游戏的补丁
|
||||
|
||||
Args:
|
||||
game_dirs: 游戏版本到游戏目录的映射字典
|
||||
|
||||
Returns:
|
||||
tuple: (成功数量, 失败数量, 详细结果列表)
|
||||
"""
|
||||
success_count = 0
|
||||
fail_count = 0
|
||||
debug_mode = self._is_debug_mode()
|
||||
results = []
|
||||
|
||||
for version, path in game_dirs.items():
|
||||
try:
|
||||
# 在批量模式下使用静默卸载
|
||||
result = self.uninstall_patch(path, version, silent=True)
|
||||
|
||||
if isinstance(result, dict): # 使用了静默模式
|
||||
if result["success"]:
|
||||
success_count += 1
|
||||
else:
|
||||
fail_count += 1
|
||||
results.append({
|
||||
"version": version,
|
||||
"success": result["success"],
|
||||
"message": result["message"],
|
||||
"files_removed": result["files_removed"]
|
||||
})
|
||||
else: # 兼容旧代码,不应该执行到这里
|
||||
if result:
|
||||
success_count += 1
|
||||
else:
|
||||
fail_count += 1
|
||||
results.append({
|
||||
"version": version,
|
||||
"success": result,
|
||||
"message": f"{version} 卸载{'成功' if result else '失败'}",
|
||||
"files_removed": 0
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 卸载 {version} 时出错: {str(e)}")
|
||||
fail_count += 1
|
||||
results.append({
|
||||
"version": version,
|
||||
"success": False,
|
||||
"message": f"卸载出错: {str(e)}",
|
||||
"files_removed": 0
|
||||
})
|
||||
|
||||
return success_count, fail_count, results
|
||||
|
||||
def show_uninstall_result(self, success_count, fail_count, results=None):
|
||||
"""显示批量卸载结果
|
||||
|
||||
Args:
|
||||
success_count: 成功卸载的数量
|
||||
fail_count: 卸载失败的数量
|
||||
results: 详细结果列表,如果提供,会显示更详细的信息
|
||||
"""
|
||||
result_text = f"\n批量卸载完成!\n成功: {success_count} 个\n失败: {fail_count} 个\n"
|
||||
|
||||
# 如果有详细结果,添加到消息中
|
||||
if results:
|
||||
success_list = [r["version"] for r in results if r["success"]]
|
||||
fail_list = [r["version"] for r in results if not r["success"]]
|
||||
|
||||
if success_list:
|
||||
result_text += f"\n【成功卸载】:\n{chr(10).join(success_list)}\n"
|
||||
|
||||
if fail_list:
|
||||
result_text += f"\n【卸载失败】:\n{chr(10).join(fail_list)}\n"
|
||||
|
||||
QMessageBox.information(
|
||||
None,
|
||||
f"批量卸载完成 - {self.app_name}",
|
||||
result_text,
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
|
||||
def check_patch_installed(self, game_dir, game_version):
|
||||
"""检查游戏是否已安装补丁
|
||||
|
||||
Args:
|
||||
game_dir: 游戏目录路径
|
||||
game_version: 游戏版本
|
||||
|
||||
Returns:
|
||||
bool: 如果已安装补丁返回True,否则返回False
|
||||
"""
|
||||
debug_mode = self._is_debug_mode()
|
||||
|
||||
if game_version not in self.game_info:
|
||||
return False
|
||||
|
||||
# 获取可能的补丁文件路径
|
||||
install_path_base = os.path.basename(self.game_info[game_version]["install_path"])
|
||||
patch_file_path = os.path.join(game_dir, install_path_base)
|
||||
|
||||
# 尝试查找补丁文件,支持不同大小写
|
||||
patch_files_to_check = [
|
||||
patch_file_path,
|
||||
patch_file_path.lower(),
|
||||
patch_file_path.upper(),
|
||||
patch_file_path.replace("_", ""),
|
||||
patch_file_path.replace("_", "-"),
|
||||
]
|
||||
|
||||
# 查找补丁文件
|
||||
for patch_path in patch_files_to_check:
|
||||
if os.path.exists(patch_path):
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 找到补丁文件: {patch_path}")
|
||||
return True
|
||||
|
||||
# 检查是否有补丁文件夹
|
||||
patch_folders_to_check = [
|
||||
os.path.join(game_dir, "patch"),
|
||||
os.path.join(game_dir, "Patch"),
|
||||
os.path.join(game_dir, "PATCH"),
|
||||
]
|
||||
|
||||
for patch_folder in patch_folders_to_check:
|
||||
if os.path.exists(patch_folder):
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 找到补丁文件夹: {patch_folder}")
|
||||
return True
|
||||
|
||||
# 检查game/patch文件夹
|
||||
game_folders = ["game", "Game", "GAME"]
|
||||
patch_folders = ["patch", "Patch", "PATCH"]
|
||||
|
||||
for game_folder in game_folders:
|
||||
for patch_folder in patch_folders:
|
||||
game_patch_folder = os.path.join(game_dir, game_folder, patch_folder)
|
||||
if os.path.exists(game_patch_folder):
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 找到game/patch文件夹: {game_patch_folder}")
|
||||
return True
|
||||
|
||||
# 检查配置文件
|
||||
config_files = ["config.json", "Config.json", "CONFIG.JSON"]
|
||||
script_files = ["scripts.json", "Scripts.json", "SCRIPTS.JSON"]
|
||||
|
||||
for game_folder in game_folders:
|
||||
game_path = os.path.join(game_dir, game_folder)
|
||||
if os.path.exists(game_path):
|
||||
# 检查配置文件
|
||||
for config_file in config_files:
|
||||
config_path = os.path.join(game_path, config_file)
|
||||
if os.path.exists(config_path):
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 找到配置文件: {config_path}")
|
||||
return True
|
||||
|
||||
# 检查脚本文件
|
||||
for script_file in script_files:
|
||||
script_path = os.path.join(game_path, script_file)
|
||||
if os.path.exists(script_path):
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 找到脚本文件: {script_path}")
|
||||
return True
|
||||
|
||||
# 没有找到补丁文件或文件夹
|
||||
if debug_mode:
|
||||
print(f"DEBUG: {game_version} 在 {game_dir} 中没有安装补丁")
|
||||
return False
|
||||
226
source/core/privacy_manager.py
Normal file
@@ -0,0 +1,226 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
import json
|
||||
from PySide6.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QTextBrowser, QPushButton, QCheckBox, QLabel, QMessageBox
|
||||
from PySide6.QtCore import Qt
|
||||
|
||||
from data.privacy_policy import PRIVACY_POLICY_BRIEF, get_local_privacy_policy, PRIVACY_POLICY_VERSION
|
||||
from data.config import CACHE, APP_NAME, APP_VERSION
|
||||
from utils import msgbox_frame
|
||||
from utils.logger import setup_logger
|
||||
|
||||
class PrivacyManager:
|
||||
"""隐私协议管理器,负责显示隐私协议对话框并处理用户选择"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化隐私协议管理器"""
|
||||
# 初始化日志
|
||||
self.logger = setup_logger("privacy_manager")
|
||||
self.logger.info("正在初始化隐私协议管理器")
|
||||
# 确保缓存目录存在
|
||||
os.makedirs(CACHE, exist_ok=True)
|
||||
self.config_file = os.path.join(CACHE, "privacy_config.json")
|
||||
self.privacy_config = self._load_privacy_config()
|
||||
|
||||
# 获取隐私协议内容和版本
|
||||
self.logger.info("读取本地隐私协议文件")
|
||||
self.privacy_content, self.current_privacy_version, error = get_local_privacy_policy()
|
||||
if error:
|
||||
self.logger.warning(f"读取本地隐私协议文件警告: {error}")
|
||||
# 使用默认版本作为备用
|
||||
self.current_privacy_version = PRIVACY_POLICY_VERSION
|
||||
self.logger.info(f"隐私协议版本: {self.current_privacy_version}")
|
||||
|
||||
# 检查隐私协议版本和用户同意状态
|
||||
self.privacy_accepted = self._check_privacy_acceptance()
|
||||
|
||||
def _load_privacy_config(self):
|
||||
"""加载隐私协议配置
|
||||
|
||||
Returns:
|
||||
dict: 隐私协议配置信息
|
||||
"""
|
||||
if os.path.exists(self.config_file):
|
||||
try:
|
||||
with open(self.config_file, "r", encoding="utf-8") as f:
|
||||
config = json.load(f)
|
||||
return config
|
||||
except (json.JSONDecodeError, IOError) as e:
|
||||
self.logger.error(f"读取隐私配置失败: {e}")
|
||||
# 如果读取失败,返回空配置,强制显示隐私协议
|
||||
return {"privacy_accepted": False}
|
||||
return {"privacy_accepted": False}
|
||||
|
||||
def _check_privacy_acceptance(self):
|
||||
"""检查隐私协议是否需要重新同意
|
||||
|
||||
如果隐私协议版本变更,则需要重新同意
|
||||
|
||||
Returns:
|
||||
bool: 是否已有有效的隐私协议同意
|
||||
"""
|
||||
# 获取存储的版本信息
|
||||
stored_privacy_version = self.privacy_config.get("privacy_version", "0.0.0")
|
||||
stored_app_version = self.privacy_config.get("app_version", "0.0.0")
|
||||
privacy_accepted = self.privacy_config.get("privacy_accepted", False)
|
||||
|
||||
self.logger.info(f"存储的隐私协议版本: {stored_privacy_version}, 当前版本: {self.current_privacy_version}")
|
||||
self.logger.info(f"存储的应用版本: {stored_app_version}, 当前版本: {APP_VERSION}")
|
||||
self.logger.info(f"隐私协议接受状态: {privacy_accepted}")
|
||||
|
||||
# 如果隐私协议版本变更,需要重新同意
|
||||
if stored_privacy_version != self.current_privacy_version:
|
||||
self.logger.info("隐私协议版本已变更,需要重新同意")
|
||||
return False
|
||||
|
||||
# 返回当前的同意状态
|
||||
return privacy_accepted
|
||||
|
||||
def _save_privacy_config(self, accepted):
|
||||
"""保存隐私协议配置
|
||||
|
||||
Args:
|
||||
accepted: 用户是否同意隐私协议
|
||||
|
||||
Returns:
|
||||
bool: 配置是否保存成功
|
||||
"""
|
||||
try:
|
||||
# 确保目录存在
|
||||
os.makedirs(os.path.dirname(self.config_file), exist_ok=True)
|
||||
|
||||
# 写入配置文件,包含应用版本和隐私协议版本
|
||||
with open(self.config_file, "w", encoding="utf-8") as f:
|
||||
json.dump({
|
||||
"privacy_accepted": accepted,
|
||||
"privacy_version": self.current_privacy_version, # 保存当前隐私协议版本
|
||||
"app_version": APP_VERSION # 保存当前应用版本
|
||||
}, f, indent=2)
|
||||
|
||||
# 更新实例变量
|
||||
self.privacy_accepted = accepted
|
||||
self.privacy_config = {
|
||||
"privacy_accepted": accepted,
|
||||
"privacy_version": self.current_privacy_version,
|
||||
"app_version": APP_VERSION
|
||||
}
|
||||
return True
|
||||
except IOError as e:
|
||||
self.logger.error(f"保存隐私协议配置失败: {e}")
|
||||
# 显示保存失败的提示
|
||||
QMessageBox.warning(
|
||||
None,
|
||||
f"配置保存警告 - {APP_NAME}",
|
||||
f"隐私设置无法保存到配置文件,下次启动时可能需要重新确认。\n\n错误信息:{e}"
|
||||
)
|
||||
return False
|
||||
|
||||
def show_privacy_dialog(self):
|
||||
"""显示隐私协议对话框
|
||||
|
||||
Returns:
|
||||
bool: 用户是否同意隐私协议
|
||||
"""
|
||||
# 如果用户已经同意了隐私协议,直接返回True不显示对话框
|
||||
if self.privacy_accepted:
|
||||
self.logger.info("用户已同意当前版本的隐私协议,无需再次显示")
|
||||
return True
|
||||
|
||||
self.logger.info("首次运行或隐私协议版本变更,显示隐私对话框")
|
||||
|
||||
# 创建隐私协议对话框
|
||||
dialog = QDialog()
|
||||
dialog.setWindowTitle(f"隐私政策 - {APP_NAME}")
|
||||
dialog.setMinimumSize(600, 400)
|
||||
dialog.setWindowFlags(dialog.windowFlags() & ~Qt.WindowContextHelpButtonHint)
|
||||
|
||||
# 创建布局
|
||||
layout = QVBoxLayout(dialog)
|
||||
|
||||
# 添加标题和版本信息
|
||||
title_label = QLabel(f"请阅读并同意以下隐私政策 (更新日期: {self.current_privacy_version})")
|
||||
title_label.setStyleSheet("font-size: 14px; font-weight: bold;")
|
||||
layout.addWidget(title_label)
|
||||
|
||||
# 添加隐私协议文本框
|
||||
text_browser = QTextBrowser()
|
||||
# 这里使用PRIVACY_POLICY_BRIEF而不是self.privacy_content,保持UI简洁
|
||||
text_browser.setMarkdown(PRIVACY_POLICY_BRIEF)
|
||||
text_browser.setOpenExternalLinks(True)
|
||||
layout.addWidget(text_browser)
|
||||
|
||||
# 添加同意选择框
|
||||
checkbox = QCheckBox("我已阅读并同意上述隐私政策")
|
||||
layout.addWidget(checkbox)
|
||||
|
||||
# 添加按钮
|
||||
buttons_layout = QHBoxLayout()
|
||||
agree_button = QPushButton("同意并继续")
|
||||
agree_button.setEnabled(False) # 初始状态为禁用
|
||||
decline_button = QPushButton("不同意并退出")
|
||||
buttons_layout.addWidget(agree_button)
|
||||
buttons_layout.addWidget(decline_button)
|
||||
layout.addLayout(buttons_layout)
|
||||
|
||||
# 连接选择框状态变化 - 修复勾选后按钮不亮起的问题
|
||||
def on_checkbox_state_changed(state):
|
||||
self.logger.debug(f"复选框状态变更为: {state}")
|
||||
agree_button.setEnabled(state == 2) # Qt.Checked 在 PySide6 中值为 2
|
||||
|
||||
checkbox.stateChanged.connect(on_checkbox_state_changed)
|
||||
|
||||
# 连接按钮点击事件
|
||||
agree_button.clicked.connect(lambda: self._on_agree(dialog))
|
||||
decline_button.clicked.connect(lambda: self._on_decline(dialog))
|
||||
|
||||
# 显示对话框
|
||||
result = dialog.exec()
|
||||
|
||||
# 返回用户选择结果
|
||||
return self.privacy_accepted
|
||||
|
||||
def _on_agree(self, dialog):
|
||||
"""处理用户同意隐私协议
|
||||
|
||||
Args:
|
||||
dialog: 对话框实例
|
||||
"""
|
||||
# 保存配置并更新状态
|
||||
self._save_privacy_config(True)
|
||||
dialog.accept()
|
||||
|
||||
def _on_decline(self, dialog):
|
||||
"""处理用户拒绝隐私协议
|
||||
|
||||
Args:
|
||||
dialog: 对话框实例
|
||||
"""
|
||||
# 显示拒绝信息
|
||||
msg_box = msgbox_frame(
|
||||
f"退出 - {APP_NAME}",
|
||||
"\n您需要同意隐私政策才能使用本软件。\n软件将立即退出。\n",
|
||||
QMessageBox.Ok,
|
||||
)
|
||||
msg_box.exec()
|
||||
|
||||
# 保存拒绝状态
|
||||
self._save_privacy_config(False)
|
||||
dialog.reject()
|
||||
|
||||
def is_privacy_accepted(self):
|
||||
"""检查用户是否已同意隐私协议
|
||||
|
||||
Returns:
|
||||
bool: 用户是否已同意隐私协议
|
||||
"""
|
||||
return self.privacy_accepted
|
||||
|
||||
def reset_privacy_agreement(self):
|
||||
"""重置隐私协议同意状态,用于测试或重新显示隐私协议
|
||||
|
||||
Returns:
|
||||
bool: 重置是否成功
|
||||
"""
|
||||
return self._save_privacy_config(False)
|
||||
673
source/core/ui_manager.py
Normal file
@@ -0,0 +1,673 @@
|
||||
from PySide6.QtGui import QIcon, QAction, QFont, QCursor
|
||||
from PySide6.QtWidgets import QMessageBox, QMainWindow, QMenu, QPushButton
|
||||
from PySide6.QtCore import Qt, QRect
|
||||
import webbrowser
|
||||
import os
|
||||
|
||||
from utils import load_base64_image, msgbox_frame, resource_path
|
||||
from data.config import APP_NAME, APP_VERSION, LOG_FILE
|
||||
from core.ipv6_manager import IPv6Manager # 导入新的IPv6Manager类
|
||||
|
||||
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
|
||||
self.turbo_download_action = None
|
||||
self.dev_menu = None
|
||||
self.privacy_menu = None # 隐私协议菜单
|
||||
self.about_menu = None # 关于菜单
|
||||
self.about_btn = None # 关于按钮
|
||||
|
||||
# 获取主窗口的IPv6Manager实例
|
||||
self.ipv6_manager = getattr(main_window, 'ipv6_manager', 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._create_about_button()
|
||||
|
||||
# 设置菜单
|
||||
self._setup_help_menu()
|
||||
self._setup_about_menu() # 新增关于菜单
|
||||
self._setup_settings_menu()
|
||||
|
||||
def _create_about_button(self):
|
||||
"""创建"关于"按钮"""
|
||||
if not self.ui or not hasattr(self.ui, 'menu_area'):
|
||||
return
|
||||
|
||||
# 获取菜单字体和样式
|
||||
menu_font = self._get_menu_font()
|
||||
|
||||
# 创建关于按钮
|
||||
self.about_btn = QPushButton("关于", self.ui.menu_area)
|
||||
self.about_btn.setObjectName(u"about_btn")
|
||||
|
||||
# 获取帮助按钮的位置和样式
|
||||
help_btn_x = 0
|
||||
help_btn_width = 0
|
||||
if hasattr(self.ui, 'help_btn'):
|
||||
help_btn_x = self.ui.help_btn.x()
|
||||
help_btn_width = self.ui.help_btn.width()
|
||||
|
||||
# 设置位置在"帮助"按钮右侧
|
||||
self.about_btn.setGeometry(QRect(help_btn_x + help_btn_width + 20, 1, 80, 28))
|
||||
self.about_btn.setFont(menu_font)
|
||||
self.about_btn.setCursor(QCursor(Qt.CursorShape.PointingHandCursor))
|
||||
|
||||
# 复制帮助按钮的样式
|
||||
if hasattr(self.ui, 'help_btn'):
|
||||
self.about_btn.setStyleSheet(self.ui.help_btn.styleSheet())
|
||||
else:
|
||||
# 默认样式
|
||||
self.about_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;
|
||||
}
|
||||
""")
|
||||
|
||||
def _setup_help_menu(self):
|
||||
"""设置"帮助"菜单"""
|
||||
if not self.ui or not hasattr(self.ui, 'menu_2'):
|
||||
return
|
||||
|
||||
# 获取菜单字体
|
||||
menu_font = self._get_menu_font()
|
||||
|
||||
# 创建菜单项 - 移除"项目主页",添加"常见问题"和"提交错误"
|
||||
faq_action = QAction("常见问题", self.main_window)
|
||||
faq_action.triggered.connect(self.open_faq_page)
|
||||
faq_action.setFont(menu_font)
|
||||
|
||||
report_issue_action = QAction("提交错误", self.main_window)
|
||||
report_issue_action.triggered.connect(self.open_issues_page)
|
||||
report_issue_action.setFont(menu_font)
|
||||
|
||||
# 清除现有菜单项并添加新的菜单项
|
||||
self.ui.menu_2.clear()
|
||||
self.ui.menu_2.addAction(faq_action)
|
||||
self.ui.menu_2.addAction(report_issue_action)
|
||||
|
||||
# 连接按钮点击事件,如果使用按钮式菜单
|
||||
if hasattr(self.ui, 'help_btn'):
|
||||
# 按钮已经连接到显示菜单,不需要额外处理
|
||||
pass
|
||||
|
||||
def _setup_about_menu(self):
|
||||
"""设置"关于"菜单"""
|
||||
# 获取菜单字体
|
||||
menu_font = self._get_menu_font()
|
||||
|
||||
# 创建关于菜单
|
||||
self.about_menu = QMenu("关于", self.main_window)
|
||||
self.about_menu.setFont(menu_font)
|
||||
|
||||
# 设置菜单样式
|
||||
font_family = menu_font.family()
|
||||
menu_style = self._get_menu_style(font_family)
|
||||
self.about_menu.setStyleSheet(menu_style)
|
||||
|
||||
# 创建菜单项
|
||||
about_project_action = QAction("关于本项目", self.main_window)
|
||||
about_project_action.setFont(menu_font)
|
||||
about_project_action.triggered.connect(self.show_about_dialog)
|
||||
|
||||
# 添加项目主页选项(从帮助菜单移动过来)
|
||||
project_home_action = QAction("Github项目主页", self.main_window)
|
||||
project_home_action.setFont(menu_font)
|
||||
project_home_action.triggered.connect(self.open_project_home_page)
|
||||
|
||||
# 添加加入QQ群选项
|
||||
qq_group_action = QAction("加入QQ群", self.main_window)
|
||||
qq_group_action.setFont(menu_font)
|
||||
qq_group_action.triggered.connect(self.open_qq_group)
|
||||
|
||||
# 创建隐私协议菜单
|
||||
self._setup_privacy_menu()
|
||||
|
||||
# 添加到关于菜单
|
||||
self.about_menu.addAction(about_project_action)
|
||||
self.about_menu.addAction(project_home_action)
|
||||
self.about_menu.addAction(qq_group_action)
|
||||
self.about_menu.addSeparator()
|
||||
self.about_menu.addMenu(self.privacy_menu)
|
||||
|
||||
# 连接按钮点击事件
|
||||
if self.about_btn:
|
||||
self.about_btn.clicked.connect(lambda: self.show_menu(self.about_menu, self.about_btn))
|
||||
|
||||
def _setup_privacy_menu(self):
|
||||
"""设置"隐私协议"菜单"""
|
||||
# 获取菜单字体
|
||||
menu_font = self._get_menu_font()
|
||||
|
||||
# 创建隐私协议子菜单
|
||||
self.privacy_menu = QMenu("隐私协议", self.main_window)
|
||||
self.privacy_menu.setFont(menu_font)
|
||||
|
||||
# 设置与其他菜单一致的样式
|
||||
font_family = menu_font.family()
|
||||
menu_style = self._get_menu_style(font_family)
|
||||
self.privacy_menu.setStyleSheet(menu_style)
|
||||
|
||||
# 添加子选项
|
||||
view_privacy_action = QAction("查看完整隐私协议", self.main_window)
|
||||
view_privacy_action.setFont(menu_font)
|
||||
view_privacy_action.triggered.connect(self.open_privacy_policy)
|
||||
|
||||
revoke_privacy_action = QAction("撤回隐私协议", self.main_window)
|
||||
revoke_privacy_action.setFont(menu_font)
|
||||
revoke_privacy_action.triggered.connect(self.revoke_privacy_agreement)
|
||||
|
||||
# 添加到子菜单
|
||||
self.privacy_menu.addAction(view_privacy_action)
|
||||
self.privacy_menu.addAction(revoke_privacy_action)
|
||||
|
||||
def _get_menu_style(self, font_family):
|
||||
"""获取统一的菜单样式"""
|
||||
return f"""
|
||||
QMenu {{
|
||||
background-color: #E96948;
|
||||
color: white;
|
||||
font-family: "{font_family}";
|
||||
font-size: 14px;
|
||||
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;
|
||||
font-family: "{font_family}";
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}}
|
||||
QMenu::item:selected {{
|
||||
background-color: #F47A5B;
|
||||
border-radius: 4px;
|
||||
}}
|
||||
QMenu::separator {{
|
||||
height: 1px;
|
||||
background-color: #F47A5B;
|
||||
margin: 5px 15px;
|
||||
}}
|
||||
QMenu::item:checked {{
|
||||
background-color: #D25A3C;
|
||||
border-radius: 4px;
|
||||
}}
|
||||
"""
|
||||
|
||||
def _get_menu_font(self):
|
||||
"""获取菜单字体"""
|
||||
font_family = "Arial" # 默认字体族
|
||||
|
||||
try:
|
||||
from PySide6.QtGui import QFontDatabase
|
||||
|
||||
# 尝试加载字体
|
||||
font_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "fonts", "SmileySans-Oblique.ttf")
|
||||
if os.path.exists(font_path):
|
||||
font_id = QFontDatabase.addApplicationFont(font_path)
|
||||
if font_id != -1:
|
||||
font_family = QFontDatabase.applicationFontFamilies(font_id)[0]
|
||||
|
||||
# 创建菜单字体
|
||||
menu_font = QFont(font_family, 14)
|
||||
menu_font.setBold(True)
|
||||
return menu_font
|
||||
|
||||
except Exception as e:
|
||||
print(f"加载字体失败: {e}")
|
||||
# 返回默认字体
|
||||
menu_font = QFont(font_family, 14)
|
||||
menu_font.setBold(True)
|
||||
return menu_font
|
||||
|
||||
def _setup_settings_menu(self):
|
||||
"""设置"设置"菜单"""
|
||||
if not self.ui or not hasattr(self.ui, 'menu'):
|
||||
return
|
||||
|
||||
# 获取菜单字体
|
||||
menu_font = self._get_menu_font()
|
||||
font_family = menu_font.family()
|
||||
|
||||
# 创建开发者选项子菜单
|
||||
self.dev_menu = QMenu("开发者选项", self.main_window)
|
||||
self.dev_menu.setFont(menu_font) # 设置与UI_install.py中相同的字体
|
||||
|
||||
# 使用和主菜单相同的样式
|
||||
menu_style = self._get_menu_style(font_family)
|
||||
self.dev_menu.setStyleSheet(menu_style)
|
||||
|
||||
# 创建Debug子菜单
|
||||
self.debug_submenu = QMenu("Debug模式", self.main_window)
|
||||
self.debug_submenu.setFont(menu_font)
|
||||
self.debug_submenu.setStyleSheet(menu_style)
|
||||
|
||||
# 创建hosts文件选项子菜单
|
||||
self.hosts_submenu = QMenu("hosts文件选项", self.main_window)
|
||||
self.hosts_submenu.setFont(menu_font)
|
||||
self.hosts_submenu.setStyleSheet(menu_style)
|
||||
|
||||
# 添加IPv6支持选项
|
||||
self.ipv6_action = QAction("启用IPv6支持", self.main_window, checkable=True)
|
||||
self.ipv6_action.setFont(menu_font)
|
||||
|
||||
# 添加IPv6检测按钮,用于显示详细信息
|
||||
self.ipv6_test_action = QAction("测试IPv6连接", self.main_window)
|
||||
self.ipv6_test_action.setFont(menu_font)
|
||||
if self.ipv6_manager:
|
||||
self.ipv6_test_action.triggered.connect(self.ipv6_manager.show_ipv6_details)
|
||||
else:
|
||||
self.ipv6_test_action.triggered.connect(self.show_ipv6_manager_not_ready)
|
||||
|
||||
# 创建IPv6支持子菜单
|
||||
self.ipv6_submenu = QMenu("IPv6支持", self.main_window)
|
||||
self.ipv6_submenu.setFont(menu_font)
|
||||
self.ipv6_submenu.setStyleSheet(menu_style)
|
||||
|
||||
# 检查IPv6是否可用
|
||||
ipv6_available = False
|
||||
if self.ipv6_manager:
|
||||
ipv6_available = self.ipv6_manager.check_ipv6_availability()
|
||||
|
||||
if not ipv6_available:
|
||||
self.ipv6_action.setText("启用IPv6支持 (不可用)")
|
||||
self.ipv6_action.setEnabled(False)
|
||||
self.ipv6_action.setToolTip("未检测到可用的IPv6连接")
|
||||
|
||||
# 检查配置中是否已启用IPv6
|
||||
config = getattr(self.main_window, 'config', {})
|
||||
ipv6_enabled = False
|
||||
if isinstance(config, dict):
|
||||
ipv6_enabled = config.get("ipv6_enabled", False)
|
||||
# 如果配置中启用了IPv6但实际不可用,则强制禁用
|
||||
if ipv6_enabled and not ipv6_available:
|
||||
config["ipv6_enabled"] = False
|
||||
ipv6_enabled = False
|
||||
# 使用utils.save_config直接保存配置
|
||||
from utils import save_config
|
||||
save_config(config)
|
||||
|
||||
self.ipv6_action.setChecked(ipv6_enabled)
|
||||
|
||||
# 连接IPv6支持切换事件
|
||||
self.ipv6_action.triggered.connect(self._handle_ipv6_toggle)
|
||||
|
||||
# 将选项添加到IPv6子菜单
|
||||
self.ipv6_submenu.addAction(self.ipv6_action)
|
||||
self.ipv6_submenu.addAction(self.ipv6_test_action)
|
||||
|
||||
# 添加hosts子选项
|
||||
self.restore_hosts_action = QAction("还原软件备份的hosts文件", self.main_window)
|
||||
self.restore_hosts_action.setFont(menu_font)
|
||||
self.restore_hosts_action.triggered.connect(self.restore_hosts_backup)
|
||||
|
||||
self.clean_hosts_action = QAction("手动删除软件添加的hosts条目", self.main_window)
|
||||
self.clean_hosts_action.setFont(menu_font)
|
||||
self.clean_hosts_action.triggered.connect(self.clean_hosts_entries)
|
||||
|
||||
# 添加打开hosts文件选项
|
||||
self.open_hosts_action = QAction("打开hosts文件", self.main_window)
|
||||
self.open_hosts_action.setFont(menu_font)
|
||||
self.open_hosts_action.triggered.connect(self.open_hosts_file)
|
||||
|
||||
# 添加到hosts子菜单
|
||||
self.hosts_submenu.addAction(self.restore_hosts_action)
|
||||
self.hosts_submenu.addAction(self.clean_hosts_action)
|
||||
self.hosts_submenu.addAction(self.open_hosts_action)
|
||||
|
||||
# 创建Debug开关选项
|
||||
self.debug_action = QAction("Debug开关", self.main_window, checkable=True)
|
||||
self.debug_action.setFont(menu_font)
|
||||
|
||||
# 安全地获取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)
|
||||
|
||||
# 创建打开log文件选项
|
||||
self.open_log_action = QAction("打开log.txt", self.main_window)
|
||||
self.open_log_action.setFont(menu_font)
|
||||
# 初始状态根据debug模式设置启用状态
|
||||
self.open_log_action.setEnabled(debug_mode)
|
||||
|
||||
# 连接打开log文件的事件
|
||||
self.open_log_action.triggered.connect(self.open_log_file)
|
||||
|
||||
# 添加到Debug子菜单
|
||||
self.debug_submenu.addAction(self.debug_action)
|
||||
self.debug_submenu.addAction(self.open_log_action)
|
||||
|
||||
# 创建下载设置子菜单
|
||||
self.download_settings_menu = QMenu("下载设置", self.main_window)
|
||||
self.download_settings_menu.setFont(menu_font)
|
||||
self.download_settings_menu.setStyleSheet(menu_style)
|
||||
|
||||
# "修改下载源"按钮移至下载设置菜单
|
||||
self.switch_source_action = QAction("修改下载源", self.main_window)
|
||||
self.switch_source_action.setFont(menu_font)
|
||||
self.switch_source_action.setEnabled(True)
|
||||
self.switch_source_action.triggered.connect(self.show_under_development)
|
||||
|
||||
# 添加下载线程设置选项
|
||||
self.thread_settings_action = QAction("下载线程设置", self.main_window)
|
||||
self.thread_settings_action.setFont(menu_font)
|
||||
# 连接到下载线程设置对话框
|
||||
self.thread_settings_action.triggered.connect(self.show_download_thread_settings)
|
||||
|
||||
# 添加到下载设置子菜单
|
||||
self.download_settings_menu.addAction(self.switch_source_action)
|
||||
self.download_settings_menu.addAction(self.thread_settings_action)
|
||||
|
||||
# 添加到主菜单
|
||||
self.ui.menu.addMenu(self.download_settings_menu) # 添加下载设置子菜单
|
||||
self.ui.menu.addSeparator()
|
||||
self.ui.menu.addMenu(self.dev_menu) # 添加开发者选项子菜单
|
||||
|
||||
# 添加Debug子菜单到开发者选项菜单
|
||||
self.dev_menu.addMenu(self.debug_submenu)
|
||||
self.dev_menu.addMenu(self.hosts_submenu) # 添加hosts文件选项子菜单
|
||||
self.dev_menu.addMenu(self.ipv6_submenu) # 添加IPv6支持子菜单
|
||||
|
||||
def _handle_ipv6_toggle(self, enabled):
|
||||
"""处理IPv6支持切换事件
|
||||
|
||||
Args:
|
||||
enabled: 是否启用IPv6支持
|
||||
"""
|
||||
if not self.ipv6_manager:
|
||||
# 显示错误提示
|
||||
msg_box = self._create_message_box("错误", "\nIPv6管理器尚未初始化,请稍后再试。\n")
|
||||
msg_box.exec()
|
||||
# 恢复复选框状态
|
||||
self.ipv6_action.setChecked(not enabled)
|
||||
return
|
||||
|
||||
# 使用IPv6Manager处理切换
|
||||
success = self.ipv6_manager.toggle_ipv6_support(enabled)
|
||||
# 如果切换失败,恢复复选框状态
|
||||
if not success:
|
||||
self.ipv6_action.setChecked(not enabled)
|
||||
|
||||
def show_menu(self, menu, button):
|
||||
"""显示菜单
|
||||
|
||||
Args:
|
||||
menu: 要显示的菜单
|
||||
button: 触发菜单的按钮
|
||||
"""
|
||||
# 检查Ui_install中是否定义了show_menu方法
|
||||
if hasattr(self.ui, 'show_menu'):
|
||||
# 如果存在,使用UI中定义的方法
|
||||
self.ui.show_menu(menu, button)
|
||||
else:
|
||||
# 否则,使用默认的弹出方法
|
||||
global_pos = button.mapToGlobal(button.rect().bottomLeft())
|
||||
menu.popup(global_pos)
|
||||
|
||||
def open_project_home_page(self):
|
||||
"""打开项目主页"""
|
||||
webbrowser.open("https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT")
|
||||
|
||||
def open_github_page(self):
|
||||
"""打开项目GitHub页面"""
|
||||
webbrowser.open("https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT")
|
||||
|
||||
def open_faq_page(self):
|
||||
"""打开常见问题页面"""
|
||||
import locale
|
||||
# 根据系统语言选择FAQ页面
|
||||
system_lang = locale.getdefaultlocale()[0]
|
||||
if system_lang and system_lang.startswith('zh'):
|
||||
webbrowser.open("https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/blob/master/FAQ.md")
|
||||
else:
|
||||
webbrowser.open("https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/blob/master/FAQ-en.md")
|
||||
|
||||
def open_issues_page(self):
|
||||
"""打开GitHub问题页面"""
|
||||
webbrowser.open("https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/issues")
|
||||
|
||||
def open_qq_group(self):
|
||||
"""打开QQ群链接"""
|
||||
webbrowser.open("https://qm.qq.com/q/g9i04i5eec")
|
||||
|
||||
def open_privacy_policy(self):
|
||||
"""打开完整隐私协议(在GitHub上)"""
|
||||
webbrowser.open("https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/blob/master/PRIVACY.md")
|
||||
|
||||
def revoke_privacy_agreement(self):
|
||||
"""撤回隐私协议同意,并重启软件"""
|
||||
# 创建确认对话框
|
||||
msg_box = self._create_message_box(
|
||||
"确认操作",
|
||||
"\n您确定要撤回隐私协议同意吗?\n\n撤回后软件将立即重启,您需要重新阅读并同意隐私协议。\n",
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||
)
|
||||
|
||||
reply = msg_box.exec()
|
||||
if reply == QMessageBox.StandardButton.Yes:
|
||||
# 用户确认撤回
|
||||
try:
|
||||
# 导入隐私管理器
|
||||
from core.privacy_manager import PrivacyManager
|
||||
import sys
|
||||
import subprocess
|
||||
import os
|
||||
|
||||
# 创建实例并重置隐私协议同意
|
||||
privacy_manager = PrivacyManager()
|
||||
if privacy_manager.reset_privacy_agreement():
|
||||
# 显示重启提示
|
||||
restart_msg = self._create_message_box(
|
||||
"操作成功",
|
||||
"\n已成功撤回隐私协议同意。\n\n软件将立即重启。\n"
|
||||
)
|
||||
restart_msg.exec()
|
||||
|
||||
# 获取当前执行的Python解释器路径和脚本路径
|
||||
python_executable = sys.executable
|
||||
script_path = os.path.abspath(sys.argv[0])
|
||||
|
||||
# 构建重启命令
|
||||
restart_cmd = [python_executable, script_path]
|
||||
|
||||
# 启动新进程
|
||||
subprocess.Popen(restart_cmd)
|
||||
|
||||
# 退出当前进程
|
||||
sys.exit(0)
|
||||
else:
|
||||
# 显示失败提示
|
||||
fail_msg = self._create_message_box(
|
||||
"操作失败",
|
||||
"\n撤回隐私协议同意失败。\n\n请检查应用权限或稍后再试。\n"
|
||||
)
|
||||
fail_msg.exec()
|
||||
except Exception as e:
|
||||
# 显示错误提示
|
||||
error_msg = self._create_message_box(
|
||||
"错误",
|
||||
f"\n撤回隐私协议同意时发生错误:\n\n{str(e)}\n"
|
||||
)
|
||||
error_msg.exec()
|
||||
|
||||
def _create_message_box(self, title, message, buttons=QMessageBox.StandardButton.Ok):
|
||||
"""创建统一风格的消息框
|
||||
|
||||
Args:
|
||||
title: 消息框标题
|
||||
message: 消息内容
|
||||
buttons: 按钮类型,默认为确定按钮
|
||||
|
||||
Returns:
|
||||
QMessageBox: 配置好的消息框实例
|
||||
"""
|
||||
msg_box = msgbox_frame(
|
||||
f"{title} - {APP_NAME}",
|
||||
message,
|
||||
buttons,
|
||||
)
|
||||
return msg_box
|
||||
|
||||
def show_under_development(self):
|
||||
"""显示功能正在开发中的提示"""
|
||||
msg_box = self._create_message_box("提示", "\n该功能正在开发中,敬请期待!\n")
|
||||
msg_box.exec()
|
||||
|
||||
def show_download_thread_settings(self):
|
||||
"""显示下载线程设置对话框"""
|
||||
if hasattr(self.main_window, 'download_manager'):
|
||||
self.main_window.download_manager.show_download_thread_settings()
|
||||
else:
|
||||
# 如果下载管理器不可用,显示错误信息
|
||||
msg_box = self._create_message_box("错误", "\n下载管理器未初始化,无法修改下载线程设置。\n")
|
||||
msg_box.exec()
|
||||
|
||||
def open_log_file(self):
|
||||
"""打开log.txt文件"""
|
||||
try:
|
||||
# 使用操作系统默认程序打开日志文件
|
||||
if os.name == 'nt': # Windows
|
||||
os.startfile(LOG_FILE)
|
||||
else: # macOS 和 Linux
|
||||
import subprocess
|
||||
subprocess.call(['xdg-open', LOG_FILE])
|
||||
except Exception as e:
|
||||
msg_box = self._create_message_box("错误", f"\n打开log.txt文件失败:\n\n{str(e)}\n")
|
||||
msg_box.exec()
|
||||
|
||||
def restore_hosts_backup(self):
|
||||
"""还原软件备份的hosts文件"""
|
||||
if hasattr(self.main_window, 'download_manager') and hasattr(self.main_window.download_manager, 'hosts_manager'):
|
||||
try:
|
||||
# 调用恢复hosts文件的方法
|
||||
result = self.main_window.download_manager.hosts_manager.restore()
|
||||
|
||||
if result:
|
||||
msg_box = self._create_message_box("成功", "\nhosts文件已成功还原为备份版本。\n")
|
||||
else:
|
||||
msg_box = self._create_message_box("警告", "\n还原hosts文件失败或没有找到备份文件。\n")
|
||||
|
||||
msg_box.exec()
|
||||
except Exception as e:
|
||||
msg_box = self._create_message_box("错误", f"\n还原hosts文件时发生错误:\n\n{str(e)}\n")
|
||||
msg_box.exec()
|
||||
else:
|
||||
msg_box = self._create_message_box("错误", "\n无法访问hosts管理器。\n")
|
||||
msg_box.exec()
|
||||
|
||||
def clean_hosts_entries(self):
|
||||
"""手动删除软件添加的hosts条目"""
|
||||
if hasattr(self.main_window, 'download_manager') and hasattr(self.main_window.download_manager, 'hosts_manager'):
|
||||
try:
|
||||
# 调用清理hosts条目的方法
|
||||
result = self.main_window.download_manager.hosts_manager.check_and_clean_all_entries()
|
||||
|
||||
if result:
|
||||
msg_box = self._create_message_box("成功", "\n已成功清理软件添加的hosts条目。\n")
|
||||
else:
|
||||
msg_box = self._create_message_box("提示", "\n未发现软件添加的hosts条目或清理操作失败。\n")
|
||||
|
||||
msg_box.exec()
|
||||
except Exception as e:
|
||||
msg_box = self._create_message_box("错误", f"\n清理hosts条目时发生错误:\n\n{str(e)}\n")
|
||||
msg_box.exec()
|
||||
else:
|
||||
msg_box = self._create_message_box("错误", "\n无法访问hosts管理器。\n")
|
||||
msg_box.exec()
|
||||
|
||||
def open_hosts_file(self):
|
||||
"""打开系统hosts文件"""
|
||||
try:
|
||||
# 获取hosts文件路径
|
||||
hosts_path = os.path.join(os.environ['SystemRoot'], 'System32', 'drivers', 'etc', 'hosts')
|
||||
|
||||
# 检查文件是否存在
|
||||
if os.path.exists(hosts_path):
|
||||
# 使用操作系统默认程序打开hosts文件
|
||||
if os.name == 'nt': # Windows
|
||||
# 尝试以管理员权限打开记事本编辑hosts文件
|
||||
try:
|
||||
# 使用PowerShell以管理员身份启动记事本
|
||||
subprocess.Popen(["powershell", "Start-Process", "notepad", hosts_path, "-Verb", "RunAs"])
|
||||
except Exception as e:
|
||||
# 如果失败,尝试直接打开
|
||||
os.startfile(hosts_path)
|
||||
else: # macOS 和 Linux
|
||||
import subprocess
|
||||
subprocess.call(['xdg-open', hosts_path])
|
||||
else:
|
||||
msg_box = self._create_message_box("错误", f"\nhosts文件不存在:\n{hosts_path}\n")
|
||||
msg_box.exec()
|
||||
except Exception as e:
|
||||
msg_box = self._create_message_box("错误", f"\n打开hosts文件时发生错误:\n\n{str(e)}\n")
|
||||
msg_box.exec()
|
||||
|
||||
def show_about_dialog(self):
|
||||
"""显示关于对话框"""
|
||||
about_text = f"""
|
||||
<p><b>{APP_NAME} v{APP_VERSION}</b></p>
|
||||
<p>GitHub: <a href="https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT">https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT</a></p>
|
||||
<p>原作: <a href="https://github.com/Yanam1Anna">Yanam1Anna</a></p>
|
||||
<p>此应用根据 <a href="https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/blob/master/LICENSE">GPL-3.0 许可证</a> 授权。</p>
|
||||
<br>
|
||||
<p><b>感谢:</b></p>
|
||||
<p>- <a href="https://github.com/HTony03">HTony03</a>:对原项目部分源码的重构、逻辑优化和功能实现提供了支持。</p>
|
||||
<p>- <a href="https://github.com/ABSIDIA">钨鸮</a>:对于云端资源存储提供了支持。</p>
|
||||
<p>- <a href="https://github.com/XIU2/CloudflareSpeedTest">XIU2/CloudflareSpeedTest</a>:提供了 IP 优选功能的核心支持。</p>
|
||||
<p>- <a href="https://github.com/hosxy/aria2-fast">hosxy/aria2-fast</a>:提供了修改版aria2c,提高了下载速度和性能。</p>
|
||||
"""
|
||||
msg_box = msgbox_frame(
|
||||
f"关于 - {APP_NAME}",
|
||||
about_text,
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
msg_box.setTextFormat(Qt.TextFormat.RichText) # 使用Qt.TextFormat
|
||||
msg_box.exec()
|
||||
|
||||
def show_ipv6_manager_not_ready(self):
|
||||
"""显示IPv6管理器未准备好的提示"""
|
||||
msg_box = self._create_message_box("错误", "\nIPv6管理器尚未初始化,请稍后再试。\n")
|
||||
msg_box.exec()
|
||||
141
source/core/window_manager.py
Normal file
@@ -0,0 +1,141 @@
|
||||
from PySide6.QtCore import Qt, QPoint, QRect, QSize
|
||||
from PySide6.QtGui import QPainterPath, QRegion
|
||||
|
||||
class WindowManager:
|
||||
"""窗口管理器类,用于处理窗口的基本行为,如拖拽、调整大小和圆角设置"""
|
||||
|
||||
def __init__(self, parent_window):
|
||||
"""初始化窗口管理器
|
||||
|
||||
Args:
|
||||
parent_window: 父窗口实例
|
||||
"""
|
||||
self.window = parent_window
|
||||
self.ui = parent_window.ui
|
||||
|
||||
# 拖动窗口相关变量
|
||||
self._drag_position = QPoint()
|
||||
self._is_dragging = False
|
||||
|
||||
# 窗口比例
|
||||
self.aspect_ratio = 16 / 9
|
||||
self.updateRoundedCorners = True
|
||||
|
||||
# 设置圆角窗口
|
||||
self.setRoundedCorners()
|
||||
|
||||
def setRoundedCorners(self):
|
||||
"""设置窗口圆角"""
|
||||
# 实现圆角窗口
|
||||
path = QPainterPath()
|
||||
path.addRoundedRect(self.window.rect(), 20, 20)
|
||||
mask = QRegion(path.toFillPolygon().toPolygon())
|
||||
self.window.setMask(mask)
|
||||
|
||||
# 更新resize事件时更新圆角
|
||||
self.updateRoundedCorners = True
|
||||
|
||||
def handle_mouse_press(self, event):
|
||||
"""处理鼠标按下事件
|
||||
|
||||
Args:
|
||||
event: 鼠标事件
|
||||
"""
|
||||
if event.button() == Qt.MouseButton.LeftButton:
|
||||
# 只有当鼠标在标题栏区域时才可以拖动
|
||||
if hasattr(self.ui, 'title_bar') and self.ui.title_bar.geometry().contains(event.position().toPoint()):
|
||||
self._is_dragging = True
|
||||
self._drag_position = event.globalPosition().toPoint() - self.window.frameGeometry().topLeft()
|
||||
event.accept()
|
||||
|
||||
def handle_mouse_move(self, event):
|
||||
"""处理鼠标移动事件
|
||||
|
||||
Args:
|
||||
event: 鼠标事件
|
||||
"""
|
||||
if event.buttons() & Qt.MouseButton.LeftButton and self._is_dragging:
|
||||
self.window.move(event.globalPosition().toPoint() - self._drag_position)
|
||||
event.accept()
|
||||
|
||||
def handle_mouse_release(self, event):
|
||||
"""处理鼠标释放事件
|
||||
|
||||
Args:
|
||||
event: 鼠标事件
|
||||
"""
|
||||
if event.button() == Qt.MouseButton.LeftButton:
|
||||
self._is_dragging = False
|
||||
event.accept()
|
||||
|
||||
def handle_resize(self, event):
|
||||
"""当窗口大小改变时更新圆角和维持纵横比
|
||||
|
||||
Args:
|
||||
event: 窗口大小改变事件
|
||||
"""
|
||||
# 计算基于当前宽度的合适高度,以维持16:9比例
|
||||
new_width = event.size().width()
|
||||
new_height = int(new_width / self.aspect_ratio)
|
||||
|
||||
if new_height != event.size().height():
|
||||
# 阻止变形,保持比例
|
||||
self.window.resize(new_width, new_height)
|
||||
|
||||
# 更新主容器大小
|
||||
if hasattr(self.ui, 'main_container'):
|
||||
self.ui.main_container.setGeometry(0, 0, new_width, new_height)
|
||||
|
||||
# 更新内容容器大小
|
||||
if hasattr(self.ui, 'content_container'):
|
||||
self.ui.content_container.setGeometry(0, 0, new_width, new_height)
|
||||
|
||||
# 更新标题栏宽度和高度
|
||||
if hasattr(self.ui, 'title_bar'):
|
||||
self.ui.title_bar.setGeometry(0, 0, new_width, 35)
|
||||
|
||||
# 更新菜单区域
|
||||
if hasattr(self.ui, 'menu_area'):
|
||||
self.ui.menu_area.setGeometry(0, 35, new_width, 30)
|
||||
|
||||
# 更新内容区域大小
|
||||
if hasattr(self.ui, 'inner_content'):
|
||||
self.ui.inner_content.setGeometry(0, 65, new_width, new_height - 65)
|
||||
|
||||
# 更新背景图大小
|
||||
if hasattr(self.ui, 'Mainbg'):
|
||||
self.ui.Mainbg.setGeometry(0, 0, new_width, new_height - 65)
|
||||
|
||||
if hasattr(self.ui, 'loadbg'):
|
||||
self.ui.loadbg.setGeometry(0, 0, new_width, new_height - 65)
|
||||
|
||||
# 调整按钮位置 - 固定在右侧
|
||||
right_margin = 20 # 减小右边距,使按钮更靠右
|
||||
if hasattr(self.ui, 'button_container'):
|
||||
btn_width = 211 # 扩大后的容器宽度
|
||||
btn_height = 111 # 扩大后的容器高度
|
||||
x_pos = new_width - btn_width - right_margin
|
||||
y_pos = int((new_height - 65) * 0.28) - 10 # 调整为更靠上的位置
|
||||
self.ui.button_container.setGeometry(x_pos, y_pos, btn_width, btn_height)
|
||||
|
||||
# 添加卸载补丁按钮容器的位置调整
|
||||
if hasattr(self.ui, 'uninstall_container'):
|
||||
btn_width = 211 # 扩大后的容器宽度
|
||||
btn_height = 111 # 扩大后的容器高度
|
||||
x_pos = new_width - btn_width - right_margin
|
||||
y_pos = int((new_height - 65) * 0.46) - 10 # 调整为中间位置
|
||||
self.ui.uninstall_container.setGeometry(x_pos, y_pos, btn_width, btn_height)
|
||||
|
||||
if hasattr(self.ui, 'exit_container'):
|
||||
btn_width = 211 # 扩大后的容器宽度
|
||||
btn_height = 111 # 扩大后的容器高度
|
||||
x_pos = new_width - btn_width - right_margin
|
||||
y_pos = int((new_height - 65) * 0.64) - 10 # 调整为更靠下的位置
|
||||
self.ui.exit_container.setGeometry(x_pos, y_pos, btn_width, btn_height)
|
||||
|
||||
# 更新圆角
|
||||
if hasattr(self, 'updateRoundedCorners') and self.updateRoundedCorners:
|
||||
path = QPainterPath()
|
||||
path.addRoundedRect(self.window.rect(), 20, 20)
|
||||
mask = QRegion(path.toFillPolygon().toPolygon())
|
||||
self.window.setMask(mask)
|
||||
@@ -3,7 +3,7 @@ import base64
|
||||
|
||||
# 配置信息
|
||||
app_data = {
|
||||
"APP_VERSION": "1.1.0",
|
||||
"APP_VERSION": "1.3.1",
|
||||
"APP_NAME": "FRAISEMOE Addons Installer NEXT",
|
||||
"TEMP": "TEMP",
|
||||
"CACHE": "FRAISEMOE",
|
||||
@@ -63,4 +63,16 @@ GAME_INFO = app_data["game_info"]
|
||||
BLOCK_SIZE = 67108864
|
||||
HASH_SIZE = 134217728
|
||||
PLUGIN_HASH = {game: info["hash"] for game, info in GAME_INFO.items()}
|
||||
PROCESS_INFO = {info["exe"]: game for game, info in GAME_INFO.items()}
|
||||
PROCESS_INFO = {info["exe"]: game for game, info in GAME_INFO.items()}
|
||||
|
||||
# 下载线程档位设置
|
||||
DOWNLOAD_THREADS = {
|
||||
"low": 1, # 低速
|
||||
"medium": 8, # 中速(默认)
|
||||
"high": 16, # 高速
|
||||
"extreme": 32, # 极速
|
||||
"insane": 64 # 狂暴
|
||||
}
|
||||
|
||||
# 默认下载线程档位
|
||||
DEFAULT_DOWNLOAD_THREAD_LEVEL = "high"
|
||||
96
source/data/privacy_policy.py
Normal file
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
# 隐私协议的缩略版内容
|
||||
PRIVACY_POLICY_BRIEF = """
|
||||
# FRAISEMOE Addons Installer NEXT 隐私政策摘要
|
||||
|
||||
本应用在运行过程中会收集和处理以下信息:
|
||||
|
||||
## 收集的信息
|
||||
- **系统信息**:程序版本号。
|
||||
- **网络信息**:IP 地址、ISP、地理位置(用于使用统计)、下载统计、IPv6 连接测试(通过访问 testipv6.cn)、IPv6 地址获取(通过 ipw.cn)。
|
||||
- **文件信息**:游戏安装路径、文件哈希值。
|
||||
|
||||
## 系统修改
|
||||
- 使用 Cloudflare 加速时会临时修改系统 hosts 文件。
|
||||
- 修改前会自动备份,程序退出时自动恢复。
|
||||
|
||||
## 第三方服务
|
||||
- **Cloudflare 服务**:通过开源项目 CloudflareSpeedTest (CFST) 提供,用于优化下载速度。此过程会将您的 IP 提交至 Cloudflare 节点。
|
||||
- **云端配置服务**:获取配置信息。服务器会记录您的 IP、ISP 及地理位置用于统计。
|
||||
- **IPv6 测试服务**:应用使用 testipv6.cn 和 ipw.cn 测试和获取 IPv6 连接信息。
|
||||
|
||||
完整的隐私政策可在本程序的 GitHub 仓库中查看。
|
||||
"""
|
||||
|
||||
# 隐私协议的英文版缩略版内容
|
||||
PRIVACY_POLICY_BRIEF_EN = """
|
||||
# FRAISEMOE Addons Installer NEXT Privacy Policy Summary
|
||||
|
||||
This application collects and processes the following information:
|
||||
|
||||
## Information Collected
|
||||
- **System info**: Application version.
|
||||
- **Network info**: IP address, ISP, geographic location (for usage statistics), download statistics, IPv6 connectivity test (via testipv6.cn), IPv6 address acquisition (via ipw.cn).
|
||||
- **File info**: Game installation paths, file hash values.
|
||||
|
||||
## System Modifications
|
||||
- Temporarily modifies system hosts file when using Cloudflare acceleration.
|
||||
- Automatically backs up before modification and restores upon exit.
|
||||
|
||||
## Third-party Services
|
||||
- **Cloudflare services**: Provided via the open-source project CloudflareSpeedTest (CFST) to optimize download speeds. This process submits your IP to Cloudflare nodes.
|
||||
- **Cloud configuration services**: For obtaining configuration information. The server logs your IP, ISP, and location for statistical purposes.
|
||||
- **IPv6 testing services**: The application uses testipv6.cn and ipw.cn to test and retrieve IPv6 connection information.
|
||||
|
||||
The complete privacy policy can be found in the program's GitHub repository.
|
||||
"""
|
||||
|
||||
# 默认隐私协议版本 - 本地版本的日期
|
||||
PRIVACY_POLICY_VERSION = "2025.08.04"
|
||||
|
||||
def get_local_privacy_policy():
|
||||
"""获取本地打包的隐私协议文件
|
||||
|
||||
Returns:
|
||||
tuple: (隐私协议内容, 版本号, 错误信息)
|
||||
"""
|
||||
# 尝试不同的可能路径
|
||||
possible_paths = [
|
||||
"PRIVACY.md", # 相对于可执行文件
|
||||
os.path.join(os.path.dirname(sys.executable), "PRIVACY.md"), # 可执行文件目录
|
||||
os.path.join(os.path.dirname(__file__), "PRIVACY.md"), # 当前模块目录
|
||||
]
|
||||
|
||||
for path in possible_paths:
|
||||
try:
|
||||
if os.path.exists(path):
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
# 提取更新日期
|
||||
date_pattern = r'最后更新日期:(\d{4}年\d{1,2}月\d{1,2}日)'
|
||||
match = re.search(date_pattern, content)
|
||||
|
||||
if match:
|
||||
date_str = match.group(1)
|
||||
try:
|
||||
date_obj = datetime.strptime(date_str, '%Y年%m月%d日')
|
||||
date_version = date_obj.strftime('%Y.%m.%d')
|
||||
print(f"成功读取本地隐私协议文件: {path}, 版本: {date_version}")
|
||||
return content, date_version, ""
|
||||
except ValueError:
|
||||
print(f"本地隐私协议日期格式解析错误: {path}")
|
||||
else:
|
||||
print(f"本地隐私协议未找到更新日期: {path}")
|
||||
except Exception as e:
|
||||
print(f"读取本地隐私协议失败 {path}: {str(e)}")
|
||||
|
||||
# 所有路径都尝试失败,使用默认版本
|
||||
return PRIVACY_POLICY_BRIEF, PRIVACY_POLICY_VERSION, "无法读取本地隐私协议文件"
|
||||
@@ -1,186 +0,0 @@
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import re
|
||||
from urllib.parse import urlparse
|
||||
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
|
||||
|
||||
# 下载线程类
|
||||
class DownloadThread(QThread):
|
||||
progress = Signal(dict)
|
||||
finished = Signal(bool, str)
|
||||
|
||||
def __init__(self, url, _7z_path, game_version, parent=None):
|
||||
super().__init__(parent)
|
||||
self.url = url
|
||||
self._7z_path = _7z_path
|
||||
self.game_version = game_version
|
||||
self.process = None
|
||||
self._is_running = True
|
||||
|
||||
def stop(self):
|
||||
if self.process and self.process.poll() is None:
|
||||
self._is_running = False
|
||||
try:
|
||||
# 使用 taskkill 强制终止进程及其子进程,并隐藏窗口
|
||||
subprocess.run(['taskkill', '/F', '/T', '/PID', str(self.process.pid)], check=True, creationflags=subprocess.CREATE_NO_WINDOW)
|
||||
except (subprocess.CalledProcessError, FileNotFoundError) as e:
|
||||
print(f"停止下载进程时出错: {e}")
|
||||
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
if not self._is_running:
|
||||
self.finished.emit(False, "下载已手动停止。")
|
||||
return
|
||||
|
||||
aria2c_path = resource_path("aria2c.exe")
|
||||
download_dir = os.path.dirname(self._7z_path)
|
||||
file_name = os.path.basename(self._7z_path)
|
||||
|
||||
parsed_url = urlparse(self.url)
|
||||
referer = f"{parsed_url.scheme}://{parsed_url.netloc}/"
|
||||
|
||||
command = [
|
||||
aria2c_path,
|
||||
]
|
||||
|
||||
command.extend([
|
||||
'--dir', download_dir,
|
||||
'--out', file_name,
|
||||
'--user-agent', UA,
|
||||
'--referer', referer,
|
||||
'--header', f'Origin: {referer.rstrip("/")}',
|
||||
'--header', 'Accept: */*',
|
||||
'--header', 'Accept-Language: zh-CN,zh;q=0.9,en;q=0.8',
|
||||
'--header', 'Accept-Encoding: gzip, deflate, br',
|
||||
'--header', 'Cache-Control: no-cache',
|
||||
'--header', 'Pragma: no-cache',
|
||||
'--header', 'DNT: 1',
|
||||
'--header', 'Sec-Fetch-Dest: empty',
|
||||
'--header', 'Sec-Fetch-Mode: cors',
|
||||
'--header', 'Sec-Fetch-Site: same-origin',
|
||||
'--http-accept-gzip=true',
|
||||
'--console-log-level=info',
|
||||
'--summary-interval=1',
|
||||
'--log-level=info',
|
||||
'--max-tries=3',
|
||||
'--retry-wait=2',
|
||||
'--connect-timeout=60',
|
||||
'--timeout=60',
|
||||
'--auto-file-renaming=false',
|
||||
'--allow-overwrite=true',
|
||||
'--split=16',
|
||||
'--max-connection-per-server=16'
|
||||
])
|
||||
|
||||
# 证书验证现在总是需要,因为我们依赖hosts文件
|
||||
command.append('--check-certificate=false')
|
||||
|
||||
command.append(self.url)
|
||||
|
||||
# 打印将要执行的命令,用于调试
|
||||
print(f"即将执行的 Aria2c 命令: {' '.join(command)}")
|
||||
|
||||
creation_flags = subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0
|
||||
self.process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, encoding='utf-8', errors='replace', creationflags=creation_flags)
|
||||
|
||||
# 正则表达式用于解析aria2c的输出
|
||||
# 例如: #1 GID[...]( 5%) CN:1 DL:10.5MiB/s ETA:1m30s
|
||||
progress_pattern = re.compile(r'\((\d{1,3})%\).*?CN:(\d+).*?DL:\s*([^\s]+).*?ETA:\s*([^\s]+)')
|
||||
|
||||
full_output = []
|
||||
while self._is_running and self.process.poll() is None:
|
||||
if self.process.stdout:
|
||||
line = self.process.stdout.readline()
|
||||
if not line:
|
||||
break
|
||||
else:
|
||||
break
|
||||
|
||||
full_output.append(line)
|
||||
print(line.strip()) # 在控制台输出实时日志
|
||||
|
||||
match = progress_pattern.search(line)
|
||||
if match:
|
||||
percent = int(match.group(1))
|
||||
threads = match.group(2)
|
||||
speed = match.group(3)
|
||||
eta = match.group(4)
|
||||
self.progress.emit({
|
||||
"game": self.game_version,
|
||||
"percent": percent,
|
||||
"threads": threads,
|
||||
"speed": speed,
|
||||
"eta": eta
|
||||
})
|
||||
|
||||
return_code = self.process.wait()
|
||||
|
||||
if not self._is_running: # 如果是手动停止的
|
||||
self.finished.emit(False, "下载已手动停止。")
|
||||
return
|
||||
|
||||
if return_code == 0:
|
||||
self.progress.emit({
|
||||
"game": self.game_version,
|
||||
"percent": 100,
|
||||
"threads": "N/A",
|
||||
"speed": "N/A",
|
||||
"eta": "完成"
|
||||
})
|
||||
self.finished.emit(True, "")
|
||||
else:
|
||||
error_message = f"\nAria2c下载失败,退出码: {return_code}\n\n--- Aria2c 输出 ---\n{''.join(full_output)}\n---------------------\n"
|
||||
self.finished.emit(False, error_message)
|
||||
|
||||
except Exception as e:
|
||||
if self._is_running:
|
||||
self.finished.emit(False, f"\n下载时发生未知错误\n\n【错误信息】: {e}\n")
|
||||
|
||||
# 下载进度窗口类
|
||||
class ProgressWindow(QDialog):
|
||||
def __init__(self, parent=None):
|
||||
super(ProgressWindow, self).__init__(parent)
|
||||
self.setWindowTitle(f"下载进度 - {APP_NAME}")
|
||||
self.resize(450, 180)
|
||||
self.setWindowFlags(self.windowFlags() & ~Qt.WindowType.WindowCloseButtonHint)
|
||||
self.setWindowFlags(self.windowFlags() & ~Qt.WindowType.WindowSystemMenuHint)
|
||||
|
||||
layout = QVBoxLayout()
|
||||
self.game_label = QLabel("正在启动下载,请稍后...")
|
||||
self.progress_bar = QProgressBar()
|
||||
self.progress_bar.setValue(0)
|
||||
self.stats_label = QLabel("速度: - | 线程: - | 剩余时间: -")
|
||||
self.stop_button = QtWidgets.QPushButton("停止下载")
|
||||
|
||||
layout.addWidget(self.game_label)
|
||||
layout.addWidget(self.progress_bar)
|
||||
layout.addWidget(self.stats_label)
|
||||
layout.addWidget(self.stop_button)
|
||||
self.setLayout(layout)
|
||||
|
||||
def update_progress(self, data):
|
||||
game_version = data.get("game", "未知游戏")
|
||||
percent = data.get("percent", 0)
|
||||
speed = data.get("speed", "-")
|
||||
threads = data.get("threads", "-")
|
||||
eta = data.get("eta", "-")
|
||||
|
||||
self.game_label.setText(f"正在下载: {game_version}")
|
||||
self.progress_bar.setValue(int(percent))
|
||||
self.stats_label.setText(f"速度: {speed} | 线程: {threads} | 剩余时间: {eta}")
|
||||
|
||||
if percent == 100:
|
||||
self.stop_button.setEnabled(False)
|
||||
self.stop_button.setText("下载完成")
|
||||
QTimer.singleShot(1500, self.accept)
|
||||
|
||||
def closeEvent(self, event):
|
||||
# 覆盖默认的关闭事件,防止用户通过其他方式关闭窗口
|
||||
# 如果需要,可以在这里添加逻辑,例如询问用户是否要停止下载
|
||||
event.ignore()
|
||||
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,146 +0,0 @@
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from utils import resource_path
|
||||
|
||||
class IpOptimizer:
|
||||
def __init__(self):
|
||||
self.process = None
|
||||
|
||||
def get_optimal_ip(self, url: str) -> str | None:
|
||||
"""
|
||||
使用 CloudflareSpeedTest 工具获取给定 URL 的最优 Cloudflare IP。
|
||||
|
||||
Args:
|
||||
url: 需要进行优选的下载链接。
|
||||
|
||||
Returns:
|
||||
最优的 IP 地址字符串,如果找不到则返回 None。
|
||||
"""
|
||||
try:
|
||||
cst_path = resource_path("cfst.exe")
|
||||
if not os.path.exists(cst_path):
|
||||
print(f"错误: cfst.exe 未在资源路径中找到。")
|
||||
return None
|
||||
|
||||
ip_txt_path = resource_path("ip.txt")
|
||||
command = [
|
||||
cst_path,
|
||||
"-p", "1",
|
||||
"-o", "",
|
||||
"-url", url,
|
||||
"-f", ip_txt_path,
|
||||
"-dd",
|
||||
]
|
||||
|
||||
creation_flags = subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0
|
||||
|
||||
print("--- CloudflareSpeedTest 开始执行 ---")
|
||||
|
||||
self.process = subprocess.Popen(
|
||||
command,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
encoding='utf-8',
|
||||
errors='replace',
|
||||
creationflags=creation_flags,
|
||||
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+.*$')
|
||||
|
||||
stdout = self.process.stdout
|
||||
if not stdout:
|
||||
print("错误: 无法获取子进程的输出流。")
|
||||
return None
|
||||
|
||||
optimal_ip = None
|
||||
timeout_counter = 0
|
||||
max_timeout = 60
|
||||
|
||||
while True:
|
||||
if self.process.poll() is not None:
|
||||
break
|
||||
try:
|
||||
ready = True
|
||||
try:
|
||||
line = stdout.readline()
|
||||
except:
|
||||
ready = False
|
||||
|
||||
if not ready or not line:
|
||||
timeout_counter += 1
|
||||
if timeout_counter > max_timeout:
|
||||
print("超时: CloudflareSpeedTest 响应超时")
|
||||
break
|
||||
time.sleep(1)
|
||||
continue
|
||||
|
||||
timeout_counter = 0
|
||||
|
||||
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
|
||||
|
||||
except Exception as e:
|
||||
print(f"读取输出时发生错误: {e}")
|
||||
break
|
||||
|
||||
self.stop()
|
||||
|
||||
print("--- CloudflareSpeedTest 执行结束 ---")
|
||||
return optimal_ip
|
||||
|
||||
except Exception as e:
|
||||
print(f"执行 CloudflareSpeedTest 时发生错误: {e}")
|
||||
return None
|
||||
|
||||
def stop(self):
|
||||
if self.process and self.process.poll() is None:
|
||||
print("正在终止 CloudflareSpeedTest 进程...")
|
||||
try:
|
||||
if self.process.stdin and not self.process.stdin.closed:
|
||||
self.process.stdin.write('\n')
|
||||
self.process.stdin.flush()
|
||||
self.process.stdin.close()
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
self.process.terminate()
|
||||
self.process.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
self.process.kill()
|
||||
self.process.wait()
|
||||
print("CloudflareSpeedTest 进程已终止。")
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 用于直接测试此模块
|
||||
test_url = "https://speed.cloudflare.com/__down?during=download&bytes=104857600"
|
||||
optimizer = IpOptimizer()
|
||||
ip = optimizer.get_optimal_ip(test_url)
|
||||
if ip:
|
||||
print(f"为 {test_url} 找到的最优 IP 是: {ip}")
|
||||
else:
|
||||
print(f"未能为 {test_url} 找到最优 IP。")
|
||||
@@ -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>
|
||||
519
source/ui/Ui_install.py
Normal file
@@ -0,0 +1,519 @@
|
||||
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)
|
||||
# 创建菜单样式表,直接在样式表中指定字体族
|
||||
menu_style = f"""
|
||||
QMenu {{
|
||||
background-color: #E96948;
|
||||
color: white;
|
||||
font-family: "{font_family}";
|
||||
font-size: 14px;
|
||||
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;
|
||||
font-family: "{font_family}";
|
||||
font-weight: bold;
|
||||
}}
|
||||
QMenu::item:selected {{
|
||||
background-color: #F47A5B;
|
||||
border-radius: 4px;
|
||||
}}
|
||||
QMenu::separator {{
|
||||
height: 1px;
|
||||
background-color: #F47A5B;
|
||||
margin: 5px 15px;
|
||||
}}
|
||||
QMenu::item:checked {{
|
||||
background-color: #D25A3C;
|
||||
border-radius: 4px;
|
||||
}}
|
||||
"""
|
||||
self.menu.setStyleSheet(menu_style)
|
||||
|
||||
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(menu_style)
|
||||
|
||||
# 连接按钮点击事件到显示对应菜单
|
||||
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)
|
||||
|
||||
5
source/ui/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from .Ui_install import Ui_MainWindows
|
||||
|
||||
__all__ = [
|
||||
'Ui_MainWindows'
|
||||
]
|
||||
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-fast_x64.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,8 +290,50 @@ 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):
|
||||
def apply_ip(self, hostname, ip_address, clean=True):
|
||||
if not self.original_content:
|
||||
if not self.backup():
|
||||
return False
|
||||
@@ -256,17 +347,24 @@ 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))]
|
||||
# 首先清理已有的同域名记录(如果需要)
|
||||
if clean:
|
||||
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 +373,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 +429,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 +448,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 +460,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 +471,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):
|
||||
64
source/utils/logger.py
Normal file
@@ -0,0 +1,64 @@
|
||||
from .helpers import censor_url
|
||||
import logging
|
||||
import os
|
||||
from data.config import CACHE
|
||||
|
||||
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()
|
||||
|
||||
def setup_logger(name):
|
||||
"""设置并返回一个命名的logger
|
||||
|
||||
Args:
|
||||
name: logger的名称
|
||||
|
||||
Returns:
|
||||
logging.Logger: 配置好的logger对象
|
||||
"""
|
||||
# 创建logger
|
||||
logger = logging.getLogger(name)
|
||||
|
||||
# 避免重复添加处理器
|
||||
if logger.hasHandlers():
|
||||
return logger
|
||||
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
# 确保日志目录存在
|
||||
log_dir = os.path.join(CACHE, "logs")
|
||||
os.makedirs(log_dir, exist_ok=True)
|
||||
log_file = os.path.join(log_dir, f"{name}.log")
|
||||
|
||||
# 创建文件处理器
|
||||
file_handler = logging.FileHandler(log_file, encoding="utf-8")
|
||||
file_handler.setLevel(logging.DEBUG)
|
||||
|
||||
# 创建控制台处理器
|
||||
console_handler = logging.StreamHandler()
|
||||
console_handler.setLevel(logging.INFO)
|
||||
|
||||
# 创建格式器并添加到处理器
|
||||
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
file_handler.setFormatter(formatter)
|
||||
console_handler.setFormatter(formatter)
|
||||
|
||||
# 添加处理器到logger
|
||||
logger.addHandler(file_handler)
|
||||
logger.addHandler(console_handler)
|
||||
|
||||
return logger
|
||||
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 ---")
|
||||
372
source/workers/download.py
Normal file
@@ -0,0 +1,372 @@
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import re
|
||||
from urllib.parse import urlparse
|
||||
from PySide6 import QtCore, QtWidgets
|
||||
from PySide6.QtCore import (Qt, Signal, QThread, QTimer)
|
||||
from PySide6.QtWidgets import (QLabel, QProgressBar, QVBoxLayout, QDialog, QHBoxLayout)
|
||||
from utils import resource_path
|
||||
from data.config import APP_NAME, UA
|
||||
import signal
|
||||
import ctypes
|
||||
import time
|
||||
|
||||
# Windows API常量和函数
|
||||
if sys.platform == 'win32':
|
||||
kernel32 = ctypes.windll.kernel32
|
||||
PROCESS_ALL_ACCESS = 0x1F0FFF
|
||||
THREAD_SUSPEND_RESUME = 0x0002
|
||||
TH32CS_SNAPTHREAD = 0x00000004
|
||||
|
||||
class THREADENTRY32(ctypes.Structure):
|
||||
_fields_ = [
|
||||
('dwSize', ctypes.c_ulong),
|
||||
('cntUsage', ctypes.c_ulong),
|
||||
('th32ThreadID', ctypes.c_ulong),
|
||||
('th32OwnerProcessID', ctypes.c_ulong),
|
||||
('tpBasePri', ctypes.c_ulong),
|
||||
('tpDeltaPri', ctypes.c_ulong),
|
||||
('dwFlags', ctypes.c_ulong)
|
||||
]
|
||||
|
||||
# 下载线程类
|
||||
class DownloadThread(QThread):
|
||||
progress = Signal(dict)
|
||||
finished = Signal(bool, str)
|
||||
|
||||
def __init__(self, url, _7z_path, game_version, parent=None):
|
||||
super().__init__(parent)
|
||||
self.url = url
|
||||
self._7z_path = _7z_path
|
||||
self.game_version = game_version
|
||||
self.process = None
|
||||
self._is_running = True
|
||||
self._is_paused = False
|
||||
self.threads = []
|
||||
|
||||
def stop(self):
|
||||
if self.process and self.process.poll() is None:
|
||||
self._is_running = False
|
||||
try:
|
||||
# 使用 taskkill 强制终止进程及其子进程,并隐藏窗口
|
||||
subprocess.run(['taskkill', '/F', '/T', '/PID', str(self.process.pid)], check=True, creationflags=subprocess.CREATE_NO_WINDOW)
|
||||
except (subprocess.CalledProcessError, FileNotFoundError) as e:
|
||||
print(f"停止下载进程时出错: {e}")
|
||||
|
||||
def _get_process_threads(self, pid):
|
||||
"""获取进程的所有线程ID"""
|
||||
if sys.platform != 'win32':
|
||||
return []
|
||||
|
||||
thread_ids = []
|
||||
h_snapshot = kernel32.CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0)
|
||||
if h_snapshot == -1:
|
||||
return []
|
||||
|
||||
thread_entry = THREADENTRY32()
|
||||
thread_entry.dwSize = ctypes.sizeof(THREADENTRY32)
|
||||
|
||||
res = kernel32.Thread32First(h_snapshot, ctypes.byref(thread_entry))
|
||||
while res:
|
||||
if thread_entry.th32OwnerProcessID == pid:
|
||||
thread_ids.append(thread_entry.th32ThreadID)
|
||||
res = kernel32.Thread32Next(h_snapshot, ctypes.byref(thread_entry))
|
||||
|
||||
kernel32.CloseHandle(h_snapshot)
|
||||
return thread_ids
|
||||
|
||||
def pause(self):
|
||||
"""暂停下载进程"""
|
||||
if not self._is_paused and self.process and self.process.poll() is None:
|
||||
try:
|
||||
if sys.platform == 'win32':
|
||||
# 获取所有线程
|
||||
self.threads = self._get_process_threads(self.process.pid)
|
||||
if not self.threads:
|
||||
print("未找到可暂停的线程")
|
||||
return False
|
||||
|
||||
# 暂停所有线程
|
||||
for thread_id in self.threads:
|
||||
h_thread = kernel32.OpenThread(THREAD_SUSPEND_RESUME, False, thread_id)
|
||||
if h_thread:
|
||||
kernel32.SuspendThread(h_thread)
|
||||
kernel32.CloseHandle(h_thread)
|
||||
|
||||
self._is_paused = True
|
||||
print(f"下载进程已暂停: PID {self.process.pid}, 线程数: {len(self.threads)}")
|
||||
return True
|
||||
else:
|
||||
# 在Unix系统上使用SIGSTOP
|
||||
os.kill(self.process.pid, signal.SIGSTOP)
|
||||
self._is_paused = True
|
||||
print(f"下载进程已暂停: PID {self.process.pid}")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"暂停下载进程时出错: {e}")
|
||||
return False
|
||||
return False
|
||||
|
||||
def resume(self):
|
||||
"""恢复下载进程"""
|
||||
if self._is_paused and self.process and self.process.poll() is None:
|
||||
try:
|
||||
if sys.platform == 'win32':
|
||||
# 恢复所有线程
|
||||
for thread_id in self.threads:
|
||||
h_thread = kernel32.OpenThread(THREAD_SUSPEND_RESUME, False, thread_id)
|
||||
if h_thread:
|
||||
kernel32.ResumeThread(h_thread)
|
||||
kernel32.CloseHandle(h_thread)
|
||||
|
||||
self._is_paused = False
|
||||
print(f"下载进程已恢复: PID {self.process.pid}, 线程数: {len(self.threads)}")
|
||||
return True
|
||||
else:
|
||||
# 在Unix系统上使用SIGCONT
|
||||
os.kill(self.process.pid, signal.SIGCONT)
|
||||
self._is_paused = False
|
||||
print(f"下载进程已恢复: PID {self.process.pid}")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"恢复下载进程时出错: {e}")
|
||||
return False
|
||||
return False
|
||||
|
||||
def is_paused(self):
|
||||
"""返回当前下载是否处于暂停状态"""
|
||||
return self._is_paused
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
if not self._is_running:
|
||||
self.finished.emit(False, "下载已手动停止。")
|
||||
return
|
||||
|
||||
aria2c_path = resource_path("aria2c-fast_x64.exe")
|
||||
download_dir = os.path.dirname(self._7z_path)
|
||||
file_name = os.path.basename(self._7z_path)
|
||||
|
||||
parsed_url = urlparse(self.url)
|
||||
referer = f"{parsed_url.scheme}://{parsed_url.netloc}/"
|
||||
|
||||
command = [
|
||||
aria2c_path,
|
||||
]
|
||||
|
||||
# 获取主窗口的下载管理器对象
|
||||
thread_count = 64 # 默认值
|
||||
if hasattr(self.parent(), 'download_manager'):
|
||||
# 从下载管理器获取线程数设置
|
||||
thread_count = self.parent().download_manager.get_download_thread_count()
|
||||
|
||||
# 检查是否启用IPv6支持
|
||||
ipv6_enabled = False
|
||||
if hasattr(self.parent(), 'config'):
|
||||
ipv6_enabled = self.parent().config.get("ipv6_enabled", False)
|
||||
|
||||
# 打印IPv6状态
|
||||
print(f"IPv6支持状态: {ipv6_enabled}")
|
||||
|
||||
# 将所有的优化参数应用于每个下载任务
|
||||
command.extend([
|
||||
'--dir', download_dir,
|
||||
'--out', file_name,
|
||||
'--user-agent', UA,
|
||||
'--referer', referer,
|
||||
'--header', f'Origin: {referer.rstrip("/")}',
|
||||
'--header', 'Accept: */*',
|
||||
'--header', 'Accept-Language: zh-CN,zh;q=0.9,en;q=0.8',
|
||||
'--header', 'Accept-Encoding: gzip, deflate, br',
|
||||
'--header', 'Cache-Control: no-cache',
|
||||
'--header', 'Pragma: no-cache',
|
||||
'--header', 'DNT: 1',
|
||||
'--header', 'Sec-Fetch-Dest: empty',
|
||||
'--header', 'Sec-Fetch-Mode: cors',
|
||||
'--header', 'Sec-Fetch-Site: same-origin',
|
||||
'--http-accept-gzip=true',
|
||||
'--console-log-level=notice',
|
||||
'--summary-interval=1',
|
||||
'--log-level=notice',
|
||||
'--max-tries=3',
|
||||
'--retry-wait=2',
|
||||
'--connect-timeout=60',
|
||||
'--timeout=60',
|
||||
'--auto-file-renaming=false',
|
||||
'--allow-overwrite=true',
|
||||
'--split=128',
|
||||
f'--max-connection-per-server={thread_count}', # 使用动态的线程数
|
||||
'--min-split-size=1M', # 减小最小分片大小
|
||||
'--optimize-concurrent-downloads=true', # 优化并发下载
|
||||
'--file-allocation=none', # 禁用文件预分配加快开始
|
||||
'--async-dns=true', # 使用异步DNS
|
||||
])
|
||||
|
||||
# 根据IPv6设置决定是否禁用IPv6
|
||||
if not ipv6_enabled:
|
||||
command.append('--disable-ipv6=true')
|
||||
print("已禁用IPv6支持")
|
||||
else:
|
||||
print("已启用IPv6支持")
|
||||
|
||||
# 证书验证现在总是需要,因为我们依赖hosts文件
|
||||
command.append('--check-certificate=false')
|
||||
|
||||
command.append(self.url)
|
||||
|
||||
# 打印将要执行的命令,用于调试
|
||||
print(f"即将执行的 Aria2c 命令: {' '.join(command)}")
|
||||
|
||||
creation_flags = subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0
|
||||
self.process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, encoding='utf-8', errors='replace', creationflags=creation_flags)
|
||||
|
||||
# 正则表达式用于解析aria2c的输出
|
||||
# 例如: #1 GID[...]( 5%) CN:1 DL:10.5MiB/s ETA:1m30s
|
||||
progress_pattern = re.compile(r'\((\d{1,3})%\).*?CN:(\d+).*?DL:\s*([^\s]+).*?ETA:\s*([^\s\]]+)')
|
||||
|
||||
# 添加限流计时器,防止更新过于频繁导致UI卡顿
|
||||
last_update_time = 0
|
||||
update_interval = 0.2 # 限制UI更新频率,每0.2秒最多更新一次
|
||||
|
||||
full_output = []
|
||||
while self._is_running and self.process.poll() is None:
|
||||
if self.process.stdout:
|
||||
line = self.process.stdout.readline()
|
||||
if not line:
|
||||
break
|
||||
else:
|
||||
break
|
||||
|
||||
full_output.append(line)
|
||||
print(line.strip()) # 在控制台输出实时日志
|
||||
|
||||
match = progress_pattern.search(line)
|
||||
if match:
|
||||
# 检查是否达到更新间隔
|
||||
current_time = time.time()
|
||||
if current_time - last_update_time >= update_interval:
|
||||
percent = int(match.group(1))
|
||||
threads = match.group(2)
|
||||
speed = match.group(3)
|
||||
eta = match.group(4)
|
||||
|
||||
# 直接发送进度信号,不使用invokeMethod
|
||||
self.progress.emit({
|
||||
"game": self.game_version,
|
||||
"percent": percent,
|
||||
"threads": threads,
|
||||
"speed": speed,
|
||||
"eta": eta
|
||||
})
|
||||
|
||||
last_update_time = current_time
|
||||
|
||||
return_code = self.process.wait()
|
||||
|
||||
if not self._is_running: # 如果是手动停止的
|
||||
self.finished.emit(False, "下载已手动停止。")
|
||||
return
|
||||
|
||||
if return_code == 0:
|
||||
self.progress.emit({
|
||||
"game": self.game_version,
|
||||
"percent": 100,
|
||||
"threads": "N/A",
|
||||
"speed": "N/A",
|
||||
"eta": "完成"
|
||||
})
|
||||
self.finished.emit(True, "")
|
||||
else:
|
||||
error_message = f"\nAria2c下载失败,退出码: {return_code}\n\n--- Aria2c 输出 ---\n{''.join(full_output)}\n---------------------\n"
|
||||
self.finished.emit(False, error_message)
|
||||
|
||||
except Exception as e:
|
||||
if self._is_running:
|
||||
self.finished.emit(False, f"\n下载时发生未知错误\n\n【错误信息】: {e}\n")
|
||||
|
||||
# 下载进度窗口类
|
||||
class ProgressWindow(QDialog):
|
||||
def __init__(self, parent=None):
|
||||
super(ProgressWindow, self).__init__(parent)
|
||||
self.setWindowTitle(f"下载进度 - {APP_NAME}")
|
||||
self.resize(450, 180)
|
||||
self.setWindowFlags(self.windowFlags() & ~Qt.WindowType.WindowCloseButtonHint)
|
||||
self.setWindowFlags(self.windowFlags() & ~Qt.WindowType.WindowSystemMenuHint)
|
||||
|
||||
layout = QVBoxLayout()
|
||||
self.game_label = QLabel("正在启动下载,请稍后...")
|
||||
self.progress_bar = QProgressBar()
|
||||
self.progress_bar.setValue(0)
|
||||
self.stats_label = QLabel("速度: - | 线程: - | 剩余时间: -")
|
||||
|
||||
# 创建按钮布局
|
||||
button_layout = QHBoxLayout()
|
||||
|
||||
# 创建暂停/恢复按钮
|
||||
self.pause_resume_button = QtWidgets.QPushButton("暂停下载")
|
||||
self.pause_resume_button.setToolTip("暂停或恢复下载")
|
||||
|
||||
# 创建停止按钮
|
||||
self.stop_button = QtWidgets.QPushButton("取消下载")
|
||||
self.stop_button.setToolTip("取消整个下载过程")
|
||||
|
||||
# 添加按钮到按钮布局
|
||||
button_layout.addWidget(self.pause_resume_button)
|
||||
button_layout.addWidget(self.stop_button)
|
||||
|
||||
layout.addWidget(self.game_label)
|
||||
layout.addWidget(self.progress_bar)
|
||||
layout.addWidget(self.stats_label)
|
||||
layout.addLayout(button_layout)
|
||||
self.setLayout(layout)
|
||||
|
||||
# 设置暂停/恢复状态
|
||||
self.is_paused = False
|
||||
|
||||
# 添加最后进度记录,用于优化UI更新
|
||||
self._last_percent = -1
|
||||
|
||||
def update_pause_button_state(self, is_paused):
|
||||
"""更新暂停按钮的显示状态
|
||||
|
||||
Args:
|
||||
is_paused: 是否处于暂停状态
|
||||
"""
|
||||
self.is_paused = is_paused
|
||||
if is_paused:
|
||||
self.pause_resume_button.setText("恢复下载")
|
||||
else:
|
||||
self.pause_resume_button.setText("暂停下载")
|
||||
|
||||
def update_progress(self, data):
|
||||
game_version = data.get("game", "未知游戏")
|
||||
percent = data.get("percent", 0)
|
||||
speed = data.get("speed", "-")
|
||||
threads = data.get("threads", "-")
|
||||
eta = data.get("eta", "-")
|
||||
|
||||
# 清除ETA值中可能存在的"]"符号
|
||||
if isinstance(eta, str):
|
||||
eta = eta.replace("]", "")
|
||||
|
||||
# 优化UI更新
|
||||
if hasattr(self, '_last_percent') and self._last_percent == percent and percent < 100:
|
||||
# 如果百分比没变,只更新速度和ETA信息
|
||||
self.stats_label.setText(f"速度: {speed} | 线程: {threads} | 剩余时间: {eta}")
|
||||
else:
|
||||
# 百分比变化或初次更新,更新所有信息
|
||||
self._last_percent = percent
|
||||
self.game_label.setText(f"正在下载 {game_version} 的补丁")
|
||||
self.progress_bar.setValue(int(percent))
|
||||
self.stats_label.setText(f"速度: {speed} | 线程: {threads} | 剩余时间: {eta}")
|
||||
|
||||
if percent == 100:
|
||||
self.pause_resume_button.setEnabled(False)
|
||||
self.stop_button.setEnabled(False)
|
||||
self.stop_button.setText("下载完成")
|
||||
QTimer.singleShot(1500, self.accept)
|
||||
|
||||
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)
|
||||
337
source/workers/ip_optimizer.py
Normal file
@@ -0,0 +1,337 @@
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from PySide6.QtCore import QThread, Signal
|
||||
from utils import resource_path
|
||||
|
||||
class IpOptimizer:
|
||||
def __init__(self):
|
||||
self.process = None
|
||||
|
||||
def get_optimal_ip(self, url: str) -> str | None:
|
||||
"""
|
||||
使用 CloudflareSpeedTest 工具获取给定 URL 的最优 Cloudflare IP。
|
||||
|
||||
Args:
|
||||
url: 需要进行优选的下载链接。
|
||||
|
||||
Returns:
|
||||
最优的 IP 地址字符串,如果找不到则返回 None。
|
||||
"""
|
||||
try:
|
||||
cst_path = resource_path("cfst.exe")
|
||||
if not os.path.exists(cst_path):
|
||||
print(f"错误: cfst.exe 未在资源路径中找到。")
|
||||
return None
|
||||
|
||||
ip_txt_path = resource_path("ip.txt")
|
||||
|
||||
# 正确的参数设置,根据cfst帮助文档
|
||||
command = [
|
||||
cst_path,
|
||||
"-n", "1000", # 延迟测速线程数 (默认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
|
||||
|
||||
print("--- CloudflareSpeedTest 开始执行 ---")
|
||||
|
||||
self.process = subprocess.Popen(
|
||||
command,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
encoding='utf-8',
|
||||
errors='replace',
|
||||
creationflags=creation_flags,
|
||||
bufsize=0
|
||||
)
|
||||
|
||||
# 更新正则表达式以匹配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:
|
||||
print("错误: 无法获取子进程的输出流。")
|
||||
return None
|
||||
|
||||
optimal_ip = None
|
||||
timeout_counter = 0
|
||||
max_timeout = 300 # 增加超时时间到5分钟
|
||||
|
||||
while True:
|
||||
if self.process.poll() is not None:
|
||||
break
|
||||
try:
|
||||
ready = True
|
||||
try:
|
||||
line = stdout.readline()
|
||||
except:
|
||||
ready = False
|
||||
|
||||
if not ready or not line:
|
||||
timeout_counter += 1
|
||||
if timeout_counter > max_timeout:
|
||||
print("超时: CloudflareSpeedTest 响应超时")
|
||||
break
|
||||
time.sleep(1)
|
||||
continue
|
||||
|
||||
timeout_counter = 0
|
||||
|
||||
cleaned_line = line.strip()
|
||||
if cleaned_line:
|
||||
print(cleaned_line)
|
||||
|
||||
# 检测结果表头
|
||||
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 执行结束 ---")
|
||||
return optimal_ip
|
||||
|
||||
except Exception as e:
|
||||
print(f"执行 CloudflareSpeedTest 时发生错误: {e}")
|
||||
return None
|
||||
|
||||
def get_optimal_ipv6(self, url: str) -> str | None:
|
||||
"""
|
||||
使用 CloudflareSpeedTest 工具获取给定 URL 的最优 Cloudflare IPv6 地址。
|
||||
|
||||
Args:
|
||||
url: 需要进行优选的下载链接。
|
||||
|
||||
Returns:
|
||||
最优的 IPv6 地址字符串,如果找不到则返回 None。
|
||||
"""
|
||||
try:
|
||||
cst_path = resource_path("cfst.exe")
|
||||
if not os.path.exists(cst_path):
|
||||
print(f"错误: cfst.exe 未在资源路径中找到。")
|
||||
return None
|
||||
|
||||
ipv6_txt_path = resource_path("data/ipv6.txt")
|
||||
if not os.path.exists(ipv6_txt_path):
|
||||
print(f"错误: ipv6.txt 未在资源路径中找到。")
|
||||
return None
|
||||
|
||||
# 正确的参数设置,根据cfst帮助文档
|
||||
command = [
|
||||
cst_path,
|
||||
"-n", "1000", # 延迟测速线程数,IPv6测试线程稍少
|
||||
"-p", "1", # 显示结果数量 (默认10个)
|
||||
"-url", url, # 指定测速地址
|
||||
"-f", ipv6_txt_path, # IPv6文件
|
||||
"-dd", # 禁用下载测速,按延迟排序
|
||||
"-o", " " # 不写入结果文件
|
||||
]
|
||||
|
||||
creation_flags = subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0
|
||||
|
||||
print("--- CloudflareSpeedTest IPv6 开始执行 ---")
|
||||
|
||||
self.process = subprocess.Popen(
|
||||
command,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
encoding='utf-8',
|
||||
errors='replace',
|
||||
creationflags=creation_flags,
|
||||
bufsize=0
|
||||
)
|
||||
|
||||
# 更新正则表达式以匹配cfst输出中的IPv6格式
|
||||
# IPv6格式更加复杂,可能有多种表示形式
|
||||
ipv6_pattern = re.compile(r'^([0-9a-fA-F:]+)\s+.*')
|
||||
|
||||
# 标记是否已经找到结果表头和完成标记
|
||||
found_header = False
|
||||
found_completion = False
|
||||
|
||||
stdout = self.process.stdout
|
||||
if not stdout:
|
||||
print("错误: 无法获取子进程的输出流。")
|
||||
return None
|
||||
|
||||
optimal_ipv6 = None
|
||||
timeout_counter = 0
|
||||
max_timeout = 300 # 增加超时时间到5分钟
|
||||
|
||||
while True:
|
||||
if self.process.poll() is not None:
|
||||
break
|
||||
try:
|
||||
ready = True
|
||||
try:
|
||||
line = stdout.readline()
|
||||
except:
|
||||
ready = False
|
||||
|
||||
if not ready or not line:
|
||||
timeout_counter += 1
|
||||
if timeout_counter > max_timeout:
|
||||
print("超时: CloudflareSpeedTest IPv6 响应超时")
|
||||
break
|
||||
time.sleep(1)
|
||||
continue
|
||||
|
||||
timeout_counter = 0
|
||||
|
||||
cleaned_line = line.strip()
|
||||
if cleaned_line:
|
||||
print(cleaned_line)
|
||||
|
||||
# 检测结果表头
|
||||
if "IP 地址" in cleaned_line and "平均延迟" in cleaned_line:
|
||||
print("检测到IPv6结果表头,准备获取IPv6地址...")
|
||||
found_header = True
|
||||
continue
|
||||
|
||||
# 检测完成标记
|
||||
if "完整测速结果已写入" in cleaned_line or "按下 回车键 或 Ctrl+C 退出" in cleaned_line:
|
||||
print("检测到IPv6测速完成信息")
|
||||
found_completion = True
|
||||
|
||||
# 如果已经找到了IPv6,可以退出了
|
||||
if optimal_ipv6:
|
||||
break
|
||||
|
||||
# 已找到表头后,尝试匹配IPv6地址行
|
||||
if found_header:
|
||||
match = ipv6_pattern.search(cleaned_line)
|
||||
if match and not optimal_ipv6: # 只保存第一个匹配的IPv6(最优IPv6)
|
||||
optimal_ipv6 = match.group(1)
|
||||
print(f"找到最优 IPv6: {optimal_ipv6}")
|
||||
# 找到最优IPv6后立即退出循环,不等待完成标记
|
||||
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 IPv6 执行结束 ---")
|
||||
return optimal_ipv6
|
||||
|
||||
except Exception as e:
|
||||
print(f"执行 CloudflareSpeedTest IPv6 时发生错误: {e}")
|
||||
return None
|
||||
|
||||
def stop(self):
|
||||
if self.process and self.process.poll() is None:
|
||||
print("正在终止 CloudflareSpeedTest 进程...")
|
||||
try:
|
||||
if self.process.stdin and not self.process.stdin.closed:
|
||||
self.process.stdin.write('\n')
|
||||
self.process.stdin.flush()
|
||||
self.process.stdin.close()
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
self.process.terminate()
|
||||
self.process.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
self.process.kill()
|
||||
self.process.wait()
|
||||
print("CloudflareSpeedTest 进程已终止。")
|
||||
|
||||
|
||||
class IpOptimizerThread(QThread):
|
||||
"""用于在后台线程中运行IP优化的类
|
||||
|
||||
注意:IPv6连接测试功能已迁移至IPv6Manager类,
|
||||
本类仅负责IP优化相关功能
|
||||
"""
|
||||
finished = Signal(str)
|
||||
|
||||
def __init__(self, url, parent=None, use_ipv6=False):
|
||||
super().__init__(parent)
|
||||
self.url = url
|
||||
self.optimizer = IpOptimizer()
|
||||
self.use_ipv6 = use_ipv6
|
||||
|
||||
def run(self):
|
||||
if self.use_ipv6:
|
||||
optimal_ip = self.optimizer.get_optimal_ipv6(self.url)
|
||||
else:
|
||||
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"
|
||||
optimizer = IpOptimizer()
|
||||
ip = optimizer.get_optimal_ip(test_url)
|
||||
if ip:
|
||||
print(f"为 {test_url} 找到的最优 IP 是: {ip}")
|
||||
else:
|
||||
print(f"未能为 {test_url} 找到最优 IP。")
|
||||