This commit is contained in:
欧阳淇淇
2025-12-16 19:48:19 +08:00
73 changed files with 6212 additions and 2787 deletions

8
.gitignore vendored
View File

@@ -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

View File

@@ -21,6 +21,10 @@
- 游戏安装路径:用于识别已安装的游戏和安装补丁
- 文件哈希值:用于验证文件完整性
### 2.4 离线模式和本地文件
- **离线模式**:本应用提供离线模式。在离线模式下,应用不会进行任何网络连接,包括检查更新、获取云端配置或进行任何网络相关的测试,安装过程将只使用本地文件。
- **本地文件使用**:为了支持离线安装,本应用会扫描其所在目录下的压缩包,以查找用于安装的补丁压缩包。此文件扫描操作仅限于应用所在的文件夹,不会访问或修改您系统中的其他文件。
## 3. 信息使用
我们收集的信息仅用于以下目的:
@@ -88,4 +92,4 @@
本隐私政策可能会根据应用功能的变化而更新。请定期查看最新版本。
最后更新日期2025年8月4
最后更新日期2025年8月15

View File

@@ -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())

View File

@@ -0,0 +1,7 @@
# Assets package initialization
"""
包含应用程序使用的静态资源文件:
- fonts: 字体文件
- images: 图片资源
- resources: 其他资源文件
"""

View File

Before

Width:  |  Height:  |  Size: 162 KiB

After

Width:  |  Height:  |  Size: 162 KiB

View File

Before

Width:  |  Height:  |  Size: 179 KiB

After

Width:  |  Height:  |  Size: 179 KiB

View File

Before

Width:  |  Height:  |  Size: 571 KiB

After

Width:  |  Height:  |  Size: 571 KiB

View File

Before

Width:  |  Height:  |  Size: 250 KiB

After

Width:  |  Height:  |  Size: 250 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

View File

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

View File

Before

Width:  |  Height:  |  Size: 229 KiB

After

Width:  |  Height:  |  Size: 229 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

View File

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

View File

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 66 KiB

View File

Before

Width:  |  Height:  |  Size: 264 KiB

After

Width:  |  Height:  |  Size: 264 KiB

View File

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

View File

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

View File

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 51 KiB

View File

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

View File

Before

Width:  |  Height:  |  Size: 177 KiB

After

Width:  |  Height:  |  Size: 177 KiB

View File

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

View File

Before

Width:  |  Height:  |  Size: 327 KiB

After

Width:  |  Height:  |  Size: 327 KiB

View File

Before

Width:  |  Height:  |  Size: 166 KiB

After

Width:  |  Height:  |  Size: 166 KiB

View File

Before

Width:  |  Height:  |  Size: 184 KiB

After

Width:  |  Height:  |  Size: 184 KiB

View File

Before

Width:  |  Height:  |  Size: 197 KiB

After

Width:  |  Height:  |  Size: 197 KiB

View File

Before

Width:  |  Height:  |  Size: 166 KiB

After

Width:  |  Height:  |  Size: 166 KiB

View File

@@ -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"],

View File

@@ -20,6 +20,7 @@ PRIVACY_POLICY_BRIEF = """
- **系统信息**程序版本号
- **网络信息**IP 地址ISP地理位置用于使用统计下载统计IPv6 连接测试通过访问 testipv6.cnIPv6 地址获取通过 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}")

View File

@@ -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',
]

View File

@@ -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

View File

@@ -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)

View 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',
]

View 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)

View File

@@ -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):
"""

View File

@@ -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)
# 添加列表控件,只显示已安装补丁的游戏

View 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',
]

View File

@@ -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)

View File

@@ -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
# 用户点击了继续,重新禁用主窗口

View File

@@ -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

View 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()

View File

@@ -0,0 +1,12 @@
"""
下载管理器模块
包含下载相关的管理器类
"""
from .download_manager import DownloadManager
from .download_task_manager import DownloadTaskManager
__all__ = [
'DownloadManager',
'DownloadTaskManager',
]

View File

@@ -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()

View File

@@ -3,7 +3,9 @@ from PySide6.QtCore import Qt
from PySide6.QtWidgets import QDialog, QVBoxLayout, QRadioButton, QPushButton, QLabel, QButtonGroup, QHBoxLayout
from PySide6.QtGui import QFont
from data.config import DOWNLOAD_THREADS
from config.config import DOWNLOAD_THREADS
from workers.download import DownloadThread
from utils.logger import setup_logger
class DownloadTaskManager:
@@ -34,7 +36,7 @@ class DownloadTaskManager:
# 按钮在file_dialog中已设置为禁用状态
# 创建并连接下载线程
self.current_download_thread = self.main_window.create_download_thread(url, _7z_path, game_version)
self.current_download_thread = DownloadThread(url, _7z_path, game_version, self.main_window)
self.current_download_thread.progress.connect(self.main_window.progress_window.update_progress)
self.current_download_thread.finished.connect(
lambda success, error: self.main_window.download_manager.on_download_finished(
@@ -52,7 +54,7 @@ class DownloadTaskManager:
self.main_window.progress_window.stop_button.clicked.connect(self.main_window.download_manager.on_download_stopped)
# 连接暂停/恢复按钮
self.main_window.progress_window.pause_resume_button.clicked.connect(self.toggle_download_pause)
self.main_window.progress_window.pause_resume_button.clicked.connect(self._on_pause_resume_clicked)
# 启动线程和显示进度窗口
self.current_download_thread.start()
@@ -60,6 +62,8 @@ class DownloadTaskManager:
def toggle_download_pause(self):
"""切换下载的暂停/恢复状态"""
logger = setup_logger("download_task_manager")
logger.debug("执行暂停/恢复下载操作")
if not self.current_download_thread:
return
@@ -111,6 +115,8 @@ class DownloadTaskManager:
def show_download_thread_settings(self):
"""显示下载线程设置对话框"""
logger = setup_logger("download_task_manager")
logger.info("用户打开下载线程数设置对话框")
# 创建对话框
dialog = QDialog(self.main_window)
dialog.setWindowTitle(f"下载线程设置 - {self.APP_NAME}")
@@ -178,6 +184,7 @@ class DownloadTaskManager:
break
if selected_level:
old_level = self.download_thread_level
# 为极速和狂暴模式显示警告
if selected_level in ["extreme", "insane"]:
warning_result = QtWidgets.QMessageBox.warning(
@@ -193,6 +200,9 @@ class DownloadTaskManager:
success = self.set_download_thread_level(selected_level)
logger.info(f"用户修改下载线程数设置: {old_level} -> {selected_level}")
logger.debug(f"对应线程数: {DOWNLOAD_THREADS[old_level]} -> {DOWNLOAD_THREADS[selected_level]}")
if success:
# 显示设置成功消息
thread_count = DOWNLOAD_THREADS[selected_level]
@@ -214,8 +224,19 @@ class DownloadTaskManager:
def stop_download(self):
"""停止当前下载线程"""
logger = setup_logger("download_task_manager")
logger.info("用户点击停止下载按钮")
if self.current_download_thread and self.current_download_thread.isRunning():
self.current_download_thread.stop()
self.current_download_thread.wait() # 等待线程完全终止
return True
return False
return False
def _on_pause_resume_clicked(self):
"""处理暂停/恢复按钮点击"""
logger = setup_logger("download_task_manager")
logger.info("用户点击暂停/恢复下载按钮")
self.toggle_download_pause()
def toggle_download_pause(self):
"""切换下载暂停/恢复状态"""

View File

@@ -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):
"""检查是否处于调试模式

View File

@@ -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

File diff suppressed because it is too large Load Diff

View 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)

View File

@@ -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
)

View File

@@ -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("首次运行或隐私协议版本变更,显示隐私对话框")

View 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()

View File

@@ -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):
"""设置窗口圆角"""
# 实现圆角窗口

View File

@@ -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)

View File

@@ -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()

View File

@@ -1,8 +0,0 @@
"""
handlers包包含各种处理程序用于分离主窗口的功能
"""
from .patch_toggle_handler import PatchToggleHandler
from .uninstall_handler import UninstallHandler
__all__ = ['PatchToggleHandler', 'UninstallHandler']

View File

@@ -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()

View File

@@ -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}")
# 创建文本标签布局的按钮
# 开始安装按钮 - 基于背景图片和标签组合

View 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'
]

View 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

View 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()

View 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

View 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")

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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)}", "")