chore: 项目文件结构重构

删除多个不再使用的源文件,包括动画、下载、配置、UI 相关文件及图标,清理代码库以提高可维护性。
This commit is contained in:
hyb-oyqq
2025-07-18 18:59:19 +08:00
parent 2e6f71d962
commit f202925333
49 changed files with 2876 additions and 684 deletions

11
source/bin/result.csv Normal file
View File

@@ -0,0 +1,11 @@
IP 地址,已发送,已接收,丢包率,平均延迟,下载速度(MB/s),地区码
104.21.237.213,4,4,0.00,173.18,86.14,LAX
141.101.120.105,4,4,0.00,173.02,40.72,SJC
162.159.251.244,4,4,0.00,171.02,8.59,LAX
104.18.19.226,4,4,0.00,172.26,5.27,LAX
104.19.32.102,4,4,0.00,173.01,4.52,SJC
141.101.121.113,4,4,0.00,169.51,3.69,SJC
162.159.207.149,4,4,0.00,169.94,3.24,SJC
104.19.21.114,4,4,0.00,171.00,0.38,SJC
104.19.127.210,4,4,0.00,170.53,0.22,SJC
104.19.60.94,4,4,0.00,168.33,0.00,N/A
1 IP 地址 已发送 已接收 丢包率 平均延迟 下载速度(MB/s) 地区码
2 104.21.237.213 4 4 0.00 173.18 86.14 LAX
3 141.101.120.105 4 4 0.00 173.02 40.72 SJC
4 162.159.251.244 4 4 0.00 171.02 8.59 LAX
5 104.18.19.226 4 4 0.00 172.26 5.27 LAX
6 104.19.32.102 4 4 0.00 173.01 4.52 SJC
7 141.101.121.113 4 4 0.00 169.51 3.69 SJC
8 162.159.207.149 4 4 0.00 169.94 3.24 SJC
9 104.19.21.114 4 4 0.00 171.00 0.38 SJC
10 104.19.127.210 4 4 0.00 170.53 0.22 SJC
11 104.19.60.94 4 4 0.00 168.33 0.00 N/A

11
source/core/__init__.py Normal file
View File

@@ -0,0 +1,11 @@
from .animations import MultiStageAnimations
from .ui_manager import UIManager
from .download_manager import DownloadManager
from .debug_manager import DebugManager
__all__ = [
'MultiStageAnimations',
'UIManager',
'DownloadManager',
'DebugManager'
]

View File

@@ -0,0 +1,57 @@
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
def toggle_debug_mode(self, checked):
"""切换调试模式
Args:
checked: 是否启用调试模式
"""
self.main_window.config["debug_mode"] = checked
self.main_window.save_config(self.main_window.config)
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

