diff --git a/.gitignore b/.gitignore index b40428f..d0a23b1 100644 --- a/.gitignore +++ b/.gitignore @@ -180,3 +180,10 @@ vol.2.7z vol.3.7z vol.4.7z log/ +__pycache__/ +*.py[cod] +venv/ +.venv/ +Thumbs.db +.DS_Store +STRUCTURE.md \ No newline at end of file diff --git a/source/IMG/After/voaf_ga01.jpg b/source/IMG/After/voaf_ga01.jpg deleted file mode 100644 index 9412413..0000000 Binary files a/source/IMG/After/voaf_ga01.jpg and /dev/null differ diff --git a/source/IMG/After/voaf_ga02.jpg b/source/IMG/After/voaf_ga02.jpg deleted file mode 100644 index a5377d5..0000000 Binary files a/source/IMG/After/voaf_ga02.jpg and /dev/null differ diff --git a/source/IMG/BG/bg1.jpg b/source/IMG/BG/bg1.jpg deleted file mode 100644 index 9dbe5b5..0000000 Binary files a/source/IMG/BG/bg1.jpg and /dev/null differ diff --git a/source/IMG/BG/bg2.jpg b/source/IMG/BG/bg2.jpg deleted file mode 100644 index e83bcf1..0000000 Binary files a/source/IMG/BG/bg2.jpg and /dev/null differ diff --git a/source/IMG/BG/bg3.jpg b/source/IMG/BG/bg3.jpg deleted file mode 100644 index 2acb228..0000000 Binary files a/source/IMG/BG/bg3.jpg and /dev/null differ diff --git a/source/IMG/BG/bg4.jpg b/source/IMG/BG/bg4.jpg deleted file mode 100644 index dbed973..0000000 Binary files a/source/IMG/BG/bg4.jpg and /dev/null differ diff --git a/source/IMG/BG/menubg.jpg b/source/IMG/BG/menubg.jpg deleted file mode 100644 index 9cde541..0000000 Binary files a/source/IMG/BG/menubg.jpg and /dev/null differ diff --git a/source/IMG/BG/title_bg1.png b/source/IMG/BG/title_bg1.png deleted file mode 100644 index 8b39823..0000000 Binary files a/source/IMG/BG/title_bg1.png and /dev/null differ diff --git a/source/IMG/BG/title_bg2.png b/source/IMG/BG/title_bg2.png deleted file mode 100644 index 8e2bd7f..0000000 Binary files a/source/IMG/BG/title_bg2.png and /dev/null differ diff --git a/source/IMG/BTN/Button.png b/source/IMG/BTN/Button.png deleted file mode 100644 index 671274c..0000000 Binary files a/source/IMG/BTN/Button.png and /dev/null differ diff --git a/source/IMG/BTN/exit.bmp b/source/IMG/BTN/exit.bmp deleted file mode 100644 index 93303e3..0000000 Binary files a/source/IMG/BTN/exit.bmp and /dev/null differ diff --git a/source/IMG/BTN/start_install.bmp b/source/IMG/BTN/start_install.bmp deleted file mode 100644 index b9b7404..0000000 Binary files a/source/IMG/BTN/start_install.bmp and /dev/null differ diff --git a/source/IMG/ICO/cloudflare_logo_icon.ico b/source/IMG/ICO/cloudflare_logo_icon.ico deleted file mode 100644 index 0c58cfc..0000000 Binary files a/source/IMG/ICO/cloudflare_logo_icon.ico and /dev/null differ diff --git a/source/IMG/ICO/icon.ico b/source/IMG/ICO/icon.ico deleted file mode 100644 index 4bf517d..0000000 Binary files a/source/IMG/ICO/icon.ico and /dev/null differ diff --git a/source/IMG/ICO/icon.png b/source/IMG/ICO/icon.png deleted file mode 100644 index d5ca97d..0000000 Binary files a/source/IMG/ICO/icon.png and /dev/null differ diff --git a/source/IMG/LOGO/gl_head_logo_jp.png b/source/IMG/LOGO/gl_head_logo_jp.png deleted file mode 100644 index fe01f25..0000000 Binary files a/source/IMG/LOGO/gl_head_logo_jp.png and /dev/null differ diff --git a/source/IMG/LOGO/vo01_logo.png b/source/IMG/LOGO/vo01_logo.png deleted file mode 100644 index e88567b..0000000 Binary files a/source/IMG/LOGO/vo01_logo.png and /dev/null differ diff --git a/source/IMG/LOGO/vo02_logo.png b/source/IMG/LOGO/vo02_logo.png deleted file mode 100644 index a8b948c..0000000 Binary files a/source/IMG/LOGO/vo02_logo.png and /dev/null differ diff --git a/source/IMG/LOGO/vo03_logo.png b/source/IMG/LOGO/vo03_logo.png deleted file mode 100644 index 6273e6b..0000000 Binary files a/source/IMG/LOGO/vo03_logo.png and /dev/null differ diff --git a/source/IMG/LOGO/vo04_logo.png b/source/IMG/LOGO/vo04_logo.png deleted file mode 100644 index f110beb..0000000 Binary files a/source/IMG/LOGO/vo04_logo.png and /dev/null differ diff --git a/source/IMG/LOGO/voaf_logo.png b/source/IMG/LOGO/voaf_logo.png deleted file mode 100644 index 3470111..0000000 Binary files a/source/IMG/LOGO/voaf_logo.png and /dev/null differ diff --git a/source/IMG/vol4/vo04_ga01.jpg b/source/IMG/vol4/vo04_ga01.jpg deleted file mode 100644 index 8bc9bf0..0000000 Binary files a/source/IMG/vol4/vo04_ga01.jpg and /dev/null differ diff --git a/source/IMG/vol4/vo04_ga05.jpg b/source/IMG/vol4/vo04_ga05.jpg deleted file mode 100644 index 4afc4de..0000000 Binary files a/source/IMG/vol4/vo04_ga05.jpg and /dev/null differ diff --git a/source/IMG/vol4/vo04_ga06.jpg b/source/IMG/vol4/vo04_ga06.jpg deleted file mode 100644 index be16715..0000000 Binary files a/source/IMG/vol4/vo04_ga06.jpg and /dev/null differ diff --git a/source/IMG/vol4/vo04_ga07.jpg b/source/IMG/vol4/vo04_ga07.jpg deleted file mode 100644 index 86cf2c3..0000000 Binary files a/source/IMG/vol4/vo04_ga07.jpg and /dev/null differ diff --git a/source/core/__init__.py b/source/core/__init__.py index bb4d9a3..c953dce 100644 --- a/source/core/__init__.py +++ b/source/core/__init__.py @@ -24,6 +24,5 @@ __all__ = [ 'PrivacyManager', 'CloudflareOptimizer', 'DownloadTaskManager', - 'ExtractionHandler', - 'PatchDetector' + 'PatchDetector', ] \ No newline at end of file diff --git a/source/core/animations.py b/source/core/animations.py deleted file mode 100644 index 9dd6b8c..0000000 --- a/source/core/animations.py +++ /dev/null @@ -1,374 +0,0 @@ -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() \ No newline at end of file diff --git a/source/core/cloudflare_optimizer.py b/source/core/cloudflare_optimizer.py deleted file mode 100644 index 93d7c88..0000000 --- a/source/core/cloudflare_optimizer.py +++ /dev/null @@ -1,438 +0,0 @@ -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 \ No newline at end of file diff --git a/source/core/config_manager.py b/source/core/config_manager.py deleted file mode 100644 index a5647ff..0000000 --- a/source/core/config_manager.py +++ /dev/null @@ -1,213 +0,0 @@ -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 \ No newline at end of file diff --git a/source/core/debug_manager.py b/source/core/debug_manager.py deleted file mode 100644 index cc2a924..0000000 --- a/source/core/debug_manager.py +++ /dev/null @@ -1,149 +0,0 @@ -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 \ No newline at end of file diff --git a/source/core/download_manager.py b/source/core/download_manager.py deleted file mode 100644 index 7d1913c..0000000 --- a/source/core/download_manager.py +++ /dev/null @@ -1,1047 +0,0 @@ -import os -import requests -import json -from collections import deque -from urllib.parse import urlparse -import re - -from PySide6 import QtWidgets, QtCore -from PySide6.QtCore import Qt, QTimer -from PySide6.QtGui import QIcon, QPixmap, QFont -from PySide6.QtWidgets import QPushButton, QDialog, QHBoxLayout - -from utils import msgbox_frame, HostsManager, resource_path -from data.config import APP_NAME, PLUGIN, GAME_INFO, UA, CONFIG_URL, DOWNLOAD_THREADS, DEFAULT_DOWNLOAD_THREAD_LEVEL -from workers import IpOptimizerThread -from core.cloudflare_optimizer import CloudflareOptimizer -from core.download_task_manager import DownloadTaskManager -from core.extraction_handler import ExtractionHandler -from utils.logger import setup_logger - -# 初始化logger -logger = setup_logger("download_manager") - -class DownloadManager: - def __init__(self, main_window): - """初始化下载管理器 - - Args: - main_window: 主窗口实例,用于访问UI和状态 - """ - self.main_window = main_window - self.main_window.APP_NAME = APP_NAME - self.selected_folder = "" - self.download_queue = deque() - self.current_download_thread = None - self.hosts_manager = HostsManager() - - self.download_thread_level = DEFAULT_DOWNLOAD_THREAD_LEVEL - - self.cloudflare_optimizer = CloudflareOptimizer(main_window, self.hosts_manager) - self.download_task_manager = DownloadTaskManager(main_window, self.download_thread_level) - self.extraction_handler = ExtractionHandler(main_window) - - def file_dialog(self): - """显示文件夹选择对话框,选择游戏安装目录""" - self.selected_folder = QtWidgets.QFileDialog.getExistingDirectory( - self.main_window, f"选择游戏所在【上级目录】 {APP_NAME}" - ) - if not self.selected_folder: - QtWidgets.QMessageBox.warning( - self.main_window, f"通知 - {APP_NAME}", "\n未选择任何目录,请重新选择\n" - ) - return - - self.main_window.ui.start_install_text.setText("正在安装") - - self.main_window.setEnabled(False) - - self.download_action() - - def get_install_paths(self): - """获取所有游戏版本的安装路径""" - game_dirs = self.main_window.game_detector.identify_game_directories_improved(self.selected_folder) - install_paths = {} - - debug_mode = self.is_debug_mode() - - for game, info in GAME_INFO.items(): - if game in game_dirs: - game_dir = game_dirs[game] - install_path = os.path.join(game_dir, os.path.basename(info["install_path"])) - install_paths[game] = install_path - if debug_mode: - logger.debug(f"DEBUG: 使用识别到的游戏目录 {game}: {game_dir}") - logger.debug(f"DEBUG: 安装路径设置为: {install_path}") - - return install_paths - - def is_debug_mode(self): - """检查是否处于调试模式""" - if hasattr(self.main_window, 'ui_manager') and self.main_window.ui_manager: - if hasattr(self.main_window.ui_manager, 'debug_action') and self.main_window.ui_manager.debug_action: - return self.main_window.ui_manager.debug_action.isChecked() - return False - - def get_download_url(self) -> dict: - """获取所有游戏版本的下载链接 - - Returns: - dict: 包含游戏版本和下载URL的字典 - """ - try: - if self.main_window.cloud_config: - if self.is_debug_mode(): - logger.info("--- Using pre-fetched cloud config ---") - config_data = self.main_window.cloud_config - else: - headers = {"User-Agent": UA} - response = requests.get(CONFIG_URL, headers=headers, timeout=10) - response.raise_for_status() - config_data = response.json() - - if not config_data: - raise ValueError("未能获取或解析配置数据") - - if self.is_debug_mode(): - # 创建安全版本的配置数据用于调试输出 - safe_config = self._create_safe_config_for_logging(config_data) - logger.debug(f"DEBUG: Parsed JSON data: {json.dumps(safe_config, indent=2)}") - - urls = {} - missing_urls = [] - - # 检查每个游戏版本的URL - for i in range(4): - key = f"vol.{i+1}.data" - if key in config_data and "url" in config_data[key]: - urls[f"vol{i+1}"] = config_data[key]["url"] - else: - missing_urls.append(f"NEKOPARA Vol.{i+1}") - if self.is_debug_mode(): - logger.warning(f"DEBUG: 未找到 NEKOPARA Vol.{i+1} 的下载URL") - - # 检查After的URL - if "after.data" in config_data and "url" in config_data["after.data"]: - urls["after"] = config_data["after.data"]["url"] - else: - missing_urls.append("NEKOPARA After") - if self.is_debug_mode(): - logger.warning(f"DEBUG: 未找到 NEKOPARA After 的下载URL") - - # 如果有缺失的URL,记录详细信息 - if missing_urls: - if self.is_debug_mode(): - logger.warning(f"DEBUG: 以下游戏版本缺少下载URL: {', '.join(missing_urls)}") - logger.warning(f"DEBUG: 当前云端配置中的键: {list(config_data.keys())}") - - # 检查每个游戏数据是否包含url键 - for i in range(4): - key = f"vol.{i+1}.data" - if key in config_data: - logger.warning(f"DEBUG: {key} 内容: {list(config_data[key].keys())}") - - if "after.data" in config_data: - logger.warning(f"DEBUG: after.data 内容: {list(config_data['after.data'].keys())}") - - if len(urls) != 5: - missing_keys_map = { - f"vol{i+1}": f"vol.{i+1}.data" for i in range(4) - } - missing_keys_map["after"] = "after.data" - - extracted_keys = set(urls.keys()) - all_keys = set(missing_keys_map.keys()) - missing_simple_keys = all_keys - extracted_keys - - missing_original_keys = [missing_keys_map[k] for k in missing_simple_keys] - - # 记录详细的缺失信息 - if self.is_debug_mode(): - logger.warning(f"DEBUG: 缺失的URL键: {missing_original_keys}") - - # 如果所有URL都缺失,可能是云端配置问题 - if len(urls) == 0: - raise ValueError(f"配置文件缺少所有下载URL键: {', '.join(missing_original_keys)}") - - # 否则只是部分缺失,可以继续使用已有的URL - logger.warning(f"配置文件缺少部分键: {', '.join(missing_original_keys)}") - - if self.is_debug_mode(): - # 创建安全版本的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***" - logger.debug(f"DEBUG: Extracted URLs: {safe_urls}") - logger.info("--- Finished getting download URL successfully ---") - return urls - - except requests.exceptions.RequestException as e: - status_code = e.response.status_code if e.response is not None else "未知" - try: - error_response = e.response.json() if e.response else {} - json_title = error_response.get("title", "无错误类型") - json_message = error_response.get("message", "无附加错误信息") - except (ValueError, AttributeError): - json_title = "配置文件异常,无法解析错误类型" - json_message = "配置文件异常,无法解析错误信息" - - if self.is_debug_mode(): - logger.error(f"ERROR: Failed to get download config due to RequestException: {e}") - - QtWidgets.QMessageBox.critical( - self.main_window, - f"错误 - {APP_NAME}", - f"\n下载配置获取失败\n\n【HTTP状态】:{status_code}\n【错误类型】:{json_title}\n【错误信息】:{json_message}\n", - ) - return {} - except ValueError as e: - if self.is_debug_mode(): - logger.error(f"ERROR: Failed to parse download config due to ValueError: {e}") - - QtWidgets.QMessageBox.critical( - self.main_window, - f"错误 - {APP_NAME}", - f"\n配置文件格式异常\n\n【错误信息】:{e}\n", - ) - 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): - """下载操作的主入口点""" - if not self.selected_folder: - QtWidgets.QMessageBox.warning( - self.main_window, f"通知 - {APP_NAME}", "\n未选择任何目录,请重新选择\n" - ) - return - - # 识别游戏目录 - game_dirs = self.main_window.game_detector.identify_game_directories_improved(self.selected_folder) - - if not game_dirs: - QtWidgets.QMessageBox.warning( - self.main_window, f"通知 - {APP_NAME}", "\n未在选择的目录中找到支持的游戏\n" - ) - self.main_window.setEnabled(True) - self.main_window.ui.start_install_text.setText("开始安装") - return - - # 关闭可能存在的哈希校验窗口 - self.main_window.close_hash_msg_box() - - # 显示文件检验窗口 - self.main_window.hash_msg_box = self.main_window.hash_manager.hash_pop_window( - check_type="pre", - auto_close=True, # 添加自动关闭参数 - close_delay=1000 # 1秒后自动关闭 - ) - - # 获取安装路径 - install_paths = self.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_pre_hash_finished_with_dirs(updated_status, game_dirs) - ) - self.main_window.hash_thread.start() - - def on_pre_hash_finished_with_dirs(self, updated_status, game_dirs): - """优化的哈希预检查完成处理,带有游戏目录信息 - - Args: - updated_status: 更新后的安装状态 - game_dirs: 识别到的游戏目录 - """ - self.main_window.installed_status = updated_status - - # 关闭哈希校验窗口 - self.main_window.close_hash_msg_box() - - debug_mode = self.is_debug_mode() - - self.main_window.setEnabled(True) - - # 使用patch_detector检测可安装的游戏 - already_installed_games, installable_games, disabled_patch_games = self.main_window.patch_detector.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是否要启用这些补丁?" - - 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: - # 用户选择启用补丁 - if debug_mode: - logger.debug(f"DEBUG: 用户选择启用被禁用的补丁") - - # 为每个禁用的游戏创建目录映射 - 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: - if debug_mode: - logger.info(f"DEBUG: 用户选择不启用被禁用的补丁,这些游戏将被添加到可安装列表") - # 用户选择不启用,将这些游戏视为可以安装补丁 - installable_games.extend(disabled_patch_games) - - # 如果有可安装的游戏,显示选择对话框 - if installable_games: - # 创建游戏选择对话框 - dialog = QtWidgets.QDialog(self.main_window) - dialog.setWindowTitle(f"选择要安装的游戏 - {APP_NAME}") - dialog.setMinimumWidth(400) - dialog.setMinimumHeight(300) - - layout = QtWidgets.QVBoxLayout() - - # 添加说明标签 - label = QtWidgets.QLabel("请选择要安装的游戏:") - layout.addWidget(label) - - # 添加已安装游戏的状态提示 - if already_installed_games: - installed_label = QtWidgets.QLabel(status_message) - installed_label.setStyleSheet("color: green;") - layout.addWidget(installed_label) - - # 创建列表控件 - list_widget = QtWidgets.QListWidget() - list_widget.setSelectionMode(QtWidgets.QAbstractItemView.SelectionMode.MultiSelection) - - # 添加可安装的游戏 - for game in installable_games: - item = QtWidgets.QListWidgetItem(game) - item.setSelected(True) # 默认全选 - list_widget.addItem(item) - - layout.addWidget(list_widget) - - # 添加按钮 - button_layout = QtWidgets.QHBoxLayout() - ok_button = QtWidgets.QPushButton("确定") - cancel_button = QtWidgets.QPushButton("取消") - button_layout.addWidget(ok_button) - button_layout.addWidget(cancel_button) - layout.addLayout(button_layout) - - dialog.setLayout(layout) - - # 连接按钮信号 - ok_button.clicked.connect(dialog.accept) - cancel_button.clicked.connect(dialog.reject) - - # 显示对话框 - if dialog.exec() == QtWidgets.QDialog.DialogCode.Accepted: - selected_games = [item.text() for item in list_widget.selectedItems()] - if debug_mode: - logger.debug(f"DEBUG: 用户选择了以下游戏进行安装: {selected_games}") - - selected_game_dirs = {game: game_dirs[game] for game in selected_games if game in game_dirs} - - self.main_window.setEnabled(False) - - # 检查是否处于离线模式 - 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() - - if is_offline_mode: - if debug_mode: - logger.info("DEBUG: 使用离线模式,跳过网络配置获取") - self._fill_offline_download_queue(selected_game_dirs) - else: - # 在线模式下,重新获取云端配置 - if hasattr(self.main_window, 'fetch_cloud_config'): - if debug_mode: - logger.info("DEBUG: 重新获取云端配置以确保URL最新") - # 重新获取云端配置并继续下载流程 - from workers.config_fetch_thread import ConfigFetchThread - self.main_window.config_manager.fetch_cloud_config( - ConfigFetchThread, - lambda data, error: self._continue_download_after_config_fetch(data, error, selected_game_dirs) - ) - else: - # 如果无法重新获取配置,使用当前配置 - config = self.get_download_url() - self._continue_download_with_config(config, selected_game_dirs) - else: - if debug_mode: - logger.debug("DEBUG: 用户取消了游戏选择") - self.main_window.ui.start_install_text.setText("开始安装") - else: - # 如果没有可安装的游戏,显示提示 - if already_installed_games: - msg = f"所有游戏已安装补丁,无需重复安装。\n\n已安装的游戏:\n{chr(10).join(already_installed_games)}" - else: - msg = "未检测到可安装的游戏。" - - QtWidgets.QMessageBox.information( - self.main_window, - f"通知 - {APP_NAME}", - msg - ) - self.main_window.ui.start_install_text.setText("开始安装") - - def _continue_download_after_config_fetch(self, data, error, selected_game_dirs): - """云端配置获取完成后继续下载流程 - - Args: - data: 获取到的配置数据 - error: 错误信息 - selected_game_dirs: 选择的游戏目录 - """ - debug_mode = self.is_debug_mode() - - if error: - if debug_mode: - logger.error(f"DEBUG: 重新获取云端配置失败: {error}") - # 使用当前配置 - config = self.get_download_url() - else: - # 使用新获取的配置 - self.main_window.cloud_config = data - config = self.get_download_url() - - self._continue_download_with_config(config, selected_game_dirs) - - def _continue_download_with_config(self, config, selected_game_dirs): - """使用配置继续下载流程 - - Args: - config: 下载配置 - selected_game_dirs: 选择的游戏目录 - """ - debug_mode = self.is_debug_mode() - - 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) - - if not self.download_queue: - # 所有下载任务都已完成,进行后检查 - if debug_mode: - logger.debug("DEBUG: 所有下载任务完成,进行后检查") - # 使用patch_detector进行安装后哈希比较 - self.main_window.patch_detector.after_hash_compare() - return - - # 检查是否处于离线模式 - 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() - - # 如果是离线模式,直接开始下一个下载任务 - if is_offline_mode: - if debug_mode: - logger.info("DEBUG: 离线模式,跳过Cloudflare优化") - self.next_download_task() - else: - self._show_cloudflare_option() - - def _fill_download_queue(self, config, game_dirs): - """填充下载队列 - - Args: - config: 包含下载URL的配置字典 - 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: - logger.debug(f"DEBUG: 填充下载队列, 游戏目录: {game_dirs}") - - 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): - url = config.get(f"vol{i}") - if not url: continue - - game_folder = game_dirs[game_version] - if debug_mode: - logger.debug(f"DEBUG: 使用识别到的游戏目录 {game_version}: {game_folder}") - - _7z_path = os.path.join(PLUGIN, f"vol.{i}.7z") - plugin_path = os.path.join(PLUGIN, GAME_INFO[game_version]["plugin_path"]) - self.download_queue.append((url, game_folder, game_version, _7z_path, plugin_path)) - 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): - url = config.get("after") - if url: - game_folder = game_dirs[game_version] - if debug_mode: - logger.debug(f"DEBUG: 使用识别到的游戏目录 {game_version}: {game_folder}") - - _7z_path = os.path.join(PLUGIN, "after.7z") - plugin_path = os.path.join(PLUGIN, GAME_INFO[game_version]["plugin_path"]) - self.download_queue.append((url, game_folder, game_version, _7z_path, plugin_path)) - 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: - logger.debug(f"DEBUG: 填充离线下载队列, 游戏目录: {game_dirs}") - - # 检查是否有离线模式管理器 - if not hasattr(self.main_window, 'offline_mode_manager'): - if debug_mode: - logger.warning("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: - logger.warning(f"DEBUG: 未找到 {game_version} 的离线补丁文件,跳过") - continue - - game_folder = game_dirs[game_version] - if debug_mode: - logger.debug(f"DEBUG: 使用识别到的游戏目录 {game_version}: {game_folder}") - logger.debug(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: - logger.debug(f"DEBUG: 使用识别到的游戏目录 {game_version}: {game_folder}") - logger.debug(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: - logger.warning(f"DEBUG: 未找到 {game_version} 的离线补丁文件,跳过") - - def _show_cloudflare_option(self): - """显示Cloudflare加速选择对话框""" - if self.download_queue: - first_url = self.download_queue[0][0] - - # 直接检查是否本次会话已执行过优选 - if self.cloudflare_optimizer.has_optimized_in_session: - logger.info("本次会话已执行过优选,跳过询问直接使用") - - self.cloudflare_optimizer.optimization_done = True - self.cloudflare_optimizer.countdown_finished = True - - self.main_window.current_url = first_url - self.next_download_task() - return - - self.main_window.setEnabled(True) - - msg_box = QtWidgets.QMessageBox(self.main_window) - msg_box.setWindowTitle(f"下载优化 - {APP_NAME}") - msg_box.setText("是否愿意通过Cloudflare加速来优化下载速度?\n\n这将临时修改系统的hosts文件,并需要管理员权限。\n如您的杀毒软件提醒有软件正在修改hosts文件,请注意放行。") - - 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(): - msg_box.setWindowIcon(QIcon(cf_pixmap)) - msg_box.setIconPixmap(cf_pixmap.scaled(64, 64, Qt.AspectRatioMode.KeepAspectRatio, - Qt.TransformationMode.SmoothTransformation)) - else: - msg_box.setIcon(QtWidgets.QMessageBox.Icon.Question) - - yes_button = msg_box.addButton("是,开启加速", QtWidgets.QMessageBox.ButtonRole.YesRole) - no_button = msg_box.addButton("否,直接下载", QtWidgets.QMessageBox.ButtonRole.NoRole) - cancel_button = msg_box.addButton("取消安装", QtWidgets.QMessageBox.ButtonRole.RejectRole) - - msg_box.exec() - - clicked_button = msg_box.clickedButton() - if clicked_button == cancel_button: - self.main_window.setEnabled(True) - self.main_window.ui.start_install_text.setText("开始安装") - self.download_queue.clear() - return - - self.main_window.setEnabled(False) - - use_optimization = clicked_button == yes_button - - if use_optimization and not self.cloudflare_optimizer.is_optimization_done(): - first_url = self.download_queue[0][0] - self.main_window.current_url = first_url - self.cloudflare_optimizer.start_ip_optimization(first_url) - QtCore.QTimer.singleShot(100, self.check_optimization_status) - else: - self.next_download_task() - - def check_optimization_status(self): - """检查IP优化状态并继续下载流程""" - if self.cloudflare_optimizer.is_optimization_done() and self.cloudflare_optimizer.is_countdown_finished(): - self.next_download_task() - else: - QtCore.QTimer.singleShot(100, self.check_optimization_status) - - def next_download_task(self): - """处理下载队列中的下一个任务""" - if not self.download_queue: - # 所有下载任务都已完成,进行后检查 - debug_mode = self.is_debug_mode() - if debug_mode: - logger.debug("DEBUG: 所有下载任务完成,进行后检查") - # 使用patch_detector进行安装后哈希比较 - self.main_window.patch_detector.after_hash_compare() - return - - if self.download_task_manager.current_download_thread and self.download_task_manager.current_download_thread.isRunning(): - return - - url, game_folder, game_version, _7z_path, plugin_path = self.download_queue.popleft() - self.download_setting(url, game_folder, game_version, _7z_path, plugin_path) - - def download_setting(self, url, game_folder, game_version, _7z_path, plugin_path): - """准备下载特定游戏版本 - - Args: - url: 下载URL或本地文件路径 - game_folder: 游戏文件夹路径 - game_version: 游戏版本名称 - _7z_path: 7z文件保存路径 - plugin_path: 插件路径 - """ - install_paths = self.get_install_paths() - - debug_mode = self.is_debug_mode() - if debug_mode: - logger.debug(f"DEBUG: 准备下载游戏 {game_version}") - logger.debug(f"DEBUG: 游戏文件夹: {game_folder}") - - # 隐藏敏感URL - safe_url = "***URL protection***" # 完全隐藏URL - logger.debug(f"DEBUG: 下载URL: {safe_url}") - - game_exe_exists = True - - if ( - not game_exe_exists - or self.main_window.installed_status[game_version] - ): - if debug_mode: - logger.debug(f"DEBUG: 跳过下载游戏 {game_version}") - logger.debug(f"DEBUG: 游戏存在: {game_exe_exists}") - logger.debug(f"DEBUG: 已安装补丁: {self.main_window.installed_status[game_version]}") - self.main_window.installed_status[game_version] = False if not game_exe_exists else True - self.next_download_task() - return - - # 检查是否处于离线模式 - 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() - - # 如果是离线模式且URL是本地文件路径 - if is_offline_mode and os.path.isfile(url): - if debug_mode: - logger.debug(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: - logger.debug(f"DEBUG: 开始验证补丁文件哈希: {_7z_path}") - hash_valid = self.main_window.offline_mode_manager.verify_patch_hash(game_version, _7z_path) - if debug_mode: - logger.debug(f"DEBUG: 补丁文件哈希验证结果: {'成功' if hash_valid else '失败'}") - else: - if debug_mode: - logger.warning("DEBUG: 离线模式管理器不可用,跳过哈希验证") - hash_valid = True # 如果没有离线模式管理器,假设验证成功 - - if hash_valid: - if debug_mode: - logger.info(f"DEBUG: 成功复制并验证补丁文件 {_7z_path}") - # 直接进入解压阶段 - self.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: - logger.warning(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: - logger.error(f"DEBUG: 复制补丁文件失败: {e}") - # 显示错误消息 - QtWidgets.QMessageBox.critical( - self.main_window, - f"错误 - {APP_NAME}", - f"\n复制补丁文件失败: {game_version}\n错误: {e}\n" - ) - # 继续下一个任务 - self.next_download_task() - else: - # 在线模式,正常下载 - self.main_window.progress_window = self.main_window.create_progress_window() - - self.optimized_ip = self.cloudflare_optimizer.get_optimized_ip() - if self.optimized_ip: - logger.info(f"已为 {game_version} 获取到优选IP: {self.optimized_ip}") - else: - logger.info(f"未能为 {game_version} 获取优选IP,将使用默认线路。") - - 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): - """下载完成后的回调函数 - - Args: - success: 是否下载成功 - error: 错误信息 - url: 下载URL - game_folder: 游戏文件夹路径 - game_version: 游戏版本 - _7z_path: 7z文件保存路径 - plugin_path: 插件保存路径 - """ - # 如果下载失败,显示错误并询问是否重试 - if not success: - logger.error(f"--- Download Failed: {game_version} ---") - logger.error(error) - logger.error("------------------------------------") - - self.main_window.setEnabled(True) - - # 分析错误类型 - error_type = "未知错误" - suggestion = "" - - if "SSL/TLS handshake failure" in error: - error_type = "SSL/TLS连接失败" - suggestion = "可能是由于网络连接不稳定或证书问题,建议:\n1. 检查网络连接\n2. 尝试使用其他网络\n3. 确保系统时间和日期正确\n4. 可能需要使用代理或VPN" - elif "Connection timed out" in error or "read timed out" in error: - error_type = "连接超时" - suggestion = "下载服务器响应时间过长,建议:\n1. 检查网络连接\n2. 稍后重试\n3. 使用优化网络选项" - elif "404" in error: - error_type = "文件不存在" - suggestion = "请求的文件不存在或已移除,请联系开发者" - elif "403" in error: - error_type = "访问被拒绝" - suggestion = "服务器拒绝请求,可能需要使用优化网络选项" - elif "No space left on device" in error or "空间不足" in error: - error_type = "存储空间不足" - suggestion = "请确保有足够的磁盘空间用于下载和解压文件" - - msg_box = QtWidgets.QMessageBox(self.main_window) - msg_box.setWindowTitle(f"下载失败 - {APP_NAME}") - error_message = f"\n文件获取失败: {game_version}\n错误类型: {error_type}" - - if suggestion: - error_message += f"\n\n可能的解决方案:\n{suggestion}" - - error_message += "\n\n是否重试?" - msg_box.setText(error_message) - - retry_button = msg_box.addButton("重试", QtWidgets.QMessageBox.ButtonRole.YesRole) - next_button = msg_box.addButton("下一个", QtWidgets.QMessageBox.ButtonRole.NoRole) - end_button = msg_box.addButton("结束", QtWidgets.QMessageBox.ButtonRole.RejectRole) - - msg_box.exec() - clicked_button = msg_box.clickedButton() - - if clicked_button == retry_button: - self.main_window.setEnabled(False) - self.download_setting(url, game_folder, game_version, _7z_path, plugin_path) - elif clicked_button == next_button: - self.main_window.setEnabled(False) - self.next_download_task() - else: - self.on_download_stopped() - return - - # 下载成功后,直接进入解压阶段 - debug_mode = self.is_debug_mode() - - # 关闭进度窗口 - if hasattr(self.main_window, 'progress_window') and self.main_window.progress_window: - if self.main_window.progress_window.isVisible(): - self.main_window.progress_window.accept() - self.main_window.progress_window = None - - if debug_mode: - logger.debug(f"DEBUG: 下载完成,直接进入解压阶段") - - # 直接进入解压阶段 - self.extraction_handler.start_extraction(_7z_path, game_folder, plugin_path, game_version) - self.main_window.extraction_handler.extraction_finished.connect(self.on_extraction_finished) - - def on_extraction_finished(self, continue_download): - """解压完成后的回调,决定是否继续下载队列 - - Args: - continue_download: 是否继续下载队列中的下一个任务 - """ - if continue_download: - self.next_download_task() - else: - self.download_queue.clear() - self.main_window.show_result() - - def on_download_stopped(self): - """当用户点击停止按钮或选择结束时调用的函数""" - self.cloudflare_optimizer.stop_optimization() - - self.download_task_manager.stop_download() - - self.download_queue.clear() - - if hasattr(self.main_window, 'progress_window') and self.main_window.progress_window: - if self.main_window.progress_window.isVisible(): - self.main_window.progress_window.reject() - self.main_window.progress_window = None - - logger.info("下载已全部停止。") - - self.main_window.setEnabled(True) - self.main_window.ui.start_install_text.setText("开始安装") - - QtWidgets.QMessageBox.information( - self.main_window, - f"已取消 - {APP_NAME}", - "\n已成功取消安装进程。\n" - ) - - def get_download_thread_count(self): - """获取当前下载线程设置对应的线程数""" - return self.download_task_manager.get_download_thread_count() - - def set_download_thread_level(self, level): - """设置下载线程级别""" - return self.download_task_manager.set_download_thread_level(level) - - def show_download_thread_settings(self): - """显示下载线程设置对话框""" - return self.download_task_manager.show_download_thread_settings() - - def direct_download_action(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 not self.selected_folder: - QtWidgets.QMessageBox.warning( - self.main_window, f"通知 - {APP_NAME}", "\n未选择任何目录,请重新选择\n" - ) - return - - # 识别游戏目录 - game_dirs = self.main_window.game_detector.identify_game_directories_improved(self.selected_folder) - - if not game_dirs: - QtWidgets.QMessageBox.warning( - self.main_window, f"通知 - {APP_NAME}", "\n未在选择的目录中找到支持的游戏\n" - ) - self.main_window.setEnabled(True) - self.main_window.ui.start_install_text.setText("开始安装") - return - - # 过滤出存在的游戏目录 - selected_game_dirs = {game: game_dirs[game] for game in games_to_download if game in game_dirs} - - if not selected_game_dirs: - QtWidgets.QMessageBox.warning( - self.main_window, f"通知 - {APP_NAME}", "\n未找到指定游戏的安装目录\n" - ) - self.main_window.setEnabled(True) - self.main_window.ui.start_install_text.setText("开始安装") - return - - self.main_window.setEnabled(False) - - # 获取下载配置 - 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_direct_download_queue(config, selected_game_dirs) - - if not self.download_queue: - # 所有下载任务都已完成,进行后检查 - if debug_mode: - logger.debug("DEBUG: 所有下载任务完成,进行后检查") - # 使用patch_detector进行安装后哈希比较 - self.main_window.patch_detector.after_hash_compare() - return - - # 显示Cloudflare优化选项 - self._show_cloudflare_option() - - def _fill_direct_download_queue(self, config, game_dirs): - """直接填充下载队列,不检查补丁是否已安装 - - Args: - config: 包含下载URL的配置字典 - 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: - logger.debug(f"DEBUG: 直接填充下载队列, 游戏目录: {game_dirs}") - - # 记录要下载的游戏,用于历史记录 - games_to_download = list(game_dirs.keys()) - self.main_window.download_queue_history = games_to_download - - for i in range(1, 5): - game_version = f"NEKOPARA Vol.{i}" - if game_version in game_dirs: - # 从配置中获取下载URL - url_key = f"vol.{i}.data" - if url_key in config and "url" in config[url_key]: - url = config[url_key]["url"] - - game_folder = game_dirs[game_version] - if debug_mode: - logger.debug(f"DEBUG: 添加下载任务 {game_version}: {game_folder}") - - _7z_path = os.path.join(PLUGIN, f"vol.{i}.7z") - plugin_path = os.path.join(PLUGIN, GAME_INFO[game_version]["plugin_path"]) - self.download_queue.append((url, game_folder, game_version, _7z_path, plugin_path)) - else: - if debug_mode: - logger.warning(f"DEBUG: 未找到 {game_version} 的下载URL") - - game_version = "NEKOPARA After" - if game_version in game_dirs: - # 从配置中获取下载URL - url_key = "after.data" - if url_key in config and "url" in config[url_key]: - url = config[url_key]["url"] - - game_folder = game_dirs[game_version] - if debug_mode: - logger.debug(f"DEBUG: 添加下载任务 {game_version}: {game_folder}") - - _7z_path = os.path.join(PLUGIN, "after.7z") - plugin_path = os.path.join(PLUGIN, GAME_INFO[game_version]["plugin_path"]) - self.download_queue.append((url, game_folder, game_version, _7z_path, plugin_path)) - else: - if debug_mode: - logger.warning(f"DEBUG: 未找到 {game_version} 的下载URL") \ No newline at end of file diff --git a/source/core/download_task_manager.py b/source/core/download_task_manager.py deleted file mode 100644 index 72ad5ee..0000000 --- a/source/core/download_task_manager.py +++ /dev/null @@ -1,221 +0,0 @@ -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 \ No newline at end of file diff --git a/source/core/extraction_handler.py b/source/core/extraction_handler.py deleted file mode 100644 index dbf01be..0000000 --- a/source/core/extraction_handler.py +++ /dev/null @@ -1,265 +0,0 @@ -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) \ No newline at end of file diff --git a/source/core/game_detector.py b/source/core/game_detector.py deleted file mode 100644 index 896e0ba..0000000 --- a/source/core/game_detector.py +++ /dev/null @@ -1,354 +0,0 @@ -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("已清除目录缓存") \ No newline at end of file diff --git a/source/core/handlers/extraction_handler.py b/source/core/handlers/extraction_handler.py index dbf01be..bc8ee13 100644 --- a/source/core/handlers/extraction_handler.py +++ b/source/core/handlers/extraction_handler.py @@ -124,7 +124,7 @@ class ExtractionHandler: game_version: 游戏版本 """ # 导入所需模块 - from data.config import GAME_INFO, PLUGIN_HASH + from config.config import GAME_INFO, PLUGIN_HASH from workers.hash_thread import HashThread # 获取安装路径 @@ -179,7 +179,7 @@ class ExtractionHandler: result: 校验结果,包含通过状态、游戏版本和消息 """ # 导入所需模块 - from data.config import GAME_INFO + from config.config import GAME_INFO # 关闭哈希检查窗口 self.main_window.close_hash_msg_box() diff --git a/source/core/ipv6_manager.py b/source/core/ipv6_manager.py deleted file mode 100644 index d6e7272..0000000 --- a/source/core/ipv6_manager.py +++ /dev/null @@ -1,323 +0,0 @@ -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 \ No newline at end of file diff --git a/source/core/managers/cloudflare_optimizer.py b/source/core/managers/cloudflare_optimizer.py index 93d7c88..c262eb8 100644 --- a/source/core/managers/cloudflare_optimizer.py +++ b/source/core/managers/cloudflare_optimizer.py @@ -112,7 +112,7 @@ class CloudflareOptimizer: ipv6_warning.setIcon(QtWidgets.QMessageBox.Icon.Warning) # 设置图标 - icon_path = resource_path(os.path.join("IMG", "ICO", "icon.png")) + icon_path = resource_path(os.path.join("assets", "images", "ICO", "icon.png")) if os.path.exists(icon_path): pixmap = QPixmap(icon_path) if not pixmap.isNull(): @@ -150,7 +150,7 @@ class CloudflareOptimizer: optimization_msg ) # 设置Cloudflare图标 - cf_icon_path = resource_path("IMG/ICO/cloudflare_logo_icon.ico") + cf_icon_path = resource_path("assets/images/ICO/cloudflare_logo_icon.ico") if os.path.exists(cf_icon_path): cf_pixmap = QPixmap(cf_icon_path) if not cf_pixmap.isNull(): diff --git a/source/core/managers/debug_manager.py b/source/core/managers/debug_manager.py index cc2a924..82e7399 100644 --- a/source/core/managers/debug_manager.py +++ b/source/core/managers/debug_manager.py @@ -1,11 +1,11 @@ import os import sys from PySide6 import QtWidgets -from data.config import LOG_FILE +from config.config import LOG_FILE from utils.logger import setup_logger from utils import Logger import datetime -from data.config import APP_NAME +from config.config import APP_NAME # 初始化logger logger = setup_logger("debug_manager") @@ -64,7 +64,7 @@ class DebugManager: # 创建或删除debug_mode.txt标记文件 try: - from data.config import CACHE + from config.config import CACHE debug_file = os.path.join(os.path.dirname(CACHE), "debug_mode.txt") if checked: diff --git a/source/core/managers/download_manager.py b/source/core/managers/download_manager.py index efb8239..6feaa50 100644 --- a/source/core/managers/download_manager.py +++ b/source/core/managers/download_manager.py @@ -11,11 +11,11 @@ from PySide6.QtGui import QIcon, QPixmap, QFont from PySide6.QtWidgets import QPushButton, QDialog, QHBoxLayout from utils import msgbox_frame, HostsManager, resource_path -from data.config import APP_NAME, PLUGIN, GAME_INFO, UA, CONFIG_URL, DOWNLOAD_THREADS, DEFAULT_DOWNLOAD_THREAD_LEVEL +from config.config import APP_NAME, PLUGIN, GAME_INFO, UA, CONFIG_URL, DOWNLOAD_THREADS, DEFAULT_DOWNLOAD_THREAD_LEVEL from workers import IpOptimizerThread -from core.cloudflare_optimizer import CloudflareOptimizer -from core.download_task_manager import DownloadTaskManager -from core.extraction_handler import ExtractionHandler +from core.managers.cloudflare_optimizer import CloudflareOptimizer +from core.managers.download_task_manager import DownloadTaskManager +from core.handlers.extraction_handler import ExtractionHandler from utils.logger import setup_logger # 初始化logger @@ -629,7 +629,7 @@ class DownloadManager: msg_box.setWindowTitle(f"下载优化 - {APP_NAME}") msg_box.setText("是否愿意通过Cloudflare加速来优化下载速度?\n\n这将临时修改系统的hosts文件,并需要管理员权限。\n如您的杀毒软件提醒有软件正在修改hosts文件,请注意放行。") - cf_icon_path = resource_path("IMG/ICO/cloudflare_logo_icon.ico") + cf_icon_path = resource_path("assets/images/ICO/cloudflare_logo_icon.ico") if os.path.exists(cf_icon_path): cf_pixmap = QPixmap(cf_icon_path) if not cf_pixmap.isNull(): diff --git a/source/core/managers/download_task_manager.py b/source/core/managers/download_task_manager.py index 72ad5ee..af5b9e0 100644 --- a/source/core/managers/download_task_manager.py +++ b/source/core/managers/download_task_manager.py @@ -3,7 +3,7 @@ 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 +from config.config import DOWNLOAD_THREADS class DownloadTaskManager: diff --git a/source/core/managers/ipv6_manager.py b/source/core/managers/ipv6_manager.py index d6e7272..2a6ab85 100644 --- a/source/core/managers/ipv6_manager.py +++ b/source/core/managers/ipv6_manager.py @@ -8,7 +8,7 @@ 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 config.config import APP_NAME from utils import msgbox_frame diff --git a/source/core/managers/offline_mode_manager.py b/source/core/managers/offline_mode_manager.py index a2ff652..6415dbd 100644 --- a/source/core/managers/offline_mode_manager.py +++ b/source/core/managers/offline_mode_manager.py @@ -8,7 +8,7 @@ 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 config.config import PLUGIN, PLUGIN_HASH, GAME_INFO from utils import msgbox_frame from utils.logger import setup_logger @@ -140,7 +140,7 @@ class OfflineModeManager: # 更新窗口标题 if hasattr(self.main_window, 'setWindowTitle'): - from data.config import APP_NAME, APP_VERSION + from config.config import APP_NAME, APP_VERSION mode_indicator = "[离线模式]" if enabled else "[在线模式]" self.main_window.setWindowTitle(f"{APP_NAME} v{APP_VERSION} {mode_indicator}") @@ -245,7 +245,7 @@ class OfflineModeManager: # 创建进度对话框 from utils.helpers import ProgressHashVerifyDialog - from data.config import PLUGIN_HASH + from config.config import PLUGIN_HASH from workers.hash_thread import OfflineHashVerifyThread # 创建并显示进度对话框 @@ -327,7 +327,7 @@ class OfflineModeManager: debug_mode = self._is_debug_mode() # 导入所需模块 - from data.config import GAME_INFO, PLUGIN + from config.config import GAME_INFO, PLUGIN # 存储结果到对话框,以便在exec()返回后获取 dialog.hash_result = result @@ -476,7 +476,7 @@ class OfflineModeManager: debug_mode = self._is_debug_mode() # 导入所需模块 - from data.config import GAME_INFO, PLUGIN_HASH + from config.config import GAME_INFO, PLUGIN_HASH from workers.hash_thread import HashThread # 获取安装路径 @@ -557,7 +557,7 @@ class OfflineModeManager: self.main_window.download_manager.selected_folder ) - from data.config import GAME_INFO + from config.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"])) diff --git a/source/core/managers/patch_detector.py b/source/core/managers/patch_detector.py index 07206ed..af52a7b 100644 --- a/source/core/managers/patch_detector.py +++ b/source/core/managers/patch_detector.py @@ -6,7 +6,7 @@ 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 +from config.config import PLUGIN_HASH, APP_NAME # 初始化logger logger = setup_logger("patch_detector") @@ -43,7 +43,7 @@ class PatchDetector: def _load_game_info(self): """从配置中加载游戏信息和补丁哈希值""" try: - from data.config import GAME_INFO, PLUGIN_HASH + from config.config import GAME_INFO, PLUGIN_HASH self.game_info = GAME_INFO self.plugin_hash = PLUGIN_HASH except ImportError: diff --git a/source/core/managers/patch_manager.py b/source/core/managers/patch_manager.py index fa3d8fc..e547a6d 100644 --- a/source/core/managers/patch_manager.py +++ b/source/core/managers/patch_manager.py @@ -3,7 +3,7 @@ import shutil import traceback from PySide6.QtWidgets import QMessageBox from utils.logger import setup_logger -from data.config import APP_NAME +from config.config import APP_NAME from utils import msgbox_frame class PatchManager: diff --git a/source/core/managers/privacy_manager.py b/source/core/managers/privacy_manager.py index 2d16115..387ed36 100644 --- a/source/core/managers/privacy_manager.py +++ b/source/core/managers/privacy_manager.py @@ -6,8 +6,8 @@ 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 config.privacy_policy import PRIVACY_POLICY_BRIEF, get_local_privacy_policy, PRIVACY_POLICY_VERSION +from config.config import CACHE, APP_NAME, APP_VERSION from utils import msgbox_frame from utils.logger import setup_logger diff --git a/source/core/managers/ui_manager.py b/source/core/managers/ui_manager.py index 1cfd24b..25f64ee 100644 --- a/source/core/managers/ui_manager.py +++ b/source/core/managers/ui_manager.py @@ -5,8 +5,8 @@ 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类 +from config.config import APP_NAME, APP_VERSION, LOG_FILE +from core.managers.ipv6_manager import IPv6Manager # 导入新的IPv6Manager类 class UIManager: def __init__(self, main_window): @@ -33,7 +33,7 @@ class UIManager: # 设置窗口图标 import os from utils import resource_path - icon_path = resource_path(os.path.join("IMG", "ICO", "icon.png")) + icon_path = resource_path(os.path.join("assets", "images", "ICO", "icon.png")) if os.path.exists(icon_path): self.main_window.setWindowIcon(QIcon(icon_path)) @@ -572,7 +572,7 @@ class UIManager: # 用户确认撤回 try: # 导入隐私管理器 - from core.privacy_manager import PrivacyManager + from core.managers.privacy_manager import PrivacyManager import sys import subprocess import os diff --git a/source/core/offline_mode_manager.py b/source/core/offline_mode_manager.py deleted file mode 100644 index 65a2ad6..0000000 --- a/source/core/offline_mode_manager.py +++ /dev/null @@ -1,995 +0,0 @@ -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.singleShot(100, 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("开始安装") - # 用户拒绝联网后,再显示本次安装结果 - try: - QTimer.singleShot(100, self.main_window.show_result) - except Exception: - pass \ No newline at end of file diff --git a/source/core/patch_detector.py b/source/core/patch_detector.py deleted file mode 100644 index 5742487..0000000 --- a/source/core/patch_detector.py +++ /dev/null @@ -1,387 +0,0 @@ -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("开始安装") - # 当处于离线模式且存在缺失补丁需要联网继续时,暂不立即显示安装结果 - 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): - 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) \ No newline at end of file diff --git a/source/core/patch_manager.py b/source/core/patch_manager.py deleted file mode 100644 index fa3d8fc..0000000 --- a/source/core/patch_manager.py +++ /dev/null @@ -1,983 +0,0 @@ -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 - ) \ No newline at end of file diff --git a/source/core/privacy_manager.py b/source/core/privacy_manager.py deleted file mode 100644 index 2d16115..0000000 --- a/source/core/privacy_manager.py +++ /dev/null @@ -1,226 +0,0 @@ -#!/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) \ No newline at end of file diff --git a/source/core/ui_manager.py b/source/core/ui_manager.py deleted file mode 100644 index 1cfd24b..0000000 --- a/source/core/ui_manager.py +++ /dev/null @@ -1,971 +0,0 @@ -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""" -

