21 Commits

Author SHA1 Message Date
ac0d4fec8a wip: navigation system - NavItemWidgets 2023-11-23 00:42:34 +08:00
7821816d09 wip: navigation components 2023-11-18 23:05:10 +08:00
a930cffe39 wip: navigating system 2023-11-07 22:04:31 +08:00
4442e496aa feat: SlidingStackedWidget 2023-10-30 01:55:16 +08:00
1ec302d98c Merge branch 'master' of github.com:283375/arcaea-offline-pyside-ui 2023-10-29 17:13:52 +08:00
3d6e5f997e pre-commit 2023-10-29 00:12:01 +08:00
495f6dc424 impr: TabDb_RemoveDuplicateScores chart selecting 2023-10-25 20:04:23 +08:00
0b599e3d9c fix: ensure database reset works 2023-10-25 18:58:13 +08:00
a51a67fae3 impr: minor improvements 2023-10-25 17:53:21 +08:00
b48e177ae8 feat: TabDb_RemoveDuplicateScores 2023-10-25 17:41:40 +08:00
865fc8b7c8 style: isort & black ignore files 2023-10-23 23:51:29 +08:00
1eeec6f745 wip: TabDb_RemoveDuplicateScores ui 2023-10-23 23:51:11 +08:00
8558f5e403 impr: handle exceptions in TabOverview 2023-10-23 16:18:35 +08:00
1a37310091 impr: TabTools_Andreal source code link 2023-10-23 16:15:26 +08:00
d460e935b4 fix: DbB30TableModel 2023-10-23 16:08:51 +08:00
38d2e4ad5a fix: translation file extraction script 2023-10-23 16:08:11 +08:00
28599cfb04 feat: DatabaseChecker re-init database button 2023-10-23 15:31:56 +08:00
1d01356327 impr: popup PlayRatingCalculator when double clicking an item in TabTools_ChartRecommend 2023-10-23 15:19:56 +08:00
738975a83d chore: translations 2023-10-23 14:39:46 +08:00
5adea908f9 impr: PlayRatingCalculator ui 2023-10-23 14:39:36 +08:00
21ca1018db impr: translation file extraction script 2023-10-23 14:39:11 +08:00
40 changed files with 2347 additions and 251 deletions

14
.pre-commit-config.yaml Normal file
View File

@ -0,0 +1,14 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/psf/black
rev: 23.1.0
hooks:
- id: black
- repo: https://github.com/PyCQA/isort
rev: 5.12.0
hooks:
- id: isort

View File

@ -68,7 +68,7 @@ if __name__ == "__main__":
databaseChecker = DatabaseChecker()
databaseChecker.setWindowIcon(QIcon(":/images/icon.png"))
databaseCheckResult = databaseChecker.confirmDb()
databaseCheckResult = databaseChecker.confirmDb() if Settings().databaseUrl() else 0
if not databaseCheckResult & DatabaseCheckerResult.Initted:
result = databaseChecker.exec()

View File

