mirror of
https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT.git
synced 2025-12-25 16:26:46 +00:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19da86c808 | ||
|
|
96d20c6a5b | ||
|
|
0d33d5610a | ||
|
|
1c749079a2 | ||
|
|
291c471b9e | ||
|
|
6399382242 | ||
|
|
3fc74555cb | ||
|
|
5c06802f65 | ||
|
|
a93991ca9d | ||
|
|
c5b9f1746a |
@@ -34,7 +34,7 @@
|
||||
|
||||
> **2. Patches will not be installed for games you do not own ❗**
|
||||
|
||||
> **3. This tool is only for installing patches, not for installing games ❗ It only runs on Windows x64 systems ❗**
|
||||
> **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 ❗**
|
||||
|
||||
2
FAQ.md
2
FAQ.md
@@ -31,7 +31,7 @@
|
||||
|
||||
> **2. 尚未拥有的游戏将不会进行补丁安装 ❗**
|
||||
|
||||
> **3. 本工具仅适用于补丁安装,不适用于安装游戏 ❗ 且仅限于在 Windows x64 系统上运行 ❗**
|
||||
> **3. 本工具仅适用于补丁安装,不适用于安装游戏 ❗ 且仅限于在 Windows 10/11 64位系统上运行(其他平台或版本未经测试)❗**
|
||||
|
||||
> **4. 工具需要使用管理员权限运行 ❗
|
||||
> 原因:为了防止用户在没有关闭正在运行的游戏而影响本工具的安装效果,启用应用前会获取游戏进程信息从而关闭游戏 ❗**
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
### 2.2 网络相关信息
|
||||
- **IP 地址、ISP 及地理位置**: 应用启动时,为获取云端配置,您的 IP 地址会被服务器记录。服务器可能会根据您的 IP 地址推断您的互联网服务提供商(ISP)和地理位置,这些信息仅用于用户数量、区域分布的统计和软件使用情况分析。当您使用 Cloudflare 加速功能时,您的 IP 地址也会被用于节点优选。
|
||||
- **下载统计信息**:用于监控下载进度和速度
|
||||
- **IPv6 连接测试**:应用会访问 testipv6.cn(test-ipv6.com 的中国大陆镜像网站)以判断软件是否支持 IPv6 连接
|
||||
- **IPv6 地址获取**:应用在测试 IPv6 功能时会请求 ipw.cn 获取您的公网 IPv6 地址,仅用于显示和连接测试目的
|
||||
|
||||
### 2.3 文件信息
|
||||
- 游戏安装路径:用于识别已安装的游戏和安装补丁
|
||||
@@ -86,4 +88,4 @@
|
||||
|
||||
本隐私政策可能会根据应用功能的变化而更新。请定期查看最新版本。
|
||||
|
||||
最后更新日期:2025年7月31日
|
||||
最后更新日期:2025年8月4日
|
||||
@@ -75,6 +75,7 @@ This project uses Git for version control. You can view the currently available
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
@@ -87,6 +88,7 @@ This project uses Git for version control. You can view the currently available
|
||||
- [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
|
||||
|
||||
|
||||
@@ -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):对于原项目部分源码的重构、逻辑优化和功能实现提供了支持。
|
||||
- [钨鸮](https://github.com/ABSIDIA):对于云端资源存储提供了支持。
|
||||
- [XIU2/CloudflareSpeedTest](https://github.com/XIU2/CloudflareSpeedTest):为本项目提供了 IP 优选功能的核心支持。
|
||||
- [hosxy/aria2-fast](https://github.com/hosxy/aria2-fast):提供了修改版aria2c,提高了下载速度和性能。
|
||||
|
||||
## 📖 协议
|
||||
|
||||
|
||||
Binary file not shown.
@@ -7,6 +7,9 @@ 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',
|
||||
@@ -17,5 +20,8 @@ __all__ = [
|
||||
'GameDetector',
|
||||
'PatchManager',
|
||||
'ConfigManager',
|
||||
'PrivacyManager'
|
||||
'PrivacyManager',
|
||||
'CloudflareOptimizer',
|
||||
'DownloadTaskManager',
|
||||
'ExtractionHandler'
|
||||
]
|
||||
@@ -185,6 +185,11 @@ class MultiStageAnimations(QObject):
|
||||
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动画序列"""
|
||||
@@ -337,6 +342,12 @@ class MultiStageAnimations(QObject):
|
||||
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):
|
||||
|
||||
401
source/core/cloudflare_optimizer.py
Normal file
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
|
||||
@@ -7,11 +7,14 @@ import re # Added for recursive search
|
||||
|
||||
from PySide6 import QtWidgets, QtCore
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtGui import QIcon, QPixmap
|
||||
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
|
||||
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):
|
||||
@@ -21,14 +24,20 @@ class DownloadManager:
|
||||
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.optimized_ip = None
|
||||
self.optimization_done = False # 标记是否已执行过优选
|
||||
self.optimizing_msg_box = None
|
||||
|
||||
|
||||
# 添加下载线程级别
|
||||
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(
|
||||
@@ -65,12 +74,6 @@ class DownloadManager:
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 使用识别到的游戏目录 {game}: {game_dir}")
|
||||
print(f"DEBUG: 安装路径设置为: {install_path}")
|
||||
else:
|
||||
# 回退到原始路径计算方式
|
||||
install_path = os.path.join(self.selected_folder, info["install_path"])
|
||||
install_paths[game] = install_path
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 未识别到游戏目录 {game}, 使用默认路径: {install_path}")
|
||||
|
||||
return install_paths
|
||||
|
||||
@@ -264,8 +267,21 @@ class DownloadManager:
|
||||
|
||||
layout = QVBoxLayout(dialog)
|
||||
|
||||
# 添加说明标签
|
||||
info_label = QLabel(f"请选择要安装补丁的游戏版本:\n{status_message}", 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)
|
||||
|
||||
# 添加列表控件
|
||||
@@ -427,208 +443,28 @@ class DownloadManager:
|
||||
|
||||
use_optimization = clicked_button == yes_button
|
||||
|
||||
if use_optimization and not self.optimization_done:
|
||||
if use_optimization and not self.cloudflare_optimizer.is_optimization_done():
|
||||
first_url = self.download_queue[0][0]
|
||||
self._start_ip_optimization(first_url)
|
||||
# 保存当前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 _start_ip_optimization(self, url):
|
||||
"""开始IP优化过程
|
||||
|
||||
Args:
|
||||
url: 用于优化的URL
|
||||
"""
|
||||
# 创建取消状态标记
|
||||
self.optimization_cancelled = False
|
||||
|
||||
# 使用Cloudflare图标创建消息框
|
||||
self.optimizing_msg_box = msgbox_frame(
|
||||
f"通知 - {APP_NAME}",
|
||||
"\n正在优选Cloudflare IP,请稍候...\n\n这可能需要5-10分钟,请耐心等待喵~"
|
||||
)
|
||||
# 设置Cloudflare图标
|
||||
cf_icon_path = resource_path("IMG/ICO/cloudflare_logo_icon.ico")
|
||||
if os.path.exists(cf_icon_path):
|
||||
cf_pixmap = QPixmap(cf_icon_path)
|
||||
if not cf_pixmap.isNull():
|
||||
self.optimizing_msg_box.setWindowIcon(QIcon(cf_pixmap))
|
||||
self.optimizing_msg_box.setIconPixmap(cf_pixmap.scaled(64, 64, Qt.AspectRatioMode.KeepAspectRatio,
|
||||
Qt.TransformationMode.SmoothTransformation))
|
||||
|
||||
# 添加取消按钮
|
||||
self.optimizing_msg_box.setStandardButtons(QtWidgets.QMessageBox.StandardButton.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_optimization_finished)
|
||||
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()
|
||||
|
||||
# 恢复主窗口状态
|
||||
self.main_window.setEnabled(True)
|
||||
self.main_window.ui.start_install_text.setText("开始安装")
|
||||
|
||||
# 清空下载队列
|
||||
self.download_queue.clear()
|
||||
|
||||
# 显示取消消息
|
||||
QtWidgets.QMessageBox.information(
|
||||
self.main_window,
|
||||
f"已取消 - {APP_NAME}",
|
||||
"\n已取消IP优选和安装过程。\n"
|
||||
)
|
||||
|
||||
def on_optimization_finished(self, ip):
|
||||
"""IP优化完成后的处理
|
||||
|
||||
Args:
|
||||
ip: 优选的IP地址,如果失败则为空字符串
|
||||
"""
|
||||
# 如果已经取消,则不继续处理
|
||||
if hasattr(self, 'optimization_cancelled') and self.optimization_cancelled:
|
||||
return
|
||||
|
||||
self.optimized_ip = ip
|
||||
self.optimization_done = True
|
||||
|
||||
# 关闭提示框
|
||||
if hasattr(self, 'optimizing_msg_box') and self.optimizing_msg_box:
|
||||
if self.optimizing_msg_box.isVisible():
|
||||
self.optimizing_msg_box.accept()
|
||||
self.optimizing_msg_box = None
|
||||
|
||||
# 显示优选结果
|
||||
if not ip:
|
||||
# 临时启用窗口以显示对话框
|
||||
self.main_window.setEnabled(True)
|
||||
|
||||
msg_box = QtWidgets.QMessageBox(self.main_window)
|
||||
msg_box.setWindowTitle(f"优选失败 - {APP_NAME}")
|
||||
msg_box.setText("\n未能找到合适的Cloudflare IP,将使用默认网络进行下载。\n\n10秒后自动继续...")
|
||||
msg_box.setIcon(QtWidgets.QMessageBox.Icon.Warning)
|
||||
ok_button = msg_box.addButton("确定 (10)", QtWidgets.QMessageBox.ButtonRole.AcceptRole)
|
||||
cancel_button = msg_box.addButton("取消安装", QtWidgets.QMessageBox.ButtonRole.RejectRole)
|
||||
|
||||
# 创建计时器实现倒计时
|
||||
countdown = 10
|
||||
timer = QtCore.QTimer(self.main_window)
|
||||
|
||||
def update_countdown():
|
||||
nonlocal countdown
|
||||
countdown -= 1
|
||||
ok_button.setText(f"确定 ({countdown})")
|
||||
if countdown <= 0:
|
||||
timer.stop()
|
||||
if msg_box.isVisible():
|
||||
msg_box.accept()
|
||||
|
||||
timer.timeout.connect(update_countdown)
|
||||
timer.start(1000) # 每秒更新一次
|
||||
|
||||
# 显示对话框并等待用户响应
|
||||
result = msg_box.exec()
|
||||
|
||||
# 停止计时器
|
||||
timer.stop()
|
||||
|
||||
# 如果用户点击了取消安装
|
||||
if result == QtWidgets.QMessageBox.StandardButton.RejectRole:
|
||||
# 恢复主窗口状态
|
||||
self.main_window.ui.start_install_text.setText("开始安装")
|
||||
# 清空下载队列
|
||||
self.download_queue.clear()
|
||||
return
|
||||
|
||||
# 用户点击了继续,重新禁用主窗口
|
||||
self.main_window.setEnabled(False)
|
||||
|
||||
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:
|
||||
# 应用优选IP到hosts文件
|
||||
if self.download_queue:
|
||||
first_url = self.download_queue[0][0]
|
||||
hostname = urlparse(first_url).hostname
|
||||
|
||||
# 先清理可能存在的旧记录
|
||||
self.hosts_manager.clean_hostname_entries(hostname)
|
||||
|
||||
# 临时启用窗口以显示对话框
|
||||
self.main_window.setEnabled(True)
|
||||
|
||||
if self.hosts_manager.apply_ip(hostname, ip):
|
||||
msg_box = QtWidgets.QMessageBox(self.main_window)
|
||||
msg_box.setWindowTitle(f"成功 - {APP_NAME}")
|
||||
msg_box.setText(f"\n已将优选IP ({ip}) 应用到hosts文件。\n\n10秒后自动继续...")
|
||||
msg_box.setIcon(QtWidgets.QMessageBox.Icon.Information)
|
||||
ok_button = msg_box.addButton("确定 (10)", QtWidgets.QMessageBox.ButtonRole.AcceptRole)
|
||||
cancel_button = msg_box.addButton("取消安装", QtWidgets.QMessageBox.ButtonRole.RejectRole)
|
||||
|
||||
# 创建计时器实现倒计时
|
||||
countdown = 10
|
||||
timer = QtCore.QTimer(self.main_window)
|
||||
|
||||
def update_countdown():
|
||||
nonlocal countdown
|
||||
countdown -= 1
|
||||
ok_button.setText(f"确定 ({countdown})")
|
||||
if countdown <= 0:
|
||||
timer.stop()
|
||||
if msg_box.isVisible():
|
||||
msg_box.accept()
|
||||
|
||||
timer.timeout.connect(update_countdown)
|
||||
timer.start(1000) # 每秒更新一次
|
||||
|
||||
# 显示对话框并等待用户响应
|
||||
result = msg_box.exec()
|
||||
|
||||
# 停止计时器
|
||||
timer.stop()
|
||||
|
||||
# 如果用户点击了取消安装
|
||||
if result == QtWidgets.QMessageBox.StandardButton.RejectRole:
|
||||
# 恢复主窗口状态
|
||||
self.main_window.setEnabled(True)
|
||||
self.main_window.ui.start_install_text.setText("开始安装")
|
||||
# 清空下载队列
|
||||
self.download_queue.clear()
|
||||
return
|
||||
else:
|
||||
QtWidgets.QMessageBox.critical(
|
||||
self.main_window,
|
||||
f"错误 - {APP_NAME}",
|
||||
"\n修改hosts文件失败,请检查程序是否以管理员权限运行。\n"
|
||||
)
|
||||
# 恢复主窗口状态
|
||||
self.main_window.ui.start_install_text.setText("开始安装")
|
||||
# 清空下载队列并退出
|
||||
self.download_queue.clear()
|
||||
return
|
||||
|
||||
# 用户点击了继续,重新禁用主窗口
|
||||
self.main_window.setEnabled(False)
|
||||
|
||||
# 计时器结束或用户点击确定时,继续下载
|
||||
QtCore.QTimer.singleShot(100, self.next_download_task)
|
||||
|
||||
# 否则,继续等待100ms后再次检查
|
||||
QtCore.QTimer.singleShot(100, self.check_optimization_status)
|
||||
|
||||
def next_download_task(self):
|
||||
"""处理下载队列中的下一个任务"""
|
||||
if not self.download_queue:
|
||||
@@ -636,7 +472,7 @@ class DownloadManager:
|
||||
return
|
||||
|
||||
# 检查下载线程是否仍在运行,以避免在手动停止后立即开始下一个任务
|
||||
if self.current_download_thread and self.current_download_thread.isRunning():
|
||||
if self.download_task_manager.current_download_thread and self.download_task_manager.current_download_thread.isRunning():
|
||||
return
|
||||
|
||||
# 获取下一个下载任务并开始
|
||||
@@ -661,89 +497,9 @@ class DownloadManager:
|
||||
print(f"DEBUG: 准备下载游戏 {game_version}")
|
||||
print(f"DEBUG: 游戏文件夹: {game_folder}")
|
||||
|
||||
# 获取游戏可执行文件路径
|
||||
game_dirs = self.main_window.game_detector.identify_game_directories_improved(self.selected_folder)
|
||||
game_exe_exists = False
|
||||
|
||||
if game_version in game_dirs:
|
||||
game_dir = game_dirs[game_version]
|
||||
# 游戏目录已经通过可执行文件验证了,可以直接认为存在
|
||||
game_exe_exists = True
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 游戏目录已验证: {game_dir}")
|
||||
print(f"DEBUG: 游戏可执行文件存在: {game_exe_exists}")
|
||||
else:
|
||||
# 回退到传统方法检查游戏是否存在
|
||||
# 尝试多种可能的文件名格式
|
||||
expected_exe = GAME_INFO[game_version]["exe"]
|
||||
traditional_folder = os.path.join(
|
||||
self.selected_folder,
|
||||
GAME_INFO[game_version]["install_path"].split("/")[0]
|
||||
)
|
||||
|
||||
# 定义多种可能的可执行文件变体
|
||||
exe_variants = [
|
||||
expected_exe, # 标准文件名
|
||||
expected_exe + ".nocrack", # Steam加密版本
|
||||
expected_exe.replace(".exe", ""), # 无扩展名版本
|
||||
expected_exe.replace("NEKOPARA", "nekopara").lower(), # 全小写变体
|
||||
expected_exe.lower(), # 小写变体
|
||||
expected_exe.lower() + ".nocrack", # 小写变体的Steam加密版本
|
||||
]
|
||||
|
||||
# 对于Vol.3可能有特殊名称
|
||||
if "Vol.3" in game_version:
|
||||
# 增加可能的卷3特定的变体
|
||||
exe_variants.extend([
|
||||
"NEKOPARAVol3.exe",
|
||||
"NEKOPARAVol3.exe.nocrack",
|
||||
"nekoparavol3.exe",
|
||||
"nekoparavol3.exe.nocrack",
|
||||
"nekopara_vol3.exe",
|
||||
"nekopara_vol3.exe.nocrack",
|
||||
"vol3.exe",
|
||||
"vol3.exe.nocrack"
|
||||
])
|
||||
|
||||
# 检查所有可能的文件名
|
||||
for exe_variant in exe_variants:
|
||||
exe_path = os.path.join(traditional_folder, exe_variant)
|
||||
if os.path.exists(exe_path):
|
||||
game_exe_exists = True
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 找到游戏可执行文件: {exe_path}")
|
||||
break
|
||||
|
||||
# 如果仍未找到,尝试递归搜索
|
||||
if not game_exe_exists and os.path.exists(traditional_folder):
|
||||
# 提取卷号或检查是否是After
|
||||
vol_match = re.search(r"Vol\.(\d+)", game_version)
|
||||
vol_num = None
|
||||
if vol_match:
|
||||
vol_num = vol_match.group(1)
|
||||
|
||||
is_after = "After" in game_version
|
||||
|
||||
# 遍历游戏目录及其子目录
|
||||
for root, dirs, files in os.walk(traditional_folder):
|
||||
for file in files:
|
||||
file_lower = file.lower()
|
||||
if file.endswith('.exe') or file.endswith('.exe.nocrack'):
|
||||
# 检查文件名中是否包含卷号或关键词
|
||||
if ((vol_num and (f"vol{vol_num}" in file_lower or
|
||||
f"vol.{vol_num}" in file_lower or
|
||||
f"vol {vol_num}" in file_lower)) or
|
||||
(is_after and "after" in file_lower)):
|
||||
game_exe_exists = True
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 通过递归搜索找到游戏可执行文件: {os.path.join(root, file)}")
|
||||
break
|
||||
if game_exe_exists:
|
||||
break
|
||||
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 使用传统方法检查游戏目录: {traditional_folder}")
|
||||
print(f"DEBUG: 游戏可执行文件存在: {game_exe_exists}")
|
||||
# 游戏可执行文件已在填充下载队列时验证过,不需要再次检查
|
||||
# 因为game_folder是从已验证的game_dirs中获取的
|
||||
game_exe_exists = True
|
||||
|
||||
# 检查游戏是否已安装
|
||||
if (
|
||||
@@ -760,69 +516,18 @@ class DownloadManager:
|
||||
|
||||
# 创建进度窗口并开始下载
|
||||
self.main_window.progress_window = self.main_window.create_progress_window()
|
||||
self.start_download(url, _7z_path, game_version, game_folder, plugin_path)
|
||||
|
||||
def start_download(self, url, _7z_path, game_version, game_folder, plugin_path):
|
||||
"""启动下载线程
|
||||
|
||||
Args:
|
||||
url: 下载URL
|
||||
_7z_path: 7z文件保存路径
|
||||
game_version: 游戏版本名称
|
||||
game_folder: 游戏文件夹路径
|
||||
plugin_path: 插件路径
|
||||
"""
|
||||
# 按钮在file_dialog中已设置为禁用状态
|
||||
|
||||
# 从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,将使用默认线路。")
|
||||
|
||||
# 创建并连接下载线程
|
||||
self.current_download_thread = self.main_window.create_download_thread(url, _7z_path, game_version)
|
||||
self.current_download_thread.progress.connect(self.main_window.progress_window.update_progress)
|
||||
self.current_download_thread.finished.connect(
|
||||
lambda success, error: self.on_download_finished(
|
||||
success,
|
||||
error,
|
||||
url,
|
||||
game_folder,
|
||||
game_version,
|
||||
_7z_path,
|
||||
plugin_path,
|
||||
)
|
||||
)
|
||||
|
||||
# 连接停止按钮到我们的on_download_stopped方法
|
||||
self.main_window.progress_window.stop_button.clicked.connect(self.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)
|
||||
# 使用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):
|
||||
"""下载完成后的处理
|
||||
|
||||
@@ -874,79 +579,31 @@ class DownloadManager:
|
||||
self.on_download_stopped()
|
||||
return
|
||||
|
||||
# 下载成功,开始解压缩
|
||||
self.main_window.hash_msg_box = self.main_window.hash_manager.hash_pop_window(check_type="extraction")
|
||||
|
||||
# 创建并启动解压线程
|
||||
self.main_window.extraction_thread = self.main_window.create_extraction_thread(
|
||||
_7z_path, game_folder, plugin_path, game_version
|
||||
)
|
||||
self.main_window.extraction_thread.finished.connect(self.on_extraction_finished)
|
||||
self.main_window.extraction_thread.start()
|
||||
# 下载成功,使用ExtractionHandler开始解压缩
|
||||
self.extraction_handler.start_extraction(_7z_path, game_folder, plugin_path, game_version)
|
||||
# extraction_handler的回调会处理下一步操作
|
||||
|
||||
def on_extraction_finished(self, success, error_message, game_version):
|
||||
"""解压完成后的处理
|
||||
def on_extraction_finished(self, continue_download):
|
||||
"""解压完成后的回调,决定是否继续下载队列
|
||||
|
||||
Args:
|
||||
success: 是否解压成功
|
||||
error_message: 错误信息
|
||||
game_version: 游戏版本
|
||||
continue_download: 是否继续下载队列中的下一个任务
|
||||
"""
|
||||
# 关闭哈希检查窗口
|
||||
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"错误 - {APP_NAME}", error_message)
|
||||
self.main_window.installed_status[game_version] = False
|
||||
|
||||
# 询问用户是否继续其他游戏的安装
|
||||
reply = QtWidgets.QMessageBox.question(
|
||||
self.main_window,
|
||||
f"继续安装? - {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)
|
||||
self.next_download_task()
|
||||
else:
|
||||
# 用户选择停止,保持窗口启用状态
|
||||
self.main_window.ui.start_install_text.setText("开始安装")
|
||||
# 清空剩余队列
|
||||
self.download_queue.clear()
|
||||
# 显示已完成的安装结果
|
||||
self.main_window.show_result()
|
||||
return
|
||||
if continue_download:
|
||||
# 继续下一个下载任务
|
||||
self.next_download_task()
|
||||
else:
|
||||
self.main_window.installed_status[game_version] = True
|
||||
|
||||
# 继续下一个下载任务
|
||||
self.next_download_task()
|
||||
# 清空剩余队列并显示结果
|
||||
self.download_queue.clear()
|
||||
self.main_window.show_result()
|
||||
|
||||
def on_download_stopped(self):
|
||||
"""当用户点击停止按钮或选择结束时调用的函数"""
|
||||
# 停止IP优化线程
|
||||
if hasattr(self, 'ip_optimizer_thread') and self.ip_optimizer_thread and self.ip_optimizer_thread.isRunning():
|
||||
self.ip_optimizer_thread.stop()
|
||||
self.ip_optimizer_thread.wait()
|
||||
if hasattr(self, 'optimizing_msg_box') and self.optimizing_msg_box:
|
||||
if self.optimizing_msg_box.isVisible():
|
||||
self.optimizing_msg_box.accept()
|
||||
self.optimizing_msg_box = None
|
||||
self.cloudflare_optimizer.stop_optimization()
|
||||
|
||||
# 停止当前可能仍在运行的下载线程
|
||||
if self.current_download_thread and self.current_download_thread.isRunning():
|
||||
self.current_download_thread.stop()
|
||||
self.current_download_thread.wait() # 等待线程完全终止
|
||||
self.download_task_manager.stop_download()
|
||||
|
||||
# 清空下载队列,因为用户决定停止
|
||||
self.download_queue.clear()
|
||||
@@ -957,7 +614,7 @@ class DownloadManager:
|
||||
self.main_window.progress_window.reject()
|
||||
self.main_window.progress_window = None
|
||||
|
||||
# 可以在这里决定是否立即进行哈希比较或显示结果
|
||||
# 退出应用程序
|
||||
print("下载已全部停止。")
|
||||
|
||||
# 恢复主窗口状态
|
||||
@@ -969,4 +626,17 @@ class DownloadManager:
|
||||
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
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
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)
|
||||
345
source/core/ipv6_manager.py
Normal file
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
|
||||
@@ -55,26 +55,29 @@ class PatchManager:
|
||||
return self.installed_status.get(game_version, False)
|
||||
return self.installed_status
|
||||
|
||||
def uninstall_patch(self, game_dir, game_version):
|
||||
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:
|
||||
QMessageBox.critical(
|
||||
None,
|
||||
f"错误 - {self.app_name}",
|
||||
f"\n无法识别游戏版本: {game_version}\n",
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
return False
|
||||
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}")
|
||||
@@ -173,8 +176,8 @@ class PatchManager:
|
||||
# 更新安装状态
|
||||
self.installed_status[game_version] = False
|
||||
|
||||
# 在非批量卸载模式下显示卸载成功消息
|
||||
if game_version != "all":
|
||||
# 在非静默模式且非批量卸载模式下显示卸载成功消息
|
||||
if not silent and game_version != "all":
|
||||
# 显示卸载成功消息
|
||||
if files_removed > 0:
|
||||
QMessageBox.information(
|
||||
@@ -192,11 +195,13 @@ class PatchManager:
|
||||
)
|
||||
|
||||
# 卸载成功
|
||||
if silent:
|
||||
return {"success": True, "message": f"{game_version} 补丁卸载成功", "files_removed": files_removed}
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
# 在非批量卸载模式下显示卸载失败消息
|
||||
if game_version != "all":
|
||||
# 在非静默模式且非批量卸载模式下显示卸载失败消息
|
||||
if not silent and game_version != "all":
|
||||
# 显示卸载失败消息
|
||||
error_message = f"\n卸载 {game_version} 补丁时出错:\n\n{str(e)}\n"
|
||||
if debug_mode:
|
||||
@@ -210,6 +215,8 @@ class PatchManager:
|
||||
)
|
||||
|
||||
# 卸载失败
|
||||
if silent:
|
||||
return {"success": False, "message": f"卸载 {game_version} 补丁时出错: {str(e)}", "files_removed": 0}
|
||||
return False
|
||||
|
||||
def batch_uninstall_patches(self, game_dirs):
|
||||
@@ -219,35 +226,166 @@ class PatchManager:
|
||||
game_dirs: 游戏版本到游戏目录的映射字典
|
||||
|
||||
Returns:
|
||||
tuple: (成功数量, 失败数量)
|
||||
tuple: (成功数量, 失败数量, 详细结果列表)
|
||||
"""
|
||||
success_count = 0
|
||||
fail_count = 0
|
||||
debug_mode = self._is_debug_mode()
|
||||
results = []
|
||||
|
||||
for version, path in game_dirs.items():
|
||||
try:
|
||||
if self.uninstall_patch(path, version):
|
||||
success_count += 1
|
||||
else:
|
||||
fail_count += 1
|
||||
# 在批量模式下使用静默卸载
|
||||
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
|
||||
return success_count, fail_count, results
|
||||
|
||||
def show_uninstall_result(self, success_count, fail_count):
|
||||
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}",
|
||||
f"\n批量卸载完成!\n成功: {success_count} 个\n失败: {fail_count} 个\n",
|
||||
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
|
||||
@@ -6,6 +6,7 @@ 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):
|
||||
@@ -24,6 +25,9 @@ class UIManager:
|
||||
self.about_menu = None # 关于菜单
|
||||
self.about_btn = None # 关于按钮
|
||||
|
||||
# 获取主窗口的IPv6Manager实例
|
||||
self.ipv6_manager = getattr(main_window, 'ipv6_manager', None)
|
||||
|
||||
def setup_ui(self):
|
||||
"""设置UI元素,包括窗口图标、标题和菜单"""
|
||||
# 设置窗口图标
|
||||
@@ -279,6 +283,55 @@ class UIManager:
|
||||
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)
|
||||
@@ -288,9 +341,15 @@ class UIManager:
|
||||
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)
|
||||
@@ -321,20 +380,56 @@ class UIManager:
|
||||
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.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.addAction(self.switch_source_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):
|
||||
"""显示菜单
|
||||
@@ -385,8 +480,8 @@ class UIManager:
|
||||
def revoke_privacy_agreement(self):
|
||||
"""撤回隐私协议同意,并重启软件"""
|
||||
# 创建确认对话框
|
||||
msg_box = msgbox_frame(
|
||||
f"确认操作 - {APP_NAME}",
|
||||
msg_box = self._create_message_box(
|
||||
"确认操作",
|
||||
"\n您确定要撤回隐私协议同意吗?\n\n撤回后软件将立即重启,您需要重新阅读并同意隐私协议。\n",
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||
)
|
||||
@@ -405,10 +500,9 @@ class UIManager:
|
||||
privacy_manager = PrivacyManager()
|
||||
if privacy_manager.reset_privacy_agreement():
|
||||
# 显示重启提示
|
||||
restart_msg = msgbox_frame(
|
||||
f"操作成功 - {APP_NAME}",
|
||||
"\n已成功撤回隐私协议同意。\n\n软件将立即重启。\n",
|
||||
QMessageBox.StandardButton.Ok,
|
||||
restart_msg = self._create_message_box(
|
||||
"操作成功",
|
||||
"\n已成功撤回隐私协议同意。\n\n软件将立即重启。\n"
|
||||
)
|
||||
restart_msg.exec()
|
||||
|
||||
@@ -426,53 +520,62 @@ class UIManager:
|
||||
sys.exit(0)
|
||||
else:
|
||||
# 显示失败提示
|
||||
fail_msg = msgbox_frame(
|
||||
f"操作失败 - {APP_NAME}",
|
||||
"\n撤回隐私协议同意失败。\n\n请检查应用权限或稍后再试。\n",
|
||||
QMessageBox.StandardButton.Ok,
|
||||
fail_msg = self._create_message_box(
|
||||
"操作失败",
|
||||
"\n撤回隐私协议同意失败。\n\n请检查应用权限或稍后再试。\n"
|
||||
)
|
||||
fail_msg.exec()
|
||||
except Exception as e:
|
||||
# 显示错误提示
|
||||
error_msg = msgbox_frame(
|
||||
f"错误 - {APP_NAME}",
|
||||
f"\n撤回隐私协议同意时发生错误:\n\n{str(e)}\n",
|
||||
QMessageBox.StandardButton.Ok,
|
||||
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 = msgbox_frame(
|
||||
f"提示 - {APP_NAME}",
|
||||
"\n该功能正在开发中,敬请期待!\n",
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
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文件"""
|
||||
if os.path.exists(LOG_FILE):
|
||||
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 = msgbox_frame(
|
||||
f"错误 - {APP_NAME}",
|
||||
f"\n打开log.txt文件失败:\n\n{str(e)}\n",
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
msg_box.exec()
|
||||
else:
|
||||
msg_box = msgbox_frame(
|
||||
f"提示 - {APP_NAME}",
|
||||
"\nlog.txt文件不存在,请确保Debug模式已开启并生成日志。\n",
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
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):
|
||||
@@ -483,32 +586,16 @@ class UIManager:
|
||||
result = self.main_window.download_manager.hosts_manager.restore()
|
||||
|
||||
if result:
|
||||
msg_box = msgbox_frame(
|
||||
f"成功 - {APP_NAME}",
|
||||
"\nhosts文件已成功还原为备份版本。\n",
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
msg_box = self._create_message_box("成功", "\nhosts文件已成功还原为备份版本。\n")
|
||||
else:
|
||||
msg_box = msgbox_frame(
|
||||
f"警告 - {APP_NAME}",
|
||||
"\n还原hosts文件失败或没有找到备份文件。\n",
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
msg_box = self._create_message_box("警告", "\n还原hosts文件失败或没有找到备份文件。\n")
|
||||
|
||||
msg_box.exec()
|
||||
except Exception as e:
|
||||
msg_box = msgbox_frame(
|
||||
f"错误 - {APP_NAME}",
|
||||
f"\n还原hosts文件时发生错误:\n\n{str(e)}\n",
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
msg_box = self._create_message_box("错误", f"\n还原hosts文件时发生错误:\n\n{str(e)}\n")
|
||||
msg_box.exec()
|
||||
else:
|
||||
msg_box = msgbox_frame(
|
||||
f"错误 - {APP_NAME}",
|
||||
"\n无法访问hosts管理器。\n",
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
msg_box = self._create_message_box("错误", "\n无法访问hosts管理器。\n")
|
||||
msg_box.exec()
|
||||
|
||||
def clean_hosts_entries(self):
|
||||
@@ -519,32 +606,43 @@ class UIManager:
|
||||
result = self.main_window.download_manager.hosts_manager.check_and_clean_all_entries()
|
||||
|
||||
if result:
|
||||
msg_box = msgbox_frame(
|
||||
f"成功 - {APP_NAME}",
|
||||
"\n已成功清理软件添加的hosts条目。\n",
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
msg_box = self._create_message_box("成功", "\n已成功清理软件添加的hosts条目。\n")
|
||||
else:
|
||||
msg_box = msgbox_frame(
|
||||
f"提示 - {APP_NAME}",
|
||||
"\n未发现软件添加的hosts条目或清理操作失败。\n",
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
msg_box = self._create_message_box("提示", "\n未发现软件添加的hosts条目或清理操作失败。\n")
|
||||
|
||||
msg_box.exec()
|
||||
except Exception as e:
|
||||
msg_box = msgbox_frame(
|
||||
f"错误 - {APP_NAME}",
|
||||
f"\n清理hosts条目时发生错误:\n\n{str(e)}\n",
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
msg_box = self._create_message_box("错误", f"\n清理hosts条目时发生错误:\n\n{str(e)}\n")
|
||||
msg_box.exec()
|
||||
else:
|
||||
msg_box = msgbox_frame(
|
||||
f"错误 - {APP_NAME}",
|
||||
"\n无法访问hosts管理器。\n",
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
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):
|
||||
@@ -559,6 +657,7 @@ class UIManager:
|
||||
<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}",
|
||||
@@ -566,4 +665,9 @@ class UIManager:
|
||||
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()
|
||||
@@ -3,7 +3,7 @@ import base64
|
||||
|
||||
# 配置信息
|
||||
app_data = {
|
||||
"APP_VERSION": "1.2.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"
|
||||
File diff suppressed because one or more lines are too long
@@ -14,7 +14,7 @@ PRIVACY_POLICY_BRIEF = """
|
||||
|
||||
## 收集的信息
|
||||
- **系统信息**:程序版本号。
|
||||
- **网络信息**:IP 地址、ISP、地理位置(用于使用统计)、下载统计。
|
||||
- **网络信息**:IP 地址、ISP、地理位置(用于使用统计)、下载统计、IPv6 连接测试(通过访问 testipv6.cn)、IPv6 地址获取(通过 ipw.cn)。
|
||||
- **文件信息**:游戏安装路径、文件哈希值。
|
||||
|
||||
## 系统修改
|
||||
@@ -24,6 +24,7 @@ PRIVACY_POLICY_BRIEF = """
|
||||
## 第三方服务
|
||||
- **Cloudflare 服务**:通过开源项目 CloudflareSpeedTest (CFST) 提供,用于优化下载速度。此过程会将您的 IP 提交至 Cloudflare 节点。
|
||||
- **云端配置服务**:获取配置信息。服务器会记录您的 IP、ISP 及地理位置用于统计。
|
||||
- **IPv6 测试服务**:应用使用 testipv6.cn 和 ipw.cn 测试和获取 IPv6 连接信息。
|
||||
|
||||
完整的隐私政策可在本程序的 GitHub 仓库中查看。
|
||||
"""
|
||||
@@ -36,7 +37,7 @@ 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.
|
||||
- **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
|
||||
@@ -46,12 +47,13 @@ This application collects and processes the following information:
|
||||
## 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.07.31"
|
||||
PRIVACY_POLICY_VERSION = "2025.08.04"
|
||||
|
||||
def get_local_privacy_policy():
|
||||
"""获取本地打包的隐私协议文件
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import shutil
|
||||
import json
|
||||
import webbrowser
|
||||
@@ -7,13 +8,14 @@ import webbrowser
|
||||
from PySide6 import QtWidgets
|
||||
from PySide6.QtCore import QTimer, Qt, QPoint, QRect, QSize
|
||||
from PySide6.QtWidgets import QMainWindow, QMessageBox, QGraphicsOpacityEffect, QGraphicsColorizeEffect
|
||||
from PySide6.QtGui import QPalette, QColor, QPainterPath, QRegion
|
||||
from PySide6.QtGui import QPalette, QColor, QPainterPath, QRegion, QFont
|
||||
from PySide6.QtGui import QAction # Added for menu actions
|
||||
|
||||
from ui.Ui_install import Ui_MainWindows
|
||||
from data.config import (
|
||||
APP_NAME, PLUGIN, GAME_INFO, BLOCK_SIZE,
|
||||
PLUGIN_HASH, UA, CONFIG_URL, LOG_FILE
|
||||
PLUGIN_HASH, UA, CONFIG_URL, LOG_FILE,
|
||||
DOWNLOAD_THREADS, DEFAULT_DOWNLOAD_THREAD_LEVEL # 添加下载线程常量
|
||||
)
|
||||
from utils import (
|
||||
load_config, save_config, HashManager, AdminPrivileges, msgbox_frame, load_image_from_file
|
||||
@@ -26,6 +28,7 @@ from core import (
|
||||
MultiStageAnimations, UIManager, DownloadManager, DebugManager,
|
||||
WindowManager, GameDetector, PatchManager, ConfigManager
|
||||
)
|
||||
from core.ipv6_manager import IPv6Manager
|
||||
|
||||
class MainWindow(QMainWindow):
|
||||
def __init__(self):
|
||||
@@ -53,25 +56,36 @@ class MainWindow(QMainWindow):
|
||||
self.hash_manager = HashManager(BLOCK_SIZE)
|
||||
self.admin_privileges = AdminPrivileges()
|
||||
|
||||
# 初始化管理器
|
||||
# 初始化各种管理器
|
||||
# 1. 首先创建必要的基础管理器
|
||||
self.animator = MultiStageAnimations(self.ui, self)
|
||||
self.ui_manager = UIManager(self)
|
||||
|
||||
# 首先设置UI - 确保debug_action已初始化
|
||||
self.ui_manager.setup_ui()
|
||||
|
||||
# 初始化新的管理器类
|
||||
self.window_manager = WindowManager(self)
|
||||
self.debug_manager = DebugManager(self)
|
||||
# 为debug_manager设置ui_manager引用
|
||||
|
||||
# 2. 初始化IPv6Manager(应在UIManager之前)
|
||||
self.ipv6_manager = IPv6Manager(self)
|
||||
|
||||
# 3. 创建UIManager(依赖IPv6Manager)
|
||||
self.ui_manager = UIManager(self)
|
||||
|
||||
# 4. 为debug_manager设置ui_manager引用
|
||||
self.debug_manager.set_ui_manager(self.ui_manager)
|
||||
|
||||
# 设置UI - 确保debug_action已初始化
|
||||
self.ui_manager.setup_ui()
|
||||
|
||||
# 5. 初始化其他管理器
|
||||
self.config_manager = ConfigManager(APP_NAME, CONFIG_URL, UA, self.debug_manager)
|
||||
self.game_detector = GameDetector(GAME_INFO, self.debug_manager)
|
||||
self.patch_manager = PatchManager(APP_NAME, GAME_INFO, self.debug_manager)
|
||||
|
||||
# 初始化下载管理器 - 应该放在其他管理器之后,因为它可能依赖于它们
|
||||
# 6. 初始化下载管理器 - 放在最后,因为它可能依赖于其他管理器
|
||||
self.download_manager = DownloadManager(self)
|
||||
|
||||
# 加载用户下载线程设置
|
||||
if "download_thread_level" in self.config and self.config["download_thread_level"] in DOWNLOAD_THREADS:
|
||||
self.download_manager.download_thread_level = self.config["download_thread_level"]
|
||||
|
||||
# 初始化状态变量
|
||||
self.cloud_config = None
|
||||
self.config_valid = False # 添加配置有效标志
|
||||
@@ -159,6 +173,11 @@ class MainWindow(QMainWindow):
|
||||
"""动画完成后启用按钮"""
|
||||
self.animation_in_progress = False
|
||||
|
||||
# 启用所有菜单按钮
|
||||
self.ui.start_install_btn.setEnabled(True)
|
||||
self.ui.uninstall_btn.setEnabled(True)
|
||||
self.ui.exit_btn.setEnabled(True)
|
||||
|
||||
# 只有在配置有效时才启用开始安装按钮
|
||||
if self.config_valid:
|
||||
self.set_start_button_enabled(True)
|
||||
@@ -246,12 +265,13 @@ class MainWindow(QMainWindow):
|
||||
Args:
|
||||
url: 下载URL
|
||||
_7z_path: 7z文件保存路径
|
||||
game_version: 游戏版本
|
||||
game_version: 游戏版本名称
|
||||
|
||||
Returns:
|
||||
DownloadThread: 下载线程实例
|
||||
"""
|
||||
return DownloadThread(url, _7z_path, game_version, self)
|
||||
from workers import DownloadThread
|
||||
return DownloadThread(url, _7z_path, game_version, parent=self)
|
||||
|
||||
def create_progress_window(self):
|
||||
"""创建下载进度窗口
|
||||
@@ -349,49 +369,47 @@ class MainWindow(QMainWindow):
|
||||
install_paths = self.download_manager.get_install_paths() if hasattr(self.download_manager, "get_install_paths") else {}
|
||||
|
||||
for game_version, is_installed in self.installed_status.items():
|
||||
path = install_paths.get(game_version, "")
|
||||
|
||||
# 检查游戏是否存在但未通过本次安装补丁
|
||||
if is_installed:
|
||||
# 游戏已安装补丁
|
||||
if hasattr(self, 'download_queue_history') and game_version not in self.download_queue_history:
|
||||
# 已有补丁,被跳过下载
|
||||
skipped_versions.append(game_version)
|
||||
# 只处理install_paths中存在的游戏版本
|
||||
if game_version in install_paths:
|
||||
path = install_paths[game_version]
|
||||
|
||||
# 检查游戏是否存在但未通过本次安装补丁
|
||||
if is_installed:
|
||||
# 游戏已安装补丁
|
||||
if hasattr(self, 'download_queue_history') and game_version not in self.download_queue_history:
|
||||
# 已有补丁,被跳过下载
|
||||
skipped_versions.append(game_version)
|
||||
else:
|
||||
# 本次成功安装
|
||||
installed_versions.append(game_version)
|
||||
else:
|
||||
# 本次成功安装
|
||||
installed_versions.append(game_version)
|
||||
else:
|
||||
# 游戏未安装补丁
|
||||
if os.path.exists(path):
|
||||
# 游戏文件夹存在,但安装失败
|
||||
failed_versions.append(game_version)
|
||||
else:
|
||||
# 游戏文件夹不存在
|
||||
not_found_versions.append(game_version)
|
||||
# 游戏未安装补丁
|
||||
if os.path.exists(path):
|
||||
# 游戏文件夹存在,但安装失败
|
||||
failed_versions.append(game_version)
|
||||
else:
|
||||
# 游戏文件夹不存在
|
||||
not_found_versions.append(game_version)
|
||||
|
||||
# 构建结果信息
|
||||
result_text = f"\n安装结果:\n"
|
||||
|
||||
# 总数统计
|
||||
# 总数统计 - 不再显示已跳过的数量
|
||||
total_installed = len(installed_versions)
|
||||
total_skipped = len(skipped_versions)
|
||||
total_failed = len(failed_versions)
|
||||
|
||||
result_text += f"安装成功:{total_installed} 个 已跳过:{total_skipped} 个 安装失败:{total_failed} 个\n\n"
|
||||
result_text += f"安装成功:{total_installed} 个 安装失败:{total_failed} 个\n\n"
|
||||
|
||||
# 详细列表
|
||||
if installed_versions:
|
||||
result_text += f"【成功安装】:\n{chr(10).join(installed_versions)}\n\n"
|
||||
|
||||
if skipped_versions:
|
||||
result_text += f"【已安装跳过】:\n{chr(10).join(skipped_versions)}\n\n"
|
||||
|
||||
if failed_versions:
|
||||
result_text += f"【安装失败】:\n{chr(10).join(failed_versions)}\n\n"
|
||||
|
||||
if not_found_versions and (installed_versions or failed_versions):
|
||||
# 只有当有其他版本存在时,才显示未找到的版本
|
||||
result_text += f"【未在指定目录找到】:\n{chr(10).join(not_found_versions)}\n"
|
||||
if not_found_versions:
|
||||
# 只有在真正检测到了游戏但未安装补丁时才显示
|
||||
result_text += f"【尚未安装补丁的游戏】:\n{chr(10).join(not_found_versions)}\n"
|
||||
|
||||
QMessageBox.information(
|
||||
self,
|
||||
@@ -528,47 +546,110 @@ class MainWindow(QMainWindow):
|
||||
game_dirs = self.game_detector.identify_game_directories_improved(selected_folder)
|
||||
|
||||
if game_dirs and len(game_dirs) > 0:
|
||||
# 找到了游戏目录,显示选择对话框
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 卸载功能 - 在上级目录中找到以下游戏: {list(game_dirs.keys())}")
|
||||
|
||||
# 如果只有一个游戏,直接选择它
|
||||
if len(game_dirs) == 1:
|
||||
game_version = list(game_dirs.keys())[0]
|
||||
game_dir = game_dirs[game_version]
|
||||
self._confirm_and_uninstall(game_dir, game_version)
|
||||
else:
|
||||
# 有多个游戏,让用户选择
|
||||
from PySide6.QtWidgets import QInputDialog
|
||||
game_versions = list(game_dirs.keys())
|
||||
# 添加"全部卸载"选项
|
||||
game_versions.append("全部卸载")
|
||||
|
||||
selected_game, ok = QInputDialog.getItem(
|
||||
self, "选择游戏", "选择要卸载补丁的游戏:",
|
||||
game_versions, 0, False
|
||||
# 查找已安装补丁的游戏,只处理那些已安装补丁的游戏
|
||||
games_with_patch = {}
|
||||
for game_version, game_dir in game_dirs.items():
|
||||
if self.patch_manager.check_patch_installed(game_dir, game_version):
|
||||
games_with_patch[game_version] = game_dir
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 卸载功能 - {game_version} 已安装补丁")
|
||||
|
||||
# 检查是否有已安装补丁的游戏
|
||||
if not games_with_patch:
|
||||
QMessageBox.information(
|
||||
self,
|
||||
f"提示 - {APP_NAME}",
|
||||
"\n未在选择的目录中找到已安装补丁的游戏。\n请确认您选择了正确的游戏目录,并且该目录中的游戏已安装过补丁。\n",
|
||||
QMessageBox.StandardButton.Ok
|
||||
)
|
||||
return
|
||||
|
||||
# 创建自定义选择对话框,允许多选
|
||||
from PySide6.QtWidgets import QDialog, QVBoxLayout, QLabel, QListWidget, QPushButton, QHBoxLayout, QAbstractItemView
|
||||
|
||||
dialog = QDialog(self)
|
||||
dialog.setWindowTitle("选择要卸载的游戏补丁")
|
||||
dialog.resize(400, 300)
|
||||
|
||||
layout = QVBoxLayout(dialog)
|
||||
|
||||
# 添加"已安装补丁的游戏"标签
|
||||
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)
|
||||
|
||||
# 添加已安装游戏列表(可选,这里使用静态标签替代,保持一致性)
|
||||
installed_games_text = ", ".join(games_with_patch.keys())
|
||||
installed_games_label = QLabel(installed_games_text, dialog)
|
||||
layout.addWidget(installed_games_label)
|
||||
|
||||
# 添加一些间距
|
||||
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 games_with_patch.keys():
|
||||
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() == []:
|
||||
# 用户取消或未选择任何游戏
|
||||
return
|
||||
|
||||
if ok and selected_game:
|
||||
if selected_game == "全部卸载":
|
||||
# 卸载所有游戏补丁
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
f"确认卸载 - {APP_NAME}",
|
||||
f"\n确定要卸载所有游戏的补丁吗?\n这将卸载以下游戏的补丁:\n{chr(10).join(list(game_dirs.keys()))}\n",
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||
QMessageBox.StandardButton.No
|
||||
)
|
||||
|
||||
if reply == QMessageBox.StandardButton.Yes:
|
||||
# 使用批量卸载方法
|
||||
success_count, fail_count = self.patch_manager.batch_uninstall_patches(game_dirs)
|
||||
self.patch_manager.show_uninstall_result(success_count, fail_count)
|
||||
else:
|
||||
# 卸载选中的单个游戏
|
||||
game_version = selected_game
|
||||
game_dir = game_dirs[game_version]
|
||||
self._confirm_and_uninstall(game_dir, game_version)
|
||||
# 获取用户选择的游戏
|
||||
selected_games = [item.text() for item in list_widget.selectedItems()]
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 卸载功能 - 用户选择了以下游戏进行卸载: {selected_games}")
|
||||
|
||||
# 过滤game_dirs,只保留选中的游戏
|
||||
selected_game_dirs = {game: games_with_patch[game] for game in selected_games if game in games_with_patch}
|
||||
|
||||
# 确认卸载
|
||||
game_list = '\n'.join(selected_games)
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
f"确认卸载 - {APP_NAME}",
|
||||
f"\n确定要卸载以下游戏的补丁吗?\n\n{game_list}\n",
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||
QMessageBox.StandardButton.No
|
||||
)
|
||||
|
||||
if reply == QMessageBox.StandardButton.No:
|
||||
return
|
||||
|
||||
# 使用批量卸载方法
|
||||
success_count, fail_count, results = self.patch_manager.batch_uninstall_patches(selected_game_dirs)
|
||||
self.patch_manager.show_uninstall_result(success_count, fail_count, results)
|
||||
|
||||
else:
|
||||
# 未找到游戏目录,尝试将选择的目录作为游戏目录
|
||||
if debug_mode:
|
||||
@@ -579,7 +660,31 @@ class MainWindow(QMainWindow):
|
||||
if game_version:
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 卸载功能 - 识别为游戏: {game_version}")
|
||||
self._confirm_and_uninstall(selected_folder, game_version)
|
||||
|
||||
# 检查是否已安装补丁
|
||||
if self.patch_manager.check_patch_installed(selected_folder, game_version):
|
||||
# 确认卸载
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
f"确认卸载 - {APP_NAME}",
|
||||
f"\n确定要卸载 {game_version} 的补丁吗?\n游戏目录: {selected_folder}\n",
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||
QMessageBox.StandardButton.No
|
||||
)
|
||||
|
||||
if reply == QMessageBox.StandardButton.Yes:
|
||||
# 创建单个游戏的目录字典,使用批量卸载流程
|
||||
single_game_dir = {game_version: selected_folder}
|
||||
success_count, fail_count, results = self.patch_manager.batch_uninstall_patches(single_game_dir)
|
||||
self.patch_manager.show_uninstall_result(success_count, fail_count, results)
|
||||
else:
|
||||
# 没有安装补丁
|
||||
QMessageBox.information(
|
||||
self,
|
||||
f"提示 - {APP_NAME}",
|
||||
f"\n未在 {game_version} 中找到已安装的补丁。\n请确认该游戏已经安装过补丁。\n",
|
||||
QMessageBox.StandardButton.Ok
|
||||
)
|
||||
else:
|
||||
# 两种方式都未识别到游戏
|
||||
if debug_mode:
|
||||
@@ -592,28 +697,6 @@ class MainWindow(QMainWindow):
|
||||
)
|
||||
msg_box.exec()
|
||||
|
||||
def _confirm_and_uninstall(self, game_dir, game_version):
|
||||
"""确认并卸载补丁
|
||||
|
||||
Args:
|
||||
game_dir: 游戏目录
|
||||
game_version: 游戏版本
|
||||
"""
|
||||
debug_mode = self.debug_manager._is_debug_mode()
|
||||
|
||||
# 确认卸载
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
f"确认卸载 - {APP_NAME}",
|
||||
f"\n确定要卸载 {game_version} 的补丁吗?\n游戏目录: {game_dir}\n",
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||
QMessageBox.StandardButton.No
|
||||
)
|
||||
|
||||
if reply == QMessageBox.StandardButton.No:
|
||||
return
|
||||
|
||||
# 开始卸载补丁
|
||||
self.patch_manager.uninstall_patch(game_dir, game_version)
|
||||
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ def resource_path(relative_path):
|
||||
base_path = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||
|
||||
# 处理特殊的可执行文件和数据文件路径
|
||||
if relative_path in ("aria2c.exe", "cfst.exe"):
|
||||
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)
|
||||
@@ -333,7 +333,7 @@ class HostsManager:
|
||||
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
|
||||
@@ -347,8 +347,9 @@ class HostsManager:
|
||||
return False
|
||||
|
||||
try:
|
||||
# 首先清理已有的同域名记录
|
||||
self.clean_hostname_entries(hostname)
|
||||
# 首先清理已有的同域名记录(如果需要)
|
||||
if clean:
|
||||
self.clean_hostname_entries(hostname)
|
||||
|
||||
# 然后添加新记录
|
||||
lines = self.original_content.splitlines()
|
||||
|
||||
@@ -9,6 +9,26 @@ from PySide6.QtWidgets import (QLabel, QProgressBar, QVBoxLayout, QDialog, QHBox
|
||||
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):
|
||||
@@ -23,7 +43,7 @@ class DownloadThread(QThread):
|
||||
self.process = None
|
||||
self._is_running = True
|
||||
self._is_paused = False
|
||||
self.pause_process = None
|
||||
self.threads = []
|
||||
|
||||
def stop(self):
|
||||
if self.process and self.process.poll() is None:
|
||||
@@ -34,24 +54,56 @@ class DownloadThread(QThread):
|
||||
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:
|
||||
# 使用SIGSTOP信号暂停进程
|
||||
# Windows下使用不同的方式,因为没有SIGSTOP
|
||||
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
|
||||
# 在Windows上,使用暂停进程的方法
|
||||
self.pause_process = subprocess.Popen(['powershell', '-Command', f'(Get-Process -Id {self.process.pid}).Suspend()'],
|
||||
creationflags=subprocess.CREATE_NO_WINDOW)
|
||||
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 (subprocess.CalledProcessError, FileNotFoundError, OSError) as e:
|
||||
print(f"下载进程已暂停: PID {self.process.pid}")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"暂停下载进程时出错: {e}")
|
||||
return False
|
||||
return False
|
||||
@@ -60,21 +112,24 @@ class DownloadThread(QThread):
|
||||
"""恢复下载进程"""
|
||||
if self._is_paused and self.process and self.process.poll() is None:
|
||||
try:
|
||||
# 使用SIGCONT信号恢复进程
|
||||
# Windows下使用不同的方式
|
||||
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
|
||||
# 在Windows上,使用恢复进程的方法
|
||||
resume_process = subprocess.Popen(['powershell', '-Command', f'(Get-Process -Id {self.process.pid}).Resume()'],
|
||||
creationflags=subprocess.CREATE_NO_WINDOW)
|
||||
resume_process.wait()
|
||||
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 (subprocess.CalledProcessError, FileNotFoundError, OSError) as e:
|
||||
print(f"下载进程已恢复: PID {self.process.pid}")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"恢复下载进程时出错: {e}")
|
||||
return False
|
||||
return False
|
||||
@@ -82,14 +137,14 @@ class DownloadThread(QThread):
|
||||
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.exe")
|
||||
aria2c_path = resource_path("aria2c-fast_x64.exe")
|
||||
download_dir = os.path.dirname(self._7z_path)
|
||||
file_name = os.path.basename(self._7z_path)
|
||||
|
||||
@@ -100,6 +155,20 @@ class DownloadThread(QThread):
|
||||
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,
|
||||
@@ -117,25 +186,30 @@ class DownloadThread(QThread):
|
||||
'--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',
|
||||
'--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',
|
||||
# 优化参数 - 使用aria2允许的最佳设置
|
||||
'--split=128', # 增加分片数到128
|
||||
'--max-connection-per-server=16', # 最大允许值16
|
||||
'--split=128',
|
||||
f'--max-connection-per-server={thread_count}', # 使用动态的线程数
|
||||
'--min-split-size=1M', # 减小最小分片大小
|
||||
'--optimize-concurrent-downloads=true', # 优化并发下载
|
||||
'--file-allocation=none', # 禁用文件预分配加快开始
|
||||
'--async-dns=true', # 使用异步DNS
|
||||
'--disable-ipv6=true' # 禁用IPv6提高速度
|
||||
])
|
||||
|
||||
# 根据IPv6设置决定是否禁用IPv6
|
||||
if not ipv6_enabled:
|
||||
command.append('--disable-ipv6=true')
|
||||
print("已禁用IPv6支持")
|
||||
else:
|
||||
print("已启用IPv6支持")
|
||||
|
||||
# 证书验证现在总是需要,因为我们依赖hosts文件
|
||||
command.append('--check-certificate=false')
|
||||
|
||||
@@ -151,6 +225,10 @@ class DownloadThread(QThread):
|
||||
# 例如: #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:
|
||||
@@ -165,17 +243,24 @@ class DownloadThread(QThread):
|
||||
|
||||
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
|
||||
})
|
||||
# 检查是否达到更新间隔
|
||||
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()
|
||||
|
||||
@@ -239,6 +324,9 @@ class ProgressWindow(QDialog):
|
||||
# 设置暂停/恢复状态
|
||||
self.is_paused = False
|
||||
|
||||
# 添加最后进度记录,用于优化UI更新
|
||||
self._last_percent = -1
|
||||
|
||||
def update_pause_button_state(self, is_paused):
|
||||
"""更新暂停按钮的显示状态
|
||||
|
||||
@@ -261,10 +349,17 @@ class ProgressWindow(QDialog):
|
||||
# 清除ETA值中可能存在的"]"符号
|
||||
if isinstance(eta, str):
|
||||
eta = eta.replace("]", "")
|
||||
|
||||
self.game_label.setText(f"正在下载 {game_version} 的补丁")
|
||||
self.progress_bar.setValue(int(percent))
|
||||
self.stats_label.setText(f"速度: {speed} | 线程: {threads} | 剩余时间: {eta}")
|
||||
|
||||
# 优化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)
|
||||
|
||||
@@ -33,12 +33,12 @@ class IpOptimizer:
|
||||
# 正确的参数设置,根据cfst帮助文档
|
||||
command = [
|
||||
cst_path,
|
||||
"-n", "500", # 延迟测速线程数 (默认200)
|
||||
"-n", "1000", # 延迟测速线程数 (默认200)
|
||||
"-p", "1", # 显示结果数量 (默认10个)
|
||||
"-url", url, # 指定测速地址
|
||||
"-f", ip_txt_path, # IP文件
|
||||
"-dd", # 禁用下载测速,按延迟排序
|
||||
"-o","" # 不写入结果文件
|
||||
"-o"," " # 不写入结果文件
|
||||
]
|
||||
|
||||
creation_flags = subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0
|
||||
@@ -145,6 +145,142 @@ class IpOptimizer:
|
||||
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 进程...")
|
||||
@@ -166,16 +302,24 @@ class IpOptimizer:
|
||||
|
||||
|
||||
class IpOptimizerThread(QThread):
|
||||
"""用于在后台线程中运行IP优化的类"""
|
||||
"""用于在后台线程中运行IP优化的类
|
||||
|
||||
注意:IPv6连接测试功能已迁移至IPv6Manager类,
|
||||
本类仅负责IP优化相关功能
|
||||
"""
|
||||
finished = Signal(str)
|
||||
|
||||
def __init__(self, url, parent=None):
|
||||
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):
|
||||
optimal_ip = self.optimizer.get_optimal_ip(self.url)
|
||||
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):
|
||||
|
||||
Reference in New Issue
Block a user