Compare commits
No commits in common. "main" and "old-cpp" have entirely different histories.
19 changed files with 227 additions and 995 deletions
111
.gitignore
vendored
111
.gitignore
vendored
|
|
@ -1,3 +1,93 @@
|
|||
# ---> C++
|
||||
# Prerequisites
|
||||
*.d
|
||||
|
||||
# Compiled Object files
|
||||
*.slo
|
||||
*.lo
|
||||
*.o
|
||||
*.obj
|
||||
|
||||
# Precompiled Headers
|
||||
*.gch
|
||||
*.pch
|
||||
|
||||
# Compiled Dynamic libraries
|
||||
*.so
|
||||
*.dylib
|
||||
*.dll
|
||||
|
||||
# Fortran module files
|
||||
*.mod
|
||||
*.smod
|
||||
|
||||
# Compiled Static libraries
|
||||
*.lai
|
||||
*.la
|
||||
*.a
|
||||
*.lib
|
||||
|
||||
# Executables
|
||||
*.exe
|
||||
*.out
|
||||
*.app
|
||||
|
||||
# ---> Qt
|
||||
# C++ objects and libs
|
||||
*.slo
|
||||
*.lo
|
||||
*.o
|
||||
*.a
|
||||
*.la
|
||||
*.lai
|
||||
*.so
|
||||
*.so.*
|
||||
*.dll
|
||||
*.dylib
|
||||
|
||||
# Qt-es
|
||||
object_script.*.Release
|
||||
object_script.*.Debug
|
||||
*_plugin_import.cpp
|
||||
/.qmake.cache
|
||||
/.qmake.stash
|
||||
*.pro.user
|
||||
*.pro.user.*
|
||||
*.qbs.user
|
||||
*.qbs.user.*
|
||||
*.moc
|
||||
moc_*.cpp
|
||||
moc_*.h
|
||||
qrc_*.cpp
|
||||
ui_*.h
|
||||
*.qmlc
|
||||
*.jsc
|
||||
Makefile*
|
||||
*build-*
|
||||
*.qm
|
||||
*.prl
|
||||
|
||||
# Qt unit tests
|
||||
target_wrapper.*
|
||||
|
||||
# QtCreator
|
||||
*.autosave
|
||||
|
||||
# QtCreator Qml
|
||||
*.qmlproject.user
|
||||
*.qmlproject.user.*
|
||||
|
||||
# QtCreator CMake
|
||||
CMakeLists.txt.user*
|
||||
|
||||
# QtCreator 4.8< compilation database
|
||||
compile_commands.json
|
||||
|
||||
# QtCreator local machine specific files for imported projects
|
||||
*creator.user*
|
||||
|
||||
*_qmlcache.qrc
|
||||
|
||||
# ---> Nix
|
||||
# Ignore build outputs from performing a nix-build or `nix build` command
|
||||
result
|
||||
|
|
@ -6,6 +96,21 @@ result-*
|
|||
# ---> Direnv
|
||||
.direnv
|
||||
|
||||
# ---> Python
|
||||
.venv
|
||||
__pycache__
|
||||
# ---> CMake
|
||||
CMakeLists.txt.user
|
||||
CMakeCache.txt
|
||||
CMakeFiles
|
||||
CMakeScripts
|
||||
Testing
|
||||
Makefile
|
||||
cmake_install.cmake
|
||||
install_manifest.txt
|
||||
compile_commands.json
|
||||
CTestTestfile.cmake
|
||||
_deps
|
||||
CMakeUserPresets.json
|
||||
build
|
||||
|
||||
# ---> Clangd
|
||||
.cache/clangd
|
||||
|
||||
|
|
|
|||
28
CMakeLists.txt
Normal file
28
CMakeLists.txt
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
cmake_minimum_required(VERSION 3.20)
|
||||
project(leek)
|
||||
|
||||
find_package(ECM 6.0.0 REQUIRED NO_MODULE)
|
||||
set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH})
|
||||
|
||||
set(CMAKE_EXPORT_COMPILE_COMMANDS TRUE)
|
||||
|
||||
include(KDEInstallDirs)
|
||||
include(KDECMakeSettings)
|
||||
include(KDECompilerSettings NO_POLICY_SCOPE)
|
||||
include(ECMFindQmlModule)
|
||||
include(ECMQmlModule)
|
||||
|
||||
find_package(Qt6 REQUIRED COMPONENTS Core Quick Gui QuickControls2 Widgets)
|
||||
|
||||
find_package(KF6 REQUIRED COMPONENTS Kirigami I18n CoreAddons QQC2DesktopStyle
|
||||
IconThemes)
|
||||
|
||||
# Don't mark this as required, since qml module finding is broken on NixOS
|
||||
ecm_find_qmlmodule(org.kde.kirigami)
|
||||
|
||||
add_subdirectory(src)
|
||||
|
||||
install(PROGRAMS xyz.toast003.leek.desktop DESTINATION ${KDE_INSTALL_APPDIR})
|
||||
|
||||
feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES
|
||||
FATAL_ON_MISSING_REQUIRED_PACKAGES)
|
||||
57
flake.nix
57
flake.nix
|
|
@ -3,36 +3,31 @@
|
|||
|
||||
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
|
||||
|
||||
#App dependencies
|
||||
pyside6
|
||||
tomlkit
|
||||
vdf
|
||||
]
|
||||
)
|
||||
)
|
||||
];
|
||||
outputs = { nixpkgs, ... }:
|
||||
let
|
||||
pkgs = nixpkgs.legacyPackages.x86_64-linux;
|
||||
lib = nixpkgs.lib;
|
||||
in
|
||||
{
|
||||
devShells.x86_64-linux.default = pkgs.mkShell {
|
||||
name = "leek-devshell";
|
||||
packages =
|
||||
let
|
||||
kdeDeps = with pkgs.kdePackages; [
|
||||
extra-cmake-modules
|
||||
qtbase
|
||||
qtdeclarative
|
||||
kirigami
|
||||
ki18n
|
||||
kcoreaddons
|
||||
qqc2-desktop-style
|
||||
];
|
||||
in
|
||||
with pkgs; [
|
||||
clang-tools
|
||||
cmake
|
||||
] ++ kdeDeps;
|
||||
};
|
||||
};
|
||||
packages.x86_64-linux = rec {
|
||||
default = leek;
|
||||
leek = pkgs.callPackage ./package.nix {};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
35
package.nix
35
package.nix
|
|
@ -1,35 +0,0 @@
|
|||
{
|
||||
kdePackages,
|
||||
python3Packages,
|
||||
qt6,
|
||||
}:
|
||||
python3Packages.buildPythonApplication rec {
|
||||
pname = "leek";
|
||||
version = "0.3";
|
||||
pyproject = true;
|
||||
|
||||
src = ./.;
|
||||
|
||||
build-system = with python3Packages; [
|
||||
setuptools
|
||||
setuptools-scm
|
||||
];
|
||||
|
||||
dependencies = with python3Packages; [
|
||||
pyside6
|
||||
tomlkit
|
||||
vdf
|
||||
];
|
||||
|
||||
nativeBuildInputs = [
|
||||
qt6.wrapQtAppsHook
|
||||
];
|
||||
|
||||
propagatedBuildInputs = [
|
||||
kdePackages.kirigami
|
||||
];
|
||||
|
||||
makeWrapperArgs = [
|
||||
"\${qtWrapperArgs[@]}"
|
||||
];
|
||||
}
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
[build-system]
|
||||
requires = ["setuptools", "setuptools-scm"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "leek"
|
||||
dynamic = ["version"]
|
||||
authors = [{name = "Toast"}]
|
||||
description = "Project diva megamix + mod manager"
|
||||
license = "MIT"
|
||||
classifiers = [
|
||||
"Intended Audience :: End Users/Desktop",
|
||||
"Topic :: Utilities",
|
||||
"Programming Language :: Python",
|
||||
"Operating System :: POSIX :: Linux",
|
||||
]
|
||||
dependencies = [
|
||||
"pyside6",
|
||||
"tomlkit",
|
||||
"vdf",
|
||||
]
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[project.readme]
|
||||
file = "README.md"
|
||||
content-type = "text/markdown"
|
||||
|
||||
[project.scripts]
|
||||
leek = "leek.leek_app:main"
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = [
|
||||
# Defaults
|
||||
"E4",
|
||||
"E7",
|
||||
"E9",
|
||||
"F",
|
||||
|
||||
"W", # Pycodestyle warning
|
||||
"N" # Pep-8 naming
|
||||
]
|
||||
|
||||
[tool.mypy]
|
||||
mypy_path = "$MYPY_CONFIG_FILE_DIR/src"
|
||||
|
||||
# Pyside 6 doesn't have type info so it's better to just ignore it
|
||||
[[tool.mypy.overrides]]
|
||||
module = [
|
||||
"PySide6.QtGui",
|
||||
"PySide6.QtCore",
|
||||
"PySide6.QtQml",
|
||||
"vdf"
|
||||
]
|
||||
ignore_missing_imports = true
|
||||
|
||||
[tool.setuptools.data-files]
|
||||
"share/applications" = ["xyz.toast003.leek.desktop"]
|
||||
|
||||
[tool.setuptools_scm]
|
||||
|
||||
13
src/CMakeLists.txt
Normal file
13
src/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
add_executable(leek)
|
||||
|
||||
ecm_add_qml_module(leek URI xyz.toast003.leek)
|
||||
|
||||
target_sources(leek PRIVATE main.cpp)
|
||||
|
||||
ecm_target_qml_sources(leek SOURCES Main.qml)
|
||||
|
||||
target_link_libraries(leek PRIVATE Qt6::Quick Qt6::Qml Qt6::Gui
|
||||
Qt6::QuickControls2 Qt6::Widgets)
|
||||
target_link_libraries(leek PRIVATE KF6::I18n KF6::CoreAddons KF6::IconThemes)
|
||||
|
||||
install(TARGETS leek ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
|
||||
17
src/Main.qml
Normal file
17
src/Main.qml
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls as Controls
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
Kirigami.ApplicationWindow {
|
||||
id: root
|
||||
title: "Leek"
|
||||
|
||||
pageStack.initialPage: Kirigami.Page {
|
||||
Controls.Label {
|
||||
anchors.centerIn: parent
|
||||
text: "Hello world"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
# https://packaging.python.org/en/latest/discussions/src-layout-vs-flat-layout/#running-a-command-line-interface-from-source-with-src-layout
|
||||
import os
|
||||
import sys
|
||||
|
||||
if not __package__:
|
||||
# Make CLI runnable from source tree with
|
||||
# python src/package
|
||||
package_source_path = os.path.dirname(os.path.dirname(__file__))
|
||||
sys.path.insert(0, package_source_path)
|
||||
|
||||
|
||||
from leek import leek_app
|
||||
|
||||
leek_app.main()
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import sys
|
||||
import signal
|
||||
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():
|
||||
"""Initializes and manages the application execution"""
|
||||
app: QApplication = QApplication(sys.argv)
|
||||
app.setDesktopFileName("xyz.toast003.leek")
|
||||
app.setApplicationName("Leek")
|
||||
engine = QQmlApplicationEngine()
|
||||
|
||||
"""Needed to close the app with Ctrl+C"""
|
||||
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
||||
|
||||
"""Needed to get proper KDE style outside of Plasma"""
|
||||
if not os.environ.get("QT_QUICK_CONTROLS_STYLE"):
|
||||
os.environ["QT_QUICK_CONTROLS_STYLE"] = "org.kde.desktop"
|
||||
|
||||
base_path = os.path.abspath(os.path.dirname(__file__))
|
||||
url = QUrl(f"file://{base_path}/qml/Main.qml")
|
||||
engine.load(url)
|
||||
|
||||
if len(engine.rootObjects()) == 0:
|
||||
quit()
|
||||
|
||||
app.exec()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
115
src/leek/mod.py
115
src/leek/mod.py
|
|
@ -1,115 +0,0 @@
|
|||
import tomlkit
|
||||
import json
|
||||
from pathlib import Path
|
||||
from tomlkit import TOMLDocument
|
||||
from typing import Any
|
||||
|
||||
|
||||
class Mod:
|
||||
__config: TOMLDocument
|
||||
|
||||
__has_meta_json: bool
|
||||
__meta: dict[str, Any]
|
||||
|
||||
__path: Path
|
||||
__name: str
|
||||
__description: str
|
||||
__author: str
|
||||
__enabled: bool
|
||||
|
||||
@property
|
||||
def path(self) -> Path:
|
||||
return self.__path
|
||||
|
||||
# Mod metadata
|
||||
@property
|
||||
def name(self) -> str | None:
|
||||
if self.__has_meta_json:
|
||||
if "name" in self.__meta:
|
||||
return self.__meta.get("name", str)
|
||||
if "name" not in self.__config:
|
||||
return None
|
||||
return self.__config.get("name", str)
|
||||
|
||||
@property
|
||||
def description(self) -> str | None:
|
||||
if self.__has_meta_json:
|
||||
if "description" in self.__meta:
|
||||
return self.__meta.get("description", str)
|
||||
if "description" not in self.__config.keys():
|
||||
return None
|
||||
else:
|
||||
return self.__config.get("description", str)
|
||||
|
||||
@property
|
||||
def long_description(self) -> str | None:
|
||||
if "descriptionLong" in self.__meta:
|
||||
return self.__meta.get("descriptionLong", str)
|
||||
return None
|
||||
|
||||
@property
|
||||
def authors(self) -> list[str] | None:
|
||||
if self.__has_meta_json:
|
||||
if "authors" in self.__meta:
|
||||
return self.__meta.get("authors", list[str])
|
||||
if "author" not in self.__config:
|
||||
return None
|
||||
return [self.__config.get("author", str)]
|
||||
|
||||
@property
|
||||
def enabled(self) -> bool:
|
||||
return self.__enabled
|
||||
|
||||
@enabled.setter
|
||||
def enabled(self, value: bool) -> None:
|
||||
if value == self.__enabled:
|
||||
# Nothing to do
|
||||
return
|
||||
|
||||
config_toml = Path(self.__path, "config.toml")
|
||||
with config_toml.open("w") as config_file:
|
||||
self.__config["enabled"] = value
|
||||
tomlkit.dump(self.__config, config_file)
|
||||
self.__enabled = value
|
||||
|
||||
def __init__(self, path: Path) -> None:
|
||||
self.__path = path
|
||||
self.__meta = {}
|
||||
|
||||
try:
|
||||
config_toml = Path(self.__path, "config.toml")
|
||||
with config_toml.open() as config_file:
|
||||
self.__config = tomlkit.load(config_file)
|
||||
|
||||
if "enabled" not in self.__config:
|
||||
raise InvalidModError("config.toml does not contain the enabled key")
|
||||
self.__enabled = self.__config.get("enabled", bool)
|
||||
except FileNotFoundError:
|
||||
raise InvalidModError("config.toml does not exist")
|
||||
|
||||
meta_json: Path = Path(self.__path, "meta.json")
|
||||
if meta_json.exists():
|
||||
self.__has_meta_json = True
|
||||
try:
|
||||
with meta_json.open() as file:
|
||||
self.__meta = json.load(file)
|
||||
except json.JSONDecodeError as e:
|
||||
print("Failed to parse meta.json!: ", e.msg)
|
||||
else:
|
||||
self.__has_meta_json = False
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"Mod({self.__path})"
|
||||
|
||||
|
||||
class InvalidModError(Exception):
|
||||
"""
|
||||
This exception is raised when the Mod class gets given a path of something that's not a valid mod
|
||||
"""
|
||||
|
||||
def __init__(self, message: str) -> None:
|
||||
super().__init__(message)
|
||||
self.message = message
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.message}"
|
||||
|
|
@ -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,63 +0,0 @@
|
|||
from PySide6.QtQml import QmlElement, QmlSingleton
|
||||
from PySide6.QtCore import QAbstractListModel, QModelIndex
|
||||
from leek.mod import InvalidModError
|
||||
from leek.qmod import QMod
|
||||
from pathlib import Path
|
||||
from leek.utils import GameFinder
|
||||
|
||||
QML_IMPORT_NAME = "Leek"
|
||||
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)
|
||||
mods: list[QMod] = []
|
||||
|
||||
mod_path: Path = GameFinder.find() / "mods"
|
||||
|
||||
for dir in mod_path.iterdir():
|
||||
if dir.name == ".stfolder":
|
||||
continue
|
||||
try:
|
||||
new_mod: QMod = QMod(dir, self)
|
||||
mods.append(new_mod)
|
||||
except InvalidModError as e:
|
||||
print(f"Found invalid mod at {dir}: {e.message}")
|
||||
continue
|
||||
|
||||
self.mods = mods
|
||||
|
||||
def roleNames(self) -> dict[int, bytes]:
|
||||
return {0: b"mod"}
|
||||
|
||||
def rowCount(self, parent=QModelIndex()) -> int:
|
||||
return len(self.mods)
|
||||
|
||||
def data(self, index: QModelIndex, role: int) -> None | QMod:
|
||||
i: int = index.row()
|
||||
result: None | QMod
|
||||
if not index.isValid():
|
||||
result = None
|
||||
elif role == 0:
|
||||
result = self.mods[i]
|
||||
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
|
||||
|
|
@ -1,190 +0,0 @@
|
|||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
|
||||
// headerOrientation: Qt.Horizontal
|
||||
contentItem: Item {
|
||||
implicitHeight: modCardLayout.implicitHeight
|
||||
implicitWidth: modCardLayout.implicitWidth
|
||||
|
||||
GridLayout {
|
||||
id: modCardLayout
|
||||
|
||||
columnSpacing: Kirigami.Units.largeSpacing
|
||||
columns: root.wideScreen ? 4 : 2
|
||||
rowSpacing: Kirigami.Units.largeSpacing
|
||||
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
top: parent.top
|
||||
}
|
||||
|
||||
// TODO: Replace this with an image once we can get them
|
||||
|
||||
Kirigami.Padding {
|
||||
padding: Kirigami.Units.largeSpacing
|
||||
|
||||
contentItem: Kirigami.Icon {
|
||||
implicitHeight: Kirigami.Units.iconSizes.huge
|
||||
implicitWidth: implicitHeight
|
||||
source: "package-x-generic"
|
||||
}
|
||||
}
|
||||
ColumnLayout {
|
||||
Kirigami.Heading {
|
||||
Layout.fillWidth: true
|
||||
text: mod.name
|
||||
type: Kirigami.Heading.Type.Primary
|
||||
}
|
||||
Kirigami.Separator {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
Controls.Label {
|
||||
property string desc: mod.description
|
||||
|
||||
Layout.fillWidth: true
|
||||
font.italic: desc ? false : true
|
||||
text: desc ? desc : "No description available"
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
ColumnLayout {
|
||||
Layout.alignment: Qt.AlignRight
|
||||
|
||||
Controls.Switch {
|
||||
checked: mod.enabled
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
|
||||
onClicked: mod.enabled = checked
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
from pathlib import Path
|
||||
|
||||
from PySide6.QtCore import Property, QObject, Signal, Slot
|
||||
from PySide6.QtQml import QmlElement, QmlUncreatable
|
||||
|
||||
from leek.mod import Mod
|
||||
|
||||
QML_IMPORT_NAME = "Leek"
|
||||
QML_IMPORT_MAJOR_VERSION = 1
|
||||
|
||||
|
||||
# Qt follows C++ naming conventions
|
||||
# ruff: noqa: N802
|
||||
@QmlElement
|
||||
@QmlUncreatable("QMod is intended to be returned by a QModListModel")
|
||||
class QMod(QObject):
|
||||
"""
|
||||
Qt wrapper around the Mod() class
|
||||
"""
|
||||
|
||||
__mod: Mod
|
||||
|
||||
def __init__(self, mod_path: Path, parent=None) -> None:
|
||||
QObject.__init__(self, parent)
|
||||
try:
|
||||
self.__mod = Mod(mod_path)
|
||||
except:
|
||||
# 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
|
||||
|
||||
@Property(str, constant=True)
|
||||
def description(self) -> str | None:
|
||||
return self.__mod.description
|
||||
|
||||
@Property(str, constant=True)
|
||||
def longDescription(self) -> str | None:
|
||||
return self.__mod.long_description
|
||||
|
||||
@Property(list, constant=True)
|
||||
def authors(self) -> list[str] | None:
|
||||
return self.__mod.authors
|
||||
|
||||
mod_enabled = Signal(name="enabled")
|
||||
|
||||
@Property(bool, notify=mod_enabled)
|
||||
def enabled(self) -> bool:
|
||||
return self.__mod.enabled
|
||||
|
||||
@enabled.setter # type: ignore[no-redef]
|
||||
def enabled(self, value: bool) -> None:
|
||||
self.__mod.enabled = value
|
||||
self.mod_enabled.emit()
|
||||
|
|
@ -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
|
||||
35
src/main.cpp
Normal file
35
src/main.cpp
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
#include <KIconTheme>
|
||||
#include <KLocalizedContext>
|
||||
#include <KLocalizedString>
|
||||
#include <QApplication>
|
||||
#include <QQmlApplicationEngine>
|
||||
#include <QQuickStyle>
|
||||
#include <QUrl>
|
||||
#include <QtQml>
|
||||
#include <qapplication.h>
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
KIconTheme::initTheme();
|
||||
QApplication app(argc, argv);
|
||||
KLocalizedString::setApplicationDomain("leek");
|
||||
QApplication::setOrganizationName(QStringLiteral("Toast"));
|
||||
QApplication::setOrganizationDomain(QStringLiteral("toast003.xyz"));
|
||||
QApplication::setApplicationName(QStringLiteral("Leek"));
|
||||
QApplication::setDesktopFileName(QStringLiteral("xyz.toast003.leek"));
|
||||
|
||||
QApplication::setStyle(QStringLiteral("breeze"));
|
||||
if (qEnvironmentVariableIsEmpty("QT_QUICK_CONTROLS_STYLE")) {
|
||||
QQuickStyle::setStyle(QStringLiteral("org.kde.desktop"));
|
||||
}
|
||||
|
||||
QQmlApplicationEngine engine;
|
||||
|
||||
engine.rootContext()->setContextObject(new KLocalizedContext(&engine));
|
||||
engine.loadFromModule("xyz.toast003.leek", "Main");
|
||||
|
||||
if (engine.rootObjects().isEmpty()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return app.exec();
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue