mirror of
https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT.git
synced 2025-12-20 05:48:35 +00:00
feat(core): 优化主窗口和管理器功能
- 在主窗口中重构初始化逻辑,增强UI组件的管理和信号连接,提升代码可读性。 - 添加资源验证和加载测试功能,确保关键资源文件的完整性和可用性。 - 更新下载管理器和离线模式管理器,优化线程管理和状态更新,提升用户体验。 - 增强日志记录,确保在关键操作中提供详细的调试信息,便于后续排查和用户反馈。 - 删除不再使用的进度窗口创建函数,改为由UIManager管理,提升代码整洁性。
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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**: 负责在后台执行耗时操作的线程。
|
||||
|
||||
这种结构更加清晰地区分了各个模块的职责,使代码更容易维护和扩展。
|
||||
@@ -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"],
|
||||
|
||||
@@ -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
|
||||
@@ -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(
|
||||
@@ -1059,3 +1070,40 @@ class DownloadManager:
|
||||
self.download_queue.append((url, game_folder, game_version, _7z_path, plugin_path))
|
||||
elif debug_mode:
|
||||
logger.warning(f"DEBUG: 未找到 {game_version} 的下载URL")
|
||||
|
||||
def graceful_stop_threads(self, threads_dict, timeout_ms=2000):
|
||||
"""优雅地停止一组线程.
|
||||
|
||||
Args:
|
||||
threads_dict (dict): 线程名字和线程对象的字典.
|
||||
timeout_ms (int): 等待线程自然结束的超时时间.
|
||||
"""
|
||||
for name, thread_obj in threads_dict.items():
|
||||
if not thread_obj or not hasattr(thread_obj, 'isRunning') or not thread_obj.isRunning():
|
||||
continue
|
||||
|
||||
try:
|
||||
if hasattr(thread_obj, 'requestInterruption'):
|
||||
thread_obj.requestInterruption()
|
||||
|
||||
if thread_obj.wait(timeout_ms):
|
||||
if self.debug_manager:
|
||||
self.debug_manager.log_debug(f"线程 {name} 已优雅停止.")
|
||||
else:
|
||||
if self.debug_manager:
|
||||
self.debug_manager.log_warning(f"线程 {name} 超时,强制终止.")
|
||||
thread_obj.terminate()
|
||||
thread_obj.wait(1000) # a short wait after termination
|
||||
except Exception as e:
|
||||
if self.debug_manager:
|
||||
self.debug_manager.log_error(f"停止线程 {name} 时发生错误: {e}")
|
||||
|
||||
def on_game_directories_identified(self, game_dirs):
|
||||
"""当游戏目录识别完成后的回调.
|
||||
|
||||
Args:
|
||||
game_dirs: 识别到的游戏目录
|
||||
"""
|
||||
self.main_window.hide_loading_dialog()
|
||||
self.main_window.setEnabled(True)
|
||||
self.main_window.patch_detector.on_offline_pre_hash_finished(updated_status, game_dirs)
|
||||
@@ -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
|
||||
|
||||
@@ -746,58 +822,53 @@ 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
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -969,3 +1015,61 @@ class UIManager:
|
||||
"\n已切换到在线模式。\n\n将从网络下载补丁进行安装。\n"
|
||||
)
|
||||
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
|
||||
@@ -24,6 +24,57 @@ 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):
|
||||
"""设置窗口圆角"""
|
||||
# 实现圆角窗口
|
||||
|
||||
@@ -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,167 +42,252 @@ 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):
|
||||
self.window_manager.handle_mouse_press(event)
|
||||
@@ -334,109 +420,18 @@ class MainWindow(QMainWindow):
|
||||
"""保存配置的便捷方法"""
|
||||
self.config_manager.save_config(config)
|
||||
|
||||
def create_download_thread(self, url, _7z_path, game_version):
|
||||
"""创建下载线程
|
||||
# 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): ...
|
||||
|
||||
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_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方法"""
|
||||
@@ -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
|
||||
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)
|
||||
}
|
||||
|
||||
# 在退出前优雅地清理后台线程,避免 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
|
||||
# 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
|
||||
|
||||
# 清理主窗口直接持有的线程
|
||||
_graceful_stop(getattr(self, 'pre_hash_thread', None), 'pre_hash_thread')
|
||||
_graceful_stop(getattr(self, 'hash_thread', None), 'hash_thread')
|
||||
self.download_manager.graceful_stop_threads(threads_to_stop)
|
||||
|
||||
# 清理离线管理器中的线程
|
||||
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
|
||||
|
||||
# 清理配置获取线程
|
||||
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):
|
||||
|
||||
@@ -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,7 +370,11 @@ 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():
|
||||
@@ -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}")
|
||||
|
||||
# 创建文本标签布局的按钮
|
||||
# 开始安装按钮 - 基于背景图片和标签组合
|
||||
|
||||
@@ -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)
|
||||
|
||||
return 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 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()
|
||||
|
||||
@@ -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"""
|
||||
@@ -62,18 +68,41 @@ 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)
|
||||
|
||||
@@ -81,18 +110,17 @@ def setup_logger(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)
|
||||
@@ -100,25 +128,30 @@ def setup_logger(name):
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user