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

View File

@ -124,8 +124,8 @@ class Emitter(GObject.GObject):
def load_maps(self, store: ListStore) -> None: def load_maps(self, store: ListStore) -> None:
pass pass
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=()) @GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=(str,))
def mod_page_loaded(self) -> None: def mods_updated(self, msg: str) -> None:
pass pass
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=()) @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.connection import ConnectionManager
from dzgui.managers.contextmenu import ContextMenuManager from dzgui.managers.contextmenu import ContextMenuManager
from dzgui.managers.notes import NoteManager from dzgui.managers.notes import NoteManager
from dzgui.model.proxy_model import ProxyModelManager
from dzgui.model.servers import ServerModelManager from dzgui.model.servers import ServerModelManager
from dzgui.model.model_factory import ModelFactory from dzgui.model.model_factory import ModelFactory
from dzgui.util import strings from dzgui.util import strings
@ -122,25 +121,6 @@ class Controller(GObject.GObject):
def get_emitter(self) -> Emitter: def get_emitter(self) -> Emitter:
return 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: def register_widget(self, attr: str, widget: Gtk.Widget) -> None:
try: try:
setattr(self.mediator, attr, widget) setattr(self.mediator, attr, widget)
@ -282,135 +262,22 @@ class Controller(GObject.GObject):
client = self.query_config(Preferences.CLIENT) client = self.query_config(Preferences.CLIENT)
open_user_workshop(uid, 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: def uncolorize_mods(self) -> None:
model = self.get_mod_store() self.mod_man.uncolorize_mods()
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)
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: def highlight_stale(self) -> None:
# TODO: set progress bar for number of mods self.mod_man.highlight_stale()
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)
def toggle_mod_selection(self, state: bool) -> None: def toggle_mod_selection(self, state: bool) -> None:
sel = self.mediator.modtreeview.get_selection() self.mod_man.toggle_mod_selection(state)
if state:
sel.select_all()
else:
sel.unselect_all()
# TODO: signal
self.mediator.modtreeview.grab_focus()
def load_mods_cleanup(self, model: Gtk.ListStore) -> None: def delete_mods(self) -> None:
self.mediator.modtreeview.set_model(model) self.mod_man.delete_mods()
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 get_mod_store(self) -> Gtk.TreeModel | None: def get_mod_store(self) -> Gtk.TreeModel | None:
return self.mediator.modtreeview.get_model() return self.mediator.modtreeview.get_model()
@ -652,3 +519,6 @@ class Controller(GObject.GObject):
log_man = LogManager() log_man = LogManager()
alerts = log_man.get_alerts(self.prefs.paths.debug) alerts = log_man.get_alerts(self.prefs.paths.debug)
return alerts return alerts
def get_modtreeview(self) -> "ModTreeView":
return self.mediator.modtreeview

View File

@ -44,14 +44,7 @@ class ContextMenuManager:
case ContextMenu.COPY_SERVER_NAME: case ContextMenu.COPY_SERVER_NAME:
self.copy_server_name() self.copy_server_name()
case ContextMenu.DELETE_MOD: case ContextMenu.DELETE_MOD:
print("DEBUG: unimplemented") self.controller.delete_mods()
# 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
case ContextMenu.OPEN_WORKSHOP: case ContextMenu.OPEN_WORKSHOP:
self.open_mod_page() self.open_mod_page()
case ContextMenu.SET_FAV: case ContextMenu.SET_FAV:

View File

@ -81,7 +81,7 @@ class ModSelectionPanel(Gtk.Box):
case ModButton.UNSELECT_ALL: case ModButton.UNSELECT_ALL:
self.controller.toggle_mod_selection(False) self.controller.toggle_mod_selection(False)
case ModButton.DELETE_SELECTED: case ModButton.DELETE_SELECTED:
self.controller.delete_multiple_mods() self.controller.delete_mods()
case ModButton.HIGHLIGHT_STALE: case ModButton.HIGHLIGHT_STALE:
self.controller.highlight_stale() self.controller.highlight_stale()
case ModButton.UNHIGHLIGHT_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_started", self._on_distcalc_started)
self.emitter.connect("distcalc_ended", self._on_distcalc_ended) self.emitter.connect("distcalc_ended", self._on_distcalc_ended)
self.emitter.connect("servers_loaded", self._on_servers_loaded) 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) self.emitter.connect("log_page_loaded", self._on_log_page_loaded)
def _on_alerts_clicked(self, button: LoggerAlertsButton) -> None: def _on_alerts_clicked(self, button: LoggerAlertsButton) -> None:
@ -70,8 +70,7 @@ class Statusbar(Gtk.Grid):
# FIXME: fails if button was not previously drawn # FIXME: fails if button was not previously drawn
self.alert_button.hide() self.alert_button.hide()
def _on_mod_page_loaded(self, emitter: "Emitter") -> None: def _on_mods_updated(self, emitter: "Emitter", msg: str) -> None:
msg = self.controller.format_mod_statusbar()
self.set_by_context(NotebookPage.MODS, msg) self.set_by_context(NotebookPage.MODS, msg)
def _on_notebook_page_changed( def _on_notebook_page_changed(

View File

@ -26,7 +26,7 @@ class ModTreeView(ModsMixin, ContextMixin, TreeView):
super().__init__(controller, menu=ContextMenuGroup.MOD) super().__init__(controller, menu=ContextMenuGroup.MOD)
self.controller = controller self.controller = controller
emitter = self.controller.get_emitter() 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) emitter.connect("mods_highlighted", self._on_mods_highlighted)
self.set_fixed_height_mode(True) self.set_fixed_height_mode(True)
@ -62,7 +62,7 @@ class ModTreeView(ModsMixin, ContextMixin, TreeView):
self.get_selection().unselect_all() self.get_selection().unselect_all()
# TODO: test loading with no mods # 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) self.set_cursor(0)
def get_selected_mod(self) -> str: def get_selected_mod(self) -> str: