Compare commits

..
55 Commits
Author SHA1 Message Date
aclist e3027fa830 Merge pull request #310 from aclist/fix/7.0.0b6-hotfixes
fix: 7.0.0b6 hotfixes
2026-05-26 04:54:48 +09:00
aclist 92699ebaa9 chore: revert delay to 3s 2026-05-26 04:42:50 +09:00
aclist ef2709561d fix: resolve #306 2026-05-26 04:40:14 +09:00
aclist 89a6996695 Merge pull request #304 from aclist/chore/prebuild-script
chore: update build tools
2026-05-26 04:01:24 +09:00
aclist 41b3ad8eca chore: clear typehinting errors 2026-05-26 03:59:11 +09:00
aclist d57277f527 fix: drop unused import 2026-05-26 03:57:13 +09:00
aclist c45678ca88 chore: add custom ScrolledWindow class 2026-05-26 03:56:35 +09:00
aclist c95ad53f6f chore: bump version 2026-05-26 03:55:51 +09:00
aclist 3e144dc7d2 fix: resolve #305 2026-05-26 03:54:30 +09:00
aclist afdfa76fdf fix: resolve #309, #307 2026-05-26 03:53:01 +09:00
aclist 00c27ad020 fix: resolve #308 2026-05-26 03:52:11 +09:00
aclist d60204b676 chore: reorder operations in entrypoint function 2026-05-26 03:51:44 +09:00
aclist 3d2e27bfe1 chore: update release script 2026-05-26 00:56:02 +09:00
aclist 8a66bafe4c chore: update build tools 2026-05-26 00:55:11 +09:00
aclist 2963c4e01c Merge pull request #299 from aclist/chore/move-updates
chore: move updates
2026-05-25 16:18:07 +09:00
aclist f849cbc183 chore: update tests 2026-05-25 16:07:44 +09:00
aclist 77505db621 fix: collision with native method name 2026-05-25 15:13:08 +09:00
aclist ba7a2b7ab4 fix: fallback for missing entries in columns file 2026-05-25 15:11:21 +09:00
aclist 6d7ed80996 Merge branch 'chore/move-updates' of github.com:aclist/dztui into chore/move-updates 2026-05-25 00:04:14 +09:00
aclist 72024c5d43 fix: refresh increment overwriting when changing tabs 2026-05-25 00:04:06 +09:00
aclist 0426d50d3e fix: refresh increment ov refresh decrement resetting when changing tabs 2026-05-25 00:03:21 +09:00
aclist 26ee560cb4 chore: test build version after export 2026-05-24 18:56:41 +09:00
aclist 6ec77f6542 fix: pass update args to manager 2026-05-24 01:53:16 +09:00
aclist 59959ae3a1 chore: clear typehinting errors 2026-05-24 01:49:08 +09:00
aclist 05fd95cd8e chore: bump version 2026-05-24 01:34:22 +09:00
aclist 86ba8d9187 chore: move update logic to manager 2026-05-24 01:34:16 +09:00
aclist e715e6da1c fix: use LC_ALL var in test 2026-05-23 22:23:44 +09:00
aclist 18ba851b7e chore: drop unused code 2026-05-23 22:23:29 +09:00
aclist d2643d08e9 Merge pull request #296 from aclist/fix/empty-statusbar
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
fix: statusbar being wiped when calcdist fails on empty row
2026-05-21 21:07:23 +09:00
aclist 6274f62c93 chore: remove print statement
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
2026-05-21 21:06:40 +09:00
aclist 24609a1c71 docs: update changelog 2026-05-21 20:58:32 +09:00
aclist 1b344c1260 chore: clear typehinting errors
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
2026-05-21 20:55:01 +09:00
aclist e72e09eec1 feat: save filters on exit 2026-05-21 20:42:24 +09:00
aclist 00e78b1228 fix: set filter panel to insensitive on lan load 2026-05-21 20:42:00 +09:00
aclist efddd61ea1 chore: bump version 2026-05-21 01:49:55 +09:00
aclist d006908bbc fix: statusbar being wiped when calcdist fails on empty row 2026-05-21 01:47:05 +09:00
aclist 5ba6b6c040 Merge pull request #295 from aclist/fix/lan-server
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
fix: break out of loop when sek out of loop on first server
2026-05-20 00:36:58 +09:00
aclist d3b9ba031d Merge branch 'fix/lan-server' of github.com:aclist/dztui into fix/lan-server 2026-05-20 00:35:49 +09:00
aclist 8bfcb6c8fe fix: break out of loop when scanning LAN 2026-05-20 00:35:35 +09:00
aclist 353b10ebfc fix: break out of loop when sek out of loop on first server 2026-05-20 00:25:10 +09:00
aclist 698150645d Merge pull request #290 from aclist/feat/airgapped-installer
feat: airgapped installer
2026-05-20 00:09:31 +09:00
aclist 87f347870f chore: clean up try/except block
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
2026-05-19 23:41:51 +09:00
aclist 74bf4b5cfc se str, not Path
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
2026-05-19 18:35:47 +09:00
aclist 896799f904 chore: update string 2026-05-19 18:34:35 +09:00
aclist 0f591e658c fix: call proper workshop method 2026-05-19 18:31:59 +09:00
aclist f9083b33ef fix: change lan panel tate on map/unmap 2026-05-19 18:28:45 +09:00
aclist 89fcfcb5d6 fix: use hostname.local 2026-05-19 18:26:09 +09:00
aclist fc7f1d7815 chore: update build script paths 2026-05-19 18:25:29 +09:00
aclist fb36425083 chore: bump version 2026-05-19 06:23:53 +09:00
aclist c5d8afbc4d chore: build dependencies into distribution tarball 2026-05-19 06:08:55 +09:00
aclist 17f732fafe feat: continue to main app after config wizard closes 2026-05-19 06:05:22 +09:00
aclist 676a7ea9eb fix: notes file may not be present 2026-05-19 06:04:45 +09:00
aclist d3f64c1ba5 chore: trigger backup workflow
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
2026-05-18 22:05:46 +09:00
aclist 5da3397a29 Merge branch 'dzgui7' of github.com:aclist/dztui into dzgui7 2026-05-18 22:03:02 +09:00
aclist eca6df90d4 chore: add .github directory 2026-05-18 22:02:16 +09:00
46 changed files with 635 additions and 166 deletions

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
# These are supported funding model platforms
github: aclist
+57
View File
@@ -0,0 +1,57 @@
name: Bug report
description: Submit bugs that affect functionality here
title: "[BUG] Your title here"
labels: ["bug"]
assignees:
- aclist
body:
- type: markdown
attributes:
value: |
### WARNING: When posting log/config data, redact any sensitive information like API keys.
- type: checkboxes
id: kb
attributes:
label: Did you check if this issue already exists?
options:
- label: I have checked the [Knowledge Base](https://aclist.github.io/dzgui/kb.html) and searched through [past issues](https://github.com/aclist/dztui/issues?q=is%3Aissue+is%3Aclosed) and did not find my issue there.
required: true
- type: dropdown
id: device
attributes:
label: Device type
options:
- Desktop PC
- Steam Deck
default: 0
validations:
required: true
- type: input
id: version
attributes:
label: DZGUI version
validations:
required: true
placeholder: "5.6.0"
- type: input
id: distro
attributes:
label: Linux distribution
validations:
required: true
placeholder: "Arch Linux"
- type: textarea
id: what-happened
attributes:
label: Describe the bug
description: What did you expect to happen?
placeholder: "E.g., When doing X, Y occurred. Include as much detail as possible."
validations:
required: true
- type: textarea
id: logs
attributes:
label: Additional log output
description: Attach the file `$HOME/.local/state/dzgui/DZGUI_DEBUG.log` by clicking this area to highlight it and then dragging the file in.
validations:
required: false
+1
View File
@@ -0,0 +1 @@
blank_issues_enabled: false
+47
View File
@@ -0,0 +1,47 @@
name: Feature suggestion
description: Submit requests for new features here
title: "[FEAT] Your title here"
labels: ["enhancement"]
assignees:
- aclist
body:
- type: dropdown
id: device
attributes:
label: Device type
options:
- Desktop PC
- Steam Deck
default: 0
validations:
required: true
- type: input
id: version
attributes:
label: DZGUI version
validations:
required: true
placeholder: "5.6.0"
- type: input
id: distro
attributes:
label: Linux distribution
validations:
required: true
placeholder: "Arch Linux"
- type: textarea
id: what-happened
attributes:
label: Describe the feature
description: A clear and detailed description
placeholder: "E.g., Add a dialog to the Options menu that automatically lets the user do Y"
validations:
required: true
- type: textarea
id: why
attributes:
label: Describe the benefit
description: How will this feature benefit large numbers of users?
placeholder: "E.g., Currently, users have to manually do X to achieve Y. Implementing this feature would achieve A, B, and C."
validations:
required: true
+50
View File
@@ -0,0 +1,50 @@
name: Troubleshooting
description: Submit user error/troubleshooting questions here
title: "[HELP] Your title here"
labels: ["troubleshooting"]
assignees:
- aclist
body:
- type: markdown
attributes:
value: |
### WARNING: When posting log/config data, redact any sensitive information like API keys.
- type: dropdown
id: device
attributes:
label: Device type
options:
- Desktop PC
- Steam Deck
default: 0
validations:
required: true
- type: input
id: version
attributes:
label: DZGUI version
validations:
required: true
placeholder: "5.6.0"
- type: input
id: distro
attributes:
label: Linux distribution
validations:
required: true
placeholder: "Arch Linux"
- type: textarea
id: what-happened
attributes:
label: Describe the issue
description: A detailed description. What did you expect to happen?
placeholder: "Include as much detail as possible and state the steps leading up to the problem. E.g., First I do this, then I do this, then this happens."
validations:
required: true
- type: textarea
id: logs
attributes:
label: Additional log output
description: Attach the file `$HOME/.local/state/dzgui/DZGUI_DEBUG.log` by clicking this area to highlight it and then dragging the file in.
validations:
required: false
+18
View File
@@ -0,0 +1,18 @@
name: Mirror to Codeberg
on:
push:
workflow_dispatch:
jobs:
mirror-to-codeberg:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: pixta-dev/repository-mirroring-action@v1
if: ${{ vars.GIT_REMOTE != '' }}
with:
target_repo_url: ${{ vars.GIT_REMOTE }}
ssh_private_key: ${{ secrets.GIT_SSH_PRIVATE_KEY }}
+1
View File
@@ -34,6 +34,7 @@
- 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
## Changed ## Changed
+10 -5
View File
@@ -1,18 +1,23 @@
# 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
# Building standalone DZGUI release binaries # Building standalone DZGUI release binaries
1. Run `./scripts/dl_pyapp.sh` 1. Run `./scripts/dl_pyapp.sh`
2. Run `python3 scripts/release.py` # TODO: secondary script that packs this archive
3. Generate a release and tag against the bundled `dzgui` executable 2. Download cpython 3.13, linux, gnu, v3
https://github.com/astral-sh/python-build-standalone/releases/download/20251014/cpython-3.13.9%2B20251014-x86_64_v3-unknown-linux-gnu-install_only_stripped.tar.gz
3. Explicitly install dependencies into site-packages
4. Pack back into a tarball
5. Run `python3 scripts/release.py`
6. Generate a release and tag against the bundled `dzgui` executable
+2 -1
View File
@@ -106,7 +106,7 @@ class A2SInfo:
def get_netmask() -> str: def get_netmask() -> str:
hostname = os.uname()[1] hostname = os.uname()[1]
i = socket.gethostbyname(hostname) i = socket.gethostbyname(f"{hostname}.local")
netmask = i.rsplit(".", 1)[0] netmask = i.rsplit(".", 1)[0]
return netmask return netmask
@@ -472,6 +472,7 @@ 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
+3 -3
View File
@@ -17,6 +17,7 @@ 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:
@@ -63,9 +64,6 @@ 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"]
@@ -80,6 +78,7 @@ 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,
@@ -91,4 +90,5 @@ def parse_filepaths(xdg: dict) -> Xdg:
system, system,
debug, debug,
ips, ips,
filters
) )
+4
View File
@@ -12,6 +12,7 @@ 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__()
@@ -150,3 +151,6 @@ 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
+30 -11
View File
@@ -84,7 +84,6 @@ 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
@@ -260,6 +259,21 @@ 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()
@@ -303,8 +317,22 @@ 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
@@ -319,16 +347,7 @@ 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()
# TODO: rename # FIXME: wipe maps store when changing tabs if model is none
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()
+1
View File
@@ -34,6 +34,7 @@
- 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
## Changed ## Changed
+21 -9
View File
@@ -1,6 +1,7 @@
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
@@ -17,17 +18,28 @@ def has_new_config(config: Path) -> bool:
return config.exists() return config.exists()
def migrate_cols_file(res: Path) -> None: def convert_cols_file(res: Path) -> Any | None:
# NOTE: dzg.columns.json is API 7 spec j = read_json(res)
old_res = Path.home() / LEGACY_COLS_PATH cols = j["cols"]
if old_res.is_file(): # NOTE: implies prior conversion
j = read_json(old_res) if "View" in cols:
cols = j["cols"] return None
if "View" in cols: # NOTE: user may not have changed these widths in API 6
return try:
cols["View"] = cols.pop("Perspective") cols["View"] = cols.pop("Perspective")
cols["Max"] = cols.pop("Maximum") cols["Max"] = cols.pop("Maximum")
write_json(j, res) except Exception:
pass
return j
def migrate_cols_file(res: Path) -> None:
# NOTE: filename "dzg.columns.json" is API 7 spec
old_res = Path.home() / LEGACY_COLS_PATH
if old_res.is_file():
j = convert_cols_file(old_res)
if j is not None:
write_json(j, res)
def copy_state_files(state_path: Path) -> None: def copy_state_files(state_path: Path) -> None:
+11 -4
View File
@@ -112,14 +112,15 @@ def main() -> None:
copy_state_files(xdg_paths["XDG_STATE_HOME"]) copy_state_files(xdg_paths["XDG_STATE_HOME"])
# TODO: add logging inside wizard # TODO: add logging inside wizard
SetupWizard(_is_steam_deck, XDG.config) SetupWizard(_is_steam_deck, XDG.config)
# NOTE: implies that setup wizard failed or was closed
if has_new_config(XDG.config) is False:
return return
setup_logger(XDG.debug) setup_logger(XDG.debug)
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()
@@ -127,16 +128,22 @@ def main() -> None:
if has_steam_client() is False: if has_steam_client() is False:
EarlyAlertDialog(init.requires_steam) EarlyAlertDialog(init.requires_steam)
# NOTE: clear versions file of unlinked mods is_dayz_installed(XDG.config)
# TODO: slow running procs here--needs dialog
# NOTE: clears versions file of unlinked mods
rebuild_symlinks(XDG.config) rebuild_symlinks(XDG.config)
remove_stale_signatures(XDG.config, XDG.version) remove_stale_signatures(XDG.config, XDG.version)
is_dayz_installed(XDG.config)
# TODO: handle IP DB failure and use coords fallback # TODO: handle IP DB failure and use coords fallback
# TODO: drop this after dialog is complete
print("Fetching geolocation data, may take some time...")
get_ipdb(XDG.ips) get_ipdb(XDG.ips)
local_coords = get_local_coords(XDG.ips) local_coords = get_local_coords(XDG.ips)
use_miles = lookup(XDG.config, Preferences.DIST) use_miles = lookup(XDG.config, Preferences.DIST)
latest_release = check_updates(version)
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 -1
View File
@@ -22,7 +22,6 @@ import gi
gi.require_version("Gtk", "3.0") gi.require_version("Gtk", "3.0")
from gi.repository.Gtk import main_quit # noqa E402 from gi.repository.Gtk import main_quit # noqa E402
# import dzgui.api.servers as Servers
if TYPE_CHECKING: if TYPE_CHECKING:
from dzgui.config.userprefs import UserPrefs from dzgui.config.userprefs import UserPrefs
from dzgui.controllers.mc import Controller from dzgui.controllers.mc import Controller
@@ -160,6 +159,9 @@ 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)
@@ -174,6 +176,7 @@ 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)
@@ -217,3 +220,12 @@ 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
+9 -3
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(required_size) free_mib = format_mib(free)
dayz_running = is_dayz_running() dayz_running = is_dayz_running()
@@ -222,7 +222,8 @@ 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: except Exception as e:
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
) )
@@ -279,8 +280,13 @@ 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(2) time.sleep(3)
if raise_window is True: if raise_window is True:
logger.info("Bringing window to foreground") logger.info("Bringing window to foreground")
+4 -1
View File
@@ -20,7 +20,10 @@ logger = logging.getLogger(APP_NAME)
class NoteManager: class NoteManager:
def __init__(self, controller: "Controller", path: "Path") -> None: def __init__(self, controller: "Controller", path: "Path") -> None:
self.notes_path = path self.notes_path = path
self.cache: dict[str, str] = read_json(self.notes_path) try:
self.cache: dict[str, str] = read_json(self.notes_path)
except Exception:
self.cache = {}
def add_note(self, server: str, note: str) -> None: def add_note(self, server: str, note: str) -> None:
self.cache[server] = note self.cache[server] = note
+67
View File
@@ -0,0 +1,67 @@
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()
+21 -13
View File
@@ -73,6 +73,7 @@ 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
@@ -123,8 +124,10 @@ 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) @call_on_thread(dialog.scanning, show_cancel=True)
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)
@@ -135,19 +138,20 @@ 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()
servers.append(res) break
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)
@@ -239,9 +243,11 @@ 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("servers_loaded_init") self.emitter.emit("load_maps", store)
# 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()
@@ -318,6 +324,7 @@ 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:
@@ -368,30 +375,31 @@ 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)
self.emitter.emit("servers_loaded", self.enum) if self.controller.get_active_treeview().get_enum == ServerTab.SAVED:
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())
self.emitter.emit("servers_loaded_init") store = filter_man.get_map_store()
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()
+1 -1
View File
@@ -67,4 +67,4 @@ label_client = "Steam client"
### Completion page ### Completion page
heading_completion = "Setup complete" heading_completion = "Setup complete"
blurb_completion = "Configuration completed successfully. Please exit and restart DZGUI to apply changes." blurb_completion = "Configuration completed successfully. Click 'Close' to close this dialog and launch DZGUI."
+5 -7
View File
@@ -2,19 +2,17 @@ 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:
try: data = json.load(infile)
data = json.load(infile) return data
return data except Exception as e:
except json.decoder.JSONDecodeError as e:
raise e
except OSError as e:
raise e raise e
def write_json(data: dict, path: Path) -> None: def write_json(data: dict[str, Any], path: Path) -> None:
try: try:
j = json.dumps(data, indent=2) j = json.dumps(data, indent=2)
path.write_text(j) path.write_text(j)
+1 -1
View File
@@ -101,7 +101,7 @@ build_corrupted = (
"Steam settings or DayZ installation may be corrupted. Try restarting Steam." "Steam settings or DayZ installation may be corrupted. Try restarting Steam."
) )
api_warn_msg = """No servers returned. Please wait and try again. api_warn_msg = """No servers returned. Please wait and try again.
If this issue persists, your API key may be defunct or your network is blocking requests. This usually indicates a transitory network timeout. If this issue persists, your API key or network may be malfunctioning.
""" """
server_timeout = "Timed out when querying server, check IP or try again later." server_timeout = "Timed out when querying server, check IP or try again later."
server_error = ( server_error = (
+10 -7
View File
@@ -43,11 +43,14 @@ 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
for file in exp_path.iterdir(): try:
if file.is_symlink(): for file in exp_path.iterdir():
file.unlink() if file.is_symlink():
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)
+1 -1
View File
@@ -262,8 +262,8 @@ class Grid(Gtk.Grid):
self.emitter.connect("server_page_toggled", self.toggle_filter_panels) self.emitter.connect("server_page_toggled", self.toggle_filter_panels)
# TODO: drop, use map/signal
def hide_widgets_on_init(self) -> None: def hide_widgets_on_init(self) -> None:
self.conpan.set_lan_visible(False)
self.right_panel.sel_panel.hide() self.right_panel.sel_panel.hide()
def toggle_filter_panels(self, emitter: "Emitter", state: bool) -> None: def toggle_filter_panels(self, emitter: "Emitter", state: bool) -> None:
+12 -5
View File
@@ -94,9 +94,11 @@ 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
@@ -110,17 +112,21 @@ 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.loading = True self.is_clicked = 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:
self.set_sensitive(False) return
self.loading = False self.set_sensitive(False)
self.show_time(True) self.loading = True
GLib.timeout_add_seconds(1, self.decrement) self.is_clicked = False
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
@@ -128,6 +134,7 @@ 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 -4
View File
@@ -278,9 +278,7 @@ class ConnectPanel(Gtk.Box):
for el in self.lan, self.fav, self.add_panel: for el in self.lan, self.fav, self.add_panel:
self.add(el) self.add(el)
self.lan.set_visible(False)
def set_lan_visible(self, state: bool) -> None:
self.lan.set_visible(state)
def _on_lan_tab_toggled(self, emitter: "Emitter", state: bool) -> None: def _on_lan_tab_toggled(self, emitter: "Emitter", state: bool) -> None:
self.set_lan_visible(state) self.lan.set_visible(state)
+6
View File
@@ -68,6 +68,7 @@ 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()
@@ -330,3 +331,8 @@ 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])
+14 -53
View File
@@ -1,9 +1,5 @@
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
@@ -13,20 +9,14 @@ 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.threading import call_on_thread, StoredFunc, ThreadingManager from dzgui.managers.update import UpdateManager
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
@@ -45,13 +35,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)
@@ -95,54 +85,20 @@ class RightPanel(Gtk.Box):
valign=Gtk.Align.END, valign=Gtk.Align.END,
spacing=10, spacing=10,
) )
if ALLOW_UPDATES and update is not None: exe_path = os.getenv("PYAPP")
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, update "clicked", self._on_update_button_clicked, exe_path, 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_success(self) -> None: def _on_update_button_clicked(
msg = dialogs.update_success self, button: Gtk.Button, exe_path: str, url: str
dialog = QuitDialog(self.controller, msg) ) -> None:
dialog.run() UpdateManager(self.controller).update_version(exe_path, url)
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"
@@ -151,6 +107,11 @@ 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
@@ -0,0 +1,9 @@
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()
# TODO: CalcDist is being called when table is not loaded # FIXME: CalcDist is being called when table is not loaded
if dist is None: if dist is None:
self.set_by_context(context, "") return
else: else:
pretty = self.append_distance(dist) pretty = self.append_distance(dist)
self.set_by_context(context, pretty) self.set_by_context(context, pretty)
+2
View File
@@ -53,3 +53,5 @@ 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 -2
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:
path = self.get_focused_row_path() # type: ignore mod = self.get_value_at_index(2) # type: ignore
self.controller.open_mod_page(path) # type: ignore self.controller.open_workshop_page(mod) # type: ignore
+2 -1
View File
@@ -1,5 +1,6 @@
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
@@ -21,7 +22,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 = Gtk.ScrolledWindow() self.scrolled = NoOverlayScrolledWindow()
self.treeview = LogTreeView(controller) self.treeview = LogTreeView(controller)
self.scrolled.add(self.treeview) self.scrolled.add(self.treeview)
+3 -1
View File
@@ -1,4 +1,5 @@
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
@@ -10,7 +11,8 @@ 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__()
+1 -1
View File
@@ -17,7 +17,7 @@ from dzgui.views.trees.tree_server_mods import ServerModTreeView
import gi import gi
gi.require_version("Gtk", "3.0") gi.require_version("Gtk", "3.0")
from gi.repository import Gdk, Gtk # type: ignore # noqa E402 from gi.repository import Gdk, Gtk # noqa E402
if TYPE_CHECKING: if TYPE_CHECKING:
-13
View File
@@ -162,19 +162,6 @@ 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()
+6 -6
View File
@@ -72,6 +72,7 @@ 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,
@@ -96,8 +97,9 @@ 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:
w = width_map[column_title] if column_title in width_map:
column.set_fixed_width(w) w = width_map[column_title]
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)
@@ -162,8 +164,8 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore
def _on_map(self, widget: Self) -> None: def _on_map(self, widget: Self) -> None:
# TODO: disable filter panel if current model is None # TODO: disable filter panel if current model is None
if self.get_enum() is ServerTab.LAN: state = self.get_enum() is ServerTab.LAN
self.emitter.emit("lan_tab_toggled", True) self.emitter.emit("lan_tab_toggled", state)
store = self.filter_man.get_map_store() store = self.filter_man.get_map_store()
@@ -178,8 +180,6 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore
# NOTE: removes queue checker for this tab # NOTE: removes queue checker for this tab
GLib.Source.remove(self.queue_id) GLib.Source.remove(self.queue_id)
self.emitter.disconnect(self.handler_id) self.emitter.disconnect(self.handler_id)
if self.get_enum() is ServerTab.LAN:
self.emitter.emit("lan_tab_toggled", False)
def _on_col_width_changed( def _on_col_width_changed(
self, col: Gtk.TreeViewColumn, width: GObject.ParamSpecInt self, col: Gtk.TreeViewColumn, width: GObject.ParamSpecInt
+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.0b2" version = "7.0.0b6"
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
@@ -0,0 +1,81 @@
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()
+26 -6
View File
@@ -5,10 +5,12 @@ 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")
builder = build.ProjectBuilder(root) builder = build.ProjectBuilder(root)
wheel = builder.build("wheel", output_directory=output) wheel = builder.build("wheel", output_directory=output)
@@ -19,23 +21,41 @@ metadata = stem.split("-")
appname = metadata[0] appname = metadata[0]
version = metadata[1] version = metadata[1]
entrypoint = "dzgui.main:main" 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"
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_DISTRIBUTION_EMBED"] = "true"
env["PYAPP_PYTHON_VERSION"] = "3.13"
env["PYAPP_PASS_LOCATION"] = "true" env["PYAPP_PASS_LOCATION"] = "true"
# NOTE: explicitly install all dependencies into distribution
env["PYAPP_SKIP_INSTALL"] = "true"
env["PYAPP_DISTRIBUTION_EMBED"] = "true"
env["PYAPP_FULL_ISOLATION"] = "true"
env["PYAPP_DISTRIBUTION_PATH"] = str(cpython)
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 = root.joinpath("pyapp-latest/target/release/pyapp") output_exe = build_dir.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:
tar.add(release_exe, arcname=appname) tar.add(release_exe, arcname=appname)
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
@@ -0,0 +1,14 @@
{
"cols": {
"Name": 1,
"Map": 1,
"Gametime": 1,
"Players": 1,
"Queue": 1,
"IP": 1,
"Qport": 1,
"Ping": 1,
"Maximum": 1,
"Perspective": 1
}
}
+12
View File
@@ -0,0 +1,12 @@
{
"cols": {
"Name": 398,
"Map": 181,
"Gametime": 151,
"Players": 119,
"Queue": 96,
"IP": 247,
"Qport": 94,
"Ping": 253
}
}
+13
View File
@@ -0,0 +1,13 @@
{
"cols": {
"Name": 1,
"Map": 1,
"Gametime": 1,
"Players": 1,
"Queue": 1,
"IP": 1,
"Qport": 1,
"Ping": 1,
"View": 1
}
}
+33
View File
@@ -0,0 +1,33 @@
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_CTYPE", "de_DE.UTF-8") monkeypatch.setenv("LC_ALL", "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_CTYPE", "en_US.UTF-8") monkeypatch.setenv("LC_ALL", "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_CTYPE", "") monkeypatch.setenv("LC_ALL", "")
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"