update:目录排序

This commit is contained in:
2025-07-04 11:17:25 +08:00
parent fec5ef8da2
commit fbb49974b9
37 changed files with 9288 additions and 296 deletions

171
.gitignore vendored Normal file
View File

@@ -0,0 +1,171 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
#uv.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
.pdm.toml
.pdm-python
.pdm-build/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# PyPI configuration file
.pypirc

Binary file not shown.

254
core.py
View File

@@ -1,254 +0,0 @@
import os
import shutil
import requests
import py7zr
import hashlib
import psutil
import ctypes
import base64
from PySide6.QtWidgets import (QMessageBox, QFileDialog, QProgressDialog)
from PySide6.QtCore import QThread, Signal
# 配置信息(完全保持不变)
app_data = {
"APP_VERSION": "4.10.0.17496",
"APP_NAME": "@FRAISEMOE Addons Installer",
"TEMP": "TEMP",
"CACHE": "FRAISEMOE",
"PLUGIN": "PLUGIN",
"CONFIG_URL": "aHR0cHM6Ly9hcmNoaXZlLm92b2Zpc2guY29tL2FwaS93aWRnZXQvbmVrb3BhcmEvZG93bmxvYWRfdXJsLmpzb24=",
"UA": "TW96aWxsYS81LjAgKExpbnV4IGRlYmlhbjEyIEZyYWlzZU1vZS1BY2NlcHQpIEdlY2tvLzIwMTAwMTAxIEZpcmVmb3gvMTE0LjA=",
"game_info": {
"NEKOPARA Vol.1": {
"exe": "nekopara_vol1.exe",
"hash": "04b48b231a7f34431431e5027fcc7b27affaa951b8169c541709156acf754f3e",
"install_path": "NEKOPARA Vol. 1/adultsonly.xp3",
"plugin_path": "vol.1/adultsonly.xp3",
},
"NEKOPARA Vol.2": {
"exe": "nekopara_vol2.exe",
"hash": "b9c00a2b113a1e768bf78400e4f9075ceb7b35349cdeca09be62eb014f0d4b42",
"install_path": "NEKOPARA Vol. 2/adultsonly.xp3",
"plugin_path": "vol.2/adultsonly.xp3",
},
"NEKOPARA Vol.3": {
"exe": "NEKOPARAvol3.exe",
"hash": "2ce7b223c84592e1ebc3b72079dee1e5e8d064ade15723328a64dee58833b9d5",
"install_path": "NEKOPARA Vol. 3/update00.int",
"plugin_path": "vol.3/update00.int",
},
"NEKOPARA Vol.4": {
"exe": "nekopara_vol4.exe",
"hash": "4a4a9ae5a75a18aacbe3ab0774d7f93f99c046afe3a777ee0363e8932b90f36a",
"install_path": "NEKOPARA Vol. 4/vol4adult.xp3",
"plugin_path": "vol.4/vol4adult.xp3",
},
},
}
def decode_base64(encoded_str):
"""Base64解码函数"""
return base64.b64decode(encoded_str).decode("utf-8")
class DownloadThread(QThread):
progress_updated = Signal(int)
download_finished = Signal(bool, str)
def __init__(self, url, save_path, headers):
super().__init__()
self.url = url
self.save_path = save_path
self.headers = headers
def run(self):
try:
response = requests.get(self.url, headers=self.headers, stream=True)
response.raise_for_status()
total_size = int(response.headers.get('content-length', 0))
downloaded = 0
with open(self.save_path, 'wb') as f:
for chunk in response.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
downloaded += len(chunk)
progress = int((downloaded / total_size) * 100)
self.progress_updated.emit(progress)
self.download_finished.emit(True, "")
except Exception as e:
self.download_finished.emit(False, str(e))
class AppCore:
def __init__(self, ui):
self.ui = ui
self.install_dir = ""
self.temp_dir = os.path.join(os.getenv(app_data["TEMP"]), app_data["CACHE"])
self.plugin_dir = os.path.join(self.temp_dir, app_data["PLUGIN"])
self._create_dirs() # 确保这个方法被正确定义
# 解码配置
self.CONFIG_URL = decode_base64(app_data["CONFIG_URL"])
self.UA = decode_base64(app_data["UA"]) + f" FraiseMoe/{app_data['APP_VERSION']}"
self.GAME_INFO = app_data["game_info"]
self.current_download_thread = None
def _create_dirs(self):
"""创建必要的目录"""
os.makedirs(self.plugin_dir, exist_ok=True)
def start_installation(self):
"""开始安装流程"""
if not self._select_install_dir():
return
if not self._check_processes():
return
self._download_and_install()
def _select_install_dir(self):
"""选择安装目录"""
dir_path = QFileDialog.getExistingDirectory(
self.ui, "选择游戏安装目录"
)
if not dir_path:
QMessageBox.warning(self.ui, "警告", "必须选择安装目录")
return False
self.install_dir = dir_path
return True
def _check_processes(self):
"""检查游戏进程"""
for proc in psutil.process_iter(['name']):
if proc.info['name'] in [info['exe'] for info in self.GAME_INFO.values()]:
reply = QMessageBox.question(
self.ui, "警告",
"检测到游戏正在运行,需要关闭才能继续安装",
QMessageBox.Yes | QMessageBox.No
)
if reply == QMessageBox.Yes:
try:
proc.kill()
except:
QMessageBox.critical(
self.ui, "错误",
"无法关闭游戏进程,请手动关闭"
)
return False
else:
return False
return True
def _get_download_config(self):
"""获取下载配置"""
try:
headers = {"User-Agent": self.UA}
response = requests.get(self.CONFIG_URL, headers=headers, timeout=10)
response.raise_for_status()
return response.json()
except Exception as e:
QMessageBox.critical(
self.ui, "错误",
f"获取下载配置失败: {str(e)}"
)
return None
def _download_and_install(self):
"""下载并安装文件"""
config = self._get_download_config()
if not config:
return
# 创建进度对话框
progress = QProgressDialog("下载补丁文件中...", "取消", 0, 100, self.ui)
progress.setWindowTitle("安装进度")
progress.setAutoClose(True)
# 为每个游戏版本下载补丁
for game_name, game_info in self.GAME_INFO.items():
vol_key = f"vol.{game_name.split()[-1]}.data"
if vol_key not in config:
continue
download_url = config[vol_key]["url"]
temp_file = os.path.join(self.plugin_dir, f"{vol_key}.7z")
# 启动下载线程
self.current_download_thread = DownloadThread(
download_url,
temp_file,
{"User-Agent": self.UA}
)
self.current_download_thread.progress_updated.connect(progress.setValue)
self.current_download_thread.download_finished.connect(
lambda success, err, f=temp_file, g=game_name, i=game_info:
self._handle_download_result(success, err, f, g, i)
)
self.current_download_thread.start()
progress.exec_()
def _handle_download_result(self, success, error, temp_file, game_name, game_info):
"""处理下载结果"""
if not success:
QMessageBox.critical(self.ui, "下载失败", f"错误: {error}")
return
try:
# 解压文件
with py7zr.SevenZipFile(temp_file, mode='r') as archive:
archive.extractall(path=self.plugin_dir)
# 复制到游戏目录
plugin_path = os.path.join(self.plugin_dir, game_info["plugin_path"])
install_path = os.path.join(self.install_dir, game_info["install_path"])
os.makedirs(os.path.dirname(install_path), exist_ok=True)
shutil.copy(plugin_path, install_path)
# 验证哈希
if self._verify_hash(install_path, game_info["hash"]):
QMessageBox.information(
self.ui, "安装完成",
f"{game_name} 补丁已成功安装!"
)
else:
QMessageBox.warning(
self.ui, "警告",
f"{game_name} 文件校验失败,安装可能不完整"
)
except Exception as e:
QMessageBox.critical(
self.ui, "安装失败",
f"安装过程中出错: {str(e)}"
)
finally:
# 清理临时文件
if os.path.exists(temp_file):
os.remove(temp_file)
def _verify_hash(self, file_path, expected_hash):
"""验证文件哈希"""
if not os.path.exists(file_path):
return False
sha256_hash = hashlib.sha256()
with open(file_path, "rb") as f:
for byte_block in iter(lambda: f.read(8192), b""):
sha256_hash.update(byte_block)
return sha256_hash.hexdigest() == expected_hash
def shutdown_app(self):
"""关闭应用程序"""
reply = QMessageBox.question(
self.ui, "退出",
"确定要退出安装程序吗?",
QMessageBox.Yes | QMessageBox.No
)
if reply == QMessageBox.Yes:
# 清理临时目录
if os.path.exists(self.temp_dir):
shutil.rmtree(self.temp_dir)
self.ui.close()

