Compare commits

..

3 commits

9 changed files with 41 additions and 354 deletions

View file

@ -5,7 +5,7 @@
}: }:
python3Packages.buildPythonApplication rec { python3Packages.buildPythonApplication rec {
pname = "leek"; pname = "leek";
version = "0.3"; version = "0.2";
pyproject = true; pyproject = true;
src = ./.; src = ./.;

View file

@ -19,7 +19,7 @@ dependencies = [
"tomlkit", "tomlkit",
"vdf", "vdf",
] ]
requires-python = ">=3.12" requires-python = ">=3.6"
[project.readme] [project.readme]
file = "README.md" file = "README.md"

View file

@ -7,7 +7,6 @@ from PySide6.QtWidgets import QApplication
from PySide6.QtCore import QUrl from PySide6.QtCore import QUrl
from PySide6.QtQml import QQmlApplicationEngine from PySide6.QtQml import QQmlApplicationEngine
from leek.mod_list import QAbstractListModel # noqa: F401, needs to be imported for QML from leek.mod_list import QAbstractListModel # noqa: F401, needs to be imported for QML
from leek.qmod_installer import QModInstaller # noqa: F401, needs to be imported for QML
def main(): def main():

View file

@ -1,118 +0,0 @@
from pathlib import Path
from zipfile import Path as ZipPath
from zipfile import ZipFile, is_zipfile
from leek.utils import GameFinder
class ModInstaller:
__archive_path: Path
__archive_format: str
__is_flat: bool
__is_installed: bool
def __init__(self, mod_path: Path) -> None:
self.__archive_path = mod_path
self.__is_installed = False
if is_zipfile(mod_path):
self.__archive_format = "zip"
archive: ZipFile = ZipFile(mod_path)
path: ZipPath = ZipPath(archive)
# Make sure archive contains a mod
things_in_root: list[ZipPath] = list(path.iterdir())
config_toml: ZipPath
match len(things_in_root):
case 0:
raise EmptyArchiveError
case 1:
config_toml = things_in_root[0] / "config.toml"
if config_toml.exists() and config_toml.is_file():
# Mod can be extracted as is
self.__is_flat = False
self.__check_if_already_installed()
return
case _:
config_toml = path / "config.toml"
if config_toml.exists() and config_toml.is_file():
# Mod needs to be extracted in a folder
self.__is_flat = True
self.__check_if_already_installed()
return
# If we ever get here there's either no mod on the archive, or we failed to find it
raise NoModExceptionError(str(mod_path))
else:
raise UnsupportedArchiveTypeError(str(mod_path))
@property
def is_installed(self) -> bool:
return self.__is_installed
def install(self) -> None:
"""
Install the mod to Project Diva's mod folder
"""
if self.__is_installed:
return
game_path: Path = GameFinder.find()
if self.__is_flat:
mod_folder_name: str = self.__get_mod_folder_name()
ZipFile(self.__archive_path).extractall(
path=str(game_path / "mods" / mod_folder_name)
)
else:
ZipFile(self.__archive_path).extractall(path=str(game_path / "mods"))
self.__is_installed = True
def __get_mod_folder_name(self) -> str:
mod_folder_name: str
if self.__is_flat:
mod_folder_name = self.__archive_path.stem
else:
path: ZipPath = ZipPath(ZipFile(self.__archive_path))
things_in_root: list[ZipPath] = list(path.iterdir())
mod_folder_name = things_in_root[0].stem
return mod_folder_name
def __check_if_already_installed(self) -> None:
installed_mod_path: Path = (
GameFinder.find() / "mods" / self.__get_mod_folder_name()
)
if installed_mod_path.exists():
self.__is_installed = True
class UnsupportedArchiveTypeError(Exception):
"""
Rased when ModInstaller runs into an archive that it can't extract
"""
__message: str
def __init__(self, archive_path: str) -> None:
self.__message = f"Don't know how to unpack archive at {archive_path}"
def __str__(self) -> str:
return f"{self.__message}"
class EmptyArchiveError(Exception):
pass
class NoModExceptionError(Exception):
"""
Raised if the archive does not have a mod inside
"""
__message: str
def __init__(self, archive_path: str) -> None:
self.__message = f"Archive at {archive_path} does not have a mod"
def __str__(self) -> str:
return f"{self.__message}"

View file

@ -1,4 +1,4 @@
from PySide6.QtQml import QmlElement, QmlSingleton from PySide6.QtQml import QmlElement
from PySide6.QtCore import QAbstractListModel, QModelIndex from PySide6.QtCore import QAbstractListModel, QModelIndex
from leek.mod import InvalidModError from leek.mod import InvalidModError
from leek.qmod import QMod from leek.qmod import QMod
@ -12,7 +12,6 @@ QML_IMPORT_MAJOR_VERSION = 1
# Qt follows C++ naming conventions # Qt follows C++ naming conventions
# ruff: noqa: N802 # ruff: noqa: N802
@QmlElement @QmlElement
@QmlSingleton
class QModListModel(QAbstractListModel): class QModListModel(QAbstractListModel):
def __init__(self, parent=None) -> None: def __init__(self, parent=None) -> None:
super().__init__(parent=parent) super().__init__(parent=parent)
@ -48,16 +47,3 @@ class QModListModel(QAbstractListModel):
else: else:
result = None result = None
return result return result
def removeRows(self, row, count, parent=QModelIndex()) -> bool:
super().beginRemoveRows(parent, row, row + count - 1)
for index in range(row, row + count):
deleted_mod: QMod = self.mods.pop(index)
for root, dirs, files in deleted_mod.pathlib_path.walk(top_down=False):
for name in files:
(root / name).unlink()
for name in dirs:
(root / name).rmdir()
deleted_mod.pathlib_path.rmdir()
super().endRemoveRows()
return False

View file

@ -1,10 +1,8 @@
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Controls as Controls import QtQuick.Controls as Controls
import QtQuick.Dialogs as Dialogs
import org.kde.kirigami as Kirigami import org.kde.kirigami as Kirigami
import Leek import Leek
import Leek.QModInstaller
Kirigami.ApplicationWindow { Kirigami.ApplicationWindow {
id: root id: root
@ -13,89 +11,15 @@ Kirigami.ApplicationWindow {
globalDrawer: Kirigami.GlobalDrawer { globalDrawer: Kirigami.GlobalDrawer {
actions: [ actions: [
Kirigami.Action {
// download-symbolic and install-symbolic are the same icon
// but install looks worse for some reason
id: addAction
icon.name: "download-symbolic"
text: "Add mod"
shortcut: StandardKey.New
onTriggered: modFileDialog.open()
},
Kirigami.Action { Kirigami.Action {
text: "Quit" text: "Quit"
icon.name: "application-exit-symbolic" icon.name: "application-exit-symbolic"
shortcut: StandardKey.Quit
onTriggered: Qt.quit() onTriggered: Qt.quit()
} }
] ]
isMenu: true isMenu: true
} }
Component.onCompleted: {
QModInstaller.statusChanged.connect(installerStatusChanged);
QModInstaller.finishedInstall.connect(installDialog.close);
}
function installerStatusChanged() {
switch (QModInstaller.status) {
case "initialized":
if (QModInstaller.installed) {
installInfoMessage.text = "This mod is already installed!";
installInfoDialog.open();
} else {
installDialog.open();
QModInstaller.install();
}
break;
case "NoModExceptionError":
installInfoMessage.text = "This file does not have a mod";
installInfoDialog.open();
break;
case "UnsupportedArchiveTypeError":
installInfoMessage.text = "I don't know how to unpack this file, sorry :(";
installInfoDialog.open();
break;
}
}
Dialogs.FileDialog {
id: modFileDialog
nameFilters: ["Project DIVA Mods (*.zip *.7z *.rar)"]
selectedNameFilter.index: 0
onAccepted: {
QModInstaller.modPath = selectedFile;
}
}
Kirigami.Dialog {
id: installDialog
title: "Installing…"
// standardButtons: Kirigami.Dialog.Ok
closePolicy: Controls.Popup.NoAutoClose
showCloseButton: false
padding: Kirigami.Units.largeSpacing
Kirigami.LoadingPlaceholder {
// Layout.alignment: Qt.AlignCenter
anchors.centerIn: parent
determinate: false
text: "Unpacking…"
}
}
Kirigami.Dialog {
id: installInfoDialog
title: "Add mod"
standardButtons: Kirigami.Dialog.Ok
padding: Kirigami.Units.largeSpacing
Controls.Label {
id: installInfoMessage
text: "You should never see this text"
}
}
pageStack.columnView.columnResizeMode: Kirigami.ColumnView.SingleColumn pageStack.columnView.columnResizeMode: Kirigami.ColumnView.SingleColumn
pageStack.initialPage: Kirigami.ScrollablePage { pageStack.initialPage: Kirigami.ScrollablePage {
title: "Mods" title: "Mods"
@ -104,27 +28,17 @@ Kirigami.ApplicationWindow {
id: modsView id: modsView
delegate: ModCardDelegate {} delegate: ModCardDelegate {}
model: QModListModel model: QModListModel {}
Kirigami.PlaceholderMessage {
anchors.centerIn: parent
icon.name: "edit-none"
visible: modsView.count === 0
text: "There's no mods installed"
explanation: "Install a mod, and it will show up here"
helpfulAction: addAction
}
} }
} }
component ModCardDelegate: Kirigami.AbstractCard { component ModCardDelegate: Kirigami.AbstractCard {
required property int index
required property QMod mod required property QMod mod
id: card
showClickFeedback: true showClickFeedback: true
onClicked: pageStack.push(Qt.resolvedUrl("ModPage.qml"), { onClicked: pageStack.push(Qt.resolvedUrl("ModPage.qml"), {
mod: mod, mod: mod
index: index
}) })
// headerOrientation: Qt.Horizontal // headerOrientation: Qt.Horizontal
@ -161,6 +75,25 @@ Kirigami.ApplicationWindow {
Layout.fillWidth: true Layout.fillWidth: true
text: mod.name text: mod.name
type: Kirigami.Heading.Type.Primary type: Kirigami.Heading.Type.Primary
wrapMode: Text.WordWrap
maximumLineCount: 2
elide: Text.ElideRight
// https://stackoverflow.com/a/50504960
Controls.ToolTip.visible: labelArea.containsMouse && truncated
Controls.ToolTip.text: mod.name
Controls.ToolTip.delay: Kirigami.Units.toolTipDelay
MouseArea {
id: labelArea
anchors.fill: parent
hoverEnabled: true
// For some reason I can't connect the pressed signals, so there's no feedback for that
// Whoops
Component.onCompleted: {
clicked.connect(card.clicked)
}
}
} }
Kirigami.Separator { Kirigami.Separator {
Layout.fillWidth: true Layout.fillWidth: true
@ -172,6 +105,9 @@ Kirigami.ApplicationWindow {
font.italic: desc ? false : true font.italic: desc ? false : true
text: desc ? desc : "No description available" text: desc ? desc : "No description available"
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
maximumLineCount: 6
elide: Text.ElideRight
} }
} }
ColumnLayout { ColumnLayout {
@ -183,6 +119,19 @@ Kirigami.ApplicationWindow {
onClicked: mod.enabled = checked onClicked: mod.enabled = checked
} }
Controls.Button {
text: "Delete"
onClicked: notImplementedDialog.open()
Kirigami.Dialog {
id: notImplementedDialog
title: "Not implemented!"
standardButtons: Kirigami.Dialog.Ok
padding: Kirigami.Units.largeSpacing
Controls.Label {
text: "Deleting is not implemented yet"
}
}
}
} }
} }
} }

