Merge branch 'master' of https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT
8
.gitignore
vendored
@@ -180,3 +180,11 @@ 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,22 +1,58 @@
|
||||
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 data.config import LOG_FILE, APP_NAME
|
||||
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:
|
||||
@@ -24,18 +60,17 @@ if __name__ == "__main__":
|
||||
log_dir = os.path.dirname(LOG_FILE)
|
||||
if not os.path.exists(log_dir):
|
||||
os.makedirs(log_dir, exist_ok=True)
|
||||
logger.info(f"已创建日志目录: {log_dir}")
|
||||
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.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.info(f"调试模式已启用,日志文件路径: {os.path.abspath(LOG_FILE)}")
|
||||
logger.debug(f"调试模式已启用,日志文件路径: {os.path.abspath(LOG_FILE)}")
|
||||
except Exception as e:
|
||||
logger.error(f"创建日志文件失败: {e}")
|
||||
|
||||
@@ -45,6 +80,7 @@ if __name__ == "__main__":
|
||||
privacy_manager = PrivacyManager()
|
||||
except Exception as e:
|
||||
logger.error(f"初始化隐私协议管理器失败: {e}")
|
||||
logger.error(f"错误详情: {traceback.format_exc()}")
|
||||
QMessageBox.critical(
|
||||
None,
|
||||
"隐私协议加载错误",
|
||||
@@ -59,4 +95,4 @@ if __name__ == "__main__":
|
||||
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 |
@@ -4,7 +4,7 @@ import datetime
|
||||
|
||||
# 配置信息
|
||||
app_data = {
|
||||
"APP_VERSION": "1.3.2",
|
||||
"APP_VERSION": "1.4.0",
|
||||
"APP_NAME": "FRAISEMOE Addons Installer NEXT",
|
||||
"TEMP": "TEMP",
|
||||
"CACHE": "FRAISEMOE",
|
||||
@@ -46,29 +46,51 @@ 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_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
|
||||
|
||||
# 资源哈希值
|
||||
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"],
|
||||
@@ -20,6 +20,7 @@ PRIVACY_POLICY_BRIEF = """
|
||||
- **系统信息**:程序版本号。
|
||||
- **网络信息**:IP 地址、ISP、地理位置(用于使用统计)、下载统计、IPv6 连接测试(通过访问 testipv6.cn)、IPv6 地址获取(通过 ipw.cn)。
|
||||
- **文件信息**:游戏安装路径、文件哈希值。
|
||||
- **离线模式**:在离线模式下,本应用不会进行任何网络活动,仅使用本地文件进行安装。为实现此功能,应用会扫描其所在目录下的压缩包文件。
|
||||
|
||||
## 系统修改
|
||||
- 使用 Cloudflare 加速时会临时修改系统 hosts 文件。
|
||||
@@ -43,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.
|
||||
@@ -57,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():
|
||||
"""获取本地打包的隐私协议文件
|
||||
@@ -87,7 +89,7 @@ def get_local_privacy_policy():
|
||||
try:
|
||||
date_obj = datetime.strptime(date_str, '%Y年%m月%d日')
|
||||
date_version = date_obj.strftime('%Y.%m.%d')
|
||||
logger.info(f"成功读取本地隐私协议文件: {path}, 版本: {date_version}")
|
||||
logger.debug(f"成功读取本地隐私协议文件: {path}, 版本: {date_version}")
|
||||
return content, date_version, ""
|
||||
except ValueError:
|
||||
logger.error(f"本地隐私协议日期格式解析错误: {path}")
|
||||
@@ -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,149 +0,0 @@
|
||||
import os
|
||||
import sys
|
||||
from PySide6 import QtWidgets
|
||||
from data.config import LOG_FILE
|
||||
from utils.logger import setup_logger
|
||||
from utils import Logger
|
||||
import datetime
|
||||
from data.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 data.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.info(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.info(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.info(f"已创建日志文件: {os.path.abspath(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
|
||||
|
||||
logger.info(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.info("--- Debug mode disabled ---")
|
||||
sys.stdout = self.original_stdout
|
||||
sys.stderr = self.original_stderr
|
||||
self.logger.close()
|
||||
self.logger = None
|
||||
@@ -1,89 +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: 游戏版本名称
|
||||
"""
|
||||
# 检查是否处于离线模式
|
||||
is_offline = False
|
||||
if hasattr(self.main_window, 'offline_mode_manager'):
|
||||
is_offline = self.main_window.offline_mode_manager.is_in_offline_mode()
|
||||
|
||||
# 显示解压中的消息窗口
|
||||
self.main_window.hash_msg_box = self.main_window.hash_manager.hash_pop_window(
|
||||
check_type="offline_extraction" if is_offline else "extraction",
|
||||
is_offline=is_offline
|
||||
)
|
||||
|
||||
# 创建并启动解压线程
|
||||
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)
|
||||
@@ -3,7 +3,7 @@ from PySide6.QtWidgets import (
|
||||
QDialog, QVBoxLayout, QLabel, QListWidget, QPushButton, QHBoxLayout,
|
||||
QAbstractItemView, QRadioButton, QButtonGroup, QFileDialog, QMessageBox
|
||||
)
|
||||
from PySide6.QtCore import QObject
|
||||
from PySide6.QtCore import QObject, Signal, QThread
|
||||
from PySide6.QtGui import QFont
|
||||
from utils import msgbox_frame
|
||||
from utils.logger import setup_logger
|
||||
@@ -11,6 +11,20 @@ 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):
|
||||
"""
|
||||
处理补丁启用/禁用功能的类
|
||||
@@ -28,32 +42,59 @@ class PatchToggleHandler(QObject):
|
||||
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):
|
||||
"""
|
||||
处理禁/启用补丁按钮点击事件
|
||||
打开文件选择对话框选择游戏目录,然后禁用或启用对应游戏的补丁
|
||||
"""
|
||||
# 获取游戏目录
|
||||
debug_mode = self.debug_manager._is_debug_mode()
|
||||
selected_folder = QFileDialog.getExistingDirectory(self.main_window, "选择游戏上级目录", "")
|
||||
|
||||
# 提示用户选择目录
|
||||
file_dialog_info = "选择游戏上级目录" if debug_mode else "选择游戏目录"
|
||||
selected_folder = QFileDialog.getExistingDirectory(self.main_window, file_dialog_info, "")
|
||||
if not selected_folder:
|
||||
return
|
||||
|
||||
self.main_window.show_loading_dialog("正在识别游戏目录并检查补丁状态...")
|
||||
|
||||
if not selected_folder or selected_folder == "":
|
||||
return # 用户取消了选择
|
||||
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 debug_mode:
|
||||
logger.debug(f"DEBUG: 禁/启用功能 - 用户选择了目录: {selected_folder}")
|
||||
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)
|
||||
|
||||
# 首先尝试将选择的目录视为上级目录,使用增强的目录识别功能
|
||||
game_dirs = self.game_detector.identify_game_directories_improved(selected_folder)
|
||||
if not selected_games:
|
||||
return
|
||||
|
||||
if game_dirs and len(game_dirs) > 0:
|
||||
self._handle_multiple_games(game_dirs, debug_mode)
|
||||
else:
|
||||
self._handle_single_game(selected_folder, debug_mode)
|
||||
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):
|
||||
"""
|
||||
@@ -3,7 +3,7 @@ from PySide6.QtWidgets import (
|
||||
QDialog, QVBoxLayout, QLabel, QListWidget, QPushButton, QHBoxLayout,
|
||||
QAbstractItemView, QFileDialog, QMessageBox
|
||||
)
|
||||
from PySide6.QtCore import QObject
|
||||
from PySide6.QtCore import QObject, Signal, QThread
|
||||
from PySide6.QtGui import QFont
|
||||
from utils import msgbox_frame
|
||||
from utils.logger import setup_logger
|
||||
@@ -11,6 +11,20 @@ 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):
|
||||
"""
|
||||
处理补丁卸载功能的类
|
||||
@@ -28,6 +42,12 @@ class UninstallHandler(QObject):
|
||||
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):
|
||||
"""
|
||||
@@ -37,23 +57,84 @@ class UninstallHandler(QObject):
|
||||
# 获取游戏目录
|
||||
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"DEBUG: 卸载功能 - 用户选择了目录: {selected_folder}")
|
||||
logger.debug(f"卸载功能 - 用户选择了目录: {selected_folder}")
|
||||
|
||||
# 首先尝试将选择的目录视为上级目录,使用增强的目录识别功能
|
||||
game_dirs = self.game_detector.identify_game_directories_improved(selected_folder)
|
||||
|
||||
if game_dirs and len(game_dirs) > 0:
|
||||
self._handle_multiple_games(game_dirs, debug_mode)
|
||||
# 使用UI管理器显示加载对话框
|
||||
if hasattr(self.main_window, 'ui_manager'):
|
||||
self.main_window.ui_manager.show_loading_dialog("正在识别游戏目录...")
|
||||
else:
|
||||
self._handle_single_game(selected_folder, debug_mode)
|
||||
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):
|
||||
"""
|
||||
@@ -67,15 +148,26 @@ class UninstallHandler(QObject):
|
||||
logger.debug(f"DEBUG: 卸载功能 - 在上级目录中找到以下游戏: {list(game_dirs.keys())}")
|
||||
|
||||
# 查找已安装补丁的游戏,只处理那些已安装补丁的游戏
|
||||
logger.info("检查哪些游戏已安装补丁")
|
||||
games_with_patch = {}
|
||||
for game_version, game_dir in game_dirs.items():
|
||||
if self.patch_manager.check_patch_installed(game_dir, game_version):
|
||||
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} 已安装补丁")
|
||||
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}",
|
||||
@@ -85,16 +177,31 @@ class UninstallHandler(QObject):
|
||||
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}",
|
||||
@@ -104,10 +211,26 @@ class UninstallHandler(QObject):
|
||||
)
|
||||
|
||||
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):
|
||||
@@ -122,15 +245,28 @@ class UninstallHandler(QObject):
|
||||
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}")
|
||||
|
||||
# 检查是否已安装补丁
|
||||
if self.patch_manager.check_patch_installed(selected_folder, 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}",
|
||||
@@ -140,11 +276,37 @@ class UninstallHandler(QObject):
|
||||
)
|
||||
|
||||
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,
|
||||
@@ -154,8 +316,9 @@ class UninstallHandler(QObject):
|
||||
)
|
||||
else:
|
||||
# 两种方式都未识别到游戏
|
||||
logger.info("无法识别游戏")
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 卸载功能 - 无法识别游戏")
|
||||
logger.debug(f"DEBUG: 卸载功能 - 无法识别游戏,显示错误消息")
|
||||
|
||||
msg_box = msgbox_frame(
|
||||
f"错误 - {self.app_name}",
|
||||
@@ -182,7 +345,7 @@ class UninstallHandler(QObject):
|
||||
|
||||
# 添加"已安装补丁的游戏"标签
|
||||
already_installed_label = QLabel("已安装补丁的游戏:", dialog)
|
||||
already_installed_label.setFont(QFont(already_installed_label.font().family(), already_installed_label.font().pointSize(), QFont.Bold))
|
||||
already_installed_label.setFont(QFont(already_installed_label.font().family(), already_installed_label.font().pointSize(), QFont.Weight.Bold))
|
||||
layout.addWidget(already_installed_label)
|
||||
|
||||
# 添加已安装游戏列表(可选,这里使用静态标签替代,保持一致性)
|
||||
@@ -195,7 +358,7 @@ class UninstallHandler(QObject):
|
||||
|
||||
# 添加"请选择要卸载补丁的游戏"标签
|
||||
info_label = QLabel("请选择要卸载补丁的游戏:", dialog)
|
||||
info_label.setFont(QFont(info_label.font().family(), info_label.font().pointSize(), QFont.Bold))
|
||||
info_label.setFont(QFont(info_label.font().family(), info_label.font().pointSize(), QFont.Weight.Bold))
|
||||
layout.addWidget(info_label)
|
||||
|
||||
# 添加列表控件,只显示已安装补丁的游戏
|
||||
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',
|
||||
]
|
||||
@@ -176,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:
|
||||
@@ -188,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)
|
||||
|
||||
@@ -358,7 +358,7 @@ class MultiStageAnimations(QObject):
|
||||
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)
|
||||
|
||||
@@ -75,35 +75,25 @@ class CloudflareOptimizer:
|
||||
# 解析域名
|
||||
hostname = urlparse(url).hostname
|
||||
|
||||
# 检查hosts文件中是否已有该域名的IP记录
|
||||
existing_ips = self.hosts_manager.get_hostname_entries(hostname) if hostname else []
|
||||
|
||||
# 判断是否继续优选的逻辑
|
||||
if existing_ips and self.has_optimized_in_session:
|
||||
# 如果本次会话中已执行过优选且hosts中存在记录,则跳过优选过程
|
||||
logger.info(f"发现hosts文件中已有域名 {hostname} 的优选IP记录且本次会话已优选过,跳过优选过程")
|
||||
if self.has_optimized_in_session:
|
||||
# 如果本次会话中已执行过优选,则跳过优选过程
|
||||
logger.info("本次会话已执行过优选,跳过优选过程")
|
||||
|
||||
# 设置标记为已优选完成
|
||||
self.optimization_done = True
|
||||
self.countdown_finished = True
|
||||
|
||||
# 尝试获取现有的IPv4和IPv6地址
|
||||
ipv4_entries = [ip for ip in existing_ips if ':' not in ip] # IPv4地址不含冒号
|
||||
ipv6_entries = [ip for ip in existing_ips if ':' in ip] # IPv6地址包含冒号
|
||||
|
||||
if ipv4_entries:
|
||||
self.optimized_ip = ipv4_entries[0]
|
||||
if ipv6_entries:
|
||||
self.optimized_ipv6 = ipv6_entries[0]
|
||||
|
||||
logger.info(f"使用已存在的优选IP - IPv4: {self.optimized_ip}, IPv6: {self.optimized_ipv6}")
|
||||
return True
|
||||
else:
|
||||
# 如果本次会话尚未优选过,或hosts中没有记录,则显示优选窗口
|
||||
if existing_ips:
|
||||
logger.info(f"发现hosts文件中已有域名 {hostname} 的优选IP记录,但本次会话尚未优选过")
|
||||
# 清理已有的hosts记录,准备重新优选
|
||||
self.hosts_manager.clean_hostname_entries(hostname)
|
||||
# 如果本次会话尚未优选过,则清理可能存在的旧记录
|
||||
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
|
||||
@@ -122,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():
|
||||
@@ -160,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():
|
||||
@@ -209,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(
|
||||
@@ -282,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)
|
||||
@@ -339,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
|
||||
|
||||
# 用户点击了继续,重新禁用主窗口
|
||||
@@ -376,9 +371,6 @@ class CloudflareOptimizer:
|
||||
from utils import save_config
|
||||
save_config(self.main_window.config)
|
||||
|
||||
# 记录本次会话已执行过优选
|
||||
self.has_optimized_in_session = True
|
||||
|
||||
if success:
|
||||
msg_box = QtWidgets.QMessageBox(self.main_window)
|
||||
msg_box.setWindowTitle(f"成功 - {self.main_window.APP_NAME}")
|
||||
@@ -413,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(
|
||||
@@ -422,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
|
||||
|
||||
# 用户点击了继续,重新禁用主窗口
|
||||
@@ -210,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',
|
||||
]
|
||||
@@ -6,17 +6,22 @@ from urllib.parse import urlparse
|
||||
import re
|
||||
|
||||
from PySide6 import QtWidgets, QtCore
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtCore import Qt, QTimer
|
||||
from PySide6.QtGui import QIcon, QPixmap, QFont
|
||||
from PySide6.QtWidgets import QPushButton, QDialog, QHBoxLayout
|
||||
|
||||
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 config.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
|
||||
from core.managers.cloudflare_optimizer import CloudflareOptimizer
|
||||
from .download_task_manager import DownloadTaskManager
|
||||
from core.handlers.extraction_handler import ExtractionHandler
|
||||
from utils.logger import setup_logger
|
||||
from utils.url_censor import censor_url
|
||||
from utils.helpers import (
|
||||
HashManager, AdminPrivileges, msgbox_frame, HostsManager
|
||||
)
|
||||
from workers.download import DownloadThread, ProgressWindow
|
||||
|
||||
# 初始化logger
|
||||
logger = setup_logger("download_manager")
|
||||
@@ -41,6 +46,12 @@ class DownloadManager:
|
||||
self.download_task_manager = DownloadTaskManager(main_window, self.download_thread_level)
|
||||
self.extraction_handler = ExtractionHandler(main_window)
|
||||
|
||||
self.extraction_thread = None
|
||||
self.progress_window = None
|
||||
|
||||
# 调试管理器
|
||||
self.debug_manager = getattr(main_window, 'debug_manager', None)
|
||||
|
||||
def file_dialog(self):
|
||||
"""显示文件夹选择对话框,选择游戏安装目录"""
|
||||
self.selected_folder = QtWidgets.QFileDialog.getExistingDirectory(
|
||||
@@ -52,7 +63,8 @@ class DownloadManager:
|
||||
)
|
||||
return
|
||||
|
||||
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_INSTALLING)
|
||||
|
||||
self.main_window.setEnabled(False)
|
||||
|
||||
@@ -109,13 +121,40 @@ class DownloadManager:
|
||||
logger.debug(f"DEBUG: Parsed JSON data: {json.dumps(safe_config, indent=2)}")
|
||||
|
||||
urls = {}
|
||||
missing_urls = []
|
||||
|
||||
# 检查每个游戏版本的URL
|
||||
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"]
|
||||
else:
|
||||
missing_urls.append(f"NEKOPARA Vol.{i+1}")
|
||||
if self.is_debug_mode():
|
||||
logger.warning(f"DEBUG: 未找到 NEKOPARA Vol.{i+1} 的下载URL")
|
||||
|
||||
# 检查After的URL
|
||||
if "after.data" in config_data and "url" in config_data["after.data"]:
|
||||
urls["after"] = config_data["after.data"]["url"]
|
||||
else:
|
||||
missing_urls.append("NEKOPARA After")
|
||||
if self.is_debug_mode():
|
||||
logger.warning(f"DEBUG: 未找到 NEKOPARA After 的下载URL")
|
||||
|
||||
# 如果有缺失的URL,记录详细信息
|
||||
if missing_urls:
|
||||
if self.is_debug_mode():
|
||||
logger.warning(f"DEBUG: 以下游戏版本缺少下载URL: {', '.join(missing_urls)}")
|
||||
logger.warning(f"DEBUG: 当前云端配置中的键: {list(config_data.keys())}")
|
||||
|
||||
# 检查每个游戏数据是否包含url键
|
||||
for i in range(4):
|
||||
key = f"vol.{i+1}.data"
|
||||
if key in config_data:
|
||||
logger.warning(f"DEBUG: {key} 内容: {list(config_data[key].keys())}")
|
||||
|
||||
if "after.data" in config_data:
|
||||
logger.warning(f"DEBUG: after.data 内容: {list(config_data['after.data'].keys())}")
|
||||
|
||||
if len(urls) != 5:
|
||||
missing_keys_map = {
|
||||
@@ -128,7 +167,17 @@ class DownloadManager:
|
||||
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():
|
||||
logger.warning(f"DEBUG: 缺失的URL键: {missing_original_keys}")
|
||||
|
||||
# 如果所有URL都缺失,可能是云端配置问题
|
||||
if len(urls) == 0:
|
||||
raise ValueError(f"配置文件缺少所有下载URL键: {', '.join(missing_original_keys)}")
|
||||
|
||||
# 否则只是部分缺失,可以继续使用已有的URL
|
||||
logger.warning(f"配置文件缺少部分键: {', '.join(missing_original_keys)}")
|
||||
|
||||
if self.is_debug_mode():
|
||||
# 创建安全版本的URL字典用于调试输出
|
||||
@@ -201,44 +250,65 @@ class DownloadManager:
|
||||
return safe_config
|
||||
|
||||
def download_action(self):
|
||||
"""开始下载流程"""
|
||||
self.main_window.download_queue_history = []
|
||||
|
||||
# 清除游戏检测器的目录缓存,确保获取最新的目录状态
|
||||
if hasattr(self.main_window, 'game_detector') and hasattr(self.main_window.game_detector, 'clear_directory_cache'):
|
||||
self.main_window.game_detector.clear_directory_cache()
|
||||
if self.is_debug_mode():
|
||||
logger.debug("DEBUG: 已清除游戏目录缓存,确保获取最新状态")
|
||||
|
||||
game_dirs = self.main_window.game_detector.identify_game_directories_improved(self.selected_folder)
|
||||
|
||||
debug_mode = self.is_debug_mode()
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 开始下载流程, 识别到 {len(game_dirs)} 个游戏目录")
|
||||
|
||||
if not game_dirs:
|
||||
if debug_mode:
|
||||
logger.warning("DEBUG: 未识别到任何游戏目录,设置目录未找到错误")
|
||||
self.main_window.last_error_message = "directory_not_found"
|
||||
"""下载操作的主入口点"""
|
||||
if not self.selected_folder:
|
||||
QtWidgets.QMessageBox.warning(
|
||||
self.main_window,
|
||||
f"目录错误 - {APP_NAME}",
|
||||
"\n未能识别到任何游戏目录。\n\n请确认您选择的是游戏的上级目录,并且该目录中包含NEKOPARA系列游戏文件夹。\n"
|
||||
self.main_window, f"通知 - {APP_NAME}", "\n未选择任何目录,请重新选择\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", is_offline=False)
|
||||
# 识别游戏目录
|
||||
game_dirs = self.main_window.game_detector.identify_game_directories_improved(self.selected_folder)
|
||||
|
||||
if not game_dirs:
|
||||
QtWidgets.QMessageBox.warning(
|
||||
self.main_window, f"通知 - {APP_NAME}", "\n未在选择的目录中找到支持的游戏\n"
|
||||
)
|
||||
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)
|
||||
return
|
||||
|
||||
# 检查是否禁用了安装前哈希预检查
|
||||
config = getattr(self.main_window, 'config', {})
|
||||
disable_pre_hash = False
|
||||
if isinstance(config, dict):
|
||||
disable_pre_hash = config.get("disable_pre_hash_check", False)
|
||||
|
||||
debug_mode = self.is_debug_mode()
|
||||
|
||||
if disable_pre_hash:
|
||||
if debug_mode:
|
||||
logger.debug("DEBUG: 哈希预检查已被用户禁用,跳过预检查")
|
||||
# 直接跳过哈希预检查,进入安装流程
|
||||
# 创建一个空的安装状态字典,所有游戏都标记为未安装
|
||||
updated_status = {}
|
||||
for game in game_dirs.keys():
|
||||
updated_status[game] = False
|
||||
|
||||
# 直接调用预检查完成的处理方法
|
||||
self.on_pre_hash_finished_with_dirs(updated_status, game_dirs)
|
||||
else:
|
||||
# 关闭可能存在的哈希校验窗口
|
||||
self.main_window.close_hash_msg_box()
|
||||
|
||||
# 显示文件检验窗口
|
||||
self.main_window.hash_msg_box = self.main_window.hash_manager.hash_pop_window(
|
||||
check_type="pre",
|
||||
auto_close=True, # 添加自动关闭参数
|
||||
close_delay=1000 # 1秒后自动关闭
|
||||
)
|
||||
|
||||
# 获取安装路径
|
||||
install_paths = self.get_install_paths()
|
||||
|
||||
# 创建并启动哈希线程进行预检查
|
||||
self.main_window.hash_thread = self.main_window.patch_detector.create_hash_thread("pre", install_paths)
|
||||
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()
|
||||
|
||||
install_paths = self.get_install_paths()
|
||||
|
||||
self.main_window.hash_thread = self.main_window.create_hash_thread("pre", install_paths)
|
||||
self.main_window.hash_thread.pre_finished.connect(
|
||||
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):
|
||||
"""优化的哈希预检查完成处理,带有游戏目录信息
|
||||
|
||||
@@ -247,44 +317,16 @@ class DownloadManager:
|
||||
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.close_hash_msg_box()
|
||||
|
||||
debug_mode = self.is_debug_mode()
|
||||
|
||||
self.main_window.setEnabled(True)
|
||||
|
||||
installable_games = []
|
||||
already_installed_games = []
|
||||
disabled_patch_games = [] # 存储检测到禁用补丁的游戏
|
||||
|
||||
for game_version, game_dir in game_dirs.items():
|
||||
# 首先通过文件检查确认补丁是否已安装
|
||||
is_patch_installed = self.main_window.patch_manager.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.info(f"DEBUG: {game_version} 已安装补丁,不需要再次安装")
|
||||
logger.info(f"DEBUG: 文件检查结果: {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.main_window.patch_manager.check_patch_disabled(game_dir, game_version)
|
||||
if is_disabled:
|
||||
if debug_mode:
|
||||
logger.info(f"DEBUG: {game_version} 存在被禁用的补丁: {disabled_path}")
|
||||
disabled_patch_games.append(game_version)
|
||||
else:
|
||||
if debug_mode:
|
||||
logger.info(f"DEBUG: {game_version} 未安装补丁,可以安装")
|
||||
logger.info(f"DEBUG: 文件检查结果: {is_patch_installed}, 哈希检查结果: {hash_check_passed}")
|
||||
installable_games.append(game_version)
|
||||
# 使用patch_detector检测可安装的游戏
|
||||
already_installed_games, installable_games, disabled_patch_games = self.main_window.patch_detector.detect_installable_games(game_dirs)
|
||||
|
||||
status_message = ""
|
||||
if already_installed_games:
|
||||
@@ -328,108 +370,193 @@ class DownloadManager:
|
||||
already_installed_games.append(game_version)
|
||||
else:
|
||||
if debug_mode:
|
||||
logger.info(f"DEBUG: 用户选择不启用被禁用的补丁,这些游戏将被添加到可安装列表")
|
||||
logger.debug(f"用户选择不启用被禁用的补丁,这些游戏将被添加到可安装列表")
|
||||
# 用户选择不启用,将这些游戏视为可以安装补丁
|
||||
installable_games.extend(disabled_patch_games)
|
||||
|
||||
# 更新status_message
|
||||
if already_installed_games:
|
||||
status_message = f"已安装补丁的游戏:\n{chr(10).join(already_installed_games)}\n\n"
|
||||
|
||||
# 如果有可安装的游戏,显示选择对话框
|
||||
if installable_games:
|
||||
# 创建游戏选择对话框
|
||||
dialog = QtWidgets.QDialog(self.main_window)
|
||||
dialog.setWindowTitle(f"选择要安装的游戏 - {APP_NAME}")
|
||||
dialog.setMinimumWidth(400)
|
||||
dialog.setMinimumHeight(300)
|
||||
|
||||
if not installable_games:
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
|
||||
# 添加说明标签
|
||||
label = QtWidgets.QLabel("请选择要安装的游戏:")
|
||||
layout.addWidget(label)
|
||||
|
||||
# 添加已安装游戏的状态提示
|
||||
if already_installed_games:
|
||||
installed_label = QtWidgets.QLabel(status_message)
|
||||
installed_label.setStyleSheet("color: green;")
|
||||
layout.addWidget(installed_label)
|
||||
|
||||
# 创建列表控件
|
||||
list_widget = QtWidgets.QListWidget()
|
||||
list_widget.setSelectionMode(QtWidgets.QAbstractItemView.SelectionMode.MultiSelection)
|
||||
|
||||
# 添加可安装的游戏
|
||||
for game in installable_games:
|
||||
item = QtWidgets.QListWidgetItem(game)
|
||||
item.setSelected(True) # 默认全选
|
||||
list_widget.addItem(item)
|
||||
|
||||
layout.addWidget(list_widget)
|
||||
|
||||
# 添加全选/取消全选按钮
|
||||
select_all_layout = QtWidgets.QHBoxLayout()
|
||||
select_all_button = QtWidgets.QPushButton("全选")
|
||||
deselect_all_button = QtWidgets.QPushButton("取消全选")
|
||||
select_all_layout.addWidget(select_all_button)
|
||||
select_all_layout.addWidget(deselect_all_button)
|
||||
select_all_layout.addStretch() # 添加弹性空间,将按钮左对齐
|
||||
layout.addLayout(select_all_layout)
|
||||
|
||||
# 添加主要操作按钮
|
||||
button_layout = QtWidgets.QHBoxLayout()
|
||||
ok_button = QtWidgets.QPushButton("确定")
|
||||
cancel_button = QtWidgets.QPushButton("取消")
|
||||
button_layout.addWidget(ok_button)
|
||||
button_layout.addWidget(cancel_button)
|
||||
layout.addLayout(button_layout)
|
||||
|
||||
dialog.setLayout(layout)
|
||||
|
||||
# 全选功能的实现
|
||||
def select_all_items():
|
||||
"""选择所有游戏项目"""
|
||||
for i in range(list_widget.count()):
|
||||
item = list_widget.item(i)
|
||||
item.setSelected(True)
|
||||
|
||||
def deselect_all_items():
|
||||
"""取消选择所有游戏项目"""
|
||||
for i in range(list_widget.count()):
|
||||
item = list_widget.item(i)
|
||||
item.setSelected(False)
|
||||
|
||||
# 连接按钮信号
|
||||
select_all_button.clicked.connect(select_all_items)
|
||||
deselect_all_button.clicked.connect(deselect_all_items)
|
||||
ok_button.clicked.connect(dialog.accept)
|
||||
cancel_button.clicked.connect(dialog.reject)
|
||||
|
||||
# 显示对话框
|
||||
if dialog.exec() == QtWidgets.QDialog.DialogCode.Accepted:
|
||||
selected_games = [item.text() for item in list_widget.selectedItems()]
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 用户选择了以下游戏进行安装: {selected_games}")
|
||||
|
||||
selected_game_dirs = {game: game_dirs[game] for game in selected_games if game in game_dirs}
|
||||
|
||||
self.main_window.setEnabled(False)
|
||||
|
||||
# 检查是否处于离线模式
|
||||
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()
|
||||
|
||||
if is_offline_mode:
|
||||
if debug_mode:
|
||||
logger.debug("使用离线模式,跳过网络配置获取")
|
||||
self._fill_offline_download_queue(selected_game_dirs)
|
||||
else:
|
||||
# 在线模式下,重新获取云端配置
|
||||
if hasattr(self.main_window, 'fetch_cloud_config'):
|
||||
if debug_mode:
|
||||
logger.debug("重新获取云端配置以确保URL最新")
|
||||
# 重新获取云端配置并继续下载流程
|
||||
from workers.config_fetch_thread import ConfigFetchThread
|
||||
self.main_window.config_manager.fetch_cloud_config(
|
||||
ConfigFetchThread,
|
||||
lambda data, error: self._continue_download_after_config_fetch(data, error, selected_game_dirs)
|
||||
)
|
||||
else:
|
||||
# 如果无法重新获取配置,使用当前配置
|
||||
config = self.get_download_url()
|
||||
self._continue_download_with_config(config, selected_game_dirs)
|
||||
else:
|
||||
if debug_mode:
|
||||
logger.debug("DEBUG: 用户取消了游戏选择")
|
||||
if hasattr(self.main_window, 'window_manager'):
|
||||
self.main_window.window_manager.change_window_state(self.main_window.window_manager.STATE_READY)
|
||||
else:
|
||||
# 如果没有可安装的游戏,显示提示
|
||||
if already_installed_games:
|
||||
msg = f"所有游戏已安装补丁,无需重复安装。\n\n已安装的游戏:\n{chr(10).join(already_installed_games)}"
|
||||
else:
|
||||
msg = "未检测到可安装的游戏。"
|
||||
|
||||
QtWidgets.QMessageBox.information(
|
||||
self.main_window,
|
||||
f"信息 - {APP_NAME}",
|
||||
f"\n所有检测到的游戏都已安装补丁。\n\n{status_message}"
|
||||
self.main_window,
|
||||
f"通知 - {APP_NAME}",
|
||||
msg
|
||||
)
|
||||
if hasattr(self.main_window, 'window_manager'):
|
||||
self.main_window.window_manager.change_window_state(self.main_window.window_manager.STATE_READY)
|
||||
|
||||
def _continue_download_after_config_fetch(self, data, error, selected_game_dirs):
|
||||
"""云端配置获取完成后继续下载流程
|
||||
|
||||
Args:
|
||||
data: 获取到的配置数据
|
||||
error: 错误信息
|
||||
selected_game_dirs: 选择的游戏目录
|
||||
"""
|
||||
debug_mode = self.is_debug_mode()
|
||||
|
||||
if error:
|
||||
if debug_mode:
|
||||
logger.error(f"DEBUG: 重新获取云端配置失败: {error}")
|
||||
# 使用当前配置
|
||||
config = self.get_download_url()
|
||||
else:
|
||||
# 使用新获取的配置
|
||||
self.main_window.cloud_config = data
|
||||
config = self.get_download_url()
|
||||
|
||||
self._continue_download_with_config(config, selected_game_dirs)
|
||||
|
||||
def _continue_download_with_config(self, config, selected_game_dirs):
|
||||
"""使用配置继续下载流程
|
||||
|
||||
Args:
|
||||
config: 下载配置
|
||||
selected_game_dirs: 选择的游戏目录
|
||||
"""
|
||||
debug_mode = self.is_debug_mode()
|
||||
|
||||
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("开始安装")
|
||||
if hasattr(self.main_window, 'window_manager'):
|
||||
self.main_window.window_manager.change_window_state(self.main_window.window_manager.STATE_READY)
|
||||
return
|
||||
|
||||
dialog = QtWidgets.QDialog(self.main_window)
|
||||
dialog.setWindowTitle("选择要安装的游戏")
|
||||
dialog.resize(400, 300)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(dialog)
|
||||
|
||||
if already_installed_games:
|
||||
already_installed_label = QtWidgets.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)
|
||||
|
||||
already_installed_list = QtWidgets.QLabel(chr(10).join(already_installed_games), dialog)
|
||||
layout.addWidget(already_installed_list)
|
||||
|
||||
layout.addSpacing(10)
|
||||
|
||||
info_label = QtWidgets.QLabel("请选择你需要安装补丁的游戏:", dialog)
|
||||
info_label.setFont(QFont(info_label.font().family(), info_label.font().pointSize(), QFont.Weight.Bold))
|
||||
layout.addWidget(info_label)
|
||||
|
||||
list_widget = QtWidgets.QListWidget(dialog)
|
||||
list_widget.setSelectionMode(QtWidgets.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:
|
||||
logger.debug(f"DEBUG: 用户选择了以下游戏进行安装: {selected_games}")
|
||||
|
||||
selected_game_dirs = {game: game_dirs[game] for game in selected_games if game in game_dirs}
|
||||
|
||||
self.main_window.setEnabled(False)
|
||||
|
||||
self._fill_download_queue(config, selected_game_dirs)
|
||||
|
||||
if not self.download_queue:
|
||||
# 所有下载任务都已完成,进行后检查
|
||||
if debug_mode:
|
||||
logger.debug("DEBUG: 所有下载任务完成,进行后检查")
|
||||
# 使用patch_detector进行安装后哈希比较
|
||||
self.main_window.patch_detector.after_hash_compare()
|
||||
return
|
||||
|
||||
# 检查是否处于离线模式
|
||||
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()
|
||||
|
||||
if is_offline_mode:
|
||||
if debug_mode:
|
||||
logger.info("DEBUG: 使用离线模式,跳过网络配置获取")
|
||||
self._fill_offline_download_queue(selected_game_dirs)
|
||||
else:
|
||||
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
|
||||
|
||||
# 如果是离线模式,直接开始下一个下载任务
|
||||
if is_offline_mode:
|
||||
if debug_mode:
|
||||
logger.info("DEBUG: 离线模式,跳过Cloudflare优化")
|
||||
logger.debug("离线模式,跳过Cloudflare优化")
|
||||
self.next_download_task()
|
||||
else:
|
||||
self._show_cloudflare_option()
|
||||
@@ -544,37 +671,25 @@ class DownloadManager:
|
||||
"""显示Cloudflare加速选择对话框"""
|
||||
if self.download_queue:
|
||||
first_url = self.download_queue[0][0]
|
||||
hostname = urlparse(first_url).hostname
|
||||
|
||||
if hostname:
|
||||
existing_ips = self.cloudflare_optimizer.hosts_manager.get_hostname_entries(hostname)
|
||||
# 直接检查是否本次会话已执行过优选
|
||||
if self.cloudflare_optimizer.has_optimized_in_session:
|
||||
logger.info("本次会话已执行过优选,跳过询问直接使用")
|
||||
|
||||
self.cloudflare_optimizer.optimization_done = True
|
||||
self.cloudflare_optimizer.countdown_finished = True
|
||||
|
||||
self.main_window.current_url = first_url
|
||||
self.next_download_task()
|
||||
return
|
||||
|
||||
if existing_ips:
|
||||
logger.info(f"发现hosts文件中已有域名 {hostname} 的优选IP记录,跳过询问直接使用")
|
||||
|
||||
self.cloudflare_optimizer.optimization_done = True
|
||||
self.cloudflare_optimizer.countdown_finished = True
|
||||
|
||||
ipv4_entries = [ip for ip in existing_ips if ':' not in ip]
|
||||
ipv6_entries = [ip for ip in existing_ips if ':' in ip]
|
||||
|
||||
if ipv4_entries:
|
||||
self.cloudflare_optimizer.optimized_ip = ipv4_entries[0]
|
||||
if ipv6_entries:
|
||||
self.cloudflare_optimizer.optimized_ipv6 = ipv6_entries[0]
|
||||
|
||||
self.main_window.current_url = first_url
|
||||
|
||||
self.next_download_task()
|
||||
return
|
||||
|
||||
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文件,请注意放行。")
|
||||
|
||||
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():
|
||||
@@ -593,7 +708,8 @@ class DownloadManager:
|
||||
clicked_button = msg_box.clickedButton()
|
||||
if clicked_button == 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)
|
||||
self.download_queue.clear()
|
||||
return
|
||||
|
||||
@@ -619,7 +735,12 @@ class DownloadManager:
|
||||
def next_download_task(self):
|
||||
"""处理下载队列中的下一个任务"""
|
||||
if not self.download_queue:
|
||||
self.main_window.after_hash_compare()
|
||||
# 所有下载任务都已完成,进行后检查
|
||||
debug_mode = self.is_debug_mode()
|
||||
if debug_mode:
|
||||
logger.debug("DEBUG: 所有下载任务完成,进行后检查")
|
||||
# 使用patch_detector进行安装后哈希比较
|
||||
self.main_window.patch_detector.after_hash_compare()
|
||||
return
|
||||
|
||||
if self.download_task_manager.current_download_thread and self.download_task_manager.current_download_thread.isRunning():
|
||||
@@ -696,10 +817,9 @@ class DownloadManager:
|
||||
|
||||
if hash_valid:
|
||||
if debug_mode:
|
||||
logger.info(f"DEBUG: 成功复制并验证补丁文件 {_7z_path}")
|
||||
logger.debug(f"成功复制并验证补丁文件 {_7z_path}")
|
||||
# 直接进入解压阶段
|
||||
self.main_window.extraction_handler.start_extraction(_7z_path, game_folder, plugin_path, game_version)
|
||||
self.main_window.extraction_handler.extraction_finished.connect(self.on_extraction_finished)
|
||||
self.extraction_handler.start_extraction(_7z_path, game_folder, plugin_path, game_version)
|
||||
else:
|
||||
if debug_mode:
|
||||
logger.warning(f"DEBUG: 补丁文件哈希验证失败")
|
||||
@@ -735,21 +855,18 @@ class DownloadManager:
|
||||
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: 游戏版本名称
|
||||
game_version: 游戏版本
|
||||
_7z_path: 7z文件保存路径
|
||||
plugin_path: 插件路径
|
||||
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:
|
||||
logger.error(f"--- Download Failed: {game_version} ---")
|
||||
logger.error(error)
|
||||
@@ -803,9 +920,21 @@ class DownloadManager:
|
||||
else:
|
||||
self.on_download_stopped()
|
||||
return
|
||||
|
||||
|
||||
# 下载成功后,直接进入解压阶段
|
||||
debug_mode = self.is_debug_mode()
|
||||
|
||||
# 关闭进度窗口
|
||||
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.accept()
|
||||
self.main_window.progress_window = None
|
||||
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 下载完成,直接进入解压阶段")
|
||||
|
||||
# 直接进入解压阶段
|
||||
self.extraction_handler.start_extraction(_7z_path, game_folder, plugin_path, game_version)
|
||||
self.extraction_handler.extraction_finished.connect(self.on_extraction_finished)
|
||||
|
||||
def on_extraction_finished(self, continue_download):
|
||||
"""解压完成后的回调,决定是否继续下载队列
|
||||
@@ -835,7 +964,8 @@ class DownloadManager:
|
||||
logger.info("下载已全部停止。")
|
||||
|
||||
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(
|
||||
self.main_window,
|
||||
@@ -853,4 +983,206 @@ class DownloadManager:
|
||||
|
||||
def show_download_thread_settings(self):
|
||||
"""显示下载线程设置对话框"""
|
||||
return self.download_task_manager.show_download_thread_settings()
|
||||
return self.download_task_manager.show_download_thread_settings()
|
||||
|
||||
def direct_download_action(self, games_to_download):
|
||||
"""直接下载指定游戏的补丁,绕过补丁判断,用于从离线模式转接过来的任务
|
||||
|
||||
Args:
|
||||
games_to_download: 要下载的游戏列表
|
||||
"""
|
||||
debug_mode = self.is_debug_mode()
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 直接下载模式,绕过补丁判断,游戏列表: {games_to_download}")
|
||||
|
||||
if not self.selected_folder:
|
||||
QtWidgets.QMessageBox.warning(
|
||||
self.main_window, f"通知 - {APP_NAME}", "\n未选择任何目录,请重新选择\n"
|
||||
)
|
||||
return
|
||||
|
||||
# 识别游戏目录
|
||||
game_dirs = self.main_window.game_detector.identify_game_directories_improved(self.selected_folder)
|
||||
|
||||
if not game_dirs:
|
||||
QtWidgets.QMessageBox.warning(
|
||||
self.main_window, f"通知 - {APP_NAME}", "\n未在选择的目录中找到支持的游戏\n"
|
||||
)
|
||||
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)
|
||||
return
|
||||
|
||||
# 过滤出存在的游戏目录
|
||||
selected_game_dirs = {game: game_dirs[game] for game in games_to_download if game in game_dirs}
|
||||
|
||||
if not selected_game_dirs:
|
||||
QtWidgets.QMessageBox.warning(
|
||||
self.main_window, f"通知 - {APP_NAME}", "\n未找到指定游戏的安装目录\n"
|
||||
)
|
||||
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)
|
||||
return
|
||||
|
||||
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)
|
||||
if hasattr(self.main_window, 'window_manager'):
|
||||
self.main_window.window_manager.change_window_state(self.main_window.window_manager.STATE_READY)
|
||||
return
|
||||
|
||||
# 填充下载队列
|
||||
self._fill_direct_download_queue(config, selected_game_dirs)
|
||||
|
||||
if not self.download_queue:
|
||||
# 所有下载任务都已完成,进行后检查
|
||||
if debug_mode:
|
||||
logger.debug("DEBUG: 所有下载任务完成,进行后检查")
|
||||
# 使用patch_detector进行安装后哈希比较
|
||||
self.main_window.patch_detector.after_hash_compare()
|
||||
return
|
||||
|
||||
# 显示Cloudflare优化选项
|
||||
self._show_cloudflare_option()
|
||||
|
||||
def _fill_direct_download_queue(self, config, game_dirs):
|
||||
"""直接填充下载队列,不检查补丁是否已安装
|
||||
|
||||
兼容两种配置格式:
|
||||
1) 扁平格式: {"vol1": url, "vol2": url, ..., "after": url}
|
||||
2) 原始JSON格式: {"vol.1.data": {"url": url}, ..., "after.data": {"url": url}}
|
||||
|
||||
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:
|
||||
logger.debug(f"DEBUG: 直接填充下载队列, 游戏目录: {game_dirs}")
|
||||
|
||||
# 记录要下载的游戏,用于历史记录
|
||||
games_to_download = list(game_dirs.keys())
|
||||
self.main_window.download_queue_history = games_to_download
|
||||
|
||||
def _extract_url(cfg, simple_key, nested_key):
|
||||
"""从配置中提取URL,优先扁平键,其次原始JSON嵌套键"""
|
||||
try:
|
||||
if isinstance(cfg, dict):
|
||||
# 扁平格式: {"vol1": url} 或 {"after": url}
|
||||
val = cfg.get(simple_key)
|
||||
if isinstance(val, str) and val:
|
||||
return val
|
||||
# 原始格式: {"vol.1.data": {"url": url}} 或 {"after.data": {"url": url}}
|
||||
nested = cfg.get(nested_key)
|
||||
if isinstance(nested, dict) and isinstance(nested.get("url"), str) and nested.get("url"):
|
||||
return nested.get("url")
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
# Vol.1-4
|
||||
for i in range(1, 5):
|
||||
game_version = f"NEKOPARA Vol.{i}"
|
||||
if game_version in game_dirs:
|
||||
url = _extract_url(config, f"vol{i}", f"vol.{i}.data")
|
||||
if url:
|
||||
game_folder = game_dirs[game_version]
|
||||
if debug_mode:
|
||||
logger.debug(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))
|
||||
elif debug_mode:
|
||||
logger.warning(f"DEBUG: 未找到 {game_version} 的下载URL")
|
||||
|
||||
# After
|
||||
game_version = "NEKOPARA After"
|
||||
if game_version in game_dirs:
|
||||
url = _extract_url(config, "after", "after.data")
|
||||
if url:
|
||||
game_folder = game_dirs[game_version]
|
||||
if debug_mode:
|
||||
logger.debug(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))
|
||||
elif debug_mode:
|
||||
logger.warning(f"DEBUG: 未找到 {game_version} 的下载URL")
|
||||
|
||||
def graceful_stop_threads(self, threads_dict, timeout_ms=2000):
|
||||
"""优雅地停止一组线程.
|
||||
|
||||
Args:
|
||||
threads_dict (dict): 线程名字和线程对象的字典.
|
||||
timeout_ms (int): 等待线程自然结束的超时时间.
|
||||
"""
|
||||
for name, thread_obj in threads_dict.items():
|
||||
if not thread_obj or not hasattr(thread_obj, 'isRunning') or not thread_obj.isRunning():
|
||||
continue
|
||||
|
||||
try:
|
||||
if hasattr(thread_obj, 'requestInterruption'):
|
||||
thread_obj.requestInterruption()
|
||||
|
||||
if thread_obj.wait(timeout_ms):
|
||||
if self.debug_manager:
|
||||
self.debug_manager.log_debug(f"线程 {name} 已优雅停止.")
|
||||
else:
|
||||
if self.debug_manager:
|
||||
self.debug_manager.log_warning(f"线程 {name} 超时,强制终止.")
|
||||
thread_obj.terminate()
|
||||
thread_obj.wait(1000) # a short wait after termination
|
||||
except Exception as e:
|
||||
if self.debug_manager:
|
||||
self.debug_manager.log_error(f"停止线程 {name} 时发生错误: {e}")
|
||||
|
||||
def on_game_directories_identified(self, game_dirs):
|
||||
"""当游戏目录识别完成后的回调.
|
||||
|
||||
Args:
|
||||
game_dirs: 识别到的游戏目录
|
||||
"""
|
||||
self.main_window.ui_manager.hide_loading_dialog()
|
||||
|
||||
if not game_dirs:
|
||||
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)
|
||||
from PySide6.QtWidgets import QMessageBox
|
||||
from config.config import APP_NAME
|
||||
QMessageBox.warning(
|
||||
self.main_window,
|
||||
f"目录错误 - {APP_NAME}",
|
||||
"\n未能识别到任何游戏目录。\n\n请确认您选择的是游戏的上级目录,并且该目录中包含NEKOPARA系列游戏文件夹。\n"
|
||||
)
|
||||
return
|
||||
|
||||
self.main_window.ui_manager.show_loading_dialog("正在检查补丁状态...")
|
||||
|
||||
install_paths = self.get_install_paths()
|
||||
|
||||
# 使用异步方式进行哈希预检查
|
||||
self.main_window.pre_hash_thread = self.main_window.patch_detector.create_hash_thread("pre", install_paths)
|
||||
self.main_window.pre_hash_thread.pre_finished.connect(
|
||||
lambda updated_status: self.on_pre_hash_finished_with_dirs(updated_status, game_dirs)
|
||||
)
|
||||
# 在线程自然结束时清理引用
|
||||
try:
|
||||
self.main_window.pre_hash_thread.finished.connect(lambda: setattr(self.main_window, 'pre_hash_thread', None))
|
||||
except Exception:
|
||||
pass
|
||||
self.main_window.pre_hash_thread.start()
|
||||
@@ -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,7 +1,21 @@
|
||||
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:
|
||||
"""游戏检测器,用于识别游戏目录和版本"""
|
||||
|
||||
@@ -16,6 +30,17 @@ class GameDetector:
|
||||
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):
|
||||
"""检查是否处于调试模式
|
||||
@@ -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
|
||||
|
||||
|
||||
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)
|
||||
@@ -3,23 +3,36 @@ 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):
|
||||
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):
|
||||
"""检查是否处于调试模式
|
||||
@@ -73,17 +86,24 @@ class PatchManager:
|
||||
debug_mode = self._is_debug_mode()
|
||||
|
||||
if debug_mode:
|
||||
self.logger.debug(f"开始卸载 {game_version} 补丁,目录: {game_dir}")
|
||||
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无法识别游戏版本: {game_version}\n",
|
||||
f"\n{error_msg}\n",
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
return False if not silent else {"success": False, "message": f"无法识别游戏版本: {game_version}", "files_removed": 0}
|
||||
return False if not silent else {"success": False, "message": error_msg, "files_removed": 0}
|
||||
|
||||
try:
|
||||
files_removed = 0
|
||||
@@ -92,6 +112,9 @@ class PatchManager:
|
||||
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,
|
||||
@@ -102,7 +125,7 @@ class PatchManager:
|
||||
]
|
||||
|
||||
if debug_mode:
|
||||
self.logger.debug(f"查找以下可能的补丁文件路径: {patch_files_to_check}")
|
||||
self.logger.debug(f"DEBUG: 查找以下可能的补丁文件路径: {patch_files_to_check}")
|
||||
|
||||
# 查找并删除补丁文件,包括启用和禁用的
|
||||
patch_file_found = False
|
||||
@@ -110,44 +133,68 @@ class PatchManager:
|
||||
# 检查常规补丁文件
|
||||
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"已删除补丁文件: {patch_path}")
|
||||
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"已删除被禁用的补丁文件: {disabled_path}")
|
||||
self.logger.debug(f"DEBUG: 已删除被禁用的补丁文件: {disabled_path}")
|
||||
|
||||
if not patch_file_found and debug_mode:
|
||||
self.logger.debug(f"未找到补丁文件,检查了以下路径: {patch_files_to_check}")
|
||||
self.logger.debug(f"也检查了禁用的补丁文件(.fain后缀)")
|
||||
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"已删除签名文件: {sig_file_path}")
|
||||
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"已删除被禁用补丁的签名文件: {disabled_sig_path}")
|
||||
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"),
|
||||
@@ -156,12 +203,20 @@ class PatchManager:
|
||||
|
||||
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"已删除补丁文件夹: {patch_folder}")
|
||||
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"]
|
||||
|
||||
@@ -169,12 +224,20 @@ class PatchManager:
|
||||
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"已删除game/patch文件夹: {game_patch_folder}")
|
||||
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"]
|
||||
|
||||
@@ -185,55 +248,82 @@ class PatchManager:
|
||||
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"已删除配置文件: {config_path}")
|
||||
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"已删除脚本文件: {script_path}")
|
||||
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}",
|
||||
f"\n{game_version} 补丁卸载成功!\n共删除 {files_removed} 个文件/文件夹。\n",
|
||||
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}",
|
||||
f"\n未找到 {game_version} 的补丁文件,可能未安装补丁或已被移除。\n",
|
||||
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"卸载错误 - {str(e)}")
|
||||
import traceback
|
||||
self.logger.debug(f"错误详情:\n{traceback.format_exc()}")
|
||||
self.logger.debug(f"DEBUG: 显示卸载失败消息")
|
||||
|
||||
QMessageBox.critical(
|
||||
None,
|
||||
@@ -261,16 +351,38 @@ class PatchManager:
|
||||
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"],
|
||||
@@ -280,8 +392,15 @@ class PatchManager:
|
||||
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,
|
||||
@@ -291,7 +410,12 @@ class PatchManager:
|
||||
|
||||
except Exception as e:
|
||||
if debug_mode:
|
||||
self.logger.debug(f"卸载 {version} 时出错: {str(e)}")
|
||||
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,
|
||||
@@ -299,7 +423,12 @@ class PatchManager:
|
||||
"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):
|
||||
@@ -310,6 +439,11 @@ class PatchManager:
|
||||
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"
|
||||
|
||||
# 如果有详细结果,添加到消息中
|
||||
@@ -317,11 +451,24 @@ class PatchManager:
|
||||
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,
|
||||
@@ -331,7 +478,7 @@ class PatchManager:
|
||||
)
|
||||
|
||||
def check_patch_installed(self, game_dir, game_version):
|
||||
"""检查游戏是否已安装补丁
|
||||
"""检查游戏是否已安装补丁(调用patch_detector)
|
||||
|
||||
Args:
|
||||
game_dir: 游戏目录路径
|
||||
@@ -340,6 +487,10 @@ class PatchManager:
|
||||
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:
|
||||
@@ -425,7 +576,7 @@ class PatchManager:
|
||||
return False
|
||||
|
||||
def check_patch_disabled(self, game_dir, game_version):
|
||||
"""检查游戏的补丁是否已被禁用
|
||||
"""检查游戏的补丁是否已被禁用(调用patch_detector)
|
||||
|
||||
Args:
|
||||
game_dir: 游戏目录路径
|
||||
@@ -435,6 +586,10 @@ class PatchManager:
|
||||
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:
|
||||
@@ -743,4 +898,86 @@ class PatchManager:
|
||||
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):
|
||||
"""设置窗口圆角"""
|
||||
# 实现圆角窗口
|
||||
@@ -1,702 +0,0 @@
|
||||
import os
|
||||
import hashlib
|
||||
import shutil
|
||||
import tempfile
|
||||
import py7zr
|
||||
import traceback
|
||||
from PySide6.QtWidgets import QMessageBox
|
||||
|
||||
from data.config import PLUGIN, PLUGIN_HASH, GAME_INFO
|
||||
from utils import msgbox_frame
|
||||
from utils.logger import setup_logger
|
||||
|
||||
# 初始化logger
|
||||
logger = setup_logger("offline_mode_manager")
|
||||
|
||||
class OfflineModeManager:
|
||||
"""离线模式管理器,用于管理离线模式下的补丁安装和检测"""
|
||||
|
||||
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.offline_patches = {} # 存储离线补丁信息 {补丁名称: 文件路径}
|
||||
self.is_offline_mode = False
|
||||
|
||||
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'):
|
||||
# 尝试直接从debug_manager获取状态
|
||||
return self.main_window.debug_manager._is_debug_mode()
|
||||
elif hasattr(self.main_window, 'config'):
|
||||
# 如果debug_manager还没准备好,尝试从配置中获取
|
||||
return self.main_window.config.get('debug_mode', False)
|
||||
# 如果以上都不可行,返回False
|
||||
return False
|
||||
except Exception:
|
||||
# 捕获任何异常,默认返回False
|
||||
return False
|
||||
|
||||
def scan_for_offline_patches(self, directory=None):
|
||||
"""扫描指定目录(默认为软件所在目录)查找离线补丁文件
|
||||
|
||||
Args:
|
||||
directory: 要扫描的目录,如果为None则使用软件所在目录
|
||||
|
||||
Returns:
|
||||
dict: 找到的补丁文件 {补丁名称: 文件路径}
|
||||
"""
|
||||
if directory is None:
|
||||
# 获取软件所在目录
|
||||
directory = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
debug_mode = self._is_debug_mode()
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 扫描离线补丁文件,目录: {directory}")
|
||||
|
||||
# 要查找的补丁文件名
|
||||
patch_files = ["vol.1.7z", "vol.2.7z", "vol.3.7z", "vol.4.7z", "after.7z"]
|
||||
|
||||
found_patches = {}
|
||||
|
||||
# 扫描目录中的文件
|
||||
for file in os.listdir(directory):
|
||||
if file.lower() in patch_files:
|
||||
file_path = os.path.join(directory, file)
|
||||
if os.path.isfile(file_path):
|
||||
patch_name = file.lower()
|
||||
found_patches[patch_name] = file_path
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 找到离线补丁文件: {patch_name} 路径: {file_path}")
|
||||
|
||||
self.offline_patches = found_patches
|
||||
return found_patches
|
||||
|
||||
def has_offline_patches(self):
|
||||
"""检查是否有可用的离线补丁文件
|
||||
|
||||
Returns:
|
||||
bool: 是否有可用的离线补丁
|
||||
"""
|
||||
if not self.offline_patches:
|
||||
self.scan_for_offline_patches()
|
||||
|
||||
return len(self.offline_patches) > 0
|
||||
|
||||
def set_offline_mode(self, enabled):
|
||||
"""设置离线模式状态
|
||||
|
||||
Args:
|
||||
enabled: 是否启用离线模式
|
||||
|
||||
Returns:
|
||||
bool: 是否成功设置离线模式
|
||||
"""
|
||||
debug_mode = self._is_debug_mode()
|
||||
|
||||
if enabled:
|
||||
# 检查是否有离线补丁文件
|
||||
if not self.has_offline_patches() and not debug_mode:
|
||||
msgbox_frame(
|
||||
f"离线模式错误 - {self.app_name}",
|
||||
"\n未找到任何离线补丁文件,无法启用离线模式。\n\n请将补丁文件放置在软件所在目录后再尝试。\n",
|
||||
QMessageBox.StandardButton.Ok
|
||||
).exec()
|
||||
return False
|
||||
|
||||
if debug_mode:
|
||||
logger.debug("DEBUG: 已启用离线模式(调试模式下允许强制启用)")
|
||||
|
||||
self.is_offline_mode = enabled
|
||||
|
||||
# 更新窗口标题
|
||||
if hasattr(self.main_window, 'setWindowTitle'):
|
||||
from data.config import APP_NAME, APP_VERSION
|
||||
mode_indicator = "[离线模式]" if enabled 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}")
|
||||
|
||||
# 同步更新UI菜单中的模式选择状态
|
||||
if hasattr(self.main_window, 'ui_manager'):
|
||||
ui_manager = self.main_window.ui_manager
|
||||
if hasattr(ui_manager, 'online_mode_action') and hasattr(ui_manager, 'offline_mode_action'):
|
||||
ui_manager.online_mode_action.setChecked(not enabled)
|
||||
ui_manager.offline_mode_action.setChecked(enabled)
|
||||
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 离线模式已{'启用' if enabled else '禁用'}")
|
||||
|
||||
return True
|
||||
|
||||
def get_offline_patch_path(self, game_version):
|
||||
"""根据游戏版本获取对应的离线补丁文件路径
|
||||
|
||||
Args:
|
||||
game_version: 游戏版本名称,如"NEKOPARA Vol.1"
|
||||
|
||||
Returns:
|
||||
str: 离线补丁文件路径,如果没有找到则返回None
|
||||
"""
|
||||
# 确保已扫描过补丁文件
|
||||
if not self.offline_patches:
|
||||
self.scan_for_offline_patches()
|
||||
|
||||
# 根据游戏版本获取对应的补丁文件名
|
||||
patch_file = None
|
||||
|
||||
if "Vol.1" in game_version:
|
||||
patch_file = "vol.1.7z"
|
||||
elif "Vol.2" in game_version:
|
||||
patch_file = "vol.2.7z"
|
||||
elif "Vol.3" in game_version:
|
||||
patch_file = "vol.3.7z"
|
||||
elif "Vol.4" in game_version:
|
||||
patch_file = "vol.4.7z"
|
||||
elif "After" in game_version:
|
||||
patch_file = "after.7z"
|
||||
|
||||
# 检查是否有对应的补丁文件
|
||||
if patch_file and patch_file in self.offline_patches:
|
||||
return self.offline_patches[patch_file]
|
||||
|
||||
return None
|
||||
|
||||
def prepare_offline_patch(self, game_version, target_path):
|
||||
"""准备离线补丁文件,复制到缓存目录
|
||||
|
||||
Args:
|
||||
game_version: 游戏版本名称
|
||||
target_path: 目标路径(通常是缓存目录中的路径)
|
||||
|
||||
Returns:
|
||||
bool: 是否成功准备补丁文件
|
||||
"""
|
||||
source_path = self.get_offline_patch_path(game_version)
|
||||
|
||||
if not source_path:
|
||||
return False
|
||||
|
||||
debug_mode = self._is_debug_mode()
|
||||
|
||||
try:
|
||||
# 确保目标目录存在
|
||||
os.makedirs(os.path.dirname(target_path), exist_ok=True)
|
||||
|
||||
# 复制文件
|
||||
shutil.copy2(source_path, target_path)
|
||||
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 已复制离线补丁文件 {source_path} 到 {target_path}")
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
if debug_mode:
|
||||
logger.error(f"DEBUG: 复制离线补丁文件失败: {e}")
|
||||
return False
|
||||
|
||||
def verify_patch_hash(self, game_version, file_path):
|
||||
"""验证补丁文件的哈希值
|
||||
|
||||
Args:
|
||||
game_version: 游戏版本名称
|
||||
file_path: 补丁压缩包文件路径
|
||||
|
||||
Returns:
|
||||
bool: 哈希值是否匹配
|
||||
"""
|
||||
# 获取预期的哈希值
|
||||
expected_hash = None
|
||||
|
||||
if "Vol.1" in game_version:
|
||||
expected_hash = PLUGIN_HASH.get("vol1", "")
|
||||
elif "Vol.2" in game_version:
|
||||
expected_hash = PLUGIN_HASH.get("vol2", "")
|
||||
elif "Vol.3" in game_version:
|
||||
expected_hash = PLUGIN_HASH.get("vol3", "")
|
||||
elif "Vol.4" in game_version:
|
||||
expected_hash = PLUGIN_HASH.get("vol4", "")
|
||||
elif "After" in game_version:
|
||||
expected_hash = PLUGIN_HASH.get("after", "")
|
||||
|
||||
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):
|
||||
if debug_mode:
|
||||
logger.warning(f"DEBUG: 补丁文件不存在: {file_path}")
|
||||
return False
|
||||
|
||||
# 检查文件大小
|
||||
file_size = os.path.getsize(file_path)
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 补丁文件大小: {file_size} 字节")
|
||||
|
||||
if file_size == 0:
|
||||
if debug_mode:
|
||||
logger.warning(f"DEBUG: 补丁文件大小为0,无效文件")
|
||||
return False
|
||||
|
||||
# 创建临时目录用于解压文件
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 创建临时目录: {temp_dir}")
|
||||
|
||||
# 解压补丁文件
|
||||
try:
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 开始解压文件: {file_path}")
|
||||
|
||||
with py7zr.SevenZipFile(file_path, mode="r") as archive:
|
||||
# 获取压缩包内文件列表
|
||||
file_list = archive.getnames()
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 压缩包内文件列表: {file_list}")
|
||||
|
||||
# 解压所有文件
|
||||
archive.extractall(path=temp_dir)
|
||||
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 解压完成")
|
||||
# 列出解压后的文件
|
||||
extracted_files = []
|
||||
for root, dirs, files in os.walk(temp_dir):
|
||||
for file in files:
|
||||
extracted_files.append(os.path.join(root, file))
|
||||
logger.debug(f"DEBUG: 解压后的文件列表: {extracted_files}")
|
||||
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()}")
|
||||
return False
|
||||
|
||||
# 获取补丁文件路径
|
||||
patch_file = None
|
||||
if "Vol.1" in game_version:
|
||||
patch_file = os.path.join(temp_dir, "vol.1", "adultsonly.xp3")
|
||||
elif "Vol.2" in game_version:
|
||||
patch_file = os.path.join(temp_dir, "vol.2", "adultsonly.xp3")
|
||||
elif "Vol.3" in game_version:
|
||||
patch_file = os.path.join(temp_dir, "vol.3", "update00.int")
|
||||
elif "Vol.4" in game_version:
|
||||
patch_file = os.path.join(temp_dir, "vol.4", "vol4adult.xp3")
|
||||
elif "After" in game_version:
|
||||
patch_file = os.path.join(temp_dir, "after", "afteradult.xp3")
|
||||
|
||||
if not patch_file or 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}")
|
||||
|
||||
# 检查解压目录结构
|
||||
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}")
|
||||
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 '失败'}")
|
||||
logger.debug(f"DEBUG: 预期哈希值: {expected_hash}")
|
||||
logger.debug(f"DEBUG: 实际哈希值: {file_hash}")
|
||||
|
||||
return result
|
||||
except Exception as e:
|
||||
if debug_mode:
|
||||
logger.error(f"DEBUG: 计算补丁文件哈希值失败: {e}")
|
||||
logger.error(f"DEBUG: 错误类型: {type(e).__name__}")
|
||||
return False
|
||||
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()}")
|
||||
return False
|
||||
|
||||
def is_offline_mode_available(self):
|
||||
"""检查是否可以使用离线模式
|
||||
|
||||
Returns:
|
||||
bool: 是否可以使用离线模式
|
||||
"""
|
||||
# 在调试模式下始终允许离线模式
|
||||
if self._is_debug_mode():
|
||||
return True
|
||||
|
||||
# 检查是否有离线补丁文件
|
||||
return self.has_offline_patches()
|
||||
|
||||
def is_in_offline_mode(self):
|
||||
"""检查当前是否处于离线模式
|
||||
|
||||
Returns:
|
||||
bool: 是否处于离线模式
|
||||
"""
|
||||
return self.is_offline_mode
|
||||
|
||||
def install_offline_patches(self, selected_games):
|
||||
"""直接安装离线补丁,完全绕过下载模块
|
||||
|
||||
Args:
|
||||
selected_games: 用户选择安装的游戏列表
|
||||
|
||||
Returns:
|
||||
bool: 是否成功启动安装流程
|
||||
"""
|
||||
debug_mode = self._is_debug_mode()
|
||||
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 开始离线安装流程,选择的游戏: {selected_games}")
|
||||
|
||||
if not self.is_in_offline_mode():
|
||||
if debug_mode:
|
||||
logger.warning("DEBUG: 当前不是离线模式,无法使用离线安装")
|
||||
return False
|
||||
|
||||
# 确保已扫描过补丁文件
|
||||
if not self.offline_patches:
|
||||
self.scan_for_offline_patches()
|
||||
|
||||
if not self.offline_patches:
|
||||
if debug_mode:
|
||||
logger.warning("DEBUG: 未找到任何离线补丁文件")
|
||||
msgbox_frame(
|
||||
f"离线安装错误 - {self.app_name}",
|
||||
"\n未找到任何离线补丁文件,无法进行离线安装。\n\n请将补丁文件放置在软件所在目录后再尝试。\n",
|
||||
QMessageBox.StandardButton.Ok
|
||||
).exec()
|
||||
return False
|
||||
|
||||
# 获取游戏目录
|
||||
game_dirs = self.main_window.game_detector.identify_game_directories_improved(
|
||||
self.main_window.download_manager.selected_folder
|
||||
)
|
||||
|
||||
if not game_dirs:
|
||||
if debug_mode:
|
||||
logger.warning("DEBUG: 未识别到任何游戏目录")
|
||||
return False
|
||||
|
||||
# 显示文件检验窗口
|
||||
self.main_window.hash_msg_box = self.main_window.hash_manager.hash_pop_window(check_type="pre", is_offline=True)
|
||||
|
||||
# 获取安装路径
|
||||
install_paths = self.main_window.download_manager.get_install_paths()
|
||||
|
||||
# 创建并启动哈希线程进行预检查
|
||||
self.main_window.hash_thread = self.main_window.create_hash_thread("pre", install_paths)
|
||||
self.main_window.hash_thread.pre_finished.connect(
|
||||
lambda updated_status: self.on_offline_pre_hash_finished(updated_status, game_dirs, selected_games)
|
||||
)
|
||||
self.main_window.hash_thread.start()
|
||||
|
||||
return True
|
||||
|
||||
def on_offline_pre_hash_finished(self, updated_status, game_dirs, selected_games):
|
||||
"""离线模式下的哈希预检查完成处理
|
||||
|
||||
Args:
|
||||
updated_status: 更新后的安装状态
|
||||
game_dirs: 识别到的游戏目录
|
||||
selected_games: 用户选择安装的游戏列表
|
||||
"""
|
||||
debug_mode = self._is_debug_mode()
|
||||
|
||||
# 更新安装状态
|
||||
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)
|
||||
|
||||
# 过滤出需要安装的游戏
|
||||
installable_games = []
|
||||
for game_version in selected_games:
|
||||
if game_version in game_dirs and not self.main_window.installed_status.get(game_version, False):
|
||||
# 检查是否有对应的离线补丁
|
||||
if self.get_offline_patch_path(game_version):
|
||||
installable_games.append(game_version)
|
||||
elif debug_mode:
|
||||
logger.warning(f"DEBUG: 未找到 {game_version} 的离线补丁文件,跳过")
|
||||
|
||||
if not installable_games:
|
||||
if debug_mode:
|
||||
logger.info("DEBUG: 没有需要安装的游戏或未找到对应的离线补丁")
|
||||
msgbox_frame(
|
||||
f"离线安装信息 - {self.app_name}",
|
||||
"\n没有需要安装的游戏或未找到对应的离线补丁文件。\n",
|
||||
QMessageBox.StandardButton.Ok
|
||||
).exec()
|
||||
self.main_window.ui.start_install_text.setText("开始安装")
|
||||
return
|
||||
|
||||
# 开始安装流程
|
||||
if debug_mode:
|
||||
logger.info(f"DEBUG: 开始离线安装流程,安装游戏: {installable_games}")
|
||||
|
||||
# 创建安装任务列表
|
||||
install_tasks = []
|
||||
for game_version in installable_games:
|
||||
# 获取离线补丁文件路径
|
||||
patch_file = self.get_offline_patch_path(game_version)
|
||||
if not patch_file:
|
||||
continue
|
||||
|
||||
# 获取游戏目录
|
||||
game_folder = game_dirs.get(game_version)
|
||||
if not game_folder:
|
||||
continue
|
||||
|
||||
# 获取目标路径
|
||||
if "Vol.1" in game_version:
|
||||
_7z_path = os.path.join(PLUGIN, "vol.1.7z")
|
||||
plugin_path = os.path.join(PLUGIN, GAME_INFO[game_version]["plugin_path"])
|
||||
elif "Vol.2" in game_version:
|
||||
_7z_path = os.path.join(PLUGIN, "vol.2.7z")
|
||||
plugin_path = os.path.join(PLUGIN, GAME_INFO[game_version]["plugin_path"])
|
||||
elif "Vol.3" in game_version:
|
||||
_7z_path = os.path.join(PLUGIN, "vol.3.7z")
|
||||
plugin_path = os.path.join(PLUGIN, GAME_INFO[game_version]["plugin_path"])
|
||||
elif "Vol.4" in game_version:
|
||||
_7z_path = os.path.join(PLUGIN, "vol.4.7z")
|
||||
plugin_path = os.path.join(PLUGIN, GAME_INFO[game_version]["plugin_path"])
|
||||
elif "After" in game_version:
|
||||
_7z_path = os.path.join(PLUGIN, "after.7z")
|
||||
plugin_path = os.path.join(PLUGIN, GAME_INFO[game_version]["plugin_path"])
|
||||
else:
|
||||
continue
|
||||
|
||||
# 添加到安装任务列表
|
||||
install_tasks.append((patch_file, game_folder, game_version, _7z_path, plugin_path))
|
||||
|
||||
# 开始执行第一个安装任务
|
||||
if install_tasks:
|
||||
self.process_next_offline_install_task(install_tasks)
|
||||
else:
|
||||
self.main_window.ui.start_install_text.setText("开始安装")
|
||||
|
||||
def process_next_offline_install_task(self, install_tasks):
|
||||
"""处理下一个离线安装任务
|
||||
|
||||
Args:
|
||||
install_tasks: 安装任务列表,每个任务是一个元组 (patch_file, game_folder, game_version, _7z_path, plugin_path)
|
||||
"""
|
||||
debug_mode = self._is_debug_mode()
|
||||
|
||||
if not install_tasks:
|
||||
# 所有任务完成,进行后检查
|
||||
if debug_mode:
|
||||
logger.info("DEBUG: 所有离线安装任务完成,进行后检查")
|
||||
self.main_window.after_hash_compare()
|
||||
return
|
||||
|
||||
# 获取下一个任务
|
||||
patch_file, game_folder, game_version, _7z_path, plugin_path = install_tasks.pop(0)
|
||||
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 处理离线安装任务: {game_version}")
|
||||
logger.debug(f"DEBUG: 补丁文件: {patch_file}")
|
||||
logger.debug(f"DEBUG: 游戏目录: {game_folder}")
|
||||
|
||||
# 确保目标目录存在
|
||||
os.makedirs(os.path.dirname(_7z_path), exist_ok=True)
|
||||
|
||||
try:
|
||||
# 复制补丁文件到缓存目录
|
||||
shutil.copy2(patch_file, _7z_path)
|
||||
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 已复制补丁文件到缓存目录: {_7z_path}")
|
||||
logger.debug(f"DEBUG: 开始验证补丁文件哈希值")
|
||||
|
||||
# 获取预期的哈希值
|
||||
expected_hash = None
|
||||
if "Vol.1" in game_version:
|
||||
expected_hash = PLUGIN_HASH.get("vol1", "")
|
||||
elif "Vol.2" in game_version:
|
||||
expected_hash = PLUGIN_HASH.get("vol2", "")
|
||||
elif "Vol.3" in game_version:
|
||||
expected_hash = PLUGIN_HASH.get("vol3", "")
|
||||
elif "Vol.4" in game_version:
|
||||
expected_hash = PLUGIN_HASH.get("vol4", "")
|
||||
elif "After" in game_version:
|
||||
expected_hash = PLUGIN_HASH.get("after", "")
|
||||
|
||||
if debug_mode and expected_hash:
|
||||
logger.debug(f"DEBUG: 预期哈希值: {expected_hash}")
|
||||
|
||||
# 显示哈希验证窗口 - 使用离线特定消息
|
||||
self.main_window.hash_msg_box = self.main_window.hash_manager.hash_pop_window(check_type="offline_verify", is_offline=True)
|
||||
|
||||
# 验证补丁文件哈希
|
||||
hash_valid = self.verify_patch_hash(game_version, _7z_path)
|
||||
|
||||
# 关闭哈希验证窗口
|
||||
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 hash_valid:
|
||||
if debug_mode:
|
||||
logger.info(f"DEBUG: 补丁文件哈希验证成功,开始解压")
|
||||
|
||||
# 显示解压窗口 - 使用离线特定消息
|
||||
self.main_window.hash_msg_box = self.main_window.hash_manager.hash_pop_window(check_type="offline_extraction", is_offline=True)
|
||||
|
||||
try:
|
||||
# 创建解压线程
|
||||
extraction_thread = self.main_window.create_extraction_thread(
|
||||
_7z_path, game_folder, plugin_path, game_version
|
||||
)
|
||||
|
||||
# 正确连接信号
|
||||
extraction_thread.finished.connect(
|
||||
lambda success, error, game_ver: self.on_extraction_thread_finished(
|
||||
success, error, game_ver, install_tasks
|
||||
)
|
||||
)
|
||||
|
||||
# 启动解压线程
|
||||
extraction_thread.start()
|
||||
except Exception as e:
|
||||
if debug_mode:
|
||||
logger.error(f"DEBUG: 创建或启动解压线程失败: {e}")
|
||||
|
||||
# 关闭解压窗口
|
||||
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
|
||||
|
||||
# 显示错误消息
|
||||
msgbox_frame(
|
||||
f"解压错误 - {self.app_name}",
|
||||
f"\n{game_version} 的解压过程中发生错误: {str(e)}\n\n跳过此游戏的安装。\n",
|
||||
QMessageBox.StandardButton.Ok
|
||||
).exec()
|
||||
|
||||
# 继续下一个任务
|
||||
self.process_next_offline_install_task(install_tasks)
|
||||
else:
|
||||
if debug_mode:
|
||||
logger.warning(f"DEBUG: 补丁文件哈希验证失败")
|
||||
|
||||
# 显示错误消息
|
||||
msgbox_frame(
|
||||
f"哈希验证失败 - {self.app_name}",
|
||||
f"\n{game_version} 的补丁文件哈希验证失败,可能已损坏或被篡改。\n\n跳过此游戏的安装。\n",
|
||||
QMessageBox.StandardButton.Ok
|
||||
).exec()
|
||||
|
||||
# 继续下一个任务
|
||||
self.process_next_offline_install_task(install_tasks)
|
||||
except Exception as e:
|
||||
if debug_mode:
|
||||
logger.error(f"DEBUG: 离线安装任务处理失败: {e}")
|
||||
|
||||
# 显示错误消息
|
||||
msgbox_frame(
|
||||
f"安装错误 - {self.app_name}",
|
||||
f"\n{game_version} 的安装过程中发生错误: {str(e)}\n\n跳过此游戏的安装。\n",
|
||||
QMessageBox.StandardButton.Ok
|
||||
).exec()
|
||||
|
||||
# 继续下一个任务
|
||||
self.process_next_offline_install_task(install_tasks)
|
||||
|
||||
def on_extraction_thread_finished(self, success, error_message, game_version, remaining_tasks):
|
||||
"""解压线程完成后的处理
|
||||
|
||||
Args:
|
||||
success: 是否解压成功
|
||||
error_message: 错误信息
|
||||
game_version: 游戏版本
|
||||
remaining_tasks: 剩余的安装任务列表
|
||||
"""
|
||||
debug_mode = self._is_debug_mode()
|
||||
|
||||
# 关闭解压窗口
|
||||
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 debug_mode:
|
||||
logger.debug(f"DEBUG: 离线解压完成,状态: {'成功' if success else '失败'}")
|
||||
if not success:
|
||||
logger.error(f"DEBUG: 错误信息: {error_message}")
|
||||
|
||||
if not success:
|
||||
# 显示错误消息
|
||||
msgbox_frame(
|
||||
f"解压失败 - {self.app_name}",
|
||||
f"\n{game_version} 的补丁解压失败。\n\n错误信息: {error_message}\n\n跳过此游戏的安装。\n",
|
||||
QMessageBox.StandardButton.Ok
|
||||
).exec()
|
||||
|
||||
# 更新安装状态
|
||||
self.main_window.installed_status[game_version] = False
|
||||
else:
|
||||
# 更新安装状态
|
||||
self.main_window.installed_status[game_version] = True
|
||||
|
||||
# 处理下一个任务
|
||||
self.process_next_offline_install_task(remaining_tasks)
|
||||
|
||||
def on_offline_extraction_finished(self, remaining_tasks):
|
||||
"""离线模式下的解压完成处理(旧方法,保留兼容性)
|
||||
|
||||
Args:
|
||||
remaining_tasks: 剩余的安装任务列表
|
||||
"""
|
||||
debug_mode = self._is_debug_mode()
|
||||
|
||||
if debug_mode:
|
||||
logger.debug("DEBUG: 离线解压完成,继续处理下一个任务")
|
||||
|
||||
# 处理下一个任务
|
||||
self.process_next_offline_install_task(remaining_tasks)
|
||||
@@ -1,967 +0,0 @@
|
||||
from PySide6.QtGui import QIcon, QAction, QFont, QCursor, QActionGroup
|
||||
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))
|
||||
|
||||
# 获取当前离线模式状态
|
||||
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._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.work_mode_menu = QMenu("工作模式", self.main_window)
|
||||
self.work_mode_menu.setFont(menu_font)
|
||||
self.work_mode_menu.setStyleSheet(self._get_menu_style(font_family))
|
||||
|
||||
# 获取当前离线模式状态
|
||||
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) # 确保只能选择一个模式
|
||||
|
||||
# 连接切换事件
|
||||
self.online_mode_action.triggered.connect(lambda: self.switch_work_mode("online"))
|
||||
self.offline_mode_action.triggered.connect(lambda: self.switch_work_mode("offline"))
|
||||
|
||||
# 添加到工作模式子菜单
|
||||
self.work_mode_menu.addAction(self.online_mode_action)
|
||||
self.work_mode_menu.addAction(self.offline_mode_action)
|
||||
|
||||
# 创建开发者选项子菜单
|
||||
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
|
||||
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)
|
||||
|
||||
# 添加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.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.toggle_disable_auto_restore_hosts)
|
||||
|
||||
# 添加打开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.disable_auto_restore_action)
|
||||
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.work_mode_menu) # 添加工作模式子菜单
|
||||
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
|
||||
|
||||
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_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):
|
||||
"""打开当前日志文件"""
|
||||
try:
|
||||
# 检查日志文件是否存在
|
||||
if os.path.exists(LOG_FILE):
|
||||
# 获取日志文件大小
|
||||
file_size = os.path.getsize(LOG_FILE)
|
||||
if file_size == 0:
|
||||
msg_box = self._create_message_box("提示", f"\n当前日志文件 {os.path.basename(LOG_FILE)} 存在但为空。\n\n日志文件位置:{os.path.abspath(LOG_FILE)}")
|
||||
msg_box.exec()
|
||||
return
|
||||
|
||||
# 根据文件大小决定是使用文本查看器还是直接打开
|
||||
if file_size > 1024 * 1024: # 大于1MB
|
||||
# 文件较大,显示警告
|
||||
msg_box = self._create_message_box(
|
||||
"警告",
|
||||
f"\n日志文件较大 ({file_size / 1024 / 1024:.2f} MB),是否仍要打开?\n\n日志文件位置:{os.path.abspath(LOG_FILE)}",
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
|
||||
)
|
||||
if msg_box.exec() != 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:
|
||||
# 文件不存在,显示信息
|
||||
# 搜索log文件夹下所有可能的日志文件
|
||||
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)
|
||||
msg_box = self._create_message_box(
|
||||
"信息",
|
||||
f"\n日志文件夹不存在,已创建新的日志文件夹:\n{log_dir}\n\n请在启用调试模式后重试。"
|
||||
)
|
||||
msg_box.exec()
|
||||
return
|
||||
except Exception as e:
|
||||
msg_box = self._create_message_box(
|
||||
"错误",
|
||||
f"\n创建日志文件夹失败:\n\n{str(e)}"
|
||||
)
|
||||
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:
|
||||
msg_box = self._create_message_box(
|
||||
"错误",
|
||||
f"\n无法读取日志文件夹:\n\n{str(e)}"
|
||||
)
|
||||
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 = "时间未知"
|
||||
|
||||
msg_box = self._create_message_box(
|
||||
"信息",
|
||||
f"\n当前日志文件 {os.path.basename(LOG_FILE)} 不存在。\n\n"
|
||||
f"发现最新的日志文件: {os.path.basename(latest_log)}\n"
|
||||
f"({date_info}{time_info})\n\n"
|
||||
f"是否打开此文件?",
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
|
||||
)
|
||||
|
||||
if msg_box.exec() == QMessageBox.StandardButton.Yes:
|
||||
if os.name == 'nt': # Windows
|
||||
os.startfile(latest_log)
|
||||
else: # macOS 和 Linux
|
||||
import subprocess
|
||||
subprocess.call(['xdg-open', latest_log])
|
||||
return
|
||||
|
||||
# 如果没有找到任何日志文件或用户选择不打开最新的日志文件
|
||||
msg_box = self._create_message_box(
|
||||
"信息",
|
||||
f"\n没有找到有效的日志文件。\n\n"
|
||||
f"预期的日志文件夹:{log_dir}\n\n"
|
||||
f"请确认调试模式已启用,并执行一些操作后再查看日志。"
|
||||
)
|
||||
msg_box.exec()
|
||||
|
||||
except Exception as e:
|
||||
msg_box = self._create_message_box("错误", f"\n处理日志文件时出错:\n\n{str(e)}\n\n文件位置:{os.path.abspath(LOG_FILE)}")
|
||||
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 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 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()
|
||||
|
||||
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, 'set_start_button_enabled'):
|
||||
self.main_window.set_start_button_enabled(True)
|
||||
|
||||
# 清除版本警告标志
|
||||
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, '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()
|
||||
@@ -1,8 +0,0 @@
|
||||
"""
|
||||
handlers包,包含各种处理程序,用于分离主窗口的功能
|
||||
"""
|
||||
|
||||
from .patch_toggle_handler import PatchToggleHandler
|
||||
from .uninstall_handler import UninstallHandler
|
||||
|
||||
__all__ = ['PatchToggleHandler', 'UninstallHandler']
|
||||
@@ -4,34 +4,36 @@ import subprocess
|
||||
import shutil
|
||||
import json
|
||||
import webbrowser
|
||||
import traceback
|
||||
|
||||
from PySide6 import QtWidgets
|
||||
from PySide6.QtCore import QTimer, Qt, QPoint, QRect, QSize
|
||||
from PySide6.QtWidgets import QMainWindow, QMessageBox, QGraphicsOpacityEffect, QGraphicsColorizeEffect
|
||||
from PySide6.QtGui import QPalette, QColor, QPainterPath, QRegion, QFont
|
||||
from PySide6.QtGui import QAction # Added for menu actions
|
||||
from PySide6.QtGui import QAction
|
||||
|
||||
from ui.Ui_install import Ui_MainWindows
|
||||
from data.config import (
|
||||
from config.config import (
|
||||
APP_NAME, PLUGIN, GAME_INFO, BLOCK_SIZE,
|
||||
PLUGIN_HASH, UA, CONFIG_URL, LOG_FILE,
|
||||
DOWNLOAD_THREADS, DEFAULT_DOWNLOAD_THREAD_LEVEL, APP_VERSION # 添加APP_VERSION导入
|
||||
DOWNLOAD_THREADS, DEFAULT_DOWNLOAD_THREAD_LEVEL, APP_VERSION
|
||||
)
|
||||
from utils import (
|
||||
load_config, save_config, HashManager, AdminPrivileges, msgbox_frame, load_image_from_file
|
||||
)
|
||||
from workers import (
|
||||
DownloadThread, ProgressWindow, IpOptimizerThread,
|
||||
HashThread, ExtractionThread, ConfigFetchThread
|
||||
IpOptimizerThread,
|
||||
HashThread, ConfigFetchThread
|
||||
)
|
||||
from core import (
|
||||
MultiStageAnimations, UIManager, DownloadManager, DebugManager,
|
||||
WindowManager, GameDetector, PatchManager, ConfigManager
|
||||
WindowManager, GameDetector, PatchManager, ConfigManager, PatchDetector
|
||||
)
|
||||
from core.ipv6_manager import IPv6Manager
|
||||
from handlers import PatchToggleHandler, UninstallHandler
|
||||
from core.managers.ipv6_manager import IPv6Manager
|
||||
from core.handlers import PatchToggleHandler, UninstallHandler
|
||||
from utils.logger import setup_logger
|
||||
|
||||
|
||||
# 初始化logger
|
||||
logger = setup_logger("main_window")
|
||||
|
||||
@@ -39,153 +41,117 @@ class MainWindow(QMainWindow):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
# 设置窗口为无边框
|
||||
self.setWindowFlags(Qt.WindowType.FramelessWindowHint)
|
||||
# 设置窗口背景透明
|
||||
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
||||
self._setup_window_properties()
|
||||
self._init_ui()
|
||||
self._init_config_and_tools()
|
||||
self._init_managers()
|
||||
self._connect_signals()
|
||||
self._setup_environment()
|
||||
|
||||
# 调整窗口大小以适应背景图片比例 (1280x720)
|
||||
self.download_manager.hosts_manager.backup()
|
||||
self._setup_debug_mode()
|
||||
|
||||
self.check_and_set_offline_mode()
|
||||
self.fetch_cloud_config()
|
||||
self.start_animations()
|
||||
|
||||
def _setup_window_properties(self):
|
||||
"""设置窗口的基本属性,如无边框、透明背景和大小."""
|
||||
self.setWindowFlags(Qt.WindowType.FramelessWindowHint)
|
||||
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
||||
self.resize(1280, 720)
|
||||
# 设置固定尺寸范围
|
||||
self.setMinimumSize(QSize(1024, 576))
|
||||
self.setMaximumSize(QSize(1280, 720))
|
||||
|
||||
# 初始化UI (从Ui_install.py导入)
|
||||
|
||||
def _init_ui(self):
|
||||
"""初始化UI组件."""
|
||||
self.ui = Ui_MainWindows()
|
||||
self.ui.setupUi(self)
|
||||
|
||||
# 初始化配置
|
||||
|
||||
def _init_config_and_tools(self):
|
||||
"""加载配置并初始化核心工具."""
|
||||
self.config = load_config()
|
||||
|
||||
# 初始化工具类
|
||||
self.hash_manager = HashManager(BLOCK_SIZE)
|
||||
self.admin_privileges = AdminPrivileges()
|
||||
|
||||
# 初始化各种管理器 - 调整初始化顺序,避免循环依赖
|
||||
# 1. 首先创建必要的基础管理器
|
||||
self.animator = MultiStageAnimations(self.ui, self)
|
||||
self.window_manager = WindowManager(self)
|
||||
self.debug_manager = DebugManager(self)
|
||||
|
||||
# 2. 初始化IPv6Manager(应在UIManager之前)
|
||||
self.ipv6_manager = IPv6Manager(self)
|
||||
|
||||
# 3. 创建UIManager(依赖IPv6Manager)
|
||||
self.ui_manager = UIManager(self)
|
||||
|
||||
# 4. 为debug_manager设置ui_manager引用
|
||||
self.debug_manager.set_ui_manager(self.ui_manager)
|
||||
|
||||
# 5. 初始化其他管理器
|
||||
self.config_manager = ConfigManager(APP_NAME, CONFIG_URL, UA, self.debug_manager)
|
||||
self.game_detector = GameDetector(GAME_INFO, self.debug_manager)
|
||||
self.patch_manager = PatchManager(APP_NAME, GAME_INFO, self.debug_manager)
|
||||
|
||||
# 6. 初始化离线模式管理器
|
||||
from core.offline_mode_manager import OfflineModeManager
|
||||
self.offline_mode_manager = OfflineModeManager(self)
|
||||
|
||||
# 7. 初始化下载管理器 - 放在最后,因为它可能依赖于其他管理器
|
||||
self.download_manager = DownloadManager(self)
|
||||
|
||||
# 8. 初始化功能处理程序
|
||||
self.uninstall_handler = UninstallHandler(self)
|
||||
self.patch_toggle_handler = PatchToggleHandler(self)
|
||||
|
||||
# 加载用户下载线程设置
|
||||
if "download_thread_level" in self.config and self.config["download_thread_level"] in DOWNLOAD_THREADS:
|
||||
self.download_manager.download_thread_level = self.config["download_thread_level"]
|
||||
self.patch_detector = PatchDetector(self)
|
||||
|
||||
# 初始化状态变量
|
||||
self.cloud_config = None
|
||||
self.config_valid = False # 添加配置有效标志
|
||||
self.patch_manager.initialize_status()
|
||||
self.installed_status = self.patch_manager.get_status() # 获取初始化后的状态
|
||||
self.hash_msg_box = None
|
||||
self.last_error_message = "" # 添加错误信息记录
|
||||
self.version_warning = False # 添加版本警告标志
|
||||
self.install_button_enabled = True # 默认启用安装按钮
|
||||
self.config_valid = False
|
||||
self.last_error_message = ""
|
||||
self.version_warning = False
|
||||
self.install_button_enabled = True
|
||||
self.progress_window = None
|
||||
self.pre_hash_thread = None
|
||||
self.hash_thread = None
|
||||
self.installed_status = {}
|
||||
self.hash_msg_box = None
|
||||
|
||||
# 设置关闭按钮事件连接
|
||||
# 资源验证已移除,素材通过正常加载流程使用
|
||||
|
||||
def _init_managers(self):
|
||||
"""初始化所有管理器."""
|
||||
self.animator = MultiStageAnimations(self.ui, self)
|
||||
self.window_manager = WindowManager(self)
|
||||
self.debug_manager = DebugManager(self)
|
||||
self.ipv6_manager = IPv6Manager(self)
|
||||
self.ui_manager = UIManager(self)
|
||||
self.debug_manager.set_ui_manager(self.ui_manager)
|
||||
self.config_manager = ConfigManager(APP_NAME, CONFIG_URL, UA, self.debug_manager)
|
||||
self.game_detector = GameDetector(GAME_INFO, self.debug_manager)
|
||||
self.patch_manager = PatchManager(APP_NAME, GAME_INFO, self.debug_manager, self)
|
||||
self.patch_manager.set_patch_detector(self.patch_detector)
|
||||
from core.managers.offline_mode_manager import OfflineModeManager
|
||||
self.offline_mode_manager = OfflineModeManager(self)
|
||||
self.download_manager = DownloadManager(self)
|
||||
self.uninstall_handler = UninstallHandler(self)
|
||||
self.patch_toggle_handler = PatchToggleHandler(self)
|
||||
|
||||
# Load user's download thread setting
|
||||
if "download_thread_level" in self.config and self.config["download_thread_level"] in DOWNLOAD_THREADS:
|
||||
self.download_manager.download_thread_level = self.config["download_thread_level"]
|
||||
|
||||
def _connect_signals(self):
|
||||
"""连接UI组件的信号到相应的槽函数."""
|
||||
if hasattr(self.ui, 'close_btn'):
|
||||
self.ui.close_btn.clicked.connect(self.close)
|
||||
|
||||
self.ui.close_btn.clicked.connect(self._on_close_clicked)
|
||||
if hasattr(self.ui, 'minimize_btn'):
|
||||
self.ui.minimize_btn.clicked.connect(self.showMinimized)
|
||||
self.ui.minimize_btn.clicked.connect(self._on_minimize_clicked)
|
||||
|
||||
# 创建缓存目录
|
||||
self.ui.start_install_btn.clicked.connect(self.handle_install_button_click)
|
||||
self.ui.uninstall_btn.clicked.connect(self.uninstall_handler.handle_uninstall_button_click)
|
||||
self.ui.toggle_patch_btn.clicked.connect(self.patch_toggle_handler.handle_toggle_patch_button_click)
|
||||
self.ui.exit_btn.clicked.connect(self.shutdown_app)
|
||||
|
||||
def _setup_environment(self):
|
||||
"""准备应用运行所需的环境,如创建缓存目录和检查权限."""
|
||||
if not os.path.exists(PLUGIN):
|
||||
try:
|
||||
os.makedirs(PLUGIN)
|
||||
except OSError as e:
|
||||
QtWidgets.QMessageBox.critical(
|
||||
self,
|
||||
f"错误 - {APP_NAME}",
|
||||
f"\n无法创建缓存位置\n\n使用管理员身份运行或检查文件读写权限\n\n【错误信息】:{e}\n",
|
||||
)
|
||||
QtWidgets.QMessageBox.critical(self, f"错误 - {APP_NAME}", f"无法创建缓存位置: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# 连接信号 - 绑定到新按钮
|
||||
self.ui.start_install_btn.clicked.connect(self.handle_install_button_click)
|
||||
self.ui.uninstall_btn.clicked.connect(self.uninstall_handler.handle_uninstall_button_click) # 使用卸载处理程序
|
||||
self.ui.toggle_patch_btn.clicked.connect(self.patch_toggle_handler.handle_toggle_patch_button_click) # 使用补丁切换处理程序
|
||||
self.ui.exit_btn.clicked.connect(self.shutdown_app)
|
||||
|
||||
# 初始化按钮状态标记
|
||||
self.install_button_enabled = False
|
||||
self.last_error_message = ""
|
||||
|
||||
# 检查管理员权限和进程
|
||||
try:
|
||||
# 检查管理员权限
|
||||
self.admin_privileges.request_admin_privileges()
|
||||
# 检查并终止相关进程
|
||||
self.admin_privileges.check_and_terminate_processes()
|
||||
except KeyboardInterrupt:
|
||||
logger.warning("权限检查或进程检查被用户中断")
|
||||
QtWidgets.QMessageBox.warning(
|
||||
self,
|
||||
f"警告 - {APP_NAME}",
|
||||
"\n操作被中断,请重新启动应用。\n"
|
||||
)
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
logger.error(f"权限检查或进程检查时发生错误: {e}")
|
||||
QtWidgets.QMessageBox.critical(
|
||||
self,
|
||||
f"错误 - {APP_NAME}",
|
||||
f"\n权限检查或进程检查时发生错误,请重新启动应用。\n\n【错误信息】:{e}\n"
|
||||
)
|
||||
logger.error(f"权限或进程检查失败: {e}")
|
||||
QtWidgets.QMessageBox.critical(self, f"错误 - {APP_NAME}", f"权限检查失败: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# 备份hosts文件
|
||||
self.download_manager.hosts_manager.backup()
|
||||
|
||||
# 根据初始配置决定是否开启Debug模式
|
||||
if "debug_mode" in self.config and self.config["debug_mode"]:
|
||||
# 先启用日志系统
|
||||
|
||||
def _setup_debug_mode(self):
|
||||
"""根据配置设置调试模式."""
|
||||
if self.config.get("debug_mode"):
|
||||
self.debug_manager.start_logging()
|
||||
logger.info("通过配置启动调试模式")
|
||||
# 检查UI设置
|
||||
if hasattr(self.ui_manager, 'debug_action') and self.ui_manager.debug_action:
|
||||
if self.ui_manager.debug_action.isChecked():
|
||||
# 如果通过UI启用了调试模式,确保日志系统已启动
|
||||
if not self.debug_manager.logger:
|
||||
self.debug_manager.start_logging()
|
||||
logger.info("通过UI启动调试模式")
|
||||
logger.debug("通过配置启动调试模式")
|
||||
|
||||
if hasattr(self.ui_manager, 'debug_action') and self.ui_manager.debug_action and self.ui_manager.debug_action.isChecked():
|
||||
if not self.debug_manager.logger:
|
||||
self.debug_manager.start_logging()
|
||||
logger.debug("通过UI启动调试模式")
|
||||
|
||||
# 设置UI,包括窗口图标和菜单
|
||||
self.ui_manager.setup_ui()
|
||||
|
||||
# 检查是否有离线补丁文件,如果有则自动切换到离线模式
|
||||
self.check_and_set_offline_mode()
|
||||
|
||||
# 获取云端配置
|
||||
self.fetch_cloud_config()
|
||||
|
||||
# 启动动画
|
||||
self.start_animations()
|
||||
|
||||
# 窗口事件处理 - 委托给WindowManager
|
||||
def mousePressEvent(self, event):
|
||||
@@ -225,9 +191,9 @@ class MainWindow(QMainWindow):
|
||||
self.animation_in_progress = False
|
||||
|
||||
# 启用所有菜单按钮
|
||||
self.ui.start_install_btn.setEnabled(True)
|
||||
# 按钮状态由WindowManager统一管理
|
||||
self.ui.uninstall_btn.setEnabled(True)
|
||||
self.ui.toggle_patch_btn.setEnabled(True) # 启用禁/启用补丁按钮
|
||||
self.ui.toggle_patch_btn.setEnabled(True)
|
||||
self.ui.exit_btn.setEnabled(True)
|
||||
|
||||
# 检查是否处于离线模式
|
||||
@@ -235,35 +201,31 @@ class MainWindow(QMainWindow):
|
||||
if hasattr(self, 'offline_mode_manager'):
|
||||
is_offline_mode = self.offline_mode_manager.is_in_offline_mode()
|
||||
|
||||
# 如果是离线模式,始终启用开始安装按钮
|
||||
if is_offline_mode:
|
||||
self.set_start_button_enabled(True)
|
||||
# 否则,只有在配置有效时才启用开始安装按钮
|
||||
elif self.config_valid:
|
||||
self.set_start_button_enabled(True)
|
||||
# 根据离线模式和配置状态设置按钮
|
||||
if is_offline_mode or self.config_valid:
|
||||
self.window_manager.change_window_state(self.window_manager.STATE_READY)
|
||||
else:
|
||||
self.set_start_button_enabled(False)
|
||||
self.window_manager.change_window_state(self.window_manager.STATE_ERROR)
|
||||
|
||||
# 确保工作模式菜单状态与实际状态同步
|
||||
if hasattr(self, 'ui_manager') and hasattr(self.ui_manager, 'sync_work_mode_menu_state'):
|
||||
self.ui_manager.sync_work_mode_menu_state()
|
||||
|
||||
def set_start_button_enabled(self, enabled, installing=False):
|
||||
"""[已弃用] 设置按钮启用状态的旧方法,保留以兼容旧代码
|
||||
"""[过渡方法] 设置按钮状态,将调用委托给WindowManager
|
||||
|
||||
现在推荐使用主窗口的setEnabled方法和直接设置按钮文本
|
||||
这个方法将逐步被淘汰,请使用 self.window_manager.change_window_state()
|
||||
|
||||
Args:
|
||||
enabled: 是否启用按钮
|
||||
installing: 是否正在安装中
|
||||
"""
|
||||
# 直接设置按钮文本,不改变窗口启用状态
|
||||
if installing:
|
||||
self.ui.start_install_text.setText("正在安装")
|
||||
self.install_button_enabled = False
|
||||
self.window_manager.change_window_state(self.window_manager.STATE_INSTALLING)
|
||||
elif enabled:
|
||||
self.window_manager.change_window_state(self.window_manager.STATE_READY)
|
||||
else:
|
||||
if enabled:
|
||||
self.ui.start_install_text.setText("开始安装")
|
||||
else:
|
||||
self.ui.start_install_text.setText("!无法安装!")
|
||||
|
||||
self.install_button_enabled = enabled
|
||||
self.window_manager.change_window_state(self.window_manager.STATE_ERROR)
|
||||
|
||||
def fetch_cloud_config(self):
|
||||
"""获取云端配置(异步方式)"""
|
||||
@@ -282,6 +244,11 @@ class MainWindow(QMainWindow):
|
||||
# 处理返回结果
|
||||
result = self.config_manager.on_config_fetched(data, error_message)
|
||||
|
||||
# 先同步状态
|
||||
self.cloud_config = self.config_manager.get_cloud_config()
|
||||
self.config_valid = self.config_manager.is_config_valid()
|
||||
self.last_error_message = self.config_manager.get_last_error()
|
||||
|
||||
# 根据返回的操作执行相应动作
|
||||
if result and "action" in result:
|
||||
if result["action"] == "exit":
|
||||
@@ -289,21 +256,16 @@ class MainWindow(QMainWindow):
|
||||
self.shutdown_app(force_exit=True)
|
||||
elif result["action"] == "disable_button":
|
||||
# 禁用开始安装按钮
|
||||
self.set_start_button_enabled(False)
|
||||
self.window_manager.change_window_state(self.window_manager.STATE_ERROR)
|
||||
elif result["action"] == "enable_button":
|
||||
# 启用开始安装按钮
|
||||
self.set_start_button_enabled(True)
|
||||
self.window_manager.change_window_state(self.window_manager.STATE_READY)
|
||||
# 检查是否需要记录版本警告
|
||||
if "version_warning" in result and result["version_warning"]:
|
||||
self.version_warning = True
|
||||
else:
|
||||
self.version_warning = False
|
||||
|
||||
# 同步状态
|
||||
self.cloud_config = self.config_manager.get_cloud_config()
|
||||
self.config_valid = self.config_manager.is_config_valid()
|
||||
self.last_error_message = self.config_manager.get_last_error()
|
||||
|
||||
# 重新启用窗口,恢复用户交互
|
||||
self.setEnabled(True)
|
||||
|
||||
@@ -318,170 +280,11 @@ class MainWindow(QMainWindow):
|
||||
def save_config(self, config):
|
||||
"""保存配置的便捷方法"""
|
||||
self.config_manager.save_config(config)
|
||||
|
||||
def create_download_thread(self, url, _7z_path, game_version):
|
||||
"""创建下载线程
|
||||
|
||||
Args:
|
||||
url: 下载URL
|
||||
_7z_path: 7z文件保存路径
|
||||
game_version: 游戏版本名称
|
||||
|
||||
Returns:
|
||||
DownloadThread: 下载线程实例
|
||||
"""
|
||||
from workers import DownloadThread
|
||||
return DownloadThread(url, _7z_path, game_version, parent=self)
|
||||
|
||||
def create_progress_window(self):
|
||||
"""创建下载进度窗口
|
||||
|
||||
Returns:
|
||||
ProgressWindow: 进度窗口实例
|
||||
"""
|
||||
return ProgressWindow(self)
|
||||
|
||||
def create_hash_thread(self, mode, install_paths):
|
||||
"""创建哈希检查线程
|
||||
|
||||
Args:
|
||||
mode: 检查模式,"pre"或"after"
|
||||
install_paths: 安装路径字典
|
||||
|
||||
Returns:
|
||||
HashThread: 哈希检查线程实例
|
||||
"""
|
||||
return HashThread(mode, install_paths, PLUGIN_HASH, self.installed_status, self)
|
||||
|
||||
def create_extraction_thread(self, _7z_path, game_folder, plugin_path, game_version):
|
||||
"""创建解压线程
|
||||
|
||||
Args:
|
||||
_7z_path: 7z文件路径
|
||||
game_folder: 游戏文件夹路径
|
||||
plugin_path: 插件路径
|
||||
game_version: 游戏版本
|
||||
|
||||
Returns:
|
||||
ExtractionThread: 解压线程实例
|
||||
"""
|
||||
return ExtractionThread(_7z_path, game_folder, plugin_path, game_version, self)
|
||||
|
||||
def after_hash_compare(self):
|
||||
"""进行安装后哈希比较"""
|
||||
# 禁用窗口已在安装流程开始时完成
|
||||
|
||||
# 检查是否处于离线模式
|
||||
is_offline = False
|
||||
if hasattr(self, 'offline_mode_manager'):
|
||||
is_offline = self.offline_mode_manager.is_in_offline_mode()
|
||||
|
||||
self.hash_msg_box = self.hash_manager.hash_pop_window(check_type="after", is_offline=is_offline)
|
||||
|
||||
install_paths = self.download_manager.get_install_paths()
|
||||
|
||||
self.hash_thread = self.create_hash_thread("after", install_paths)
|
||||
self.hash_thread.after_finished.connect(self.on_after_hash_finished)
|
||||
self.hash_thread.start()
|
||||
|
||||
def on_after_hash_finished(self, result):
|
||||
"""哈希比较完成后的处理
|
||||
|
||||
Args:
|
||||
result: 哈希比较结果
|
||||
"""
|
||||
# 确保哈希检查窗口关闭,无论是否还在显示
|
||||
if self.hash_msg_box:
|
||||
try:
|
||||
if self.hash_msg_box.isVisible():
|
||||
self.hash_msg_box.close()
|
||||
else:
|
||||
# 如果窗口已经不可见但没有关闭,也要尝试关闭
|
||||
self.hash_msg_box.close()
|
||||
except:
|
||||
pass # 忽略任何关闭窗口时的错误
|
||||
self.hash_msg_box = None
|
||||
|
||||
if not result["passed"]:
|
||||
# 启用窗口以显示错误消息
|
||||
self.setEnabled(True)
|
||||
|
||||
game = result.get("game", "未知游戏")
|
||||
message = result.get("message", "发生未知错误。")
|
||||
msg_box = msgbox_frame(
|
||||
f"文件校验失败 - {APP_NAME}",
|
||||
message,
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
msg_box.exec()
|
||||
|
||||
# 恢复窗口状态
|
||||
self.setEnabled(True)
|
||||
self.ui.start_install_text.setText("开始安装")
|
||||
|
||||
# 添加短暂延迟确保UI更新
|
||||
QTimer.singleShot(100, self.show_result)
|
||||
|
||||
def show_result(self):
|
||||
"""显示安装结果,区分不同情况"""
|
||||
# 获取当前安装状态
|
||||
installed_versions = [] # 成功安装的版本
|
||||
skipped_versions = [] # 已有补丁跳过的版本
|
||||
failed_versions = [] # 安装失败的版本
|
||||
not_found_versions = [] # 未找到的版本
|
||||
"""显示安装结果,调用patch_manager的show_result方法"""
|
||||
self.patch_manager.show_result()
|
||||
|
||||
# 获取所有游戏版本路径
|
||||
install_paths = self.download_manager.get_install_paths() if hasattr(self.download_manager, "get_install_paths") else {}
|
||||
|
||||
for game_version, is_installed in self.installed_status.items():
|
||||
# 只处理install_paths中存在的游戏版本
|
||||
if game_version in install_paths:
|
||||
path = install_paths[game_version]
|
||||
|
||||
# 检查游戏是否存在但未通过本次安装补丁
|
||||
if is_installed:
|
||||
# 游戏已安装补丁
|
||||
if hasattr(self, 'download_queue_history') and game_version not in self.download_queue_history:
|
||||
# 已有补丁,被跳过下载
|
||||
skipped_versions.append(game_version)
|
||||
else:
|
||||
# 本次成功安装
|
||||
installed_versions.append(game_version)
|
||||
else:
|
||||
# 游戏未安装补丁
|
||||
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,
|
||||
f"安装完成 - {APP_NAME}",
|
||||
result_text
|
||||
)
|
||||
|
||||
def closeEvent(self, event):
|
||||
"""窗口关闭事件处理
|
||||
|
||||
@@ -491,50 +294,38 @@ class MainWindow(QMainWindow):
|
||||
self.shutdown_app(event)
|
||||
|
||||
def shutdown_app(self, event=None, force_exit=False):
|
||||
"""关闭应用程序
|
||||
"""关闭应用程序"""
|
||||
logger = setup_logger("main_window")
|
||||
logger.info("用户点击退出按钮")
|
||||
logger.debug("开始关闭应用程序")
|
||||
|
||||
Args:
|
||||
event: 关闭事件,如果是从closeEvent调用的
|
||||
force_exit: 是否强制退出
|
||||
"""
|
||||
# 检查是否有动画或任务正在进行
|
||||
if hasattr(self, 'animation_in_progress') and self.animation_in_progress and not force_exit:
|
||||
# 如果动画正在进行,阻止退出
|
||||
if event:
|
||||
event.ignore()
|
||||
return
|
||||
|
||||
# 检查是否有下载任务正在进行
|
||||
if hasattr(self.download_manager, 'current_download_thread') and \
|
||||
self.download_manager.current_download_thread and \
|
||||
self.download_manager.current_download_thread.isRunning() and not force_exit:
|
||||
# 询问用户是否确认退出
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
f"确认退出 - {APP_NAME}",
|
||||
"\n下载任务正在进行中,确定要退出吗?\n",
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||
QMessageBox.StandardButton.No
|
||||
)
|
||||
if reply == QMessageBox.StandardButton.No:
|
||||
if event:
|
||||
event.ignore()
|
||||
return
|
||||
|
||||
# 恢复hosts文件(如果未禁用自动还原)
|
||||
self.download_manager.hosts_manager.restore()
|
||||
|
||||
threads_to_stop = {
|
||||
'pre_hash': getattr(self, 'pre_hash_thread', None),
|
||||
'hash': getattr(self, 'hash_thread', None),
|
||||
'offline_hash': getattr(self.offline_mode_manager, 'hash_thread', None),
|
||||
'extraction': getattr(self.offline_mode_manager, 'extraction_thread', None),
|
||||
'config_fetch': getattr(self.config_manager, 'config_fetch_thread', None),
|
||||
'game_detector': getattr(self.game_detector, 'detection_thread', None),
|
||||
'patch_check': getattr(self.patch_detector, 'patch_check_thread', None)
|
||||
}
|
||||
|
||||
# 额外检查并清理hosts文件中的残留记录(如果未禁用自动还原)
|
||||
self.download_manager.hosts_manager.check_and_clean_all_entries()
|
||||
|
||||
# 停止日志记录
|
||||
# Add current download thread if it's running
|
||||
if hasattr(self.download_manager, 'current_download_thread') and self.download_manager.current_download_thread:
|
||||
threads_to_stop['download'] = self.download_manager.current_download_thread
|
||||
|
||||
self.download_manager.graceful_stop_threads(threads_to_stop)
|
||||
|
||||
|
||||
self.debug_manager.stop_logging()
|
||||
|
||||
if not force_exit:
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
f"确认退出 - {APP_NAME}",
|
||||
"\n确定要退出吗?\n",
|
||||
self, f"确认退出 - {APP_NAME}", "\n确定要退出吗?\n",
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||
QMessageBox.StandardButton.No
|
||||
)
|
||||
@@ -542,8 +333,15 @@ class MainWindow(QMainWindow):
|
||||
if event:
|
||||
event.ignore()
|
||||
return
|
||||
|
||||
# 用户确认退出后,再执行hosts相关操作
|
||||
self.download_manager.hosts_manager.restore()
|
||||
self.download_manager.hosts_manager.check_and_clean_all_entries()
|
||||
else:
|
||||
# 强制退出时,也需执行hosts相关操作
|
||||
self.download_manager.hosts_manager.restore()
|
||||
self.download_manager.hosts_manager.check_and_clean_all_entries()
|
||||
|
||||
# 退出应用程序
|
||||
if event:
|
||||
event.accept()
|
||||
else:
|
||||
@@ -553,6 +351,9 @@ class MainWindow(QMainWindow):
|
||||
"""处理安装按钮点击事件
|
||||
根据按钮当前状态决定是显示错误还是执行安装
|
||||
"""
|
||||
logger = setup_logger("main_window")
|
||||
logger.info("用户点击开始安装按钮")
|
||||
logger.debug("开始处理安装按钮点击事件")
|
||||
# 检查是否处于离线模式
|
||||
is_offline_mode = False
|
||||
if hasattr(self, 'offline_mode_manager'):
|
||||
@@ -612,103 +413,20 @@ class MainWindow(QMainWindow):
|
||||
# 重试获取配置
|
||||
self.fetch_cloud_config()
|
||||
else:
|
||||
# 按钮处于"开始安装"状态,正常执行安装流程
|
||||
# 检查是否处于离线模式
|
||||
if is_offline_mode:
|
||||
# 如果是离线模式,使用离线安装流程
|
||||
# 先选择游戏目录
|
||||
if self.offline_mode_manager.is_in_offline_mode():
|
||||
self.selected_folder = QtWidgets.QFileDialog.getExistingDirectory(
|
||||
self, f"选择游戏所在【上级目录】 {APP_NAME}"
|
||||
)
|
||||
if not self.selected_folder:
|
||||
QtWidgets.QMessageBox.warning(
|
||||
self, f"通知 - {APP_NAME}", "\n未选择任何目录,请重新选择\n"
|
||||
)
|
||||
QtWidgets.QMessageBox.warning(self, f"通知 - {APP_NAME}", "\n未选择任何目录,请重新选择\n")
|
||||
return
|
||||
|
||||
# 保存选择的目录到下载管理器
|
||||
self.download_manager.selected_folder = self.selected_folder
|
||||
|
||||
# 设置按钮状态
|
||||
self.ui.start_install_text.setText("正在安装")
|
||||
self.ui_manager.show_loading_dialog("正在识别游戏目录...")
|
||||
self.setEnabled(False)
|
||||
|
||||
# 清除游戏检测器的目录缓存
|
||||
if hasattr(self, 'game_detector') and hasattr(self.game_detector, 'clear_directory_cache'):
|
||||
self.game_detector.clear_directory_cache()
|
||||
|
||||
# 识别游戏目录
|
||||
game_dirs = self.game_detector.identify_game_directories_improved(self.selected_folder)
|
||||
|
||||
if not game_dirs:
|
||||
self.last_error_message = "directory_not_found"
|
||||
QtWidgets.QMessageBox.warning(
|
||||
self,
|
||||
f"目录错误 - {APP_NAME}",
|
||||
"\n未能识别到任何游戏目录。\n\n请确认您选择的是游戏的上级目录,并且该目录中包含NEKOPARA系列游戏文件夹。\n"
|
||||
)
|
||||
self.setEnabled(True)
|
||||
self.ui.start_install_text.setText("开始安装")
|
||||
return
|
||||
|
||||
# 显示游戏选择对话框
|
||||
dialog = QtWidgets.QDialog(self)
|
||||
dialog.setWindowTitle("选择要安装的游戏")
|
||||
dialog.resize(400, 300)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(dialog)
|
||||
|
||||
# 添加"选择要安装的游戏"标签
|
||||
title_label = QtWidgets.QLabel("选择要安装的游戏", dialog)
|
||||
title_label.setFont(QFont(title_label.font().family(), title_label.font().pointSize(), QFont.Bold))
|
||||
layout.addWidget(title_label)
|
||||
|
||||
# 添加游戏列表控件
|
||||
list_widget = QtWidgets.QListWidget(dialog)
|
||||
list_widget.setSelectionMode(QtWidgets.QAbstractItemView.SelectionMode.ExtendedSelection) # 允许多选
|
||||
for game_version in game_dirs.keys():
|
||||
list_widget.addItem(game_version)
|
||||
# 默认选中所有项目
|
||||
list_widget.item(list_widget.count() - 1).setSelected(True)
|
||||
layout.addWidget(list_widget)
|
||||
|
||||
# 添加全选按钮
|
||||
select_all_btn = QtWidgets.QPushButton("全选", dialog)
|
||||
select_all_btn.clicked.connect(lambda: list_widget.selectAll())
|
||||
layout.addWidget(select_all_btn)
|
||||
|
||||
# 添加确定和取消按钮
|
||||
buttons_layout = QtWidgets.QHBoxLayout()
|
||||
ok_button = QtWidgets.QPushButton("确定", dialog)
|
||||
cancel_button = QtWidgets.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)
|
||||
|
||||
# 显示对话框
|
||||
if dialog.exec() == QtWidgets.QDialog.DialogCode.Accepted:
|
||||
# 获取选择的游戏
|
||||
selected_games = [item.text() for item in list_widget.selectedItems()]
|
||||
|
||||
if selected_games:
|
||||
# 使用离线模式管理器进行安装
|
||||
self.offline_mode_manager.install_offline_patches(selected_games)
|
||||
else:
|
||||
QtWidgets.QMessageBox.information(
|
||||
self,
|
||||
f"通知 - {APP_NAME}",
|
||||
"\n未选择任何游戏,安装已取消。\n"
|
||||
)
|
||||
self.setEnabled(True)
|
||||
self.ui.start_install_text.setText("开始安装")
|
||||
else:
|
||||
# 用户取消了选择
|
||||
self.setEnabled(True)
|
||||
self.ui.start_install_text.setText("开始安装")
|
||||
|
||||
# 异步识别游戏目录
|
||||
self.game_detector.identify_game_directories_async(self.selected_folder, self.on_game_directories_identified)
|
||||
else:
|
||||
# 在线模式下,检查版本是否过低
|
||||
if hasattr(self, 'version_warning') and self.version_warning:
|
||||
@@ -723,6 +441,42 @@ class MainWindow(QMainWindow):
|
||||
# 版本正常,使用原有的下载流程
|
||||
self.download_manager.file_dialog()
|
||||
|
||||
def on_game_directories_identified(self, game_dirs):
|
||||
self.ui_manager.hide_loading_dialog()
|
||||
|
||||
if not game_dirs:
|
||||
self.setEnabled(True)
|
||||
self.window_manager.change_window_state(self.window_manager.STATE_READY)
|
||||
QtWidgets.QMessageBox.warning(
|
||||
self,
|
||||
f"目录错误 - {APP_NAME}",
|
||||
"\n未能识别到任何游戏目录。\n\n请确认您选择的是游戏的上级目录,并且该目录中包含NEKOPARA系列游戏文件夹。\n"
|
||||
)
|
||||
return
|
||||
|
||||
self.ui_manager.show_loading_dialog("正在检查补丁状态...")
|
||||
|
||||
install_paths = self.download_manager.get_install_paths()
|
||||
|
||||
# 使用异步方式进行哈希预检查
|
||||
self.pre_hash_thread = self.patch_detector.create_hash_thread("pre", install_paths)
|
||||
self.pre_hash_thread.pre_finished.connect(
|
||||
lambda updated_status: self.on_pre_hash_finished(updated_status, game_dirs)
|
||||
)
|
||||
# 在线程自然结束时清理引用
|
||||
try:
|
||||
self.pre_hash_thread.finished.connect(lambda: setattr(self, 'pre_hash_thread', None))
|
||||
except Exception:
|
||||
pass
|
||||
self.pre_hash_thread.start()
|
||||
|
||||
def on_pre_hash_finished(self, updated_status, game_dirs):
|
||||
self.ui_manager.hide_loading_dialog()
|
||||
self.setEnabled(True)
|
||||
self.patch_detector.on_offline_pre_hash_finished(updated_status, game_dirs)
|
||||
|
||||
|
||||
|
||||
def check_and_set_offline_mode(self):
|
||||
"""检查是否有离线补丁文件,如果有则自动启用离线模式
|
||||
|
||||
@@ -732,9 +486,32 @@ class MainWindow(QMainWindow):
|
||||
try:
|
||||
# 初始化离线模式管理器
|
||||
if not hasattr(self, 'offline_mode_manager') or self.offline_mode_manager is None:
|
||||
from core.offline_mode_manager import OfflineModeManager
|
||||
from core.managers.offline_mode_manager import OfflineModeManager
|
||||
self.offline_mode_manager = OfflineModeManager(self)
|
||||
|
||||
# 在调试模式下记录当前执行路径
|
||||
is_debug_mode = self.config.get('debug_mode', False) if hasattr(self, 'config') else False
|
||||
if is_debug_mode:
|
||||
import os
|
||||
import sys
|
||||
current_dir = os.getcwd()
|
||||
logger.debug(f"DEBUG: 当前工作目录: {current_dir}")
|
||||
logger.debug(f"DEBUG: 是否为打包环境: {getattr(sys, 'frozen', False)}")
|
||||
if getattr(sys, 'frozen', False):
|
||||
logger.debug(f"DEBUG: 可执行文件路径: {sys.executable}")
|
||||
|
||||
# 尝试列出当前目录中的文件(调试用)
|
||||
try:
|
||||
files = os.listdir(current_dir)
|
||||
logger.debug(f"DEBUG: 当前目录文件列表: {files}")
|
||||
|
||||
# 检查上级目录
|
||||
parent_dir = os.path.dirname(current_dir)
|
||||
parent_files = os.listdir(parent_dir)
|
||||
logger.debug(f"DEBUG: 上级目录 {parent_dir} 文件列表: {parent_files}")
|
||||
except Exception as e:
|
||||
logger.debug(f"DEBUG: 列出目录文件时出错: {str(e)}")
|
||||
|
||||
# 扫描离线补丁文件
|
||||
self.offline_mode_manager.scan_for_offline_patches()
|
||||
|
||||
@@ -745,8 +522,20 @@ class MainWindow(QMainWindow):
|
||||
# 启用开始安装按钮
|
||||
self.set_start_button_enabled(True)
|
||||
|
||||
logger.debug(f"DEBUG: 已自动切换到离线模式,找到离线补丁文件: {list(self.offline_mode_manager.offline_patches.keys())}")
|
||||
# 记录日志
|
||||
found_patches = list(self.offline_mode_manager.offline_patches.keys())
|
||||
logger.debug(f"DEBUG: 已自动切换到离线模式,找到离线补丁文件: {found_patches}")
|
||||
logger.info(f"发现离线补丁文件: {found_patches},将自动切换到离线模式")
|
||||
logger.debug(f"DEBUG: 离线模式下启用开始安装按钮")
|
||||
|
||||
# 显示提示弹窗
|
||||
from PySide6.QtWidgets import QMessageBox
|
||||
QMessageBox.information(
|
||||
self,
|
||||
f"离线模式提示 - {APP_NAME}",
|
||||
f"已找到本地补丁,将主动转为离线模式。\n\n检测到的补丁文件: {', '.join(found_patches)}"
|
||||
)
|
||||
|
||||
return True
|
||||
else:
|
||||
# 如果没有找到离线补丁文件,禁用离线模式
|
||||
@@ -769,8 +558,60 @@ class MainWindow(QMainWindow):
|
||||
self.set_start_button_enabled(False)
|
||||
|
||||
logger.error(f"错误: 检查离线模式时发生异常: {e}")
|
||||
logger.error(f"错误详情: {traceback.format_exc()}")
|
||||
return False
|
||||
|
||||
def close_hash_msg_box(self):
|
||||
"""关闭哈希校验窗口,确保在创建新窗口前关闭旧窗口"""
|
||||
if hasattr(self, 'hash_msg_box') and self.hash_msg_box:
|
||||
try:
|
||||
if self.hash_msg_box.isVisible():
|
||||
self.hash_msg_box.close()
|
||||
QtWidgets.QApplication.processEvents() # 确保UI更新,窗口真正关闭
|
||||
except Exception as e:
|
||||
logger.error(f"关闭哈希校验窗口时发生错误: {e}")
|
||||
self.hash_msg_box = None
|
||||
|
||||
def create_progress_window(self, title="下载进度", initial_text="准备中..."):
|
||||
"""创建一个用于显示下载进度的窗口
|
||||
|
||||
Args:
|
||||
title (str): 窗口标题,默认为"下载进度"
|
||||
initial_text (str): 初始状态文本,默认为"准备中..."
|
||||
|
||||
Returns:
|
||||
进度窗口实例
|
||||
"""
|
||||
return self.ui_manager.create_progress_window(title, initial_text)
|
||||
|
||||
|
||||
def create_extraction_progress_window(self):
|
||||
"""创建一个用于显示解压进度的窗口
|
||||
|
||||
Returns:
|
||||
解压进度窗口实例
|
||||
"""
|
||||
return self.ui_manager.create_progress_window("解压进度", "正在准备解压...")
|
||||
|
||||
def show_loading_dialog(self, message):
|
||||
"""显示加载对话框
|
||||
|
||||
Args:
|
||||
message: 要显示的消息
|
||||
"""
|
||||
self.ui_manager.show_loading_dialog(message)
|
||||
|
||||
def _on_close_clicked(self):
|
||||
"""处理关闭按钮点击"""
|
||||
logger = setup_logger("main_window")
|
||||
logger.info("用户点击关闭按钮")
|
||||
self.close()
|
||||
|
||||
def _on_minimize_clicked(self):
|
||||
"""处理最小化按钮点击"""
|
||||
logger = setup_logger("main_window")
|
||||
logger.info("用户点击最小化按钮")
|
||||
self.showMinimized()
|
||||
|
||||
def hide_loading_dialog(self):
|
||||
"""隐藏加载对话框"""
|
||||
self.ui_manager.hide_loading_dialog()
|
||||
@@ -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,7 +383,11 @@ 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}")
|
||||
|
||||
# 创建文本标签布局的按钮
|
||||
# 开始安装按钮 - 基于背景图片和标签组合
|
||||
|
||||
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")
|
||||
@@ -9,31 +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")
|
||||
|
||||
def resource_path(relative_path):
|
||||
"""获取资源的绝对路径,适用于开发环境和Nuitka打包环境"""
|
||||
if getattr(sys, 'frozen', False):
|
||||
# Nuitka/PyInstaller创建的临时文件夹,并将路径存储在_MEIPASS中或与可执行文件同目录
|
||||
if hasattr(sys, '_MEIPASS'):
|
||||
base_path = sys._MEIPASS
|
||||
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:
|
||||
base_path = os.path.dirname(sys.executable)
|
||||
else:
|
||||
# 在开发环境中运行
|
||||
base_path = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||
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):
|
||||
"""获取资源的绝对路径,适用于开发环境和打包环境"""
|
||||
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.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()
|
||||
@@ -49,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()
|
||||
@@ -59,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():
|
||||
@@ -116,12 +273,14 @@ class HashManager:
|
||||
logger.error(f"Error calculating hash for {file_path}: {e}")
|
||||
return results
|
||||
|
||||
def hash_pop_window(self, check_type="default", is_offline=False):
|
||||
def hash_pop_window(self, check_type="default", is_offline=False, auto_close=False, close_delay=500):
|
||||
"""显示文件检验窗口
|
||||
|
||||
Args:
|
||||
check_type: 检查类型,可以是 'pre'(预检查), 'after'(后检查), 'extraction'(解压后检查), 'offline_extraction'(离线解压), 'offline_verify'(离线验证)
|
||||
is_offline: 是否处于离线模式
|
||||
auto_close: 是否自动关闭窗口
|
||||
close_delay: 自动关闭延迟(毫秒)
|
||||
|
||||
Returns:
|
||||
QMessageBox: 消息框实例
|
||||
@@ -138,6 +297,8 @@ class HashManager:
|
||||
message = "\n正在验证本地补丁压缩文件完整性...\n"
|
||||
elif check_type == "offline_extraction":
|
||||
message = "\n正在解压安装补丁文件...\n"
|
||||
elif check_type == "offline_installation":
|
||||
message = "\n正在安装补丁文件...\n"
|
||||
else:
|
||||
message = "\n正在处理离线补丁文件...\n"
|
||||
else:
|
||||
@@ -148,10 +309,27 @@ class HashManager:
|
||||
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):
|
||||
@@ -160,7 +338,7 @@ class HashManager:
|
||||
|
||||
# 尝试检测是否处于调试模式
|
||||
try:
|
||||
from data.config import CACHE
|
||||
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:
|
||||
@@ -210,7 +388,7 @@ class HashManager:
|
||||
|
||||
# 尝试检测是否处于调试模式
|
||||
try:
|
||||
from data.config import CACHE
|
||||
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:
|
||||
@@ -436,7 +614,7 @@ class HostsManager:
|
||||
self.original_content = f.read()
|
||||
with open(self.backup_path, 'w', encoding='utf-8') as f:
|
||||
f.write(self.original_content)
|
||||
logger.info(f"Hosts文件已备份到: {self.backup_path}")
|
||||
logger.debug(f"Hosts文件已备份到: {self.backup_path}")
|
||||
return True
|
||||
except IOError as e:
|
||||
logger.error(f"备份hosts文件失败: {e}")
|
||||
@@ -567,14 +745,17 @@ class HostsManager:
|
||||
self.auto_restore_disabled = auto_restore_disabled
|
||||
return auto_restore_disabled
|
||||
|
||||
def check_and_clean_all_entries(self):
|
||||
def check_and_clean_all_entries(self, force_clean=False):
|
||||
"""检查并清理所有由本应用程序添加的hosts记录
|
||||
|
||||
Args:
|
||||
force_clean: 是否强制清理,即使禁用了自动还原
|
||||
|
||||
Returns:
|
||||
bool: 清理是否成功
|
||||
"""
|
||||
# 如果禁用了自动还原,则不执行清理操作
|
||||
if self.is_auto_restore_disabled():
|
||||
# 如果禁用了自动还原,且不是强制清理,则不执行清理操作
|
||||
if self.is_auto_restore_disabled() and not force_clean:
|
||||
logger.info("已禁用自动还原hosts,跳过清理操作")
|
||||
return True
|
||||
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
from .url_censor 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"""
|
||||
@@ -44,7 +51,7 @@ class Logger:
|
||||
except Exception as e:
|
||||
# 发生错误时记录到控制台
|
||||
self.terminal.write(f"Error writing to log: {e}\n")
|
||||
|
||||
|
||||
def flush(self):
|
||||
try:
|
||||
self.terminal.flush()
|
||||
@@ -62,65 +69,120 @@ class Logger:
|
||||
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对象
|
||||
"""
|
||||
# 导入LOG_FILE
|
||||
from data.config import LOG_FILE
|
||||
|
||||
# 创建logger
|
||||
logger = logging.getLogger(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)
|
||||
|
||||
# 创建主日志文件处理器 - 所有日志合并到主LOG_FILE
|
||||
# 创建主日志文件的轮转处理器
|
||||
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}")
|
||||
|
||||
main_file_handler = logging.FileHandler(LOG_FILE, encoding="utf-8", mode="w")
|
||||
main_file_handler.setLevel(logging.DEBUG)
|
||||
|
||||
# 使用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 = URLCensorFormatter('%(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
|
||||
@@ -16,7 +16,7 @@ def censor_url(text):
|
||||
return text # 直接返回原始文本,不做任何隐藏
|
||||
|
||||
# 以下是原始代码,现在被注释掉
|
||||
'''
|
||||
r'''
|
||||
# 匹配URL并替换为固定文本
|
||||
url_pattern = re.compile(r'https?://[^\s/$.?#].[^\s]*')
|
||||
censored = url_pattern.sub('***URL protection***', text)
|
||||
|
||||
@@ -22,7 +22,7 @@ class ConfigFetchThread(QThread):
|
||||
def run(self):
|
||||
try:
|
||||
if self.debug_mode:
|
||||
logger.info("--- Starting to fetch cloud config ---")
|
||||
logger.debug("--- Starting to fetch cloud config ---")
|
||||
# 完全隐藏URL
|
||||
logger.debug(f"DEBUG: Requesting URL: ***URL protection***")
|
||||
logger.debug(f"DEBUG: Using Headers: {self.headers}")
|
||||
@@ -72,7 +72,7 @@ class ConfigFetchThread(QThread):
|
||||
self.finished.emit(None, error_msg)
|
||||
finally:
|
||||
if self.debug_mode:
|
||||
logger.info("--- Finished fetching cloud config ---")
|
||||
logger.debug("--- Finished fetching cloud config ---")
|
||||
|
||||
def _create_safe_config_for_logging(self, config_data):
|
||||
"""创建用于日志记录的安全配置副本,隐藏敏感URL
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)}", "")
|
||||