19 Commits

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
hyb-oyqq
331f7a25d2 refactor(ui): 重构 UI 相关代码并移除冗余资源
- 从 ui_manager.py 和 install.ui 中移除了使用 base64 图片数据的方式
- 采用直接加载图片文件的方法,提高了代码的可读性和维护性
- 删除了未使用的 popup.ui 文件
- 优化了资源路径的获取方式,使用 resource_path 函数统一处理
2025-07-28 15:22:31 +08:00
hyb-oyqq
642b2ec17f feat(main_window): 添加批量卸载游戏补丁功能
- 在卸载补丁界面添加"全部卸载"选项
- 实现批量卸载所有游戏补丁的功能
- 优化单个游戏卸载流程,统一卸载成功和失败的返回值
- 调整卸载消息提示,适应批量卸载和单个卸载的不同场景
2025-07-28 13:09:43 +08:00
hyb-oyqq
41aab89669 feat(core): 优化游戏目录识别和补丁卸载流程
- 改进游戏目录识别算法,支持大小写不敏感和特殊字符处理
- 增加递归搜索可执行文件的功能,提高识别准确率
- 优化补丁卸载流程,支持更灵活的文件路径和名称
- 增加调试模式下的日志输出,便于问题排查
- 重构部分代码结构,提高可维护性和可扩展性
2025-07-28 11:54:52 +08:00
hyb-oyqq
f6a57215c2 refactor(source): 优化窗口大小和背景图显示
- 设置窗口最小和最大尺寸
- 优化背景图加载和显示,使用setScaledContents简化处理
- 调整标题栏和菜单区域的大小和位置
- 重构部分UI代码,提高可读性和维护性
2025-07-25 17:21:30 +08:00
hyb-oyqq
38549e098e feat(core): 添加卸载补丁功能并优化用户界面
- 在主界面添加卸载补丁按钮,实现卸载功能
- 优化菜单区域,使用按钮替代传统菜单栏
- 更新主题颜色,调整按钮布局和样式
- 优化帮助和设置菜单,提升用户体验
2025-07-25 17:00:55 +08:00
hyb-oyqq
286270a819 feat(core): 优化窗口动画和布局
- 新增按钮点击动画效果
- 动态调整窗口大小和布局
- 实现圆角窗口和拖动功能
- 优化安装按钮状态管理
- 更新菜单动画逻辑
- 为2.0新窗口做预实现
2025-07-25 15:18:05 +08:00
hyb-oyqq
0f9c91b59a docs(README): 更新文档状态说明
- 中英文 README 文件中的文档状态说明已更新
2025-07-24 18:24:58 +08:00
hyb-oyqq
3753375bed feat(core): 重构按钮显示和动画
- 更新动画逻辑,使用新的按钮容器
- 优化按钮点击区域和视觉效果
- 添加了得意黑作为字体和样式
2025-07-24 17:43:11 +08:00
hyb-oyqq
dab2ba2dc5 feat(core): 优化安装结果显示和下载管理
- 优化 Cloudflare 加速提示信息格式
- 增加下载历史记录列表,跟踪本次安装的游戏
- 改进安装结果显示,区分不同情况
- 优化哈希检查提示信息
2025-07-24 16:57:16 +08:00
hyb-oyqq
f86cb7aa7e refactor(core): 优化消息框显示和下载流程
- 修改 hash_pop_window 函数,增加检查类型参数
- 优化 Cloudflare 优选失败和成功消息框显示
- 调整下载前后检查的消息框内容
- 改进解压缩后的文件检查流程
2025-07-24 16:29:30 +08:00
hyb-oyqq
0cf9f5e6c2 feat(core): 优化 Cloudflare IP 加速功能
- 在消息框中添加 Cloudflare 图标
- 更新应用版本号至 1.1.3
- 优化配置获取流程,增加错误处理
- 移除未使用的资源文件
- 调整资源路径获取逻辑
2025-07-24 15:14:29 +08:00
hyb-oyqq
f9715f91f7 build: 更新应用版本号 2025-07-24 11:21:52 +08:00
hyb-oyqq
98e51d443e perf(ip_optimizer): 优化 IP 优选逻辑
- 修改 speedtest-cli 命令参数,避免写入结果文件
- 修复最优 IP 查找逻辑,确保只保存第一个匹配的 IP
- 移除不必要的循环退出条件,简化代码逻辑
2025-07-24 11:20:56 +08:00
56 changed files with 3657 additions and 699 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,7 +1,7 @@
# 🍓FRAISEMOE-Addons-Installer-NEXT🍓
```
🔊 Note: This repository is still under active development, and most of the documentation is not yet available. We appreciate your understanding.
🔊 Note: This repository's documentation updates have stabilized. If there are any missing parts, please promptly raise an issue. Thank you all for your support!
The English version is not updated in real-time! Please check the Simplified Chinese version for more updates! Thank you for your support!
```

View File

@@ -1,7 +1,7 @@
# 🍓FRAISEMOE-Addons-Installer-NEXT🍓
```
🔊 注意:本库仍然努力更新中,大部分文档不可用,敬请谅解。
🔊 注意:本库文档更新已趋于稳定如有遗漏部分请及时提出issue感谢各位支持
```
<!-- PROJECT SHIELDS -->

BIN
source/IMG/BG/title_bg1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

BIN
source/IMG/BG/title_bg2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

BIN
source/IMG/BTN/Button.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

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

@@ -1,15 +1,24 @@
import sys
from PySide6.QtCore import (QObject, QPropertyAnimation, QParallelAnimationGroup,
QPoint, QEasingCurve, QTimer, Signal)
from PySide6.QtWidgets import QGraphicsOpacityEffect
QPoint, QEasingCurve, QTimer, Signal, QRect)
from PySide6.QtWidgets import QGraphicsOpacityEffect, QPushButton
from PySide6.QtGui import QColor
class MultiStageAnimations(QObject):
animation_finished = Signal()
def __init__(self, ui, parent=None):
super().__init__(parent)
self.ui = ui
# 获取画布尺寸
self.canvas_width = ui.centralwidget.width()
self.canvas_height = ui.centralwidget.height()
self.parent = parent # 保存父窗口引用以获取当前尺寸
# 获取画布尺寸 - 动态从父窗口获取
if parent:
self.canvas_width = parent.width()
self.canvas_height = parent.height()
else:
# 默认尺寸
self.canvas_width = 1280
self.canvas_height = 720
# 动画时序配置
self.animation_config = {
@@ -18,29 +27,141 @@ class MultiStageAnimations(QObject):
},
"mainbg": {
"delay_after": 500
},
"button_click": {
"scale_duration": 100,
"scale_min": 0.95,
"scale_max": 1.0
}
}
# 第一阶段Logo动画配置
# 第一阶段Logo动画配置根据新布局调整Y坐标
self.logo_widgets = [
{"widget": ui.vol1bg, "delay": 0, "duration": 500, "end_pos": QPoint(0, 120)},
{"widget": ui.vol2bg, "delay": 80, "duration": 500, "end_pos": QPoint(0, 180)},
{"widget": ui.vol3bg, "delay": 160, "duration": 500, "end_pos": QPoint(0, 240)},
{"widget": ui.vol4bg, "delay": 240, "duration": 500, "end_pos": QPoint(0, 300)},
{"widget": ui.afterbg, "delay": 320, "duration": 500, "end_pos": QPoint(0, 360)}
{"widget": ui.vol1bg, "delay": 0, "duration": 500, "end_pos": QPoint(0, 150)},
{"widget": ui.vol2bg, "delay": 80, "duration": 500, "end_pos": QPoint(0, 210)},
{"widget": ui.vol3bg, "delay": 160, "duration": 500, "end_pos": QPoint(0, 270)},
{"widget": ui.vol4bg, "delay": 240, "duration": 500, "end_pos": QPoint(0, 330)},
{"widget": ui.afterbg, "delay": 320, "duration": 500, "end_pos": QPoint(0, 390)}
]
# 第二阶段:菜单元素
# 第二阶段:菜单元素,位置会在开始动画时动态计算
self.menu_widgets = [
{"widget": ui.menubg, "end_pos": QPoint(710, 0), "duration": 600},
{"widget": ui.start_install_btn, "end_pos": QPoint(780, 250), "duration": 600},
{"widget": ui.exit_btn, "end_pos": QPoint(780, 340), "duration": 600}
# 移除菜单背景动画
# {"widget": ui.menubg, "end_pos": QPoint(720, 55), "duration": 600},
{"widget": ui.button_container, "end_pos": None, "duration": 600},
{"widget": ui.uninstall_container, "end_pos": None, "duration": 600}, # 添加卸载补丁按钮
{"widget": ui.exit_container, "end_pos": None, "duration": 600}
]
self.animations = []
self.timers = []
# 设置按钮点击动画
self.setup_button_click_animations()
def setup_button_click_animations(self):
"""设置按钮点击动画"""
# 为开始安装按钮添加点击动画
self.ui.start_install_btn.pressed.connect(
lambda: self.start_button_click_animation(self.ui.button_container)
)
self.ui.start_install_btn.released.connect(
lambda: self.end_button_click_animation(self.ui.button_container)
)
# 为卸载补丁按钮添加点击动画
self.ui.uninstall_btn.pressed.connect(
lambda: self.start_button_click_animation(self.ui.uninstall_container)
)
self.ui.uninstall_btn.released.connect(
lambda: self.end_button_click_animation(self.ui.uninstall_container)
)
# 为退出按钮添加点击动画
self.ui.exit_btn.pressed.connect(
lambda: self.start_button_click_animation(self.ui.exit_container)
)
self.ui.exit_btn.released.connect(
lambda: self.end_button_click_animation(self.ui.exit_container)
)
def start_button_click_animation(self, button_container):
"""开始按钮点击动画"""
# 创建缩放动画
scale_anim = QPropertyAnimation(button_container.children()[0], b"geometry") # 只对按钮背景应用动画
scale_anim.setDuration(self.animation_config["button_click"]["scale_duration"])
# 获取当前几何形状
current_geometry = button_container.children()[0].geometry()
# 计算缩放后的几何形状(保持中心点不变)
scale_factor = self.animation_config["button_click"]["scale_min"]
width_diff = current_geometry.width() * (1 - scale_factor) / 2
height_diff = current_geometry.height() * (1 - scale_factor) / 2
new_geometry = QRect(
current_geometry.x() + width_diff,
current_geometry.y() + height_diff,
current_geometry.width() * scale_factor,
current_geometry.height() * scale_factor
)
scale_anim.setEndValue(new_geometry)
scale_anim.setEasingCurve(QEasingCurve.Type.OutQuad)
# 启动动画
scale_anim.start()
self.animations.append(scale_anim)
# 对文本标签也应用同样的动画
text_anim = QPropertyAnimation(button_container.children()[1], b"geometry")
text_anim.setDuration(self.animation_config["button_click"]["scale_duration"])
text_geometry = button_container.children()[1].geometry()
new_text_geometry = QRect(
text_geometry.x() + width_diff,
text_geometry.y() + height_diff,
text_geometry.width() * scale_factor,
text_geometry.height() * scale_factor
)
text_anim.setEndValue(new_text_geometry)
text_anim.setEasingCurve(QEasingCurve.Type.OutQuad)
text_anim.start()
self.animations.append(text_anim)
def end_button_click_animation(self, button_container):
"""结束按钮点击动画,恢复正常外观"""
# 创建恢复动画 - 对背景
scale_anim = QPropertyAnimation(button_container.children()[0], b"geometry")
scale_anim.setDuration(self.animation_config["button_click"]["scale_duration"])
# 恢复到原始大小 (10,10,191,91)
original_geometry = QRect(10, 10, 191, 91)
scale_anim.setEndValue(original_geometry)
scale_anim.setEasingCurve(QEasingCurve.Type.OutElastic)
# 启动动画
scale_anim.start()
self.animations.append(scale_anim)
# 恢复文本标签
text_anim = QPropertyAnimation(button_container.children()[1], b"geometry")
text_anim.setDuration(self.animation_config["button_click"]["scale_duration"])
# 恢复文本到原始大小 (10,7,191,91)
text_anim.setEndValue(QRect(10, 7, 191, 91))
text_anim.setEasingCurve(QEasingCurve.Type.OutElastic)
text_anim.start()
self.animations.append(text_anim)
def initialize(self):
"""初始化所有组件状态"""
# 更新画布尺寸
if self.parent:
self.canvas_width = self.parent.width()
self.canvas_height = self.parent.height()
# 设置Mainbg初始状态
effect = QGraphicsOpacityEffect(self.ui.Mainbg)
effect.setOpacity(0)
@@ -125,6 +246,12 @@ class MultiStageAnimations(QObject):
self.animations.append(main_anim)
def start_menu_animations(self):
"""启动菜单动画(从下往上)"""
# 更新按钮最终位置
self._update_button_positions()
# 跟踪最后一个动画用于连接finished信号
last_anim = None
for item in self.menu_widgets:
anim_group = QParallelAnimationGroup()
@@ -144,11 +271,69 @@ class MultiStageAnimations(QObject):
anim_group.addAnimation(pos_anim)
anim_group.addAnimation(opacity_anim)
if item["widget"] == self.ui.exit_btn:
anim_group.finished.connect(self.animation_finished.emit)
# 记录最后一个按钮的动画
if item["widget"] == self.ui.exit_container:
last_anim = anim_group
anim_group.start()
self.animations.append(anim_group)
# 在最后一个动画完成时发出信号
if last_anim:
last_anim.finished.connect(self.animation_finished.emit)
def _update_button_positions(self):
"""更新按钮最终位置"""
# 根据当前窗口大小动态计算按钮位置
if self.parent:
width = self.parent.width()
height = self.parent.height()
# 计算按钮位置
right_margin = 20 # 减小右边距,使按钮更靠右
# 开始安装按钮
if hasattr(self.ui, 'button_container'):
btn_width = self.ui.button_container.width()
x_pos = width - btn_width - right_margin
y_pos = int((height - 65) * 0.28) - 10 # 与resizeEvent中保持一致
# 更新动画目标位置
for item in self.menu_widgets:
if item["widget"] == self.ui.button_container:
item["end_pos"] = QPoint(x_pos, y_pos)
# 卸载补丁按钮
if hasattr(self.ui, 'uninstall_container'):
btn_width = self.ui.uninstall_container.width()
x_pos = width - btn_width - right_margin
y_pos = int((height - 65) * 0.46) - 10 # 与resizeEvent中保持一致
# 更新动画目标位置
for item in self.menu_widgets:
if item["widget"] == self.ui.uninstall_container:
item["end_pos"] = QPoint(x_pos, y_pos)
# 退出按钮
if hasattr(self.ui, 'exit_container'):
btn_width = self.ui.exit_container.width()
x_pos = width - btn_width - right_margin
y_pos = int((height - 65) * 0.64) - 10 # 与resizeEvent中保持一致
# 更新动画目标位置
for item in self.menu_widgets:
if item["widget"] == self.ui.exit_container:
item["end_pos"] = QPoint(x_pos, y_pos)
else:
# 默认位置
for item in self.menu_widgets:
if item["widget"] == self.ui.button_container:
item["end_pos"] = QPoint(1050, 200)
elif item["widget"] == self.ui.uninstall_container:
item["end_pos"] = QPoint(1050, 310)
elif item["widget"] == self.ui.exit_container:
item["end_pos"] = QPoint(1050, 420)
def start_animations(self):
"""启动完整动画序列"""
self.clear_animations()

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