@ -28,6 +28,7 @@ classifiers = [
force-exclude = '''
(
ui/designer
| .*_ui.py
| .*_rc.py
)
'''
@ -35,7 +36,7 @@ force-exclude = '''
[tool.isort]
profile = "black"
extend_skip = ["ui/designer"]
extend_skip_glob = ["*_rc.py"]
extend_skip_glob = ["*_ui.py", "*_rc.py"]
[tool.pyright]
ignore = ["**/__debug*.*"]

View File

@ -2,3 +2,4 @@ black == 23.7.0
isort == 5.12.0
imageio==2.31.4
Nuitka==1.8.4
pytest==7.4.3

View File

@ -2,3 +2,4 @@ arcaea-offline==0.1.0
arcaea-offline-ocr==0.1.0
exif==1.6.0
PySide6==6.5.2
typing-extensions==4.8.0

0
tests/__init__.py Normal file
View File

0
tests/ui/__init__.py Normal file
View File

View File

View File

@ -0,0 +1,31 @@
from ui.navigation.navhost import NavHost
from ui.navigation.navitem import NavItem
class TestNavHost:
def test_auto_append_parent(self):
navHost = NavHost()
navHost.registerNavItem(NavItem(id="aaa.bbb.ccc.ddd"))
navItems = navHost.navItems
assert NavItem(id="aaa.bbb.ccc.ddd") in navItems
assert NavItem(id="aaa.bbb.ccc") in navItems
assert NavItem(id="aaa.bbb") in navItems
assert NavItem(id="aaa") in navItems
def test_auto_select_child(self):
navHost = NavHost()
navHost.registerNavItem(NavItem(id="aaa"))
navHost.registerNavItem(NavItem(id="bbb"))
assert navHost.currentNavItem.id == "aaa"
navHost.registerNavItem(NavItem(id="aaa.bbb"))
navHost.registerNavItem(NavItem(id="aaa.ccc"))
navHost.navigate("aaa")
assert navHost.currentNavItem.id == "aaa.bbb"

View File

View File

View File

@ -0,0 +1,47 @@
from PySide6.QtWidgets import QWidget
from ui.navigation.navhost import NavHost, navHost
from ui.navigation.navitem import NavItem
from ui.navigation.navitemwidgets import NavItemWidgets
from ui.widgets.slidingstackedwidget import SlidingStackedWidget
class AnimatedStackedNavItemsWidgets(SlidingStackedWidget):
def __init__(
self, navItemWidgets: NavItemWidgets, navHost: NavHost = navHost, parent=None
):
super().__init__(parent)
self.navItemWidgets = navItemWidgets
self.navHost = navHost
self.navHost.activated.connect(self.__switchTo)
self.animationFinished.connect(self.endChangingWidget)
def __switchTo(self, oldNavItem: NavItem, newNavItem: NavItem):
oldNavItemDepth = self.navHost.getNavItemDepth(oldNavItem.id)
newNavItemDepth = self.navHost.getNavItemDepth(newNavItem.id)
if oldNavItemDepth != newNavItemDepth:
slidingDirection = (
self.slidingDirection.RightToLeft
if newNavItemDepth > oldNavItemDepth
else self.slidingDirection.LeftToRight
)
else:
slidingDirection = self.slidingDirection.TopToBottom
newWidget = self.navItemWidgets.get(newNavItem.id) or QWidget()
self.startChangingWidget(newWidget, slidingDirection)
def startChangingWidget(self, newWidget: QWidget, slidingDirection):
newIndex = self.addWidget(newWidget)
[self.widget(i).setEnabled(False) for i in range(self.count())]
self.slideInIdx(newIndex, slidingDirection)
def endChangingWidget(self):
oldWidget = self.widget(0)
self.removeWidget(oldWidget)
newWidget = self.widget(0)
newWidget.setEnabled(True)

View File

@ -0,0 +1,286 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>TabDb_RemoveDuplicateScores</class>
<widget class="QWidget" name="TabDb_RemoveDuplicateScores">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>600</width>
<height>500</height>
</rect>
</property>
<property name="windowTitle">
<string notr="true">TabDb_RemoveDuplicateScores</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>scan.title</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QCheckBox" name="scan_option_scoreCheckBox">
<property name="text">
<string>scan.option.score</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="scan_option_pureCheckBox">
<property name="text">
<string notr="true">PURE</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="scan_option_farCheckBox">
<property name="text">
<string notr="true">FAR</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="scan_option_lostCheckBox">
<property name="text">
<string notr="true">LOST</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="scan_option_maxRecallCheckBox">
<property name="text">
<string notr="true">MAX RECALL</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QCheckBox" name="scan_option_dateCheckBox">
<property name="text">
<string>scan.option.date</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="scan_option_modifierCheckBox">
<property name="text">
<string>scan.option.modifier</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="scan_option_clearTypeCheckBox">
<property name="text">
<string>scan.option.clearType</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QPushButton" name="scan_scanButton">
<property name="text">
<string>scan.scanButton</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QTreeView" name="treeView">
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::NoSelection</enum>
</property>
<property name="verticalScrollMode">
<enum>QAbstractItemView::ScrollPerPixel</enum>
</property>
<property name="headerHidden">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_6">
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>quickSelect.title</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>quickSelect.description</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="quickSelect_comboBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="quickSelect_selectButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>quickSelect.selectButton</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_3">
<property name="title">
<string/>
</property>
<layout class="QVBoxLayout" name="verticalLayout_5">
<item>
<widget class="QPushButton" name="deselectAllButton">
<property name="text">
<string>deselectAllButton</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="reverseSelectionButton">
<property name="text">
<string>reverseSelectionButton</string>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_3">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="collapseAllButton">
<property name="text">
<string>collapseAllButton</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="expandAllButton">
<property name="text">
<string>expandAllButton</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="resetModelButton">
<property name="text">
<string>resetModelButton</string>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="deleteSelectionButton">
<property name="font">
<font>
<bold>true</bold>
</font>
</property>
<property name="styleSheet">
<string notr="true">QPushButton { color: red };</string>
</property>
<property name="text">
<string>deleteSelectionButton</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</widget>
<tabstops>
<tabstop>scan_option_scoreCheckBox</tabstop>
<tabstop>scan_option_pureCheckBox</tabstop>
<tabstop>scan_option_farCheckBox</tabstop>
<tabstop>scan_option_lostCheckBox</tabstop>
<tabstop>scan_option_maxRecallCheckBox</tabstop>
<tabstop>scan_option_dateCheckBox</tabstop>
<tabstop>scan_option_modifierCheckBox</tabstop>
<tabstop>scan_option_clearTypeCheckBox</tabstop>
<tabstop>scan_scanButton</tabstop>
<tabstop>treeView</tabstop>
<tabstop>quickSelect_comboBox</tabstop>
<tabstop>quickSelect_selectButton</tabstop>
<tabstop>deselectAllButton</tabstop>
<tabstop>reverseSelectionButton</tabstop>
<tabstop>collapseAllButton</tabstop>
<tabstop>expandAllButton</tabstop>
<tabstop>resetModelButton</tabstop>
<tabstop>deleteSelectionButton</tabstop>
</tabstops>
<resources/>
<connections/>
</ui>

View File

@ -0,0 +1,245 @@
# -*- coding: utf-8 -*-
################################################################################
## Form generated from reading UI file 'tabDb_RemoveDuplicateScores.ui'
##
## Created by: Qt User Interface Compiler version 6.5.2
##
## 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,
QGroupBox, QHBoxLayout, QHeaderView, QLabel,
QPushButton, QSizePolicy, QSpacerItem, QTreeView,
QVBoxLayout, QWidget)
class Ui_TabDb_RemoveDuplicateScores(object):
def setupUi(self, TabDb_RemoveDuplicateScores):
if not TabDb_RemoveDuplicateScores.objectName():
TabDb_RemoveDuplicateScores.setObjectName(u"TabDb_RemoveDuplicateScores")
TabDb_RemoveDuplicateScores.resize(600, 500)
TabDb_RemoveDuplicateScores.setWindowTitle(u"TabDb_RemoveDuplicateScores")
self.verticalLayout_2 = QVBoxLayout(TabDb_RemoveDuplicateScores)
self.verticalLayout_2.setObjectName(u"verticalLayout_2")
self.groupBox_2 = QGroupBox(TabDb_RemoveDuplicateScores)
self.groupBox_2.setObjectName(u"groupBox_2")
self.verticalLayout = QVBoxLayout(self.groupBox_2)
self.verticalLayout.setObjectName(u"verticalLayout")
self.verticalLayout_4 = QVBoxLayout()
self.verticalLayout_4.setObjectName(u"verticalLayout_4")
self.horizontalLayout_2 = QHBoxLayout()
self.horizontalLayout_2.setObjectName(u"horizontalLayout_2")
self.scan_option_scoreCheckBox = QCheckBox(self.groupBox_2)
self.scan_option_scoreCheckBox.setObjectName(u"scan_option_scoreCheckBox")
self.horizontalLayout_2.addWidget(self.scan_option_scoreCheckBox)
self.scan_option_pureCheckBox = QCheckBox(self.groupBox_2)
self.scan_option_pureCheckBox.setObjectName(u"scan_option_pureCheckBox")
self.scan_option_pureCheckBox.setText(u"PURE")
self.horizontalLayout_2.addWidget(self.scan_option_pureCheckBox)
self.scan_option_farCheckBox = QCheckBox(self.groupBox_2)
self.scan_option_farCheckBox.setObjectName(u"scan_option_farCheckBox")
self.scan_option_farCheckBox.setText(u"FAR")
self.horizontalLayout_2.addWidget(self.scan_option_farCheckBox)
self.scan_option_lostCheckBox = QCheckBox(self.groupBox_2)
self.scan_option_lostCheckBox.setObjectName(u"scan_option_lostCheckBox")
self.scan_option_lostCheckBox.setText(u"LOST")
self.horizontalLayout_2.addWidget(self.scan_option_lostCheckBox)
self.scan_option_maxRecallCheckBox = QCheckBox(self.groupBox_2)
self.scan_option_maxRecallCheckBox.setObjectName(u"scan_option_maxRecallCheckBox")
self.scan_option_maxRecallCheckBox.setText(u"MAX RECALL")
self.horizontalLayout_2.addWidget(self.scan_option_maxRecallCheckBox)
self.verticalLayout_4.addLayout(self.horizontalLayout_2)
self.verticalLayout.addLayout(self.verticalLayout_4)
self.horizontalLayout_3 = QHBoxLayout()
self.horizontalLayout_3.setObjectName(u"horizontalLayout_3")
self.scan_option_dateCheckBox = QCheckBox(self.groupBox_2)
self.scan_option_dateCheckBox.setObjectName(u"scan_option_dateCheckBox")
self.horizontalLayout_3.addWidget(self.scan_option_dateCheckBox)
self.scan_option_modifierCheckBox = QCheckBox(self.groupBox_2)
self.scan_option_modifierCheckBox.setObjectName(u"scan_option_modifierCheckBox")
self.horizontalLayout_3.addWidget(self.scan_option_modifierCheckBox)
self.scan_option_clearTypeCheckBox = QCheckBox(self.groupBox_2)
self.scan_option_clearTypeCheckBox.setObjectName(u"scan_option_clearTypeCheckBox")
self.horizontalLayout_3.addWidget(self.scan_option_clearTypeCheckBox)
self.verticalLayout.addLayout(self.horizontalLayout_3)
self.scan_scanButton = QPushButton(self.groupBox_2)
self.scan_scanButton.setObjectName(u"scan_scanButton")
self.verticalLayout.addWidget(self.scan_scanButton)
self.verticalLayout_2.addWidget(self.groupBox_2)
self.horizontalLayout = QHBoxLayout()
self.horizontalLayout.setObjectName(u"horizontalLayout")
self.treeView = QTreeView(TabDb_RemoveDuplicateScores)
self.treeView.setObjectName(u"treeView")
self.treeView.setEditTriggers(QAbstractItemView.NoEditTriggers)
self.treeView.setSelectionMode(QAbstractItemView.NoSelection)
self.treeView.setVerticalScrollMode(QAbstractItemView.ScrollPerPixel)
self.treeView.setHeaderHidden(True)
self.horizontalLayout.addWidget(self.treeView)
self.verticalLayout_6 = QVBoxLayout()
self.verticalLayout_6.setObjectName(u"verticalLayout_6")
self.groupBox = QGroupBox(TabDb_RemoveDuplicateScores)
self.groupBox.setObjectName(u"groupBox")
self.verticalLayout_3 = QVBoxLayout(self.groupBox)
self.verticalLayout_3.setObjectName(u"verticalLayout_3")
self.label = QLabel(self.groupBox)
self.label.setObjectName(u"label")
self.verticalLayout_3.addWidget(self.label)
self.quickSelect_comboBox = QComboBox(self.groupBox)
self.quickSelect_comboBox.setObjectName(u"quickSelect_comboBox")
sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.quickSelect_comboBox.sizePolicy().hasHeightForWidth())
self.quickSelect_comboBox.setSizePolicy(sizePolicy)
self.verticalLayout_3.addWidget(self.quickSelect_comboBox)
self.verticalSpacer_2 = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding)
self.verticalLayout_3.addItem(self.verticalSpacer_2)
self.quickSelect_selectButton = QPushButton(self.groupBox)
self.quickSelect_selectButton.setObjectName(u"quickSelect_selectButton")
sizePolicy.setHeightForWidth(self.quickSelect_selectButton.sizePolicy().hasHeightForWidth())
self.quickSelect_selectButton.setSizePolicy(sizePolicy)
self.verticalLayout_3.addWidget(self.quickSelect_selectButton)
self.verticalLayout_6.addWidget(self.groupBox)
self.groupBox_3 = QGroupBox(TabDb_RemoveDuplicateScores)
self.groupBox_3.setObjectName(u"groupBox_3")
self.verticalLayout_5 = QVBoxLayout(self.groupBox_3)
self.verticalLayout_5.setObjectName(u"verticalLayout_5")
self.deselectAllButton = QPushButton(self.groupBox_3)
self.deselectAllButton.setObjectName(u"deselectAllButton")
self.verticalLayout_5.addWidget(self.deselectAllButton)
self.reverseSelectionButton = QPushButton(self.groupBox_3)
self.reverseSelectionButton.setObjectName(u"reverseSelectionButton")
self.verticalLayout_5.addWidget(self.reverseSelectionButton)
self.verticalSpacer_3 = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding)
self.verticalLayout_5.addItem(self.verticalSpacer_3)
self.collapseAllButton = QPushButton(self.groupBox_3)
self.collapseAllButton.setObjectName(u"collapseAllButton")
self.verticalLayout_5.addWidget(self.collapseAllButton)
self.expandAllButton = QPushButton(self.groupBox_3)
self.expandAllButton.setObjectName(u"expandAllButton")
self.verticalLayout_5.addWidget(self.expandAllButton)
self.resetModelButton = QPushButton(self.groupBox_3)
self.resetModelButton.setObjectName(u"resetModelButton")
self.verticalLayout_5.addWidget(self.resetModelButton)
self.verticalSpacer = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding)
self.verticalLayout_5.addItem(self.verticalSpacer)
self.deleteSelectionButton = QPushButton(self.groupBox_3)
self.deleteSelectionButton.setObjectName(u"deleteSelectionButton")
font = QFont()
font.setBold(True)
self.deleteSelectionButton.setFont(font)
self.deleteSelectionButton.setStyleSheet(u"QPushButton { color: red };")
self.verticalLayout_5.addWidget(self.deleteSelectionButton)
self.verticalLayout_6.addWidget(self.groupBox_3)
self.horizontalLayout.addLayout(self.verticalLayout_6)
self.verticalLayout_2.addLayout(self.horizontalLayout)
QWidget.setTabOrder(self.scan_option_scoreCheckBox, self.scan_option_pureCheckBox)
QWidget.setTabOrder(self.scan_option_pureCheckBox, self.scan_option_farCheckBox)
QWidget.setTabOrder(self.scan_option_farCheckBox, self.scan_option_lostCheckBox)
QWidget.setTabOrder(self.scan_option_lostCheckBox, self.scan_option_maxRecallCheckBox)
QWidget.setTabOrder(self.scan_option_maxRecallCheckBox, self.scan_option_dateCheckBox)
QWidget.setTabOrder(self.scan_option_dateCheckBox, self.scan_option_modifierCheckBox)
QWidget.setTabOrder(self.scan_option_modifierCheckBox, self.scan_option_clearTypeCheckBox)
QWidget.setTabOrder(self.scan_option_clearTypeCheckBox, self.scan_scanButton)
QWidget.setTabOrder(self.scan_scanButton, self.treeView)
QWidget.setTabOrder(self.treeView, self.quickSelect_comboBox)
QWidget.setTabOrder(self.quickSelect_comboBox, self.quickSelect_selectButton)
QWidget.setTabOrder(self.quickSelect_selectButton, self.deselectAllButton)
QWidget.setTabOrder(self.deselectAllButton, self.reverseSelectionButton)
QWidget.setTabOrder(self.reverseSelectionButton, self.collapseAllButton)
QWidget.setTabOrder(self.collapseAllButton, self.expandAllButton)
QWidget.setTabOrder(self.expandAllButton, self.resetModelButton)
QWidget.setTabOrder(self.resetModelButton, self.deleteSelectionButton)
self.retranslateUi(TabDb_RemoveDuplicateScores)
QMetaObject.connectSlotsByName(TabDb_RemoveDuplicateScores)
# setupUi
def retranslateUi(self, TabDb_RemoveDuplicateScores):
self.groupBox_2.setTitle(QCoreApplication.translate("TabDb_RemoveDuplicateScores", u"scan.title", None))
self.scan_option_scoreCheckBox.setText(QCoreApplication.translate("TabDb_RemoveDuplicateScores", u"scan.option.score", None))
self.scan_option_dateCheckBox.setText(QCoreApplication.translate("TabDb_RemoveDuplicateScores", u"scan.option.date", None))
self.scan_option_modifierCheckBox.setText(QCoreApplication.translate("TabDb_RemoveDuplicateScores", u"scan.option.modifier", None))
self.scan_option_clearTypeCheckBox.setText(QCoreApplication.translate("TabDb_RemoveDuplicateScores", u"scan.option.clearType", None))
self.scan_scanButton.setText(QCoreApplication.translate("TabDb_RemoveDuplicateScores", u"scan.scanButton", None))
self.groupBox.setTitle(QCoreApplication.translate("TabDb_RemoveDuplicateScores", u"quickSelect.title", None))
self.label.setText(QCoreApplication.translate("TabDb_RemoveDuplicateScores", u"quickSelect.description", None))
self.quickSelect_selectButton.setText(QCoreApplication.translate("TabDb_RemoveDuplicateScores", u"quickSelect.selectButton", None))
self.groupBox_3.setTitle("")
self.deselectAllButton.setText(QCoreApplication.translate("TabDb_RemoveDuplicateScores", u"deselectAllButton", None))
self.reverseSelectionButton.setText(QCoreApplication.translate("TabDb_RemoveDuplicateScores", u"reverseSelectionButton", None))
self.collapseAllButton.setText(QCoreApplication.translate("TabDb_RemoveDuplicateScores", u"collapseAllButton", None))
self.expandAllButton.setText(QCoreApplication.translate("TabDb_RemoveDuplicateScores", u"expandAllButton", None))
self.resetModelButton.setText(QCoreApplication.translate("TabDb_RemoveDuplicateScores", u"resetModelButton", None))
self.deleteSelectionButton.setText(QCoreApplication.translate("TabDb_RemoveDuplicateScores", u"deleteSelectionButton", None))
pass
# retranslateUi

View File

@ -29,6 +29,11 @@
<string>tab.chartInfoEditor</string>
</attribute>
</widget>
<widget class="TabDb_RemoveDuplicateScores" name="tab_removeDuplicateScores">
<attribute name="title">
<string>tab.removeDuplicateScores</string>
</attribute>
</widget>
</widget>
</item>
</layout>
@ -46,6 +51,12 @@
<header>ui.implements.tabs.tabDb.tabDb_ChartInfoEditor</header>
<container>1</container>
</customwidget>
<customwidget>
<class>TabDb_RemoveDuplicateScores</class>
<extends>QWidget</extends>
<header>ui.implements.tabs.tabDb.tabDb_RemoveDuplicateScores</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections/>

View File

@ -20,6 +20,7 @@ from PySide6.QtWidgets import (QApplication, QSizePolicy, QTabWidget, QVBoxLayou
from ui.implements.tabs.tabDb.tabDb_ChartInfoEditor import TabDb_ChartInfoEditor
from ui.implements.tabs.tabDb.tabDb_Manage import TabDb_Manage
from ui.implements.tabs.tabDb.tabDb_RemoveDuplicateScores import TabDb_RemoveDuplicateScores
class Ui_TabDbEntry(object):
def setupUi(self, TabDbEntry):
@ -37,6 +38,9 @@ class Ui_TabDbEntry(object):
self.tab_chartInfoEditor = TabDb_ChartInfoEditor()
self.tab_chartInfoEditor.setObjectName(u"tab_chartInfoEditor")
self.tabWidget.addTab(self.tab_chartInfoEditor, "")
self.tab_removeDuplicateScores = TabDb_RemoveDuplicateScores()
self.tab_removeDuplicateScores.setObjectName(u"tab_removeDuplicateScores")
self.tabWidget.addTab(self.tab_removeDuplicateScores, "")
self.verticalLayout.addWidget(self.tabWidget)
@ -52,6 +56,7 @@ class Ui_TabDbEntry(object):
def retranslateUi(self, TabDbEntry):
self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_manage), QCoreApplication.translate("TabDbEntry", u"tab.manage", None))
self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_chartInfoEditor), QCoreApplication.translate("TabDbEntry", u"tab.chartInfoEditor", None))
self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_removeDuplicateScores), QCoreApplication.translate("TabDbEntry", u"tab.removeDuplicateScores", None))
pass
# retranslateUi

View File

@ -329,6 +329,36 @@
</item>
</layout>
</item>
<item row="9" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>sourceCode</string>
</property>
</widget>
</item>
<item row="9" column="1">
<widget class="QLabel" name="label_7">
<property name="text">
<string notr="true">&lt;a href=&quot;https://github.com/283375/AndrealImageGenerator&quot;&gt;283375/AndrealImageGenerator&lt;/a&gt;&lt;br&gt;(forked from &lt;a href=&quot;https://github.com/Awbugl/AndrealImageGenerator&quot;&gt;Awbugl/AndrealImageGenerator&lt;/a&gt;)</string>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
</widget>
</item>
<item row="8" column="0" colspan="2">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<customwidgets>

View File

@ -220,6 +220,22 @@ class Ui_TabTools_Andreal(object):
self.formLayout.setLayout(7, QFormLayout.SpanningRole, self.horizontalLayout_5)
self.label_4 = QLabel(TabTools_Andreal)
self.label_4.setObjectName(u"label_4")
self.formLayout.setWidget(9, QFormLayout.LabelRole, self.label_4)
self.label_7 = QLabel(TabTools_Andreal)
self.label_7.setObjectName(u"label_7")
self.label_7.setText(u"<a href=\"https://github.com/283375/AndrealImageGenerator\">283375/AndrealImageGenerator</a><br>(forked from <a href=\"https://github.com/Awbugl/AndrealImageGenerator\">Awbugl/AndrealImageGenerator</a>)")
self.label_7.setOpenExternalLinks(True)
self.formLayout.setWidget(9, QFormLayout.FieldRole, self.label_7)
self.verticalSpacer = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding)
self.formLayout.setItem(8, QFormLayout.SpanningRole, self.verticalSpacer)
self.retranslateUi(TabTools_Andreal)
self.imageFormat_jpgRadioButton.toggled.connect(self.jpgQualityHolderWidget.setEnabled)
@ -241,6 +257,7 @@ class Ui_TabTools_Andreal(object):
self.exportJsonButton.setText(QCoreApplication.translate("TabTools_Andreal", u"exportJsonButton", None))
self.generatePreviewButton.setText(QCoreApplication.translate("TabTools_Andreal", u"generatePreviewButton", None))
self.generateImageButton.setText(QCoreApplication.translate("TabTools_Andreal", u"generateImageButton", None))
self.label_4.setText(QCoreApplication.translate("TabTools_Andreal", u"sourceCode", None))
pass
# retranslateUi

View File

@ -82,6 +82,9 @@
<property name="selectionMode">
<enum>QAbstractItemView::NoSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
<property name="verticalScrollMode">
<enum>QAbstractItemView::ScrollPerPixel</enum>
</property>
@ -325,7 +328,7 @@
<set>QAbstractItemView::NoEditTriggers</set>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::SingleSelection</enum>
<enum>QAbstractItemView::NoSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>

View File

@ -68,6 +68,7 @@ class Ui_TabTools_ChartRecommend(object):
self.chartsByConstant_modelView.setMinimumSize(QSize(150, 0))
self.chartsByConstant_modelView.setEditTriggers(QAbstractItemView.NoEditTriggers)
self.chartsByConstant_modelView.setSelectionMode(QAbstractItemView.NoSelection)
self.chartsByConstant_modelView.setSelectionBehavior(QAbstractItemView.SelectRows)
self.chartsByConstant_modelView.setVerticalScrollMode(QAbstractItemView.ScrollPerPixel)
self.chartsByConstant_modelView.setHorizontalScrollMode(QAbstractItemView.ScrollPerPixel)
@ -228,7 +229,7 @@ class Ui_TabTools_ChartRecommend(object):
self.chartsRecommendFromPlayRating_modelView.setSizePolicy(sizePolicy2)
self.chartsRecommendFromPlayRating_modelView.setMinimumSize(QSize(200, 0))
self.chartsRecommendFromPlayRating_modelView.setEditTriggers(QAbstractItemView.NoEditTriggers)
self.chartsRecommendFromPlayRating_modelView.setSelectionMode(QAbstractItemView.SingleSelection)
self.chartsRecommendFromPlayRating_modelView.setSelectionMode(QAbstractItemView.NoSelection)
self.chartsRecommendFromPlayRating_modelView.setSelectionBehavior(QAbstractItemView.SelectRows)
self.chartsRecommendFromPlayRating_modelView.setVerticalScrollMode(QAbstractItemView.ScrollPerPixel)
self.chartsRecommendFromPlayRating_modelView.setHorizontalScrollMode(QAbstractItemView.ScrollPerPixel)

View File

@ -1,6 +1,5 @@
from arcaea_offline.models import Chart, Score, ScoreBest
from PySide6.QtCore import QCoreApplication, QModelIndex, QSortFilterProxyModel, Qt
from sqlalchemy import select
from .base import DbTableModel
@ -18,50 +17,45 @@ class DbB30TableModel(DbTableModel):
def retranslateHeaders(self):
self._horizontalHeaders = [
# fmt: off
QCoreApplication.translate("DB30TableModel", "horizontalHeader.id"),
QCoreApplication.translate("DB30TableModel", "horizontalHeader.chart"),
QCoreApplication.translate("DB30TableModel", "horizontalHeader.score"),
QCoreApplication.translate("DB30TableModel", "horizontalHeader.potential"),
QCoreApplication.translate("DbB30TableModel", "horizontalHeader.id"),
QCoreApplication.translate("DbB30TableModel", "horizontalHeader.chart"),
QCoreApplication.translate("DbB30TableModel", "horizontalHeader.score"),
QCoreApplication.translate("DbB30TableModel", "horizontalHeader.potential"),
# fmt: on
]
def syncDb(self):
self.beginResetModel()
self.beginRemoveRows(QModelIndex(), 0, self.rowCount())
self.__items.clear()
self.endRemoveRows()
self.endResetModel()
with self._db.sessionmaker() as session:
results = list(
session.scalars(
select(ScoreBest).order_by(ScoreBest.potential.desc()).limit(40)
results = (
session.query(ScoreBest, Chart)
.join(
Chart,
(ScoreBest.song_id == Chart.song_id)
& (ScoreBest.rating_class == Chart.rating_class),
)
.order_by(ScoreBest.potential)
.limit(50)
.all()
)
songIds = [r.id for r in results]
ptts = [r.potential for r in results]
for scoreId, ptt in zip(songIds, ptts):
score = self._db.get_score(scoreId)
chart = self._db.get_chart(score.song_id, score.rating_class)
self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount())
self.beginInsertRows(QModelIndex(), 0, len(results) - 1)
for scoreBest, chart in results:
self.__items.append(
{
self.IdRole: score.id,
self.IdRole: scoreBest.id,
self.ChartRole: chart,
self.ScoreRole: score,
self.PttRole: ptt,
self.ScoreRole: scoreBest,
self.PttRole: scoreBest.potential,
}
)
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)
@ -117,30 +111,35 @@ class DbB30TableSortFilterProxyModel(QSortFilterProxyModel):
return super().headerData(section, orientation, role)
return section + 1
def lessThan(self, source_left, source_right) -> bool:
if source_left.column() != source_right.column():
def lessThan(self, sourceLeft: QModelIndex, sourceRight: QModelIndex) -> bool:
if sourceLeft.column() != sourceRight.column():
return
column = source_left.column()
column = sourceLeft.column()
if column == 0:
return source_left.data(DbB30TableModel.IdRole) < source_right.data(
return sourceLeft.data(DbB30TableModel.IdRole) < sourceRight.data(
DbB30TableModel.IdRole
)
elif column == 2:
score_left = source_left.data(DbB30TableModel.ScoreRole)
score_right = source_right.data(DbB30TableModel.ScoreRole)
if isinstance(score_left, Score) and isinstance(score_right, Score):
scoreLeft = sourceLeft.data(DbB30TableModel.ScoreRole)
scoreRight = sourceRight.data(DbB30TableModel.ScoreRole)
if isinstance(scoreLeft, Score) and isinstance(scoreRight, Score):
if self.sortRole() == self.Sort_C2_ScoreRole:
return score_left.score < score_right.score
return scoreLeft.score < scoreRight.score
elif self.sortRole() == self.Sort_C2_TimeRole:
if score_left.date and score_right.date:
return score_left.date < score_right.date
elif score_left.date:
if scoreLeft.date and scoreRight.date:
return scoreLeft.date < scoreRight.date
elif scoreLeft.date:
return False
else:
return True
elif column == 3:
return source_left.data(DbB30TableModel.PttRole) < source_right.data(
DbB30TableModel.PttRole
)
return super().lessThan(source_left, source_right)
pttLeft = sourceLeft.data(DbB30TableModel.PttRole)
pttRight = sourceRight.data(DbB30TableModel.PttRole)
if pttLeft and pttRight:
return pttLeft < pttRight
elif pttLeft:
return False
else:
return True
return super().lessThan(sourceLeft, sourceRight)

View File

@ -39,14 +39,12 @@ class PlayRatingCalculator(QWidget):
return None
score = self.arcaeaScoreLineEdit.score()
if score is None:
return None
return calculate_play_rating(self.constant, score)
return None if score is None else calculate_play_rating(self.constant, score)
def updateResultLabel(self):
result = self.result
self.resultLabel.setText(str(result) if result is not None else "...")
self.resultLabel.setText(str(round(result, 3)) if result is not None else "...")
self.resultLabel.setToolTip(str(result))
def on_copyButton_clicked(self):
result = self.result
@ -68,6 +66,7 @@ class PlayRatingCalculator(QWidget):
self.resultLabel.setSizePolicy(
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred
)
self.resultLabel.setMinimumWidth(100)
self.horizontalLayout.addWidget(self.resultLabel)
self.horizontalSpacer = QSpacerItem(

View File

@ -0,0 +1,353 @@
from enum import IntEnum
from arcaea_offline.database import Database
from arcaea_offline.models import Chart, Difficulty, Score, Song
from PySide6.QtCore import QCoreApplication, QModelIndex, Qt, Slot
from PySide6.QtGui import QStandardItem, QStandardItemModel
from PySide6.QtWidgets import QMessageBox, QStyledItemDelegate, QWidget
from sqlalchemy import delete, func, select
from sqlalchemy.orm import InstrumentedAttribute, Session
from ui.designer.tabs.tabDb.tabDb_RemoveDuplicateScores_ui import (
Ui_TabDb_RemoveDuplicateScores,
)
from ui.extends.shared.delegates.chartDelegate import ChartDelegate
from ui.extends.shared.delegates.scoreDelegate import ScoreDelegate
from ui.extends.shared.language import LanguageChangeEventFilter
class RemoveDuplicateScoresModel(QStandardItemModel):
ScoreRole = Qt.ItemDataRole.UserRole
ChartRole = Qt.ItemDataRole.UserRole + 10
SongRole = Qt.ItemDataRole.UserRole + 11
DifficultyRole = Qt.ItemDataRole.UserRole + 12
def setChartDelegateDatas(
self, item: QStandardItem, songId: str, ratingClass: int, session: Session
):
chart = (
session.query(Chart)
.where((Chart.song_id == songId) & (Chart.rating_class == ratingClass))
.first()
)
song = session.query(Song).where(Song.id == songId).first()
difficulty = (
session.query(Difficulty)
.where(
(Difficulty.song_id == songId)
& (Difficulty.rating_class == ratingClass)
)
.first()
)
if chart is None and song is None and difficulty is None:
chart = Chart(song_id=songId, rating_class=ratingClass, set="unknown")
item.setData(chart, self.ChartRole)
item.setData(song, self.SongRole)
item.setData(difficulty, self.DifficultyRole)
def getGroupKey(self, score: Score, columns: list[InstrumentedAttribute]) -> str:
baseKeys = [score.song_id, str(score.rating_class)]
for column in columns:
key = f"{column.key}{getattr(score,column.key)}"
baseKeys.append(key)
return "||".join(baseKeys)
def setScores(self, scores: list[Score], columns: list[InstrumentedAttribute]):
self.clear()
scoreKeyMap: dict[str, list[Score]] = {}
for score in scores:
key = self.getGroupKey(score, columns)
if scoreKeyMap.get(key) is None:
scoreKeyMap[key] = [score]
else:
scoreKeyMap[key].append(score)
db = Database()
with db.sessionmaker() as session:
for key, scores in scoreKeyMap.items():
songId, ratingClass = key.split("||")[:2]
ratingClass = int(ratingClass)
parentCheckBoxItem = QStandardItem(f"{len(scores)} items")
parentChartItem = QStandardItem()
self.setChartDelegateDatas(
parentChartItem, songId, ratingClass, session
)
for i, score in enumerate(scores):
scoreCheckBoxItem = QStandardItem()
scoreCheckBoxItem.setEditable(False)
scoreCheckBoxItem.setCheckable(True)
scoreCheckBoxItem.setEnabled(True)
scoreItem = QStandardItem()
scoreItem.setData(score, self.ScoreRole)
scoreItem.setEditable(False)
scoreItem.setEnabled(True)
parentCheckBoxItem.setChild(i, 0, scoreCheckBoxItem)
parentCheckBoxItem.setChild(i, 1, scoreItem)
self.appendRow([parentCheckBoxItem, parentChartItem])
class TreeViewChartDelegate(ChartDelegate):
def getChart(self, index: QModelIndex):
return index.data(RemoveDuplicateScoresModel.ChartRole)
def getSong(self, index: QModelIndex):
return index.data(RemoveDuplicateScoresModel.SongRole)
def getDifficulty(self, index: QModelIndex):
return index.data(RemoveDuplicateScoresModel.DifficultyRole)
class TreeViewScoreDelegate(ScoreDelegate):
def getScore(self, index: QModelIndex):
return index.data(RemoveDuplicateScoresModel.ScoreRole)
class TreeViewProxyDelegate(QStyledItemDelegate):
def __init__(
self, chartDelegate: ChartDelegate, scoreDelegate: ScoreDelegate, parent=None
):
super().__init__(parent)
self.chartDelegate = chartDelegate
self.scoreDelegate = scoreDelegate
def delegateForIndex(self, index: QModelIndex) -> QStyledItemDelegate:
return self.scoreDelegate if index.parent().isValid() else self.chartDelegate
def sizeHint(self, option, index: QModelIndex):
return self.delegateForIndex(index).sizeHint(option, index)
def paint(self, painter, option, index: QModelIndex):
self.delegateForIndex(index).paint(painter, option, index)
QStyledItemDelegate.paint(self, painter, option, index)
class QuickSelectComboBoxValues(IntEnum):
ID_EARLIER = 0
DATE_EARLIER = 1
COLUMNS_INTEGRAL = 2
class TabDb_RemoveDuplicateScores(Ui_TabDb_RemoveDuplicateScores, QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setupUi(self)
self.languageChangeEventFilter = LanguageChangeEventFilter(self)
self.installEventFilter(self.languageChangeEventFilter)
self.db = Database()
self.removeDuplicateScoresModel = RemoveDuplicateScoresModel(self)
self.treeView.setModel(self.removeDuplicateScoresModel)
self.treeViewChartDelegate = TreeViewChartDelegate(self.treeView)
self.treeViewScoreDelegate = TreeViewScoreDelegate(self.treeView)
self.treeViewProxyDelegate = TreeViewProxyDelegate(
self.treeViewChartDelegate, self.treeViewScoreDelegate, self.treeView
)
self.treeView.setItemDelegateForColumn(1, self.treeViewProxyDelegate)
self.quickSelect_comboBox.addItem(
# fmt: off
QCoreApplication.translate("TabDb_RemoveDuplicateScores", "quickSelectComboBox.idEarlier"),
# fmt: on
QuickSelectComboBoxValues.ID_EARLIER
)
self.quickSelect_comboBox.addItem(
# fmt: off
QCoreApplication.translate("TabDb_RemoveDuplicateScores", "quickSelectComboBox.dateEarlier"),
# fmt: on
QuickSelectComboBoxValues.DATE_EARLIER
)
self.quickSelect_comboBox.addItem(
# fmt: off
QCoreApplication.translate("TabDb_RemoveDuplicateScores", "quickSelectComboBox.columnsIntegral"),
# fmt: on
QuickSelectComboBoxValues.COLUMNS_INTEGRAL
)
def getQueryColumns(self):
columns: list[InstrumentedAttribute] = [Score.song_id, Score.rating_class]
if self.scan_option_scoreCheckBox.isChecked():
columns.append(Score.score)
if self.scan_option_pureCheckBox.isChecked():
columns.append(Score.pure)
if self.scan_option_farCheckBox.isChecked():
columns.append(Score.far)
if self.scan_option_lostCheckBox.isChecked():
columns.append(Score.lost)
if self.scan_option_maxRecallCheckBox.isChecked():
columns.append(Score.max_recall)
if self.scan_option_dateCheckBox.isChecked():
columns.append(Score.date)
if self.scan_option_modifierCheckBox.isChecked():
columns.append(Score.modifier)
if self.scan_option_clearTypeCheckBox.isChecked():
columns.append(Score.clear_type)
return columns
def getQueryScores(self):
columns = self.getQueryColumns()
with self.db.sessionmaker() as session:
groupBySubquery = (
select(*columns).group_by(*columns).having(func.count() > 1).subquery()
)
selectInClause = [
col == getattr(groupBySubquery.c, col.key) for col in columns
]
return session.query(Score).where(*selectInClause).all()
def scan(self):
scores = self.getQueryScores()
self.removeDuplicateScoresModel.setScores(scores, self.getQueryColumns())
self.treeView.expandAll()
def deselectAll(self):
for row in range(self.removeDuplicateScoresModel.rowCount()):
parentItem = self.removeDuplicateScoresModel.item(row, 0)
for childRow in range(parentItem.rowCount()):
childCheckBoxItem = parentItem.child(childRow, 0)
childCheckBoxItem.setCheckState(Qt.CheckState.Unchecked)
def quickSelect(self):
mode = self.quickSelect_comboBox.currentData()
if mode is None:
return
for row in range(self.removeDuplicateScoresModel.rowCount()):
parentItem = self.removeDuplicateScoresModel.item(row, 0)
scores: list[Score] = []
for childRow in range(parentItem.rowCount()):
childScoreItem = parentItem.child(childRow, 1)
scores.append(childScoreItem.data(RemoveDuplicateScoresModel.ScoreRole))
if mode == QuickSelectComboBoxValues.ID_EARLIER:
chosenRow = min(enumerate(scores), key=lambda i: i[1].id)[0]
elif mode == QuickSelectComboBoxValues.DATE_EARLIER:
chosenRow = min(
enumerate(scores),
key=lambda i: float("inf") if i[1].date is None else i[1].date,
)[0]
elif mode == QuickSelectComboBoxValues.COLUMNS_INTEGRAL:
chosenRow = max(
enumerate(scores),
key=lambda i: sum(
getattr(i[1], col.key) is not None
for col in i[1].__table__.columns
),
)[0]
for childRow in range(parentItem.rowCount()):
childCheckBoxItem = parentItem.child(childRow, 0)
if childRow != chosenRow:
childCheckBoxItem.setCheckState(Qt.CheckState.Checked)
else:
childCheckBoxItem.setCheckState(Qt.CheckState.Unchecked)
def reverseSelection(self):
for row in range(self.removeDuplicateScoresModel.rowCount()):
parentItem = self.removeDuplicateScoresModel.item(row, 0)
# only when there's a checked item in this group, we perform a reversed selection
# otherwise we ignore this group
performReverse = any(
parentItem.child(childRow, 0).checkState() == Qt.CheckState.Checked
for childRow in range(parentItem.rowCount())
)
if not performReverse:
continue
for childRow in range(parentItem.rowCount()):
childCheckBoxItem = parentItem.child(childRow, 0)
newCheckState = (
Qt.CheckState.Unchecked
if childCheckBoxItem.checkState() != Qt.CheckState.Unchecked
else Qt.CheckState.Checked
)
childCheckBoxItem.setCheckState(newCheckState)
def deleteSelection(self):
selectedScores: list[Score] = []
for row in range(self.removeDuplicateScoresModel.rowCount()):
parentItem = self.removeDuplicateScoresModel.item(row, 0)
for childRow in range(parentItem.rowCount()):
childCheckBoxItem = parentItem.child(childRow, 0)
if childCheckBoxItem.checkState() == Qt.CheckState.Checked:
childScoreItem = parentItem.child(childRow, 1)
selectedScores.append(
childScoreItem.data(RemoveDuplicateScoresModel.ScoreRole)
)
confirm = QMessageBox.warning(
self,
None,
# fmt: off
QCoreApplication.translate("TabDb_RemoveDuplicateScores", "deleteSelectionDialog.content {}").format(len(selectedScores)),
# fmt: on
QMessageBox.StandardButton.Yes,
QMessageBox.StandardButton.No,
)
if confirm != QMessageBox.StandardButton.Yes:
return
with self.db.sessionmaker() as session:
ids = [s.id for s in selectedScores]
session.execute(delete(Score).where(Score.id.in_(ids)))
session.commit()
self.scan()
@Slot()
def on_scan_scanButton_clicked(self):
if len(self.getQueryColumns()) <= 2:
result = QMessageBox.warning(
self,
None,
# fmt: off
QCoreApplication.translate("TabDb_RemoveDuplicateScores", "scan_noColumnsDialog.content"),
# fmt: on
QMessageBox.StandardButton.Yes,
QMessageBox.StandardButton.No,
)
if result != QMessageBox.StandardButton.Yes:
return
self.scan()
@Slot()
def on_quickSelect_selectButton_clicked(self):
self.quickSelect()
@Slot()
def on_deselectAllButton_clicked(self):
self.deselectAll()
@Slot()
def on_reverseSelectionButton_clicked(self):
self.reverseSelection()
@Slot()
def on_expandAllButton_clicked(self):
self.treeView.expandAll()
@Slot()
def on_collapseAllButton_clicked(self):
self.treeView.collapseAll()
@Slot()
def on_resetModelButton_clicked(self):
self.removeDuplicateScoresModel.clear()
@Slot()
def on_deleteSelectionButton_clicked(self):
self.deleteSelection()

View File

@ -1,3 +1,5 @@
import logging
from arcaea_offline.database import Database
from PySide6.QtCore import QCoreApplication
from PySide6.QtGui import QShowEvent
@ -6,6 +8,8 @@ from PySide6.QtWidgets import QWidget
from ui.designer.tabs.tabOverview_ui import Ui_TabOverview
from ui.extends.shared.language import LanguageChangeEventFilter
logger = logging.getLogger(__name__)
class TabOverview(Ui_TabOverview, QWidget):
def __init__(self, parent=None):
@ -22,8 +26,14 @@ class TabOverview(Ui_TabOverview, QWidget):
return super().showEvent(event)
def updateOverview(self):
try:
b30 = self.db.get_b30() or 0.00
self.b30Label.setText(str(f"{b30:.3f}"))
except Exception:
logger.exception("Cannot get b30:")
self.b30Label.setText("ERR")
try:
self.databaseDescribeLabel.setText(
self.describeFormatString.format(
self.db.count_packs(),
@ -34,6 +44,9 @@ class TabOverview(Ui_TabOverview, QWidget):
self.db.count_scores(),
)
)
except Exception:
logger.exception("Cannot update overview:")
self.databaseDescribeLabel.setText("ERR")
def retranslateUi(self, *args):
super().retranslateUi(self)

View File

@ -2,31 +2,52 @@ import logging
from arcaea_offline.calculate import calculate_constants_from_play_rating
from arcaea_offline.database import Database
from arcaea_offline.models import Chart, ScoreBest
from arcaea_offline.models import Chart, Score
from arcaea_offline.utils.rating import rating_class_to_text
from arcaea_offline.utils.score import score_to_grade_text
from PySide6.QtCore import Slot
from PySide6.QtWidgets import QWidget
from PySide6.QtCore import QModelIndex, Qt, Slot
from PySide6.QtWidgets import QDialog, QLabel, QVBoxLayout, QWidget
from ui.designer.tabs.tabTools.tabTools_ChartRecommend_ui import (
Ui_TabTools_ChartRecommend,
)
from ui.extends.shared.language import LanguageChangeEventFilter
from ui.extends.tabs.tabTools.tabTools_ChartRecommend import (
ChartsModel,
ChartsWithScoreBestModel,
CustomChartDelegate,
CustomScoreBestDelegate,
)
from ui.implements.components.playRatingCalculator import PlayRatingCalculator
logger = logging.getLogger(__name__)
def chartToText(chart: Chart):
return f"{chart.artist} - {chart.title}<br>({chart.song_id}) {rating_class_to_text(chart.rating_class)}"
class QuickPlayRatingCalculatorDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent)
self.verticalLayout = QVBoxLayout(self)
def scoreBestToText(score: ScoreBest):
return f"{score_to_grade_text(score.score)} {score.score} > {score.potential:.4f}"
self.chartLabel = QLabel(self)
self.verticalLayout.addWidget(self.chartLabel)
self.playRatingCalculator = PlayRatingCalculator(self)
self.verticalLayout.addWidget(self.playRatingCalculator)
self.setMinimumWidth(400)
self.playRatingCalculator.arcaeaScoreLineEdit.setFocus(
Qt.FocusReason.PopupFocusReason
)
def setChart(self, chart: Chart):
self.chartLabel.setText(
f"{chart.title} {rating_class_to_text(chart.rating_class)} {chart.constant / 10}"
)
self.playRatingCalculator.setConstant(chart.constant)
def setScore(self, score: Score):
self.playRatingCalculator.arcaeaScoreLineEdit.setText(str(score))
class TabTools_ChartRecommend(Ui_TabTools_ChartRecommend, QWidget):
@ -34,6 +55,9 @@ class TabTools_ChartRecommend(Ui_TabTools_ChartRecommend, QWidget):
super().__init__(parent)
self.setupUi(self)
self.languageChangeEventFilter = LanguageChangeEventFilter(self)
self.installEventFilter(self.languageChangeEventFilter)
self.db = Database()
self.chartsByConstantModel = ChartsModel(self)
@ -62,6 +86,13 @@ class TabTools_ChartRecommend(Ui_TabTools_ChartRecommend, QWidget):
self.updateChartsRecommendFromPlayRating
)
self.chartsByConstant_modelView.doubleClicked.connect(
self.openQuickPlayRatingCalculator_chartsByConstant
)
self.chartsRecommendFromPlayRating_modelView.doubleClicked.connect(
self.openQuickPlayRatingCalculator_chartsRecommendFromPlayRating
)
@Slot(float)
def on_rangeFromPlayRating_playRatingSpinBox_valueChanged(self, value: float):
try:
@ -120,3 +151,26 @@ class TabTools_ChartRecommend(Ui_TabTools_ChartRecommend, QWidget):
self.chartsRecommendFromPlayRatingModel.setChartAndScore(charts, scores)
self.chartsRecommendFromPlayRating_modelView.resizeRowsToContents()
self.chartsRecommendFromPlayRating_modelView.resizeColumnsToContents()
@Slot(QModelIndex)
def openQuickPlayRatingCalculator_chartsByConstant(self, index: QModelIndex):
dialog = QuickPlayRatingCalculatorDialog(self)
chart = index.data(ChartsModel.ChartRole)
dialog.setChart(chart)
dialog.show()
@Slot(QModelIndex)
def openQuickPlayRatingCalculator_chartsRecommendFromPlayRating(
self, index: QModelIndex
):
dialog = QuickPlayRatingCalculatorDialog(self)
row = index.row()
chartIndex = self.chartsRecommendFromPlayRatingModel.item(row, 0)
scoreIndex = self.chartsRecommendFromPlayRatingModel.item(row, 1)
chart = chartIndex.data(ChartsWithScoreBestModel.ChartRole)
score: Score = scoreIndex.data(ChartsWithScoreBestModel.ScoreBestRole)
dialog.setChart(chart)
dialog.setScore(score.score)
dialog.show()

View File

151
ui/navigation/navhost.py Normal file
View File

@ -0,0 +1,151 @@
import logging
from dataclasses import dataclass
from typing import Optional
from PySide6.QtCore import QObject, Signal
from .navitem import NavItem
logger = logging.getLogger(__name__)
@dataclass
class NavItemRelatives:
parent: Optional[NavItem]
children: list[NavItem]
class NavHost(QObject):
activated = Signal(NavItem, NavItem)
navItemsChanged = Signal()
def __init__(self, parent=None):
super().__init__(parent)
self.__navItems: list[NavItem] = []
self.__cachedNavItems: list[NavItem] = []
self.__currentNavItem: NavItem = None
def __flushCachedNavItems(self):
navItems = set(self.__navItems)
for item in self.__navItems:
parts = item.id.split(".")
for i in range(1, len(parts)):
parentItemId = ".".join(parts[:i])
navItems.add(NavItem(id=parentItemId))
self.__cachedNavItems = list(navItems)
@property
def navItems(self) -> list[NavItem]:
return self.__cachedNavItems
@property
def currentNavItem(self) -> NavItem:
if self.__currentNavItem:
return self.__currentNavItem
if self.__navItems:
self.__currentNavItem = self.__navItems[0]
return self.__currentNavItem
def findNavItem(self, navItemId: str) -> Optional[NavItem]:
navItemIds = [item.id for item in self.__navItems]
try:
index = navItemIds.index(navItemId)
return self.__navItems[index]
except IndexError:
return None
def isNavigatingBack(self, oldNavItemId: str, newNavItemId: str) -> bool:
# sourcery skip: class-extract-method
# | oldNavItemId | newNavItemId | back? |
# |-----------------------|--------------------|-------|
# | database.manage.packs | database | True |
# | ocr.device | ocr | True |
# | database.manage.songs | ocr.b30 | False |
# | database | database | False |
oldNavItemIdSplitted = self.getNavItemIdSplitted(oldNavItemId)
newNavItemIdSplitted = self.getNavItemIdSplitted(newNavItemId)
return self.getNavItemDepth(newNavItemId) < self.getNavItemDepth(
oldNavItemId
) and all((idFrag in oldNavItemIdSplitted) for idFrag in newNavItemIdSplitted)
def isChild(self, childNavItemId: str, parentNavItemId: str) -> bool:
childNavItemIdSplitted = self.getNavItemIdSplitted(childNavItemId)
parentNavItemIdSplitted = self.getNavItemIdSplitted(parentNavItemId)
return all(
(idFrag in childNavItemIdSplitted) for idFrag in parentNavItemIdSplitted
)
def getNavItemIdSplitted(self, navItemId: str) -> list[str]:
return navItemId.split(".")
def getNavItemDepth(self, navItemId: str) -> int:
return len(self.getNavItemIdSplitted(navItemId))
def getNavItemRelatives(self, navItemId: str) -> NavItemRelatives:
parent = None
if "." in navItemId:
navItemIdSplitted = navItemId.split(".")
parentId = navItemIdSplitted[:-1]
parent = self.findNavItem(".".join(parentId))
if not navItemId:
# return root navItems
children = [navItem for navItem in self.__navItems if "." not in navItem.id]
else:
children = [
navItem
for navItem in self.__navItems
if navItem.id.startswith(navItemId)
and navItem.id != navItemId
and self.getNavItemDepth(navItem.id)
== self.getNavItemDepth(navItemId) + 1
]
return NavItemRelatives(parent=parent, children=children)
def registerNavItem(self, item: NavItem):
self.__navItems.append(item)
self.__flushCachedNavItems()
self.navItemsChanged.emit()
def navigate(self, navItemId: str):
oldNavItem = self.__currentNavItem or NavItem("")
newNavItem = self.findNavItem(navItemId)
if newNavItem is None:
raise IndexError(
f"Cannot find '{navItemId}' in {repr(self)}. "
"Maybe try registering it?"
)
# if the navItem have children, navigate to first child
# but if the navItem is going back, e.g. 'database.manage' -> 'database'
# then don't navigate to it's child.
if self.isNavigatingBack(oldNavItem.id, newNavItem.id):
# navItem is going back
currentNavItem = newNavItem
else:
newNavItemRelatives = self.getNavItemRelatives(newNavItem.id)
if newNavItemRelatives.children:
currentNavItem = newNavItemRelatives.children[0]
else:
currentNavItem = newNavItem
self.__currentNavItem = currentNavItem
self.activated.emit(oldNavItem, self.currentNavItem)
def navigateUp(self):
navItemRelatives = self.getNavItemRelatives(self.currentNavItem.id)
if navItemRelatives.parent:
self.navigate(navItemRelatives.parent.id)
navHost = NavHost()

17
ui/navigation/navitem.py Normal file
View File

@ -0,0 +1,17 @@
from dataclasses import dataclass
from typing import Optional
from PySide6.QtCore import QCoreApplication
from PySide6.QtGui import QIcon, QPixmap
@dataclass
class NavItem:
id: str
icon: Optional[QIcon | QPixmap | str] = None
def text(self):
return QCoreApplication.translate("NavItem", f"{self.id}.title")
def __hash__(self):
return hash(self.id)

View File

@ -0,0 +1,84 @@
from PySide6.QtCore import QObject
from PySide6.QtGui import QFont
from PySide6.QtWidgets import QLabel, QSizePolicy, QSpacerItem, QVBoxLayout, QWidget
from ui.navigation.navhost import NavHost, navHost
from ui.navigation.navitem import NavItem
from ui.navigation.navsidebar import NavigationSideBar
class DefaultParentNavItemWidget(QWidget):
def __init__(self, navItem: NavItem, navItemChildren: list[NavItem], parent=None):
super().__init__(parent)
self.navItem = navItem
self.partialNavHost = NavHost(self)
self.partialNavHost.registerNavItem(navItem)
for _navItem in navItemChildren:
self.partialNavHost.registerNavItem(_navItem)
self.partialNavHost.navigate(navItem.id)
self.verticalLayout = QVBoxLayout(self)
self.navItemLabelFont = QFont(self.font())
self.navItemLabelFont.setPointSize(14)
self.navItemLabel = QLabel(self)
self.navItemLabel.setFont(self.navItemLabelFont)
spacer = QSpacerItem(
20, 20, QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Expanding
)
self.verticalLayout.addSpacerItem(spacer)
self.navSideBar = NavigationSideBar(self, self.partialNavHost)
self.navSideBar.navigateUpButton.setEnabled(False)
self.verticalLayout.addWidget(self.navSideBar)
self.verticalLayout.addSpacerItem(spacer)
self.retranslateUi()
def retranslateUi(self):
self.navItemLabel.setText(self.navItem.text())
class NavItemWidgets(QObject):
def __init__(self, parent=None):
super().__init__(parent)
self.__map: dict[str, QWidget] = {}
# this reference holds all the `DefaultParentNavItemWidget`s
# since these widgets are created with no parents, not keeping a reference to
# them may result in errors, even a silent app crash that is hard to debug
self.__defaultParentWidgetRefs: dict[str, QWidget] = {}
def register(self, navItemId: str, widget: QWidget):
self.__map[navItemId] = widget
def unregister(self, navItemId: str) -> bool:
try:
widget = self.__map.pop(navItemId)
widget.deleteLater()
return True
except KeyError:
return False
def get(self, navItemId: str) -> QWidget | None:
widget = self.__map.get(navItemId)
if widget is not None:
return widget
elif navItemChildren := navHost.getNavItemRelatives(navItemId).children:
if self.__defaultParentWidgetRefs.get(navItemId) is None:
defaultParentNavItemWidget = DefaultParentNavItemWidget(
navHost.findNavItem(navItemId), navItemChildren
)
self.__defaultParentWidgetRefs[navItemId] = defaultParentNavItemWidget
defaultParentNavItemWidget.partialNavHost.activated.connect(
lambda o, n: navHost.navigate(n.id)
)
return self.__defaultParentWidgetRefs.get(navItemId)
else:
return None

190
ui/navigation/navsidebar.py Normal file
View File

@ -0,0 +1,190 @@
from PySide6.QtCore import QModelIndex, Qt, Signal, Slot
from PySide6.QtGui import QFont, QIcon, QKeySequence, QShortcut
from PySide6.QtWidgets import (
QListWidget,
QListWidgetItem,
QPushButton,
QVBoxLayout,
QWidget,
)
from ui.navigation.navhost import NavHost, NavItem, navHost
from ui.widgets.slidingstackedwidget import SlidingStackedWidget
class NavItemListWidget(QListWidget):
NavItemRole = Qt.ItemDataRole.UserRole
def __init__(self, parent=None):
super().__init__(parent)
font = QFont(self.font())
font.setPointSize(14)
self.setFont(font)
self.clicked.connect(self.activated)
def setNavItems(self, items: list[NavItem]):
self.clear()
for navItem in items:
if navItem.icon:
listWidgetItem = QListWidgetItem(QIcon(navItem.icon), navItem.text())
else:
listWidgetItem = QListWidgetItem(navItem.text())
listWidgetItem.setData(self.NavItemRole, navItem)
listWidgetItem.setTextAlignment(
Qt.AlignmentFlag.AlignLeading | Qt.AlignmentFlag.AlignVCenter
)
self.addItem(listWidgetItem)
def selectNavItem(self, navItemId: str):
navItemIds = [
self.item(r).data(self.NavItemRole).id for r in range(self.count())
]
index = navItemIds.index(navItemId)
self.setCurrentIndex(self.model().index(index, 0))
class NavigationWidget(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.navHost = navHost
self.verticalLayout = QVBoxLayout(self)
self.verticalLayout.setContentsMargins(0, 0, 0, 0)
self.backButton = QPushButton(QIcon(":/icons/back.svg"), "")
self.backButton.setFlat(True)
self.backButton.setFixedHeight(20)
self.verticalLayout.addWidget(self.backButton)
self.navListWidget = NavItemListWidget(self)
self.verticalLayout.addWidget(self.navListWidget)
def setNavigationItems(self, items: list[NavItem]):
self.navListWidget.setNavItems(items)
class NavigationSideBar(QWidget):
navItemActivated = Signal(NavItem)
def __init__(self, parent=None, navHost=navHost):
super().__init__(parent)
self.navHost = None
self.navigateUpKeyboardShortcut = QShortcut(
QKeySequence(Qt.Modifier.ALT | Qt.Key.Key_Left), self, lambda: True
)
self.verticalLayout = QVBoxLayout(self)
self.navigateUpButton = QPushButton(QIcon(":/icons/back.svg"), "")
self.navigateUpButton.setFlat(True)
self.navigateUpButton.setFixedHeight(20)
self.verticalLayout.addWidget(self.navigateUpButton)
self.slidingStackedWidget = SlidingStackedWidget(self)
self.slidingStackedWidget.animationFinished.connect(
self.endChangingNavItemListWidget
)
self.verticalLayout.addWidget(self.slidingStackedWidget)
navItemListWidget = NavItemListWidget(self)
navItemListWidget.activated.connect(self.navItemListWidgetActivatedProxy)
self.slidingStackedWidget.addWidget(navItemListWidget)
self.setNavHost(navHost)
self.reloadNavWidget()
def setNavHost(self, navHost: NavHost):
if self.navHost is not None:
self.navHost.navItemsChanged.disconnect(self.reloadNavWidget)
self.navHost.activated.disconnect(self.navItemChanged)
self.navigateUpKeyboardShortcut.activated.disconnect(
self.navHost.navigateUp
)
self.navigateUpButton.clicked.disconnect(self.navHost.navigateUp)
self.navHost = navHost
self.navHost.navItemsChanged.connect(self.reloadNavWidget)
self.navHost.activated.connect(self.navItemChanged)
self.navigateUpKeyboardShortcut.activated.connect(self.navHost.navigateUp)
self.navigateUpButton.clicked.connect(self.navHost.navigateUp)
@Slot(QModelIndex)
def navItemListWidgetActivatedProxy(self, index: QModelIndex):
self.navHost.navigate(index.data(NavItemListWidget.NavItemRole).id)
self.navItemActivated.emit(index.data(NavItemListWidget.NavItemRole))
def fillNavItemListWidget(
self, currentNavItem: NavItem, listWidget: NavItemListWidget
):
currentNavItemParent = self.navHost.getNavItemRelatives(
currentNavItem.id
).parent
currentNavItems = self.navHost.getNavItemRelatives(
currentNavItemParent.id if currentNavItemParent else ""
)
listWidget.setNavItems(currentNavItems.children)
listWidget.selectNavItem(currentNavItem.id)
def reloadNavWidget(self):
self.fillNavItemListWidget(
self.navHost.currentNavItem, self.slidingStackedWidget.widget(0)
)
self.navItemChanged(self.navHost.currentNavItem, self.navHost.currentNavItem)
@Slot(NavItem, NavItem)
def navItemChanged(self, oldNavItem: NavItem, newNavItem: NavItem):
# update navigateUpButton text
if newNavItemParent := self.navHost.getNavItemRelatives(newNavItem.id).parent:
self.navigateUpButton.setText(newNavItemParent.text())
else:
self.navigateUpButton.setText("Arcaea Offline")
# update navItemListWidget
oldNavItemIdSplitted = self.navHost.getNavItemIdSplitted(oldNavItem.id)
newNavItemIdSplitted = self.navHost.getNavItemIdSplitted(newNavItem.id)
oldNavItemDepth = len(oldNavItemIdSplitted)
newNavItemDepth = len(newNavItemIdSplitted)
if oldNavItemDepth != newNavItemDepth:
# navItem depth changed, replace current NavItemListWidget
newNavItemListWidget = NavItemListWidget(self)
slidingDirection = (
self.slidingStackedWidget.slidingDirection.RightToLeft
if newNavItemDepth > oldNavItemDepth
else self.slidingStackedWidget.slidingDirection.LeftToRight
)
self.fillNavItemListWidget(newNavItem, newNavItemListWidget)
newNavItemListWidget.activated.connect(self.navItemListWidgetActivatedProxy)
self.startChangingNavItemListWidget(newNavItemListWidget, slidingDirection)
def startChangingNavItemListWidget(
self, newNavItemListWidget: NavItemListWidget, slidingDirection
):
newIndex = self.slidingStackedWidget.addWidget(newNavItemListWidget)
[
self.slidingStackedWidget.widget(i).setEnabled(False)
for i in range(self.slidingStackedWidget.count())
]
self.navigateUpButton.setEnabled(False)
self.navigateUpKeyboardShortcut.setEnabled(False)
self.slidingStackedWidget.slideInIdx(newIndex, slidingDirection)
def endChangingNavItemListWidget(self):
oldWidget = self.slidingStackedWidget.widget(0)
self.slidingStackedWidget.removeWidget(oldWidget)
oldWidget.deleteLater()
newWidget = self.slidingStackedWidget.widget(0)
newWidget.setEnabled(True)
self.navigateUpButton.setEnabled(True)
self.navigateUpKeyboardShortcut.setEnabled(True)

View File

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="24"
height="24"
viewBox="0 0 24 24"
version="1.1"
id="svg1"
inkscape:version="1.3 (0e150ed6c4, 2023-07-21)"
sodipodi:docname="back.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="true"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="px"
showgrid="true"
inkscape:zoom="24.40625"
inkscape:cx="15.323944"
inkscape:cy="10.632522"
inkscape:current-layer="layer1">
<inkscape:grid
id="grid4"
units="px"
originx="12"
originy="12"
spacingx="1"
spacingy="1"
empcolor="#0099e5"
empopacity="0.30196078"
color="#0099e5"
opacity="0.14901961"
empspacing="5"
dotted="false"
gridanglex="30"
gridanglez="30"
visible="true" />
</sodipodi:namedview>
<defs
id="defs1" />
<g
inkscape:label="图层 1"
inkscape:groupmode="layer"
id="layer1">
<path
style="fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:2;stroke-dasharray:none;stroke-opacity:1"
d="M 17,2 7,12 17,22"
id="path2" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -27,64 +27,45 @@
<translation>Reset</translation>
</message>
</context>
<context>
<name>DB30TableModel</name>
<message>
<location filename="../../extends/shared/models/tables/b30.py" line="21"/>
<source>horizontalHeader.id</source>
<translation>ID</translation>
</message>
<message>
<location filename="../../extends/shared/models/tables/b30.py" line="22"/>
<source>horizontalHeader.chart</source>
<translation>Chart</translation>
</message>
<message>
<location filename="../../extends/shared/models/tables/b30.py" line="23"/>
<source>horizontalHeader.score</source>
<translation>Score</translation>
</message>
<message>
<location filename="../../extends/shared/models/tables/b30.py" line="24"/>
<source>horizontalHeader.potential</source>
<translation>Potential</translation>
</message>
</context>
<context>
<name>DatabaseChecker</name>
<message>
<location filename="../../startup/databaseChecker.ui" line="23"/>
<location filename="../../startup/databaseChecker_ui.py" line="162"/>
<source>dbPathLabel</source>
<translation>Database path</translation>
</message>
<message>
<location filename="../../startup/databaseChecker.ui" line="33"/>
<location filename="../../startup/databaseChecker_ui.py" line="165"/>
<source>dbFilenameLabel</source>
<translation>Database filename</translation>
</message>
<message>
<location filename="../../startup/databaseChecker.ui" line="64"/>
<location filename="../../startup/databaseChecker_ui.py" line="168"/>
<source>confirmDbPathButton</source>
<translation>Confirm</translation>
</message>
<message>
<location filename="../../startup/databaseChecker.ui" line="117"/>
<location filename="../../startup/databaseChecker_ui.py" line="177"/>
<source>dbVersionLabel</source>
<translation>Database version</translation>
</message>
<message>
<location filename="../../startup/databaseChecker.ui" line="131"/>
<source>dbReInitLabel</source>
<translation>Re-initialize database</translation>
</message>
<message>
<location filename="../../startup/databaseChecker.ui" line="138"/>
<source>dbReInitButton</source>
<translation>Re-initialize</translation>
</message>
<message>
<location filename="../../startup/databaseChecker.ui" line="93"/>
<location filename="../../startup/databaseChecker_ui.py" line="171"/>
<source>dbCheckConnLabel</source>
<translation>Database connection</translation>
</message>
<message>
<location filename="../../startup/databaseChecker.ui" line="110"/>
<location filename="../../startup/databaseChecker_ui.py" line="174"/>
<source>continueButton</source>
<translation>Continue</translation>
</message>
@ -99,6 +80,29 @@
<translation>Database file does not exist. Create now?</translation>
</message>
</context>
<context>
<name>DbB30TableModel</name>
<message>
<location filename="../../extends/shared/models/tables/b30.py" line="20"/>
<source>horizontalHeader.id</source>
<translation>ID</translation>
</message>
<message>
<location filename="../../extends/shared/models/tables/b30.py" line="21"/>
<source>horizontalHeader.chart</source>
<translation>Chart</translation>
</message>
<message>
<location filename="../../extends/shared/models/tables/b30.py" line="22"/>
<source>horizontalHeader.score</source>
<translation>Score</translation>
</message>
<message>
<location filename="../../extends/shared/models/tables/b30.py" line="23"/>
<source>horizontalHeader.potential</source>
<translation>Potential</translation>
</message>
</context>
<context>
<name>DbScoreTableModel</name>
<message>
@ -303,6 +307,14 @@ validation</translation>
<translation>Score</translation>
</message>
</context>
<context>
<name>PotentialCalculator</name>
<message>
<location filename="../../implements/components/playRatingCalculator.py" line="85"/>
<source>copyButton</source>
<translation>Copy</translation>
</message>
</context>
<context>
<name>ResettableItem</name>
<message>
@ -554,6 +566,14 @@ validation</translation>
<translation>Next</translation>
</message>
</context>
<context>
<name>StepCalculator</name>
<message>
<location filename="../../implements/tabs/tabTools/tabTools_StepCalculator.py" line="96"/>
<source>playRatingCalculatorDialog.acceptButton</source>
<translation>Accept</translation>
</message>
</context>
<context>
<name>TabAbout</name>
<message>
@ -579,6 +599,11 @@ validation</translation>
<source>tab.chartInfoEditor</source>
<translation>Chart Info Editor</translation>
</message>
<message>
<location filename="../../designer/tabs/tabDbEntry.ui" line="34"/>
<source>tab.removeDuplicateScores</source>
<translation>Remove Duplicate Scores</translation>
</message>
<message>
<location filename="../../implements/tabs/tabDbEntry.py" line="20"/>
<source>tab.scoreTableViewer</source>
@ -737,6 +762,109 @@ validation</translation>
<translation>Export all your scores to &lt;a href=&quot;https://smartrte.github.io/b30gen.html&quot;&gt;smartrte.github.io&lt;/a&gt; compatible CSV file</translation>
</message>
</context>
<context>
<name>TabDb_RemoveDuplicateScores</name>
<message>
<location filename="../../designer/tabs/tabDb/tabDb_RemoveDuplicateScores.ui" line="20"/>
<source>scan.title</source>
<translation>Scan Options</translation>
</message>
<message>
<location filename="../../designer/tabs/tabDb/tabDb_RemoveDuplicateScores.ui" line="30"/>
<source>scan.option.score</source>
<translation>Score</translation>
</message>
<message>
<location filename="../../designer/tabs/tabDb/tabDb_RemoveDuplicateScores.ui" line="71"/>
<source>scan.option.date</source>
<translation>Date</translation>
</message>
<message>
<location filename="../../designer/tabs/tabDb/tabDb_RemoveDuplicateScores.ui" line="78"/>
<source>scan.option.modifier</source>
<translation>Modifier</translation>
</message>
<message>
<location filename="../../designer/tabs/tabDb/tabDb_RemoveDuplicateScores.ui" line="85"/>
<source>scan.option.clearType</source>
<translation>Clear Type</translation>
</message>
<message>
<location filename="../../designer/tabs/tabDb/tabDb_RemoveDuplicateScores.ui" line="94"/>
<source>scan.scanButton</source>
<translation>Scan</translation>
</message>
<message>
<location filename="../../designer/tabs/tabDb/tabDb_RemoveDuplicateScores.ui" line="124"/>
<source>quickSelect.title</source>
<translation>Quick Select</translation>
</message>
<message>
<location filename="../../designer/tabs/tabDb/tabDb_RemoveDuplicateScores.ui" line="130"/>
<source>quickSelect.description</source>
<translation>Keep the first score item&lt;br&gt;that matches:</translation>
</message>
<message>
<location filename="../../designer/tabs/tabDb/tabDb_RemoveDuplicateScores.ui" line="166"/>
<source>quickSelect.selectButton</source>
<translation>Select</translation>
</message>
<message>
<location filename="../../designer/tabs/tabDb/tabDb_RemoveDuplicateScores.ui" line="182"/>
<source>deselectAllButton</source>
<translation>Clear Selection</translation>
</message>
<message>
<location filename="../../designer/tabs/tabDb/tabDb_RemoveDuplicateScores.ui" line="189"/>
<source>reverseSelectionButton</source>
<translation>Reverse Selection</translation>
</message>
<message>
<location filename="../../designer/tabs/tabDb/tabDb_RemoveDuplicateScores.ui" line="209"/>
<source>collapseAllButton</source>
<translation>Collapse All Groups</translation>
</message>
<message>
<location filename="../../designer/tabs/tabDb/tabDb_RemoveDuplicateScores.ui" line="216"/>
<source>expandAllButton</source>
<translation>Expand All Groups</translation>
</message>
<message>
<location filename="../../designer/tabs/tabDb/tabDb_RemoveDuplicateScores.ui" line="223"/>
<source>resetModelButton</source>
<translation>Reset Model</translation>
</message>
<message>
<location filename="../../designer/tabs/tabDb/tabDb_RemoveDuplicateScores.ui" line="251"/>
<source>deleteSelectionButton</source>
<translation>Delete Selected Scores</translation>
</message>
<message>
<location filename="../../implements/tabs/tabDb/tabDb_RemoveDuplicateScores.py" line="142"/>
<source>quickSelectComboBox.idEarlier</source>
<translation>Earlier ID</translation>
</message>
<message>
<location filename="../../implements/tabs/tabDb/tabDb_RemoveDuplicateScores.py" line="148"/>
<source>quickSelectComboBox.dateEarlier</source>
<translation>Earlier date</translation>
</message>
<message>
<location filename="../../implements/tabs/tabDb/tabDb_RemoveDuplicateScores.py" line="154"/>
<source>quickSelectComboBox.columnsIntegral</source>
<translation>More complete data</translation>
</message>
<message>
<location filename="../../implements/tabs/tabDb/tabDb_RemoveDuplicateScores.py" line="279"/>
<source>deleteSelectionDialog.content {}</source>
<translation>Deleting {} scores from database, this cannot be undone!&lt;br&gt;Confirm?</translation>
</message>
<message>
<location filename="../../implements/tabs/tabDb/tabDb_RemoveDuplicateScores.py" line="301"/>
<source>scan_noColumnsDialog.content</source>
<translation>You haven&apos;t selected any column! Are you sure to continue?</translation>
</message>
</context>
<context>
<name>TabOcrDisabled</name>
<message>
@ -891,7 +1019,7 @@ validation</translation>
<context>
<name>TabOverview</name>
<message>
<location filename="../../implements/tabs/tabOverview.py" line="43"/>
<location filename="../../implements/tabs/tabOverview.py" line="56"/>
<source>databaseDescribeLabel {} {} {} {} {} {}</source>
<translation>There are {} packs, {} songs, {} difficulties, {} chart info ({} complete) and {} scores in database.</translation>
</message>
@ -981,6 +1109,11 @@ validation</translation>
<source>generateImageButton</source>
<translation>Generate</translation>
</message>
<message>
<location filename="../../designer/tabs/tabTools/tabTools_Andreal.ui" line="335"/>
<source>sourceCode</source>
<translation>Source code</translation>
</message>
<message>
<location filename="../../implements/tabs/tabTools/tabTools_Andreal.py" line="138"/>
<source>imageWhatIsThisDialog.description</source>
@ -990,7 +1123,7 @@ validation</translation>
<context>
<name>TabTools_ChartRecommend</name>
<message>
<location filename="../../designer/tabs/tabTools/tabTools_ChartRecommend.ui" line="99"/>
<location filename="../../designer/tabs/tabTools/tabTools_ChartRecommend.ui" line="102"/>
<source>constantRangeFromPlayRating</source>
<translation>Chart Constant Range from Play Rating</translation>
</message>
@ -1000,7 +1133,7 @@ validation</translation>
<translation>Charts by Constant</translation>
</message>
<message>
<location filename="../../designer/tabs/tabTools/tabTools_ChartRecommend.ui" line="245"/>
<location filename="../../designer/tabs/tabTools/tabTools_ChartRecommend.ui" line="248"/>
<source>chartsRecommendFromPlayRating</source>
<translation>Chart from Play Rating Based on Best Score</translation>
</message>

View File

@ -1,5 +1,5 @@
import argparse
import os
import subprocess
import sys
from pathlib import Path
@ -32,23 +32,31 @@ 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
[
"pyside6-lupdate",
"-extensions",
"py,ui",
str(designer.absolute()),
str(extends.absolute()),
str(implements.absolute()),
str(startup.absolute()),
"-ts",
str((output_dir_path / "zh_CN.ts").absolute()),
], # zh_CN
[
"pyside6-lupdate",
"-extensions",
"py,ui",
str(designer.absolute()),
str(extends.absolute()),
str(implements.absolute()),
str(startup.absolute()),
"-ts",
str((output_dir_path / "en_US.ts").absolute()),
], # en_US
]
if no_obsolete:
commands = [f"{command} -no-obsolete" for command in commands]
commands = [command + ["-no-obsolete"] for command in commands]
for command in commands:
print(f"Executing '{command}'")
output = os.popen(command).read()
print(output)
subprocess.run(command)

View File

@ -27,64 +27,45 @@
<translation></translation>
</message>
</context>
<context>
<name>DB30TableModel</name>
<message>
<location filename="../../extends/shared/models/tables/b30.py" line="21"/>
<source>horizontalHeader.id</source>
<translation>ID</translation>
</message>
<message>
<location filename="../../extends/shared/models/tables/b30.py" line="22"/>
<source>horizontalHeader.chart</source>
<translation></translation>
</message>
<message>
<location filename="../../extends/shared/models/tables/b30.py" line="23"/>
<source>horizontalHeader.score</source>
<translation></translation>
</message>
<message>
<location filename="../../extends/shared/models/tables/b30.py" line="24"/>
<source>horizontalHeader.potential</source>
<translation> PTT</translation>
</message>
</context>
<context>
<name>DatabaseChecker</name>
<message>
<location filename="../../startup/databaseChecker.ui" line="23"/>
<location filename="../../startup/databaseChecker_ui.py" line="162"/>
<source>dbPathLabel</source>
<translation></translation>
</message>
<message>
<location filename="../../startup/databaseChecker.ui" line="33"/>
<location filename="../../startup/databaseChecker_ui.py" line="165"/>
<source>dbFilenameLabel</source>
<translation></translation>
</message>
<message>
<location filename="../../startup/databaseChecker.ui" line="64"/>
<location filename="../../startup/databaseChecker_ui.py" line="168"/>
<source>confirmDbPathButton</source>
<translation></translation>
</message>
<message>
<location filename="../../startup/databaseChecker.ui" line="117"/>
<location filename="../../startup/databaseChecker_ui.py" line="177"/>
<source>dbVersionLabel</source>
<translation></translation>
</message>
<message>
<location filename="../../startup/databaseChecker.ui" line="131"/>
<source>dbReInitLabel</source>
<translation></translation>
</message>
<message>
<location filename="../../startup/databaseChecker.ui" line="138"/>
<source>dbReInitButton</source>
<translation></translation>
</message>
<message>
<location filename="../../startup/databaseChecker.ui" line="93"/>
<location filename="../../startup/databaseChecker_ui.py" line="171"/>
<source>dbCheckConnLabel</source>
<translation></translation>
</message>
<message>
<location filename="../../startup/databaseChecker.ui" line="110"/>
<location filename="../../startup/databaseChecker_ui.py" line="174"/>
<source>continueButton</source>
<translation></translation>
</message>
@ -99,6 +80,29 @@
<translation></translation>
</message>
</context>
<context>
<name>DbB30TableModel</name>
<message>
<location filename="../../extends/shared/models/tables/b30.py" line="20"/>
<source>horizontalHeader.id</source>
<translation>ID</translation>
</message>
<message>
<location filename="../../extends/shared/models/tables/b30.py" line="21"/>
<source>horizontalHeader.chart</source>
<translation></translation>
</message>
<message>
<location filename="../../extends/shared/models/tables/b30.py" line="22"/>
<source>horizontalHeader.score</source>
<translation></translation>
</message>
<message>
<location filename="../../extends/shared/models/tables/b30.py" line="23"/>
<source>horizontalHeader.potential</source>
<translation> PTT</translation>
</message>
</context>
<context>
<name>DbScoreTableModel</name>
<message>
@ -302,6 +306,14 @@
<translation></translation>
</message>
</context>
<context>
<name>PotentialCalculator</name>
<message>
<location filename="../../implements/components/playRatingCalculator.py" line="85"/>
<source>copyButton</source>
<translation></translation>
</message>
</context>
<context>
<name>ResettableItem</name>
<message>
@ -553,6 +565,14 @@
<translation></translation>
</message>
</context>
<context>
<name>StepCalculator</name>
<message>
<location filename="../../implements/tabs/tabTools/tabTools_StepCalculator.py" line="96"/>
<source>playRatingCalculatorDialog.acceptButton</source>
<translation></translation>
</message>
</context>
<context>
<name>TabAbout</name>
<message>
@ -578,6 +598,11 @@
<source>tab.chartInfoEditor</source>
<translation></translation>
</message>
<message>
<location filename="../../designer/tabs/tabDbEntry.ui" line="34"/>
<source>tab.removeDuplicateScores</source>
<translation></translation>
</message>
<message>
<location filename="../../implements/tabs/tabDbEntry.py" line="20"/>
<source>tab.scoreTableViewer</source>
@ -736,6 +761,109 @@
<translation> &lt;a href=&quot;https://smartrte.github.io/b30gen.html&quot;&gt;smartrte.github.io&lt;/a&gt; 的 CSV 文件</translation>
</message>
</context>
<context>
<name>TabDb_RemoveDuplicateScores</name>
<message>
<location filename="../../designer/tabs/tabDb/tabDb_RemoveDuplicateScores.ui" line="20"/>
<source>scan.title</source>
<translation></translation>
</message>
<message>
<location filename="../../designer/tabs/tabDb/tabDb_RemoveDuplicateScores.ui" line="30"/>
<source>scan.option.score</source>
<translation></translation>
</message>
<message>
<location filename="../../designer/tabs/tabDb/tabDb_RemoveDuplicateScores.ui" line="71"/>
<source>scan.option.date</source>
<translation></translation>
</message>
<message>
<location filename="../../designer/tabs/tabDb/tabDb_RemoveDuplicateScores.ui" line="78"/>
<source>scan.option.modifier</source>
<translation>Modifier</translation>
</message>
<message>
<location filename="../../designer/tabs/tabDb/tabDb_RemoveDuplicateScores.ui" line="85"/>
<source>scan.option.clearType</source>
<translation>Clear Type</translation>
</message>
<message>
<location filename="../../designer/tabs/tabDb/tabDb_RemoveDuplicateScores.ui" line="94"/>
<source>scan.scanButton</source>
<translation></translation>
</message>
<message>
<location filename="../../designer/tabs/tabDb/tabDb_RemoveDuplicateScores.ui" line="124"/>
<source>quickSelect.title</source>
<translation></translation>
</message>
<message>
<location filename="../../designer/tabs/tabDb/tabDb_RemoveDuplicateScores.ui" line="130"/>
<source>quickSelect.description</source>
<translation>&lt;br&gt;</translation>
</message>
<message>
<location filename="../../designer/tabs/tabDb/tabDb_RemoveDuplicateScores.ui" line="166"/>
<source>quickSelect.selectButton</source>
<translation></translation>
</message>
<message>
<location filename="../../designer/tabs/tabDb/tabDb_RemoveDuplicateScores.ui" line="182"/>
<source>deselectAllButton</source>
<translation></translation>
</message>
<message>
<location filename="../../designer/tabs/tabDb/tabDb_RemoveDuplicateScores.ui" line="189"/>
<source>reverseSelectionButton</source>
<translation></translation>
</message>
<message>
<location filename="../../designer/tabs/tabDb/tabDb_RemoveDuplicateScores.ui" line="209"/>
<source>collapseAllButton</source>
<translation></translation>
</message>
<message>
<location filename="../../designer/tabs/tabDb/tabDb_RemoveDuplicateScores.ui" line="216"/>
<source>expandAllButton</source>
<translation></translation>
</message>
<message>
<location filename="../../designer/tabs/tabDb/tabDb_RemoveDuplicateScores.ui" line="223"/>
<source>resetModelButton</source>
<translation></translation>
</message>
<message>
<location filename="../../designer/tabs/tabDb/tabDb_RemoveDuplicateScores.ui" line="251"/>
<source>deleteSelectionButton</source>
<translation></translation>
</message>
<message>
<location filename="../../implements/tabs/tabDb/tabDb_RemoveDuplicateScores.py" line="142"/>
<source>quickSelectComboBox.idEarlier</source>
<translation>ID </translation>
</message>
<message>
<location filename="../../implements/tabs/tabDb/tabDb_RemoveDuplicateScores.py" line="148"/>
<source>quickSelectComboBox.dateEarlier</source>
<translation></translation>
</message>
<message>
<location filename="../../implements/tabs/tabDb/tabDb_RemoveDuplicateScores.py" line="154"/>
<source>quickSelectComboBox.columnsIntegral</source>
<translation></translation>
</message>
<message>
<location filename="../../implements/tabs/tabDb/tabDb_RemoveDuplicateScores.py" line="279"/>
<source>deleteSelectionDialog.content {}</source>
<translation> {} &lt;br&gt;</translation>
</message>
<message>
<location filename="../../implements/tabs/tabDb/tabDb_RemoveDuplicateScores.py" line="301"/>
<source>scan_noColumnsDialog.content</source>
<translation></translation>
</message>
</context>
<context>
<name>TabOcrDisabled</name>
<message>
@ -890,7 +1018,7 @@
<context>
<name>TabOverview</name>
<message>
<location filename="../../implements/tabs/tabOverview.py" line="43"/>
<location filename="../../implements/tabs/tabOverview.py" line="56"/>
<source>databaseDescribeLabel {} {} {} {} {} {}</source>
<translation> {} {} {} {} {} {} </translation>
</message>
@ -980,6 +1108,11 @@
<source>generateImageButton</source>
<translation></translation>
</message>
<message>
<location filename="../../designer/tabs/tabTools/tabTools_Andreal.ui" line="335"/>
<source>sourceCode</source>
<translation></translation>
</message>
<message>
<location filename="../../implements/tabs/tabTools/tabTools_Andreal.py" line="138"/>
<source>imageWhatIsThisDialog.description</source>
@ -989,7 +1122,7 @@
<context>
<name>TabTools_ChartRecommend</name>
<message>
<location filename="../../designer/tabs/tabTools/tabTools_ChartRecommend.ui" line="99"/>
<location filename="../../designer/tabs/tabTools/tabTools_ChartRecommend.ui" line="102"/>
<source>constantRangeFromPlayRating</source>
<translation> PTT </translation>
</message>
@ -999,7 +1132,7 @@
<translation></translation>
</message>
<message>
<location filename="../../designer/tabs/tabTools/tabTools_ChartRecommend.ui" line="245"/>
<location filename="../../designer/tabs/tabTools/tabTools_ChartRecommend.ui" line="248"/>
<source>chartsRecommendFromPlayRating</source>
<translation> PTT </translation>
</message>

View File

@ -8,6 +8,8 @@
<file>fonts/GeosansLight.ttf</file>
<file>icons/back.svg</file>
<file>images/icon.png</file>
<file>images/logo.png</file>
<file>images/jacket-placeholder.png</file>

View File

@ -138,6 +138,11 @@ class DatabaseChecker(Ui_DatabaseChecker, QDialog):
db.init()
self.updateLabels()
@Slot()
def on_dbReInitButton_clicked(self):
Database().init(checkfirst=True)
QMessageBox.information(self, None, "OK")
@Slot()
def on_continueButton_clicked(self):
self.accept()

View File

@ -125,6 +125,20 @@
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>dbReInitLabel</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QPushButton" name="dbReInitButton">
<property name="text">
<string>dbReInitButton</string>
</property>
</widget>
</item>
</layout>
</widget>
<customwidgets>

View File

@ -8,173 +8,131 @@
## 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,
QFrame,
QHBoxLayout,
QLabel,
QLineEdit,
QPushButton,
QSizePolicy,
QSpacerItem,
QWidget,
)
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, QFrame, QHBoxLayout,
QLabel, QLineEdit, 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.setObjectName(u"DatabaseChecker")
DatabaseChecker.resize(350, 250)
DatabaseChecker.setWindowTitle("DatabaseChecker")
DatabaseChecker.setWindowTitle(u"DatabaseChecker")
self.formLayout = QFormLayout(DatabaseChecker)
self.formLayout.setObjectName("formLayout")
self.formLayout.setLabelAlignment(
Qt.AlignRight | Qt.AlignTrailing | Qt.AlignVCenter
)
self.formLayout.setObjectName(u"formLayout")
self.formLayout.setLabelAlignment(Qt.AlignRight|Qt.AlignTrailing|Qt.AlignVCenter)
self.label = QLabel(DatabaseChecker)
self.label.setObjectName("label")
self.label.setObjectName(u"label")
self.formLayout.setWidget(0, QFormLayout.LabelRole, self.label)
self.dbDirSelector = FileSelector(DatabaseChecker)
self.dbDirSelector.setObjectName("dbDirSelector")
self.dbDirSelector.setObjectName(u"dbDirSelector")
self.formLayout.setWidget(0, QFormLayout.FieldRole, self.dbDirSelector)
self.label_3 = QLabel(DatabaseChecker)
self.label_3.setObjectName("label_3")
self.label_3.setObjectName(u"label_3")
self.formLayout.setWidget(1, QFormLayout.LabelRole, self.label_3)
self.dbFilenameLineEdit = QLineEdit(DatabaseChecker)
self.dbFilenameLineEdit.setObjectName("dbFilenameLineEdit")
self.dbFilenameLineEdit.setObjectName(u"dbFilenameLineEdit")
self.formLayout.setWidget(1, QFormLayout.FieldRole, self.dbFilenameLineEdit)
self.horizontalLayout = QHBoxLayout()
self.horizontalLayout.setObjectName("horizontalLayout")
self.horizontalSpacer = QSpacerItem(
40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum
)
self.horizontalLayout.setObjectName(u"horizontalLayout")
self.horizontalSpacer = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
self.horizontalLayout.addItem(self.horizontalSpacer)
self.confirmDbPathButton = QPushButton(DatabaseChecker)
self.confirmDbPathButton.setObjectName("confirmDbPathButton")
self.confirmDbPathButton.setObjectName(u"confirmDbPathButton")
sizePolicy = QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(
self.confirmDbPathButton.sizePolicy().hasHeightForWidth()
)
sizePolicy.setHeightForWidth(self.confirmDbPathButton.sizePolicy().hasHeightForWidth())
self.confirmDbPathButton.setSizePolicy(sizePolicy)
self.horizontalLayout.addWidget(self.confirmDbPathButton)
self.formLayout.setLayout(2, QFormLayout.FieldRole, self.horizontalLayout)
self.dbVersionLabel = QLabel(DatabaseChecker)
self.dbVersionLabel.setObjectName("dbVersionLabel")
self.dbVersionLabel.setText("-")
self.dbVersionLabel.setObjectName(u"dbVersionLabel")
self.dbVersionLabel.setText(u"-")
self.formLayout.setWidget(4, QFormLayout.FieldRole, self.dbVersionLabel)
self.verticalSpacer = QSpacerItem(
20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding
)
self.verticalSpacer = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding)
self.formLayout.setItem(6, QFormLayout.FieldRole, self.verticalSpacer)
self.label_5 = QLabel(DatabaseChecker)
self.label_5.setObjectName("label_5")
self.label_5.setObjectName(u"label_5")
self.formLayout.setWidget(7, QFormLayout.LabelRole, self.label_5)
self.dbCheckConnLabel = QLabel(DatabaseChecker)
self.dbCheckConnLabel.setObjectName("dbCheckConnLabel")
self.dbCheckConnLabel.setText("...")
self.dbCheckConnLabel.setObjectName(u"dbCheckConnLabel")
self.dbCheckConnLabel.setText(u"...")
self.formLayout.setWidget(7, QFormLayout.FieldRole, self.dbCheckConnLabel)
self.continueButton = QPushButton(DatabaseChecker)
self.continueButton.setObjectName("continueButton")
self.continueButton.setObjectName(u"continueButton")
self.continueButton.setEnabled(False)
self.formLayout.setWidget(8, QFormLayout.SpanningRole, self.continueButton)
self.label_2 = QLabel(DatabaseChecker)
self.label_2.setObjectName("label_2")
self.label_2.setObjectName(u"label_2")
self.formLayout.setWidget(4, QFormLayout.LabelRole, self.label_2)
self.line = QFrame(DatabaseChecker)
self.line.setObjectName("line")
self.line.setObjectName(u"line")
self.line.setFrameShape(QFrame.HLine)
self.line.setFrameShadow(QFrame.Sunken)
self.formLayout.setWidget(3, QFormLayout.SpanningRole, self.line)
self.label_4 = QLabel(DatabaseChecker)
self.label_4.setObjectName(u"label_4")
self.formLayout.setWidget(5, QFormLayout.LabelRole, self.label_4)
self.dbReInitButton = QPushButton(DatabaseChecker)
self.dbReInitButton.setObjectName(u"dbReInitButton")
self.formLayout.setWidget(5, QFormLayout.FieldRole, self.dbReInitButton)
self.retranslateUi(DatabaseChecker)
QMetaObject.connectSlotsByName(DatabaseChecker)
# setupUi
def retranslateUi(self, DatabaseChecker):
self.label.setText(
QCoreApplication.translate("DatabaseChecker", "dbPathLabel", None)
)
self.label_3.setText(
QCoreApplication.translate("DatabaseChecker", "dbFilenameLabel", None)
)
self.confirmDbPathButton.setText(
QCoreApplication.translate("DatabaseChecker", "confirmDbPathButton", None)
)
self.label_5.setText(
QCoreApplication.translate("DatabaseChecker", "dbCheckConnLabel", None)
)
self.continueButton.setText(
QCoreApplication.translate("DatabaseChecker", "continueButton", None)
)
self.label_2.setText(
QCoreApplication.translate("DatabaseChecker", "dbVersionLabel", None)
)
self.label.setText(QCoreApplication.translate("DatabaseChecker", u"dbPathLabel", None))
self.label_3.setText(QCoreApplication.translate("DatabaseChecker", u"dbFilenameLabel", None))
self.confirmDbPathButton.setText(QCoreApplication.translate("DatabaseChecker", u"confirmDbPathButton", None))
self.label_5.setText(QCoreApplication.translate("DatabaseChecker", u"dbCheckConnLabel", None))
self.continueButton.setText(QCoreApplication.translate("DatabaseChecker", u"continueButton", None))
self.label_2.setText(QCoreApplication.translate("DatabaseChecker", u"dbVersionLabel", None))
self.label_4.setText(QCoreApplication.translate("DatabaseChecker", u"dbReInitLabel", None))
self.dbReInitButton.setText(QCoreApplication.translate("DatabaseChecker", u"dbReInitButton", None))
pass
# retranslateUi

0
ui/widgets/__init__.py Normal file
View File

View File

@ -0,0 +1,231 @@
"""
Adapted from https://github.com/Qt-Widgets/SlidingStackedWidget-1
MIT License
Copyright (c) 2020 Tim Schneeberger (ThePBone) <tim.schneeberger(at)outlook.de>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
from enum import IntEnum
from PySide6.QtCore import (
QAbstractAnimation,
QEasingCurve,
QParallelAnimationGroup,
QPoint,
QPropertyAnimation,
Signal,
)
from PySide6.QtWidgets import (
QGraphicsEffect,
QGraphicsOpacityEffect,
QStackedWidget,
QWidget,
)
class SlidingDirection(IntEnum):
Auto = 0
LeftToRight = 1
RightToLeft = 2
TopToBottom = 3
BottomToTop = 4
class SlidingStackedWidget(QStackedWidget):
slidingDirection = SlidingDirection
animationFinished = Signal()
def __init__(self, parent=None):
super().__init__(parent)
self.vertical = False
self.speedMs = 300
self.animationEasingCurve = QEasingCurve.Type.OutQuart
self.animationCurrentIndex = 0
self.animationNextIndex = 0
self.animationCurrentPoint = QPoint(0, 0)
self.animationRunning = False
self.wrap = False
self.opacityAnimation = False
def setVertical(self, vertical: bool):
self.vertical = vertical
def setSpeedMs(self, speedMs: int):
self.speedMs = speedMs
def setAnimationEasingCurve(self, easingCurve: QEasingCurve.Type):
self.animationEasingCurve = easingCurve
def setWrap(self, wrap: bool):
self.wrap = wrap
def setOpacityAnimation(self, value: bool):
self.opacityAnimation = value
def slideInNext(self) -> bool:
currentIndex = self.currentIndex()
if self.wrap or (currentIndex < self.count() - 1):
self.slideInIdx(currentIndex + 1)
else:
return False
return True
def slideInPrev(self) -> bool:
currentIndex = self.currentIndex()
if self.wrap or (currentIndex > 0):
self.slideInIdx(currentIndex - 1)
else:
return False
return True
def slideInIdx(self, idx: int, direction: SlidingDirection = SlidingDirection.Auto):
if idx > self.count() - 1:
direction = (
SlidingDirection.TopToBottom
if self.vertical
else SlidingDirection.RightToLeft
)
idx %= self.count()
elif idx < 0:
direction = (
SlidingDirection.BottomToTop
if self.vertical
else SlidingDirection.LeftToRight
)
idx = (idx + self.count()) % self.count()
self.slideInWgt(self.widget(idx), direction)
def slideInWgt(self, newwidget: QWidget, direction: SlidingDirection):
if self.animationRunning:
return
self.animationRunning = True
autoDirection = SlidingDirection.LeftToRight
currentIndex = self.currentIndex()
nextIndex = self.indexOf(newwidget)
if currentIndex == nextIndex:
self.animationRunning = False
return
elif currentIndex < nextIndex:
autoDirection = (
SlidingDirection.TopToBottom
if self.vertical
else SlidingDirection.RightToLeft
)
else:
autoDirection = (
SlidingDirection.BottomToTop
if self.vertical
else SlidingDirection.LeftToRight
)
if direction == SlidingDirection.Auto:
direction = autoDirection
offsetX = self.frameRect().width()
offsetY = self.frameRect().height()
self.widget(nextIndex).setGeometry(0, 0, offsetX, offsetY)
if direction == SlidingDirection.BottomToTop:
offsetX = 0
offsetY = -offsetY
elif direction == SlidingDirection.TopToBottom:
offsetX = 0
elif direction == SlidingDirection.RightToLeft:
offsetX = -offsetX
offsetY = 0
elif direction == SlidingDirection.LeftToRight:
offsetY = 0
nextPoint = self.widget(nextIndex).pos()
currentPoint = self.widget(currentIndex).pos()
self.animationCurrentPoint = currentPoint
self.widget(nextIndex).move(nextPoint.x() - offsetX, nextPoint.y() - offsetY)
self.widget(nextIndex).show()
self.widget(nextIndex).raise_()
currentWidgetAnimation = self.widgetPosAnimation(currentIndex)
currentWidgetAnimation.setStartValue(QPoint(currentPoint.x(), currentPoint.y()))
currentWidgetAnimation.setEndValue(
QPoint(offsetX + currentPoint.x(), offsetY + currentPoint.y())
)
nextWidgetAnimation = self.widgetPosAnimation(nextIndex)
nextWidgetAnimation.setStartValue(
QPoint(-offsetX + nextPoint.x(), offsetY + nextPoint.y())
)
nextWidgetAnimation.setEndValue(QPoint(nextPoint.x(), nextPoint.y()))
animationGroup = QParallelAnimationGroup(self)
animationGroup.addAnimation(currentWidgetAnimation)
animationGroup.addAnimation(nextWidgetAnimation)
if self.opacityAnimation:
currentWidgetOpacityEffect = QGraphicsOpacityEffect()
currentWidgetOpacityEffectAnimation = self.widgetOpacityAnimation(
currentIndex, currentWidgetOpacityEffect, 1, 0
)
nextWidgetOpacityEffect = QGraphicsOpacityEffect()
nextWidgetOpacityEffect.setOpacity(0)
nextWidgetOpacityEffectAnimation = self.widgetOpacityAnimation(
nextIndex, nextWidgetOpacityEffect, 0, 1
)
animationGroup.addAnimation(currentWidgetOpacityEffectAnimation)
animationGroup.addAnimation(nextWidgetOpacityEffectAnimation)
animationGroup.finished.connect(self.animationDoneSlot)
self.animationNextIndex = nextIndex
self.animationCurrentIndex = currentIndex
self.animationRunning = True
animationGroup.start(QAbstractAnimation.DeletionPolicy.DeleteWhenStopped)
def widgetPosAnimation(self, widgetIndex: int):
result = QPropertyAnimation(self.widget(widgetIndex), b"pos")
result.setDuration(self.speedMs)
result.setEasingCurve(self.animationEasingCurve)
return result
def widgetOpacityAnimation(
self, widgetIndex: int, graphicEffect: QGraphicsEffect, startValue, endValue
):
self.widget(widgetIndex).setGraphicsEffect(graphicEffect)
result = QPropertyAnimation(graphicEffect, b"opacity")
result.setDuration(round(self.speedMs / 2))
result.setStartValue(startValue)
result.setEndValue(endValue)
result.finished.connect(
lambda: graphicEffect.deleteLater() if graphicEffect is not None else ...
)
return result
def animationDoneSlot(self):
self.setCurrentIndex(self.animationNextIndex)
self.widget(self.animationCurrentIndex).hide()
self.widget(self.animationCurrentIndex).move(self.animationCurrentPoint)
self.animationRunning = False
self.animationFinished.emit()