diff --git a/source/core/__init__.py b/source/core/__init__.py index 559866b..66a7036 100644 --- a/source/core/__init__.py +++ b/source/core/__init__.py @@ -2,10 +2,18 @@ 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 __all__ = [ 'MultiStageAnimations', 'UIManager', 'DownloadManager', - 'DebugManager' + 'DebugManager', + 'WindowManager', + 'GameDetector', + 'PatchManager', + 'ConfigManager' ] \ No newline at end of file diff --git a/source/core/config_manager.py b/source/core/config_manager.py new file mode 100644 index 0000000..235e9db --- /dev/null +++ b/source/core/config_manager.py @@ -0,0 +1,169 @@ +import json +import webbrowser +from PySide6.QtWidgets import QMessageBox + +from utils import load_config, save_config, msgbox_frame + +class ConfigManager: + """配置管理器,用于处理配置的加载、保存和获取云端配置""" + + def __init__(self, app_name, config_url, ua, debug_manager=None): + """初始化配置管理器 + + Args: + app_name: 应用程序名称,用于显示消息框标题 + config_url: 云端配置URL + ua: User-Agent字符串 + debug_manager: 调试管理器实例,用于输出调试信息 + """ + self.app_name = app_name + self.config_url = config_url + self.ua = ua + self.debug_manager = debug_manager + self.cloud_config = None + self.config_valid = False + self.last_error_message = "" + + def _is_debug_mode(self): + """检查是否处于调试模式 + + Returns: + bool: 是否处于调试模式 + """ + if hasattr(self.debug_manager, 'ui_manager') and hasattr(self.debug_manager.ui_manager, 'debug_action'): + return self.debug_manager.ui_manager.debug_action.isChecked() + return False + + def load_config(self): + """加载本地配置 + + Returns: + dict: 加载的配置 + """ + return load_config() + + def save_config(self, config): + """保存配置 + + Args: + config: 要保存的配置 + """ + save_config(config) + + def fetch_cloud_config(self, config_fetch_thread_class, callback=None): + """获取云端配置 + + Args: + config_fetch_thread_class: 用于获取云端配置的线程类 + callback: 获取完成后的回调函数,接受两个参数(data, error_message) + """ + headers = {"User-Agent": self.ua} + debug_mode = self._is_debug_mode() + self.config_fetch_thread = config_fetch_thread_class(self.config_url, headers, debug_mode) + + # 如果提供了回调,使用它;否则使用内部的on_config_fetched方法 + if callback: + self.config_fetch_thread.finished.connect(callback) + else: + self.config_fetch_thread.finished.connect(self.on_config_fetched) + + self.config_fetch_thread.start() + + def on_config_fetched(self, data, error_message): + """云端配置获取完成的回调处理 + + Args: + data: 获取到的配置数据 + error_message: 错误信息,如果有 + """ + debug_mode = self._is_debug_mode() + + if error_message: + # 标记配置无效 + self.config_valid = False + + # 记录错误信息,用于按钮点击时显示 + 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"} + + elif "missing_keys" in error_message: + self.last_error_message = "missing_keys" + missing_versions = error_message.split(":")[1] + msg_box = msgbox_frame( + f"配置缺失 - {self.app_name}", + f'\n云端缺失下载链接,可能云服务器正在维护,不影响其他版本下载。\n当前缺失版本:"{missing_versions}"\n', + QMessageBox.StandardButton.Ok, + ) + msg_box.exec() + # 对于部分缺失,仍然允许使用,因为可能只影响部分游戏版本 + self.config_valid = True + return {"action": "enable_button"} + else: + # 设置网络错误标记 + self.last_error_message = "network_error" + + # 显示通用错误消息,只在debug模式下显示详细错误 + error_msg = "访问云端配置失败,请检查网络状况或稍后再试。" + if debug_mode and "详细错误:" in error_message: + msg_box = msgbox_frame( + f"错误 - {self.app_name}", + f"\n{error_message}\n", + QMessageBox.StandardButton.Ok, + ) + else: + msg_box = msgbox_frame( + f"错误 - {self.app_name}", + f"\n{error_msg}\n", + QMessageBox.StandardButton.Ok, + ) + msg_box.exec() + + # 网络错误时应当显示"无法安装" + return {"action": "disable_button"} + else: + self.cloud_config = data + # 标记配置有效 + self.config_valid = True + # 清除错误信息 + self.last_error_message = "" + + if debug_mode: + print("--- Cloud config fetched successfully ---") + print(json.dumps(data, indent=2)) + + # 获取配置成功,允许安装 + return {"action": "enable_button"} + + def is_config_valid(self): + """检查配置是否有效 + + Returns: + bool: 配置是否有效 + """ + return self.config_valid + + def get_cloud_config(self): + """获取云端配置 + + Returns: + dict: 云端配置 + """ + return self.cloud_config + + def get_last_error(self): + """获取最后一次错误信息 + + Returns: + str: 错误信息 + """ + return self.last_error_message \ No newline at end of file diff --git a/source/core/debug_manager.py b/source/core/debug_manager.py index 5e5f82e..7e75aba 100644 --- a/source/core/debug_manager.py +++ b/source/core/debug_manager.py @@ -15,6 +15,25 @@ class DebugManager: self.logger = None self.original_stdout = None self.original_stderr = None + self.ui_manager = None # 添加ui_manager属性 + + def set_ui_manager(self, ui_manager): + """设置UI管理器引用 + + Args: + ui_manager: UI管理器实例 + """ + self.ui_manager = ui_manager + + def _is_debug_mode(self): + """检查是否处于调试模式 + + Returns: + bool: 是否处于调试模式 + """ + if hasattr(self, 'ui_manager') and hasattr(self.ui_manager, 'debug_action'): + return self.ui_manager.debug_action.isChecked() + return False def toggle_debug_mode(self, checked): """切换调试模式 diff --git a/source/core/download_manager.py b/source/core/download_manager.py index 3faae36..c24b9cb 100644 --- a/source/core/download_manager.py +++ b/source/core/download_manager.py @@ -39,12 +39,14 @@ class DownloadManager: self.main_window, f"通知 - {APP_NAME}", "\n未选择任何目录,请重新选择\n" ) return + # 将按钮设置为"正在安装"状态 + self.main_window.set_start_button_enabled(False, installing=True) self.download_action() def get_install_paths(self): """获取所有游戏版本的安装路径""" # 使用改进的目录识别功能 - game_dirs = self.main_window.identify_game_directories_improved(self.selected_folder) + game_dirs = self.main_window.game_detector.identify_game_directories_improved(self.selected_folder) install_paths = {} debug_mode = self.is_debug_mode() @@ -159,14 +161,13 @@ class DownloadManager: def download_action(self): """开始下载流程""" - # 禁用开始安装按钮 - self.main_window.set_start_button_enabled(False) + # 按钮在file_dialog中已设置为"正在安装"状态 # 清空下载历史记录 self.main_window.download_queue_history = [] # 使用改进的目录识别功能 - game_dirs = self.main_window.identify_game_directories_improved(self.selected_folder) + game_dirs = self.main_window.game_detector.identify_game_directories_improved(self.selected_folder) debug_mode = self.is_debug_mode() if debug_mode: @@ -183,6 +184,8 @@ class DownloadManager: f"目录错误 - {APP_NAME}", "\n未能识别到任何游戏目录。\n\n请确认您选择的是游戏的上级目录,并且该目录中包含NEKOPARA系列游戏文件夹。\n" ) + # 恢复按钮为"无法安装"状态 + self.main_window.set_start_button_enabled(False) return # 显示哈希检查窗口 @@ -212,8 +215,8 @@ class DownloadManager: QtWidgets.QMessageBox.critical( self.main_window, f"错误 - {APP_NAME}", "\n网络状态异常或服务器故障,请重试\n" ) - # 重新启用开始安装按钮 - self.main_window.set_start_button_enabled(True) + # 网络故障时,使用"无法安装"状态 + self.main_window.set_start_button_enabled(False) return # 填充下载队列 @@ -269,7 +272,7 @@ class DownloadManager: self.main_window.download_queue_history = [] # 获取所有识别到的游戏目录 - game_dirs = self.main_window.identify_game_directories_improved(self.selected_folder) + game_dirs = self.main_window.game_detector.identify_game_directories_improved(self.selected_folder) debug_mode = self.is_debug_mode() if debug_mode: @@ -478,7 +481,7 @@ class DownloadManager: print(f"DEBUG: 游戏文件夹: {game_folder}") # 获取游戏可执行文件路径 - game_dirs = self.main_window.identify_game_directories_improved(self.selected_folder) + game_dirs = self.main_window.game_detector.identify_game_directories_improved(self.selected_folder) game_exe_exists = False if game_version in game_dirs: diff --git a/source/core/game_detector.py b/source/core/game_detector.py new file mode 100644 index 0000000..ca03c6e --- /dev/null +++ b/source/core/game_detector.py @@ -0,0 +1,311 @@ +import os +import re + +class GameDetector: + """游戏检测器,用于识别游戏目录和版本""" + + def __init__(self, game_info, debug_manager=None): + """初始化游戏检测器 + + Args: + game_info: 游戏信息字典,包含各版本的安装路径和可执行文件名 + debug_manager: 调试管理器实例,用于输出调试信息 + """ + self.game_info = game_info + self.debug_manager = debug_manager + + def _is_debug_mode(self): + """检查是否处于调试模式 + + Returns: + bool: 是否处于调试模式 + """ + if hasattr(self.debug_manager, 'ui_manager') and hasattr(self.debug_manager.ui_manager, 'debug_action'): + return self.debug_manager.ui_manager.debug_action.isChecked() + return False + + def identify_game_version(self, game_dir): + """识别游戏版本 + + Args: + game_dir: 游戏目录路径 + + Returns: + str: 游戏版本名称,如果不是有效的游戏目录则返回None + """ + debug_mode = self._is_debug_mode() + + if debug_mode: + print(f"DEBUG: 尝试识别游戏版本: {game_dir}") + + # 先通过目录名称进行初步推测(这将作为递归搜索的提示) + dir_name = os.path.basename(game_dir).lower() + potential_version = None + vol_num = None + + # 提取卷号或判断是否是After + if "vol" in dir_name or "vol." in dir_name: + vol_match = re.search(r"vol(?:\.|\s*)?(\d+)", dir_name) + if vol_match: + vol_num = vol_match.group(1) + potential_version = f"NEKOPARA Vol.{vol_num}" + if debug_mode: + print(f"DEBUG: 从目录名推测游戏版本: {potential_version}, 卷号: {vol_num}") + elif "after" in dir_name: + potential_version = "NEKOPARA After" + if debug_mode: + print(f"DEBUG: 从目录名推测游戏版本: NEKOPARA After") + + # 检查是否为NEKOPARA游戏目录 + # 通过检查游戏可执行文件来识别游戏版本 + for game_version, info in self.game_info.items(): + # 尝试多种可能的可执行文件名变体 + exe_variants = [ + info["exe"], # 标准文件名 + info["exe"] + ".nocrack", # Steam加密版本 + info["exe"].replace(".exe", ""), # 无扩展名版本 + info["exe"].replace("NEKOPARA", "nekopara").lower(), # 全小写变体 + info["exe"].lower(), # 小写变体 + info["exe"].lower() + ".nocrack", # 小写变体的Steam加密版本 + ] + + # 对于Vol.3可能有特殊名称 + if "Vol.3" in game_version: + # 增加可能的卷3特定的变体 + exe_variants.extend([ + "NEKOPARAVol3.exe", + "NEKOPARAVol3.exe.nocrack", + "nekoparavol3.exe", + "nekoparavol3.exe.nocrack", + "nekopara_vol3.exe", + "nekopara_vol3.exe.nocrack", + "vol3.exe", + "vol3.exe.nocrack" + ]) + + for exe_variant in exe_variants: + exe_path = os.path.join(game_dir, exe_variant) + if os.path.exists(exe_path): + if debug_mode: + print(f"DEBUG: 通过可执行文件确认游戏版本: {game_version}, 文件: {exe_variant}") + return game_version + + # 如果没有直接匹配,尝试递归搜索 + if potential_version: + # 从预测的版本中获取卷号或确认是否是After + is_after = "After" in potential_version + if not vol_num and not is_after: + vol_match = re.search(r"Vol\.(\d+)", potential_version) + if vol_match: + vol_num = vol_match.group(1) + + # 递归搜索可执行文件 + for root, dirs, files in os.walk(game_dir): + for file in files: + file_lower = file.lower() + if file.endswith('.exe') or file.endswith('.exe.nocrack'): + # 检查文件名中是否包含卷号或关键词 + if ((vol_num and (f"vol{vol_num}" in file_lower or + f"vol.{vol_num}" in file_lower or + f"vol {vol_num}" in file_lower)) or + (is_after and "after" in file_lower)): + if debug_mode: + print(f"DEBUG: 通过递归搜索确认游戏版本: {potential_version}, 文件: {file}") + return potential_version + + # 如果仍然没有找到,基于目录名的推测返回结果 + if potential_version: + if debug_mode: + print(f"DEBUG: 基于目录名返回推测的游戏版本: {potential_version}") + return potential_version + + if debug_mode: + print(f"DEBUG: 无法识别游戏版本: {game_dir}") + + return None + + def identify_game_directories_improved(self, selected_folder): + """改进的游戏目录识别,支持大小写不敏感和特殊字符处理 + + Args: + selected_folder: 选择的上级目录 + + Returns: + dict: 游戏版本到游戏目录的映射 + """ + debug_mode = self._is_debug_mode() + + if debug_mode: + print(f"--- 开始识别目录: {selected_folder} ---") + + game_paths = {} + + # 获取上级目录中的所有文件夹 + try: + all_dirs = [d for d in os.listdir(selected_folder) if os.path.isdir(os.path.join(selected_folder, d))] + if debug_mode: + print(f"DEBUG: 找到以下子目录: {all_dirs}") + except Exception as e: + if debug_mode: + print(f"DEBUG: 无法读取目录 {selected_folder}: {str(e)}") + return {} + + for game, info in self.game_info.items(): + expected_dir = info["install_path"].split("/")[0] # 例如 "NEKOPARA Vol. 1" + expected_exe = info["exe"] # 标准可执行文件名 + + if debug_mode: + print(f"DEBUG: 搜索游戏 {game}, 预期目录: {expected_dir}, 预期可执行文件: {expected_exe}") + + # 尝试不同的匹配方法 + found_dir = None + + # 1. 精确匹配 + if expected_dir in all_dirs: + found_dir = expected_dir + if debug_mode: + print(f"DEBUG: 精确匹配成功: {expected_dir}") + + # 2. 大小写不敏感匹配 + if not found_dir: + for dir_name in all_dirs: + if expected_dir.lower() == dir_name.lower(): + found_dir = dir_name + if debug_mode: + print(f"DEBUG: 大小写不敏感匹配成功: {dir_name}") + break + + # 3. 更模糊的匹配(允许特殊字符差异) + if not found_dir: + # 准备用于模糊匹配的正则表达式模式 + # 替换空格为可选空格或连字符,替换点为可选点 + pattern_text = expected_dir.replace(" ", "[ -]?").replace(".", "\\.?") + pattern = re.compile(f"^{pattern_text}$", re.IGNORECASE) + + for dir_name in all_dirs: + if pattern.match(dir_name): + found_dir = dir_name + if debug_mode: + print(f"DEBUG: 模糊匹配成功: {dir_name} 匹配模式 {pattern_text}") + break + + # 4. 如果还是没找到,尝试更宽松的匹配 + if not found_dir: + vol_match = re.search(r"vol(?:\.|\s*)?(\d+)", expected_dir, re.IGNORECASE) + vol_num = None + if vol_match: + vol_num = vol_match.group(1) + if debug_mode: + print(f"DEBUG: 提取卷号: {vol_num}") + + is_after = "after" in expected_dir.lower() + + for dir_name in all_dirs: + dir_lower = dir_name.lower() + + # 对于After特殊处理 + if is_after and "after" in dir_lower: + found_dir = dir_name + if debug_mode: + print(f"DEBUG: After特殊匹配成功: {dir_name}") + break + + # 对于Vol特殊处理 + if vol_num: + # 查找目录名中的卷号 + dir_vol_match = re.search(r"vol(?:\.|\s*)?(\d+)", dir_lower) + if dir_vol_match and dir_vol_match.group(1) == vol_num: + found_dir = dir_name + if debug_mode: + print(f"DEBUG: 卷号匹配成功: {dir_name} 卷号 {vol_num}") + break + + # 如果找到匹配的目录,验证exe文件是否存在 + if found_dir: + potential_path = os.path.join(selected_folder, found_dir) + + # 尝试多种可能的可执行文件名变体 + # 包括Steam加密版本和其他可能的变体 + exe_variants = [ + expected_exe, # 标准文件名 + expected_exe + ".nocrack", # Steam加密版本 + expected_exe.replace(".exe", ""),# 无扩展名版本 + # Vol.3的特殊变体,因为它的文件名可能不一样 + expected_exe.replace("NEKOPARA", "nekopara").lower(), # 全小写变体 + expected_exe.lower(), # 小写变体 + expected_exe.lower() + ".nocrack", # 小写变体的Steam加密版本 + ] + + # 对于Vol.3可能有特殊名称 + if "Vol.3" in game: + # 增加可能的卷3特定的变体 + exe_variants.extend([ + "NEKOPARAVol3.exe", + "NEKOPARAVol3.exe.nocrack", + "nekoparavol3.exe", + "nekoparavol3.exe.nocrack", + "nekopara_vol3.exe", + "nekopara_vol3.exe.nocrack", + "vol3.exe", + "vol3.exe.nocrack" + ]) + + exe_exists = False + found_exe = None + + # 尝试所有可能的变体 + for exe_variant in exe_variants: + exe_path = os.path.join(potential_path, exe_variant) + if os.path.exists(exe_path): + exe_exists = True + found_exe = exe_variant + if debug_mode: + print(f"DEBUG: 验证成功,找到游戏可执行文件: {exe_variant}") + break + + # 如果没有直接找到,尝试递归搜索当前目录下的所有可执行文件 + if not exe_exists: + # 遍历当前目录下的所有文件和文件夹 + for root, dirs, files in os.walk(potential_path): + for file in files: + file_lower = file.lower() + # 检查是否是游戏可执行文件(根据关键字) + if file.endswith('.exe') or file.endswith('.exe.nocrack'): + # 检查文件名中是否包含卷号或关键词 + if "Vol." in game: + vol_match = re.search(r"Vol\.(\d+)", game) + if vol_match: + vol_num = vol_match.group(1) + if (f"vol{vol_num}" in file_lower or + f"vol.{vol_num}" in file_lower or + f"vol {vol_num}" in file_lower): + exe_path = os.path.join(root, file) + exe_exists = True + found_exe = os.path.relpath(exe_path, potential_path) + if debug_mode: + print(f"DEBUG: 通过递归搜索找到游戏可执行文件: {found_exe}") + break + elif "After" in game and "after" in file_lower: + exe_path = os.path.join(root, file) + exe_exists = True + found_exe = os.path.relpath(exe_path, potential_path) + if debug_mode: + print(f"DEBUG: 通过递归搜索找到After游戏可执行文件: {found_exe}") + break + if exe_exists: + break + + # 如果找到了可执行文件,将该目录添加到游戏目录列表 + if exe_exists: + game_paths[game] = potential_path + if debug_mode: + print(f"DEBUG: 验证成功,将 {potential_path} 添加为 {game} 的目录") + else: + if debug_mode: + print(f"DEBUG: 未找到任何可执行文件变体,游戏 {game} 在 {potential_path} 未找到") + + if debug_mode: + print(f"DEBUG: 最终识别的游戏目录: {game_paths}") + print(f"--- 目录识别结束 ---") + + return game_paths \ No newline at end of file diff --git a/source/core/patch_manager.py b/source/core/patch_manager.py new file mode 100644 index 0000000..2e9d661 --- /dev/null +++ b/source/core/patch_manager.py @@ -0,0 +1,253 @@ +import os +import shutil +from PySide6.QtWidgets import QMessageBox + +class PatchManager: + """补丁管理器,用于处理补丁的安装和卸载""" + + def __init__(self, app_name, game_info, debug_manager=None): + """初始化补丁管理器 + + Args: + app_name: 应用程序名称,用于显示消息框标题 + game_info: 游戏信息字典,包含各版本的安装路径和可执行文件名 + debug_manager: 调试管理器实例,用于输出调试信息 + """ + self.app_name = app_name + self.game_info = game_info + self.debug_manager = debug_manager + self.installed_status = {} # 游戏版本的安装状态 + + def _is_debug_mode(self): + """检查是否处于调试模式 + + Returns: + bool: 是否处于调试模式 + """ + if hasattr(self.debug_manager, 'ui_manager') and hasattr(self.debug_manager.ui_manager, 'debug_action'): + return self.debug_manager.ui_manager.debug_action.isChecked() + return False + + def initialize_status(self): + """初始化所有游戏版本的安装状态""" + self.installed_status = {f"NEKOPARA Vol.{i}": False for i in range(1, 5)} + self.installed_status["NEKOPARA After"] = False + + def update_status(self, game_version, is_installed): + """更新游戏版本的安装状态 + + Args: + game_version: 游戏版本 + is_installed: 是否已安装 + """ + self.installed_status[game_version] = is_installed + + def get_status(self, game_version=None): + """获取游戏版本的安装状态 + + Args: + game_version: 游戏版本,如果为None则返回所有状态 + + Returns: + bool或dict: 指定版本的安装状态或所有版本的安装状态 + """ + if game_version: + return self.installed_status.get(game_version, False) + return self.installed_status + + def uninstall_patch(self, game_dir, game_version): + """卸载补丁 + + Args: + game_dir: 游戏目录路径 + game_version: 游戏版本 + + Returns: + bool: 卸载成功返回True,失败返回False + """ + debug_mode = self._is_debug_mode() + + if game_version not in self.game_info: + QMessageBox.critical( + None, + f"错误 - {self.app_name}", + f"\n无法识别游戏版本: {game_version}\n", + QMessageBox.StandardButton.Ok, + ) + return False + + if debug_mode: + print(f"DEBUG: 开始卸载 {game_version} 补丁,目录: {game_dir}") + + try: + files_removed = 0 + + # 获取可能的补丁文件路径 + install_path_base = os.path.basename(self.game_info[game_version]["install_path"]) + patch_file_path = os.path.join(game_dir, install_path_base) + + # 尝试查找补丁文件,支持不同大小写 + patch_files_to_check = [ + patch_file_path, + patch_file_path.lower(), + patch_file_path.upper(), + patch_file_path.replace("_", ""), + patch_file_path.replace("_", "-"), + ] + + # 查找并删除补丁文件 + patch_file_found = False + for patch_path in patch_files_to_check: + if os.path.exists(patch_path): + patch_file_found = True + os.remove(patch_path) + files_removed += 1 + if debug_mode: + print(f"DEBUG: 已删除补丁文件: {patch_path}") + + if not patch_file_found and debug_mode: + print(f"DEBUG: 未找到补丁文件,检查了以下路径: {patch_files_to_check}") + + # 检查是否有额外的签名文件 (.sig) + if game_version == "NEKOPARA After": + for patch_path in patch_files_to_check: + sig_file_path = f"{patch_path}.sig" + if os.path.exists(sig_file_path): + os.remove(sig_file_path) + files_removed += 1 + if debug_mode: + print(f"DEBUG: 已删除签名文件: {sig_file_path}") + + # 删除patch文件夹 + patch_folders_to_check = [ + os.path.join(game_dir, "patch"), + os.path.join(game_dir, "Patch"), + os.path.join(game_dir, "PATCH"), + ] + + for patch_folder in patch_folders_to_check: + if os.path.exists(patch_folder): + shutil.rmtree(patch_folder) + files_removed += 1 + if debug_mode: + print(f"DEBUG: 已删除补丁文件夹: {patch_folder}") + + # 删除game/patch文件夹 + game_folders = ["game", "Game", "GAME"] + patch_folders = ["patch", "Patch", "PATCH"] + + for game_folder in game_folders: + for patch_folder in patch_folders: + game_patch_folder = os.path.join(game_dir, game_folder, patch_folder) + if os.path.exists(game_patch_folder): + shutil.rmtree(game_patch_folder) + files_removed += 1 + if debug_mode: + print(f"DEBUG: 已删除game/patch文件夹: {game_patch_folder}") + + # 删除配置文件 + config_files = ["config.json", "Config.json", "CONFIG.JSON"] + script_files = ["scripts.json", "Scripts.json", "SCRIPTS.JSON"] + + for game_folder in game_folders: + game_path = os.path.join(game_dir, game_folder) + if os.path.exists(game_path): + # 删除配置文件 + for config_file in config_files: + config_path = os.path.join(game_path, config_file) + if os.path.exists(config_path): + os.remove(config_path) + files_removed += 1 + if debug_mode: + print(f"DEBUG: 已删除配置文件: {config_path}") + + # 删除脚本文件 + for script_file in script_files: + script_path = os.path.join(game_path, script_file) + if os.path.exists(script_path): + os.remove(script_path) + files_removed += 1 + if debug_mode: + print(f"DEBUG: 已删除脚本文件: {script_path}") + + # 更新安装状态 + self.installed_status[game_version] = False + + # 在非批量卸载模式下显示卸载成功消息 + if game_version != "all": + # 显示卸载成功消息 + if files_removed > 0: + QMessageBox.information( + None, + f"卸载完成 - {self.app_name}", + f"\n{game_version} 补丁卸载成功!\n共删除 {files_removed} 个文件/文件夹。\n", + QMessageBox.StandardButton.Ok, + ) + else: + QMessageBox.warning( + None, + f"警告 - {self.app_name}", + f"\n未找到 {game_version} 的补丁文件,可能未安装补丁或已被移除。\n", + QMessageBox.StandardButton.Ok, + ) + + # 卸载成功 + return True + + except Exception as e: + # 在非批量卸载模式下显示卸载失败消息 + if game_version != "all": + # 显示卸载失败消息 + error_message = f"\n卸载 {game_version} 补丁时出错:\n\n{str(e)}\n" + if debug_mode: + print(f"DEBUG: 卸载错误 - {str(e)}") + + QMessageBox.critical( + None, + f"卸载失败 - {self.app_name}", + error_message, + QMessageBox.StandardButton.Ok, + ) + + # 卸载失败 + return False + + def batch_uninstall_patches(self, game_dirs): + """批量卸载多个游戏的补丁 + + Args: + game_dirs: 游戏版本到游戏目录的映射字典 + + Returns: + tuple: (成功数量, 失败数量) + """ + success_count = 0 + fail_count = 0 + debug_mode = self._is_debug_mode() + + for version, path in game_dirs.items(): + try: + if self.uninstall_patch(path, version): + success_count += 1 + else: + fail_count += 1 + except Exception as e: + if debug_mode: + print(f"DEBUG: 卸载 {version} 时出错: {str(e)}") + fail_count += 1 + + return success_count, fail_count + + def show_uninstall_result(self, success_count, fail_count): + """显示批量卸载结果 + + Args: + success_count: 成功卸载的数量 + fail_count: 卸载失败的数量 + """ + QMessageBox.information( + None, + f"批量卸载完成 - {self.app_name}", + f"\n批量卸载完成!\n成功: {success_count} 个\n失败: {fail_count} 个\n", + QMessageBox.StandardButton.Ok, + ) \ No newline at end of file diff --git a/source/core/ui_manager.py b/source/core/ui_manager.py index ea5cb43..f717a9c 100644 --- a/source/core/ui_manager.py +++ b/source/core/ui_manager.py @@ -101,6 +101,11 @@ class UIManager:

{APP_NAME} v{APP_VERSION}

原作: Yanam1Anna

此应用根据 GPL-3.0 许可证 授权。

+
+

感谢:

+

- HTony03:对原项目部分源码的重构、逻辑优化和功能实现提供了支持。

+

- 钨鸮:对于云端资源存储提供了支持。

+

- XIU2/CloudflareSpeedTest:提供了 IP 优选功能的核心支持。

""" msg_box = msgbox_frame( f"关于 - {APP_NAME}", diff --git a/source/core/window_manager.py b/source/core/window_manager.py new file mode 100644 index 0000000..f447fb2 --- /dev/null +++ b/source/core/window_manager.py @@ -0,0 +1,141 @@ +from PySide6.QtCore import Qt, QPoint, QRect, QSize +from PySide6.QtGui import QPainterPath, QRegion + +class WindowManager: + """窗口管理器类,用于处理窗口的基本行为,如拖拽、调整大小和圆角设置""" + + def __init__(self, parent_window): + """初始化窗口管理器 + + Args: + parent_window: 父窗口实例 + """ + self.window = parent_window + self.ui = parent_window.ui + + # 拖动窗口相关变量 + self._drag_position = QPoint() + self._is_dragging = False + + # 窗口比例 + self.aspect_ratio = 16 / 9 + self.updateRoundedCorners = True + + # 设置圆角窗口 + self.setRoundedCorners() + + def setRoundedCorners(self): + """设置窗口圆角""" + # 实现圆角窗口 + path = QPainterPath() + path.addRoundedRect(self.window.rect(), 20, 20) + mask = QRegion(path.toFillPolygon().toPolygon()) + self.window.setMask(mask) + + # 更新resize事件时更新圆角 + self.updateRoundedCorners = True + + def handle_mouse_press(self, event): + """处理鼠标按下事件 + + Args: + event: 鼠标事件 + """ + if event.button() == Qt.MouseButton.LeftButton: + # 只有当鼠标在标题栏区域时才可以拖动 + if hasattr(self.ui, 'title_bar') and self.ui.title_bar.geometry().contains(event.position().toPoint()): + self._is_dragging = True + self._drag_position = event.globalPosition().toPoint() - self.window.frameGeometry().topLeft() + event.accept() + + def handle_mouse_move(self, event): + """处理鼠标移动事件 + + Args: + event: 鼠标事件 + """ + if event.buttons() & Qt.MouseButton.LeftButton and self._is_dragging: + self.window.move(event.globalPosition().toPoint() - self._drag_position) + event.accept() + + def handle_mouse_release(self, event): + """处理鼠标释放事件 + + Args: + event: 鼠标事件 + """ + if event.button() == Qt.MouseButton.LeftButton: + self._is_dragging = False + event.accept() + + def handle_resize(self, event): + """当窗口大小改变时更新圆角和维持纵横比 + + Args: + event: 窗口大小改变事件 + """ + # 计算基于当前宽度的合适高度,以维持16:9比例 + new_width = event.size().width() + new_height = int(new_width / self.aspect_ratio) + + if new_height != event.size().height(): + # 阻止变形,保持比例 + self.window.resize(new_width, new_height) + + # 更新主容器大小 + if hasattr(self.ui, 'main_container'): + self.ui.main_container.setGeometry(0, 0, new_width, new_height) + + # 更新内容容器大小 + if hasattr(self.ui, 'content_container'): + self.ui.content_container.setGeometry(0, 0, new_width, new_height) + + # 更新标题栏宽度和高度 + if hasattr(self.ui, 'title_bar'): + self.ui.title_bar.setGeometry(0, 0, new_width, 35) + + # 更新菜单区域 + if hasattr(self.ui, 'menu_area'): + self.ui.menu_area.setGeometry(0, 35, new_width, 30) + + # 更新内容区域大小 + if hasattr(self.ui, 'inner_content'): + self.ui.inner_content.setGeometry(0, 65, new_width, new_height - 65) + + # 更新背景图大小 + if hasattr(self.ui, 'Mainbg'): + self.ui.Mainbg.setGeometry(0, 0, new_width, new_height - 65) + + if hasattr(self.ui, 'loadbg'): + self.ui.loadbg.setGeometry(0, 0, new_width, new_height - 65) + + # 调整按钮位置 - 固定在右侧 + right_margin = 20 # 减小右边距,使按钮更靠右 + if hasattr(self.ui, 'button_container'): + btn_width = 211 # 扩大后的容器宽度 + btn_height = 111 # 扩大后的容器高度 + x_pos = new_width - btn_width - right_margin + y_pos = int((new_height - 65) * 0.28) - 10 # 调整为更靠上的位置 + self.ui.button_container.setGeometry(x_pos, y_pos, btn_width, btn_height) + + # 添加卸载补丁按钮容器的位置调整 + if hasattr(self.ui, 'uninstall_container'): + btn_width = 211 # 扩大后的容器宽度 + btn_height = 111 # 扩大后的容器高度 + x_pos = new_width - btn_width - right_margin + y_pos = int((new_height - 65) * 0.46) - 10 # 调整为中间位置 + self.ui.uninstall_container.setGeometry(x_pos, y_pos, btn_width, btn_height) + + if hasattr(self.ui, 'exit_container'): + btn_width = 211 # 扩大后的容器宽度 + btn_height = 111 # 扩大后的容器高度 + x_pos = new_width - btn_width - right_margin + y_pos = int((new_height - 65) * 0.64) - 10 # 调整为更靠下的位置 + self.ui.exit_container.setGeometry(x_pos, y_pos, btn_width, btn_height) + + # 更新圆角 + if hasattr(self, 'updateRoundedCorners') and self.updateRoundedCorners: + path = QPainterPath() + path.addRoundedRect(self.window.rect(), 20, 20) + mask = QRegion(path.toFillPolygon().toPolygon()) + self.window.setMask(mask) \ No newline at end of file diff --git a/source/data/config.py b/source/data/config.py index 01879cf..eaea795 100644 --- a/source/data/config.py +++ b/source/data/config.py @@ -3,7 +3,7 @@ import base64 # 配置信息 app_data = { - "APP_VERSION": "1.1.3", + "APP_VERSION": "1.2.0", "APP_NAME": "FRAISEMOE Addons Installer NEXT", "TEMP": "TEMP", "CACHE": "FRAISEMOE", diff --git a/source/main_window.py b/source/main_window.py index 6c309eb..4eb4b3d 100644 --- a/source/main_window.py +++ b/source/main_window.py @@ -23,7 +23,8 @@ from workers import ( HashThread, ExtractionThread, ConfigFetchThread ) from core import ( - MultiStageAnimations, UIManager, DownloadManager, DebugManager + MultiStageAnimations, UIManager, DownloadManager, DebugManager, + WindowManager, GameDetector, PatchManager, ConfigManager ) class MainWindow(QMainWindow): @@ -41,31 +42,13 @@ class MainWindow(QMainWindow): self.setMinimumSize(QSize(1024, 576)) self.setMaximumSize(QSize(1280, 720)) - # 窗口比例 (16:9) - self.aspect_ratio = 16 / 9 - - # 拖动窗口相关变量 - self._drag_position = QPoint() - self._is_dragging = False - # 初始化UI (从Ui_install.py导入) self.ui = Ui_MainWindows() self.ui.setupUi(self) - # 设置圆角窗口 - self.setRoundedCorners() - # 初始化配置 self.config = load_config() - # 初始化状态变量 - self.cloud_config = None - self.config_valid = False # 添加配置有效标志 - self.installed_status = {f"NEKOPARA Vol.{i}": False for i in range(1, 5)} - self.installed_status["NEKOPARA After"] = False # 添加After的状态 - self.hash_msg_box = None - self.progress_window = None - # 初始化工具类 self.hash_manager = HashManager(BLOCK_SIZE) self.admin_privileges = AdminPrivileges() @@ -77,9 +60,26 @@ class MainWindow(QMainWindow): # 首先设置UI - 确保debug_action已初始化 self.ui_manager.setup_ui() + # 初始化新的管理器类 + self.window_manager = WindowManager(self) self.debug_manager = DebugManager(self) + # 为debug_manager设置ui_manager引用 + 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.download_manager = DownloadManager(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.progress_window = None + # 设置关闭按钮事件连接 if hasattr(self.ui, 'close_btn'): self.ui.close_btn.clicked.connect(self.close) @@ -126,105 +126,18 @@ class MainWindow(QMainWindow): # 窗口显示后延迟100ms启动动画 QTimer.singleShot(100, self.start_animations) - def setRoundedCorners(self): - """设置窗口圆角""" - # 实现圆角窗口 - path = QPainterPath() - path.addRoundedRect(self.rect(), 20, 20) - mask = QRegion(path.toFillPolygon().toPolygon()) - self.setMask(mask) - - # 更新resize事件时更新圆角 - self.updateRoundedCorners = True - - # 添加鼠标事件处理,实现窗口拖动 + # 窗口事件处理 - 委托给WindowManager def mousePressEvent(self, event): - if event.button() == Qt.MouseButton.LeftButton: - # 只有当鼠标在标题栏区域时才可以拖动 - if hasattr(self.ui, 'title_bar') and self.ui.title_bar.geometry().contains(event.position().toPoint()): - self._is_dragging = True - self._drag_position = event.globalPosition().toPoint() - self.frameGeometry().topLeft() - event.accept() + self.window_manager.handle_mouse_press(event) def mouseMoveEvent(self, event): - if event.buttons() & Qt.MouseButton.LeftButton and self._is_dragging: - self.move(event.globalPosition().toPoint() - self._drag_position) - event.accept() + self.window_manager.handle_mouse_move(event) def mouseReleaseEvent(self, event): - if event.button() == Qt.MouseButton.LeftButton: - self._is_dragging = False - event.accept() + self.window_manager.handle_mouse_release(event) def resizeEvent(self, event): - """当窗口大小改变时更新圆角和维持纵横比""" - # 计算基于当前宽度的合适高度,以维持16:9比例 - new_width = event.size().width() - new_height = int(new_width / self.aspect_ratio) - - if new_height != event.size().height(): - # 阻止变形,保持比例 - self.resize(new_width, new_height) - - # 更新主容器大小 - if hasattr(self.ui, 'main_container'): - self.ui.main_container.setGeometry(0, 0, new_width, new_height) - - # 更新内容容器大小 - if hasattr(self.ui, 'content_container'): - self.ui.content_container.setGeometry(0, 0, new_width, new_height) - - # 更新标题栏宽度和高度 - if hasattr(self.ui, 'title_bar'): - self.ui.title_bar.setGeometry(0, 0, new_width, 35) - - # 更新菜单区域 - if hasattr(self.ui, 'menu_area'): - self.ui.menu_area.setGeometry(0, 35, new_width, 30) - - # 更新内容区域大小 - if hasattr(self.ui, 'inner_content'): - self.ui.inner_content.setGeometry(0, 65, new_width, new_height - 65) - - # 更新背景图大小 - 使用setScaledContents简化处理 - if hasattr(self.ui, 'Mainbg'): - self.ui.Mainbg.setGeometry(0, 0, new_width, new_height - 65) - # 使用setScaledContents=True,不需要手动缩放 - - if hasattr(self.ui, 'loadbg'): - self.ui.loadbg.setGeometry(0, 0, new_width, new_height - 65) - - # 调整按钮位置 - 固定在右侧 - right_margin = 20 # 减小右边距,使按钮更靠右 - if hasattr(self.ui, 'button_container'): - btn_width = 211 # 扩大后的容器宽度 - btn_height = 111 # 扩大后的容器高度 - x_pos = new_width - btn_width - right_margin - y_pos = int((new_height - 65) * 0.28) - 10 # 调整为更靠上的位置 - self.ui.button_container.setGeometry(x_pos, y_pos, btn_width, btn_height) - - # 添加卸载补丁按钮容器的位置调整 - if hasattr(self.ui, 'uninstall_container'): - btn_width = 211 # 扩大后的容器宽度 - btn_height = 111 # 扩大后的容器高度 - x_pos = new_width - btn_width - right_margin - y_pos = int((new_height - 65) * 0.46) - 10 # 调整为中间位置 - self.ui.uninstall_container.setGeometry(x_pos, y_pos, btn_width, btn_height) - - if hasattr(self.ui, 'exit_container'): - btn_width = 211 # 扩大后的容器宽度 - btn_height = 111 # 扩大后的容器高度 - x_pos = new_width - btn_width - right_margin - y_pos = int((new_height - 65) * 0.64) - 10 # 调整为更靠下的位置 - self.ui.exit_container.setGeometry(x_pos, y_pos, btn_width, btn_height) - - # 更新圆角 - if hasattr(self, 'updateRoundedCorners') and self.updateRoundedCorners: - path = QPainterPath() - path.addRoundedRect(self.rect(), 20, 20) - mask = QRegion(path.toFillPolygon().toPolygon()) - self.setMask(mask) - + self.window_manager.handle_resize(event) super().resizeEvent(event) def start_animations(self): @@ -251,30 +164,37 @@ class MainWindow(QMainWindow): else: self.set_start_button_enabled(False) - def set_start_button_enabled(self, enabled): + def set_start_button_enabled(self, enabled, installing=False): """设置开始安装按钮的启用状态和视觉效果 Args: enabled: 是否启用按钮 + installing: 是否正在安装中 """ - self.ui.start_install_btn.setEnabled(True) # 始终启用按钮,以便捕获点击事件 - - # 根据状态修改文本内容,但不再修改颜色样式 - if enabled: - self.ui.start_install_text.setText("开始安装") + if installing: + # 安装中状态 - 按钮被禁用但显示"正在安装" + self.ui.start_install_btn.setEnabled(False) + self.ui.start_install_text.setText("正在安装") + self.install_button_enabled = False else: - self.ui.start_install_text.setText("!无法安装!") + # 正常状态 - 按钮可点击,但根据enabled决定是否显示"无法安装" + self.ui.start_install_btn.setEnabled(True) # 始终启用按钮,以便捕获点击事件 - # 记录当前按钮状态,用于点击事件处理 - self.install_button_enabled = enabled + # 根据状态修改文本内容 + if enabled: + self.ui.start_install_text.setText("开始安装") + else: + self.ui.start_install_text.setText("!无法安装!") + + # 记录当前按钮状态,用于点击事件处理 + self.install_button_enabled = enabled def fetch_cloud_config(self): """获取云端配置""" - headers = {"User-Agent": UA} - debug_mode = self.ui_manager.debug_action.isChecked() if self.ui_manager.debug_action else False - self.config_fetch_thread = ConfigFetchThread(CONFIG_URL, headers, debug_mode, self) - self.config_fetch_thread.finished.connect(self.on_config_fetched) - self.config_fetch_thread.start() + self.config_manager.fetch_cloud_config( + lambda url, headers, debug_mode, parent=None: ConfigFetchThread(url, headers, debug_mode, self), + self.on_config_fetched + ) def on_config_fetched(self, data, error_message): """云端配置获取完成的回调处理 @@ -283,71 +203,30 @@ class MainWindow(QMainWindow): data: 获取到的配置数据 error_message: 错误信息,如果有 """ - # 定义debug_mode变量在方法开头 - debug_mode = self.ui_manager.debug_action.isChecked() if hasattr(self.ui_manager, 'debug_action') and self.ui_manager.debug_action else False + # 处理返回结果 + result = self.config_manager.on_config_fetched(data, error_message) - if error_message: - # 标记配置无效 - self.config_valid = False - - # 记录错误信息,用于按钮点击时显示 - if error_message == "update_required": - self.last_error_message = "update_required" - msg_box = msgbox_frame( - f"更新提示 - {APP_NAME}", - "\n当前版本过低,请及时更新。\n", - QMessageBox.StandardButton.Ok, - ) - msg_box.exec() - # 在浏览器中打开项目主页 - webbrowser.open("https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/") + # 根据返回的操作执行相应动作 + if result and "action" in result: + if result["action"] == "exit": # 强制关闭程序 self.shutdown_app(force_exit=True) - return - elif "missing_keys" in error_message: - self.last_error_message = "missing_keys" - missing_versions = error_message.split(":")[1] - msg_box = msgbox_frame( - f"配置缺失 - {APP_NAME}", - f'\n云端缺失下载链接,可能云服务器正在维护,不影响其他版本下载。\n当前缺失版本:"{missing_versions}"\n', - QMessageBox.StandardButton.Ok, - ) - msg_box.exec() - # 对于部分缺失,仍然允许使用,因为可能只影响部分游戏版本 - self.config_valid = True - else: - # 设置网络错误标记 - self.last_error_message = "network_error" - - # 显示通用错误消息,只在debug模式下显示详细错误 - error_msg = "访问云端配置失败,请检查网络状况或稍后再试。" - if debug_mode and "详细错误:" in error_message: - msg_box = msgbox_frame( - f"错误 - {APP_NAME}", - f"\n{error_message}\n", - QMessageBox.StandardButton.Ok, - ) - else: - msg_box = msgbox_frame( - f"错误 - {APP_NAME}", - f"\n{error_msg}\n", - QMessageBox.StandardButton.Ok, - ) - msg_box.exec() - # 在无法连接到云端时禁用开始安装按钮 + elif result["action"] == "disable_button": + # 禁用开始安装按钮 self.set_start_button_enabled(False) - else: - self.cloud_config = data - # 标记配置有效 - self.config_valid = True - # 清除错误信息 - self.last_error_message = "" - - if debug_mode: - print("--- Cloud config fetched successfully ---") - print(json.dumps(data, indent=2)) - # 确保按钮在成功获取配置时启用 - self.set_start_button_enabled(True) + + # 检查是否有后续操作 + 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) + + # 同步状态 + 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() def toggle_debug_mode(self, checked): """切换调试模式 @@ -359,7 +238,7 @@ class MainWindow(QMainWindow): def save_config(self, config): """保存配置的便捷方法""" - save_config(config) + self.config_manager.save_config(config) def create_download_thread(self, url, _7z_path, game_version): """创建下载线程 @@ -631,7 +510,7 @@ class MainWindow(QMainWindow): # 获取游戏目录 from PySide6.QtWidgets import QFileDialog - debug_mode = self.ui_manager.debug_action.isChecked() if hasattr(self.ui_manager, 'debug_action') and self.ui_manager.debug_action else False + debug_mode = self.debug_manager._is_debug_mode() # 提示用户选择目录 file_dialog_info = "选择游戏上级目录" if debug_mode else "选择游戏目录" @@ -644,7 +523,7 @@ class MainWindow(QMainWindow): print(f"DEBUG: 卸载功能 - 用户选择了目录: {selected_folder}") # 首先尝试将选择的目录视为上级目录,使用增强的目录识别功能 - game_dirs = self.identify_game_directories_improved(selected_folder) + game_dirs = self.game_detector.identify_game_directories_improved(selected_folder) if game_dirs and len(game_dirs) > 0: # 找到了游戏目录,显示选择对话框 @@ -680,26 +559,9 @@ class MainWindow(QMainWindow): ) if reply == QMessageBox.StandardButton.Yes: - success_count = 0 - fail_count = 0 - for version, path in game_dirs.items(): - try: - if self.uninstall_patch(path, version): - success_count += 1 - else: - fail_count += 1 - except Exception as e: - if debug_mode: - print(f"DEBUG: 卸载 {version} 时出错: {str(e)}") - fail_count += 1 - - # 显示批量卸载结果 - QMessageBox.information( - self, - f"批量卸载完成 - {APP_NAME}", - f"\n批量卸载完成!\n成功: {success_count} 个\n失败: {fail_count} 个\n", - QMessageBox.StandardButton.Ok, - ) + # 使用批量卸载方法 + success_count, fail_count = self.patch_manager.batch_uninstall_patches(game_dirs) + self.patch_manager.show_uninstall_result(success_count, fail_count) else: # 卸载选中的单个游戏 game_version = selected_game @@ -710,7 +572,7 @@ class MainWindow(QMainWindow): if debug_mode: print(f"DEBUG: 卸载功能 - 未在上级目录找到游戏,尝试将选择的目录视为游戏目录") - game_version = self.identify_game_version(selected_folder) + game_version = self.game_detector.identify_game_version(selected_folder) if game_version: if debug_mode: @@ -735,7 +597,7 @@ class MainWindow(QMainWindow): game_dir: 游戏目录 game_version: 游戏版本 """ - debug_mode = self.ui_manager.debug_action.isChecked() if hasattr(self.ui_manager, 'debug_action') and self.ui_manager.debug_action else False + debug_mode = self.debug_manager._is_debug_mode() # 确认卸载 reply = QMessageBox.question( @@ -750,450 +612,6 @@ class MainWindow(QMainWindow): return # 开始卸载补丁 - self.uninstall_patch(game_dir, game_version) - - def identify_game_version(self, game_dir): - """识别游戏版本 - - Args: - game_dir: 游戏目录路径 - - Returns: - str: 游戏版本名称,如果不是有效的游戏目录则返回None - """ - import os - import re - - # 调试模式 - debug_mode = self.ui_manager.debug_action.isChecked() if hasattr(self.ui_manager, 'debug_action') and self.ui_manager.debug_action else False - - if debug_mode: - print(f"DEBUG: 尝试识别游戏版本: {game_dir}") - - # 先通过目录名称进行初步推测(这将作为递归搜索的提示) - dir_name = os.path.basename(game_dir).lower() - potential_version = None - vol_num = None - - # 提取卷号或判断是否是After - if "vol" in dir_name or "vol." in dir_name: - vol_match = re.search(r"vol(?:\.|\s*)?(\d+)", dir_name) - if vol_match: - vol_num = vol_match.group(1) - potential_version = f"NEKOPARA Vol.{vol_num}" - if debug_mode: - print(f"DEBUG: 从目录名推测游戏版本: {potential_version}, 卷号: {vol_num}") - elif "after" in dir_name: - potential_version = "NEKOPARA After" - if debug_mode: - print(f"DEBUG: 从目录名推测游戏版本: NEKOPARA After") - - # 检查是否为NEKOPARA游戏目录 - # 通过检查游戏可执行文件来识别游戏版本 - for game_version, info in GAME_INFO.items(): - # 尝试多种可能的可执行文件名变体 - exe_variants = [ - info["exe"], # 标准文件名 - info["exe"] + ".nocrack", # Steam加密版本 - info["exe"].replace(".exe", ""), # 无扩展名版本 - info["exe"].replace("NEKOPARA", "nekopara").lower(), # 全小写变体 - info["exe"].lower(), # 小写变体 - info["exe"].lower() + ".nocrack", # 小写变体的Steam加密版本 - ] - - # 对于Vol.3可能有特殊名称 - if "Vol.3" in game_version: - # 增加可能的卷3特定的变体 - exe_variants.extend([ - "NEKOPARAVol3.exe", - "NEKOPARAVol3.exe.nocrack", - "nekoparavol3.exe", - "nekoparavol3.exe.nocrack", - "nekopara_vol3.exe", - "nekopara_vol3.exe.nocrack", - "vol3.exe", - "vol3.exe.nocrack" - ]) - - for exe_variant in exe_variants: - exe_path = os.path.join(game_dir, exe_variant) - if os.path.exists(exe_path): - if debug_mode: - print(f"DEBUG: 通过可执行文件确认游戏版本: {game_version}, 文件: {exe_variant}") - return game_version - - # 如果没有直接匹配,尝试递归搜索 - if potential_version: - # 从预测的版本中获取卷号或确认是否是After - is_after = "After" in potential_version - if not vol_num and not is_after: - vol_match = re.search(r"Vol\.(\d+)", potential_version) - if vol_match: - vol_num = vol_match.group(1) - - # 递归搜索可执行文件 - for root, dirs, files in os.walk(game_dir): - for file in files: - file_lower = file.lower() - if file.endswith('.exe') or file.endswith('.exe.nocrack'): - # 检查文件名中是否包含卷号或关键词 - if ((vol_num and (f"vol{vol_num}" in file_lower or - f"vol.{vol_num}" in file_lower or - f"vol {vol_num}" in file_lower)) or - (is_after and "after" in file_lower)): - if debug_mode: - print(f"DEBUG: 通过递归搜索确认游戏版本: {potential_version}, 文件: {file}") - return potential_version - - # 如果仍然没有找到,基于目录名的推测返回结果 - if potential_version: - if debug_mode: - print(f"DEBUG: 基于目录名返回推测的游戏版本: {potential_version}") - return potential_version - - if debug_mode: - print(f"DEBUG: 无法识别游戏版本: {game_dir}") - - return None - - def identify_game_directories_improved(self, selected_folder): - """改进的游戏目录识别,支持大小写不敏感和特殊字符处理 - - Args: - selected_folder: 选择的上级目录 - - Returns: - dict: 游戏版本到游戏目录的映射 - """ - import os - import re - - # 添加debug日志 - debug_mode = self.ui_manager.debug_action.isChecked() if hasattr(self.ui_manager, 'debug_action') and self.ui_manager.debug_action else False - - if debug_mode: - print(f"--- 开始识别目录: {selected_folder} ---") - - game_paths = {} - - # 获取上级目录中的所有文件夹 - try: - all_dirs = [d for d in os.listdir(selected_folder) if os.path.isdir(os.path.join(selected_folder, d))] - if debug_mode: - print(f"DEBUG: 找到以下子目录: {all_dirs}") - except Exception as e: - if debug_mode: - print(f"DEBUG: 无法读取目录 {selected_folder}: {str(e)}") - return {} - - for game, info in GAME_INFO.items(): - expected_dir = info["install_path"].split("/")[0] # 例如 "NEKOPARA Vol. 1" - expected_exe = info["exe"] # 标准可执行文件名 - - if debug_mode: - print(f"DEBUG: 搜索游戏 {game}, 预期目录: {expected_dir}, 预期可执行文件: {expected_exe}") - - # 尝试不同的匹配方法 - found_dir = None - - # 1. 精确匹配 - if expected_dir in all_dirs: - found_dir = expected_dir - if debug_mode: - print(f"DEBUG: 精确匹配成功: {expected_dir}") - - # 2. 大小写不敏感匹配 - if not found_dir: - for dir_name in all_dirs: - if expected_dir.lower() == dir_name.lower(): - found_dir = dir_name - if debug_mode: - print(f"DEBUG: 大小写不敏感匹配成功: {dir_name}") - break - - # 3. 更模糊的匹配(允许特殊字符差异) - if not found_dir: - # 准备用于模糊匹配的正则表达式模式 - # 替换空格为可选空格或连字符,替换点为可选点 - pattern_text = expected_dir.replace(" ", "[ -]?").replace(".", "\\.?") - pattern = re.compile(f"^{pattern_text}$", re.IGNORECASE) - - for dir_name in all_dirs: - if pattern.match(dir_name): - found_dir = dir_name - if debug_mode: - print(f"DEBUG: 模糊匹配成功: {dir_name} 匹配模式 {pattern_text}") - break - - # 4. 如果还是没找到,尝试更宽松的匹配 - if not found_dir: - vol_match = re.search(r"vol(?:\.|\s*)?(\d+)", expected_dir, re.IGNORECASE) - vol_num = None - if vol_match: - vol_num = vol_match.group(1) - if debug_mode: - print(f"DEBUG: 提取卷号: {vol_num}") - - is_after = "after" in expected_dir.lower() - - for dir_name in all_dirs: - dir_lower = dir_name.lower() - - # 对于After特殊处理 - if is_after and "after" in dir_lower: - found_dir = dir_name - if debug_mode: - print(f"DEBUG: After特殊匹配成功: {dir_name}") - break - - # 对于Vol特殊处理 - if vol_num: - # 查找目录名中的卷号 - dir_vol_match = re.search(r"vol(?:\.|\s*)?(\d+)", dir_lower) - if dir_vol_match and dir_vol_match.group(1) == vol_num: - found_dir = dir_name - if debug_mode: - print(f"DEBUG: 卷号匹配成功: {dir_name} 卷号 {vol_num}") - break - - # 如果找到匹配的目录,验证exe文件是否存在 - if found_dir: - potential_path = os.path.join(selected_folder, found_dir) - - # 尝试多种可能的可执行文件名变体 - # 包括Steam加密版本和其他可能的变体 - exe_variants = [ - expected_exe, # 标准文件名 - expected_exe + ".nocrack", # Steam加密版本 - expected_exe.replace(".exe", ""),# 无扩展名版本 - # Vol.3的特殊变体,因为它的文件名可能不一样 - expected_exe.replace("NEKOPARA", "nekopara").lower(), # 全小写变体 - expected_exe.lower(), # 小写变体 - expected_exe.lower() + ".nocrack", # 小写变体的Steam加密版本 - ] - - # 对于Vol.3可能有特殊名称 - if "Vol.3" in game: - # 增加可能的卷3特定的变体 - exe_variants.extend([ - "NEKOPARAVol3.exe", - "NEKOPARAVol3.exe.nocrack", - "nekoparavol3.exe", - "nekoparavol3.exe.nocrack", - "nekopara_vol3.exe", - "nekopara_vol3.exe.nocrack", - "vol3.exe", - "vol3.exe.nocrack" - ]) - - exe_exists = False - found_exe = None - - # 尝试所有可能的变体 - for exe_variant in exe_variants: - exe_path = os.path.join(potential_path, exe_variant) - if os.path.exists(exe_path): - exe_exists = True - found_exe = exe_variant - if debug_mode: - print(f"DEBUG: 验证成功,找到游戏可执行文件: {exe_variant}") - break - - # 如果没有直接找到,尝试递归搜索当前目录下的所有可执行文件 - if not exe_exists: - # 遍历当前目录下的所有文件和文件夹 - for root, dirs, files in os.walk(potential_path): - for file in files: - file_lower = file.lower() - # 检查是否是游戏可执行文件(根据关键字) - if file.endswith('.exe') or file.endswith('.exe.nocrack'): - # 检查文件名中是否包含卷号或关键词 - if (f"vol{vol_num}" in file_lower or - f"vol.{vol_num}" in file_lower or - f"vol {vol_num}" in file_lower or - (is_after and "after" in file_lower)): - exe_path = os.path.join(root, file) - exe_exists = True - found_exe = os.path.relpath(exe_path, potential_path) - if debug_mode: - print(f"DEBUG: 通过递归搜索找到游戏可执行文件: {found_exe}") - break - if exe_exists: - break - - # 如果找到了可执行文件,将该目录添加到游戏目录列表 - if exe_exists: - game_paths[game] = potential_path - if debug_mode: - print(f"DEBUG: 验证成功,将 {potential_path} 添加为 {game} 的目录") - else: - if debug_mode: - print(f"DEBUG: 未找到任何可执行文件变体,游戏 {game} 在 {potential_path} 未找到") - - if debug_mode: - print(f"DEBUG: 最终识别的游戏目录: {game_paths}") - print(f"--- 目录识别结束 ---") - - return game_paths - - def uninstall_patch(self, game_dir, game_version): - """卸载补丁 - - Args: - game_dir: 游戏目录路径 - game_version: 游戏版本 - - Returns: - bool: 卸载成功返回True,失败返回False - """ - import os - import shutil - - debug_mode = self.ui_manager.debug_action.isChecked() if hasattr(self.ui_manager, 'debug_action') and self.ui_manager.debug_action else False - - if game_version not in GAME_INFO: - QMessageBox.critical( - self, - f"错误 - {APP_NAME}", - f"\n无法识别游戏版本: {game_version}\n", - QMessageBox.StandardButton.Ok, - ) - return False - - if debug_mode: - print(f"DEBUG: 开始卸载 {game_version} 补丁,目录: {game_dir}") - - try: - files_removed = 0 - - # 获取可能的补丁文件路径 - install_path_base = os.path.basename(GAME_INFO[game_version]["install_path"]) - patch_file_path = os.path.join(game_dir, install_path_base) - - # 尝试查找补丁文件,支持不同大小写 - patch_files_to_check = [ - patch_file_path, - patch_file_path.lower(), - patch_file_path.upper(), - patch_file_path.replace("_", ""), - patch_file_path.replace("_", "-"), - ] - - # 查找并删除补丁文件 - patch_file_found = False - for patch_path in patch_files_to_check: - if os.path.exists(patch_path): - patch_file_found = True - os.remove(patch_path) - files_removed += 1 - if debug_mode: - print(f"DEBUG: 已删除补丁文件: {patch_path}") - - if not patch_file_found and debug_mode: - print(f"DEBUG: 未找到补丁文件,检查了以下路径: {patch_files_to_check}") - - # 检查是否有额外的签名文件 (.sig) - if game_version == "NEKOPARA After": - for patch_path in patch_files_to_check: - sig_file_path = f"{patch_path}.sig" - if os.path.exists(sig_file_path): - os.remove(sig_file_path) - files_removed += 1 - if debug_mode: - print(f"DEBUG: 已删除签名文件: {sig_file_path}") - - # 删除patch文件夹 - patch_folders_to_check = [ - os.path.join(game_dir, "patch"), - os.path.join(game_dir, "Patch"), - os.path.join(game_dir, "PATCH"), - ] - - for patch_folder in patch_folders_to_check: - if os.path.exists(patch_folder): - shutil.rmtree(patch_folder) - files_removed += 1 - if debug_mode: - print(f"DEBUG: 已删除补丁文件夹: {patch_folder}") - - # 删除game/patch文件夹 - game_folders = ["game", "Game", "GAME"] - patch_folders = ["patch", "Patch", "PATCH"] - - for game_folder in game_folders: - for patch_folder in patch_folders: - game_patch_folder = os.path.join(game_dir, game_folder, patch_folder) - if os.path.exists(game_patch_folder): - shutil.rmtree(game_patch_folder) - files_removed += 1 - if debug_mode: - print(f"DEBUG: 已删除game/patch文件夹: {game_patch_folder}") - - # 删除配置文件 - config_files = ["config.json", "Config.json", "CONFIG.JSON"] - script_files = ["scripts.json", "Scripts.json", "SCRIPTS.JSON"] - - for game_folder in game_folders: - game_path = os.path.join(game_dir, game_folder) - if os.path.exists(game_path): - # 删除配置文件 - for config_file in config_files: - config_path = os.path.join(game_path, config_file) - if os.path.exists(config_path): - os.remove(config_path) - files_removed += 1 - if debug_mode: - print(f"DEBUG: 已删除配置文件: {config_path}") - - # 删除脚本文件 - for script_file in script_files: - script_path = os.path.join(game_path, script_file) - if os.path.exists(script_path): - os.remove(script_path) - files_removed += 1 - if debug_mode: - print(f"DEBUG: 已删除脚本文件: {script_path}") - - # 更新安装状态 - self.installed_status[game_version] = False - - # 在非批量卸载模式下显示卸载成功消息 - if game_version != "all": - # 显示卸载成功消息 - if files_removed > 0: - QMessageBox.information( - self, - f"卸载完成 - {APP_NAME}", - f"\n{game_version} 补丁卸载成功!\n共删除 {files_removed} 个文件/文件夹。\n", - QMessageBox.StandardButton.Ok, - ) - else: - QMessageBox.warning( - self, - f"警告 - {APP_NAME}", - f"\n未找到 {game_version} 的补丁文件,可能未安装补丁或已被移除。\n", - QMessageBox.StandardButton.Ok, - ) - - # 卸载成功 - return True - - except Exception as e: - # 在非批量卸载模式下显示卸载失败消息 - if game_version != "all": - # 显示卸载失败消息 - error_message = f"\n卸载 {game_version} 补丁时出错:\n\n{str(e)}\n" - if debug_mode: - print(f"DEBUG: 卸载错误 - {str(e)}") - - QMessageBox.critical( - self, - f"卸载失败 - {APP_NAME}", - error_message, - QMessageBox.StandardButton.Ok, - ) - - # 卸载失败 - return False + self.patch_manager.uninstall_patch(game_dir, game_version) \ No newline at end of file diff --git a/source/workers/download.py b/source/workers/download.py index 30a253d..1348214 100644 --- a/source/workers/download.py +++ b/source/workers/download.py @@ -91,7 +91,7 @@ class DownloadThread(QThread): # 正则表达式用于解析aria2c的输出 # 例如: #1 GID[...]( 5%) CN:1 DL:10.5MiB/s ETA:1m30s - progress_pattern = re.compile(r'\((\d{1,3})%\).*?CN:(\d+).*?DL:\s*([^\s]+).*?ETA:\s*([^\s]+)') + progress_pattern = re.compile(r'\((\d{1,3})%\).*?CN:(\d+).*?DL:\s*([^\s]+).*?ETA:\s*([^\s\]]+)') full_output = [] while self._is_running and self.process.poll() is None: @@ -170,8 +170,12 @@ class ProgressWindow(QDialog): speed = data.get("speed", "-") threads = data.get("threads", "-") eta = data.get("eta", "-") + + # 清除ETA值中可能存在的"]"符号 + if isinstance(eta, str): + eta = eta.replace("]", "") - self.game_label.setText(f"正在下载: {game_version}") + self.game_label.setText(f"正在下载 {game_version} 的补丁") self.progress_bar.setValue(int(percent)) self.stats_label.setText(f"速度: {speed} | 线程: {threads} | 剩余时间: {eta}")