6 Commits
1.2.0 ... 1.3.0

Author SHA1 Message Date
hyb-oyqq
c941c03446 feat(core): 优化隐私协议管理并添加日志功能
- 重构 PrivacyManager 类,增加隐私协议版本检查和用户同意状态管理
- 在主窗口初始化时获取云端配置,提高效率
- 添加日志功能,记录应用启动、隐私协议加载等关键事件
- 优化错误处理和用户提示信息
2025-07-31 14:38:12 +08:00
hyb-oyqq
5ad4062346 refactor(branding): 修正软件名称
- 在隐私政策文档和代码中替换正确的软件名称
2025-07-31 11:05:44 +08:00
hyb-oyqq
cbfe0d7ff6 feat(core): 添加隐私协议管理和关于菜单功能
- 在 Main.py 中初始化隐私协议管理器,并在程序启动前显示隐私协议对话框
- 在 core/__init__.py 中添加 PrivacyManager 的引用
- 在 ui_manager.py 中实现关于菜单和隐私协议相关功能,包括:
  - 创建关于按钮和菜单
  - 添加隐私协议子菜单
  - 实现撤回隐私协议同意并重启软件的功能
  - 优化菜单样式和字体加载
2025-07-31 10:59:42 +08:00
hyb-oyqq
c837370470 feat(core): 优化安装流程并添加新功能
- 重构安装流程,提高用户体验
- 添加游戏版本选择功能
- 实现下载暂停和恢复功能
- 优化 Cloudflare 加速选项
- 改进错误处理和用户提示
2025-07-30 18:53:05 +08:00
hyb-oyqq
db9736cc4e feat(core): 优化菜单样式和字体,添加开发者选项菜单
- 从 UI_install.py 中获取字体属性,确保菜单样式一致
- 优化下载命令参数,提高下载速度和性能
- 修复部分 UI 元素的显示问题
2025-07-30 18:12:02 +08:00
hyb-oyqq
a411461f63 feat(core): 重构核心模块并添加新功能
- 重构 __init__.py,引入新模块:WindowManager, GameDetector, PatchManager, ConfigManager
- 更新 DebugManager,添加 set_ui_manager 方法
- 改进 DownloadManager,优化下载流程和错误处理
- 重构 MainWindow 类,移除冗余代码,委托部分功能给新模块
- 更新 UI 组件,简化界面逻辑
- 优化导入结构,提高代码可维护性
2025-07-30 17:18:57 +08:00
18 changed files with 2450 additions and 830 deletions

89
PRIVACY.md Normal file
View File

