mirror of
https://github.com/283375/arcaea-offline-pyside-ui.git
synced 2025-11-07 04:52: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):
|
class _General(StrEnum):
|
||||||
Language = "Language"
|
Language = "Language"
|
||||||
DatabaseUrl = "DatabaseUrl"
|
DatabaseType = "DatabaseType"
|
||||||
|
DatabaseConn = "DatabaseConn"
|
||||||
|
|
||||||
|
|
||||||
class _Ocr(StrEnum):
|
class _Ocr(StrEnum):
|
||||||
|
|||||||
@ -1,4 +1,10 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from enum import StrEnum
|
||||||
|
|
||||||
|
|
||||||
|
class GeneralDatabaseType(StrEnum):
|
||||||
|
FILE = "file"
|
||||||
|
URL = "url"
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
|
|||||||
@ -51,6 +51,7 @@ ignore = [
|
|||||||
"N806", # non-lowercase-variable-in-function
|
"N806", # non-lowercase-variable-in-function
|
||||||
"N815", # mixed-case-variable-in-class-scope
|
"N815", # mixed-case-variable-in-class-scope
|
||||||
"N816", # mixed-case-variable-in-global-scope
|
"N816", # mixed-case-variable-in-global-scope
|
||||||
|
"N999", # invalid-module-name
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.pyright]
|
[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.topMargin: 7
|
||||||
anchors.bottomMargin: 10
|
anchors.bottomMargin: 10
|
||||||
|
|
||||||
font.pointSize: 12
|
font.pointSize: 14
|
||||||
font.bold: true
|
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