diff --git a/flake.nix b/flake.nix index 8424972..bbdb95c 100644 --- a/flake.nix +++ b/flake.nix @@ -3,36 +3,37 @@ inputs.nixpkgs.url = "nixpkgs/nixos-unstable"; - outputs = {nixpkgs, ...}: let - pkgs = nixpkgs.legacyPackages.x86_64-linux; - lib = nixpkgs.lib; - in { - devShells.x86_64-linux.default = pkgs.mkShellNoCC { - name = "leek-devshell"; - packages = with pkgs; [ - ruff - ( - python3.withPackages ( - ps: - with ps; [ - # Dev dependencies - python-lsp-server - pylsp-mypy - mypy - setuptools-scm + outputs = { nixpkgs, ... }: + let + pkgs = nixpkgs.legacyPackages.x86_64-linux; + lib = nixpkgs.lib; + in + { + devShells.x86_64-linux.default = pkgs.mkShellNoCC { + name = "leek-devshell"; + packages = with pkgs; [ + ruff + ( + python3.withPackages (ps: with ps;[ + # Dev dependencies + python-lsp-server + pylsp-mypy + mypy + setuptools-scm - #App dependencies - pyside6 - tomlkit - vdf - ] + #App dependencies + pyside6 + tomlkit + vdf + ] + ) ) - ) - ]; + ]; + }; + packages.x86_64-linux = rec { + default = leek; + leek = pkgs.callPackage ./package.nix { }; + }; }; - packages.x86_64-linux = rec { - default = leek; - leek = pkgs.callPackage ./package.nix {}; - }; - }; } + diff --git a/package.nix b/package.nix index 48f4f64..987b8f4 100644 --- a/package.nix +++ b/package.nix @@ -1,11 +1,10 @@ { kdePackages, python3Packages, - qt6, -}: -python3Packages.buildPythonApplication rec { + qt6 +}: python3Packages.buildPythonApplication rec { pname = "leek"; - version = "0.3"; + version = "0.2"; pyproject = true; src = ./.; @@ -15,6 +14,7 @@ python3Packages.buildPythonApplication rec { setuptools-scm ]; + dependencies = with python3Packages; [ pyside6 tomlkit @@ -27,7 +27,7 @@ python3Packages.buildPythonApplication rec { propagatedBuildInputs = [ kdePackages.kirigami - ]; + ]; makeWrapperArgs = [ "\${qtWrapperArgs[@]}" 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..adec38c 100644 --- a/src/leek/qml/Main.qml +++ b/src/leek/qml/Main.qml @@ -1,131 +1,32 @@ 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 title: "Leek" - 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" Kirigami.CardsListView { 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 + delegate: ModCardDelegate { + } + model: QModListModel { } } } component ModCardDelegate: Kirigami.AbstractCard { - required property int index required property QMod mod showClickFeedback: true - onClicked: pageStack.push(Qt.resolvedUrl("ModPage.qml"), { - mod: mod, - index: index - }) + onClicked: pageStack.pushDialogLayer(Qt.resolvedUrl("ModPage.qml"), {mod: mod}) // headerOrientation: Qt.Horizontal contentItem: Item { @@ -183,6 +84,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..ab1e5d3 100644 --- a/src/leek/qml/ModPage.qml +++ b/src/leek/qml/ModPage.qml @@ -6,43 +6,10 @@ import Leek Kirigami.Page { id: root - required property int index required property QMod mod title: "Local mod" - onBackRequested: event => { - event.accepted = true; - 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 @@ -66,7 +33,11 @@ Kirigami.Page { implicitHeight: headerContents.implicitHeight + (headerContents.anchors.topMargin * 2) // Tint the header with the dominant color of the mod's icon - color: Kirigami.ColorUtils.tintWithAlpha(Kirigami.Theme.backgroundColor, iconColors.dominant, 0.1) + color: Kirigami.ColorUtils.tintWithAlpha( + Kirigami.Theme.backgroundColor, + iconColors.dominant, + 0.1 + ) GridLayout { id: headerContents @@ -98,10 +69,10 @@ Kirigami.Page { Controls.Label { property bool hasAuthors: mod.authors.length > 0 function joinAuthors() { - return mod.authors.join(", "); + return mod.authors.join(", ") } - text: hasAuthors ? joinAuthors() : "Unknown author" + text: hasAuthors ? joinAuthors() : "Unknown author" } } } @@ -125,12 +96,12 @@ Kirigami.Page { text: { if (hasDescription) { if (mod.longDescription) { - text = mod.longDescription; + text = mod.longDescription } else { - text = mod.description; + text = mod.description } } else { - text = "The description for this mod is not available"; + text = "The description for this mod is not available" } } } 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()