View file

@ -6,7 +6,6 @@ import Leek
Kirigami.Page { Kirigami.Page {
id: root id: root
required property int index
required property QMod mod required property QMod mod
title: "Local mod" title: "Local mod"
@ -16,33 +15,6 @@ Kirigami.Page {
applicationWindow().pageStack.pop(); applicationWindow().pageStack.pop();
} }
Kirigami.Dialog {
id: deleteConfirmationDialog
title: "Delete mod"
standardButtons: Kirigami.Dialog.Ok | Kirigami.Dialog.Cancel
padding: Kirigami.Units.largeSpacing
Controls.Label {
text: "Permanently delete this mod?"
}
onAccepted: {
QModListModel.removeRows(index, 1);
applicationWindow().pageStack.pop();
}
onOpened: {
const deleteButton = standardButton(Kirigami.Dialog.Cancel);
deleteButton.forceActiveFocus();
}
}
actions: [
Kirigami.Action {
text: "Delete"
icon.name: "delete-symbolic"
shortcut: StandardKey.Delete
onTriggered: deleteConfirmationDialog.open()
}
]
ColumnLayout { ColumnLayout {
anchors { anchors {
top: parent.top top: parent.top

View file

@ -28,14 +28,6 @@ class QMod(QObject):
# Pass though all exceptions # Pass though all exceptions
raise raise
@property
def pathlib_path(self) -> Path:
return self.__mod.path
@Property(str, constant=True)
def path(self) -> str:
return str(self.__mod.path)
@Property(str, constant=True) @Property(str, constant=True)
def name(self) -> str | None: def name(self) -> str | None:
return self.__mod.name return self.__mod.name

View file

@ -1,93 +0,0 @@
from pathlib import Path
from urllib.parse import urlparse
from PySide6.QtCore import Property, QObject, QRunnable, QThreadPool, Signal, Slot
from PySide6.QtQml import QmlElement, QmlSingleton
from leek.mod_installer import (
ModInstaller,
NoModExceptionError,
UnsupportedArchiveTypeError,
)
QML_IMPORT_NAME = "Leek.QModInstaller"
QML_IMPORT_MAJOR_VERSION = 1
# Qt follows C++ naming conventions
# ruff: noqa: N802
@QmlElement
@QmlSingleton
class QModInstaller(QObject):
"""
Qt Wrapper around the ModInstaller() class
"""
__mod_installer: ModInstaller
__status: str
def __init__(self, parent=None):
super().__init__(parent=parent)
self.__status = "uninitialized"
mod_path_changed = Signal(name="modPathChanged")
@Property(str, notify=mod_path_changed)
def modPath(self) -> str:
return self.test
@modPath.setter # type: ignore[no-redef]
def modPath(self, value: str) -> None:
try:
# In python 3.13 pathlib gained a from_uri() method which would be very useful
parsed_path: str = urlparse(value, "file").path
self.__mod_installer = ModInstaller(Path(parsed_path))
self.__status = "initialized"
self.status_changed.emit()
except UnsupportedArchiveTypeError:
self.__status = "UnsupportedArchiveTypeError"
self.status_changed.emit()
except NoModExceptionError:
self.__status = "NoModExceptionError"
self.status_changed.emit()
install_status_changed = Signal(name="installStatusChanged")
@Property(bool, notify=install_status_changed)
def installed(self) -> bool:
return self.__mod_installer.is_installed
status_changed = Signal(name="statusChanged")
@Property(str, notify=status_changed)
def status(self) -> str:
return self.__status
finished_install = Signal(name="finishedInstall")
@Slot()
def install(self) -> None:
worker = InstallWorker(InstallWorkerSignals(), self.__mod_installer)
worker.signals.installed.connect(self.finished_install)
QThreadPool.globalInstance().start(worker)
class InstallWorker(QRunnable):
__installer: ModInstaller
def __init__(self, signals, installer: ModInstaller) -> None:
super().__init__()
self.signals = signals
self.__installer = installer
# @Slot()
def run(self) -> None:
self.__installer.install()
self.signals.installed.emit()
class InstallWorkerSignals(QObject):
def __init__(self):
super().__init__()
installed = Signal()