12
main.py
View File

@@ -1,12 +0,0 @@
import sys
from PySide6.QtWidgets import QApplication
from main_window import MainWindow
def main():
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
if __name__ == "__main__":
main()

View File

@@ -1,28 +0,0 @@
from PySide6.QtWidgets import QMainWindow
from PySide6.QtCore import QTimer
from Ui_install import Ui_MainWindows
from animations import MultiStageAnimations
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
# 先初始化UI
self.ui = Ui_MainWindows()
self.ui.setupUi(self)
# 然后初始化动画系统
self.animator = MultiStageAnimations(self.ui)
# 在窗口显示前设置初始状态
self.animator.initialize()
# 窗口显示后延迟100ms启动动画
QTimer.singleShot(100, self.start_animations)
def start_animations(self):
self.animator.start_animations()
def closeEvent(self, event):
self.animator.clear_animations()
super().closeEvent(event)

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

0
IMG/BG/bg1.jpg → source/IMG/BG/bg1.jpg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 571 KiB

After

Width:  |  Height:  |  Size: 571 KiB

0
IMG/BG/bg2.jpg → source/IMG/BG/bg2.jpg Executable file → Normal file
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: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

BIN
source/IMG/ICO/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

BIN
source/IMG/ICO/icon.png Normal file

Binary file not shown.

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

