commit 95da43261e910b680cb8f0361b6a7ff899d30434 Author: 283375 Date: Fri Jul 7 01:41:19 2023 +0800 init diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..b1ae5f4 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true + +[*.py] +indent_size = 4 +indent_style = space diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1ae2f94 --- /dev/null +++ b/.gitignore @@ -0,0 +1,170 @@ +__debug* + +arcaea_offline.db +arcaea_offline.ini + +# 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 + +# Nuitka +*.dist/ +*.build/ +*.onefile-build/ + +# 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 + +# 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/#use-with-ide +.pdm.toml + +# 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/ diff --git a/index.py b/index.py new file mode 100644 index 0000000..94d0584 --- /dev/null +++ b/index.py @@ -0,0 +1,52 @@ +import logging +import sys +import traceback + +from arcaea_offline.database import Database +from PySide6.QtCore import QLibraryInfo, QLocale, QTranslator +from PySide6.QtGui import QIcon +from PySide6.QtWidgets import QApplication, QDialog, QMessageBox + +from ui.startup.databaseChecker import DatabaseChecker +from ui.implements.mainwindow import MainWindow +import ui.resources.images.images_rc +import ui.resources.translations.translations_rc + +logging.basicConfig(level=logging.INFO, stream=sys.stdout, force=True) + +if __name__ == "__main__": + locale = QLocale.system() + translator = QTranslator() + translator_load_success = translator.load(QLocale.system(), "", "", ":/lang/") + if not translator_load_success: + translator.load(":/lang/en_US.qm") + baseTranslator = QTranslator() + baseTranslator.load( + QLocale.system(), + "qt", + "_", + QLibraryInfo.path(QLibraryInfo.LibraryPath.TranslationsPath), + ) + app = QApplication(sys.argv) + + app.installTranslator(translator) + app.installTranslator(baseTranslator) + + databaseChecker = DatabaseChecker() + result = databaseChecker.exec() + + if result == QDialog.DialogCode.Accepted: + try: + Database() + except Exception as e: + QMessageBox.critical( + None, "Database Error", "\n".join(traceback.format_exception(e)) + ) + sys.exit(1) + + window = MainWindow() + window.setWindowIcon(QIcon(":/images/icon.png")) + window.show() + sys.exit(app.exec()) + else: + sys.exit(1) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..cd5ca03 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,40 @@ +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "arcaea-offline-pyside-ui" +version = "0.1.0" +authors = [{ name = "283375", email = "log_283375@163.com" }] +description = "No description." +readme = "README.md" +requires-python = ">=3.9" +dependencies = [ + "arcaea-offline==0.1.0", + "arcaea-offline-ocr==0.1.0", + "exif==1.6.0", +] +classifiers = [ + "Development Status :: 3 - Alpha", + "Programming Language :: Python :: 3", +] + +[project.urls] +"Homepage" = "https://github.com/283375/arcaea-offline-pyside-ui" +"Bug Tracker" = "https://github.com/283375/arcaea-offline-pyside-ui/issues" + +[tool.black] +force-exclude = ''' +( + ui/designer + | .*_rc.py +) +''' + +[tool.isort] +profile = "black" +extend_skip = ["ui/designer"] +extend_skip_glob = ["*_rc.py"] + +[tool.pyright] +ignore = ["**/__debug*.*"] diff --git a/ui/__init__.py b/ui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ui/designer/components/chartSelector.ui b/ui/designer/components/chartSelector.ui new file mode 100644 index 0000000..61ad9c6 --- /dev/null +++ b/ui/designer/components/chartSelector.ui @@ -0,0 +1,251 @@ + + + ChartSelector + + + + 0 + 0 + 671 + 295 + + + + ChartSelector + + + + + + + 0 + 0 + + + + songIdSelector.title + + + + + + + 300 + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + true + + + fuzzySearch.lineEdit.placeholder + + + true + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + + + + + songIdSelector.quickActions + + + + + + songIdSelector.quickActions.previousPackageButton + + + + + + + songIdSelector.quickActions.previousSongIdButton + + + + + + + songIdSelector.quickActions.nextSongIdButton + + + + + + + songIdSelector.quickActions.nextPackageButton + + + + + + + + + + + + + + 200 + 0 + + + + ratingClassSelector.title + + + + 0 + + + + + + 0 + 0 + + + + PAST + + + false + + + + + + + + 0 + 0 + + + + PRESENT + + + false + + + + + + + + 0 + 0 + + + + FUTURE + + + false + + + + + + + false + + + + 0 + 0 + + + + BEYOND + + + false + + + + + + + + + + + + + 0 + 0 + + + + ... + + + Qt::RichText + + + + + + + resetButton + + + + + + + + + + RatingClassRadioButton + QRadioButton +
ui.implements.components.ratingClassRadioButton
+
+
+ + bydButton + + + +
diff --git a/ui/designer/components/chartSelector_ui.py b/ui/designer/components/chartSelector_ui.py new file mode 100644 index 0000000..6410692 --- /dev/null +++ b/ui/designer/components/chartSelector_ui.py @@ -0,0 +1,190 @@ +# -*- coding: utf-8 -*- + +################################################################################ +## Form generated from reading UI file 'chartSelector.ui' +## +## Created by: Qt User Interface Compiler version 6.5.0 +## +## WARNING! All changes made in this file will be lost when recompiling UI file! +################################################################################ + +from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, + QMetaObject, QObject, QPoint, QRect, + QSize, QTime, QUrl, Qt) +from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, + QFont, QFontDatabase, QGradient, QIcon, + QImage, QKeySequence, QLinearGradient, QPainter, + QPalette, QPixmap, QRadialGradient, QTransform) +from PySide6.QtWidgets import (QApplication, QComboBox, QGroupBox, QHBoxLayout, + QLabel, QLineEdit, QPushButton, QSizePolicy, + QSpacerItem, QVBoxLayout, QWidget) + +from ui.implements.components.ratingClassRadioButton import RatingClassRadioButton + +class Ui_ChartSelector(object): + def setupUi(self, ChartSelector): + if not ChartSelector.objectName(): + ChartSelector.setObjectName(u"ChartSelector") + ChartSelector.resize(671, 295) + ChartSelector.setWindowTitle(u"ChartSelector") + self.mainVerticalLayout = QVBoxLayout(ChartSelector) + self.mainVerticalLayout.setObjectName(u"mainVerticalLayout") + self.songIdSelectorGroupBox = QGroupBox(ChartSelector) + self.songIdSelectorGroupBox.setObjectName(u"songIdSelectorGroupBox") + sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.songIdSelectorGroupBox.sizePolicy().hasHeightForWidth()) + self.songIdSelectorGroupBox.setSizePolicy(sizePolicy) + self.horizontalLayout = QHBoxLayout(self.songIdSelectorGroupBox) + self.horizontalLayout.setObjectName(u"horizontalLayout") + self.widget = QWidget(self.songIdSelectorGroupBox) + self.widget.setObjectName(u"widget") + self.widget.setMinimumSize(QSize(300, 0)) + self.verticalLayout = QVBoxLayout(self.widget) + self.verticalLayout.setObjectName(u"verticalLayout") + self.verticalLayout.setContentsMargins(0, 0, 0, 0) + self.fuzzySearchLineEdit = QLineEdit(self.widget) + self.fuzzySearchLineEdit.setObjectName(u"fuzzySearchLineEdit") + self.fuzzySearchLineEdit.setFrame(True) + self.fuzzySearchLineEdit.setClearButtonEnabled(True) + + self.verticalLayout.addWidget(self.fuzzySearchLineEdit) + + self.verticalSpacer = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding) + + self.verticalLayout.addItem(self.verticalSpacer) + + self.packageComboBox = QComboBox(self.widget) + self.packageComboBox.setObjectName(u"packageComboBox") + + self.verticalLayout.addWidget(self.packageComboBox) + + self.songIdComboBox = QComboBox(self.widget) + self.songIdComboBox.setObjectName(u"songIdComboBox") + + self.verticalLayout.addWidget(self.songIdComboBox) + + + self.horizontalLayout.addWidget(self.widget) + + self.songIdSelectorQuickActionsGroupBox = QGroupBox(self.songIdSelectorGroupBox) + self.songIdSelectorQuickActionsGroupBox.setObjectName(u"songIdSelectorQuickActionsGroupBox") + self.verticalLayout_2 = QVBoxLayout(self.songIdSelectorQuickActionsGroupBox) + self.verticalLayout_2.setObjectName(u"verticalLayout_2") + self.previousPackageButton = QPushButton(self.songIdSelectorQuickActionsGroupBox) + self.previousPackageButton.setObjectName(u"previousPackageButton") + + self.verticalLayout_2.addWidget(self.previousPackageButton) + + self.previousSongIdButton = QPushButton(self.songIdSelectorQuickActionsGroupBox) + self.previousSongIdButton.setObjectName(u"previousSongIdButton") + + self.verticalLayout_2.addWidget(self.previousSongIdButton) + + self.nextSongIdButton = QPushButton(self.songIdSelectorQuickActionsGroupBox) + self.nextSongIdButton.setObjectName(u"nextSongIdButton") + + self.verticalLayout_2.addWidget(self.nextSongIdButton) + + self.nextPackageButton = QPushButton(self.songIdSelectorQuickActionsGroupBox) + self.nextPackageButton.setObjectName(u"nextPackageButton") + + self.verticalLayout_2.addWidget(self.nextPackageButton) + + + self.horizontalLayout.addWidget(self.songIdSelectorQuickActionsGroupBox) + + + self.mainVerticalLayout.addWidget(self.songIdSelectorGroupBox) + + self.ratingClassGroupBox = QGroupBox(ChartSelector) + self.ratingClassGroupBox.setObjectName(u"ratingClassGroupBox") + self.ratingClassGroupBox.setMinimumSize(QSize(200, 0)) + self.horizontalLayout_2 = QHBoxLayout(self.ratingClassGroupBox) + self.horizontalLayout_2.setSpacing(0) + self.horizontalLayout_2.setObjectName(u"horizontalLayout_2") + self.pstButton = RatingClassRadioButton(self.ratingClassGroupBox) + self.pstButton.setObjectName(u"pstButton") + sizePolicy1 = QSizePolicy(QSizePolicy.Minimum, QSizePolicy.MinimumExpanding) + sizePolicy1.setHorizontalStretch(0) + sizePolicy1.setVerticalStretch(0) + sizePolicy1.setHeightForWidth(self.pstButton.sizePolicy().hasHeightForWidth()) + self.pstButton.setSizePolicy(sizePolicy1) + self.pstButton.setText(u"PAST") + self.pstButton.setAutoExclusive(False) + + self.horizontalLayout_2.addWidget(self.pstButton) + + self.prsButton = RatingClassRadioButton(self.ratingClassGroupBox) + self.prsButton.setObjectName(u"prsButton") + sizePolicy1.setHeightForWidth(self.prsButton.sizePolicy().hasHeightForWidth()) + self.prsButton.setSizePolicy(sizePolicy1) + self.prsButton.setText(u"PRESENT") + self.prsButton.setAutoExclusive(False) + + self.horizontalLayout_2.addWidget(self.prsButton) + + self.ftrButton = RatingClassRadioButton(self.ratingClassGroupBox) + self.ftrButton.setObjectName(u"ftrButton") + sizePolicy1.setHeightForWidth(self.ftrButton.sizePolicy().hasHeightForWidth()) + self.ftrButton.setSizePolicy(sizePolicy1) + self.ftrButton.setText(u"FUTURE") + self.ftrButton.setAutoExclusive(False) + + self.horizontalLayout_2.addWidget(self.ftrButton) + + self.bydButton = RatingClassRadioButton(self.ratingClassGroupBox) + self.bydButton.setObjectName(u"bydButton") + self.bydButton.setEnabled(False) + sizePolicy1.setHeightForWidth(self.bydButton.sizePolicy().hasHeightForWidth()) + self.bydButton.setSizePolicy(sizePolicy1) + self.bydButton.setText(u"BEYOND") + self.bydButton.setAutoExclusive(False) + + self.horizontalLayout_2.addWidget(self.bydButton) + + + self.mainVerticalLayout.addWidget(self.ratingClassGroupBox) + + self.resultsHorizontalLayout = QHBoxLayout() + self.resultsHorizontalLayout.setObjectName(u"resultsHorizontalLayout") + self.resultLabel = QLabel(ChartSelector) + self.resultLabel.setObjectName(u"resultLabel") + sizePolicy2 = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) + sizePolicy2.setHorizontalStretch(0) + sizePolicy2.setVerticalStretch(0) + sizePolicy2.setHeightForWidth(self.resultLabel.sizePolicy().hasHeightForWidth()) + self.resultLabel.setSizePolicy(sizePolicy2) + self.resultLabel.setText(u"...") + self.resultLabel.setTextFormat(Qt.RichText) + + self.resultsHorizontalLayout.addWidget(self.resultLabel) + + self.resetButton = QPushButton(ChartSelector) + self.resetButton.setObjectName(u"resetButton") + + self.resultsHorizontalLayout.addWidget(self.resetButton) + + + self.mainVerticalLayout.addLayout(self.resultsHorizontalLayout) + + + self.retranslateUi(ChartSelector) + + QMetaObject.connectSlotsByName(ChartSelector) + # setupUi + + def retranslateUi(self, ChartSelector): + self.songIdSelectorGroupBox.setTitle(QCoreApplication.translate("ChartSelector", u"songIdSelector.title", None)) + self.fuzzySearchLineEdit.setPlaceholderText(QCoreApplication.translate("ChartSelector", u"fuzzySearch.lineEdit.placeholder", None)) + self.songIdSelectorQuickActionsGroupBox.setTitle(QCoreApplication.translate("ChartSelector", u"songIdSelector.quickActions", None)) + self.previousPackageButton.setText(QCoreApplication.translate("ChartSelector", u"songIdSelector.quickActions.previousPackageButton", None)) + self.previousSongIdButton.setText(QCoreApplication.translate("ChartSelector", u"songIdSelector.quickActions.previousSongIdButton", None)) + self.nextSongIdButton.setText(QCoreApplication.translate("ChartSelector", u"songIdSelector.quickActions.nextSongIdButton", None)) + self.nextPackageButton.setText(QCoreApplication.translate("ChartSelector", u"songIdSelector.quickActions.nextPackageButton", None)) + self.ratingClassGroupBox.setTitle(QCoreApplication.translate("ChartSelector", u"ratingClassSelector.title", None)) + self.resetButton.setText(QCoreApplication.translate("ChartSelector", u"resetButton", None)) + pass + # retranslateUi + diff --git a/ui/designer/components/dbTableViewer.ui b/ui/designer/components/dbTableViewer.ui new file mode 100644 index 0000000..1bb1898 --- /dev/null +++ b/ui/designer/components/dbTableViewer.ui @@ -0,0 +1,159 @@ + + + DbTableViewer + + + + 0 + 0 + 681 + 575 + + + + DbTableViewer + + + + + + actions + + + + + + actions.removeSelected + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + actions.refresh + + + + + + + + + + QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed + + + false + + + QAbstractItemView::ExtendedSelection + + + QAbstractItemView::SelectRows + + + QAbstractItemView::ScrollPerPixel + + + QAbstractItemView::ScrollPerPixel + + + false + + + + + + + view + + + + + + + + view.sort.label + + + + + + + + 0 + 0 + + + + + + + + view.sort.descendingCheckBox + + + true + + + + + + + + + 9 + + + 9 + + + + + view.filter.label + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + view.filter.configureButton + + + + + + + + + + + + + diff --git a/ui/designer/components/dbTableViewer_ui.py b/ui/designer/components/dbTableViewer_ui.py new file mode 100644 index 0000000..22c6dcc --- /dev/null +++ b/ui/designer/components/dbTableViewer_ui.py @@ -0,0 +1,134 @@ +# -*- coding: utf-8 -*- + +################################################################################ +## Form generated from reading UI file 'dbTableViewer.ui' +## +## Created by: Qt User Interface Compiler version 6.5.0 +## +## WARNING! All changes made in this file will be lost when recompiling UI file! +################################################################################ + +from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, + QMetaObject, QObject, QPoint, QRect, + QSize, QTime, QUrl, Qt) +from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, + QFont, QFontDatabase, QGradient, QIcon, + QImage, QKeySequence, QLinearGradient, QPainter, + QPalette, QPixmap, QRadialGradient, QTransform) +from PySide6.QtWidgets import (QAbstractItemView, QApplication, QCheckBox, QComboBox, + QGridLayout, QGroupBox, QHBoxLayout, QHeaderView, + QLabel, QPushButton, QSizePolicy, QSpacerItem, + QTableView, QVBoxLayout, QWidget) + +class Ui_DbTableViewer(object): + def setupUi(self, DbTableViewer): + if not DbTableViewer.objectName(): + DbTableViewer.setObjectName(u"DbTableViewer") + DbTableViewer.resize(681, 575) + DbTableViewer.setWindowTitle(u"DbTableViewer") + self.gridLayout = QGridLayout(DbTableViewer) + self.gridLayout.setObjectName(u"gridLayout") + self.groupBox = QGroupBox(DbTableViewer) + self.groupBox.setObjectName(u"groupBox") + self.verticalLayout = QVBoxLayout(self.groupBox) + self.verticalLayout.setObjectName(u"verticalLayout") + self.action_removeSelectedButton = QPushButton(self.groupBox) + self.action_removeSelectedButton.setObjectName(u"action_removeSelectedButton") + + self.verticalLayout.addWidget(self.action_removeSelectedButton) + + self.verticalSpacer = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding) + + self.verticalLayout.addItem(self.verticalSpacer) + + self.refreshButton = QPushButton(self.groupBox) + self.refreshButton.setObjectName(u"refreshButton") + + self.verticalLayout.addWidget(self.refreshButton) + + + self.gridLayout.addWidget(self.groupBox, 0, 1, 1, 1) + + self.tableView = QTableView(DbTableViewer) + self.tableView.setObjectName(u"tableView") + self.tableView.setEditTriggers(QAbstractItemView.DoubleClicked|QAbstractItemView.EditKeyPressed) + self.tableView.setProperty("showDropIndicator", False) + self.tableView.setSelectionMode(QAbstractItemView.ExtendedSelection) + self.tableView.setSelectionBehavior(QAbstractItemView.SelectRows) + self.tableView.setVerticalScrollMode(QAbstractItemView.ScrollPerPixel) + self.tableView.setHorizontalScrollMode(QAbstractItemView.ScrollPerPixel) + self.tableView.verticalHeader().setVisible(False) + + self.gridLayout.addWidget(self.tableView, 0, 0, 1, 1) + + self.groupBox_2 = QGroupBox(DbTableViewer) + self.groupBox_2.setObjectName(u"groupBox_2") + self.verticalLayout_3 = QVBoxLayout(self.groupBox_2) + self.verticalLayout_3.setObjectName(u"verticalLayout_3") + self.horizontalLayout_2 = QHBoxLayout() + self.horizontalLayout_2.setObjectName(u"horizontalLayout_2") + self.label = QLabel(self.groupBox_2) + self.label.setObjectName(u"label") + + self.horizontalLayout_2.addWidget(self.label) + + self.sort_comboBox = QComboBox(self.groupBox_2) + self.sort_comboBox.setObjectName(u"sort_comboBox") + sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.sort_comboBox.sizePolicy().hasHeightForWidth()) + self.sort_comboBox.setSizePolicy(sizePolicy) + + self.horizontalLayout_2.addWidget(self.sort_comboBox) + + self.sort_descendingCheckBox = QCheckBox(self.groupBox_2) + self.sort_descendingCheckBox.setObjectName(u"sort_descendingCheckBox") + self.sort_descendingCheckBox.setChecked(True) + + self.horizontalLayout_2.addWidget(self.sort_descendingCheckBox) + + + self.verticalLayout_3.addLayout(self.horizontalLayout_2) + + self.horizontalLayout = QHBoxLayout() + self.horizontalLayout.setObjectName(u"horizontalLayout") + self.horizontalLayout.setContentsMargins(-1, 9, -1, 9) + self.label_2 = QLabel(self.groupBox_2) + self.label_2.setObjectName(u"label_2") + + self.horizontalLayout.addWidget(self.label_2) + + self.horizontalSpacer = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) + + self.horizontalLayout.addItem(self.horizontalSpacer) + + self.pushButton = QPushButton(self.groupBox_2) + self.pushButton.setObjectName(u"pushButton") + + self.horizontalLayout.addWidget(self.pushButton) + + + self.verticalLayout_3.addLayout(self.horizontalLayout) + + + self.gridLayout.addWidget(self.groupBox_2, 1, 0, 1, 1) + + + self.retranslateUi(DbTableViewer) + + QMetaObject.connectSlotsByName(DbTableViewer) + # setupUi + + def retranslateUi(self, DbTableViewer): + self.groupBox.setTitle(QCoreApplication.translate("DbTableViewer", u"actions", None)) + self.action_removeSelectedButton.setText(QCoreApplication.translate("DbTableViewer", u"actions.removeSelected", None)) + self.refreshButton.setText(QCoreApplication.translate("DbTableViewer", u"actions.refresh", None)) + self.groupBox_2.setTitle(QCoreApplication.translate("DbTableViewer", u"view", None)) + self.label.setText(QCoreApplication.translate("DbTableViewer", u"view.sort.label", None)) + self.sort_descendingCheckBox.setText(QCoreApplication.translate("DbTableViewer", u"view.sort.descendingCheckBox", None)) + self.label_2.setText(QCoreApplication.translate("DbTableViewer", u"view.filter.label", None)) + self.pushButton.setText(QCoreApplication.translate("DbTableViewer", u"view.filter.configureButton", None)) + pass + # retranslateUi + diff --git a/ui/designer/components/fileSelector.ui b/ui/designer/components/fileSelector.ui new file mode 100644 index 0000000..e42d676 --- /dev/null +++ b/ui/designer/components/fileSelector.ui @@ -0,0 +1,60 @@ + + + FileSelector + + + + 0 + 0 + 559 + 42 + + + + FileSelector + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + ... + + + + + + + selectButton + + + + + + + + ElidedLabel + QLabel +
ui.implements.components.elidedLabel
+
+
+ + +
diff --git a/ui/designer/components/fileSelector_ui.py b/ui/designer/components/fileSelector_ui.py new file mode 100644 index 0000000..380c195 --- /dev/null +++ b/ui/designer/components/fileSelector_ui.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- + +################################################################################ +## Form generated from reading UI file 'fileSelector.ui' +## +## Created by: Qt User Interface Compiler version 6.5.0 +## +## WARNING! All changes made in this file will be lost when recompiling UI file! +################################################################################ + +from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, + QMetaObject, QObject, QPoint, QRect, + QSize, QTime, QUrl, Qt) +from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, + QFont, QFontDatabase, QGradient, QIcon, + QImage, QKeySequence, QLinearGradient, QPainter, + QPalette, QPixmap, QRadialGradient, QTransform) +from PySide6.QtWidgets import (QApplication, QHBoxLayout, QPushButton, QSizePolicy, + QWidget) + +from ui.implements.components.elidedLabel import ElidedLabel + +class Ui_FileSelector(object): + def setupUi(self, FileSelector): + if not FileSelector.objectName(): + FileSelector.setObjectName(u"FileSelector") + FileSelector.resize(559, 42) + FileSelector.setWindowTitle(u"FileSelector") + self.horizontalLayout_2 = QHBoxLayout(FileSelector) + self.horizontalLayout_2.setObjectName(u"horizontalLayout_2") + self.horizontalLayout_2.setContentsMargins(0, 0, 0, 0) + self.elidedLabel = ElidedLabel(FileSelector) + self.elidedLabel.setObjectName(u"elidedLabel") + sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.elidedLabel.sizePolicy().hasHeightForWidth()) + self.elidedLabel.setSizePolicy(sizePolicy) + self.elidedLabel.setText(u"...") + + self.horizontalLayout_2.addWidget(self.elidedLabel) + + self.selectButton = QPushButton(FileSelector) + self.selectButton.setObjectName(u"selectButton") + + self.horizontalLayout_2.addWidget(self.selectButton) + + + self.retranslateUi(FileSelector) + + QMetaObject.connectSlotsByName(FileSelector) + # setupUi + + def retranslateUi(self, FileSelector): + self.selectButton.setText(QCoreApplication.translate("FileSelector", u"selectButton", None)) + pass + # retranslateUi + diff --git a/ui/designer/components/scoreEditor.ui b/ui/designer/components/scoreEditor.ui new file mode 100644 index 0000000..369696a --- /dev/null +++ b/ui/designer/components/scoreEditor.ui @@ -0,0 +1,228 @@ + + + ScoreEditor + + + + 0 + 0 + 365 + 253 + + + + ScoreEditor + + + + QFormLayout::ExpandingFieldsGrow + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + formLabel.score + + + + + + + B9'999'999;_ + + + + + + + PURE + + + + + + + + 100 + 0 + + + + 0 + + + + + + + FAR + + + + + + + + 100 + 0 + + + + 0 + + + + + + + LOST + + + + + + + + 100 + 0 + + + + 0 + + + + + + + formLabel.time + + + + + + + + 0 + 0 + + + + + 0 + 0 + 0 + 2017 + 1 + 22 + + + + + 2017 + 1 + 22 + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + MAX RECALL + + + + + + + + 100 + 0 + + + + -1 + + + 0 + + + -1 + + + + + + + + + + 0 + 0 + + + + ... + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + commitButton + + + + + + + + + formLabel.clearType + + + + + + + false + + + + 100 + 0 + + + + + + + + + FocusSelectAllLineEdit + QLineEdit +
ui.implements.components.focusSelectAllLineEdit
+
+
+ + +
diff --git a/ui/designer/components/scoreEditor_ui.py b/ui/designer/components/scoreEditor_ui.py new file mode 100644 index 0000000..df2ebe5 --- /dev/null +++ b/ui/designer/components/scoreEditor_ui.py @@ -0,0 +1,167 @@ +# -*- coding: utf-8 -*- + +################################################################################ +## Form generated from reading UI file 'scoreEditor.ui' +## +## Created by: Qt User Interface Compiler version 6.5.0 +## +## WARNING! All changes made in this file will be lost when recompiling UI file! +################################################################################ + +from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, + QMetaObject, QObject, QPoint, QRect, + QSize, QTime, QUrl, Qt) +from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, + QFont, QFontDatabase, QGradient, QIcon, + QImage, QKeySequence, QLinearGradient, QPainter, + QPalette, QPixmap, QRadialGradient, QTransform) +from PySide6.QtWidgets import (QApplication, QComboBox, QDateTimeEdit, QFormLayout, + QHBoxLayout, QLabel, QPushButton, QSizePolicy, + QSpacerItem, QSpinBox, QWidget) + +from ui.implements.components.focusSelectAllLineEdit import FocusSelectAllLineEdit + +class Ui_ScoreEditor(object): + def setupUi(self, ScoreEditor): + if not ScoreEditor.objectName(): + ScoreEditor.setObjectName(u"ScoreEditor") + ScoreEditor.resize(365, 253) + ScoreEditor.setWindowTitle(u"ScoreEditor") + self.formLayout = QFormLayout(ScoreEditor) + self.formLayout.setObjectName(u"formLayout") + self.formLayout.setFieldGrowthPolicy(QFormLayout.ExpandingFieldsGrow) + self.formLayout.setLabelAlignment(Qt.AlignRight|Qt.AlignTrailing|Qt.AlignVCenter) + self.label = QLabel(ScoreEditor) + self.label.setObjectName(u"label") + + self.formLayout.setWidget(0, QFormLayout.LabelRole, self.label) + + self.scoreLineEdit = FocusSelectAllLineEdit(ScoreEditor) + self.scoreLineEdit.setObjectName(u"scoreLineEdit") + self.scoreLineEdit.setInputMask(u"B9'999'999;_") + + self.formLayout.setWidget(0, QFormLayout.FieldRole, self.scoreLineEdit) + + self.label_2 = QLabel(ScoreEditor) + self.label_2.setObjectName(u"label_2") + self.label_2.setText(u"PURE") + + self.formLayout.setWidget(1, QFormLayout.LabelRole, self.label_2) + + self.pureSpinBox = QSpinBox(ScoreEditor) + self.pureSpinBox.setObjectName(u"pureSpinBox") + self.pureSpinBox.setMinimumSize(QSize(100, 0)) + self.pureSpinBox.setMaximum(0) + + self.formLayout.setWidget(1, QFormLayout.FieldRole, self.pureSpinBox) + + self.label_3 = QLabel(ScoreEditor) + self.label_3.setObjectName(u"label_3") + self.label_3.setText(u"FAR") + + self.formLayout.setWidget(2, QFormLayout.LabelRole, self.label_3) + + self.farSpinBox = QSpinBox(ScoreEditor) + self.farSpinBox.setObjectName(u"farSpinBox") + self.farSpinBox.setMinimumSize(QSize(100, 0)) + self.farSpinBox.setMaximum(0) + + self.formLayout.setWidget(2, QFormLayout.FieldRole, self.farSpinBox) + + self.label_4 = QLabel(ScoreEditor) + self.label_4.setObjectName(u"label_4") + self.label_4.setText(u"LOST") + + self.formLayout.setWidget(3, QFormLayout.LabelRole, self.label_4) + + self.lostSpinBox = QSpinBox(ScoreEditor) + self.lostSpinBox.setObjectName(u"lostSpinBox") + self.lostSpinBox.setMinimumSize(QSize(100, 0)) + self.lostSpinBox.setMaximum(0) + + self.formLayout.setWidget(3, QFormLayout.FieldRole, self.lostSpinBox) + + self.label_5 = QLabel(ScoreEditor) + self.label_5.setObjectName(u"label_5") + + self.formLayout.setWidget(4, QFormLayout.LabelRole, self.label_5) + + self.dateTimeEdit = QDateTimeEdit(ScoreEditor) + self.dateTimeEdit.setObjectName(u"dateTimeEdit") + sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.dateTimeEdit.sizePolicy().hasHeightForWidth()) + self.dateTimeEdit.setSizePolicy(sizePolicy) + self.dateTimeEdit.setDateTime(QDateTime(QDate(2017, 1, 22), QTime(0, 0, 0))) + self.dateTimeEdit.setMinimumDate(QDate(2017, 1, 22)) + + self.formLayout.setWidget(4, QFormLayout.FieldRole, self.dateTimeEdit) + + self.verticalSpacer = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding) + + self.formLayout.setItem(5, QFormLayout.LabelRole, self.verticalSpacer) + + self.label_6 = QLabel(ScoreEditor) + self.label_6.setObjectName(u"label_6") + self.label_6.setText(u"MAX RECALL") + + self.formLayout.setWidget(6, QFormLayout.LabelRole, self.label_6) + + self.maxRecallSpinBox = QSpinBox(ScoreEditor) + self.maxRecallSpinBox.setObjectName(u"maxRecallSpinBox") + self.maxRecallSpinBox.setMinimumSize(QSize(100, 0)) + self.maxRecallSpinBox.setMinimum(-1) + self.maxRecallSpinBox.setMaximum(0) + self.maxRecallSpinBox.setValue(-1) + + self.formLayout.setWidget(6, QFormLayout.FieldRole, self.maxRecallSpinBox) + + self.horizontalLayout = QHBoxLayout() + self.horizontalLayout.setObjectName(u"horizontalLayout") + self.validateLabel = QLabel(ScoreEditor) + self.validateLabel.setObjectName(u"validateLabel") + sizePolicy1 = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) + sizePolicy1.setHorizontalStretch(0) + sizePolicy1.setVerticalStretch(0) + sizePolicy1.setHeightForWidth(self.validateLabel.sizePolicy().hasHeightForWidth()) + self.validateLabel.setSizePolicy(sizePolicy1) + self.validateLabel.setText(u"...") + self.validateLabel.setAlignment(Qt.AlignRight|Qt.AlignTrailing|Qt.AlignVCenter) + + self.horizontalLayout.addWidget(self.validateLabel) + + self.commitButton = QPushButton(ScoreEditor) + self.commitButton.setObjectName(u"commitButton") + + self.horizontalLayout.addWidget(self.commitButton) + + + self.formLayout.setLayout(8, QFormLayout.SpanningRole, self.horizontalLayout) + + self.label_8 = QLabel(ScoreEditor) + self.label_8.setObjectName(u"label_8") + + self.formLayout.setWidget(7, QFormLayout.LabelRole, self.label_8) + + self.clearTypeComboBox = QComboBox(ScoreEditor) + self.clearTypeComboBox.setObjectName(u"clearTypeComboBox") + self.clearTypeComboBox.setEnabled(False) + self.clearTypeComboBox.setMinimumSize(QSize(100, 0)) + + self.formLayout.setWidget(7, QFormLayout.FieldRole, self.clearTypeComboBox) + + + self.retranslateUi(ScoreEditor) + + QMetaObject.connectSlotsByName(ScoreEditor) + # setupUi + + def retranslateUi(self, ScoreEditor): + self.label.setText(QCoreApplication.translate("ScoreEditor", u"formLabel.score", None)) + self.label_5.setText(QCoreApplication.translate("ScoreEditor", u"formLabel.time", None)) + self.commitButton.setText(QCoreApplication.translate("ScoreEditor", u"commitButton", None)) + self.label_8.setText(QCoreApplication.translate("ScoreEditor", u"formLabel.clearType", None)) + pass + # retranslateUi + diff --git a/ui/designer/mainwindow.ui b/ui/designer/mainwindow.ui new file mode 100644 index 0000000..4aaa966 --- /dev/null +++ b/ui/designer/mainwindow.ui @@ -0,0 +1,92 @@ + + + MainWindow + + + + 0 + 0 + 800 + 601 + + + + Arcaea Offline + + + + + + + 0 + + + + tab.overview + + + + + tab.input + + + + + tab.db + + + + + tab.ocr + + + + + tab.settings + + + + + tab.about + + + + + + + + + + TabInputScore + QWidget +
ui.implements.tabs.tabInputScore
+ 1 +
+ + TabOverview + QWidget +
ui.implements.tabs.tabOverview
+ 1 +
+ + TabSettings + QWidget +
ui.implements.tabs.tabSettings
+ 1 +
+ + TabAbout + QWidget +
ui.implements.tabs.tabAbout
+ 1 +
+ + TabDbEntry + QWidget +
ui.implements.tabs.tabDbEntry
+ 1 +
+
+ + +
diff --git a/ui/designer/mainwindow_ui.py b/ui/designer/mainwindow_ui.py new file mode 100644 index 0000000..506e2a2 --- /dev/null +++ b/ui/designer/mainwindow_ui.py @@ -0,0 +1,79 @@ +# -*- coding: utf-8 -*- + +################################################################################ +## Form generated from reading UI file 'mainwindow.ui' +## +## Created by: Qt User Interface Compiler version 6.5.0 +## +## WARNING! All changes made in this file will be lost when recompiling UI file! +################################################################################ + +from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, + QMetaObject, QObject, QPoint, QRect, + QSize, QTime, QUrl, Qt) +from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, + QFont, QFontDatabase, QGradient, QIcon, + QImage, QKeySequence, QLinearGradient, QPainter, + QPalette, QPixmap, QRadialGradient, QTransform) +from PySide6.QtWidgets import (QApplication, QMainWindow, QSizePolicy, QTabWidget, + QVBoxLayout, QWidget) + +from ui.implements.tabs.tabAbout import TabAbout +from ui.implements.tabs.tabDbEntry import TabDbEntry +from ui.implements.tabs.tabInputScore import TabInputScore +from ui.implements.tabs.tabOverview import TabOverview +from ui.implements.tabs.tabSettings import TabSettings + +class Ui_MainWindow(object): + def setupUi(self, MainWindow): + if not MainWindow.objectName(): + MainWindow.setObjectName(u"MainWindow") + MainWindow.resize(800, 601) + MainWindow.setWindowTitle(u"Arcaea Offline") + self.centralwidget = QWidget(MainWindow) + self.centralwidget.setObjectName(u"centralwidget") + self.verticalLayout = QVBoxLayout(self.centralwidget) + self.verticalLayout.setObjectName(u"verticalLayout") + self.tabWidget = QTabWidget(self.centralwidget) + self.tabWidget.setObjectName(u"tabWidget") + self.tab_overview = TabOverview() + self.tab_overview.setObjectName(u"tab_overview") + self.tabWidget.addTab(self.tab_overview, "") + self.tab_input = TabInputScore() + self.tab_input.setObjectName(u"tab_input") + self.tabWidget.addTab(self.tab_input, "") + self.tab_db = TabDbEntry() + self.tab_db.setObjectName(u"tab_db") + self.tabWidget.addTab(self.tab_db, "") + self.tab_ocr = QWidget() + self.tab_ocr.setObjectName(u"tab_ocr") + self.tabWidget.addTab(self.tab_ocr, "") + self.tab_settings = TabSettings() + self.tab_settings.setObjectName(u"tab_settings") + self.tabWidget.addTab(self.tab_settings, "") + self.tab_about = TabAbout() + self.tab_about.setObjectName(u"tab_about") + self.tabWidget.addTab(self.tab_about, "") + + self.verticalLayout.addWidget(self.tabWidget) + + MainWindow.setCentralWidget(self.centralwidget) + + self.retranslateUi(MainWindow) + + self.tabWidget.setCurrentIndex(0) + + + QMetaObject.connectSlotsByName(MainWindow) + # setupUi + + def retranslateUi(self, MainWindow): + self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_overview), QCoreApplication.translate("MainWindow", u"tab.overview", None)) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_input), QCoreApplication.translate("MainWindow", u"tab.input", None)) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_db), QCoreApplication.translate("MainWindow", u"tab.db", None)) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_ocr), QCoreApplication.translate("MainWindow", u"tab.ocr", None)) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_settings), QCoreApplication.translate("MainWindow", u"tab.settings", None)) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_about), QCoreApplication.translate("MainWindow", u"tab.about", None)) + pass + # retranslateUi + diff --git a/ui/designer/settings/settingsDefault.ui b/ui/designer/settings/settingsDefault.ui new file mode 100644 index 0000000..c3f6d37 --- /dev/null +++ b/ui/designer/settings/settingsDefault.ui @@ -0,0 +1,168 @@ + + + SettingsDefault + + + + 0 + 0 + 682 + 493 + + + + SettingsDefault + + + + QFormLayout::ExpandingFieldsGrow + + + QFormLayout::DontWrapRows + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + 0 + 0 + + + + devicesJsonFile + + + + + + + + + + 0 + 0 + + + + + 200 + 0 + + + + + + + + devicesJsonPath.resetButton + + + + + + + + + + 0 + 0 + + + + deviceUuid + + + + + + + + + + 0 + 0 + + + + + 200 + 0 + + + + + + + + defaultDevice.resetButton + + + + + + + + + + 0 + 0 + + + + tesseractFile + + + + + + + + 0 + 0 + + + + + 200 + 0 + + + + + + + + Qt::Vertical + + + QSizePolicy::Expanding + + + + 20 + 500000 + + + + + + + + + FileSelector + QWidget +
ui.implements.components.fileSelector
+ 1 +
+ + DevicesComboBox + QComboBox +
ui.implements.components
+
+
+ + +
diff --git a/ui/designer/settings/settingsDefault_ui.py b/ui/designer/settings/settingsDefault_ui.py new file mode 100644 index 0000000..3fcf8c9 --- /dev/null +++ b/ui/designer/settings/settingsDefault_ui.py @@ -0,0 +1,130 @@ +# -*- coding: utf-8 -*- + +################################################################################ +## Form generated from reading UI file 'settingsDefault.ui' +## +## Created by: Qt User Interface Compiler version 6.5.0 +## +## WARNING! All changes made in this file will be lost when recompiling UI file! +################################################################################ + +from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, + QMetaObject, QObject, QPoint, QRect, + QSize, QTime, QUrl, Qt) +from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, + QFont, QFontDatabase, QGradient, QIcon, + QImage, QKeySequence, QLinearGradient, QPainter, + QPalette, QPixmap, QRadialGradient, QTransform) +from PySide6.QtWidgets import (QApplication, QFormLayout, QHBoxLayout, QLabel, + QPushButton, QSizePolicy, QSpacerItem, QWidget) + +from ui.implements.components import DevicesComboBox +from ui.implements.components.fileSelector import FileSelector + +class Ui_SettingsDefault(object): + def setupUi(self, SettingsDefault): + if not SettingsDefault.objectName(): + SettingsDefault.setObjectName(u"SettingsDefault") + SettingsDefault.resize(682, 493) + SettingsDefault.setWindowTitle(u"SettingsDefault") + self.formLayout = QFormLayout(SettingsDefault) + self.formLayout.setObjectName(u"formLayout") + self.formLayout.setFieldGrowthPolicy(QFormLayout.ExpandingFieldsGrow) + self.formLayout.setRowWrapPolicy(QFormLayout.DontWrapRows) + self.formLayout.setLabelAlignment(Qt.AlignRight|Qt.AlignTrailing|Qt.AlignVCenter) + self.label_2 = QLabel(SettingsDefault) + self.label_2.setObjectName(u"label_2") + sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.MinimumExpanding) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.label_2.sizePolicy().hasHeightForWidth()) + self.label_2.setSizePolicy(sizePolicy) + + self.formLayout.setWidget(0, QFormLayout.LabelRole, self.label_2) + + self.horizontalLayout_2 = QHBoxLayout() + self.horizontalLayout_2.setObjectName(u"horizontalLayout_2") + self.devicesJsonFileSelector = FileSelector(SettingsDefault) + self.devicesJsonFileSelector.setObjectName(u"devicesJsonFileSelector") + sizePolicy1 = QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Preferred) + sizePolicy1.setHorizontalStretch(0) + sizePolicy1.setVerticalStretch(0) + sizePolicy1.setHeightForWidth(self.devicesJsonFileSelector.sizePolicy().hasHeightForWidth()) + self.devicesJsonFileSelector.setSizePolicy(sizePolicy1) + self.devicesJsonFileSelector.setMinimumSize(QSize(200, 0)) + + self.horizontalLayout_2.addWidget(self.devicesJsonFileSelector) + + self.devicesJsonFileResetButton = QPushButton(SettingsDefault) + self.devicesJsonFileResetButton.setObjectName(u"devicesJsonFileResetButton") + + self.horizontalLayout_2.addWidget(self.devicesJsonFileResetButton) + + + self.formLayout.setLayout(0, QFormLayout.FieldRole, self.horizontalLayout_2) + + self.label_3 = QLabel(SettingsDefault) + self.label_3.setObjectName(u"label_3") + sizePolicy.setHeightForWidth(self.label_3.sizePolicy().hasHeightForWidth()) + self.label_3.setSizePolicy(sizePolicy) + + self.formLayout.setWidget(1, QFormLayout.LabelRole, self.label_3) + + self.horizontalLayout = QHBoxLayout() + self.horizontalLayout.setObjectName(u"horizontalLayout") + self.devicesComboBox = DevicesComboBox(SettingsDefault) + self.devicesComboBox.setObjectName(u"devicesComboBox") + sizePolicy2 = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + sizePolicy2.setHorizontalStretch(0) + sizePolicy2.setVerticalStretch(0) + sizePolicy2.setHeightForWidth(self.devicesComboBox.sizePolicy().hasHeightForWidth()) + self.devicesComboBox.setSizePolicy(sizePolicy2) + self.devicesComboBox.setMinimumSize(QSize(200, 0)) + + self.horizontalLayout.addWidget(self.devicesComboBox) + + self.deviceUuidResetButton = QPushButton(SettingsDefault) + self.deviceUuidResetButton.setObjectName(u"deviceUuidResetButton") + + self.horizontalLayout.addWidget(self.deviceUuidResetButton) + + + self.formLayout.setLayout(1, QFormLayout.FieldRole, self.horizontalLayout) + + self.label_4 = QLabel(SettingsDefault) + self.label_4.setObjectName(u"label_4") + sizePolicy.setHeightForWidth(self.label_4.sizePolicy().hasHeightForWidth()) + self.label_4.setSizePolicy(sizePolicy) + + self.formLayout.setWidget(3, QFormLayout.LabelRole, self.label_4) + + self.tesseractFileSelector = FileSelector(SettingsDefault) + self.tesseractFileSelector.setObjectName(u"tesseractFileSelector") + sizePolicy3 = QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred) + sizePolicy3.setHorizontalStretch(0) + sizePolicy3.setVerticalStretch(0) + sizePolicy3.setHeightForWidth(self.tesseractFileSelector.sizePolicy().hasHeightForWidth()) + self.tesseractFileSelector.setSizePolicy(sizePolicy3) + self.tesseractFileSelector.setMinimumSize(QSize(200, 0)) + + self.formLayout.setWidget(3, QFormLayout.FieldRole, self.tesseractFileSelector) + + self.verticalSpacer = QSpacerItem(20, 500000, QSizePolicy.Minimum, QSizePolicy.Expanding) + + self.formLayout.setItem(5, QFormLayout.FieldRole, self.verticalSpacer) + + + self.retranslateUi(SettingsDefault) + + QMetaObject.connectSlotsByName(SettingsDefault) + # setupUi + + def retranslateUi(self, SettingsDefault): + self.label_2.setText(QCoreApplication.translate("SettingsDefault", u"devicesJsonFile", None)) + self.devicesJsonFileResetButton.setText(QCoreApplication.translate("SettingsDefault", u"devicesJsonPath.resetButton", None)) + self.label_3.setText(QCoreApplication.translate("SettingsDefault", u"deviceUuid", None)) + self.deviceUuidResetButton.setText(QCoreApplication.translate("SettingsDefault", u"defaultDevice.resetButton", None)) + self.label_4.setText(QCoreApplication.translate("SettingsDefault", u"tesseractFile", None)) + pass + # retranslateUi + diff --git a/ui/designer/tabs/tabAbout.ui b/ui/designer/tabs/tabAbout.ui new file mode 100644 index 0000000..1e1d116 --- /dev/null +++ b/ui/designer/tabs/tabAbout.ui @@ -0,0 +1,102 @@ + + + TabAbout + + + + 0 + 0 + 587 + 431 + + + + TabAbout + + + + + + + 0 + 0 + + + + + + + Qt::AlignCenter + + + + + + + + 14 + + + + arcaea-offline-pyside-ui + + + Qt::AlignBottom|Qt::AlignHCenter + + + + + + + A part of <a href="https://github.com/283375/arcaea-offline">arcaea-offline project</a> + + + Qt::AlignHCenter|Qt::AlignTop + + + true + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + About Qt + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + diff --git a/ui/designer/tabs/tabAbout_ui.py b/ui/designer/tabs/tabAbout_ui.py new file mode 100644 index 0000000..e99a355 --- /dev/null +++ b/ui/designer/tabs/tabAbout_ui.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- + +################################################################################ +## Form generated from reading UI file 'tabAbout.ui' +## +## Created by: Qt User Interface Compiler version 6.5.0 +## +## WARNING! All changes made in this file will be lost when recompiling UI file! +################################################################################ + +from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, + QMetaObject, QObject, QPoint, QRect, + QSize, QTime, QUrl, Qt) +from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, + QFont, QFontDatabase, QGradient, QIcon, + QImage, QKeySequence, QLinearGradient, QPainter, + QPalette, QPixmap, QRadialGradient, QTransform) +from PySide6.QtWidgets import (QApplication, QHBoxLayout, QLabel, QPushButton, + QSizePolicy, QSpacerItem, QVBoxLayout, QWidget) + +class Ui_TabAbout(object): + def setupUi(self, TabAbout): + if not TabAbout.objectName(): + TabAbout.setObjectName(u"TabAbout") + TabAbout.resize(587, 431) + TabAbout.setWindowTitle(u"TabAbout") + self.verticalLayout = QVBoxLayout(TabAbout) + self.verticalLayout.setObjectName(u"verticalLayout") + self.logoLabel = QLabel(TabAbout) + self.logoLabel.setObjectName(u"logoLabel") + sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.logoLabel.sizePolicy().hasHeightForWidth()) + self.logoLabel.setSizePolicy(sizePolicy) + self.logoLabel.setText(u"") + self.logoLabel.setAlignment(Qt.AlignCenter) + + self.verticalLayout.addWidget(self.logoLabel) + + self.label = QLabel(TabAbout) + self.label.setObjectName(u"label") + font = QFont() + font.setPointSize(14) + self.label.setFont(font) + self.label.setText(u"arcaea-offline-pyside-ui") + self.label.setAlignment(Qt.AlignBottom|Qt.AlignHCenter) + + self.verticalLayout.addWidget(self.label) + + self.label_2 = QLabel(TabAbout) + self.label_2.setObjectName(u"label_2") + self.label_2.setText(u"A part of arcaea-offline project") + self.label_2.setAlignment(Qt.AlignHCenter|Qt.AlignTop) + self.label_2.setOpenExternalLinks(True) + + self.verticalLayout.addWidget(self.label_2) + + self.horizontalLayout = QHBoxLayout() + self.horizontalLayout.setObjectName(u"horizontalLayout") + self.horizontalSpacer = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) + + self.horizontalLayout.addItem(self.horizontalSpacer) + + self.aboutQtButton = QPushButton(TabAbout) + self.aboutQtButton.setObjectName(u"aboutQtButton") + + self.horizontalLayout.addWidget(self.aboutQtButton) + + self.horizontalSpacer_2 = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) + + self.horizontalLayout.addItem(self.horizontalSpacer_2) + + + self.verticalLayout.addLayout(self.horizontalLayout) + + + self.retranslateUi(TabAbout) + + QMetaObject.connectSlotsByName(TabAbout) + # setupUi + + def retranslateUi(self, TabAbout): + self.aboutQtButton.setText(QCoreApplication.translate("TabAbout", u"About Qt", None)) + pass + # retranslateUi + diff --git a/ui/designer/tabs/tabDb/tabDb_Manage.ui b/ui/designer/tabs/tabDb/tabDb_Manage.ui new file mode 100644 index 0000000..cbc2e06 --- /dev/null +++ b/ui/designer/tabs/tabDb/tabDb_Manage.ui @@ -0,0 +1,38 @@ + + + TabDb_Manage + + + + 0 + 0 + 630 + 528 + + + + TabDb_Manage + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + syncArcSongDbButton + + + + + + + syncArcSongDb.description + + + + + + + + diff --git a/ui/designer/tabs/tabDb/tabDb_Manage_ui.py b/ui/designer/tabs/tabDb/tabDb_Manage_ui.py new file mode 100644 index 0000000..03027cd --- /dev/null +++ b/ui/designer/tabs/tabDb/tabDb_Manage_ui.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- + +################################################################################ +## Form generated from reading UI file 'tabDb_Manage.ui' +## +## Created by: Qt User Interface Compiler version 6.5.0 +## +## WARNING! All changes made in this file will be lost when recompiling UI file! +################################################################################ + +from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, + QMetaObject, QObject, QPoint, QRect, + QSize, QTime, QUrl, Qt) +from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, + QFont, QFontDatabase, QGradient, QIcon, + QImage, QKeySequence, QLinearGradient, QPainter, + QPalette, QPixmap, QRadialGradient, QTransform) +from PySide6.QtWidgets import (QApplication, QFormLayout, QLabel, QPushButton, + QSizePolicy, QWidget) + +class Ui_TabDb_Manage(object): + def setupUi(self, TabDb_Manage): + if not TabDb_Manage.objectName(): + TabDb_Manage.setObjectName(u"TabDb_Manage") + TabDb_Manage.resize(630, 528) + TabDb_Manage.setWindowTitle(u"TabDb_Manage") + self.formLayout = QFormLayout(TabDb_Manage) + self.formLayout.setObjectName(u"formLayout") + self.formLayout.setLabelAlignment(Qt.AlignRight|Qt.AlignTrailing|Qt.AlignVCenter) + self.syncArcSongDbButton = QPushButton(TabDb_Manage) + self.syncArcSongDbButton.setObjectName(u"syncArcSongDbButton") + + self.formLayout.setWidget(0, QFormLayout.LabelRole, self.syncArcSongDbButton) + + self.label = QLabel(TabDb_Manage) + self.label.setObjectName(u"label") + + self.formLayout.setWidget(0, QFormLayout.FieldRole, self.label) + + + self.retranslateUi(TabDb_Manage) + + QMetaObject.connectSlotsByName(TabDb_Manage) + # setupUi + + def retranslateUi(self, TabDb_Manage): + self.syncArcSongDbButton.setText(QCoreApplication.translate("TabDb_Manage", u"syncArcSongDbButton", None)) + self.label.setText(QCoreApplication.translate("TabDb_Manage", u"syncArcSongDb.description", None)) + pass + # retranslateUi + diff --git a/ui/designer/tabs/tabDbEntry.ui b/ui/designer/tabs/tabDbEntry.ui new file mode 100644 index 0000000..be1da1d --- /dev/null +++ b/ui/designer/tabs/tabDbEntry.ui @@ -0,0 +1,41 @@ + + + TabDbEntry + + + + 0 + 0 + 648 + 579 + + + + TabDbEntry + + + + + + 0 + + + + tab.manage + + + + + + + + + TabDb_Manage + QWidget +
ui.implements.tabs.tabDb.tabDb_Manage
+ 1 +
+
+ + +
diff --git a/ui/designer/tabs/tabDbEntry_ui.py b/ui/designer/tabs/tabDbEntry_ui.py new file mode 100644 index 0000000..44c2e1d --- /dev/null +++ b/ui/designer/tabs/tabDbEntry_ui.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- + +################################################################################ +## Form generated from reading UI file 'tabDbEntry.ui' +## +## Created by: Qt User Interface Compiler version 6.5.0 +## +## WARNING! All changes made in this file will be lost when recompiling UI file! +################################################################################ + +from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, + QMetaObject, QObject, QPoint, QRect, + QSize, QTime, QUrl, Qt) +from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, + QFont, QFontDatabase, QGradient, QIcon, + QImage, QKeySequence, QLinearGradient, QPainter, + QPalette, QPixmap, QRadialGradient, QTransform) +from PySide6.QtWidgets import (QApplication, QSizePolicy, QTabWidget, QVBoxLayout, + QWidget) + +from ui.implements.tabs.tabDb.tabDb_Manage import TabDb_Manage + +class Ui_TabDbEntry(object): + def setupUi(self, TabDbEntry): + if not TabDbEntry.objectName(): + TabDbEntry.setObjectName(u"TabDbEntry") + TabDbEntry.resize(648, 579) + TabDbEntry.setWindowTitle(u"TabDbEntry") + self.verticalLayout = QVBoxLayout(TabDbEntry) + self.verticalLayout.setObjectName(u"verticalLayout") + self.tabWidget = QTabWidget(TabDbEntry) + self.tabWidget.setObjectName(u"tabWidget") + self.tab_manage = TabDb_Manage() + self.tab_manage.setObjectName(u"tab_manage") + self.tabWidget.addTab(self.tab_manage, "") + + self.verticalLayout.addWidget(self.tabWidget) + + + self.retranslateUi(TabDbEntry) + + self.tabWidget.setCurrentIndex(0) + + + QMetaObject.connectSlotsByName(TabDbEntry) + # setupUi + + def retranslateUi(self, TabDbEntry): + self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_manage), QCoreApplication.translate("TabDbEntry", u"tab.manage", None)) + pass + # retranslateUi + diff --git a/ui/designer/tabs/tabInputScore.ui b/ui/designer/tabs/tabInputScore.ui new file mode 100644 index 0000000..6cf7a43 --- /dev/null +++ b/ui/designer/tabs/tabInputScore.ui @@ -0,0 +1,89 @@ + + + TabInputScore + + + + 0 + 0 + 514 + 400 + + + + TabInputScore + + + + + + + 0 + 0 + + + + tab.selectChart + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + + + tab.scoreEdit + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + + + + ChartSelector + QWidget +
ui.implements.components.chartSelector
+ 1 +
+ + ScoreEditor + QWidget +
ui.implements.components.scoreEditor
+ 1 +
+
+ + +
diff --git a/ui/designer/tabs/tabInputScore_ui.py b/ui/designer/tabs/tabInputScore_ui.py new file mode 100644 index 0000000..68c34a7 --- /dev/null +++ b/ui/designer/tabs/tabInputScore_ui.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- + +################################################################################ +## Form generated from reading UI file 'tabInputScore.ui' +## +## Created by: Qt User Interface Compiler version 6.5.0 +## +## WARNING! All changes made in this file will be lost when recompiling UI file! +################################################################################ + +from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, + QMetaObject, QObject, QPoint, QRect, + QSize, QTime, QUrl, Qt) +from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, + QFont, QFontDatabase, QGradient, QIcon, + QImage, QKeySequence, QLinearGradient, QPainter, + QPalette, QPixmap, QRadialGradient, QTransform) +from PySide6.QtWidgets import (QApplication, QGroupBox, QSizePolicy, QVBoxLayout, + QWidget) + +from ui.implements.components.chartSelector import ChartSelector +from ui.implements.components.scoreEditor import ScoreEditor + +class Ui_TabInputScore(object): + def setupUi(self, TabInputScore): + if not TabInputScore.objectName(): + TabInputScore.setObjectName(u"TabInputScore") + TabInputScore.resize(514, 400) + TabInputScore.setWindowTitle(u"TabInputScore") + self.verticalLayout = QVBoxLayout(TabInputScore) + self.verticalLayout.setObjectName(u"verticalLayout") + self.groupBox = QGroupBox(TabInputScore) + self.groupBox.setObjectName(u"groupBox") + sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.groupBox.sizePolicy().hasHeightForWidth()) + self.groupBox.setSizePolicy(sizePolicy) + self.verticalLayout_2 = QVBoxLayout(self.groupBox) + self.verticalLayout_2.setObjectName(u"verticalLayout_2") + self.verticalLayout_2.setContentsMargins(0, 0, 0, 0) + self.chartSelector = ChartSelector(self.groupBox) + self.chartSelector.setObjectName(u"chartSelector") + + self.verticalLayout_2.addWidget(self.chartSelector) + + + self.verticalLayout.addWidget(self.groupBox) + + self.groupBox_2 = QGroupBox(TabInputScore) + self.groupBox_2.setObjectName(u"groupBox_2") + self.verticalLayout_3 = QVBoxLayout(self.groupBox_2) + self.verticalLayout_3.setObjectName(u"verticalLayout_3") + self.verticalLayout_3.setContentsMargins(0, 0, 0, 0) + self.scoreEditor = ScoreEditor(self.groupBox_2) + self.scoreEditor.setObjectName(u"scoreEditor") + + self.verticalLayout_3.addWidget(self.scoreEditor) + + + self.verticalLayout.addWidget(self.groupBox_2) + + + self.retranslateUi(TabInputScore) + + QMetaObject.connectSlotsByName(TabInputScore) + # setupUi + + def retranslateUi(self, TabInputScore): + self.groupBox.setTitle(QCoreApplication.translate("TabInputScore", u"tab.selectChart", None)) + self.groupBox_2.setTitle(QCoreApplication.translate("TabInputScore", u"tab.scoreEdit", None)) + pass + # retranslateUi + diff --git a/ui/designer/tabs/tabOcr.ui b/ui/designer/tabs/tabOcr.ui new file mode 100644 index 0000000..4be5b93 --- /dev/null +++ b/ui/designer/tabs/tabOcr.ui @@ -0,0 +1,198 @@ + + + TabOcr + + + + 0 + 0 + 632 + 527 + + + + TabOcr + + + + + + openWizardButton + + + + + + + deviceSelector.title + + + + + + + + + + + + + + + tesseractSelector.title + + + + + + + + + + + + ocr.title + + + + + + ocr.queue.title + + + + + + ocr.queue.addImageButton + + + + + + + true + + + ocr.queue.removeSelected + + + + + + + true + + + ocr.queue.removeAll + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + ocr.queue.startOcrButton + + + + + + + + + + QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed + + + QAbstractItemView::MultiSelection + + + QAbstractItemView::SelectRows + + + QAbstractItemView::ScrollPerPixel + + + QAbstractItemView::ScrollPerPixel + + + + + + + ocr.results + + + + + + true + + + ocr.results.acceptSelectedButton + + + + + + + ocr.results.acceptAllButton + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + ocr.results.ignoreValidate + + + + + + + + + + + + + + FileSelector + QWidget +
ui.implements.components
+ 1 +
+ + DevicesComboBox + QComboBox +
ui.implements.components
+
+
+ + +
diff --git a/ui/designer/tabs/tabOcrDisabled.ui b/ui/designer/tabs/tabOcrDisabled.ui new file mode 100644 index 0000000..04ac70c --- /dev/null +++ b/ui/designer/tabs/tabOcrDisabled.ui @@ -0,0 +1,105 @@ + + + TabOcrDisabled + + + + 0 + 0 + 564 + 468 + + + + TabOcrDisabled + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + + + ocrDisabled.title + + + + + + + + 0 + 0 + + + + ... + + + + + + + + + + + diff --git a/ui/designer/tabs/tabOcrDisabled_ui.py b/ui/designer/tabs/tabOcrDisabled_ui.py new file mode 100644 index 0000000..e6575c8 --- /dev/null +++ b/ui/designer/tabs/tabOcrDisabled_ui.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- + +################################################################################ +## Form generated from reading UI file 'tabOcrDisabled.ui' +## +## Created by: Qt User Interface Compiler version 6.5.0 +## +## WARNING! All changes made in this file will be lost when recompiling UI file! +################################################################################ + +from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, + QMetaObject, QObject, QPoint, QRect, + QSize, QTime, QUrl, Qt) +from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, + QFont, QFontDatabase, QGradient, QIcon, + QImage, QKeySequence, QLinearGradient, QPainter, + QPalette, QPixmap, QRadialGradient, QTransform) +from PySide6.QtWidgets import (QApplication, QGridLayout, QLabel, QSizePolicy, + QSpacerItem, QVBoxLayout, QWidget) + +class Ui_TabOcrDisabled(object): + def setupUi(self, TabOcrDisabled): + if not TabOcrDisabled.objectName(): + TabOcrDisabled.setObjectName(u"TabOcrDisabled") + TabOcrDisabled.resize(564, 468) + TabOcrDisabled.setWindowTitle(u"TabOcrDisabled") + self.gridLayout = QGridLayout(TabOcrDisabled) + self.gridLayout.setObjectName(u"gridLayout") + self.horizontalSpacer = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) + + self.gridLayout.addItem(self.horizontalSpacer, 1, 0, 1, 1) + + self.verticalSpacer_2 = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding) + + self.gridLayout.addItem(self.verticalSpacer_2, 2, 1, 1, 1) + + self.verticalSpacer = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding) + + self.gridLayout.addItem(self.verticalSpacer, 0, 1, 1, 1) + + self.horizontalSpacer_2 = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) + + self.gridLayout.addItem(self.horizontalSpacer_2, 1, 2, 1, 1) + + self.widget = QWidget(TabOcrDisabled) + self.widget.setObjectName(u"widget") + sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.widget.sizePolicy().hasHeightForWidth()) + self.widget.setSizePolicy(sizePolicy) + self.verticalLayout = QVBoxLayout(self.widget) + self.verticalLayout.setObjectName(u"verticalLayout") + self.label = QLabel(self.widget) + self.label.setObjectName(u"label") + + self.verticalLayout.addWidget(self.label) + + self.contentLabel = QLabel(self.widget) + self.contentLabel.setObjectName(u"contentLabel") + sizePolicy.setHeightForWidth(self.contentLabel.sizePolicy().hasHeightForWidth()) + self.contentLabel.setSizePolicy(sizePolicy) + self.contentLabel.setText(u"...") + + self.verticalLayout.addWidget(self.contentLabel) + + + self.gridLayout.addWidget(self.widget, 1, 1, 1, 1) + + + self.retranslateUi(TabOcrDisabled) + + QMetaObject.connectSlotsByName(TabOcrDisabled) + # setupUi + + def retranslateUi(self, TabOcrDisabled): + self.label.setText(QCoreApplication.translate("TabOcrDisabled", u"ocrDisabled.title", None)) + pass + # retranslateUi + diff --git a/ui/designer/tabs/tabOcr_ui.py b/ui/designer/tabs/tabOcr_ui.py new file mode 100644 index 0000000..3b466ab --- /dev/null +++ b/ui/designer/tabs/tabOcr_ui.py @@ -0,0 +1,165 @@ +# -*- coding: utf-8 -*- + +################################################################################ +## Form generated from reading UI file 'tabOcr.ui' +## +## Created by: Qt User Interface Compiler version 6.5.0 +## +## WARNING! All changes made in this file will be lost when recompiling UI file! +################################################################################ + +from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, + QMetaObject, QObject, QPoint, QRect, + QSize, QTime, QUrl, Qt) +from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, + QFont, QFontDatabase, QGradient, QIcon, + QImage, QKeySequence, QLinearGradient, QPainter, + QPalette, QPixmap, QRadialGradient, QTransform) +from PySide6.QtWidgets import (QAbstractItemView, QApplication, QCheckBox, QGroupBox, + QHBoxLayout, QHeaderView, QPushButton, QSizePolicy, + QSpacerItem, QTableView, QVBoxLayout, QWidget) + +from ui.implements.components import (DevicesComboBox, FileSelector) + +class Ui_TabOcr(object): + def setupUi(self, TabOcr): + if not TabOcr.objectName(): + TabOcr.setObjectName(u"TabOcr") + TabOcr.resize(632, 527) + TabOcr.setWindowTitle(u"TabOcr") + self.verticalLayout_3 = QVBoxLayout(TabOcr) + self.verticalLayout_3.setObjectName(u"verticalLayout_3") + self.openWizardButton = QPushButton(TabOcr) + self.openWizardButton.setObjectName(u"openWizardButton") + + self.verticalLayout_3.addWidget(self.openWizardButton) + + self.groupBox = QGroupBox(TabOcr) + self.groupBox.setObjectName(u"groupBox") + self.verticalLayout = QVBoxLayout(self.groupBox) + self.verticalLayout.setObjectName(u"verticalLayout") + self.deviceFileSelector = FileSelector(self.groupBox) + self.deviceFileSelector.setObjectName(u"deviceFileSelector") + + self.verticalLayout.addWidget(self.deviceFileSelector) + + self.deviceComboBox = DevicesComboBox(self.groupBox) + self.deviceComboBox.setObjectName(u"deviceComboBox") + + self.verticalLayout.addWidget(self.deviceComboBox) + + + self.verticalLayout_3.addWidget(self.groupBox) + + self.groupBox_4 = QGroupBox(TabOcr) + self.groupBox_4.setObjectName(u"groupBox_4") + self.verticalLayout_5 = QVBoxLayout(self.groupBox_4) + self.verticalLayout_5.setObjectName(u"verticalLayout_5") + self.tesseractFileSelector = FileSelector(self.groupBox_4) + self.tesseractFileSelector.setObjectName(u"tesseractFileSelector") + + self.verticalLayout_5.addWidget(self.tesseractFileSelector) + + + self.verticalLayout_3.addWidget(self.groupBox_4) + + self.groupBox_2 = QGroupBox(TabOcr) + self.groupBox_2.setObjectName(u"groupBox_2") + self.horizontalLayout = QHBoxLayout(self.groupBox_2) + self.horizontalLayout.setObjectName(u"horizontalLayout") + self.groupBox_3 = QGroupBox(self.groupBox_2) + self.groupBox_3.setObjectName(u"groupBox_3") + self.verticalLayout_2 = QVBoxLayout(self.groupBox_3) + self.verticalLayout_2.setObjectName(u"verticalLayout_2") + self.ocr_addImageButton = QPushButton(self.groupBox_3) + self.ocr_addImageButton.setObjectName(u"ocr_addImageButton") + + self.verticalLayout_2.addWidget(self.ocr_addImageButton) + + self.ocr_removeSelectedButton = QPushButton(self.groupBox_3) + self.ocr_removeSelectedButton.setObjectName(u"ocr_removeSelectedButton") + self.ocr_removeSelectedButton.setEnabled(True) + + self.verticalLayout_2.addWidget(self.ocr_removeSelectedButton) + + self.ocr_removeAllButton = QPushButton(self.groupBox_3) + self.ocr_removeAllButton.setObjectName(u"ocr_removeAllButton") + self.ocr_removeAllButton.setEnabled(True) + + self.verticalLayout_2.addWidget(self.ocr_removeAllButton) + + self.verticalSpacer = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding) + + self.verticalLayout_2.addItem(self.verticalSpacer) + + self.ocr_startButton = QPushButton(self.groupBox_3) + self.ocr_startButton.setObjectName(u"ocr_startButton") + + self.verticalLayout_2.addWidget(self.ocr_startButton) + + + self.horizontalLayout.addWidget(self.groupBox_3) + + self.tableView = QTableView(self.groupBox_2) + self.tableView.setObjectName(u"tableView") + self.tableView.setEditTriggers(QAbstractItemView.DoubleClicked|QAbstractItemView.EditKeyPressed) + self.tableView.setSelectionMode(QAbstractItemView.MultiSelection) + self.tableView.setSelectionBehavior(QAbstractItemView.SelectRows) + self.tableView.setVerticalScrollMode(QAbstractItemView.ScrollPerPixel) + self.tableView.setHorizontalScrollMode(QAbstractItemView.ScrollPerPixel) + + self.horizontalLayout.addWidget(self.tableView) + + self.groupBox_5 = QGroupBox(self.groupBox_2) + self.groupBox_5.setObjectName(u"groupBox_5") + self.verticalLayout_4 = QVBoxLayout(self.groupBox_5) + self.verticalLayout_4.setObjectName(u"verticalLayout_4") + self.ocr_acceptSelectedButton = QPushButton(self.groupBox_5) + self.ocr_acceptSelectedButton.setObjectName(u"ocr_acceptSelectedButton") + self.ocr_acceptSelectedButton.setEnabled(True) + + self.verticalLayout_4.addWidget(self.ocr_acceptSelectedButton) + + self.ocr_acceptAllButton = QPushButton(self.groupBox_5) + self.ocr_acceptAllButton.setObjectName(u"ocr_acceptAllButton") + + self.verticalLayout_4.addWidget(self.ocr_acceptAllButton) + + self.verticalSpacer_2 = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding) + + self.verticalLayout_4.addItem(self.verticalSpacer_2) + + self.ocr_ignoreValidateCheckBox = QCheckBox(self.groupBox_5) + self.ocr_ignoreValidateCheckBox.setObjectName(u"ocr_ignoreValidateCheckBox") + + self.verticalLayout_4.addWidget(self.ocr_ignoreValidateCheckBox) + + + self.horizontalLayout.addWidget(self.groupBox_5) + + + self.verticalLayout_3.addWidget(self.groupBox_2) + + + self.retranslateUi(TabOcr) + + QMetaObject.connectSlotsByName(TabOcr) + # setupUi + + def retranslateUi(self, TabOcr): + self.openWizardButton.setText(QCoreApplication.translate("TabOcr", u"openWizardButton", None)) + self.groupBox.setTitle(QCoreApplication.translate("TabOcr", u"deviceSelector.title", None)) + self.groupBox_4.setTitle(QCoreApplication.translate("TabOcr", u"tesseractSelector.title", None)) + self.groupBox_2.setTitle(QCoreApplication.translate("TabOcr", u"ocr.title", None)) + self.groupBox_3.setTitle(QCoreApplication.translate("TabOcr", u"ocr.queue.title", None)) + self.ocr_addImageButton.setText(QCoreApplication.translate("TabOcr", u"ocr.queue.addImageButton", None)) + self.ocr_removeSelectedButton.setText(QCoreApplication.translate("TabOcr", u"ocr.queue.removeSelected", None)) + self.ocr_removeAllButton.setText(QCoreApplication.translate("TabOcr", u"ocr.queue.removeAll", None)) + self.ocr_startButton.setText(QCoreApplication.translate("TabOcr", u"ocr.queue.startOcrButton", None)) + self.groupBox_5.setTitle(QCoreApplication.translate("TabOcr", u"ocr.results", None)) + self.ocr_acceptSelectedButton.setText(QCoreApplication.translate("TabOcr", u"ocr.results.acceptSelectedButton", None)) + self.ocr_acceptAllButton.setText(QCoreApplication.translate("TabOcr", u"ocr.results.acceptAllButton", None)) + self.ocr_ignoreValidateCheckBox.setText(QCoreApplication.translate("TabOcr", u"ocr.results.ignoreValidate", None)) + pass + # retranslateUi + diff --git a/ui/designer/tabs/tabOverview.ui b/ui/designer/tabs/tabOverview.ui new file mode 100644 index 0000000..358ec4c --- /dev/null +++ b/ui/designer/tabs/tabOverview.ui @@ -0,0 +1,117 @@ + + + TabOverview + + + + 0 + 0 + 696 + 509 + + + + TabOverview + + + + + + + + + + 0 + 0 + + + + + + + + + + + 30 + + + + 0.00 + + + Qt::AlignBottom|Qt::AlignHCenter + + + + + + + + 20 + + + + B30 + + + Qt::AlignHCenter|Qt::AlignTop + + + + + + + + + + false + + + + + + false + + + + 30 + + + + -- + + + Qt::AlignBottom|Qt::AlignHCenter + + + + + + + false + + + + 20 + + + + R10 + + + Qt::AlignHCenter|Qt::AlignTop + + + + + + + + + + + + + + diff --git a/ui/designer/tabs/tabOverview_ui.py b/ui/designer/tabs/tabOverview_ui.py new file mode 100644 index 0000000..5881001 --- /dev/null +++ b/ui/designer/tabs/tabOverview_ui.py @@ -0,0 +1,108 @@ +# -*- coding: utf-8 -*- + +################################################################################ +## Form generated from reading UI file 'tabOverview.ui' +## +## Created by: Qt User Interface Compiler version 6.5.0 +## +## WARNING! All changes made in this file will be lost when recompiling UI file! +################################################################################ + +from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, + QMetaObject, QObject, QPoint, QRect, + QSize, QTime, QUrl, Qt) +from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, + QFont, QFontDatabase, QGradient, QIcon, + QImage, QKeySequence, QLinearGradient, QPainter, + QPalette, QPixmap, QRadialGradient, QTransform) +from PySide6.QtWidgets import (QApplication, QHBoxLayout, QLabel, QSizePolicy, + QVBoxLayout, QWidget) + +class Ui_TabOverview(object): + def setupUi(self, TabOverview): + if not TabOverview.objectName(): + TabOverview.setObjectName(u"TabOverview") + TabOverview.resize(696, 509) + TabOverview.setWindowTitle(u"TabOverview") + self.verticalLayout = QVBoxLayout(TabOverview) + self.verticalLayout.setObjectName(u"verticalLayout") + self.widget = QWidget(TabOverview) + self.widget.setObjectName(u"widget") + + self.verticalLayout.addWidget(self.widget) + + self.widget_2 = QWidget(TabOverview) + self.widget_2.setObjectName(u"widget_2") + sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.widget_2.sizePolicy().hasHeightForWidth()) + self.widget_2.setSizePolicy(sizePolicy) + self.horizontalLayout = QHBoxLayout(self.widget_2) + self.horizontalLayout.setObjectName(u"horizontalLayout") + self.widget_3 = QWidget(self.widget_2) + self.widget_3.setObjectName(u"widget_3") + self.verticalLayout_2 = QVBoxLayout(self.widget_3) + self.verticalLayout_2.setObjectName(u"verticalLayout_2") + self.b30Label = QLabel(self.widget_3) + self.b30Label.setObjectName(u"b30Label") + font = QFont() + font.setPointSize(30) + self.b30Label.setFont(font) + self.b30Label.setText(u"0.00") + self.b30Label.setAlignment(Qt.AlignBottom|Qt.AlignHCenter) + + self.verticalLayout_2.addWidget(self.b30Label) + + self.label_2 = QLabel(self.widget_3) + self.label_2.setObjectName(u"label_2") + font1 = QFont() + font1.setPointSize(20) + self.label_2.setFont(font1) + self.label_2.setText(u"B30") + self.label_2.setAlignment(Qt.AlignHCenter|Qt.AlignTop) + + self.verticalLayout_2.addWidget(self.label_2) + + + self.horizontalLayout.addWidget(self.widget_3) + + self.widget_4 = QWidget(self.widget_2) + self.widget_4.setObjectName(u"widget_4") + self.widget_4.setEnabled(False) + self.verticalLayout_3 = QVBoxLayout(self.widget_4) + self.verticalLayout_3.setObjectName(u"verticalLayout_3") + self.r10Label = QLabel(self.widget_4) + self.r10Label.setObjectName(u"r10Label") + self.r10Label.setEnabled(False) + self.r10Label.setFont(font) + self.r10Label.setText(u"--") + self.r10Label.setAlignment(Qt.AlignBottom|Qt.AlignHCenter) + + self.verticalLayout_3.addWidget(self.r10Label) + + self.label_4 = QLabel(self.widget_4) + self.label_4.setObjectName(u"label_4") + self.label_4.setEnabled(False) + self.label_4.setFont(font1) + self.label_4.setText(u"R10") + self.label_4.setAlignment(Qt.AlignHCenter|Qt.AlignTop) + + self.verticalLayout_3.addWidget(self.label_4) + + + self.horizontalLayout.addWidget(self.widget_4) + + + self.verticalLayout.addWidget(self.widget_2) + + + self.retranslateUi(TabOverview) + + QMetaObject.connectSlotsByName(TabOverview) + # setupUi + + def retranslateUi(self, TabOverview): + pass + # retranslateUi + diff --git a/ui/designer/tabs/tabSettings.ui b/ui/designer/tabs/tabSettings.ui new file mode 100644 index 0000000..322d5c3 --- /dev/null +++ b/ui/designer/tabs/tabSettings.ui @@ -0,0 +1,74 @@ + + + TabSettings + + + + 0 + 0 + 562 + 499 + + + + TabSettings + + + + + + + 0 + 0 + + + + + 100 + 0 + + + + + 100 + 0 + + + + QAbstractScrollArea::AdjustToContents + + + QAbstractItemView::NoEditTriggers + + + true + + + QAbstractItemView::SelectRows + + + + + + + + 0 + 0 + + + + + + + + + + SettingsDefault + QWidget +
ui.implements.settings.settingsDefault
+ 1 +
+
+ + +
diff --git a/ui/designer/tabs/tabSettings_ui.py b/ui/designer/tabs/tabSettings_ui.py new file mode 100644 index 0000000..29e3397 --- /dev/null +++ b/ui/designer/tabs/tabSettings_ui.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- + +################################################################################ +## Form generated from reading UI file 'tabSettings.ui' +## +## Created by: Qt User Interface Compiler version 6.5.0 +## +## WARNING! All changes made in this file will be lost when recompiling UI file! +################################################################################ + +from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, + QMetaObject, QObject, QPoint, QRect, + QSize, QTime, QUrl, Qt) +from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, + QFont, QFontDatabase, QGradient, QIcon, + QImage, QKeySequence, QLinearGradient, QPainter, + QPalette, QPixmap, QRadialGradient, QTransform) +from PySide6.QtWidgets import (QAbstractItemView, QAbstractScrollArea, QApplication, QHBoxLayout, + QListWidget, QListWidgetItem, QSizePolicy, QStackedWidget, + QWidget) + +from ui.implements.settings.settingsDefault import SettingsDefault + +class Ui_TabSettings(object): + def setupUi(self, TabSettings): + if not TabSettings.objectName(): + TabSettings.setObjectName(u"TabSettings") + TabSettings.resize(562, 499) + TabSettings.setWindowTitle(u"TabSettings") + self.horizontalLayout = QHBoxLayout(TabSettings) + self.horizontalLayout.setObjectName(u"horizontalLayout") + self.listWidget = QListWidget(TabSettings) + self.listWidget.setObjectName(u"listWidget") + sizePolicy = QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Expanding) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.listWidget.sizePolicy().hasHeightForWidth()) + self.listWidget.setSizePolicy(sizePolicy) + self.listWidget.setMinimumSize(QSize(100, 0)) + self.listWidget.setBaseSize(QSize(100, 0)) + self.listWidget.setSizeAdjustPolicy(QAbstractScrollArea.AdjustToContents) + self.listWidget.setEditTriggers(QAbstractItemView.NoEditTriggers) + self.listWidget.setAlternatingRowColors(True) + self.listWidget.setSelectionBehavior(QAbstractItemView.SelectRows) + + self.horizontalLayout.addWidget(self.listWidget) + + self.stackedWidget = QStackedWidget(TabSettings) + self.stackedWidget.setObjectName(u"stackedWidget") + sizePolicy1 = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) + sizePolicy1.setHorizontalStretch(0) + sizePolicy1.setVerticalStretch(0) + sizePolicy1.setHeightForWidth(self.stackedWidget.sizePolicy().hasHeightForWidth()) + self.stackedWidget.setSizePolicy(sizePolicy1) + self.page_default = SettingsDefault() + self.page_default.setObjectName(u"page_default") + self.stackedWidget.addWidget(self.page_default) + + self.horizontalLayout.addWidget(self.stackedWidget) + + self.horizontalLayout.setStretch(1, 1) + + self.retranslateUi(TabSettings) + + QMetaObject.connectSlotsByName(TabSettings) + # setupUi + + def retranslateUi(self, TabSettings): + pass + # retranslateUi + diff --git a/ui/extends/__init__.py b/ui/extends/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ui/extends/color.py b/ui/extends/color.py new file mode 100644 index 0000000..9d36507 --- /dev/null +++ b/ui/extends/color.py @@ -0,0 +1,16 @@ +from PySide6.QtGui import QColor + + +def mix_color(source_color: QColor, mix_color: QColor, mix_ratio: float = 0.5): + r = round((mix_color.red() - source_color.red()) * mix_ratio + source_color.red()) + g = round( + (mix_color.green() - source_color.green()) * mix_ratio + source_color.green() + ) + b = round( + (mix_color.blue() - source_color.blue()) * mix_ratio + source_color.blue() + ) + a = round( + (mix_color.alpha() - source_color.alpha()) * mix_ratio + source_color.alpha() + ) + + return QColor(r, g, b, a) diff --git a/ui/extends/components/chartSelector.py b/ui/extends/components/chartSelector.py new file mode 100644 index 0000000..abf69cc --- /dev/null +++ b/ui/extends/components/chartSelector.py @@ -0,0 +1,33 @@ +from arcaea_offline.database import Database +from arcaea_offline.models import Chart +from arcaea_offline.utils import rating_class_to_short_text +from PySide6.QtCore import Qt +from PySide6.QtGui import QStandardItem, QStandardItemModel + + +class FuzzySearchCompleterModel(QStandardItemModel): + def fillDbFuzzySearchResults(self, db: Database, kw: str): + self.clear() + + results = db.fuzzy_search_song_id(kw, limit=10) + results = sorted(results, key=lambda r: r.confidence, reverse=True) + songIds = [r.song_id for r in results] + charts: list[Chart] = [] + for songId in songIds: + dbChartRows = db.get_charts_by_song_id(songId) + _charts = [Chart.from_db_row(dbRow) for dbRow in dbChartRows] + _charts = sorted(_charts, key=lambda c: c.rating_class, reverse=True) + charts += _charts + + for chart in charts: + displayText = ( + f"{chart.name_en} [{rating_class_to_short_text(chart.rating_class)}]" + ) + item = QStandardItem(kw) + item.setData(kw) + item.setData(displayText, Qt.ItemDataRole.UserRole + 75) + item.setData( + f"{chart.song_id}, {chart.package_id}", Qt.ItemDataRole.UserRole + 76 + ) + item.setData(chart, Qt.ItemDataRole.UserRole + 10) + self.appendRow(item) diff --git a/ui/extends/components/dbTableViewer.py b/ui/extends/components/dbTableViewer.py new file mode 100644 index 0000000..7b4935e --- /dev/null +++ b/ui/extends/components/dbTableViewer.py @@ -0,0 +1,5 @@ +from PySide6.QtCore import QAbstractTableModel + + +class DbTableModel(QAbstractTableModel): + pass diff --git a/ui/extends/ocr.py b/ui/extends/ocr.py new file mode 100644 index 0000000..56563e7 --- /dev/null +++ b/ui/extends/ocr.py @@ -0,0 +1,18 @@ +try: + import json + + from arcaea_offline_ocr.device import Device + + def load_devices_json(filepath: str) -> list[Device]: + with open(filepath, "r", encoding="utf-8") as f: + file_content = f.read() + if len(file_content) == 0: + return [] + content = json.loads(file_content) + assert isinstance(content, list) + return [Device.from_json_object(item) for item in content] + +except Exception: + + def load_devices_json(*args, **kwargs): + pass diff --git a/ui/extends/score.py b/ui/extends/score.py new file mode 100644 index 0000000..e69de29 diff --git a/ui/extends/settings.py b/ui/extends/settings.py new file mode 100644 index 0000000..828f02b --- /dev/null +++ b/ui/extends/settings.py @@ -0,0 +1,57 @@ +from PySide6.QtCore import QDir, QSettings + +__all__ = [ + "DATABASE_PATH", + "DEVICES_JSON_FILE", + "DEVICE_UUID", + "TESSERACT_FILE", + "Settings", +] + +DATABASE_PATH = "General/DatabasePath" + +DEVICES_JSON_FILE = "Ocr/DevicesJsonFile" +DEVICE_UUID = "Ocr/DeviceUuid" +TESSERACT_FILE = "Ocr/TesseractFile" + + +class Settings(QSettings): + def __init__(self, parent=None): + super().__init__( + QDir.current().absoluteFilePath("arcaea_offline.ini"), + QSettings.Format.IniFormat, + parent, + ) + + def devicesJsonFile(self) -> str | None: + return self.value(DEVICES_JSON_FILE, None, str) + + def setDevicesJsonFile(self, path: str): + self.setValue(DEVICES_JSON_FILE, path) + self.sync() + + def resetDevicesJsonFile(self): + self.setValue(DEVICES_JSON_FILE, None) + self.sync() + + def deviceUuid(self) -> str | None: + return self.value(DEVICE_UUID, None, str) + + def setDeviceUuid(self, uuid: str): + self.setValue(DEVICE_UUID, uuid) + self.sync() + + def resetDeviceUuid(self): + self.setValue(DEVICE_UUID, None) + self.sync() + + def tesseractPath(self): + return self.value(TESSERACT_FILE, None, str) + + def setTesseractPath(self, path: str): + self.setValue(TESSERACT_FILE, path) + self.sync() + + def resetTesseractPath(self): + self.setValue(TESSERACT_FILE, None) + self.sync() diff --git a/ui/extends/shared/delegates/__init__.py b/ui/extends/shared/delegates/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ui/extends/shared/delegates/base.py b/ui/extends/shared/delegates/base.py new file mode 100644 index 0000000..9311468 --- /dev/null +++ b/ui/extends/shared/delegates/base.py @@ -0,0 +1,152 @@ +from typing import Callable + +from PySide6.QtCore import QEvent, QModelIndex, QObject, QPoint, QSize, Qt +from PySide6.QtGui import QBrush, QColor, QFont, QFontMetrics, QLinearGradient, QPainter +from PySide6.QtWidgets import QApplication, QStyledItemDelegate, QStyleOptionViewItem + + +class TextSegmentDelegate(QStyledItemDelegate): + VerticalPadding = 3 + HorizontalPadding = 5 + + TextRole = 3375 + ColorRole = TextRole + 1 + BrushRole = TextRole + 2 + GradientWrapperRole = TextRole + 3 + FontRole = TextRole + 20 + + def getTextSegments( + self, index: QModelIndex, option + ) -> list[ + list[ + dict[ + int, + str + | QColor + | QBrush + | Callable[[float, float, float, float], QLinearGradient] + | QFont, + ] + ] + ]: + return [] + + def sizeHint(self, option, index) -> QSize: + width = 0 + height = self.VerticalPadding + fm: QFontMetrics = option.fontMetrics + for line in self.getTextSegments(index, option): + lineWidth = 4 * self.HorizontalPadding + lineHeight = 0 + for textFrag in line: + font = textFrag.get(self.FontRole) + _fm = QFontMetrics(font) if font else fm + text = textFrag[self.TextRole] + textWidth = _fm.horizontalAdvance(text) + textHeight = _fm.height() + lineWidth += textWidth + lineHeight = max(lineHeight, textHeight) + width = max(lineWidth, width) + height += lineHeight + self.VerticalPadding + return QSize(width, height) + + def paint( + self, painter: QPainter, option: QStyleOptionViewItem, index: QModelIndex + ): + self.initStyleOption(option, index) + # draw text only + baseX = option.rect.x() + self.HorizontalPadding + baseY = option.rect.y() + self.VerticalPadding + maxWidth = option.rect.width() - (2 * self.HorizontalPadding) + fm: QFontMetrics = option.fontMetrics + painter.save() + for line in self.getTextSegments(index, option): + lineBaseX = baseX + lineBaseY = baseY + lineHeight = 0 + for textFrag in line: + painter.save() + # elide text, get font values + text = textFrag[self.TextRole] + fragMaxWidth = maxWidth - (lineBaseX - baseX) + font = textFrag.get(self.FontRole) + if font: + painter.setFont(font) + _fm = QFontMetrics(font) + else: + _fm = fm + lineHeight = max(lineHeight, _fm.height()) + elidedText = _fm.elidedText( + text, Qt.TextElideMode.ElideRight, fragMaxWidth + ) + + # confirm proper color + brush = textFrag.get(self.BrushRole) + gradientWrapper = textFrag.get(self.GradientWrapperRole) + color = textFrag.get(self.ColorRole) + pen = painter.pen() + if brush: + pen.setBrush(brush) + elif gradientWrapper: + gradient = gradientWrapper( + lineBaseX, + lineBaseY + lineHeight - _fm.height(), + fragMaxWidth, + _fm.height(), + ) + pen.setBrush(gradient) + elif color: + pen.setColor(color) + painter.setPen(pen) + + painter.drawText( + QPoint(lineBaseX, lineBaseY + lineHeight - _fm.descent()), + elidedText, + ) + painter.restore() + + # if text elided, skip to next line + # remember to add height before skipping + if _fm.boundingRect(text).width() >= fragMaxWidth: + break + lineBaseX += _fm.horizontalAdvance(elidedText) + + baseY += lineHeight + self.VerticalPadding + painter.restore() + + def super_styledItemDelegate_paint(self, painter, option, index): + return super().paint(painter, option, index) + + +class NoCommitWhenFocusOutEventFilter(QObject): + """ + --DEPRECATED-- + + The default QAbstractItemDelegate implementation has a private function + `editorEventFilter()`, when editor sends focusOut/hide event, it emits the + `commitData(editor)` signal. We don't want this since we need to validate + the input, so we filter the event out and handle it by ourselves. + + Reimplement `checkIsEditor(self, val) -> bool` to ensure this filter is + working. The default implementation always return `False`. + """ + + def checkIsEditor(self, val) -> bool: + return False + + def eventFilter(self, object: QObject, event: QEvent) -> bool: + if self.checkIsEditor(object) and event.type() in [ + QEvent.Type.FocusOut, + QEvent.Type.Hide, + ]: + widget = QApplication.focusWidget() + while widget: + # check if focus changed into editor's child + if self.checkIsEditor(widget): + return False + widget = widget.parentWidget() + + object.hide() + object.deleteLater() + return True + return False diff --git a/ui/extends/shared/delegates/chartDelegate.py b/ui/extends/shared/delegates/chartDelegate.py new file mode 100644 index 0000000..93ca7a4 --- /dev/null +++ b/ui/extends/shared/delegates/chartDelegate.py @@ -0,0 +1,176 @@ +from typing import Union + +from arcaea_offline.models import Chart +from arcaea_offline.utils import rating_class_to_short_text, rating_class_to_text +from PySide6.QtCore import QDateTime, QModelIndex, Qt, Signal +from PySide6.QtGui import QBrush, QColor +from PySide6.QtWidgets import ( + QFrame, + QHBoxLayout, + QLabel, + QMessageBox, + QPushButton, + QSizePolicy, + QStyleOptionViewItem, + QWidget, +) + +from ui.implements.components.chartSelector import ChartSelector + +from .base import TextSegmentDelegate + + +def chartToRichText(chart: Chart): + if isinstance(chart, Chart): + text = f"{chart.name_en} [{rating_class_to_short_text(chart.rating_class)}]" + text += "
" + text += f'({chart.song_id}, {chart.package_id})' + else: + text = "(unknown chart)" + return text + + +class ChartSelectorDelegateWrapper(ChartSelector): + accepted = Signal() + rejected = Signal() + + def __init__(self, parent=None): + super().__init__(parent) + + self.delegateHLine = QFrame(self) + self.delegateHLine.setFrameShape(QFrame.Shape.HLine) + self.delegateHLine.setFrameShadow(QFrame.Shadow.Plain) + self.delegateHLine.setFixedHeight(5) + self.mainVerticalLayout.insertWidget(0, self.delegateHLine) + + self.delegateHeader = QWidget(self) + self.delegateHeaderHBoxLayout = QHBoxLayout(self.delegateHeader) + self.delegateHeaderHBoxLayout.setContentsMargins(0, 0, 0, 0) + self.mainVerticalLayout.insertWidget(0, self.delegateHeader) + + self.editorLabel = QLabel(self) + self.editorLabel.setSizePolicy( + QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred + ) + self.delegateHeaderHBoxLayout.addWidget(self.editorLabel) + + self.editorCommitButton = QPushButton("Commit", self.delegateHeader) + self.editorCommitButton.clicked.connect(self.accepted) + self.delegateHeaderHBoxLayout.addWidget(self.editorCommitButton) + + self.editorDiscardButton = QPushButton("Discard", self.delegateHeader) + self.editorDiscardButton.clicked.connect(self.rejected) + self.delegateHeaderHBoxLayout.addWidget(self.editorDiscardButton) + + def setText(self, chart: Chart, _extra: str = None): + text = "Editing " + text += _extra or "" + text += "
" + text += ( + chartToRichText(chart) if isinstance(chart, Chart) else "(unknown chart)" + ) + self.editorLabel.setText(text) + + def validate(self): + return isinstance(self.value(), Chart) + + +class ChartDelegate(TextSegmentDelegate): + RatingClassColors = [ + QColor("#399bb2"), + QColor("#809955"), + QColor("#702d60"), + QColor("#710f25"), + ] + ChartInvalidBackgroundColor = QColor("#e6a23c") + + def getChart(self, index: QModelIndex) -> Chart | None: + return None + + def getTextSegments(self, index: QModelIndex, option): + chart = self.getChart(index) + if not isinstance(chart, Chart): + return [ + [{self.TextRole: "Chart Invalid", self.ColorRole: QColor("#ff0000")}] + ] + + return [ + [ + {self.TextRole: f"{chart.name_en}"}, + ], + [ + { + self.TextRole: f"{rating_class_to_text(chart.rating_class)} {chart.rating / 10:.1f}", + self.ColorRole: self.RatingClassColors[chart.rating_class], + }, + ], + [ + { + self.TextRole: f"({chart.song_id}, {chart.package_id})", + self.ColorRole: option.widget.palette().placeholderText().color(), + }, + ], + ] + + def paintWarningBackground(self, index: QModelIndex) -> bool: + return True + + def paint(self, painter, option, index): + # draw chartInvalid warning background + chart = self.getChart(index) + if not isinstance(chart, Chart) and self.paintWarningBackground(index): + painter.save() + painter.setPen(Qt.PenStyle.NoPen) + bgColor = QColor(self.ChartInvalidBackgroundColor) + bgColor.setAlpha(50) + painter.setBrush(bgColor) + painter.drawRect(option.rect) + painter.restore() + option.text = "" + super().paint(painter, option, index) + + def checkIsEditor(self, val): + return isinstance(val, ChartSelectorDelegateWrapper) + + def _closeEditor(self): + editor = self.sender() + self.closeEditor.emit(editor) + + def _commitEditor(self): + editor = self.sender() + if editor.validate(): + confirm = QMessageBox.question( + editor, + "Confirm", + f"Are you sure to change chart to

