Compare commits

..
1 Commits
Author SHA1 Message Date
aclist 6966db8ae7 Merge 74bf4b5cfc into d3f64c1ba5 2026-05-19 18:35:54 +09:00
45 changed files with 221 additions and 771 deletions

No files matched your search

-3
View File
@@ -34,8 +34,6 @@
- Early load alerts button - Early load alerts button
- Preconnect dialog - Preconnect dialog
- Preconnect warnings/failsafes like filesize - Preconnect warnings/failsafes like filesize
- Save filters per server context between sessions
- Preboot progress dialog
## Changed ## Changed
@@ -51,7 +49,6 @@
- Suppress log messages from imported modules - Suppress log messages from imported modules
- Embed Workshop link in Options menu - Embed Workshop link in Options menu
- Disable overlay scrollbars on server tables - Disable overlay scrollbars on server tables
- Reduce size of geolocation DB on disk (~100MB)
## Dropped ## Dropped
- Debug mode - Debug mode
+3 -3
View File
@@ -1,14 +1,14 @@
# TODO: link to here from sphinx docs # TODO: link to here from sphinx docs
# Installing dev dependencies
uv pip install -e .[dev]
# Setting up precommit hooks # Setting up precommit hooks
pre-commit install pre-commit install
# Fetching submodules # Fetching submodules
git submodule update --recursive --init git submodule update --recursive --init
# Installing dev dependencies
uv pip install -e .[dev]
# Building documentation # Building documentation
sphinx-build -M html source build -a sphinx-build -M html source build -a
+1 -1
View File
@@ -34,7 +34,7 @@ DZG-004: On Steam Deck, some mods in the Workshop show a black screen when DZGUI
--------------------------------------------------------------------------------------------------------- ---------------------------------------------------------------------------------------------------------
This is a bug in the Steam client that is being tracked at Valve's Steam for Linux issue tracker here: https://github.com/ValveSoftware/steam-for-linux/issues/9598. This is a bug in the Steam client that is being tracked at Valve's Steam for Linux issue tracker here: https://github.com/ValveSoftware/steam-for-linux/issues/9598.
To resolve this issue, manually intervene in the Steam client by selecting a different context (e.g., Store, Library), waiting for it to load, then navigating back to the Workshop context. This should To resolve this issue, manually intervene in the Steam client by selecting a different context (e.g., Store, Library), waiting for it to load, then navigating back to the Workshop context. This should
clear the blockage and allow the contents to render. clear the blockage and allow the contents to render.
.. _DZG-005: .. _DZG-005:
-1
View File
@@ -472,7 +472,6 @@ def query_by_ip(addr: str) -> A2SInfo:
return A2SInfo(Record("0", 0, 0), None) return A2SInfo(Record("0", 0, 0), None)
return query_by_record(record, update_gameport=True) return query_by_record(record, update_gameport=True)
def query_by_id(server_id: int, key: str) -> A2SInfo: def query_by_id(server_id: int, key: str) -> A2SInfo:
""" """
Used with numeric Battlemetrics IDs Used with numeric Battlemetrics IDs
+41 -52
View File
@@ -23,36 +23,40 @@ def find_download_url(url: str) -> str | None:
def get_ipdb(ips_path: Path) -> None: def get_ipdb(ips_path: Path) -> None:
try: url = find_download_url(DB_IP)
url = find_download_url(DB_IP) if url is None:
if url is None: return
date = find_date(url)
month_file = ips_path.parent / ".month"
if ips_path.exists() and month_file.exists():
old_date = month_file.read_text().rstrip("\n")
if old_date == date:
logger.info(f"IP DB date matches: {date}")
return return
date = find_date(url) # TODO: log additional output
month_file = ips_path.parent / ".month" logger.info(f"Fetching IPDB for {date} from {url}")
if ips_path.exists() and month_file.exists(): try:
old_date = month_file.read_text().rstrip("\n")
if old_date == date:
logger.info(f"IP DB date matches: {date}")
return
# TODO: log additional output
logger.info(f"Fetching IPDB for {date} from {url}")
tmp = serialize(url) tmp = serialize(url)
logger.info(f"Extracting {tmp}")
unzip(tmp, ips_path)
logger.info("Stripping IPv6 records")
strip_ipv6(ips_path)
with open(month_file, "w") as f:
f.write(date)
logger.info(f"Wrote {date} to {month_file}")
except Exception as e: except Exception as e:
# NOTE: in the event of failure, geolocation calc is simply not performed
logger.critical(e) logger.critical(e)
return return
logger.info(f"Extracting {tmp}")
unzip(tmp, ips_path)
logger.info("Stripping IPv6 records")
try:
strip_ipv6(ips_path)
except Exception as e:
logger.critical(e)
return
with open(month_file, "w") as f:
f.write(date)
logger.info(f"Wrote {date} to {month_file}")
def find_date(url: str) -> str: def find_date(url: str) -> str:
date = re.sub(r"(dbip-city-lite-)(.*)(.csv.gz)", r"\2", url) date = re.sub(r"(dbip-city-lite-)(.*)(.csv.gz)", r"\2", url)
@@ -77,34 +81,19 @@ def serialize(url: str) -> Path:
return tmp return tmp
# TODO: optimize this function
# consider using grep
# cf. grep -vE "^[a-z0-9]{4}:" | grep -v "::" > "$ip_file"
def strip_ipv6(path: Path) -> None: def strip_ipv6(path: Path) -> None:
"""Can be IO intensive and cause visual lag on UI frames ips = []
running in the main thread even when run in its own thread; with open(path, "r") as f:
lines are batched into memory-manageable chunks to reduce s = f.read()
disk writes. Raw file can be 8M+ records long, so it is
not read into memory at once.
Relative size is reduced by ~100MB by pruning unwanted columns reg = r"^\d{1,3}\..*"
""" ips = re.findall(reg, s, re.MULTILINE)
# NOTE: "^::," is the boundary line between IPv4 and IPv6 assert ips[0].split(",")[0] == "0.0.0.0"
# NOTE: deprecated regex matching (slower by 5s) assert ips[-1].split(",")[0] == "224.0.0.0"
# reg = r"^\d{1,3}\..*"
alt_path = path.parent.joinpath("ips_stripped.csv") with open(path, "w") as f:
merged = "" for ip in ips:
its = 0 f.write(ip + "\n")
with open(path, "r") as f, open(alt_path, "w") as out:
for line in f:
els = line.split(",")
if "." not in els[0]:
break
final = ",".join([els[0], els[1], els[-2], els[-1]])
merged += final
its += 1
if its == 500:
out.write(merged)
its = 0
merged = ""
if its > 0:
out.write(merged)
path.unlink()
alt_path.rename(path)
+3 -3
View File
@@ -17,7 +17,6 @@ class Xdg:
system: Path system: Path
debug: Path debug: Path
ips: Path ips: Path
filters: Path
def is_writeable(path_str: str) -> bool: def is_writeable(path_str: str) -> bool:
@@ -64,6 +63,9 @@ def get_xdg_paths() -> dict:
def parse_filepaths(xdg: dict) -> Xdg: def parse_filepaths(xdg: dict) -> Xdg:
# cache = xdg["XDG_CACHE_HOME"]
# data = xdg["XDG_DATA_HOME"]
config = xdg["XDG_CONFIG_HOME"] config = xdg["XDG_CONFIG_HOME"]
state = xdg["XDG_STATE_HOME"] state = xdg["XDG_STATE_HOME"]
@@ -78,7 +80,6 @@ def parse_filepaths(xdg: dict) -> Xdg:
history = state / "dzg.history" history = state / "dzg.history"
versions = state / "dzg.versions" versions = state / "dzg.versions"
ips = state / "ips.csv" ips = state / "ips.csv"
filters = state / "dzg.filters.json"
return Xdg( return Xdg(
config, config,
@@ -90,5 +91,4 @@ def parse_filepaths(xdg: dict) -> Xdg:
system, system,
debug, debug,
ips, ips,
filters
) )
-1
View File
@@ -28,7 +28,6 @@ APP_NAME = "DZGUI"
APP_NAME_LOWER = "dzgui" APP_NAME_LOWER = "dzgui"
APP_NAME_ABBR = "dzg" APP_NAME_ABBR = "dzg"
HEX_GREEN = "#32CD32"
HEX_RED = "#FF0000" HEX_RED = "#FF0000"
HEX_ORANGE = "#FFAC1C" HEX_ORANGE = "#FFAC1C"
-4
View File
@@ -12,7 +12,6 @@ if TYPE_CHECKING:
# TODO: if servers fail to load, may leave dangling widgets waiting for a signal # TODO: if servers fail to load, may leave dangling widgets waiting for a signal
class Emitter(GObject.GObject): class Emitter(GObject.GObject):
def __init__(self) -> None: def __init__(self) -> None:
super().__init__() super().__init__()
@@ -151,6 +150,3 @@ class Emitter(GObject.GObject):
def keyword_set(self, keyword: str) -> None: def keyword_set(self, keyword: str) -> None:
pass pass
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=())
def lan_page_initialized(self) -> None:
pass
+11 -30
View File
@@ -84,6 +84,7 @@ class Controller(GObject.GObject):
self.emitter = Emitter() self.emitter = Emitter()
self.emitter.connect("map_selection_changed", self._on_map_selection_changed) self.emitter.connect("map_selection_changed", self._on_map_selection_changed)
self.emitter.connect("check_toggled", self._on_check_toggled) self.emitter.connect("check_toggled", self._on_check_toggled)
self.emitter.connect("servers_loaded_init", self._on_servers_loaded_init)
# NOTE: suppress requests until entire UI is loaded # NOTE: suppress requests until entire UI is loaded
self.loaded = False self.loaded = False
@@ -259,21 +260,6 @@ class Controller(GObject.GObject):
def select_colorized(self) -> None: def select_colorized(self) -> None:
self.mod_man.select_colorized() self.mod_man.select_colorized()
def get_all_tree_filters(self) -> dict[str, dict[str, bool]]:
servers = self.get_servers()
all_filters: dict[str, dict[str, bool]] = {}
trees = (
servers.get_browser(),
servers.get_saved(),
servers.get_recent(),
servers.get_lan(),
)
for tree in trees:
enum = str(tree.get_enum())
filters = tree.get_filter_man().get_filters()
all_filters[enum] = filters
return all_filters
def get_filter_man(self) -> "FilterManager": def get_filter_man(self) -> "FilterManager":
"""Each ServerTreeView has an atomic FilterManager""" """Each ServerTreeView has an atomic FilterManager"""
return self.get_active_treeview().get_filter_man() return self.get_active_treeview().get_filter_man()
@@ -317,22 +303,8 @@ class Controller(GObject.GObject):
self.emitter.emit("servers_loaded", tv.get_enum()) self.emitter.emit("servers_loaded", tv.get_enum())
return return
self.mediator.statusbar.set_by_context(tv.get_enum(), "") self.mediator.statusbar.set_by_context(tv.get_enum(), "")
self.mediator.filters.button_grid.block_toggles(True)
self.populate_filter_prefs()
self.mediator.filters.button_grid.block_toggles(False)
self.emitter.emit("lan_page_initialized")
ServerModelManager(self, tv).load() ServerModelManager(self, tv).load()
def populate_filter_prefs(self) -> None:
tv = self.get_active_treeview()
filters = self.config_man.get_filters(tv)
if filters is None:
return
filter_man = self.get_filter_man()
for filt, state in filters.items():
filter_man.set_filter(filt, state)
self.mediator.filters.set_filters(filters)
def get_dist_cache(self) -> dict[str, "Haversine"]: def get_dist_cache(self) -> dict[str, "Haversine"]:
return self.dist_cache return self.dist_cache
@@ -347,7 +319,16 @@ class Controller(GObject.GObject):
filter_man = self.get_filter_man() filter_man = self.get_filter_man()
return filter_man.get_filters() return filter_man.get_filters()
# FIXME: wipe maps store when changing tabs if model is none # TODO: rename
def _on_servers_loaded_init(self, emitter: "Emitter") -> None:
"""Triggered after servers load but prior to maps loading"""
# FIXME: wipe maps store when changing tabs if model is none
# e.g. select recent, toggle map, then select lan -> not wiped
tv = self.get_active_treeview()
if tv.loaded is False:
return
store = self.get_map_store()
self.emitter.emit("load_maps", store)
def has_server_model(self) -> bool: def has_server_model(self) -> bool:
treeview = self.get_active_treeview() treeview = self.get_active_treeview()
-2
View File
@@ -34,8 +34,6 @@
- Early load alerts button - Early load alerts button
- Preconnect dialog - Preconnect dialog
- Preconnect warnings/failsafes like filesize - Preconnect warnings/failsafes like filesize
- Save filters per server context between sessions
- Preboot progress dialog
## Changed ## Changed
+5
View File
@@ -13,6 +13,11 @@ logger = logging.getLogger(APP_NAME)
def get_local_coords(path: "Path") -> Coords | None: def get_local_coords(path: "Path") -> Coords | None:
try: try:
my_ip = get_local_ip() my_ip = get_local_ip()
except Exception as e:
logger.warn(e)
return None
try:
return get_coords(path, my_ip) return get_coords(path, my_ip)
except Exception as e: except Exception as e:
logger.warn(e) logger.warn(e)
+8 -20
View File
@@ -1,7 +1,6 @@
import shutil import shutil
from pathlib import Path from pathlib import Path
from typing import Any
from dzgui.const.constants import LEGACY_CONFIG_PATH, LEGACY_COLS_PATH, LEGACY_IPS_PATH from dzgui.const.constants import LEGACY_CONFIG_PATH, LEGACY_COLS_PATH, LEGACY_IPS_PATH
from dzgui.config.convert import rc2json from dzgui.config.convert import rc2json
from dzgui.util._json import read_json, write_json from dzgui.util._json import read_json, write_json
@@ -18,28 +17,17 @@ def has_new_config(config: Path) -> bool:
return config.exists() return config.exists()
def convert_cols_file(res: Path) -> Any | None:
j = read_json(res)
cols = j["cols"]
# NOTE: implies prior conversion
if "View" in cols:
return None
# NOTE: user may not have changed these widths in API 6
try:
cols["View"] = cols.pop("Perspective")
cols["Max"] = cols.pop("Maximum")
except Exception:
pass
return j
def migrate_cols_file(res: Path) -> None: def migrate_cols_file(res: Path) -> None:
# NOTE: filename "dzg.columns.json" is API 7 spec # NOTE: dzg.columns.json is API 7 spec
old_res = Path.home() / LEGACY_COLS_PATH old_res = Path.home() / LEGACY_COLS_PATH
if old_res.is_file(): if old_res.is_file():
j = convert_cols_file(old_res) j = read_json(old_res)
if j is not None: cols = j["cols"]
write_json(j, res) if "View" in cols:
return
cols["View"] = cols.pop("Perspective")
cols["Max"] = cols.pop("Maximum")
write_json(j, res)
def copy_state_files(state_path: Path) -> None: def copy_state_files(state_path: Path) -> None:
+16 -13
View File
@@ -6,11 +6,14 @@ import warnings
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from dzgui.api.mods import remove_stale_signatures
from dzgui.const.constants import APP_NAME from dzgui.const.constants import APP_NAME
from dzgui.const.enum import Preferences from dzgui.const.enum import Preferences
from dzgui.config.ipdb import get_ipdb
from dzgui.config.query import lookup from dzgui.config.query import lookup
from dzgui.config.userprefs import UserPrefs from dzgui.config.userprefs import UserPrefs
from dzgui.config.xdg import get_xdg_paths, parse_filepaths from dzgui.config.xdg import get_xdg_paths, parse_filepaths
from dzgui.init.coords import get_local_coords
from dzgui.init.dayz import is_dayz_installed from dzgui.init.dayz import is_dayz_installed
from dzgui.init.flock import lock_acquire from dzgui.init.flock import lock_acquire
from dzgui.init.migrate import ( from dzgui.init.migrate import (
@@ -21,16 +24,17 @@ from dzgui.init.migrate import (
from dzgui.init.prefix import get_version from dzgui.init.prefix import get_version
from dzgui.init.prereqs import has_steam_client from dzgui.init.prereqs import has_steam_client
from dzgui.strings import boot from dzgui.strings import boot
from dzgui.init.update import check_updates
# from dzgui.util.map_count import get_map_count # from dzgui.util.map_count import get_map_count
from dzgui.util.deck import is_steam_deck, is_game_mode from dzgui.util.deck import is_steam_deck, is_game_mode
from dzgui.util.localize import set_locale from dzgui.util.localize import set_locale
from dzgui.util.symlink import rebuild_symlinks
from dzgui.util.strings import init, flags from dzgui.util.strings import init, flags
from dzgui.views.base import App from dzgui.views.base import App
from dzgui.views.dialogs.boot import BootWindow
from dzgui.views.dialogs.early_alert import EarlyAlertDialog
from dzgui.views.dialogs.wizard import SetupWizard from dzgui.views.dialogs.wizard import SetupWizard
from dzgui.views.dialogs.early_alert import EarlyAlertDialog
if TYPE_CHECKING: if TYPE_CHECKING:
from pathlib import Path from pathlib import Path
@@ -117,6 +121,8 @@ def main() -> None:
with open(XDG.debug, "w") as f: with open(XDG.debug, "w") as f:
f.truncate(0) f.truncate(0)
latest_release = check_updates(version)
if _is_steam_deck is False: if _is_steam_deck is False:
# TODO: sudo escalation dialog # TODO: sudo escalation dialog
# count = get_map_count() # count = get_map_count()
@@ -124,19 +130,16 @@ def main() -> None:
if has_steam_client() is False: if has_steam_client() is False:
EarlyAlertDialog(init.requires_steam) EarlyAlertDialog(init.requires_steam)
# NOTE: clears versions file of unlinked mods
rebuild_symlinks(XDG.config)
remove_stale_signatures(XDG.config, XDG.version)
is_dayz_installed(XDG.config) is_dayz_installed(XDG.config)
# TODO: handle IP DB failure and use coords fallback
bootwin = BootWindow(XDG, version) get_ipdb(XDG.ips)
local_coords, latest_release = bootwin.get_results() local_coords = get_local_coords(XDG.ips)
# rebuild_symlinks(XDG.config)
# remove_stale_signatures(XDG.config, XDG.version)
## TODO: handle IP DB failure and use coords fallback
# local_coords = get_local_coords(XDG.ips)
# latest_release = check_updates(version)
use_miles = lookup(XDG.config, Preferences.DIST) use_miles = lookup(XDG.config, Preferences.DIST)
prefs = UserPrefs( prefs = UserPrefs(
is_steam_deck=_is_steam_deck, is_steam_deck=_is_steam_deck,
is_game_mode=_is_game_mode, is_game_mode=_is_game_mode,
-13
View File
@@ -159,9 +159,6 @@ class ConfigManager:
def save_res_and_quit(self, tv: "ServerTreeView", window: "OuterWindow") -> None: def save_res_and_quit(self, tv: "ServerTreeView", window: "OuterWindow") -> None:
columns = tv.get_columns() columns = tv.get_columns()
filters = self.controller.get_all_tree_filters()
filters_file = self.prefs.paths.filters
columns_file = self.prefs.paths.columns columns_file = self.prefs.paths.columns
try: try:
data = read_json(columns_file) data = read_json(columns_file)
@@ -176,7 +173,6 @@ class ConfigManager:
try: try:
write_json(data, columns_file) write_json(data, columns_file)
write_json(filters, filters_file)
except Exception as e: except Exception as e:
logger.critical(e) logger.critical(e)
@@ -220,12 +216,3 @@ class ConfigManager:
h = WINDOW_DEFAULT_Y h = WINDOW_DEFAULT_Y
logger.info(f"Using default window size {w},{h}") logger.info(f"Using default window size {w},{h}")
window.set_default_size(w, h) window.set_default_size(w, h)
def get_filters(self, tv: "ServerTreeView") -> Any | None:
filters = self.prefs.paths.filters
try:
enum = tv.get_enum()
filters = read_json(filters)
return filters[str(enum)]
except Exception:
return None
+3 -9
View File
@@ -161,7 +161,7 @@ class ConnectionManager:
if len(self.missing_mods) > 0: if len(self.missing_mods) > 0:
required_size = sum(row[2] for row in self.missing_mods) required_size = sum(row[2] for row in self.missing_mods)
required_mib = format_mib(required_size) required_mib = format_mib(required_size)
free_mib = format_mib(free) free_mib = format_mib(required_size)
dayz_running = is_dayz_running() dayz_running = is_dayz_running()
@@ -222,8 +222,7 @@ class ConnectionManager:
def query_modlist_and_present(self, record: Servers.Record) -> None: def query_modlist_and_present(self, record: Servers.Record) -> None:
try: try:
mods = self._query_modlist(record) mods = self._query_modlist(record)
except Exception as e: except Exception:
print(f"{type(e).__name__}: {e}")
self.thread_man.set_cleanup_func( self.thread_man.set_cleanup_func(
StoredFunc(self._server_timeout), destroy_first=True StoredFunc(self._server_timeout), destroy_first=True
) )
@@ -280,13 +279,8 @@ class ConnectionManager:
for title, mod, stamp, size in self.missing_mods: for title, mod, stamp, size in self.missing_mods:
# TODO: check cancel and exit events # TODO: check cancel and exit events
if self.controller.get_exit_event().is_set():
return
if self.controller.get_cancel_event().is_set():
self.controller.clear_cancel_event()
return
enqueue_mod(mod, self.appid) enqueue_mod(mod, self.appid)
time.sleep(3) time.sleep(2)
if raise_window is True: if raise_window is True:
logger.info("Bringing window to foreground") logger.info("Bringing window to foreground")
+3 -3
View File
@@ -47,8 +47,8 @@ class StoredFunc:
self.func = func self.func = func
self.bindings = sig.bind(*args, **kwargs) self.bindings = sig.bind(*args, **kwargs)
def call(self) -> Any: def call(self) -> None:
return self.func(*self.bindings.args, *self.bindings.kwargs) self.func(*self.bindings.args, *self.bindings.kwargs)
class ThreadingManager: class ThreadingManager:
@@ -113,8 +113,8 @@ class ThreadingManager:
func = self.get_cleanup_func() func = self.get_cleanup_func()
if func is not None: if func is not None:
self.set_cleanup_func(None)
func.call() func.call()
self.set_cleanup_func(None)
if not self.destroy_first: if not self.destroy_first:
self.destroy_dialog() self.destroy_dialog()
-67
View File
@@ -1,67 +0,0 @@
import logging
import requests
import shutil
import subprocess
import tarfile
from typing import TYPE_CHECKING
from dzgui.const.constants import (
APP_NAME,
TMP_EXE,
TMP_PATH,
TMP_TARBALL,
)
from dzgui.strings import dialogs
from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager
from dzgui.views.dialogs.generic import ExceptionDialog, QuitDialog
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, GLib # noqa E402
if TYPE_CHECKING:
from dzgui.controllers.mc import Controller
logger = logging.getLogger(APP_NAME)
class UpdateManager:
def __init__(self, controller: "Controller") -> None:
self.thread_man = ThreadingManager(controller)
self.controller = controller
@call_on_thread(dialogs.fetching_update)
def update_version(self, exe_path: str, url: str) -> None:
try:
res = requests.get(url)
if res.status_code == 200:
with open(TMP_TARBALL, "wb") as file:
file.write(res.content)
with tarfile.open(TMP_TARBALL) as tar:
tar.extractall(TMP_PATH)
shutil.move(TMP_EXE, exe_path)
proc = subprocess.run([exe_path, "self", "restore"])
if proc.returncode == 0:
func = StoredFunc(self._on_update_success)
self.thread_man.set_cleanup_func(func, destroy_first=True)
else:
msg = dialogs.failed_to_update
func = StoredFunc(self._on_update_failure, msg)
self.thread_man.set_cleanup_func(func, destroy_first=True)
except Exception as e:
func = StoredFunc(self._on_update_failure, e)
self.thread_man.set_cleanup_func(func, destroy_first=True)
logger.warning(e)
def _on_update_success(self) -> None:
msg = dialogs.update_success
dialog = QuitDialog(self.controller, msg)
dialog.run()
def _on_update_failure(self, msg: str) -> None:
dialog = ExceptionDialog(self.controller, msg)
dialog.run()
+13 -21
View File
@@ -73,7 +73,6 @@ class ServerModelManager:
self._dump_history() self._dump_history()
case ServerTab.LAN: case ServerTab.LAN:
# NOTE: LAN tab is only loaded on demand # NOTE: LAN tab is only loaded on demand
self.emitter.emit("lan_page_initialized")
pass pass
case _: case _:
pass pass
@@ -124,10 +123,8 @@ class ServerModelManager:
parsed = Servers.parse_json(servers) parsed = Servers.parse_json(servers)
self._push_data(parsed) self._push_data(parsed)
@call_on_thread(dialog.scanning, show_cancel=True) @call_on_thread(dialog.scanning)
def dump_lan(self, port: int, early_abort: bool) -> None: def dump_lan(self, port: int, early_abort: bool) -> None:
self.first_iteration = True
servers = [] servers = []
ports = range(1, 256) ports = range(1, 256)
failure_func = StoredFunc(self._cleanup_on_failure) failure_func = StoredFunc(self._cleanup_on_failure)
@@ -138,20 +135,19 @@ class ServerModelManager:
for future in as_completed(futures): for future in as_completed(futures):
try: try:
if self.controller.get_exit_event().is_set(): if self.controller.get_exit_event().is_set():
event.set()
return
if self.controller.get_cancel_event().is_set():
event.set()
self.controller.clear_cancel_event()
return return
res = future.result(timeout=LAN_TIMEOUT) res = future.result(timeout=LAN_TIMEOUT)
if res is None: if res is None:
continue continue
servers.append(res)
if early_abort is True: if early_abort is True:
# NOTE: on first non-empty hit, flag pending threads to close # NOTE: on first non-empty hit, flag pending threads to close
event.set() event.set()
break servers.append(res)
self.thread_man.set_cleanup_func(
StoredFunc(self._cleanup_on_success)
)
return
servers.append(res)
except Exception as e: except Exception as e:
logger.critical(e) logger.critical(e)
self.thread_man.set_cleanup_func(failure_func, destroy_first=True) self.thread_man.set_cleanup_func(failure_func, destroy_first=True)
@@ -243,11 +239,9 @@ class ServerModelManager:
filter_man = self.tv.get_filter_man() filter_man = self.tv.get_filter_man()
maps = self._get_new_maps() maps = self._get_new_maps()
filter_man.set_unique_maps(maps) filter_man.set_unique_maps(maps)
store = filter_man.get_map_store()
self.first_iteration = False self.first_iteration = False
self.emitter.emit("load_maps", store) self.emitter.emit("servers_loaded_init")
# self.emitter.emit("servers_loaded_init")
def add_to_history(self, record: dict[str, Any]) -> None: def add_to_history(self, record: dict[str, Any]) -> None:
proxy_man = self._get_proxy_man() proxy_man = self._get_proxy_man()
@@ -324,7 +318,6 @@ class ServerModelManager:
self.thread_man.set_cleanup_func(func) self.thread_man.set_cleanup_func(func)
return return
config_man.add_saved_server(fqip) config_man.add_saved_server(fqip)
# FIXME: this is valid if saved servers tab is already open, # FIXME: this is valid if saved servers tab is already open,
# but not if app was just booted; causes single row to appear prematurely # but not if app was just booted; causes single row to appear prematurely
if proxy_man.has_control_model() is False: if proxy_man.has_control_model() is False:
@@ -375,31 +368,30 @@ class ServerModelManager:
proxy = self._get_proxy_man().get_proxy_model() proxy = self._get_proxy_man().get_proxy_model()
self.tv.set_model(proxy) self.tv.set_model(proxy)
if self.controller.get_active_treeview().get_enum == ServerTab.SAVED: self.emitter.emit("servers_loaded", self.enum)
self.emitter.emit("servers_loaded", self.enum)
filter_man = self.tv.get_filter_man() filter_man = self.tv.get_filter_man()
# NOTE: maps are set outside of thread because it triggers map changed signals # NOTE: maps are set outside of thread because it triggers map changed signals
maps = self._get_new_maps() maps = self._get_new_maps()
filter_man.set_unique_maps(maps) filter_man.set_unique_maps(maps)
store = filter_man.get_map_store()
self.emitter.emit("load_maps", store)
self.first_iteration = False self.first_iteration = False
self.emitter.emit("servers_loaded_init")
self.emitter.emit("saved_servers_changed") self.emitter.emit("saved_servers_changed")
def _update_maps(self) -> None: def _update_maps(self) -> None:
filter_man = self.tv.get_filter_man() filter_man = self.tv.get_filter_man()
filter_man.set_unique_maps(self._get_new_maps()) filter_man.set_unique_maps(self._get_new_maps())
store = filter_man.get_map_store() self.emitter.emit("servers_loaded_init")
self.emitter.emit("load_maps", store)
self.first_iteration = False self.first_iteration = False
def _cleanup_on_success(self) -> None: def _cleanup_on_success(self) -> None:
proxy = self._get_proxy_man().get_proxy_model() proxy = self._get_proxy_man().get_proxy_model()
self.tv.set_model(proxy) self.tv.set_model(proxy)
# TODO: servers_loaded vs servers_reloaded
self.emitter.emit("servers_loaded", self.enum) self.emitter.emit("servers_loaded", self.enum)
if self.first_iteration: if self.first_iteration:
self._update_maps() self._update_maps()
-5
View File
@@ -1,5 +0,0 @@
symlinks = "Rebuilding symlinks"
signatures = "Updating mod signatures"
geo = "Checking geolocation records"
coords = "Checking local coordinates"
updates = "Checking for updates"
+7 -6
View File
@@ -2,19 +2,20 @@ import json
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
def read_json(path: Path) -> Any: def read_json(path: Path) -> Any:
try: try:
with open(path, "r") as infile: with open(path, "r") as infile:
data = json.load(infile) try:
return data data = json.load(infile)
except Exception as e: return data
except json.decoder.JSONDecodeError as e:
raise e
except OSError as e:
raise e raise e
def write_json(data: dict[str, Any], path: Path) -> None: def write_json(data: dict, path: Path) -> None:
try: try:
path.parent.mkdir(parents=True, exist_ok=True)
j = json.dumps(data, indent=2) j = json.dumps(data, indent=2)
path.write_text(j) path.write_text(j)
except Exception as e: except Exception as e:
-2
View File
@@ -360,14 +360,12 @@ thanks = Thanks(
"bongjutsu", "bongjutsu",
"Deku", "Deku",
"dj3hac", "dj3hac",
"finntux",
"GaryBlackbourne", "GaryBlackbourne",
"jiriks74", "jiriks74",
"Johnofwrong", "Johnofwrong",
"MatheusLasserr", "MatheusLasserr",
"nolan-perez", "nolan-perez",
"scandalouss", "scandalouss",
"SnackSBR",
"StevelDusa", "StevelDusa",
"Thoughtduck216", "Thoughtduck216",
], ],
+7 -10
View File
@@ -43,14 +43,11 @@ def clone_symlinks(steam_path: Path) -> None:
# TODO: test these two operations # TODO: test these two operations
# use less time intensive logic like the above # use less time intensive logic like the above
try: for file in exp_path.iterdir():
for file in exp_path.iterdir(): if file.is_symlink():
if file.is_symlink(): file.unlink()
file.unlink()
for file in dayz_path.iterdir(): for file in dayz_path.iterdir():
if file.is_symlink(): if file.is_symlink():
uid = file.name uid = file.name
Path(exp_path / uid).symlink_to(dayz_path / uid) Path(exp_path / uid).symlink_to(dayz_path / uid)
except Exception as e:
logger.critical(e)
+8 -23
View File
@@ -1,4 +1,4 @@
from typing import Callable, Self, TYPE_CHECKING, Union from typing import Callable, Self, TYPE_CHECKING
from dzgui.util.clip import copy_clipboard from dzgui.util.clip import copy_clipboard
from dzgui.util.format import pluralize from dzgui.util.format import pluralize
@@ -70,26 +70,18 @@ class LargeIconTextButton(IconButton):
class ClipboardButton(IconTextButton): class ClipboardButton(IconTextButton):
def __init__(self, controller: Union["Controller", None], func: Callable) -> None: def __init__(self, controller: "Controller", func: Callable) -> None:
super().__init__(CLIPBOARD, atomic_buttons.copy) super().__init__(CLIPBOARD, atomic_buttons.copy)
self.controller = controller self.controller = controller
self.connect("clicked", self._on_button_clicked, func) self.connect("clicked", self._on_button_clicked, func)
self.set_tooltip_text("Copy to clipboard") self.set_tooltip_text("Copy IP to clipboard")
def _on_button_clicked(self, button: Self, func: Callable) -> None: def _on_button_clicked(self, button: Self, func: Callable) -> None:
data = func() data = func()
copy_clipboard(data) copy_clipboard(data)
# TODO: determine when controller would be passed to this button or drop
class CopyIpButton(ClipboardButton):
def __init__(self, controller: Union["Controller", None], func: Callable) -> None:
super().__init__(controller, func)
self.set_tooltip_text("Copy IP to clipboard")
class WebButton(IconTextButton): class WebButton(IconTextButton):
def __init__(self, label: str) -> None: def __init__(self, label: str) -> None:
super().__init__(icon=WEB_BROWSER, label=label) super().__init__(icon=WEB_BROWSER, label=label)
@@ -102,11 +94,9 @@ class RefreshButton(IconTextButton):
icon=REFRESH_ICON, icon=REFRESH_ICON,
label=atomic_buttons.refresh, label=atomic_buttons.refresh,
) )
self.controller = controller self.controller = controller
emitter = self.controller.get_emitter() emitter = self.controller.get_emitter()
self.loading = False self.loading = False
self.is_clicked = False
self.time = 30 self.time = 30
@@ -120,21 +110,17 @@ class RefreshButton(IconTextButton):
def _on_refresh_clicked(self, button: Self) -> None: def _on_refresh_clicked(self, button: Self) -> None:
"""Spawned in a thread""" """Spawned in a thread"""
self.is_clicked = True self.loading = True
self.controller.refresh_tree() self.controller.refresh_tree()
# TODO: get server tab enum # TODO: get server tab enum
# if LAN tab, reload existing entries in place # if LAN tab, reload existing entries in place
def start_decrement(self, emitter: "Emitter", tab: "ServerTab") -> None: def start_decrement(self, emitter: "Emitter", tab: "ServerTab") -> None:
if self.is_clicked is False:
return
if self.loading: if self.loading:
return self.set_sensitive(False)
self.set_sensitive(False) self.loading = False
self.loading = True self.show_time(True)
self.is_clicked = False GLib.timeout_add_seconds(1, self.decrement)
self.show_time(True)
GLib.timeout_add_seconds(1, self.decrement)
def decrement(self) -> bool: def decrement(self) -> bool:
self.time -= 1 self.time -= 1
@@ -142,7 +128,6 @@ class RefreshButton(IconTextButton):
self.time = 30 self.time = 30
self.show_time(False) self.show_time(False)
self.set_sensitive(True) self.set_sensitive(True)
self.loading = False
return False return False
self.show_time(True) self.show_time(True)
return True return True
+2 -2
View File
@@ -6,7 +6,7 @@ from dzgui.strings import connect_panel
from dzgui.util.keys import is_ctrl_mask from dzgui.util.keys import is_ctrl_mask
from dzgui.views.components.buttons import ( from dzgui.views.components.buttons import (
AddButton, AddButton,
CopyIpButton, ClipboardButton,
SteamConnectButton, SteamConnectButton,
) )
from dzgui.views.components.entry import IpEntry, PortEntry from dzgui.views.components.entry import IpEntry, PortEntry
@@ -151,7 +151,7 @@ class FavPanel(Gtk.Frame):
self.fav_button = SteamConnectButton() self.fav_button = SteamConnectButton()
self.fav_button.connect("clicked", self._on_connect_clicked) self.fav_button.connect("clicked", self._on_connect_clicked)
self.copy_button = CopyIpButton(self.controller, self.get_fav_ip) self.copy_button = ClipboardButton(self.controller, self.get_fav_ip)
if favorite is None: if favorite is None:
self.toggle_buttons(False) self.toggle_buttons(False)
-6
View File
@@ -68,7 +68,6 @@ class ButtonGrid(Gtk.Grid):
) )
def reload_filters(self) -> None: def reload_filters(self) -> None:
# TODO: may be superfluous
checkboxes = self.checks checkboxes = self.checks
self.block_toggles(True) self.block_toggles(True)
filters = self.controller.get_enabled_filters() filters = self.controller.get_enabled_filters()
@@ -331,8 +330,3 @@ class FilterPanel(Gtk.Box):
self.block_map_change_propagation = False self.block_map_change_propagation = False
return return
self.emitter.emit("map_selection_changed", name) self.emitter.emit("map_selection_changed", name)
def set_filters(self, filters: dict[str, bool]) -> None:
for check in self.button_grid.checks:
label = check.get_label()
check.set_active(filters[label])
+53 -14
View File
@@ -1,5 +1,9 @@
import logging import logging
import os import os
import requests
import shutil
import subprocess
import tarfile
from typing import Literal, TYPE_CHECKING from typing import Literal, TYPE_CHECKING
@@ -9,14 +13,20 @@ from dzgui.const.constants import (
NO_FILL, NO_FILL,
FILL, FILL,
NO_PADDING, NO_PADDING,
TMP_EXE,
TMP_PATH,
TMP_TARBALL,
) )
from dzgui.const.update import ALLOW_UPDATES
from dzgui.const.enum import ServerTab from dzgui.const.enum import ServerTab
from dzgui.managers.update import UpdateManager from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager
from dzgui.strings import dialogs
from dzgui.util.clip import copy_clipboard from dzgui.util.clip import copy_clipboard
from dzgui.views.components.buttonbox import ButtonBox from dzgui.views.components.buttonbox import ButtonBox
from dzgui.views.components.filter_panel import FilterPanel from dzgui.views.components.filter_panel import FilterPanel
from dzgui.views.components.mod_panel import ModSelectionPanel from dzgui.views.components.mod_panel import ModSelectionPanel
from dzgui.views.components.buttons import IconTextButton, RefreshButton, KeysButton from dzgui.views.components.buttons import IconTextButton, RefreshButton, KeysButton
from dzgui.views.dialogs.generic import ExceptionDialog, QuitDialog
import gi import gi
@@ -35,13 +45,13 @@ class RightPanel(Gtk.Box):
def __init__(self, controller: "Controller"): def __init__(self, controller: "Controller"):
super().__init__(spacing=6, orientation=Gtk.Orientation.VERTICAL) super().__init__(spacing=6, orientation=Gtk.Orientation.VERTICAL)
self.thread_man = ThreadingManager(controller)
self.controller = controller self.controller = controller
self.controller.register_widget("right_panel", self) self.controller.register_widget("right_panel", self)
emitter = controller.get_emitter() emitter = controller.get_emitter()
emitter.connect("servers_loaded", self._on_servers_loaded) emitter.connect("servers_loaded", self._on_servers_loaded)
emitter.connect("server_page_changed", self._on_server_page_changed) emitter.connect("server_page_changed", self._on_server_page_changed)
emitter.connect("lan_page_initialized", self._on_lan_page_init)
self.button_vbox = ButtonBox(controller) self.button_vbox = ButtonBox(controller)
self.filters_vbox = FilterPanel(controller) self.filters_vbox = FilterPanel(controller)
@@ -85,20 +95,54 @@ class RightPanel(Gtk.Box):
valign=Gtk.Align.END, valign=Gtk.Align.END,
spacing=10, spacing=10,
) )
exe_path = os.getenv("PYAPP") if ALLOW_UPDATES and update is not None:
if exe_path and update is not None:
self.update_button.set_halign(Gtk.Align.END) self.update_button.set_halign(Gtk.Align.END)
self.gutter_box.add(self.update_button) self.gutter_box.add(self.update_button)
self.update_button.connect( self.update_button.connect(
"clicked", self._on_update_button_clicked, exe_path, update "clicked", self._on_update_button_clicked, update
) )
self.gutter_box.add(eb) self.gutter_box.add(eb)
self.pack_start(self.gutter_box, NO_EXPAND, FILL, NO_PADDING) self.pack_start(self.gutter_box, NO_EXPAND, FILL, NO_PADDING)
def _on_update_button_clicked( def _on_update_success(self) -> None:
self, button: Gtk.Button, exe_path: str, url: str msg = dialogs.update_success
) -> None: dialog = QuitDialog(self.controller, msg)
UpdateManager(self.controller).update_version(exe_path, url) dialog.run()
def _on_update_failure(self, msg: str) -> None:
dialog = ExceptionDialog(self.controller, msg)
dialog.run()
@call_on_thread(dialogs.fetching_update)
def _on_update_button_clicked(self, button: Gtk.Button, url: str) -> None:
try:
res = requests.get(url)
if res.status_code == 200:
with open(TMP_TARBALL, "wb") as file:
file.write(res.content)
with tarfile.open(TMP_TARBALL) as tar:
tar.extractall(TMP_PATH)
exe_path = os.getenv("PYAPP")
if exe_path is None:
msg = dialogs.failed_to_update
func = StoredFunc(self._on_update_failure, msg)
self.thread_man.set_cleanup_func(func, destroy_first=True)
return
shutil.move(TMP_EXE, exe_path)
proc = subprocess.run([exe_path, "self", "restore"])
if proc.returncode == 0:
func = StoredFunc(self._on_update_success)
self.thread_man.set_cleanup_func(func, destroy_first=True)
else:
msg = dialogs.failed_to_update
func = StoredFunc(self._on_update_failure, msg)
self.thread_man.set_cleanup_func(func, destroy_first=True)
except Exception as e:
func = StoredFunc(self._on_update_failure, e)
self.thread_man.set_cleanup_func(func, destroy_first=True)
logger.warning(e)
def _on_server_page_changed( def _on_server_page_changed(
self, emitter: "Emitter", page: "ServerTreeView" self, emitter: "Emitter", page: "ServerTreeView"
@@ -107,11 +151,6 @@ class RightPanel(Gtk.Box):
if page.loaded is True: if page.loaded is True:
return return
self.filters_vbox.set_sensitive(False) self.filters_vbox.set_sensitive(False)
# TODO: unless it is lan page
#self.refresh_button.set_sensitive(False)
def _on_lan_page_init(self, emitter: "Emitter") -> None:
self.filters_vbox.set_sensitive(False)
def _on_servers_loaded(self, emitter: "Emitter", context: "ServerTab") -> None: def _on_servers_loaded(self, emitter: "Emitter", context: "ServerTab") -> None:
# TODO: similar logic on notebook page change # TODO: similar logic on notebook page change
-9
View File
@@ -1,9 +0,0 @@
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk # noqa
class NoOverlayScrolledWindow(Gtk.ScrolledWindow):
def __init__(self) -> None:
super().__init__(overlay_scrolling=False)
+2 -2
View File
@@ -111,9 +111,9 @@ class Statusbar(Gtk.Grid):
context: Union["ServerTab", NotebookPage], context: Union["ServerTab", NotebookPage],
) -> None: ) -> None:
self.spinner.stop() self.spinner.stop()
# FIXME: CalcDist is being called when table is not loaded # TODO: CalcDist is being called when table is not loaded
if dist is None: if dist is None:
return self.set_by_context(context, "")
else: else:
pretty = self.append_distance(dist) pretty = self.append_distance(dist)
self.set_by_context(context, pretty) self.set_by_context(context, pretty)
-244
View File
@@ -1,244 +0,0 @@
import sys
from typing import Literal, TYPE_CHECKING
# import time
from enum import Enum
from typing import Any, Self
# TODO: import dialog titles
from dzgui.api.mods import remove_stale_signatures as remove_stale
from dzgui.config.ipdb import get_ipdb
from dzgui.const.constants import HEX_GREEN, HEX_RED
from dzgui.init.coords import get_local_coords
from dzgui.init.update import check_updates
from dzgui.const.constants import EXPAND, FILL
from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager
from dzgui.strings import preboot
from dzgui.util.strings import dialog_header
from dzgui.util.symlink import rebuild_symlinks
from dzgui.views.components.buttons import ClipboardButton
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, GLib, Gdk # noqa
if TYPE_CHECKING:
from dzgui.config.xdg import Xdg
from dzgui.util.ip import Coords
class Success(Enum):
OK = 1
FAIL = 2
# TODO: strings for "Running", "Failed", etc.
# TODO: add margins to tree
class BootDialog(Gtk.Dialog):
def __init__(self, parent: "BootWindow", xdg: "Xdg", version: str) -> None:
super().__init__(
title=dialog_header,
parent=parent,
modal=True,
)
self.parent = parent
self.set_modal(True)
self.xdg = xdg
self.version = version
self.thread_man = ThreadingManager(None)
self.set_size_request(700, 500)
self.store = Gtk.ListStore(str, str, bool, int)
self.view = Gtk.TreeView(enable_search=False, headers_visible=False)
self.view.set_model(self.store)
for i, column_title in enumerate(["Task", "State"]):
renderer = Gtk.CellRendererText()
column = Gtk.TreeViewColumn(column_title, renderer, text=i)
column.set_sizing(Gtk.TreeViewColumnSizing.FIXED)
column.set_resizable(False)
column.set_sort_column_id(i)
column.set_cell_data_func(renderer, self._format_color, func_data=None)
self.view.append_column(column)
if i == 0:
column.set_fixed_width(300)
self.spinner_renderer = Gtk.CellRendererSpinner()
self.spinner_renderer.set_property("size", Gtk.IconSize.LARGE_TOOLBAR)
col_bool = Gtk.TreeViewColumn("Spinner", self.spinner_renderer, active=3)
col_bool.set_alignment(0.0)
col_bool.set_cell_data_func(
self.spinner_renderer, self._set_spinner_vis, func_data=None
)
self.view.append_column(col_bool)
col_int = Gtk.TreeViewColumn("Int", Gtk.CellRendererText(), text=3)
col_int.set_visible(False)
self.view.append_column(col_int)
self.view.get_selection().set_mode(Gtk.SelectionMode.NONE)
self.connect("delete-event", self._on_delete)
self.scrollable_tree = Gtk.ScrolledWindow(overlay_scrolling=False)
self.scrollable_tree.add(self.view)
self.scrollable_tree.set_size_request(700, 400)
self.error_box = Gtk.Box(
halign=Gtk.Align.CENTER,
orientation=Gtk.Orientation.VERTICAL,
spacing=20,
margin_top=30,
margin_bottom=30,
)
self.error_label = Gtk.Label()
# TODO: abstract class
self.copy_button = ClipboardButton(None, lambda: self.error_label.get_text())
self.button_hbox = Gtk.Box(
orientation=Gtk.Orientation.HORIZONTAL, halign=Gtk.Align.CENTER, spacing=10
)
self.exit_button = Gtk.Button(label="Exit", halign=Gtk.Align.CENTER)
self.exit_button.connect("clicked", lambda _: sys.exit(1))
self.button_hbox.add(self.copy_button)
self.button_hbox.add(self.exit_button)
self.error_box.add(self.error_label)
self.error_box.add(self.button_hbox)
self.content = self.get_content_area()
self.content.pack_start(self.scrollable_tree, EXPAND, FILL, 0)
self.content.pack_start(self.error_box, EXPAND, FILL, 0)
self.show_all()
self.error_box.hide()
steps = [
(StoredFunc(rebuild_symlinks, self.xdg.config), preboot.symlinks, False),
(
StoredFunc(remove_stale, self.xdg.config, self.xdg.version),
preboot.signatures,
False,
),
(StoredFunc(get_ipdb, self.xdg.ips), preboot.geo, False),
(StoredFunc(get_local_coords, self.xdg.ips), preboot.coords, True),
(StoredFunc(check_updates, self.version), preboot.updates, True),
]
self.results: list[Any] = []
self.failed = False
self.steps = iter(steps)
GLib.timeout_add(100, self.pulse_spinner)
def pulse_spinner(self) -> Literal[True]:
for row in self.store:
if row[2]:
if row[3] == 150:
row[3] = 0
else:
row[3] += 1
self.spinner_renderer.set_property("pulse", row[3])
return True
def run(self) -> None:
self.iter_step()
def _on_delete(self, widget: Self, event: Gdk.Event) -> Literal[True]:
return True
def iter_step(self) -> None:
if self.failed:
self.error_box.show()
self.error_label.set_text(self.exception)
return
try:
step, label, store_output = next(self.steps)
self.update_task(label)
self.background(step, store_output)
except StopIteration:
self.parent.set_results(self.results)
self.destroy()
@call_on_thread("", show_dialog=False)
def background(self, func: StoredFunc, store_output: bool) -> None:
try:
if store_output:
res = func.call()
self.results.append(res)
else:
func.call()
callback = StoredFunc(self.update_status, Success.OK)
self.thread_man.set_cleanup_func(callback)
except Exception as e:
self.failed = True
# TODO: generic exception wrapper
self.exception = f"{type(e).__name__}: {e}"
callback = StoredFunc(self.update_status, Success.FAIL)
self.thread_man.set_cleanup_func(callback)
def update_task(self, task: str) -> None:
self.store.append((task, "Running", True, 0))
def update_status(self, state: Success) -> None:
d = {Success.OK: "OK", Success.FAIL: "FAILED"}
msg = d[state]
self.store[len(self.store) - 1][1] = msg
self.iter_step()
def _set_spinner_vis(
self,
column: Gtk.TreeViewColumn,
cell: Gtk.CellRendererSpinner,
model: Gtk.TreeModel,
it: Gtk.TreeIter,
data: Any,
) -> None:
if model[it][1] == "Running":
cell.set_property("visible", True)
else:
cell.set_property("visible", False)
def _format_color(
self,
column: Gtk.TreeViewColumn,
cell: Gtk.CellRendererText,
model: Gtk.TreeModel,
it: Gtk.TreeIter,
data: Any,
) -> None:
prop = "foreground"
state = model[it][1]
if column.get_sort_column_id() != 1:
return
if state == "OK":
cell.set_property(prop, HEX_GREEN)
elif state == "FAILED":
cell.set_property(prop, HEX_RED)
else:
cell.set_property(prop, None)
class BootWindow(Gtk.Window):
def __init__(self, xdg: "Xdg", version: str) -> None:
super().__init__()
self.results: list[Any] = []
dialog = BootDialog(self, xdg, version)
dialog.run()
dialog.connect("destroy", self._on_destroy)
Gtk.main()
def set_results(self, res: list[Any]) -> None:
self.results = res
def get_results(self) -> tuple["Coords", str]:
coords, version_url = self.results
return (coords, version_url)
def _on_destroy(self, dialog: BootDialog) -> None:
Gtk.main_quit()
-2
View File
@@ -53,5 +53,3 @@ class EarlyIgnoreDialog(EarlyAlertDialog):
# TODO: reverse order # TODO: reverse order
self.add_button("Ignore", Gtk.ResponseType.CANCEL) self.add_button("Ignore", Gtk.ResponseType.CANCEL)
# TODO: if exit, sys.exit(1), else pass
+2 -7
View File
@@ -31,6 +31,8 @@ gi.require_version("Gtk", "3.0")
from gi.repository import Gdk, Gtk, GLib, GObject, GdkPixbuf # noqa E402 from gi.repository import Gdk, Gtk, GLib, GObject, GdkPixbuf # noqa E402
# TODO: currently unused, corresponds to linear index
# TODO: use of pagenum enum and page_type attribute is redundant
class PageNum(Enum): class PageNum(Enum):
INTRO = 1 INTRO = 1
HAS_CONFIG = 2 HAS_CONFIG = 2
@@ -91,9 +93,6 @@ class ScrolledWizardPage(Gtk.ScrolledWindow):
self.connect("map", self._on_map) self.connect("map", self._on_map)
def get_enum(self) -> PageNum:
return self.enum
def get_progress_bar(self) -> Progress: def get_progress_bar(self) -> Progress:
return self.prog return self.prog
@@ -475,7 +474,6 @@ class Assistant(Gtk.Assistant):
def _advance_page(self, index: int) -> int: def _advance_page(self, index: int) -> int:
page = self.get_nth_page(index) page = self.get_nth_page(index)
# TODO: use enums
match page: match page:
case self.page1: case self.page1:
pass pass
@@ -536,9 +534,6 @@ class Assistant(Gtk.Assistant):
bar.set_text(f"{page_num}/{total}") bar.set_text(f"{page_num}/{total}")
# NOTE: disable forward action # NOTE: disable forward action
# TODO: use page enums
if page == self.page5:
return
if page != self.page1: if page != self.page1:
EMITTER.emit("step_pending") EMITTER.emit("step_pending")
+1 -1
View File
@@ -11,5 +11,5 @@ class ModsMixin:
self, tree: TreeView, path: Gtk.TreePath, column: Gtk.TreeViewColumn self, tree: TreeView, path: Gtk.TreePath, column: Gtk.TreeViewColumn
) -> None: ) -> None:
mod = self.get_value_at_index(2) # type: ignore mod = self.get_value_at_index(2)
self.controller.open_workshop_page(mod) # type: ignore self.controller.open_workshop_page(mod) # type: ignore
+1 -2
View File
@@ -1,6 +1,5 @@
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from dzgui.const.constants import LOG_FILTERS from dzgui.const.constants import LOG_FILTERS
from dzgui.views.components.scrollable import NoOverlayScrolledWindow
from dzgui.views.mixins.cursor_mixin import CursorMixin from dzgui.views.mixins.cursor_mixin import CursorMixin
from dzgui.views.mixins.help_menu_mixin import HelpMenuMixin from dzgui.views.mixins.help_menu_mixin import HelpMenuMixin
from dzgui.views.trees.tree_log import LogTreeView from dzgui.views.trees.tree_log import LogTreeView
@@ -22,7 +21,7 @@ class Log(CursorMixin, HelpMenuMixin, Gtk.Box): # type: ignore
NOTE: LogTreeView is kept in a separate ScrolledWindow so that NOTE: LogTreeView is kept in a separate ScrolledWindow so that
checkboxes will be flush on bottom checkboxes will be flush on bottom
""" """
self.scrolled = NoOverlayScrolledWindow() self.scrolled = Gtk.ScrolledWindow()
self.treeview = LogTreeView(controller) self.treeview = LogTreeView(controller)
self.scrolled.add(self.treeview) self.scrolled.add(self.treeview)
+1 -3
View File
@@ -1,5 +1,4 @@
from typing import Self, TYPE_CHECKING from typing import Self, TYPE_CHECKING
from dzgui.views.components.scrollable import NoOverlayScrolledWindow
from dzgui.views.trees.tree_mods import ModTreeView from dzgui.views.trees.tree_mods import ModTreeView
import gi import gi
@@ -11,8 +10,7 @@ if TYPE_CHECKING:
from dzgui.controllers.mc import Controller from dzgui.controllers.mc import Controller
class Mods(Gtk.ScrolledWindow):
class Mods(NoOverlayScrolledWindow):
def __init__(self, controller: "Controller") -> None: def __init__(self, controller: "Controller") -> None:
super().__init__() super().__init__()
+13
View File
@@ -162,6 +162,19 @@ class ServerNotebook(Gtk.ScrolledWindow):
return tv return tv
raise ValueError("No treeview set") raise ValueError("No treeview set")
def add_notification(self) -> None:
saved = self.notebook.get_nth_page(1)
if saved is None:
return
text = self.notebook.get_tab_label_text(saved)
if text is None:
return
# TODO: strings
if "*" in text:
return
text += "*"
self.notebook.set_tab_label_text(saved, text)
def update_tab_widths(self, col: Gtk.TreeViewColumn) -> None: def update_tab_widths(self, col: Gtk.TreeViewColumn) -> None:
# TODO: may cause pixel offsets when application is maximized # TODO: may cause pixel offsets when application is maximized
width = col.get_width() width = col.get_width()
+4 -2
View File
@@ -88,12 +88,13 @@ class ModTreeView(ModsMixin, ContextMixin, TreeView): # type: ignore
model: Gtk.TreeModel, model: Gtk.TreeModel,
it: Gtk.TreeIter, it: Gtk.TreeIter,
data: Any, data: Any,
) -> None: ) -> Any:
state = model[it][4] state = model[it][4]
if state is True: if state is True:
cell.set_property("foreground", HEX_RED) cell.set_property("foreground", HEX_RED)
else: else:
cell.set_property("foreground", None) cell.set_property("foreground", None)
return
def _format_float( def _format_float(
self, self,
@@ -102,7 +103,8 @@ class ModTreeView(ModsMixin, ContextMixin, TreeView): # type: ignore
model: Gtk.TreeModel, model: Gtk.TreeModel,
it: Gtk.TreeIter, it: Gtk.TreeIter,
data: Any, data: Any,
) -> None: ) -> Any:
val = model[it][3] val = model[it][3]
formatted = localize.number(val) formatted = localize.number(val)
cell.set_property("text", formatted) cell.set_property("text", formatted)
return
+2 -4
View File
@@ -72,7 +72,6 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore
logger.critical(e) logger.critical(e)
valid_json = False valid_json = False
# NOTE: reasonable defaults for long columns
width_map = { width_map = {
"Name": 800, "Name": 800,
"Map": 300, "Map": 300,
@@ -97,9 +96,8 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore
column.set_fixed_width(saved_size) column.set_fixed_width(saved_size)
column.set_expand(True) column.set_expand(True)
else: else:
if column_title in width_map: w = width_map[column_title]
w = width_map[column_title] column.set_fixed_width(w)
column.set_fixed_width(w)
if column_title == "Ping": if column_title == "Ping":
column.set_cell_data_func(renderer, self._get_ping) column.set_cell_data_func(renderer, self._get_ping)
+1 -1
View File
@@ -4,7 +4,7 @@ description = "DayZ server browser and mod manager for Linux"
authors = [ authors = [
{name = "aclist"} {name = "aclist"}
] ]
version = "7.0.0b7" version = "7.0.0b3"
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
license-files = ["LICENSE"] license-files = ["LICENSE"]
readme = "README.md" readme = "README.md"
-81
View File
@@ -1,81 +0,0 @@
import os
import requests
import subprocess
import tarfile
from pathlib import Path
from urllib.parse import unquote
BUILD = "3.13.9"
DATE = "20251014"
CPYTHON_BUILD = f"cpython-{BUILD}"
root = Path(__file__).resolve().parents[1]
build_dir = root.joinpath("build")
cpython_dir = build_dir.joinpath(CPYTHON_BUILD)
def fetch(url: str, destination: str) -> None:
print(f"Fetching URL '{url}'")
res = requests.get(url)
code = res.status_code
if code != 200:
raise Exception(f"URL '{url}' returned status code {code}")
with open(build_dir.joinpath(destination), "wb") as f:
f.write(res.content)
def get_cpython() -> None:
source = f"https://github.com/astral-sh/python-build-standalone/releases/download/{DATE}/"
build = f"{CPYTHON_BUILD}%2B{DATE}-x86_64_v3-unknown-linux-gnu-install_only_stripped.tar.gz"
url = f"{source}{build}"
outfile = unquote(build)
fetch(url, outfile)
version = build.split("%")[0]
untar(outfile, "python", version)
def get_pyapp() -> None:
url = "https://github.com/ofek/pyapp/releases/latest/download/source.tar.gz"
outfile = "pyapp-latest.tar.gz"
fetch(url, outfile)
untar(outfile, "pyapp-*", "pyapp-latest")
def untar(file: str, glob: str, renamed: str) -> None:
full_path = build_dir.joinpath(file)
with tarfile.open(full_path, "r:gz") as tar:
tar.extractall(path=build_dir)
full_path.unlink()
for _dir in build_dir.glob(glob):
os.rename(_dir, build_dir.joinpath(renamed))
def install_deps() -> None:
pip = build_dir.joinpath(f"{CPYTHON_BUILD}/bin/pip")
subprocess.run([pip, "install", root])
def repackage() -> None:
print("Preparing tar archive...")
outfile = build_dir.joinpath("airgapped.tar.gz")
with tarfile.open(outfile, "w:gz") as tar:
tar.add(build_dir.joinpath(CPYTHON_BUILD), arcname="python")
def rebuild_cpython() -> None:
if cpython_dir.is_dir() is False:
get_cpython()
install_deps()
repackage()
return get_packaged_version()
def get_packaged_version() -> str:
proc = subprocess.run(
[cpython_dir.joinpath("bin/dzgui"), "-v"],
capture_output=True,
text=True,
check=True,
)
return proc.stdout.rstrip()
+7 -24
View File
@@ -5,12 +5,11 @@ import tarfile
from pathlib import Path from pathlib import Path
from prebuild import get_pyapp, rebuild_cpython
root = Path(__file__).resolve().parents[1] root = Path(__file__).resolve().parents[1]
output = root.joinpath("dist") output = root.joinpath("dist")
build_dir = root.joinpath("build")
pyapp_dir = root.joinpath("pyapp-latest")
cpython = root.joinpath("build/cpython/airgapped.tar.gz")
builder = build.ProjectBuilder(root) builder = build.ProjectBuilder(root)
wheel = builder.build("wheel", output_directory=output) wheel = builder.build("wheel", output_directory=output)
@@ -21,21 +20,15 @@ metadata = stem.split("-")
appname = metadata[0] appname = metadata[0]
version = metadata[1] version = metadata[1]
pyapp_dir = build_dir.joinpath("pyapp-latest")
if pyapp_dir.is_dir() is False:
get_pyapp()
packaged_version = rebuild_cpython()
assert packaged_version == version
cpython = build_dir.joinpath("airgapped.tar.gz")
entrypoint = "dzgui.main:main" entrypoint = "dzgui.main:main"
env = os.environ env = os.environ
env["PYAPP_PROJECT_VERSION"] = version env["PYAPP_PROJECT_VERSION"] = version
env["PYAPP_PROJECT_NAME"] = appname env["PYAPP_PROJECT_NAME"] = appname
env["PYAPP_EXEC_SPEC"] = entrypoint env["PYAPP_EXEC_SPEC"] = entrypoint
env["PYAPP_PROJECT_PATH"] = wheel env["PYAPP_PROJECT_PATH"] = wheel
env["PYAPP_PASS_LOCATION"] = "true" env["PYAPP_PASS_LOCATION"] = "true"
# env["PYAPP_PYTHON_VERSION"] = "3.13"
# NOTE: explicitly install all dependencies into distribution # NOTE: explicitly install all dependencies into distribution
env["PYAPP_SKIP_INSTALL"] = "true" env["PYAPP_SKIP_INSTALL"] = "true"
@@ -46,20 +39,10 @@ env["PYAPP_DISTRIBUTION_PYTHON_PATH"] = "python/bin/python3"
proc = subprocess.run(["cargo", "build", "--release"], env=env, cwd=pyapp_dir) proc = subprocess.run(["cargo", "build", "--release"], env=env, cwd=pyapp_dir)
if proc.returncode == 0: if proc.returncode == 0:
output_exe = build_dir.joinpath("pyapp-latest/target/release/pyapp") output_exe = root.joinpath("pyapp-latest/target/release/pyapp")
release_exe = output.joinpath(appname) release_exe = output.joinpath(appname)
output_exe.rename(release_exe) output_exe.rename(release_exe)
tarpath = output.joinpath(tarname) tarpath = output.joinpath(tarname)
with tarfile.open(tarpath, "w:gz") as tar: with tarfile.open(tarpath, "w:gz") as tar:
info = tar.gettarinfo(release_exe) tar.add(release_exe, arcname=appname)
info.uname = appname
info.name = appname
with open(release_exe, "rb") as f:
tar.addfile(info, f)
print(f"Wrote tarfile to '{tarpath}'") print(f"Wrote tarfile to '{tarpath}'")
proc = subprocess.run([release_exe, "-v"], capture_output=True, text=True)
assert proc.stdout.rstrip() == version
release_exe.unlink()
Path(wheel).unlink()
-14
View File
@@ -1,14 +0,0 @@
{
"cols": {
"Name": 1,
"Map": 1,
"Gametime": 1,
"Players": 1,
"Queue": 1,
"IP": 1,
"Qport": 1,
"Ping": 1,
"Maximum": 1,
"Perspective": 1
}
}
-12
View File
@@ -1,12 +0,0 @@
{
"cols": {
"Name": 398,
"Map": 181,
"Gametime": 151,
"Players": 119,
"Queue": 96,
"IP": 247,
"Qport": 94,
"Ping": 253
}
}
-13
View File
@@ -1,13 +0,0 @@
{
"cols": {
"Name": 1,
"Map": 1,
"Gametime": 1,
"Players": 1,
"Queue": 1,
"IP": 1,
"Qport": 1,
"Ping": 1,
"View": 1
}
}
-33
View File
@@ -1,33 +0,0 @@
import pytest
from dzgui.init.migrate import convert_cols_file
from tests.fixtures import fixture_path
@pytest.fixture
def columns_with_perspective():
return fixture_path("columns_1")
@pytest.fixture
def columns_without_perspective():
return fixture_path("columns_2")
@pytest.fixture
def columns_with_view():
return fixture_path("columns_3")
@pytest.mark.config
def test_columns_with_perspective(columns_with_perspective):
j = convert_cols_file(columns_with_perspective)
assert "View" in j["cols"]
assert "Max" in j["cols"]
@pytest.mark.config
def test_columns_without_perspective(columns_without_perspective):
j = convert_cols_file(columns_without_perspective)
assert "View" not in j["cols"]
assert "Max" not in j["cols"]
@pytest.mark.config
def test_columns_with_view(columns_with_view):
j = convert_cols_file(columns_with_view)
assert j is None
+3 -3
View File
@@ -2,14 +2,14 @@ from dzgui.util import localize
def test_de(monkeypatch): def test_de(monkeypatch):
monkeypatch.setenv("LC_ALL", "de_DE.UTF-8") monkeypatch.setenv("LC_CTYPE", "de_DE.UTF-8")
localize.set_locale() localize.set_locale()
assert localize.number(9900) == "9.900" assert localize.number(9900) == "9.900"
assert localize.number(99.88) == "99,88" assert localize.number(99.88) == "99,88"
def test_en(monkeypatch): def test_en(monkeypatch):
monkeypatch.setenv("LC_ALL", "en_US.UTF-8") monkeypatch.setenv("LC_CTYPE", "en_US.UTF-8")
localize.set_locale() localize.set_locale()
assert localize.number(9900) == "9,900" assert localize.number(9900) == "9,900"
assert localize.number(99.88) == "99.88" assert localize.number(99.88) == "99.88"
@@ -22,7 +22,7 @@ def test_none(monkeypatch):
def test_empty(monkeypatch): def test_empty(monkeypatch):
monkeypatch.setenv("LC_ALL", "") monkeypatch.setenv("LC_CTYPE", "")
localize.set_locale() localize.set_locale()
assert localize.number(9900) == "9,900" assert localize.number(9900) == "9,900"
assert localize.number(99.88) == "99.88" assert localize.number(99.88) == "99.88"