mirror of
https://github.com/283375/arcaea-offline-pyside-ui.git
synced 2025-11-06 20:42:15 +00:00
wip: database checker
This commit is contained in:
52
app.py
Normal file
52
app.py
Normal file
@ -0,0 +1,52 @@
|
||||
import logging
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from PySide6.QtCore import QCoreApplication, QObject, Qt, QUrl
|
||||
from PySide6.QtGui import QGuiApplication, QIcon
|
||||
from PySide6.QtQml import QQmlApplicationEngine
|
||||
from PySide6.QtQuickControls2 import QQuickStyle
|
||||
|
||||
from ui.resources import resources_rc # noqa: F401
|
||||
from ui.utils import url # noqa: F401
|
||||
from ui.viewmodels import overview # noqa: F401
|
||||
|
||||
CURRENT_DIRECTORY = Path(__file__).resolve().parent
|
||||
DEFAULT_FONTS = ["微软雅黑", "Microsoft YaHei UI", "Microsoft YaHei", "Segoe UI"]
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="[%(asctime)s][%(levelname)s] %(message)s",
|
||||
datefmt="%H:%M:%S",
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
app = QGuiApplication(sys.argv)
|
||||
app.setFont(DEFAULT_FONTS)
|
||||
app.setApplicationName("arcaea-offline-pyside-ui")
|
||||
app.setApplicationDisplayName("Arcaea Offline")
|
||||
app.setWindowIcon(QIcon(":/images/icon.png"))
|
||||
|
||||
QQuickStyle.setStyle("Fusion")
|
||||
|
||||
engine = QQmlApplicationEngine()
|
||||
|
||||
def onEngineObjectCreated(obj: QObject | None, objUrl: QUrl) -> None:
|
||||
if obj is None:
|
||||
logger.critical("rootObject is None! Exiting!")
|
||||
QCoreApplication.exit(-1)
|
||||
|
||||
engine.objectCreated.connect(
|
||||
onEngineObjectCreated,
|
||||
Qt.ConnectionType.QueuedConnection,
|
||||
)
|
||||
|
||||
engine.load("ui/qmls/App.qml")
|
||||
|
||||
sys.exit(app.exec())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -4,7 +4,8 @@ from enum import StrEnum
|
||||
|
||||
class _General(StrEnum):
|
||||
Language = "Language"
|
||||
DatabaseUrl = "DatabaseUrl"
|
||||
DatabaseType = "DatabaseType"
|
||||
DatabaseConn = "DatabaseConn"
|
||||
|
||||
|
||||
class _Ocr(StrEnum):
|
||||
|
||||
@ -1,4 +1,10 @@
|
||||
from dataclasses import dataclass
|
||||
from enum import StrEnum
|
||||
|
||||
|
||||
class GeneralDatabaseType(StrEnum):
|
||||
FILE = "file"
|
||||
URL = "url"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
||||
@ -51,6 +51,7 @@ ignore = [
|
||||
"N806", # non-lowercase-variable-in-function
|
||||
"N815", # mixed-case-variable-in-class-scope
|
||||
"N816", # mixed-case-variable-in-global-scope
|
||||
"N999", # invalid-module-name
|
||||
]
|
||||
|
||||
[tool.pyright]
|
||||
|
||||
24
ui/qmls/App.qml
Normal file
24
ui/qmls/App.qml
Normal file
@ -0,0 +1,24 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import "./DatabaseChecker" as DatabaseChecker
|
||||
|
||||
ApplicationWindow {
|
||||
visible: true
|
||||
|
||||
width: 800
|
||||
height: 600
|
||||
|
||||
StackLayout {
|
||||
id: stackLayout
|
||||
anchors.fill: parent
|
||||
currentIndex: 0
|
||||
|
||||
DatabaseChecker.Index {
|
||||
onReady: parent.currentIndex = 1
|
||||
}
|
||||
|
||||
AppMain {}
|
||||
}
|
||||
}
|
||||
68
ui/qmls/AppMain.qml
Normal file
68
ui/qmls/AppMain.qml
Normal file
@ -0,0 +1,68 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
RowLayout {
|
||||
id: layout
|
||||
spacing: 5
|
||||
|
||||
ListModel {
|
||||
id: navListModel
|
||||
|
||||
ListElement {
|
||||
_id: 'home'
|
||||
label: 'Overview'
|
||||
qmlSource: 'Overview.qml'
|
||||
}
|
||||
ListElement {
|
||||
_id: 'database'
|
||||
label: 'Database'
|
||||
qmlSource: '404.qml'
|
||||
}
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: navListView
|
||||
|
||||
Layout.preferredWidth: 200
|
||||
Layout.fillHeight: true
|
||||
|
||||
model: navListModel
|
||||
focus: true
|
||||
|
||||
delegate: Item {
|
||||
required property int index
|
||||
required property string label
|
||||
width: parent.width
|
||||
height: 30
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: () => {
|
||||
navListView.currentIndex = index;
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.margins: 5
|
||||
anchors.fill: parent
|
||||
|
||||
text: parent.label
|
||||
}
|
||||
}
|
||||
|
||||
highlight: Rectangle {
|
||||
width: parent.width
|
||||
height: 30
|
||||
color: "#FFFF88"
|
||||
y: ListView.view.currentItem.y
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
Layout.preferredWidth: 500
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
source: navListView.currentIndex > -1 ? navListModel.get(navListView.currentIndex).qmlSource : '404.qml'
|
||||
}
|
||||
}
|
||||
@ -8,6 +8,6 @@ Label {
|
||||
anchors.topMargin: 7
|
||||
anchors.bottomMargin: 10
|
||||
|
||||
font.pointSize: 12
|
||||
font.pointSize: 14
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
56
ui/qmls/DatabaseChecker/DatabaseFileCreator.qml
Normal file
56
ui/qmls/DatabaseChecker/DatabaseFileCreator.qml
Normal file
@ -0,0 +1,56 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import "../Components"
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
signal confirm
|
||||
|
||||
required property url directoryUrl
|
||||
required property string filename
|
||||
|
||||
GridLayout {
|
||||
columns: 2
|
||||
|
||||
Label {
|
||||
text: "Directory"
|
||||
}
|
||||
|
||||
DirectorySelector {
|
||||
Layout.fillWidth: true
|
||||
|
||||
directoryUrl: root.directoryUrl
|
||||
onDirectoryUrlChanged: {
|
||||
root.directoryUrl = this.directoryUrl;
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
text: "Filename"
|
||||
}
|
||||
|
||||
TextField {
|
||||
Layout.fillWidth: true
|
||||
|
||||
text: root.filename
|
||||
placeholderText: 'Please enter…'
|
||||
onEditingFinished: {
|
||||
root.filename = this.text;
|
||||
}
|
||||
onAccepted: {
|
||||
confirmButton.click();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
id: confirmButton
|
||||
Layout.alignment: Qt.AlignRight
|
||||
|
||||
text: 'Confirm'
|
||||
onClicked: root.confirm()
|
||||
}
|
||||
}
|
||||
31
ui/qmls/DatabaseChecker/Dialog_ConfirmConnection.qml
Normal file
31
ui/qmls/DatabaseChecker/Dialog_ConfirmConnection.qml
Normal file
@ -0,0 +1,31 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
Dialog {
|
||||
id: root
|
||||
|
||||
required property string databaseUrl
|
||||
|
||||
padding: 10
|
||||
|
||||
title: qsTr('Confirm Database Connection')
|
||||
standardButtons: Dialog.Ok | Dialog.Cancel
|
||||
modal: true
|
||||
Overlay.modal: Rectangle {
|
||||
color: Qt.alpha('gray', 0.2)
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Label {
|
||||
text: 'The connection below will be saved to settings. Confirm?'
|
||||
}
|
||||
|
||||
Pane {
|
||||
Label {
|
||||
id: databaseUrlLabel
|
||||
text: root.databaseUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
34
ui/qmls/DatabaseChecker/Dialog_Error.qml
Normal file
34
ui/qmls/DatabaseChecker/Dialog_Error.qml
Normal file
@ -0,0 +1,34 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
Dialog {
|
||||
id: root
|
||||
|
||||
required property string errorTitle
|
||||
required property string errorMessage
|
||||
|
||||
padding: 10
|
||||
|
||||
title: qsTr('Error')
|
||||
standardButtons: Dialog.Ok
|
||||
modal: true
|
||||
Overlay.modal: Rectangle {
|
||||
color: Qt.alpha('darkgray', 0.5)
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 5
|
||||
|
||||
Label {
|
||||
font.pointSize: 12
|
||||
font.bold: true
|
||||
|
||||
text: root.errorTitle
|
||||
}
|
||||
|
||||
Label {
|
||||
text: root.errorMessage
|
||||
}
|
||||
}
|
||||
}
|
||||
13
ui/qmls/DatabaseChecker/HorizontalDivider.qml
Normal file
13
ui/qmls/DatabaseChecker/HorizontalDivider.qml
Normal file
@ -0,0 +1,13 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 1
|
||||
|
||||
Layout.topMargin: 5
|
||||
Layout.bottomMargin: 5
|
||||
|
||||
color: "lightgray"
|
||||
opacity: 0.5
|
||||
}
|
||||
124
ui/qmls/DatabaseChecker/Index.qml
Normal file
124
ui/qmls/DatabaseChecker/Index.qml
Normal file
@ -0,0 +1,124 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import internal.ui.vm 1.0
|
||||
import "../Components"
|
||||
|
||||
Page {
|
||||
id: root
|
||||
|
||||
property bool showConfirmConnectionDialog: false
|
||||
|
||||
signal ready
|
||||
|
||||
function confirm(source: string): void {
|
||||
if (!source)
|
||||
throw new Error("source is required");
|
||||
|
||||
const shouldConfirm = (source === 'continueButton' && vm.canConfirmSilently) || source === 'dialog';
|
||||
|
||||
if (shouldConfirm) {
|
||||
vm.confirmCurrentConnection();
|
||||
root.ready();
|
||||
} else {
|
||||
root.showConfirmConnectionDialog = true;
|
||||
}
|
||||
}
|
||||
|
||||
DatabaseInitViewModel {
|
||||
id: vm
|
||||
|
||||
onSelectFileUrlChanged: {
|
||||
selectOrCreate.selectFileUrl = this.selectFileUrl;
|
||||
}
|
||||
onDirectoryUrlChanged: {
|
||||
selectOrCreate.directoryUrl = this.directoryUrl;
|
||||
}
|
||||
onFilenameChanged: {
|
||||
selectOrCreate.filename = this.filename;
|
||||
}
|
||||
onDatabaseUrlChanged: {
|
||||
confirmConnectionDialog.databaseUrl = this.databaseUrl;
|
||||
}
|
||||
onDatabaseInfoChanged: {
|
||||
databaseInfo.info = this.databaseInfo;
|
||||
}
|
||||
onCanContinueChanged: {
|
||||
continueButton.enabled = this.canContinue;
|
||||
}
|
||||
}
|
||||
|
||||
Page {
|
||||
padding: 10
|
||||
anchors.fill: parent
|
||||
|
||||
Dialog_ConfirmConnection {
|
||||
id: confirmConnectionDialog
|
||||
|
||||
anchors.centerIn: parent
|
||||
visible: root.showConfirmConnectionDialog
|
||||
|
||||
onAccepted: root.confirm('dialog')
|
||||
onClosed: root.showConfirmConnectionDialog = false
|
||||
|
||||
databaseUrl: vm.databaseUrl
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
width: parent.width
|
||||
spacing: 2
|
||||
|
||||
SectionTitle {
|
||||
text: qsTr('Select or Create Database')
|
||||
}
|
||||
|
||||
Section_SelectOrCreate {
|
||||
id: selectOrCreate
|
||||
selectFileUrl: vm.selectFileUrl
|
||||
createDirectoryUrl: vm.directoryUrl
|
||||
createFilename: vm.filename
|
||||
|
||||
onUiModeChanged: uiMode => vm.uiMode = uiMode
|
||||
onSelectFileUrlChanged: {
|
||||
vm.selectFileUrl = selectFileUrl;
|
||||
}
|
||||
onCreateDirectoryUrlChanged: {
|
||||
vm.directoryUrl = createDirectoryUrl;
|
||||
}
|
||||
onCreateFilenameChanged: {
|
||||
vm.filename = createFilename;
|
||||
}
|
||||
onConfirmCreate: {
|
||||
vm.loadDatabaseInfo();
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalDivider {}
|
||||
|
||||
SectionTitle {
|
||||
text: 'Database Status'
|
||||
}
|
||||
|
||||
Section_DatabaseInfo {
|
||||
id: databaseInfo
|
||||
info: vm.databaseInfo
|
||||
}
|
||||
}
|
||||
|
||||
footer: Pane {
|
||||
padding: 5
|
||||
RowLayout {
|
||||
Button {
|
||||
id: continueButton
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
text: qsTr('Continue')
|
||||
enabled: vm.canContinue
|
||||
|
||||
onClicked: root.confirm('continueButton')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
138
ui/qmls/DatabaseChecker/Section_DatabaseInfo.qml
Normal file
138
ui/qmls/DatabaseChecker/Section_DatabaseInfo.qml
Normal file
@ -0,0 +1,138 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
property var info: {
|
||||
'url': undefined,
|
||||
'error': {
|
||||
'title': undefined,
|
||||
'message': undefined
|
||||
},
|
||||
'initialized': undefined,
|
||||
'version': undefined
|
||||
}
|
||||
property bool showErrorDialog: false
|
||||
|
||||
function hasError(): bool {
|
||||
return info?.error?.title !== undefined || info?.error?.message !== undefined;
|
||||
}
|
||||
|
||||
function displayText(value): string {
|
||||
return value ?? '-';
|
||||
}
|
||||
|
||||
function displayBool(value): string {
|
||||
if (value === undefined)
|
||||
return '-';
|
||||
// TODO: color success & error
|
||||
return value ? `<font color="lightgreen">Yes</font>` : `<font color="lightpink">No</font>`;
|
||||
}
|
||||
|
||||
component LabelLabel: Label {
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignBaseline
|
||||
font.pointSize: 10
|
||||
}
|
||||
|
||||
SystemPalette {
|
||||
id: palette
|
||||
}
|
||||
|
||||
Dialog_Error {
|
||||
parent: Overlay.overlay
|
||||
|
||||
anchors.centerIn: parent
|
||||
visible: root.hasError() && root.showErrorDialog
|
||||
|
||||
errorTitle: root.displayText(root.info?.error?.title)
|
||||
errorMessage: root.displayText(root.info?.error?.message)
|
||||
|
||||
onClosed: root.showErrorDialog = false
|
||||
}
|
||||
|
||||
Pane {
|
||||
clip: true
|
||||
background: Rectangle {
|
||||
color: Qt.darker(Qt.alpha(palette.window, 0.9), 0.2)
|
||||
}
|
||||
|
||||
// Layout.preferredHeight: root.hasError() ? this.implicitHeight : 0
|
||||
Layout.preferredHeight: 0
|
||||
Behavior on Layout.preferredHeight {
|
||||
PropertyAnimation {
|
||||
duration: 300
|
||||
easing.type: Easing.InOutCubic
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
|
||||
Label {
|
||||
font.bold: true
|
||||
text: root.displayText(root.info.error?.title)
|
||||
}
|
||||
|
||||
Label {
|
||||
text: root.displayText(root.info.error?.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GridLayout {
|
||||
columns: 2
|
||||
columnSpacing: 10
|
||||
|
||||
LabelLabel {
|
||||
text: 'Connection'
|
||||
}
|
||||
|
||||
Label {
|
||||
text: root.info.url
|
||||
}
|
||||
|
||||
LabelLabel {
|
||||
text: 'Initialized'
|
||||
}
|
||||
|
||||
Label {
|
||||
text: root.displayBool(root.info?.initialized)
|
||||
}
|
||||
|
||||
LabelLabel {
|
||||
text: 'Version'
|
||||
}
|
||||
|
||||
Label {
|
||||
text: root.displayText(root.info?.version)
|
||||
}
|
||||
|
||||
Column {
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignBaseline
|
||||
|
||||
LabelLabel {
|
||||
text: 'Error'
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
Layout.preferredWidth: root.hasError() ? this.implicitWidth : 0
|
||||
Behavior on Layout.preferredWidth {
|
||||
PropertyAnimation {
|
||||
duration: 300
|
||||
easing.type: Easing.InOutCubic
|
||||
}
|
||||
}
|
||||
|
||||
text: '[?]'
|
||||
onClicked: root.showErrorDialog = true
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.alignment: Qt.AlignBaseline
|
||||
text: root.displayText(root.info?.error?.title)
|
||||
}
|
||||
}
|
||||
}
|
||||
69
ui/qmls/DatabaseChecker/Section_SelectOrCreate.qml
Normal file
69
ui/qmls/DatabaseChecker/Section_SelectOrCreate.qml
Normal file
@ -0,0 +1,69 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import "../Components"
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
property alias selectFileUrl: fileSelector.url
|
||||
property alias createDirectoryUrl: fileCreator.directoryUrl
|
||||
property alias createFilename: fileCreator.filename
|
||||
|
||||
signal uiModeChanged(string value)
|
||||
signal confirmCreate
|
||||
|
||||
ListModel {
|
||||
id: uiModeModel
|
||||
ListElement {
|
||||
value: 'select'
|
||||
}
|
||||
ListElement {
|
||||
value: 'create'
|
||||
}
|
||||
}
|
||||
|
||||
TabBar {
|
||||
id: tabBar
|
||||
Layout.fillWidth: true
|
||||
|
||||
TabButton {
|
||||
text: qsTr("Select Existing")
|
||||
width: implicitWidth + 10
|
||||
}
|
||||
TabButton {
|
||||
text: qsTr("Create New File")
|
||||
width: implicitWidth + 10
|
||||
}
|
||||
|
||||
onCurrentIndexChanged: {
|
||||
const idx = this.currentIndex;
|
||||
root.uiModeChanged(uiModeModel.get(idx).value);
|
||||
}
|
||||
}
|
||||
|
||||
StackLayout {
|
||||
currentIndex: tabBar.currentIndex
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: children[currentIndex].height
|
||||
Behavior on Layout.preferredHeight {
|
||||
PropertyAnimation {
|
||||
duration: 300
|
||||
easing.type: Easing.InOutCubic
|
||||
}
|
||||
}
|
||||
clip: true
|
||||
|
||||
FileSelector {
|
||||
id: fileSelector
|
||||
}
|
||||
|
||||
DatabaseFileCreator {
|
||||
id: fileCreator
|
||||
|
||||
onConfirm: root.confirmCreate()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,136 +0,0 @@
|
||||
import logging
|
||||
import traceback
|
||||
from pathlib import Path
|
||||
|
||||
from arcaea_offline.database import Database
|
||||
from PySide6.QtCore import QCoreApplication, QDir, QFileInfo, QSysInfo, Qt, QUrl, Slot
|
||||
from PySide6.QtWidgets import QDialog, QMessageBox
|
||||
|
||||
from core.database import DatabaseInitCheckResult, check_db_init, create_engine
|
||||
from core.settings import SettingsKeys, settings
|
||||
|
||||
from .databaseChecker_ui import Ui_DatabaseChecker
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DatabaseChecker(Ui_DatabaseChecker, QDialog):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
self.setWindowFlag(Qt.WindowType.WindowMinimizeButtonHint, False)
|
||||
self.setWindowFlag(Qt.WindowType.WindowMaximizeButtonHint, False)
|
||||
self.setWindowFlag(Qt.WindowType.WindowCloseButtonHint, True)
|
||||
self.dbDirSelector.setMode(self.dbDirSelector.getExistingDirectory)
|
||||
|
||||
self.confirmDbByExistingSettings = False
|
||||
if dbUrlString := settings.stringValue(SettingsKeys.General.DatabaseUrl):
|
||||
dbFileUrl = QUrl(dbUrlString.replace("sqlite://", "file://"))
|
||||
dbFileInfo = QFileInfo(dbFileUrl.toLocalFile())
|
||||
if dbFileInfo.exists():
|
||||
self.dbDirSelector.selectFile(dbFileInfo.path())
|
||||
self.dbFilenameLineEdit.setText(dbFileInfo.fileName())
|
||||
self.confirmDbByExistingSettings = True
|
||||
self.confirmDbPathButton.click()
|
||||
else:
|
||||
self.dbDirSelector.selectFile(QDir.currentPath())
|
||||
self.dbFilenameLineEdit.setText("arcaea_offline.db")
|
||||
else:
|
||||
self.dbDirSelector.selectFile(QDir.currentPath())
|
||||
self.dbFilenameLineEdit.setText("arcaea_offline.db")
|
||||
|
||||
def writeDatabaseUrlToSettings(self, databaseUrl: str):
|
||||
settings.setValue(SettingsKeys.General.DatabaseUrl, databaseUrl)
|
||||
|
||||
def dbPath(self):
|
||||
return QDir(self.dbDirSelector.selectedFiles()[0])
|
||||
|
||||
def dbFileInfo(self):
|
||||
return QFileInfo(
|
||||
QDir.cleanPath(
|
||||
self.dbPath().absoluteFilePath(self.dbFilenameLineEdit.text())
|
||||
)
|
||||
)
|
||||
|
||||
def dbFileUrl(self):
|
||||
return QUrl.fromLocalFile(self.dbFileInfo().filePath())
|
||||
|
||||
def dbSqliteUrl(self):
|
||||
kernelType = QSysInfo.kernelType()
|
||||
# the slash count varies depending on the kernel
|
||||
# https://docs.sqlalchemy.org/en/20/core/engines.html#sqlite
|
||||
if kernelType == "winnt":
|
||||
return QUrl(self.dbFileUrl().toString().replace("file://", "sqlite://"))
|
||||
else:
|
||||
return QUrl(self.dbFileUrl().toString().replace("file://", "sqlite:///"))
|
||||
|
||||
def confirmDb(self) -> DatabaseInitCheckResult:
|
||||
dbFileInfo = self.dbFileInfo()
|
||||
dbPath = Path(dbFileInfo.absoluteFilePath())
|
||||
|
||||
return check_db_init(dbPath)
|
||||
|
||||
def updateLabels(self):
|
||||
result = self.confirmDb()
|
||||
try:
|
||||
db = Database()
|
||||
version = db.version()
|
||||
initted = db.check_init()
|
||||
self.dbVersionLabel.setText(str(version))
|
||||
self.dbCheckConnLabel.setText(
|
||||
'<font color="green">OK</font>'
|
||||
if initted
|
||||
else '<font color="red">Not initted</font>'
|
||||
)
|
||||
self.continueButton.setEnabled(initted)
|
||||
except Exception as e:
|
||||
self.dbVersionLabel.setText("-")
|
||||
self.dbCheckConnLabel.setText(
|
||||
f'<font color="red">Error: {e}</font>'
|
||||
if result & DatabaseInitCheckResult.FILE_EXISTS
|
||||
else "-"
|
||||
)
|
||||
self.continueButton.setEnabled(False)
|
||||
|
||||
@Slot()
|
||||
def on_confirmDbPathButton_clicked(self):
|
||||
dbSqliteUrl = self.dbSqliteUrl()
|
||||
self.writeDatabaseUrlToSettings(dbSqliteUrl.toString())
|
||||
|
||||
result = self.confirmDb()
|
||||
if result & DatabaseInitCheckResult.INITIALIZED:
|
||||
if not self.confirmDbByExistingSettings:
|
||||
self.writeDatabaseUrlToSettings(dbSqliteUrl.toString())
|
||||
elif result & DatabaseInitCheckResult.FILE_EXISTS:
|
||||
confirm_try_init = QMessageBox.question(
|
||||
self,
|
||||
None,
|
||||
QCoreApplication.translate("DatabaseChecker", "dialog.tryInitExistingDatabase"),
|
||||
) # fmt: skip
|
||||
if confirm_try_init == QMessageBox.StandardButton.Yes:
|
||||
try:
|
||||
Database().init(checkfirst=True)
|
||||
except Exception as e:
|
||||
logger.exception("Error while initializing an existing database")
|
||||
QMessageBox.critical(
|
||||
self, None, "\n".join(traceback.format_exception(e))
|
||||
)
|
||||
else:
|
||||
confirm_new_database = QMessageBox.question(
|
||||
self,
|
||||
None,
|
||||
QCoreApplication.translate("DatabaseChecker", "dialog.confirmNewDatabase"),
|
||||
) # fmt: skip
|
||||
if confirm_new_database == QMessageBox.StandardButton.Yes:
|
||||
db = Database(create_engine(dbSqliteUrl))
|
||||
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()
|
||||
@ -1,154 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>DatabaseChecker</class>
|
||||
<widget class="QWidget" name="DatabaseChecker">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>350</width>
|
||||
<height>250</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string notr="true">DatabaseChecker</string>
|
||||
</property>
|
||||
<layout class="QFormLayout" name="formLayout">
|
||||
<property name="labelAlignment">
|
||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>dbPathLabel</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="FileSelector" name="dbDirSelector" native="true"/>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="text">
|
||||
<string>dbFilenameLabel</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QLineEdit" name="dbFilenameLineEdit"/>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="confirmDbPathButton">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>confirmDbPathButton</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="4" column="1">
|
||||
<widget class="QLabel" name="dbVersionLabel">
|
||||
<property name="text">
|
||||
<string notr="true">-</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="1">
|
||||
<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 row="7" column="0">
|
||||
<widget class="QLabel" name="label_5">
|
||||
<property name="text">
|
||||
<string>dbCheckConnLabel</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="7" column="1">
|
||||
<widget class="QLabel" name="dbCheckConnLabel">
|
||||
<property name="text">
|
||||
<string notr="true">...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="8" column="0" colspan="2">
|
||||
<widget class="QPushButton" name="continueButton">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>continueButton</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>dbVersionLabel</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0" colspan="2">
|
||||
<widget class="Line" name="line">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</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>
|
||||
<customwidget>
|
||||
<class>FileSelector</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>ui.implements.components.fileSelector</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@ -1,138 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
################################################################################
|
||||
## Form generated from reading UI file 'databaseChecker.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 (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(u"DatabaseChecker")
|
||||
DatabaseChecker.resize(350, 250)
|
||||
DatabaseChecker.setWindowTitle(u"DatabaseChecker")
|
||||
self.formLayout = QFormLayout(DatabaseChecker)
|
||||
self.formLayout.setObjectName(u"formLayout")
|
||||
self.formLayout.setLabelAlignment(Qt.AlignRight|Qt.AlignTrailing|Qt.AlignVCenter)
|
||||
self.label = QLabel(DatabaseChecker)
|
||||
self.label.setObjectName(u"label")
|
||||
|
||||
self.formLayout.setWidget(0, QFormLayout.LabelRole, self.label)
|
||||
|
||||
self.dbDirSelector = FileSelector(DatabaseChecker)
|
||||
self.dbDirSelector.setObjectName(u"dbDirSelector")
|
||||
|
||||
self.formLayout.setWidget(0, QFormLayout.FieldRole, self.dbDirSelector)
|
||||
|
||||
self.label_3 = QLabel(DatabaseChecker)
|
||||
self.label_3.setObjectName(u"label_3")
|
||||
|
||||
self.formLayout.setWidget(1, QFormLayout.LabelRole, self.label_3)
|
||||
|
||||
self.dbFilenameLineEdit = QLineEdit(DatabaseChecker)
|
||||
self.dbFilenameLineEdit.setObjectName(u"dbFilenameLineEdit")
|
||||
|
||||
self.formLayout.setWidget(1, QFormLayout.FieldRole, self.dbFilenameLineEdit)
|
||||
|
||||
self.horizontalLayout = QHBoxLayout()
|
||||
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(u"confirmDbPathButton")
|
||||
sizePolicy = QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
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(u"dbVersionLabel")
|
||||
self.dbVersionLabel.setText(u"-")
|
||||
|
||||
self.formLayout.setWidget(4, QFormLayout.FieldRole, self.dbVersionLabel)
|
||||
|
||||
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(u"label_5")
|
||||
|
||||
self.formLayout.setWidget(7, QFormLayout.LabelRole, self.label_5)
|
||||
|
||||
self.dbCheckConnLabel = QLabel(DatabaseChecker)
|
||||
self.dbCheckConnLabel.setObjectName(u"dbCheckConnLabel")
|
||||
self.dbCheckConnLabel.setText(u"...")
|
||||
|
||||
self.formLayout.setWidget(7, QFormLayout.FieldRole, self.dbCheckConnLabel)
|
||||
|
||||
self.continueButton = QPushButton(DatabaseChecker)
|
||||
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(u"label_2")
|
||||
|
||||
self.formLayout.setWidget(4, QFormLayout.LabelRole, self.label_2)
|
||||
|
||||
self.line = QFrame(DatabaseChecker)
|
||||
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", 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
|
||||
|
||||
1
ui/utils/common.py
Normal file
1
ui/utils/common.py
Normal file
@ -0,0 +1 @@
|
||||
UTILS_QML_IMPORT_NAME = "internal.ui.utils"
|
||||
48
ui/utils/url.py
Normal file
48
ui/utils/url.py
Normal file
@ -0,0 +1,48 @@
|
||||
from pathlib import Path
|
||||
|
||||
from PySide6.QtCore import QFileInfo, QObject, QUrl, Slot
|
||||
from PySide6.QtQml import QmlElement, QmlSingleton
|
||||
|
||||
from .common import UTILS_QML_IMPORT_NAME
|
||||
|
||||
QML_IMPORT_NAME = UTILS_QML_IMPORT_NAME
|
||||
QML_IMPORT_MAJOR_VERSION = 1
|
||||
QML_IMPORT_MINOR_VERSION = 0
|
||||
|
||||
|
||||
@QmlElement
|
||||
@QmlSingleton
|
||||
class UrlUtils(QObject):
|
||||
@Slot(str, result=bool)
|
||||
@staticmethod
|
||||
def isDir(url: str):
|
||||
return QFileInfo(QUrl(url).toLocalFile()).isDir()
|
||||
|
||||
@Slot(str, result=bool)
|
||||
@staticmethod
|
||||
def isFile(url: str):
|
||||
return QFileInfo(QUrl(url).toLocalFile()).isFile()
|
||||
|
||||
@Slot(str, result=str)
|
||||
@staticmethod
|
||||
def stem(url: str):
|
||||
return Path(QUrl(url).toLocalFile()).stem
|
||||
|
||||
@Slot(str, result=str)
|
||||
@staticmethod
|
||||
def name(url: str):
|
||||
return Path(QUrl(url).toLocalFile()).name
|
||||
|
||||
@Slot(str, result=QUrl)
|
||||
@staticmethod
|
||||
def parent(url: str):
|
||||
return QUrl.fromLocalFile(Path(QUrl(url).toLocalFile()).parent)
|
||||
|
||||
|
||||
@QmlElement
|
||||
@QmlSingleton
|
||||
class UrlFormatUtils(QObject):
|
||||
@Slot(str, result=str)
|
||||
@staticmethod
|
||||
def toLocalFile(url: str):
|
||||
return QUrl(url).toLocalFile()
|
||||
2
ui/viewmodels/__init__.py
Normal file
2
ui/viewmodels/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
from .databaseInit import DatabaseInitViewModel
|
||||
from .overview import OverviewViewModel
|
||||
1
ui/viewmodels/common.py
Normal file
1
ui/viewmodels/common.py
Normal file
@ -0,0 +1 @@
|
||||
VM_QML_IMPORT_NAME = "internal.ui.vm"
|
||||
368
ui/viewmodels/databaseInit.py
Normal file
368
ui/viewmodels/databaseInit.py
Normal file
@ -0,0 +1,368 @@
|
||||
import dataclasses
|
||||
import logging
|
||||
from enum import StrEnum
|
||||
from pathlib import Path
|
||||
|
||||
from arcaea_offline.models import (
|
||||
CalculatedPotential,
|
||||
Chart,
|
||||
ConfigBase,
|
||||
ScoreBest,
|
||||
ScoreCalculated,
|
||||
ScoresBase,
|
||||
ScoresViewBase,
|
||||
SongsBase,
|
||||
SongsViewBase,
|
||||
)
|
||||
from arcaea_offline.models import (
|
||||
Property as AoProperty,
|
||||
)
|
||||
from PySide6.QtCore import Property, QObject, QUrl, Signal, Slot
|
||||
from PySide6.QtQml import QmlElement
|
||||
from sqlalchemy import inspect, select
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from core.database import create_engine, db_path_to_sqlite_url, sqlite_url_to_db_path
|
||||
from core.settings import SettingsKeys, settings
|
||||
from core.settings.values import GeneralDatabaseType
|
||||
|
||||
from .common import VM_QML_IMPORT_NAME
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
QML_IMPORT_NAME = VM_QML_IMPORT_NAME
|
||||
QML_IMPORT_MAJOR_VERSION = 1
|
||||
QML_IMPORT_MINOR_VERSION = 0
|
||||
|
||||
|
||||
class _FmwiwsDatabase:
|
||||
"""Fuck Me Why I Wrote Singleton Database"""
|
||||
|
||||
def __init__(self, url: str):
|
||||
self.engine = create_engine(url)
|
||||
|
||||
def init(self, *, checkfirst: bool = True):
|
||||
# create tables & views
|
||||
if checkfirst:
|
||||
# > https://github.com/kvesteri/sqlalchemy-utils/issues/396
|
||||
# > view.create_view() causes DuplicateTableError on
|
||||
# > Base.metadata.create_all(checkfirst=True)
|
||||
# so if `checkfirst` is True, drop these views before creating
|
||||
SongsViewBase.metadata.drop_all(self.engine)
|
||||
ScoresViewBase.metadata.drop_all(self.engine)
|
||||
|
||||
SongsBase.metadata.create_all(self.engine, checkfirst=checkfirst)
|
||||
SongsViewBase.metadata.create_all(self.engine)
|
||||
ScoresBase.metadata.create_all(self.engine, checkfirst=checkfirst)
|
||||
ScoresViewBase.metadata.create_all(self.engine)
|
||||
ConfigBase.metadata.create_all(self.engine, checkfirst=checkfirst)
|
||||
|
||||
version_property = AoProperty(key="version", value="4")
|
||||
with Session(self.engine) as session:
|
||||
session.merge(version_property)
|
||||
session.commit()
|
||||
|
||||
def is_initialized(self):
|
||||
expect_tables = (
|
||||
list(SongsBase.metadata.tables.keys())
|
||||
+ list(ScoresBase.metadata.tables.keys())
|
||||
+ list(ConfigBase.metadata.tables.keys())
|
||||
+ [
|
||||
Chart.__tablename__,
|
||||
ScoreCalculated.__tablename__,
|
||||
ScoreBest.__tablename__,
|
||||
CalculatedPotential.__tablename__,
|
||||
]
|
||||
)
|
||||
|
||||
return all(inspect(self.engine).has_table(t) for t in expect_tables)
|
||||
|
||||
def version(self):
|
||||
with Session(self.engine) as session:
|
||||
stmt = select(AoProperty.value).where(AoProperty.key == "version")
|
||||
result = session.scalar(stmt)
|
||||
|
||||
return None if result is None else int(result)
|
||||
|
||||
|
||||
class _UiMode(StrEnum):
|
||||
SELECT = "select"
|
||||
CREATE = "create"
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class DatabaseInfo:
|
||||
url: str
|
||||
error: Exception | None = None
|
||||
initialized: bool | None = None
|
||||
version: int | None = None
|
||||
|
||||
@property
|
||||
def error_dict(self):
|
||||
e = self.error
|
||||
|
||||
if e is None:
|
||||
return None
|
||||
|
||||
return {
|
||||
"title": e.__class__.__name__,
|
||||
"message": str(e),
|
||||
}
|
||||
|
||||
def to_qml_dict(self):
|
||||
return {
|
||||
"url": self.url,
|
||||
"error": self.error_dict,
|
||||
"initialized": self.initialized,
|
||||
"version": self.version,
|
||||
}
|
||||
|
||||
|
||||
@QmlElement
|
||||
class DatabaseInitViewModel(QObject):
|
||||
_void = Signal()
|
||||
uiModeChanged = Signal()
|
||||
selectFileUrlChanged = Signal()
|
||||
directoryUrlChanged = Signal()
|
||||
filenameChanged = Signal()
|
||||
databaseUrlChanged = Signal()
|
||||
databaseInfoChanged = Signal()
|
||||
canContinueChanged = Signal()
|
||||
canConfirmSilentlyChanged = Signal()
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self._selectFileUrl: QUrl | None = None
|
||||
self._directoryUrl: QUrl = QUrl()
|
||||
self._filename: str = ""
|
||||
self._databaseInfo: DatabaseInfo | None = None
|
||||
|
||||
self._uiMode = _UiMode.SELECT
|
||||
|
||||
self.directoryUrlChanged.connect(lambda: self.databaseUrlChanged.emit())
|
||||
self.filenameChanged.connect(lambda: self.databaseUrlChanged.emit())
|
||||
self.selectFileUrlChanged.connect(self.databaseUrlChanged)
|
||||
self.databaseInfoChanged.connect(self.canContinueChanged)
|
||||
|
||||
self.databaseUrlChanged.connect(self.onDatabaseUrlChanged)
|
||||
|
||||
self._loadSettings()
|
||||
|
||||
def onDatabaseUrlChanged(self):
|
||||
self.loadDatabaseInfo()
|
||||
|
||||
@property
|
||||
def _settingsDatabaseFile(self) -> Path | None:
|
||||
# TODO: process database type when available
|
||||
if (
|
||||
settings.stringValue(SettingsKeys.General.DatabaseType)
|
||||
!= GeneralDatabaseType.FILE
|
||||
):
|
||||
return None
|
||||
|
||||
file = settings.stringValue(SettingsKeys.General.DatabaseConn)
|
||||
|
||||
if not file:
|
||||
logger.debug("No database file specified in settings")
|
||||
return
|
||||
|
||||
filepath = Path(file)
|
||||
if not filepath.exists():
|
||||
logger.warning("Cannot find database file: %s", file)
|
||||
return
|
||||
|
||||
return filepath
|
||||
|
||||
def _loadSettings(self) -> None:
|
||||
fileUrl = self._settingsDatabaseFile
|
||||
if fileUrl is None:
|
||||
return
|
||||
logger.info("Loading database from settings: %s", fileUrl)
|
||||
|
||||
self.setUiMode(_UiMode.SELECT)
|
||||
self.setSelectFileUrl(fileUrl)
|
||||
self.loadDatabaseInfo()
|
||||
|
||||
def _makeDatabaseInfo(self, dbUrl: str):
|
||||
info = DatabaseInfo(url=dbUrl)
|
||||
|
||||
path = sqlite_url_to_db_path(dbUrl)
|
||||
if not path.exists():
|
||||
e = FileNotFoundError()
|
||||
e.strerror = f"{path} does not exist"
|
||||
info.error = e
|
||||
return info
|
||||
|
||||
try:
|
||||
db = _FmwiwsDatabase(dbUrl)
|
||||
info.initialized = db.is_initialized()
|
||||
info.version = db.version()
|
||||
except SQLAlchemyError as e:
|
||||
logger.exception("Error loading database info")
|
||||
info.error = e
|
||||
|
||||
logger.debug("Database info for %r: %r", dbUrl, info)
|
||||
return info
|
||||
|
||||
@Slot()
|
||||
def loadDatabaseInfo(self):
|
||||
dbUrl = self.getDatabaseUrl()
|
||||
logger.info("Loading database info: %s", dbUrl)
|
||||
if dbUrl is None:
|
||||
logger.warning("Database URL is None")
|
||||
return
|
||||
|
||||
self._databaseInfo = self._makeDatabaseInfo(dbUrl.toString())
|
||||
self.databaseInfoChanged.emit()
|
||||
|
||||
@Slot(str)
|
||||
def createFile(self, dbUrl: str):
|
||||
file = sqlite_url_to_db_path(dbUrl)
|
||||
|
||||
if file.exists():
|
||||
logger.warning(
|
||||
"Attempted to create an existing file, check UI logic? (%s)", file
|
||||
)
|
||||
return
|
||||
|
||||
file.touch(mode=0o644)
|
||||
logger.info("Created file %s", file)
|
||||
|
||||
@Slot()
|
||||
def confirmCurrentConnection(self):
|
||||
dbInfo = self._databaseInfo
|
||||
if dbInfo is None:
|
||||
logger.warning("Current database info is None, ignoring")
|
||||
return
|
||||
|
||||
settings.setValue(
|
||||
SettingsKeys.General.DatabaseType,
|
||||
GeneralDatabaseType.FILE.value,
|
||||
)
|
||||
settings.setValue(
|
||||
SettingsKeys.General.DatabaseConn,
|
||||
str(sqlite_url_to_db_path(dbInfo.url).resolve().as_posix()),
|
||||
)
|
||||
|
||||
@Slot(str)
|
||||
def initialize(self, dbUrl: str):
|
||||
try:
|
||||
db = _FmwiwsDatabase(dbUrl)
|
||||
db.init()
|
||||
except SQLAlchemyError:
|
||||
logger.exception("Error initializing database %s", dbUrl)
|
||||
|
||||
# region properties
|
||||
def getUiMode(self):
|
||||
return self._uiMode.value
|
||||
|
||||
def setUiMode(self, mode: str | _UiMode):
|
||||
if isinstance(mode, _UiMode):
|
||||
self._uiMode = mode
|
||||
elif isinstance(mode, str):
|
||||
try:
|
||||
self._uiMode = _UiMode(mode)
|
||||
except ValueError:
|
||||
logger.warning("Invalid UI mode: %s", mode)
|
||||
|
||||
self.uiModeChanged.emit()
|
||||
|
||||
def getSelectFileUrl(self):
|
||||
return self._selectFileUrl
|
||||
|
||||
def setSelectFileUrl(self, value: Path | QUrl | None):
|
||||
if isinstance(value, Path):
|
||||
value = QUrl.fromLocalFile(value.as_posix())
|
||||
|
||||
self._selectFileUrl = value
|
||||
self.selectFileUrlChanged.emit()
|
||||
|
||||
def getDirectoryUrl(self):
|
||||
return self._directoryUrl
|
||||
|
||||
def setDirectoryUrl(self, value: QUrl | None):
|
||||
self._directoryUrl = value or QUrl()
|
||||
self.directoryUrlChanged.emit()
|
||||
|
||||
def getFilename(self):
|
||||
return self._filename
|
||||
|
||||
def setFilename(self, value: str | None):
|
||||
self._filename = value or ""
|
||||
self.filenameChanged.emit()
|
||||
|
||||
def getDatabaseUrl(self):
|
||||
if self._uiMode == _UiMode.SELECT:
|
||||
fileUrl = self.getSelectFileUrl()
|
||||
if fileUrl is None:
|
||||
return None
|
||||
return db_path_to_sqlite_url(Path(fileUrl.toLocalFile()))
|
||||
|
||||
directoryUrl = self.getDirectoryUrl()
|
||||
filename = self.getFilename()
|
||||
|
||||
databasePath = Path(directoryUrl.toLocalFile()) / filename
|
||||
databaseUrl = db_path_to_sqlite_url(databasePath)
|
||||
|
||||
return databaseUrl
|
||||
|
||||
def getDatabaseInfo(self):
|
||||
if self._databaseInfo is None:
|
||||
return {
|
||||
"url": "",
|
||||
"initialized": None,
|
||||
"version": None,
|
||||
"error": None,
|
||||
}
|
||||
|
||||
return self._databaseInfo.to_qml_dict()
|
||||
|
||||
def getCanContinue(self):
|
||||
return (
|
||||
self._databaseInfo is not None
|
||||
and self._databaseInfo.error is None
|
||||
and self._databaseInfo.version == 4 # noqa: PLR2004
|
||||
and self._databaseInfo.initialized
|
||||
)
|
||||
|
||||
def getCanConfirmSilently(self):
|
||||
"""Whether the user can confirm database connection without a dialog popping up"""
|
||||
if self.getUiMode() != _UiMode.SELECT:
|
||||
return False
|
||||
|
||||
filepath = self._settingsDatabaseFile
|
||||
if filepath is None:
|
||||
return False
|
||||
|
||||
return (
|
||||
self._databaseInfo is not None
|
||||
and self._databaseInfo.error is None
|
||||
and self.getDatabaseUrl() == db_path_to_sqlite_url(filepath)
|
||||
)
|
||||
|
||||
uiMode = Property(str, getUiMode, setUiMode, notify=uiModeChanged)
|
||||
selectFileUrl = Property(
|
||||
QUrl,
|
||||
getSelectFileUrl,
|
||||
setSelectFileUrl,
|
||||
notify=selectFileUrlChanged,
|
||||
)
|
||||
directoryUrl = Property(
|
||||
QUrl,
|
||||
getDirectoryUrl,
|
||||
setDirectoryUrl,
|
||||
notify=directoryUrlChanged,
|
||||
)
|
||||
filename = Property(str, getFilename, setFilename, notify=filenameChanged)
|
||||
databaseUrl = Property(QUrl, getDatabaseUrl, None, notify=databaseUrlChanged)
|
||||
databaseInfo = Property(dict, getDatabaseInfo, None, notify=databaseInfoChanged)
|
||||
canContinue = Property(bool, getCanContinue, None, notify=canContinueChanged)
|
||||
canConfirmSilently = Property(
|
||||
bool,
|
||||
getCanConfirmSilently,
|
||||
None,
|
||||
notify=canConfirmSilentlyChanged,
|
||||
)
|
||||
# endregion
|
||||
21
ui/viewmodels/overview.py
Normal file
21
ui/viewmodels/overview.py
Normal file
@ -0,0 +1,21 @@
|
||||
from PySide6.QtCore import Property, QObject, Signal
|
||||
from PySide6.QtQml import QmlElement
|
||||
|
||||
from .common import VM_QML_IMPORT_NAME
|
||||
|
||||
QML_IMPORT_NAME = VM_QML_IMPORT_NAME
|
||||
QML_IMPORT_MAJOR_VERSION = 1
|
||||
QML_IMPORT_MINOR_VERSION = 0
|
||||
|
||||
|
||||
@QmlElement
|
||||
class OverviewViewModel(QObject):
|
||||
_void = Signal()
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def get_b30(self):
|
||||
return 0
|
||||
|
||||
b30 = Property(float, get_b30, None, notify=_void)
|
||||
Reference in New Issue
Block a user