@@ -0,0 +1,493 @@
import os
import requests
import json
from collections import deque
from urllib.parse import urlparse
from PySide6 import QtWidgets
from PySide6.QtCore import Qt
from utils import msgbox_frame, HostsManager
from data.config import APP_NAME, PLUGIN, GAME_INFO, UA, CONFIG_URL
from workers import IpOptimizerThread
class DownloadManager:
def __init__(self, main_window):
"""初始化下载管理器
Args:
main_window: 主窗口实例用于访问UI和状态
"""
self.main_window = main_window
self.selected_folder = ""
self.download_queue = deque()
self.current_download_thread = None
self.hosts_manager = HostsManager()
self.optimized_ip = None
self.optimization_done = False # 标记是否已执行过优选
self.optimizing_msg_box = None
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.download_action()
def get_install_paths(self):
"""获取所有游戏版本的安装路径"""
return {
game: os.path.join(self.selected_folder, info["install_path"])
for game, info in GAME_INFO.items()
}
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):
"""开始下载流程"""
# 禁用开始安装按钮
self.main_window.ui.start_install_btn.setEnabled(False)
# 显示哈希检查窗口
self.main_window.hash_msg_box = self.main_window.hash_manager.hash_pop_window()
# 执行预检查
install_paths = self.get_install_paths()
self.main_window.hash_thread = self.main_window.create_hash_thread("pre", install_paths)
self.main_window.hash_thread.pre_finished.connect(self.on_pre_hash_finished)
self.main_window.hash_thread.start()
def on_pre_hash_finished(self, updated_status):
"""哈希预检查完成后的处理
Args:
updated_status: 更新后的安装状态
"""
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
# 获取下载配置
config = self.get_download_url()
if not config:
QtWidgets.QMessageBox.critical(
self.main_window, f"错误 - {APP_NAME}", "\n网络状态异常或服务器故障,请重试\n"
)
# 重新启用开始安装按钮
self.main_window.ui.start_install_btn.setEnabled(True)
return
# 填充下载队列
self._fill_download_queue(config)
# 如果没有需要下载的内容,直接进行最终校验
if not self.download_queue:
self.main_window.after_hash_compare()
return
# 只有当有需要下载内容时才询问是否使用Cloudflare加速
# 询问用户是否使用Cloudflare加速
msg_box = QtWidgets.QMessageBox(self.main_window)
msg_box.setWindowTitle(f"下载优化 - {APP_NAME}")
msg_box.setText("是否愿意通过Cloudflare加速来优化下载速度\n\n这将临时修改系统的hosts文件并需要管理员权限。")
msg_box.setIcon(QtWidgets.QMessageBox.Icon.Question)
yes_button = msg_box.addButton("是,开启加速", QtWidgets.QMessageBox.ButtonRole.YesRole)
no_button = msg_box.addButton("否,直接下载", QtWidgets.QMessageBox.ButtonRole.NoRole)
msg_box.exec()
use_optimization = msg_box.clickedButton() == yes_button
if use_optimization and not self.optimization_done:
first_url = self.download_queue[0][0]
self._start_ip_optimization(first_url)
else:
# 如果用户选择不优化,或已经优化过,直接开始下载
self.next_download_task()
def _fill_download_queue(self, config):
"""填充下载队列
Args:
config: 包含下载URL的配置字典
"""
# 清空现有队列
self.download_queue.clear()
# 添加nekopara 1-4
for i in range(1, 5):
game_version = f"NEKOPARA Vol.{i}"
if not self.main_window.installed_status.get(game_version, False):
url = config.get(f"vol{i}")
if not url: continue
game_folder = os.path.join(self.selected_folder, f"NEKOPARA Vol. {i}")
_7z_path = os.path.join(PLUGIN, f"vol.{i}.7z")
plugin_path = os.path.join(PLUGIN, GAME_INFO[game_version]["plugin_path"])
self.download_queue.append((url, game_folder, game_version, _7z_path, plugin_path))
# 添加nekopara after
game_version = "NEKOPARA After"
if not self.main_window.installed_status.get(game_version, False):
url = config.get("after")
if url:
game_folder = os.path.join(self.selected_folder, "NEKOPARA After")
_7z_path = os.path.join(PLUGIN, "after.7z")
plugin_path = os.path.join(PLUGIN, GAME_INFO[game_version]["plugin_path"])
self.download_queue.append((url, game_folder, game_version, _7z_path, plugin_path))
def _start_ip_optimization(self, url):
"""开始IP优化过程
Args:
url: 用于优化的URL
"""
# 禁用退出按钮
self.main_window.ui.exit_btn.setEnabled(False)
self.optimizing_msg_box = msgbox_frame(
f"通知 - {APP_NAME}",
"\n正在优选Cloudflare IP请稍候...\n\n这可能需要5-10分钟请耐心等待喵~"
)
# 我们不再提供"跳过"按钮
self.optimizing_msg_box.setStandardButtons(QtWidgets.QMessageBox.StandardButton.NoButton)
self.optimizing_msg_box.setWindowModality(Qt.WindowModality.ApplicationModal)
self.optimizing_msg_box.open()
# 创建并启动优化线程
self.ip_optimizer_thread = IpOptimizerThread(url)
self.ip_optimizer_thread.finished.connect(self.on_optimization_finished)
self.ip_optimizer_thread.start()
def on_optimization_finished(self, ip):
"""IP优化完成后的处理
Args:
ip: 优选的IP地址如果失败则为空字符串
"""
self.optimized_ip = ip
self.optimization_done = True
# 关闭提示框
if hasattr(self, 'optimizing_msg_box') and self.optimizing_msg_box:
if self.optimizing_msg_box.isVisible():
self.optimizing_msg_box.accept()
self.optimizing_msg_box = None
# 显示优选结果
if not ip:
QtWidgets.QMessageBox.warning(
self.main_window,
f"优选失败 - {APP_NAME}",
"\n未能找到合适的Cloudflare IP将使用默认网络进行下载。\n"
)
else:
# 应用优选IP到hosts文件
if self.download_queue:
first_url = self.download_queue[0][0]
hostname = urlparse(first_url).hostname
# 先清理可能存在的旧记录
self.hosts_manager.clean_hostname_entries(hostname)
if self.hosts_manager.apply_ip(hostname, ip):
QtWidgets.QMessageBox.information(
self.main_window,
f"成功 - {APP_NAME}",
f"\n已将优选IP ({ip}) 应用到hosts文件。\n"
)
else:
QtWidgets.QMessageBox.critical(
self.main_window,
f"错误 - {APP_NAME}",
"\n修改hosts文件失败请检查程序是否以管理员权限运行。\n"
)
# 开始下载
self.next_download_task()
def next_download_task(self):
"""处理下载队列中的下一个任务"""
if not self.download_queue:
self.main_window.after_hash_compare()
return
# 检查下载线程是否仍在运行,以避免在手动停止后立即开始下一个任务
if self.current_download_thread and self.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: 插件路径
"""
game_exe = {
game: os.path.join(
self.selected_folder, info["install_path"].split("/")[0], info["exe"]
)
for game, info in GAME_INFO.items()
}
# 检查游戏是否已安装
if (
game_version not in game_exe
or not os.path.exists(game_exe[game_version])
or self.main_window.installed_status[game_version]
):
self.main_window.installed_status[game_version] = False
self.main_window.show_result()
return
# 创建进度窗口并开始下载
self.main_window.progress_window = self.main_window.create_progress_window()
self.start_download(url, _7z_path, game_version, game_folder, plugin_path)
def start_download(self, url, _7z_path, game_version, game_folder, plugin_path):
"""启动下载线程
Args:
url: 下载URL
_7z_path: 7z文件保存路径
game_version: 游戏版本名称
game_folder: 游戏文件夹路径
plugin_path: 插件路径
"""
# 禁用退出按钮
self.main_window.ui.exit_btn.setEnabled(False)
if self.optimized_ip:
print(f"已为 {game_version} 获取到优选IP: {self.optimized_ip}")
else:
print(f"未能为 {game_version} 获取优选IP将使用默认线路。")
# 创建并连接下载线程
self.current_download_thread = self.main_window.create_download_thread(url, _7z_path, game_version)
self.current_download_thread.progress.connect(self.main_window.progress_window.update_progress)
self.current_download_thread.finished.connect(
lambda success, error: self.on_download_finished(
success,
error,
url,
game_folder,
game_version,
_7z_path,
plugin_path,
)
)
# 连接停止按钮
self.main_window.progress_window.stop_button.clicked.connect(self.current_download_thread.stop)
# 启动线程和显示进度窗口
self.current_download_thread.start()
self.main_window.progress_window.exec()
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.isVisible():
self.main_window.progress_window.reject()
# 处理下载失败
if not success:
print(f"--- Download Failed: {game_version} ---")
print(error)
print("------------------------------------")
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.download_setting(url, game_folder, game_version, _7z_path, plugin_path)
elif clicked_button == next_button:
self.next_download_task()
else:
self.on_download_stopped()
return
# 下载成功,开始解压缩
self.main_window.hash_msg_box = self.main_window.hash_manager.hash_pop_window()
# 创建并启动解压线程
self.main_window.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()
# 处理解压结果
if not success:
QtWidgets.QMessageBox.critical(self.main_window, f"错误 - {APP_NAME}", error_message)
self.main_window.installed_status[game_version] = False
else:
self.main_window.installed_status[game_version] = True
# 继续下一个下载任务
self.next_download_task()
def on_download_stopped(self):
"""当用户点击停止按钮或选择结束时调用的函数"""
# 停止IP优化线程
if hasattr(self, 'ip_optimizer_thread') and self.ip_optimizer_thread and self.ip_optimizer_thread.isRunning():
self.ip_optimizer_thread.stop()
self.ip_optimizer_thread.wait()
if hasattr(self, 'optimizing_msg_box') and self.optimizing_msg_box:
if self.optimizing_msg_box.isVisible():
self.optimizing_msg_box.accept()
self.optimizing_msg_box = None
# 停止当前可能仍在运行的下载线程
if self.current_download_thread and self.current_download_thread.isRunning():
self.current_download_thread.stop()
self.current_download_thread.wait() # 等待线程完全终止
# 清空下载队列,因为用户决定停止
self.download_queue.clear()
# 确保进度窗口已关闭
if hasattr(self.main_window, 'progress_window') and self.main_window.progress_window.isVisible():
self.main_window.progress_window.reject()
# 可以在这里决定是否立即进行哈希比较或显示结果
print("下载已全部停止。")
self.main_window.setEnabled(True) # 恢复主窗口交互
# 重新启用退出按钮和开始安装按钮
self.main_window.ui.exit_btn.setEnabled(True)
self.main_window.ui.start_install_btn.setEnabled(True)
self.main_window.show_result()

94
source/core/ui_manager.py Normal file
View File

@@ -0,0 +1,94 @@
from PySide6.QtGui import QIcon, QAction
from PySide6.QtWidgets import QMessageBox, QMainWindow
from PySide6.QtCore import Qt
import webbrowser
from utils import load_base64_image, msgbox_frame
from data.config import APP_NAME, APP_VERSION
from data.pic_data import img_data
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
def setup_ui(self):
"""设置UI元素包括窗口图标、标题和菜单"""
# 设置窗口图标
icon_data = img_data.get("icon")
if icon_data:
pixmap = load_base64_image(icon_data)
self.main_window.setWindowIcon(QIcon(pixmap))
# 设置窗口标题
self.main_window.setWindowTitle(f"{APP_NAME} v{APP_VERSION}")
# 设置菜单
self._setup_help_menu()
self._setup_settings_menu()
def _setup_help_menu(self):
"""设置"帮助"菜单"""
if not self.ui or not hasattr(self.ui, 'menu_2'):
return
project_home_action = QAction("项目主页", self.main_window)
project_home_action.triggered.connect(self.open_project_home_page)
about_action = QAction("关于", self.main_window)
about_action.triggered.connect(self.show_about_dialog)
self.ui.menu_2.addAction(project_home_action)
self.ui.menu_2.addAction(about_action)
def _setup_settings_menu(self):
"""设置"设置"菜单"""
if not self.ui or not hasattr(self.ui, 'menu'):
return
self.debug_action = QAction("Debug模式", self.main_window, checkable=True)
# 安全地获取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)
self.ui.menu.addAction(self.debug_action)
# 为未来功能预留的"切换下载源"按钮
self.switch_source_action = QAction("切换下载源", self.main_window)
self.switch_source_action.setEnabled(False) # 暂时禁用
self.ui.menu.addAction(self.switch_source_action)
def open_project_home_page(self):
"""打开项目主页"""
webbrowser.open("https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT")
def show_about_dialog(self):
"""显示关于对话框"""
about_text = f"""
<p><b>{APP_NAME} v{APP_VERSION}</b></p>
<p>原作: <a href="https://github.com/Yanam1Anna">Yanam1Anna</a></p>
<p>此应用根据 <a href="https://github.com/hyb-oyqq/FRAISEMOE2-Installer/blob/master/LICENSE">GPL-3.0 许可证</a> 授权。</p>
"""
msg_box = msgbox_frame(
f"关于 - {APP_NAME}",
about_text,
QMessageBox.StandardButton.Ok,
)
msg_box.setTextFormat(Qt.TextFormat.RichText) # 使用Qt.TextFormat
msg_box.exec()

View File

@@ -1,167 +1,27 @@
import os
import sys
import shutil
import webbrowser
import requests
import py7zr
import json
from urllib.parse import urlparse
from collections import deque
from PySide6 import QtWidgets
from PySide6.QtCore import QTimer, Qt, QThread, Signal
from PySide6.QtGui import QIcon, QAction
from PySide6.QtWidgets import QMainWindow, QFileDialog, QApplication, QMessageBox, QPushButton
from Ui_install import Ui_MainWindows
from animations import MultiStageAnimations
from config import (
APP_NAME, APP_VERSION, PLUGIN, GAME_INFO, BLOCK_SIZE,
from PySide6 import QtWidgets
from PySide6.QtCore import QTimer
from PySide6.QtWidgets import QMainWindow, QMessageBox
from ui.Ui_install import Ui_MainWindows
from data.config import (
APP_NAME, PLUGIN, GAME_INFO, BLOCK_SIZE,
PLUGIN_HASH, UA, CONFIG_URL, LOG_FILE
)
from utils import (
load_base64_image, HashManager, AdminPrivileges, msgbox_frame,
load_config, save_config, HostsManager, censor_url
load_config, save_config, HashManager, AdminPrivileges, msgbox_frame
)
from workers import (
DownloadThread, ProgressWindow, IpOptimizerThread,
HashThread, ExtractionThread, ConfigFetchThread
)
from core import (
MultiStageAnimations, UIManager, DownloadManager, DebugManager
)
from download import DownloadThread, ProgressWindow
from ip_optimizer import IpOptimizer
from pic_data import img_data
class Logger:
def __init__(self, filename, stream):
self.terminal = stream
self.log = open(filename, "w", encoding="utf-8")
def write(self, message):
censored_message = censor_url(message)
self.terminal.write(censored_message)
self.log.write(censored_message)
self.flush()
def flush(self):
self.terminal.flush()
self.log.flush()
def close(self):
self.log.close()
class IpOptimizerThread(QThread):
finished = Signal(str)
def __init__(self, url, parent=None):
super().__init__(parent)
self.url = url
self.optimizer = IpOptimizer()
def run(self):
optimal_ip = self.optimizer.get_optimal_ip(self.url)
self.finished.emit(optimal_ip if optimal_ip else "")
def stop(self):
self.optimizer.stop()
class HashThread(QThread):
pre_finished = Signal(dict)
after_finished = Signal(dict)
def __init__(self, mode, install_paths, plugin_hash, installed_status, parent=None):
super().__init__(parent)
self.mode = mode
self.install_paths = install_paths
self.plugin_hash = plugin_hash
self.installed_status = installed_status
# 每个线程都应该有自己的HashManager实例
self.hash_manager = HashManager(BLOCK_SIZE)
def run(self):
if self.mode == "pre":
updated_status = self.hash_manager.cfg_pre_hash_compare(
self.install_paths, self.plugin_hash, self.installed_status
)
self.pre_finished.emit(updated_status)
elif self.mode == "after":
result = self.hash_manager.cfg_after_hash_compare(
self.install_paths, self.plugin_hash, self.installed_status
)
self.after_finished.emit(result)
class ExtractionThread(QThread):
finished = Signal(bool, str, str) # success, error_message, game_version
def __init__(self, _7z_path, game_folder, plugin_path, game_version, parent=None):
super().__init__(parent)
self._7z_path = _7z_path
self.game_folder = game_folder
self.plugin_path = plugin_path
self.game_version = game_version
def run(self):
try:
with py7zr.SevenZipFile(self._7z_path, mode="r") as archive:
archive.extractall(path=PLUGIN)
os.makedirs(self.game_folder, exist_ok=True)
shutil.copy(self.plugin_path, self.game_folder)
if self.game_version == "NEKOPARA After":
sig_path = os.path.join(PLUGIN, GAME_INFO[self.game_version]["sig_path"])
shutil.copy(sig_path, self.game_folder)
self.finished.emit(True, "", self.game_version)
except (py7zr.Bad7zFile, FileNotFoundError, Exception) as e:
self.finished.emit(False, f"\n文件操作失败,请重试\n\n【错误信息】:{e}\n", self.game_version)
class ConfigFetchThread(QThread):
finished = Signal(object, str) # data, error_message
def __init__(self, url, headers, debug_mode=False, parent=None):
super().__init__(parent)
self.url = url
self.headers = headers
self.debug_mode = debug_mode
def run(self):
try:
if self.debug_mode:
print("--- Starting to fetch cloud config ---")
print(f"DEBUG: Requesting URL: {self.url}")
print(f"DEBUG: Using Headers: {self.headers}")
response = requests.get(self.url, headers=self.headers, timeout=10)
if self.debug_mode:
print(f"DEBUG: Response Status Code: {response.status_code}")
print(f"DEBUG: Response Headers: {response.headers}")
print(f"DEBUG: Response Text: {response.text}")
response.raise_for_status()
# 首先总是尝试解析JSON
config_data = response.json()
# 检查是否是要求更新的错误信息
if config_data.get("message") == "请使用最新版本的FRAISEMOE Addons Installer NEXT进行下载安装":
self.finished.emit(None, "update_required")
return
# 检查是否是有效的配置文件
required_keys = [f"vol.{i+1}.data" for i in range(4)] + ["after.data"]
missing_keys = [key for key in required_keys if key not in config_data]
if missing_keys:
self.finished.emit(None, f"missing_keys:{','.join(missing_keys)}")
return
self.finished.emit(config_data, "")
except requests.exceptions.RequestException as e:
self.finished.emit(None, f"网络请求失败: {e}")
except (ValueError, json.JSONDecodeError) as e:
self.finished.emit(None, f"JSON解析失败: {e}")
finally:
if self.debug_mode:
print("--- Finished fetching cloud config ---")
class MainWindow(QMainWindow):
def __init__(self):
@@ -170,45 +30,42 @@ class MainWindow(QMainWindow):
# 初始化UI (从Ui_install.py导入)
self.ui = Ui_MainWindows()
self.ui.setupUi(self)
icon_data = img_data.get("icon")
if icon_data:
pixmap = load_base64_image(icon_data)
self.setWindowIcon(QIcon(pixmap))
# 设置窗口标题为APP_NAME加版本号
self.setWindowTitle(f"{APP_NAME} v{APP_VERSION}")
# 初始化动画系统 (从animations.py导入)
self.animator = MultiStageAnimations(self.ui, self)
# 初始化功能变量
self.selected_folder = ""
self.installed_status = {f"NEKOPARA Vol.{i}": False for i in range(1, 5)}
self.installed_status["NEKOPARA After"] = False # 添加After的状态
self.download_queue = deque()
self.current_download_thread = None
self.hash_manager = HashManager(BLOCK_SIZE)
self.hash_thread = None
self.extraction_thread = None
self.hash_msg_box = None
self.optimized_ip = None
self.optimization_done = False # 标记是否已执行过优选
self.logger = None
self.hosts_manager = HostsManager() # 实例化HostsManager
self.cloud_config = None
self.config_fetch_thread = None
# 加载配置
# 初始化配置
self.config = load_config()
# 初始化状态变量
self.cloud_config = None
self.installed_status = {f"NEKOPARA Vol.{i}": False for i in range(1, 5)}
self.installed_status["NEKOPARA After"] = False # 添加After的状态
self.hash_msg_box = None
self.progress_window = None
# 初始化工具类
self.hash_manager = HashManager(BLOCK_SIZE)
self.admin_privileges = AdminPrivileges()
# 初始化管理器
self.animator = MultiStageAnimations(self.ui, self)
self.ui_manager = UIManager(self)
# 首先设置UI - 确保debug_action已初始化
self.ui_manager.setup_ui()
self.debug_manager = DebugManager(self)
self.download_manager = DownloadManager(self)
# 设置退出按钮和开始安装按钮的样式表,使其在禁用状态下不会变灰
button_style = "QPushButton:disabled { opacity: 1.0; }"
self.ui.exit_btn.setStyleSheet(button_style)
self.ui.start_install_btn.setStyleSheet(button_style)
# 检查管理员权限和进程
admin_privileges = AdminPrivileges()
admin_privileges.request_admin_privileges()
admin_privileges.check_and_terminate_processes()
self.admin_privileges.request_admin_privileges()
self.admin_privileges.check_and_terminate_processes()
# 备份hosts文件
self.hosts_manager.backup()
self.download_manager.hosts_manager.backup()
# 创建缓存目录
if not os.path.exists(PLUGIN):
@@ -222,32 +79,14 @@ class MainWindow(QMainWindow):
)
sys.exit(1)
# 连接信号 (使用Ui_install.py中的组件名称)
self.ui.start_install_btn.clicked.connect(self.file_dialog)
# 连接信号
self.ui.start_install_btn.clicked.connect(self.download_manager.file_dialog)
self.ui.exit_btn.clicked.connect(self.shutdown_app)
# “帮助”菜单
project_home_action = QAction("项目主页", self)
project_home_action.triggered.connect(self.open_project_home_page)
about_action = QAction("关于", self)
about_action.triggered.connect(self.show_about_dialog)
self.ui.menu_2.addAction(project_home_action)
self.ui.menu_2.addAction(about_action)
# “设置”菜单
self.debug_action = QAction("Debug模式", self, checkable=True)
self.debug_action.setChecked(self.config.get("debug_mode", False))
self.debug_action.triggered.connect(self.toggle_debug_mode)
self.ui.menu.addAction(self.debug_action)
# 为未来功能预留的“切换下载源”按钮
self.switch_source_action = QAction("切换下载源", self)
self.switch_source_action.setEnabled(False) # 暂时禁用
self.ui.menu.addAction(self.switch_source_action)
# 根据初始配置决定是否开启Debug模式
if self.debug_action.isChecked():
self.start_logging()
if hasattr(self.ui_manager, 'debug_action') and self.ui_manager.debug_action:
if self.ui_manager.debug_action.isChecked():
self.debug_manager.start_logging()
# 在窗口显示前设置初始状态
self.animator.initialize()
@@ -256,437 +95,159 @@ class MainWindow(QMainWindow):
QTimer.singleShot(100, self.start_animations)
def start_animations(self):
self.ui.exit_btn.setEnabled(False)
"""开始启动动画"""
# 不再禁用退出按钮的交互性,只通过样式表控制外观
# 但仍然需要跟踪动画状态,防止用户在动画播放过程中退出
self.animation_in_progress = True
# 禁用开始安装按钮,防止在动画播放期间点击
self.ui.start_install_btn.setEnabled(False)
self.animator.animation_finished.connect(self.on_animations_finished)
self.animator.start_animations()
self.fetch_cloud_config()
def on_animations_finished(self):
self.ui.exit_btn.setEnabled(True)
"""动画完成后启用按钮"""
self.animation_in_progress = False
# 启用开始安装按钮
self.ui.start_install_btn.setEnabled(True)
def fetch_cloud_config(self):
"""获取云端配置"""
headers = {"User-Agent": UA}
debug_mode = self.debug_action.isChecked()
debug_mode = self.ui_manager.debug_action.isChecked() if self.ui_manager.debug_action else False
self.config_fetch_thread = ConfigFetchThread(CONFIG_URL, headers, debug_mode, self)
self.config_fetch_thread.finished.connect(self.on_config_fetched)
self.config_fetch_thread.start()
def on_config_fetched(self, data, error_message):
"""云端配置获取完成的回调处理
Args:
data: 获取到的配置数据
error_message: 错误信息,如果有
"""
if error_message:
if error_message == "update_required":
msg_box = msgbox_frame(
f"更新提示 - {APP_NAME}",
"\n当前版本过低,请及时更新。\n",
QtWidgets.QMessageBox.StandardButton.Ok,
QMessageBox.StandardButton.Ok,
)
msg_box.exec()
self.open_project_home_page()
self.ui_manager.open_project_home_page()
self.shutdown_app(force_exit=True)
elif "missing_keys" in error_message:
missing_versions = error_message.split(":")[1]
msg_box = msgbox_frame(
f"配置缺失 - {APP_NAME}",
f'\n云端缺失下载链接,可能云服务器正在维护,不影响其他版本下载。\n当前缺失版本:"{missing_versions}"\n',
QtWidgets.QMessageBox.StandardButton.Ok,
QMessageBox.StandardButton.Ok,
)
msg_box.exec()
else:
# 其他错误暂时只在debug模式下打印
if self.debug_action.isChecked():
debug_mode = self.ui_manager.debug_action.isChecked() if self.ui_manager.debug_action else False
if debug_mode:
print(f"获取云端配置失败: {error_message}")
else:
self.cloud_config = data
if self.debug_action.isChecked():
debug_mode = self.ui_manager.debug_action.isChecked() if self.ui_manager.debug_action else False
if debug_mode:
print("--- Cloud config fetched successfully ---")
print(json.dumps(data, indent=2))
def toggle_debug_mode(self, checked):
self.config["debug_mode"] = checked
save_config(self.config)
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, "错误", 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
"""切换调试模式
def get_install_paths(self):
return {
game: os.path.join(self.selected_folder, info["install_path"])
for game, info in GAME_INFO.items()
}
def file_dialog(self):
self.selected_folder = QFileDialog.getExistingDirectory(
self, f"选择游戏所在【上级目录】 {APP_NAME}"
)
if not self.selected_folder:
QtWidgets.QMessageBox.warning(
self, f"通知 - {APP_NAME}", "\n未选择任何目录,请重新选择\n"
)
return
self.download_action()
def get_download_url(self) -> dict:
try:
if self.cloud_config:
if self.debug_action.isChecked():
print("--- Using pre-fetched cloud config ---")
config_data = self.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.debug_action.isChecked():
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"]
Args:
checked: 是否启用调试模式
"""
self.debug_manager.toggle_debug_mode(checked)
def save_config(self, config):
"""保存配置的便捷方法"""
save_config(config)
def create_download_thread(self, url, _7z_path, game_version):
"""创建下载线程
Args:
url: 下载URL
_7z_path: 7z文件保存路径
game_version: 游戏版本
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.debug_action.isChecked():
print(f"DEBUG: Extracted URLs: {urls}")
print("--- Finished getting download URL successfully ---")
return urls
if self.debug_action.isChecked():
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.debug_action.isChecked():
print(f"ERROR: Failed to get download config due to RequestException: {e}")
Returns:
DownloadThread: 下载线程实例
"""
return DownloadThread(url, _7z_path, game_version, self)
def create_progress_window(self):
"""创建下载进度窗口
Returns:
ProgressWindow: 进度窗口实例
"""
return ProgressWindow(self)
def create_hash_thread(self, mode, install_paths):
"""创建哈希检查线程
Args:
mode: 检查模式,"pre""after"
install_paths: 安装路径字典
QtWidgets.QMessageBox.critical(
self,
f"错误 - {APP_NAME}",
f"\n下载配置获取失败\n\n【HTTP状态】{status_code}\n【错误类型】:{json_title}\n【错误信息】:{json_message}\n",
)
return {}
except ValueError as e:
if self.debug_action.isChecked():
print(f"ERROR: Failed to parse download config due to ValueError: {e}")
QtWidgets.QMessageBox.critical(
self,
f"错误 - {APP_NAME}",
f"\n配置文件格式异常\n\n【错误信息】:{e}\n",
)
return {}
def download_setting(self, url, game_folder, game_version, _7z_path, plugin_path):
game_exe = {
game: os.path.join(
self.selected_folder, info["install_path"].split("/")[0], info["exe"]
)
for game, info in GAME_INFO.items()
}
Returns:
HashThread: 哈希检查线程实例
"""
return HashThread(mode, install_paths, PLUGIN_HASH, self.installed_status, self)
if (
game_version not in game_exe
or not os.path.exists(game_exe[game_version])
or self.installed_status[game_version]
):
self.installed_status[game_version] = False
self.show_result()
return
def create_extraction_thread(self, _7z_path, game_folder, plugin_path, game_version):
"""创建解压线程
Args:
_7z_path: 7z文件路径
game_folder: 游戏文件夹路径
plugin_path: 插件路径
game_version: 游戏版本
self.progress_window = ProgressWindow(self)
self.start_download_with_ip(self.optimized_ip, url, _7z_path, game_version, game_folder, plugin_path)
def on_optimization_and_hosts_finished(self, ip):
self.optimized_ip = ip
self.optimization_done = True
if hasattr(self, 'optimizing_msg_box') and self.optimizing_msg_box:
if self.optimizing_msg_box.isVisible():
self.optimizing_msg_box.accept()
self.optimizing_msg_box = None
if not ip:
QtWidgets.QMessageBox.warning(self, f"优选失败 - {APP_NAME}", "\n未能找到合适的Cloudflare IP将使用默认网络进行下载。\n")
else:
if self.download_queue:
first_url = self.download_queue[0][0]
hostname = urlparse(first_url).hostname
if self.hosts_manager.apply_ip(hostname, ip):
QtWidgets.QMessageBox.information(self, f"成功 - {APP_NAME}", f"\n已将优选IP ({ip}) 应用到hosts文件。\n")
else:
QtWidgets.QMessageBox.critical(self, f"错误 - {APP_NAME}", "\n修改hosts文件失败请检查程序是否以管理员权限运行。\n")
Returns:
ExtractionThread: 解压线程实例
"""
return ExtractionThread(_7z_path, game_folder, plugin_path, game_version, self)
self.next_download_task()
def start_download_with_ip(self, preferred_ip, url, _7z_path, game_version, game_folder, plugin_path):
if preferred_ip:
print(f"已为 {game_version} 获取到优选IP: {preferred_ip}")
else:
print(f"未能为 {game_version} 获取优选IP将使用默认线路。")
self.current_download_thread = DownloadThread(url, _7z_path, game_version, self)
self.current_download_thread.progress.connect(self.progress_window.update_progress)
self.current_download_thread.finished.connect(
lambda success, error: self.install_setting(
success,
error,
self.progress_window,
url,
game_folder,
game_version,
_7z_path,
plugin_path,
)
)
def after_hash_compare(self):
"""进行安装后哈希比较"""
# 禁用退出按钮
self.ui.exit_btn.setEnabled(False)
self.progress_window.stop_button.clicked.connect(self.current_download_thread.stop)
self.current_download_thread.start()
self.progress_window.exec()
def install_setting(
self,
success,
error,
progress_window,
url,
game_folder,
game_version,
_7z_path,
plugin_path,
):
if progress_window.isVisible():
progress_window.reject()
if not success:
print(f"--- Download Failed: {game_version} ---")
print(error)
print("------------------------------------")
msg_box = QtWidgets.QMessageBox(self)
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.download_setting(url, game_folder, game_version, _7z_path, plugin_path)
elif clicked_button == next_button:
self.next_download_task()
else:
self.on_download_stopped()
return
# --- Start Extraction in a new thread ---
self.hash_msg_box = self.hash_manager.hash_pop_window()
self.extraction_thread = ExtractionThread(_7z_path, game_folder, plugin_path, game_version, self)
self.extraction_thread.finished.connect(self.on_extraction_finished)
self.extraction_thread.start()
def on_extraction_finished(self, success, error_message, game_version):
if self.hash_msg_box and self.hash_msg_box.isVisible():
self.hash_msg_box.close()
if not success:
QtWidgets.QMessageBox.critical(self, f"错误 - {APP_NAME}", error_message)
self.installed_status[game_version] = False
else:
self.installed_status[game_version] = True
self.next_download_task()
def download_action(self):
# 询问用户是否使用Cloudflare加速
msg_box = QMessageBox(self)
msg_box.setWindowTitle(f"下载优化 - {APP_NAME}")
msg_box.setText("是否愿意通过Cloudflare加速来优化下载速度\n\n这将临时修改系统的hosts文件并需要管理员权限。")
msg_box.setIcon(QMessageBox.Icon.Question)
yes_button = msg_box.addButton("是,开启加速", QMessageBox.ButtonRole.YesRole)
no_button = msg_box.addButton("否,直接下载", QMessageBox.ButtonRole.NoRole)
msg_box.exec()
use_optimization = msg_box.clickedButton() == yes_button
self.hash_msg_box = self.hash_manager.hash_pop_window()
install_paths = self.get_install_paths()
install_paths = self.download_manager.get_install_paths()
self.hash_thread = HashThread("pre", install_paths, PLUGIN_HASH, self.installed_status, self)
# 将用户选择传递给哈希完成后的处理函数
self.hash_thread.pre_finished.connect(lambda status: self.on_pre_hash_finished(status, use_optimization))
self.hash_thread.start()
def on_pre_hash_finished(self, updated_status, use_optimization):
self.installed_status = updated_status
if self.hash_msg_box and self.hash_msg_box.isVisible():
self.hash_msg_box.accept()
self.hash_msg_box = None
config = self.get_download_url()
if not config:
QtWidgets.QMessageBox.critical(
self, f"错误 - {APP_NAME}", "\n网络状态异常或服务器故障,请重试\n"
)
return
# --- 填充下载队列 ---
for i in range(1, 5):
game_version = f"NEKOPARA Vol.{i}"
if not self.installed_status.get(game_version, False):
url = config.get(f"vol{i}")
if not url: continue
game_folder = os.path.join(self.selected_folder, f"NEKOPARA Vol. {i}")
_7z_path = os.path.join(PLUGIN, f"vol.{i}.7z")
plugin_path = os.path.join(PLUGIN, GAME_INFO[game_version]["plugin_path"])
self.download_queue.append((url, game_folder, game_version, _7z_path, plugin_path))
game_version = "NEKOPARA After"
if not self.installed_status.get(game_version, False):
url = config.get("after")
if url:
game_folder = os.path.join(self.selected_folder, "NEKOPARA After")
_7z_path = os.path.join(PLUGIN, "after.7z")
plugin_path = os.path.join(PLUGIN, GAME_INFO[game_version]["plugin_path"])
self.download_queue.append((url, game_folder, game_version, _7z_path, plugin_path))
if not self.download_queue:
self.after_hash_compare(PLUGIN_HASH)
return
if use_optimization and not self.optimization_done:
first_url = self.download_queue[0][0]
self.optimizing_msg_box = msgbox_frame(
f"通知 - {APP_NAME}",
"\n正在优选Cloudflare IP请稍候...\n\n这可能需要5-10分钟请耐心等待喵~"
)
# 我们不再提供“跳过”按钮,因为用户已经做出了选择
self.optimizing_msg_box.setStandardButtons(QMessageBox.StandardButton.NoButton)
self.optimizing_msg_box.setWindowModality(Qt.WindowModality.ApplicationModal)
self.optimizing_msg_box.open()
self.ip_optimizer_thread = IpOptimizerThread(first_url)
# 优选完成后需要修改hosts并开始下载
self.ip_optimizer_thread.finished.connect(self.on_optimization_and_hosts_finished)
self.ip_optimizer_thread.start()
else:
# 如果用户选择不优化,或已经优化过,直接开始下载
self.next_download_task()
def next_download_task(self):
if not self.download_queue:
self.after_hash_compare(PLUGIN_HASH)
return
# 检查下载线程是否仍在运行,以避免在手动停止后立即开始下一个任务
if self.current_download_thread and self.current_download_thread.isRunning():
return
# 在开始下载前确保hosts文件已修改如果需要
# 这里的逻辑保持不变因为hosts文件应该在队列开始前就被修改了
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 on_download_stopped(self):
"""当用户点击停止按钮或选择结束时调用的槽函数"""
# 停止IP优选线程
if hasattr(self, 'ip_optimizer_thread') and self.ip_optimizer_thread and self.ip_optimizer_thread.isRunning():
self.ip_optimizer_thread.stop()
self.ip_optimizer_thread.wait()
if hasattr(self, 'optimizing_msg_box') and self.optimizing_msg_box:
if self.optimizing_msg_box.isVisible():
self.optimizing_msg_box.accept()
self.optimizing_msg_box = None
# 停止当前可能仍在运行的下载线程
if self.current_download_thread and self.current_download_thread.isRunning():
self.current_download_thread.stop()
self.current_download_thread.wait() # 等待线程完全终止
# 清空下载队列,因为用户决定停止
self.download_queue.clear()
# 确保进度窗口已关闭
if hasattr(self, 'progress_window') and self.progress_window.isVisible():
self.progress_window.reject()
# 可以在这里决定是否立即进行哈希比较或显示结果
print("下载已全部停止。")
self.setEnabled(True) # 恢复主窗口交互
self.show_result()
def after_hash_compare(self, plugin_hash):
self.hash_msg_box = self.hash_manager.hash_pop_window()
install_paths = self.get_install_paths()
self.hash_thread = HashThread("after", install_paths, plugin_hash, self.installed_status, self)
self.hash_thread = self.create_hash_thread("after", install_paths)
self.hash_thread.after_finished.connect(self.on_after_hash_finished)
self.hash_thread.start()
def on_after_hash_finished(self, result):
if self.hash_msg_box and self.hash_msg_box.isVisible():
self.hash_msg_box.close()
"""哈希比较完成后的处理
Args:
result: 哈希比较结果
"""
# 确保哈希检查窗口关闭,无论是否还在显示
if self.hash_msg_box:
try:
if self.hash_msg_box.isVisible():
self.hash_msg_box.close()
else:
# 如果窗口已经不可见但没有关闭,也要尝试关闭
self.hash_msg_box.close()
except:
pass # 忽略任何关闭窗口时的错误
self.hash_msg_box = None
if not result["passed"]:
game = result.get("game", "未知游戏")
@@ -694,94 +255,95 @@ class MainWindow(QMainWindow):
msg_box = msgbox_frame(
f"文件校验失败 - {APP_NAME}",
message,
QtWidgets.QMessageBox.StandardButton.Ok,
QMessageBox.StandardButton.Ok,
)
msg_box.exec()
self.show_result()
# 重新启用退出按钮和开始安装按钮
self.ui.exit_btn.setEnabled(True)
self.ui.start_install_btn.setEnabled(True)
# 添加短暂延迟确保UI更新
QTimer.singleShot(100, self.show_result)
def show_result(self):
"""显示安装结果"""
installed_version = "\n".join(
[i for i in self.installed_status if self.installed_status[i]]
)
failed_ver = "\n".join(
[i for i in self.installed_status if not self.installed_status[i]]
)
QtWidgets.QMessageBox.information(
QMessageBox.information(
self,
f"完成 - {APP_NAME}",
f"\n安装结果:\n安装成功数:{len(installed_version.splitlines())} 安装失败数:{len(failed_ver.splitlines())}\n"
f"安装成功的版本:\n{installed_version}\n尚未持有或未使用本工具安装补丁的版本:\n{failed_ver}\n",
)
def show_about_dialog(self):
"""显示关于对话框"""
about_text = f"""
<p><b>{APP_NAME} v{APP_VERSION}</b></p>
<p>原作: <a href="https://github.com/Yanam1Anna">Yanam1Anna</a></p>
<p>此应用根据 <a href="https://github.com/hyb-oyqq/FRAISEMOE2-Installer/blob/master/LICENSE">GPL-3.0 许可证</a> 授权。</p>
"""
msg_box = msgbox_frame(
f"关于 - {APP_NAME}",
about_text,
QtWidgets.QMessageBox.StandardButton.Ok,
)
msg_box.setTextFormat(Qt.TextFormat.RichText) # 启用富文本
msg_box.exec()
def open_project_home_page(self):
"""打开项目主页"""
webbrowser.open("https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT")
def closeEvent(self, event):
"""窗口关闭事件处理
Args:
event: 关闭事件
"""
self.shutdown_app(event)
def shutdown_app(self, event=None, force_exit=False):
self.hosts_manager.restore() # 恢复hosts文件
self.stop_logging() # 确保在退出时停止日志记录
"""关闭应用程序
Args:
event: 关闭事件如果是从closeEvent调用的
force_exit: 是否强制退出
"""
# 检查是否有动画或任务正在进行
if hasattr(self, 'animation_in_progress') and self.animation_in_progress and not force_exit:
# 如果动画正在进行,阻止退出
if event:
event.ignore()
return
# 检查是否有下载任务正在进行
if hasattr(self.download_manager, 'current_download_thread') and \
self.download_manager.current_download_thread and \
self.download_manager.current_download_thread.isRunning() and not force_exit:
# 询问用户是否确认退出
reply = QMessageBox.question(
self,
f"确认退出 - {APP_NAME}",
"\n下载任务正在进行中,确定要退出吗?\n",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.No:
if event:
event.ignore()
return
# 恢复hosts文件
self.download_manager.hosts_manager.restore()
# 额外检查并清理hosts文件中的残留记录
self.download_manager.hosts_manager.check_and_clean_all_entries()
# 停止日志记录
self.debug_manager.stop_logging()
if not force_exit:
reply = QtWidgets.QMessageBox.question(
reply = QMessageBox.question(
self,
"退出程序",
"\n是否确定退出?\n",
QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No,
QtWidgets.QMessageBox.StandardButton.No,
f"确认退出 - {APP_NAME}",
"\n确定退出吗?\n",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No
)
if reply != QtWidgets.QMessageBox.StandardButton.Yes:
if reply == QMessageBox.StandardButton.No:
if event:
event.ignore()
return
if (
self.current_download_thread
and self.current_download_thread.isRunning()
):
QtWidgets.QMessageBox.critical(
self,
f"错误 - {APP_NAME}",
"\n当前有下载任务正在进行,完成后再试\n",
)
if event:
event.ignore()
return
if os.path.exists(PLUGIN):
for attempt in range(3):
try:
shutil.rmtree(PLUGIN)
break
except Exception as e:
if attempt == 2:
QtWidgets.QMessageBox.critical(
self,
f"错误 - {APP_NAME}",
f"\n清理缓存失败\n\n【错误信息】:{e}\n",
)
if event:
event.accept()
sys.exit(1)
# 退出应用程序
if event:
event.accept()
else:
sys.exit(0)
sys.exit(0)

656
source/main_window.py.bak Normal file
View File

@@ -0,0 +1,656 @@
import os
import sys
import shutil
import webbrowser
import requests
import json
from urllib.parse import urlparse
from collections import deque
from PySide6 import QtWidgets
from PySide6.QtCore import QTimer, Qt
from PySide6.QtGui import QIcon, QAction
from PySide6.QtWidgets import QMainWindow, QFileDialog, QApplication, QMessageBox
from ui.Ui_install import Ui_MainWindows
from core.animations import MultiStageAnimations
from data.config import (
APP_NAME, APP_VERSION, PLUGIN, GAME_INFO, BLOCK_SIZE,
PLUGIN_HASH, UA, CONFIG_URL, LOG_FILE
)
from utils import (
load_base64_image, HashManager, AdminPrivileges, msgbox_frame,
load_config, save_config, HostsManager, Logger
)
from workers.download import DownloadThread, ProgressWindow
from data.pic_data import img_data
from workers import (
IpOptimizerThread, HashThread, ExtractionThread, ConfigFetchThread
)
from core import (
MultiStageAnimations, UIManager, DownloadManager, DebugManager
)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
# 初始化UI (从Ui_install.py导入)
self.ui = Ui_MainWindows()
self.ui.setupUi(self)
icon_data = img_data.get("icon")
if icon_data:
pixmap = load_base64_image(icon_data)
self.setWindowIcon(QIcon(pixmap))
# 设置窗口标题为APP_NAME加版本号
self.setWindowTitle(f"{APP_NAME} v{APP_VERSION}")
# 初始化动画系统 (从animations.py导入)
self.animator = MultiStageAnimations(self.ui, self)
# 初始化功能变量
self.selected_folder = ""
self.installed_status = {f"NEKOPARA Vol.{i}": False for i in range(1, 5)}
self.installed_status["NEKOPARA After"] = False # 添加After的状态
self.download_queue = deque()
self.current_download_thread = None
self.hash_manager = HashManager(BLOCK_SIZE)
self.hash_thread = None
self.extraction_thread = None
self.hash_msg_box = None
self.optimized_ip = None
self.optimization_done = False # 标记是否已执行过优选
self.logger = None
self.hosts_manager = HostsManager() # 实例化HostsManager
self.cloud_config = None
self.config_fetch_thread = None
# 加载配置
self.config = load_config()
# 检查管理员权限和进程
self.admin_privileges = AdminPrivileges()
self.admin_privileges.request_admin_privileges()
self.admin_privileges.check_and_terminate_processes()
# 备份hosts文件
self.hosts_manager.backup()
# 创建缓存目录
if not os.path.exists(PLUGIN):
try:
os.makedirs(PLUGIN)
except OSError as e:
QtWidgets.QMessageBox.critical(
self,
f"错误 - {APP_NAME}",
f"\n无法创建缓存位置\n\n使用管理员身份运行或检查文件读写权限\n\n【错误信息】:{e}\n",
)
sys.exit(1)
# 连接信号 (使用Ui_install.py中的组件名称)
self.ui.start_install_btn.clicked.connect(self.download_manager.file_dialog)
self.ui.exit_btn.clicked.connect(self.shutdown_app)
# “帮助”菜单
project_home_action = QAction("项目主页", self)
project_home_action.triggered.connect(self.open_project_home_page)
about_action = QAction("关于", self)
about_action.triggered.connect(self.show_about_dialog)
self.ui.menu_2.addAction(project_home_action)
self.ui.menu_2.addAction(about_action)
# “设置”菜单
self.debug_action = QAction("Debug模式", self, checkable=True)
self.debug_action.setChecked(self.config.get("debug_mode", False))
self.debug_action.triggered.connect(self.toggle_debug_mode)
self.ui.menu.addAction(self.debug_action)
# 为未来功能预留的“切换下载源”按钮
self.switch_source_action = QAction("切换下载源", self)
self.switch_source_action.setEnabled(False) # 暂时禁用
self.ui.menu.addAction(self.switch_source_action)
# 根据初始配置决定是否开启Debug模式
if self.debug_action.isChecked():
self.start_logging()
# 在窗口显示前设置初始状态
self.animator.initialize()
# 窗口显示后延迟100ms启动动画
QTimer.singleShot(100, self.start_animations)
def start_animations(self):
self.ui.exit_btn.setEnabled(False)
self.animator.animation_finished.connect(self.on_animations_finished)
self.animator.start_animations()
self.fetch_cloud_config()
def on_animations_finished(self):
self.ui.exit_btn.setEnabled(True)
def fetch_cloud_config(self):
headers = {"User-Agent": UA}
debug_mode = self.debug_action.isChecked()
self.config_fetch_thread = ConfigFetchThread(CONFIG_URL, headers, debug_mode, self)
self.config_fetch_thread.finished.connect(self.on_config_fetched)
self.config_fetch_thread.start()
def on_config_fetched(self, data, error_message):
if error_message:
if error_message == "update_required":
msg_box = msgbox_frame(
f"更新提示 - {APP_NAME}",
"\n当前版本过低,请及时更新。\n",
QtWidgets.QMessageBox.StandardButton.Ok,
)
msg_box.exec()
self.open_project_home_page()
self.shutdown_app(force_exit=True)
elif "missing_keys" in error_message:
missing_versions = error_message.split(":")[1]
msg_box = msgbox_frame(
f"配置缺失 - {APP_NAME}",
f'\n云端缺失下载链接,可能云服务器正在维护,不影响其他版本下载。\n当前缺失版本:"{missing_versions}"\n',
QtWidgets.QMessageBox.StandardButton.Ok,
)
msg_box.exec()
else:
# 其他错误暂时只在debug模式下打印
if self.debug_action.isChecked():
print(f"获取云端配置失败: {error_message}")
else:
self.cloud_config = data
if self.debug_action.isChecked():
print("--- Cloud config fetched successfully ---")
print(json.dumps(data, indent=2))
def toggle_debug_mode(self, checked):
self.config["debug_mode"] = checked
save_config(self.config)
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, "错误", 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
def get_install_paths(self):
return {
game: os.path.join(self.selected_folder, info["install_path"])
for game, info in GAME_INFO.items()
}
def file_dialog(self):
self.selected_folder = QFileDialog.getExistingDirectory(
self, f"选择游戏所在【上级目录】 {APP_NAME}"
)
if not self.selected_folder:
QtWidgets.QMessageBox.warning(
self, f"通知 - {APP_NAME}", "\n未选择任何目录,请重新选择\n"
)
return
self.download_action()
def get_download_url(self) -> dict:
try:
if self.cloud_config:
if self.debug_action.isChecked():
print("--- Using pre-fetched cloud config ---")
config_data = self.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.debug_action.isChecked():
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.debug_action.isChecked():
print(f"DEBUG: Extracted URLs: {urls}")
print("--- Finished getting download URL successfully ---")
return urls
if self.debug_action.isChecked():
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.debug_action.isChecked():
print(f"ERROR: Failed to get download config due to RequestException: {e}")
QtWidgets.QMessageBox.critical(
self,
f"错误 - {APP_NAME}",
f"\n下载配置获取失败\n\n【HTTP状态】{status_code}\n【错误类型】:{json_title}\n【错误信息】:{json_message}\n",
)
return {}
except ValueError as e:
if self.debug_action.isChecked():
print(f"ERROR: Failed to parse download config due to ValueError: {e}")
QtWidgets.QMessageBox.critical(
self,
f"错误 - {APP_NAME}",
f"\n配置文件格式异常\n\n【错误信息】:{e}\n",
)
return {}
def download_setting(self, url, game_folder, game_version, _7z_path, plugin_path):
game_exe = {
game: os.path.join(
self.selected_folder, info["install_path"].split("/")[0], info["exe"]
)
for game, info in GAME_INFO.items()
}
if (
game_version not in game_exe
or not os.path.exists(game_exe[game_version])
or self.installed_status[game_version]
):
self.installed_status[game_version] = False
self.show_result()
return
self.progress_window = ProgressWindow(self)
self.start_download_with_ip(self.optimized_ip, url, _7z_path, game_version, game_folder, plugin_path)
def on_optimization_and_hosts_finished(self, ip):
self.optimized_ip = ip
self.optimization_done = True
if hasattr(self, 'optimizing_msg_box') and self.optimizing_msg_box:
if self.optimizing_msg_box.isVisible():
self.optimizing_msg_box.accept()
self.optimizing_msg_box = None
if not ip:
QtWidgets.QMessageBox.warning(self, f"优选失败 - {APP_NAME}", "\n未能找到合适的Cloudflare IP将使用默认网络进行下载。\n")
else:
if self.download_queue:
first_url = self.download_queue[0][0]
hostname = urlparse(first_url).hostname
if self.hosts_manager.apply_ip(hostname, ip):
QtWidgets.QMessageBox.information(self, f"成功 - {APP_NAME}", f"\n已将优选IP ({ip}) 应用到hosts文件。\n")
else:
QtWidgets.QMessageBox.critical(self, f"错误 - {APP_NAME}", "\n修改hosts文件失败请检查程序是否以管理员权限运行。\n")
self.next_download_task()
def start_download_with_ip(self, preferred_ip, url, _7z_path, game_version, game_folder, plugin_path):
if preferred_ip:
print(f"已为 {game_version} 获取到优选IP: {preferred_ip}")
else:
print(f"未能为 {game_version} 获取优选IP将使用默认线路。")
self.current_download_thread = DownloadThread(url, _7z_path, game_version, self)
self.current_download_thread.progress.connect(self.progress_window.update_progress)
self.current_download_thread.finished.connect(
lambda success, error: self.install_setting(
success,
error,
self.progress_window,
url,
game_folder,
game_version,
_7z_path,
plugin_path,
)
)
self.progress_window.stop_button.clicked.connect(self.current_download_thread.stop)
self.current_download_thread.start()
self.progress_window.exec()
def install_setting(
self,
success,
error,
progress_window,
url,
game_folder,
game_version,
_7z_path,
plugin_path,
):
if progress_window.isVisible():
progress_window.reject()
if not success:
print(f"--- Download Failed: {game_version} ---")
print(error)
print("------------------------------------")
msg_box = QtWidgets.QMessageBox(self)
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.download_setting(url, game_folder, game_version, _7z_path, plugin_path)
elif clicked_button == next_button:
self.next_download_task()
else:
self.on_download_stopped()
return
# --- Start Extraction in a new thread ---
self.hash_msg_box = self.hash_manager.hash_pop_window()
self.extraction_thread = ExtractionThread(_7z_path, game_folder, plugin_path, game_version, self)
self.extraction_thread.finished.connect(self.on_extraction_finished)
self.extraction_thread.start()
def on_extraction_finished(self, success, error_message, game_version):
if self.hash_msg_box and self.hash_msg_box.isVisible():
self.hash_msg_box.close()
if not success:
QtWidgets.QMessageBox.critical(self, f"错误 - {APP_NAME}", error_message)
self.installed_status[game_version] = False
else:
self.installed_status[game_version] = True
self.next_download_task()
def download_action(self):
# 询问用户是否使用Cloudflare加速
msg_box = QMessageBox(self)
msg_box.setWindowTitle(f"下载优化 - {APP_NAME}")
msg_box.setText("是否愿意通过Cloudflare加速来优化下载速度\n\n这将临时修改系统的hosts文件并需要管理员权限。")
msg_box.setIcon(QMessageBox.Icon.Question)
yes_button = msg_box.addButton("是,开启加速", QMessageBox.ButtonRole.YesRole)
no_button = msg_box.addButton("否,直接下载", QMessageBox.ButtonRole.NoRole)
msg_box.exec()
use_optimization = msg_box.clickedButton() == yes_button
self.hash_msg_box = self.hash_manager.hash_pop_window()
install_paths = self.get_install_paths()
self.hash_thread = HashThread("pre", install_paths, PLUGIN_HASH, self.installed_status, self)
# 将用户选择传递给哈希完成后的处理函数
self.hash_thread.pre_finished.connect(lambda status: self.on_pre_hash_finished(status, use_optimization))
self.hash_thread.start()
def on_pre_hash_finished(self, updated_status, use_optimization):
self.installed_status = updated_status
if self.hash_msg_box and self.hash_msg_box.isVisible():
self.hash_msg_box.accept()
self.hash_msg_box = None
config = self.get_download_url()
if not config:
QtWidgets.QMessageBox.critical(
self, f"错误 - {APP_NAME}", "\n网络状态异常或服务器故障,请重试\n"
)
return
# --- 填充下载队列 ---
for i in range(1, 5):
game_version = f"NEKOPARA Vol.{i}"
if not self.installed_status.get(game_version, False):
url = config.get(f"vol{i}")
if not url: continue
game_folder = os.path.join(self.selected_folder, f"NEKOPARA Vol. {i}")
_7z_path = os.path.join(PLUGIN, f"vol.{i}.7z")
plugin_path = os.path.join(PLUGIN, GAME_INFO[game_version]["plugin_path"])
self.download_queue.append((url, game_folder, game_version, _7z_path, plugin_path))
game_version = "NEKOPARA After"
if not self.installed_status.get(game_version, False):
url = config.get("after")
if url:
game_folder = os.path.join(self.selected_folder, "NEKOPARA After")
_7z_path = os.path.join(PLUGIN, "after.7z")
plugin_path = os.path.join(PLUGIN, GAME_INFO[game_version]["plugin_path"])
self.download_queue.append((url, game_folder, game_version, _7z_path, plugin_path))
if not self.download_queue:
self.after_hash_compare(PLUGIN_HASH)
return
if use_optimization and not self.optimization_done:
first_url = self.download_queue[0][0]
self.optimizing_msg_box = msgbox_frame(
f"通知 - {APP_NAME}",
"\n正在优选Cloudflare IP请稍候...\n\n这可能需要5-10分钟请耐心等待喵~"
)
# 我们不再提供“跳过”按钮,因为用户已经做出了选择
self.optimizing_msg_box.setStandardButtons(QMessageBox.StandardButton.NoButton)
self.optimizing_msg_box.setWindowModality(Qt.WindowModality.ApplicationModal)
self.optimizing_msg_box.open()
self.ip_optimizer_thread = IpOptimizerThread(first_url)
# 优选完成后需要修改hosts并开始下载
self.ip_optimizer_thread.finished.connect(self.on_optimization_and_hosts_finished)
self.ip_optimizer_thread.start()
else:
# 如果用户选择不优化,或已经优化过,直接开始下载
self.next_download_task()
def next_download_task(self):
if not self.download_queue:
self.after_hash_compare(PLUGIN_HASH)
return
# 检查下载线程是否仍在运行,以避免在手动停止后立即开始下一个任务
if self.current_download_thread and self.current_download_thread.isRunning():
return
# 在开始下载前确保hosts文件已修改如果需要
# 这里的逻辑保持不变因为hosts文件应该在队列开始前就被修改了
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 on_download_stopped(self):
"""当用户点击停止按钮或选择结束时调用的槽函数"""
# 停止IP优选线程
if hasattr(self, 'ip_optimizer_thread') and self.ip_optimizer_thread and self.ip_optimizer_thread.isRunning():
self.ip_optimizer_thread.stop()
self.ip_optimizer_thread.wait()
if hasattr(self, 'optimizing_msg_box') and self.optimizing_msg_box:
if self.optimizing_msg_box.isVisible():
self.optimizing_msg_box.accept()
self.optimizing_msg_box = None
# 停止当前可能仍在运行的下载线程
if self.current_download_thread and self.current_download_thread.isRunning():
self.current_download_thread.stop()
self.current_download_thread.wait() # 等待线程完全终止
# 清空下载队列,因为用户决定停止
self.download_queue.clear()
# 确保进度窗口已关闭
if hasattr(self, 'progress_window') and self.progress_window.isVisible():
self.progress_window.reject()
# 可以在这里决定是否立即进行哈希比较或显示结果
print("下载已全部停止。")
self.setEnabled(True) # 恢复主窗口交互
self.show_result()
def after_hash_compare(self, plugin_hash):
self.hash_msg_box = self.hash_manager.hash_pop_window()
install_paths = self.get_install_paths()
self.hash_thread = HashThread("after", install_paths, plugin_hash, self.installed_status, self)
self.hash_thread.after_finished.connect(self.on_after_hash_finished)
self.hash_thread.start()
def on_after_hash_finished(self, result):
if self.hash_msg_box and self.hash_msg_box.isVisible():
self.hash_msg_box.close()
if not result["passed"]:
game = result.get("game", "未知游戏")
message = result.get("message", "发生未知错误。")
msg_box = msgbox_frame(
f"文件校验失败 - {APP_NAME}",
message,
QtWidgets.QMessageBox.StandardButton.Ok,
)
msg_box.exec()
self.show_result()
def show_result(self):
installed_version = "\n".join(
[i for i in self.installed_status if self.installed_status[i]]
)
failed_ver = "\n".join(
[i for i in self.installed_status if not self.installed_status[i]]
)
QtWidgets.QMessageBox.information(
self,
f"完成 - {APP_NAME}",
f"\n安装结果:\n安装成功数:{len(installed_version.splitlines())} 安装失败数:{len(failed_ver.splitlines())}\n"
f"安装成功的版本:\n{installed_version}\n尚未持有或未使用本工具安装补丁的版本:\n{failed_ver}\n",
)
def show_about_dialog(self):
"""显示关于对话框"""
about_text = f"""
<p><b>{APP_NAME} v{APP_VERSION}</b></p>
<p>原作: <a href="https://github.com/Yanam1Anna">Yanam1Anna</a></p>
<p>此应用根据 <a href="https://github.com/hyb-oyqq/FRAISEMOE2-Installer/blob/master/LICENSE">GPL-3.0 许可证</a> 授权。</p>
"""
msg_box = msgbox_frame(
f"关于 - {APP_NAME}",
about_text,
QtWidgets.QMessageBox.StandardButton.Ok,
)
msg_box.setTextFormat(Qt.TextFormat.RichText) # 启用富文本
msg_box.exec()
def open_project_home_page(self):
"""打开项目主页"""
webbrowser.open("https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT")
def closeEvent(self, event):
self.shutdown_app(event)
def shutdown_app(self, event=None, force_exit=False):
self.hosts_manager.restore() # 恢复hosts文件
self.stop_logging() # 确保在退出时停止日志记录
if not force_exit:
reply = QtWidgets.QMessageBox.question(
self,
"退出程序",
"\n是否确定退出?\n",
QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No,
QtWidgets.QMessageBox.StandardButton.No,
)
if reply != QtWidgets.QMessageBox.StandardButton.Yes:
if event:
event.ignore()
return
if (
self.current_download_thread
and self.current_download_thread.isRunning()
):
QtWidgets.QMessageBox.critical(
self,
f"错误 - {APP_NAME}",
"\n当前有下载任务正在进行,完成后再试\n",
)
if event:
event.ignore()
return
if os.path.exists(PLUGIN):
for attempt in range(3):
try:
shutil.rmtree(PLUGIN)
break
except Exception as e:
if attempt == 2:
QtWidgets.QMessageBox.critical(
self,
f"错误 - {APP_NAME}",
f"\n清理缓存失败\n\n【错误信息】:{e}\n",
)
if event:
event.accept()
sys.exit(1)
if event:
event.accept()
else:
sys.exit(0)

View File

Before

Width:  |  Height:  |  Size: 264 KiB

After

Width:  |  Height:  |  Size: 264 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 571 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

View File

@@ -1,4 +1,4 @@
from pic_data import img_data
from data.pic_data import img_data
from PySide6.QtGui import QPixmap
import base64
from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,

18
source/utils/__init__.py Normal file
View File

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

View File

@@ -9,8 +9,8 @@ import psutil
from PySide6 import QtCore, QtWidgets
import re
from PySide6.QtGui import QIcon, QPixmap
from pic_data import img_data
from config import APP_NAME, CONFIG_FILE
from data.pic_data import img_data
from data.config import APP_NAME, CONFIG_FILE
def resource_path(relative_path):
"""获取资源的绝对路径适用于开发环境和PyInstaller打包环境"""
@@ -19,7 +19,18 @@ def resource_path(relative_path):
base_path = getattr(sys, '_MEIPASS', os.path.dirname(sys.executable))
else:
# 在开发环境中运行
base_path = os.path.dirname(os.path.abspath(__file__))
base_path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
# 现在base_path是项目根目录我们需要进入source目录
base_path = os.path.join(base_path, 'source')
# 处理特殊情况
if relative_path == "aria2c.exe":
return os.path.join(base_path, 'bin', relative_path)
elif relative_path == "cfst.exe":
return os.path.join(base_path, 'bin', relative_path)
elif relative_path == "ip.txt" or relative_path == "ipv6.txt":
return os.path.join(base_path, 'resources', 'data', relative_path)
return os.path.join(base_path, relative_path)
def load_base64_image(base64_str):
@@ -224,6 +235,7 @@ class HostsManager:
self.backup_path = os.path.join(os.path.dirname(self.hosts_path), f'hosts.bak.{APP_NAME}')
self.original_content = None
self.modified = False
self.modified_hostnames = set() # 跟踪被修改的主机名
def backup(self):
if not AdminPrivileges().is_admin():
@@ -241,6 +253,48 @@ class HostsManager:
msg_box = msgbox_frame(f"错误 - {APP_NAME}", f"\n无法备份hosts文件请检查权限。\n\n【错误信息】:{e}\n", QtWidgets.QMessageBox.StandardButton.Ok)
msg_box.exec()
return False
def clean_hostname_entries(self, hostname):
"""清理hosts文件中指定域名的所有记录
Args:
hostname: 要清理的域名
Returns:
bool: 清理是否成功
"""
if not self.original_content:
if not self.backup():
return False
# 确保original_content不为None
if not self.original_content:
print("无法读取hosts文件内容操作中止。")
return False
if not AdminPrivileges().is_admin():
print("需要管理员权限来修改hosts文件。")
return False
try:
lines = self.original_content.splitlines()
new_lines = [line for line in lines if hostname not in line]
# 如果没有变化,不需要写入
if len(new_lines) == len(lines):
print(f"Hosts文件中没有找到 {hostname} 的记录")
return True
with open(self.hosts_path, 'w', encoding='utf-8') as f:
f.write('\n'.join(new_lines))
# 更新原始内容
self.original_content = '\n'.join(new_lines)
print(f"已从hosts文件中清理 {hostname} 的记录")
return True
except IOError as e:
print(f"清理hosts文件失败: {e}")
return False
def apply_ip(self, hostname, ip_address):
if not self.original_content:
@@ -256,17 +310,23 @@ class HostsManager:
return False
try:
lines = self.original_content.splitlines()
new_lines = [line for line in lines if not (hostname in line and line.strip().startswith(ip_address))]
# 首先清理已有的同域名记录
self.clean_hostname_entries(hostname)
# 然后添加新记录
lines = self.original_content.splitlines()
new_entry = f"{ip_address}\t{hostname}"
new_lines.append(f"\n# Added by {APP_NAME}")
new_lines.append(new_entry)
lines.append(f"\n# Added by {APP_NAME}")
lines.append(new_entry)
with open(self.hosts_path, 'w', encoding='utf-8') as f:
f.write('\n'.join(new_lines))
f.write('\n'.join(lines))
# 更新原始内容
self.original_content = '\n'.join(lines)
self.modified = True
# 记录被修改的主机名,用于最终清理
self.modified_hostnames.add(hostname)
print(f"Hosts文件已更新: {new_entry}")
return True
except IOError as e:
@@ -275,6 +335,55 @@ class HostsManager:
msg_box.exec()
return False
def check_and_clean_all_entries(self):
"""检查并清理所有由本应用程序添加的hosts记录
Returns:
bool: 清理是否成功
"""
if not AdminPrivileges().is_admin():
print("需要管理员权限来检查和清理hosts文件。")
return False
try:
# 读取当前hosts文件内容
with open(self.hosts_path, 'r', encoding='utf-8') as f:
current_content = f.read()
lines = current_content.splitlines()
new_lines = []
skip_next = False
for line in lines:
# 如果上一行是我们的注释标记,跳过当前行
if skip_next:
skip_next = False
continue
# 检查是否是我们添加的注释行
if f"# Added by {APP_NAME}" in line:
skip_next = True # 跳过下一行实际的hosts记录
continue
# 保留其他所有行
new_lines.append(line)
# 检查是否有变化
if len(new_lines) == len(lines):
print("Hosts文件中没有找到由本应用添加的记录")
return True
# 写回清理后的内容
with open(self.hosts_path, 'w', encoding='utf-8') as f:
f.write('\n'.join(new_lines))
print(f"已清理所有由 {APP_NAME} 添加的hosts记录")
return True
except IOError as e:
print(f"检查和清理hosts文件失败: {e}")
return False
def restore(self):
if not self.modified:
if os.path.exists(self.backup_path):
@@ -282,6 +391,8 @@ class HostsManager:
os.remove(self.backup_path)
except OSError:
pass
# 即使没有修改过,也检查一次是否有残留
self.check_and_clean_all_entries()
return True
if not AdminPrivileges().is_admin():
@@ -299,6 +410,8 @@ class HostsManager:
os.remove(self.backup_path)
except OSError:
pass
# 恢复后再检查一次是否有残留
self.check_and_clean_all_entries()
return True
except IOError as e:
print(f"从内存恢复hosts文件失败: {e}")
@@ -309,6 +422,8 @@ class HostsManager:
def restore_from_backup_file(self):
if not os.path.exists(self.backup_path):
print("未找到hosts备份文件无法恢复。")
# 即使没有备份文件,也尝试清理可能的残留
self.check_and_clean_all_entries()
return False
try:
with open(self.backup_path, 'r', encoding='utf-8') as bf:
@@ -318,11 +433,15 @@ class HostsManager:
os.remove(self.backup_path)
self.modified = False
print("Hosts文件已从备份文件恢复。")
# 恢复后再检查一次是否有残留
self.check_and_clean_all_entries()
return True
except (IOError, OSError) as e:
print(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.exec()
# 尽管恢复失败,仍然尝试清理可能的残留
self.check_and_clean_all_entries()
return False
def censor_url(text):

19
source/utils/logger.py Normal file
View File

@@ -0,0 +1,19 @@
from .helpers import censor_url
class Logger:
def __init__(self, filename, stream):
self.terminal = stream
self.log = open(filename, "w", encoding="utf-8")
def write(self, message):
censored_message = censor_url(message)
self.terminal.write(censored_message)
self.log.write(censored_message)
self.flush()
def flush(self):
self.terminal.flush()
self.log.flush()
def close(self):
self.log.close()

View File

@@ -0,0 +1,14 @@
from .hash_thread import HashThread
from .extraction_thread import ExtractionThread
from .config_fetch_thread import ConfigFetchThread
from .ip_optimizer import IpOptimizerThread
from .download import DownloadThread, ProgressWindow
__all__ = [
'IpOptimizerThread',
'HashThread',
'ExtractionThread',
'ConfigFetchThread',
'DownloadThread',
'ProgressWindow'
]

View File

@@ -0,0 +1,52 @@
import json
import requests
from PySide6.QtCore import QThread, Signal
class ConfigFetchThread(QThread):
finished = Signal(object, str) # data, error_message
def __init__(self, url, headers, debug_mode=False, parent=None):
super().__init__(parent)
self.url = url
self.headers = headers
self.debug_mode = debug_mode
def run(self):
try:
if self.debug_mode:
print("--- Starting to fetch cloud config ---")
print(f"DEBUG: Requesting URL: {self.url}")
print(f"DEBUG: Using Headers: {self.headers}")
response = requests.get(self.url, headers=self.headers, timeout=10)
if self.debug_mode:
print(f"DEBUG: Response Status Code: {response.status_code}")
print(f"DEBUG: Response Headers: {response.headers}")
print(f"DEBUG: Response Text: {response.text}")
response.raise_for_status()
# 首先总是尝试解析JSON
config_data = response.json()
# 检查是否是要求更新的错误信息
if config_data.get("message") == "请使用最新版本的FRAISEMOE Addons Installer NEXT进行下载安装":
self.finished.emit(None, "update_required")
return
# 检查是否是有效的配置文件
required_keys = [f"vol.{i+1}.data" for i in range(4)] + ["after.data"]
missing_keys = [key for key in required_keys if key not in config_data]
if missing_keys:
self.finished.emit(None, f"missing_keys:{','.join(missing_keys)}")
return
self.finished.emit(config_data, "")
except requests.exceptions.RequestException as e:
self.finished.emit(None, f"网络请求失败: {e}")
except (ValueError, json.JSONDecodeError) as e:
self.finished.emit(None, f"JSON解析失败: {e}")
finally:
if self.debug_mode:
print("--- Finished fetching cloud config ---")

View File

@@ -7,7 +7,7 @@ from PySide6 import QtCore, QtWidgets
from PySide6.QtCore import (Qt, Signal, QThread, QTimer)
from PySide6.QtWidgets import (QLabel, QProgressBar, QVBoxLayout, QDialog)
from utils import resource_path
from config import APP_NAME, UA
from data.config import APP_NAME, UA
# 下载线程类
class DownloadThread(QThread):

View File

@@ -0,0 +1,31 @@
import os
import shutil
import py7zr
from PySide6.QtCore import QThread, Signal
from data.config import PLUGIN, GAME_INFO
class ExtractionThread(QThread):
finished = Signal(bool, str, str) # success, error_message, game_version
def __init__(self, _7z_path, game_folder, plugin_path, game_version, parent=None):
super().__init__(parent)
self._7z_path = _7z_path
self.game_folder = game_folder
self.plugin_path = plugin_path
self.game_version = game_version
def run(self):
try:
with py7zr.SevenZipFile(self._7z_path, mode="r") as archive:
archive.extractall(path=PLUGIN)
os.makedirs(self.game_folder, exist_ok=True)
shutil.copy(self.plugin_path, self.game_folder)
if self.game_version == "NEKOPARA After":
sig_path = os.path.join(PLUGIN, GAME_INFO[self.game_version]["sig_path"])
shutil.copy(sig_path, self.game_folder)
self.finished.emit(True, "", self.game_version)
except (py7zr.Bad7zFile, FileNotFoundError, Exception) as e:
self.finished.emit(False, f"\n文件操作失败,请重试\n\n【错误信息】:{e}\n", self.game_version)

View File

@@ -0,0 +1,28 @@
from PySide6.QtCore import QThread, Signal
from utils import HashManager
from data.config import BLOCK_SIZE
class HashThread(QThread):
pre_finished = Signal(dict)
after_finished = Signal(dict)
def __init__(self, mode, install_paths, plugin_hash, installed_status, parent=None):
super().__init__(parent)
self.mode = mode
self.install_paths = install_paths
self.plugin_hash = plugin_hash
self.installed_status = installed_status
# 每个线程都应该有自己的HashManager实例
self.hash_manager = HashManager(BLOCK_SIZE)
def run(self):
if self.mode == "pre":
updated_status = self.hash_manager.cfg_pre_hash_compare(
self.install_paths, self.plugin_hash, self.installed_status
)
self.pre_finished.emit(updated_status)
elif self.mode == "after":
result = self.hash_manager.cfg_after_hash_compare(
self.install_paths, self.plugin_hash, self.installed_status
)
self.after_finished.emit(result)

View File

@@ -5,6 +5,7 @@ import sys
import time
from urllib.parse import urlparse
from PySide6.QtCore import QThread, Signal
from utils import resource_path
class IpOptimizer:
@@ -28,13 +29,15 @@ class IpOptimizer:
return None
ip_txt_path = resource_path("ip.txt")
# 正确的参数设置根据cfst帮助文档
command = [
cst_path,
"-p", "1",
"-o", "",
"-url", url,
"-f", ip_txt_path,
"-dd",
"-n", "500", # 延迟测速线程数 (默认200)
"-p", "1", # 显示结果数量 (默认10个)
"-url", url, # 指定测速地址
"-f", ip_txt_path, # IP文件
"-dd", # 禁用下载测速,按延迟排序
]
creation_flags = subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0
@@ -53,17 +56,13 @@ class IpOptimizer:
bufsize=0
)
# 立即向 stdin 发送换行符,以便程序在 Windows 下正常退出
if self.process.stdin:
try:
self.process.stdin.write('\n')
self.process.stdin.flush()
except:
pass
finally:
self.process.stdin.close()
ip_pattern = re.compile(r'^\s*([\d\.]+)\s+\d+\s+\d+\s+[\d\.]+%?\s+[\d\.]+\s+[\d\.]+\s+.*$')
# 更新正则表达式以匹配cfst输出中的IP格式
# 匹配格式: IP地址在行首后面跟着一些数字和文本
ip_pattern = re.compile(r'^(\d+\.\d+\.\d+\.\d+)\s+.*')
# 标记是否已经找到结果表头和完成标记
found_header = False
found_completion = False
stdout = self.process.stdout
if not stdout:
@@ -72,7 +71,7 @@ class IpOptimizer:
optimal_ip = None
timeout_counter = 0
max_timeout = 60
max_timeout = 300 # 增加超时时间到5分钟
while True:
if self.process.poll() is not None:
@@ -97,16 +96,47 @@ class IpOptimizer:
cleaned_line = line.strip()
if cleaned_line:
print(cleaned_line)
match = ip_pattern.match(cleaned_line)
if match:
optimal_ip = match.group(1)
print(f"找到最优 IP: {optimal_ip}, 正在终止测速进程...")
break
# 检测结果表头
if "IP 地址" in cleaned_line and "平均延迟" in cleaned_line:
print("检测到IP结果表头准备获取IP地址...")
found_header = True
continue
# 检测完成标记
if "完整测速结果已写入" in cleaned_line or "按下 回车键 或 Ctrl+C 退出" in cleaned_line:
print("检测到测速完成信息")
found_completion = True
# 如果已经找到了IP可以退出了
if optimal_ip:
break
# 已找到表头后尝试匹配IP地址行
if found_header:
match = ip_pattern.search(cleaned_line)
if match and not optimal_ip: # 只保存第一个匹配的IP最优IP
optimal_ip = match.group(1)
print(f"找到最优 IP: {optimal_ip}")
# 如果已经看到完成标记,可以退出了
if found_completion:
break
except Exception as e:
print(f"读取输出时发生错误: {e}")
break
# 确保完全读取输出后再发送退出信号
if self.process and self.process.poll() is None:
try:
if self.process.stdin and not self.process.stdin.closed:
print("发送退出信号...")
self.process.stdin.write('\n')
self.process.stdin.flush()
except:
pass
self.stop()
print("--- CloudflareSpeedTest 执行结束 ---")
@@ -135,6 +165,24 @@ class IpOptimizer:
self.process.wait()
print("CloudflareSpeedTest 进程已终止。")
class IpOptimizerThread(QThread):
"""用于在后台线程中运行IP优化的类"""
finished = Signal(str)
def __init__(self, url, parent=None):
super().__init__(parent)
self.url = url
self.optimizer = IpOptimizer()
def run(self):
optimal_ip = self.optimizer.get_optimal_ip(self.url)
self.finished.emit(optimal_ip if optimal_ip else "")
def stop(self):
self.optimizer.stop()
if __name__ == '__main__':
# 用于直接测试此模块
test_url = "https://speed.cloudflare.com/__down?during=download&bytes=104857600"
@@ -143,4 +191,4 @@ if __name__ == '__main__':
if ip:
print(f"{test_url} 找到的最优 IP 是: {ip}")
else:
print(f"未能为 {test_url} 找到最优 IP。")
print(f"未能为 {test_url} 找到最优 IP。")