From b8ed0723e158ca4b0ddd71a56b78184d8be4c438 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Thu, 30 Apr 2026 18:48:50 +0900 Subject: [PATCH] feat: add ModManager file --- dzgui/managers/mods.py | 178 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 dzgui/managers/mods.py diff --git a/dzgui/managers/mods.py b/dzgui/managers/mods.py new file mode 100644 index 0000000..46dff80 --- /dev/null +++ b/dzgui/managers/mods.py @@ -0,0 +1,178 @@ +import logging +import shutil + +from pathlib import Path +from typing import TYPE_CHECKING + +from dzgui.api.mods import ( + get_delimited_mods, + get_local_mod_path, + find_stale_mods, + _hash, + remove_stale_signatures, +) +from dzgui.const.constants import APP_NAME, APPID_DAYZ, APPID_DAYZ_EXP +from dzgui.const.enum import Preferences +from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager +from dzgui.model.model_factory import ModelFactory +from dzgui.util.format import format_mods + +import dzgui.api.pefile as PeFile + +import gi + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk # noqa E402 + +if TYPE_CHECKING: + from dzgui.controllers.mc import Controller + +logger = logging.getLogger(APP_NAME) + + +class ModManager: + """ + Because mods may be dynamically updated on the system, + this manager is instantiated each time the Mods page is opened + and persists until the next time the page is opened + """ + def __init__(self, controller: "Controller") -> None: + + self.controller = controller + self.emitter = controller.get_emitter() + self.prefs = controller.get_prefs() + self.path = controller.query_config(Preferences.DEFAULT) + self.treeview = self.controller.get_modtreeview() + + self.thread_man = ThreadingManager(controller) + self._get_mods() + + @call_on_thread("getting mods") + def _get_mods(self) -> list: + mods = get_delimited_mods(self.path) + if len(mods) < 1: + msg = self.format_mod_statusbar() + self.emitter.emit("mods_updated", msg, 0) + return + + func = StoredFunc(self._on_mods_loaded, mods) + self.thread_man.set_cleanup_func(func) + + def _on_mods_loaded(self, mods: list[int]) -> None: + model = ModelFactory().make_mod_store() + model.extend(mods) + self.treeview.set_model(model) + msg = self.format_mod_statusbar() + mods = len(model) + self.emitter.emit("mods_updated", msg, mods) + + @call_on_thread("deleting mods") + def delete_mods(self) -> None: + sel = self.treeview.get_selection() + model, pathlist = sel.get_selected_rows() + # NOTE reverse when multiple selection + iters = [] + for path in reversed(pathlist): + _iter = self.delete_single_mod(path) + iters.append(_iter) + + func = StoredFunc(self._on_mods_deleted, iters) + self.thread_man.set_cleanup_func(func) + + def get_mod_from_tree_path( + self, tree_path: Gtk.TreePath + ) -> tuple[str, Gtk.TreeIter]: + model = self.treeview.get_model() + tree_iter = model.get_iter(tree_path) + mod = model.get(tree_iter, 2)[0] + return mod, tree_iter + + def delete_single_mod(self, tree_path: Gtk.TreePath) -> None: + mod, _iter = self.get_mod_from_tree_path(tree_path) + + steam_path = Path(self.path) + mods_path = get_local_mod_path(steam_path) + app_path = PeFile.get_nested_app_path(steam_path, APPID_DAYZ) + + md5 = _hash(mod) + symlink = app_path / md5 + symlink.unlink() + shutil.rmtree(mods_path / mod) + + # NOTE: second pass to unlink DAYZ_EXP mods + # TODO: test this with working APPID_DAYZ_EXP installation + try: + app_path_exp = PeFile.get_nested_app_path(steam_path, APPID_DAYZ_EXP) + symlink = app_path_exp / md5 + symlink.unlink() + except PeFile.AppNotInstalledError: + pass + return _iter + + def _on_mods_deleted(self, iters: list[Gtk.TreeIter]) -> None: + for _iter in iters: + self.treeview.get_model().remove(_iter) + remove_stale_signatures(self.prefs.paths.config, self.prefs.paths.version) + mods = len(self.treeview.get_model()) + msg = self.format_mod_statusbar() + self.emitter.emit("mods_updated", msg, mods) + + def uncolorize_mods(self) -> None: + model = self.treeview.get_model() + for mod in model: + _iter = mod.iter + path = model.get_path(_iter) + model[path][4] = None + path = Gtk.TreePath.new_from_indices([0]) + self.treeview.set_cursor(path) + + def toggle_mod_selection(self, state: bool) -> None: + sel = self.treeview.get_selection() + if state: + sel.select_all() + else: + sel.unselect_all() + self.treeview.grab_focus() + + def format_mod_statusbar(self) -> str: + total_mods, total_size = self.calc_mod_size() + msg = format_mods(total_size, total_mods) + return msg + + def calc_mod_size(self) -> tuple[int, int]: + model = self.treeview.get_model() + if model is None: + return 0, 0 + total_mods = len(model) + total_size = 0 + for mod in model: + total_size += mod[3] + return total_mods, total_size + + def _on_stale_mods_found(self, stale_mods: list) -> None: + """Manipulates attached ListStore in the main event loop""" + model = self.treeview.get_model() + if model is None: + return + for mod in model: + it = mod.iter + path = model.get_path(it) + if int(mod[2]) in stale_mods: + model[path][4] = True + self.emitter.emit("mods_highlighted") + + @call_on_thread("working") + def highlight_stale(self) -> None: + # TODO: set progress bar for number of mods + stale = find_stale_mods(self.prefs.paths.config) + func = StoredFunc(self._on_stale_mods_found, stale) + self.thread_man.set_cleanup_func(func) + + def select_colorized(self) -> None: + model = self.treeview.get_model() + sel = self.treeview.get_selection() + for mod in model: + it = mod.iter + path = model.get_path(it) + if mod[4] is True: + sel.select_path(path)