From 7befe19f305d7ed0046f26de6d044a162480e7bc Mon Sep 17 00:00:00 2001 From: hyb-oyqq <1512383570@qq.com> Date: Wed, 6 Aug 2025 15:22:44 +0800 Subject: [PATCH] =?UTF-8?q?feat(core):=20=E5=A2=9E=E5=BC=BA=E7=A6=BB?= =?UTF-8?q?=E7=BA=BF=E6=A8=A1=E5=BC=8F=E6=94=AF=E6=8C=81=E5=92=8C=E7=89=88?= =?UTF-8?q?=E6=9C=AC=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在主窗口中添加离线模式管理器,支持自动切换到离线模式。 - 更新下载管理器以处理离线模式下的下载逻辑,确保用户体验流畅。 - 添加版本警告机制,提示用户在版本过低时的操作选项。 - 优化配置管理器,确保在离线模式下仍可使用相关功能。 - 更新UI管理器以反映当前工作模式,提升用户界面友好性。 --- .gitignore | 7 +- source/core/config_manager.py | 70 ++- source/core/debug_manager.py | 32 +- source/core/download_manager.py | 226 ++++++++- source/core/extraction_handler.py | 10 +- source/core/game_detector.py | 18 +- source/core/ipv6_manager.py | 22 - source/core/offline_mode_manager.py | 692 ++++++++++++++++++++++++++ source/core/ui_manager.py | 172 ++++++- source/data/config.py | 10 +- source/main_window.py | 398 +++++++++++++-- source/ui/Ui_install.py | 1 - source/utils/helpers.py | 99 +++- source/utils/logger.py | 11 +- source/workers/config_fetch_thread.py | 42 +- source/workers/download.py | 17 +- source/workers/ip_optimizer.py | 28 +- 17 files changed, 1707 insertions(+), 148 deletions(-) create mode 100644 source/core/offline_mode_manager.py diff --git a/.gitignore b/.gitignore index bec3b8f..6e5bcd8 100644 --- a/.gitignore +++ b/.gitignore @@ -173,4 +173,9 @@ cython_debug/ nuitka-crash-report.xml build.bat log.txt -result.csv \ No newline at end of file +result.csv +after.7z +vol.1.7z +vol.2.7z +vol.3.7z +vol.4.7z diff --git a/source/core/config_manager.py b/source/core/config_manager.py index 235e9db..a5647ff 100644 --- a/source/core/config_manager.py +++ b/source/core/config_manager.py @@ -85,16 +85,34 @@ class ConfigManager: # 记录错误信息,用于按钮点击时显示 if error_message == "update_required": self.last_error_message = "update_required" - msg_box = msgbox_frame( - f"更新提示 - {self.app_name}", - "\n当前版本过低,请及时更新。\n", - QMessageBox.StandardButton.Ok, - ) - msg_box.exec() - # 在浏览器中打开项目主页 - webbrowser.open("https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/") - # 版本过低,应当显示"无法安装" - return {"action": "disable_button", "then": "exit"} + + # 检查是否处于离线模式 + is_offline_mode = False + if hasattr(self.debug_manager, 'main_window') and hasattr(self.debug_manager.main_window, 'offline_mode_manager'): + is_offline_mode = self.debug_manager.main_window.offline_mode_manager.is_in_offline_mode() + + if is_offline_mode: + # 离线模式下只显示提示,不禁用开始安装按钮 + msg_box = msgbox_frame( + f"更新提示 - {self.app_name}", + "\n当前版本过低,请及时更新。\n在离线模式下,您仍可使用禁用/启用补丁、卸载补丁和离线安装功能。\n", + QMessageBox.StandardButton.Ok, + ) + msg_box.exec() + # 移除在浏览器中打开项目主页的代码 + # 离线模式下版本过低,仍然允许使用安装按钮 + return {"action": "enable_button"} + else: + # 在线模式下显示强制更新提示 + msg_box = msgbox_frame( + f"更新提示 - {self.app_name}", + "\n当前版本过低,请及时更新。\n如需联网下载补丁,请更新到最新版,否则无法下载。\n", + QMessageBox.StandardButton.Ok, + ) + msg_box.exec() + # 移除在浏览器中打开项目主页的代码 + # 在线模式下版本过低,但不直接禁用按钮,而是在点击时提示 + return {"action": "enable_button", "version_warning": True} elif "missing_keys" in error_message: self.last_error_message = "missing_keys" @@ -128,8 +146,8 @@ class ConfigManager: ) msg_box.exec() - # 网络错误时应当显示"无法安装" - return {"action": "disable_button"} + # 网络错误时仍然允许使用按钮,用户可以尝试离线模式 + return {"action": "enable_button"} else: self.cloud_config = data # 标记配置有效 @@ -139,10 +157,36 @@ class ConfigManager: if debug_mode: print("--- Cloud config fetched successfully ---") - print(json.dumps(data, indent=2)) + # 创建一个数据副本,隐藏敏感URL + safe_data = self._create_safe_config_for_logging(data) + print(json.dumps(safe_data, indent=2)) # 获取配置成功,允许安装 return {"action": "enable_button"} + + def _create_safe_config_for_logging(self, config_data): + """创建用于日志记录的安全配置副本,隐藏敏感URL + + Args: + config_data: 原始配置数据 + + Returns: + dict: 安全的配置数据副本 + """ + if not config_data or not isinstance(config_data, dict): + return config_data + + # 创建深拷贝,避免修改原始数据 + import copy + safe_config = copy.deepcopy(config_data) + + # 隐藏敏感URL + for key in safe_config: + if isinstance(safe_config[key], dict) and "url" in safe_config[key]: + # 完全隐藏URL + safe_config[key]["url"] = "***URL protection***" + + return safe_config def is_config_valid(self): """检查配置是否有效 diff --git a/source/core/debug_manager.py b/source/core/debug_manager.py index 94d8cd1..0c265c3 100644 --- a/source/core/debug_manager.py +++ b/source/core/debug_manager.py @@ -31,9 +31,20 @@ class DebugManager: Returns: bool: 是否处于调试模式 """ - if hasattr(self, 'ui_manager') and hasattr(self.ui_manager, 'debug_action'): - return self.ui_manager.debug_action.isChecked() - return False + 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): """切换调试模式 @@ -51,6 +62,21 @@ class DebugManager: 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: + print("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() diff --git a/source/core/download_manager.py b/source/core/download_manager.py index c6837b9..f19b4da 100644 --- a/source/core/download_manager.py +++ b/source/core/download_manager.py @@ -100,7 +100,9 @@ class DownloadManager: raise ValueError("未能获取或解析配置数据") if self.is_debug_mode(): - print(f"DEBUG: Parsed JSON data: {json.dumps(config_data, indent=2)}") + # 创建安全版本的配置数据用于调试输出 + safe_config = self._create_safe_config_for_logging(config_data) + print(f"DEBUG: Parsed JSON data: {json.dumps(safe_config, indent=2)}") urls = {} for i in range(4): @@ -125,7 +127,18 @@ class DownloadManager: raise ValueError(f"配置文件缺少必要的键: {', '.join(missing_original_keys)}") if self.is_debug_mode(): - print(f"DEBUG: Extracted URLs: {urls}") + # 创建安全版本的URL字典用于调试输出 + safe_urls = {} + for key, url in urls.items(): + # 保留域名部分,隐藏路径 + import re + domain_match = re.match(r'(https?://[^/]+)/.*', url) + if domain_match: + domain = domain_match.group(1) + safe_urls[key] = f"{domain}/***隐藏URL路径***" + else: + safe_urls[key] = "***隐藏URL***" + print(f"DEBUG: Extracted URLs: {safe_urls}") print("--- Finished getting download URL successfully ---") return urls @@ -158,11 +171,41 @@ class DownloadManager: f"\n配置文件格式异常\n\n【错误信息】:{e}\n", ) return {} + + def _create_safe_config_for_logging(self, config_data): + """创建用于日志记录的安全配置副本,隐藏敏感URL + + Args: + config_data: 原始配置数据 + + Returns: + dict: 安全的配置数据副本 + """ + if not config_data or not isinstance(config_data, dict): + return config_data + + # 创建深拷贝,避免修改原始数据 + import copy + safe_config = copy.deepcopy(config_data) + + # 隐藏敏感URL + for key in safe_config: + if isinstance(safe_config[key], dict) and "url" in safe_config[key]: + # 完全隐藏URL + safe_config[key]["url"] = "***URL protection***" + + return safe_config def 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(): + print("DEBUG: 已清除游戏目录缓存,确保获取最新状态") + game_dirs = self.main_window.game_detector.identify_game_directories_improved(self.selected_folder) debug_mode = self.is_debug_mode() @@ -182,7 +225,7 @@ class DownloadManager: 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") + self.main_window.hash_msg_box = self.main_window.hash_manager.hash_pop_window(check_type="pre", is_offline=False) install_paths = self.get_install_paths() @@ -345,22 +388,38 @@ class DownloadManager: self.main_window.setEnabled(False) - config = self.get_download_url() - if not config: - QtWidgets.QMessageBox.critical( - self.main_window, f"错误 - {APP_NAME}", "\n网络状态异常或服务器故障,请重试\n" - ) - self.main_window.setEnabled(True) - self.main_window.ui.start_install_text.setText("开始安装") - return + # 检查是否处于离线模式 + 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: + print("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) + self._fill_download_queue(config, selected_game_dirs) if not self.download_queue: self.main_window.after_hash_compare() return - self._show_cloudflare_option() + # 如果是离线模式,直接开始下一个下载任务 + if is_offline_mode: + if debug_mode: + print("DEBUG: 离线模式,跳过Cloudflare优化") + self.next_download_task() + else: + self._show_cloudflare_option() def _fill_download_queue(self, config, game_dirs): """填充下载队列 @@ -406,6 +465,68 @@ class DownloadManager: self.download_queue.append((url, game_folder, game_version, _7z_path, plugin_path)) self.main_window.download_queue_history.append(game_version) + def _fill_offline_download_queue(self, game_dirs): + """填充离线模式下的下载队列 + + Args: + game_dirs: 包含游戏文件夹路径的字典 + """ + self.download_queue.clear() + + if not hasattr(self.main_window, 'download_queue_history'): + self.main_window.download_queue_history = [] + + debug_mode = self.is_debug_mode() + if debug_mode: + print(f"DEBUG: 填充离线下载队列, 游戏目录: {game_dirs}") + + # 检查是否有离线模式管理器 + if not hasattr(self.main_window, 'offline_mode_manager'): + if debug_mode: + print("DEBUG: 离线模式管理器未初始化,无法使用离线模式") + return + + for i in range(1, 5): + game_version = f"NEKOPARA Vol.{i}" + if game_version in game_dirs and not self.main_window.installed_status.get(game_version, False): + # 获取离线补丁文件路径 + offline_patch_path = self.main_window.offline_mode_manager.get_offline_patch_path(game_version) + if not offline_patch_path: + if debug_mode: + print(f"DEBUG: 未找到 {game_version} 的离线补丁文件,跳过") + continue + + game_folder = game_dirs[game_version] + if debug_mode: + print(f"DEBUG: 使用识别到的游戏目录 {game_version}: {game_folder}") + print(f"DEBUG: 使用离线补丁文件: {offline_patch_path}") + + _7z_path = os.path.join(PLUGIN, f"vol.{i}.7z") + plugin_path = os.path.join(PLUGIN, GAME_INFO[game_version]["plugin_path"]) + + # 将本地文件路径作为URL添加到下载队列 + self.download_queue.append((offline_patch_path, game_folder, game_version, _7z_path, plugin_path)) + self.main_window.download_queue_history.append(game_version) + + game_version = "NEKOPARA After" + if game_version in game_dirs and not self.main_window.installed_status.get(game_version, False): + # 获取离线补丁文件路径 + offline_patch_path = self.main_window.offline_mode_manager.get_offline_patch_path(game_version) + if offline_patch_path: + game_folder = game_dirs[game_version] + if debug_mode: + print(f"DEBUG: 使用识别到的游戏目录 {game_version}: {game_folder}") + print(f"DEBUG: 使用离线补丁文件: {offline_patch_path}") + + _7z_path = os.path.join(PLUGIN, "after.7z") + plugin_path = os.path.join(PLUGIN, GAME_INFO[game_version]["plugin_path"]) + + # 将本地文件路径作为URL添加到下载队列 + self.download_queue.append((offline_patch_path, game_folder, game_version, _7z_path, plugin_path)) + self.main_window.download_queue_history.append(game_version) + elif debug_mode: + print(f"DEBUG: 未找到 {game_version} 的离线补丁文件,跳过") + def _show_cloudflare_option(self): """显示Cloudflare加速选择对话框""" if self.download_queue: @@ -498,7 +619,7 @@ class DownloadManager: """准备下载特定游戏版本 Args: - url: 下载URL + url: 下载URL或本地文件路径 game_folder: 游戏文件夹路径 game_version: 游戏版本名称 _7z_path: 7z文件保存路径 @@ -511,6 +632,10 @@ class DownloadManager: print(f"DEBUG: 准备下载游戏 {game_version}") print(f"DEBUG: 游戏文件夹: {game_folder}") + # 隐藏敏感URL + safe_url = "***URL protection***" # 完全隐藏URL + print(f"DEBUG: 下载URL: {safe_url}") + game_exe_exists = True if ( @@ -525,15 +650,76 @@ class DownloadManager: self.next_download_task() return - self.main_window.progress_window = self.main_window.create_progress_window() + # 检查是否处于离线模式 + 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.optimized_ip = self.cloudflare_optimizer.get_optimized_ip() - if self.optimized_ip: - print(f"已为 {game_version} 获取到优选IP: {self.optimized_ip}") + # 如果是离线模式且URL是本地文件路径 + if is_offline_mode and os.path.isfile(url): + if debug_mode: + print(f"DEBUG: 离线模式,复制本地补丁文件 {url} 到 {_7z_path}") + + try: + # 确保目标目录存在 + os.makedirs(os.path.dirname(_7z_path), exist_ok=True) + + # 复制文件 + import shutil + shutil.copy2(url, _7z_path) + + # 验证文件哈希 + hash_valid = False + if hasattr(self.main_window, 'offline_mode_manager'): + if debug_mode: + print(f"DEBUG: 开始验证补丁文件哈希: {_7z_path}") + hash_valid = self.main_window.offline_mode_manager.verify_patch_hash(game_version, _7z_path) + if debug_mode: + print(f"DEBUG: 补丁文件哈希验证结果: {'成功' if hash_valid else '失败'}") + else: + if debug_mode: + print("DEBUG: 离线模式管理器不可用,跳过哈希验证") + hash_valid = True # 如果没有离线模式管理器,假设验证成功 + + if hash_valid: + if debug_mode: + print(f"DEBUG: 成功复制并验证补丁文件 {_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) + else: + if debug_mode: + print(f"DEBUG: 补丁文件哈希验证失败") + # 显示错误消息 + QtWidgets.QMessageBox.critical( + self.main_window, + f"错误 - {APP_NAME}", + f"\n补丁文件校验失败: {game_version}\n\n文件可能已损坏或被篡改,请重新获取补丁文件。\n" + ) + # 继续下一个任务 + self.next_download_task() + except Exception as e: + if debug_mode: + print(f"DEBUG: 复制补丁文件失败: {e}") + # 显示错误消息 + QtWidgets.QMessageBox.critical( + self.main_window, + f"错误 - {APP_NAME}", + f"\n复制补丁文件失败: {game_version}\n错误: {e}\n" + ) + # 继续下一个任务 + self.next_download_task() else: - print(f"未能为 {game_version} 获取优选IP,将使用默认线路。") + # 在线模式,正常下载 + self.main_window.progress_window = self.main_window.create_progress_window() + + self.optimized_ip = self.cloudflare_optimizer.get_optimized_ip() + if self.optimized_ip: + print(f"已为 {game_version} 获取到优选IP: {self.optimized_ip}") + else: + print(f"未能为 {game_version} 获取优选IP,将使用默认线路。") - self.download_task_manager.start_download(url, _7z_path, game_version, game_folder, plugin_path) + 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): """下载完成后的处理 diff --git a/source/core/extraction_handler.py b/source/core/extraction_handler.py index 69c08f5..f0d00e8 100644 --- a/source/core/extraction_handler.py +++ b/source/core/extraction_handler.py @@ -24,8 +24,16 @@ class ExtractionHandler: 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="extraction") + 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( diff --git a/source/core/game_detector.py b/source/core/game_detector.py index ca03c6e..692508e 100644 --- a/source/core/game_detector.py +++ b/source/core/game_detector.py @@ -13,6 +13,7 @@ class GameDetector: """ self.game_info = game_info self.debug_manager = debug_manager + self.directory_cache = {} # 添加目录缓存 def _is_debug_mode(self): """检查是否处于调试模式 @@ -135,6 +136,12 @@ class GameDetector: """ debug_mode = self._is_debug_mode() + # 检查缓存中是否已有该目录的识别结果 + if selected_folder in self.directory_cache: + if debug_mode: + print(f"DEBUG: 使用缓存的目录识别结果: {selected_folder}") + return self.directory_cache[selected_folder] + if debug_mode: print(f"--- 开始识别目录: {selected_folder} ---") @@ -307,5 +314,14 @@ class GameDetector: if debug_mode: print(f"DEBUG: 最终识别的游戏目录: {game_paths}") print(f"--- 目录识别结束 ---") + + # 将识别结果存入缓存 + self.directory_cache[selected_folder] = game_paths - return game_paths \ No newline at end of file + return game_paths + + def clear_directory_cache(self): + """清除目录缓存""" + self.directory_cache = {} + if self._is_debug_mode(): + print("DEBUG: 已清除目录缓存") \ No newline at end of file diff --git a/source/core/ipv6_manager.py b/source/core/ipv6_manager.py index cff1dd6..d6e7272 100644 --- a/source/core/ipv6_manager.py +++ b/source/core/ipv6_manager.py @@ -291,28 +291,6 @@ class IPv6Manager: """ print(f"Toggle IPv6 support: {enabled}") - # 如果用户尝试启用IPv6,检查系统是否支持IPv6并发出警告 - if enabled: - # 先显示警告提示 - warning_msg_box = self._create_message_box( - "警告", - "\n目前IPv6支持功能仍在测试阶段,可能会发生意料之外的bug!\n\n您确定需要启用吗?\n", - QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No - ) - response = warning_msg_box.exec() - - # 如果用户选择不启用,直接返回 - if response != QMessageBox.StandardButton.Yes: - return False - - # 用户确认启用后,继续检查IPv6可用性 - ipv6_available = self.check_ipv6_availability() - - if not ipv6_available: - msg_box = self._create_message_box("错误", "\n未检测到可用的IPv6连接,无法启用IPv6支持。\n\n请确保您的网络环境支持IPv6且已正确配置。\n") - msg_box.exec() - return False - # 保存设置到配置 if self.config is not None: self.config["ipv6_enabled"] = enabled diff --git a/source/core/offline_mode_manager.py b/source/core/offline_mode_manager.py new file mode 100644 index 0000000..08f9446 --- /dev/null +++ b/source/core/offline_mode_manager.py @@ -0,0 +1,692 @@ +import os +import hashlib +import shutil +import tempfile +import py7zr +from PySide6.QtWidgets import QMessageBox + +from data.config import PLUGIN, PLUGIN_HASH, GAME_INFO +from utils import msgbox_frame + +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: + print(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: + print(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: + print("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}") + + if debug_mode: + print(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: + print(f"DEBUG: 已复制离线补丁文件 {source_path} 到 {target_path}") + + return True + except Exception as e: + if debug_mode: + print(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: + print(f"DEBUG: 未找到 {game_version} 的预期哈希值") + return False + + debug_mode = self._is_debug_mode() + + if debug_mode: + print(f"DEBUG: 开始验证离线补丁文件: {file_path}") + print(f"DEBUG: 游戏版本: {game_version}") + print(f"DEBUG: 预期哈希值: {expected_hash}") + + try: + # 检查文件是否存在 + if not os.path.exists(file_path): + if debug_mode: + print(f"DEBUG: 补丁文件不存在: {file_path}") + return False + + # 检查文件大小 + file_size = os.path.getsize(file_path) + if debug_mode: + print(f"DEBUG: 补丁文件大小: {file_size} 字节") + + if file_size == 0: + if debug_mode: + print(f"DEBUG: 补丁文件大小为0,无效文件") + return False + + # 创建临时目录用于解压文件 + with tempfile.TemporaryDirectory() as temp_dir: + if debug_mode: + print(f"DEBUG: 创建临时目录: {temp_dir}") + + # 解压补丁文件 + try: + if debug_mode: + print(f"DEBUG: 开始解压文件: {file_path}") + + with py7zr.SevenZipFile(file_path, mode="r") as archive: + # 获取压缩包内文件列表 + file_list = archive.getnames() + if debug_mode: + print(f"DEBUG: 压缩包内文件列表: {file_list}") + + # 解压所有文件 + archive.extractall(path=temp_dir) + + if debug_mode: + print(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)) + print(f"DEBUG: 解压后的文件列表: {extracted_files}") + except Exception as e: + if debug_mode: + print(f"DEBUG: 解压补丁文件失败: {e}") + print(f"DEBUG: 错误类型: {type(e).__name__}") + import traceback + print(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: + print(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: + print(f"DEBUG: 找到可能的替代文件: {alternative_files}") + + # 检查解压目录结构 + print(f"DEBUG: 检查解压目录结构:") + for root, dirs, files in os.walk(temp_dir): + print(f"DEBUG: 目录: {root}") + print(f"DEBUG: 子目录: {dirs}") + print(f"DEBUG: 文件: {files}") + return False + + if debug_mode: + print(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: + print(f"DEBUG: 补丁文件 {patch_file} 哈希值验证: {'成功' if result else '失败'}") + print(f"DEBUG: 预期哈希值: {expected_hash}") + print(f"DEBUG: 实际哈希值: {file_hash}") + + return result + except Exception as e: + if debug_mode: + print(f"DEBUG: 计算补丁文件哈希值失败: {e}") + print(f"DEBUG: 错误类型: {type(e).__name__}") + return False + except Exception as e: + if debug_mode: + print(f"DEBUG: 验证补丁哈希值失败: {e}") + print(f"DEBUG: 错误类型: {type(e).__name__}") + import traceback + print(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: + print(f"DEBUG: 开始离线安装流程,选择的游戏: {selected_games}") + + if not self.is_in_offline_mode(): + if debug_mode: + print("DEBUG: 当前不是离线模式,无法使用离线安装") + return False + + # 确保已扫描过补丁文件 + if not self.offline_patches: + self.scan_for_offline_patches() + + if not self.offline_patches: + if debug_mode: + print("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: + print("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: + print(f"DEBUG: 未找到 {game_version} 的离线补丁文件,跳过") + + if not installable_games: + if debug_mode: + print("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: + print(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: + print("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: + print(f"DEBUG: 处理离线安装任务: {game_version}") + print(f"DEBUG: 补丁文件: {patch_file}") + print(f"DEBUG: 游戏目录: {game_folder}") + + # 确保目标目录存在 + os.makedirs(os.path.dirname(_7z_path), exist_ok=True) + + try: + # 复制补丁文件到缓存目录 + shutil.copy2(patch_file, _7z_path) + + if debug_mode: + print(f"DEBUG: 已复制补丁文件到缓存目录: {_7z_path}") + print(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: + print(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: + print(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: + print(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: + print(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: + print(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: + print(f"DEBUG: 离线解压完成,状态: {'成功' if success else '失败'}") + if not success: + print(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: + print("DEBUG: 离线解压完成,继续处理下一个任务") + + # 处理下一个任务 + self.process_next_offline_install_task(remaining_tasks) \ No newline at end of file diff --git a/source/core/ui_manager.py b/source/core/ui_manager.py index 28dca08..6a29ef5 100644 --- a/source/core/ui_manager.py +++ b/source/core/ui_manager.py @@ -1,4 +1,4 @@ -from PySide6.QtGui import QIcon, QAction, QFont, QCursor +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 @@ -37,8 +37,18 @@ class UIManager: if os.path.exists(icon_path): self.main_window.setWindowIcon(QIcon(icon_path)) - # 设置窗口标题 - self.main_window.setWindowTitle(f"{APP_NAME} v{APP_VERSION}") + # 获取当前离线模式状态 + 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() @@ -265,6 +275,34 @@ class UIManager: 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)) + + # 创建在线模式和离线模式选项 + self.online_mode_action = QAction("在线模式", self.main_window, checkable=True) + self.online_mode_action.setFont(menu_font) + self.online_mode_action.setChecked(True) # 默认选中在线模式 + + self.offline_mode_action = QAction("离线模式", self.main_window, checkable=True) + self.offline_mode_action.setFont(menu_font) + self.offline_mode_action.setChecked(False) + + # 将两个模式选项添加到同一个互斥组 + 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中相同的字体 @@ -300,28 +338,11 @@ class UIManager: self.ipv6_submenu.setFont(menu_font) self.ipv6_submenu.setStyleSheet(menu_style) - # 检查IPv6是否可用 - ipv6_available = False - if self.ipv6_manager: - ipv6_available = self.ipv6_manager.check_ipv6_availability() - - if not ipv6_available: - self.ipv6_action.setText("启用IPv6支持 (不可用)") - self.ipv6_action.setEnabled(False) - self.ipv6_action.setToolTip("未检测到可用的IPv6连接") - # 检查配置中是否已启用IPv6 config = getattr(self.main_window, 'config', {}) ipv6_enabled = False if isinstance(config, dict): ipv6_enabled = config.get("ipv6_enabled", False) - # 如果配置中启用了IPv6但实际不可用,则强制禁用 - if ipv6_enabled and not ipv6_available: - config["ipv6_enabled"] = False - ipv6_enabled = False - # 使用utils.save_config直接保存配置 - from utils import save_config - save_config(config) self.ipv6_action.setChecked(ipv6_enabled) @@ -416,6 +437,7 @@ class UIManager: 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) # 添加开发者选项子菜单 @@ -438,7 +460,47 @@ class UIManager: # 恢复复选框状态 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) # 如果切换失败,恢复复选框状态 @@ -732,4 +794,72 @@ class UIManager: def show_ipv6_manager_not_ready(self): """显示IPv6管理器未准备好的提示""" msg_box = self._create_message_box("错误", "\nIPv6管理器尚未初始化,请稍后再试。\n") - msg_box.exec() \ No newline at end of file + 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() \ No newline at end of file diff --git a/source/data/config.py b/source/data/config.py index 7614f72..c8eab72 100644 --- a/source/data/config.py +++ b/source/data/config.py @@ -3,7 +3,7 @@ import base64 # 配置信息 app_data = { - "APP_VERSION": "1.3.2", + "APP_VERSION": "1.4.0", "APP_NAME": "FRAISEMOE Addons Installer NEXT", "TEMP": "TEMP", "CACHE": "FRAISEMOE", @@ -62,7 +62,13 @@ UA = app_data["UA_TEMPLATE"].format(APP_VERSION) GAME_INFO = app_data["game_info"] BLOCK_SIZE = 67108864 HASH_SIZE = 134217728 -PLUGIN_HASH = {game: info["hash"] for game, info in GAME_INFO.items()} +PLUGIN_HASH = { + "vol1": GAME_INFO["NEKOPARA Vol.1"]["hash"], + "vol2": GAME_INFO["NEKOPARA Vol.2"]["hash"], + "vol3": GAME_INFO["NEKOPARA Vol.3"]["hash"], + "vol4": GAME_INFO["NEKOPARA Vol.4"]["hash"], + "after": GAME_INFO["NEKOPARA After"]["hash"] +} PROCESS_INFO = {info["exe"]: game for game, info in GAME_INFO.items()} # 下载线程档位设置 diff --git a/source/main_window.py b/source/main_window.py index 4d0c775..6fc0dc5 100644 --- a/source/main_window.py +++ b/source/main_window.py @@ -15,7 +15,7 @@ from ui.Ui_install import Ui_MainWindows from data.config import ( APP_NAME, PLUGIN, GAME_INFO, BLOCK_SIZE, PLUGIN_HASH, UA, CONFIG_URL, LOG_FILE, - DOWNLOAD_THREADS, DEFAULT_DOWNLOAD_THREAD_LEVEL # 添加下载线程常量 + DOWNLOAD_THREADS, DEFAULT_DOWNLOAD_THREAD_LEVEL, APP_VERSION # 添加APP_VERSION导入 ) from utils import ( load_config, save_config, HashManager, AdminPrivileges, msgbox_frame, load_image_from_file @@ -57,7 +57,7 @@ class MainWindow(QMainWindow): self.hash_manager = HashManager(BLOCK_SIZE) self.admin_privileges = AdminPrivileges() - # 初始化各种管理器 + # 初始化各种管理器 - 调整初始化顺序,避免循环依赖 # 1. 首先创建必要的基础管理器 self.animator = MultiStageAnimations(self.ui, self) self.window_manager = WindowManager(self) @@ -72,18 +72,19 @@ class MainWindow(QMainWindow): # 4. 为debug_manager设置ui_manager引用 self.debug_manager.set_ui_manager(self.ui_manager) - # 设置UI - 确保debug_action已初始化 - self.ui_manager.setup_ui() - # 5. 初始化其他管理器 self.config_manager = ConfigManager(APP_NAME, CONFIG_URL, UA, self.debug_manager) self.game_detector = GameDetector(GAME_INFO, self.debug_manager) self.patch_manager = PatchManager(APP_NAME, GAME_INFO, self.debug_manager) - # 6. 初始化下载管理器 - 放在最后,因为它可能依赖于其他管理器 + # 6. 初始化离线模式管理器 + from core.offline_mode_manager import OfflineModeManager + self.offline_mode_manager = OfflineModeManager(self) + + # 7. 初始化下载管理器 - 放在最后,因为它可能依赖于其他管理器 self.download_manager = DownloadManager(self) - # 7. 初始化功能处理程序 + # 8. 初始化功能处理程序 self.uninstall_handler = UninstallHandler(self) self.patch_toggle_handler = PatchToggleHandler(self) @@ -97,6 +98,9 @@ class MainWindow(QMainWindow): 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.progress_window = None # 设置关闭按钮事件连接 @@ -140,11 +144,17 @@ class MainWindow(QMainWindow): if self.ui_manager.debug_action.isChecked(): self.debug_manager.start_logging() - # 在窗口显示前设置初始状态 - self.animator.initialize() + # 设置UI,包括窗口图标和菜单 + self.ui_manager.setup_ui() - # 窗口显示后延迟100ms启动动画 - QTimer.singleShot(100, self.start_animations) + # 检查是否有离线补丁文件,如果有则自动切换到离线模式 + self.check_and_set_offline_mode() + + # 获取云端配置 + self.fetch_cloud_config() + + # 启动动画 + self.start_animations() # 窗口事件处理 - 委托给WindowManager def mousePressEvent(self, event): @@ -170,10 +180,14 @@ class MainWindow(QMainWindow): # 但确保开始安装按钮仍然处于禁用状态 self.set_start_button_enabled(False) + # 在动画开始前初始化 + self.animator.initialize() + + # 连接动画完成信号 self.animator.animation_finished.connect(self.on_animations_finished) + + # 启动动画 self.animator.start_animations() - # 在动画开始时获取云端配置 - self.fetch_cloud_config() def on_animations_finished(self): """动画完成后启用按钮""" @@ -185,8 +199,16 @@ class MainWindow(QMainWindow): self.ui.toggle_patch_btn.setEnabled(True) # 启用禁/启用补丁按钮 self.ui.exit_btn.setEnabled(True) - # 只有在配置有效时才启用开始安装按钮 - if self.config_valid: + # 检查是否处于离线模式 + is_offline_mode = False + 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) else: self.set_start_button_enabled(False) @@ -237,14 +259,14 @@ class MainWindow(QMainWindow): elif result["action"] == "disable_button": # 禁用开始安装按钮 self.set_start_button_enabled(False) - - # 检查是否有后续操作 - if "then" in result and result["then"] == "exit": - # 强制关闭程序 - self.shutdown_app(force_exit=True) elif result["action"] == "enable_button": # 启用开始安装按钮 self.set_start_button_enabled(True) + # 检查是否需要记录版本警告 + 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() @@ -318,7 +340,12 @@ class MainWindow(QMainWindow): """进行安装后哈希比较""" # 禁用窗口已在安装流程开始时完成 - self.hash_msg_box = self.hash_manager.hash_pop_window(check_type="after") + # 检查是否处于离线模式 + 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() @@ -495,16 +522,28 @@ class MainWindow(QMainWindow): """处理安装按钮点击事件 根据按钮当前状态决定是显示错误还是执行安装 """ + # 检查是否处于离线模式 + is_offline_mode = False + if hasattr(self, 'offline_mode_manager'): + is_offline_mode = self.offline_mode_manager.is_in_offline_mode() + + # 如果版本过低且在在线模式下,提示用户更新 + if self.last_error_message == "update_required" and not is_offline_mode: + # 在线模式下提示用户更新软件 + msg_box = msgbox_frame( + f"更新提示 - {APP_NAME}", + "\n当前版本过低,请及时更新。\n如需联网下载补丁,请更新到最新版,否则无法下载。\n\n是否切换到离线模式继续使用?\n", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, + ) + if msg_box.exec() == QMessageBox.StandardButton.Yes: + # 切换到离线模式 + if self.ui_manager and hasattr(self.ui_manager, 'switch_work_mode'): + self.ui_manager.switch_work_mode("offline") + return + if not self.install_button_enabled: # 按钮处于"无法安装"状态 - if self.last_error_message == "update_required": - msg_box = msgbox_frame( - f"更新提示 - {APP_NAME}", - "\n当前版本过低,请及时更新。\n", - QMessageBox.StandardButton.Ok, - ) - msg_box.exec() - elif self.last_error_message == "directory_not_found": + if self.last_error_message == "directory_not_found": # 目录识别失败的特定错误信息 reply = msgbox_frame( f"目录错误 - {APP_NAME}", @@ -517,18 +556,299 @@ class MainWindow(QMainWindow): # 直接调用文件对话框 self.download_manager.file_dialog() else: - # 网络错误或其他错误 - reply = msgbox_frame( - f"错误 - {APP_NAME}", - "\n访问云端配置失败,请检查网络状况或稍后再试。\n\n是否重新尝试连接?\n", - QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, - ) - if reply.exec() == QMessageBox.StandardButton.Yes: - # 重试获取配置 - self.fetch_cloud_config() + # 检查是否处于离线模式 + if is_offline_mode and self.last_error_message == "network_error": + # 如果是离线模式且错误是网络相关的,提示切换到在线模式 + reply = msgbox_frame( + f"离线模式提示 - {APP_NAME}", + "\n当前处于离线模式,但本地补丁文件不完整。\n\n是否切换到在线模式尝试下载?\n", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, + ) + if reply.exec() == QMessageBox.StandardButton.Yes: + # 切换到在线模式 + if self.ui_manager and hasattr(self.ui_manager, 'switch_work_mode'): + self.ui_manager.switch_work_mode("online") + # 重试获取配置 + self.fetch_cloud_config() + else: + # 网络错误或其他错误 + reply = msgbox_frame( + f"错误 - {APP_NAME}", + "\n访问云端配置失败,请检查网络状况或稍后再试。\n\n是否重新尝试连接?\n", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, + ) + if reply.exec() == QMessageBox.StandardButton.Yes: + # 重试获取配置 + self.fetch_cloud_config() else: # 按钮处于"开始安装"状态,正常执行安装流程 - self.download_manager.file_dialog() + # 检查是否处于离线模式 + if is_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" + ) + return + + # 保存选择的目录到下载管理器 + self.download_manager.selected_folder = self.selected_folder + + # 设置按钮状态 + self.ui.start_install_text.setText("正在安装") + 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) + + # 设置对话框样式 + dialog.setStyleSheet(""" + QDialog { + background-color: #2D2D30; + color: #FFFFFF; + } + QCheckBox { + color: #FFFFFF; + font-size: 14px; + padding: 5px; + margin: 5px; + } + QCheckBox:hover { + background-color: #3E3E42; + border-radius: 4px; + } + QCheckBox:checked { + color: #F47A5B; + } + QPushButton { + background-color: #3E3E42; + color: #FFFFFF; + border: none; + border-radius: 4px; + padding: 8px 16px; + font-size: 14px; + min-width: 100px; + } + QPushButton:hover { + background-color: #F47A5B; + } + QPushButton:pressed { + background-color: #E06A4B; + } + """) + + layout = QtWidgets.QVBoxLayout(dialog) + layout.setContentsMargins(20, 20, 20, 20) + layout.setSpacing(10) + + # 添加标题标签 + title_label = QtWidgets.QLabel("选择要安装的游戏", dialog) + title_label.setStyleSheet("font-size: 16px; font-weight: bold; color: #F47A5B; margin-bottom: 10px;") + layout.addWidget(title_label) + + # 添加分隔线 + line = QtWidgets.QFrame(dialog) + line.setFrameShape(QtWidgets.QFrame.Shape.HLine) + line.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) + line.setStyleSheet("background-color: #3E3E42; margin: 10px 0px;") + layout.addWidget(line) + + # 添加游戏选择框 + game_checkboxes = {} + scroll_area = QtWidgets.QScrollArea(dialog) + scroll_area.setWidgetResizable(True) + scroll_area.setStyleSheet("border: none; background-color: transparent;") + + scroll_content = QtWidgets.QWidget(scroll_area) + scroll_layout = QtWidgets.QVBoxLayout(scroll_content) + scroll_layout.setContentsMargins(5, 5, 5, 5) + scroll_layout.setSpacing(8) + scroll_layout.setAlignment(Qt.AlignmentFlag.AlignTop) + + for game_version in game_dirs.keys(): + checkbox = QtWidgets.QCheckBox(game_version, scroll_content) + checkbox.setChecked(True) # 默认选中 + scroll_layout.addWidget(checkbox) + game_checkboxes[game_version] = checkbox + + scroll_content.setLayout(scroll_layout) + scroll_area.setWidget(scroll_content) + layout.addWidget(scroll_area) + + # 添加按钮 + button_layout = QtWidgets.QHBoxLayout() + button_layout.setSpacing(15) + + # 全选按钮 + select_all_btn = QtWidgets.QPushButton("全选", dialog) + select_all_btn.clicked.connect(lambda: self.select_all_games(game_checkboxes, True)) + + # 全不选按钮 + deselect_all_btn = QtWidgets.QPushButton("全不选", dialog) + deselect_all_btn.clicked.connect(lambda: self.select_all_games(game_checkboxes, False)) + + # 确定和取消按钮 + ok_button = QtWidgets.QPushButton("确定", dialog) + ok_button.setStyleSheet(ok_button.styleSheet() + "background-color: #007ACC;") + + cancel_button = QtWidgets.QPushButton("取消", dialog) + + # 添加按钮到布局 + button_layout.addWidget(select_all_btn) + button_layout.addWidget(deselect_all_btn) + button_layout.addStretch() + button_layout.addWidget(ok_button) + button_layout.addWidget(cancel_button) + + layout.addLayout(button_layout) + + # 连接信号 + ok_button.clicked.connect(dialog.accept) + cancel_button.clicked.connect(dialog.reject) + + # 显示对话框 + if dialog.exec() == QtWidgets.QDialog.DialogCode.Accepted: + # 获取选择的游戏 + selected_games = [] + for game_version, checkbox in game_checkboxes.items(): + if checkbox.isChecked(): + selected_games.append(game_version) + + 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("开始安装") + else: + # 在线模式下,检查版本是否过低 + if hasattr(self, 'version_warning') and self.version_warning: + # 版本过低,提示用户更新 + msg_box = msgbox_frame( + f"版本过低 - {APP_NAME}", + "\n当前版本过低,无法使用在线下载功能。\n\n请更新到最新版本或切换到离线模式。\n", + QMessageBox.StandardButton.Ok + ) + msg_box.exec() + else: + # 版本正常,使用原有的下载流程 + self.download_manager.file_dialog() + + def check_and_set_offline_mode(self): + """检查是否有离线补丁文件,如果有则自动切换到离线模式""" + try: + # 检查是否有离线补丁文件 + has_offline_patches = self.offline_mode_manager.has_offline_patches() + + # 获取调试模式状态 + debug_mode = False + if hasattr(self.debug_manager, '_is_debug_mode'): + debug_mode = self.debug_manager._is_debug_mode() + + # 检查配置中是否已设置离线模式 + offline_mode_enabled = False + if isinstance(self.config, dict): + offline_mode_enabled = self.config.get("offline_mode", False) + + # 如果有离线补丁文件或者调试模式下强制启用离线模式 + if has_offline_patches or (debug_mode and offline_mode_enabled): + # 设置离线模式 + self.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) + + # 更新配置 + self.config["offline_mode"] = True + self.save_config(self.config) + + # 在离线模式下始终启用开始安装按钮 + self.set_start_button_enabled(True) + + # 清除版本警告标志 + self.version_warning = False + + if debug_mode: + print(f"DEBUG: 已自动切换到离线模式,找到离线补丁文件: {list(self.offline_mode_manager.offline_patches.keys())}") + print(f"DEBUG: 离线模式下启用开始安装按钮") + else: + # 如果没有离线补丁文件,确保使用在线模式 + self.offline_mode_manager.set_offline_mode(False) + + # 更新UI中的在线模式选项 + if hasattr(self.ui_manager, 'online_mode_action') and self.ui_manager.online_mode_action: + self.ui_manager.online_mode_action.setChecked(True) + self.ui_manager.offline_mode_action.setChecked(False) + + # 更新配置 + self.config["offline_mode"] = False + self.save_config(self.config) + + # 如果当前版本过低,设置版本警告标志 + if hasattr(self, 'last_error_message') and self.last_error_message == "update_required": + # 设置版本警告标志 + self.version_warning = True + + if debug_mode: + print("DEBUG: 未找到离线补丁文件,使用在线模式") + + # 确保标题标签显示正确的模式 + if hasattr(self, 'ui') and hasattr(self.ui, 'title_label'): + from data.config import APP_NAME, APP_VERSION + mode_indicator = "[离线模式]" if self.offline_mode_manager.is_in_offline_mode() else "[在线模式]" + self.ui.title_label.setText(f"{APP_NAME} v{APP_VERSION} {mode_indicator}") + + except Exception as e: + # 捕获任何异常,确保程序不会崩溃 + print(f"错误: 检查离线模式时发生异常: {e}") + # 默认使用在线模式 + if hasattr(self, 'offline_mode_manager'): + self.offline_mode_manager.is_offline_mode = False + + def select_all_games(self, game_checkboxes, checked): + """选择或取消选择所有游戏 + + Args: + game_checkboxes: 游戏复选框字典 + checked: 是否选中 + """ + for checkbox in game_checkboxes.values(): + checkbox.setChecked(checked) diff --git a/source/ui/Ui_install.py b/source/ui/Ui_install.py index d6776f2..4735c22 100644 --- a/source/ui/Ui_install.py +++ b/source/ui/Ui_install.py @@ -542,7 +542,6 @@ class Ui_MainWindows(object): # setupUi def retranslateUi(self, MainWindows): - MainWindows.setWindowTitle(QCoreApplication.translate("MainWindows", f"{APP_NAME} v{APP_VERSION}", None)) self.loadbg.setText("") self.vol1bg.setText("") self.vol2bg.setText("") diff --git a/source/utils/helpers.py b/source/utils/helpers.py index e0c3e43..c281d96 100644 --- a/source/utils/helpers.py +++ b/source/utils/helpers.py @@ -112,23 +112,38 @@ class HashManager: print(f"Error calculating hash for {file_path}: {e}") return results - def hash_pop_window(self, check_type="default"): + def hash_pop_window(self, check_type="default", is_offline=False): """显示文件检验窗口 Args: - check_type: 检查类型,可以是 'pre'(预检查), 'after'(后检查), 'extraction'(解压后检查) - + check_type: 检查类型,可以是 'pre'(预检查), 'after'(后检查), 'extraction'(解压后检查), 'offline_extraction'(离线解压), 'offline_verify'(离线验证) + is_offline: 是否处于离线模式 + Returns: QMessageBox: 消息框实例 """ message = "\n正在检验文件状态...\n" - if check_type == "pre": - message = "\n正在检查游戏文件以确定需要安装的补丁...\n" - elif check_type == "after": - message = "\n正在检验本地文件完整性...\n" - elif check_type == "extraction": - message = "\n正在验证下载的解压文件完整性...\n" + if is_offline: + # 离线模式的消息 + if check_type == "pre": + message = "\n正在检查游戏文件以确定需要安装的补丁...\n" + elif check_type == "after": + message = "\n正在检验本地文件完整性...\n" + elif check_type == "offline_verify": + message = "\n正在验证本地补丁压缩文件完整性...\n" + elif check_type == "offline_extraction": + message = "\n正在解压安装补丁文件...\n" + else: + message = "\n正在处理离线补丁文件...\n" + else: + # 在线模式的消息 + if check_type == "pre": + message = "\n正在检查游戏文件以确定需要安装的补丁...\n" + elif check_type == "after": + message = "\n正在检验本地文件完整性...\n" + elif check_type == "extraction": + message = "\n正在验证下载的解压文件完整性...\n" msg_box = msgbox_frame(f"通知 - {APP_NAME}", message) msg_box.open() @@ -137,49 +152,94 @@ class HashManager: def cfg_pre_hash_compare(self, install_paths, plugin_hash, installed_status): status_copy = installed_status.copy() + debug_mode = False + + # 尝试检测是否处于调试模式 + try: + from data.config import CACHE + debug_file = os.path.join(os.path.dirname(CACHE), "debug_mode.txt") + debug_mode = os.path.exists(debug_file) + except: + pass for game_version, install_path in install_paths.items(): if not os.path.exists(install_path): status_copy[game_version] = False + if debug_mode: + print(f"DEBUG: 哈希预检查 - {game_version} 补丁文件不存在: {install_path}") continue try: + expected_hash = plugin_hash.get(game_version, "") file_hash = self.hash_calculate(install_path) - if file_hash == plugin_hash.get(game_version): + + if debug_mode: + print(f"DEBUG: 哈希预检查 - {game_version}") + print(f"DEBUG: 文件路径: {install_path}") + print(f"DEBUG: 预期哈希值: {expected_hash}") + print(f"DEBUG: 实际哈希值: {file_hash}") + print(f"DEBUG: 哈希匹配: {file_hash == expected_hash}") + + if file_hash == expected_hash: status_copy[game_version] = True else: status_copy[game_version] = False - except Exception: + except Exception as e: status_copy[game_version] = False + if debug_mode: + print(f"DEBUG: 哈希预检查异常 - {game_version}: {str(e)}") return status_copy def cfg_after_hash_compare(self, install_paths, plugin_hash, installed_status): + debug_mode = False + + # 尝试检测是否处于调试模式 + try: + from data.config import CACHE + debug_file = os.path.join(os.path.dirname(CACHE), "debug_mode.txt") + debug_mode = os.path.exists(debug_file) + except: + pass + file_paths = [ install_paths[game] for game in plugin_hash if installed_status.get(game) ] hash_results = self.calculate_hashes_in_parallel(file_paths) - for game, hash_value in plugin_hash.items(): + for game, expected_hash in plugin_hash.items(): if installed_status.get(game): file_path = install_paths[game] file_hash = hash_results.get(file_path) + if debug_mode: + print(f"DEBUG: 哈希后检查 - {game}") + print(f"DEBUG: 文件路径: {file_path}") + print(f"DEBUG: 预期哈希值: {expected_hash}") + print(f"DEBUG: 实际哈希值: {file_hash if file_hash else '计算失败'}") + if file_hash is None: installed_status[game] = False + if debug_mode: + print(f"DEBUG: 哈希后检查失败 - 无法计算文件哈希值: {game}") return { "passed": False, "game": game, "message": f"\n无法计算 {game} 的文件哈希值,文件可能已损坏或被占用。\n" } - if file_hash != hash_value: + if file_hash != expected_hash: installed_status[game] = False + if debug_mode: + print(f"DEBUG: 哈希后检查失败 - 哈希值不匹配: {game}") return { "passed": False, "game": game, "message": f"\n检测到 {game} 的文件哈希值不匹配。\n" } + + if debug_mode: + print(f"DEBUG: 哈希后检查通过 - 所有文件哈希值匹配") return {"passed": True} class AdminPrivileges: @@ -580,8 +640,17 @@ class HostsManager: return False def censor_url(text): - """Censors URLs in a given text string.""" + """Censors URLs in a given text string, replacing them with a protection message. + + Args: + text: 要处理的文本 + + Returns: + str: 处理后的文本,URL被完全隐藏 + """ if not isinstance(text, str): text = str(text) + + # 匹配URL并替换为固定文本 url_pattern = re.compile(r'https?://[^\s/$.?#].[^\s]*') - return url_pattern.sub('***URL HIDDEN***', text) \ No newline at end of file + return url_pattern.sub('***URL protection***', text) \ No newline at end of file diff --git a/source/utils/logger.py b/source/utils/logger.py index 236d2ee..84d5708 100644 --- a/source/utils/logger.py +++ b/source/utils/logger.py @@ -3,6 +3,15 @@ import logging import os from data.config import CACHE +class URLCensorFormatter(logging.Formatter): + """自定义的日志格式化器,用于隐藏日志消息中的URL""" + + def format(self, record): + # 先使用原始的format方法格式化日志 + formatted_message = super().format(record) + # 然后对格式化后的消息进行URL审查 + return censor_url(formatted_message) + class Logger: def __init__(self, filename, stream): self.terminal = stream @@ -53,7 +62,7 @@ def setup_logger(name): console_handler.setLevel(logging.INFO) # 创建格式器并添加到处理器 - formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + formatter = URLCensorFormatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') file_handler.setFormatter(formatter) console_handler.setFormatter(formatter) diff --git a/source/workers/config_fetch_thread.py b/source/workers/config_fetch_thread.py index fe4ae5d..cc0a85f 100644 --- a/source/workers/config_fetch_thread.py +++ b/source/workers/config_fetch_thread.py @@ -18,7 +18,8 @@ class ConfigFetchThread(QThread): try: if self.debug_mode: print("--- Starting to fetch cloud config ---") - print(f"DEBUG: Requesting URL: {self.url}") + # 完全隐藏URL + print(f"DEBUG: Requesting URL: ***URL protection***") print(f"DEBUG: Using Headers: {self.headers}") response = requests.get(self.url, headers=self.headers, timeout=10) @@ -26,7 +27,18 @@ class ConfigFetchThread(QThread): if self.debug_mode: print(f"DEBUG: Response Status Code: {response.status_code}") print(f"DEBUG: Response Headers: {response.headers}") - print(f"DEBUG: Response Text: {response.text}") + + # 解析并隐藏响应中的敏感URL + try: + response_data = response.json() + # 创建安全版本用于日志输出 + safe_response = self._create_safe_config_for_logging(response_data) + print(f"DEBUG: Response Text: {json.dumps(safe_response, indent=2)}") + except: + # 如果不是JSON,直接打印文本 + from utils.helpers import censor_url + censored_text = censor_url(response.text) + print(f"DEBUG: Response Text: {censored_text}") response.raise_for_status() @@ -62,4 +74,28 @@ class ConfigFetchThread(QThread): self.finished.emit(None, error_msg) finally: if self.debug_mode: - print("--- Finished fetching cloud config ---") \ No newline at end of file + print("--- Finished fetching cloud config ---") + + def _create_safe_config_for_logging(self, config_data): + """创建用于日志记录的安全配置副本,隐藏敏感URL + + Args: + config_data: 原始配置数据 + + Returns: + dict: 安全的配置数据副本 + """ + if not config_data or not isinstance(config_data, dict): + return config_data + + # 创建深拷贝,避免修改原始数据 + import copy + safe_config = copy.deepcopy(config_data) + + # 隐藏敏感URL + for key in safe_config: + if isinstance(safe_config[key], dict) and "url" in safe_config[key]: + # 完全隐藏URL + safe_config[key]["url"] = "***URL protection***" + + return safe_config \ No newline at end of file diff --git a/source/workers/download.py b/source/workers/download.py index cf10873..31632de 100644 --- a/source/workers/download.py +++ b/source/workers/download.py @@ -200,7 +200,15 @@ class DownloadThread(QThread): command.append(self.url) - print(f"即将执行的 Aria2c 命令: {' '.join(command)}") + # 创建一个安全的命令副本,隐藏URL + safe_command = command.copy() + if len(safe_command) > 0: + # 替换最后一个参数(URL)为安全版本 + url = safe_command[-1] + if isinstance(url, str) and url.startswith("http"): + safe_command[-1] = "***URL protection***" + + print(f"即将执行的 Aria2c 命令: {' '.join(safe_command)}") creation_flags = subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0 self.process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, encoding='utf-8', errors='replace', creationflags=creation_flags) @@ -220,8 +228,11 @@ class DownloadThread(QThread): else: break - full_output.append(line) - print(line.strip()) + # 处理输出行,隐藏可能包含的URL + from utils.helpers import censor_url + censored_line = censor_url(line) + full_output.append(censored_line) + print(censored_line.strip()) match = progress_pattern.search(line) if match: diff --git a/source/workers/ip_optimizer.py b/source/workers/ip_optimizer.py index 5ef704f..1ad52cc 100644 --- a/source/workers/ip_optimizer.py +++ b/source/workers/ip_optimizer.py @@ -30,6 +30,9 @@ class IpOptimizer: ip_txt_path = resource_path("ip.txt") + # 隐藏敏感URL + safe_url = "***URL protection***" + command = [ cst_path, "-n", "1000", # 延迟测速线程数 @@ -39,10 +42,17 @@ class IpOptimizer: "-dd", # 禁用下载测速 "-o"," " # 不写入结果文件 ] + + # 创建用于显示的安全命令副本 + safe_command = command.copy() + for i, arg in enumerate(safe_command): + if arg == url: + safe_command[i] = safe_url creation_flags = subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0 print("--- CloudflareSpeedTest 开始执行 ---") + print(f"执行命令: {' '.join(safe_command)}") self.process = subprocess.Popen( command, @@ -91,7 +101,9 @@ class IpOptimizer: timeout_counter = 0 - cleaned_line = line.strip() + # 处理输出行,隐藏可能包含的URL + from utils.helpers import censor_url + cleaned_line = censor_url(line.strip()) if cleaned_line: print(cleaned_line) @@ -157,6 +169,9 @@ class IpOptimizer: print(f"错误: ipv6.txt 未在资源路径中找到。") return None + # 隐藏敏感URL + safe_url = "***URL protection***" + command = [ cst_path, "-n", "1000", # 延迟测速线程数 @@ -166,10 +181,17 @@ class IpOptimizer: "-dd", # 禁用下载测速 "-o", " " # 不写入结果文件 ] + + # 创建用于显示的安全命令副本 + safe_command = command.copy() + for i, arg in enumerate(safe_command): + if arg == url: + safe_command[i] = safe_url creation_flags = subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0 print("--- CloudflareSpeedTest IPv6 开始执行 ---") + print(f"执行命令: {' '.join(safe_command)}") self.process = subprocess.Popen( command, @@ -218,7 +240,9 @@ class IpOptimizer: timeout_counter = 0 - cleaned_line = line.strip() + # 处理输出行,隐藏可能包含的URL + from utils.helpers import censor_url + cleaned_line = censor_url(line.strip()) if cleaned_line: print(cleaned_line)