From 1eeec6f7458776dc1508cebc73e34ebb90027fe9 Mon Sep 17 00:00:00 2001 From: 283375 Date: Mon, 23 Oct 2023 23:51:11 +0800 Subject: [PATCH] wip: `TabDb_RemoveDuplicateScores` ui --- .../tabs/tabDb/tabDb_RemoveDuplicateScores.ui | 206 ++++++++++++++++++ .../tabDb/tabDb_RemoveDuplicateScores_ui.py | 190 ++++++++++++++++ .../tabs/tabDb/tabDb_RemoveDuplicateScores.py | 176 +++++++++++++++ 3 files changed, 572 insertions(+) create mode 100644 ui/designer/tabs/tabDb/tabDb_RemoveDuplicateScores.ui create mode 100644 ui/designer/tabs/tabDb/tabDb_RemoveDuplicateScores_ui.py create mode 100644 ui/implements/tabs/tabDb/tabDb_RemoveDuplicateScores.py diff --git a/ui/designer/tabs/tabDb/tabDb_RemoveDuplicateScores.ui b/ui/designer/tabs/tabDb/tabDb_RemoveDuplicateScores.ui new file mode 100644 index 0000000..6955e94 --- /dev/null +++ b/ui/designer/tabs/tabDb/tabDb_RemoveDuplicateScores.ui @@ -0,0 +1,206 @@ + + + TabDb_RemoveDuplicateScores + + + + 0 + 0 + 700 + 500 + + + + TabDb_RemoveDuplicateScores + + + + + + + + scan.title + + + + + + scan.option.score + + + + + + + + + PURE + + + + + + + FAR + + + + + + + LOST + + + + + + + + + MAX RECALL + + + + + + + + + scan.option.clearType + + + + + + + scan.option.modifier + + + + + + + + + scan.scanButton + + + + + + + + + + quickSelect.title + + + + + + quickSelect.keepSingleLabel + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 91 + + + + + + + + quickSelect.selectButton + + + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + collapseAllButton + + + + + + + expandAllButton + + + + + + + resetModelButton + + + + + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::NoSelection + + + QAbstractItemView::ScrollPerPixel + + + + + + + + true + + + + QPushButton { color: red }; + + + deleteSelectedButton + + + + + + + + diff --git a/ui/designer/tabs/tabDb/tabDb_RemoveDuplicateScores_ui.py b/ui/designer/tabs/tabDb/tabDb_RemoveDuplicateScores_ui.py new file mode 100644 index 0000000..cae37a9 --- /dev/null +++ b/ui/designer/tabs/tabDb/tabDb_RemoveDuplicateScores_ui.py @@ -0,0 +1,190 @@ +# -*- 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, + QGridLayout, 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(700, 500) + TabDb_RemoveDuplicateScores.setWindowTitle(u"TabDb_RemoveDuplicateScores") + self.gridLayout = QGridLayout(TabDb_RemoveDuplicateScores) + self.gridLayout.setObjectName(u"gridLayout") + self.verticalLayout = QVBoxLayout() + self.verticalLayout.setObjectName(u"verticalLayout") + self.groupBox_2 = QGroupBox(TabDb_RemoveDuplicateScores) + self.groupBox_2.setObjectName(u"groupBox_2") + self.verticalLayout_2 = QVBoxLayout(self.groupBox_2) + self.verticalLayout_2.setObjectName(u"verticalLayout_2") + self.scan_option_scoreCheckBox = QCheckBox(self.groupBox_2) + self.scan_option_scoreCheckBox.setObjectName(u"scan_option_scoreCheckBox") + + self.verticalLayout_2.addWidget(self.scan_option_scoreCheckBox) + + self.horizontalLayout_2 = QHBoxLayout() + self.horizontalLayout_2.setObjectName(u"horizontalLayout_2") + 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.verticalLayout_2.addLayout(self.horizontalLayout_2) + + 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.verticalLayout_2.addWidget(self.scan_option_maxRecallCheckBox) + + self.horizontalLayout_3 = QHBoxLayout() + self.horizontalLayout_3.setObjectName(u"horizontalLayout_3") + 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.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.verticalLayout_2.addLayout(self.horizontalLayout_3) + + self.scan_scanButton = QPushButton(self.groupBox_2) + self.scan_scanButton.setObjectName(u"scan_scanButton") + + self.verticalLayout_2.addWidget(self.scan_scanButton) + + + self.verticalLayout.addWidget(self.groupBox_2) + + 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_timeComboBox = QComboBox(self.groupBox) + self.quickSelect_timeComboBox.setObjectName(u"quickSelect_timeComboBox") + + self.verticalLayout_3.addWidget(self.quickSelect_timeComboBox) + + self.quickSelect_ColumnComboBox = QComboBox(self.groupBox) + self.quickSelect_ColumnComboBox.setObjectName(u"quickSelect_ColumnComboBox") + + self.verticalLayout_3.addWidget(self.quickSelect_ColumnComboBox) + + self.verticalSpacer = QSpacerItem(20, 91, QSizePolicy.Minimum, QSizePolicy.Expanding) + + self.verticalLayout_3.addItem(self.verticalSpacer) + + self.quickSelect_selectButton = QPushButton(self.groupBox) + self.quickSelect_selectButton.setObjectName(u"quickSelect_selectButton") + + self.verticalLayout_3.addWidget(self.quickSelect_selectButton) + + + self.verticalLayout.addWidget(self.groupBox) + + + self.gridLayout.addLayout(self.verticalLayout, 0, 1, 1, 1) + + self.horizontalLayout_4 = QHBoxLayout() + self.horizontalLayout_4.setObjectName(u"horizontalLayout_4") + self.horizontalSpacer = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) + + self.horizontalLayout_4.addItem(self.horizontalSpacer) + + self.collapseAllButton = QPushButton(TabDb_RemoveDuplicateScores) + self.collapseAllButton.setObjectName(u"collapseAllButton") + + self.horizontalLayout_4.addWidget(self.collapseAllButton) + + self.expandAllButton = QPushButton(TabDb_RemoveDuplicateScores) + self.expandAllButton.setObjectName(u"expandAllButton") + + self.horizontalLayout_4.addWidget(self.expandAllButton) + + self.resetModelButton = QPushButton(TabDb_RemoveDuplicateScores) + self.resetModelButton.setObjectName(u"resetModelButton") + + self.horizontalLayout_4.addWidget(self.resetModelButton) + + + self.gridLayout.addLayout(self.horizontalLayout_4, 1, 0, 1, 1) + + 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.gridLayout.addWidget(self.treeView, 0, 0, 1, 1) + + self.deleteSelectedButton = QPushButton(TabDb_RemoveDuplicateScores) + self.deleteSelectedButton.setObjectName(u"deleteSelectedButton") + font = QFont() + font.setBold(True) + self.deleteSelectedButton.setFont(font) + self.deleteSelectedButton.setStyleSheet(u"QPushButton { color: red };") + + self.gridLayout.addWidget(self.deleteSelectedButton, 1, 1, 1, 1) + + + 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_clearTypeCheckBox.setText(QCoreApplication.translate("TabDb_RemoveDuplicateScores", u"scan.option.clearType", None)) + self.scan_option_modifierCheckBox.setText(QCoreApplication.translate("TabDb_RemoveDuplicateScores", u"scan.option.modifier", 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.keepSingleLabel", None)) + self.quickSelect_selectButton.setText(QCoreApplication.translate("TabDb_RemoveDuplicateScores", u"quickSelect.selectButton", 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.deleteSelectedButton.setText(QCoreApplication.translate("TabDb_RemoveDuplicateScores", u"deleteSelectedButton", None)) + pass + # retranslateUi + diff --git a/ui/implements/tabs/tabDb/tabDb_RemoveDuplicateScores.py b/ui/implements/tabs/tabDb/tabDb_RemoveDuplicateScores.py new file mode 100644 index 0000000..4702681 --- /dev/null +++ b/ui/implements/tabs/tabDb/tabDb_RemoveDuplicateScores.py @@ -0,0 +1,176 @@ +from arcaea_offline.database import Database +from arcaea_offline.models import Chart, Difficulty, Score, Song +from PySide6.QtCore import QModelIndex, Qt, Slot +from PySide6.QtGui import QStandardItem, QStandardItemModel +from PySide6.QtWidgets import QStyledItemDelegate, QWidget +from sqlalchemy import 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 + + +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)) + .one() + ) + song = session.query(Song).where(Song.id == songId).one() + difficulty = ( + session.query(Difficulty) + .where( + (Difficulty.song_id == songId) + & (Difficulty.rating_class == ratingClass) + ) + .one() + ) + item.setData(chart, self.ChartRole) + item.setData(song, self.SongRole) + item.setData(difficulty, self.DifficultyRole) + + def setScores(self, scores: list[Score]): + self.clear() + + scoreKeyMap: dict[tuple[str, int], list[Score]] = {} + for score in scores: + key = (score.song_id, score.rating_class) + 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 + + 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 TabDb_RemoveDuplicateScores(Ui_TabDb_RemoveDuplicateScores, QWidget): + def __init__(self, parent=None): + super().__init__(parent) + self.setupUi(self) + + self.db = Database() + + self.scan_scanButton.clicked.connect(self.fillModel) + + self.model = RemoveDuplicateScoresModel(self) + self.treeView.setModel(self.model) + + self.treeViewChartDelegate = TreeViewChartDelegate(self.treeView) + self.treeViewScoreDelegate = TreeViewScoreDelegate(self.treeView) + self.treeViewDelegate = TreeViewProxyDelegate( + self.treeViewChartDelegate, self.treeViewScoreDelegate, self.treeView + ) + self.treeView.setItemDelegateForColumn(1, self.treeViewDelegate) + + def getGroupByColumns(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_clearTypeCheckBox.isChecked(): + columns.append(Score.clear_type) + if self.scan_option_modifierCheckBox.isChecked(): + columns.append(Score.modifier) + + return columns + + def getQueryScores(self): + columns = self.getGroupByColumns() + 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 fillModel(self): + scores = self.getQueryScores() + self.model.setScores(scores) + self.treeView.expandAll() + + @Slot() + def on_expandAllButton_clicked(self): + self.treeView.expandAll() + + @Slot() + def on_collapseAllButton_clicked(self): + self.treeView.collapseAll()