Compare commits

...
83 Commits
Author SHA1 Message Date
aclist 28cd6b8c45 Merge pull request #436 from aclist/chore/restore-maximize-logic
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
chore: restore maximize logic
2026-08-05 19:14:27 +09:00
aclist 81e327fb35 chore: drop unused import 2026-08-05 19:13:31 +09:00
aclist 37e247d1d9 chore: restore maximize logic following changelog margin hotfix 2026-08-05 19:13:11 +09:00
aclist 286e3f2d80 Merge pull request #435 from aclist/fix/margins-after-expansion
fix: set margins after expansion
2026-08-05 19:08:35 +09:00
aclist 2ef27a55d8 fix: set margins only after widget expansion 2026-08-05 19:05:57 +09:00
aclist d0ac09ffe5 change: add line breaks to tooltip 2026-08-05 19:05:10 +09:00
aclist c0015c05ec Merge pull request #434 from aclist/fix/maximize-after-realize
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
fix: test maximizing after widget and children are fully realized
2026-08-05 00:24:08 +09:00
aclist 682b5bb4c0 chore: rename method 2026-08-05 00:23:10 +09:00
aclist 3551877192 chore: test maximizing after widget and children are fully realized 2026-08-05 00:22:12 +09:00
aclist b22aba8593 Merge pull request #433 from aclist/fix/set-default-before-maximize
chore: test setting default size before maximizing
2026-08-05 00:06:09 +09:00
aclist f85f1ef2a5 chore: test setting default size before maximizing 2026-08-05 00:04:21 +09:00
aclist 010d7984b6 Merge pull request #432 from aclist/fix/set-default-size
chore: test using default dimensions defined in wizard
2026-08-04 23:57:50 +09:00
aclist 70e1e9e740 Merge branch 'fix/set-default-size' of github.com:aclist/dztui into fix/set-default-size 2026-08-04 23:57:06 +09:00
aclist 734af7cf22 chore: test using default dimensions defined in wizard 2026-08-04 23:56:56 +09:00
aclist d8f4977ffe chore: use default dimensiontest usingefined in wizard 2026-08-04 23:55:54 +09:00
aclist c9fd00f938 Merge pull request #431 from aclist/fix/defer-maximize
chore: test deferring maximize call until after widget is realized
2026-08-04 23:38:30 +09:00
aclist f4db1484d8 chore: test deferring maximize call until after widget is realized 2026-08-04 23:36:08 +09:00
aclist 99ef9a31b9 Merge pull request #430 from aclist/fix/steam-deck-fs
fix: maximize UI on Steam Deck
2026-08-04 23:16:44 +09:00
aclist 4c392dc571 chore: bump version 2026-08-04 23:15:22 +09:00
aclist 155ffea15d chore: more idiomatic syntax 2026-08-04 23:15:01 +09:00
aclist 57d231331d fix: trigger maximize on steam deck (#426) 2026-08-04 23:14:20 +09:00
aclist d5fe3c35dc Merge pull request #429 from aclist/fix/scrollable-key-nav
fix: repack certain pages to support keybindings
2026-08-04 23:12:07 +09:00
aclist 3cf547e903 chore: backport changes from dzgui7 branch
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
2026-08-04 23:09:54 +09:00
aclist bd0bfcb03d Merge pull request #428 from aclist/fix/invalid-mod-manifest
fix: handle invalid mods on server
2026-08-04 23:06:05 +09:00
aclist 4f6c583294 chore: add comment
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
2026-08-04 23:05:12 +09:00
aclist 5e5a6b4eaf chore: more generic warning message 2026-08-04 23:04:47 +09:00
aclist ff59d6dc35 chore: clear typehinting errors 2026-08-04 18:04:22 +09:00
aclist 1c154a709d fix: repack certain pages to support mixins 2026-08-04 18:03:14 +09:00
aclist fb47564618 chore: trigger checks
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
2026-07-31 23:13:03 +09:00
aclist 30e309442b chore: trigger checks 2026-07-30 20:45:58 +09:00
aclist 3e4ed95908 chore: trigger checks 2026-07-30 20:37:14 +09:00
aclist 93edbac5b0 fix: handle invalid mods on server 2026-07-30 20:35:23 +09:00
aclist c947bc2c9b Merge pull request #425 from aclist/fix/flickering-cancel-button
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
fix: flickering cancel button
2026-07-28 17:12:43 +09:00
aclist 06315972b1 Merge pull request #424 from aclist/fix/options-web-button
chore: normalize position of web button following BM API drop
2026-07-28 17:11:45 +09:00
aclist 4e36824ed7 Merge pull request #423 from aclist/fix/lockstep-history
fix:  maintain lockstep with history file (#362)
2026-07-28 17:11:15 +09:00
aclist 0bb618b703 fix: more applicable error message
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
2026-07-28 17:10:19 +09:00
aclist b8e11d5b59 chore: clean up comments 2026-07-28 16:48:01 +09:00
aclist db8b1acbc6 fix: pack spinner in dedicated box
Resolves #357. Spinner and cancel button occupied the same region of the
content area, causing spinner repaint events on each frame to redraw the
cancel button, triggering a flickering effect.
2026-07-28 16:40:01 +09:00
aclist 1e5d66c9be chore: change alignment property to hold future items 2026-07-27 23:19:23 +09:00
aclist 202620c062 chore: normalize position of web button following BM API drop 2026-07-27 23:17:49 +09:00
aclist 673038a1ab fix: maintain lockstep with history file (#362) 2026-07-27 22:11:48 +09:00
aclist c97e34c336 Merge pull request #422 from aclist/chore/drop-bm-api
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
chore: drop support for BM api
2026-07-27 21:24:56 +09:00
aclist 799ea263a0 chore: drop temporary test mark 2026-07-27 21:14:58 +09:00
aclist 08cbd88262 chore: add ip entry validator tests 2026-07-27 21:13:30 +09:00
aclist 5b5e91c2fa chore: drop digit parsing from connect panel entry 2026-07-27 21:13:13 +09:00
aclist f8be7e33d1 fix: drop unused import resulting from prior PR 2026-07-27 21:03:03 +09:00
aclist 54c950fc41 chore: drop unused import 2026-07-27 21:02:39 +09:00
aclist 1e266c3cd2 chore: drop BM API 2026-07-27 21:02:14 +09:00
aclist 16959a692d Merge pull request #421 from aclist/fix/make-desktop
fix: create Desktop path if missing
2026-07-27 20:40:37 +09:00
aclist 7c79b17fdc fix: drop f-string 2026-07-27 20:39:38 +09:00
aclist 87e0af33cc fix: create Desktop path if missing 2026-07-27 20:36:04 +09:00
aclist 3b4a736ce6 Merge pull request #420 from aclist/chore/update-tests
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
chore: update tests
2026-07-27 20:22:43 +09:00
aclist b70428fe3d chore: BM API is expected to fail (#417) 2026-07-27 20:21:56 +09:00
aclist 3f037cdad6 chore: add realconfig namespace to local file tests 2026-07-27 20:21:39 +09:00
aclist 9b63d38c5a chore: more granular test namespaces 2026-07-27 20:21:13 +09:00
aclist 4c26d85afd Merge branch 'dzgui7' of github.com:aclist/dztui into dzgui7 2026-07-27 20:15:38 +09:00
aclist 35b8d21181 Merge pull request #419 from aclist/fix/deprecation-warnings-2
chore: comment out unused code
2026-07-27 20:15:30 +09:00
aclist e368e98a1a Merge branch 'fix/deprecation-warnings-2' of github.com:aclist/dztui into fix/deprecation-warnings-2 2026-07-27 20:14:55 +09:00
aclist 2ea6490a9b chore: comment out unused code 2026-07-27 20:14:43 +09:00
aclist 7b57f95f5f Merge pull request #418 from aclist/fix/deprecation-warnings
fix: clear deprecation warnings
2026-07-27 20:14:30 +09:00
aclist a811e5945d chore: commend out unused code 2026-07-27 20:13:47 +09:00
aclist 9b305d806b Merge pull request #418 from aclist/fix/deprecation-warnings
fix: clear deprecation warnings
2026-07-27 20:11:32 +09:00
aclist 1955abd3e6 Merge branch 'dzgui7' into fix/deprecation-warnings 2026-07-27 20:10:45 +09:00
aclist aac29c0a5c chore: bump version 2026-07-27 20:10:08 +09:00
aclist 218ed43403 Merge pull request #416 from aclist/feat/dynamic-copy
feat: dynamic clipboard button
2026-07-27 20:08:39 +09:00
aclist 380f6f82a1 Merge pull request #414 from aclist/feat/offline-eventbox
feat: offline eventbox
2026-07-27 20:07:38 +09:00
aclist cef9e1984f Merge pull request #415 from aclist/feat/wizard-mixins
feat: wizard mixins
2026-07-27 20:06:06 +09:00
aclist 0520ced2ea chore: drop unused import 2026-07-27 18:58:41 +09:00
aclist 4bbe179940 chore: comment out more unused blocks 2026-07-27 17:42:45 +09:00
aclist 1527239567 fix: clear deprecation warnings 2026-07-27 17:41:12 +09:00
aclist 64c8b905c4 chore: add comments 2026-07-24 02:00:18 +09:00
aclist 52ab81f4ec feat: dynamic update on clipboard button click 2026-07-24 01:56:29 +09:00
aclist ec7828df85 feat: add eventbox to offline mods page 2026-07-24 01:30:32 +09:00
aclist 525de56036 feat: redact user id in logs
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
2026-07-24 01:13:00 +09:00
aclist 7edb66c311 chore: distinguish between api and web tests 2026-07-24 01:05:01 +09:00
aclist dc2a635d2f chore: add typehints to test 2026-07-24 01:04:41 +09:00
aclist 8528300851 chore: update crc tests 2026-07-24 01:01:39 +09:00
aclist daa4aa0059 chore: clear typehinting errors resulting from mixins
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
2026-07-24 00:48:45 +09:00
aclist 71a0481600 fix: add comma 2026-07-24 00:47:28 +09:00
aclist ba1b980a7f chore: rename test namespace 2026-07-24 00:47:08 +09:00
aclist 1dc20b838d fix: support source version of DZGUI 2026-07-24 00:45:27 +09:00
aclist 0c6421fa63 chore: update freedesktop comment
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
2026-07-21 16:30:07 +09:00
aclist a4f9a67da1 fix: use drop-in mixin to set page progress state 2026-07-21 15:01:11 +09:00
43 changed files with 285 additions and 305 deletions

No files matched your search

+3
View File
@@ -44,6 +44,7 @@
- Play offline (load mods directly) - Play offline (load mods directly)
- Generate Steam shortcuts and cover art - Generate Steam shortcuts and cover art
- Warn user if background downloads are disabled - Warn user if background downloads are disabled
- Dynamic copy button
### Changed ### Changed
- Conform to PEP 440 versioning for beta versions - Conform to PEP 440 versioning for beta versions
@@ -63,6 +64,7 @@
- Propagate subscribed mods to Steam client - Propagate subscribed mods to Steam client
### Dropped ### Dropped
- Battlemetrics API support
- Debug mode - Debug mode
- Branch switching - Branch switching
- Manual mod install mode (describe rationale) - Manual mod install mode (describe rationale)
@@ -73,6 +75,7 @@
- Center server title text on server dialogs - Center server title text on server dialogs
- Rare segfaults when changing maps (threading) - Rare segfaults when changing maps (threading)
- Moved dialogs out of threads - Moved dialogs out of threads
- History table not being in lock-step with state file
### Unreleased ### Unreleased
- Load offline mods - Load offline mods
-40
View File
@@ -1,40 +0,0 @@
import logging
import requests
from typing import Any, Optional, TYPE_CHECKING
from dzgui.const.constants import APP_NAME
from dzgui.const.endpoints import BM_SERVERS
logger = logging.getLogger(APP_NAME)
if TYPE_CHECKING:
from dzgui.api.servers import Record
def get_attributes(key: str, uid: int) -> Any:
# TODO: handle if key is not set
# TODO: tests for malformed IDs/values
hdr = {"Authorization": "Bearer " + key}
payload: dict[str, str] = {
"filter[game]": "dayz",
"sort": "-players",
"filter[ids][whitelist]": str(uid),
}
res = requests.get(BM_SERVERS, params=payload, headers=hdr)
res.raise_for_status()
j = res.json()["data"][0]["attributes"]
return j
def map_id_to_record(key: str, uid: int) -> Optional["Record"]:
from dzgui.api.servers import Record
try:
record = get_attributes(key, uid)
ip = record["ip"]
port = int(record["port"])
qport = int(record["portQuery"])
return Record(ip, port, qport)
except Exception:
return None
-15
View File
@@ -41,21 +41,6 @@ def test_ipdb() -> bool:
return False return False
def test_bm_api(key: str) -> bool:
payload: dict[str, str] = {
"filter[game]": "dayz",
}
hdr = {"Authorization": "Bearer " + key}
try:
res = requests.get(
endpoints.BM_SERVERS, params=payload, headers=hdr, timeout=REQUEST_TIMEOUT
)
return is_remote_up(res)
except Exception as e:
logger.critical(e)
return False
def is_remote_up(res: "Response") -> bool: def is_remote_up(res: "Response") -> bool:
if res.status_code == 200: if res.status_code == 200:
return True return True
-11
View File
@@ -11,7 +11,6 @@ import threading
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any, Optional, TYPE_CHECKING, Union from typing import Any, Optional, TYPE_CHECKING, Union
from dzgui.api.bm import map_id_to_record
from dzgui.const.constants import APP_NAME, REQUEST_TIMEOUT from dzgui.const.constants import APP_NAME, REQUEST_TIMEOUT
from dzgui.const.endpoints import STEAM_SERVERS from dzgui.const.endpoints import STEAM_SERVERS
from dzgui.util.format import format_exception from dzgui.util.format import format_exception
@@ -478,16 +477,6 @@ def query_by_ip(addr: str) -> A2SInfo:
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:
"""
Used with numeric Battlemetrics IDs
"""
record = map_id_to_record(key, server_id)
if record is None:
return A2SInfo(Record("0", 0, 0), None)
return query_by_record(record)
def query_by_record(record: Record, update_gameport: bool = False) -> A2SInfo: def query_by_record(record: Record, update_gameport: bool = False) -> A2SInfo:
try: try:
info = a2s.info((record.ip, record.qport), 3.0) info = a2s.info((record.ip, record.qport), 3.0)
+5 -1
View File
@@ -139,6 +139,9 @@ def get_remote_signatures(mods: list[str]) -> list[tuple[str, str, int, int]]:
j = r.json() j = r.json()
rows = j["response"]["publishedfiledetails"] rows = j["response"]["publishedfiledetails"]
for row in rows: for row in rows:
# NOTE: not a valid published file
if row["result"] == 9:
continue
title = str(row["title"]) title = str(row["title"])
_id = str(row["publishedfileid"]) _id = str(row["publishedfileid"])
time = int(row["time_updated"]) time = int(row["time_updated"])
@@ -205,9 +208,11 @@ def launch_offline(
proc = subprocess.run([*client_args, *params]) proc = subprocess.run([*client_args, *params])
return proc.returncode return proc.returncode
def find_loginusers(path: Path) -> Path: def find_loginusers(path: Path) -> Path:
return path / "config" / "loginusers.vdf" return path / "config" / "loginusers.vdf"
def find_user_id(path: Path) -> str | None: def find_user_id(path: Path) -> str | None:
resolved_path = find_loginusers(path) resolved_path = find_loginusers(path)
try: try:
@@ -226,7 +231,6 @@ def find_user_id(path: Path) -> str | None:
return None return None
def find_user_id_32(path: Path) -> int: def find_user_id_32(path: Path) -> int:
uid = find_user_id(path) uid = find_user_id(path)
if uid is None: if uid is None:
+1 -2
View File
@@ -42,6 +42,7 @@ def rc2json(file: Path) -> str:
toggles = ["fullscreen"] toggles = ["fullscreen"]
deprecated = [ deprecated = [
"api_key",
"staging_dir", "staging_dir",
"src_path", "src_path",
"steam_path", "steam_path",
@@ -66,8 +67,6 @@ def rc2json(file: Path) -> str:
value = str2bool(ntok) value = str2bool(ntok)
elif tok == "preferred_client": elif tok == "preferred_client":
tok = "client" tok = "client"
elif tok == "api_key":
tok = "bm_api"
elif tok == "ip_list": elif tok == "ip_list":
while True: while True:
ntok = lex.get_token() ntok = lex.get_token()
+3 -3
View File
@@ -3,7 +3,6 @@ import textwrap
from pathlib import Path from pathlib import Path
from dzgui.const.constants import APP_NAME
from dzgui.util.dirs import copy_dzgui_to_xdg_data, find_icon_resource, make_parents from dzgui.util.dirs import copy_dzgui_to_xdg_data, find_icon_resource, make_parents
@@ -21,7 +20,7 @@ def write_desktop_file(exe_path: Path) -> Path:
Terminal=false Terminal=false
Exec={exe_path} Exec={exe_path}
Name=DZGUI Name=DZGUI
Comment=dzgui Comment=DayZ server browser and mod manager
Icon={icon} Icon={icon}
Categories=Game""" Categories=Game"""
@@ -37,7 +36,8 @@ def write_desktop_file(exe_path: Path) -> Path:
def write_desktop_shortcut(desktop_file: Path) -> None: def write_desktop_shortcut(desktop_file: Path) -> None:
# NOTE: necessarily depends on the above (UI blocks creation without XDG entry first) # NOTE: necessarily depends on the above (UI blocks creation without XDG entry first)
link = Path.home().joinpath(f"Desktop/{APP_NAME}.desktop") link = Path.home().joinpath("Desktop/dzgui.desktop")
make_parents(link)
if link.exists(): if link.exists():
link.unlink() link.unlink()
link.symlink_to(desktop_file) link.symlink_to(desktop_file)
-1
View File
@@ -2,7 +2,6 @@
Generic defaults used when initializing a config file from scratch Generic defaults used when initializing a config file from scratch
""" """
config_boilerplate = { config_boilerplate = {
"bm_api": "",
"fav_server": "", "fav_server": "",
"fav_label": "", "fav_label": "",
"name": "", "name": "",
-3
View File
@@ -7,7 +7,6 @@ SUB_ENDPOINT = "https://api.steampowered.com/IPublishedFileService/Subscribe/v1"
UNSUB_ENDPOINT = "https://api.steampowered.com/IPublishedFileService/Unsubscribe/v1" UNSUB_ENDPOINT = "https://api.steampowered.com/IPublishedFileService/Unsubscribe/v1"
APP_DETAILS = "https://store.steampowered.com/api/appdetails?" APP_DETAILS = "https://store.steampowered.com/api/appdetails?"
BM_SERVERS = "https://api.battlemetrics.com/servers?"
GITHUB = "https://github.com/aclist" GITHUB = "https://github.com/aclist"
GITHUB_RELEASES = "https://api.github.com/repos/aclist/dztui/releases/latest" GITHUB_RELEASES = "https://api.github.com/repos/aclist/dztui/releases/latest"
CODEBERG_RELEASES = "https://codeberg.org/api/v1/repos/aclist/dzgui/releases/latest" CODEBERG_RELEASES = "https://codeberg.org/api/v1/repos/aclist/dzgui/releases/latest"
@@ -27,5 +26,3 @@ GITHUB_ISSUES = "https://github.com/aclist/dzgui/issues"
FORUM = "https://old.reddit.com/r/dzgui" FORUM = "https://old.reddit.com/r/dzgui"
SPONSORS = "https://github.com/sponsors/aclist" SPONSORS = "https://github.com/sponsors/aclist"
STEAM_API_SETUP = "https://steamcommunity.com/dev/apikey" STEAM_API_SETUP = "https://steamcommunity.com/dev/apikey"
BM_API_SETUP = "https://www.battlemetrics.com/developers"
BM_BROWSE = "https://www.battlemetrics.com/servers/dayz"
-3
View File
@@ -61,9 +61,6 @@ class Preferences(EnumWithAttrs):
STEAM = { STEAM = {
"key": "steam_api", "key": "steam_api",
} }
BM = {
"key": "bm_api",
}
CLIENT = { CLIENT = {
"key": "client", "key": "client",
} }
+3 -9
View File
@@ -413,7 +413,7 @@ class Controller(GObject.GObject):
def add_by_str(self, addr: str) -> None: def add_by_str(self, addr: str) -> None:
saved_tree = self.get_servers().get_saved() saved_tree = self.get_servers().get_saved()
ServerModelManager(self, saved_tree).add_by_str(addr) ServerModelManager(self, saved_tree).add_by_ip(addr)
def add_by_record(self, record: "Record") -> None: def add_by_record(self, record: "Record") -> None:
saved_tree = self.get_servers().get_saved() saved_tree = self.get_servers().get_saved()
@@ -437,14 +437,8 @@ class Controller(GObject.GObject):
ServerModelManager(self, tv).remove_from_history(record) ServerModelManager(self, tv).remove_from_history(record)
def connect_by_str(self, addr: str) -> None: def connect_by_str(self, addr: str) -> None:
if addr.isdigit(): self.connection_man = ConnectionManager(self)
config_man = self.get_config_man() self.connection_man.connect_by_ip(addr)
key = config_man.lookup(Preferences.BM)
self.connection_man = ConnectionManager(self)
self.connection_man.connect_by_id(int(addr), key)
else:
self.connection_man = ConnectionManager(self)
self.connection_man.connect_by_ip(addr)
def connect_by_record(self, record: "Record") -> None: def connect_by_record(self, record: "Record") -> None:
self.connection_man = ConnectionManager(self) self.connection_man = ConnectionManager(self)
+1 -3
View File
@@ -12,7 +12,7 @@ from dzgui.const.constants import (
WINDOW_DEFAULT_X, WINDOW_DEFAULT_X,
WINDOW_DEFAULT_Y, WINDOW_DEFAULT_Y,
) )
from dzgui.api.probe import test_steam_api, test_bm_api from dzgui.api.probe import test_steam_api
from dzgui.const.enum import Preferences from dzgui.const.enum import Preferences
from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager
from dzgui.views.dialogs.generic import ExceptionDialog from dzgui.views.dialogs.generic import ExceptionDialog
@@ -102,8 +102,6 @@ class ConfigManager:
def update_api_key(self, key: Preferences, text: str) -> None: def update_api_key(self, key: Preferences, text: str) -> None:
if key is Preferences.STEAM: if key is Preferences.STEAM:
res = test_steam_api(text) res = test_steam_api(text)
else:
res = test_bm_api(text)
if res is True: if res is True:
self.update_config(key, text) self.update_config(key, text)
else: else:
+10 -6
View File
@@ -86,6 +86,7 @@ class Prerequisites:
mods: list[list[str]] mods: list[list[str]]
game_mode: bool game_mode: bool
is_last_server: bool is_last_server: bool
invalid_mods: list[tuple[str, str]]
allows_downloads: tuple[bool, str] allows_downloads: tuple[bool, str]
@@ -103,11 +104,6 @@ class ConnectionManager:
self.remote_mod_ids: list[str] = [] self.remote_mod_ids: list[str] = []
self.missing_mods: list[tuple[str, str, int, int]] = [] self.missing_mods: list[tuple[str, str, int, int]] = []
@call_on_thread(dialog.querying)
def connect_by_id(self, _id: int, key: str) -> None:
res = Servers.query_by_id(_id, key)
self._prepare_connection(res)
@call_on_thread(dialog.querying) @call_on_thread(dialog.querying)
def connect_by_ip(self, addr: str) -> None: def connect_by_ip(self, addr: str) -> None:
res = Servers.query_by_ip(addr) res = Servers.query_by_ip(addr)
@@ -186,7 +182,6 @@ class ConnectionManager:
else: else:
allows_downloads = (True, "") allows_downloads = (True, "")
client_name = self.controller.get_steam_client_name() client_name = self.controller.get_steam_client_name()
client = self.controller.query_config(Preferences.CLIENT) client = self.controller.query_config(Preferences.CLIENT)
running = is_steam_running(client) running = is_steam_running(client)
@@ -197,6 +192,9 @@ class ConnectionManager:
is_last = self.is_last_server() is_last = self.is_last_server()
# TODO: strings
invalid_mods = [(mod[0], mod[1]) for mod in remote_mods if mod[2] == "Invalid mod"]
prereqs = Prerequisites( prereqs = Prerequisites(
name=info.server_name, name=info.server_name,
appid=info.game_id, appid=info.game_id,
@@ -212,6 +210,7 @@ class ConnectionManager:
mods=remote_mods, mods=remote_mods,
game_mode=game_mode, game_mode=game_mode,
is_last_server=is_last, is_last_server=is_last,
invalid_mods=invalid_mods,
allows_downloads=allows_downloads, allows_downloads=allows_downloads,
) )
@@ -267,6 +266,11 @@ class ConnectionManager:
for mod in alpha_mods: for mod in alpha_mods:
if any(mod[1] in tuple for tuple in missing_mods): if any(mod[1] in tuple for tuple in missing_mods):
mod[2] = resync mod[2] = resync
for mod in alpha_mods:
# NOTE: if the mod is neither synched or out of date, it is a malformed mod
if mod[2] == "":
# TODO: strings
mod[2] = "Invalid mod"
return alpha_mods, missing_mods return alpha_mods, missing_mods
+3 -4
View File
@@ -1,7 +1,6 @@
import re import re
from typing import Any, TYPE_CHECKING, Union from typing import Any, TYPE_CHECKING, Union
from warnings import deprecated
from dzgui.const.enum import FilterMode from dzgui.const.enum import FilterMode
from dzgui.model.model_factory import ModelFactory from dzgui.model.model_factory import ModelFactory
@@ -86,8 +85,8 @@ class ProxyModelManager:
if found is False: if found is False:
self.control_model.append(history) self.control_model.append(history)
# if len(self.control_model) == 11: if len(self.control_model) == 11:
# del self.control_model[0] del self.control_model[0]
self.filter(FilterMode.INITIAL, skip_cache=True) self.filter(FilterMode.INITIAL, skip_cache=True)
@@ -259,7 +258,7 @@ class ProxyModelManager:
def set_cache(self, filters: tuple[str], filtered_rows: list[tuple]) -> None: def set_cache(self, filters: tuple[str], filtered_rows: list[tuple]) -> None:
self.filter_cache[filters] = filtered_rows self.filter_cache[filters] = filtered_rows
@deprecated("Currently unused") # @deprecated("Currently unused")
# def convert_model_to_list(self, model: "FastInsertListStore") -> list: # def convert_model_to_list(self, model: "FastInsertListStore") -> list:
# return [[el for el in row] for row in model] # return [[el for el in row] for row in model]
+20 -27
View File
@@ -15,7 +15,7 @@ from dzgui.const.constants import (
from dzgui.const.enum import FilterMode, Preferences, ServerTab from dzgui.const.enum import FilterMode, Preferences, ServerTab
from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager
from dzgui.strings import dialogs from dzgui.strings import dialogs
from dzgui.util.strings import api_warn_msg, dialog from dzgui.util.strings import server_timeout, dialog
from dzgui.views.dialogs.generic import ExceptionDialog from dzgui.views.dialogs.generic import ExceptionDialog
import gi import gi
@@ -175,29 +175,29 @@ class ServerModelManager:
# TODO: wrap except # TODO: wrap except
if self.controller.get_exit_event().is_set(): if self.controller.get_exit_event().is_set():
return return
res = future.result(timeout=API_TIMEOUT)
self.thread_man.increment_dialog() self.thread_man.increment_dialog()
# NOTE: failing entries are culled
if res is None: """
# TODO: log which servers failed Collects futures in linear order after completion
continue for lock-step representation of the state file
servers.append(res) """
if len(servers) == 0: for future in futures:
self.thread_man.set_cleanup_func( res = future.result(timeout=API_TIMEOUT)
StoredFunc(self._cleanup_on_failure), destroy_first=True # NOTE: failing entries are culled
) if res is None:
return # TODO: log which servers failed
# query_direct() currently does not return enough information
continue
servers.append(res)
if len(servers) == 0:
self.thread_man.set_cleanup_func(
StoredFunc(self._cleanup_on_failure), destroy_first=True
)
return
parsed = Servers.parse_json(servers) parsed = Servers.parse_json(servers)
self._push_data(parsed) self._push_data(parsed)
@call_on_thread(dialog.querying)
def add_by_id(self, _id: str) -> None:
config_man = self.controller.get_config_man()
key = config_man.lookup(Preferences.BM)
res = Servers.query_by_id(int(_id), key)
self._parse_single_record(res)
@call_on_thread(dialog.querying) @call_on_thread(dialog.querying)
def add_by_ip(self, addr: str) -> None: def add_by_ip(self, addr: str) -> None:
res = Servers.query_by_ip(addr) res = Servers.query_by_ip(addr)
@@ -241,7 +241,6 @@ class ServerModelManager:
self.first_iteration = False self.first_iteration = False
self.emitter.emit("load_maps", store) self.emitter.emit("load_maps", store)
# self.emitter.emit("servers_loaded_init")
# TODO: dataclass for record rows; check for other dict annotations # TODO: dataclass for record rows; check for other dict annotations
def add_to_history(self, data: tuple[dict[str, Any], "Record"]) -> None: def add_to_history(self, data: tuple[dict[str, Any], "Record"]) -> None:
@@ -263,12 +262,6 @@ class ServerModelManager:
proxy_man.remove_row_from_control(record) proxy_man.remove_row_from_control(record)
self.update_history() self.update_history()
def add_by_str(self, addr: str) -> None:
if addr.isdigit():
self.add_by_id(addr)
else:
self.add_by_ip(addr)
@call_on_thread(dialog.querying) @call_on_thread(dialog.querying)
def update_playercount( def update_playercount(
self, treeiter: Gtk.TreeIter, record: Servers.Record self, treeiter: Gtk.TreeIter, record: Servers.Record
@@ -426,7 +419,7 @@ class ServerModelManager:
# TODO: distinguish signals, e.g. "servers_failed_to_load", "servers_loaded_empty" # TODO: distinguish signals, e.g. "servers_failed_to_load", "servers_loaded_empty"
# customize statusbar and dialog accordingly # customize statusbar and dialog accordingly
if show_dialog: if show_dialog:
dialog = ExceptionDialog(self.controller, api_warn_msg) dialog = ExceptionDialog(self.controller, server_timeout)
dialog.run() dialog.run()
def _push_data(self, data: list[Any]) -> None: def _push_data(self, data: list[Any]) -> None:
+2 -3
View File
@@ -5,10 +5,9 @@ add_tooltip="Add to Saved Servers"
connect_button="Connect" connect_button="Connect"
connect_tooltip="Connect to this server" connect_tooltip="Connect to this server"
connect_entry_tooltip=( connect_entry_tooltip=(
"- IP: format as IP:Query port\ne.g. 192.168.1.1:27016\n" "- IP: format as IP:Query port\ne.g. 192.168.1.1:27016"
"- Battlemetrics: numeric server ID\ne.g. 123456"
) )
connect_entry_placeholder="Enter IP (IP:Query port) or Battlemetrics ID (numerical id)" connect_entry_placeholder="Enter IP (IP:Query port)"
fav_heading="Favorite server" fav_heading="Favorite server"
favs_empty="None set. Right click a server and select 'Set favorite' to set." favs_empty="None set. Right click a server and select 'Set favorite' to set."
+3
View File
@@ -1,4 +1,7 @@
heading = "Play offline" heading = "Play offline"
heading_disclaimer = (
"Not all mod combinations will function offline. Use at your own discretion."
)
mission_button = "Select mission folder" mission_button = "Select mission folder"
mission_frame = "Mission" mission_frame = "Mission"
-5
View File
@@ -46,11 +46,6 @@ You must set up a Steam Web API key in order to browse the global server list.
\nIf you don't have one already, it can be set up via the page below. \nIf you don't have one already, it can be set up via the page below.
\nPlease refer to the DZGUI documentation for more instructions. \nPlease refer to the DZGUI documentation for more instructions.
""" """
heading_bm_api = "Battlemetrics Web API key"
blurb_bm_api = """A Battlemetrics key is <b>optional</b>, but allows you to add/search for servers\n
by numeric ID on the web. For example, in the URL https://www.battlemetrics.net/servers/dayz/24819107,\n
the ID would be <b>24819107</b>.
"""
### PreferencesPage ### PreferencesPage
heading_prefs = "User preferences" heading_prefs = "User preferences"
+2 -1
View File
@@ -4,8 +4,9 @@ from typing import Literal
api_filter = r"(.*&key=)([^&]*)(.*)" api_filter = r"(.*&key=)([^&]*)(.*)"
home_filter = r"(/home/)([^\s'\/]*)(.*)" home_filter = r"(/home/)([^\s'\/]*)(.*)"
user_filter = r"(.*Steam/userdata/)([^/]*)(.*)"
REDACTED = r"\1REDACTED\3" REDACTED = r"\1REDACTED\3"
REDACTION_PATTERNS = [api_filter, home_filter] REDACTION_PATTERNS = [api_filter, home_filter, user_filter]
def redact_home(text: str) -> str: def redact_home(text: str) -> str:
+7 -17
View File
@@ -100,10 +100,7 @@ steam_missing = "Local Steam installation is not set, possibly malformed config
build_corrupted = ( 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. server_timeout = "Timed out when querying server(s). Check IP or try again later."
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_error = ( server_error = (
"Error while contacting server, possibly timed out. Please wait and try again." "Error while contacting server, possibly timed out. Please wait and try again."
) )
@@ -286,11 +283,8 @@ class Thanks:
class Options: class Options:
header: str header: str
steam_web: str steam_web: str
bm_web: str
enter_steam: str enter_steam: str
enter_bm: str
steam_placeholder: str steam_placeholder: str
bm_placeholder: str
name_placeholder: str name_placeholder: str
last_used: str last_used: str
always_fs: str always_fs: str
@@ -311,7 +305,7 @@ class Options:
force_eventbox: str force_eventbox: str
self_update: str self_update: str
no_self_update: str no_self_update: str
api_keys: str api_key: str
prefs: str prefs: str
mods: str mods: str
version: str version: str
@@ -333,8 +327,8 @@ mod_panel = ModPanelStrings(
unhighlight_stale_tooltip="Clears highlight from stale mods", unhighlight_stale_tooltip="Clears highlight from stale mods",
highlight_stale="Highlight stale", highlight_stale="Highlight stale",
highlight_stale_tooltip=( highlight_stale_tooltip=(
"Shows locally-installed mods which are not used by any server " "Shows locally-installed mods which are not \n"
"in your Saved Servers" "used by any server in your Saved Servers"
), ),
unsub_selected="Unsubscribe selected", unsub_selected="Unsubscribe selected",
unsub_selected_tooltip="Unsubscribes from selected mods", unsub_selected_tooltip="Unsubscribes from selected mods",
@@ -378,11 +372,8 @@ thanks = Thanks(
options = Options( options = Options(
header="Options", header="Options",
steam_web="Steam API page", steam_web="Steam API page",
bm_web="Battlemetrics API page",
enter_steam="Enter your Steam API key", enter_steam="Enter your Steam API key",
enter_bm="Enter your Battlemetrics API key",
steam_placeholder="Steam API key", steam_placeholder="Steam API key",
bm_placeholder="Battlemetrics API key",
name_placeholder="Identifies you to other players in-game", name_placeholder="Identifies you to other players in-game",
last_used="Last used dimensions", last_used="Last used dimensions",
always_fs="Always fullscreen", always_fs="Always fullscreen",
@@ -411,7 +402,7 @@ options = Options(
"In-app updates are disabled when DZGUI is " "In-app updates are disabled when DZGUI is "
"installed globally (e.g., via package manager)." "installed globally (e.g., via package manager)."
), ),
api_keys="API Keys", api_key="API Key",
prefs="Preferences", prefs="Preferences",
mods="Mods", mods="Mods",
version="Version", version="Version",
@@ -549,10 +540,9 @@ connect_panel = ConnectPanel(
connect="Connect", connect="Connect",
add="Add", add="Add",
add_con="Add/connect", add_con="Add/connect",
placeholder="Enter IP (IP:Query port) or Battlemetrics ID (numerical id)", placeholder="Enter IP (IP:Query port)",
entry_tooltip=( entry_tooltip=(
"- IP: format as IP:Query port\ne.g. 192.168.1.1:27016\n" "- IP: format as IP:Query port\ne.g. 192.168.1.1:27016"
"- Battlemetrics: numeric server ID\ne.g. 123456"
), ),
add_tooltip="Add to Saved Servers", add_tooltip="Add to Saved Servers",
connect_tooltip="Connect to this server", connect_tooltip="Connect to this server",
+4 -1
View File
@@ -54,7 +54,10 @@ class OuterWindow(Gtk.Window):
MainController.register_widget("window", self) MainController.register_widget("window", self)
# NOTE: steam deck taskbar may occlude elements # NOTE: steam deck taskbar may occlude elements
if MainController.get_prefs().is_steam_deck is False: is_deck = MainController.get_prefs().is_steam_deck
if is_deck:
self.maximize()
else:
self.set_titlebar(self.hb) self.set_titlebar(self.hb)
self.connect("delete-event", self._on_delete_event) self.connect("delete-event", self._on_delete_event)
+18 -1
View File
@@ -1,4 +1,4 @@
from typing import Callable, Self, TYPE_CHECKING, Union from typing import Callable, Literal, Self, TYPE_CHECKING, Union
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
@@ -66,6 +66,15 @@ class IconButton(Gtk.Button):
# self.set_image_position(Gtk.PositionType.RIGHT) # self.set_image_position(Gtk.PositionType.RIGHT)
self.set_focus_on_click(False) self.set_focus_on_click(False)
def swap_icon(self, icon: str) -> None:
start = self.icon.get_margin_start()
end = self.icon.get_margin_end()
alt_icon = Icon(icon, margin_start=start, margin_end=end)
self.set_image(alt_icon)
def revert_icon(self) -> None:
self.set_image(self.icon)
class IconTextButton(IconButton): class IconTextButton(IconButton):
def __init__( def __init__(
@@ -96,11 +105,18 @@ class ClipboardButton(IconTextButton):
self.controller = controller self.controller = controller
self.connect("clicked", self._on_button_clicked, func) self.connect("clicked", self._on_button_clicked, func)
# TODO: strings
self.set_tooltip_text("Copy to clipboard") self.set_tooltip_text("Copy to clipboard")
def _on_button_clicked(self, button: Self, func: Callable) -> None: def _on_button_clicked(self, button: Self, func: Callable) -> None:
def revert() -> Literal[False]:
self.revert_icon()
return False
self.swap_icon("object-select-symbolic")
data = func() data = func()
copy_clipboard(data) copy_clipboard(data)
GLib.timeout_add(600, revert)
# TODO: determine when controller would be passed to this button or drop # TODO: determine when controller would be passed to this button or drop
@@ -108,6 +124,7 @@ class CopyIpButton(ClipboardButton):
def __init__(self, controller: Union["Controller", None], func: Callable) -> None: def __init__(self, controller: Union["Controller", None], func: Callable) -> None:
super().__init__(controller, func) super().__init__(controller, func)
# TODO: strings
self.set_tooltip_text("Copy IP to clipboard") self.set_tooltip_text("Copy IP to clipboard")
+5 -7
View File
@@ -26,15 +26,12 @@ def validate_port(text: str) -> bool:
return False return False
def validate_ip_or_id(text: str) -> bool: def validate_ip_truthy(text: str) -> bool:
try: try:
validate_ip(text) validate_ip(text)
return True return True
except Exception: except Exception:
if text.isdigit(): return False
return True
else:
return False
class ValidatedEntry(Gtk.Entry): class ValidatedEntry(Gtk.Entry):
@@ -124,7 +121,7 @@ class IpEntry(ValidatedEntry):
def __init__(self, controller: "Controller") -> None: def __init__(self, controller: "Controller") -> None:
super().__init__( super().__init__(
controller, controller,
func=validate_ip_or_id, func=validate_ip_truthy,
placeholder_text=connect_panel.placeholder, placeholder_text=connect_panel.placeholder,
tooltip_text=connect_panel.entry_tooltip, tooltip_text=connect_panel.entry_tooltip,
) )
@@ -162,7 +159,8 @@ class APIEntry(Gtk.Box):
self.entry.connect("icon-release", self._on_icon_release) self.entry.connect("icon-release", self._on_icon_release)
self.entry.connect("activate", self._on_field_activated) self.entry.connect("activate", self._on_field_activated)
self.submit = Gtk.Button(label="Submit") # TODO: strings
self.submit = Gtk.Button(label="Validate")
self.submit.set_sensitive(False) self.submit.set_sensitive(False)
self.submit.connect("clicked", self._on_submit) self.submit.connect("clicked", self._on_submit)
+3
View File
@@ -30,6 +30,9 @@ class InfoEventBox(Gtk.EventBox):
self.connect("leave-notify-event", self._on_leave_tooltip) self.connect("leave-notify-event", self._on_leave_tooltip)
self.add(box) self.add(box)
def set_icon_yalign(self, offset: float) -> None:
self.icon.set_property("yalign", offset)
def _on_enter_tooltip( def _on_enter_tooltip(
self, eventbox: Gtk.EventBox, eventcrossing: Gdk.EventCrossing self, eventbox: Gtk.EventBox, eventcrossing: Gdk.EventCrossing
) -> None: ) -> None:
+1
View File
@@ -131,6 +131,7 @@ class RightPanel(Gtk.Box):
return return
self.copying = True self.copying = True
version = self.version_label.get_text() version = self.version_label.get_text()
# TODO: strings
self.version_label.set_text("Copied!") self.version_label.set_text("Copied!")
copy_clipboard(version) copy_clipboard(version)
GLib.timeout_add_seconds(1, revert) GLib.timeout_add_seconds(1, revert)
+1 -1
View File
@@ -41,7 +41,7 @@ class BootDialog(Gtk.Dialog):
def __init__(self, parent: "BootWindow", xdg: "Xdg", version: str) -> None: def __init__(self, parent: "BootWindow", xdg: "Xdg", version: str) -> None:
super().__init__( super().__init__(
title=dialog_header, title=dialog_header,
parent=parent, transient_for=parent,
modal=True, modal=True,
) )
+1 -1
View File
@@ -13,7 +13,7 @@ class FilePicker(Gtk.FileChooserDialog):
super().__init__( super().__init__(
title=picker.title, title=picker.title,
action=Gtk.FileChooserAction.SAVE, action=Gtk.FileChooserAction.SAVE,
parent=parent, transient_for=parent,
resizable=True, resizable=True,
) )
self.add_buttons("_Cancel", Gtk.ResponseType.CANCEL) self.add_buttons("_Cancel", Gtk.ResponseType.CANCEL)
+9 -6
View File
@@ -111,19 +111,22 @@ class WaitDialog(GenericDialog):
self.connect("delete-event", lambda widget, event: True) self.connect("delete-event", lambda widget, event: True)
content = self.get_content_area() content = self.get_content_area()
spinner = Gtk.Spinner()
self.prog = Gtk.ProgressBar() self.prog = Gtk.ProgressBar()
content.pack_end(self.cancel, NO_EXPAND, NO_FILL, 0) spinner_box = Gtk.Box(
content.pack_end(spinner, NO_EXPAND, NO_FILL, 0) orientation=Gtk.Orientation.HORIZONTAL, halign=Gtk.Align.CENTER
)
spinner = Gtk.Spinner()
spinner_box.add(spinner)
if self.jobs > 1: if self.jobs > 1:
content.pack_end(self.prog, NO_EXPAND, NO_FILL, 0) content.pack_start(self.prog, NO_EXPAND, NO_FILL, 0)
else: else:
content.pack_start(spinner_box, NO_EXPAND, NO_FILL, 0)
spinner.start() spinner.start()
if show_cancel is False: if show_cancel is True:
self.connect("realize", lambda _: self.cancel.set_visible(False)) content.pack_start(self.cancel, NO_EXPAND, NO_FILL, 0)
def update_text(self, msg: str) -> None: def update_text(self, msg: str) -> None:
self.format_secondary_text(msg) self.format_secondary_text(msg)
+35 -54
View File
@@ -6,7 +6,7 @@ from importlib import resources
from pathlib import Path from pathlib import Path
from typing import Any, Callable, Self, TYPE_CHECKING from typing import Any, Callable, Self, TYPE_CHECKING
from dzgui.api.probe import test_steam_api, test_bm_api from dzgui.api.probe import test_steam_api
from dzgui.api.shortcuts import add_steam_shortcut from dzgui.api.shortcuts import add_steam_shortcut
from dzgui.api.steam import get_steam_paths from dzgui.api.steam import get_steam_paths
from dzgui.const.constants import ( from dzgui.const.constants import (
@@ -16,7 +16,7 @@ from dzgui.const.constants import (
LEGACY_CONFIG_PATH, LEGACY_CONFIG_PATH,
) )
from dzgui.const.boilerplate import config_boilerplate from dzgui.const.boilerplate import config_boilerplate
from dzgui.const.endpoints import BM_API_SETUP, STEAM_API_SETUP from dzgui.const.endpoints import STEAM_API_SETUP
from dzgui.const.enum import Preferences from dzgui.const.enum import Preferences
from dzgui.config import freedesktop from dzgui.config import freedesktop
from dzgui.config.query import lookup from dzgui.config.query import lookup
@@ -44,10 +44,16 @@ class PageNum(Enum):
HAS_CONFIG = 2 HAS_CONFIG = 2
STEAM_PATH = 3 STEAM_PATH = 3
STEAM_API = 4 STEAM_API = 4
BM_API = 5 USER_PREFS = 5
USER_PREFS = 6 SHORTCUTS = 6
SHORTCUTS = 7 FINAL = 7
FINAL = 8
class OptionalPageMixin:
"""Marks optional pages as advanceable"""
def _on_map(self, page: "ScrolledWizardPage") -> None:
EMITTER.emit("step_complete")
class DescriptionArea(Gtk.Box): class DescriptionArea(Gtk.Box):
@@ -91,6 +97,7 @@ class ScrolledWizardPage(Gtk.ScrolledWindow):
margin_top=50, margin_top=50,
spacing=20, spacing=20,
) )
self.add(self.box) self.add(self.box)
self.prog = Progress() self.prog = Progress()
self.box.pack_end(self.prog, expand=False, fill=False, padding=0) self.box.pack_end(self.prog, expand=False, fill=False, padding=0)
@@ -215,23 +222,6 @@ class APIValidationPage(ScrolledWizardPage):
self.spinner.stop() self.spinner.stop()
class BMValidationPage(APIValidationPage):
def __init__(self) -> None:
super().__init__(
enum=PageNum.BM_API,
heading=wizard.heading_bm_api,
description=wizard.blurb_bm_api,
link=BM_API_SETUP,
func=self._validate,
)
@call_on_thread("", show_dialog=False)
def _validate(self, key: str) -> None:
is_valid = test_bm_api(key.strip())
cleanup = StoredFunc(self._cleanup, is_valid, key)
self.thread_man.set_cleanup_func(cleanup)
class SteamValidationPage(APIValidationPage): class SteamValidationPage(APIValidationPage):
def __init__(self) -> None: def __init__(self) -> None:
super().__init__( super().__init__(
@@ -445,6 +435,7 @@ class Assistant(Gtk.Assistant):
else: else:
self.set_default_size(1500, 900) self.set_default_size(1500, 900)
self.is_binary = False if os.getenv("PYAPP") is None else True
self.config_path = XDG.config self.config_path = XDG.config
self.config_values: dict[str, Any] = config_boilerplate self.config_values: dict[str, Any] = config_boilerplate
@@ -455,10 +446,9 @@ class Assistant(Gtk.Assistant):
self.page2 = ConfigMigrationPage(XDG.config) self.page2 = ConfigMigrationPage(XDG.config)
self.page3 = SteamPathPage() self.page3 = SteamPathPage()
self.page4 = SteamValidationPage() self.page4 = SteamValidationPage()
self.page5 = BMValidationPage() self.page5 = PreferencesPage()
self.page6 = PreferencesPage() self.page6 = ShortcutCreationPage(XDG.shortcut)
self.page7 = ShortcutCreationPage(XDG.shortcut) self.page7 = CompletionPage()
self.page8 = CompletionPage()
self.set_forward_page_func(self._advance_page) self.set_forward_page_func(self._advance_page)
@@ -476,7 +466,6 @@ class Assistant(Gtk.Assistant):
self.page5, self.page5,
self.page6, self.page6,
self.page7, self.page7,
self.page8,
): ):
# NOTE: skip config migration page if no legacy config file # NOTE: skip config migration page if no legacy config file
if ( if (
@@ -485,7 +474,7 @@ class Assistant(Gtk.Assistant):
): ):
continue continue
# NOTE: disabled for now on system-provided packages # NOTE: disabled for now on system-provided packages
if isinstance(page, ShortcutCreationPage) and os.getenv("PYAPP") is None: if isinstance(page, ShortcutCreationPage) and not self.is_binary:
continue continue
self._add_page(page, page.get_page_type()) self._add_page(page, page.get_page_type())
@@ -500,32 +489,31 @@ 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/isinstance
match page: match page:
case self.page1: case IntroductionPage():
pass pass
case self.page2: case ConfigMigrationPage():
if self.page2.is_migrated(): if page.is_migrated():
steam_path = lookup(self.config_path, Preferences.DEFAULT) steam_path = lookup(self.config_path, Preferences.DEFAULT)
self.page7.set_steam_path(steam_path) self.page6.set_steam_path(steam_path)
return self.get_n_pages() - 2 offset = 1 if not self.is_binary else 2
case self.page3: self.setup_complete = True
return self.get_n_pages() - offset
case SteamPathPage():
self.config_values["default_steam_path"] = page.get_path_from_radio() self.config_values["default_steam_path"] = page.get_path_from_radio()
case self.page4: case SteamValidationPage():
self.config_values["steam_api"] = page.get_api_key() self.config_values["steam_api"] = page.get_api_key()
case self.page5: case PreferencesPage():
self.config_values["bm_api"] = page.get_api_key() # NOTE: collects config values before advancing to last page
# NOTE: collects config values before advancing to last page name, use_miles, client = self.page5.get_prefs()
case self.page6:
name, use_miles, client = self.page6.get_prefs()
self.config_values["name"] = name self.config_values["name"] = name
self.config_values["use_miles"] = use_miles self.config_values["use_miles"] = use_miles
self.config_values["client"] = client self.config_values["client"] = client
self.write_config() self.write_config()
self.page7.set_steam_path(self.config_values["default_steam_path"]) self.page6.set_steam_path(self.config_values["default_steam_path"])
case self.page7:
self.page7.create_shortcuts()
self.setup_complete = True self.setup_complete = True
case ShortcutCreationPage():
page.create_shortcuts()
case _: case _:
raise AttributeError("Trying to advance a non-canonical page") raise AttributeError("Trying to advance a non-canonical page")
return index + 1 return index + 1
@@ -569,11 +557,7 @@ class Assistant(Gtk.Assistant):
bar.set_fraction(fraction) bar.set_fraction(fraction)
bar.set_text(f"{page_num}/{total}") bar.set_text(f"{page_num}/{total}")
# NOTE: disable forward action if not isinstance(page, IntroductionPage):
# TODO: use page enums
if page == self.page5:
return
if page != self.page1:
EMITTER.emit("step_pending") EMITTER.emit("step_pending")
@@ -600,7 +584,7 @@ class CheckboxWithLabel(Gtk.Box):
self.button.set_active(state) self.button.set_active(state)
class ShortcutCreationPage(ScrolledWizardPage): class ShortcutCreationPage(OptionalPageMixin, ScrolledWizardPage): # type: ignore
def __init__(self, shortcut: Path) -> None: def __init__(self, shortcut: Path) -> None:
super().__init__( super().__init__(
enum=PageNum.SHORTCUTS, enum=PageNum.SHORTCUTS,
@@ -643,9 +627,6 @@ class ShortcutCreationPage(ScrolledWizardPage):
self.desktop_checkbox.set_active(state) self.desktop_checkbox.set_active(state)
self.desktop_checkbox.set_sensitive(state) self.desktop_checkbox.set_sensitive(state)
def _on_map(self, page: "ScrolledWizardPage") -> None:
EMITTER.emit("step_complete")
def set_steam_path(self, path: Path) -> None: def set_steam_path(self, path: Path) -> None:
self.steam_path = path self.steam_path = path
+5 -1
View File
@@ -37,7 +37,11 @@ class CursorMixin:
end = len(model) - 1 end = len(model) - 1
else: else:
return False return False
cur_row = self.get_focused_row_index() # type: ignore
try:
cur_row = self.get_focused_row_index() # type: ignore
except Exception:
return False
if position == CursorPosition.DOWN: if position == CursorPosition.DOWN:
if cur_row == end: if cur_row == end:
+20 -4
View File
@@ -33,7 +33,9 @@ class Changelog(HelpMenuMixin, ScrollableMixin, Gtk.ScrolledWindow): # type: ig
# TODO: should long text be wrapped? # TODO: should long text be wrapped?
self.controller = controller self.controller = controller
self.box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5, margin_top=10) self.box = Gtk.Box(
orientation=Gtk.Orientation.VERTICAL, spacing=5, margin_top=10
)
self.add(self.box) self.add(self.box)
self.connect("key-press-event", self._on_keypress) self.connect("key-press-event", self._on_keypress)
@@ -55,7 +57,6 @@ class Changelog(HelpMenuMixin, ScrollableMixin, Gtk.ScrolledWindow): # type: ig
return version, date return version, date
raise ValueError("Changelog parse error") raise ValueError("Changelog parse error")
def _generate_nodes(self, releases: list[tuple[str, list[str]]]) -> None: def _generate_nodes(self, releases: list[tuple[str, list[str]]]) -> None:
for header, changes in releases: for header, changes in releases:
for ind in (0, -1): for ind in (0, -1):
@@ -64,8 +65,12 @@ class Changelog(HelpMenuMixin, ScrollableMixin, Gtk.ScrolledWindow): # type: ig
text = "\n".join(changes) text = "\n".join(changes)
formatted = format_pango(text) formatted = format_pango(text)
label = Gtk.Label(valign=Gtk.Align.START, margin=15, halign=Gtk.Align.START) container = Gtk.Box(
valign=Gtk.Align.START, halign=Gtk.Align.START
)
label = Gtk.Label()
label.set_markup(formatted) label.set_markup(formatted)
container.add(label)
version, date = self._parse_version(header) version, date = self._parse_version(header)
button_text = f"{version} ({date})" button_text = f"{version} ({date})"
@@ -73,9 +78,20 @@ class Changelog(HelpMenuMixin, ScrollableMixin, Gtk.ScrolledWindow): # type: ig
expander = Gtk.Expander() expander = Gtk.Expander()
expander.set_label_widget(button) expander.set_label_widget(button)
expander.add(label) expander.add(container)
expander.connect("activate", self._on_expand, container)
self.box.add(expander) self.box.add(expander)
def _on_expand(self, expander: Gtk.Box, container: Gtk.Box) -> None:
"""
Wayland/some themes do not expect widgets to have a size allocation until expanded.
Trying to set margins a priori may cause negative allocation,
as the margins are subtracted from zero. In effect, the widget is not realized
until it is expanded for the first time.
"""
container.set_margin_start(15)
container.set_margin_top(15)
container.set_margin_bottom(15)
def _parse(self, changelog: str) -> list[tuple[str, list[str]]]: def _parse(self, changelog: str) -> list[tuple[str, list[str]]]:
release = "" release = ""
+1 -1
View File
@@ -103,4 +103,4 @@ class Keybindings(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore
return grid return grid
def grab_content_area(self) -> None: def grab_content_area(self) -> None:
self.box.grab_focus() self.grab_focus()
+17 -4
View File
@@ -41,9 +41,18 @@ class FolderError(Enum):
FOLDER_CHANGED = 3 FOLDER_CHANGED = 3
class PageHeading(Gtk.Label): class PageHeading(Gtk.Box):
def __init__(self, label: str) -> None: def __init__(self, label: str, controller: "Controller") -> None:
super().__init__(label=label, halign=Gtk.Align.CENTER) super().__init__(
halign=Gtk.Align.CENTER, orientation=Gtk.Orientation.HORIZONTAL
)
self.label = Gtk.Label(label=label)
self.eb = InfoEventBox(offline.heading_disclaimer, controller)
self.eb.set_icon_yalign(0.7)
for el in self.label, self.eb:
self.add(el)
css.add_class(self, "page-heading") css.add_class(self, "page-heading")
@@ -434,7 +443,8 @@ class OfflineLoader(Gtk.Box):
self.emitter = controller.get_emitter() self.emitter = controller.get_emitter()
self.offline_man = OfflineManager(controller) self.offline_man = OfflineManager(controller)
self.add(PageHeading(offline.heading)) heading = PageHeading(offline.heading, controller)
self.add(heading)
self.local_frame = ModFrame(self, controller, offline.local_frame) self.local_frame = ModFrame(self, controller, offline.local_frame)
self.custom_frame = CustomModFrame(self, controller, offline.custom_frame) self.custom_frame = CustomModFrame(self, controller, offline.custom_frame)
@@ -511,3 +521,6 @@ class OfflineLoader(Gtk.Box):
self.mission_frame.present_folder_changed(mission) self.mission_frame.present_folder_changed(mission)
return return
self.offline_man.launch(appid, mission, local_mods, custom_folder, custom_mods) self.offline_man.launch(appid, mission, local_mods, custom_folder, custom_mods)
def grab_content_area(self) -> None:
self.local_frame.tree.grab_focus()
+21 -51
View File
@@ -15,16 +15,18 @@ from dzgui.const.constants import (
VIEW_CONCEAL, VIEW_CONCEAL,
VIEW_REVEAL, VIEW_REVEAL,
) )
from dzgui.const.endpoints import STEAM_API_SETUP, BM_API_SETUP from dzgui.const.endpoints import STEAM_API_SETUP
from dzgui.const.enum import Preferences, ServerTab from dzgui.const.enum import Preferences, ServerTab
from dzgui.strings import errors, options from dzgui.strings import errors, options
from dzgui.util import strings, css, open_links from dzgui.util import strings, css, open_links
from dzgui.views.components.box import VBox
from dzgui.views.components.labels import LeftLabel from dzgui.views.components.labels import LeftLabel
from dzgui.views.components.buttons import WebButton from dzgui.views.components.buttons import WebButton
from dzgui.views.components.frame import HeadingFrame from dzgui.views.components.frame import HeadingFrame
from dzgui.views.components.misc import ClientCombo from dzgui.views.components.misc import ClientCombo
from dzgui.views.dialogs.generic import ExceptionDialog from dzgui.views.dialogs.generic import ExceptionDialog
from dzgui.views.mixins.scrollable_mixin import ScrollableMixin
import gi import gi
@@ -44,10 +46,9 @@ class ShortHBox(Gtk.Box):
self.pack_start(widget, NO_EXPAND, NO_FILL, NO_PADDING) self.pack_start(widget, NO_EXPAND, NO_FILL, NO_PADDING)
class Options(Gtk.Box): class Options(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore
def __init__(self, controller: "Controller"): def __init__(self, controller: "Controller"):
super().__init__( super().__init__(
orientation=Gtk.Orientation.VERTICAL,
margin_start=10, margin_start=10,
margin_end=10, margin_end=10,
) )
@@ -60,29 +61,16 @@ class Options(Gtk.Box):
self.DEFAULT_WIDTH = 1 self.DEFAULT_WIDTH = 1
self.DEFAULT_HEIGHT = 1 self.DEFAULT_HEIGHT = 1
label = Gtk.Label(label=strings.options.header)
label.set_halign(Gtk.Align.CENTER)
css.add_class(label, "page-heading")
self.add(label)
self.steam_entry: Gtk.Entry self.steam_entry: Gtk.Entry
self.bm_entry: Gtk.Entry
self.steam = WebButton(label=strings.options.steam_web) self.steam = WebButton(label=strings.options.steam_web)
self.steam.connect("clicked", self._on_link_button_clicked, STEAM_API_SETUP) self.steam.connect("clicked", self._on_link_button_clicked, STEAM_API_SETUP)
self.bm = WebButton(label=strings.options.bm_web)
self.bm.connect("clicked", self._on_link_button_clicked, BM_API_SETUP)
self.steam_box = self._make_submit_field( self.steam_box = self._make_submit_field(
strings.options.enter_steam, Preferences.STEAM, True strings.options.enter_steam, Preferences.STEAM, True
) )
self.bm_box = self._make_submit_field(
strings.options.enter_bm, Preferences.BM, True
)
api_rows = [ api_rows = [
[LeftLabel(strings.options.steam_placeholder), self.steam_box], [LeftLabel(strings.options.steam_placeholder), self.steam_box, self.steam],
[LeftLabel(strings.options.bm_placeholder), self.bm_box],
] ]
self.player_box = self._make_submit_field( self.player_box = self._make_submit_field(
@@ -144,18 +132,6 @@ class Options(Gtk.Box):
api_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) api_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
api_grid = self._make_grid(api_rows) api_grid = self._make_grid(api_rows)
api_box.add(api_grid) api_box.add(api_grid)
api_links_box = Gtk.Box(
orientation=Gtk.Orientation.HORIZONTAL,
margin_start=5,
margin_end=5,
margin_top=5,
margin_bottom=10,
homogeneous=True,
spacing=10,
)
api_links_box.add(self.steam)
api_links_box.add(self.bm)
api_box.add(api_links_box)
prefs_grid = self._make_grid(pref_rows) prefs_grid = self._make_grid(pref_rows)
version_grid = self._make_grid(version_rows) version_grid = self._make_grid(version_rows)
@@ -176,7 +152,7 @@ class Options(Gtk.Box):
grid.attach(developers, 1, 0, self.DEFAULT_WIDTH, self.DEFAULT_HEIGHT) grid.attach(developers, 1, 0, self.DEFAULT_WIDTH, self.DEFAULT_HEIGHT)
for pair in [ for pair in [
(api_box, strings.options.api_keys), (api_box, strings.options.api_key),
(prefs_grid, strings.options.prefs), (prefs_grid, strings.options.prefs),
(version_grid, strings.options.version), (version_grid, strings.options.version),
]: ]:
@@ -185,9 +161,16 @@ class Options(Gtk.Box):
grid.attach(frame, col, row, self.DEFAULT_WIDTH, self.DEFAULT_HEIGHT) grid.attach(frame, col, row, self.DEFAULT_WIDTH, self.DEFAULT_HEIGHT)
row += 1 row += 1
self.scrollable = Gtk.ScrolledWindow(vexpand=True) label = Gtk.Label(label=strings.options.header)
self.scrollable.add(grid) label.set_halign(Gtk.Align.CENTER)
self.add(self.scrollable) css.add_class(label, "page-heading")
box = VBox()
box.add(label)
box.add(grid)
self.add(box)
self.connect("key-press-event", self._on_keypress)
def get_client_name(self) -> str: def get_client_name(self) -> str:
model = self.client_combo.get_model() model = self.client_combo.get_model()
@@ -195,13 +178,11 @@ class Options(Gtk.Box):
return str(model[ind][0]) return str(model[ind][0])
def block_text_entry(self) -> None: def block_text_entry(self) -> None:
for entry in self.steam_entry, self.bm_entry: self.steam_entry.set_position(-1)
entry.set_position(-1) self.steam_entry.set_can_focus(False)
entry.set_can_focus(False)
def unblock_text_entry(self) -> None: def unblock_text_entry(self) -> None:
for entry in self.steam_entry, self.bm_entry: self.steam_entry.set_can_focus(True)
entry.set_can_focus(True)
def _on_developers_clicked(self, button: Gtk.Button) -> None: def _on_developers_clicked(self, button: Gtk.Button) -> None:
self.controller.show_developers_page() self.controller.show_developers_page()
@@ -234,8 +215,6 @@ class Options(Gtk.Box):
if context == Preferences.STEAM: if context == Preferences.STEAM:
self.steam_entry = entry self.steam_entry = entry
else:
self.bm_entry = entry
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10) box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
box.add(entry) box.add(entry)
@@ -282,7 +261,7 @@ class Options(Gtk.Box):
case Preferences.NAME: case Preferences.NAME:
value = entry.get_text().strip() value = entry.get_text().strip()
self.controller.update_config(enum, value) self.controller.update_config(enum, value)
case Preferences.BM | Preferences.STEAM: case Preferences.STEAM:
text = "".join(entry.get_text().split()) text = "".join(entry.get_text().split())
self.controller.update_api_key(enum, text) self.controller.update_api_key(enum, text)
@@ -298,9 +277,6 @@ class Options(Gtk.Box):
def revert(self, mode: Preferences) -> None: def revert(self, mode: Preferences) -> None:
if mode == Preferences.STEAM: if mode == Preferences.STEAM:
self.steam_entry.set_text(self.old_steam) self.steam_entry.set_text(self.old_steam)
else:
self.bm_entry.set_text(self.old_bm)
pass
def _on_start_tab_changed(self, combo: Gtk.ComboBoxText) -> None: def _on_start_tab_changed(self, combo: Gtk.ComboBoxText) -> None:
_iter = combo.get_active_iter() _iter = combo.get_active_iter()
@@ -336,8 +312,6 @@ class Options(Gtk.Box):
old = self.old_name old = self.old_name
case Preferences.STEAM: case Preferences.STEAM:
old = self.old_steam old = self.old_steam
case Preferences.BM:
old = self.old_bm
if text == old: if text == old:
return False return False
return True return True
@@ -401,16 +375,13 @@ class Options(Gtk.Box):
name = self.controller.query_config(Preferences.NAME) name = self.controller.query_config(Preferences.NAME)
default_steam_path = self.controller.query_config(Preferences.DEFAULT) default_steam_path = self.controller.query_config(Preferences.DEFAULT)
steam = self.controller.query_config(Preferences.STEAM) steam = self.controller.query_config(Preferences.STEAM)
bm = self.controller.query_config(Preferences.BM)
steam_path = Path(default_steam_path) steam_path = Path(default_steam_path)
self.old_steam = steam self.old_steam = steam
self.old_bm = bm
self.old_name = name self.old_name = name
self.steam_entry.set_text(steam) self.steam_entry.set_text(steam)
self.bm_entry.set_text(bm)
p = self.player_box.get_children()[0] p = self.player_box.get_children()[0]
if hasattr(p, "set_text"): if hasattr(p, "set_text"):
p.set_text(name) p.set_text(name)
@@ -428,7 +399,6 @@ class Options(Gtk.Box):
for field in ( for field in (
[name, self.player_box], [name, self.player_box],
[steam, self.steam_box], [steam, self.steam_box],
[bm, self.bm_box],
): ):
if field[0] == "": if field[0] == "":
field[1].get_children()[1].set_sensitive(False) field[1].get_children()[1].set_sensitive(False)
@@ -475,4 +445,4 @@ class Options(Gtk.Box):
widget.set_visibility(state) widget.set_visibility(state)
def grab_content_area(self) -> None: def grab_content_area(self) -> None:
return self.grab_focus()
+11 -2
View File
@@ -239,6 +239,15 @@ class PreConnectionAssistant(Gtk.Box):
) )
"""Errors""" """Errors"""
if len(prereqs.invalid_mods) > 0:
pairs = [": ".join(sub) for sub in prereqs.invalid_mods]
lines = "\n".join(pairs)
msg = (
"Server has invalid mods that are not recognized by Steam.\n"
"Contact the server owner and include these mod IDs in your report:\n"
f"{lines}"
)
errors.append(msg)
if prereqs.binary_missing: if prereqs.binary_missing:
errors.append( errors.append(
f"Remote server is running the build '{prereqs.build}', but it is not installed.\n{resync_msg}" f"Remote server is running the build '{prereqs.build}', but it is not installed.\n{resync_msg}"
@@ -275,10 +284,10 @@ class PreConnectionAssistant(Gtk.Box):
allows_dl, running_app = prereqs.allows_downloads allows_dl, running_app = prereqs.allows_downloads
if len(prereqs.mods) > 0 and allows_dl is False: if len(prereqs.mods) > 0 and allows_dl is False:
msg = ( msg = (
f"The game '{running_app}' is currently running in Steam, but background downloads are not enabled.\n" f"The app '{running_app}' is currently running in Steam, but background downloads are not enabled.\n"
"Either stop the game first, or update your global Steam settings or the game's local settings.\n" "Either stop the game first, or update your global Steam settings or the game's local settings.\n"
"Otherwise, mods may be queued for download but never update." "Otherwise, mods may be queued for download but never update."
) )
warnings.append(msg) warnings.append(msg)
self.add_warnings(warnings) self.add_warnings(warnings)
+2 -3
View File
@@ -1,7 +1,6 @@
import logging import logging
from typing import Any, Optional, TYPE_CHECKING from typing import Any, Optional, TYPE_CHECKING
from warnings import deprecated
from dzgui.const.constants import APP_NAME, SEPARATOR from dzgui.const.constants import APP_NAME, SEPARATOR
from dzgui.util.keys import is_ctrl_mask, is_navkey from dzgui.util.keys import is_ctrl_mask, is_navkey
@@ -173,7 +172,7 @@ class TreeView(CursorMixin, Gtk.TreeView): # type: ignore
(model, pathlist) = sels (model, pathlist) = sels
return (model, pathlist) return (model, pathlist)
@deprecated("Currently unused") # @deprecated("Currently unused")
# def get_mpath(self) -> Optional[Gtk.TreePath]: # def get_mpath(self) -> Optional[Gtk.TreePath]:
# (model, pathlist) = self.get_model_and_pathlist() # (model, pathlist) = self.get_model_and_pathlist()
# if len(pathlist) < 1: # if len(pathlist) < 1:
@@ -195,7 +194,7 @@ class TreeView(CursorMixin, Gtk.TreeView): # type: ignore
return True return True
return False return False
@deprecated("unused") # @deprecated("unused")
# def get_selected_row(self) -> Optional[Gtk.TreeModelRow]: # def get_selected_row(self) -> Optional[Gtk.TreeModelRow]:
# ind = self.get_selected_row_index() # ind = self.get_selected_row_index()
# model = self.get_model() # model = self.get_model()
+1 -1
View File
@@ -53,7 +53,7 @@ class LogTreeView(ContextMixin, TreeView): # type: ignore
model = ModelFactory().new_model_from_logfile(filepath) model = ModelFactory().new_model_from_logfile(filepath)
_filter = model.filter_new() _filter = model.filter_new()
_filter.set_visible_func(self._filter_rows) _filter.set_visible_func(self._filter_rows)
sortable = Gtk.TreeModelSort(_filter) sortable = Gtk.TreeModelSort.new_with_model(_filter)
self.set_model(sortable) self.set_model(sortable)
_filter.refilter() _filter.refilter()
path = Gtk.TreePath.new_from_indices([0]) path = Gtk.TreePath.new_from_indices([0])
+4 -2
View File
@@ -4,7 +4,7 @@ description = "DayZ server browser and mod manager for Linux"
authors = [ authors = [
{name = "aclist"} {name = "aclist"}
] ]
version = "7.0.0b19" version = "7.0.0b21"
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
license-files = ["LICENSE"] license-files = ["LICENSE"]
readme = "README.md" readme = "README.md"
@@ -107,11 +107,13 @@ config-settings-package = { pygobject-stubs = { config = "Gtk3,Gdk3,Soup2" } }
[tool.pytest.ini_options] [tool.pytest.ini_options]
markers = [ markers = [
"apitest: checks remote endpoints",
"config: config file keys/values", "config: config file keys/values",
"realconfig: depends on actual generated local config file",
"mods: tests mod metadata/link creation", "mods: tests mod metadata/link creation",
"pefile: validate PE files", "pefile: validate PE files",
"post_install: requires a completed installation", "post_install: requires a completed installation",
"redact: log redaction mechanisms", "redact: log redaction mechanisms",
"slow: long-running tests", "slow: long-running tests",
"webtest: checks remote endpoints" "webtest: depends on remote endpoint",
] ]
+6 -4
View File
@@ -7,13 +7,12 @@ from dzgui.config.xdg import get_xdg_paths, parse_filepaths
from dzgui.config import convert from dzgui.config import convert
from tests.fixtures import fixture_path from tests.fixtures import fixture_path
pytestmark = pytest.mark.config
@pytest.fixture @pytest.fixture
def legacy_config(): def legacy_config():
return fixture_path("dztuirc_1") return fixture_path("dztuirc_1")
@pytest.fixture @pytest.fixture
def unset_values(): def unset_values():
return fixture_path("dztuirc_3") return fixture_path("dztuirc_3")
@@ -22,7 +21,6 @@ def unset_values():
@pytest.fixture @pytest.fixture
def keys(): def keys():
return [ return [
"bm_api",
"fav_server", "fav_server",
"fav_label", "fav_label",
"name", "name",
@@ -38,6 +36,7 @@ def keys():
# TODO: use a static fixture instead of system config # TODO: use a static fixture instead of system config
@pytest.fixture @pytest.fixture
@pytest.mark.realconfig
def config(): def config():
paths = get_xdg_paths() paths = get_xdg_paths()
xdg = parse_filepaths(paths) xdg = parse_filepaths(paths)
@@ -45,18 +44,21 @@ def config():
@pytest.mark.post_install @pytest.mark.post_install
@pytest.mark.realconfig
def test_invalid_config_value(config): def test_invalid_config_value(config):
with pytest.raises(Exception): with pytest.raises(Exception):
assert config["foo"] is None assert config["foo"] is None
@pytest.mark.post_install @pytest.mark.post_install
@pytest.mark.realconfig
def test_default_config_values(keys, config): def test_default_config_values(keys, config):
for key in keys: for key in keys:
assert config[key] is not None assert config[key] is not None
@pytest.mark.post_install @pytest.mark.post_install
@pytest.mark.realconfig
def test_contains_invalid_values(keys, config): def test_contains_invalid_values(keys, config):
for key in config: for key in config:
assert key in keys assert key in keys
@@ -81,7 +83,6 @@ def test_key_conversion(legacy_config):
j = convert.rc2json(legacy_config) j = convert.rc2json(legacy_config)
j = json.loads(j) j = json.loads(j)
keys = [ keys = [
"api_key",
"staging_dir", "staging_dir",
"src_path", "src_path",
"steam_path", "steam_path",
@@ -91,6 +92,7 @@ def test_key_conversion(legacy_config):
for key in keys: for key in keys:
assert key not in j assert key not in j
def test_missing_values(unset_values): def test_missing_values(unset_values):
j = convert.rc2json(unset_values) j = convert.rc2json(unset_values)
j = json.loads(j) j = json.loads(j)
+20
View File
@@ -1,6 +1,24 @@
import pytest import pytest
from dzgui.api.servers import validate_ip from dzgui.api.servers import validate_ip
from dzgui.views.components.entry import validate_ip_truthy
@pytest.mark.parametrize(
"ip",
[
("foo", False),
("999", False),
("192.168.1.101", False),
("192.168.1.101:", False),
(":1", False),
("192.168.1.101:27016", True),
],
)
def test_ip_entry(ip) -> None:
string, state = ip
assert validate_ip_truthy(string) is state
def test_ip_validation() -> None: def test_ip_validation() -> None:
ip = "192.168.1.1:100" ip = "192.168.1.1:100"
@@ -15,6 +33,7 @@ def test_invalid_port() -> None:
with pytest.raises(Exception): with pytest.raises(Exception):
validate_ip(ip) validate_ip(ip)
@pytest.mark.parametrize("port", [-1, 65536]) @pytest.mark.parametrize("port", [-1, 65536])
def test_port_out_of_range(port: int) -> None: def test_port_out_of_range(port: int) -> None:
ip = f"192.168.1.1:{port}" ip = f"192.168.1.1:{port}"
@@ -27,6 +46,7 @@ def test_invalid_socket() -> None:
with pytest.raises(Exception): with pytest.raises(Exception):
validate_ip(ip) validate_ip(ip)
# TODO: ? # TODO: ?
def test_ipdb() -> None: def test_ipdb() -> None:
pass pass
+8
View File
@@ -37,6 +37,14 @@ class RecordsListHandler(logging.Handler):
"Error in directory: '/home/SENSITIVE_USERNAME'", "Error in directory: '/home/SENSITIVE_USERNAME'",
"Error in directory: '/home/REDACTED'", "Error in directory: '/home/REDACTED'",
), ),
(
"User directory: /home/user/.local/share/Steam/userdata/999999/grid",
"User directory: /home/REDACTED/.local/share/Steam/userdata/REDACTED/grid",
),
(
"User directory: /drive/.local/share/Steam/userdata/999999",
"User directory: /drive/.local/share/Steam/userdata/REDACTED",
),
], ],
) )
def test_log_redaction(log_error: str, expect: str) -> None: def test_log_redaction(log_error: str, expect: str) -> None:
+2 -4
View File
@@ -13,13 +13,11 @@ def config():
xdg = parse_filepaths(paths) xdg = parse_filepaths(paths)
return get_config(xdg.config) return get_config(xdg.config)
def test_ipdb(): def test_ipdb():
assert probe.test_ipdb() assert probe.test_ipdb()
def test_steam(config): def test_steam(config):
key = config["steam_api"] key = config["steam_api"]
assert probe.test_steam_api(key) assert probe.test_steam_api(key)
def test_bm(config):
key = config["bm_api"]
assert probe.test_bm_api(key)
+27 -3
View File
@@ -38,14 +38,14 @@ def dummy_app() -> None:
return d return d
def test_wrap_exe(dummy_app) -> None: def test_wrap_exe(dummy_app: dict[str, str]) -> None:
s = Shortcuts(Path("")) s = Shortcuts(Path(""))
s.add_shortcut(*dummy_app.values()) s.add_shortcut(*dummy_app.values())
assert s.shortcuts["shortcuts"]["0"]["Exe"][0] == '"' assert s.shortcuts["shortcuts"]["0"]["Exe"][0] == '"'
assert s.shortcuts["shortcuts"]["0"]["Exe"][-1] == '"' assert s.shortcuts["shortcuts"]["0"]["Exe"][-1] == '"'
def test_add_shortcut(dummy_app) -> None: def test_add_shortcut(dummy_app: dict[str, str]) -> None:
s = Shortcuts(Path("")) s = Shortcuts(Path(""))
s.add_shortcut(*dummy_app.values()) s.add_shortcut(*dummy_app.values())
new = s.shortcuts["shortcuts"] new = s.shortcuts["shortcuts"]
@@ -56,7 +56,7 @@ def test_add_shortcut(dummy_app) -> None:
assert new[ind][k] == v assert new[ind][k] == v
def test_save_shortcut(dummy_app) -> None: def test_save_shortcut(dummy_app: dict[str, str]) -> None:
s = Shortcuts(Path("")) s = Shortcuts(Path(""))
s.add_shortcut(*dummy_app.values()) s.add_shortcut(*dummy_app.values())
with tempfile.NamedTemporaryFile() as f: with tempfile.NamedTemporaryFile() as f:
@@ -65,3 +65,27 @@ def test_save_shortcut(dummy_app) -> None:
s.save_shortcuts() s.save_shortcuts()
s._load_shortcuts(tmp) s._load_shortcuts(tmp)
assert len(s.shortcuts["shortcuts"]) == 1 assert len(s.shortcuts["shortcuts"]) == 1
def test_shortcut_crc(dummy_app: dict[str, str]) -> None:
s = Shortcuts(Path(""))
s.add_shortcut(*dummy_app.values())
for key in s.shortcuts["shortcuts"].keys():
entry = s.shortcuts["shortcuts"][key]
name = entry["AppName"]
exe = entry["Exe"]
uid = name + exe
bpid = s.gen_bpid(uid)
assert entry["appid"] & 0xFFFFFFFF == bpid
def test_reverse_crc(dummy_app: dict[str, str]) -> None:
s = Shortcuts(Path(""))
s.add_shortcut(*dummy_app.values())
for key in s.shortcuts["shortcuts"].keys():
entry = s.shortcuts["shortcuts"][key]
name = entry["AppName"]
exe = entry["Exe"]
uid = name + exe
bpid = s.gen_bpid(uid)
assert s.find_appname_by_unsigned_id(bpid) == name