42 Commits

Author SHA1 Message Date
欧阳淇淇
803f32b567 ci(build-release): 放宽触发条件以支持任意标签
将 GitHub Actions 工作流的触发条件从仅限 'v*' 标签更改为
任意标签都会触发构建,提高发布流程的灵活性。
2025-12-17 23:46:50 +08:00
欧阳淇淇
ca6ef6443b ci(build-release): 添加图标文件验证步骤并修复图标路径配置
在构建发布工作流中添加了图标文件存在性验证步骤,确保构建时图标文件存在。
同时修改了 build.spec 文件中的图标路径配置方式,使用相对路径替代原有的绝对路径拼接方式,
以提高配置的可靠性和可维护性。
2025-12-17 23:43:56 +08:00
欧阳淇淇
ef7763dea0 refactor(extraction_thread): 优化压缩包内容分析的日志输出格式
将文件类型判断逻辑提取为变量,提高代码可读性。
2025-12-17 23:37:34 +08:00
欧阳淇淇
4b170b14f4 build(spec): 更新构建配置以包含新的模块和PySide6依赖
新增对 core.managers、core.handlers 和 ui.components 模块的子模块收集,
并移除旧的 handlers 模块引用。同时添加 PySide6 相关的隐藏导入依赖,
确保打包时包含必要的 Qt 模块。
2025-12-17 23:29:49 +08:00
欧阳淇淇
7f8e94fc25 build(gitignore): 添加 source/build.spec 到版本控制
新增 PyInstaller 构建配置文件 build.spec,用于定义应用打包时的模块、资源及可执行文件属性。
同时更新 .gitignore 以确保该配置文件被纳入版本管理。
2025-12-17 23:23:32 +08:00
欧阳淇淇
ba653b3470 build(workflow): 简化PyInstaller构建命令
将GitHub工作流中的PyInstaller构建步骤从冗长的手动参数调用改为使用build.spec配置文件,
以提高构建脚本的可维护性和清晰度。
2025-12-17 23:18:55 +08:00
欧阳淇淇
b1807abff7 build(workflow): 更新构建工作流配置以包含新增模块和资源
在 PyInstaller 构建命令中添加了新的数据目录 (bin, ui) 和多个隐藏导入模块,
包括 workers、core、handlers、utils 和 ui 等子模块。同时使用 --collect-submodules
选项确保所有相关子模块被正确收集,以解决打包后可能缺失依赖的问题。
2025-12-17 23:13:23 +08:00
欧阳淇淇
75ac82cb41 feat(ui): 为多个按钮文本添加颜色样式
在Ui_install.py中为“开始安装”、“禁/启用补丁”、“卸载补丁”和“退出程序”按钮
文本添加了颜色样式(color: #3333),以提升界面视觉效果。
2025-12-17 23:04:36 +08:00
欧阳淇淇
e75f52ab9c feat(workflow): 添加构建和发布 GitHub Actions 工作流
新增一个 GitHub Actions 配置文件,用于在推送版本标签时自动构建并发布应用。
该工作流使用 PyInstaller 打包 Windows 可执行文件,并上传为 Release 资源。
2025-12-17 22:45:49 +08:00
hyb-oyqq
adcd6da5b4 feat(core): 增强工作模式菜单状态同步功能
- 在主窗口和离线模式管理器中添加工作模式菜单状态同步逻辑,确保UI状态与实际工作模式一致。
- 在UI管理器中实现同步方法,提升菜单状态更新的可靠性和兼容性。
- 优化代码结构,确保在菜单创建后立即同步状态,增强用户体验。
2025-08-15 14:06:22 +08:00
hyb-oyqq
d7ceb71607 feat(privacy): 更新隐私政策,增加离线模式说明
- 在隐私政策中新增离线模式的相关说明,明确应用在离线状态下的行为。
- 更新隐私政策版本号至2025年8月15日。
2025-08-15 11:05:15 +08:00
hyb-oyqq
41e3c88a2f feat(core): 重构下载管理器,合并下载管理相关模块
- 将 DownloadManager 和 DownloadTaskManager 合并,简化下载管理逻辑。
- 更新导入路径,修正相关模块的引用,确保代码一致性。
- 移除不再使用的下载管理模块,提升代码可维护性和可读性。
2025-08-13 18:36:00 +08:00
hyb-oyqq
66403406db style(core): 调整代码缩进格式
- 修正 ExtractionHandler 类中的缩进问题
- 修正 CloudflareOptimizer 类中的多个缩进问题
- 统一使用一致的缩进格式,提高代码可读性
2025-08-13 12:48:25 +08:00
hyb-oyqq
e82e5dcd63 feat(core): 优化UI管理器,增强组件初始化和菜单构建
- 移除不再使用的UI组件和方法,简化代码结构。
- 引入新的UI组件管理类,提升UI组件的初始化和菜单构建逻辑。
- 更新加载对话框和消息框的创建逻辑,确保使用统一的对话框工厂方法。
- 保留向后兼容性,添加委托方法以支持旧功能,提升用户体验。
2025-08-13 12:38:37 +08:00
hyb-oyqq
979c23f8b8 feat(core): 增强配置管理和日志文件处理功能
- 在ConfigManager中添加切换禁用安装前哈希预检查的功能,支持通过主窗口更新配置并保存。
- 在DebugManager中实现打开日志文件的功能,增加对日志文件存在性和大小的检查,提供用户友好的提示。
- 在UIManager中更新信号连接,确保调试管理器的正确初始化,并优化哈希预检查的状态切换逻辑,提升代码可读性和用户体验。
2025-08-13 12:06:31 +08:00
hyb-oyqq
d07ef20e51 feat(core): 更新窗口状态管理,统一使用window_manager
- 在多个模块中,将安装按钮状态管理从ui_manager迁移至window_manager,确保窗口状态的一致性和可维护性。
- 优化了ExtractionHandler、CloudflareOptimizer、DownloadManager、OfflineModeManager、PatchDetector等类中的状态更新逻辑,提升了代码的可读性和一致性。
2025-08-13 11:58:43 +08:00
hyb-oyqq
43a66f66a9 feat(core): 更新日志记录级别,增强调试信息
- 将多个模块中的日志记录级别从info调整为debug,以减少生产环境中的日志噪声,同时在调试模式下提供更详细的信息。
- 更新了主窗口、下载管理器、隐私管理器等多个文件的日志记录,确保在调试过程中能够获取到更丰富的上下文信息,便于问题排查和用户反馈。
2025-08-13 11:45:28 +08:00
hyb-oyqq
ac2b0112e8 feat(core): 移除资源验证功能,简化主窗口代码
- 删除了关键资源验证和测试功能,优化了主窗口的代码结构。
- 资源加载流程将通过正常的加载机制进行,提升了代码的可读性和维护性。
2025-08-12 18:22:55 +08:00
hyb-oyqq
a97cdf4c23 feat(core): 优化主窗口信号连接和状态管理
- 更新主窗口信号连接,使用私有方法处理关闭和最小化按钮点击事件,增强代码可读性。
- 根据离线模式和配置状态统一管理开始安装按钮的状态,简化逻辑。
- 增强日志记录,确保在用户操作时提供详细的调试信息,便于后续排查和用户反馈。
- 优化卸载处理程序的日志记录,提升用户体验和系统稳定性。
2025-08-12 18:02:10 +08:00
hyb-oyqq
4f2217ca95 feat(core): 优化主窗口和UI管理功能
- 移除不再使用的UI组件,简化主窗口代码结构。
- 更新按钮状态管理,统一通过UIManager控制安装按钮状态,提升代码可读性。
- 优化解压和下载管理逻辑,确保在操作过程中提供清晰的用户反馈。
- 增强日志记录,确保在关键操作中提供详细的调试信息,便于后续排查和用户反馈。
2025-08-12 17:11:09 +08:00
hyb-oyqq
2c91319d5f feat(core): 增强加载对话框和哈希验证功能
- 在主窗口中添加显示和隐藏加载对话框的方法,提升用户体验。
- 更新补丁切换处理程序,增加调试模式参数以优化批量操作。
- 在离线模式管理器中增强哈希校验失败的日志记录,提供更详细的错误信息。
- 优化解压线程,增加对签名文件的处理逻辑,确保补丁安装的完整性和准确性。
- 在哈希验证线程中添加超时检测和进度更新,提升验证过程的可控性和用户反馈。
2025-08-12 15:49:43 +08:00
hyb-oyqq
1b6d275433 feat(core): 增强主窗口功能和解压处理
- 在主窗口中添加解压进度窗口和解压线程创建功能,提升用户体验。
- 优化退出逻辑,确保在用户确认退出后和强制退出时均执行hosts相关操作。
- 移除不必要的hosts操作,简化代码结构。
- 更新UI管理器,确保加载对话框的显示和隐藏逻辑更加清晰。
2025-08-12 13:14:32 +08:00
hyb-oyqq
7d71ffe099 feat(core): 增强卸载处理程序的UI反馈和异常日志记录
- 在卸载处理程序中使用UI管理器显示和隐藏加载对话框,提升用户体验。
- 增加异常钩子,确保未捕获的异常能够记录到日志文件中,增强系统的稳定性和可追溯性。
2025-08-11 17:54:14 +08:00
hyb-oyqq
68bbafc564 feat(core): 优化主窗口和管理器功能
- 在主窗口中重构初始化逻辑,增强UI组件的管理和信号连接,提升代码可读性。
- 添加资源验证和加载测试功能,确保关键资源文件的完整性和可用性。
- 更新下载管理器和离线模式管理器,优化线程管理和状态更新,提升用户体验。
- 增强日志记录,确保在关键操作中提供详细的调试信息,便于后续排查和用户反馈。
- 删除不再使用的进度窗口创建函数,改为由UIManager管理,提升代码整洁性。
2025-08-11 17:42:52 +08:00
hyb-oyqq
dc433a2ac9 feat(core): 更新.gitignore和清理无用文件
- 在.gitignore中添加source/STRUCTURE.md以优化文件管理。
- 删除不再使用的ui_manager.py文件,提升项目整洁性。
- 在Main.py中调整代码格式,确保一致性。
2025-08-11 16:14:34 +08:00
hyb-oyqq
4d847f58d0 feat(core): 更新配置管理和文件忽略规则
- 修改.gitignore文件,添加对Python缓存文件、虚拟环境和系统文件的忽略规则,提升项目整洁性。
- 更新主窗口和相关模块的导入路径,确保从新的配置模块中正确导入配置项,增强代码结构的清晰度。
- 删除不再使用的图片和模块,优化项目资源,减少冗余文件,提升维护效率。
2025-08-11 16:13:58 +08:00
hyb-oyqq
6a4c6ca1f1 feat(core): 优化线程管理和清理机制
- 在主窗口中添加优雅的线程清理逻辑,确保在退出时安全停止所有后台线程,避免潜在的资源泄漏。
- 更新离线模式管理器和哈希线程,增强对线程引用的管理,确保在操作完成后及时清理引用。
- 改进补丁检测器,支持在离线模式下的补丁状态检查,提升用户体验和系统稳定性。
- 增强日志记录,确保在关键操作中提供详细的调试信息,便于后续排查和用户反馈。
2025-08-11 14:42:38 +08:00
hyb-oyqq
f0031ed17c feat(core): 增强补丁管理和进度反馈功能
- 在主窗口中添加解压进度窗口,提供用户友好的解压反馈。
- 更新补丁检测器和下载管理器,支持异步游戏目录识别和补丁状态检查,提升用户体验。
- 优化哈希验证和解压流程,确保在关键操作中提供详细的进度信息和错误处理。
- 增强日志记录,确保在补丁管理过程中记录详细的调试信息,便于后续排查和用户反馈。
2025-08-11 11:14:38 +08:00
hyb-oyqq
09d6883432 feat(core): 优化解压和哈希验证流程
- 在解压线程中添加已解压文件路径参数,支持直接使用已解压的补丁文件,提升解压效率。
- 更新下载管理器,简化下载成功后的处理逻辑,直接进入解压阶段,去除冗余的哈希验证步骤。
- 在离线模式管理器中增强哈希验证功能,确保在解压后进行哈希校验,提升补丁文件的完整性检查。
- 增强日志记录,确保在关键操作中提供详细的调试信息,便于后续排查和用户反馈。
2025-08-08 11:27:11 +08:00
hyb-oyqq
ee72f76952 feat(ui): 添加Qt核心组件以支持进度对话框功能
- 在helpers.py中引入Qt核心和窗口组件,准备实现进度对话框功能。
- 更新导入模块,提升代码结构和可维护性。
2025-08-07 18:25:54 +08:00
hyb-oyqq
ba5e3cdbc1 feat(core): 增强哈希验证机制和进度反馈
- 在下载管理器和离线模式管理器中集成哈希验证功能,确保补丁文件的完整性。
- 添加进度对话框以显示哈希验证过程,提升用户体验。
- 优化哈希验证线程,支持进度更新和错误处理,确保在验证失败时提供清晰反馈。
- 更新相关逻辑以支持新功能,提升代码可维护性和可读性。
2025-08-07 18:22:22 +08:00
hyb-oyqq
575116e45c feat(core): 增强离线模式和日志记录功能
- 在主窗口中添加离线模式提示弹窗,用户可清晰了解离线模式切换情况。
- 更新离线模式管理器,优化补丁文件扫描和日志记录,确保无论调试模式下均能记录相关信息。
- 在下载管理器和补丁管理器中增强调试信息的记录,提升错误处理能力。
- 更新卸载处理程序,增加详细的日志记录,确保用户操作的透明性和可追溯性。
2025-08-07 16:10:11 +08:00
hyb-oyqq
bf80c19fe1 feat(core): 集成补丁检测器以增强补丁管理功能
- 在主窗口中添加补丁检测器,支持补丁的检测和验证。
- 更新补丁管理器以使用补丁检测器进行补丁安装状态检查。
- 优化下载管理器和离线模式管理器,整合补丁检测逻辑,提升用户体验。
- 添加进度窗口以显示下载状态,增强用户反馈。
- 重构相关逻辑以支持新功能,确保代码可维护性和可读性。
2025-08-07 15:24:22 +08:00
欧阳淇淇
d12739baab feat(core): 增强日志记录和错误处理功能
- 更新日志记录机制,将日志文件存储在程序根目录下的log文件夹中,并使用日期+时间戳格式命名。
- 在多个模块中添加详细的错误处理逻辑,确保在发生异常时能够记录相关信息,便于后续排查。
- 优化UI管理器中的日志文件打开功能,增加对日志文件存在性和大小的检查,提升用户体验。
- 在下载管理器和补丁管理器中增强调试信息的记录,确保在关键操作中提供更清晰的反馈。
2025-08-07 00:31:24 +08:00
hyb-oyqq
19cdd5b8cd feat(ui): 优化游戏选择对话框和离线模式菜单
- 重构游戏选择对话框,使用列表控件替代复选框,提升用户体验。
- 添加全选按钮功能,简化游戏选择操作。
- 更新离线模式管理器和UI管理器,确保菜单状态与当前模式同步。
2025-08-06 17:51:37 +08:00
hyb-oyqq
dfdeb54b43 feat(core): 集成日志记录功能以增强调试和错误处理
- 在多个模块中添加日志记录功能,替代原有的打印输出,提升调试信息的管理。
- 更新主窗口、下载管理器、离线模式管理器等,确保在关键操作中记录详细日志。
- 优化异常处理逻辑,确保在发生错误时能够记录相关信息,便于后续排查。
- 增强用户体验,通过日志记录提供更清晰的操作反馈和状态信息。
2025-08-06 17:16:21 +08:00
hyb-oyqq
7befe19f30 feat(core): 增强离线模式支持和版本管理
- 在主窗口中添加离线模式管理器,支持自动切换到离线模式。
- 更新下载管理器以处理离线模式下的下载逻辑,确保用户体验流畅。
- 添加版本警告机制,提示用户在版本过低时的操作选项。
- 优化配置管理器,确保在离线模式下仍可使用相关功能。
- 更新UI管理器以反映当前工作模式,提升用户界面友好性。
2025-08-06 15:22:44 +08:00
hyb-oyqq
b18f4a276c feat(ui): 拆分臃肿脚本
- 更新按钮事件连接,使用新的处理程序替代原有逻辑。
- 增强代码结构,提升可维护性和可读性。
2025-08-05 16:31:20 +08:00
hyb-oyqq
534f7381bd feat(core): 修复下载模块的bug
- 修复下载模块缺失部分模块的bug
2025-08-05 11:19:00 +08:00
hyb-oyqq
2158331532 feat(ui): 添加禁/启用补丁按钮及其功能
- 新增禁/启用补丁功能
- 更新动画模块以支持禁/启用补丁按钮的动画效果。
- 在下载模块中添加对禁用补丁游戏的检测和处理逻辑,优化用户体验。
- 扩展补丁管理模块,支持批量切换补丁的启用/禁用状态。
- 更新UI布局
2025-08-05 10:59:19 +08:00
hyb-oyqq
8b4dedc8c6 feat(core): 优化主程序和下载管理器逻辑
- 移除冗余注释,简化代码可读性。
- 更新隐私协议管理器的异常处理逻辑,确保用户体验流畅。
- 改进下载管理器中的下载流程,优化用户选择游戏的对话框逻辑。
- 调整下载线程设置,确保更高效的下载管理。
2025-08-04 12:46:59 +08:00
hyb-oyqq
98bfddeb04 feat(core): 增强hosts管理功能和自动还原设置
- 添加对hosts文件优选IP记录的检查,避免重复优选。
- 添加禁用自动还原hosts的选项,允许用户自定义设置。
- 更新HostsManager以支持自动还原状态的设置和检查,优化hosts文件的管理逻辑。
2025-08-04 11:44:10 +08:00
77 changed files with 9270 additions and 2793 deletions

65
.github/workflows/build-release.yml vendored Normal file
View File

@@ -0,0 +1,65 @@
name: Build and Release
on:
push:
tags:
- '*' # 任意 tag 都会触发构建
jobs:
build:
runs-on: windows-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
cache: 'pip'
cache-dependency-path: 'source/requirements.txt'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r source/requirements.txt
- name: Get version from tag
id: get_version
run: |
$version = "${{ github.ref_name }}"
echo "VERSION=$version" >> $env:GITHUB_OUTPUT
- name: Verify icon file
run: |
cd source
if (Test-Path "assets/images/ICO/icon.ico") {
Write-Host "Icon file found"
Get-Item "assets/images/ICO/icon.ico"
} else {
Write-Host "Icon file NOT found!"
exit 1
}
- name: Build with PyInstaller
run: |
cd source
pyinstaller --noconfirm build.spec
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: release-${{ steps.get_version.outputs.VERSION }}
path: source/dist/FRAISEMOE_Addons_Installer_NEXT.exe
- name: Create Release
uses: softprops/action-gh-release@v2
with:
name: release-${{ steps.get_version.outputs.VERSION }}
files: source/dist/FRAISEMOE_Addons_Installer_NEXT.exe
draft: false
prerelease: false
generate_release_notes: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

15
.gitignore vendored
View File

@@ -31,6 +31,7 @@ MANIFEST
# before PyInstaller builds the exe, so as to inject date/other infos into it. # before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest *.manifest
*.spec *.spec
!source/build.spec
# Installer logs # Installer logs
pip-log.txt pip-log.txt
@@ -174,3 +175,17 @@ nuitka-crash-report.xml
build.bat build.bat
log.txt log.txt
result.csv result.csv
after.7z
vol.1.7z
vol.2.7z
vol.3.7z
vol.4.7z
log/
__pycache__/
*.py[cod]
venv/
.venv/
Thumbs.db
.DS_Store
STRUCTURE.md
source/STRUCTURE.md

View File

@@ -21,6 +21,10 @@
- 游戏安装路径:用于识别已安装的游戏和安装补丁 - 游戏安装路径:用于识别已安装的游戏和安装补丁
- 文件哈希值:用于验证文件完整性 - 文件哈希值:用于验证文件完整性
### 2.4 离线模式和本地文件
- **离线模式**:本应用提供离线模式。在离线模式下,应用不会进行任何网络连接,包括检查更新、获取云端配置或进行任何网络相关的测试,安装过程将只使用本地文件。
- **本地文件使用**:为了支持离线安装,本应用会扫描其所在目录下的压缩包,以查找用于安装的补丁压缩包。此文件扫描操作仅限于应用所在的文件夹,不会访问或修改您系统中的其他文件。
## 3. 信息使用 ## 3. 信息使用
我们收集的信息仅用于以下目的: 我们收集的信息仅用于以下目的:
@@ -88,4 +92,4 @@
本隐私政策可能会根据应用功能的变化而更新。请定期查看最新版本。 本隐私政策可能会根据应用功能的变化而更新。请定期查看最新版本。
最后更新日期2025年8月4 最后更新日期2025年8月15

View File

@@ -1,21 +1,86 @@
import sys import sys
import os
import datetime
import traceback
from PySide6.QtWidgets import QApplication, QMessageBox from PySide6.QtWidgets import QApplication, QMessageBox
from main_window import MainWindow from main_window import MainWindow
from core.privacy_manager import PrivacyManager from core.managers.privacy_manager import PrivacyManager
from utils.logger import setup_logger from utils.logger import setup_logger, cleanup_old_logs, log_uncaught_exceptions
from config.config import LOG_FILE, APP_NAME, LOG_RETENTION_DAYS
from utils import load_config
def excepthook(exc_type, exc_value, exc_traceback):
"""全局异常处理钩子,将未捕获的异常记录到日志并显示错误对话框"""
# 记录异常到日志
if hasattr(sys, '_excepthook'):
sys._excepthook(exc_type, exc_value, exc_traceback)
else:
log_uncaught_exceptions(exc_type, exc_value, exc_traceback)
# 将异常格式化为易读的形式
exception_text = ''.join(traceback.format_exception(exc_type, exc_value, exc_traceback))
# 创建错误对话框
msg = f"程序遇到未处理的异常:\n\n{str(exc_value)}\n\n详细错误已记录到日志文件。"
try:
# 尝试使用QMessageBox显示错误
app = QApplication.instance()
if app:
QMessageBox.critical(None, f"错误 - {APP_NAME}", msg)
except Exception:
# 如果QMessageBox失败则使用标准输出
print(f"严重错误: {msg}")
print(f"详细错误: {exception_text}")
if __name__ == "__main__": if __name__ == "__main__":
# 初始化日志 # 设置主日志
logger = setup_logger("main") logger = setup_logger("main")
logger.info("应用启动") logger.info("应用启动")
# 设置全局异常处理钩子
sys._excepthook = sys.excepthook
sys.excepthook = excepthook
# 记录程序启动信息
logger.debug(f"Python版本: {sys.version}")
logger.debug(f"运行平台: {sys.platform}")
# 检查配置中是否启用了调试模式
config = load_config()
debug_mode = config.get("debug_mode", False)
# 在应用启动时清理过期的日志文件
cleanup_old_logs(LOG_RETENTION_DAYS)
logger.debug(f"已执行日志清理,保留最近{LOG_RETENTION_DAYS}天的日志")
# 如果调试模式已启用,确保立即创建主日志文件
if debug_mode:
try:
# 确保log目录存在
log_dir = os.path.dirname(LOG_FILE)
if not os.path.exists(log_dir):
os.makedirs(log_dir, exist_ok=True)
logger.debug(f"已创建日志目录: {log_dir}")
# 记录调试会话信息
logger.debug(f"--- 新调试会话开始于 {os.path.basename(LOG_FILE)} ---")
logger.debug(f"--- 应用版本: {APP_NAME} ---")
current_time = datetime.datetime.now()
formatted_date = current_time.strftime("%Y-%m-%d")
formatted_time = current_time.strftime("%H:%M:%S")
logger.debug(f"--- 日期: {formatted_date} 时间: {formatted_time} ---")
logger.debug(f"调试模式已启用,日志文件路径: {os.path.abspath(LOG_FILE)}")
except Exception as e:
logger.error(f"创建日志文件失败: {e}")
app = QApplication(sys.argv) app = QApplication(sys.argv)
# 初始化隐私协议管理器
try: try:
privacy_manager = PrivacyManager() privacy_manager = PrivacyManager()
except Exception as e: except Exception as e:
logger.error(f"初始化隐私协议管理器失败: {e}") logger.error(f"初始化隐私协议管理器失败: {e}")
logger.error(f"错误详情: {traceback.format_exc()}")
QMessageBox.critical( QMessageBox.critical(
None, None,
"隐私协议加载错误", "隐私协议加载错误",
@@ -23,12 +88,10 @@ if __name__ == "__main__":
) )
sys.exit(1) sys.exit(1)
# 显示隐私协议对话框
if not privacy_manager.show_privacy_dialog(): if not privacy_manager.show_privacy_dialog():
logger.info("用户未同意隐私协议,程序退出") logger.info("用户未同意隐私协议,程序退出")
sys.exit(0) # 如果用户不同意隐私协议,退出程序 sys.exit(0)
# 用户已同意隐私协议,继续启动程序
logger.info("隐私协议已同意,启动主程序") logger.info("隐私协议已同意,启动主程序")
window = MainWindow() window = MainWindow()
window.show() window.show()

View File

@@ -0,0 +1,7 @@
# Assets package initialization
"""
包含应用程序使用的静态资源文件:
- fonts: 字体文件
- images: 图片资源
- resources: 其他资源文件
"""

View File

Before

Width:  |  Height:  |  Size: 162 KiB

After

Width:  |  Height:  |  Size: 162 KiB

View File

Before

Width:  |  Height:  |  Size: 179 KiB

After

Width:  |  Height:  |  Size: 179 KiB

View File

Before

Width:  |  Height:  |  Size: 571 KiB

After

Width:  |  Height:  |  Size: 571 KiB

View File

Before

Width:  |  Height:  |  Size: 250 KiB

After

Width:  |  Height:  |  Size: 250 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

View File

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

View File

Before

Width:  |  Height:  |  Size: 229 KiB

After

Width:  |  Height:  |  Size: 229 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

View File

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

View File

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 66 KiB

View File

Before

Width:  |  Height:  |  Size: 264 KiB

After

Width:  |  Height:  |  Size: 264 KiB

View File

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

View File

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

View File

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 51 KiB

View File

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

View File

Before

Width:  |  Height:  |  Size: 177 KiB

After

Width:  |  Height:  |  Size: 177 KiB

View File

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

View File

Before

Width:  |  Height:  |  Size: 327 KiB

After

Width:  |  Height:  |  Size: 327 KiB

View File

Before

Width:  |  Height:  |  Size: 166 KiB

After

Width:  |  Height:  |  Size: 166 KiB

View File

Before

Width:  |  Height:  |  Size: 184 KiB

After

Width:  |  Height:  |  Size: 184 KiB

View File

Before

Width:  |  Height:  |  Size: 197 KiB

After

Width:  |  Height:  |  Size: 197 KiB

View File

Before

Width:  |  Height:  |  Size: 166 KiB

After

Width:  |  Height:  |  Size: 166 KiB

73
source/build.spec Normal file
View File

@@ -0,0 +1,73 @@
# -*- mode: python ; coding: utf-8 -*-
import os
import sys
from PyInstaller.utils.hooks import collect_submodules, collect_data_files
block_cipher = None
# 收集所有子模块
hiddenimports = []
hiddenimports += collect_submodules('workers')
hiddenimports += collect_submodules('core')
hiddenimports += collect_submodules('core.managers')
hiddenimports += collect_submodules('core.handlers')
hiddenimports += collect_submodules('ui')
hiddenimports += collect_submodules('ui.components')
hiddenimports += collect_submodules('utils')
hiddenimports += collect_submodules('config')
# PySide6 相关隐藏导入
hiddenimports += ['PySide6.QtCore', 'PySide6.QtGui', 'PySide6.QtWidgets']
a = Analysis(
['Main.py'],
pathex=['.'],
binaries=[],
datas=[
('assets', 'assets'),
('data', 'data'),
('config', 'config'),
('bin', 'bin'),
('ui', 'ui'),
('workers', 'workers'),
('core', 'core'),
('utils', 'utils'),
],
hiddenimports=hiddenimports,
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name='FRAISEMOE_Addons_Installer_NEXT',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=False,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
icon=os.path.join('assets', 'images', 'ICO', 'icon.ico'),
)

View File

@@ -0,0 +1 @@

View File

@@ -1,14 +1,15 @@
import os import os
import base64 import base64
import datetime
# 配置信息 # 配置信息
app_data = { app_data = {
"APP_VERSION": "1.3.1", "APP_VERSION": "1.4.2",
"APP_NAME": "FRAISEMOE Addons Installer NEXT", "APP_NAME": "FRAISEMOE Addons Installer NEXT",
"TEMP": "TEMP", "TEMP": "TEMP",
"CACHE": "FRAISEMOE", "CACHE": "FRAISEMOE",
"PLUGIN": "PLUGIN", "PLUGIN": "PLUGIN",
"CONFIG_URL": "aHR0cHM6Ly9hcGkuMncyLnRvcC9hcGkvb3V5YW5ncWlxaS9uZWtvcGFyYS9kb3dubG9hZF91cmwuanNvbg==", "CONFIG_URL": "aHR0cHM6Ly9uZWtvcGFyYS1hcGkub3ZvZmlzaC5jb20vYXBpL291eWFuZ3FpcWkvbmVrb3BhcmEvZG93bmxvYWRfdXJsLmpzb24=",
"UA_TEMPLATE": "Mozilla/5.0 (Linux debian12 FraiseMoe2-Accept-Next) Gecko/20100101 Firefox/114.0 FraiseMoe2/{}", "UA_TEMPLATE": "Mozilla/5.0 (Linux debian12 FraiseMoe2-Accept-Next) Gecko/20100101 Firefox/114.0 FraiseMoe2/{}",
"game_info": { "game_info": {
"NEKOPARA Vol.1": { "NEKOPARA Vol.1": {
@@ -45,24 +46,58 @@ app_data = {
}, },
} }
# Base64解码 def decode_base64(b64str):
def decode_base64(encoded_str): """解码base64字符串"""
return base64.b64decode(encoded_str).decode("utf-8") try:
return base64.b64decode(b64str).decode('utf-8')
except:
return b64str
# 确保缓存目录存在
def ensure_cache_dirs():
os.makedirs(CACHE, exist_ok=True)
os.makedirs(PLUGIN, exist_ok=True)
# 全局变量 # 全局变量
APP_VERSION = app_data["APP_VERSION"]
APP_NAME = app_data["APP_NAME"] APP_NAME = app_data["APP_NAME"]
APP_VERSION = app_data["APP_VERSION"] # 从app_data中获取不再重复定义
TEMP = os.getenv(app_data["TEMP"]) or app_data["TEMP"] TEMP = os.getenv(app_data["TEMP"]) or app_data["TEMP"]
CACHE = os.path.join(TEMP, app_data["CACHE"]) CACHE = os.path.join(TEMP, app_data["CACHE"])
CONFIG_FILE = os.path.join(CACHE, "config.json") CONFIG_FILE = os.path.join(CACHE, "config.json")
LOG_FILE = "log.txt"
# 日志配置
LOG_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "log")
LOG_LEVEL = "DEBUG" # 可选值: DEBUG, INFO, WARNING, ERROR, CRITICAL
# 日志文件大小和轮转配置(新增)
LOG_MAX_SIZE = 10 * 1024 * 1024 # 10MB
LOG_BACKUP_COUNT = 3 # 保留3个备份文件
LOG_RETENTION_DAYS = 7 # 日志保留7天
# 将log文件放在程序根目录下的log文件夹中使用日期+时间戳格式命名
root_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
log_dir = os.path.join(root_dir, "log")
os.makedirs(log_dir, exist_ok=True)
current_datetime = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
LOG_FILE = os.path.join(log_dir, f"log-{current_datetime}.txt")
PLUGIN = os.path.join(CACHE, app_data["PLUGIN"]) PLUGIN = os.path.join(CACHE, app_data["PLUGIN"])
CONFIG_URL = decode_base64(app_data["CONFIG_URL"]) CONFIG_URL = decode_base64(app_data["CONFIG_URL"])
UA = app_data["UA_TEMPLATE"].format(APP_VERSION) UA = app_data["UA_TEMPLATE"].format(APP_VERSION)
GAME_INFO = app_data["game_info"]
# 哈希计算块大小
BLOCK_SIZE = 67108864 BLOCK_SIZE = 67108864
HASH_SIZE = 134217728 HASH_SIZE = 134217728
PLUGIN_HASH = {game: info["hash"] for game, info in GAME_INFO.items()}
# 资源哈希值
GAME_INFO = app_data["game_info"]
PLUGIN_HASH = {
"NEKOPARA Vol.1": GAME_INFO["NEKOPARA Vol.1"]["hash"],
"NEKOPARA Vol.2": GAME_INFO["NEKOPARA Vol.2"]["hash"],
"NEKOPARA Vol.3": GAME_INFO["NEKOPARA Vol.3"]["hash"],
"NEKOPARA Vol.4": GAME_INFO["NEKOPARA Vol.4"]["hash"],
"NEKOPARA After": GAME_INFO["NEKOPARA After"]["hash"]
}
PROCESS_INFO = {info["exe"]: game for game, info in GAME_INFO.items()} PROCESS_INFO = {info["exe"]: game for game, info in GAME_INFO.items()}
# 下载线程档位设置 # 下载线程档位设置

View File

@@ -5,6 +5,10 @@ import os
import re import re
import sys import sys
from datetime import datetime from datetime import datetime
from utils.logger import setup_logger
# 初始化logger
logger = setup_logger("privacy_policy")
# 隐私协议的缩略版内容 # 隐私协议的缩略版内容
PRIVACY_POLICY_BRIEF = """ PRIVACY_POLICY_BRIEF = """
@@ -16,6 +20,7 @@ PRIVACY_POLICY_BRIEF = """
- **系统信息**程序版本号 - **系统信息**程序版本号
- **网络信息**IP 地址ISP地理位置用于使用统计下载统计IPv6 连接测试通过访问 testipv6.cnIPv6 地址获取通过 ipw.cn - **网络信息**IP 地址ISP地理位置用于使用统计下载统计IPv6 连接测试通过访问 testipv6.cnIPv6 地址获取通过 ipw.cn
- **文件信息**游戏安装路径文件哈希值 - **文件信息**游戏安装路径文件哈希值
- **离线模式**在离线模式下本应用不会进行任何网络活动仅使用本地文件进行安装为实现此功能应用会扫描其所在目录下的压缩包文件
## 系统修改 ## 系统修改
- 使用 Cloudflare 加速时会临时修改系统 hosts 文件 - 使用 Cloudflare 加速时会临时修改系统 hosts 文件
@@ -39,6 +44,7 @@ This application collects and processes the following information:
- **System info**: Application version. - **System info**: Application version.
- **Network info**: IP address, ISP, geographic location (for usage statistics), download statistics, IPv6 connectivity test (via testipv6.cn), IPv6 address acquisition (via ipw.cn). - **Network info**: IP address, ISP, geographic location (for usage statistics), download statistics, IPv6 connectivity test (via testipv6.cn), IPv6 address acquisition (via ipw.cn).
- **File info**: Game installation paths, file hash values. - **File info**: Game installation paths, file hash values.
- **Offline Mode**: In offline mode, the application will not perform any network activities and will only use local files for installation. To achieve this, the application scans for compressed files in its directory.
## System Modifications ## System Modifications
- Temporarily modifies system hosts file when using Cloudflare acceleration. - Temporarily modifies system hosts file when using Cloudflare acceleration.
@@ -53,7 +59,7 @@ The complete privacy policy can be found in the program's GitHub repository.
""" """
# 默认隐私协议版本 - 本地版本的日期 # 默认隐私协议版本 - 本地版本的日期
PRIVACY_POLICY_VERSION = "2025.08.04" PRIVACY_POLICY_VERSION = "2025.08.15"
def get_local_privacy_policy(): def get_local_privacy_policy():
"""获取本地打包的隐私协议文件 """获取本地打包的隐私协议文件
@@ -83,14 +89,14 @@ def get_local_privacy_policy():
try: try:
date_obj = datetime.strptime(date_str, '%Y年%m月%d') date_obj = datetime.strptime(date_str, '%Y年%m月%d')
date_version = date_obj.strftime('%Y.%m.%d') date_version = date_obj.strftime('%Y.%m.%d')
print(f"成功读取本地隐私协议文件: {path}, 版本: {date_version}") logger.debug(f"成功读取本地隐私协议文件: {path}, 版本: {date_version}")
return content, date_version, "" return content, date_version, ""
except ValueError: except ValueError:
print(f"本地隐私协议日期格式解析错误: {path}") logger.error(f"本地隐私协议日期格式解析错误: {path}")
else: else:
print(f"本地隐私协议未找到更新日期: {path}") logger.warning(f"本地隐私协议未找到更新日期: {path}")
except Exception as e: except Exception as e:
print(f"读取本地隐私协议失败 {path}: {str(e)}") logger.error(f"读取本地隐私协议失败 {path}: {str(e)}")
# 所有路径都尝试失败,使用默认版本 # 所有路径都尝试失败,使用默认版本
return PRIVACY_POLICY_BRIEF, PRIVACY_POLICY_VERSION, "无法读取本地隐私协议文件" return PRIVACY_POLICY_BRIEF, PRIVACY_POLICY_VERSION, "无法读取本地隐私协议文件"

View File

@@ -1,15 +1,16 @@
from .animations import MultiStageAnimations from .managers.ui_manager import UIManager
from .ui_manager import UIManager from .managers.download_managers import DownloadManager
from .download_manager import DownloadManager from .managers.debug_manager import DebugManager
from .debug_manager import DebugManager from .managers.window_manager import WindowManager
from .window_manager import WindowManager from .managers.game_detector import GameDetector
from .game_detector import GameDetector from .managers.patch_manager import PatchManager
from .patch_manager import PatchManager from .managers.config_manager import ConfigManager
from .config_manager import ConfigManager from .managers.privacy_manager import PrivacyManager
from .privacy_manager import PrivacyManager from .managers.cloudflare_optimizer import CloudflareOptimizer
from .cloudflare_optimizer import CloudflareOptimizer from .managers.download_managers import DownloadTaskManager
from .download_task_manager import DownloadTaskManager from .managers.patch_detector import PatchDetector
from .extraction_handler import ExtractionHandler from .managers.animations import MultiStageAnimations
from .handlers.extraction_handler import ExtractionHandler
__all__ = [ __all__ = [
'MultiStageAnimations', 'MultiStageAnimations',
@@ -23,5 +24,5 @@ __all__ = [
'PrivacyManager', 'PrivacyManager',
'CloudflareOptimizer', 'CloudflareOptimizer',
'DownloadTaskManager', 'DownloadTaskManager',
'ExtractionHandler' 'PatchDetector',
] ]

View File

@@ -1,82 +0,0 @@
import os
import sys
from PySide6 import QtWidgets
from data.config import LOG_FILE
from utils import Logger
class DebugManager:
def __init__(self, main_window):
"""初始化调试管理器
Args:
main_window: 主窗口实例
"""
self.main_window = main_window
self.logger = None
self.original_stdout = None
self.original_stderr = None
self.ui_manager = None # 添加ui_manager属性
def set_ui_manager(self, ui_manager):
"""设置UI管理器引用
Args:
ui_manager: UI管理器实例
"""
self.ui_manager = ui_manager
def _is_debug_mode(self):
"""检查是否处于调试模式
Returns:
bool: 是否处于调试模式
"""
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):
"""切换调试模式
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:
self.stop_logging()
def start_logging(self):
"""启动日志记录"""
if self.logger is None:
try:
if os.path.exists(LOG_FILE):
os.remove(LOG_FILE)
# 保存原始的 stdout 和 stderr
self.original_stdout = sys.stdout
self.original_stderr = sys.stderr
# 创建 Logger 实例
self.logger = Logger(LOG_FILE, self.original_stdout)
sys.stdout = self.logger
sys.stderr = self.logger
print("--- Debug mode enabled ---")
except (IOError, OSError) as e:
QtWidgets.QMessageBox.critical(self.main_window, "错误", f"无法创建日志文件: {e}")
self.logger = None
def stop_logging(self):
"""停止日志记录"""
if self.logger:
print("--- Debug mode disabled ---")
sys.stdout = self.original_stdout
sys.stderr = self.original_stderr
self.logger.close()
self.logger = None

View File

@@ -1,642 +0,0 @@
import os
import requests
import json
from collections import deque
from urllib.parse import urlparse
import re # Added for recursive search
from PySide6 import QtWidgets, QtCore
from PySide6.QtCore import Qt
from PySide6.QtGui import QIcon, QPixmap, QFont
from utils import msgbox_frame, HostsManager, resource_path
from data.config import APP_NAME, PLUGIN, GAME_INFO, UA, CONFIG_URL, DOWNLOAD_THREADS, DEFAULT_DOWNLOAD_THREAD_LEVEL
from workers import IpOptimizerThread
from core.cloudflare_optimizer import CloudflareOptimizer
from core.download_task_manager import DownloadTaskManager
from core.extraction_handler import ExtractionHandler
class DownloadManager:
def __init__(self, main_window):
"""初始化下载管理器
Args:
main_window: 主窗口实例用于访问UI和状态
"""
self.main_window = main_window
self.main_window.APP_NAME = APP_NAME # 为了让子模块能够访问APP_NAME
self.selected_folder = ""
self.download_queue = deque()
self.current_download_thread = None
self.hosts_manager = HostsManager()
# 添加下载线程级别
self.download_thread_level = DEFAULT_DOWNLOAD_THREAD_LEVEL
# 初始化子模块
self.cloudflare_optimizer = CloudflareOptimizer(main_window, self.hosts_manager)
self.download_task_manager = DownloadTaskManager(main_window, self.download_thread_level)
self.extraction_handler = ExtractionHandler(main_window)
def file_dialog(self):
"""显示文件夹选择对话框,选择游戏安装目录"""
self.selected_folder = QtWidgets.QFileDialog.getExistingDirectory(
self.main_window, f"选择游戏所在【上级目录】 {APP_NAME}"
)
if not self.selected_folder:
QtWidgets.QMessageBox.warning(
self.main_window, f"通知 - {APP_NAME}", "\n未选择任何目录,请重新选择\n"
)
return
# 将按钮文本设为安装中状态
self.main_window.ui.start_install_text.setText("正在安装")
# 禁用整个主窗口,防止用户操作
self.main_window.setEnabled(False)
self.download_action()
def get_install_paths(self):
"""获取所有游戏版本的安装路径"""
# 使用改进的目录识别功能
game_dirs = self.main_window.game_detector.identify_game_directories_improved(self.selected_folder)
install_paths = {}
debug_mode = self.is_debug_mode()
for game, info in GAME_INFO.items():
if game in game_dirs:
# 如果找到了游戏目录,使用它
game_dir = game_dirs[game]
install_path = os.path.join(game_dir, os.path.basename(info["install_path"]))
install_paths[game] = install_path
if debug_mode:
print(f"DEBUG: 使用识别到的游戏目录 {game}: {game_dir}")
print(f"DEBUG: 安装路径设置为: {install_path}")
return install_paths
def is_debug_mode(self):
"""检查是否处于调试模式"""
if hasattr(self.main_window, 'ui_manager') and self.main_window.ui_manager:
if hasattr(self.main_window.ui_manager, 'debug_action') and self.main_window.ui_manager.debug_action:
return self.main_window.ui_manager.debug_action.isChecked()
return False
def get_download_url(self) -> dict:
"""获取所有游戏版本的下载链接
Returns:
dict: 包含游戏版本和下载URL的字典
"""
try:
if self.main_window.cloud_config:
if self.is_debug_mode():
print("--- Using pre-fetched cloud config ---")
config_data = self.main_window.cloud_config
else:
# 如果没有预加载的配置,则同步获取
headers = {"User-Agent": UA}
response = requests.get(CONFIG_URL, headers=headers, timeout=10)
response.raise_for_status()
config_data = response.json()
if not config_data:
raise ValueError("未能获取或解析配置数据")
if self.is_debug_mode():
print(f"DEBUG: Parsed JSON data: {json.dumps(config_data, indent=2)}")
# 统一处理URL提取确保返回扁平化的字典
urls = {}
for i in range(4):
key = f"vol.{i+1}.data"
if key in config_data and "url" in config_data[key]:
urls[f"vol{i+1}"] = config_data[key]["url"]
if "after.data" in config_data and "url" in config_data["after.data"]:
urls["after"] = config_data["after.data"]["url"]
# 检查是否成功提取了所有URL
if len(urls) != 5:
missing_keys_map = {
f"vol{i+1}": f"vol.{i+1}.data" for i in range(4)
}
missing_keys_map["after"] = "after.data"
extracted_keys = set(urls.keys())
all_keys = set(missing_keys_map.keys())
missing_simple_keys = all_keys - extracted_keys
missing_original_keys = [missing_keys_map[k] for k in missing_simple_keys]
raise ValueError(f"配置文件缺少必要的键: {', '.join(missing_original_keys)}")
if self.is_debug_mode():
print(f"DEBUG: Extracted URLs: {urls}")
print("--- Finished getting download URL successfully ---")
return urls
except requests.exceptions.RequestException as e:
status_code = e.response.status_code if e.response is not None else "未知"
try:
error_response = e.response.json() if e.response else {}
json_title = error_response.get("title", "无错误类型")
json_message = error_response.get("message", "无附加错误信息")
except (ValueError, AttributeError):
json_title = "配置文件异常,无法解析错误类型"
json_message = "配置文件异常,无法解析错误信息"
if self.is_debug_mode():
print(f"ERROR: Failed to get download config due to RequestException: {e}")
QtWidgets.QMessageBox.critical(
self.main_window,
f"错误 - {APP_NAME}",
f"\n下载配置获取失败\n\n【HTTP状态】{status_code}\n【错误类型】:{json_title}\n【错误信息】:{json_message}\n",
)
return {}
except ValueError as e:
if self.is_debug_mode():
print(f"ERROR: Failed to parse download config due to ValueError: {e}")
QtWidgets.QMessageBox.critical(
self.main_window,
f"错误 - {APP_NAME}",
f"\n配置文件格式异常\n\n【错误信息】:{e}\n",
)
return {}
def download_action(self):
"""开始下载流程"""
# 主窗口在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(check_type="pre")
# 执行预检查,先判断哪些游戏版本已安装了补丁
install_paths = self.get_install_paths()
self.main_window.hash_thread = self.main_window.create_hash_thread("pre", install_paths)
# 使用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_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)
# 先显示已安装补丁的游戏
if already_installed_games:
already_installed_label = QLabel("已安装补丁的游戏:", dialog)
already_installed_label.setFont(QFont(already_installed_label.font().family(), already_installed_label.font().pointSize(), QFont.Bold))
layout.addWidget(already_installed_label)
already_installed_list = QLabel(chr(10).join(already_installed_games), dialog)
layout.addWidget(already_installed_list)
# 添加一些间距
layout.addSpacing(10)
# 添加"请选择你需要安装补丁的游戏"的标签
info_label = QLabel("请选择你需要安装补丁的游戏:", dialog)
info_label.setFont(QFont(info_label.font().family(), info_label.font().pointSize(), QFont.Bold))
layout.addWidget(info_label)
# 添加列表控件
list_widget = QListWidget(dialog)
list_widget.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) # 允许多选
for game 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()
if not config:
QtWidgets.QMessageBox.critical(
self.main_window, f"错误 - {APP_NAME}", "\n网络状态异常或服务器故障,请重试\n"
)
# 网络故障时,恢复主窗口
self.main_window.setEnabled(True)
self.main_window.ui.start_install_text.setText("开始安装")
return
# 填充下载队列,传入选定的游戏目录
self._fill_download_queue(config, selected_game_dirs)
# 如果没有需要下载的内容,直接进行最终校验
if not self.download_queue:
self.main_window.after_hash_compare()
return
# 询问是否使用Cloudflare加速
self._show_cloudflare_option()
def _fill_download_queue(self, config, game_dirs):
"""填充下载队列
Args:
config: 包含下载URL的配置字典
game_dirs: 包含游戏文件夹路径的字典
"""
# 清空现有队列
self.download_queue.clear()
# 创建下载历史记录列表,用于跟踪本次安装的游戏
if not hasattr(self.main_window, 'download_queue_history'):
self.main_window.download_queue_history = []
debug_mode = self.is_debug_mode()
if debug_mode:
print(f"DEBUG: 填充下载队列, 游戏目录: {game_dirs}")
# 添加nekopara 1-4
for i in range(1, 5):
game_version = f"NEKOPARA Vol.{i}"
# 只处理game_dirs中包含的游戏版本(如果用户选择了特定版本)
if game_version in game_dirs and not self.main_window.installed_status.get(game_version, False):
url = config.get(f"vol{i}")
if not url: continue
# 获取识别到的游戏文件夹路径
game_folder = game_dirs[game_version]
if debug_mode:
print(f"DEBUG: 使用识别到的游戏目录 {game_version}: {game_folder}")
_7z_path = os.path.join(PLUGIN, f"vol.{i}.7z")
plugin_path = os.path.join(PLUGIN, GAME_INFO[game_version]["plugin_path"])
self.download_queue.append((url, game_folder, game_version, _7z_path, plugin_path))
# 记录到下载历史
self.main_window.download_queue_history.append(game_version)
# 添加nekopara after
game_version = "NEKOPARA After"
# 只处理game_dirs中包含的游戏版本(如果用户选择了特定版本)
if game_version in game_dirs and not self.main_window.installed_status.get(game_version, False):
url = config.get("after")
if url:
# 获取识别到的游戏文件夹路径
game_folder = game_dirs[game_version]
if debug_mode:
print(f"DEBUG: 使用识别到的游戏目录 {game_version}: {game_folder}")
_7z_path = os.path.join(PLUGIN, "after.7z")
plugin_path = os.path.join(PLUGIN, GAME_INFO[game_version]["plugin_path"])
self.download_queue.append((url, game_folder, game_version, _7z_path, plugin_path))
# 记录到下载历史
self.main_window.download_queue_history.append(game_version)
def _show_cloudflare_option(self):
"""显示Cloudflare加速选择对话框"""
# 临时启用窗口以显示对话框
self.main_window.setEnabled(True)
msg_box = QtWidgets.QMessageBox(self.main_window)
msg_box.setWindowTitle(f"下载优化 - {APP_NAME}")
msg_box.setText("是否愿意通过Cloudflare加速来优化下载速度\n\n这将临时修改系统的hosts文件并需要管理员权限。\n如您的杀毒软件提醒有软件正在修改hosts文件请注意放行。")
# 设置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()
clicked_button = msg_box.clickedButton()
if clicked_button == cancel_button:
# 用户取消了安装,保持主窗口启用
self.main_window.setEnabled(True)
self.main_window.ui.start_install_text.setText("开始安装")
self.download_queue.clear() # 清空下载队列
return
# 用户点击了继续按钮,重新禁用主窗口
self.main_window.setEnabled(False)
use_optimization = clicked_button == yes_button
if use_optimization and not self.cloudflare_optimizer.is_optimization_done():
first_url = self.download_queue[0][0]
# 保存当前URL供CloudflareOptimizer使用
self.main_window.current_url = first_url
# 使用CloudflareOptimizer进行IP优化
self.cloudflare_optimizer.start_ip_optimization(first_url)
# 等待CloudflareOptimizer的回调
# on_optimization_finished会被调用然后决定是否继续
QtCore.QTimer.singleShot(100, self.check_optimization_status)
else:
# 如果用户选择不优化,或已经优化过,直接开始下载
self.next_download_task()
def check_optimization_status(self):
"""检查IP优化状态并继续下载流程"""
# 必须同时满足:优化已完成且倒计时已结束
if self.cloudflare_optimizer.is_optimization_done() and self.cloudflare_optimizer.is_countdown_finished():
self.next_download_task()
else:
# 否则继续等待100ms后再次检查
QtCore.QTimer.singleShot(100, self.check_optimization_status)
def next_download_task(self):
"""处理下载队列中的下一个任务"""
if not self.download_queue:
self.main_window.after_hash_compare()
return
# 检查下载线程是否仍在运行,以避免在手动停止后立即开始下一个任务
if self.download_task_manager.current_download_thread and self.download_task_manager.current_download_thread.isRunning():
return
# 获取下一个下载任务并开始
url, game_folder, game_version, _7z_path, plugin_path = self.download_queue.popleft()
self.download_setting(url, game_folder, game_version, _7z_path, plugin_path)
def download_setting(self, url, game_folder, game_version, _7z_path, plugin_path):
"""准备下载特定游戏版本
Args:
url: 下载URL
game_folder: 游戏文件夹路径
game_version: 游戏版本名称
_7z_path: 7z文件保存路径
plugin_path: 插件路径
"""
# 使用改进的目录识别获取安装路径
install_paths = self.get_install_paths()
debug_mode = self.is_debug_mode()
if debug_mode:
print(f"DEBUG: 准备下载游戏 {game_version}")
print(f"DEBUG: 游戏文件夹: {game_folder}")
# 游戏可执行文件已在填充下载队列时验证过,不需要再次检查
# 因为game_folder是从已验证的game_dirs中获取的
game_exe_exists = True
# 检查游戏是否已安装
if (
not game_exe_exists
or self.main_window.installed_status[game_version]
):
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
# 创建进度窗口并开始下载
self.main_window.progress_window = self.main_window.create_progress_window()
# 从CloudflareOptimizer获取已优选的IP
self.optimized_ip = self.cloudflare_optimizer.get_optimized_ip()
if self.optimized_ip:
print(f"已为 {game_version} 获取到优选IP: {self.optimized_ip}")
else:
print(f"未能为 {game_version} 获取优选IP将使用默认线路。")
# 使用DownloadTaskManager开始下载
self.download_task_manager.start_download(url, _7z_path, game_version, game_folder, plugin_path)
# 连接到主窗口中的下载完成处理函数
def on_download_finished(self, success, error, url, game_folder, game_version, _7z_path, plugin_path):
"""下载完成后的处理
Args:
success: 是否下载成功
error: 错误信息
url: 下载URL
game_folder: 游戏文件夹路径
game_version: 游戏版本名称
_7z_path: 7z文件保存路径
plugin_path: 插件路径
"""
# 关闭进度窗口
if 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是否重试?")
retry_button = msg_box.addButton("重试", QtWidgets.QMessageBox.ButtonRole.YesRole)
next_button = msg_box.addButton("下一个", QtWidgets.QMessageBox.ButtonRole.NoRole)
end_button = msg_box.addButton("结束", QtWidgets.QMessageBox.ButtonRole.RejectRole)
msg_box.exec()
clicked_button = msg_box.clickedButton()
# 处理用户选择
if clicked_button == retry_button:
# 重试,重新禁用窗口
self.main_window.setEnabled(False)
self.download_setting(url, game_folder, game_version, _7z_path, plugin_path)
elif clicked_button == next_button:
# 继续下一个,重新禁用窗口
self.main_window.setEnabled(False)
self.next_download_task()
else:
# 结束,保持窗口启用
self.on_download_stopped()
return
# 下载成功使用ExtractionHandler开始解压缩
self.extraction_handler.start_extraction(_7z_path, game_folder, plugin_path, game_version)
# extraction_handler的回调会处理下一步操作
def on_extraction_finished(self, continue_download):
"""解压完成后的回调,决定是否继续下载队列
Args:
continue_download: 是否继续下载队列中的下一个任务
"""
if continue_download:
# 继续下一个下载任务
self.next_download_task()
else:
# 清空剩余队列并显示结果
self.download_queue.clear()
self.main_window.show_result()
def on_download_stopped(self):
"""当用户点击停止按钮或选择结束时调用的函数"""
# 停止IP优化线程
self.cloudflare_optimizer.stop_optimization()
# 停止当前可能仍在运行的下载线程
self.download_task_manager.stop_download()
# 清空下载队列,因为用户决定停止
self.download_queue.clear()
# 确保进度窗口已关闭
if hasattr(self.main_window, 'progress_window') and self.main_window.progress_window:
if self.main_window.progress_window.isVisible():
self.main_window.progress_window.reject()
self.main_window.progress_window = None
# 退出应用程序
print("下载已全部停止。")
# 恢复主窗口状态
self.main_window.setEnabled(True)
self.main_window.ui.start_install_text.setText("开始安装")
# 显示取消安装的消息
QtWidgets.QMessageBox.information(
self.main_window,
f"已取消 - {APP_NAME}",
"\n已成功取消安装进程。\n"
)
# 以下方法委托给DownloadTaskManager
def get_download_thread_count(self):
"""获取当前下载线程设置对应的线程数"""
return self.download_task_manager.get_download_thread_count()
def set_download_thread_level(self, level):
"""设置下载线程级别"""
return self.download_task_manager.set_download_thread_level(level)
def show_download_thread_settings(self):
"""显示下载线程设置对话框"""
return self.download_task_manager.show_download_thread_settings()

View File

@@ -1,81 +0,0 @@
import os
from PySide6 import QtWidgets
from PySide6.QtWidgets import QMessageBox
class ExtractionHandler:
"""解压处理器,负责管理解压任务和结果处理"""
def __init__(self, main_window):
"""初始化解压处理器
Args:
main_window: 主窗口实例用于访问UI和状态
"""
self.main_window = main_window
self.APP_NAME = main_window.APP_NAME if hasattr(main_window, 'APP_NAME') else ""
def start_extraction(self, _7z_path, game_folder, plugin_path, game_version):
"""开始解压任务
Args:
_7z_path: 7z文件路径
game_folder: 游戏文件夹路径
plugin_path: 插件路径
game_version: 游戏版本名称
"""
# 显示解压中的消息窗口
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(
_7z_path, game_folder, plugin_path, game_version
)
self.main_window.extraction_thread.finished.connect(self.on_extraction_finished)
self.main_window.extraction_thread.start()
def on_extraction_finished(self, success, error_message, game_version):
"""解压完成后的处理
Args:
success: 是否解压成功
error_message: 错误信息
game_version: 游戏版本
"""
# 关闭哈希检查窗口
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"错误 - {self.APP_NAME}", error_message)
self.main_window.installed_status[game_version] = False
# 询问用户是否继续其他游戏的安装
reply = QtWidgets.QMessageBox.question(
self.main_window,
f"继续安装? - {self.APP_NAME}",
f"\n{game_version} 的补丁安装失败。\n\n是否继续安装其他游戏的补丁?\n",
QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No,
QtWidgets.QMessageBox.StandardButton.Yes
)
if reply == QtWidgets.QMessageBox.StandardButton.Yes:
# 继续下一个,重新禁用窗口
self.main_window.setEnabled(False)
# 通知DownloadManager继续下一个下载任务
self.main_window.download_manager.on_extraction_finished(True)
else:
# 用户选择停止,保持窗口启用状态
self.main_window.ui.start_install_text.setText("开始安装")
# 通知DownloadManager停止下载队列
self.main_window.download_manager.on_extraction_finished(False)
else:
# 更新安装状态
self.main_window.installed_status[game_version] = True
# 通知DownloadManager继续下一个下载任务
self.main_window.download_manager.on_extraction_finished(True)

View File

@@ -0,0 +1,10 @@
# Handlers package initialization
from .extraction_handler import ExtractionHandler
from .patch_toggle_handler import PatchToggleHandler
from .uninstall_handler import UninstallHandler
__all__ = [
'ExtractionHandler',
'PatchToggleHandler',
'UninstallHandler',
]

View File

@@ -0,0 +1,267 @@
import os
import shutil
from PySide6 import QtWidgets
from PySide6.QtWidgets import QMessageBox
from PySide6.QtCore import QTimer, QCoreApplication
from utils.logger import setup_logger
from workers.extraction_thread import ExtractionThread
# 初始化logger
logger = setup_logger("extraction_handler")
class ExtractionHandler:
"""解压处理器,负责管理解压任务和结果处理"""
def __init__(self, main_window):
"""初始化解压处理器
Args:
main_window: 主窗口实例用于访问UI和状态
"""
self.main_window = main_window
self.APP_NAME = main_window.APP_NAME if hasattr(main_window, 'APP_NAME') else ""
self.extraction_progress_window = None
def start_extraction(self, _7z_path, game_folder, plugin_path, game_version, extracted_path=None):
"""开始解压任务
Args:
_7z_path: 7z文件路径
game_folder: 游戏文件夹路径
plugin_path: 插件路径
game_version: 游戏版本名称
extracted_path: 已解压的补丁文件路径,如果提供则直接使用它而不进行解压
"""
# 检查是否处于离线模式
is_offline = False
if hasattr(self.main_window, 'offline_mode_manager'):
is_offline = self.main_window.offline_mode_manager.is_in_offline_mode()
# 创建并显示解压进度窗口,替代原来的消息框
self.extraction_progress_window = self.main_window.create_extraction_progress_window()
self.extraction_progress_window.show()
# 确保UI更新
QCoreApplication.processEvents()
# 创建并启动解压线程
self.main_window.extraction_thread = ExtractionThread(
_7z_path, game_folder, plugin_path, game_version, self.main_window, extracted_path
)
# 连接进度信号
self.main_window.extraction_thread.progress.connect(self.update_extraction_progress)
# 连接完成信号
self.main_window.extraction_thread.finished.connect(self.on_extraction_finished_with_hash_check)
# 启动线程
self.main_window.extraction_thread.start()
def update_extraction_progress(self, progress, status_text):
"""更新解压进度
Args:
progress: 进度百分比
status_text: 状态文本
"""
if self.extraction_progress_window and hasattr(self.extraction_progress_window, 'progress_bar'):
self.extraction_progress_window.progress_bar.setValue(progress)
self.extraction_progress_window.status_label.setText(status_text)
# 确保UI更新
QCoreApplication.processEvents()
def on_extraction_finished_with_hash_check(self, success, error_message, game_version):
"""解压完成后进行哈希校验
Args:
success: 是否解压成功
error_message: 错误信息
game_version: 游戏版本
"""
# 关闭解压进度窗口
if self.extraction_progress_window:
self.extraction_progress_window.close()
self.extraction_progress_window = None
# 如果解压失败,显示错误并询问是否继续
if not success:
# 临时启用窗口以显示错误消息
self.main_window.setEnabled(True)
QtWidgets.QMessageBox.critical(self.main_window, f"错误 - {self.APP_NAME}", error_message)
self.main_window.installed_status[game_version] = False
# 询问用户是否继续其他游戏的安装
reply = QtWidgets.QMessageBox.question(
self.main_window,
f"继续安装? - {self.APP_NAME}",
f"\n{game_version} 的补丁安装失败。\n\n是否继续安装其他游戏的补丁?\n",
QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No,
QtWidgets.QMessageBox.StandardButton.Yes
)
if reply == QtWidgets.QMessageBox.StandardButton.Yes:
# 继续下一个,重新禁用窗口
self.main_window.setEnabled(False)
# 通知DownloadManager继续下一个下载任务
self.main_window.download_manager.on_extraction_finished(True)
else:
# 用户选择停止,保持窗口启用状态
if hasattr(self.main_window, 'window_manager'):
self.main_window.window_manager.change_window_state(self.main_window.window_manager.STATE_READY)
# 通知DownloadManager停止下载队列
self.main_window.download_manager.on_extraction_finished(False)
return
# 解压成功,进行哈希校验
self._perform_hash_check(game_version)
def _perform_hash_check(self, game_version):
"""解压成功后进行哈希校验
Args:
game_version: 游戏版本
"""
# 导入所需模块
from config.config import GAME_INFO, PLUGIN_HASH
from workers.hash_thread import HashThread
# 获取安装路径
install_paths = {}
if hasattr(self.main_window, 'game_detector') and hasattr(self.main_window, 'download_manager'):
game_dirs = self.main_window.game_detector.identify_game_directories_improved(
self.main_window.download_manager.selected_folder
)
for game, info in GAME_INFO.items():
if game in game_dirs and game == game_version:
game_dir = game_dirs[game]
install_path = os.path.join(game_dir, os.path.basename(info["install_path"]))
install_paths[game] = install_path
break
if not install_paths:
# 如果找不到安装路径,直接认为安装成功
logger.warning(f"未找到 {game_version} 的安装路径,跳过哈希校验")
self.main_window.installed_status[game_version] = True
self.main_window.download_manager.on_extraction_finished(True)
return
# 关闭可能存在的哈希校验窗口
self.main_window.close_hash_msg_box()
# 显示哈希校验窗口
self.main_window.hash_msg_box = self.main_window.hash_manager.hash_pop_window(
check_type="post",
auto_close=True, # 添加自动关闭参数
close_delay=1000 # 1秒后自动关闭
)
# 直接创建并启动哈希线程进行校验
hash_thread = HashThread(
"after",
install_paths,
PLUGIN_HASH,
self.main_window.installed_status,
self.main_window
)
hash_thread.after_finished.connect(self.on_hash_check_finished)
# 保存引用以便后续使用
self.hash_thread = hash_thread
hash_thread.start()
def on_hash_check_finished(self, result):
"""哈希校验完成后的处理
Args:
result: 校验结果,包含通过状态、游戏版本和消息
"""
# 导入所需模块
from config.config import GAME_INFO
# 关闭哈希检查窗口
self.main_window.close_hash_msg_box()
if not result["passed"]:
# 校验失败,删除已解压的文件并提示重新下载
game_version = result["game"]
error_message = result["message"]
# 临时启用窗口以显示错误消息
self.main_window.setEnabled(True)
# 获取安装路径
install_path = None
if hasattr(self.main_window, 'game_detector') and hasattr(self.main_window, 'download_manager'):
game_dirs = self.main_window.game_detector.identify_game_directories_improved(
self.main_window.download_manager.selected_folder
)
if game_version in game_dirs and game_version in GAME_INFO:
game_dir = game_dirs[game_version]
install_path = os.path.join(game_dir, os.path.basename(GAME_INFO[game_version]["install_path"]))
# 如果找到安装路径,尝试删除已解压的文件
if install_path and os.path.exists(install_path):
try:
os.remove(install_path)
logger.debug(f"已删除校验失败的文件: {install_path}")
except Exception as e:
logger.error(f"删除文件失败: {e}")
# 显示错误消息并询问是否重试
reply = QtWidgets.QMessageBox.question(
self.main_window,
f"校验失败 - {self.APP_NAME}",
f"{error_message}\n\n是否重新下载并安装?",
QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No,
QtWidgets.QMessageBox.StandardButton.Yes
)
if reply == QtWidgets.QMessageBox.StandardButton.Yes:
# 重新下载,将游戏重新添加到下载队列
self.main_window.setEnabled(False)
self.main_window.installed_status[game_version] = False
# 获取游戏目录和下载URL
if hasattr(self.main_window, 'download_manager') and hasattr(self.main_window, 'game_detector'):
game_dirs = self.main_window.game_detector.identify_game_directories_improved(
self.main_window.download_manager.selected_folder
)
if game_version in game_dirs:
# 重新将游戏添加到下载队列
self.main_window.download_manager.download_queue.appendleft([game_version])
# 继续下一个下载任务
self.main_window.download_manager.next_download_task()
else:
# 如果找不到游戏目录,继续下一个
self.main_window.download_manager.on_extraction_finished(True)
else:
# 如果无法重新下载,继续下一个
self.main_window.download_manager.on_extraction_finished(True)
else:
# 用户选择不重试,继续下一个
self.main_window.installed_status[game_version] = False
self.main_window.download_manager.on_extraction_finished(True)
else:
# 校验通过,更新安装状态
game_version = result["game"]
self.main_window.installed_status[game_version] = True
# 通知DownloadManager继续下一个下载任务
self.main_window.download_manager.on_extraction_finished(True)
def on_extraction_finished(self, success, error_message, game_version):
"""兼容旧版本的回调函数
Args:
success: 是否解压成功
error_message: 错误信息
game_version: 游戏版本
"""
# 调用新的带哈希校验的回调函数
self.on_extraction_finished_with_hash_check(success, error_message, game_version)

View File

@@ -0,0 +1,438 @@
import os
from PySide6.QtWidgets import (
QDialog, QVBoxLayout, QLabel, QListWidget, QPushButton, QHBoxLayout,
QAbstractItemView, QRadioButton, QButtonGroup, QFileDialog, QMessageBox
)
from PySide6.QtCore import QObject, Signal, QThread
from PySide6.QtGui import QFont
from utils import msgbox_frame
from utils.logger import setup_logger
# 初始化logger
logger = setup_logger("patch_toggle_handler")
class PatchToggleThread(QThread):
"""在后台线程中处理补丁切换逻辑"""
finished = Signal(object)
def __init__(self, handler, selected_folder):
super().__init__()
self.handler = handler
self.selected_folder = selected_folder
def run(self):
# 在后台线程中执行耗时操作
game_dirs = self.handler.game_detector.identify_game_directories_improved(self.selected_folder)
self.finished.emit(game_dirs)
class PatchToggleHandler(QObject):
"""
处理补丁启用/禁用功能的类
"""
def __init__(self, main_window):
"""
初始化补丁切换处理程序
Args:
main_window: 主窗口实例,用于访问其他组件
"""
super().__init__()
self.main_window = main_window
self.debug_manager = main_window.debug_manager
self.game_detector = main_window.game_detector
self.patch_manager = main_window.patch_manager
self.app_name = main_window.patch_manager.app_name
self.toggle_thread = None
def handle_toggle_patch_button_click(self):
"""
处理禁/启用补丁按钮点击事件
打开文件选择对话框选择游戏目录,然后禁用或启用对应游戏的补丁
"""
selected_folder = QFileDialog.getExistingDirectory(self.main_window, "选择游戏上级目录", "")
if not selected_folder:
return
self.main_window.show_loading_dialog("正在识别游戏目录并检查补丁状态...")
self.toggle_thread = PatchToggleThread(self, selected_folder)
self.toggle_thread.finished.connect(self.on_game_detection_finished)
self.toggle_thread.start()
def on_game_detection_finished(self, game_dirs):
"""游戏识别完成后的回调"""
self.main_window.hide_loading_dialog()
if not game_dirs:
QMessageBox.information(
self.main_window,
f"提示 - {self.app_name}",
"\n未在选择的目录中找到任何支持的游戏。\n",
)
return
games_with_patch = {}
for game_version, game_dir in game_dirs.items():
if self.patch_manager.check_patch_installed(game_dir, game_version):
is_disabled, _ = self.patch_manager.check_patch_disabled(game_dir, game_version)
status = "已禁用" if is_disabled else "已启用"
games_with_patch[game_version] = {"dir": game_dir, "status": status}
if not games_with_patch:
QMessageBox.information(
self.main_window,
f"提示 - {self.app_name}",
"\n目录中未找到已安装补丁的游戏。\n",
)
return
selected_games, operation = self._show_multi_game_dialog(games_with_patch)
if not selected_games:
return
selected_game_dirs = {game: games_with_patch[game]["dir"] for game in selected_games if game in games_with_patch}
self._execute_batch_toggle(selected_game_dirs, operation, self.debug_manager._is_debug_mode)
def _handle_multiple_games(self, game_dirs, debug_mode):
"""
处理多个游戏的补丁切换
Args:
game_dirs: 游戏目录字典
debug_mode: 是否为调试模式
"""
if debug_mode:
logger.debug(f"DEBUG: 禁/启用功能 - 在上级目录中找到以下游戏: {list(game_dirs.keys())}")
# 查找已安装补丁的游戏,只处理那些已安装补丁的游戏
games_with_patch = {}
for game_version, game_dir in game_dirs.items():
if self.patch_manager.check_patch_installed(game_dir, game_version):
# 检查补丁当前状态(是否禁用)
is_disabled, disabled_path = self.patch_manager.check_patch_disabled(game_dir, game_version)
status = "已禁用" if is_disabled else "已启用"
games_with_patch[game_version] = {
"dir": game_dir,
"disabled": is_disabled,
"status": status
}
if debug_mode:
logger.debug(f"DEBUG: 禁/启用功能 - {game_version} 已安装补丁,当前状态: {status}")
# 检查是否有已安装补丁的游戏
if not games_with_patch:
QMessageBox.information(
self.main_window,
f"提示 - {self.app_name}",
"\n未在选择的目录中找到已安装补丁的游戏。\n请确认您选择了正确的游戏目录,并且该目录中的游戏已安装过补丁。\n",
QMessageBox.StandardButton.Ok
)
return
# 显示选择对话框
selected_games, operation = self._show_multi_game_dialog(games_with_patch)
if not selected_games:
return # 用户取消了操作
# 过滤games_with_patch只保留选中的游戏
selected_game_dirs = {}
for game in selected_games:
if game in games_with_patch:
selected_game_dirs[game] = games_with_patch[game]["dir"]
# 确认操作
operation_text = "禁用" if operation == "disable" else "启用" if operation == "enable" else "切换"
game_list = '\n'.join([f"{game} ({games_with_patch[game]['status']})" for game in selected_games])
reply = QMessageBox.question(
self.main_window,
f"确认{operation_text}操作 - {self.app_name}",
f"\n确定要{operation_text}以下游戏补丁吗?\n\n{game_list}\n",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.No:
return
# 执行批量操作
self._execute_batch_toggle(selected_game_dirs, operation, debug_mode)
def _handle_single_game(self, selected_folder, debug_mode):
"""
处理单个游戏的补丁切换
Args:
selected_folder: 选择的游戏目录
debug_mode: 是否为调试模式
"""
# 未找到游戏目录,尝试将选择的目录作为游戏目录
if debug_mode:
logger.debug(f"DEBUG: 禁/启用功能 - 未在上级目录找到游戏,尝试将选择的目录视为游戏目录")
game_version = self.game_detector.identify_game_version(selected_folder)
if game_version:
if debug_mode:
logger.debug(f"DEBUG: 禁/启用功能 - 识别为游戏: {game_version}")
# 检查是否已安装补丁
if self.patch_manager.check_patch_installed(selected_folder, game_version):
# 检查补丁当前状态
is_disabled, disabled_path = self.patch_manager.check_patch_disabled(selected_folder, game_version)
current_status = "已禁用" if is_disabled else "已启用"
# 显示单游戏操作对话框
operation = self._show_single_game_dialog(game_version, current_status, is_disabled)
if not operation:
return # 用户取消了操作
# 执行操作
result = self.patch_manager.toggle_patch(selected_folder, game_version, operation=operation)
if not result["success"]:
# 操作失败的消息已在toggle_patch中显示
pass
else:
# 没有安装补丁
QMessageBox.information(
self.main_window,
f"提示 - {self.app_name}",
f"\n未在 {game_version} 中找到已安装的补丁。\n请确认该游戏已经安装过补丁。\n",
QMessageBox.StandardButton.Ok
)
else:
# 两种方式都未识别到游戏
if debug_mode:
logger.debug(f"DEBUG: 禁/启用功能 - 无法识别游戏")
msg_box = msgbox_frame(
f"错误 - {self.app_name}",
"\n所选目录不是有效的NEKOPARA游戏目录。\n请选择包含游戏可执行文件的目录或其上级目录。\n",
QMessageBox.StandardButton.Ok,
)
msg_box.exec()
def _show_multi_game_dialog(self, games_with_patch):
"""
显示多游戏选择对话框
Args:
games_with_patch: 已安装补丁的游戏信息
Returns:
tuple: (选择的游戏列表, 操作类型)
"""
dialog = QDialog(self.main_window)
dialog.setWindowTitle("选择要操作的游戏补丁")
dialog.resize(400, 400) # 增加高度以适应新增的单选按钮
layout = QVBoxLayout(dialog)
# 添加"已安装补丁的游戏"标签
already_installed_label = QLabel("已安装补丁的游戏:", dialog)
already_installed_label.setFont(QFont(already_installed_label.font().family(), already_installed_label.font().pointSize(), QFont.Bold))
layout.addWidget(already_installed_label)
# 添加游戏列表和状态
games_status_text = ""
for game, info in games_with_patch.items():
games_status_text += f"{game} ({info['status']})\n"
games_status_label = QLabel(games_status_text.strip(), dialog)
layout.addWidget(games_status_label)
# 添加一些间距
layout.addSpacing(10)
# 添加"请选择要操作的游戏"标签
info_label = QLabel("请选择要操作的游戏:", dialog)
info_label.setFont(QFont(info_label.font().family(), info_label.font().pointSize(), QFont.Bold))
layout.addWidget(info_label)
# 添加列表控件,只显示已安装补丁的游戏
list_widget = QListWidget(dialog)
list_widget.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) # 允许多选
for game, info in games_with_patch.items():
list_widget.addItem(f"{game} ({info['status']})")
layout.addWidget(list_widget)
# 添加全选按钮
select_all_btn = QPushButton("全选", dialog)
select_all_btn.clicked.connect(lambda: list_widget.selectAll())
layout.addWidget(select_all_btn)
# 添加操作选择单选按钮
operation_label = QLabel("请选择要执行的操作:", dialog)
operation_label.setFont(QFont(operation_label.font().family(), operation_label.font().pointSize(), QFont.Bold))
layout.addWidget(operation_label)
# 创建单选按钮组
radio_button_group = QButtonGroup(dialog)
# 添加"自动切换状态"单选按钮(默认选中)
auto_toggle_radio = QRadioButton("自动切换状态(禁用<->启用)", dialog)
auto_toggle_radio.setChecked(True)
radio_button_group.addButton(auto_toggle_radio, 0)
layout.addWidget(auto_toggle_radio)
# 添加"全部禁用"单选按钮
disable_all_radio = QRadioButton("禁用选中的补丁", dialog)
radio_button_group.addButton(disable_all_radio, 1)
layout.addWidget(disable_all_radio)
# 添加"全部启用"单选按钮
enable_all_radio = QRadioButton("启用选中的补丁", dialog)
radio_button_group.addButton(enable_all_radio, 2)
layout.addWidget(enable_all_radio)
# 添加确定和取消按钮
buttons_layout = QHBoxLayout()
ok_button = QPushButton("确定", dialog)
cancel_button = QPushButton("取消", dialog)
buttons_layout.addWidget(ok_button)
buttons_layout.addWidget(cancel_button)
layout.addLayout(buttons_layout)
# 连接按钮事件
ok_button.clicked.connect(dialog.accept)
cancel_button.clicked.connect(dialog.reject)
# 显示对话框并等待用户选择
result = dialog.exec()
if result != QDialog.DialogCode.Accepted or list_widget.selectedItems() == []:
# 用户取消或未选择任何游戏
return [], None
# 获取用户选择的游戏
selected_items = [item.text() for item in list_widget.selectedItems()]
selected_games = []
# 从选中项文本中提取游戏名称
for item in selected_items:
# 去除状态后缀 " (已启用)" 或 " (已禁用)"
game_name = item.split(" (")[0]
selected_games.append(game_name)
# 获取选中的操作类型
operation = None
if radio_button_group.checkedId() == 1: # 禁用选中的补丁
operation = "disable"
elif radio_button_group.checkedId() == 2: # 启用选中的补丁
operation = "enable"
# 否则为None表示自动切换状态
return selected_games, operation
def _show_single_game_dialog(self, game_version, current_status, is_disabled):
"""
显示单游戏操作对话框
Args:
game_version: 游戏版本
current_status: 当前补丁状态
is_disabled: 是否已禁用
Returns:
str: 操作类型,"enable""disable"或None表示取消
"""
dialog = QDialog(self.main_window)
dialog.setWindowTitle(f"{game_version} 补丁操作")
dialog.resize(300, 200)
layout = QVBoxLayout(dialog)
# 添加当前状态标签
status_label = QLabel(f"当前补丁状态: {current_status}", dialog)
status_label.setFont(QFont(status_label.font().family(), status_label.font().pointSize(), QFont.Bold))
layout.addWidget(status_label)
# 添加操作选择单选按钮
operation_label = QLabel("请选择要执行的操作:", dialog)
layout.addWidget(operation_label)
# 创建单选按钮组
radio_button_group = QButtonGroup(dialog)
# 添加可选操作
if is_disabled:
# 当前是禁用状态,显示启用选项
enable_radio = QRadioButton("启用补丁", dialog)
enable_radio.setChecked(True)
radio_button_group.addButton(enable_radio, 0)
layout.addWidget(enable_radio)
else:
# 当前是启用状态,显示禁用选项
disable_radio = QRadioButton("禁用补丁", dialog)
disable_radio.setChecked(True)
radio_button_group.addButton(disable_radio, 0)
layout.addWidget(disable_radio)
# 添加确定和取消按钮
buttons_layout = QHBoxLayout()
ok_button = QPushButton("确定", dialog)
cancel_button = QPushButton("取消", dialog)
buttons_layout.addWidget(ok_button)
buttons_layout.addWidget(cancel_button)
layout.addLayout(buttons_layout)
# 连接按钮事件
ok_button.clicked.connect(dialog.accept)
cancel_button.clicked.connect(dialog.reject)
# 显示对话框并等待用户选择
result = dialog.exec()
if result != QDialog.DialogCode.Accepted:
# 用户取消
return None
# 根据当前状态确定操作
return "enable" if is_disabled else "disable"
def _execute_batch_toggle(self, selected_game_dirs, operation, debug_mode):
"""
执行批量补丁切换操作
Args:
selected_game_dirs: 选择的游戏目录
operation: 操作类型
debug_mode: 是否为调试模式
"""
success_count = 0
fail_count = 0
results = []
for game_version, game_dir in selected_game_dirs.items():
try:
# 使用静默模式进行操作
result = self.patch_manager.toggle_patch(game_dir, game_version, operation=operation, silent=True)
if result["success"]:
success_count += 1
else:
fail_count += 1
results.append({
"version": game_version,
"success": result["success"],
"message": result["message"],
"action": result["action"]
})
except Exception as e:
if debug_mode:
logger.error(f"DEBUG: 切换 {game_version} 补丁状态时出错: {str(e)}")
fail_count += 1
results.append({
"version": game_version,
"success": False,
"message": f"操作出错: {str(e)}",
"action": "none"
})
# 显示操作结果
self.patch_manager.show_toggle_result(success_count, fail_count, results)

View File

@@ -0,0 +1,396 @@
import os
from PySide6.QtWidgets import (
QDialog, QVBoxLayout, QLabel, QListWidget, QPushButton, QHBoxLayout,
QAbstractItemView, QFileDialog, QMessageBox
)
from PySide6.QtCore import QObject, Signal, QThread
from PySide6.QtGui import QFont
from utils import msgbox_frame
from utils.logger import setup_logger
# 初始化logger
logger = setup_logger("uninstall_handler")
class UninstallThread(QThread):
"""在后台线程中处理卸载逻辑"""
finished = Signal(object)
def __init__(self, handler, selected_folder):
super().__init__()
self.handler = handler
self.selected_folder = selected_folder
def run(self):
# 在后台线程中执行耗时操作
game_dirs = self.handler.game_detector.identify_game_directories_improved(self.selected_folder)
self.finished.emit(game_dirs)
class UninstallHandler(QObject):
"""
处理补丁卸载功能的类
"""
def __init__(self, main_window):
"""
初始化卸载处理程序
Args:
main_window: 主窗口实例,用于访问其他组件
"""
super().__init__()
self.main_window = main_window
self.debug_manager = main_window.debug_manager
self.game_detector = main_window.game_detector
self.patch_manager = main_window.patch_manager
self.app_name = main_window.patch_manager.app_name
self.uninstall_thread = None
# 记录初始化日志
debug_mode = self.debug_manager._is_debug_mode() if hasattr(self.debug_manager, '_is_debug_mode') else False
if debug_mode:
logger.debug("DEBUG: 卸载处理程序已初始化")
def handle_uninstall_button_click(self):
"""
处理卸载补丁按钮点击事件
打开文件选择对话框选择游戏目录,然后卸载对应游戏的补丁
"""
# 获取游戏目录
debug_mode = self.debug_manager._is_debug_mode()
logger.info("用户点击了卸载补丁按钮")
if debug_mode:
logger.debug("DEBUG: 处理卸载补丁按钮点击事件")
# 提示用户选择目录
file_dialog_info = "选择游戏上级目录" if debug_mode else "选择游戏目录"
selected_folder = QFileDialog.getExistingDirectory(self.main_window, file_dialog_info, "")
if not selected_folder or selected_folder == "":
logger.info("用户取消了目录选择")
if debug_mode:
logger.debug("DEBUG: 用户取消了目录选择,退出卸载流程")
return # 用户取消了选择
logger.info(f"用户选择了目录: {selected_folder}")
if debug_mode:
logger.debug(f"卸载功能 - 用户选择了目录: {selected_folder}")
# 使用UI管理器显示加载对话框
if hasattr(self.main_window, 'ui_manager'):
self.main_window.ui_manager.show_loading_dialog("正在识别游戏目录...")
else:
logger.warning("无法访问UI管理器无法显示加载对话框")
self.uninstall_thread = UninstallThread(self, selected_folder)
self.uninstall_thread.finished.connect(self.on_game_detection_finished)
self.uninstall_thread.start()
def on_game_detection_finished(self, game_dirs):
"""游戏识别完成后的回调"""
# 使用UI管理器隐藏加载对话框
if hasattr(self.main_window, 'ui_manager'):
self.main_window.ui_manager.hide_loading_dialog()
else:
logger.warning("无法访问UI管理器无法隐藏加载对话框")
if not game_dirs:
QMessageBox.information(
self.main_window,
f"提示 - {self.app_name}",
"\n未在选择的目录中找到任何支持的游戏。\n",
)
return
games_with_patch = {}
for game_version, game_dir in game_dirs.items():
if self.patch_manager.check_patch_installed(game_dir, game_version):
games_with_patch[game_version] = game_dir
if not games_with_patch:
QMessageBox.information(
self.main_window,
f"提示 - {self.app_name}",
"\n目录中未找到已安装补丁的游戏。\n",
)
return
selected_games = self._show_game_selection_dialog(games_with_patch)
if not selected_games:
return
selected_game_dirs = {game: games_with_patch[game] for game in selected_games if game in games_with_patch}
game_list = '\n'.join(selected_games)
reply = QMessageBox.question(
self.main_window,
f"确认卸载 - {self.app_name}",
f"\n确定要卸载以下游戏的补丁吗?\n\n{game_list}\n",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.No:
return
success_count, fail_count, results = self.patch_manager.batch_uninstall_patches(selected_game_dirs)
self.patch_manager.show_uninstall_result(success_count, fail_count, results)
def _handle_multiple_games(self, game_dirs, debug_mode):
"""
处理多个游戏的补丁卸载
Args:
game_dirs: 游戏目录字典
debug_mode: 是否为调试模式
"""
if debug_mode:
logger.debug(f"DEBUG: 卸载功能 - 在上级目录中找到以下游戏: {list(game_dirs.keys())}")
# 查找已安装补丁的游戏,只处理那些已安装补丁的游戏
logger.info("检查哪些游戏已安装补丁")
games_with_patch = {}
for game_version, game_dir in game_dirs.items():
is_installed = self.patch_manager.check_patch_installed(game_dir, game_version)
if is_installed:
games_with_patch[game_version] = game_dir
logger.info(f"游戏 {game_version} 已安装补丁")
if debug_mode:
logger.debug(f"DEBUG: 卸载功能 - {game_version} 已安装补丁,目录: {game_dir}")
else:
logger.info(f"游戏 {game_version} 未安装补丁")
if debug_mode:
logger.debug(f"DEBUG: 卸载功能 - {game_version} 未安装补丁,跳过")
# 检查是否有已安装补丁的游戏
if not games_with_patch:
logger.info("未找到已安装补丁的游戏")
if debug_mode:
logger.debug("DEBUG: 卸载功能 - 未找到已安装补丁的游戏,显示提示消息")
QMessageBox.information(
self.main_window,
f"提示 - {self.app_name}",
"\n未在选择的目录中找到已安装补丁的游戏。\n请确认您选择了正确的游戏目录,并且该目录中的游戏已安装过补丁。\n",
QMessageBox.StandardButton.Ok
)
return
# 显示选择对话框
logger.info("显示游戏选择对话框")
if debug_mode:
logger.debug(f"DEBUG: 卸载功能 - 显示游戏选择对话框,可选游戏: {list(games_with_patch.keys())}")
selected_games = self._show_game_selection_dialog(games_with_patch)
if not selected_games:
logger.info("用户未选择任何游戏或取消了选择")
if debug_mode:
logger.debug("DEBUG: 卸载功能 - 用户未选择任何游戏或取消了选择,退出卸载流程")
return # 用户取消了选择
logger.info(f"用户选择了以下游戏: {selected_games}")
if debug_mode:
logger.debug(f"卸载功能 - 用户选择了以下游戏: {selected_games}")
# 过滤game_dirs只保留选中的游戏
selected_game_dirs = {game: games_with_patch[game] for game in selected_games if game in games_with_patch}
# 确认卸载
game_list = '\n'.join(selected_games)
logger.info("显示卸载确认对话框")
if debug_mode:
logger.debug(f"DEBUG: 卸载功能 - 显示卸载确认对话框,选择的游戏: {selected_games}")
reply = QMessageBox.question(
self.main_window,
f"确认卸载 - {self.app_name}",
f"\n确定要卸载以下游戏的补丁吗?\n\n{game_list}\n",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.No:
logger.info("用户取消了卸载操作")
if debug_mode:
logger.debug("DEBUG: 卸载功能 - 用户取消了卸载操作,退出卸载流程")
return
logger.info("开始批量卸载补丁")
if debug_mode:
logger.debug(f"DEBUG: 卸载功能 - 开始批量卸载补丁,游戏: {list(selected_game_dirs.keys())}")
# 使用批量卸载方法
success_count, fail_count, results = self.patch_manager.batch_uninstall_patches(selected_game_dirs)
logger.info(f"批量卸载完成,成功: {success_count},失败: {fail_count}")
if debug_mode:
logger.debug(f"DEBUG: 卸载功能 - 批量卸载完成,成功: {success_count},失败: {fail_count}")
if results:
for result in results:
status = "成功" if result["success"] else "失败"
logger.debug(f"DEBUG: 卸载结果 - {result['version']}: {status}, 消息: {result['message']}, 删除文件数: {result['files_removed']}")
self.patch_manager.show_uninstall_result(success_count, fail_count, results)
def _handle_single_game(self, selected_folder, debug_mode):
"""
处理单个游戏的补丁卸载
Args:
selected_folder: 选择的游戏目录
debug_mode: 是否为调试模式
"""
# 未找到游戏目录,尝试将选择的目录作为游戏目录
if debug_mode:
logger.debug(f"DEBUG: 卸载功能 - 未在上级目录找到游戏,尝试将选择的目录视为游戏目录")
logger.info("尝试识别单个游戏版本")
game_version = self.game_detector.identify_game_version(selected_folder)
if game_version:
logger.info(f"识别为游戏: {game_version}")
if debug_mode:
logger.debug(f"DEBUG: 卸载功能 - 识别为游戏: {game_version}")
# 检查是否已安装补丁
logger.info(f"检查 {game_version} 是否已安装补丁")
is_installed = self.patch_manager.check_patch_installed(selected_folder, game_version)
if is_installed:
logger.info(f"{game_version} 已安装补丁")
if debug_mode:
logger.debug(f"DEBUG: 卸载功能 - {game_version} 已安装补丁")
# 确认卸载
logger.info("显示卸载确认对话框")
if debug_mode:
logger.debug(f"DEBUG: 卸载功能 - 显示卸载确认对话框,游戏: {game_version}")
reply = QMessageBox.question(
self.main_window,
f"确认卸载 - {self.app_name}",
f"\n确定要卸载 {game_version} 的补丁吗?\n游戏目录: {selected_folder}\n",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.Yes:
logger.info(f"开始卸载 {game_version} 的补丁")
if debug_mode:
logger.debug(f"DEBUG: 卸载功能 - 用户确认卸载 {game_version} 的补丁")
# 创建单个游戏的目录字典,使用批量卸载流程
single_game_dir = {game_version: selected_folder}
logger.info("执行批量卸载方法(单游戏)")
if debug_mode:
logger.debug(f"DEBUG: 卸载功能 - 执行批量卸载方法(单游戏): {game_version}")
success_count, fail_count, results = self.patch_manager.batch_uninstall_patches(single_game_dir)
logger.info(f"卸载完成,成功: {success_count},失败: {fail_count}")
if debug_mode:
logger.debug(f"DEBUG: 卸载功能 - 卸载完成,成功: {success_count},失败: {fail_count}")
if results:
for result in results:
status = "成功" if result["success"] else "失败"
logger.debug(f"DEBUG: 卸载结果 - {result['version']}: {status}, 消息: {result['message']}, 删除文件数: {result['files_removed']}")
self.patch_manager.show_uninstall_result(success_count, fail_count, results)
else:
logger.info("用户取消了卸载操作")
if debug_mode:
logger.debug(f"DEBUG: 卸载功能 - 用户取消了卸载 {game_version} 的补丁")
else:
logger.info(f"{game_version} 未安装补丁")
if debug_mode:
logger.debug(f"DEBUG: 卸载功能 - {game_version} 未安装补丁,显示提示消息")
# 没有安装补丁
QMessageBox.information(
self.main_window,
f"提示 - {self.app_name}",
f"\n未在 {game_version} 中找到已安装的补丁。\n请确认该游戏已经安装过补丁。\n",
QMessageBox.StandardButton.Ok
)
else:
# 两种方式都未识别到游戏
logger.info("无法识别游戏")
if debug_mode:
logger.debug(f"DEBUG: 卸载功能 - 无法识别游戏,显示错误消息")
msg_box = msgbox_frame(
f"错误 - {self.app_name}",
"\n所选目录不是有效的NEKOPARA游戏目录。\n请选择包含游戏可执行文件的目录或其上级目录。\n",
QMessageBox.StandardButton.Ok,
)
msg_box.exec()
def _show_game_selection_dialog(self, games_with_patch):
"""
显示游戏选择对话框
Args:
games_with_patch: 已安装补丁的游戏目录字典
Returns:
list: 选择的游戏列表
"""
dialog = QDialog(self.main_window)
dialog.setWindowTitle("选择要卸载的游戏补丁")
dialog.resize(400, 300)
layout = QVBoxLayout(dialog)
# 添加"已安装补丁的游戏"标签
already_installed_label = QLabel("已安装补丁的游戏:", dialog)
already_installed_label.setFont(QFont(already_installed_label.font().family(), already_installed_label.font().pointSize(), QFont.Weight.Bold))
layout.addWidget(already_installed_label)
# 添加已安装游戏列表(可选,这里使用静态标签替代,保持一致性)
installed_games_text = ", ".join(games_with_patch.keys())
installed_games_label = QLabel(installed_games_text, dialog)
layout.addWidget(installed_games_label)
# 添加一些间距
layout.addSpacing(10)
# 添加"请选择要卸载补丁的游戏"标签
info_label = QLabel("请选择要卸载补丁的游戏:", dialog)
info_label.setFont(QFont(info_label.font().family(), info_label.font().pointSize(), QFont.Weight.Bold))
layout.addWidget(info_label)
# 添加列表控件,只显示已安装补丁的游戏
list_widget = QListWidget(dialog)
list_widget.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) # 允许多选
for game in games_with_patch.keys():
list_widget.addItem(game)
layout.addWidget(list_widget)
# 添加全选按钮
select_all_btn = QPushButton("全选", dialog)
select_all_btn.clicked.connect(lambda: list_widget.selectAll())
layout.addWidget(select_all_btn)
# 添加确定和取消按钮
buttons_layout = QHBoxLayout()
ok_button = QPushButton("确定", dialog)
cancel_button = QPushButton("取消", dialog)
buttons_layout.addWidget(ok_button)
buttons_layout.addWidget(cancel_button)
layout.addLayout(buttons_layout)
# 连接按钮事件
ok_button.clicked.connect(dialog.accept)
cancel_button.clicked.connect(dialog.reject)
# 显示对话框并等待用户选择
result = dialog.exec()
if result != QDialog.DialogCode.Accepted or list_widget.selectedItems() == []:
# 用户取消或未选择任何游戏
return []
# 获取用户选择的游戏
return [item.text() for item in list_widget.selectedItems()]

View File

@@ -0,0 +1,28 @@
# Managers package initialization
from .ui_manager import UIManager
from .download_managers 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
from .cloudflare_optimizer import CloudflareOptimizer
from .download_managers import DownloadTaskManager
from .patch_detector import PatchDetector
from .animations import MultiStageAnimations
__all__ = [
'UIManager',
'DownloadManager',
'DebugManager',
'WindowManager',
'GameDetector',
'PatchManager',
'ConfigManager',
'PrivacyManager',
'CloudflareOptimizer',
'DownloadTaskManager',
'PatchDetector',
'MultiStageAnimations',
]

View File

@@ -49,6 +49,7 @@ class MultiStageAnimations(QObject):
# 移除菜单背景动画 # 移除菜单背景动画
# {"widget": ui.menubg, "end_pos": QPoint(720, 55), "duration": 600}, # {"widget": ui.menubg, "end_pos": QPoint(720, 55), "duration": 600},
{"widget": ui.button_container, "end_pos": None, "duration": 600}, {"widget": ui.button_container, "end_pos": None, "duration": 600},
{"widget": ui.toggle_patch_container, "end_pos": None, "duration": 600}, # 添加禁/启用补丁按钮
{"widget": ui.uninstall_container, "end_pos": None, "duration": 600}, # 添加卸载补丁按钮 {"widget": ui.uninstall_container, "end_pos": None, "duration": 600}, # 添加卸载补丁按钮
{"widget": ui.exit_container, "end_pos": None, "duration": 600} {"widget": ui.exit_container, "end_pos": None, "duration": 600}
] ]
@@ -175,7 +176,7 @@ class MultiStageAnimations(QObject):
widget.setGraphicsEffect(effect) widget.setGraphicsEffect(effect)
widget.move(-widget.width(), item["end_pos"].y()) widget.move(-widget.width(), item["end_pos"].y())
widget.show() widget.show()
print("初始化支持栏动画") # 初始化支持栏动画,这是内部处理,不需要日志输出
# 初始化菜单元素(底部外) # 初始化菜单元素(底部外)
for item in self.menu_widgets: for item in self.menu_widgets:
@@ -187,7 +188,7 @@ class MultiStageAnimations(QObject):
widget.show() widget.show()
# 禁用所有按钮,直到动画完成 # 禁用所有按钮,直到动画完成
self.ui.start_install_btn.setEnabled(False) self.ui.start_install_btn.setEnabled(False) # 动画期间禁用
self.ui.uninstall_btn.setEnabled(False) self.ui.uninstall_btn.setEnabled(False)
self.ui.exit_btn.setEnabled(False) self.ui.exit_btn.setEnabled(False)
@@ -301,18 +302,29 @@ class MultiStageAnimations(QObject):
if hasattr(self.ui, 'button_container'): if hasattr(self.ui, 'button_container'):
btn_width = self.ui.button_container.width() btn_width = self.ui.button_container.width()
x_pos = width - btn_width - right_margin x_pos = width - btn_width - right_margin
y_pos = int((height - 65) * 0.28) - 10 # 与resizeEvent中保持一致 y_pos = int((height - 65) * 0.18) - 10 # 从0.28改为0.18,向上移动
# 更新动画目标位置 # 更新动画目标位置
for item in self.menu_widgets: for item in self.menu_widgets:
if item["widget"] == self.ui.button_container: if item["widget"] == self.ui.button_container:
item["end_pos"] = QPoint(x_pos, y_pos) item["end_pos"] = QPoint(x_pos, y_pos)
# 禁用补丁按钮
if hasattr(self.ui, 'toggle_patch_container'):
btn_width = self.ui.toggle_patch_container.width()
x_pos = width - btn_width - right_margin
y_pos = int((height - 65) * 0.36) - 10 # 从0.46改为0.36,向上移动
# 更新动画目标位置
for item in self.menu_widgets:
if item["widget"] == self.ui.toggle_patch_container:
item["end_pos"] = QPoint(x_pos, y_pos)
# 卸载补丁按钮 # 卸载补丁按钮
if hasattr(self.ui, 'uninstall_container'): if hasattr(self.ui, 'uninstall_container'):
btn_width = self.ui.uninstall_container.width() btn_width = self.ui.uninstall_container.width()
x_pos = width - btn_width - right_margin x_pos = width - btn_width - right_margin
y_pos = int((height - 65) * 0.46) - 10 # 与resizeEvent中保持一致 y_pos = int((height - 65) * 0.54) - 10 # 从0.64改为0.54,向上移动
# 更新动画目标位置 # 更新动画目标位置
for item in self.menu_widgets: for item in self.menu_widgets:
@@ -323,7 +335,7 @@ class MultiStageAnimations(QObject):
if hasattr(self.ui, 'exit_container'): if hasattr(self.ui, 'exit_container'):
btn_width = self.ui.exit_container.width() btn_width = self.ui.exit_container.width()
x_pos = width - btn_width - right_margin x_pos = width - btn_width - right_margin
y_pos = int((height - 65) * 0.64) - 10 # 与resizeEvent中保持一致 y_pos = int((height - 65) * 0.72) - 10 # 从0.82改为0.72,向上移动
# 更新动画目标位置 # 更新动画目标位置
for item in self.menu_widgets: for item in self.menu_widgets:
@@ -334,17 +346,19 @@ class MultiStageAnimations(QObject):
for item in self.menu_widgets: for item in self.menu_widgets:
if item["widget"] == self.ui.button_container: if item["widget"] == self.ui.button_container:
item["end_pos"] = QPoint(1050, 200) item["end_pos"] = QPoint(1050, 200)
elif item["widget"] == self.ui.uninstall_container: elif item["widget"] == self.ui.toggle_patch_container:
item["end_pos"] = QPoint(1050, 310) item["end_pos"] = QPoint(1050, 310)
elif item["widget"] == self.ui.exit_container: elif item["widget"] == self.ui.uninstall_container:
item["end_pos"] = QPoint(1050, 420) item["end_pos"] = QPoint(1050, 420)
elif item["widget"] == self.ui.exit_container:
item["end_pos"] = QPoint(1050, 530)
def start_animations(self): def start_animations(self):
"""启动完整动画序列""" """启动完整动画序列"""
self.clear_animations() self.clear_animations()
# 确保按钮在动画开始时被禁用 # 确保按钮在动画开始时被禁用
self.ui.start_install_btn.setEnabled(False) self.ui.start_install_btn.setEnabled(False) # 动画期间禁用
self.ui.uninstall_btn.setEnabled(False) self.ui.uninstall_btn.setEnabled(False)
self.ui.exit_btn.setEnabled(False) self.ui.exit_btn.setEnabled(False)

View File

@@ -6,6 +6,10 @@ from PySide6.QtGui import QIcon, QPixmap
from utils import msgbox_frame, resource_path from utils import msgbox_frame, resource_path
from workers import IpOptimizerThread from workers import IpOptimizerThread
from utils.logger import setup_logger
# 初始化logger
logger = setup_logger("cloudflare_optimizer")
class CloudflareOptimizer: class CloudflareOptimizer:
@@ -28,6 +32,7 @@ class CloudflareOptimizer:
self.optimization_cancelled = False self.optimization_cancelled = False
self.ip_optimizer_thread = None self.ip_optimizer_thread = None
self.ipv6_optimizer_thread = None self.ipv6_optimizer_thread = None
self.has_optimized_in_session = False # 本次启动是否已执行过优选
def is_optimization_done(self): def is_optimization_done(self):
"""检查是否已完成优化 """检查是否已完成优化
@@ -67,6 +72,29 @@ class CloudflareOptimizer:
Args: Args:
url: 用于优化的URL url: 用于优化的URL
""" """
# 解析域名
hostname = urlparse(url).hostname
# 判断是否继续优选的逻辑
if self.has_optimized_in_session:
# 如果本次会话中已执行过优选,则跳过优选过程
logger.info("本次会话已执行过优选,跳过优选过程")
# 设置标记为已优选完成
self.optimization_done = True
self.countdown_finished = True
return True
else:
# 如果本次会话尚未优选过,则清理可能存在的旧记录
if hostname:
# 检查hosts文件中是否已有该域名的IP记录
existing_ips = self.hosts_manager.get_hostname_entries(hostname)
if existing_ips:
logger.info(f"发现hosts文件中已有域名 {hostname} 的优选IP记录但本次会话尚未优选过")
# 清理已有的hosts记录准备重新优选
self.hosts_manager.clean_hostname_entries(hostname)
# 创建取消状态标记 # 创建取消状态标记
self.optimization_cancelled = False self.optimization_cancelled = False
self.countdown_finished = False self.countdown_finished = False
@@ -84,7 +112,7 @@ class CloudflareOptimizer:
ipv6_warning.setIcon(QtWidgets.QMessageBox.Icon.Warning) ipv6_warning.setIcon(QtWidgets.QMessageBox.Icon.Warning)
# 设置图标 # 设置图标
icon_path = resource_path(os.path.join("IMG", "ICO", "icon.png")) icon_path = resource_path(os.path.join("assets", "images", "ICO", "icon.png"))
if os.path.exists(icon_path): if os.path.exists(icon_path):
pixmap = QPixmap(icon_path) pixmap = QPixmap(icon_path)
if not pixmap.isNull(): if not pixmap.isNull():
@@ -122,7 +150,7 @@ class CloudflareOptimizer:
optimization_msg optimization_msg
) )
# 设置Cloudflare图标 # 设置Cloudflare图标
cf_icon_path = resource_path("IMG/ICO/cloudflare_logo_icon.ico") cf_icon_path = resource_path("assets/images/ICO/cloudflare_logo_icon.ico")
if os.path.exists(cf_icon_path): if os.path.exists(cf_icon_path):
cf_pixmap = QPixmap(cf_icon_path) cf_pixmap = QPixmap(cf_icon_path)
if not cf_pixmap.isNull(): if not cf_pixmap.isNull():
@@ -141,7 +169,7 @@ class CloudflareOptimizer:
# 如果启用IPv6同时启动IPv6优化线程 # 如果启用IPv6同时启动IPv6优化线程
if use_ipv6: if use_ipv6:
print("IPv6已启用将同时优选IPv6地址") logger.info("IPv6已启用将同时优选IPv6地址")
self.ipv6_optimizer_thread = IpOptimizerThread(url, use_ipv6=True) self.ipv6_optimizer_thread = IpOptimizerThread(url, use_ipv6=True)
self.ipv6_optimizer_thread.finished.connect(self.on_ipv6_optimization_finished) self.ipv6_optimizer_thread.finished.connect(self.on_ipv6_optimization_finished)
self.ipv6_optimizer_thread.start() self.ipv6_optimizer_thread.start()
@@ -171,7 +199,8 @@ class CloudflareOptimizer:
# 恢复主窗口状态 # 恢复主窗口状态
self.main_window.setEnabled(True) self.main_window.setEnabled(True)
self.main_window.ui.start_install_text.setText("开始安装") if hasattr(self.main_window, 'window_manager'):
self.main_window.window_manager.change_window_state(self.main_window.window_manager.STATE_READY)
# 显示取消消息 # 显示取消消息
QtWidgets.QMessageBox.information( QtWidgets.QMessageBox.information(
@@ -191,11 +220,11 @@ class CloudflareOptimizer:
return return
self.optimized_ip = ip self.optimized_ip = ip
print(f"IPv4优选完成结果: {ip if ip else '未找到合适的IP'}") logger.info(f"IPv4优选完成结果: {ip if ip else '未找到合适的IP'}")
# 检查是否还有IPv6优化正在运行 # 检查是否还有IPv6优化正在运行
if hasattr(self, 'ipv6_optimizer_thread') and self.ipv6_optimizer_thread and self.ipv6_optimizer_thread.isRunning(): if hasattr(self, 'ipv6_optimizer_thread') and self.ipv6_optimizer_thread and self.ipv6_optimizer_thread.isRunning():
print("等待IPv6优选完成...") logger.info("等待IPv6优选完成...")
return return
# 所有优选都已完成,继续处理 # 所有优选都已完成,继续处理
@@ -222,11 +251,11 @@ class CloudflareOptimizer:
return return
self.optimized_ipv6 = ipv6 self.optimized_ipv6 = ipv6
print(f"IPv6优选完成结果: {ipv6 if ipv6 else '未找到合适的IPv6'}") logger.info(f"IPv6优选完成结果: {ipv6 if ipv6 else '未找到合适的IPv6'}")
# 检查IPv4优化是否已完成 # 检查IPv4优化是否已完成
if hasattr(self, 'ip_optimizer_thread') and self.ip_optimizer_thread and self.ip_optimizer_thread.isRunning(): if hasattr(self, 'ip_optimizer_thread') and self.ip_optimizer_thread and self.ip_optimizer_thread.isRunning():
print("等待IPv4优选完成...") logger.info("等待IPv4优选完成...")
return return
# 所有优选都已完成,继续处理 # 所有优选都已完成,继续处理
@@ -244,6 +273,9 @@ class CloudflareOptimizer:
def _process_optimization_results(self): def _process_optimization_results(self):
"""处理优选的IP结果显示相应提示""" """处理优选的IP结果显示相应提示"""
# 无论优选结果如何,都标记本次会话已执行过优选
self.has_optimized_in_session = True
use_ipv6 = False use_ipv6 = False
if hasattr(self.main_window, 'config'): if hasattr(self.main_window, 'config'):
use_ipv6 = self.main_window.config.get("ipv6_enabled", False) use_ipv6 = self.main_window.config.get("ipv6_enabled", False)
@@ -301,7 +333,8 @@ class CloudflareOptimizer:
if msg_box.clickedButton() == cancel_button: if msg_box.clickedButton() == cancel_button:
# 恢复主窗口状态 # 恢复主窗口状态
self.main_window.setEnabled(True) self.main_window.setEnabled(True)
self.main_window.ui.start_install_text.setText("开始安装") if hasattr(self.main_window, 'window_manager'):
self.main_window.window_manager.change_window_state(self.main_window.window_manager.STATE_READY)
return False return False
# 用户点击了继续,重新禁用主窗口 # 用户点击了继续,重新禁用主窗口
@@ -332,6 +365,12 @@ class CloudflareOptimizer:
if ipv6_success: if ipv6_success:
success = self.hosts_manager.apply_ip(hostname, self.optimized_ipv6, clean=False) or success success = self.hosts_manager.apply_ip(hostname, self.optimized_ipv6, clean=False) or success
# 记录此次优选操作对hosts文件进行了更新
if hasattr(self.main_window, 'config'):
self.main_window.config['last_hosts_optimized_hostname'] = hostname
from utils import save_config
save_config(self.main_window.config)
if success: if success:
msg_box = QtWidgets.QMessageBox(self.main_window) msg_box = QtWidgets.QMessageBox(self.main_window)
msg_box.setWindowTitle(f"成功 - {self.main_window.APP_NAME}") msg_box.setWindowTitle(f"成功 - {self.main_window.APP_NAME}")
@@ -366,7 +405,8 @@ class CloudflareOptimizer:
if msg_box.clickedButton() == cancel_button: if msg_box.clickedButton() == cancel_button:
# 恢复主窗口状态 # 恢复主窗口状态
self.main_window.setEnabled(True) self.main_window.setEnabled(True)
self.main_window.ui.start_install_text.setText("开始安装") if hasattr(self.main_window, 'window_manager'):
self.main_window.window_manager.change_window_state(self.main_window.window_manager.STATE_READY)
return False return False
else: else:
QtWidgets.QMessageBox.critical( QtWidgets.QMessageBox.critical(
@@ -375,7 +415,8 @@ class CloudflareOptimizer:
"\n修改hosts文件失败请检查程序是否以管理员权限运行。\n" "\n修改hosts文件失败请检查程序是否以管理员权限运行。\n"
) )
# 恢复主窗口状态 # 恢复主窗口状态
self.main_window.ui.start_install_text.setText("开始安装") if hasattr(self.main_window, 'window_manager'):
self.main_window.window_manager.change_window_state(self.main_window.window_manager.STATE_READY)
return False return False
# 用户点击了继续,重新禁用主窗口 # 用户点击了继续,重新禁用主窗口

View File

@@ -85,16 +85,34 @@ class ConfigManager:
# 记录错误信息,用于按钮点击时显示 # 记录错误信息,用于按钮点击时显示
if error_message == "update_required": if error_message == "update_required":
self.last_error_message = "update_required" self.last_error_message = "update_required"
msg_box = msgbox_frame(
f"更新提示 - {self.app_name}", # 检查是否处于离线模式
"\n当前版本过低,请及时更新。\n", is_offline_mode = False
QMessageBox.StandardButton.Ok, if hasattr(self.debug_manager, 'main_window') and hasattr(self.debug_manager.main_window, 'offline_mode_manager'):
) is_offline_mode = self.debug_manager.main_window.offline_mode_manager.is_in_offline_mode()
msg_box.exec()
# 在浏览器中打开项目主页 if is_offline_mode:
webbrowser.open("https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/") # 离线模式下只显示提示,不禁用开始安装按钮
# 版本过低,应当显示"无法安装" msg_box = msgbox_frame(
return {"action": "disable_button", "then": "exit"} f"更新提示 - {self.app_name}",
"\n当前版本过低,请及时更新。\n在离线模式下,您仍可使用禁用/启用补丁、卸载补丁和离线安装功能。\n",
QMessageBox.StandardButton.Ok,
)
msg_box.exec()
# 移除在浏览器中打开项目主页的代码
# 离线模式下版本过低,仍然允许使用安装按钮
return {"action": "enable_button"}
else:
# 在线模式下显示强制更新提示
msg_box = msgbox_frame(
f"更新提示 - {self.app_name}",
"\n当前版本过低,请及时更新。\n如需联网下载补丁,请更新到最新版,否则无法下载。\n",
QMessageBox.StandardButton.Ok,
)
msg_box.exec()
# 移除在浏览器中打开项目主页的代码
# 在线模式下版本过低,但不直接禁用按钮,而是在点击时提示
return {"action": "enable_button", "version_warning": True}
elif "missing_keys" in error_message: elif "missing_keys" in error_message:
self.last_error_message = "missing_keys" self.last_error_message = "missing_keys"
@@ -128,8 +146,8 @@ class ConfigManager:
) )
msg_box.exec() msg_box.exec()
# 网络错误时应当显示"无法安装" # 网络错误时仍然允许使用按钮,用户可以尝试离线模式
return {"action": "disable_button"} return {"action": "enable_button"}
else: else:
self.cloud_config = data self.cloud_config = data
# 标记配置有效 # 标记配置有效
@@ -139,11 +157,37 @@ class ConfigManager:
if debug_mode: if debug_mode:
print("--- Cloud config fetched successfully ---") print("--- Cloud config fetched successfully ---")
print(json.dumps(data, indent=2)) # 创建一个数据副本隐藏敏感URL
safe_data = self._create_safe_config_for_logging(data)
print(json.dumps(safe_data, indent=2))
# 获取配置成功,允许安装 # 获取配置成功,允许安装
return {"action": "enable_button"} return {"action": "enable_button"}
def _create_safe_config_for_logging(self, config_data):
"""创建用于日志记录的安全配置副本隐藏敏感URL
Args:
config_data: 原始配置数据
Returns:
dict: 安全的配置数据副本
"""
if not config_data or not isinstance(config_data, dict):
return config_data
# 创建深拷贝,避免修改原始数据
import copy
safe_config = copy.deepcopy(config_data)
# 隐藏敏感URL
for key in safe_config:
if isinstance(safe_config[key], dict) and "url" in safe_config[key]:
# 完全隐藏URL
safe_config[key]["url"] = "***URL protection***"
return safe_config
def is_config_valid(self): def is_config_valid(self):
"""检查配置是否有效 """检查配置是否有效
@@ -167,3 +211,53 @@ class ConfigManager:
str: 错误信息 str: 错误信息
""" """
return self.last_error_message return self.last_error_message
def toggle_disable_pre_hash_check(self, main_window, checked):
"""切换禁用安装前哈希预检查的状态
Args:
main_window: 主窗口实例
checked: 是否禁用安装前哈希预检查
Returns:
bool: 操作是否成功
"""
try:
# 更新配置
if hasattr(main_window, 'config'):
main_window.config['disable_pre_hash_check'] = checked
# 保存配置到文件
if hasattr(main_window, 'save_config'):
main_window.save_config(main_window.config)
# 显示成功提示
status = "禁用" if checked else "启用"
from utils import msgbox_frame
msg_box = msgbox_frame(
f"设置已更新 - {self.app_name}",
f"\n{status}安装前哈希预检查。\n\n{'安装时将跳过哈希预检查' if checked else '安装时将进行哈希预检查'}\n",
QMessageBox.StandardButton.Ok,
)
msg_box.exec()
return True
else:
# 如果配置不可用,显示错误
from utils import msgbox_frame
msg_box = msgbox_frame(
f"错误 - {self.app_name}",
"\n配置管理器不可用,无法更新设置。\n",
QMessageBox.StandardButton.Ok,
)
msg_box.exec()
return False
except Exception as e:
# 如果发生异常,显示错误
from utils import msgbox_frame
msg_box = msgbox_frame(
f"错误 - {self.app_name}",
f"\n更新设置时发生异常:\n\n{str(e)}\n",
QMessageBox.StandardButton.Ok,
)
msg_box.exec()
return False

View File

@@ -0,0 +1,276 @@
import os
import sys
from PySide6 import QtWidgets
from config.config import LOG_FILE
from utils.logger import setup_logger
from utils import Logger
import datetime
from config.config import APP_NAME
# 初始化logger
logger = setup_logger("debug_manager")
class DebugManager:
def __init__(self, main_window):
"""初始化调试管理器
Args:
main_window: 主窗口实例
"""
self.main_window = main_window
self.logger = None
self.original_stdout = None
self.original_stderr = None
self.ui_manager = None # 添加ui_manager属性
def set_ui_manager(self, ui_manager):
"""设置UI管理器引用
Args:
ui_manager: UI管理器实例
"""
self.ui_manager = ui_manager
def _is_debug_mode(self):
"""检查是否处于调试模式
Returns:
bool: 是否处于调试模式
"""
try:
# 首先尝试从UI管理器获取状态
if hasattr(self, 'ui_manager') and self.ui_manager and hasattr(self.ui_manager, 'debug_action') and self.ui_manager.debug_action:
return self.ui_manager.debug_action.isChecked()
# 如果UI管理器还没准备好尝试从配置中获取
if hasattr(self.main_window, 'config') and isinstance(self.main_window.config, dict):
return self.main_window.config.get('debug_mode', False)
# 如果以上都不可行返回False
return False
except Exception:
# 捕获任何异常默认返回False
return False
def toggle_debug_mode(self, checked):
"""切换调试模式
Args:
checked: 是否启用调试模式
"""
logger.info(f"Toggle debug mode: {checked}")
self.main_window.config["debug_mode"] = checked
self.main_window.save_config(self.main_window.config)
# 创建或删除debug_mode.txt标记文件
try:
from config.config import CACHE
debug_file = os.path.join(os.path.dirname(CACHE), "debug_mode.txt")
if checked:
# 确保目录存在
os.makedirs(os.path.dirname(debug_file), exist_ok=True)
# 创建标记文件
with open(debug_file, 'w', encoding='utf-8') as f:
f.write(f"Debug mode enabled at {os.path.abspath(debug_file)}\n")
logger.info(f"已创建调试模式标记文件: {debug_file}")
elif os.path.exists(debug_file):
# 删除标记文件
os.remove(debug_file)
logger.debug(f"已删除调试模式标记文件: {debug_file}")
except Exception as e:
logger.warning(f"处理调试模式标记文件时发生错误: {e}")
# 更新打开log文件按钮状态
if hasattr(self, 'ui_manager') and hasattr(self.ui_manager, 'open_log_action'):
self.ui_manager.open_log_action.setEnabled(checked)
if checked:
self.start_logging()
# 如果启用了调试模式,检查是否需要强制启用离线模式
if hasattr(self.main_window, 'offline_mode_manager'):
# 检查配置中是否已设置离线模式
offline_mode_enabled = self.main_window.config.get("offline_mode", False)
# 如果配置中已设置离线模式,则在调试模式下强制启用
if offline_mode_enabled:
logger.debug("DEBUG: 调试模式下强制启用离线模式")
self.main_window.offline_mode_manager.set_offline_mode(True)
# 更新UI中的离线模式选项
if hasattr(self.ui_manager, 'offline_mode_action') and self.ui_manager.offline_mode_action:
self.ui_manager.offline_mode_action.setChecked(True)
self.ui_manager.online_mode_action.setChecked(False)
else:
self.stop_logging()
def start_logging(self):
"""启动日志记录"""
if self.logger is None:
try:
# 确保log目录存在
log_dir = os.path.dirname(LOG_FILE)
if not os.path.exists(log_dir):
os.makedirs(log_dir, exist_ok=True)
logger.debug(f"已创建日志目录: {log_dir}")
# 创建新的日志文件,使用覆盖模式而不是追加模式
with open(LOG_FILE, 'w', encoding='utf-8') as f:
current_time = datetime.datetime.now()
formatted_date = current_time.strftime("%Y-%m-%d")
formatted_time = current_time.strftime("%H:%M:%S")
f.write(f"--- 新调试会话开始于 {os.path.basename(LOG_FILE)} ---\n")
f.write(f"--- 应用版本: {APP_NAME} ---\n")
f.write(f"--- 日期: {formatted_date} 时间: {formatted_time} ---\n\n")
logger.debug(f"已创建日志文件: {os.path.abspath(LOG_FILE)}")
# 保存原始的 stdout 并创建Logger实例
self.original_stdout = sys.stdout
self.logger = Logger(LOG_FILE, self.original_stdout)
logger.debug(f"--- Debug mode enabled (log file: {os.path.abspath(LOG_FILE)}) ---")
except (IOError, OSError) as e:
QtWidgets.QMessageBox.critical(self.main_window, "错误", f"无法创建日志文件: {e}")
self.logger = None
def stop_logging(self):
"""停止日志记录"""
if self.logger:
logger.debug("--- Debug mode disabled ---")
# 恢复stdout到原始状态
if hasattr(self, 'original_stdout') and self.original_stdout:
sys.stdout = self.original_stdout
# 关闭日志文件
if hasattr(self.logger, 'close'):
self.logger.close()
self.logger = None
def open_log_file(self):
"""打开当前日志文件"""
try:
# 检查日志文件是否存在
if os.path.exists(LOG_FILE):
# 获取日志文件大小
file_size = os.path.getsize(LOG_FILE)
if file_size == 0:
from utils import msgbox_frame
msg_box = msgbox_frame(
f"提示 - {APP_NAME}",
f"\n当前日志文件 {os.path.basename(LOG_FILE)} 存在但为空。\n\n日志文件位置:{os.path.abspath(LOG_FILE)}",
QtWidgets.QMessageBox.StandardButton.Ok
)
msg_box.exec()
return
# 根据文件大小决定是使用文本查看器还是直接打开
if file_size > 1024 * 1024: # 大于1MB
# 文件较大,显示警告
from utils import msgbox_frame
msg_box = msgbox_frame(
f"警告 - {APP_NAME}",
f"\n日志文件较大 ({file_size / 1024 / 1024:.2f} MB),是否仍要打开?\n\n日志文件位置:{os.path.abspath(LOG_FILE)}",
QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No
)
if msg_box.exec() != QtWidgets.QMessageBox.StandardButton.Yes:
return
# 使用操作系统默认程序打开日志文件
if os.name == 'nt': # Windows
os.startfile(LOG_FILE)
else: # macOS 和 Linux
import subprocess
subprocess.call(['xdg-open', LOG_FILE])
else:
# 文件不存在,显示信息和搜索其他日志文件
root_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
log_dir = os.path.join(root_dir, "log")
# 如果log文件夹不存在尝试创建它
if not os.path.exists(log_dir):
try:
os.makedirs(log_dir, exist_ok=True)
from utils import msgbox_frame
msg_box = msgbox_frame(
f"信息 - {APP_NAME}",
f"\n日志文件夹不存在,已创建新的日志文件夹:\n{log_dir}\n\n请在启用调试模式后重试。",
QtWidgets.QMessageBox.StandardButton.Ok
)
msg_box.exec()
return
except Exception as e:
from utils import msgbox_frame
msg_box = msgbox_frame(
f"错误 - {APP_NAME}",
f"\n创建日志文件夹失败:\n\n{str(e)}",
QtWidgets.QMessageBox.StandardButton.Ok
)
msg_box.exec()
return
# 搜索log文件夹中的日志文件
try:
log_files = [f for f in os.listdir(log_dir) if f.startswith("log-") and f.endswith(".txt")]
except Exception as e:
from utils import msgbox_frame
msg_box = msgbox_frame(
f"错误 - {APP_NAME}",
f"\n无法读取日志文件夹:\n\n{str(e)}",
QtWidgets.QMessageBox.StandardButton.Ok
)
msg_box.exec()
return
if log_files:
# 按照修改时间排序,获取最新的日志文件
log_files.sort(key=lambda x: os.path.getmtime(os.path.join(log_dir, x)), reverse=True)
latest_log = os.path.join(log_dir, log_files[0])
# 获取最新日志文件的创建时间信息
try:
log_datetime = "-".join(os.path.basename(latest_log)[4:-4].split("-")[:2])
log_date = log_datetime.split("-")[0]
log_time = log_datetime.split("-")[1] if "-" in log_datetime else "未知时间"
date_info = f"日期: {log_date[:4]}-{log_date[4:6]}-{log_date[6:]}"
time_info = f"时间: {log_time[:2]}:{log_time[2:4]}:{log_time[4:]}"
except:
date_info = "日期未知"
time_info = "时间未知"
from utils import msgbox_frame
msg_box = msgbox_frame(
f"信息 - {APP_NAME}",
f"\n当前日志文件 {os.path.basename(LOG_FILE)} 不存在。\n\n"
f"发现最新的日志文件: {os.path.basename(latest_log)}\n"
f"({date_info} {time_info})\n\n"
f"是否打开此文件?",
QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No
)
if msg_box.exec() == QtWidgets.QMessageBox.StandardButton.Yes:
if os.name == 'nt': # Windows
os.startfile(latest_log)
else: # macOS 和 Linux
import subprocess
subprocess.call(['xdg-open', latest_log])
return
# 如果没有找到任何日志文件
from utils import msgbox_frame
msg_box = msgbox_frame(
f"信息 - {APP_NAME}",
f"\n没有找到有效的日志文件。\n\n"
f"预期的日志文件夹:{log_dir}\n\n"
f"请确认调试模式已启用,并执行一些操作后再查看日志。",
QtWidgets.QMessageBox.StandardButton.Ok
)
msg_box.exec()
except Exception as e:
from utils import msgbox_frame
msg_box = msgbox_frame(
f"错误 - {APP_NAME}",
f"\n处理日志文件时出错:\n\n{str(e)}\n\n文件位置:{os.path.abspath(LOG_FILE)}",
QtWidgets.QMessageBox.StandardButton.Ok
)
msg_box.exec()

View File

@@ -0,0 +1,12 @@
"""
下载管理器模块
包含下载相关的管理器类
"""
from .download_manager import DownloadManager
from .download_task_manager import DownloadTaskManager
__all__ = [
'DownloadManager',
'DownloadTaskManager',
]

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,9 @@ from PySide6.QtCore import Qt
from PySide6.QtWidgets import QDialog, QVBoxLayout, QRadioButton, QPushButton, QLabel, QButtonGroup, QHBoxLayout from PySide6.QtWidgets import QDialog, QVBoxLayout, QRadioButton, QPushButton, QLabel, QButtonGroup, QHBoxLayout
from PySide6.QtGui import QFont from PySide6.QtGui import QFont
from data.config import DOWNLOAD_THREADS from config.config import DOWNLOAD_THREADS
from workers.download import DownloadThread
from utils.logger import setup_logger
class DownloadTaskManager: class DownloadTaskManager:
@@ -34,7 +36,7 @@ class DownloadTaskManager:
# 按钮在file_dialog中已设置为禁用状态 # 按钮在file_dialog中已设置为禁用状态
# 创建并连接下载线程 # 创建并连接下载线程
self.current_download_thread = self.main_window.create_download_thread(url, _7z_path, game_version) self.current_download_thread = DownloadThread(url, _7z_path, game_version, self.main_window)
self.current_download_thread.progress.connect(self.main_window.progress_window.update_progress) self.current_download_thread.progress.connect(self.main_window.progress_window.update_progress)
self.current_download_thread.finished.connect( self.current_download_thread.finished.connect(
lambda success, error: self.main_window.download_manager.on_download_finished( lambda success, error: self.main_window.download_manager.on_download_finished(
@@ -52,7 +54,7 @@ class DownloadTaskManager:
self.main_window.progress_window.stop_button.clicked.connect(self.main_window.download_manager.on_download_stopped) self.main_window.progress_window.stop_button.clicked.connect(self.main_window.download_manager.on_download_stopped)
# 连接暂停/恢复按钮 # 连接暂停/恢复按钮
self.main_window.progress_window.pause_resume_button.clicked.connect(self.toggle_download_pause) self.main_window.progress_window.pause_resume_button.clicked.connect(self._on_pause_resume_clicked)
# 启动线程和显示进度窗口 # 启动线程和显示进度窗口
self.current_download_thread.start() self.current_download_thread.start()
@@ -60,6 +62,8 @@ class DownloadTaskManager:
def toggle_download_pause(self): def toggle_download_pause(self):
"""切换下载的暂停/恢复状态""" """切换下载的暂停/恢复状态"""
logger = setup_logger("download_task_manager")
logger.debug("执行暂停/恢复下载操作")
if not self.current_download_thread: if not self.current_download_thread:
return return
@@ -111,6 +115,8 @@ class DownloadTaskManager:
def show_download_thread_settings(self): def show_download_thread_settings(self):
"""显示下载线程设置对话框""" """显示下载线程设置对话框"""
logger = setup_logger("download_task_manager")
logger.info("用户打开下载线程数设置对话框")
# 创建对话框 # 创建对话框
dialog = QDialog(self.main_window) dialog = QDialog(self.main_window)
dialog.setWindowTitle(f"下载线程设置 - {self.APP_NAME}") dialog.setWindowTitle(f"下载线程设置 - {self.APP_NAME}")
@@ -178,6 +184,7 @@ class DownloadTaskManager:
break break
if selected_level: if selected_level:
old_level = self.download_thread_level
# 为极速和狂暴模式显示警告 # 为极速和狂暴模式显示警告
if selected_level in ["extreme", "insane"]: if selected_level in ["extreme", "insane"]:
warning_result = QtWidgets.QMessageBox.warning( warning_result = QtWidgets.QMessageBox.warning(
@@ -193,6 +200,9 @@ class DownloadTaskManager:
success = self.set_download_thread_level(selected_level) success = self.set_download_thread_level(selected_level)
logger.info(f"用户修改下载线程数设置: {old_level} -> {selected_level}")
logger.debug(f"对应线程数: {DOWNLOAD_THREADS[old_level]} -> {DOWNLOAD_THREADS[selected_level]}")
if success: if success:
# 显示设置成功消息 # 显示设置成功消息
thread_count = DOWNLOAD_THREADS[selected_level] thread_count = DOWNLOAD_THREADS[selected_level]
@@ -214,8 +224,19 @@ class DownloadTaskManager:
def stop_download(self): def stop_download(self):
"""停止当前下载线程""" """停止当前下载线程"""
logger = setup_logger("download_task_manager")
logger.info("用户点击停止下载按钮")
if self.current_download_thread and self.current_download_thread.isRunning(): if self.current_download_thread and self.current_download_thread.isRunning():
self.current_download_thread.stop() self.current_download_thread.stop()
self.current_download_thread.wait() # 等待线程完全终止 self.current_download_thread.wait() # 等待线程完全终止
return True return True
return False return False
def _on_pause_resume_clicked(self):
"""处理暂停/恢复按钮点击"""
logger = setup_logger("download_task_manager")
logger.info("用户点击暂停/恢复下载按钮")
self.toggle_download_pause()
def toggle_download_pause(self):
"""切换下载暂停/恢复状态"""

View File

@@ -1,5 +1,20 @@
from PySide6.QtCore import QThread, Signal
import os import os
import re import re
from utils.logger import setup_logger
class GameDetectionThread(QThread):
"""用于在后台线程中执行游戏目录识别的线程"""
finished = Signal(dict)
def __init__(self, detector_func, selected_folder):
super().__init__()
self.detector_func = detector_func
self.selected_folder = selected_folder
def run(self):
result = self.detector_func(self.selected_folder)
self.finished.emit(result)
class GameDetector: class GameDetector:
"""游戏检测器,用于识别游戏目录和版本""" """游戏检测器,用于识别游戏目录和版本"""
@@ -13,6 +28,19 @@ class GameDetector:
""" """
self.game_info = game_info self.game_info = game_info
self.debug_manager = debug_manager self.debug_manager = debug_manager
self.directory_cache = {} # 添加目录缓存
self.logger = setup_logger("game_detector")
self.detection_thread = None
def identify_game_directories_async(self, selected_folder, callback):
"""异步识别游戏目录"""
def on_finished(game_dirs):
callback(game_dirs)
self.detection_thread = None
self.detection_thread = GameDetectionThread(self.identify_game_directories_improved, selected_folder)
self.detection_thread.finished.connect(on_finished)
self.detection_thread.start()
def _is_debug_mode(self): def _is_debug_mode(self):
"""检查是否处于调试模式 """检查是否处于调试模式
@@ -36,7 +64,7 @@ class GameDetector:
debug_mode = self._is_debug_mode() debug_mode = self._is_debug_mode()
if debug_mode: if debug_mode:
print(f"DEBUG: 尝试识别游戏版本: {game_dir}") self.logger.debug(f"尝试识别游戏版本: {game_dir}")
# 先通过目录名称进行初步推测(这将作为递归搜索的提示) # 先通过目录名称进行初步推测(这将作为递归搜索的提示)
dir_name = os.path.basename(game_dir).lower() dir_name = os.path.basename(game_dir).lower()
@@ -50,11 +78,11 @@ class GameDetector:
vol_num = vol_match.group(1) vol_num = vol_match.group(1)
potential_version = f"NEKOPARA Vol.{vol_num}" potential_version = f"NEKOPARA Vol.{vol_num}"
if debug_mode: if debug_mode:
print(f"DEBUG: 从目录名推测游戏版本: {potential_version}, 卷号: {vol_num}") self.logger.debug(f"从目录名推测游戏版本: {potential_version}, 卷号: {vol_num}")
elif "after" in dir_name: elif "after" in dir_name:
potential_version = "NEKOPARA After" potential_version = "NEKOPARA After"
if debug_mode: if debug_mode:
print(f"DEBUG: 从目录名推测游戏版本: NEKOPARA After") self.logger.debug(f"从目录名推测游戏版本: NEKOPARA After")
# 检查是否为NEKOPARA游戏目录 # 检查是否为NEKOPARA游戏目录
# 通过检查游戏可执行文件来识别游戏版本 # 通过检查游戏可执行文件来识别游戏版本
@@ -87,7 +115,7 @@ class GameDetector:
exe_path = os.path.join(game_dir, exe_variant) exe_path = os.path.join(game_dir, exe_variant)
if os.path.exists(exe_path): if os.path.exists(exe_path):
if debug_mode: if debug_mode:
print(f"DEBUG: 通过可执行文件确认游戏版本: {game_version}, 文件: {exe_variant}") self.logger.debug(f"通过可执行文件确认游戏版本: {game_version}, 文件: {exe_variant}")
return game_version return game_version
# 如果没有直接匹配,尝试递归搜索 # 如果没有直接匹配,尝试递归搜索
@@ -110,17 +138,17 @@ class GameDetector:
f"vol {vol_num}" in file_lower)) or f"vol {vol_num}" in file_lower)) or
(is_after and "after" in file_lower)): (is_after and "after" in file_lower)):
if debug_mode: if debug_mode:
print(f"DEBUG: 通过递归搜索确认游戏版本: {potential_version}, 文件: {file}") self.logger.debug(f"通过递归搜索确认游戏版本: {potential_version}, 文件: {file}")
return potential_version return potential_version
# 如果仍然没有找到,基于目录名的推测返回结果 # 如果仍然没有找到,基于目录名的推测返回结果
if potential_version: if potential_version:
if debug_mode: if debug_mode:
print(f"DEBUG: 基于目录名返回推测的游戏版本: {potential_version}") self.logger.debug(f"基于目录名返回推测的游戏版本: {potential_version}")
return potential_version return potential_version
if debug_mode: if debug_mode:
print(f"DEBUG: 无法识别游戏版本: {game_dir}") self.logger.debug(f"无法识别游戏版本: {game_dir}")
return None return None
@@ -135,8 +163,14 @@ class GameDetector:
""" """
debug_mode = self._is_debug_mode() debug_mode = self._is_debug_mode()
# 检查缓存中是否已有该目录的识别结果
if selected_folder in self.directory_cache:
if debug_mode:
self.logger.debug(f"使用缓存的目录识别结果: {selected_folder}")
return self.directory_cache[selected_folder]
if debug_mode: if debug_mode:
print(f"--- 开始识别目录: {selected_folder} ---") self.logger.debug(f"--- 开始识别目录: {selected_folder} ---")
game_paths = {} game_paths = {}
@@ -144,10 +178,10 @@ class GameDetector:
try: try:
all_dirs = [d for d in os.listdir(selected_folder) if os.path.isdir(os.path.join(selected_folder, d))] all_dirs = [d for d in os.listdir(selected_folder) if os.path.isdir(os.path.join(selected_folder, d))]
if debug_mode: if debug_mode:
print(f"DEBUG: 找到以下子目录: {all_dirs}") self.logger.debug(f"找到以下子目录: {all_dirs}")
except Exception as e: except Exception as e:
if debug_mode: if debug_mode:
print(f"DEBUG: 无法读取目录 {selected_folder}: {str(e)}") self.logger.debug(f"无法读取目录 {selected_folder}: {str(e)}")
return {} return {}
for game, info in self.game_info.items(): for game, info in self.game_info.items():
@@ -155,7 +189,7 @@ class GameDetector:
expected_exe = info["exe"] # 标准可执行文件名 expected_exe = info["exe"] # 标准可执行文件名
if debug_mode: if debug_mode:
print(f"DEBUG: 搜索游戏 {game}, 预期目录: {expected_dir}, 预期可执行文件: {expected_exe}") self.logger.debug(f"搜索游戏 {game}, 预期目录: {expected_dir}, 预期可执行文件: {expected_exe}")
# 尝试不同的匹配方法 # 尝试不同的匹配方法
found_dir = None found_dir = None
@@ -164,7 +198,7 @@ class GameDetector:
if expected_dir in all_dirs: if expected_dir in all_dirs:
found_dir = expected_dir found_dir = expected_dir
if debug_mode: if debug_mode:
print(f"DEBUG: 精确匹配成功: {expected_dir}") self.logger.debug(f"精确匹配成功: {expected_dir}")
# 2. 大小写不敏感匹配 # 2. 大小写不敏感匹配
if not found_dir: if not found_dir:
@@ -172,7 +206,7 @@ class GameDetector:
if expected_dir.lower() == dir_name.lower(): if expected_dir.lower() == dir_name.lower():
found_dir = dir_name found_dir = dir_name
if debug_mode: if debug_mode:
print(f"DEBUG: 大小写不敏感匹配成功: {dir_name}") self.logger.debug(f"大小写不敏感匹配成功: {dir_name}")
break break
# 3. 更模糊的匹配(允许特殊字符差异) # 3. 更模糊的匹配(允许特殊字符差异)
@@ -186,7 +220,7 @@ class GameDetector:
if pattern.match(dir_name): if pattern.match(dir_name):
found_dir = dir_name found_dir = dir_name
if debug_mode: if debug_mode:
print(f"DEBUG: 模糊匹配成功: {dir_name} 匹配模式 {pattern_text}") self.logger.debug(f"模糊匹配成功: {dir_name} 匹配模式 {pattern_text}")
break break
# 4. 如果还是没找到,尝试更宽松的匹配 # 4. 如果还是没找到,尝试更宽松的匹配
@@ -196,7 +230,7 @@ class GameDetector:
if vol_match: if vol_match:
vol_num = vol_match.group(1) vol_num = vol_match.group(1)
if debug_mode: if debug_mode:
print(f"DEBUG: 提取卷号: {vol_num}") self.logger.debug(f"提取卷号: {vol_num}")
is_after = "after" in expected_dir.lower() is_after = "after" in expected_dir.lower()
@@ -207,7 +241,7 @@ class GameDetector:
if is_after and "after" in dir_lower: if is_after and "after" in dir_lower:
found_dir = dir_name found_dir = dir_name
if debug_mode: if debug_mode:
print(f"DEBUG: After特殊匹配成功: {dir_name}") self.logger.debug(f"After特殊匹配成功: {dir_name}")
break break
# 对于Vol特殊处理 # 对于Vol特殊处理
@@ -217,7 +251,7 @@ class GameDetector:
if dir_vol_match and dir_vol_match.group(1) == vol_num: if dir_vol_match and dir_vol_match.group(1) == vol_num:
found_dir = dir_name found_dir = dir_name
if debug_mode: if debug_mode:
print(f"DEBUG: 卷号匹配成功: {dir_name} 卷号 {vol_num}") self.logger.debug(f"卷号匹配成功: {dir_name} 卷号 {vol_num}")
break break
# 如果找到匹配的目录验证exe文件是否存在 # 如果找到匹配的目录验证exe文件是否存在
@@ -260,7 +294,7 @@ class GameDetector:
exe_exists = True exe_exists = True
found_exe = exe_variant found_exe = exe_variant
if debug_mode: if debug_mode:
print(f"DEBUG: 验证成功,找到游戏可执行文件: {exe_variant}") self.logger.debug(f"验证成功,找到游戏可执行文件: {exe_variant}")
break break
# 如果没有直接找到,尝试递归搜索当前目录下的所有可执行文件 # 如果没有直接找到,尝试递归搜索当前目录下的所有可执行文件
@@ -283,14 +317,14 @@ class GameDetector:
exe_exists = True exe_exists = True
found_exe = os.path.relpath(exe_path, potential_path) found_exe = os.path.relpath(exe_path, potential_path)
if debug_mode: if debug_mode:
print(f"DEBUG: 通过递归搜索找到游戏可执行文件: {found_exe}") self.logger.debug(f"通过递归搜索找到游戏可执行文件: {found_exe}")
break break
elif "After" in game and "after" in file_lower: elif "After" in game and "after" in file_lower:
exe_path = os.path.join(root, file) exe_path = os.path.join(root, file)
exe_exists = True exe_exists = True
found_exe = os.path.relpath(exe_path, potential_path) found_exe = os.path.relpath(exe_path, potential_path)
if debug_mode: if debug_mode:
print(f"DEBUG: 通过递归搜索找到After游戏可执行文件: {found_exe}") self.logger.debug(f"通过递归搜索找到After游戏可执行文件: {found_exe}")
break break
if exe_exists: if exe_exists:
break break
@@ -299,13 +333,22 @@ class GameDetector:
if exe_exists: if exe_exists:
game_paths[game] = potential_path game_paths[game] = potential_path
if debug_mode: if debug_mode:
print(f"DEBUG: 验证成功,将 {potential_path} 添加为 {game} 的目录") self.logger.debug(f"验证成功,将 {potential_path} 添加为 {game} 的目录")
else: else:
if debug_mode: if debug_mode:
print(f"DEBUG: 未找到任何可执行文件变体,游戏 {game}{potential_path} 未找到") self.logger.debug(f"未找到任何可执行文件变体,游戏 {game}{potential_path} 未找到")
if debug_mode: if debug_mode:
print(f"DEBUG: 最终识别的游戏目录: {game_paths}") self.logger.debug(f"最终识别的游戏目录: {game_paths}")
print(f"--- 目录识别结束 ---") self.logger.debug(f"--- 目录识别结束 ---")
# 将识别结果存入缓存
self.directory_cache[selected_folder] = game_paths
return game_paths return game_paths
def clear_directory_cache(self):
"""清除目录缓存"""
self.directory_cache = {}
if self._is_debug_mode():
self.logger.debug("已清除目录缓存")

View File

@@ -8,7 +8,7 @@ import threading
from PySide6.QtCore import QObject, Signal from PySide6.QtCore import QObject, Signal
from PySide6.QtWidgets import QDialog, QVBoxLayout, QLabel, QPushButton, QTextEdit, QProgressBar, QMessageBox from PySide6.QtWidgets import QDialog, QVBoxLayout, QLabel, QPushButton, QTextEdit, QProgressBar, QMessageBox
from data.config import APP_NAME from config.config import APP_NAME
from utils import msgbox_frame from utils import msgbox_frame
@@ -291,28 +291,6 @@ class IPv6Manager:
""" """
print(f"Toggle IPv6 support: {enabled}") print(f"Toggle IPv6 support: {enabled}")
# 如果用户尝试启用IPv6检查系统是否支持IPv6并发出警告
if enabled:
# 先显示警告提示
warning_msg_box = self._create_message_box(
"警告",
"\n目前IPv6支持功能仍在测试阶段可能会发生意料之外的bug\n\n您确定需要启用吗?\n",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
response = warning_msg_box.exec()
# 如果用户选择不启用,直接返回
if response != QMessageBox.StandardButton.Yes:
return False
# 用户确认启用后继续检查IPv6可用性
ipv6_available = self.check_ipv6_availability()
if not ipv6_available:
msg_box = self._create_message_box("错误", "\n未检测到可用的IPv6连接无法启用IPv6支持。\n\n请确保您的网络环境支持IPv6且已正确配置。\n")
msg_box.exec()
return False
# 保存设置到配置 # 保存设置到配置
if self.config is not None: if self.config is not None:
self.config["ipv6_enabled"] = enabled self.config["ipv6_enabled"] = enabled

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,378 @@
import os
import hashlib
import tempfile
import py7zr
import traceback
from utils.logger import setup_logger
from PySide6.QtWidgets import QMessageBox
from PySide6.QtCore import QTimer, QThread, Signal
from config.config import PLUGIN_HASH, APP_NAME
# 初始化logger
logger = setup_logger("patch_detector")
class PatchCheckThread(QThread):
"""用于在后台线程中执行补丁检查的线程"""
finished = Signal(bool) # (is_installed)
def __init__(self, checker_func, *args):
super().__init__()
self.checker_func = checker_func
self.args = args
def run(self):
result = self.checker_func(*self.args)
self.finished.emit(result)
class PatchDetector:
"""补丁检测与校验模块,用于统一处理在线和离线模式下的补丁检测和校验"""
def __init__(self, main_window):
"""初始化补丁检测器
Args:
main_window: 主窗口实例用于访问UI和状态
"""
self.main_window = main_window
self.app_name = main_window.APP_NAME if hasattr(main_window, 'APP_NAME') else ""
self.game_info = {}
self.plugin_hash = {}
self._load_game_info()
self.patch_check_thread = None
def _load_game_info(self):
"""从配置中加载游戏信息和补丁哈希值"""
try:
from config.config import GAME_INFO, PLUGIN_HASH
self.game_info = GAME_INFO
self.plugin_hash = PLUGIN_HASH
except ImportError:
logger.error("无法加载游戏信息或补丁哈希值配置")
def _is_debug_mode(self):
"""检查是否处于调试模式
Returns:
bool: 是否处于调试模式
"""
try:
if hasattr(self.main_window, 'debug_manager') and self.main_window.debug_manager:
if hasattr(self.main_window.debug_manager, '_is_debug_mode'):
return self.main_window.debug_manager._is_debug_mode()
elif hasattr(self.main_window, 'config'):
return self.main_window.config.get('debug_mode', False)
return False
except Exception:
return False
def check_patch_installed_async(self, game_dir, game_version, callback):
"""异步检查游戏是否已安装补丁"""
def on_finished(is_installed):
callback(is_installed)
self.patch_check_thread = None
self.patch_check_thread = PatchCheckThread(self._check_patch_installed_sync, game_dir, game_version)
self.patch_check_thread.finished.connect(on_finished)
self.patch_check_thread.start()
def _check_patch_installed_sync(self, game_dir, game_version):
"""同步检查游戏是否已安装补丁(在工作线程中运行)"""
debug_mode = self._is_debug_mode()
if debug_mode:
logger.debug(f"DEBUG: 检查 {game_version} 是否已安装补丁,目录: {game_dir}")
if game_version not in self.game_info:
if debug_mode:
logger.debug(f"DEBUG: {game_version} 不在支持的游戏列表中,跳过检查")
return False
install_path_base = os.path.basename(self.game_info[game_version]["install_path"])
patch_file_path = os.path.join(game_dir, install_path_base)
# 检查补丁文件和禁用的补丁文件
if os.path.exists(patch_file_path) or os.path.exists(f"{patch_file_path}.fain"):
return True
return False
def check_patch_installed(self, game_dir, game_version):
"""检查游戏是否已安装补丁(此方法可能导致阻塞,推荐使用异步版本)"""
return self._check_patch_installed_sync(game_dir, game_version)
def check_patch_disabled(self, game_dir, game_version):
"""检查游戏的补丁是否已被禁用"""
debug_mode = self._is_debug_mode()
if game_version not in self.game_info:
return False, None
install_path_base = os.path.basename(self.game_info[game_version]["install_path"])
patch_file_path = os.path.join(game_dir, install_path_base)
disabled_path = f"{patch_file_path}.fain"
if os.path.exists(disabled_path):
if debug_mode:
logger.debug(f"找到禁用的补丁文件: {disabled_path}")
return True, disabled_path
if debug_mode:
logger.debug(f"{game_version}{game_dir} 的补丁未被禁用")
return False, None
def detect_installable_games(self, game_dirs):
"""检测可安装补丁的游戏"""
debug_mode = self._is_debug_mode()
if debug_mode:
logger.debug(f"开始检测可安装补丁的游戏,游戏目录: {game_dirs}")
already_installed_games = []
installable_games = []
disabled_patch_games = []
for game_version, game_dir in game_dirs.items():
is_patch_installed = self.check_patch_installed(game_dir, game_version)
hash_check_passed = self.main_window.installed_status.get(game_version, False)
if is_patch_installed or hash_check_passed:
if debug_mode:
logger.debug(f"{game_version} 已安装补丁,不需要再次安装")
logger.debug(f"文件检查结果: {is_patch_installed}, 哈希检查结果: {hash_check_passed}")
already_installed_games.append(game_version)
self.main_window.installed_status[game_version] = True
else:
is_disabled, disabled_path = self.check_patch_disabled(game_dir, game_version)
if is_disabled:
if debug_mode:
logger.debug(f"{game_version} 存在被禁用的补丁: {disabled_path}")
disabled_patch_games.append(game_version)
else:
if debug_mode:
logger.debug(f"{game_version} 未安装补丁,可以安装")
logger.debug(f"文件检查结果: {is_patch_installed}, 哈希检查结果: {hash_check_passed}")
installable_games.append(game_version)
if debug_mode:
logger.debug(f"检测结果 - 已安装补丁: {already_installed_games}")
logger.debug(f"检测结果 - 可安装补丁: {installable_games}")
logger.debug(f"检测结果 - 禁用补丁: {disabled_patch_games}")
return already_installed_games, installable_games, disabled_patch_games
def verify_patch_hash(self, game_version, file_path):
"""验证补丁文件的哈希值"""
expected_hash = self.plugin_hash.get(game_version, "")
if not expected_hash:
logger.warning(f"DEBUG: 未找到 {game_version} 的预期哈希值")
return False
debug_mode = self._is_debug_mode()
if debug_mode:
logger.debug(f"DEBUG: 开始验证补丁文件: {file_path}")
logger.debug(f"DEBUG: 游戏版本: {game_version}")
logger.debug(f"DEBUG: 预期哈希值: {expected_hash}")
try:
if not os.path.exists(file_path) or os.path.getsize(file_path) == 0:
return False
with tempfile.TemporaryDirectory() as temp_dir:
if debug_mode:
logger.debug(f"DEBUG: 创建临时目录: {temp_dir}")
try:
with py7zr.SevenZipFile(file_path, mode="r") as archive:
archive.extractall(path=temp_dir)
except Exception as e:
if debug_mode:
logger.error(f"DEBUG: 解压补丁文件失败: {e}")
return False
patch_file = self._find_patch_file_in_temp_dir(temp_dir, game_version)
if not patch_file or not os.path.exists(patch_file):
if debug_mode:
logger.warning(f"DEBUG: 未找到解压后的补丁文件")
return False
if debug_mode:
logger.debug(f"DEBUG: 找到解压后的补丁文件: {patch_file}")
try:
with open(patch_file, "rb") as f:
file_hash = hashlib.sha256(f.read()).hexdigest()
result = file_hash.lower() == expected_hash.lower()
if debug_mode:
logger.debug(f"DEBUG: 补丁文件 {patch_file} 哈希值验证: {'成功' if result else '失败'}")
return result
except Exception as e:
if debug_mode:
logger.error(f"DEBUG: 计算补丁文件哈希值失败: {e}")
return False
except Exception as e:
if debug_mode:
logger.error(f"DEBUG: 验证补丁哈希值失败: {e}")
return False
def _find_patch_file_in_temp_dir(self, temp_dir, game_version):
"""在临时目录中查找解压后的补丁文件"""
game_patch_map = {
"Vol.1": os.path.join("vol.1", "adultsonly.xp3"),
"Vol.2": os.path.join("vol.2", "adultsonly.xp3"),
"Vol.3": os.path.join("vol.3", "update00.int"),
"Vol.4": os.path.join("vol.4", "vol4adult.xp3"),
"After": os.path.join("after", "afteradult.xp3"),
}
for version_keyword, relative_path in game_patch_map.items():
if version_keyword in game_version:
return os.path.join(temp_dir, relative_path)
# 如果没有找到,则进行通用搜索
for root, dirs, files in os.walk(temp_dir):
for file in files:
if file.endswith('.xp3') or file.endswith('.int'):
return os.path.join(root, file)
return None
def create_hash_thread(self, mode, install_paths):
from workers.hash_thread import HashThread
return HashThread(mode, install_paths, PLUGIN_HASH, self.main_window.installed_status, self.main_window)
def after_hash_compare(self):
is_offline = self.main_window.offline_mode_manager.is_in_offline_mode()
self.main_window.close_hash_msg_box()
self.main_window.hash_msg_box = self.main_window.hash_manager.hash_pop_window(check_type="after", is_offline=is_offline)
install_paths = self.main_window.download_manager.get_install_paths()
self.main_window.hash_thread = self.create_hash_thread("after", install_paths)
self.main_window.hash_thread.after_finished.connect(self.on_after_hash_finished)
self.main_window.hash_thread.start()
def on_after_hash_finished(self, result):
self.main_window.close_hash_msg_box()
if not result["passed"]:
self.main_window.setEnabled(True)
game = result.get("game", "未知游戏")
message = result.get("message", "发生未知错误。")
QMessageBox.critical(self.main_window, f"文件校验失败 - {APP_NAME}", message)
self.main_window.setEnabled(True)
if hasattr(self.main_window, 'window_manager'):
self.main_window.window_manager.change_window_state(self.main_window.window_manager.STATE_READY)
QTimer.singleShot(100, self.main_window.show_result)
def on_offline_pre_hash_finished(self, updated_status, game_dirs):
self.main_window.installed_status = updated_status
if self.main_window.hash_msg_box and self.main_window.hash_msg_box.isVisible():
self.main_window.hash_msg_box.accept()
self.main_window.hash_msg_box = None
self.main_window.setEnabled(True)
already_installed_games, installable_games, disabled_patch_games = self.detect_installable_games(game_dirs)
status_message = ""
if already_installed_games:
status_message += f"已安装补丁的游戏:\n{chr(10).join(already_installed_games)}\n\n"
if disabled_patch_games:
disabled_msg = f"检测到以下游戏的补丁已被禁用:\n{chr(10).join(disabled_patch_games)}\n\n是否要启用这些补丁?"
from PySide6 import QtWidgets
reply = QtWidgets.QMessageBox.question(
self.main_window,
f"检测到禁用补丁 - {APP_NAME}",
disabled_msg,
QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No
)
if reply == QtWidgets.QMessageBox.StandardButton.Yes:
disabled_game_dirs = {game: game_dirs[game] for game in disabled_patch_games}
success_count, fail_count, results = self.main_window.patch_manager.batch_toggle_patches(
disabled_game_dirs,
operation="enable"
)
self.main_window.patch_manager.show_toggle_result(success_count, fail_count, results)
for game_version in disabled_patch_games:
self.main_window.installed_status[game_version] = True
if game_version in installable_games:
installable_games.remove(game_version)
if game_version not in already_installed_games:
already_installed_games.append(game_version)
else:
installable_games.extend(disabled_patch_games)
if disabled_patch_games:
status_message += f"禁用补丁的游戏:\n{chr(10).join(disabled_patch_games)}\n\n"
if not installable_games:
if already_installed_games:
QMessageBox.information(
self.main_window,
f"信息 - {APP_NAME}",
f"\n所有游戏已安装补丁,无需重复安装。\n\n{status_message}",
)
else:
QMessageBox.warning(
self.main_window,
f"警告 - {APP_NAME}",
"\n未检测到任何需要安装补丁的游戏。\n\n请确保游戏文件夹位于选择的目录中。\n",
)
if hasattr(self.main_window, 'window_manager'):
self.main_window.window_manager.change_window_state(self.main_window.window_manager.STATE_READY)
return
from PySide6 import QtWidgets
dialog = QtWidgets.QDialog(self.main_window)
dialog.setWindowTitle(f"选择要安装的游戏 - {APP_NAME}")
dialog.setMinimumWidth(300)
layout = QtWidgets.QVBoxLayout()
label = QtWidgets.QLabel("请选择要安装补丁的游戏:")
layout.addWidget(label)
list_widget = QtWidgets.QListWidget()
list_widget.setSelectionMode(QtWidgets.QAbstractItemView.SelectionMode.MultiSelection)
for game in installable_games:
item = QtWidgets.QListWidgetItem(game)
list_widget.addItem(item)
item.setSelected(True)
layout.addWidget(list_widget)
button_box = QtWidgets.QDialogButtonBox(
QtWidgets.QDialogButtonBox.StandardButton.Ok |
QtWidgets.QDialogButtonBox.StandardButton.Cancel
)
button_box.accepted.connect(dialog.accept)
button_box.rejected.connect(dialog.reject)
layout.addWidget(button_box)
dialog.setLayout(layout)
result = dialog.exec()
if result != QtWidgets.QDialog.DialogCode.Accepted or not list_widget.selectedItems():
if hasattr(self.main_window, 'window_manager'):
self.main_window.window_manager.change_window_state(self.main_window.window_manager.STATE_READY)
return
selected_games = [item.text() for item in list_widget.selectedItems()]
self.main_window.offline_mode_manager.install_offline_patches(selected_games)

View File

@@ -0,0 +1,983 @@
import os
import shutil
import traceback
from PySide6.QtWidgets import QMessageBox
from utils.logger import setup_logger
from config.config import APP_NAME
from utils import msgbox_frame
class PatchManager:
"""补丁管理器,用于处理补丁的安装和卸载"""
def __init__(self, app_name, game_info, debug_manager=None, main_window=None):
"""初始化补丁管理器
Args:
app_name: 应用程序名称,用于显示消息框标题
game_info: 游戏信息字典,包含各版本的安装路径和可执行文件名
debug_manager: 调试管理器实例,用于输出调试信息
main_window: 主窗口实例用于访问UI和状态
"""
self.app_name = app_name
self.game_info = game_info
self.debug_manager = debug_manager
self.main_window = main_window # 添加main_window属性
self.installed_status = {} # 游戏版本的安装状态
self.logger = setup_logger("patch_manager")
self.patch_detector = None # 将在main_window初始化后设置
def set_patch_detector(self, patch_detector):
"""设置补丁检测器实例
Args:
patch_detector: 补丁检测器实例
"""
self.patch_detector = patch_detector
def _is_debug_mode(self):
"""检查是否处于调试模式
Returns:
bool: 是否处于调试模式
"""
if hasattr(self.debug_manager, 'ui_manager') and hasattr(self.debug_manager.ui_manager, 'debug_action'):
return self.debug_manager.ui_manager.debug_action.isChecked()
return False
def initialize_status(self):
"""初始化所有游戏版本的安装状态"""
self.installed_status = {f"NEKOPARA Vol.{i}": False for i in range(1, 5)}
self.installed_status["NEKOPARA After"] = False
def update_status(self, game_version, is_installed):
"""更新游戏版本的安装状态
Args:
game_version: 游戏版本
is_installed: 是否已安装
"""
self.installed_status[game_version] = is_installed
def get_status(self, game_version=None):
"""获取游戏版本的安装状态
Args:
game_version: 游戏版本如果为None则返回所有状态
Returns:
bool或dict: 指定版本的安装状态或所有版本的安装状态
"""
if game_version:
return self.installed_status.get(game_version, False)
return self.installed_status
def uninstall_patch(self, game_dir, game_version, silent=False):
"""卸载补丁
Args:
game_dir: 游戏目录路径
game_version: 游戏版本
silent: 是否静默模式(不显示弹窗)
Returns:
bool: 卸载成功返回True失败返回False
dict: 在silent=True时返回包含卸载结果信息的字典
"""
debug_mode = self._is_debug_mode()
if debug_mode:
self.logger.debug(f"DEBUG: 开始卸载 {game_version} 补丁,目录: {game_dir}")
self.logger.info(f"开始卸载 {game_version} 补丁,目录: {game_dir}")
if game_version not in self.game_info:
error_msg = f"无法识别游戏版本: {game_version}"
if debug_mode:
self.logger.debug(f"DEBUG: 卸载失败 - {error_msg}")
self.logger.error(f"卸载失败 - {error_msg}")
if not silent:
QMessageBox.critical(
None,
f"错误 - {self.app_name}",
f"\n{error_msg}\n",
QMessageBox.StandardButton.Ok,
)
return False if not silent else {"success": False, "message": error_msg, "files_removed": 0}
try:
files_removed = 0
# 获取可能的补丁文件路径
install_path_base = os.path.basename(self.game_info[game_version]["install_path"])
patch_file_path = os.path.join(game_dir, install_path_base)
if debug_mode:
self.logger.debug(f"DEBUG: 基础补丁文件路径: {patch_file_path}")
# 尝试查找补丁文件,支持不同大小写
patch_files_to_check = [
patch_file_path,
patch_file_path.lower(),
patch_file_path.upper(),
patch_file_path.replace("_", ""),
patch_file_path.replace("_", "-"),
]
if debug_mode:
self.logger.debug(f"DEBUG: 查找以下可能的补丁文件路径: {patch_files_to_check}")
# 查找并删除补丁文件,包括启用和禁用的
patch_file_found = False
for patch_path in patch_files_to_check:
# 检查常规补丁文件
if os.path.exists(patch_path):
patch_file_found = True
if debug_mode:
self.logger.debug(f"DEBUG: 找到补丁文件: {patch_path},准备删除")
self.logger.debug(f"删除补丁文件: {patch_path}")
os.remove(patch_path)
files_removed += 1
if debug_mode:
self.logger.debug(f"DEBUG: 已删除补丁文件: {patch_path}")
# 检查被禁用的补丁文件(带.fain后缀
disabled_path = f"{patch_path}.fain"
if os.path.exists(disabled_path):
patch_file_found = True
if debug_mode:
self.logger.debug(f"DEBUG: 找到被禁用的补丁文件: {disabled_path},准备删除")
self.logger.debug(f"删除被禁用的补丁文件: {disabled_path}")
os.remove(disabled_path)
files_removed += 1
if debug_mode:
self.logger.debug(f"DEBUG: 已删除被禁用的补丁文件: {disabled_path}")
if not patch_file_found:
if debug_mode:
self.logger.debug(f"DEBUG: 未找到补丁文件,检查了以下路径: {patch_files_to_check}")
self.logger.debug(f"DEBUG: 也检查了禁用的补丁文件(.fain后缀")
self.logger.warning(f"未找到 {game_version} 的补丁文件")
# 检查是否有额外的签名文件 (.sig)
if game_version == "NEKOPARA After":
if debug_mode:
self.logger.debug(f"DEBUG: {game_version} 需要检查额外的签名文件")
for patch_path in patch_files_to_check:
# 检查常规签名文件
sig_file_path = f"{patch_path}.sig"
if os.path.exists(sig_file_path):
if debug_mode:
self.logger.debug(f"DEBUG: 找到签名文件: {sig_file_path},准备删除")
self.logger.debug(f"删除签名文件: {sig_file_path}")
os.remove(sig_file_path)
files_removed += 1
if debug_mode:
self.logger.debug(f"DEBUG: 已删除签名文件: {sig_file_path}")
# 检查被禁用补丁的签名文件
disabled_sig_path = f"{patch_path}.fain.sig"
if os.path.exists(disabled_sig_path):
if debug_mode:
self.logger.debug(f"DEBUG: 找到被禁用补丁的签名文件: {disabled_sig_path},准备删除")
self.logger.debug(f"删除被禁用补丁的签名文件: {disabled_sig_path}")
os.remove(disabled_sig_path)
files_removed += 1
if debug_mode:
self.logger.debug(f"DEBUG: 已删除被禁用补丁的签名文件: {disabled_sig_path}")
# 删除patch文件夹
if debug_mode:
self.logger.debug(f"DEBUG: 检查并删除patch文件夹")
patch_folders_to_check = [
os.path.join(game_dir, "patch"),
os.path.join(game_dir, "Patch"),
os.path.join(game_dir, "PATCH"),
]
for patch_folder in patch_folders_to_check:
if os.path.exists(patch_folder):
if debug_mode:
self.logger.debug(f"DEBUG: 找到补丁文件夹: {patch_folder},准备删除")
self.logger.debug(f"删除补丁文件夹: {patch_folder}")
import shutil
shutil.rmtree(patch_folder)
files_removed += 1
if debug_mode:
self.logger.debug(f"DEBUG: 已删除补丁文件夹: {patch_folder}")
# 删除game/patch文件夹
if debug_mode:
self.logger.debug(f"DEBUG: 检查并删除game/patch文件夹")
game_folders = ["game", "Game", "GAME"]
patch_folders = ["patch", "Patch", "PATCH"]
for game_folder in game_folders:
for patch_folder in patch_folders:
game_patch_folder = os.path.join(game_dir, game_folder, patch_folder)
if os.path.exists(game_patch_folder):
if debug_mode:
self.logger.debug(f"DEBUG: 找到game/patch文件夹: {game_patch_folder},准备删除")
self.logger.debug(f"删除game/patch文件夹: {game_patch_folder}")
import shutil
shutil.rmtree(game_patch_folder)
files_removed += 1
if debug_mode:
self.logger.debug(f"DEBUG: 已删除game/patch文件夹: {game_patch_folder}")
# 删除配置文件
if debug_mode:
self.logger.debug(f"DEBUG: 检查并删除配置文件和脚本文件")
config_files = ["config.json", "Config.json", "CONFIG.JSON"]
script_files = ["scripts.json", "Scripts.json", "SCRIPTS.JSON"]
for game_folder in game_folders:
game_path = os.path.join(game_dir, game_folder)
if os.path.exists(game_path):
# 删除配置文件
for config_file in config_files:
config_path = os.path.join(game_path, config_file)
if os.path.exists(config_path):
if debug_mode:
self.logger.debug(f"DEBUG: 找到配置文件: {config_path},准备删除")
self.logger.debug(f"删除配置文件: {config_path}")
os.remove(config_path)
files_removed += 1
if debug_mode:
self.logger.debug(f"DEBUG: 已删除配置文件: {config_path}")
# 删除脚本文件
for script_file in script_files:
script_path = os.path.join(game_path, script_file)
if os.path.exists(script_path):
if debug_mode:
self.logger.debug(f"DEBUG: 找到脚本文件: {script_path},准备删除")
self.logger.debug(f"删除脚本文件: {script_path}")
os.remove(script_path)
files_removed += 1
if debug_mode:
self.logger.debug(f"DEBUG: 已删除脚本文件: {script_path}")
# 更新安装状态
self.installed_status[game_version] = False
if debug_mode:
self.logger.debug(f"DEBUG: 已更新 {game_version} 的安装状态为未安装")
# 在非静默模式且非批量卸载模式下显示卸载成功消息
if not silent and game_version != "all":
# 显示卸载成功消息
if files_removed > 0:
success_msg = f"\n{game_version} 补丁卸载成功!\n共删除 {files_removed} 个文件/文件夹。\n"
if debug_mode:
self.logger.debug(f"DEBUG: 显示卸载成功消息: {success_msg}")
QMessageBox.information(
None,
f"卸载完成 - {self.app_name}",
success_msg,
QMessageBox.StandardButton.Ok,
)
else:
warning_msg = f"\n未找到 {game_version} 的补丁文件,可能未安装补丁或已被移除。\n"
if debug_mode:
self.logger.debug(f"DEBUG: 显示警告消息: {warning_msg}")
QMessageBox.warning(
None,
f"警告 - {self.app_name}",
warning_msg,
QMessageBox.StandardButton.Ok,
)
# 卸载成功
if debug_mode:
self.logger.debug(f"DEBUG: {game_version} 卸载完成,共删除 {files_removed} 个文件/文件夹")
self.logger.info(f"{game_version} 卸载完成,共删除 {files_removed} 个文件/文件夹")
if silent:
return {"success": True, "message": f"{game_version} 补丁卸载成功", "files_removed": files_removed}
return True
except Exception as e:
error_message = f"卸载 {game_version} 补丁时出错:{str(e)}"
if debug_mode:
self.logger.debug(f"DEBUG: {error_message}")
import traceback
self.logger.debug(f"DEBUG: 错误详情:\n{traceback.format_exc()}")
self.logger.error(error_message)
# 在非静默模式且非批量卸载模式下显示卸载失败消息
if not silent and game_version != "all":
# 显示卸载失败消息
error_message = f"\n卸载 {game_version} 补丁时出错:\n\n{str(e)}\n"
if debug_mode:
self.logger.debug(f"DEBUG: 显示卸载失败消息")
QMessageBox.critical(
None,
f"卸载失败 - {self.app_name}",
error_message,
QMessageBox.StandardButton.Ok,
)
# 卸载失败
if silent:
return {"success": False, "message": f"卸载 {game_version} 补丁时出错: {str(e)}", "files_removed": 0}
return False
def batch_uninstall_patches(self, game_dirs):
"""批量卸载多个游戏的补丁
Args:
game_dirs: 游戏版本到游戏目录的映射字典
Returns:
tuple: (成功数量, 失败数量, 详细结果列表)
"""
success_count = 0
fail_count = 0
debug_mode = self._is_debug_mode()
results = []
if debug_mode:
self.logger.debug(f"DEBUG: 开始批量卸载补丁,游戏数量: {len(game_dirs)}")
self.logger.debug(f"DEBUG: 要卸载的游戏: {list(game_dirs.keys())}")
self.logger.info(f"开始批量卸载补丁,游戏数量: {len(game_dirs)}")
self.logger.debug(f"要卸载的游戏: {list(game_dirs.keys())}")
for version, path in game_dirs.items():
if debug_mode:
self.logger.debug(f"DEBUG: 处理游戏 {version},路径: {path}")
self.logger.info(f"开始卸载 {version} 的补丁")
try:
# 在批量模式下使用静默卸载
if debug_mode:
self.logger.debug(f"DEBUG: 使用静默模式卸载 {version}")
result = self.uninstall_patch(path, version, silent=True)
if isinstance(result, dict): # 使用了静默模式
if result["success"]:
success_count += 1
if debug_mode:
self.logger.debug(f"DEBUG: {version} 卸载成功,删除了 {result['files_removed']} 个文件/文件夹")
self.logger.info(f"{version} 卸载成功,删除了 {result['files_removed']} 个文件/文件夹")
else:
fail_count += 1
if debug_mode:
self.logger.debug(f"DEBUG: {version} 卸载失败,原因: {result['message']}")
self.logger.warning(f"{version} 卸载失败,原因: {result['message']}")
results.append({
"version": version,
"success": result["success"],
"message": result["message"],
"files_removed": result["files_removed"]
})
else: # 兼容旧代码,不应该执行到这里
if result:
success_count += 1
if debug_mode:
self.logger.debug(f"DEBUG: {version} 卸载成功(旧格式)")
self.logger.info(f"{version} 卸载成功(旧格式)")
else:
fail_count += 1
if debug_mode:
self.logger.debug(f"DEBUG: {version} 卸载失败(旧格式)")
self.logger.warning(f"{version} 卸载失败(旧格式)")
results.append({
"version": version,
"success": result,
"message": f"{version} 卸载{'成功' if result else '失败'}",
"files_removed": 0
})
except Exception as e:
if debug_mode:
self.logger.debug(f"DEBUG: 卸载 {version} 时出错: {str(e)}")
import traceback
self.logger.debug(f"DEBUG: 错误详情:\n{traceback.format_exc()}")
self.logger.error(f"卸载 {version} 时出错: {str(e)}")
fail_count += 1
results.append({
"version": version,
"success": False,
"message": f"卸载出错: {str(e)}",
"files_removed": 0
})
if debug_mode:
self.logger.debug(f"DEBUG: 批量卸载完成,成功: {success_count},失败: {fail_count}")
self.logger.info(f"批量卸载完成,成功: {success_count},失败: {fail_count}")
return success_count, fail_count, results
def show_uninstall_result(self, success_count, fail_count, results=None):
"""显示批量卸载结果
Args:
success_count: 成功卸载的数量
fail_count: 卸载失败的数量
results: 详细结果列表,如果提供,会显示更详细的信息
"""
debug_mode = self._is_debug_mode()
if debug_mode:
self.logger.debug(f"DEBUG: 显示卸载结果,成功: {success_count},失败: {fail_count}")
result_text = f"\n批量卸载完成!\n成功: {success_count}\n失败: {fail_count}\n"
# 如果有详细结果,添加到消息中
if results:
success_list = [r["version"] for r in results if r["success"]]
fail_list = [r["version"] for r in results if not r["success"]]
if debug_mode:
self.logger.debug(f"DEBUG: 成功卸载的游戏: {success_list}")
self.logger.debug(f"DEBUG: 卸载失败的游戏: {fail_list}")
if success_list:
result_text += f"\n【成功卸载】:\n{chr(10).join(success_list)}\n"
if fail_list:
result_text += f"\n【卸载失败】:\n{chr(10).join(fail_list)}\n"
# 记录更详细的失败原因
if debug_mode:
for r in results:
if not r["success"]:
self.logger.debug(f"DEBUG: {r['version']} 卸载失败原因: {r['message']}")
if debug_mode:
self.logger.debug(f"DEBUG: 显示卸载结果对话框")
QMessageBox.information(
None,
f"批量卸载完成 - {self.app_name}",
result_text,
QMessageBox.StandardButton.Ok,
)
def check_patch_installed(self, game_dir, game_version):
"""检查游戏是否已安装补丁调用patch_detector
Args:
game_dir: 游戏目录路径
game_version: 游戏版本
Returns:
bool: 如果已安装补丁或有被禁用的补丁文件返回True否则返回False
"""
if self.patch_detector:
return self.patch_detector.check_patch_installed(game_dir, game_version)
# 如果patch_detector未设置使用原始逻辑应该不会执行到这里
debug_mode = self._is_debug_mode()
if game_version not in self.game_info:
return False
# 获取可能的补丁文件路径
install_path_base = os.path.basename(self.game_info[game_version]["install_path"])
patch_file_path = os.path.join(game_dir, install_path_base)
# 尝试查找补丁文件,支持不同大小写
patch_files_to_check = [
patch_file_path,
patch_file_path.lower(),
patch_file_path.upper(),
patch_file_path.replace("_", ""),
patch_file_path.replace("_", "-"),
]
# 查找补丁文件
for patch_path in patch_files_to_check:
if os.path.exists(patch_path):
if debug_mode:
self.logger.debug(f"找到补丁文件: {patch_path}")
return True
# 检查是否存在被禁用的补丁文件(带.fain后缀
disabled_path = f"{patch_path}.fain"
if os.path.exists(disabled_path):
if debug_mode:
self.logger.debug(f"找到被禁用的补丁文件: {disabled_path}")
return True
# 检查是否有补丁文件夹
patch_folders_to_check = [
os.path.join(game_dir, "patch"),
os.path.join(game_dir, "Patch"),
os.path.join(game_dir, "PATCH"),
]
for patch_folder in patch_folders_to_check:
if os.path.exists(patch_folder):
if debug_mode:
self.logger.debug(f"找到补丁文件夹: {patch_folder}")
return True
# 检查game/patch文件夹
game_folders = ["game", "Game", "GAME"]
patch_folders = ["patch", "Patch", "PATCH"]
for game_folder in game_folders:
for patch_folder in patch_folders:
game_patch_folder = os.path.join(game_dir, game_folder, patch_folder)
if os.path.exists(game_patch_folder):
if debug_mode:
self.logger.debug(f"找到game/patch文件夹: {game_patch_folder}")
return True
# 检查配置文件
config_files = ["config.json", "Config.json", "CONFIG.JSON"]
script_files = ["scripts.json", "Scripts.json", "SCRIPTS.JSON"]
for game_folder in game_folders:
game_path = os.path.join(game_dir, game_folder)
if os.path.exists(game_path):
# 检查配置文件
for config_file in config_files:
config_path = os.path.join(game_path, config_file)
if os.path.exists(config_path):
if debug_mode:
self.logger.debug(f"找到配置文件: {config_path}")
return True
# 检查脚本文件
for script_file in script_files:
script_path = os.path.join(game_path, script_file)
if os.path.exists(script_path):
if debug_mode:
self.logger.debug(f"找到脚本文件: {script_path}")
return True
# 没有找到补丁文件或文件夹
if debug_mode:
self.logger.debug(f"{game_version}{game_dir} 中没有安装补丁")
return False
def check_patch_disabled(self, game_dir, game_version):
"""检查游戏的补丁是否已被禁用调用patch_detector
Args:
game_dir: 游戏目录路径
game_version: 游戏版本
Returns:
bool: 如果补丁被禁用返回True否则返回False
str: 禁用的补丁文件路径如果没有禁用返回None
"""
if self.patch_detector:
return self.patch_detector.check_patch_disabled(game_dir, game_version)
# 如果patch_detector未设置使用原始逻辑应该不会执行到这里
debug_mode = self._is_debug_mode()
if game_version not in self.game_info:
return False, None
# 获取可能的补丁文件路径
install_path_base = os.path.basename(self.game_info[game_version]["install_path"])
patch_file_path = os.path.join(game_dir, install_path_base)
# 检查是否存在禁用的补丁文件(.fain后缀
disabled_patch_files = [
f"{patch_file_path}.fain",
f"{patch_file_path.lower()}.fain",
f"{patch_file_path.upper()}.fain",
f"{patch_file_path.replace('_', '')}.fain",
f"{patch_file_path.replace('_', '-')}.fain",
]
# 检查是否有禁用的补丁文件
for disabled_path in disabled_patch_files:
if os.path.exists(disabled_path):
if debug_mode:
self.logger.debug(f"找到禁用的补丁文件: {disabled_path}")
return True, disabled_path
if debug_mode:
self.logger.debug(f"{game_version}{game_dir} 的补丁未被禁用")
return False, None
def toggle_patch(self, game_dir, game_version, operation=None, silent=False):
"""切换补丁的禁用/启用状态
Args:
game_dir: 游戏目录路径
game_version: 游戏版本
operation: 指定操作,可以是"enable""disable"或NoneNone则自动切换当前状态
silent: 是否静默模式(不显示弹窗)
Returns:
dict: 包含操作结果信息的字典
"""
debug_mode = self._is_debug_mode()
if debug_mode:
self.logger.debug(f"开始切换补丁状态 - 游戏版本: {game_version}, 游戏目录: {game_dir}, 操作: {operation}")
if game_version not in self.game_info:
if debug_mode:
self.logger.debug(f"无法识别游戏版本: {game_version}")
if not silent:
QMessageBox.critical(
None,
f"错误 - {self.app_name}",
f"\n无法识别游戏版本: {game_version}\n",
QMessageBox.StandardButton.Ok,
)
return {"success": False, "message": f"无法识别游戏版本: {game_version}", "action": "none"}
# 检查补丁是否已安装
is_patch_installed = self.check_patch_installed(game_dir, game_version)
if debug_mode:
self.logger.debug(f"补丁安装状态检查结果: {is_patch_installed}")
if not is_patch_installed:
if debug_mode:
self.logger.debug(f"{game_version} 未安装补丁,无法进行禁用/启用操作")
if not silent:
QMessageBox.warning(
None,
f"提示 - {self.app_name}",
f"\n{game_version} 未安装补丁,无法进行禁用/启用操作。\n",
QMessageBox.StandardButton.Ok,
)
return {"success": False, "message": f"{game_version} 未安装补丁", "action": "none"}
try:
# 检查当前状态
is_disabled, disabled_path = self.check_patch_disabled(game_dir, game_version)
if debug_mode:
self.logger.debug(f"补丁禁用状态检查结果 - 是否禁用: {is_disabled}, 禁用路径: {disabled_path}")
# 获取可能的补丁文件路径
install_path_base = os.path.basename(self.game_info[game_version]["install_path"])
patch_file_path = os.path.join(game_dir, install_path_base)
# 尝试查找原始补丁文件,支持不同大小写
patch_files_to_check = [
patch_file_path,
patch_file_path.lower(),
patch_file_path.upper(),
patch_file_path.replace("_", ""),
patch_file_path.replace("_", "-"),
]
if debug_mode:
self.logger.debug(f"将检查以下可能的补丁文件: {patch_files_to_check}")
# 确定操作类型
if operation:
if operation == "enable":
action_needed = is_disabled # 只有当前是禁用状态时才需要启用
elif operation == "disable":
action_needed = not is_disabled # 只有当前是启用状态时才需要禁用
else:
action_needed = True # 无效操作类型,强制进行操作
else:
action_needed = True # 未指定操作类型,始终执行切换
if debug_mode:
self.logger.debug(f"操作决策 - 操作类型: {operation}, 是否需要执行操作: {action_needed}")
if not action_needed:
# 补丁已经是目标状态,无需操作
if operation == "enable":
message = f"{game_version} 补丁已经是启用状态"
else:
message = f"{game_version} 补丁已经是禁用状态"
if debug_mode:
self.logger.debug(f"{message}, 无需操作")
if not silent:
QMessageBox.information(
None,
f"提示 - {self.app_name}",
f"\n{message}\n",
QMessageBox.StandardButton.Ok,
)
return {"success": True, "message": message, "action": "none"}
if is_disabled:
# 当前是禁用状态,需要启用
if disabled_path and os.path.exists(disabled_path):
# 从禁用文件名去掉.fain后缀
enabled_path = disabled_path[:-5] # 去掉.fain
if debug_mode:
self.logger.debug(f"正在启用补丁 - 从 {disabled_path} 重命名为 {enabled_path}")
os.rename(disabled_path, enabled_path)
if debug_mode:
self.logger.debug(f"已启用 {game_version} 的补丁,重命名文件成功")
action = "enable"
message = f"{game_version} 补丁已启用"
else:
# 未找到禁用的补丁文件,但状态是禁用
message = f"未找到禁用的补丁文件: {disabled_path}"
if debug_mode:
self.logger.debug(f"{message}")
return {"success": False, "message": message, "action": "none"}
else:
# 当前是启用状态,需要禁用
# 查找正在使用的补丁文件
active_patch_file = None
for patch_path in patch_files_to_check:
if os.path.exists(patch_path):
active_patch_file = patch_path
if debug_mode:
self.logger.debug(f"找到活跃的补丁文件: {active_patch_file}")
break
if active_patch_file:
# 给补丁文件添加.fain后缀禁用它
disabled_path = f"{active_patch_file}.fain"
if debug_mode:
self.logger.debug(f"正在禁用补丁 - 从 {active_patch_file} 重命名为 {disabled_path}")
os.rename(active_patch_file, disabled_path)
if debug_mode:
self.logger.debug(f"已禁用 {game_version} 的补丁,重命名文件成功")
action = "disable"
message = f"{game_version} 补丁已禁用"
else:
# 未找到活跃的补丁文件,但状态是启用
message = f"未找到启用的补丁文件,请检查游戏目录: {game_dir}"
if debug_mode:
self.logger.debug(f"{message}")
return {"success": False, "message": message, "action": "none"}
# 非静默模式下显示操作结果
if not silent:
QMessageBox.information(
None,
f"操作成功 - {self.app_name}",
f"\n{message}\n",
QMessageBox.StandardButton.Ok,
)
if debug_mode:
self.logger.debug(f"切换补丁状态操作完成 - 结果: 成功, 操作: {action}, 消息: {message}")
return {"success": True, "message": message, "action": action}
except Exception as e:
error_message = f"切换 {game_version} 补丁状态时出错: {str(e)}"
if debug_mode:
self.logger.debug(f"{error_message}")
import traceback
self.logger.debug(f"错误详情:\n{traceback.format_exc()}")
if not silent:
QMessageBox.critical(
None,
f"操作失败 - {self.app_name}",
f"\n{error_message}\n",
QMessageBox.StandardButton.Ok,
)
return {"success": False, "message": error_message, "action": "none"}
def batch_toggle_patches(self, game_dirs, operation=None):
"""批量切换多个游戏补丁的禁用/启用状态
Args:
game_dirs: 游戏版本到游戏目录的映射字典
operation: 指定操作,可以是"enable""disable"或NoneNone则自动切换当前状态
Returns:
tuple: (成功数量, 失败数量, 详细结果列表)
"""
success_count = 0
fail_count = 0
debug_mode = self._is_debug_mode()
results = []
if debug_mode:
self.logger.debug(f"开始批量切换补丁状态 - 操作: {operation}, 游戏数量: {len(game_dirs)}")
self.logger.debug(f"游戏列表: {list(game_dirs.keys())}")
for version, path in game_dirs.items():
try:
if debug_mode:
self.logger.debug(f"处理游戏 {version}, 目录: {path}")
# 在批量模式下使用静默操作
result = self.toggle_patch(path, version, operation=operation, silent=True)
if debug_mode:
self.logger.debug(f"游戏 {version} 操作结果: {result}")
if result["success"]:
success_count += 1
if debug_mode:
self.logger.debug(f"游戏 {version} 操作成功,操作类型: {result['action']}")
else:
fail_count += 1
if debug_mode:
self.logger.debug(f"游戏 {version} 操作失败,原因: {result['message']}")
results.append({
"version": version,
"success": result["success"],
"message": result["message"],
"action": result["action"]
})
except Exception as e:
if debug_mode:
self.logger.debug(f"切换 {version} 补丁状态时出错: {str(e)}")
import traceback
self.logger.debug(f"错误详情:\n{traceback.format_exc()}")
fail_count += 1
results.append({
"version": version,
"success": False,
"message": f"操作出错: {str(e)}",
"action": "none"
})
if debug_mode:
self.logger.debug(f"批量切换补丁状态完成 - 成功: {success_count}, 失败: {fail_count}")
return success_count, fail_count, results
def show_toggle_result(self, success_count, fail_count, results=None):
"""显示批量切换补丁状态的结果
Args:
success_count: 成功操作的数量
fail_count: 操作失败的数量
results: 详细结果列表,如果提供,会显示更详细的信息
"""
result_text = f"\n批量操作完成!\n成功: {success_count}\n失败: {fail_count}\n"
# 如果有详细结果,添加到消息中
if results:
enabled_list = [r["version"] for r in results if r["success"] and r["action"] == "enable"]
disabled_list = [r["version"] for r in results if r["success"] and r["action"] == "disable"]
skipped_list = [r["version"] for r in results if r["success"] and r["action"] == "none"]
fail_list = [r["version"] for r in results if not r["success"]]
if enabled_list:
result_text += f"\n【已启用补丁】:\n{chr(10).join(enabled_list)}\n"
if disabled_list:
result_text += f"\n【已禁用补丁】:\n{chr(10).join(disabled_list)}\n"
if skipped_list:
result_text += f"\n【无需操作】:\n{chr(10).join(skipped_list)}\n"
if fail_list:
result_text += f"\n【操作失败】:\n{chr(10).join(fail_list)}\n"
QMessageBox.information(
None,
f"批量操作完成 - {self.app_name}",
result_text,
QMessageBox.StandardButton.Ok,
)
def show_result(self):
"""显示安装结果,区分不同情况"""
# 获取当前安装状态
installed_versions = [] # 成功安装的版本
skipped_versions = [] # 已有补丁跳过的版本
failed_versions = [] # 安装失败的版本
not_found_versions = [] # 未找到的版本
# 获取所有游戏版本路径
install_paths = self.main_window.download_manager.get_install_paths() if hasattr(self.main_window.download_manager, "get_install_paths") else {}
# 检查是否处于离线模式
is_offline_mode = False
if hasattr(self.main_window, 'offline_mode_manager'):
is_offline_mode = self.main_window.offline_mode_manager.is_in_offline_mode()
# 获取本次实际安装的游戏列表
installed_games = []
# 在线模式下使用download_queue_history
if hasattr(self.main_window, 'download_queue_history') and self.main_window.download_queue_history:
installed_games = self.main_window.download_queue_history
# 离线模式下使用offline_mode_manager.installed_games
if is_offline_mode and hasattr(self.main_window.offline_mode_manager, 'installed_games'):
installed_games = self.main_window.offline_mode_manager.installed_games
debug_mode = self._is_debug_mode()
if debug_mode:
self.logger.debug(f"DEBUG: 显示安装结果,离线模式: {is_offline_mode}")
self.logger.debug(f"DEBUG: 本次安装的游戏: {installed_games}")
for game_version, is_installed in self.main_window.installed_status.items():
# 只处理install_paths中存在的游戏版本
if game_version in install_paths:
path = install_paths[game_version]
# 检查游戏是否存在但未通过本次安装补丁
if is_installed:
# 游戏已安装补丁
if game_version in installed_games:
# 本次成功安装
installed_versions.append(game_version)
else:
# 已有补丁,被跳过下载
skipped_versions.append(game_version)
else:
# 游戏未安装补丁
if os.path.exists(path):
# 游戏文件夹存在,但安装失败
failed_versions.append(game_version)
else:
# 游戏文件夹不存在
not_found_versions.append(game_version)
# 构建结果信息
result_text = f"\n安装结果:\n"
# 总数统计 - 只显示本次实际安装的数量
total_installed = len(installed_versions)
total_failed = len(failed_versions)
result_text += f"安装成功:{total_installed} 个 安装失败:{total_failed}\n\n"
# 详细列表
if installed_versions:
result_text += f"【成功安装】:\n{chr(10).join(installed_versions)}\n\n"
if failed_versions:
result_text += f"【安装失败】:\n{chr(10).join(failed_versions)}\n\n"
if not_found_versions:
# 只有在真正检测到了游戏但未安装补丁时才显示
result_text += f"【尚未安装补丁的游戏】:\n{chr(10).join(not_found_versions)}\n"
QMessageBox.information(
self.main_window,
f"安装完成 - {APP_NAME}",
result_text
)

View File

@@ -6,8 +6,8 @@ import json
from PySide6.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QTextBrowser, QPushButton, QCheckBox, QLabel, QMessageBox from PySide6.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QTextBrowser, QPushButton, QCheckBox, QLabel, QMessageBox
from PySide6.QtCore import Qt from PySide6.QtCore import Qt
from data.privacy_policy import PRIVACY_POLICY_BRIEF, get_local_privacy_policy, PRIVACY_POLICY_VERSION from config.privacy_policy import PRIVACY_POLICY_BRIEF, get_local_privacy_policy, PRIVACY_POLICY_VERSION
from data.config import CACHE, APP_NAME, APP_VERSION from config.config import CACHE, APP_NAME, APP_VERSION
from utils import msgbox_frame from utils import msgbox_frame
from utils.logger import setup_logger from utils.logger import setup_logger
@@ -18,20 +18,20 @@ class PrivacyManager:
"""初始化隐私协议管理器""" """初始化隐私协议管理器"""
# 初始化日志 # 初始化日志
self.logger = setup_logger("privacy_manager") self.logger = setup_logger("privacy_manager")
self.logger.info("正在初始化隐私协议管理器") self.logger.debug("正在初始化隐私协议管理器")
# 确保缓存目录存在 # 确保缓存目录存在
os.makedirs(CACHE, exist_ok=True) os.makedirs(CACHE, exist_ok=True)
self.config_file = os.path.join(CACHE, "privacy_config.json") self.config_file = os.path.join(CACHE, "privacy_config.json")
self.privacy_config = self._load_privacy_config() self.privacy_config = self._load_privacy_config()
# 获取隐私协议内容和版本 # 获取隐私协议内容和版本
self.logger.info("读取本地隐私协议文件") self.logger.debug("读取本地隐私协议文件")
self.privacy_content, self.current_privacy_version, error = get_local_privacy_policy() self.privacy_content, self.current_privacy_version, error = get_local_privacy_policy()
if error: if error:
self.logger.warning(f"读取本地隐私协议文件警告: {error}") self.logger.warning(f"读取本地隐私协议文件警告: {error}")
# 使用默认版本作为备用 # 使用默认版本作为备用
self.current_privacy_version = PRIVACY_POLICY_VERSION self.current_privacy_version = PRIVACY_POLICY_VERSION
self.logger.info(f"隐私协议版本: {self.current_privacy_version}") self.logger.debug(f"隐私协议版本: {self.current_privacy_version}")
# 检查隐私协议版本和用户同意状态 # 检查隐私协议版本和用户同意状态
self.privacy_accepted = self._check_privacy_acceptance() self.privacy_accepted = self._check_privacy_acceptance()
@@ -66,9 +66,9 @@ class PrivacyManager:
stored_app_version = self.privacy_config.get("app_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) privacy_accepted = self.privacy_config.get("privacy_accepted", False)
self.logger.info(f"存储的隐私协议版本: {stored_privacy_version}, 当前版本: {self.current_privacy_version}") self.logger.debug(f"存储的隐私协议版本: {stored_privacy_version}, 当前版本: {self.current_privacy_version}")
self.logger.info(f"存储的应用版本: {stored_app_version}, 当前版本: {APP_VERSION}") self.logger.debug(f"存储的应用版本: {stored_app_version}, 当前版本: {APP_VERSION}")
self.logger.info(f"隐私协议接受状态: {privacy_accepted}") self.logger.debug(f"隐私协议接受状态: {privacy_accepted}")
# 如果隐私协议版本变更,需要重新同意 # 如果隐私协议版本变更,需要重新同意
if stored_privacy_version != self.current_privacy_version: if stored_privacy_version != self.current_privacy_version:
@@ -125,7 +125,7 @@ class PrivacyManager:
""" """
# 如果用户已经同意了隐私协议直接返回True不显示对话框 # 如果用户已经同意了隐私协议直接返回True不显示对话框
if self.privacy_accepted: if self.privacy_accepted:
self.logger.info("用户已同意当前版本的隐私协议,无需再次显示") self.logger.debug("用户已同意当前版本的隐私协议,无需再次显示")
return True return True
self.logger.info("首次运行或隐私协议版本变更,显示隐私对话框") self.logger.info("首次运行或隐私协议版本变更,显示隐私对话框")

View File

@@ -0,0 +1,417 @@
from PySide6.QtGui import QIcon
from PySide6.QtWidgets import QMessageBox
import os
import logging
import subprocess
from utils import load_base64_image, resource_path
from config.config import APP_NAME, APP_VERSION
from ui.components import FontStyleManager, DialogFactory, ExternalLinksHandler, MenuBuilder
logger = logging.getLogger(__name__)
class UIManager:
def __init__(self, main_window):
"""初始化UI管理器
Args:
main_window: 主窗口实例用于设置UI元素
"""
self.main_window = main_window
# 使用getattr获取ui属性如果不存在则为None
self.ui = getattr(main_window, 'ui', None)
# 获取主窗口的IPv6Manager实例
self.ipv6_manager = getattr(main_window, 'ipv6_manager', None)
# 初始化UI组件
self.font_style_manager = FontStyleManager()
self.dialog_factory = DialogFactory(main_window)
self.external_links_handler = ExternalLinksHandler(main_window, self.dialog_factory)
self.menu_builder = MenuBuilder(main_window, self.font_style_manager, self.external_links_handler, self.dialog_factory)
# 保留一些快捷访问属性以保持兼容性
self.debug_action = None
self.disable_auto_restore_action = None
self.disable_pre_hash_action = None
def setup_ui(self):
"""设置UI元素包括窗口图标、标题和菜单"""
# 设置窗口图标
icon_path = resource_path(os.path.join("assets", "images", "ICO", "icon.png"))
if os.path.exists(icon_path):
self.main_window.setWindowIcon(QIcon(icon_path))
# 获取当前离线模式状态
is_offline_mode = False
if hasattr(self.main_window, 'offline_mode_manager'):
is_offline_mode = self.main_window.offline_mode_manager.is_in_offline_mode()
# 设置窗口标题和UI标题标签
mode_indicator = "[离线模式]" if is_offline_mode else "[在线模式]"
self.main_window.setWindowTitle(f"{APP_NAME} v{APP_VERSION} {mode_indicator}")
# 更新UI中的标题标签
if hasattr(self.main_window, 'ui') and hasattr(self.main_window.ui, 'title_label'):
self.main_window.ui.title_label.setText(f"{APP_NAME} v{APP_VERSION} {mode_indicator}")
# 使用新的菜单构建器设置所有菜单
self.menu_builder.setup_all_menus()
# 保持对一些重要UI元素的引用以确保兼容性
self.debug_action = self.menu_builder.debug_action
self.disable_auto_restore_action = self.menu_builder.disable_auto_restore_action
self.disable_pre_hash_action = self.menu_builder.disable_pre_hash_action
# 保存对工作模式菜单项的引用,确保能正确同步状态
self.online_mode_action = self.menu_builder.online_mode_action
self.offline_mode_action = self.menu_builder.offline_mode_action
# 在菜单创建完成后,强制同步一次工作模式状态
self.sync_work_mode_menu_state()
# 为了向后兼容性,添加委托方法
def create_progress_window(self, title, initial_text="准备中..."):
"""创建进度窗口委托给dialog_factory"""
return self.dialog_factory.create_progress_window(title, initial_text)
def show_loading_dialog(self, message):
"""显示加载对话框委托给dialog_factory"""
return self.dialog_factory.show_loading_dialog(message)
def hide_loading_dialog(self):
"""隐藏加载对话框委托给dialog_factory"""
return self.dialog_factory.hide_loading_dialog()
def _create_message_box(self, title, message, buttons=QMessageBox.StandardButton.Ok):
"""创建消息框委托给dialog_factory"""
return self.dialog_factory.create_message_box(title, message, buttons)
def show_menu(self, menu, button):
"""显示菜单委托给menu_builder"""
return self.menu_builder.show_menu(menu, button)
def sync_work_mode_menu_state(self):
"""同步工作模式菜单状态,确保菜单选择状态与实际工作模式一致"""
try:
# 检查是否有离线模式管理器和菜单项
if not hasattr(self.main_window, 'offline_mode_manager') or not self.main_window.offline_mode_manager:
return
if not hasattr(self, 'online_mode_action') or not hasattr(self, 'offline_mode_action'):
return
if not self.online_mode_action or not self.offline_mode_action:
return
# 获取当前离线模式状态
is_offline_mode = self.main_window.offline_mode_manager.is_in_offline_mode()
# 同步菜单选择状态
self.online_mode_action.setChecked(not is_offline_mode)
self.offline_mode_action.setChecked(is_offline_mode)
# 记录同步操作(仅在调试模式下)
if hasattr(self.main_window, 'config') and self.main_window.config.get('debug_mode', False):
from utils.logger import setup_logger
logger = setup_logger("ui_manager")
logger.debug(f"已同步工作模式菜单状态: 离线模式={is_offline_mode}")
except Exception as e:
# 静默处理异常,避免影响程序正常运行
if hasattr(self.main_window, 'config') and self.main_window.config.get('debug_mode', False):
from utils.logger import setup_logger
logger = setup_logger("ui_manager")
logger.debug(f"同步工作模式菜单状态时出错: {e}")
def _handle_ipv6_toggle(self, enabled):
"""处理IPv6支持切换事件
Args:
enabled: 是否启用IPv6支持
"""
if not self.ipv6_manager:
# 显示错误提示
msg_box = self._create_message_box("错误", "\nIPv6管理器尚未初始化请稍后再试。\n")
msg_box.exec()
# 恢复复选框状态
self.ipv6_action.setChecked(not enabled)
return
if enabled:
# 先显示警告提示
warning_msg_box = self._create_message_box(
"警告",
"\n目前IPv6支持功能仍在测试阶段可能会发生意料之外的bug\n\n您确定需要启用吗?\n",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
response = warning_msg_box.exec()
# 如果用户选择不启用,直接返回
if response != QMessageBox.StandardButton.Yes:
# 恢复复选框状态
self.ipv6_action.setChecked(False)
return
# 显示正在校验IPv6的提示
msg_box = self._create_message_box("IPv6检测", "\n正在校验是否支持IPv6请稍候...\n")
msg_box.open() # 使用open而不是exec这样不会阻塞UI
# 处理消息队列,确保对话框显示
from PySide6.QtCore import QCoreApplication
QCoreApplication.processEvents()
# 检查IPv6是否可用
ipv6_available = self.ipv6_manager.check_ipv6_availability()
# 关闭提示对话框
msg_box.accept()
if not ipv6_available:
# 显示IPv6不可用的提示
error_msg_box = self._create_message_box(
"IPv6不可用",
"\n未检测到可用的IPv6连接无法启用IPv6支持。\n\n请确保您的网络环境支持IPv6且已正确配置。\n"
)
error_msg_box.exec()
# 恢复复选框状态
self.ipv6_action.setChecked(False)
return False
# 使用IPv6Manager处理切换
success = self.ipv6_manager.toggle_ipv6_support(enabled)
# 如果切换失败,恢复复选框状态
if not success:
self.ipv6_action.setChecked(not enabled)
def show_download_thread_settings(self):
"""显示下载线程设置对话框"""
if hasattr(self.main_window, 'download_manager'):
self.main_window.download_manager.show_download_thread_settings()
else:
# 如果下载管理器不可用,显示错误信息
self.dialog_factory.show_simple_message("错误", "\n下载管理器未初始化,无法修改下载线程设置。\n", "error")
def restore_hosts_backup(self):
"""还原软件备份的hosts文件"""
if hasattr(self.main_window, 'download_manager') and hasattr(self.main_window.download_manager, 'hosts_manager'):
try:
# 调用恢复hosts文件的方法
result = self.main_window.download_manager.hosts_manager.restore()
if result:
msg_box = self._create_message_box("成功", "\nhosts文件已成功还原为备份版本。\n")
else:
msg_box = self._create_message_box("警告", "\n还原hosts文件失败或没有找到备份文件。\n")
msg_box.exec()
except Exception as e:
msg_box = self._create_message_box("错误", f"\n还原hosts文件时发生错误\n\n{str(e)}\n")
msg_box.exec()
else:
msg_box = self._create_message_box("错误", "\n无法访问hosts管理器。\n")
msg_box.exec()
def clean_hosts_entries(self):
"""手动删除软件添加的hosts条目"""
if hasattr(self.main_window, 'download_manager') and hasattr(self.main_window.download_manager, 'hosts_manager'):
try:
# 调用清理hosts条目的方法强制清理即使禁用了自动还原
result = self.main_window.download_manager.hosts_manager.check_and_clean_all_entries(force_clean=True)
if result:
msg_box = self._create_message_box("成功", "\n已成功清理软件添加的hosts条目。\n")
else:
msg_box = self._create_message_box("提示", "\n未发现软件添加的hosts条目或清理操作失败。\n")
msg_box.exec()
except Exception as e:
msg_box = self._create_message_box("错误", f"\n清理hosts条目时发生错误\n\n{str(e)}\n")
msg_box.exec()
else:
msg_box = self._create_message_box("错误", "\n无法访问hosts管理器。\n")
msg_box.exec()
def open_hosts_file(self):
"""打开系统hosts文件"""
try:
# 获取hosts文件路径
hosts_path = os.path.join(os.environ['SystemRoot'], 'System32', 'drivers', 'etc', 'hosts')
# 检查文件是否存在
if os.path.exists(hosts_path):
# 使用操作系统默认程序打开hosts文件
if os.name == 'nt': # Windows
# 尝试以管理员权限打开记事本编辑hosts文件
try:
# 使用PowerShell以管理员身份启动记事本
subprocess.Popen(["powershell", "Start-Process", "notepad", hosts_path, "-Verb", "RunAs"])
except Exception as e:
# 如果失败,尝试直接打开
os.startfile(hosts_path)
else: # macOS 和 Linux
import subprocess
subprocess.call(['xdg-open', hosts_path])
else:
msg_box = self._create_message_box("错误", f"\nhosts文件不存在\n{hosts_path}\n")
msg_box.exec()
except Exception as e:
msg_box = self._create_message_box("错误", f"\n打开hosts文件时发生错误\n\n{str(e)}\n")
msg_box.exec()
def toggle_disable_auto_restore_hosts(self, checked):
"""切换禁用自动还原hosts的状态
Args:
checked: 是否禁用自动还原
"""
if hasattr(self.main_window, 'download_manager') and hasattr(self.main_window.download_manager, 'hosts_manager'):
try:
# 调用HostsManager的方法设置自动还原标志
result = self.main_window.download_manager.hosts_manager.set_auto_restore_disabled(checked)
if result:
# 同时更新内部配置,确保立即生效
if hasattr(self.main_window, 'config'):
self.main_window.config['disable_auto_restore_hosts'] = checked
# 显示成功提示
status = "禁用" if checked else "启用"
msg_box = self._create_message_box(
"设置已更新",
f"\n{status}关闭/重启时自动还原hosts。\n\n{'hosts将被保留' if checked else 'hosts将在关闭时自动还原'}\n"
)
msg_box.exec()
else:
# 如果设置失败,恢复复选框状态
self.disable_auto_restore_action.setChecked(not checked)
msg_box = self._create_message_box(
"设置失败",
"\n更新设置时发生错误,请稍后再试。\n"
)
msg_box.exec()
except Exception as e:
# 如果发生异常,恢复复选框状态
self.disable_auto_restore_action.setChecked(not checked)
msg_box = self._create_message_box(
"错误",
f"\n更新设置时发生异常:\n\n{str(e)}\n"
)
msg_box.exec()
else:
# 如果hosts管理器不可用恢复复选框状态
self.disable_auto_restore_action.setChecked(not checked)
msg_box = self._create_message_box(
"错误",
"\nhosts管理器不可用无法更新设置。\n"
)
msg_box.exec()
def _handle_pre_hash_toggle(self, checked):
"""处理禁用安装前哈希预检查的切换
Args:
checked: 是否禁用安装前哈希预检查
"""
if hasattr(self.main_window, 'config_manager'):
success = self.main_window.config_manager.toggle_disable_pre_hash_check(self.main_window, checked)
if not success:
# 如果操作失败,恢复复选框状态
self.disable_pre_hash_action.setChecked(not checked)
else:
# 如果配置管理器不可用,恢复复选框状态并显示错误
self.disable_pre_hash_action.setChecked(not checked)
self._create_message_box("错误", "\n配置管理器未初始化。\n").exec()
def switch_work_mode(self, mode):
"""切换工作模式
Args:
mode: 要切换的模式,"online""offline"
"""
# 检查主窗口是否有离线模式管理器
if not hasattr(self.main_window, 'offline_mode_manager'):
# 如果没有离线模式管理器,创建提示
msg_box = self._create_message_box(
"错误",
"\n离线模式管理器未初始化,无法切换工作模式。\n"
)
msg_box.exec()
# 恢复选择状态
self.online_mode_action.setChecked(True)
self.offline_mode_action.setChecked(False)
return
if mode == "offline":
# 尝试切换到离线模式
success = self.main_window.offline_mode_manager.set_offline_mode(True)
if not success:
# 如果切换失败,恢复选择状态
self.online_mode_action.setChecked(True)
self.offline_mode_action.setChecked(False)
return
# 更新配置
self.main_window.config["offline_mode"] = True
self.main_window.save_config(self.main_window.config)
# 在离线模式下启用开始安装按钮
if hasattr(self.main_window, 'window_manager'):
self.main_window.window_manager.change_window_state(self.main_window.window_manager.STATE_READY)
# 清除版本警告标志
if hasattr(self.main_window, 'version_warning'):
self.main_window.version_warning = False
# 显示提示
msg_box = self._create_message_box(
"模式已切换",
"\n已切换到离线模式。\n\n将使用本地补丁文件进行安装,不会从网络下载补丁。\n"
)
msg_box.exec()
else:
# 切换到在线模式
self.main_window.offline_mode_manager.set_offline_mode(False)
# 更新配置
self.main_window.config["offline_mode"] = False
self.main_window.save_config(self.main_window.config)
# 重新获取云端配置
if hasattr(self.main_window, 'fetch_cloud_config'):
self.main_window.fetch_cloud_config()
# 如果当前版本过低,设置版本警告标志
if hasattr(self.main_window, 'last_error_message') and self.main_window.last_error_message == "update_required":
# 设置版本警告标志
if hasattr(self.main_window, 'version_warning'):
self.main_window.version_warning = True
# 显示提示
msg_box = self._create_message_box(
"模式已切换",
"\n已切换到在线模式。\n\n将从网络下载补丁进行安装。\n"
)
msg_box.exec()

View File

@@ -24,6 +24,57 @@ class WindowManager:
# 设置圆角窗口 # 设置圆角窗口
self.setRoundedCorners() self.setRoundedCorners()
# 初始化状态管理
self._setup_window_state()
def _setup_window_state(self):
"""初始化窗口状态管理."""
self.STATE_INITIALIZING = "initializing"
self.STATE_READY = "ready"
self.STATE_DOWNLOADING = "downloading"
self.STATE_EXTRACTING = "extracting"
self.STATE_VERIFYING = "verifying"
self.STATE_INSTALLING = "installing"
self.STATE_COMPLETED = "completed"
self.STATE_ERROR = "error"
self.current_state = self.STATE_INITIALIZING
def change_window_state(self, new_state, error_message=None):
"""更改窗口状态并更新UI.
Args:
new_state (str): 新的状态.
error_message (str, optional): 错误信息. Defaults to None.
"""
if new_state == self.current_state:
return
self.current_state = new_state
self._update_ui_for_state(new_state, error_message)
def _update_ui_for_state(self, state, error_message=None):
"""根据当前状态更新UI组件."""
is_offline = self.window.offline_mode_manager.is_in_offline_mode()
config_valid = self.window.config_valid
button_enabled = False
button_text = "!无法安装!"
if state == self.STATE_READY:
if is_offline or config_valid:
button_enabled = True
button_text = "开始安装"
elif state in [self.STATE_DOWNLOADING, self.STATE_EXTRACTING, self.STATE_VERIFYING, self.STATE_INSTALLING]:
button_text = "正在安装"
elif state == self.STATE_COMPLETED:
button_enabled = True
button_text = "安装完成" # Or back to "开始安装"
self.window.ui.start_install_btn.setEnabled(button_enabled)
self.window.ui.start_install_text.setText(button_text)
self.window.install_button_enabled = button_enabled
def setRoundedCorners(self): def setRoundedCorners(self):
"""设置窗口圆角""" """设置窗口圆角"""
# 实现圆角窗口 # 实现圆角窗口
@@ -115,22 +166,30 @@ class WindowManager:
btn_width = 211 # 扩大后的容器宽度 btn_width = 211 # 扩大后的容器宽度
btn_height = 111 # 扩大后的容器高度 btn_height = 111 # 扩大后的容器高度
x_pos = new_width - btn_width - right_margin x_pos = new_width - btn_width - right_margin
y_pos = int((new_height - 65) * 0.28) - 10 # 调整为更靠上的位置 y_pos = int((new_height - 65) * 0.18) - 10 # 从0.28改为0.18,向上移动
self.ui.button_container.setGeometry(x_pos, y_pos, btn_width, btn_height) self.ui.button_container.setGeometry(x_pos, y_pos, btn_width, btn_height)
# 添加禁/启用补丁按钮容器的位置调整
if hasattr(self.ui, 'toggle_patch_container'):
btn_width = 211 # 扩大后的容器宽度
btn_height = 111 # 扩大后的容器高度
x_pos = new_width - btn_width - right_margin
y_pos = int((new_height - 65) * 0.36) - 10 # 从0.46改为0.36,向上移动
self.ui.toggle_patch_container.setGeometry(x_pos, y_pos, btn_width, btn_height)
# 添加卸载补丁按钮容器的位置调整 # 添加卸载补丁按钮容器的位置调整
if hasattr(self.ui, 'uninstall_container'): if hasattr(self.ui, 'uninstall_container'):
btn_width = 211 # 扩大后的容器宽度 btn_width = 211 # 扩大后的容器宽度
btn_height = 111 # 扩大后的容器高度 btn_height = 111 # 扩大后的容器高度
x_pos = new_width - btn_width - right_margin x_pos = new_width - btn_width - right_margin
y_pos = int((new_height - 65) * 0.46) - 10 # 调整为中间位置 y_pos = int((new_height - 65) * 0.54) - 10 # 从0.64改为0.54,向上移动
self.ui.uninstall_container.setGeometry(x_pos, y_pos, btn_width, btn_height) self.ui.uninstall_container.setGeometry(x_pos, y_pos, btn_width, btn_height)
if hasattr(self.ui, 'exit_container'): if hasattr(self.ui, 'exit_container'):
btn_width = 211 # 扩大后的容器宽度 btn_width = 211 # 扩大后的容器宽度
btn_height = 111 # 扩大后的容器高度 btn_height = 111 # 扩大后的容器高度
x_pos = new_width - btn_width - right_margin x_pos = new_width - btn_width - right_margin
y_pos = int((new_height - 65) * 0.64) - 10 # 调整为更靠下的位置 y_pos = int((new_height - 65) * 0.72) - 10 # 从0.82改为0.72,向上移动
self.ui.exit_container.setGeometry(x_pos, y_pos, btn_width, btn_height) self.ui.exit_container.setGeometry(x_pos, y_pos, btn_width, btn_height)
# 更新圆角 # 更新圆角

View File

@@ -1,391 +0,0 @@
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, silent=False):
"""卸载补丁
Args:
game_dir: 游戏目录路径
game_version: 游戏版本
silent: 是否静默模式(不显示弹窗)
Returns:
bool: 卸载成功返回True失败返回False
dict: 在silent=True时返回包含卸载结果信息的字典
"""
debug_mode = self._is_debug_mode()
if game_version not in self.game_info:
if not silent:
QMessageBox.critical(
None,
f"错误 - {self.app_name}",
f"\n无法识别游戏版本: {game_version}\n",
QMessageBox.StandardButton.Ok,
)
return False if not silent else {"success": False, "message": f"无法识别游戏版本: {game_version}", "files_removed": 0}
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 not silent and 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,
)
# 卸载成功
if silent:
return {"success": True, "message": f"{game_version} 补丁卸载成功", "files_removed": files_removed}
return True
except Exception as e:
# 在非静默模式且非批量卸载模式下显示卸载失败消息
if not silent and 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,
)
# 卸载失败
if silent:
return {"success": False, "message": f"卸载 {game_version} 补丁时出错: {str(e)}", "files_removed": 0}
return False
def batch_uninstall_patches(self, game_dirs):
"""批量卸载多个游戏的补丁
Args:
game_dirs: 游戏版本到游戏目录的映射字典
Returns:
tuple: (成功数量, 失败数量, 详细结果列表)
"""
success_count = 0
fail_count = 0
debug_mode = self._is_debug_mode()
results = []
for version, path in game_dirs.items():
try:
# 在批量模式下使用静默卸载
result = self.uninstall_patch(path, version, silent=True)
if isinstance(result, dict): # 使用了静默模式
if result["success"]:
success_count += 1
else:
fail_count += 1
results.append({
"version": version,
"success": result["success"],
"message": result["message"],
"files_removed": result["files_removed"]
})
else: # 兼容旧代码,不应该执行到这里
if result:
success_count += 1
else:
fail_count += 1
results.append({
"version": version,
"success": result,
"message": f"{version} 卸载{'成功' if result else '失败'}",
"files_removed": 0
})
except Exception as e:
if debug_mode:
print(f"DEBUG: 卸载 {version} 时出错: {str(e)}")
fail_count += 1
results.append({
"version": version,
"success": False,
"message": f"卸载出错: {str(e)}",
"files_removed": 0
})
return success_count, fail_count, results
def show_uninstall_result(self, success_count, fail_count, results=None):
"""显示批量卸载结果
Args:
success_count: 成功卸载的数量
fail_count: 卸载失败的数量
results: 详细结果列表,如果提供,会显示更详细的信息
"""
result_text = f"\n批量卸载完成!\n成功: {success_count}\n失败: {fail_count}\n"
# 如果有详细结果,添加到消息中
if results:
success_list = [r["version"] for r in results if r["success"]]
fail_list = [r["version"] for r in results if not r["success"]]
if success_list:
result_text += f"\n【成功卸载】:\n{chr(10).join(success_list)}\n"
if fail_list:
result_text += f"\n【卸载失败】:\n{chr(10).join(fail_list)}\n"
QMessageBox.information(
None,
f"批量卸载完成 - {self.app_name}",
result_text,
QMessageBox.StandardButton.Ok,
)
def check_patch_installed(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:
return False
# 获取可能的补丁文件路径
install_path_base = os.path.basename(self.game_info[game_version]["install_path"])
patch_file_path = os.path.join(game_dir, install_path_base)
# 尝试查找补丁文件,支持不同大小写
patch_files_to_check = [
patch_file_path,
patch_file_path.lower(),
patch_file_path.upper(),
patch_file_path.replace("_", ""),
patch_file_path.replace("_", "-"),
]
# 查找补丁文件
for patch_path in patch_files_to_check:
if os.path.exists(patch_path):
if debug_mode:
print(f"DEBUG: 找到补丁文件: {patch_path}")
return True
# 检查是否有补丁文件夹
patch_folders_to_check = [
os.path.join(game_dir, "patch"),
os.path.join(game_dir, "Patch"),
os.path.join(game_dir, "PATCH"),
]
for patch_folder in patch_folders_to_check:
if os.path.exists(patch_folder):
if debug_mode:
print(f"DEBUG: 找到补丁文件夹: {patch_folder}")
return True
# 检查game/patch文件夹
game_folders = ["game", "Game", "GAME"]
patch_folders = ["patch", "Patch", "PATCH"]
for game_folder in game_folders:
for patch_folder in patch_folders:
game_patch_folder = os.path.join(game_dir, game_folder, patch_folder)
if os.path.exists(game_patch_folder):
if debug_mode:
print(f"DEBUG: 找到game/patch文件夹: {game_patch_folder}")
return True
# 检查配置文件
config_files = ["config.json", "Config.json", "CONFIG.JSON"]
script_files = ["scripts.json", "Scripts.json", "SCRIPTS.JSON"]
for game_folder in game_folders:
game_path = os.path.join(game_dir, game_folder)
if os.path.exists(game_path):
# 检查配置文件
for config_file in config_files:
config_path = os.path.join(game_path, config_file)
if os.path.exists(config_path):
if debug_mode:
print(f"DEBUG: 找到配置文件: {config_path}")
return True
# 检查脚本文件
for script_file in script_files:
script_path = os.path.join(game_path, script_file)
if os.path.exists(script_path):
if debug_mode:
print(f"DEBUG: 找到脚本文件: {script_path}")
return True
# 没有找到补丁文件或文件夹
if debug_mode:
print(f"DEBUG: {game_version}{game_dir} 中没有安装补丁")
return False

View File

@@ -1,673 +0,0 @@
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, resource_path
from data.config import APP_NAME, APP_VERSION, LOG_FILE
from core.ipv6_manager import IPv6Manager # 导入新的IPv6Manager类
class UIManager:
def __init__(self, main_window):
"""初始化UI管理器
Args:
main_window: 主窗口实例用于设置UI元素
"""
self.main_window = main_window
# 使用getattr获取ui属性如果不存在则为None
self.ui = getattr(main_window, 'ui', None)
self.debug_action = None
self.turbo_download_action = None
self.dev_menu = None
self.privacy_menu = None # 隐私协议菜单
self.about_menu = None # 关于菜单
self.about_btn = None # 关于按钮
# 获取主窗口的IPv6Manager实例
self.ipv6_manager = getattr(main_window, 'ipv6_manager', None)
def setup_ui(self):
"""设置UI元素包括窗口图标、标题和菜单"""
# 设置窗口图标
import os
from utils import resource_path
icon_path = resource_path(os.path.join("IMG", "ICO", "icon.png"))
if os.path.exists(icon_path):
self.main_window.setWindowIcon(QIcon(icon_path))
# 设置窗口标题
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
# 获取菜单字体
menu_font = self._get_menu_font()
# 创建菜单项 - 移除"项目主页",添加"常见问题"和"提交错误"
faq_action = QAction("常见问题", self.main_window)
faq_action.triggered.connect(self.open_faq_page)
faq_action.setFont(menu_font)
report_issue_action = QAction("提交错误", self.main_window)
report_issue_action.triggered.connect(self.open_issues_page)
report_issue_action.setFont(menu_font)
# 清除现有菜单项并添加新的菜单项
self.ui.menu_2.clear()
self.ui.menu_2.addAction(faq_action)
self.ui.menu_2.addAction(report_issue_action)
# 连接按钮点击事件,如果使用按钮式菜单
if hasattr(self.ui, 'help_btn'):
# 按钮已经连接到显示菜单,不需要额外处理
pass
def _setup_about_menu(self):
"""设置"关于"菜单"""
# 获取菜单字体
menu_font = self._get_menu_font()
# 创建关于菜单
self.about_menu = QMenu("关于", self.main_window)
self.about_menu.setFont(menu_font)
# 设置菜单样式
font_family = menu_font.family()
menu_style = self._get_menu_style(font_family)
self.about_menu.setStyleSheet(menu_style)
# 创建菜单项
about_project_action = QAction("关于本项目", self.main_window)
about_project_action.setFont(menu_font)
about_project_action.triggered.connect(self.show_about_dialog)
# 添加项目主页选项(从帮助菜单移动过来)
project_home_action = QAction("Github项目主页", self.main_window)
project_home_action.setFont(menu_font)
project_home_action.triggered.connect(self.open_project_home_page)
# 添加加入QQ群选项
qq_group_action = QAction("加入QQ群", self.main_window)
qq_group_action.setFont(menu_font)
qq_group_action.triggered.connect(self.open_qq_group)
# 创建隐私协议菜单
self._setup_privacy_menu()
# 添加到关于菜单
self.about_menu.addAction(about_project_action)
self.about_menu.addAction(project_home_action)
self.about_menu.addAction(qq_group_action)
self.about_menu.addSeparator()
self.about_menu.addMenu(self.privacy_menu)
# 连接按钮点击事件
if self.about_btn:
self.about_btn.clicked.connect(lambda: self.show_menu(self.about_menu, self.about_btn))
def _setup_privacy_menu(self):
"""设置"隐私协议"菜单"""
# 获取菜单字体
menu_font = self._get_menu_font()
# 创建隐私协议子菜单
self.privacy_menu = QMenu("隐私协议", self.main_window)
self.privacy_menu.setFont(menu_font)
# 设置与其他菜单一致的样式
font_family = menu_font.family()
menu_style = self._get_menu_style(font_family)
self.privacy_menu.setStyleSheet(menu_style)
# 添加子选项
view_privacy_action = QAction("查看完整隐私协议", self.main_window)
view_privacy_action.setFont(menu_font)
view_privacy_action.triggered.connect(self.open_privacy_policy)
revoke_privacy_action = QAction("撤回隐私协议", self.main_window)
revoke_privacy_action.setFont(menu_font)
revoke_privacy_action.triggered.connect(self.revoke_privacy_agreement)
# 添加到子菜单
self.privacy_menu.addAction(view_privacy_action)
self.privacy_menu.addAction(revoke_privacy_action)
def _get_menu_style(self, font_family):
"""获取统一的菜单样式"""
return f"""
QMenu {{
background-color: #E96948;
color: white;
font-family: "{font_family}";
font-size: 14px;
font-weight: bold;
border: 1px solid #F47A5B;
padding: 8px;
border-radius: 6px;
margin-top: 2px;
}}
QMenu::item {{
padding: 6px 20px 6px 15px;
background-color: transparent;
min-width: 120px;
color: white;
font-family: "{font_family}";
font-size: 14px;
font-weight: bold;
}}
QMenu::item:selected {{
background-color: #F47A5B;
border-radius: 4px;
}}
QMenu::separator {{
height: 1px;
background-color: #F47A5B;
margin: 5px 15px;
}}
QMenu::item:checked {{
background-color: #D25A3C;
border-radius: 4px;
}}
"""
def _get_menu_font(self):
"""获取菜单字体"""
font_family = "Arial" # 默认字体族
try:
from PySide6.QtGui import QFontDatabase
# 尝试加载字体
font_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "fonts", "SmileySans-Oblique.ttf")
if os.path.exists(font_path):
font_id = QFontDatabase.addApplicationFont(font_path)
if font_id != -1:
font_family = QFontDatabase.applicationFontFamilies(font_id)[0]
# 创建菜单字体
menu_font = QFont(font_family, 14)
menu_font.setBold(True)
return menu_font
except Exception as e:
print(f"加载字体失败: {e}")
# 返回默认字体
menu_font = QFont(font_family, 14)
menu_font.setBold(True)
return menu_font
def _setup_settings_menu(self):
"""设置"设置"菜单"""
if not self.ui or not hasattr(self.ui, 'menu'):
return
# 获取菜单字体
menu_font = self._get_menu_font()
font_family = menu_font.family()
# 创建开发者选项子菜单
self.dev_menu = QMenu("开发者选项", self.main_window)
self.dev_menu.setFont(menu_font) # 设置与UI_install.py中相同的字体
# 使用和主菜单相同的样式
menu_style = self._get_menu_style(font_family)
self.dev_menu.setStyleSheet(menu_style)
# 创建Debug子菜单
self.debug_submenu = QMenu("Debug模式", self.main_window)
self.debug_submenu.setFont(menu_font)
self.debug_submenu.setStyleSheet(menu_style)
# 创建hosts文件选项子菜单
self.hosts_submenu = QMenu("hosts文件选项", self.main_window)
self.hosts_submenu.setFont(menu_font)
self.hosts_submenu.setStyleSheet(menu_style)
# 添加IPv6支持选项
self.ipv6_action = QAction("启用IPv6支持", self.main_window, checkable=True)
self.ipv6_action.setFont(menu_font)
# 添加IPv6检测按钮用于显示详细信息
self.ipv6_test_action = QAction("测试IPv6连接", self.main_window)
self.ipv6_test_action.setFont(menu_font)
if self.ipv6_manager:
self.ipv6_test_action.triggered.connect(self.ipv6_manager.show_ipv6_details)
else:
self.ipv6_test_action.triggered.connect(self.show_ipv6_manager_not_ready)
# 创建IPv6支持子菜单
self.ipv6_submenu = QMenu("IPv6支持", self.main_window)
self.ipv6_submenu.setFont(menu_font)
self.ipv6_submenu.setStyleSheet(menu_style)
# 检查IPv6是否可用
ipv6_available = False
if self.ipv6_manager:
ipv6_available = self.ipv6_manager.check_ipv6_availability()
if not ipv6_available:
self.ipv6_action.setText("启用IPv6支持 (不可用)")
self.ipv6_action.setEnabled(False)
self.ipv6_action.setToolTip("未检测到可用的IPv6连接")
# 检查配置中是否已启用IPv6
config = getattr(self.main_window, 'config', {})
ipv6_enabled = False
if isinstance(config, dict):
ipv6_enabled = config.get("ipv6_enabled", False)
# 如果配置中启用了IPv6但实际不可用则强制禁用
if ipv6_enabled and not ipv6_available:
config["ipv6_enabled"] = False
ipv6_enabled = False
# 使用utils.save_config直接保存配置
from utils import save_config
save_config(config)
self.ipv6_action.setChecked(ipv6_enabled)
# 连接IPv6支持切换事件
self.ipv6_action.triggered.connect(self._handle_ipv6_toggle)
# 将选项添加到IPv6子菜单
self.ipv6_submenu.addAction(self.ipv6_action)
self.ipv6_submenu.addAction(self.ipv6_test_action)
# 添加hosts子选项
self.restore_hosts_action = QAction("还原软件备份的hosts文件", self.main_window)
self.restore_hosts_action.setFont(menu_font)
self.restore_hosts_action.triggered.connect(self.restore_hosts_backup)
self.clean_hosts_action = QAction("手动删除软件添加的hosts条目", self.main_window)
self.clean_hosts_action.setFont(menu_font)
self.clean_hosts_action.triggered.connect(self.clean_hosts_entries)
# 添加打开hosts文件选项
self.open_hosts_action = QAction("打开hosts文件", self.main_window)
self.open_hosts_action.setFont(menu_font)
self.open_hosts_action.triggered.connect(self.open_hosts_file)
# 添加到hosts子菜单
self.hosts_submenu.addAction(self.restore_hosts_action)
self.hosts_submenu.addAction(self.clean_hosts_action)
self.hosts_submenu.addAction(self.open_hosts_action)
# 创建Debug开关选项
self.debug_action = QAction("Debug开关", self.main_window, checkable=True)
self.debug_action.setFont(menu_font)
# 安全地获取config属性
config = getattr(self.main_window, 'config', {})
debug_mode = False
if isinstance(config, dict):
debug_mode = config.get("debug_mode", False)
self.debug_action.setChecked(debug_mode)
# 安全地连接toggle_debug_mode方法
if hasattr(self.main_window, 'toggle_debug_mode'):
self.debug_action.triggered.connect(self.main_window.toggle_debug_mode)
# 创建打开log文件选项
self.open_log_action = QAction("打开log.txt", self.main_window)
self.open_log_action.setFont(menu_font)
# 初始状态根据debug模式设置启用状态
self.open_log_action.setEnabled(debug_mode)
# 连接打开log文件的事件
self.open_log_action.triggered.connect(self.open_log_file)
# 添加到Debug子菜单
self.debug_submenu.addAction(self.debug_action)
self.debug_submenu.addAction(self.open_log_action)
# 创建下载设置子菜单
self.download_settings_menu = QMenu("下载设置", self.main_window)
self.download_settings_menu.setFont(menu_font)
self.download_settings_menu.setStyleSheet(menu_style)
# "修改下载源"按钮移至下载设置菜单
self.switch_source_action = QAction("修改下载源", self.main_window)
self.switch_source_action.setFont(menu_font)
self.switch_source_action.setEnabled(True)
self.switch_source_action.triggered.connect(self.show_under_development)
# 添加下载线程设置选项
self.thread_settings_action = QAction("下载线程设置", self.main_window)
self.thread_settings_action.setFont(menu_font)
# 连接到下载线程设置对话框
self.thread_settings_action.triggered.connect(self.show_download_thread_settings)
# 添加到下载设置子菜单
self.download_settings_menu.addAction(self.switch_source_action)
self.download_settings_menu.addAction(self.thread_settings_action)
# 添加到主菜单
self.ui.menu.addMenu(self.download_settings_menu) # 添加下载设置子菜单
self.ui.menu.addSeparator()
self.ui.menu.addMenu(self.dev_menu) # 添加开发者选项子菜单
# 添加Debug子菜单到开发者选项菜单
self.dev_menu.addMenu(self.debug_submenu)
self.dev_menu.addMenu(self.hosts_submenu) # 添加hosts文件选项子菜单
self.dev_menu.addMenu(self.ipv6_submenu) # 添加IPv6支持子菜单
def _handle_ipv6_toggle(self, enabled):
"""处理IPv6支持切换事件
Args:
enabled: 是否启用IPv6支持
"""
if not self.ipv6_manager:
# 显示错误提示
msg_box = self._create_message_box("错误", "\nIPv6管理器尚未初始化请稍后再试。\n")
msg_box.exec()
# 恢复复选框状态
self.ipv6_action.setChecked(not enabled)
return
# 使用IPv6Manager处理切换
success = self.ipv6_manager.toggle_ipv6_support(enabled)
# 如果切换失败,恢复复选框状态
if not success:
self.ipv6_action.setChecked(not enabled)
def show_menu(self, menu, button):
"""显示菜单
Args:
menu: 要显示的菜单
button: 触发菜单的按钮
"""
# 检查Ui_install中是否定义了show_menu方法
if hasattr(self.ui, 'show_menu'):
# 如果存在使用UI中定义的方法
self.ui.show_menu(menu, button)
else:
# 否则,使用默认的弹出方法
global_pos = button.mapToGlobal(button.rect().bottomLeft())
menu.popup(global_pos)
def open_project_home_page(self):
"""打开项目主页"""
webbrowser.open("https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT")
def open_github_page(self):
"""打开项目GitHub页面"""
webbrowser.open("https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT")
def open_faq_page(self):
"""打开常见问题页面"""
import locale
# 根据系统语言选择FAQ页面
system_lang = locale.getdefaultlocale()[0]
if system_lang and system_lang.startswith('zh'):
webbrowser.open("https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/blob/master/FAQ.md")
else:
webbrowser.open("https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/blob/master/FAQ-en.md")
def open_issues_page(self):
"""打开GitHub问题页面"""
webbrowser.open("https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/issues")
def open_qq_group(self):
"""打开QQ群链接"""
webbrowser.open("https://qm.qq.com/q/g9i04i5eec")
def open_privacy_policy(self):
"""打开完整隐私协议在GitHub上"""
webbrowser.open("https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/blob/master/PRIVACY.md")
def revoke_privacy_agreement(self):
"""撤回隐私协议同意,并重启软件"""
# 创建确认对话框
msg_box = self._create_message_box(
"确认操作",
"\n您确定要撤回隐私协议同意吗?\n\n撤回后软件将立即重启,您需要重新阅读并同意隐私协议。\n",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
)
reply = msg_box.exec()
if reply == QMessageBox.StandardButton.Yes:
# 用户确认撤回
try:
# 导入隐私管理器
from core.privacy_manager import PrivacyManager
import sys
import subprocess
import os
# 创建实例并重置隐私协议同意
privacy_manager = PrivacyManager()
if privacy_manager.reset_privacy_agreement():
# 显示重启提示
restart_msg = self._create_message_box(
"操作成功",
"\n已成功撤回隐私协议同意。\n\n软件将立即重启。\n"
)
restart_msg.exec()
# 获取当前执行的Python解释器路径和脚本路径
python_executable = sys.executable
script_path = os.path.abspath(sys.argv[0])
# 构建重启命令
restart_cmd = [python_executable, script_path]
# 启动新进程
subprocess.Popen(restart_cmd)
# 退出当前进程
sys.exit(0)
else:
# 显示失败提示
fail_msg = self._create_message_box(
"操作失败",
"\n撤回隐私协议同意失败。\n\n请检查应用权限或稍后再试。\n"
)
fail_msg.exec()
except Exception as e:
# 显示错误提示
error_msg = self._create_message_box(
"错误",
f"\n撤回隐私协议同意时发生错误:\n\n{str(e)}\n"
)
error_msg.exec()
def _create_message_box(self, title, message, buttons=QMessageBox.StandardButton.Ok):
"""创建统一风格的消息框
Args:
title: 消息框标题
message: 消息内容
buttons: 按钮类型,默认为确定按钮
Returns:
QMessageBox: 配置好的消息框实例
"""
msg_box = msgbox_frame(
f"{title} - {APP_NAME}",
message,
buttons,
)
return msg_box
def show_under_development(self):
"""显示功能正在开发中的提示"""
msg_box = self._create_message_box("提示", "\n该功能正在开发中,敬请期待!\n")
msg_box.exec()
def show_download_thread_settings(self):
"""显示下载线程设置对话框"""
if hasattr(self.main_window, 'download_manager'):
self.main_window.download_manager.show_download_thread_settings()
else:
# 如果下载管理器不可用,显示错误信息
msg_box = self._create_message_box("错误", "\n下载管理器未初始化,无法修改下载线程设置。\n")
msg_box.exec()
def open_log_file(self):
"""打开log.txt文件"""
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 = self._create_message_box("错误", f"\n打开log.txt文件失败\n\n{str(e)}\n")
msg_box.exec()
def restore_hosts_backup(self):
"""还原软件备份的hosts文件"""
if hasattr(self.main_window, 'download_manager') and hasattr(self.main_window.download_manager, 'hosts_manager'):
try:
# 调用恢复hosts文件的方法
result = self.main_window.download_manager.hosts_manager.restore()
if result:
msg_box = self._create_message_box("成功", "\nhosts文件已成功还原为备份版本。\n")
else:
msg_box = self._create_message_box("警告", "\n还原hosts文件失败或没有找到备份文件。\n")
msg_box.exec()
except Exception as e:
msg_box = self._create_message_box("错误", f"\n还原hosts文件时发生错误\n\n{str(e)}\n")
msg_box.exec()
else:
msg_box = self._create_message_box("错误", "\n无法访问hosts管理器。\n")
msg_box.exec()
def clean_hosts_entries(self):
"""手动删除软件添加的hosts条目"""
if hasattr(self.main_window, 'download_manager') and hasattr(self.main_window.download_manager, 'hosts_manager'):
try:
# 调用清理hosts条目的方法
result = self.main_window.download_manager.hosts_manager.check_and_clean_all_entries()
if result:
msg_box = self._create_message_box("成功", "\n已成功清理软件添加的hosts条目。\n")
else:
msg_box = self._create_message_box("提示", "\n未发现软件添加的hosts条目或清理操作失败。\n")
msg_box.exec()
except Exception as e:
msg_box = self._create_message_box("错误", f"\n清理hosts条目时发生错误\n\n{str(e)}\n")
msg_box.exec()
else:
msg_box = self._create_message_box("错误", "\n无法访问hosts管理器。\n")
msg_box.exec()
def open_hosts_file(self):
"""打开系统hosts文件"""
try:
# 获取hosts文件路径
hosts_path = os.path.join(os.environ['SystemRoot'], 'System32', 'drivers', 'etc', 'hosts')
# 检查文件是否存在
if os.path.exists(hosts_path):
# 使用操作系统默认程序打开hosts文件
if os.name == 'nt': # Windows
# 尝试以管理员权限打开记事本编辑hosts文件
try:
# 使用PowerShell以管理员身份启动记事本
subprocess.Popen(["powershell", "Start-Process", "notepad", hosts_path, "-Verb", "RunAs"])
except Exception as e:
# 如果失败,尝试直接打开
os.startfile(hosts_path)
else: # macOS 和 Linux
import subprocess
subprocess.call(['xdg-open', hosts_path])
else:
msg_box = self._create_message_box("错误", f"\nhosts文件不存在\n{hosts_path}\n")
msg_box.exec()
except Exception as e:
msg_box = self._create_message_box("错误", f"\n打开hosts文件时发生错误\n\n{str(e)}\n")
msg_box.exec()
def 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/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>
<p>- <a href="https://github.com/hosxy/aria2-fast">hosxy/aria2-fast</a>提供了修改版aria2c提高了下载速度和性能。</p>
"""
msg_box = msgbox_frame(
f"关于 - {APP_NAME}",
about_text,
QMessageBox.StandardButton.Ok,
)
msg_box.setTextFormat(Qt.TextFormat.RichText) # 使用Qt.TextFormat
msg_box.exec()
def show_ipv6_manager_not_ready(self):
"""显示IPv6管理器未准备好的提示"""
msg_box = self._create_message_box("错误", "\nIPv6管理器尚未初始化请稍后再试。\n")
msg_box.exec()

File diff suppressed because it is too large Load Diff

View File

@@ -11,10 +11,14 @@ from PySide6.QtGui import (QAction, QBrush, QColor, QConicalGradient,
from PySide6.QtWidgets import (QApplication, QLabel, QMainWindow, QMenu, from PySide6.QtWidgets import (QApplication, QLabel, QMainWindow, QMenu,
QMenuBar, QPushButton, QSizePolicy, QWidget, QHBoxLayout) QMenuBar, QPushButton, QSizePolicy, QWidget, QHBoxLayout)
import os import os
import logging
# 初始化日志记录器
logger = logging.getLogger(__name__)
# 导入配置常量 # 导入配置常量
from data.config import APP_NAME, APP_VERSION from config.config import APP_NAME, APP_VERSION
from utils import load_image_from_file from utils import load_image_from_file, resource_path
class Ui_MainWindows(object): class Ui_MainWindows(object):
def setupUi(self, MainWindows): def setupUi(self, MainWindows):
@@ -38,8 +42,21 @@ class Ui_MainWindows(object):
MainWindows.setDockNestingEnabled(False) MainWindows.setDockNestingEnabled(False)
# 加载自定义字体 # 加载自定义字体
font_id = QFontDatabase.addApplicationFont(os.path.join(os.path.dirname(os.path.dirname(__file__)), "fonts", "SmileySans-Oblique.ttf")) font_path = resource_path(os.path.join("assets", "fonts", "SmileySans-Oblique.ttf"))
font_family = QFontDatabase.applicationFontFamilies(font_id)[0] if font_id != -1 else "Arial" logger.info(f"尝试加载字体文件: {font_path}")
font_id = QFontDatabase.addApplicationFont(font_path)
if font_id != -1:
font_families = QFontDatabase.applicationFontFamilies(font_id)
if font_families:
font_family = font_families[0]
logger.info(f"成功加载字体: {font_family}{font_path}")
else:
logger.warning(f"字体加载成功但无法获取字体族: {font_path}")
font_family = "Arial"
else:
logger.error(f"字体加载失败: {font_path}")
font_family = "Arial"
self.custom_font = QFont(font_family, 16) # 创建字体对象大小为16 self.custom_font = QFont(font_family, 16) # 创建字体对象大小为16
self.custom_font.setWeight(QFont.Weight.Medium) # 设置为中等粗细,不要太粗 self.custom_font.setWeight(QFont.Weight.Medium) # 设置为中等粗细,不要太粗
@@ -299,8 +316,11 @@ class Ui_MainWindows(object):
self.loadbg.setObjectName(u"loadbg") self.loadbg.setObjectName(u"loadbg")
self.loadbg.setGeometry(QRect(0, 0, 1280, 655)) 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_path = resource_path(os.path.join("assets", "images", "BG", "bg1.jpg"))
logger.info(f"加载背景图: {bg_path}")
bg_pixmap = QPixmap(bg_path) bg_pixmap = QPixmap(bg_path)
if bg_pixmap.isNull():
logger.error(f"背景图加载失败: {bg_path}")
self.loadbg.setPixmap(bg_pixmap) self.loadbg.setPixmap(bg_pixmap)
self.loadbg.setScaledContents(True) self.loadbg.setScaledContents(True)
@@ -308,7 +328,8 @@ class Ui_MainWindows(object):
self.vol1bg.setObjectName(u"vol1bg") self.vol1bg.setObjectName(u"vol1bg")
self.vol1bg.setGeometry(QRect(0, 150, 93, 64)) 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") vol1_path = resource_path(os.path.join("assets", "images", "LOGO", "vo01_logo.png"))
logger.info(f"加载LOGO图: {vol1_path}")
self.vol1bg.setPixmap(QPixmap(vol1_path)) self.vol1bg.setPixmap(QPixmap(vol1_path))
self.vol1bg.setScaledContents(True) self.vol1bg.setScaledContents(True)
@@ -316,7 +337,7 @@ class Ui_MainWindows(object):
self.vol2bg.setObjectName(u"vol2bg") self.vol2bg.setObjectName(u"vol2bg")
self.vol2bg.setGeometry(QRect(0, 210, 93, 64)) 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") vol2_path = resource_path(os.path.join("assets", "images", "LOGO", "vo02_logo.png"))
self.vol2bg.setPixmap(QPixmap(vol2_path)) self.vol2bg.setPixmap(QPixmap(vol2_path))
self.vol2bg.setScaledContents(True) self.vol2bg.setScaledContents(True)
@@ -324,7 +345,7 @@ class Ui_MainWindows(object):
self.vol3bg.setObjectName(u"vol3bg") self.vol3bg.setObjectName(u"vol3bg")
self.vol3bg.setGeometry(QRect(0, 270, 93, 64)) 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") vol3_path = resource_path(os.path.join("assets", "images", "LOGO", "vo03_logo.png"))
self.vol3bg.setPixmap(QPixmap(vol3_path)) self.vol3bg.setPixmap(QPixmap(vol3_path))
self.vol3bg.setScaledContents(True) self.vol3bg.setScaledContents(True)
@@ -332,7 +353,7 @@ class Ui_MainWindows(object):
self.vol4bg.setObjectName(u"vol4bg") self.vol4bg.setObjectName(u"vol4bg")
self.vol4bg.setGeometry(QRect(0, 330, 93, 64)) 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") vol4_path = resource_path(os.path.join("assets", "images", "LOGO", "vo04_logo.png"))
self.vol4bg.setPixmap(QPixmap(vol4_path)) self.vol4bg.setPixmap(QPixmap(vol4_path))
self.vol4bg.setScaledContents(True) self.vol4bg.setScaledContents(True)
@@ -340,7 +361,7 @@ class Ui_MainWindows(object):
self.afterbg.setObjectName(u"afterbg") self.afterbg.setObjectName(u"afterbg")
self.afterbg.setGeometry(QRect(0, 390, 93, 64)) 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") after_path = resource_path(os.path.join("assets", "images", "LOGO", "voaf_logo.png"))
self.afterbg.setPixmap(QPixmap(after_path)) self.afterbg.setPixmap(QPixmap(after_path))
self.afterbg.setScaledContents(True) self.afterbg.setScaledContents(True)
@@ -349,7 +370,11 @@ class Ui_MainWindows(object):
self.Mainbg.setObjectName(u"Mainbg") self.Mainbg.setObjectName(u"Mainbg")
self.Mainbg.setGeometry(QRect(0, 0, 1280, 655)) 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")) main_bg_path = resource_path(os.path.join("assets", "images", "BG", "title_bg1.png"))
logger.info(f"加载主背景图: {main_bg_path}")
main_bg_pixmap = QPixmap(main_bg_path)
if main_bg_pixmap.isNull():
logger.error(f"主背景图加载失败: {main_bg_path}")
# 如果加载的图片不是空的,则设置,并允许拉伸填满 # 如果加载的图片不是空的,则设置,并允许拉伸填满
if not main_bg_pixmap.isNull(): if not main_bg_pixmap.isNull():
@@ -358,14 +383,18 @@ class Ui_MainWindows(object):
self.Mainbg.setAlignment(Qt.AlignmentFlag.AlignCenter) 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")) button_path = resource_path(os.path.join("assets", "images", "BTN", "Button.png"))
logger.info(f"加载按钮图片: {button_path}")
button_pixmap = QPixmap(button_path)
if button_pixmap.isNull():
logger.error(f"按钮图片加载失败: {button_path}")
# 创建文本标签布局的按钮 # 创建文本标签布局的按钮
# 开始安装按钮 - 基于背景图片和标签组合 # 开始安装按钮 - 基于背景图片和标签组合
# 调整开始安装按钮的位置 # 调整开始安装按钮的位置
self.button_container = QWidget(self.inner_content) self.button_container = QWidget(self.inner_content)
self.button_container.setObjectName(u"start_install_container") self.button_container.setObjectName(u"start_install_container")
self.button_container.setGeometry(QRect(1050, 200, 211, 111)) # 调整Y坐标,上移至200 self.button_container.setGeometry(QRect(1045, 20, 211, 111)) # 调整坐标,Y设为20X稍微左移
# 不要隐藏容器,让动画系统来控制它的可见性和位置 # 不要隐藏容器,让动画系统来控制它的可见性和位置
# 使用原来的按钮背景图片 # 使用原来的按钮背景图片
@@ -381,7 +410,7 @@ class Ui_MainWindows(object):
self.start_install_text.setText("开始安装") self.start_install_text.setText("开始安装")
self.start_install_text.setFont(self.custom_font) self.start_install_text.setFont(self.custom_font)
self.start_install_text.setAlignment(Qt.AlignmentFlag.AlignCenter) self.start_install_text.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.start_install_text.setStyleSheet("letter-spacing: 1px;") self.start_install_text.setStyleSheet("color: #333333; letter-spacing: 1px;")
# 点击区域透明按钮 # 点击区域透明按钮
self.start_install_btn = QPushButton(self.button_container) self.start_install_btn = QPushButton(self.button_container)
@@ -397,10 +426,44 @@ class Ui_MainWindows(object):
} }
""") """)
# 添加禁/启用补丁按钮 - 新增在开始安装和卸载补丁之间
self.toggle_patch_container = QWidget(self.inner_content)
self.toggle_patch_container.setObjectName(u"toggle_patch_container")
self.toggle_patch_container.setGeometry(QRect(1050, 180, 211, 111)) # 调整Y坐标设为180增大与开始安装的间距
# 使用相同的按钮背景图片
self.toggle_patch_bg = QLabel(self.toggle_patch_container)
self.toggle_patch_bg.setObjectName(u"toggle_patch_bg")
self.toggle_patch_bg.setGeometry(QRect(10, 10, 191, 91)) # 居中放置在扩大的容器中
self.toggle_patch_bg.setPixmap(button_pixmap)
self.toggle_patch_bg.setScaledContents(True)
self.toggle_patch_text = QLabel(self.toggle_patch_container)
self.toggle_patch_text.setObjectName(u"toggle_patch_text")
self.toggle_patch_text.setGeometry(QRect(10, 7, 191, 91)) # 居中放置在扩大的容器中
self.toggle_patch_text.setText("禁/启用补丁")
self.toggle_patch_text.setFont(self.custom_font)
self.toggle_patch_text.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.toggle_patch_text.setStyleSheet("color: #333333; letter-spacing: 1px;")
# 点击区域透明按钮
self.toggle_patch_btn = QPushButton(self.toggle_patch_container)
self.toggle_patch_btn.setObjectName(u"toggle_patch_btn")
self.toggle_patch_btn.setGeometry(QRect(10, 10, 191, 91)) # 居中放置在扩大的容器中
self.toggle_patch_btn.setCursor(QCursor(Qt.CursorShape.PointingHandCursor)) # 设置鼠标悬停时为手形光标
self.toggle_patch_btn.setFlat(True)
self.toggle_patch_btn.raise_() # 确保按钮在最上层
self.toggle_patch_btn.setStyleSheet("""
QPushButton {
background-color: transparent;
border: none;
}
""")
# 添加卸载补丁按钮 - 新增 # 添加卸载补丁按钮 - 新增
self.uninstall_container = QWidget(self.inner_content) self.uninstall_container = QWidget(self.inner_content)
self.uninstall_container.setObjectName(u"uninstall_container") self.uninstall_container.setObjectName(u"uninstall_container")
self.uninstall_container.setGeometry(QRect(1050, 310, 211, 111)) # 调整Y坐标位于310位置 self.uninstall_container.setGeometry(QRect(1050, 320, 211, 111)) # 设置Y坐标为320
# 使用相同的按钮背景图片 # 使用相同的按钮背景图片
self.uninstall_bg = QLabel(self.uninstall_container) self.uninstall_bg = QLabel(self.uninstall_container)
@@ -415,7 +478,7 @@ class Ui_MainWindows(object):
self.uninstall_text.setText("卸载补丁") self.uninstall_text.setText("卸载补丁")
self.uninstall_text.setFont(self.custom_font) self.uninstall_text.setFont(self.custom_font)
self.uninstall_text.setAlignment(Qt.AlignmentFlag.AlignCenter) self.uninstall_text.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.uninstall_text.setStyleSheet("letter-spacing: 1px;") self.uninstall_text.setStyleSheet("color: #333333; letter-spacing: 1px;")
# 点击区域透明按钮 # 点击区域透明按钮
self.uninstall_btn = QPushButton(self.uninstall_container) self.uninstall_btn = QPushButton(self.uninstall_container)
@@ -434,7 +497,7 @@ class Ui_MainWindows(object):
# 退出按钮 - 基于背景图片和标签组合,调整位置 # 退出按钮 - 基于背景图片和标签组合,调整位置
self.exit_container = QWidget(self.inner_content) self.exit_container = QWidget(self.inner_content)
self.exit_container.setObjectName(u"exit_container") self.exit_container.setObjectName(u"exit_container")
self.exit_container.setGeometry(QRect(1050, 420, 211, 111)) # 调整Y坐标下移至420 self.exit_container.setGeometry(QRect(1050, 450, 211, 111)) # 调整Y坐标设为450
# 不要隐藏容器,让动画系统来控制它的可见性和位置 # 不要隐藏容器,让动画系统来控制它的可见性和位置
# 使用原来的按钮背景图片 # 使用原来的按钮背景图片
@@ -450,7 +513,7 @@ class Ui_MainWindows(object):
self.exit_text.setText("退出程序") self.exit_text.setText("退出程序")
self.exit_text.setFont(self.custom_font) self.exit_text.setFont(self.custom_font)
self.exit_text.setAlignment(Qt.AlignmentFlag.AlignCenter) self.exit_text.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.exit_text.setStyleSheet("letter-spacing: 1px;") self.exit_text.setStyleSheet("color: #333333; letter-spacing: 1px;")
# 点击区域透明按钮 # 点击区域透明按钮
self.exit_btn = QPushButton(self.exit_container) self.exit_btn = QPushButton(self.exit_container)
@@ -476,11 +539,28 @@ class Ui_MainWindows(object):
self.vol4bg.raise_() self.vol4bg.raise_()
self.afterbg.raise_() self.afterbg.raise_()
self.Mainbg.raise_() self.Mainbg.raise_()
# 显式单独抬升开始安装按钮的所有组件
self.button_container.raise_() self.button_container.raise_()
self.uninstall_container.raise_() # 添加新按钮到层级顺序 self.start_install_bg.raise_()
self.start_install_text.raise_()
self.start_install_btn.raise_()
# 显式单独抬升禁/启用补丁按钮的所有组件
self.toggle_patch_container.raise_()
self.toggle_patch_bg.raise_()
self.toggle_patch_text.raise_()
self.toggle_patch_btn.raise_()
# 显式单独抬升卸载补丁按钮的所有组件
self.uninstall_container.raise_()
self.uninstall_bg.raise_()
self.uninstall_text.raise_()
self.uninstall_btn.raise_()
# 显式单独抬升退出按钮的所有组件
self.exit_container.raise_() self.exit_container.raise_()
self.exit_bg.raise_()
self.exit_text.raise_()
self.exit_btn.raise_()
# 其他UI元素
self.menu_area.raise_() # 确保菜单区域在背景之上 self.menu_area.raise_() # 确保菜单区域在背景之上
# self.menubar.raise_() # 不再需要菜单栏
self.settings_btn.raise_() # 确保设置按钮在上层 self.settings_btn.raise_() # 确保设置按钮在上层
self.help_btn.raise_() # 确保帮助按钮在上层 self.help_btn.raise_() # 确保帮助按钮在上层
self.title_bar.raise_() # 确保标题栏在最上层 self.title_bar.raise_() # 确保标题栏在最上层
@@ -491,7 +571,6 @@ class Ui_MainWindows(object):
# setupUi # setupUi
def retranslateUi(self, MainWindows): def retranslateUi(self, MainWindows):
MainWindows.setWindowTitle(QCoreApplication.translate("MainWindows", f"{APP_NAME} v{APP_VERSION}", None))
self.loadbg.setText("") self.loadbg.setText("")
self.vol1bg.setText("") self.vol1bg.setText("")
self.vol2bg.setText("") self.vol2bg.setText("")

View File

@@ -0,0 +1,16 @@
"""
UI组件模块
提供各种UI组件类用于构建应用程序界面
"""
from .font_style_manager import FontStyleManager
from .dialog_factory import DialogFactory
from .external_links_handler import ExternalLinksHandler
from .menu_builder import MenuBuilder
__all__ = [
'FontStyleManager',
'DialogFactory',
'ExternalLinksHandler',
'MenuBuilder'
]

View File

@@ -0,0 +1,147 @@
"""
对话框工厂
负责创建和管理各种类型的对话框
"""
from PySide6.QtWidgets import QMessageBox, QDialog, QVBoxLayout, QProgressBar, QLabel, QApplication
from PySide6.QtCore import Qt
from utils import msgbox_frame
from config.config import APP_NAME
from workers.download import ProgressWindow
class DialogFactory:
"""对话框工厂类"""
def __init__(self, main_window):
"""初始化对话框工厂
Args:
main_window: 主窗口实例
"""
self.main_window = main_window
self.loading_dialog = None
def create_message_box(self, title, message, buttons=QMessageBox.StandardButton.Ok):
"""创建统一风格的消息框
Args:
title: 消息框标题
message: 消息内容
buttons: 按钮类型,默认为确定按钮
Returns:
QMessageBox: 配置好的消息框实例
"""
msg_box = msgbox_frame(
f"{title} - {APP_NAME}",
message,
buttons,
)
return msg_box
def create_progress_window(self, title, initial_text="准备中..."):
"""创建并返回一个通用的进度窗口
Args:
title (str): 窗口标题
initial_text (str): 初始状态文本
Returns:
QDialog: 配置好的进度窗口实例
"""
# 如果是下载进度窗口使用专用的ProgressWindow类
if "下载" in title:
return ProgressWindow(self.main_window)
# 其他情况使用基本的进度窗口
progress_window = QDialog(self.main_window)
progress_window.setWindowTitle(f"{title} - {APP_NAME}")
progress_window.setFixedSize(400, 150)
layout = QVBoxLayout()
progress_bar = QProgressBar()
progress_bar.setRange(0, 100)
progress_bar.setValue(0)
layout.addWidget(progress_bar)
status_label = QLabel(initial_text)
layout.addWidget(status_label)
progress_window.setLayout(layout)
# 将控件附加到窗口对象上,以便外部访问
progress_window.progress_bar = progress_bar
progress_window.status_label = status_label
return progress_window
def show_loading_dialog(self, message):
"""显示或更新加载对话框
Args:
message: 要显示的加载消息
"""
if not self.loading_dialog:
self.loading_dialog = QDialog(self.main_window)
self.loading_dialog.setWindowTitle(f"请稍候 - {APP_NAME}")
self.loading_dialog.setFixedSize(300, 100)
self.loading_dialog.setModal(True)
layout = QVBoxLayout()
loading_label = QLabel(message)
loading_label.setAlignment(Qt.AlignCenter)
layout.addWidget(loading_label)
self.loading_dialog.setLayout(layout)
# 将label附加到dialog方便后续更新
self.loading_dialog.loading_label = loading_label
else:
self.loading_dialog.loading_label.setText(message)
self.loading_dialog.show()
# 强制UI更新
QApplication.processEvents()
def hide_loading_dialog(self):
"""隐藏并销毁加载对话框"""
if self.loading_dialog:
self.loading_dialog.hide()
self.loading_dialog = None
def show_simple_message(self, title, message, message_type="info"):
"""显示简单的消息提示
Args:
title: 标题
message: 消息内容
message_type: 消息类型,可选 "info", "warning", "error", "question"
"""
if message_type == "question":
buttons = QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
else:
buttons = QMessageBox.StandardButton.Ok
msg_box = self.create_message_box(title, message, buttons)
if message_type == "question":
return msg_box.exec()
else:
msg_box.exec()
return None
def show_confirmation_dialog(self, title, message):
"""显示确认对话框
Args:
title: 标题
message: 消息内容
Returns:
bool: 用户是否选择了确认
"""
msg_box = self.create_message_box(
title,
message,
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
return msg_box.exec() == QMessageBox.StandardButton.Yes

View File

@@ -0,0 +1,145 @@
"""
外部链接处理器
负责处理所有外部链接打开和关于信息显示
"""
import webbrowser
import locale
import sys
import subprocess
import os
from PySide6.QtWidgets import QMessageBox
from PySide6.QtCore import Qt
from config.config import APP_NAME, APP_VERSION
from utils import msgbox_frame
class ExternalLinksHandler:
"""外部链接处理器类"""
def __init__(self, main_window, dialog_factory=None):
"""初始化外部链接处理器
Args:
main_window: 主窗口实例
dialog_factory: 对话框工厂实例
"""
self.main_window = main_window
self.dialog_factory = dialog_factory
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):
"""打开常见问题页面"""
# 根据系统语言选择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 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/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>
<p>- <a href="https://github.com/hosxy/aria2-fast">hosxy/aria2-fast</a>提供了修改版aria2c提高了下载速度和性能。</p>
"""
msg_box = msgbox_frame(
f"关于 - {APP_NAME}",
about_text,
QMessageBox.StandardButton.Ok,
)
msg_box.setTextFormat(Qt.TextFormat.RichText)
msg_box.exec()
def revoke_privacy_agreement(self):
"""撤回隐私协议同意,并重启软件"""
# 创建确认对话框
if self.dialog_factory:
response = self.dialog_factory.show_confirmation_dialog(
"确认操作",
"\n您确定要撤回隐私协议同意吗?\n\n撤回后软件将立即重启,您需要重新阅读并同意隐私协议。\n"
)
else:
msg_box = msgbox_frame(
f"确认操作 - {APP_NAME}",
"\n您确定要撤回隐私协议同意吗?\n\n撤回后软件将立即重启,您需要重新阅读并同意隐私协议。\n",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
)
response = msg_box.exec() == QMessageBox.StandardButton.Yes
if response:
try:
from core.managers.privacy_manager import PrivacyManager
privacy_manager = PrivacyManager()
if privacy_manager.reset_privacy_agreement():
# 显示重启提示
if self.dialog_factory:
self.dialog_factory.show_simple_message(
"操作成功",
"\n已成功撤回隐私协议同意。\n\n软件将立即重启。\n"
)
else:
restart_msg = msgbox_frame(
f"操作成功 - {APP_NAME}",
"\n已成功撤回隐私协议同意。\n\n软件将立即重启。\n",
QMessageBox.StandardButton.Ok
)
restart_msg.exec()
# 重启应用程序
python_executable = sys.executable
script_path = os.path.abspath(sys.argv[0])
subprocess.Popen([python_executable, script_path])
sys.exit(0)
else:
if self.dialog_factory:
self.dialog_factory.show_simple_message(
"操作失败",
"\n撤回隐私协议同意失败。\n\n请检查应用权限或稍后再试。\n",
"error"
)
else:
msgbox_frame(
f"操作失败 - {APP_NAME}",
"\n撤回隐私协议同意失败。\n\n请检查应用权限或稍后再试。\n",
QMessageBox.StandardButton.Ok
).exec()
except Exception as e:
error_message = f"\n撤回隐私协议同意时发生错误:\n\n{str(e)}\n"
if self.dialog_factory:
self.dialog_factory.show_simple_message("错误", error_message, "error")
else:
msgbox_frame(
f"错误 - {APP_NAME}",
error_message,
QMessageBox.StandardButton.Ok
).exec()

View File

@@ -0,0 +1,147 @@
"""
字体和样式管理器
负责管理应用程序的字体加载和UI样式
"""
import os
import logging
import traceback
from PySide6.QtGui import QFont, QFontDatabase
from utils import resource_path
logger = logging.getLogger(__name__)
class FontStyleManager:
"""字体和样式管理器"""
def __init__(self):
"""初始化字体样式管理器"""
self._cached_font = None
self._font_family = "Arial" # 默认字体族
self._load_custom_font()
def _load_custom_font(self):
"""加载自定义字体"""
try:
# 使用resource_path查找字体文件
font_path = resource_path(os.path.join("assets", "fonts", "SmileySans-Oblique.ttf"))
# 详细记录字体加载过程
if os.path.exists(font_path):
logger.info(f"尝试加载字体文件: {font_path}")
font_id = QFontDatabase.addApplicationFont(font_path)
if font_id != -1:
font_families = QFontDatabase.applicationFontFamilies(font_id)
if font_families:
self._font_family = font_families[0]
logger.info(f"成功加载字体: {self._font_family}{font_path}")
else:
logger.warning(f"字体加载成功但无法获取字体族: {font_path}")
else:
logger.warning(f"字体加载失败: {font_path} (返回ID: {font_id})")
self._check_font_file_issues(font_path)
else:
logger.error(f"找不到字体文件: {font_path}")
self._list_font_directory(font_path)
except Exception as e:
logger.error(f"加载字体过程中发生异常: {e}")
logger.error(f"异常详情: {traceback.format_exc()}")
def _check_font_file_issues(self, font_path):
"""检查字体文件的问题"""
try:
file_size = os.path.getsize(font_path)
logger.debug(f"字体文件大小: {file_size} 字节")
if file_size == 0:
logger.error(f"字体文件大小为0字节: {font_path}")
# 尝试打开文件测试可读性
with open(font_path, 'rb') as f:
f.read(10) # 只读取前几个字节测试可访问性
logger.debug(f"字体文件可以正常打开和读取")
except Exception as file_error:
logger.error(f"字体文件访问错误: {file_error}")
def _list_font_directory(self, font_path):
"""列出字体目录下的文件"""
try:
fonts_dir = os.path.dirname(font_path)
if os.path.exists(fonts_dir):
files = os.listdir(fonts_dir)
logger.debug(f"字体目录 {fonts_dir} 中的文件: {files}")
else:
logger.debug(f"字体目录不存在: {fonts_dir}")
except Exception as dir_error:
logger.error(f"无法列出字体目录内容: {dir_error}")
def get_menu_font(self, size=14, bold=True):
"""获取菜单字体
Args:
size: 字体大小默认14
bold: 是否加粗默认True
Returns:
QFont: 配置好的菜单字体
"""
if self._cached_font is None or self._cached_font.pointSize() != size:
self._cached_font = QFont(self._font_family, size)
self._cached_font.setBold(bold)
return self._cached_font
def get_menu_style(self, font_family=None):
"""获取统一的菜单样式
Args:
font_family: 字体族,如果不提供则使用默认
Returns:
str: CSS样式字符串
"""
if font_family is None:
font_family = 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;
}}
"""
@property
def font_family(self):
"""获取当前字体族"""
return self._font_family

View File

@@ -0,0 +1,502 @@
"""
菜单构建器
负责构建和管理应用程序的各种菜单
"""
from PySide6.QtGui import QAction, QActionGroup, QCursor
from PySide6.QtWidgets import QMenu, QPushButton
from PySide6.QtCore import Qt, QRect
from config.config import APP_NAME, APP_VERSION
class MenuBuilder:
"""菜单构建器类"""
def __init__(self, main_window, font_style_manager, external_links_handler, dialog_factory):
"""初始化菜单构建器
Args:
main_window: 主窗口实例
font_style_manager: 字体样式管理器
external_links_handler: 外部链接处理器
dialog_factory: 对话框工厂
"""
self.main_window = main_window
self.ui = getattr(main_window, 'ui', None)
self.font_style_manager = font_style_manager
self.external_links_handler = external_links_handler
self.dialog_factory = dialog_factory
# 菜单引用
self.dev_menu = None
self.privacy_menu = None
self.about_menu = None
self.about_btn = None
# 工作模式相关
self.work_mode_menu = None
self.online_mode_action = None
self.offline_mode_action = None
# 开发者选项相关
self.debug_submenu = None
self.hosts_submenu = None
self.ipv6_submenu = None
self.hash_settings_menu = None
self.download_settings_menu = None
# 各种action引用
self.debug_action = None
self.open_log_action = None
self.ipv6_action = None
self.ipv6_test_action = None
self.disable_auto_restore_action = None
self.disable_pre_hash_action = None
def setup_all_menus(self):
"""设置所有菜单"""
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.font_style_manager.get_menu_font()
# 创建关于按钮
self.about_btn = QPushButton("关于", self.ui.menu_area)
self.about_btn.setObjectName(u"about_btn")
# 获取帮助按钮的位置和样式
help_btn_x = 0
help_btn_width = 0
if hasattr(self.ui, 'help_btn'):
help_btn_x = self.ui.help_btn.x()
help_btn_width = self.ui.help_btn.width()
# 设置位置在"帮助"按钮右侧
self.about_btn.setGeometry(QRect(help_btn_x + help_btn_width + 20, 1, 80, 28))
self.about_btn.setFont(menu_font)
self.about_btn.setCursor(QCursor(Qt.CursorShape.PointingHandCursor))
# 复制帮助按钮的样式
if hasattr(self.ui, 'help_btn'):
self.about_btn.setStyleSheet(self.ui.help_btn.styleSheet())
else:
# 默认样式
self.about_btn.setStyleSheet("""
QPushButton {
background-color: transparent;
color: white;
border: none;
text-align: left;
padding-left: 10px;
}
QPushButton:hover {
background-color: #F47A5B;
border-radius: 4px;
}
QPushButton:pressed {
background-color: #D25A3C;
border-radius: 4px;
}
""")
def setup_help_menu(self):
"""设置"帮助"菜单"""
if not self.ui or not hasattr(self.ui, 'menu_2'):
return
# 获取菜单字体
menu_font = self.font_style_manager.get_menu_font()
# 创建菜单项
faq_action = QAction("常见问题", self.main_window)
faq_action.triggered.connect(self.external_links_handler.open_faq_page)
faq_action.setFont(menu_font)
report_issue_action = QAction("提交错误", self.main_window)
report_issue_action.triggered.connect(self.external_links_handler.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)
def setup_about_menu(self):
"""设置"关于"菜单"""
# 获取菜单字体
menu_font = self.font_style_manager.get_menu_font()
# 创建关于菜单
self.about_menu = QMenu("关于", self.main_window)
self.about_menu.setFont(menu_font)
# 设置菜单样式
menu_style = self.font_style_manager.get_menu_style()
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.external_links_handler.show_about_dialog)
project_home_action = QAction("Github项目主页", self.main_window)
project_home_action.setFont(menu_font)
project_home_action.triggered.connect(self.external_links_handler.open_project_home_page)
qq_group_action = QAction("加入QQ群", self.main_window)
qq_group_action.setFont(menu_font)
qq_group_action.triggered.connect(self.external_links_handler.open_qq_group)
# 创建隐私协议菜单
self.setup_privacy_menu()
# 添加到关于菜单
self.about_menu.addAction(about_project_action)
self.about_menu.addAction(project_home_action)
self.about_menu.addAction(qq_group_action)
self.about_menu.addSeparator()
self.about_menu.addMenu(self.privacy_menu)
# 连接按钮点击事件
if self.about_btn:
self.about_btn.clicked.connect(lambda: self.show_menu(self.about_menu, self.about_btn))
def setup_privacy_menu(self):
"""设置"隐私协议"菜单"""
menu_font = self.font_style_manager.get_menu_font()
# 创建隐私协议子菜单
self.privacy_menu = QMenu("隐私协议", self.main_window)
self.privacy_menu.setFont(menu_font)
# 设置样式
menu_style = self.font_style_manager.get_menu_style()
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.external_links_handler.open_privacy_policy)
revoke_privacy_action = QAction("撤回隐私协议", self.main_window)
revoke_privacy_action.setFont(menu_font)
revoke_privacy_action.triggered.connect(self.external_links_handler.revoke_privacy_agreement)
# 添加到子菜单
self.privacy_menu.addAction(view_privacy_action)
self.privacy_menu.addAction(revoke_privacy_action)
def setup_settings_menu(self):
"""设置"设置"菜单"""
if not self.ui or not hasattr(self.ui, 'menu'):
return
# 获取菜单字体
menu_font = self.font_style_manager.get_menu_font()
menu_style = self.font_style_manager.get_menu_style()
# 创建各个子菜单
self._create_work_mode_menu(menu_font, menu_style)
self._create_download_settings_menu(menu_font, menu_style)
self._create_developer_options_menu(menu_font, menu_style)
# 添加到主菜单
self.ui.menu.addMenu(self.work_mode_menu)
self.ui.menu.addMenu(self.download_settings_menu)
self.ui.menu.addSeparator()
self.ui.menu.addMenu(self.dev_menu)
def _create_work_mode_menu(self, menu_font, menu_style):
"""创建工作模式子菜单"""
self.work_mode_menu = QMenu("工作模式", self.main_window)
self.work_mode_menu.setFont(menu_font)
self.work_mode_menu.setStyleSheet(menu_style)
# 获取当前离线模式状态
is_offline_mode = False
if hasattr(self.main_window, 'offline_mode_manager'):
is_offline_mode = self.main_window.offline_mode_manager.is_in_offline_mode()
# 创建在线模式和离线模式选项
self.online_mode_action = QAction("在线模式", self.main_window, checkable=True)
self.online_mode_action.setFont(menu_font)
self.online_mode_action.setChecked(not is_offline_mode)
self.offline_mode_action = QAction("离线模式", self.main_window, checkable=True)
self.offline_mode_action.setFont(menu_font)
self.offline_mode_action.setChecked(is_offline_mode)
# 将两个模式选项添加到同一个互斥组
mode_group = QActionGroup(self.main_window)
mode_group.addAction(self.online_mode_action)
mode_group.addAction(self.offline_mode_action)
mode_group.setExclusive(True)
# 连接切换事件这里需要在ui_manager中处理
self.online_mode_action.triggered.connect(lambda: self._handle_mode_switch("online"))
self.offline_mode_action.triggered.connect(lambda: self._handle_mode_switch("offline"))
# 添加到工作模式子菜单
self.work_mode_menu.addAction(self.online_mode_action)
self.work_mode_menu.addAction(self.offline_mode_action)
def _create_download_settings_menu(self, menu_font, menu_style):
"""创建下载设置子菜单"""
self.download_settings_menu = QMenu("下载设置", self.main_window)
self.download_settings_menu.setFont(menu_font)
self.download_settings_menu.setStyleSheet(menu_style)
# "修改下载源"按钮
switch_source_action = QAction("修改下载源", self.main_window)
switch_source_action.setFont(menu_font)
switch_source_action.setEnabled(True)
switch_source_action.triggered.connect(
lambda: self.dialog_factory.show_simple_message("提示", "\n该功能正在开发中,敬请期待!\n")
)
# 添加下载线程设置选项
thread_settings_action = QAction("下载线程设置", self.main_window)
thread_settings_action.setFont(menu_font)
thread_settings_action.triggered.connect(self._handle_download_thread_settings)
# 添加到下载设置子菜单
self.download_settings_menu.addAction(switch_source_action)
self.download_settings_menu.addAction(thread_settings_action)
def _create_developer_options_menu(self, menu_font, menu_style):
"""创建开发者选项子菜单"""
self.dev_menu = QMenu("开发者选项", self.main_window)
self.dev_menu.setFont(menu_font)
self.dev_menu.setStyleSheet(menu_style)
# 创建各个子菜单
self._create_debug_submenu(menu_font, menu_style)
self._create_hosts_submenu(menu_font, menu_style)
self._create_ipv6_submenu(menu_font, menu_style)
self._create_hash_settings_submenu(menu_font, menu_style)
# 添加到开发者选项菜单
self.dev_menu.addMenu(self.debug_submenu)
self.dev_menu.addMenu(self.hosts_submenu)
self.dev_menu.addMenu(self.ipv6_submenu)
self.dev_menu.addMenu(self.hash_settings_menu)
def _create_debug_submenu(self, menu_font, menu_style):
"""创建Debug子菜单"""
self.debug_submenu = QMenu("Debug模式", self.main_window)
self.debug_submenu.setFont(menu_font)
self.debug_submenu.setStyleSheet(menu_style)
# 创建Debug开关选项
self.debug_action = QAction("Debug开关", self.main_window, checkable=True)
self.debug_action.setFont(menu_font)
# 获取debug模式状态
config = getattr(self.main_window, 'config', {})
debug_mode = False
if isinstance(config, dict):
debug_mode = config.get("debug_mode", False)
self.debug_action.setChecked(debug_mode)
# 连接toggle_debug_mode方法
if hasattr(self.main_window, 'toggle_debug_mode'):
self.debug_action.triggered.connect(self.main_window.toggle_debug_mode)
# 创建打开log文件选项
self.open_log_action = QAction("打开log.txt", self.main_window)
self.open_log_action.setFont(menu_font)
self.open_log_action.setEnabled(debug_mode)
# 连接打开log文件的事件
if hasattr(self.main_window, 'debug_manager'):
self.open_log_action.triggered.connect(self.main_window.debug_manager.open_log_file)
else:
self.open_log_action.triggered.connect(
lambda: self.dialog_factory.show_simple_message("错误", "\n调试管理器未初始化。\n", "error")
)
# 添加到Debug子菜单
self.debug_submenu.addAction(self.debug_action)
self.debug_submenu.addAction(self.open_log_action)
def _create_hosts_submenu(self, menu_font, menu_style):
"""创建hosts文件选项子菜单"""
self.hosts_submenu = QMenu("hosts文件选项", self.main_window)
self.hosts_submenu.setFont(menu_font)
self.hosts_submenu.setStyleSheet(menu_style)
# 添加hosts子选项
restore_hosts_action = QAction("还原软件备份的hosts文件", self.main_window)
restore_hosts_action.setFont(menu_font)
restore_hosts_action.triggered.connect(self._handle_restore_hosts_backup)
clean_hosts_action = QAction("手动删除软件添加的hosts条目", self.main_window)
clean_hosts_action.setFont(menu_font)
clean_hosts_action.triggered.connect(self._handle_clean_hosts_entries)
# 添加禁用自动还原hosts的选项
self.disable_auto_restore_action = QAction("禁用关闭/重启自动还原hosts", self.main_window, checkable=True)
self.disable_auto_restore_action.setFont(menu_font)
# 从配置中读取当前状态
config = getattr(self.main_window, 'config', {})
disable_auto_restore = False
if isinstance(config, dict):
disable_auto_restore = config.get("disable_auto_restore_hosts", False)
self.disable_auto_restore_action.setChecked(disable_auto_restore)
self.disable_auto_restore_action.triggered.connect(self._handle_toggle_disable_auto_restore_hosts)
# 添加打开hosts文件选项
open_hosts_action = QAction("打开hosts文件", self.main_window)
open_hosts_action.setFont(menu_font)
open_hosts_action.triggered.connect(self._handle_open_hosts_file)
# 添加到hosts子菜单
self.hosts_submenu.addAction(self.disable_auto_restore_action)
self.hosts_submenu.addAction(restore_hosts_action)
self.hosts_submenu.addAction(clean_hosts_action)
self.hosts_submenu.addAction(open_hosts_action)
def _create_ipv6_submenu(self, menu_font, menu_style):
"""创建IPv6支持子菜单"""
self.ipv6_submenu = QMenu("IPv6支持", self.main_window)
self.ipv6_submenu.setFont(menu_font)
self.ipv6_submenu.setStyleSheet(menu_style)
# 添加IPv6支持选项
self.ipv6_action = QAction("启用IPv6支持", self.main_window, checkable=True)
self.ipv6_action.setFont(menu_font)
# 添加IPv6检测按钮
self.ipv6_test_action = QAction("测试IPv6连接", self.main_window)
self.ipv6_test_action.setFont(menu_font)
# 获取IPv6Manager实例
ipv6_manager = getattr(self.main_window, 'ipv6_manager', None)
if ipv6_manager:
self.ipv6_test_action.triggered.connect(ipv6_manager.show_ipv6_details)
else:
self.ipv6_test_action.triggered.connect(
lambda: self.dialog_factory.show_simple_message("错误", "\nIPv6管理器尚未初始化请稍后再试。\n", "error")
)
# 检查配置中是否已启用IPv6
config = getattr(self.main_window, 'config', {})
ipv6_enabled = False
if isinstance(config, dict):
ipv6_enabled = config.get("ipv6_enabled", False)
self.ipv6_action.setChecked(ipv6_enabled)
# 连接IPv6支持切换事件
self.ipv6_action.triggered.connect(self._handle_ipv6_toggle)
# 将选项添加到IPv6子菜单
self.ipv6_submenu.addAction(self.ipv6_action)
self.ipv6_submenu.addAction(self.ipv6_test_action)
def _create_hash_settings_submenu(self, menu_font, menu_style):
"""创建哈希校验设置子菜单"""
self.hash_settings_menu = QMenu("哈希校验设置", self.main_window)
self.hash_settings_menu.setFont(menu_font)
self.hash_settings_menu.setStyleSheet(menu_style)
# 添加禁用安装前哈希预检查选项
self.disable_pre_hash_action = QAction("禁用安装前哈希预检查", self.main_window, checkable=True)
self.disable_pre_hash_action.setFont(menu_font)
# 从配置中读取当前状态
config = getattr(self.main_window, 'config', {})
disable_pre_hash = False
if isinstance(config, dict):
disable_pre_hash = config.get("disable_pre_hash_check", False)
self.disable_pre_hash_action.setChecked(disable_pre_hash)
self.disable_pre_hash_action.triggered.connect(lambda checked: self._handle_pre_hash_toggle(checked))
# 添加到哈希校验设置子菜单
self.hash_settings_menu.addAction(self.disable_pre_hash_action)
def show_menu(self, menu, button):
"""显示菜单
Args:
menu: 要显示的菜单
button: 触发菜单的按钮
"""
# 检查Ui_install中是否定义了show_menu方法
if hasattr(self.ui, 'show_menu'):
self.ui.show_menu(menu, button)
else:
# 否则,使用默认的弹出方法
global_pos = button.mapToGlobal(button.rect().bottomLeft())
menu.popup(global_pos)
# 以下方法需要委托给ui_manager处理
def _handle_mode_switch(self, mode):
"""处理工作模式切换"""
# 这个方法需要在ui_manager中实现
if hasattr(self.main_window, 'ui_manager') and hasattr(self.main_window.ui_manager, 'switch_work_mode'):
self.main_window.ui_manager.switch_work_mode(mode)
else:
self.dialog_factory.show_simple_message("错误", "\n工作模式切换功能不可用。\n", "error")
def _handle_download_thread_settings(self):
"""处理下载线程设置"""
if hasattr(self.main_window, 'download_manager'):
self.main_window.download_manager.show_download_thread_settings()
else:
self.dialog_factory.show_simple_message("错误", "\n下载管理器未初始化,无法修改下载线程设置。\n", "error")
def _handle_ipv6_toggle(self, enabled):
"""处理IPv6支持切换"""
if hasattr(self.main_window, 'ui_manager') and hasattr(self.main_window.ui_manager, '_handle_ipv6_toggle'):
self.main_window.ui_manager._handle_ipv6_toggle(enabled)
else:
self.dialog_factory.show_simple_message("错误", "\nIPv6管理功能不可用。\n", "error")
def _handle_pre_hash_toggle(self, checked):
"""处理禁用安装前哈希预检查的切换"""
if hasattr(self.main_window, 'ui_manager') and hasattr(self.main_window.ui_manager, '_handle_pre_hash_toggle'):
self.main_window.ui_manager._handle_pre_hash_toggle(checked)
else:
self.dialog_factory.show_simple_message("错误", "\n哈希检查设置功能不可用。\n", "error")
def _handle_restore_hosts_backup(self):
"""处理还原hosts备份"""
if hasattr(self.main_window, 'ui_manager') and hasattr(self.main_window.ui_manager, 'restore_hosts_backup'):
self.main_window.ui_manager.restore_hosts_backup()
else:
self.dialog_factory.show_simple_message("错误", "\nhosts管理功能不可用。\n", "error")
def _handle_clean_hosts_entries(self):
"""处理清理hosts条目"""
if hasattr(self.main_window, 'ui_manager') and hasattr(self.main_window.ui_manager, 'clean_hosts_entries'):
self.main_window.ui_manager.clean_hosts_entries()
else:
self.dialog_factory.show_simple_message("错误", "\nhosts管理功能不可用。\n", "error")
def _handle_toggle_disable_auto_restore_hosts(self, checked):
"""处理切换禁用自动还原hosts"""
if hasattr(self.main_window, 'ui_manager') and hasattr(self.main_window.ui_manager, 'toggle_disable_auto_restore_hosts'):
self.main_window.ui_manager.toggle_disable_auto_restore_hosts(checked)
else:
self.dialog_factory.show_simple_message("错误", "\nhosts管理功能不可用。\n", "error")
def _handle_open_hosts_file(self):
"""处理打开hosts文件"""
if hasattr(self.main_window, 'ui_manager') and hasattr(self.main_window.ui_manager, 'open_hosts_file'):
self.main_window.ui_manager.open_hosts_file()
else:
self.dialog_factory.show_simple_message("错误", "\nhosts管理功能不可用。\n", "error")

View File

@@ -1,7 +1,8 @@
from .logger import Logger from .logger import Logger
from .url_censor import censor_url
from .helpers import ( from .helpers import (
load_base64_image, HashManager, AdminPrivileges, msgbox_frame, load_base64_image, HashManager, AdminPrivileges, msgbox_frame,
load_config, save_config, HostsManager, censor_url, resource_path, load_config, save_config, HostsManager, resource_path,
load_image_from_file load_image_from_file
) )

View File

@@ -9,27 +9,140 @@ import psutil
from PySide6 import QtCore, QtWidgets from PySide6 import QtCore, QtWidgets
import re import re
from PySide6.QtGui import QIcon, QPixmap from PySide6.QtGui import QIcon, QPixmap
from data.config import APP_NAME, CONFIG_FILE from PySide6.QtCore import Qt
from PySide6.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QProgressBar
from config.config import APP_NAME, CONFIG_FILE
from utils.logger import setup_logger
import datetime
import traceback
import subprocess
from pathlib import Path
# 初始化logger
logger = setup_logger("helpers")
class ProgressHashVerifyDialog(QDialog):
"""带进度条的哈希验证对话框"""
def __init__(self, title, message, parent=None):
"""初始化对话框
Args:
title: 对话框标题
message: 对话框消息
parent: 父窗口
"""
super().__init__(parent)
self.setWindowTitle(title)
self.setMinimumWidth(400)
self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint)
# 创建布局
layout = QVBoxLayout(self)
# 添加消息标签
self.message_label = QLabel(message)
self.message_label.setAlignment(Qt.AlignCenter)
layout.addWidget(self.message_label)
# 添加进度条
self.progress_bar = QProgressBar()
self.progress_bar.setRange(0, 100)
self.progress_bar.setValue(0)
layout.addWidget(self.progress_bar)
# 添加状态标签
self.status_label = QLabel("正在准备...")
self.status_label.setAlignment(Qt.AlignCenter)
layout.addWidget(self.status_label)
# 添加取消按钮
button_layout = QHBoxLayout()
self.cancel_button = QPushButton("取消")
self.cancel_button.clicked.connect(self.reject)
button_layout.addStretch()
button_layout.addWidget(self.cancel_button)
layout.addLayout(button_layout)
def update_progress(self, value):
"""更新进度条
Args:
value: 进度值 (0-100)
"""
self.progress_bar.setValue(value)
# 更新状态文本
if value < 10:
self.status_label.setText("正在准备...")
elif value < 50:
self.status_label.setText("正在解压文件...")
elif value < 70:
self.status_label.setText("正在查找补丁文件...")
elif value < 95:
self.status_label.setText("正在计算哈希值...")
else:
self.status_label.setText("正在验证哈希值...")
def set_message(self, message):
"""设置消息文本
Args:
message: 消息文本
"""
self.message_label.setText(message)
def set_status(self, status):
"""设置状态文本
Args:
status: 状态文本
"""
self.status_label.setText(status)
def resource_path(relative_path): def resource_path(relative_path):
"""获取资源的绝对路径,适用于开发环境和Nuitka打包环境""" """获取资源的绝对路径,适用于开发环境和打包环境"""
if getattr(sys, 'frozen', False): try:
# Nuitka/PyInstaller创建的临时文件夹并将路径存储在_MEIPASS中或与可执行文件同目录 if getattr(sys, 'frozen', False):
if hasattr(sys, '_MEIPASS'): # 打包环境 - 可执行文件所在目录
base_path = sys._MEIPASS if hasattr(sys, '_MEIPASS'):
# PyInstaller打包的临时目录
base_path = sys._MEIPASS
else:
# 其他打包方式,直接使用可执行文件目录
base_path = os.path.dirname(sys.executable)
# 对于离线补丁文件,需要在可执行文件所在目录查找
if relative_path.lower() in ["vol.1.7z", "vol.2.7z", "vol.3.7z", "vol.4.7z", "after.7z"]:
exe_dir = os.path.dirname(sys.executable)
patch_path = os.path.join(exe_dir, relative_path)
if os.path.exists(patch_path):
logger.debug(f"找到离线补丁文件: {patch_path}")
return patch_path
else: else:
base_path = os.path.dirname(sys.executable) # 在开发环境中运行
else: base_path = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
# 在开发环境中运行
base_path = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
# 处理特殊的可执行文件和数据文件路径 # 处理特殊的可执行文件和数据文件路径
if relative_path in ("aria2c-fast_x64.exe", "cfst.exe"): if relative_path in ("aria2c-fast_x64.exe", "cfst.exe"):
return os.path.join(base_path, 'bin', relative_path) result_path = os.path.join(base_path, 'bin', relative_path)
elif relative_path in ("ip.txt", "ipv6.txt"): elif relative_path in ("ip.txt", "ipv6.txt"):
return os.path.join(base_path, 'data', relative_path) result_path = os.path.join(base_path, 'data', relative_path)
else:
# 标准资源路径
result_path = os.path.join(base_path, relative_path)
return os.path.join(base_path, relative_path) # 记录资源路径并验证是否存在
if not os.path.exists(result_path) and relative_path: # 只在非空路径时检查
logger.warning(f"资源文件不存在: {result_path}")
elif relative_path: # 避免记录空路径
logger.debug(f"已找到资源文件: {result_path}")
return result_path
except Exception as e:
logger.error(f"资源路径解析错误 ({relative_path}): {e}")
# 出错时仍返回一个基本路径
return os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", relative_path)
def load_base64_image(base64_str): def load_base64_image(base64_str):
pixmap = QPixmap() pixmap = QPixmap()
@@ -45,9 +158,57 @@ def load_image_from_file(file_path):
Returns: Returns:
QPixmap: 加载的图像 QPixmap: 加载的图像
""" """
if os.path.exists(file_path): try:
return QPixmap(file_path) if os.path.exists(file_path):
return QPixmap() logger.info(f"加载图片: {file_path}")
pixmap = QPixmap(file_path)
if pixmap.isNull():
logger.error(f"图片加载失败(pixmap为空): {file_path}")
# 检查文件大小和是否可读
try:
file_size = os.path.getsize(file_path)
logger.debug(f"图片文件大小: {file_size} 字节")
if file_size == 0:
logger.error(f"图片文件大小为0字节: {file_path}")
# 尝试打开文件测试可读性
with open(file_path, 'rb') as f:
# 只读取前几个字节测试可访问性
f.read(10)
logger.debug(f"图片文件可以正常打开和读取")
# 检查文件扩展名是否正确
ext = os.path.splitext(file_path)[1].lower()
if ext not in ['.png', '.jpg', '.jpeg', '.bmp', '.ico']:
logger.warning(f"图片文件扩展名可能不受支持: {ext}")
except Exception as file_error:
logger.error(f"图片文件访问错误: {file_error}")
return QPixmap()
else:
logger.debug(f"图片加载成功: {file_path}, 大小: {pixmap.width()}x{pixmap.height()}")
return pixmap
else:
logger.warning(f"图片文件不存在: {file_path}")
# 尝试列出父目录下的文件
try:
parent_dir = os.path.dirname(file_path)
if os.path.exists(parent_dir):
files = os.listdir(parent_dir)
logger.debug(f"目录 {parent_dir} 中的文件: {files}")
else:
logger.debug(f"目录不存在: {parent_dir}")
except Exception as dir_error:
logger.error(f"无法列出目录内容: {dir_error}")
return QPixmap()
except Exception as e:
logger.error(f"加载图片时发生异常: {e}")
logger.error(f"异常详情: {traceback.format_exc()}")
return QPixmap()
def msgbox_frame(title, text, buttons=QtWidgets.QMessageBox.StandardButton.NoButton): def msgbox_frame(title, text, buttons=QtWidgets.QMessageBox.StandardButton.NoButton):
msg_box = QtWidgets.QMessageBox() msg_box = QtWidgets.QMessageBox()
@@ -55,7 +216,7 @@ def msgbox_frame(title, text, buttons=QtWidgets.QMessageBox.StandardButton.NoBut
msg_box.setWindowModality(QtCore.Qt.WindowModality.WindowModal) msg_box.setWindowModality(QtCore.Qt.WindowModality.WindowModal)
# 直接加载图标文件 # 直接加载图标文件
icon_path = resource_path(os.path.join("IMG", "ICO", "icon.png")) icon_path = resource_path(os.path.join("assets", "images", "ICO", "icon.png"))
if os.path.exists(icon_path): if os.path.exists(icon_path):
pixmap = QPixmap(icon_path) pixmap = QPixmap(icon_path)
if not pixmap.isNull(): if not pixmap.isNull():
@@ -83,7 +244,7 @@ def save_config(config):
with open(CONFIG_FILE, "w", encoding="utf-8") as f: with open(CONFIG_FILE, "w", encoding="utf-8") as f:
json.dump(config, f, indent=4) json.dump(config, f, indent=4)
except IOError as e: except IOError as e:
print(f"Error saving config: {e}") logger.error(f"Error saving config: {e}")
class HashManager: class HashManager:
@@ -109,77 +270,168 @@ class HashManager:
results[file_path] = future.result() results[file_path] = future.result()
except Exception as e: except Exception as e:
results[file_path] = None # Mark as failed results[file_path] = None # Mark as failed
print(f"Error calculating hash for {file_path}: {e}") logger.error(f"Error calculating hash for {file_path}: {e}")
return results return results
def hash_pop_window(self, check_type="default"): def hash_pop_window(self, check_type="default", is_offline=False, auto_close=False, close_delay=500):
"""显示文件检验窗口 """显示文件检验窗口
Args: Args:
check_type: 检查类型,可以是 'pre'(预检查), 'after'(后检查), 'extraction'(解压后检查) check_type: 检查类型,可以是 'pre'(预检查), 'after'(后检查), 'extraction'(解压后检查), 'offline_extraction'(离线解压), 'offline_verify'(离线验证)
is_offline: 是否处于离线模式
auto_close: 是否自动关闭窗口
close_delay: 自动关闭延迟(毫秒)
Returns: Returns:
QMessageBox: 消息框实例 QMessageBox: 消息框实例
""" """
message = "\n正在检验文件状态...\n" message = "\n正在检验文件状态...\n"
if check_type == "pre": if is_offline:
message = "\n正在检查游戏文件以确定需要安装的补丁...\n" # 离线模式的消息
elif check_type == "after": if check_type == "pre":
message = "\n正在检验本地文件完整性...\n" message = "\n正在检查游戏文件以确定需要安装的补丁...\n"
elif check_type == "extraction": elif check_type == "after":
message = "\n正在验证下载的解压文件完整性...\n" message = "\n正在检验本地文件完整性...\n"
elif check_type == "offline_verify":
message = "\n正在验证本地补丁压缩文件完整性...\n"
elif check_type == "offline_extraction":
message = "\n正在解压安装补丁文件...\n"
elif check_type == "offline_installation":
message = "\n正在安装补丁文件...\n"
else:
message = "\n正在处理离线补丁文件...\n"
else:
# 在线模式的消息
if check_type == "pre":
message = "\n正在检查游戏文件以确定需要安装的补丁...\n"
elif check_type == "after":
message = "\n正在检验本地文件完整性...\n"
elif check_type == "extraction":
message = "\n正在验证下载的解压文件完整性...\n"
elif check_type == "post":
message = "\n正在检验补丁文件完整性...\n"
# 创建新的消息框
msg_box = msgbox_frame(f"通知 - {APP_NAME}", message) msg_box = msgbox_frame(f"通知 - {APP_NAME}", message)
# 使用open()而不是exec()避免阻塞UI线程
msg_box.open() msg_box.open()
# 处理事件循环,确保窗口显示
QtWidgets.QApplication.processEvents() QtWidgets.QApplication.processEvents()
# 如果设置了自动关闭,添加定时器
if auto_close:
timer = QtCore.QTimer()
timer.setSingleShot(True)
timer.timeout.connect(msg_box.close)
timer.start(close_delay)
# 保存定时器引用,防止被垃圾回收
msg_box.close_timer = timer
return msg_box return msg_box
def cfg_pre_hash_compare(self, install_paths, plugin_hash, installed_status): def cfg_pre_hash_compare(self, install_paths, plugin_hash, installed_status):
status_copy = installed_status.copy() status_copy = installed_status.copy()
debug_mode = False
# 尝试检测是否处于调试模式
try:
from config.config import CACHE
debug_file = os.path.join(os.path.dirname(CACHE), "debug_mode.txt")
debug_mode = os.path.exists(debug_file)
except:
pass
for game_version, install_path in install_paths.items(): for game_version, install_path in install_paths.items():
if not os.path.exists(install_path): if not os.path.exists(install_path):
status_copy[game_version] = False status_copy[game_version] = False
if debug_mode:
logger.debug(f"DEBUG: 哈希预检查 - {game_version} 补丁文件不存在: {install_path}")
continue continue
try: try:
expected_hash = plugin_hash.get(game_version, "")
if not expected_hash:
if debug_mode:
logger.debug(f"DEBUG: 哈希预检查 - {game_version} 没有预期哈希值,跳过哈希检查")
# 当没有预期哈希值时,保持当前状态不变
continue
file_hash = self.hash_calculate(install_path) file_hash = self.hash_calculate(install_path)
if file_hash == plugin_hash.get(game_version):
if debug_mode:
logger.debug(f"DEBUG: 哈希预检查 - {game_version}")
logger.debug(f"DEBUG: 文件路径: {install_path}")
logger.debug(f"DEBUG: 预期哈希值: {expected_hash}")
logger.debug(f"DEBUG: 实际哈希值: {file_hash}")
logger.debug(f"DEBUG: 哈希匹配: {file_hash == expected_hash}")
if file_hash == expected_hash:
status_copy[game_version] = True status_copy[game_version] = True
if debug_mode:
logger.debug(f"DEBUG: 哈希预检查 - {game_version} 哈希匹配成功")
else: else:
status_copy[game_version] = False status_copy[game_version] = False
except Exception: if debug_mode:
logger.debug(f"DEBUG: 哈希预检查 - {game_version} 哈希不匹配")
except Exception as e:
status_copy[game_version] = False status_copy[game_version] = False
if debug_mode:
logger.debug(f"DEBUG: 哈希预检查异常 - {game_version}: {str(e)}")
return status_copy return status_copy
def cfg_after_hash_compare(self, install_paths, plugin_hash, installed_status): def cfg_after_hash_compare(self, install_paths, plugin_hash, installed_status):
debug_mode = False
# 尝试检测是否处于调试模式
try:
from config.config import CACHE
debug_file = os.path.join(os.path.dirname(CACHE), "debug_mode.txt")
debug_mode = os.path.exists(debug_file)
except:
pass
file_paths = [ file_paths = [
install_paths[game] for game in plugin_hash if installed_status.get(game) install_paths[game] for game in plugin_hash if installed_status.get(game)
] ]
hash_results = self.calculate_hashes_in_parallel(file_paths) hash_results = self.calculate_hashes_in_parallel(file_paths)
for game, hash_value in plugin_hash.items(): for game, expected_hash in plugin_hash.items():
if installed_status.get(game): if installed_status.get(game):
file_path = install_paths[game] file_path = install_paths[game]
file_hash = hash_results.get(file_path) file_hash = hash_results.get(file_path)
if debug_mode:
logger.debug(f"DEBUG: 哈希后检查 - {game}")
logger.debug(f"DEBUG: 文件路径: {file_path}")
logger.debug(f"DEBUG: 预期哈希值: {expected_hash}")
logger.debug(f"DEBUG: 实际哈希值: {file_hash if file_hash else '计算失败'}")
if file_hash is None: if file_hash is None:
installed_status[game] = False installed_status[game] = False
if debug_mode:
logger.debug(f"DEBUG: 哈希后检查失败 - 无法计算文件哈希值: {game}")
return { return {
"passed": False, "passed": False,
"game": game, "game": game,
"message": f"\n无法计算 {game} 的文件哈希值,文件可能已损坏或被占用。\n" "message": f"\n无法计算 {game} 的文件哈希值,文件可能已损坏或被占用。\n"
} }
if file_hash != hash_value: if file_hash != expected_hash:
installed_status[game] = False installed_status[game] = False
if debug_mode:
logger.debug(f"DEBUG: 哈希后检查失败 - 哈希值不匹配: {game}")
return { return {
"passed": False, "passed": False,
"game": game, "game": game,
"message": f"\n检测到 {game} 的文件哈希值不匹配。\n" "message": f"\n检测到 {game} 的文件哈希值不匹配。\n"
} }
if debug_mode:
logger.debug(f"DEBUG: 哈希后检查通过 - 所有文件哈希值匹配")
return {"passed": True} return {"passed": True}
class AdminPrivileges: class AdminPrivileges:
@@ -206,65 +458,98 @@ class AdminPrivileges:
"\n需要管理员权限运行此程序\n", "\n需要管理员权限运行此程序\n",
QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No, QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No,
) )
reply = msg_box.exec() try:
if reply == QtWidgets.QMessageBox.StandardButton.Yes: reply = msg_box.exec()
try: if reply == QtWidgets.QMessageBox.StandardButton.Yes:
ctypes.windll.shell32.ShellExecuteW( try:
None, "runas", sys.executable, " ".join(sys.argv), None, 1 ctypes.windll.shell32.ShellExecuteW(
) None, "runas", sys.executable, " ".join(sys.argv), None, 1
except Exception as e: )
except Exception as e:
msg_box = msgbox_frame(
f"错误 - {APP_NAME}",
f"\n请求管理员权限失败\n\n【错误信息】:{e}\n",
QtWidgets.QMessageBox.StandardButton.Ok,
)
msg_box.exec()
sys.exit(1)
else:
msg_box = msgbox_frame( msg_box = msgbox_frame(
f"错误 - {APP_NAME}", f"权限检测 - {APP_NAME}",
f"\n请求管理员权限失败\n\n【错误信息】:{e}\n", "\n无法获取管理员权限,程序将退出\n",
QtWidgets.QMessageBox.StandardButton.Ok, QtWidgets.QMessageBox.StandardButton.Ok,
) )
msg_box.exec() msg_box.exec()
sys.exit(1) sys.exit(1)
else: except KeyboardInterrupt:
logger.warning("管理员权限请求被用户中断")
msg_box = msgbox_frame( msg_box = msgbox_frame(
f"权限检测 - {APP_NAME}", f"权限检测 - {APP_NAME}",
"\n无法获取管理员权限,程序将退出\n", "\n操作被中断,程序将退出\n",
QtWidgets.QMessageBox.StandardButton.Ok, QtWidgets.QMessageBox.StandardButton.Ok,
) )
msg_box.exec()
sys.exit(1)
except Exception as e:
logger.error(f"管理员权限请求时发生错误: {e}")
msg_box = msgbox_frame(
f"错误 - {APP_NAME}",
f"\n请求管理员权限时发生未知错误\n\n【错误信息】:{e}\n",
QtWidgets.QMessageBox.StandardButton.Ok,
)
msg_box.exec() msg_box.exec()
sys.exit(1) sys.exit(1)
def check_and_terminate_processes(self): def check_and_terminate_processes(self):
for proc in psutil.process_iter(["pid", "name"]): try:
proc_name = proc.info["name"].lower() if proc.info["name"] else "" for proc in psutil.process_iter(["pid", "name"]):
proc_name = proc.info["name"].lower() if proc.info["name"] else ""
# 检查进程名是否匹配任何需要终止的游戏进程 # 检查进程名是否匹配任何需要终止的游戏进程
for exe in self.required_exes: for exe in self.required_exes:
if exe.lower() == proc_name: if exe.lower() == proc_name:
# 获取不带.nocrack的游戏名称用于显示 # 获取不带.nocrack的游戏名称用于显示
display_name = exe.replace(".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( msg_box = msgbox_frame(
f"进程检测 - {APP_NAME}", f"进程检测 - {APP_NAME}",
f"\n未关闭的游戏 {display_name} \n\n请手动关闭后重启应用\n", f"\n检测到游戏正在运行 {display_name} \n\n是否终止?\n",
QtWidgets.QMessageBox.StandardButton.Ok, QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No,
) )
msg_box.exec() try:
sys.exit(1) 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未关闭的游戏: {display_name} \n\n请手动关闭后重启应用\n",
QtWidgets.QMessageBox.StandardButton.Ok,
)
msg_box.exec()
sys.exit(1)
except KeyboardInterrupt:
logger.warning(f"进程 {display_name} 终止操作被用户中断")
raise
except Exception as e:
logger.error(f"进程 {display_name} 终止操作时发生错误: {e}")
raise
except KeyboardInterrupt:
logger.warning("进程检查被用户中断")
raise
except Exception as e:
logger.error(f"进程检查时发生错误: {e}")
raise
class HostsManager: class HostsManager:
def __init__(self): def __init__(self):
@@ -273,20 +558,66 @@ class HostsManager:
self.original_content = None self.original_content = None
self.modified = False self.modified = False
self.modified_hostnames = set() # 跟踪被修改的主机名 self.modified_hostnames = set() # 跟踪被修改的主机名
self.auto_restore_disabled = False # 是否禁用自动还原hosts
def get_hostname_entries(self, hostname):
"""获取hosts文件中指定域名的所有IP记录
Args:
hostname: 要查询的域名
Returns:
list: 域名对应的IP地址列表如果未找到则返回空列表
"""
try:
# 如果original_content为空先读取hosts文件
if not self.original_content:
try:
with open(self.hosts_path, 'r', encoding='utf-8') as f:
self.original_content = f.read()
except Exception as e:
logger.error(f"读取hosts文件失败: {e}")
return []
# 解析hosts文件中的每一行
ip_addresses = []
lines = self.original_content.splitlines()
for line in lines:
# 跳过注释和空行
line = line.strip()
if not line or line.startswith('#'):
continue
# 分割行内容获取IP和域名
parts = line.split()
if len(parts) >= 2: # 至少包含IP和一个域名
ip = parts[0]
domains = parts[1:]
# 如果当前行包含目标域名
if hostname in domains:
ip_addresses.append(ip)
return ip_addresses
except Exception as e:
logger.error(f"获取hosts记录失败: {e}")
return []
def backup(self): def backup(self):
if not AdminPrivileges().is_admin(): if not AdminPrivileges().is_admin():
print("需要管理员权限来备份hosts文件。") logger.warning("需要管理员权限来备份hosts文件。")
return False return False
try: try:
with open(self.hosts_path, 'r', encoding='utf-8') as f: with open(self.hosts_path, 'r', encoding='utf-8') as f:
self.original_content = f.read() self.original_content = f.read()
with open(self.backup_path, 'w', encoding='utf-8') as f: with open(self.backup_path, 'w', encoding='utf-8') as f:
f.write(self.original_content) f.write(self.original_content)
print(f"Hosts文件已备份到: {self.backup_path}") logger.debug(f"Hosts文件已备份到: {self.backup_path}")
return True return True
except IOError as e: except IOError as e:
print(f"备份hosts文件失败: {e}") logger.error(f"备份hosts文件失败: {e}")
msg_box = msgbox_frame(f"错误 - {APP_NAME}", f"\n无法备份hosts文件请检查权限。\n\n【错误信息】:{e}\n", QtWidgets.QMessageBox.StandardButton.Ok) msg_box = msgbox_frame(f"错误 - {APP_NAME}", f"\n无法备份hosts文件请检查权限。\n\n【错误信息】:{e}\n", QtWidgets.QMessageBox.StandardButton.Ok)
msg_box.exec() msg_box.exec()
return False return False
@@ -306,11 +637,11 @@ class HostsManager:
# 确保original_content不为None # 确保original_content不为None
if not self.original_content: if not self.original_content:
print("无法读取hosts文件内容操作中止。") logger.error("无法读取hosts文件内容操作中止。")
return False return False
if not AdminPrivileges().is_admin(): if not AdminPrivileges().is_admin():
print("需要管理员权限来修改hosts文件。") logger.warning("需要管理员权限来修改hosts文件。")
return False return False
try: try:
@@ -319,7 +650,7 @@ class HostsManager:
# 如果没有变化,不需要写入 # 如果没有变化,不需要写入
if len(new_lines) == len(lines): if len(new_lines) == len(lines):
print(f"Hosts文件中没有找到 {hostname} 的记录") logger.info(f"Hosts文件中没有找到 {hostname} 的记录")
return True return True
with open(self.hosts_path, 'w', encoding='utf-8') as f: with open(self.hosts_path, 'w', encoding='utf-8') as f:
@@ -327,10 +658,10 @@ class HostsManager:
# 更新原始内容 # 更新原始内容
self.original_content = '\n'.join(new_lines) self.original_content = '\n'.join(new_lines)
print(f"已从hosts文件中清理 {hostname} 的记录") logger.info(f"已从hosts文件中清理 {hostname} 的记录")
return True return True
except IOError as e: except IOError as e:
print(f"清理hosts文件失败: {e}") logger.error(f"清理hosts文件失败: {e}")
return False return False
def apply_ip(self, hostname, ip_address, clean=True): def apply_ip(self, hostname, ip_address, clean=True):
@@ -339,11 +670,11 @@ class HostsManager:
return False return False
if not self.original_content: # 再次检查确保backup成功 if not self.original_content: # 再次检查确保backup成功
print("无法读取hosts文件内容操作中止。") logger.error("无法读取hosts文件内容操作中止。")
return False return False
if not AdminPrivileges().is_admin(): if not AdminPrivileges().is_admin():
print("需要管理员权限来修改hosts文件。") logger.warning("需要管理员权限来修改hosts文件。")
return False return False
try: try:
@@ -365,22 +696,71 @@ class HostsManager:
self.modified = True self.modified = True
# 记录被修改的主机名,用于最终清理 # 记录被修改的主机名,用于最终清理
self.modified_hostnames.add(hostname) self.modified_hostnames.add(hostname)
print(f"Hosts文件已更新: {new_entry}") logger.info(f"Hosts文件已更新: {new_entry}")
return True return True
except IOError as e: except IOError as e:
print(f"修改hosts文件失败: {e}") logger.error(f"修改hosts文件失败: {e}")
msg_box = msgbox_frame(f"错误 - {APP_NAME}", f"\n无法修改hosts文件请检查权限。\n\n【错误信息】:{e}\n", QtWidgets.QMessageBox.StandardButton.Ok) msg_box = msgbox_frame(f"错误 - {APP_NAME}", f"\n无法修改hosts文件请检查权限。\n\n【错误信息】:{e}\n", QtWidgets.QMessageBox.StandardButton.Ok)
msg_box.exec() msg_box.exec()
return False return False
def check_and_clean_all_entries(self): def set_auto_restore_disabled(self, disabled):
"""设置是否禁用自动还原hosts
Args:
disabled: 是否禁用自动还原hosts
Returns:
bool: 操作是否成功
"""
try:
# 更新状态
self.auto_restore_disabled = disabled
# 从配置文件读取当前配置
from utils import load_config, save_config
config = load_config()
# 更新配置
config['disable_auto_restore_hosts'] = disabled
# 保存配置
save_config(config)
logger.info(f"{'禁用' if disabled else '启用'}自动还原hosts")
return True
except Exception as e:
logger.error(f"设置自动还原hosts状态失败: {e}")
return False
def is_auto_restore_disabled(self):
"""检查是否禁用了自动还原hosts
Returns:
bool: 是否禁用自动还原hosts
"""
from utils import load_config
config = load_config()
auto_restore_disabled = config.get('disable_auto_restore_hosts', False)
self.auto_restore_disabled = auto_restore_disabled
return auto_restore_disabled
def check_and_clean_all_entries(self, force_clean=False):
"""检查并清理所有由本应用程序添加的hosts记录 """检查并清理所有由本应用程序添加的hosts记录
Args:
force_clean: 是否强制清理,即使禁用了自动还原
Returns: Returns:
bool: 清理是否成功 bool: 清理是否成功
""" """
# 如果禁用了自动还原,且不是强制清理,则不执行清理操作
if self.is_auto_restore_disabled() and not force_clean:
logger.info("已禁用自动还原hosts跳过清理操作")
return True
if not AdminPrivileges().is_admin(): if not AdminPrivileges().is_admin():
print("需要管理员权限来检查和清理hosts文件。") logger.warning("需要管理员权限来检查和清理hosts文件。")
return False return False
try: try:
@@ -408,21 +788,26 @@ class HostsManager:
# 检查是否有变化 # 检查是否有变化
if len(new_lines) == len(lines): if len(new_lines) == len(lines):
print("Hosts文件中没有找到由本应用添加的记录") logger.info("Hosts文件中没有找到由本应用添加的记录")
return True return True
# 写回清理后的内容 # 写回清理后的内容
with open(self.hosts_path, 'w', encoding='utf-8') as f: with open(self.hosts_path, 'w', encoding='utf-8') as f:
f.write('\n'.join(new_lines)) f.write('\n'.join(new_lines))
print(f"已清理所有由 {APP_NAME} 添加的hosts记录") logger.info(f"已清理所有由 {APP_NAME} 添加的hosts记录")
return True return True
except IOError as e: except IOError as e:
print(f"检查和清理hosts文件失败: {e}") logger.error(f"检查和清理hosts文件失败: {e}")
return False return False
def restore(self): def restore(self):
# 如果禁用了自动还原,则不执行还原操作
if self.is_auto_restore_disabled():
logger.info("已禁用自动还原hosts跳过还原操作")
return True
if not self.modified: if not self.modified:
if os.path.exists(self.backup_path): if os.path.exists(self.backup_path):
try: try:
@@ -434,7 +819,7 @@ class HostsManager:
return True return True
if not AdminPrivileges().is_admin(): if not AdminPrivileges().is_admin():
print("需要管理员权限来恢复hosts文件。") logger.warning("需要管理员权限来恢复hosts文件。")
return False return False
if self.original_content: if self.original_content:
@@ -442,7 +827,7 @@ class HostsManager:
with open(self.hosts_path, 'w', encoding='utf-8') as f: with open(self.hosts_path, 'w', encoding='utf-8') as f:
f.write(self.original_content) f.write(self.original_content)
self.modified = False self.modified = False
print("Hosts文件已从内存恢复。") logger.info("Hosts文件已从内存恢复。")
if os.path.exists(self.backup_path): if os.path.exists(self.backup_path):
try: try:
os.remove(self.backup_path) os.remove(self.backup_path)
@@ -451,15 +836,15 @@ class HostsManager:
# 恢复后再检查一次是否有残留 # 恢复后再检查一次是否有残留
self.check_and_clean_all_entries() self.check_and_clean_all_entries()
return True return True
except IOError as e: except (IOError, OSError) as e:
print(f"从内存恢复hosts文件失败: {e}") logger.error(f"从内存恢复hosts文件失败: {e}")
return self.restore_from_backup_file() return self.restore_from_backup_file()
else: else:
return self.restore_from_backup_file() return self.restore_from_backup_file()
def restore_from_backup_file(self): def restore_from_backup_file(self):
if not os.path.exists(self.backup_path): if not os.path.exists(self.backup_path):
print("未找到hosts备份文件无法恢复。") logger.warning("未找到hosts备份文件无法恢复。")
# 即使没有备份文件,也尝试清理可能的残留 # 即使没有备份文件,也尝试清理可能的残留
self.check_and_clean_all_entries() self.check_and_clean_all_entries()
return False return False
@@ -470,21 +855,14 @@ class HostsManager:
hf.write(backup_content) hf.write(backup_content)
os.remove(self.backup_path) os.remove(self.backup_path)
self.modified = False self.modified = False
print("Hosts文件已从备份文件恢复。") logger.info("Hosts文件已从备份文件恢复。")
# 恢复后再检查一次是否有残留 # 恢复后再检查一次是否有残留
self.check_and_clean_all_entries() self.check_and_clean_all_entries()
return True return True
except (IOError, OSError) as e: except (IOError, OSError) as e:
print(f"从备份文件恢复hosts失败: {e}") logger.error(f"从备份文件恢复hosts失败: {e}")
msg_box = msgbox_frame(f"警告 - {APP_NAME}", f"\n自动恢复hosts文件失败请手动从 {self.backup_path} 恢复。\n\n【错误信息】:{e}\n", QtWidgets.QMessageBox.StandardButton.Ok) msg_box = msgbox_frame(f"警告 - {APP_NAME}", f"\n自动恢复hosts文件失败请手动从 {self.backup_path} 恢复。\n\n【错误信息】:{e}\n", QtWidgets.QMessageBox.StandardButton.Ok)
msg_box.exec() msg_box.exec()
# 尽管恢复失败,仍然尝试清理可能的残留 # 尽管恢复失败,仍然尝试清理可能的残留
self.check_and_clean_all_entries() self.check_and_clean_all_entries()
return False return False
def censor_url(text):
"""Censors URLs in a given text string."""
if not isinstance(text, str):
text = str(text)
url_pattern = re.compile(r'https?://[^\s/$.?#].[^\s]*')
return url_pattern.sub('***URL HIDDEN***', text)

View File

@@ -1,29 +1,127 @@
from .helpers import censor_url
import logging
import os import os
from data.config import CACHE import logging
import datetime
import sys
import glob
import time
import traceback
from logging.handlers import RotatingFileHandler, TimedRotatingFileHandler
from config.config import LOG_DIR, LOG_FILE, LOG_LEVEL, LOG_MAX_SIZE, LOG_BACKUP_COUNT, LOG_RETENTION_DAYS
from .url_censor import censor_url
class URLCensorFormatter(logging.Formatter):
"""自定义的日志格式化器用于隐藏日志消息中的URL"""
def format(self, record):
# 先使用原始的format方法格式化日志
formatted_message = super().format(record)
# 临时禁用URL隐藏直接返回原始消息
return formatted_message
# 然后对格式化后的消息进行URL审查已禁用
# return censor_url(formatted_message)
class Logger: class Logger:
def __init__(self, filename, stream): def __init__(self, filename, stream):
self.terminal = stream self.terminal = stream
self.log = open(filename, "w", encoding="utf-8") try:
# 确保目录存在
log_dir = os.path.dirname(filename)
if log_dir and not os.path.exists(log_dir):
os.makedirs(log_dir, exist_ok=True)
print(f"已创建日志目录: {log_dir}")
# 以追加模式打开,避免覆盖现有内容
self.log = open(filename, "a", encoding="utf-8", errors="replace")
self.log.write("\n\n--- New logging session started ---\n\n")
except (IOError, OSError) as e:
# 如果打开文件失败,记录错误并使用空的写入操作
print(f"Error opening log file {filename}: {e}")
self.log = None
def write(self, message): def write(self, message):
censored_message = censor_url(message) try:
self.terminal.write(censored_message) # 临时禁用URL隐藏
self.log.write(censored_message) # censored_message = censor_url(message)
self.flush() censored_message = message # 直接使用原始消息
self.terminal.write(censored_message)
if self.log:
self.log.write(censored_message)
self.flush()
except Exception as e:
# 发生错误时记录到控制台
self.terminal.write(f"Error writing to log: {e}\n")
def flush(self): def flush(self):
self.terminal.flush() try:
self.log.flush() self.terminal.flush()
if self.log:
self.log.flush()
except Exception:
pass
def close(self): def close(self):
self.log.close() try:
if self.log:
self.log.write("\n--- Logging session ended ---\n")
self.log.close()
self.log = None
except Exception:
pass
# 增加异常钩子,确保未捕获的异常也会记录到日志文件中
def log_uncaught_exceptions(exc_type, exc_value, exc_traceback):
"""处理未捕获的异常,记录到日志中"""
if issubclass(exc_type, KeyboardInterrupt):
# 对于键盘中断,使用默认处理
sys.__excepthook__(exc_type, exc_value, exc_traceback)
return
# 获取主日志记录器
logger = logging.getLogger('main')
# 格式化异常信息
lines = traceback.format_exception(exc_type, exc_value, exc_traceback)
error_message = '未捕获的异常:\n' + ''.join(lines)
# 记录到日志中
logger.critical(error_message)
# 同时也显示在控制台
sys.__excepthook__(exc_type, exc_value, exc_traceback)
# 设置全局异常处理器
sys.excepthook = log_uncaught_exceptions
def cleanup_old_logs(retention_days=7):
"""清理超过指定天数的旧日志文件
Args:
retention_days: 日志保留天数默认7天
"""
try:
now = time.time()
cutoff = now - (retention_days * 86400) # 86400秒 = 1天
# 获取所有日志文件
log_files = glob.glob(os.path.join(LOG_DIR, "log-*.txt"))
for log_file in log_files:
# 检查文件修改时间
if os.path.getmtime(log_file) < cutoff:
try:
os.remove(log_file)
print(f"已删除过期日志: {log_file}")
except Exception as e:
print(f"删除日志文件失败 {log_file}: {e}")
except Exception as e:
print(f"清理旧日志文件时出错: {e}")
def setup_logger(name): def setup_logger(name):
"""设置并返回一个命名的logger """设置并返回一个命名的logger
使用统一的日志文件,添加日志轮转功能,实现自动清理过期日志
Args: Args:
name: logger的名称 name: logger的名称
@@ -37,28 +135,54 @@ def setup_logger(name):
if logger.hasHandlers(): if logger.hasHandlers():
return logger return logger
logger.setLevel(logging.DEBUG) # 根据配置设置日志级别
log_level = getattr(logging, LOG_LEVEL.upper(), logging.DEBUG)
logger.setLevel(log_level)
# 确保日志目录存在 # 确保日志目录存在
log_dir = os.path.join(CACHE, "logs") os.makedirs(LOG_DIR, exist_ok=True)
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") cleanup_old_logs(LOG_RETENTION_DAYS)
file_handler.setLevel(logging.DEBUG)
# 创建主日志文件的轮转处理器
try:
# 确保主日志文件目录存在
log_file_dir = os.path.dirname(LOG_FILE)
if log_file_dir and not os.path.exists(log_file_dir):
os.makedirs(log_file_dir, exist_ok=True)
print(f"已创建主日志目录: {log_file_dir}")
# 使用RotatingFileHandler实现日志轮转
main_file_handler = RotatingFileHandler(
LOG_FILE,
maxBytes=LOG_MAX_SIZE,
backupCount=LOG_BACKUP_COUNT,
encoding="utf-8"
)
main_file_handler.setLevel(log_level)
except (IOError, OSError) as e:
print(f"无法创建主日志文件处理器: {e}")
main_file_handler = None
# 创建控制台处理器 # 创建控制台处理器
console_handler = logging.StreamHandler() console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO) console_handler.setLevel(logging.INFO) # 控制台只显示INFO以上级别
# 创建格式器并添加到处理器 # 创建更详细的格式器,包括模块名、文件名和行号
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') formatter = URLCensorFormatter('%(asctime)s - %(name)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s')
file_handler.setFormatter(formatter)
# 设置处理器的格式化器
console_handler.setFormatter(formatter) console_handler.setFormatter(formatter)
if main_file_handler:
main_file_handler.setFormatter(formatter)
# 添加处理器到logger # 添加处理器到logger
logger.addHandler(file_handler)
logger.addHandler(console_handler) logger.addHandler(console_handler)
if main_file_handler:
logger.addHandler(main_file_handler)
# 确保异常可以被正确记录
logger.propagate = True
return logger return logger

View File

@@ -0,0 +1,33 @@
import re
def censor_url(text):
"""Censors URLs in a given text string, replacing them with a protection message.
Args:
text: 要处理的文本
Returns:
str: 处理后的文本URL被完全隐藏
"""
# 临时禁用URL隐藏功能直接返回原始文本以便调试
if not isinstance(text, str):
text = str(text)
return text # 直接返回原始文本,不做任何隐藏
# 以下是原始代码,现在被注释掉
r'''
# 匹配URL并替换为固定文本
url_pattern = re.compile(r'https?://[^\s/$.?#].[^\s]*')
censored = url_pattern.sub('***URL protection***', text)
# 额外处理带referer参数的情况
referer_pattern = re.compile(r'--referer\s+(\S+)')
censored = referer_pattern.sub('--referer ***URL protection***', censored)
# 处理Origin头
origin_pattern = re.compile(r'Origin:\s+(\S+)')
censored = origin_pattern.sub('Origin: ***URL protection***', censored)
return censored
'''

View File

@@ -4,6 +4,11 @@ import webbrowser
from PySide6.QtCore import QThread, Signal from PySide6.QtCore import QThread, Signal
from PySide6.QtWidgets import QMessageBox from PySide6.QtWidgets import QMessageBox
import sys import sys
from utils.logger import setup_logger
from utils.url_censor import censor_url
# 初始化logger
logger = setup_logger("config_fetch")
class ConfigFetchThread(QThread): class ConfigFetchThread(QThread):
finished = Signal(object, str) # data, error_message finished = Signal(object, str) # data, error_message
@@ -17,16 +22,21 @@ class ConfigFetchThread(QThread):
def run(self): def run(self):
try: try:
if self.debug_mode: if self.debug_mode:
print("--- Starting to fetch cloud config ---") logger.debug("--- Starting to fetch cloud config ---")
print(f"DEBUG: Requesting URL: {self.url}") # 完全隐藏URL
print(f"DEBUG: Using Headers: {self.headers}") logger.debug(f"DEBUG: Requesting URL: ***URL protection***")
logger.debug(f"DEBUG: Using Headers: {self.headers}")
response = requests.get(self.url, headers=self.headers, timeout=10) response = requests.get(self.url, headers=self.headers, timeout=10)
if self.debug_mode: if self.debug_mode:
print(f"DEBUG: Response Status Code: {response.status_code}") logger.debug(f"DEBUG: Response Status Code: {response.status_code}")
print(f"DEBUG: Response Headers: {response.headers}") logger.debug(f"DEBUG: Response Headers: {response.headers}")
print(f"DEBUG: Response Text: {response.text}")
# 记录实际响应内容但隐藏URL等敏感信息临时禁用
# censored_text = censor_url(response.text)
censored_text = response.text # 直接使用原始文本
logger.debug(f"DEBUG: Response Text: {censored_text}")
response.raise_for_status() response.raise_for_status()
@@ -62,4 +72,28 @@ class ConfigFetchThread(QThread):
self.finished.emit(None, error_msg) self.finished.emit(None, error_msg)
finally: finally:
if self.debug_mode: if self.debug_mode:
print("--- Finished fetching cloud config ---") logger.debug("--- Finished fetching cloud config ---")
def _create_safe_config_for_logging(self, config_data):
"""创建用于日志记录的安全配置副本隐藏敏感URL
Args:
config_data: 原始配置数据
Returns:
dict: 安全的配置数据副本
"""
if not config_data or not isinstance(config_data, dict):
return config_data
# 创建深拷贝,避免修改原始数据
import copy
safe_config = copy.deepcopy(config_data)
# 隐藏敏感URL
for key in safe_config:
if isinstance(safe_config[key], dict) and "url" in safe_config[key]:
# 完全隐藏URL
safe_config[key]["url"] = "***URL protection***"
return safe_config

View File

@@ -7,7 +7,7 @@ from PySide6 import QtCore, QtWidgets
from PySide6.QtCore import (Qt, Signal, QThread, QTimer) from PySide6.QtCore import (Qt, Signal, QThread, QTimer)
from PySide6.QtWidgets import (QLabel, QProgressBar, QVBoxLayout, QDialog, QHBoxLayout) from PySide6.QtWidgets import (QLabel, QProgressBar, QVBoxLayout, QDialog, QHBoxLayout)
from utils import resource_path from utils import resource_path
from data.config import APP_NAME, UA from config.config import APP_NAME, UA
import signal import signal
import ctypes import ctypes
import time import time
@@ -156,7 +156,7 @@ class DownloadThread(QThread):
] ]
# 获取主窗口的下载管理器对象 # 获取主窗口的下载管理器对象
thread_count = 64 # 默认值 thread_count = 64 # 默认值
if hasattr(self.parent(), 'download_manager'): if hasattr(self.parent(), 'download_manager'):
# 从下载管理器获取线程数设置 # 从下载管理器获取线程数设置
thread_count = self.parent().download_manager.get_download_thread_count() thread_count = self.parent().download_manager.get_download_thread_count()
@@ -196,11 +196,11 @@ class DownloadThread(QThread):
'--auto-file-renaming=false', '--auto-file-renaming=false',
'--allow-overwrite=true', '--allow-overwrite=true',
'--split=128', '--split=128',
f'--max-connection-per-server={thread_count}', # 使用动态的线程数 f'--max-connection-per-server={thread_count}', # 使用动态的线程数
'--min-split-size=1M', # 减小最小分片大小 '--min-split-size=1M', # 减小最小分片大小
'--optimize-concurrent-downloads=true', # 优化并发下载 '--optimize-concurrent-downloads=true', # 优化并发下载
'--file-allocation=none', # 禁用文件预分配加快开始 '--file-allocation=none', # 禁用文件预分配加快开始
'--async-dns=true', # 使用异步DNS '--async-dns=true', # 使用异步DNS
]) ])
# 根据IPv6设置决定是否禁用IPv6 # 根据IPv6设置决定是否禁用IPv6
@@ -222,7 +222,7 @@ class DownloadThread(QThread):
self.process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, encoding='utf-8', errors='replace', creationflags=creation_flags) self.process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, encoding='utf-8', errors='replace', creationflags=creation_flags)
# 正则表达式用于解析aria2c的输出 # 正则表达式用于解析aria2c的输出
# 例如: #1 GID[...]( 5%) CN:1 DL:10.5MiB/s ETA:1m30s # 例如: #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\]]+)')
# 添加限流计时器防止更新过于频繁导致UI卡顿 # 添加限流计时器防止更新过于频繁导致UI卡顿
@@ -264,7 +264,8 @@ class DownloadThread(QThread):
return_code = self.process.wait() return_code = self.process.wait()
if not self._is_running: # 如果是手动停止的 if not self._is_running:
# 如果是手动停止的
self.finished.emit(False, "下载已手动停止。") self.finished.emit(False, "下载已手动停止。")
return return
@@ -323,7 +324,6 @@ class ProgressWindow(QDialog):
# 设置暂停/恢复状态 # 设置暂停/恢复状态
self.is_paused = False self.is_paused = False
# 添加最后进度记录用于优化UI更新 # 添加最后进度记录用于优化UI更新
self._last_percent = -1 self._last_percent = -1

View File

@@ -1,31 +1,353 @@
import os import os
import shutil import shutil
import py7zr import py7zr
import tempfile
import traceback
from PySide6.QtCore import QThread, Signal from PySide6.QtCore import QThread, Signal
from data.config import PLUGIN, GAME_INFO from config.config import PLUGIN, GAME_INFO
import time # 用于时间计算
import threading
import queue
from concurrent.futures import TimeoutError
class ExtractionThread(QThread): class ExtractionThread(QThread):
finished = Signal(bool, str, str) # success, error_message, game_version finished = Signal(bool, str, str) # success, error_message, game_version
progress = Signal(int, str) # 添加进度信号,传递进度百分比和状态信息
def __init__(self, _7z_path, game_folder, plugin_path, game_version, parent=None): def __init__(self, _7z_path, game_folder, plugin_path, game_version, parent=None, extracted_path=None):
super().__init__(parent) super().__init__(parent)
self._7z_path = _7z_path self._7z_path = _7z_path
self.game_folder = game_folder self.game_folder = game_folder
self.plugin_path = plugin_path self.plugin_path = plugin_path
self.game_version = game_version self.game_version = game_version
self.extracted_path = extracted_path # 添加已解压文件路径参数
def run(self): def run(self):
try: try:
with py7zr.SevenZipFile(self._7z_path, mode="r") as archive: # 确保游戏目录存在
archive.extractall(path=PLUGIN)
os.makedirs(self.game_folder, exist_ok=True) os.makedirs(self.game_folder, exist_ok=True)
shutil.copy(self.plugin_path, self.game_folder)
if self.game_version == "NEKOPARA After": def update_progress(percent: int, message: str):
sig_path = os.path.join(PLUGIN, GAME_INFO[self.game_version]["sig_path"]) try:
shutil.copy(sig_path, self.game_folder) self.progress.emit(percent, message)
except Exception:
pass
self.finished.emit(True, "", self.game_version) # 记录调试信息
from utils.logger import setup_logger
debug_logger = setup_logger("extraction_thread")
debug_logger.info(f"====== 开始处理 {self.game_version} 补丁文件 ======")
debug_logger.info(f"压缩包路径: {self._7z_path}")
debug_logger.info(f"游戏目录: {self.game_folder}")
debug_logger.info(f"插件路径: {self.plugin_path}")
update_progress(0, f"开始处理 {self.game_version} 的补丁文件...")
# 支持外部请求中断
if self.isInterruptionRequested():
self.finished.emit(False, "操作已取消", self.game_version)
return
# 如果提供了已解压文件路径,直接使用它
if self.extracted_path and os.path.exists(self.extracted_path):
update_progress(20, f"正在复制 {self.game_version} 的补丁文件...\n(在此过程中可能会卡顿或无响应,请不要关闭软件)")
# 直接复制已解压的文件到游戏目录
target_file = os.path.join(self.game_folder, os.path.basename(self.plugin_path))
shutil.copy(self.extracted_path, target_file)
update_progress(60, f"正在完成 {self.game_version} 的补丁安装...")
# 对于NEKOPARA After还需要复制签名文件
if self.game_version == "NEKOPARA After":
try:
update_progress(70, f"正在处理 {self.game_version} 的签名文件...")
# 从已解压文件的目录中获取签名文件
extracted_dir = os.path.dirname(self.extracted_path)
sig_filename = os.path.basename(GAME_INFO[self.game_version]["sig_path"])
sig_path = os.path.join(extracted_dir, sig_filename)
# 尝试多种可能的签名文件路径
if not os.path.exists(sig_path):
# 尝试在同级目录查找
sig_path = os.path.join(os.path.dirname(extracted_dir), sig_filename)
# 如果签名文件存在,则复制它
if os.path.exists(sig_path):
target_sig = os.path.join(self.game_folder, sig_filename)
shutil.copy(sig_path, target_sig)
update_progress(80, f"签名文件复制完成")
else:
# 如果签名文件不存在,则使用原始路径
sig_path = os.path.join(PLUGIN, GAME_INFO[self.game_version]["sig_path"])
if os.path.exists(sig_path):
target_sig = os.path.join(self.game_folder, os.path.basename(sig_path))
shutil.copy(sig_path, target_sig)
update_progress(80, f"使用内置签名文件完成")
else:
update_progress(80, f"未找到签名文件,继续安装主补丁文件")
except Exception as sig_err:
# 签名文件处理失败时记录错误但不中断主流程
update_progress(80, f"签名文件处理失败: {str(sig_err)}")
update_progress(100, f"{self.game_version} 补丁文件处理完成")
self.finished.emit(True, "", self.game_version)
return
# 否则解压源压缩包到临时目录,再复制目标文件
update_progress(10, f"正在打开 {self.game_version} 的补丁压缩包...")
with py7zr.SevenZipFile(self._7z_path, mode="r") as archive:
# 获取压缩包内的文件列表
file_list = archive.getnames()
# 详细记录压缩包中的所有文件
debug_logger.debug(f"压缩包内容分析:")
debug_logger.debug(f"- 文件总数: {len(file_list)}")
for i, f in enumerate(file_list):
is_folder = f.endswith('/') or f.endswith('\\')
file_type = '文件夹' if is_folder else '文件'
debug_logger.debug(f" {i+1}. {f} - 类型: {file_type}")
update_progress(20, f"正在分析 {self.game_version} 的补丁文件...")
update_progress(30, f"正在解压 {self.game_version} 的补丁文件...\n(在此过程中可能会卡顿或无响应,请不要关闭软件)")
with tempfile.TemporaryDirectory() as temp_dir:
# 查找主补丁文件和签名文件
target_filename = os.path.basename(self.plugin_path)
# 只有NEKOPARA After版本才需要查找签名文件
if self.game_version == "NEKOPARA After":
sig_filename = target_filename + ".sig" # 签名文件名
debug_logger.debug(f"查找主补丁文件: {target_filename}")
debug_logger.debug(f"查找签名文件: {sig_filename}")
else:
sig_filename = None
debug_logger.debug(f"查找主补丁文件: {target_filename}")
debug_logger.debug(f"{self.game_version} 不需要签名文件")
target_file_in_archive = None
sig_file_in_archive = None
# 对于NEKOPARA After增加特殊处理
if self.game_version == "NEKOPARA After":
# 增加专门的检查,同时识别主补丁和签名文件
debug_logger.debug("执行NEKOPARA After特殊补丁文件识别")
# 查找主补丁和签名文件
for file_path in file_list:
basename = os.path.basename(file_path)
# 查找主补丁文件
if basename == "afteradult.xp3" and not basename.endswith('.sig'):
target_file_in_archive = file_path
debug_logger.debug(f"找到精确匹配的After主补丁文件: {target_file_in_archive}")
# 查找签名文件
elif basename == "afteradult.xp3.sig" or basename.endswith('.sig'):
sig_file_in_archive = file_path
debug_logger.debug(f"找到After签名文件: {sig_file_in_archive}")
# 如果没找到主补丁文件,寻找可能的替代文件
if not target_file_in_archive:
for file_path in file_list:
if "afteradult.xp3" in file_path and not file_path.endswith('.sig'):
target_file_in_archive = file_path
debug_logger.debug(f"找到备选After主补丁文件: {target_file_in_archive}")
break
else:
# 标准处理逻辑
for file_path in file_list:
basename = os.path.basename(file_path)
# 查找主补丁文件
if basename == target_filename and not basename.endswith('.sig'):
target_file_in_archive = file_path
debug_logger.debug(f"在压缩包中找到主补丁文件: {target_file_in_archive}")
# 查找签名文件
elif basename == sig_filename:
sig_file_in_archive = file_path
debug_logger.debug(f"在压缩包中找到签名文件: {sig_file_in_archive}")
# 如果没有找到精确匹配的主补丁文件,使用更宽松的搜索
if not target_file_in_archive:
debug_logger.warning(f"没有找到精确匹配的主补丁文件,尝试更宽松的搜索")
for file_path in file_list:
if target_filename in file_path and not file_path.endswith('.sig'):
target_file_in_archive = file_path
debug_logger.info(f"在压缩包中找到可能的主补丁文件: {target_file_in_archive}")
break
# 如果找不到主补丁文件,使用回退方案:提取全部内容
if not target_file_in_archive:
debug_logger.warning(f"未能识别正确的主补丁文件,将提取所有文件并尝试查找")
# 提取所有文件到临时目录
update_progress(30, f"正在解压所有文件...")
archive.extractall(path=temp_dir)
debug_logger.debug(f"已提取所有文件到临时目录")
# 在提取的文件中查找主补丁文件和签名文件
found_main = False
found_sig = False
for root, dirs, files in os.walk(temp_dir):
for file in files:
# 查找主补丁文件
if file == target_filename and not file.endswith('.sig'):
extracted_file_path = os.path.join(root, file)
file_size = os.path.getsize(extracted_file_path)
debug_logger.debug(f"在提取的文件中找到主补丁文件: {extracted_file_path}, 大小: {file_size} 字节")
# 复制到目标位置
target_path = os.path.join(self.game_folder, target_filename)
shutil.copy2(extracted_file_path, target_path)
debug_logger.debug(f"已复制主补丁文件到: {target_path}")
found_main = True
# 查找签名文件
elif file == sig_filename or file.endswith('.sig'):
extracted_sig_path = os.path.join(root, file)
sig_size = os.path.getsize(extracted_sig_path)
debug_logger.debug(f"在提取的文件中找到签名文件: {extracted_sig_path}, 大小: {sig_size} 字节")
# 复制到目标位置
sig_target = os.path.join(self.game_folder, sig_filename)
shutil.copy2(extracted_sig_path, sig_target)
debug_logger.debug(f"已复制签名文件到: {sig_target}")
found_sig = True
# 如果两个文件都找到,可以停止遍历
if found_main and found_sig:
debug_logger.debug("已找到所有需要的文件,停止遍历")
break
if found_main and found_sig:
break
if not found_main:
debug_logger.error(f"无法找到主补丁文件,安装失败")
raise FileNotFoundError(f"在压缩包中未找到主补丁文件 {target_filename}")
# 只有NEKOPARA After版本才需要处理签名文件
if self.game_version == "NEKOPARA After":
# 签名文件没找到不影响主流程,但记录警告
if not found_sig:
debug_logger.warning(f"未找到签名文件 {sig_filename},但继续安装主补丁文件")
else:
debug_logger.info(f"{self.game_version} 不需要签名文件,跳过签名文件处理")
else:
# 准备要解压的文件列表
files_to_extract = [target_file_in_archive]
# 只有NEKOPARA After版本才需要解压签名文件
if self.game_version == "NEKOPARA After" and sig_file_in_archive:
files_to_extract.append(sig_file_in_archive)
debug_logger.debug(f"将同时解压主补丁文件和签名文件: {files_to_extract}")
else:
debug_logger.debug(f"将仅解压主补丁文件: {files_to_extract}")
# 解压选定的文件到临时目录
debug_logger.debug(f"开始解压选定文件到临时目录: {temp_dir}")
# 设置解压超时时间(秒)
extract_timeout = 180 # 3分钟超时
debug_logger.debug(f"设置解压超时: {extract_timeout}")
# 创建子线程执行解压
import threading
import queue
extract_result = queue.Queue()
def extract_files():
try:
archive.extract(path=temp_dir, targets=files_to_extract)
extract_result.put(("success", None))
except Exception as e:
extract_result.put(("error", e))
extract_thread = threading.Thread(target=extract_files)
extract_thread.daemon = True
extract_thread.start()
# 每5秒更新一次进度最多等待设定的超时时间
total_waited = 0
while extract_thread.is_alive() and total_waited < extract_timeout:
update_progress(30 + int(30 * total_waited / extract_timeout),
f"正在解压文件...已等待{total_waited}")
extract_thread.join(5) # 等待5秒
total_waited += 5
# 检查是否超时
if extract_thread.is_alive():
debug_logger.error(f"解压超时(超过{extract_timeout}秒)")
raise TimeoutError(f"解压超时(超过{extract_timeout}秒),请检查补丁文件是否完整")
# 检查解压结果
if not extract_result.empty():
status, error = extract_result.get()
if status == "error":
debug_logger.error(f"解压错误: {error}")
raise error
debug_logger.debug(f"文件解压完成")
update_progress(60, f"正在复制 {self.game_version} 的补丁文件...")
# 复制主补丁文件到游戏目录
extracted_file_path = os.path.join(temp_dir, target_file_in_archive)
# 检查解压后的文件是否存在及其大小
if os.path.exists(extracted_file_path):
file_size = os.path.getsize(extracted_file_path)
debug_logger.debug(f"解压后的主补丁文件存在: {extracted_file_path}, 大小: {file_size} 字节")
else:
debug_logger.error(f"解压后的主补丁文件不存在: {extracted_file_path}")
raise FileNotFoundError(f"解压后的文件不存在: {extracted_file_path}")
# 构建目标路径并复制
target_path = os.path.join(self.game_folder, target_filename)
debug_logger.debug(f"复制主补丁文件: {extracted_file_path}{target_path}")
shutil.copy2(extracted_file_path, target_path)
# 验证主补丁文件是否成功复制
if os.path.exists(target_path):
target_size = os.path.getsize(target_path)
debug_logger.debug(f"主补丁文件成功复制: {target_path}, 大小: {target_size} 字节")
else:
debug_logger.error(f"主补丁文件复制失败: {target_path}")
raise FileNotFoundError(f"目标文件复制失败: {target_path}")
# 只有NEKOPARA After版本才需要处理签名文件
if self.game_version == "NEKOPARA After":
# 如果有找到签名文件,也复制它
if sig_file_in_archive:
update_progress(80, f"正在复制签名文件...")
extracted_sig_path = os.path.join(temp_dir, sig_file_in_archive)
if os.path.exists(extracted_sig_path):
sig_size = os.path.getsize(extracted_sig_path)
debug_logger.debug(f"解压后的签名文件存在: {extracted_sig_path}, 大小: {sig_size} 字节")
# 复制签名文件到游戏目录
sig_target = os.path.join(self.game_folder, sig_filename)
shutil.copy2(extracted_sig_path, sig_target)
debug_logger.debug(f"签名文件成功复制: {sig_target}")
else:
debug_logger.warning(f"解压后的签名文件不存在: {extracted_sig_path}")
else:
debug_logger.warning(f"压缩包中没有找到签名文件,但继续安装主补丁文件")
else:
debug_logger.info(f"{self.game_version} 不需要签名文件,跳过签名文件处理")
update_progress(100, f"{self.game_version} 补丁文件解压完成")
self.finished.emit(True, "", self.game_version)
except (py7zr.Bad7zFile, FileNotFoundError, Exception) as e: except (py7zr.Bad7zFile, FileNotFoundError, Exception) as e:
try:
self.progress.emit(100, f"处理 {self.game_version} 的补丁文件失败")
except Exception:
pass
self.finished.emit(False, f"\n文件操作失败,请重试\n\n【错误信息】:{e}\n", self.game_version) self.finished.emit(False, f"\n文件操作失败,请重试\n\n【错误信息】:{e}\n", self.game_version)

View File

@@ -1,28 +1,545 @@
import os
import hashlib
import py7zr
import tempfile
import traceback
import time # Added for time.time()
from PySide6.QtCore import QThread, Signal from PySide6.QtCore import QThread, Signal
from utils import HashManager from PySide6.QtWidgets import QApplication
from data.config import BLOCK_SIZE from utils.logger import setup_logger
# 初始化logger
logger = setup_logger("hash_thread")
class HashThread(QThread): class HashThread(QThread):
pre_finished = Signal(dict) pre_finished = Signal(dict)
after_finished = Signal(dict) after_finished = Signal(dict)
def __init__(self, mode, install_paths, plugin_hash, installed_status, parent=None): def __init__(self, mode, install_paths, plugin_hash, installed_status, main_window=None):
super().__init__(parent) """初始化哈希检查线程
Args:
mode: 检查模式,"pre""after"
install_paths: 安装路径字典
plugin_hash: 插件哈希值字典
installed_status: 安装状态字典
main_window: 主窗口实例用于访问UI和状态
"""
super().__init__()
self.mode = mode self.mode = mode
self.install_paths = install_paths self.install_paths = install_paths
self.plugin_hash = plugin_hash self.plugin_hash = plugin_hash
self.installed_status = installed_status self.installed_status = installed_status.copy()
# 每个线程都应该有自己的HashManager实例 self.main_window = main_window
self.hash_manager = HashManager(BLOCK_SIZE)
def run(self): def run(self):
"""运行线程"""
debug_mode = False
# 设置超时限制(分钟)
timeout_minutes = 10
max_execution_time = timeout_minutes * 60 # 转换为秒
start_execution_time = time.time()
# 尝试检测是否处于调试模式
if self.main_window and hasattr(self.main_window, 'debug_manager'):
debug_mode = self.main_window.debug_manager._is_debug_mode()
if debug_mode:
logger.debug(f"DEBUG: 设置哈希计算超时时间: {timeout_minutes} 分钟")
# 在各个关键步骤添加超时检测
def check_timeout():
elapsed = time.time() - start_execution_time
if elapsed > max_execution_time:
if debug_mode:
logger.error(f"DEBUG: 哈希计算超时,已执行 {elapsed:.1f} 秒,超过限制的 {max_execution_time}")
return True
return False
if self.mode == "pre": if self.mode == "pre":
updated_status = self.hash_manager.cfg_pre_hash_compare( status_copy = self.installed_status.copy()
self.install_paths, self.plugin_hash, self.installed_status
) for game_version, install_path in self.install_paths.items():
self.pre_finished.emit(updated_status) if self.isInterruptionRequested():
break
if not os.path.exists(install_path):
status_copy[game_version] = False
if debug_mode:
logger.debug(f"DEBUG: 哈希预检查 - {game_version} 补丁文件不存在: {install_path}")
continue
try:
expected_hash = self.plugin_hash.get(game_version, "")
if not expected_hash:
if debug_mode:
logger.debug(f"DEBUG: 哈希预检查 - {game_version} 没有预期哈希值,跳过哈希检查")
# 当没有预期哈希值时,保持当前状态不变
continue
# 分块读取,避免大文件一次性读取内存
hash_obj = hashlib.sha256()
with open(install_path, "rb") as f:
while True:
if self.isInterruptionRequested():
break
# 检查超时
if check_timeout():
logger.error(f"哈希计算超时,强制终止")
result["passed"] = False
result["game"] = game_version
result["message"] = f"\n{game_version} 哈希计算超时,已超过 {timeout_minutes} 分钟。\n\n请考虑跳过哈希校验或稍后再试。\n"
break
chunk = f.read(1024 * 1024)
if not chunk:
break
hash_obj.update(chunk)
file_hash = hash_obj.hexdigest()
if debug_mode:
logger.debug(f"DEBUG: 哈希预检查 - {game_version}")
logger.debug(f"DEBUG: 文件路径: {install_path}")
logger.debug(f"DEBUG: 预期哈希值: {expected_hash}")
logger.debug(f"DEBUG: 实际哈希值: {file_hash}")
logger.debug(f"DEBUG: 哈希匹配: {file_hash == expected_hash}")
if file_hash == expected_hash:
status_copy[game_version] = True
if debug_mode:
logger.debug(f"DEBUG: 哈希预检查 - {game_version} 哈希匹配成功")
else:
status_copy[game_version] = False
if debug_mode:
logger.debug(f"DEBUG: 哈希预检查 - {game_version} 哈希不匹配")
except Exception as e:
status_copy[game_version] = False
if debug_mode:
logger.debug(f"DEBUG: 哈希预检查异常 - {game_version}: {str(e)}")
self.pre_finished.emit(status_copy)
elif self.mode == "after": elif self.mode == "after":
result = self.hash_manager.cfg_after_hash_compare( result = {"passed": True, "game": "", "message": ""}
self.install_paths, self.plugin_hash, self.installed_status
) for game_version, install_path in self.install_paths.items():
if self.isInterruptionRequested():
break
if not os.path.exists(install_path):
if debug_mode:
logger.debug(f"DEBUG: 哈希后检查 - {game_version} 补丁文件不存在: {install_path}")
continue
# 设置当前处理的游戏版本
result["game"] = game_version
try:
expected_hash = self.plugin_hash.get(game_version, "")
if not expected_hash:
if debug_mode:
logger.debug(f"DEBUG: 哈希后检查 - {game_version} 没有预期哈希值,跳过哈希检查")
# 当没有预期哈希值时,跳过检查
continue
# 检查文件存在和可读性
if not os.path.exists(install_path):
logger.error(f"哈希校验失败 - 文件不存在: {install_path}")
result["passed"] = False
result["game"] = game_version
result["message"] = f"\n{game_version} 安装后的文件不存在,无法校验。\n\n文件路径: {install_path}\n"
break
# 记录文件大小信息
file_size = os.path.getsize(install_path)
logger.info(f"开始校验 {game_version} 补丁文件")
logger.debug(f"文件路径: {install_path}, 文件大小: {file_size} 字节")
# 增加块大小,提高大文件处理性能
# 文件越大块越大最大256MB
chunk_size = min(256 * 1024 * 1024, max(16 * 1024 * 1024, file_size // 20))
logger.debug(f"使用块大小: {chunk_size // (1024 * 1024)}MB")
# 分块读取,避免大文件一次性读取内存
hash_obj = hashlib.sha256()
bytes_read = 0
start_time = time.time()
last_progress_time = start_time
with open(install_path, "rb") as f:
while True:
if self.isInterruptionRequested():
break
# 检查超时
if check_timeout():
logger.error(f"哈希计算超时,强制终止")
result["passed"] = False
result["game"] = game_version
result["message"] = f"\n{game_version} 哈希计算超时,已超过 {timeout_minutes} 分钟。\n\n请考虑跳过哈希校验或稍后再试。\n"
break
chunk = f.read(chunk_size)
if not chunk:
break
bytes_read += len(chunk)
hash_obj.update(chunk)
# 每秒更新一次进度
current_time = time.time()
if current_time - last_progress_time >= 1.0:
progress = bytes_read / file_size * 100
elapsed = current_time - start_time
speed = bytes_read / (elapsed if elapsed > 0 else 1) / (1024 * 1024) # MB/s
logger.debug(f"哈希计算进度: {progress:.1f}% - 已处理: {bytes_read/(1024*1024):.1f}MB/{file_size/(1024*1024):.1f}MB - 速度: {speed:.1f}MB/s")
last_progress_time = current_time
# 计算最终的哈希值
file_hash = hash_obj.hexdigest()
# 记录总用时
total_time = time.time() - start_time
logger.debug(f"哈希计算完成,耗时: {total_time:.1f}秒,平均速度: {file_size/(total_time*1024*1024):.1f}MB/s")
# 记录哈希比较结果
is_valid = file_hash == expected_hash
logger.info(f"{game_version} 哈希校验{'通过' if is_valid else '失败'}")
logger.debug(f"哈希校验详情 - {game_version}:")
logger.debug(f" 文件: {install_path}")
logger.debug(f" 读取字节数: {bytes_read} / {file_size}")
logger.debug(f" 预期哈希: {expected_hash}")
logger.debug(f" 实际哈希: {file_hash}")
if debug_mode:
logger.debug(f"DEBUG: 哈希后检查 - {game_version}")
logger.debug(f"DEBUG: 文件路径: {install_path}")
logger.debug(f"DEBUG: 预期哈希值: {expected_hash}")
logger.debug(f"DEBUG: 实际哈希值: {file_hash}")
logger.debug(f"DEBUG: 哈希匹配: {file_hash == expected_hash}")
if file_hash != expected_hash:
result["passed"] = False
result["game"] = game_version
result["message"] = f"\n{game_version} 安装后的文件校验失败。\n\n文件可能已损坏或被篡改,请重新安装。\n预期哈希: {expected_hash[:10]}...\n实际哈希: {file_hash[:10]}...\n"
if debug_mode:
logger.debug(f"DEBUG: 哈希后检查 - {game_version} 哈希不匹配")
break
elif debug_mode:
logger.debug(f"DEBUG: 哈希后检查 - {game_version} 哈希匹配成功")
except Exception as e:
result["passed"] = False
result["game"] = game_version
result["message"] = f"\n{game_version} 安装后的文件校验过程中发生错误。\n\n错误信息: {str(e)}\n"
if debug_mode:
logger.debug(f"DEBUG: 哈希后检查异常 - {game_version}: {str(e)}")
break
self.after_finished.emit(result) self.after_finished.emit(result)
class OfflineHashVerifyThread(QThread):
"""离线模式下验证补丁文件哈希的线程,支持进度更新"""
progress = Signal(int) # 进度信号0-100
finished = Signal(bool, str, str) # 完成信号,(成功/失败, 错误信息, 解压后的补丁文件路径)
def __init__(self, game_version, file_path, plugin_hash, main_window=None):
super().__init__()
self.game_version = game_version
self.file_path = file_path
self.plugin_hash = plugin_hash
self.main_window = main_window
self.extracted_patch_path = None # 添加解压后的补丁文件路径
# 获取预期的哈希值
self.expected_hash = None
# 直接使用完整游戏名称作为键
self.expected_hash = self.plugin_hash.get(game_version, "")
# 设置调试模式标志
self.debug_mode = False
if main_window and hasattr(main_window, 'debug_manager'):
self.debug_mode = main_window.debug_manager._is_debug_mode()
def run(self):
"""运行线程"""
debug_mode = False
# 设置超时限制(分钟)
timeout_minutes = 10
max_execution_time = timeout_minutes * 60 # 转换为秒
start_execution_time = time.time()
# 尝试检测是否处于调试模式
if self.main_window and hasattr(self.main_window, 'debug_manager'):
debug_mode = self.main_window.debug_manager._is_debug_mode()
# 检查超时的函数
def check_timeout():
elapsed = time.time() - start_execution_time
if elapsed > max_execution_time:
if debug_mode:
logger.debug(f"DEBUG: 哈希校验超时,已运行 {elapsed:.1f}")
return True
return False
# 获取预期的哈希值
expected_hash = self.plugin_hash.get(self.game_version, "")
if not expected_hash:
logger.warning(f"DEBUG: 未找到 {self.game_version} 的预期哈希值")
self.progress.emit(100)
self.finished.emit(False, f"未找到 {self.game_version} 的预期哈希值", "")
return
if debug_mode:
logger.debug(f"DEBUG: 开始验证补丁文件: {self.file_path}")
logger.debug(f"DEBUG: 游戏版本: {self.game_version}")
logger.debug(f"DEBUG: 预期哈希值: {expected_hash}")
try:
# 检查文件是否存在
if not os.path.exists(self.file_path):
if debug_mode:
logger.warning(f"DEBUG: 补丁文件不存在: {self.file_path}")
self.progress.emit(100)
self.finished.emit(False, f"补丁文件不存在: {self.file_path}", "")
return
# 检查文件大小
file_size = os.path.getsize(self.file_path)
if debug_mode:
logger.debug(f"DEBUG: 补丁文件大小: {file_size} 字节")
if file_size == 0:
if debug_mode:
logger.warning(f"DEBUG: 补丁文件大小为0无效文件")
self.progress.emit(100)
self.finished.emit(False, "补丁文件大小为0无效文件", "")
return
# 创建临时目录用于解压文件
with tempfile.TemporaryDirectory() as temp_dir:
if debug_mode:
logger.debug(f"DEBUG: 创建临时目录: {temp_dir}")
# 发送进度信号 - 10%
self.progress.emit(10)
# 解压补丁文件
try:
if debug_mode:
logger.debug(f"DEBUG: 开始解压文件: {self.file_path}")
# 确定目标文件名
target_filename = None
if "Vol.1" in self.game_version:
target_filename = "adultsonly.xp3"
elif "Vol.2" in self.game_version:
target_filename = "adultsonly.xp3"
elif "Vol.3" in self.game_version:
target_filename = "update00.int"
elif "Vol.4" in self.game_version:
target_filename = "vol4adult.xp3"
elif "After" in self.game_version:
target_filename = "afteradult.xp3"
if not target_filename:
if debug_mode:
logger.warning(f"DEBUG: 未知的游戏版本: {self.game_version}")
self.progress.emit(100)
self.finished.emit(False, f"未知的游戏版本: {self.game_version}", "")
return
with py7zr.SevenZipFile(self.file_path, mode="r") as archive:
# 获取压缩包内文件列表
file_list = archive.getnames()
if debug_mode:
logger.debug(f"DEBUG: 压缩包内文件列表: {file_list}")
# 查找目标文件
target_file_in_archive = None
for file_path in file_list:
if target_filename in file_path:
target_file_in_archive = file_path
break
if not target_file_in_archive:
if debug_mode:
logger.warning(f"DEBUG: 在压缩包中未找到目标文件: {target_filename}")
# 尝试查找可能的替代文件
alternative_files = []
for file_path in file_list:
if file_path.endswith('.xp3') or file_path.endswith('.int'):
alternative_files.append(file_path)
if alternative_files:
if debug_mode:
logger.debug(f"DEBUG: 找到可能的替代文件: {alternative_files}")
target_file_in_archive = alternative_files[0]
else:
# 如果找不到任何替代文件,解压全部文件
if debug_mode:
logger.debug(f"DEBUG: 未找到任何替代文件,解压全部文件")
archive.extractall(path=temp_dir)
# 尝试在解压后的目录中查找目标文件
for root, dirs, files in os.walk(temp_dir):
for file in files:
if file.endswith('.xp3') or file.endswith('.int'):
patch_file = os.path.join(root, file)
if debug_mode:
logger.debug(f"DEBUG: 找到可能的补丁文件: {patch_file}")
break
if patch_file:
break
if not patch_file:
if debug_mode:
logger.warning(f"DEBUG: 未找到解压后的补丁文件")
self.progress.emit(100)
self.finished.emit(False, "未找到解压后的补丁文件", "")
return
else:
# 只解压目标文件
if debug_mode:
logger.debug(f"DEBUG: 解压目标文件: {target_file_in_archive}")
archive.extract(path=temp_dir, targets=[target_file_in_archive])
patch_file = os.path.join(temp_dir, target_file_in_archive)
# 发送进度信号 - 50%
self.progress.emit(50)
# 如果还没有设置patch_file尝试查找
if not 'patch_file' in locals():
if "Vol.1" in self.game_version:
patch_file = os.path.join(temp_dir, "vol.1", "adultsonly.xp3")
elif "Vol.2" in self.game_version:
patch_file = os.path.join(temp_dir, "vol.2", "adultsonly.xp3")
elif "Vol.3" in self.game_version:
patch_file = os.path.join(temp_dir, "vol.3", "update00.int")
elif "Vol.4" in self.game_version:
patch_file = os.path.join(temp_dir, "vol.4", "vol4adult.xp3")
elif "After" in self.game_version:
patch_file = os.path.join(temp_dir, "after", "afteradult.xp3")
if not os.path.exists(patch_file):
if debug_mode:
logger.warning(f"DEBUG: 未找到解压后的补丁文件: {patch_file}")
# 尝试查找可能的替代文件
alternative_files = []
for root, dirs, files in os.walk(temp_dir):
for file in files:
if file.endswith('.xp3') or file.endswith('.int'):
alternative_files.append(os.path.join(root, file))
if alternative_files:
logger.debug(f"DEBUG: 找到可能的替代文件: {alternative_files}")
patch_file = alternative_files[0]
else:
# 检查解压目录结构
logger.debug(f"DEBUG: 检查解压目录结构:")
for root, dirs, files in os.walk(temp_dir):
logger.debug(f"DEBUG: 目录: {root}")
logger.debug(f"DEBUG: 子目录: {dirs}")
logger.debug(f"DEBUG: 文件: {files}")
if not os.path.exists(patch_file):
self.progress.emit(100)
self.finished.emit(False, f"未找到解压后的补丁文件", "")
return
# 发送进度信号 - 70%
self.progress.emit(70)
if debug_mode:
logger.debug(f"DEBUG: 找到解压后的补丁文件: {patch_file}")
# 计算补丁文件哈希值
try:
# 读取文件内容并计算哈希值,同时更新进度
file_size = os.path.getsize(patch_file)
# 根据文件大小动态调整块大小
# 文件越大块越大最大256MB
chunk_size = min(256 * 1024 * 1024, max(16 * 1024 * 1024, file_size // 20))
if debug_mode:
logger.debug(f"DEBUG: 文件大小: {file_size} 字节, 使用块大小: {chunk_size // (1024 * 1024)}MB")
hash_obj = hashlib.sha256()
with open(patch_file, "rb") as f:
bytes_read = 0
start_time = time.time()
last_progress_time = start_time
while True:
if self.isInterruptionRequested():
break
# 检查超时
if check_timeout():
logger.error(f"哈希计算超时,强制终止")
self.progress.emit(100)
self.finished.emit(
False,
f"{self.game_version} 哈希计算超时,已超过 {timeout_minutes} 分钟。请考虑跳过哈希校验或稍后再试。",
""
)
return
chunk = f.read(chunk_size)
if not chunk:
break
hash_obj.update(chunk)
bytes_read += len(chunk)
# 计算进度 (70-95%)
progress = 70 + int(25 * bytes_read / file_size)
self.progress.emit(min(95, progress))
# 每秒更新一次日志进度
current_time = time.time()
if debug_mode and current_time - last_progress_time >= 1.0:
elapsed = current_time - start_time
speed = bytes_read / (elapsed if elapsed > 0 else 1) / (1024 * 1024) # MB/s
percent = bytes_read / file_size * 100
logger.debug(f"DEBUG: 哈希计算进度 - {percent:.1f}% - 已处理: {bytes_read/(1024*1024):.1f}MB/{file_size/(1024*1024):.1f}MB - 速度: {speed:.1f}MB/s")
last_progress_time = current_time
# 记录总用时
if debug_mode:
total_time = time.time() - start_time
logger.debug(f"DEBUG: 哈希计算完成,耗时: {total_time:.1f}秒,平均速度: {file_size/(total_time*1024*1024):.1f}MB/s")
file_hash = hash_obj.hexdigest()
# 比较哈希值
result = file_hash.lower() == expected_hash.lower()
# 发送进度信号 - 100%
self.progress.emit(100)
if debug_mode:
logger.debug(f"DEBUG: 补丁文件 {patch_file} 哈希值验证: {'成功' if result else '失败'}")
logger.debug(f"DEBUG: 预期哈希值: {expected_hash}")
logger.debug(f"DEBUG: 实际哈希值: {file_hash}")
# 将验证结果和解压后的文件路径传递回去
# 注意:由于使用了临时目录,此路径在函数返回后将不再有效
# 但这里返回的路径只是用于标识验证成功,实际安装时会重新解压
self.finished.emit(result, "" if result else "补丁文件哈希验证失败,文件可能已损坏或被篡改", patch_file if result else "")
except Exception as e:
if debug_mode:
logger.error(f"DEBUG: 计算补丁文件哈希值失败: {e}")
logger.error(f"DEBUG: 错误类型: {type(e).__name__}")
self.progress.emit(100)
self.finished.emit(False, f"计算补丁文件哈希值失败: {str(e)}", "")
except Exception as e:
if debug_mode:
logger.error(f"DEBUG: 解压补丁文件失败: {e}")
logger.error(f"DEBUG: 错误类型: {type(e).__name__}")
logger.error(f"DEBUG: 错误堆栈: {traceback.format_exc()}")
self.progress.emit(100)
self.finished.emit(False, f"解压补丁文件失败: {str(e)}", "")
return
except Exception as e:
if debug_mode:
logger.error(f"DEBUG: 验证补丁哈希值失败: {e}")
logger.error(f"DEBUG: 错误类型: {type(e).__name__}")
logger.error(f"DEBUG: 错误堆栈: {traceback.format_exc()}" )
self.progress.emit(100)
self.finished.emit(False, f"验证补丁哈希值失败: {str(e)}", "")

View File

@@ -7,6 +7,11 @@ from urllib.parse import urlparse
from PySide6.QtCore import QThread, Signal from PySide6.QtCore import QThread, Signal
from utils import resource_path from utils import resource_path
from utils.logger import setup_logger
from utils.url_censor import censor_url
# 初始化logger
logger = setup_logger("ip_optimizer")
class IpOptimizer: class IpOptimizer:
def __init__(self): def __init__(self):
@@ -23,27 +28,46 @@ class IpOptimizer:
最优的 IP 地址字符串,如果找不到则返回 None。 最优的 IP 地址字符串,如果找不到则返回 None。
""" """
try: try:
# 解析URL获取协议和主机名
parsed_url = urlparse(url)
protocol = parsed_url.scheme
hostname = parsed_url.netloc
# 如果是HTTPS可能需要特殊处理
is_https = protocol.lower() == 'https'
logger.info(f"协议: {protocol}, 主机名: {hostname}, 是否HTTPS: {is_https}")
cst_path = resource_path("cfst.exe") cst_path = resource_path("cfst.exe")
if not os.path.exists(cst_path): if not os.path.exists(cst_path):
print(f"错误: cfst.exe 未在资源路径中找到。") logger.error(f"错误: cfst.exe 未在资源路径中找到。")
return None return None
ip_txt_path = resource_path("ip.txt") ip_txt_path = resource_path("ip.txt")
# 正确的参数设置根据cfst帮助文档 # 隐藏敏感URL
safe_url = "***URL protection***"
command = [ command = [
cst_path, cst_path,
"-n", "1000", # 延迟测速线程数 (默认200) "-n", "1000", # 延迟测速线程数
"-p", "1", # 显示结果数量 (默认10个) "-p", "1", # 显示结果数量
"-url", url, # 指定测速地址 "-url", url,
"-f", ip_txt_path, # IP文件 "-f", ip_txt_path,
"-dd", # 禁用下载测速,按延迟排序 "-dd", # 禁用下载测速
"-o"," " # 不写入结果文件 "-o"," " # 不写入结果文件
] ]
# 创建用于显示的安全命令副本
safe_command = command.copy()
for i, arg in enumerate(safe_command):
if arg == url:
safe_command[i] = safe_url
creation_flags = subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0 creation_flags = subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0
print("--- CloudflareSpeedTest 开始执行 ---") logger.info("--- CloudflareSpeedTest 开始执行 ---")
logger.info(f"执行命令: {' '.join(safe_command)}")
self.process = subprocess.Popen( self.process = subprocess.Popen(
command, command,
@@ -57,22 +81,20 @@ class IpOptimizer:
bufsize=0 bufsize=0
) )
# 更新正则表达式以匹配cfst输出中的IP格式
# 匹配格式: IP地址在行首后面跟着一些数字和文本 # 匹配格式: IP地址在行首后面跟着一些数字和文本
ip_pattern = re.compile(r'^(\d+\.\d+\.\d+\.\d+)\s+.*') ip_pattern = re.compile(r'^(\d+\.\d+\.\d+\.\d+)\s+.*')
# 标记是否已经找到结果表头和完成标记
found_header = False found_header = False
found_completion = False found_completion = False
stdout = self.process.stdout stdout = self.process.stdout
if not stdout: if not stdout:
print("错误: 无法获取子进程的输出流。") logger.error("错误: 无法获取子进程的输出流。")
return None return None
optimal_ip = None optimal_ip = None
timeout_counter = 0 timeout_counter = 0
max_timeout = 300 # 增加超时时间5分钟 max_timeout = 300 # 超时时间5分钟
while True: while True:
if self.process.poll() is not None: if self.process.poll() is not None:
@@ -87,50 +109,47 @@ class IpOptimizer:
if not ready or not line: if not ready or not line:
timeout_counter += 1 timeout_counter += 1
if timeout_counter > max_timeout: if timeout_counter > max_timeout:
print("超时: CloudflareSpeedTest 响应超时") logger.warning("超时: CloudflareSpeedTest 响应超时")
break break
time.sleep(1) time.sleep(1)
continue continue
timeout_counter = 0 timeout_counter = 0
cleaned_line = line.strip() # 处理输出行隐藏可能包含的URL
# 临时禁用URL隐藏
# cleaned_line = censor_url(line.strip())
cleaned_line = line.strip() # 直接使用原始输出
if cleaned_line: if cleaned_line:
print(cleaned_line) logger.debug(cleaned_line)
# 检测结果表头
if "IP 地址" in cleaned_line and "平均延迟" in cleaned_line: if "IP 地址" in cleaned_line and "平均延迟" in cleaned_line:
print("检测到IP结果表头准备获取IP地址...") logger.info("检测到IP结果表头准备获取IP地址...")
found_header = True found_header = True
continue continue
# 检测完成标记
if "完整测速结果已写入" in cleaned_line or "按下 回车键 或 Ctrl+C 退出" in cleaned_line: if "完整测速结果已写入" in cleaned_line or "按下 回车键 或 Ctrl+C 退出" in cleaned_line:
print("检测到测速完成信息") logger.info("检测到测速完成信息")
found_completion = True found_completion = True
# 如果已经找到了IP可以退出了
if optimal_ip: if optimal_ip:
break break
# 已找到表头后尝试匹配IP地址行
if found_header: if found_header:
match = ip_pattern.search(cleaned_line) match = ip_pattern.search(cleaned_line)
if match and not optimal_ip: # 只保存第一个匹配的IP最优IP if match and not optimal_ip:
optimal_ip = match.group(1) optimal_ip = match.group(1)
print(f"找到最优 IP: {optimal_ip}") logger.info(f"找到最优 IP: {optimal_ip}")
# 找到最优IP后立即退出循环不等待完成标记
break break
except Exception as e: except Exception as e:
print(f"读取输出时发生错误: {e}") logger.error(f"读取输出时发生错误: {e}")
break break
# 确保完全读取输出后再发送退出信号
if self.process and self.process.poll() is None: if self.process and self.process.poll() is None:
try: try:
if self.process.stdin and not self.process.stdin.closed: if self.process.stdin and not self.process.stdin.closed:
print("发送退出信号...") logger.debug("发送退出信号...")
self.process.stdin.write('\n') self.process.stdin.write('\n')
self.process.stdin.flush() self.process.stdin.flush()
except: except:
@@ -138,11 +157,11 @@ class IpOptimizer:
self.stop() self.stop()
print("--- CloudflareSpeedTest 执行结束 ---") logger.info("--- CloudflareSpeedTest 执行结束 ---")
return optimal_ip return optimal_ip
except Exception as e: except Exception as e:
print(f"执行 CloudflareSpeedTest 时发生错误: {e}") logger.error(f"执行 CloudflareSpeedTest 时发生错误: {e}")
return None return None
def get_optimal_ipv6(self, url: str) -> str | None: def get_optimal_ipv6(self, url: str) -> str | None:
@@ -156,30 +175,49 @@ class IpOptimizer:
最优的 IPv6 地址字符串,如果找不到则返回 None。 最优的 IPv6 地址字符串,如果找不到则返回 None。
""" """
try: try:
# 解析URL获取协议和主机名
parsed_url = urlparse(url)
protocol = parsed_url.scheme
hostname = parsed_url.netloc
# 如果是HTTPS可能需要特殊处理
is_https = protocol.lower() == 'https'
logger.info(f"IPv6优选 - 协议: {protocol}, 主机名: {hostname}, 是否HTTPS: {is_https}")
cst_path = resource_path("cfst.exe") cst_path = resource_path("cfst.exe")
if not os.path.exists(cst_path): if not os.path.exists(cst_path):
print(f"错误: cfst.exe 未在资源路径中找到。") logger.error(f"错误: cfst.exe 未在资源路径中找到。")
return None return None
ipv6_txt_path = resource_path("data/ipv6.txt") ipv6_txt_path = resource_path("data/ipv6.txt")
if not os.path.exists(ipv6_txt_path): if not os.path.exists(ipv6_txt_path):
print(f"错误: ipv6.txt 未在资源路径中找到。") logger.error(f"错误: ipv6.txt 未在资源路径中找到。")
return None return None
# 正确的参数设置根据cfst帮助文档 # 隐藏敏感URL
safe_url = "***URL protection***"
command = [ command = [
cst_path, cst_path,
"-n", "1000", # 延迟测速线程数IPv6测试线程稍少 "-n", "1000", # 延迟测速线程数
"-p", "1", # 显示结果数量 (默认10个) "-p", "1", # 显示结果数量
"-url", url, # 指定测速地址 "-url", url,
"-f", ipv6_txt_path, # IPv6文件 "-f", ipv6_txt_path,
"-dd", # 禁用下载测速,按延迟排序 "-dd", # 禁用下载测速
"-o", " " # 不写入结果文件 "-o", " " # 不写入结果文件
] ]
# 创建用于显示的安全命令副本
safe_command = command.copy()
for i, arg in enumerate(safe_command):
if arg == url:
safe_command[i] = safe_url
creation_flags = subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0 creation_flags = subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0
print("--- CloudflareSpeedTest IPv6 开始执行 ---") logger.info("--- CloudflareSpeedTest IPv6 开始执行 ---")
logger.info(f"执行命令: {' '.join(safe_command)}")
self.process = subprocess.Popen( self.process = subprocess.Popen(
command, command,
@@ -193,22 +231,20 @@ class IpOptimizer:
bufsize=0 bufsize=0
) )
# 更新正则表达式以匹配cfst输出中的IPv6格式 # IPv6格式可能有多种表示形
# IPv6格式更加复杂可能有多种表示形式
ipv6_pattern = re.compile(r'^([0-9a-fA-F:]+)\s+.*') ipv6_pattern = re.compile(r'^([0-9a-fA-F:]+)\s+.*')
# 标记是否已经找到结果表头和完成标记
found_header = False found_header = False
found_completion = False found_completion = False
stdout = self.process.stdout stdout = self.process.stdout
if not stdout: if not stdout:
print("错误: 无法获取子进程的输出流。") logger.error("错误: 无法获取子进程的输出流。")
return None return None
optimal_ipv6 = None optimal_ipv6 = None
timeout_counter = 0 timeout_counter = 0
max_timeout = 300 # 增加超时时间5分钟 max_timeout = 300 # 超时时间5分钟
while True: while True:
if self.process.poll() is not None: if self.process.poll() is not None:
@@ -223,50 +259,47 @@ class IpOptimizer:
if not ready or not line: if not ready or not line:
timeout_counter += 1 timeout_counter += 1
if timeout_counter > max_timeout: if timeout_counter > max_timeout:
print("超时: CloudflareSpeedTest IPv6 响应超时") logger.warning("超时: CloudflareSpeedTest IPv6 响应超时")
break break
time.sleep(1) time.sleep(1)
continue continue
timeout_counter = 0 timeout_counter = 0
cleaned_line = line.strip() # 处理输出行隐藏可能包含的URL
# 临时禁用URL隐藏
# cleaned_line = censor_url(line.strip())
cleaned_line = line.strip() # 直接使用原始输出
if cleaned_line: if cleaned_line:
print(cleaned_line) logger.debug(cleaned_line)
# 检测结果表头
if "IP 地址" in cleaned_line and "平均延迟" in cleaned_line: if "IP 地址" in cleaned_line and "平均延迟" in cleaned_line:
print("检测到IPv6结果表头准备获取IPv6地址...") logger.info("检测到IPv6结果表头准备获取IPv6地址...")
found_header = True found_header = True
continue continue
# 检测完成标记
if "完整测速结果已写入" in cleaned_line or "按下 回车键 或 Ctrl+C 退出" in cleaned_line: if "完整测速结果已写入" in cleaned_line or "按下 回车键 或 Ctrl+C 退出" in cleaned_line:
print("检测到IPv6测速完成信息") logger.info("检测到IPv6测速完成信息")
found_completion = True found_completion = True
# 如果已经找到了IPv6可以退出了
if optimal_ipv6: if optimal_ipv6:
break break
# 已找到表头后尝试匹配IPv6地址行
if found_header: if found_header:
match = ipv6_pattern.search(cleaned_line) match = ipv6_pattern.search(cleaned_line)
if match and not optimal_ipv6: # 只保存第一个匹配的IPv6最优IPv6 if match and not optimal_ipv6:
optimal_ipv6 = match.group(1) optimal_ipv6 = match.group(1)
print(f"找到最优 IPv6: {optimal_ipv6}") logger.info(f"找到最优 IPv6: {optimal_ipv6}")
# 找到最优IPv6后立即退出循环不等待完成标记
break break
except Exception as e: except Exception as e:
print(f"读取输出时发生错误: {e}") logger.error(f"读取输出时发生错误: {e}")
break break
# 确保完全读取输出后再发送退出信号
if self.process and self.process.poll() is None: if self.process and self.process.poll() is None:
try: try:
if self.process.stdin and not self.process.stdin.closed: if self.process.stdin and not self.process.stdin.closed:
print("发送退出信号...") logger.debug("发送退出信号...")
self.process.stdin.write('\n') self.process.stdin.write('\n')
self.process.stdin.flush() self.process.stdin.flush()
except: except:
@@ -274,16 +307,16 @@ class IpOptimizer:
self.stop() self.stop()
print("--- CloudflareSpeedTest IPv6 执行结束 ---") logger.info("--- CloudflareSpeedTest IPv6 执行结束 ---")
return optimal_ipv6 return optimal_ipv6
except Exception as e: except Exception as e:
print(f"执行 CloudflareSpeedTest IPv6 时发生错误: {e}") logger.error(f"执行 CloudflareSpeedTest IPv6 时发生错误: {e}")
return None return None
def stop(self): def stop(self):
if self.process and self.process.poll() is None: if self.process and self.process.poll() is None:
print("正在终止 CloudflareSpeedTest 进程...") logger.info("正在终止 CloudflareSpeedTest 进程...")
try: try:
if self.process.stdin and not self.process.stdin.closed: if self.process.stdin and not self.process.stdin.closed:
self.process.stdin.write('\n') self.process.stdin.write('\n')
@@ -298,7 +331,7 @@ class IpOptimizer:
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired:
self.process.kill() self.process.kill()
self.process.wait() self.process.wait()
print("CloudflareSpeedTest 进程已终止。") logger.info("CloudflareSpeedTest 进程已终止。")
class IpOptimizerThread(QThread): class IpOptimizerThread(QThread):
@@ -324,14 +357,3 @@ class IpOptimizerThread(QThread):
def stop(self): def stop(self):
self.optimizer.stop() self.optimizer.stop()
if __name__ == '__main__':
# 用于直接测试此模块
test_url = "https://speed.cloudflare.com/__down?during=download&bytes=104857600"
optimizer = IpOptimizer()
ip = optimizer.get_optimal_ip(test_url)
if ip:
print(f"{test_url} 找到的最优 IP 是: {ip}")
else:
print(f"未能为 {test_url} 找到最优 IP。")