Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c941c03446 | ||
|
|
5ad4062346 | ||
|
|
cbfe0d7ff6 | ||
|
|
c837370470 | ||
|
|
db9736cc4e | ||
|
|
a411461f63 | ||
|
|
331f7a25d2 | ||
|
|
642b2ec17f | ||
|
|
41aab89669 | ||
|
|
f6a57215c2 | ||
|
|
38549e098e | ||
|
|
286270a819 | ||
|
|
0f9c91b59a | ||
|
|
3753375bed | ||
|
|
dab2ba2dc5 | ||
|
|
f86cb7aa7e | ||
|
|
0cf9f5e6c2 | ||
|
|
f9715f91f7 | ||
|
|
98e51d443e |
89
PRIVACY.md
Normal 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日
|
||||
@@ -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!
|
||||
```
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# 🍓FRAISEMOE-Addons-Installer-NEXT🍓
|
||||
|
||||
```
|
||||
🔊 注意:本库仍然努力更新中,大部分文档不可用,敬请谅解。
|
||||
🔊 注意:本库文档更新已趋于稳定,如有遗漏部分请及时提出issue,感谢各位支持!
|
||||
```
|
||||
|
||||
<!-- PROJECT SHIELDS -->
|
||||
|
||||
BIN
source/IMG/BG/title_bg1.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
source/IMG/BG/title_bg2.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
source/IMG/BTN/Button.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
source/IMG/ICO/cloudflare_logo_icon.ico
Normal file
|
After Width: | Height: | Size: 66 KiB |
@@ -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())
|
||||
@@ -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'
|
||||
]
|
||||
@@ -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()
|
||||
|
||||
169
source/core/config_manager.py
Normal 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
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
311
source/core/game_detector.py
Normal 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
|
||||
253
source/core/patch_manager.py
Normal 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,
|
||||
)
|
||||
226
source/core/privacy_manager.py
Normal file
@@ -0,0 +1,226 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
import json
|
||||
from PySide6.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QTextBrowser, QPushButton, QCheckBox, QLabel, QMessageBox
|
||||
from PySide6.QtCore import Qt
|
||||
|
||||
from data.privacy_policy import PRIVACY_POLICY_BRIEF, get_local_privacy_policy, PRIVACY_POLICY_VERSION
|
||||
from data.config import CACHE, APP_NAME, APP_VERSION
|
||||
from utils import msgbox_frame
|
||||
from utils.logger import setup_logger
|
||||
|
||||
class PrivacyManager:
|
||||
"""隐私协议管理器,负责显示隐私协议对话框并处理用户选择"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化隐私协议管理器"""
|
||||
# 初始化日志
|
||||
self.logger = setup_logger("privacy_manager")
|
||||
self.logger.info("正在初始化隐私协议管理器")
|
||||
# 确保缓存目录存在
|
||||
os.makedirs(CACHE, exist_ok=True)
|
||||
self.config_file = os.path.join(CACHE, "privacy_config.json")
|
||||
self.privacy_config = self._load_privacy_config()
|
||||
|
||||
# 获取隐私协议内容和版本
|
||||
self.logger.info("读取本地隐私协议文件")
|
||||
self.privacy_content, self.current_privacy_version, error = get_local_privacy_policy()
|
||||
if error:
|
||||
self.logger.warning(f"读取本地隐私协议文件警告: {error}")
|
||||
# 使用默认版本作为备用
|
||||
self.current_privacy_version = PRIVACY_POLICY_VERSION
|
||||
self.logger.info(f"隐私协议版本: {self.current_privacy_version}")
|
||||
|
||||
# 检查隐私协议版本和用户同意状态
|
||||
self.privacy_accepted = self._check_privacy_acceptance()
|
||||
|
||||
def _load_privacy_config(self):
|
||||
"""加载隐私协议配置
|
||||
|
||||
Returns:
|
||||
dict: 隐私协议配置信息
|
||||
"""
|
||||
if os.path.exists(self.config_file):
|
||||
try:
|
||||
with open(self.config_file, "r", encoding="utf-8") as f:
|
||||
config = json.load(f)
|
||||
return config
|
||||
except (json.JSONDecodeError, IOError) as e:
|
||||
self.logger.error(f"读取隐私配置失败: {e}")
|
||||
# 如果读取失败,返回空配置,强制显示隐私协议
|
||||
return {"privacy_accepted": False}
|
||||
return {"privacy_accepted": False}
|
||||
|
||||
def _check_privacy_acceptance(self):
|
||||
"""检查隐私协议是否需要重新同意
|
||||
|
||||
如果隐私协议版本变更,则需要重新同意
|
||||
|
||||
Returns:
|
||||
bool: 是否已有有效的隐私协议同意
|
||||
"""
|
||||
# 获取存储的版本信息
|
||||
stored_privacy_version = self.privacy_config.get("privacy_version", "0.0.0")
|
||||
stored_app_version = self.privacy_config.get("app_version", "0.0.0")
|
||||
privacy_accepted = self.privacy_config.get("privacy_accepted", False)
|
||||
|
||||
self.logger.info(f"存储的隐私协议版本: {stored_privacy_version}, 当前版本: {self.current_privacy_version}")
|
||||
self.logger.info(f"存储的应用版本: {stored_app_version}, 当前版本: {APP_VERSION}")
|
||||
self.logger.info(f"隐私协议接受状态: {privacy_accepted}")
|
||||
|
||||
# 如果隐私协议版本变更,需要重新同意
|
||||
if stored_privacy_version != self.current_privacy_version:
|
||||
self.logger.info("隐私协议版本已变更,需要重新同意")
|
||||
return False
|
||||
|
||||
# 返回当前的同意状态
|
||||
return privacy_accepted
|
||||
|
||||
def _save_privacy_config(self, accepted):
|
||||
"""保存隐私协议配置
|
||||
|
||||
Args:
|
||||
accepted: 用户是否同意隐私协议
|
||||
|
||||
Returns:
|
||||
bool: 配置是否保存成功
|
||||
"""
|
||||
try:
|
||||
# 确保目录存在
|
||||
os.makedirs(os.path.dirname(self.config_file), exist_ok=True)
|
||||
|
||||
# 写入配置文件,包含应用版本和隐私协议版本
|
||||
with open(self.config_file, "w", encoding="utf-8") as f:
|
||||
json.dump({
|
||||
"privacy_accepted": accepted,
|
||||
"privacy_version": self.current_privacy_version, # 保存当前隐私协议版本
|
||||
"app_version": APP_VERSION # 保存当前应用版本
|
||||
}, f, indent=2)
|
||||
|
||||
# 更新实例变量
|
||||
self.privacy_accepted = accepted
|
||||
self.privacy_config = {
|
||||
"privacy_accepted": accepted,
|
||||
"privacy_version": self.current_privacy_version,
|
||||
"app_version": APP_VERSION
|
||||
}
|
||||
return True
|
||||
except IOError as e:
|
||||
self.logger.error(f"保存隐私协议配置失败: {e}")
|
||||
# 显示保存失败的提示
|
||||
QMessageBox.warning(
|
||||
None,
|
||||
f"配置保存警告 - {APP_NAME}",
|
||||
f"隐私设置无法保存到配置文件,下次启动时可能需要重新确认。\n\n错误信息:{e}"
|
||||
)
|
||||
return False
|
||||
|
||||
def show_privacy_dialog(self):
|
||||
"""显示隐私协议对话框
|
||||
|
||||
Returns:
|
||||
bool: 用户是否同意隐私协议
|
||||
"""
|
||||
# 如果用户已经同意了隐私协议,直接返回True不显示对话框
|
||||
if self.privacy_accepted:
|
||||
self.logger.info("用户已同意当前版本的隐私协议,无需再次显示")
|
||||
return True
|
||||
|
||||
self.logger.info("首次运行或隐私协议版本变更,显示隐私对话框")
|
||||
|
||||
# 创建隐私协议对话框
|
||||
dialog = QDialog()
|
||||
dialog.setWindowTitle(f"隐私政策 - {APP_NAME}")
|
||||
dialog.setMinimumSize(600, 400)
|
||||
dialog.setWindowFlags(dialog.windowFlags() & ~Qt.WindowContextHelpButtonHint)
|
||||
|
||||
# 创建布局
|
||||
layout = QVBoxLayout(dialog)
|
||||
|
||||
# 添加标题和版本信息
|
||||
title_label = QLabel(f"请阅读并同意以下隐私政策 (更新日期: {self.current_privacy_version})")
|
||||
title_label.setStyleSheet("font-size: 14px; font-weight: bold;")
|
||||
layout.addWidget(title_label)
|
||||
|
||||
# 添加隐私协议文本框
|
||||
text_browser = QTextBrowser()
|
||||
# 这里使用PRIVACY_POLICY_BRIEF而不是self.privacy_content,保持UI简洁
|
||||
text_browser.setMarkdown(PRIVACY_POLICY_BRIEF)
|
||||
text_browser.setOpenExternalLinks(True)
|
||||
layout.addWidget(text_browser)
|
||||
|
||||
# 添加同意选择框
|
||||
checkbox = QCheckBox("我已阅读并同意上述隐私政策")
|
||||
layout.addWidget(checkbox)
|
||||
|
||||
# 添加按钮
|
||||
buttons_layout = QHBoxLayout()
|
||||
agree_button = QPushButton("同意并继续")
|
||||
agree_button.setEnabled(False) # 初始状态为禁用
|
||||
decline_button = QPushButton("不同意并退出")
|
||||
buttons_layout.addWidget(agree_button)
|
||||
buttons_layout.addWidget(decline_button)
|
||||
layout.addLayout(buttons_layout)
|
||||
|
||||
# 连接选择框状态变化 - 修复勾选后按钮不亮起的问题
|
||||
def on_checkbox_state_changed(state):
|
||||
self.logger.debug(f"复选框状态变更为: {state}")
|
||||
agree_button.setEnabled(state == 2) # Qt.Checked 在 PySide6 中值为 2
|
||||
|
||||
checkbox.stateChanged.connect(on_checkbox_state_changed)
|
||||
|
||||
# 连接按钮点击事件
|
||||
agree_button.clicked.connect(lambda: self._on_agree(dialog))
|
||||
decline_button.clicked.connect(lambda: self._on_decline(dialog))
|
||||
|
||||
# 显示对话框
|
||||
result = dialog.exec()
|
||||
|
||||
# 返回用户选择结果
|
||||
return self.privacy_accepted
|
||||
|
||||
def _on_agree(self, dialog):
|
||||
"""处理用户同意隐私协议
|
||||
|
||||
Args:
|
||||
dialog: 对话框实例
|
||||
"""
|
||||
# 保存配置并更新状态
|
||||
self._save_privacy_config(True)
|
||||
dialog.accept()
|
||||
|
||||
def _on_decline(self, dialog):
|
||||
"""处理用户拒绝隐私协议
|
||||
|
||||
Args:
|
||||
dialog: 对话框实例
|
||||
"""
|
||||
# 显示拒绝信息
|
||||
msg_box = msgbox_frame(
|
||||
f"退出 - {APP_NAME}",
|
||||
"\n您需要同意隐私政策才能使用本软件。\n软件将立即退出。\n",
|
||||
QMessageBox.Ok,
|
||||
)
|
||||
msg_box.exec()
|
||||
|
||||
# 保存拒绝状态
|
||||
self._save_privacy_config(False)
|
||||
dialog.reject()
|
||||
|
||||
def is_privacy_accepted(self):
|
||||
"""检查用户是否已同意隐私协议
|
||||
|
||||
Returns:
|
||||
bool: 用户是否已同意隐私协议
|
||||
"""
|
||||
return self.privacy_accepted
|
||||
|
||||
def reset_privacy_agreement(self):
|
||||
"""重置隐私协议同意状态,用于测试或重新显示隐私协议
|
||||
|
||||
Returns:
|
||||
bool: 重置是否成功
|
||||
"""
|
||||
return self._save_privacy_config(False)
|
||||
@@ -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}",
|
||||
|
||||
141
source/core/window_manager.py
Normal 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)
|
||||
@@ -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",
|
||||
|
||||
94
source/data/privacy_policy.py
Normal 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, "无法读取本地隐私协议文件"
|
||||
BIN
source/fonts/SmileySans-Oblique.ttf
Normal 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)
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 264 KiB |
|
Before Width: | Height: | Size: 162 KiB |
|
Before Width: | Height: | Size: 179 KiB |
|
Before Width: | Height: | Size: 571 KiB |
|
Before Width: | Height: | Size: 250 KiB |
|
Before Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 229 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 264 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 177 KiB |
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 327 KiB |
|
Before Width: | Height: | Size: 166 KiB |
|
Before Width: | Height: | Size: 184 KiB |
|
Before Width: | Height: | Size: 197 KiB |
|
Before Width: | Height: | Size: 166 KiB |
@@ -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
@@ -0,0 +1,5 @@
|
||||
from .Ui_install import Ui_MainWindows
|
||||
|
||||
__all__ = [
|
||||
'Ui_MainWindows'
|
||||
]
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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',
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
@@ -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 ---")
|
||||
@@ -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)
|
||||
|
||||
@@ -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}")
|
||||
|
||||