From 95da43261e910b680cb8f0361b6a7ff899d30434 Mon Sep 17 00:00:00 2001 From: 283375 Date: Fri, 7 Jul 2023 01:41:19 +0800 Subject: [PATCH] init --- .editorconfig | 9 + .gitignore | 170 +++++++ index.py | 52 ++ pyproject.toml | 40 ++ ui/__init__.py | 0 ui/designer/components/chartSelector.ui | 251 +++++++++ ui/designer/components/chartSelector_ui.py | 190 +++++++ ui/designer/components/dbTableViewer.ui | 159 ++++++ ui/designer/components/dbTableViewer_ui.py | 134 +++++ ui/designer/components/fileSelector.ui | 60 +++ ui/designer/components/fileSelector_ui.py | 58 +++ ui/designer/components/scoreEditor.ui | 228 +++++++++ ui/designer/components/scoreEditor_ui.py | 167 ++++++ ui/designer/mainwindow.ui | 92 ++++ ui/designer/mainwindow_ui.py | 79 +++ ui/designer/settings/settingsDefault.ui | 168 +++++++ ui/designer/settings/settingsDefault_ui.py | 130 +++++ ui/designer/tabs/tabAbout.ui | 102 ++++ ui/designer/tabs/tabAbout_ui.py | 87 ++++ ui/designer/tabs/tabDb/tabDb_Manage.ui | 38 ++ ui/designer/tabs/tabDb/tabDb_Manage_ui.py | 51 ++ ui/designer/tabs/tabDbEntry.ui | 41 ++ ui/designer/tabs/tabDbEntry_ui.py | 52 ++ ui/designer/tabs/tabInputScore.ui | 89 ++++ ui/designer/tabs/tabInputScore_ui.py | 74 +++ ui/designer/tabs/tabOcr.ui | 198 ++++++++ ui/designer/tabs/tabOcrDisabled.ui | 105 ++++ ui/designer/tabs/tabOcrDisabled_ui.py | 80 +++ ui/designer/tabs/tabOcr_ui.py | 165 ++++++ ui/designer/tabs/tabOverview.ui | 117 +++++ ui/designer/tabs/tabOverview_ui.py | 108 ++++ ui/designer/tabs/tabSettings.ui | 74 +++ ui/designer/tabs/tabSettings_ui.py | 71 +++ ui/extends/__init__.py | 0 ui/extends/color.py | 16 + ui/extends/components/chartSelector.py | 33 ++ ui/extends/components/dbTableViewer.py | 5 + ui/extends/ocr.py | 18 + ui/extends/score.py | 0 ui/extends/settings.py | 57 +++ ui/extends/shared/delegates/__init__.py | 0 ui/extends/shared/delegates/base.py | 152 ++++++ ui/extends/shared/delegates/chartDelegate.py | 176 +++++++ .../shared/delegates/descriptionDelegate.py | 35 ++ ui/extends/shared/delegates/scoreDelegate.py | 247 +++++++++ ui/extends/shared/models/tables/base.py | 35 ++ ui/extends/shared/models/tables/score.py | 197 ++++++++ ui/extends/shared/utils.py | 54 ++ ui/extends/tabs/tabOcr.py | 476 ++++++++++++++++++ ui/implements/components/__init__.py | 6 + ui/implements/components/chartSelector.py | 254 ++++++++++ ui/implements/components/dbTableViewer.py | 9 + ui/implements/components/devicesComboBox.py | 32 ++ ui/implements/components/elidedLabel.py | 50 ++ ui/implements/components/fileSelector.py | 91 ++++ .../components/focusSelectAllLineEdit.py | 15 + .../components/ratingClassRadioButton.py | 100 ++++ ui/implements/components/scoreEditor.py | 197 ++++++++ ui/implements/mainwindow.py | 38 ++ ui/implements/settings/settingsDefault.py | 73 +++ ui/implements/tabs/tabAbout.py | 23 + ui/implements/tabs/tabDb/tabDb_Manage.py | 30 ++ .../tabs/tabDb/tabDb_ScoreTableViewer.py | 114 +++++ ui/implements/tabs/tabDbEntry.py | 16 + ui/implements/tabs/tabInputScore.py | 33 ++ ui/implements/tabs/tabOcr.py | 133 +++++ ui/implements/tabs/tabOcrDisabled.py | 9 + ui/implements/tabs/tabOverview.py | 18 + ui/implements/tabs/tabSettings.py | 23 + ui/resources/images/__init__.py | 0 ui/resources/images/icon.ico | Bin 0 -> 165662 bytes ui/resources/images/icon.png | Bin 0 -> 25356 bytes ui/resources/images/images.qrc | 7 + ui/resources/images/logo.png | Bin 0 -> 32768 bytes ui/resources/translations/__init__.py | 0 ui/resources/translations/en_US.ts | 460 +++++++++++++++++ .../translations/extract_translations.py | 54 ++ ui/resources/translations/translations.qrc | 7 + ui/resources/translations/zh_CN.ts | 459 +++++++++++++++++ ui/startup/__init__.py | 0 ui/startup/databaseChecker.py | 83 +++ ui/startup/databaseChecker.ui | 107 ++++ ui/startup/databaseChecker_ui.py | 148 ++++++ 83 files changed, 7529 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 index.py create mode 100644 pyproject.toml create mode 100644 ui/__init__.py create mode 100644 ui/designer/components/chartSelector.ui create mode 100644 ui/designer/components/chartSelector_ui.py create mode 100644 ui/designer/components/dbTableViewer.ui create mode 100644 ui/designer/components/dbTableViewer_ui.py create mode 100644 ui/designer/components/fileSelector.ui create mode 100644 ui/designer/components/fileSelector_ui.py create mode 100644 ui/designer/components/scoreEditor.ui create mode 100644 ui/designer/components/scoreEditor_ui.py create mode 100644 ui/designer/mainwindow.ui create mode 100644 ui/designer/mainwindow_ui.py create mode 100644 ui/designer/settings/settingsDefault.ui create mode 100644 ui/designer/settings/settingsDefault_ui.py create mode 100644 ui/designer/tabs/tabAbout.ui create mode 100644 ui/designer/tabs/tabAbout_ui.py create mode 100644 ui/designer/tabs/tabDb/tabDb_Manage.ui create mode 100644 ui/designer/tabs/tabDb/tabDb_Manage_ui.py create mode 100644 ui/designer/tabs/tabDbEntry.ui create mode 100644 ui/designer/tabs/tabDbEntry_ui.py create mode 100644 ui/designer/tabs/tabInputScore.ui create mode 100644 ui/designer/tabs/tabInputScore_ui.py create mode 100644 ui/designer/tabs/tabOcr.ui create mode 100644 ui/designer/tabs/tabOcrDisabled.ui create mode 100644 ui/designer/tabs/tabOcrDisabled_ui.py create mode 100644 ui/designer/tabs/tabOcr_ui.py create mode 100644 ui/designer/tabs/tabOverview.ui create mode 100644 ui/designer/tabs/tabOverview_ui.py create mode 100644 ui/designer/tabs/tabSettings.ui create mode 100644 ui/designer/tabs/tabSettings_ui.py create mode 100644 ui/extends/__init__.py create mode 100644 ui/extends/color.py create mode 100644 ui/extends/components/chartSelector.py create mode 100644 ui/extends/components/dbTableViewer.py create mode 100644 ui/extends/ocr.py create mode 100644 ui/extends/score.py create mode 100644 ui/extends/settings.py create mode 100644 ui/extends/shared/delegates/__init__.py create mode 100644 ui/extends/shared/delegates/base.py create mode 100644 ui/extends/shared/delegates/chartDelegate.py create mode 100644 ui/extends/shared/delegates/descriptionDelegate.py create mode 100644 ui/extends/shared/delegates/scoreDelegate.py create mode 100644 ui/extends/shared/models/tables/base.py create mode 100644 ui/extends/shared/models/tables/score.py create mode 100644 ui/extends/shared/utils.py create mode 100644 ui/extends/tabs/tabOcr.py create mode 100644 ui/implements/components/__init__.py create mode 100644 ui/implements/components/chartSelector.py create mode 100644 ui/implements/components/dbTableViewer.py create mode 100644 ui/implements/components/devicesComboBox.py create mode 100644 ui/implements/components/elidedLabel.py create mode 100644 ui/implements/components/fileSelector.py create mode 100644 ui/implements/components/focusSelectAllLineEdit.py create mode 100644 ui/implements/components/ratingClassRadioButton.py create mode 100644 ui/implements/components/scoreEditor.py create mode 100644 ui/implements/mainwindow.py create mode 100644 ui/implements/settings/settingsDefault.py create mode 100644 ui/implements/tabs/tabAbout.py create mode 100644 ui/implements/tabs/tabDb/tabDb_Manage.py create mode 100644 ui/implements/tabs/tabDb/tabDb_ScoreTableViewer.py create mode 100644 ui/implements/tabs/tabDbEntry.py create mode 100644 ui/implements/tabs/tabInputScore.py create mode 100644 ui/implements/tabs/tabOcr.py create mode 100644 ui/implements/tabs/tabOcrDisabled.py create mode 100644 ui/implements/tabs/tabOverview.py create mode 100644 ui/implements/tabs/tabSettings.py create mode 100644 ui/resources/images/__init__.py create mode 100644 ui/resources/images/icon.ico create mode 100644 ui/resources/images/icon.png create mode 100644 ui/resources/images/images.qrc create mode 100644 ui/resources/images/logo.png create mode 100644 ui/resources/translations/__init__.py create mode 100644 ui/resources/translations/en_US.ts create mode 100644 ui/resources/translations/extract_translations.py create mode 100644 ui/resources/translations/translations.qrc create mode 100644 ui/resources/translations/zh_CN.ts create mode 100644 ui/startup/__init__.py create mode 100644 ui/startup/databaseChecker.py create mode 100644 ui/startup/databaseChecker.ui create mode 100644 ui/startup/databaseChecker_ui.py 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 0000000000000000000000000000000000000000..94f3c30e241cfc464513c340d6c5e6b1d18280b3 GIT binary patch literal 165662 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 literal 0 HcmV?d00001 diff --git a/ui/resources/images/icon.png b/ui/resources/images/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..8a632eb9db759a48ec3a87cc85e93be3b8c5b82e GIT binary patch literal 25356 zcmdSAg;$kd)Gd5ykQ9^-2|<*QhC@l0bazR2cStv)DBU3;Aky8Tba!`m*SF8_zVA2g zzi=6Y!QjBNpB*danrrTG1vv@KXT;AS5D2D}q^J@E0(TDkMnwjn^i3{BfFEe~lA2Bs z2v!H|8?MK$;1l?e$XQIoS=r9i+0D?=1mfoA#$;h@yDpGp%v9;zE!m=?lUSr zIkxjji%nKP?eO4Z1rbMhH;1_BbA>kD7tf;`0#H%^EZ(IqQ4|&u6Vz_;UGZHl9^GB+ z?oH4v9zBTueOPo#uXsPmgGw2MvpUC6gn%QAN-0Xp;S7FsVk?Ny1>p$aL;fHBRYMi! z)n=vp$*RG*D3)4UAdVw}#NLF<-lR)Wj8K0lF|>}(O5Z)2dRjou1xvU$&C|0Z7C}-u zEie&Vw(eW3$1n3P*3dDjfm9s4KXa*T_yh@jIZzZ93d`+v<6o|KPX&V?Q0ut5A)y@G zCf$TSzTbF^!Wbb`B1ku3fc4~*%GRsR z5145NQYU|gc=rblVY5OWJhaEs)~UQdQr&vBJCa=sNyvB#;GjKsE}pPNS4<1WStUy7 zoA4+xb{DnSIb)j841=-@W81I9)axs<(RJ2nmP{_&-=RSTsai)+`TbuK2(r<%8#Wn~ zo1b^Z;86T@(mk?1lGJ;8EQ&0uz~R^qX`G%=AK0bchL)|`Q!qTN^P#BdZWbWY5!cyXs#STTi zsu|^?2=zH+Y|QpS5H=ky*)h`RF^hslgF=mw;@fba97ZZS0t0{jB&--F;1Rx0E!~@6 za53)REh*lEJuF1A4<}l2T2qjOhfBzSk3Pn6mjbr*C=2@vQvSPkT`_b(KoeL~*cErG z(6@vaOny3GUS#9L2tggY*VH!3)HK8Ir%7X$3gm>2kP;^RB%$md9y=|v1>-p;~60o;x!>9{GVo)19 z1vWaG-xm1A1A`ap;8eS_pVw1(Sqn)eci?-D87k;y!ih!Dbx_n`wPA>hT_)9cP68HoRew{1!o8Wg;7QxVtq$`b)2vRUKADD31fQ7c~Ru!J`Z6FKsfMs5X={+ik zvtYUV;%Q6O9O++}-Ft^F97<|xRNhknuW%gB<`(?&pWUwmyEpwmcCW2-*j#FLK;14Q zli^l4DBKAqge^L~=PLoiz$#j?Df+XTjO&tJQc=?AVILib6E+bl(n)muNOAo^R7&y@ z7u}ttcQ+(u;gJrkajPKfuAnDE&3nMm2?G|)Rt5Jt zz3#ten=C1&1-ggecim*(-w$yci+DtuRzbmD23-@>%q@&XZwl>Amh7pd1!G`mz+4DL zoFnziw6P4rv2)#C%x_wX2Ze4!!BYJ1 z@4pUD|NgRr{ z7pznJ@nKn8!;v+-eJryq;8R~(H=Mb*7~kk5XF*Xs-dk?dnoS8WR=*d2w}MLf@4cy^ zP)w$3krNm&#MUS>QYO7}j*J@2JqV{^fW=aA<#Y&G@?k7-y%b9~gR9jrvxT)BDhqn( z*a@z2)NP2=2t||>BfaM-0cQ#$aCKqSx-QeIV8*vvmw$Fi*B8y_lF>G{1c~qnY+J0$ zHNezygnJL1b=|IXt;p(KuV%$3G<(y`gKUN4n-SDzQY*}Lm zHIsMjxy?X;LZ!S~vqv1z($NskP^J~ZT=B4Tf{1NSsyW+R{Cr z((30{oWFRAp)#2{$VTv-KM%-YR;J$xg5=g92RLTS?SG8PHAbZ!hp9zS6v`m?f3|LN zK9#k(8ZI#(BrtVZ8Wt{zKg^AzQZ`4jAphLF|F%o|Ifl4iI5JY*d(5_?=f5q;-goty z|LW1#Tnv?Xl_Ta5vA*TkO@!C2U6MSJ1tMfn@3Q07#bxGe%Y}NEw4w?`D&nknxI5dv zX)wdh(BMeX-NW=11FsaLc1FTy&=6ql$Jend*VtYGe=pmvA8c@Uo2_>#OIJvHJv@x6 z3x`rsnqw-5dLn@z8EnCXZey(MPh6kCnACFb@N*$4AGQ?2ETIi7mJF zOlxNSilGSLFX1d7#!_28IlJVlD~eoYzwYYOuhcqFgqeNVh&88nCk!qqrcr+Z;fR@@ zo<7`wpHdb-(o`8_Vd2KMk_H_vPbiArV&iW8F2iSk@ol})DO+t3a4@)%?2!|@pQ^c3 zwoqRH?+geImXVmYj)8&<3v+)QyV1JC!`)rNhPv}K<)VsfUd#7-!wwZ|ULQ~%^zek? zMYwQ`qju_>aRDNs z{2c8NUG`}oX_p5%dr1dj!*syw2VByB>v1GicB*)np+?q`F%sV(bb59s;t`vJ)v7R| z2_Pj+sq5a4@uH$=E)|p$33g#F)FT&f64;RIu7QE<41i3$CQ{J{JAv z;Fb(LhZH^hy0?#sv?tLO6`~s}jY^5IW)x)>f4JzV;)0_uLG6r2IOh8dHuY7scU${r zVCo2#+X1Ie5%}3qA3DR_9=w-hVFaoi_bDEP32SnBdfAtP{tgx^!4l!}W@UQ|R z3QEU{z4v*`tF6$h`{@H$jY?j>bx$wP%sr@%E!XYsZDzy5_Q8Rex*Hc13LW-1qlMZo zib_bJfIq3+?>%Q#fL3fLB6ILDNtI1j3^T5f)V1flW^fmBH=aQ%Ekz-*_AIyUUK6uQC^${AEfgI8w&JrRNh)hXI2`3VYA*7>2YP7bBvzeEFKwmQ# zJvphy&dIqtnkQ3he?rBiTOX93PChXr>Ay04@PzqL zbQYA9fsoElQ4AR=AxB5n5G*429{v1YpR_@q|YS?JMnBw{*`S2|dnw-wBj{RDi<--k?3 zYjk#ZQ}OeYLF~^qWm5%wh%tZ4=VWEU9UL4)DC4$t#l5gRdZ2>{Dv))l52H3^Y;H10 z+vk*FeFy`ELqkA7z?bCYh>;v|!}Z=c)Mw9>$o{eulaP!I5BuLL|2%F(p%`{@ry@FX zw7Q+vZCK>}2@Pk?eC{b&H*$riz=lmI)Yc|R#!tL9Hu~OiU9yENI5HwKQUq+@pNfi+ ziHYEE7Z(L3C0&KeKbKt_RC}leVv#gm_F^G?CI2%}e>#gGN$L@DD@fYi<#PvwT(+dN z^2tGl9ibiV+p%-XT)a>PuT{hta{dUXt-%o|H>!Y+fJU;76t_YwBiHd^ihyzg29KHV z-y!(`Kdl=_8BYH;?CtGstz|JbsF+igIOQ!VDJhs~p+LbR{U9L`y35_kb--NQ?;CWs zwUac6pdjKGHp3Pnh!C>`3}M}oJ1z0OFCXhqc?lhz)Lm}4&rUIHW`mje`N&m0v10{k z7^rLeY2XvgCj;vq==d;C#_r}r1<-fF9oBcqXE0g z#N*@RnGFrvE~Kj@|5sFFd3P)71xabx(&x@GJ;eU{?Hh|zH%1gWKXFug^gDu=FN50y z;MX@cS6kZx-!L=(WVBy5MCN625LQ>woGUPtt;^HZCyD^1elH^05#TAs5VS8O3c9otwo)53`oT^v# zkFeEMR*r+zlQbNEr<=?)e@(^VwDkP>^Uv8HXJ3B&0FSZw514lJ6S2L2O^sk;Vp33A zn1#2E3d!B!k7+?VMxPMs4-((5N#d|FjL&b73!oNv!9O+NEPk1kl+-&g5S)e;h(nLlYT67TTuXpA2Z(R`#U!5k}W1FE2~NtoS4|R(H=v_`@11KKzt7_%&9u}m*W_8f=31YIz+%wIGmq$wfnif zLeKl86AKHCT6;5j_g zCNZOf8+Nq#CMo%`bF_y-LPaHh_f2OH8PuDQgv(Y`Uj762EWA{MZC7F%FPFX=ndolo?IXFkCV>Yb8`B3nlD>AJjL3yG0@U0(;FW9nokJ&*YV?BOC16^lMa9HcoH8C5KqvuQ-7U3`S>>(kf@+6e@Z8gPGC__)-hwl; z?;6i{f+!wKoCkeIx$RW<+w;P5d}0DvBSu>TyD8b#;~}P*D&5JCR^lwH+sI7QQqeDC z;bs}05y?C(m?;wx8+|_vZ>eJoU=IY$*YEWdDZ4ATMh%}iMmQ2`@IaEcF8&-J+w+~B zk5y76CQbrxwq9yZ8~l7Fl6unkXF=PwXmAA+jz!pP9|;{n8_jZ4kARFUdgyUr2>_0K zPOHt2{me}HE@Zx84%J`F3E;D*kUCq|_kaCsf%|Y~sfQ2#S>A6~IV>!*<2vj{Hba06 zoq#Df|Im-sv7%TY2O7k6{SbfGpCr@%%Tu1rpAumEui68A3=AJ#UI-)Snm+k9-W@iu zKOE{4+cX`HUllD`DU)?d`U(n9oiF+!aX6Jlgon482|QR>Zg$eirm-T$3f#Vl9)FP0 z$yhjLE)pReD3jQBGGidz*}P?eU`=Ski+%b;^SH^DA>{5}yZa^xwXm?Tt$~DdSaQMd zynbP}Q`d_#RfVkcjW55jo6{qk0^3#cmoGT5EgJ6+*H47HynTlLOr_`5=`=_2ouF#y z`>#u!=R7=TTkzg-z@n($a1&;DUTOlgR2(B5#?HgT(>pk*Mi$uHUd}D}vsS|eBj?&g zWN2upW_S6WDuDTF#V_TVv_ooZxhE7+<81W|DQV@?e?gJsf)E7LIBk+;U9hOZ6H{KK zPuQ)96Fl&6#uzgA%;LCdk+SzMj4>h!7~6Z}sDUjxMxncO`YO7hQeQ7**QTbf3b`)R zK$7n+0zd>dm;Q?J!}I#IGYbFYI`xk! zoYBcVXoOe|Wby(xH0{D7uz$`rIC@Cm#rboiDl#;n^#!@RITMs?l5%q0-cP#P!oB># z!68O7ryIlUvMw*E!VvrtQaJno5@!Vu)M#7}K{Qmij@>{$&`?tnrm&=W^_rH(V5B{` z$QZ{Vxho)}5R3BL68m}M{!J_bj5c{3G_5e~I^94;%hEQNT9%fV?-?&tR_%G!ew{kQ zb0(Mt4QB8i8K(RArkt%CVRunmm0b5X-*}efL{S^OpzBf z5UAbw2L}g->-#MM02rBvj9=yI+b%YSkNuQi>qntj1`+qa%pKl4vhru!_+MX*o)Vd8 z&=B)0CvEE|ID5-Y2`M&)YU_QD-4@3KGKi>V-!SO9=poF z1Ho2KZA4|E{jy!2oGftyEP}=7)_H;j^(1+7)09-e5m6$RT+GR-0zf3*TC7h8zor}2 zU2x=Y_EFPVx=$S=)B=>*xJC;UbB=pqelHZZV(aR>d>lv3?S1}+N&j>wNqv>bbmjha zw+7bLNL=X<=lKjzm%PIuAR|_W0s;feKmEmh-wjep=lcX^UEzzz2X60eoLe2H35PK0 zC=*y|h}-(ED962>?&thZS#45M%Oid)|H~-C?Qa?PG_-P=ytZLaI|m0{j|eEg?Un?R zmFV172*|itO#8AJxm4+h-eizwg-&}mW4;GFL(Fgag2QRm4U`!Qy1LQ)URT-0#WF6W zM$!yfbcjoc$7My|Fye=zhtKi}UbDR8g?k0a4ZjV0?&B-{dyv~<$z3$!#}EAd`8vGN zSkljP2WkLA>2s}F>sV~$W2>MLp=OX|u@!Ipg#0wM&8&0UQeva?&C1I|0+^|Nr6aU3 z?%QX3$8JCPOi`CFpN|Wl-=^++usNH~Ao;6V5PjMn&bpolCy&k;L@lsyDuE*Hw%__W z5T!5MDa8A9WT~u70ij@@?luf9P(C4J2QOhm&ex~KEwiQ16s|31JBV%*uknO`=VKXy2 zXhZ$*MZ(k4>}&+6r_LWYrf}$!6LaXQDl02hUA}~#gg>tzYi~iZ+zd(Puma51sxm;Y zu7|V13k_~5u9o~D_MHTx7~AWRB4d+BC2pIGii=UQvje6)#g^l0Z>ntV-$Ze(=fzuC z0W&#iI_-5^uCcgx&E)T=M7D1DQBZhjh@QOba}I&jZj2 zm{$*Kmb$B~rc@+u!n%xvyuJCpe*5|*J~b-0Q#2?vWpA;d^w3s^+mC!}D5IzDYztPv zonKrO#%!S9GT?JMc-`bfCxpm5&oh=&&k|Y*Ih+s%lGs9%c}~!n642*lWE3?uEh4ms zvU74+JjdU_VE$hzsoygPMb!KXAh(X>NEn}Qk3a-c+gsdsaOKjV!O9mGwazzx=zaBN zdp9R<{NOYSpuK^KoQAlKF>=X|c0qN1y7^Ka`#ewx|F!lfT1$J92LQY7RR2ZyQ)hVWr=MFrl` zFYVBJ=#gMO1{PNL`rmIXF8^M1c6PcyJg7QxlP2<6;KGW*m>4WlK4;XstD~Nay(v`* zWee*>{>>*iK+^O!B{~MBe^ch`#TrK-%!Nh@=8W*LRP-eVc3?Dk+I-cQ9JP45dw|7Md~D( zCjo!vA}mL)vFPEg{l;APe1gA_eYP`KTs>ZZ<;V+$bm3A9NUqDGqNZlC zo#zHVT_<8^$GoW1M0{~^5z<0njA60XZFo3eN5~#Viy(%Ggha{AjIB8{GhJgx;rcxx zr{{^JL2PPTx?6*ZYouUWh5G!SNKA&u%npR60DV1=3ss#a&xzwJ zX&IT0c^)SmGBTTMFQn|jLAm|6%sq=uUJt$|jlqUHmkTLV1%nQe7Q~*uzRlmRxGmu@ z)YG~$c+%wfuI8QW6Yn7yxZI_BO%Z}Er_W~R<}iISuCLtz#uJy8j>tL^5g4DJk7CrW zSp|krltZ|qBT?p~<8o0iHz5RJldZdgwjhIi2NDPl;C!~X%{xUUsQLIvv$C?n!oofP zSk*Ov3j^T<Crlrlwd|fLqpSQR9+Ax*6Tk$IcYR7TNUC{Duk!S*k@T~B8>mJF0?nN?LU0o!8U;DC^pmbSgSFKVVv00KV*;?vYb@u0^;hNV%WHofo4 zz3*zCj2t=^{(D#f1Sd+?x6c&g;y^e#3$KDFVAkuLtyw(HFx$i6FnKvM#^bk`Xjk+Q zp7|$h%Y?uug5^@e1ny(EX_e7^5E?86sAAZ*f`JEtjYl?@o^fM4D zJ}c&k-*mnpqD-%yoeopi_mcHwtC>5{Xf%IL`!VrVRVf|dJ{7(0xp(*W99G(qIwn!v zBb;KoyTt*OY4r5?U;s!(N;u>0n67CS z{QtwR`}>o`uy(@;L(K})k=Hn#M^)9;t8Oh%P3I!U+1c5@jcyz}ySk)GcyVlOY`7iQ zQ31&i(x+bfrjJs@*_pjNnk@4C+&OQc2}FLoUs}NxW<5Hw&cA0{T|zn`NffHHmk8fh$FPSw@t*W5uj?@?o5heV9~LzBr0sTCk?nJ+Hy zJe#tH(lt}D$>y4F^CMorrp?a$jYI$UFW>=IU#2?U-+WgGiSkv|VXed3@%374hv~)P zSbOQ9oXXU=!O>!q`O)_Au2(TGE^a{7x%0;~=)@k^h{GVruUPx@Ref$P>^t6zF-43= z9%qbAv%@md(v{ulT$c!m>s4H;Zx|RbkdTlP*&V3(sp;TM#`0ySsc)W*_xFE%@$#jI z%aw$-Hp#!fK8NFBN)h==Hnwg;TG`~F#(ABxrH^jQt#H4zTNc$*Zdo?BrI;BRFJ`3$ z%X|lH8mAv?Q2k9yEr|bA`~iF(dIMTb1ddVe!`+pP-6mi%Mcd}h4iKoiAL~u6ZZYk9MlWR(-dcf3DHdGiD0p61fv; zEbp0jcKQATmSVoX0)PJe`B*{m1IKQ6celOF_ra>Nx>~xJ$8M2t{gatYv6~=ti`#w~ zQl0Q))zqJln!u!Hj)j*Q8_;}untX1z=N65ot(Oux4hANgU7K5tCwh7o+a5E)YlLH1 zaKDKjO`ib@w*zqf&q+v%0W~7Mow`;vr6=O!(^Q$fNSvf!=ZLg7S=RIVss4SZ6>?9P zA&A;kf6*g-qxo!xKQMK>i|-sVN<{lom$AdjpA^Uol1Fj9Zo-ap334GrL&{%zdM+|y zN&AaUBu6%Lv?YtblGs?Lmd!Q+NQCj01;5U#kPV?xET3|J=KRit2qEjMYHMppa;cYH zRs}oGxvE$*vERyK*lkx-MB;xgcL`|@hnM?_m*Kw85=F!&4#r-|EB_Ti7#oN0bFLFDTO9m{>^{OvF>tKb&*sjm4lf; zP~{rzjOKwL&c5#B=;Sm4staJzgi1>gYP;$kefVR2@uts))aOKO5v$gzX@~~RcNqNf zDJhZnGZi~u2!$c3{9gEe_hT8kAHBr?4|n#_Zq)UxL+-D0 z!-D%o)l_bVqI*L z@5-C5Q{MjD)c1H<=zay;5AbLoKxGlq-9w_LKgJ%-!-rZrdi>4Jbu&Da!(RfUYRJrG4 z^2Xx@ioj)!h!dFg#Z6Sd$7{`KY7z?iHpXk!IDrM<9n>hQsckC1%X$c|NxoqOTm?um zaq5dQ@aR*F%q$4`UU^)AR>?MjIGRjdRA(+5c;C*L?S!xk+jj8ubPuFt?bX|t#K*q? zUy_n8bP0XIQ~G{np;#0@^iudzYf^3)@n}DOP*eM%zNGv?&Q@&%?@y$(%{C9uYoVxe zioRck0bn6)^8CS+&UQSMZZM90FoTy)r)S4`^+euV_?G1_02i>6QSjT1AFt2#1Kj6H zZh)z;jG+)*2;p_V9<6CxC5!gy)*(wEt&_T*hRo*9la}CKHnKr+WY!Q#A!{K?y*asH}r=?wqq?;@Q`FKg6FI7 zL6iPBO(iW*J86CCCA_Qu<@t8{mm1&A`Zvjsk_u$OIAb$aRs^%Jb#%U_r^kSr2^aT4 zH3Mvdo}cc&eK;a+)v^66ToRH`EfYR9=V_WNCX0xj`_V7iL9t*!BRD zd&M3K2+Gb%kcxv-HosiE=gROwZ%obT)Jn9(rZV0dII{`5?i1?y-k20#jbjtBA``cH34ai#r=Fo>ft?#u!ZAIrTXIqfc>;>sHc*hw|M z*qv}5iGW`;-?W-66$9xK)O1?^sQR@x2a^y&J19%i6}dZ{JT%Y5y>2~YN$O#}JO3NM z&_haUelk2dITf-%9OdGIUoU)J1q2vnch!a?78)$PlHnIEH*0(s0WVVSab{|9AtEdi z$1C$s*fD1&sp}L^UVmRvWGF?EJrquc&qw*Yk&)5w52pee!uPVEN00Tal)OfRK}nrR zzu6fLW-`SRw>@(M_dk-8iAjq~zF!IeLjchIo7&Ka*nz)E`s;tuQRZQ2hrPRe^c$D* zfKmA}UdDCMj=Cvs_Uz-s-N@A3I~C*8T&dB+!b8w`*et2puB%HZ1hWL7h8$E7N{gTb zy8ZWt40$srRSzYXgGf|Gq0A+`<;&DdBO+Qfr|9jjuInHI^Q zFOawhb|F}Ah_C}B?A%WPd!=3M<;}Oq-AY`r-!)k;{y}OAi1oKk0<}$hM@QZ`t44gE zCCMn>qXq8`vsfDTiTWo}-c6iM3D{!%R9K%@%A z$Okb%fEk^f3?ci9FKU^*$t&UG^mygL6MK30+^vNKXe|VhmJt&A7wJJYHy;2G;|S4E zu*qMLW15+n$^0E1n|pU4lFFw8R1E2y_Q_4v)zwR2NTn(>>{6XNs@Hv$g~~n~3M|n= zM^{wmz8vQ~Y!M0XTXRg#iw5Gn*J&sp_x+b&GyMJg7evG_U-N$b!XU$qg?oKvmT$Is zm9qz3oU4|VCJ*iST!<^r%#NS&ErH+9!OwR?gyc2|?X@e}v3pK~G_2HGW%Dx+DsZ_O zeGQmM^oib7rmgh+%S+e&xdwG<#!~~+G}D#@P~L-_t|t6;IZlH7B${mz?tQmky+K<* zDEa#3a0oe>!y1o$?!WL2T_mTiQDJW3+ns`CO7gb0HbP$4U-1kkyl&F`x8LSdW`52b znMi0R#X}Vp6$x34g5tIG-h`&yPt|c`@qmz3I4Wu0JPa@&C?hNW55rL)G2N z|Cf62oslF*DOweoz=U?dZx&@(l#AV?yRqR+mD=VFi=)=ejNg^a_7ho9(a_?Q`q0}M znD~zCPwzPYYi%0fG~4S22X%@@k@6@3^Y!iy$D<3<%Seo;*+`rMYz-+cpGZ1iBGFY6 z_V`SN*$EJLpiYgj4@@!b-k$Fhol(>+2{xLEm_@N5Kjhs#I92qe@hH0>3VTZf=sUbe zmzyW`x4f*L*vH35$uchXEDy;ZyFy|#`)~MKk4)j(Ykd1h+=ij^Gky|RTi20ikY)#g zMO_$GJFb$U(;*Rt3p6R9`Y!4zGmD~>6&7)8SOA1nhCP|1U9-S|Z_PJ4`E1?x>h3Hw zG=S&gzw}!DIr|ftu(U_v{VQv0Jsy!|S)$vFRa{LCpP!fR69g2tZ9G@k7eE82RGRmz zxH!jW3;E<{+zq2`jrPA)scypVyU90XkSD2;_3T*w`zH;Nnzim4NUrO8+S+)rVIPBg6#CT|C|{bZe}Bf=2$I;! zk&yZ8a6ZjK@6sE3f0A_bFg`VvBhfxllta_c?m=eUcV_xxWARp|FVyb{2|aM*HAJ8x znSq~Q7k1(?<{}9!MHaTU_*y3NhwDQC4|IVNyK)(d10m!v3X*3kFikB1jfN5b7SL2n z{Wk=hi=9#p5tWU?UJl}-n)$F;?hnO5D@rW z(EYbP84_#q;L8CJ{$IgQI` zjEj2LB$=6-8vR%{G@#qt%UhS0JNb*Lw~Qgkkg7&#I09?j;*>$Zpf1gcNQQZ;GwI`N zh0z*2FHXQ1ASTo)H0S2qe(%}w!Z%EB+W}_~ybC(=tMheE?H*ro@q(@1;7*y<+(!m~ z9#T&e4H~fRLCw%k10w-^jPL2=sPxr7Bt-eAxl6+>bV$ zzSxyZe=$h2CQ}5p{*07*w|-Gj1Gt@t%XGZe&+h`zM{r+9nGQysLU-304qG`Pyv%Dp-bwYY2u4h}Z#{X$7T<=U`NOLa|%1_8w_+SD;C zf$=@hGV|Qzzqd=gSc86roMWwBKy}8)rvnQ!a)3ziM8aANizU#P@LIX|@bo=|^Gh~u znFGa0(4t5;yo^5Rku4MCHPnzI+49m4+En>qV+dThc+f--qMd~GZj*jti+HXZPY`xr z2V&*>HXCEAt+(^5#+VQFo30PblltD{!)^zjHAn_S#edg{mh|Ez!=O{zT0wdgWO&r6 zS&GXePWnF==R_psvuMquVB^G^|oJP!k zwn^3g63suJ1QGM+*%qMDk2EIj>h7lY-z@1oJFaO=_{YY2*@}x>FpEg+Df%h*IR2BB zRj~!r)%%JMiKZ>uI@Z62y&_4?yd$ya=j&W0CVpHUKBiDuZ9|qP&&%MpIr+Xq;w->= zbiKS9ddj{#9EU_<>-rId4Uj@WuPJ!3MXVilVI8w6Vehu)@B;gA2DY0=?5%#L*0e0y zORK18pFnlZ?^_Zjp$ims3<~Ki*Iu5;094gLE+2sT_n@LGqVsuwz3kA3AVF_;Gm#ay*uaE7mT>g49GIqL|nZS zwly1qhgaA~bHg$+GQ8YdPu3r4S%3g55E)Ym7D>mq9NS*oi{|%+9x$fq>b7&-$E}R* zS5;{kE3greB`wK)ieH?Zq`iLx!MmL!s`ay-imFZHQO(L7r!!<)v+BYRZvT6_w8AHq z_@jM1n$A!4NGTS-gLZo5KGXa8i4EMBiK-<m~X+HHRlS(0zW@k0TRbqES^Wm!W^CNP~wEQy5mz_ zrKR#shwq382^3=9|Hud-YmcJ|&kd0a>zTK$8Qx?3;p=0~_l0RCS;p_f!2KdAJoP7a z{fy&0!Q?t(PtafR`zhG+TmQ+VS-<>|%6Px|-Ckp~To`qyQ#Ph<_jcFo#5M|d6RTvn z0iEv2%Fq$SW_HKHV0<5Vf7SK`HBIg;;^C(1LEh%b_jxM{x#`z`@#y%dC(pAf zj+67z%y1r*R`y_3bmeF5F-y$ZWKNr16?*M4a`AJSobAd63-N7?ExSveTOZnwEhL^6 z78YEs%`2L3>ryuts3pUezMUwN_co}&`($|dg*AsVT?daa2xeg17f(#=-8Ka{e>i)_ z`bgk&QgEjJJ)zWG4cXs>*Xo)eYfAXMKq(9(`l zTW{Bj>2p^)5K4G$hjI%|`oz~kO`{MV6Jx^nR4p@oyD4MNao2i|CV)UdmQ`%KO?rKC zS;WYJ;FBgHb2G1Y&;W^FCVIT}2K46M>`}>E3vX+Kj-0n0LRnCln)3Rs^Q7QWX<{TdNr7&wL zqb7gGB%{(S#NtvLCSoD~Ofp9G`rg`Qv5{WK*#DLt{f=&gsvWbb(&&ud|3SEN@B`v) zd0bG~vzLTjKqrdU(jv&|oNStqQ(msPeZg8fW!vB&QrqqS{@xqbTpwXoQcG~z+xLQ$ z_7+T(UEwtjC4Koo$I3|0N_IPP7aDpuVGFJ*ow9Rspfw3d3yUP2un9n9`xmKArV`^r z^i-=(kWuc^qqH6)_>8O{6MPnKp8wazw zS7ot1rj3p&(c8VTd72vh_iM@Du$6AkUAP;TK$5oKIuTL0vy6PO^1J#hMvX$>~sSJ>Iv`{CAQ1tUB> z9E^}{%A~KZPEe}bunABPGB*C8eNM@M)m|mTZ*P-{BGu8wkGp7$bMI;nk*ED_Lr(zV z5jtWg6s8{;9{$|(LAA|7v9*Q-%Hd?3%Ik)O&=|Xp1r$r|M5THc+g~{48|z?tlKz0v zSe-Sdhhhi@nQ)D?hKY>itH`jZ7nb7cNa|DwAB2SiEJ*qShXtJ0QlT!=+ZRBi&`$K} z-ug;}Yi*wdkaKq1CSEnETd7Yft_yP>9!OM_zCG=U!)(?gC1Q|eXK1k5vqnZ99wK>? z+M&kkYJ5Oud>7c@1uQnSA`na1)#G)6nQYS&CjJ%igSQt~=5vtmV|_ zRdAVkd0hpgL5NX6wic@oYN-fvv^fbpT-+clcXTph<6rstv{^%!1qE;DC7$(N|G71| z&t9W_!lr&pGCEvbJmtdL{n!;Y3$X_h7pP7Mj*tWVoEP^N5btN(htQx=zqJGY*v;#G z{d;&XG%pv`Z2RyKUn_o!B6TfA8_R}e{SCCElyOe6{O0BRcoN0A16LVM*f!x2Wg)cQ zoNWcO<;IZP+?t9E^+)&c^u*;z1Y1TC^J{VU+@KQ#;cxK2*iz*!u5Pwe@#r9iZl!9^ z-@k)k)Da&LV*4lyxow;`E3K1+zN9&){i?hMe-oay0* z$ho+wE*{3`|UK#hR43GJoXSTWM0?Obl^cq5ANf{Zu zL5ItI&2u7W4laZzLywzDnUEJP(JKH~lrEs&F+(m`tz2DQN87oD5%W+v&u=|>o-}{v z=Svq-N2DitC6w-g-pGrhxWxD%Al_MFxCjWHou}Z%lsJ+Oj96-WhxxEQqS};NVfVMx zYWoaJ82*E`PU>()55WDIGVKSUfR}6M5FKG}xFX~6RJqoP1IStOFO{eC0)lr5P?x`R z^YzNhLinJ$kzWOwdt0%|W2{btyL!bCMJZ(vKgg57dB6mHUJ6kZq_JmVagF3kvV!&%cy{5EQYaLLZVd0mu;f(iB)=OQ;z@J^B>-oSh{x~Eg?<)T_0_D96o&2+{-&!S- z8GM?xa(*M0t}d7RGt_)g3O@JW@jdK7?o!KOc(<`&GGg_VzMqY~20DVkSD+}qufR0l z;h~|CW_z`nMBC@frtN+g7b~8bu&}cOOv*LKGMl4g?!5phzw5_GeDm&Rb*-ksy|ErW zyKRVp!}&eD|8!fEAB>rhth)&R@_}}CcyDiSWM+mwl^Uow@Va9OO3z-g50p;+DA69F zz7C_MqJm$0{g4`__sj{qhu1UNm%hQ=?~TQFUS1v(`n39|amR)QZbfyvoEMaE@4zJ* zAXOu8Sp>E0a+ZkFpY@FmqRC2d3ND~6Ncyrd2npi&ry>ia_B3$W1Wo>7DH=|{q_p(# zn^t8;JUc*{dgVJl{v`>DGTqpyl%JzmXuWIu0GF5dQMbFZQ*ut*o7ZyF^1|88_@UwG z*%TT!c0x5_DNq{Iy9l)ZE(Lkdv`*|!UQ+}G5?#IpIS-rU@!7?KC2Vl%DO()t51e!s%`Uw6$cb8Isae+!&>KiR!0ta-Od?qHiY2D$_AsFED6cG^- zE-oBk`r^G^dHpP=(lJ-(_z~aOfU=@`x&`}#`_PAh&j`4q;Bp`EUIM;=s>^E!kfo*m zvVnFgXImT)xLp7SFo03JTCz};-7Y=eY^%OB$8v}qEnHyKd~d?)96;g z8lONeNM8l<4}3WDnmGD6L>lon2hAlvXQEM;bj)eS5nY z!F3hGty10TcNR4WK+lcO^g@r+L*@Zc)_yAi@mi3hQHDu7skgMjg~Xl~6W!=hYuJoUqwz|vACdJkCVc>3$>D;v-M9k|)2q0$r9HRQBTjMqwa zqUUN{XiwarJ33mlSJM(g`#3QSml+!RfC9-{$^{DLzk=2oUm&>P>JI3J33GVXHMmzJ@`s5*1uR z8Qqn7G}5fgG1?>f6Y=-czmP$mEz%#%2*=kgxiHD&uh(l#0Rf?dxV((dm!$sNX`#|twY0EQ zZTOhokU>`GLKA+;KHI-k#Oa;vu%rc84`m_YRFD(x%+CF9IzK&JX=u#bf-Wh=%smi? zlsFFG%&pVJUSfUNZ;%F@z~P?_FaAaR`^nFlnW-fuTGsuA&#Th)bT~}ryvIvD-ql;t z#(by**2^(JOF6gY=+2IA`oO>#6+%lJn56aiF%u|;@$rdi*xKIyUSufL@If;PJ}Q#< z29&Ix78`}rfl`a~z@7`R?^^-_(fdH z&mEMNsXuuxT?@FkP~hteD5Y`e^JC*u4uIC47`cLdUkjqOiv7(vlg&d_IPi}BF?PpX zyjskC$wE_6Q+86{3ym*L2SjqbN<2LSa75ybPJVlzk43dagVNtSp{K-3k@fVS$ttzF zqe}p|!28!;zaz}_80)o5$TOndq>H|e!C?M-7}O8_RSF76nVO$q2Fsu%n-k+9ioFAJ zU{Z!-rB)k8^(UdmbA+J`Y<&=j7($qZqN1VztZ2e%xddE&wfCQu@pQFg#>X#Vbmqrc zddQ@!y;l$!qHXO_l9ZZEMM#vqgvb7C6dzm5(b@i=>bHv_0mxr36Hn)z2OI&s?_}Hi zj4J7_ZH&v!^JxGkRZ$5pe>Z3Mdu6>7tc=g>!kxYcy@FDO?{xZ1WQXu1kf~EPA6ZS~ znV}&rj2w*1m$;1rPu7Yd&PtJCXrpZ8xl_Df*2G5UDx98OHq7{JJHgFtR8V0|22{6U z#*J(lN|EtK9;e59V%a|oefB>Vm_FS3fHiu0G~-BrqMF$W(6HKidi+9+TqOKbQddBZ zhp}jQWF+ABg|SL4@pB=70|14)las}St?$q6uQfUhy{MYc&HmI%(9XyloAI85SXC@O;Plu}FR03x7QqGLWH~o3KiWlkKO6BorI8 zWQDU@#J?`BJvNHc$G_~$q?(!11K6X4|2CbXuk!%z?Xc$k#mBShG@|b$ZN^k5W^KMa z+nN~kVgW}iMxFZ~wfj$8!bEui?WDCm_-aZ#7I2~4W^7VHaOZH?&A=x67PVCF+yu^S z^S`q{#WW_eA8yvp5Er*jQ>??Bngw%`H&g}88AnFL0Y*$GqJozf zI=x0{?rl1x0D1918%2Osp-nlz2bg)hymfN+5<7L%*6RXo*cXQ-FO*eP{iaiy*4IDh z*Rd`Sd{R_UAk&bPm^%2-_W&rAJ@Dzq1RmD7_n~8IF=)y7ZY>vFsRK3r)<^pOvhey34 zYBHoFk6A(~XcU6DrTHjIMMZJJ;flMd4|J{@0uBHXc$r(upjTwj0!~T}e||_V>Zw?1 zq(r>gS-Q4w^>@Z&p7zWYnjpp2-7DagM52bY2GsI-`=(6_04V4V zQB-c8`#3*XT<-@eegb=9VqzucTc!dP>x(O+`EE{o)&_Mq%P)38L9!5p0uP>;^AgJr zC+CxOJDWK@3UqTDt{N_!yf1^(Q>m!~ePE@DNM%0?$~{4y(5l|O%E4jTs#9A0_h&YW zHicu~-5d5w_iIP~=-Bgo4lx(g?^dr=!95`7@(ZR87=@^yVpPP$XHOLzExw`$js~Q6 z&OQZsH*M&?ua|ae1VBU;6&1WB9b!0@##fn_l!o*21+J0laIm3%Tv<6~qGW(yx)vZm-_Ii9MaEyDA-Gx@Zqg^64C zT|L>Pk^_*U`UhWerd`4B__qM8CnG<sMCF3=zMA(;}SantdZR zgV6+$yGX(CtFlx0Y-?{$pTbVVtSx5n#3*#{ZIt_Jp>p}Ap-TZ2!6_rdDt9&%seXIM zpNih3-=_Fl;|&Dj-uj3b6`KJ8g3socy=c-MwAfT-M%B~Pb5R+2*#pqh!U_st3Y{@J zzxKwAy8t@zs@Ti~s0NYA0qo127p>%vmxK8-9)`EFyH5Ek-fR@1k~PXb?^zkOGzj;s z<`=vn>aHauy)SeyyzO*fE}-2w{Ho2?ua4>^*4aCzx6tvoHrOo=t$DMhY)>L$; z*hW_)b00TLcgFB4c!~x>&6w={GWei z4y1=1Nq-uJG}^SIYKzP!s+z(9GLu!5O>J`VSCC}9pWChdHP^pk*sFx~{Q&5-t#d8%lH3WD*%$KcV7u)xessSNw21HEtvO zQKAG*)wD}eS=)>Bp<6^{3(%p!7ez~vWScV)>;RE0agx7M?D@y0 z+H*q8!0l%sHbO=V0OMZ1vg?dUmKg&4j3^Q*y#7z;U~kWrC+bOX-J@g{0B%3*`cd(4 zHqqrFV|r!YfRtWs0mM+BKv6?$rJ5^AdgtWxAYc+X{4KNSIp1QE{t7DGrR)7tbRv5| zAp@pgMs8g7^6$tIlAB>|;tQ3G>w5dl_oraT9RIfZY|sjTQ~@IK9*|VW93D!ou*j4H5PmKndpZ&1APJ{D&@}`N>aJIW@`X-`1Wt*q}!h05QN3`c2%@BriRLNXQ_YFy#dRv?CkaHgM7)M94xUfMj)Sp}6v1tWg`02K^xe#}Ca!>_^a-bI4#L z1~ivupR=%WwDo-KkaJD)8QU z^xEFK4EN0y7S|`7{l4wq2P6<(NeSOk^3*f`27HOSE zx^MO^^8pgd{Fcdnfqqt)eZ{&K^-qVZKO1Fv`S?f>oEpQ>jm@NAAn)T}ael%Nmysz< zMupUoVmcz9%j9AV3<)1T^fC&o-*060$yHJZ5yf15lKs+GH1hN^H90h=K*II}xRCiy zRy%oX>%3P^_~yO6eN+G0NKkUk8y}pFiOI5XZ!j`D(EfqT2R`lsnq``oYo^?^NMGE_ z^k-{erT$BVxmAk^)M9Zl>(oS3UA08@XiPu*Xv0(UB!v2yRa=HRHx32CHTWYg}pn z`tFv{?T+uRv%1$Z?m>5NYh7!;AgWEEkPgjK#uY+flk#4RaeAhuUH$;rlb^+l&IH+x zxp2&?`t%H60D!HsT-e@;j)^h-ac}p7GOu54E&Pd|UXthA)uRAOI&mTEx0AZq5`_9? zIOOtz+oz~zw9xVhhJ9kpD70p0NkC>Oq??AghFiys#80*27czMl7MJH&3Sg7Aj!v`! zd@gI_zTe8k^7rFXWG-zYE$~%37#r%8jM-QQwrieZ8k=$kUc|i@+_mV9|IJZxW0u@E|H)D{*g{2J*A2`yk~;Za(nW%i z+^8n6`*)jmA}pM;K7Zx{@xm9&0VJt$#zrP-=)6+50JF^K1a~DG4{Q{MWjKQ_kZvQ3AL?NHweJ{jPLGGj?at)$X4Wk zA5>F4$tW9eOy?`7EmjI#RhS7@BM)@Ok5olHtBvFK&_C!aEGe4n&?m&$W#w#`~x^q-Gg zeRGxZ06g{G+M@Xop(6RE@>lQ0-iS5DWWTQhAWyf@(4n!hY*-se21Wst%B#oOfG?<^ zPcM#faY@h3rI`fB5|eW;W~FlaV5r-;@oJqWhMw895>I>K0=D;4dorXmX8Bvb~-p>2!yY&fW1;S_9t4JO(04+0`HZ~!!$s&)j}I~eUJQ`IM` z5)u+73W|ZYGmF-V*wC)9mg-V7QM)x%7`qHho2@&U8AznoosOnh{|BqglovJWhn80EuZMx=lp1^qk;?y z!8EUih7e_rz(RNkA|;~J&kz@rJ$;>(o&Ef{W^HY)T?wGL-X3mddxJfn?LgAKiTZWk zMN4mm9^Iv6uGdcBc91*gKNOwP6JJ8+^7cWDLtJoxY4`161oD|X9D9i)BKChy92|)N z3=;3mo(NS|2SDZzFPk%C-K=_9V&h{|Cl}1j!huK@u_IgP2+tQ!zx7DVG0N@Pxh94n;85(Vm|a_FV>Qk=*HDpl(^|KdKZl z7wcZ6r99x+6oPd9yHHGk-lsXpr+=XY+ZCs#^z7nXvj-032W&HiVd7^Nr6B|I$0 z`nb#)eiEYa8xAL1F^}Ac&VCY9A_OiRo}QINp@8Ni1A`}`M~0P^_3x7|;uYz`)whd` zw{(b2N@TV4bPC`B$))%~1+u3Q0+70ZN;86fEMitjsTOAlIN;#iiy@^$HTQQ zJfVYD#nAIJ>Y#`AO@JP>(LlPOvOzYSmLXbgME85#f-1{!>L!h6RA#K$^$s*d8;Aqr z3^=b-JT2*?iVTz`*Tpneacu(eOC&Oae@jOfKb8u|zAMEC-4bSOq-u5~j)iRvDRHAo zhrRVTilFes$k+=dFPjdgEc&sEZ%}^TTPodU2RKVD$&ZN0w`}rXhPO z;OQ=hhr~fzfleWVv^)~=JPjU@1Uh=6yJ5@N45XYNAkAGF-D<^9yG1K(cC8306e{ws zq)!I1!33)Ad(vj=3e>I*e2>{XcA?&4C642@T958OW9)KZWvQh>A_#*%3wkO@r19R& z*TxKQZT8l16yfIP-_sX#KZqi@xV4A=sEP6M@oabyYbpMb4g+ik;*0rrlR)yB9BP(? zoPnQDVa`my@$2NxWK1}m&&*3?({l{9lv!HRONGJ%GWjzV;^-F1KjqAzb{ki&+I1qN z^igW=)DRJN&UDni_FK&b*F1%4cG_ukNlE*UQBWYe>W@=qra9^@>nzh4m7anHd=Tw7=d_&qHpZCzm0!M~TmBCt59S{b{!m);Q& zYv5Lh@QTQ0Y`#aF!;35h1wu6%Wd^V=erHAAa%M<%oIh@g_W~EN!BLYmA)RZxb}UgM z|N6P=+eB#{`UV%z4Ra23r&y{86|F28ON4g+)UsI(PYi__k6B0db|qnh+HF)rw;0o0USE)ezI+ds_2!Ky+1m? zNQiw6sNswg>|t_wa7s^Xpq==&gsd})=BNKIrK#`+e9~t3YsBSG+1?Uy9eT^vLHX|r zGK7%c^W&S&s5Bw;Fu+bBu7Z!*=L9%5B+{xR-oj23=A0Bl7)l|b22#t&UFim~>kUd6 zYPdK{?IT^igg-G*P2$|{UOVdQIsRrjDnRH%5$ zzMtKY6UaP_BX+OTV>~kE5ebHC)rF|4k#E@GgU|u%*S^R!{zNxrCuHTSvmLnmI~~=u zSG^<--dqg1oQlYTD6}q6(S%E})RGgmT%ihS;y(OBI!|^8z?I-3{957QhSoE8$y5Y`*+15w*VO4i;T% z%Y3EemF~eUHxd}p(%Do~VOp!tbKsP8bSZd4dHETdR1h(8qcclxmgr!puLJi>d+5(6 z!@C(voCV~suOZiGT}B075>N!;G`{_N)TzKwxzVv^4N**;y{$b>eRP zQ2{ZIgB$2H+dT zz_nkHxri=qCI1fhW!- zzFBBBapYx}xXL|Fyv|jCz#iMZ-sujYJ!@2%{Y-u-ZvcS|QPDTnH%{a^Qkbnc3|KeS zQO#A#=p{kwT5)lI&ZI0b`B2A7JH05lqIQ?$&=|Z@Wp9sNrG zS=Ux;ln>lwZkkf7J^1^8rFj`=AZc)APH!C~!G)U<{M_7V5*9cELo{s(J{WAQK7krY z3vpAhgLiTsH-Z@c+@|jAQ28w(T#$mPPPS77!Ih4P+6*#P9zA3P}#I literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..904e794b6c5b64971f206385b2cc4148b5785d14 GIT binary patch literal 32768 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 + + + + 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