mirror of
https://github.com/283375/arcaea-offline-pyside-ui.git
synced 2025-07-01 12:26:26 +00:00
feat: TabDb_RemoveDuplicateScores
This commit is contained in:
@ -1,9 +1,11 @@
|
||||
from enum import IntEnum
|
||||
|
||||
from arcaea_offline.database import Database
|
||||
from arcaea_offline.models import Chart, Difficulty, Score, Song
|
||||
from PySide6.QtCore import QModelIndex, Qt, Slot
|
||||
from PySide6.QtCore import QCoreApplication, QModelIndex, Qt, Slot
|
||||
from PySide6.QtGui import QStandardItem, QStandardItemModel
|
||||
from PySide6.QtWidgets import QStyledItemDelegate, QWidget
|
||||
from sqlalchemy import func, select
|
||||
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 (
|
||||
@ -11,6 +13,7 @@ from ui.designer.tabs.tabDb.tabDb_RemoveDuplicateScores_ui import (
|
||||
)
|
||||
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):
|
||||
@ -40,12 +43,19 @@ class RemoveDuplicateScoresModel(QStandardItemModel):
|
||||
item.setData(song, self.SongRole)
|
||||
item.setData(difficulty, self.DifficultyRole)
|
||||
|
||||
def setScores(self, scores: list[Score]):
|
||||
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[tuple[str, int], list[Score]] = {}
|
||||
scoreKeyMap: dict[str, list[Score]] = {}
|
||||
for score in scores:
|
||||
key = (score.song_id, score.rating_class)
|
||||
key = self.getGroupKey(score, columns)
|
||||
if scoreKeyMap.get(key) is None:
|
||||
scoreKeyMap[key] = [score]
|
||||
else:
|
||||
@ -54,7 +64,8 @@ class RemoveDuplicateScoresModel(QStandardItemModel):
|
||||
db = Database()
|
||||
with db.sessionmaker() as session:
|
||||
for key, scores in scoreKeyMap.items():
|
||||
songId, ratingClass = key
|
||||
songId, ratingClass = key.split("||")[:2]
|
||||
ratingClass = int(ratingClass)
|
||||
|
||||
parentCheckBoxItem = QStandardItem(f"{len(scores)} items")
|
||||
parentChartItem = QStandardItem()
|
||||
@ -112,26 +123,52 @@ class TreeViewProxyDelegate(QStyledItemDelegate):
|
||||
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.scan_scanButton.clicked.connect(self.fillModel)
|
||||
|
||||
self.model = RemoveDuplicateScoresModel(self)
|
||||
self.treeView.setModel(self.model)
|
||||
self.removeDuplicateScoresModel = RemoveDuplicateScoresModel(self)
|
||||
self.treeView.setModel(self.removeDuplicateScoresModel)
|
||||
|
||||
self.treeViewChartDelegate = TreeViewChartDelegate(self.treeView)
|
||||
self.treeViewScoreDelegate = TreeViewScoreDelegate(self.treeView)
|
||||
self.treeViewDelegate = TreeViewProxyDelegate(
|
||||
self.treeViewProxyDelegate = TreeViewProxyDelegate(
|
||||
self.treeViewChartDelegate, self.treeViewScoreDelegate, self.treeView
|
||||
)
|
||||
self.treeView.setItemDelegateForColumn(1, self.treeViewDelegate)
|
||||
self.treeView.setItemDelegateForColumn(1, self.treeViewProxyDelegate)
|
||||
|
||||
def getGroupByColumns(self):
|
||||
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():
|
||||
@ -144,15 +181,17 @@ class TabDb_RemoveDuplicateScores(Ui_TabDb_RemoveDuplicateScores, QWidget):
|
||||
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_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.getGroupByColumns()
|
||||
columns = self.getQueryColumns()
|
||||
with self.db.sessionmaker() as session:
|
||||
groupBySubquery = (
|
||||
select(*columns).group_by(*columns).having(func.count() > 1).subquery()
|
||||
@ -162,11 +201,137 @@ class TabDb_RemoveDuplicateScores(Ui_TabDb_RemoveDuplicateScores, QWidget):
|
||||
]
|
||||
return session.query(Score).where(*selectInClause).all()
|
||||
|
||||
def fillModel(self):
|
||||
def scan(self):
|
||||
scores = self.getQueryScores()
|
||||
self.model.setScores(scores)
|
||||
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()
|
||||
@ -174,3 +339,11 @@ class TabDb_RemoveDuplicateScores(Ui_TabDb_RemoveDuplicateScores, QWidget):
|
||||
@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()
|
||||
|
Reference in New Issue
Block a user