feat: ModManager

This commit is contained in:
aclist 2026-04-30 00:21:18 +09:00
parent efaaf7f434
commit 4f8d397bbc
7 changed files with 26 additions and 165 deletions

View File

@ -9,7 +9,7 @@ from dataclasses import dataclass
from pathlib import Path
import dzgui.api.pefile as PeFile
from dzgui.api.servers import Record, get_rules
from dzgui.api.servers import Record, get_rules, fqip_to_record
from dzgui.const.constants import (
APP_NAME,
APPID_DAYZ,
@ -153,10 +153,8 @@ def remove_stale_signatures(config: Path, versions: Path) -> None:
def find_stale_mods(config: Path) -> list[int]:
def push_record(rec: str) -> list:
add = rec.split(":")
ip = add[0]
qport = add[2]
return get_rules(ip, qport)
record = fqip_to_record(rec)
return [mod.workshop_id for mod in get_rules(record)]
steam = lookup(config, Preferences.DEFAULT)
steam_path = Path(steam)
@ -171,7 +169,8 @@ def find_stale_mods(config: Path) -> list[int]:
wait(futures)
for future in futures:
res = future.result()
remote_mods += res
remote_mods.extend(res)
stale = [mod for mod in local if mod not in remote_mods]
unique_mods = set(remote_mods)
stale = [mod for mod in local if mod not in unique_mods]
return stale

View File

@ -124,8 +124,8 @@ class Emitter(GObject.GObject):
def load_maps(self, store: ListStore) -> None:
pass
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=())
def mod_page_loaded(self) -> None:
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=(str,))
def mods_updated(self, msg: str) -> None:
pass
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=())

View File