View File

@@ -12,10 +12,10 @@ class MultiStageAnimations:
# 动画时序配置
self.animation_config = {
"logo": {
"delay_after": 800 # Logo动画完成后等待300ms
"delay_after": 1800 # Logo动画完成后等待300ms
},
"mainbg": {
"delay_after": 200 # 主背景淡入完成后等待200ms
"delay_after": 500 # 主背景淡入完成后等待200ms
}
}

605
source/main.py Normal file
View File

@@ -0,0 +1,605 @@
import os
import py7zr
import requests
import shutil
import hashlib
import sys
import base64
import psutil
import ctypes
import concurrent.futures
from PySide6.QtGui import QIcon
from collections import deque
from PySide6.QtCore import ( Qt,
Signal, QThread, QTimer)
from PySide6.QtGui import (QIcon, QPixmap, )
from PySide6.QtWidgets import (QApplication, QLabel, QMainWindow, QMessageBox,
QProgressBar, QVBoxLayout, QFileDialog, QDialog)
from Ui_install import Ui_MainWindows
from animations import MultiStageAnimations
import sys
import os
# 配置信息
app_data = {
"APP_VERSION": "4.10.0.17496",
"APP_NAME": "@FRAISEMOE Addons Installer",
"TEMP": "TEMP",
"CACHE": "FRAISEMOE",
"PLUGIN": "PLUGIN",
"CONFIG_URL": "aHR0cHM6Ly9hcmNoaXZlLm92b2Zpc2guY29tL2FwaS93aWRnZXQvbmVrb3BhcmEvZG93bmxvYWRfdXJsLmpzb24=",
"UA": "TW96aWxsYS81LjAgKExpbnV4IGRlYmlhbjEyIEZyYWlzZU1vZS1BY2NlcHQpIEdlY2tvLzIwMTAwMTAxIEZpcmVmb3gvMTE0LjA=",
"game_info": {
"NEKOPARA Vol.1": {
"exe": "nekopara_vol1.exe",
"hash": "04b48b231a7f34431431e5027fcc7b27affaa951b8169c541709156acf754f3e",
"install_path": "NEKOPARA Vol. 1/adultsonly.xp3",
"plugin_path": "vol.1/adultsonly.xp3",
},
"NEKOPARA Vol.2": {
"exe": "nekopara_vol2.exe",
"hash": "b9c00a2b113a1e768bf78400e4f9075ceb7b35349cdeca09be62eb014f0d4b42",
"install_path": "NEKOPARA Vol. 2/adultsonly.xp3",
"plugin_path": "vol.2/adultsonly.xp3",
},
"NEKOPARA Vol.3": {
"exe": "NEKOPARAvol3.exe",
"hash": "2ce7b223c84592e1ebc3b72079dee1e5e8d064ade15723328a64dee58833b9d5",
"install_path": "NEKOPARA Vol. 3/update00.int",
"plugin_path": "vol.3/update00.int",
},
"NEKOPARA Vol.4": {
"exe": "nekopara_vol4.exe",
"hash": "4a4a9ae5a75a18aacbe3ab0774d7f93f99c046afe3a777ee0363e8932b90f36a",
"install_path": "NEKOPARA Vol. 4/vol4adult.xp3",
"plugin_path": "vol.4/vol4adult.xp3",
},
},
}
# Base64解码
def decode_base64(encoded_str):
return base64.b64decode(encoded_str).decode("utf-8")
# 全局变量
APP_VERSION = app_data["APP_VERSION"]
APP_NAME = app_data["APP_NAME"]
TEMP = os.getenv(app_data["TEMP"])
CACHE = os.path.join(TEMP, app_data["CACHE"])
PLUGIN = os.path.join(CACHE, app_data["PLUGIN"])
CONFIG_URL = decode_base64(app_data["CONFIG_URL"])
UA = decode_base64(app_data["UA"]) + f" FraiseMoe/{APP_VERSION}"
GAME_INFO = app_data["game_info"]
BLOCK_SIZE = 67108864
HASH_SIZE = 134217728
PLUGIN_HASH = {game: info["hash"] for game, info in GAME_INFO.items()}
PROCESS_INFO = {info["exe"]: game for game, info in GAME_INFO.items()}
def msgbox_frame(title, text, buttons=QMessageBox.StandardButton.NoButton):
msg_box = QMessageBox()
msg_box.setWindowTitle(title)
# 设置弹窗图标
icon_path = "IMG/ICO/icon.png"
if os.path.exists(icon_path):
msg_box.setWindowIcon(QIcon(icon_path))
pixmap = QPixmap(icon_path)
if not pixmap.isNull():
msg_box.setIconPixmap(pixmap.scaled(64, 64, Qt.KeepAspectRatio))
else:
msg_box.setIcon(QMessageBox.Information)
msg_box.setText(text)
msg_box.setStandardButtons(buttons)
return msg_box
# 哈希值计算类
class HashManager:
def __init__(self, HASH_SIZE):
self.HASH_SIZE = HASH_SIZE
def hash_calculate(self, file_path):
sha256_hash = hashlib.sha256()
with open(file_path, "rb") as f:
for byte_block in iter(lambda: f.read(self.HASH_SIZE), b""):
sha256_hash.update(byte_block)
return sha256_hash.hexdigest()
def calculate_hashes_in_parallel(self, file_paths):
with concurrent.futures.ThreadPoolExecutor() as executor:
future_to_file = {
executor.submit(self.hash_calculate, path): path for path in file_paths
}
results = {}
for future in concurrent.futures.as_completed(future_to_file):
file_path = future_to_file[future]
try:
results[file_path] = future.result()
except Exception as e:
results[file_path] = None
msg_box = msgbox_frame(
f"错误 {APP_NAME}",
f"\n文件哈希值计算失败\n\n【错误信息】:{e}\n",
QMessageBox.StandardButton.Ok,
)
msg_box.exec()
return results
def hash_pop_window(self):
msg_box = msgbox_frame(f"通知 {APP_NAME}", "\n正在检验文件状态...\n")
msg_box.show()
QApplication.processEvents()
return msg_box
def cfg_pre_hash_compare(self, install_path, game_version, plugin_hash, installed_status):
if not os.path.exists(install_path):
installed_status[game_version] = False
return
file_hash = self.hash_calculate(install_path)
if file_hash == plugin_hash[game_version]:
installed_status[game_version] = True
else:
reply = msgbox_frame(
f"文件校验 {APP_NAME}",
f"\n检测到 {game_version} 的文件哈希值不匹配,是否重新安装?\n",
QMessageBox.Yes | QMessageBox.No,
).exec()
if reply == QMessageBox.Yes:
installed_status[game_version] = False
else:
installed_status[game_version] = True
def cfg_after_hash_compare(self, install_paths, plugin_hash, installed_status):
passed = True
file_paths = [
install_paths[game] for game in plugin_hash if installed_status.get(game)
]
hash_results = self.calculate_hashes_in_parallel(file_paths)
for game, hash_value in plugin_hash.items():
if installed_status.get(game):
file_hash = hash_results.get(install_paths[game])
if file_hash != hash_value:
msg_box = msgbox_frame(
f"文件校验 {APP_NAME}",
f"\n检测到 {game} 的文件哈希值不匹配\n",
QMessageBox.StandardButton.Ok,
)
msg_box.exec()
installed_status[game] = False
passed = False
break
return passed
# 管理员权限检查类
class AdminPrivileges:
def __init__(self):
self.required_exes = [
"nekopara_vol1.exe",
"nekopara_vol2.exe",
"NEKOPARAvol3.exe",
"nekopara_vol4.exe",
]
def is_admin(self):
try:
return ctypes.windll.shell32.IsUserAnAdmin()
except:
return False
def request_admin_privileges(self):
if not self.is_admin():
msg_box = msgbox_frame(
f"权限检测 {APP_NAME}",
"\n需要管理员权限运行此程序\n",
QMessageBox.Yes | QMessageBox.No,
)
reply = msg_box.exec()
if reply == QMessageBox.Yes:
try:
ctypes.windll.shell32.ShellExecuteW(
None, "runas", sys.executable, " ".join(sys.argv), None, 1
)
except Exception as e:
msg_box = msgbox_frame(
f"错误 {APP_NAME}",
f"\n请求管理员权限失败\n\n【错误信息】:{e}\n",
QMessageBox.StandardButton.Ok,
)
msg_box.exec()
sys.exit(1)
else:
msg_box = msgbox_frame(
f"权限检测 {APP_NAME}",
"\n无法获取管理员权限,程序将退出\n",
QMessageBox.StandardButton.Ok,
)
msg_box.exec()
sys.exit(1)
def check_and_terminate_processes(self):
for proc in psutil.process_iter(["pid", "name"]):
if proc.info["name"] in self.required_exes:
msg_box = msgbox_frame(
f"进程检测 {APP_NAME}",
f"\n检测到游戏正在运行: {proc.info['name']} \n\n是否终止?\n",
QMessageBox.Yes | QMessageBox.No,
)
reply = msg_box.exec()
if reply == QMessageBox.Yes:
try:
proc.terminate()
proc.wait(timeout=3)
except psutil.AccessDenied:
msg_box = msgbox_frame(
f"错误 {APP_NAME}",
f"\n无法关闭游戏: {proc.info['name']} \n\n请手动关闭后重启应用\n",
QMessageBox.StandardButton.Ok,
)
msg_box.exec()
sys.exit(1)
else:
msg_box = msgbox_frame(
f"进程检测 {APP_NAME}",
f"\n未关闭的游戏: {proc.info['name']} \n\n请手动关闭后重启应用\n",
QMessageBox.StandardButton.Ok,
)
msg_box.exec()
sys.exit(1)
# 下载线程类
class DownloadThread(QThread):
progress = Signal(int)
finished = Signal(bool, str)
def __init__(self, url, _7z_path, parent=None):
super().__init__(parent)
self.url = url
self._7z_path = _7z_path
def run(self):
try:
headers = {"User-Agent": UA}
r = requests.get(self.url, headers=headers, stream=True, timeout=10)
r.raise_for_status()
total_size = int(r.headers.get("content-length", 0))
with open(self._7z_path, "wb") as f:
for chunk in r.iter_content(chunk_size=BLOCK_SIZE):
f.write(chunk)
self.progress.emit(f.tell() * 100 // total_size)
self.finished.emit(True, "")
except requests.exceptions.RequestException as e:
self.finished.emit(False, f"\n网络请求错误\n\n【错误信息】: {e}\n")
except Exception as e:
self.finished.emit(False, f"\n未知错误\n\n【错误信息】: {e}\n")
# 下载进度窗口类
class ProgressWindow(QDialog):
def __init__(self, parent=None):
super(ProgressWindow, self).__init__(parent)
self.setWindowTitle(f"下载进度 {APP_NAME}")
self.resize(400, 100)
self.progress_bar_max = 100
self.setWindowFlags(self.windowFlags() & ~Qt.WindowCloseButtonHint)
self.setWindowFlags(self.windowFlags() & ~Qt.WindowSystemMenuHint)
layout = QVBoxLayout()
self.progress_bar = QProgressBar()
self.progress_bar.setValue(0)
self.label = QLabel("\n正在下载...\n")
layout.addWidget(self.label)
layout.addWidget(self.progress_bar)
self.setLayout(layout)
def setmaxvalue(self, value):
self.progress_bar_max = value
self.progress_bar.setMaximum(value)
def setprogressbarval(self, value):
self.progress_bar.setValue(value)
if value == self.progress_bar_max:
QTimer.singleShot(2000, self.close)
# 主窗口类
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
# 初始化UI (从Ui_install.py导入)
self.ui = Ui_MainWindows()
self.ui.setupUi(self)
icon_path = "IMG/ICO/icon.png"
if os.path.exists(icon_path):
self.setWindowIcon(QIcon(icon_path))
# 设置窗口标题为APP_NAME加版本号
self.setWindowTitle(f"{APP_NAME} v{APP_VERSION}")
# 初始化动画系统 (从animations.py导入)
self.animator = MultiStageAnimations(self.ui)
# 初始化功能变量
self.selected_folder = ""
self.installed_status = {f"NEKOPARA Vol.{i}": False for i in range(1, 5)}
self.download_queue = deque()
self.current_download_thread = None
self.hash_manager = HashManager(BLOCK_SIZE)
# 检查管理员权限和进程
admin_privileges = AdminPrivileges()
admin_privileges.request_admin_privileges()
admin_privileges.check_and_terminate_processes()
# 创建缓存目录
if not os.path.exists(PLUGIN):
try:
os.makedirs(PLUGIN)
except OSError as e:
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.file_dialog)
self.ui.exit_btn.clicked.connect(self.shutdown_app)
# 在窗口显示前设置初始状态
self.animator.initialize()
# 窗口显示后延迟100ms启动动画
QTimer.singleShot(100, self.start_animations)
def start_animations(self):
self.animator.start_animations()
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:
QMessageBox.warning(
self, f"通知 {APP_NAME}", "\n未选择任何目录,请重新选择\n"
)
return
self.download_action()
def get_download_url(self) -> dict:
try:
headers = {"User-Agent": UA}
response = requests.get(CONFIG_URL, headers=headers, timeout=10)
response.raise_for_status()
config_data = response.json()
if not all(f"vol.{i+1}.data" in config_data for i in range(4)):
raise ValueError("配置文件数据异常")
return {
f"vol{i+1}": config_data[f"vol.{i+1}.data"]["url"] for i in range(4)
}
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 = "配置文件异常,无法解析错误信息"
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:
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
progress_window = ProgressWindow(self)
progress_window.show()
self.current_download_thread = DownloadThread(url, _7z_path, self)
self.current_download_thread.progress.connect(progress_window.setprogressbarval)
self.current_download_thread.finished.connect(
lambda success, error: self.install_setting(
success,
error,
progress_window,
game_folder,
game_version,
_7z_path,
plugin_path,
)
)
self.current_download_thread.start()
def install_setting(
self,
success,
error,
progress_window,
game_folder,
game_version,
_7z_path,
plugin_path,
):
progress_window.close()
if success:
try:
msg_box = self.hash_manager.hash_pop_window()
QApplication.processEvents()
with py7zr.SevenZipFile(_7z_path, mode="r") as archive:
archive.extractall(path=PLUGIN)
shutil.copy(plugin_path, game_folder)
self.installed_status[game_version] = True
QMessageBox.information(
self, f"通知 {APP_NAME}", f"\n{game_version} 补丁已安装\n"
)
except (py7zr.Bad7zFile, FileNotFoundError, Exception) as e:
QMessageBox.critical(
self,
f"错误 {APP_NAME}",
f"\n文件操作失败,请重试\n\n【错误信息】:{e}\n",
)
finally:
msg_box.close()
else:
QMessageBox.critical(
self,
f"错误 {APP_NAME}",
f"\n文件获取失败\n网络状态异常或服务器故障\n\n【错误信息】:{error}\n",
)
self.next_download_task()
def pre_hash_compare(self, install_path, game_version, plugin_hash):
msg_box = self.hash_manager.hash_pop_window()
self.hash_manager.cfg_pre_hash_compare(
install_path, game_version, plugin_hash, self.installed_status
)
msg_box.close()
def download_action(self):
install_paths = self.get_install_paths()
for game_version, install_path in install_paths.items():
self.pre_hash_compare(install_path, game_version, PLUGIN_HASH)
config = self.get_download_url()
if not config:
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[game_version]:
url = config[f"vol{i}"]
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)
)
self.next_download_task()
def next_download_task(self):
if not self.download_queue:
self.after_hash_compare(PLUGIN_HASH)
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 after_hash_compare(self, plugin_hash):
msg_box = self.hash_manager.hash_pop_window()
result = self.hash_manager.cfg_after_hash_compare(
self.get_install_paths(), plugin_hash, self.installed_status
)
msg_box.close()
self.show_result()
return 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]]
)
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 closeEvent(self, event):
self.shutdown_app(event)
def shutdown_app(self, event=None):
reply = QMessageBox.question(
self,
"退出程序",
"\n是否确定退出?\n",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No,
)
if reply == QMessageBox.Yes:
if (
self.current_download_thread
and self.current_download_thread.isRunning()
):
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:
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)
else:
if event:
event.ignore()
if __name__ == "__main__":
app = QApplication([])
window = MainWindow()
window.show()
sys.exit(app.exec())

