Compare commits
No commits in common. "main" and "v0.1" have entirely different histories.
11 changed files with 60 additions and 561 deletions
57
flake.nix
57
flake.nix
|
|
@ -3,36 +3,33 @@
|
||||||
|
|
||||||
inputs.nixpkgs.url = "nixpkgs/nixos-unstable";
|
inputs.nixpkgs.url = "nixpkgs/nixos-unstable";
|
||||||
|
|
||||||
outputs = {nixpkgs, ...}: let
|
outputs = { nixpkgs, ... }:
|
||||||
pkgs = nixpkgs.legacyPackages.x86_64-linux;
|
let
|
||||||
lib = nixpkgs.lib;
|
pkgs = nixpkgs.legacyPackages.x86_64-linux;
|
||||||
in {
|
lib = nixpkgs.lib;
|
||||||
devShells.x86_64-linux.default = pkgs.mkShellNoCC {
|
in
|
||||||
name = "leek-devshell";
|
{
|
||||||
packages = with pkgs; [
|
devShells.x86_64-linux.default = pkgs.mkShellNoCC {
|
||||||
ruff
|
name = "leek-devshell";
|
||||||
(
|
packages = with pkgs; [
|
||||||
python3.withPackages (
|
ruff
|
||||||
ps:
|
(
|
||||||
with ps; [
|
python3.withPackages (ps: with ps;[
|
||||||
# Dev dependencies
|
python-lsp-server
|
||||||
python-lsp-server
|
pylsp-mypy
|
||||||
pylsp-mypy
|
mypy
|
||||||
mypy
|
setuptools-scm
|
||||||
setuptools-scm
|
pyside6
|
||||||
|
tomlkit
|
||||||
#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 {};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
11
package.nix
11
package.nix
|
|
@ -1,11 +1,10 @@
|
||||||
{
|
{
|
||||||
kdePackages,
|
kdePackages,
|
||||||
python3Packages,
|
python3Packages,
|
||||||
qt6,
|
qt6
|
||||||
}:
|
}: python3Packages.buildPythonApplication rec {
|
||||||
python3Packages.buildPythonApplication rec {
|
|
||||||
pname = "leek";
|
pname = "leek";
|
||||||
version = "0.3";
|
version = "0.1";
|
||||||
pyproject = true;
|
pyproject = true;
|
||||||
|
|
||||||
src = ./.;
|
src = ./.;
|
||||||
|
|
@ -15,10 +14,10 @@ python3Packages.buildPythonApplication rec {
|
||||||
setuptools-scm
|
setuptools-scm
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
dependencies = with python3Packages; [
|
dependencies = with python3Packages; [
|
||||||
pyside6
|
pyside6
|
||||||
tomlkit
|
tomlkit
|
||||||
vdf
|
|
||||||
];
|
];
|
||||||
|
|
||||||
nativeBuildInputs = [
|
nativeBuildInputs = [
|
||||||
|
|
@ -27,7 +26,7 @@ python3Packages.buildPythonApplication rec {
|
||||||
|
|
||||||
propagatedBuildInputs = [
|
propagatedBuildInputs = [
|
||||||
kdePackages.kirigami
|
kdePackages.kirigami
|
||||||
];
|
];
|
||||||
|
|
||||||
makeWrapperArgs = [
|
makeWrapperArgs = [
|
||||||
"\${qtWrapperArgs[@]}"
|
"\${qtWrapperArgs[@]}"
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,8 @@ classifiers = [
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"pyside6",
|
"pyside6",
|
||||||
"tomlkit",
|
"tomlkit",
|
||||||
"vdf",
|
|
||||||
]
|
]
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.6"
|
||||||
|
|
||||||
[project.readme]
|
[project.readme]
|
||||||
file = "README.md"
|
file = "README.md"
|
||||||
|
|
@ -48,8 +47,7 @@ mypy_path = "$MYPY_CONFIG_FILE_DIR/src"
|
||||||
module = [
|
module = [
|
||||||
"PySide6.QtGui",
|
"PySide6.QtGui",
|
||||||
"PySide6.QtCore",
|
"PySide6.QtCore",
|
||||||
"PySide6.QtQml",
|
"PySide6.QtQml"
|
||||||
"vdf"
|
|
||||||
]
|
]
|
||||||
ignore_missing_imports = true
|
ignore_missing_imports = true
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,16 +3,15 @@
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import signal
|
import signal
|
||||||
from PySide6.QtWidgets import QApplication
|
from PySide6.QtGui import QGuiApplication
|
||||||
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():
|
||||||
"""Initializes and manages the application execution"""
|
"""Initializes and manages the application execution"""
|
||||||
app: QApplication = QApplication(sys.argv)
|
app: QGuiApplication = QGuiApplication(sys.argv)
|
||||||
app.setDesktopFileName("xyz.toast003.leek")
|
app.setDesktopFileName("xyz.toast003.leek")
|
||||||
app.setApplicationName("Leek")
|
app.setApplicationName("Leek")
|
||||||
engine = QQmlApplicationEngine()
|
engine = QQmlApplicationEngine()
|
||||||
|
|
|
||||||
|
|
@ -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}"
|
|
||||||
|
|
@ -1,26 +1,28 @@
|
||||||
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
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from leek.utils import GameFinder
|
|
||||||
|
|
||||||
QML_IMPORT_NAME = "Leek"
|
QML_IMPORT_NAME = "Leek"
|
||||||
QML_IMPORT_MAJOR_VERSION = 1
|
QML_IMPORT_MAJOR_VERSION = 1
|
||||||
|
|
||||||
|
# TODO: Don't harcode the mods path
|
||||||
|
GAME_PATH = Path(
|
||||||
|
"/home/toast/.local/share/Steam/steamapps/common/Hatsune Miku Project DIVA Mega Mix Plus/"
|
||||||
|
)
|
||||||
|
MOD_PATH = Path(GAME_PATH, "mods")
|
||||||
|
|
||||||
|
|
||||||
# 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)
|
||||||
mods: list[QMod] = []
|
mods: list[QMod] = []
|
||||||
|
|
||||||
mod_path: Path = GameFinder.find() / "mods"
|
for dir in MOD_PATH.iterdir():
|
||||||
|
|
||||||
for dir in mod_path.iterdir():
|
|
||||||
if dir.name == ".stfolder":
|
if dir.name == ".stfolder":
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
|
|
@ -48,16 +50,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
|
|
||||||
|
|
|
||||||
|
|
@ -1,132 +1,30 @@
|
||||||
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
|
||||||
|
|
||||||
title: "Leek"
|
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 {
|
pageStack.initialPage: Kirigami.ScrollablePage {
|
||||||
title: "Mods"
|
title: "Mods"
|
||||||
|
|
||||||
Kirigami.CardsListView {
|
Kirigami.CardsListView {
|
||||||
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
|
||||||
|
|
||||||
showClickFeedback: true
|
|
||||||
onClicked: pageStack.push(Qt.resolvedUrl("ModPage.qml"), {
|
|
||||||
mod: mod,
|
|
||||||
index: index
|
|
||||||
})
|
|
||||||
|
|
||||||
// headerOrientation: Qt.Horizontal
|
// headerOrientation: Qt.Horizontal
|
||||||
contentItem: Item {
|
contentItem: Item {
|
||||||
implicitHeight: modCardLayout.implicitHeight
|
implicitHeight: modCardLayout.implicitHeight
|
||||||
|
|
@ -183,6 +81,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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,139 +0,0 @@
|
||||||
import QtQuick
|
|
||||||
import QtQuick.Layouts
|
|
||||||
import QtQuick.Controls as Controls
|
|
||||||
import org.kde.kirigami as Kirigami
|
|
||||||
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
|
|
||||||
left: parent.left
|
|
||||||
right: parent.right
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: header
|
|
||||||
Layout.fillWidth: true
|
|
||||||
|
|
||||||
Layout.topMargin: -root.topPadding
|
|
||||||
Layout.leftMargin: -root.leftPadding
|
|
||||||
Layout.rightMargin: -root.rightPadding
|
|
||||||
|
|
||||||
Kirigami.ImageColors {
|
|
||||||
id: iconColors
|
|
||||||
source: modIcon.source
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
GridLayout {
|
|
||||||
id: headerContents
|
|
||||||
anchors {
|
|
||||||
top: parent.top
|
|
||||||
left: parent.left
|
|
||||||
right: parent.right
|
|
||||||
topMargin: root.padding
|
|
||||||
bottomMargin: root.padding
|
|
||||||
leftMargin: root.padding
|
|
||||||
rightMargin: root.padding
|
|
||||||
}
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
// Icon
|
|
||||||
Kirigami.Icon {
|
|
||||||
id: modIcon
|
|
||||||
implicitHeight: Kirigami.Units.iconSizes.huge
|
|
||||||
implicitWidth: implicitHeight
|
|
||||||
source: "package-x-generic"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Name and author
|
|
||||||
ColumnLayout {
|
|
||||||
Kirigami.Heading {
|
|
||||||
text: mod.name
|
|
||||||
type: Kirigami.Heading.Type.Primary
|
|
||||||
}
|
|
||||||
Controls.Label {
|
|
||||||
property bool hasAuthors: mod.authors.length > 0
|
|
||||||
function joinAuthors() {
|
|
||||||
return mod.authors.join(", ");
|
|
||||||
}
|
|
||||||
|
|
||||||
text: hasAuthors ? joinAuthors() : "Unknown author"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Kirigami.Separator {
|
|
||||||
width: header.width
|
|
||||||
anchors.top: header.bottom
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Description
|
|
||||||
ColumnLayout {
|
|
||||||
Kirigami.Heading {
|
|
||||||
// TODO: this should be a short description of some sort
|
|
||||||
text: mod.name
|
|
||||||
type: Kirigami.Heading.Type.Primary
|
|
||||||
}
|
|
||||||
Controls.Label {
|
|
||||||
readonly property bool hasDescription: mod.longDescription || mod.description
|
|
||||||
text: {
|
|
||||||
if (hasDescription) {
|
|
||||||
if (mod.longDescription) {
|
|
||||||
text = mod.longDescription;
|
|
||||||
} else {
|
|
||||||
text = mod.description;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
text = "The description for this mod is not available";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -48,10 +40,6 @@ class QMod(QObject):
|
||||||
def longDescription(self) -> str | None:
|
def longDescription(self) -> str | None:
|
||||||
return self.__mod.long_description
|
return self.__mod.long_description
|
||||||
|
|
||||||
@Property(list, constant=True)
|
|
||||||
def authors(self) -> list[str] | None:
|
|
||||||
return self.__mod.authors
|
|
||||||
|
|
||||||
mod_enabled = Signal(name="enabled")
|
mod_enabled = Signal(name="enabled")
|
||||||
|
|
||||||
@Property(bool, notify=mod_enabled)
|
@Property(bool, notify=mod_enabled)
|
||||||
|
|
|
||||||
|
|
@ -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()
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
import vdf
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
class GameFinder:
|
|
||||||
__game_path: Path | None = None
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def find() -> Path:
|
|
||||||
if GameFinder.__game_path is not None:
|
|
||||||
return GameFinder.__game_path
|
|
||||||
|
|
||||||
# .local/share/Steam/config/libraryfolders.vdf
|
|
||||||
steam_path: Path = Path.home() / ".local/share/Steam"
|
|
||||||
libraries_vdf_path: Path = steam_path / "config/libraryfolders.vdf"
|
|
||||||
|
|
||||||
with libraries_vdf_path.open() as vdf_file:
|
|
||||||
data: dict = vdf.parse(vdf_file)
|
|
||||||
for index in data["libraryfolders"]:
|
|
||||||
library: dict = data["libraryfolders"][index]
|
|
||||||
|
|
||||||
project_diva_id: str = "1761390"
|
|
||||||
if project_diva_id in library["apps"]:
|
|
||||||
libray_path: Path = Path(library["path"])
|
|
||||||
GameFinder.__game_path = (
|
|
||||||
libray_path
|
|
||||||
/ "steamapps/common/Hatsune Miku Project DIVA Mega Mix Plus"
|
|
||||||
)
|
|
||||||
return GameFinder.__game_path
|
|
||||||
|
|
||||||
# Could not find the game :(
|
|
||||||
raise FileNotFoundError
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue