diff --git a/dzgui/api/servers.py b/dzgui/api/servers.py index fbc09e2..82d5e10 100644 --- a/dzgui/api/servers.py +++ b/dzgui/api/servers.py @@ -191,6 +191,7 @@ def parse_json(json: list) -> list: return rows + def query_direct(ip: str, qport: int, timeout: float = 3.0) -> dict | None: try: info = a2s.info((ip, qport), timeout) @@ -238,12 +239,12 @@ class Details: success: bool -@dataclass(slots=True, frozen=True) -class Prereqs: - password: bool - gameport: int - appid: Union[int, None] - version: Union[str, None] +#@dataclass(slots=True, frozen=True) +#class Prereqs: +# password: bool +# gameport: int +# appid: Union[int, None] +# version: Union[str, None] @dataclass(slots=True) @@ -256,19 +257,25 @@ class Record: gameport: int qport: int +@dataclass(frozen=True, slots=True) +class PreReqs: + record: Record + source: a2s.SourceInfo -def get_prereqs(ip: str, qport: int) -> Prereqs: - try: - info = a2s.info((ip, qport)) - except TimeoutError: - return Prereqs(False, 0, None, None) - gameport = getattr(info, "port", 0) - is_password = getattr(info, "password_protected", False) - appid = getattr(info, "game_id", None) - version = getattr(info, "version", None) - return Prereqs(is_password, gameport, appid, version) +#def get_prereqs(ip: str, qport: int) -> Prereqs: +# try: +# info = a2s.info((ip, qport)) +# except TimeoutError: +# return Prereqs(False, 0, None, None) +# +# gameport = getattr(info, "port", 0) +# is_password = getattr(info, "password_protected", False) +# appid = getattr(info, "game_id", None) +# version = getattr(info, "version", None) +# +# return Prereqs(is_password, gameport, appid, version) def get_details(record: Record) -> Details: @@ -443,13 +450,13 @@ def get_rules(record: Record) -> list["DayzMod"]: #return [] -def query_by_id(addr: str, key: str) -> dict[Any] | None: +def query_by_id(addr: str, key: str, full: bool = False) -> dict[Any] | None: """ Used with numeric Battlemetrics IDs """ try: resolved = map_id_to_record(key, addr) - return query_direct(resolved.ip, resolved.qport) + return query_direct(resolved.ip, resolved.qport, full) except Exception as e: logger.critical(e) return None @@ -470,12 +477,19 @@ def query_playercount(record: Record) -> tuple[int, int] | None: return None -def query_by_ip(addr: str) -> dict[Any] | None: +def query_by_ip(addr: str, full: bool = False) -> dict[Any] | None: record = short_ip_to_record(addr) - return query_by_record(record) + return query_by_record(record, full) +def query_by_record(record: Record, full: bool = False) -> dict[Any] | None: + if full: + try: + info = a2s.info((record.ip, record.qport), 3.0) + return PreReqs(record, info) + except Exception as e: + logger.critical(e) + return None -def query_by_record(record: Record) -> dict[Any] | None: try: return query_direct(record.ip, record.qport) except Exception as e: @@ -504,6 +518,11 @@ def response_to_fqip(res: dict) -> str: return f"{ip}:{gameport}:{qport}" +def source_info_to_record(res: a2s.SourceInfo) -> Record: + print(res) + return + return Record(ip, int(gameport), int(qport)) + def response_to_record(res: dict) -> Record: ip = res["addr"].split(":")[0] gameport = res["gameport"] diff --git a/dzgui/const/constants.py b/dzgui/const/constants.py index 105fcc5..c50f0fe 100644 --- a/dzgui/const/constants.py +++ b/dzgui/const/constants.py @@ -39,6 +39,7 @@ SEARCH_ICON = "system-search-symbolic" STEAM_ICON = "steam_tray_mono" VIEW_CONCEAL = "view-conceal-symbolic" VIEW_REVEAL = "view-reveal-symbolic" +ERROR = "dialog-error-symbolic" WARNING = "dialog-warning-symbolic" WEB_BROWSER = "web-browser-symbolic" @@ -61,5 +62,6 @@ LEGACY_IPS_PATH = ".local/share/dzgui/helpers/ips.csv" DEBUG_LOG = f"{APP_NAME}_DEBUG.LOG" SYSTEM_LOG = f"{APP_NAME}_SYSTEM.LOG" CHANGELOG_PATH = "data/CHANGELOG.md" +CSS_PATH = "data/app.css" LOG_FILTERS = ("CRITICAL", "WARNING", "INFO", "DEBUG") diff --git a/dzgui/managers/connection.py b/dzgui/managers/connection.py index 6e1cab3..e49489c 100644 --- a/dzgui/managers/connection.py +++ b/dzgui/managers/connection.py @@ -1,5 +1,9 @@ -from typing import Any, TYPE_CHECKING +import shutil +from pathlib import Path +from typing import Any, Union, TYPE_CHECKING + +import dzgui.api.pefile as PeFile import dzgui.api.servers as Servers from dzgui.api.mods import get_local_mod_ids @@ -15,6 +19,7 @@ gi.require_version("Gtk", "3.0") from gi.repository import Gtk # noqa E402 if TYPE_CHECKING: + from dzgui.api.servers import PreReqs from dzgui.controllers.mc import Controller @@ -26,35 +31,50 @@ class ConnectionManager: @call_on_thread(dialog.querying) def connect_by_id(self, addr: str, key: str) -> None: - res = Servers.query_by_id(addr, key) + res = Servers.query_by_id(addr, key, full=True) self._prepare_connection(res) @call_on_thread(dialog.querying) def connect_by_ip(self, addr: str) -> None: - res = Servers.query_by_ip(addr) + res = Servers.query_by_ip(addr, full=True) self._prepare_connection(res) @call_on_thread(dialog.querying) def connect_by_record(self, record: Servers.Record) -> None: - res = Servers.query_by_record(record) + res = Servers.query_by_record(record, full=True) self._prepare_connection(res) - def _prepare_connection(self, res: dict[Any] | None) -> None: + def _prepare_connection(self, res: Union["PreReqs", None]) -> None: + failure_func = StoredFunc(self._server_timeout) + if res is None: - self.thread_man.set_cleanup_func( - StoredFunc(self._server_timeout), destroy_first=True - ) + self.thread_man.set_cleanup_func(failure_func, destroy_first=True) return - record = Servers.response_to_record(res) + record = res.record + info = res.source + try: # TODO: proper error handling (currently returns empty list) mods = self._query_modlist(record) except Exception: - self.thread_man.set_cleanup_func( - StoredFunc(self._server_timeout), destroy_first=True - ) + self.thread_man.set_cleanup_func(failure_func, destroy_first=True) return + + # TODO: get missing mod diff + # TODO: get missing mod sizes + + try: + path = Path(self.controller.query_config(Preferences.DEFAULT)) + dayz_path = PeFile.get_pefile_path(path, info.game_id) + total, used, free = shutil.disk_usage(dayz_path) + free_mib = free / (1024**2) + print(free_mib) + except Exception: + # TODO: if this fails, need to show missing build warning + # logger.warning(e) + self.thread_man.set_cleanup_func(failure_func, destroy_first=True) + func = StoredFunc(self.controller.open_connection_assistant, res, mods) self.thread_man.set_cleanup_func(func, destroy_first=True) diff --git a/dzgui/strings/preconnect.py b/dzgui/strings/preconnect.py new file mode 100644 index 0000000..b2a522d --- /dev/null +++ b/dzgui/strings/preconnect.py @@ -0,0 +1,7 @@ +update_mods = "Update mods and connect" +back = "Back" +cancel = "Cancel" +warnings = "Warnings" +errors = "Errors" +mods = "Mods" +total_mods = "Total mods: " diff --git a/dzgui/util/css.py b/dzgui/util/css.py index 3c18249..44bf7cd 100644 --- a/dzgui/util/css.py +++ b/dzgui/util/css.py @@ -1,4 +1,9 @@ +from importlib import resources + +from dzgui.const.constants import APP_NAME_LOWER, CSS_PATH + import gi + gi.require_version("Gtk", "3.0") from gi.repository import Gtk, Gdk # noqa E402 @@ -18,38 +23,7 @@ def remove_class(widget: Gtk.Widget, label: str) -> None: def load_css() -> None: - # TODO: consider storing this in a data file - css = """ - .invalid-entry { - border-color: red; - } - .frame { - border: 0px; - } - .toast-label { - background-color: black; - border: 1px solid white; - } - .frame > border { - border-radius: 5px; - padding: 5px; - } - .page-heading { - font-size: 1.5rem; - font-weight: 800; - margin-bottom: 0.5rem; - } - .settings-subheading { - font-size: 1.2rem; - font-weight: 700; - } - .left-label { - font-size: 1.3rem; - } - .details-heading { - font-size: 1.2rem - } - """ + css = resources.read_text(APP_NAME_LOWER, CSS_PATH) prov = Gtk.CssProvider() prov.load_from_data(css.encode("ascii")) screen = Gdk.Screen.get_default() diff --git a/dzgui/views/components/labels.py b/dzgui/views/components/labels.py index 3a5c243..9ecc52a 100644 --- a/dzgui/views/components/labels.py +++ b/dzgui/views/components/labels.py @@ -21,3 +21,14 @@ class LeftLabel(Gtk.Label): halign=Gtk.Align.START, ) self.set_tooltip_text(tooltip) + + +class IconLabel(Gtk.Box): + def __init__(self, text: str, label: str) -> None: + super().__init__( + orientation=Gtk.Orientation.HORIZONTAL, spacing=10, margin_start=10 + ) + self.label = Gtk.Label(label=text, halign=Gtk.Align.START, margin_bottom=2) + self.icon = Gtk.Image.new_from_icon_name(label, Gtk.IconSize.BUTTON) + for el in self.icon, self.label: + self.add(el) diff --git a/dzgui/views/pages/preconnect.py b/dzgui/views/pages/preconnect.py index 4134743..731daa7 100644 --- a/dzgui/views/pages/preconnect.py +++ b/dzgui/views/pages/preconnect.py @@ -6,9 +6,14 @@ from dzgui.const.constants import ( APPID_DAYZ_EXP, APPNAME_DAYZ, APPNAME_DAYZ_EXP, + ERROR, + WARNING, ) from dayzquery import DayzMod +from dzgui.util.css import add_class +from dzgui.strings import preconnect from dzgui.views.components.frame import HeadingFrame +from dzgui.views.components.labels import IconLabel from dzgui.views.trees.tree_server_mods import ServerModTreeView import gi @@ -18,13 +23,23 @@ from gi.repository import Gtk # type: ignore # noqa E402 if TYPE_CHECKING: + from dzgui.api.servers import PreReqs from dzgui.controllers.mc import Controller -# TODO: pack entire box in scrolled window -class PreConnectionAssistant(Gtk.Box): +class WarningLabel(IconLabel): + def __init__(self, text: str) -> None: + super().__init__(text, WARNING) + + +class ErrorLabel(IconLabel): + def __init__(self, text: str) -> None: + super().__init__(text, ERROR) + + +class PreConnectionAssistant(Gtk.ScrolledWindow): def __init__(self, controller: "Controller") -> None: - super().__init__(orientation=Gtk.Orientation.VERTICAL) + super().__init__() self.controller = controller @@ -44,11 +59,11 @@ class PreConnectionAssistant(Gtk.Box): # TODO: strings # TODO: dynamic button text if no mods needed - self.back = Gtk.Button(label="Back", halign=Gtk.Align.START) + self.back = Gtk.Button(label=preconnect.back, halign=Gtk.Align.START) self.cancel = Gtk.Button( - label="Cancel", halign=Gtk.Align.END, sensitive=False, hexpand=True + label=preconnect.cancel, halign=Gtk.Align.END, sensitive=False, hexpand=True ) - self.ok = Gtk.Button(label="Update mods and connect", halign=Gtk.Align.END) + self.ok = Gtk.Button(label=preconnect.update_mods, halign=Gtk.Align.END) self.button_box = Gtk.Box( orientation=Gtk.Orientation.HORIZONTAL, @@ -63,9 +78,11 @@ class PreConnectionAssistant(Gtk.Box): self.ok.connect("clicked", self._on_ok_clicked) self.title = Gtk.Label(label="") + add_class(self.title, "preconnect-heading") self.tree = ServerModTreeView(self.controller) self.mod_count = Gtk.Label(label="", halign=Gtk.Align.START, margin_start=5) + # TODO: live count of remaining downloads # "Steam is downloading: {mod_name}" # mention whether manual or auto mod is active @@ -78,7 +95,7 @@ class PreConnectionAssistant(Gtk.Box): self.tree_box.add(self.scrolled) self.tree_box.add(self.mod_count) - self.tree_frame = HeadingFrame(self.tree_box, "Mods") + self.tree_frame = HeadingFrame(self.tree_box, preconnect.mods) self.warnings = [] # TODO: populate with strings and icons @@ -94,12 +111,23 @@ class PreConnectionAssistant(Gtk.Box): - steam is not running - server has password """ - frame = HeadingFrame(Gtk.Label(label="ITEM ONE"), "Warnings") + # TODO: abstract into components + warning_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + warning_box.add(WarningLabel("Warning 1")) + self.warning_frame = HeadingFrame(warning_box, preconnect.warnings) - self.add(self.title) - self.add(self.tree_frame) - self.add(frame) - self.add(self.button_box) + error_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + error_box.add(ErrorLabel("Error 1")) + self.error_frame = HeadingFrame(error_box, preconnect.errors) + + self.box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + self.box.add(self.title) + self.box.add(self.tree_frame) + self.box.add(self.warning_frame) + self.box.add(self.error_frame) + self.box.add(self.button_box) + + self.add(self.box) def _on_ok_clicked(self, button: Gtk.Button) -> None: # TODO: update mod store in place with spinner/toast @@ -110,10 +138,12 @@ class PreConnectionAssistant(Gtk.Box): page = self.controller.get_prior_page() self.controller.open_page(page) - def populate(self, res: dict[Any], mods: list["DayzMod"]) -> None: + def populate(self, res: "PreReqs", mods: list["DayzMod"]) -> None: self.tree.populate(mods) total = len(mods) - self.title.set_text(f"Connecting to {res["name"]}") + + name = res.source.server_name + self.title.set_text(name) if total < 1: self.tree_frame.set_visible(False) self.mod_count.set_visible(False) @@ -121,14 +151,13 @@ class PreConnectionAssistant(Gtk.Box): else: self.tree.set_visible(True) self.mod_count.set_visible(True) - self.mod_count.set_text(f"Total mods: {str(total)}") + prefix = preconnect.total_mods + self.mod_count.set_text(f"{prefix}{str(total)}") # TODO: check which mods need updating - # TODO: fourth column: needs update # steam_path = self.controller.get_config_man().lookup(Preferences.DEFAULT) # dayz_version = PeFile.get_pretty_version(steam_path, APPID_DAYZ) # dayz_exp_version = PeFile.get_pretty_version(steam_path, APPID_DAYZ_EXP) - pass def download_mods(self) -> None: pass