@@ -3,11 +3,13 @@ import requests
import json
from collections import deque
from urllib.parse import urlparse
import re # Added for recursive search
from PySide6 import QtWidgets
from PySide6 import QtWidgets, QtCore
from PySide6.QtCore import Qt
from PySide6.QtGui import QIcon, QPixmap
from utils import msgbox_frame, HostsManager
from utils import msgbox_frame, HostsManager, resource_path
from data.config import APP_NAME, PLUGIN, GAME_INFO, UA, CONFIG_URL
from workers import IpOptimizerThread
@@ -37,14 +39,40 @@ 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):
"""获取所有游戏版本的安装路径"""
return {
game: os.path.join(self.selected_folder, info["install_path"])
for game, info in GAME_INFO.items()
}
# 使用改进的目录识别功能
game_dirs = self.main_window.game_detector.identify_game_directories_improved(self.selected_folder)
install_paths = {}
debug_mode = self.is_debug_mode()
for game, info in GAME_INFO.items():
if game in game_dirs:
# 如果找到了游戏目录,使用它
game_dir = game_dirs[game]
install_path = os.path.join(game_dir, os.path.basename(info["install_path"]))
install_paths[game] = install_path
if debug_mode:
print(f"DEBUG: 使用识别到的游戏目录 {game}: {game_dir}")
print(f"DEBUG: 安装路径设置为: {install_path}")
else:
# 回退到原始路径计算方式
install_path = os.path.join(self.selected_folder, info["install_path"])
install_paths[game] = install_path
if debug_mode:
print(f"DEBUG: 未识别到游戏目录 {game}, 使用默认路径: {install_path}")
return install_paths
def is_debug_mode(self):
"""检查是否处于调试模式"""
@@ -138,29 +166,151 @@ class DownloadManager:
def download_action(self):
"""开始下载流程"""
# 禁用开始安装按钮
self.main_window.ui.start_install_btn.setEnabled(False)
# 主窗口在file_dialog中已被禁用
# 清空下载历史记录
self.main_window.download_queue_history = []
# 使用改进的目录识别功能
game_dirs = self.main_window.game_detector.identify_game_directories_improved(self.selected_folder)
debug_mode = self.is_debug_mode()
if debug_mode:
print(f"DEBUG: 开始下载流程, 识别到 {len(game_dirs)} 个游戏目录")
# 检查是否找到任何游戏目录
if not game_dirs:
if debug_mode:
print("DEBUG: 未识别到任何游戏目录,设置目录未找到错误")
# 设置特定的错误类型,以便在按钮点击处理中区分处理
self.main_window.last_error_message = "directory_not_found"
QtWidgets.QMessageBox.warning(
self.main_window,
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()
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()
@@ -168,98 +318,196 @@ class DownloadManager:
QtWidgets.QMessageBox.critical(
self.main_window, f"错误 - {APP_NAME}", "\n网络状态异常或服务器故障,请重试\n"
)
# 重新启用开始安装按钮
self.main_window.ui.start_install_btn.setEnabled(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文件并需要管理员权限。")
msg_box.setIcon(QtWidgets.QMessageBox.Icon.Question)
msg_box.setText("是否愿意通过Cloudflare加速来优化下载速度\n\n这将临时修改系统的hosts文件并需要管理员权限。\n如您的杀毒软件提醒有软件正在修改hosts文件请注意放行。")
# 设置Cloudflare图标
cf_icon_path = resource_path("IMG/ICO/cloudflare_logo_icon.ico")
if os.path.exists(cf_icon_path):
cf_pixmap = QPixmap(cf_icon_path)
if not cf_pixmap.isNull():
msg_box.setWindowIcon(QIcon(cf_pixmap))
msg_box.setIconPixmap(cf_pixmap.scaled(64, 64, Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation))
else:
msg_box.setIcon(QtWidgets.QMessageBox.Icon.Question)
yes_button = msg_box.addButton("是,开启加速", QtWidgets.QMessageBox.ButtonRole.YesRole)
no_button = msg_box.addButton("否,直接下载", QtWidgets.QMessageBox.ButtonRole.NoRole)
cancel_button = msg_box.addButton("取消安装", QtWidgets.QMessageBox.ButtonRole.RejectRole)
msg_box.exec()
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()
# 添加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
game_folder = os.path.join(self.selected_folder, f"NEKOPARA Vol. {i}")
_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))
# 添加nekopara after
game_version = "NEKOPARA After"
if not self.main_window.installed_status.get(game_version, False):
url = config.get("after")
if url:
game_folder = os.path.join(self.selected_folder, "NEKOPARA After")
_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))
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分钟请耐心等待喵~"
)
# 我们不再提供"跳过"按钮
self.optimizing_msg_box.setStandardButtons(QtWidgets.QMessageBox.StandardButton.NoButton)
# 设置Cloudflare图标
cf_icon_path = resource_path("IMG/ICO/cloudflare_logo_icon.ico")
if os.path.exists(cf_icon_path):
cf_pixmap = QPixmap(cf_icon_path)
if not cf_pixmap.isNull():
self.optimizing_msg_box.setWindowIcon(QIcon(cf_pixmap))
self.optimizing_msg_box.setIconPixmap(cf_pixmap.scaled(64, 64, Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation))
# 添加取消按钮
self.optimizing_msg_box.setStandardButtons(QtWidgets.QMessageBox.StandardButton.Cancel)
self.optimizing_msg_box.buttonClicked.connect(self._on_optimization_dialog_clicked)
self.optimizing_msg_box.setWindowModality(Qt.WindowModality.ApplicationModal)
self.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
@@ -271,11 +519,48 @@ class DownloadManager:
# 显示优选结果
if not ip:
QtWidgets.QMessageBox.warning(
self.main_window,
f"优选失败 - {APP_NAME}",
"\n未能找到合适的Cloudflare IP将使用默认网络进行下载。\n"
)
# 临时启用窗口以显示对话框
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
timer = QtCore.QTimer(self.main_window)
def update_countdown():
nonlocal countdown
countdown -= 1
ok_button.setText(f"确定 ({countdown})")
if countdown <= 0:
timer.stop()
if msg_box.isVisible():
msg_box.accept()
timer.timeout.connect(update_countdown)
timer.start(1000) # 每秒更新一次
# 显示对话框并等待用户响应
result = msg_box.exec()
# 停止计时器
timer.stop()
# 如果用户点击了取消安装
if 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:
@@ -285,21 +570,64 @@ class DownloadManager:
# 先清理可能存在的旧记录
self.hosts_manager.clean_hostname_entries(hostname)
# 临时启用窗口以显示对话框
self.main_window.setEnabled(True)
if self.hosts_manager.apply_ip(hostname, ip):
QtWidgets.QMessageBox.information(
self.main_window,
f"成功 - {APP_NAME}",
f"\n已将优选IP ({ip}) 应用到hosts文件。\n"
)
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
timer = QtCore.QTimer(self.main_window)
def update_countdown():
nonlocal countdown
countdown -= 1
ok_button.setText(f"确定 ({countdown})")
if countdown <= 0:
timer.stop()
if msg_box.isVisible():
msg_box.accept()
timer.timeout.connect(update_countdown)
timer.start(1000) # 每秒更新一次
# 显示对话框并等待用户响应
result = msg_box.exec()
# 停止计时器
timer.stop()
# 如果用户点击了取消安装
if 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)
# 开始下载
self.next_download_task()
# 计时器结束或用户点击确定时,继续下载
QtCore.QTimer.singleShot(100, self.next_download_task)
def next_download_task(self):
"""处理下载队列中的下一个任务"""
@@ -325,21 +653,109 @@ class DownloadManager:
_7z_path: 7z文件保存路径
plugin_path: 插件路径
"""
game_exe = {
game: os.path.join(
self.selected_folder, info["install_path"].split("/")[0], info["exe"]
# 使用改进的目录识别获取安装路径
install_paths = self.get_install_paths()
debug_mode = self.is_debug_mode()
if debug_mode:
print(f"DEBUG: 准备下载游戏 {game_version}")
print(f"DEBUG: 游戏文件夹: {game_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:
game_dir = game_dirs[game_version]
# 游戏目录已经通过可执行文件验证了,可以直接认为存在
game_exe_exists = True
if debug_mode:
print(f"DEBUG: 游戏目录已验证: {game_dir}")
print(f"DEBUG: 游戏可执行文件存在: {game_exe_exists}")
else:
# 回退到传统方法检查游戏是否存在
# 尝试多种可能的文件名格式
expected_exe = GAME_INFO[game_version]["exe"]
traditional_folder = os.path.join(
self.selected_folder,
GAME_INFO[game_version]["install_path"].split("/")[0]
)
for game, info in GAME_INFO.items()
}
# 定义多种可能的可执行文件变体
exe_variants = [
expected_exe, # 标准文件名
expected_exe + ".nocrack", # Steam加密版本
expected_exe.replace(".exe", ""), # 无扩展名版本
expected_exe.replace("NEKOPARA", "nekopara").lower(), # 全小写变体
expected_exe.lower(), # 小写变体
expected_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(traditional_folder, exe_variant)
if os.path.exists(exe_path):
game_exe_exists = True
if debug_mode:
print(f"DEBUG: 找到游戏可执行文件: {exe_path}")
break
# 如果仍未找到,尝试递归搜索
if not game_exe_exists and os.path.exists(traditional_folder):
# 提取卷号或检查是否是After
vol_match = re.search(r"Vol\.(\d+)", game_version)
vol_num = None
if vol_match:
vol_num = vol_match.group(1)
is_after = "After" in game_version
# 遍历游戏目录及其子目录
for root, dirs, files in os.walk(traditional_folder):
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)):
game_exe_exists = True
if debug_mode:
print(f"DEBUG: 通过递归搜索找到游戏可执行文件: {os.path.join(root, file)}")
break
if game_exe_exists:
break
if debug_mode:
print(f"DEBUG: 使用传统方法检查游戏目录: {traditional_folder}")
print(f"DEBUG: 游戏可执行文件存在: {game_exe_exists}")
# 检查游戏是否已安装
if (
game_version not in game_exe
or not os.path.exists(game_exe[game_version])
not game_exe_exists
or self.main_window.installed_status[game_version]
):
self.main_window.installed_status[game_version] = False
self.main_window.show_result()
if debug_mode:
print(f"DEBUG: 跳过下载游戏 {game_version}")
print(f"DEBUG: 游戏存在: {game_exe_exists}")
print(f"DEBUG: 已安装补丁: {self.main_window.installed_status[game_version]}")
self.main_window.installed_status[game_version] = False if not game_exe_exists else True
self.next_download_task()
return
# 创建进度窗口并开始下载
@@ -356,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}")
@@ -379,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):
"""下载完成后的处理
@@ -399,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是否重试?")
@@ -420,15 +862,20 @@ 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
# 下载成功,开始解压缩
self.main_window.hash_msg_box = self.main_window.hash_manager.hash_pop_window()
self.main_window.hash_msg_box = self.main_window.hash_manager.hash_pop_window(check_type="extraction")
# 创建并启动解压线程
self.main_window.extraction_thread = self.main_window.create_extraction_thread(
@@ -448,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
@@ -479,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.ui.start_install_btn.setEnabled(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,11 +1,11 @@
from PySide6.QtGui import QIcon, QAction
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 data.pic_data import img_data
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):
@@ -18,42 +18,283 @@ 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元素包括窗口图标、标题和菜单"""
# 设置窗口图标
icon_data = img_data.get("icon")
if icon_data:
pixmap = load_base64_image(icon_data)
self.main_window.setWindowIcon(QIcon(pixmap))
import os
from utils import resource_path
icon_path = resource_path(os.path.join("IMG", "ICO", "icon.png"))
if os.path.exists(icon_path):
self.main_window.setWindowIcon(QIcon(icon_path))
# 设置窗口标题
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)
# 获取菜单字体
menu_font = self._get_menu_font()
# 创建菜单项 - 移除"项目主页",添加"常见问题"和"提交错误"
faq_action = QAction("常见问题", self.main_window)
faq_action.triggered.connect(self.open_faq_page)
faq_action.setFont(menu_font)
report_issue_action = QAction("提交错误", self.main_window)
report_issue_action.triggered.connect(self.open_issues_page)
report_issue_action.setFont(menu_font)
# 清除现有菜单项并添加新的菜单项
self.ui.menu_2.clear()
self.ui.menu_2.addAction(faq_action)
self.ui.menu_2.addAction(report_issue_action)
# 连接按钮点击事件,如果使用按钮式菜单
if hasattr(self.ui, 'help_btn'):
# 按钮已经连接到显示菜单,不需要额外处理
pass
def _setup_about_menu(self):
"""设置"关于"菜单"""
# 获取菜单字体
menu_font = self._get_menu_font()
# 创建关于菜单
self.about_menu = QMenu("关于", self.main_window)
self.about_menu.setFont(menu_font)
# 设置菜单样式
font_family = menu_font.family()
menu_style = self._get_menu_style(font_family)
self.about_menu.setStyleSheet(menu_style)
# 创建菜单项
about_project_action = QAction("关于本项目", self.main_window)
about_project_action.setFont(menu_font)
about_project_action.triggered.connect(self.show_about_dialog)
# 添加项目主页选项(从帮助菜单移动过来)
project_home_action = QAction("Github项目主页", self.main_window)
project_home_action.setFont(menu_font)
project_home_action.triggered.connect(self.open_project_home_page)
about_action = QAction("关于", self.main_window)
about_action.triggered.connect(self.show_about_dialog)
# 添加加入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.ui.menu_2.addAction(project_home_action)
self.ui.menu_2.addAction(about_action)
# 创建隐私协议菜单
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', {})
@@ -66,24 +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) # 添加开发者选项子菜单
# 添加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.2",
"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, "无法读取本地隐私协议文件"

Binary file not shown.

View File

@@ -2,10 +2,13 @@ import os
import sys
import shutil
import json
import webbrowser
from PySide6 import QtWidgets
from PySide6.QtCore import QTimer
from PySide6.QtWidgets import QMainWindow, QMessageBox
from PySide6.QtCore import QTimer, Qt, QPoint, QRect, QSize
from PySide6.QtWidgets import QMainWindow, QMessageBox, QGraphicsOpacityEffect, QGraphicsColorizeEffect
from PySide6.QtGui import QPalette, QColor, QPainterPath, QRegion
from PySide6.QtGui import QAction # Added for menu actions
from ui.Ui_install import Ui_MainWindows
from data.config import (
@@ -13,20 +16,32 @@ from data.config import (
PLUGIN_HASH, UA, CONFIG_URL, LOG_FILE
)
from utils import (
load_config, save_config, HashManager, AdminPrivileges, msgbox_frame
load_config, save_config, HashManager, AdminPrivileges, msgbox_frame, load_image_from_file
)
from workers import (
DownloadThread, ProgressWindow, IpOptimizerThread,
HashThread, ExtractionThread, ConfigFetchThread
)
from core import (
MultiStageAnimations, UIManager, DownloadManager, DebugManager
MultiStageAnimations, UIManager, DownloadManager, DebugManager,
WindowManager, GameDetector, PatchManager, ConfigManager
)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
# 设置窗口为无边框
self.setWindowFlags(Qt.WindowType.FramelessWindowHint)
# 设置窗口背景透明
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
# 调整窗口大小以适应背景图片比例 (1280x720)
self.resize(1280, 720)
# 设置固定尺寸范围
self.setMinimumSize(QSize(1024, 576))
self.setMaximumSize(QSize(1280, 720))
# 初始化UI (从Ui_install.py导入)
self.ui = Ui_MainWindows()
self.ui.setupUi(self)
@@ -34,13 +49,6 @@ class MainWindow(QMainWindow):
# 初始化配置
self.config = load_config()
# 初始化状态变量
self.cloud_config = None
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()
@@ -52,13 +60,32 @@ 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)
# 设置退出按钮和开始安装按钮的样式表,使其在禁用状态下不会变灰
button_style = "QPushButton:disabled { opacity: 1.0; }"
self.ui.exit_btn.setStyleSheet(button_style)
self.ui.start_install_btn.setStyleSheet(button_style)
# 初始化状态变量
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)
if hasattr(self.ui, 'minimize_btn'):
self.ui.minimize_btn.clicked.connect(self.showMinimized)
# 检查管理员权限和进程
self.admin_privileges.request_admin_privileges()
@@ -79,10 +106,15 @@ class MainWindow(QMainWindow):
)
sys.exit(1)
# 连接信号
self.ui.start_install_btn.clicked.connect(self.download_manager.file_dialog)
# 连接信号 - 绑定到新按钮
self.ui.start_install_btn.clicked.connect(self.handle_install_button_click)
self.ui.uninstall_btn.clicked.connect(self.handle_uninstall_button_click) # 添加卸载补丁按钮事件连接
self.ui.exit_btn.clicked.connect(self.shutdown_app)
# 初始化按钮状态标记
self.install_button_enabled = False
self.last_error_message = ""
# 根据初始配置决定是否开启Debug模式
if hasattr(self.ui_manager, 'debug_action') and self.ui_manager.debug_action:
if self.ui_manager.debug_action.isChecked():
@@ -94,33 +126,72 @@ class MainWindow(QMainWindow):
# 窗口显示后延迟100ms启动动画
QTimer.singleShot(100, self.start_animations)
# 窗口事件处理 - 委托给WindowManager
def mousePressEvent(self, event):
self.window_manager.handle_mouse_press(event)
def mouseMoveEvent(self, event):
self.window_manager.handle_mouse_move(event)
def mouseReleaseEvent(self, event):
self.window_manager.handle_mouse_release(event)
def resizeEvent(self, event):
self.window_manager.handle_resize(event)
super().resizeEvent(event)
def start_animations(self):
"""开始启动动画"""
# 不再禁用退出按钮的交互性,只通过样式表控制外观
# 但仍然需要跟踪动画状态,防止用户在动画播放过程中退出
self.animation_in_progress = True
# 禁用开始安装按钮,防止在动画播放期间点击
self.ui.start_install_btn.setEnabled(False)
# 按钮容器初始是隐藏的,无需在这里禁用
# 但确保开始安装按钮仍然处于禁用状态
self.set_start_button_enabled(False)
self.animator.animation_finished.connect(self.on_animations_finished)
self.animator.start_animations()
# 在动画开始时获取云端配置
self.fetch_cloud_config()
def on_animations_finished(self):
"""动画完成后启用按钮"""
self.animation_in_progress = False
# 启用开始安装按钮
self.ui.start_install_btn.setEnabled(True)
# 只有在配置有效时才启用开始安装按钮
if self.config_valid:
self.set_start_button_enabled(True)
else:
self.set_start_button_enabled(False)
def set_start_button_enabled(self, enabled, installing=False):
"""[已弃用] 设置按钮启用状态的旧方法,保留以兼容旧代码
现在推荐使用主窗口的setEnabled方法和直接设置按钮文本
Args:
enabled: 是否启用按钮
installing: 是否正在安装中
"""
# 直接设置按钮文本,不改变窗口启用状态
if installing:
self.ui.start_install_text.setText("正在安装")
self.install_button_enabled = False
else:
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):
"""云端配置获取完成的回调处理
@@ -129,35 +200,33 @@ class MainWindow(QMainWindow):
data: 获取到的配置数据
error_message: 错误信息,如果有
"""
if error_message:
if error_message == "update_required":
msg_box = msgbox_frame(
f"更新提示 - {APP_NAME}",
"\n当前版本过低,请及时更新。\n",
QMessageBox.StandardButton.Ok,
)
msg_box.exec()
self.ui_manager.open_project_home_page()
# 处理返回结果
result = self.config_manager.on_config_fetched(data, error_message)
# 根据返回的操作执行相应动作
if result and "action" in result:
if result["action"] == "exit":
# 强制关闭程序
self.shutdown_app(force_exit=True)
elif "missing_keys" in error_message:
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()
else:
# 其他错误暂时只在debug模式下打印
debug_mode = self.ui_manager.debug_action.isChecked() if self.ui_manager.debug_action else False
if debug_mode:
print(f"获取云端配置失败: {error_message}")
else:
self.cloud_config = data
debug_mode = self.ui_manager.debug_action.isChecked() if self.ui_manager.debug_action else False
if debug_mode:
print("--- Cloud config fetched successfully ---")
print(json.dumps(data, indent=2))
elif result["action"] == "disable_button":
# 禁用开始安装按钮
self.set_start_button_enabled(False)
# 检查是否有后续操作
if "then" in result and result["then"] == "exit":
# 强制关闭程序
self.shutdown_app(force_exit=True)
elif result["action"] == "enable_button":
# 启用开始安装按钮
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):
"""切换调试模式
@@ -169,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):
"""创建下载线程
@@ -220,10 +289,9 @@ class MainWindow(QMainWindow):
def after_hash_compare(self):
"""进行安装后哈希比较"""
# 禁用退出按钮
self.ui.exit_btn.setEnabled(False)
# 禁用窗口已在安装流程开始时完成
self.hash_msg_box = self.hash_manager.hash_pop_window()
self.hash_msg_box = self.hash_manager.hash_pop_window(check_type="after")
install_paths = self.download_manager.get_install_paths()
@@ -250,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(
@@ -259,26 +330,73 @@ class MainWindow(QMainWindow):
)
msg_box.exec()
# 重新启用退出按钮和开始安装按钮
self.ui.exit_btn.setEnabled(True)
self.ui.start_install_btn.setEnabled(True)
# 恢复窗口状态
self.setEnabled(True)
self.ui.start_install_text.setText("开始安装")
# 添加短暂延迟确保UI更新
QTimer.singleShot(100, self.show_result)
def show_result(self):
"""显示安装结果"""
installed_version = "\n".join(
[i for i in self.installed_status if self.installed_status[i]]
)
failed_ver = "\n".join(
[i for i in self.installed_status if not self.installed_status[i]]
)
"""显示安装结果,区分不同情况"""
# 获取当前安装状态
installed_versions = [] # 成功安装的版本
skipped_versions = [] # 已有补丁跳过的版本
failed_versions = [] # 安装失败的版本
not_found_versions = [] # 未找到的版本
# 获取所有游戏版本路径
install_paths = self.download_manager.get_install_paths() if hasattr(self.download_manager, "get_install_paths") else {}
for game_version, is_installed in self.installed_status.items():
path = install_paths.get(game_version, "")
# 检查游戏是否存在但未通过本次安装补丁
if is_installed:
# 游戏已安装补丁
if hasattr(self, 'download_queue_history') and game_version not in self.download_queue_history:
# 已有补丁,被跳过下载
skipped_versions.append(game_version)
else:
# 本次成功安装
installed_versions.append(game_version)
else:
# 游戏未安装补丁
if os.path.exists(path):
# 游戏文件夹存在,但安装失败
failed_versions.append(game_version)
else:
# 游戏文件夹不存在
not_found_versions.append(game_version)
# 构建结果信息
result_text = f"\n安装结果:\n"
# 总数统计
total_installed = len(installed_versions)
total_skipped = len(skipped_versions)
total_failed = len(failed_versions)
result_text += f"安装成功:{total_installed} 个 已跳过:{total_skipped} 个 安装失败:{total_failed}\n\n"
# 详细列表
if installed_versions:
result_text += f"【成功安装】:\n{chr(10).join(installed_versions)}\n\n"
if skipped_versions:
result_text += f"【已安装跳过】:\n{chr(10).join(skipped_versions)}\n\n"
if failed_versions:
result_text += f"【安装失败】:\n{chr(10).join(failed_versions)}\n\n"
if not_found_versions and (installed_versions or failed_versions):
# 只有当有其他版本存在时,才显示未找到的版本
result_text += f"【未在指定目录找到】:\n{chr(10).join(not_found_versions)}\n"
QMessageBox.information(
self,
f"完成 - {APP_NAME}",
f"\n安装结果:\n安装成功数:{len(installed_version.splitlines())} 安装失败数:{len(failed_ver.splitlines())}\n"
f"安装成功的版本:\n{installed_version}\n尚未持有或未使用本工具安装补丁的版本:\n{failed_ver}\n",
f"安装完成 - {APP_NAME}",
result_text
)
def closeEvent(self, event):
@@ -346,4 +464,156 @@ class MainWindow(QMainWindow):
if event:
event.accept()
else:
sys.exit(0)
sys.exit(0)
def handle_install_button_click(self):
"""处理安装按钮点击事件
根据按钮当前状态决定是显示错误还是执行安装
"""
if not self.install_button_enabled:
# 按钮处于"无法安装"状态
if self.last_error_message == "update_required":
msg_box = msgbox_frame(
f"更新提示 - {APP_NAME}",
"\n当前版本过低,请及时更新。\n",
QMessageBox.StandardButton.Ok,
)
msg_box.exec()
elif self.last_error_message == "directory_not_found":
# 目录识别失败的特定错误信息
reply = msgbox_frame(
f"目录错误 - {APP_NAME}",
"\n未能识别游戏目录请确认选择的是游戏的上级目录并且目录中包含Nekopara游戏文件夹。\n\n是否重新选择目录?\n",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
)
if reply.exec() == QMessageBox.StandardButton.Yes:
# 重新启用按钮并允许用户选择目录
self.set_start_button_enabled(True)
# 直接调用文件对话框
self.download_manager.file_dialog()
else:
# 网络错误或其他错误
reply = msgbox_frame(
f"错误 - {APP_NAME}",
"\n访问云端配置失败,请检查网络状况或稍后再试。\n\n是否重新尝试连接?\n",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
)
if reply.exec() == QMessageBox.StandardButton.Yes:
# 重试获取配置
self.fetch_cloud_config()
else:
# 按钮处于"开始安装"状态,正常执行安装流程
self.download_manager.file_dialog()
def handle_uninstall_button_click(self):
"""处理卸载补丁按钮点击事件
打开文件选择对话框选择游戏目录,然后卸载对应游戏的补丁
"""
# 获取游戏目录
from PySide6.QtWidgets import QFileDialog
debug_mode = self.debug_manager._is_debug_mode()
# 提示用户选择目录
file_dialog_info = "选择游戏上级目录" if debug_mode else "选择游戏目录"
selected_folder = QFileDialog.getExistingDirectory(self, file_dialog_info, "")
if not selected_folder or selected_folder == "":
return # 用户取消了选择
if debug_mode:
print(f"DEBUG: 卸载功能 - 用户选择了目录: {selected_folder}")
# 首先尝试将选择的目录视为上级目录,使用增强的目录识别功能
game_dirs = self.game_detector.identify_game_directories_improved(selected_folder)
if game_dirs and len(game_dirs) > 0:
# 找到了游戏目录,显示选择对话框
if debug_mode:
print(f"DEBUG: 卸载功能 - 在上级目录中找到以下游戏: {list(game_dirs.keys())}")
# 如果只有一个游戏,直接选择它
if len(game_dirs) == 1:
game_version = list(game_dirs.keys())[0]
game_dir = game_dirs[game_version]
self._confirm_and_uninstall(game_dir, game_version)
else:
# 有多个游戏,让用户选择
from PySide6.QtWidgets import QInputDialog
game_versions = list(game_dirs.keys())
# 添加"全部卸载"选项
game_versions.append("全部卸载")
selected_game, ok = QInputDialog.getItem(
self, "选择游戏", "选择要卸载补丁的游戏:",
game_versions, 0, False
)
if ok and selected_game:
if selected_game == "全部卸载":
# 卸载所有游戏补丁
reply = QMessageBox.question(
self,
f"确认卸载 - {APP_NAME}",
f"\n确定要卸载所有游戏的补丁吗?\n这将卸载以下游戏的补丁:\n{chr(10).join(list(game_dirs.keys()))}\n",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.Yes:
# 使用批量卸载方法
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
game_dir = game_dirs[game_version]
self._confirm_and_uninstall(game_dir, game_version)
else:
# 未找到游戏目录,尝试将选择的目录作为游戏目录
if debug_mode:
print(f"DEBUG: 卸载功能 - 未在上级目录找到游戏,尝试将选择的目录视为游戏目录")
game_version = self.game_detector.identify_game_version(selected_folder)
if game_version:
if debug_mode:
print(f"DEBUG: 卸载功能 - 识别为游戏: {game_version}")
self._confirm_and_uninstall(selected_folder, game_version)
else:
# 两种方式都未识别到游戏
if debug_mode:
print(f"DEBUG: 卸载功能 - 无法识别游戏")
msg_box = msgbox_frame(
f"错误 - {APP_NAME}",
"\n所选目录不是有效的NEKOPARA游戏目录。\n请选择包含游戏可执行文件的目录或其上级目录。\n",
QMessageBox.StandardButton.Ok,
)
msg_box.exec()
def _confirm_and_uninstall(self, game_dir, game_version):
"""确认并卸载补丁
Args:
game_dir: 游戏目录
game_version: 游戏版本
"""
debug_mode = self.debug_manager._is_debug_mode()
# 确认卸载
reply = QMessageBox.question(
self,
f"确认卸载 - {APP_NAME}",
f"\n确定要卸载 {game_version} 的补丁吗?\n游戏目录: {game_dir}\n",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.No:
return
# 开始卸载补丁
self.patch_manager.uninstall_patch(game_dir, game_version)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 264 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 571 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 229 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 264 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 327 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 197 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 166 KiB

View File

@@ -1,4 +1,3 @@
from data.pic_data import img_data
from PySide6.QtGui import QPixmap
import base64
from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
@@ -8,105 +7,468 @@ from PySide6.QtGui import (QAction, QBrush, QColor, QConicalGradient,
QCursor, QFont, QFontDatabase, QGradient,
QIcon, QImage, QKeySequence, QLinearGradient,
QPainter, QPalette, QPixmap, QRadialGradient,
QTransform)
QTransform, QPainterPath, QRegion)
from PySide6.QtWidgets import (QApplication, QLabel, QMainWindow, QMenu,
QMenuBar, QPushButton, QSizePolicy, QWidget)
def load_base64_image(base64_str):
pixmap = QPixmap()
pixmap.loadFromData(base64.b64decode(base64_str))
return pixmap
QMenuBar, QPushButton, QSizePolicy, QWidget, QHBoxLayout)
import os
# 导入配置常量
from data.config import APP_NAME, APP_VERSION
from utils import load_image_from_file
class Ui_MainWindows(object):
def setupUi(self, MainWindows):
if not MainWindows.objectName():
MainWindows.setObjectName(u"MainWindows")
MainWindows.setEnabled(True)
MainWindows.resize(1024, 576)
MainWindows.setMinimumSize(QSize(1024, 576))
MainWindows.setMaximumSize(QSize(1024, 576))
# 调整窗口默认大小为1280x720以匹配背景图片
MainWindows.resize(1280, 720)
# 锁定窗口比例为16:9确保不会变形同时限制最大尺寸不超过背景图片
MainWindows.setMinimumSize(QSize(1024, 576)) # 16:9最小尺寸
MainWindows.setMaximumSize(QSize(1280, 720)) # 将最大尺寸限制为1280x720
# 设置固定纵横比
self.aspect_ratio = 16/9
MainWindows.setMouseTracking(False)
MainWindows.setTabletTracking(False)
MainWindows.setAcceptDrops(True)
MainWindows.setAutoFillBackground(True)
MainWindows.setAutoFillBackground(False)
MainWindows.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
MainWindows.setAnimated(True)
MainWindows.setDocumentMode(False)
MainWindows.setDockNestingEnabled(False)
# 加载自定义字体
font_id = QFontDatabase.addApplicationFont(os.path.join(os.path.dirname(os.path.dirname(__file__)), "fonts", "SmileySans-Oblique.ttf"))
font_family = QFontDatabase.applicationFontFamilies(font_id)[0] if font_id != -1 else "Arial"
self.custom_font = QFont(font_family, 16) # 创建字体对象大小为16
self.custom_font.setWeight(QFont.Weight.Medium) # 设置为中等粗细,不要太粗
self.centralwidget = QWidget(MainWindows)
self.centralwidget.setObjectName(u"centralwidget")
self.centralwidget.setAutoFillBackground(True)
self.loadbg = QLabel(self.centralwidget)
self.centralwidget.setAutoFillBackground(False) # 修改为False以支持透明背景
self.centralwidget.setStyleSheet("""
QWidget#centralwidget {
background-color: transparent;
}
""")
# 圆角背景容器
self.main_container = QWidget(self.centralwidget)
self.main_container.setObjectName(u"main_container")
self.main_container.setGeometry(QRect(0, 0, 1280, 720))
self.main_container.setStyleSheet("""
QWidget#main_container {
background-color: #E96948;
border-radius: 20px;
border: 1px solid #E96948;
}
""")
# 内容容器 - 用于限制内容在圆角范围内
self.content_container = QWidget(self.main_container)
self.content_container.setObjectName(u"content_container")
self.content_container.setGeometry(QRect(0, 0, 1280, 720))
self.content_container.setStyleSheet("""
QWidget#content_container {
background-color: transparent;
border-radius: 20px;
}
""")
# 添加圆角裁剪,确保内容在圆角范围内
rect = QRect(0, 0, 1280, 720)
path = QPainterPath()
path.addRoundedRect(rect, 20, 20)
region = QRegion(path.toFillPolygon().toPolygon())
self.content_container.setMask(region)
# 标题栏
self.title_bar = QWidget(self.content_container)
self.title_bar.setObjectName(u"title_bar")
self.title_bar.setGeometry(QRect(0, 0, 1280, 35)) # 减小高度从40到35
self.title_bar.setStyleSheet("""
QWidget#title_bar {
background-color: #E96948;
border-top-left-radius: 20px;
border-top-right-radius: 20px;
border-bottom: 1px solid #F47A5B;
}
""")
# 标题栏布局
self.title_layout = QHBoxLayout(self.title_bar)
self.title_layout.setSpacing(10)
self.title_layout.setContentsMargins(10, 0, 10, 0)
# 添加最小化和关闭按钮到标题栏
self.minimize_btn = QPushButton(self.title_bar)
self.minimize_btn.setObjectName(u"minimize_btn")
self.minimize_btn.setMinimumSize(QSize(24, 24))
self.minimize_btn.setMaximumSize(QSize(24, 24))
self.minimize_btn.setCursor(QCursor(Qt.CursorShape.PointingHandCursor))
self.minimize_btn.setStyleSheet("""
QPushButton {
background-color: #FFC107;
border-radius: 12px;
border: none;
}
QPushButton:hover {
background-color: #FFD54F;
}
QPushButton:pressed {
background-color: #FFA000;
}
""")
self.minimize_btn.setText("")
self.minimize_btn.setFont(QFont(font_family, 10))
self.close_btn = QPushButton(self.title_bar)
self.close_btn.setObjectName(u"close_btn")
self.close_btn.setMinimumSize(QSize(24, 24))
self.close_btn.setMaximumSize(QSize(24, 24))
self.close_btn.setCursor(QCursor(Qt.CursorShape.PointingHandCursor))
self.close_btn.setStyleSheet("""
QPushButton {
background-color: #F44336;
border-radius: 12px;
border: none;
color: white;
font-weight: bold;
}
QPushButton:hover {
background-color: #EF5350;
}
QPushButton:pressed {
background-color: #D32F2F;
}
""")
self.close_btn.setText("×")
self.close_btn.setFont(QFont(font_family, 14))
# 标题文本
self.title_label = QLabel(self.title_bar)
self.title_label.setObjectName(u"title_label")
# 直接使用APP_NAME并添加版本号
self.title_label.setText(f"{APP_NAME} v{APP_VERSION}")
title_font = QFont(font_family, 14) # 减小字体从16到14
title_font.setBold(True)
self.title_label.setFont(title_font)
self.title_label.setStyleSheet("color: #333333; padding-left: 10px;")
self.title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
# 添加按钮到标题栏布局
self.title_layout.addWidget(self.title_label)
self.title_layout.addStretch(1)
self.title_layout.addWidget(self.minimize_btn)
self.title_layout.addSpacing(5)
self.title_layout.addWidget(self.close_btn)
# 修改菜单区域 - 确保足够宽以容纳更多菜单项
self.menu_area = QWidget(self.content_container)
self.menu_area.setObjectName(u"menu_area")
self.menu_area.setGeometry(QRect(0, 35, 1280, 30)) # 调整位置从40到35高度从35到30
self.menu_area.setStyleSheet("""
QWidget#menu_area {
background-color: #E96948;
}
""")
# 不再使用菜单栏,改用普通按钮
# 创建菜单按钮字体
menu_font = QFont(font_family, 14) # 进一步减小字体大小到14
menu_font.setBold(True)
# 设置按钮
self.settings_btn = QPushButton("设置", self.menu_area)
self.settings_btn.setObjectName(u"settings_btn")
self.settings_btn.setGeometry(QRect(20, 1, 80, 28)) # 调整高度和Y位置
self.settings_btn.setFont(menu_font)
self.settings_btn.setCursor(QCursor(Qt.CursorShape.PointingHandCursor))
self.settings_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;
}
""")
# 帮助按钮
self.help_btn = QPushButton("帮助", self.menu_area)
self.help_btn.setObjectName(u"help_btn")
self.help_btn.setGeometry(QRect(120, 1, 80, 28)) # 调整高度和Y位置
self.help_btn.setFont(menu_font)
self.help_btn.setCursor(QCursor(Qt.CursorShape.PointingHandCursor))
self.help_btn.setStyleSheet(self.settings_btn.styleSheet())
# 将原来的菜单项移到全局,方便访问
self.menu = QMenu(self.content_container)
self.menu.setObjectName(u"menu")
self.menu.setTitle("设置")
self.menu.setFont(menu_font)
# 创建菜单样式表,直接在样式表中指定字体族
menu_style = 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-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;
}}
"""
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(menu_style)
# 连接按钮点击事件到显示对应菜单
self.settings_btn.clicked.connect(lambda: self.show_menu(self.menu, self.settings_btn))
self.help_btn.clicked.connect(lambda: self.show_menu(self.menu_2, self.help_btn))
# 预留位置给未来可能的第三个按钮
# 第三个按钮可以这样添加:
# self.third_btn = QPushButton("第三项", self.menu_area)
# self.third_btn.setObjectName(u"third_btn")
# self.third_btn.setGeometry(QRect(320, 0, 120, 35))
# self.third_btn.setFont(menu_font)
# self.third_btn.setCursor(QCursor(Qt.CursorShape.PointingHandCursor))
# self.third_btn.setStyleSheet(self.settings_btn.styleSheet())
# self.third_btn.clicked.connect(lambda: self.show_menu(self.menu_3, self.third_btn))
# 内容子容器
self.inner_content = QWidget(self.content_container)
self.inner_content.setObjectName(u"inner_content")
# 确保宽度足够大,保证右侧元素完全显示
self.inner_content.setGeometry(QRect(0, 65, 1280, 655)) # 调整Y位置从75到65高度从645到655
self.inner_content.setStyleSheet("""
QWidget#inner_content {
background-color: transparent;
border-bottom-left-radius: 20px;
border-bottom-right-radius: 20px;
}
""")
# 添加底部圆角裁剪
inner_rect = QRect(0, 0, 1280, 665)
inner_path = QPainterPath()
inner_path.addRoundedRect(inner_rect, 20, 20)
inner_region = QRegion(inner_path.toFillPolygon().toPolygon())
self.inner_content.setMask(inner_region)
# 在主容器中添加背景和内容元素
# 修改loadbg使用title_bg1.png作为整个背景
# 原来的loadbg保持不变
self.loadbg = QLabel(self.inner_content)
self.loadbg.setObjectName(u"loadbg")
self.loadbg.setGeometry(QRect(0, 0, 1031, 561))
self.loadbg.setPixmap(load_base64_image(img_data["loadbg"]))
self.loadbg.setGeometry(QRect(0, 0, 1280, 655))
# 加载背景图并允许拉伸
bg_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "IMG", "BG", "bg1.jpg")
bg_pixmap = QPixmap(bg_path)
self.loadbg.setPixmap(bg_pixmap)
self.loadbg.setScaledContents(True)
self.vol1bg = QLabel(self.centralwidget)
self.vol1bg = QLabel(self.inner_content)
self.vol1bg.setObjectName(u"vol1bg")
self.vol1bg.setGeometry(QRect(0, 120, 93, 64))
self.vol1bg.setPixmap(load_base64_image(img_data["vol1"]))
self.vol1bg.setGeometry(QRect(0, 150, 93, 64))
# 直接加载图片文件
vol1_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "IMG", "LOGO", "vo01_logo.png")
self.vol1bg.setPixmap(QPixmap(vol1_path))
self.vol1bg.setScaledContents(True)
self.vol2bg = QLabel(self.centralwidget)
self.vol2bg = QLabel(self.inner_content)
self.vol2bg.setObjectName(u"vol2bg")
self.vol2bg.setGeometry(QRect(0, 180, 93, 64))
self.vol2bg.setPixmap(load_base64_image(img_data["vol2"]))
self.vol2bg.setGeometry(QRect(0, 210, 93, 64))
# 直接加载图片文件
vol2_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "IMG", "LOGO", "vo02_logo.png")
self.vol2bg.setPixmap(QPixmap(vol2_path))
self.vol2bg.setScaledContents(True)
self.vol3bg = QLabel(self.centralwidget)
self.vol3bg = QLabel(self.inner_content)
self.vol3bg.setObjectName(u"vol3bg")
self.vol3bg.setGeometry(QRect(0, 240, 93, 64))
self.vol3bg.setPixmap(load_base64_image(img_data["vol3"]))
self.vol3bg.setGeometry(QRect(0, 270, 93, 64))
# 直接加载图片文件
vol3_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "IMG", "LOGO", "vo03_logo.png")
self.vol3bg.setPixmap(QPixmap(vol3_path))
self.vol3bg.setScaledContents(True)
self.vol4bg = QLabel(self.centralwidget)
self.vol4bg = QLabel(self.inner_content)
self.vol4bg.setObjectName(u"vol4bg")
self.vol4bg.setGeometry(QRect(0, 300, 93, 64))
self.vol4bg.setPixmap(load_base64_image(img_data["vol4"]))
self.vol4bg.setGeometry(QRect(0, 330, 93, 64))
# 直接加载图片文件
vol4_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "IMG", "LOGO", "vo04_logo.png")
self.vol4bg.setPixmap(QPixmap(vol4_path))
self.vol4bg.setScaledContents(True)
self.afterbg = QLabel(self.centralwidget)
self.afterbg = QLabel(self.inner_content)
self.afterbg.setObjectName(u"afterbg")
self.afterbg.setGeometry(QRect(0, 360, 93, 64))
self.afterbg.setPixmap(load_base64_image(img_data["after"]))
self.afterbg.setGeometry(QRect(0, 390, 93, 64))
# 直接加载图片文件
after_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "IMG", "LOGO", "voaf_logo.png")
self.afterbg.setPixmap(QPixmap(after_path))
self.afterbg.setScaledContents(True)
self.Mainbg = QLabel(self.centralwidget)
# 修复Mainbg位置并使用title_bg1.png作为背景图片
self.Mainbg = QLabel(self.inner_content)
self.Mainbg.setObjectName(u"Mainbg")
self.Mainbg.setGeometry(QRect(0, 0, 1031, 561))
self.Mainbg.setPixmap(load_base64_image(img_data["Mainbg"]))
self.Mainbg.setScaledContents(True)
self.start_install_btn = QPushButton(self.centralwidget)
self.Mainbg.setGeometry(QRect(0, 0, 1280, 655))
# 允许拉伸以填满整个区域
main_bg_pixmap = load_image_from_file(os.path.join(os.path.dirname(os.path.dirname(__file__)), "IMG", "BG", "title_bg1.png"))
# 如果加载的图片不是空的,则设置,并允许拉伸填满
if not main_bg_pixmap.isNull():
self.Mainbg.setPixmap(main_bg_pixmap)
self.Mainbg.setScaledContents(True)
self.Mainbg.setAlignment(Qt.AlignmentFlag.AlignCenter)
# 使用新的按钮图片
button_pixmap = load_image_from_file(os.path.join(os.path.dirname(os.path.dirname(__file__)), "IMG", "BTN", "Button.png"))
# 创建文本标签布局的按钮
# 开始安装按钮 - 基于背景图片和标签组合
# 调整开始安装按钮的位置
self.button_container = QWidget(self.inner_content)
self.button_container.setObjectName(u"start_install_container")
self.button_container.setGeometry(QRect(1050, 200, 211, 111)) # 调整Y坐标上移至200
# 不要隐藏容器,让动画系统来控制它的可见性和位置
# 使用原来的按钮背景图片
self.start_install_bg = QLabel(self.button_container)
self.start_install_bg.setObjectName(u"start_install_bg")
self.start_install_bg.setGeometry(QRect(10, 10, 191, 91)) # 居中放置在扩大的容器中
self.start_install_bg.setPixmap(button_pixmap)
self.start_install_bg.setScaledContents(True)
self.start_install_text = QLabel(self.button_container)
self.start_install_text.setObjectName(u"start_install_text")
self.start_install_text.setGeometry(QRect(10, 7, 191, 91)) # 居中放置在扩大的容器中
self.start_install_text.setText("开始安装")
self.start_install_text.setFont(self.custom_font)
self.start_install_text.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.start_install_text.setStyleSheet("letter-spacing: 1px;")
# 点击区域透明按钮
self.start_install_btn = QPushButton(self.button_container)
self.start_install_btn.setObjectName(u"start_install_btn")
self.start_install_btn.setEnabled(True)
self.start_install_btn.setGeometry(QRect(780, 250, 191, 91))
self.start_install_btn.setAutoFillBackground(False)
start_install_icon = QIcon()
start_install_pixmap = load_base64_image(img_data["start_install_btn"])
if not start_install_pixmap.isNull():
start_install_icon.addPixmap(start_install_pixmap)
self.start_install_btn.setIcon(start_install_icon)
self.start_install_btn.setIcon(start_install_icon)
self.start_install_btn.setIconSize(QSize(189, 110))
self.start_install_btn.setCheckable(False)
self.start_install_btn.setAutoRepeat(False)
self.start_install_btn.setAutoDefault(False)
self.start_install_btn.setGeometry(QRect(10, 10, 191, 91)) # 居中放置在扩大的容器中
self.start_install_btn.setCursor(QCursor(Qt.CursorShape.PointingHandCursor)) # 设置鼠标悬停时为手形光标
self.start_install_btn.setFlat(True)
self.exit_btn = QPushButton(self.centralwidget)
self.start_install_btn.raise_() # 确保按钮在最上层
self.start_install_btn.setStyleSheet("""
QPushButton {
background-color: transparent;
border: none;
}
""")
# 添加卸载补丁按钮 - 新增
self.uninstall_container = QWidget(self.inner_content)
self.uninstall_container.setObjectName(u"uninstall_container")
self.uninstall_container.setGeometry(QRect(1050, 310, 211, 111)) # 调整Y坐标位于310位置
# 使用相同的按钮背景图片
self.uninstall_bg = QLabel(self.uninstall_container)
self.uninstall_bg.setObjectName(u"uninstall_bg")
self.uninstall_bg.setGeometry(QRect(10, 10, 191, 91)) # 居中放置在扩大的容器中
self.uninstall_bg.setPixmap(button_pixmap)
self.uninstall_bg.setScaledContents(True)
self.uninstall_text = QLabel(self.uninstall_container)
self.uninstall_text.setObjectName(u"uninstall_text")
self.uninstall_text.setGeometry(QRect(10, 7, 191, 91)) # 居中放置在扩大的容器中
self.uninstall_text.setText("卸载补丁")
self.uninstall_text.setFont(self.custom_font)
self.uninstall_text.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.uninstall_text.setStyleSheet("letter-spacing: 1px;")
# 点击区域透明按钮
self.uninstall_btn = QPushButton(self.uninstall_container)
self.uninstall_btn.setObjectName(u"uninstall_btn")
self.uninstall_btn.setGeometry(QRect(10, 10, 191, 91)) # 居中放置在扩大的容器中
self.uninstall_btn.setCursor(QCursor(Qt.CursorShape.PointingHandCursor)) # 设置鼠标悬停时为手形光标
self.uninstall_btn.setFlat(True)
self.uninstall_btn.raise_() # 确保按钮在最上层
self.uninstall_btn.setStyleSheet("""
QPushButton {
background-color: transparent;
border: none;
}
""")
# 退出按钮 - 基于背景图片和标签组合,调整位置
self.exit_container = QWidget(self.inner_content)
self.exit_container.setObjectName(u"exit_container")
self.exit_container.setGeometry(QRect(1050, 420, 211, 111)) # 调整Y坐标下移至420
# 不要隐藏容器,让动画系统来控制它的可见性和位置
# 使用原来的按钮背景图片
self.exit_bg = QLabel(self.exit_container)
self.exit_bg.setObjectName(u"exit_bg")
self.exit_bg.setGeometry(QRect(10, 10, 191, 91)) # 居中放置在扩大的容器中
self.exit_bg.setPixmap(button_pixmap)
self.exit_bg.setScaledContents(True)
self.exit_text = QLabel(self.exit_container)
self.exit_text.setObjectName(u"exit_text")
self.exit_text.setGeometry(QRect(10, 7, 191, 91)) # 居中放置在扩大的容器中
self.exit_text.setText("退出程序")
self.exit_text.setFont(self.custom_font)
self.exit_text.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.exit_text.setStyleSheet("letter-spacing: 1px;")
# 点击区域透明按钮
self.exit_btn = QPushButton(self.exit_container)
self.exit_btn.setObjectName(u"exit_btn")
self.exit_btn.setEnabled(True)
self.exit_btn.setGeometry(QRect(780, 340, 191, 91))
self.exit_btn.setAutoFillBackground(False)
exit_icon = QIcon()
exit_pixmap = load_base64_image(img_data["exit_btn"])
if not exit_pixmap.isNull():
exit_icon.addPixmap(exit_pixmap)
self.exit_btn.setIcon(exit_icon)
self.exit_btn.setIcon(exit_icon)
self.exit_btn.setIconSize(QSize(189, 110))
self.exit_btn.setCheckable(False)
self.exit_btn.setGeometry(QRect(10, 10, 191, 91)) # 居中放置在扩大的容器中
self.exit_btn.setCursor(QCursor(Qt.CursorShape.PointingHandCursor)) # 设置鼠标悬停时为手形光标
self.exit_btn.setFlat(True)
self.menubg = QLabel(self.centralwidget)
self.menubg.setObjectName(u"menubg")
self.menubg.setGeometry(QRect(710, 0, 321, 561))
self.menubg.setPixmap(load_base64_image(img_data["menubg"]))
self.menubg.setScaledContents(True)
self.exit_btn.raise_() # 确保按钮在最上层
self.exit_btn.setStyleSheet("""
QPushButton {
background-color: transparent;
border: none;
}
""")
MainWindows.setCentralWidget(self.centralwidget)
# 调整层级顺序
self.loadbg.raise_()
self.vol1bg.raise_()
self.vol2bg.raise_()
@@ -114,28 +476,22 @@ class Ui_MainWindows(object):
self.vol4bg.raise_()
self.afterbg.raise_()
self.Mainbg.raise_()
self.menubg.raise_()
self.start_install_btn.raise_()
self.exit_btn.raise_()
self.menubar = QMenuBar(MainWindows)
self.menubar.setObjectName(u"menubar")
self.menubar.setGeometry(QRect(0, 0, 1024, 21))
self.menu = QMenu(self.menubar)
self.menu.setObjectName(u"menu")
self.menu_2 = QMenu(self.menubar)
self.menu_2.setObjectName(u"menu_2")
MainWindows.setMenuBar(self.menubar)
self.menubar.addAction(self.menu.menuAction())
self.menubar.addAction(self.menu_2.menuAction())
self.menu.addSeparator()
self.button_container.raise_()
self.uninstall_container.raise_() # 添加新按钮到层级顺序
self.exit_container.raise_()
self.menu_area.raise_() # 确保菜单区域在背景之上
# self.menubar.raise_() # 不再需要菜单栏
self.settings_btn.raise_() # 确保设置按钮在上层
self.help_btn.raise_() # 确保帮助按钮在上层
self.title_bar.raise_() # 确保标题栏在最上层
self.retranslateUi(MainWindows)
QMetaObject.connectSlotsByName(MainWindows)
# setupUi
def retranslateUi(self, MainWindows):
MainWindows.setWindowTitle(QCoreApplication.translate("MainWindows", u" UI Test", None))
MainWindows.setWindowTitle(QCoreApplication.translate("MainWindows", f"{APP_NAME} v{APP_VERSION}", None))
self.loadbg.setText("")
self.vol1bg.setText("")
self.vol2bg.setText("")
@@ -146,10 +502,18 @@ class Ui_MainWindows(object):
#if QT_CONFIG(accessibility)
self.start_install_btn.setAccessibleDescription("")
#endif // QT_CONFIG(accessibility)
self.start_install_btn.setText("")
self.exit_btn.setText("")
self.menubg.setText("")
self.menu.setTitle(QCoreApplication.translate("MainWindows", u"\u8bbe\u7f6e", None))
self.menu_2.setTitle(QCoreApplication.translate("MainWindows", u"\u5e2e\u52a9", None))
self.menu.setTitle(QCoreApplication.translate("MainWindows", u"设置", None))
self.menu_2.setTitle(QCoreApplication.translate("MainWindows", u"帮助", None))
# retranslateUi
def show_menu(self, menu, button):
"""显示菜单
Args:
menu: 要显示的菜单
button: 触发菜单的按钮
"""
# 计算菜单显示位置
pos = button.mapToGlobal(button.rect().bottomLeft())
menu.exec(pos)

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,333 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindows</class>
<widget class="QMainWindow" name="MainWindows">
<property name="enabled">
<bool>true</bool>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1024</width>
<height>576</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>1024</width>
<height>576</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>1024</width>
<height>576</height>
</size>
</property>
<property name="mouseTracking">
<bool>false</bool>
</property>
<property name="tabletTracking">
<bool>false</bool>
</property>
<property name="acceptDrops">
<bool>true</bool>
</property>
<property name="windowTitle">
<string> UI Test</string>
</property>
<property name="autoFillBackground">
<bool>true</bool>
</property>
<property name="toolButtonStyle">
<enum>Qt::ToolButtonStyle::ToolButtonIconOnly</enum>
</property>
<property name="animated">
<bool>true</bool>
</property>
<property name="documentMode">
<bool>false</bool>
</property>
<property name="dockNestingEnabled">
<bool>false</bool>
</property>
<widget class="QWidget" name="centralwidget">
<property name="autoFillBackground">
<bool>true</bool>
</property>
<widget class="QLabel" name="loadbg">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1031</width>
<height>561</height>
</rect>
</property>
<property name="text">
<string/>
</property>
<property name="pixmap">
<pixmap>IMG/BG/bg2.jpg</pixmap>
</property>
<property name="scaledContents">
<bool>true</bool>
</property>
</widget>
<widget class="QLabel" name="vol1bg">
<property name="geometry">
<rect>
<x>0</x>
<y>120</y>
<width>93</width>
<height>64</height>
</rect>
</property>
<property name="text">
<string/>
</property>
<property name="pixmap">
<pixmap>IMG/LOGO/vo01_logo.png</pixmap>
</property>
<property name="scaledContents">
<bool>true</bool>
</property>
</widget>
<widget class="QLabel" name="vol2bg">
<property name="geometry">
<rect>
<x>0</x>
<y>180</y>
<width>93</width>
<height>64</height>
</rect>
</property>
<property name="text">
<string/>
</property>
<property name="pixmap">
<pixmap>IMG/LOGO/vo02_logo.png</pixmap>
</property>
<property name="scaledContents">
<bool>true</bool>
</property>
</widget>
<widget class="QLabel" name="vol3bg">
<property name="geometry">
<rect>
<x>0</x>
<y>240</y>
<width>93</width>
<height>64</height>
</rect>
</property>
<property name="text">
<string/>
</property>
<property name="pixmap">
<pixmap>IMG/LOGO/vo03_logo.png</pixmap>
</property>
<property name="scaledContents">
<bool>true</bool>
</property>
</widget>
<widget class="QLabel" name="vol4bg">
<property name="geometry">
<rect>
<x>0</x>
<y>300</y>
<width>93</width>
<height>64</height>
</rect>
</property>
<property name="text">
<string/>
</property>
<property name="pixmap">
<pixmap>IMG/LOGO/vo04_logo.png</pixmap>
</property>
<property name="scaledContents">
<bool>true</bool>
</property>
</widget>
<widget class="QLabel" name="afterbg">
<property name="geometry">
<rect>
<x>0</x>
<y>360</y>
<width>93</width>
<height>64</height>
</rect>
</property>
<property name="text">
<string/>
</property>
<property name="pixmap">
<pixmap>IMG/LOGO/voaf_logo.png</pixmap>
</property>
<property name="scaledContents">
<bool>true</bool>
</property>
</widget>
<widget class="QLabel" name="Mainbg">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1031</width>
<height>561</height>
</rect>
</property>
<property name="text">
<string/>
</property>
<property name="pixmap">
<pixmap>IMG/BG/bg3.jpg</pixmap>
</property>
<property name="scaledContents">
<bool>true</bool>
</property>
</widget>
<widget class="QPushButton" name="start_install_btn">
<property name="enabled">
<bool>true</bool>
</property>
<property name="geometry">
<rect>
<x>780</x>
<y>250</y>
<width>191</width>
<height>91</height>
</rect>
</property>
<property name="accessibleDescription">
<string/>
</property>
<property name="autoFillBackground">
<bool>false</bool>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset>
<normaloff>IMG/BTN/start_install.bmp</normaloff>IMG/BTN/start_install.bmp</iconset>
</property>
<property name="iconSize">
<size>
<width>189</width>
<height>110</height>
</size>
</property>
<property name="checkable">
<bool>false</bool>
</property>
<property name="autoRepeat">
<bool>false</bool>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
<property name="flat">
<bool>true</bool>
</property>
</widget>
<widget class="QPushButton" name="exit_btn">
<property name="enabled">
<bool>true</bool>
</property>
<property name="geometry">
<rect>
<x>780</x>
<y>340</y>
<width>191</width>
<height>91</height>
</rect>
</property>
<property name="autoFillBackground">
<bool>false</bool>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset>
<normaloff>IMG/BTN/exit.bmp</normaloff>IMG/BTN/exit.bmp</iconset>
</property>
<property name="iconSize">
<size>
<width>189</width>
<height>110</height>
</size>
</property>
<property name="checkable">
<bool>false</bool>
</property>
<property name="flat">
<bool>true</bool>
</property>
</widget>
<widget class="QLabel" name="menubg">
<property name="geometry">
<rect>
<x>710</x>
<y>0</y>
<width>321</width>
<height>561</height>
</rect>
</property>
<property name="text">
<string/>
</property>
<property name="pixmap">
<pixmap>IMG/BG/menubg.jpg</pixmap>
</property>
<property name="scaledContents">
<bool>true</bool>
</property>
</widget>
<zorder>loadbg</zorder>
<zorder>vol1bg</zorder>
<zorder>vol2bg</zorder>
<zorder>vol3bg</zorder>
<zorder>vol4bg</zorder>
<zorder>afterbg</zorder>
<zorder>Mainbg</zorder>
<zorder>menubg</zorder>
<zorder>start_install_btn</zorder>
<zorder>exit_btn</zorder>
</widget>
<widget class="QMenuBar" name="menubar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1024</width>
<height>21</height>
</rect>
</property>
<widget class="QMenu" name="menu">
<property name="title">
<string>设置</string>
</property>
<addaction name="separator"/>
<addaction name="action_2"/>
</widget>
<widget class="QMenu" name="menu_2">
<property name="title">
<string>关于</string>
</property>
</widget>
<addaction name="menu"/>
<addaction name="menu_2"/>
</widget>
<action name="action_2">
<property name="text">
<string>update - sd</string>
</property>
</action>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -1,19 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>480</width>
<height>270</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -1,12 +1,14 @@
from .logger import Logger
from .helpers import (
load_base64_image, HashManager, AdminPrivileges, msgbox_frame,
load_config, save_config, HostsManager, censor_url, resource_path
load_config, save_config, HostsManager, censor_url, resource_path,
load_image_from_file
)
__all__ = [
'Logger',
'load_base64_image',
'load_image_from_file',
'HashManager',
'AdminPrivileges',
'msgbox_frame',

View File

@@ -9,7 +9,6 @@ import psutil
from PySide6 import QtCore, QtWidgets
import re
from PySide6.QtGui import QIcon, QPixmap
from data.pic_data import img_data
from data.config import APP_NAME, CONFIG_FILE
def resource_path(relative_path):
@@ -28,7 +27,7 @@ def resource_path(relative_path):
if relative_path in ("aria2c.exe", "cfst.exe"):
return os.path.join(base_path, 'bin', relative_path)
elif relative_path in ("ip.txt", "ipv6.txt"):
return os.path.join(base_path, 'resources', 'data', relative_path)
return os.path.join(base_path, 'data', relative_path)
return os.path.join(base_path, relative_path)
@@ -37,14 +36,28 @@ def load_base64_image(base64_str):
pixmap.loadFromData(base64.b64decode(base64_str))
return pixmap
def load_image_from_file(file_path):
"""加载图像文件到QPixmap
Args:
file_path: 图像文件路径
Returns:
QPixmap: 加载的图像
"""
if os.path.exists(file_path):
return QPixmap(file_path)
return QPixmap()
def msgbox_frame(title, text, buttons=QtWidgets.QMessageBox.StandardButton.NoButton):
msg_box = QtWidgets.QMessageBox()
msg_box.setWindowTitle(title)
msg_box.setWindowModality(QtCore.Qt.WindowModality.WindowModal)
icon_data = img_data.get("icon")
if icon_data:
pixmap = load_base64_image(icon_data)
# 直接加载图标文件
icon_path = resource_path(os.path.join("IMG", "ICO", "icon.png"))
if os.path.exists(icon_path):
pixmap = QPixmap(icon_path)
if not pixmap.isNull():
msg_box.setWindowIcon(QIcon(pixmap))
msg_box.setIconPixmap(pixmap.scaled(64, 64, QtCore.Qt.AspectRatioMode.KeepAspectRatio, QtCore.Qt.TransformationMode.SmoothTransformation))
@@ -99,8 +112,25 @@ class HashManager:
print(f"Error calculating hash for {file_path}: {e}")
return results
def hash_pop_window(self):
msg_box = msgbox_frame(f"通知 - {APP_NAME}", "\n正在检验文件状态...\n")
def hash_pop_window(self, check_type="default"):
"""显示文件检验窗口
Args:
check_type: 检查类型,可以是 'pre'(预检查), 'after'(后检查), 'extraction'(解压后检查)
Returns:
QMessageBox: 消息框实例
"""
message = "\n正在检验文件状态...\n"
if check_type == "pre":
message = "\n正在检查游戏文件以确定需要安装的补丁...\n"
elif check_type == "after":
message = "\n正在检验本地文件完整性...\n"
elif check_type == "extraction":
message = "\n正在验证下载的解压文件完整性...\n"
msg_box = msgbox_frame(f"通知 - {APP_NAME}", message)
msg_box.open()
QtWidgets.QApplication.processEvents()
return msg_box
@@ -158,6 +188,7 @@ class AdminPrivileges:
"nekopara_vol1.exe",
"nekopara_vol2.exe",
"NEKOPARAvol3.exe",
"NEKOPARAvol3.exe.nocrack",
"nekopara_vol4.exe",
"nekopara_after.exe",
]
@@ -200,33 +231,40 @@ class AdminPrivileges:
def check_and_terminate_processes(self):
for proc in psutil.process_iter(["pid", "name"]):
if proc.info["name"] in self.required_exes:
msg_box = msgbox_frame(
f"进程检测 - {APP_NAME}",
f"\n检测到游戏正在运行: {proc.info['name']} \n\n是否终止?\n",
QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No,
)
reply = msg_box.exec()
if reply == QtWidgets.QMessageBox.StandardButton.Yes:
try:
proc.terminate()
proc.wait(timeout=3)
except psutil.AccessDenied:
proc_name = proc.info["name"].lower() if proc.info["name"] else ""
# 检查进程名是否匹配任何需要终止的游戏进程
for exe in self.required_exes:
if exe.lower() == proc_name:
# 获取不带.nocrack的游戏名称用于显示
display_name = exe.replace(".nocrack", "")
msg_box = msgbox_frame(
f"进程检测 - {APP_NAME}",
f"\n检测到游戏正在运行: {display_name} \n\n是否终止?\n",
QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No,
)
reply = msg_box.exec()
if reply == QtWidgets.QMessageBox.StandardButton.Yes:
try:
proc.terminate()
proc.wait(timeout=3)
except psutil.AccessDenied:
msg_box = msgbox_frame(
f"错误 - {APP_NAME}",
f"\n无法关闭游戏: {display_name} \n\n请手动关闭后重启应用\n",
QtWidgets.QMessageBox.StandardButton.Ok,
)
msg_box.exec()
sys.exit(1)
else:
msg_box = msgbox_frame(
f"错误 - {APP_NAME}",
f"\n无法关闭游戏: {proc.info['name']} \n\n请手动关闭后重启应用\n",
f"进程检测 - {APP_NAME}",
f"\n关闭游戏: {display_name} \n\n请手动关闭后重启应用\n",
QtWidgets.QMessageBox.StandardButton.Ok,
)
msg_box.exec()
sys.exit(1)
else:
msg_box = msgbox_frame(
f"进程检测 - {APP_NAME}",
f"\n未关闭的游戏: {proc.info['name']} \n\n请手动关闭后重启应用\n",
QtWidgets.QMessageBox.StandardButton.Ok,
)
msg_box.exec()
sys.exit(1)
class HostsManager:
def __init__(self):

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

@@ -1,6 +1,9 @@
import json
import requests
import webbrowser
from PySide6.QtCore import QThread, Signal
from PySide6.QtWidgets import QMessageBox
import sys
class ConfigFetchThread(QThread):
finished = Signal(object, str) # data, error_message
@@ -30,8 +33,12 @@ class ConfigFetchThread(QThread):
# 首先总是尝试解析JSON
config_data = response.json()
# 检查是否是要求更新的错误信息
if config_data.get("message") == "请使用最新版本的FRAISEMOE Addons Installer NEXT进行下载安装":
# 检查是否是要求更新的错误信息 - 使用Unicode编码的更新提示文本
update_required_msg = "\u8bf7\u4f7f\u7528\u6700\u65b0\u7248\u672c\u7684FraiseMoe2-Next\u8fdb\u884c\u4e0b\u8f7d"
if isinstance(config_data, str) and config_data == update_required_msg:
self.finished.emit(None, "update_required")
return
elif isinstance(config_data, dict) and config_data.get("message") == update_required_msg:
self.finished.emit(None, "update_required")
return
@@ -44,9 +51,15 @@ class ConfigFetchThread(QThread):
self.finished.emit(config_data, "")
except requests.exceptions.RequestException as e:
self.finished.emit(None, f"网络请求失败: {e}")
error_msg = "访问云端配置失败,请检查网络状况或稍后再试。"
if self.debug_mode:
error_msg += f"\n详细错误: {e}"
self.finished.emit(None, error_msg)
except (ValueError, json.JSONDecodeError) as e:
self.finished.emit(None, f"JSON解析失败: {e}")
error_msg = "访问云端配置失败,请检查网络状况或稍后再试。"
if self.debug_mode:
error_msg += f"\nJSON解析失败: {e}"
self.finished.emit(None, error_msg)
finally:
if self.debug_mode:
print("--- Finished fetching cloud config ---")

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)

View File

@@ -38,7 +38,7 @@ class IpOptimizer:
"-url", url, # 指定测速地址
"-f", ip_txt_path, # IP文件
"-dd", # 禁用下载测速,按延迟排序
"-o",
"-o","" # 不写入结果文件
]
creation_flags = subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0
@@ -119,10 +119,8 @@ class IpOptimizer:
if match and not optimal_ip: # 只保存第一个匹配的IP最优IP
optimal_ip = match.group(1)
print(f"找到最优 IP: {optimal_ip}")
# 如果已经看到完成标记,可以退出了
if found_completion:
break
# 找到最优IP后立即退出循环不等待完成标记
break
except Exception as e:
print(f"读取输出时发生错误: {e}")