feat(core): 优化主窗口和管理器功能

- 在主窗口中重构初始化逻辑,增强UI组件的管理和信号连接,提升代码可读性。
- 添加资源验证和加载测试功能,确保关键资源文件的完整性和可用性。
- 更新下载管理器和离线模式管理器,优化线程管理和状态更新,提升用户体验。
- 增强日志记录,确保在关键操作中提供详细的调试信息,便于后续排查和用户反馈。
- 删除不再使用的进度窗口创建函数,改为由UIManager管理,提升代码整洁性。
This commit is contained in:
hyb-oyqq
2025-08-11 17:42:52 +08:00
parent dc433a2ac9
commit 68bbafc564
12 changed files with 842 additions and 551 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -23,7 +23,58 @@ class WindowManager:
# 设置圆角窗口
self.setRoundedCorners()
# 初始化状态管理
self._setup_window_state()
def _setup_window_state(self):
"""初始化窗口状态管理."""
self.STATE_INITIALIZING = "initializing"
self.STATE_READY = "ready"
self.STATE_DOWNLOADING = "downloading"
self.STATE_EXTRACTING = "extracting"
self.STATE_VERIFYING = "verifying"
self.STATE_INSTALLING = "installing"
self.STATE_COMPLETED = "completed"
self.STATE_ERROR = "error"
self.current_state = self.STATE_INITIALIZING
def change_window_state(self, new_state, error_message=None):
"""更改窗口状态并更新UI.
Args:
new_state (str): 新的状态.
error_message (str, optional): 错误信息. Defaults to None.
"""
if new_state == self.current_state:
return
self.current_state = new_state
self._update_ui_for_state(new_state, error_message)
def _update_ui_for_state(self, state, error_message=None):
"""根据当前状态更新UI组件."""
is_offline = self.window.offline_mode_manager.is_in_offline_mode()
config_valid = self.window.config_valid
button_enabled = False
button_text = "!无法安装!"
if state == self.STATE_READY:
if is_offline or config_valid:
button_enabled = True
button_text = "开始安装"
elif state in [self.STATE_DOWNLOADING, self.STATE_EXTRACTING, self.STATE_VERIFYING, self.STATE_INSTALLING]:
button_text = "正在安装"
elif state == self.STATE_COMPLETED:
button_enabled = True
button_text = "安装完成" # Or back to "开始安装"
self.window.ui.start_install_btn.setEnabled(button_enabled)
self.window.ui.start_install_text.setText(button_text)
self.window.install_button_enabled = button_enabled
def setRoundedCorners(self):
"""设置窗口圆角"""
# 实现圆角窗口