update:目录排序
171
.gitignore
vendored
Normal 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
|
||||
254
core.py
@@ -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
@@ -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()
|
||||
@@ -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)
|
||||
0
IMG/After/voaf_ga01.jpg → source/IMG/After/voaf_ga01.jpg
Executable file → Normal file
|
Before Width: | Height: | Size: 162 KiB After Width: | Height: | Size: 162 KiB |
0
IMG/After/voaf_ga02.jpg → source/IMG/After/voaf_ga02.jpg
Executable file → Normal 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
|
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
|
Before Width: | Height: | Size: 250 KiB After Width: | Height: | Size: 250 KiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 229 KiB After Width: | Height: | Size: 229 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
BIN
source/IMG/ICO/icon.ico
Normal file
|
After Width: | Height: | Size: 264 KiB |
BIN
source/IMG/ICO/icon.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 8.6 KiB |
0
IMG/LOGO/vo01_logo.png → source/IMG/LOGO/vo01_logo.png
Executable file → Normal file
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 51 KiB |
0
IMG/LOGO/vo02_logo.png → source/IMG/LOGO/vo02_logo.png
Executable file → Normal file
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
0
IMG/LOGO/vo03_logo.png → source/IMG/LOGO/vo03_logo.png
Executable file → Normal file
|
Before Width: | Height: | Size: 177 KiB After Width: | Height: | Size: 177 KiB |
0
IMG/LOGO/vo04_logo.png → source/IMG/LOGO/vo04_logo.png
Executable file → Normal file
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
0
IMG/LOGO/voaf_logo.png → source/IMG/LOGO/voaf_logo.png
Executable file → Normal file
|
Before Width: | Height: | Size: 327 KiB After Width: | Height: | Size: 327 KiB |
0
IMG/vol4/vo04_ga01.jpg → source/IMG/vol4/vo04_ga01.jpg
Executable file → Normal file
|
Before Width: | Height: | Size: 166 KiB After Width: | Height: | Size: 166 KiB |
0
IMG/vol4/vo04_ga05.jpg → source/IMG/vol4/vo04_ga05.jpg
Executable file → Normal file
|
Before Width: | Height: | Size: 184 KiB After Width: | Height: | Size: 184 KiB |
0
IMG/vol4/vo04_ga06.jpg → source/IMG/vol4/vo04_ga06.jpg
Executable file → Normal file
|
Before Width: | Height: | Size: 197 KiB After Width: | Height: | Size: 197 KiB |
0
IMG/vol4/vo04_ga07.jpg → source/IMG/vol4/vo04_ga07.jpg
Executable file → Normal file
|
Before Width: | Height: | Size: 166 KiB After Width: | Height: | Size: 166 KiB |
@@ -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
@@ -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())
|
||||
8419
source/nuitka-crash-report.xml
Normal file
14
source/pic_data.py
Normal 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
@@ -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
|
||||