diff --git a/dzgui/api/mods.py b/dzgui/api/mods.py index 5e833ad..4a37fbd 100644 --- a/dzgui/api/mods.py +++ b/dzgui/api/mods.py @@ -109,6 +109,7 @@ def get_missing_mods(local: list, remote: list) -> list: return [mod for mod in remote if mod not in local] +# FIXME: steam path is missing def get_server_modlist(server: Record, steam: Path) -> list: try: rules = dayzquery.dayz_rules((server.ip, server.qport)) diff --git a/dzgui/api/servers.py b/dzgui/api/servers.py index 84e2e1d..5aee04f 100644 --- a/dzgui/api/servers.py +++ b/dzgui/api/servers.py @@ -273,7 +273,7 @@ def get_prereqs(ip: str, qport: int) -> Prereqs: return Prereqs(is_password, gameport, appid, version) -def details(record: Record) -> Details: +def get_details(record: Record) -> Details: ip = record.ip qport = record.qport default_str = strings.none_provided @@ -448,9 +448,11 @@ def validate_ip(addr: str) -> Record: return record -def get_rules(ip: str, qport: int) -> list[int]: +def get_rules(record: Record) -> list[int]: + ip = record.ip + qport = record.qport try: - mods = dayzquery.dayz_rules((ip, int(qport))).mods + mods = dayzquery.dayz_rules((ip, qport)).mods return [mod for mod in mods] except Exception as e: logger.critical(e) diff --git a/dzgui/controllers/mc.py b/dzgui/controllers/mc.py index 728bdc2..a77bf53 100644 --- a/dzgui/controllers/mc.py +++ b/dzgui/controllers/mc.py @@ -615,3 +615,6 @@ class Controller(GObject.GObject): def get_details(self, record: "Record") -> None: ConnectionManager(self).query_details(record) + + def get_modlist(self, record: "Record") -> None: + ConnectionManager(self).query_modlist(record) diff --git a/dzgui/managers/connection.py b/dzgui/managers/connection.py index bbd35ba..5c677de 100644 --- a/dzgui/managers/connection.py +++ b/dzgui/managers/connection.py @@ -4,7 +4,7 @@ import dzgui.api.servers as Servers from dzgui.managers.thread_man import call_on_thread, StoredFunc, ThreadingManager from dzgui.util.strings import api_warn_msg, dialog, server_timeout from dzgui.views.dialogs.generic import ExceptionDialog -from dzgui.views.dialogs.server_details import ServerDetailsDialog +from dzgui.views.dialogs.servers import ServerDetailsDialog, ServerModDialog import gi @@ -57,7 +57,7 @@ class ConnectionManager: @call_on_thread(dialog.querying) def query_details(self, record: Servers.Record) -> None: - details = Servers.details(record) + details = Servers.get_details(record) if details.success is False: self.thread_man.set_cleanup_func( StoredFunc(self._server_timeout), destroy_first=True @@ -67,6 +67,31 @@ class ConnectionManager: StoredFunc(self._present_details_dialog, details), destroy_first=True ) + @call_on_thread(dialog.querying) + def query_modlist(self, record: Servers.Record) -> None: + mods = Servers.get_rules(record) + # TODO: test if locally installed + # see api.mods for this same logic + # call controller to look up steam path from ConfigManager + if len(mods) == 0: + # TODO: message for no mods + # TODO: message for actual timeout + self.thread_man.set_cleanup_func( + StoredFunc(self._server_timeout), destroy_first=True + ) + return + alpha_mods = [[mod.name, str(mod.workshop_id), "Test"] for mod in mods] + alpha_mods.sort(key=lambda x: x[0]) + + self.thread_man.set_cleanup_func( + StoredFunc(self._present_modlist_dialog, alpha_mods), destroy_first=True + ) + + # TODO: data type is dayzquery.DayzMod + def _present_modlist_dialog(self, mods: list[str]) -> None: + dialog = ServerModDialog(self.controller, mods) + dialog.run() + def _present_details_dialog(self, details: Servers.Details) -> None: dialog = ServerDetailsDialog(self.controller, details) dialog.run() @@ -74,8 +99,3 @@ class ConnectionManager: def _server_timeout(self) -> None: dialog = ExceptionDialog(self.controller, server_timeout) dialog.run() - - # def _connection_failure(self) -> None: - # # FIXME: returns api warning, but this is more likely a localized server issue - # dialog = ExceptionDialog(self.controller, api_warn_msg) - # dialog.run() diff --git a/dzgui/managers/contextmenu.py b/dzgui/managers/contextmenu.py index 771e0e3..a2c54f6 100644 --- a/dzgui/managers/contextmenu.py +++ b/dzgui/managers/contextmenu.py @@ -81,8 +81,8 @@ class ContextMenuManager: record = self.treeview.get_record() self.controller.get_details(record) case ContextMenu.SHOW_MODS: - print("DEBUG: unimplemented") - pass + record = self.treeview.get_record() + self.controller.get_modlist(record) def copy_server_ip(self) -> None: if not isinstance(self.treeview, ServerTreeView): diff --git a/dzgui/model/model_factory.py b/dzgui/model/model_factory.py index 0c62ba8..4ac37fc 100644 --- a/dzgui/model/model_factory.py +++ b/dzgui/model/model_factory.py @@ -53,7 +53,7 @@ class LogCols: @dataclass(slots=True, frozen=True) class ServerModCols: name: str - uid: int # GObject.TYPE_INT64 + uid: str installed: str diff --git a/dzgui/views/dialogs/server_mods.py b/dzgui/views/dialogs/server_mods.py index 1a86a81..c00c16b 100644 --- a/dzgui/views/dialogs/server_mods.py +++ b/dzgui/views/dialogs/server_mods.py @@ -1,25 +1,48 @@ import gi +import textwrap +from typing import TYPE_CHECKING + +from dzgui.const.constants import EXPAND, FILL +from dzgui.model.model_factory import ModelFactory +from dzgui.util import strings +from dzgui.views.dialogs.generic import GenericDialog + gi.require_version("Gtk", "3.0") from gi.repository import Gtk, GLib, Gdk, GObject, Pango # noqa -class ModDialog(GenericDialog): - def __init__(self, record: str): +if TYPE_CHECKING: + from dzgui.api.servers import Record + + +class ServerModDialog(GenericDialog): + def __init__(self, controller, mods: list[str]): + # TODO: center secondary text - msg = strings.workshop - super().__init__(textwrap.dedent(msg), Popup.MODLIST) + msg = textwrap.dedent(strings.workshop) + super().__init__( + controller=controller, + text=msg, + buttons="Gtk.ButtonsType.OK", + mtype="Gtk.MessageType.INFO", + secondary="", + ) + + self.mod_store = ModelFactory().make_server_mod_store() dialogBox = self.get_content_area() self.set_default_response(Gtk.ResponseType.OK) - self.set_size_request(800, 500) + self.set_size_request(800, 700) self.scrollable = Gtk.ScrolledWindow() self.view = Gtk.TreeView( enable_search=False, search_column=-1, fixed_height_mode=True ) self.scrollable.add(self.view) - set_surrounding_margins(self.scrollable, 20) + # set_surrounding_margins(self.scrollable, 20) + self.connect("response", self._on_response) self.view.connect("row-activated", self._on_row_activated) + self.view.set_model(self.mod_store) for i, column_title in enumerate(strings.server_mod_cols): renderer = Gtk.CellRendererText(ellipsize=Pango.EllipsizeMode.END) @@ -36,46 +59,20 @@ class ModDialog(GenericDialog): pass dialogBox.pack_end(self.scrollable, EXPAND, FILL, 0) - wait_dialog = GenericDialog(strings.modlist, Popup.WAIT) - wait_dialog.show_all() - thread = threading.Thread( - target=self._background, args=(wait_dialog, record) - ) - thread.start() + mod_count = len(mods) + self.set_markup(f"Modlist ({mod_count} mods") + for mod in mods: + self.mod_store.append(mod) - # TODO: why are we passing record here? - def _background(self, dialog: "GenericDialog", record: str) -> None: - def _load(): - dialog.destroy() - # TODO: natively implement - #if data.returncode == 1: - # AppNav.window.spawn_dialog(strings.server_error, Popup.NOTIFY) - # return - self.show_all() - self.set_markup(f"Modlist ({mod_count} mods)") - self.run() + self.show_all() + + # TODO: handle response a la details dialog + + def _on_keypress(self, view: Gtk.TreeView, event: Gdk.EventKey) -> None: + if event.keyval == Gdk.KEY_Escape: self.destroy() - - record = AppNav.treeview.get_record() - if not record: - return - - # TODO: thread - try: - # TODO: get default_steam_path from config - modlist = get_server_modlist(record) - mod_count = self._parse_modlist_rows(modlist) - except Exception: - # TODO: needs to pop error dialog - mod_count = 0 - modlist_store.clear() - self.view.set_model(modlist_store) - GLib.idle_add(_load) - - def _parse_modlist_rows(self, rows: list) -> bool | int: - for row in rows: - modlist_store.append(row) - return len(rows) + return True + return False def _on_row_activated( self, @@ -91,4 +88,5 @@ class ModDialog(GenericDialog): path = pathlist[0] tree_iter = model.get_iter(path) mod_id = model.get_value(tree_iter, 1) - call_bash_func("open_workshop_page", mod_id) + print(mod_id) + # call_bash_func("open_workshop_page", mod_id) diff --git a/dzgui/views/dialogs/server_details.py b/dzgui/views/dialogs/servers.py similarity index 52% rename from dzgui/views/dialogs/server_details.py rename to dzgui/views/dialogs/servers.py index 3ef6948..d0abf8e 100644 --- a/dzgui/views/dialogs/server_details.py +++ b/dzgui/views/dialogs/servers.py @@ -1,14 +1,14 @@ -import gi +import textwrap +from typing import Self, TYPE_CHECKING -import dzgui.api.servers as Servers from dzgui.const.constants import EXPAND, FILL, NO_PADDING -from dzgui.const.enum import Popup +from dzgui.model.model_factory import ModelFactory from dzgui.util import css from dzgui.util import strings from dzgui.util.format import format_hyperlinks from dzgui.views.dialogs.generic import GenericDialog -from typing import Self, TYPE_CHECKING +import gi gi.require_version("Gtk", "3.0") from gi.repository import Gtk, GLib, Gdk, GObject, Pango # noqa @@ -17,6 +17,7 @@ if TYPE_CHECKING: from dzgui.api.servers import Details +# TODO: make a more generic base class class ServerDetailsDialog(GenericDialog): def __init__(self, controller, details: "Details"): super().__init__( @@ -99,3 +100,95 @@ class ServerDetailsDialog(GenericDialog): col: Gtk.TreeViewColumn, ) -> None: self.destroy() + + def __init__(self, controller, details: "Details"): + super().__init__( + controller=controller, + text=details.name, + buttons=Gtk.ButtonsType.OK, + mtype=Gtk.MessageType.INFO, + secondary=strings.server_details, + ) + + +# TODO: data type is dayzquery.DayzMod + + +class ServerModDialog(GenericDialog): + def __init__(self, controller, mods: list[str]): + + # TODO: center secondary text + msg = textwrap.dedent(strings.workshop) + super().__init__( + controller=controller, + text=msg, + buttons=Gtk.ButtonsType.OK, + mtype=Gtk.MessageType.INFO, + secondary="", + ) + + self.mod_store = ModelFactory().make_server_mod_store() + + dialogBox = self.get_content_area() + self.set_default_response(Gtk.ResponseType.OK) + self.set_size_request(800, 700) + + self.scrollable = Gtk.ScrolledWindow() + self.view = Gtk.TreeView( + enable_search=False, search_column=-1, fixed_height_mode=True + ) + self.scrollable.add(self.view) + + # set_surrounding_margins(self.scrollable, 20) + self.connect("response", self._on_response) + self.view.connect("row-activated", self._on_row_activated) + self.view.set_model(self.mod_store) + + for i, column_title in enumerate(strings.server_mod_cols): + renderer = Gtk.CellRendererText(ellipsize=Pango.EllipsizeMode.END) + column = Gtk.TreeViewColumn(column_title, renderer, text=i) + column.set_sizing(Gtk.TreeViewColumnSizing.FIXED) + self.view.append_column(column) + column.set_sort_column_id(i) + # FIXME: do not recycle generic string vars + match column_title: + case strings.mod: + column.set_fixed_width(350) + case strings._id: + column.set_fixed_width(200) + case _: + pass + dialogBox.pack_end(self.scrollable, EXPAND, FILL, 0) + + mod_count = len(mods) + self.set_markup(f"Modlist ({mod_count} mods)") + for mod in mods: + self.mod_store.append(mod) + + self.show_all() + + def _on_response(self, dialog: Self, response: Gtk.ResponseType) -> None: + self.destroy() + + def _on_keypress(self, view: Gtk.TreeView, event: Gdk.EventKey) -> None: + if event.keyval == Gdk.KEY_Escape: + self.destroy() + return True + return False + + def _on_row_activated( + self, + treeview: Gtk.TreeView, + tree_iter: Gtk.TreeIter, + col: Gtk.TreeViewColumn, + ) -> None: + select = treeview.get_selection() + sels = select.get_selected_rows() + (model, pathlist) = sels + if len(pathlist) < 1: + return + path = pathlist[0] + tree_iter = model.get_iter(path) + mod_id = model.get_value(tree_iter, 1) + print(mod_id) + # call_bash_func("open_workshop_page", mod_id)