diff --git a/source/Main.py b/source/Main.py index ae3efe1..f33ef5c 100644 --- a/source/Main.py +++ b/source/Main.py @@ -4,8 +4,8 @@ import datetime from PySide6.QtWidgets import QApplication, QMessageBox from main_window import MainWindow from core.managers.privacy_manager import PrivacyManager -from utils.logger import setup_logger -from config.config import LOG_FILE, APP_NAME +from utils.logger import setup_logger, cleanup_old_logs +from config.config import LOG_FILE, APP_NAME, LOG_RETENTION_DAYS from utils import load_config if __name__ == "__main__": @@ -17,6 +17,10 @@ if __name__ == "__main__": config = load_config() debug_mode = config.get("debug_mode", False) + # 在应用启动时清理过期的日志文件 + cleanup_old_logs(LOG_RETENTION_DAYS) + logger.info(f"已执行日志清理,保留最近{LOG_RETENTION_DAYS}天的日志") + # 如果调试模式已启用,确保立即创建主日志文件 if debug_mode: try: @@ -26,14 +30,13 @@ if __name__ == "__main__": os.makedirs(log_dir, exist_ok=True) logger.info(f"已创建日志目录: {log_dir}") - # 创建新的日志文件(使用覆盖模式) - with open(LOG_FILE, 'w', encoding='utf-8') as f: - current_time = datetime.datetime.now() - formatted_date = current_time.strftime("%Y-%m-%d") - formatted_time = current_time.strftime("%H:%M:%S") - f.write(f"--- 新调试会话开始于 {os.path.basename(LOG_FILE)} ---\n") - f.write(f"--- 应用版本: {APP_NAME} ---\n") - f.write(f"--- 日期: {formatted_date} 时间: {formatted_time} ---\n\n") + # 记录调试会话信息 + logger.info(f"--- 新调试会话开始于 {os.path.basename(LOG_FILE)} ---") + logger.info(f"--- 应用版本: {APP_NAME} ---") + current_time = datetime.datetime.now() + formatted_date = current_time.strftime("%Y-%m-%d") + formatted_time = current_time.strftime("%H:%M:%S") + logger.info(f"--- 日期: {formatted_date} 时间: {formatted_time} ---") logger.info(f"调试模式已启用,日志文件路径: {os.path.abspath(LOG_FILE)}") except Exception as e: diff --git a/source/STRUCTURE.md b/source/STRUCTURE.md deleted file mode 100644 index e516569..0000000 --- a/source/STRUCTURE.md +++ /dev/null @@ -1,63 +0,0 @@ -# FRAISEMOE Addons Installer NEXT - 项目结构 - -## 目录结构 - -``` -source/ -├── assets/ # 所有静态资源文件 -│ ├── fonts/ # 字体文件 -│ ├── images/ # 图片资源 -│ └── resources/ # 其他资源文件 -├── bin/ # 二进制工具文件 -├── config/ # 配置文件 -├── core/ # 核心功能模块 -│ ├── managers/ # 所有管理器类 -│ └── handlers/ # 处理器类 -├── data/ # 数据文件 -├── ui/ # 用户界面相关 -│ ├── components/ # UI组件 -│ ├── windows/ # 窗口定义 -│ └── views/ # 视图定义 -├── utils/ # 工具类和辅助函数 -├── workers/ # 后台工作线程 -└── main.py # 主入口文件 -``` - -## 文件路径映射 - -| 重构前 | 重构后 | -| ------ | ------ | -| source/Main.py | source/main.py | -| source/fonts/* | source/assets/fonts/* | -| source/IMG/* | source/assets/images/* | -| source/resources/* | source/assets/resources/* | -| source/data/config.py | source/config/config.py | -| source/data/privacy_policy.py | source/config/privacy_policy.py | -| source/core/animations.py | source/core/managers/animations.py | -| source/core/cloudflare_optimizer.py | source/core/managers/cloudflare_optimizer.py | -| source/core/config_manager.py | source/core/managers/config_manager.py | -| source/core/debug_manager.py | source/core/managers/debug_manager.py | -| source/core/download_manager.py | source/core/managers/download_manager.py | -| source/core/download_task_manager.py | source/core/managers/download_task_manager.py | -| source/core/extraction_handler.py | source/core/handlers/extraction_handler.py | -| source/core/game_detector.py | source/core/managers/game_detector.py | -| source/core/ipv6_manager.py | source/core/managers/ipv6_manager.py | -| source/core/offline_mode_manager.py | source/core/managers/offline_mode_manager.py | -| source/core/patch_detector.py | source/core/managers/patch_detector.py | -| source/core/patch_manager.py | source/core/managers/patch_manager.py | -| source/core/privacy_manager.py | source/core/managers/privacy_manager.py | -| source/core/ui_manager.py | source/core/managers/ui_manager.py | -| source/core/window_manager.py | source/core/managers/window_manager.py | -| source/handlers/* | source/core/handlers/* | - -## 模块职责划分 - -1. **managers**: 负责管理应用程序的各个方面,如配置、下载、游戏检测等。 -2. **handlers**: 负责处理特定的操作,如提取文件、打补丁、卸载等。 -3. **assets**: 存储应用程序使用的静态资源。 -4. **config**: 存储应用程序的配置信息。 -5. **ui**: 负责用户界面相关的组件和视图。 -6. **utils**: 提供各种实用工具函数。 -7. **workers**: 负责在后台执行耗时操作的线程。 - -这种结构更加清晰地区分了各个模块的职责,使代码更容易维护和扩展。 \ No newline at end of file diff --git a/source/config/config.py b/source/config/config.py index c0ae378..fe66a1f 100644 --- a/source/config/config.py +++ b/source/config/config.py @@ -46,29 +46,51 @@ app_data = { }, } -# Base64解码 -def decode_base64(encoded_str): - return base64.b64decode(encoded_str).decode("utf-8") +def decode_base64(b64str): + """解码base64字符串""" + try: + return base64.b64decode(b64str).decode('utf-8') + except: + return b64str + +# 确保缓存目录存在 +def ensure_cache_dirs(): + os.makedirs(CACHE, exist_ok=True) + os.makedirs(PLUGIN, exist_ok=True) # 全局变量 -APP_VERSION = app_data["APP_VERSION"] APP_NAME = app_data["APP_NAME"] +APP_VERSION = app_data["APP_VERSION"] # 从app_data中获取,不再重复定义 TEMP = os.getenv(app_data["TEMP"]) or app_data["TEMP"] CACHE = os.path.join(TEMP, app_data["CACHE"]) CONFIG_FILE = os.path.join(CACHE, "config.json") +# 日志配置 +LOG_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "log") +LOG_LEVEL = "DEBUG" # 可选值: DEBUG, INFO, WARNING, ERROR, CRITICAL + +# 日志文件大小和轮转配置(新增) +LOG_MAX_SIZE = 10 * 1024 * 1024 # 10MB +LOG_BACKUP_COUNT = 3 # 保留3个备份文件 +LOG_RETENTION_DAYS = 7 # 日志保留7天 + # 将log文件放在程序根目录下的log文件夹中,使用日期+时间戳格式命名 root_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) log_dir = os.path.join(root_dir, "log") +os.makedirs(log_dir, exist_ok=True) current_datetime = datetime.datetime.now().strftime("%Y%m%d-%H%M%S") LOG_FILE = os.path.join(log_dir, f"log-{current_datetime}.txt") PLUGIN = os.path.join(CACHE, app_data["PLUGIN"]) CONFIG_URL = decode_base64(app_data["CONFIG_URL"]) UA = app_data["UA_TEMPLATE"].format(APP_VERSION) -GAME_INFO = app_data["game_info"] + +# 哈希计算块大小 BLOCK_SIZE = 67108864 HASH_SIZE = 134217728 + +# 资源哈希值 +GAME_INFO = app_data["game_info"] PLUGIN_HASH = { "NEKOPARA Vol.1": GAME_INFO["NEKOPARA Vol.1"]["hash"], "NEKOPARA Vol.2": GAME_INFO["NEKOPARA Vol.2"]["hash"], diff --git a/source/core/managers/debug_manager.py b/source/core/managers/debug_manager.py index 82e7399..f5771b7 100644 --- a/source/core/managers/debug_manager.py +++ b/source/core/managers/debug_manager.py @@ -125,14 +125,9 @@ class DebugManager: f.write(f"--- 日期: {formatted_date} 时间: {formatted_time} ---\n\n") logger.info(f"已创建日志文件: {os.path.abspath(LOG_FILE)}") - # 保存原始的 stdout 和 stderr + # 保存原始的 stdout 并创建Logger实例 self.original_stdout = sys.stdout - self.original_stderr = sys.stderr - - # 创建 Logger 实例 self.logger = Logger(LOG_FILE, self.original_stdout) - sys.stdout = self.logger - sys.stderr = self.logger logger.info(f"--- Debug mode enabled (log file: {os.path.abspath(LOG_FILE)}) ---") except (IOError, OSError) as e: @@ -143,7 +138,10 @@ class DebugManager: """停止日志记录""" if self.logger: logger.info("--- Debug mode disabled ---") - sys.stdout = self.original_stdout - sys.stderr = self.original_stderr - self.logger.close() + # 恢复stdout到原始状态 + if hasattr(self, 'original_stdout') and self.original_stdout: + sys.stdout = self.original_stdout + # 关闭日志文件 + if hasattr(self.logger, 'close'): + self.logger.close() self.logger = None \ No newline at end of file diff --git a/source/core/managers/download_manager.py b/source/core/managers/download_manager.py index 6feaa50..96de813 100644 --- a/source/core/managers/download_manager.py +++ b/source/core/managers/download_manager.py @@ -17,6 +17,11 @@ from core.managers.cloudflare_optimizer import CloudflareOptimizer from core.managers.download_task_manager import DownloadTaskManager from core.handlers.extraction_handler import ExtractionHandler from utils.logger import setup_logger +from utils.url_censor import censor_url +from utils.helpers import ( + HashManager, AdminPrivileges, msgbox_frame, HostsManager +) +from workers.download import DownloadThread, ProgressWindow # 初始化logger logger = setup_logger("download_manager") @@ -41,6 +46,12 @@ class DownloadManager: self.download_task_manager = DownloadTaskManager(main_window, self.download_thread_level) self.extraction_handler = ExtractionHandler(main_window) + self.extraction_thread = None + self.progress_window = None + + # 调试管理器 + self.debug_manager = getattr(main_window, 'debug_manager', None) + def file_dialog(self): """显示文件夹选择对话框,选择游戏安装目录""" self.selected_folder = QtWidgets.QFileDialog.getExistingDirectory( @@ -1058,4 +1069,41 @@ class DownloadManager: plugin_path = os.path.join(PLUGIN, GAME_INFO[game_version]["plugin_path"]) self.download_queue.append((url, game_folder, game_version, _7z_path, plugin_path)) elif debug_mode: - logger.warning(f"DEBUG: 未找到 {game_version} 的下载URL") \ No newline at end of file + logger.warning(f"DEBUG: 未找到 {game_version} 的下载URL") + + def graceful_stop_threads(self, threads_dict, timeout_ms=2000): + """优雅地停止一组线程. + + Args: + threads_dict (dict): 线程名字和线程对象的字典. + timeout_ms (int): 等待线程自然结束的超时时间. + """ + for name, thread_obj in threads_dict.items(): + if not thread_obj or not hasattr(thread_obj, 'isRunning') or not thread_obj.isRunning(): + continue + + try: + if hasattr(thread_obj, 'requestInterruption'): + thread_obj.requestInterruption() + + if thread_obj.wait(timeout_ms): + if self.debug_manager: + self.debug_manager.log_debug(f"线程 {name} 已优雅停止.") + else: + if self.debug_manager: + self.debug_manager.log_warning(f"线程 {name} 超时,强制终止.") + thread_obj.terminate() + thread_obj.wait(1000) # a short wait after termination + except Exception as e: + if self.debug_manager: + self.debug_manager.log_error(f"停止线程 {name} 时发生错误: {e}") + + def on_game_directories_identified(self, game_dirs): + """当游戏目录识别完成后的回调. + + Args: + game_dirs: 识别到的游戏目录 + """ + self.main_window.hide_loading_dialog() + self.main_window.setEnabled(True) + self.main_window.patch_detector.on_offline_pre_hash_finished(updated_status, game_dirs) \ No newline at end of file diff --git a/source/core/managers/offline_mode_manager.py b/source/core/managers/offline_mode_manager.py index 6415dbd..3d6020a 100644 --- a/source/core/managers/offline_mode_manager.py +++ b/source/core/managers/offline_mode_manager.py @@ -65,8 +65,43 @@ class OfflineModeManager: dict: 找到的补丁文件 {补丁名称: 文件路径} """ if directory is None: - # 获取软件所在目录 - directory = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + # 获取软件所在目录 - 直接使用最简单的方式 + try: + import sys + + if getattr(sys, 'frozen', False): + # 如果是PyInstaller打包的环境,使用可执行文件所在目录 + directory = os.path.dirname(sys.executable) + else: + # 直接取当前工作目录 + directory = os.getcwd() + + # 对于开发环境的特殊处理: + # 如果当前目录路径中包含'source',则可能是在开发模式下从source目录运行 + # 尝试找到项目根目录 + if 'source' in directory: + # 尝试向上一级查找补丁文件 + parent_dir = os.path.dirname(directory) + # 看看父目录是否存在补丁文件 + potential_patches = ["vol.1.7z", "vol.2.7z", "vol.3.7z", "vol.4.7z", "after.7z"] + for patch_file in potential_patches: + if os.path.exists(os.path.join(parent_dir, patch_file)): + # 如果在父目录找到了补丁文件,使用父目录作为扫描目录 + directory = parent_dir + break + + if self._is_debug_mode(): + logger.debug(f"DEBUG: 使用目录 {directory} 扫描离线补丁文件") + current_dir = os.getcwd() + logger.debug(f"DEBUG: 当前工作目录: {current_dir}") + logger.debug(f"DEBUG: 是否为打包环境: {getattr(sys, 'frozen', False)}") + if getattr(sys, 'frozen', False): + logger.debug(f"DEBUG: 可执行文件路径: {sys.executable}") + except Exception as e: + # 如果出现异常,使用当前工作目录 + directory = os.getcwd() + if self._is_debug_mode(): + logger.debug(f"DEBUG: 路径计算出错,使用工作目录: {directory}, 错误: {e}") debug_mode = self._is_debug_mode() @@ -76,19 +111,60 @@ class OfflineModeManager: # 要查找的补丁文件名 patch_files = ["vol.1.7z", "vol.2.7z", "vol.3.7z", "vol.4.7z", "after.7z"] + # 尝试多个可能的目录位置,从指定目录开始,然后是其父目录 + search_dirs = [directory] + + # 添加可能的父目录 + current = directory + for i in range(3): # 最多向上查找3层目录 + parent = os.path.dirname(current) + if parent and parent != current: # 确保不是根目录 + search_dirs.append(parent) + current = parent + else: + break + + # 添加工作目录(如果与指定目录不同) + work_dir = os.getcwd() + if work_dir not in search_dirs: + search_dirs.append(work_dir) + + if debug_mode: + logger.debug(f"DEBUG: 将在以下目录中查找补丁文件: {search_dirs}") + 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 - # 无论是否为调试模式,都记录找到的补丁文件 - logger.info(f"找到离线补丁文件: {patch_name} 路径: {file_path}") + # 扫描所有可能的目录查找补丁文件 + try: + # 首先尝试在指定目录查找 + for search_dir in search_dirs: + if debug_mode: + logger.debug(f"DEBUG: 正在搜索目录: {search_dir}") + + if not os.path.exists(search_dir): if debug_mode: - logger.debug(f"DEBUG: 找到离线补丁文件: {patch_name} 路径: {file_path}") + logger.debug(f"DEBUG: 目录不存在,跳过: {search_dir}") + continue + + for file in os.listdir(search_dir): + if file.lower() in patch_files: + file_path = os.path.join(search_dir, file) + if os.path.isfile(file_path): + patch_name = file.lower() + found_patches[patch_name] = file_path + # 无论是否为调试模式,都记录找到的补丁文件 + logger.info(f"找到离线补丁文件: {patch_name} 路径: {file_path}") + if debug_mode: + logger.debug(f"DEBUG: 找到离线补丁文件: {patch_name} 路径: {file_path}") + + # 如果在当前目录找到了补丁文件,停止继续查找 + if found_patches: + if debug_mode: + logger.debug(f"DEBUG: 在目录 {search_dir} 找到补丁文件,停止继续搜索") + break + + except Exception as e: + logger.error(f"扫描目录时出错: {str(e)}") self.offline_patches = found_patches @@ -745,59 +821,54 @@ class OfflineModeManager: # 添加到安装任务列表 install_tasks.append((patch_file, game_folder, game_version, _7z_path, plugin_path)) - + + # 检查是否有未找到离线补丁文件的游戏 + if self.missing_offline_patches: + if debug_mode: + logger.debug(f"DEBUG: 有未找到离线补丁文件的游戏: {self.missing_offline_patches}") + + # 询问用户是否切换到在线模式 + msg_box = msgbox_frame( + f"离线安装信息 - {self.app_name}", + f"\n本地未发现对应离线文件,是否切换为在线模式安装?\n\n以下游戏未找到对应的离线补丁文件:\n\n{chr(10).join(self.missing_offline_patches)}\n", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No + ) + result = msg_box.exec() + + if result == QMessageBox.StandardButton.Yes: + if debug_mode: + logger.debug("DEBUG: 用户选择切换到在线模式") + + # 切换到在线模式 + if hasattr(self.main_window, 'ui_manager'): + self.main_window.ui_manager.switch_work_mode("online") + + # 直接启动下载流程 + self.main_window.setEnabled(True) + # 保存当前选择的游戏列表,以便在线模式使用 + missing_games = self.missing_offline_patches.copy() + # 启动下载流程 + QTimer.singleShot(500, lambda: self._start_online_download(missing_games)) + return True + # 开始执行第一个安装任务 if install_tasks: if debug_mode: logger.info(f"DEBUG: 开始离线安装流程,安装游戏数量: {len(install_tasks)}") self.process_next_offline_install_task(install_tasks) + return True else: if debug_mode: logger.warning("DEBUG: 没有可安装的游戏,安装流程结束") - - # 检查是否有未找到离线补丁文件的游戏 - if self.missing_offline_patches: - if debug_mode: - logger.debug(f"DEBUG: 有未找到离线补丁文件的游戏: {self.missing_offline_patches}") - # 询问用户是否切换到在线模式 - msg_box = msgbox_frame( - f"离线安装信息 - {self.app_name}", - f"\n本地未发现对应离线文件,是否切换为在线模式安装?\n\n以下游戏未找到对应的离线补丁文件:\n\n{chr(10).join(self.missing_offline_patches)}\n", - QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No - ) - result = msg_box.exec() - - if result == QMessageBox.StandardButton.Yes: - if debug_mode: - logger.debug("DEBUG: 用户选择切换到在线模式") - - # 切换到在线模式 - if hasattr(self.main_window, 'ui_manager'): - self.main_window.ui_manager.switch_work_mode("online") - - # 直接启动下载流程 - self.main_window.setEnabled(True) - # 保存当前选择的游戏列表,以便在线模式使用 - missing_games = self.missing_offline_patches.copy() - # 启动下载流程 - QTimer.singleShot(500, lambda: self._start_online_download(missing_games)) - else: - if debug_mode: - logger.debug("DEBUG: 用户选择不切换到在线模式") - - # 恢复UI状态 - self.main_window.setEnabled(True) - self.main_window.ui.start_install_text.setText("开始安装") - else: - # 没有缺少离线补丁的游戏,显示一般消息 - msgbox_frame( - f"离线安装信息 - {self.app_name}", - "\n没有可安装的游戏或未找到对应的离线补丁文件。\n", - QMessageBox.StandardButton.Ok - ).exec() - self.main_window.setEnabled(True) - self.main_window.ui.start_install_text.setText("开始安装") + # 如果没有找到任何可安装的游戏,显示一般消息 + msgbox_frame( + f"离线安装信息 - {self.app_name}", + "\n没有可安装的游戏或未找到对应的离线补丁文件。\n", + QMessageBox.StandardButton.Ok + ).exec() + self.main_window.setEnabled(True) + self.main_window.ui.start_install_text.setText("开始安装") return True diff --git a/source/core/managers/ui_manager.py b/source/core/managers/ui_manager.py index 25f64ee..e37acc7 100644 --- a/source/core/managers/ui_manager.py +++ b/source/core/managers/ui_manager.py @@ -1,13 +1,17 @@ from PySide6.QtGui import QIcon, QAction, QFont, QCursor, QActionGroup -from PySide6.QtWidgets import QMessageBox, QMainWindow, QMenu, QPushButton +from PySide6.QtWidgets import QMessageBox, QMainWindow, QMenu, QPushButton, QDialog, QVBoxLayout, QProgressBar, QLabel from PySide6.QtCore import Qt, QRect import webbrowser import os +import logging +import traceback from utils import load_base64_image, msgbox_frame, resource_path from config.config import APP_NAME, APP_VERSION, LOG_FILE from core.managers.ipv6_manager import IPv6Manager # 导入新的IPv6Manager类 +logger = logging.getLogger(__name__) + class UIManager: def __init__(self, main_window): """初始化UI管理器 @@ -24,6 +28,7 @@ class UIManager: self.privacy_menu = None # 隐私协议菜单 self.about_menu = None # 关于菜单 self.about_btn = None # 关于按钮 + self.loading_dialog = None # 添加loading_dialog实例变量 # 获取主窗口的IPv6Manager实例 self.ipv6_manager = getattr(main_window, 'ipv6_manager', None) @@ -246,13 +251,53 @@ class UIManager: try: from PySide6.QtGui import QFontDatabase + from utils import resource_path - # 尝试加载字体 - font_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "fonts", "SmileySans-Oblique.ttf") + # 使用resource_path查找字体文件 + font_path = resource_path(os.path.join("assets", "fonts", "SmileySans-Oblique.ttf")) + + # 详细记录字体加载过程 if os.path.exists(font_path): + logger.info(f"尝试加载字体文件: {font_path}") font_id = QFontDatabase.addApplicationFont(font_path) + if font_id != -1: - font_family = QFontDatabase.applicationFontFamilies(font_id)[0] + font_families = QFontDatabase.applicationFontFamilies(font_id) + if font_families: + font_family = font_families[0] + logger.info(f"成功加载字体: {font_family} 从 {font_path}") + else: + logger.warning(f"字体加载成功但无法获取字体族: {font_path}") + else: + logger.warning(f"字体加载失败: {font_path} (返回ID: {font_id})") + + # 检查文件大小和是否可读 + try: + file_size = os.path.getsize(font_path) + logger.debug(f"字体文件大小: {file_size} 字节") + if file_size == 0: + logger.error(f"字体文件大小为0字节: {font_path}") + + # 尝试打开文件测试可读性 + with open(font_path, 'rb') as f: + # 只读取前几个字节测试可访问性 + f.read(10) + logger.debug(f"字体文件可以正常打开和读取") + except Exception as file_error: + logger.error(f"字体文件访问错误: {file_error}") + else: + logger.error(f"找不到字体文件: {font_path}") + + # 尝试列出assets/fonts目录下的文件 + try: + fonts_dir = os.path.dirname(font_path) + if os.path.exists(fonts_dir): + files = os.listdir(fonts_dir) + logger.debug(f"字体目录 {fonts_dir} 中的文件: {files}") + else: + logger.debug(f"字体目录不存在: {fonts_dir}") + except Exception as dir_error: + logger.error(f"无法列出字体目录内容: {dir_error}") # 创建菜单字体 menu_font = QFont(font_family, 14) @@ -260,7 +305,8 @@ class UIManager: return menu_font except Exception as e: - print(f"加载字体失败: {e}") + logger.error(f"加载字体过程中发生异常: {e}") + logger.error(f"异常详情: {traceback.format_exc()}") # 返回默认字体 menu_font = QFont(font_family, 14) menu_font.setBold(True) @@ -968,4 +1014,62 @@ class UIManager: "模式已切换", "\n已切换到在线模式。\n\n将从网络下载补丁进行安装。\n" ) - msg_box.exec() \ No newline at end of file + msg_box.exec() + + def create_progress_window(self, title, initial_text="准备中..."): + """创建并返回一个通用的进度窗口. + + Args: + title (str): 窗口标题. + initial_text (str): 初始状态文本. + + Returns: + QDialog: 配置好的进度窗口实例. + """ + progress_window = QDialog(self.main_window) + progress_window.setWindowTitle(f"{title} - {APP_NAME}") + progress_window.setFixedSize(400, 150) + + layout = QVBoxLayout() + + progress_bar = QProgressBar() + progress_bar.setRange(0, 100) + progress_bar.setValue(0) + layout.addWidget(progress_bar) + + status_label = QLabel(initial_text) + layout.addWidget(status_label) + + progress_window.setLayout(layout) + # 将控件附加到窗口对象上,以便外部访问 + progress_window.progress_bar = progress_bar + progress_window.status_label = status_label + + return progress_window + + def show_loading_dialog(self, message): + """显示或更新加载对话框.""" + if not self.loading_dialog: + self.loading_dialog = QDialog(self.main_window) + self.loading_dialog.setWindowTitle(f"请稍候 - {APP_NAME}") + self.loading_dialog.setFixedSize(300, 100) + self.loading_dialog.setModal(True) + layout = QVBoxLayout() + loading_label = QLabel(message) + loading_label.setAlignment(Qt.AlignCenter) + layout.addWidget(loading_label) + self.loading_dialog.setLayout(layout) + # 将label附加到dialog,方便后续更新 + self.loading_dialog.loading_label = loading_label + else: + self.loading_dialog.loading_label.setText(message) + + self.loading_dialog.show() + # force UI update + QMessageBox.QApplication.processEvents() + + def hide_loading_dialog(self): + """隐藏并销毁加载对话框.""" + if self.loading_dialog: + self.loading_dialog.hide() + self.loading_dialog = None \ No newline at end of file diff --git a/source/core/managers/window_manager.py b/source/core/managers/window_manager.py index ae55bc9..d754877 100644 --- a/source/core/managers/window_manager.py +++ b/source/core/managers/window_manager.py @@ -23,7 +23,58 @@ class WindowManager: # 设置圆角窗口 self.setRoundedCorners() - + + # 初始化状态管理 + self._setup_window_state() + + def _setup_window_state(self): + """初始化窗口状态管理.""" + self.STATE_INITIALIZING = "initializing" + self.STATE_READY = "ready" + self.STATE_DOWNLOADING = "downloading" + self.STATE_EXTRACTING = "extracting" + self.STATE_VERIFYING = "verifying" + self.STATE_INSTALLING = "installing" + self.STATE_COMPLETED = "completed" + self.STATE_ERROR = "error" + + self.current_state = self.STATE_INITIALIZING + + def change_window_state(self, new_state, error_message=None): + """更改窗口状态并更新UI. + + Args: + new_state (str): 新的状态. + error_message (str, optional): 错误信息. Defaults to None. + """ + if new_state == self.current_state: + return + + self.current_state = new_state + self._update_ui_for_state(new_state, error_message) + + def _update_ui_for_state(self, state, error_message=None): + """根据当前状态更新UI组件.""" + is_offline = self.window.offline_mode_manager.is_in_offline_mode() + config_valid = self.window.config_valid + + button_enabled = False + button_text = "!无法安装!" + + if state == self.STATE_READY: + if is_offline or config_valid: + button_enabled = True + button_text = "开始安装" + elif state in [self.STATE_DOWNLOADING, self.STATE_EXTRACTING, self.STATE_VERIFYING, self.STATE_INSTALLING]: + button_text = "正在安装" + elif state == self.STATE_COMPLETED: + button_enabled = True + button_text = "安装完成" # Or back to "开始安装" + + self.window.ui.start_install_btn.setEnabled(button_enabled) + self.window.ui.start_install_text.setText(button_text) + self.window.install_button_enabled = button_enabled + def setRoundedCorners(self): """设置窗口圆角""" # 实现圆角窗口 diff --git a/source/main_window.py b/source/main_window.py index 1edf7ae..04c2183 100644 --- a/source/main_window.py +++ b/source/main_window.py @@ -4,13 +4,14 @@ import subprocess import shutil import json import webbrowser +import traceback from PySide6 import QtWidgets from PySide6.QtCore import QTimer, Qt, QPoint, QRect, QSize from PySide6.QtWidgets import QMainWindow, QMessageBox, QGraphicsOpacityEffect, QGraphicsColorizeEffect, QDialog, QVBoxLayout, QProgressBar, QLabel from PySide6.QtGui import QPalette, QColor, QPainterPath, QRegion, QFont from PySide6.QtGui import QAction # Added for menu actions -from PySide6.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QProgressBar, QLabel # Added for progress window +# Removed QDialog, QVBoxLayout, QProgressBar, QLabel from here as they are managed by UIManager from ui.Ui_install import Ui_MainWindows from config.config import ( @@ -22,8 +23,8 @@ from utils import ( load_config, save_config, HashManager, AdminPrivileges, msgbox_frame, load_image_from_file ) from workers import ( - DownloadThread, ProgressWindow, IpOptimizerThread, - HashThread, ExtractionThread, ConfigFetchThread + IpOptimizerThread, + HashThread, ExtractionThread, ConfigFetchThread, DownloadThread ) from core import ( MultiStageAnimations, UIManager, DownloadManager, DebugManager, @@ -41,166 +42,251 @@ class MainWindow(QMainWindow): def __init__(self): super().__init__() - # 设置窗口为无边框 - self.setWindowFlags(Qt.WindowType.FramelessWindowHint) - # 设置窗口背景透明 - self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) + self._setup_window_properties() + self._init_ui() + self._init_config_and_tools() + self._init_managers() + self._connect_signals() + self._setup_environment() - # 调整窗口大小以适应背景图片比例 (1280x720) + self.download_manager.hosts_manager.backup() + self._setup_debug_mode() + + self.check_and_set_offline_mode() + self.fetch_cloud_config() + self.start_animations() + + def _setup_window_properties(self): + """设置窗口的基本属性,如无边框、透明背景和大小.""" + self.setWindowFlags(Qt.WindowType.FramelessWindowHint) + self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) self.resize(1280, 720) - # 设置固定尺寸范围 self.setMinimumSize(QSize(1024, 576)) self.setMaximumSize(QSize(1280, 720)) - - # 初始化UI (从Ui_install.py导入) + + def _init_ui(self): + """初始化UI组件.""" self.ui = Ui_MainWindows() self.ui.setupUi(self) - - # 初始化配置 + + def _init_config_and_tools(self): + """加载配置并初始化核心工具.""" self.config = load_config() - - # 初始化工具类 self.hash_manager = HashManager(BLOCK_SIZE) self.admin_privileges = AdminPrivileges() - - # 初始化哈希校验窗口引用 - self.hash_msg_box = None - self.loading_dialog = None self.patch_detector = PatchDetector(self) - # 初始化各种管理器 - 调整初始化顺序,避免循环依赖 - # 1. 首先创建必要的基础管理器 - self.animator = MultiStageAnimations(self.ui, self) - self.window_manager = WindowManager(self) - self.debug_manager = DebugManager(self) - - # 2. 初始化IPv6Manager(应在UIManager之前) - self.ipv6_manager = IPv6Manager(self) - - # 3. 创建UIManager(依赖IPv6Manager) - self.ui_manager = UIManager(self) - - # 4. 为debug_manager设置ui_manager引用 - self.debug_manager.set_ui_manager(self.ui_manager) - - # 5. 初始化其他管理器 - self.config_manager = ConfigManager(APP_NAME, CONFIG_URL, UA, self.debug_manager) - self.game_detector = GameDetector(GAME_INFO, self.debug_manager) - self.patch_manager = PatchManager(APP_NAME, GAME_INFO, self.debug_manager, self) - - # 6. 初始化补丁检测模块 - self.patch_detector = PatchDetector(self) - - # 7. 设置补丁检测器到补丁管理器 - self.patch_manager.set_patch_detector(self.patch_detector) - - # 8. 初始化离线模式管理器 - from core.offline_mode_manager import OfflineModeManager - self.offline_mode_manager = OfflineModeManager(self) - - # 9. 初始化下载管理器 - 放在最后,因为它可能依赖于其他管理器 - self.download_manager = DownloadManager(self) - - # 10. 初始化功能处理程序 - self.uninstall_handler = UninstallHandler(self) - self.patch_toggle_handler = PatchToggleHandler(self) - - # 加载用户下载线程设置 - if "download_thread_level" in self.config and self.config["download_thread_level"] in DOWNLOAD_THREADS: - self.download_manager.download_thread_level = self.config["download_thread_level"] - # 初始化状态变量 self.cloud_config = None - self.config_valid = False # 添加配置有效标志 - self.patch_manager.initialize_status() - self.installed_status = self.patch_manager.get_status() # 获取初始化后的状态 - self.last_error_message = "" # 添加错误信息记录 - self.version_warning = False # 添加版本警告标志 - self.install_button_enabled = True # 默认启用安装按钮 + self.config_valid = False + self.last_error_message = "" + self.version_warning = False + self.install_button_enabled = True self.progress_window = None - # 线程持有引用,避免 QThread 在运行中被销毁 self.pre_hash_thread = None - self.hash_thread = None # after 校验线程引用(由 PatchDetector 赋值) + self.hash_thread = None - # 设置关闭按钮事件连接 + # 验证关键资源 + self._verify_resources() + + def _verify_resources(self): + """验证关键资源文件是否存在,帮助调试资源加载问题""" + from utils import resource_path + logger.info("开始验证关键资源文件...") + + # 关键字体文件 + font_files = [ + os.path.join("assets", "fonts", "SmileySans-Oblique.ttf") + ] + + # 关键图标文件 + icon_files = [ + os.path.join("assets", "images", "ICO", "icon.png"), + os.path.join("assets", "images", "ICO", "icon.ico"), + os.path.join("assets", "images", "BTN", "Button.png"), + os.path.join("assets", "images", "BTN", "exit.bmp"), + os.path.join("assets", "images", "BTN", "start_install.bmp") + ] + + # 关键背景图片 + bg_files = [ + os.path.join("assets", "images", "BG", "bg1.jpg"), + os.path.join("assets", "images", "BG", "bg2.jpg"), + os.path.join("assets", "images", "BG", "bg3.jpg"), + os.path.join("assets", "images", "BG", "bg4.jpg"), + os.path.join("assets", "images", "BG", "menubg.jpg") + ] + + # 记录缺失的资源 + missing_resources = [] + + # 验证字体文件 + for font_file in font_files: + path = resource_path(font_file) + if not os.path.exists(path): + missing_resources.append(font_file) + logger.warning(f"缺失字体文件: {font_file}, 尝试路径: {path}") + else: + logger.info(f"已找到字体文件: {font_file}") + + # 验证图标文件 + for icon_file in icon_files: + path = resource_path(icon_file) + if not os.path.exists(path): + missing_resources.append(icon_file) + logger.warning(f"缺失图标文件: {icon_file}, 尝试路径: {path}") + else: + logger.info(f"已找到图标文件: {icon_file}") + + # 验证背景图片 + for bg_file in bg_files: + path = resource_path(bg_file) + if not os.path.exists(path): + missing_resources.append(bg_file) + logger.warning(f"缺失背景图片: {bg_file}, 尝试路径: {path}") + else: + logger.info(f"已找到背景图片: {bg_file}") + + # 如果有缺失资源,记录摘要 + if missing_resources: + logger.error(f"总计 {len(missing_resources)} 个关键资源文件缺失!") + # 如果在调试模式下,显示警告对话框 + if self.config.get("debug_mode", False): + from PySide6.QtWidgets import QMessageBox + msg = QMessageBox() + msg.setIcon(QMessageBox.Warning) + msg.setWindowTitle("资源加载警告") + msg.setText("检测到缺失的关键资源文件!") + msg.setInformativeText(f"有 {len(missing_resources)} 个资源文件未找到。\n请检查日志文件获取详细信息。") + msg.setStandardButtons(QMessageBox.Ok) + msg.exec() + else: + logger.info("所有关键资源文件验证通过") + + # 测试资源加载功能 + self._test_resource_loading() + + def _test_resource_loading(self): + """测试资源加载功能,验证图片和字体是否可以正确加载""" + logger.info("开始测试资源加载功能...") + + from PySide6.QtGui import QFontDatabase, QPixmap, QImage, QFont + from utils import resource_path, load_image_from_file + + # 测试字体加载 + logger.info("测试字体加载...") + font_path = resource_path(os.path.join("assets", "fonts", "SmileySans-Oblique.ttf")) + if os.path.exists(font_path): + font_id = QFontDatabase.addApplicationFont(font_path) + if font_id != -1: + font_families = QFontDatabase.applicationFontFamilies(font_id) + if font_families: + logger.info(f"字体加载测试成功: {font_families[0]}") + else: + logger.error("字体加载测试失败: 无法获取字体族") + else: + logger.error(f"字体加载测试失败: 无法加载字体文件 {font_path}") + else: + logger.error(f"字体加载测试失败: 文件不存在 {font_path}") + + # 测试图片加载 - 使用QPixmap直接加载 + logger.info("测试图片加载 (QPixmap)...") + icon_path = resource_path(os.path.join("assets", "images", "ICO", "icon.png")) + if os.path.exists(icon_path): + pixmap = QPixmap(icon_path) + if not pixmap.isNull(): + logger.info(f"QPixmap加载测试成功: {icon_path}, 大小: {pixmap.width()}x{pixmap.height()}") + else: + logger.error(f"QPixmap加载测试失败: {icon_path}") + else: + logger.error(f"QPixmap加载测试失败: 文件不存在 {icon_path}") + + # 测试图片加载 - 使用QImage加载 + logger.info("测试图片加载 (QImage)...") + bg_path = resource_path(os.path.join("assets", "images", "BG", "bg1.jpg")) + if os.path.exists(bg_path): + image = QImage(bg_path) + if not image.isNull(): + logger.info(f"QImage加载测试成功: {bg_path}, 大小: {image.width()}x{image.height()}") + else: + logger.error(f"QImage加载测试失败: {bg_path}") + else: + logger.error(f"QImage加载测试失败: 文件不存在 {bg_path}") + + # 测试自定义加载函数 + logger.info("测试自定义图片加载函数...") + btn_path = resource_path(os.path.join("assets", "images", "BTN", "Button.png")) + pixmap = load_image_from_file(btn_path) + if not pixmap.isNull(): + logger.info(f"自定义图片加载函数测试成功: {btn_path}, 大小: {pixmap.width()}x{pixmap.height()}") + else: + logger.error(f"自定义图片加载函数测试失败: {btn_path}") + + logger.info("资源加载功能测试完成") + + def _init_managers(self): + """初始化所有管理器.""" + self.animator = MultiStageAnimations(self.ui, self) + self.window_manager = WindowManager(self) + self.debug_manager = DebugManager(self) + self.ipv6_manager = IPv6Manager(self) + self.ui_manager = UIManager(self) + self.debug_manager.set_ui_manager(self.ui_manager) + self.config_manager = ConfigManager(APP_NAME, CONFIG_URL, UA, self.debug_manager) + self.game_detector = GameDetector(GAME_INFO, self.debug_manager) + self.patch_manager = PatchManager(APP_NAME, GAME_INFO, self.debug_manager, self) + self.patch_manager.set_patch_detector(self.patch_detector) + from core.managers.offline_mode_manager import OfflineModeManager + self.offline_mode_manager = OfflineModeManager(self) + self.download_manager = DownloadManager(self) + self.uninstall_handler = UninstallHandler(self) + self.patch_toggle_handler = PatchToggleHandler(self) + + # Load user's download thread setting + if "download_thread_level" in self.config and self.config["download_thread_level"] in DOWNLOAD_THREADS: + self.download_manager.download_thread_level = self.config["download_thread_level"] + + def _connect_signals(self): + """连接UI组件的信号到相应的槽函数.""" if hasattr(self.ui, 'close_btn'): self.ui.close_btn.clicked.connect(self.close) - if hasattr(self.ui, 'minimize_btn'): self.ui.minimize_btn.clicked.connect(self.showMinimized) - # 创建缓存目录 + self.ui.start_install_btn.clicked.connect(self.handle_install_button_click) + self.ui.uninstall_btn.clicked.connect(self.uninstall_handler.handle_uninstall_button_click) + self.ui.toggle_patch_btn.clicked.connect(self.patch_toggle_handler.handle_toggle_patch_button_click) + self.ui.exit_btn.clicked.connect(self.shutdown_app) + + def _setup_environment(self): + """准备应用运行所需的环境,如创建缓存目录和检查权限.""" if not os.path.exists(PLUGIN): try: os.makedirs(PLUGIN) except OSError as e: - QtWidgets.QMessageBox.critical( - self, - f"错误 - {APP_NAME}", - f"\n无法创建缓存位置\n\n使用管理员身份运行或检查文件读写权限\n\n【错误信息】:{e}\n", - ) + QtWidgets.QMessageBox.critical(self, f"错误 - {APP_NAME}", f"无法创建缓存位置: {e}") sys.exit(1) - # 连接信号 - 绑定到新按钮 - self.ui.start_install_btn.clicked.connect(self.handle_install_button_click) - self.ui.uninstall_btn.clicked.connect(self.uninstall_handler.handle_uninstall_button_click) # 使用卸载处理程序 - self.ui.toggle_patch_btn.clicked.connect(self.patch_toggle_handler.handle_toggle_patch_button_click) # 使用补丁切换处理程序 - self.ui.exit_btn.clicked.connect(self.shutdown_app) - - # 初始化按钮状态标记 - self.install_button_enabled = False - self.last_error_message = "" - - # 检查管理员权限和进程 try: - # 检查管理员权限 self.admin_privileges.request_admin_privileges() - # 检查并终止相关进程 self.admin_privileges.check_and_terminate_processes() - except KeyboardInterrupt: - logger.warning("权限检查或进程检查被用户中断") - QtWidgets.QMessageBox.warning( - self, - f"警告 - {APP_NAME}", - "\n操作被中断,请重新启动应用。\n" - ) - sys.exit(1) except Exception as e: - logger.error(f"权限检查或进程检查时发生错误: {e}") - QtWidgets.QMessageBox.critical( - self, - f"错误 - {APP_NAME}", - f"\n权限检查或进程检查时发生错误,请重新启动应用。\n\n【错误信息】:{e}\n" - ) + logger.error(f"权限或进程检查失败: {e}") + QtWidgets.QMessageBox.critical(self, f"错误 - {APP_NAME}", f"权限检查失败: {e}") sys.exit(1) - - # 备份hosts文件 - self.download_manager.hosts_manager.backup() - - # 根据初始配置决定是否开启Debug模式 - if "debug_mode" in self.config and self.config["debug_mode"]: - # 先启用日志系统 + + def _setup_debug_mode(self): + """根据配置设置调试模式.""" + if self.config.get("debug_mode"): self.debug_manager.start_logging() logger.info("通过配置启动调试模式") - # 检查UI设置 - if hasattr(self.ui_manager, 'debug_action') and self.ui_manager.debug_action: - if self.ui_manager.debug_action.isChecked(): - # 如果通过UI启用了调试模式,确保日志系统已启动 - if not self.debug_manager.logger: - self.debug_manager.start_logging() - logger.info("通过UI启动调试模式") - # 设置UI,包括窗口图标和菜单 + if hasattr(self.ui_manager, 'debug_action') and self.ui_manager.debug_action and self.ui_manager.debug_action.isChecked(): + if not self.debug_manager.logger: + self.debug_manager.start_logging() + logger.info("通过UI启动调试模式") + self.ui_manager.setup_ui() - - # 检查是否有离线补丁文件,如果有则自动切换到离线模式 - self.check_and_set_offline_mode() - - # 获取云端配置 - self.fetch_cloud_config() - - # 启动动画 - self.start_animations() # 窗口事件处理 - 委托给WindowManager def mousePressEvent(self, event): @@ -334,110 +420,19 @@ class MainWindow(QMainWindow): """保存配置的便捷方法""" self.config_manager.save_config(config) - def create_download_thread(self, url, _7z_path, game_version): - """创建下载线程 - - Args: - url: 下载URL - _7z_path: 7z文件保存路径 - game_version: 游戏版本 - - Returns: - DownloadThread: 下载线程实例 - """ - return DownloadThread(url, _7z_path, game_version, self) - - def create_progress_window(self): - """创建进度窗口 - - Returns: - QDialog: 进度窗口实例 - """ - progress_window = QDialog(self) - progress_window.setWindowTitle(f"下载进度 - {APP_NAME}") - progress_window.setFixedSize(400, 150) - - layout = QVBoxLayout() - - # 添加进度条 - progress_bar = QProgressBar() - progress_bar.setRange(0, 100) - progress_bar.setValue(0) - layout.addWidget(progress_bar) - - # 添加标签 - status_label = QLabel("准备下载...") - layout.addWidget(status_label) - - progress_window.setLayout(layout) - progress_window.progress_bar = progress_bar - progress_window.status_label = status_label - - return progress_window - - def create_extraction_progress_window(self): - """创建解压进度窗口 - - Returns: - QDialog: 解压进度窗口实例 - """ - progress_window = QDialog(self) - progress_window.setWindowTitle(f"解压进度 - {APP_NAME}") - progress_window.setFixedSize(400, 150) - - layout = QVBoxLayout() - - # 添加进度条 - progress_bar = QProgressBar() - progress_bar.setRange(0, 100) - progress_bar.setValue(0) - layout.addWidget(progress_bar) - - # 添加标签 - status_label = QLabel("准备解压...") - layout.addWidget(status_label) - - progress_window.setLayout(layout) - progress_window.progress_bar = progress_bar - progress_window.status_label = status_label - - return progress_window - - def create_extraction_thread(self, _7z_path, game_folder, plugin_path, game_version, extracted_path=None): - """创建解压线程 - - Args: - _7z_path: 7z文件路径 - game_folder: 游戏文件夹路径 - plugin_path: 插件路径 - game_version: 游戏版本 - extracted_path: 已解压的补丁文件路径,如果提供则直接使用它而不进行解压 - - Returns: - ExtractionThread: 解压线程实例 - """ - return ExtractionThread(_7z_path, game_folder, plugin_path, game_version, self, extracted_path) - - def create_hash_thread(self, mode, install_paths, plugin_hash=None, installed_status=None): - """创建哈希校验线程 - - Args: - mode: 校验模式,"pre"或"after" - install_paths: 安装路径字典 - plugin_hash: 插件哈希值字典,如果为None则使用self.plugin_hash - installed_status: 安装状态字典,如果为None则使用self.installed_status - - Returns: - HashThread: 哈希校验线程实例 - """ - if plugin_hash is None: - plugin_hash = PLUGIN_HASH - - if installed_status is None: - installed_status = self.installed_status - - return HashThread(mode, install_paths, plugin_hash, installed_status, self) - + # Remove create_progress_window, create_extraction_progress_window, show_loading_dialog, hide_loading_dialog + # These are now handled by UIManager + # def create_progress_window(self): ... + # def create_extraction_progress_window(self): ... + # def show_loading_dialog(self, message): ... + # def hide_loading_dialog(self): ... + + # Remove create_download_thread, create_extraction_thread, create_hash_thread + # These are now handled by their respective managers or a new ThreadManager if we create one + # def create_download_thread(self, ...): ... + # def create_extraction_thread(self, ...): ... + # def create_hash_thread(self, ...): ... + def show_result(self): """显示安装结果,调用patch_manager的show_result方法""" self.patch_manager.show_result() @@ -451,104 +446,35 @@ class MainWindow(QMainWindow): self.shutdown_app(event) def shutdown_app(self, event=None, force_exit=False): - """关闭应用程序 - - Args: - event: 关闭事件,如果是从closeEvent调用的 - force_exit: 是否强制退出 - """ - # 检查是否有动画或任务正在进行 + """关闭应用程序""" if hasattr(self, 'animation_in_progress') and self.animation_in_progress and not force_exit: - # 如果动画正在进行,阻止退出 if event: event.ignore() return - - # 检查是否有下载任务正在进行 - if hasattr(self.download_manager, 'current_download_thread') and \ - self.download_manager.current_download_thread and \ - self.download_manager.current_download_thread.isRunning() and not force_exit: - # 询问用户是否确认退出 - reply = QMessageBox.question( - self, - f"确认退出 - {APP_NAME}", - "\n下载任务正在进行中,确定要退出吗?\n", - QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, - QMessageBox.StandardButton.No - ) - if reply == QMessageBox.StandardButton.No: - if event: - event.ignore() - return - - # 在退出前优雅地清理后台线程,避免 QThread 在运行中被销毁 - def _graceful_stop(thread_obj, name="thread", timeout_ms=2000): - try: - if thread_obj and hasattr(thread_obj, 'isRunning') and thread_obj.isRunning(): - # 首选等待自然结束 - if hasattr(thread_obj, 'requestInterruption'): - try: - thread_obj.requestInterruption() - except Exception: - pass - thread_obj.wait(timeout_ms) - # 仍未退出时,最后手段终止 - if thread_obj.isRunning(): - try: - thread_obj.terminate() - except Exception: - pass - thread_obj.wait(1000) - except Exception: - pass - # 清理主窗口直接持有的线程 - _graceful_stop(getattr(self, 'pre_hash_thread', None), 'pre_hash_thread') - _graceful_stop(getattr(self, 'hash_thread', None), 'hash_thread') + threads_to_stop = { + 'pre_hash': getattr(self, 'pre_hash_thread', None), + 'hash': getattr(self, 'hash_thread', None), + 'offline_hash': getattr(self.offline_mode_manager, 'hash_thread', None), + 'extraction': getattr(self.offline_mode_manager, 'extraction_thread', None), + 'config_fetch': getattr(self.config_manager, 'config_fetch_thread', None), + 'game_detector': getattr(self.game_detector, 'detection_thread', None), + 'patch_check': getattr(self.patch_detector, 'patch_check_thread', None) + } + + # Add current download thread if it's running + if hasattr(self.download_manager, 'current_download_thread') and self.download_manager.current_download_thread: + threads_to_stop['download'] = self.download_manager.current_download_thread - # 清理离线管理器中的线程 - try: - if hasattr(self, 'offline_mode_manager') and self.offline_mode_manager: - _graceful_stop(getattr(self.offline_mode_manager, 'hash_thread', None), 'offline_hash_thread') - _graceful_stop(getattr(self.offline_mode_manager, 'extraction_thread', None), 'extraction_thread') - except Exception: - pass + self.download_manager.graceful_stop_threads(threads_to_stop) - # 清理配置获取线程 - try: - if hasattr(self, 'config_manager') and hasattr(self.config_manager, 'config_fetch_thread'): - _graceful_stop(self.config_manager.config_fetch_thread, 'config_fetch_thread', 1000) - except Exception: - pass - - # 清理游戏识别线程 - try: - if hasattr(self, 'game_detector') and hasattr(self.game_detector, 'detection_thread'): - _graceful_stop(self.game_detector.detection_thread, 'detection_thread', 1000) - except Exception: - pass - - # 清理补丁检查线程 - try: - if hasattr(self, 'patch_detector') and hasattr(self.patch_detector, 'patch_check_thread'): - _graceful_stop(self.patch_detector.patch_check_thread, 'patch_check_thread', 1000) - except Exception: - pass - - # 恢复hosts文件(如果未禁用自动还原) self.download_manager.hosts_manager.restore() - - # 额外检查并清理hosts文件中的残留记录(如果未禁用自动还原) self.download_manager.hosts_manager.check_and_clean_all_entries() - - # 停止日志记录 self.debug_manager.stop_logging() if not force_exit: reply = QMessageBox.question( - self, - f"确认退出 - {APP_NAME}", - "\n确定要退出吗?\n", + self, f"确认退出 - {APP_NAME}", "\n确定要退出吗?\n", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No ) @@ -557,7 +483,6 @@ class MainWindow(QMainWindow): event.ignore() return - # 退出应用程序 if event: event.accept() else: @@ -635,7 +560,7 @@ class MainWindow(QMainWindow): return self.download_manager.selected_folder = self.selected_folder - self.show_loading_dialog("正在识别游戏目录...") + self.ui_manager.show_loading_dialog("正在识别游戏目录...") self.setEnabled(False) # 异步识别游戏目录 @@ -654,33 +579,8 @@ class MainWindow(QMainWindow): # 版本正常,使用原有的下载流程 self.download_manager.file_dialog() - def show_loading_dialog(self, message): - """显示加载对话框""" - if not self.loading_dialog: - self.loading_dialog = QDialog(self) - self.loading_dialog.setWindowTitle(f"请稍候 - {APP_NAME}") - self.loading_dialog.setFixedSize(300, 100) - self.loading_dialog.setModal(True) - layout = QVBoxLayout() - self.loading_label = QLabel(message) - self.loading_label.setAlignment(Qt.AlignCenter) - layout.addWidget(self.loading_label) - self.loading_dialog.setLayout(layout) - else: - self.loading_label.setText(message) - - self.loading_dialog.show() - QtWidgets.QApplication.processEvents() - - def hide_loading_dialog(self): - """隐藏加载对话框""" - if self.loading_dialog: - self.loading_dialog.hide() - self.loading_dialog = None - def on_game_directories_identified(self, game_dirs): - """游戏目录识别完成后的回调""" - self.hide_loading_dialog() + self.ui_manager.hide_loading_dialog() if not game_dirs: self.setEnabled(True) @@ -692,7 +592,7 @@ class MainWindow(QMainWindow): ) return - self.show_loading_dialog("正在检查补丁状态...") + self.ui_manager.show_loading_dialog("正在检查补丁状态...") install_paths = self.download_manager.get_install_paths() @@ -709,8 +609,7 @@ class MainWindow(QMainWindow): self.pre_hash_thread.start() def on_pre_hash_finished(self, updated_status, game_dirs): - """哈希预检查完成后的回调""" - self.hide_loading_dialog() + self.ui_manager.hide_loading_dialog() self.setEnabled(True) self.patch_detector.on_offline_pre_hash_finished(updated_status, game_dirs) @@ -725,9 +624,32 @@ class MainWindow(QMainWindow): try: # 初始化离线模式管理器 if not hasattr(self, 'offline_mode_manager') or self.offline_mode_manager is None: - from core.offline_mode_manager import OfflineModeManager + from core.managers.offline_mode_manager import OfflineModeManager self.offline_mode_manager = OfflineModeManager(self) + # 在调试模式下记录当前执行路径 + is_debug_mode = self.config.get('debug_mode', False) if hasattr(self, 'config') else False + if is_debug_mode: + import os + import sys + current_dir = os.getcwd() + logger.debug(f"DEBUG: 当前工作目录: {current_dir}") + logger.debug(f"DEBUG: 是否为打包环境: {getattr(sys, 'frozen', False)}") + if getattr(sys, 'frozen', False): + logger.debug(f"DEBUG: 可执行文件路径: {sys.executable}") + + # 尝试列出当前目录中的文件(调试用) + try: + files = os.listdir(current_dir) + logger.debug(f"DEBUG: 当前目录文件列表: {files}") + + # 检查上级目录 + parent_dir = os.path.dirname(current_dir) + parent_files = os.listdir(parent_dir) + logger.debug(f"DEBUG: 上级目录 {parent_dir} 文件列表: {parent_files}") + except Exception as e: + logger.debug(f"DEBUG: 列出目录文件时出错: {str(e)}") + # 扫描离线补丁文件 self.offline_mode_manager.scan_for_offline_patches() @@ -774,6 +696,7 @@ class MainWindow(QMainWindow): self.set_start_button_enabled(False) logger.error(f"错误: 检查离线模式时发生异常: {e}") + logger.error(f"错误详情: {traceback.format_exc()}") return False def close_hash_msg_box(self): diff --git a/source/ui/Ui_install.py b/source/ui/Ui_install.py index ba027ef..439f92e 100644 --- a/source/ui/Ui_install.py +++ b/source/ui/Ui_install.py @@ -11,10 +11,14 @@ from PySide6.QtGui import (QAction, QBrush, QColor, QConicalGradient, from PySide6.QtWidgets import (QApplication, QLabel, QMainWindow, QMenu, QMenuBar, QPushButton, QSizePolicy, QWidget, QHBoxLayout) import os +import logging + +# 初始化日志记录器 +logger = logging.getLogger(__name__) # 导入配置常量 from config.config import APP_NAME, APP_VERSION -from utils import load_image_from_file +from utils import load_image_from_file, resource_path class Ui_MainWindows(object): def setupUi(self, MainWindows): @@ -38,8 +42,21 @@ class Ui_MainWindows(object): MainWindows.setDockNestingEnabled(False) # 加载自定义字体 - font_id = QFontDatabase.addApplicationFont(os.path.join(os.path.dirname(os.path.dirname(__file__)), "fonts", "SmileySans-Oblique.ttf")) - font_family = QFontDatabase.applicationFontFamilies(font_id)[0] if font_id != -1 else "Arial" + font_path = resource_path(os.path.join("assets", "fonts", "SmileySans-Oblique.ttf")) + logger.info(f"尝试加载字体文件: {font_path}") + font_id = QFontDatabase.addApplicationFont(font_path) + if font_id != -1: + font_families = QFontDatabase.applicationFontFamilies(font_id) + if font_families: + font_family = font_families[0] + logger.info(f"成功加载字体: {font_family} 从 {font_path}") + else: + logger.warning(f"字体加载成功但无法获取字体族: {font_path}") + font_family = "Arial" + else: + logger.error(f"字体加载失败: {font_path}") + font_family = "Arial" + self.custom_font = QFont(font_family, 16) # 创建字体对象,大小为16 self.custom_font.setWeight(QFont.Weight.Medium) # 设置为中等粗细,不要太粗 @@ -299,8 +316,11 @@ class Ui_MainWindows(object): self.loadbg.setObjectName(u"loadbg") self.loadbg.setGeometry(QRect(0, 0, 1280, 655)) # 加载背景图并允许拉伸 - bg_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "IMG", "BG", "bg1.jpg") + bg_path = resource_path(os.path.join("assets", "images", "BG", "bg1.jpg")) + logger.info(f"加载背景图: {bg_path}") bg_pixmap = QPixmap(bg_path) + if bg_pixmap.isNull(): + logger.error(f"背景图加载失败: {bg_path}") self.loadbg.setPixmap(bg_pixmap) self.loadbg.setScaledContents(True) @@ -308,7 +328,8 @@ class Ui_MainWindows(object): self.vol1bg.setObjectName(u"vol1bg") self.vol1bg.setGeometry(QRect(0, 150, 93, 64)) # 直接加载图片文件 - vol1_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "IMG", "LOGO", "vo01_logo.png") + vol1_path = resource_path(os.path.join("assets", "images", "LOGO", "vo01_logo.png")) + logger.info(f"加载LOGO图: {vol1_path}") self.vol1bg.setPixmap(QPixmap(vol1_path)) self.vol1bg.setScaledContents(True) @@ -316,7 +337,7 @@ class Ui_MainWindows(object): self.vol2bg.setObjectName(u"vol2bg") self.vol2bg.setGeometry(QRect(0, 210, 93, 64)) # 直接加载图片文件 - vol2_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "IMG", "LOGO", "vo02_logo.png") + vol2_path = resource_path(os.path.join("assets", "images", "LOGO", "vo02_logo.png")) self.vol2bg.setPixmap(QPixmap(vol2_path)) self.vol2bg.setScaledContents(True) @@ -324,7 +345,7 @@ class Ui_MainWindows(object): self.vol3bg.setObjectName(u"vol3bg") self.vol3bg.setGeometry(QRect(0, 270, 93, 64)) # 直接加载图片文件 - vol3_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "IMG", "LOGO", "vo03_logo.png") + vol3_path = resource_path(os.path.join("assets", "images", "LOGO", "vo03_logo.png")) self.vol3bg.setPixmap(QPixmap(vol3_path)) self.vol3bg.setScaledContents(True) @@ -332,7 +353,7 @@ class Ui_MainWindows(object): self.vol4bg.setObjectName(u"vol4bg") self.vol4bg.setGeometry(QRect(0, 330, 93, 64)) # 直接加载图片文件 - vol4_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "IMG", "LOGO", "vo04_logo.png") + vol4_path = resource_path(os.path.join("assets", "images", "LOGO", "vo04_logo.png")) self.vol4bg.setPixmap(QPixmap(vol4_path)) self.vol4bg.setScaledContents(True) @@ -340,7 +361,7 @@ class Ui_MainWindows(object): self.afterbg.setObjectName(u"afterbg") self.afterbg.setGeometry(QRect(0, 390, 93, 64)) # 直接加载图片文件 - after_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "IMG", "LOGO", "voaf_logo.png") + after_path = resource_path(os.path.join("assets", "images", "LOGO", "voaf_logo.png")) self.afterbg.setPixmap(QPixmap(after_path)) self.afterbg.setScaledContents(True) @@ -349,8 +370,12 @@ class Ui_MainWindows(object): self.Mainbg.setObjectName(u"Mainbg") self.Mainbg.setGeometry(QRect(0, 0, 1280, 655)) # 允许拉伸以填满整个区域 - main_bg_pixmap = load_image_from_file(os.path.join(os.path.dirname(os.path.dirname(__file__)), "IMG", "BG", "title_bg1.png")) - + main_bg_path = resource_path(os.path.join("assets", "images", "BG", "title_bg1.png")) + logger.info(f"加载主背景图: {main_bg_path}") + main_bg_pixmap = QPixmap(main_bg_path) + if main_bg_pixmap.isNull(): + logger.error(f"主背景图加载失败: {main_bg_path}") + # 如果加载的图片不是空的,则设置,并允许拉伸填满 if not main_bg_pixmap.isNull(): self.Mainbg.setPixmap(main_bg_pixmap) @@ -358,7 +383,11 @@ class Ui_MainWindows(object): self.Mainbg.setAlignment(Qt.AlignmentFlag.AlignCenter) # 使用新的按钮图片 - button_pixmap = load_image_from_file(os.path.join(os.path.dirname(os.path.dirname(__file__)), "IMG", "BTN", "Button.png")) + button_path = resource_path(os.path.join("assets", "images", "BTN", "Button.png")) + logger.info(f"加载按钮图片: {button_path}") + button_pixmap = QPixmap(button_path) + if button_pixmap.isNull(): + logger.error(f"按钮图片加载失败: {button_path}") # 创建文本标签布局的按钮 # 开始安装按钮 - 基于背景图片和标签组合 diff --git a/source/utils/helpers.py b/source/utils/helpers.py index 750e65d..531c74f 100644 --- a/source/utils/helpers.py +++ b/source/utils/helpers.py @@ -101,24 +101,48 @@ class ProgressHashVerifyDialog(QDialog): self.status_label.setText(status) def resource_path(relative_path): - """获取资源的绝对路径,适用于开发环境和Nuitka打包环境""" - if getattr(sys, 'frozen', False): - # Nuitka/PyInstaller创建的临时文件夹,并将路径存储在_MEIPASS中或与可执行文件同目录 - if hasattr(sys, '_MEIPASS'): - base_path = sys._MEIPASS + """获取资源的绝对路径,适用于开发环境和打包环境""" + try: + if getattr(sys, 'frozen', False): + # 打包环境 - 可执行文件所在目录 + if hasattr(sys, '_MEIPASS'): + # PyInstaller打包的临时目录 + base_path = sys._MEIPASS + else: + # 其他打包方式,直接使用可执行文件目录 + base_path = os.path.dirname(sys.executable) + + # 对于离线补丁文件,需要在可执行文件所在目录查找 + if relative_path.lower() in ["vol.1.7z", "vol.2.7z", "vol.3.7z", "vol.4.7z", "after.7z"]: + exe_dir = os.path.dirname(sys.executable) + patch_path = os.path.join(exe_dir, relative_path) + if os.path.exists(patch_path): + logger.debug(f"找到离线补丁文件: {patch_path}") + return patch_path else: - base_path = os.path.dirname(sys.executable) - else: - # 在开发环境中运行 - base_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) + # 在开发环境中运行 + base_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) # 处理特殊的可执行文件和数据文件路径 if relative_path in ("aria2c-fast_x64.exe", "cfst.exe"): - return os.path.join(base_path, 'bin', relative_path) + result_path = os.path.join(base_path, 'bin', relative_path) elif relative_path in ("ip.txt", "ipv6.txt"): - return os.path.join(base_path, 'data', relative_path) + result_path = os.path.join(base_path, 'data', relative_path) + else: + # 标准资源路径 + result_path = os.path.join(base_path, relative_path) + + # 记录资源路径并验证是否存在 + if not os.path.exists(result_path) and relative_path: # 只在非空路径时检查 + logger.warning(f"资源文件不存在: {result_path}") + elif relative_path: # 避免记录空路径 + logger.debug(f"已找到资源文件: {result_path}") - return os.path.join(base_path, relative_path) + return result_path + except Exception as e: + logger.error(f"资源路径解析错误 ({relative_path}): {e}") + # 出错时仍返回一个基本路径 + return os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", relative_path) def load_base64_image(base64_str): pixmap = QPixmap() @@ -134,9 +158,57 @@ def load_image_from_file(file_path): Returns: QPixmap: 加载的图像 """ - if os.path.exists(file_path): - return QPixmap(file_path) - return QPixmap() + try: + if os.path.exists(file_path): + logger.info(f"加载图片: {file_path}") + pixmap = QPixmap(file_path) + + if pixmap.isNull(): + logger.error(f"图片加载失败(pixmap为空): {file_path}") + + # 检查文件大小和是否可读 + try: + file_size = os.path.getsize(file_path) + logger.debug(f"图片文件大小: {file_size} 字节") + if file_size == 0: + logger.error(f"图片文件大小为0字节: {file_path}") + + # 尝试打开文件测试可读性 + with open(file_path, 'rb') as f: + # 只读取前几个字节测试可访问性 + f.read(10) + logger.debug(f"图片文件可以正常打开和读取") + + # 检查文件扩展名是否正确 + ext = os.path.splitext(file_path)[1].lower() + if ext not in ['.png', '.jpg', '.jpeg', '.bmp', '.ico']: + logger.warning(f"图片文件扩展名可能不受支持: {ext}") + + except Exception as file_error: + logger.error(f"图片文件访问错误: {file_error}") + + return QPixmap() + else: + logger.debug(f"图片加载成功: {file_path}, 大小: {pixmap.width()}x{pixmap.height()}") + return pixmap + else: + logger.warning(f"图片文件不存在: {file_path}") + # 尝试列出父目录下的文件 + try: + parent_dir = os.path.dirname(file_path) + if os.path.exists(parent_dir): + files = os.listdir(parent_dir) + logger.debug(f"目录 {parent_dir} 中的文件: {files}") + else: + logger.debug(f"目录不存在: {parent_dir}") + except Exception as dir_error: + logger.error(f"无法列出目录内容: {dir_error}") + + return QPixmap() + except Exception as e: + logger.error(f"加载图片时发生异常: {e}") + logger.error(f"异常详情: {traceback.format_exc()}") + return QPixmap() def msgbox_frame(title, text, buttons=QtWidgets.QMessageBox.StandardButton.NoButton): msg_box = QtWidgets.QMessageBox() diff --git a/source/utils/logger.py b/source/utils/logger.py index 242992d..b7d059f 100644 --- a/source/utils/logger.py +++ b/source/utils/logger.py @@ -1,7 +1,13 @@ -from .url_censor import censor_url -import logging import os -from config.config import CACHE +import logging +import datetime +import sys +import glob +import time +from logging.handlers import RotatingFileHandler, TimedRotatingFileHandler +from config.config import LOG_DIR, LOG_FILE, LOG_LEVEL, LOG_MAX_SIZE, LOG_BACKUP_COUNT, LOG_RETENTION_DAYS + +from .url_censor import censor_url class URLCensorFormatter(logging.Formatter): """自定义的日志格式化器,用于隐藏日志消息中的URL""" @@ -44,7 +50,7 @@ class Logger: except Exception as e: # 发生错误时记录到控制台 self.terminal.write(f"Error writing to log: {e}\n") - + def flush(self): try: self.terminal.flush() @@ -62,63 +68,90 @@ class Logger: except Exception: pass +def cleanup_old_logs(retention_days=7): + """清理超过指定天数的旧日志文件 + + Args: + retention_days: 日志保留天数,默认7天 + """ + try: + now = time.time() + cutoff = now - (retention_days * 86400) # 86400秒 = 1天 + + # 获取所有日志文件 + log_files = glob.glob(os.path.join(LOG_DIR, "log-*.txt")) + + for log_file in log_files: + # 检查文件修改时间 + if os.path.getmtime(log_file) < cutoff: + try: + os.remove(log_file) + print(f"已删除过期日志: {log_file}") + except Exception as e: + print(f"删除日志文件失败 {log_file}: {e}") + except Exception as e: + print(f"清理旧日志文件时出错: {e}") + def setup_logger(name): """设置并返回一个命名的logger + 使用统一的日志文件,添加日志轮转功能,实现自动清理过期日志 + Args: name: logger的名称 - + Returns: logging.Logger: 配置好的logger对象 """ - # 导入LOG_FILE - from config.config import LOG_FILE - # 创建logger logger = logging.getLogger(name) # 避免重复添加处理器 if logger.hasHandlers(): return logger - - logger.setLevel(logging.DEBUG) + + # 根据配置设置日志级别 + log_level = getattr(logging, LOG_LEVEL.upper(), logging.DEBUG) + logger.setLevel(log_level) # 确保日志目录存在 - log_dir = os.path.join(CACHE, "logs") - os.makedirs(log_dir, exist_ok=True) - log_file = os.path.join(log_dir, f"{name}.log") + os.makedirs(LOG_DIR, exist_ok=True) - # 创建文件处理器 - 模块日志 - file_handler = logging.FileHandler(log_file, encoding="utf-8") - file_handler.setLevel(logging.DEBUG) + # 清理过期日志文件 + cleanup_old_logs(LOG_RETENTION_DAYS) - # 创建主日志文件处理器 - 所有日志合并到主LOG_FILE + # 创建主日志文件的轮转处理器 try: # 确保主日志文件目录存在 log_file_dir = os.path.dirname(LOG_FILE) if log_file_dir and not os.path.exists(log_file_dir): os.makedirs(log_file_dir, exist_ok=True) print(f"已创建主日志目录: {log_file_dir}") - - main_file_handler = logging.FileHandler(LOG_FILE, encoding="utf-8", mode="w") - main_file_handler.setLevel(logging.DEBUG) + + # 使用RotatingFileHandler实现日志轮转 + main_file_handler = RotatingFileHandler( + LOG_FILE, + maxBytes=LOG_MAX_SIZE, + backupCount=LOG_BACKUP_COUNT, + encoding="utf-8" + ) + main_file_handler.setLevel(log_level) except (IOError, OSError) as e: print(f"无法创建主日志文件处理器: {e}") main_file_handler = None # 创建控制台处理器 console_handler = logging.StreamHandler() - console_handler.setLevel(logging.INFO) + console_handler.setLevel(logging.INFO) # 控制台只显示INFO以上级别 + + # 创建更详细的格式器,包括模块名、文件名和行号 + formatter = URLCensorFormatter('%(asctime)s - %(name)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s') - # 创建格式器并添加到处理器 - formatter = URLCensorFormatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') - file_handler.setFormatter(formatter) console_handler.setFormatter(formatter) if main_file_handler: main_file_handler.setFormatter(formatter) # 添加处理器到logger - logger.addHandler(file_handler) logger.addHandler(console_handler) if main_file_handler: logger.addHandler(main_file_handler)