diff --git a/package.nix b/package.nix index 48f4f64..b242333 100644 --- a/package.nix +++ b/package.nix @@ -5,7 +5,7 @@ }: python3Packages.buildPythonApplication rec { pname = "leek"; - version = "0.3"; + version = "0.2"; pyproject = true; src = ./.; diff --git a/pyproject.toml b/pyproject.toml index 1ccbed4..b0cc6ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,7 @@ dependencies = [ "tomlkit", "vdf", ] -requires-python = ">=3.12" +requires-python = ">=3.6" [project.readme] file = "README.md" diff --git a/src/leek/leek_app.py b/src/leek/leek_app.py index f85592e..701046c 100644 --- a/src/leek/leek_app.py +++ b/src/leek/leek_app.py @@ -7,7 +7,6 @@ from PySide6.QtWidgets import QApplication from PySide6.QtCore import QUrl from PySide6.QtQml import QQmlApplicationEngine 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(): diff --git a/src/leek/mod_installer.py b/src/leek/mod_installer.py deleted file mode 100644 index 485d93f..0000000 --- a/src/leek/mod_installer.py +++ /dev/null @@ -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}" diff --git a/src/leek/mod_list.py b/src/leek/mod_list.py index 3c13d58..ee12488 100644 --- a/src/leek/mod_list.py +++ b/src/leek/mod_list.py @@ -1,4 +1,4 @@ -from PySide6.QtQml import QmlElement, QmlSingleton +from PySide6.QtQml import QmlElement from PySide6.QtCore import QAbstractListModel, QModelIndex from leek.mod import InvalidModError from leek.qmod import QMod @@ -12,7 +12,6 @@ QML_IMPORT_MAJOR_VERSION = 1 # Qt follows C++ naming conventions # ruff: noqa: N802 @QmlElement -@QmlSingleton class QModListModel(QAbstractListModel): def __init__(self, parent=None) -> None: super().__init__(parent=parent) @@ -48,16 +47,3 @@ class QModListModel(QAbstractListModel): else: result = None 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 diff --git a/src/leek/qml/Main.qml b/src/leek/qml/Main.qml index cf6889b..667eb01 100644 --- a/src/leek/qml/Main.qml +++ b/src/leek/qml/Main.qml @@ -1,10 +1,8 @@ import QtQuick import QtQuick.Layouts import QtQuick.Controls as Controls -import QtQuick.Dialogs as Dialogs import org.kde.kirigami as Kirigami import Leek -import Leek.QModInstaller Kirigami.ApplicationWindow { id: root @@ -13,89 +11,15 @@ Kirigami.ApplicationWindow { globalDrawer: Kirigami.GlobalDrawer { 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 { text: "Quit" icon.name: "application-exit-symbolic" - shortcut: StandardKey.Quit onTriggered: Qt.quit() } ] 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.initialPage: Kirigami.ScrollablePage { title: "Mods" @@ -104,27 +28,17 @@ Kirigami.ApplicationWindow { id: modsView delegate: ModCardDelegate {} - 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 - } + model: QModListModel {} } } component ModCardDelegate: Kirigami.AbstractCard { - required property int index required property QMod mod + id: card showClickFeedback: true onClicked: pageStack.push(Qt.resolvedUrl("ModPage.qml"), { - mod: mod, - index: index + mod: mod }) // headerOrientation: Qt.Horizontal @@ -161,6 +75,25 @@ Kirigami.ApplicationWindow { Layout.fillWidth: true text: mod.name 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 { Layout.fillWidth: true @@ -172,6 +105,9 @@ Kirigami.ApplicationWindow { font.italic: desc ? false : true text: desc ? desc : "No description available" wrapMode: Text.WordWrap + + maximumLineCount: 6 + elide: Text.ElideRight } } ColumnLayout { @@ -183,6 +119,19 @@ Kirigami.ApplicationWindow { 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" + } + } + } } } } diff --git a/src/leek/qml/ModPage.qml b/src/leek/qml/ModPage.qml index 528affb..326b760 100644 --- a/src/leek/qml/ModPage.qml +++ b/src/leek/qml/ModPage.qml @@ -6,7 +6,6 @@ import Leek Kirigami.Page { id: root - required property int index required property QMod mod title: "Local mod" @@ -16,33 +15,6 @@ Kirigami.Page { 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 { anchors { top: parent.top diff --git a/src/leek/qmod.py b/src/leek/qmod.py index 1131468..1e12bc4 100644 --- a/src/leek/qmod.py +++ b/src/leek/qmod.py @@ -28,14 +28,6 @@ class QMod(QObject): # Pass though all exceptions 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) def name(self) -> str | None: return self.__mod.name diff --git a/src/leek/qmod_installer.py b/src/leek/qmod_installer.py deleted file mode 100644 index 7d7939f..0000000 --- a/src/leek/qmod_installer.py +++ /dev/null @@ -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()