feat(core): 优化线程管理和清理机制
- 在主窗口中添加优雅的线程清理逻辑,确保在退出时安全停止所有后台线程,避免潜在的资源泄漏。 - 更新离线模式管理器和哈希线程,增强对线程引用的管理,确保在操作完成后及时清理引用。 - 改进补丁检测器,支持在离线模式下的补丁状态检查,提升用户体验和系统稳定性。 - 增强日志记录,确保在关键操作中提供详细的调试信息,便于后续排查和用户反馈。
@@ -3,9 +3,9 @@ import os
|
||||
import datetime
|
||||
from PySide6.QtWidgets import QApplication, QMessageBox
|
||||
from main_window import MainWindow
|
||||
from core.privacy_manager import PrivacyManager
|
||||
from core.managers.privacy_manager import PrivacyManager
|
||||
from utils.logger import setup_logger
|
||||
from data.config import LOG_FILE, APP_NAME
|
||||
from config.config import LOG_FILE, APP_NAME
|
||||
from utils import load_config
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
63
source/STRUCTURE.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# 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**: 负责在后台执行耗时操作的线程。
|
||||
|
||||
这种结构更加清晰地区分了各个模块的职责,使代码更容易维护和扩展。
|
||||
7
source/assets/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
# Assets package initialization
|
||||
"""
|
||||
包含应用程序使用的静态资源文件:
|
||||
- fonts: 字体文件
|
||||
- images: 图片资源
|
||||
- resources: 其他资源文件
|
||||
"""
|
||||
BIN
source/assets/fonts/SmileySans-Oblique.ttf
Normal file
BIN
source/assets/images/After/voaf_ga01.jpg
Normal file
|
After Width: | Height: | Size: 162 KiB |
BIN
source/assets/images/After/voaf_ga02.jpg
Normal file
|
After Width: | Height: | Size: 179 KiB |
BIN
source/assets/images/BG/bg1.jpg
Normal file
|
After Width: | Height: | Size: 571 KiB |
BIN
source/assets/images/BG/bg2.jpg
Normal file
|
After Width: | Height: | Size: 250 KiB |
BIN
source/assets/images/BG/bg3.jpg
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
source/assets/images/BG/bg4.jpg
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
source/assets/images/BG/menubg.jpg
Normal file
|
After Width: | Height: | Size: 229 KiB |
BIN
source/assets/images/BG/title_bg1.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
source/assets/images/BG/title_bg2.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
source/assets/images/BTN/Button.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
source/assets/images/BTN/exit.bmp
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
source/assets/images/BTN/start_install.bmp
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
source/assets/images/ICO/cloudflare_logo_icon.ico
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
source/assets/images/ICO/icon.ico
Normal file
|
After Width: | Height: | Size: 264 KiB |
BIN
source/assets/images/ICO/icon.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
source/assets/images/LOGO/gl_head_logo_jp.png
Normal file
|
After Width: | Height: | Size: 8.6 KiB |
BIN
source/assets/images/LOGO/vo01_logo.png
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
source/assets/images/LOGO/vo02_logo.png
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
source/assets/images/LOGO/vo03_logo.png
Normal file
|
After Width: | Height: | Size: 177 KiB |
BIN
source/assets/images/LOGO/vo04_logo.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
source/assets/images/LOGO/voaf_logo.png
Normal file
|
After Width: | Height: | Size: 327 KiB |
BIN
source/assets/images/vol4/vo04_ga01.jpg
Normal file
|
After Width: | Height: | Size: 166 KiB |
BIN
source/assets/images/vol4/vo04_ga05.jpg
Normal file
|
After Width: | Height: | Size: 184 KiB |
BIN
source/assets/images/vol4/vo04_ga06.jpg
Normal file
|
After Width: | Height: | Size: 197 KiB |
BIN
source/assets/images/vol4/vo04_ga07.jpg
Normal file
|
After Width: | Height: | Size: 166 KiB |
1
source/config/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
91
source/config/config.py
Normal file
@@ -0,0 +1,91 @@
|
||||
import os
|
||||
import base64
|
||||
import datetime
|
||||
|
||||
# 配置信息
|
||||
app_data = {
|
||||
"APP_VERSION": "1.3.2",
|
||||
"APP_NAME": "FRAISEMOE Addons Installer NEXT",
|
||||
"TEMP": "TEMP",
|
||||
"CACHE": "FRAISEMOE",
|
||||
"PLUGIN": "PLUGIN",
|
||||
"CONFIG_URL": "aHR0cHM6Ly9hcGkuMncyLnRvcC9hcGkvb3V5YW5ncWlxaS9uZWtvcGFyYS9kb3dubG9hZF91cmwuanNvbg==",
|
||||
"UA_TEMPLATE": "Mozilla/5.0 (Linux debian12 FraiseMoe2-Accept-Next) Gecko/20100101 Firefox/114.0 FraiseMoe2/{}",
|
||||
"game_info": {
|
||||
"NEKOPARA Vol.1": {
|
||||
"exe": "nekopara_vol1.exe",
|
||||
"hash": "04b48b231a7f34431431e5027fcc7b27affaa951b8169c541709156acf754f3e",
|
||||
"install_path": "NEKOPARA Vol. 1/adultsonly.xp3",
|
||||
"plugin_path": "vol.1/adultsonly.xp3",
|
||||
},
|
||||
"NEKOPARA Vol.2": {
|
||||
"exe": "nekopara_vol2.exe",
|
||||
"hash": "b9c00a2b113a1e768bf78400e4f9075ceb7b35349cdeca09be62eb014f0d4b42",
|
||||
"install_path": "NEKOPARA Vol. 2/adultsonly.xp3",
|
||||
"plugin_path": "vol.2/adultsonly.xp3",
|
||||
},
|
||||
"NEKOPARA Vol.3": {
|
||||
"exe": "NEKOPARAvol3.exe",
|
||||
"hash": "2ce7b223c84592e1ebc3b72079dee1e5e8d064ade15723328a64dee58833b9d5",
|
||||
"install_path": "NEKOPARA Vol. 3/update00.int",
|
||||
"plugin_path": "vol.3/update00.int",
|
||||
},
|
||||
"NEKOPARA Vol.4": {
|
||||
"exe": "nekopara_vol4.exe",
|
||||
"hash": "4a4a9ae5a75a18aacbe3ab0774d7f93f99c046afe3a777ee0363e8932b90f36a",
|
||||
"install_path": "NEKOPARA Vol. 4/vol4adult.xp3",
|
||||
"plugin_path": "vol.4/vol4adult.xp3",
|
||||
},
|
||||
"NEKOPARA After": {
|
||||
"exe": "nekopara_after.exe",
|
||||
"hash": "eb26ff6850096a240af8340ba21c5c3232e90f29fb8191e24b6ce701acae0aa9",
|
||||
"install_path": "NEKOPARA After/afteradult.xp3",
|
||||
"plugin_path": "after/afteradult.xp3",
|
||||
"sig_path": "after/afteradult.xp3.sig"
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
# Base64解码
|
||||
def decode_base64(encoded_str):
|
||||
return base64.b64decode(encoded_str).decode("utf-8")
|
||||
|
||||
# 全局变量
|
||||
APP_VERSION = app_data["APP_VERSION"]
|
||||
APP_NAME = app_data["APP_NAME"]
|
||||
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文件放在程序根目录下的log文件夹中,使用日期+时间戳格式命名
|
||||
root_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
||||
log_dir = os.path.join(root_dir, "log")
|
||||
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
|
||||
PLUGIN_HASH = {
|
||||
"NEKOPARA Vol.1": GAME_INFO["NEKOPARA Vol.1"]["hash"],
|
||||
"NEKOPARA Vol.2": GAME_INFO["NEKOPARA Vol.2"]["hash"],
|
||||
"NEKOPARA Vol.3": GAME_INFO["NEKOPARA Vol.3"]["hash"],
|
||||
"NEKOPARA Vol.4": GAME_INFO["NEKOPARA Vol.4"]["hash"],
|
||||
"NEKOPARA After": GAME_INFO["NEKOPARA After"]["hash"]
|
||||
}
|
||||
PROCESS_INFO = {info["exe"]: game for game, info in GAME_INFO.items()}
|
||||
|
||||
# 下载线程档位设置
|
||||
DOWNLOAD_THREADS = {
|
||||
"low": 1, # 低速
|
||||
"medium": 8, # 中速(默认)
|
||||
"high": 16, # 高速
|
||||
"extreme": 32, # 极速
|
||||
"insane": 64 # 狂暴
|
||||
}
|
||||
|
||||
# 默认下载线程档位
|
||||
DEFAULT_DOWNLOAD_THREAD_LEVEL = "high"
|
||||
100
source/config/privacy_policy.py
Normal file
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from utils.logger import setup_logger
|
||||
|
||||
# 初始化logger
|
||||
logger = setup_logger("privacy_policy")
|
||||
|
||||
# 隐私协议的缩略版内容
|
||||
PRIVACY_POLICY_BRIEF = """
|
||||
# FRAISEMOE Addons Installer NEXT 隐私政策摘要
|
||||
|
||||
本应用在运行过程中会收集和处理以下信息:
|
||||
|
||||
## 收集的信息
|
||||
- **系统信息**:程序版本号。
|
||||
- **网络信息**:IP 地址、ISP、地理位置(用于使用统计)、下载统计、IPv6 连接测试(通过访问 testipv6.cn)、IPv6 地址获取(通过 ipw.cn)。
|
||||
- **文件信息**:游戏安装路径、文件哈希值。
|
||||
|
||||
## 系统修改
|
||||
- 使用 Cloudflare 加速时会临时修改系统 hosts 文件。
|
||||
- 修改前会自动备份,程序退出时自动恢复。
|
||||
|
||||
## 第三方服务
|
||||
- **Cloudflare 服务**:通过开源项目 CloudflareSpeedTest (CFST) 提供,用于优化下载速度。此过程会将您的 IP 提交至 Cloudflare 节点。
|
||||
- **云端配置服务**:获取配置信息。服务器会记录您的 IP、ISP 及地理位置用于统计。
|
||||
- **IPv6 测试服务**:应用使用 testipv6.cn 和 ipw.cn 测试和获取 IPv6 连接信息。
|
||||
|
||||
完整的隐私政策可在本程序的 GitHub 仓库中查看。
|
||||
"""
|
||||
|
||||
# 隐私协议的英文版缩略版内容
|
||||
PRIVACY_POLICY_BRIEF_EN = """
|
||||
# FRAISEMOE Addons Installer NEXT Privacy Policy Summary
|
||||
|
||||
This application collects and processes the following information:
|
||||
|
||||
## Information Collected
|
||||
- **System info**: Application version.
|
||||
- **Network info**: IP address, ISP, geographic location (for usage statistics), download statistics, IPv6 connectivity test (via testipv6.cn), IPv6 address acquisition (via ipw.cn).
|
||||
- **File info**: Game installation paths, file hash values.
|
||||
|
||||
## System Modifications
|
||||
- Temporarily modifies system hosts file when using Cloudflare acceleration.
|
||||
- Automatically backs up before modification and restores upon exit.
|
||||
|
||||
## Third-party Services
|
||||
- **Cloudflare services**: Provided via the open-source project CloudflareSpeedTest (CFST) to optimize download speeds. This process submits your IP to Cloudflare nodes.
|
||||
- **Cloud configuration services**: For obtaining configuration information. The server logs your IP, ISP, and location for statistical purposes.
|
||||
- **IPv6 testing services**: The application uses testipv6.cn and ipw.cn to test and retrieve IPv6 connection information.
|
||||
|
||||
The complete privacy policy can be found in the program's GitHub repository.
|
||||
"""
|
||||
|
||||
# 默认隐私协议版本 - 本地版本的日期
|
||||
PRIVACY_POLICY_VERSION = "2025.08.04"
|
||||
|
||||
def get_local_privacy_policy():
|
||||
"""获取本地打包的隐私协议文件
|
||||
|
||||
Returns:
|
||||
tuple: (隐私协议内容, 版本号, 错误信息)
|
||||
"""
|
||||
# 尝试不同的可能路径
|
||||
possible_paths = [
|
||||
"PRIVACY.md", # 相对于可执行文件
|
||||
os.path.join(os.path.dirname(sys.executable), "PRIVACY.md"), # 可执行文件目录
|
||||
os.path.join(os.path.dirname(__file__), "PRIVACY.md"), # 当前模块目录
|
||||
]
|
||||
|
||||
for path in possible_paths:
|
||||
try:
|
||||
if os.path.exists(path):
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
# 提取更新日期
|
||||
date_pattern = r'最后更新日期:(\d{4}年\d{1,2}月\d{1,2}日)'
|
||||
match = re.search(date_pattern, content)
|
||||
|
||||
if match:
|
||||
date_str = match.group(1)
|
||||
try:
|
||||
date_obj = datetime.strptime(date_str, '%Y年%m月%d日')
|
||||
date_version = date_obj.strftime('%Y.%m.%d')
|
||||
logger.info(f"成功读取本地隐私协议文件: {path}, 版本: {date_version}")
|
||||
return content, date_version, ""
|
||||
except ValueError:
|
||||
logger.error(f"本地隐私协议日期格式解析错误: {path}")
|
||||
else:
|
||||
logger.warning(f"本地隐私协议未找到更新日期: {path}")
|
||||
except Exception as e:
|
||||
logger.error(f"读取本地隐私协议失败 {path}: {str(e)}")
|
||||
|
||||
# 所有路径都尝试失败,使用默认版本
|
||||
return PRIVACY_POLICY_BRIEF, PRIVACY_POLICY_VERSION, "无法读取本地隐私协议文件"
|
||||
@@ -1,16 +1,16 @@
|
||||
from .animations import MultiStageAnimations
|
||||
from .ui_manager import UIManager
|
||||
from .download_manager import DownloadManager
|
||||
from .debug_manager import DebugManager
|
||||
from .window_manager import WindowManager
|
||||
from .game_detector import GameDetector
|
||||
from .patch_manager import PatchManager
|
||||
from .config_manager import ConfigManager
|
||||
from .privacy_manager import PrivacyManager
|
||||
from .cloudflare_optimizer import CloudflareOptimizer
|
||||
from .download_task_manager import DownloadTaskManager
|
||||
from .extraction_handler import ExtractionHandler
|
||||
from .patch_detector import PatchDetector
|
||||
from .managers.ui_manager import UIManager
|
||||
from .managers.download_manager import DownloadManager
|
||||
from .managers.debug_manager import DebugManager
|
||||
from .managers.window_manager import WindowManager
|
||||
from .managers.game_detector import GameDetector
|
||||
from .managers.patch_manager import PatchManager
|
||||
from .managers.config_manager import ConfigManager
|
||||
from .managers.privacy_manager import PrivacyManager
|
||||
from .managers.cloudflare_optimizer import CloudflareOptimizer
|
||||
from .managers.download_task_manager import DownloadTaskManager
|
||||
from .managers.patch_detector import PatchDetector
|
||||
from .managers.animations import MultiStageAnimations
|
||||
from .handlers.extraction_handler import ExtractionHandler
|
||||
|
||||
__all__ = [
|
||||
'MultiStageAnimations',
|
||||
|
||||
10
source/core/handlers/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
||||
# Handlers package initialization
|
||||
from .extraction_handler import ExtractionHandler
|
||||
from .patch_toggle_handler import PatchToggleHandler
|
||||
from .uninstall_handler import UninstallHandler
|
||||
|
||||
__all__ = [
|
||||
'ExtractionHandler',
|
||||
'PatchToggleHandler',
|
||||
'UninstallHandler',
|
||||
]
|
||||
265
source/core/handlers/extraction_handler.py
Normal file
@@ -0,0 +1,265 @@
|
||||
import os
|
||||
import shutil
|
||||
from PySide6 import QtWidgets
|
||||
from PySide6.QtWidgets import QMessageBox
|
||||
from PySide6.QtCore import QTimer, QCoreApplication
|
||||
|
||||
from utils.logger import setup_logger
|
||||
|
||||
# 初始化logger
|
||||
logger = setup_logger("extraction_handler")
|
||||
|
||||
class ExtractionHandler:
|
||||
"""解压处理器,负责管理解压任务和结果处理"""
|
||||
|
||||
def __init__(self, main_window):
|
||||
"""初始化解压处理器
|
||||
|
||||
Args:
|
||||
main_window: 主窗口实例,用于访问UI和状态
|
||||
"""
|
||||
self.main_window = main_window
|
||||
self.APP_NAME = main_window.APP_NAME if hasattr(main_window, 'APP_NAME') else ""
|
||||
self.extraction_progress_window = None
|
||||
|
||||
def start_extraction(self, _7z_path, game_folder, plugin_path, game_version, extracted_path=None):
|
||||
"""开始解压任务
|
||||
|
||||
Args:
|
||||
_7z_path: 7z文件路径
|
||||
game_folder: 游戏文件夹路径
|
||||
plugin_path: 插件路径
|
||||
game_version: 游戏版本名称
|
||||
extracted_path: 已解压的补丁文件路径,如果提供则直接使用它而不进行解压
|
||||
"""
|
||||
# 检查是否处于离线模式
|
||||
is_offline = False
|
||||
if hasattr(self.main_window, 'offline_mode_manager'):
|
||||
is_offline = self.main_window.offline_mode_manager.is_in_offline_mode()
|
||||
|
||||
# 创建并显示解压进度窗口,替代原来的消息框
|
||||
self.extraction_progress_window = self.main_window.create_extraction_progress_window()
|
||||
self.extraction_progress_window.show()
|
||||
|
||||
# 确保UI更新
|
||||
QCoreApplication.processEvents()
|
||||
|
||||
# 创建并启动解压线程
|
||||
self.main_window.extraction_thread = self.main_window.create_extraction_thread(
|
||||
_7z_path, game_folder, plugin_path, game_version, extracted_path
|
||||
)
|
||||
|
||||
# 连接进度信号
|
||||
self.main_window.extraction_thread.progress.connect(self.update_extraction_progress)
|
||||
|
||||
# 连接完成信号
|
||||
self.main_window.extraction_thread.finished.connect(self.on_extraction_finished_with_hash_check)
|
||||
|
||||
# 启动线程
|
||||
self.main_window.extraction_thread.start()
|
||||
|
||||
def update_extraction_progress(self, progress, status_text):
|
||||
"""更新解压进度
|
||||
|
||||
Args:
|
||||
progress: 进度百分比
|
||||
status_text: 状态文本
|
||||
"""
|
||||
if self.extraction_progress_window and hasattr(self.extraction_progress_window, 'progress_bar'):
|
||||
self.extraction_progress_window.progress_bar.setValue(progress)
|
||||
self.extraction_progress_window.status_label.setText(status_text)
|
||||
|
||||
# 确保UI更新
|
||||
QCoreApplication.processEvents()
|
||||
|
||||
def on_extraction_finished_with_hash_check(self, success, error_message, game_version):
|
||||
"""解压完成后进行哈希校验
|
||||
|
||||
Args:
|
||||
success: 是否解压成功
|
||||
error_message: 错误信息
|
||||
game_version: 游戏版本
|
||||
"""
|
||||
# 关闭解压进度窗口
|
||||
if self.extraction_progress_window:
|
||||
self.extraction_progress_window.close()
|
||||
self.extraction_progress_window = None
|
||||
|
||||
# 如果解压失败,显示错误并询问是否继续
|
||||
if not success:
|
||||
# 临时启用窗口以显示错误消息
|
||||
self.main_window.setEnabled(True)
|
||||
|
||||
QtWidgets.QMessageBox.critical(self.main_window, f"错误 - {self.APP_NAME}", error_message)
|
||||
self.main_window.installed_status[game_version] = False
|
||||
|
||||
# 询问用户是否继续其他游戏的安装
|
||||
reply = QtWidgets.QMessageBox.question(
|
||||
self.main_window,
|
||||
f"继续安装? - {self.APP_NAME}",
|
||||
f"\n{game_version} 的补丁安装失败。\n\n是否继续安装其他游戏的补丁?\n",
|
||||
QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No,
|
||||
QtWidgets.QMessageBox.StandardButton.Yes
|
||||
)
|
||||
|
||||
if reply == QtWidgets.QMessageBox.StandardButton.Yes:
|
||||
# 继续下一个,重新禁用窗口
|
||||
self.main_window.setEnabled(False)
|
||||
# 通知DownloadManager继续下一个下载任务
|
||||
self.main_window.download_manager.on_extraction_finished(True)
|
||||
else:
|
||||
# 用户选择停止,保持窗口启用状态
|
||||
self.main_window.ui.start_install_text.setText("开始安装")
|
||||
# 通知DownloadManager停止下载队列
|
||||
self.main_window.download_manager.on_extraction_finished(False)
|
||||
return
|
||||
|
||||
# 解压成功,进行哈希校验
|
||||
self._perform_hash_check(game_version)
|
||||
|
||||
def _perform_hash_check(self, game_version):
|
||||
"""解压成功后进行哈希校验
|
||||
|
||||
Args:
|
||||
game_version: 游戏版本
|
||||
"""
|
||||
# 导入所需模块
|
||||
from data.config import GAME_INFO, PLUGIN_HASH
|
||||
from workers.hash_thread import HashThread
|
||||
|
||||
# 获取安装路径
|
||||
install_paths = {}
|
||||
if hasattr(self.main_window, 'game_detector') and hasattr(self.main_window, 'download_manager'):
|
||||
game_dirs = self.main_window.game_detector.identify_game_directories_improved(
|
||||
self.main_window.download_manager.selected_folder
|
||||
)
|
||||
|
||||
for game, info in GAME_INFO.items():
|
||||
if game in game_dirs and game == game_version:
|
||||
game_dir = game_dirs[game]
|
||||
install_path = os.path.join(game_dir, os.path.basename(info["install_path"]))
|
||||
install_paths[game] = install_path
|
||||
break
|
||||
|
||||
if not install_paths:
|
||||
# 如果找不到安装路径,直接认为安装成功
|
||||
logger.warning(f"未找到 {game_version} 的安装路径,跳过哈希校验")
|
||||
self.main_window.installed_status[game_version] = True
|
||||
self.main_window.download_manager.on_extraction_finished(True)
|
||||
return
|
||||
|
||||
# 关闭可能存在的哈希校验窗口
|
||||
self.main_window.close_hash_msg_box()
|
||||
|
||||
# 显示哈希校验窗口
|
||||
self.main_window.hash_msg_box = self.main_window.hash_manager.hash_pop_window(
|
||||
check_type="post",
|
||||
auto_close=True, # 添加自动关闭参数
|
||||
close_delay=1000 # 1秒后自动关闭
|
||||
)
|
||||
|
||||
# 直接创建并启动哈希线程进行校验
|
||||
hash_thread = HashThread(
|
||||
"after",
|
||||
install_paths,
|
||||
PLUGIN_HASH,
|
||||
self.main_window.installed_status,
|
||||
self.main_window
|
||||
)
|
||||
hash_thread.after_finished.connect(self.on_hash_check_finished)
|
||||
|
||||
# 保存引用以便后续使用
|
||||
self.hash_thread = hash_thread
|
||||
hash_thread.start()
|
||||
|
||||
def on_hash_check_finished(self, result):
|
||||
"""哈希校验完成后的处理
|
||||
|
||||
Args:
|
||||
result: 校验结果,包含通过状态、游戏版本和消息
|
||||
"""
|
||||
# 导入所需模块
|
||||
from data.config import GAME_INFO
|
||||
|
||||
# 关闭哈希检查窗口
|
||||
self.main_window.close_hash_msg_box()
|
||||
|
||||
if not result["passed"]:
|
||||
# 校验失败,删除已解压的文件并提示重新下载
|
||||
game_version = result["game"]
|
||||
error_message = result["message"]
|
||||
|
||||
# 临时启用窗口以显示错误消息
|
||||
self.main_window.setEnabled(True)
|
||||
|
||||
# 获取安装路径
|
||||
install_path = None
|
||||
if hasattr(self.main_window, 'game_detector') and hasattr(self.main_window, 'download_manager'):
|
||||
game_dirs = self.main_window.game_detector.identify_game_directories_improved(
|
||||
self.main_window.download_manager.selected_folder
|
||||
)
|
||||
|
||||
if game_version in game_dirs and game_version in GAME_INFO:
|
||||
game_dir = game_dirs[game_version]
|
||||
install_path = os.path.join(game_dir, os.path.basename(GAME_INFO[game_version]["install_path"]))
|
||||
|
||||
# 如果找到安装路径,尝试删除已解压的文件
|
||||
if install_path and os.path.exists(install_path):
|
||||
try:
|
||||
os.remove(install_path)
|
||||
logger.info(f"已删除校验失败的文件: {install_path}")
|
||||
except Exception as e:
|
||||
logger.error(f"删除文件失败: {e}")
|
||||
|
||||
# 显示错误消息并询问是否重试
|
||||
reply = QtWidgets.QMessageBox.question(
|
||||
self.main_window,
|
||||
f"校验失败 - {self.APP_NAME}",
|
||||
f"{error_message}\n\n是否重新下载并安装?",
|
||||
QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No,
|
||||
QtWidgets.QMessageBox.StandardButton.Yes
|
||||
)
|
||||
|
||||
if reply == QtWidgets.QMessageBox.StandardButton.Yes:
|
||||
# 重新下载,将游戏重新添加到下载队列
|
||||
self.main_window.setEnabled(False)
|
||||
self.main_window.installed_status[game_version] = False
|
||||
|
||||
# 获取游戏目录和下载URL
|
||||
if hasattr(self.main_window, 'download_manager') and hasattr(self.main_window, 'game_detector'):
|
||||
game_dirs = self.main_window.game_detector.identify_game_directories_improved(
|
||||
self.main_window.download_manager.selected_folder
|
||||
)
|
||||
|
||||
if game_version in game_dirs:
|
||||
# 重新将游戏添加到下载队列
|
||||
self.main_window.download_manager.download_queue.appendleft([game_version])
|
||||
# 继续下一个下载任务
|
||||
self.main_window.download_manager.next_download_task()
|
||||
else:
|
||||
# 如果找不到游戏目录,继续下一个
|
||||
self.main_window.download_manager.on_extraction_finished(True)
|
||||
else:
|
||||
# 如果无法重新下载,继续下一个
|
||||
self.main_window.download_manager.on_extraction_finished(True)
|
||||
else:
|
||||
# 用户选择不重试,继续下一个
|
||||
self.main_window.installed_status[game_version] = False
|
||||
self.main_window.download_manager.on_extraction_finished(True)
|
||||
else:
|
||||
# 校验通过,更新安装状态
|
||||
game_version = result["game"]
|
||||
self.main_window.installed_status[game_version] = True
|
||||
# 通知DownloadManager继续下一个下载任务
|
||||
self.main_window.download_manager.on_extraction_finished(True)
|
||||
|
||||
def on_extraction_finished(self, success, error_message, game_version):
|
||||
"""兼容旧版本的回调函数
|
||||
|
||||
Args:
|
||||
success: 是否解压成功
|
||||
error_message: 错误信息
|
||||
game_version: 游戏版本
|
||||
"""
|
||||
# 调用新的带哈希校验的回调函数
|
||||
self.on_extraction_finished_with_hash_check(success, error_message, game_version)
|
||||
438
source/core/handlers/patch_toggle_handler.py
Normal file
@@ -0,0 +1,438 @@
|
||||
import os
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog, QVBoxLayout, QLabel, QListWidget, QPushButton, QHBoxLayout,
|
||||
QAbstractItemView, QRadioButton, QButtonGroup, QFileDialog, QMessageBox
|
||||
)
|
||||
from PySide6.QtCore import QObject, Signal, QThread
|
||||
from PySide6.QtGui import QFont
|
||||
from utils import msgbox_frame
|
||||
from utils.logger import setup_logger
|
||||
|
||||
# 初始化logger
|
||||
logger = setup_logger("patch_toggle_handler")
|
||||
|
||||
class PatchToggleThread(QThread):
|
||||
"""在后台线程中处理补丁切换逻辑"""
|
||||
finished = Signal(object)
|
||||
|
||||
def __init__(self, handler, selected_folder):
|
||||
super().__init__()
|
||||
self.handler = handler
|
||||
self.selected_folder = selected_folder
|
||||
|
||||
def run(self):
|
||||
# 在后台线程中执行耗时操作
|
||||
game_dirs = self.handler.game_detector.identify_game_directories_improved(self.selected_folder)
|
||||
self.finished.emit(game_dirs)
|
||||
|
||||
class PatchToggleHandler(QObject):
|
||||
"""
|
||||
处理补丁启用/禁用功能的类
|
||||
"""
|
||||
def __init__(self, main_window):
|
||||
"""
|
||||
初始化补丁切换处理程序
|
||||
|
||||
Args:
|
||||
main_window: 主窗口实例,用于访问其他组件
|
||||
"""
|
||||
super().__init__()
|
||||
self.main_window = main_window
|
||||
self.debug_manager = main_window.debug_manager
|
||||
self.game_detector = main_window.game_detector
|
||||
self.patch_manager = main_window.patch_manager
|
||||
self.app_name = main_window.patch_manager.app_name
|
||||
self.toggle_thread = None
|
||||
|
||||
def handle_toggle_patch_button_click(self):
|
||||
"""
|
||||
处理禁/启用补丁按钮点击事件
|
||||
打开文件选择对话框选择游戏目录,然后禁用或启用对应游戏的补丁
|
||||
"""
|
||||
selected_folder = QFileDialog.getExistingDirectory(self.main_window, "选择游戏上级目录", "")
|
||||
|
||||
if not selected_folder:
|
||||
return
|
||||
|
||||
self.main_window.show_loading_dialog("正在识别游戏目录并检查补丁状态...")
|
||||
|
||||
self.toggle_thread = PatchToggleThread(self, selected_folder)
|
||||
self.toggle_thread.finished.connect(self.on_game_detection_finished)
|
||||
self.toggle_thread.start()
|
||||
|
||||
def on_game_detection_finished(self, game_dirs):
|
||||
"""游戏识别完成后的回调"""
|
||||
self.main_window.hide_loading_dialog()
|
||||
|
||||
if not game_dirs:
|
||||
QMessageBox.information(
|
||||
self.main_window,
|
||||
f"提示 - {self.app_name}",
|
||||
"\n未在选择的目录中找到任何支持的游戏。\n",
|
||||
)
|
||||
return
|
||||
|
||||
games_with_patch = {}
|
||||
for game_version, game_dir in game_dirs.items():
|
||||
if self.patch_manager.check_patch_installed(game_dir, game_version):
|
||||
is_disabled, _ = self.patch_manager.check_patch_disabled(game_dir, game_version)
|
||||
status = "已禁用" if is_disabled else "已启用"
|
||||
games_with_patch[game_version] = {"dir": game_dir, "status": status}
|
||||
|
||||
if not games_with_patch:
|
||||
QMessageBox.information(
|
||||
self.main_window,
|
||||
f"提示 - {self.app_name}",
|
||||
"\n目录中未找到已安装补丁的游戏。\n",
|
||||
)
|
||||
return
|
||||
|
||||
selected_games, operation = self._show_multi_game_dialog(games_with_patch)
|
||||
|
||||
if not selected_games:
|
||||
return
|
||||
|
||||
selected_game_dirs = {game: games_with_patch[game]["dir"] for game in selected_games if game in games_with_patch}
|
||||
|
||||
self._execute_batch_toggle(selected_game_dirs, operation)
|
||||
|
||||
def _handle_multiple_games(self, game_dirs, debug_mode):
|
||||
"""
|
||||
处理多个游戏的补丁切换
|
||||
|
||||
Args:
|
||||
game_dirs: 游戏目录字典
|
||||
debug_mode: 是否为调试模式
|
||||
"""
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 禁/启用功能 - 在上级目录中找到以下游戏: {list(game_dirs.keys())}")
|
||||
|
||||
# 查找已安装补丁的游戏,只处理那些已安装补丁的游戏
|
||||
games_with_patch = {}
|
||||
for game_version, game_dir in game_dirs.items():
|
||||
if self.patch_manager.check_patch_installed(game_dir, game_version):
|
||||
# 检查补丁当前状态(是否禁用)
|
||||
is_disabled, disabled_path = self.patch_manager.check_patch_disabled(game_dir, game_version)
|
||||
status = "已禁用" if is_disabled else "已启用"
|
||||
games_with_patch[game_version] = {
|
||||
"dir": game_dir,
|
||||
"disabled": is_disabled,
|
||||
"status": status
|
||||
}
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 禁/启用功能 - {game_version} 已安装补丁,当前状态: {status}")
|
||||
|
||||
# 检查是否有已安装补丁的游戏
|
||||
if not games_with_patch:
|
||||
QMessageBox.information(
|
||||
self.main_window,
|
||||
f"提示 - {self.app_name}",
|
||||
"\n未在选择的目录中找到已安装补丁的游戏。\n请确认您选择了正确的游戏目录,并且该目录中的游戏已安装过补丁。\n",
|
||||
QMessageBox.StandardButton.Ok
|
||||
)
|
||||
return
|
||||
|
||||
# 显示选择对话框
|
||||
selected_games, operation = self._show_multi_game_dialog(games_with_patch)
|
||||
|
||||
if not selected_games:
|
||||
return # 用户取消了操作
|
||||
|
||||
# 过滤games_with_patch,只保留选中的游戏
|
||||
selected_game_dirs = {}
|
||||
for game in selected_games:
|
||||
if game in games_with_patch:
|
||||
selected_game_dirs[game] = games_with_patch[game]["dir"]
|
||||
|
||||
# 确认操作
|
||||
operation_text = "禁用" if operation == "disable" else "启用" if operation == "enable" else "切换"
|
||||
game_list = '\n'.join([f"{game} ({games_with_patch[game]['status']})" for game in selected_games])
|
||||
reply = QMessageBox.question(
|
||||
self.main_window,
|
||||
f"确认{operation_text}操作 - {self.app_name}",
|
||||
f"\n确定要{operation_text}以下游戏补丁吗?\n\n{game_list}\n",
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||
QMessageBox.StandardButton.No
|
||||
)
|
||||
|
||||
if reply == QMessageBox.StandardButton.No:
|
||||
return
|
||||
|
||||
# 执行批量操作
|
||||
self._execute_batch_toggle(selected_game_dirs, operation, debug_mode)
|
||||
|
||||
def _handle_single_game(self, selected_folder, debug_mode):
|
||||
"""
|
||||
处理单个游戏的补丁切换
|
||||
|
||||
Args:
|
||||
selected_folder: 选择的游戏目录
|
||||
debug_mode: 是否为调试模式
|
||||
"""
|
||||
# 未找到游戏目录,尝试将选择的目录作为游戏目录
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 禁/启用功能 - 未在上级目录找到游戏,尝试将选择的目录视为游戏目录")
|
||||
|
||||
game_version = self.game_detector.identify_game_version(selected_folder)
|
||||
|
||||
if game_version:
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 禁/启用功能 - 识别为游戏: {game_version}")
|
||||
|
||||
# 检查是否已安装补丁
|
||||
if self.patch_manager.check_patch_installed(selected_folder, game_version):
|
||||
# 检查补丁当前状态
|
||||
is_disabled, disabled_path = self.patch_manager.check_patch_disabled(selected_folder, game_version)
|
||||
current_status = "已禁用" if is_disabled else "已启用"
|
||||
|
||||
# 显示单游戏操作对话框
|
||||
operation = self._show_single_game_dialog(game_version, current_status, is_disabled)
|
||||
|
||||
if not operation:
|
||||
return # 用户取消了操作
|
||||
|
||||
# 执行操作
|
||||
result = self.patch_manager.toggle_patch(selected_folder, game_version, operation=operation)
|
||||
if not result["success"]:
|
||||
# 操作失败的消息已在toggle_patch中显示
|
||||
pass
|
||||
else:
|
||||
# 没有安装补丁
|
||||
QMessageBox.information(
|
||||
self.main_window,
|
||||
f"提示 - {self.app_name}",
|
||||
f"\n未在 {game_version} 中找到已安装的补丁。\n请确认该游戏已经安装过补丁。\n",
|
||||
QMessageBox.StandardButton.Ok
|
||||
)
|
||||
else:
|
||||
# 两种方式都未识别到游戏
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 禁/启用功能 - 无法识别游戏")
|
||||
|
||||
msg_box = msgbox_frame(
|
||||
f"错误 - {self.app_name}",
|
||||
"\n所选目录不是有效的NEKOPARA游戏目录。\n请选择包含游戏可执行文件的目录或其上级目录。\n",
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
msg_box.exec()
|
||||
|
||||
def _show_multi_game_dialog(self, games_with_patch):
|
||||
"""
|
||||
显示多游戏选择对话框
|
||||
|
||||
Args:
|
||||
games_with_patch: 已安装补丁的游戏信息
|
||||
|
||||
Returns:
|
||||
tuple: (选择的游戏列表, 操作类型)
|
||||
"""
|
||||
dialog = QDialog(self.main_window)
|
||||
dialog.setWindowTitle("选择要操作的游戏补丁")
|
||||
dialog.resize(400, 400) # 增加高度以适应新增的单选按钮
|
||||
|
||||
layout = QVBoxLayout(dialog)
|
||||
|
||||
# 添加"已安装补丁的游戏"标签
|
||||
already_installed_label = QLabel("已安装补丁的游戏:", dialog)
|
||||
already_installed_label.setFont(QFont(already_installed_label.font().family(), already_installed_label.font().pointSize(), QFont.Bold))
|
||||
layout.addWidget(already_installed_label)
|
||||
|
||||
# 添加游戏列表和状态
|
||||
games_status_text = ""
|
||||
for game, info in games_with_patch.items():
|
||||
games_status_text += f"{game} ({info['status']})\n"
|
||||
games_status_label = QLabel(games_status_text.strip(), dialog)
|
||||
layout.addWidget(games_status_label)
|
||||
|
||||
# 添加一些间距
|
||||
layout.addSpacing(10)
|
||||
|
||||
# 添加"请选择要操作的游戏"标签
|
||||
info_label = QLabel("请选择要操作的游戏:", dialog)
|
||||
info_label.setFont(QFont(info_label.font().family(), info_label.font().pointSize(), QFont.Bold))
|
||||
layout.addWidget(info_label)
|
||||
|
||||
# 添加列表控件,只显示已安装补丁的游戏
|
||||
list_widget = QListWidget(dialog)
|
||||
list_widget.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) # 允许多选
|
||||
for game, info in games_with_patch.items():
|
||||
list_widget.addItem(f"{game} ({info['status']})")
|
||||
layout.addWidget(list_widget)
|
||||
|
||||
# 添加全选按钮
|
||||
select_all_btn = QPushButton("全选", dialog)
|
||||
select_all_btn.clicked.connect(lambda: list_widget.selectAll())
|
||||
layout.addWidget(select_all_btn)
|
||||
|
||||
# 添加操作选择单选按钮
|
||||
operation_label = QLabel("请选择要执行的操作:", dialog)
|
||||
operation_label.setFont(QFont(operation_label.font().family(), operation_label.font().pointSize(), QFont.Bold))
|
||||
layout.addWidget(operation_label)
|
||||
|
||||
# 创建单选按钮组
|
||||
radio_button_group = QButtonGroup(dialog)
|
||||
|
||||
# 添加"自动切换状态"单选按钮(默认选中)
|
||||
auto_toggle_radio = QRadioButton("自动切换状态(禁用<->启用)", dialog)
|
||||
auto_toggle_radio.setChecked(True)
|
||||
radio_button_group.addButton(auto_toggle_radio, 0)
|
||||
layout.addWidget(auto_toggle_radio)
|
||||
|
||||
# 添加"全部禁用"单选按钮
|
||||
disable_all_radio = QRadioButton("禁用选中的补丁", dialog)
|
||||
radio_button_group.addButton(disable_all_radio, 1)
|
||||
layout.addWidget(disable_all_radio)
|
||||
|
||||
# 添加"全部启用"单选按钮
|
||||
enable_all_radio = QRadioButton("启用选中的补丁", dialog)
|
||||
radio_button_group.addButton(enable_all_radio, 2)
|
||||
layout.addWidget(enable_all_radio)
|
||||
|
||||
# 添加确定和取消按钮
|
||||
buttons_layout = QHBoxLayout()
|
||||
ok_button = QPushButton("确定", dialog)
|
||||
cancel_button = QPushButton("取消", dialog)
|
||||
buttons_layout.addWidget(ok_button)
|
||||
buttons_layout.addWidget(cancel_button)
|
||||
layout.addLayout(buttons_layout)
|
||||
|
||||
# 连接按钮事件
|
||||
ok_button.clicked.connect(dialog.accept)
|
||||
cancel_button.clicked.connect(dialog.reject)
|
||||
|
||||
# 显示对话框并等待用户选择
|
||||
result = dialog.exec()
|
||||
|
||||
if result != QDialog.DialogCode.Accepted or list_widget.selectedItems() == []:
|
||||
# 用户取消或未选择任何游戏
|
||||
return [], None
|
||||
|
||||
# 获取用户选择的游戏
|
||||
selected_items = [item.text() for item in list_widget.selectedItems()]
|
||||
selected_games = []
|
||||
|
||||
# 从选中项文本中提取游戏名称
|
||||
for item in selected_items:
|
||||
# 去除状态后缀 " (已启用)" 或 " (已禁用)"
|
||||
game_name = item.split(" (")[0]
|
||||
selected_games.append(game_name)
|
||||
|
||||
# 获取选中的操作类型
|
||||
operation = None
|
||||
if radio_button_group.checkedId() == 1: # 禁用选中的补丁
|
||||
operation = "disable"
|
||||
elif radio_button_group.checkedId() == 2: # 启用选中的补丁
|
||||
operation = "enable"
|
||||
# 否则为None,表示自动切换状态
|
||||
|
||||
return selected_games, operation
|
||||
|
||||
def _show_single_game_dialog(self, game_version, current_status, is_disabled):
|
||||
"""
|
||||
显示单游戏操作对话框
|
||||
|
||||
Args:
|
||||
game_version: 游戏版本
|
||||
current_status: 当前补丁状态
|
||||
is_disabled: 是否已禁用
|
||||
|
||||
Returns:
|
||||
str: 操作类型,"enable"或"disable",或None表示取消
|
||||
"""
|
||||
dialog = QDialog(self.main_window)
|
||||
dialog.setWindowTitle(f"{game_version} 补丁操作")
|
||||
dialog.resize(300, 200)
|
||||
|
||||
layout = QVBoxLayout(dialog)
|
||||
|
||||
# 添加当前状态标签
|
||||
status_label = QLabel(f"当前补丁状态: {current_status}", dialog)
|
||||
status_label.setFont(QFont(status_label.font().family(), status_label.font().pointSize(), QFont.Bold))
|
||||
layout.addWidget(status_label)
|
||||
|
||||
# 添加操作选择单选按钮
|
||||
operation_label = QLabel("请选择要执行的操作:", dialog)
|
||||
layout.addWidget(operation_label)
|
||||
|
||||
# 创建单选按钮组
|
||||
radio_button_group = QButtonGroup(dialog)
|
||||
|
||||
# 添加可选操作
|
||||
if is_disabled:
|
||||
# 当前是禁用状态,显示启用选项
|
||||
enable_radio = QRadioButton("启用补丁", dialog)
|
||||
enable_radio.setChecked(True)
|
||||
radio_button_group.addButton(enable_radio, 0)
|
||||
layout.addWidget(enable_radio)
|
||||
else:
|
||||
# 当前是启用状态,显示禁用选项
|
||||
disable_radio = QRadioButton("禁用补丁", dialog)
|
||||
disable_radio.setChecked(True)
|
||||
radio_button_group.addButton(disable_radio, 0)
|
||||
layout.addWidget(disable_radio)
|
||||
|
||||
# 添加确定和取消按钮
|
||||
buttons_layout = QHBoxLayout()
|
||||
ok_button = QPushButton("确定", dialog)
|
||||
cancel_button = QPushButton("取消", dialog)
|
||||
buttons_layout.addWidget(ok_button)
|
||||
buttons_layout.addWidget(cancel_button)
|
||||
layout.addLayout(buttons_layout)
|
||||
|
||||
# 连接按钮事件
|
||||
ok_button.clicked.connect(dialog.accept)
|
||||
cancel_button.clicked.connect(dialog.reject)
|
||||
|
||||
# 显示对话框并等待用户选择
|
||||
result = dialog.exec()
|
||||
|
||||
if result != QDialog.DialogCode.Accepted:
|
||||
# 用户取消
|
||||
return None
|
||||
|
||||
# 根据当前状态确定操作
|
||||
return "enable" if is_disabled else "disable"
|
||||
|
||||
def _execute_batch_toggle(self, selected_game_dirs, operation, debug_mode):
|
||||
"""
|
||||
执行批量补丁切换操作
|
||||
|
||||
Args:
|
||||
selected_game_dirs: 选择的游戏目录
|
||||
operation: 操作类型
|
||||
debug_mode: 是否为调试模式
|
||||
"""
|
||||
success_count = 0
|
||||
fail_count = 0
|
||||
results = []
|
||||
|
||||
for game_version, game_dir in selected_game_dirs.items():
|
||||
try:
|
||||
# 使用静默模式进行操作
|
||||
result = self.patch_manager.toggle_patch(game_dir, game_version, operation=operation, silent=True)
|
||||
|
||||
if result["success"]:
|
||||
success_count += 1
|
||||
else:
|
||||
fail_count += 1
|
||||
|
||||
results.append({
|
||||
"version": game_version,
|
||||
"success": result["success"],
|
||||
"message": result["message"],
|
||||
"action": result["action"]
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
if debug_mode:
|
||||
logger.error(f"DEBUG: 切换 {game_version} 补丁状态时出错: {str(e)}")
|
||||
fail_count += 1
|
||||
results.append({
|
||||
"version": game_version,
|
||||
"success": False,
|
||||
"message": f"操作出错: {str(e)}",
|
||||
"action": "none"
|
||||
})
|
||||
|
||||
# 显示操作结果
|
||||
self.patch_manager.show_toggle_result(success_count, fail_count, results)
|
||||
388
source/core/handlers/uninstall_handler.py
Normal file
@@ -0,0 +1,388 @@
|
||||
import os
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog, QVBoxLayout, QLabel, QListWidget, QPushButton, QHBoxLayout,
|
||||
QAbstractItemView, QFileDialog, QMessageBox
|
||||
)
|
||||
from PySide6.QtCore import QObject, Signal, QThread
|
||||
from PySide6.QtGui import QFont
|
||||
from utils import msgbox_frame
|
||||
from utils.logger import setup_logger
|
||||
|
||||
# 初始化logger
|
||||
logger = setup_logger("uninstall_handler")
|
||||
|
||||
class UninstallThread(QThread):
|
||||
"""在后台线程中处理卸载逻辑"""
|
||||
finished = Signal(object)
|
||||
|
||||
def __init__(self, handler, selected_folder):
|
||||
super().__init__()
|
||||
self.handler = handler
|
||||
self.selected_folder = selected_folder
|
||||
|
||||
def run(self):
|
||||
# 在后台线程中执行耗时操作
|
||||
game_dirs = self.handler.game_detector.identify_game_directories_improved(self.selected_folder)
|
||||
self.finished.emit(game_dirs)
|
||||
|
||||
class UninstallHandler(QObject):
|
||||
"""
|
||||
处理补丁卸载功能的类
|
||||
"""
|
||||
def __init__(self, main_window):
|
||||
"""
|
||||
初始化卸载处理程序
|
||||
|
||||
Args:
|
||||
main_window: 主窗口实例,用于访问其他组件
|
||||
"""
|
||||
super().__init__()
|
||||
self.main_window = main_window
|
||||
self.debug_manager = main_window.debug_manager
|
||||
self.game_detector = main_window.game_detector
|
||||
self.patch_manager = main_window.patch_manager
|
||||
self.app_name = main_window.patch_manager.app_name
|
||||
self.uninstall_thread = None
|
||||
|
||||
# 记录初始化日志
|
||||
debug_mode = self.debug_manager._is_debug_mode() if hasattr(self.debug_manager, '_is_debug_mode') else False
|
||||
if debug_mode:
|
||||
logger.debug("DEBUG: 卸载处理程序已初始化")
|
||||
|
||||
def handle_uninstall_button_click(self):
|
||||
"""
|
||||
处理卸载补丁按钮点击事件
|
||||
打开文件选择对话框选择游戏目录,然后卸载对应游戏的补丁
|
||||
"""
|
||||
# 获取游戏目录
|
||||
debug_mode = self.debug_manager._is_debug_mode()
|
||||
|
||||
logger.info("用户点击了卸载补丁按钮")
|
||||
if debug_mode:
|
||||
logger.debug("DEBUG: 处理卸载补丁按钮点击事件")
|
||||
|
||||
# 提示用户选择目录
|
||||
file_dialog_info = "选择游戏上级目录" if debug_mode else "选择游戏目录"
|
||||
selected_folder = QFileDialog.getExistingDirectory(self.main_window, file_dialog_info, "")
|
||||
|
||||
if not selected_folder or selected_folder == "":
|
||||
logger.info("用户取消了目录选择")
|
||||
if debug_mode:
|
||||
logger.debug("DEBUG: 用户取消了目录选择,退出卸载流程")
|
||||
return # 用户取消了选择
|
||||
|
||||
logger.info(f"用户选择了目录: {selected_folder}")
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 卸载功能 - 用户选择了目录: {selected_folder}")
|
||||
|
||||
self.main_window.show_loading_dialog("正在识别游戏目录...")
|
||||
|
||||
self.uninstall_thread = UninstallThread(self, selected_folder)
|
||||
self.uninstall_thread.finished.connect(self.on_game_detection_finished)
|
||||
self.uninstall_thread.start()
|
||||
|
||||
def on_game_detection_finished(self, game_dirs):
|
||||
"""游戏识别完成后的回调"""
|
||||
self.main_window.hide_loading_dialog()
|
||||
|
||||
if not game_dirs:
|
||||
QMessageBox.information(
|
||||
self.main_window,
|
||||
f"提示 - {self.app_name}",
|
||||
"\n未在选择的目录中找到任何支持的游戏。\n",
|
||||
)
|
||||
return
|
||||
|
||||
games_with_patch = {}
|
||||
for game_version, game_dir in game_dirs.items():
|
||||
if self.patch_manager.check_patch_installed(game_dir, game_version):
|
||||
games_with_patch[game_version] = game_dir
|
||||
|
||||
if not games_with_patch:
|
||||
QMessageBox.information(
|
||||
self.main_window,
|
||||
f"提示 - {self.app_name}",
|
||||
"\n目录中未找到已安装补丁的游戏。\n",
|
||||
)
|
||||
return
|
||||
|
||||
selected_games = self._show_game_selection_dialog(games_with_patch)
|
||||
|
||||
if not selected_games:
|
||||
return
|
||||
|
||||
selected_game_dirs = {game: games_with_patch[game] for game in selected_games if game in games_with_patch}
|
||||
|
||||
game_list = '\n'.join(selected_games)
|
||||
reply = QMessageBox.question(
|
||||
self.main_window,
|
||||
f"确认卸载 - {self.app_name}",
|
||||
f"\n确定要卸载以下游戏的补丁吗?\n\n{game_list}\n",
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||
QMessageBox.StandardButton.No
|
||||
)
|
||||
|
||||
if reply == QMessageBox.StandardButton.No:
|
||||
return
|
||||
|
||||
success_count, fail_count, results = self.patch_manager.batch_uninstall_patches(selected_game_dirs)
|
||||
self.patch_manager.show_uninstall_result(success_count, fail_count, results)
|
||||
|
||||
def _handle_multiple_games(self, game_dirs, debug_mode):
|
||||
"""
|
||||
处理多个游戏的补丁卸载
|
||||
|
||||
Args:
|
||||
game_dirs: 游戏目录字典
|
||||
debug_mode: 是否为调试模式
|
||||
"""
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 卸载功能 - 在上级目录中找到以下游戏: {list(game_dirs.keys())}")
|
||||
|
||||
# 查找已安装补丁的游戏,只处理那些已安装补丁的游戏
|
||||
logger.info("检查哪些游戏已安装补丁")
|
||||
games_with_patch = {}
|
||||
for game_version, game_dir in game_dirs.items():
|
||||
is_installed = self.patch_manager.check_patch_installed(game_dir, game_version)
|
||||
if is_installed:
|
||||
games_with_patch[game_version] = game_dir
|
||||
logger.info(f"游戏 {game_version} 已安装补丁")
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 卸载功能 - {game_version} 已安装补丁,目录: {game_dir}")
|
||||
else:
|
||||
logger.info(f"游戏 {game_version} 未安装补丁")
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 卸载功能 - {game_version} 未安装补丁,跳过")
|
||||
|
||||
# 检查是否有已安装补丁的游戏
|
||||
if not games_with_patch:
|
||||
logger.info("未找到已安装补丁的游戏")
|
||||
if debug_mode:
|
||||
logger.debug("DEBUG: 卸载功能 - 未找到已安装补丁的游戏,显示提示消息")
|
||||
|
||||
QMessageBox.information(
|
||||
self.main_window,
|
||||
f"提示 - {self.app_name}",
|
||||
"\n未在选择的目录中找到已安装补丁的游戏。\n请确认您选择了正确的游戏目录,并且该目录中的游戏已安装过补丁。\n",
|
||||
QMessageBox.StandardButton.Ok
|
||||
)
|
||||
return
|
||||
|
||||
# 显示选择对话框
|
||||
logger.info("显示游戏选择对话框")
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 卸载功能 - 显示游戏选择对话框,可选游戏: {list(games_with_patch.keys())}")
|
||||
|
||||
selected_games = self._show_game_selection_dialog(games_with_patch)
|
||||
|
||||
if not selected_games:
|
||||
logger.info("用户未选择任何游戏或取消了选择")
|
||||
if debug_mode:
|
||||
logger.debug("DEBUG: 卸载功能 - 用户未选择任何游戏或取消了选择,退出卸载流程")
|
||||
return # 用户取消了选择
|
||||
|
||||
logger.info(f"用户选择了以下游戏: {selected_games}")
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 卸载功能 - 用户选择了以下游戏: {selected_games}")
|
||||
|
||||
# 过滤game_dirs,只保留选中的游戏
|
||||
selected_game_dirs = {game: games_with_patch[game] for game in selected_games if game in games_with_patch}
|
||||
|
||||
# 确认卸载
|
||||
game_list = '\n'.join(selected_games)
|
||||
logger.info("显示卸载确认对话框")
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 卸载功能 - 显示卸载确认对话框,选择的游戏: {selected_games}")
|
||||
|
||||
reply = QMessageBox.question(
|
||||
self.main_window,
|
||||
f"确认卸载 - {self.app_name}",
|
||||
f"\n确定要卸载以下游戏的补丁吗?\n\n{game_list}\n",
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||
QMessageBox.StandardButton.No
|
||||
)
|
||||
|
||||
if reply == QMessageBox.StandardButton.No:
|
||||
logger.info("用户取消了卸载操作")
|
||||
if debug_mode:
|
||||
logger.debug("DEBUG: 卸载功能 - 用户取消了卸载操作,退出卸载流程")
|
||||
return
|
||||
|
||||
logger.info("开始批量卸载补丁")
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 卸载功能 - 开始批量卸载补丁,游戏: {list(selected_game_dirs.keys())}")
|
||||
|
||||
# 使用批量卸载方法
|
||||
success_count, fail_count, results = self.patch_manager.batch_uninstall_patches(selected_game_dirs)
|
||||
|
||||
logger.info(f"批量卸载完成,成功: {success_count},失败: {fail_count}")
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 卸载功能 - 批量卸载完成,成功: {success_count},失败: {fail_count}")
|
||||
if results:
|
||||
for result in results:
|
||||
status = "成功" if result["success"] else "失败"
|
||||
logger.debug(f"DEBUG: 卸载结果 - {result['version']}: {status}, 消息: {result['message']}, 删除文件数: {result['files_removed']}")
|
||||
|
||||
self.patch_manager.show_uninstall_result(success_count, fail_count, results)
|
||||
|
||||
def _handle_single_game(self, selected_folder, debug_mode):
|
||||
"""
|
||||
处理单个游戏的补丁卸载
|
||||
|
||||
Args:
|
||||
selected_folder: 选择的游戏目录
|
||||
debug_mode: 是否为调试模式
|
||||
"""
|
||||
# 未找到游戏目录,尝试将选择的目录作为游戏目录
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 卸载功能 - 未在上级目录找到游戏,尝试将选择的目录视为游戏目录")
|
||||
|
||||
logger.info("尝试识别单个游戏版本")
|
||||
game_version = self.game_detector.identify_game_version(selected_folder)
|
||||
|
||||
if game_version:
|
||||
logger.info(f"识别为游戏: {game_version}")
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 卸载功能 - 识别为游戏: {game_version}")
|
||||
|
||||
# 检查是否已安装补丁
|
||||
logger.info(f"检查 {game_version} 是否已安装补丁")
|
||||
is_installed = self.patch_manager.check_patch_installed(selected_folder, game_version)
|
||||
|
||||
if is_installed:
|
||||
logger.info(f"{game_version} 已安装补丁")
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 卸载功能 - {game_version} 已安装补丁")
|
||||
|
||||
# 确认卸载
|
||||
logger.info("显示卸载确认对话框")
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 卸载功能 - 显示卸载确认对话框,游戏: {game_version}")
|
||||
|
||||
reply = QMessageBox.question(
|
||||
self.main_window,
|
||||
f"确认卸载 - {self.app_name}",
|
||||
f"\n确定要卸载 {game_version} 的补丁吗?\n游戏目录: {selected_folder}\n",
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||
QMessageBox.StandardButton.No
|
||||
)
|
||||
|
||||
if reply == QMessageBox.StandardButton.Yes:
|
||||
logger.info(f"开始卸载 {game_version} 的补丁")
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 卸载功能 - 用户确认卸载 {game_version} 的补丁")
|
||||
|
||||
# 创建单个游戏的目录字典,使用批量卸载流程
|
||||
single_game_dir = {game_version: selected_folder}
|
||||
|
||||
logger.info("执行批量卸载方法(单游戏)")
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 卸载功能 - 执行批量卸载方法(单游戏): {game_version}")
|
||||
|
||||
success_count, fail_count, results = self.patch_manager.batch_uninstall_patches(single_game_dir)
|
||||
|
||||
logger.info(f"卸载完成,成功: {success_count},失败: {fail_count}")
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 卸载功能 - 卸载完成,成功: {success_count},失败: {fail_count}")
|
||||
if results:
|
||||
for result in results:
|
||||
status = "成功" if result["success"] else "失败"
|
||||
logger.debug(f"DEBUG: 卸载结果 - {result['version']}: {status}, 消息: {result['message']}, 删除文件数: {result['files_removed']}")
|
||||
|
||||
self.patch_manager.show_uninstall_result(success_count, fail_count, results)
|
||||
else:
|
||||
logger.info("用户取消了卸载操作")
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 卸载功能 - 用户取消了卸载 {game_version} 的补丁")
|
||||
else:
|
||||
logger.info(f"{game_version} 未安装补丁")
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 卸载功能 - {game_version} 未安装补丁,显示提示消息")
|
||||
|
||||
# 没有安装补丁
|
||||
QMessageBox.information(
|
||||
self.main_window,
|
||||
f"提示 - {self.app_name}",
|
||||
f"\n未在 {game_version} 中找到已安装的补丁。\n请确认该游戏已经安装过补丁。\n",
|
||||
QMessageBox.StandardButton.Ok
|
||||
)
|
||||
else:
|
||||
# 两种方式都未识别到游戏
|
||||
logger.info("无法识别游戏")
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 卸载功能 - 无法识别游戏,显示错误消息")
|
||||
|
||||
msg_box = msgbox_frame(
|
||||
f"错误 - {self.app_name}",
|
||||
"\n所选目录不是有效的NEKOPARA游戏目录。\n请选择包含游戏可执行文件的目录或其上级目录。\n",
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
msg_box.exec()
|
||||
|
||||
def _show_game_selection_dialog(self, games_with_patch):
|
||||
"""
|
||||
显示游戏选择对话框
|
||||
|
||||
Args:
|
||||
games_with_patch: 已安装补丁的游戏目录字典
|
||||
|
||||
Returns:
|
||||
list: 选择的游戏列表
|
||||
"""
|
||||
dialog = QDialog(self.main_window)
|
||||
dialog.setWindowTitle("选择要卸载的游戏补丁")
|
||||
dialog.resize(400, 300)
|
||||
|
||||
layout = QVBoxLayout(dialog)
|
||||
|
||||
# 添加"已安装补丁的游戏"标签
|
||||
already_installed_label = QLabel("已安装补丁的游戏:", dialog)
|
||||
already_installed_label.setFont(QFont(already_installed_label.font().family(), already_installed_label.font().pointSize(), QFont.Weight.Bold))
|
||||
layout.addWidget(already_installed_label)
|
||||
|
||||
# 添加已安装游戏列表(可选,这里使用静态标签替代,保持一致性)
|
||||
installed_games_text = ", ".join(games_with_patch.keys())
|
||||
installed_games_label = QLabel(installed_games_text, dialog)
|
||||
layout.addWidget(installed_games_label)
|
||||
|
||||
# 添加一些间距
|
||||
layout.addSpacing(10)
|
||||
|
||||
# 添加"请选择要卸载补丁的游戏"标签
|
||||
info_label = QLabel("请选择要卸载补丁的游戏:", dialog)
|
||||
info_label.setFont(QFont(info_label.font().family(), info_label.font().pointSize(), QFont.Weight.Bold))
|
||||
layout.addWidget(info_label)
|
||||
|
||||
# 添加列表控件,只显示已安装补丁的游戏
|
||||
list_widget = QListWidget(dialog)
|
||||
list_widget.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) # 允许多选
|
||||
for game in games_with_patch.keys():
|
||||
list_widget.addItem(game)
|
||||
layout.addWidget(list_widget)
|
||||
|
||||
# 添加全选按钮
|
||||
select_all_btn = QPushButton("全选", dialog)
|
||||
select_all_btn.clicked.connect(lambda: list_widget.selectAll())
|
||||
layout.addWidget(select_all_btn)
|
||||
|
||||
# 添加确定和取消按钮
|
||||
buttons_layout = QHBoxLayout()
|
||||
ok_button = QPushButton("确定", dialog)
|
||||
cancel_button = QPushButton("取消", dialog)
|
||||
buttons_layout.addWidget(ok_button)
|
||||
buttons_layout.addWidget(cancel_button)
|
||||
layout.addLayout(buttons_layout)
|
||||
|
||||
# 连接按钮事件
|
||||
ok_button.clicked.connect(dialog.accept)
|
||||
cancel_button.clicked.connect(dialog.reject)
|
||||
|
||||
# 显示对话框并等待用户选择
|
||||
result = dialog.exec()
|
||||
|
||||
if result != QDialog.DialogCode.Accepted or list_widget.selectedItems() == []:
|
||||
# 用户取消或未选择任何游戏
|
||||
return []
|
||||
|
||||
# 获取用户选择的游戏
|
||||
return [item.text() for item in list_widget.selectedItems()]
|
||||
28
source/core/managers/__init__.py
Normal file
@@ -0,0 +1,28 @@
|
||||
# Managers package initialization
|
||||
from .ui_manager import UIManager
|
||||
from .download_manager import DownloadManager
|
||||
from .debug_manager import DebugManager
|
||||
from .window_manager import WindowManager
|
||||
from .game_detector import GameDetector
|
||||
from .patch_manager import PatchManager
|
||||
from .config_manager import ConfigManager
|
||||
from .privacy_manager import PrivacyManager
|
||||
from .cloudflare_optimizer import CloudflareOptimizer
|
||||
from .download_task_manager import DownloadTaskManager
|
||||
from .patch_detector import PatchDetector
|
||||
from .animations import MultiStageAnimations
|
||||
|
||||
__all__ = [
|
||||
'UIManager',
|
||||
'DownloadManager',
|
||||
'DebugManager',
|
||||
'WindowManager',
|
||||
'GameDetector',
|
||||
'PatchManager',
|
||||
'ConfigManager',
|
||||
'PrivacyManager',
|
||||
'CloudflareOptimizer',
|
||||
'DownloadTaskManager',
|
||||
'PatchDetector',
|
||||
'MultiStageAnimations',
|
||||
]
|
||||
374
source/core/managers/animations.py
Normal file
@@ -0,0 +1,374 @@
|
||||
import sys
|
||||
from PySide6.QtCore import (QObject, QPropertyAnimation, QParallelAnimationGroup,
|
||||
QPoint, QEasingCurve, QTimer, Signal, QRect)
|
||||
from PySide6.QtWidgets import QGraphicsOpacityEffect, QPushButton
|
||||
from PySide6.QtGui import QColor
|
||||
|
||||
class MultiStageAnimations(QObject):
|
||||
animation_finished = Signal()
|
||||
def __init__(self, ui, parent=None):
|
||||
super().__init__(parent)
|
||||
self.ui = ui
|
||||
self.parent = parent # 保存父窗口引用以获取当前尺寸
|
||||
|
||||
# 获取画布尺寸 - 动态从父窗口获取
|
||||
if parent:
|
||||
self.canvas_width = parent.width()
|
||||
self.canvas_height = parent.height()
|
||||
else:
|
||||
# 默认尺寸
|
||||
self.canvas_width = 1280
|
||||
self.canvas_height = 720
|
||||
|
||||
# 动画时序配置
|
||||
self.animation_config = {
|
||||
"logo": {
|
||||
"delay_after": 2800
|
||||
},
|
||||
"mainbg": {
|
||||
"delay_after": 500
|
||||
},
|
||||
"button_click": {
|
||||
"scale_duration": 100,
|
||||
"scale_min": 0.95,
|
||||
"scale_max": 1.0
|
||||
}
|
||||
}
|
||||
|
||||
# 第一阶段:Logo动画配置,根据新布局调整Y坐标
|
||||
self.logo_widgets = [
|
||||
{"widget": ui.vol1bg, "delay": 0, "duration": 500, "end_pos": QPoint(0, 150)},
|
||||
{"widget": ui.vol2bg, "delay": 80, "duration": 500, "end_pos": QPoint(0, 210)},
|
||||
{"widget": ui.vol3bg, "delay": 160, "duration": 500, "end_pos": QPoint(0, 270)},
|
||||
{"widget": ui.vol4bg, "delay": 240, "duration": 500, "end_pos": QPoint(0, 330)},
|
||||
{"widget": ui.afterbg, "delay": 320, "duration": 500, "end_pos": QPoint(0, 390)}
|
||||
]
|
||||
|
||||
# 第二阶段:菜单元素,位置会在开始动画时动态计算
|
||||
self.menu_widgets = [
|
||||
# 移除菜单背景动画
|
||||
# {"widget": ui.menubg, "end_pos": QPoint(720, 55), "duration": 600},
|
||||
{"widget": ui.button_container, "end_pos": None, "duration": 600},
|
||||
{"widget": ui.toggle_patch_container, "end_pos": None, "duration": 600}, # 添加禁/启用补丁按钮
|
||||
{"widget": ui.uninstall_container, "end_pos": None, "duration": 600}, # 添加卸载补丁按钮
|
||||
{"widget": ui.exit_container, "end_pos": None, "duration": 600}
|
||||
]
|
||||
|
||||
self.animations = []
|
||||
self.timers = []
|
||||
|
||||
# 设置按钮点击动画
|
||||
self.setup_button_click_animations()
|
||||
|
||||
def setup_button_click_animations(self):
|
||||
"""设置按钮点击动画"""
|
||||
# 为开始安装按钮添加点击动画
|
||||
self.ui.start_install_btn.pressed.connect(
|
||||
lambda: self.start_button_click_animation(self.ui.button_container)
|
||||
)
|
||||
self.ui.start_install_btn.released.connect(
|
||||
lambda: self.end_button_click_animation(self.ui.button_container)
|
||||
)
|
||||
|
||||
# 为卸载补丁按钮添加点击动画
|
||||
self.ui.uninstall_btn.pressed.connect(
|
||||
lambda: self.start_button_click_animation(self.ui.uninstall_container)
|
||||
)
|
||||
self.ui.uninstall_btn.released.connect(
|
||||
lambda: self.end_button_click_animation(self.ui.uninstall_container)
|
||||
)
|
||||
|
||||
# 为退出按钮添加点击动画
|
||||
self.ui.exit_btn.pressed.connect(
|
||||
lambda: self.start_button_click_animation(self.ui.exit_container)
|
||||
)
|
||||
self.ui.exit_btn.released.connect(
|
||||
lambda: self.end_button_click_animation(self.ui.exit_container)
|
||||
)
|
||||
|
||||
def start_button_click_animation(self, button_container):
|
||||
"""开始按钮点击动画"""
|
||||
# 创建缩放动画
|
||||
scale_anim = QPropertyAnimation(button_container.children()[0], b"geometry") # 只对按钮背景应用动画
|
||||
scale_anim.setDuration(self.animation_config["button_click"]["scale_duration"])
|
||||
|
||||
# 获取当前几何形状
|
||||
current_geometry = button_container.children()[0].geometry()
|
||||
|
||||
# 计算缩放后的几何形状(保持中心点不变)
|
||||
scale_factor = self.animation_config["button_click"]["scale_min"]
|
||||
width_diff = current_geometry.width() * (1 - scale_factor) / 2
|
||||
height_diff = current_geometry.height() * (1 - scale_factor) / 2
|
||||
|
||||
new_geometry = QRect(
|
||||
current_geometry.x() + width_diff,
|
||||
current_geometry.y() + height_diff,
|
||||
current_geometry.width() * scale_factor,
|
||||
current_geometry.height() * scale_factor
|
||||
)
|
||||
|
||||
scale_anim.setEndValue(new_geometry)
|
||||
scale_anim.setEasingCurve(QEasingCurve.Type.OutQuad)
|
||||
|
||||
# 启动动画
|
||||
scale_anim.start()
|
||||
self.animations.append(scale_anim)
|
||||
|
||||
# 对文本标签也应用同样的动画
|
||||
text_anim = QPropertyAnimation(button_container.children()[1], b"geometry")
|
||||
text_anim.setDuration(self.animation_config["button_click"]["scale_duration"])
|
||||
text_geometry = button_container.children()[1].geometry()
|
||||
|
||||
new_text_geometry = QRect(
|
||||
text_geometry.x() + width_diff,
|
||||
text_geometry.y() + height_diff,
|
||||
text_geometry.width() * scale_factor,
|
||||
text_geometry.height() * scale_factor
|
||||
)
|
||||
|
||||
text_anim.setEndValue(new_text_geometry)
|
||||
text_anim.setEasingCurve(QEasingCurve.Type.OutQuad)
|
||||
text_anim.start()
|
||||
self.animations.append(text_anim)
|
||||
|
||||
def end_button_click_animation(self, button_container):
|
||||
"""结束按钮点击动画,恢复正常外观"""
|
||||
# 创建恢复动画 - 对背景
|
||||
scale_anim = QPropertyAnimation(button_container.children()[0], b"geometry")
|
||||
scale_anim.setDuration(self.animation_config["button_click"]["scale_duration"])
|
||||
|
||||
# 恢复到原始大小 (10,10,191,91)
|
||||
original_geometry = QRect(10, 10, 191, 91)
|
||||
scale_anim.setEndValue(original_geometry)
|
||||
scale_anim.setEasingCurve(QEasingCurve.Type.OutElastic)
|
||||
|
||||
# 启动动画
|
||||
scale_anim.start()
|
||||
self.animations.append(scale_anim)
|
||||
|
||||
# 恢复文本标签
|
||||
text_anim = QPropertyAnimation(button_container.children()[1], b"geometry")
|
||||
text_anim.setDuration(self.animation_config["button_click"]["scale_duration"])
|
||||
|
||||
# 恢复文本到原始大小 (10,7,191,91)
|
||||
text_anim.setEndValue(QRect(10, 7, 191, 91))
|
||||
text_anim.setEasingCurve(QEasingCurve.Type.OutElastic)
|
||||
text_anim.start()
|
||||
self.animations.append(text_anim)
|
||||
|
||||
def initialize(self):
|
||||
"""初始化所有组件状态"""
|
||||
# 更新画布尺寸
|
||||
if self.parent:
|
||||
self.canvas_width = self.parent.width()
|
||||
self.canvas_height = self.parent.height()
|
||||
|
||||
# 设置Mainbg初始状态
|
||||
effect = QGraphicsOpacityEffect(self.ui.Mainbg)
|
||||
effect.setOpacity(0)
|
||||
self.ui.Mainbg.setGraphicsEffect(effect)
|
||||
|
||||
# 初始化Logo位置(移到左侧外)
|
||||
for item in self.logo_widgets:
|
||||
widget = item["widget"]
|
||||
effect = QGraphicsOpacityEffect(widget)
|
||||
effect.setOpacity(0)
|
||||
widget.setGraphicsEffect(effect)
|
||||
widget.move(-widget.width(), item["end_pos"].y())
|
||||
widget.show()
|
||||
print("初始化支持栏动画")
|
||||
|
||||
# 初始化菜单元素(底部外)
|
||||
for item in self.menu_widgets:
|
||||
widget = item["widget"]
|
||||
effect = QGraphicsOpacityEffect(widget)
|
||||
effect.setOpacity(0)
|
||||
widget.setGraphicsEffect(effect)
|
||||
widget.move(widget.x(), self.canvas_height + 100)
|
||||
widget.show()
|
||||
|
||||
# 禁用所有按钮,直到动画完成
|
||||
self.ui.start_install_btn.setEnabled(False)
|
||||
self.ui.uninstall_btn.setEnabled(False)
|
||||
self.ui.exit_btn.setEnabled(False)
|
||||
|
||||
def start_logo_animations(self):
|
||||
"""启动Logo动画序列"""
|
||||
for item in self.logo_widgets:
|
||||
timer = QTimer()
|
||||
timer.setSingleShot(True)
|
||||
timer.timeout.connect(
|
||||
lambda w=item["widget"], d=item["duration"], pos=item["end_pos"]:
|
||||
self.animate_logo(w, pos, d)
|
||||
)
|
||||
timer.start(item["delay"])
|
||||
self.timers.append(timer)
|
||||
|
||||
def animate_logo(self, widget, end_pos, duration):
|
||||
"""执行单个Logo动画"""
|
||||
anim_group = QParallelAnimationGroup()
|
||||
|
||||
# 位置动画
|
||||
pos_anim = QPropertyAnimation(widget, b"pos")
|
||||
pos_anim.setDuration(duration)
|
||||
pos_anim.setStartValue(QPoint(-widget.width(), end_pos.y()))
|
||||
pos_anim.setEndValue(end_pos)
|
||||
pos_anim.setEasingCurve(QEasingCurve.Type.OutBack)
|
||||
|
||||
# 透明度动画
|
||||
opacity_anim = QPropertyAnimation(widget.graphicsEffect(), b"opacity")
|
||||
opacity_anim.setDuration(duration)
|
||||
opacity_anim.setStartValue(0)
|
||||
opacity_anim.setEndValue(1)
|
||||
|
||||
anim_group.addAnimation(pos_anim)
|
||||
anim_group.addAnimation(opacity_anim)
|
||||
|
||||
# 最后一个Logo动画完成后添加延迟
|
||||
if widget == self.logo_widgets[-1]["widget"]:
|
||||
anim_group.finished.connect(
|
||||
lambda: QTimer.singleShot(
|
||||
self.animation_config["logo"]["delay_after"],
|
||||
self.start_mainbg_animation
|
||||
)
|
||||
)
|
||||
|
||||
anim_group.start()
|
||||
self.animations.append(anim_group)
|
||||
|
||||
def start_mainbg_animation(self):
|
||||
"""启动主背景淡入动画(带延迟)"""
|
||||
main_anim = QPropertyAnimation(self.ui.Mainbg.graphicsEffect(), b"opacity")
|
||||
main_anim.setDuration(800)
|
||||
main_anim.setStartValue(0)
|
||||
main_anim.setEndValue(1)
|
||||
main_anim.finished.connect(
|
||||
lambda: QTimer.singleShot(
|
||||
self.animation_config["mainbg"]["delay_after"],
|
||||
self.start_menu_animations
|
||||
)
|
||||
)
|
||||
main_anim.start()
|
||||
self.animations.append(main_anim)
|
||||
def start_menu_animations(self):
|
||||
"""启动菜单动画(从下往上)"""
|
||||
# 更新按钮最终位置
|
||||
self._update_button_positions()
|
||||
|
||||
# 跟踪最后一个动画,用于连接finished信号
|
||||
last_anim = None
|
||||
|
||||
for item in self.menu_widgets:
|
||||
anim_group = QParallelAnimationGroup()
|
||||
|
||||
# 位置动画(从下往上)
|
||||
pos_anim = QPropertyAnimation(item["widget"], b"pos")
|
||||
pos_anim.setDuration(item["duration"])
|
||||
pos_anim.setStartValue(QPoint(item["end_pos"].x(), self.canvas_height + 100))
|
||||
pos_anim.setEndValue(item["end_pos"])
|
||||
pos_anim.setEasingCurve(QEasingCurve.Type.OutBack)
|
||||
|
||||
# 透明度动画
|
||||
opacity_anim = QPropertyAnimation(item["widget"].graphicsEffect(), b"opacity")
|
||||
opacity_anim.setDuration(item["duration"])
|
||||
opacity_anim.setStartValue(0)
|
||||
opacity_anim.setEndValue(1)
|
||||
|
||||
anim_group.addAnimation(pos_anim)
|
||||
anim_group.addAnimation(opacity_anim)
|
||||
|
||||
# 记录最后一个按钮的动画
|
||||
if item["widget"] == self.ui.exit_container:
|
||||
last_anim = anim_group
|
||||
|
||||
anim_group.start()
|
||||
self.animations.append(anim_group)
|
||||
|
||||
# 在最后一个动画完成时发出信号
|
||||
if last_anim:
|
||||
last_anim.finished.connect(self.animation_finished.emit)
|
||||
|
||||
def _update_button_positions(self):
|
||||
"""更新按钮最终位置"""
|
||||
# 根据当前窗口大小动态计算按钮位置
|
||||
if self.parent:
|
||||
width = self.parent.width()
|
||||
height = self.parent.height()
|
||||
|
||||
# 计算按钮位置
|
||||
right_margin = 20 # 减小右边距,使按钮更靠右
|
||||
|
||||
# 开始安装按钮
|
||||
if hasattr(self.ui, 'button_container'):
|
||||
btn_width = self.ui.button_container.width()
|
||||
x_pos = width - btn_width - right_margin
|
||||
y_pos = int((height - 65) * 0.18) - 10 # 从0.28改为0.18,向上移动
|
||||
|
||||
# 更新动画目标位置
|
||||
for item in self.menu_widgets:
|
||||
if item["widget"] == self.ui.button_container:
|
||||
item["end_pos"] = QPoint(x_pos, y_pos)
|
||||
|
||||
# 禁用补丁按钮
|
||||
if hasattr(self.ui, 'toggle_patch_container'):
|
||||
btn_width = self.ui.toggle_patch_container.width()
|
||||
x_pos = width - btn_width - right_margin
|
||||
y_pos = int((height - 65) * 0.36) - 10 # 从0.46改为0.36,向上移动
|
||||
|
||||
# 更新动画目标位置
|
||||
for item in self.menu_widgets:
|
||||
if item["widget"] == self.ui.toggle_patch_container:
|
||||
item["end_pos"] = QPoint(x_pos, y_pos)
|
||||
|
||||
# 卸载补丁按钮
|
||||
if hasattr(self.ui, 'uninstall_container'):
|
||||
btn_width = self.ui.uninstall_container.width()
|
||||
x_pos = width - btn_width - right_margin
|
||||
y_pos = int((height - 65) * 0.54) - 10 # 从0.64改为0.54,向上移动
|
||||
|
||||
# 更新动画目标位置
|
||||
for item in self.menu_widgets:
|
||||
if item["widget"] == self.ui.uninstall_container:
|
||||
item["end_pos"] = QPoint(x_pos, y_pos)
|
||||
|
||||
# 退出按钮
|
||||
if hasattr(self.ui, 'exit_container'):
|
||||
btn_width = self.ui.exit_container.width()
|
||||
x_pos = width - btn_width - right_margin
|
||||
y_pos = int((height - 65) * 0.72) - 10 # 从0.82改为0.72,向上移动
|
||||
|
||||
# 更新动画目标位置
|
||||
for item in self.menu_widgets:
|
||||
if item["widget"] == self.ui.exit_container:
|
||||
item["end_pos"] = QPoint(x_pos, y_pos)
|
||||
else:
|
||||
# 默认位置
|
||||
for item in self.menu_widgets:
|
||||
if item["widget"] == self.ui.button_container:
|
||||
item["end_pos"] = QPoint(1050, 200)
|
||||
elif item["widget"] == self.ui.toggle_patch_container:
|
||||
item["end_pos"] = QPoint(1050, 310)
|
||||
elif item["widget"] == self.ui.uninstall_container:
|
||||
item["end_pos"] = QPoint(1050, 420)
|
||||
elif item["widget"] == self.ui.exit_container:
|
||||
item["end_pos"] = QPoint(1050, 530)
|
||||
|
||||
def start_animations(self):
|
||||
"""启动完整动画序列"""
|
||||
self.clear_animations()
|
||||
|
||||
# 确保按钮在动画开始时被禁用
|
||||
self.ui.start_install_btn.setEnabled(False)
|
||||
self.ui.uninstall_btn.setEnabled(False)
|
||||
self.ui.exit_btn.setEnabled(False)
|
||||
|
||||
self.start_logo_animations()
|
||||
|
||||
def clear_animations(self):
|
||||
"""清理所有动画资源"""
|
||||
for timer in self.timers:
|
||||
timer.stop()
|
||||
for anim in self.animations:
|
||||
anim.stop()
|
||||
self.timers.clear()
|
||||
self.animations.clear()
|
||||
438
source/core/managers/cloudflare_optimizer.py
Normal file
@@ -0,0 +1,438 @@
|
||||
import os
|
||||
from urllib.parse import urlparse
|
||||
from PySide6 import QtWidgets
|
||||
from PySide6.QtCore import Qt, QTimer
|
||||
from PySide6.QtGui import QIcon, QPixmap
|
||||
|
||||
from utils import msgbox_frame, resource_path
|
||||
from workers import IpOptimizerThread
|
||||
from utils.logger import setup_logger
|
||||
|
||||
# 初始化logger
|
||||
logger = setup_logger("cloudflare_optimizer")
|
||||
|
||||
|
||||
class CloudflareOptimizer:
|
||||
"""Cloudflare IP优化器,负责处理IP优化和Cloudflare加速相关功能"""
|
||||
|
||||
def __init__(self, main_window, hosts_manager):
|
||||
"""初始化Cloudflare优化器
|
||||
|
||||
Args:
|
||||
main_window: 主窗口实例,用于访问UI和状态
|
||||
hosts_manager: Hosts文件管理器实例
|
||||
"""
|
||||
self.main_window = main_window
|
||||
self.hosts_manager = hosts_manager
|
||||
self.optimized_ip = None
|
||||
self.optimized_ipv6 = None
|
||||
self.optimization_done = False # 标记是否已执行过优选
|
||||
self.countdown_finished = False # 标记倒计时是否结束
|
||||
self.optimizing_msg_box = None
|
||||
self.optimization_cancelled = False
|
||||
self.ip_optimizer_thread = None
|
||||
self.ipv6_optimizer_thread = None
|
||||
self.has_optimized_in_session = False # 本次启动是否已执行过优选
|
||||
|
||||
def is_optimization_done(self):
|
||||
"""检查是否已完成优化
|
||||
|
||||
Returns:
|
||||
bool: 是否已完成优化
|
||||
"""
|
||||
return self.optimization_done
|
||||
|
||||
def is_countdown_finished(self):
|
||||
"""检查倒计时是否已完成
|
||||
|
||||
Returns:
|
||||
bool: 倒计时是否已完成
|
||||
"""
|
||||
return self.countdown_finished
|
||||
|
||||
def get_optimized_ip(self):
|
||||
"""获取优选的IP地址
|
||||
|
||||
Returns:
|
||||
str: 优选的IP地址,如果未优选则为None
|
||||
"""
|
||||
return self.optimized_ip
|
||||
|
||||
def get_optimized_ipv6(self):
|
||||
"""获取优选的IPv6地址
|
||||
|
||||
Returns:
|
||||
str: 优选的IPv6地址,如果未优选则为None
|
||||
"""
|
||||
return self.optimized_ipv6
|
||||
|
||||
def start_ip_optimization(self, url):
|
||||
"""开始IP优化过程
|
||||
|
||||
Args:
|
||||
url: 用于优化的URL
|
||||
"""
|
||||
# 解析域名
|
||||
hostname = urlparse(url).hostname
|
||||
|
||||
# 判断是否继续优选的逻辑
|
||||
if self.has_optimized_in_session:
|
||||
# 如果本次会话中已执行过优选,则跳过优选过程
|
||||
logger.info("本次会话已执行过优选,跳过优选过程")
|
||||
|
||||
# 设置标记为已优选完成
|
||||
self.optimization_done = True
|
||||
self.countdown_finished = True
|
||||
|
||||
return True
|
||||
else:
|
||||
# 如果本次会话尚未优选过,则清理可能存在的旧记录
|
||||
if hostname:
|
||||
# 检查hosts文件中是否已有该域名的IP记录
|
||||
existing_ips = self.hosts_manager.get_hostname_entries(hostname)
|
||||
if existing_ips:
|
||||
logger.info(f"发现hosts文件中已有域名 {hostname} 的优选IP记录,但本次会话尚未优选过")
|
||||
# 清理已有的hosts记录,准备重新优选
|
||||
self.hosts_manager.clean_hostname_entries(hostname)
|
||||
|
||||
# 创建取消状态标记
|
||||
self.optimization_cancelled = False
|
||||
self.countdown_finished = False
|
||||
|
||||
# 检查是否启用了IPv6
|
||||
use_ipv6 = False
|
||||
if hasattr(self.main_window, 'config'):
|
||||
use_ipv6 = self.main_window.config.get("ipv6_enabled", False)
|
||||
|
||||
# 如果启用了IPv6,显示警告消息
|
||||
if use_ipv6:
|
||||
ipv6_warning = QtWidgets.QMessageBox(self.main_window)
|
||||
ipv6_warning.setWindowTitle(f"IPv6优选警告 - {self.main_window.APP_NAME}")
|
||||
ipv6_warning.setText("\nIPv6优选比IPv4耗时更长且感知不强(预计耗时10分钟以上),不建议使用。\n\n确定要同时执行IPv6优选吗?\n")
|
||||
ipv6_warning.setIcon(QtWidgets.QMessageBox.Icon.Warning)
|
||||
|
||||
# 设置图标
|
||||
icon_path = resource_path(os.path.join("IMG", "ICO", "icon.png"))
|
||||
if os.path.exists(icon_path):
|
||||
pixmap = QPixmap(icon_path)
|
||||
if not pixmap.isNull():
|
||||
ipv6_warning.setWindowIcon(QIcon(pixmap))
|
||||
|
||||
yes_button = ipv6_warning.addButton("是", QtWidgets.QMessageBox.ButtonRole.YesRole)
|
||||
no_button = ipv6_warning.addButton("否,仅使用IPv4", QtWidgets.QMessageBox.ButtonRole.NoRole)
|
||||
cancel_button = ipv6_warning.addButton("取消优选", QtWidgets.QMessageBox.ButtonRole.RejectRole)
|
||||
|
||||
ipv6_warning.setDefaultButton(no_button)
|
||||
ipv6_warning.exec()
|
||||
|
||||
if ipv6_warning.clickedButton() == cancel_button:
|
||||
# 用户取消了优选
|
||||
self.optimization_cancelled = True
|
||||
return
|
||||
|
||||
# 根据用户选择调整IPv6设置
|
||||
if ipv6_warning.clickedButton() == no_button:
|
||||
use_ipv6 = False
|
||||
# 临时覆盖配置(不保存到文件)
|
||||
if hasattr(self.main_window, 'config'):
|
||||
self.main_window.config["ipv6_enabled"] = False
|
||||
|
||||
# 准备提示信息
|
||||
optimization_msg = "\n正在优选Cloudflare IP,请稍候...\n\n"
|
||||
if use_ipv6:
|
||||
optimization_msg += "已启用IPv6支持,同时进行IPv4和IPv6优选。\n这可能需要10分钟以上,请耐心等待喵~\n"
|
||||
else:
|
||||
optimization_msg += "这可能需要5-10分钟,请耐心等待喵~\n"
|
||||
|
||||
# 使用Cloudflare图标创建消息框
|
||||
self.optimizing_msg_box = msgbox_frame(
|
||||
f"通知 - {self.main_window.APP_NAME}",
|
||||
optimization_msg
|
||||
)
|
||||
# 设置Cloudflare图标
|
||||
cf_icon_path = resource_path("IMG/ICO/cloudflare_logo_icon.ico")
|
||||
if os.path.exists(cf_icon_path):
|
||||
cf_pixmap = QPixmap(cf_icon_path)
|
||||
if not cf_pixmap.isNull():
|
||||
self.optimizing_msg_box.setWindowIcon(QIcon(cf_pixmap))
|
||||
self.optimizing_msg_box.setIconPixmap(cf_pixmap.scaled(64, 64, Qt.AspectRatioMode.KeepAspectRatio,
|
||||
Qt.TransformationMode.SmoothTransformation))
|
||||
|
||||
# 添加取消按钮
|
||||
self.optimizing_msg_box.setStandardButtons(QtWidgets.QMessageBox.StandardButton.Cancel)
|
||||
self.optimizing_msg_box.buttonClicked.connect(self._on_optimization_dialog_clicked)
|
||||
self.optimizing_msg_box.setWindowModality(Qt.WindowModality.ApplicationModal)
|
||||
|
||||
# 创建并启动优化线程
|
||||
self.ip_optimizer_thread = IpOptimizerThread(url)
|
||||
self.ip_optimizer_thread.finished.connect(self.on_ipv4_optimization_finished)
|
||||
|
||||
# 如果启用IPv6,同时启动IPv6优化线程
|
||||
if use_ipv6:
|
||||
logger.info("IPv6已启用,将同时优选IPv6地址")
|
||||
self.ipv6_optimizer_thread = IpOptimizerThread(url, use_ipv6=True)
|
||||
self.ipv6_optimizer_thread.finished.connect(self.on_ipv6_optimization_finished)
|
||||
self.ipv6_optimizer_thread.start()
|
||||
|
||||
# 启动IPv4优化线程
|
||||
self.ip_optimizer_thread.start()
|
||||
|
||||
# 显示消息框(非模态,不阻塞)
|
||||
self.optimizing_msg_box.open()
|
||||
|
||||
def _on_optimization_dialog_clicked(self, button):
|
||||
"""处理优化对话框按钮点击
|
||||
|
||||
Args:
|
||||
button: 被点击的按钮
|
||||
"""
|
||||
if button.text() == "Cancel": # 如果是取消按钮
|
||||
# 标记已取消
|
||||
self.optimization_cancelled = True
|
||||
|
||||
# 停止优化线程
|
||||
if hasattr(self, 'ip_optimizer_thread') and self.ip_optimizer_thread and self.ip_optimizer_thread.isRunning():
|
||||
self.ip_optimizer_thread.stop()
|
||||
|
||||
if hasattr(self, 'ipv6_optimizer_thread') and self.ipv6_optimizer_thread and self.ipv6_optimizer_thread.isRunning():
|
||||
self.ipv6_optimizer_thread.stop()
|
||||
|
||||
# 恢复主窗口状态
|
||||
self.main_window.setEnabled(True)
|
||||
self.main_window.ui.start_install_text.setText("开始安装")
|
||||
|
||||
# 显示取消消息
|
||||
QtWidgets.QMessageBox.information(
|
||||
self.main_window,
|
||||
f"已取消 - {self.main_window.APP_NAME}",
|
||||
"\n已取消IP优选和安装过程。\n"
|
||||
)
|
||||
|
||||
def on_ipv4_optimization_finished(self, ip):
|
||||
"""IPv4优化完成后的处理
|
||||
|
||||
Args:
|
||||
ip: 优选的IP地址,如果失败则为空字符串
|
||||
"""
|
||||
# 如果已经取消,则不继续处理
|
||||
if hasattr(self, 'optimization_cancelled') and self.optimization_cancelled:
|
||||
return
|
||||
|
||||
self.optimized_ip = ip
|
||||
logger.info(f"IPv4优选完成,结果: {ip if ip else '未找到合适的IP'}")
|
||||
|
||||
# 检查是否还有IPv6优化正在运行
|
||||
if hasattr(self, 'ipv6_optimizer_thread') and self.ipv6_optimizer_thread and self.ipv6_optimizer_thread.isRunning():
|
||||
logger.info("等待IPv6优选完成...")
|
||||
return
|
||||
|
||||
# 所有优选都已完成,继续处理
|
||||
self.optimization_done = True
|
||||
self.countdown_finished = False # 确保倒计时标志重置
|
||||
|
||||
# 关闭提示框
|
||||
if hasattr(self, 'optimizing_msg_box') and self.optimizing_msg_box:
|
||||
if self.optimizing_msg_box.isVisible():
|
||||
self.optimizing_msg_box.accept()
|
||||
self.optimizing_msg_box = None
|
||||
|
||||
# 处理优选结果
|
||||
self._process_optimization_results()
|
||||
|
||||
def on_ipv6_optimization_finished(self, ipv6):
|
||||
"""IPv6优化完成后的处理
|
||||
|
||||
Args:
|
||||
ipv6: 优选的IPv6地址,如果失败则为空字符串
|
||||
"""
|
||||
# 如果已经取消,则不继续处理
|
||||
if hasattr(self, 'optimization_cancelled') and self.optimization_cancelled:
|
||||
return
|
||||
|
||||
self.optimized_ipv6 = ipv6
|
||||
logger.info(f"IPv6优选完成,结果: {ipv6 if ipv6 else '未找到合适的IPv6'}")
|
||||
|
||||
# 检查IPv4优化是否已完成
|
||||
if hasattr(self, 'ip_optimizer_thread') and self.ip_optimizer_thread and self.ip_optimizer_thread.isRunning():
|
||||
logger.info("等待IPv4优选完成...")
|
||||
return
|
||||
|
||||
# 所有优选都已完成,继续处理
|
||||
self.optimization_done = True
|
||||
self.countdown_finished = False # 确保倒计时标志重置
|
||||
|
||||
# 关闭提示框
|
||||
if hasattr(self, 'optimizing_msg_box') and self.optimizing_msg_box:
|
||||
if self.optimizing_msg_box.isVisible():
|
||||
self.optimizing_msg_box.accept()
|
||||
self.optimizing_msg_box = None
|
||||
|
||||
# 处理优选结果
|
||||
self._process_optimization_results()
|
||||
|
||||
def _process_optimization_results(self):
|
||||
"""处理优选的IP结果,显示相应提示"""
|
||||
# 无论优选结果如何,都标记本次会话已执行过优选
|
||||
self.has_optimized_in_session = True
|
||||
|
||||
use_ipv6 = False
|
||||
if hasattr(self.main_window, 'config'):
|
||||
use_ipv6 = self.main_window.config.get("ipv6_enabled", False)
|
||||
|
||||
# 判断优选结果
|
||||
ipv4_success = bool(self.optimized_ip)
|
||||
ipv6_success = bool(self.optimized_ipv6) if use_ipv6 else False
|
||||
|
||||
# 临时启用窗口以显示对话框
|
||||
self.main_window.setEnabled(True)
|
||||
|
||||
hostname = urlparse(self.main_window.current_url).hostname if hasattr(self.main_window, 'current_url') else None
|
||||
|
||||
if not ipv4_success and (not use_ipv6 or not ipv6_success):
|
||||
# 两种IP都没有优选成功
|
||||
msg_box = QtWidgets.QMessageBox(self.main_window)
|
||||
msg_box.setWindowTitle(f"优选失败 - {self.main_window.APP_NAME}")
|
||||
|
||||
fail_message = "\n未能找到合适的Cloudflare "
|
||||
if use_ipv6:
|
||||
fail_message += "IPv4和IPv6地址"
|
||||
else:
|
||||
fail_message += "IP地址"
|
||||
|
||||
fail_message += ",将使用默认网络进行下载。\n\n10秒后自动继续..."
|
||||
|
||||
msg_box.setText(fail_message)
|
||||
msg_box.setIcon(QtWidgets.QMessageBox.Icon.Warning)
|
||||
ok_button = msg_box.addButton("确定 (10)", QtWidgets.QMessageBox.ButtonRole.AcceptRole)
|
||||
cancel_button = msg_box.addButton("取消安装", QtWidgets.QMessageBox.ButtonRole.RejectRole)
|
||||
|
||||
# 创建计时器实现倒计时
|
||||
countdown = 10
|
||||
timer = QTimer(self.main_window)
|
||||
|
||||
def update_countdown():
|
||||
nonlocal countdown
|
||||
countdown -= 1
|
||||
ok_button.setText(f"确定 ({countdown})")
|
||||
if countdown <= 0:
|
||||
timer.stop()
|
||||
if msg_box.isVisible():
|
||||
msg_box.accept()
|
||||
|
||||
timer.timeout.connect(update_countdown)
|
||||
timer.start(1000) # 每秒更新一次
|
||||
|
||||
# 显示对话框并等待用户响应
|
||||
result = msg_box.exec()
|
||||
|
||||
# 停止计时器
|
||||
timer.stop()
|
||||
|
||||
# 如果用户点击了取消安装
|
||||
if msg_box.clickedButton() == cancel_button:
|
||||
# 恢复主窗口状态
|
||||
self.main_window.setEnabled(True)
|
||||
self.main_window.ui.start_install_text.setText("开始安装")
|
||||
return False
|
||||
|
||||
# 用户点击了继续,重新禁用主窗口
|
||||
self.main_window.setEnabled(False)
|
||||
# 标记倒计时已完成
|
||||
self.countdown_finished = True
|
||||
return True
|
||||
else:
|
||||
# 至少有一种IP优选成功
|
||||
success_message = ""
|
||||
if ipv4_success:
|
||||
success_message += f"IPv4: {self.optimized_ip}\n"
|
||||
|
||||
if ipv6_success:
|
||||
success_message += f"IPv6: {self.optimized_ipv6}\n"
|
||||
|
||||
if hostname:
|
||||
# 先清理可能存在的旧记录(只清理一次)
|
||||
self.hosts_manager.clean_hostname_entries(hostname)
|
||||
|
||||
success = False
|
||||
|
||||
# 应用优选IP到hosts文件
|
||||
if ipv4_success:
|
||||
success = self.hosts_manager.apply_ip(hostname, self.optimized_ip, clean=False) or success
|
||||
|
||||
# 如果启用IPv6并且找到了IPv6地址,也应用到hosts
|
||||
if ipv6_success:
|
||||
success = self.hosts_manager.apply_ip(hostname, self.optimized_ipv6, clean=False) or success
|
||||
|
||||
# 记录此次优选操作对hosts文件进行了更新
|
||||
if hasattr(self.main_window, 'config'):
|
||||
self.main_window.config['last_hosts_optimized_hostname'] = hostname
|
||||
from utils import save_config
|
||||
save_config(self.main_window.config)
|
||||
|
||||
if success:
|
||||
msg_box = QtWidgets.QMessageBox(self.main_window)
|
||||
msg_box.setWindowTitle(f"成功 - {self.main_window.APP_NAME}")
|
||||
msg_box.setText(f"\n已将优选IP应用到hosts文件:\n{success_message}\n10秒后自动继续...")
|
||||
msg_box.setIcon(QtWidgets.QMessageBox.Icon.Information)
|
||||
ok_button = msg_box.addButton("确定 (10)", QtWidgets.QMessageBox.ButtonRole.AcceptRole)
|
||||
cancel_button = msg_box.addButton("取消安装", QtWidgets.QMessageBox.ButtonRole.RejectRole)
|
||||
|
||||
# 创建计时器实现倒计时
|
||||
countdown = 10
|
||||
timer = QTimer(self.main_window)
|
||||
|
||||
def update_countdown():
|
||||
nonlocal countdown
|
||||
countdown -= 1
|
||||
ok_button.setText(f"确定 ({countdown})")
|
||||
if countdown <= 0:
|
||||
timer.stop()
|
||||
if msg_box.isVisible():
|
||||
msg_box.accept()
|
||||
|
||||
timer.timeout.connect(update_countdown)
|
||||
timer.start(1000) # 每秒更新一次
|
||||
|
||||
# 显示对话框并等待用户响应
|
||||
result = msg_box.exec()
|
||||
|
||||
# 停止计时器
|
||||
timer.stop()
|
||||
|
||||
# 如果用户点击了取消安装
|
||||
if msg_box.clickedButton() == cancel_button:
|
||||
# 恢复主窗口状态
|
||||
self.main_window.setEnabled(True)
|
||||
self.main_window.ui.start_install_text.setText("开始安装")
|
||||
return False
|
||||
else:
|
||||
QtWidgets.QMessageBox.critical(
|
||||
self.main_window,
|
||||
f"错误 - {self.main_window.APP_NAME}",
|
||||
"\n修改hosts文件失败,请检查程序是否以管理员权限运行。\n"
|
||||
)
|
||||
# 恢复主窗口状态
|
||||
self.main_window.ui.start_install_text.setText("开始安装")
|
||||
return False
|
||||
|
||||
# 用户点击了继续,重新禁用主窗口
|
||||
self.main_window.setEnabled(False)
|
||||
# 标记倒计时已完成
|
||||
self.countdown_finished = True
|
||||
|
||||
return True
|
||||
|
||||
def stop_optimization(self):
|
||||
"""停止正在进行的IP优化"""
|
||||
if hasattr(self, 'ip_optimizer_thread') and self.ip_optimizer_thread and self.ip_optimizer_thread.isRunning():
|
||||
self.ip_optimizer_thread.stop()
|
||||
self.ip_optimizer_thread.wait()
|
||||
|
||||
if hasattr(self, 'ipv6_optimizer_thread') and self.ipv6_optimizer_thread and self.ipv6_optimizer_thread.isRunning():
|
||||
self.ipv6_optimizer_thread.stop()
|
||||
self.ipv6_optimizer_thread.wait()
|
||||
|
||||
if hasattr(self, 'optimizing_msg_box') and self.optimizing_msg_box:
|
||||
if self.optimizing_msg_box.isVisible():
|
||||
self.optimizing_msg_box.accept()
|
||||
self.optimizing_msg_box = None
|
||||
213
source/core/managers/config_manager.py
Normal file
@@ -0,0 +1,213 @@
|
||||
import json
|
||||
import webbrowser
|
||||
from PySide6.QtWidgets import QMessageBox
|
||||
|
||||
from utils import load_config, save_config, msgbox_frame
|
||||
|
||||
class ConfigManager:
|
||||
"""配置管理器,用于处理配置的加载、保存和获取云端配置"""
|
||||
|
||||
def __init__(self, app_name, config_url, ua, debug_manager=None):
|
||||
"""初始化配置管理器
|
||||
|
||||
Args:
|
||||
app_name: 应用程序名称,用于显示消息框标题
|
||||
config_url: 云端配置URL
|
||||
ua: User-Agent字符串
|
||||
debug_manager: 调试管理器实例,用于输出调试信息
|
||||
"""
|
||||
self.app_name = app_name
|
||||
self.config_url = config_url
|
||||
self.ua = ua
|
||||
self.debug_manager = debug_manager
|
||||
self.cloud_config = None
|
||||
self.config_valid = False
|
||||
self.last_error_message = ""
|
||||
|
||||
def _is_debug_mode(self):
|
||||
"""检查是否处于调试模式
|
||||
|
||||
Returns:
|
||||
bool: 是否处于调试模式
|
||||
"""
|
||||
if hasattr(self.debug_manager, 'ui_manager') and hasattr(self.debug_manager.ui_manager, 'debug_action'):
|
||||
return self.debug_manager.ui_manager.debug_action.isChecked()
|
||||
return False
|
||||
|
||||
def load_config(self):
|
||||
"""加载本地配置
|
||||
|
||||
Returns:
|
||||
dict: 加载的配置
|
||||
"""
|
||||
return load_config()
|
||||
|
||||
def save_config(self, config):
|
||||
"""保存配置
|
||||
|
||||
Args:
|
||||
config: 要保存的配置
|
||||
"""
|
||||
save_config(config)
|
||||
|
||||
def fetch_cloud_config(self, config_fetch_thread_class, callback=None):
|
||||
"""获取云端配置
|
||||
|
||||
Args:
|
||||
config_fetch_thread_class: 用于获取云端配置的线程类
|
||||
callback: 获取完成后的回调函数,接受两个参数(data, error_message)
|
||||
"""
|
||||
headers = {"User-Agent": self.ua}
|
||||
debug_mode = self._is_debug_mode()
|
||||
self.config_fetch_thread = config_fetch_thread_class(self.config_url, headers, debug_mode)
|
||||
|
||||
# 如果提供了回调,使用它;否则使用内部的on_config_fetched方法
|
||||
if callback:
|
||||
self.config_fetch_thread.finished.connect(callback)
|
||||
else:
|
||||
self.config_fetch_thread.finished.connect(self.on_config_fetched)
|
||||
|
||||
self.config_fetch_thread.start()
|
||||
|
||||
def on_config_fetched(self, data, error_message):
|
||||
"""云端配置获取完成的回调处理
|
||||
|
||||
Args:
|
||||
data: 获取到的配置数据
|
||||
error_message: 错误信息,如果有
|
||||
"""
|
||||
debug_mode = self._is_debug_mode()
|
||||
|
||||
if error_message:
|
||||
# 标记配置无效
|
||||
self.config_valid = False
|
||||
|
||||
# 记录错误信息,用于按钮点击时显示
|
||||
if error_message == "update_required":
|
||||
self.last_error_message = "update_required"
|
||||
|
||||
# 检查是否处于离线模式
|
||||
is_offline_mode = False
|
||||
if hasattr(self.debug_manager, 'main_window') and hasattr(self.debug_manager.main_window, 'offline_mode_manager'):
|
||||
is_offline_mode = self.debug_manager.main_window.offline_mode_manager.is_in_offline_mode()
|
||||
|
||||
if is_offline_mode:
|
||||
# 离线模式下只显示提示,不禁用开始安装按钮
|
||||
msg_box = msgbox_frame(
|
||||
f"更新提示 - {self.app_name}",
|
||||
"\n当前版本过低,请及时更新。\n在离线模式下,您仍可使用禁用/启用补丁、卸载补丁和离线安装功能。\n",
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
msg_box.exec()
|
||||
# 移除在浏览器中打开项目主页的代码
|
||||
# 离线模式下版本过低,仍然允许使用安装按钮
|
||||
return {"action": "enable_button"}
|
||||
else:
|
||||
# 在线模式下显示强制更新提示
|
||||
msg_box = msgbox_frame(
|
||||
f"更新提示 - {self.app_name}",
|
||||
"\n当前版本过低,请及时更新。\n如需联网下载补丁,请更新到最新版,否则无法下载。\n",
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
msg_box.exec()
|
||||
# 移除在浏览器中打开项目主页的代码
|
||||
# 在线模式下版本过低,但不直接禁用按钮,而是在点击时提示
|
||||
return {"action": "enable_button", "version_warning": True}
|
||||
|
||||
elif "missing_keys" in error_message:
|
||||
self.last_error_message = "missing_keys"
|
||||
missing_versions = error_message.split(":")[1]
|
||||
msg_box = msgbox_frame(
|
||||
f"配置缺失 - {self.app_name}",
|
||||
f'\n云端缺失下载链接,可能云服务器正在维护,不影响其他版本下载。\n当前缺失版本:"{missing_versions}"\n',
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
msg_box.exec()
|
||||
# 对于部分缺失,仍然允许使用,因为可能只影响部分游戏版本
|
||||
self.config_valid = True
|
||||
return {"action": "enable_button"}
|
||||
else:
|
||||
# 设置网络错误标记
|
||||
self.last_error_message = "network_error"
|
||||
|
||||
# 显示通用错误消息,只在debug模式下显示详细错误
|
||||
error_msg = "访问云端配置失败,请检查网络状况或稍后再试。"
|
||||
if debug_mode and "详细错误:" in error_message:
|
||||
msg_box = msgbox_frame(
|
||||
f"错误 - {self.app_name}",
|
||||
f"\n{error_message}\n",
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
else:
|
||||
msg_box = msgbox_frame(
|
||||
f"错误 - {self.app_name}",
|
||||
f"\n{error_msg}\n",
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
msg_box.exec()
|
||||
|
||||
# 网络错误时仍然允许使用按钮,用户可以尝试离线模式
|
||||
return {"action": "enable_button"}
|
||||
else:
|
||||
self.cloud_config = data
|
||||
# 标记配置有效
|
||||
self.config_valid = True
|
||||
# 清除错误信息
|
||||
self.last_error_message = ""
|
||||
|
||||
if debug_mode:
|
||||
print("--- Cloud config fetched successfully ---")
|
||||
# 创建一个数据副本,隐藏敏感URL
|
||||
safe_data = self._create_safe_config_for_logging(data)
|
||||
print(json.dumps(safe_data, indent=2))
|
||||
|
||||
# 获取配置成功,允许安装
|
||||
return {"action": "enable_button"}
|
||||
|
||||
def _create_safe_config_for_logging(self, config_data):
|
||||
"""创建用于日志记录的安全配置副本,隐藏敏感URL
|
||||
|
||||
Args:
|
||||
config_data: 原始配置数据
|
||||
|
||||
Returns:
|
||||
dict: 安全的配置数据副本
|
||||
"""
|
||||
if not config_data or not isinstance(config_data, dict):
|
||||
return config_data
|
||||
|
||||
# 创建深拷贝,避免修改原始数据
|
||||
import copy
|
||||
safe_config = copy.deepcopy(config_data)
|
||||
|
||||
# 隐藏敏感URL
|
||||
for key in safe_config:
|
||||
if isinstance(safe_config[key], dict) and "url" in safe_config[key]:
|
||||
# 完全隐藏URL
|
||||
safe_config[key]["url"] = "***URL protection***"
|
||||
|
||||
return safe_config
|
||||
|
||||
def is_config_valid(self):
|
||||
"""检查配置是否有效
|
||||
|
||||
Returns:
|
||||
bool: 配置是否有效
|
||||
"""
|
||||
return self.config_valid
|
||||
|
||||
def get_cloud_config(self):
|
||||
"""获取云端配置
|
||||
|
||||
Returns:
|
||||
dict: 云端配置
|
||||
"""
|
||||
return self.cloud_config
|
||||
|
||||
def get_last_error(self):
|
||||
"""获取最后一次错误信息
|
||||
|
||||
Returns:
|
||||
str: 错误信息
|
||||
"""
|
||||
return self.last_error_message
|
||||
149
source/core/managers/debug_manager.py
Normal file
@@ -0,0 +1,149 @@
|
||||
import os
|
||||
import sys
|
||||
from PySide6 import QtWidgets
|
||||
from data.config import LOG_FILE
|
||||
from utils.logger import setup_logger
|
||||
from utils import Logger
|
||||
import datetime
|
||||
from data.config import APP_NAME
|
||||
|
||||
# 初始化logger
|
||||
logger = setup_logger("debug_manager")
|
||||
|
||||
class DebugManager:
|
||||
def __init__(self, main_window):
|
||||
"""初始化调试管理器
|
||||
|
||||
Args:
|
||||
main_window: 主窗口实例
|
||||
"""
|
||||
self.main_window = main_window
|
||||
self.logger = None
|
||||
self.original_stdout = None
|
||||
self.original_stderr = None
|
||||
self.ui_manager = None # 添加ui_manager属性
|
||||
|
||||
def set_ui_manager(self, ui_manager):
|
||||
"""设置UI管理器引用
|
||||
|
||||
Args:
|
||||
ui_manager: UI管理器实例
|
||||
"""
|
||||
self.ui_manager = ui_manager
|
||||
|
||||
def _is_debug_mode(self):
|
||||
"""检查是否处于调试模式
|
||||
|
||||
Returns:
|
||||
bool: 是否处于调试模式
|
||||
"""
|
||||
try:
|
||||
# 首先尝试从UI管理器获取状态
|
||||
if hasattr(self, 'ui_manager') and self.ui_manager and hasattr(self.ui_manager, 'debug_action') and self.ui_manager.debug_action:
|
||||
return self.ui_manager.debug_action.isChecked()
|
||||
|
||||
# 如果UI管理器还没准备好,尝试从配置中获取
|
||||
if hasattr(self.main_window, 'config') and isinstance(self.main_window.config, dict):
|
||||
return self.main_window.config.get('debug_mode', False)
|
||||
|
||||
# 如果以上都不可行,返回False
|
||||
return False
|
||||
except Exception:
|
||||
# 捕获任何异常,默认返回False
|
||||
return False
|
||||
|
||||
def toggle_debug_mode(self, checked):
|
||||
"""切换调试模式
|
||||
|
||||
Args:
|
||||
checked: 是否启用调试模式
|
||||
"""
|
||||
logger.info(f"Toggle debug mode: {checked}")
|
||||
self.main_window.config["debug_mode"] = checked
|
||||
self.main_window.save_config(self.main_window.config)
|
||||
|
||||
# 创建或删除debug_mode.txt标记文件
|
||||
try:
|
||||
from data.config import CACHE
|
||||
debug_file = os.path.join(os.path.dirname(CACHE), "debug_mode.txt")
|
||||
|
||||
if checked:
|
||||
# 确保目录存在
|
||||
os.makedirs(os.path.dirname(debug_file), exist_ok=True)
|
||||
# 创建标记文件
|
||||
with open(debug_file, 'w', encoding='utf-8') as f:
|
||||
f.write(f"Debug mode enabled at {os.path.abspath(debug_file)}\n")
|
||||
logger.info(f"已创建调试模式标记文件: {debug_file}")
|
||||
elif os.path.exists(debug_file):
|
||||
# 删除标记文件
|
||||
os.remove(debug_file)
|
||||
logger.info(f"已删除调试模式标记文件: {debug_file}")
|
||||
except Exception as e:
|
||||
logger.warning(f"处理调试模式标记文件时发生错误: {e}")
|
||||
|
||||
# 更新打开log文件按钮状态
|
||||
if hasattr(self, 'ui_manager') and hasattr(self.ui_manager, 'open_log_action'):
|
||||
self.ui_manager.open_log_action.setEnabled(checked)
|
||||
|
||||
if checked:
|
||||
self.start_logging()
|
||||
|
||||
# 如果启用了调试模式,检查是否需要强制启用离线模式
|
||||
if hasattr(self.main_window, 'offline_mode_manager'):
|
||||
# 检查配置中是否已设置离线模式
|
||||
offline_mode_enabled = self.main_window.config.get("offline_mode", False)
|
||||
|
||||
# 如果配置中已设置离线模式,则在调试模式下强制启用
|
||||
if offline_mode_enabled:
|
||||
logger.debug("DEBUG: 调试模式下强制启用离线模式")
|
||||
self.main_window.offline_mode_manager.set_offline_mode(True)
|
||||
|
||||
# 更新UI中的离线模式选项
|
||||
if hasattr(self.ui_manager, 'offline_mode_action') and self.ui_manager.offline_mode_action:
|
||||
self.ui_manager.offline_mode_action.setChecked(True)
|
||||
self.ui_manager.online_mode_action.setChecked(False)
|
||||
else:
|
||||
self.stop_logging()
|
||||
|
||||
def start_logging(self):
|
||||
"""启动日志记录"""
|
||||
if self.logger is None:
|
||||
try:
|
||||
# 确保log目录存在
|
||||
log_dir = os.path.dirname(LOG_FILE)
|
||||
if not os.path.exists(log_dir):
|
||||
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.abspath(LOG_FILE)}")
|
||||
|
||||
# 保存原始的 stdout 和 stderr
|
||||
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:
|
||||
QtWidgets.QMessageBox.critical(self.main_window, "错误", f"无法创建日志文件: {e}")
|
||||
self.logger = None
|
||||
|
||||
def stop_logging(self):
|
||||
"""停止日志记录"""
|
||||
if self.logger:
|
||||
logger.info("--- Debug mode disabled ---")
|
||||
sys.stdout = self.original_stdout
|
||||
sys.stderr = self.original_stderr
|
||||
self.logger.close()
|
||||
self.logger = None
|
||||
1061
source/core/managers/download_manager.py
Normal file
221
source/core/managers/download_task_manager.py
Normal file
@@ -0,0 +1,221 @@
|
||||
from PySide6 import QtWidgets
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtWidgets import QDialog, QVBoxLayout, QRadioButton, QPushButton, QLabel, QButtonGroup, QHBoxLayout
|
||||
from PySide6.QtGui import QFont
|
||||
|
||||
from data.config import DOWNLOAD_THREADS
|
||||
|
||||
|
||||
class DownloadTaskManager:
|
||||
"""下载任务管理器,负责管理下载任务和线程设置"""
|
||||
|
||||
def __init__(self, main_window, download_thread_level="medium"):
|
||||
"""初始化下载任务管理器
|
||||
|
||||
Args:
|
||||
main_window: 主窗口实例,用于访问UI和状态
|
||||
download_thread_level: 下载线程级别,默认为"medium"
|
||||
"""
|
||||
self.main_window = main_window
|
||||
self.APP_NAME = main_window.APP_NAME if hasattr(main_window, 'APP_NAME') else ""
|
||||
self.current_download_thread = None
|
||||
self.download_thread_level = download_thread_level
|
||||
|
||||
def start_download(self, url, _7z_path, game_version, game_folder, plugin_path):
|
||||
"""启动下载线程
|
||||
|
||||
Args:
|
||||
url: 下载URL
|
||||
_7z_path: 7z文件保存路径
|
||||
game_version: 游戏版本名称
|
||||
game_folder: 游戏文件夹路径
|
||||
plugin_path: 插件路径
|
||||
"""
|
||||
# 按钮在file_dialog中已设置为禁用状态
|
||||
|
||||
# 创建并连接下载线程
|
||||
self.current_download_thread = self.main_window.create_download_thread(url, _7z_path, game_version)
|
||||
self.current_download_thread.progress.connect(self.main_window.progress_window.update_progress)
|
||||
self.current_download_thread.finished.connect(
|
||||
lambda success, error: self.main_window.download_manager.on_download_finished(
|
||||
success,
|
||||
error,
|
||||
url,
|
||||
game_folder,
|
||||
game_version,
|
||||
_7z_path,
|
||||
plugin_path,
|
||||
)
|
||||
)
|
||||
|
||||
# 连接停止按钮到download_manager的on_download_stopped方法
|
||||
self.main_window.progress_window.stop_button.clicked.connect(self.main_window.download_manager.on_download_stopped)
|
||||
|
||||
# 连接暂停/恢复按钮
|
||||
self.main_window.progress_window.pause_resume_button.clicked.connect(self.toggle_download_pause)
|
||||
|
||||
# 启动线程和显示进度窗口
|
||||
self.current_download_thread.start()
|
||||
self.main_window.progress_window.exec()
|
||||
|
||||
def toggle_download_pause(self):
|
||||
"""切换下载的暂停/恢复状态"""
|
||||
if not self.current_download_thread:
|
||||
return
|
||||
|
||||
# 获取当前暂停状态
|
||||
is_paused = self.current_download_thread.is_paused()
|
||||
|
||||
if is_paused:
|
||||
# 如果已暂停,则恢复下载
|
||||
success = self.current_download_thread.resume()
|
||||
if success:
|
||||
self.main_window.progress_window.update_pause_button_state(False)
|
||||
else:
|
||||
# 如果未暂停,则暂停下载
|
||||
success = self.current_download_thread.pause()
|
||||
if success:
|
||||
self.main_window.progress_window.update_pause_button_state(True)
|
||||
|
||||
def get_download_thread_count(self):
|
||||
"""获取当前下载线程设置对应的线程数
|
||||
|
||||
Returns:
|
||||
int: 下载线程数
|
||||
"""
|
||||
# 获取当前线程级别对应的线程数
|
||||
thread_count = DOWNLOAD_THREADS.get(self.download_thread_level, DOWNLOAD_THREADS["medium"])
|
||||
return thread_count
|
||||
|
||||
def set_download_thread_level(self, level):
|
||||
"""设置下载线程级别
|
||||
|
||||
Args:
|
||||
level: 线程级别 (low, medium, high, extreme, insane)
|
||||
|
||||
Returns:
|
||||
bool: 设置是否成功
|
||||
"""
|
||||
if level in DOWNLOAD_THREADS:
|
||||
old_level = self.download_thread_level
|
||||
self.download_thread_level = level
|
||||
|
||||
# 只有非极端级别才保存到配置
|
||||
if level not in ["extreme", "insane"]:
|
||||
if hasattr(self.main_window, 'config'):
|
||||
self.main_window.config["download_thread_level"] = level
|
||||
self.main_window.save_config(self.main_window.config)
|
||||
|
||||
return True
|
||||
return False
|
||||
|
||||
def show_download_thread_settings(self):
|
||||
"""显示下载线程设置对话框"""
|
||||
# 创建对话框
|
||||
dialog = QDialog(self.main_window)
|
||||
dialog.setWindowTitle(f"下载线程设置 - {self.APP_NAME}")
|
||||
dialog.setMinimumWidth(350)
|
||||
|
||||
layout = QVBoxLayout(dialog)
|
||||
|
||||
# 添加说明标签
|
||||
info_label = QLabel("选择下载线程数量(更多线程通常可以提高下载速度):", dialog)
|
||||
info_label.setWordWrap(True)
|
||||
layout.addWidget(info_label)
|
||||
|
||||
# 创建按钮组
|
||||
button_group = QButtonGroup(dialog)
|
||||
|
||||
# 添加线程选项
|
||||
thread_options = {
|
||||
"low": f"低速 - {DOWNLOAD_THREADS['low']}线程(慢慢来,不着急)",
|
||||
"medium": f"中速 - {DOWNLOAD_THREADS['medium']}线程(快人半步)",
|
||||
"high": f"高速 - {DOWNLOAD_THREADS['high']}线程(默认,推荐配置)",
|
||||
"extreme": f"极速 - {DOWNLOAD_THREADS['extreme']}线程(如果你对你的网和电脑很自信的话)",
|
||||
"insane": f"狂暴 - {DOWNLOAD_THREADS['insane']}线程(看看是带宽和性能先榨干还是牛牛先榨干)"
|
||||
}
|
||||
|
||||
radio_buttons = {}
|
||||
|
||||
for level, text in thread_options.items():
|
||||
radio = QRadioButton(text, dialog)
|
||||
|
||||
# 选中当前使用的线程级别
|
||||
if level == self.download_thread_level:
|
||||
radio.setChecked(True)
|
||||
|
||||
button_group.addButton(radio)
|
||||
layout.addWidget(radio)
|
||||
radio_buttons[level] = radio
|
||||
|
||||
layout.addSpacing(10)
|
||||
|
||||
# 添加按钮区域
|
||||
btn_layout = QHBoxLayout()
|
||||
|
||||
ok_button = QPushButton("确定", dialog)
|
||||
cancel_button = QPushButton("取消", dialog)
|
||||
|
||||
btn_layout.addWidget(ok_button)
|
||||
btn_layout.addWidget(cancel_button)
|
||||
|
||||
layout.addLayout(btn_layout)
|
||||
|
||||
# 连接按钮事件
|
||||
ok_button.clicked.connect(dialog.accept)
|
||||
cancel_button.clicked.connect(dialog.reject)
|
||||
|
||||
# 显示对话框
|
||||
result = dialog.exec()
|
||||
|
||||
# 处理结果
|
||||
if result == QDialog.DialogCode.Accepted:
|
||||
# 获取用户选择的线程级别
|
||||
selected_level = None
|
||||
for level, radio in radio_buttons.items():
|
||||
if radio.isChecked():
|
||||
selected_level = level
|
||||
break
|
||||
|
||||
if selected_level:
|
||||
# 为极速和狂暴模式显示警告
|
||||
if selected_level in ["extreme", "insane"]:
|
||||
warning_result = QtWidgets.QMessageBox.warning(
|
||||
self.main_window,
|
||||
f"高风险警告 - {self.APP_NAME}",
|
||||
"警告!过高的线程数可能导致CPU负载过高或其他恶性问题!\n你确定要这么做吗?",
|
||||
QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No,
|
||||
QtWidgets.QMessageBox.StandardButton.No
|
||||
)
|
||||
|
||||
if warning_result != QtWidgets.QMessageBox.StandardButton.Yes:
|
||||
return False
|
||||
|
||||
success = self.set_download_thread_level(selected_level)
|
||||
|
||||
if success:
|
||||
# 显示设置成功消息
|
||||
thread_count = DOWNLOAD_THREADS[selected_level]
|
||||
message = f"\n已成功设置下载线程为: {thread_count}线程\n"
|
||||
|
||||
# 对于极速和狂暴模式,添加仅本次生效的提示
|
||||
if selected_level in ["extreme", "insane"]:
|
||||
message += "\n注意:极速/狂暴模式仅本次生效。软件重启后将恢复默认设置。\n"
|
||||
|
||||
QtWidgets.QMessageBox.information(
|
||||
self.main_window,
|
||||
f"设置成功 - {self.APP_NAME}",
|
||||
message
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def stop_download(self):
|
||||
"""停止当前下载线程"""
|
||||
if self.current_download_thread and self.current_download_thread.isRunning():
|
||||
self.current_download_thread.stop()
|
||||
self.current_download_thread.wait() # 等待线程完全终止
|
||||
return True
|
||||
return False
|
||||
354
source/core/managers/game_detector.py
Normal file
@@ -0,0 +1,354 @@
|
||||
from PySide6.QtCore import QThread, Signal
|
||||
import os
|
||||
import re
|
||||
from utils.logger import setup_logger
|
||||
|
||||
class GameDetectionThread(QThread):
|
||||
"""用于在后台线程中执行游戏目录识别的线程"""
|
||||
finished = Signal(dict)
|
||||
|
||||
def __init__(self, detector_func, selected_folder):
|
||||
super().__init__()
|
||||
self.detector_func = detector_func
|
||||
self.selected_folder = selected_folder
|
||||
|
||||
def run(self):
|
||||
result = self.detector_func(self.selected_folder)
|
||||
self.finished.emit(result)
|
||||
|
||||
class GameDetector:
|
||||
"""游戏检测器,用于识别游戏目录和版本"""
|
||||
|
||||
def __init__(self, game_info, debug_manager=None):
|
||||
"""初始化游戏检测器
|
||||
|
||||
Args:
|
||||
game_info: 游戏信息字典,包含各版本的安装路径和可执行文件名
|
||||
debug_manager: 调试管理器实例,用于输出调试信息
|
||||
"""
|
||||
self.game_info = game_info
|
||||
self.debug_manager = debug_manager
|
||||
self.directory_cache = {} # 添加目录缓存
|
||||
self.logger = setup_logger("game_detector")
|
||||
self.detection_thread = None
|
||||
|
||||
def identify_game_directories_async(self, selected_folder, callback):
|
||||
"""异步识别游戏目录"""
|
||||
def on_finished(game_dirs):
|
||||
callback(game_dirs)
|
||||
self.detection_thread = None
|
||||
|
||||
self.detection_thread = GameDetectionThread(self.identify_game_directories_improved, selected_folder)
|
||||
self.detection_thread.finished.connect(on_finished)
|
||||
self.detection_thread.start()
|
||||
|
||||
def _is_debug_mode(self):
|
||||
"""检查是否处于调试模式
|
||||
|
||||
Returns:
|
||||
bool: 是否处于调试模式
|
||||
"""
|
||||
if hasattr(self.debug_manager, 'ui_manager') and hasattr(self.debug_manager.ui_manager, 'debug_action'):
|
||||
return self.debug_manager.ui_manager.debug_action.isChecked()
|
||||
return False
|
||||
|
||||
def identify_game_version(self, game_dir):
|
||||
"""识别游戏版本
|
||||
|
||||
Args:
|
||||
game_dir: 游戏目录路径
|
||||
|
||||
Returns:
|
||||
str: 游戏版本名称,如果不是有效的游戏目录则返回None
|
||||
"""
|
||||
debug_mode = self._is_debug_mode()
|
||||
|
||||
if debug_mode:
|
||||
self.logger.debug(f"尝试识别游戏版本: {game_dir}")
|
||||
|
||||
# 先通过目录名称进行初步推测(这将作为递归搜索的提示)
|
||||
dir_name = os.path.basename(game_dir).lower()
|
||||
potential_version = None
|
||||
vol_num = None
|
||||
|
||||
# 提取卷号或判断是否是After
|
||||
if "vol" in dir_name or "vol." in dir_name:
|
||||
vol_match = re.search(r"vol(?:\.|\s*)?(\d+)", dir_name)
|
||||
if vol_match:
|
||||
vol_num = vol_match.group(1)
|
||||
potential_version = f"NEKOPARA Vol.{vol_num}"
|
||||
if debug_mode:
|
||||
self.logger.debug(f"从目录名推测游戏版本: {potential_version}, 卷号: {vol_num}")
|
||||
elif "after" in dir_name:
|
||||
potential_version = "NEKOPARA After"
|
||||
if debug_mode:
|
||||
self.logger.debug(f"从目录名推测游戏版本: NEKOPARA After")
|
||||
|
||||
# 检查是否为NEKOPARA游戏目录
|
||||
# 通过检查游戏可执行文件来识别游戏版本
|
||||
for game_version, info in self.game_info.items():
|
||||
# 尝试多种可能的可执行文件名变体
|
||||
exe_variants = [
|
||||
info["exe"], # 标准文件名
|
||||
info["exe"] + ".nocrack", # Steam加密版本
|
||||
info["exe"].replace(".exe", ""), # 无扩展名版本
|
||||
info["exe"].replace("NEKOPARA", "nekopara").lower(), # 全小写变体
|
||||
info["exe"].lower(), # 小写变体
|
||||
info["exe"].lower() + ".nocrack", # 小写变体的Steam加密版本
|
||||
]
|
||||
|
||||
# 对于Vol.3可能有特殊名称
|
||||
if "Vol.3" in game_version:
|
||||
# 增加可能的卷3特定的变体
|
||||
exe_variants.extend([
|
||||
"NEKOPARAVol3.exe",
|
||||
"NEKOPARAVol3.exe.nocrack",
|
||||
"nekoparavol3.exe",
|
||||
"nekoparavol3.exe.nocrack",
|
||||
"nekopara_vol3.exe",
|
||||
"nekopara_vol3.exe.nocrack",
|
||||
"vol3.exe",
|
||||
"vol3.exe.nocrack"
|
||||
])
|
||||
|
||||
for exe_variant in exe_variants:
|
||||
exe_path = os.path.join(game_dir, exe_variant)
|
||||
if os.path.exists(exe_path):
|
||||
if debug_mode:
|
||||
self.logger.debug(f"通过可执行文件确认游戏版本: {game_version}, 文件: {exe_variant}")
|
||||
return game_version
|
||||
|
||||
# 如果没有直接匹配,尝试递归搜索
|
||||
if potential_version:
|
||||
# 从预测的版本中获取卷号或确认是否是After
|
||||
is_after = "After" in potential_version
|
||||
if not vol_num and not is_after:
|
||||
vol_match = re.search(r"Vol\.(\d+)", potential_version)
|
||||
if vol_match:
|
||||
vol_num = vol_match.group(1)
|
||||
|
||||
# 递归搜索可执行文件
|
||||
for root, dirs, files in os.walk(game_dir):
|
||||
for file in files:
|
||||
file_lower = file.lower()
|
||||
if file.endswith('.exe') or file.endswith('.exe.nocrack'):
|
||||
# 检查文件名中是否包含卷号或关键词
|
||||
if ((vol_num and (f"vol{vol_num}" in file_lower or
|
||||
f"vol.{vol_num}" in file_lower or
|
||||
f"vol {vol_num}" in file_lower)) or
|
||||
(is_after and "after" in file_lower)):
|
||||
if debug_mode:
|
||||
self.logger.debug(f"通过递归搜索确认游戏版本: {potential_version}, 文件: {file}")
|
||||
return potential_version
|
||||
|
||||
# 如果仍然没有找到,基于目录名的推测返回结果
|
||||
if potential_version:
|
||||
if debug_mode:
|
||||
self.logger.debug(f"基于目录名返回推测的游戏版本: {potential_version}")
|
||||
return potential_version
|
||||
|
||||
if debug_mode:
|
||||
self.logger.debug(f"无法识别游戏版本: {game_dir}")
|
||||
|
||||
return None
|
||||
|
||||
def identify_game_directories_improved(self, selected_folder):
|
||||
"""改进的游戏目录识别,支持大小写不敏感和特殊字符处理
|
||||
|
||||
Args:
|
||||
selected_folder: 选择的上级目录
|
||||
|
||||
Returns:
|
||||
dict: 游戏版本到游戏目录的映射
|
||||
"""
|
||||
debug_mode = self._is_debug_mode()
|
||||
|
||||
# 检查缓存中是否已有该目录的识别结果
|
||||
if selected_folder in self.directory_cache:
|
||||
if debug_mode:
|
||||
self.logger.debug(f"使用缓存的目录识别结果: {selected_folder}")
|
||||
return self.directory_cache[selected_folder]
|
||||
|
||||
if debug_mode:
|
||||
self.logger.debug(f"--- 开始识别目录: {selected_folder} ---")
|
||||
|
||||
game_paths = {}
|
||||
|
||||
# 获取上级目录中的所有文件夹
|
||||
try:
|
||||
all_dirs = [d for d in os.listdir(selected_folder) if os.path.isdir(os.path.join(selected_folder, d))]
|
||||
if debug_mode:
|
||||
self.logger.debug(f"找到以下子目录: {all_dirs}")
|
||||
except Exception as e:
|
||||
if debug_mode:
|
||||
self.logger.debug(f"无法读取目录 {selected_folder}: {str(e)}")
|
||||
return {}
|
||||
|
||||
for game, info in self.game_info.items():
|
||||
expected_dir = info["install_path"].split("/")[0] # 例如 "NEKOPARA Vol. 1"
|
||||
expected_exe = info["exe"] # 标准可执行文件名
|
||||
|
||||
if debug_mode:
|
||||
self.logger.debug(f"搜索游戏 {game}, 预期目录: {expected_dir}, 预期可执行文件: {expected_exe}")
|
||||
|
||||
# 尝试不同的匹配方法
|
||||
found_dir = None
|
||||
|
||||
# 1. 精确匹配
|
||||
if expected_dir in all_dirs:
|
||||
found_dir = expected_dir
|
||||
if debug_mode:
|
||||
self.logger.debug(f"精确匹配成功: {expected_dir}")
|
||||
|
||||
# 2. 大小写不敏感匹配
|
||||
if not found_dir:
|
||||
for dir_name in all_dirs:
|
||||
if expected_dir.lower() == dir_name.lower():
|
||||
found_dir = dir_name
|
||||
if debug_mode:
|
||||
self.logger.debug(f"大小写不敏感匹配成功: {dir_name}")
|
||||
break
|
||||
|
||||
# 3. 更模糊的匹配(允许特殊字符差异)
|
||||
if not found_dir:
|
||||
# 准备用于模糊匹配的正则表达式模式
|
||||
# 替换空格为可选空格或连字符,替换点为可选点
|
||||
pattern_text = expected_dir.replace(" ", "[ -]?").replace(".", "\\.?")
|
||||
pattern = re.compile(f"^{pattern_text}$", re.IGNORECASE)
|
||||
|
||||
for dir_name in all_dirs:
|
||||
if pattern.match(dir_name):
|
||||
found_dir = dir_name
|
||||
if debug_mode:
|
||||
self.logger.debug(f"模糊匹配成功: {dir_name} 匹配模式 {pattern_text}")
|
||||
break
|
||||
|
||||
# 4. 如果还是没找到,尝试更宽松的匹配
|
||||
if not found_dir:
|
||||
vol_match = re.search(r"vol(?:\.|\s*)?(\d+)", expected_dir, re.IGNORECASE)
|
||||
vol_num = None
|
||||
if vol_match:
|
||||
vol_num = vol_match.group(1)
|
||||
if debug_mode:
|
||||
self.logger.debug(f"提取卷号: {vol_num}")
|
||||
|
||||
is_after = "after" in expected_dir.lower()
|
||||
|
||||
for dir_name in all_dirs:
|
||||
dir_lower = dir_name.lower()
|
||||
|
||||
# 对于After特殊处理
|
||||
if is_after and "after" in dir_lower:
|
||||
found_dir = dir_name
|
||||
if debug_mode:
|
||||
self.logger.debug(f"After特殊匹配成功: {dir_name}")
|
||||
break
|
||||
|
||||
# 对于Vol特殊处理
|
||||
if vol_num:
|
||||
# 查找目录名中的卷号
|
||||
dir_vol_match = re.search(r"vol(?:\.|\s*)?(\d+)", dir_lower)
|
||||
if dir_vol_match and dir_vol_match.group(1) == vol_num:
|
||||
found_dir = dir_name
|
||||
if debug_mode:
|
||||
self.logger.debug(f"卷号匹配成功: {dir_name} 卷号 {vol_num}")
|
||||
break
|
||||
|
||||
# 如果找到匹配的目录,验证exe文件是否存在
|
||||
if found_dir:
|
||||
potential_path = os.path.join(selected_folder, found_dir)
|
||||
|
||||
# 尝试多种可能的可执行文件名变体
|
||||
# 包括Steam加密版本和其他可能的变体
|
||||
exe_variants = [
|
||||
expected_exe, # 标准文件名
|
||||
expected_exe + ".nocrack", # Steam加密版本
|
||||
expected_exe.replace(".exe", ""),# 无扩展名版本
|
||||
# Vol.3的特殊变体,因为它的文件名可能不一样
|
||||
expected_exe.replace("NEKOPARA", "nekopara").lower(), # 全小写变体
|
||||
expected_exe.lower(), # 小写变体
|
||||
expected_exe.lower() + ".nocrack", # 小写变体的Steam加密版本
|
||||
]
|
||||
|
||||
# 对于Vol.3可能有特殊名称
|
||||
if "Vol.3" in game:
|
||||
# 增加可能的卷3特定的变体
|
||||
exe_variants.extend([
|
||||
"NEKOPARAVol3.exe",
|
||||
"NEKOPARAVol3.exe.nocrack",
|
||||
"nekoparavol3.exe",
|
||||
"nekoparavol3.exe.nocrack",
|
||||
"nekopara_vol3.exe",
|
||||
"nekopara_vol3.exe.nocrack",
|
||||
"vol3.exe",
|
||||
"vol3.exe.nocrack"
|
||||
])
|
||||
|
||||
exe_exists = False
|
||||
found_exe = None
|
||||
|
||||
# 尝试所有可能的变体
|
||||
for exe_variant in exe_variants:
|
||||
exe_path = os.path.join(potential_path, exe_variant)
|
||||
if os.path.exists(exe_path):
|
||||
exe_exists = True
|
||||
found_exe = exe_variant
|
||||
if debug_mode:
|
||||
self.logger.debug(f"验证成功,找到游戏可执行文件: {exe_variant}")
|
||||
break
|
||||
|
||||
# 如果没有直接找到,尝试递归搜索当前目录下的所有可执行文件
|
||||
if not exe_exists:
|
||||
# 遍历当前目录下的所有文件和文件夹
|
||||
for root, dirs, files in os.walk(potential_path):
|
||||
for file in files:
|
||||
file_lower = file.lower()
|
||||
# 检查是否是游戏可执行文件(根据关键字)
|
||||
if file.endswith('.exe') or file.endswith('.exe.nocrack'):
|
||||
# 检查文件名中是否包含卷号或关键词
|
||||
if "Vol." in game:
|
||||
vol_match = re.search(r"Vol\.(\d+)", game)
|
||||
if vol_match:
|
||||
vol_num = vol_match.group(1)
|
||||
if (f"vol{vol_num}" in file_lower or
|
||||
f"vol.{vol_num}" in file_lower or
|
||||
f"vol {vol_num}" in file_lower):
|
||||
exe_path = os.path.join(root, file)
|
||||
exe_exists = True
|
||||
found_exe = os.path.relpath(exe_path, potential_path)
|
||||
if debug_mode:
|
||||
self.logger.debug(f"通过递归搜索找到游戏可执行文件: {found_exe}")
|
||||
break
|
||||
elif "After" in game and "after" in file_lower:
|
||||
exe_path = os.path.join(root, file)
|
||||
exe_exists = True
|
||||
found_exe = os.path.relpath(exe_path, potential_path)
|
||||
if debug_mode:
|
||||
self.logger.debug(f"通过递归搜索找到After游戏可执行文件: {found_exe}")
|
||||
break
|
||||
if exe_exists:
|
||||
break
|
||||
|
||||
# 如果找到了可执行文件,将该目录添加到游戏目录列表
|
||||
if exe_exists:
|
||||
game_paths[game] = potential_path
|
||||
if debug_mode:
|
||||
self.logger.debug(f"验证成功,将 {potential_path} 添加为 {game} 的目录")
|
||||
else:
|
||||
if debug_mode:
|
||||
self.logger.debug(f"未找到任何可执行文件变体,游戏 {game} 在 {potential_path} 未找到")
|
||||
|
||||
if debug_mode:
|
||||
self.logger.debug(f"最终识别的游戏目录: {game_paths}")
|
||||
self.logger.debug(f"--- 目录识别结束 ---")
|
||||
|
||||
# 将识别结果存入缓存
|
||||
self.directory_cache[selected_folder] = game_paths
|
||||
|
||||
return game_paths
|
||||
|
||||
def clear_directory_cache(self):
|
||||
"""清除目录缓存"""
|
||||
self.directory_cache = {}
|
||||
if self._is_debug_mode():
|
||||
self.logger.debug("已清除目录缓存")
|
||||
323
source/core/managers/ipv6_manager.py
Normal file
@@ -0,0 +1,323 @@
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import subprocess
|
||||
import urllib.request
|
||||
import ssl
|
||||
import threading
|
||||
from PySide6.QtCore import QObject, Signal
|
||||
from PySide6.QtWidgets import QDialog, QVBoxLayout, QLabel, QPushButton, QTextEdit, QProgressBar, QMessageBox
|
||||
|
||||
from data.config import APP_NAME
|
||||
from utils import msgbox_frame
|
||||
|
||||
|
||||
class IPv6Manager:
|
||||
"""管理IPv6相关功能的类"""
|
||||
|
||||
def __init__(self, main_window):
|
||||
"""初始化IPv6管理器
|
||||
|
||||
Args:
|
||||
main_window: 主窗口实例,用于显示对话框和访问配置
|
||||
"""
|
||||
self.main_window = main_window
|
||||
self.config = getattr(main_window, 'config', {})
|
||||
|
||||
def check_ipv6_availability(self):
|
||||
"""检查IPv6是否可用
|
||||
|
||||
通过访问IPv6专用图片URL测试IPv6连接
|
||||
|
||||
Returns:
|
||||
bool: IPv6是否可用
|
||||
"""
|
||||
import urllib.request
|
||||
import time
|
||||
|
||||
print("开始检测IPv6可用性...")
|
||||
|
||||
try:
|
||||
# 获取IPv6测试请求
|
||||
ipv6_test_url, req, context = self._get_ipv6_test_request()
|
||||
|
||||
# 设置3秒超时,避免长时间等待
|
||||
start_time = time.time()
|
||||
with urllib.request.urlopen(req, timeout=3, context=context) as response:
|
||||
# 读取图片数据
|
||||
image_data = response.read()
|
||||
|
||||
# 检查是否成功
|
||||
if response.status == 200 and len(image_data) > 0:
|
||||
elapsed = time.time() - start_time
|
||||
print(f"IPv6测试成功! 用时: {elapsed:.2f}秒")
|
||||
return True
|
||||
else:
|
||||
print(f"IPv6测试失败: 状态码 {response.status}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"IPv6测试失败: {e}")
|
||||
return False
|
||||
|
||||
def _get_ipv6_test_request(self):
|
||||
"""获取IPv6测试请求
|
||||
|
||||
Returns:
|
||||
tuple: (测试URL, 请求对象, SSL上下文)
|
||||
"""
|
||||
import urllib.request
|
||||
import ssl
|
||||
|
||||
# IPv6测试URL - 这是一个只能通过IPv6访问的资源
|
||||
ipv6_test_url = "https://ipv6.testipv6.cn/images-nc/knob_green.png?&testdomain=www.test-ipv6.com&testname=sites"
|
||||
|
||||
# 创建SSL上下文
|
||||
context = ssl._create_unverified_context()
|
||||
|
||||
# 创建请求并添加常见的HTTP头
|
||||
req = urllib.request.Request(ipv6_test_url)
|
||||
req.add_header('User-Agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)')
|
||||
req.add_header('Accept', 'image/webp,image/apng,image/*,*/*;q=0.8')
|
||||
|
||||
return ipv6_test_url, req, context
|
||||
|
||||
def get_ipv6_address(self):
|
||||
"""获取公网IPv6地址
|
||||
|
||||
Returns:
|
||||
str: IPv6地址,如果失败则返回None
|
||||
"""
|
||||
try:
|
||||
# 使用curl命令获取IPv6地址
|
||||
process = subprocess.Popen(
|
||||
["curl", "-6", "6.ipw.cn"],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
encoding='utf-8',
|
||||
errors='replace',
|
||||
creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0
|
||||
)
|
||||
|
||||
# 设置超时
|
||||
timeout = 5 # 5秒超时
|
||||
start_time = time.time()
|
||||
while process.poll() is None and (time.time() - start_time) < timeout:
|
||||
time.sleep(0.1)
|
||||
|
||||
# 如果进程仍在运行,则强制终止
|
||||
if process.poll() is None:
|
||||
process.terminate()
|
||||
print("获取IPv6地址超时")
|
||||
return None
|
||||
|
||||
stdout, stderr = process.communicate()
|
||||
|
||||
if process.returncode == 0 and stdout.strip():
|
||||
ipv6_address = stdout.strip()
|
||||
print(f"获取到IPv6地址: {ipv6_address}")
|
||||
return ipv6_address
|
||||
else:
|
||||
print("未能获取到IPv6地址")
|
||||
if stderr:
|
||||
print(f"错误信息: {stderr}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
print(f"获取IPv6地址失败: {e}")
|
||||
return None
|
||||
|
||||
def show_ipv6_details(self):
|
||||
"""显示IPv6连接详情"""
|
||||
class SignalEmitter(QObject):
|
||||
update_signal = Signal(str)
|
||||
complete_signal = Signal(bool, float)
|
||||
|
||||
# 创建对话框
|
||||
dialog = QDialog(self.main_window)
|
||||
dialog.setWindowTitle(f"IPv6连接测试 - {APP_NAME}")
|
||||
dialog.resize(500, 300)
|
||||
|
||||
# 创建布局
|
||||
layout = QVBoxLayout(dialog)
|
||||
|
||||
# 创建状态标签
|
||||
status_label = QLabel("正在测试IPv6连接...", dialog)
|
||||
layout.addWidget(status_label)
|
||||
|
||||
# 创建进度条
|
||||
progress = QProgressBar(dialog)
|
||||
progress.setRange(0, 0) # 不确定进度
|
||||
layout.addWidget(progress)
|
||||
|
||||
# 创建结果文本框
|
||||
result_text = QTextEdit(dialog)
|
||||
result_text.setReadOnly(True)
|
||||
layout.addWidget(result_text)
|
||||
|
||||
# 创建关闭按钮
|
||||
close_button = QPushButton("关闭", dialog)
|
||||
close_button.clicked.connect(dialog.accept)
|
||||
close_button.setEnabled(False) # 测试完成前禁用
|
||||
layout.addWidget(close_button)
|
||||
|
||||
# 信号发射器
|
||||
signal_emitter = SignalEmitter()
|
||||
|
||||
# 连接信号
|
||||
signal_emitter.update_signal.connect(
|
||||
lambda text: result_text.append(text)
|
||||
)
|
||||
|
||||
def on_test_complete(success, elapsed_time):
|
||||
# 停止进度条动画
|
||||
progress.setRange(0, 100)
|
||||
progress.setValue(100 if success else 0)
|
||||
|
||||
# 更新状态
|
||||
if success:
|
||||
status_label.setText(f"IPv6连接测试完成: 可用 (用时: {elapsed_time:.2f}秒)")
|
||||
else:
|
||||
status_label.setText("IPv6连接测试完成: 不可用")
|
||||
|
||||
# 启用关闭按钮
|
||||
close_button.setEnabled(True)
|
||||
|
||||
signal_emitter.complete_signal.connect(on_test_complete)
|
||||
|
||||
# 测试函数
|
||||
def test_ipv6():
|
||||
try:
|
||||
signal_emitter.update_signal.emit("正在测试IPv6连接,请稍候...")
|
||||
|
||||
# 先进行标准的IPv6连接测试
|
||||
signal_emitter.update_signal.emit("正在进行标准IPv6连接测试...")
|
||||
|
||||
# 使用IPv6测试URL
|
||||
ipv6_test_url, req, context = self._get_ipv6_test_request()
|
||||
ipv6_connected = False
|
||||
ipv6_test_elapsed_time = 0
|
||||
|
||||
try:
|
||||
# 设置5秒超时
|
||||
start_time = time.time()
|
||||
signal_emitter.update_signal.emit(f"开始连接: {ipv6_test_url}")
|
||||
|
||||
# 尝试下载图片
|
||||
with urllib.request.urlopen(req, timeout=5, context=context) as response:
|
||||
image_data = response.read()
|
||||
|
||||
# 计算耗时
|
||||
elapsed_time = time.time() - start_time
|
||||
ipv6_test_elapsed_time = elapsed_time
|
||||
|
||||
# 检查是否成功
|
||||
if response.status == 200 and len(image_data) > 0:
|
||||
ipv6_connected = True
|
||||
signal_emitter.update_signal.emit(f"✓ 成功! 已下载 {len(image_data)} 字节")
|
||||
signal_emitter.update_signal.emit(f"✓ 响应时间: {elapsed_time:.2f}秒")
|
||||
else:
|
||||
signal_emitter.update_signal.emit(f"✗ 失败: 状态码 {response.status}")
|
||||
signal_emitter.update_signal.emit("\n结论: 您的网络不支持IPv6连接 ✗")
|
||||
signal_emitter.complete_signal.emit(False, 0)
|
||||
return
|
||||
|
||||
except Exception as e:
|
||||
signal_emitter.update_signal.emit(f"✗ 连接失败: {e}")
|
||||
signal_emitter.update_signal.emit("\n结论: 您的网络不支持IPv6连接 ✗")
|
||||
signal_emitter.complete_signal.emit(False, 0)
|
||||
return
|
||||
|
||||
# 如果IPv6连接测试成功,再尝试获取公网IPv6地址
|
||||
if ipv6_connected:
|
||||
signal_emitter.update_signal.emit("\n正在获取您的公网IPv6地址...")
|
||||
|
||||
try:
|
||||
# 使用curl命令获取IPv6地址
|
||||
process = subprocess.Popen(
|
||||
["curl", "-6", "6.ipw.cn"],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
encoding='utf-8',
|
||||
errors='replace',
|
||||
creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0
|
||||
)
|
||||
|
||||
# 设置超时
|
||||
timeout = 5 # 5秒超时
|
||||
start_time = time.time()
|
||||
while process.poll() is None and (time.time() - start_time) < timeout:
|
||||
time.sleep(0.1)
|
||||
|
||||
# 如果进程仍在运行,则强制终止
|
||||
if process.poll() is None:
|
||||
process.terminate()
|
||||
signal_emitter.update_signal.emit("✗ 获取IPv6地址超时")
|
||||
else:
|
||||
stdout, stderr = process.communicate()
|
||||
|
||||
if process.returncode == 0 and stdout.strip():
|
||||
ipv6_address = stdout.strip()
|
||||
signal_emitter.update_signal.emit(f"✓ 获取到的IPv6地址: {ipv6_address}")
|
||||
else:
|
||||
signal_emitter.update_signal.emit("✗ 未能获取到IPv6地址")
|
||||
if stderr:
|
||||
signal_emitter.update_signal.emit(f"错误信息: {stderr}")
|
||||
|
||||
except Exception as e:
|
||||
signal_emitter.update_signal.emit(f"✗ 获取IPv6地址失败: {e}")
|
||||
|
||||
# 输出最终结论
|
||||
signal_emitter.update_signal.emit("\n结论: 您的网络支持IPv6连接 ✓")
|
||||
signal_emitter.complete_signal.emit(True, ipv6_test_elapsed_time)
|
||||
return
|
||||
|
||||
except Exception as e:
|
||||
signal_emitter.update_signal.emit(f"测试过程中出错: {e}")
|
||||
signal_emitter.complete_signal.emit(False, 0)
|
||||
|
||||
# 启动测试线程
|
||||
threading.Thread(target=test_ipv6, daemon=True).start()
|
||||
|
||||
# 显示对话框
|
||||
dialog.exec()
|
||||
|
||||
def toggle_ipv6_support(self, enabled):
|
||||
"""切换IPv6支持
|
||||
|
||||
Args:
|
||||
enabled: 是否启用IPv6支持
|
||||
"""
|
||||
print(f"Toggle IPv6 support: {enabled}")
|
||||
|
||||
# 保存设置到配置
|
||||
if self.config is not None:
|
||||
self.config["ipv6_enabled"] = enabled
|
||||
# 直接使用utils.save_config保存配置
|
||||
from utils import save_config
|
||||
save_config(self.config)
|
||||
|
||||
# 显示设置已保存的消息
|
||||
status = "启用" if enabled else "禁用"
|
||||
msg_box = self._create_message_box("IPv6设置", f"\nIPv6支持已{status}。新的设置将在下一次下载时生效。\n")
|
||||
msg_box.exec()
|
||||
return True
|
||||
|
||||
def _create_message_box(self, title, message, buttons=QMessageBox.StandardButton.Ok):
|
||||
"""创建统一风格的消息框
|
||||
|
||||
Args:
|
||||
title: 消息框标题
|
||||
message: 消息内容
|
||||
buttons: 按钮类型,默认为确定按钮
|
||||
|
||||
Returns:
|
||||
QMessageBox: 配置好的消息框实例
|
||||
"""
|
||||
msg_box = msgbox_frame(
|
||||
f"{title} - {APP_NAME}",
|
||||
message,
|
||||
buttons,
|
||||
)
|
||||
return msg_box
|
||||
992
source/core/managers/offline_mode_manager.py
Normal file
@@ -0,0 +1,992 @@
|
||||
import os
|
||||
import hashlib
|
||||
import shutil
|
||||
import tempfile
|
||||
import py7zr
|
||||
import traceback
|
||||
from PySide6 import QtWidgets, QtCore
|
||||
from PySide6.QtCore import QTimer
|
||||
from PySide6.QtWidgets import QMessageBox
|
||||
|
||||
from data.config import PLUGIN, PLUGIN_HASH, GAME_INFO
|
||||
from utils import msgbox_frame
|
||||
from utils.logger import setup_logger
|
||||
|
||||
# 初始化logger
|
||||
logger = setup_logger("offline_mode_manager")
|
||||
|
||||
class OfflineModeManager:
|
||||
"""离线模式管理器,用于管理离线模式下的补丁安装和检测"""
|
||||
|
||||
def __init__(self, main_window):
|
||||
"""初始化离线模式管理器
|
||||
|
||||
Args:
|
||||
main_window: 主窗口实例,用于访问UI和状态
|
||||
"""
|
||||
self.main_window = main_window
|
||||
self.app_name = main_window.APP_NAME if hasattr(main_window, 'APP_NAME') else ""
|
||||
self.offline_patches = {} # 存储离线补丁信息 {补丁名称: 文件路径}
|
||||
self.is_offline_mode = False
|
||||
self.installed_games = [] # 跟踪本次实际安装的游戏
|
||||
# 保持对哈希线程的引用,避免运行中被销毁
|
||||
self.hash_thread = None
|
||||
# 解压线程与进度窗口引用,避免运行中被销毁,且确保UI可更新
|
||||
self.extraction_thread = None
|
||||
self.extraction_progress_window = None
|
||||
|
||||
def _is_debug_mode(self):
|
||||
"""检查是否处于调试模式
|
||||
|
||||
Returns:
|
||||
bool: 是否处于调试模式
|
||||
"""
|
||||
try:
|
||||
if hasattr(self.main_window, 'debug_manager') and self.main_window.debug_manager:
|
||||
if hasattr(self.main_window.debug_manager, '_is_debug_mode'):
|
||||
# 尝试直接从debug_manager获取状态
|
||||
return self.main_window.debug_manager._is_debug_mode()
|
||||
elif hasattr(self.main_window, 'config'):
|
||||
# 如果debug_manager还没准备好,尝试从配置中获取
|
||||
return self.main_window.config.get('debug_mode', False)
|
||||
# 如果以上都不可行,返回False
|
||||
return False
|
||||
except Exception:
|
||||
# 捕获任何异常,默认返回False
|
||||
return False
|
||||
|
||||
def scan_for_offline_patches(self, directory=None):
|
||||
"""扫描指定目录(默认为软件所在目录)查找离线补丁文件
|
||||
|
||||
Args:
|
||||
directory: 要扫描的目录,如果为None则使用软件所在目录
|
||||
|
||||
Returns:
|
||||
dict: 找到的补丁文件 {补丁名称: 文件路径}
|
||||
"""
|
||||
if directory is None:
|
||||
# 获取软件所在目录
|
||||
directory = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
debug_mode = self._is_debug_mode()
|
||||
|
||||
# 无论是否为调试模式,都记录扫描操作
|
||||
logger.info(f"扫描离线补丁文件,目录: {directory}")
|
||||
|
||||
# 要查找的补丁文件名
|
||||
patch_files = ["vol.1.7z", "vol.2.7z", "vol.3.7z", "vol.4.7z", "after.7z"]
|
||||
|
||||
found_patches = {}
|
||||
|
||||
# 扫描目录中的文件
|
||||
for file in os.listdir(directory):
|
||||
if file.lower() in patch_files:
|
||||
file_path = os.path.join(directory, file)
|
||||
if os.path.isfile(file_path):
|
||||
patch_name = file.lower()
|
||||
found_patches[patch_name] = file_path
|
||||
# 无论是否为调试模式,都记录找到的补丁文件
|
||||
logger.info(f"找到离线补丁文件: {patch_name} 路径: {file_path}")
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 找到离线补丁文件: {patch_name} 路径: {file_path}")
|
||||
|
||||
self.offline_patches = found_patches
|
||||
|
||||
# 记录扫描结果
|
||||
if found_patches:
|
||||
logger.info(f"共找到 {len(found_patches)} 个离线补丁文件: {list(found_patches.keys())}")
|
||||
else:
|
||||
logger.info("未找到任何离线补丁文件")
|
||||
|
||||
return found_patches
|
||||
|
||||
def has_offline_patches(self):
|
||||
"""检查是否有可用的离线补丁文件
|
||||
|
||||
Returns:
|
||||
bool: 是否有可用的离线补丁
|
||||
"""
|
||||
if not self.offline_patches:
|
||||
self.scan_for_offline_patches()
|
||||
|
||||
return len(self.offline_patches) > 0
|
||||
|
||||
def set_offline_mode(self, enabled):
|
||||
"""设置离线模式状态
|
||||
|
||||
Args:
|
||||
enabled: 是否启用离线模式
|
||||
|
||||
Returns:
|
||||
bool: 是否成功设置离线模式
|
||||
"""
|
||||
debug_mode = self._is_debug_mode()
|
||||
|
||||
if enabled:
|
||||
# 检查是否有离线补丁文件
|
||||
if not self.has_offline_patches() and not debug_mode:
|
||||
msgbox_frame(
|
||||
f"离线模式错误 - {self.app_name}",
|
||||
"\n未找到任何离线补丁文件,无法启用离线模式。\n\n请将补丁文件放置在软件所在目录后再尝试。\n",
|
||||
QMessageBox.StandardButton.Ok
|
||||
).exec()
|
||||
logger.warning("尝试启用离线模式失败:未找到任何离线补丁文件")
|
||||
return False
|
||||
|
||||
if debug_mode:
|
||||
logger.debug("DEBUG: 已启用离线模式(调试模式下允许强制启用)")
|
||||
|
||||
self.is_offline_mode = enabled
|
||||
|
||||
# 更新窗口标题
|
||||
if hasattr(self.main_window, 'setWindowTitle'):
|
||||
from data.config import APP_NAME, APP_VERSION
|
||||
mode_indicator = "[离线模式]" if enabled else "[在线模式]"
|
||||
self.main_window.setWindowTitle(f"{APP_NAME} v{APP_VERSION} {mode_indicator}")
|
||||
|
||||
# 同时更新UI中的标题标签
|
||||
if hasattr(self.main_window, 'ui') and hasattr(self.main_window.ui, 'title_label'):
|
||||
self.main_window.ui.title_label.setText(f"{APP_NAME} v{APP_VERSION} {mode_indicator}")
|
||||
|
||||
# 同步更新UI菜单中的模式选择状态
|
||||
if hasattr(self.main_window, 'ui_manager'):
|
||||
ui_manager = self.main_window.ui_manager
|
||||
if hasattr(ui_manager, 'online_mode_action') and hasattr(ui_manager, 'offline_mode_action'):
|
||||
ui_manager.online_mode_action.setChecked(not enabled)
|
||||
ui_manager.offline_mode_action.setChecked(enabled)
|
||||
|
||||
# 无论是否为调试模式,都记录离线模式状态变化
|
||||
logger.info(f"离线模式已{'启用' if enabled else '禁用'}")
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 离线模式已{'启用' if enabled else '禁用'}")
|
||||
|
||||
return True
|
||||
|
||||
def get_offline_patch_path(self, game_version):
|
||||
"""根据游戏版本获取对应的离线补丁文件路径
|
||||
|
||||
Args:
|
||||
game_version: 游戏版本名称,如"NEKOPARA Vol.1"
|
||||
|
||||
Returns:
|
||||
str: 离线补丁文件路径,如果没有找到则返回None
|
||||
"""
|
||||
# 确保已扫描过补丁文件
|
||||
if not self.offline_patches:
|
||||
self.scan_for_offline_patches()
|
||||
|
||||
# 根据游戏版本获取对应的补丁文件名
|
||||
patch_file = None
|
||||
|
||||
if "Vol.1" in game_version:
|
||||
patch_file = "vol.1.7z"
|
||||
elif "Vol.2" in game_version:
|
||||
patch_file = "vol.2.7z"
|
||||
elif "Vol.3" in game_version:
|
||||
patch_file = "vol.3.7z"
|
||||
elif "Vol.4" in game_version:
|
||||
patch_file = "vol.4.7z"
|
||||
elif "After" in game_version:
|
||||
patch_file = "after.7z"
|
||||
|
||||
# 检查是否有对应的补丁文件
|
||||
if patch_file and patch_file in self.offline_patches:
|
||||
return self.offline_patches[patch_file]
|
||||
|
||||
return None
|
||||
|
||||
def prepare_offline_patch(self, game_version, target_path):
|
||||
"""准备离线补丁文件,复制到缓存目录
|
||||
|
||||
Args:
|
||||
game_version: 游戏版本名称
|
||||
target_path: 目标路径(通常是缓存目录中的路径)
|
||||
|
||||
Returns:
|
||||
bool: 是否成功准备补丁文件
|
||||
"""
|
||||
source_path = self.get_offline_patch_path(game_version)
|
||||
|
||||
if not source_path:
|
||||
return False
|
||||
|
||||
debug_mode = self._is_debug_mode()
|
||||
|
||||
try:
|
||||
# 确保目标目录存在
|
||||
os.makedirs(os.path.dirname(target_path), exist_ok=True)
|
||||
|
||||
# 复制文件
|
||||
shutil.copy2(source_path, target_path)
|
||||
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 已复制离线补丁文件 {source_path} 到 {target_path}")
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
if debug_mode:
|
||||
logger.error(f"DEBUG: 复制离线补丁文件失败: {e}")
|
||||
return False
|
||||
|
||||
def verify_patch_hash(self, game_version, file_path):
|
||||
"""验证补丁文件的哈希值,使用patch_detector模块
|
||||
|
||||
Args:
|
||||
game_version: 游戏版本名称
|
||||
file_path: 补丁压缩包文件路径
|
||||
|
||||
Returns:
|
||||
bool: 哈希值是否匹配
|
||||
"""
|
||||
debug_mode = self._is_debug_mode()
|
||||
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 开始验证补丁文件哈希: {file_path}")
|
||||
|
||||
# 创建进度对话框
|
||||
from utils.helpers import ProgressHashVerifyDialog
|
||||
from data.config import PLUGIN_HASH
|
||||
from workers.hash_thread import OfflineHashVerifyThread
|
||||
|
||||
# 创建并显示进度对话框
|
||||
progress_dialog = ProgressHashVerifyDialog(
|
||||
f"验证补丁文件 - {self.app_name}",
|
||||
f"正在验证 {game_version} 的补丁文件完整性...",
|
||||
self.main_window
|
||||
)
|
||||
|
||||
# 创建哈希验证线程
|
||||
hash_thread = OfflineHashVerifyThread(game_version, file_path, PLUGIN_HASH, self.main_window)
|
||||
|
||||
# 连接信号
|
||||
hash_thread.progress.connect(progress_dialog.update_progress)
|
||||
hash_thread.finished.connect(lambda result, error, extracted_path: self._on_hash_verify_finished(result, error, extracted_path, progress_dialog))
|
||||
|
||||
# 启动线程
|
||||
hash_thread.start()
|
||||
|
||||
# 显示对话框,阻塞直到对话框关闭
|
||||
result = progress_dialog.exec()
|
||||
|
||||
# 如果用户取消了验证,停止线程
|
||||
if result == ProgressHashVerifyDialog.Rejected and hash_thread.isRunning():
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 用户取消了哈希验证")
|
||||
hash_thread.terminate()
|
||||
hash_thread.wait()
|
||||
return False
|
||||
|
||||
# 返回对话框中存储的验证结果
|
||||
return hasattr(progress_dialog, 'hash_result') and progress_dialog.hash_result
|
||||
|
||||
def _on_hash_verify_finished(self, result, error, extracted_path, dialog):
|
||||
"""哈希验证线程完成后的回调
|
||||
|
||||
Args:
|
||||
result: 验证结果
|
||||
error: 错误信息
|
||||
extracted_path: 解压后的补丁文件路径,如果哈希验证成功则包含此路径
|
||||
dialog: 进度对话框
|
||||
"""
|
||||
debug_mode = self._is_debug_mode()
|
||||
|
||||
# 存储结果到对话框,以便在exec()返回后获取
|
||||
dialog.hash_result = result
|
||||
|
||||
if result:
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 哈希验证成功")
|
||||
if extracted_path:
|
||||
logger.debug(f"DEBUG: 解压后的补丁文件路径: {extracted_path}")
|
||||
dialog.set_status("验证成功")
|
||||
# 短暂延时后关闭对话框
|
||||
QTimer.singleShot(500, dialog.accept)
|
||||
else:
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 哈希验证失败: {error}")
|
||||
dialog.set_status(f"验证失败: {error}")
|
||||
dialog.set_message("补丁文件验证失败,可能已损坏或被篡改。")
|
||||
# 将取消按钮改为关闭按钮
|
||||
dialog.cancel_button.setText("关闭")
|
||||
# 不自动关闭,让用户查看错误信息
|
||||
|
||||
def _on_offline_install_hash_finished(self, result, error, extracted_path, dialog, game_version, _7z_path, game_folder, plugin_path, install_tasks):
|
||||
"""离线安装哈希验证线程完成后的回调
|
||||
|
||||
Args:
|
||||
result: 验证结果
|
||||
error: 错误信息
|
||||
extracted_path: 解压后的补丁文件路径
|
||||
dialog: 进度对话框
|
||||
game_version: 游戏版本
|
||||
_7z_path: 7z文件路径
|
||||
game_folder: 游戏文件夹路径
|
||||
plugin_path: 插件路径
|
||||
install_tasks: 剩余的安装任务列表
|
||||
"""
|
||||
debug_mode = self._is_debug_mode()
|
||||
|
||||
# 导入所需模块
|
||||
from data.config import GAME_INFO, PLUGIN
|
||||
|
||||
# 存储结果到对话框,以便在exec()返回后获取
|
||||
dialog.hash_result = result
|
||||
|
||||
# 关闭哈希验证窗口
|
||||
self.main_window.close_hash_msg_box()
|
||||
|
||||
if not result:
|
||||
# 哈希验证失败
|
||||
if debug_mode:
|
||||
logger.warning(f"DEBUG: 补丁文件哈希验证失败: {error}")
|
||||
|
||||
# 显示错误消息
|
||||
msgbox_frame(
|
||||
f"哈希验证失败 - {self.app_name}",
|
||||
f"\n{game_version} 的补丁文件哈希验证失败,可能已损坏或被篡改。\n\n跳过此游戏的安装。\n",
|
||||
QMessageBox.StandardButton.Ok
|
||||
).exec()
|
||||
|
||||
# 继续下一个任务
|
||||
self.process_next_offline_install_task(install_tasks)
|
||||
return
|
||||
|
||||
# 哈希验证成功,直接进行安装
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 哈希验证成功,开始安装")
|
||||
|
||||
# 显示安装进度窗口
|
||||
self.main_window.hash_msg_box = self.main_window.hash_manager.hash_pop_window(check_type="offline_installation", is_offline=True)
|
||||
|
||||
try:
|
||||
# 确保游戏目录存在
|
||||
os.makedirs(game_folder, exist_ok=True)
|
||||
|
||||
# 根据游戏版本确定目标文件名
|
||||
target_filename = None
|
||||
if "Vol.1" in game_version:
|
||||
target_filename = "adultsonly.xp3"
|
||||
elif "Vol.2" in game_version:
|
||||
target_filename = "adultsonly.xp3"
|
||||
elif "Vol.3" in game_version:
|
||||
target_filename = "update00.int"
|
||||
elif "Vol.4" in game_version:
|
||||
target_filename = "vol4adult.xp3"
|
||||
elif "After" in game_version:
|
||||
target_filename = "afteradult.xp3"
|
||||
|
||||
if not target_filename:
|
||||
raise ValueError(f"未知的游戏版本: {game_version}")
|
||||
|
||||
# 直接解压文件到游戏目录
|
||||
import py7zr
|
||||
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 直接解压文件 {_7z_path} 到游戏目录 {game_folder}")
|
||||
|
||||
# 解压文件
|
||||
with py7zr.SevenZipFile(_7z_path, mode="r") as archive:
|
||||
# 获取压缩包内的文件列表
|
||||
file_list = archive.getnames()
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 压缩包内文件列表: {file_list}")
|
||||
|
||||
# 解析压缩包内的文件结构
|
||||
target_file_in_archive = None
|
||||
for file_path in file_list:
|
||||
if target_filename in file_path:
|
||||
target_file_in_archive = file_path
|
||||
break
|
||||
|
||||
if not target_file_in_archive:
|
||||
if debug_mode:
|
||||
logger.warning(f"DEBUG: 在压缩包中未找到目标文件 {target_filename}")
|
||||
raise FileNotFoundError(f"在压缩包中未找到目标文件 {target_filename}")
|
||||
|
||||
# 准备解压特定文件到游戏目录
|
||||
target_path = os.path.join(game_folder, target_filename)
|
||||
|
||||
# 创建一个临时目录用于解压单个文件
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
# 解压特定文件到临时目录
|
||||
archive.extract(path=temp_dir, targets=[target_file_in_archive])
|
||||
|
||||
# 找到解压后的文件
|
||||
extracted_file_path = os.path.join(temp_dir, target_file_in_archive)
|
||||
|
||||
# 复制到目标位置
|
||||
shutil.copy2(extracted_file_path, target_path)
|
||||
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 已解压并复制文件到 {target_path}")
|
||||
|
||||
# 对于NEKOPARA After,还需要复制签名文件
|
||||
if game_version == "NEKOPARA After":
|
||||
sig_filename = f"{target_filename}.sig"
|
||||
sig_file_in_archive = None
|
||||
|
||||
# 查找签名文件
|
||||
for file_path in file_list:
|
||||
if sig_filename in file_path:
|
||||
sig_file_in_archive = file_path
|
||||
break
|
||||
|
||||
if sig_file_in_archive:
|
||||
# 解压签名文件
|
||||
archive.extract(path=temp_dir, targets=[sig_file_in_archive])
|
||||
extracted_sig_path = os.path.join(temp_dir, sig_file_in_archive)
|
||||
sig_target = os.path.join(game_folder, sig_filename)
|
||||
shutil.copy2(extracted_sig_path, sig_target)
|
||||
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 已解压并复制签名文件到 {sig_target}")
|
||||
else:
|
||||
if debug_mode:
|
||||
logger.warning(f"DEBUG: 未找到签名文件 {sig_filename}")
|
||||
|
||||
# 进行安装后的哈希校验
|
||||
self._perform_hash_check(game_version, install_tasks)
|
||||
|
||||
except Exception as e:
|
||||
if debug_mode:
|
||||
logger.error(f"DEBUG: 安装补丁文件失败: {e}")
|
||||
import traceback
|
||||
logger.error(f"DEBUG: 错误堆栈: {traceback.format_exc()}")
|
||||
|
||||
# 关闭安装进度窗口
|
||||
self.main_window.close_hash_msg_box()
|
||||
|
||||
# 显示错误消息
|
||||
msgbox_frame(
|
||||
f"安装错误 - {self.app_name}",
|
||||
f"\n{game_version} 的安装过程中发生错误: {str(e)}\n\n跳过此游戏的安装。\n",
|
||||
QMessageBox.StandardButton.Ok
|
||||
).exec()
|
||||
|
||||
# 继续下一个任务
|
||||
self.process_next_offline_install_task(install_tasks)
|
||||
|
||||
def _perform_hash_check(self, game_version, install_tasks):
|
||||
"""安装完成后进行哈希校验
|
||||
|
||||
Args:
|
||||
game_version: 游戏版本
|
||||
install_tasks: 剩余的安装任务列表
|
||||
"""
|
||||
debug_mode = self._is_debug_mode()
|
||||
|
||||
# 导入所需模块
|
||||
from data.config import GAME_INFO, PLUGIN_HASH
|
||||
from workers.hash_thread import HashThread
|
||||
|
||||
# 获取安装路径
|
||||
install_paths = {}
|
||||
game_dirs = self.main_window.game_detector.identify_game_directories_improved(
|
||||
self.main_window.download_manager.selected_folder
|
||||
)
|
||||
|
||||
for game, info in GAME_INFO.items():
|
||||
if game in game_dirs and game == game_version:
|
||||
game_dir = game_dirs[game]
|
||||
install_path = os.path.join(game_dir, os.path.basename(info["install_path"]))
|
||||
install_paths[game] = install_path
|
||||
break
|
||||
|
||||
if not install_paths:
|
||||
# 如果找不到安装路径,直接认为安装成功
|
||||
logger.warning(f"未找到 {game_version} 的安装路径,跳过哈希校验")
|
||||
self.main_window.installed_status[game_version] = True
|
||||
|
||||
# 添加到已安装游戏列表
|
||||
if game_version not in self.installed_games:
|
||||
self.installed_games.append(game_version)
|
||||
|
||||
# 关闭安装进度窗口
|
||||
self.main_window.close_hash_msg_box()
|
||||
|
||||
# 继续下一个任务
|
||||
self.process_next_offline_install_task(install_tasks)
|
||||
return
|
||||
|
||||
# 关闭可能存在的哈希校验窗口,然后创建新窗口
|
||||
self.main_window.close_hash_msg_box()
|
||||
|
||||
# 显示哈希校验窗口
|
||||
self.main_window.hash_msg_box = self.main_window.hash_manager.hash_pop_window(check_type="post", is_offline=True)
|
||||
|
||||
# 直接创建并启动哈希线程进行校验,而不是通过主窗口
|
||||
hash_thread = HashThread(
|
||||
"after",
|
||||
install_paths,
|
||||
PLUGIN_HASH,
|
||||
self.main_window.installed_status,
|
||||
self.main_window
|
||||
)
|
||||
hash_thread.after_finished.connect(
|
||||
lambda result: self._on_hash_check_finished(result, game_version, install_tasks)
|
||||
)
|
||||
|
||||
# 保存引用以便后续使用
|
||||
self.hash_thread = hash_thread
|
||||
try:
|
||||
self.hash_thread.finished.connect(lambda: setattr(self, 'hash_thread', None))
|
||||
except Exception:
|
||||
pass
|
||||
hash_thread.start()
|
||||
|
||||
def _on_hash_check_finished(self, result, game_version, install_tasks):
|
||||
"""哈希校验完成后的处理
|
||||
|
||||
Args:
|
||||
result: 校验结果,包含通过状态、游戏版本和消息
|
||||
game_version: 游戏版本
|
||||
install_tasks: 剩余的安装任务列表
|
||||
"""
|
||||
debug_mode = self._is_debug_mode()
|
||||
|
||||
# 关闭哈希检查窗口
|
||||
self.main_window.close_hash_msg_box()
|
||||
|
||||
if not result["passed"]:
|
||||
# 校验失败,删除已解压的文件并提示重新安装
|
||||
error_message = result["message"]
|
||||
|
||||
# 获取安装路径
|
||||
install_path = None
|
||||
game_dirs = self.main_window.game_detector.identify_game_directories_improved(
|
||||
self.main_window.download_manager.selected_folder
|
||||
)
|
||||
|
||||
from data.config import GAME_INFO
|
||||
if game_version in game_dirs and game_version in GAME_INFO:
|
||||
game_dir = game_dirs[game_version]
|
||||
install_path = os.path.join(game_dir, os.path.basename(GAME_INFO[game_version]["install_path"]))
|
||||
|
||||
# 如果找到安装路径,尝试删除已解压的文件
|
||||
if install_path and os.path.exists(install_path):
|
||||
try:
|
||||
os.remove(install_path)
|
||||
logger.info(f"已删除校验失败的文件: {install_path}")
|
||||
except Exception as e:
|
||||
logger.error(f"删除文件失败: {e}")
|
||||
|
||||
# 显示错误消息
|
||||
msgbox_frame(
|
||||
f"校验失败 - {self.app_name}",
|
||||
f"{error_message}\n\n跳过此游戏的安装。",
|
||||
QMessageBox.StandardButton.Ok
|
||||
).exec()
|
||||
|
||||
# 更新安装状态
|
||||
self.main_window.installed_status[game_version] = False
|
||||
else:
|
||||
# 校验通过,更新安装状态
|
||||
self.main_window.installed_status[game_version] = True
|
||||
|
||||
# 添加到已安装游戏列表
|
||||
if game_version not in self.installed_games:
|
||||
self.installed_games.append(game_version)
|
||||
|
||||
# 显示安装成功消息
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: {game_version} 安装成功并通过哈希校验")
|
||||
|
||||
# 继续处理下一个任务
|
||||
self.process_next_offline_install_task(install_tasks)
|
||||
|
||||
def _on_extraction_finished_with_hash_check(self, success, error_message, game_version, install_tasks):
|
||||
"""解压完成后进行哈希校验(后台线程回调)"""
|
||||
# 关闭解压进度窗口
|
||||
try:
|
||||
if self.extraction_progress_window and self.extraction_progress_window.isVisible():
|
||||
self.extraction_progress_window.close()
|
||||
except Exception:
|
||||
pass
|
||||
self.extraction_progress_window = None
|
||||
|
||||
# 清理线程引用
|
||||
self.extraction_thread = None
|
||||
|
||||
if not success:
|
||||
# 解压失败,提示并继续下一个任务
|
||||
msgbox_frame(
|
||||
f"安装错误 - {self.app_name}",
|
||||
error_message or f"\n{game_version} 的安装过程中发生错误。\n",
|
||||
QMessageBox.StandardButton.Ok
|
||||
).exec()
|
||||
self.process_next_offline_install_task(install_tasks)
|
||||
return
|
||||
|
||||
# 解压成功,进入安装后哈希校验
|
||||
self._perform_hash_check(game_version, install_tasks)
|
||||
|
||||
def on_extraction_thread_finished(self, success, error_message, game_version, install_tasks):
|
||||
"""解压线程完成后的处理(兼容旧版本)
|
||||
|
||||
Args:
|
||||
success: 是否解压成功
|
||||
error_message: 错误信息
|
||||
game_version: 游戏版本
|
||||
install_tasks: 剩余的安装任务列表
|
||||
"""
|
||||
# 这个方法已不再使用,但为了兼容性,我们直接处理下一个任务
|
||||
if success:
|
||||
# 更新安装状态
|
||||
self.main_window.installed_status[game_version] = True
|
||||
|
||||
# 添加到已安装游戏列表
|
||||
if game_version not in self.installed_games:
|
||||
self.installed_games.append(game_version)
|
||||
else:
|
||||
# 更新安装状态
|
||||
self.main_window.installed_status[game_version] = False
|
||||
|
||||
# 显示错误消息
|
||||
debug_mode = self._is_debug_mode()
|
||||
if debug_mode:
|
||||
logger.error(f"DEBUG: 解压失败: {error_message}")
|
||||
|
||||
# 继续下一个任务
|
||||
self.process_next_offline_install_task(install_tasks)
|
||||
|
||||
def install_offline_patches(self, selected_games):
|
||||
"""直接安装离线补丁,完全绕过下载模块
|
||||
|
||||
Args:
|
||||
selected_games: 用户选择安装的游戏列表
|
||||
|
||||
Returns:
|
||||
bool: 是否成功启动安装流程
|
||||
"""
|
||||
debug_mode = self._is_debug_mode()
|
||||
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 开始离线安装流程,选择的游戏: {selected_games}")
|
||||
|
||||
if not self.is_in_offline_mode():
|
||||
if debug_mode:
|
||||
logger.warning("DEBUG: 当前不是离线模式,无法使用离线安装")
|
||||
return False
|
||||
|
||||
# 确保已扫描过补丁文件
|
||||
if not self.offline_patches:
|
||||
self.scan_for_offline_patches()
|
||||
|
||||
if not self.offline_patches:
|
||||
if debug_mode:
|
||||
logger.warning("DEBUG: 未找到任何离线补丁文件")
|
||||
msgbox_frame(
|
||||
f"离线安装错误 - {self.app_name}",
|
||||
"\n未找到任何离线补丁文件,无法进行离线安装。\n\n请将补丁文件放置在软件所在目录后再尝试。\n",
|
||||
QMessageBox.StandardButton.Ok
|
||||
).exec()
|
||||
return False
|
||||
|
||||
# 获取游戏目录
|
||||
game_dirs = self.main_window.game_detector.identify_game_directories_improved(
|
||||
self.main_window.download_manager.selected_folder
|
||||
)
|
||||
|
||||
if not game_dirs:
|
||||
if debug_mode:
|
||||
logger.warning("DEBUG: 未识别到任何游戏目录")
|
||||
return False
|
||||
|
||||
self.main_window.setEnabled(False)
|
||||
|
||||
# 重置已安装游戏列表
|
||||
self.installed_games = []
|
||||
|
||||
# 设置到主窗口,供结果显示使用
|
||||
self.main_window.download_queue_history = selected_games
|
||||
|
||||
# 记录未找到离线补丁文件的游戏
|
||||
self.missing_offline_patches = []
|
||||
|
||||
# 创建安装任务列表
|
||||
install_tasks = []
|
||||
for game_version in selected_games:
|
||||
# 获取离线补丁文件路径
|
||||
patch_file = self.get_offline_patch_path(game_version)
|
||||
if not patch_file:
|
||||
if debug_mode:
|
||||
logger.warning(f"DEBUG: 未找到 {game_version} 的离线补丁文件,跳过")
|
||||
# 记录未找到离线补丁文件的游戏
|
||||
self.missing_offline_patches.append(game_version)
|
||||
continue
|
||||
|
||||
# 获取游戏目录
|
||||
game_folder = game_dirs.get(game_version)
|
||||
if not game_folder:
|
||||
if debug_mode:
|
||||
logger.warning(f"DEBUG: 未找到 {game_version} 的游戏目录,跳过")
|
||||
continue
|
||||
|
||||
# 获取目标路径
|
||||
if "Vol.1" in game_version:
|
||||
_7z_path = os.path.join(PLUGIN, "vol.1.7z")
|
||||
plugin_path = os.path.join(PLUGIN, GAME_INFO[game_version]["plugin_path"])
|
||||
elif "Vol.2" in game_version:
|
||||
_7z_path = os.path.join(PLUGIN, "vol.2.7z")
|
||||
plugin_path = os.path.join(PLUGIN, GAME_INFO[game_version]["plugin_path"])
|
||||
elif "Vol.3" in game_version:
|
||||
_7z_path = os.path.join(PLUGIN, "vol.3.7z")
|
||||
plugin_path = os.path.join(PLUGIN, GAME_INFO[game_version]["plugin_path"])
|
||||
elif "Vol.4" in game_version:
|
||||
_7z_path = os.path.join(PLUGIN, "vol.4.7z")
|
||||
plugin_path = os.path.join(PLUGIN, GAME_INFO[game_version]["plugin_path"])
|
||||
elif "After" in game_version:
|
||||
_7z_path = os.path.join(PLUGIN, "after.7z")
|
||||
plugin_path = os.path.join(PLUGIN, GAME_INFO[game_version]["plugin_path"])
|
||||
else:
|
||||
if debug_mode:
|
||||
logger.warning(f"DEBUG: {game_version} 不是支持的游戏版本,跳过")
|
||||
continue
|
||||
|
||||
# 添加到安装任务列表
|
||||
install_tasks.append((patch_file, game_folder, game_version, _7z_path, plugin_path))
|
||||
|
||||
# 开始执行第一个安装任务
|
||||
if install_tasks:
|
||||
if debug_mode:
|
||||
logger.info(f"DEBUG: 开始离线安装流程,安装游戏数量: {len(install_tasks)}")
|
||||
self.process_next_offline_install_task(install_tasks)
|
||||
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("开始安装")
|
||||
|
||||
return True
|
||||
|
||||
def _start_online_download(self, games_to_download):
|
||||
"""启动在线下载流程
|
||||
|
||||
Args:
|
||||
games_to_download: 要下载的游戏列表
|
||||
"""
|
||||
debug_mode = self._is_debug_mode()
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 启动在线下载流程,游戏列表: {games_to_download}")
|
||||
|
||||
# 确保下载管理器已初始化
|
||||
if hasattr(self.main_window, 'download_manager'):
|
||||
# 使用直接下载方法,绕过补丁判断
|
||||
self.main_window.download_manager.direct_download_action(games_to_download)
|
||||
else:
|
||||
if debug_mode:
|
||||
logger.error("DEBUG: 下载管理器未初始化,无法启动下载流程")
|
||||
# 显示错误消息
|
||||
msgbox_frame(
|
||||
f"错误 - {self.app_name}",
|
||||
"\n下载管理器未初始化,无法启动下载流程。\n",
|
||||
QMessageBox.StandardButton.Ok
|
||||
).exec()
|
||||
|
||||
def process_next_offline_install_task(self, install_tasks):
|
||||
"""处理下一个离线安装任务
|
||||
|
||||
Args:
|
||||
install_tasks: 安装任务列表,每个任务是一个元组 (patch_file, game_folder, game_version, _7z_path, plugin_path)
|
||||
"""
|
||||
debug_mode = self._is_debug_mode()
|
||||
|
||||
if not install_tasks:
|
||||
# 所有任务完成,进行后检查
|
||||
if debug_mode:
|
||||
logger.info("DEBUG: 所有离线安装任务完成,进行后检查")
|
||||
|
||||
# 使用patch_detector进行安装后哈希比较
|
||||
self.main_window.patch_detector.after_hash_compare()
|
||||
|
||||
# 检查是否有未找到离线补丁文件的游戏
|
||||
if hasattr(self, 'missing_offline_patches') and self.missing_offline_patches:
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 有未找到离线补丁文件的游戏: {self.missing_offline_patches}")
|
||||
|
||||
# 先显示已安装的结果
|
||||
if self.installed_games:
|
||||
installed_msg = f"已成功安装以下补丁:\n\n{chr(10).join(self.installed_games)}\n\n"
|
||||
else:
|
||||
installed_msg = ""
|
||||
|
||||
# 使用QTimer延迟显示询问对话框,确保安装结果窗口先显示并关闭
|
||||
QTimer.singleShot(500, lambda: self._show_missing_patches_dialog(installed_msg))
|
||||
else:
|
||||
# 恢复UI状态
|
||||
self.main_window.setEnabled(True)
|
||||
self.main_window.ui.start_install_text.setText("开始安装")
|
||||
|
||||
return
|
||||
|
||||
# 获取下一个任务
|
||||
patch_file, game_folder, game_version, _7z_path, plugin_path = install_tasks.pop(0)
|
||||
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 处理离线安装任务: {game_version}")
|
||||
logger.debug(f"DEBUG: 补丁文件: {patch_file}")
|
||||
logger.debug(f"DEBUG: 游戏目录: {game_folder}")
|
||||
|
||||
# 使用后台线程进行解压,避免阻塞UI
|
||||
try:
|
||||
# 确保游戏目录存在
|
||||
os.makedirs(game_folder, exist_ok=True)
|
||||
|
||||
# 创建非阻塞的解压进度窗口
|
||||
self.extraction_progress_window = self.main_window.create_extraction_progress_window()
|
||||
try:
|
||||
self.extraction_progress_window.show()
|
||||
QtWidgets.QApplication.processEvents()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 启动解压线程
|
||||
self.extraction_thread = self.main_window.create_extraction_thread(
|
||||
patch_file, game_folder, plugin_path, game_version
|
||||
)
|
||||
|
||||
# 连接进度更新到窗口控件
|
||||
if self.extraction_thread and self.extraction_progress_window:
|
||||
self.extraction_thread.progress.connect(
|
||||
lambda percent, status: (
|
||||
self.extraction_progress_window.progress_bar.setValue(percent),
|
||||
self.extraction_progress_window.status_label.setText(status)
|
||||
)
|
||||
)
|
||||
|
||||
# 完成后进入哈希校验
|
||||
self.extraction_thread.finished.connect(
|
||||
lambda success, error, gv: self._on_extraction_finished_with_hash_check(success, error, gv, install_tasks)
|
||||
)
|
||||
|
||||
# 线程结束时清理引用
|
||||
try:
|
||||
self.extraction_thread.finished.connect(lambda *_: setattr(self, 'extraction_thread', None))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self.extraction_thread.start()
|
||||
|
||||
except Exception as e:
|
||||
if debug_mode:
|
||||
logger.error(f"DEBUG: 离线安装任务处理失败: {e}")
|
||||
logger.error(f"DEBUG: 错误堆栈: {traceback.format_exc()}")
|
||||
|
||||
# 关闭可能存在的解压进度窗口
|
||||
try:
|
||||
if self.extraction_progress_window and self.extraction_progress_window.isVisible():
|
||||
self.extraction_progress_window.close()
|
||||
except Exception:
|
||||
pass
|
||||
self.extraction_progress_window = None
|
||||
|
||||
# 显示错误消息
|
||||
msgbox_frame(
|
||||
f"安装错误 - {self.app_name}",
|
||||
f"\n{game_version} 的安装过程中发生错误: {str(e)}\n\n跳过此游戏的安装。\n",
|
||||
QMessageBox.StandardButton.Ok
|
||||
).exec()
|
||||
|
||||
# 继续下一个任务
|
||||
self.process_next_offline_install_task(install_tasks)
|
||||
|
||||
def is_offline_mode_available(self):
|
||||
"""检查是否可以使用离线模式
|
||||
|
||||
Returns:
|
||||
bool: 是否可以使用离线模式
|
||||
"""
|
||||
# 在调试模式下始终允许离线模式
|
||||
if self._is_debug_mode():
|
||||
return True
|
||||
|
||||
# 检查是否有离线补丁文件
|
||||
return self.has_offline_patches()
|
||||
|
||||
def is_in_offline_mode(self):
|
||||
"""检查当前是否处于离线模式
|
||||
|
||||
Returns:
|
||||
bool: 是否处于离线模式
|
||||
"""
|
||||
return self.is_offline_mode
|
||||
|
||||
def _show_missing_patches_dialog(self, installed_msg):
|
||||
"""显示缺少离线补丁文件的对话框
|
||||
|
||||
Args:
|
||||
installed_msg: 已安装的补丁信息
|
||||
"""
|
||||
debug_mode = self._is_debug_mode()
|
||||
|
||||
# 在安装完成后询问用户是否切换到在线模式
|
||||
msg_box = msgbox_frame(
|
||||
f"离线安装完成 - {self.app_name}",
|
||||
f"\n{installed_msg}以下游戏未找到对应的离线补丁文件:\n\n{chr(10).join(self.missing_offline_patches)}\n\n是否切换到在线模式继续安装?\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("开始安装")
|
||||
375
source/core/managers/patch_detector.py
Normal file
@@ -0,0 +1,375 @@
|
||||
import os
|
||||
import hashlib
|
||||
import tempfile
|
||||
import py7zr
|
||||
import traceback
|
||||
from utils.logger import setup_logger
|
||||
from PySide6.QtWidgets import QMessageBox
|
||||
from PySide6.QtCore import QTimer, QThread, Signal
|
||||
from data.config import PLUGIN_HASH, APP_NAME
|
||||
|
||||
# 初始化logger
|
||||
logger = setup_logger("patch_detector")
|
||||
|
||||
class PatchCheckThread(QThread):
|
||||
"""用于在后台线程中执行补丁检查的线程"""
|
||||
finished = Signal(bool) # (is_installed)
|
||||
|
||||
def __init__(self, checker_func, *args):
|
||||
super().__init__()
|
||||
self.checker_func = checker_func
|
||||
self.args = args
|
||||
|
||||
def run(self):
|
||||
result = self.checker_func(*self.args)
|
||||
self.finished.emit(result)
|
||||
|
||||
class PatchDetector:
|
||||
"""补丁检测与校验模块,用于统一处理在线和离线模式下的补丁检测和校验"""
|
||||
|
||||
def __init__(self, main_window):
|
||||
"""初始化补丁检测器
|
||||
|
||||
Args:
|
||||
main_window: 主窗口实例,用于访问UI和状态
|
||||
"""
|
||||
self.main_window = main_window
|
||||
self.app_name = main_window.APP_NAME if hasattr(main_window, 'APP_NAME') else ""
|
||||
self.game_info = {}
|
||||
self.plugin_hash = {}
|
||||
self._load_game_info()
|
||||
self.patch_check_thread = None
|
||||
|
||||
def _load_game_info(self):
|
||||
"""从配置中加载游戏信息和补丁哈希值"""
|
||||
try:
|
||||
from data.config import GAME_INFO, PLUGIN_HASH
|
||||
self.game_info = GAME_INFO
|
||||
self.plugin_hash = PLUGIN_HASH
|
||||
except ImportError:
|
||||
logger.error("无法加载游戏信息或补丁哈希值配置")
|
||||
|
||||
def _is_debug_mode(self):
|
||||
"""检查是否处于调试模式
|
||||
|
||||
Returns:
|
||||
bool: 是否处于调试模式
|
||||
"""
|
||||
try:
|
||||
if hasattr(self.main_window, 'debug_manager') and self.main_window.debug_manager:
|
||||
if hasattr(self.main_window.debug_manager, '_is_debug_mode'):
|
||||
return self.main_window.debug_manager._is_debug_mode()
|
||||
elif hasattr(self.main_window, 'config'):
|
||||
return self.main_window.config.get('debug_mode', False)
|
||||
return False
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def check_patch_installed_async(self, game_dir, game_version, callback):
|
||||
"""异步检查游戏是否已安装补丁"""
|
||||
def on_finished(is_installed):
|
||||
callback(is_installed)
|
||||
self.patch_check_thread = None
|
||||
|
||||
self.patch_check_thread = PatchCheckThread(self._check_patch_installed_sync, game_dir, game_version)
|
||||
self.patch_check_thread.finished.connect(on_finished)
|
||||
self.patch_check_thread.start()
|
||||
|
||||
def _check_patch_installed_sync(self, game_dir, game_version):
|
||||
"""同步检查游戏是否已安装补丁(在工作线程中运行)"""
|
||||
debug_mode = self._is_debug_mode()
|
||||
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 检查 {game_version} 是否已安装补丁,目录: {game_dir}")
|
||||
|
||||
if game_version not in self.game_info:
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: {game_version} 不在支持的游戏列表中,跳过检查")
|
||||
return False
|
||||
|
||||
install_path_base = os.path.basename(self.game_info[game_version]["install_path"])
|
||||
patch_file_path = os.path.join(game_dir, install_path_base)
|
||||
|
||||
# 检查补丁文件和禁用的补丁文件
|
||||
if os.path.exists(patch_file_path) or os.path.exists(f"{patch_file_path}.fain"):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def check_patch_installed(self, game_dir, game_version):
|
||||
"""检查游戏是否已安装补丁(此方法可能导致阻塞,推荐使用异步版本)"""
|
||||
return self._check_patch_installed_sync(game_dir, game_version)
|
||||
|
||||
def check_patch_disabled(self, game_dir, game_version):
|
||||
"""检查游戏的补丁是否已被禁用"""
|
||||
debug_mode = self._is_debug_mode()
|
||||
|
||||
if game_version not in self.game_info:
|
||||
return False, None
|
||||
|
||||
install_path_base = os.path.basename(self.game_info[game_version]["install_path"])
|
||||
patch_file_path = os.path.join(game_dir, install_path_base)
|
||||
disabled_path = f"{patch_file_path}.fain"
|
||||
|
||||
if os.path.exists(disabled_path):
|
||||
if debug_mode:
|
||||
logger.debug(f"找到禁用的补丁文件: {disabled_path}")
|
||||
return True, disabled_path
|
||||
|
||||
if debug_mode:
|
||||
logger.debug(f"{game_version} 在 {game_dir} 的补丁未被禁用")
|
||||
|
||||
return False, None
|
||||
|
||||
def detect_installable_games(self, game_dirs):
|
||||
"""检测可安装补丁的游戏"""
|
||||
debug_mode = self._is_debug_mode()
|
||||
|
||||
if debug_mode:
|
||||
logger.debug(f"开始检测可安装补丁的游戏,游戏目录: {game_dirs}")
|
||||
|
||||
already_installed_games = []
|
||||
installable_games = []
|
||||
disabled_patch_games = []
|
||||
|
||||
for game_version, game_dir in game_dirs.items():
|
||||
is_patch_installed = self.check_patch_installed(game_dir, game_version)
|
||||
hash_check_passed = self.main_window.installed_status.get(game_version, False)
|
||||
|
||||
if is_patch_installed or hash_check_passed:
|
||||
if debug_mode:
|
||||
logger.info(f"DEBUG: {game_version} 已安装补丁,不需要再次安装")
|
||||
logger.info(f"DEBUG: 文件检查结果: {is_patch_installed}, 哈希检查结果: {hash_check_passed}")
|
||||
already_installed_games.append(game_version)
|
||||
self.main_window.installed_status[game_version] = True
|
||||
else:
|
||||
is_disabled, disabled_path = self.check_patch_disabled(game_dir, game_version)
|
||||
if is_disabled:
|
||||
if debug_mode:
|
||||
logger.info(f"DEBUG: {game_version} 存在被禁用的补丁: {disabled_path}")
|
||||
disabled_patch_games.append(game_version)
|
||||
else:
|
||||
if debug_mode:
|
||||
logger.info(f"DEBUG: {game_version} 未安装补丁,可以安装")
|
||||
logger.info(f"DEBUG: 文件检查结果: {is_patch_installed}, 哈希检查结果: {hash_check_passed}")
|
||||
installable_games.append(game_version)
|
||||
|
||||
if debug_mode:
|
||||
logger.debug(f"检测结果 - 已安装补丁: {already_installed_games}")
|
||||
logger.debug(f"检测结果 - 可安装补丁: {installable_games}")
|
||||
logger.debug(f"检测结果 - 禁用补丁: {disabled_patch_games}")
|
||||
|
||||
return already_installed_games, installable_games, disabled_patch_games
|
||||
|
||||
def verify_patch_hash(self, game_version, file_path):
|
||||
"""验证补丁文件的哈希值"""
|
||||
expected_hash = self.plugin_hash.get(game_version, "")
|
||||
|
||||
if not expected_hash:
|
||||
logger.warning(f"DEBUG: 未找到 {game_version} 的预期哈希值")
|
||||
return False
|
||||
|
||||
debug_mode = self._is_debug_mode()
|
||||
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 开始验证补丁文件: {file_path}")
|
||||
logger.debug(f"DEBUG: 游戏版本: {game_version}")
|
||||
logger.debug(f"DEBUG: 预期哈希值: {expected_hash}")
|
||||
|
||||
try:
|
||||
if not os.path.exists(file_path) or os.path.getsize(file_path) == 0:
|
||||
return False
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 创建临时目录: {temp_dir}")
|
||||
|
||||
try:
|
||||
with py7zr.SevenZipFile(file_path, mode="r") as archive:
|
||||
archive.extractall(path=temp_dir)
|
||||
except Exception as e:
|
||||
if debug_mode:
|
||||
logger.error(f"DEBUG: 解压补丁文件失败: {e}")
|
||||
return False
|
||||
|
||||
patch_file = self._find_patch_file_in_temp_dir(temp_dir, game_version)
|
||||
|
||||
if not patch_file or not os.path.exists(patch_file):
|
||||
if debug_mode:
|
||||
logger.warning(f"DEBUG: 未找到解压后的补丁文件")
|
||||
return False
|
||||
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 找到解压后的补丁文件: {patch_file}")
|
||||
|
||||
try:
|
||||
with open(patch_file, "rb") as f:
|
||||
file_hash = hashlib.sha256(f.read()).hexdigest()
|
||||
|
||||
result = file_hash.lower() == expected_hash.lower()
|
||||
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 补丁文件 {patch_file} 哈希值验证: {'成功' if result else '失败'}")
|
||||
|
||||
return result
|
||||
except Exception as e:
|
||||
if debug_mode:
|
||||
logger.error(f"DEBUG: 计算补丁文件哈希值失败: {e}")
|
||||
return False
|
||||
except Exception as e:
|
||||
if debug_mode:
|
||||
logger.error(f"DEBUG: 验证补丁哈希值失败: {e}")
|
||||
return False
|
||||
|
||||
def _find_patch_file_in_temp_dir(self, temp_dir, game_version):
|
||||
"""在临时目录中查找解压后的补丁文件"""
|
||||
game_patch_map = {
|
||||
"Vol.1": os.path.join("vol.1", "adultsonly.xp3"),
|
||||
"Vol.2": os.path.join("vol.2", "adultsonly.xp3"),
|
||||
"Vol.3": os.path.join("vol.3", "update00.int"),
|
||||
"Vol.4": os.path.join("vol.4", "vol4adult.xp3"),
|
||||
"After": os.path.join("after", "afteradult.xp3"),
|
||||
}
|
||||
|
||||
for version_keyword, relative_path in game_patch_map.items():
|
||||
if version_keyword in game_version:
|
||||
return os.path.join(temp_dir, relative_path)
|
||||
|
||||
# 如果没有找到,则进行通用搜索
|
||||
for root, dirs, files in os.walk(temp_dir):
|
||||
for file in files:
|
||||
if file.endswith('.xp3') or file.endswith('.int'):
|
||||
return os.path.join(root, file)
|
||||
return None
|
||||
|
||||
def create_hash_thread(self, mode, install_paths):
|
||||
from workers.hash_thread import HashThread
|
||||
return HashThread(mode, install_paths, PLUGIN_HASH, self.main_window.installed_status, self.main_window)
|
||||
|
||||
def after_hash_compare(self):
|
||||
is_offline = self.main_window.offline_mode_manager.is_in_offline_mode()
|
||||
|
||||
self.main_window.close_hash_msg_box()
|
||||
self.main_window.hash_msg_box = self.main_window.hash_manager.hash_pop_window(check_type="after", is_offline=is_offline)
|
||||
|
||||
install_paths = self.main_window.download_manager.get_install_paths()
|
||||
|
||||
self.main_window.hash_thread = self.create_hash_thread("after", install_paths)
|
||||
self.main_window.hash_thread.after_finished.connect(self.on_after_hash_finished)
|
||||
self.main_window.hash_thread.start()
|
||||
|
||||
def on_after_hash_finished(self, result):
|
||||
self.main_window.close_hash_msg_box()
|
||||
|
||||
if not result["passed"]:
|
||||
self.main_window.setEnabled(True)
|
||||
game = result.get("game", "未知游戏")
|
||||
message = result.get("message", "发生未知错误。")
|
||||
QMessageBox.critical(self.main_window, f"文件校验失败 - {APP_NAME}", message)
|
||||
|
||||
self.main_window.setEnabled(True)
|
||||
self.main_window.ui.start_install_text.setText("开始安装")
|
||||
QTimer.singleShot(100, self.main_window.show_result)
|
||||
|
||||
def on_offline_pre_hash_finished(self, updated_status, game_dirs):
|
||||
self.main_window.installed_status = updated_status
|
||||
|
||||
if self.main_window.hash_msg_box and self.main_window.hash_msg_box.isVisible():
|
||||
self.main_window.hash_msg_box.accept()
|
||||
self.main_window.hash_msg_box = None
|
||||
|
||||
self.main_window.setEnabled(True)
|
||||
|
||||
already_installed_games, installable_games, disabled_patch_games = self.detect_installable_games(game_dirs)
|
||||
|
||||
status_message = ""
|
||||
if already_installed_games:
|
||||
status_message += f"已安装补丁的游戏:\n{chr(10).join(already_installed_games)}\n\n"
|
||||
|
||||
if disabled_patch_games:
|
||||
disabled_msg = f"检测到以下游戏的补丁已被禁用:\n{chr(10).join(disabled_patch_games)}\n\n是否要启用这些补丁?"
|
||||
|
||||
from PySide6 import QtWidgets
|
||||
reply = QtWidgets.QMessageBox.question(
|
||||
self.main_window,
|
||||
f"检测到禁用补丁 - {APP_NAME}",
|
||||
disabled_msg,
|
||||
QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No
|
||||
)
|
||||
|
||||
if reply == QtWidgets.QMessageBox.StandardButton.Yes:
|
||||
disabled_game_dirs = {game: game_dirs[game] for game in disabled_patch_games}
|
||||
|
||||
success_count, fail_count, results = self.main_window.patch_manager.batch_toggle_patches(
|
||||
disabled_game_dirs,
|
||||
operation="enable"
|
||||
)
|
||||
|
||||
self.main_window.patch_manager.show_toggle_result(success_count, fail_count, results)
|
||||
|
||||
for game_version in disabled_patch_games:
|
||||
self.main_window.installed_status[game_version] = True
|
||||
if game_version in installable_games:
|
||||
installable_games.remove(game_version)
|
||||
if game_version not in already_installed_games:
|
||||
already_installed_games.append(game_version)
|
||||
else:
|
||||
installable_games.extend(disabled_patch_games)
|
||||
|
||||
if disabled_patch_games:
|
||||
status_message += f"禁用补丁的游戏:\n{chr(10).join(disabled_patch_games)}\n\n"
|
||||
|
||||
if not installable_games:
|
||||
if already_installed_games:
|
||||
QMessageBox.information(
|
||||
self.main_window,
|
||||
f"信息 - {APP_NAME}",
|
||||
f"\n所有游戏已安装补丁,无需重复安装。\n\n{status_message}",
|
||||
)
|
||||
else:
|
||||
QMessageBox.warning(
|
||||
self.main_window,
|
||||
f"警告 - {APP_NAME}",
|
||||
"\n未检测到任何需要安装补丁的游戏。\n\n请确保游戏文件夹位于选择的目录中。\n",
|
||||
)
|
||||
|
||||
self.main_window.ui.start_install_text.setText("开始安装")
|
||||
return
|
||||
|
||||
from PySide6 import QtWidgets
|
||||
dialog = QtWidgets.QDialog(self.main_window)
|
||||
dialog.setWindowTitle(f"选择要安装的游戏 - {APP_NAME}")
|
||||
dialog.setMinimumWidth(300)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
label = QtWidgets.QLabel("请选择要安装补丁的游戏:")
|
||||
layout.addWidget(label)
|
||||
|
||||
list_widget = QtWidgets.QListWidget()
|
||||
list_widget.setSelectionMode(QtWidgets.QAbstractItemView.SelectionMode.MultiSelection)
|
||||
|
||||
for game in installable_games:
|
||||
item = QtWidgets.QListWidgetItem(game)
|
||||
list_widget.addItem(item)
|
||||
item.setSelected(True)
|
||||
|
||||
layout.addWidget(list_widget)
|
||||
|
||||
button_box = QtWidgets.QDialogButtonBox(
|
||||
QtWidgets.QDialogButtonBox.StandardButton.Ok |
|
||||
QtWidgets.QDialogButtonBox.StandardButton.Cancel
|
||||
)
|
||||
button_box.accepted.connect(dialog.accept)
|
||||
button_box.rejected.connect(dialog.reject)
|
||||
layout.addWidget(button_box)
|
||||
|
||||
dialog.setLayout(layout)
|
||||
|
||||
result = dialog.exec()
|
||||
if result != QtWidgets.QDialog.DialogCode.Accepted or not list_widget.selectedItems():
|
||||
self.main_window.ui.start_install_text.setText("开始安装")
|
||||
return
|
||||
|
||||
selected_games = [item.text() for item in list_widget.selectedItems()]
|
||||
|
||||
self.main_window.offline_mode_manager.install_offline_patches(selected_games)
|
||||
983
source/core/managers/patch_manager.py
Normal file
@@ -0,0 +1,983 @@
|
||||
import os
|
||||
import shutil
|
||||
import traceback
|
||||
from PySide6.QtWidgets import QMessageBox
|
||||
from utils.logger import setup_logger
|
||||
from data.config import APP_NAME
|
||||
from utils import msgbox_frame
|
||||
|
||||
class PatchManager:
|
||||
"""补丁管理器,用于处理补丁的安装和卸载"""
|
||||
|
||||
def __init__(self, app_name, game_info, debug_manager=None, main_window=None):
|
||||
"""初始化补丁管理器
|
||||
|
||||
Args:
|
||||
app_name: 应用程序名称,用于显示消息框标题
|
||||
game_info: 游戏信息字典,包含各版本的安装路径和可执行文件名
|
||||
debug_manager: 调试管理器实例,用于输出调试信息
|
||||
main_window: 主窗口实例,用于访问UI和状态
|
||||
"""
|
||||
self.app_name = app_name
|
||||
self.game_info = game_info
|
||||
self.debug_manager = debug_manager
|
||||
self.main_window = main_window # 添加main_window属性
|
||||
self.installed_status = {} # 游戏版本的安装状态
|
||||
self.logger = setup_logger("patch_manager")
|
||||
self.patch_detector = None # 将在main_window初始化后设置
|
||||
|
||||
def set_patch_detector(self, patch_detector):
|
||||
"""设置补丁检测器实例
|
||||
|
||||
Args:
|
||||
patch_detector: 补丁检测器实例
|
||||
"""
|
||||
self.patch_detector = patch_detector
|
||||
|
||||
def _is_debug_mode(self):
|
||||
"""检查是否处于调试模式
|
||||
|
||||
Returns:
|
||||
bool: 是否处于调试模式
|
||||
"""
|
||||
if hasattr(self.debug_manager, 'ui_manager') and hasattr(self.debug_manager.ui_manager, 'debug_action'):
|
||||
return self.debug_manager.ui_manager.debug_action.isChecked()
|
||||
return False
|
||||
|
||||
def initialize_status(self):
|
||||
"""初始化所有游戏版本的安装状态"""
|
||||
self.installed_status = {f"NEKOPARA Vol.{i}": False for i in range(1, 5)}
|
||||
self.installed_status["NEKOPARA After"] = False
|
||||
|
||||
def update_status(self, game_version, is_installed):
|
||||
"""更新游戏版本的安装状态
|
||||
|
||||
Args:
|
||||
game_version: 游戏版本
|
||||
is_installed: 是否已安装
|
||||
"""
|
||||
self.installed_status[game_version] = is_installed
|
||||
|
||||
def get_status(self, game_version=None):
|
||||
"""获取游戏版本的安装状态
|
||||
|
||||
Args:
|
||||
game_version: 游戏版本,如果为None则返回所有状态
|
||||
|
||||
Returns:
|
||||
bool或dict: 指定版本的安装状态或所有版本的安装状态
|
||||
"""
|
||||
if game_version:
|
||||
return self.installed_status.get(game_version, False)
|
||||
return self.installed_status
|
||||
|
||||
def uninstall_patch(self, game_dir, game_version, silent=False):
|
||||
"""卸载补丁
|
||||
|
||||
Args:
|
||||
game_dir: 游戏目录路径
|
||||
game_version: 游戏版本
|
||||
silent: 是否静默模式(不显示弹窗)
|
||||
|
||||
Returns:
|
||||
bool: 卸载成功返回True,失败返回False
|
||||
dict: 在silent=True时,返回包含卸载结果信息的字典
|
||||
"""
|
||||
debug_mode = self._is_debug_mode()
|
||||
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: 开始卸载 {game_version} 补丁,目录: {game_dir}")
|
||||
|
||||
self.logger.info(f"开始卸载 {game_version} 补丁,目录: {game_dir}")
|
||||
|
||||
if game_version not in self.game_info:
|
||||
error_msg = f"无法识别游戏版本: {game_version}"
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: 卸载失败 - {error_msg}")
|
||||
self.logger.error(f"卸载失败 - {error_msg}")
|
||||
|
||||
if not silent:
|
||||
QMessageBox.critical(
|
||||
None,
|
||||
f"错误 - {self.app_name}",
|
||||
f"\n{error_msg}\n",
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
return False if not silent else {"success": False, "message": error_msg, "files_removed": 0}
|
||||
|
||||
try:
|
||||
files_removed = 0
|
||||
|
||||
# 获取可能的补丁文件路径
|
||||
install_path_base = os.path.basename(self.game_info[game_version]["install_path"])
|
||||
patch_file_path = os.path.join(game_dir, install_path_base)
|
||||
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: 基础补丁文件路径: {patch_file_path}")
|
||||
|
||||
# 尝试查找补丁文件,支持不同大小写
|
||||
patch_files_to_check = [
|
||||
patch_file_path,
|
||||
patch_file_path.lower(),
|
||||
patch_file_path.upper(),
|
||||
patch_file_path.replace("_", ""),
|
||||
patch_file_path.replace("_", "-"),
|
||||
]
|
||||
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: 查找以下可能的补丁文件路径: {patch_files_to_check}")
|
||||
|
||||
# 查找并删除补丁文件,包括启用和禁用的
|
||||
patch_file_found = False
|
||||
for patch_path in patch_files_to_check:
|
||||
# 检查常规补丁文件
|
||||
if os.path.exists(patch_path):
|
||||
patch_file_found = True
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: 找到补丁文件: {patch_path},准备删除")
|
||||
self.logger.info(f"删除补丁文件: {patch_path}")
|
||||
|
||||
os.remove(patch_path)
|
||||
files_removed += 1
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: 已删除补丁文件: {patch_path}")
|
||||
|
||||
# 检查被禁用的补丁文件(带.fain后缀)
|
||||
disabled_path = f"{patch_path}.fain"
|
||||
if os.path.exists(disabled_path):
|
||||
patch_file_found = True
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: 找到被禁用的补丁文件: {disabled_path},准备删除")
|
||||
self.logger.info(f"删除被禁用的补丁文件: {disabled_path}")
|
||||
|
||||
os.remove(disabled_path)
|
||||
files_removed += 1
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: 已删除被禁用的补丁文件: {disabled_path}")
|
||||
|
||||
if not patch_file_found:
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: 未找到补丁文件,检查了以下路径: {patch_files_to_check}")
|
||||
self.logger.debug(f"DEBUG: 也检查了禁用的补丁文件(.fain后缀)")
|
||||
self.logger.warning(f"未找到 {game_version} 的补丁文件")
|
||||
|
||||
# 检查是否有额外的签名文件 (.sig)
|
||||
if game_version == "NEKOPARA After":
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: {game_version} 需要检查额外的签名文件")
|
||||
|
||||
for patch_path in patch_files_to_check:
|
||||
# 检查常规签名文件
|
||||
sig_file_path = f"{patch_path}.sig"
|
||||
if os.path.exists(sig_file_path):
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: 找到签名文件: {sig_file_path},准备删除")
|
||||
self.logger.info(f"删除签名文件: {sig_file_path}")
|
||||
|
||||
os.remove(sig_file_path)
|
||||
files_removed += 1
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: 已删除签名文件: {sig_file_path}")
|
||||
|
||||
# 检查被禁用补丁的签名文件
|
||||
disabled_sig_path = f"{patch_path}.fain.sig"
|
||||
if os.path.exists(disabled_sig_path):
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: 找到被禁用补丁的签名文件: {disabled_sig_path},准备删除")
|
||||
self.logger.info(f"删除被禁用补丁的签名文件: {disabled_sig_path}")
|
||||
|
||||
os.remove(disabled_sig_path)
|
||||
files_removed += 1
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: 已删除被禁用补丁的签名文件: {disabled_sig_path}")
|
||||
|
||||
# 删除patch文件夹
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: 检查并删除patch文件夹")
|
||||
|
||||
patch_folders_to_check = [
|
||||
os.path.join(game_dir, "patch"),
|
||||
os.path.join(game_dir, "Patch"),
|
||||
os.path.join(game_dir, "PATCH"),
|
||||
]
|
||||
|
||||
for patch_folder in patch_folders_to_check:
|
||||
if os.path.exists(patch_folder):
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: 找到补丁文件夹: {patch_folder},准备删除")
|
||||
self.logger.info(f"删除补丁文件夹: {patch_folder}")
|
||||
|
||||
import shutil
|
||||
shutil.rmtree(patch_folder)
|
||||
files_removed += 1
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: 已删除补丁文件夹: {patch_folder}")
|
||||
|
||||
# 删除game/patch文件夹
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: 检查并删除game/patch文件夹")
|
||||
|
||||
game_folders = ["game", "Game", "GAME"]
|
||||
patch_folders = ["patch", "Patch", "PATCH"]
|
||||
|
||||
for game_folder in game_folders:
|
||||
for patch_folder in patch_folders:
|
||||
game_patch_folder = os.path.join(game_dir, game_folder, patch_folder)
|
||||
if os.path.exists(game_patch_folder):
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: 找到game/patch文件夹: {game_patch_folder},准备删除")
|
||||
self.logger.info(f"删除game/patch文件夹: {game_patch_folder}")
|
||||
|
||||
import shutil
|
||||
shutil.rmtree(game_patch_folder)
|
||||
files_removed += 1
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: 已删除game/patch文件夹: {game_patch_folder}")
|
||||
|
||||
# 删除配置文件
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: 检查并删除配置文件和脚本文件")
|
||||
|
||||
config_files = ["config.json", "Config.json", "CONFIG.JSON"]
|
||||
script_files = ["scripts.json", "Scripts.json", "SCRIPTS.JSON"]
|
||||
|
||||
for game_folder in game_folders:
|
||||
game_path = os.path.join(game_dir, game_folder)
|
||||
if os.path.exists(game_path):
|
||||
# 删除配置文件
|
||||
for config_file in config_files:
|
||||
config_path = os.path.join(game_path, config_file)
|
||||
if os.path.exists(config_path):
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: 找到配置文件: {config_path},准备删除")
|
||||
self.logger.info(f"删除配置文件: {config_path}")
|
||||
|
||||
os.remove(config_path)
|
||||
files_removed += 1
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: 已删除配置文件: {config_path}")
|
||||
|
||||
# 删除脚本文件
|
||||
for script_file in script_files:
|
||||
script_path = os.path.join(game_path, script_file)
|
||||
if os.path.exists(script_path):
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: 找到脚本文件: {script_path},准备删除")
|
||||
self.logger.info(f"删除脚本文件: {script_path}")
|
||||
|
||||
os.remove(script_path)
|
||||
files_removed += 1
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: 已删除脚本文件: {script_path}")
|
||||
|
||||
# 更新安装状态
|
||||
self.installed_status[game_version] = False
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: 已更新 {game_version} 的安装状态为未安装")
|
||||
|
||||
# 在非静默模式且非批量卸载模式下显示卸载成功消息
|
||||
if not silent and game_version != "all":
|
||||
# 显示卸载成功消息
|
||||
if files_removed > 0:
|
||||
success_msg = f"\n{game_version} 补丁卸载成功!\n共删除 {files_removed} 个文件/文件夹。\n"
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: 显示卸载成功消息: {success_msg}")
|
||||
|
||||
QMessageBox.information(
|
||||
None,
|
||||
f"卸载完成 - {self.app_name}",
|
||||
success_msg,
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
else:
|
||||
warning_msg = f"\n未找到 {game_version} 的补丁文件,可能未安装补丁或已被移除。\n"
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: 显示警告消息: {warning_msg}")
|
||||
|
||||
QMessageBox.warning(
|
||||
None,
|
||||
f"警告 - {self.app_name}",
|
||||
warning_msg,
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
|
||||
# 卸载成功
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: {game_version} 卸载完成,共删除 {files_removed} 个文件/文件夹")
|
||||
self.logger.info(f"{game_version} 卸载完成,共删除 {files_removed} 个文件/文件夹")
|
||||
|
||||
if silent:
|
||||
return {"success": True, "message": f"{game_version} 补丁卸载成功", "files_removed": files_removed}
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
error_message = f"卸载 {game_version} 补丁时出错:{str(e)}"
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: {error_message}")
|
||||
import traceback
|
||||
self.logger.debug(f"DEBUG: 错误详情:\n{traceback.format_exc()}")
|
||||
self.logger.error(error_message)
|
||||
|
||||
# 在非静默模式且非批量卸载模式下显示卸载失败消息
|
||||
if not silent and game_version != "all":
|
||||
# 显示卸载失败消息
|
||||
error_message = f"\n卸载 {game_version} 补丁时出错:\n\n{str(e)}\n"
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: 显示卸载失败消息")
|
||||
|
||||
QMessageBox.critical(
|
||||
None,
|
||||
f"卸载失败 - {self.app_name}",
|
||||
error_message,
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
|
||||
# 卸载失败
|
||||
if silent:
|
||||
return {"success": False, "message": f"卸载 {game_version} 补丁时出错: {str(e)}", "files_removed": 0}
|
||||
return False
|
||||
|
||||
def batch_uninstall_patches(self, game_dirs):
|
||||
"""批量卸载多个游戏的补丁
|
||||
|
||||
Args:
|
||||
game_dirs: 游戏版本到游戏目录的映射字典
|
||||
|
||||
Returns:
|
||||
tuple: (成功数量, 失败数量, 详细结果列表)
|
||||
"""
|
||||
success_count = 0
|
||||
fail_count = 0
|
||||
debug_mode = self._is_debug_mode()
|
||||
results = []
|
||||
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: 开始批量卸载补丁,游戏数量: {len(game_dirs)}")
|
||||
self.logger.debug(f"DEBUG: 要卸载的游戏: {list(game_dirs.keys())}")
|
||||
|
||||
self.logger.info(f"开始批量卸载补丁,游戏数量: {len(game_dirs)}")
|
||||
self.logger.info(f"要卸载的游戏: {list(game_dirs.keys())}")
|
||||
|
||||
for version, path in game_dirs.items():
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: 处理游戏 {version},路径: {path}")
|
||||
|
||||
self.logger.info(f"开始卸载 {version} 的补丁")
|
||||
|
||||
try:
|
||||
# 在批量模式下使用静默卸载
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: 使用静默模式卸载 {version}")
|
||||
|
||||
result = self.uninstall_patch(path, version, silent=True)
|
||||
|
||||
if isinstance(result, dict): # 使用了静默模式
|
||||
if result["success"]:
|
||||
success_count += 1
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: {version} 卸载成功,删除了 {result['files_removed']} 个文件/文件夹")
|
||||
self.logger.info(f"{version} 卸载成功,删除了 {result['files_removed']} 个文件/文件夹")
|
||||
else:
|
||||
fail_count += 1
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: {version} 卸载失败,原因: {result['message']}")
|
||||
self.logger.warning(f"{version} 卸载失败,原因: {result['message']}")
|
||||
|
||||
results.append({
|
||||
"version": version,
|
||||
"success": result["success"],
|
||||
"message": result["message"],
|
||||
"files_removed": result["files_removed"]
|
||||
})
|
||||
else: # 兼容旧代码,不应该执行到这里
|
||||
if result:
|
||||
success_count += 1
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: {version} 卸载成功(旧格式)")
|
||||
self.logger.info(f"{version} 卸载成功(旧格式)")
|
||||
else:
|
||||
fail_count += 1
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: {version} 卸载失败(旧格式)")
|
||||
self.logger.warning(f"{version} 卸载失败(旧格式)")
|
||||
|
||||
results.append({
|
||||
"version": version,
|
||||
"success": result,
|
||||
"message": f"{version} 卸载{'成功' if result else '失败'}",
|
||||
"files_removed": 0
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: 卸载 {version} 时出错: {str(e)}")
|
||||
import traceback
|
||||
self.logger.debug(f"DEBUG: 错误详情:\n{traceback.format_exc()}")
|
||||
|
||||
self.logger.error(f"卸载 {version} 时出错: {str(e)}")
|
||||
|
||||
fail_count += 1
|
||||
results.append({
|
||||
"version": version,
|
||||
"success": False,
|
||||
"message": f"卸载出错: {str(e)}",
|
||||
"files_removed": 0
|
||||
})
|
||||
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: 批量卸载完成,成功: {success_count},失败: {fail_count}")
|
||||
|
||||
self.logger.info(f"批量卸载完成,成功: {success_count},失败: {fail_count}")
|
||||
|
||||
return success_count, fail_count, results
|
||||
|
||||
def show_uninstall_result(self, success_count, fail_count, results=None):
|
||||
"""显示批量卸载结果
|
||||
|
||||
Args:
|
||||
success_count: 成功卸载的数量
|
||||
fail_count: 卸载失败的数量
|
||||
results: 详细结果列表,如果提供,会显示更详细的信息
|
||||
"""
|
||||
debug_mode = self._is_debug_mode()
|
||||
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: 显示卸载结果,成功: {success_count},失败: {fail_count}")
|
||||
|
||||
result_text = f"\n批量卸载完成!\n成功: {success_count} 个\n失败: {fail_count} 个\n"
|
||||
|
||||
# 如果有详细结果,添加到消息中
|
||||
if results:
|
||||
success_list = [r["version"] for r in results if r["success"]]
|
||||
fail_list = [r["version"] for r in results if not r["success"]]
|
||||
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: 成功卸载的游戏: {success_list}")
|
||||
self.logger.debug(f"DEBUG: 卸载失败的游戏: {fail_list}")
|
||||
|
||||
if success_list:
|
||||
result_text += f"\n【成功卸载】:\n{chr(10).join(success_list)}\n"
|
||||
|
||||
if fail_list:
|
||||
result_text += f"\n【卸载失败】:\n{chr(10).join(fail_list)}\n"
|
||||
|
||||
# 记录更详细的失败原因
|
||||
if debug_mode:
|
||||
for r in results:
|
||||
if not r["success"]:
|
||||
self.logger.debug(f"DEBUG: {r['version']} 卸载失败原因: {r['message']}")
|
||||
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: 显示卸载结果对话框")
|
||||
|
||||
QMessageBox.information(
|
||||
None,
|
||||
f"批量卸载完成 - {self.app_name}",
|
||||
result_text,
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
|
||||
def check_patch_installed(self, game_dir, game_version):
|
||||
"""检查游戏是否已安装补丁(调用patch_detector)
|
||||
|
||||
Args:
|
||||
game_dir: 游戏目录路径
|
||||
game_version: 游戏版本
|
||||
|
||||
Returns:
|
||||
bool: 如果已安装补丁或有被禁用的补丁文件返回True,否则返回False
|
||||
"""
|
||||
if self.patch_detector:
|
||||
return self.patch_detector.check_patch_installed(game_dir, game_version)
|
||||
|
||||
# 如果patch_detector未设置,使用原始逻辑(应该不会执行到这里)
|
||||
debug_mode = self._is_debug_mode()
|
||||
|
||||
if game_version not in self.game_info:
|
||||
return False
|
||||
|
||||
# 获取可能的补丁文件路径
|
||||
install_path_base = os.path.basename(self.game_info[game_version]["install_path"])
|
||||
patch_file_path = os.path.join(game_dir, install_path_base)
|
||||
|
||||
# 尝试查找补丁文件,支持不同大小写
|
||||
patch_files_to_check = [
|
||||
patch_file_path,
|
||||
patch_file_path.lower(),
|
||||
patch_file_path.upper(),
|
||||
patch_file_path.replace("_", ""),
|
||||
patch_file_path.replace("_", "-"),
|
||||
]
|
||||
|
||||
# 查找补丁文件
|
||||
for patch_path in patch_files_to_check:
|
||||
if os.path.exists(patch_path):
|
||||
if debug_mode:
|
||||
self.logger.debug(f"找到补丁文件: {patch_path}")
|
||||
return True
|
||||
# 检查是否存在被禁用的补丁文件(带.fain后缀)
|
||||
disabled_path = f"{patch_path}.fain"
|
||||
if os.path.exists(disabled_path):
|
||||
if debug_mode:
|
||||
self.logger.debug(f"找到被禁用的补丁文件: {disabled_path}")
|
||||
return True
|
||||
|
||||
# 检查是否有补丁文件夹
|
||||
patch_folders_to_check = [
|
||||
os.path.join(game_dir, "patch"),
|
||||
os.path.join(game_dir, "Patch"),
|
||||
os.path.join(game_dir, "PATCH"),
|
||||
]
|
||||
|
||||
for patch_folder in patch_folders_to_check:
|
||||
if os.path.exists(patch_folder):
|
||||
if debug_mode:
|
||||
self.logger.debug(f"找到补丁文件夹: {patch_folder}")
|
||||
return True
|
||||
|
||||
# 检查game/patch文件夹
|
||||
game_folders = ["game", "Game", "GAME"]
|
||||
patch_folders = ["patch", "Patch", "PATCH"]
|
||||
|
||||
for game_folder in game_folders:
|
||||
for patch_folder in patch_folders:
|
||||
game_patch_folder = os.path.join(game_dir, game_folder, patch_folder)
|
||||
if os.path.exists(game_patch_folder):
|
||||
if debug_mode:
|
||||
self.logger.debug(f"找到game/patch文件夹: {game_patch_folder}")
|
||||
return True
|
||||
|
||||
# 检查配置文件
|
||||
config_files = ["config.json", "Config.json", "CONFIG.JSON"]
|
||||
script_files = ["scripts.json", "Scripts.json", "SCRIPTS.JSON"]
|
||||
|
||||
for game_folder in game_folders:
|
||||
game_path = os.path.join(game_dir, game_folder)
|
||||
if os.path.exists(game_path):
|
||||
# 检查配置文件
|
||||
for config_file in config_files:
|
||||
config_path = os.path.join(game_path, config_file)
|
||||
if os.path.exists(config_path):
|
||||
if debug_mode:
|
||||
self.logger.debug(f"找到配置文件: {config_path}")
|
||||
return True
|
||||
|
||||
# 检查脚本文件
|
||||
for script_file in script_files:
|
||||
script_path = os.path.join(game_path, script_file)
|
||||
if os.path.exists(script_path):
|
||||
if debug_mode:
|
||||
self.logger.debug(f"找到脚本文件: {script_path}")
|
||||
return True
|
||||
|
||||
# 没有找到补丁文件或文件夹
|
||||
if debug_mode:
|
||||
self.logger.debug(f"{game_version} 在 {game_dir} 中没有安装补丁")
|
||||
return False
|
||||
|
||||
def check_patch_disabled(self, game_dir, game_version):
|
||||
"""检查游戏的补丁是否已被禁用(调用patch_detector)
|
||||
|
||||
Args:
|
||||
game_dir: 游戏目录路径
|
||||
game_version: 游戏版本
|
||||
|
||||
Returns:
|
||||
bool: 如果补丁被禁用返回True,否则返回False
|
||||
str: 禁用的补丁文件路径,如果没有禁用返回None
|
||||
"""
|
||||
if self.patch_detector:
|
||||
return self.patch_detector.check_patch_disabled(game_dir, game_version)
|
||||
|
||||
# 如果patch_detector未设置,使用原始逻辑(应该不会执行到这里)
|
||||
debug_mode = self._is_debug_mode()
|
||||
|
||||
if game_version not in self.game_info:
|
||||
return False, None
|
||||
|
||||
# 获取可能的补丁文件路径
|
||||
install_path_base = os.path.basename(self.game_info[game_version]["install_path"])
|
||||
patch_file_path = os.path.join(game_dir, install_path_base)
|
||||
|
||||
# 检查是否存在禁用的补丁文件(.fain后缀)
|
||||
disabled_patch_files = [
|
||||
f"{patch_file_path}.fain",
|
||||
f"{patch_file_path.lower()}.fain",
|
||||
f"{patch_file_path.upper()}.fain",
|
||||
f"{patch_file_path.replace('_', '')}.fain",
|
||||
f"{patch_file_path.replace('_', '-')}.fain",
|
||||
]
|
||||
|
||||
# 检查是否有禁用的补丁文件
|
||||
for disabled_path in disabled_patch_files:
|
||||
if os.path.exists(disabled_path):
|
||||
if debug_mode:
|
||||
self.logger.debug(f"找到禁用的补丁文件: {disabled_path}")
|
||||
return True, disabled_path
|
||||
|
||||
if debug_mode:
|
||||
self.logger.debug(f"{game_version} 在 {game_dir} 的补丁未被禁用")
|
||||
|
||||
return False, None
|
||||
|
||||
def toggle_patch(self, game_dir, game_version, operation=None, silent=False):
|
||||
"""切换补丁的禁用/启用状态
|
||||
|
||||
Args:
|
||||
game_dir: 游戏目录路径
|
||||
game_version: 游戏版本
|
||||
operation: 指定操作,可以是"enable"、"disable"或None(None则自动切换当前状态)
|
||||
silent: 是否静默模式(不显示弹窗)
|
||||
|
||||
Returns:
|
||||
dict: 包含操作结果信息的字典
|
||||
"""
|
||||
debug_mode = self._is_debug_mode()
|
||||
|
||||
if debug_mode:
|
||||
self.logger.debug(f"开始切换补丁状态 - 游戏版本: {game_version}, 游戏目录: {game_dir}, 操作: {operation}")
|
||||
|
||||
if game_version not in self.game_info:
|
||||
if debug_mode:
|
||||
self.logger.debug(f"无法识别游戏版本: {game_version}")
|
||||
if not silent:
|
||||
QMessageBox.critical(
|
||||
None,
|
||||
f"错误 - {self.app_name}",
|
||||
f"\n无法识别游戏版本: {game_version}\n",
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
return {"success": False, "message": f"无法识别游戏版本: {game_version}", "action": "none"}
|
||||
|
||||
# 检查补丁是否已安装
|
||||
is_patch_installed = self.check_patch_installed(game_dir, game_version)
|
||||
if debug_mode:
|
||||
self.logger.debug(f"补丁安装状态检查结果: {is_patch_installed}")
|
||||
|
||||
if not is_patch_installed:
|
||||
if debug_mode:
|
||||
self.logger.debug(f"{game_version} 未安装补丁,无法进行禁用/启用操作")
|
||||
if not silent:
|
||||
QMessageBox.warning(
|
||||
None,
|
||||
f"提示 - {self.app_name}",
|
||||
f"\n{game_version} 未安装补丁,无法进行禁用/启用操作。\n",
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
return {"success": False, "message": f"{game_version} 未安装补丁", "action": "none"}
|
||||
|
||||
try:
|
||||
# 检查当前状态
|
||||
is_disabled, disabled_path = self.check_patch_disabled(game_dir, game_version)
|
||||
if debug_mode:
|
||||
self.logger.debug(f"补丁禁用状态检查结果 - 是否禁用: {is_disabled}, 禁用路径: {disabled_path}")
|
||||
|
||||
# 获取可能的补丁文件路径
|
||||
install_path_base = os.path.basename(self.game_info[game_version]["install_path"])
|
||||
patch_file_path = os.path.join(game_dir, install_path_base)
|
||||
|
||||
# 尝试查找原始补丁文件,支持不同大小写
|
||||
patch_files_to_check = [
|
||||
patch_file_path,
|
||||
patch_file_path.lower(),
|
||||
patch_file_path.upper(),
|
||||
patch_file_path.replace("_", ""),
|
||||
patch_file_path.replace("_", "-"),
|
||||
]
|
||||
|
||||
if debug_mode:
|
||||
self.logger.debug(f"将检查以下可能的补丁文件: {patch_files_to_check}")
|
||||
|
||||
# 确定操作类型
|
||||
if operation:
|
||||
if operation == "enable":
|
||||
action_needed = is_disabled # 只有当前是禁用状态时才需要启用
|
||||
elif operation == "disable":
|
||||
action_needed = not is_disabled # 只有当前是启用状态时才需要禁用
|
||||
else:
|
||||
action_needed = True # 无效操作类型,强制进行操作
|
||||
else:
|
||||
action_needed = True # 未指定操作类型,始终执行切换
|
||||
|
||||
if debug_mode:
|
||||
self.logger.debug(f"操作决策 - 操作类型: {operation}, 是否需要执行操作: {action_needed}")
|
||||
|
||||
if not action_needed:
|
||||
# 补丁已经是目标状态,无需操作
|
||||
if operation == "enable":
|
||||
message = f"{game_version} 补丁已经是启用状态"
|
||||
else:
|
||||
message = f"{game_version} 补丁已经是禁用状态"
|
||||
|
||||
if debug_mode:
|
||||
self.logger.debug(f"{message}, 无需操作")
|
||||
|
||||
if not silent:
|
||||
QMessageBox.information(
|
||||
None,
|
||||
f"提示 - {self.app_name}",
|
||||
f"\n{message}\n",
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
return {"success": True, "message": message, "action": "none"}
|
||||
|
||||
if is_disabled:
|
||||
# 当前是禁用状态,需要启用
|
||||
if disabled_path and os.path.exists(disabled_path):
|
||||
# 从禁用文件名去掉.fain后缀
|
||||
enabled_path = disabled_path[:-5] # 去掉.fain
|
||||
if debug_mode:
|
||||
self.logger.debug(f"正在启用补丁 - 从 {disabled_path} 重命名为 {enabled_path}")
|
||||
os.rename(disabled_path, enabled_path)
|
||||
if debug_mode:
|
||||
self.logger.debug(f"已启用 {game_version} 的补丁,重命名文件成功")
|
||||
action = "enable"
|
||||
message = f"{game_version} 补丁已启用"
|
||||
else:
|
||||
# 未找到禁用的补丁文件,但状态是禁用
|
||||
message = f"未找到禁用的补丁文件: {disabled_path}"
|
||||
if debug_mode:
|
||||
self.logger.debug(f"{message}")
|
||||
return {"success": False, "message": message, "action": "none"}
|
||||
else:
|
||||
# 当前是启用状态,需要禁用
|
||||
# 查找正在使用的补丁文件
|
||||
active_patch_file = None
|
||||
for patch_path in patch_files_to_check:
|
||||
if os.path.exists(patch_path):
|
||||
active_patch_file = patch_path
|
||||
if debug_mode:
|
||||
self.logger.debug(f"找到活跃的补丁文件: {active_patch_file}")
|
||||
break
|
||||
|
||||
if active_patch_file:
|
||||
# 给补丁文件添加.fain后缀禁用它
|
||||
disabled_path = f"{active_patch_file}.fain"
|
||||
if debug_mode:
|
||||
self.logger.debug(f"正在禁用补丁 - 从 {active_patch_file} 重命名为 {disabled_path}")
|
||||
os.rename(active_patch_file, disabled_path)
|
||||
if debug_mode:
|
||||
self.logger.debug(f"已禁用 {game_version} 的补丁,重命名文件成功")
|
||||
action = "disable"
|
||||
message = f"{game_version} 补丁已禁用"
|
||||
else:
|
||||
# 未找到活跃的补丁文件,但状态是启用
|
||||
message = f"未找到启用的补丁文件,请检查游戏目录: {game_dir}"
|
||||
if debug_mode:
|
||||
self.logger.debug(f"{message}")
|
||||
return {"success": False, "message": message, "action": "none"}
|
||||
|
||||
# 非静默模式下显示操作结果
|
||||
if not silent:
|
||||
QMessageBox.information(
|
||||
None,
|
||||
f"操作成功 - {self.app_name}",
|
||||
f"\n{message}\n",
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
|
||||
if debug_mode:
|
||||
self.logger.debug(f"切换补丁状态操作完成 - 结果: 成功, 操作: {action}, 消息: {message}")
|
||||
|
||||
return {"success": True, "message": message, "action": action}
|
||||
|
||||
except Exception as e:
|
||||
error_message = f"切换 {game_version} 补丁状态时出错: {str(e)}"
|
||||
|
||||
if debug_mode:
|
||||
self.logger.debug(f"{error_message}")
|
||||
import traceback
|
||||
self.logger.debug(f"错误详情:\n{traceback.format_exc()}")
|
||||
|
||||
if not silent:
|
||||
QMessageBox.critical(
|
||||
None,
|
||||
f"操作失败 - {self.app_name}",
|
||||
f"\n{error_message}\n",
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
|
||||
return {"success": False, "message": error_message, "action": "none"}
|
||||
|
||||
def batch_toggle_patches(self, game_dirs, operation=None):
|
||||
"""批量切换多个游戏补丁的禁用/启用状态
|
||||
|
||||
Args:
|
||||
game_dirs: 游戏版本到游戏目录的映射字典
|
||||
operation: 指定操作,可以是"enable"、"disable"或None(None则自动切换当前状态)
|
||||
|
||||
Returns:
|
||||
tuple: (成功数量, 失败数量, 详细结果列表)
|
||||
"""
|
||||
success_count = 0
|
||||
fail_count = 0
|
||||
debug_mode = self._is_debug_mode()
|
||||
results = []
|
||||
|
||||
if debug_mode:
|
||||
self.logger.debug(f"开始批量切换补丁状态 - 操作: {operation}, 游戏数量: {len(game_dirs)}")
|
||||
self.logger.debug(f"游戏列表: {list(game_dirs.keys())}")
|
||||
|
||||
for version, path in game_dirs.items():
|
||||
try:
|
||||
if debug_mode:
|
||||
self.logger.debug(f"处理游戏 {version}, 目录: {path}")
|
||||
|
||||
# 在批量模式下使用静默操作
|
||||
result = self.toggle_patch(path, version, operation=operation, silent=True)
|
||||
|
||||
if debug_mode:
|
||||
self.logger.debug(f"游戏 {version} 操作结果: {result}")
|
||||
|
||||
if result["success"]:
|
||||
success_count += 1
|
||||
if debug_mode:
|
||||
self.logger.debug(f"游戏 {version} 操作成功,操作类型: {result['action']}")
|
||||
else:
|
||||
fail_count += 1
|
||||
if debug_mode:
|
||||
self.logger.debug(f"游戏 {version} 操作失败,原因: {result['message']}")
|
||||
|
||||
results.append({
|
||||
"version": version,
|
||||
"success": result["success"],
|
||||
"message": result["message"],
|
||||
"action": result["action"]
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
if debug_mode:
|
||||
self.logger.debug(f"切换 {version} 补丁状态时出错: {str(e)}")
|
||||
import traceback
|
||||
self.logger.debug(f"错误详情:\n{traceback.format_exc()}")
|
||||
|
||||
fail_count += 1
|
||||
results.append({
|
||||
"version": version,
|
||||
"success": False,
|
||||
"message": f"操作出错: {str(e)}",
|
||||
"action": "none"
|
||||
})
|
||||
|
||||
if debug_mode:
|
||||
self.logger.debug(f"批量切换补丁状态完成 - 成功: {success_count}, 失败: {fail_count}")
|
||||
|
||||
return success_count, fail_count, results
|
||||
|
||||
def show_toggle_result(self, success_count, fail_count, results=None):
|
||||
"""显示批量切换补丁状态的结果
|
||||
|
||||
Args:
|
||||
success_count: 成功操作的数量
|
||||
fail_count: 操作失败的数量
|
||||
results: 详细结果列表,如果提供,会显示更详细的信息
|
||||
"""
|
||||
result_text = f"\n批量操作完成!\n成功: {success_count} 个\n失败: {fail_count} 个\n"
|
||||
|
||||
# 如果有详细结果,添加到消息中
|
||||
if results:
|
||||
enabled_list = [r["version"] for r in results if r["success"] and r["action"] == "enable"]
|
||||
disabled_list = [r["version"] for r in results if r["success"] and r["action"] == "disable"]
|
||||
skipped_list = [r["version"] for r in results if r["success"] and r["action"] == "none"]
|
||||
fail_list = [r["version"] for r in results if not r["success"]]
|
||||
|
||||
if enabled_list:
|
||||
result_text += f"\n【已启用补丁】:\n{chr(10).join(enabled_list)}\n"
|
||||
|
||||
if disabled_list:
|
||||
result_text += f"\n【已禁用补丁】:\n{chr(10).join(disabled_list)}\n"
|
||||
|
||||
if skipped_list:
|
||||
result_text += f"\n【无需操作】:\n{chr(10).join(skipped_list)}\n"
|
||||
|
||||
if fail_list:
|
||||
result_text += f"\n【操作失败】:\n{chr(10).join(fail_list)}\n"
|
||||
|
||||
QMessageBox.information(
|
||||
None,
|
||||
f"批量操作完成 - {self.app_name}",
|
||||
result_text,
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
|
||||
def show_result(self):
|
||||
"""显示安装结果,区分不同情况"""
|
||||
# 获取当前安装状态
|
||||
installed_versions = [] # 成功安装的版本
|
||||
skipped_versions = [] # 已有补丁跳过的版本
|
||||
failed_versions = [] # 安装失败的版本
|
||||
not_found_versions = [] # 未找到的版本
|
||||
|
||||
# 获取所有游戏版本路径
|
||||
install_paths = self.main_window.download_manager.get_install_paths() if hasattr(self.main_window.download_manager, "get_install_paths") else {}
|
||||
|
||||
# 检查是否处于离线模式
|
||||
is_offline_mode = False
|
||||
if hasattr(self.main_window, 'offline_mode_manager'):
|
||||
is_offline_mode = self.main_window.offline_mode_manager.is_in_offline_mode()
|
||||
|
||||
# 获取本次实际安装的游戏列表
|
||||
installed_games = []
|
||||
|
||||
# 在线模式下使用download_queue_history
|
||||
if hasattr(self.main_window, 'download_queue_history') and self.main_window.download_queue_history:
|
||||
installed_games = self.main_window.download_queue_history
|
||||
|
||||
# 离线模式下使用offline_mode_manager.installed_games
|
||||
if is_offline_mode and hasattr(self.main_window.offline_mode_manager, 'installed_games'):
|
||||
installed_games = self.main_window.offline_mode_manager.installed_games
|
||||
|
||||
debug_mode = self._is_debug_mode()
|
||||
|
||||
if debug_mode:
|
||||
self.logger.debug(f"DEBUG: 显示安装结果,离线模式: {is_offline_mode}")
|
||||
self.logger.debug(f"DEBUG: 本次安装的游戏: {installed_games}")
|
||||
|
||||
for game_version, is_installed in self.main_window.installed_status.items():
|
||||
# 只处理install_paths中存在的游戏版本
|
||||
if game_version in install_paths:
|
||||
path = install_paths[game_version]
|
||||
|
||||
# 检查游戏是否存在但未通过本次安装补丁
|
||||
if is_installed:
|
||||
# 游戏已安装补丁
|
||||
if game_version in installed_games:
|
||||
# 本次成功安装
|
||||
installed_versions.append(game_version)
|
||||
else:
|
||||
# 已有补丁,被跳过下载
|
||||
skipped_versions.append(game_version)
|
||||
else:
|
||||
# 游戏未安装补丁
|
||||
if os.path.exists(path):
|
||||
# 游戏文件夹存在,但安装失败
|
||||
failed_versions.append(game_version)
|
||||
else:
|
||||
# 游戏文件夹不存在
|
||||
not_found_versions.append(game_version)
|
||||
|
||||
# 构建结果信息
|
||||
result_text = f"\n安装结果:\n"
|
||||
|
||||
# 总数统计 - 只显示本次实际安装的数量
|
||||
total_installed = len(installed_versions)
|
||||
total_failed = len(failed_versions)
|
||||
|
||||
result_text += f"安装成功:{total_installed} 个 安装失败:{total_failed} 个\n\n"
|
||||
|
||||
# 详细列表
|
||||
if installed_versions:
|
||||
result_text += f"【成功安装】:\n{chr(10).join(installed_versions)}\n\n"
|
||||
|
||||
if failed_versions:
|
||||
result_text += f"【安装失败】:\n{chr(10).join(failed_versions)}\n\n"
|
||||
|
||||
if not_found_versions:
|
||||
# 只有在真正检测到了游戏但未安装补丁时才显示
|
||||
result_text += f"【尚未安装补丁的游戏】:\n{chr(10).join(not_found_versions)}\n"
|
||||
|
||||
QMessageBox.information(
|
||||
self.main_window,
|
||||
f"安装完成 - {APP_NAME}",
|
||||
result_text
|
||||
)
|
||||
226
source/core/managers/privacy_manager.py
Normal file
@@ -0,0 +1,226 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
import json
|
||||
from PySide6.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QTextBrowser, QPushButton, QCheckBox, QLabel, QMessageBox
|
||||
from PySide6.QtCore import Qt
|
||||
|
||||
from data.privacy_policy import PRIVACY_POLICY_BRIEF, get_local_privacy_policy, PRIVACY_POLICY_VERSION
|
||||
from data.config import CACHE, APP_NAME, APP_VERSION
|
||||
from utils import msgbox_frame
|
||||
from utils.logger import setup_logger
|
||||
|
||||
class PrivacyManager:
|
||||
"""隐私协议管理器,负责显示隐私协议对话框并处理用户选择"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化隐私协议管理器"""
|
||||
# 初始化日志
|
||||
self.logger = setup_logger("privacy_manager")
|
||||
self.logger.info("正在初始化隐私协议管理器")
|
||||
# 确保缓存目录存在
|
||||
os.makedirs(CACHE, exist_ok=True)
|
||||
self.config_file = os.path.join(CACHE, "privacy_config.json")
|
||||
self.privacy_config = self._load_privacy_config()
|
||||
|
||||
# 获取隐私协议内容和版本
|
||||
self.logger.info("读取本地隐私协议文件")
|
||||
self.privacy_content, self.current_privacy_version, error = get_local_privacy_policy()
|
||||
if error:
|
||||
self.logger.warning(f"读取本地隐私协议文件警告: {error}")
|
||||
# 使用默认版本作为备用
|
||||
self.current_privacy_version = PRIVACY_POLICY_VERSION
|
||||
self.logger.info(f"隐私协议版本: {self.current_privacy_version}")
|
||||
|
||||
# 检查隐私协议版本和用户同意状态
|
||||
self.privacy_accepted = self._check_privacy_acceptance()
|
||||
|
||||
def _load_privacy_config(self):
|
||||
"""加载隐私协议配置
|
||||
|
||||
Returns:
|
||||
dict: 隐私协议配置信息
|
||||
"""
|
||||
if os.path.exists(self.config_file):
|
||||
try:
|
||||
with open(self.config_file, "r", encoding="utf-8") as f:
|
||||
config = json.load(f)
|
||||
return config
|
||||
except (json.JSONDecodeError, IOError) as e:
|
||||
self.logger.error(f"读取隐私配置失败: {e}")
|
||||
# 如果读取失败,返回空配置,强制显示隐私协议
|
||||
return {"privacy_accepted": False}
|
||||
return {"privacy_accepted": False}
|
||||
|
||||
def _check_privacy_acceptance(self):
|
||||
"""检查隐私协议是否需要重新同意
|
||||
|
||||
如果隐私协议版本变更,则需要重新同意
|
||||
|
||||
Returns:
|
||||
bool: 是否已有有效的隐私协议同意
|
||||
"""
|
||||
# 获取存储的版本信息
|
||||
stored_privacy_version = self.privacy_config.get("privacy_version", "0.0.0")
|
||||
stored_app_version = self.privacy_config.get("app_version", "0.0.0")
|
||||
privacy_accepted = self.privacy_config.get("privacy_accepted", False)
|
||||
|
||||
self.logger.info(f"存储的隐私协议版本: {stored_privacy_version}, 当前版本: {self.current_privacy_version}")
|
||||
self.logger.info(f"存储的应用版本: {stored_app_version}, 当前版本: {APP_VERSION}")
|
||||
self.logger.info(f"隐私协议接受状态: {privacy_accepted}")
|
||||
|
||||
# 如果隐私协议版本变更,需要重新同意
|
||||
if stored_privacy_version != self.current_privacy_version:
|
||||
self.logger.info("隐私协议版本已变更,需要重新同意")
|
||||
return False
|
||||
|
||||
# 返回当前的同意状态
|
||||
return privacy_accepted
|
||||
|
||||
def _save_privacy_config(self, accepted):
|
||||
"""保存隐私协议配置
|
||||
|
||||
Args:
|
||||
accepted: 用户是否同意隐私协议
|
||||
|
||||
Returns:
|
||||
bool: 配置是否保存成功
|
||||
"""
|
||||
try:
|
||||
# 确保目录存在
|
||||
os.makedirs(os.path.dirname(self.config_file), exist_ok=True)
|
||||
|
||||
# 写入配置文件,包含应用版本和隐私协议版本
|
||||
with open(self.config_file, "w", encoding="utf-8") as f:
|
||||
json.dump({
|
||||
"privacy_accepted": accepted,
|
||||
"privacy_version": self.current_privacy_version, # 保存当前隐私协议版本
|
||||
"app_version": APP_VERSION # 保存当前应用版本
|
||||
}, f, indent=2)
|
||||
|
||||
# 更新实例变量
|
||||
self.privacy_accepted = accepted
|
||||
self.privacy_config = {
|
||||
"privacy_accepted": accepted,
|
||||
"privacy_version": self.current_privacy_version,
|
||||
"app_version": APP_VERSION
|
||||
}
|
||||
return True
|
||||
except IOError as e:
|
||||
self.logger.error(f"保存隐私协议配置失败: {e}")
|
||||
# 显示保存失败的提示
|
||||
QMessageBox.warning(
|
||||
None,
|
||||
f"配置保存警告 - {APP_NAME}",
|
||||
f"隐私设置无法保存到配置文件,下次启动时可能需要重新确认。\n\n错误信息:{e}"
|
||||
)
|
||||
return False
|
||||
|
||||
def show_privacy_dialog(self):
|
||||
"""显示隐私协议对话框
|
||||
|
||||
Returns:
|
||||
bool: 用户是否同意隐私协议
|
||||
"""
|
||||
# 如果用户已经同意了隐私协议,直接返回True不显示对话框
|
||||
if self.privacy_accepted:
|
||||
self.logger.info("用户已同意当前版本的隐私协议,无需再次显示")
|
||||
return True
|
||||
|
||||
self.logger.info("首次运行或隐私协议版本变更,显示隐私对话框")
|
||||
|
||||
# 创建隐私协议对话框
|
||||
dialog = QDialog()
|
||||
dialog.setWindowTitle(f"隐私政策 - {APP_NAME}")
|
||||
dialog.setMinimumSize(600, 400)
|
||||
dialog.setWindowFlags(dialog.windowFlags() & ~Qt.WindowContextHelpButtonHint)
|
||||
|
||||
# 创建布局
|
||||
layout = QVBoxLayout(dialog)
|
||||
|
||||
# 添加标题和版本信息
|
||||
title_label = QLabel(f"请阅读并同意以下隐私政策 (更新日期: {self.current_privacy_version})")
|
||||
title_label.setStyleSheet("font-size: 14px; font-weight: bold;")
|
||||
layout.addWidget(title_label)
|
||||
|
||||
# 添加隐私协议文本框
|
||||
text_browser = QTextBrowser()
|
||||
# 这里使用PRIVACY_POLICY_BRIEF而不是self.privacy_content,保持UI简洁
|
||||
text_browser.setMarkdown(PRIVACY_POLICY_BRIEF)
|
||||
text_browser.setOpenExternalLinks(True)
|
||||
layout.addWidget(text_browser)
|
||||
|
||||
# 添加同意选择框
|
||||
checkbox = QCheckBox("我已阅读并同意上述隐私政策")
|
||||
layout.addWidget(checkbox)
|
||||
|
||||
# 添加按钮
|
||||
buttons_layout = QHBoxLayout()
|
||||
agree_button = QPushButton("同意并继续")
|
||||
agree_button.setEnabled(False) # 初始状态为禁用
|
||||
decline_button = QPushButton("不同意并退出")
|
||||
buttons_layout.addWidget(agree_button)
|
||||
buttons_layout.addWidget(decline_button)
|
||||
layout.addLayout(buttons_layout)
|
||||
|
||||
# 连接选择框状态变化 - 修复勾选后按钮不亮起的问题
|
||||
def on_checkbox_state_changed(state):
|
||||
self.logger.debug(f"复选框状态变更为: {state}")
|
||||
agree_button.setEnabled(state == 2) # Qt.Checked 在 PySide6 中值为 2
|
||||
|
||||
checkbox.stateChanged.connect(on_checkbox_state_changed)
|
||||
|
||||
# 连接按钮点击事件
|
||||
agree_button.clicked.connect(lambda: self._on_agree(dialog))
|
||||
decline_button.clicked.connect(lambda: self._on_decline(dialog))
|
||||
|
||||
# 显示对话框
|
||||
result = dialog.exec()
|
||||
|
||||
# 返回用户选择结果
|
||||
return self.privacy_accepted
|
||||
|
||||
def _on_agree(self, dialog):
|
||||
"""处理用户同意隐私协议
|
||||
|
||||
Args:
|
||||
dialog: 对话框实例
|
||||
"""
|
||||
# 保存配置并更新状态
|
||||
self._save_privacy_config(True)
|
||||
dialog.accept()
|
||||
|
||||
def _on_decline(self, dialog):
|
||||
"""处理用户拒绝隐私协议
|
||||
|
||||
Args:
|
||||
dialog: 对话框实例
|
||||
"""
|
||||
# 显示拒绝信息
|
||||
msg_box = msgbox_frame(
|
||||
f"退出 - {APP_NAME}",
|
||||
"\n您需要同意隐私政策才能使用本软件。\n软件将立即退出。\n",
|
||||
QMessageBox.Ok,
|
||||
)
|
||||
msg_box.exec()
|
||||
|
||||
# 保存拒绝状态
|
||||
self._save_privacy_config(False)
|
||||
dialog.reject()
|
||||
|
||||
def is_privacy_accepted(self):
|
||||
"""检查用户是否已同意隐私协议
|
||||
|
||||
Returns:
|
||||
bool: 用户是否已同意隐私协议
|
||||
"""
|
||||
return self.privacy_accepted
|
||||
|
||||
def reset_privacy_agreement(self):
|
||||
"""重置隐私协议同意状态,用于测试或重新显示隐私协议
|
||||
|
||||
Returns:
|
||||
bool: 重置是否成功
|
||||
"""
|
||||
return self._save_privacy_config(False)
|
||||
971
source/core/managers/ui_manager.py
Normal file
@@ -0,0 +1,971 @@
|
||||
from PySide6.QtGui import QIcon, QAction, QFont, QCursor, QActionGroup
|
||||
from PySide6.QtWidgets import QMessageBox, QMainWindow, QMenu, QPushButton
|
||||
from PySide6.QtCore import Qt, QRect
|
||||
import webbrowser
|
||||
import os
|
||||
|
||||
from utils import load_base64_image, msgbox_frame, resource_path
|
||||
from data.config import APP_NAME, APP_VERSION, LOG_FILE
|
||||
from core.ipv6_manager import IPv6Manager # 导入新的IPv6Manager类
|
||||
|
||||
class UIManager:
|
||||
def __init__(self, main_window):
|
||||
"""初始化UI管理器
|
||||
|
||||
Args:
|
||||
main_window: 主窗口实例,用于设置UI元素
|
||||
"""
|
||||
self.main_window = main_window
|
||||
# 使用getattr获取ui属性,如果不存在则为None
|
||||
self.ui = getattr(main_window, 'ui', None)
|
||||
self.debug_action = None
|
||||
self.turbo_download_action = None
|
||||
self.dev_menu = None
|
||||
self.privacy_menu = None # 隐私协议菜单
|
||||
self.about_menu = None # 关于菜单
|
||||
self.about_btn = None # 关于按钮
|
||||
|
||||
# 获取主窗口的IPv6Manager实例
|
||||
self.ipv6_manager = getattr(main_window, 'ipv6_manager', None)
|
||||
|
||||
def setup_ui(self):
|
||||
"""设置UI元素,包括窗口图标、标题和菜单"""
|
||||
# 设置窗口图标
|
||||
import os
|
||||
from utils import resource_path
|
||||
icon_path = resource_path(os.path.join("IMG", "ICO", "icon.png"))
|
||||
if os.path.exists(icon_path):
|
||||
self.main_window.setWindowIcon(QIcon(icon_path))
|
||||
|
||||
# 获取当前离线模式状态
|
||||
is_offline_mode = False
|
||||
if hasattr(self.main_window, 'offline_mode_manager'):
|
||||
is_offline_mode = self.main_window.offline_mode_manager.is_in_offline_mode()
|
||||
|
||||
# 设置窗口标题和UI标题标签
|
||||
mode_indicator = "[离线模式]" if is_offline_mode else "[在线模式]"
|
||||
self.main_window.setWindowTitle(f"{APP_NAME} v{APP_VERSION} {mode_indicator}")
|
||||
|
||||
# 更新UI中的标题标签
|
||||
if hasattr(self.main_window, 'ui') and hasattr(self.main_window.ui, 'title_label'):
|
||||
self.main_window.ui.title_label.setText(f"{APP_NAME} v{APP_VERSION} {mode_indicator}")
|
||||
|
||||
# 创建关于按钮
|
||||
self._create_about_button()
|
||||
|
||||
# 设置菜单
|
||||
self._setup_help_menu()
|
||||
self._setup_about_menu() # 新增关于菜单
|
||||
self._setup_settings_menu()
|
||||
|
||||
def _create_about_button(self):
|
||||
"""创建"关于"按钮"""
|
||||
if not self.ui or not hasattr(self.ui, 'menu_area'):
|
||||
return
|
||||
|
||||
# 获取菜单字体和样式
|
||||
menu_font = self._get_menu_font()
|
||||
|
||||
# 创建关于按钮
|
||||
self.about_btn = QPushButton("关于", self.ui.menu_area)
|
||||
self.about_btn.setObjectName(u"about_btn")
|
||||
|
||||
# 获取帮助按钮的位置和样式
|
||||
help_btn_x = 0
|
||||
help_btn_width = 0
|
||||
if hasattr(self.ui, 'help_btn'):
|
||||
help_btn_x = self.ui.help_btn.x()
|
||||
help_btn_width = self.ui.help_btn.width()
|
||||
|
||||
# 设置位置在"帮助"按钮右侧
|
||||
self.about_btn.setGeometry(QRect(help_btn_x + help_btn_width + 20, 1, 80, 28))
|
||||
self.about_btn.setFont(menu_font)
|
||||
self.about_btn.setCursor(QCursor(Qt.CursorShape.PointingHandCursor))
|
||||
|
||||
# 复制帮助按钮的样式
|
||||
if hasattr(self.ui, 'help_btn'):
|
||||
self.about_btn.setStyleSheet(self.ui.help_btn.styleSheet())
|
||||
else:
|
||||
# 默认样式
|
||||
self.about_btn.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: transparent;
|
||||
color: white;
|
||||
border: none;
|
||||
text-align: left;
|
||||
padding-left: 10px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #F47A5B;
|
||||
border-radius: 4px;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #D25A3C;
|
||||
border-radius: 4px;
|
||||
}
|
||||
""")
|
||||
|
||||
def _setup_help_menu(self):
|
||||
"""设置"帮助"菜单"""
|
||||
if not self.ui or not hasattr(self.ui, 'menu_2'):
|
||||
return
|
||||
|
||||
# 获取菜单字体
|
||||
menu_font = self._get_menu_font()
|
||||
|
||||
# 创建菜单项 - 移除"项目主页",添加"常见问题"和"提交错误"
|
||||
faq_action = QAction("常见问题", self.main_window)
|
||||
faq_action.triggered.connect(self.open_faq_page)
|
||||
faq_action.setFont(menu_font)
|
||||
|
||||
report_issue_action = QAction("提交错误", self.main_window)
|
||||
report_issue_action.triggered.connect(self.open_issues_page)
|
||||
report_issue_action.setFont(menu_font)
|
||||
|
||||
# 清除现有菜单项并添加新的菜单项
|
||||
self.ui.menu_2.clear()
|
||||
self.ui.menu_2.addAction(faq_action)
|
||||
self.ui.menu_2.addAction(report_issue_action)
|
||||
|
||||
# 连接按钮点击事件,如果使用按钮式菜单
|
||||
if hasattr(self.ui, 'help_btn'):
|
||||
# 按钮已经连接到显示菜单,不需要额外处理
|
||||
pass
|
||||
|
||||
def _setup_about_menu(self):
|
||||
"""设置"关于"菜单"""
|
||||
# 获取菜单字体
|
||||
menu_font = self._get_menu_font()
|
||||
|
||||
# 创建关于菜单
|
||||
self.about_menu = QMenu("关于", self.main_window)
|
||||
self.about_menu.setFont(menu_font)
|
||||
|
||||
# 设置菜单样式
|
||||
font_family = menu_font.family()
|
||||
menu_style = self._get_menu_style(font_family)
|
||||
self.about_menu.setStyleSheet(menu_style)
|
||||
|
||||
# 创建菜单项
|
||||
about_project_action = QAction("关于本项目", self.main_window)
|
||||
about_project_action.setFont(menu_font)
|
||||
about_project_action.triggered.connect(self.show_about_dialog)
|
||||
|
||||
# 添加项目主页选项(从帮助菜单移动过来)
|
||||
project_home_action = QAction("Github项目主页", self.main_window)
|
||||
project_home_action.setFont(menu_font)
|
||||
project_home_action.triggered.connect(self.open_project_home_page)
|
||||
|
||||
# 添加加入QQ群选项
|
||||
qq_group_action = QAction("加入QQ群", self.main_window)
|
||||
qq_group_action.setFont(menu_font)
|
||||
qq_group_action.triggered.connect(self.open_qq_group)
|
||||
|
||||
# 创建隐私协议菜单
|
||||
self._setup_privacy_menu()
|
||||
|
||||
# 添加到关于菜单
|
||||
self.about_menu.addAction(about_project_action)
|
||||
self.about_menu.addAction(project_home_action)
|
||||
self.about_menu.addAction(qq_group_action)
|
||||
self.about_menu.addSeparator()
|
||||
self.about_menu.addMenu(self.privacy_menu)
|
||||
|
||||
# 连接按钮点击事件
|
||||
if self.about_btn:
|
||||
self.about_btn.clicked.connect(lambda: self.show_menu(self.about_menu, self.about_btn))
|
||||
|
||||
def _setup_privacy_menu(self):
|
||||
"""设置"隐私协议"菜单"""
|
||||
# 获取菜单字体
|
||||
menu_font = self._get_menu_font()
|
||||
|
||||
# 创建隐私协议子菜单
|
||||
self.privacy_menu = QMenu("隐私协议", self.main_window)
|
||||
self.privacy_menu.setFont(menu_font)
|
||||
|
||||
# 设置与其他菜单一致的样式
|
||||
font_family = menu_font.family()
|
||||
menu_style = self._get_menu_style(font_family)
|
||||
self.privacy_menu.setStyleSheet(menu_style)
|
||||
|
||||
# 添加子选项
|
||||
view_privacy_action = QAction("查看完整隐私协议", self.main_window)
|
||||
view_privacy_action.setFont(menu_font)
|
||||
view_privacy_action.triggered.connect(self.open_privacy_policy)
|
||||
|
||||
revoke_privacy_action = QAction("撤回隐私协议", self.main_window)
|
||||
revoke_privacy_action.setFont(menu_font)
|
||||
revoke_privacy_action.triggered.connect(self.revoke_privacy_agreement)
|
||||
|
||||
# 添加到子菜单
|
||||
self.privacy_menu.addAction(view_privacy_action)
|
||||
self.privacy_menu.addAction(revoke_privacy_action)
|
||||
|
||||
def _get_menu_style(self, font_family):
|
||||
"""获取统一的菜单样式"""
|
||||
return f"""
|
||||
QMenu {{
|
||||
background-color: #E96948;
|
||||
color: white;
|
||||
font-family: "{font_family}";
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
border: 1px solid #F47A5B;
|
||||
padding: 8px;
|
||||
border-radius: 6px;
|
||||
margin-top: 2px;
|
||||
}}
|
||||
QMenu::item {{
|
||||
padding: 6px 20px 6px 15px;
|
||||
background-color: transparent;
|
||||
min-width: 120px;
|
||||
color: white;
|
||||
font-family: "{font_family}";
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}}
|
||||
QMenu::item:selected {{
|
||||
background-color: #F47A5B;
|
||||
border-radius: 4px;
|
||||
}}
|
||||
QMenu::separator {{
|
||||
height: 1px;
|
||||
background-color: #F47A5B;
|
||||
margin: 5px 15px;
|
||||
}}
|
||||
QMenu::item:checked {{
|
||||
background-color: #D25A3C;
|
||||
border-radius: 4px;
|
||||
}}
|
||||
"""
|
||||
|
||||
def _get_menu_font(self):
|
||||
"""获取菜单字体"""
|
||||
font_family = "Arial" # 默认字体族
|
||||
|
||||
try:
|
||||
from PySide6.QtGui import QFontDatabase
|
||||
|
||||
# 尝试加载字体
|
||||
font_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "fonts", "SmileySans-Oblique.ttf")
|
||||
if os.path.exists(font_path):
|
||||
font_id = QFontDatabase.addApplicationFont(font_path)
|
||||
if font_id != -1:
|
||||
font_family = QFontDatabase.applicationFontFamilies(font_id)[0]
|
||||
|
||||
# 创建菜单字体
|
||||
menu_font = QFont(font_family, 14)
|
||||
menu_font.setBold(True)
|
||||
return menu_font
|
||||
|
||||
except Exception as e:
|
||||
print(f"加载字体失败: {e}")
|
||||
# 返回默认字体
|
||||
menu_font = QFont(font_family, 14)
|
||||
menu_font.setBold(True)
|
||||
return menu_font
|
||||
|
||||
def _setup_settings_menu(self):
|
||||
"""设置"设置"菜单"""
|
||||
if not self.ui or not hasattr(self.ui, 'menu'):
|
||||
return
|
||||
|
||||
# 获取菜单字体
|
||||
menu_font = self._get_menu_font()
|
||||
font_family = menu_font.family()
|
||||
|
||||
# 创建工作模式子菜单
|
||||
self.work_mode_menu = QMenu("工作模式", self.main_window)
|
||||
self.work_mode_menu.setFont(menu_font)
|
||||
self.work_mode_menu.setStyleSheet(self._get_menu_style(font_family))
|
||||
|
||||
# 获取当前离线模式状态
|
||||
is_offline_mode = False
|
||||
if hasattr(self.main_window, 'offline_mode_manager'):
|
||||
is_offline_mode = self.main_window.offline_mode_manager.is_in_offline_mode()
|
||||
|
||||
# 创建在线模式和离线模式选项
|
||||
self.online_mode_action = QAction("在线模式", self.main_window, checkable=True)
|
||||
self.online_mode_action.setFont(menu_font)
|
||||
self.online_mode_action.setChecked(not is_offline_mode) # 根据当前状态设置
|
||||
|
||||
self.offline_mode_action = QAction("离线模式", self.main_window, checkable=True)
|
||||
self.offline_mode_action.setFont(menu_font)
|
||||
self.offline_mode_action.setChecked(is_offline_mode) # 根据当前状态设置
|
||||
|
||||
# 将两个模式选项添加到同一个互斥组
|
||||
mode_group = QActionGroup(self.main_window)
|
||||
mode_group.addAction(self.online_mode_action)
|
||||
mode_group.addAction(self.offline_mode_action)
|
||||
mode_group.setExclusive(True) # 确保只能选择一个模式
|
||||
|
||||
# 连接切换事件
|
||||
self.online_mode_action.triggered.connect(lambda: self.switch_work_mode("online"))
|
||||
self.offline_mode_action.triggered.connect(lambda: self.switch_work_mode("offline"))
|
||||
|
||||
# 添加到工作模式子菜单
|
||||
self.work_mode_menu.addAction(self.online_mode_action)
|
||||
self.work_mode_menu.addAction(self.offline_mode_action)
|
||||
|
||||
# 创建开发者选项子菜单
|
||||
self.dev_menu = QMenu("开发者选项", self.main_window)
|
||||
self.dev_menu.setFont(menu_font) # 设置与UI_install.py中相同的字体
|
||||
|
||||
# 使用和主菜单相同的样式
|
||||
menu_style = self._get_menu_style(font_family)
|
||||
self.dev_menu.setStyleSheet(menu_style)
|
||||
|
||||
# 创建Debug子菜单
|
||||
self.debug_submenu = QMenu("Debug模式", self.main_window)
|
||||
self.debug_submenu.setFont(menu_font)
|
||||
self.debug_submenu.setStyleSheet(menu_style)
|
||||
|
||||
# 创建hosts文件选项子菜单
|
||||
self.hosts_submenu = QMenu("hosts文件选项", self.main_window)
|
||||
self.hosts_submenu.setFont(menu_font)
|
||||
self.hosts_submenu.setStyleSheet(menu_style)
|
||||
|
||||
# 添加IPv6支持选项
|
||||
self.ipv6_action = QAction("启用IPv6支持", self.main_window, checkable=True)
|
||||
self.ipv6_action.setFont(menu_font)
|
||||
|
||||
# 添加IPv6检测按钮,用于显示详细信息
|
||||
self.ipv6_test_action = QAction("测试IPv6连接", self.main_window)
|
||||
self.ipv6_test_action.setFont(menu_font)
|
||||
if self.ipv6_manager:
|
||||
self.ipv6_test_action.triggered.connect(self.ipv6_manager.show_ipv6_details)
|
||||
else:
|
||||
self.ipv6_test_action.triggered.connect(self.show_ipv6_manager_not_ready)
|
||||
|
||||
# 创建IPv6支持子菜单
|
||||
self.ipv6_submenu = QMenu("IPv6支持", self.main_window)
|
||||
self.ipv6_submenu.setFont(menu_font)
|
||||
self.ipv6_submenu.setStyleSheet(menu_style)
|
||||
|
||||
# 检查配置中是否已启用IPv6
|
||||
config = getattr(self.main_window, 'config', {})
|
||||
ipv6_enabled = False
|
||||
if isinstance(config, dict):
|
||||
ipv6_enabled = config.get("ipv6_enabled", False)
|
||||
|
||||
self.ipv6_action.setChecked(ipv6_enabled)
|
||||
|
||||
# 连接IPv6支持切换事件
|
||||
self.ipv6_action.triggered.connect(self._handle_ipv6_toggle)
|
||||
|
||||
# 将选项添加到IPv6子菜单
|
||||
self.ipv6_submenu.addAction(self.ipv6_action)
|
||||
self.ipv6_submenu.addAction(self.ipv6_test_action)
|
||||
|
||||
# 添加hosts子选项
|
||||
self.restore_hosts_action = QAction("还原软件备份的hosts文件", self.main_window)
|
||||
self.restore_hosts_action.setFont(menu_font)
|
||||
self.restore_hosts_action.triggered.connect(self.restore_hosts_backup)
|
||||
|
||||
self.clean_hosts_action = QAction("手动删除软件添加的hosts条目", self.main_window)
|
||||
self.clean_hosts_action.setFont(menu_font)
|
||||
self.clean_hosts_action.triggered.connect(self.clean_hosts_entries)
|
||||
|
||||
# 添加禁用自动还原hosts的选项
|
||||
self.disable_auto_restore_action = QAction("禁用关闭/重启自动还原hosts", self.main_window, checkable=True)
|
||||
self.disable_auto_restore_action.setFont(menu_font)
|
||||
|
||||
# 从配置中读取当前状态
|
||||
config = getattr(self.main_window, 'config', {})
|
||||
disable_auto_restore = False
|
||||
if isinstance(config, dict):
|
||||
disable_auto_restore = config.get("disable_auto_restore_hosts", False)
|
||||
|
||||
self.disable_auto_restore_action.setChecked(disable_auto_restore)
|
||||
self.disable_auto_restore_action.triggered.connect(self.toggle_disable_auto_restore_hosts)
|
||||
|
||||
# 添加打开hosts文件选项
|
||||
self.open_hosts_action = QAction("打开hosts文件", self.main_window)
|
||||
self.open_hosts_action.setFont(menu_font)
|
||||
self.open_hosts_action.triggered.connect(self.open_hosts_file)
|
||||
|
||||
# 添加到hosts子菜单
|
||||
self.hosts_submenu.addAction(self.disable_auto_restore_action)
|
||||
self.hosts_submenu.addAction(self.restore_hosts_action)
|
||||
self.hosts_submenu.addAction(self.clean_hosts_action)
|
||||
self.hosts_submenu.addAction(self.open_hosts_action)
|
||||
|
||||
# 创建Debug开关选项
|
||||
self.debug_action = QAction("Debug开关", self.main_window, checkable=True)
|
||||
self.debug_action.setFont(menu_font)
|
||||
|
||||
# 安全地获取config属性
|
||||
config = getattr(self.main_window, 'config', {})
|
||||
debug_mode = False
|
||||
if isinstance(config, dict):
|
||||
debug_mode = config.get("debug_mode", False)
|
||||
|
||||
self.debug_action.setChecked(debug_mode)
|
||||
|
||||
# 安全地连接toggle_debug_mode方法
|
||||
if hasattr(self.main_window, 'toggle_debug_mode'):
|
||||
self.debug_action.triggered.connect(self.main_window.toggle_debug_mode)
|
||||
|
||||
# 创建打开log文件选项
|
||||
self.open_log_action = QAction("打开log.txt", self.main_window)
|
||||
self.open_log_action.setFont(menu_font)
|
||||
# 初始状态根据debug模式设置启用状态
|
||||
self.open_log_action.setEnabled(debug_mode)
|
||||
|
||||
# 连接打开log文件的事件
|
||||
self.open_log_action.triggered.connect(self.open_log_file)
|
||||
|
||||
# 添加到Debug子菜单
|
||||
self.debug_submenu.addAction(self.debug_action)
|
||||
self.debug_submenu.addAction(self.open_log_action)
|
||||
|
||||
# 创建下载设置子菜单
|
||||
self.download_settings_menu = QMenu("下载设置", self.main_window)
|
||||
self.download_settings_menu.setFont(menu_font)
|
||||
self.download_settings_menu.setStyleSheet(menu_style)
|
||||
|
||||
# "修改下载源"按钮移至下载设置菜单
|
||||
self.switch_source_action = QAction("修改下载源", self.main_window)
|
||||
self.switch_source_action.setFont(menu_font)
|
||||
self.switch_source_action.setEnabled(True)
|
||||
self.switch_source_action.triggered.connect(self.show_under_development)
|
||||
|
||||
# 添加下载线程设置选项
|
||||
self.thread_settings_action = QAction("下载线程设置", self.main_window)
|
||||
self.thread_settings_action.setFont(menu_font)
|
||||
# 连接到下载线程设置对话框
|
||||
self.thread_settings_action.triggered.connect(self.show_download_thread_settings)
|
||||
|
||||
# 添加到下载设置子菜单
|
||||
self.download_settings_menu.addAction(self.switch_source_action)
|
||||
self.download_settings_menu.addAction(self.thread_settings_action)
|
||||
|
||||
# 添加到主菜单
|
||||
self.ui.menu.addMenu(self.work_mode_menu) # 添加工作模式子菜单
|
||||
self.ui.menu.addMenu(self.download_settings_menu) # 添加下载设置子菜单
|
||||
self.ui.menu.addSeparator()
|
||||
self.ui.menu.addMenu(self.dev_menu) # 添加开发者选项子菜单
|
||||
|
||||
# 添加Debug子菜单到开发者选项菜单
|
||||
self.dev_menu.addMenu(self.debug_submenu)
|
||||
self.dev_menu.addMenu(self.hosts_submenu) # 添加hosts文件选项子菜单
|
||||
self.dev_menu.addMenu(self.ipv6_submenu) # 添加IPv6支持子菜单
|
||||
|
||||
def _handle_ipv6_toggle(self, enabled):
|
||||
"""处理IPv6支持切换事件
|
||||
|
||||
Args:
|
||||
enabled: 是否启用IPv6支持
|
||||
"""
|
||||
if not self.ipv6_manager:
|
||||
# 显示错误提示
|
||||
msg_box = self._create_message_box("错误", "\nIPv6管理器尚未初始化,请稍后再试。\n")
|
||||
msg_box.exec()
|
||||
# 恢复复选框状态
|
||||
self.ipv6_action.setChecked(not enabled)
|
||||
return
|
||||
|
||||
if enabled:
|
||||
# 先显示警告提示
|
||||
warning_msg_box = self._create_message_box(
|
||||
"警告",
|
||||
"\n目前IPv6支持功能仍在测试阶段,可能会发生意料之外的bug!\n\n您确定需要启用吗?\n",
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
|
||||
)
|
||||
response = warning_msg_box.exec()
|
||||
|
||||
# 如果用户选择不启用,直接返回
|
||||
if response != QMessageBox.StandardButton.Yes:
|
||||
# 恢复复选框状态
|
||||
self.ipv6_action.setChecked(False)
|
||||
return
|
||||
|
||||
# 显示正在校验IPv6的提示
|
||||
msg_box = self._create_message_box("IPv6检测", "\n正在校验是否支持IPv6,请稍候...\n")
|
||||
msg_box.open() # 使用open而不是exec,这样不会阻塞UI
|
||||
|
||||
# 处理消息队列,确保对话框显示
|
||||
from PySide6.QtCore import QCoreApplication
|
||||
QCoreApplication.processEvents()
|
||||
|
||||
# 检查IPv6是否可用
|
||||
ipv6_available = self.ipv6_manager.check_ipv6_availability()
|
||||
|
||||
# 关闭提示对话框
|
||||
msg_box.accept()
|
||||
|
||||
if not ipv6_available:
|
||||
# 显示IPv6不可用的提示
|
||||
error_msg_box = self._create_message_box(
|
||||
"IPv6不可用",
|
||||
"\n未检测到可用的IPv6连接,无法启用IPv6支持。\n\n请确保您的网络环境支持IPv6且已正确配置。\n"
|
||||
)
|
||||
error_msg_box.exec()
|
||||
# 恢复复选框状态
|
||||
self.ipv6_action.setChecked(False)
|
||||
return False
|
||||
|
||||
# 使用IPv6Manager处理切换
|
||||
success = self.ipv6_manager.toggle_ipv6_support(enabled)
|
||||
# 如果切换失败,恢复复选框状态
|
||||
if not success:
|
||||
self.ipv6_action.setChecked(not enabled)
|
||||
|
||||
def show_menu(self, menu, button):
|
||||
"""显示菜单
|
||||
|
||||
Args:
|
||||
menu: 要显示的菜单
|
||||
button: 触发菜单的按钮
|
||||
"""
|
||||
# 检查Ui_install中是否定义了show_menu方法
|
||||
if hasattr(self.ui, 'show_menu'):
|
||||
# 如果存在,使用UI中定义的方法
|
||||
self.ui.show_menu(menu, button)
|
||||
else:
|
||||
# 否则,使用默认的弹出方法
|
||||
global_pos = button.mapToGlobal(button.rect().bottomLeft())
|
||||
menu.popup(global_pos)
|
||||
|
||||
def open_project_home_page(self):
|
||||
"""打开项目主页"""
|
||||
webbrowser.open("https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT")
|
||||
|
||||
def open_github_page(self):
|
||||
"""打开项目GitHub页面"""
|
||||
webbrowser.open("https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT")
|
||||
|
||||
def open_faq_page(self):
|
||||
"""打开常见问题页面"""
|
||||
import locale
|
||||
# 根据系统语言选择FAQ页面
|
||||
system_lang = locale.getdefaultlocale()[0]
|
||||
if system_lang and system_lang.startswith('zh'):
|
||||
webbrowser.open("https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/blob/master/FAQ.md")
|
||||
else:
|
||||
webbrowser.open("https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/blob/master/FAQ-en.md")
|
||||
|
||||
def open_issues_page(self):
|
||||
"""打开GitHub问题页面"""
|
||||
webbrowser.open("https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/issues")
|
||||
|
||||
def open_qq_group(self):
|
||||
"""打开QQ群链接"""
|
||||
webbrowser.open("https://qm.qq.com/q/g9i04i5eec")
|
||||
|
||||
def open_privacy_policy(self):
|
||||
"""打开完整隐私协议(在GitHub上)"""
|
||||
webbrowser.open("https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/blob/master/PRIVACY.md")
|
||||
|
||||
def revoke_privacy_agreement(self):
|
||||
"""撤回隐私协议同意,并重启软件"""
|
||||
# 创建确认对话框
|
||||
msg_box = self._create_message_box(
|
||||
"确认操作",
|
||||
"\n您确定要撤回隐私协议同意吗?\n\n撤回后软件将立即重启,您需要重新阅读并同意隐私协议。\n",
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||
)
|
||||
|
||||
reply = msg_box.exec()
|
||||
if reply == QMessageBox.StandardButton.Yes:
|
||||
# 用户确认撤回
|
||||
try:
|
||||
# 导入隐私管理器
|
||||
from core.privacy_manager import PrivacyManager
|
||||
import sys
|
||||
import subprocess
|
||||
import os
|
||||
|
||||
# 创建实例并重置隐私协议同意
|
||||
privacy_manager = PrivacyManager()
|
||||
if privacy_manager.reset_privacy_agreement():
|
||||
# 显示重启提示
|
||||
restart_msg = self._create_message_box(
|
||||
"操作成功",
|
||||
"\n已成功撤回隐私协议同意。\n\n软件将立即重启。\n"
|
||||
)
|
||||
restart_msg.exec()
|
||||
|
||||
# 获取当前执行的Python解释器路径和脚本路径
|
||||
python_executable = sys.executable
|
||||
script_path = os.path.abspath(sys.argv[0])
|
||||
|
||||
# 构建重启命令
|
||||
restart_cmd = [python_executable, script_path]
|
||||
|
||||
# 启动新进程
|
||||
subprocess.Popen(restart_cmd)
|
||||
|
||||
# 退出当前进程
|
||||
sys.exit(0)
|
||||
else:
|
||||
# 显示失败提示
|
||||
fail_msg = self._create_message_box(
|
||||
"操作失败",
|
||||
"\n撤回隐私协议同意失败。\n\n请检查应用权限或稍后再试。\n"
|
||||
)
|
||||
fail_msg.exec()
|
||||
except Exception as e:
|
||||
# 显示错误提示
|
||||
error_msg = self._create_message_box(
|
||||
"错误",
|
||||
f"\n撤回隐私协议同意时发生错误:\n\n{str(e)}\n"
|
||||
)
|
||||
error_msg.exec()
|
||||
|
||||
def _create_message_box(self, title, message, buttons=QMessageBox.StandardButton.Ok):
|
||||
"""创建统一风格的消息框
|
||||
|
||||
Args:
|
||||
title: 消息框标题
|
||||
message: 消息内容
|
||||
buttons: 按钮类型,默认为确定按钮
|
||||
|
||||
Returns:
|
||||
QMessageBox: 配置好的消息框实例
|
||||
"""
|
||||
msg_box = msgbox_frame(
|
||||
f"{title} - {APP_NAME}",
|
||||
message,
|
||||
buttons,
|
||||
)
|
||||
return msg_box
|
||||
|
||||
def show_under_development(self):
|
||||
"""显示功能正在开发中的提示"""
|
||||
msg_box = self._create_message_box("提示", "\n该功能正在开发中,敬请期待!\n")
|
||||
msg_box.exec()
|
||||
|
||||
def show_download_thread_settings(self):
|
||||
"""显示下载线程设置对话框"""
|
||||
if hasattr(self.main_window, 'download_manager'):
|
||||
self.main_window.download_manager.show_download_thread_settings()
|
||||
else:
|
||||
# 如果下载管理器不可用,显示错误信息
|
||||
msg_box = self._create_message_box("错误", "\n下载管理器未初始化,无法修改下载线程设置。\n")
|
||||
msg_box.exec()
|
||||
|
||||
def open_log_file(self):
|
||||
"""打开当前日志文件"""
|
||||
try:
|
||||
# 检查日志文件是否存在
|
||||
if os.path.exists(LOG_FILE):
|
||||
# 获取日志文件大小
|
||||
file_size = os.path.getsize(LOG_FILE)
|
||||
if file_size == 0:
|
||||
msg_box = self._create_message_box("提示", f"\n当前日志文件 {os.path.basename(LOG_FILE)} 存在但为空。\n\n日志文件位置:{os.path.abspath(LOG_FILE)}")
|
||||
msg_box.exec()
|
||||
return
|
||||
|
||||
# 根据文件大小决定是使用文本查看器还是直接打开
|
||||
if file_size > 1024 * 1024: # 大于1MB
|
||||
# 文件较大,显示警告
|
||||
msg_box = self._create_message_box(
|
||||
"警告",
|
||||
f"\n日志文件较大 ({file_size / 1024 / 1024:.2f} MB),是否仍要打开?\n\n日志文件位置:{os.path.abspath(LOG_FILE)}",
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
|
||||
)
|
||||
if msg_box.exec() != QMessageBox.StandardButton.Yes:
|
||||
return
|
||||
|
||||
# 使用操作系统默认程序打开日志文件
|
||||
if os.name == 'nt': # Windows
|
||||
os.startfile(LOG_FILE)
|
||||
else: # macOS 和 Linux
|
||||
import subprocess
|
||||
subprocess.call(['xdg-open', LOG_FILE])
|
||||
else:
|
||||
# 文件不存在,显示信息
|
||||
# 搜索log文件夹下所有可能的日志文件
|
||||
root_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
||||
log_dir = os.path.join(root_dir, "log")
|
||||
|
||||
# 如果log文件夹不存在,尝试创建它
|
||||
if not os.path.exists(log_dir):
|
||||
try:
|
||||
os.makedirs(log_dir, exist_ok=True)
|
||||
msg_box = self._create_message_box(
|
||||
"信息",
|
||||
f"\n日志文件夹不存在,已创建新的日志文件夹:\n{log_dir}\n\n请在启用调试模式后重试。"
|
||||
)
|
||||
msg_box.exec()
|
||||
return
|
||||
except Exception as e:
|
||||
msg_box = self._create_message_box(
|
||||
"错误",
|
||||
f"\n创建日志文件夹失败:\n\n{str(e)}"
|
||||
)
|
||||
msg_box.exec()
|
||||
return
|
||||
|
||||
# 搜索log文件夹中的日志文件
|
||||
try:
|
||||
log_files = [f for f in os.listdir(log_dir) if f.startswith("log-") and f.endswith(".txt")]
|
||||
except Exception as e:
|
||||
msg_box = self._create_message_box(
|
||||
"错误",
|
||||
f"\n无法读取日志文件夹:\n\n{str(e)}"
|
||||
)
|
||||
msg_box.exec()
|
||||
return
|
||||
|
||||
if log_files:
|
||||
# 按照修改时间排序,获取最新的日志文件
|
||||
log_files.sort(key=lambda x: os.path.getmtime(os.path.join(log_dir, x)), reverse=True)
|
||||
latest_log = os.path.join(log_dir, log_files[0])
|
||||
|
||||
# 获取最新日志文件的创建时间信息
|
||||
try:
|
||||
log_datetime = "-".join(os.path.basename(latest_log)[4:-4].split("-")[:2])
|
||||
log_date = log_datetime.split("-")[0]
|
||||
log_time = log_datetime.split("-")[1] if "-" in log_datetime else "未知时间"
|
||||
date_info = f"日期: {log_date[:4]}-{log_date[4:6]}-{log_date[6:]}"
|
||||
time_info = f"时间: {log_time[:2]}:{log_time[2:4]}:{log_time[4:]}"
|
||||
except:
|
||||
date_info = "日期未知 "
|
||||
time_info = "时间未知"
|
||||
|
||||
msg_box = self._create_message_box(
|
||||
"信息",
|
||||
f"\n当前日志文件 {os.path.basename(LOG_FILE)} 不存在。\n\n"
|
||||
f"发现最新的日志文件: {os.path.basename(latest_log)}\n"
|
||||
f"({date_info}{time_info})\n\n"
|
||||
f"是否打开此文件?",
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
|
||||
)
|
||||
|
||||
if msg_box.exec() == QMessageBox.StandardButton.Yes:
|
||||
if os.name == 'nt': # Windows
|
||||
os.startfile(latest_log)
|
||||
else: # macOS 和 Linux
|
||||
import subprocess
|
||||
subprocess.call(['xdg-open', latest_log])
|
||||
return
|
||||
|
||||
# 如果没有找到任何日志文件或用户选择不打开最新的日志文件
|
||||
msg_box = self._create_message_box(
|
||||
"信息",
|
||||
f"\n没有找到有效的日志文件。\n\n"
|
||||
f"预期的日志文件夹:{log_dir}\n\n"
|
||||
f"请确认调试模式已启用,并执行一些操作后再查看日志。"
|
||||
)
|
||||
msg_box.exec()
|
||||
|
||||
except Exception as e:
|
||||
msg_box = self._create_message_box("错误", f"\n处理日志文件时出错:\n\n{str(e)}\n\n文件位置:{os.path.abspath(LOG_FILE)}")
|
||||
msg_box.exec()
|
||||
|
||||
def restore_hosts_backup(self):
|
||||
"""还原软件备份的hosts文件"""
|
||||
if hasattr(self.main_window, 'download_manager') and hasattr(self.main_window.download_manager, 'hosts_manager'):
|
||||
try:
|
||||
# 调用恢复hosts文件的方法
|
||||
result = self.main_window.download_manager.hosts_manager.restore()
|
||||
|
||||
if result:
|
||||
msg_box = self._create_message_box("成功", "\nhosts文件已成功还原为备份版本。\n")
|
||||
else:
|
||||
msg_box = self._create_message_box("警告", "\n还原hosts文件失败或没有找到备份文件。\n")
|
||||
|
||||
msg_box.exec()
|
||||
except Exception as e:
|
||||
msg_box = self._create_message_box("错误", f"\n还原hosts文件时发生错误:\n\n{str(e)}\n")
|
||||
msg_box.exec()
|
||||
else:
|
||||
msg_box = self._create_message_box("错误", "\n无法访问hosts管理器。\n")
|
||||
msg_box.exec()
|
||||
|
||||
def clean_hosts_entries(self):
|
||||
"""手动删除软件添加的hosts条目"""
|
||||
if hasattr(self.main_window, 'download_manager') and hasattr(self.main_window.download_manager, 'hosts_manager'):
|
||||
try:
|
||||
# 调用清理hosts条目的方法,强制清理即使禁用了自动还原
|
||||
result = self.main_window.download_manager.hosts_manager.check_and_clean_all_entries(force_clean=True)
|
||||
|
||||
if result:
|
||||
msg_box = self._create_message_box("成功", "\n已成功清理软件添加的hosts条目。\n")
|
||||
else:
|
||||
msg_box = self._create_message_box("提示", "\n未发现软件添加的hosts条目或清理操作失败。\n")
|
||||
|
||||
msg_box.exec()
|
||||
except Exception as e:
|
||||
msg_box = self._create_message_box("错误", f"\n清理hosts条目时发生错误:\n\n{str(e)}\n")
|
||||
msg_box.exec()
|
||||
else:
|
||||
msg_box = self._create_message_box("错误", "\n无法访问hosts管理器。\n")
|
||||
msg_box.exec()
|
||||
|
||||
def open_hosts_file(self):
|
||||
"""打开系统hosts文件"""
|
||||
try:
|
||||
# 获取hosts文件路径
|
||||
hosts_path = os.path.join(os.environ['SystemRoot'], 'System32', 'drivers', 'etc', 'hosts')
|
||||
|
||||
# 检查文件是否存在
|
||||
if os.path.exists(hosts_path):
|
||||
# 使用操作系统默认程序打开hosts文件
|
||||
if os.name == 'nt': # Windows
|
||||
# 尝试以管理员权限打开记事本编辑hosts文件
|
||||
try:
|
||||
# 使用PowerShell以管理员身份启动记事本
|
||||
subprocess.Popen(["powershell", "Start-Process", "notepad", hosts_path, "-Verb", "RunAs"])
|
||||
except Exception as e:
|
||||
# 如果失败,尝试直接打开
|
||||
os.startfile(hosts_path)
|
||||
else: # macOS 和 Linux
|
||||
import subprocess
|
||||
subprocess.call(['xdg-open', hosts_path])
|
||||
else:
|
||||
msg_box = self._create_message_box("错误", f"\nhosts文件不存在:\n{hosts_path}\n")
|
||||
msg_box.exec()
|
||||
except Exception as e:
|
||||
msg_box = self._create_message_box("错误", f"\n打开hosts文件时发生错误:\n\n{str(e)}\n")
|
||||
msg_box.exec()
|
||||
|
||||
def toggle_disable_auto_restore_hosts(self, checked):
|
||||
"""切换禁用自动还原hosts的状态
|
||||
|
||||
Args:
|
||||
checked: 是否禁用自动还原
|
||||
"""
|
||||
if hasattr(self.main_window, 'download_manager') and hasattr(self.main_window.download_manager, 'hosts_manager'):
|
||||
try:
|
||||
# 调用HostsManager的方法设置自动还原标志
|
||||
result = self.main_window.download_manager.hosts_manager.set_auto_restore_disabled(checked)
|
||||
|
||||
if result:
|
||||
# 同时更新内部配置,确保立即生效
|
||||
if hasattr(self.main_window, 'config'):
|
||||
self.main_window.config['disable_auto_restore_hosts'] = checked
|
||||
|
||||
# 显示成功提示
|
||||
status = "禁用" if checked else "启用"
|
||||
msg_box = self._create_message_box(
|
||||
"设置已更新",
|
||||
f"\n已{status}关闭/重启时自动还原hosts。\n\n{'hosts将被保留' if checked else 'hosts将在关闭时自动还原'}。\n"
|
||||
)
|
||||
msg_box.exec()
|
||||
else:
|
||||
# 如果设置失败,恢复复选框状态
|
||||
self.disable_auto_restore_action.setChecked(not checked)
|
||||
msg_box = self._create_message_box(
|
||||
"设置失败",
|
||||
"\n更新设置时发生错误,请稍后再试。\n"
|
||||
)
|
||||
msg_box.exec()
|
||||
except Exception as e:
|
||||
# 如果发生异常,恢复复选框状态
|
||||
self.disable_auto_restore_action.setChecked(not checked)
|
||||
msg_box = self._create_message_box(
|
||||
"错误",
|
||||
f"\n更新设置时发生异常:\n\n{str(e)}\n"
|
||||
)
|
||||
msg_box.exec()
|
||||
else:
|
||||
# 如果hosts管理器不可用,恢复复选框状态
|
||||
self.disable_auto_restore_action.setChecked(not checked)
|
||||
msg_box = self._create_message_box(
|
||||
"错误",
|
||||
"\nhosts管理器不可用,无法更新设置。\n"
|
||||
)
|
||||
msg_box.exec()
|
||||
|
||||
def show_about_dialog(self):
|
||||
"""显示关于对话框"""
|
||||
about_text = f"""
|
||||
<p><b>{APP_NAME} v{APP_VERSION}</b></p>
|
||||
<p>GitHub: <a href="https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT">https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT</a></p>
|
||||
<p>原作: <a href="https://github.com/Yanam1Anna">Yanam1Anna</a></p>
|
||||
<p>此应用根据 <a href="https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/blob/master/LICENSE">GPL-3.0 许可证</a> 授权。</p>
|
||||
<br>
|
||||
<p><b>感谢:</b></p>
|
||||
<p>- <a href="https://github.com/HTony03">HTony03</a>:对原项目部分源码的重构、逻辑优化和功能实现提供了支持。</p>
|
||||
<p>- <a href="https://github.com/ABSIDIA">钨鸮</a>:对于云端资源存储提供了支持。</p>
|
||||
<p>- <a href="https://github.com/XIU2/CloudflareSpeedTest">XIU2/CloudflareSpeedTest</a>:提供了 IP 优选功能的核心支持。</p>
|
||||
<p>- <a href="https://github.com/hosxy/aria2-fast">hosxy/aria2-fast</a>:提供了修改版aria2c,提高了下载速度和性能。</p>
|
||||
"""
|
||||
msg_box = msgbox_frame(
|
||||
f"关于 - {APP_NAME}",
|
||||
about_text,
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
msg_box.setTextFormat(Qt.TextFormat.RichText) # 使用Qt.TextFormat
|
||||
msg_box.exec()
|
||||
|
||||
def show_ipv6_manager_not_ready(self):
|
||||
"""显示IPv6管理器未准备好的提示"""
|
||||
msg_box = self._create_message_box("错误", "\nIPv6管理器尚未初始化,请稍后再试。\n")
|
||||
msg_box.exec()
|
||||
|
||||
def switch_work_mode(self, mode):
|
||||
"""切换工作模式
|
||||
|
||||
Args:
|
||||
mode: 要切换的模式,"online"或"offline"
|
||||
"""
|
||||
# 检查主窗口是否有离线模式管理器
|
||||
if not hasattr(self.main_window, 'offline_mode_manager'):
|
||||
# 如果没有离线模式管理器,创建提示
|
||||
msg_box = self._create_message_box(
|
||||
"错误",
|
||||
"\n离线模式管理器未初始化,无法切换工作模式。\n"
|
||||
)
|
||||
msg_box.exec()
|
||||
|
||||
# 恢复选择状态
|
||||
self.online_mode_action.setChecked(True)
|
||||
self.offline_mode_action.setChecked(False)
|
||||
return
|
||||
|
||||
if mode == "offline":
|
||||
# 尝试切换到离线模式
|
||||
success = self.main_window.offline_mode_manager.set_offline_mode(True)
|
||||
if not success:
|
||||
# 如果切换失败,恢复选择状态
|
||||
self.online_mode_action.setChecked(True)
|
||||
self.offline_mode_action.setChecked(False)
|
||||
return
|
||||
|
||||
# 更新配置
|
||||
self.main_window.config["offline_mode"] = True
|
||||
self.main_window.save_config(self.main_window.config)
|
||||
|
||||
# 在离线模式下始终启用开始安装按钮
|
||||
if hasattr(self.main_window, 'set_start_button_enabled'):
|
||||
self.main_window.set_start_button_enabled(True)
|
||||
|
||||
# 清除版本警告标志
|
||||
if hasattr(self.main_window, 'version_warning'):
|
||||
self.main_window.version_warning = False
|
||||
|
||||
# 显示提示
|
||||
msg_box = self._create_message_box(
|
||||
"模式已切换",
|
||||
"\n已切换到离线模式。\n\n将使用本地补丁文件进行安装,不会从网络下载补丁。\n"
|
||||
)
|
||||
msg_box.exec()
|
||||
else:
|
||||
# 切换到在线模式
|
||||
self.main_window.offline_mode_manager.set_offline_mode(False)
|
||||
|
||||
# 更新配置
|
||||
self.main_window.config["offline_mode"] = False
|
||||
self.main_window.save_config(self.main_window.config)
|
||||
|
||||
# 重新获取云端配置
|
||||
if hasattr(self.main_window, 'fetch_cloud_config'):
|
||||
self.main_window.fetch_cloud_config()
|
||||
|
||||
# 如果当前版本过低,设置版本警告标志
|
||||
if hasattr(self.main_window, 'last_error_message') and self.main_window.last_error_message == "update_required":
|
||||
# 设置版本警告标志
|
||||
if hasattr(self.main_window, 'version_warning'):
|
||||
self.main_window.version_warning = True
|
||||
|
||||
# 显示提示
|
||||
msg_box = self._create_message_box(
|
||||
"模式已切换",
|
||||
"\n已切换到在线模式。\n\n将从网络下载补丁进行安装。\n"
|
||||
)
|
||||
msg_box.exec()
|
||||
149
source/core/managers/window_manager.py
Normal file
@@ -0,0 +1,149 @@
|
||||
from PySide6.QtCore import Qt, QPoint, QRect, QSize
|
||||
from PySide6.QtGui import QPainterPath, QRegion
|
||||
|
||||
class WindowManager:
|
||||
"""窗口管理器类,用于处理窗口的基本行为,如拖拽、调整大小和圆角设置"""
|
||||
|
||||
def __init__(self, parent_window):
|
||||
"""初始化窗口管理器
|
||||
|
||||
Args:
|
||||
parent_window: 父窗口实例
|
||||
"""
|
||||
self.window = parent_window
|
||||
self.ui = parent_window.ui
|
||||
|
||||
# 拖动窗口相关变量
|
||||
self._drag_position = QPoint()
|
||||
self._is_dragging = False
|
||||
|
||||
# 窗口比例
|
||||
self.aspect_ratio = 16 / 9
|
||||
self.updateRoundedCorners = True
|
||||
|
||||
# 设置圆角窗口
|
||||
self.setRoundedCorners()
|
||||
|
||||
def setRoundedCorners(self):
|
||||
"""设置窗口圆角"""
|
||||
# 实现圆角窗口
|
||||
path = QPainterPath()
|
||||
path.addRoundedRect(self.window.rect(), 20, 20)
|
||||
mask = QRegion(path.toFillPolygon().toPolygon())
|
||||
self.window.setMask(mask)
|
||||
|
||||
# 更新resize事件时更新圆角
|
||||
self.updateRoundedCorners = True
|
||||
|
||||
def handle_mouse_press(self, event):
|
||||
"""处理鼠标按下事件
|
||||
|
||||
Args:
|
||||
event: 鼠标事件
|
||||
"""
|
||||
if event.button() == Qt.MouseButton.LeftButton:
|
||||
# 只有当鼠标在标题栏区域时才可以拖动
|
||||
if hasattr(self.ui, 'title_bar') and self.ui.title_bar.geometry().contains(event.position().toPoint()):
|
||||
self._is_dragging = True
|
||||
self._drag_position = event.globalPosition().toPoint() - self.window.frameGeometry().topLeft()
|
||||
event.accept()
|
||||
|
||||
def handle_mouse_move(self, event):
|
||||
"""处理鼠标移动事件
|
||||
|
||||
Args:
|
||||
event: 鼠标事件
|
||||
"""
|
||||
if event.buttons() & Qt.MouseButton.LeftButton and self._is_dragging:
|
||||
self.window.move(event.globalPosition().toPoint() - self._drag_position)
|
||||
event.accept()
|
||||
|
||||
def handle_mouse_release(self, event):
|
||||
"""处理鼠标释放事件
|
||||
|
||||
Args:
|
||||
event: 鼠标事件
|
||||
"""
|
||||
if event.button() == Qt.MouseButton.LeftButton:
|
||||
self._is_dragging = False
|
||||
event.accept()
|
||||
|
||||
def handle_resize(self, event):
|
||||
"""当窗口大小改变时更新圆角和维持纵横比
|
||||
|
||||
Args:
|
||||
event: 窗口大小改变事件
|
||||
"""
|
||||
# 计算基于当前宽度的合适高度,以维持16:9比例
|
||||
new_width = event.size().width()
|
||||
new_height = int(new_width / self.aspect_ratio)
|
||||
|
||||
if new_height != event.size().height():
|
||||
# 阻止变形,保持比例
|
||||
self.window.resize(new_width, new_height)
|
||||
|
||||
# 更新主容器大小
|
||||
if hasattr(self.ui, 'main_container'):
|
||||
self.ui.main_container.setGeometry(0, 0, new_width, new_height)
|
||||
|
||||
# 更新内容容器大小
|
||||
if hasattr(self.ui, 'content_container'):
|
||||
self.ui.content_container.setGeometry(0, 0, new_width, new_height)
|
||||
|
||||
# 更新标题栏宽度和高度
|
||||
if hasattr(self.ui, 'title_bar'):
|
||||
self.ui.title_bar.setGeometry(0, 0, new_width, 35)
|
||||
|
||||
# 更新菜单区域
|
||||
if hasattr(self.ui, 'menu_area'):
|
||||
self.ui.menu_area.setGeometry(0, 35, new_width, 30)
|
||||
|
||||
# 更新内容区域大小
|
||||
if hasattr(self.ui, 'inner_content'):
|
||||
self.ui.inner_content.setGeometry(0, 65, new_width, new_height - 65)
|
||||
|
||||
# 更新背景图大小
|
||||
if hasattr(self.ui, 'Mainbg'):
|
||||
self.ui.Mainbg.setGeometry(0, 0, new_width, new_height - 65)
|
||||
|
||||
if hasattr(self.ui, 'loadbg'):
|
||||
self.ui.loadbg.setGeometry(0, 0, new_width, new_height - 65)
|
||||
|
||||
# 调整按钮位置 - 固定在右侧
|
||||
right_margin = 20 # 减小右边距,使按钮更靠右
|
||||
if hasattr(self.ui, 'button_container'):
|
||||
btn_width = 211 # 扩大后的容器宽度
|
||||
btn_height = 111 # 扩大后的容器高度
|
||||
x_pos = new_width - btn_width - right_margin
|
||||
y_pos = int((new_height - 65) * 0.18) - 10 # 从0.28改为0.18,向上移动
|
||||
self.ui.button_container.setGeometry(x_pos, y_pos, btn_width, btn_height)
|
||||
|
||||
# 添加禁/启用补丁按钮容器的位置调整
|
||||
if hasattr(self.ui, 'toggle_patch_container'):
|
||||
btn_width = 211 # 扩大后的容器宽度
|
||||
btn_height = 111 # 扩大后的容器高度
|
||||
x_pos = new_width - btn_width - right_margin
|
||||
y_pos = int((new_height - 65) * 0.36) - 10 # 从0.46改为0.36,向上移动
|
||||
self.ui.toggle_patch_container.setGeometry(x_pos, y_pos, btn_width, btn_height)
|
||||
|
||||
# 添加卸载补丁按钮容器的位置调整
|
||||
if hasattr(self.ui, 'uninstall_container'):
|
||||
btn_width = 211 # 扩大后的容器宽度
|
||||
btn_height = 111 # 扩大后的容器高度
|
||||
x_pos = new_width - btn_width - right_margin
|
||||
y_pos = int((new_height - 65) * 0.54) - 10 # 从0.64改为0.54,向上移动
|
||||
self.ui.uninstall_container.setGeometry(x_pos, y_pos, btn_width, btn_height)
|
||||
|
||||
if hasattr(self.ui, 'exit_container'):
|
||||
btn_width = 211 # 扩大后的容器宽度
|
||||
btn_height = 111 # 扩大后的容器高度
|
||||
x_pos = new_width - btn_width - right_margin
|
||||
y_pos = int((new_height - 65) * 0.72) - 10 # 从0.82改为0.72,向上移动
|
||||
self.ui.exit_container.setGeometry(x_pos, y_pos, btn_width, btn_height)
|
||||
|
||||
# 更新圆角
|
||||
if hasattr(self, 'updateRoundedCorners') and self.updateRoundedCorners:
|
||||
path = QPainterPath()
|
||||
path.addRoundedRect(self.window.rect(), 20, 20)
|
||||
mask = QRegion(path.toFillPolygon().toPolygon())
|
||||
self.window.setMask(mask)
|
||||
@@ -29,6 +29,11 @@ class OfflineModeManager:
|
||||
self.offline_patches = {} # 存储离线补丁信息 {补丁名称: 文件路径}
|
||||
self.is_offline_mode = False
|
||||
self.installed_games = [] # 跟踪本次实际安装的游戏
|
||||
# 保持对哈希线程的引用,避免运行中被销毁
|
||||
self.hash_thread = None
|
||||
# 解压线程与进度窗口引用,避免运行中被销毁,且确保UI可更新
|
||||
self.extraction_thread = None
|
||||
self.extraction_progress_window = None
|
||||
|
||||
def _is_debug_mode(self):
|
||||
"""检查是否处于调试模式
|
||||
@@ -523,6 +528,10 @@ class OfflineModeManager:
|
||||
|
||||
# 保存引用以便后续使用
|
||||
self.hash_thread = hash_thread
|
||||
try:
|
||||
self.hash_thread.finished.connect(lambda: setattr(self, 'hash_thread', None))
|
||||
except Exception:
|
||||
pass
|
||||
hash_thread.start()
|
||||
|
||||
def _on_hash_check_finished(self, result, game_version, install_tasks):
|
||||
@@ -586,16 +595,30 @@ class OfflineModeManager:
|
||||
self.process_next_offline_install_task(install_tasks)
|
||||
|
||||
def _on_extraction_finished_with_hash_check(self, success, error_message, game_version, install_tasks):
|
||||
"""解压完成后进行哈希校验
|
||||
|
||||
Args:
|
||||
success: 是否解压成功
|
||||
error_message: 错误信息
|
||||
game_version: 游戏版本
|
||||
install_tasks: 剩余的安装任务列表
|
||||
"""
|
||||
# 这个方法已不再使用,保留为空以兼容旧版本调用
|
||||
"""解压完成后进行哈希校验(后台线程回调)"""
|
||||
# 关闭解压进度窗口
|
||||
try:
|
||||
if self.extraction_progress_window and self.extraction_progress_window.isVisible():
|
||||
self.extraction_progress_window.close()
|
||||
except Exception:
|
||||
pass
|
||||
self.extraction_progress_window = None
|
||||
|
||||
# 清理线程引用
|
||||
self.extraction_thread = None
|
||||
|
||||
if not success:
|
||||
# 解压失败,提示并继续下一个任务
|
||||
msgbox_frame(
|
||||
f"安装错误 - {self.app_name}",
|
||||
error_message or f"\n{game_version} 的安装过程中发生错误。\n",
|
||||
QMessageBox.StandardButton.Ok
|
||||
).exec()
|
||||
self.process_next_offline_install_task(install_tasks)
|
||||
return
|
||||
|
||||
# 解压成功,进入安装后哈希校验
|
||||
self._perform_hash_check(game_version, install_tasks)
|
||||
|
||||
def on_extraction_thread_finished(self, success, error_message, game_version, install_tasks):
|
||||
"""解压线程完成后的处理(兼容旧版本)
|
||||
@@ -823,14 +846,12 @@ class OfflineModeManager:
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 有未找到离线补丁文件的游戏: {self.missing_offline_patches}")
|
||||
|
||||
# 先显示已安装的结果
|
||||
# 不再先弹出安装结果,直接询问是否联网继续
|
||||
if self.installed_games:
|
||||
installed_msg = f"已成功安装以下补丁:\n\n{chr(10).join(self.installed_games)}\n\n"
|
||||
else:
|
||||
installed_msg = ""
|
||||
|
||||
# 使用QTimer延迟显示询问对话框,确保安装结果窗口先显示并关闭
|
||||
QTimer.singleShot(500, lambda: self._show_missing_patches_dialog(installed_msg))
|
||||
QTimer.singleShot(100, lambda: self._show_missing_patches_dialog(installed_msg))
|
||||
else:
|
||||
# 恢复UI状态
|
||||
self.main_window.setEnabled(True)
|
||||
@@ -846,96 +867,58 @@ class OfflineModeManager:
|
||||
logger.debug(f"DEBUG: 补丁文件: {patch_file}")
|
||||
logger.debug(f"DEBUG: 游戏目录: {game_folder}")
|
||||
|
||||
# 显示安装进度窗口
|
||||
self.main_window.hash_msg_box = self.main_window.hash_manager.hash_pop_window(check_type="offline_installation", is_offline=True)
|
||||
|
||||
# 使用后台线程进行解压,避免阻塞UI
|
||||
try:
|
||||
# 确保游戏目录存在
|
||||
os.makedirs(game_folder, exist_ok=True)
|
||||
|
||||
# 从GAME_INFO获取目标文件名
|
||||
target_filename = os.path.basename(GAME_INFO[game_version]["install_path"])
|
||||
if not target_filename:
|
||||
raise ValueError(f"未知的游戏版本或配置错误: {game_version}")
|
||||
|
||||
# 直接从源7z文件解压
|
||||
with py7zr.SevenZipFile(patch_file, mode="r") as archive:
|
||||
file_list = archive.getnames()
|
||||
target_file_in_archive = None
|
||||
|
||||
# 查找压缩包中的目标文件
|
||||
for f_path in file_list:
|
||||
if target_filename in f_path:
|
||||
target_file_in_archive = f_path
|
||||
break
|
||||
|
||||
if not target_file_in_archive:
|
||||
raise FileNotFoundError(f"在压缩包 {os.path.basename(patch_file)} 中未找到目标文件 {target_filename}")
|
||||
|
||||
# 使用临时目录来解压单个文件
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
archive.extract(path=temp_dir, targets=[target_file_in_archive])
|
||||
extracted_file_path = os.path.join(temp_dir, target_file_in_archive)
|
||||
|
||||
# 最终目标路径
|
||||
target_path = os.path.join(game_folder, target_filename)
|
||||
|
||||
# 复制到游戏目录
|
||||
shutil.copy2(extracted_file_path, target_path)
|
||||
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 已解压并复制文件到 {target_path}")
|
||||
|
||||
# 对于NEKOPARA After,还需要处理签名文件
|
||||
if game_version == "NEKOPARA After":
|
||||
sig_filename = f"{target_filename}.sig"
|
||||
sig_file_in_archive = None
|
||||
|
||||
for f_path in file_list:
|
||||
if sig_filename in f_path:
|
||||
sig_file_in_archive = f_path
|
||||
break
|
||||
|
||||
if sig_file_in_archive:
|
||||
# 创建非阻塞的解压进度窗口
|
||||
self.extraction_progress_window = self.main_window.create_extraction_progress_window()
|
||||
try:
|
||||
archive.extract(path=temp_dir, targets=[sig_file_in_archive])
|
||||
extracted_sig_path = os.path.join(temp_dir, sig_file_in_archive)
|
||||
sig_target = os.path.join(game_folder, sig_filename)
|
||||
shutil.copy2(extracted_sig_path, sig_target)
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 已解压并复制签名文件到 {sig_target}")
|
||||
except py7zr.exceptions.CrcError as sig_e:
|
||||
if debug_mode:
|
||||
logger.warning(f"DEBUG: 签名文件 '{sig_e.filename}' CRC校验失败,已忽略此文件。")
|
||||
self.extraction_progress_window.show()
|
||||
QtWidgets.QApplication.processEvents()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 进行安装后的哈希校验
|
||||
self._perform_hash_check(game_version, install_tasks)
|
||||
# 启动解压线程
|
||||
self.extraction_thread = self.main_window.create_extraction_thread(
|
||||
patch_file, game_folder, plugin_path, game_version
|
||||
)
|
||||
|
||||
except py7zr.exceptions.CrcError as e:
|
||||
if debug_mode:
|
||||
logger.error(f"DEBUG: CRC校验失败,文件可能已损坏: {e}")
|
||||
logger.error(f"DEBUG: 错误堆栈: {traceback.format_exc()}")
|
||||
# 连接进度更新到窗口控件
|
||||
if self.extraction_thread and self.extraction_progress_window:
|
||||
self.extraction_thread.progress.connect(
|
||||
lambda percent, status: (
|
||||
self.extraction_progress_window.progress_bar.setValue(percent),
|
||||
self.extraction_progress_window.status_label.setText(status)
|
||||
)
|
||||
)
|
||||
|
||||
self.main_window.close_hash_msg_box()
|
||||
# 完成后进入哈希校验
|
||||
self.extraction_thread.finished.connect(
|
||||
lambda success, error, gv: self._on_extraction_finished_with_hash_check(success, error, gv, install_tasks)
|
||||
)
|
||||
|
||||
msgbox_frame(
|
||||
f"安装错误 - {self.app_name}",
|
||||
f"\n补丁文件 {os.path.basename(patch_file)} 在解压时CRC校验失败。\n"
|
||||
f"这通常意味着文件已损坏,请尝试重新下载该文件。\n\n"
|
||||
f"游戏: {game_version}\n"
|
||||
f"错误文件: {e.filename}\n\n"
|
||||
"跳过此游戏的安装。",
|
||||
QMessageBox.StandardButton.Ok
|
||||
).exec()
|
||||
# 线程结束时清理引用
|
||||
try:
|
||||
self.extraction_thread.finished.connect(lambda *_: setattr(self, 'extraction_thread', None))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self.extraction_thread.start()
|
||||
|
||||
self.process_next_offline_install_task(install_tasks)
|
||||
except Exception as e:
|
||||
if debug_mode:
|
||||
logger.error(f"DEBUG: 离线安装任务处理失败: {e}")
|
||||
logger.error(f"DEBUG: 错误堆栈: {traceback.format_exc()}")
|
||||
|
||||
# 关闭安装进度窗口
|
||||
self.main_window.close_hash_msg_box()
|
||||
# 关闭可能存在的解压进度窗口
|
||||
try:
|
||||
if self.extraction_progress_window and self.extraction_progress_window.isVisible():
|
||||
self.extraction_progress_window.close()
|
||||
except Exception:
|
||||
pass
|
||||
self.extraction_progress_window = None
|
||||
|
||||
# 显示错误消息
|
||||
msgbox_frame(
|
||||
@@ -1005,3 +988,8 @@ class OfflineModeManager:
|
||||
# 恢复UI状态
|
||||
self.main_window.setEnabled(True)
|
||||
self.main_window.ui.start_install_text.setText("开始安装")
|
||||
# 用户拒绝联网后,再显示本次安装结果
|
||||
try:
|
||||
QTimer.singleShot(100, self.main_window.show_result)
|
||||
except Exception:
|
||||
pass
|
||||
@@ -269,6 +269,18 @@ class PatchDetector:
|
||||
|
||||
self.main_window.setEnabled(True)
|
||||
self.main_window.ui.start_install_text.setText("开始安装")
|
||||
# 当处于离线模式且存在缺失补丁需要联网继续时,暂不立即显示安装结果
|
||||
should_defer_result = False
|
||||
try:
|
||||
offline_mgr = getattr(self.main_window, 'offline_mode_manager', None)
|
||||
if offline_mgr and offline_mgr.is_in_offline_mode():
|
||||
missing_list = getattr(offline_mgr, 'missing_offline_patches', [])
|
||||
if missing_list:
|
||||
should_defer_result = True
|
||||
except Exception:
|
||||
should_defer_result = False
|
||||
|
||||
if not should_defer_result:
|
||||
QTimer.singleShot(100, self.main_window.show_result)
|
||||
|
||||
def on_offline_pre_hash_finished(self, updated_status, game_dirs):
|
||||
|
||||
@@ -118,6 +118,9 @@ class MainWindow(QMainWindow):
|
||||
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 赋值)
|
||||
|
||||
# 设置关闭按钮事件连接
|
||||
if hasattr(self.ui, 'close_btn'):
|
||||
@@ -478,6 +481,60 @@ class MainWindow(QMainWindow):
|
||||
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')
|
||||
|
||||
# 清理离线管理器中的线程
|
||||
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()
|
||||
|
||||
@@ -640,11 +697,16 @@ class MainWindow(QMainWindow):
|
||||
install_paths = self.download_manager.get_install_paths()
|
||||
|
||||
# 使用异步方式进行哈希预检查
|
||||
hash_thread = self.patch_detector.create_hash_thread("pre", install_paths)
|
||||
hash_thread.pre_finished.connect(
|
||||
self.pre_hash_thread = self.patch_detector.create_hash_thread("pre", install_paths)
|
||||
self.pre_hash_thread.pre_finished.connect(
|
||||
lambda updated_status: self.on_pre_hash_finished(updated_status, game_dirs)
|
||||
)
|
||||
hash_thread.start()
|
||||
# 在线程自然结束时清理引用
|
||||
try:
|
||||
self.pre_hash_thread.finished.connect(lambda: setattr(self, 'pre_hash_thread', None))
|
||||
except Exception:
|
||||
pass
|
||||
self.pre_hash_thread.start()
|
||||
|
||||
def on_pre_hash_finished(self, updated_status, game_dirs):
|
||||
"""哈希预检查完成后的回调"""
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import os
|
||||
import shutil
|
||||
import py7zr
|
||||
from PySide6.QtCore import QThread, Signal, QCoreApplication
|
||||
from PySide6.QtCore import QThread, Signal
|
||||
from data.config import PLUGIN, GAME_INFO
|
||||
|
||||
class ExtractionThread(QThread):
|
||||
@@ -21,24 +21,28 @@ class ExtractionThread(QThread):
|
||||
# 确保游戏目录存在
|
||||
os.makedirs(self.game_folder, exist_ok=True)
|
||||
|
||||
# 发送初始进度信号
|
||||
self.progress.emit(0, f"开始处理 {self.game_version} 的补丁文件...")
|
||||
# 确保UI更新
|
||||
QCoreApplication.processEvents()
|
||||
def update_progress(percent: int, message: str):
|
||||
try:
|
||||
self.progress.emit(percent, message)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
update_progress(0, f"开始处理 {self.game_version} 的补丁文件...")
|
||||
|
||||
# 支持外部请求中断
|
||||
if self.isInterruptionRequested():
|
||||
self.finished.emit(False, "操作已取消", self.game_version)
|
||||
return
|
||||
|
||||
# 如果提供了已解压文件路径,直接使用它
|
||||
if self.extracted_path and os.path.exists(self.extracted_path):
|
||||
# 发送进度信号
|
||||
self.progress.emit(20, f"正在复制 {self.game_version} 的补丁文件...")
|
||||
QCoreApplication.processEvents()
|
||||
update_progress(20, f"正在复制 {self.game_version} 的补丁文件...\n(在此过程中可能会卡顿或无响应,请不要关闭软件)")
|
||||
|
||||
# 直接复制已解压的文件到游戏目录
|
||||
target_file = os.path.join(self.game_folder, os.path.basename(self.plugin_path))
|
||||
shutil.copy(self.extracted_path, target_file)
|
||||
|
||||
# 发送进度信号
|
||||
self.progress.emit(60, f"正在完成 {self.game_version} 的补丁安装...")
|
||||
QCoreApplication.processEvents()
|
||||
update_progress(60, f"正在完成 {self.game_version} 的补丁安装...")
|
||||
|
||||
# 对于NEKOPARA After,还需要复制签名文件
|
||||
if self.game_version == "NEKOPARA After":
|
||||
@@ -55,29 +59,23 @@ class ExtractionThread(QThread):
|
||||
sig_path = os.path.join(PLUGIN, GAME_INFO[self.game_version]["sig_path"])
|
||||
shutil.copy(sig_path, self.game_folder)
|
||||
|
||||
# 发送完成进度信号
|
||||
self.progress.emit(100, f"{self.game_version} 补丁文件处理完成")
|
||||
QCoreApplication.processEvents()
|
||||
else:
|
||||
# 如果没有提供已解压文件路径,直接解压到游戏目录
|
||||
# 获取目标文件名
|
||||
update_progress(100, f"{self.game_version} 补丁文件处理完成")
|
||||
self.finished.emit(True, "", self.game_version)
|
||||
return
|
||||
|
||||
# 否则解压源压缩包到临时目录,再复制目标文件
|
||||
target_filename = os.path.basename(self.plugin_path)
|
||||
target_path = os.path.join(self.game_folder, target_filename)
|
||||
|
||||
# 发送进度信号
|
||||
self.progress.emit(10, f"正在打开 {self.game_version} 的补丁压缩包...")
|
||||
QCoreApplication.processEvents()
|
||||
update_progress(10, f"正在打开 {self.game_version} 的补丁压缩包...")
|
||||
|
||||
# 使用7z解压
|
||||
with py7zr.SevenZipFile(self._7z_path, mode="r") as archive:
|
||||
# 获取压缩包内的文件列表
|
||||
file_list = archive.getnames()
|
||||
|
||||
# 发送进度信号
|
||||
self.progress.emit(20, f"正在分析 {self.game_version} 的补丁文件...")
|
||||
QCoreApplication.processEvents()
|
||||
update_progress(20, f"正在分析 {self.game_version} 的补丁文件...")
|
||||
|
||||
# 解析压缩包内的文件结构
|
||||
# 查找压缩包内的目标文件
|
||||
target_file_in_archive = None
|
||||
for file_path in file_list:
|
||||
if target_filename in file_path:
|
||||
@@ -87,19 +85,14 @@ class ExtractionThread(QThread):
|
||||
if not target_file_in_archive:
|
||||
raise FileNotFoundError(f"在压缩包中未找到目标文件 {target_filename}")
|
||||
|
||||
# 发送进度信号
|
||||
self.progress.emit(30, f"正在解压 {self.game_version} 的补丁文件...")
|
||||
QCoreApplication.processEvents()
|
||||
update_progress(30, f"正在解压 {self.game_version} 的补丁文件...\n(在此过程中可能会卡顿或无响应,请不要关闭软件)")
|
||||
|
||||
# 创建一个临时目录用于解压单个文件
|
||||
import tempfile
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
# 解压特定文件到临时目录
|
||||
archive.extract(path=temp_dir, targets=[target_file_in_archive])
|
||||
|
||||
# 发送进度信号
|
||||
self.progress.emit(60, f"正在复制 {self.game_version} 的补丁文件...")
|
||||
QCoreApplication.processEvents()
|
||||
update_progress(60, f"正在复制 {self.game_version} 的补丁文件...")
|
||||
|
||||
# 找到解压后的文件
|
||||
extracted_file_path = os.path.join(temp_dir, target_file_in_archive)
|
||||
@@ -107,9 +100,7 @@ class ExtractionThread(QThread):
|
||||
# 复制到目标位置
|
||||
shutil.copy2(extracted_file_path, target_path)
|
||||
|
||||
# 发送进度信号
|
||||
self.progress.emit(80, f"正在完成 {self.game_version} 的补丁安装...")
|
||||
QCoreApplication.processEvents()
|
||||
update_progress(80, f"正在完成 {self.game_version} 的补丁安装...")
|
||||
|
||||
# 对于NEKOPARA After,还需要复制签名文件
|
||||
if self.game_version == "NEKOPARA After":
|
||||
@@ -135,12 +126,11 @@ class ExtractionThread(QThread):
|
||||
sig_target = os.path.join(self.game_folder, sig_filename)
|
||||
shutil.copy2(sig_path, sig_target)
|
||||
|
||||
# 发送完成进度信号
|
||||
self.progress.emit(100, f"{self.game_version} 补丁文件解压完成")
|
||||
QCoreApplication.processEvents()
|
||||
|
||||
update_progress(100, f"{self.game_version} 补丁文件解压完成")
|
||||
self.finished.emit(True, "", self.game_version)
|
||||
except (py7zr.Bad7zFile, FileNotFoundError, Exception) as e:
|
||||
try:
|
||||
self.progress.emit(100, f"处理 {self.game_version} 的补丁文件失败")
|
||||
QCoreApplication.processEvents()
|
||||
except Exception:
|
||||
pass
|
||||
self.finished.emit(False, f"\n文件操作失败,请重试\n\n【错误信息】:{e}\n", self.game_version)
|
||||
@@ -43,6 +43,8 @@ class HashThread(QThread):
|
||||
status_copy = self.installed_status.copy()
|
||||
|
||||
for game_version, install_path in self.install_paths.items():
|
||||
if self.isInterruptionRequested():
|
||||
break
|
||||
if not os.path.exists(install_path):
|
||||
status_copy[game_version] = False
|
||||
if debug_mode:
|
||||
@@ -57,8 +59,17 @@ class HashThread(QThread):
|
||||
# 当没有预期哈希值时,保持当前状态不变
|
||||
continue
|
||||
|
||||
# 分块读取,避免大文件一次性读取内存
|
||||
hash_obj = hashlib.sha256()
|
||||
with open(install_path, "rb") as f:
|
||||
file_hash = hashlib.sha256(f.read()).hexdigest()
|
||||
while True:
|
||||
if self.isInterruptionRequested():
|
||||
break
|
||||
chunk = f.read(1024 * 1024)
|
||||
if not chunk:
|
||||
break
|
||||
hash_obj.update(chunk)
|
||||
file_hash = hash_obj.hexdigest()
|
||||
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 哈希预检查 - {game_version}")
|
||||
@@ -86,6 +97,8 @@ class HashThread(QThread):
|
||||
result = {"passed": True, "game": "", "message": ""}
|
||||
|
||||
for game_version, install_path in self.install_paths.items():
|
||||
if self.isInterruptionRequested():
|
||||
break
|
||||
if not os.path.exists(install_path):
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 哈希后检查 - {game_version} 补丁文件不存在: {install_path}")
|
||||
@@ -99,8 +112,17 @@ class HashThread(QThread):
|
||||
# 当没有预期哈希值时,跳过检查
|
||||
continue
|
||||
|
||||
# 分块读取,避免大文件一次性读取内存
|
||||
hash_obj = hashlib.sha256()
|
||||
with open(install_path, "rb") as f:
|
||||
file_hash = hashlib.sha256(f.read()).hexdigest()
|
||||
while True:
|
||||
if self.isInterruptionRequested():
|
||||
break
|
||||
chunk = f.read(1024 * 1024)
|
||||
if not chunk:
|
||||
break
|
||||
hash_obj.update(chunk)
|
||||
file_hash = hash_obj.hexdigest()
|
||||
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 哈希后检查 - {game_version}")
|
||||
@@ -167,9 +189,7 @@ class OfflineHashVerifyThread(QThread):
|
||||
|
||||
if not expected_hash:
|
||||
logger.warning(f"DEBUG: 未找到 {self.game_version} 的预期哈希值")
|
||||
# 确保发送100%进度信号,以便UI更新
|
||||
self.progress.emit(100)
|
||||
QApplication.processEvents()
|
||||
self.finished.emit(False, f"未找到 {self.game_version} 的预期哈希值", "")
|
||||
return
|
||||
|
||||
@@ -183,9 +203,7 @@ class OfflineHashVerifyThread(QThread):
|
||||
if not os.path.exists(self.file_path):
|
||||
if debug_mode:
|
||||
logger.warning(f"DEBUG: 补丁文件不存在: {self.file_path}")
|
||||
# 确保发送100%进度信号,以便UI更新
|
||||
self.progress.emit(100)
|
||||
QApplication.processEvents()
|
||||
self.finished.emit(False, f"补丁文件不存在: {self.file_path}", "")
|
||||
return
|
||||
|
||||
@@ -197,9 +215,7 @@ class OfflineHashVerifyThread(QThread):
|
||||
if file_size == 0:
|
||||
if debug_mode:
|
||||
logger.warning(f"DEBUG: 补丁文件大小为0,无效文件")
|
||||
# 确保发送100%进度信号,以便UI更新
|
||||
self.progress.emit(100)
|
||||
QApplication.processEvents()
|
||||
self.finished.emit(False, "补丁文件大小为0,无效文件", "")
|
||||
return
|
||||
|
||||
@@ -233,7 +249,6 @@ class OfflineHashVerifyThread(QThread):
|
||||
if debug_mode:
|
||||
logger.warning(f"DEBUG: 未知的游戏版本: {self.game_version}")
|
||||
self.progress.emit(100)
|
||||
QApplication.processEvents()
|
||||
self.finished.emit(False, f"未知的游戏版本: {self.game_version}", "")
|
||||
return
|
||||
|
||||
@@ -284,7 +299,6 @@ class OfflineHashVerifyThread(QThread):
|
||||
if debug_mode:
|
||||
logger.warning(f"DEBUG: 未找到解压后的补丁文件")
|
||||
self.progress.emit(100)
|
||||
QApplication.processEvents()
|
||||
self.finished.emit(False, "未找到解压后的补丁文件", "")
|
||||
return
|
||||
else:
|
||||
@@ -331,9 +345,7 @@ class OfflineHashVerifyThread(QThread):
|
||||
logger.debug(f"DEBUG: 文件: {files}")
|
||||
|
||||
if not os.path.exists(patch_file):
|
||||
# 确保发送100%进度信号,以便UI更新
|
||||
self.progress.emit(100)
|
||||
QApplication.processEvents()
|
||||
self.finished.emit(False, f"未找到解压后的补丁文件", "")
|
||||
return
|
||||
|
||||
@@ -352,7 +364,12 @@ class OfflineHashVerifyThread(QThread):
|
||||
|
||||
with open(patch_file, "rb") as f:
|
||||
bytes_read = 0
|
||||
while chunk := f.read(chunk_size):
|
||||
while True:
|
||||
if self.isInterruptionRequested():
|
||||
break
|
||||
chunk = f.read(chunk_size)
|
||||
if not chunk:
|
||||
break
|
||||
hash_obj.update(chunk)
|
||||
bytes_read += len(chunk)
|
||||
# 计算进度 (70-95%)
|
||||
@@ -366,8 +383,6 @@ class OfflineHashVerifyThread(QThread):
|
||||
|
||||
# 发送进度信号 - 100%
|
||||
self.progress.emit(100)
|
||||
# 确保UI更新
|
||||
QApplication.processEvents()
|
||||
|
||||
if debug_mode:
|
||||
logger.debug(f"DEBUG: 补丁文件 {patch_file} 哈希值验证: {'成功' if result else '失败'}")
|
||||
@@ -382,26 +397,20 @@ class OfflineHashVerifyThread(QThread):
|
||||
if debug_mode:
|
||||
logger.error(f"DEBUG: 计算补丁文件哈希值失败: {e}")
|
||||
logger.error(f"DEBUG: 错误类型: {type(e).__name__}")
|
||||
# 确保发送100%进度信号,以便UI更新
|
||||
self.progress.emit(100)
|
||||
QApplication.processEvents()
|
||||
self.finished.emit(False, f"计算补丁文件哈希值失败: {str(e)}", "")
|
||||
except Exception as e:
|
||||
if debug_mode:
|
||||
logger.error(f"DEBUG: 解压补丁文件失败: {e}")
|
||||
logger.error(f"DEBUG: 错误类型: {type(e).__name__}")
|
||||
logger.error(f"DEBUG: 错误堆栈: {traceback.format_exc()}")
|
||||
# 确保发送100%进度信号,以便UI更新
|
||||
self.progress.emit(100)
|
||||
QApplication.processEvents()
|
||||
self.finished.emit(False, f"解压补丁文件失败: {str(e)}", "")
|
||||
return
|
||||
except Exception as e:
|
||||
if debug_mode:
|
||||
logger.error(f"DEBUG: 验证补丁哈希值失败: {e}")
|
||||
logger.error(f"DEBUG: 错误类型: {type(e).__name__}")
|
||||
logger.error(f"DEBUG: 错误堆栈: {traceback.format_exc()}")
|
||||
# 确保发送100%进度信号,以便UI更新
|
||||
logger.error(f"DEBUG: 错误堆栈: {traceback.format_exc()}" )
|
||||
self.progress.emit(100)
|
||||
QApplication.processEvents()
|
||||
self.finished.emit(False, f"验证补丁哈希值失败: {str(e)}", "")
|
||||