diff --git a/ui/extends/shared/models/tables/b30.py b/ui/extends/shared/models/tables/b30.py
new file mode 100644
index 0000000..e6eebdc
--- /dev/null
+++ b/ui/extends/shared/models/tables/b30.py
@@ -0,0 +1,139 @@
+from arcaea_offline.models import Chart, Score
+from PySide6.QtCore import QCoreApplication, QModelIndex, QSortFilterProxyModel, Qt
+
+from .base import DbTableModel
+
+
+class DbB30TableModel(DbTableModel):
+ IdRole = Qt.ItemDataRole.UserRole + 10
+ ChartRole = Qt.ItemDataRole.UserRole + 11
+ ScoreRole = Qt.ItemDataRole.UserRole + 12
+ PttRole = Qt.ItemDataRole.UserRole + 13
+
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.__items = []
+
+ def retranslateHeaders(self):
+ self._horizontalHeaders = [
+ # fmt: off
+ QCoreApplication.translate("DB30TableModel", "horizontalHeader.id"),
+ QCoreApplication.translate("DB30TableModel", "horizontalHeader.chart"),
+ QCoreApplication.translate("DB30TableModel", "horizontalHeader.score"),
+ QCoreApplication.translate("DB30TableModel", "horizontalHeader.potential"),
+ # fmt: on
+ ]
+
+ def syncDb(self):
+ self.__items.clear()
+
+ results = self._db.conn.execute(
+ 'SELECT * FROM calculated ORDER BY "potential" DESC LIMIT 40'
+ ).fetchall()
+
+ songIds = [r[0] for r in results]
+ ptts = [r[-1] for r in results]
+
+ for scoreId, ptt in zip(songIds, ptts):
+ score = Score.from_db_row(self._db.get_scores(score_id=scoreId)[0])
+ chart = Chart.from_db_row(
+ self._db.get_chart(score.song_id, score.rating_class)
+ )
+
+ self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount())
+ self.__items.append(
+ {
+ self.IdRole: score.id,
+ self.ChartRole: chart,
+ self.ScoreRole: score,
+ self.PttRole: ptt,
+ }
+ )
+ self.endInsertRows()
+
+ # trigger view update
+ topLeft = self.index(0, 0)
+ bottomRight = self.index(self.rowCount() - 1, self.columnCount() - 1)
+ self.dataChanged.emit(
+ topLeft,
+ bottomRight,
+ [Qt.ItemDataRole.DisplayRole, self.IdRole, self.ChartRole, self.ScoreRole],
+ )
+
+ def rowCount(self, *args):
+ return len(self.__items)
+
+ def data(self, index, role):
+ if index.isValid() and self.checkIndex(index):
+ if index.column() == 0 and role in [
+ Qt.ItemDataRole.DisplayRole,
+ self.IdRole,
+ ]:
+ return self.__items[index.row()][self.IdRole]
+ elif index.column() == 1 and role == self.ChartRole:
+ return self.__items[index.row()][self.ChartRole]
+ elif index.column() == 2 and role in [self.ChartRole, self.ScoreRole]:
+ return self.__items[index.row()][role]
+ elif index.column() == 3:
+ if role == Qt.ItemDataRole.DisplayRole:
+ return f"{self.__items[index.row()][self.PttRole]:.3f}"
+ elif role == self.PttRole:
+ return self.__items[index.row()][self.PttRole]
+ return None
+
+ def setData(self, index, value, role):
+ return False
+
+ def flags(self, index) -> Qt.ItemFlag:
+ flags = super().flags(index)
+ flags &= ~(Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEditable)
+ return flags
+
+ def _removeRow(self, row: int, syncDb: bool = True):
+ return False
+
+ def removeRow(self, row: int, parent=...):
+ return False
+
+ def removeRows(self, row: int, count: int, parent=...):
+ return False
+
+ def removeRowList(self, rowList: list[int]):
+ return False
+
+
+class DbB30TableSortFilterProxyModel(QSortFilterProxyModel):
+ Sort_C2_ScoreRole = Qt.ItemDataRole.UserRole + 75
+ Sort_C2_TimeRole = Qt.ItemDataRole.UserRole + 76
+
+ def headerData(self, section: int, orientation: Qt.Orientation, role: int):
+ # always show not sorted row sequence
+ if (
+ orientation != Qt.Orientation.Vertical
+ or role != Qt.ItemDataRole.DisplayRole
+ ):
+ return super().headerData(section, orientation, role)
+ return section + 1
+
+ def lessThan(self, source_left, source_right) -> bool:
+ if source_left.column() != source_right.column():
+ return
+
+ column = source_left.column()
+ if column == 0:
+ return source_left.data(DbB30TableModel.IdRole) < source_right.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):
+ if self.sortRole() == self.Sort_C2_ScoreRole:
+ return score_left.score < score_right.score
+ elif self.sortRole() == self.Sort_C2_TimeRole:
+ return score_left.time < score_right.time
+ elif column == 3:
+ return source_left.data(DbB30TableModel.PttRole) < source_right.data(
+ DbB30TableModel.PttRole
+ )
+ return super().lessThan(source_left, source_right)
diff --git a/ui/implements/tabs/tabDb/tabDb_B30TableViewer.py b/ui/implements/tabs/tabDb/tabDb_B30TableViewer.py
new file mode 100644
index 0000000..b113d2d
--- /dev/null
+++ b/ui/implements/tabs/tabDb/tabDb_B30TableViewer.py
@@ -0,0 +1,100 @@
+from arcaea_offline.models import ScoreInsert
+from PySide6.QtCore import QModelIndex, Qt, Slot
+from PySide6.QtGui import QColor, QPalette
+from PySide6.QtWidgets import QMessageBox
+
+from ui.extends.shared.delegates.chartDelegate import ChartDelegate
+from ui.extends.shared.delegates.scoreDelegate import ScoreDelegate
+from ui.extends.shared.models.tables.b30 import (
+ DbB30TableModel,
+ DbB30TableSortFilterProxyModel,
+)
+from ui.implements.components.dbTableViewer import DbTableViewer
+
+
+class TableChartDelegate(ChartDelegate):
+ def getChart(self, index):
+ return index.data(DbB30TableModel.ChartRole)
+
+
+class TableScoreDelegate(ScoreDelegate):
+ def getChart(self, index):
+ return index.data(DbB30TableModel.ChartRole)
+
+ def getScoreInsert(self, index: QModelIndex) -> ScoreInsert | None:
+ return super().getScoreInsert(index)
+
+ def getScore(self, index):
+ return index.data(DbB30TableModel.ScoreRole)
+
+ def setModelData(self, editor, model, index):
+ QMessageBox.information(self, None, "Cannot edit read only table.")
+ return False
+
+
+class DbB30TableViewer(DbTableViewer):
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.tableView.verticalHeader().setVisible(True)
+
+ self.tableModel = DbB30TableModel(self)
+ self.tableProxyModel = DbB30TableSortFilterProxyModel(self)
+ self.tableProxyModel.setSourceModel(self.tableModel)
+ self.tableView.setModel(self.tableProxyModel)
+ self.tableView.setItemDelegateForColumn(1, TableChartDelegate(self.tableView))
+ self.tableView.setItemDelegateForColumn(2, TableScoreDelegate(self.tableView))
+
+ tableViewPalette = QPalette(self.tableView.palette())
+ highlightColor = QColor(tableViewPalette.color(QPalette.ColorRole.Highlight))
+ highlightColor.setAlpha(25)
+ tableViewPalette.setColor(QPalette.ColorRole.Highlight, highlightColor)
+ self.tableView.setPalette(tableViewPalette)
+ self.tableModel.dataChanged.connect(self.resizeTableView)
+
+ self.fillSortComboBox()
+
+ def fillSortComboBox(self):
+ self.sort_comboBox.addItem("ID", [0, 1])
+ self.sort_comboBox.addItem(
+ "Score", [3, DbB30TableSortFilterProxyModel.Sort_C2_ScoreRole]
+ )
+ self.sort_comboBox.addItem(
+ "Time", [3, DbB30TableSortFilterProxyModel.Sort_C2_TimeRole]
+ )
+ self.sort_comboBox.addItem("Potential", [3, 1])
+ self.sort_comboBox.setCurrentIndex(0)
+ self.on_sort_comboBox_activated()
+
+ @Slot()
+ def resizeTableView(self):
+ self.tableView.resizeRowsToContents()
+ self.tableView.resizeColumnsToContents()
+
+ @Slot()
+ def on_sort_comboBox_activated(self):
+ self.sortProxyModel()
+
+ @Slot()
+ def on_sort_descendingCheckBox_toggled(self):
+ self.sortProxyModel()
+
+ @Slot()
+ def sortProxyModel(self):
+ if self.sort_comboBox.currentIndex() > -1:
+ column, role = self.sort_comboBox.currentData()
+ self.tableProxyModel.setSortRole(role)
+ self.tableProxyModel.sort(
+ column,
+ Qt.SortOrder.DescendingOrder
+ if self.sort_descendingCheckBox.isChecked()
+ else Qt.SortOrder.AscendingOrder,
+ )
+
+ @Slot()
+ def on_action_removeSelectedButton_clicked(self):
+ QMessageBox.information(self, None, "Cannot edit read only table.")
+
+ @Slot()
+ def on_refreshButton_clicked(self):
+ self.tableModel.syncDb()
+ self.resizeTableView()
diff --git a/ui/implements/tabs/tabDbEntry.py b/ui/implements/tabs/tabDbEntry.py
index 835bb5c..41d9094 100644
--- a/ui/implements/tabs/tabDbEntry.py
+++ b/ui/implements/tabs/tabDbEntry.py
@@ -2,6 +2,7 @@ from PySide6.QtCore import QCoreApplication
from PySide6.QtWidgets import QWidget
from ui.designer.tabs.tabDbEntry_ui import Ui_TabDbEntry
+from ui.implements.tabs.tabDb.tabDb_B30TableViewer import DbB30TableViewer
from ui.implements.tabs.tabDb.tabDb_ScoreTableViewer import DbScoreTableViewer
@@ -14,3 +15,7 @@ class TabDbEntry(Ui_TabDbEntry, QWidget):
DbScoreTableViewer(self),
QCoreApplication.translate("TabDbEntry", "tab.scoreTableViewer"),
)
+ self.tabWidget.addTab(
+ DbB30TableViewer(self),
+ QCoreApplication.translate("TabDbEntry", "tab.b30TableViewer"),
+ )
diff --git a/ui/implements/tabs/tabOcr/tabOcr_B30.py b/ui/implements/tabs/tabOcr/tabOcr_B30.py
index b63975e..c7a4a70 100644
--- a/ui/implements/tabs/tabOcr/tabOcr_B30.py
+++ b/ui/implements/tabs/tabOcr/tabOcr_B30.py
@@ -12,8 +12,8 @@ from PySide6.QtWidgets import QWidget
from ui.designer.tabs.tabOcr.tabOcr_B30_ui import Ui_TabOcr_B30
from ui.extends.components.ocrQueue import OcrQueueModel
-from ui.extends.shared.settings import Settings
from ui.extends.shared.cv2_utils import cv2BgrMatToQImage, qImageToCvMatBgr
+from ui.extends.shared.settings import Settings
from ui.extends.tabs.tabOcr.tabOcr_B30 import (
ChieriV4OcrRunnable,
b30ResultToScoreInsert,
diff --git a/ui/resources/translations/en_US.ts b/ui/resources/translations/en_US.ts
index 6da6f8a..24bae0e 100644
--- a/ui/resources/translations/en_US.ts
+++ b/ui/resources/translations/en_US.ts
@@ -49,6 +49,34 @@
Reset
+
+ DB30TableModel
+
+
+ horizontalHeader.tableId
+ No.
+
+
+
+ horizontalHeader.id
+ ID
+
+
+
+ horizontalHeader.chart
+ Chart
+
+
+
+ horizontalHeader.score
+ Score
+
+
+
+ horizontalHeader.potential
+ Potential
+
+
DatabaseChecker
@@ -260,22 +288,22 @@ validation
OcrTableModel
-
+
horizontalHeader.title.select
Select
-
+
horizontalHeader.title.imagePreview
Image Preview
-
+
horizontalHeader.title.chart
Chart
-
+
horizontalHeader.title.score
Score
@@ -412,10 +440,15 @@ validation
Manage
-
+
tab.scoreTableViewer
TABLE [Score]
+
+
+ tab.b30TableViewer
+ TABLE [B30]
+
TabDb_Manage
diff --git a/ui/resources/translations/zh_CN.ts b/ui/resources/translations/zh_CN.ts
index 5d9d864..102d09d 100644
--- a/ui/resources/translations/zh_CN.ts
+++ b/ui/resources/translations/zh_CN.ts
@@ -49,6 +49,34 @@
重置
+
+ DB30TableModel
+
+
+ horizontalHeader.tableId
+ 序号
+
+
+
+ horizontalHeader.id
+ ID
+
+
+
+ horizontalHeader.chart
+ 谱面
+
+
+
+ horizontalHeader.score
+ 分数
+
+
+
+ horizontalHeader.potential
+ 单曲 PTT
+
+
DatabaseChecker
@@ -259,22 +287,22 @@
OcrTableModel
-
+
horizontalHeader.title.select
选择
-
+
horizontalHeader.title.imagePreview
图像预览
-
+
horizontalHeader.title.chart
谱面
-
+
horizontalHeader.title.score
分数
@@ -411,10 +439,15 @@
管理
-
+
tab.scoreTableViewer
表 [分数]
+
+
+ tab.b30TableViewer
+ 表 [B30]
+
TabDb_Manage