File diff suppressed because it is too large Load Diff

14
source/pic_data.py Normal file
View File

@@ -0,0 +1,14 @@
img_data = {
"firstbg": "",
"vol1": "",
"vol2": "",
"vol3": "",
"vol4": "",
"after": "",
"defaultbg": "",
"menubg": "",
"start_install_btn": "",
"exit_btn": "",
"icon": ""
}

77
source/requirements.txt Normal file
View File

@@ -0,0 +1,77 @@
altgraph==0.17.4
annotated-types==0.7.0
anyio==4.9.0
async-timeout==5.0.1
auto-py-to-exe==2.46.0
bottle==0.13.4
bottle-websocket==0.2.9
Brotli==1.1.0
certifi==2025.6.15
cffi==1.17.1
charset-normalizer==3.4.2
click==8.2.1
colorama==0.4.6
colorthief==0.2.1
darkdetect==0.8.0
Eel==0.18.2
exceptiongroup==1.3.0
fastapi==0.115.14
future==1.0.0
gevent==25.5.1
gevent-websocket==0.10.1
greenlet==3.2.3
h11==0.16.0
httpcore==1.0.9
httpx==0.28.1
idna==3.10
importlib_resources==6.5.2
inflate64==1.0.3
multivolumefile==0.2.3
Nuitka==2.7.11
numpy==2.2.6
ordered-set==4.1.0
packaging==25.0
pefile==2023.2.7
pillow==11.3.0
playwright==1.53.0
psutil==7.0.0
py7zr==1.0.0
pybcj==1.0.6
pycparser==2.22
pycryptodomex==3.23.0
pydantic==2.11.7
pydantic_core==2.33.2
pyee==13.0.0
pyinstaller==6.14.1
pyinstaller-hooks-contrib==2025.5
pyparsing==3.2.3
pyppmd==1.2.0
PyQt-SiliconUI==1.0.1
PyQt5==5.15.11
PyQt5-Qt5==5.15.2
PyQt5_sip==12.17.0
PySide6==6.9.1
PySide6-Fluent-Widgets==1.8.3
PySide6_Addons==6.9.1
PySide6_Essentials==6.9.1
PySideSix-Frameless-Window==0.7.3
python-dateutil==2.9.0.post0
python-multipart==0.0.20
pywin32==310
pywin32-ctypes==0.2.3
pyzstd==0.17.0
redis==6.2.0
requests==2.32.4
scipy==1.15.3
shiboken6==6.9.1
six==1.17.0
sniffio==1.3.1
starlette==0.46.2
texttable==1.7.0
typing-inspection==0.4.1
typing_extensions==4.14.0
urllib3==2.5.0
uvicorn==0.35.0
zope.event==5.1
zope.interface==7.2
zstandard==0.23.0