{APP_NAME} v{APP_VERSION}

-

GitHub: https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT

-

原作: Yanam1Anna

-

此应用根据 GPL-3.0 许可证 授权。

-
-

感谢:

-

- HTony03:对原项目部分源码的重构、逻辑优化和功能实现提供了支持。

-

- 钨鸮:对于云端资源存储提供了支持。

-

- XIU2/CloudflareSpeedTest:提供了 IP 优选功能的核心支持。

-

- hosxy/aria2-fast:提供了修改版aria2c,提高了下载速度和性能。

- """ - 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() \ No newline at end of file diff --git a/source/core/window_manager.py b/source/core/window_manager.py deleted file mode 100644 index ae55bc9..0000000 --- a/source/core/window_manager.py +++ /dev/null @@ -1,149 +0,0 @@ -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) \ No newline at end of file diff --git a/source/data/config.py b/source/data/config.py deleted file mode 100644 index c0ae378..0000000 --- a/source/data/config.py +++ /dev/null @@ -1,91 +0,0 @@ -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" \ No newline at end of file diff --git a/source/data/privacy_policy.py b/source/data/privacy_policy.py deleted file mode 100644 index 1d45dc4..0000000 --- a/source/data/privacy_policy.py +++ /dev/null @@ -1,100 +0,0 @@ -#!/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, "无法读取本地隐私协议文件" \ No newline at end of file diff --git a/source/fonts/SmileySans-Oblique.ttf b/source/fonts/SmileySans-Oblique.ttf deleted file mode 100644 index c297dc6..0000000 Binary files a/source/fonts/SmileySans-Oblique.ttf and /dev/null differ diff --git a/source/handlers/__init__.py b/source/handlers/__init__.py deleted file mode 100644 index f0bd89c..0000000 --- a/source/handlers/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -""" -handlers包,包含各种处理程序,用于分离主窗口的功能 -""" - -from .patch_toggle_handler import PatchToggleHandler -from .uninstall_handler import UninstallHandler - -__all__ = ['PatchToggleHandler', 'UninstallHandler'] \ No newline at end of file diff --git a/source/handlers/patch_toggle_handler.py b/source/handlers/patch_toggle_handler.py deleted file mode 100644 index 188138a..0000000 --- a/source/handlers/patch_toggle_handler.py +++ /dev/null @@ -1,438 +0,0 @@ -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) \ No newline at end of file diff --git a/source/handlers/uninstall_handler.py b/source/handlers/uninstall_handler.py deleted file mode 100644 index 7171c96..0000000 --- a/source/handlers/uninstall_handler.py +++ /dev/null @@ -1,388 +0,0 @@ -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()] \ No newline at end of file diff --git a/source/main_window.py b/source/main_window.py index 5e1b5fd..1edf7ae 100644 --- a/source/main_window.py +++ b/source/main_window.py @@ -13,7 +13,7 @@ from PySide6.QtGui import QAction # Added for menu actions from PySide6.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QProgressBar, QLabel # Added for progress window from ui.Ui_install import Ui_MainWindows -from data.config import ( +from config.config import ( APP_NAME, PLUGIN, GAME_INFO, BLOCK_SIZE, PLUGIN_HASH, UA, CONFIG_URL, LOG_FILE, DOWNLOAD_THREADS, DEFAULT_DOWNLOAD_THREAD_LEVEL, APP_VERSION # 添加APP_VERSION导入 @@ -29,10 +29,10 @@ from core import ( MultiStageAnimations, UIManager, DownloadManager, DebugManager, WindowManager, GameDetector, PatchManager, ConfigManager, PatchDetector ) -from core.ipv6_manager import IPv6Manager -from handlers import PatchToggleHandler, UninstallHandler +from core.managers.ipv6_manager import IPv6Manager +from core.handlers import PatchToggleHandler, UninstallHandler from utils.logger import setup_logger -from core.patch_detector import PatchDetector + # 初始化logger logger = setup_logger("main_window") diff --git a/source/ui/Ui_install.py b/source/ui/Ui_install.py index 4735c22..ba027ef 100644 --- a/source/ui/Ui_install.py +++ b/source/ui/Ui_install.py @@ -13,7 +13,7 @@ from PySide6.QtWidgets import (QApplication, QLabel, QMainWindow, QMenu, import os # 导入配置常量 -from data.config import APP_NAME, APP_VERSION +from config.config import APP_NAME, APP_VERSION from utils import load_image_from_file class Ui_MainWindows(object): diff --git a/source/utils/helpers.py b/source/utils/helpers.py index 2196a46..750e65d 100644 --- a/source/utils/helpers.py +++ b/source/utils/helpers.py @@ -11,7 +11,7 @@ import re from PySide6.QtGui import QIcon, QPixmap from PySide6.QtCore import Qt from PySide6.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QProgressBar -from data.config import APP_NAME, CONFIG_FILE +from config.config import APP_NAME, CONFIG_FILE from utils.logger import setup_logger import datetime import traceback @@ -144,7 +144,7 @@ def msgbox_frame(title, text, buttons=QtWidgets.QMessageBox.StandardButton.NoBut msg_box.setWindowModality(QtCore.Qt.WindowModality.WindowModal) # 直接加载图标文件 - icon_path = resource_path(os.path.join("IMG", "ICO", "icon.png")) + icon_path = resource_path(os.path.join("assets", "images", "ICO", "icon.png")) if os.path.exists(icon_path): pixmap = QPixmap(icon_path) if not pixmap.isNull(): @@ -266,7 +266,7 @@ class HashManager: # 尝试检测是否处于调试模式 try: - from data.config import CACHE + from config.config import CACHE debug_file = os.path.join(os.path.dirname(CACHE), "debug_mode.txt") debug_mode = os.path.exists(debug_file) except: @@ -316,7 +316,7 @@ class HashManager: # 尝试检测是否处于调试模式 try: - from data.config import CACHE + from config.config import CACHE debug_file = os.path.join(os.path.dirname(CACHE), "debug_mode.txt") debug_mode = os.path.exists(debug_file) except: diff --git a/source/utils/logger.py b/source/utils/logger.py index bceccd8..242992d 100644 --- a/source/utils/logger.py +++ b/source/utils/logger.py @@ -1,7 +1,7 @@ from .url_censor import censor_url import logging import os -from data.config import CACHE +from config.config import CACHE class URLCensorFormatter(logging.Formatter): """自定义的日志格式化器,用于隐藏日志消息中的URL""" @@ -72,7 +72,7 @@ def setup_logger(name): logging.Logger: 配置好的logger对象 """ # 导入LOG_FILE - from data.config import LOG_FILE + from config.config import LOG_FILE # 创建logger logger = logging.getLogger(name) diff --git a/source/workers/download.py b/source/workers/download.py index 4eada7c..51a4b99 100644 --- a/source/workers/download.py +++ b/source/workers/download.py @@ -7,7 +7,7 @@ from PySide6 import QtCore, QtWidgets from PySide6.QtCore import (Qt, Signal, QThread, QTimer) from PySide6.QtWidgets import (QLabel, QProgressBar, QVBoxLayout, QDialog, QHBoxLayout) from utils import resource_path -from data.config import APP_NAME, UA +from config.config import APP_NAME, UA import signal import ctypes import time diff --git a/source/workers/extraction_thread.py b/source/workers/extraction_thread.py index f348020..0c68a2c 100644 --- a/source/workers/extraction_thread.py +++ b/source/workers/extraction_thread.py @@ -2,7 +2,7 @@ import os import shutil import py7zr from PySide6.QtCore import QThread, Signal -from data.config import PLUGIN, GAME_INFO +from config.config import PLUGIN, GAME_INFO class ExtractionThread(QThread): finished = Signal(bool, str, str) # success, error_message, game_version