{chartToRichText(editor.value())}", + ) + if confirm == QMessageBox.StandardButton.Yes: + self.commitData.emit(editor) + self.closeEditor.emit(editor) + else: + QMessageBox.critical(editor, "Invalid chart", "Cannot commit") + + def createEditor( + self, parent: QWidget, option: QStyleOptionViewItem, index: QModelIndex + ) -> ChartSelectorDelegateWrapper: + if isinstance(self.getChart(index), Chart): + editor = ChartSelectorDelegateWrapper(parent) + editor.setWindowFlag(Qt.WindowType.Sheet, True) + editor.setWindowFlag(Qt.WindowType.FramelessWindowHint, True) + editor.setText(self.getChart(index)) + editor.move(parent.mapToGlobal(parent.pos())) + editor.accepted.connect(self._commitEditor) + editor.rejected.connect(self._closeEditor) + return editor + + def updateEditorGeometry(self, editor: QWidget, option, index: QModelIndex) -> None: + editor.move(editor.pos() + option.rect.topLeft()) + editor.setMaximumWidth(option.rect.width()) + + def setEditorData(self, editor: ChartSelectorDelegateWrapper, index: QModelIndex): + if self.checkIsEditor(editor) and isinstance(self.getChart(index), Chart): + editor.selectChart(self.getChart(index)) + return super().setEditorData(editor, index) + + def setModelData(self, editor: ChartSelectorDelegateWrapper, model, index): + ... diff --git a/ui/extends/shared/delegates/descriptionDelegate.py b/ui/extends/shared/delegates/descriptionDelegate.py new file mode 100644 index 0000000..7b65dc9 --- /dev/null +++ b/ui/extends/shared/delegates/descriptionDelegate.py @@ -0,0 +1,35 @@ +from PySide6.QtCore import QModelIndex, Qt +from PySide6.QtWidgets import QStyle, QStyleOptionViewItem + +from .base import TextSegmentDelegate + + +class DescriptionDelegate(TextSegmentDelegate): + MainTextRole = Qt.ItemDataRole.UserRole + 75 + DescriptionTextRole = Qt.ItemDataRole.UserRole + 76 + + def getMainText(self, index: QModelIndex) -> str | None: + return index.data(self.MainTextRole) + + def getDescriptionText(self, index: QModelIndex) -> str | None: + return index.data(self.DescriptionTextRole) + + def getTextSegments(self, index: QModelIndex, option): + return [ + [ + {self.TextRole: self.getMainText(index) or ""}, + {self.TextRole: " "}, + { + self.TextRole: self.getDescriptionText(index) or "", + self.ColorRole: option.widget.palette().placeholderText().color(), + }, + ] + ] + + def paint(self, painter, option, index): + super().paint(painter, option, index) + + optionNoText = QStyleOptionViewItem(option) + optionNoText.text = "" + style = option.widget.style() # type: QStyle + style.drawControl(QStyle.ControlElement.CE_ItemViewItem, optionNoText, painter) diff --git a/ui/extends/shared/delegates/scoreDelegate.py b/ui/extends/shared/delegates/scoreDelegate.py new file mode 100644 index 0000000..c2c19e0 --- /dev/null +++ b/ui/extends/shared/delegates/scoreDelegate.py @@ -0,0 +1,247 @@ +from typing import Union + +from arcaea_offline.calculate import calculate_score_range +from arcaea_offline.models import Chart, Score, ScoreInsert +from arcaea_offline.utils import ( + rating_class_to_text, + score_to_grade_text, + zip_score_grade, +) +from PySide6.QtCore import QAbstractItemModel, QDateTime, QModelIndex, Qt, Signal +from PySide6.QtGui import QColor, QFont, QLinearGradient +from PySide6.QtWidgets import ( + QAbstractItemDelegate, + QFrame, + QHBoxLayout, + QLabel, + QPushButton, + QSizePolicy, + QWidget, +) + +from ui.implements.components.scoreEditor import ScoreEditor + +from ..utils import keepWidgetInScreen +from .base import TextSegmentDelegate + + +class ScoreEditorDelegateWrapper(ScoreEditor): + rejected = Signal() + + def __init__(self, parent=None): + super().__init__(parent) + + self.hLine = QFrame(self) + self.hLine.setFrameShape(QFrame.Shape.HLine) + self.hLine.setFrameShadow(QFrame.Shadow.Plain) + self.hLine.setFixedHeight(5) + self.formLayout.insertRow(0, self.hLine) + + self.delegateHeader = QWidget(self) + self.delegateHeaderHBoxLayout = QHBoxLayout(self.delegateHeader) + self.delegateHeaderHBoxLayout.setContentsMargins(0, 0, 0, 0) + + self.editorLabel = QLabel(self.delegateHeader) + self.editorLabel.setSizePolicy( + QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred + ) + self.delegateHeaderHBoxLayout.addWidget(self.editorLabel) + + self.editorDiscardButton = QPushButton("Discard", self.delegateHeader) + self.editorDiscardButton.clicked.connect(self.rejected) + self.delegateHeaderHBoxLayout.addWidget(self.editorDiscardButton) + + self.formLayout.insertRow(0, self.delegateHeader) + + def setText(self, score: Score | ScoreInsert, _extra: str = None): + text = "Editing " + text += _extra or "" + text += f"score {score.score}" + text += f"
(P{score.pure} F{score.far} L{score.lost} | MR{score.max_recall})" + self.editorLabel.setText(text) + + +class ScoreDelegate(TextSegmentDelegate): + @staticmethod + def createGradeGradientWrapper(topColor: QColor, bottomColor: QColor): + def wrapper(x, y, width, height): + gradient = QLinearGradient(x + (width / 2), y, x + (width / 2), y + height) + gradient.setColorAt(0.1, topColor) + gradient.setColorAt(0.9, bottomColor) + return gradient + + return wrapper + + ScoreMismatchBackgroundColor = QColor("#e6a23c") + PureFarLostColors = [ + QColor("#f22ec6"), + QColor("#ff9028"), + QColor("#ff0c43"), + ] + GradeGradientsWrappers = [ # EX+, EX, AA, A. B, C, D + createGradeGradientWrapper(QColor("#83238c"), QColor("#2c72ae")), + createGradeGradientWrapper(QColor("#721b6b"), QColor("#295b8d")), + createGradeGradientWrapper(QColor("#5a3463"), QColor("#9b4b8d")), + createGradeGradientWrapper(QColor("#46324d"), QColor("#92588a")), + createGradeGradientWrapper(QColor("#43334a"), QColor("#755b7c")), + createGradeGradientWrapper(QColor("#3b2b27"), QColor("#80566b")), + createGradeGradientWrapper(QColor("#5d1d35"), QColor("#9f3c55")), + ] + + def getScore(self, index: QModelIndex) -> Score | None: + return None + + def getScoreInsert(self, index: QModelIndex) -> ScoreInsert | None: + return None + + def _getScore(self, index: QModelIndex): + score = self.getScore(index) + scoreInsert = self.getScoreInsert(index) + return scoreInsert if score is None else score + + def getChart(self, index: QModelIndex) -> Chart | None: + return None + + def getScoreValidateOk(self, index: QModelIndex) -> bool | None: + score = self._getScore(index) + chart = self.getChart(index) + + if isinstance(score, (Score, ScoreInsert)) and isinstance(chart, Chart): + scoreRange = calculate_score_range(chart, score.pure, score.far) + return scoreRange[0] <= score.score <= scoreRange[1] + + def getScoreGradeGradientWrapper(self, score: int): + return zip_score_grade(score, self.GradeGradientsWrappers) + + def getTextSegments(self, index, option): + score = self._getScore(index) + chart = self.getChart(index) + if not (isinstance(score, (Score, ScoreInsert)) and isinstance(chart, Chart)): + return [ + [ + { + self.TextRole: "Chart/Score Invalid", + self.ColorRole: QColor("#ff0000"), + } + ] + ] + + score_font = QFont(option.font) + score_font.setPointSize(12) + score_grade_font = QFont(score_font) + score_grade_font.setBold(True) + return [ + [ + { + self.TextRole: score_to_grade_text(score.score), + self.GradientWrapperRole: self.getScoreGradeGradientWrapper( + score.score + ), + self.FontRole: score_grade_font, + }, + {self.TextRole: " | "}, + {self.TextRole: str(score.score), self.FontRole: score_font}, + ], + [ + { + self.TextRole: f"PURE {score.pure}", + self.ColorRole: self.PureFarLostColors[0], + }, + {self.TextRole: " "}, + { + self.TextRole: f"FAR {score.far}", + self.ColorRole: self.PureFarLostColors[1], + }, + {self.TextRole: " "}, + { + self.TextRole: f"LOST {score.lost}", + self.ColorRole: self.PureFarLostColors[2], + }, + {self.TextRole: " | "}, + {self.TextRole: f"MAX RECALL {score.max_recall}"}, + ], + [ + { + self.TextRole: QDateTime.fromSecsSinceEpoch(score.time).toString( + "yyyy-MM-dd hh:mm:ss" + ) + } + ], + ] + + def paintWarningBackground(self, index: QModelIndex) -> bool: + return True + + def paint(self, painter, option, index): + # draw scoreMismatch warning background + score = self._getScore(index) + chart = self.getChart(index) + if ( + isinstance(score, (Score, ScoreInsert)) + and isinstance(chart, Chart) + and self.paintWarningBackground(index) + ): + scoreValidateOk = self.getScoreValidateOk(index) + if not scoreValidateOk: + painter.save() + painter.setPen(Qt.PenStyle.NoPen) + bgColor = QColor(self.ScoreMismatchBackgroundColor) + bgColor.setAlpha(50) + painter.setBrush(bgColor) + painter.drawRect(option.rect) + painter.restore() + + option.text = "" + super().paint(painter, option, index) + + def _closeEditor(self): + editor = self.sender() + self.closeEditor.emit(editor) + + def _commitEditor(self): + editor = self.sender() + self.commitData.emit(editor) + self.closeEditor.emit(editor) + + def createEditor(self, parent, option, index) -> ScoreEditorDelegateWrapper: + score = self._getScore(index) + chart = self.getChart(index) + if isinstance(score, (Score, ScoreInsert)) and isinstance(chart, Chart): + editor = ScoreEditorDelegateWrapper(parent) + editor.setWindowFlag(Qt.WindowType.Sheet, True) + editor.setWindowFlag(Qt.WindowType.FramelessWindowHint, True) + editor.setWindowTitle( + f"{chart.name_en}({chart.song_id}) | {rating_class_to_text(chart.rating_class)} | {chart.package_id}" + ) + editor.setText(self._getScore(index)) + editor.setValidateBeforeAccept(False) + editor.move(parent.mapToGlobal(parent.pos())) + editor.accepted.connect(self._commitEditor) + editor.rejected.connect(self._closeEditor) + editor.show() + return editor + return super().createEditor(parent, option, index) + + def updateEditorGeometry(self, editor, option, index): + editor.setMaximumWidth(option.rect.width()) + editor.move(editor.pos() + option.rect.topLeft()) + + keepWidgetInScreen(editor) + + def setEditorData(self, editor: ScoreEditorDelegateWrapper, index) -> None: + score = self._getScore(index) + chart = self.getChart(index) + if isinstance(score, (Score, ScoreInsert)) and isinstance(chart, Chart): + editor.setChart(chart) + editor.setValue(score) + + def confirmSetModelData(self, editor: ScoreEditorDelegateWrapper): + return editor.triggerValidateMessageBox() + + def setModelData( + self, + editor: ScoreEditorDelegateWrapper, + model: QAbstractItemModel, + index: QModelIndex, + ): + ... diff --git a/ui/extends/shared/models/tables/base.py b/ui/extends/shared/models/tables/base.py new file mode 100644 index 0000000..c9c093e --- /dev/null +++ b/ui/extends/shared/models/tables/base.py @@ -0,0 +1,35 @@ +from typing import Union + +from arcaea_offline.database import Database +from PySide6.QtCore import QAbstractTableModel, Qt + + +class DbTableModel(QAbstractTableModel): + def __init__(self, parent=None): + super().__init__(parent) + + self._horizontalHeaders = [] + self.retranslateHeaders() + + self._db = Database() + + def retranslateHeaders(self): + ... + + def syncDb(self): + ... + + def headerData(self, section: int, orientation: Qt.Orientation, role: int): + if ( + orientation == Qt.Orientation.Horizontal + and self._horizontalHeaders + and 0 <= section < len(self._horizontalHeaders) + and role == Qt.ItemDataRole.DisplayRole + ): + return self._horizontalHeaders[section] + return super().headerData(section, orientation, role) + + def columnCount(self, parent=None): + if self._horizontalHeaders: + return len(self._horizontalHeaders) + return super().columnCount(parent) diff --git a/ui/extends/shared/models/tables/score.py b/ui/extends/shared/models/tables/score.py new file mode 100644 index 0000000..69f3f11 --- /dev/null +++ b/ui/extends/shared/models/tables/score.py @@ -0,0 +1,197 @@ +from arcaea_offline.calculate import calculate_score +from arcaea_offline.models import Chart, Score, ScoreInsert +from PySide6.QtCore import QCoreApplication, QModelIndex, QSortFilterProxyModel, Qt + +from .base import DbTableModel + + +class DbScoreTableModel(DbTableModel): + IdRole = Qt.ItemDataRole.UserRole + 10 + ChartRole = Qt.ItemDataRole.UserRole + 11 + ScoreRole = Qt.ItemDataRole.UserRole + 12 + PttRole = Qt.ItemDataRole.UserRole + 13 + + def __init__(self, parent=None): + super().__init__(parent) + + self.__items = [] + + def retranslateHeaders(self): + self._horizontalHeaders = [ + # fmt: off + QCoreApplication.translate("DbScoreTableModel", "horizontalHeader.id"), + QCoreApplication.translate("DbScoreTableModel", "horizontalHeader.chart"), + QCoreApplication.translate("DbScoreTableModel", "horizontalHeader.score"), + QCoreApplication.translate("DbScoreTableModel", "horizontalHeader.potential"), + # fmt: on + ] + + def syncDb(self): + newScores = [Score.from_db_row(dbRow) for dbRow in self._db.get_scores()] + newScores = sorted(newScores, key=lambda x: x.id) + newCharts = [ + Chart.from_db_row(dbRow) + for dbRow in [ + self._db.get_chart(score.song_id, score.rating_class) + for score in newScores + ] + ] + newPtts = [] + for chart, score in zip(newCharts, newScores): + if isinstance(chart, Chart) and isinstance(score, Score): + newPtts.append(calculate_score(chart, score).potential) + else: + newPtts.append(None) + + newScoreIds = [score.id for score in newScores] + oldScoreIds = [item[self.ScoreRole].id for item in self.__items] + + deleteIds = list(set(oldScoreIds) - set(newScoreIds)) + newIds = list(set(newScoreIds) - set(oldScoreIds)) + deleteRowIndexes = [oldScoreIds.index(deleteId) for deleteId in deleteIds] + + # first delete rows + for deleteRowIndex in sorted(deleteRowIndexes, reverse=True): + self.beginRemoveRows(QModelIndex(), deleteRowIndex, deleteRowIndex) + self.__items.pop(deleteRowIndex) + self.endRemoveRows() + + # now update existing datas + for oldItem, newChart, newScore, newPtt in zip( + self.__items, newCharts, newScores, newPtts + ): + oldItem[self.IdRole] = newScore.id + oldItem[self.ChartRole] = newChart + oldItem[self.ScoreRole] = newScore + oldItem[self.PttRole] = newPtt + + # finally insert new rows + for newId in newIds: + insertRowIndex = self.rowCount() + itemListIndex = newScoreIds.index(newId) + score = newScores[itemListIndex] + chart = newCharts[itemListIndex] + ptt = newPtts[itemListIndex] + self.beginInsertRows(QModelIndex(), insertRowIndex, insertRowIndex) + self.__items.append( + { + self.IdRole: score.id, + self.ChartRole: chart, + self.ScoreRole: score, + self.PttRole: ptt, + } + ) + self.endInsertRows() + + # trigger view update + topLeft = self.index(0, 0) + bottomRight = self.index(self.rowCount() - 1, self.columnCount() - 1) + self.dataChanged.emit( + topLeft, + bottomRight, + [Qt.ItemDataRole.DisplayRole, self.IdRole, self.ChartRole, self.ScoreRole], + ) + + def rowCount(self, *args): + return len(self.__items) + + def data(self, index, role): + if index.isValid() and self.checkIndex(index): + if index.column() == 0 and role in [ + Qt.ItemDataRole.DisplayRole, + self.IdRole, + ]: + return self.__items[index.row()][self.IdRole] + elif index.column() == 1 and role == self.ChartRole: + return self.__items[index.row()][self.ChartRole] + elif index.column() == 2 and role in [self.ChartRole, self.ScoreRole]: + return self.__items[index.row()][role] + elif index.column() == 3: + if role == Qt.ItemDataRole.DisplayRole: + return f"{self.__items[index.row()][self.PttRole]:.3f}" + elif role == self.PttRole: + return self.__items[index.row()][self.PttRole] + return None + + def setData(self, index, value, role): + if not (index.isValid() and self.checkIndex(index)): + return False + + if ( + index.column() == 2 + and isinstance(value, ScoreInsert) + and role == self.ScoreRole + ): + self._db.update_score(self.__items[index.row()][self.IdRole], value) + self.syncDb() + return True + + return False + + def flags(self, index) -> Qt.ItemFlag: + flags = super().flags(index) + flags |= Qt.ItemFlag.ItemIsSelectable + if index.column() in [1, 2]: + flags |= Qt.ItemFlag.ItemIsEditable + return flags + + def _removeRow(self, row: int, syncDb: bool = True): + if not 0 <= row < self.rowCount(): + return False + + try: + self._db.delete_score(self.__items[row][self.IdRole]) + if syncDb: + self.syncDb() + return True + except Exception: + return False + + def removeRow(self, row: int, parent=...): + return self._removeRow(row) + + def removeRows(self, row: int, count: int, parent=...): + maxRow = min(self.rowCount() - 1, row + count - 1) + if row > maxRow: + return False + + result = all( + self._removeRow(row, syncDb=False) for row in range(row, row + count) + ) + self.syncDb() + return result + + def removeRowList(self, rowList: list[int]): + result = all( + self._removeRow(row, syncDb=False) for row in sorted(rowList, reverse=True) + ) + self.syncDb() + return result + + +class DbScoreTableSortFilterProxyModel(QSortFilterProxyModel): + Sort_C2_ScoreRole = Qt.ItemDataRole.UserRole + 75 + Sort_C2_TimeRole = Qt.ItemDataRole.UserRole + 76 + + def lessThan(self, source_left, source_right) -> bool: + if source_left.column() != source_right.column(): + return + + column = source_left.column() + if column == 0: + return source_left.data(DbScoreTableModel.IdRole) < source_right.data( + DbScoreTableModel.IdRole + ) + elif column == 2: + score_left = source_left.data(DbScoreTableModel.ScoreRole) + score_right = source_right.data(DbScoreTableModel.ScoreRole) + if isinstance(score_left, Score) and isinstance(score_right, Score): + if self.sortRole() == self.Sort_C2_ScoreRole: + return score_left.score < score_right.score + elif self.sortRole() == self.Sort_C2_TimeRole: + return score_left.time < score_right.time + elif column == 3: + return source_left.data(DbScoreTableModel.PttRole) < source_right.data( + DbScoreTableModel.PttRole + ) + return super().lessThan(source_left, source_right) diff --git a/ui/extends/shared/utils.py b/ui/extends/shared/utils.py new file mode 100644 index 0000000..7fd6cdf --- /dev/null +++ b/ui/extends/shared/utils.py @@ -0,0 +1,54 @@ +from PySide6.QtCore import QPoint +from PySide6.QtGui import QGuiApplication, QScreen +from PySide6.QtWidgets import QWidget + + +def keepWidgetInScreen(widget: QWidget, screen: QScreen = None): + """ensure your widget is visible""" + + # see https://doc.qt.io/qt-6/application-windows.html + # for why using frameGeometry.width() / frameGeometry.height() + # instead of width() / height(). + + screen = screen or QGuiApplication.primaryScreen() + screenAvailableGeometry = screen.availableGeometry() + + # X boundary + if widget.pos().x() < screenAvailableGeometry.x(): + pos = QPoint(widget.pos()) + pos.setX(screenAvailableGeometry.x()) + widget.move(pos) + elif ( + widget.pos().x() + widget.frameGeometry().width() + > screenAvailableGeometry.width() + ): + pos = QPoint(widget.pos()) + pos.setX( + pos.x() + - ( + pos.x() + + widget.frameGeometry().width() + - screenAvailableGeometry.width() + ) + ) + widget.move(pos) + + # Y boundary + if widget.pos().y() < screenAvailableGeometry.y(): + pos = QPoint(widget.pos()) + pos.setY(screenAvailableGeometry.y()) + widget.move(pos) + elif ( + widget.pos().y() + widget.frameGeometry().height() + > screenAvailableGeometry.height() + ): + pos = QPoint(widget.pos()) + pos.setY( + pos.y() + - ( + pos.y() + + widget.frameGeometry().height() + - screenAvailableGeometry.height() + ) + ) + widget.move(pos) diff --git a/ui/extends/tabs/tabOcr.py b/ui/extends/tabs/tabOcr.py new file mode 100644 index 0000000..38e9950 --- /dev/null +++ b/ui/extends/tabs/tabOcr.py @@ -0,0 +1,476 @@ +import contextlib +import logging +from typing import Any + +import exif +from arcaea_offline.calculate import calculate_score_range +from arcaea_offline.database import Database +from arcaea_offline.models import Chart, ScoreInsert +from arcaea_offline_ocr.device import Device +from arcaea_offline_ocr.recognize import RecognizeResult, recognize +from PySide6.QtCore import ( + QAbstractListModel, + QAbstractTableModel, + QCoreApplication, + QDateTime, + QFileInfo, + QModelIndex, + QObject, + QRect, + QRunnable, + QSize, + Qt, + QThreadPool, + Signal, + Slot, +) +from PySide6.QtGui import QPixmap +from PySide6.QtWidgets import QLabel, QStyledItemDelegate, QWidget + +from ui.extends.shared.delegates.chartDelegate import ChartDelegate +from ui.extends.shared.delegates.scoreDelegate import ScoreDelegate +from ui.implements.components.scoreEditor import ScoreEditor + +logger = logging.getLogger(__name__) + + +class OcrTaskSignals(QObject): + resultReady = Signal(int, RecognizeResult) + finished = Signal(int) + + +class OcrTask(QRunnable): + def __init__(self, index: int, device: Device, imagePath: str): + super().__init__() + self.index = index + self.device = device + self.imagePath = imagePath + self.signals = OcrTaskSignals() + + def run(self): + try: + result = recognize(self.imagePath, self.device) + self.signals.resultReady.emit(self.index, result) + logger.info( + f"OcrTask {self.imagePath} with {repr(self.device)} got result {repr(result)}" + ) + except Exception as e: + logger.exception( + f"OcrTask {self.imagePath} with {repr(self.device)} failed" + ) + finally: + self.signals.finished.emit(self.index) + + +class OcrQueueModel(QAbstractListModel): + ImagePathRole = Qt.ItemDataRole.UserRole + 1 + ImagePixmapRole = Qt.ItemDataRole.UserRole + 2 + RecognizeResultRole = Qt.ItemDataRole.UserRole + 10 + ScoreInsertRole = Qt.ItemDataRole.UserRole + 11 + ChartRole = Qt.ItemDataRole.UserRole + 12 + ScoreValidateOkRole = Qt.ItemDataRole.UserRole + 13 + + started = Signal() + finished = Signal() + + def __init__(self, parent=None): + super().__init__(parent) + self.__db = Database() + self.__items: list[dict[int, Any]] = [] + + @property + def imagePaths(self): + return [item.get(self.ImagePathRole) for item in self.__items] + + def clear(self): + self.beginResetModel() + self.beginRemoveRows(QModelIndex(), 0, self.rowCount() - 1) + self.__items.clear() + self.endRemoveRows() + self.endResetModel() + + def rowCount(self, *args): + return len(self.__items) + + def data(self, index, role): + if ( + index.isValid() + and 0 <= index.row() < self.rowCount() + and index.column() == 0 + ): + return self.__items[index.row()].get(role) + return None + + def setData(self, *args): + return False + + def addItem(self, imagePath: str): + if imagePath in self.imagePaths or not QFileInfo(imagePath).exists(): + return + + self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount()) + self.__items.append( + { + self.ImagePathRole: imagePath, + self.ImagePixmapRole: QPixmap(imagePath), + self.RecognizeResultRole: None, + self.ScoreInsertRole: None, + self.ChartRole: None, + self.ScoreValidateOkRole: False, + } + ) + self.endInsertRows() + + def updateOcrResult(self, row: int, result: RecognizeResult) -> bool: + if not 0 <= row < self.rowCount() or not isinstance(result, RecognizeResult): + return False + + item = self.__items[row] + + imagePath: str = item[self.ImagePathRole] + datetime = None + with contextlib.suppress(Exception): + with open(imagePath, "rb") as imgf: + exifImage = exif.Image(imgf.read()) + if exifImage.has_exif and exifImage.get("datetime_original"): + datetimeStr = exifImage.get("datetime_original") + datetime = QDateTime.fromString(datetimeStr, "yyyy:MM:dd hh:mm:ss") + if not isinstance(datetime, QDateTime): + datetime = QFileInfo(imagePath).birthTime() + + score = ScoreInsert( + song_id=self.__db.fuzzy_search_song_id(result.title)[0][0], + rating_class=result.rating_class, + score=result.score, + pure=result.pure, + far=result.far, + lost=result.lost, + time=datetime.toSecsSinceEpoch(), + max_recall=result.max_recall, + clear_type=None, + ) + chart = Chart.from_db_row( + self.__db.get_chart(score.song_id, score.rating_class) + ) + + item[self.RecognizeResultRole] = result + self.setItemChart(row, chart) + self.setItemScore(row, score) + modelIndex = self.index(row, 0) + self.dataChanged.emit( + modelIndex, + modelIndex, + [self.RecognizeResultRole, self.ScoreInsertRole, self.ChartRole], + ) + return True + + @Slot(int, RecognizeResult) + def ocrTaskReady(self, row: int, result: RecognizeResult): + self.updateOcrResult(row, result) + + @Slot(int) + def ocrTaskFinished(self, row: int): + self.__taskFinishedNum += 1 + if self.__taskFinishedNum == self.__taskNum: + self.finished.emit() + + def startQueue(self, device: Device): + self.__taskNum = self.rowCount() + self.__taskFinishedNum = 0 + self.started.emit() + for row in range(self.rowCount()): + modelIndex = self.index(row, 0) + imagePath: str = modelIndex.data(self.ImagePathRole) + task = OcrTask(row, device, imagePath) + task.signals.resultReady.connect(self.ocrTaskReady) + task.signals.finished.connect(self.ocrTaskFinished) + QThreadPool.globalInstance().start(task) + + def updateScoreValidateOk(self, row: int): + if not 0 <= row < self.rowCount(): + return + + item = self.__items[row] + chart = item[self.ChartRole] + score = item[self.ScoreInsertRole] + if isinstance(chart, Chart) and isinstance(score, ScoreInsert): + scoreRange = calculate_score_range(chart, score.pure, score.far) + scoreValidateOk = scoreRange[0] <= score.score <= scoreRange[1] + item[self.ScoreValidateOkRole] = scoreValidateOk + else: + item[self.ScoreValidateOkRole] = False + + modelIndex = self.index(row, 0) + self.dataChanged.emit(modelIndex, modelIndex, [self.ScoreValidateOkRole]) + + def setItemChart(self, row: int, chart: Chart): + if not 0 <= row < self.rowCount() or not isinstance(chart, Chart): + return False + + item = self.__items[row] + item[self.ChartRole] = chart + updatedRoles = [self.ChartRole] + + self.updateScoreValidateOk(row) + + modelIndex = self.index(row, 0) + self.dataChanged.emit(modelIndex, modelIndex, updatedRoles) + return True + + def setItemScore(self, row: int, score: ScoreInsert) -> bool: + if not 0 <= row < self.rowCount() or not isinstance(score, ScoreInsert): + return False + + item = self.__items[row] + item[self.ScoreInsertRole] = score + updatedRoles = [self.ScoreInsertRole] + + self.updateScoreValidateOk(row) + + modelIndex = self.index(row, 0) + self.dataChanged.emit(modelIndex, modelIndex, updatedRoles) + return True + + def acceptItem(self, row: int, ignoreValidate: bool = False): + if not 0 <= row < self.rowCount(): + return + + item = self.__items[row] + score = item[self.ScoreInsertRole] + if not isinstance(score, ScoreInsert) or ( + not item[self.ScoreValidateOkRole] and not ignoreValidate + ): + return + + try: + self.__db.insert_score(score) + self.beginRemoveRows(QModelIndex(), row, row) + self.__items.pop(row) + self.endRemoveRows() + return + except Exception as e: + logger.exception(f"Error accepting {repr(item)}") + return + + def acceptItems(self, __rows: list[int], ignoreValidate: bool = False): + items = sorted(__rows, reverse=True) + [self.acceptItem(item, ignoreValidate) for item in items] + + def acceptAllItems(self, ignoreValidate: bool = False): + self.acceptItems([*range(self.rowCount())], ignoreValidate) + + def removeItem(self, row: int): + if not 0 <= row < self.rowCount(): + return + + self.beginRemoveRows(QModelIndex(), row, row) + self.__items.pop(row) + self.endRemoveRows() + + def removeItems(self, __rows: list[int]): + rows = sorted(__rows, reverse=True) + [self.removeItem(row) for row in rows] + + +class OcrQueueTableProxyModel(QAbstractTableModel): + def __init__(self, parent=None): + super().__init__(parent) + self.retranslateHeaders() + self.__sourceModel = None + self.__columnRoleMapping = [ + [Qt.ItemDataRole.CheckStateRole], + [OcrQueueModel.ImagePathRole, OcrQueueModel.ImagePixmapRole], + [ + OcrQueueModel.RecognizeResultRole, + OcrQueueModel.ChartRole, + ], + [ + OcrQueueModel.RecognizeResultRole, + OcrQueueModel.ScoreInsertRole, + OcrQueueModel.ChartRole, + OcrQueueModel.ScoreValidateOkRole, + ], + ] + + def retranslateHeaders(self): + self.__horizontalHeaders = [ + # fmt: off + QCoreApplication.translate("OcrTableModel", "horizontalHeader.title.select"), + QCoreApplication.translate("OcrTableModel", "horizontalHeader.title.imagePreview"), + QCoreApplication.translate("OcrTableModel", "horizontalHeader.title.chart"), + QCoreApplication.translate("OcrTableModel", "horizontalHeader.title.score"), + # fmt: on + ] + + def sourceModel(self) -> OcrQueueModel: + return self.__sourceModel + + def setSourceModel(self, sourceModel): + if not isinstance(sourceModel, OcrQueueModel): + return False + + # connect signals + sourceModel.rowsAboutToBeInserted.connect(self.rowsAboutToBeInserted) + sourceModel.rowsInserted.connect(self.rowsInserted) + sourceModel.rowsAboutToBeRemoved.connect(self.rowsAboutToBeRemoved) + sourceModel.rowsRemoved.connect(self.rowsRemoved) + sourceModel.dataChanged.connect(self.dataChanged) + sourceModel.layoutAboutToBeChanged.connect(self.layoutAboutToBeChanged) + sourceModel.layoutChanged.connect(self.layoutChanged) + + self.__sourceModel = sourceModel + return True + + def rowCount(self, *args): + return self.sourceModel().rowCount() + + def columnCount(self, *args): + return len(self.__horizontalHeaders) + + def headerData(self, section: int, orientation: Qt.Orientation, role: int): + if ( + orientation == Qt.Orientation.Horizontal + and 0 <= section < len(self.__horizontalHeaders) + and role == Qt.ItemDataRole.DisplayRole + ): + return self.__horizontalHeaders[section] + return None + + def data(self, index, role): + if ( + 0 <= index.row() < self.rowCount() + and 0 <= index.column() < self.columnCount() + and role in self.__columnRoleMapping[index.column()] + ): + srcIndex = self.sourceModel().index(index.row(), 0) + return srcIndex.data(role) + return None + + def setData(self, index, value, role): + if index.column() == 2 and role == OcrQueueModel.ChartRole: + return self.sourceModel().setItemChart(index.row(), value) + if index.column() == 3 and role == OcrQueueModel.ScoreInsertRole: + return self.sourceModel().setItemScore(index.row(), value) + return False + + def flags(self, index: QModelIndex) -> Qt.ItemFlag: + flags = ( + self.sourceModel().flags(index) + if isinstance(self.sourceModel(), OcrQueueModel) + else super().flags(index) + ) + flags = flags | Qt.ItemFlag.ItemIsEnabled + flags = flags | Qt.ItemFlag.ItemIsEditable + flags = flags | Qt.ItemFlag.ItemIsSelectable + if index.column() == 0: + flags = flags & ~Qt.ItemFlag.ItemIsEnabled & ~Qt.ItemFlag.ItemIsEditable + return flags + + +class ImageDelegate(QStyledItemDelegate): + def getPixmap(self, index: QModelIndex): + return index.data(OcrQueueModel.ImagePixmapRole) + + def getImagePath(self, index: QModelIndex): + return index.data(OcrQueueModel.ImagePathRole) + + def scalePixmap(self, pixmap: QPixmap): + return pixmap.scaled( + 100, + 100, + Qt.AspectRatioMode.KeepAspectRatio, + Qt.TransformationMode.SmoothTransformation, + ) + + def paint(self, painter, option, index): + pixmap = self.getPixmap(index) + if not isinstance(pixmap, QPixmap): + imagePath = self.getImagePath(index) + option.text = imagePath + super().paint(painter, option, index) + else: + pixmap = self.scalePixmap(pixmap) + # https://stackoverflow.com/a/32047499/16484891 + # CC BY-SA 3.0 + x = option.rect.center().x() - pixmap.rect().width() / 2 + y = option.rect.center().y() - pixmap.rect().height() / 2 + + painter.drawPixmap( + QRect(x, y, pixmap.rect().width(), pixmap.rect().height()), pixmap + ) + + def sizeHint(self, option, index) -> QSize: + pixmap = self.getPixmap(index) + if isinstance(pixmap, QPixmap): + pixmap = self.scalePixmap(pixmap) + return pixmap.size() + else: + return QSize(100, 75) + + def createEditor(self, parent, option, index) -> QWidget: + pixmap = self.getPixmap(index) + if isinstance(pixmap, QPixmap): + label = QLabel(parent) + label.setWindowFlags(Qt.WindowType.Window) + label.setWindowFlag(Qt.WindowType.WindowMinimizeButtonHint, False) + label.setWindowFlag(Qt.WindowType.WindowMaximizeButtonHint, False) + label.setWindowFlag(Qt.WindowType.WindowCloseButtonHint, True) + label.setWindowTitle(QFileInfo(self.getImagePath(index)).fileName()) + pixmap = pixmap.scaled( + 800, + 800, + Qt.AspectRatioMode.KeepAspectRatio, + Qt.TransformationMode.SmoothTransformation, + ) + label.setMinimumSize(pixmap.size()) + label.setPixmap(pixmap) + label.move(parent.mapToGlobal(parent.pos())) + return label + + def setModelData(self, *args): + ... + + def updateEditorGeometry(self, *args): + ... + + +class TableChartDelegate(ChartDelegate): + def getChart(self, index: QModelIndex) -> Chart | None: + return index.data(OcrQueueModel.ChartRole) + + def paintWarningBackground(self, index: QModelIndex) -> bool: + return isinstance( + index.data(OcrQueueModel.RecognizeResultRole), RecognizeResult + ) + + def setModelData(self, editor, model: OcrQueueTableProxyModel, index): + if editor.validate(): + model.setData(index, editor.value(), OcrQueueModel.ChartRole) + + +class TableScoreDelegate(ScoreDelegate): + def getScoreInsert(self, index: QModelIndex): + return index.data(OcrQueueModel.ScoreInsertRole) + + def getChart(self, index: QModelIndex): + return index.data(OcrQueueModel.ChartRole) + + def getScoreValidateOk(self, index: QModelIndex): + return index.data(OcrQueueModel.ScoreValidateOkRole) + + def paintWarningBackground(self, index: QModelIndex) -> bool: + return isinstance( + index.data(OcrQueueModel.RecognizeResultRole), RecognizeResult + ) + + # def createEditor(self, parent, option, index): + # editor = super().createEditor(parent, option, index) + # editor.setManualHandleCommit(True) + # return editor + + def setModelData(self, editor, model: OcrQueueTableProxyModel, index): + # userAcceptMessageBox = editor.triggerValidateMessageBox() + # if userAcceptMessageBox: + # model.setData(index, editor.value(), OcrQueueModel.ScoreInsertRole) + if super().confirmSetModelData(editor): + model.setData(index, editor.value(), OcrQueueModel.ScoreInsertRole) diff --git a/ui/implements/components/__init__.py b/ui/implements/components/__init__.py new file mode 100644 index 0000000..b6670d2 --- /dev/null +++ b/ui/implements/components/__init__.py @@ -0,0 +1,6 @@ +from .chartSelector import ChartSelector +from .devicesComboBox import DevicesComboBox +from .elidedLabel import ElidedLabel +from .fileSelector import FileSelector +from .ratingClassRadioButton import RatingClassRadioButton +from .scoreEditor import ScoreEditor diff --git a/ui/implements/components/chartSelector.py b/ui/implements/components/chartSelector.py new file mode 100644 index 0000000..8e3208a --- /dev/null +++ b/ui/implements/components/chartSelector.py @@ -0,0 +1,254 @@ +from typing import Literal + +from arcaea_offline.database import Database +from arcaea_offline.models import Chart, Package +from arcaea_offline.utils import rating_class_to_text +from PySide6.QtCore import QModelIndex, Qt, Signal, Slot +from PySide6.QtGui import QColor +from PySide6.QtWidgets import QCompleter, QWidget + +from ui.designer.components.chartSelector_ui import Ui_ChartSelector +from ui.extends.components.chartSelector import FuzzySearchCompleterModel +from ui.extends.shared.delegates.descriptionDelegate import DescriptionDelegate +from ui.implements.components.ratingClassRadioButton import RatingClassRadioButton + + +class ChartSelector(Ui_ChartSelector, QWidget): + valueChanged = Signal() + + def __init__(self, parent=None): + super().__init__(parent) + self.db = Database() + self.db.register_update_hook(self.fillPackageComboBox) + self.setupUi(self) + + self.pstButton.setColors(QColor("#399bb2"), QColor("#f0f8fa")) + self.prsButton.setColors(QColor("#809955"), QColor("#f7f9f4")) + self.ftrButton.setColors(QColor("#702d60"), QColor("#f7ebf4")) + self.bydButton.setColors(QColor("#710f25"), QColor("#f9ced8")) + self.__RATING_CLASS_BUTTONS = [ + self.pstButton, + self.prsButton, + self.ftrButton, + self.bydButton, + ] + self.pstButton.clicked.connect(self.selectRatingClass) + self.prsButton.clicked.connect(self.selectRatingClass) + self.ftrButton.clicked.connect(self.selectRatingClass) + self.bydButton.clicked.connect(self.selectRatingClass) + self.deselectAllRatingClassButtons() + self.updateRatingClassButtonsEnabled([]) + + self.previousPackageButton.clicked.connect( + lambda: self.quickSwitchSelection("previous", "package") + ) + self.previousSongIdButton.clicked.connect( + lambda: self.quickSwitchSelection("previous", "songId") + ) + self.nextSongIdButton.clicked.connect( + lambda: self.quickSwitchSelection("next", "songId") + ) + self.nextPackageButton.clicked.connect( + lambda: self.quickSwitchSelection("next", "package") + ) + + self.valueChanged.connect(self.updateResultLabel) + + self.fillPackageComboBox() + self.packageComboBox.setCurrentIndex(-1) + self.songIdComboBox.setCurrentIndex(-1) + + self.fuzzySearchCompleterModel = FuzzySearchCompleterModel() + self.fuzzySearchCompleter = QCompleter(self.fuzzySearchCompleterModel) + self.fuzzySearchCompleter.popup().setItemDelegate( + DescriptionDelegate(self.fuzzySearchCompleter.popup()) + ) + self.fuzzySearchCompleter.activated[QModelIndex].connect( + self.fuzzySearchCompleterSetSelection + ) + self.fuzzySearchLineEdit.setCompleter(self.fuzzySearchCompleter) + + self.packageComboBox.setItemDelegate(DescriptionDelegate(self.packageComboBox)) + self.songIdComboBox.setItemDelegate(DescriptionDelegate(self.songIdComboBox)) + + self.pstButton.toggled.connect(self.valueChanged) + self.prsButton.toggled.connect(self.valueChanged) + self.ftrButton.toggled.connect(self.valueChanged) + self.bydButton.toggled.connect(self.valueChanged) + self.packageComboBox.currentIndexChanged.connect(self.valueChanged) + self.songIdComboBox.currentIndexChanged.connect(self.valueChanged) + + def quickSwitchSelection( + self, + direction: Literal["previous", "next"], + model: Literal["package", "songId"], + ): + minIndex = 0 + if model == "package": + maxIndex = self.packageComboBox.count() - 1 + currentIndex = self.packageComboBox.currentIndex() + ( + 1 if direction == "next" else -1 + ) + currentIndex = max(min(maxIndex, currentIndex), minIndex) + self.packageComboBox.setCurrentIndex(currentIndex) + elif model == "songId": + maxIndex = self.songIdComboBox.count() - 1 + currentIndex = self.songIdComboBox.currentIndex() + ( + 1 if direction == "next" else -1 + ) + currentIndex = max(min(maxIndex, currentIndex), minIndex) + self.songIdComboBox.setCurrentIndex(currentIndex) + else: + return + + def value(self): + packageId = self.packageComboBox.currentData() + songId = self.songIdComboBox.currentData() + ratingClass = self.selectedRatingClass() + + if packageId and songId and isinstance(ratingClass, int): + return Chart.from_db_row(self.db.get_chart(songId, ratingClass)) + return None + + @Slot() + def updateResultLabel(self): + chart = self.value() + if isinstance(chart, Chart): + package = Package.from_db_row( + self.db.get_package_by_package_id(chart.package_id) + ) + texts = [ + [package.name, chart.name_en, rating_class_to_text(chart.rating_class)], + [package.id, chart.song_id, str(chart.rating_class)], + ] + texts = [" | ".join(t) for t in texts] + text = f'{texts[0]}
{texts[1]}' + self.resultLabel.setText(text) + else: + self.resultLabel.setText("...") + + def fillPackageComboBox(self): + self.packageComboBox.clear() + packages = [Package.from_db_row(dbRow) for dbRow in self.db.get_packages()] + for package in packages: + self.packageComboBox.addItem(f"{package.name} ({package.id})", package.id) + row = self.packageComboBox.count() - 1 + self.packageComboBox.setItemData( + row, package.name, DescriptionDelegate.MainTextRole + ) + self.packageComboBox.setItemData( + row, package.id, DescriptionDelegate.DescriptionTextRole + ) + + self.packageComboBox.setCurrentIndex(-1) + + def fillSongIdComboBox(self): + self.songIdComboBox.clear() + packageId = self.packageComboBox.currentData() + if packageId: + charts = [ + Chart.from_db_row(dbRow) + for dbRow in self.db.get_charts_by_package_id(packageId) + ] + inserted_song_ids = [] + for chart in charts: + if chart.song_id not in inserted_song_ids: + self.songIdComboBox.addItem( + f"{chart.name_en} ({chart.song_id})", chart.song_id + ) + inserted_song_ids.append(chart.song_id) + row = self.songIdComboBox.count() - 1 + self.songIdComboBox.setItemData( + row, chart.name_en, DescriptionDelegate.MainTextRole + ) + self.songIdComboBox.setItemData( + row, chart.song_id, DescriptionDelegate.DescriptionTextRole + ) + self.songIdComboBox.setCurrentIndex(-1) + + @Slot() + def on_packageComboBox_activated(self): + self.fillSongIdComboBox() + + @Slot(int) + def on_songIdComboBox_currentIndexChanged(self, index: int): + rating_classes = [] + if index > -1: + charts = [ + Chart.from_db_row(dbRow) + for dbRow in self.db.get_charts_by_song_id( + self.songIdComboBox.currentData() + ) + ] + rating_classes = [chart.rating_class for chart in charts] + self.updateRatingClassButtonsEnabled(rating_classes) + + @Slot() + def on_resetButton_clicked(self): + self.packageComboBox.setCurrentIndex(-1) + self.songIdComboBox.setCurrentIndex(-1) + + @Slot(str) + def on_fuzzySearchLineEdit_textChanged(self, text: str): + if text: + self.fuzzySearchCompleterModel.fillDbFuzzySearchResults(self.db, text) + else: + self.fuzzySearchCompleterModel.clear() + + def selectChart(self, chart: Chart): + packageIdIndex = self.packageComboBox.findData(chart.package_id) + if packageIdIndex > -1: + self.packageComboBox.setCurrentIndex(packageIdIndex) + else: + # QMessageBox + return + + self.fillSongIdComboBox() + songIdIndex = self.songIdComboBox.findData(chart.song_id) + if songIdIndex > -1: + self.songIdComboBox.setCurrentIndex(songIdIndex) + else: + # QMessageBox + return + + self.selectRatingClass(chart.rating_class) + + @Slot(QModelIndex) + def fuzzySearchCompleterSetSelection(self, index: QModelIndex): + chart = index.data(Qt.ItemDataRole.UserRole + 10) # type: Chart + self.selectChart(chart) + + self.fuzzySearchLineEdit.clear() + self.fuzzySearchLineEdit.clearFocus() + + def ratingClassButtons(self): + return self.__RATING_CLASS_BUTTONS + + def selectedRatingClass(self): + for i, button in enumerate(self.__RATING_CLASS_BUTTONS): + if button.isChecked(): + return i + + def updateRatingClassButtonsEnabled(self, rating_classes: list[int]): + for i, button in enumerate(self.__RATING_CLASS_BUTTONS): + if i in rating_classes: + button.setEnabled(True) + else: + button.setChecked(False) + button.setEnabled(False) + + def deselectAllRatingClassButtons(self): + [button.setChecked(False) for button in self.__RATING_CLASS_BUTTONS] + + @Slot() + def selectRatingClass(self, rating_class: int | None = None): + if type(rating_class) == int and rating_class in range(4): + self.deselectAllRatingClassButtons() + button = self.__RATING_CLASS_BUTTONS[rating_class] + if button.isEnabled(): + button.setChecked(True) + else: + button = self.sender() + if isinstance(button, RatingClassRadioButton) and button.isEnabled(): + self.deselectAllRatingClassButtons() + button.setChecked(True) diff --git a/ui/implements/components/dbTableViewer.py b/ui/implements/components/dbTableViewer.py new file mode 100644 index 0000000..ee9dec5 --- /dev/null +++ b/ui/implements/components/dbTableViewer.py @@ -0,0 +1,9 @@ +from PySide6.QtWidgets import QWidget + +from ui.designer.components.dbTableViewer_ui import Ui_DbTableViewer + + +class DbTableViewer(Ui_DbTableViewer, QWidget): + def __init__(self, parent=None): + super().__init__(parent) + self.setupUi(self) diff --git a/ui/implements/components/devicesComboBox.py b/ui/implements/components/devicesComboBox.py new file mode 100644 index 0000000..d8e8d0f --- /dev/null +++ b/ui/implements/components/devicesComboBox.py @@ -0,0 +1,32 @@ +from arcaea_offline_ocr.device import Device +from PySide6.QtCore import Qt +from PySide6.QtWidgets import QComboBox + +from ui.extends.ocr import load_devices_json +from ui.extends.shared.delegates.descriptionDelegate import DescriptionDelegate + + +class DevicesComboBox(QComboBox): + DeviceUuidRole = Qt.ItemDataRole.UserRole + 10 + + def __init__(self, parent=None): + super().__init__(parent) + self.setItemDelegate(DescriptionDelegate(self)) + + def setDevices(self, devices: list[Device]): + self.clear() + for device in devices: + self.addItem(f"{device.name} ({device.uuid})", device) + row = self.count() - 1 + self.setItemData(row, device.uuid, self.DeviceUuidRole) + self.setItemData(row, device.name, DescriptionDelegate.MainTextRole) + self.setItemData(row, device.uuid, DescriptionDelegate.DescriptionTextRole) + self.setCurrentIndex(-1) + + def loadDevicesJson(self, path: str): + devices = load_devices_json(path) + self.setDevices(devices) + + def selectDevice(self, deviceUuid: str): + index = self.findData(deviceUuid, self.DeviceUuidRole) + self.setCurrentIndex(index) diff --git a/ui/implements/components/elidedLabel.py b/ui/implements/components/elidedLabel.py new file mode 100644 index 0000000..5452269 --- /dev/null +++ b/ui/implements/components/elidedLabel.py @@ -0,0 +1,50 @@ +from PySide6.QtCore import Qt +from PySide6.QtWidgets import QLabel + + +class ElidedLabel(QLabel): + """ + Adapted from https://wiki.qt.io/Elided_Label + """ + + def __init__(self, parent=None): + super().__init__(parent) + + self.__elideMode: Qt.TextElideMode = Qt.TextElideMode.ElideNone + self.__cachedElidedText = "" + self.__cachedText = "" + + def elideMode(self): + return self.__elideMode + + def setElideMode(self, mode): + self.__elideMode = mode + self.__cachedText = "" + self.update() + + def resizeEvent(self, event): + super().resizeEvent(event) + self.__cachedText = "" + + def paintEvent(self, event) -> None: + if self.__elideMode == Qt.TextElideMode.ElideNone: + return super().paintEvent(event) + + self.updateCachedTexts() + super().setText(self.__cachedElidedText) + super().paintEvent(event) + super().setText(self.__cachedText) + + def updateCachedTexts(self): + text = self.text() + if self.__cachedText == text: + return + self.__cachedText = text + fontMetrics = self.fontMetrics() + self.__cachedElidedText = fontMetrics.elidedText( + self.text(), self.__elideMode, self.width(), Qt.TextFlag.TextShowMnemonic + ) + # make sure to show at least the first character + if self.__cachedText: + firstChar = f"{self.__cachedText[0]}..." + self.setMinimumWidth(fontMetrics.horizontalAdvance(firstChar) + 1) diff --git a/ui/implements/components/fileSelector.py b/ui/implements/components/fileSelector.py new file mode 100644 index 0000000..84bc369 --- /dev/null +++ b/ui/implements/components/fileSelector.py @@ -0,0 +1,91 @@ +from PySide6.QtCore import QDir, QFileInfo, QMetaObject, Qt, Signal, Slot +from PySide6.QtWidgets import QFileDialog, QWidget + +from ui.designer.components.fileSelector_ui import Ui_FileSelector + + +class FileSelector(Ui_FileSelector, QWidget): + accepted = Signal() + filesSelected = Signal() + + def __init__(self, parent=None): + super().__init__(parent) + self.setupUi(self) + self.reset() + + self.elidedLabel.setElideMode(Qt.TextElideMode.ElideMiddle) + + self.accepted.connect(self.filesSelected) + self.accepted.connect(self.updateLabel) + self.filesSelected.connect(self.updateLabel) + + self.__mode = self.getOpenFileNames + + def getOpenFileNames(self): + selectedFiles, filter = QFileDialog.getOpenFileNames( + self, + self.__caption, + self.__startDirectory, + self.__filter, + "", + options=self.__options, + ) + if selectedFiles: + self.__selectedFiles = selectedFiles + self.accepted.emit() + + def getExistingDirectory(self): + selectedDir = QFileDialog.getExistingDirectory( + self, + self.__caption, + self.__startDirectory, + QFileDialog.Option.ShowDirsOnly | self.__options, + ) + if selectedDir: + self.__selectedFiles = [selectedDir] + self.accepted.emit() + + def selectFile(self, filename: str): + fileInfo = QFileInfo(filename) + if not fileInfo.exists(): + return + + self.__selectedFiles = [fileInfo.absoluteFilePath()] + self.__startDirectory = fileInfo.dir().absolutePath() + self.filesSelected.emit() + + def selectedFiles(self): + return self.__selectedFiles + + def setNameFilters(self, filters: list[str]): + self.__filter = ";;".join(filters) if filters else "" + + def setOptions(self, options: QFileDialog.Option): + self.__options = options + + def setMode(self, mode): + if mode in [self.getOpenFileNames, self.getExistingDirectory]: + self.__mode = mode + else: + raise ValueError("Invalid mode") + + def reset(self): + self.__selectedFiles = [] + self.__caption = None + self.__startDirectory = QDir.currentPath() + self.__filter = "" + self.__options = QFileDialog.Option(0) + + self.updateLabel() + + def updateLabel(self): + selectedFiles = self.selectedFiles() + + if not selectedFiles: + self.elidedLabel.setText("...") + else: + self.elidedLabel.setText("
".join(selectedFiles)) + + @Slot() + def on_selectButton_clicked(self): + self.__mode() diff --git a/ui/implements/components/focusSelectAllLineEdit.py b/ui/implements/components/focusSelectAllLineEdit.py new file mode 100644 index 0000000..e332fe2 --- /dev/null +++ b/ui/implements/components/focusSelectAllLineEdit.py @@ -0,0 +1,15 @@ +from PySide6.QtWidgets import QLineEdit + + +class FocusSelectAllLineEdit(QLineEdit): + def mousePressEvent(self, event): + super().mousePressEvent(event) + self.selectAll() + + def focusInEvent(self, event): + super().focusInEvent(event) + self.selectAll() + + def focusOutEvent(self, event): + super().focusOutEvent(event) + self.deselect() diff --git a/ui/implements/components/ratingClassRadioButton.py b/ui/implements/components/ratingClassRadioButton.py new file mode 100644 index 0000000..0d02200 --- /dev/null +++ b/ui/implements/components/ratingClassRadioButton.py @@ -0,0 +1,100 @@ +from PySide6.QtCore import Slot +from PySide6.QtGui import QColor +from PySide6.QtWidgets import QGraphicsColorizeEffect, QRadioButton + +from ui.extends.color import mix_color + +STYLESHEET = """ +QRadioButton {{ + padding: 10px; + background-color: qlineargradient(spread:pad, x1:0.7, y1:0.5, x2:1, y2:0.525, stop:0 {dark_color}, stop:1 {mid_color}); + color: {text_color}; +}} + +QRadioButton::indicator {{ + border: 2px solid palette(Window); + width: 7px; + height: 7px; + border-radius: 0px; +}} + +QRadioButton::indicator:unchecked {{ + background-color: palette(Window); +}} + +QPushButton::indicator:checked {{ + background-color: {mid_color}; +}} +""" + + +class RatingClassRadioButton(QRadioButton): + def __init__(self, parent): + super().__init__(parent) + + self.toggled.connect(self.updateCheckedEffect) + + self.grayscaleEffect = QGraphicsColorizeEffect(self) + self.grayscaleEffect.setColor("#000000") + + def setColors(self, dark_color: QColor, text_color: QColor): + self._dark_color = dark_color + self._text_color = text_color + self._mid_color = mix_color(dark_color, text_color, 0.616) + self.updateEffects() + + def isColorsSet(self) -> bool: + return ( + hasattr(self, "_dark_color") + and hasattr(self, "_text_color") + and hasattr(self, "_mid_color") + and isinstance(self._dark_color, QColor) + and isinstance(self._text_color, QColor) + and isinstance(self._mid_color, QColor) + ) + + def setNormalStyleSheet(self): + self.setStyleSheet( + STYLESHEET.format( + dark_color=self._dark_color.name(QColor.NameFormat.HexArgb), + mid_color=self._mid_color.name(QColor.NameFormat.HexArgb), + text_color=self._text_color.name(QColor.NameFormat.HexArgb), + ) + ) + + def setDisabledStyleSheet(self): + self.setStyleSheet( + STYLESHEET.format( + dark_color="#282828", + mid_color="#282828", + text_color="#9e9e9e", + ).replace("palette(Window)", "#333333") + ) + + @Slot() + def updateEnabledEffect(self): + if self.isColorsSet(): + if self.isEnabled(): + self.setNormalStyleSheet() + else: + self.setDisabledStyleSheet() + + @Slot() + def updateCheckedEffect(self): + if self.isColorsSet(): + if self.isEnabled(): + self.grayscaleEffect.setStrength(0.0 if self.isChecked() else 1.0) + self.setGraphicsEffect(self.grayscaleEffect) + + @Slot() + def updateEffects(self): + self.updateCheckedEffect() + self.updateEnabledEffect() + + def setChecked(self, arg__1: bool): + super().setChecked(arg__1) + self.updateEffects() + + def setEnabled(self, arg__1: bool): + super().setEnabled(arg__1) + self.updateEffects() diff --git a/ui/implements/components/scoreEditor.py b/ui/implements/components/scoreEditor.py new file mode 100644 index 0000000..0ed7803 --- /dev/null +++ b/ui/implements/components/scoreEditor.py @@ -0,0 +1,197 @@ +from enum import IntEnum +from typing import Optional + +from arcaea_offline.calculate import calculate_score_range +from arcaea_offline.models import Chart, Score, ScoreInsert +from PySide6.QtCore import QCoreApplication, QDateTime, Signal, Slot +from PySide6.QtWidgets import QMessageBox, QWidget + +from ui.designer.components.scoreEditor_ui import Ui_ScoreEditor + + +class ScoreValidateResult(IntEnum): + Ok = 0 + ScoreMismatch = 1 + ScoreEmpty = 2 + ChartInvalid = 50 + + +class ScoreEditor(Ui_ScoreEditor, QWidget): + valueChanged = Signal() + accepted = Signal() + + def __init__(self, parent=None): + super().__init__(parent) + self.setupUi(self) + + self.__validateBeforeAccept = True + self.__chart = None + + self.scoreLineEdit.textChanged.connect(self.valueChanged) + self.pureSpinBox.valueChanged.connect(self.valueChanged) + self.farSpinBox.valueChanged.connect(self.valueChanged) + self.lostSpinBox.valueChanged.connect(self.valueChanged) + self.dateTimeEdit.dateTimeChanged.connect(self.valueChanged) + self.maxRecallSpinBox.valueChanged.connect(self.valueChanged) + self.clearTypeComboBox.currentIndexChanged.connect(self.valueChanged) + self.valueChanged.connect(self.validateScore) + self.valueChanged.connect(self.updateValidateLabel) + + self.clearTypeComboBox.addItem("HARD LOST", -1) + self.clearTypeComboBox.addItem("TRACK LOST", 0) + self.clearTypeComboBox.addItem("TRACK COMPLETE", 1) + self.clearTypeComboBox.setCurrentIndex(-1) + + def setValidateBeforeAccept(self, __bool: bool): + self.__validateBeforeAccept = __bool + + def triggerValidateMessageBox(self): + validate = self.validateScore() + + if validate == ScoreValidateResult.Ok: + return True + if validate == ScoreValidateResult.ChartInvalid: + QMessageBox.critical( + self, + # fmt: off + QCoreApplication.translate("ScoreEditor", "chartInvalidDialog.title"), + QCoreApplication.translate("ScoreEditor", "chartInvalidDialog.title"), + # fmt: on + ) + return False + if validate == ScoreValidateResult.ScoreMismatch: + result = QMessageBox.warning( + self, + # fmt: off + QCoreApplication.translate("ScoreEditor", "scoreMismatchDialog.title"), + QCoreApplication.translate("ScoreEditor", "scoreMismatchDialog.content"), + # fmt: on + QMessageBox.StandardButton.Yes, + QMessageBox.StandardButton.No, + ) + return result == QMessageBox.StandardButton.Yes + elif validate == ScoreValidateResult.ScoreEmpty: + result = QMessageBox.warning( + self, + # fmt: off + QCoreApplication.translate("ScoreEditor", "emptyScoreDialog.title"), + QCoreApplication.translate("ScoreEditor", "emptyScoreDialog.content"), + # fmt: on + QMessageBox.StandardButton.Yes, + QMessageBox.StandardButton.No, + ) + return result == QMessageBox.StandardButton.Yes + else: + return False + + @Slot() + def on_commitButton_clicked(self): + userAccept = ( + self.triggerValidateMessageBox() if self.__validateBeforeAccept else True + ) + + if userAccept: + self.accepted.emit() + + def score(self): + score_text = self.scoreLineEdit.text().replace("'", "") + return int(score_text) if score_text else 0 + + def setMinimums(self): + self.pureSpinBox.setMinimum(0) + self.farSpinBox.setMinimum(0) + self.lostSpinBox.setMinimum(0) + self.maxRecallSpinBox.setMinimum(-1) + + def setLimits(self, chart: Chart): + self.setMinimums() + self.pureSpinBox.setMaximum(chart.note) + self.farSpinBox.setMaximum(chart.note) + self.lostSpinBox.setMaximum(chart.note) + self.maxRecallSpinBox.setMaximum(chart.note) + + def resetLimits(self): + self.setMinimums() + self.pureSpinBox.setMaximum(0) + self.farSpinBox.setMaximum(0) + self.lostSpinBox.setMaximum(0) + self.maxRecallSpinBox.setMaximum(0) + + def setChart(self, chart: Optional[Chart]): + if isinstance(chart, Chart): + self.__chart = chart + self.setLimits(chart) + else: + self.__chart = None + self.resetLimits() + self.updateValidateLabel() + + def validateScore(self) -> ScoreValidateResult: + if not isinstance(self.__chart, Chart): + return ScoreValidateResult.ChartInvalid + + score = self.value() + + score_range = calculate_score_range(self.__chart, score.pure, score.far) + score_in_range = score_range[0] <= score.score <= score_range[1] + note_in_range = score.pure + score.far + score.lost <= self.__chart.note + if not score_in_range or not note_in_range: + return ScoreValidateResult.ScoreMismatch + if score.score == 0: + return ScoreValidateResult.ScoreEmpty + return ScoreValidateResult.Ok + + def updateValidateLabel(self): + validate = self.validateScore() + + if validate == ScoreValidateResult.Ok: + text = QCoreApplication.translate("ScoreEditor", "validate.ok") + elif validate == ScoreValidateResult.ChartInvalid: + text = QCoreApplication.translate("ScoreEditor", "validate.chartInvalid") + elif validate == ScoreValidateResult.ScoreMismatch: + text = QCoreApplication.translate("ScoreEditor", "validate.scoreMismatch") + elif validate == ScoreValidateResult.ScoreEmpty: + text = QCoreApplication.translate("ScoreEditor", "validate.scoreEmpty") + else: + text = QCoreApplication.translate("ScoreEditor", "validate.unknownState") + + self.validateLabel.setText(text) + + def value(self): + if isinstance(self.__chart, Chart): + return ScoreInsert( + song_id=self.__chart.song_id, + rating_class=self.__chart.rating_class, + score=self.score(), + pure=self.pureSpinBox.value(), + far=self.farSpinBox.value(), + lost=self.lostSpinBox.value(), + time=self.dateTimeEdit.dateTime().toSecsSinceEpoch(), + max_recall=self.maxRecallSpinBox.value() + if self.maxRecallSpinBox.value() > -1 + else None, + clear_type=None, + ) + + def setValue(self, score: Score | ScoreInsert): + if isinstance(score, (Score, ScoreInsert)): + scoreText = str(score.score) + scoreText = scoreText.rjust(8, "0") + self.scoreLineEdit.setText(scoreText) + self.pureSpinBox.setValue(score.pure) + self.farSpinBox.setValue(score.far) + self.lostSpinBox.setValue(score.lost) + self.dateTimeEdit.setDateTime(QDateTime.fromSecsSinceEpoch(score.time)) + if score.max_recall is not None: + self.maxRecallSpinBox.setValue(score.max_recall) + if score.clear_type is not None: + self.clearTypeComboBox.setCurrentIndex(score.clear_type) + + def reset(self): + self.setChart(None) + self.scoreLineEdit.setText("''") + self.pureSpinBox.setValue(0) + self.farSpinBox.setValue(0) + self.lostSpinBox.setValue(0) + self.maxRecallSpinBox.setValue(-1) + self.clearTypeComboBox.setCurrentIndex(-1) diff --git a/ui/implements/mainwindow.py b/ui/implements/mainwindow.py new file mode 100644 index 0000000..0770a5c --- /dev/null +++ b/ui/implements/mainwindow.py @@ -0,0 +1,38 @@ +from traceback import format_exception + +from PySide6.QtWidgets import QMainWindow + +from ui.designer.mainwindow_ui import Ui_MainWindow +from ui.implements.tabs.tabOcr import TabOcr + +# try: +# import arcaea_offline_ocr + +# from ui.implements.tabs.tabOcr import TabOcr + +# OCR_ENABLED_FLAG = True +# except Exception as e: +# from ui.implements.tabs.tabOcrDisabled import TabOcrDisabled + +# OCR_ENABLED_FLAG = False +# OCR_ERROR_TEXT = "\n".join(format_exception(e)) + + +class MainWindow(Ui_MainWindow, QMainWindow): + def __init__(self, parent=None): + super().__init__(parent) + self.setupUi(self) + + currentIndex = self.tabWidget.currentIndex() + ocrTabIndex = self.tabWidget.indexOf(self.tab_ocr) + self.tabWidget.removeTab(ocrTabIndex) + self.tab_ocr.deleteLater() + # if OCR_ENABLED_FLAG: + # self.tab_ocr = TabOcr(self.tabWidget) + # else: + # self.tab_ocr = TabOcrDisabled(self.tabWidget) + # self.tab_ocr.contentLabel.setText(OCR_ERROR_TEXT) + self.tab_ocr = TabOcr(self.tabWidget) + self.tabWidget.insertTab(ocrTabIndex, self.tab_ocr, "") + self.tabWidget.setCurrentIndex(currentIndex) + self.retranslateUi(self) diff --git a/ui/implements/settings/settingsDefault.py b/ui/implements/settings/settingsDefault.py new file mode 100644 index 0000000..28e5fd3 --- /dev/null +++ b/ui/implements/settings/settingsDefault.py @@ -0,0 +1,73 @@ +from arcaea_offline.database import Database +from PySide6.QtCore import Slot +from PySide6.QtWidgets import QWidget + +from ui.designer.settings.settingsDefault_ui import Ui_SettingsDefault +from ui.extends.ocr import load_devices_json +from ui.extends.settings import * + + +class SettingsDefault(Ui_SettingsDefault, QWidget): + def __init__(self, parent=None): + super().__init__(parent) + self.setupUi(self) + + self.settings = Settings(self) + + self.devicesJsonFileSelector.filesSelected.connect(self.fillDevicesComboBox) + self.devicesJsonFileResetButton.clicked.connect(self.resetDevicesJsonFile) + self.deviceUuidResetButton.clicked.connect(self.resetDeviceUuid) + + devicesJsonPath = self.settings.devicesJsonFile() + self.devicesJsonFileSelector.selectFile(devicesJsonPath) + tesseractPath = self.settings.tesseractPath() + self.tesseractFileSelector.selectFile(tesseractPath) + + self.devicesJsonFileSelector.accepted.connect( + self.on_devicesJsonFileSelector_accepted + ) + self.tesseractFileSelector.accepted.connect( + self.on_tesseractFileSelector_accepted + ) + + def setDevicesJsonFile(self): + try: + filename = self.devicesJsonFileSelector.selectedFiles()[0] + devices = load_devices_json(filename) + assert isinstance(devices, list) + self.settings.setDevicesJsonFile(filename) + except Exception as e: + print(e) + # QMessageBox + return + + def resetDevicesJsonFile(self): + self.devicesJsonFileSelector.reset() + self.settings.resetDevicesJsonFile() + + def on_devicesJsonFileSelector_accepted(self): + self.setDevicesJsonFile() + + def fillDevicesComboBox(self): + devicesJsonPath = self.devicesJsonFileSelector.selectedFiles()[0] + self.devicesComboBox.loadDevicesJson(devicesJsonPath) + + storedDeviceUuid = self.settings.deviceUuid() + self.devicesComboBox.selectDevice(storedDeviceUuid) + + @Slot() + def on_devicesComboBox_activated(self): + device = self.devicesComboBox.currentData() + if device: + self.settings.setDeviceUuid(device.uuid) + + def resetDeviceUuid(self): + self.devicesComboBox.setCurrentIndex(-1) + self.settings.resetDeviceUuid() + + def setTesseractFile(self): + file = self.tesseractFileSelector.selectedFiles()[0] + self.settings.setTesseractPath(file) + + def on_tesseractFileSelector_accepted(self): + self.setTesseractFile() diff --git a/ui/implements/tabs/tabAbout.py b/ui/implements/tabs/tabAbout.py new file mode 100644 index 0000000..9d92956 --- /dev/null +++ b/ui/implements/tabs/tabAbout.py @@ -0,0 +1,23 @@ +from PySide6.QtCore import Qt, Slot +from PySide6.QtGui import QPixmap +from PySide6.QtWidgets import QMessageBox, QWidget + +from ui.designer.tabs.tabAbout_ui import Ui_TabAbout + + +class TabAbout(Ui_TabAbout, QWidget): + def __init__(self, parent=None): + super().__init__(parent) + self.setupUi(self) + + logoPixmap = QPixmap(":/images/logo.png").scaled( + 300, + 300, + Qt.AspectRatioMode.KeepAspectRatio, + Qt.TransformationMode.SmoothTransformation, + ) + self.logoLabel.setPixmap(logoPixmap) + + @Slot() + def on_aboutQtButton_clicked(self): + QMessageBox.aboutQt(self) diff --git a/ui/implements/tabs/tabDb/tabDb_Manage.py b/ui/implements/tabs/tabDb/tabDb_Manage.py new file mode 100644 index 0000000..0bc14eb --- /dev/null +++ b/ui/implements/tabs/tabDb/tabDb_Manage.py @@ -0,0 +1,30 @@ +import logging +import traceback + +from arcaea_offline.database import Database +from PySide6.QtCore import Slot +from PySide6.QtWidgets import QFileDialog, QMessageBox, QWidget + +from ui.designer.tabs.tabDb.tabDb_Manage_ui import Ui_TabDb_Manage + +logger = logging.getLogger(__name__) + + +class TabDb_Manage(Ui_TabDb_Manage, QWidget): + def __init__(self, parent=None): + super().__init__(parent) + self.setupUi(self) + + @Slot() + def on_syncArcSongDbButton_clicked(self): + dbFile, filter = QFileDialog.getOpenFileName( + self, None, "", "DB File (*.db);;*" + ) + try: + Database().update_arcsong_db(dbFile) + QMessageBox.information(self, "OK", "OK") + except Exception as e: + logging.exception("Sync arcsong.db error") + QMessageBox.critical( + self, "Sync Error", "\n".join(traceback.format_exception(e)) + ) diff --git a/ui/implements/tabs/tabDb/tabDb_ScoreTableViewer.py b/ui/implements/tabs/tabDb/tabDb_ScoreTableViewer.py new file mode 100644 index 0000000..8e117fe --- /dev/null +++ b/ui/implements/tabs/tabDb/tabDb_ScoreTableViewer.py @@ -0,0 +1,114 @@ +from arcaea_offline.models import ScoreInsert +from PySide6.QtCore import QModelIndex, Qt, Slot +from PySide6.QtGui import QColor, QPalette +from PySide6.QtWidgets import QMessageBox + +from ui.extends.shared.delegates.chartDelegate import ChartDelegate +from ui.extends.shared.delegates.scoreDelegate import ScoreDelegate +from ui.extends.shared.models.tables.score import ( + DbScoreTableModel, + DbScoreTableSortFilterProxyModel, +) +from ui.implements.components.dbTableViewer import DbTableViewer + + +class TableChartDelegate(ChartDelegate): + def getChart(self, index): + return index.data(DbScoreTableModel.ChartRole) + + +class TableScoreDelegate(ScoreDelegate): + def getChart(self, index): + return index.data(DbScoreTableModel.ChartRole) + + def getScoreInsert(self, index: QModelIndex) -> ScoreInsert | None: + return super().getScoreInsert(index) + + def getScore(self, index): + return index.data(DbScoreTableModel.ScoreRole) + + def setModelData(self, editor, model, index): + if super().confirmSetModelData(editor): + model.setData(index, editor.value(), DbScoreTableModel.ScoreRole) + + +class DbScoreTableViewer(DbTableViewer): + def __init__(self, parent=None): + super().__init__(parent) + + self.tableModel = DbScoreTableModel(self) + self.tableProxyModel = DbScoreTableSortFilterProxyModel(self) + self.tableProxyModel.setSourceModel(self.tableModel) + self.tableView.setModel(self.tableProxyModel) + self.tableView.setItemDelegateForColumn(1, TableChartDelegate(self.tableView)) + self.tableView.setItemDelegateForColumn(2, TableScoreDelegate(self.tableView)) + + tableViewPalette = QPalette(self.tableView.palette()) + highlightColor = QColor(tableViewPalette.color(QPalette.ColorRole.Highlight)) + highlightColor.setAlpha(25) + tableViewPalette.setColor(QPalette.ColorRole.Highlight, highlightColor) + self.tableView.setPalette(tableViewPalette) + self.tableModel.dataChanged.connect(self.resizeTableView) + + self.fillSortComboBox() + + def fillSortComboBox(self): + self.sort_comboBox.addItem("ID", [0, 1]) + self.sort_comboBox.addItem( + "Score", [2, DbScoreTableSortFilterProxyModel.Sort_C2_ScoreRole] + ) + self.sort_comboBox.addItem( + "Time", [2, DbScoreTableSortFilterProxyModel.Sort_C2_TimeRole] + ) + self.sort_comboBox.addItem("Potential", [3, 1]) + self.sort_comboBox.setCurrentIndex(0) + self.on_sort_comboBox_activated() + + @Slot() + def resizeTableView(self): + self.tableView.resizeRowsToContents() + self.tableView.resizeColumnsToContents() + + @Slot() + def on_sort_comboBox_activated(self): + self.sortProxyModel() + + @Slot() + def on_sort_descendingCheckBox_toggled(self): + self.sortProxyModel() + + @Slot() + def sortProxyModel(self): + if self.sort_comboBox.currentIndex() > -1: + column, role = self.sort_comboBox.currentData() + self.tableProxyModel.setSortRole(role) + self.tableProxyModel.sort( + column, + Qt.SortOrder.DescendingOrder + if self.sort_descendingCheckBox.isChecked() + else Qt.SortOrder.AscendingOrder, + ) + + @Slot() + def on_action_removeSelectedButton_clicked(self): + rows = [ + srcIndex.row() + for srcIndex in [ + self.tableProxyModel.mapToSource(proxyIndex) + for proxyIndex in self.tableView.selectionModel().selectedRows() + ] + ] + result = QMessageBox.warning( + self, + "Warning", + f"Removing {len(rows)} row(s). Are you sure?", + QMessageBox.StandardButton.Yes, + QMessageBox.StandardButton.No, + ) + if result == QMessageBox.StandardButton.Yes: + self.tableModel.removeRowList(rows) + + @Slot() + def on_refreshButton_clicked(self): + self.tableModel.syncDb() + self.resizeTableView() diff --git a/ui/implements/tabs/tabDbEntry.py b/ui/implements/tabs/tabDbEntry.py new file mode 100644 index 0000000..835bb5c --- /dev/null +++ b/ui/implements/tabs/tabDbEntry.py @@ -0,0 +1,16 @@ +from PySide6.QtCore import QCoreApplication +from PySide6.QtWidgets import QWidget + +from ui.designer.tabs.tabDbEntry_ui import Ui_TabDbEntry +from ui.implements.tabs.tabDb.tabDb_ScoreTableViewer import DbScoreTableViewer + + +class TabDbEntry(Ui_TabDbEntry, QWidget): + def __init__(self, parent=None): + super().__init__(parent) + self.setupUi(self) + + self.tabWidget.addTab( + DbScoreTableViewer(self), + QCoreApplication.translate("TabDbEntry", "tab.scoreTableViewer"), + ) diff --git a/ui/implements/tabs/tabInputScore.py b/ui/implements/tabs/tabInputScore.py new file mode 100644 index 0000000..cb1b769 --- /dev/null +++ b/ui/implements/tabs/tabInputScore.py @@ -0,0 +1,33 @@ +import traceback + +from arcaea_offline.database import Database +from PySide6.QtCore import QCoreApplication, QModelIndex +from PySide6.QtWidgets import QMessageBox, QWidget + +from ui.designer.tabs.tabInputScore_ui import Ui_TabInputScore + + +class TabInputScore(Ui_TabInputScore, QWidget): + def __init__(self, parent=None): + super().__init__(parent) + self.setupUi(self) + + self.chartSelector.valueChanged.connect(self.updateScoreEditorChart) + self.scoreEditor.accepted.connect(self.commit) + + def updateScoreEditorChart(self): + chart = self.chartSelector.value() + self.scoreEditor.setChart(chart) + + def commit(self): + try: + Database().insert_score(self.scoreEditor.value()) + self.scoreEditor.reset() + except Exception as e: + QMessageBox.critical( + self, + # fmt: off + QCoreApplication.translate("General", "tracebackFormatExceptionOnly.title"), + QCoreApplication.translate("General", "tracebackFormatExceptionOnly.content").format(traceback.format_exception_only(e)) + # fmt: on + ) diff --git a/ui/implements/tabs/tabOcr.py b/ui/implements/tabs/tabOcr.py new file mode 100644 index 0000000..d06a5b2 --- /dev/null +++ b/ui/implements/tabs/tabOcr.py @@ -0,0 +1,133 @@ +import pytesseract +from arcaea_offline_ocr_device_creation_wizard.implements.wizard import Wizard +from PySide6.QtCore import QModelIndex, Qt, Slot +from PySide6.QtGui import QColor, QPalette +from PySide6.QtWidgets import QFileDialog, QWidget + +from ui.designer.tabs.tabOcr_ui import Ui_TabOcr +from ui.extends.settings import Settings +from ui.extends.tabs.tabOcr import ( + ImageDelegate, + OcrQueueModel, + OcrQueueTableProxyModel, + TableChartDelegate, + TableScoreDelegate, +) + + +class TabOcr(Ui_TabOcr, QWidget): + def __init__(self, parent=None): + super().__init__(parent) + self.setupUi(self) + + self.deviceFileSelector.filesSelected.connect(self.deviceFileSelected) + self.tesseractFileSelector.filesSelected.connect( + self.tesseractFileSelectorFilesSelected + ) + + settings = Settings() + self.deviceFileSelector.selectFile(settings.devicesJsonFile()) + self.tesseractFileSelector.selectFile(settings.tesseractPath()) + self.deviceComboBox.selectDevice(settings.deviceUuid()) + + self.ocrQueueModel = OcrQueueModel(self) + self.ocrQueueModel.dataChanged.connect(self.resizeViewWhenScoreChanged) + self.ocrQueueModel.started.connect(self.ocrStarted) + self.ocrQueueModel.finished.connect(self.ocrFinished) + self.ocrQueueProxyModel = OcrQueueTableProxyModel(self) + self.ocrQueueProxyModel.setSourceModel(self.ocrQueueModel) + + self.tableView.setModel(self.ocrQueueProxyModel) + self.tableView.setItemDelegateForColumn(1, ImageDelegate(self.tableView)) + self.tableView.setItemDelegateForColumn(2, TableChartDelegate(self.tableView)) + self.tableView.setItemDelegateForColumn(3, TableScoreDelegate(self.tableView)) + + tableViewPalette = QPalette(self.tableView.palette()) + highlightColor = QColor(tableViewPalette.color(QPalette.ColorRole.Highlight)) + highlightColor.setAlpha(25) + tableViewPalette.setColor(QPalette.ColorRole.Highlight, highlightColor) + self.tableView.setPalette(tableViewPalette) + + @Slot(QModelIndex, QModelIndex, list) + def resizeViewWhenScoreChanged( + self, topleft: QModelIndex, bottomRight: QModelIndex, roles: list[int] + ): + if OcrQueueModel.ScoreInsertRole in roles: + rows = [*range(topleft.row(), bottomRight.row() + 1)] + [self.tableView.resizeRowToContents(row) for row in rows] + self.tableView.resizeColumnsToContents() + + @Slot() + def on_openWizardButton_clicked(self): + wizard = Wizard(self) + wizard.open() + + def deviceFileSelected(self): + selectedFiles = self.deviceFileSelector.selectedFiles() + if selectedFiles: + file = selectedFiles[0] + self.deviceComboBox.loadDevicesJson(file) + + def tesseractFileSelectorFilesSelected(self): + selectedFiles = self.tesseractFileSelector.selectedFiles() + if selectedFiles: + pytesseract.pytesseract.tesseract_cmd = selectedFiles[0] + + def setOcrButtonsEnabled(self, __bool: bool): + self.ocr_addImageButton.setEnabled(__bool) + self.ocr_removeSelectedButton.setEnabled(__bool) + self.ocr_removeAllButton.setEnabled(__bool) + self.ocr_startButton.setEnabled(__bool) + self.ocr_acceptSelectedButton.setEnabled(__bool) + self.ocr_acceptAllButton.setEnabled(__bool) + self.ocr_ignoreValidateCheckBox.setEnabled(__bool) + + @Slot() + def on_ocr_addImageButton_clicked(self): + files, _filter = QFileDialog.getOpenFileNames( + self, None, "", "Image Files (*.png *.jpg *.jpeg *.bmp *.webp);;*" + ) + for file in files: + self.ocrQueueModel.addItem(file) + self.tableView.resizeRowsToContents() + self.tableView.resizeColumnsToContents() + + @Slot() + def on_ocr_startButton_clicked(self): + self.ocrQueueModel.startQueue(self.deviceComboBox.currentData()) + + def ocrStarted(self): + self.setOcrButtonsEnabled(False) + + def ocrFinished(self): + self.setOcrButtonsEnabled(True) + + @Slot() + def on_ocr_removeSelectedButton_clicked(self): + rows = [ + modelIndex.row() + for modelIndex in self.tableView.selectionModel().selectedRows(0) + ] + self.ocrQueueModel.removeItems(rows) + + @Slot() + def on_ocr_removeAllButton_clicked(self): + self.ocrQueueModel.clear() + + @Slot() + def on_ocr_acceptSelectedButton_clicked(self): + ignoreValidate = ( + self.ocr_ignoreValidateCheckBox.checkState() == Qt.CheckState.Checked + ) + rows = [ + modelIndex.row() + for modelIndex in self.tableView.selectionModel().selectedRows(0) + ] + self.ocrQueueModel.acceptItems(rows, ignoreValidate) + + @Slot() + def on_ocr_acceptAllButton_clicked(self): + ignoreValidate = ( + self.ocr_ignoreValidateCheckBox.checkState() == Qt.CheckState.Checked + ) + self.ocrQueueModel.acceptAllItems(ignoreValidate) diff --git a/ui/implements/tabs/tabOcrDisabled.py b/ui/implements/tabs/tabOcrDisabled.py new file mode 100644 index 0000000..2f32949 --- /dev/null +++ b/ui/implements/tabs/tabOcrDisabled.py @@ -0,0 +1,9 @@ +from PySide6.QtWidgets import QWidget + +from ui.designer.tabs.tabOcrDisabled_ui import Ui_TabOcrDisabled + + +class TabOcrDisabled(Ui_TabOcrDisabled, QWidget): + def __init__(self, parent=None): + super().__init__(parent) + self.setupUi(self) diff --git a/ui/implements/tabs/tabOverview.py b/ui/implements/tabs/tabOverview.py new file mode 100644 index 0000000..7ef4d44 --- /dev/null +++ b/ui/implements/tabs/tabOverview.py @@ -0,0 +1,18 @@ +from arcaea_offline.database import Database +from PySide6.QtWidgets import QWidget + +from ui.designer.tabs.tabOverview_ui import Ui_TabOverview + + +class TabOverview(Ui_TabOverview, QWidget): + def __init__(self, parent=None): + super().__init__(parent) + self.setupUi(self) + + self.db = Database() + self.db.register_update_hook(self.updateOverview) + self.updateOverview() + + def updateOverview(self): + b30 = self.db.get_b30() or 0.00 + self.b30Label.setText(str(f"{b30:.3f}")) diff --git a/ui/implements/tabs/tabSettings.py b/ui/implements/tabs/tabSettings.py new file mode 100644 index 0000000..32860ee --- /dev/null +++ b/ui/implements/tabs/tabSettings.py @@ -0,0 +1,23 @@ +from PySide6.QtCore import QModelIndex, Slot +from PySide6.QtWidgets import QListWidgetItem, QWidget + +from ui.designer.tabs.tabSettings_ui import Ui_TabSettings + + +class SettingsEntryItem(QListWidgetItem): + pass + + +class TabSettings(Ui_TabSettings, QWidget): + def __init__(self, parent=None): + super().__init__(parent) + self.setupUi(self) + + self.listWidget.addItem("Default") + self.listWidget.activated.connect(self.switchPage) + + self.listWidget.setCurrentRow(self.stackedWidget.currentIndex()) + + @Slot(QModelIndex) + def switchPage(self, index: QModelIndex): + self.stackedWidget.setCurrentIndex(index.row()) diff --git a/ui/resources/images/__init__.py b/ui/resources/images/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ui/resources/images/icon.ico b/ui/resources/images/icon.ico new file mode 100644 index 0000000..94f3c30 Binary files /dev/null and b/ui/resources/images/icon.ico differ diff --git a/ui/resources/images/icon.png b/ui/resources/images/icon.png new file mode 100644 index 0000000..8a632eb Binary files /dev/null and b/ui/resources/images/icon.png differ diff --git a/ui/resources/images/images.qrc b/ui/resources/images/images.qrc new file mode 100644 index 0000000..5bf9acd --- /dev/null +++ b/ui/resources/images/images.qrc @@ -0,0 +1,7 @@ + + + + icon.png + logo.png + + diff --git a/ui/resources/images/logo.png b/ui/resources/images/logo.png new file mode 100644 index 0000000..904e794 Binary files /dev/null and b/ui/resources/images/logo.png differ diff --git a/ui/resources/translations/__init__.py b/ui/resources/translations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ui/resources/translations/en_US.ts b/ui/resources/translations/en_US.ts new file mode 100644 index 0000000..3082179 --- /dev/null +++ b/ui/resources/translations/en_US.ts @@ -0,0 +1,460 @@ + + + + + ChartSelector + + + fuzzySearch.lineEdit.placeholder + Input here... + + + + songIdSelector.title + Select Song + + + + songIdSelector.quickActions + Quick Actions + + + + songIdSelector.quickActions.previousPackageButton + Previous Package + + + + songIdSelector.quickActions.previousSongIdButton + Previous Song + + + + songIdSelector.quickActions.nextSongIdButton + Next Song + + + + songIdSelector.quickActions.nextPackageButton + Next Package + + + + ratingClassSelector.title + Rating Select + + + + resetButton + Reset + + + + DatabaseChecker + + + + dbPathLabel + Database Path + + + + + dbVersionLabel + Database Version + + + + + dbInitLabel + Initialize + + + + + dbCheckConnLabel + Database Connection + + + + + dbInitButton + Initialize Database + + + + + continueButton + Continue + + + + DbScoreTableModel + + + horizontalHeader.id + ID + + + + horizontalHeader.chart + Chart + + + + horizontalHeader.score + Score + + + + horizontalHeader.potential + Potential + + + + DbTableViewer + + + actions + Actions + + + + actions.removeSelected + Remove Selected + + + + actions.refresh + Refresh + + + + view + View + + + + view.sort.label + Sort By + + + + view.sort.descendingCheckBox + Descending + + + + view.filter.label + + + + + view.filter.configureButton + + + + + FileSelector + + + selectButton + Select... + + + + General + + + tracebackFormatExceptionOnly.title + Error + + + + tracebackFormatExceptionOnly.content + Unexpected Error<br>{0} + + + + MainWindow + + + tab.overview + Overview + + + + tab.input + Input + + + + tab.db + Database + + + + tab.ocr + OCR + + + + tab.settings + Settings + + + + tab.about + About + + + + OcrTableModel + + + horizontalHeader.title.select + Select + + + + horizontalHeader.title.imagePreview + Image Preview + + + + horizontalHeader.title.chart + Chart + + + + horizontalHeader.title.score + Score + + + + ScoreEditor + + + formLabel.score + Score + + + + formLabel.time + Time + + + + commitButton + Commit + + + + formLabel.clearType + Clear Type + + + + emptyScoreDialog.title + Empty Score + + + + emptyScoreDialog.content + Are you sure to commit an empty score? + + + + + chartInvalidDialog.title + Chart Invalid + + + + scoreMismatchDialog.title + Possible Invalid Score + + + + scoreMismatchDialog.content + The entered score may not match the selected chart. Commit this score anyway? + + + + validate.ok + OK + + + + validate.chartInvalid + Chart invalid + + + + validate.scoreMismatch + Possible invalid score + + + + validate.scoreEmpty + Empty score + + + + validate.unknownState + Unknown + + + + SettingsDefault + + + devicesJsonFile + Default devices.json + + + + deviceUuid + Default Device + + + + tesseractFile + tesseract Path + + + + defaultDevice.resetButton + + + + + devicesJsonPath.resetButton + + + + + TabAbout + + + About Qt + About Qt + + + + TabDbEntry + + + tab.manage + Manage + + + + tab.scoreTableViewer + TABLE [Score] + + + + TabDb_Manage + + + syncArcSongDbButton + Sync arcsong.db + + + + syncArcSongDb.description + Write chart info to database + + + + TabInputScore + + + tab.selectChart + Chart Selector + + + + tab.scoreEdit + Score Edit + + + + TabOcr + + + openWizardButton + Open Device Creation Wizard + + + + deviceSelector.title + Select Device + + + + tesseractSelector.title + Select tesseract Path + + + + ocr.title + OCR + + + + ocr.queue.title + Queue + + + + ocr.queue.addImageButton + Add Image + + + + ocr.queue.removeSelected + Remove Selected + + + + ocr.queue.removeAll + Remove All + + + + ocr.queue.startOcrButton + Start OCR + + + + ocr.results + Results + + + + ocr.results.acceptSelectedButton + Accept Selected + + + + ocr.results.acceptAllButton + Accept All + + + + ocr.results.ignoreValidate + Ignore +validation + + + + TabOcrDisabled + + + ocrDisabled.title + + + + diff --git a/ui/resources/translations/extract_translations.py b/ui/resources/translations/extract_translations.py new file mode 100644 index 0000000..6268b3e --- /dev/null +++ b/ui/resources/translations/extract_translations.py @@ -0,0 +1,54 @@ +import argparse +import os +import sys +from pathlib import Path + +ap = argparse.ArgumentParser() +ap.add_argument( + "-no-obsolete", + action="store_true", + default=False, + required=False, + dest="no_obsolete", +) +args = ap.parse_args(sys.argv[1:]) + + +script_file_path = Path(__file__) + +root_dir_path = Path(script_file_path.parent.parent.parent) +output_dir_path = Path(script_file_path.parent) + +designer = root_dir_path / "designer" +extends = root_dir_path / "extends" +implements = root_dir_path / "implements" +startup = root_dir_path / "startup" + +assert designer.exists() +assert extends.exists() +assert implements.exists() +assert startup.exists() + +no_obsolete = args.no_obsolete + +commands = [ + ( + "pyside6-lupdate" + " -extensions py,ui" + f" {designer.absolute()} {extends.absolute()} {implements.absolute()} {startup.absolute()}" + f" -ts {str((output_dir_path / 'zh_CN.ts').absolute())}" + ), # zh_CN + ( + "pyside6-lupdate" + " -extensions py,ui" + f" {designer.absolute()} {extends.absolute()} {implements.absolute()} {startup.absolute()}" + f" -ts {str((output_dir_path / 'en_US.ts').absolute())}" + ), # en_US +] +if no_obsolete: + commands = [f"{command} -no-obsolete" for command in commands] + +for command in commands: + print(f"Executing '{command}'") + output = os.popen(command).read() + print(output) diff --git a/ui/resources/translations/translations.qrc b/ui/resources/translations/translations.qrc new file mode 100644 index 0000000..cd974b2 --- /dev/null +++ b/ui/resources/translations/translations.qrc @@ -0,0 +1,7 @@ + + + + zh_CN.qm + en_US.qm + + diff --git a/ui/resources/translations/zh_CN.ts b/ui/resources/translations/zh_CN.ts new file mode 100644 index 0000000..f5f3134 --- /dev/null +++ b/ui/resources/translations/zh_CN.ts @@ -0,0 +1,459 @@ + + + + + ChartSelector + + + fuzzySearch.lineEdit.placeholder + 在此输入…… + + + + songIdSelector.title + 选择曲目 + + + + songIdSelector.quickActions + 快速操作 + + + + songIdSelector.quickActions.previousPackageButton + 上一曲包 + + + + songIdSelector.quickActions.previousSongIdButton + 上一曲目 + + + + songIdSelector.quickActions.nextSongIdButton + 下一曲目 + + + + songIdSelector.quickActions.nextPackageButton + 下一曲包 + + + + ratingClassSelector.title + 难度选择 + + + + resetButton + 重置 + + + + DatabaseChecker + + + + dbPathLabel + 数据库路径 + + + + + dbVersionLabel + 数据库版本 + + + + + dbInitLabel + 初始化 + + + + + dbCheckConnLabel + 数据库连接 + + + + + dbInitButton + 初始化数据库 + + + + + continueButton + 继续 + + + + DbScoreTableModel + + + horizontalHeader.id + ID + + + + horizontalHeader.chart + 谱面 + + + + horizontalHeader.score + 分数 + + + + horizontalHeader.potential + 单曲 PTT + + + + DbTableViewer + + + actions + 操作 + + + + actions.removeSelected + 移除选中 + + + + actions.refresh + 刷新 + + + + view + 视图 + + + + view.sort.label + 排序 + + + + view.sort.descendingCheckBox + 降序 + + + + view.filter.label + + + + + view.filter.configureButton + + + + + FileSelector + + + selectButton + 选择 + + + + General + + + tracebackFormatExceptionOnly.title + 错误 + + + + tracebackFormatExceptionOnly.content + 错误:{0} + + + + MainWindow + + + tab.overview + 概览 + + + + tab.input + 录入 + + + + tab.db + 数据库 + + + + tab.ocr + OCR + + + + tab.settings + 设置 + + + + tab.about + 关于 + + + + OcrTableModel + + + horizontalHeader.title.select + 选择 + + + + horizontalHeader.title.imagePreview + 图像预览 + + + + horizontalHeader.title.chart + 谱面 + + + + horizontalHeader.title.score + 分数 + + + + ScoreEditor + + + formLabel.score + 分数 + + + + formLabel.time + 时间 + + + + commitButton + 提交 + + + + formLabel.clearType + 通关状态 + + + + emptyScoreDialog.title + 分数为空 + + + + emptyScoreDialog.content + 确定提交空分数吗? + + + + + chartInvalidDialog.title + 谱面无效 + + + + scoreMismatchDialog.title + 分数可能有误 + + + + scoreMismatchDialog.content + 输入的分数不在理论计算范围内。是否确认提交? + + + + validate.ok + OK + + + + validate.chartInvalid + 谱面无效 + + + + validate.scoreMismatch + 分数可能有误 + + + + validate.scoreEmpty + 分数为空 + + + + validate.unknownState + 未知 + + + + SettingsDefault + + + devicesJsonFile + 默认设备文件 + + + + deviceUuid + 默认设备 + + + + tesseractFile + tesseract 路径 + + + + defaultDevice.resetButton + + + + + devicesJsonPath.resetButton + + + + + TabAbout + + + About Qt + 关于 Qt + + + + TabDbEntry + + + tab.manage + 管理 + + + + tab.scoreTableViewer + 表 [分数] + + + + TabDb_Manage + + + syncArcSongDbButton + 同步 arcsong.db + + + + syncArcSongDb.description + 将谱面信息写入数据库 + + + + TabInputScore + + + tab.selectChart + 谱面选择 + + + + tab.scoreEdit + 分数编辑 + + + + TabOcr + + + openWizardButton + 打开设备创建向导 + + + + deviceSelector.title + 选择设备 + + + + tesseractSelector.title + 选择 tesseract 路径 + + + + ocr.title + OCR + + + + ocr.queue.title + 队列 + + + + ocr.queue.addImageButton + 添加图像文件 + + + + ocr.queue.removeSelected + 移除选中 + + + + ocr.queue.removeAll + 移除所有 + + + + ocr.queue.startOcrButton + 开始 OCR + + + + ocr.results + 结果 + + + + ocr.results.acceptSelectedButton + 提交选中 + + + + ocr.results.acceptAllButton + 提交所有 + + + + ocr.results.ignoreValidate + 忽略验证 + + + + TabOcrDisabled + + + ocrDisabled.title + + + + diff --git a/ui/startup/__init__.py b/ui/startup/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ui/startup/databaseChecker.py b/ui/startup/databaseChecker.py new file mode 100644 index 0000000..ec390bd --- /dev/null +++ b/ui/startup/databaseChecker.py @@ -0,0 +1,83 @@ +import traceback + +from arcaea_offline.database import Database +from PySide6.QtCore import QDir, QFile, Qt, QTimer, Slot +from PySide6.QtWidgets import QDialog, QMessageBox + +from ui.extends.settings import Settings + +from .databaseChecker_ui import Ui_DatabaseChecker + + +class DatabaseChecker(Ui_DatabaseChecker, QDialog): + def __init__(self, parent=None): + super().__init__(parent) + self.setupUi(self) + self.setWindowFlag(Qt.WindowType.WindowMinimizeButtonHint, False) + self.setWindowFlag(Qt.WindowType.WindowMaximizeButtonHint, False) + self.setWindowFlag(Qt.WindowType.WindowCloseButtonHint, True) + self.dbFileSelector.setMode(self.dbFileSelector.getExistingDirectory) + self.dbFileSelector.filesSelected.connect(self.fileSelected) + + self.settings = Settings(self) + dbDir = self.settings.value("Default/DbDir", None, str) + if dbDir and QFile(QDir(dbDir).filePath(Database.dbFilename)).exists(): + self.dbFileSelector.selectFile(dbDir) + result = self.checkDbVersion() + if result: + QTimer.singleShot(50, self.accept) + else: + self.dbFileSelector.selectFile(QDir.currentPath()) + + def fileSelected(self): + self.checkDbVersion() + + def checkDbVersion(self) -> str | None: + dbQDir = QDir(self.dbFileSelector.selectedFiles()[0]) + dbDir = dbQDir.absolutePath() + + dbDir = self.dbFileSelector.selectedFiles()[0] + dbQFile = QFile(QDir(dbDir).filePath(Database.dbFilename)) + if not dbQFile.exists(): + result = QMessageBox.question(self, "Database", "Create database file now?") + if result != QMessageBox.StandardButton.Yes: + return + dbQFile.open(QFile.OpenModeFlag.WriteOnly) + dbQFile.close() + + Database.dbDir = dbDir + + try: + with Database().conn as conn: + version = conn.execute( + "SELECT value FROM properties WHERE key = 'db_version'" + ).fetchone()[0] + self.dbVersionLabel.setText(version) + self.continueButton.setEnabled(True) + + self.dbCheckConnLabel.setText('OK') + self.settings.setValue("Default/DbDir", dbDir) + return version + except Exception as e: + QMessageBox.critical( + self, "Database Error", "\n".join(traceback.format_exception(e)) + ) + self.dbInitButton.setEnabled(True) + self.continueButton.setEnabled(False) + self.dbCheckConnLabel.setText('Error') + return False + + @Slot() + def on_dbInitButton_clicked(self): + try: + Database().init() + except Exception as e: + QMessageBox.critical( + self, "Database Error", "\n".join(traceback.format_exception(e)) + ) + finally: + self.checkDbVersion() + + @Slot() + def on_continueButton_clicked(self): + self.accept() diff --git a/ui/startup/databaseChecker.ui b/ui/startup/databaseChecker.ui new file mode 100644 index 0000000..9f42d1f --- /dev/null +++ b/ui/startup/databaseChecker.ui @@ -0,0 +1,107 @@ + + + DatabaseChecker + + + + 0 + 0 + 350 + 250 + + + + DatabaseChecker + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + dbPathLabel + + + + + + + + + + dbVersionLabel + + + + + + + - + + + + + + + dbInitLabel + + + + + + + dbCheckConnLabel + + + + + + + dbInitButton + + + + + + + ... + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + false + + + continueButton + + + + + + + + FileSelector + QWidget +
ui.implements.components.fileSelector
+ 1 +
+
+ + +
diff --git a/ui/startup/databaseChecker_ui.py b/ui/startup/databaseChecker_ui.py new file mode 100644 index 0000000..e07ca63 --- /dev/null +++ b/ui/startup/databaseChecker_ui.py @@ -0,0 +1,148 @@ +# -*- coding: utf-8 -*- + +################################################################################ +## Form generated from reading UI file 'databaseChecker.ui' +## +## Created by: Qt User Interface Compiler version 6.5.0 +## +## WARNING! All changes made in this file will be lost when recompiling UI file! +################################################################################ + +from PySide6.QtCore import ( + QCoreApplication, + QDate, + QDateTime, + QLocale, + QMetaObject, + QObject, + QPoint, + QRect, + QSize, + Qt, + QTime, + QUrl, +) +from PySide6.QtGui import ( + QBrush, + QColor, + QConicalGradient, + QCursor, + QFont, + QFontDatabase, + QGradient, + QIcon, + QImage, + QKeySequence, + QLinearGradient, + QPainter, + QPalette, + QPixmap, + QRadialGradient, + QTransform, +) +from PySide6.QtWidgets import ( + QApplication, + QFormLayout, + QLabel, + QPushButton, + QSizePolicy, + QSpacerItem, + QWidget, +) + +from ui.implements.components.fileSelector import FileSelector + + +class Ui_DatabaseChecker(object): + def setupUi(self, DatabaseChecker): + if not DatabaseChecker.objectName(): + DatabaseChecker.setObjectName("DatabaseChecker") + DatabaseChecker.resize(350, 250) + DatabaseChecker.setWindowTitle("DatabaseChecker") + self.formLayout = QFormLayout(DatabaseChecker) + self.formLayout.setObjectName("formLayout") + self.formLayout.setLabelAlignment( + Qt.AlignRight | Qt.AlignTrailing | Qt.AlignVCenter + ) + self.label = QLabel(DatabaseChecker) + self.label.setObjectName("label") + + self.formLayout.setWidget(0, QFormLayout.LabelRole, self.label) + + self.dbFileSelector = FileSelector(DatabaseChecker) + self.dbFileSelector.setObjectName("dbFileSelector") + + self.formLayout.setWidget(0, QFormLayout.FieldRole, self.dbFileSelector) + + self.label_2 = QLabel(DatabaseChecker) + self.label_2.setObjectName("label_2") + + self.formLayout.setWidget(1, QFormLayout.LabelRole, self.label_2) + + self.dbVersionLabel = QLabel(DatabaseChecker) + self.dbVersionLabel.setObjectName("dbVersionLabel") + self.dbVersionLabel.setText("-") + + self.formLayout.setWidget(1, QFormLayout.FieldRole, self.dbVersionLabel) + + self.label_4 = QLabel(DatabaseChecker) + self.label_4.setObjectName("label_4") + + self.formLayout.setWidget(2, QFormLayout.LabelRole, self.label_4) + + self.label_5 = QLabel(DatabaseChecker) + self.label_5.setObjectName("label_5") + + self.formLayout.setWidget(4, QFormLayout.LabelRole, self.label_5) + + self.dbInitButton = QPushButton(DatabaseChecker) + self.dbInitButton.setObjectName("dbInitButton") + + self.formLayout.setWidget(2, QFormLayout.FieldRole, self.dbInitButton) + + self.dbCheckConnLabel = QLabel(DatabaseChecker) + self.dbCheckConnLabel.setObjectName("dbCheckConnLabel") + self.dbCheckConnLabel.setText("...") + + self.formLayout.setWidget(4, QFormLayout.FieldRole, self.dbCheckConnLabel) + + self.verticalSpacer = QSpacerItem( + 20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding + ) + + self.formLayout.setItem(3, QFormLayout.FieldRole, self.verticalSpacer) + + self.continueButton = QPushButton(DatabaseChecker) + self.continueButton.setObjectName("continueButton") + self.continueButton.setEnabled(False) + + self.formLayout.setWidget(5, QFormLayout.SpanningRole, self.continueButton) + + self.retranslateUi(DatabaseChecker) + + QMetaObject.connectSlotsByName(DatabaseChecker) + + # setupUi + + def retranslateUi(self, DatabaseChecker): + self.label.setText( + QCoreApplication.translate("DatabaseChecker", "dbPathLabel", None) + ) + self.label_2.setText( + QCoreApplication.translate("DatabaseChecker", "dbVersionLabel", None) + ) + self.label_4.setText( + QCoreApplication.translate("DatabaseChecker", "dbInitLabel", None) + ) + self.label_5.setText( + QCoreApplication.translate("DatabaseChecker", "dbCheckConnLabel", None) + ) + self.dbInitButton.setText( + QCoreApplication.translate("DatabaseChecker", "dbInitButton", None) + ) + self.continueButton.setText( + QCoreApplication.translate("DatabaseChecker", "continueButton", None) + ) + pass + + # retranslateUi