Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
adcd6da5b4 | ||
|
|
d7ceb71607 | ||
|
|
41e3c88a2f | ||
|
|
66403406db | ||
|
|
e82e5dcd63 | ||
|
|
979c23f8b8 | ||
|
|
d07ef20e51 | ||
|
|
43a66f66a9 | ||
|
|
ac2b0112e8 | ||
|
|
a97cdf4c23 | ||
|
|
4f2217ca95 | ||
|
|
2c91319d5f | ||
|
|
1b6d275433 | ||
|
|
7d71ffe099 | ||
|
|
68bbafc564 | ||
|
|
dc433a2ac9 | ||
|
|
4d847f58d0 | ||
|
|
6a4c6ca1f1 | ||
|
|
f0031ed17c | ||
|
|
09d6883432 | ||
|
|
ee72f76952 | ||
|
|
ba5e3cdbc1 | ||
|
|
575116e45c | ||
|
|
bf80c19fe1 | ||
|
|
d12739baab | ||
|
|
19cdd5b8cd | ||
|
|
dfdeb54b43 | ||
|
|
7befe19f30 | ||
|
|
b18f4a276c | ||
|
|
534f7381bd | ||
|
|
2158331532 | ||
|
|
8b4dedc8c6 | ||
|
|
98bfddeb04 |
16
.gitignore
vendored
@@ -173,4 +173,18 @@ cython_debug/
|
||||
nuitka-crash-report.xml
|
||||
build.bat
|
||||
log.txt
|
||||
result.csv
|
||||
result.csv
|
||||
after.7z
|
||||
vol.1.7z
|
||||
vol.2.7z
|
||||
vol.3.7z
|
||||
vol.4.7z
|
||||
log/
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
venv/
|
||||
.venv/
|
||||
Thumbs.db
|
||||
.DS_Store
|
||||
STRUCTURE.md
|
||||
source/STRUCTURE.md
|
||||
|
||||
@@ -21,6 +21,10 @@
|
||||
- 游戏安装路径:用于识别已安装的游戏和安装补丁
|
||||
- 文件哈希值:用于验证文件完整性
|
||||
|
||||
### 2.4 离线模式和本地文件
|
||||
- **离线模式**:本应用提供离线模式。在离线模式下,应用不会进行任何网络连接,包括检查更新、获取云端配置或进行任何网络相关的测试,安装过程将只使用本地文件。
|
||||
- **本地文件使用**:为了支持离线安装,本应用会扫描其所在目录下的压缩包,以查找用于安装的补丁压缩包。此文件扫描操作仅限于应用所在的文件夹,不会访问或修改您系统中的其他文件。
|
||||
|
||||
## 3. 信息使用
|
||||
|
||||
我们收集的信息仅用于以下目的:
|
||||
@@ -88,4 +92,4 @@
|
||||
|
||||
本隐私政策可能会根据应用功能的变化而更新。请定期查看最新版本。
|
||||
|
||||
最后更新日期:2025年8月4日
|
||||
最后更新日期:2025年8月15日
|
||||
@@ -1,21 +1,86 @@
|
||||
import sys
|
||||
import os
|
||||
import datetime
|
||||
import traceback
|
||||
from PySide6.QtWidgets import QApplication, QMessageBox
|
||||
from main_window import MainWindow
|
||||
from core.privacy_manager import PrivacyManager
|
||||
from utils.logger import setup_logger
|
||||
from core.managers.privacy_manager import PrivacyManager
|
||||
from utils.logger import setup_logger, cleanup_old_logs, log_uncaught_exceptions
|
||||
from config.config import LOG_FILE, APP_NAME, LOG_RETENTION_DAYS
|
||||
from utils import load_config
|
||||
|
||||
def excepthook(exc_type, exc_value, exc_traceback):
|
||||
"""全局异常处理钩子,将未捕获的异常记录到日志并显示错误对话框"""
|
||||
# 记录异常到日志
|
||||
if hasattr(sys, '_excepthook'):
|
||||
sys._excepthook(exc_type, exc_value, exc_traceback)
|
||||
else:
|
||||
log_uncaught_exceptions(exc_type, exc_value, exc_traceback)
|
||||
|
||||
# 将异常格式化为易读的形式
|
||||
exception_text = ''.join(traceback.format_exception(exc_type, exc_value, exc_traceback))
|
||||
|
||||
# 创建错误对话框
|
||||
msg = f"程序遇到未处理的异常:\n\n{str(exc_value)}\n\n详细错误已记录到日志文件。"
|
||||
try:
|
||||
# 尝试使用QMessageBox显示错误
|
||||
app = QApplication.instance()
|
||||
if app:
|
||||
QMessageBox.critical(None, f"错误 - {APP_NAME}", msg)
|
||||
except Exception:
|
||||
# 如果QMessageBox失败,则使用标准输出
|
||||
print(f"严重错误: {msg}")
|
||||
print(f"详细错误: {exception_text}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 初始化日志
|
||||
# 设置主日志
|
||||
logger = setup_logger("main")
|
||||
logger.info("应用启动")
|
||||
|
||||
# 设置全局异常处理钩子
|
||||
sys._excepthook = sys.excepthook
|
||||
sys.excepthook = excepthook
|
||||
|
||||
# 记录程序启动信息
|
||||
logger.debug(f"Python版本: {sys.version}")
|
||||
logger.debug(f"运行平台: {sys.platform}")
|
||||
|
||||
# 检查配置中是否启用了调试模式
|
||||
config = load_config()
|
||||
debug_mode = config.get("debug_mode", False)
|
||||
|
||||
# 在应用启动时清理过期的日志文件
|
||||
cleanup_old_logs(LOG_RETENTION_DAYS)
|
||||
logger.debug(f"已执行日志清理,保留最近{LOG_RETENTION_DAYS}天的日志")
|
||||
|
||||
# 如果调试模式已启用,确保立即创建主日志文件
|
||||
if debug_mode:
|
||||
try:
|
||||
# 确保log目录存在
|
||||
log_dir = os.path.dirname(LOG_FILE)
|
||||
if not os.path.exists(log_dir):
|
||||
os.makedirs(log_dir, exist_ok=True)
|
||||
logger.debug(f"已创建日志目录: {log_dir}")
|
||||
|
||||
# 记录调试会话信息
|
||||
logger.debug(f"--- 新调试会话开始于 {os.path.basename(LOG_FILE)} ---")
|
||||
logger.debug(f"--- 应用版本: {APP_NAME} ---")
|
||||
current_time = datetime.datetime.now()
|
||||
formatted_date = current_time.strftime("%Y-%m-%d")
|
||||
formatted_time = current_time.strftime("%H:%M:%S")
|
||||
logger.debug(f"--- 日期: {formatted_date} 时间: {formatted_time} ---")
|
||||
|
||||
logger.debug(f"调试模式已启用,日志文件路径: {os.path.abspath(LOG_FILE)}")
|
||||
except Exception as e:
|
||||
logger.error(f"创建日志文件失败: {e}")
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
# 初始化隐私协议管理器
|
||||
try:
|
||||
privacy_manager = PrivacyManager()
|
||||
except Exception as e:
|
||||
logger.error(f"初始化隐私协议管理器失败: {e}")
|
||||
logger.error(f"错误详情: {traceback.format_exc()}")
|
||||
QMessageBox.critical(
|
||||
None,
|
||||
"隐私协议加载错误",
|
||||
@@ -23,13 +88,11 @@ if __name__ == "__main__":
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
# 显示隐私协议对话框
|
||||
if not privacy_manager.show_privacy_dialog():
|
||||
logger.info("用户未同意隐私协议,程序退出")
|
||||
sys.exit(0) # 如果用户不同意隐私协议,退出程序
|
||||
sys.exit(0)
|
||||
|
||||
# 用户已同意隐私协议,继续启动程序
|
||||
logger.info("隐私协议已同意,启动主程序")
|
||||
window = MainWindow()
|
||||
window.show()
|
||||
sys.exit(app.exec())
|
||||
sys.exit(app.exec())
|
||||
7
source/assets/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
# Assets package initialization
|
||||
"""
|
||||
包含应用程序使用的静态资源文件:
|
||||
- fonts: 字体文件
|
||||
- images: 图片资源
|
||||
- resources: 其他资源文件
|
||||
"""
|
||||
|
Before Width: | Height: | Size: 162 KiB After Width: | Height: | Size: 162 KiB |
|
Before Width: | Height: | Size: 179 KiB After Width: | Height: | Size: 179 KiB |
|
Before Width: | Height: | Size: 571 KiB After Width: | Height: | Size: 571 KiB |
|
Before Width: | Height: | Size: 250 KiB After Width: | Height: | Size: 250 KiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 229 KiB After Width: | Height: | Size: 229 KiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 264 KiB After Width: | Height: | Size: 264 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 177 KiB After Width: | Height: | Size: 177 KiB |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 327 KiB After Width: | Height: | Size: 327 KiB |
|
Before Width: | Height: | Size: 166 KiB After Width: | Height: | Size: 166 KiB |
|
Before Width: | Height: | Size: 184 KiB After Width: | Height: | Size: 184 KiB |
|
Before Width: | Height: | Size: 197 KiB After Width: | Height: | Size: 197 KiB |
|
Before Width: | Height: | Size: 166 KiB After Width: | Height: | Size: 166 KiB |
1
source/config/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import os
|
||||
import base64
|
||||
import datetime
|
||||
|
||||
# 配置信息
|
||||
app_data = {
|
||||
"APP_VERSION": "1.3.1",
|
||||
"APP_VERSION": "1.4.0",
|
||||
"APP_NAME": "FRAISEMOE Addons Installer NEXT",
|
||||
"TEMP": "TEMP",
|
||||
"CACHE": "FRAISEMOE",
|
||||
@@ -45,24 +46,58 @@ app_data = {
|
||||
},
|
||||
}
|
||||
|
||||
# Base64解码
|
||||
def decode_base64(encoded_str):
|
||||
return base64.b64decode(encoded_str).decode("utf-8")
|
||||
def decode_base64(b64str):
|
||||
"""解码base64字符串"""
|
||||
try:
|
||||
return base64.b64decode(b64str).decode('utf-8')
|
||||
except:
|
||||
return b64str
|
||||
|
||||
# 确保缓存目录存在
|
||||
def ensure_cache_dirs():
|
||||
os.makedirs(CACHE, exist_ok=True)
|
||||
os.makedirs(PLUGIN, exist_ok=True)
|
||||
|
||||
# 全局变量
|
||||
APP_VERSION = app_data["APP_VERSION"]
|
||||
APP_NAME = app_data["APP_NAME"]
|
||||
APP_VERSION = app_data["APP_VERSION"] # 从app_data中获取,不再重复定义
|
||||
TEMP = os.getenv(app_data["TEMP"]) or app_data["TEMP"]
|
||||
CACHE = os.path.join(TEMP, app_data["CACHE"])
|
||||
CONFIG_FILE = os.path.join(CACHE, "config.json")
|
||||
LOG_FILE = "log.txt"
|
||||
|
||||
# 日志配置
|
||||
LOG_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "log")
|
||||
LOG_LEVEL = "DEBUG" # 可选值: DEBUG, INFO, WARNING, ERROR, CRITICAL
|
||||
|
||||
# 日志文件大小和轮转配置(新增)
|
||||
LOG_MAX_SIZE = 10 * 1024 * 1024 # 10MB
|
||||
LOG_BACKUP_COUNT = 3 # 保留3个备份文件
|
||||
LOG_RETENTION_DAYS = 7 # 日志保留7天
|
||||
|
||||
# 将log文件放在程序根目录下的log文件夹中,使用日期+时间戳格式命名
|
||||
root_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
||||
log_dir = os.path.join(root_dir, "log")
|
||||
os.makedirs(log_dir, exist_ok=True)
|
||||
current_datetime = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
|
||||
LOG_FILE = os.path.join(log_dir, f"log-{current_datetime}.txt")
|
||||
|
||||
PLUGIN = os.path.join(CACHE, app_data["PLUGIN"])
|
||||
CONFIG_URL = decode_base64(app_data["CONFIG_URL"])
|
||||
UA = app_data["UA_TEMPLATE"].format(APP_VERSION)
|
||||
GAME_INFO = app_data["game_info"]
|
||||
|
||||
# 哈希计算块大小
|
||||
BLOCK_SIZE = 67108864
|
||||
HASH_SIZE = 134217728
|
||||
PLUGIN_HASH = {game: info["hash"] for game, info in GAME_INFO.items()}
|
||||
|
||||
# 资源哈希值
|
||||
GAME_INFO = app_data["game_info"]
|
||||
PLUGIN_HASH = {
|
||||
"NEKOPARA Vol.1": GAME_INFO["NEKOPARA Vol.1"]["hash"],
|
||||
"NEKOPARA Vol.2": GAME_INFO["NEKOPARA Vol.2"]["hash"],
|
||||
"NEKOPARA Vol.3": GAME_INFO["NEKOPARA Vol.3"]["hash"],
|
||||
"NEKOPARA Vol.4": GAME_INFO["NEKOPARA Vol.4"]["hash"],
|
||||
"NEKOPARA After": GAME_INFO["NEKOPARA After"]["hash"]
|
||||
}
|
||||
PROCESS_INFO = {info["exe"]: game for game, info in GAME_INFO.items()}
|
||||
|
||||
# 下载线程档位设置
|
||||
@@ -5,6 +5,10 @@ import os
|
||||
import re
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from utils.logger import setup_logger
|
||||
|
||||
# 初始化logger
|
||||
logger = setup_logger("privacy_policy")
|
||||
|
||||
# 隐私协议的缩略版内容
|
||||
PRIVACY_POLICY_BRIEF = """
|
||||
@@ -16,6 +20,7 @@ PRIVACY_POLICY_BRIEF = """
|
||||
- **系统信息**:程序版本号。
|
||||
- **网络信息**:IP 地址、ISP、地理位置(用于使用统计)、下载统计、IPv6 连接测试(通过访问 testipv6.cn)、IPv6 地址获取(通过 ipw.cn)。
|
||||
- **文件信息**:游戏安装路径、文件哈希值。
|
||||
- **离线模式**:在离线模式下,本应用不会进行任何网络活动,仅使用本地文件进行安装。为实现此功能,应用会扫描其所在目录下的压缩包文件。
|
||||
|
||||
## 系统修改
|
||||
- 使用 Cloudflare 加速时会临时修改系统 hosts 文件。
|
||||
@@ -39,6 +44,7 @@ This application collects and processes the following information:
|
||||
- **System info**: Application version.
|
||||
- **Network info**: IP address, ISP, geographic location (for usage statistics), download statistics, IPv6 connectivity test (via testipv6.cn), IPv6 address acquisition (via ipw.cn).
|
||||
- **File info**: Game installation paths, file hash values.
|
||||
- **Offline Mode**: In offline mode, the application will not perform any network activities and will only use local files for installation. To achieve this, the application scans for compressed files in its directory.
|
||||
|
||||
## System Modifications
|
||||
- Temporarily modifies system hosts file when using Cloudflare acceleration.
|
||||
@@ -53,7 +59,7 @@ The complete privacy policy can be found in the program's GitHub repository.
|
||||
"""
|
||||
|
||||
# 默认隐私协议版本 - 本地版本的日期
|
||||
PRIVACY_POLICY_VERSION = "2025.08.04"
|
||||
PRIVACY_POLICY_VERSION = "2025.08.15"
|
||||
|
||||
def get_local_privacy_policy():
|
||||
"""获取本地打包的隐私协议文件
|
||||
@@ -83,14 +89,14 @@ def get_local_privacy_policy():
|
||||
try:
|
||||
date_obj = datetime.strptime(date_str, '%Y年%m月%d日')
|
||||
date_version = date_obj.strftime('%Y.%m.%d')
|
||||
print(f"成功读取本地隐私协议文件: {path}, 版本: {date_version}")
|
||||
logger.debug(f"成功读取本地隐私协议文件: {path}, 版本: {date_version}")
|
||||
return content, date_version, ""
|
||||
except ValueError:
|
||||
print(f"本地隐私协议日期格式解析错误: {path}")
|
||||
logger.error(f"本地隐私协议日期格式解析错误: {path}")
|
||||
else:
|
||||
print(f"本地隐私协议未找到更新日期: {path}")
|
||||
logger.warning(f"本地隐私协议未找到更新日期: {path}")
|
||||
except Exception as e:
|
||||
print(f"读取本地隐私协议失败 {path}: {str(e)}")
|
||||
logger.error(f"读取本地隐私协议失败 {path}: {str(e)}")
|
||||
|
||||
# 所有路径都尝试失败,使用默认版本
|
||||
return PRIVACY_POLICY_BRIEF, PRIVACY_POLICY_VERSION, "无法读取本地隐私协议文件"
|
||||
@@ -1,15 +1,16 @@
|
||||
from .animations import MultiStageAnimations
|
||||
from .ui_manager import UIManager
|
||||
from .download_manager import DownloadManager
|
||||
from .debug_manager import DebugManager
|
||||
from .window_manager import WindowManager
|
||||
from .game_detector import GameDetector
|
||||
from .patch_manager import PatchManager
|
||||
from .config_manager import ConfigManager
|
||||
from .privacy_manager import PrivacyManager
|
||||
from .cloudflare_optimizer import CloudflareOptimizer
|
||||
from .download_task_manager import DownloadTaskManager
|
||||
from .extraction_handler import ExtractionHandler
|
||||
from .managers.ui_manager import UIManager
|
||||
from .managers.download_managers import DownloadManager
|
||||
from .managers.debug_manager import DebugManager
|
||||
from .managers.window_manager import WindowManager
|
||||
from .managers.game_detector import GameDetector
|
||||
from .managers.patch_manager import PatchManager
|
||||
from .managers.config_manager import ConfigManager
|
||||
from .managers.privacy_manager import PrivacyManager
|
||||
from .managers.cloudflare_optimizer import CloudflareOptimizer
|
||||
from .managers.download_managers import DownloadTaskManager
|
||||
from .managers.patch_detector import PatchDetector
|
||||
from .managers.animations import MultiStageAnimations
|
||||
from .handlers.extraction_handler import ExtractionHandler
|
||||
|
||||
__all__ = [
|
||||
'MultiStageAnimations',
|
||||
@@ -23,5 +24,5 @@ __all__ = [
|
||||
'PrivacyManager',
|
||||
'CloudflareOptimizer',
|
||||
'DownloadTaskManager',
|
||||
'ExtractionHandler'
|
||||
'PatchDetector',
|
||||
]
|
||||
@@ -1,82 +0,0 @@
|
||||
import os
|
||||
import sys
|
||||
from PySide6 import QtWidgets
|
||||
from data.config import LOG_FILE
|
||||
from utils import Logger
|
||||
|
||||
class DebugManager:
|
||||
def __init__(self, main_window):
|
||||
"""初始化调试管理器
|
||||
|
||||
Args:
|
||||
main_window: 主窗口实例
|
||||
"""
|
||||
self.main_window = main_window
|
||||
self.logger = None
|
||||
self.original_stdout = None
|
||||
self.original_stderr = None
|
||||
self.ui_manager = None # 添加ui_manager属性
|
||||
|
||||
def set_ui_manager(self, ui_manager):
|
||||
"""设置UI管理器引用
|
||||
|
||||
Args:
|
||||
ui_manager: UI管理器实例
|
||||
"""
|
||||
self.ui_manager = ui_manager
|
||||
|
||||
def _is_debug_mode(self):
|
||||
"""检查是否处于调试模式
|
||||
|
||||
Returns:
|
||||
bool: 是否处于调试模式
|
||||
"""
|
||||
if hasattr(self, 'ui_manager') and hasattr(self.ui_manager, 'debug_action'):
|
||||
return self.ui_manager.debug_action.isChecked()
|
||||
return False
|
||||
|
||||
def toggle_debug_mode(self, checked):
|
||||
"""切换调试模式
|
||||
|
||||
Args:
|
||||
checked: 是否启用调试模式
|
||||
"""
|
||||
print(f"Toggle debug mode: {checked}")
|
||||
self.main_window.config["debug_mode"] = checked
|
||||
self.main_window.save_config(self.main_window.config)
|
||||
|
||||
# 更新打开log文件按钮状态
|
||||
if hasattr(self, 'ui_manager') and hasattr(self.ui_manager, 'open_log_action'):
|
||||
self.ui_manager.open_log_action.setEnabled(checked)
|
||||
|
||||
if checked:
|
||||
self.start_logging()
|
||||
else:
|
||||
self.stop_logging()
|
||||
|
||||
def start_logging(self):
|
||||
"""启动日志记录"""
|
||||
if self.logger is None:
|
||||
try:
|
||||
if os.path.exists(LOG_FILE):
|
||||
os.remove(LOG_FILE)
|
||||
# 保存原始的 stdout 和 stderr
|
||||
self.original_stdout = sys.stdout
|
||||
self.original_stderr = sys.stderr
|
||||
# 创建 Logger 实例
|
||||
self.logger = Logger(LOG_FILE, self.original_stdout)
|
||||
sys.stdout = self.logger
|
||||
sys.stderr = self.logger
|
||||
print("--- Debug mode enabled ---")
|
||||
except (IOError, OSError) as e:
|
||||
QtWidgets.QMessageBox.critical(self.main_window, "错误", f"无法创建日志文件: {e}")
|
||||
self.logger = None
|
||||
|
||||
def stop_logging(self):
|
||||
"""停止日志记录"""
|
||||
if self.logger:
|
||||
print("--- Debug mode disabled ---")
|
||||
sys.stdout = self.original_stdout
|
||||
sys.stderr = self.original_stderr
|
||||
self.logger.close()
|
||||
self.logger = None
|
||||
@@ -1,642 +0,0 @@
|
||||
import os
|
||||
import requests
|
||||
import json
|
||||
from collections import deque
|
||||
from urllib.parse import urlparse
|
||||
import re # Added for recursive search
|
||||
|
||||
from PySide6 import QtWidgets, QtCore
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtGui import QIcon, QPixmap, QFont
|
||||
|
||||
from utils import msgbox_frame, HostsManager, resource_path
|
||||
from data.config import APP_NAME, PLUGIN, GAME_INFO, UA, CONFIG_URL, DOWNLOAD_THREADS, DEFAULT_DOWNLOAD_THREAD_LEVEL
|
||||
from workers import IpOptimizerThread
|
||||
from core.cloudflare_optimizer import CloudflareOptimizer
|
||||
from core.download_task_manager import DownloadTaskManager
|
||||
from core.extraction_handler import ExtractionHandler
|
||||
|
||||
class DownloadManager:
|
||||
def __init__(self, main_window):
|
||||
"""初始化下载管理器
|
||||
|
||||
Args:
|
||||
main_window: 主窗口实例,用于访问UI和状态
|
||||
"""
|
||||
self.main_window = main_window
|
||||
self.main_window.APP_NAME = APP_NAME # 为了让子模块能够访问APP_NAME
|
||||
self.selected_folder = ""
|
||||
self.download_queue = deque()
|
||||
self.current_download_thread = None
|
||||
self.hosts_manager = HostsManager()
|
||||
|
||||
# 添加下载线程级别
|
||||
self.download_thread_level = DEFAULT_DOWNLOAD_THREAD_LEVEL
|
||||
|
||||
# 初始化子模块
|
||||
self.cloudflare_optimizer = CloudflareOptimizer(main_window, self.hosts_manager)
|
||||
self.download_task_manager = DownloadTaskManager(main_window, self.download_thread_level)
|
||||
self.extraction_handler = ExtractionHandler(main_window)
|
||||
|
||||
def file_dialog(self):
|
||||
"""显示文件夹选择对话框,选择游戏安装目录"""
|
||||
self.selected_folder = QtWidgets.QFileDialog.getExistingDirectory(
|
||||
self.main_window, f"选择游戏所在【上级目录】 {APP_NAME}"
|
||||
)
|
||||
if not self.selected_folder:
|
||||
QtWidgets.QMessageBox.warning(
|
||||
self.main_window, f"通知 - {APP_NAME}", "\n未选择任何目录,请重新选择\n"
|
||||
)
|
||||
return
|
||||
|
||||
# 将按钮文本设为安装中状态
|
||||
self.main_window.ui.start_install_text.setText("正在安装")
|
||||
|
||||
# 禁用整个主窗口,防止用户操作
|
||||
self.main_window.setEnabled(False)
|
||||
|
||||
self.download_action()
|
||||
|
||||
def get_install_paths(self):
|
||||
"""获取所有游戏版本的安装路径"""
|
||||
# 使用改进的目录识别功能
|
||||
game_dirs = self.main_window.game_detector.identify_game_directories_improved(self.selected_folder)
|
||||
install_paths = {}
|
||||
|
||||
debug_mode = self.is_debug_mode()
|
||||
|
||||
for game, info in GAME_INFO.items():
|
||||
if game in game_dirs:
|
||||
# 如果找到了游戏目录,使用它
|
||||
game_dir = game_dirs[game]
|
||||
install_path = os.path.join(game_dir, os.path.basename(info["install_path"]))
|
||||
install_paths[game] = install_path
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 使用识别到的游戏目录 {game}: {game_dir}")
|
||||
print(f"DEBUG: 安装路径设置为: {install_path}")
|
||||
|
||||
return install_paths
|
||||
|
||||
def is_debug_mode(self):
|
||||
"""检查是否处于调试模式"""
|
||||
if hasattr(self.main_window, 'ui_manager') and self.main_window.ui_manager:
|
||||
if hasattr(self.main_window.ui_manager, 'debug_action') and self.main_window.ui_manager.debug_action:
|
||||
return self.main_window.ui_manager.debug_action.isChecked()
|
||||
return False
|
||||
|
||||
def get_download_url(self) -> dict:
|
||||
"""获取所有游戏版本的下载链接
|
||||
|
||||
Returns:
|
||||
dict: 包含游戏版本和下载URL的字典
|
||||
"""
|
||||
try:
|
||||
if self.main_window.cloud_config:
|
||||
if self.is_debug_mode():
|
||||
print("--- Using pre-fetched cloud config ---")
|
||||
config_data = self.main_window.cloud_config
|
||||
else:
|
||||
# 如果没有预加载的配置,则同步获取
|
||||
headers = {"User-Agent": UA}
|
||||
response = requests.get(CONFIG_URL, headers=headers, timeout=10)
|
||||
response.raise_for_status()
|
||||
config_data = response.json()
|
||||
|
||||
if not config_data:
|
||||
raise ValueError("未能获取或解析配置数据")
|
||||
|
||||
if self.is_debug_mode():
|
||||
print(f"DEBUG: Parsed JSON data: {json.dumps(config_data, indent=2)}")
|
||||
|
||||
# 统一处理URL提取,确保返回扁平化的字典
|
||||
urls = {}
|
||||
for i in range(4):
|
||||
key = f"vol.{i+1}.data"
|
||||
if key in config_data and "url" in config_data[key]:
|
||||
urls[f"vol{i+1}"] = config_data[key]["url"]
|
||||
|
||||
if "after.data" in config_data and "url" in config_data["after.data"]:
|
||||
urls["after"] = config_data["after.data"]["url"]
|
||||
|
||||
# 检查是否成功提取了所有URL
|
||||
if len(urls) != 5:
|
||||
missing_keys_map = {
|
||||
f"vol{i+1}": f"vol.{i+1}.data" for i in range(4)
|
||||
}
|
||||
missing_keys_map["after"] = "after.data"
|
||||
|
||||
extracted_keys = set(urls.keys())
|
||||
all_keys = set(missing_keys_map.keys())
|
||||
missing_simple_keys = all_keys - extracted_keys
|
||||
|
||||
missing_original_keys = [missing_keys_map[k] for k in missing_simple_keys]
|
||||
raise ValueError(f"配置文件缺少必要的键: {', '.join(missing_original_keys)}")
|
||||
|
||||
if self.is_debug_mode():
|
||||
print(f"DEBUG: Extracted URLs: {urls}")
|
||||
print("--- Finished getting download URL successfully ---")
|
||||
return urls
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
status_code = e.response.status_code if e.response is not None else "未知"
|
||||
try:
|
||||
error_response = e.response.json() if e.response else {}
|
||||
json_title = error_response.get("title", "无错误类型")
|
||||
json_message = error_response.get("message", "无附加错误信息")
|
||||
except (ValueError, AttributeError):
|
||||
json_title = "配置文件异常,无法解析错误类型"
|
||||
json_message = "配置文件异常,无法解析错误信息"
|
||||
|
||||
if self.is_debug_mode():
|
||||
print(f"ERROR: Failed to get download config due to RequestException: {e}")
|
||||
|
||||
QtWidgets.QMessageBox.critical(
|
||||
self.main_window,
|
||||
f"错误 - {APP_NAME}",
|
||||
f"\n下载配置获取失败\n\n【HTTP状态】:{status_code}\n【错误类型】:{json_title}\n【错误信息】:{json_message}\n",
|
||||
)
|
||||
return {}
|
||||
except ValueError as e:
|
||||
if self.is_debug_mode():
|
||||
print(f"ERROR: Failed to parse download config due to ValueError: {e}")
|
||||
|
||||
QtWidgets.QMessageBox.critical(
|
||||
self.main_window,
|
||||
f"错误 - {APP_NAME}",
|
||||
f"\n配置文件格式异常\n\n【错误信息】:{e}\n",
|
||||
)
|
||||
return {}
|
||||
|
||||
def download_action(self):
|
||||
"""开始下载流程"""
|
||||
# 主窗口在file_dialog中已被禁用
|
||||
|
||||
# 清空下载历史记录
|
||||
self.main_window.download_queue_history = []
|
||||
|
||||
# 使用改进的目录识别功能
|
||||
game_dirs = self.main_window.game_detector.identify_game_directories_improved(self.selected_folder)
|
||||
|
||||
debug_mode = self.is_debug_mode()
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 开始下载流程, 识别到 {len(game_dirs)} 个游戏目录")
|
||||
|
||||
# 检查是否找到任何游戏目录
|
||||
if not game_dirs:
|
||||
if debug_mode:
|
||||
print("DEBUG: 未识别到任何游戏目录,设置目录未找到错误")
|
||||
# 设置特定的错误类型,以便在按钮点击处理中区分处理
|
||||
self.main_window.last_error_message = "directory_not_found"
|
||||
QtWidgets.QMessageBox.warning(
|
||||
self.main_window,
|
||||
f"目录错误 - {APP_NAME}",
|
||||
"\n未能识别到任何游戏目录。\n\n请确认您选择的是游戏的上级目录,并且该目录中包含NEKOPARA系列游戏文件夹。\n"
|
||||
)
|
||||
# 恢复主窗口
|
||||
self.main_window.setEnabled(True)
|
||||
self.main_window.ui.start_install_text.setText("开始安装")
|
||||
return
|
||||
|
||||
# 显示哈希检查窗口
|
||||
self.main_window.hash_msg_box = self.main_window.hash_manager.hash_pop_window(check_type="pre")
|
||||
|
||||
# 执行预检查,先判断哪些游戏版本已安装了补丁
|
||||
install_paths = self.get_install_paths()
|
||||
|
||||
self.main_window.hash_thread = self.main_window.create_hash_thread("pre", install_paths)
|
||||
# 使用lambda连接,传递game_dirs参数
|
||||
self.main_window.hash_thread.pre_finished.connect(
|
||||
lambda updated_status: self.on_pre_hash_finished_with_dirs(updated_status, game_dirs)
|
||||
)
|
||||
self.main_window.hash_thread.start()
|
||||
|
||||
def on_pre_hash_finished_with_dirs(self, updated_status, game_dirs):
|
||||
"""优化的哈希预检查完成处理,带有游戏目录信息
|
||||
|
||||
Args:
|
||||
updated_status: 更新后的安装状态
|
||||
game_dirs: 识别到的游戏目录
|
||||
"""
|
||||
self.main_window.installed_status = updated_status
|
||||
if self.main_window.hash_msg_box and self.main_window.hash_msg_box.isVisible():
|
||||
self.main_window.hash_msg_box.accept()
|
||||
self.main_window.hash_msg_box = None
|
||||
|
||||
debug_mode = self.is_debug_mode()
|
||||
|
||||
# 临时启用窗口以显示选择对话框
|
||||
self.main_window.setEnabled(True)
|
||||
|
||||
# 获取可安装的游戏版本列表(尚未安装补丁的版本)
|
||||
installable_games = []
|
||||
already_installed_games = []
|
||||
for game_version, game_dir in game_dirs.items():
|
||||
if self.main_window.installed_status.get(game_version, False):
|
||||
if debug_mode:
|
||||
print(f"DEBUG: {game_version} 已安装补丁,不需要再次安装")
|
||||
already_installed_games.append(game_version)
|
||||
else:
|
||||
if debug_mode:
|
||||
print(f"DEBUG: {game_version} 未安装补丁,可以安装")
|
||||
installable_games.append(game_version)
|
||||
|
||||
# 显示状态消息
|
||||
status_message = ""
|
||||
if already_installed_games:
|
||||
status_message += f"已安装补丁的游戏:\n{chr(10).join(already_installed_games)}\n\n"
|
||||
|
||||
if not installable_games:
|
||||
# 如果没有可安装的游戏
|
||||
QtWidgets.QMessageBox.information(
|
||||
self.main_window,
|
||||
f"信息 - {APP_NAME}",
|
||||
f"\n所有检测到的游戏都已安装补丁。\n\n{status_message}"
|
||||
)
|
||||
# 恢复主窗口
|
||||
self.main_window.setEnabled(True)
|
||||
self.main_window.ui.start_install_text.setText("开始安装")
|
||||
return
|
||||
|
||||
# 如果有可安装的游戏版本,让用户选择
|
||||
from PySide6.QtWidgets import QInputDialog, QListWidget, QVBoxLayout, QDialog, QLabel, QPushButton, QAbstractItemView, QHBoxLayout
|
||||
|
||||
# 创建自定义选择对话框
|
||||
dialog = QDialog(self.main_window)
|
||||
dialog.setWindowTitle("选择要安装的游戏")
|
||||
dialog.resize(400, 300)
|
||||
|
||||
layout = QVBoxLayout(dialog)
|
||||
|
||||
# 先显示已安装补丁的游戏
|
||||
if already_installed_games:
|
||||
already_installed_label = QLabel("已安装补丁的游戏:", dialog)
|
||||
already_installed_label.setFont(QFont(already_installed_label.font().family(), already_installed_label.font().pointSize(), QFont.Bold))
|
||||
layout.addWidget(already_installed_label)
|
||||
|
||||
already_installed_list = QLabel(chr(10).join(already_installed_games), dialog)
|
||||
layout.addWidget(already_installed_list)
|
||||
|
||||
# 添加一些间距
|
||||
layout.addSpacing(10)
|
||||
|
||||
# 添加"请选择你需要安装补丁的游戏"的标签
|
||||
info_label = QLabel("请选择你需要安装补丁的游戏:", dialog)
|
||||
info_label.setFont(QFont(info_label.font().family(), info_label.font().pointSize(), QFont.Bold))
|
||||
layout.addWidget(info_label)
|
||||
|
||||
# 添加列表控件
|
||||
list_widget = QListWidget(dialog)
|
||||
list_widget.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) # 允许多选
|
||||
for game in installable_games:
|
||||
list_widget.addItem(game)
|
||||
layout.addWidget(list_widget)
|
||||
|
||||
# 添加全选按钮
|
||||
select_all_btn = QPushButton("全选", dialog)
|
||||
select_all_btn.clicked.connect(lambda: list_widget.selectAll())
|
||||
layout.addWidget(select_all_btn)
|
||||
|
||||
# 添加确定和取消按钮
|
||||
buttons_layout = QHBoxLayout()
|
||||
ok_button = QPushButton("确定", dialog)
|
||||
cancel_button = QPushButton("取消", dialog)
|
||||
buttons_layout.addWidget(ok_button)
|
||||
buttons_layout.addWidget(cancel_button)
|
||||
layout.addLayout(buttons_layout)
|
||||
|
||||
# 连接按钮事件
|
||||
ok_button.clicked.connect(dialog.accept)
|
||||
cancel_button.clicked.connect(dialog.reject)
|
||||
|
||||
# 显示对话框并等待用户选择
|
||||
result = dialog.exec()
|
||||
|
||||
if result != QDialog.DialogCode.Accepted or list_widget.selectedItems() == []:
|
||||
# 用户取消或未选择任何游戏
|
||||
self.main_window.setEnabled(True)
|
||||
self.main_window.ui.start_install_text.setText("开始安装")
|
||||
return
|
||||
|
||||
# 获取用户选择的游戏
|
||||
selected_games = [item.text() for item in list_widget.selectedItems()]
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 用户选择了以下游戏进行安装: {selected_games}")
|
||||
|
||||
# 过滤game_dirs,只保留选中的游戏
|
||||
selected_game_dirs = {game: game_dirs[game] for game in selected_games if game in game_dirs}
|
||||
|
||||
# 重新禁用窗口
|
||||
self.main_window.setEnabled(False)
|
||||
|
||||
# 获取下载配置
|
||||
config = self.get_download_url()
|
||||
if not config:
|
||||
QtWidgets.QMessageBox.critical(
|
||||
self.main_window, f"错误 - {APP_NAME}", "\n网络状态异常或服务器故障,请重试\n"
|
||||
)
|
||||
# 网络故障时,恢复主窗口
|
||||
self.main_window.setEnabled(True)
|
||||
self.main_window.ui.start_install_text.setText("开始安装")
|
||||
return
|
||||
|
||||
# 填充下载队列,传入选定的游戏目录
|
||||
self._fill_download_queue(config, selected_game_dirs)
|
||||
|
||||
# 如果没有需要下载的内容,直接进行最终校验
|
||||
if not self.download_queue:
|
||||
self.main_window.after_hash_compare()
|
||||
return
|
||||
|
||||
# 询问是否使用Cloudflare加速
|
||||
self._show_cloudflare_option()
|
||||
|
||||
def _fill_download_queue(self, config, game_dirs):
|
||||
"""填充下载队列
|
||||
|
||||
Args:
|
||||
config: 包含下载URL的配置字典
|
||||
game_dirs: 包含游戏文件夹路径的字典
|
||||
"""
|
||||
# 清空现有队列
|
||||
self.download_queue.clear()
|
||||
|
||||
# 创建下载历史记录列表,用于跟踪本次安装的游戏
|
||||
if not hasattr(self.main_window, 'download_queue_history'):
|
||||
self.main_window.download_queue_history = []
|
||||
|
||||
debug_mode = self.is_debug_mode()
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 填充下载队列, 游戏目录: {game_dirs}")
|
||||
|
||||
# 添加nekopara 1-4
|
||||
for i in range(1, 5):
|
||||
game_version = f"NEKOPARA Vol.{i}"
|
||||
# 只处理game_dirs中包含的游戏版本(如果用户选择了特定版本)
|
||||
if game_version in game_dirs and not self.main_window.installed_status.get(game_version, False):
|
||||
url = config.get(f"vol{i}")
|
||||
if not url: continue
|
||||
|
||||
# 获取识别到的游戏文件夹路径
|
||||
game_folder = game_dirs[game_version]
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 使用识别到的游戏目录 {game_version}: {game_folder}")
|
||||
|
||||
_7z_path = os.path.join(PLUGIN, f"vol.{i}.7z")
|
||||
plugin_path = os.path.join(PLUGIN, GAME_INFO[game_version]["plugin_path"])
|
||||
self.download_queue.append((url, game_folder, game_version, _7z_path, plugin_path))
|
||||
# 记录到下载历史
|
||||
self.main_window.download_queue_history.append(game_version)
|
||||
|
||||
# 添加nekopara after
|
||||
game_version = "NEKOPARA After"
|
||||
# 只处理game_dirs中包含的游戏版本(如果用户选择了特定版本)
|
||||
if game_version in game_dirs and not self.main_window.installed_status.get(game_version, False):
|
||||
url = config.get("after")
|
||||
if url:
|
||||
# 获取识别到的游戏文件夹路径
|
||||
game_folder = game_dirs[game_version]
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 使用识别到的游戏目录 {game_version}: {game_folder}")
|
||||
|
||||
_7z_path = os.path.join(PLUGIN, "after.7z")
|
||||
plugin_path = os.path.join(PLUGIN, GAME_INFO[game_version]["plugin_path"])
|
||||
self.download_queue.append((url, game_folder, game_version, _7z_path, plugin_path))
|
||||
# 记录到下载历史
|
||||
self.main_window.download_queue_history.append(game_version)
|
||||
|
||||
def _show_cloudflare_option(self):
|
||||
"""显示Cloudflare加速选择对话框"""
|
||||
# 临时启用窗口以显示对话框
|
||||
self.main_window.setEnabled(True)
|
||||
|
||||
msg_box = QtWidgets.QMessageBox(self.main_window)
|
||||
msg_box.setWindowTitle(f"下载优化 - {APP_NAME}")
|
||||
msg_box.setText("是否愿意通过Cloudflare加速来优化下载速度?\n\n这将临时修改系统的hosts文件,并需要管理员权限。\n如您的杀毒软件提醒有软件正在修改hosts文件,请注意放行。")
|
||||
|
||||
# 设置Cloudflare图标
|
||||
cf_icon_path = resource_path("IMG/ICO/cloudflare_logo_icon.ico")
|
||||
if os.path.exists(cf_icon_path):
|
||||
cf_pixmap = QPixmap(cf_icon_path)
|
||||
if not cf_pixmap.isNull():
|
||||
msg_box.setWindowIcon(QIcon(cf_pixmap))
|
||||
msg_box.setIconPixmap(cf_pixmap.scaled(64, 64, Qt.AspectRatioMode.KeepAspectRatio,
|
||||
Qt.TransformationMode.SmoothTransformation))
|
||||
else:
|
||||
msg_box.setIcon(QtWidgets.QMessageBox.Icon.Question)
|
||||
|
||||
yes_button = msg_box.addButton("是,开启加速", QtWidgets.QMessageBox.ButtonRole.YesRole)
|
||||
no_button = msg_box.addButton("否,直接下载", QtWidgets.QMessageBox.ButtonRole.NoRole)
|
||||
cancel_button = msg_box.addButton("取消安装", QtWidgets.QMessageBox.ButtonRole.RejectRole)
|
||||
|
||||
msg_box.exec()
|
||||
|
||||
clicked_button = msg_box.clickedButton()
|
||||
if clicked_button == cancel_button:
|
||||
# 用户取消了安装,保持主窗口启用
|
||||
self.main_window.setEnabled(True)
|
||||
self.main_window.ui.start_install_text.setText("开始安装")
|
||||
self.download_queue.clear() # 清空下载队列
|
||||
return
|
||||
|
||||
# 用户点击了继续按钮,重新禁用主窗口
|
||||
self.main_window.setEnabled(False)
|
||||
|
||||
use_optimization = clicked_button == yes_button
|
||||
|
||||
if use_optimization and not self.cloudflare_optimizer.is_optimization_done():
|
||||
first_url = self.download_queue[0][0]
|
||||
# 保存当前URL供CloudflareOptimizer使用
|
||||
self.main_window.current_url = first_url
|
||||
# 使用CloudflareOptimizer进行IP优化
|
||||
self.cloudflare_optimizer.start_ip_optimization(first_url)
|
||||
# 等待CloudflareOptimizer的回调
|
||||
# on_optimization_finished会被调用,然后决定是否继续
|
||||
QtCore.QTimer.singleShot(100, self.check_optimization_status)
|
||||
else:
|
||||
# 如果用户选择不优化,或已经优化过,直接开始下载
|
||||
self.next_download_task()
|
||||
|
||||
def check_optimization_status(self):
|
||||
"""检查IP优化状态并继续下载流程"""
|
||||
# 必须同时满足:优化已完成且倒计时已结束
|
||||
if self.cloudflare_optimizer.is_optimization_done() and self.cloudflare_optimizer.is_countdown_finished():
|
||||
self.next_download_task()
|
||||
else:
|
||||
# 否则,继续等待100ms后再次检查
|
||||
QtCore.QTimer.singleShot(100, self.check_optimization_status)
|
||||
|
||||
def next_download_task(self):
|
||||
"""处理下载队列中的下一个任务"""
|
||||
if not self.download_queue:
|
||||
self.main_window.after_hash_compare()
|
||||
return
|
||||
|
||||
# 检查下载线程是否仍在运行,以避免在手动停止后立即开始下一个任务
|
||||
if self.download_task_manager.current_download_thread and self.download_task_manager.current_download_thread.isRunning():
|
||||
return
|
||||
|
||||
# 获取下一个下载任务并开始
|
||||
url, game_folder, game_version, _7z_path, plugin_path = self.download_queue.popleft()
|
||||
self.download_setting(url, game_folder, game_version, _7z_path, plugin_path)
|
||||
|
||||
def download_setting(self, url, game_folder, game_version, _7z_path, plugin_path):
|
||||
"""准备下载特定游戏版本
|
||||
|
||||
Args:
|
||||
url: 下载URL
|
||||
game_folder: 游戏文件夹路径
|
||||
game_version: 游戏版本名称
|
||||
_7z_path: 7z文件保存路径
|
||||
plugin_path: 插件路径
|
||||
"""
|
||||
# 使用改进的目录识别获取安装路径
|
||||
install_paths = self.get_install_paths()
|
||||
|
||||
debug_mode = self.is_debug_mode()
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 准备下载游戏 {game_version}")
|
||||
print(f"DEBUG: 游戏文件夹: {game_folder}")
|
||||
|
||||
# 游戏可执行文件已在填充下载队列时验证过,不需要再次检查
|
||||
# 因为game_folder是从已验证的game_dirs中获取的
|
||||
game_exe_exists = True
|
||||
|
||||
# 检查游戏是否已安装
|
||||
if (
|
||||
not game_exe_exists
|
||||
or self.main_window.installed_status[game_version]
|
||||
):
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 跳过下载游戏 {game_version}")
|
||||
print(f"DEBUG: 游戏存在: {game_exe_exists}")
|
||||
print(f"DEBUG: 已安装补丁: {self.main_window.installed_status[game_version]}")
|
||||
self.main_window.installed_status[game_version] = False if not game_exe_exists else True
|
||||
self.next_download_task()
|
||||
return
|
||||
|
||||
# 创建进度窗口并开始下载
|
||||
self.main_window.progress_window = self.main_window.create_progress_window()
|
||||
|
||||
# 从CloudflareOptimizer获取已优选的IP
|
||||
self.optimized_ip = self.cloudflare_optimizer.get_optimized_ip()
|
||||
if self.optimized_ip:
|
||||
print(f"已为 {game_version} 获取到优选IP: {self.optimized_ip}")
|
||||
else:
|
||||
print(f"未能为 {game_version} 获取优选IP,将使用默认线路。")
|
||||
|
||||
# 使用DownloadTaskManager开始下载
|
||||
self.download_task_manager.start_download(url, _7z_path, game_version, game_folder, plugin_path)
|
||||
|
||||
# 连接到主窗口中的下载完成处理函数
|
||||
def on_download_finished(self, success, error, url, game_folder, game_version, _7z_path, plugin_path):
|
||||
"""下载完成后的处理
|
||||
|
||||
Args:
|
||||
success: 是否下载成功
|
||||
error: 错误信息
|
||||
url: 下载URL
|
||||
game_folder: 游戏文件夹路径
|
||||
game_version: 游戏版本名称
|
||||
_7z_path: 7z文件保存路径
|
||||
plugin_path: 插件路径
|
||||
"""
|
||||
# 关闭进度窗口
|
||||
if self.main_window.progress_window and self.main_window.progress_window.isVisible():
|
||||
self.main_window.progress_window.reject()
|
||||
self.main_window.progress_window = None
|
||||
|
||||
# 处理下载失败
|
||||
if not success:
|
||||
print(f"--- Download Failed: {game_version} ---")
|
||||
print(error)
|
||||
print("------------------------------------")
|
||||
|
||||
# 临时启用窗口以显示对话框
|
||||
self.main_window.setEnabled(True)
|
||||
|
||||
msg_box = QtWidgets.QMessageBox(self.main_window)
|
||||
msg_box.setWindowTitle(f"下载失败 - {APP_NAME}")
|
||||
msg_box.setText(f"\n文件获取失败: {game_version}\n错误: {error}\n\n是否重试?")
|
||||
|
||||
retry_button = msg_box.addButton("重试", QtWidgets.QMessageBox.ButtonRole.YesRole)
|
||||
next_button = msg_box.addButton("下一个", QtWidgets.QMessageBox.ButtonRole.NoRole)
|
||||
end_button = msg_box.addButton("结束", QtWidgets.QMessageBox.ButtonRole.RejectRole)
|
||||
|
||||
msg_box.exec()
|
||||
clicked_button = msg_box.clickedButton()
|
||||
|
||||
# 处理用户选择
|
||||
if clicked_button == retry_button:
|
||||
# 重试,重新禁用窗口
|
||||
self.main_window.setEnabled(False)
|
||||
self.download_setting(url, game_folder, game_version, _7z_path, plugin_path)
|
||||
elif clicked_button == next_button:
|
||||
# 继续下一个,重新禁用窗口
|
||||
self.main_window.setEnabled(False)
|
||||
self.next_download_task()
|
||||
else:
|
||||
# 结束,保持窗口启用
|
||||
self.on_download_stopped()
|
||||
return
|
||||
|
||||
# 下载成功,使用ExtractionHandler开始解压缩
|
||||
self.extraction_handler.start_extraction(_7z_path, game_folder, plugin_path, game_version)
|
||||
# extraction_handler的回调会处理下一步操作
|
||||
|
||||
def on_extraction_finished(self, continue_download):
|
||||
"""解压完成后的回调,决定是否继续下载队列
|
||||
|
||||
Args:
|
||||
continue_download: 是否继续下载队列中的下一个任务
|
||||
"""
|
||||
if continue_download:
|
||||
# 继续下一个下载任务
|
||||
self.next_download_task()
|
||||
else:
|
||||
# 清空剩余队列并显示结果
|
||||
self.download_queue.clear()
|
||||
self.main_window.show_result()
|
||||
|
||||
def on_download_stopped(self):
|
||||
"""当用户点击停止按钮或选择结束时调用的函数"""
|
||||
# 停止IP优化线程
|
||||
self.cloudflare_optimizer.stop_optimization()
|
||||
|
||||
# 停止当前可能仍在运行的下载线程
|
||||
self.download_task_manager.stop_download()
|
||||
|
||||
# 清空下载队列,因为用户决定停止
|
||||
self.download_queue.clear()
|
||||
|
||||
# 确保进度窗口已关闭
|
||||
if hasattr(self.main_window, 'progress_window') and self.main_window.progress_window:
|
||||
if self.main_window.progress_window.isVisible():
|
||||
self.main_window.progress_window.reject()
|
||||
self.main_window.progress_window = None
|
||||
|
||||
# 退出应用程序
|
||||
print("下载已全部停止。")
|
||||
|
||||
# 恢复主窗口状态
|
||||
self.main_window.setEnabled(True)
|
||||
self.main_window.ui.start_install_text.setText("开始安装")
|
||||
|
||||
# 显示取消安装的消息
|
||||
QtWidgets.QMessageBox.information(
|
||||
self.main_window,
|
||||
f"已取消 - {APP_NAME}",
|
||||
"\n已成功取消安装进程。\n"
|
||||
)
|
||||
|
||||
# 以下方法委托给DownloadTaskManager
|
||||
def get_download_thread_count(self):
|
||||
"""获取当前下载线程设置对应的线程数"""
|
||||
return self.download_task_manager.get_download_thread_count()
|
||||
|
||||
def set_download_thread_level(self, level):
|
||||
"""设置下载线程级别"""
|
||||
return self.download_task_manager.set_download_thread_level(level)
|
||||
|
||||
def show_download_thread_settings(self):
|
||||
"""显示下载线程设置对话框"""
|
||||
return self.download_task_manager.show_download_thread_settings()
|
||||
@@ -1,81 +0,0 @@
|
||||
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)
|
||||
10
source/core/handlers/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
||||
# Handlers package initialization
|
||||
from .extraction_handler import ExtractionHandler
|
||||
from .patch_toggle_handler import PatchToggleHandler
|
||||
from .uninstall_handler import UninstallHandler
|
||||
|
||||
__all__ = [
|
||||
'ExtractionHandler',
|
||||
'PatchToggleHandler',
|
||||
'UninstallHandler',
|
||||
]
|
||||
267
source/core/handlers/extraction_handler.py
Normal file
@@ -0,0 +1,267 @@
|
||||
import os
|
||||
import shutil
|
||||
from PySide6 import QtWidgets
|
||||
from PySide6.QtWidgets import QMessageBox
|
||||
from PySide6.QtCore import QTimer, QCoreApplication
|
||||
|
||||
from utils.logger import setup_logger
|
||||
from workers.extraction_thread import ExtractionThread
|
||||
|
||||
# 初始化logger
|
||||
logger = setup_logger("extraction_handler")
|
||||
|
||||
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 ""
|
||||
self.extraction_progress_window = None
|
||||
|
||||
def start_extraction(self, _7z_path, game_folder, plugin_path, game_version, extracted_path=None):
|
||||
"""开始解压任务
|
||||
|
||||
Args:
|
||||
_7z_path: 7z文件路径
|
||||
game_folder: 游戏文件夹路径
|
||||
plugin_path: 插件路径
|
||||
game_version: 游戏版本名称
|
||||
extracted_path: 已解压的补丁文件路径,如果提供则直接使用它而不进行解压
|
||||
"""
|
||||
# 检查是否处于离线模式
|
||||
is_offline = False
|
||||
if hasattr(self.main_window, 'offline_mode_manager'):
|
||||
is_offline = self.main_window.offline_mode_manager.is_in_offline_mode()
|
||||
|
||||
# 创建并显示解压进度窗口,替代原来的消息框
|
||||
self.extraction_progress_window = self.main_window.create_extraction_progress_window()
|
||||
self.extraction_progress_window.show()
|
||||
|
||||
# 确保UI更新
|
||||
QCoreApplication.processEvents()
|
||||
|
||||
# 创建并启动解压线程
|
||||
self.main_window.extraction_thread = ExtractionThread(
|
||||
_7z_path, game_folder, plugin_path, game_version, self.main_window, extracted_path
|
||||
)
|
||||
|
||||
# 连接进度信号
|
||||
self.main_window.extraction_thread.progress.connect(self.update_extraction_progress)
|
||||
|
||||
# 连接完成信号
|
||||
self.main_window.extraction_thread.finished.connect(self.on_extraction_finished_with_hash_check)
|
||||
|
||||
# 启动线程
|
||||
self.main_window.extraction_thread.start()
|
||||
|
||||
def update_extraction_progress(self, progress, status_text):
|
||||
"""更新解压进度
|
||||
|
||||
Args:
|
||||
progress: 进度百分比
|
||||
status_text: 状态文本
|
||||
"""
|
||||
if self.extraction_progress_window and hasattr(self.extraction_progress_window, 'progress_bar'):
|
||||
self.extraction_progress_window.progress_bar.setValue(progress)
|
||||
self.extraction_progress_window.status_label.setText(status_text)
|
||||
|
||||
# 确保UI更新
|
||||
QCoreApplication.processEvents()
|
||||
|
||||
def on_extraction_finished_with_hash_check(self, success, error_message, game_version):
|
||||
"""解压完成后进行哈希校验
|
||||
|
||||
Args:
|
||||
success: 是否解压成功
|
||||
error_message: 错误信息
|
||||
game_version: 游戏版本
|
||||
"""
|
||||
# 关闭解压进度窗口
|
||||
if self.extraction_progress_window:
|
||||
self.extraction_progress_window.close()
|
||||
self.extraction_progress_window = 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:
|
||||
# 用户选择停止,保持窗口启用状态
|
||||
if hasattr(self.main_window, 'window_manager'):
|
||||
self.main_window.window_manager.change_window_state(self.main_window.window_manager.STATE_READY)
|
||||
# 通知DownloadManager停止下载队列
|
||||
self.main_window.download_manager.on_extraction_finished(False)
|
||||
return
|
||||
|
||||
# 解压成功,进行哈希校验
|
||||
self._perform_hash_check(game_version)
|
||||
|
||||
def _perform_hash_check(self, game_version):
|
||||
"""解压成功后进行哈希校验
|
||||
|
||||
Args:
|
||||
game_version: 游戏版本
|
||||
"""
|
||||
# 导入所需模块
|
||||
from config.config import GAME_INFO, PLUGIN_HASH
|
||||
from workers.hash_thread import HashThread
|
||||
|
||||
# 获取安装路径
|
||||
install_paths = {}
|
||||
if hasattr(self.main_window, 'game_detector') and hasattr(self.main_window, 'download_manager'):
|
||||
game_dirs = self.main_window.game_detector.identify_game_directories_improved(
|
||||
self.main_window.download_manager.selected_folder
|
||||
)
|
||||
|
||||
for game, info in GAME_INFO.items():
|
||||
if game in game_dirs and game == game_version:
|
||||
game_dir = game_dirs[game]
|
||||
install_path = os.path.join(game_dir, os.path.basename(info["install_path"]))
|
||||
install_paths[game] = install_path
|
||||
break
|
||||
|
||||
if not install_paths:
|
||||
# 如果找不到安装路径,直接认为安装成功
|
||||
logger.warning(f"未找到 {game_version} 的安装路径,跳过哈希校验")
|
||||
self.main_window.installed_status[game_version] = True
|
||||
self.main_window.download_manager.on_extraction_finished(True)
|
||||
return
|
||||
|
||||
# 关闭可能存在的哈希校验窗口
|
||||
self.main_window.close_hash_msg_box()
|
||||
|
||||
# 显示哈希校验窗口
|
||||
self.main_window.hash_msg_box = self.main_window.hash_manager.hash_pop_window(
|
||||
check_type="post",
|
||||
auto_close=True, # 添加自动关闭参数
|
||||
close_delay=1000 # 1秒后自动关闭
|
||||
)
|
||||
|
||||
# 直接创建并启动哈希线程进行校验
|
||||
hash_thread = HashThread(
|
||||
"after",
|
||||
install_paths,
|
||||
PLUGIN_HASH,
|
||||
self.main_window.installed_status,
|
||||
self.main_window
|
||||
)
|
||||
hash_thread.after_finished.connect(self.on_hash_check_finished)
|
||||
|
||||
# 保存引用以便后续使用
|
||||
self.hash_thread = hash_thread
|
||||
hash_thread.start()
|
||||
|
||||
def on_hash_check_finished(self, result):
|
||||
"""哈希校验完成后的处理
|
||||
|
||||
Args:
|
||||
result: 校验结果,包含通过状态、游戏版本和消息
|
||||
"""
|
||||
# 导入所需模块
|
||||
from config.config import GAME_INFO
|
||||
|
||||
# 关闭哈希检查窗口
|
||||
self.main_window.close_hash_msg_box()
|
||||
|
||||
if not result["passed"]:
|
||||
# 校验失败,删除已解压的文件并提示重新下载
|
||||
game_version = result["game"]
|
||||
error_message = result["message"]
|
||||
|
||||
# 临时启用窗口以显示错误消息
|
||||
self.main_window.setEnabled(True)
|
||||
|
||||
# 获取安装路径
|
||||
install_path = None
|
||||
if hasattr(self.main_window, 'game_detector') and hasattr(self.main_window, 'download_manager'):
|
||||
game_dirs = self.main_window.game_detector.identify_game_directories_improved(
|
||||
self.main_window.download_manager.selected_folder
|
||||
)
|
||||
|
||||
if game_version in game_dirs and game_version in GAME_INFO:
|
||||
game_dir = game_dirs[game_version]
|
||||
install_path = os.path.join(game_dir, os.path.basename(GAME_INFO[game_version]["install_path"]))
|
||||
|
||||
# 如果找到安装路径,尝试删除已解压的文件
|
||||
if install_path and os.path.exists(install_path):
|
||||
try:
|
||||
os.remove(install_path)
|
||||
logger.debug(f"已删除校验失败的文件: {install_path}")
|
||||
except Exception as e:
|
||||
logger.error(f"删除文件失败: {e}")
|
||||
|
||||
# 显示错误消息并询问是否重试
|
||||
reply = QtWidgets.QMessageBox.question(
|
||||
self.main_window,
|
||||
f"校验失败 - {self.APP_NAME}",
|
||||
f"{error_message}\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.main_window.installed_status[game_version] = False
|
||||
|
||||
# 获取游戏目录和下载URL
|
||||
if hasattr(self.main_window, 'download_manager') and hasattr(self.main_window, 'game_detector'):
|
||||
game_dirs = self.main_window.game_detector.identify_game_directories_improved(
|
||||
self.main_window.download_manager.selected_folder
|
||||
)
|
||||
|
||||
if game_version in game_dirs:
|
||||
# 重新将游戏添加到下载队列
|
||||
self.main_window.download_manager.download_queue.appendleft([game_version])
|
||||
# 继续下一个下载任务
|
||||
self.main_window.download_manager.next_download_task()
|
||||
else:
|
||||
# 如果找不到游戏目录,继续下一个
|
||||
self.main_window.download_manager.on_extraction_finished(True)
|
||||
else:
|
||||
# 如果无法重新下载,继续下一个
|
||||
self.main_window.download_manager.on_extraction_finished(True)
|
||||
else:
|
||||
# 用户选择不重试,继续下一个
|
||||
self.main_window.installed_status[game_version] = False
|
||||
self.main_window.download_manager.on_extraction_finished(True)
|
||||
else:
|
||||
# 校验通过,更新安装状态
|
||||
game_version = result["game"]
|
||||
self.main_window.installed_status[game_version] = True
|
||||
# 通知DownloadManager继续下一个下载任务
|
||||
self.main_window.download_manager.on_extraction_finished(True)
|
||||
|
||||
def on_extraction_finished(self, success, error_message, game_version):
|
||||
"""兼容旧版本的回调函数
|
||||
|
||||
Args:
|
||||
success: 是否解压成功
|
||||
error_message: 错误信息
|
||||
game_version: 游戏版本
|
||||
"""
|
||||
# 调用新的带哈希校验的回调函数
|
||||
self.on_extraction_finished_with_hash_check(success, error_message, game_version)
|
||||
438
source/core/handlers/patch_toggle_handler.py
Normal file
@@ -0,0 +1,438 @@
|
||||
import os
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog, QVBoxLayout, QLabel, QListWidget, QPushButton, QHBoxLayout,
|
||||
QAbstractItemView, QRadioButton, QButtonGroup, QFileDialog, QMessageBox
|
||||
)
|
||||
from PySide6.QtCore import QObject, Signal, QThread
|
||||
from PySide6.QtGui import QFont
|
||||
from utils import msgbox_frame
|
||||
from utils.logger import setup_logger
|
||||
|
||||
# 初始化logger
|
||||
logger = setup_logger("patch_toggle_handler")
|
||||
|
||||
class PatchToggleThread(QThread):
|
||||
"""在后台线程中处理补丁切换逻辑"""
|
||||
finished = Signal(object)
|
||||
|
||||
def __init__(self, handler, selected_folder):
|
||||
super().__init__()
|
||||
self.handler = handler
|
||||
self.selected_folder = selected_folder
|
||||
|
||||
def run(self):
|
||||
# 在后台线程中执行耗时操作
|
||||
game_dirs = self.handler.game_detector.identify_game_directories_improved(self.selected_folder)
|
||||
self.finished.emit(game_dirs)
|
||||
|
||||
class PatchToggleHandler(QObject):
|
||||
"""
|
||||
处理补丁启用/禁用功能的类
|
||||
"""
|
||||
def __init__(self, main_window):
|
||||
"""
|
||||
初始化补丁切换处理程序
|
||||
|
||||
Args:
|
||||
main_window: 主窗口实例,用于访问其他组件
|
||||
"""
|
||||
super().__init__()
|
||||
self.main_window = main_window
|
||||
self.debug_manager = main_window.debug_manager
|
||||
self.game_detector = main_window.game_detector
|
||||
self.patch_manager = main_window.patch_manager
|
||||
self.app_name = main_window.patch_manager.app_name
|
||||
self.toggle_thread = None
|
||||
|
||||
def handle_toggle_patch_button_click(self):
|
||||
"""
|
||||
处理禁/启用补丁按钮点击事件
|
||||
打开文件选择对话框选择游戏目录,然后禁用或启用对应游戏的补丁
|
||||
"""
|
||||
selected_folder = QFileDialog.getExistingDirectory(self.main_window, "选择游戏上级目录", "")
|
||||
|
||||
if not selected_folder:
|
||||
return
|
||||
|
||||
self.main_window.show_loading_dialog("正在识别游戏目录并检查补丁状态...")
|
||||
|
||||
self.toggle_thread = PatchToggleThread(self, selected_folder)
|
||||
self.toggle_thread.finished.connect(self.on_game_detection_finished)
|
||||
self.toggle_thread.start()
|
||||
|
||||
def on_game_detection_finished(self, game_dirs):
|
||||
"""游戏识别完成后的回调"""
|
||||
self.main_window.hide_loading_dialog()
|
||||
|
||||
if not game_dirs:
|
||||
QMessageBox.information(
|
||||
self.main_window,
|
||||
f"提示 - {self.app_name}",
|
||||
"\n未在选择的目录中找到任何支持的游戏。\n",
|
||||
)
|
||||
return
|
||||
|
||||
games_with_patch = {}
|
||||
for game_version, game_dir in game_dirs.items():
|
||||
if self.patch_manager.check_patch_installed(game_dir, game_version):
|
||||
is_disabled, _ = self.patch_manager.check_patch_disabled(game_dir, game_version)
|
||||
status = "已禁用" if is_disabled else "已启用"
|
||||
games_with_patch[game_version] = {"dir": game_dir, "status": status}
|
||||
|
||||
if not games_with_patch:
|
||||
QMessageBox.information(
|
||||
self.main_window,
|
||||
f"提示 - {self.app_name}",
|
||||
"\n目录中未找到已安装补丁的游戏。\n",
|
||||
)
|
||||
return
|
||||
|
||||
selected_games, operation = self._show_multi_game_dialog(games_with_patch)
|
||||
|
||||
if not selected_games:
|
||||
return
|
||||
|
||||
selected_game_dirs = {game: games_with_patch[game]["dir"] for game in selected_games if game in games_with_patch}
|
||||
|
||||
self._execute_batch_toggle(selected_game_dirs, operation, self.debug_manager._is_debug_mode)
|
||||
|
||||
def _handle_multiple_games(self, game_dirs, debug_mode):
|
||||
"""
|
||||
处理多个游戏的补丁切换
|
||||
|
||||
Args:
|
||||
game_dirs: 游戏目录字典
|
||||
debug_mode: 是否为调试模式
|
||||
"""
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 禁/启用功能 - 在上级目录中找到以下游戏: {list(game_dirs.keys())}")
|
||||
|
||||
# 查找已安装补丁的游戏,只处理那些已安装补丁的游戏
|
||||
games_with_patch = {}
|
||||
for game_version, game_dir in game_dirs.items():
|
||||
if self.patch_manager.check_patch_installed(game_dir, game_version):
|
||||
# 检查补丁当前状态(是否禁用)
|
||||
is_disabled, disabled_path = self.patch_manager.check_patch_disabled(game_dir, game_version)
|
||||
status = "已禁用" if is_disabled else "已启用"
|
||||
games_with_patch[game_version] = {
|
||||
"dir": game_dir,
|
||||
"disabled": is_disabled,
|
||||
"status": status
|
||||
}
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 禁/启用功能 - {game_version} 已安装补丁,当前状态: {status}")
|
||||
|
||||
# 检查是否有已安装补丁的游戏
|
||||
if not games_with_patch:
|
||||
QMessageBox.information(
|
||||
self.main_window,
|
||||
f"提示 - {self.app_name}",
|
||||
"\n未在选择的目录中找到已安装补丁的游戏。\n请确认您选择了正确的游戏目录,并且该目录中的游戏已安装过补丁。\n",
|
||||
QMessageBox.StandardButton.Ok
|
||||
)
|
||||
return
|
||||
|
||||
# 显示选择对话框
|
||||
selected_games, operation = self._show_multi_game_dialog(games_with_patch)
|
||||
|
||||
if not selected_games:
|
||||
return # 用户取消了操作
|
||||
|
||||
# 过滤games_with_patch,只保留选中的游戏
|
||||
selected_game_dirs = {}
|
||||
for game in selected_games:
|
||||
if game in games_with_patch:
|
||||
selected_game_dirs[game] = games_with_patch[game]["dir"]
|
||||
|
||||
# 确认操作
|
||||
operation_text = "禁用" if operation == "disable" else "启用" if operation == "enable" else "切换"
|
||||
game_list = '\n'.join([f"{game} ({games_with_patch[game]['status']})" for game in selected_games])
|
||||
reply = QMessageBox.question(
|
||||
self.main_window,
|
||||
f"确认{operation_text}操作 - {self.app_name}",
|
||||
f"\n确定要{operation_text}以下游戏补丁吗?\n\n{game_list}\n",
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||
QMessageBox.StandardButton.No
|
||||
)
|
||||
|
||||
if reply == QMessageBox.StandardButton.No:
|
||||
return
|
||||
|
||||
# 执行批量操作
|
||||
self._execute_batch_toggle(selected_game_dirs, operation, debug_mode)
|
||||
|
||||
def _handle_single_game(self, selected_folder, debug_mode):
|
||||
"""
|
||||
处理单个游戏的补丁切换
|
||||
|
||||
Args:
|
||||
selected_folder: 选择的游戏目录
|
||||
debug_mode: 是否为调试模式
|
||||
"""
|
||||
# 未找到游戏目录,尝试将选择的目录作为游戏目录
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 禁/启用功能 - 未在上级目录找到游戏,尝试将选择的目录视为游戏目录")
|
||||
|
||||
game_version = self.game_detector.identify_game_version(selected_folder)
|
||||
|
||||
if game_version:
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 禁/启用功能 - 识别为游戏: {game_version}")
|
||||
|
||||
# 检查是否已安装补丁
|
||||
if self.patch_manager.check_patch_installed(selected_folder, game_version):
|
||||
# 检查补丁当前状态
|
||||
is_disabled, disabled_path = self.patch_manager.check_patch_disabled(selected_folder, game_version)
|
||||
current_status = "已禁用" if is_disabled else "已启用"
|
||||
|
||||
# 显示单游戏操作对话框
|
||||
operation = self._show_single_game_dialog(game_version, current_status, is_disabled)
|
||||
|
||||
if not operation:
|
||||
return # 用户取消了操作
|
||||
|
||||
# 执行操作
|
||||
result = self.patch_manager.toggle_patch(selected_folder, game_version, operation=operation)
|
||||
if not result["success"]:
|
||||
# 操作失败的消息已在toggle_patch中显示
|
||||
pass
|
||||
else:
|
||||
# 没有安装补丁
|
||||
QMessageBox.information(
|
||||
self.main_window,
|
||||
f"提示 - {self.app_name}",
|
||||
f"\n未在 {game_version} 中找到已安装的补丁。\n请确认该游戏已经安装过补丁。\n",
|
||||
QMessageBox.StandardButton.Ok
|
||||
)
|
||||
else:
|
||||
# 两种方式都未识别到游戏
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 禁/启用功能 - 无法识别游戏")
|
||||
|
||||
msg_box = msgbox_frame(
|
||||
f"错误 - {self.app_name}",
|
||||
"\n所选目录不是有效的NEKOPARA游戏目录。\n请选择包含游戏可执行文件的目录或其上级目录。\n",
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
msg_box.exec()
|
||||
|
||||
def _show_multi_game_dialog(self, games_with_patch):
|
||||
"""
|
||||
显示多游戏选择对话框
|
||||
|
||||
Args:
|
||||
games_with_patch: 已安装补丁的游戏信息
|
||||
|
||||
Returns:
|
||||
tuple: (选择的游戏列表, 操作类型)
|
||||
"""
|
||||
dialog = QDialog(self.main_window)
|
||||
dialog.setWindowTitle("选择要操作的游戏补丁")
|
||||
dialog.resize(400, 400) # 增加高度以适应新增的单选按钮
|
||||
|
||||
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)
|
||||
|
||||
# 添加游戏列表和状态
|
||||
games_status_text = ""
|
||||
for game, info in games_with_patch.items():
|
||||
games_status_text += f"{game} ({info['status']})\n"
|
||||
games_status_label = QLabel(games_status_text.strip(), dialog)
|
||||
layout.addWidget(games_status_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, info in games_with_patch.items():
|
||||
list_widget.addItem(f"{game} ({info['status']})")
|
||||
layout.addWidget(list_widget)
|
||||
|
||||
# 添加全选按钮
|
||||
select_all_btn = QPushButton("全选", dialog)
|
||||
select_all_btn.clicked.connect(lambda: list_widget.selectAll())
|
||||
layout.addWidget(select_all_btn)
|
||||
|
||||
# 添加操作选择单选按钮
|
||||
operation_label = QLabel("请选择要执行的操作:", dialog)
|
||||
operation_label.setFont(QFont(operation_label.font().family(), operation_label.font().pointSize(), QFont.Bold))
|
||||
layout.addWidget(operation_label)
|
||||
|
||||
# 创建单选按钮组
|
||||
radio_button_group = QButtonGroup(dialog)
|
||||
|
||||
# 添加"自动切换状态"单选按钮(默认选中)
|
||||
auto_toggle_radio = QRadioButton("自动切换状态(禁用<->启用)", dialog)
|
||||
auto_toggle_radio.setChecked(True)
|
||||
radio_button_group.addButton(auto_toggle_radio, 0)
|
||||
layout.addWidget(auto_toggle_radio)
|
||||
|
||||
# 添加"全部禁用"单选按钮
|
||||
disable_all_radio = QRadioButton("禁用选中的补丁", dialog)
|
||||
radio_button_group.addButton(disable_all_radio, 1)
|
||||
layout.addWidget(disable_all_radio)
|
||||
|
||||
# 添加"全部启用"单选按钮
|
||||
enable_all_radio = QRadioButton("启用选中的补丁", dialog)
|
||||
radio_button_group.addButton(enable_all_radio, 2)
|
||||
layout.addWidget(enable_all_radio)
|
||||
|
||||
# 添加确定和取消按钮
|
||||
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 [], None
|
||||
|
||||
# 获取用户选择的游戏
|
||||
selected_items = [item.text() for item in list_widget.selectedItems()]
|
||||
selected_games = []
|
||||
|
||||
# 从选中项文本中提取游戏名称
|
||||
for item in selected_items:
|
||||
# 去除状态后缀 " (已启用)" 或 " (已禁用)"
|
||||
game_name = item.split(" (")[0]
|
||||
selected_games.append(game_name)
|
||||
|
||||
# 获取选中的操作类型
|
||||
operation = None
|
||||
if radio_button_group.checkedId() == 1: # 禁用选中的补丁
|
||||
operation = "disable"
|
||||
elif radio_button_group.checkedId() == 2: # 启用选中的补丁
|
||||
operation = "enable"
|
||||
# 否则为None,表示自动切换状态
|
||||
|
||||
return selected_games, operation
|
||||
|
||||
def _show_single_game_dialog(self, game_version, current_status, is_disabled):
|
||||
"""
|
||||
显示单游戏操作对话框
|
||||
|
||||
Args:
|
||||
game_version: 游戏版本
|
||||
current_status: 当前补丁状态
|
||||
is_disabled: 是否已禁用
|
||||
|
||||
Returns:
|
||||
str: 操作类型,"enable"或"disable",或None表示取消
|
||||
"""
|
||||
dialog = QDialog(self.main_window)
|
||||
dialog.setWindowTitle(f"{game_version} 补丁操作")
|
||||
dialog.resize(300, 200)
|
||||
|
||||
layout = QVBoxLayout(dialog)
|
||||
|
||||
# 添加当前状态标签
|
||||
status_label = QLabel(f"当前补丁状态: {current_status}", dialog)
|
||||
status_label.setFont(QFont(status_label.font().family(), status_label.font().pointSize(), QFont.Bold))
|
||||
layout.addWidget(status_label)
|
||||
|
||||
# 添加操作选择单选按钮
|
||||
operation_label = QLabel("请选择要执行的操作:", dialog)
|
||||
layout.addWidget(operation_label)
|
||||
|
||||
# 创建单选按钮组
|
||||
radio_button_group = QButtonGroup(dialog)
|
||||
|
||||
# 添加可选操作
|
||||
if is_disabled:
|
||||
# 当前是禁用状态,显示启用选项
|
||||
enable_radio = QRadioButton("启用补丁", dialog)
|
||||
enable_radio.setChecked(True)
|
||||
radio_button_group.addButton(enable_radio, 0)
|
||||
layout.addWidget(enable_radio)
|
||||
else:
|
||||
# 当前是启用状态,显示禁用选项
|
||||
disable_radio = QRadioButton("禁用补丁", dialog)
|
||||
disable_radio.setChecked(True)
|
||||
radio_button_group.addButton(disable_radio, 0)
|
||||
layout.addWidget(disable_radio)
|
||||
|
||||
# 添加确定和取消按钮
|
||||
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:
|
||||
# 用户取消
|
||||
return None
|
||||
|
||||
# 根据当前状态确定操作
|
||||
return "enable" if is_disabled else "disable"
|
||||
|
||||
def _execute_batch_toggle(self, selected_game_dirs, operation, debug_mode):
|
||||
"""
|
||||
执行批量补丁切换操作
|
||||
|
||||
Args:
|
||||
selected_game_dirs: 选择的游戏目录
|
||||
operation: 操作类型
|
||||
debug_mode: 是否为调试模式
|
||||
"""
|
||||
success_count = 0
|
||||
fail_count = 0
|
||||
results = []
|
||||
|
||||
for game_version, game_dir in selected_game_dirs.items():
|
||||
try:
|
||||
# 使用静默模式进行操作
|
||||
result = self.patch_manager.toggle_patch(game_dir, game_version, operation=operation, silent=True)
|
||||
|
||||
if result["success"]:
|
||||
success_count += 1
|
||||
else:
|
||||
fail_count += 1
|
||||
|
||||
results.append({
|
||||
"version": game_version,
|
||||
"success": result["success"],
|
||||
"message": result["message"],
|
||||
"action": result["action"]
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
if debug_mode:
|
||||
logger.error(f"DEBUG: 切换 {game_version} 补丁状态时出错: {str(e)}")
|
||||
fail_count += 1
|
||||
results.append({
|
||||
"version": game_version,
|
||||
"success": False,
|
||||
"message": f"操作出错: {str(e)}",
|
||||
"action": "none"
|
||||
})
|
||||
|
||||
# 显示操作结果
|
||||
self.patch_manager.show_toggle_result(success_count, fail_count, results)
|
||||
396
source/core/handlers/uninstall_handler.py
Normal file
@@ -0,0 +1,396 @@
|
||||
import os
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog, QVBoxLayout, QLabel, QListWidget, QPushButton, QHBoxLayout,
|
||||
QAbstractItemView, QFileDialog, QMessageBox
|
||||
)
|
||||
from PySide6.QtCore import QObject, Signal, QThread
|
||||
from PySide6.QtGui import QFont
|
||||
from utils import msgbox_frame
|
||||
from utils.logger import setup_logger
|
||||
|
||||
# 初始化logger
|
||||
logger = setup_logger("uninstall_handler")
|
||||
|
||||
class UninstallThread(QThread):
|
||||
"""在后台线程中处理卸载逻辑"""
|
||||
finished = Signal(object)
|
||||
|
||||
def __init__(self, handler, selected_folder):
|
||||
super().__init__()
|
||||
self.handler = handler
|
||||
self.selected_folder = selected_folder
|
||||
|
||||
def run(self):
|
||||
# 在后台线程中执行耗时操作
|
||||
game_dirs = self.handler.game_detector.identify_game_directories_improved(self.selected_folder)
|
||||
self.finished.emit(game_dirs)
|
||||
|
||||
class UninstallHandler(QObject):
|
||||
"""
|
||||
处理补丁卸载功能的类
|
||||
"""
|
||||
def __init__(self, main_window):
|
||||
"""
|
||||
初始化卸载处理程序
|
||||
|
||||
Args:
|
||||
main_window: 主窗口实例,用于访问其他组件
|
||||
"""
|
||||
super().__init__()
|
||||
self.main_window = main_window
|
||||
self.debug_manager = main_window.debug_manager
|
||||
self.game_detector = main_window.game_detector
|
||||
self.patch_manager = main_window.patch_manager
|
||||
self.app_name = main_window.patch_manager.app_name
|
||||
self.uninstall_thread = None
|
||||
|
||||
# 记录初始化日志
|
||||
debug_mode = self.debug_manager._is_debug_mode() if hasattr(self.debug_manager, '_is_debug_mode') else False
|
||||
if debug_mode:
|
||||
logger.debug("DEBUG: 卸载处理程序已初始化")
|
||||
|
||||
def handle_uninstall_button_click(self):
|
||||
"""
|
||||
处理卸载补丁按钮点击事件
|
||||
打开文件选择对话框选择游戏目录,然后卸载对应游戏的补丁
|
||||
"""
|
||||
# 获取游戏目录
|
||||
debug_mode = self.debug_manager._is_debug_mode()
|
||||
|
||||
logger.info("用户点击了卸载补丁按钮")
|
||||
if debug_mode:
|
||||
logger.debug("DEBUG: 处理卸载补丁按钮点击事件")
|
||||
|
||||
# 提示用户选择目录
|
||||
file_dialog_info = "选择游戏上级目录" if debug_mode else "选择游戏目录"
|
||||
selected_folder = QFileDialog.getExistingDirectory(self.main_window, file_dialog_info, "")
|
||||
|
||||
if not selected_folder or selected_folder == "":
|
||||
logger.info("用户取消了目录选择")
|
||||
if debug_mode:
|
||||
logger.debug("DEBUG: 用户取消了目录选择,退出卸载流程")
|
||||
return # 用户取消了选择
|
||||
|
||||
logger.info(f"用户选择了目录: {selected_folder}")
|
||||
if debug_mode:
|
||||
logger.debug(f"卸载功能 - 用户选择了目录: {selected_folder}")
|
||||
|
||||
# 使用UI管理器显示加载对话框
|
||||
if hasattr(self.main_window, 'ui_manager'):
|
||||
self.main_window.ui_manager.show_loading_dialog("正在识别游戏目录...")
|
||||
else:
|
||||
logger.warning("无法访问UI管理器,无法显示加载对话框")
|
||||
|
||||
self.uninstall_thread = UninstallThread(self, selected_folder)
|
||||
self.uninstall_thread.finished.connect(self.on_game_detection_finished)
|
||||
self.uninstall_thread.start()
|
||||
|
||||
def on_game_detection_finished(self, game_dirs):
|
||||
"""游戏识别完成后的回调"""
|
||||
# 使用UI管理器隐藏加载对话框
|
||||
if hasattr(self.main_window, 'ui_manager'):
|
||||
self.main_window.ui_manager.hide_loading_dialog()
|
||||
else:
|
||||
logger.warning("无法访问UI管理器,无法隐藏加载对话框")
|
||||
|
||||
if not game_dirs:
|
||||
QMessageBox.information(
|
||||
self.main_window,
|
||||
f"提示 - {self.app_name}",
|
||||
"\n未在选择的目录中找到任何支持的游戏。\n",
|
||||
)
|
||||
return
|
||||
|
||||
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 not games_with_patch:
|
||||
QMessageBox.information(
|
||||
self.main_window,
|
||||
f"提示 - {self.app_name}",
|
||||
"\n目录中未找到已安装补丁的游戏。\n",
|
||||
)
|
||||
return
|
||||
|
||||
selected_games = self._show_game_selection_dialog(games_with_patch)
|
||||
|
||||
if not selected_games:
|
||||
return
|
||||
|
||||
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.main_window,
|
||||
f"确认卸载 - {self.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)
|
||||
|
||||
def _handle_multiple_games(self, game_dirs, debug_mode):
|
||||
"""
|
||||
处理多个游戏的补丁卸载
|
||||
|
||||
Args:
|
||||
game_dirs: 游戏目录字典
|
||||
debug_mode: 是否为调试模式
|
||||
"""
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 卸载功能 - 在上级目录中找到以下游戏: {list(game_dirs.keys())}")
|
||||
|
||||
# 查找已安装补丁的游戏,只处理那些已安装补丁的游戏
|
||||
logger.info("检查哪些游戏已安装补丁")
|
||||
games_with_patch = {}
|
||||
for game_version, game_dir in game_dirs.items():
|
||||
is_installed = self.patch_manager.check_patch_installed(game_dir, game_version)
|
||||
if is_installed:
|
||||
games_with_patch[game_version] = game_dir
|
||||
logger.info(f"游戏 {game_version} 已安装补丁")
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 卸载功能 - {game_version} 已安装补丁,目录: {game_dir}")
|
||||
else:
|
||||
logger.info(f"游戏 {game_version} 未安装补丁")
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 卸载功能 - {game_version} 未安装补丁,跳过")
|
||||
|
||||
# 检查是否有已安装补丁的游戏
|
||||
if not games_with_patch:
|
||||
logger.info("未找到已安装补丁的游戏")
|
||||
if debug_mode:
|
||||
logger.debug("DEBUG: 卸载功能 - 未找到已安装补丁的游戏,显示提示消息")
|
||||
|
||||
QMessageBox.information(
|
||||
self.main_window,
|
||||
f"提示 - {self.app_name}",
|
||||
"\n未在选择的目录中找到已安装补丁的游戏。\n请确认您选择了正确的游戏目录,并且该目录中的游戏已安装过补丁。\n",
|
||||
QMessageBox.StandardButton.Ok
|
||||
)
|
||||
return
|
||||
|
||||
# 显示选择对话框
|
||||
logger.info("显示游戏选择对话框")
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 卸载功能 - 显示游戏选择对话框,可选游戏: {list(games_with_patch.keys())}")
|
||||
|
||||
selected_games = self._show_game_selection_dialog(games_with_patch)
|
||||
|
||||
if not selected_games:
|
||||
logger.info("用户未选择任何游戏或取消了选择")
|
||||
if debug_mode:
|
||||
logger.debug("DEBUG: 卸载功能 - 用户未选择任何游戏或取消了选择,退出卸载流程")
|
||||
return # 用户取消了选择
|
||||
|
||||
logger.info(f"用户选择了以下游戏: {selected_games}")
|
||||
if debug_mode:
|
||||
logger.debug(f"卸载功能 - 用户选择了以下游戏: {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)
|
||||
logger.info("显示卸载确认对话框")
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 卸载功能 - 显示卸载确认对话框,选择的游戏: {selected_games}")
|
||||
|
||||
reply = QMessageBox.question(
|
||||
self.main_window,
|
||||
f"确认卸载 - {self.app_name}",
|
||||
f"\n确定要卸载以下游戏的补丁吗?\n\n{game_list}\n",
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||
QMessageBox.StandardButton.No
|
||||
)
|
||||
|
||||
if reply == QMessageBox.StandardButton.No:
|
||||
logger.info("用户取消了卸载操作")
|
||||
if debug_mode:
|
||||
logger.debug("DEBUG: 卸载功能 - 用户取消了卸载操作,退出卸载流程")
|
||||
return
|
||||
|
||||
logger.info("开始批量卸载补丁")
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 卸载功能 - 开始批量卸载补丁,游戏: {list(selected_game_dirs.keys())}")
|
||||
|
||||
# 使用批量卸载方法
|
||||
success_count, fail_count, results = self.patch_manager.batch_uninstall_patches(selected_game_dirs)
|
||||
|
||||
logger.info(f"批量卸载完成,成功: {success_count},失败: {fail_count}")
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 卸载功能 - 批量卸载完成,成功: {success_count},失败: {fail_count}")
|
||||
if results:
|
||||
for result in results:
|
||||
status = "成功" if result["success"] else "失败"
|
||||
logger.debug(f"DEBUG: 卸载结果 - {result['version']}: {status}, 消息: {result['message']}, 删除文件数: {result['files_removed']}")
|
||||
|
||||
self.patch_manager.show_uninstall_result(success_count, fail_count, results)
|
||||
|
||||
def _handle_single_game(self, selected_folder, debug_mode):
|
||||
"""
|
||||
处理单个游戏的补丁卸载
|
||||
|
||||
Args:
|
||||
selected_folder: 选择的游戏目录
|
||||
debug_mode: 是否为调试模式
|
||||
"""
|
||||
# 未找到游戏目录,尝试将选择的目录作为游戏目录
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 卸载功能 - 未在上级目录找到游戏,尝试将选择的目录视为游戏目录")
|
||||
|
||||
logger.info("尝试识别单个游戏版本")
|
||||
game_version = self.game_detector.identify_game_version(selected_folder)
|
||||
|
||||
if game_version:
|
||||
logger.info(f"识别为游戏: {game_version}")
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 卸载功能 - 识别为游戏: {game_version}")
|
||||
|
||||
# 检查是否已安装补丁
|
||||
logger.info(f"检查 {game_version} 是否已安装补丁")
|
||||
is_installed = self.patch_manager.check_patch_installed(selected_folder, game_version)
|
||||
|
||||
if is_installed:
|
||||
logger.info(f"{game_version} 已安装补丁")
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 卸载功能 - {game_version} 已安装补丁")
|
||||
|
||||
# 确认卸载
|
||||
logger.info("显示卸载确认对话框")
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 卸载功能 - 显示卸载确认对话框,游戏: {game_version}")
|
||||
|
||||
reply = QMessageBox.question(
|
||||
self.main_window,
|
||||
f"确认卸载 - {self.app_name}",
|
||||
f"\n确定要卸载 {game_version} 的补丁吗?\n游戏目录: {selected_folder}\n",
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||
QMessageBox.StandardButton.No
|
||||
)
|
||||
|
||||
if reply == QMessageBox.StandardButton.Yes:
|
||||
logger.info(f"开始卸载 {game_version} 的补丁")
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 卸载功能 - 用户确认卸载 {game_version} 的补丁")
|
||||
|
||||
# 创建单个游戏的目录字典,使用批量卸载流程
|
||||
single_game_dir = {game_version: selected_folder}
|
||||
|
||||
logger.info("执行批量卸载方法(单游戏)")
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 卸载功能 - 执行批量卸载方法(单游戏): {game_version}")
|
||||
|
||||
success_count, fail_count, results = self.patch_manager.batch_uninstall_patches(single_game_dir)
|
||||
|
||||
logger.info(f"卸载完成,成功: {success_count},失败: {fail_count}")
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 卸载功能 - 卸载完成,成功: {success_count},失败: {fail_count}")
|
||||
if results:
|
||||
for result in results:
|
||||
status = "成功" if result["success"] else "失败"
|
||||
logger.debug(f"DEBUG: 卸载结果 - {result['version']}: {status}, 消息: {result['message']}, 删除文件数: {result['files_removed']}")
|
||||
|
||||
self.patch_manager.show_uninstall_result(success_count, fail_count, results)
|
||||
else:
|
||||
logger.info("用户取消了卸载操作")
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 卸载功能 - 用户取消了卸载 {game_version} 的补丁")
|
||||
else:
|
||||
logger.info(f"{game_version} 未安装补丁")
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 卸载功能 - {game_version} 未安装补丁,显示提示消息")
|
||||
|
||||
# 没有安装补丁
|
||||
QMessageBox.information(
|
||||
self.main_window,
|
||||
f"提示 - {self.app_name}",
|
||||
f"\n未在 {game_version} 中找到已安装的补丁。\n请确认该游戏已经安装过补丁。\n",
|
||||
QMessageBox.StandardButton.Ok
|
||||
)
|
||||
else:
|
||||
# 两种方式都未识别到游戏
|
||||
logger.info("无法识别游戏")
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 卸载功能 - 无法识别游戏,显示错误消息")
|
||||
|
||||
msg_box = msgbox_frame(
|
||||
f"错误 - {self.app_name}",
|
||||
"\n所选目录不是有效的NEKOPARA游戏目录。\n请选择包含游戏可执行文件的目录或其上级目录。\n",
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
msg_box.exec()
|
||||
|
||||
def _show_game_selection_dialog(self, games_with_patch):
|
||||
"""
|
||||
显示游戏选择对话框
|
||||
|
||||
Args:
|
||||
games_with_patch: 已安装补丁的游戏目录字典
|
||||
|
||||
Returns:
|
||||
list: 选择的游戏列表
|
||||
"""
|
||||
dialog = QDialog(self.main_window)
|
||||
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.Weight.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.Weight.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 []
|
||||
|
||||
# 获取用户选择的游戏
|
||||
return [item.text() for item in list_widget.selectedItems()]
|
||||
28
source/core/managers/__init__.py
Normal file
@@ -0,0 +1,28 @@
|
||||
# Managers package initialization
|
||||
from .ui_manager import UIManager
|
||||
from .download_managers import DownloadManager
|
||||
from .debug_manager import DebugManager
|
||||
from .window_manager import WindowManager
|
||||
from .game_detector import GameDetector
|
||||
from .patch_manager import PatchManager
|
||||
from .config_manager import ConfigManager
|
||||
from .privacy_manager import PrivacyManager
|
||||
from .cloudflare_optimizer import CloudflareOptimizer
|
||||
from .download_managers import DownloadTaskManager
|
||||
from .patch_detector import PatchDetector
|
||||
from .animations import MultiStageAnimations
|
||||
|
||||
__all__ = [
|
||||
'UIManager',
|
||||
'DownloadManager',
|
||||
'DebugManager',
|
||||
'WindowManager',
|
||||
'GameDetector',
|
||||
'PatchManager',
|
||||
'ConfigManager',
|
||||
'PrivacyManager',
|
||||
'CloudflareOptimizer',
|
||||
'DownloadTaskManager',
|
||||
'PatchDetector',
|
||||
'MultiStageAnimations',
|
||||
]
|
||||
@@ -49,6 +49,7 @@ class MultiStageAnimations(QObject):
|
||||
# 移除菜单背景动画
|
||||
# {"widget": ui.menubg, "end_pos": QPoint(720, 55), "duration": 600},
|
||||
{"widget": ui.button_container, "end_pos": None, "duration": 600},
|
||||
{"widget": ui.toggle_patch_container, "end_pos": None, "duration": 600}, # 添加禁/启用补丁按钮
|
||||
{"widget": ui.uninstall_container, "end_pos": None, "duration": 600}, # 添加卸载补丁按钮
|
||||
{"widget": ui.exit_container, "end_pos": None, "duration": 600}
|
||||
]
|
||||
@@ -175,7 +176,7 @@ class MultiStageAnimations(QObject):
|
||||
widget.setGraphicsEffect(effect)
|
||||
widget.move(-widget.width(), item["end_pos"].y())
|
||||
widget.show()
|
||||
print("初始化支持栏动画")
|
||||
# 初始化支持栏动画,这是内部处理,不需要日志输出
|
||||
|
||||
# 初始化菜单元素(底部外)
|
||||
for item in self.menu_widgets:
|
||||
@@ -187,7 +188,7 @@ class MultiStageAnimations(QObject):
|
||||
widget.show()
|
||||
|
||||
# 禁用所有按钮,直到动画完成
|
||||
self.ui.start_install_btn.setEnabled(False)
|
||||
self.ui.start_install_btn.setEnabled(False) # 动画期间禁用
|
||||
self.ui.uninstall_btn.setEnabled(False)
|
||||
self.ui.exit_btn.setEnabled(False)
|
||||
|
||||
@@ -301,18 +302,29 @@ class MultiStageAnimations(QObject):
|
||||
if hasattr(self.ui, 'button_container'):
|
||||
btn_width = self.ui.button_container.width()
|
||||
x_pos = width - btn_width - right_margin
|
||||
y_pos = int((height - 65) * 0.28) - 10 # 与resizeEvent中保持一致
|
||||
y_pos = int((height - 65) * 0.18) - 10 # 从0.28改为0.18,向上移动
|
||||
|
||||
# 更新动画目标位置
|
||||
for item in self.menu_widgets:
|
||||
if item["widget"] == self.ui.button_container:
|
||||
item["end_pos"] = QPoint(x_pos, y_pos)
|
||||
|
||||
# 禁用补丁按钮
|
||||
if hasattr(self.ui, 'toggle_patch_container'):
|
||||
btn_width = self.ui.toggle_patch_container.width()
|
||||
x_pos = width - btn_width - right_margin
|
||||
y_pos = int((height - 65) * 0.36) - 10 # 从0.46改为0.36,向上移动
|
||||
|
||||
# 更新动画目标位置
|
||||
for item in self.menu_widgets:
|
||||
if item["widget"] == self.ui.toggle_patch_container:
|
||||
item["end_pos"] = QPoint(x_pos, y_pos)
|
||||
|
||||
# 卸载补丁按钮
|
||||
if hasattr(self.ui, 'uninstall_container'):
|
||||
btn_width = self.ui.uninstall_container.width()
|
||||
x_pos = width - btn_width - right_margin
|
||||
y_pos = int((height - 65) * 0.46) - 10 # 与resizeEvent中保持一致
|
||||
y_pos = int((height - 65) * 0.54) - 10 # 从0.64改为0.54,向上移动
|
||||
|
||||
# 更新动画目标位置
|
||||
for item in self.menu_widgets:
|
||||
@@ -323,7 +335,7 @@ class MultiStageAnimations(QObject):
|
||||
if hasattr(self.ui, 'exit_container'):
|
||||
btn_width = self.ui.exit_container.width()
|
||||
x_pos = width - btn_width - right_margin
|
||||
y_pos = int((height - 65) * 0.64) - 10 # 与resizeEvent中保持一致
|
||||
y_pos = int((height - 65) * 0.72) - 10 # 从0.82改为0.72,向上移动
|
||||
|
||||
# 更新动画目标位置
|
||||
for item in self.menu_widgets:
|
||||
@@ -334,17 +346,19 @@ class MultiStageAnimations(QObject):
|
||||
for item in self.menu_widgets:
|
||||
if item["widget"] == self.ui.button_container:
|
||||
item["end_pos"] = QPoint(1050, 200)
|
||||
elif item["widget"] == self.ui.uninstall_container:
|
||||
elif item["widget"] == self.ui.toggle_patch_container:
|
||||
item["end_pos"] = QPoint(1050, 310)
|
||||
elif item["widget"] == self.ui.exit_container:
|
||||
elif item["widget"] == self.ui.uninstall_container:
|
||||
item["end_pos"] = QPoint(1050, 420)
|
||||
elif item["widget"] == self.ui.exit_container:
|
||||
item["end_pos"] = QPoint(1050, 530)
|
||||
|
||||
def start_animations(self):
|
||||
"""启动完整动画序列"""
|
||||
self.clear_animations()
|
||||
|
||||
# 确保按钮在动画开始时被禁用
|
||||
self.ui.start_install_btn.setEnabled(False)
|
||||
self.ui.start_install_btn.setEnabled(False) # 动画期间禁用
|
||||
self.ui.uninstall_btn.setEnabled(False)
|
||||
self.ui.exit_btn.setEnabled(False)
|
||||
|
||||
@@ -6,6 +6,10 @@ from PySide6.QtGui import QIcon, QPixmap
|
||||
|
||||
from utils import msgbox_frame, resource_path
|
||||
from workers import IpOptimizerThread
|
||||
from utils.logger import setup_logger
|
||||
|
||||
# 初始化logger
|
||||
logger = setup_logger("cloudflare_optimizer")
|
||||
|
||||
|
||||
class CloudflareOptimizer:
|
||||
@@ -28,6 +32,7 @@ class CloudflareOptimizer:
|
||||
self.optimization_cancelled = False
|
||||
self.ip_optimizer_thread = None
|
||||
self.ipv6_optimizer_thread = None
|
||||
self.has_optimized_in_session = False # 本次启动是否已执行过优选
|
||||
|
||||
def is_optimization_done(self):
|
||||
"""检查是否已完成优化
|
||||
@@ -67,6 +72,29 @@ class CloudflareOptimizer:
|
||||
Args:
|
||||
url: 用于优化的URL
|
||||
"""
|
||||
# 解析域名
|
||||
hostname = urlparse(url).hostname
|
||||
|
||||
# 判断是否继续优选的逻辑
|
||||
if self.has_optimized_in_session:
|
||||
# 如果本次会话中已执行过优选,则跳过优选过程
|
||||
logger.info("本次会话已执行过优选,跳过优选过程")
|
||||
|
||||
# 设置标记为已优选完成
|
||||
self.optimization_done = True
|
||||
self.countdown_finished = True
|
||||
|
||||
return True
|
||||
else:
|
||||
# 如果本次会话尚未优选过,则清理可能存在的旧记录
|
||||
if hostname:
|
||||
# 检查hosts文件中是否已有该域名的IP记录
|
||||
existing_ips = self.hosts_manager.get_hostname_entries(hostname)
|
||||
if existing_ips:
|
||||
logger.info(f"发现hosts文件中已有域名 {hostname} 的优选IP记录,但本次会话尚未优选过")
|
||||
# 清理已有的hosts记录,准备重新优选
|
||||
self.hosts_manager.clean_hostname_entries(hostname)
|
||||
|
||||
# 创建取消状态标记
|
||||
self.optimization_cancelled = False
|
||||
self.countdown_finished = False
|
||||
@@ -84,7 +112,7 @@ class CloudflareOptimizer:
|
||||
ipv6_warning.setIcon(QtWidgets.QMessageBox.Icon.Warning)
|
||||
|
||||
# 设置图标
|
||||
icon_path = resource_path(os.path.join("IMG", "ICO", "icon.png"))
|
||||
icon_path = resource_path(os.path.join("assets", "images", "ICO", "icon.png"))
|
||||
if os.path.exists(icon_path):
|
||||
pixmap = QPixmap(icon_path)
|
||||
if not pixmap.isNull():
|
||||
@@ -122,7 +150,7 @@ class CloudflareOptimizer:
|
||||
optimization_msg
|
||||
)
|
||||
# 设置Cloudflare图标
|
||||
cf_icon_path = resource_path("IMG/ICO/cloudflare_logo_icon.ico")
|
||||
cf_icon_path = resource_path("assets/images/ICO/cloudflare_logo_icon.ico")
|
||||
if os.path.exists(cf_icon_path):
|
||||
cf_pixmap = QPixmap(cf_icon_path)
|
||||
if not cf_pixmap.isNull():
|
||||
@@ -141,7 +169,7 @@ class CloudflareOptimizer:
|
||||
|
||||
# 如果启用IPv6,同时启动IPv6优化线程
|
||||
if use_ipv6:
|
||||
print("IPv6已启用,将同时优选IPv6地址")
|
||||
logger.info("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()
|
||||
@@ -171,7 +199,8 @@ class CloudflareOptimizer:
|
||||
|
||||
# 恢复主窗口状态
|
||||
self.main_window.setEnabled(True)
|
||||
self.main_window.ui.start_install_text.setText("开始安装")
|
||||
if hasattr(self.main_window, 'window_manager'):
|
||||
self.main_window.window_manager.change_window_state(self.main_window.window_manager.STATE_READY)
|
||||
|
||||
# 显示取消消息
|
||||
QtWidgets.QMessageBox.information(
|
||||
@@ -191,11 +220,11 @@ class CloudflareOptimizer:
|
||||
return
|
||||
|
||||
self.optimized_ip = ip
|
||||
print(f"IPv4优选完成,结果: {ip if ip else '未找到合适的IP'}")
|
||||
logger.info(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优选完成...")
|
||||
logger.info("等待IPv6优选完成...")
|
||||
return
|
||||
|
||||
# 所有优选都已完成,继续处理
|
||||
@@ -222,11 +251,11 @@ class CloudflareOptimizer:
|
||||
return
|
||||
|
||||
self.optimized_ipv6 = ipv6
|
||||
print(f"IPv6优选完成,结果: {ipv6 if ipv6 else '未找到合适的IPv6'}")
|
||||
logger.info(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优选完成...")
|
||||
logger.info("等待IPv4优选完成...")
|
||||
return
|
||||
|
||||
# 所有优选都已完成,继续处理
|
||||
@@ -244,6 +273,9 @@ class CloudflareOptimizer:
|
||||
|
||||
def _process_optimization_results(self):
|
||||
"""处理优选的IP结果,显示相应提示"""
|
||||
# 无论优选结果如何,都标记本次会话已执行过优选
|
||||
self.has_optimized_in_session = True
|
||||
|
||||
use_ipv6 = False
|
||||
if hasattr(self.main_window, 'config'):
|
||||
use_ipv6 = self.main_window.config.get("ipv6_enabled", False)
|
||||
@@ -301,7 +333,8 @@ class CloudflareOptimizer:
|
||||
if msg_box.clickedButton() == cancel_button:
|
||||
# 恢复主窗口状态
|
||||
self.main_window.setEnabled(True)
|
||||
self.main_window.ui.start_install_text.setText("开始安装")
|
||||
if hasattr(self.main_window, 'window_manager'):
|
||||
self.main_window.window_manager.change_window_state(self.main_window.window_manager.STATE_READY)
|
||||
return False
|
||||
|
||||
# 用户点击了继续,重新禁用主窗口
|
||||
@@ -332,6 +365,12 @@ class CloudflareOptimizer:
|
||||
if ipv6_success:
|
||||
success = self.hosts_manager.apply_ip(hostname, self.optimized_ipv6, clean=False) or success
|
||||
|
||||
# 记录此次优选操作对hosts文件进行了更新
|
||||
if hasattr(self.main_window, 'config'):
|
||||
self.main_window.config['last_hosts_optimized_hostname'] = hostname
|
||||
from utils import save_config
|
||||
save_config(self.main_window.config)
|
||||
|
||||
if success:
|
||||
msg_box = QtWidgets.QMessageBox(self.main_window)
|
||||
msg_box.setWindowTitle(f"成功 - {self.main_window.APP_NAME}")
|
||||
@@ -366,7 +405,8 @@ class CloudflareOptimizer:
|
||||
if msg_box.clickedButton() == cancel_button:
|
||||
# 恢复主窗口状态
|
||||
self.main_window.setEnabled(True)
|
||||
self.main_window.ui.start_install_text.setText("开始安装")
|
||||
if hasattr(self.main_window, 'window_manager'):
|
||||
self.main_window.window_manager.change_window_state(self.main_window.window_manager.STATE_READY)
|
||||
return False
|
||||
else:
|
||||
QtWidgets.QMessageBox.critical(
|
||||
@@ -375,7 +415,8 @@ class CloudflareOptimizer:
|
||||
"\n修改hosts文件失败,请检查程序是否以管理员权限运行。\n"
|
||||
)
|
||||
# 恢复主窗口状态
|
||||
self.main_window.ui.start_install_text.setText("开始安装")
|
||||
if hasattr(self.main_window, 'window_manager'):
|
||||
self.main_window.window_manager.change_window_state(self.main_window.window_manager.STATE_READY)
|
||||
return False
|
||||
|
||||
# 用户点击了继续,重新禁用主窗口
|
||||
@@ -85,16 +85,34 @@ class ConfigManager:
|
||||
# 记录错误信息,用于按钮点击时显示
|
||||
if error_message == "update_required":
|
||||
self.last_error_message = "update_required"
|
||||
msg_box = msgbox_frame(
|
||||
f"更新提示 - {self.app_name}",
|
||||
"\n当前版本过低,请及时更新。\n",
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
msg_box.exec()
|
||||
# 在浏览器中打开项目主页
|
||||
webbrowser.open("https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/")
|
||||
# 版本过低,应当显示"无法安装"
|
||||
return {"action": "disable_button", "then": "exit"}
|
||||
|
||||
# 检查是否处于离线模式
|
||||
is_offline_mode = False
|
||||
if hasattr(self.debug_manager, 'main_window') and hasattr(self.debug_manager.main_window, 'offline_mode_manager'):
|
||||
is_offline_mode = self.debug_manager.main_window.offline_mode_manager.is_in_offline_mode()
|
||||
|
||||
if is_offline_mode:
|
||||
# 离线模式下只显示提示,不禁用开始安装按钮
|
||||
msg_box = msgbox_frame(
|
||||
f"更新提示 - {self.app_name}",
|
||||
"\n当前版本过低,请及时更新。\n在离线模式下,您仍可使用禁用/启用补丁、卸载补丁和离线安装功能。\n",
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
msg_box.exec()
|
||||
# 移除在浏览器中打开项目主页的代码
|
||||
# 离线模式下版本过低,仍然允许使用安装按钮
|
||||
return {"action": "enable_button"}
|
||||
else:
|
||||
# 在线模式下显示强制更新提示
|
||||
msg_box = msgbox_frame(
|
||||
f"更新提示 - {self.app_name}",
|
||||
"\n当前版本过低,请及时更新。\n如需联网下载补丁,请更新到最新版,否则无法下载。\n",
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
msg_box.exec()
|
||||
# 移除在浏览器中打开项目主页的代码
|
||||
# 在线模式下版本过低,但不直接禁用按钮,而是在点击时提示
|
||||
return {"action": "enable_button", "version_warning": True}
|
||||
|
||||
elif "missing_keys" in error_message:
|
||||
self.last_error_message = "missing_keys"
|
||||
@@ -128,8 +146,8 @@ class ConfigManager:
|
||||
)
|
||||
msg_box.exec()
|
||||
|
||||
# 网络错误时应当显示"无法安装"
|
||||
return {"action": "disable_button"}
|
||||
# 网络错误时仍然允许使用按钮,用户可以尝试离线模式
|
||||
return {"action": "enable_button"}
|
||||
else:
|
||||
self.cloud_config = data
|
||||
# 标记配置有效
|
||||
@@ -139,10 +157,36 @@ class ConfigManager:
|
||||
|
||||
if debug_mode:
|
||||
print("--- Cloud config fetched successfully ---")
|
||||
print(json.dumps(data, indent=2))
|
||||
# 创建一个数据副本,隐藏敏感URL
|
||||
safe_data = self._create_safe_config_for_logging(data)
|
||||
print(json.dumps(safe_data, indent=2))
|
||||
|
||||
# 获取配置成功,允许安装
|
||||
return {"action": "enable_button"}
|
||||
|
||||
def _create_safe_config_for_logging(self, config_data):
|
||||
"""创建用于日志记录的安全配置副本,隐藏敏感URL
|
||||
|
||||
Args:
|
||||
config_data: 原始配置数据
|
||||
|
||||
Returns:
|
||||
dict: 安全的配置数据副本
|
||||
"""
|
||||
if not config_data or not isinstance(config_data, dict):
|
||||
return config_data
|
||||
|
||||
# 创建深拷贝,避免修改原始数据
|
||||
import copy
|
||||
safe_config = copy.deepcopy(config_data)
|
||||
|
||||
# 隐藏敏感URL
|
||||
for key in safe_config:
|
||||
if isinstance(safe_config[key], dict) and "url" in safe_config[key]:
|
||||
# 完全隐藏URL
|
||||
safe_config[key]["url"] = "***URL protection***"
|
||||
|
||||
return safe_config
|
||||
|
||||
def is_config_valid(self):
|
||||
"""检查配置是否有效
|
||||
@@ -166,4 +210,54 @@ class ConfigManager:
|
||||
Returns:
|
||||
str: 错误信息
|
||||
"""
|
||||
return self.last_error_message
|
||||
return self.last_error_message
|
||||
|
||||
def toggle_disable_pre_hash_check(self, main_window, checked):
|
||||
"""切换禁用安装前哈希预检查的状态
|
||||
|
||||
Args:
|
||||
main_window: 主窗口实例
|
||||
checked: 是否禁用安装前哈希预检查
|
||||
|
||||
Returns:
|
||||
bool: 操作是否成功
|
||||
"""
|
||||
try:
|
||||
# 更新配置
|
||||
if hasattr(main_window, 'config'):
|
||||
main_window.config['disable_pre_hash_check'] = checked
|
||||
|
||||
# 保存配置到文件
|
||||
if hasattr(main_window, 'save_config'):
|
||||
main_window.save_config(main_window.config)
|
||||
|
||||
# 显示成功提示
|
||||
status = "禁用" if checked else "启用"
|
||||
from utils import msgbox_frame
|
||||
msg_box = msgbox_frame(
|
||||
f"设置已更新 - {self.app_name}",
|
||||
f"\n已{status}安装前哈希预检查。\n\n{'安装时将跳过哈希预检查' if checked else '安装时将进行哈希预检查'}。\n",
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
msg_box.exec()
|
||||
return True
|
||||
else:
|
||||
# 如果配置不可用,显示错误
|
||||
from utils import msgbox_frame
|
||||
msg_box = msgbox_frame(
|
||||
f"错误 - {self.app_name}",
|
||||
"\n配置管理器不可用,无法更新设置。\n",
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
msg_box.exec()
|
||||
return False
|
||||
except Exception as e:
|
||||
# 如果发生异常,显示错误
|
||||
from utils import msgbox_frame
|
||||
msg_box = msgbox_frame(
|
||||
f"错误 - {self.app_name}",
|
||||
f"\n更新设置时发生异常:\n\n{str(e)}\n",
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
msg_box.exec()
|
||||
return False
|
||||
276
source/core/managers/debug_manager.py
Normal file
@@ -0,0 +1,276 @@
|
||||
import os
|
||||
import sys
|
||||
from PySide6 import QtWidgets
|
||||
from config.config import LOG_FILE
|
||||
from utils.logger import setup_logger
|
||||
from utils import Logger
|
||||
import datetime
|
||||
from config.config import APP_NAME
|
||||
|
||||
# 初始化logger
|
||||
logger = setup_logger("debug_manager")
|
||||
|
||||
class DebugManager:
|
||||
def __init__(self, main_window):
|
||||
"""初始化调试管理器
|
||||
|
||||
Args:
|
||||
main_window: 主窗口实例
|
||||
"""
|
||||
self.main_window = main_window
|
||||
self.logger = None
|
||||
self.original_stdout = None
|
||||
self.original_stderr = None
|
||||
self.ui_manager = None # 添加ui_manager属性
|
||||
|
||||
def set_ui_manager(self, ui_manager):
|
||||
"""设置UI管理器引用
|
||||
|
||||
Args:
|
||||
ui_manager: UI管理器实例
|
||||
"""
|
||||
self.ui_manager = ui_manager
|
||||
|
||||
def _is_debug_mode(self):
|
||||
"""检查是否处于调试模式
|
||||
|
||||
Returns:
|
||||
bool: 是否处于调试模式
|
||||
"""
|
||||
try:
|
||||
# 首先尝试从UI管理器获取状态
|
||||
if hasattr(self, 'ui_manager') and self.ui_manager and hasattr(self.ui_manager, 'debug_action') and self.ui_manager.debug_action:
|
||||
return self.ui_manager.debug_action.isChecked()
|
||||
|
||||
# 如果UI管理器还没准备好,尝试从配置中获取
|
||||
if hasattr(self.main_window, 'config') and isinstance(self.main_window.config, dict):
|
||||
return self.main_window.config.get('debug_mode', False)
|
||||
|
||||
# 如果以上都不可行,返回False
|
||||
return False
|
||||
except Exception:
|
||||
# 捕获任何异常,默认返回False
|
||||
return False
|
||||
|
||||
def toggle_debug_mode(self, checked):
|
||||
"""切换调试模式
|
||||
|
||||
Args:
|
||||
checked: 是否启用调试模式
|
||||
"""
|
||||
logger.info(f"Toggle debug mode: {checked}")
|
||||
self.main_window.config["debug_mode"] = checked
|
||||
self.main_window.save_config(self.main_window.config)
|
||||
|
||||
# 创建或删除debug_mode.txt标记文件
|
||||
try:
|
||||
from config.config import CACHE
|
||||
debug_file = os.path.join(os.path.dirname(CACHE), "debug_mode.txt")
|
||||
|
||||
if checked:
|
||||
# 确保目录存在
|
||||
os.makedirs(os.path.dirname(debug_file), exist_ok=True)
|
||||
# 创建标记文件
|
||||
with open(debug_file, 'w', encoding='utf-8') as f:
|
||||
f.write(f"Debug mode enabled at {os.path.abspath(debug_file)}\n")
|
||||
logger.info(f"已创建调试模式标记文件: {debug_file}")
|
||||
elif os.path.exists(debug_file):
|
||||
# 删除标记文件
|
||||
os.remove(debug_file)
|
||||
logger.debug(f"已删除调试模式标记文件: {debug_file}")
|
||||
except Exception as e:
|
||||
logger.warning(f"处理调试模式标记文件时发生错误: {e}")
|
||||
|
||||
# 更新打开log文件按钮状态
|
||||
if hasattr(self, 'ui_manager') and hasattr(self.ui_manager, 'open_log_action'):
|
||||
self.ui_manager.open_log_action.setEnabled(checked)
|
||||
|
||||
if checked:
|
||||
self.start_logging()
|
||||
|
||||
# 如果启用了调试模式,检查是否需要强制启用离线模式
|
||||
if hasattr(self.main_window, 'offline_mode_manager'):
|
||||
# 检查配置中是否已设置离线模式
|
||||
offline_mode_enabled = self.main_window.config.get("offline_mode", False)
|
||||
|
||||
# 如果配置中已设置离线模式,则在调试模式下强制启用
|
||||
if offline_mode_enabled:
|
||||
logger.debug("DEBUG: 调试模式下强制启用离线模式")
|
||||
self.main_window.offline_mode_manager.set_offline_mode(True)
|
||||
|
||||
# 更新UI中的离线模式选项
|
||||
if hasattr(self.ui_manager, 'offline_mode_action') and self.ui_manager.offline_mode_action:
|
||||
self.ui_manager.offline_mode_action.setChecked(True)
|
||||
self.ui_manager.online_mode_action.setChecked(False)
|
||||
else:
|
||||
self.stop_logging()
|
||||
|
||||
def start_logging(self):
|
||||
"""启动日志记录"""
|
||||
if self.logger is None:
|
||||
try:
|
||||
# 确保log目录存在
|
||||
log_dir = os.path.dirname(LOG_FILE)
|
||||
if not os.path.exists(log_dir):
|
||||
os.makedirs(log_dir, exist_ok=True)
|
||||
logger.debug(f"已创建日志目录: {log_dir}")
|
||||
|
||||
# 创建新的日志文件,使用覆盖模式而不是追加模式
|
||||
with open(LOG_FILE, 'w', encoding='utf-8') as f:
|
||||
current_time = datetime.datetime.now()
|
||||
formatted_date = current_time.strftime("%Y-%m-%d")
|
||||
formatted_time = current_time.strftime("%H:%M:%S")
|
||||
f.write(f"--- 新调试会话开始于 {os.path.basename(LOG_FILE)} ---\n")
|
||||
f.write(f"--- 应用版本: {APP_NAME} ---\n")
|
||||
f.write(f"--- 日期: {formatted_date} 时间: {formatted_time} ---\n\n")
|
||||
logger.debug(f"已创建日志文件: {os.path.abspath(LOG_FILE)}")
|
||||
|
||||
# 保存原始的 stdout 并创建Logger实例
|
||||
self.original_stdout = sys.stdout
|
||||
self.logger = Logger(LOG_FILE, self.original_stdout)
|
||||
|
||||
logger.debug(f"--- Debug mode enabled (log file: {os.path.abspath(LOG_FILE)}) ---")
|
||||
except (IOError, OSError) as e:
|
||||
QtWidgets.QMessageBox.critical(self.main_window, "错误", f"无法创建日志文件: {e}")
|
||||
self.logger = None
|
||||
|
||||
def stop_logging(self):
|
||||
"""停止日志记录"""
|
||||
if self.logger:
|
||||
logger.debug("--- Debug mode disabled ---")
|
||||
# 恢复stdout到原始状态
|
||||
if hasattr(self, 'original_stdout') and self.original_stdout:
|
||||
sys.stdout = self.original_stdout
|
||||
# 关闭日志文件
|
||||
if hasattr(self.logger, 'close'):
|
||||
self.logger.close()
|
||||
self.logger = None
|
||||
|
||||
def open_log_file(self):
|
||||
"""打开当前日志文件"""
|
||||
try:
|
||||
# 检查日志文件是否存在
|
||||
if os.path.exists(LOG_FILE):
|
||||
# 获取日志文件大小
|
||||
file_size = os.path.getsize(LOG_FILE)
|
||||
if file_size == 0:
|
||||
from utils import msgbox_frame
|
||||
msg_box = msgbox_frame(
|
||||
f"提示 - {APP_NAME}",
|
||||
f"\n当前日志文件 {os.path.basename(LOG_FILE)} 存在但为空。\n\n日志文件位置:{os.path.abspath(LOG_FILE)}",
|
||||
QtWidgets.QMessageBox.StandardButton.Ok
|
||||
)
|
||||
msg_box.exec()
|
||||
return
|
||||
|
||||
# 根据文件大小决定是使用文本查看器还是直接打开
|
||||
if file_size > 1024 * 1024: # 大于1MB
|
||||
# 文件较大,显示警告
|
||||
from utils import msgbox_frame
|
||||
msg_box = msgbox_frame(
|
||||
f"警告 - {APP_NAME}",
|
||||
f"\n日志文件较大 ({file_size / 1024 / 1024:.2f} MB),是否仍要打开?\n\n日志文件位置:{os.path.abspath(LOG_FILE)}",
|
||||
QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No
|
||||
)
|
||||
if msg_box.exec() != QtWidgets.QMessageBox.StandardButton.Yes:
|
||||
return
|
||||
|
||||
# 使用操作系统默认程序打开日志文件
|
||||
if os.name == 'nt': # Windows
|
||||
os.startfile(LOG_FILE)
|
||||
else: # macOS 和 Linux
|
||||
import subprocess
|
||||
subprocess.call(['xdg-open', LOG_FILE])
|
||||
else:
|
||||
# 文件不存在,显示信息和搜索其他日志文件
|
||||
root_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
||||
log_dir = os.path.join(root_dir, "log")
|
||||
|
||||
# 如果log文件夹不存在,尝试创建它
|
||||
if not os.path.exists(log_dir):
|
||||
try:
|
||||
os.makedirs(log_dir, exist_ok=True)
|
||||
from utils import msgbox_frame
|
||||
msg_box = msgbox_frame(
|
||||
f"信息 - {APP_NAME}",
|
||||
f"\n日志文件夹不存在,已创建新的日志文件夹:\n{log_dir}\n\n请在启用调试模式后重试。",
|
||||
QtWidgets.QMessageBox.StandardButton.Ok
|
||||
)
|
||||
msg_box.exec()
|
||||
return
|
||||
except Exception as e:
|
||||
from utils import msgbox_frame
|
||||
msg_box = msgbox_frame(
|
||||
f"错误 - {APP_NAME}",
|
||||
f"\n创建日志文件夹失败:\n\n{str(e)}",
|
||||
QtWidgets.QMessageBox.StandardButton.Ok
|
||||
)
|
||||
msg_box.exec()
|
||||
return
|
||||
|
||||
# 搜索log文件夹中的日志文件
|
||||
try:
|
||||
log_files = [f for f in os.listdir(log_dir) if f.startswith("log-") and f.endswith(".txt")]
|
||||
except Exception as e:
|
||||
from utils import msgbox_frame
|
||||
msg_box = msgbox_frame(
|
||||
f"错误 - {APP_NAME}",
|
||||
f"\n无法读取日志文件夹:\n\n{str(e)}",
|
||||
QtWidgets.QMessageBox.StandardButton.Ok
|
||||
)
|
||||
msg_box.exec()
|
||||
return
|
||||
|
||||
if log_files:
|
||||
# 按照修改时间排序,获取最新的日志文件
|
||||
log_files.sort(key=lambda x: os.path.getmtime(os.path.join(log_dir, x)), reverse=True)
|
||||
latest_log = os.path.join(log_dir, log_files[0])
|
||||
|
||||
# 获取最新日志文件的创建时间信息
|
||||
try:
|
||||
log_datetime = "-".join(os.path.basename(latest_log)[4:-4].split("-")[:2])
|
||||
log_date = log_datetime.split("-")[0]
|
||||
log_time = log_datetime.split("-")[1] if "-" in log_datetime else "未知时间"
|
||||
date_info = f"日期: {log_date[:4]}-{log_date[4:6]}-{log_date[6:]}"
|
||||
time_info = f"时间: {log_time[:2]}:{log_time[2:4]}:{log_time[4:]}"
|
||||
except:
|
||||
date_info = "日期未知"
|
||||
time_info = "时间未知"
|
||||
|
||||
from utils import msgbox_frame
|
||||
msg_box = msgbox_frame(
|
||||
f"信息 - {APP_NAME}",
|
||||
f"\n当前日志文件 {os.path.basename(LOG_FILE)} 不存在。\n\n"
|
||||
f"发现最新的日志文件: {os.path.basename(latest_log)}\n"
|
||||
f"({date_info} {time_info})\n\n"
|
||||
f"是否打开此文件?",
|
||||
QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No
|
||||
)
|
||||
|
||||
if msg_box.exec() == QtWidgets.QMessageBox.StandardButton.Yes:
|
||||
if os.name == 'nt': # Windows
|
||||
os.startfile(latest_log)
|
||||
else: # macOS 和 Linux
|
||||
import subprocess
|
||||
subprocess.call(['xdg-open', latest_log])
|
||||
return
|
||||
|
||||
# 如果没有找到任何日志文件
|
||||
from utils import msgbox_frame
|
||||
msg_box = msgbox_frame(
|
||||
f"信息 - {APP_NAME}",
|
||||
f"\n没有找到有效的日志文件。\n\n"
|
||||
f"预期的日志文件夹:{log_dir}\n\n"
|
||||
f"请确认调试模式已启用,并执行一些操作后再查看日志。",
|
||||
QtWidgets.QMessageBox.StandardButton.Ok
|
||||
)
|
||||
msg_box.exec()
|
||||
|
||||
except Exception as e:
|
||||
from utils import msgbox_frame
|
||||
msg_box = msgbox_frame(
|
||||
f"错误 - {APP_NAME}",
|
||||
f"\n处理日志文件时出错:\n\n{str(e)}\n\n文件位置:{os.path.abspath(LOG_FILE)}",
|
||||
QtWidgets.QMessageBox.StandardButton.Ok
|
||||
)
|
||||
msg_box.exec()
|
||||
12
source/core/managers/download_managers/__init__.py
Normal file
@@ -0,0 +1,12 @@
|
||||
"""
|
||||
下载管理器模块
|
||||
包含下载相关的管理器类
|
||||
"""
|
||||
|
||||
from .download_manager import DownloadManager
|
||||
from .download_task_manager import DownloadTaskManager
|
||||
|
||||
__all__ = [
|
||||
'DownloadManager',
|
||||
'DownloadTaskManager',
|
||||
]
|
||||
1188
source/core/managers/download_managers/download_manager.py
Normal file
@@ -3,7 +3,9 @@ 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
|
||||
from config.config import DOWNLOAD_THREADS
|
||||
from workers.download import DownloadThread
|
||||
from utils.logger import setup_logger
|
||||
|
||||
|
||||
class DownloadTaskManager:
|
||||
@@ -34,7 +36,7 @@ class DownloadTaskManager:
|
||||
# 按钮在file_dialog中已设置为禁用状态
|
||||
|
||||
# 创建并连接下载线程
|
||||
self.current_download_thread = self.main_window.create_download_thread(url, _7z_path, game_version)
|
||||
self.current_download_thread = DownloadThread(url, _7z_path, game_version, self.main_window)
|
||||
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(
|
||||
@@ -52,7 +54,7 @@ class DownloadTaskManager:
|
||||
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.main_window.progress_window.pause_resume_button.clicked.connect(self._on_pause_resume_clicked)
|
||||
|
||||
# 启动线程和显示进度窗口
|
||||
self.current_download_thread.start()
|
||||
@@ -60,6 +62,8 @@ class DownloadTaskManager:
|
||||
|
||||
def toggle_download_pause(self):
|
||||
"""切换下载的暂停/恢复状态"""
|
||||
logger = setup_logger("download_task_manager")
|
||||
logger.debug("执行暂停/恢复下载操作")
|
||||
if not self.current_download_thread:
|
||||
return
|
||||
|
||||
@@ -111,6 +115,8 @@ class DownloadTaskManager:
|
||||
|
||||
def show_download_thread_settings(self):
|
||||
"""显示下载线程设置对话框"""
|
||||
logger = setup_logger("download_task_manager")
|
||||
logger.info("用户打开下载线程数设置对话框")
|
||||
# 创建对话框
|
||||
dialog = QDialog(self.main_window)
|
||||
dialog.setWindowTitle(f"下载线程设置 - {self.APP_NAME}")
|
||||
@@ -178,6 +184,7 @@ class DownloadTaskManager:
|
||||
break
|
||||
|
||||
if selected_level:
|
||||
old_level = self.download_thread_level
|
||||
# 为极速和狂暴模式显示警告
|
||||
if selected_level in ["extreme", "insane"]:
|
||||
warning_result = QtWidgets.QMessageBox.warning(
|
||||
@@ -193,6 +200,9 @@ class DownloadTaskManager:
|
||||
|
||||
success = self.set_download_thread_level(selected_level)
|
||||
|
||||
logger.info(f"用户修改下载线程数设置: {old_level} -> {selected_level}")
|
||||
logger.debug(f"对应线程数: {DOWNLOAD_THREADS[old_level]} -> {DOWNLOAD_THREADS[selected_level]}")
|
||||
|
||||
if success:
|
||||
# 显示设置成功消息
|
||||
thread_count = DOWNLOAD_THREADS[selected_level]
|
||||
@@ -214,8 +224,19 @@ class DownloadTaskManager:
|
||||
|
||||
def stop_download(self):
|
||||
"""停止当前下载线程"""
|
||||
logger = setup_logger("download_task_manager")
|
||||
logger.info("用户点击停止下载按钮")
|
||||
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
|
||||
return False
|
||||
|
||||
def _on_pause_resume_clicked(self):
|
||||
"""处理暂停/恢复按钮点击"""
|
||||
logger = setup_logger("download_task_manager")
|
||||
logger.info("用户点击暂停/恢复下载按钮")
|
||||
self.toggle_download_pause()
|
||||
|
||||
def toggle_download_pause(self):
|
||||
"""切换下载暂停/恢复状态"""
|
||||
@@ -1,5 +1,20 @@
|
||||
from PySide6.QtCore import QThread, Signal
|
||||
import os
|
||||
import re
|
||||
from utils.logger import setup_logger
|
||||
|
||||
class GameDetectionThread(QThread):
|
||||
"""用于在后台线程中执行游戏目录识别的线程"""
|
||||
finished = Signal(dict)
|
||||
|
||||
def __init__(self, detector_func, selected_folder):
|
||||
super().__init__()
|
||||
self.detector_func = detector_func
|
||||
self.selected_folder = selected_folder
|
||||
|
||||
def run(self):
|
||||
result = self.detector_func(self.selected_folder)
|
||||
self.finished.emit(result)
|
||||
|
||||
class GameDetector:
|
||||
"""游戏检测器,用于识别游戏目录和版本"""
|
||||
@@ -13,6 +28,19 @@ class GameDetector:
|
||||
"""
|
||||
self.game_info = game_info
|
||||
self.debug_manager = debug_manager
|
||||
self.directory_cache = {} # 添加目录缓存
|
||||
self.logger = setup_logger("game_detector")
|
||||
self.detection_thread = None
|
||||
|
||||
def identify_game_directories_async(self, selected_folder, callback):
|
||||
"""异步识别游戏目录"""
|
||||
def on_finished(game_dirs):
|
||||
callback(game_dirs)
|
||||
self.detection_thread = None
|
||||
|
||||
self.detection_thread = GameDetectionThread(self.identify_game_directories_improved, selected_folder)
|
||||
self.detection_thread.finished.connect(on_finished)
|
||||
self.detection_thread.start()
|
||||
|
||||
def _is_debug_mode(self):
|
||||
"""检查是否处于调试模式
|
||||
@@ -36,7 +64,7 @@ class GameDetector:
|
||||
debug_mode = self._is_debug_mode()
|
||||
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 尝试识别游戏版本: {game_dir}")
|
||||
self.logger.debug(f"尝试识别游戏版本: {game_dir}")
|
||||
|
||||
# 先通过目录名称进行初步推测(这将作为递归搜索的提示)
|
||||
dir_name = os.path.basename(game_dir).lower()
|
||||
@@ -50,11 +78,11 @@ class GameDetector:
|
||||
vol_num = vol_match.group(1)
|
||||
potential_version = f"NEKOPARA Vol.{vol_num}"
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 从目录名推测游戏版本: {potential_version}, 卷号: {vol_num}")
|
||||
self.logger.debug(f"从目录名推测游戏版本: {potential_version}, 卷号: {vol_num}")
|
||||
elif "after" in dir_name:
|
||||
potential_version = "NEKOPARA After"
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 从目录名推测游戏版本: NEKOPARA After")
|
||||
self.logger.debug(f"从目录名推测游戏版本: NEKOPARA After")
|
||||
|
||||
# 检查是否为NEKOPARA游戏目录
|
||||
# 通过检查游戏可执行文件来识别游戏版本
|
||||
@@ -87,7 +115,7 @@ class GameDetector:
|
||||
exe_path = os.path.join(game_dir, exe_variant)
|
||||
if os.path.exists(exe_path):
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 通过可执行文件确认游戏版本: {game_version}, 文件: {exe_variant}")
|
||||
self.logger.debug(f"通过可执行文件确认游戏版本: {game_version}, 文件: {exe_variant}")
|
||||
return game_version
|
||||
|
||||
# 如果没有直接匹配,尝试递归搜索
|
||||
@@ -110,17 +138,17 @@ class GameDetector:
|
||||
f"vol {vol_num}" in file_lower)) or
|
||||
(is_after and "after" in file_lower)):
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 通过递归搜索确认游戏版本: {potential_version}, 文件: {file}")
|
||||
self.logger.debug(f"通过递归搜索确认游戏版本: {potential_version}, 文件: {file}")
|
||||
return potential_version
|
||||
|
||||
# 如果仍然没有找到,基于目录名的推测返回结果
|
||||
if potential_version:
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 基于目录名返回推测的游戏版本: {potential_version}")
|
||||
self.logger.debug(f"基于目录名返回推测的游戏版本: {potential_version}")
|
||||
return potential_version
|
||||
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 无法识别游戏版本: {game_dir}")
|
||||
self.logger.debug(f"无法识别游戏版本: {game_dir}")
|
||||
|
||||
return None
|
||||
|
||||
@@ -135,8 +163,14 @@ class GameDetector:
|
||||
"""
|
||||
debug_mode = self._is_debug_mode()
|
||||
|
||||
# 检查缓存中是否已有该目录的识别结果
|
||||
if selected_folder in self.directory_cache:
|
||||
if debug_mode:
|
||||
self.logger.debug(f"使用缓存的目录识别结果: {selected_folder}")
|
||||
return self.directory_cache[selected_folder]
|
||||
|
||||
if debug_mode:
|
||||
print(f"--- 开始识别目录: {selected_folder} ---")
|
||||
self.logger.debug(f"--- 开始识别目录: {selected_folder} ---")
|
||||
|
||||
game_paths = {}
|
||||
|
||||
@@ -144,10 +178,10 @@ class GameDetector:
|
||||
try:
|
||||
all_dirs = [d for d in os.listdir(selected_folder) if os.path.isdir(os.path.join(selected_folder, d))]
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 找到以下子目录: {all_dirs}")
|
||||
self.logger.debug(f"找到以下子目录: {all_dirs}")
|
||||
except Exception as e:
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 无法读取目录 {selected_folder}: {str(e)}")
|
||||
self.logger.debug(f"无法读取目录 {selected_folder}: {str(e)}")
|
||||
return {}
|
||||
|
||||
for game, info in self.game_info.items():
|
||||
@@ -155,7 +189,7 @@ class GameDetector:
|
||||
expected_exe = info["exe"] # 标准可执行文件名
|
||||
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 搜索游戏 {game}, 预期目录: {expected_dir}, 预期可执行文件: {expected_exe}")
|
||||
self.logger.debug(f"搜索游戏 {game}, 预期目录: {expected_dir}, 预期可执行文件: {expected_exe}")
|
||||
|
||||
# 尝试不同的匹配方法
|
||||
found_dir = None
|
||||
@@ -164,7 +198,7 @@ class GameDetector:
|
||||
if expected_dir in all_dirs:
|
||||
found_dir = expected_dir
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 精确匹配成功: {expected_dir}")
|
||||
self.logger.debug(f"精确匹配成功: {expected_dir}")
|
||||
|
||||
# 2. 大小写不敏感匹配
|
||||
if not found_dir:
|
||||
@@ -172,7 +206,7 @@ class GameDetector:
|
||||
if expected_dir.lower() == dir_name.lower():
|
||||
found_dir = dir_name
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 大小写不敏感匹配成功: {dir_name}")
|
||||
self.logger.debug(f"大小写不敏感匹配成功: {dir_name}")
|
||||
break
|
||||
|
||||
# 3. 更模糊的匹配(允许特殊字符差异)
|
||||
@@ -186,7 +220,7 @@ class GameDetector:
|
||||
if pattern.match(dir_name):
|
||||
found_dir = dir_name
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 模糊匹配成功: {dir_name} 匹配模式 {pattern_text}")
|
||||
self.logger.debug(f"模糊匹配成功: {dir_name} 匹配模式 {pattern_text}")
|
||||
break
|
||||
|
||||
# 4. 如果还是没找到,尝试更宽松的匹配
|
||||
@@ -196,7 +230,7 @@ class GameDetector:
|
||||
if vol_match:
|
||||
vol_num = vol_match.group(1)
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 提取卷号: {vol_num}")
|
||||
self.logger.debug(f"提取卷号: {vol_num}")
|
||||
|
||||
is_after = "after" in expected_dir.lower()
|
||||
|
||||
@@ -207,7 +241,7 @@ class GameDetector:
|
||||
if is_after and "after" in dir_lower:
|
||||
found_dir = dir_name
|
||||
if debug_mode:
|
||||
print(f"DEBUG: After特殊匹配成功: {dir_name}")
|
||||
self.logger.debug(f"After特殊匹配成功: {dir_name}")
|
||||
break
|
||||
|
||||
# 对于Vol特殊处理
|
||||
@@ -217,7 +251,7 @@ class GameDetector:
|
||||
if dir_vol_match and dir_vol_match.group(1) == vol_num:
|
||||
found_dir = dir_name
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 卷号匹配成功: {dir_name} 卷号 {vol_num}")
|
||||
self.logger.debug(f"卷号匹配成功: {dir_name} 卷号 {vol_num}")
|
||||
break
|
||||
|
||||
# 如果找到匹配的目录,验证exe文件是否存在
|
||||
@@ -260,7 +294,7 @@ class GameDetector:
|
||||
exe_exists = True
|
||||
found_exe = exe_variant
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 验证成功,找到游戏可执行文件: {exe_variant}")
|
||||
self.logger.debug(f"验证成功,找到游戏可执行文件: {exe_variant}")
|
||||
break
|
||||
|
||||
# 如果没有直接找到,尝试递归搜索当前目录下的所有可执行文件
|
||||
@@ -283,14 +317,14 @@ class GameDetector:
|
||||
exe_exists = True
|
||||
found_exe = os.path.relpath(exe_path, potential_path)
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 通过递归搜索找到游戏可执行文件: {found_exe}")
|
||||
self.logger.debug(f"通过递归搜索找到游戏可执行文件: {found_exe}")
|
||||
break
|
||||
elif "After" in game and "after" in file_lower:
|
||||
exe_path = os.path.join(root, file)
|
||||
exe_exists = True
|
||||
found_exe = os.path.relpath(exe_path, potential_path)
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 通过递归搜索找到After游戏可执行文件: {found_exe}")
|
||||
self.logger.debug(f"通过递归搜索找到After游戏可执行文件: {found_exe}")
|
||||
break
|
||||
if exe_exists:
|
||||
break
|
||||
@@ -299,13 +333,22 @@ class GameDetector:
|
||||
if exe_exists:
|
||||
game_paths[game] = potential_path
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 验证成功,将 {potential_path} 添加为 {game} 的目录")
|
||||
self.logger.debug(f"验证成功,将 {potential_path} 添加为 {game} 的目录")
|
||||
else:
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 未找到任何可执行文件变体,游戏 {game} 在 {potential_path} 未找到")
|
||||
self.logger.debug(f"未找到任何可执行文件变体,游戏 {game} 在 {potential_path} 未找到")
|
||||
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 最终识别的游戏目录: {game_paths}")
|
||||
print(f"--- 目录识别结束 ---")
|
||||
self.logger.debug(f"最终识别的游戏目录: {game_paths}")
|
||||
self.logger.debug(f"--- 目录识别结束 ---")
|
||||
|
||||
# 将识别结果存入缓存
|
||||
self.directory_cache[selected_folder] = game_paths
|
||||
|
||||
return game_paths
|
||||
return game_paths
|
||||
|
||||
def clear_directory_cache(self):
|
||||
"""清除目录缓存"""
|
||||
self.directory_cache = {}
|
||||
if self._is_debug_mode():
|
||||
self.logger.debug("已清除目录缓存")
|
||||
@@ -8,7 +8,7 @@ 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 config.config import APP_NAME
|
||||
from utils import msgbox_frame
|
||||
|
||||
|
||||
@@ -291,28 +291,6 @@ class IPv6Manager:
|
||||
"""
|
||||
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
|
||||
1051
source/core/managers/offline_mode_manager.py
Normal file
378
source/core/managers/patch_detector.py
Normal file
@@ -0,0 +1,378 @@
|
||||
import os
|
||||
import hashlib
|
||||
import tempfile
|
||||
import py7zr
|
||||
import traceback
|
||||
from utils.logger import setup_logger
|
||||
from PySide6.QtWidgets import QMessageBox
|
||||
from PySide6.QtCore import QTimer, QThread, Signal
|
||||
from config.config import PLUGIN_HASH, APP_NAME
|
||||
|
||||
# 初始化logger
|
||||
logger = setup_logger("patch_detector")
|
||||
|
||||
class PatchCheckThread(QThread):
|
||||
"""用于在后台线程中执行补丁检查的线程"""
|
||||
finished = Signal(bool) # (is_installed)
|
||||
|
||||
def __init__(self, checker_func, *args):
|
||||
super().__init__()
|
||||
self.checker_func = checker_func
|
||||
self.args = args
|
||||
|
||||
def run(self):
|
||||
result = self.checker_func(*self.args)
|
||||
self.finished.emit(result)
|
||||
|
||||
class PatchDetector:
|
||||
"""补丁检测与校验模块,用于统一处理在线和离线模式下的补丁检测和校验"""
|
||||
|
||||
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 ""
|
||||
self.game_info = {}
|
||||
self.plugin_hash = {}
|
||||
self._load_game_info()
|
||||
self.patch_check_thread = None
|
||||
|
||||
def _load_game_info(self):
|
||||
"""从配置中加载游戏信息和补丁哈希值"""
|
||||
try:
|
||||
from config.config import GAME_INFO, PLUGIN_HASH
|
||||
self.game_info = GAME_INFO
|
||||
self.plugin_hash = PLUGIN_HASH
|
||||
except ImportError:
|
||||
logger.error("无法加载游戏信息或补丁哈希值配置")
|
||||
|
||||
def _is_debug_mode(self):
|
||||
"""检查是否处于调试模式
|
||||
|
||||
Returns:
|
||||
bool: 是否处于调试模式
|
||||
"""
|
||||
try:
|
||||
if hasattr(self.main_window, 'debug_manager') and self.main_window.debug_manager:
|
||||
if hasattr(self.main_window.debug_manager, '_is_debug_mode'):
|
||||
return self.main_window.debug_manager._is_debug_mode()
|
||||
elif hasattr(self.main_window, 'config'):
|
||||
return self.main_window.config.get('debug_mode', False)
|
||||
return False
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def check_patch_installed_async(self, game_dir, game_version, callback):
|
||||
"""异步检查游戏是否已安装补丁"""
|
||||
def on_finished(is_installed):
|
||||
callback(is_installed)
|
||||
self.patch_check_thread = None
|
||||
|
||||
self.patch_check_thread = PatchCheckThread(self._check_patch_installed_sync, game_dir, game_version)
|
||||
self.patch_check_thread.finished.connect(on_finished)
|
||||
self.patch_check_thread.start()
|
||||
|
||||
def _check_patch_installed_sync(self, game_dir, game_version):
|
||||
"""同步检查游戏是否已安装补丁(在工作线程中运行)"""
|
||||
debug_mode = self._is_debug_mode()
|
||||
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 检查 {game_version} 是否已安装补丁,目录: {game_dir}")
|
||||
|
||||
if game_version not in self.game_info:
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: {game_version} 不在支持的游戏列表中,跳过检查")
|
||||
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)
|
||||
|
||||
# 检查补丁文件和禁用的补丁文件
|
||||
if os.path.exists(patch_file_path) or os.path.exists(f"{patch_file_path}.fain"):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def check_patch_installed(self, game_dir, game_version):
|
||||
"""检查游戏是否已安装补丁(此方法可能导致阻塞,推荐使用异步版本)"""
|
||||
return self._check_patch_installed_sync(game_dir, game_version)
|
||||
|
||||
def check_patch_disabled(self, game_dir, game_version):
|
||||
"""检查游戏的补丁是否已被禁用"""
|
||||
debug_mode = self._is_debug_mode()
|
||||
|
||||
if game_version not in self.game_info:
|
||||
return False, None
|
||||
|
||||
install_path_base = os.path.basename(self.game_info[game_version]["install_path"])
|
||||
patch_file_path = os.path.join(game_dir, install_path_base)
|
||||
disabled_path = f"{patch_file_path}.fain"
|
||||
|
||||
if os.path.exists(disabled_path):
|
||||
if debug_mode:
|
||||
logger.debug(f"找到禁用的补丁文件: {disabled_path}")
|
||||
return True, disabled_path
|
||||
|
||||
if debug_mode:
|
||||
logger.debug(f"{game_version} 在 {game_dir} 的补丁未被禁用")
|
||||
|
||||
return False, None
|
||||
|
||||
def detect_installable_games(self, game_dirs):
|
||||
"""检测可安装补丁的游戏"""
|
||||
debug_mode = self._is_debug_mode()
|
||||
|
||||
if debug_mode:
|
||||
logger.debug(f"开始检测可安装补丁的游戏,游戏目录: {game_dirs}")
|
||||
|
||||
already_installed_games = []
|
||||
installable_games = []
|
||||
disabled_patch_games = []
|
||||
|
||||
for game_version, game_dir in game_dirs.items():
|
||||
is_patch_installed = self.check_patch_installed(game_dir, game_version)
|
||||
hash_check_passed = self.main_window.installed_status.get(game_version, False)
|
||||
|
||||
if is_patch_installed or hash_check_passed:
|
||||
if debug_mode:
|
||||
logger.debug(f"{game_version} 已安装补丁,不需要再次安装")
|
||||
logger.debug(f"文件检查结果: {is_patch_installed}, 哈希检查结果: {hash_check_passed}")
|
||||
already_installed_games.append(game_version)
|
||||
self.main_window.installed_status[game_version] = True
|
||||
else:
|
||||
is_disabled, disabled_path = self.check_patch_disabled(game_dir, game_version)
|
||||
if is_disabled:
|
||||
if debug_mode:
|
||||
logger.debug(f"{game_version} 存在被禁用的补丁: {disabled_path}")
|
||||
disabled_patch_games.append(game_version)
|
||||
else:
|
||||
if debug_mode:
|
||||
logger.debug(f"{game_version} 未安装补丁,可以安装")
|
||||
logger.debug(f"文件检查结果: {is_patch_installed}, 哈希检查结果: {hash_check_passed}")
|
||||
installable_games.append(game_version)
|
||||
|
||||
if debug_mode:
|
||||
logger.debug(f"检测结果 - 已安装补丁: {already_installed_games}")
|
||||
logger.debug(f"检测结果 - 可安装补丁: {installable_games}")
|
||||
logger.debug(f"检测结果 - 禁用补丁: {disabled_patch_games}")
|
||||
|
||||
return already_installed_games, installable_games, disabled_patch_games
|
||||
|
||||
def verify_patch_hash(self, game_version, file_path):
|
||||
"""验证补丁文件的哈希值"""
|
||||
expected_hash = self.plugin_hash.get(game_version, "")
|
||||
|
||||
if not expected_hash:
|
||||
logger.warning(f"DEBUG: 未找到 {game_version} 的预期哈希值")
|
||||
return False
|
||||
|
||||
debug_mode = self._is_debug_mode()
|
||||
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 开始验证补丁文件: {file_path}")
|
||||
logger.debug(f"DEBUG: 游戏版本: {game_version}")
|
||||
logger.debug(f"DEBUG: 预期哈希值: {expected_hash}")
|
||||
|
||||
try:
|
||||
if not os.path.exists(file_path) or os.path.getsize(file_path) == 0:
|
||||
return False
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 创建临时目录: {temp_dir}")
|
||||
|
||||
try:
|
||||
with py7zr.SevenZipFile(file_path, mode="r") as archive:
|
||||
archive.extractall(path=temp_dir)
|
||||
except Exception as e:
|
||||
if debug_mode:
|
||||
logger.error(f"DEBUG: 解压补丁文件失败: {e}")
|
||||
return False
|
||||
|
||||
patch_file = self._find_patch_file_in_temp_dir(temp_dir, game_version)
|
||||
|
||||
if not patch_file or not os.path.exists(patch_file):
|
||||
if debug_mode:
|
||||
logger.warning(f"DEBUG: 未找到解压后的补丁文件")
|
||||
return False
|
||||
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 找到解压后的补丁文件: {patch_file}")
|
||||
|
||||
try:
|
||||
with open(patch_file, "rb") as f:
|
||||
file_hash = hashlib.sha256(f.read()).hexdigest()
|
||||
|
||||
result = file_hash.lower() == expected_hash.lower()
|
||||
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 补丁文件 {patch_file} 哈希值验证: {'成功' if result else '失败'}")
|
||||
|
||||
return result
|
||||
except Exception as e:
|
||||
if debug_mode:
|
||||
logger.error(f"DEBUG: 计算补丁文件哈希值失败: {e}")
|
||||
return False
|
||||
except Exception as e:
|
||||
if debug_mode:
|
||||
logger.error(f"DEBUG: 验证补丁哈希值失败: {e}")
|
||||
return False
|
||||
|
||||
def _find_patch_file_in_temp_dir(self, temp_dir, game_version):
|
||||
"""在临时目录中查找解压后的补丁文件"""
|
||||
game_patch_map = {
|
||||
"Vol.1": os.path.join("vol.1", "adultsonly.xp3"),
|
||||
"Vol.2": os.path.join("vol.2", "adultsonly.xp3"),
|
||||
"Vol.3": os.path.join("vol.3", "update00.int"),
|
||||
"Vol.4": os.path.join("vol.4", "vol4adult.xp3"),
|
||||
"After": os.path.join("after", "afteradult.xp3"),
|
||||
}
|
||||
|
||||
for version_keyword, relative_path in game_patch_map.items():
|
||||
if version_keyword in game_version:
|
||||
return os.path.join(temp_dir, relative_path)
|
||||
|
||||
# 如果没有找到,则进行通用搜索
|
||||
for root, dirs, files in os.walk(temp_dir):
|
||||
for file in files:
|
||||
if file.endswith('.xp3') or file.endswith('.int'):
|
||||
return os.path.join(root, file)
|
||||
return None
|
||||
|
||||
def create_hash_thread(self, mode, install_paths):
|
||||
from workers.hash_thread import HashThread
|
||||
return HashThread(mode, install_paths, PLUGIN_HASH, self.main_window.installed_status, self.main_window)
|
||||
|
||||
def after_hash_compare(self):
|
||||
is_offline = self.main_window.offline_mode_manager.is_in_offline_mode()
|
||||
|
||||
self.main_window.close_hash_msg_box()
|
||||
self.main_window.hash_msg_box = self.main_window.hash_manager.hash_pop_window(check_type="after", is_offline=is_offline)
|
||||
|
||||
install_paths = self.main_window.download_manager.get_install_paths()
|
||||
|
||||
self.main_window.hash_thread = self.create_hash_thread("after", install_paths)
|
||||
self.main_window.hash_thread.after_finished.connect(self.on_after_hash_finished)
|
||||
self.main_window.hash_thread.start()
|
||||
|
||||
def on_after_hash_finished(self, result):
|
||||
self.main_window.close_hash_msg_box()
|
||||
|
||||
if not result["passed"]:
|
||||
self.main_window.setEnabled(True)
|
||||
game = result.get("game", "未知游戏")
|
||||
message = result.get("message", "发生未知错误。")
|
||||
QMessageBox.critical(self.main_window, f"文件校验失败 - {APP_NAME}", message)
|
||||
|
||||
self.main_window.setEnabled(True)
|
||||
if hasattr(self.main_window, 'window_manager'):
|
||||
self.main_window.window_manager.change_window_state(self.main_window.window_manager.STATE_READY)
|
||||
QTimer.singleShot(100, self.main_window.show_result)
|
||||
|
||||
def on_offline_pre_hash_finished(self, updated_status, game_dirs):
|
||||
self.main_window.installed_status = updated_status
|
||||
|
||||
if self.main_window.hash_msg_box and self.main_window.hash_msg_box.isVisible():
|
||||
self.main_window.hash_msg_box.accept()
|
||||
self.main_window.hash_msg_box = None
|
||||
|
||||
self.main_window.setEnabled(True)
|
||||
|
||||
already_installed_games, installable_games, disabled_patch_games = self.detect_installable_games(game_dirs)
|
||||
|
||||
status_message = ""
|
||||
if already_installed_games:
|
||||
status_message += f"已安装补丁的游戏:\n{chr(10).join(already_installed_games)}\n\n"
|
||||
|
||||
if disabled_patch_games:
|
||||
disabled_msg = f"检测到以下游戏的补丁已被禁用:\n{chr(10).join(disabled_patch_games)}\n\n是否要启用这些补丁?"
|
||||
|
||||
from PySide6 import QtWidgets
|
||||
reply = QtWidgets.QMessageBox.question(
|
||||
self.main_window,
|
||||
f"检测到禁用补丁 - {APP_NAME}",
|
||||
disabled_msg,
|
||||
QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No
|
||||
)
|
||||
|
||||
if reply == QtWidgets.QMessageBox.StandardButton.Yes:
|
||||
disabled_game_dirs = {game: game_dirs[game] for game in disabled_patch_games}
|
||||
|
||||
success_count, fail_count, results = self.main_window.patch_manager.batch_toggle_patches(
|
||||
disabled_game_dirs,
|
||||
operation="enable"
|
||||
)
|
||||
|
||||
self.main_window.patch_manager.show_toggle_result(success_count, fail_count, results)
|
||||
|
||||
for game_version in disabled_patch_games:
|
||||
self.main_window.installed_status[game_version] = True
|
||||
if game_version in installable_games:
|
||||
installable_games.remove(game_version)
|
||||
if game_version not in already_installed_games:
|
||||
already_installed_games.append(game_version)
|
||||
else:
|
||||
installable_games.extend(disabled_patch_games)
|
||||
|
||||
if disabled_patch_games:
|
||||
status_message += f"禁用补丁的游戏:\n{chr(10).join(disabled_patch_games)}\n\n"
|
||||
|
||||
if not installable_games:
|
||||
if already_installed_games:
|
||||
QMessageBox.information(
|
||||
self.main_window,
|
||||
f"信息 - {APP_NAME}",
|
||||
f"\n所有游戏已安装补丁,无需重复安装。\n\n{status_message}",
|
||||
)
|
||||
else:
|
||||
QMessageBox.warning(
|
||||
self.main_window,
|
||||
f"警告 - {APP_NAME}",
|
||||
"\n未检测到任何需要安装补丁的游戏。\n\n请确保游戏文件夹位于选择的目录中。\n",
|
||||
)
|
||||
|
||||
if hasattr(self.main_window, 'window_manager'):
|
||||
self.main_window.window_manager.change_window_state(self.main_window.window_manager.STATE_READY)
|
||||
return
|
||||
|
||||
from PySide6 import QtWidgets
|
||||
dialog = QtWidgets.QDialog(self.main_window)
|
||||
dialog.setWindowTitle(f"选择要安装的游戏 - {APP_NAME}")
|
||||
dialog.setMinimumWidth(300)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
label = QtWidgets.QLabel("请选择要安装补丁的游戏:")
|
||||
layout.addWidget(label)
|
||||
|
||||
list_widget = QtWidgets.QListWidget()
|
||||
list_widget.setSelectionMode(QtWidgets.QAbstractItemView.SelectionMode.MultiSelection)
|
||||
|
||||
for game in installable_games:
|
||||
item = QtWidgets.QListWidgetItem(game)
|
||||
list_widget.addItem(item)
|
||||
item.setSelected(True)
|
||||
|
||||
layout.addWidget(list_widget)
|
||||
|
||||
button_box = QtWidgets.QDialogButtonBox(
|
||||
QtWidgets.QDialogButtonBox.StandardButton.Ok |
|
||||
QtWidgets.QDialogButtonBox.StandardButton.Cancel
|
||||
)
|
||||
button_box.accepted.connect(dialog.accept)
|
||||
button_box.rejected.connect(dialog.reject)
|
||||
layout.addWidget(button_box)
|
||||
|
||||
dialog.setLayout(layout)
|
||||
|
||||
result = dialog.exec()
|
||||
if result != QtWidgets.QDialog.DialogCode.Accepted or not list_widget.selectedItems():
|
||||
if hasattr(self.main_window, 'window_manager'):
|
||||
self.main_window.window_manager.change_window_state(self.main_window.window_manager.STATE_READY)
|
||||
return
|
||||
|
||||
selected_games = [item.text() for item in list_widget.selectedItems()]
|
||||
|
||||
self.main_window.offline_mode_manager.install_offline_patches(selected_games)
|
||||
983
source/core/managers/patch_manager.py
Normal file
@@ -0,0 +1,983 @@
|
||||
import os
|
||||
import shutil
|
||||
import traceback
|
||||
from PySide6.QtWidgets import QMessageBox
|
||||
from utils.logger import setup_logger
|
||||
from config.config import APP_NAME
|
||||
from utils import msgbox_frame
|
||||
|
||||
class PatchManager:
|
||||
"""补丁管理器,用于处理补丁的安装和卸载"""
|
||||
|
||||
def __init__(self, app_name, game_info, debug_manager=None, main_window=None):
|
||||
"""初始化补丁管理器
|
||||
|
||||
Args:
|
||||
app_name: 应用程序名称,用于显示消息框标题
|
||||
game_info: 游戏信息字典,包含各版本的安装路径和可执行文件名
|
||||
debug_manager: 调试管理器实例,用于输出调试信息
|
||||
main_window: 主窗口实例,用于访问UI和状态
|
||||
"""
|
||||
self.app_name = app_name
|
||||
self.game_info = game_info
|
||||
self.debug_manager = debug_manager
|
||||
self.main_window = main_window # 添加main_window属性
|
||||
self.installed_status = {} # 游戏版本的安装状态
|
||||
self.logger = setup_logger("patch_manager")
|
||||
self.patch_detector = None # 将在main_window初始化后设置
|
||||
|
||||
def set_patch_detector(self, patch_detector):
|
||||
"""设置补丁检测器实例
|
||||
|
||||
Args:
|
||||
patch_detector: 补丁检测器实例
|
||||
"""
|
||||
self.patch_detector = patch_detector
|
||||
|
||||
def _is_debug_mode(self):
|
||||
"""检查是否处于调试模式
|
||||
|
||||
Returns:
|
||||
bool: 是否处于调试模式
|
||||
"""
|
||||
if hasattr(self.debug_manager, 'ui_manager') and hasattr(self.debug_manager.ui_manager, 'debug_action'):
|
||||
return self.debug_manager.ui_manager.debug_action.isChecked()
|
||||
return False
|
||||
|
||||
def initialize_status(self):
|
||||
"""初始化所有游戏版本的安装状态"""
|
||||
self.installed_status = {f"NEKOPARA Vol.{i}": False for i in range(1, 5)}
|
||||
self.installed_status["NEKOPARA After"] = False
|
||||
|
||||
def update_status(self, game_version, is_installed):
|
||||
"""更新游戏版本的安装状态
|
||||
|
||||
Args:
|
||||
game_version: 游戏版本
|
||||
is_installed: 是否已安装
|
||||
"""
|
||||
self.installed_status[game_version] = is_installed
|
||||
|
||||
def get_status(self, game_version=None):
|
||||
"""获取游戏版本的安装状态
|
||||
|
||||
Args:
|
||||
game_version: 游戏版本,如果为None则返回所有状态
|
||||
|
||||
Returns:
|
||||
bool或dict: 指定版本的安装状态或所有版本的安装状态
|
||||
"""
|
||||
if game_version:
|
||||
return self.installed_status.get(game_version, False)
|
||||
return self.installed_status
|
||||
|
||||
def uninstall_patch(self, game_dir, game_version, silent=False):
|
||||
"""卸载补丁
|
||||
|
||||
Args:
|
||||
game_dir: 游戏目录路径
|
||||
game_version: 游戏版本
|
||||
silent: 是否静默模式(不显示弹窗)
|
||||
|
||||
Returns:
|
||||
bool: 卸载成功返回True,失败返回False
|
||||
dict: 在silent=True时,返回包含卸载结果信息的字典
|
||||
"""
|
||||
debug_mode = self._is_debug_mode()
|
||||
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: 开始卸载 {game_version} 补丁,目录: {game_dir}")
|
||||
|
||||
self.logger.info(f"开始卸载 {game_version} 补丁,目录: {game_dir}")
|
||||
|
||||
if game_version not in self.game_info:
|
||||
error_msg = f"无法识别游戏版本: {game_version}"
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: 卸载失败 - {error_msg}")
|
||||
self.logger.error(f"卸载失败 - {error_msg}")
|
||||
|
||||
if not silent:
|
||||
QMessageBox.critical(
|
||||
None,
|
||||
f"错误 - {self.app_name}",
|
||||
f"\n{error_msg}\n",
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
return False if not silent else {"success": False, "message": error_msg, "files_removed": 0}
|
||||
|
||||
try:
|
||||
files_removed = 0
|
||||
|
||||
# 获取可能的补丁文件路径
|
||||
install_path_base = os.path.basename(self.game_info[game_version]["install_path"])
|
||||
patch_file_path = os.path.join(game_dir, install_path_base)
|
||||
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: 基础补丁文件路径: {patch_file_path}")
|
||||
|
||||
# 尝试查找补丁文件,支持不同大小写
|
||||
patch_files_to_check = [
|
||||
patch_file_path,
|
||||
patch_file_path.lower(),
|
||||
patch_file_path.upper(),
|
||||
patch_file_path.replace("_", ""),
|
||||
patch_file_path.replace("_", "-"),
|
||||
]
|
||||
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: 查找以下可能的补丁文件路径: {patch_files_to_check}")
|
||||
|
||||
# 查找并删除补丁文件,包括启用和禁用的
|
||||
patch_file_found = False
|
||||
for patch_path in patch_files_to_check:
|
||||
# 检查常规补丁文件
|
||||
if os.path.exists(patch_path):
|
||||
patch_file_found = True
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: 找到补丁文件: {patch_path},准备删除")
|
||||
self.logger.debug(f"删除补丁文件: {patch_path}")
|
||||
|
||||
os.remove(patch_path)
|
||||
files_removed += 1
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: 已删除补丁文件: {patch_path}")
|
||||
|
||||
# 检查被禁用的补丁文件(带.fain后缀)
|
||||
disabled_path = f"{patch_path}.fain"
|
||||
if os.path.exists(disabled_path):
|
||||
patch_file_found = True
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: 找到被禁用的补丁文件: {disabled_path},准备删除")
|
||||
self.logger.debug(f"删除被禁用的补丁文件: {disabled_path}")
|
||||
|
||||
os.remove(disabled_path)
|
||||
files_removed += 1
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: 已删除被禁用的补丁文件: {disabled_path}")
|
||||
|
||||
if not patch_file_found:
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: 未找到补丁文件,检查了以下路径: {patch_files_to_check}")
|
||||
self.logger.debug(f"DEBUG: 也检查了禁用的补丁文件(.fain后缀)")
|
||||
self.logger.warning(f"未找到 {game_version} 的补丁文件")
|
||||
|
||||
# 检查是否有额外的签名文件 (.sig)
|
||||
if game_version == "NEKOPARA After":
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: {game_version} 需要检查额外的签名文件")
|
||||
|
||||
for patch_path in patch_files_to_check:
|
||||
# 检查常规签名文件
|
||||
sig_file_path = f"{patch_path}.sig"
|
||||
if os.path.exists(sig_file_path):
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: 找到签名文件: {sig_file_path},准备删除")
|
||||
self.logger.debug(f"删除签名文件: {sig_file_path}")
|
||||
|
||||
os.remove(sig_file_path)
|
||||
files_removed += 1
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: 已删除签名文件: {sig_file_path}")
|
||||
|
||||
# 检查被禁用补丁的签名文件
|
||||
disabled_sig_path = f"{patch_path}.fain.sig"
|
||||
if os.path.exists(disabled_sig_path):
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: 找到被禁用补丁的签名文件: {disabled_sig_path},准备删除")
|
||||
self.logger.debug(f"删除被禁用补丁的签名文件: {disabled_sig_path}")
|
||||
|
||||
os.remove(disabled_sig_path)
|
||||
files_removed += 1
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: 已删除被禁用补丁的签名文件: {disabled_sig_path}")
|
||||
|
||||
# 删除patch文件夹
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: 检查并删除patch文件夹")
|
||||
|
||||
patch_folders_to_check = [
|
||||
os.path.join(game_dir, "patch"),
|
||||
os.path.join(game_dir, "Patch"),
|
||||
os.path.join(game_dir, "PATCH"),
|
||||
]
|
||||
|
||||
for patch_folder in patch_folders_to_check:
|
||||
if os.path.exists(patch_folder):
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: 找到补丁文件夹: {patch_folder},准备删除")
|
||||
self.logger.debug(f"删除补丁文件夹: {patch_folder}")
|
||||
|
||||
import shutil
|
||||
shutil.rmtree(patch_folder)
|
||||
files_removed += 1
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: 已删除补丁文件夹: {patch_folder}")
|
||||
|
||||
# 删除game/patch文件夹
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: 检查并删除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:
|
||||
self.logger.debug(f"DEBUG: 找到game/patch文件夹: {game_patch_folder},准备删除")
|
||||
self.logger.debug(f"删除game/patch文件夹: {game_patch_folder}")
|
||||
|
||||
import shutil
|
||||
shutil.rmtree(game_patch_folder)
|
||||
files_removed += 1
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: 已删除game/patch文件夹: {game_patch_folder}")
|
||||
|
||||
# 删除配置文件
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: 检查并删除配置文件和脚本文件")
|
||||
|
||||
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:
|
||||
self.logger.debug(f"DEBUG: 找到配置文件: {config_path},准备删除")
|
||||
self.logger.debug(f"删除配置文件: {config_path}")
|
||||
|
||||
os.remove(config_path)
|
||||
files_removed += 1
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: 已删除配置文件: {config_path}")
|
||||
|
||||
# 删除脚本文件
|
||||
for script_file in script_files:
|
||||
script_path = os.path.join(game_path, script_file)
|
||||
if os.path.exists(script_path):
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: 找到脚本文件: {script_path},准备删除")
|
||||
self.logger.debug(f"删除脚本文件: {script_path}")
|
||||
|
||||
os.remove(script_path)
|
||||
files_removed += 1
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: 已删除脚本文件: {script_path}")
|
||||
|
||||
# 更新安装状态
|
||||
self.installed_status[game_version] = False
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: 已更新 {game_version} 的安装状态为未安装")
|
||||
|
||||
# 在非静默模式且非批量卸载模式下显示卸载成功消息
|
||||
if not silent and game_version != "all":
|
||||
# 显示卸载成功消息
|
||||
if files_removed > 0:
|
||||
success_msg = f"\n{game_version} 补丁卸载成功!\n共删除 {files_removed} 个文件/文件夹。\n"
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: 显示卸载成功消息: {success_msg}")
|
||||
|
||||
QMessageBox.information(
|
||||
None,
|
||||
f"卸载完成 - {self.app_name}",
|
||||
success_msg,
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
else:
|
||||
warning_msg = f"\n未找到 {game_version} 的补丁文件,可能未安装补丁或已被移除。\n"
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: 显示警告消息: {warning_msg}")
|
||||
|
||||
QMessageBox.warning(
|
||||
None,
|
||||
f"警告 - {self.app_name}",
|
||||
warning_msg,
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
|
||||
# 卸载成功
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: {game_version} 卸载完成,共删除 {files_removed} 个文件/文件夹")
|
||||
self.logger.info(f"{game_version} 卸载完成,共删除 {files_removed} 个文件/文件夹")
|
||||
|
||||
if silent:
|
||||
return {"success": True, "message": f"{game_version} 补丁卸载成功", "files_removed": files_removed}
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
error_message = f"卸载 {game_version} 补丁时出错:{str(e)}"
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: {error_message}")
|
||||
import traceback
|
||||
self.logger.debug(f"DEBUG: 错误详情:\n{traceback.format_exc()}")
|
||||
self.logger.error(error_message)
|
||||
|
||||
# 在非静默模式且非批量卸载模式下显示卸载失败消息
|
||||
if not silent and game_version != "all":
|
||||
# 显示卸载失败消息
|
||||
error_message = f"\n卸载 {game_version} 补丁时出错:\n\n{str(e)}\n"
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: 显示卸载失败消息")
|
||||
|
||||
QMessageBox.critical(
|
||||
None,
|
||||
f"卸载失败 - {self.app_name}",
|
||||
error_message,
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
|
||||
# 卸载失败
|
||||
if silent:
|
||||
return {"success": False, "message": f"卸载 {game_version} 补丁时出错: {str(e)}", "files_removed": 0}
|
||||
return False
|
||||
|
||||
def batch_uninstall_patches(self, game_dirs):
|
||||
"""批量卸载多个游戏的补丁
|
||||
|
||||
Args:
|
||||
game_dirs: 游戏版本到游戏目录的映射字典
|
||||
|
||||
Returns:
|
||||
tuple: (成功数量, 失败数量, 详细结果列表)
|
||||
"""
|
||||
success_count = 0
|
||||
fail_count = 0
|
||||
debug_mode = self._is_debug_mode()
|
||||
results = []
|
||||
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: 开始批量卸载补丁,游戏数量: {len(game_dirs)}")
|
||||
self.logger.debug(f"DEBUG: 要卸载的游戏: {list(game_dirs.keys())}")
|
||||
|
||||
self.logger.info(f"开始批量卸载补丁,游戏数量: {len(game_dirs)}")
|
||||
self.logger.debug(f"要卸载的游戏: {list(game_dirs.keys())}")
|
||||
|
||||
for version, path in game_dirs.items():
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: 处理游戏 {version},路径: {path}")
|
||||
|
||||
self.logger.info(f"开始卸载 {version} 的补丁")
|
||||
|
||||
try:
|
||||
# 在批量模式下使用静默卸载
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: 使用静默模式卸载 {version}")
|
||||
|
||||
result = self.uninstall_patch(path, version, silent=True)
|
||||
|
||||
if isinstance(result, dict): # 使用了静默模式
|
||||
if result["success"]:
|
||||
success_count += 1
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: {version} 卸载成功,删除了 {result['files_removed']} 个文件/文件夹")
|
||||
self.logger.info(f"{version} 卸载成功,删除了 {result['files_removed']} 个文件/文件夹")
|
||||
else:
|
||||
fail_count += 1
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: {version} 卸载失败,原因: {result['message']}")
|
||||
self.logger.warning(f"{version} 卸载失败,原因: {result['message']}")
|
||||
|
||||
results.append({
|
||||
"version": version,
|
||||
"success": result["success"],
|
||||
"message": result["message"],
|
||||
"files_removed": result["files_removed"]
|
||||
})
|
||||
else: # 兼容旧代码,不应该执行到这里
|
||||
if result:
|
||||
success_count += 1
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: {version} 卸载成功(旧格式)")
|
||||
self.logger.info(f"{version} 卸载成功(旧格式)")
|
||||
else:
|
||||
fail_count += 1
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: {version} 卸载失败(旧格式)")
|
||||
self.logger.warning(f"{version} 卸载失败(旧格式)")
|
||||
|
||||
results.append({
|
||||
"version": version,
|
||||
"success": result,
|
||||
"message": f"{version} 卸载{'成功' if result else '失败'}",
|
||||
"files_removed": 0
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: 卸载 {version} 时出错: {str(e)}")
|
||||
import traceback
|
||||
self.logger.debug(f"DEBUG: 错误详情:\n{traceback.format_exc()}")
|
||||
|
||||
self.logger.error(f"卸载 {version} 时出错: {str(e)}")
|
||||
|
||||
fail_count += 1
|
||||
results.append({
|
||||
"version": version,
|
||||
"success": False,
|
||||
"message": f"卸载出错: {str(e)}",
|
||||
"files_removed": 0
|
||||
})
|
||||
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: 批量卸载完成,成功: {success_count},失败: {fail_count}")
|
||||
|
||||
self.logger.info(f"批量卸载完成,成功: {success_count},失败: {fail_count}")
|
||||
|
||||
return success_count, fail_count, results
|
||||
|
||||
def show_uninstall_result(self, success_count, fail_count, results=None):
|
||||
"""显示批量卸载结果
|
||||
|
||||
Args:
|
||||
success_count: 成功卸载的数量
|
||||
fail_count: 卸载失败的数量
|
||||
results: 详细结果列表,如果提供,会显示更详细的信息
|
||||
"""
|
||||
debug_mode = self._is_debug_mode()
|
||||
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: 显示卸载结果,成功: {success_count},失败: {fail_count}")
|
||||
|
||||
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 debug_mode:
|
||||
self.logger.debug(f"DEBUG: 成功卸载的游戏: {success_list}")
|
||||
self.logger.debug(f"DEBUG: 卸载失败的游戏: {fail_list}")
|
||||
|
||||
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"
|
||||
|
||||
# 记录更详细的失败原因
|
||||
if debug_mode:
|
||||
for r in results:
|
||||
if not r["success"]:
|
||||
self.logger.debug(f"DEBUG: {r['version']} 卸载失败原因: {r['message']}")
|
||||
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: 显示卸载结果对话框")
|
||||
|
||||
QMessageBox.information(
|
||||
None,
|
||||
f"批量卸载完成 - {self.app_name}",
|
||||
result_text,
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
|
||||
def check_patch_installed(self, game_dir, game_version):
|
||||
"""检查游戏是否已安装补丁(调用patch_detector)
|
||||
|
||||
Args:
|
||||
game_dir: 游戏目录路径
|
||||
game_version: 游戏版本
|
||||
|
||||
Returns:
|
||||
bool: 如果已安装补丁或有被禁用的补丁文件返回True,否则返回False
|
||||
"""
|
||||
if self.patch_detector:
|
||||
return self.patch_detector.check_patch_installed(game_dir, game_version)
|
||||
|
||||
# 如果patch_detector未设置,使用原始逻辑(应该不会执行到这里)
|
||||
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:
|
||||
self.logger.debug(f"找到补丁文件: {patch_path}")
|
||||
return True
|
||||
# 检查是否存在被禁用的补丁文件(带.fain后缀)
|
||||
disabled_path = f"{patch_path}.fain"
|
||||
if os.path.exists(disabled_path):
|
||||
if debug_mode:
|
||||
self.logger.debug(f"找到被禁用的补丁文件: {disabled_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:
|
||||
self.logger.debug(f"找到补丁文件夹: {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:
|
||||
self.logger.debug(f"找到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:
|
||||
self.logger.debug(f"找到配置文件: {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:
|
||||
self.logger.debug(f"找到脚本文件: {script_path}")
|
||||
return True
|
||||
|
||||
# 没有找到补丁文件或文件夹
|
||||
if debug_mode:
|
||||
self.logger.debug(f"{game_version} 在 {game_dir} 中没有安装补丁")
|
||||
return False
|
||||
|
||||
def check_patch_disabled(self, game_dir, game_version):
|
||||
"""检查游戏的补丁是否已被禁用(调用patch_detector)
|
||||
|
||||
Args:
|
||||
game_dir: 游戏目录路径
|
||||
game_version: 游戏版本
|
||||
|
||||
Returns:
|
||||
bool: 如果补丁被禁用返回True,否则返回False
|
||||
str: 禁用的补丁文件路径,如果没有禁用返回None
|
||||
"""
|
||||
if self.patch_detector:
|
||||
return self.patch_detector.check_patch_disabled(game_dir, game_version)
|
||||
|
||||
# 如果patch_detector未设置,使用原始逻辑(应该不会执行到这里)
|
||||
debug_mode = self._is_debug_mode()
|
||||
|
||||
if game_version not in self.game_info:
|
||||
return False, None
|
||||
|
||||
# 获取可能的补丁文件路径
|
||||
install_path_base = os.path.basename(self.game_info[game_version]["install_path"])
|
||||
patch_file_path = os.path.join(game_dir, install_path_base)
|
||||
|
||||
# 检查是否存在禁用的补丁文件(.fain后缀)
|
||||
disabled_patch_files = [
|
||||
f"{patch_file_path}.fain",
|
||||
f"{patch_file_path.lower()}.fain",
|
||||
f"{patch_file_path.upper()}.fain",
|
||||
f"{patch_file_path.replace('_', '')}.fain",
|
||||
f"{patch_file_path.replace('_', '-')}.fain",
|
||||
]
|
||||
|
||||
# 检查是否有禁用的补丁文件
|
||||
for disabled_path in disabled_patch_files:
|
||||
if os.path.exists(disabled_path):
|
||||
if debug_mode:
|
||||
self.logger.debug(f"找到禁用的补丁文件: {disabled_path}")
|
||||
return True, disabled_path
|
||||
|
||||
if debug_mode:
|
||||
self.logger.debug(f"{game_version} 在 {game_dir} 的补丁未被禁用")
|
||||
|
||||
return False, None
|
||||
|
||||
def toggle_patch(self, game_dir, game_version, operation=None, silent=False):
|
||||
"""切换补丁的禁用/启用状态
|
||||
|
||||
Args:
|
||||
game_dir: 游戏目录路径
|
||||
game_version: 游戏版本
|
||||
operation: 指定操作,可以是"enable"、"disable"或None(None则自动切换当前状态)
|
||||
silent: 是否静默模式(不显示弹窗)
|
||||
|
||||
Returns:
|
||||
dict: 包含操作结果信息的字典
|
||||
"""
|
||||
debug_mode = self._is_debug_mode()
|
||||
|
||||
if debug_mode:
|
||||
self.logger.debug(f"开始切换补丁状态 - 游戏版本: {game_version}, 游戏目录: {game_dir}, 操作: {operation}")
|
||||
|
||||
if game_version not in self.game_info:
|
||||
if debug_mode:
|
||||
self.logger.debug(f"无法识别游戏版本: {game_version}")
|
||||
if not silent:
|
||||
QMessageBox.critical(
|
||||
None,
|
||||
f"错误 - {self.app_name}",
|
||||
f"\n无法识别游戏版本: {game_version}\n",
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
return {"success": False, "message": f"无法识别游戏版本: {game_version}", "action": "none"}
|
||||
|
||||
# 检查补丁是否已安装
|
||||
is_patch_installed = self.check_patch_installed(game_dir, game_version)
|
||||
if debug_mode:
|
||||
self.logger.debug(f"补丁安装状态检查结果: {is_patch_installed}")
|
||||
|
||||
if not is_patch_installed:
|
||||
if debug_mode:
|
||||
self.logger.debug(f"{game_version} 未安装补丁,无法进行禁用/启用操作")
|
||||
if not silent:
|
||||
QMessageBox.warning(
|
||||
None,
|
||||
f"提示 - {self.app_name}",
|
||||
f"\n{game_version} 未安装补丁,无法进行禁用/启用操作。\n",
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
return {"success": False, "message": f"{game_version} 未安装补丁", "action": "none"}
|
||||
|
||||
try:
|
||||
# 检查当前状态
|
||||
is_disabled, disabled_path = self.check_patch_disabled(game_dir, game_version)
|
||||
if debug_mode:
|
||||
self.logger.debug(f"补丁禁用状态检查结果 - 是否禁用: {is_disabled}, 禁用路径: {disabled_path}")
|
||||
|
||||
# 获取可能的补丁文件路径
|
||||
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("_", "-"),
|
||||
]
|
||||
|
||||
if debug_mode:
|
||||
self.logger.debug(f"将检查以下可能的补丁文件: {patch_files_to_check}")
|
||||
|
||||
# 确定操作类型
|
||||
if operation:
|
||||
if operation == "enable":
|
||||
action_needed = is_disabled # 只有当前是禁用状态时才需要启用
|
||||
elif operation == "disable":
|
||||
action_needed = not is_disabled # 只有当前是启用状态时才需要禁用
|
||||
else:
|
||||
action_needed = True # 无效操作类型,强制进行操作
|
||||
else:
|
||||
action_needed = True # 未指定操作类型,始终执行切换
|
||||
|
||||
if debug_mode:
|
||||
self.logger.debug(f"操作决策 - 操作类型: {operation}, 是否需要执行操作: {action_needed}")
|
||||
|
||||
if not action_needed:
|
||||
# 补丁已经是目标状态,无需操作
|
||||
if operation == "enable":
|
||||
message = f"{game_version} 补丁已经是启用状态"
|
||||
else:
|
||||
message = f"{game_version} 补丁已经是禁用状态"
|
||||
|
||||
if debug_mode:
|
||||
self.logger.debug(f"{message}, 无需操作")
|
||||
|
||||
if not silent:
|
||||
QMessageBox.information(
|
||||
None,
|
||||
f"提示 - {self.app_name}",
|
||||
f"\n{message}\n",
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
return {"success": True, "message": message, "action": "none"}
|
||||
|
||||
if is_disabled:
|
||||
# 当前是禁用状态,需要启用
|
||||
if disabled_path and os.path.exists(disabled_path):
|
||||
# 从禁用文件名去掉.fain后缀
|
||||
enabled_path = disabled_path[:-5] # 去掉.fain
|
||||
if debug_mode:
|
||||
self.logger.debug(f"正在启用补丁 - 从 {disabled_path} 重命名为 {enabled_path}")
|
||||
os.rename(disabled_path, enabled_path)
|
||||
if debug_mode:
|
||||
self.logger.debug(f"已启用 {game_version} 的补丁,重命名文件成功")
|
||||
action = "enable"
|
||||
message = f"{game_version} 补丁已启用"
|
||||
else:
|
||||
# 未找到禁用的补丁文件,但状态是禁用
|
||||
message = f"未找到禁用的补丁文件: {disabled_path}"
|
||||
if debug_mode:
|
||||
self.logger.debug(f"{message}")
|
||||
return {"success": False, "message": message, "action": "none"}
|
||||
else:
|
||||
# 当前是启用状态,需要禁用
|
||||
# 查找正在使用的补丁文件
|
||||
active_patch_file = None
|
||||
for patch_path in patch_files_to_check:
|
||||
if os.path.exists(patch_path):
|
||||
active_patch_file = patch_path
|
||||
if debug_mode:
|
||||
self.logger.debug(f"找到活跃的补丁文件: {active_patch_file}")
|
||||
break
|
||||
|
||||
if active_patch_file:
|
||||
# 给补丁文件添加.fain后缀禁用它
|
||||
disabled_path = f"{active_patch_file}.fain"
|
||||
if debug_mode:
|
||||
self.logger.debug(f"正在禁用补丁 - 从 {active_patch_file} 重命名为 {disabled_path}")
|
||||
os.rename(active_patch_file, disabled_path)
|
||||
if debug_mode:
|
||||
self.logger.debug(f"已禁用 {game_version} 的补丁,重命名文件成功")
|
||||
action = "disable"
|
||||
message = f"{game_version} 补丁已禁用"
|
||||
else:
|
||||
# 未找到活跃的补丁文件,但状态是启用
|
||||
message = f"未找到启用的补丁文件,请检查游戏目录: {game_dir}"
|
||||
if debug_mode:
|
||||
self.logger.debug(f"{message}")
|
||||
return {"success": False, "message": message, "action": "none"}
|
||||
|
||||
# 非静默模式下显示操作结果
|
||||
if not silent:
|
||||
QMessageBox.information(
|
||||
None,
|
||||
f"操作成功 - {self.app_name}",
|
||||
f"\n{message}\n",
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
|
||||
if debug_mode:
|
||||
self.logger.debug(f"切换补丁状态操作完成 - 结果: 成功, 操作: {action}, 消息: {message}")
|
||||
|
||||
return {"success": True, "message": message, "action": action}
|
||||
|
||||
except Exception as e:
|
||||
error_message = f"切换 {game_version} 补丁状态时出错: {str(e)}"
|
||||
|
||||
if debug_mode:
|
||||
self.logger.debug(f"{error_message}")
|
||||
import traceback
|
||||
self.logger.debug(f"错误详情:\n{traceback.format_exc()}")
|
||||
|
||||
if not silent:
|
||||
QMessageBox.critical(
|
||||
None,
|
||||
f"操作失败 - {self.app_name}",
|
||||
f"\n{error_message}\n",
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
|
||||
return {"success": False, "message": error_message, "action": "none"}
|
||||
|
||||
def batch_toggle_patches(self, game_dirs, operation=None):
|
||||
"""批量切换多个游戏补丁的禁用/启用状态
|
||||
|
||||
Args:
|
||||
game_dirs: 游戏版本到游戏目录的映射字典
|
||||
operation: 指定操作,可以是"enable"、"disable"或None(None则自动切换当前状态)
|
||||
|
||||
Returns:
|
||||
tuple: (成功数量, 失败数量, 详细结果列表)
|
||||
"""
|
||||
success_count = 0
|
||||
fail_count = 0
|
||||
debug_mode = self._is_debug_mode()
|
||||
results = []
|
||||
|
||||
if debug_mode:
|
||||
self.logger.debug(f"开始批量切换补丁状态 - 操作: {operation}, 游戏数量: {len(game_dirs)}")
|
||||
self.logger.debug(f"游戏列表: {list(game_dirs.keys())}")
|
||||
|
||||
for version, path in game_dirs.items():
|
||||
try:
|
||||
if debug_mode:
|
||||
self.logger.debug(f"处理游戏 {version}, 目录: {path}")
|
||||
|
||||
# 在批量模式下使用静默操作
|
||||
result = self.toggle_patch(path, version, operation=operation, silent=True)
|
||||
|
||||
if debug_mode:
|
||||
self.logger.debug(f"游戏 {version} 操作结果: {result}")
|
||||
|
||||
if result["success"]:
|
||||
success_count += 1
|
||||
if debug_mode:
|
||||
self.logger.debug(f"游戏 {version} 操作成功,操作类型: {result['action']}")
|
||||
else:
|
||||
fail_count += 1
|
||||
if debug_mode:
|
||||
self.logger.debug(f"游戏 {version} 操作失败,原因: {result['message']}")
|
||||
|
||||
results.append({
|
||||
"version": version,
|
||||
"success": result["success"],
|
||||
"message": result["message"],
|
||||
"action": result["action"]
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
if debug_mode:
|
||||
self.logger.debug(f"切换 {version} 补丁状态时出错: {str(e)}")
|
||||
import traceback
|
||||
self.logger.debug(f"错误详情:\n{traceback.format_exc()}")
|
||||
|
||||
fail_count += 1
|
||||
results.append({
|
||||
"version": version,
|
||||
"success": False,
|
||||
"message": f"操作出错: {str(e)}",
|
||||
"action": "none"
|
||||
})
|
||||
|
||||
if debug_mode:
|
||||
self.logger.debug(f"批量切换补丁状态完成 - 成功: {success_count}, 失败: {fail_count}")
|
||||
|
||||
return success_count, fail_count, results
|
||||
|
||||
def show_toggle_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:
|
||||
enabled_list = [r["version"] for r in results if r["success"] and r["action"] == "enable"]
|
||||
disabled_list = [r["version"] for r in results if r["success"] and r["action"] == "disable"]
|
||||
skipped_list = [r["version"] for r in results if r["success"] and r["action"] == "none"]
|
||||
fail_list = [r["version"] for r in results if not r["success"]]
|
||||
|
||||
if enabled_list:
|
||||
result_text += f"\n【已启用补丁】:\n{chr(10).join(enabled_list)}\n"
|
||||
|
||||
if disabled_list:
|
||||
result_text += f"\n【已禁用补丁】:\n{chr(10).join(disabled_list)}\n"
|
||||
|
||||
if skipped_list:
|
||||
result_text += f"\n【无需操作】:\n{chr(10).join(skipped_list)}\n"
|
||||
|
||||
if fail_list:
|
||||
result_text += f"\n【操作失败】:\n{chr(10).join(fail_list)}\n"
|
||||
|
||||
QMessageBox.information(
|
||||
None,
|
||||
f"批量操作完成 - {self.app_name}",
|
||||
result_text,
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
|
||||
def show_result(self):
|
||||
"""显示安装结果,区分不同情况"""
|
||||
# 获取当前安装状态
|
||||
installed_versions = [] # 成功安装的版本
|
||||
skipped_versions = [] # 已有补丁跳过的版本
|
||||
failed_versions = [] # 安装失败的版本
|
||||
not_found_versions = [] # 未找到的版本
|
||||
|
||||
# 获取所有游戏版本路径
|
||||
install_paths = self.main_window.download_manager.get_install_paths() if hasattr(self.main_window.download_manager, "get_install_paths") else {}
|
||||
|
||||
# 检查是否处于离线模式
|
||||
is_offline_mode = False
|
||||
if hasattr(self.main_window, 'offline_mode_manager'):
|
||||
is_offline_mode = self.main_window.offline_mode_manager.is_in_offline_mode()
|
||||
|
||||
# 获取本次实际安装的游戏列表
|
||||
installed_games = []
|
||||
|
||||
# 在线模式下使用download_queue_history
|
||||
if hasattr(self.main_window, 'download_queue_history') and self.main_window.download_queue_history:
|
||||
installed_games = self.main_window.download_queue_history
|
||||
|
||||
# 离线模式下使用offline_mode_manager.installed_games
|
||||
if is_offline_mode and hasattr(self.main_window.offline_mode_manager, 'installed_games'):
|
||||
installed_games = self.main_window.offline_mode_manager.installed_games
|
||||
|
||||
debug_mode = self._is_debug_mode()
|
||||
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: 显示安装结果,离线模式: {is_offline_mode}")
|
||||
self.logger.debug(f"DEBUG: 本次安装的游戏: {installed_games}")
|
||||
|
||||
for game_version, is_installed in self.main_window.installed_status.items():
|
||||
# 只处理install_paths中存在的游戏版本
|
||||
if game_version in install_paths:
|
||||
path = install_paths[game_version]
|
||||
|
||||
# 检查游戏是否存在但未通过本次安装补丁
|
||||
if is_installed:
|
||||
# 游戏已安装补丁
|
||||
if game_version in installed_games:
|
||||
# 本次成功安装
|
||||
installed_versions.append(game_version)
|
||||
else:
|
||||
# 已有补丁,被跳过下载
|
||||
skipped_versions.append(game_version)
|
||||
else:
|
||||
# 游戏未安装补丁
|
||||
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_failed = len(failed_versions)
|
||||
|
||||
result_text += f"安装成功:{total_installed} 个 安装失败:{total_failed} 个\n\n"
|
||||
|
||||
# 详细列表
|
||||
if installed_versions:
|
||||
result_text += f"【成功安装】:\n{chr(10).join(installed_versions)}\n\n"
|
||||
|
||||
if failed_versions:
|
||||
result_text += f"【安装失败】:\n{chr(10).join(failed_versions)}\n\n"
|
||||
|
||||
if not_found_versions:
|
||||
# 只有在真正检测到了游戏但未安装补丁时才显示
|
||||
result_text += f"【尚未安装补丁的游戏】:\n{chr(10).join(not_found_versions)}\n"
|
||||
|
||||
QMessageBox.information(
|
||||
self.main_window,
|
||||
f"安装完成 - {APP_NAME}",
|
||||
result_text
|
||||
)
|
||||
@@ -6,8 +6,8 @@ import json
|
||||
from PySide6.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QTextBrowser, QPushButton, QCheckBox, QLabel, QMessageBox
|
||||
from PySide6.QtCore import Qt
|
||||
|
||||
from data.privacy_policy import PRIVACY_POLICY_BRIEF, get_local_privacy_policy, PRIVACY_POLICY_VERSION
|
||||
from data.config import CACHE, APP_NAME, APP_VERSION
|
||||
from config.privacy_policy import PRIVACY_POLICY_BRIEF, get_local_privacy_policy, PRIVACY_POLICY_VERSION
|
||||
from config.config import CACHE, APP_NAME, APP_VERSION
|
||||
from utils import msgbox_frame
|
||||
from utils.logger import setup_logger
|
||||
|
||||
@@ -18,20 +18,20 @@ class PrivacyManager:
|
||||
"""初始化隐私协议管理器"""
|
||||
# 初始化日志
|
||||
self.logger = setup_logger("privacy_manager")
|
||||
self.logger.info("正在初始化隐私协议管理器")
|
||||
self.logger.debug("正在初始化隐私协议管理器")
|
||||
# 确保缓存目录存在
|
||||
os.makedirs(CACHE, exist_ok=True)
|
||||
self.config_file = os.path.join(CACHE, "privacy_config.json")
|
||||
self.privacy_config = self._load_privacy_config()
|
||||
|
||||
# 获取隐私协议内容和版本
|
||||
self.logger.info("读取本地隐私协议文件")
|
||||
self.logger.debug("读取本地隐私协议文件")
|
||||
self.privacy_content, self.current_privacy_version, error = get_local_privacy_policy()
|
||||
if error:
|
||||
self.logger.warning(f"读取本地隐私协议文件警告: {error}")
|
||||
# 使用默认版本作为备用
|
||||
self.current_privacy_version = PRIVACY_POLICY_VERSION
|
||||
self.logger.info(f"隐私协议版本: {self.current_privacy_version}")
|
||||
self.logger.debug(f"隐私协议版本: {self.current_privacy_version}")
|
||||
|
||||
# 检查隐私协议版本和用户同意状态
|
||||
self.privacy_accepted = self._check_privacy_acceptance()
|
||||
@@ -66,9 +66,9 @@ class PrivacyManager:
|
||||
stored_app_version = self.privacy_config.get("app_version", "0.0.0")
|
||||
privacy_accepted = self.privacy_config.get("privacy_accepted", False)
|
||||
|
||||
self.logger.info(f"存储的隐私协议版本: {stored_privacy_version}, 当前版本: {self.current_privacy_version}")
|
||||
self.logger.info(f"存储的应用版本: {stored_app_version}, 当前版本: {APP_VERSION}")
|
||||
self.logger.info(f"隐私协议接受状态: {privacy_accepted}")
|
||||
self.logger.debug(f"存储的隐私协议版本: {stored_privacy_version}, 当前版本: {self.current_privacy_version}")
|
||||
self.logger.debug(f"存储的应用版本: {stored_app_version}, 当前版本: {APP_VERSION}")
|
||||
self.logger.debug(f"隐私协议接受状态: {privacy_accepted}")
|
||||
|
||||
# 如果隐私协议版本变更,需要重新同意
|
||||
if stored_privacy_version != self.current_privacy_version:
|
||||
@@ -125,7 +125,7 @@ class PrivacyManager:
|
||||
"""
|
||||
# 如果用户已经同意了隐私协议,直接返回True不显示对话框
|
||||
if self.privacy_accepted:
|
||||
self.logger.info("用户已同意当前版本的隐私协议,无需再次显示")
|
||||
self.logger.debug("用户已同意当前版本的隐私协议,无需再次显示")
|
||||
return True
|
||||
|
||||
self.logger.info("首次运行或隐私协议版本变更,显示隐私对话框")
|
||||
417
source/core/managers/ui_manager.py
Normal file
@@ -0,0 +1,417 @@
|
||||
from PySide6.QtGui import QIcon
|
||||
from PySide6.QtWidgets import QMessageBox
|
||||
import os
|
||||
import logging
|
||||
import subprocess
|
||||
|
||||
from utils import load_base64_image, resource_path
|
||||
from config.config import APP_NAME, APP_VERSION
|
||||
from ui.components import FontStyleManager, DialogFactory, ExternalLinksHandler, MenuBuilder
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class UIManager:
|
||||
def __init__(self, main_window):
|
||||
"""初始化UI管理器
|
||||
|
||||
Args:
|
||||
main_window: 主窗口实例,用于设置UI元素
|
||||
"""
|
||||
self.main_window = main_window
|
||||
# 使用getattr获取ui属性,如果不存在则为None
|
||||
self.ui = getattr(main_window, 'ui', None)
|
||||
|
||||
# 获取主窗口的IPv6Manager实例
|
||||
self.ipv6_manager = getattr(main_window, 'ipv6_manager', None)
|
||||
|
||||
# 初始化UI组件
|
||||
self.font_style_manager = FontStyleManager()
|
||||
self.dialog_factory = DialogFactory(main_window)
|
||||
self.external_links_handler = ExternalLinksHandler(main_window, self.dialog_factory)
|
||||
self.menu_builder = MenuBuilder(main_window, self.font_style_manager, self.external_links_handler, self.dialog_factory)
|
||||
|
||||
# 保留一些快捷访问属性以保持兼容性
|
||||
self.debug_action = None
|
||||
self.disable_auto_restore_action = None
|
||||
self.disable_pre_hash_action = None
|
||||
|
||||
def setup_ui(self):
|
||||
"""设置UI元素,包括窗口图标、标题和菜单"""
|
||||
# 设置窗口图标
|
||||
icon_path = resource_path(os.path.join("assets", "images", "ICO", "icon.png"))
|
||||
if os.path.exists(icon_path):
|
||||
self.main_window.setWindowIcon(QIcon(icon_path))
|
||||
|
||||
# 获取当前离线模式状态
|
||||
is_offline_mode = False
|
||||
if hasattr(self.main_window, 'offline_mode_manager'):
|
||||
is_offline_mode = self.main_window.offline_mode_manager.is_in_offline_mode()
|
||||
|
||||
# 设置窗口标题和UI标题标签
|
||||
mode_indicator = "[离线模式]" if is_offline_mode else "[在线模式]"
|
||||
self.main_window.setWindowTitle(f"{APP_NAME} v{APP_VERSION} {mode_indicator}")
|
||||
|
||||
# 更新UI中的标题标签
|
||||
if hasattr(self.main_window, 'ui') and hasattr(self.main_window.ui, 'title_label'):
|
||||
self.main_window.ui.title_label.setText(f"{APP_NAME} v{APP_VERSION} {mode_indicator}")
|
||||
|
||||
# 使用新的菜单构建器设置所有菜单
|
||||
self.menu_builder.setup_all_menus()
|
||||
|
||||
# 保持对一些重要UI元素的引用以确保兼容性
|
||||
self.debug_action = self.menu_builder.debug_action
|
||||
self.disable_auto_restore_action = self.menu_builder.disable_auto_restore_action
|
||||
self.disable_pre_hash_action = self.menu_builder.disable_pre_hash_action
|
||||
|
||||
# 保存对工作模式菜单项的引用,确保能正确同步状态
|
||||
self.online_mode_action = self.menu_builder.online_mode_action
|
||||
self.offline_mode_action = self.menu_builder.offline_mode_action
|
||||
|
||||
# 在菜单创建完成后,强制同步一次工作模式状态
|
||||
self.sync_work_mode_menu_state()
|
||||
|
||||
# 为了向后兼容性,添加委托方法
|
||||
def create_progress_window(self, title, initial_text="准备中..."):
|
||||
"""创建进度窗口(委托给dialog_factory)"""
|
||||
return self.dialog_factory.create_progress_window(title, initial_text)
|
||||
|
||||
def show_loading_dialog(self, message):
|
||||
"""显示加载对话框(委托给dialog_factory)"""
|
||||
return self.dialog_factory.show_loading_dialog(message)
|
||||
|
||||
def hide_loading_dialog(self):
|
||||
"""隐藏加载对话框(委托给dialog_factory)"""
|
||||
return self.dialog_factory.hide_loading_dialog()
|
||||
|
||||
def _create_message_box(self, title, message, buttons=QMessageBox.StandardButton.Ok):
|
||||
"""创建消息框(委托给dialog_factory)"""
|
||||
return self.dialog_factory.create_message_box(title, message, buttons)
|
||||
|
||||
def show_menu(self, menu, button):
|
||||
"""显示菜单(委托给menu_builder)"""
|
||||
return self.menu_builder.show_menu(menu, button)
|
||||
|
||||
def sync_work_mode_menu_state(self):
|
||||
"""同步工作模式菜单状态,确保菜单选择状态与实际工作模式一致"""
|
||||
try:
|
||||
# 检查是否有离线模式管理器和菜单项
|
||||
if not hasattr(self.main_window, 'offline_mode_manager') or not self.main_window.offline_mode_manager:
|
||||
return
|
||||
|
||||
if not hasattr(self, 'online_mode_action') or not hasattr(self, 'offline_mode_action'):
|
||||
return
|
||||
|
||||
if not self.online_mode_action or not self.offline_mode_action:
|
||||
return
|
||||
|
||||
# 获取当前离线模式状态
|
||||
is_offline_mode = self.main_window.offline_mode_manager.is_in_offline_mode()
|
||||
|
||||
# 同步菜单选择状态
|
||||
self.online_mode_action.setChecked(not is_offline_mode)
|
||||
self.offline_mode_action.setChecked(is_offline_mode)
|
||||
|
||||
# 记录同步操作(仅在调试模式下)
|
||||
if hasattr(self.main_window, 'config') and self.main_window.config.get('debug_mode', False):
|
||||
from utils.logger import setup_logger
|
||||
logger = setup_logger("ui_manager")
|
||||
logger.debug(f"已同步工作模式菜单状态: 离线模式={is_offline_mode}")
|
||||
|
||||
except Exception as e:
|
||||
# 静默处理异常,避免影响程序正常运行
|
||||
if hasattr(self.main_window, 'config') and self.main_window.config.get('debug_mode', False):
|
||||
from utils.logger import setup_logger
|
||||
logger = setup_logger("ui_manager")
|
||||
logger.debug(f"同步工作模式菜单状态时出错: {e}")
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
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
|
||||
|
||||
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:
|
||||
# 恢复复选框状态
|
||||
self.ipv6_action.setChecked(False)
|
||||
return
|
||||
|
||||
# 显示正在校验IPv6的提示
|
||||
msg_box = self._create_message_box("IPv6检测", "\n正在校验是否支持IPv6,请稍候...\n")
|
||||
msg_box.open() # 使用open而不是exec,这样不会阻塞UI
|
||||
|
||||
# 处理消息队列,确保对话框显示
|
||||
from PySide6.QtCore import QCoreApplication
|
||||
QCoreApplication.processEvents()
|
||||
|
||||
# 检查IPv6是否可用
|
||||
ipv6_available = self.ipv6_manager.check_ipv6_availability()
|
||||
|
||||
# 关闭提示对话框
|
||||
msg_box.accept()
|
||||
|
||||
if not ipv6_available:
|
||||
# 显示IPv6不可用的提示
|
||||
error_msg_box = self._create_message_box(
|
||||
"IPv6不可用",
|
||||
"\n未检测到可用的IPv6连接,无法启用IPv6支持。\n\n请确保您的网络环境支持IPv6且已正确配置。\n"
|
||||
)
|
||||
error_msg_box.exec()
|
||||
# 恢复复选框状态
|
||||
self.ipv6_action.setChecked(False)
|
||||
return False
|
||||
|
||||
# 使用IPv6Manager处理切换
|
||||
success = self.ipv6_manager.toggle_ipv6_support(enabled)
|
||||
# 如果切换失败,恢复复选框状态
|
||||
if not success:
|
||||
self.ipv6_action.setChecked(not enabled)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def show_download_thread_settings(self):
|
||||
"""显示下载线程设置对话框"""
|
||||
if hasattr(self.main_window, 'download_manager'):
|
||||
self.main_window.download_manager.show_download_thread_settings()
|
||||
else:
|
||||
# 如果下载管理器不可用,显示错误信息
|
||||
self.dialog_factory.show_simple_message("错误", "\n下载管理器未初始化,无法修改下载线程设置。\n", "error")
|
||||
|
||||
|
||||
|
||||
def restore_hosts_backup(self):
|
||||
"""还原软件备份的hosts文件"""
|
||||
if hasattr(self.main_window, 'download_manager') and hasattr(self.main_window.download_manager, 'hosts_manager'):
|
||||
try:
|
||||
# 调用恢复hosts文件的方法
|
||||
result = self.main_window.download_manager.hosts_manager.restore()
|
||||
|
||||
if result:
|
||||
msg_box = self._create_message_box("成功", "\nhosts文件已成功还原为备份版本。\n")
|
||||
else:
|
||||
msg_box = self._create_message_box("警告", "\n还原hosts文件失败或没有找到备份文件。\n")
|
||||
|
||||
msg_box.exec()
|
||||
except Exception as e:
|
||||
msg_box = self._create_message_box("错误", f"\n还原hosts文件时发生错误:\n\n{str(e)}\n")
|
||||
msg_box.exec()
|
||||
else:
|
||||
msg_box = self._create_message_box("错误", "\n无法访问hosts管理器。\n")
|
||||
msg_box.exec()
|
||||
|
||||
def clean_hosts_entries(self):
|
||||
"""手动删除软件添加的hosts条目"""
|
||||
if hasattr(self.main_window, 'download_manager') and hasattr(self.main_window.download_manager, 'hosts_manager'):
|
||||
try:
|
||||
# 调用清理hosts条目的方法,强制清理即使禁用了自动还原
|
||||
result = self.main_window.download_manager.hosts_manager.check_and_clean_all_entries(force_clean=True)
|
||||
|
||||
if result:
|
||||
msg_box = self._create_message_box("成功", "\n已成功清理软件添加的hosts条目。\n")
|
||||
else:
|
||||
msg_box = self._create_message_box("提示", "\n未发现软件添加的hosts条目或清理操作失败。\n")
|
||||
|
||||
msg_box.exec()
|
||||
except Exception as e:
|
||||
msg_box = self._create_message_box("错误", f"\n清理hosts条目时发生错误:\n\n{str(e)}\n")
|
||||
msg_box.exec()
|
||||
else:
|
||||
msg_box = self._create_message_box("错误", "\n无法访问hosts管理器。\n")
|
||||
msg_box.exec()
|
||||
|
||||
def open_hosts_file(self):
|
||||
"""打开系统hosts文件"""
|
||||
try:
|
||||
# 获取hosts文件路径
|
||||
hosts_path = os.path.join(os.environ['SystemRoot'], 'System32', 'drivers', 'etc', 'hosts')
|
||||
|
||||
# 检查文件是否存在
|
||||
if os.path.exists(hosts_path):
|
||||
# 使用操作系统默认程序打开hosts文件
|
||||
if os.name == 'nt': # Windows
|
||||
# 尝试以管理员权限打开记事本编辑hosts文件
|
||||
try:
|
||||
# 使用PowerShell以管理员身份启动记事本
|
||||
subprocess.Popen(["powershell", "Start-Process", "notepad", hosts_path, "-Verb", "RunAs"])
|
||||
except Exception as e:
|
||||
# 如果失败,尝试直接打开
|
||||
os.startfile(hosts_path)
|
||||
else: # macOS 和 Linux
|
||||
import subprocess
|
||||
subprocess.call(['xdg-open', hosts_path])
|
||||
else:
|
||||
msg_box = self._create_message_box("错误", f"\nhosts文件不存在:\n{hosts_path}\n")
|
||||
msg_box.exec()
|
||||
except Exception as e:
|
||||
msg_box = self._create_message_box("错误", f"\n打开hosts文件时发生错误:\n\n{str(e)}\n")
|
||||
msg_box.exec()
|
||||
|
||||
def toggle_disable_auto_restore_hosts(self, checked):
|
||||
"""切换禁用自动还原hosts的状态
|
||||
|
||||
Args:
|
||||
checked: 是否禁用自动还原
|
||||
"""
|
||||
if hasattr(self.main_window, 'download_manager') and hasattr(self.main_window.download_manager, 'hosts_manager'):
|
||||
try:
|
||||
# 调用HostsManager的方法设置自动还原标志
|
||||
result = self.main_window.download_manager.hosts_manager.set_auto_restore_disabled(checked)
|
||||
|
||||
if result:
|
||||
# 同时更新内部配置,确保立即生效
|
||||
if hasattr(self.main_window, 'config'):
|
||||
self.main_window.config['disable_auto_restore_hosts'] = checked
|
||||
|
||||
# 显示成功提示
|
||||
status = "禁用" if checked else "启用"
|
||||
msg_box = self._create_message_box(
|
||||
"设置已更新",
|
||||
f"\n已{status}关闭/重启时自动还原hosts。\n\n{'hosts将被保留' if checked else 'hosts将在关闭时自动还原'}。\n"
|
||||
)
|
||||
msg_box.exec()
|
||||
else:
|
||||
# 如果设置失败,恢复复选框状态
|
||||
self.disable_auto_restore_action.setChecked(not checked)
|
||||
msg_box = self._create_message_box(
|
||||
"设置失败",
|
||||
"\n更新设置时发生错误,请稍后再试。\n"
|
||||
)
|
||||
msg_box.exec()
|
||||
except Exception as e:
|
||||
# 如果发生异常,恢复复选框状态
|
||||
self.disable_auto_restore_action.setChecked(not checked)
|
||||
msg_box = self._create_message_box(
|
||||
"错误",
|
||||
f"\n更新设置时发生异常:\n\n{str(e)}\n"
|
||||
)
|
||||
msg_box.exec()
|
||||
else:
|
||||
# 如果hosts管理器不可用,恢复复选框状态
|
||||
self.disable_auto_restore_action.setChecked(not checked)
|
||||
msg_box = self._create_message_box(
|
||||
"错误",
|
||||
"\nhosts管理器不可用,无法更新设置。\n"
|
||||
)
|
||||
msg_box.exec()
|
||||
|
||||
def _handle_pre_hash_toggle(self, checked):
|
||||
"""处理禁用安装前哈希预检查的切换
|
||||
|
||||
Args:
|
||||
checked: 是否禁用安装前哈希预检查
|
||||
"""
|
||||
if hasattr(self.main_window, 'config_manager'):
|
||||
success = self.main_window.config_manager.toggle_disable_pre_hash_check(self.main_window, checked)
|
||||
if not success:
|
||||
# 如果操作失败,恢复复选框状态
|
||||
self.disable_pre_hash_action.setChecked(not checked)
|
||||
else:
|
||||
# 如果配置管理器不可用,恢复复选框状态并显示错误
|
||||
self.disable_pre_hash_action.setChecked(not checked)
|
||||
self._create_message_box("错误", "\n配置管理器未初始化。\n").exec()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def switch_work_mode(self, mode):
|
||||
"""切换工作模式
|
||||
|
||||
Args:
|
||||
mode: 要切换的模式,"online"或"offline"
|
||||
"""
|
||||
# 检查主窗口是否有离线模式管理器
|
||||
if not hasattr(self.main_window, 'offline_mode_manager'):
|
||||
# 如果没有离线模式管理器,创建提示
|
||||
msg_box = self._create_message_box(
|
||||
"错误",
|
||||
"\n离线模式管理器未初始化,无法切换工作模式。\n"
|
||||
)
|
||||
msg_box.exec()
|
||||
|
||||
# 恢复选择状态
|
||||
self.online_mode_action.setChecked(True)
|
||||
self.offline_mode_action.setChecked(False)
|
||||
return
|
||||
|
||||
if mode == "offline":
|
||||
# 尝试切换到离线模式
|
||||
success = self.main_window.offline_mode_manager.set_offline_mode(True)
|
||||
if not success:
|
||||
# 如果切换失败,恢复选择状态
|
||||
self.online_mode_action.setChecked(True)
|
||||
self.offline_mode_action.setChecked(False)
|
||||
return
|
||||
|
||||
# 更新配置
|
||||
self.main_window.config["offline_mode"] = True
|
||||
self.main_window.save_config(self.main_window.config)
|
||||
|
||||
# 在离线模式下启用开始安装按钮
|
||||
if hasattr(self.main_window, 'window_manager'):
|
||||
self.main_window.window_manager.change_window_state(self.main_window.window_manager.STATE_READY)
|
||||
|
||||
# 清除版本警告标志
|
||||
if hasattr(self.main_window, 'version_warning'):
|
||||
self.main_window.version_warning = False
|
||||
|
||||
# 显示提示
|
||||
msg_box = self._create_message_box(
|
||||
"模式已切换",
|
||||
"\n已切换到离线模式。\n\n将使用本地补丁文件进行安装,不会从网络下载补丁。\n"
|
||||
)
|
||||
msg_box.exec()
|
||||
else:
|
||||
# 切换到在线模式
|
||||
self.main_window.offline_mode_manager.set_offline_mode(False)
|
||||
|
||||
# 更新配置
|
||||
self.main_window.config["offline_mode"] = False
|
||||
self.main_window.save_config(self.main_window.config)
|
||||
|
||||
# 重新获取云端配置
|
||||
if hasattr(self.main_window, 'fetch_cloud_config'):
|
||||
self.main_window.fetch_cloud_config()
|
||||
|
||||
# 如果当前版本过低,设置版本警告标志
|
||||
if hasattr(self.main_window, 'last_error_message') and self.main_window.last_error_message == "update_required":
|
||||
# 设置版本警告标志
|
||||
if hasattr(self.main_window, 'version_warning'):
|
||||
self.main_window.version_warning = True
|
||||
|
||||
# 显示提示
|
||||
msg_box = self._create_message_box(
|
||||
"模式已切换",
|
||||
"\n已切换到在线模式。\n\n将从网络下载补丁进行安装。\n"
|
||||
)
|
||||
msg_box.exec()
|
||||
|
||||
|
||||
|
||||
@@ -23,7 +23,58 @@ class WindowManager:
|
||||
|
||||
# 设置圆角窗口
|
||||
self.setRoundedCorners()
|
||||
|
||||
|
||||
# 初始化状态管理
|
||||
self._setup_window_state()
|
||||
|
||||
def _setup_window_state(self):
|
||||
"""初始化窗口状态管理."""
|
||||
self.STATE_INITIALIZING = "initializing"
|
||||
self.STATE_READY = "ready"
|
||||
self.STATE_DOWNLOADING = "downloading"
|
||||
self.STATE_EXTRACTING = "extracting"
|
||||
self.STATE_VERIFYING = "verifying"
|
||||
self.STATE_INSTALLING = "installing"
|
||||
self.STATE_COMPLETED = "completed"
|
||||
self.STATE_ERROR = "error"
|
||||
|
||||
self.current_state = self.STATE_INITIALIZING
|
||||
|
||||
def change_window_state(self, new_state, error_message=None):
|
||||
"""更改窗口状态并更新UI.
|
||||
|
||||
Args:
|
||||
new_state (str): 新的状态.
|
||||
error_message (str, optional): 错误信息. Defaults to None.
|
||||
"""
|
||||
if new_state == self.current_state:
|
||||
return
|
||||
|
||||
self.current_state = new_state
|
||||
self._update_ui_for_state(new_state, error_message)
|
||||
|
||||
def _update_ui_for_state(self, state, error_message=None):
|
||||
"""根据当前状态更新UI组件."""
|
||||
is_offline = self.window.offline_mode_manager.is_in_offline_mode()
|
||||
config_valid = self.window.config_valid
|
||||
|
||||
button_enabled = False
|
||||
button_text = "!无法安装!"
|
||||
|
||||
if state == self.STATE_READY:
|
||||
if is_offline or config_valid:
|
||||
button_enabled = True
|
||||
button_text = "开始安装"
|
||||
elif state in [self.STATE_DOWNLOADING, self.STATE_EXTRACTING, self.STATE_VERIFYING, self.STATE_INSTALLING]:
|
||||
button_text = "正在安装"
|
||||
elif state == self.STATE_COMPLETED:
|
||||
button_enabled = True
|
||||
button_text = "安装完成" # Or back to "开始安装"
|
||||
|
||||
self.window.ui.start_install_btn.setEnabled(button_enabled)
|
||||
self.window.ui.start_install_text.setText(button_text)
|
||||
self.window.install_button_enabled = button_enabled
|
||||
|
||||
def setRoundedCorners(self):
|
||||
"""设置窗口圆角"""
|
||||
# 实现圆角窗口
|
||||
@@ -115,22 +166,30 @@ class WindowManager:
|
||||
btn_width = 211 # 扩大后的容器宽度
|
||||
btn_height = 111 # 扩大后的容器高度
|
||||
x_pos = new_width - btn_width - right_margin
|
||||
y_pos = int((new_height - 65) * 0.28) - 10 # 调整为更靠上的位置
|
||||
y_pos = int((new_height - 65) * 0.18) - 10 # 从0.28改为0.18,向上移动
|
||||
self.ui.button_container.setGeometry(x_pos, y_pos, btn_width, btn_height)
|
||||
|
||||
# 添加禁/启用补丁按钮容器的位置调整
|
||||
if hasattr(self.ui, 'toggle_patch_container'):
|
||||
btn_width = 211 # 扩大后的容器宽度
|
||||
btn_height = 111 # 扩大后的容器高度
|
||||
x_pos = new_width - btn_width - right_margin
|
||||
y_pos = int((new_height - 65) * 0.36) - 10 # 从0.46改为0.36,向上移动
|
||||
self.ui.toggle_patch_container.setGeometry(x_pos, y_pos, btn_width, btn_height)
|
||||
|
||||
# 添加卸载补丁按钮容器的位置调整
|
||||
if hasattr(self.ui, 'uninstall_container'):
|
||||
btn_width = 211 # 扩大后的容器宽度
|
||||
btn_height = 111 # 扩大后的容器高度
|
||||
x_pos = new_width - btn_width - right_margin
|
||||
y_pos = int((new_height - 65) * 0.46) - 10 # 调整为中间位置
|
||||
y_pos = int((new_height - 65) * 0.54) - 10 # 从0.64改为0.54,向上移动
|
||||
self.ui.uninstall_container.setGeometry(x_pos, y_pos, btn_width, btn_height)
|
||||
|
||||
if hasattr(self.ui, 'exit_container'):
|
||||
btn_width = 211 # 扩大后的容器宽度
|
||||
btn_height = 111 # 扩大后的容器高度
|
||||
x_pos = new_width - btn_width - right_margin
|
||||
y_pos = int((new_height - 65) * 0.64) - 10 # 调整为更靠下的位置
|
||||
y_pos = int((new_height - 65) * 0.72) - 10 # 从0.82改为0.72,向上移动
|
||||
self.ui.exit_container.setGeometry(x_pos, y_pos, btn_width, btn_height)
|
||||
|
||||
# 更新圆角
|
||||
@@ -1,391 +0,0 @@
|
||||
import os
|
||||
import shutil
|
||||
from PySide6.QtWidgets import QMessageBox
|
||||
|
||||
class PatchManager:
|
||||
"""补丁管理器,用于处理补丁的安装和卸载"""
|
||||
|
||||
def __init__(self, app_name, game_info, debug_manager=None):
|
||||
"""初始化补丁管理器
|
||||
|
||||
Args:
|
||||
app_name: 应用程序名称,用于显示消息框标题
|
||||
game_info: 游戏信息字典,包含各版本的安装路径和可执行文件名
|
||||
debug_manager: 调试管理器实例,用于输出调试信息
|
||||
"""
|
||||
self.app_name = app_name
|
||||
self.game_info = game_info
|
||||
self.debug_manager = debug_manager
|
||||
self.installed_status = {} # 游戏版本的安装状态
|
||||
|
||||
def _is_debug_mode(self):
|
||||
"""检查是否处于调试模式
|
||||
|
||||
Returns:
|
||||
bool: 是否处于调试模式
|
||||
"""
|
||||
if hasattr(self.debug_manager, 'ui_manager') and hasattr(self.debug_manager.ui_manager, 'debug_action'):
|
||||
return self.debug_manager.ui_manager.debug_action.isChecked()
|
||||
return False
|
||||
|
||||
def initialize_status(self):
|
||||
"""初始化所有游戏版本的安装状态"""
|
||||
self.installed_status = {f"NEKOPARA Vol.{i}": False for i in range(1, 5)}
|
||||
self.installed_status["NEKOPARA After"] = False
|
||||
|
||||
def update_status(self, game_version, is_installed):
|
||||
"""更新游戏版本的安装状态
|
||||
|
||||
Args:
|
||||
game_version: 游戏版本
|
||||
is_installed: 是否已安装
|
||||
"""
|
||||
self.installed_status[game_version] = is_installed
|
||||
|
||||
def get_status(self, game_version=None):
|
||||
"""获取游戏版本的安装状态
|
||||
|
||||
Args:
|
||||
game_version: 游戏版本,如果为None则返回所有状态
|
||||
|
||||
Returns:
|
||||
bool或dict: 指定版本的安装状态或所有版本的安装状态
|
||||
"""
|
||||
if game_version:
|
||||
return self.installed_status.get(game_version, False)
|
||||
return self.installed_status
|
||||
|
||||
def uninstall_patch(self, game_dir, game_version, silent=False):
|
||||
"""卸载补丁
|
||||
|
||||
Args:
|
||||
game_dir: 游戏目录路径
|
||||
game_version: 游戏版本
|
||||
silent: 是否静默模式(不显示弹窗)
|
||||
|
||||
Returns:
|
||||
bool: 卸载成功返回True,失败返回False
|
||||
dict: 在silent=True时,返回包含卸载结果信息的字典
|
||||
"""
|
||||
debug_mode = self._is_debug_mode()
|
||||
|
||||
if game_version not in self.game_info:
|
||||
if not silent:
|
||||
QMessageBox.critical(
|
||||
None,
|
||||
f"错误 - {self.app_name}",
|
||||
f"\n无法识别游戏版本: {game_version}\n",
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
return False if not silent else {"success": False, "message": f"无法识别游戏版本: {game_version}", "files_removed": 0}
|
||||
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 开始卸载 {game_version} 补丁,目录: {game_dir}")
|
||||
|
||||
try:
|
||||
files_removed = 0
|
||||
|
||||
# 获取可能的补丁文件路径
|
||||
install_path_base = os.path.basename(self.game_info[game_version]["install_path"])
|
||||
patch_file_path = os.path.join(game_dir, install_path_base)
|
||||
|
||||
# 尝试查找补丁文件,支持不同大小写
|
||||
patch_files_to_check = [
|
||||
patch_file_path,
|
||||
patch_file_path.lower(),
|
||||
patch_file_path.upper(),
|
||||
patch_file_path.replace("_", ""),
|
||||
patch_file_path.replace("_", "-"),
|
||||
]
|
||||
|
||||
# 查找并删除补丁文件
|
||||
patch_file_found = False
|
||||
for patch_path in patch_files_to_check:
|
||||
if os.path.exists(patch_path):
|
||||
patch_file_found = True
|
||||
os.remove(patch_path)
|
||||
files_removed += 1
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 已删除补丁文件: {patch_path}")
|
||||
|
||||
if not patch_file_found and debug_mode:
|
||||
print(f"DEBUG: 未找到补丁文件,检查了以下路径: {patch_files_to_check}")
|
||||
|
||||
# 检查是否有额外的签名文件 (.sig)
|
||||
if game_version == "NEKOPARA After":
|
||||
for patch_path in patch_files_to_check:
|
||||
sig_file_path = f"{patch_path}.sig"
|
||||
if os.path.exists(sig_file_path):
|
||||
os.remove(sig_file_path)
|
||||
files_removed += 1
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 已删除签名文件: {sig_file_path}")
|
||||
|
||||
# 删除patch文件夹
|
||||
patch_folders_to_check = [
|
||||
os.path.join(game_dir, "patch"),
|
||||
os.path.join(game_dir, "Patch"),
|
||||
os.path.join(game_dir, "PATCH"),
|
||||
]
|
||||
|
||||
for patch_folder in patch_folders_to_check:
|
||||
if os.path.exists(patch_folder):
|
||||
shutil.rmtree(patch_folder)
|
||||
files_removed += 1
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 已删除补丁文件夹: {patch_folder}")
|
||||
|
||||
# 删除game/patch文件夹
|
||||
game_folders = ["game", "Game", "GAME"]
|
||||
patch_folders = ["patch", "Patch", "PATCH"]
|
||||
|
||||
for game_folder in game_folders:
|
||||
for patch_folder in patch_folders:
|
||||
game_patch_folder = os.path.join(game_dir, game_folder, patch_folder)
|
||||
if os.path.exists(game_patch_folder):
|
||||
shutil.rmtree(game_patch_folder)
|
||||
files_removed += 1
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 已删除game/patch文件夹: {game_patch_folder}")
|
||||
|
||||
# 删除配置文件
|
||||
config_files = ["config.json", "Config.json", "CONFIG.JSON"]
|
||||
script_files = ["scripts.json", "Scripts.json", "SCRIPTS.JSON"]
|
||||
|
||||
for game_folder in game_folders:
|
||||
game_path = os.path.join(game_dir, game_folder)
|
||||
if os.path.exists(game_path):
|
||||
# 删除配置文件
|
||||
for config_file in config_files:
|
||||
config_path = os.path.join(game_path, config_file)
|
||||
if os.path.exists(config_path):
|
||||
os.remove(config_path)
|
||||
files_removed += 1
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 已删除配置文件: {config_path}")
|
||||
|
||||
# 删除脚本文件
|
||||
for script_file in script_files:
|
||||
script_path = os.path.join(game_path, script_file)
|
||||
if os.path.exists(script_path):
|
||||
os.remove(script_path)
|
||||
files_removed += 1
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 已删除脚本文件: {script_path}")
|
||||
|
||||
# 更新安装状态
|
||||
self.installed_status[game_version] = False
|
||||
|
||||
# 在非静默模式且非批量卸载模式下显示卸载成功消息
|
||||
if not silent and game_version != "all":
|
||||
# 显示卸载成功消息
|
||||
if files_removed > 0:
|
||||
QMessageBox.information(
|
||||
None,
|
||||
f"卸载完成 - {self.app_name}",
|
||||
f"\n{game_version} 补丁卸载成功!\n共删除 {files_removed} 个文件/文件夹。\n",
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
else:
|
||||
QMessageBox.warning(
|
||||
None,
|
||||
f"警告 - {self.app_name}",
|
||||
f"\n未找到 {game_version} 的补丁文件,可能未安装补丁或已被移除。\n",
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
|
||||
# 卸载成功
|
||||
if silent:
|
||||
return {"success": True, "message": f"{game_version} 补丁卸载成功", "files_removed": files_removed}
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
# 在非静默模式且非批量卸载模式下显示卸载失败消息
|
||||
if not silent and game_version != "all":
|
||||
# 显示卸载失败消息
|
||||
error_message = f"\n卸载 {game_version} 补丁时出错:\n\n{str(e)}\n"
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 卸载错误 - {str(e)}")
|
||||
|
||||
QMessageBox.critical(
|
||||
None,
|
||||
f"卸载失败 - {self.app_name}",
|
||||
error_message,
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
|
||||
# 卸载失败
|
||||
if silent:
|
||||
return {"success": False, "message": f"卸载 {game_version} 补丁时出错: {str(e)}", "files_removed": 0}
|
||||
return False
|
||||
|
||||
def batch_uninstall_patches(self, game_dirs):
|
||||
"""批量卸载多个游戏的补丁
|
||||
|
||||
Args:
|
||||
game_dirs: 游戏版本到游戏目录的映射字典
|
||||
|
||||
Returns:
|
||||
tuple: (成功数量, 失败数量, 详细结果列表)
|
||||
"""
|
||||
success_count = 0
|
||||
fail_count = 0
|
||||
debug_mode = self._is_debug_mode()
|
||||
results = []
|
||||
|
||||
for version, path in game_dirs.items():
|
||||
try:
|
||||
# 在批量模式下使用静默卸载
|
||||
result = self.uninstall_patch(path, version, silent=True)
|
||||
|
||||
if isinstance(result, dict): # 使用了静默模式
|
||||
if result["success"]:
|
||||
success_count += 1
|
||||
else:
|
||||
fail_count += 1
|
||||
results.append({
|
||||
"version": version,
|
||||
"success": result["success"],
|
||||
"message": result["message"],
|
||||
"files_removed": result["files_removed"]
|
||||
})
|
||||
else: # 兼容旧代码,不应该执行到这里
|
||||
if result:
|
||||
success_count += 1
|
||||
else:
|
||||
fail_count += 1
|
||||
results.append({
|
||||
"version": version,
|
||||
"success": result,
|
||||
"message": f"{version} 卸载{'成功' if result else '失败'}",
|
||||
"files_removed": 0
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 卸载 {version} 时出错: {str(e)}")
|
||||
fail_count += 1
|
||||
results.append({
|
||||
"version": version,
|
||||
"success": False,
|
||||
"message": f"卸载出错: {str(e)}",
|
||||
"files_removed": 0
|
||||
})
|
||||
|
||||
return success_count, fail_count, results
|
||||
|
||||
def show_uninstall_result(self, success_count, fail_count, results=None):
|
||||
"""显示批量卸载结果
|
||||
|
||||
Args:
|
||||
success_count: 成功卸载的数量
|
||||
fail_count: 卸载失败的数量
|
||||
results: 详细结果列表,如果提供,会显示更详细的信息
|
||||
"""
|
||||
result_text = f"\n批量卸载完成!\n成功: {success_count} 个\n失败: {fail_count} 个\n"
|
||||
|
||||
# 如果有详细结果,添加到消息中
|
||||
if results:
|
||||
success_list = [r["version"] for r in results if r["success"]]
|
||||
fail_list = [r["version"] for r in results if not r["success"]]
|
||||
|
||||
if success_list:
|
||||
result_text += f"\n【成功卸载】:\n{chr(10).join(success_list)}\n"
|
||||
|
||||
if fail_list:
|
||||
result_text += f"\n【卸载失败】:\n{chr(10).join(fail_list)}\n"
|
||||
|
||||
QMessageBox.information(
|
||||
None,
|
||||
f"批量卸载完成 - {self.app_name}",
|
||||
result_text,
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
|
||||
def check_patch_installed(self, game_dir, game_version):
|
||||
"""检查游戏是否已安装补丁
|
||||
|
||||
Args:
|
||||
game_dir: 游戏目录路径
|
||||
game_version: 游戏版本
|
||||
|
||||
Returns:
|
||||
bool: 如果已安装补丁返回True,否则返回False
|
||||
"""
|
||||
debug_mode = self._is_debug_mode()
|
||||
|
||||
if game_version not in self.game_info:
|
||||
return False
|
||||
|
||||
# 获取可能的补丁文件路径
|
||||
install_path_base = os.path.basename(self.game_info[game_version]["install_path"])
|
||||
patch_file_path = os.path.join(game_dir, install_path_base)
|
||||
|
||||
# 尝试查找补丁文件,支持不同大小写
|
||||
patch_files_to_check = [
|
||||
patch_file_path,
|
||||
patch_file_path.lower(),
|
||||
patch_file_path.upper(),
|
||||
patch_file_path.replace("_", ""),
|
||||
patch_file_path.replace("_", "-"),
|
||||
]
|
||||
|
||||
# 查找补丁文件
|
||||
for patch_path in patch_files_to_check:
|
||||
if os.path.exists(patch_path):
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 找到补丁文件: {patch_path}")
|
||||
return True
|
||||
|
||||
# 检查是否有补丁文件夹
|
||||
patch_folders_to_check = [
|
||||
os.path.join(game_dir, "patch"),
|
||||
os.path.join(game_dir, "Patch"),
|
||||
os.path.join(game_dir, "PATCH"),
|
||||
]
|
||||
|
||||
for patch_folder in patch_folders_to_check:
|
||||
if os.path.exists(patch_folder):
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 找到补丁文件夹: {patch_folder}")
|
||||
return True
|
||||
|
||||
# 检查game/patch文件夹
|
||||
game_folders = ["game", "Game", "GAME"]
|
||||
patch_folders = ["patch", "Patch", "PATCH"]
|
||||
|
||||
for game_folder in game_folders:
|
||||
for patch_folder in patch_folders:
|
||||
game_patch_folder = os.path.join(game_dir, game_folder, patch_folder)
|
||||
if os.path.exists(game_patch_folder):
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 找到game/patch文件夹: {game_patch_folder}")
|
||||
return True
|
||||
|
||||
# 检查配置文件
|
||||
config_files = ["config.json", "Config.json", "CONFIG.JSON"]
|
||||
script_files = ["scripts.json", "Scripts.json", "SCRIPTS.JSON"]
|
||||
|
||||
for game_folder in game_folders:
|
||||
game_path = os.path.join(game_dir, game_folder)
|
||||
if os.path.exists(game_path):
|
||||
# 检查配置文件
|
||||
for config_file in config_files:
|
||||
config_path = os.path.join(game_path, config_file)
|
||||
if os.path.exists(config_path):
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 找到配置文件: {config_path}")
|
||||
return True
|
||||
|
||||
# 检查脚本文件
|
||||
for script_file in script_files:
|
||||
script_path = os.path.join(game_path, script_file)
|
||||
if os.path.exists(script_path):
|
||||
if debug_mode:
|
||||
print(f"DEBUG: 找到脚本文件: {script_path}")
|
||||
return True
|
||||
|
||||
# 没有找到补丁文件或文件夹
|
||||
if debug_mode:
|
||||
print(f"DEBUG: {game_version} 在 {game_dir} 中没有安装补丁")
|
||||
return False
|
||||
@@ -1,673 +0,0 @@
|
||||
from PySide6.QtGui import QIcon, QAction, QFont, QCursor
|
||||
from PySide6.QtWidgets import QMessageBox, QMainWindow, QMenu, QPushButton
|
||||
from PySide6.QtCore import Qt, QRect
|
||||
import webbrowser
|
||||
import os
|
||||
|
||||
from utils import load_base64_image, msgbox_frame, resource_path
|
||||
from data.config import APP_NAME, APP_VERSION, LOG_FILE
|
||||
from core.ipv6_manager import IPv6Manager # 导入新的IPv6Manager类
|
||||
|
||||
class UIManager:
|
||||
def __init__(self, main_window):
|
||||
"""初始化UI管理器
|
||||
|
||||
Args:
|
||||
main_window: 主窗口实例,用于设置UI元素
|
||||
"""
|
||||
self.main_window = main_window
|
||||
# 使用getattr获取ui属性,如果不存在则为None
|
||||
self.ui = getattr(main_window, 'ui', None)
|
||||
self.debug_action = None
|
||||
self.turbo_download_action = None
|
||||
self.dev_menu = None
|
||||
self.privacy_menu = None # 隐私协议菜单
|
||||
self.about_menu = None # 关于菜单
|
||||
self.about_btn = None # 关于按钮
|
||||
|
||||
# 获取主窗口的IPv6Manager实例
|
||||
self.ipv6_manager = getattr(main_window, 'ipv6_manager', None)
|
||||
|
||||
def setup_ui(self):
|
||||
"""设置UI元素,包括窗口图标、标题和菜单"""
|
||||
# 设置窗口图标
|
||||
import os
|
||||
from utils import resource_path
|
||||
icon_path = resource_path(os.path.join("IMG", "ICO", "icon.png"))
|
||||
if os.path.exists(icon_path):
|
||||
self.main_window.setWindowIcon(QIcon(icon_path))
|
||||
|
||||
# 设置窗口标题
|
||||
self.main_window.setWindowTitle(f"{APP_NAME} v{APP_VERSION}")
|
||||
|
||||
# 创建关于按钮
|
||||
self._create_about_button()
|
||||
|
||||
# 设置菜单
|
||||
self._setup_help_menu()
|
||||
self._setup_about_menu() # 新增关于菜单
|
||||
self._setup_settings_menu()
|
||||
|
||||
def _create_about_button(self):
|
||||
"""创建"关于"按钮"""
|
||||
if not self.ui or not hasattr(self.ui, 'menu_area'):
|
||||
return
|
||||
|
||||
# 获取菜单字体和样式
|
||||
menu_font = self._get_menu_font()
|
||||
|
||||
# 创建关于按钮
|
||||
self.about_btn = QPushButton("关于", self.ui.menu_area)
|
||||
self.about_btn.setObjectName(u"about_btn")
|
||||
|
||||
# 获取帮助按钮的位置和样式
|
||||
help_btn_x = 0
|
||||
help_btn_width = 0
|
||||
if hasattr(self.ui, 'help_btn'):
|
||||
help_btn_x = self.ui.help_btn.x()
|
||||
help_btn_width = self.ui.help_btn.width()
|
||||
|
||||
# 设置位置在"帮助"按钮右侧
|
||||
self.about_btn.setGeometry(QRect(help_btn_x + help_btn_width + 20, 1, 80, 28))
|
||||
self.about_btn.setFont(menu_font)
|
||||
self.about_btn.setCursor(QCursor(Qt.CursorShape.PointingHandCursor))
|
||||
|
||||
# 复制帮助按钮的样式
|
||||
if hasattr(self.ui, 'help_btn'):
|
||||
self.about_btn.setStyleSheet(self.ui.help_btn.styleSheet())
|
||||
else:
|
||||
# 默认样式
|
||||
self.about_btn.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: transparent;
|
||||
color: white;
|
||||
border: none;
|
||||
text-align: left;
|
||||
padding-left: 10px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #F47A5B;
|
||||
border-radius: 4px;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #D25A3C;
|
||||
border-radius: 4px;
|
||||
}
|
||||
""")
|
||||
|
||||
def _setup_help_menu(self):
|
||||
"""设置"帮助"菜单"""
|
||||
if not self.ui or not hasattr(self.ui, 'menu_2'):
|
||||
return
|
||||
|
||||
# 获取菜单字体
|
||||
menu_font = self._get_menu_font()
|
||||
|
||||
# 创建菜单项 - 移除"项目主页",添加"常见问题"和"提交错误"
|
||||
faq_action = QAction("常见问题", self.main_window)
|
||||
faq_action.triggered.connect(self.open_faq_page)
|
||||
faq_action.setFont(menu_font)
|
||||
|
||||
report_issue_action = QAction("提交错误", self.main_window)
|
||||
report_issue_action.triggered.connect(self.open_issues_page)
|
||||
report_issue_action.setFont(menu_font)
|
||||
|
||||
# 清除现有菜单项并添加新的菜单项
|
||||
self.ui.menu_2.clear()
|
||||
self.ui.menu_2.addAction(faq_action)
|
||||
self.ui.menu_2.addAction(report_issue_action)
|
||||
|
||||
# 连接按钮点击事件,如果使用按钮式菜单
|
||||
if hasattr(self.ui, 'help_btn'):
|
||||
# 按钮已经连接到显示菜单,不需要额外处理
|
||||
pass
|
||||
|
||||
def _setup_about_menu(self):
|
||||
"""设置"关于"菜单"""
|
||||
# 获取菜单字体
|
||||
menu_font = self._get_menu_font()
|
||||
|
||||
# 创建关于菜单
|
||||
self.about_menu = QMenu("关于", self.main_window)
|
||||
self.about_menu.setFont(menu_font)
|
||||
|
||||
# 设置菜单样式
|
||||
font_family = menu_font.family()
|
||||
menu_style = self._get_menu_style(font_family)
|
||||
self.about_menu.setStyleSheet(menu_style)
|
||||
|
||||
# 创建菜单项
|
||||
about_project_action = QAction("关于本项目", self.main_window)
|
||||
about_project_action.setFont(menu_font)
|
||||
about_project_action.triggered.connect(self.show_about_dialog)
|
||||
|
||||
# 添加项目主页选项(从帮助菜单移动过来)
|
||||
project_home_action = QAction("Github项目主页", self.main_window)
|
||||
project_home_action.setFont(menu_font)
|
||||
project_home_action.triggered.connect(self.open_project_home_page)
|
||||
|
||||
# 添加加入QQ群选项
|
||||
qq_group_action = QAction("加入QQ群", self.main_window)
|
||||
qq_group_action.setFont(menu_font)
|
||||
qq_group_action.triggered.connect(self.open_qq_group)
|
||||
|
||||
# 创建隐私协议菜单
|
||||
self._setup_privacy_menu()
|
||||
|
||||
# 添加到关于菜单
|
||||
self.about_menu.addAction(about_project_action)
|
||||
self.about_menu.addAction(project_home_action)
|
||||
self.about_menu.addAction(qq_group_action)
|
||||
self.about_menu.addSeparator()
|
||||
self.about_menu.addMenu(self.privacy_menu)
|
||||
|
||||
# 连接按钮点击事件
|
||||
if self.about_btn:
|
||||
self.about_btn.clicked.connect(lambda: self.show_menu(self.about_menu, self.about_btn))
|
||||
|
||||
def _setup_privacy_menu(self):
|
||||
"""设置"隐私协议"菜单"""
|
||||
# 获取菜单字体
|
||||
menu_font = self._get_menu_font()
|
||||
|
||||
# 创建隐私协议子菜单
|
||||
self.privacy_menu = QMenu("隐私协议", self.main_window)
|
||||
self.privacy_menu.setFont(menu_font)
|
||||
|
||||
# 设置与其他菜单一致的样式
|
||||
font_family = menu_font.family()
|
||||
menu_style = self._get_menu_style(font_family)
|
||||
self.privacy_menu.setStyleSheet(menu_style)
|
||||
|
||||
# 添加子选项
|
||||
view_privacy_action = QAction("查看完整隐私协议", self.main_window)
|
||||
view_privacy_action.setFont(menu_font)
|
||||
view_privacy_action.triggered.connect(self.open_privacy_policy)
|
||||
|
||||
revoke_privacy_action = QAction("撤回隐私协议", self.main_window)
|
||||
revoke_privacy_action.setFont(menu_font)
|
||||
revoke_privacy_action.triggered.connect(self.revoke_privacy_agreement)
|
||||
|
||||
# 添加到子菜单
|
||||
self.privacy_menu.addAction(view_privacy_action)
|
||||
self.privacy_menu.addAction(revoke_privacy_action)
|
||||
|
||||
def _get_menu_style(self, font_family):
|
||||
"""获取统一的菜单样式"""
|
||||
return f"""
|
||||
QMenu {{
|
||||
background-color: #E96948;
|
||||
color: white;
|
||||
font-family: "{font_family}";
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
border: 1px solid #F47A5B;
|
||||
padding: 8px;
|
||||
border-radius: 6px;
|
||||
margin-top: 2px;
|
||||
}}
|
||||
QMenu::item {{
|
||||
padding: 6px 20px 6px 15px;
|
||||
background-color: transparent;
|
||||
min-width: 120px;
|
||||
color: white;
|
||||
font-family: "{font_family}";
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}}
|
||||
QMenu::item:selected {{
|
||||
background-color: #F47A5B;
|
||||
border-radius: 4px;
|
||||
}}
|
||||
QMenu::separator {{
|
||||
height: 1px;
|
||||
background-color: #F47A5B;
|
||||
margin: 5px 15px;
|
||||
}}
|
||||
QMenu::item:checked {{
|
||||
background-color: #D25A3C;
|
||||
border-radius: 4px;
|
||||
}}
|
||||
"""
|
||||
|
||||
def _get_menu_font(self):
|
||||
"""获取菜单字体"""
|
||||
font_family = "Arial" # 默认字体族
|
||||
|
||||
try:
|
||||
from PySide6.QtGui import QFontDatabase
|
||||
|
||||
# 尝试加载字体
|
||||
font_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "fonts", "SmileySans-Oblique.ttf")
|
||||
if os.path.exists(font_path):
|
||||
font_id = QFontDatabase.addApplicationFont(font_path)
|
||||
if font_id != -1:
|
||||
font_family = QFontDatabase.applicationFontFamilies(font_id)[0]
|
||||
|
||||
# 创建菜单字体
|
||||
menu_font = QFont(font_family, 14)
|
||||
menu_font.setBold(True)
|
||||
return menu_font
|
||||
|
||||
except Exception as e:
|
||||
print(f"加载字体失败: {e}")
|
||||
# 返回默认字体
|
||||
menu_font = QFont(font_family, 14)
|
||||
menu_font.setBold(True)
|
||||
return menu_font
|
||||
|
||||
def _setup_settings_menu(self):
|
||||
"""设置"设置"菜单"""
|
||||
if not self.ui or not hasattr(self.ui, 'menu'):
|
||||
return
|
||||
|
||||
# 获取菜单字体
|
||||
menu_font = self._get_menu_font()
|
||||
font_family = menu_font.family()
|
||||
|
||||
# 创建开发者选项子菜单
|
||||
self.dev_menu = QMenu("开发者选项", self.main_window)
|
||||
self.dev_menu.setFont(menu_font) # 设置与UI_install.py中相同的字体
|
||||
|
||||
# 使用和主菜单相同的样式
|
||||
menu_style = self._get_menu_style(font_family)
|
||||
self.dev_menu.setStyleSheet(menu_style)
|
||||
|
||||
# 创建Debug子菜单
|
||||
self.debug_submenu = QMenu("Debug模式", self.main_window)
|
||||
self.debug_submenu.setFont(menu_font)
|
||||
self.debug_submenu.setStyleSheet(menu_style)
|
||||
|
||||
# 创建hosts文件选项子菜单
|
||||
self.hosts_submenu = QMenu("hosts文件选项", self.main_window)
|
||||
self.hosts_submenu.setFont(menu_font)
|
||||
self.hosts_submenu.setStyleSheet(menu_style)
|
||||
|
||||
# 添加IPv6支持选项
|
||||
self.ipv6_action = QAction("启用IPv6支持", self.main_window, checkable=True)
|
||||
self.ipv6_action.setFont(menu_font)
|
||||
|
||||
# 添加IPv6检测按钮,用于显示详细信息
|
||||
self.ipv6_test_action = QAction("测试IPv6连接", self.main_window)
|
||||
self.ipv6_test_action.setFont(menu_font)
|
||||
if self.ipv6_manager:
|
||||
self.ipv6_test_action.triggered.connect(self.ipv6_manager.show_ipv6_details)
|
||||
else:
|
||||
self.ipv6_test_action.triggered.connect(self.show_ipv6_manager_not_ready)
|
||||
|
||||
# 创建IPv6支持子菜单
|
||||
self.ipv6_submenu = QMenu("IPv6支持", self.main_window)
|
||||
self.ipv6_submenu.setFont(menu_font)
|
||||
self.ipv6_submenu.setStyleSheet(menu_style)
|
||||
|
||||
# 检查IPv6是否可用
|
||||
ipv6_available = False
|
||||
if self.ipv6_manager:
|
||||
ipv6_available = self.ipv6_manager.check_ipv6_availability()
|
||||
|
||||
if not ipv6_available:
|
||||
self.ipv6_action.setText("启用IPv6支持 (不可用)")
|
||||
self.ipv6_action.setEnabled(False)
|
||||
self.ipv6_action.setToolTip("未检测到可用的IPv6连接")
|
||||
|
||||
# 检查配置中是否已启用IPv6
|
||||
config = getattr(self.main_window, 'config', {})
|
||||
ipv6_enabled = False
|
||||
if isinstance(config, dict):
|
||||
ipv6_enabled = config.get("ipv6_enabled", False)
|
||||
# 如果配置中启用了IPv6但实际不可用,则强制禁用
|
||||
if ipv6_enabled and not ipv6_available:
|
||||
config["ipv6_enabled"] = False
|
||||
ipv6_enabled = False
|
||||
# 使用utils.save_config直接保存配置
|
||||
from utils import save_config
|
||||
save_config(config)
|
||||
|
||||
self.ipv6_action.setChecked(ipv6_enabled)
|
||||
|
||||
# 连接IPv6支持切换事件
|
||||
self.ipv6_action.triggered.connect(self._handle_ipv6_toggle)
|
||||
|
||||
# 将选项添加到IPv6子菜单
|
||||
self.ipv6_submenu.addAction(self.ipv6_action)
|
||||
self.ipv6_submenu.addAction(self.ipv6_test_action)
|
||||
|
||||
# 添加hosts子选项
|
||||
self.restore_hosts_action = QAction("还原软件备份的hosts文件", self.main_window)
|
||||
self.restore_hosts_action.setFont(menu_font)
|
||||
self.restore_hosts_action.triggered.connect(self.restore_hosts_backup)
|
||||
|
||||
self.clean_hosts_action = QAction("手动删除软件添加的hosts条目", self.main_window)
|
||||
self.clean_hosts_action.setFont(menu_font)
|
||||
self.clean_hosts_action.triggered.connect(self.clean_hosts_entries)
|
||||
|
||||
# 添加打开hosts文件选项
|
||||
self.open_hosts_action = QAction("打开hosts文件", self.main_window)
|
||||
self.open_hosts_action.setFont(menu_font)
|
||||
self.open_hosts_action.triggered.connect(self.open_hosts_file)
|
||||
|
||||
# 添加到hosts子菜单
|
||||
self.hosts_submenu.addAction(self.restore_hosts_action)
|
||||
self.hosts_submenu.addAction(self.clean_hosts_action)
|
||||
self.hosts_submenu.addAction(self.open_hosts_action)
|
||||
|
||||
# 创建Debug开关选项
|
||||
self.debug_action = QAction("Debug开关", self.main_window, checkable=True)
|
||||
self.debug_action.setFont(menu_font)
|
||||
|
||||
# 安全地获取config属性
|
||||
config = getattr(self.main_window, 'config', {})
|
||||
debug_mode = False
|
||||
if isinstance(config, dict):
|
||||
debug_mode = config.get("debug_mode", False)
|
||||
|
||||
self.debug_action.setChecked(debug_mode)
|
||||
|
||||
# 安全地连接toggle_debug_mode方法
|
||||
if hasattr(self.main_window, 'toggle_debug_mode'):
|
||||
self.debug_action.triggered.connect(self.main_window.toggle_debug_mode)
|
||||
|
||||
# 创建打开log文件选项
|
||||
self.open_log_action = QAction("打开log.txt", self.main_window)
|
||||
self.open_log_action.setFont(menu_font)
|
||||
# 初始状态根据debug模式设置启用状态
|
||||
self.open_log_action.setEnabled(debug_mode)
|
||||
|
||||
# 连接打开log文件的事件
|
||||
self.open_log_action.triggered.connect(self.open_log_file)
|
||||
|
||||
# 添加到Debug子菜单
|
||||
self.debug_submenu.addAction(self.debug_action)
|
||||
self.debug_submenu.addAction(self.open_log_action)
|
||||
|
||||
# 创建下载设置子菜单
|
||||
self.download_settings_menu = QMenu("下载设置", self.main_window)
|
||||
self.download_settings_menu.setFont(menu_font)
|
||||
self.download_settings_menu.setStyleSheet(menu_style)
|
||||
|
||||
# "修改下载源"按钮移至下载设置菜单
|
||||
self.switch_source_action = QAction("修改下载源", self.main_window)
|
||||
self.switch_source_action.setFont(menu_font)
|
||||
self.switch_source_action.setEnabled(True)
|
||||
self.switch_source_action.triggered.connect(self.show_under_development)
|
||||
|
||||
# 添加下载线程设置选项
|
||||
self.thread_settings_action = QAction("下载线程设置", self.main_window)
|
||||
self.thread_settings_action.setFont(menu_font)
|
||||
# 连接到下载线程设置对话框
|
||||
self.thread_settings_action.triggered.connect(self.show_download_thread_settings)
|
||||
|
||||
# 添加到下载设置子菜单
|
||||
self.download_settings_menu.addAction(self.switch_source_action)
|
||||
self.download_settings_menu.addAction(self.thread_settings_action)
|
||||
|
||||
# 添加到主菜单
|
||||
self.ui.menu.addMenu(self.download_settings_menu) # 添加下载设置子菜单
|
||||
self.ui.menu.addSeparator()
|
||||
self.ui.menu.addMenu(self.dev_menu) # 添加开发者选项子菜单
|
||||
|
||||
# 添加Debug子菜单到开发者选项菜单
|
||||
self.dev_menu.addMenu(self.debug_submenu)
|
||||
self.dev_menu.addMenu(self.hosts_submenu) # 添加hosts文件选项子菜单
|
||||
self.dev_menu.addMenu(self.ipv6_submenu) # 添加IPv6支持子菜单
|
||||
|
||||
def _handle_ipv6_toggle(self, enabled):
|
||||
"""处理IPv6支持切换事件
|
||||
|
||||
Args:
|
||||
enabled: 是否启用IPv6支持
|
||||
"""
|
||||
if not self.ipv6_manager:
|
||||
# 显示错误提示
|
||||
msg_box = self._create_message_box("错误", "\nIPv6管理器尚未初始化,请稍后再试。\n")
|
||||
msg_box.exec()
|
||||
# 恢复复选框状态
|
||||
self.ipv6_action.setChecked(not enabled)
|
||||
return
|
||||
|
||||
# 使用IPv6Manager处理切换
|
||||
success = self.ipv6_manager.toggle_ipv6_support(enabled)
|
||||
# 如果切换失败,恢复复选框状态
|
||||
if not success:
|
||||
self.ipv6_action.setChecked(not enabled)
|
||||
|
||||
def show_menu(self, menu, button):
|
||||
"""显示菜单
|
||||
|
||||
Args:
|
||||
menu: 要显示的菜单
|
||||
button: 触发菜单的按钮
|
||||
"""
|
||||
# 检查Ui_install中是否定义了show_menu方法
|
||||
if hasattr(self.ui, 'show_menu'):
|
||||
# 如果存在,使用UI中定义的方法
|
||||
self.ui.show_menu(menu, button)
|
||||
else:
|
||||
# 否则,使用默认的弹出方法
|
||||
global_pos = button.mapToGlobal(button.rect().bottomLeft())
|
||||
menu.popup(global_pos)
|
||||
|
||||
def open_project_home_page(self):
|
||||
"""打开项目主页"""
|
||||
webbrowser.open("https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT")
|
||||
|
||||
def open_github_page(self):
|
||||
"""打开项目GitHub页面"""
|
||||
webbrowser.open("https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT")
|
||||
|
||||
def open_faq_page(self):
|
||||
"""打开常见问题页面"""
|
||||
import locale
|
||||
# 根据系统语言选择FAQ页面
|
||||
system_lang = locale.getdefaultlocale()[0]
|
||||
if system_lang and system_lang.startswith('zh'):
|
||||
webbrowser.open("https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/blob/master/FAQ.md")
|
||||
else:
|
||||
webbrowser.open("https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/blob/master/FAQ-en.md")
|
||||
|
||||
def open_issues_page(self):
|
||||
"""打开GitHub问题页面"""
|
||||
webbrowser.open("https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/issues")
|
||||
|
||||
def open_qq_group(self):
|
||||
"""打开QQ群链接"""
|
||||
webbrowser.open("https://qm.qq.com/q/g9i04i5eec")
|
||||
|
||||
def open_privacy_policy(self):
|
||||
"""打开完整隐私协议(在GitHub上)"""
|
||||
webbrowser.open("https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/blob/master/PRIVACY.md")
|
||||
|
||||
def revoke_privacy_agreement(self):
|
||||
"""撤回隐私协议同意,并重启软件"""
|
||||
# 创建确认对话框
|
||||
msg_box = self._create_message_box(
|
||||
"确认操作",
|
||||
"\n您确定要撤回隐私协议同意吗?\n\n撤回后软件将立即重启,您需要重新阅读并同意隐私协议。\n",
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||
)
|
||||
|
||||
reply = msg_box.exec()
|
||||
if reply == QMessageBox.StandardButton.Yes:
|
||||
# 用户确认撤回
|
||||
try:
|
||||
# 导入隐私管理器
|
||||
from core.privacy_manager import PrivacyManager
|
||||
import sys
|
||||
import subprocess
|
||||
import os
|
||||
|
||||
# 创建实例并重置隐私协议同意
|
||||
privacy_manager = PrivacyManager()
|
||||
if privacy_manager.reset_privacy_agreement():
|
||||
# 显示重启提示
|
||||
restart_msg = self._create_message_box(
|
||||
"操作成功",
|
||||
"\n已成功撤回隐私协议同意。\n\n软件将立即重启。\n"
|
||||
)
|
||||
restart_msg.exec()
|
||||
|
||||
# 获取当前执行的Python解释器路径和脚本路径
|
||||
python_executable = sys.executable
|
||||
script_path = os.path.abspath(sys.argv[0])
|
||||
|
||||
# 构建重启命令
|
||||
restart_cmd = [python_executable, script_path]
|
||||
|
||||
# 启动新进程
|
||||
subprocess.Popen(restart_cmd)
|
||||
|
||||
# 退出当前进程
|
||||
sys.exit(0)
|
||||
else:
|
||||
# 显示失败提示
|
||||
fail_msg = self._create_message_box(
|
||||
"操作失败",
|
||||
"\n撤回隐私协议同意失败。\n\n请检查应用权限或稍后再试。\n"
|
||||
)
|
||||
fail_msg.exec()
|
||||
except Exception as e:
|
||||
# 显示错误提示
|
||||
error_msg = self._create_message_box(
|
||||
"错误",
|
||||
f"\n撤回隐私协议同意时发生错误:\n\n{str(e)}\n"
|
||||
)
|
||||
error_msg.exec()
|
||||
|
||||
def _create_message_box(self, title, message, buttons=QMessageBox.StandardButton.Ok):
|
||||
"""创建统一风格的消息框
|
||||
|
||||
Args:
|
||||
title: 消息框标题
|
||||
message: 消息内容
|
||||
buttons: 按钮类型,默认为确定按钮
|
||||
|
||||
Returns:
|
||||
QMessageBox: 配置好的消息框实例
|
||||
"""
|
||||
msg_box = msgbox_frame(
|
||||
f"{title} - {APP_NAME}",
|
||||
message,
|
||||
buttons,
|
||||
)
|
||||
return msg_box
|
||||
|
||||
def show_under_development(self):
|
||||
"""显示功能正在开发中的提示"""
|
||||
msg_box = self._create_message_box("提示", "\n该功能正在开发中,敬请期待!\n")
|
||||
msg_box.exec()
|
||||
|
||||
def show_download_thread_settings(self):
|
||||
"""显示下载线程设置对话框"""
|
||||
if hasattr(self.main_window, 'download_manager'):
|
||||
self.main_window.download_manager.show_download_thread_settings()
|
||||
else:
|
||||
# 如果下载管理器不可用,显示错误信息
|
||||
msg_box = self._create_message_box("错误", "\n下载管理器未初始化,无法修改下载线程设置。\n")
|
||||
msg_box.exec()
|
||||
|
||||
def open_log_file(self):
|
||||
"""打开log.txt文件"""
|
||||
try:
|
||||
# 使用操作系统默认程序打开日志文件
|
||||
if os.name == 'nt': # Windows
|
||||
os.startfile(LOG_FILE)
|
||||
else: # macOS 和 Linux
|
||||
import subprocess
|
||||
subprocess.call(['xdg-open', LOG_FILE])
|
||||
except Exception as e:
|
||||
msg_box = self._create_message_box("错误", f"\n打开log.txt文件失败:\n\n{str(e)}\n")
|
||||
msg_box.exec()
|
||||
|
||||
def restore_hosts_backup(self):
|
||||
"""还原软件备份的hosts文件"""
|
||||
if hasattr(self.main_window, 'download_manager') and hasattr(self.main_window.download_manager, 'hosts_manager'):
|
||||
try:
|
||||
# 调用恢复hosts文件的方法
|
||||
result = self.main_window.download_manager.hosts_manager.restore()
|
||||
|
||||
if result:
|
||||
msg_box = self._create_message_box("成功", "\nhosts文件已成功还原为备份版本。\n")
|
||||
else:
|
||||
msg_box = self._create_message_box("警告", "\n还原hosts文件失败或没有找到备份文件。\n")
|
||||
|
||||
msg_box.exec()
|
||||
except Exception as e:
|
||||
msg_box = self._create_message_box("错误", f"\n还原hosts文件时发生错误:\n\n{str(e)}\n")
|
||||
msg_box.exec()
|
||||
else:
|
||||
msg_box = self._create_message_box("错误", "\n无法访问hosts管理器。\n")
|
||||
msg_box.exec()
|
||||
|
||||
def clean_hosts_entries(self):
|
||||
"""手动删除软件添加的hosts条目"""
|
||||
if hasattr(self.main_window, 'download_manager') and hasattr(self.main_window.download_manager, 'hosts_manager'):
|
||||
try:
|
||||
# 调用清理hosts条目的方法
|
||||
result = self.main_window.download_manager.hosts_manager.check_and_clean_all_entries()
|
||||
|
||||
if result:
|
||||
msg_box = self._create_message_box("成功", "\n已成功清理软件添加的hosts条目。\n")
|
||||
else:
|
||||
msg_box = self._create_message_box("提示", "\n未发现软件添加的hosts条目或清理操作失败。\n")
|
||||
|
||||
msg_box.exec()
|
||||
except Exception as e:
|
||||
msg_box = self._create_message_box("错误", f"\n清理hosts条目时发生错误:\n\n{str(e)}\n")
|
||||
msg_box.exec()
|
||||
else:
|
||||
msg_box = self._create_message_box("错误", "\n无法访问hosts管理器。\n")
|
||||
msg_box.exec()
|
||||
|
||||
def open_hosts_file(self):
|
||||
"""打开系统hosts文件"""
|
||||
try:
|
||||
# 获取hosts文件路径
|
||||
hosts_path = os.path.join(os.environ['SystemRoot'], 'System32', 'drivers', 'etc', 'hosts')
|
||||
|
||||
# 检查文件是否存在
|
||||
if os.path.exists(hosts_path):
|
||||
# 使用操作系统默认程序打开hosts文件
|
||||
if os.name == 'nt': # Windows
|
||||
# 尝试以管理员权限打开记事本编辑hosts文件
|
||||
try:
|
||||
# 使用PowerShell以管理员身份启动记事本
|
||||
subprocess.Popen(["powershell", "Start-Process", "notepad", hosts_path, "-Verb", "RunAs"])
|
||||
except Exception as e:
|
||||
# 如果失败,尝试直接打开
|
||||
os.startfile(hosts_path)
|
||||
else: # macOS 和 Linux
|
||||
import subprocess
|
||||
subprocess.call(['xdg-open', hosts_path])
|
||||
else:
|
||||
msg_box = self._create_message_box("错误", f"\nhosts文件不存在:\n{hosts_path}\n")
|
||||
msg_box.exec()
|
||||
except Exception as e:
|
||||
msg_box = self._create_message_box("错误", f"\n打开hosts文件时发生错误:\n\n{str(e)}\n")
|
||||
msg_box.exec()
|
||||
|
||||
def show_about_dialog(self):
|
||||
"""显示关于对话框"""
|
||||
about_text = f"""
|
||||
<p><b>{APP_NAME} v{APP_VERSION}</b></p>
|
||||
<p>GitHub: <a href="https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT">https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT</a></p>
|
||||
<p>原作: <a href="https://github.com/Yanam1Anna">Yanam1Anna</a></p>
|
||||
<p>此应用根据 <a href="https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/blob/master/LICENSE">GPL-3.0 许可证</a> 授权。</p>
|
||||
<br>
|
||||
<p><b>感谢:</b></p>
|
||||
<p>- <a href="https://github.com/HTony03">HTony03</a>:对原项目部分源码的重构、逻辑优化和功能实现提供了支持。</p>
|
||||
<p>- <a href="https://github.com/ABSIDIA">钨鸮</a>:对于云端资源存储提供了支持。</p>
|
||||
<p>- <a href="https://github.com/XIU2/CloudflareSpeedTest">XIU2/CloudflareSpeedTest</a>:提供了 IP 优选功能的核心支持。</p>
|
||||
<p>- <a href="https://github.com/hosxy/aria2-fast">hosxy/aria2-fast</a>:提供了修改版aria2c,提高了下载速度和性能。</p>
|
||||
"""
|
||||
msg_box = msgbox_frame(
|
||||
f"关于 - {APP_NAME}",
|
||||
about_text,
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
msg_box.setTextFormat(Qt.TextFormat.RichText) # 使用Qt.TextFormat
|
||||
msg_box.exec()
|
||||
|
||||
def show_ipv6_manager_not_ready(self):
|
||||
"""显示IPv6管理器未准备好的提示"""
|
||||
msg_box = self._create_message_box("错误", "\nIPv6管理器尚未初始化,请稍后再试。\n")
|
||||
msg_box.exec()
|
||||
@@ -11,10 +11,14 @@ from PySide6.QtGui import (QAction, QBrush, QColor, QConicalGradient,
|
||||
from PySide6.QtWidgets import (QApplication, QLabel, QMainWindow, QMenu,
|
||||
QMenuBar, QPushButton, QSizePolicy, QWidget, QHBoxLayout)
|
||||
import os
|
||||
import logging
|
||||
|
||||
# 初始化日志记录器
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 导入配置常量
|
||||
from data.config import APP_NAME, APP_VERSION
|
||||
from utils import load_image_from_file
|
||||
from config.config import APP_NAME, APP_VERSION
|
||||
from utils import load_image_from_file, resource_path
|
||||
|
||||
class Ui_MainWindows(object):
|
||||
def setupUi(self, MainWindows):
|
||||
@@ -38,8 +42,21 @@ class Ui_MainWindows(object):
|
||||
MainWindows.setDockNestingEnabled(False)
|
||||
|
||||
# 加载自定义字体
|
||||
font_id = QFontDatabase.addApplicationFont(os.path.join(os.path.dirname(os.path.dirname(__file__)), "fonts", "SmileySans-Oblique.ttf"))
|
||||
font_family = QFontDatabase.applicationFontFamilies(font_id)[0] if font_id != -1 else "Arial"
|
||||
font_path = resource_path(os.path.join("assets", "fonts", "SmileySans-Oblique.ttf"))
|
||||
logger.info(f"尝试加载字体文件: {font_path}")
|
||||
font_id = QFontDatabase.addApplicationFont(font_path)
|
||||
if font_id != -1:
|
||||
font_families = QFontDatabase.applicationFontFamilies(font_id)
|
||||
if font_families:
|
||||
font_family = font_families[0]
|
||||
logger.info(f"成功加载字体: {font_family} 从 {font_path}")
|
||||
else:
|
||||
logger.warning(f"字体加载成功但无法获取字体族: {font_path}")
|
||||
font_family = "Arial"
|
||||
else:
|
||||
logger.error(f"字体加载失败: {font_path}")
|
||||
font_family = "Arial"
|
||||
|
||||
self.custom_font = QFont(font_family, 16) # 创建字体对象,大小为16
|
||||
self.custom_font.setWeight(QFont.Weight.Medium) # 设置为中等粗细,不要太粗
|
||||
|
||||
@@ -299,8 +316,11 @@ class Ui_MainWindows(object):
|
||||
self.loadbg.setObjectName(u"loadbg")
|
||||
self.loadbg.setGeometry(QRect(0, 0, 1280, 655))
|
||||
# 加载背景图并允许拉伸
|
||||
bg_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "IMG", "BG", "bg1.jpg")
|
||||
bg_path = resource_path(os.path.join("assets", "images", "BG", "bg1.jpg"))
|
||||
logger.info(f"加载背景图: {bg_path}")
|
||||
bg_pixmap = QPixmap(bg_path)
|
||||
if bg_pixmap.isNull():
|
||||
logger.error(f"背景图加载失败: {bg_path}")
|
||||
self.loadbg.setPixmap(bg_pixmap)
|
||||
self.loadbg.setScaledContents(True)
|
||||
|
||||
@@ -308,7 +328,8 @@ class Ui_MainWindows(object):
|
||||
self.vol1bg.setObjectName(u"vol1bg")
|
||||
self.vol1bg.setGeometry(QRect(0, 150, 93, 64))
|
||||
# 直接加载图片文件
|
||||
vol1_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "IMG", "LOGO", "vo01_logo.png")
|
||||
vol1_path = resource_path(os.path.join("assets", "images", "LOGO", "vo01_logo.png"))
|
||||
logger.info(f"加载LOGO图: {vol1_path}")
|
||||
self.vol1bg.setPixmap(QPixmap(vol1_path))
|
||||
self.vol1bg.setScaledContents(True)
|
||||
|
||||
@@ -316,7 +337,7 @@ class Ui_MainWindows(object):
|
||||
self.vol2bg.setObjectName(u"vol2bg")
|
||||
self.vol2bg.setGeometry(QRect(0, 210, 93, 64))
|
||||
# 直接加载图片文件
|
||||
vol2_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "IMG", "LOGO", "vo02_logo.png")
|
||||
vol2_path = resource_path(os.path.join("assets", "images", "LOGO", "vo02_logo.png"))
|
||||
self.vol2bg.setPixmap(QPixmap(vol2_path))
|
||||
self.vol2bg.setScaledContents(True)
|
||||
|
||||
@@ -324,7 +345,7 @@ class Ui_MainWindows(object):
|
||||
self.vol3bg.setObjectName(u"vol3bg")
|
||||
self.vol3bg.setGeometry(QRect(0, 270, 93, 64))
|
||||
# 直接加载图片文件
|
||||
vol3_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "IMG", "LOGO", "vo03_logo.png")
|
||||
vol3_path = resource_path(os.path.join("assets", "images", "LOGO", "vo03_logo.png"))
|
||||
self.vol3bg.setPixmap(QPixmap(vol3_path))
|
||||
self.vol3bg.setScaledContents(True)
|
||||
|
||||
@@ -332,7 +353,7 @@ class Ui_MainWindows(object):
|
||||
self.vol4bg.setObjectName(u"vol4bg")
|
||||
self.vol4bg.setGeometry(QRect(0, 330, 93, 64))
|
||||
# 直接加载图片文件
|
||||
vol4_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "IMG", "LOGO", "vo04_logo.png")
|
||||
vol4_path = resource_path(os.path.join("assets", "images", "LOGO", "vo04_logo.png"))
|
||||
self.vol4bg.setPixmap(QPixmap(vol4_path))
|
||||
self.vol4bg.setScaledContents(True)
|
||||
|
||||
@@ -340,7 +361,7 @@ class Ui_MainWindows(object):
|
||||
self.afterbg.setObjectName(u"afterbg")
|
||||
self.afterbg.setGeometry(QRect(0, 390, 93, 64))
|
||||
# 直接加载图片文件
|
||||
after_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "IMG", "LOGO", "voaf_logo.png")
|
||||
after_path = resource_path(os.path.join("assets", "images", "LOGO", "voaf_logo.png"))
|
||||
self.afterbg.setPixmap(QPixmap(after_path))
|
||||
self.afterbg.setScaledContents(True)
|
||||
|
||||
@@ -349,8 +370,12 @@ class Ui_MainWindows(object):
|
||||
self.Mainbg.setObjectName(u"Mainbg")
|
||||
self.Mainbg.setGeometry(QRect(0, 0, 1280, 655))
|
||||
# 允许拉伸以填满整个区域
|
||||
main_bg_pixmap = load_image_from_file(os.path.join(os.path.dirname(os.path.dirname(__file__)), "IMG", "BG", "title_bg1.png"))
|
||||
|
||||
main_bg_path = resource_path(os.path.join("assets", "images", "BG", "title_bg1.png"))
|
||||
logger.info(f"加载主背景图: {main_bg_path}")
|
||||
main_bg_pixmap = QPixmap(main_bg_path)
|
||||
if main_bg_pixmap.isNull():
|
||||
logger.error(f"主背景图加载失败: {main_bg_path}")
|
||||
|
||||
# 如果加载的图片不是空的,则设置,并允许拉伸填满
|
||||
if not main_bg_pixmap.isNull():
|
||||
self.Mainbg.setPixmap(main_bg_pixmap)
|
||||
@@ -358,14 +383,18 @@ class Ui_MainWindows(object):
|
||||
self.Mainbg.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
# 使用新的按钮图片
|
||||
button_pixmap = load_image_from_file(os.path.join(os.path.dirname(os.path.dirname(__file__)), "IMG", "BTN", "Button.png"))
|
||||
button_path = resource_path(os.path.join("assets", "images", "BTN", "Button.png"))
|
||||
logger.info(f"加载按钮图片: {button_path}")
|
||||
button_pixmap = QPixmap(button_path)
|
||||
if button_pixmap.isNull():
|
||||
logger.error(f"按钮图片加载失败: {button_path}")
|
||||
|
||||
# 创建文本标签布局的按钮
|
||||
# 开始安装按钮 - 基于背景图片和标签组合
|
||||
# 调整开始安装按钮的位置
|
||||
self.button_container = QWidget(self.inner_content)
|
||||
self.button_container.setObjectName(u"start_install_container")
|
||||
self.button_container.setGeometry(QRect(1050, 200, 211, 111)) # 调整Y坐标,上移至200
|
||||
self.button_container.setGeometry(QRect(1045, 20, 211, 111)) # 调整坐标,Y设为20,X稍微左移
|
||||
# 不要隐藏容器,让动画系统来控制它的可见性和位置
|
||||
|
||||
# 使用原来的按钮背景图片
|
||||
@@ -397,10 +426,44 @@ class Ui_MainWindows(object):
|
||||
}
|
||||
""")
|
||||
|
||||
# 添加禁/启用补丁按钮 - 新增在开始安装和卸载补丁之间
|
||||
self.toggle_patch_container = QWidget(self.inner_content)
|
||||
self.toggle_patch_container.setObjectName(u"toggle_patch_container")
|
||||
self.toggle_patch_container.setGeometry(QRect(1050, 180, 211, 111)) # 调整Y坐标,设为180,增大与开始安装的间距
|
||||
|
||||
# 使用相同的按钮背景图片
|
||||
self.toggle_patch_bg = QLabel(self.toggle_patch_container)
|
||||
self.toggle_patch_bg.setObjectName(u"toggle_patch_bg")
|
||||
self.toggle_patch_bg.setGeometry(QRect(10, 10, 191, 91)) # 居中放置在扩大的容器中
|
||||
self.toggle_patch_bg.setPixmap(button_pixmap)
|
||||
self.toggle_patch_bg.setScaledContents(True)
|
||||
|
||||
self.toggle_patch_text = QLabel(self.toggle_patch_container)
|
||||
self.toggle_patch_text.setObjectName(u"toggle_patch_text")
|
||||
self.toggle_patch_text.setGeometry(QRect(10, 7, 191, 91)) # 居中放置在扩大的容器中
|
||||
self.toggle_patch_text.setText("禁/启用补丁")
|
||||
self.toggle_patch_text.setFont(self.custom_font)
|
||||
self.toggle_patch_text.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.toggle_patch_text.setStyleSheet("letter-spacing: 1px;")
|
||||
|
||||
# 点击区域透明按钮
|
||||
self.toggle_patch_btn = QPushButton(self.toggle_patch_container)
|
||||
self.toggle_patch_btn.setObjectName(u"toggle_patch_btn")
|
||||
self.toggle_patch_btn.setGeometry(QRect(10, 10, 191, 91)) # 居中放置在扩大的容器中
|
||||
self.toggle_patch_btn.setCursor(QCursor(Qt.CursorShape.PointingHandCursor)) # 设置鼠标悬停时为手形光标
|
||||
self.toggle_patch_btn.setFlat(True)
|
||||
self.toggle_patch_btn.raise_() # 确保按钮在最上层
|
||||
self.toggle_patch_btn.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
}
|
||||
""")
|
||||
|
||||
# 添加卸载补丁按钮 - 新增
|
||||
self.uninstall_container = QWidget(self.inner_content)
|
||||
self.uninstall_container.setObjectName(u"uninstall_container")
|
||||
self.uninstall_container.setGeometry(QRect(1050, 310, 211, 111)) # 调整Y坐标,位于310位置
|
||||
self.uninstall_container.setGeometry(QRect(1050, 320, 211, 111)) # 设置Y坐标为320
|
||||
|
||||
# 使用相同的按钮背景图片
|
||||
self.uninstall_bg = QLabel(self.uninstall_container)
|
||||
@@ -434,7 +497,7 @@ class Ui_MainWindows(object):
|
||||
# 退出按钮 - 基于背景图片和标签组合,调整位置
|
||||
self.exit_container = QWidget(self.inner_content)
|
||||
self.exit_container.setObjectName(u"exit_container")
|
||||
self.exit_container.setGeometry(QRect(1050, 420, 211, 111)) # 调整Y坐标,下移至420
|
||||
self.exit_container.setGeometry(QRect(1050, 450, 211, 111)) # 调整Y坐标,设为450
|
||||
# 不要隐藏容器,让动画系统来控制它的可见性和位置
|
||||
|
||||
# 使用原来的按钮背景图片
|
||||
@@ -476,11 +539,28 @@ class Ui_MainWindows(object):
|
||||
self.vol4bg.raise_()
|
||||
self.afterbg.raise_()
|
||||
self.Mainbg.raise_()
|
||||
# 显式单独抬升开始安装按钮的所有组件
|
||||
self.button_container.raise_()
|
||||
self.uninstall_container.raise_() # 添加新按钮到层级顺序
|
||||
self.start_install_bg.raise_()
|
||||
self.start_install_text.raise_()
|
||||
self.start_install_btn.raise_()
|
||||
# 显式单独抬升禁/启用补丁按钮的所有组件
|
||||
self.toggle_patch_container.raise_()
|
||||
self.toggle_patch_bg.raise_()
|
||||
self.toggle_patch_text.raise_()
|
||||
self.toggle_patch_btn.raise_()
|
||||
# 显式单独抬升卸载补丁按钮的所有组件
|
||||
self.uninstall_container.raise_()
|
||||
self.uninstall_bg.raise_()
|
||||
self.uninstall_text.raise_()
|
||||
self.uninstall_btn.raise_()
|
||||
# 显式单独抬升退出按钮的所有组件
|
||||
self.exit_container.raise_()
|
||||
self.exit_bg.raise_()
|
||||
self.exit_text.raise_()
|
||||
self.exit_btn.raise_()
|
||||
# 其他UI元素
|
||||
self.menu_area.raise_() # 确保菜单区域在背景之上
|
||||
# self.menubar.raise_() # 不再需要菜单栏
|
||||
self.settings_btn.raise_() # 确保设置按钮在上层
|
||||
self.help_btn.raise_() # 确保帮助按钮在上层
|
||||
self.title_bar.raise_() # 确保标题栏在最上层
|
||||
@@ -491,7 +571,6 @@ class Ui_MainWindows(object):
|
||||
# setupUi
|
||||
|
||||
def retranslateUi(self, MainWindows):
|
||||
MainWindows.setWindowTitle(QCoreApplication.translate("MainWindows", f"{APP_NAME} v{APP_VERSION}", None))
|
||||
self.loadbg.setText("")
|
||||
self.vol1bg.setText("")
|
||||
self.vol2bg.setText("")
|
||||
|
||||
16
source/ui/components/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
UI组件模块
|
||||
提供各种UI组件类用于构建应用程序界面
|
||||
"""
|
||||
|
||||
from .font_style_manager import FontStyleManager
|
||||
from .dialog_factory import DialogFactory
|
||||
from .external_links_handler import ExternalLinksHandler
|
||||
from .menu_builder import MenuBuilder
|
||||
|
||||
__all__ = [
|
||||
'FontStyleManager',
|
||||
'DialogFactory',
|
||||
'ExternalLinksHandler',
|
||||
'MenuBuilder'
|
||||
]
|
||||
147
source/ui/components/dialog_factory.py
Normal file
@@ -0,0 +1,147 @@
|
||||
"""
|
||||
对话框工厂
|
||||
负责创建和管理各种类型的对话框
|
||||
"""
|
||||
|
||||
from PySide6.QtWidgets import QMessageBox, QDialog, QVBoxLayout, QProgressBar, QLabel, QApplication
|
||||
from PySide6.QtCore import Qt
|
||||
|
||||
from utils import msgbox_frame
|
||||
from config.config import APP_NAME
|
||||
from workers.download import ProgressWindow
|
||||
|
||||
|
||||
class DialogFactory:
|
||||
"""对话框工厂类"""
|
||||
|
||||
def __init__(self, main_window):
|
||||
"""初始化对话框工厂
|
||||
|
||||
Args:
|
||||
main_window: 主窗口实例
|
||||
"""
|
||||
self.main_window = main_window
|
||||
self.loading_dialog = None
|
||||
|
||||
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 create_progress_window(self, title, initial_text="准备中..."):
|
||||
"""创建并返回一个通用的进度窗口
|
||||
|
||||
Args:
|
||||
title (str): 窗口标题
|
||||
initial_text (str): 初始状态文本
|
||||
|
||||
Returns:
|
||||
QDialog: 配置好的进度窗口实例
|
||||
"""
|
||||
# 如果是下载进度窗口,使用专用的ProgressWindow类
|
||||
if "下载" in title:
|
||||
return ProgressWindow(self.main_window)
|
||||
|
||||
# 其他情况使用基本的进度窗口
|
||||
progress_window = QDialog(self.main_window)
|
||||
progress_window.setWindowTitle(f"{title} - {APP_NAME}")
|
||||
progress_window.setFixedSize(400, 150)
|
||||
|
||||
layout = QVBoxLayout()
|
||||
|
||||
progress_bar = QProgressBar()
|
||||
progress_bar.setRange(0, 100)
|
||||
progress_bar.setValue(0)
|
||||
layout.addWidget(progress_bar)
|
||||
|
||||
status_label = QLabel(initial_text)
|
||||
layout.addWidget(status_label)
|
||||
|
||||
progress_window.setLayout(layout)
|
||||
# 将控件附加到窗口对象上,以便外部访问
|
||||
progress_window.progress_bar = progress_bar
|
||||
progress_window.status_label = status_label
|
||||
|
||||
return progress_window
|
||||
|
||||
def show_loading_dialog(self, message):
|
||||
"""显示或更新加载对话框
|
||||
|
||||
Args:
|
||||
message: 要显示的加载消息
|
||||
"""
|
||||
if not self.loading_dialog:
|
||||
self.loading_dialog = QDialog(self.main_window)
|
||||
self.loading_dialog.setWindowTitle(f"请稍候 - {APP_NAME}")
|
||||
self.loading_dialog.setFixedSize(300, 100)
|
||||
self.loading_dialog.setModal(True)
|
||||
layout = QVBoxLayout()
|
||||
loading_label = QLabel(message)
|
||||
loading_label.setAlignment(Qt.AlignCenter)
|
||||
layout.addWidget(loading_label)
|
||||
self.loading_dialog.setLayout(layout)
|
||||
# 将label附加到dialog,方便后续更新
|
||||
self.loading_dialog.loading_label = loading_label
|
||||
else:
|
||||
self.loading_dialog.loading_label.setText(message)
|
||||
|
||||
self.loading_dialog.show()
|
||||
# 强制UI更新
|
||||
QApplication.processEvents()
|
||||
|
||||
def hide_loading_dialog(self):
|
||||
"""隐藏并销毁加载对话框"""
|
||||
if self.loading_dialog:
|
||||
self.loading_dialog.hide()
|
||||
self.loading_dialog = None
|
||||
|
||||
def show_simple_message(self, title, message, message_type="info"):
|
||||
"""显示简单的消息提示
|
||||
|
||||
Args:
|
||||
title: 标题
|
||||
message: 消息内容
|
||||
message_type: 消息类型,可选 "info", "warning", "error", "question"
|
||||
"""
|
||||
if message_type == "question":
|
||||
buttons = QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
|
||||
else:
|
||||
buttons = QMessageBox.StandardButton.Ok
|
||||
|
||||
msg_box = self.create_message_box(title, message, buttons)
|
||||
|
||||
if message_type == "question":
|
||||
return msg_box.exec()
|
||||
else:
|
||||
msg_box.exec()
|
||||
return None
|
||||
|
||||
def show_confirmation_dialog(self, title, message):
|
||||
"""显示确认对话框
|
||||
|
||||
Args:
|
||||
title: 标题
|
||||
message: 消息内容
|
||||
|
||||
Returns:
|
||||
bool: 用户是否选择了确认
|
||||
"""
|
||||
msg_box = self.create_message_box(
|
||||
title,
|
||||
message,
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
|
||||
)
|
||||
return msg_box.exec() == QMessageBox.StandardButton.Yes
|
||||
145
source/ui/components/external_links_handler.py
Normal file
@@ -0,0 +1,145 @@
|
||||
"""
|
||||
外部链接处理器
|
||||
负责处理所有外部链接打开和关于信息显示
|
||||
"""
|
||||
|
||||
import webbrowser
|
||||
import locale
|
||||
import sys
|
||||
import subprocess
|
||||
import os
|
||||
from PySide6.QtWidgets import QMessageBox
|
||||
from PySide6.QtCore import Qt
|
||||
|
||||
from config.config import APP_NAME, APP_VERSION
|
||||
from utils import msgbox_frame
|
||||
|
||||
|
||||
class ExternalLinksHandler:
|
||||
"""外部链接处理器类"""
|
||||
|
||||
def __init__(self, main_window, dialog_factory=None):
|
||||
"""初始化外部链接处理器
|
||||
|
||||
Args:
|
||||
main_window: 主窗口实例
|
||||
dialog_factory: 对话框工厂实例
|
||||
"""
|
||||
self.main_window = main_window
|
||||
self.dialog_factory = dialog_factory
|
||||
|
||||
def open_project_home_page(self):
|
||||
"""打开项目主页"""
|
||||
webbrowser.open("https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT")
|
||||
|
||||
def open_github_page(self):
|
||||
"""打开项目GitHub页面"""
|
||||
webbrowser.open("https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT")
|
||||
|
||||
def open_faq_page(self):
|
||||
"""打开常见问题页面"""
|
||||
# 根据系统语言选择FAQ页面
|
||||
system_lang = locale.getdefaultlocale()[0]
|
||||
if system_lang and system_lang.startswith('zh'):
|
||||
webbrowser.open("https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/blob/master/FAQ.md")
|
||||
else:
|
||||
webbrowser.open("https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/blob/master/FAQ-en.md")
|
||||
|
||||
def open_issues_page(self):
|
||||
"""打开GitHub问题页面"""
|
||||
webbrowser.open("https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/issues")
|
||||
|
||||
def open_qq_group(self):
|
||||
"""打开QQ群链接"""
|
||||
webbrowser.open("https://qm.qq.com/q/g9i04i5eec")
|
||||
|
||||
def open_privacy_policy(self):
|
||||
"""打开完整隐私协议(在GitHub上)"""
|
||||
webbrowser.open("https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/blob/master/PRIVACY.md")
|
||||
|
||||
def show_about_dialog(self):
|
||||
"""显示关于对话框"""
|
||||
about_text = f"""
|
||||
<p><b>{APP_NAME} v{APP_VERSION}</b></p>
|
||||
<p>GitHub: <a href="https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT">https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT</a></p>
|
||||
<p>原作: <a href="https://github.com/Yanam1Anna">Yanam1Anna</a></p>
|
||||
<p>此应用根据 <a href="https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/blob/master/LICENSE">GPL-3.0 许可证</a> 授权。</p>
|
||||
<br>
|
||||
<p><b>感谢:</b></p>
|
||||
<p>- <a href="https://github.com/HTony03">HTony03</a>:对原项目部分源码的重构、逻辑优化和功能实现提供了支持。</p>
|
||||
<p>- <a href="https://github.com/ABSIDIA">钨鸮</a>:对于云端资源存储提供了支持。</p>
|
||||
<p>- <a href="https://github.com/XIU2/CloudflareSpeedTest">XIU2/CloudflareSpeedTest</a>:提供了 IP 优选功能的核心支持。</p>
|
||||
<p>- <a href="https://github.com/hosxy/aria2-fast">hosxy/aria2-fast</a>:提供了修改版aria2c,提高了下载速度和性能。</p>
|
||||
"""
|
||||
msg_box = msgbox_frame(
|
||||
f"关于 - {APP_NAME}",
|
||||
about_text,
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
msg_box.setTextFormat(Qt.TextFormat.RichText)
|
||||
msg_box.exec()
|
||||
|
||||
def revoke_privacy_agreement(self):
|
||||
"""撤回隐私协议同意,并重启软件"""
|
||||
# 创建确认对话框
|
||||
if self.dialog_factory:
|
||||
response = self.dialog_factory.show_confirmation_dialog(
|
||||
"确认操作",
|
||||
"\n您确定要撤回隐私协议同意吗?\n\n撤回后软件将立即重启,您需要重新阅读并同意隐私协议。\n"
|
||||
)
|
||||
else:
|
||||
msg_box = msgbox_frame(
|
||||
f"确认操作 - {APP_NAME}",
|
||||
"\n您确定要撤回隐私协议同意吗?\n\n撤回后软件将立即重启,您需要重新阅读并同意隐私协议。\n",
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||
)
|
||||
response = msg_box.exec() == QMessageBox.StandardButton.Yes
|
||||
|
||||
if response:
|
||||
try:
|
||||
from core.managers.privacy_manager import PrivacyManager
|
||||
|
||||
privacy_manager = PrivacyManager()
|
||||
if privacy_manager.reset_privacy_agreement():
|
||||
# 显示重启提示
|
||||
if self.dialog_factory:
|
||||
self.dialog_factory.show_simple_message(
|
||||
"操作成功",
|
||||
"\n已成功撤回隐私协议同意。\n\n软件将立即重启。\n"
|
||||
)
|
||||
else:
|
||||
restart_msg = msgbox_frame(
|
||||
f"操作成功 - {APP_NAME}",
|
||||
"\n已成功撤回隐私协议同意。\n\n软件将立即重启。\n",
|
||||
QMessageBox.StandardButton.Ok
|
||||
)
|
||||
restart_msg.exec()
|
||||
|
||||
# 重启应用程序
|
||||
python_executable = sys.executable
|
||||
script_path = os.path.abspath(sys.argv[0])
|
||||
subprocess.Popen([python_executable, script_path])
|
||||
sys.exit(0)
|
||||
else:
|
||||
if self.dialog_factory:
|
||||
self.dialog_factory.show_simple_message(
|
||||
"操作失败",
|
||||
"\n撤回隐私协议同意失败。\n\n请检查应用权限或稍后再试。\n",
|
||||
"error"
|
||||
)
|
||||
else:
|
||||
msgbox_frame(
|
||||
f"操作失败 - {APP_NAME}",
|
||||
"\n撤回隐私协议同意失败。\n\n请检查应用权限或稍后再试。\n",
|
||||
QMessageBox.StandardButton.Ok
|
||||
).exec()
|
||||
except Exception as e:
|
||||
error_message = f"\n撤回隐私协议同意时发生错误:\n\n{str(e)}\n"
|
||||
if self.dialog_factory:
|
||||
self.dialog_factory.show_simple_message("错误", error_message, "error")
|
||||
else:
|
||||
msgbox_frame(
|
||||
f"错误 - {APP_NAME}",
|
||||
error_message,
|
||||
QMessageBox.StandardButton.Ok
|
||||
).exec()
|
||||
147
source/ui/components/font_style_manager.py
Normal file
@@ -0,0 +1,147 @@
|
||||
"""
|
||||
字体和样式管理器
|
||||
负责管理应用程序的字体加载和UI样式
|
||||
"""
|
||||
|
||||
import os
|
||||
import logging
|
||||
import traceback
|
||||
from PySide6.QtGui import QFont, QFontDatabase
|
||||
|
||||
from utils import resource_path
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FontStyleManager:
|
||||
"""字体和样式管理器"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化字体样式管理器"""
|
||||
self._cached_font = None
|
||||
self._font_family = "Arial" # 默认字体族
|
||||
self._load_custom_font()
|
||||
|
||||
def _load_custom_font(self):
|
||||
"""加载自定义字体"""
|
||||
try:
|
||||
# 使用resource_path查找字体文件
|
||||
font_path = resource_path(os.path.join("assets", "fonts", "SmileySans-Oblique.ttf"))
|
||||
|
||||
# 详细记录字体加载过程
|
||||
if os.path.exists(font_path):
|
||||
logger.info(f"尝试加载字体文件: {font_path}")
|
||||
font_id = QFontDatabase.addApplicationFont(font_path)
|
||||
|
||||
if font_id != -1:
|
||||
font_families = QFontDatabase.applicationFontFamilies(font_id)
|
||||
if font_families:
|
||||
self._font_family = font_families[0]
|
||||
logger.info(f"成功加载字体: {self._font_family} 从 {font_path}")
|
||||
else:
|
||||
logger.warning(f"字体加载成功但无法获取字体族: {font_path}")
|
||||
else:
|
||||
logger.warning(f"字体加载失败: {font_path} (返回ID: {font_id})")
|
||||
self._check_font_file_issues(font_path)
|
||||
else:
|
||||
logger.error(f"找不到字体文件: {font_path}")
|
||||
self._list_font_directory(font_path)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"加载字体过程中发生异常: {e}")
|
||||
logger.error(f"异常详情: {traceback.format_exc()}")
|
||||
|
||||
def _check_font_file_issues(self, font_path):
|
||||
"""检查字体文件的问题"""
|
||||
try:
|
||||
file_size = os.path.getsize(font_path)
|
||||
logger.debug(f"字体文件大小: {file_size} 字节")
|
||||
if file_size == 0:
|
||||
logger.error(f"字体文件大小为0字节: {font_path}")
|
||||
|
||||
# 尝试打开文件测试可读性
|
||||
with open(font_path, 'rb') as f:
|
||||
f.read(10) # 只读取前几个字节测试可访问性
|
||||
logger.debug(f"字体文件可以正常打开和读取")
|
||||
except Exception as file_error:
|
||||
logger.error(f"字体文件访问错误: {file_error}")
|
||||
|
||||
def _list_font_directory(self, font_path):
|
||||
"""列出字体目录下的文件"""
|
||||
try:
|
||||
fonts_dir = os.path.dirname(font_path)
|
||||
if os.path.exists(fonts_dir):
|
||||
files = os.listdir(fonts_dir)
|
||||
logger.debug(f"字体目录 {fonts_dir} 中的文件: {files}")
|
||||
else:
|
||||
logger.debug(f"字体目录不存在: {fonts_dir}")
|
||||
except Exception as dir_error:
|
||||
logger.error(f"无法列出字体目录内容: {dir_error}")
|
||||
|
||||
def get_menu_font(self, size=14, bold=True):
|
||||
"""获取菜单字体
|
||||
|
||||
Args:
|
||||
size: 字体大小,默认14
|
||||
bold: 是否加粗,默认True
|
||||
|
||||
Returns:
|
||||
QFont: 配置好的菜单字体
|
||||
"""
|
||||
if self._cached_font is None or self._cached_font.pointSize() != size:
|
||||
self._cached_font = QFont(self._font_family, size)
|
||||
self._cached_font.setBold(bold)
|
||||
return self._cached_font
|
||||
|
||||
def get_menu_style(self, font_family=None):
|
||||
"""获取统一的菜单样式
|
||||
|
||||
Args:
|
||||
font_family: 字体族,如果不提供则使用默认
|
||||
|
||||
Returns:
|
||||
str: CSS样式字符串
|
||||
"""
|
||||
if font_family is None:
|
||||
font_family = self._font_family
|
||||
|
||||
return f"""
|
||||
QMenu {{
|
||||
background-color: #E96948;
|
||||
color: white;
|
||||
font-family: "{font_family}";
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
border: 1px solid #F47A5B;
|
||||
padding: 8px;
|
||||
border-radius: 6px;
|
||||
margin-top: 2px;
|
||||
}}
|
||||
QMenu::item {{
|
||||
padding: 6px 20px 6px 15px;
|
||||
background-color: transparent;
|
||||
min-width: 120px;
|
||||
color: white;
|
||||
font-family: "{font_family}";
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}}
|
||||
QMenu::item:selected {{
|
||||
background-color: #F47A5B;
|
||||
border-radius: 4px;
|
||||
}}
|
||||
QMenu::separator {{
|
||||
height: 1px;
|
||||
background-color: #F47A5B;
|
||||
margin: 5px 15px;
|
||||
}}
|
||||
QMenu::item:checked {{
|
||||
background-color: #D25A3C;
|
||||
border-radius: 4px;
|
||||
}}
|
||||
"""
|
||||
|
||||
@property
|
||||
def font_family(self):
|
||||
"""获取当前字体族"""
|
||||
return self._font_family
|
||||
502
source/ui/components/menu_builder.py
Normal file
@@ -0,0 +1,502 @@
|
||||
"""
|
||||
菜单构建器
|
||||
负责构建和管理应用程序的各种菜单
|
||||
"""
|
||||
|
||||
from PySide6.QtGui import QAction, QActionGroup, QCursor
|
||||
from PySide6.QtWidgets import QMenu, QPushButton
|
||||
from PySide6.QtCore import Qt, QRect
|
||||
|
||||
from config.config import APP_NAME, APP_VERSION
|
||||
|
||||
|
||||
class MenuBuilder:
|
||||
"""菜单构建器类"""
|
||||
|
||||
def __init__(self, main_window, font_style_manager, external_links_handler, dialog_factory):
|
||||
"""初始化菜单构建器
|
||||
|
||||
Args:
|
||||
main_window: 主窗口实例
|
||||
font_style_manager: 字体样式管理器
|
||||
external_links_handler: 外部链接处理器
|
||||
dialog_factory: 对话框工厂
|
||||
"""
|
||||
self.main_window = main_window
|
||||
self.ui = getattr(main_window, 'ui', None)
|
||||
self.font_style_manager = font_style_manager
|
||||
self.external_links_handler = external_links_handler
|
||||
self.dialog_factory = dialog_factory
|
||||
|
||||
# 菜单引用
|
||||
self.dev_menu = None
|
||||
self.privacy_menu = None
|
||||
self.about_menu = None
|
||||
self.about_btn = None
|
||||
|
||||
# 工作模式相关
|
||||
self.work_mode_menu = None
|
||||
self.online_mode_action = None
|
||||
self.offline_mode_action = None
|
||||
|
||||
# 开发者选项相关
|
||||
self.debug_submenu = None
|
||||
self.hosts_submenu = None
|
||||
self.ipv6_submenu = None
|
||||
self.hash_settings_menu = None
|
||||
self.download_settings_menu = None
|
||||
|
||||
# 各种action引用
|
||||
self.debug_action = None
|
||||
self.open_log_action = None
|
||||
self.ipv6_action = None
|
||||
self.ipv6_test_action = None
|
||||
self.disable_auto_restore_action = None
|
||||
self.disable_pre_hash_action = None
|
||||
|
||||
def setup_all_menus(self):
|
||||
"""设置所有菜单"""
|
||||
self.create_about_button()
|
||||
self.setup_help_menu()
|
||||
self.setup_about_menu()
|
||||
self.setup_settings_menu()
|
||||
|
||||
def create_about_button(self):
|
||||
"""创建"关于"按钮"""
|
||||
if not self.ui or not hasattr(self.ui, 'menu_area'):
|
||||
return
|
||||
|
||||
# 获取菜单字体和样式
|
||||
menu_font = self.font_style_manager.get_menu_font()
|
||||
|
||||
# 创建关于按钮
|
||||
self.about_btn = QPushButton("关于", self.ui.menu_area)
|
||||
self.about_btn.setObjectName(u"about_btn")
|
||||
|
||||
# 获取帮助按钮的位置和样式
|
||||
help_btn_x = 0
|
||||
help_btn_width = 0
|
||||
if hasattr(self.ui, 'help_btn'):
|
||||
help_btn_x = self.ui.help_btn.x()
|
||||
help_btn_width = self.ui.help_btn.width()
|
||||
|
||||
# 设置位置在"帮助"按钮右侧
|
||||
self.about_btn.setGeometry(QRect(help_btn_x + help_btn_width + 20, 1, 80, 28))
|
||||
self.about_btn.setFont(menu_font)
|
||||
self.about_btn.setCursor(QCursor(Qt.CursorShape.PointingHandCursor))
|
||||
|
||||
# 复制帮助按钮的样式
|
||||
if hasattr(self.ui, 'help_btn'):
|
||||
self.about_btn.setStyleSheet(self.ui.help_btn.styleSheet())
|
||||
else:
|
||||
# 默认样式
|
||||
self.about_btn.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: transparent;
|
||||
color: white;
|
||||
border: none;
|
||||
text-align: left;
|
||||
padding-left: 10px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #F47A5B;
|
||||
border-radius: 4px;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #D25A3C;
|
||||
border-radius: 4px;
|
||||
}
|
||||
""")
|
||||
|
||||
def setup_help_menu(self):
|
||||
"""设置"帮助"菜单"""
|
||||
if not self.ui or not hasattr(self.ui, 'menu_2'):
|
||||
return
|
||||
|
||||
# 获取菜单字体
|
||||
menu_font = self.font_style_manager.get_menu_font()
|
||||
|
||||
# 创建菜单项
|
||||
faq_action = QAction("常见问题", self.main_window)
|
||||
faq_action.triggered.connect(self.external_links_handler.open_faq_page)
|
||||
faq_action.setFont(menu_font)
|
||||
|
||||
report_issue_action = QAction("提交错误", self.main_window)
|
||||
report_issue_action.triggered.connect(self.external_links_handler.open_issues_page)
|
||||
report_issue_action.setFont(menu_font)
|
||||
|
||||
# 清除现有菜单项并添加新的菜单项
|
||||
self.ui.menu_2.clear()
|
||||
self.ui.menu_2.addAction(faq_action)
|
||||
self.ui.menu_2.addAction(report_issue_action)
|
||||
|
||||
def setup_about_menu(self):
|
||||
"""设置"关于"菜单"""
|
||||
# 获取菜单字体
|
||||
menu_font = self.font_style_manager.get_menu_font()
|
||||
|
||||
# 创建关于菜单
|
||||
self.about_menu = QMenu("关于", self.main_window)
|
||||
self.about_menu.setFont(menu_font)
|
||||
|
||||
# 设置菜单样式
|
||||
menu_style = self.font_style_manager.get_menu_style()
|
||||
self.about_menu.setStyleSheet(menu_style)
|
||||
|
||||
# 创建菜单项
|
||||
about_project_action = QAction("关于本项目", self.main_window)
|
||||
about_project_action.setFont(menu_font)
|
||||
about_project_action.triggered.connect(self.external_links_handler.show_about_dialog)
|
||||
|
||||
project_home_action = QAction("Github项目主页", self.main_window)
|
||||
project_home_action.setFont(menu_font)
|
||||
project_home_action.triggered.connect(self.external_links_handler.open_project_home_page)
|
||||
|
||||
qq_group_action = QAction("加入QQ群", self.main_window)
|
||||
qq_group_action.setFont(menu_font)
|
||||
qq_group_action.triggered.connect(self.external_links_handler.open_qq_group)
|
||||
|
||||
# 创建隐私协议菜单
|
||||
self.setup_privacy_menu()
|
||||
|
||||
# 添加到关于菜单
|
||||
self.about_menu.addAction(about_project_action)
|
||||
self.about_menu.addAction(project_home_action)
|
||||
self.about_menu.addAction(qq_group_action)
|
||||
self.about_menu.addSeparator()
|
||||
self.about_menu.addMenu(self.privacy_menu)
|
||||
|
||||
# 连接按钮点击事件
|
||||
if self.about_btn:
|
||||
self.about_btn.clicked.connect(lambda: self.show_menu(self.about_menu, self.about_btn))
|
||||
|
||||
def setup_privacy_menu(self):
|
||||
"""设置"隐私协议"菜单"""
|
||||
menu_font = self.font_style_manager.get_menu_font()
|
||||
|
||||
# 创建隐私协议子菜单
|
||||
self.privacy_menu = QMenu("隐私协议", self.main_window)
|
||||
self.privacy_menu.setFont(menu_font)
|
||||
|
||||
# 设置样式
|
||||
menu_style = self.font_style_manager.get_menu_style()
|
||||
self.privacy_menu.setStyleSheet(menu_style)
|
||||
|
||||
# 添加子选项
|
||||
view_privacy_action = QAction("查看完整隐私协议", self.main_window)
|
||||
view_privacy_action.setFont(menu_font)
|
||||
view_privacy_action.triggered.connect(self.external_links_handler.open_privacy_policy)
|
||||
|
||||
revoke_privacy_action = QAction("撤回隐私协议", self.main_window)
|
||||
revoke_privacy_action.setFont(menu_font)
|
||||
revoke_privacy_action.triggered.connect(self.external_links_handler.revoke_privacy_agreement)
|
||||
|
||||
# 添加到子菜单
|
||||
self.privacy_menu.addAction(view_privacy_action)
|
||||
self.privacy_menu.addAction(revoke_privacy_action)
|
||||
|
||||
def setup_settings_menu(self):
|
||||
"""设置"设置"菜单"""
|
||||
if not self.ui or not hasattr(self.ui, 'menu'):
|
||||
return
|
||||
|
||||
# 获取菜单字体
|
||||
menu_font = self.font_style_manager.get_menu_font()
|
||||
menu_style = self.font_style_manager.get_menu_style()
|
||||
|
||||
# 创建各个子菜单
|
||||
self._create_work_mode_menu(menu_font, menu_style)
|
||||
self._create_download_settings_menu(menu_font, menu_style)
|
||||
self._create_developer_options_menu(menu_font, menu_style)
|
||||
|
||||
# 添加到主菜单
|
||||
self.ui.menu.addMenu(self.work_mode_menu)
|
||||
self.ui.menu.addMenu(self.download_settings_menu)
|
||||
self.ui.menu.addSeparator()
|
||||
self.ui.menu.addMenu(self.dev_menu)
|
||||
|
||||
def _create_work_mode_menu(self, menu_font, menu_style):
|
||||
"""创建工作模式子菜单"""
|
||||
self.work_mode_menu = QMenu("工作模式", self.main_window)
|
||||
self.work_mode_menu.setFont(menu_font)
|
||||
self.work_mode_menu.setStyleSheet(menu_style)
|
||||
|
||||
# 获取当前离线模式状态
|
||||
is_offline_mode = False
|
||||
if hasattr(self.main_window, 'offline_mode_manager'):
|
||||
is_offline_mode = self.main_window.offline_mode_manager.is_in_offline_mode()
|
||||
|
||||
# 创建在线模式和离线模式选项
|
||||
self.online_mode_action = QAction("在线模式", self.main_window, checkable=True)
|
||||
self.online_mode_action.setFont(menu_font)
|
||||
self.online_mode_action.setChecked(not is_offline_mode)
|
||||
|
||||
self.offline_mode_action = QAction("离线模式", self.main_window, checkable=True)
|
||||
self.offline_mode_action.setFont(menu_font)
|
||||
self.offline_mode_action.setChecked(is_offline_mode)
|
||||
|
||||
# 将两个模式选项添加到同一个互斥组
|
||||
mode_group = QActionGroup(self.main_window)
|
||||
mode_group.addAction(self.online_mode_action)
|
||||
mode_group.addAction(self.offline_mode_action)
|
||||
mode_group.setExclusive(True)
|
||||
|
||||
# 连接切换事件(这里需要在ui_manager中处理)
|
||||
self.online_mode_action.triggered.connect(lambda: self._handle_mode_switch("online"))
|
||||
self.offline_mode_action.triggered.connect(lambda: self._handle_mode_switch("offline"))
|
||||
|
||||
# 添加到工作模式子菜单
|
||||
self.work_mode_menu.addAction(self.online_mode_action)
|
||||
self.work_mode_menu.addAction(self.offline_mode_action)
|
||||
|
||||
def _create_download_settings_menu(self, menu_font, menu_style):
|
||||
"""创建下载设置子菜单"""
|
||||
self.download_settings_menu = QMenu("下载设置", self.main_window)
|
||||
self.download_settings_menu.setFont(menu_font)
|
||||
self.download_settings_menu.setStyleSheet(menu_style)
|
||||
|
||||
# "修改下载源"按钮
|
||||
switch_source_action = QAction("修改下载源", self.main_window)
|
||||
switch_source_action.setFont(menu_font)
|
||||
switch_source_action.setEnabled(True)
|
||||
switch_source_action.triggered.connect(
|
||||
lambda: self.dialog_factory.show_simple_message("提示", "\n该功能正在开发中,敬请期待!\n")
|
||||
)
|
||||
|
||||
# 添加下载线程设置选项
|
||||
thread_settings_action = QAction("下载线程设置", self.main_window)
|
||||
thread_settings_action.setFont(menu_font)
|
||||
thread_settings_action.triggered.connect(self._handle_download_thread_settings)
|
||||
|
||||
# 添加到下载设置子菜单
|
||||
self.download_settings_menu.addAction(switch_source_action)
|
||||
self.download_settings_menu.addAction(thread_settings_action)
|
||||
|
||||
def _create_developer_options_menu(self, menu_font, menu_style):
|
||||
"""创建开发者选项子菜单"""
|
||||
self.dev_menu = QMenu("开发者选项", self.main_window)
|
||||
self.dev_menu.setFont(menu_font)
|
||||
self.dev_menu.setStyleSheet(menu_style)
|
||||
|
||||
# 创建各个子菜单
|
||||
self._create_debug_submenu(menu_font, menu_style)
|
||||
self._create_hosts_submenu(menu_font, menu_style)
|
||||
self._create_ipv6_submenu(menu_font, menu_style)
|
||||
self._create_hash_settings_submenu(menu_font, menu_style)
|
||||
|
||||
# 添加到开发者选项菜单
|
||||
self.dev_menu.addMenu(self.debug_submenu)
|
||||
self.dev_menu.addMenu(self.hosts_submenu)
|
||||
self.dev_menu.addMenu(self.ipv6_submenu)
|
||||
self.dev_menu.addMenu(self.hash_settings_menu)
|
||||
|
||||
def _create_debug_submenu(self, menu_font, menu_style):
|
||||
"""创建Debug子菜单"""
|
||||
self.debug_submenu = QMenu("Debug模式", self.main_window)
|
||||
self.debug_submenu.setFont(menu_font)
|
||||
self.debug_submenu.setStyleSheet(menu_style)
|
||||
|
||||
# 创建Debug开关选项
|
||||
self.debug_action = QAction("Debug开关", self.main_window, checkable=True)
|
||||
self.debug_action.setFont(menu_font)
|
||||
|
||||
# 获取debug模式状态
|
||||
config = getattr(self.main_window, 'config', {})
|
||||
debug_mode = False
|
||||
if isinstance(config, dict):
|
||||
debug_mode = config.get("debug_mode", False)
|
||||
|
||||
self.debug_action.setChecked(debug_mode)
|
||||
|
||||
# 连接toggle_debug_mode方法
|
||||
if hasattr(self.main_window, 'toggle_debug_mode'):
|
||||
self.debug_action.triggered.connect(self.main_window.toggle_debug_mode)
|
||||
|
||||
# 创建打开log文件选项
|
||||
self.open_log_action = QAction("打开log.txt", self.main_window)
|
||||
self.open_log_action.setFont(menu_font)
|
||||
self.open_log_action.setEnabled(debug_mode)
|
||||
|
||||
# 连接打开log文件的事件
|
||||
if hasattr(self.main_window, 'debug_manager'):
|
||||
self.open_log_action.triggered.connect(self.main_window.debug_manager.open_log_file)
|
||||
else:
|
||||
self.open_log_action.triggered.connect(
|
||||
lambda: self.dialog_factory.show_simple_message("错误", "\n调试管理器未初始化。\n", "error")
|
||||
)
|
||||
|
||||
# 添加到Debug子菜单
|
||||
self.debug_submenu.addAction(self.debug_action)
|
||||
self.debug_submenu.addAction(self.open_log_action)
|
||||
|
||||
def _create_hosts_submenu(self, menu_font, menu_style):
|
||||
"""创建hosts文件选项子菜单"""
|
||||
self.hosts_submenu = QMenu("hosts文件选项", self.main_window)
|
||||
self.hosts_submenu.setFont(menu_font)
|
||||
self.hosts_submenu.setStyleSheet(menu_style)
|
||||
|
||||
# 添加hosts子选项
|
||||
restore_hosts_action = QAction("还原软件备份的hosts文件", self.main_window)
|
||||
restore_hosts_action.setFont(menu_font)
|
||||
restore_hosts_action.triggered.connect(self._handle_restore_hosts_backup)
|
||||
|
||||
clean_hosts_action = QAction("手动删除软件添加的hosts条目", self.main_window)
|
||||
clean_hosts_action.setFont(menu_font)
|
||||
clean_hosts_action.triggered.connect(self._handle_clean_hosts_entries)
|
||||
|
||||
# 添加禁用自动还原hosts的选项
|
||||
self.disable_auto_restore_action = QAction("禁用关闭/重启自动还原hosts", self.main_window, checkable=True)
|
||||
self.disable_auto_restore_action.setFont(menu_font)
|
||||
|
||||
# 从配置中读取当前状态
|
||||
config = getattr(self.main_window, 'config', {})
|
||||
disable_auto_restore = False
|
||||
if isinstance(config, dict):
|
||||
disable_auto_restore = config.get("disable_auto_restore_hosts", False)
|
||||
|
||||
self.disable_auto_restore_action.setChecked(disable_auto_restore)
|
||||
self.disable_auto_restore_action.triggered.connect(self._handle_toggle_disable_auto_restore_hosts)
|
||||
|
||||
# 添加打开hosts文件选项
|
||||
open_hosts_action = QAction("打开hosts文件", self.main_window)
|
||||
open_hosts_action.setFont(menu_font)
|
||||
open_hosts_action.triggered.connect(self._handle_open_hosts_file)
|
||||
|
||||
# 添加到hosts子菜单
|
||||
self.hosts_submenu.addAction(self.disable_auto_restore_action)
|
||||
self.hosts_submenu.addAction(restore_hosts_action)
|
||||
self.hosts_submenu.addAction(clean_hosts_action)
|
||||
self.hosts_submenu.addAction(open_hosts_action)
|
||||
|
||||
def _create_ipv6_submenu(self, menu_font, menu_style):
|
||||
"""创建IPv6支持子菜单"""
|
||||
self.ipv6_submenu = QMenu("IPv6支持", self.main_window)
|
||||
self.ipv6_submenu.setFont(menu_font)
|
||||
self.ipv6_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)
|
||||
|
||||
# 获取IPv6Manager实例
|
||||
ipv6_manager = getattr(self.main_window, 'ipv6_manager', None)
|
||||
if ipv6_manager:
|
||||
self.ipv6_test_action.triggered.connect(ipv6_manager.show_ipv6_details)
|
||||
else:
|
||||
self.ipv6_test_action.triggered.connect(
|
||||
lambda: self.dialog_factory.show_simple_message("错误", "\nIPv6管理器尚未初始化,请稍后再试。\n", "error")
|
||||
)
|
||||
|
||||
# 检查配置中是否已启用IPv6
|
||||
config = getattr(self.main_window, 'config', {})
|
||||
ipv6_enabled = False
|
||||
if isinstance(config, dict):
|
||||
ipv6_enabled = config.get("ipv6_enabled", False)
|
||||
|
||||
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)
|
||||
|
||||
def _create_hash_settings_submenu(self, menu_font, menu_style):
|
||||
"""创建哈希校验设置子菜单"""
|
||||
self.hash_settings_menu = QMenu("哈希校验设置", self.main_window)
|
||||
self.hash_settings_menu.setFont(menu_font)
|
||||
self.hash_settings_menu.setStyleSheet(menu_style)
|
||||
|
||||
# 添加禁用安装前哈希预检查选项
|
||||
self.disable_pre_hash_action = QAction("禁用安装前哈希预检查", self.main_window, checkable=True)
|
||||
self.disable_pre_hash_action.setFont(menu_font)
|
||||
|
||||
# 从配置中读取当前状态
|
||||
config = getattr(self.main_window, 'config', {})
|
||||
disable_pre_hash = False
|
||||
if isinstance(config, dict):
|
||||
disable_pre_hash = config.get("disable_pre_hash_check", False)
|
||||
|
||||
self.disable_pre_hash_action.setChecked(disable_pre_hash)
|
||||
self.disable_pre_hash_action.triggered.connect(lambda checked: self._handle_pre_hash_toggle(checked))
|
||||
|
||||
# 添加到哈希校验设置子菜单
|
||||
self.hash_settings_menu.addAction(self.disable_pre_hash_action)
|
||||
|
||||
def show_menu(self, menu, button):
|
||||
"""显示菜单
|
||||
|
||||
Args:
|
||||
menu: 要显示的菜单
|
||||
button: 触发菜单的按钮
|
||||
"""
|
||||
# 检查Ui_install中是否定义了show_menu方法
|
||||
if hasattr(self.ui, 'show_menu'):
|
||||
self.ui.show_menu(menu, button)
|
||||
else:
|
||||
# 否则,使用默认的弹出方法
|
||||
global_pos = button.mapToGlobal(button.rect().bottomLeft())
|
||||
menu.popup(global_pos)
|
||||
|
||||
# 以下方法需要委托给ui_manager处理
|
||||
def _handle_mode_switch(self, mode):
|
||||
"""处理工作模式切换"""
|
||||
# 这个方法需要在ui_manager中实现
|
||||
if hasattr(self.main_window, 'ui_manager') and hasattr(self.main_window.ui_manager, 'switch_work_mode'):
|
||||
self.main_window.ui_manager.switch_work_mode(mode)
|
||||
else:
|
||||
self.dialog_factory.show_simple_message("错误", "\n工作模式切换功能不可用。\n", "error")
|
||||
|
||||
def _handle_download_thread_settings(self):
|
||||
"""处理下载线程设置"""
|
||||
if hasattr(self.main_window, 'download_manager'):
|
||||
self.main_window.download_manager.show_download_thread_settings()
|
||||
else:
|
||||
self.dialog_factory.show_simple_message("错误", "\n下载管理器未初始化,无法修改下载线程设置。\n", "error")
|
||||
|
||||
def _handle_ipv6_toggle(self, enabled):
|
||||
"""处理IPv6支持切换"""
|
||||
if hasattr(self.main_window, 'ui_manager') and hasattr(self.main_window.ui_manager, '_handle_ipv6_toggle'):
|
||||
self.main_window.ui_manager._handle_ipv6_toggle(enabled)
|
||||
else:
|
||||
self.dialog_factory.show_simple_message("错误", "\nIPv6管理功能不可用。\n", "error")
|
||||
|
||||
def _handle_pre_hash_toggle(self, checked):
|
||||
"""处理禁用安装前哈希预检查的切换"""
|
||||
if hasattr(self.main_window, 'ui_manager') and hasattr(self.main_window.ui_manager, '_handle_pre_hash_toggle'):
|
||||
self.main_window.ui_manager._handle_pre_hash_toggle(checked)
|
||||
else:
|
||||
self.dialog_factory.show_simple_message("错误", "\n哈希检查设置功能不可用。\n", "error")
|
||||
|
||||
def _handle_restore_hosts_backup(self):
|
||||
"""处理还原hosts备份"""
|
||||
if hasattr(self.main_window, 'ui_manager') and hasattr(self.main_window.ui_manager, 'restore_hosts_backup'):
|
||||
self.main_window.ui_manager.restore_hosts_backup()
|
||||
else:
|
||||
self.dialog_factory.show_simple_message("错误", "\nhosts管理功能不可用。\n", "error")
|
||||
|
||||
def _handle_clean_hosts_entries(self):
|
||||
"""处理清理hosts条目"""
|
||||
if hasattr(self.main_window, 'ui_manager') and hasattr(self.main_window.ui_manager, 'clean_hosts_entries'):
|
||||
self.main_window.ui_manager.clean_hosts_entries()
|
||||
else:
|
||||
self.dialog_factory.show_simple_message("错误", "\nhosts管理功能不可用。\n", "error")
|
||||
|
||||
def _handle_toggle_disable_auto_restore_hosts(self, checked):
|
||||
"""处理切换禁用自动还原hosts"""
|
||||
if hasattr(self.main_window, 'ui_manager') and hasattr(self.main_window.ui_manager, 'toggle_disable_auto_restore_hosts'):
|
||||
self.main_window.ui_manager.toggle_disable_auto_restore_hosts(checked)
|
||||
else:
|
||||
self.dialog_factory.show_simple_message("错误", "\nhosts管理功能不可用。\n", "error")
|
||||
|
||||
def _handle_open_hosts_file(self):
|
||||
"""处理打开hosts文件"""
|
||||
if hasattr(self.main_window, 'ui_manager') and hasattr(self.main_window.ui_manager, 'open_hosts_file'):
|
||||
self.main_window.ui_manager.open_hosts_file()
|
||||
else:
|
||||
self.dialog_factory.show_simple_message("错误", "\nhosts管理功能不可用。\n", "error")
|
||||
@@ -1,7 +1,8 @@
|
||||
from .logger import Logger
|
||||
from .url_censor import censor_url
|
||||
from .helpers import (
|
||||
load_base64_image, HashManager, AdminPrivileges, msgbox_frame,
|
||||
load_config, save_config, HostsManager, censor_url, resource_path,
|
||||
load_config, save_config, HostsManager, resource_path,
|
||||
load_image_from_file
|
||||
)
|
||||
|
||||
|
||||
@@ -9,27 +9,140 @@ import psutil
|
||||
from PySide6 import QtCore, QtWidgets
|
||||
import re
|
||||
from PySide6.QtGui import QIcon, QPixmap
|
||||
from data.config import APP_NAME, CONFIG_FILE
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QProgressBar
|
||||
from config.config import APP_NAME, CONFIG_FILE
|
||||
from utils.logger import setup_logger
|
||||
import datetime
|
||||
import traceback
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
# 初始化logger
|
||||
logger = setup_logger("helpers")
|
||||
|
||||
class ProgressHashVerifyDialog(QDialog):
|
||||
"""带进度条的哈希验证对话框"""
|
||||
|
||||
def __init__(self, title, message, parent=None):
|
||||
"""初始化对话框
|
||||
|
||||
Args:
|
||||
title: 对话框标题
|
||||
message: 对话框消息
|
||||
parent: 父窗口
|
||||
"""
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle(title)
|
||||
self.setMinimumWidth(400)
|
||||
self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint)
|
||||
|
||||
# 创建布局
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
# 添加消息标签
|
||||
self.message_label = QLabel(message)
|
||||
self.message_label.setAlignment(Qt.AlignCenter)
|
||||
layout.addWidget(self.message_label)
|
||||
|
||||
# 添加进度条
|
||||
self.progress_bar = QProgressBar()
|
||||
self.progress_bar.setRange(0, 100)
|
||||
self.progress_bar.setValue(0)
|
||||
layout.addWidget(self.progress_bar)
|
||||
|
||||
# 添加状态标签
|
||||
self.status_label = QLabel("正在准备...")
|
||||
self.status_label.setAlignment(Qt.AlignCenter)
|
||||
layout.addWidget(self.status_label)
|
||||
|
||||
# 添加取消按钮
|
||||
button_layout = QHBoxLayout()
|
||||
self.cancel_button = QPushButton("取消")
|
||||
self.cancel_button.clicked.connect(self.reject)
|
||||
button_layout.addStretch()
|
||||
button_layout.addWidget(self.cancel_button)
|
||||
layout.addLayout(button_layout)
|
||||
|
||||
def update_progress(self, value):
|
||||
"""更新进度条
|
||||
|
||||
Args:
|
||||
value: 进度值 (0-100)
|
||||
"""
|
||||
self.progress_bar.setValue(value)
|
||||
|
||||
# 更新状态文本
|
||||
if value < 10:
|
||||
self.status_label.setText("正在准备...")
|
||||
elif value < 50:
|
||||
self.status_label.setText("正在解压文件...")
|
||||
elif value < 70:
|
||||
self.status_label.setText("正在查找补丁文件...")
|
||||
elif value < 95:
|
||||
self.status_label.setText("正在计算哈希值...")
|
||||
else:
|
||||
self.status_label.setText("正在验证哈希值...")
|
||||
|
||||
def set_message(self, message):
|
||||
"""设置消息文本
|
||||
|
||||
Args:
|
||||
message: 消息文本
|
||||
"""
|
||||
self.message_label.setText(message)
|
||||
|
||||
def set_status(self, status):
|
||||
"""设置状态文本
|
||||
|
||||
Args:
|
||||
status: 状态文本
|
||||
"""
|
||||
self.status_label.setText(status)
|
||||
|
||||
def resource_path(relative_path):
|
||||
"""获取资源的绝对路径,适用于开发环境和Nuitka打包环境"""
|
||||
if getattr(sys, 'frozen', False):
|
||||
# Nuitka/PyInstaller创建的临时文件夹,并将路径存储在_MEIPASS中或与可执行文件同目录
|
||||
if hasattr(sys, '_MEIPASS'):
|
||||
base_path = sys._MEIPASS
|
||||
"""获取资源的绝对路径,适用于开发环境和打包环境"""
|
||||
try:
|
||||
if getattr(sys, 'frozen', False):
|
||||
# 打包环境 - 可执行文件所在目录
|
||||
if hasattr(sys, '_MEIPASS'):
|
||||
# PyInstaller打包的临时目录
|
||||
base_path = sys._MEIPASS
|
||||
else:
|
||||
# 其他打包方式,直接使用可执行文件目录
|
||||
base_path = os.path.dirname(sys.executable)
|
||||
|
||||
# 对于离线补丁文件,需要在可执行文件所在目录查找
|
||||
if relative_path.lower() in ["vol.1.7z", "vol.2.7z", "vol.3.7z", "vol.4.7z", "after.7z"]:
|
||||
exe_dir = os.path.dirname(sys.executable)
|
||||
patch_path = os.path.join(exe_dir, relative_path)
|
||||
if os.path.exists(patch_path):
|
||||
logger.debug(f"找到离线补丁文件: {patch_path}")
|
||||
return patch_path
|
||||
else:
|
||||
base_path = os.path.dirname(sys.executable)
|
||||
else:
|
||||
# 在开发环境中运行
|
||||
base_path = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||
# 在开发环境中运行
|
||||
base_path = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||
|
||||
# 处理特殊的可执行文件和数据文件路径
|
||||
if relative_path in ("aria2c-fast_x64.exe", "cfst.exe"):
|
||||
return os.path.join(base_path, 'bin', relative_path)
|
||||
result_path = 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)
|
||||
result_path = os.path.join(base_path, 'data', relative_path)
|
||||
else:
|
||||
# 标准资源路径
|
||||
result_path = os.path.join(base_path, relative_path)
|
||||
|
||||
# 记录资源路径并验证是否存在
|
||||
if not os.path.exists(result_path) and relative_path: # 只在非空路径时检查
|
||||
logger.warning(f"资源文件不存在: {result_path}")
|
||||
elif relative_path: # 避免记录空路径
|
||||
logger.debug(f"已找到资源文件: {result_path}")
|
||||
|
||||
return os.path.join(base_path, relative_path)
|
||||
return result_path
|
||||
except Exception as e:
|
||||
logger.error(f"资源路径解析错误 ({relative_path}): {e}")
|
||||
# 出错时仍返回一个基本路径
|
||||
return os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", relative_path)
|
||||
|
||||
def load_base64_image(base64_str):
|
||||
pixmap = QPixmap()
|
||||
@@ -45,9 +158,57 @@ def load_image_from_file(file_path):
|
||||
Returns:
|
||||
QPixmap: 加载的图像
|
||||
"""
|
||||
if os.path.exists(file_path):
|
||||
return QPixmap(file_path)
|
||||
return QPixmap()
|
||||
try:
|
||||
if os.path.exists(file_path):
|
||||
logger.info(f"加载图片: {file_path}")
|
||||
pixmap = QPixmap(file_path)
|
||||
|
||||
if pixmap.isNull():
|
||||
logger.error(f"图片加载失败(pixmap为空): {file_path}")
|
||||
|
||||
# 检查文件大小和是否可读
|
||||
try:
|
||||
file_size = os.path.getsize(file_path)
|
||||
logger.debug(f"图片文件大小: {file_size} 字节")
|
||||
if file_size == 0:
|
||||
logger.error(f"图片文件大小为0字节: {file_path}")
|
||||
|
||||
# 尝试打开文件测试可读性
|
||||
with open(file_path, 'rb') as f:
|
||||
# 只读取前几个字节测试可访问性
|
||||
f.read(10)
|
||||
logger.debug(f"图片文件可以正常打开和读取")
|
||||
|
||||
# 检查文件扩展名是否正确
|
||||
ext = os.path.splitext(file_path)[1].lower()
|
||||
if ext not in ['.png', '.jpg', '.jpeg', '.bmp', '.ico']:
|
||||
logger.warning(f"图片文件扩展名可能不受支持: {ext}")
|
||||
|
||||
except Exception as file_error:
|
||||
logger.error(f"图片文件访问错误: {file_error}")
|
||||
|
||||
return QPixmap()
|
||||
else:
|
||||
logger.debug(f"图片加载成功: {file_path}, 大小: {pixmap.width()}x{pixmap.height()}")
|
||||
return pixmap
|
||||
else:
|
||||
logger.warning(f"图片文件不存在: {file_path}")
|
||||
# 尝试列出父目录下的文件
|
||||
try:
|
||||
parent_dir = os.path.dirname(file_path)
|
||||
if os.path.exists(parent_dir):
|
||||
files = os.listdir(parent_dir)
|
||||
logger.debug(f"目录 {parent_dir} 中的文件: {files}")
|
||||
else:
|
||||
logger.debug(f"目录不存在: {parent_dir}")
|
||||
except Exception as dir_error:
|
||||
logger.error(f"无法列出目录内容: {dir_error}")
|
||||
|
||||
return QPixmap()
|
||||
except Exception as e:
|
||||
logger.error(f"加载图片时发生异常: {e}")
|
||||
logger.error(f"异常详情: {traceback.format_exc()}")
|
||||
return QPixmap()
|
||||
|
||||
def msgbox_frame(title, text, buttons=QtWidgets.QMessageBox.StandardButton.NoButton):
|
||||
msg_box = QtWidgets.QMessageBox()
|
||||
@@ -55,7 +216,7 @@ def msgbox_frame(title, text, buttons=QtWidgets.QMessageBox.StandardButton.NoBut
|
||||
msg_box.setWindowModality(QtCore.Qt.WindowModality.WindowModal)
|
||||
|
||||
# 直接加载图标文件
|
||||
icon_path = resource_path(os.path.join("IMG", "ICO", "icon.png"))
|
||||
icon_path = resource_path(os.path.join("assets", "images", "ICO", "icon.png"))
|
||||
if os.path.exists(icon_path):
|
||||
pixmap = QPixmap(icon_path)
|
||||
if not pixmap.isNull():
|
||||
@@ -83,7 +244,7 @@ def save_config(config):
|
||||
with open(CONFIG_FILE, "w", encoding="utf-8") as f:
|
||||
json.dump(config, f, indent=4)
|
||||
except IOError as e:
|
||||
print(f"Error saving config: {e}")
|
||||
logger.error(f"Error saving config: {e}")
|
||||
|
||||
|
||||
class HashManager:
|
||||
@@ -109,77 +270,168 @@ class HashManager:
|
||||
results[file_path] = future.result()
|
||||
except Exception as e:
|
||||
results[file_path] = None # Mark as failed
|
||||
print(f"Error calculating hash for {file_path}: {e}")
|
||||
logger.error(f"Error calculating hash for {file_path}: {e}")
|
||||
return results
|
||||
|
||||
def hash_pop_window(self, check_type="default"):
|
||||
def hash_pop_window(self, check_type="default", is_offline=False, auto_close=False, close_delay=500):
|
||||
"""显示文件检验窗口
|
||||
|
||||
Args:
|
||||
check_type: 检查类型,可以是 'pre'(预检查), 'after'(后检查), 'extraction'(解压后检查)
|
||||
|
||||
check_type: 检查类型,可以是 'pre'(预检查), 'after'(后检查), 'extraction'(解压后检查), 'offline_extraction'(离线解压), 'offline_verify'(离线验证)
|
||||
is_offline: 是否处于离线模式
|
||||
auto_close: 是否自动关闭窗口
|
||||
close_delay: 自动关闭延迟(毫秒)
|
||||
|
||||
Returns:
|
||||
QMessageBox: 消息框实例
|
||||
"""
|
||||
message = "\n正在检验文件状态...\n"
|
||||
|
||||
if check_type == "pre":
|
||||
message = "\n正在检查游戏文件以确定需要安装的补丁...\n"
|
||||
elif check_type == "after":
|
||||
message = "\n正在检验本地文件完整性...\n"
|
||||
elif check_type == "extraction":
|
||||
message = "\n正在验证下载的解压文件完整性...\n"
|
||||
if is_offline:
|
||||
# 离线模式的消息
|
||||
if check_type == "pre":
|
||||
message = "\n正在检查游戏文件以确定需要安装的补丁...\n"
|
||||
elif check_type == "after":
|
||||
message = "\n正在检验本地文件完整性...\n"
|
||||
elif check_type == "offline_verify":
|
||||
message = "\n正在验证本地补丁压缩文件完整性...\n"
|
||||
elif check_type == "offline_extraction":
|
||||
message = "\n正在解压安装补丁文件...\n"
|
||||
elif check_type == "offline_installation":
|
||||
message = "\n正在安装补丁文件...\n"
|
||||
else:
|
||||
message = "\n正在处理离线补丁文件...\n"
|
||||
else:
|
||||
# 在线模式的消息
|
||||
if check_type == "pre":
|
||||
message = "\n正在检查游戏文件以确定需要安装的补丁...\n"
|
||||
elif check_type == "after":
|
||||
message = "\n正在检验本地文件完整性...\n"
|
||||
elif check_type == "extraction":
|
||||
message = "\n正在验证下载的解压文件完整性...\n"
|
||||
elif check_type == "post":
|
||||
message = "\n正在检验补丁文件完整性...\n"
|
||||
|
||||
# 创建新的消息框
|
||||
msg_box = msgbox_frame(f"通知 - {APP_NAME}", message)
|
||||
|
||||
# 使用open()而不是exec(),避免阻塞UI线程
|
||||
msg_box.open()
|
||||
|
||||
# 处理事件循环,确保窗口显示
|
||||
QtWidgets.QApplication.processEvents()
|
||||
|
||||
# 如果设置了自动关闭,添加定时器
|
||||
if auto_close:
|
||||
timer = QtCore.QTimer()
|
||||
timer.setSingleShot(True)
|
||||
timer.timeout.connect(msg_box.close)
|
||||
timer.start(close_delay)
|
||||
# 保存定时器引用,防止被垃圾回收
|
||||
msg_box.close_timer = timer
|
||||
|
||||
return msg_box
|
||||
|
||||
def cfg_pre_hash_compare(self, install_paths, plugin_hash, installed_status):
|
||||
status_copy = installed_status.copy()
|
||||
debug_mode = False
|
||||
|
||||
# 尝试检测是否处于调试模式
|
||||
try:
|
||||
from config.config import CACHE
|
||||
debug_file = os.path.join(os.path.dirname(CACHE), "debug_mode.txt")
|
||||
debug_mode = os.path.exists(debug_file)
|
||||
except:
|
||||
pass
|
||||
|
||||
for game_version, install_path in install_paths.items():
|
||||
if not os.path.exists(install_path):
|
||||
status_copy[game_version] = False
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 哈希预检查 - {game_version} 补丁文件不存在: {install_path}")
|
||||
continue
|
||||
|
||||
try:
|
||||
expected_hash = plugin_hash.get(game_version, "")
|
||||
if not expected_hash:
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 哈希预检查 - {game_version} 没有预期哈希值,跳过哈希检查")
|
||||
# 当没有预期哈希值时,保持当前状态不变
|
||||
continue
|
||||
|
||||
file_hash = self.hash_calculate(install_path)
|
||||
if file_hash == plugin_hash.get(game_version):
|
||||
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 哈希预检查 - {game_version}")
|
||||
logger.debug(f"DEBUG: 文件路径: {install_path}")
|
||||
logger.debug(f"DEBUG: 预期哈希值: {expected_hash}")
|
||||
logger.debug(f"DEBUG: 实际哈希值: {file_hash}")
|
||||
logger.debug(f"DEBUG: 哈希匹配: {file_hash == expected_hash}")
|
||||
|
||||
if file_hash == expected_hash:
|
||||
status_copy[game_version] = True
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 哈希预检查 - {game_version} 哈希匹配成功")
|
||||
else:
|
||||
status_copy[game_version] = False
|
||||
except Exception:
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 哈希预检查 - {game_version} 哈希不匹配")
|
||||
except Exception as e:
|
||||
status_copy[game_version] = False
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 哈希预检查异常 - {game_version}: {str(e)}")
|
||||
|
||||
return status_copy
|
||||
|
||||
def cfg_after_hash_compare(self, install_paths, plugin_hash, installed_status):
|
||||
debug_mode = False
|
||||
|
||||
# 尝试检测是否处于调试模式
|
||||
try:
|
||||
from config.config import CACHE
|
||||
debug_file = os.path.join(os.path.dirname(CACHE), "debug_mode.txt")
|
||||
debug_mode = os.path.exists(debug_file)
|
||||
except:
|
||||
pass
|
||||
|
||||
file_paths = [
|
||||
install_paths[game] for game in plugin_hash if installed_status.get(game)
|
||||
]
|
||||
hash_results = self.calculate_hashes_in_parallel(file_paths)
|
||||
|
||||
for game, hash_value in plugin_hash.items():
|
||||
for game, expected_hash in plugin_hash.items():
|
||||
if installed_status.get(game):
|
||||
file_path = install_paths[game]
|
||||
file_hash = hash_results.get(file_path)
|
||||
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 哈希后检查 - {game}")
|
||||
logger.debug(f"DEBUG: 文件路径: {file_path}")
|
||||
logger.debug(f"DEBUG: 预期哈希值: {expected_hash}")
|
||||
logger.debug(f"DEBUG: 实际哈希值: {file_hash if file_hash else '计算失败'}")
|
||||
|
||||
if file_hash is None:
|
||||
installed_status[game] = False
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 哈希后检查失败 - 无法计算文件哈希值: {game}")
|
||||
return {
|
||||
"passed": False,
|
||||
"game": game,
|
||||
"message": f"\n无法计算 {game} 的文件哈希值,文件可能已损坏或被占用。\n"
|
||||
}
|
||||
|
||||
if file_hash != hash_value:
|
||||
if file_hash != expected_hash:
|
||||
installed_status[game] = False
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 哈希后检查失败 - 哈希值不匹配: {game}")
|
||||
return {
|
||||
"passed": False,
|
||||
"game": game,
|
||||
"message": f"\n检测到 {game} 的文件哈希值不匹配。\n"
|
||||
}
|
||||
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 哈希后检查通过 - 所有文件哈希值匹配")
|
||||
return {"passed": True}
|
||||
|
||||
class AdminPrivileges:
|
||||
@@ -206,65 +458,98 @@ class AdminPrivileges:
|
||||
"\n需要管理员权限运行此程序\n",
|
||||
QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No,
|
||||
)
|
||||
reply = msg_box.exec()
|
||||
if reply == QtWidgets.QMessageBox.StandardButton.Yes:
|
||||
try:
|
||||
ctypes.windll.shell32.ShellExecuteW(
|
||||
None, "runas", sys.executable, " ".join(sys.argv), None, 1
|
||||
)
|
||||
except Exception as e:
|
||||
try:
|
||||
reply = msg_box.exec()
|
||||
if reply == QtWidgets.QMessageBox.StandardButton.Yes:
|
||||
try:
|
||||
ctypes.windll.shell32.ShellExecuteW(
|
||||
None, "runas", sys.executable, " ".join(sys.argv), None, 1
|
||||
)
|
||||
except Exception as e:
|
||||
msg_box = msgbox_frame(
|
||||
f"错误 - {APP_NAME}",
|
||||
f"\n请求管理员权限失败\n\n【错误信息】:{e}\n",
|
||||
QtWidgets.QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
msg_box.exec()
|
||||
sys.exit(1)
|
||||
else:
|
||||
msg_box = msgbox_frame(
|
||||
f"错误 - {APP_NAME}",
|
||||
f"\n请求管理员权限失败\n\n【错误信息】:{e}\n",
|
||||
f"权限检测 - {APP_NAME}",
|
||||
"\n无法获取管理员权限,程序将退出\n",
|
||||
QtWidgets.QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
)
|
||||
msg_box.exec()
|
||||
sys.exit(1)
|
||||
else:
|
||||
sys.exit(1)
|
||||
except KeyboardInterrupt:
|
||||
logger.warning("管理员权限请求被用户中断")
|
||||
msg_box = msgbox_frame(
|
||||
f"权限检测 - {APP_NAME}",
|
||||
"\n无法获取管理员权限,程序将退出\n",
|
||||
"\n操作被中断,程序将退出\n",
|
||||
QtWidgets.QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
)
|
||||
msg_box.exec()
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
logger.error(f"管理员权限请求时发生错误: {e}")
|
||||
msg_box = msgbox_frame(
|
||||
f"错误 - {APP_NAME}",
|
||||
f"\n请求管理员权限时发生未知错误\n\n【错误信息】:{e}\n",
|
||||
QtWidgets.QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
msg_box.exec()
|
||||
sys.exit(1)
|
||||
|
||||
def check_and_terminate_processes(self):
|
||||
for proc in psutil.process_iter(["pid", "name"]):
|
||||
proc_name = proc.info["name"].lower() if proc.info["name"] else ""
|
||||
|
||||
# 检查进程名是否匹配任何需要终止的游戏进程
|
||||
for exe in self.required_exes:
|
||||
if exe.lower() == proc_name:
|
||||
# 获取不带.nocrack的游戏名称用于显示
|
||||
display_name = exe.replace(".nocrack", "")
|
||||
|
||||
msg_box = msgbox_frame(
|
||||
f"进程检测 - {APP_NAME}",
|
||||
f"\n检测到游戏正在运行: {display_name} \n\n是否终止?\n",
|
||||
QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No,
|
||||
)
|
||||
reply = msg_box.exec()
|
||||
if reply == QtWidgets.QMessageBox.StandardButton.Yes:
|
||||
try:
|
||||
proc.terminate()
|
||||
proc.wait(timeout=3)
|
||||
except psutil.AccessDenied:
|
||||
msg_box = msgbox_frame(
|
||||
f"错误 - {APP_NAME}",
|
||||
f"\n无法关闭游戏: {display_name} \n\n请手动关闭后重启应用\n",
|
||||
QtWidgets.QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
msg_box.exec()
|
||||
sys.exit(1)
|
||||
else:
|
||||
try:
|
||||
for proc in psutil.process_iter(["pid", "name"]):
|
||||
proc_name = proc.info["name"].lower() if proc.info["name"] else ""
|
||||
|
||||
# 检查进程名是否匹配任何需要终止的游戏进程
|
||||
for exe in self.required_exes:
|
||||
if exe.lower() == proc_name:
|
||||
# 获取不带.nocrack的游戏名称用于显示
|
||||
display_name = exe.replace(".nocrack", "")
|
||||
|
||||
msg_box = msgbox_frame(
|
||||
f"进程检测 - {APP_NAME}",
|
||||
f"\n未关闭的游戏: {display_name} \n\n请手动关闭后重启应用\n",
|
||||
QtWidgets.QMessageBox.StandardButton.Ok,
|
||||
f"\n检测到游戏正在运行: {display_name} \n\n是否终止?\n",
|
||||
QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No,
|
||||
)
|
||||
msg_box.exec()
|
||||
sys.exit(1)
|
||||
try:
|
||||
reply = msg_box.exec()
|
||||
if reply == QtWidgets.QMessageBox.StandardButton.Yes:
|
||||
try:
|
||||
proc.terminate()
|
||||
proc.wait(timeout=3)
|
||||
except psutil.AccessDenied:
|
||||
msg_box = msgbox_frame(
|
||||
f"错误 - {APP_NAME}",
|
||||
f"\n无法关闭游戏: {display_name} \n\n请手动关闭后重启应用\n",
|
||||
QtWidgets.QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
msg_box.exec()
|
||||
sys.exit(1)
|
||||
else:
|
||||
msg_box = msgbox_frame(
|
||||
f"进程检测 - {APP_NAME}",
|
||||
f"\n未关闭的游戏: {display_name} \n\n请手动关闭后重启应用\n",
|
||||
QtWidgets.QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
msg_box.exec()
|
||||
sys.exit(1)
|
||||
except KeyboardInterrupt:
|
||||
logger.warning(f"进程 {display_name} 终止操作被用户中断")
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"进程 {display_name} 终止操作时发生错误: {e}")
|
||||
raise
|
||||
except KeyboardInterrupt:
|
||||
logger.warning("进程检查被用户中断")
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"进程检查时发生错误: {e}")
|
||||
raise
|
||||
|
||||
class HostsManager:
|
||||
def __init__(self):
|
||||
@@ -273,20 +558,66 @@ class HostsManager:
|
||||
self.original_content = None
|
||||
self.modified = False
|
||||
self.modified_hostnames = set() # 跟踪被修改的主机名
|
||||
self.auto_restore_disabled = False # 是否禁用自动还原hosts
|
||||
|
||||
def get_hostname_entries(self, hostname):
|
||||
"""获取hosts文件中指定域名的所有IP记录
|
||||
|
||||
Args:
|
||||
hostname: 要查询的域名
|
||||
|
||||
Returns:
|
||||
list: 域名对应的IP地址列表,如果未找到则返回空列表
|
||||
"""
|
||||
try:
|
||||
# 如果original_content为空,先读取hosts文件
|
||||
if not self.original_content:
|
||||
try:
|
||||
with open(self.hosts_path, 'r', encoding='utf-8') as f:
|
||||
self.original_content = f.read()
|
||||
except Exception as e:
|
||||
logger.error(f"读取hosts文件失败: {e}")
|
||||
return []
|
||||
|
||||
# 解析hosts文件中的每一行
|
||||
ip_addresses = []
|
||||
lines = self.original_content.splitlines()
|
||||
|
||||
for line in lines:
|
||||
# 跳过注释和空行
|
||||
line = line.strip()
|
||||
if not line or line.startswith('#'):
|
||||
continue
|
||||
|
||||
# 分割行内容获取IP和域名
|
||||
parts = line.split()
|
||||
if len(parts) >= 2: # 至少包含IP和一个域名
|
||||
ip = parts[0]
|
||||
domains = parts[1:]
|
||||
|
||||
# 如果当前行包含目标域名
|
||||
if hostname in domains:
|
||||
ip_addresses.append(ip)
|
||||
|
||||
return ip_addresses
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取hosts记录失败: {e}")
|
||||
return []
|
||||
|
||||
def backup(self):
|
||||
if not AdminPrivileges().is_admin():
|
||||
print("需要管理员权限来备份hosts文件。")
|
||||
logger.warning("需要管理员权限来备份hosts文件。")
|
||||
return False
|
||||
try:
|
||||
with open(self.hosts_path, 'r', encoding='utf-8') as f:
|
||||
self.original_content = f.read()
|
||||
with open(self.backup_path, 'w', encoding='utf-8') as f:
|
||||
f.write(self.original_content)
|
||||
print(f"Hosts文件已备份到: {self.backup_path}")
|
||||
logger.debug(f"Hosts文件已备份到: {self.backup_path}")
|
||||
return True
|
||||
except IOError as e:
|
||||
print(f"备份hosts文件失败: {e}")
|
||||
logger.error(f"备份hosts文件失败: {e}")
|
||||
msg_box = msgbox_frame(f"错误 - {APP_NAME}", f"\n无法备份hosts文件,请检查权限。\n\n【错误信息】:{e}\n", QtWidgets.QMessageBox.StandardButton.Ok)
|
||||
msg_box.exec()
|
||||
return False
|
||||
@@ -306,11 +637,11 @@ class HostsManager:
|
||||
|
||||
# 确保original_content不为None
|
||||
if not self.original_content:
|
||||
print("无法读取hosts文件内容,操作中止。")
|
||||
logger.error("无法读取hosts文件内容,操作中止。")
|
||||
return False
|
||||
|
||||
if not AdminPrivileges().is_admin():
|
||||
print("需要管理员权限来修改hosts文件。")
|
||||
logger.warning("需要管理员权限来修改hosts文件。")
|
||||
return False
|
||||
|
||||
try:
|
||||
@@ -319,7 +650,7 @@ class HostsManager:
|
||||
|
||||
# 如果没有变化,不需要写入
|
||||
if len(new_lines) == len(lines):
|
||||
print(f"Hosts文件中没有找到 {hostname} 的记录")
|
||||
logger.info(f"Hosts文件中没有找到 {hostname} 的记录")
|
||||
return True
|
||||
|
||||
with open(self.hosts_path, 'w', encoding='utf-8') as f:
|
||||
@@ -327,10 +658,10 @@ class HostsManager:
|
||||
|
||||
# 更新原始内容
|
||||
self.original_content = '\n'.join(new_lines)
|
||||
print(f"已从hosts文件中清理 {hostname} 的记录")
|
||||
logger.info(f"已从hosts文件中清理 {hostname} 的记录")
|
||||
return True
|
||||
except IOError as e:
|
||||
print(f"清理hosts文件失败: {e}")
|
||||
logger.error(f"清理hosts文件失败: {e}")
|
||||
return False
|
||||
|
||||
def apply_ip(self, hostname, ip_address, clean=True):
|
||||
@@ -339,11 +670,11 @@ class HostsManager:
|
||||
return False
|
||||
|
||||
if not self.original_content: # 再次检查,确保backup成功
|
||||
print("无法读取hosts文件内容,操作中止。")
|
||||
logger.error("无法读取hosts文件内容,操作中止。")
|
||||
return False
|
||||
|
||||
if not AdminPrivileges().is_admin():
|
||||
print("需要管理员权限来修改hosts文件。")
|
||||
logger.warning("需要管理员权限来修改hosts文件。")
|
||||
return False
|
||||
|
||||
try:
|
||||
@@ -365,22 +696,71 @@ class HostsManager:
|
||||
self.modified = True
|
||||
# 记录被修改的主机名,用于最终清理
|
||||
self.modified_hostnames.add(hostname)
|
||||
print(f"Hosts文件已更新: {new_entry}")
|
||||
logger.info(f"Hosts文件已更新: {new_entry}")
|
||||
return True
|
||||
except IOError as e:
|
||||
print(f"修改hosts文件失败: {e}")
|
||||
logger.error(f"修改hosts文件失败: {e}")
|
||||
msg_box = msgbox_frame(f"错误 - {APP_NAME}", f"\n无法修改hosts文件,请检查权限。\n\n【错误信息】:{e}\n", QtWidgets.QMessageBox.StandardButton.Ok)
|
||||
msg_box.exec()
|
||||
return False
|
||||
|
||||
def check_and_clean_all_entries(self):
|
||||
def set_auto_restore_disabled(self, disabled):
|
||||
"""设置是否禁用自动还原hosts
|
||||
|
||||
Args:
|
||||
disabled: 是否禁用自动还原hosts
|
||||
|
||||
Returns:
|
||||
bool: 操作是否成功
|
||||
"""
|
||||
try:
|
||||
# 更新状态
|
||||
self.auto_restore_disabled = disabled
|
||||
|
||||
# 从配置文件读取当前配置
|
||||
from utils import load_config, save_config
|
||||
config = load_config()
|
||||
|
||||
# 更新配置
|
||||
config['disable_auto_restore_hosts'] = disabled
|
||||
|
||||
# 保存配置
|
||||
save_config(config)
|
||||
|
||||
logger.info(f"已{'禁用' if disabled else '启用'}自动还原hosts")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"设置自动还原hosts状态失败: {e}")
|
||||
return False
|
||||
|
||||
def is_auto_restore_disabled(self):
|
||||
"""检查是否禁用了自动还原hosts
|
||||
|
||||
Returns:
|
||||
bool: 是否禁用自动还原hosts
|
||||
"""
|
||||
from utils import load_config
|
||||
config = load_config()
|
||||
auto_restore_disabled = config.get('disable_auto_restore_hosts', False)
|
||||
self.auto_restore_disabled = auto_restore_disabled
|
||||
return auto_restore_disabled
|
||||
|
||||
def check_and_clean_all_entries(self, force_clean=False):
|
||||
"""检查并清理所有由本应用程序添加的hosts记录
|
||||
|
||||
Args:
|
||||
force_clean: 是否强制清理,即使禁用了自动还原
|
||||
|
||||
Returns:
|
||||
bool: 清理是否成功
|
||||
"""
|
||||
# 如果禁用了自动还原,且不是强制清理,则不执行清理操作
|
||||
if self.is_auto_restore_disabled() and not force_clean:
|
||||
logger.info("已禁用自动还原hosts,跳过清理操作")
|
||||
return True
|
||||
|
||||
if not AdminPrivileges().is_admin():
|
||||
print("需要管理员权限来检查和清理hosts文件。")
|
||||
logger.warning("需要管理员权限来检查和清理hosts文件。")
|
||||
return False
|
||||
|
||||
try:
|
||||
@@ -408,21 +788,26 @@ class HostsManager:
|
||||
|
||||
# 检查是否有变化
|
||||
if len(new_lines) == len(lines):
|
||||
print("Hosts文件中没有找到由本应用添加的记录")
|
||||
logger.info("Hosts文件中没有找到由本应用添加的记录")
|
||||
return True
|
||||
|
||||
# 写回清理后的内容
|
||||
with open(self.hosts_path, 'w', encoding='utf-8') as f:
|
||||
f.write('\n'.join(new_lines))
|
||||
|
||||
print(f"已清理所有由 {APP_NAME} 添加的hosts记录")
|
||||
logger.info(f"已清理所有由 {APP_NAME} 添加的hosts记录")
|
||||
return True
|
||||
|
||||
except IOError as e:
|
||||
print(f"检查和清理hosts文件失败: {e}")
|
||||
logger.error(f"检查和清理hosts文件失败: {e}")
|
||||
return False
|
||||
|
||||
def restore(self):
|
||||
# 如果禁用了自动还原,则不执行还原操作
|
||||
if self.is_auto_restore_disabled():
|
||||
logger.info("已禁用自动还原hosts,跳过还原操作")
|
||||
return True
|
||||
|
||||
if not self.modified:
|
||||
if os.path.exists(self.backup_path):
|
||||
try:
|
||||
@@ -434,7 +819,7 @@ class HostsManager:
|
||||
return True
|
||||
|
||||
if not AdminPrivileges().is_admin():
|
||||
print("需要管理员权限来恢复hosts文件。")
|
||||
logger.warning("需要管理员权限来恢复hosts文件。")
|
||||
return False
|
||||
|
||||
if self.original_content:
|
||||
@@ -442,7 +827,7 @@ class HostsManager:
|
||||
with open(self.hosts_path, 'w', encoding='utf-8') as f:
|
||||
f.write(self.original_content)
|
||||
self.modified = False
|
||||
print("Hosts文件已从内存恢复。")
|
||||
logger.info("Hosts文件已从内存恢复。")
|
||||
if os.path.exists(self.backup_path):
|
||||
try:
|
||||
os.remove(self.backup_path)
|
||||
@@ -451,15 +836,15 @@ class HostsManager:
|
||||
# 恢复后再检查一次是否有残留
|
||||
self.check_and_clean_all_entries()
|
||||
return True
|
||||
except IOError as e:
|
||||
print(f"从内存恢复hosts文件失败: {e}")
|
||||
except (IOError, OSError) as e:
|
||||
logger.error(f"从内存恢复hosts文件失败: {e}")
|
||||
return self.restore_from_backup_file()
|
||||
else:
|
||||
return self.restore_from_backup_file()
|
||||
|
||||
def restore_from_backup_file(self):
|
||||
if not os.path.exists(self.backup_path):
|
||||
print("未找到hosts备份文件,无法恢复。")
|
||||
logger.warning("未找到hosts备份文件,无法恢复。")
|
||||
# 即使没有备份文件,也尝试清理可能的残留
|
||||
self.check_and_clean_all_entries()
|
||||
return False
|
||||
@@ -470,21 +855,14 @@ class HostsManager:
|
||||
hf.write(backup_content)
|
||||
os.remove(self.backup_path)
|
||||
self.modified = False
|
||||
print("Hosts文件已从备份文件恢复。")
|
||||
logger.info("Hosts文件已从备份文件恢复。")
|
||||
# 恢复后再检查一次是否有残留
|
||||
self.check_and_clean_all_entries()
|
||||
return True
|
||||
except (IOError, OSError) as e:
|
||||
print(f"从备份文件恢复hosts失败: {e}")
|
||||
logger.error(f"从备份文件恢复hosts失败: {e}")
|
||||
msg_box = msgbox_frame(f"警告 - {APP_NAME}", f"\n自动恢复hosts文件失败,请手动从 {self.backup_path} 恢复。\n\n【错误信息】:{e}\n", QtWidgets.QMessageBox.StandardButton.Ok)
|
||||
msg_box.exec()
|
||||
# 尽管恢复失败,仍然尝试清理可能的残留
|
||||
self.check_and_clean_all_entries()
|
||||
return False
|
||||
|
||||
def censor_url(text):
|
||||
"""Censors URLs in a given text string."""
|
||||
if not isinstance(text, str):
|
||||
text = str(text)
|
||||
url_pattern = re.compile(r'https?://[^\s/$.?#].[^\s]*')
|
||||
return url_pattern.sub('***URL HIDDEN***', text)
|
||||
return False
|
||||
@@ -1,32 +1,130 @@
|
||||
from .helpers import censor_url
|
||||
import logging
|
||||
import os
|
||||
from data.config import CACHE
|
||||
import logging
|
||||
import datetime
|
||||
import sys
|
||||
import glob
|
||||
import time
|
||||
import traceback
|
||||
from logging.handlers import RotatingFileHandler, TimedRotatingFileHandler
|
||||
from config.config import LOG_DIR, LOG_FILE, LOG_LEVEL, LOG_MAX_SIZE, LOG_BACKUP_COUNT, LOG_RETENTION_DAYS
|
||||
|
||||
from .url_censor import censor_url
|
||||
|
||||
class URLCensorFormatter(logging.Formatter):
|
||||
"""自定义的日志格式化器,用于隐藏日志消息中的URL"""
|
||||
|
||||
def format(self, record):
|
||||
# 先使用原始的format方法格式化日志
|
||||
formatted_message = super().format(record)
|
||||
# 临时禁用URL隐藏,直接返回原始消息
|
||||
return formatted_message
|
||||
# 然后对格式化后的消息进行URL审查(已禁用)
|
||||
# return censor_url(formatted_message)
|
||||
|
||||
class Logger:
|
||||
def __init__(self, filename, stream):
|
||||
self.terminal = stream
|
||||
self.log = open(filename, "w", encoding="utf-8")
|
||||
try:
|
||||
# 确保目录存在
|
||||
log_dir = os.path.dirname(filename)
|
||||
if log_dir and not os.path.exists(log_dir):
|
||||
os.makedirs(log_dir, exist_ok=True)
|
||||
print(f"已创建日志目录: {log_dir}")
|
||||
|
||||
# 以追加模式打开,避免覆盖现有内容
|
||||
self.log = open(filename, "a", encoding="utf-8", errors="replace")
|
||||
self.log.write("\n\n--- New logging session started ---\n\n")
|
||||
except (IOError, OSError) as e:
|
||||
# 如果打开文件失败,记录错误并使用空的写入操作
|
||||
print(f"Error opening log file {filename}: {e}")
|
||||
self.log = None
|
||||
|
||||
def write(self, message):
|
||||
censored_message = censor_url(message)
|
||||
self.terminal.write(censored_message)
|
||||
self.log.write(censored_message)
|
||||
self.flush()
|
||||
|
||||
try:
|
||||
# 临时禁用URL隐藏
|
||||
# censored_message = censor_url(message)
|
||||
censored_message = message # 直接使用原始消息
|
||||
self.terminal.write(censored_message)
|
||||
if self.log:
|
||||
self.log.write(censored_message)
|
||||
self.flush()
|
||||
except Exception as e:
|
||||
# 发生错误时记录到控制台
|
||||
self.terminal.write(f"Error writing to log: {e}\n")
|
||||
|
||||
def flush(self):
|
||||
self.terminal.flush()
|
||||
self.log.flush()
|
||||
try:
|
||||
self.terminal.flush()
|
||||
if self.log:
|
||||
self.log.flush()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def close(self):
|
||||
self.log.close()
|
||||
try:
|
||||
if self.log:
|
||||
self.log.write("\n--- Logging session ended ---\n")
|
||||
self.log.close()
|
||||
self.log = None
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 增加异常钩子,确保未捕获的异常也会记录到日志文件中
|
||||
def log_uncaught_exceptions(exc_type, exc_value, exc_traceback):
|
||||
"""处理未捕获的异常,记录到日志中"""
|
||||
if issubclass(exc_type, KeyboardInterrupt):
|
||||
# 对于键盘中断,使用默认处理
|
||||
sys.__excepthook__(exc_type, exc_value, exc_traceback)
|
||||
return
|
||||
|
||||
# 获取主日志记录器
|
||||
logger = logging.getLogger('main')
|
||||
|
||||
# 格式化异常信息
|
||||
lines = traceback.format_exception(exc_type, exc_value, exc_traceback)
|
||||
error_message = '未捕获的异常:\n' + ''.join(lines)
|
||||
|
||||
# 记录到日志中
|
||||
logger.critical(error_message)
|
||||
|
||||
# 同时也显示在控制台
|
||||
sys.__excepthook__(exc_type, exc_value, exc_traceback)
|
||||
|
||||
# 设置全局异常处理器
|
||||
sys.excepthook = log_uncaught_exceptions
|
||||
|
||||
def cleanup_old_logs(retention_days=7):
|
||||
"""清理超过指定天数的旧日志文件
|
||||
|
||||
Args:
|
||||
retention_days: 日志保留天数,默认7天
|
||||
"""
|
||||
try:
|
||||
now = time.time()
|
||||
cutoff = now - (retention_days * 86400) # 86400秒 = 1天
|
||||
|
||||
# 获取所有日志文件
|
||||
log_files = glob.glob(os.path.join(LOG_DIR, "log-*.txt"))
|
||||
|
||||
for log_file in log_files:
|
||||
# 检查文件修改时间
|
||||
if os.path.getmtime(log_file) < cutoff:
|
||||
try:
|
||||
os.remove(log_file)
|
||||
print(f"已删除过期日志: {log_file}")
|
||||
except Exception as e:
|
||||
print(f"删除日志文件失败 {log_file}: {e}")
|
||||
except Exception as e:
|
||||
print(f"清理旧日志文件时出错: {e}")
|
||||
|
||||
def setup_logger(name):
|
||||
"""设置并返回一个命名的logger
|
||||
|
||||
使用统一的日志文件,添加日志轮转功能,实现自动清理过期日志
|
||||
|
||||
Args:
|
||||
name: logger的名称
|
||||
|
||||
|
||||
Returns:
|
||||
logging.Logger: 配置好的logger对象
|
||||
"""
|
||||
@@ -36,29 +134,55 @@ def setup_logger(name):
|
||||
# 避免重复添加处理器
|
||||
if logger.hasHandlers():
|
||||
return logger
|
||||
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
# 根据配置设置日志级别
|
||||
log_level = getattr(logging, LOG_LEVEL.upper(), logging.DEBUG)
|
||||
logger.setLevel(log_level)
|
||||
|
||||
# 确保日志目录存在
|
||||
log_dir = os.path.join(CACHE, "logs")
|
||||
os.makedirs(log_dir, exist_ok=True)
|
||||
log_file = os.path.join(log_dir, f"{name}.log")
|
||||
os.makedirs(LOG_DIR, exist_ok=True)
|
||||
|
||||
# 创建文件处理器
|
||||
file_handler = logging.FileHandler(log_file, encoding="utf-8")
|
||||
file_handler.setLevel(logging.DEBUG)
|
||||
# 清理过期日志文件
|
||||
cleanup_old_logs(LOG_RETENTION_DAYS)
|
||||
|
||||
# 创建主日志文件的轮转处理器
|
||||
try:
|
||||
# 确保主日志文件目录存在
|
||||
log_file_dir = os.path.dirname(LOG_FILE)
|
||||
if log_file_dir and not os.path.exists(log_file_dir):
|
||||
os.makedirs(log_file_dir, exist_ok=True)
|
||||
print(f"已创建主日志目录: {log_file_dir}")
|
||||
|
||||
# 使用RotatingFileHandler实现日志轮转
|
||||
main_file_handler = RotatingFileHandler(
|
||||
LOG_FILE,
|
||||
maxBytes=LOG_MAX_SIZE,
|
||||
backupCount=LOG_BACKUP_COUNT,
|
||||
encoding="utf-8"
|
||||
)
|
||||
main_file_handler.setLevel(log_level)
|
||||
except (IOError, OSError) as e:
|
||||
print(f"无法创建主日志文件处理器: {e}")
|
||||
main_file_handler = None
|
||||
|
||||
# 创建控制台处理器
|
||||
console_handler = logging.StreamHandler()
|
||||
console_handler.setLevel(logging.INFO)
|
||||
console_handler.setLevel(logging.INFO) # 控制台只显示INFO以上级别
|
||||
|
||||
# 创建格式器并添加到处理器
|
||||
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
file_handler.setFormatter(formatter)
|
||||
# 创建更详细的格式器,包括模块名、文件名和行号
|
||||
formatter = URLCensorFormatter('%(asctime)s - %(name)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s')
|
||||
|
||||
# 设置处理器的格式化器
|
||||
console_handler.setFormatter(formatter)
|
||||
if main_file_handler:
|
||||
main_file_handler.setFormatter(formatter)
|
||||
|
||||
# 添加处理器到logger
|
||||
logger.addHandler(file_handler)
|
||||
logger.addHandler(console_handler)
|
||||
if main_file_handler:
|
||||
logger.addHandler(main_file_handler)
|
||||
|
||||
# 确保异常可以被正确记录
|
||||
logger.propagate = True
|
||||
|
||||
return logger
|
||||
33
source/utils/url_censor.py
Normal file
@@ -0,0 +1,33 @@
|
||||
import re
|
||||
|
||||
def censor_url(text):
|
||||
"""Censors URLs in a given text string, replacing them with a protection message.
|
||||
|
||||
Args:
|
||||
text: 要处理的文本
|
||||
|
||||
Returns:
|
||||
str: 处理后的文本,URL被完全隐藏
|
||||
"""
|
||||
# 临时禁用URL隐藏功能,直接返回原始文本以便调试
|
||||
if not isinstance(text, str):
|
||||
text = str(text)
|
||||
|
||||
return text # 直接返回原始文本,不做任何隐藏
|
||||
|
||||
# 以下是原始代码,现在被注释掉
|
||||
r'''
|
||||
# 匹配URL并替换为固定文本
|
||||
url_pattern = re.compile(r'https?://[^\s/$.?#].[^\s]*')
|
||||
censored = url_pattern.sub('***URL protection***', text)
|
||||
|
||||
# 额外处理带referer参数的情况
|
||||
referer_pattern = re.compile(r'--referer\s+(\S+)')
|
||||
censored = referer_pattern.sub('--referer ***URL protection***', censored)
|
||||
|
||||
# 处理Origin头
|
||||
origin_pattern = re.compile(r'Origin:\s+(\S+)')
|
||||
censored = origin_pattern.sub('Origin: ***URL protection***', censored)
|
||||
|
||||
return censored
|
||||
'''
|
||||
@@ -4,6 +4,11 @@ import webbrowser
|
||||
from PySide6.QtCore import QThread, Signal
|
||||
from PySide6.QtWidgets import QMessageBox
|
||||
import sys
|
||||
from utils.logger import setup_logger
|
||||
from utils.url_censor import censor_url
|
||||
|
||||
# 初始化logger
|
||||
logger = setup_logger("config_fetch")
|
||||
|
||||
class ConfigFetchThread(QThread):
|
||||
finished = Signal(object, str) # data, error_message
|
||||
@@ -17,16 +22,21 @@ class ConfigFetchThread(QThread):
|
||||
def run(self):
|
||||
try:
|
||||
if self.debug_mode:
|
||||
print("--- Starting to fetch cloud config ---")
|
||||
print(f"DEBUG: Requesting URL: {self.url}")
|
||||
print(f"DEBUG: Using Headers: {self.headers}")
|
||||
logger.debug("--- Starting to fetch cloud config ---")
|
||||
# 完全隐藏URL
|
||||
logger.debug(f"DEBUG: Requesting URL: ***URL protection***")
|
||||
logger.debug(f"DEBUG: Using Headers: {self.headers}")
|
||||
|
||||
response = requests.get(self.url, headers=self.headers, timeout=10)
|
||||
|
||||
if self.debug_mode:
|
||||
print(f"DEBUG: Response Status Code: {response.status_code}")
|
||||
print(f"DEBUG: Response Headers: {response.headers}")
|
||||
print(f"DEBUG: Response Text: {response.text}")
|
||||
logger.debug(f"DEBUG: Response Status Code: {response.status_code}")
|
||||
logger.debug(f"DEBUG: Response Headers: {response.headers}")
|
||||
|
||||
# 记录实际响应内容,但隐藏URL等敏感信息(临时禁用)
|
||||
# censored_text = censor_url(response.text)
|
||||
censored_text = response.text # 直接使用原始文本
|
||||
logger.debug(f"DEBUG: Response Text: {censored_text}")
|
||||
|
||||
response.raise_for_status()
|
||||
|
||||
@@ -62,4 +72,28 @@ class ConfigFetchThread(QThread):
|
||||
self.finished.emit(None, error_msg)
|
||||
finally:
|
||||
if self.debug_mode:
|
||||
print("--- Finished fetching cloud config ---")
|
||||
logger.debug("--- Finished fetching cloud config ---")
|
||||
|
||||
def _create_safe_config_for_logging(self, config_data):
|
||||
"""创建用于日志记录的安全配置副本,隐藏敏感URL
|
||||
|
||||
Args:
|
||||
config_data: 原始配置数据
|
||||
|
||||
Returns:
|
||||
dict: 安全的配置数据副本
|
||||
"""
|
||||
if not config_data or not isinstance(config_data, dict):
|
||||
return config_data
|
||||
|
||||
# 创建深拷贝,避免修改原始数据
|
||||
import copy
|
||||
safe_config = copy.deepcopy(config_data)
|
||||
|
||||
# 隐藏敏感URL
|
||||
for key in safe_config:
|
||||
if isinstance(safe_config[key], dict) and "url" in safe_config[key]:
|
||||
# 完全隐藏URL
|
||||
safe_config[key]["url"] = "***URL protection***"
|
||||
|
||||
return safe_config
|
||||
@@ -7,7 +7,7 @@ from PySide6 import QtCore, QtWidgets
|
||||
from PySide6.QtCore import (Qt, Signal, QThread, QTimer)
|
||||
from PySide6.QtWidgets import (QLabel, QProgressBar, QVBoxLayout, QDialog, QHBoxLayout)
|
||||
from utils import resource_path
|
||||
from data.config import APP_NAME, UA
|
||||
from config.config import APP_NAME, UA
|
||||
import signal
|
||||
import ctypes
|
||||
import time
|
||||
@@ -53,7 +53,7 @@ class DownloadThread(QThread):
|
||||
subprocess.run(['taskkill', '/F', '/T', '/PID', str(self.process.pid)], check=True, creationflags=subprocess.CREATE_NO_WINDOW)
|
||||
except (subprocess.CalledProcessError, FileNotFoundError) as e:
|
||||
print(f"停止下载进程时出错: {e}")
|
||||
|
||||
|
||||
def _get_process_threads(self, pid):
|
||||
"""获取进程的所有线程ID"""
|
||||
if sys.platform != 'win32':
|
||||
@@ -137,7 +137,7 @@ class DownloadThread(QThread):
|
||||
def is_paused(self):
|
||||
"""返回当前下载是否处于暂停状态"""
|
||||
return self._is_paused
|
||||
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
if not self._is_running:
|
||||
@@ -147,28 +147,28 @@ class DownloadThread(QThread):
|
||||
aria2c_path = resource_path("aria2c-fast_x64.exe")
|
||||
download_dir = os.path.dirname(self._7z_path)
|
||||
file_name = os.path.basename(self._7z_path)
|
||||
|
||||
|
||||
parsed_url = urlparse(self.url)
|
||||
referer = f"{parsed_url.scheme}://{parsed_url.netloc}/"
|
||||
|
||||
|
||||
command = [
|
||||
aria2c_path,
|
||||
]
|
||||
|
||||
|
||||
# 获取主窗口的下载管理器对象
|
||||
thread_count = 64 # 默认值
|
||||
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,
|
||||
@@ -187,7 +187,7 @@ class DownloadThread(QThread):
|
||||
'--header', 'Sec-Fetch-Site: same-origin',
|
||||
'--http-accept-gzip=true',
|
||||
'--console-log-level=notice',
|
||||
'--summary-interval=1',
|
||||
'--summary-interval=1',
|
||||
'--log-level=notice',
|
||||
'--max-tries=3',
|
||||
'--retry-wait=2',
|
||||
@@ -195,21 +195,21 @@ class DownloadThread(QThread):
|
||||
'--timeout=60',
|
||||
'--auto-file-renaming=false',
|
||||
'--allow-overwrite=true',
|
||||
'--split=128',
|
||||
f'--max-connection-per-server={thread_count}', # 使用动态的线程数
|
||||
'--min-split-size=1M', # 减小最小分片大小
|
||||
'--optimize-concurrent-downloads=true', # 优化并发下载
|
||||
'--file-allocation=none', # 禁用文件预分配加快开始
|
||||
'--async-dns=true', # 使用异步DNS
|
||||
'--split=128',
|
||||
f'--max-connection-per-server={thread_count}', # 使用动态的线程数
|
||||
'--min-split-size=1M', # 减小最小分片大小
|
||||
'--optimize-concurrent-downloads=true', # 优化并发下载
|
||||
'--file-allocation=none', # 禁用文件预分配加快开始
|
||||
'--async-dns=true', # 使用异步DNS
|
||||
])
|
||||
|
||||
|
||||
# 根据IPv6设置决定是否禁用IPv6
|
||||
if not ipv6_enabled:
|
||||
command.append('--disable-ipv6=true')
|
||||
print("已禁用IPv6支持")
|
||||
else:
|
||||
print("已启用IPv6支持")
|
||||
|
||||
|
||||
# 证书验证现在总是需要,因为我们依赖hosts文件
|
||||
command.append('--check-certificate=false')
|
||||
|
||||
@@ -222,9 +222,9 @@ class DownloadThread(QThread):
|
||||
self.process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, encoding='utf-8', errors='replace', creationflags=creation_flags)
|
||||
|
||||
# 正则表达式用于解析aria2c的输出
|
||||
# 例如: #1 GID[...]( 5%) CN:1 DL:10.5MiB/s ETA:1m30s
|
||||
# 例如: #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秒最多更新一次
|
||||
@@ -263,8 +263,9 @@ class DownloadThread(QThread):
|
||||
last_update_time = current_time
|
||||
|
||||
return_code = self.process.wait()
|
||||
|
||||
if not self._is_running: # 如果是手动停止的
|
||||
|
||||
if not self._is_running:
|
||||
# 如果是手动停止的
|
||||
self.finished.emit(False, "下载已手动停止。")
|
||||
return
|
||||
|
||||
@@ -323,7 +324,6 @@ class ProgressWindow(QDialog):
|
||||
|
||||
# 设置暂停/恢复状态
|
||||
self.is_paused = False
|
||||
|
||||
# 添加最后进度记录,用于优化UI更新
|
||||
self._last_percent = -1
|
||||
|
||||
|
||||
@@ -1,31 +1,351 @@
|
||||
import os
|
||||
import shutil
|
||||
import py7zr
|
||||
import tempfile
|
||||
import traceback
|
||||
from PySide6.QtCore import QThread, Signal
|
||||
from data.config import PLUGIN, GAME_INFO
|
||||
from config.config import PLUGIN, GAME_INFO
|
||||
import time # 用于时间计算
|
||||
import threading
|
||||
import queue
|
||||
from concurrent.futures import TimeoutError
|
||||
|
||||
class ExtractionThread(QThread):
|
||||
finished = Signal(bool, str, str) # success, error_message, game_version
|
||||
progress = Signal(int, str) # 添加进度信号,传递进度百分比和状态信息
|
||||
|
||||
def __init__(self, _7z_path, game_folder, plugin_path, game_version, parent=None):
|
||||
def __init__(self, _7z_path, game_folder, plugin_path, game_version, parent=None, extracted_path=None):
|
||||
super().__init__(parent)
|
||||
self._7z_path = _7z_path
|
||||
self.game_folder = game_folder
|
||||
self.plugin_path = plugin_path
|
||||
self.game_version = game_version
|
||||
self.extracted_path = extracted_path # 添加已解压文件路径参数
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
with py7zr.SevenZipFile(self._7z_path, mode="r") as archive:
|
||||
archive.extractall(path=PLUGIN)
|
||||
|
||||
# 确保游戏目录存在
|
||||
os.makedirs(self.game_folder, exist_ok=True)
|
||||
shutil.copy(self.plugin_path, self.game_folder)
|
||||
|
||||
def update_progress(percent: int, message: str):
|
||||
try:
|
||||
self.progress.emit(percent, message)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if self.game_version == "NEKOPARA After":
|
||||
sig_path = os.path.join(PLUGIN, GAME_INFO[self.game_version]["sig_path"])
|
||||
shutil.copy(sig_path, self.game_folder)
|
||||
# 记录调试信息
|
||||
from utils.logger import setup_logger
|
||||
debug_logger = setup_logger("extraction_thread")
|
||||
debug_logger.info(f"====== 开始处理 {self.game_version} 补丁文件 ======")
|
||||
debug_logger.info(f"压缩包路径: {self._7z_path}")
|
||||
debug_logger.info(f"游戏目录: {self.game_folder}")
|
||||
debug_logger.info(f"插件路径: {self.plugin_path}")
|
||||
|
||||
update_progress(0, f"开始处理 {self.game_version} 的补丁文件...")
|
||||
|
||||
# 支持外部请求中断
|
||||
if self.isInterruptionRequested():
|
||||
self.finished.emit(False, "操作已取消", self.game_version)
|
||||
return
|
||||
|
||||
# 如果提供了已解压文件路径,直接使用它
|
||||
if self.extracted_path and os.path.exists(self.extracted_path):
|
||||
update_progress(20, f"正在复制 {self.game_version} 的补丁文件...\n(在此过程中可能会卡顿或无响应,请不要关闭软件)")
|
||||
|
||||
# 直接复制已解压的文件到游戏目录
|
||||
target_file = os.path.join(self.game_folder, os.path.basename(self.plugin_path))
|
||||
shutil.copy(self.extracted_path, target_file)
|
||||
|
||||
update_progress(60, f"正在完成 {self.game_version} 的补丁安装...")
|
||||
|
||||
# 对于NEKOPARA After,还需要复制签名文件
|
||||
if self.game_version == "NEKOPARA After":
|
||||
try:
|
||||
update_progress(70, f"正在处理 {self.game_version} 的签名文件...")
|
||||
# 从已解压文件的目录中获取签名文件
|
||||
extracted_dir = os.path.dirname(self.extracted_path)
|
||||
sig_filename = os.path.basename(GAME_INFO[self.game_version]["sig_path"])
|
||||
sig_path = os.path.join(extracted_dir, sig_filename)
|
||||
|
||||
# 尝试多种可能的签名文件路径
|
||||
if not os.path.exists(sig_path):
|
||||
# 尝试在同级目录查找
|
||||
sig_path = os.path.join(os.path.dirname(extracted_dir), sig_filename)
|
||||
|
||||
# 如果签名文件存在,则复制它
|
||||
if os.path.exists(sig_path):
|
||||
target_sig = os.path.join(self.game_folder, sig_filename)
|
||||
shutil.copy(sig_path, target_sig)
|
||||
update_progress(80, f"签名文件复制完成")
|
||||
else:
|
||||
# 如果签名文件不存在,则使用原始路径
|
||||
sig_path = os.path.join(PLUGIN, GAME_INFO[self.game_version]["sig_path"])
|
||||
if os.path.exists(sig_path):
|
||||
target_sig = os.path.join(self.game_folder, os.path.basename(sig_path))
|
||||
shutil.copy(sig_path, target_sig)
|
||||
update_progress(80, f"使用内置签名文件完成")
|
||||
else:
|
||||
update_progress(80, f"未找到签名文件,继续安装主补丁文件")
|
||||
except Exception as sig_err:
|
||||
# 签名文件处理失败时记录错误但不中断主流程
|
||||
update_progress(80, f"签名文件处理失败: {str(sig_err)}")
|
||||
|
||||
update_progress(100, f"{self.game_version} 补丁文件处理完成")
|
||||
self.finished.emit(True, "", self.game_version)
|
||||
return
|
||||
|
||||
# 否则解压源压缩包到临时目录,再复制目标文件
|
||||
update_progress(10, f"正在打开 {self.game_version} 的补丁压缩包...")
|
||||
|
||||
with py7zr.SevenZipFile(self._7z_path, mode="r") as archive:
|
||||
# 获取压缩包内的文件列表
|
||||
file_list = archive.getnames()
|
||||
|
||||
self.finished.emit(True, "", self.game_version)
|
||||
# 详细记录压缩包中的所有文件
|
||||
debug_logger.debug(f"压缩包内容分析:")
|
||||
debug_logger.debug(f"- 文件总数: {len(file_list)}")
|
||||
for i, f in enumerate(file_list):
|
||||
debug_logger.debug(f" {i+1}. {f} - 类型: {'文件夹' if f.endswith('/') or f.endswith('\\') else '文件'}")
|
||||
|
||||
update_progress(20, f"正在分析 {self.game_version} 的补丁文件...")
|
||||
|
||||
update_progress(30, f"正在解压 {self.game_version} 的补丁文件...\n(在此过程中可能会卡顿或无响应,请不要关闭软件)")
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
# 查找主补丁文件和签名文件
|
||||
target_filename = os.path.basename(self.plugin_path)
|
||||
# 只有NEKOPARA After版本才需要查找签名文件
|
||||
if self.game_version == "NEKOPARA After":
|
||||
sig_filename = target_filename + ".sig" # 签名文件名
|
||||
debug_logger.debug(f"查找主补丁文件: {target_filename}")
|
||||
debug_logger.debug(f"查找签名文件: {sig_filename}")
|
||||
else:
|
||||
sig_filename = None
|
||||
debug_logger.debug(f"查找主补丁文件: {target_filename}")
|
||||
debug_logger.debug(f"{self.game_version} 不需要签名文件")
|
||||
|
||||
target_file_in_archive = None
|
||||
sig_file_in_archive = None
|
||||
|
||||
# 对于NEKOPARA After,增加特殊处理
|
||||
if self.game_version == "NEKOPARA After":
|
||||
# 增加专门的检查,同时识别主补丁和签名文件
|
||||
debug_logger.debug("执行NEKOPARA After特殊补丁文件识别")
|
||||
|
||||
# 查找主补丁和签名文件
|
||||
for file_path in file_list:
|
||||
basename = os.path.basename(file_path)
|
||||
|
||||
# 查找主补丁文件
|
||||
if basename == "afteradult.xp3" and not basename.endswith('.sig'):
|
||||
target_file_in_archive = file_path
|
||||
debug_logger.debug(f"找到精确匹配的After主补丁文件: {target_file_in_archive}")
|
||||
|
||||
# 查找签名文件
|
||||
elif basename == "afteradult.xp3.sig" or basename.endswith('.sig'):
|
||||
sig_file_in_archive = file_path
|
||||
debug_logger.debug(f"找到After签名文件: {sig_file_in_archive}")
|
||||
|
||||
# 如果没找到主补丁文件,寻找可能的替代文件
|
||||
if not target_file_in_archive:
|
||||
for file_path in file_list:
|
||||
if "afteradult.xp3" in file_path and not file_path.endswith('.sig'):
|
||||
target_file_in_archive = file_path
|
||||
debug_logger.debug(f"找到备选After主补丁文件: {target_file_in_archive}")
|
||||
break
|
||||
else:
|
||||
# 标准处理逻辑
|
||||
for file_path in file_list:
|
||||
basename = os.path.basename(file_path)
|
||||
|
||||
# 查找主补丁文件
|
||||
if basename == target_filename and not basename.endswith('.sig'):
|
||||
target_file_in_archive = file_path
|
||||
debug_logger.debug(f"在压缩包中找到主补丁文件: {target_file_in_archive}")
|
||||
|
||||
# 查找签名文件
|
||||
elif basename == sig_filename:
|
||||
sig_file_in_archive = file_path
|
||||
debug_logger.debug(f"在压缩包中找到签名文件: {sig_file_in_archive}")
|
||||
|
||||
# 如果没有找到精确匹配的主补丁文件,使用更宽松的搜索
|
||||
if not target_file_in_archive:
|
||||
debug_logger.warning(f"没有找到精确匹配的主补丁文件,尝试更宽松的搜索")
|
||||
for file_path in file_list:
|
||||
if target_filename in file_path and not file_path.endswith('.sig'):
|
||||
target_file_in_archive = file_path
|
||||
debug_logger.info(f"在压缩包中找到可能的主补丁文件: {target_file_in_archive}")
|
||||
break
|
||||
|
||||
# 如果找不到主补丁文件,使用回退方案:提取全部内容
|
||||
if not target_file_in_archive:
|
||||
debug_logger.warning(f"未能识别正确的主补丁文件,将提取所有文件并尝试查找")
|
||||
|
||||
# 提取所有文件到临时目录
|
||||
update_progress(30, f"正在解压所有文件...")
|
||||
archive.extractall(path=temp_dir)
|
||||
debug_logger.debug(f"已提取所有文件到临时目录")
|
||||
|
||||
# 在提取的文件中查找主补丁文件和签名文件
|
||||
found_main = False
|
||||
found_sig = False
|
||||
|
||||
for root, dirs, files in os.walk(temp_dir):
|
||||
for file in files:
|
||||
# 查找主补丁文件
|
||||
if file == target_filename and not file.endswith('.sig'):
|
||||
extracted_file_path = os.path.join(root, file)
|
||||
file_size = os.path.getsize(extracted_file_path)
|
||||
debug_logger.debug(f"在提取的文件中找到主补丁文件: {extracted_file_path}, 大小: {file_size} 字节")
|
||||
|
||||
# 复制到目标位置
|
||||
target_path = os.path.join(self.game_folder, target_filename)
|
||||
shutil.copy2(extracted_file_path, target_path)
|
||||
debug_logger.debug(f"已复制主补丁文件到: {target_path}")
|
||||
found_main = True
|
||||
|
||||
# 查找签名文件
|
||||
elif file == sig_filename or file.endswith('.sig'):
|
||||
extracted_sig_path = os.path.join(root, file)
|
||||
sig_size = os.path.getsize(extracted_sig_path)
|
||||
debug_logger.debug(f"在提取的文件中找到签名文件: {extracted_sig_path}, 大小: {sig_size} 字节")
|
||||
|
||||
# 复制到目标位置
|
||||
sig_target = os.path.join(self.game_folder, sig_filename)
|
||||
shutil.copy2(extracted_sig_path, sig_target)
|
||||
debug_logger.debug(f"已复制签名文件到: {sig_target}")
|
||||
found_sig = True
|
||||
|
||||
# 如果两个文件都找到,可以停止遍历
|
||||
if found_main and found_sig:
|
||||
debug_logger.debug("已找到所有需要的文件,停止遍历")
|
||||
break
|
||||
|
||||
if found_main and found_sig:
|
||||
break
|
||||
|
||||
if not found_main:
|
||||
debug_logger.error(f"无法找到主补丁文件,安装失败")
|
||||
raise FileNotFoundError(f"在压缩包中未找到主补丁文件 {target_filename}")
|
||||
|
||||
# 只有NEKOPARA After版本才需要处理签名文件
|
||||
if self.game_version == "NEKOPARA After":
|
||||
# 签名文件没找到不影响主流程,但记录警告
|
||||
if not found_sig:
|
||||
debug_logger.warning(f"未找到签名文件 {sig_filename},但继续安装主补丁文件")
|
||||
else:
|
||||
debug_logger.info(f"{self.game_version} 不需要签名文件,跳过签名文件处理")
|
||||
else:
|
||||
# 准备要解压的文件列表
|
||||
files_to_extract = [target_file_in_archive]
|
||||
# 只有NEKOPARA After版本才需要解压签名文件
|
||||
if self.game_version == "NEKOPARA After" and sig_file_in_archive:
|
||||
files_to_extract.append(sig_file_in_archive)
|
||||
debug_logger.debug(f"将同时解压主补丁文件和签名文件: {files_to_extract}")
|
||||
else:
|
||||
debug_logger.debug(f"将仅解压主补丁文件: {files_to_extract}")
|
||||
|
||||
# 解压选定的文件到临时目录
|
||||
debug_logger.debug(f"开始解压选定文件到临时目录: {temp_dir}")
|
||||
|
||||
# 设置解压超时时间(秒)
|
||||
extract_timeout = 180 # 3分钟超时
|
||||
debug_logger.debug(f"设置解压超时: {extract_timeout}秒")
|
||||
|
||||
# 创建子线程执行解压
|
||||
import threading
|
||||
import queue
|
||||
|
||||
extract_result = queue.Queue()
|
||||
|
||||
def extract_files():
|
||||
try:
|
||||
archive.extract(path=temp_dir, targets=files_to_extract)
|
||||
extract_result.put(("success", None))
|
||||
except Exception as e:
|
||||
extract_result.put(("error", e))
|
||||
|
||||
extract_thread = threading.Thread(target=extract_files)
|
||||
extract_thread.daemon = True
|
||||
extract_thread.start()
|
||||
|
||||
# 每5秒更新一次进度,最多等待设定的超时时间
|
||||
total_waited = 0
|
||||
while extract_thread.is_alive() and total_waited < extract_timeout:
|
||||
update_progress(30 + int(30 * total_waited / extract_timeout),
|
||||
f"正在解压文件...已等待{total_waited}秒")
|
||||
extract_thread.join(5) # 等待5秒
|
||||
total_waited += 5
|
||||
|
||||
# 检查是否超时
|
||||
if extract_thread.is_alive():
|
||||
debug_logger.error(f"解压超时(超过{extract_timeout}秒)")
|
||||
raise TimeoutError(f"解压超时(超过{extract_timeout}秒),请检查补丁文件是否完整")
|
||||
|
||||
# 检查解压结果
|
||||
if not extract_result.empty():
|
||||
status, error = extract_result.get()
|
||||
if status == "error":
|
||||
debug_logger.error(f"解压错误: {error}")
|
||||
raise error
|
||||
|
||||
debug_logger.debug(f"文件解压完成")
|
||||
|
||||
update_progress(60, f"正在复制 {self.game_version} 的补丁文件...")
|
||||
|
||||
# 复制主补丁文件到游戏目录
|
||||
extracted_file_path = os.path.join(temp_dir, target_file_in_archive)
|
||||
|
||||
# 检查解压后的文件是否存在及其大小
|
||||
if os.path.exists(extracted_file_path):
|
||||
file_size = os.path.getsize(extracted_file_path)
|
||||
debug_logger.debug(f"解压后的主补丁文件存在: {extracted_file_path}, 大小: {file_size} 字节")
|
||||
else:
|
||||
debug_logger.error(f"解压后的主补丁文件不存在: {extracted_file_path}")
|
||||
raise FileNotFoundError(f"解压后的文件不存在: {extracted_file_path}")
|
||||
|
||||
# 构建目标路径并复制
|
||||
target_path = os.path.join(self.game_folder, target_filename)
|
||||
debug_logger.debug(f"复制主补丁文件: {extracted_file_path} 到 {target_path}")
|
||||
shutil.copy2(extracted_file_path, target_path)
|
||||
|
||||
# 验证主补丁文件是否成功复制
|
||||
if os.path.exists(target_path):
|
||||
target_size = os.path.getsize(target_path)
|
||||
debug_logger.debug(f"主补丁文件成功复制: {target_path}, 大小: {target_size} 字节")
|
||||
else:
|
||||
debug_logger.error(f"主补丁文件复制失败: {target_path}")
|
||||
raise FileNotFoundError(f"目标文件复制失败: {target_path}")
|
||||
|
||||
# 只有NEKOPARA After版本才需要处理签名文件
|
||||
if self.game_version == "NEKOPARA After":
|
||||
# 如果有找到签名文件,也复制它
|
||||
if sig_file_in_archive:
|
||||
update_progress(80, f"正在复制签名文件...")
|
||||
extracted_sig_path = os.path.join(temp_dir, sig_file_in_archive)
|
||||
|
||||
if os.path.exists(extracted_sig_path):
|
||||
sig_size = os.path.getsize(extracted_sig_path)
|
||||
debug_logger.debug(f"解压后的签名文件存在: {extracted_sig_path}, 大小: {sig_size} 字节")
|
||||
|
||||
# 复制签名文件到游戏目录
|
||||
sig_target = os.path.join(self.game_folder, sig_filename)
|
||||
shutil.copy2(extracted_sig_path, sig_target)
|
||||
debug_logger.debug(f"签名文件成功复制: {sig_target}")
|
||||
else:
|
||||
debug_logger.warning(f"解压后的签名文件不存在: {extracted_sig_path}")
|
||||
else:
|
||||
debug_logger.warning(f"压缩包中没有找到签名文件,但继续安装主补丁文件")
|
||||
else:
|
||||
debug_logger.info(f"{self.game_version} 不需要签名文件,跳过签名文件处理")
|
||||
|
||||
update_progress(100, f"{self.game_version} 补丁文件解压完成")
|
||||
self.finished.emit(True, "", self.game_version)
|
||||
except (py7zr.Bad7zFile, FileNotFoundError, Exception) as e:
|
||||
self.finished.emit(False, f"\n文件操作失败,请重试\n\n【错误信息】:{e}\n", self.game_version)
|
||||
try:
|
||||
self.progress.emit(100, f"处理 {self.game_version} 的补丁文件失败")
|
||||
except Exception:
|
||||
pass
|
||||
self.finished.emit(False, f"\n文件操作失败,请重试\n\n【错误信息】:{e}\n", self.game_version)
|
||||
|
||||
|
||||
@@ -1,28 +1,545 @@
|
||||
import os
|
||||
import hashlib
|
||||
import py7zr
|
||||
import tempfile
|
||||
import traceback
|
||||
import time # Added for time.time()
|
||||
from PySide6.QtCore import QThread, Signal
|
||||
from utils import HashManager
|
||||
from data.config import BLOCK_SIZE
|
||||
from PySide6.QtWidgets import QApplication
|
||||
from utils.logger import setup_logger
|
||||
|
||||
# 初始化logger
|
||||
logger = setup_logger("hash_thread")
|
||||
|
||||
class HashThread(QThread):
|
||||
pre_finished = Signal(dict)
|
||||
after_finished = Signal(dict)
|
||||
|
||||
def __init__(self, mode, install_paths, plugin_hash, installed_status, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
def __init__(self, mode, install_paths, plugin_hash, installed_status, main_window=None):
|
||||
"""初始化哈希检查线程
|
||||
|
||||
Args:
|
||||
mode: 检查模式,"pre"或"after"
|
||||
install_paths: 安装路径字典
|
||||
plugin_hash: 插件哈希值字典
|
||||
installed_status: 安装状态字典
|
||||
main_window: 主窗口实例,用于访问UI和状态
|
||||
"""
|
||||
super().__init__()
|
||||
self.mode = mode
|
||||
self.install_paths = install_paths
|
||||
self.plugin_hash = plugin_hash
|
||||
self.installed_status = installed_status
|
||||
# 每个线程都应该有自己的HashManager实例
|
||||
self.hash_manager = HashManager(BLOCK_SIZE)
|
||||
|
||||
self.installed_status = installed_status.copy()
|
||||
self.main_window = main_window
|
||||
|
||||
def run(self):
|
||||
"""运行线程"""
|
||||
debug_mode = False
|
||||
|
||||
# 设置超时限制(分钟)
|
||||
timeout_minutes = 10
|
||||
max_execution_time = timeout_minutes * 60 # 转换为秒
|
||||
start_execution_time = time.time()
|
||||
|
||||
# 尝试检测是否处于调试模式
|
||||
if self.main_window and hasattr(self.main_window, 'debug_manager'):
|
||||
debug_mode = self.main_window.debug_manager._is_debug_mode()
|
||||
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 设置哈希计算超时时间: {timeout_minutes} 分钟")
|
||||
|
||||
# 在各个关键步骤添加超时检测
|
||||
def check_timeout():
|
||||
elapsed = time.time() - start_execution_time
|
||||
if elapsed > max_execution_time:
|
||||
if debug_mode:
|
||||
logger.error(f"DEBUG: 哈希计算超时,已执行 {elapsed:.1f} 秒,超过限制的 {max_execution_time} 秒")
|
||||
return True
|
||||
return False
|
||||
|
||||
if self.mode == "pre":
|
||||
updated_status = self.hash_manager.cfg_pre_hash_compare(
|
||||
self.install_paths, self.plugin_hash, self.installed_status
|
||||
)
|
||||
self.pre_finished.emit(updated_status)
|
||||
status_copy = self.installed_status.copy()
|
||||
|
||||
for game_version, install_path in self.install_paths.items():
|
||||
if self.isInterruptionRequested():
|
||||
break
|
||||
if not os.path.exists(install_path):
|
||||
status_copy[game_version] = False
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 哈希预检查 - {game_version} 补丁文件不存在: {install_path}")
|
||||
continue
|
||||
|
||||
try:
|
||||
expected_hash = self.plugin_hash.get(game_version, "")
|
||||
if not expected_hash:
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 哈希预检查 - {game_version} 没有预期哈希值,跳过哈希检查")
|
||||
# 当没有预期哈希值时,保持当前状态不变
|
||||
continue
|
||||
|
||||
# 分块读取,避免大文件一次性读取内存
|
||||
hash_obj = hashlib.sha256()
|
||||
with open(install_path, "rb") as f:
|
||||
while True:
|
||||
if self.isInterruptionRequested():
|
||||
break
|
||||
# 检查超时
|
||||
if check_timeout():
|
||||
logger.error(f"哈希计算超时,强制终止")
|
||||
result["passed"] = False
|
||||
result["game"] = game_version
|
||||
result["message"] = f"\n{game_version} 哈希计算超时,已超过 {timeout_minutes} 分钟。\n\n请考虑跳过哈希校验或稍后再试。\n"
|
||||
break
|
||||
chunk = f.read(1024 * 1024)
|
||||
if not chunk:
|
||||
break
|
||||
hash_obj.update(chunk)
|
||||
file_hash = hash_obj.hexdigest()
|
||||
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 哈希预检查 - {game_version}")
|
||||
logger.debug(f"DEBUG: 文件路径: {install_path}")
|
||||
logger.debug(f"DEBUG: 预期哈希值: {expected_hash}")
|
||||
logger.debug(f"DEBUG: 实际哈希值: {file_hash}")
|
||||
logger.debug(f"DEBUG: 哈希匹配: {file_hash == expected_hash}")
|
||||
|
||||
if file_hash == expected_hash:
|
||||
status_copy[game_version] = True
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 哈希预检查 - {game_version} 哈希匹配成功")
|
||||
else:
|
||||
status_copy[game_version] = False
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 哈希预检查 - {game_version} 哈希不匹配")
|
||||
except Exception as e:
|
||||
status_copy[game_version] = False
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 哈希预检查异常 - {game_version}: {str(e)}")
|
||||
|
||||
self.pre_finished.emit(status_copy)
|
||||
|
||||
elif self.mode == "after":
|
||||
result = self.hash_manager.cfg_after_hash_compare(
|
||||
self.install_paths, self.plugin_hash, self.installed_status
|
||||
)
|
||||
self.after_finished.emit(result)
|
||||
result = {"passed": True, "game": "", "message": ""}
|
||||
|
||||
for game_version, install_path in self.install_paths.items():
|
||||
if self.isInterruptionRequested():
|
||||
break
|
||||
if not os.path.exists(install_path):
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 哈希后检查 - {game_version} 补丁文件不存在: {install_path}")
|
||||
continue
|
||||
|
||||
# 设置当前处理的游戏版本
|
||||
result["game"] = game_version
|
||||
|
||||
try:
|
||||
expected_hash = self.plugin_hash.get(game_version, "")
|
||||
if not expected_hash:
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 哈希后检查 - {game_version} 没有预期哈希值,跳过哈希检查")
|
||||
# 当没有预期哈希值时,跳过检查
|
||||
continue
|
||||
|
||||
# 检查文件存在和可读性
|
||||
if not os.path.exists(install_path):
|
||||
logger.error(f"哈希校验失败 - 文件不存在: {install_path}")
|
||||
result["passed"] = False
|
||||
result["game"] = game_version
|
||||
result["message"] = f"\n{game_version} 安装后的文件不存在,无法校验。\n\n文件路径: {install_path}\n"
|
||||
break
|
||||
|
||||
# 记录文件大小信息
|
||||
file_size = os.path.getsize(install_path)
|
||||
logger.info(f"开始校验 {game_version} 补丁文件")
|
||||
logger.debug(f"文件路径: {install_path}, 文件大小: {file_size} 字节")
|
||||
|
||||
# 增加块大小,提高大文件处理性能
|
||||
# 文件越大,块越大,最大256MB
|
||||
chunk_size = min(256 * 1024 * 1024, max(16 * 1024 * 1024, file_size // 20))
|
||||
logger.debug(f"使用块大小: {chunk_size // (1024 * 1024)}MB")
|
||||
|
||||
# 分块读取,避免大文件一次性读取内存
|
||||
hash_obj = hashlib.sha256()
|
||||
bytes_read = 0
|
||||
start_time = time.time()
|
||||
last_progress_time = start_time
|
||||
with open(install_path, "rb") as f:
|
||||
while True:
|
||||
if self.isInterruptionRequested():
|
||||
break
|
||||
# 检查超时
|
||||
if check_timeout():
|
||||
logger.error(f"哈希计算超时,强制终止")
|
||||
result["passed"] = False
|
||||
result["game"] = game_version
|
||||
result["message"] = f"\n{game_version} 哈希计算超时,已超过 {timeout_minutes} 分钟。\n\n请考虑跳过哈希校验或稍后再试。\n"
|
||||
break
|
||||
chunk = f.read(chunk_size)
|
||||
if not chunk:
|
||||
break
|
||||
bytes_read += len(chunk)
|
||||
hash_obj.update(chunk)
|
||||
|
||||
# 每秒更新一次进度
|
||||
current_time = time.time()
|
||||
if current_time - last_progress_time >= 1.0:
|
||||
progress = bytes_read / file_size * 100
|
||||
elapsed = current_time - start_time
|
||||
speed = bytes_read / (elapsed if elapsed > 0 else 1) / (1024 * 1024) # MB/s
|
||||
logger.debug(f"哈希计算进度: {progress:.1f}% - 已处理: {bytes_read/(1024*1024):.1f}MB/{file_size/(1024*1024):.1f}MB - 速度: {speed:.1f}MB/s")
|
||||
last_progress_time = current_time
|
||||
|
||||
# 计算最终的哈希值
|
||||
file_hash = hash_obj.hexdigest()
|
||||
|
||||
# 记录总用时
|
||||
total_time = time.time() - start_time
|
||||
logger.debug(f"哈希计算完成,耗时: {total_time:.1f}秒,平均速度: {file_size/(total_time*1024*1024):.1f}MB/s")
|
||||
|
||||
# 记录哈希比较结果
|
||||
is_valid = file_hash == expected_hash
|
||||
logger.info(f"{game_version} 哈希校验{'通过' if is_valid else '失败'}")
|
||||
logger.debug(f"哈希校验详情 - {game_version}:")
|
||||
logger.debug(f" 文件: {install_path}")
|
||||
logger.debug(f" 读取字节数: {bytes_read} / {file_size}")
|
||||
logger.debug(f" 预期哈希: {expected_hash}")
|
||||
logger.debug(f" 实际哈希: {file_hash}")
|
||||
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 哈希后检查 - {game_version}")
|
||||
logger.debug(f"DEBUG: 文件路径: {install_path}")
|
||||
logger.debug(f"DEBUG: 预期哈希值: {expected_hash}")
|
||||
logger.debug(f"DEBUG: 实际哈希值: {file_hash}")
|
||||
logger.debug(f"DEBUG: 哈希匹配: {file_hash == expected_hash}")
|
||||
|
||||
if file_hash != expected_hash:
|
||||
result["passed"] = False
|
||||
result["game"] = game_version
|
||||
result["message"] = f"\n{game_version} 安装后的文件校验失败。\n\n文件可能已损坏或被篡改,请重新安装。\n预期哈希: {expected_hash[:10]}...\n实际哈希: {file_hash[:10]}...\n"
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 哈希后检查 - {game_version} 哈希不匹配")
|
||||
break
|
||||
elif debug_mode:
|
||||
logger.debug(f"DEBUG: 哈希后检查 - {game_version} 哈希匹配成功")
|
||||
except Exception as e:
|
||||
result["passed"] = False
|
||||
result["game"] = game_version
|
||||
result["message"] = f"\n{game_version} 安装后的文件校验过程中发生错误。\n\n错误信息: {str(e)}\n"
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 哈希后检查异常 - {game_version}: {str(e)}")
|
||||
break
|
||||
|
||||
self.after_finished.emit(result)
|
||||
|
||||
|
||||
class OfflineHashVerifyThread(QThread):
|
||||
"""离线模式下验证补丁文件哈希的线程,支持进度更新"""
|
||||
|
||||
progress = Signal(int) # 进度信号,0-100
|
||||
finished = Signal(bool, str, str) # 完成信号,(成功/失败, 错误信息, 解压后的补丁文件路径)
|
||||
|
||||
def __init__(self, game_version, file_path, plugin_hash, main_window=None):
|
||||
super().__init__()
|
||||
self.game_version = game_version
|
||||
self.file_path = file_path
|
||||
self.plugin_hash = plugin_hash
|
||||
self.main_window = main_window
|
||||
self.extracted_patch_path = None # 添加解压后的补丁文件路径
|
||||
|
||||
# 获取预期的哈希值
|
||||
self.expected_hash = None
|
||||
|
||||
# 直接使用完整游戏名称作为键
|
||||
self.expected_hash = self.plugin_hash.get(game_version, "")
|
||||
|
||||
# 设置调试模式标志
|
||||
self.debug_mode = False
|
||||
if main_window and hasattr(main_window, 'debug_manager'):
|
||||
self.debug_mode = main_window.debug_manager._is_debug_mode()
|
||||
|
||||
def run(self):
|
||||
"""运行线程"""
|
||||
debug_mode = False
|
||||
|
||||
# 设置超时限制(分钟)
|
||||
timeout_minutes = 10
|
||||
max_execution_time = timeout_minutes * 60 # 转换为秒
|
||||
start_execution_time = time.time()
|
||||
|
||||
# 尝试检测是否处于调试模式
|
||||
if self.main_window and hasattr(self.main_window, 'debug_manager'):
|
||||
debug_mode = self.main_window.debug_manager._is_debug_mode()
|
||||
|
||||
# 检查超时的函数
|
||||
def check_timeout():
|
||||
elapsed = time.time() - start_execution_time
|
||||
if elapsed > max_execution_time:
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 哈希校验超时,已运行 {elapsed:.1f} 秒")
|
||||
return True
|
||||
return False
|
||||
|
||||
# 获取预期的哈希值
|
||||
expected_hash = self.plugin_hash.get(self.game_version, "")
|
||||
|
||||
if not expected_hash:
|
||||
logger.warning(f"DEBUG: 未找到 {self.game_version} 的预期哈希值")
|
||||
self.progress.emit(100)
|
||||
self.finished.emit(False, f"未找到 {self.game_version} 的预期哈希值", "")
|
||||
return
|
||||
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 开始验证补丁文件: {self.file_path}")
|
||||
logger.debug(f"DEBUG: 游戏版本: {self.game_version}")
|
||||
logger.debug(f"DEBUG: 预期哈希值: {expected_hash}")
|
||||
|
||||
try:
|
||||
# 检查文件是否存在
|
||||
if not os.path.exists(self.file_path):
|
||||
if debug_mode:
|
||||
logger.warning(f"DEBUG: 补丁文件不存在: {self.file_path}")
|
||||
self.progress.emit(100)
|
||||
self.finished.emit(False, f"补丁文件不存在: {self.file_path}", "")
|
||||
return
|
||||
|
||||
# 检查文件大小
|
||||
file_size = os.path.getsize(self.file_path)
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 补丁文件大小: {file_size} 字节")
|
||||
|
||||
if file_size == 0:
|
||||
if debug_mode:
|
||||
logger.warning(f"DEBUG: 补丁文件大小为0,无效文件")
|
||||
self.progress.emit(100)
|
||||
self.finished.emit(False, "补丁文件大小为0,无效文件", "")
|
||||
return
|
||||
|
||||
# 创建临时目录用于解压文件
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 创建临时目录: {temp_dir}")
|
||||
|
||||
# 发送进度信号 - 10%
|
||||
self.progress.emit(10)
|
||||
|
||||
# 解压补丁文件
|
||||
try:
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 开始解压文件: {self.file_path}")
|
||||
|
||||
# 确定目标文件名
|
||||
target_filename = None
|
||||
if "Vol.1" in self.game_version:
|
||||
target_filename = "adultsonly.xp3"
|
||||
elif "Vol.2" in self.game_version:
|
||||
target_filename = "adultsonly.xp3"
|
||||
elif "Vol.3" in self.game_version:
|
||||
target_filename = "update00.int"
|
||||
elif "Vol.4" in self.game_version:
|
||||
target_filename = "vol4adult.xp3"
|
||||
elif "After" in self.game_version:
|
||||
target_filename = "afteradult.xp3"
|
||||
|
||||
if not target_filename:
|
||||
if debug_mode:
|
||||
logger.warning(f"DEBUG: 未知的游戏版本: {self.game_version}")
|
||||
self.progress.emit(100)
|
||||
self.finished.emit(False, f"未知的游戏版本: {self.game_version}", "")
|
||||
return
|
||||
|
||||
with py7zr.SevenZipFile(self.file_path, mode="r") as archive:
|
||||
# 获取压缩包内文件列表
|
||||
file_list = archive.getnames()
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 压缩包内文件列表: {file_list}")
|
||||
|
||||
# 查找目标文件
|
||||
target_file_in_archive = None
|
||||
for file_path in file_list:
|
||||
if target_filename in file_path:
|
||||
target_file_in_archive = file_path
|
||||
break
|
||||
|
||||
if not target_file_in_archive:
|
||||
if debug_mode:
|
||||
logger.warning(f"DEBUG: 在压缩包中未找到目标文件: {target_filename}")
|
||||
# 尝试查找可能的替代文件
|
||||
alternative_files = []
|
||||
for file_path in file_list:
|
||||
if file_path.endswith('.xp3') or file_path.endswith('.int'):
|
||||
alternative_files.append(file_path)
|
||||
|
||||
if alternative_files:
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 找到可能的替代文件: {alternative_files}")
|
||||
target_file_in_archive = alternative_files[0]
|
||||
else:
|
||||
# 如果找不到任何替代文件,解压全部文件
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 未找到任何替代文件,解压全部文件")
|
||||
archive.extractall(path=temp_dir)
|
||||
|
||||
# 尝试在解压后的目录中查找目标文件
|
||||
for root, dirs, files in os.walk(temp_dir):
|
||||
for file in files:
|
||||
if file.endswith('.xp3') or file.endswith('.int'):
|
||||
patch_file = os.path.join(root, file)
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 找到可能的补丁文件: {patch_file}")
|
||||
break
|
||||
if patch_file:
|
||||
break
|
||||
|
||||
if not patch_file:
|
||||
if debug_mode:
|
||||
logger.warning(f"DEBUG: 未找到解压后的补丁文件")
|
||||
self.progress.emit(100)
|
||||
self.finished.emit(False, "未找到解压后的补丁文件", "")
|
||||
return
|
||||
else:
|
||||
# 只解压目标文件
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 解压目标文件: {target_file_in_archive}")
|
||||
archive.extract(path=temp_dir, targets=[target_file_in_archive])
|
||||
patch_file = os.path.join(temp_dir, target_file_in_archive)
|
||||
|
||||
# 发送进度信号 - 50%
|
||||
self.progress.emit(50)
|
||||
|
||||
# 如果还没有设置patch_file,尝试查找
|
||||
if not 'patch_file' in locals():
|
||||
if "Vol.1" in self.game_version:
|
||||
patch_file = os.path.join(temp_dir, "vol.1", "adultsonly.xp3")
|
||||
elif "Vol.2" in self.game_version:
|
||||
patch_file = os.path.join(temp_dir, "vol.2", "adultsonly.xp3")
|
||||
elif "Vol.3" in self.game_version:
|
||||
patch_file = os.path.join(temp_dir, "vol.3", "update00.int")
|
||||
elif "Vol.4" in self.game_version:
|
||||
patch_file = os.path.join(temp_dir, "vol.4", "vol4adult.xp3")
|
||||
elif "After" in self.game_version:
|
||||
patch_file = os.path.join(temp_dir, "after", "afteradult.xp3")
|
||||
|
||||
if not os.path.exists(patch_file):
|
||||
if debug_mode:
|
||||
logger.warning(f"DEBUG: 未找到解压后的补丁文件: {patch_file}")
|
||||
# 尝试查找可能的替代文件
|
||||
alternative_files = []
|
||||
for root, dirs, files in os.walk(temp_dir):
|
||||
for file in files:
|
||||
if file.endswith('.xp3') or file.endswith('.int'):
|
||||
alternative_files.append(os.path.join(root, file))
|
||||
if alternative_files:
|
||||
logger.debug(f"DEBUG: 找到可能的替代文件: {alternative_files}")
|
||||
patch_file = alternative_files[0]
|
||||
else:
|
||||
# 检查解压目录结构
|
||||
logger.debug(f"DEBUG: 检查解压目录结构:")
|
||||
for root, dirs, files in os.walk(temp_dir):
|
||||
logger.debug(f"DEBUG: 目录: {root}")
|
||||
logger.debug(f"DEBUG: 子目录: {dirs}")
|
||||
logger.debug(f"DEBUG: 文件: {files}")
|
||||
|
||||
if not os.path.exists(patch_file):
|
||||
self.progress.emit(100)
|
||||
self.finished.emit(False, f"未找到解压后的补丁文件", "")
|
||||
return
|
||||
|
||||
# 发送进度信号 - 70%
|
||||
self.progress.emit(70)
|
||||
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 找到解压后的补丁文件: {patch_file}")
|
||||
|
||||
# 计算补丁文件哈希值
|
||||
try:
|
||||
# 读取文件内容并计算哈希值,同时更新进度
|
||||
file_size = os.path.getsize(patch_file)
|
||||
|
||||
# 根据文件大小动态调整块大小
|
||||
# 文件越大,块越大,最大256MB
|
||||
chunk_size = min(256 * 1024 * 1024, max(16 * 1024 * 1024, file_size // 20))
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 文件大小: {file_size} 字节, 使用块大小: {chunk_size // (1024 * 1024)}MB")
|
||||
|
||||
hash_obj = hashlib.sha256()
|
||||
|
||||
with open(patch_file, "rb") as f:
|
||||
bytes_read = 0
|
||||
start_time = time.time()
|
||||
last_progress_time = start_time
|
||||
|
||||
while True:
|
||||
if self.isInterruptionRequested():
|
||||
break
|
||||
# 检查超时
|
||||
if check_timeout():
|
||||
logger.error(f"哈希计算超时,强制终止")
|
||||
self.progress.emit(100)
|
||||
self.finished.emit(
|
||||
False,
|
||||
f"{self.game_version} 哈希计算超时,已超过 {timeout_minutes} 分钟。请考虑跳过哈希校验或稍后再试。",
|
||||
""
|
||||
)
|
||||
return
|
||||
chunk = f.read(chunk_size)
|
||||
if not chunk:
|
||||
break
|
||||
hash_obj.update(chunk)
|
||||
bytes_read += len(chunk)
|
||||
|
||||
# 计算进度 (70-95%)
|
||||
progress = 70 + int(25 * bytes_read / file_size)
|
||||
self.progress.emit(min(95, progress))
|
||||
|
||||
# 每秒更新一次日志进度
|
||||
current_time = time.time()
|
||||
if debug_mode and current_time - last_progress_time >= 1.0:
|
||||
elapsed = current_time - start_time
|
||||
speed = bytes_read / (elapsed if elapsed > 0 else 1) / (1024 * 1024) # MB/s
|
||||
percent = bytes_read / file_size * 100
|
||||
logger.debug(f"DEBUG: 哈希计算进度 - {percent:.1f}% - 已处理: {bytes_read/(1024*1024):.1f}MB/{file_size/(1024*1024):.1f}MB - 速度: {speed:.1f}MB/s")
|
||||
last_progress_time = current_time
|
||||
|
||||
# 记录总用时
|
||||
if debug_mode:
|
||||
total_time = time.time() - start_time
|
||||
logger.debug(f"DEBUG: 哈希计算完成,耗时: {total_time:.1f}秒,平均速度: {file_size/(total_time*1024*1024):.1f}MB/s")
|
||||
|
||||
file_hash = hash_obj.hexdigest()
|
||||
|
||||
# 比较哈希值
|
||||
result = file_hash.lower() == expected_hash.lower()
|
||||
|
||||
# 发送进度信号 - 100%
|
||||
self.progress.emit(100)
|
||||
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 补丁文件 {patch_file} 哈希值验证: {'成功' if result else '失败'}")
|
||||
logger.debug(f"DEBUG: 预期哈希值: {expected_hash}")
|
||||
logger.debug(f"DEBUG: 实际哈希值: {file_hash}")
|
||||
|
||||
# 将验证结果和解压后的文件路径传递回去
|
||||
# 注意:由于使用了临时目录,此路径在函数返回后将不再有效
|
||||
# 但这里返回的路径只是用于标识验证成功,实际安装时会重新解压
|
||||
self.finished.emit(result, "" if result else "补丁文件哈希验证失败,文件可能已损坏或被篡改", patch_file if result else "")
|
||||
except Exception as e:
|
||||
if debug_mode:
|
||||
logger.error(f"DEBUG: 计算补丁文件哈希值失败: {e}")
|
||||
logger.error(f"DEBUG: 错误类型: {type(e).__name__}")
|
||||
self.progress.emit(100)
|
||||
self.finished.emit(False, f"计算补丁文件哈希值失败: {str(e)}", "")
|
||||
except Exception as e:
|
||||
if debug_mode:
|
||||
logger.error(f"DEBUG: 解压补丁文件失败: {e}")
|
||||
logger.error(f"DEBUG: 错误类型: {type(e).__name__}")
|
||||
logger.error(f"DEBUG: 错误堆栈: {traceback.format_exc()}")
|
||||
self.progress.emit(100)
|
||||
self.finished.emit(False, f"解压补丁文件失败: {str(e)}", "")
|
||||
return
|
||||
except Exception as e:
|
||||
if debug_mode:
|
||||
logger.error(f"DEBUG: 验证补丁哈希值失败: {e}")
|
||||
logger.error(f"DEBUG: 错误类型: {type(e).__name__}")
|
||||
logger.error(f"DEBUG: 错误堆栈: {traceback.format_exc()}" )
|
||||
self.progress.emit(100)
|
||||
self.finished.emit(False, f"验证补丁哈希值失败: {str(e)}", "")
|
||||
@@ -7,6 +7,11 @@ from urllib.parse import urlparse
|
||||
|
||||
from PySide6.QtCore import QThread, Signal
|
||||
from utils import resource_path
|
||||
from utils.logger import setup_logger
|
||||
from utils.url_censor import censor_url
|
||||
|
||||
# 初始化logger
|
||||
logger = setup_logger("ip_optimizer")
|
||||
|
||||
class IpOptimizer:
|
||||
def __init__(self):
|
||||
@@ -23,27 +28,46 @@ class IpOptimizer:
|
||||
最优的 IP 地址字符串,如果找不到则返回 None。
|
||||
"""
|
||||
try:
|
||||
# 解析URL,获取协议和主机名
|
||||
parsed_url = urlparse(url)
|
||||
protocol = parsed_url.scheme
|
||||
hostname = parsed_url.netloc
|
||||
|
||||
# 如果是HTTPS,可能需要特殊处理
|
||||
is_https = protocol.lower() == 'https'
|
||||
|
||||
logger.info(f"协议: {protocol}, 主机名: {hostname}, 是否HTTPS: {is_https}")
|
||||
|
||||
cst_path = resource_path("cfst.exe")
|
||||
if not os.path.exists(cst_path):
|
||||
print(f"错误: cfst.exe 未在资源路径中找到。")
|
||||
logger.error(f"错误: cfst.exe 未在资源路径中找到。")
|
||||
return None
|
||||
|
||||
ip_txt_path = resource_path("ip.txt")
|
||||
|
||||
# 正确的参数设置,根据cfst帮助文档
|
||||
# 隐藏敏感URL
|
||||
safe_url = "***URL protection***"
|
||||
|
||||
command = [
|
||||
cst_path,
|
||||
"-n", "1000", # 延迟测速线程数 (默认200)
|
||||
"-p", "1", # 显示结果数量 (默认10个)
|
||||
"-url", url, # 指定测速地址
|
||||
"-f", ip_txt_path, # IP文件
|
||||
"-dd", # 禁用下载测速,按延迟排序
|
||||
"-o"," " # 不写入结果文件
|
||||
"-n", "1000", # 延迟测速线程数
|
||||
"-p", "1", # 显示结果数量
|
||||
"-url", url,
|
||||
"-f", ip_txt_path,
|
||||
"-dd", # 禁用下载测速
|
||||
"-o"," " # 不写入结果文件
|
||||
]
|
||||
|
||||
# 创建用于显示的安全命令副本
|
||||
safe_command = command.copy()
|
||||
for i, arg in enumerate(safe_command):
|
||||
if arg == url:
|
||||
safe_command[i] = safe_url
|
||||
|
||||
creation_flags = subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0
|
||||
|
||||
print("--- CloudflareSpeedTest 开始执行 ---")
|
||||
logger.info("--- CloudflareSpeedTest 开始执行 ---")
|
||||
logger.info(f"执行命令: {' '.join(safe_command)}")
|
||||
|
||||
self.process = subprocess.Popen(
|
||||
command,
|
||||
@@ -57,22 +81,20 @@ class IpOptimizer:
|
||||
bufsize=0
|
||||
)
|
||||
|
||||
# 更新正则表达式以匹配cfst输出中的IP格式
|
||||
# 匹配格式: IP地址在行首,后面跟着一些数字和文本
|
||||
ip_pattern = re.compile(r'^(\d+\.\d+\.\d+\.\d+)\s+.*')
|
||||
|
||||
# 标记是否已经找到结果表头和完成标记
|
||||
found_header = False
|
||||
found_completion = False
|
||||
|
||||
stdout = self.process.stdout
|
||||
if not stdout:
|
||||
print("错误: 无法获取子进程的输出流。")
|
||||
logger.error("错误: 无法获取子进程的输出流。")
|
||||
return None
|
||||
|
||||
optimal_ip = None
|
||||
timeout_counter = 0
|
||||
max_timeout = 300 # 增加超时时间到5分钟
|
||||
max_timeout = 300 # 超时时间5分钟
|
||||
|
||||
while True:
|
||||
if self.process.poll() is not None:
|
||||
@@ -87,50 +109,47 @@ class IpOptimizer:
|
||||
if not ready or not line:
|
||||
timeout_counter += 1
|
||||
if timeout_counter > max_timeout:
|
||||
print("超时: CloudflareSpeedTest 响应超时")
|
||||
logger.warning("超时: CloudflareSpeedTest 响应超时")
|
||||
break
|
||||
time.sleep(1)
|
||||
continue
|
||||
|
||||
timeout_counter = 0
|
||||
|
||||
cleaned_line = line.strip()
|
||||
# 处理输出行,隐藏可能包含的URL
|
||||
# 临时禁用URL隐藏
|
||||
# cleaned_line = censor_url(line.strip())
|
||||
cleaned_line = line.strip() # 直接使用原始输出
|
||||
if cleaned_line:
|
||||
print(cleaned_line)
|
||||
logger.debug(cleaned_line)
|
||||
|
||||
# 检测结果表头
|
||||
if "IP 地址" in cleaned_line and "平均延迟" in cleaned_line:
|
||||
print("检测到IP结果表头,准备获取IP地址...")
|
||||
logger.info("检测到IP结果表头,准备获取IP地址...")
|
||||
found_header = True
|
||||
continue
|
||||
|
||||
# 检测完成标记
|
||||
if "完整测速结果已写入" in cleaned_line or "按下 回车键 或 Ctrl+C 退出" in cleaned_line:
|
||||
print("检测到测速完成信息")
|
||||
logger.info("检测到测速完成信息")
|
||||
found_completion = True
|
||||
|
||||
# 如果已经找到了IP,可以退出了
|
||||
if optimal_ip:
|
||||
break
|
||||
|
||||
# 已找到表头后,尝试匹配IP地址行
|
||||
if found_header:
|
||||
match = ip_pattern.search(cleaned_line)
|
||||
if match and not optimal_ip: # 只保存第一个匹配的IP(最优IP)
|
||||
if match and not optimal_ip:
|
||||
optimal_ip = match.group(1)
|
||||
print(f"找到最优 IP: {optimal_ip}")
|
||||
# 找到最优IP后立即退出循环,不等待完成标记
|
||||
logger.info(f"找到最优 IP: {optimal_ip}")
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
print(f"读取输出时发生错误: {e}")
|
||||
logger.error(f"读取输出时发生错误: {e}")
|
||||
break
|
||||
|
||||
# 确保完全读取输出后再发送退出信号
|
||||
if self.process and self.process.poll() is None:
|
||||
try:
|
||||
if self.process.stdin and not self.process.stdin.closed:
|
||||
print("发送退出信号...")
|
||||
logger.debug("发送退出信号...")
|
||||
self.process.stdin.write('\n')
|
||||
self.process.stdin.flush()
|
||||
except:
|
||||
@@ -138,11 +157,11 @@ class IpOptimizer:
|
||||
|
||||
self.stop()
|
||||
|
||||
print("--- CloudflareSpeedTest 执行结束 ---")
|
||||
logger.info("--- CloudflareSpeedTest 执行结束 ---")
|
||||
return optimal_ip
|
||||
|
||||
except Exception as e:
|
||||
print(f"执行 CloudflareSpeedTest 时发生错误: {e}")
|
||||
logger.error(f"执行 CloudflareSpeedTest 时发生错误: {e}")
|
||||
return None
|
||||
|
||||
def get_optimal_ipv6(self, url: str) -> str | None:
|
||||
@@ -156,30 +175,49 @@ class IpOptimizer:
|
||||
最优的 IPv6 地址字符串,如果找不到则返回 None。
|
||||
"""
|
||||
try:
|
||||
# 解析URL,获取协议和主机名
|
||||
parsed_url = urlparse(url)
|
||||
protocol = parsed_url.scheme
|
||||
hostname = parsed_url.netloc
|
||||
|
||||
# 如果是HTTPS,可能需要特殊处理
|
||||
is_https = protocol.lower() == 'https'
|
||||
|
||||
logger.info(f"IPv6优选 - 协议: {protocol}, 主机名: {hostname}, 是否HTTPS: {is_https}")
|
||||
|
||||
cst_path = resource_path("cfst.exe")
|
||||
if not os.path.exists(cst_path):
|
||||
print(f"错误: cfst.exe 未在资源路径中找到。")
|
||||
logger.error(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 未在资源路径中找到。")
|
||||
logger.error(f"错误: ipv6.txt 未在资源路径中找到。")
|
||||
return None
|
||||
|
||||
# 正确的参数设置,根据cfst帮助文档
|
||||
# 隐藏敏感URL
|
||||
safe_url = "***URL protection***"
|
||||
|
||||
command = [
|
||||
cst_path,
|
||||
"-n", "1000", # 延迟测速线程数,IPv6测试线程稍少
|
||||
"-p", "1", # 显示结果数量 (默认10个)
|
||||
"-url", url, # 指定测速地址
|
||||
"-f", ipv6_txt_path, # IPv6文件
|
||||
"-dd", # 禁用下载测速,按延迟排序
|
||||
"-n", "1000", # 延迟测速线程数
|
||||
"-p", "1", # 显示结果数量
|
||||
"-url", url,
|
||||
"-f", ipv6_txt_path,
|
||||
"-dd", # 禁用下载测速
|
||||
"-o", " " # 不写入结果文件
|
||||
]
|
||||
|
||||
# 创建用于显示的安全命令副本
|
||||
safe_command = command.copy()
|
||||
for i, arg in enumerate(safe_command):
|
||||
if arg == url:
|
||||
safe_command[i] = safe_url
|
||||
|
||||
creation_flags = subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0
|
||||
|
||||
print("--- CloudflareSpeedTest IPv6 开始执行 ---")
|
||||
logger.info("--- CloudflareSpeedTest IPv6 开始执行 ---")
|
||||
logger.info(f"执行命令: {' '.join(safe_command)}")
|
||||
|
||||
self.process = subprocess.Popen(
|
||||
command,
|
||||
@@ -193,22 +231,20 @@ class IpOptimizer:
|
||||
bufsize=0
|
||||
)
|
||||
|
||||
# 更新正则表达式以匹配cfst输出中的IPv6格式
|
||||
# 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("错误: 无法获取子进程的输出流。")
|
||||
logger.error("错误: 无法获取子进程的输出流。")
|
||||
return None
|
||||
|
||||
optimal_ipv6 = None
|
||||
timeout_counter = 0
|
||||
max_timeout = 300 # 增加超时时间到5分钟
|
||||
max_timeout = 300 # 超时时间5分钟
|
||||
|
||||
while True:
|
||||
if self.process.poll() is not None:
|
||||
@@ -223,50 +259,47 @@ class IpOptimizer:
|
||||
if not ready or not line:
|
||||
timeout_counter += 1
|
||||
if timeout_counter > max_timeout:
|
||||
print("超时: CloudflareSpeedTest IPv6 响应超时")
|
||||
logger.warning("超时: CloudflareSpeedTest IPv6 响应超时")
|
||||
break
|
||||
time.sleep(1)
|
||||
continue
|
||||
|
||||
timeout_counter = 0
|
||||
|
||||
cleaned_line = line.strip()
|
||||
# 处理输出行,隐藏可能包含的URL
|
||||
# 临时禁用URL隐藏
|
||||
# cleaned_line = censor_url(line.strip())
|
||||
cleaned_line = line.strip() # 直接使用原始输出
|
||||
if cleaned_line:
|
||||
print(cleaned_line)
|
||||
logger.debug(cleaned_line)
|
||||
|
||||
# 检测结果表头
|
||||
if "IP 地址" in cleaned_line and "平均延迟" in cleaned_line:
|
||||
print("检测到IPv6结果表头,准备获取IPv6地址...")
|
||||
logger.info("检测到IPv6结果表头,准备获取IPv6地址...")
|
||||
found_header = True
|
||||
continue
|
||||
|
||||
# 检测完成标记
|
||||
if "完整测速结果已写入" in cleaned_line or "按下 回车键 或 Ctrl+C 退出" in cleaned_line:
|
||||
print("检测到IPv6测速完成信息")
|
||||
logger.info("检测到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)
|
||||
if match and not optimal_ipv6:
|
||||
optimal_ipv6 = match.group(1)
|
||||
print(f"找到最优 IPv6: {optimal_ipv6}")
|
||||
# 找到最优IPv6后立即退出循环,不等待完成标记
|
||||
logger.info(f"找到最优 IPv6: {optimal_ipv6}")
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
print(f"读取输出时发生错误: {e}")
|
||||
logger.error(f"读取输出时发生错误: {e}")
|
||||
break
|
||||
|
||||
# 确保完全读取输出后再发送退出信号
|
||||
if self.process and self.process.poll() is None:
|
||||
try:
|
||||
if self.process.stdin and not self.process.stdin.closed:
|
||||
print("发送退出信号...")
|
||||
logger.debug("发送退出信号...")
|
||||
self.process.stdin.write('\n')
|
||||
self.process.stdin.flush()
|
||||
except:
|
||||
@@ -274,16 +307,16 @@ class IpOptimizer:
|
||||
|
||||
self.stop()
|
||||
|
||||
print("--- CloudflareSpeedTest IPv6 执行结束 ---")
|
||||
logger.info("--- CloudflareSpeedTest IPv6 执行结束 ---")
|
||||
return optimal_ipv6
|
||||
|
||||
except Exception as e:
|
||||
print(f"执行 CloudflareSpeedTest IPv6 时发生错误: {e}")
|
||||
logger.error(f"执行 CloudflareSpeedTest IPv6 时发生错误: {e}")
|
||||
return None
|
||||
|
||||
def stop(self):
|
||||
if self.process and self.process.poll() is None:
|
||||
print("正在终止 CloudflareSpeedTest 进程...")
|
||||
logger.info("正在终止 CloudflareSpeedTest 进程...")
|
||||
try:
|
||||
if self.process.stdin and not self.process.stdin.closed:
|
||||
self.process.stdin.write('\n')
|
||||
@@ -298,7 +331,7 @@ class IpOptimizer:
|
||||
except subprocess.TimeoutExpired:
|
||||
self.process.kill()
|
||||
self.process.wait()
|
||||
print("CloudflareSpeedTest 进程已终止。")
|
||||
logger.info("CloudflareSpeedTest 进程已终止。")
|
||||
|
||||
|
||||
class IpOptimizerThread(QThread):
|
||||
@@ -324,14 +357,3 @@ class IpOptimizerThread(QThread):
|
||||
|
||||
def stop(self):
|
||||
self.optimizer.stop()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 用于直接测试此模块
|
||||
test_url = "https://speed.cloudflare.com/__down?during=download&bytes=104857600"
|
||||
optimizer = IpOptimizer()
|
||||
ip = optimizer.get_optimal_ip(test_url)
|
||||
if ip:
|
||||
print(f"为 {test_url} 找到的最优 IP 是: {ip}")
|
||||
else:
|
||||
print(f"未能为 {test_url} 找到最优 IP。")
|
||||