@ -41,7 +41,6 @@ from dzgui.managers.config import ConfigManager
from dzgui.managers.connection import ConnectionManager
from dzgui.managers.contextmenu import ContextMenuManager
from dzgui.managers.notes import NoteManager
from dzgui.model.proxy_model import ProxyModelManager
from dzgui.model.servers import ServerModelManager
from dzgui.model.model_factory import ModelFactory
from dzgui.util import strings
@ -122,25 +121,6 @@ class Controller(GObject.GObject):
def get_emitter(self) -> Emitter:
return self.emitter
@deprecated("depends on load mods logic")
def call_on_thread(dialog_str: str) -> Callable:
def decorator(func: Callable) -> Callable:
@wraps(func)
def wrapper(*args, **kwargs):
def callback() -> None:
func(*args, **kwargs)
GLib.idle_add(self._destroy_on_idle)
self = args[0]
self.wait_dialog = WaitDialog(self, dialog_str, jobs=self.pending_jobs)
self.wait_dialog.show_all()
thread = threading.Thread(target=callback)
thread.start()
return wrapper
return decorator
def register_widget(self, attr: str, widget: Gtk.Widget) -> None:
try:
setattr(self.mediator, attr, widget)
@ -282,135 +262,22 @@ class Controller(GObject.GObject):
client = self.query_config(Preferences.CLIENT)
open_user_workshop(uid, client)
### START LOAD MODS LOGIC
def load_mods(self) -> None:
from dzgui.managers.mods import ModManager
self.mod_man = ModManager(self)
def uncolorize_mods(self) -> None:
model = self.get_mod_store()
for mod in model:
it = mod.iter
path = model.get_path(it)
model[path][4] = None
path = Gtk.TreePath.new_from_indices([0])
self.mediator.modtreeview.set_cursor(path)
self.mod_man.uncolorize_mods()
def highlight_stale_cleanup(self, stale_mods: list) -> None:
"""Manipulates attached ListStore in the main event loop"""
model = self.get_mod_store()
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")
# TODO: delegate to mod manager
@call_on_thread(strings.dialog.working)
def highlight_stale(self) -> None:
# TODO: set progress bar for number of mods
stale = find_stale_mods(self.prefs.paths.config)
self.cleanup_func = StoredFunc(self.highlight_stale_cleanup, stale)
def set_cleanup_func(self, func: StoredFunc) -> None:
self.cleanup_func = func
def get_cleanup_func(self) -> StoredFunc:
return self.cleanup_func
def _destroy_on_idle(self) -> None:
self.wait_dialog.destroy()
func = self.get_cleanup_func()
if func is not None:
func.call()
self.set_cleanup_func(None)
self.mediator.window.set_sensitive(True)
self.mod_man.highlight_stale()
def toggle_mod_selection(self, state: bool) -> None:
sel = self.mediator.modtreeview.get_selection()
if state:
sel.select_all()
else:
sel.unselect_all()
# TODO: signal
self.mediator.modtreeview.grab_focus()
self.mod_man.toggle_mod_selection(state)
def load_mods_cleanup(self, model: Gtk.ListStore) -> None:
self.mediator.modtreeview.set_model(model)
self.emitter.emit("mod_page_loaded")
# def load_mods(self) -> None:
# path = self.query_config(Preferences.DEFAULT)
# ModManager(path)
# TODO: delegate to threadmanager
@call_on_thread(strings.dialog.modlist)
def load_mods(self) -> None:
model = ModelFactory().make_mod_store()
path = self.query_config(Preferences.DEFAULT)
mods = get_delimited_mods(Path(path))
model.extend(mods)
self.cleanup_func = StoredFunc(self.load_mods_cleanup, model)
def delete_multiple_mods(self) -> None:
sel = self.mediator.modtreeview.get_selection()
model, pathlist = sel.get_selected_rows()
# NOTE: reverse when multiple selection
for path in reversed(pathlist):
self.delete_single_mod(path)
total_mods, total_size = self.calc_mod_size()
self.format_mod_statusbar()
def delete_single_mod_cleanup(self, _iter: Gtk.TreeIter) -> None:
self.get_mod_store().remove(_iter)
remove_stale_signatures(self.prefs.paths.config, self.prefs.paths.version)
# TODO: strings
# TODO: delegate to LocalModManager or api/mods.py
@call_on_thread("deleting mod")
def delete_single_mod(self, tree_path: Gtk.TreePath) -> None:
config = self.prefs.paths.config
mod, _iter = self.get_mod_from_tree_path(tree_path)
path = lookup(config, Preferences.DEFAULT)
steam_path = Path(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
self.cleanup_func = StoredFunc(self.delete_single_mod_cleanup, _iter)
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.get_mod_store()
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
### END LOAD MODS LOGIC
def delete_mods(self) -> None:
self.mod_man.delete_mods()
def get_mod_store(self) -> Gtk.TreeModel | None:
return self.mediator.modtreeview.get_model()
@ -652,3 +519,6 @@ class Controller(GObject.GObject):
log_man = LogManager()
alerts = log_man.get_alerts(self.prefs.paths.debug)
return alerts
def get_modtreeview(self) -> "ModTreeView":
return self.mediator.modtreeview

View File

@ -44,14 +44,7 @@ class ContextMenuManager:
case ContextMenu.COPY_SERVER_NAME:
self.copy_server_name()
case ContextMenu.DELETE_MOD:
print("DEBUG: unimplemented")
# self.delete_single_mod()
# TODO: connect to emitter automatically
# Gtk.TreeModel, row-inserted/row-deleted
# updates statusbar
# FIXME: signal should instead be emitted off of treeview when rows added/inserted
# self.update_mod_statusbar()
pass
self.controller.delete_mods()
case ContextMenu.OPEN_WORKSHOP:
self.open_mod_page()
case ContextMenu.SET_FAV:

View File

@ -81,7 +81,7 @@ class ModSelectionPanel(Gtk.Box):
case ModButton.UNSELECT_ALL:
self.controller.toggle_mod_selection(False)
case ModButton.DELETE_SELECTED:
self.controller.delete_multiple_mods()
self.controller.delete_mods()
case ModButton.HIGHLIGHT_STALE:
self.controller.highlight_stale()
case ModButton.UNHIGHLIGHT_STALE:

View File

@ -60,7 +60,7 @@ class Statusbar(Gtk.Grid):
self.emitter.connect("distcalc_started", self._on_distcalc_started)
self.emitter.connect("distcalc_ended", self._on_distcalc_ended)
self.emitter.connect("servers_loaded", self._on_servers_loaded)
self.emitter.connect("mod_page_loaded", self._on_mod_page_loaded)
self.emitter.connect("mods_updated", self._on_mods_updated)
self.emitter.connect("log_page_loaded", self._on_log_page_loaded)
def _on_alerts_clicked(self, button: LoggerAlertsButton) -> None:
@ -70,8 +70,7 @@ class Statusbar(Gtk.Grid):
# FIXME: fails if button was not previously drawn
self.alert_button.hide()
def _on_mod_page_loaded(self, emitter: "Emitter") -> None:
msg = self.controller.format_mod_statusbar()
def _on_mods_updated(self, emitter: "Emitter", msg: str) -> None:
self.set_by_context(NotebookPage.MODS, msg)
def _on_notebook_page_changed(

View File

@ -26,7 +26,7 @@ class ModTreeView(ModsMixin, ContextMixin, TreeView):
super().__init__(controller, menu=ContextMenuGroup.MOD)
self.controller = controller
emitter = self.controller.get_emitter()
emitter.connect("mod_page_loaded", self._on_mod_page_loaded)
emitter.connect("mods_updated", self._on_mods_updated)
emitter.connect("mods_highlighted", self._on_mods_highlighted)
self.set_fixed_height_mode(True)
@ -62,7 +62,7 @@ class ModTreeView(ModsMixin, ContextMixin, TreeView):
self.get_selection().unselect_all()
# TODO: test loading with no mods
def _on_mod_page_loaded(self, emitter: "Emitter") -> None:
def _on_mods_updated(self, emitter: "Emitter", msg: str) -> None:
self.set_cursor(0)
def get_selected_mod(self) -> str: