feat(core): 增强离线模式支持和版本管理

- 在主窗口中添加离线模式管理器,支持自动切换到离线模式。
- 更新下载管理器以处理离线模式下的下载逻辑,确保用户体验流畅。
- 添加版本警告机制,提示用户在版本过低时的操作选项。
- 优化配置管理器,确保在离线模式下仍可使用相关功能。
- 更新UI管理器以反映当前工作模式,提升用户界面友好性。
This commit is contained in:
hyb-oyqq
2025-08-06 15:22:44 +08:00
parent b18f4a276c
commit 7befe19f30
17 changed files with 1707 additions and 148 deletions

7
.gitignore vendored
View File

@@ -173,4 +173,9 @@ cython_debug/
nuitka-crash-report.xml nuitka-crash-report.xml
build.bat build.bat
log.txt log.txt
result.csv result.csv
after.7z
vol.1.7z
vol.2.7z
vol.3.7z
vol.4.7z

View File

@@ -85,16 +85,34 @@ class ConfigManager:
# 记录错误信息,用于按钮点击时显示 # 记录错误信息,用于按钮点击时显示
if error_message == "update_required": if error_message == "update_required":
self.last_error_message = "update_required" self.last_error_message = "update_required"
msg_box = msgbox_frame(
f"更新提示 - {self.app_name}", # 检查是否处于离线模式
"\n当前版本过低,请及时更新。\n", is_offline_mode = False
QMessageBox.StandardButton.Ok, 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()
msg_box.exec()
# 在浏览器中打开项目主页 if is_offline_mode:
webbrowser.open("https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/") # 离线模式下只显示提示,不禁用开始安装按钮
# 版本过低,应当显示"无法安装" msg_box = msgbox_frame(
return {"action": "disable_button", "then": "exit"} 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: elif "missing_keys" in error_message:
self.last_error_message = "missing_keys" self.last_error_message = "missing_keys"
@@ -128,8 +146,8 @@ class ConfigManager:
) )
msg_box.exec() msg_box.exec()
# 网络错误时应当显示"无法安装" # 网络错误时仍然允许使用按钮,用户可以尝试离线模式
return {"action": "disable_button"} return {"action": "enable_button"}
else: else:
self.cloud_config = data self.cloud_config = data
# 标记配置有效 # 标记配置有效
@@ -139,10 +157,36 @@ class ConfigManager:
if debug_mode: if debug_mode:
print("--- Cloud config fetched successfully ---") print("--- Cloud config fetched successfully ---")
print(json.dumps(data, indent=2)) # 创建一个数据副本隐藏敏感URL
safe_data = self._create_safe_config_for_logging(data)
print(json.dumps(safe_data, indent=2))
# 获取配置成功,允许安装 # 获取配置成功,允许安装
return {"action": "enable_button"} 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): def is_config_valid(self):
"""检查配置是否有效 """检查配置是否有效

View File

@@ -31,9 +31,20 @@ class DebugManager:
Returns: Returns:
bool: 是否处于调试模式 bool: 是否处于调试模式
""" """
if hasattr(self, 'ui_manager') and hasattr(self.ui_manager, 'debug_action'): try:
return self.ui_manager.debug_action.isChecked() # 首先尝试从UI管理器获取状态
return False 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): def toggle_debug_mode(self, checked):
"""切换调试模式 """切换调试模式
@@ -51,6 +62,21 @@ class DebugManager:
if checked: if checked:
self.start_logging() 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:
print("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: else:
self.stop_logging() self.stop_logging()

View File

@@ -100,7 +100,9 @@ class DownloadManager:
raise ValueError("未能获取或解析配置数据") raise ValueError("未能获取或解析配置数据")
if self.is_debug_mode(): if self.is_debug_mode():
print(f"DEBUG: Parsed JSON data: {json.dumps(config_data, indent=2)}") # 创建安全版本的配置数据用于调试输出
safe_config = self._create_safe_config_for_logging(config_data)
print(f"DEBUG: Parsed JSON data: {json.dumps(safe_config, indent=2)}")
urls = {} urls = {}
for i in range(4): for i in range(4):
@@ -125,7 +127,18 @@ class DownloadManager:
raise ValueError(f"配置文件缺少必要的键: {', '.join(missing_original_keys)}") raise ValueError(f"配置文件缺少必要的键: {', '.join(missing_original_keys)}")
if self.is_debug_mode(): if self.is_debug_mode():
print(f"DEBUG: Extracted URLs: {urls}") # 创建安全版本的URL字典用于调试输出
safe_urls = {}
for key, url in urls.items():
# 保留域名部分,隐藏路径
import re
domain_match = re.match(r'(https?://[^/]+)/.*', url)
if domain_match:
domain = domain_match.group(1)
safe_urls[key] = f"{domain}/***隐藏URL路径***"
else:
safe_urls[key] = "***隐藏URL***"
print(f"DEBUG: Extracted URLs: {safe_urls}")
print("--- Finished getting download URL successfully ---") print("--- Finished getting download URL successfully ---")
return urls return urls
@@ -158,11 +171,41 @@ class DownloadManager:
f"\n配置文件格式异常\n\n【错误信息】:{e}\n", f"\n配置文件格式异常\n\n【错误信息】:{e}\n",
) )
return {} return {}
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 download_action(self): def download_action(self):
"""开始下载流程""" """开始下载流程"""
self.main_window.download_queue_history = [] self.main_window.download_queue_history = []
# 清除游戏检测器的目录缓存,确保获取最新的目录状态
if hasattr(self.main_window, 'game_detector') and hasattr(self.main_window.game_detector, 'clear_directory_cache'):
self.main_window.game_detector.clear_directory_cache()
if self.is_debug_mode():
print("DEBUG: 已清除游戏目录缓存,确保获取最新状态")
game_dirs = self.main_window.game_detector.identify_game_directories_improved(self.selected_folder) game_dirs = self.main_window.game_detector.identify_game_directories_improved(self.selected_folder)
debug_mode = self.is_debug_mode() debug_mode = self.is_debug_mode()
@@ -182,7 +225,7 @@ class DownloadManager:
self.main_window.ui.start_install_text.setText("开始安装") self.main_window.ui.start_install_text.setText("开始安装")
return return
self.main_window.hash_msg_box = self.main_window.hash_manager.hash_pop_window(check_type="pre") self.main_window.hash_msg_box = self.main_window.hash_manager.hash_pop_window(check_type="pre", is_offline=False)
install_paths = self.get_install_paths() install_paths = self.get_install_paths()
@@ -345,22 +388,38 @@ class DownloadManager:
self.main_window.setEnabled(False) self.main_window.setEnabled(False)
config = self.get_download_url() # 检查是否处于离线模式
if not config: is_offline_mode = False
QtWidgets.QMessageBox.critical( if hasattr(self.main_window, 'offline_mode_manager'):
self.main_window, f"错误 - {APP_NAME}", "\n网络状态异常或服务器故障,请重试\n" is_offline_mode = self.main_window.offline_mode_manager.is_in_offline_mode()
)
self.main_window.setEnabled(True) if is_offline_mode:
self.main_window.ui.start_install_text.setText("开始安装") if debug_mode:
return print("DEBUG: 使用离线模式,跳过网络配置获取")
self._fill_offline_download_queue(selected_game_dirs)
else:
config = self.get_download_url()
if not config:
QtWidgets.QMessageBox.critical(
self.main_window, f"错误 - {APP_NAME}", "\n网络状态异常或服务器故障,请重试\n"
)
self.main_window.setEnabled(True)
self.main_window.ui.start_install_text.setText("开始安装")
return
self._fill_download_queue(config, selected_game_dirs) self._fill_download_queue(config, selected_game_dirs)
if not self.download_queue: if not self.download_queue:
self.main_window.after_hash_compare() self.main_window.after_hash_compare()
return return
self._show_cloudflare_option() # 如果是离线模式,直接开始下一个下载任务
if is_offline_mode:
if debug_mode:
print("DEBUG: 离线模式跳过Cloudflare优化")
self.next_download_task()
else:
self._show_cloudflare_option()
def _fill_download_queue(self, config, game_dirs): def _fill_download_queue(self, config, game_dirs):
"""填充下载队列 """填充下载队列
@@ -406,6 +465,68 @@ class DownloadManager:
self.download_queue.append((url, game_folder, game_version, _7z_path, plugin_path)) self.download_queue.append((url, game_folder, game_version, _7z_path, plugin_path))
self.main_window.download_queue_history.append(game_version) self.main_window.download_queue_history.append(game_version)
def _fill_offline_download_queue(self, game_dirs):
"""填充离线模式下的下载队列
Args:
game_dirs: 包含游戏文件夹路径的字典
"""
self.download_queue.clear()
if not hasattr(self.main_window, 'download_queue_history'):
self.main_window.download_queue_history = []
debug_mode = self.is_debug_mode()
if debug_mode:
print(f"DEBUG: 填充离线下载队列, 游戏目录: {game_dirs}")
# 检查是否有离线模式管理器
if not hasattr(self.main_window, 'offline_mode_manager'):
if debug_mode:
print("DEBUG: 离线模式管理器未初始化,无法使用离线模式")
return
for i in range(1, 5):
game_version = f"NEKOPARA Vol.{i}"
if game_version in game_dirs and not self.main_window.installed_status.get(game_version, False):
# 获取离线补丁文件路径
offline_patch_path = self.main_window.offline_mode_manager.get_offline_patch_path(game_version)
if not offline_patch_path:
if debug_mode:
print(f"DEBUG: 未找到 {game_version} 的离线补丁文件,跳过")
continue
game_folder = game_dirs[game_version]
if debug_mode:
print(f"DEBUG: 使用识别到的游戏目录 {game_version}: {game_folder}")
print(f"DEBUG: 使用离线补丁文件: {offline_patch_path}")
_7z_path = os.path.join(PLUGIN, f"vol.{i}.7z")
plugin_path = os.path.join(PLUGIN, GAME_INFO[game_version]["plugin_path"])
# 将本地文件路径作为URL添加到下载队列
self.download_queue.append((offline_patch_path, game_folder, game_version, _7z_path, plugin_path))
self.main_window.download_queue_history.append(game_version)
game_version = "NEKOPARA After"
if game_version in game_dirs and not self.main_window.installed_status.get(game_version, False):
# 获取离线补丁文件路径
offline_patch_path = self.main_window.offline_mode_manager.get_offline_patch_path(game_version)
if offline_patch_path:
game_folder = game_dirs[game_version]
if debug_mode:
print(f"DEBUG: 使用识别到的游戏目录 {game_version}: {game_folder}")
print(f"DEBUG: 使用离线补丁文件: {offline_patch_path}")
_7z_path = os.path.join(PLUGIN, "after.7z")
plugin_path = os.path.join(PLUGIN, GAME_INFO[game_version]["plugin_path"])
# 将本地文件路径作为URL添加到下载队列
self.download_queue.append((offline_patch_path, game_folder, game_version, _7z_path, plugin_path))
self.main_window.download_queue_history.append(game_version)
elif debug_mode:
print(f"DEBUG: 未找到 {game_version} 的离线补丁文件,跳过")
def _show_cloudflare_option(self): def _show_cloudflare_option(self):
"""显示Cloudflare加速选择对话框""" """显示Cloudflare加速选择对话框"""
if self.download_queue: if self.download_queue:
@@ -498,7 +619,7 @@ class DownloadManager:
"""准备下载特定游戏版本 """准备下载特定游戏版本
Args: Args:
url: 下载URL url: 下载URL或本地文件路径
game_folder: 游戏文件夹路径 game_folder: 游戏文件夹路径
game_version: 游戏版本名称 game_version: 游戏版本名称
_7z_path: 7z文件保存路径 _7z_path: 7z文件保存路径
@@ -511,6 +632,10 @@ class DownloadManager:
print(f"DEBUG: 准备下载游戏 {game_version}") print(f"DEBUG: 准备下载游戏 {game_version}")
print(f"DEBUG: 游戏文件夹: {game_folder}") print(f"DEBUG: 游戏文件夹: {game_folder}")
# 隐藏敏感URL
safe_url = "***URL protection***" # 完全隐藏URL
print(f"DEBUG: 下载URL: {safe_url}")
game_exe_exists = True game_exe_exists = True
if ( if (
@@ -525,15 +650,76 @@ class DownloadManager:
self.next_download_task() self.next_download_task()
return return
self.main_window.progress_window = self.main_window.create_progress_window() # 检查是否处于离线模式
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.optimized_ip = self.cloudflare_optimizer.get_optimized_ip() # 如果是离线模式且URL是本地文件路径
if self.optimized_ip: if is_offline_mode and os.path.isfile(url):
print(f"已为 {game_version} 获取到优选IP: {self.optimized_ip}") if debug_mode:
print(f"DEBUG: 离线模式,复制本地补丁文件 {url}{_7z_path}")
try:
# 确保目标目录存在
os.makedirs(os.path.dirname(_7z_path), exist_ok=True)
# 复制文件
import shutil
shutil.copy2(url, _7z_path)
# 验证文件哈希
hash_valid = False
if hasattr(self.main_window, 'offline_mode_manager'):
if debug_mode:
print(f"DEBUG: 开始验证补丁文件哈希: {_7z_path}")
hash_valid = self.main_window.offline_mode_manager.verify_patch_hash(game_version, _7z_path)
if debug_mode:
print(f"DEBUG: 补丁文件哈希验证结果: {'成功' if hash_valid else '失败'}")
else:
if debug_mode:
print("DEBUG: 离线模式管理器不可用,跳过哈希验证")
hash_valid = True # 如果没有离线模式管理器,假设验证成功
if hash_valid:
if debug_mode:
print(f"DEBUG: 成功复制并验证补丁文件 {_7z_path}")
# 直接进入解压阶段
self.main_window.extraction_handler.start_extraction(_7z_path, game_folder, plugin_path, game_version)
self.main_window.extraction_handler.extraction_finished.connect(self.on_extraction_finished)
else:
if debug_mode:
print(f"DEBUG: 补丁文件哈希验证失败")
# 显示错误消息
QtWidgets.QMessageBox.critical(
self.main_window,
f"错误 - {APP_NAME}",
f"\n补丁文件校验失败: {game_version}\n\n文件可能已损坏或被篡改,请重新获取补丁文件。\n"
)
# 继续下一个任务
self.next_download_task()
except Exception as e:
if debug_mode:
print(f"DEBUG: 复制补丁文件失败: {e}")
# 显示错误消息
QtWidgets.QMessageBox.critical(
self.main_window,
f"错误 - {APP_NAME}",
f"\n复制补丁文件失败: {game_version}\n错误: {e}\n"
)
# 继续下一个任务
self.next_download_task()
else: else:
print(f"未能为 {game_version} 获取优选IP将使用默认线路。") # 在线模式,正常下载
self.main_window.progress_window = self.main_window.create_progress_window()
self.optimized_ip = self.cloudflare_optimizer.get_optimized_ip()
if self.optimized_ip:
print(f"已为 {game_version} 获取到优选IP: {self.optimized_ip}")
else:
print(f"未能为 {game_version} 获取优选IP将使用默认线路。")
self.download_task_manager.start_download(url, _7z_path, game_version, game_folder, plugin_path) self.download_task_manager.start_download(url, _7z_path, game_version, game_folder, plugin_path)
def on_download_finished(self, success, error, url, game_folder, game_version, _7z_path, plugin_path): def on_download_finished(self, success, error, url, game_folder, game_version, _7z_path, plugin_path):
"""下载完成后的处理 """下载完成后的处理

View File

@@ -24,8 +24,16 @@ class ExtractionHandler:
plugin_path: 插件路径 plugin_path: 插件路径
game_version: 游戏版本名称 game_version: 游戏版本名称
""" """
# 检查是否处于离线模式
is_offline = False
if hasattr(self.main_window, 'offline_mode_manager'):
is_offline = self.main_window.offline_mode_manager.is_in_offline_mode()
# 显示解压中的消息窗口 # 显示解压中的消息窗口
self.main_window.hash_msg_box = self.main_window.hash_manager.hash_pop_window(check_type="extraction") self.main_window.hash_msg_box = self.main_window.hash_manager.hash_pop_window(
check_type="offline_extraction" if is_offline else "extraction",
is_offline=is_offline
)
# 创建并启动解压线程 # 创建并启动解压线程
self.main_window.extraction_thread = self.main_window.create_extraction_thread( self.main_window.extraction_thread = self.main_window.create_extraction_thread(

View File

@@ -13,6 +13,7 @@ class GameDetector:
""" """
self.game_info = game_info self.game_info = game_info
self.debug_manager = debug_manager self.debug_manager = debug_manager
self.directory_cache = {} # 添加目录缓存
def _is_debug_mode(self): def _is_debug_mode(self):
"""检查是否处于调试模式 """检查是否处于调试模式
@@ -135,6 +136,12 @@ class GameDetector:
""" """
debug_mode = self._is_debug_mode() debug_mode = self._is_debug_mode()
# 检查缓存中是否已有该目录的识别结果
if selected_folder in self.directory_cache:
if debug_mode:
print(f"DEBUG: 使用缓存的目录识别结果: {selected_folder}")
return self.directory_cache[selected_folder]
if debug_mode: if debug_mode:
print(f"--- 开始识别目录: {selected_folder} ---") print(f"--- 开始识别目录: {selected_folder} ---")
@@ -307,5 +314,14 @@ class GameDetector:
if debug_mode: if debug_mode:
print(f"DEBUG: 最终识别的游戏目录: {game_paths}") print(f"DEBUG: 最终识别的游戏目录: {game_paths}")
print(f"--- 目录识别结束 ---") print(f"--- 目录识别结束 ---")
# 将识别结果存入缓存
self.directory_cache[selected_folder] = game_paths
return game_paths return game_paths
def clear_directory_cache(self):
"""清除目录缓存"""
self.directory_cache = {}
if self._is_debug_mode():
print("DEBUG: 已清除目录缓存")

View File

@@ -291,28 +291,6 @@ class IPv6Manager:
""" """
print(f"Toggle IPv6 support: {enabled}") print(f"Toggle IPv6 support: {enabled}")
# 如果用户尝试启用IPv6检查系统是否支持IPv6并发出警告
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:
return False
# 用户确认启用后继续检查IPv6可用性
ipv6_available = self.check_ipv6_availability()
if not ipv6_available:
msg_box = self._create_message_box("错误", "\n未检测到可用的IPv6连接无法启用IPv6支持。\n\n请确保您的网络环境支持IPv6且已正确配置。\n")
msg_box.exec()
return False
# 保存设置到配置 # 保存设置到配置
if self.config is not None: if self.config is not None:
self.config["ipv6_enabled"] = enabled self.config["ipv6_enabled"] = enabled

View File

@@ -0,0 +1,692 @@
import os
import hashlib
import shutil
import tempfile
import py7zr
from PySide6.QtWidgets import QMessageBox
from data.config import PLUGIN, PLUGIN_HASH, GAME_INFO
from utils import msgbox_frame
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
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()
if debug_mode:
print(f"DEBUG: 扫描离线补丁文件,目录: {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
if debug_mode:
print(f"DEBUG: 找到离线补丁文件: {patch_name} 路径: {file_path}")
self.offline_patches = found_patches
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()
return False
if debug_mode:
print("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}")
if debug_mode:
print(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:
print(f"DEBUG: 已复制离线补丁文件 {source_path}{target_path}")
return True
except Exception as e:
if debug_mode:
print(f"DEBUG: 复制离线补丁文件失败: {e}")
return False
def verify_patch_hash(self, game_version, file_path):
"""验证补丁文件的哈希值
Args:
game_version: 游戏版本名称
file_path: 补丁压缩包文件路径
Returns:
bool: 哈希值是否匹配
"""
# 获取预期的哈希值
expected_hash = None
if "Vol.1" in game_version:
expected_hash = PLUGIN_HASH.get("vol1", "")
elif "Vol.2" in game_version:
expected_hash = PLUGIN_HASH.get("vol2", "")
elif "Vol.3" in game_version:
expected_hash = PLUGIN_HASH.get("vol3", "")
elif "Vol.4" in game_version:
expected_hash = PLUGIN_HASH.get("vol4", "")
elif "After" in game_version:
expected_hash = PLUGIN_HASH.get("after", "")
if not expected_hash:
print(f"DEBUG: 未找到 {game_version} 的预期哈希值")
return False
debug_mode = self._is_debug_mode()
if debug_mode:
print(f"DEBUG: 开始验证离线补丁文件: {file_path}")
print(f"DEBUG: 游戏版本: {game_version}")
print(f"DEBUG: 预期哈希值: {expected_hash}")
try:
# 检查文件是否存在
if not os.path.exists(file_path):
if debug_mode:
print(f"DEBUG: 补丁文件不存在: {file_path}")
return False
# 检查文件大小
file_size = os.path.getsize(file_path)
if debug_mode:
print(f"DEBUG: 补丁文件大小: {file_size} 字节")
if file_size == 0:
if debug_mode:
print(f"DEBUG: 补丁文件大小为0无效文件")
return False
# 创建临时目录用于解压文件
with tempfile.TemporaryDirectory() as temp_dir:
if debug_mode:
print(f"DEBUG: 创建临时目录: {temp_dir}")
# 解压补丁文件
try:
if debug_mode:
print(f"DEBUG: 开始解压文件: {file_path}")
with py7zr.SevenZipFile(file_path, mode="r") as archive:
# 获取压缩包内文件列表
file_list = archive.getnames()
if debug_mode:
print(f"DEBUG: 压缩包内文件列表: {file_list}")
# 解压所有文件
archive.extractall(path=temp_dir)
if debug_mode:
print(f"DEBUG: 解压完成")
# 列出解压后的文件
extracted_files = []
for root, dirs, files in os.walk(temp_dir):
for file in files:
extracted_files.append(os.path.join(root, file))
print(f"DEBUG: 解压后的文件列表: {extracted_files}")
except Exception as e:
if debug_mode:
print(f"DEBUG: 解压补丁文件失败: {e}")
print(f"DEBUG: 错误类型: {type(e).__name__}")
import traceback
print(f"DEBUG: 错误堆栈: {traceback.format_exc()}")
return False
# 获取补丁文件路径
patch_file = None
if "Vol.1" in game_version:
patch_file = os.path.join(temp_dir, "vol.1", "adultsonly.xp3")
elif "Vol.2" in game_version:
patch_file = os.path.join(temp_dir, "vol.2", "adultsonly.xp3")
elif "Vol.3" in game_version:
patch_file = os.path.join(temp_dir, "vol.3", "update00.int")
elif "Vol.4" in game_version:
patch_file = os.path.join(temp_dir, "vol.4", "vol4adult.xp3")
elif "After" in game_version:
patch_file = os.path.join(temp_dir, "after", "afteradult.xp3")
if not patch_file or not os.path.exists(patch_file):
if debug_mode:
print(f"DEBUG: 未找到解压后的补丁文件: {patch_file}")
# 尝试查找可能的替代文件
alternative_files = []
for root, dirs, files in os.walk(temp_dir):
for file in files:
if file.endswith('.xp3') or file.endswith('.int'):
alternative_files.append(os.path.join(root, file))
if alternative_files:
print(f"DEBUG: 找到可能的替代文件: {alternative_files}")
# 检查解压目录结构
print(f"DEBUG: 检查解压目录结构:")
for root, dirs, files in os.walk(temp_dir):
print(f"DEBUG: 目录: {root}")
print(f"DEBUG: 子目录: {dirs}")
print(f"DEBUG: 文件: {files}")
return False
if debug_mode:
print(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:
print(f"DEBUG: 补丁文件 {patch_file} 哈希值验证: {'成功' if result else '失败'}")
print(f"DEBUG: 预期哈希值: {expected_hash}")
print(f"DEBUG: 实际哈希值: {file_hash}")
return result
except Exception as e:
if debug_mode:
print(f"DEBUG: 计算补丁文件哈希值失败: {e}")
print(f"DEBUG: 错误类型: {type(e).__name__}")
return False
except Exception as e:
if debug_mode:
print(f"DEBUG: 验证补丁哈希值失败: {e}")
print(f"DEBUG: 错误类型: {type(e).__name__}")
import traceback
print(f"DEBUG: 错误堆栈: {traceback.format_exc()}")
return False
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 install_offline_patches(self, selected_games):
"""直接安装离线补丁,完全绕过下载模块
Args:
selected_games: 用户选择安装的游戏列表
Returns:
bool: 是否成功启动安装流程
"""
debug_mode = self._is_debug_mode()
if debug_mode:
print(f"DEBUG: 开始离线安装流程,选择的游戏: {selected_games}")
if not self.is_in_offline_mode():
if debug_mode:
print("DEBUG: 当前不是离线模式,无法使用离线安装")
return False
# 确保已扫描过补丁文件
if not self.offline_patches:
self.scan_for_offline_patches()
if not self.offline_patches:
if debug_mode:
print("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:
print("DEBUG: 未识别到任何游戏目录")
return False
# 显示文件检验窗口
self.main_window.hash_msg_box = self.main_window.hash_manager.hash_pop_window(check_type="pre", is_offline=True)
# 获取安装路径
install_paths = self.main_window.download_manager.get_install_paths()
# 创建并启动哈希线程进行预检查
self.main_window.hash_thread = self.main_window.create_hash_thread("pre", install_paths)
self.main_window.hash_thread.pre_finished.connect(
lambda updated_status: self.on_offline_pre_hash_finished(updated_status, game_dirs, selected_games)
)
self.main_window.hash_thread.start()
return True
def on_offline_pre_hash_finished(self, updated_status, game_dirs, selected_games):
"""离线模式下的哈希预检查完成处理
Args:
updated_status: 更新后的安装状态
game_dirs: 识别到的游戏目录
selected_games: 用户选择安装的游戏列表
"""
debug_mode = self._is_debug_mode()
# 更新安装状态
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)
# 过滤出需要安装的游戏
installable_games = []
for game_version in selected_games:
if game_version in game_dirs and not self.main_window.installed_status.get(game_version, False):
# 检查是否有对应的离线补丁
if self.get_offline_patch_path(game_version):
installable_games.append(game_version)
elif debug_mode:
print(f"DEBUG: 未找到 {game_version} 的离线补丁文件,跳过")
if not installable_games:
if debug_mode:
print("DEBUG: 没有需要安装的游戏或未找到对应的离线补丁")
msgbox_frame(
f"离线安装信息 - {self.app_name}",
"\n没有需要安装的游戏或未找到对应的离线补丁文件。\n",
QMessageBox.StandardButton.Ok
).exec()
self.main_window.ui.start_install_text.setText("开始安装")
return
# 开始安装流程
if debug_mode:
print(f"DEBUG: 开始离线安装流程,安装游戏: {installable_games}")
# 创建安装任务列表
install_tasks = []
for game_version in installable_games:
# 获取离线补丁文件路径
patch_file = self.get_offline_patch_path(game_version)
if not patch_file:
continue
# 获取游戏目录
game_folder = game_dirs.get(game_version)
if not game_folder:
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:
continue
# 添加到安装任务列表
install_tasks.append((patch_file, game_folder, game_version, _7z_path, plugin_path))
# 开始执行第一个安装任务
if install_tasks:
self.process_next_offline_install_task(install_tasks)
else:
self.main_window.ui.start_install_text.setText("开始安装")
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:
print("DEBUG: 所有离线安装任务完成,进行后检查")
self.main_window.after_hash_compare()
return
# 获取下一个任务
patch_file, game_folder, game_version, _7z_path, plugin_path = install_tasks.pop(0)
if debug_mode:
print(f"DEBUG: 处理离线安装任务: {game_version}")
print(f"DEBUG: 补丁文件: {patch_file}")
print(f"DEBUG: 游戏目录: {game_folder}")
# 确保目标目录存在
os.makedirs(os.path.dirname(_7z_path), exist_ok=True)
try:
# 复制补丁文件到缓存目录
shutil.copy2(patch_file, _7z_path)
if debug_mode:
print(f"DEBUG: 已复制补丁文件到缓存目录: {_7z_path}")
print(f"DEBUG: 开始验证补丁文件哈希值")
# 获取预期的哈希值
expected_hash = None
if "Vol.1" in game_version:
expected_hash = PLUGIN_HASH.get("vol1", "")
elif "Vol.2" in game_version:
expected_hash = PLUGIN_HASH.get("vol2", "")
elif "Vol.3" in game_version:
expected_hash = PLUGIN_HASH.get("vol3", "")
elif "Vol.4" in game_version:
expected_hash = PLUGIN_HASH.get("vol4", "")
elif "After" in game_version:
expected_hash = PLUGIN_HASH.get("after", "")
if debug_mode and expected_hash:
print(f"DEBUG: 预期哈希值: {expected_hash}")
# 显示哈希验证窗口 - 使用离线特定消息
self.main_window.hash_msg_box = self.main_window.hash_manager.hash_pop_window(check_type="offline_verify", is_offline=True)
# 验证补丁文件哈希
hash_valid = self.verify_patch_hash(game_version, _7z_path)
# 关闭哈希验证窗口
if self.main_window.hash_msg_box and self.main_window.hash_msg_box.isVisible():
self.main_window.hash_msg_box.close()
self.main_window.hash_msg_box = None
if hash_valid:
if debug_mode:
print(f"DEBUG: 补丁文件哈希验证成功,开始解压")
# 显示解压窗口 - 使用离线特定消息
self.main_window.hash_msg_box = self.main_window.hash_manager.hash_pop_window(check_type="offline_extraction", is_offline=True)
try:
# 创建解压线程
extraction_thread = self.main_window.create_extraction_thread(
_7z_path, game_folder, plugin_path, game_version
)
# 正确连接信号
extraction_thread.finished.connect(
lambda success, error, game_ver: self.on_extraction_thread_finished(
success, error, game_ver, install_tasks
)
)
# 启动解压线程
extraction_thread.start()
except Exception as e:
if debug_mode:
print(f"DEBUG: 创建或启动解压线程失败: {e}")
# 关闭解压窗口
if self.main_window.hash_msg_box and self.main_window.hash_msg_box.isVisible():
self.main_window.hash_msg_box.close()
self.main_window.hash_msg_box = 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)
else:
if debug_mode:
print(f"DEBUG: 补丁文件哈希验证失败")
# 显示错误消息
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)
except Exception as e:
if debug_mode:
print(f"DEBUG: 离线安装任务处理失败: {e}")
# 显示错误消息
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 on_extraction_thread_finished(self, success, error_message, game_version, remaining_tasks):
"""解压线程完成后的处理
Args:
success: 是否解压成功
error_message: 错误信息
game_version: 游戏版本
remaining_tasks: 剩余的安装任务列表
"""
debug_mode = self._is_debug_mode()
# 关闭解压窗口
if self.main_window.hash_msg_box and self.main_window.hash_msg_box.isVisible():
self.main_window.hash_msg_box.close()
self.main_window.hash_msg_box = None
if debug_mode:
print(f"DEBUG: 离线解压完成,状态: {'成功' if success else '失败'}")
if not success:
print(f"DEBUG: 错误信息: {error_message}")
if not success:
# 显示错误消息
msgbox_frame(
f"解压失败 - {self.app_name}",
f"\n{game_version} 的补丁解压失败。\n\n错误信息: {error_message}\n\n跳过此游戏的安装。\n",
QMessageBox.StandardButton.Ok
).exec()
# 更新安装状态
self.main_window.installed_status[game_version] = False
else:
# 更新安装状态
self.main_window.installed_status[game_version] = True
# 处理下一个任务
self.process_next_offline_install_task(remaining_tasks)
def on_offline_extraction_finished(self, remaining_tasks):
"""离线模式下的解压完成处理(旧方法,保留兼容性)
Args:
remaining_tasks: 剩余的安装任务列表
"""
debug_mode = self._is_debug_mode()
if debug_mode:
print("DEBUG: 离线解压完成,继续处理下一个任务")
# 处理下一个任务
self.process_next_offline_install_task(remaining_tasks)

View File

@@ -1,4 +1,4 @@
from PySide6.QtGui import QIcon, QAction, QFont, QCursor from PySide6.QtGui import QIcon, QAction, QFont, QCursor, QActionGroup
from PySide6.QtWidgets import QMessageBox, QMainWindow, QMenu, QPushButton from PySide6.QtWidgets import QMessageBox, QMainWindow, QMenu, QPushButton
from PySide6.QtCore import Qt, QRect from PySide6.QtCore import Qt, QRect
import webbrowser import webbrowser
@@ -37,8 +37,18 @@ class UIManager:
if os.path.exists(icon_path): if os.path.exists(icon_path):
self.main_window.setWindowIcon(QIcon(icon_path)) self.main_window.setWindowIcon(QIcon(icon_path))
# 设置窗口标题 # 获取当前离线模式状态
self.main_window.setWindowTitle(f"{APP_NAME} v{APP_VERSION}") 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._create_about_button()
@@ -265,6 +275,34 @@ class UIManager:
menu_font = self._get_menu_font() menu_font = self._get_menu_font()
font_family = menu_font.family() 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))
# 创建在线模式和离线模式选项
self.online_mode_action = QAction("在线模式", self.main_window, checkable=True)
self.online_mode_action.setFont(menu_font)
self.online_mode_action.setChecked(True) # 默认选中在线模式
self.offline_mode_action = QAction("离线模式", self.main_window, checkable=True)
self.offline_mode_action.setFont(menu_font)
self.offline_mode_action.setChecked(False)
# 将两个模式选项添加到同一个互斥组
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 = QMenu("开发者选项", self.main_window)
self.dev_menu.setFont(menu_font) # 设置与UI_install.py中相同的字体 self.dev_menu.setFont(menu_font) # 设置与UI_install.py中相同的字体
@@ -300,28 +338,11 @@ class UIManager:
self.ipv6_submenu.setFont(menu_font) self.ipv6_submenu.setFont(menu_font)
self.ipv6_submenu.setStyleSheet(menu_style) self.ipv6_submenu.setStyleSheet(menu_style)
# 检查IPv6是否可用
ipv6_available = False
if self.ipv6_manager:
ipv6_available = self.ipv6_manager.check_ipv6_availability()
if not ipv6_available:
self.ipv6_action.setText("启用IPv6支持 (不可用)")
self.ipv6_action.setEnabled(False)
self.ipv6_action.setToolTip("未检测到可用的IPv6连接")
# 检查配置中是否已启用IPv6 # 检查配置中是否已启用IPv6
config = getattr(self.main_window, 'config', {}) config = getattr(self.main_window, 'config', {})
ipv6_enabled = False ipv6_enabled = False
if isinstance(config, dict): if isinstance(config, dict):
ipv6_enabled = config.get("ipv6_enabled", False) ipv6_enabled = config.get("ipv6_enabled", False)
# 如果配置中启用了IPv6但实际不可用则强制禁用
if ipv6_enabled and not ipv6_available:
config["ipv6_enabled"] = False
ipv6_enabled = False
# 使用utils.save_config直接保存配置
from utils import save_config
save_config(config)
self.ipv6_action.setChecked(ipv6_enabled) self.ipv6_action.setChecked(ipv6_enabled)
@@ -416,6 +437,7 @@ class UIManager:
self.download_settings_menu.addAction(self.thread_settings_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.addMenu(self.download_settings_menu) # 添加下载设置子菜单
self.ui.menu.addSeparator() self.ui.menu.addSeparator()
self.ui.menu.addMenu(self.dev_menu) # 添加开发者选项子菜单 self.ui.menu.addMenu(self.dev_menu) # 添加开发者选项子菜单
@@ -438,7 +460,47 @@ class UIManager:
# 恢复复选框状态 # 恢复复选框状态
self.ipv6_action.setChecked(not enabled) self.ipv6_action.setChecked(not enabled)
return 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处理切换 # 使用IPv6Manager处理切换
success = self.ipv6_manager.toggle_ipv6_support(enabled) success = self.ipv6_manager.toggle_ipv6_support(enabled)
# 如果切换失败,恢复复选框状态 # 如果切换失败,恢复复选框状态
@@ -732,4 +794,72 @@ class UIManager:
def show_ipv6_manager_not_ready(self): def show_ipv6_manager_not_ready(self):
"""显示IPv6管理器未准备好的提示""" """显示IPv6管理器未准备好的提示"""
msg_box = self._create_message_box("错误", "\nIPv6管理器尚未初始化请稍后再试。\n") msg_box = self._create_message_box("错误", "\nIPv6管理器尚未初始化请稍后再试。\n")
msg_box.exec() 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, '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()

View File

@@ -3,7 +3,7 @@ import base64
# 配置信息 # 配置信息
app_data = { app_data = {
"APP_VERSION": "1.3.2", "APP_VERSION": "1.4.0",
"APP_NAME": "FRAISEMOE Addons Installer NEXT", "APP_NAME": "FRAISEMOE Addons Installer NEXT",
"TEMP": "TEMP", "TEMP": "TEMP",
"CACHE": "FRAISEMOE", "CACHE": "FRAISEMOE",
@@ -62,7 +62,13 @@ UA = app_data["UA_TEMPLATE"].format(APP_VERSION)
GAME_INFO = app_data["game_info"] GAME_INFO = app_data["game_info"]
BLOCK_SIZE = 67108864 BLOCK_SIZE = 67108864
HASH_SIZE = 134217728 HASH_SIZE = 134217728
PLUGIN_HASH = {game: info["hash"] for game, info in GAME_INFO.items()} PLUGIN_HASH = {
"vol1": GAME_INFO["NEKOPARA Vol.1"]["hash"],
"vol2": GAME_INFO["NEKOPARA Vol.2"]["hash"],
"vol3": GAME_INFO["NEKOPARA Vol.3"]["hash"],
"vol4": GAME_INFO["NEKOPARA Vol.4"]["hash"],
"after": GAME_INFO["NEKOPARA After"]["hash"]
}
PROCESS_INFO = {info["exe"]: game for game, info in GAME_INFO.items()} PROCESS_INFO = {info["exe"]: game for game, info in GAME_INFO.items()}
# 下载线程档位设置 # 下载线程档位设置

View File

@@ -15,7 +15,7 @@ from ui.Ui_install import Ui_MainWindows
from data.config import ( from data.config import (
APP_NAME, PLUGIN, GAME_INFO, BLOCK_SIZE, APP_NAME, PLUGIN, GAME_INFO, BLOCK_SIZE,
PLUGIN_HASH, UA, CONFIG_URL, LOG_FILE, PLUGIN_HASH, UA, CONFIG_URL, LOG_FILE,
DOWNLOAD_THREADS, DEFAULT_DOWNLOAD_THREAD_LEVEL # 添加下载线程常量 DOWNLOAD_THREADS, DEFAULT_DOWNLOAD_THREAD_LEVEL, APP_VERSION # 添加APP_VERSION导入
) )
from utils import ( from utils import (
load_config, save_config, HashManager, AdminPrivileges, msgbox_frame, load_image_from_file load_config, save_config, HashManager, AdminPrivileges, msgbox_frame, load_image_from_file
@@ -57,7 +57,7 @@ class MainWindow(QMainWindow):
self.hash_manager = HashManager(BLOCK_SIZE) self.hash_manager = HashManager(BLOCK_SIZE)
self.admin_privileges = AdminPrivileges() self.admin_privileges = AdminPrivileges()
# 初始化各种管理器 # 初始化各种管理器 - 调整初始化顺序,避免循环依赖
# 1. 首先创建必要的基础管理器 # 1. 首先创建必要的基础管理器
self.animator = MultiStageAnimations(self.ui, self) self.animator = MultiStageAnimations(self.ui, self)
self.window_manager = WindowManager(self) self.window_manager = WindowManager(self)
@@ -72,18 +72,19 @@ class MainWindow(QMainWindow):
# 4. 为debug_manager设置ui_manager引用 # 4. 为debug_manager设置ui_manager引用
self.debug_manager.set_ui_manager(self.ui_manager) self.debug_manager.set_ui_manager(self.ui_manager)
# 设置UI - 确保debug_action已初始化
self.ui_manager.setup_ui()
# 5. 初始化其他管理器 # 5. 初始化其他管理器
self.config_manager = ConfigManager(APP_NAME, CONFIG_URL, UA, self.debug_manager) self.config_manager = ConfigManager(APP_NAME, CONFIG_URL, UA, self.debug_manager)
self.game_detector = GameDetector(GAME_INFO, self.debug_manager) self.game_detector = GameDetector(GAME_INFO, self.debug_manager)
self.patch_manager = PatchManager(APP_NAME, GAME_INFO, self.debug_manager) self.patch_manager = PatchManager(APP_NAME, GAME_INFO, self.debug_manager)
# 6. 初始化下载管理器 - 放在最后,因为它可能依赖于其他管理器 # 6. 初始化离线模式管理器
from core.offline_mode_manager import OfflineModeManager
self.offline_mode_manager = OfflineModeManager(self)
# 7. 初始化下载管理器 - 放在最后,因为它可能依赖于其他管理器
self.download_manager = DownloadManager(self) self.download_manager = DownloadManager(self)
# 7. 初始化功能处理程序 # 8. 初始化功能处理程序
self.uninstall_handler = UninstallHandler(self) self.uninstall_handler = UninstallHandler(self)
self.patch_toggle_handler = PatchToggleHandler(self) self.patch_toggle_handler = PatchToggleHandler(self)
@@ -97,6 +98,9 @@ class MainWindow(QMainWindow):
self.patch_manager.initialize_status() self.patch_manager.initialize_status()
self.installed_status = self.patch_manager.get_status() # 获取初始化后的状态 self.installed_status = self.patch_manager.get_status() # 获取初始化后的状态
self.hash_msg_box = None self.hash_msg_box = None
self.last_error_message = "" # 添加错误信息记录
self.version_warning = False # 添加版本警告标志
self.install_button_enabled = True # 默认启用安装按钮
self.progress_window = None self.progress_window = None
# 设置关闭按钮事件连接 # 设置关闭按钮事件连接
@@ -140,11 +144,17 @@ class MainWindow(QMainWindow):
if self.ui_manager.debug_action.isChecked(): if self.ui_manager.debug_action.isChecked():
self.debug_manager.start_logging() self.debug_manager.start_logging()
# 在窗口显示前设置初始状态 # 设置UI包括窗口图标和菜单
self.animator.initialize() self.ui_manager.setup_ui()
# 窗口显示后延迟100ms启动动画 # 检查是否有离线补丁文件,如果有则自动切换到离线模式
QTimer.singleShot(100, self.start_animations) self.check_and_set_offline_mode()
# 获取云端配置
self.fetch_cloud_config()
# 启动动画
self.start_animations()
# 窗口事件处理 - 委托给WindowManager # 窗口事件处理 - 委托给WindowManager
def mousePressEvent(self, event): def mousePressEvent(self, event):
@@ -170,10 +180,14 @@ class MainWindow(QMainWindow):
# 但确保开始安装按钮仍然处于禁用状态 # 但确保开始安装按钮仍然处于禁用状态
self.set_start_button_enabled(False) self.set_start_button_enabled(False)
# 在动画开始前初始化
self.animator.initialize()
# 连接动画完成信号
self.animator.animation_finished.connect(self.on_animations_finished) self.animator.animation_finished.connect(self.on_animations_finished)
# 启动动画
self.animator.start_animations() self.animator.start_animations()
# 在动画开始时获取云端配置
self.fetch_cloud_config()
def on_animations_finished(self): def on_animations_finished(self):
"""动画完成后启用按钮""" """动画完成后启用按钮"""
@@ -185,8 +199,16 @@ class MainWindow(QMainWindow):
self.ui.toggle_patch_btn.setEnabled(True) # 启用禁/启用补丁按钮 self.ui.toggle_patch_btn.setEnabled(True) # 启用禁/启用补丁按钮
self.ui.exit_btn.setEnabled(True) self.ui.exit_btn.setEnabled(True)
# 只有在配置有效时才启用开始安装按钮 # 检查是否处于离线模式
if self.config_valid: is_offline_mode = False
if hasattr(self, 'offline_mode_manager'):
is_offline_mode = self.offline_mode_manager.is_in_offline_mode()
# 如果是离线模式,始终启用开始安装按钮
if is_offline_mode:
self.set_start_button_enabled(True)
# 否则,只有在配置有效时才启用开始安装按钮
elif self.config_valid:
self.set_start_button_enabled(True) self.set_start_button_enabled(True)
else: else:
self.set_start_button_enabled(False) self.set_start_button_enabled(False)
@@ -237,14 +259,14 @@ class MainWindow(QMainWindow):
elif result["action"] == "disable_button": elif result["action"] == "disable_button":
# 禁用开始安装按钮 # 禁用开始安装按钮
self.set_start_button_enabled(False) self.set_start_button_enabled(False)
# 检查是否有后续操作
if "then" in result and result["then"] == "exit":
# 强制关闭程序
self.shutdown_app(force_exit=True)
elif result["action"] == "enable_button": elif result["action"] == "enable_button":
# 启用开始安装按钮 # 启用开始安装按钮
self.set_start_button_enabled(True) self.set_start_button_enabled(True)
# 检查是否需要记录版本警告
if "version_warning" in result and result["version_warning"]:
self.version_warning = True
else:
self.version_warning = False
# 同步状态 # 同步状态
self.cloud_config = self.config_manager.get_cloud_config() self.cloud_config = self.config_manager.get_cloud_config()
@@ -318,7 +340,12 @@ class MainWindow(QMainWindow):
"""进行安装后哈希比较""" """进行安装后哈希比较"""
# 禁用窗口已在安装流程开始时完成 # 禁用窗口已在安装流程开始时完成
self.hash_msg_box = self.hash_manager.hash_pop_window(check_type="after") # 检查是否处于离线模式
is_offline = False
if hasattr(self, 'offline_mode_manager'):
is_offline = self.offline_mode_manager.is_in_offline_mode()
self.hash_msg_box = self.hash_manager.hash_pop_window(check_type="after", is_offline=is_offline)
install_paths = self.download_manager.get_install_paths() install_paths = self.download_manager.get_install_paths()
@@ -495,16 +522,28 @@ class MainWindow(QMainWindow):
"""处理安装按钮点击事件 """处理安装按钮点击事件
根据按钮当前状态决定是显示错误还是执行安装 根据按钮当前状态决定是显示错误还是执行安装
""" """
# 检查是否处于离线模式
is_offline_mode = False
if hasattr(self, 'offline_mode_manager'):
is_offline_mode = self.offline_mode_manager.is_in_offline_mode()
# 如果版本过低且在在线模式下,提示用户更新
if self.last_error_message == "update_required" and not is_offline_mode:
# 在线模式下提示用户更新软件
msg_box = msgbox_frame(
f"更新提示 - {APP_NAME}",
"\n当前版本过低,请及时更新。\n如需联网下载补丁,请更新到最新版,否则无法下载。\n\n是否切换到离线模式继续使用?\n",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
)
if msg_box.exec() == QMessageBox.StandardButton.Yes:
# 切换到离线模式
if self.ui_manager and hasattr(self.ui_manager, 'switch_work_mode'):
self.ui_manager.switch_work_mode("offline")
return
if not self.install_button_enabled: if not self.install_button_enabled:
# 按钮处于"无法安装"状态 # 按钮处于"无法安装"状态
if self.last_error_message == "update_required": if self.last_error_message == "directory_not_found":
msg_box = msgbox_frame(
f"更新提示 - {APP_NAME}",
"\n当前版本过低,请及时更新。\n",
QMessageBox.StandardButton.Ok,
)
msg_box.exec()
elif self.last_error_message == "directory_not_found":
# 目录识别失败的特定错误信息 # 目录识别失败的特定错误信息
reply = msgbox_frame( reply = msgbox_frame(
f"目录错误 - {APP_NAME}", f"目录错误 - {APP_NAME}",
@@ -517,18 +556,299 @@ class MainWindow(QMainWindow):
# 直接调用文件对话框 # 直接调用文件对话框
self.download_manager.file_dialog() self.download_manager.file_dialog()
else: else:
# 网络错误或其他错误 # 检查是否处于离线模式
reply = msgbox_frame( if is_offline_mode and self.last_error_message == "network_error":
f"错误 - {APP_NAME}", # 如果是离线模式且错误是网络相关的,提示切换到在线模式
"\n访问云端配置失败,请检查网络状况或稍后再试。\n\n是否重新尝试连接?\n", reply = msgbox_frame(
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, f"离线模式提示 - {APP_NAME}",
) "\n当前处于离线模式,但本地补丁文件不完整。\n\n是否切换到在线模式尝试下载?\n",
if reply.exec() == QMessageBox.StandardButton.Yes: QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
# 重试获取配置 )
self.fetch_cloud_config() if reply.exec() == QMessageBox.StandardButton.Yes:
# 切换到在线模式
if self.ui_manager and hasattr(self.ui_manager, 'switch_work_mode'):
self.ui_manager.switch_work_mode("online")
# 重试获取配置
self.fetch_cloud_config()
else:
# 网络错误或其他错误
reply = msgbox_frame(
f"错误 - {APP_NAME}",
"\n访问云端配置失败,请检查网络状况或稍后再试。\n\n是否重新尝试连接?\n",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
)
if reply.exec() == QMessageBox.StandardButton.Yes:
# 重试获取配置
self.fetch_cloud_config()
else: else:
# 按钮处于"开始安装"状态,正常执行安装流程 # 按钮处于"开始安装"状态,正常执行安装流程
self.download_manager.file_dialog() # 检查是否处于离线模式
if is_offline_mode:
# 如果是离线模式,使用离线安装流程
# 先选择游戏目录
self.selected_folder = QtWidgets.QFileDialog.getExistingDirectory(
self, f"选择游戏所在【上级目录】 {APP_NAME}"
)
if not self.selected_folder:
QtWidgets.QMessageBox.warning(
self, f"通知 - {APP_NAME}", "\n未选择任何目录,请重新选择\n"
)
return
# 保存选择的目录到下载管理器
self.download_manager.selected_folder = self.selected_folder
# 设置按钮状态
self.ui.start_install_text.setText("正在安装")
self.setEnabled(False)
# 清除游戏检测器的目录缓存
if hasattr(self, 'game_detector') and hasattr(self.game_detector, 'clear_directory_cache'):
self.game_detector.clear_directory_cache()
# 识别游戏目录
game_dirs = self.game_detector.identify_game_directories_improved(self.selected_folder)
if not game_dirs:
self.last_error_message = "directory_not_found"
QtWidgets.QMessageBox.warning(
self,
f"目录错误 - {APP_NAME}",
"\n未能识别到任何游戏目录。\n\n请确认您选择的是游戏的上级目录并且该目录中包含NEKOPARA系列游戏文件夹。\n"
)
self.setEnabled(True)
self.ui.start_install_text.setText("开始安装")
return
# 显示游戏选择对话框
dialog = QtWidgets.QDialog(self)
dialog.setWindowTitle("选择要安装的游戏")
dialog.resize(400, 300)
# 设置对话框样式
dialog.setStyleSheet("""
QDialog {
background-color: #2D2D30;
color: #FFFFFF;
}
QCheckBox {
color: #FFFFFF;
font-size: 14px;
padding: 5px;
margin: 5px;
}
QCheckBox:hover {
background-color: #3E3E42;
border-radius: 4px;
}
QCheckBox:checked {
color: #F47A5B;
}
QPushButton {
background-color: #3E3E42;
color: #FFFFFF;
border: none;
border-radius: 4px;
padding: 8px 16px;
font-size: 14px;
min-width: 100px;
}
QPushButton:hover {
background-color: #F47A5B;
}
QPushButton:pressed {
background-color: #E06A4B;
}
""")
layout = QtWidgets.QVBoxLayout(dialog)
layout.setContentsMargins(20, 20, 20, 20)
layout.setSpacing(10)
# 添加标题标签
title_label = QtWidgets.QLabel("选择要安装的游戏", dialog)
title_label.setStyleSheet("font-size: 16px; font-weight: bold; color: #F47A5B; margin-bottom: 10px;")
layout.addWidget(title_label)
# 添加分隔线
line = QtWidgets.QFrame(dialog)
line.setFrameShape(QtWidgets.QFrame.Shape.HLine)
line.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken)
line.setStyleSheet("background-color: #3E3E42; margin: 10px 0px;")
layout.addWidget(line)
# 添加游戏选择框
game_checkboxes = {}
scroll_area = QtWidgets.QScrollArea(dialog)
scroll_area.setWidgetResizable(True)
scroll_area.setStyleSheet("border: none; background-color: transparent;")
scroll_content = QtWidgets.QWidget(scroll_area)
scroll_layout = QtWidgets.QVBoxLayout(scroll_content)
scroll_layout.setContentsMargins(5, 5, 5, 5)
scroll_layout.setSpacing(8)
scroll_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
for game_version in game_dirs.keys():
checkbox = QtWidgets.QCheckBox(game_version, scroll_content)
checkbox.setChecked(True) # 默认选中
scroll_layout.addWidget(checkbox)
game_checkboxes[game_version] = checkbox
scroll_content.setLayout(scroll_layout)
scroll_area.setWidget(scroll_content)
layout.addWidget(scroll_area)
# 添加按钮
button_layout = QtWidgets.QHBoxLayout()
button_layout.setSpacing(15)
# 全选按钮
select_all_btn = QtWidgets.QPushButton("全选", dialog)
select_all_btn.clicked.connect(lambda: self.select_all_games(game_checkboxes, True))
# 全不选按钮
deselect_all_btn = QtWidgets.QPushButton("全不选", dialog)
deselect_all_btn.clicked.connect(lambda: self.select_all_games(game_checkboxes, False))
# 确定和取消按钮
ok_button = QtWidgets.QPushButton("确定", dialog)
ok_button.setStyleSheet(ok_button.styleSheet() + "background-color: #007ACC;")
cancel_button = QtWidgets.QPushButton("取消", dialog)
# 添加按钮到布局
button_layout.addWidget(select_all_btn)
button_layout.addWidget(deselect_all_btn)
button_layout.addStretch()
button_layout.addWidget(ok_button)
button_layout.addWidget(cancel_button)
layout.addLayout(button_layout)
# 连接信号
ok_button.clicked.connect(dialog.accept)
cancel_button.clicked.connect(dialog.reject)
# 显示对话框
if dialog.exec() == QtWidgets.QDialog.DialogCode.Accepted:
# 获取选择的游戏
selected_games = []
for game_version, checkbox in game_checkboxes.items():
if checkbox.isChecked():
selected_games.append(game_version)
if selected_games:
# 使用离线模式管理器进行安装
self.offline_mode_manager.install_offline_patches(selected_games)
else:
QtWidgets.QMessageBox.information(
self,
f"通知 - {APP_NAME}",
"\n未选择任何游戏,安装已取消。\n"
)
self.setEnabled(True)
self.ui.start_install_text.setText("开始安装")
else:
# 用户取消了选择
self.setEnabled(True)
self.ui.start_install_text.setText("开始安装")
else:
# 在线模式下,检查版本是否过低
if hasattr(self, 'version_warning') and self.version_warning:
# 版本过低,提示用户更新
msg_box = msgbox_frame(
f"版本过低 - {APP_NAME}",
"\n当前版本过低,无法使用在线下载功能。\n\n请更新到最新版本或切换到离线模式。\n",
QMessageBox.StandardButton.Ok
)
msg_box.exec()
else:
# 版本正常,使用原有的下载流程
self.download_manager.file_dialog()
def check_and_set_offline_mode(self):
"""检查是否有离线补丁文件,如果有则自动切换到离线模式"""
try:
# 检查是否有离线补丁文件
has_offline_patches = self.offline_mode_manager.has_offline_patches()
# 获取调试模式状态
debug_mode = False
if hasattr(self.debug_manager, '_is_debug_mode'):
debug_mode = self.debug_manager._is_debug_mode()
# 检查配置中是否已设置离线模式
offline_mode_enabled = False
if isinstance(self.config, dict):
offline_mode_enabled = self.config.get("offline_mode", False)
# 如果有离线补丁文件或者调试模式下强制启用离线模式
if has_offline_patches or (debug_mode and offline_mode_enabled):
# 设置离线模式
self.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)
# 更新配置
self.config["offline_mode"] = True
self.save_config(self.config)
# 在离线模式下始终启用开始安装按钮
self.set_start_button_enabled(True)
# 清除版本警告标志
self.version_warning = False
if debug_mode:
print(f"DEBUG: 已自动切换到离线模式,找到离线补丁文件: {list(self.offline_mode_manager.offline_patches.keys())}")
print(f"DEBUG: 离线模式下启用开始安装按钮")
else:
# 如果没有离线补丁文件,确保使用在线模式
self.offline_mode_manager.set_offline_mode(False)
# 更新UI中的在线模式选项
if hasattr(self.ui_manager, 'online_mode_action') and self.ui_manager.online_mode_action:
self.ui_manager.online_mode_action.setChecked(True)
self.ui_manager.offline_mode_action.setChecked(False)
# 更新配置
self.config["offline_mode"] = False
self.save_config(self.config)
# 如果当前版本过低,设置版本警告标志
if hasattr(self, 'last_error_message') and self.last_error_message == "update_required":
# 设置版本警告标志
self.version_warning = True
if debug_mode:
print("DEBUG: 未找到离线补丁文件,使用在线模式")
# 确保标题标签显示正确的模式
if hasattr(self, 'ui') and hasattr(self.ui, 'title_label'):
from data.config import APP_NAME, APP_VERSION
mode_indicator = "[离线模式]" if self.offline_mode_manager.is_in_offline_mode() else "[在线模式]"
self.ui.title_label.setText(f"{APP_NAME} v{APP_VERSION} {mode_indicator}")
except Exception as e:
# 捕获任何异常,确保程序不会崩溃
print(f"错误: 检查离线模式时发生异常: {e}")
# 默认使用在线模式
if hasattr(self, 'offline_mode_manager'):
self.offline_mode_manager.is_offline_mode = False
def select_all_games(self, game_checkboxes, checked):
"""选择或取消选择所有游戏
Args:
game_checkboxes: 游戏复选框字典
checked: 是否选中
"""
for checkbox in game_checkboxes.values():
checkbox.setChecked(checked)

View File

@@ -542,7 +542,6 @@ class Ui_MainWindows(object):
# setupUi # setupUi
def retranslateUi(self, MainWindows): def retranslateUi(self, MainWindows):
MainWindows.setWindowTitle(QCoreApplication.translate("MainWindows", f"{APP_NAME} v{APP_VERSION}", None))
self.loadbg.setText("") self.loadbg.setText("")
self.vol1bg.setText("") self.vol1bg.setText("")
self.vol2bg.setText("") self.vol2bg.setText("")

View File

@@ -112,23 +112,38 @@ class HashManager:
print(f"Error calculating hash for {file_path}: {e}") print(f"Error calculating hash for {file_path}: {e}")
return results return results
def hash_pop_window(self, check_type="default"): def hash_pop_window(self, check_type="default", is_offline=False):
"""显示文件检验窗口 """显示文件检验窗口
Args: Args:
check_type: 检查类型,可以是 'pre'(预检查), 'after'(后检查), 'extraction'(解压后检查) check_type: 检查类型,可以是 'pre'(预检查), 'after'(后检查), 'extraction'(解压后检查), 'offline_extraction'(离线解压), 'offline_verify'(离线验证)
is_offline: 是否处于离线模式
Returns: Returns:
QMessageBox: 消息框实例 QMessageBox: 消息框实例
""" """
message = "\n正在检验文件状态...\n" message = "\n正在检验文件状态...\n"
if check_type == "pre": if is_offline:
message = "\n正在检查游戏文件以确定需要安装的补丁...\n" # 离线模式的消息
elif check_type == "after": if check_type == "pre":
message = "\n正在检验本地文件完整性...\n" message = "\n正在检查游戏文件以确定需要安装的补丁...\n"
elif check_type == "extraction": elif check_type == "after":
message = "\n正在验证下载的解压文件完整性...\n" message = "\n正在检验本地文件完整性...\n"
elif check_type == "offline_verify":
message = "\n正在验证本地补丁压缩文件完整性...\n"
elif check_type == "offline_extraction":
message = "\n正在解压安装补丁文件...\n"
else:
message = "\n正在处理离线补丁文件...\n"
else:
# 在线模式的消息
if check_type == "pre":
message = "\n正在检查游戏文件以确定需要安装的补丁...\n"
elif check_type == "after":
message = "\n正在检验本地文件完整性...\n"
elif check_type == "extraction":
message = "\n正在验证下载的解压文件完整性...\n"
msg_box = msgbox_frame(f"通知 - {APP_NAME}", message) msg_box = msgbox_frame(f"通知 - {APP_NAME}", message)
msg_box.open() msg_box.open()
@@ -137,49 +152,94 @@ class HashManager:
def cfg_pre_hash_compare(self, install_paths, plugin_hash, installed_status): def cfg_pre_hash_compare(self, install_paths, plugin_hash, installed_status):
status_copy = installed_status.copy() status_copy = installed_status.copy()
debug_mode = False
# 尝试检测是否处于调试模式
try:
from data.config import CACHE
debug_file = os.path.join(os.path.dirname(CACHE), "debug_mode.txt")
debug_mode = os.path.exists(debug_file)
except:
pass
for game_version, install_path in install_paths.items(): for game_version, install_path in install_paths.items():
if not os.path.exists(install_path): if not os.path.exists(install_path):
status_copy[game_version] = False status_copy[game_version] = False
if debug_mode:
print(f"DEBUG: 哈希预检查 - {game_version} 补丁文件不存在: {install_path}")
continue continue
try: try:
expected_hash = plugin_hash.get(game_version, "")
file_hash = self.hash_calculate(install_path) file_hash = self.hash_calculate(install_path)
if file_hash == plugin_hash.get(game_version):
if debug_mode:
print(f"DEBUG: 哈希预检查 - {game_version}")
print(f"DEBUG: 文件路径: {install_path}")
print(f"DEBUG: 预期哈希值: {expected_hash}")
print(f"DEBUG: 实际哈希值: {file_hash}")
print(f"DEBUG: 哈希匹配: {file_hash == expected_hash}")
if file_hash == expected_hash:
status_copy[game_version] = True status_copy[game_version] = True
else: else:
status_copy[game_version] = False status_copy[game_version] = False
except Exception: except Exception as e:
status_copy[game_version] = False status_copy[game_version] = False
if debug_mode:
print(f"DEBUG: 哈希预检查异常 - {game_version}: {str(e)}")
return status_copy return status_copy
def cfg_after_hash_compare(self, install_paths, plugin_hash, installed_status): def cfg_after_hash_compare(self, install_paths, plugin_hash, installed_status):
debug_mode = False
# 尝试检测是否处于调试模式
try:
from data.config import CACHE
debug_file = os.path.join(os.path.dirname(CACHE), "debug_mode.txt")
debug_mode = os.path.exists(debug_file)
except:
pass
file_paths = [ file_paths = [
install_paths[game] for game in plugin_hash if installed_status.get(game) install_paths[game] for game in plugin_hash if installed_status.get(game)
] ]
hash_results = self.calculate_hashes_in_parallel(file_paths) hash_results = self.calculate_hashes_in_parallel(file_paths)
for game, hash_value in plugin_hash.items(): for game, expected_hash in plugin_hash.items():
if installed_status.get(game): if installed_status.get(game):
file_path = install_paths[game] file_path = install_paths[game]
file_hash = hash_results.get(file_path) file_hash = hash_results.get(file_path)
if debug_mode:
print(f"DEBUG: 哈希后检查 - {game}")
print(f"DEBUG: 文件路径: {file_path}")
print(f"DEBUG: 预期哈希值: {expected_hash}")
print(f"DEBUG: 实际哈希值: {file_hash if file_hash else '计算失败'}")
if file_hash is None: if file_hash is None:
installed_status[game] = False installed_status[game] = False
if debug_mode:
print(f"DEBUG: 哈希后检查失败 - 无法计算文件哈希值: {game}")
return { return {
"passed": False, "passed": False,
"game": game, "game": game,
"message": f"\n无法计算 {game} 的文件哈希值,文件可能已损坏或被占用。\n" "message": f"\n无法计算 {game} 的文件哈希值,文件可能已损坏或被占用。\n"
} }
if file_hash != hash_value: if file_hash != expected_hash:
installed_status[game] = False installed_status[game] = False
if debug_mode:
print(f"DEBUG: 哈希后检查失败 - 哈希值不匹配: {game}")
return { return {
"passed": False, "passed": False,
"game": game, "game": game,
"message": f"\n检测到 {game} 的文件哈希值不匹配。\n" "message": f"\n检测到 {game} 的文件哈希值不匹配。\n"
} }
if debug_mode:
print(f"DEBUG: 哈希后检查通过 - 所有文件哈希值匹配")
return {"passed": True} return {"passed": True}
class AdminPrivileges: class AdminPrivileges:
@@ -580,8 +640,17 @@ class HostsManager:
return False return False
def censor_url(text): def censor_url(text):
"""Censors URLs in a given text string.""" """Censors URLs in a given text string, replacing them with a protection message.
Args:
text: 要处理的文本
Returns:
str: 处理后的文本URL被完全隐藏
"""
if not isinstance(text, str): if not isinstance(text, str):
text = str(text) text = str(text)
# 匹配URL并替换为固定文本
url_pattern = re.compile(r'https?://[^\s/$.?#].[^\s]*') url_pattern = re.compile(r'https?://[^\s/$.?#].[^\s]*')
return url_pattern.sub('***URL HIDDEN***', text) return url_pattern.sub('***URL protection***', text)

View File

@@ -3,6 +3,15 @@ import logging
import os import os
from data.config import CACHE from data.config import CACHE
class URLCensorFormatter(logging.Formatter):
"""自定义的日志格式化器用于隐藏日志消息中的URL"""
def format(self, record):
# 先使用原始的format方法格式化日志
formatted_message = super().format(record)
# 然后对格式化后的消息进行URL审查
return censor_url(formatted_message)
class Logger: class Logger:
def __init__(self, filename, stream): def __init__(self, filename, stream):
self.terminal = stream self.terminal = stream
@@ -53,7 +62,7 @@ def setup_logger(name):
console_handler.setLevel(logging.INFO) console_handler.setLevel(logging.INFO)
# 创建格式器并添加到处理器 # 创建格式器并添加到处理器
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') formatter = URLCensorFormatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
file_handler.setFormatter(formatter) file_handler.setFormatter(formatter)
console_handler.setFormatter(formatter) console_handler.setFormatter(formatter)

View File

@@ -18,7 +18,8 @@ class ConfigFetchThread(QThread):
try: try:
if self.debug_mode: if self.debug_mode:
print("--- Starting to fetch cloud config ---") print("--- Starting to fetch cloud config ---")
print(f"DEBUG: Requesting URL: {self.url}") # 完全隐藏URL
print(f"DEBUG: Requesting URL: ***URL protection***")
print(f"DEBUG: Using Headers: {self.headers}") print(f"DEBUG: Using Headers: {self.headers}")
response = requests.get(self.url, headers=self.headers, timeout=10) response = requests.get(self.url, headers=self.headers, timeout=10)
@@ -26,7 +27,18 @@ class ConfigFetchThread(QThread):
if self.debug_mode: if self.debug_mode:
print(f"DEBUG: Response Status Code: {response.status_code}") print(f"DEBUG: Response Status Code: {response.status_code}")
print(f"DEBUG: Response Headers: {response.headers}") print(f"DEBUG: Response Headers: {response.headers}")
print(f"DEBUG: Response Text: {response.text}")
# 解析并隐藏响应中的敏感URL
try:
response_data = response.json()
# 创建安全版本用于日志输出
safe_response = self._create_safe_config_for_logging(response_data)
print(f"DEBUG: Response Text: {json.dumps(safe_response, indent=2)}")
except:
# 如果不是JSON直接打印文本
from utils.helpers import censor_url
censored_text = censor_url(response.text)
print(f"DEBUG: Response Text: {censored_text}")
response.raise_for_status() response.raise_for_status()
@@ -62,4 +74,28 @@ class ConfigFetchThread(QThread):
self.finished.emit(None, error_msg) self.finished.emit(None, error_msg)
finally: finally:
if self.debug_mode: if self.debug_mode:
print("--- Finished fetching cloud config ---") print("--- Finished fetching cloud config ---")
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

View File

@@ -200,7 +200,15 @@ class DownloadThread(QThread):
command.append(self.url) command.append(self.url)
print(f"即将执行的 Aria2c 命令: {' '.join(command)}") # 创建一个安全的命令副本隐藏URL
safe_command = command.copy()
if len(safe_command) > 0:
# 替换最后一个参数URL为安全版本
url = safe_command[-1]
if isinstance(url, str) and url.startswith("http"):
safe_command[-1] = "***URL protection***"
print(f"即将执行的 Aria2c 命令: {' '.join(safe_command)}")
creation_flags = subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0 creation_flags = subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0
self.process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, encoding='utf-8', errors='replace', creationflags=creation_flags) self.process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, encoding='utf-8', errors='replace', creationflags=creation_flags)
@@ -220,8 +228,11 @@ class DownloadThread(QThread):
else: else:
break break
full_output.append(line) # 处理输出行隐藏可能包含的URL
print(line.strip()) from utils.helpers import censor_url
censored_line = censor_url(line)
full_output.append(censored_line)
print(censored_line.strip())
match = progress_pattern.search(line) match = progress_pattern.search(line)
if match: if match:

View File

@@ -30,6 +30,9 @@ class IpOptimizer:
ip_txt_path = resource_path("ip.txt") ip_txt_path = resource_path("ip.txt")
# 隐藏敏感URL
safe_url = "***URL protection***"
command = [ command = [
cst_path, cst_path,
"-n", "1000", # 延迟测速线程数 "-n", "1000", # 延迟测速线程数
@@ -39,10 +42,17 @@ class IpOptimizer:
"-dd", # 禁用下载测速 "-dd", # 禁用下载测速
"-o"," " # 不写入结果文件 "-o"," " # 不写入结果文件
] ]
# 创建用于显示的安全命令副本
safe_command = command.copy()
for i, arg in enumerate(safe_command):
if arg == url:
safe_command[i] = safe_url
creation_flags = subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0 creation_flags = subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0
print("--- CloudflareSpeedTest 开始执行 ---") print("--- CloudflareSpeedTest 开始执行 ---")
print(f"执行命令: {' '.join(safe_command)}")
self.process = subprocess.Popen( self.process = subprocess.Popen(
command, command,
@@ -91,7 +101,9 @@ class IpOptimizer:
timeout_counter = 0 timeout_counter = 0
cleaned_line = line.strip() # 处理输出行隐藏可能包含的URL
from utils.helpers import censor_url
cleaned_line = censor_url(line.strip())
if cleaned_line: if cleaned_line:
print(cleaned_line) print(cleaned_line)
@@ -157,6 +169,9 @@ class IpOptimizer:
print(f"错误: ipv6.txt 未在资源路径中找到。") print(f"错误: ipv6.txt 未在资源路径中找到。")
return None return None
# 隐藏敏感URL
safe_url = "***URL protection***"
command = [ command = [
cst_path, cst_path,
"-n", "1000", # 延迟测速线程数 "-n", "1000", # 延迟测速线程数
@@ -166,10 +181,17 @@ class IpOptimizer:
"-dd", # 禁用下载测速 "-dd", # 禁用下载测速
"-o", " " # 不写入结果文件 "-o", " " # 不写入结果文件
] ]
# 创建用于显示的安全命令副本
safe_command = command.copy()
for i, arg in enumerate(safe_command):
if arg == url:
safe_command[i] = safe_url
creation_flags = subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0 creation_flags = subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0
print("--- CloudflareSpeedTest IPv6 开始执行 ---") print("--- CloudflareSpeedTest IPv6 开始执行 ---")
print(f"执行命令: {' '.join(safe_command)}")
self.process = subprocess.Popen( self.process = subprocess.Popen(
command, command,
@@ -218,7 +240,9 @@ class IpOptimizer:
timeout_counter = 0 timeout_counter = 0
cleaned_line = line.strip() # 处理输出行隐藏可能包含的URL
from utils.helpers import censor_url
cleaned_line = censor_url(line.strip())
if cleaned_line: if cleaned_line:
print(cleaned_line) print(cleaned_line)