Update:测试版本
178
.gitignore
vendored
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
|
||||||
|
# 打包封装
|
||||||
|
main.build/
|
||||||
|
main.dist/
|
||||||
|
main.onefile-build/
|
||||||
|
main.exe
|
||||||
28
README.MD
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# 🍓FRAISEMOE Addons Installer🍓
|
||||||
|
|
||||||
|
<!-- PROJECT SHIELDS -->
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://github.com/Akatsuki-Misaki/FRAISEMOE2-Addons-Installer">
|
||||||
|
<img src="https://raw.githubusercontent.com/Yanam1Anna/FRAISEMOE-Addons-Installer/master/introduction_imgs/main.png" alt="Logo">
|
||||||
|
</a>
|
||||||
|
<br />
|
||||||
|
⚠️ 当前分支为测试版本 ⚠️
|
||||||
|
<br />
|
||||||
|
</p>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎉 特别鸣谢
|
||||||
|
|
||||||
|
### 开发协助(排名不分先后)
|
||||||
|
|
||||||
|
- [Yanam1Anna](https://github.com/Yanam1Anna/):原作者项目的开发
|
||||||
|
|
||||||
|
- [HTony03](https://github.com/HTony03):对于原项目部分源码的重构、逻辑优化和功能实现提供了大力支持。
|
||||||
|
|
||||||
|
- [Akatsuki Misaki](https://github.com/Akatsuki-Misaki):对于本项目云端资源存储提供了大力支持。
|
||||||
|
|
||||||
|
|
||||||
|
## 📖 协议
|
||||||
|
|
||||||
|
此应用根据 [GPL-3.0](https://github.com/Yanam1Anna/FRAISEMOE-Addons-Installer/blob/master/LICENSE) 许可证授权。请参阅 [LICENSE](https://github.com/Yanam1Anna/FRAISEMOE-Addons-Installer/blob/master/LICENSE) 文件了解更多信息。
|
||||||
13
data.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
img_data = {
|
||||||
|
"loadbg": "",
|
||||||
|
"vol1": "",
|
||||||
|
"vol2": "",
|
||||||
|
"vol3": "",
|
||||||
|
"vol4": "",
|
||||||
|
"after": "",
|
||||||
|
"Mainbg": "",
|
||||||
|
"menubg": "",
|
||||||
|
"start_install_btn": "",
|
||||||
|
"exit_btn": "",
|
||||||
|
"icon": "",
|
||||||
|
}
|
||||||
BIN
source/IMG/After/voaf_ga01.jpg
Normal file
|
After Width: | Height: | Size: 162 KiB |
BIN
source/IMG/After/voaf_ga02.jpg
Normal file
|
After Width: | Height: | Size: 179 KiB |
BIN
source/IMG/BG/bg1.jpg
Normal file
|
After Width: | Height: | Size: 571 KiB |
BIN
source/IMG/BG/bg2.jpg
Normal file
|
After Width: | Height: | Size: 250 KiB |
BIN
source/IMG/BG/bg3.jpg
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
source/IMG/BG/bg4.jpg
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
source/IMG/BG/menubg.jpg
Normal file
|
After Width: | Height: | Size: 229 KiB |
BIN
source/IMG/BTN/exit.bmp
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
source/IMG/BTN/start_install.bmp
Normal file
|
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 |
BIN
source/IMG/LOGO/gl_head_logo_jp.png
Normal file
|
After Width: | Height: | Size: 8.6 KiB |
BIN
source/IMG/LOGO/vo01_logo.png
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
source/IMG/LOGO/vo02_logo.png
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
source/IMG/LOGO/vo03_logo.png
Normal file
|
After Width: | Height: | Size: 177 KiB |
BIN
source/IMG/LOGO/vo04_logo.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
source/IMG/LOGO/voaf_logo.png
Normal file
|
After Width: | Height: | Size: 327 KiB |
BIN
source/IMG/vol4/vo04_ga01.jpg
Normal file
|
After Width: | Height: | Size: 166 KiB |
BIN
source/IMG/vol4/vo04_ga05.jpg
Normal file
|
After Width: | Height: | Size: 184 KiB |
BIN
source/IMG/vol4/vo04_ga06.jpg
Normal file
|
After Width: | Height: | Size: 197 KiB |
BIN
source/IMG/vol4/vo04_ga07.jpg
Normal file
|
After Width: | Height: | Size: 166 KiB |
169
source/Ui_install.py
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
################################################################################
|
||||||
|
## Form generated from reading UI file 'install.ui'
|
||||||
|
##
|
||||||
|
## Created by: Qt User Interface Compiler version 6.9.1
|
||||||
|
##
|
||||||
|
## WARNING! All changes made in this file will be lost when recompiling UI file!
|
||||||
|
################################################################################
|
||||||
|
from pic_data import img_data
|
||||||
|
from PySide6.QtGui import QPixmap
|
||||||
|
import base64
|
||||||
|
from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
|
||||||
|
QMetaObject, QObject, QPoint, QRect,
|
||||||
|
QSize, QTime, QUrl, Qt)
|
||||||
|
from PySide6.QtGui import (QAction, QBrush, QColor, QConicalGradient,
|
||||||
|
QCursor, QFont, QFontDatabase, QGradient,
|
||||||
|
QIcon, QImage, QKeySequence, QLinearGradient,
|
||||||
|
QPainter, QPalette, QPixmap, QRadialGradient,
|
||||||
|
QTransform)
|
||||||
|
from PySide6.QtWidgets import (QApplication, QLabel, QMainWindow, QMenu,
|
||||||
|
QMenuBar, QPushButton, QSizePolicy, QWidget)
|
||||||
|
def load_base64_image(base64_str):
|
||||||
|
pixmap = QPixmap()
|
||||||
|
pixmap.loadFromData(base64.b64decode(base64_str))
|
||||||
|
return pixmap
|
||||||
|
|
||||||
|
|
||||||
|
class Ui_MainWindows(object):
|
||||||
|
def setupUi(self, MainWindows):
|
||||||
|
if not MainWindows.objectName():
|
||||||
|
MainWindows.setObjectName(u"MainWindows")
|
||||||
|
MainWindows.setEnabled(True)
|
||||||
|
MainWindows.resize(1024, 576)
|
||||||
|
MainWindows.setMinimumSize(QSize(1024, 576))
|
||||||
|
MainWindows.setMaximumSize(QSize(1024, 576))
|
||||||
|
MainWindows.setMouseTracking(False)
|
||||||
|
MainWindows.setTabletTracking(False)
|
||||||
|
MainWindows.setAcceptDrops(True)
|
||||||
|
MainWindows.setAutoFillBackground(True)
|
||||||
|
MainWindows.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
|
||||||
|
MainWindows.setAnimated(True)
|
||||||
|
MainWindows.setDocumentMode(False)
|
||||||
|
MainWindows.setDockNestingEnabled(False)
|
||||||
|
self.action_2 = QAction(MainWindows)
|
||||||
|
self.action_2.setObjectName(u"action_2")
|
||||||
|
self.centralwidget = QWidget(MainWindows)
|
||||||
|
self.centralwidget.setObjectName(u"centralwidget")
|
||||||
|
self.centralwidget.setAutoFillBackground(True)
|
||||||
|
self.loadbg = QLabel(self.centralwidget)
|
||||||
|
self.loadbg.setObjectName(u"loadbg")
|
||||||
|
self.loadbg.setGeometry(QRect(0, 0, 1031, 561))
|
||||||
|
self.loadbg.setPixmap(load_base64_image(img_data["loadbg"]))
|
||||||
|
self.loadbg.setScaledContents(True)
|
||||||
|
self.vol1bg = QLabel(self.centralwidget)
|
||||||
|
self.vol1bg.setObjectName(u"vol1bg")
|
||||||
|
self.vol1bg.setGeometry(QRect(0, 120, 93, 64))
|
||||||
|
self.vol1bg.setPixmap(load_base64_image(img_data["vol1"]))
|
||||||
|
self.vol1bg.setScaledContents(True)
|
||||||
|
self.vol2bg = QLabel(self.centralwidget)
|
||||||
|
self.vol2bg.setObjectName(u"vol2bg")
|
||||||
|
self.vol2bg.setGeometry(QRect(0, 180, 93, 64))
|
||||||
|
self.vol2bg.setPixmap(load_base64_image(img_data["vol2"]))
|
||||||
|
self.vol2bg.setScaledContents(True)
|
||||||
|
self.vol3bg = QLabel(self.centralwidget)
|
||||||
|
self.vol3bg.setObjectName(u"vol3bg")
|
||||||
|
self.vol3bg.setGeometry(QRect(0, 240, 93, 64))
|
||||||
|
self.vol3bg.setPixmap(load_base64_image(img_data["vol3"]))
|
||||||
|
self.vol3bg.setScaledContents(True)
|
||||||
|
self.vol4bg = QLabel(self.centralwidget)
|
||||||
|
self.vol4bg.setObjectName(u"vol4bg")
|
||||||
|
self.vol4bg.setGeometry(QRect(0, 300, 93, 64))
|
||||||
|
self.vol4bg.setPixmap(load_base64_image(img_data["vol4"]))
|
||||||
|
self.vol4bg.setScaledContents(True)
|
||||||
|
self.afterbg = QLabel(self.centralwidget)
|
||||||
|
self.afterbg.setObjectName(u"afterbg")
|
||||||
|
self.afterbg.setGeometry(QRect(0, 360, 93, 64))
|
||||||
|
self.afterbg.setPixmap(load_base64_image(img_data["after"]))
|
||||||
|
self.afterbg.setScaledContents(True)
|
||||||
|
self.Mainbg = QLabel(self.centralwidget)
|
||||||
|
self.Mainbg.setObjectName(u"Mainbg")
|
||||||
|
self.Mainbg.setGeometry(QRect(0, 0, 1031, 561))
|
||||||
|
self.Mainbg.setPixmap(load_base64_image(img_data["Mainbg"]))
|
||||||
|
self.Mainbg.setScaledContents(True)
|
||||||
|
self.start_install_btn = QPushButton(self.centralwidget)
|
||||||
|
self.start_install_btn.setObjectName(u"start_install_btn")
|
||||||
|
self.start_install_btn.setEnabled(True)
|
||||||
|
self.start_install_btn.setGeometry(QRect(780, 250, 191, 91))
|
||||||
|
self.start_install_btn.setAutoFillBackground(False)
|
||||||
|
start_install_icon = QIcon()
|
||||||
|
start_install_pixmap = load_base64_image(img_data["start_install_btn"])
|
||||||
|
if not start_install_pixmap.isNull():
|
||||||
|
start_install_icon.addPixmap(start_install_pixmap)
|
||||||
|
self.start_install_btn.setIcon(start_install_icon)
|
||||||
|
self.start_install_btn.setIcon(start_install_icon)
|
||||||
|
self.start_install_btn.setIconSize(QSize(189, 110))
|
||||||
|
self.start_install_btn.setCheckable(False)
|
||||||
|
self.start_install_btn.setAutoRepeat(False)
|
||||||
|
self.start_install_btn.setAutoDefault(False)
|
||||||
|
self.start_install_btn.setFlat(True)
|
||||||
|
self.exit_btn = QPushButton(self.centralwidget)
|
||||||
|
self.exit_btn.setObjectName(u"exit_btn")
|
||||||
|
self.exit_btn.setEnabled(True)
|
||||||
|
self.exit_btn.setGeometry(QRect(780, 340, 191, 91))
|
||||||
|
self.exit_btn.setAutoFillBackground(False)
|
||||||
|
exit_icon = QIcon()
|
||||||
|
exit_pixmap = load_base64_image(img_data["exit_btn"])
|
||||||
|
if not exit_pixmap.isNull():
|
||||||
|
exit_icon.addPixmap(exit_pixmap)
|
||||||
|
self.exit_btn.setIcon(exit_icon)
|
||||||
|
self.exit_btn.setIcon(exit_icon)
|
||||||
|
self.exit_btn.setIconSize(QSize(189, 110))
|
||||||
|
self.exit_btn.setCheckable(False)
|
||||||
|
self.exit_btn.setFlat(True)
|
||||||
|
self.menubg = QLabel(self.centralwidget)
|
||||||
|
self.menubg.setObjectName(u"menubg")
|
||||||
|
self.menubg.setGeometry(QRect(710, 0, 321, 561))
|
||||||
|
self.menubg.setPixmap(load_base64_image(img_data["menubg"]))
|
||||||
|
self.menubg.setScaledContents(True)
|
||||||
|
MainWindows.setCentralWidget(self.centralwidget)
|
||||||
|
self.loadbg.raise_()
|
||||||
|
self.vol1bg.raise_()
|
||||||
|
self.vol2bg.raise_()
|
||||||
|
self.vol3bg.raise_()
|
||||||
|
self.vol4bg.raise_()
|
||||||
|
self.afterbg.raise_()
|
||||||
|
self.Mainbg.raise_()
|
||||||
|
self.menubg.raise_()
|
||||||
|
self.start_install_btn.raise_()
|
||||||
|
self.exit_btn.raise_()
|
||||||
|
self.menubar = QMenuBar(MainWindows)
|
||||||
|
self.menubar.setObjectName(u"menubar")
|
||||||
|
self.menubar.setGeometry(QRect(0, 0, 1024, 21))
|
||||||
|
self.menu = QMenu(self.menubar)
|
||||||
|
self.menu.setObjectName(u"menu")
|
||||||
|
self.menu_2 = QMenu(self.menubar)
|
||||||
|
self.menu_2.setObjectName(u"menu_2")
|
||||||
|
MainWindows.setMenuBar(self.menubar)
|
||||||
|
|
||||||
|
self.menubar.addAction(self.menu.menuAction())
|
||||||
|
self.menubar.addAction(self.menu_2.menuAction())
|
||||||
|
self.menu.addSeparator()
|
||||||
|
self.menu.addAction(self.action_2)
|
||||||
|
|
||||||
|
self.retranslateUi(MainWindows)
|
||||||
|
|
||||||
|
QMetaObject.connectSlotsByName(MainWindows)
|
||||||
|
# setupUi
|
||||||
|
|
||||||
|
def retranslateUi(self, MainWindows):
|
||||||
|
MainWindows.setWindowTitle(QCoreApplication.translate("MainWindows", u" UI Test", None))
|
||||||
|
self.action_2.setText(QCoreApplication.translate("MainWindows", u"\u68c0\u67e5\u66f4\u65b0(\u672a\u5b8c\u6210)", None))
|
||||||
|
self.loadbg.setText("")
|
||||||
|
self.vol1bg.setText("")
|
||||||
|
self.vol2bg.setText("")
|
||||||
|
self.vol3bg.setText("")
|
||||||
|
self.vol4bg.setText("")
|
||||||
|
self.afterbg.setText("")
|
||||||
|
self.Mainbg.setText("")
|
||||||
|
#if QT_CONFIG(accessibility)
|
||||||
|
self.start_install_btn.setAccessibleDescription("")
|
||||||
|
#endif // QT_CONFIG(accessibility)
|
||||||
|
self.start_install_btn.setText("")
|
||||||
|
self.exit_btn.setText("")
|
||||||
|
self.menubg.setText("")
|
||||||
|
self.menu.setTitle(QCoreApplication.translate("MainWindows", u"\u8bbe\u7f6e", None))
|
||||||
|
self.menu_2.setTitle(QCoreApplication.translate("MainWindows", u"\u5173\u4e8e", None))
|
||||||
|
# retranslateUi
|
||||||
|
|
||||||
168
source/animations.py
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
from PySide6.QtCore import (QPropertyAnimation, QParallelAnimationGroup,
|
||||||
|
QPoint, QEasingCurve, QTimer)
|
||||||
|
from PySide6.QtWidgets import QGraphicsOpacityEffect
|
||||||
|
|
||||||
|
class MultiStageAnimations:
|
||||||
|
def __init__(self, ui):
|
||||||
|
self.ui = ui
|
||||||
|
# 获取画布尺寸
|
||||||
|
self.canvas_width = ui.centralwidget.width()
|
||||||
|
self.canvas_height = ui.centralwidget.height()
|
||||||
|
|
||||||
|
# 动画时序配置
|
||||||
|
self.animation_config = {
|
||||||
|
"logo": {
|
||||||
|
"delay_after": 2800
|
||||||
|
},
|
||||||
|
"mainbg": {
|
||||||
|
"delay_after": 500
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 第一阶段:Logo动画配置
|
||||||
|
self.logo_widgets = [
|
||||||
|
{"widget": ui.vol1bg, "delay": 0, "duration": 500, "end_pos": QPoint(0, 120)},
|
||||||
|
{"widget": ui.vol2bg, "delay": 80, "duration": 500, "end_pos": QPoint(0, 180)},
|
||||||
|
{"widget": ui.vol3bg, "delay": 160, "duration": 500, "end_pos": QPoint(0, 240)},
|
||||||
|
{"widget": ui.vol4bg, "delay": 240, "duration": 500, "end_pos": QPoint(0, 300)},
|
||||||
|
{"widget": ui.afterbg, "delay": 320, "duration": 500, "end_pos": QPoint(0, 360)}
|
||||||
|
]
|
||||||
|
|
||||||
|
# 第二阶段:菜单元素
|
||||||
|
self.menu_widgets = [
|
||||||
|
{"widget": ui.menubg, "end_pos": QPoint(710, 0), "duration": 600},
|
||||||
|
{"widget": ui.start_install_btn, "end_pos": QPoint(780, 250), "duration": 600},
|
||||||
|
{"widget": ui.exit_btn, "end_pos": QPoint(780, 340), "duration": 600}
|
||||||
|
]
|
||||||
|
|
||||||
|
self.animations = []
|
||||||
|
self.timers = []
|
||||||
|
def initialize(self):
|
||||||
|
"""初始化所有组件状态"""
|
||||||
|
# 设置Mainbg初始状态
|
||||||
|
effect = QGraphicsOpacityEffect(self.ui.Mainbg)
|
||||||
|
effect.setOpacity(0)
|
||||||
|
self.ui.Mainbg.setGraphicsEffect(effect)
|
||||||
|
|
||||||
|
# 初始化Logo位置(移到左侧外)
|
||||||
|
for item in self.logo_widgets:
|
||||||
|
widget = item["widget"]
|
||||||
|
effect = QGraphicsOpacityEffect(widget)
|
||||||
|
effect.setOpacity(0)
|
||||||
|
widget.setGraphicsEffect(effect)
|
||||||
|
widget.move(-widget.width(), item["end_pos"].y())
|
||||||
|
widget.show()
|
||||||
|
print("初始化支持栏动画")
|
||||||
|
|
||||||
|
# 初始化菜单元素(底部外)
|
||||||
|
for item in self.menu_widgets:
|
||||||
|
widget = item["widget"]
|
||||||
|
effect = QGraphicsOpacityEffect(widget)
|
||||||
|
effect.setOpacity(0)
|
||||||
|
widget.setGraphicsEffect(effect)
|
||||||
|
widget.move(widget.x(), self.canvas_height + 100)
|
||||||
|
widget.show()
|
||||||
|
|
||||||
|
def start_logo_animations(self):
|
||||||
|
"""启动Logo动画序列"""
|
||||||
|
for item in self.logo_widgets:
|
||||||
|
timer = QTimer()
|
||||||
|
timer.setSingleShot(True)
|
||||||
|
timer.timeout.connect(
|
||||||
|
lambda w=item["widget"], d=item["duration"], pos=item["end_pos"]:
|
||||||
|
self.animate_logo(w, pos, d)
|
||||||
|
)
|
||||||
|
timer.start(item["delay"])
|
||||||
|
self.timers.append(timer)
|
||||||
|
|
||||||
|
def animate_logo(self, widget, end_pos, duration):
|
||||||
|
"""执行单个Logo动画"""
|
||||||
|
anim_group = QParallelAnimationGroup()
|
||||||
|
|
||||||
|
# 位置动画
|
||||||
|
pos_anim = QPropertyAnimation(widget, b"pos")
|
||||||
|
pos_anim.setDuration(duration)
|
||||||
|
pos_anim.setStartValue(QPoint(-widget.width(), end_pos.y()))
|
||||||
|
pos_anim.setEndValue(end_pos)
|
||||||
|
pos_anim.setEasingCurve(QEasingCurve.OutBack)
|
||||||
|
|
||||||
|
# 透明度动画
|
||||||
|
opacity_anim = QPropertyAnimation(widget.graphicsEffect(), b"opacity")
|
||||||
|
opacity_anim.setDuration(duration)
|
||||||
|
opacity_anim.setStartValue(0)
|
||||||
|
opacity_anim.setEndValue(1)
|
||||||
|
|
||||||
|
anim_group.addAnimation(pos_anim)
|
||||||
|
anim_group.addAnimation(opacity_anim)
|
||||||
|
|
||||||
|
# 最后一个Logo动画完成后添加延迟
|
||||||
|
if widget == self.logo_widgets[-1]["widget"]:
|
||||||
|
anim_group.finished.connect(
|
||||||
|
lambda: QTimer.singleShot(
|
||||||
|
self.animation_config["logo"]["delay_after"],
|
||||||
|
self.start_mainbg_animation
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
anim_group.start()
|
||||||
|
self.animations.append(anim_group)
|
||||||
|
|
||||||
|
def start_mainbg_animation(self):
|
||||||
|
"""启动主背景淡入动画"""
|
||||||
|
main_anim = QPropertyAnimation(self.ui.Mainbg.graphicsEffect(), b"opacity")
|
||||||
|
main_anim.setDuration(800)
|
||||||
|
main_anim.setStartValue(0)
|
||||||
|
main_anim.setEndValue(1)
|
||||||
|
main_anim.finished.connect(self.start_menu_animations)
|
||||||
|
main_anim.start()
|
||||||
|
self.animations.append(main_anim)
|
||||||
|
|
||||||
|
def start_mainbg_animation(self):
|
||||||
|
"""启动主背景淡入动画(带延迟)"""
|
||||||
|
main_anim = QPropertyAnimation(self.ui.Mainbg.graphicsEffect(), b"opacity")
|
||||||
|
main_anim.setDuration(800)
|
||||||
|
main_anim.setStartValue(0)
|
||||||
|
main_anim.setEndValue(1)
|
||||||
|
main_anim.finished.connect(
|
||||||
|
lambda: QTimer.singleShot(
|
||||||
|
self.animation_config["mainbg"]["delay_after"],
|
||||||
|
self.start_menu_animations
|
||||||
|
)
|
||||||
|
)
|
||||||
|
main_anim.start()
|
||||||
|
self.animations.append(main_anim)
|
||||||
|
def start_menu_animations(self):
|
||||||
|
"""启动菜单动画(从下往上)"""
|
||||||
|
for item in self.menu_widgets:
|
||||||
|
anim_group = QParallelAnimationGroup()
|
||||||
|
|
||||||
|
# 位置动画(从下往上)
|
||||||
|
pos_anim = QPropertyAnimation(item["widget"], b"pos")
|
||||||
|
pos_anim.setDuration(item["duration"])
|
||||||
|
pos_anim.setStartValue(QPoint(item["end_pos"].x(), self.canvas_height + 100))
|
||||||
|
pos_anim.setEndValue(item["end_pos"])
|
||||||
|
pos_anim.setEasingCurve(QEasingCurve.OutBack)
|
||||||
|
|
||||||
|
# 透明度动画
|
||||||
|
opacity_anim = QPropertyAnimation(item["widget"].graphicsEffect(), b"opacity")
|
||||||
|
opacity_anim.setDuration(item["duration"])
|
||||||
|
opacity_anim.setStartValue(0)
|
||||||
|
opacity_anim.setEndValue(1)
|
||||||
|
|
||||||
|
anim_group.addAnimation(pos_anim)
|
||||||
|
anim_group.addAnimation(opacity_anim)
|
||||||
|
anim_group.start()
|
||||||
|
self.animations.append(anim_group)
|
||||||
|
def start_animations(self):
|
||||||
|
"""启动完整动画序列"""
|
||||||
|
self.clear_animations()
|
||||||
|
self.start_logo_animations()
|
||||||
|
|
||||||
|
def clear_animations(self):
|
||||||
|
"""清理所有动画资源"""
|
||||||
|
for timer in self.timers:
|
||||||
|
timer.stop()
|
||||||
|
for anim in self.animations:
|
||||||
|
anim.stop()
|
||||||
|
self.timers.clear()
|
||||||
|
self.animations.clear()
|
||||||
BIN
source/icon.ico
Normal file
|
After Width: | Height: | Size: 264 KiB |
333
source/install.ui
Normal file
@@ -0,0 +1,333 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>MainWindows</class>
|
||||||
|
<widget class="QMainWindow" name="MainWindows">
|
||||||
|
<property name="enabled">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>1024</width>
|
||||||
|
<height>576</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>1024</width>
|
||||||
|
<height>576</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="maximumSize">
|
||||||
|
<size>
|
||||||
|
<width>1024</width>
|
||||||
|
<height>576</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="mouseTracking">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
<property name="tabletTracking">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
<property name="acceptDrops">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string> UI Test</string>
|
||||||
|
</property>
|
||||||
|
<property name="autoFillBackground">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
<property name="toolButtonStyle">
|
||||||
|
<enum>Qt::ToolButtonStyle::ToolButtonIconOnly</enum>
|
||||||
|
</property>
|
||||||
|
<property name="animated">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
<property name="documentMode">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
<property name="dockNestingEnabled">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
<widget class="QWidget" name="centralwidget">
|
||||||
|
<property name="autoFillBackground">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
<widget class="QLabel" name="loadbg">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>1031</width>
|
||||||
|
<height>561</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string/>
|
||||||
|
</property>
|
||||||
|
<property name="pixmap">
|
||||||
|
<pixmap>IMG/BG/bg2.jpg</pixmap>
|
||||||
|
</property>
|
||||||
|
<property name="scaledContents">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
<widget class="QLabel" name="vol1bg">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>120</y>
|
||||||
|
<width>93</width>
|
||||||
|
<height>64</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string/>
|
||||||
|
</property>
|
||||||
|
<property name="pixmap">
|
||||||
|
<pixmap>IMG/LOGO/vo01_logo.png</pixmap>
|
||||||
|
</property>
|
||||||
|
<property name="scaledContents">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
<widget class="QLabel" name="vol2bg">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>180</y>
|
||||||
|
<width>93</width>
|
||||||
|
<height>64</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string/>
|
||||||
|
</property>
|
||||||
|
<property name="pixmap">
|
||||||
|
<pixmap>IMG/LOGO/vo02_logo.png</pixmap>
|
||||||
|
</property>
|
||||||
|
<property name="scaledContents">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
<widget class="QLabel" name="vol3bg">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>240</y>
|
||||||
|
<width>93</width>
|
||||||
|
<height>64</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string/>
|
||||||
|
</property>
|
||||||
|
<property name="pixmap">
|
||||||
|
<pixmap>IMG/LOGO/vo03_logo.png</pixmap>
|
||||||
|
</property>
|
||||||
|
<property name="scaledContents">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
<widget class="QLabel" name="vol4bg">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>300</y>
|
||||||
|
<width>93</width>
|
||||||
|
<height>64</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string/>
|
||||||
|
</property>
|
||||||
|
<property name="pixmap">
|
||||||
|
<pixmap>IMG/LOGO/vo04_logo.png</pixmap>
|
||||||
|
</property>
|
||||||
|
<property name="scaledContents">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
<widget class="QLabel" name="afterbg">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>360</y>
|
||||||
|
<width>93</width>
|
||||||
|
<height>64</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string/>
|
||||||
|
</property>
|
||||||
|
<property name="pixmap">
|
||||||
|
<pixmap>IMG/LOGO/voaf_logo.png</pixmap>
|
||||||
|
</property>
|
||||||
|
<property name="scaledContents">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
<widget class="QLabel" name="Mainbg">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>1031</width>
|
||||||
|
<height>561</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string/>
|
||||||
|
</property>
|
||||||
|
<property name="pixmap">
|
||||||
|
<pixmap>IMG/BG/bg3.jpg</pixmap>
|
||||||
|
</property>
|
||||||
|
<property name="scaledContents">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
<widget class="QPushButton" name="start_install_btn">
|
||||||
|
<property name="enabled">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>780</x>
|
||||||
|
<y>250</y>
|
||||||
|
<width>191</width>
|
||||||
|
<height>91</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="accessibleDescription">
|
||||||
|
<string/>
|
||||||
|
</property>
|
||||||
|
<property name="autoFillBackground">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string/>
|
||||||
|
</property>
|
||||||
|
<property name="icon">
|
||||||
|
<iconset>
|
||||||
|
<normaloff>IMG/BTN/start_install.bmp</normaloff>IMG/BTN/start_install.bmp</iconset>
|
||||||
|
</property>
|
||||||
|
<property name="iconSize">
|
||||||
|
<size>
|
||||||
|
<width>189</width>
|
||||||
|
<height>110</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="checkable">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
<property name="autoRepeat">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
<property name="autoDefault">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
<property name="flat">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
<widget class="QPushButton" name="exit_btn">
|
||||||
|
<property name="enabled">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>780</x>
|
||||||
|
<y>340</y>
|
||||||
|
<width>191</width>
|
||||||
|
<height>91</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="autoFillBackground">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string/>
|
||||||
|
</property>
|
||||||
|
<property name="icon">
|
||||||
|
<iconset>
|
||||||
|
<normaloff>IMG/BTN/exit.bmp</normaloff>IMG/BTN/exit.bmp</iconset>
|
||||||
|
</property>
|
||||||
|
<property name="iconSize">
|
||||||
|
<size>
|
||||||
|
<width>189</width>
|
||||||
|
<height>110</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="checkable">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
<property name="flat">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
<widget class="QLabel" name="menubg">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>710</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>321</width>
|
||||||
|
<height>561</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string/>
|
||||||
|
</property>
|
||||||
|
<property name="pixmap">
|
||||||
|
<pixmap>IMG/BG/menubg.jpg</pixmap>
|
||||||
|
</property>
|
||||||
|
<property name="scaledContents">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
<zorder>loadbg</zorder>
|
||||||
|
<zorder>vol1bg</zorder>
|
||||||
|
<zorder>vol2bg</zorder>
|
||||||
|
<zorder>vol3bg</zorder>
|
||||||
|
<zorder>vol4bg</zorder>
|
||||||
|
<zorder>afterbg</zorder>
|
||||||
|
<zorder>Mainbg</zorder>
|
||||||
|
<zorder>menubg</zorder>
|
||||||
|
<zorder>start_install_btn</zorder>
|
||||||
|
<zorder>exit_btn</zorder>
|
||||||
|
</widget>
|
||||||
|
<widget class="QMenuBar" name="menubar">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>1024</width>
|
||||||
|
<height>21</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<widget class="QMenu" name="menu">
|
||||||
|
<property name="title">
|
||||||
|
<string>设置</string>
|
||||||
|
</property>
|
||||||
|
<addaction name="separator"/>
|
||||||
|
<addaction name="action_2"/>
|
||||||
|
</widget>
|
||||||
|
<widget class="QMenu" name="menu_2">
|
||||||
|
<property name="title">
|
||||||
|
<string>关于</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
<addaction name="menu"/>
|
||||||
|
<addaction name="menu_2"/>
|
||||||
|
</widget>
|
||||||
|
<action name="action_2">
|
||||||
|
<property name="text">
|
||||||
|
<string>update - sd</string>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
|
</widget>
|
||||||
|
<resources/>
|
||||||
|
<connections/>
|
||||||
|
</ui>
|
||||||
646
source/main.py
Normal file
@@ -0,0 +1,646 @@
|
|||||||
|
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 pic_data import img_data
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
def load_base64_image(base64_str):
|
||||||
|
pixmap = QPixmap()
|
||||||
|
pixmap.loadFromData(base64.b64decode(base64_str))
|
||||||
|
return pixmap
|
||||||
|
|
||||||
|
# 配置信息
|
||||||
|
app_data = {
|
||||||
|
"APP_VERSION": "1.0.0.0086",
|
||||||
|
"APP_NAME": "@FRAISEMOE2 Addons Installer",
|
||||||
|
"TEMP": "TEMP",
|
||||||
|
"CACHE": "FRAISEMOE",
|
||||||
|
"PLUGIN": "PLUGIN",
|
||||||
|
"CONFIG_URL": "aHR0cHM6Ly9hcmNoaXZlLm92b2Zpc2guY29tL2FwaS93aWRnZXQvbmVrb3BhcmEvZG93bmxvYWRfdXJsX2RlYnVnLmpzb24=",
|
||||||
|
"UA": "TW96aWxsYS81LjAgKExpbnV4IGRlYmlhbjEyIEZyYWlzZU1vZTItQWNjZXB0KSBHZWNrby8yMDEwMDEwMSBGaXJlZm94LzExNC4w",
|
||||||
|
"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",
|
||||||
|
},
|
||||||
|
"NEKOPARA After": {
|
||||||
|
"exe": "nekopara_after.exe",
|
||||||
|
"hash": "eb26ff6850096a240af8340ba21c5c3232e90f29fb8191e24b6ce701acae0aa9",
|
||||||
|
"install_path": "NEKOPARA After/afteradult.xp3",
|
||||||
|
"plugin_path": "after/afteradult.xp3",
|
||||||
|
"sig_path": "after/afteradult.xp3.sig"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# 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_data = img_data.get("icon")
|
||||||
|
if icon_data:
|
||||||
|
pixmap = load_base64_image(icon_data)
|
||||||
|
if not pixmap.isNull():
|
||||||
|
msg_box.setWindowIcon(QIcon(pixmap))
|
||||||
|
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",
|
||||||
|
"nekopara_after.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_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.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)
|
||||||
|
|
||||||
|
# 检查管理员权限和进程
|
||||||
|
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)) or "after.data" not in config_data:
|
||||||
|
raise ValueError("配置文件数据异常")
|
||||||
|
return {
|
||||||
|
f"vol{i+1}": config_data[f"vol.{i+1}.data"]["url"] for i in range(4)
|
||||||
|
} | {
|
||||||
|
"after": config_data["after.data"]["url"]
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
|
||||||
|
# 创建游戏目录(如果不存在)
|
||||||
|
os.makedirs(game_folder, exist_ok=True)
|
||||||
|
|
||||||
|
# 复制主文件
|
||||||
|
shutil.copy(plugin_path, game_folder)
|
||||||
|
|
||||||
|
# 如果是After版本,还需要复制签名文件
|
||||||
|
if game_version == "NEKOPARA After":
|
||||||
|
sig_path = os.path.join(PLUGIN, GAME_INFO[game_version]["sig_path"])
|
||||||
|
shutil.copy(sig_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
|
||||||
|
|
||||||
|
# 处理1-4卷
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
|
||||||
|
# 处理After
|
||||||
|
game_version = "NEKOPARA After"
|
||||||
|
if not self.installed_status[game_version]:
|
||||||
|
url = config["after"]
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
|
||||||
|
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
19
source/popup.ui
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>Form</class>
|
||||||
|
<widget class="QWidget" name="Form">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>480</width>
|
||||||
|
<height>270</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>Form</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
<resources/>
|
||||||
|
<connections/>
|
||||||
|
</ui>
|
||||||
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
|
||||||