From 806acd57935d8f9151e2909ea32ffbd65edd03df Mon Sep 17 00:00:00 2001 From: 283375 Date: Thu, 5 Jun 2025 17:05:47 +0800 Subject: [PATCH] refactor: SongIdSelectorViewModel --- ui/extends/components/songIdSelector.py | 39 ---- ui/implements/components/songIdSelector.py | 233 ++++++++++++++++----- 2 files changed, 185 insertions(+), 87 deletions(-) delete mode 100644 ui/extends/components/songIdSelector.py diff --git a/ui/extends/components/songIdSelector.py b/ui/extends/components/songIdSelector.py deleted file mode 100644 index cda77e0..0000000 --- a/ui/extends/components/songIdSelector.py +++ /dev/null @@ -1,39 +0,0 @@ -from arcaea_offline.database import Database -from arcaea_offline.models import Chart -from arcaea_offline.searcher import Searcher -from arcaea_offline.utils.rating import rating_class_to_short_text -from PySide6.QtCore import Qt -from PySide6.QtGui import QStandardItem, QStandardItemModel - - -class SearchCompleterModel(QStandardItemModel): - def __init__(self, parent=None): - super().__init__(parent) - self.searcher = Searcher() - self.db = Database() - - def updateSearcherSongs(self): - with self.db.sessionmaker() as session: - self.searcher.import_songs(session) - - def getSearchResult(self, kw: str): - self.clear() - - songIds = self.searcher.search(kw) - - charts: list[Chart] = [] - for songId in songIds: - _charts = self.db.get_charts_by_song_id(songId) - _charts = sorted(_charts, key=lambda c: c.rating_class, reverse=True) - charts += _charts - - for chart in charts: - displayText = ( - f"{chart.title} [{rating_class_to_short_text(chart.rating_class)}]" - ) - item = QStandardItem(kw) - item.setData(kw) - item.setData(displayText, Qt.ItemDataRole.UserRole + 75) - item.setData(f"{chart.song_id}, {chart.set}", Qt.ItemDataRole.UserRole + 76) - item.setData(chart, Qt.ItemDataRole.UserRole + 10) - self.appendRow(item) diff --git a/ui/implements/components/songIdSelector.py b/ui/implements/components/songIdSelector.py index b44aac8..64f3003 100644 --- a/ui/implements/components/songIdSelector.py +++ b/ui/implements/components/songIdSelector.py @@ -1,14 +1,18 @@ +import dataclasses import logging import re from enum import IntEnum +from typing import Any from arcaea_offline.database import Database from arcaea_offline.models import Chart -from PySide6.QtCore import QModelIndex, QSignalMapper, Qt, Signal, Slot -from PySide6.QtWidgets import QCompleter, QWidget +from arcaea_offline.searcher import Searcher +from arcaea_offline.utils.rating import rating_class_to_short_text +from PySide6.QtCore import QModelIndex, QObject, QSignalMapper, Qt, Signal, Slot +from PySide6.QtGui import QStandardItem, QStandardItemModel +from PySide6.QtWidgets import QComboBox, QCompleter, QWidget from ui.designer.components.songIdSelector_ui import Ui_SongIdSelector -from ui.extends.components.songIdSelector import SearchCompleterModel from ui.extends.shared.database import databaseUpdateSignals from ui.extends.shared.delegates.descriptionDelegate import DescriptionDelegate from ui.extends.shared.language import LanguageChangeEventFilter @@ -21,6 +25,168 @@ class SongIdSelectorMode(IntEnum): Chart = 1 +# region logics + + +@dataclasses.dataclass +class _ComboBoxItem: + text: str + userData: Any + additionalData: dict[int, Any] + + def apply(self, comboBox: QComboBox): + comboBox.addItem(self.text, self.userData) + + index = comboBox.findText(self.text) + if index > -1: + for role, value in self.additionalData.items(): + comboBox.setItemData(index, value, role) + + +class SearchCompleterModel(QStandardItemModel): + ChartRole = Qt.ItemDataRole.UserRole + 10 + + def __init__(self, parent=None): + super().__init__(parent) + self.searcher = Searcher() + self.db = Database() + + def updateSearcherSongs(self): + with self.db.sessionmaker() as session: + self.searcher.import_songs(session) + + def getSearchResult(self, kw: str): + self.clear() + + songIds = self.searcher.search(kw) + + charts: list[Chart] = [] + for songId in songIds: + _charts = self.db.get_charts_by_song_id(songId) + _charts = sorted(_charts, key=lambda c: c.rating_class, reverse=True) + charts += _charts + + for chart in charts: + displayText = ( + f"{chart.title} [{rating_class_to_short_text(chart.rating_class)}]" + ) + item = QStandardItem(kw) + item.setData(kw) + item.setData(displayText, DescriptionDelegate.MainTextRole) + item.setData( + f"{chart.song_id}, {chart.set}", DescriptionDelegate.DescriptionTextRole + ) + item.setData(chart, self.ChartRole) + self.appendRow(item) + + +class SongIdSelectorViewModel(QObject): + packComboBoxItemsReady = Signal() + songIdComboBoxItemsReady = Signal() + + def __init__(self, parent=None, *, db: Database): + super().__init__(parent) + + if not isinstance(db, Database): + raise TypeError( + "`db` should be an instance of arcaea_offline.database.Database" + ) + + self.__db: Database = None # type: ignore + self.__mode = SongIdSelectorMode.SongId + + self.__packComboBoxItems: list[_ComboBoxItem] = [] + self.__songIdComboBoxItems: list[_ComboBoxItem] = [] + + self.setDatabase(db) + + @property + def packComboBoxItems(self): + return self.__packComboBoxItems + + @property + def songIdComboBoxItems(self): + return self.__songIdComboBoxItems + + @property + def mode(self): + return self.__mode + + @mode.setter + def mode(self, value): + if not isinstance(value, SongIdSelectorMode): + raise TypeError("value is not SongIdSelectorMode") + self.__mode = value + + def setDatabase(self, db: Database): + self.__db = db + + def updatePackComboBoxItems(self): + packs = self.__db.get_packs() + + self.__packComboBoxItems.clear() + for pack in packs: + if re.search(r"_append_.*$", pack.id): + basePackId = re.sub(r"_append_.*$", "", pack.id) + basePackName = self.__db.get_pack(basePackId).name # type: ignore + packName = f"{basePackName} - {pack.name}" + else: + packName = pack.name + + self.__packComboBoxItems.append( + _ComboBoxItem( + text=f"{packName} ({pack.id})", + userData=pack.id, + additionalData={ + DescriptionDelegate.MainTextRole: packName, + DescriptionDelegate.DescriptionTextRole: pack.id, + }, + ) + ) + + self.packComboBoxItemsReady.emit() + + def updateSongIdComboBoxItems(self, packId: str): + self.__songIdComboBoxItems.clear() + + items = [] + if self.mode == SongIdSelectorMode.SongId: + items = self.__db.get_songs_by_pack_id(packId) + elif self.mode == SongIdSelectorMode.Chart: + items = self.__db.get_charts_by_pack_id(packId) + else: + assert not "reachable" + + insertedSongIds = [] + + for item in items: + if self.mode == SongIdSelectorMode.SongId: + itemId = item.id # type: ignore + elif self.mode == SongIdSelectorMode.Chart: + itemId = item.song_id # type: ignore + else: + continue + + if itemId not in insertedSongIds: + self.__songIdComboBoxItems.append( + _ComboBoxItem( + text=f"{item.title} ({itemId})", + userData=itemId, + additionalData={ + DescriptionDelegate.MainTextRole: item.title, + DescriptionDelegate.DescriptionTextRole: itemId, + }, + ) + ) + + insertedSongIds.append(itemId) + + self.songIdComboBoxItemsReady.emit() + + +# endregion + + class SongIdSelector(Ui_SongIdSelector, QWidget): valueChanged = Signal() quickSearchActivated = Signal(Chart) @@ -33,6 +199,10 @@ class SongIdSelector(Ui_SongIdSelector, QWidget): self.languageChangeEventFilter = LanguageChangeEventFilter(self) self.installEventFilter(self.languageChangeEventFilter) + self.vm = SongIdSelectorViewModel(self, db=self.db) + self.vm.packComboBoxItemsReady.connect(self.fillPackComboBox) + self.vm.songIdComboBoxItemsReady.connect(self.fillSongIdComboBox) + # quick switch bindings self.quickSwitchSignalMapper = QSignalMapper(self) self.previousPackageButton.clicked.connect(self.quickSwitchSignalMapper.map) @@ -75,6 +245,7 @@ class SongIdSelector(Ui_SongIdSelector, QWidget): databaseUpdateSignals.songAddOrDelete.connect(self.updateDatabase) def setMode(self, mode: SongIdSelectorMode): + self.vm.mode = mode self.mode = mode @Slot(str) @@ -116,7 +287,7 @@ class SongIdSelector(Ui_SongIdSelector, QWidget): pack = self.packComboBox.currentData() songId = self.songIdComboBox.currentData() - self.fillPackComboBox() + self.vm.updatePackComboBoxItems() if pack: self.selectPack(pack) @@ -125,58 +296,24 @@ class SongIdSelector(Ui_SongIdSelector, QWidget): def fillPackComboBox(self): self.packComboBox.clear() - packs = self.db.get_packs() - for pack in packs: - if isAppendPack := re.search(r"_append_.*$", pack.id): - basePackId = re.sub(r"_append_.*$", "", pack.id) - basePackName = self.db.get_pack(basePackId).name - packName = f"{basePackName} - {pack.name}" - else: - packName = pack.name - self.packComboBox.addItem(f"{packName} ({pack.id})", pack.id) - row = self.packComboBox.count() - 1 - self.packComboBox.setItemData( - row, packName, DescriptionDelegate.MainTextRole - ) - self.packComboBox.setItemData( - row, pack.id, DescriptionDelegate.DescriptionTextRole - ) + + for item in self.vm.packComboBoxItems: + item.apply(self.packComboBox) self.packComboBox.setCurrentIndex(-1) def fillSongIdComboBox(self): self.songIdComboBox.clear() - if packId := self.packComboBox.currentData(): - if self.mode == SongIdSelectorMode.SongId: - items = self.db.get_songs_by_pack_id(packId) - elif self.mode == SongIdSelectorMode.Chart: - items = self.db.get_charts_by_pack_id(packId) - else: - raise ValueError("Unknown SongIdSelectorMode.") - insertedSongIds = [] - for item in items: - if self.mode == SongIdSelectorMode.SongId: - itemId = item.id - elif self.mode == SongIdSelectorMode.Chart: - itemId = item.song_id - else: - continue - if itemId not in insertedSongIds: - self.songIdComboBox.addItem(f"{item.title} ({itemId})", itemId) - insertedSongIds.append(itemId) - row = self.songIdComboBox.count() - 1 - self.songIdComboBox.setItemData( - row, item.title, DescriptionDelegate.MainTextRole - ) - self.songIdComboBox.setItemData( - row, itemId, DescriptionDelegate.DescriptionTextRole - ) + for item in self.vm.songIdComboBoxItems: + item.apply(self.songIdComboBox) + self.songIdComboBox.setCurrentIndex(-1) @Slot() def on_packComboBox_currentIndexChanged(self): - self.fillSongIdComboBox() + if packId := self.packComboBox.currentData(): + self.vm.updateSongIdComboBoxItems(packId) @Slot(str) def on_searchLineEdit_textChanged(self, text: str): @@ -189,7 +326,7 @@ class SongIdSelector(Ui_SongIdSelector, QWidget): packIdIndex = self.packComboBox.findData(packId) if packIdIndex > -1: self.packComboBox.setCurrentIndex(packIdIndex) - self.fillSongIdComboBox() + self.vm.updateSongIdComboBoxItems(packId) return True else: logger.warning("Attempting to select an unknown pack [%s]", packId) @@ -214,7 +351,7 @@ class SongIdSelector(Ui_SongIdSelector, QWidget): @Slot(QModelIndex) def searchCompleterSetSelection(self, index: QModelIndex): - chart: Chart = index.data(Qt.ItemDataRole.UserRole + 10) + chart: Chart = index.data(SearchCompleterModel.ChartRole) self.selectChart(chart) self.quickSearchActivated.emit(chart)