@@ -0,0 +1,89 @@
# FRAISEMOE Addons Installer NEXT 隐私政策
## 1. 引言
本隐私政策旨在说明 FRAISEMOE Addons Installer NEXT以下简称"本应用")在使用过程中如何收集、使用和保护您的个人信息。我们致力于保护您的隐私,并确保您了解我们如何处理您的数据。
## 2. 收集的信息
本应用在运行过程中可能会收集或处理以下信息:
### 2.1 系统信息
- 程序版本号:用于检查更新和兼容性
### 2.2 网络相关信息
- **IP 地址、ISP 及地理位置**: 应用启动时,为获取云端配置,您的 IP 地址会被服务器记录。服务器可能会根据您的 IP 地址推断您的互联网服务提供商ISP和地理位置这些信息仅用于用户数量、区域分布的统计和软件使用情况分析。当您使用 Cloudflare 加速功能时,您的 IP 地址也会被用于节点优选。
- **下载统计信息**:用于监控下载进度和速度
### 2.3 文件信息
- 游戏安装路径:用于识别已安装的游戏和安装补丁
- 文件哈希值:用于验证文件完整性
## 3. 信息使用
我们收集的信息仅用于以下目的:
### 3.1 功能实现
- 游戏目录识别:识别已安装的游戏版本
- 文件完整性验证:确保下载文件的完整性和安全性
- 下载加速:通过 Cloudflare 优化下载速度
### 3.2 服务改进
- **应用更新**:检查应用版本并推送更新。
- **使用情况分析**通过统计IP地址、ISP和地理位置等信息分析用户下载次数与软件使用情况以帮助我们改进服务。您的所有信息都仅用于软件使用统计不会用于其他特殊目的。
- **错误报告**:收集错误信息以改进应用体验。
## 4. 数据存储
### 4.1 本地存储
- 配置文件:保存在系统临时文件夹的 FRAISEMOE 子目录下
- 临时下载文件:保存在系统临时文件夹中
- 日志文件:记录程序运行日志
### 4.2 网络传输
所有网络请求均使用安全的 HTTPS 协议进行传输。
## 5. 修改系统文件
### 5.1 hosts 文件修改
- 当您选择使用 Cloudflare 加速功能时,本应用会临时修改系统 hosts 文件
- 修改前会自动创建备份(位于 %SystemRoot%\System32\drivers\etc\hosts.bak.FRAISEMOE Addons Installer NEXT
- 程序退出时会自动恢复原始 hosts 文件
## 6. 第三方服务
本应用使用以下第三方服务:
### 6.1 Cloudflare
- 本应用使用第三方开源项目 [CloudflareSpeedTest (CFST)](https://github.com/XIU2/CloudflareSpeedTest/) 为您提供 Cloudflare 加速功能。该优选服务由 CFST 项目提供,本项目及作者不负责其功能的实际维护。
- 启用此功能时CFST 将向 Cloudflare 的所有节点发送请求以测试延迟,此过程不可避免地会将您的 IP 地址提交至 Cloudflare。我们建议您遵循并查阅 Cloudflare 的相关用户协议和隐私政策。
### 6.2 云端配置服务
- 本应用启动时会从云端服务器获取配置信息如下载链接等。在此过程中服务器会获取并统计您的IP地址、地理位置及ISP等信息以用于软件使用情况分析。
- 为确保通信安全和服务的稳定性,云端服务器设置了严格的 User-Agent 校验,仅允许本应用内置的特定 User-Agent 发出请求。非本应用指定的 User-Agent 将无法访问服务。
## 7. 用户控制
您对以下功能有完全的控制权:
- 选择是否使用 Cloudflare 加速功能(需修改 hosts 文件)
- 选择安装目录和需要安装的游戏版本
- 选择是否终止可能冲突的进程
## 8. 数据安全
我们采取以下措施保护您的数据:
- 本地配置文件不包含敏感个人信息
- 网络请求使用安全的 HTTPS 协议
- hosts 文件修改会在程序退出时自动恢复
## 9. 联系我们
如果您对本隐私政策有任何疑问或建议,请通过 GitHub 项目页面联系我们。
## 10. 政策更新
本隐私政策可能会根据应用功能的变化而更新。请定期查看最新版本。
最后更新日期2025年7月31日

View File

@@ -1,9 +1,35 @@
import sys
from PySide6.QtWidgets import QApplication
from PySide6.QtWidgets import QApplication, QMessageBox
from main_window import MainWindow
from core.privacy_manager import PrivacyManager
from utils.logger import setup_logger
if __name__ == "__main__":
# 初始化日志
logger = setup_logger("main")
logger.info("应用启动")
app = QApplication(sys.argv)
# 初始化隐私协议管理器
try:
privacy_manager = PrivacyManager()
except Exception as e:
logger.error(f"初始化隐私协议管理器失败: {e}")
QMessageBox.critical(
None,
"隐私协议加载错误",
f"无法加载隐私协议管理器,程序将退出。\n\n错误信息:{e}"
)
sys.exit(1)
# 显示隐私协议对话框
if not privacy_manager.show_privacy_dialog():
logger.info("用户未同意隐私协议,程序退出")
sys.exit(0) # 如果用户不同意隐私协议,退出程序
# 用户已同意隐私协议,继续启动程序
logger.info("隐私协议已同意,启动主程序")
window = MainWindow()
window.show()
sys.exit(app.exec())

View File

@@ -2,10 +2,20 @@ from .animations import MultiStageAnimations
from .ui_manager import UIManager
from .download_manager import DownloadManager
from .debug_manager import DebugManager
from .window_manager import WindowManager
from .game_detector import GameDetector
from .patch_manager import PatchManager
from .config_manager import ConfigManager
from .privacy_manager import PrivacyManager
__all__ = [
'MultiStageAnimations',
'UIManager',
'DownloadManager',
'DebugManager'
'DebugManager',
'WindowManager',
'GameDetector',
'PatchManager',
'ConfigManager',
'PrivacyManager'
]

View File

@@ -0,0 +1,169 @@
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"
msg_box = msgbox_frame(
f"更新提示 - {self.app_name}",
"\n当前版本过低,请及时更新。\n",
QMessageBox.StandardButton.Ok,
)
msg_box.exec()
# 在浏览器中打开项目主页
webbrowser.open("https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/")
# 版本过低,应当显示"无法安装"
return {"action": "disable_button", "then": "exit"}
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": "disable_button"}
else:
self.cloud_config = data
# 标记配置有效
self.config_valid = True
# 清除错误信息
self.last_error_message = ""
if debug_mode:
print("--- Cloud config fetched successfully ---")
print(json.dumps(data, indent=2))
# 获取配置成功,允许安装
return {"action": "enable_button"}
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

View File

@@ -15,6 +15,25 @@ class DebugManager:
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: 是否处于调试模式
"""
if hasattr(self, 'ui_manager') and hasattr(self.ui_manager, 'debug_action'):
return self.ui_manager.debug_action.isChecked()
return False
def toggle_debug_mode(self, checked):
"""切换调试模式
@@ -22,8 +41,14 @@ class DebugManager:
Args:
checked: 是否启用调试模式
"""
print(f"Toggle debug mode: {checked}")
self.main_window.config["debug_mode"] = checked
self.main_window.save_config(self.main_window.config)
# 更新打开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()
else:

View File

@@ -39,12 +39,19 @@ class DownloadManager:
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.identify_game_directories_improved(self.selected_folder)
game_dirs = self.main_window.game_detector.identify_game_directories_improved(self.selected_folder)
install_paths = {}
debug_mode = self.is_debug_mode()
@@ -159,14 +166,13 @@ class DownloadManager:
def download_action(self):
"""开始下载流程"""
# 禁用开始安装按钮
self.main_window.set_start_button_enabled(False)
# 主窗口在file_dialog中已被禁用
# 清空下载历史记录
self.main_window.download_queue_history = []
# 使用改进的目录识别功能
game_dirs = self.main_window.identify_game_directories_improved(self.selected_folder)
game_dirs = self.main_window.game_detector.identify_game_directories_improved(self.selected_folder)
debug_mode = self.is_debug_mode()
if debug_mode:
@@ -183,28 +189,128 @@ class DownloadManager:
f"目录错误 - {APP_NAME}",
"\n未能识别到任何游戏目录。\n\n请确认您选择的是游戏的上级目录并且该目录中包含NEKOPARA系列游戏文件夹。\n"
)
# 恢复主窗口
self.main_window.setEnabled(True)
self.main_window.ui.start_install_text.setText("开始安装")
return
# 显示哈希检查窗口
self.main_window.hash_msg_box = self.main_window.hash_manager.hash_pop_window(check_type="pre")
# 执行预检查
# 执行预检查,先判断哪些游戏版本已安装了补丁
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(self.on_pre_hash_finished)
# 使用lambda连接传递game_dirs参数
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(self, updated_status):
"""哈希预检查完成后的处理
def on_pre_hash_finished_with_dirs(self, updated_status, game_dirs):
"""优化的哈希预检查完成处理,带有游戏目录信息
Args:
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
debug_mode = self.is_debug_mode()
# 临时启用窗口以显示选择对话框
self.main_window.setEnabled(True)
# 获取可安装的游戏版本列表(尚未安装补丁的版本)
installable_games = []
already_installed_games = []
for game_version, game_dir in game_dirs.items():
if self.main_window.installed_status.get(game_version, False):
if debug_mode:
print(f"DEBUG: {game_version} 已安装补丁,不需要再次安装")
already_installed_games.append(game_version)
else:
if debug_mode:
print(f"DEBUG: {game_version} 未安装补丁,可以安装")
installable_games.append(game_version)
# 显示状态消息
status_message = ""
if already_installed_games:
status_message += f"已安装补丁的游戏:\n{chr(10).join(already_installed_games)}\n\n"
if not installable_games:
# 如果没有可安装的游戏
QtWidgets.QMessageBox.information(
self.main_window,
f"信息 - {APP_NAME}",
f"\n所有检测到的游戏都已安装补丁。\n\n{status_message}"
)
# 恢复主窗口
self.main_window.setEnabled(True)
self.main_window.ui.start_install_text.setText("开始安装")
return
# 如果有可安装的游戏版本,让用户选择
from PySide6.QtWidgets import QInputDialog, QListWidget, QVBoxLayout, QDialog, QLabel, QPushButton, QAbstractItemView, QHBoxLayout
# 创建自定义选择对话框
dialog = QDialog(self.main_window)
dialog.setWindowTitle("选择要安装的游戏")
dialog.resize(400, 300)
layout = QVBoxLayout(dialog)
# 添加说明标签
info_label = QLabel(f"请选择要安装补丁的游戏版本:\n{status_message}", dialog)
layout.addWidget(info_label)
# 添加列表控件
list_widget = QListWidget(dialog)
list_widget.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) # 允许多选
for game in installable_games:
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() == []:
# 用户取消或未选择任何游戏
self.main_window.setEnabled(True)
self.main_window.ui.start_install_text.setText("开始安装")
return
# 获取用户选择的游戏
selected_games = [item.text() for item in list_widget.selectedItems()]
if debug_mode:
print(f"DEBUG: 用户选择了以下游戏进行安装: {selected_games}")
# 过滤game_dirs只保留选中的游戏
selected_game_dirs = {game: game_dirs[game] for game in selected_games if game in game_dirs}
# 重新禁用窗口
self.main_window.setEnabled(False)
# 获取下载配置
config = self.get_download_url()
@@ -212,20 +318,81 @@ class DownloadManager:
QtWidgets.QMessageBox.critical(
self.main_window, f"错误 - {APP_NAME}", "\n网络状态异常或服务器故障,请重试\n"
)
# 重新启用开始安装按钮
self.main_window.set_start_button_enabled(True)
# 网络故障时,恢复主窗口
self.main_window.setEnabled(True)
self.main_window.ui.start_install_text.setText("开始安装")
return
# 填充下载队列
self._fill_download_queue(config)
# 填充下载队列,传入选定的游戏目录
self._fill_download_queue(config, selected_game_dirs)
# 如果没有需要下载的内容,直接进行最终校验
if not self.download_queue:
self.main_window.after_hash_compare()
return
# 只有当有需要下载内容时才询问是否使用Cloudflare加速
# 询问用户是否使用Cloudflare加速
# 询问是否使用Cloudflare加速
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:
print(f"DEBUG: 填充下载队列, 游戏目录: {game_dirs}")
# 添加nekopara 1-4
for i in range(1, 5):
game_version = f"NEKOPARA Vol.{i}"
# 只处理game_dirs中包含的游戏版本(如果用户选择了特定版本)
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:
print(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)
# 添加nekopara after
game_version = "NEKOPARA After"
# 只处理game_dirs中包含的游戏版本(如果用户选择了特定版本)
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:
print(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 _show_cloudflare_option(self):
"""显示Cloudflare加速选择对话框"""
# 临时启用窗口以显示对话框
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文件请注意放行。")
@@ -243,94 +410,40 @@ class DownloadManager:
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()
use_optimization = msg_box.clickedButton() == yes_button
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.optimization_done:
first_url = self.download_queue[0][0]
self._start_ip_optimization(first_url)
else:
# 如果用户选择不优化,或已经优化过,直接开始下载
self.next_download_task()
def _fill_download_queue(self, config):
"""填充下载队列
Args:
config: 包含下载URL的配置字典
"""
# 清空现有队列
self.download_queue.clear()
# 创建下载历史记录列表,用于跟踪本次安装的游戏
if not hasattr(self.main_window, 'download_queue_history'):
self.main_window.download_queue_history = []
# 获取所有识别到的游戏目录
game_dirs = self.main_window.identify_game_directories_improved(self.selected_folder)
debug_mode = self.is_debug_mode()
if debug_mode:
print(f"DEBUG: 填充下载队列, 识别到的游戏目录: {game_dirs}")
# 添加nekopara 1-4
for i in range(1, 5):
game_version = f"NEKOPARA Vol.{i}"
if not self.main_window.installed_status.get(game_version, False):
url = config.get(f"vol{i}")
if not url: continue
# 确定游戏文件夹路径
if game_version in game_dirs:
game_folder = game_dirs[game_version]
if debug_mode:
print(f"DEBUG: 使用识别到的游戏目录 {game_version}: {game_folder}")
else:
# 回退到传统方式
game_folder = os.path.join(self.selected_folder, f"NEKOPARA Vol. {i}")
if debug_mode:
print(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)
# 添加nekopara after
game_version = "NEKOPARA After"
if not self.main_window.installed_status.get(game_version, False):
url = config.get("after")
if url:
# 确定After的游戏文件夹路径
if game_version in game_dirs:
game_folder = game_dirs[game_version]
if debug_mode:
print(f"DEBUG: 使用识别到的游戏目录 {game_version}: {game_folder}")
else:
game_folder = os.path.join(self.selected_folder, "NEKOPARA After")
if debug_mode:
print(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 _start_ip_optimization(self, url):
"""开始IP优化过程
Args:
url: 用于优化的URL
"""
# 禁用退出按钮
self.main_window.ui.exit_btn.setEnabled(False)
# 创建取消状态标记
self.optimization_cancelled = False
# 使用Cloudflare图标创建消息框
self.optimizing_msg_box = msgbox_frame(
f"通知 - {APP_NAME}",
"\n正在优选Cloudflare IP请稍候...\n\n这可能需要5-10分钟请耐心等待喵~"
@@ -344,22 +457,57 @@ class DownloadManager:
self.optimizing_msg_box.setIconPixmap(cf_pixmap.scaled(64, 64, Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation))
# 我们不再提供"跳过"按钮
self.optimizing_msg_box.setStandardButtons(QtWidgets.QMessageBox.StandardButton.NoButton)
# 添加取消按钮
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.optimizing_msg_box.open()
# 创建并启动优化线程
self.ip_optimizer_thread = IpOptimizerThread(url)
self.ip_optimizer_thread.finished.connect(self.on_optimization_finished)
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()
# 恢复主窗口状态
self.main_window.setEnabled(True)
self.main_window.ui.start_install_text.setText("开始安装")
# 清空下载队列
self.download_queue.clear()
# 显示取消消息
QtWidgets.QMessageBox.information(
self.main_window,
f"已取消 - {APP_NAME}",
"\n已取消IP优选和安装过程。\n"
)
def on_optimization_finished(self, ip):
"""IP优化完成后的处理
Args:
ip: 优选的IP地址如果失败则为空字符串
"""
# 如果已经取消,则不继续处理
if hasattr(self, 'optimization_cancelled') and self.optimization_cancelled:
return
self.optimized_ip = ip
self.optimization_done = True
@@ -371,11 +519,15 @@ class DownloadManager:
# 显示优选结果
if not ip:
# 临时启用窗口以显示对话框
self.main_window.setEnabled(True)
msg_box = QtWidgets.QMessageBox(self.main_window)
msg_box.setWindowTitle(f"优选失败 - {APP_NAME}")
msg_box.setText("\n未能找到合适的Cloudflare IP将使用默认网络进行下载。\n\n10秒后自动继续...")
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
@@ -393,11 +545,22 @@ class DownloadManager:
timer.timeout.connect(update_countdown)
timer.start(1000) # 每秒更新一次
# 显示对话框,但不阻塞主线程
msg_box.open()
# 显示对话框并等待用户响应
result = msg_box.exec()
# 连接关闭信号以停止计时器
msg_box.finished.connect(timer.stop)
# 停止计时器
timer.stop()
# 如果用户点击了取消安装
if result == QtWidgets.QMessageBox.StandardButton.RejectRole:
# 恢复主窗口状态
self.main_window.ui.start_install_text.setText("开始安装")
# 清空下载队列
self.download_queue.clear()
return
# 用户点击了继续,重新禁用主窗口
self.main_window.setEnabled(False)
else:
# 应用优选IP到hosts文件
if self.download_queue:
@@ -407,12 +570,16 @@ class DownloadManager:
# 先清理可能存在的旧记录
self.hosts_manager.clean_hostname_entries(hostname)
# 临时启用窗口以显示对话框
self.main_window.setEnabled(True)
if self.hosts_manager.apply_ip(hostname, ip):
msg_box = QtWidgets.QMessageBox(self.main_window)
msg_box.setWindowTitle(f"成功 - {APP_NAME}")
msg_box.setText(f"\n已将优选IP ({ip}) 应用到hosts文件。\n\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
@@ -430,20 +597,37 @@ class DownloadManager:
timer.timeout.connect(update_countdown)
timer.start(1000) # 每秒更新一次
# 显示对话框,但不阻塞主线程
msg_box.open()
# 显示对话框并等待用户响应
result = msg_box.exec()
# 连接关闭信号以停止计时器
msg_box.finished.connect(timer.stop)
# 停止计时器
timer.stop()
# 如果用户点击了取消安装
if result == QtWidgets.QMessageBox.StandardButton.RejectRole:
# 恢复主窗口状态
self.main_window.setEnabled(True)
self.main_window.ui.start_install_text.setText("开始安装")
# 清空下载队列
self.download_queue.clear()
return
else:
QtWidgets.QMessageBox.critical(
self.main_window,
f"错误 - {APP_NAME}",
"\n修改hosts文件失败请检查程序是否以管理员权限运行。\n"
)
# 恢复主窗口状态
self.main_window.ui.start_install_text.setText("开始安装")
# 清空下载队列并退出
self.download_queue.clear()
return
# 用户点击了继续,重新禁用主窗口
self.main_window.setEnabled(False)
# 计时器结束或用户点击确定时,继续下载
QtCore.QTimer.singleShot(10000, self.next_download_task)
QtCore.QTimer.singleShot(100, self.next_download_task)
def next_download_task(self):
"""处理下载队列中的下一个任务"""
@@ -478,7 +662,7 @@ class DownloadManager:
print(f"DEBUG: 游戏文件夹: {game_folder}")
# 获取游戏可执行文件路径
game_dirs = self.main_window.identify_game_directories_improved(self.selected_folder)
game_dirs = self.main_window.game_detector.identify_game_directories_improved(self.selected_folder)
game_exe_exists = False
if game_version in game_dirs:
@@ -588,8 +772,7 @@ class DownloadManager:
game_folder: 游戏文件夹路径
plugin_path: 插件路径
"""
# 禁用退出按钮
self.main_window.ui.exit_btn.setEnabled(False)
# 按钮在file_dialog中已设置为禁用状态
if self.optimized_ip:
print(f"已为 {game_version} 获取到优选IP: {self.optimized_ip}")
@@ -611,12 +794,34 @@ class DownloadManager:
)
)
# 连接停止按钮
self.main_window.progress_window.stop_button.clicked.connect(self.current_download_thread.stop)
# 连接停止按钮到我们的on_download_stopped方法
self.main_window.progress_window.stop_button.clicked.connect(self.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 on_download_finished(self, success, error, url, game_folder, game_version, _7z_path, plugin_path):
"""下载完成后的处理
@@ -631,14 +836,19 @@ class DownloadManager:
plugin_path: 插件路径
"""
# 关闭进度窗口
if self.main_window.progress_window.isVisible():
if self.main_window.progress_window and self.main_window.progress_window.isVisible():
self.main_window.progress_window.reject()
self.main_window.progress_window = None
# 处理下载失败
if not success:
print(f"--- Download Failed: {game_version} ---")
print(error)
print("------------------------------------")
# 临时启用窗口以显示对话框
self.main_window.setEnabled(True)
msg_box = QtWidgets.QMessageBox(self.main_window)
msg_box.setWindowTitle(f"下载失败 - {APP_NAME}")
msg_box.setText(f"\n文件获取失败: {game_version}\n错误: {error}\n\n是否重试?")
@@ -652,10 +862,15 @@ class DownloadManager:
# 处理用户选择
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
@@ -680,11 +895,37 @@ class DownloadManager:
# 关闭哈希检查窗口
if self.main_window.hash_msg_box and self.main_window.hash_msg_box.isVisible():
self.main_window.hash_msg_box.close()
self.main_window.hash_msg_box = None
# 处理解压结果
if not success:
# 临时启用窗口以显示错误消息
self.main_window.setEnabled(True)
QtWidgets.QMessageBox.critical(self.main_window, f"错误 - {APP_NAME}", error_message)
self.main_window.installed_status[game_version] = False
# 询问用户是否继续其他游戏的安装
reply = QtWidgets.QMessageBox.question(
self.main_window,
f"继续安装? - {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)
self.next_download_task()
else:
# 用户选择停止,保持窗口启用状态
self.main_window.ui.start_install_text.setText("开始安装")
# 清空剩余队列
self.download_queue.clear()
# 显示已完成的安装结果
self.main_window.show_result()
return
else:
self.main_window.installed_status[game_version] = True
@@ -711,15 +952,21 @@ class DownloadManager:
self.download_queue.clear()
# 确保进度窗口已关闭
if hasattr(self.main_window, 'progress_window') and self.main_window.progress_window.isVisible():
self.main_window.progress_window.reject()
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
# 可以在这里决定是否立即进行哈希比较或显示结果
print("下载已全部停止。")
self.main_window.setEnabled(True) # 恢复主窗口交互
# 重新启用退出按钮和开始安装按钮
self.main_window.ui.exit_btn.setEnabled(True)
self.main_window.set_start_button_enabled(True)
# 恢复主窗口状态
self.main_window.setEnabled(True)
self.main_window.ui.start_install_text.setText("开始安装")
self.main_window.show_result()
# 显示取消安装的消息
QtWidgets.QMessageBox.information(
self.main_window,
f"已取消 - {APP_NAME}",
"\n已成功取消安装进程。\n"
)

View File

@@ -0,0 +1,311 @@
import os
import re
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
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:
print(f"DEBUG: 尝试识别游戏版本: {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:
print(f"DEBUG: 从目录名推测游戏版本: {potential_version}, 卷号: {vol_num}")
elif "after" in dir_name:
potential_version = "NEKOPARA After"
if debug_mode:
print(f"DEBUG: 从目录名推测游戏版本: 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:
print(f"DEBUG: 通过可执行文件确认游戏版本: {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:
print(f"DEBUG: 通过递归搜索确认游戏版本: {potential_version}, 文件: {file}")
return potential_version
# 如果仍然没有找到,基于目录名的推测返回结果
if potential_version:
if debug_mode:
print(f"DEBUG: 基于目录名返回推测的游戏版本: {potential_version}")
return potential_version
if debug_mode:
print(f"DEBUG: 无法识别游戏版本: {game_dir}")
return None
def identify_game_directories_improved(self, selected_folder):
"""改进的游戏目录识别,支持大小写不敏感和特殊字符处理
Args:
selected_folder: 选择的上级目录
Returns:
dict: 游戏版本到游戏目录的映射
"""
debug_mode = self._is_debug_mode()
if debug_mode:
print(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:
print(f"DEBUG: 找到以下子目录: {all_dirs}")
except Exception as e:
if debug_mode:
print(f"DEBUG: 无法读取目录 {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:
print(f"DEBUG: 搜索游戏 {game}, 预期目录: {expected_dir}, 预期可执行文件: {expected_exe}")
# 尝试不同的匹配方法
found_dir = None
# 1. 精确匹配
if expected_dir in all_dirs:
found_dir = expected_dir
if debug_mode:
print(f"DEBUG: 精确匹配成功: {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:
print(f"DEBUG: 大小写不敏感匹配成功: {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:
print(f"DEBUG: 模糊匹配成功: {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:
print(f"DEBUG: 提取卷号: {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:
print(f"DEBUG: 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:
print(f"DEBUG: 卷号匹配成功: {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:
print(f"DEBUG: 验证成功,找到游戏可执行文件: {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:
print(f"DEBUG: 通过递归搜索找到游戏可执行文件: {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:
print(f"DEBUG: 通过递归搜索找到After游戏可执行文件: {found_exe}")
break
if exe_exists:
break
# 如果找到了可执行文件,将该目录添加到游戏目录列表
if exe_exists:
game_paths[game] = potential_path
if debug_mode:
print(f"DEBUG: 验证成功,将 {potential_path} 添加为 {game} 的目录")
else:
if debug_mode:
print(f"DEBUG: 未找到任何可执行文件变体,游戏 {game}{potential_path} 未找到")
if debug_mode:
print(f"DEBUG: 最终识别的游戏目录: {game_paths}")
print(f"--- 目录识别结束 ---")
return game_paths

View File

@@ -0,0 +1,253 @@
import os
import shutil
from PySide6.QtWidgets import QMessageBox
class PatchManager:
"""补丁管理器,用于处理补丁的安装和卸载"""
def __init__(self, app_name, game_info, debug_manager=None):
"""初始化补丁管理器
Args:
app_name: 应用程序名称,用于显示消息框标题
game_info: 游戏信息字典,包含各版本的安装路径和可执行文件名
debug_manager: 调试管理器实例,用于输出调试信息
"""
self.app_name = app_name
self.game_info = game_info
self.debug_manager = debug_manager
self.installed_status = {} # 游戏版本的安装状态
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):
"""卸载补丁
Args:
game_dir: 游戏目录路径
game_version: 游戏版本
Returns:
bool: 卸载成功返回True失败返回False
"""
debug_mode = self._is_debug_mode()
if game_version not in self.game_info:
QMessageBox.critical(
None,
f"错误 - {self.app_name}",
f"\n无法识别游戏版本: {game_version}\n",
QMessageBox.StandardButton.Ok,
)
return False
if debug_mode:
print(f"DEBUG: 开始卸载 {game_version} 补丁,目录: {game_dir}")
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)
# 尝试查找补丁文件,支持不同大小写
patch_files_to_check = [
patch_file_path,
patch_file_path.lower(),
patch_file_path.upper(),
patch_file_path.replace("_", ""),
patch_file_path.replace("_", "-"),
]
# 查找并删除补丁文件
patch_file_found = False
for patch_path in patch_files_to_check:
if os.path.exists(patch_path):
patch_file_found = True
os.remove(patch_path)
files_removed += 1
if debug_mode:
print(f"DEBUG: 已删除补丁文件: {patch_path}")
if not patch_file_found and debug_mode:
print(f"DEBUG: 未找到补丁文件,检查了以下路径: {patch_files_to_check}")
# 检查是否有额外的签名文件 (.sig)
if game_version == "NEKOPARA After":
for patch_path in patch_files_to_check:
sig_file_path = f"{patch_path}.sig"
if os.path.exists(sig_file_path):
os.remove(sig_file_path)
files_removed += 1
if debug_mode:
print(f"DEBUG: 已删除签名文件: {sig_file_path}")
# 删除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):
shutil.rmtree(patch_folder)
files_removed += 1
if debug_mode:
print(f"DEBUG: 已删除补丁文件夹: {patch_folder}")
# 删除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):
shutil.rmtree(game_patch_folder)
files_removed += 1
if debug_mode:
print(f"DEBUG: 已删除game/patch文件夹: {game_patch_folder}")
# 删除配置文件
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):
os.remove(config_path)
files_removed += 1
if debug_mode:
print(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):
os.remove(script_path)
files_removed += 1
if debug_mode:
print(f"DEBUG: 已删除脚本文件: {script_path}")
# 更新安装状态
self.installed_status[game_version] = False
# 在非批量卸载模式下显示卸载成功消息
if game_version != "all":
# 显示卸载成功消息
if files_removed > 0:
QMessageBox.information(
None,
f"卸载完成 - {self.app_name}",
f"\n{game_version} 补丁卸载成功!\n共删除 {files_removed} 个文件/文件夹。\n",
QMessageBox.StandardButton.Ok,
)
else:
QMessageBox.warning(
None,
f"警告 - {self.app_name}",
f"\n未找到 {game_version} 的补丁文件,可能未安装补丁或已被移除。\n",
QMessageBox.StandardButton.Ok,
)
# 卸载成功
return True
except Exception as e:
# 在非批量卸载模式下显示卸载失败消息
if game_version != "all":
# 显示卸载失败消息
error_message = f"\n卸载 {game_version} 补丁时出错:\n\n{str(e)}\n"
if debug_mode:
print(f"DEBUG: 卸载错误 - {str(e)}")
QMessageBox.critical(
None,
f"卸载失败 - {self.app_name}",
error_message,
QMessageBox.StandardButton.Ok,
)
# 卸载失败
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()
for version, path in game_dirs.items():
try:
if self.uninstall_patch(path, version):
success_count += 1
else:
fail_count += 1
except Exception as e:
if debug_mode:
print(f"DEBUG: 卸载 {version} 时出错: {str(e)}")
fail_count += 1
return success_count, fail_count
def show_uninstall_result(self, success_count, fail_count):
"""显示批量卸载结果
Args:
success_count: 成功卸载的数量
fail_count: 卸载失败的数量
"""
QMessageBox.information(
None,
f"批量卸载完成 - {self.app_name}",
f"\n批量卸载完成!\n成功: {success_count}\n失败: {fail_count}\n",
QMessageBox.StandardButton.Ok,
)

View File

@@ -0,0 +1,226 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
import json
from PySide6.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QTextBrowser, QPushButton, QCheckBox, QLabel, QMessageBox
from PySide6.QtCore import Qt
from data.privacy_policy import PRIVACY_POLICY_BRIEF, get_local_privacy_policy, PRIVACY_POLICY_VERSION
from data.config import CACHE, APP_NAME, APP_VERSION
from utils import msgbox_frame
from utils.logger import setup_logger
class PrivacyManager:
"""隐私协议管理器,负责显示隐私协议对话框并处理用户选择"""
def __init__(self):
"""初始化隐私协议管理器"""
# 初始化日志
self.logger = setup_logger("privacy_manager")
self.logger.info("正在初始化隐私协议管理器")
# 确保缓存目录存在
os.makedirs(CACHE, exist_ok=True)
self.config_file = os.path.join(CACHE, "privacy_config.json")
self.privacy_config = self._load_privacy_config()
# 获取隐私协议内容和版本
self.logger.info("读取本地隐私协议文件")
self.privacy_content, self.current_privacy_version, error = get_local_privacy_policy()
if error:
self.logger.warning(f"读取本地隐私协议文件警告: {error}")
# 使用默认版本作为备用
self.current_privacy_version = PRIVACY_POLICY_VERSION
self.logger.info(f"隐私协议版本: {self.current_privacy_version}")
# 检查隐私协议版本和用户同意状态
self.privacy_accepted = self._check_privacy_acceptance()
def _load_privacy_config(self):
"""加载隐私协议配置
Returns:
dict: 隐私协议配置信息
"""
if os.path.exists(self.config_file):
try:
with open(self.config_file, "r", encoding="utf-8") as f:
config = json.load(f)
return config
except (json.JSONDecodeError, IOError) as e:
self.logger.error(f"读取隐私配置失败: {e}")
# 如果读取失败,返回空配置,强制显示隐私协议
return {"privacy_accepted": False}
return {"privacy_accepted": False}
def _check_privacy_acceptance(self):
"""检查隐私协议是否需要重新同意
如果隐私协议版本变更,则需要重新同意
Returns:
bool: 是否已有有效的隐私协议同意
"""
# 获取存储的版本信息
stored_privacy_version = self.privacy_config.get("privacy_version", "0.0.0")
stored_app_version = self.privacy_config.get("app_version", "0.0.0")
privacy_accepted = self.privacy_config.get("privacy_accepted", False)
self.logger.info(f"存储的隐私协议版本: {stored_privacy_version}, 当前版本: {self.current_privacy_version}")
self.logger.info(f"存储的应用版本: {stored_app_version}, 当前版本: {APP_VERSION}")
self.logger.info(f"隐私协议接受状态: {privacy_accepted}")
# 如果隐私协议版本变更,需要重新同意
if stored_privacy_version != self.current_privacy_version:
self.logger.info("隐私协议版本已变更,需要重新同意")
return False
# 返回当前的同意状态
return privacy_accepted
def _save_privacy_config(self, accepted):
"""保存隐私协议配置
Args:
accepted: 用户是否同意隐私协议
Returns:
bool: 配置是否保存成功
"""
try:
# 确保目录存在
os.makedirs(os.path.dirname(self.config_file), exist_ok=True)
# 写入配置文件,包含应用版本和隐私协议版本
with open(self.config_file, "w", encoding="utf-8") as f:
json.dump({
"privacy_accepted": accepted,
"privacy_version": self.current_privacy_version, # 保存当前隐私协议版本
"app_version": APP_VERSION # 保存当前应用版本
}, f, indent=2)
# 更新实例变量
self.privacy_accepted = accepted
self.privacy_config = {
"privacy_accepted": accepted,
"privacy_version": self.current_privacy_version,
"app_version": APP_VERSION
}
return True
except IOError as e:
self.logger.error(f"保存隐私协议配置失败: {e}")
# 显示保存失败的提示
QMessageBox.warning(
None,
f"配置保存警告 - {APP_NAME}",
f"隐私设置无法保存到配置文件,下次启动时可能需要重新确认。\n\n错误信息:{e}"
)
return False
def show_privacy_dialog(self):
"""显示隐私协议对话框
Returns:
bool: 用户是否同意隐私协议
"""
# 如果用户已经同意了隐私协议直接返回True不显示对话框
if self.privacy_accepted:
self.logger.info("用户已同意当前版本的隐私协议,无需再次显示")
return True
self.logger.info("首次运行或隐私协议版本变更,显示隐私对话框")
# 创建隐私协议对话框
dialog = QDialog()
dialog.setWindowTitle(f"隐私政策 - {APP_NAME}")
dialog.setMinimumSize(600, 400)
dialog.setWindowFlags(dialog.windowFlags() & ~Qt.WindowContextHelpButtonHint)
# 创建布局
layout = QVBoxLayout(dialog)
# 添加标题和版本信息
title_label = QLabel(f"请阅读并同意以下隐私政策 (更新日期: {self.current_privacy_version})")
title_label.setStyleSheet("font-size: 14px; font-weight: bold;")
layout.addWidget(title_label)
# 添加隐私协议文本框
text_browser = QTextBrowser()
# 这里使用PRIVACY_POLICY_BRIEF而不是self.privacy_content保持UI简洁
text_browser.setMarkdown(PRIVACY_POLICY_BRIEF)
text_browser.setOpenExternalLinks(True)
layout.addWidget(text_browser)
# 添加同意选择框
checkbox = QCheckBox("我已阅读并同意上述隐私政策")
layout.addWidget(checkbox)
# 添加按钮
buttons_layout = QHBoxLayout()
agree_button = QPushButton("同意并继续")
agree_button.setEnabled(False) # 初始状态为禁用
decline_button = QPushButton("不同意并退出")
buttons_layout.addWidget(agree_button)
buttons_layout.addWidget(decline_button)
layout.addLayout(buttons_layout)
# 连接选择框状态变化 - 修复勾选后按钮不亮起的问题
def on_checkbox_state_changed(state):
self.logger.debug(f"复选框状态变更为: {state}")
agree_button.setEnabled(state == 2) # Qt.Checked 在 PySide6 中值为 2
checkbox.stateChanged.connect(on_checkbox_state_changed)
# 连接按钮点击事件
agree_button.clicked.connect(lambda: self._on_agree(dialog))
decline_button.clicked.connect(lambda: self._on_decline(dialog))
# 显示对话框
result = dialog.exec()
# 返回用户选择结果
return self.privacy_accepted
def _on_agree(self, dialog):
"""处理用户同意隐私协议
Args:
dialog: 对话框实例
"""
# 保存配置并更新状态
self._save_privacy_config(True)
dialog.accept()
def _on_decline(self, dialog):
"""处理用户拒绝隐私协议
Args:
dialog: 对话框实例
"""
# 显示拒绝信息
msg_box = msgbox_frame(
f"退出 - {APP_NAME}",
"\n您需要同意隐私政策才能使用本软件。\n软件将立即退出。\n",
QMessageBox.Ok,
)
msg_box.exec()
# 保存拒绝状态
self._save_privacy_config(False)
dialog.reject()
def is_privacy_accepted(self):
"""检查用户是否已同意隐私协议
Returns:
bool: 用户是否已同意隐私协议
"""
return self.privacy_accepted
def reset_privacy_agreement(self):
"""重置隐私协议同意状态,用于测试或重新显示隐私协议
Returns:
bool: 重置是否成功
"""
return self._save_privacy_config(False)

View File

@@ -1,10 +1,11 @@
from PySide6.QtGui import QIcon, QAction, QFont
from PySide6.QtWidgets import QMessageBox, QMainWindow
from PySide6.QtCore import Qt
from PySide6.QtGui import QIcon, QAction, QFont, QCursor
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
from data.config import APP_NAME, APP_VERSION
from utils import load_base64_image, msgbox_frame, resource_path
from data.config import APP_NAME, APP_VERSION, LOG_FILE
class UIManager:
def __init__(self, main_window):
@@ -17,6 +18,11 @@ class UIManager:
# 使用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 # 关于按钮
def setup_ui(self):
"""设置UI元素包括窗口图标、标题和菜单"""
@@ -30,38 +36,265 @@ class UIManager:
# 设置窗口标题
self.main_window.setWindowTitle(f"{APP_NAME} v{APP_VERSION}")
# 创建关于按钮
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
# 创建菜单项
project_home_action = QAction("项目主页", self.main_window)
project_home_action.triggered.connect(self.open_project_home_page)
# 获取菜单字体
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)
about_action = QAction("关于", self.main_window)
about_action.triggered.connect(self.show_about_dialog)
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.addAction(project_home_action)
self.ui.menu_2.addAction(about_action)
# 清除现有菜单项并添加新的菜单
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
# 创建菜单项
self.debug_action = QAction("Debug模式", self.main_window, checkable=True)
# 获取菜单字体
menu_font = self._get_menu_font()
font_family = menu_font.family()
# 创建开发者选项子菜单
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)
# 添加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.hosts_submenu.addAction(self.restore_hosts_action)
self.hosts_submenu.addAction(self.clean_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', {})
@@ -74,33 +307,258 @@ class UIManager:
# 安全地连接toggle_debug_mode方法
if hasattr(self.main_window, 'toggle_debug_mode'):
self.debug_action.triggered.connect(self.main_window.toggle_debug_mode)
# 添加到菜单
self.ui.menu.addAction(self.debug_action)
# 为未来功能预留的"切换下载源"按钮
self.switch_source_action = QAction("切换下载源", self.main_window)
self.switch_source_action.setEnabled(False) # 暂时禁用
# 创建打开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.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.ui.menu.addAction(self.switch_source_action)
# 添加分隔符
self.ui.menu.addSeparator()
self.ui.menu.addMenu(self.dev_menu) # 添加开发者选项子菜单
# 连接按钮点击事件,如果使用按钮式菜单
if hasattr(self.ui, 'settings_btn'):
# 按钮已经连接到显示菜单,不需要额外处理
pass
# 添加Debug子菜单到开发者选项菜单
self.dev_menu.addMenu(self.debug_submenu)
self.dev_menu.addMenu(self.hosts_submenu) # 添加hosts文件选项子菜单
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 = msgbox_frame(
f"确认操作 - {APP_NAME}",
"\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 = msgbox_frame(
f"操作成功 - {APP_NAME}",
"\n已成功撤回隐私协议同意。\n\n软件将立即重启。\n",
QMessageBox.StandardButton.Ok,
)
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 = msgbox_frame(
f"操作失败 - {APP_NAME}",
"\n撤回隐私协议同意失败。\n\n请检查应用权限或稍后再试。\n",
QMessageBox.StandardButton.Ok,
)
fail_msg.exec()
except Exception as e:
# 显示错误提示
error_msg = msgbox_frame(
f"错误 - {APP_NAME}",
f"\n撤回隐私协议同意时发生错误:\n\n{str(e)}\n",
QMessageBox.StandardButton.Ok,
)
error_msg.exec()
def show_under_development(self):
"""显示功能正在开发中的提示"""
msg_box = msgbox_frame(
f"提示 - {APP_NAME}",
"\n该功能正在开发中,敬请期待!\n",
QMessageBox.StandardButton.Ok,
)
msg_box.exec()
def open_log_file(self):
"""打开log.txt文件"""
if os.path.exists(LOG_FILE):
try:
# 使用操作系统默认程序打开日志文件
if os.name == 'nt': # Windows
os.startfile(LOG_FILE)
else: # macOS 和 Linux
import subprocess
subprocess.call(['xdg-open', LOG_FILE])
except Exception as e:
msg_box = msgbox_frame(
f"错误 - {APP_NAME}",
f"\n打开log.txt文件失败\n\n{str(e)}\n",
QMessageBox.StandardButton.Ok,
)
msg_box.exec()
else:
msg_box = msgbox_frame(
f"提示 - {APP_NAME}",
"\nlog.txt文件不存在请确保Debug模式已开启并生成日志。\n",
QMessageBox.StandardButton.Ok,
)
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 = msgbox_frame(
f"成功 - {APP_NAME}",
"\nhosts文件已成功还原为备份版本。\n",
QMessageBox.StandardButton.Ok,
)
else:
msg_box = msgbox_frame(
f"警告 - {APP_NAME}",
"\n还原hosts文件失败或没有找到备份文件。\n",
QMessageBox.StandardButton.Ok,
)
msg_box.exec()
except Exception as e:
msg_box = msgbox_frame(
f"错误 - {APP_NAME}",
f"\n还原hosts文件时发生错误\n\n{str(e)}\n",
QMessageBox.StandardButton.Ok,
)
msg_box.exec()
else:
msg_box = msgbox_frame(
f"错误 - {APP_NAME}",
"\n无法访问hosts管理器。\n",
QMessageBox.StandardButton.Ok,
)
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()
if result:
msg_box = msgbox_frame(
f"成功 - {APP_NAME}",
"\n已成功清理软件添加的hosts条目。\n",
QMessageBox.StandardButton.Ok,
)
else:
msg_box = msgbox_frame(
f"提示 - {APP_NAME}",
"\n未发现软件添加的hosts条目或清理操作失败。\n",
QMessageBox.StandardButton.Ok,
)
msg_box.exec()
except Exception as e:
msg_box = msgbox_frame(
f"错误 - {APP_NAME}",
f"\n清理hosts条目时发生错误\n\n{str(e)}\n",
QMessageBox.StandardButton.Ok,
)
msg_box.exec()
else:
msg_box = msgbox_frame(
f"错误 - {APP_NAME}",
"\n无法访问hosts管理器。\n",
QMessageBox.StandardButton.Ok,
)
msg_box.exec()
def show_about_dialog(self):
"""显示关于对话框"""
about_text = f"""
<p><b>{APP_NAME} v{APP_VERSION}</b></p>
<p>GitHub: <a href="https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT">https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT</a></p>
<p>原作: <a href="https://github.com/Yanam1Anna">Yanam1Anna</a></p>
<p>此应用根据 <a href="https://github.com/hyb-oyqq/FRAISEMOE2-Installer/blob/master/LICENSE">GPL-3.0 许可证</a> 授权。</p>
<p>此应用根据 <a href="https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/blob/master/LICENSE">GPL-3.0 许可证</a> 授权。</p>
<br>
<p><b>感谢:</b></p>
<p>- <a href="https://github.com/HTony03">HTony03</a>:对原项目部分源码的重构、逻辑优化和功能实现提供了支持。</p>
<p>- <a href="https://github.com/ABSIDIA">钨鸮</a>:对于云端资源存储提供了支持。</p>
<p>- <a href="https://github.com/XIU2/CloudflareSpeedTest">XIU2/CloudflareSpeedTest</a>:提供了 IP 优选功能的核心支持。</p>
"""
msg_box = msgbox_frame(
f"关于 - {APP_NAME}",

View File

@@ -0,0 +1,141 @@
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.28) - 10 # 调整为更靠上的位置
self.ui.button_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.46) - 10 # 调整为中间位置
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.64) - 10 # 调整为更靠下的位置
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)

View File

@@ -3,7 +3,7 @@ import base64
# 配置信息
app_data = {
"APP_VERSION": "1.1.3",
"APP_VERSION": "1.2.0",
"APP_NAME": "FRAISEMOE Addons Installer NEXT",
"TEMP": "TEMP",
"CACHE": "FRAISEMOE",

View File

@@ -0,0 +1,94 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
import re
import sys
from datetime import datetime
# 隐私协议的缩略版内容
PRIVACY_POLICY_BRIEF = """
# FRAISEMOE Addons Installer NEXT 隐私政策摘要
本应用在运行过程中会收集和处理以下信息:
## 收集的信息
- **系统信息**:程序版本号。
- **网络信息**IP 地址、ISP、地理位置用于使用统计、下载统计。
- **文件信息**:游戏安装路径、文件哈希值。
## 系统修改
- 使用 Cloudflare 加速时会临时修改系统 hosts 文件。
- 修改前会自动备份,程序退出时自动恢复。
## 第三方服务
- **Cloudflare 服务**:通过开源项目 CloudflareSpeedTest (CFST) 提供,用于优化下载速度。此过程会将您的 IP 提交至 Cloudflare 节点。
- **云端配置服务**:获取配置信息。服务器会记录您的 IP、ISP 及地理位置用于统计。
完整的隐私政策可在本程序的 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.
- **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.
The complete privacy policy can be found in the program's GitHub repository.
"""
# 默认隐私协议版本 - 本地版本的日期
PRIVACY_POLICY_VERSION = "2025.07.31"
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')
print(f"成功读取本地隐私协议文件: {path}, 版本: {date_version}")
return content, date_version, ""
except ValueError:
print(f"本地隐私协议日期格式解析错误: {path}")
else:
print(f"本地隐私协议未找到更新日期: {path}")
except Exception as e:
print(f"读取本地隐私协议失败 {path}: {str(e)}")
# 所有路径都尝试失败,使用默认版本
return PRIVACY_POLICY_BRIEF, PRIVACY_POLICY_VERSION, "无法读取本地隐私协议文件"

View File

@@ -23,7 +23,8 @@ from workers import (
HashThread, ExtractionThread, ConfigFetchThread
)
from core import (
MultiStageAnimations, UIManager, DownloadManager, DebugManager
MultiStageAnimations, UIManager, DownloadManager, DebugManager,
WindowManager, GameDetector, PatchManager, ConfigManager
)
class MainWindow(QMainWindow):
@@ -41,31 +42,13 @@ class MainWindow(QMainWindow):
self.setMinimumSize(QSize(1024, 576))
self.setMaximumSize(QSize(1280, 720))
# 窗口比例 (16:9)
self.aspect_ratio = 16 / 9
# 拖动窗口相关变量
self._drag_position = QPoint()
self._is_dragging = False
# 初始化UI (从Ui_install.py导入)
self.ui = Ui_MainWindows()
self.ui.setupUi(self)
# 设置圆角窗口
self.setRoundedCorners()
# 初始化配置
self.config = load_config()
# 初始化状态变量
self.cloud_config = None
self.config_valid = False # 添加配置有效标志
self.installed_status = {f"NEKOPARA Vol.{i}": False for i in range(1, 5)}
self.installed_status["NEKOPARA After"] = False # 添加After的状态
self.hash_msg_box = None
self.progress_window = None
# 初始化工具类
self.hash_manager = HashManager(BLOCK_SIZE)
self.admin_privileges = AdminPrivileges()
@@ -77,9 +60,26 @@ class MainWindow(QMainWindow):
# 首先设置UI - 确保debug_action已初始化
self.ui_manager.setup_ui()
# 初始化新的管理器类
self.window_manager = WindowManager(self)
self.debug_manager = DebugManager(self)
# 为debug_manager设置ui_manager引用
self.debug_manager.set_ui_manager(self.ui_manager)
self.config_manager = ConfigManager(APP_NAME, CONFIG_URL, UA, self.debug_manager)
self.game_detector = GameDetector(GAME_INFO, self.debug_manager)
self.patch_manager = PatchManager(APP_NAME, GAME_INFO, self.debug_manager)
# 初始化下载管理器 - 应该放在其他管理器之后,因为它可能依赖于它们
self.download_manager = DownloadManager(self)
# 初始化状态变量
self.cloud_config = None
self.config_valid = False # 添加配置有效标志
self.patch_manager.initialize_status()
self.installed_status = self.patch_manager.get_status() # 获取初始化后的状态
self.hash_msg_box = None
self.progress_window = None
# 设置关闭按钮事件连接
if hasattr(self.ui, 'close_btn'):
self.ui.close_btn.clicked.connect(self.close)
@@ -126,105 +126,18 @@ class MainWindow(QMainWindow):
# 窗口显示后延迟100ms启动动画
QTimer.singleShot(100, self.start_animations)
def setRoundedCorners(self):
"""设置窗口圆角"""
# 实现圆角窗口
path = QPainterPath()
path.addRoundedRect(self.rect(), 20, 20)
mask = QRegion(path.toFillPolygon().toPolygon())
self.setMask(mask)
# 更新resize事件时更新圆角
self.updateRoundedCorners = True
# 添加鼠标事件处理,实现窗口拖动
# 窗口事件处理 - 委托给WindowManager
def mousePressEvent(self, 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.frameGeometry().topLeft()
event.accept()
self.window_manager.handle_mouse_press(event)
def mouseMoveEvent(self, event):
if event.buttons() & Qt.MouseButton.LeftButton and self._is_dragging:
self.move(event.globalPosition().toPoint() - self._drag_position)
event.accept()
self.window_manager.handle_mouse_move(event)
def mouseReleaseEvent(self, event):
if event.button() == Qt.MouseButton.LeftButton:
self._is_dragging = False
event.accept()
self.window_manager.handle_mouse_release(event)
def resizeEvent(self, event):
"""当窗口大小改变时更新圆角和维持纵横比"""
# 计算基于当前宽度的合适高度以维持16:9比例
new_width = event.size().width()
new_height = int(new_width / self.aspect_ratio)
if new_height != event.size().height():
# 阻止变形,保持比例
self.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)
# 更新背景图大小 - 使用setScaledContents简化处理
if hasattr(self.ui, 'Mainbg'):
self.ui.Mainbg.setGeometry(0, 0, new_width, new_height - 65)
# 使用setScaledContents=True不需要手动缩放
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.28) - 10 # 调整为更靠上的位置
self.ui.button_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.46) - 10 # 调整为中间位置
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.64) - 10 # 调整为更靠下的位置
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.rect(), 20, 20)
mask = QRegion(path.toFillPolygon().toPolygon())
self.setMask(mask)
self.window_manager.handle_resize(event)
super().resizeEvent(event)
def start_animations(self):
@@ -239,6 +152,7 @@ class MainWindow(QMainWindow):
self.animator.animation_finished.connect(self.on_animations_finished)
self.animator.start_animations()
# 在动画开始时获取云端配置
self.fetch_cloud_config()
def on_animations_finished(self):
@@ -251,30 +165,33 @@ class MainWindow(QMainWindow):
else:
self.set_start_button_enabled(False)
def set_start_button_enabled(self, enabled):
"""设置开始安装按钮启用状态和视觉效果
def set_start_button_enabled(self, enabled, installing=False):
"""[已弃用] 设置按钮启用状态的旧方法,保留以兼容旧代码
现在推荐使用主窗口的setEnabled方法和直接设置按钮文本
Args:
enabled: 是否启用按钮
installing: 是否正在安装中
"""
self.ui.start_install_btn.setEnabled(True) # 始终启用按钮,以便捕获点击事件
# 根据状态修改文本内容,但不再修改颜色样式
if enabled:
self.ui.start_install_text.setText("开始安装")
# 直接设置按钮文本,不改变窗口启用状态
if installing:
self.ui.start_install_text.setText("正在安装")
self.install_button_enabled = False
else:
self.ui.start_install_text.setText("!无法安装!")
# 记录当前按钮状态,用于点击事件处理
self.install_button_enabled = enabled
if enabled:
self.ui.start_install_text.setText("开始安装")
else:
self.ui.start_install_text.setText("!无法安装!")
self.install_button_enabled = enabled
def fetch_cloud_config(self):
"""获取云端配置"""
headers = {"User-Agent": UA}
debug_mode = self.ui_manager.debug_action.isChecked() if self.ui_manager.debug_action else False
self.config_fetch_thread = ConfigFetchThread(CONFIG_URL, headers, debug_mode, self)
self.config_fetch_thread.finished.connect(self.on_config_fetched)
self.config_fetch_thread.start()
"""获取云端配置(异步方式)"""
self.config_manager.fetch_cloud_config(
lambda url, headers, debug_mode, parent=None: ConfigFetchThread(url, headers, debug_mode, self),
self.on_config_fetched
)
def on_config_fetched(self, data, error_message):
"""云端配置获取完成的回调处理
@@ -283,71 +200,33 @@ class MainWindow(QMainWindow):
data: 获取到的配置数据
error_message: 错误信息,如果有
"""
# 定义debug_mode变量在方法开头
debug_mode = self.ui_manager.debug_action.isChecked() if hasattr(self.ui_manager, 'debug_action') and self.ui_manager.debug_action else False
# 处理返回结果
result = self.config_manager.on_config_fetched(data, error_message)
if error_message:
# 标记配置无效
self.config_valid = False
# 记录错误信息,用于按钮点击时显示
if error_message == "update_required":
self.last_error_message = "update_required"
msg_box = msgbox_frame(
f"更新提示 - {APP_NAME}",
"\n当前版本过低,请及时更新。\n",
QMessageBox.StandardButton.Ok,
)
msg_box.exec()
# 在浏览器中打开项目主页
webbrowser.open("https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/")
# 根据返回的操作执行相应动作
if result and "action" in result:
if result["action"] == "exit":
# 强制关闭程序
self.shutdown_app(force_exit=True)
return
elif "missing_keys" in error_message:
self.last_error_message = "missing_keys"
missing_versions = error_message.split(":")[1]
msg_box = msgbox_frame(
f"配置缺失 - {APP_NAME}",
f'\n云端缺失下载链接,可能云服务器正在维护,不影响其他版本下载。\n当前缺失版本:"{missing_versions}"\n',
QMessageBox.StandardButton.Ok,
)
msg_box.exec()
# 对于部分缺失,仍然允许使用,因为可能只影响部分游戏版本
self.config_valid = True
else:
# 设置网络错误标记
self.last_error_message = "network_error"
# 显示通用错误消息只在debug模式下显示详细错误
error_msg = "访问云端配置失败,请检查网络状况或稍后再试。"
if debug_mode and "详细错误:" in error_message:
msg_box = msgbox_frame(
f"错误 - {APP_NAME}",
f"\n{error_message}\n",
QMessageBox.StandardButton.Ok,
)
else:
msg_box = msgbox_frame(
f"错误 - {APP_NAME}",
f"\n{error_msg}\n",
QMessageBox.StandardButton.Ok,
)
msg_box.exec()
# 在无法连接到云端时禁用开始安装按钮
elif result["action"] == "disable_button":
# 禁用开始安装按钮
self.set_start_button_enabled(False)
else:
self.cloud_config = data
# 标记配置有效
self.config_valid = True
# 清除错误信息
self.last_error_message = ""
if debug_mode:
print("--- Cloud config fetched successfully ---")
print(json.dumps(data, indent=2))
# 确保按钮在成功获取配置时启用
self.set_start_button_enabled(True)
# 检查是否有后续操作
if "then" in result and result["then"] == "exit":
# 强制关闭程序
self.shutdown_app(force_exit=True)
elif result["action"] == "enable_button":
# 启用开始安装按钮
self.set_start_button_enabled(True)
# 同步状态
self.cloud_config = self.config_manager.get_cloud_config()
self.config_valid = self.config_manager.is_config_valid()
self.last_error_message = self.config_manager.get_last_error()
# 重新启用窗口,恢复用户交互
self.setEnabled(True)
def toggle_debug_mode(self, checked):
"""切换调试模式
@@ -359,7 +238,7 @@ class MainWindow(QMainWindow):
def save_config(self, config):
"""保存配置的便捷方法"""
save_config(config)
self.config_manager.save_config(config)
def create_download_thread(self, url, _7z_path, game_version):
"""创建下载线程
@@ -410,8 +289,7 @@ class MainWindow(QMainWindow):
def after_hash_compare(self):
"""进行安装后哈希比较"""
# 禁用退出按钮
self.ui.exit_btn.setEnabled(False)
# 禁用窗口已在安装流程开始时完成
self.hash_msg_box = self.hash_manager.hash_pop_window(check_type="after")
@@ -440,6 +318,9 @@ class MainWindow(QMainWindow):
self.hash_msg_box = None
if not result["passed"]:
# 启用窗口以显示错误消息
self.setEnabled(True)
game = result.get("game", "未知游戏")
message = result.get("message", "发生未知错误。")
msg_box = msgbox_frame(
@@ -449,9 +330,9 @@ class MainWindow(QMainWindow):
)
msg_box.exec()
# 重新启用退出按钮和开始安装按钮
self.ui.exit_btn.setEnabled(True)
self.set_start_button_enabled(True)
# 恢复窗口状态
self.setEnabled(True)
self.ui.start_install_text.setText("开始安装")
# 添加短暂延迟确保UI更新
QTimer.singleShot(100, self.show_result)
@@ -583,7 +464,7 @@ class MainWindow(QMainWindow):
if event:
event.accept()
else:
sys.exit(0)
sys.exit(0)
def handle_install_button_click(self):
"""处理安装按钮点击事件
@@ -631,7 +512,7 @@ class MainWindow(QMainWindow):
# 获取游戏目录
from PySide6.QtWidgets import QFileDialog
debug_mode = self.ui_manager.debug_action.isChecked() if hasattr(self.ui_manager, 'debug_action') and self.ui_manager.debug_action else False
debug_mode = self.debug_manager._is_debug_mode()
# 提示用户选择目录
file_dialog_info = "选择游戏上级目录" if debug_mode else "选择游戏目录"
@@ -644,7 +525,7 @@ class MainWindow(QMainWindow):
print(f"DEBUG: 卸载功能 - 用户选择了目录: {selected_folder}")
# 首先尝试将选择的目录视为上级目录,使用增强的目录识别功能
game_dirs = self.identify_game_directories_improved(selected_folder)
game_dirs = self.game_detector.identify_game_directories_improved(selected_folder)
if game_dirs and len(game_dirs) > 0:
# 找到了游戏目录,显示选择对话框
@@ -680,26 +561,9 @@ class MainWindow(QMainWindow):
)
if reply == QMessageBox.StandardButton.Yes:
success_count = 0
fail_count = 0
for version, path in game_dirs.items():
try:
if self.uninstall_patch(path, version):
success_count += 1
else:
fail_count += 1
except Exception as e:
if debug_mode:
print(f"DEBUG: 卸载 {version} 时出错: {str(e)}")
fail_count += 1
# 显示批量卸载结果
QMessageBox.information(
self,
f"批量卸载完成 - {APP_NAME}",
f"\n批量卸载完成!\n成功: {success_count}\n失败: {fail_count}\n",
QMessageBox.StandardButton.Ok,
)
# 使用批量卸载方法
success_count, fail_count = self.patch_manager.batch_uninstall_patches(game_dirs)
self.patch_manager.show_uninstall_result(success_count, fail_count)
else:
# 卸载选中的单个游戏
game_version = selected_game
@@ -710,7 +574,7 @@ class MainWindow(QMainWindow):
if debug_mode:
print(f"DEBUG: 卸载功能 - 未在上级目录找到游戏,尝试将选择的目录视为游戏目录")
game_version = self.identify_game_version(selected_folder)
game_version = self.game_detector.identify_game_version(selected_folder)
if game_version:
if debug_mode:
@@ -735,7 +599,7 @@ class MainWindow(QMainWindow):
game_dir: 游戏目录
game_version: 游戏版本
"""
debug_mode = self.ui_manager.debug_action.isChecked() if hasattr(self.ui_manager, 'debug_action') and self.ui_manager.debug_action else False
debug_mode = self.debug_manager._is_debug_mode()
# 确认卸载
reply = QMessageBox.question(
@@ -750,450 +614,6 @@ class MainWindow(QMainWindow):
return
# 开始卸载补丁
self.uninstall_patch(game_dir, game_version)
def identify_game_version(self, game_dir):
"""识别游戏版本
Args:
game_dir: 游戏目录路径
Returns:
str: 游戏版本名称如果不是有效的游戏目录则返回None
"""
import os
import re
# 调试模式
debug_mode = self.ui_manager.debug_action.isChecked() if hasattr(self.ui_manager, 'debug_action') and self.ui_manager.debug_action else False
if debug_mode:
print(f"DEBUG: 尝试识别游戏版本: {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:
print(f"DEBUG: 从目录名推测游戏版本: {potential_version}, 卷号: {vol_num}")
elif "after" in dir_name:
potential_version = "NEKOPARA After"
if debug_mode:
print(f"DEBUG: 从目录名推测游戏版本: NEKOPARA After")
# 检查是否为NEKOPARA游戏目录
# 通过检查游戏可执行文件来识别游戏版本
for game_version, info in 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:
print(f"DEBUG: 通过可执行文件确认游戏版本: {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:
print(f"DEBUG: 通过递归搜索确认游戏版本: {potential_version}, 文件: {file}")
return potential_version
# 如果仍然没有找到,基于目录名的推测返回结果
if potential_version:
if debug_mode:
print(f"DEBUG: 基于目录名返回推测的游戏版本: {potential_version}")
return potential_version
if debug_mode:
print(f"DEBUG: 无法识别游戏版本: {game_dir}")
return None
def identify_game_directories_improved(self, selected_folder):
"""改进的游戏目录识别,支持大小写不敏感和特殊字符处理
Args:
selected_folder: 选择的上级目录
Returns:
dict: 游戏版本到游戏目录的映射
"""
import os
import re
# 添加debug日志
debug_mode = self.ui_manager.debug_action.isChecked() if hasattr(self.ui_manager, 'debug_action') and self.ui_manager.debug_action else False
if debug_mode:
print(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:
print(f"DEBUG: 找到以下子目录: {all_dirs}")
except Exception as e:
if debug_mode:
print(f"DEBUG: 无法读取目录 {selected_folder}: {str(e)}")
return {}
for game, info in GAME_INFO.items():
expected_dir = info["install_path"].split("/")[0] # 例如 "NEKOPARA Vol. 1"
expected_exe = info["exe"] # 标准可执行文件名
if debug_mode:
print(f"DEBUG: 搜索游戏 {game}, 预期目录: {expected_dir}, 预期可执行文件: {expected_exe}")
# 尝试不同的匹配方法
found_dir = None
# 1. 精确匹配
if expected_dir in all_dirs:
found_dir = expected_dir
if debug_mode:
print(f"DEBUG: 精确匹配成功: {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:
print(f"DEBUG: 大小写不敏感匹配成功: {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:
print(f"DEBUG: 模糊匹配成功: {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:
print(f"DEBUG: 提取卷号: {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:
print(f"DEBUG: 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:
print(f"DEBUG: 卷号匹配成功: {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:
print(f"DEBUG: 验证成功,找到游戏可执行文件: {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 (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)):
exe_path = os.path.join(root, file)
exe_exists = True
found_exe = os.path.relpath(exe_path, potential_path)
if debug_mode:
print(f"DEBUG: 通过递归搜索找到游戏可执行文件: {found_exe}")
break
if exe_exists:
break
# 如果找到了可执行文件,将该目录添加到游戏目录列表
if exe_exists:
game_paths[game] = potential_path
if debug_mode:
print(f"DEBUG: 验证成功,将 {potential_path} 添加为 {game} 的目录")
else:
if debug_mode:
print(f"DEBUG: 未找到任何可执行文件变体,游戏 {game}{potential_path} 未找到")
if debug_mode:
print(f"DEBUG: 最终识别的游戏目录: {game_paths}")
print(f"--- 目录识别结束 ---")
return game_paths
def uninstall_patch(self, game_dir, game_version):
"""卸载补丁
Args:
game_dir: 游戏目录路径
game_version: 游戏版本
Returns:
bool: 卸载成功返回True失败返回False
"""
import os
import shutil
debug_mode = self.ui_manager.debug_action.isChecked() if hasattr(self.ui_manager, 'debug_action') and self.ui_manager.debug_action else False
if game_version not in GAME_INFO:
QMessageBox.critical(
self,
f"错误 - {APP_NAME}",
f"\n无法识别游戏版本: {game_version}\n",
QMessageBox.StandardButton.Ok,
)
return False
if debug_mode:
print(f"DEBUG: 开始卸载 {game_version} 补丁,目录: {game_dir}")
try:
files_removed = 0
# 获取可能的补丁文件路径
install_path_base = os.path.basename(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("_", "-"),
]
# 查找并删除补丁文件
patch_file_found = False
for patch_path in patch_files_to_check:
if os.path.exists(patch_path):
patch_file_found = True
os.remove(patch_path)
files_removed += 1
if debug_mode:
print(f"DEBUG: 已删除补丁文件: {patch_path}")
if not patch_file_found and debug_mode:
print(f"DEBUG: 未找到补丁文件,检查了以下路径: {patch_files_to_check}")
# 检查是否有额外的签名文件 (.sig)
if game_version == "NEKOPARA After":
for patch_path in patch_files_to_check:
sig_file_path = f"{patch_path}.sig"
if os.path.exists(sig_file_path):
os.remove(sig_file_path)
files_removed += 1
if debug_mode:
print(f"DEBUG: 已删除签名文件: {sig_file_path}")
# 删除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):
shutil.rmtree(patch_folder)
files_removed += 1
if debug_mode:
print(f"DEBUG: 已删除补丁文件夹: {patch_folder}")
# 删除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):
shutil.rmtree(game_patch_folder)
files_removed += 1
if debug_mode:
print(f"DEBUG: 已删除game/patch文件夹: {game_patch_folder}")
# 删除配置文件
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):
os.remove(config_path)
files_removed += 1
if debug_mode:
print(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):
os.remove(script_path)
files_removed += 1
if debug_mode:
print(f"DEBUG: 已删除脚本文件: {script_path}")
# 更新安装状态
self.installed_status[game_version] = False
# 在非批量卸载模式下显示卸载成功消息
if game_version != "all":
# 显示卸载成功消息
if files_removed > 0:
QMessageBox.information(
self,
f"卸载完成 - {APP_NAME}",
f"\n{game_version} 补丁卸载成功!\n共删除 {files_removed} 个文件/文件夹。\n",
QMessageBox.StandardButton.Ok,
)
else:
QMessageBox.warning(
self,
f"警告 - {APP_NAME}",
f"\n未找到 {game_version} 的补丁文件,可能未安装补丁或已被移除。\n",
QMessageBox.StandardButton.Ok,
)
# 卸载成功
return True
except Exception as e:
# 在非批量卸载模式下显示卸载失败消息
if game_version != "all":
# 显示卸载失败消息
error_message = f"\n卸载 {game_version} 补丁时出错:\n\n{str(e)}\n"
if debug_mode:
print(f"DEBUG: 卸载错误 - {str(e)}")
QMessageBox.critical(
self,
f"卸载失败 - {APP_NAME}",
error_message,
QMessageBox.StandardButton.Ok,
)
# 卸载失败
return False
self.patch_manager.uninstall_patch(game_dir, game_version)

View File

@@ -215,39 +215,48 @@ class Ui_MainWindows(object):
self.menu.setObjectName(u"menu")
self.menu.setTitle("设置")
self.menu.setFont(menu_font)
self.menu.setStyleSheet("""
QMenu {
# 创建菜单样式表,直接在样式表中指定字体族
menu_style = f"""
QMenu {{
background-color: #E96948;
color: white;
font-size: 16px;
font-family: "{font_family}";
font-size: 14px;
font-weight: bold;
border: 1px solid #F47A5B;
padding: 8px;
border-radius: 6px;
margin-top: 2px;
}
QMenu::item {
}}
QMenu::item {{
padding: 6px 20px 6px 15px;
background-color: transparent;
min-width: 120px;
color: white;
}
QMenu::item:selected {
font-family: "{font_family}";
font-weight: bold;
}}
QMenu::item:selected {{
background-color: #F47A5B;
border-radius: 4px;
}
QMenu::separator {
}}
QMenu::separator {{
height: 1px;
background-color: #F47A5B;
margin: 5px 15px;
}
""")
}}
QMenu::item:checked {{
background-color: #D25A3C;
border-radius: 4px;
}}
"""
self.menu.setStyleSheet(menu_style)
self.menu_2 = QMenu(self.content_container)
self.menu_2.setObjectName(u"menu_2")
self.menu_2.setTitle("帮助")
self.menu_2.setFont(menu_font)
self.menu_2.setStyleSheet(self.menu.styleSheet())
self.menu_2.setStyleSheet(menu_style)
# 连接按钮点击事件到显示对应菜单
self.settings_btn.clicked.connect(lambda: self.show_menu(self.menu, self.settings_btn))

5
source/ui/__init__.py Normal file
View File

@@ -0,0 +1,5 @@
from .Ui_install import Ui_MainWindows
__all__ = [
'Ui_MainWindows'
]

View File

@@ -1,4 +1,7 @@
from .helpers import censor_url
import logging
import os
from data.config import CACHE
class Logger:
def __init__(self, filename, stream):
@@ -16,4 +19,46 @@ class Logger:
self.log.flush()
def close(self):
self.log.close()
self.log.close()
def setup_logger(name):
"""设置并返回一个命名的logger
Args:
name: logger的名称
Returns:
logging.Logger: 配置好的logger对象
"""
# 创建logger
logger = logging.getLogger(name)
# 避免重复添加处理器
if logger.hasHandlers():
return logger
logger.setLevel(logging.DEBUG)
# 确保日志目录存在
log_dir = os.path.join(CACHE, "logs")
os.makedirs(log_dir, exist_ok=True)
log_file = os.path.join(log_dir, f"{name}.log")
# 创建文件处理器
file_handler = logging.FileHandler(log_file, encoding="utf-8")
file_handler.setLevel(logging.DEBUG)
# 创建控制台处理器
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
# 创建格式器并添加到处理器
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
file_handler.setFormatter(formatter)
console_handler.setFormatter(formatter)
# 添加处理器到logger
logger.addHandler(file_handler)
logger.addHandler(console_handler)
return logger

View File

@@ -5,9 +5,10 @@ import re
from urllib.parse import urlparse
from PySide6 import QtCore, QtWidgets
from PySide6.QtCore import (Qt, Signal, QThread, QTimer)
from PySide6.QtWidgets import (QLabel, QProgressBar, QVBoxLayout, QDialog)
from PySide6.QtWidgets import (QLabel, QProgressBar, QVBoxLayout, QDialog, QHBoxLayout)
from utils import resource_path
from data.config import APP_NAME, UA
import signal
# 下载线程类
class DownloadThread(QThread):
@@ -21,6 +22,8 @@ class DownloadThread(QThread):
self.game_version = game_version
self.process = None
self._is_running = True
self._is_paused = False
self.pause_process = None
def stop(self):
if self.process and self.process.poll() is None:
@@ -30,7 +33,55 @@ class DownloadThread(QThread):
subprocess.run(['taskkill', '/F', '/T', '/PID', str(self.process.pid)], check=True, creationflags=subprocess.CREATE_NO_WINDOW)
except (subprocess.CalledProcessError, FileNotFoundError) as e:
print(f"停止下载进程时出错: {e}")
def pause(self):
"""暂停下载进程"""
if not self._is_paused and self.process and self.process.poll() is None:
try:
# 使用SIGSTOP信号暂停进程
# Windows下使用不同的方式因为没有SIGSTOP
if sys.platform == 'win32':
self._is_paused = True
# 在Windows上使用暂停进程的方法
self.pause_process = subprocess.Popen(['powershell', '-Command', f'(Get-Process -Id {self.process.pid}).Suspend()'],
creationflags=subprocess.CREATE_NO_WINDOW)
else:
# 在Unix系统上使用SIGSTOP
os.kill(self.process.pid, signal.SIGSTOP)
self._is_paused = True
print(f"下载进程已暂停: PID {self.process.pid}")
return True
except (subprocess.CalledProcessError, FileNotFoundError, OSError) as e:
print(f"暂停下载进程时出错: {e}")
return False
return False
def resume(self):
"""恢复下载进程"""
if self._is_paused and self.process and self.process.poll() is None:
try:
# 使用SIGCONT信号恢复进程
# Windows下使用不同的方式
if sys.platform == 'win32':
self._is_paused = False
# 在Windows上使用恢复进程的方法
resume_process = subprocess.Popen(['powershell', '-Command', f'(Get-Process -Id {self.process.pid}).Resume()'],
creationflags=subprocess.CREATE_NO_WINDOW)
resume_process.wait()
else:
# 在Unix系统上使用SIGCONT
os.kill(self.process.pid, signal.SIGCONT)
self._is_paused = False
print(f"下载进程已恢复: PID {self.process.pid}")
return True
except (subprocess.CalledProcessError, FileNotFoundError, OSError) as e:
print(f"恢复下载进程时出错: {e}")
return False
return False
def is_paused(self):
"""返回当前下载是否处于暂停状态"""
return self._is_paused
def run(self):
try:
@@ -49,6 +100,7 @@ class DownloadThread(QThread):
aria2c_path,
]
# 将所有的优化参数应用于每个下载任务
command.extend([
'--dir', download_dir,
'--out', file_name,
@@ -74,8 +126,14 @@ class DownloadThread(QThread):
'--timeout=60',
'--auto-file-renaming=false',
'--allow-overwrite=true',
'--split=16',
'--max-connection-per-server=16'
# 优化参数 - 使用aria2允许的最佳设置
'--split=128', # 增加分片数到128
'--max-connection-per-server=16', # 最大允许值16
'--min-split-size=1M', # 减小最小分片大小
'--optimize-concurrent-downloads=true', # 优化并发下载
'--file-allocation=none', # 禁用文件预分配加快开始
'--async-dns=true', # 使用异步DNS
'--disable-ipv6=true' # 禁用IPv6提高速度
])
# 证书验证现在总是需要因为我们依赖hosts文件
@@ -91,7 +149,7 @@ class DownloadThread(QThread):
# 正则表达式用于解析aria2c的输出
# 例如: #1 GID[...]( 5%) CN:1 DL:10.5MiB/s ETA:1m30s
progress_pattern = re.compile(r'\((\d{1,3})%\).*?CN:(\d+).*?DL:\s*([^\s]+).*?ETA:\s*([^\s]+)')
progress_pattern = re.compile(r'\((\d{1,3})%\).*?CN:(\d+).*?DL:\s*([^\s]+).*?ETA:\s*([^\s\]]+)')
full_output = []
while self._is_running and self.process.poll() is None:
@@ -156,13 +214,42 @@ class ProgressWindow(QDialog):
self.progress_bar = QProgressBar()
self.progress_bar.setValue(0)
self.stats_label = QLabel("速度: - | 线程: - | 剩余时间: -")
self.stop_button = QtWidgets.QPushButton("停止下载")
# 创建按钮布局
button_layout = QHBoxLayout()
# 创建暂停/恢复按钮
self.pause_resume_button = QtWidgets.QPushButton("暂停下载")
self.pause_resume_button.setToolTip("暂停或恢复下载")
# 创建停止按钮
self.stop_button = QtWidgets.QPushButton("取消下载")
self.stop_button.setToolTip("取消整个下载过程")
# 添加按钮到按钮布局
button_layout.addWidget(self.pause_resume_button)
button_layout.addWidget(self.stop_button)
layout.addWidget(self.game_label)
layout.addWidget(self.progress_bar)
layout.addWidget(self.stats_label)
layout.addWidget(self.stop_button)
layout.addLayout(button_layout)
self.setLayout(layout)
# 设置暂停/恢复状态
self.is_paused = False
def update_pause_button_state(self, is_paused):
"""更新暂停按钮的显示状态
Args:
is_paused: 是否处于暂停状态
"""
self.is_paused = is_paused
if is_paused:
self.pause_resume_button.setText("恢复下载")
else:
self.pause_resume_button.setText("暂停下载")
def update_progress(self, data):
game_version = data.get("game", "未知游戏")
@@ -170,12 +257,17 @@ class ProgressWindow(QDialog):
speed = data.get("speed", "-")
threads = data.get("threads", "-")
eta = data.get("eta", "-")
# 清除ETA值中可能存在的"]"符号
if isinstance(eta, str):
eta = eta.replace("]", "")
self.game_label.setText(f"正在下载: {game_version}")
self.game_label.setText(f"正在下载 {game_version} 的补丁")
self.progress_bar.setValue(int(percent))
self.stats_label.setText(f"速度: {speed} | 线程: {threads} | 剩余时间: {eta}")
if percent == 100:
self.pause_resume_button.setEnabled(False)
self.stop_button.setEnabled(False)
self.stop_button.setText("下载完成")
QTimer.singleShot(1500, self.accept)