mirror of
https://github.com/aclist/dztui.git
synced 2026-11-10 12:00:38 +01:00
Compare commits
82
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c4026daf8 | ||
|
|
e03c6905c6 | ||
|
|
8fb3462f35 | ||
|
|
bfe9b9d147 | ||
|
|
9f36569fd4 | ||
|
|
da18b41136 | ||
|
|
f092207c61 | ||
|
|
15d4b71350 | ||
|
|
16d3df618a | ||
|
|
22dd8fc67f | ||
|
|
03fa707305 | ||
|
|
2479cbd41b | ||
|
|
370edbe01b | ||
|
|
0c41654645 | ||
|
|
903f8fc31d | ||
|
|
6b583f68d6 | ||
|
|
3b3ccd0de6 | ||
|
|
70aacaa866 | ||
|
|
7582ab4d3b | ||
|
|
225a2a148e | ||
|
|
9117655ebd | ||
|
|
fdf92dca40 | ||
|
|
a97766855d | ||
|
|
3097701e98 | ||
|
|
895e08d228 | ||
|
|
08a6f698a5 | ||
|
|
78be8c93b7 | ||
|
|
8a2889d15d | ||
|
|
1a33f71716 | ||
|
|
ae13dc9a89 | ||
|
|
d5706f969c | ||
|
|
790e34b20c | ||
|
|
35230ae03f | ||
|
|
596d3a59ac | ||
|
|
e7cef215c6 | ||
|
|
12daf041ff | ||
|
|
90ef6cbdbb | ||
|
|
2cc26b6573 | ||
|
|
0ec1c157f4 | ||
|
|
0cf6439538 | ||
|
|
e3c1ef59ee | ||
|
|
78144e9127 | ||
|
|
33b8dfba70 | ||
|
|
abf0060cac | ||
|
|
95daa9163b | ||
|
|
ff068e5396 | ||
|
|
80680b86b0 | ||
|
|
a4ac357c8f | ||
|
|
728b8b8242 | ||
|
|
3b51392d39 | ||
|
|
159e2da355 | ||
|
|
b83a333cea | ||
|
|
46117ecdda | ||
|
|
30119fec46 | ||
|
|
ac01818549 | ||
|
|
d486712a4c | ||
|
|
d116702145 | ||
|
|
5bee3614bd | ||
|
|
c73b07991f | ||
|
|
199cd07982 | ||
|
|
e3d62c6749 | ||
|
|
5134bf02fb | ||
|
|
f97be9352d | ||
|
|
b092a809f4 | ||
|
|
2cc7710807 | ||
|
|
5fb10d9758 | ||
|
|
100cb43afc | ||
|
|
7e2a585812 | ||
|
|
c1181435b3 | ||
|
|
22a7814ce4 | ||
|
|
b030da5c65 | ||
|
|
3c0acdd068 | ||
|
|
25ae2052e4 | ||
|
|
1b1777117f | ||
|
|
3e6bd95285 | ||
|
|
f4be9a31e9 | ||
|
|
a6282e4401 | ||
|
|
a9ae5f59b5 | ||
|
|
97ee2b7307 | ||
|
|
ff6edb6181 | ||
|
|
4c22263fb5 | ||
|
|
141f96fb9f |
No files matched your search
@@ -53,6 +53,10 @@
|
||||
- Expand/collapse changelog versions
|
||||
- Support local light/dark mode changes on system theme
|
||||
- Server time calculator
|
||||
- Add "Unmodded" filter to servers
|
||||
- List timestamp of last DayZ client update
|
||||
- Validate mods via Mod page to pull in updates
|
||||
- Support for Nasdara (Badlands) DLC
|
||||
|
||||
### Changed
|
||||
- Conform to PEP 440 versioning for beta versions
|
||||
@@ -74,6 +78,7 @@
|
||||
- Enable LAN page Empty/Full filters on first run of app
|
||||
- Propagate subscribed mods to Steam client
|
||||
- Make j/k keys in context menus follow same wraparound logic as down arrow/up arrow
|
||||
- Use SQLite DB for geolocation records
|
||||
|
||||
### Dropped
|
||||
- Battlemetrics API support
|
||||
@@ -92,6 +97,8 @@
|
||||
- Moved dialogs out of threads
|
||||
- History table not being in lock-step with state file
|
||||
- Current keyword filter not being applied when refreshing table
|
||||
- Steam Flatpak cannot run DZGUI as a non-Steam game
|
||||
- Collisions between native Steam and Steam Flatpak clients
|
||||
|
||||
### Unreleased
|
||||
- Load offline mods
|
||||
|
||||
@@ -17,7 +17,7 @@ DZGUI follows a specific set of guidelines, enumerated below:
|
||||
- Provide a free and open-source tool under GPL
|
||||
- Do not use undocumented, third-party servers to aggregate data; no central "DZGUI" server
|
||||
- All connections go to Steam and DayZ servers directly; the tool should function in perpetuity without intermediary endpoints
|
||||
- Target Linux desktop and Steam deck specifically; not a cross-platform tool
|
||||
- Target Linux desktop and Steam Deck specifically; not a cross-platform tool
|
||||
|
||||
## Setup and usage
|
||||
|
||||
@@ -35,6 +35,10 @@ Geolocation records from [DB-IP](https://db-ip.com) under [CC 4.0 license](https
|
||||
Executable versions of DZGUI published as release binaries ship with various runtime dependencies and the Python interpreter built in.
|
||||
Users wishing to review the licenses to these components can inspect the `LICENSE` file located in the release tarball.
|
||||
|
||||
Steam and the Steam logo are trademarks and/or registered trademarks of Valve Corporation in the U.S. and/or other countries.
|
||||
|
||||
DayZ is a trademark and brand of Bohemia Interactive a.s.
|
||||
|
||||
## Disclaimer
|
||||
|
||||
DZGUI is beta-quality software and is provided as-is.
|
||||
+30
-4
@@ -1,15 +1,18 @@
|
||||
import logging
|
||||
import re
|
||||
|
||||
from collections.abc import Iterator
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from typing import Any, TYPE_CHECKING
|
||||
from warnings import deprecated
|
||||
|
||||
from dzgui.const.constants import APP_NAME
|
||||
|
||||
logger = logging.getLogger(APP_NAME)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Iterator
|
||||
|
||||
|
||||
class ACF:
|
||||
def __init__(self, file: Path) -> None:
|
||||
@@ -42,7 +45,7 @@ class ACF:
|
||||
def dequote(cls, s: str) -> str:
|
||||
return s.rstrip('"').lstrip('"')
|
||||
|
||||
def parse(self, lines: Iterator[list[str]]) -> dict[str, str]:
|
||||
def parse(self, lines: "Iterator[list[str]]") -> dict[str, str]:
|
||||
acf: dict[str, Any] = {}
|
||||
try:
|
||||
while True:
|
||||
@@ -69,6 +72,28 @@ class ACF:
|
||||
except StopIteration:
|
||||
return acf
|
||||
|
||||
def search_key(self, key: str) -> str:
|
||||
return str(self.dict["AppState"][key])
|
||||
|
||||
def date_to_str(self, epoch: int) -> str | None:
|
||||
if epoch == 0:
|
||||
return None
|
||||
return datetime.fromtimestamp(epoch).isoformat(" ")
|
||||
|
||||
def get_date_by_key(self, key: str) -> str | None:
|
||||
try:
|
||||
stamp = int(self.search_key(key))
|
||||
return self.date_to_str(stamp)
|
||||
except Exception as e:
|
||||
logger.critical(e)
|
||||
return None
|
||||
|
||||
def get_last_updated(self) -> str | None:
|
||||
return self.get_date_by_key("LastUpdated")
|
||||
|
||||
def get_last_played(self) -> str | None:
|
||||
return self.get_date_by_key("LastPlayed")
|
||||
|
||||
def get_allows_downloads(self) -> int | None:
|
||||
try:
|
||||
# TODO: consider coercing strs to ints on initial import
|
||||
@@ -86,7 +111,8 @@ class WorkshopACF(ACF):
|
||||
def __init__(self, file: Path) -> None:
|
||||
super().__init__(file)
|
||||
|
||||
def unpack(self, d: dict, lines: list[Any] = []) -> str:
|
||||
def unpack(self, d: dict[str, Any], lines: list[Any] = []) -> str:
|
||||
self.indent: int
|
||||
t1 = "AppWorkshop"
|
||||
t2 = ("WorkshopItemsInstalled", "WorkshopItemDetails")
|
||||
for k, v in d.items():
|
||||
|
||||
+1
-5
@@ -114,10 +114,6 @@ def get_delimited_mods(steam_path: Path) -> list[Any]:
|
||||
return parse_mods(mods)
|
||||
|
||||
|
||||
def get_missing_mods(local: list, remote: list) -> list:
|
||||
return [mod for mod in remote if mod not in local]
|
||||
|
||||
|
||||
def _hash(uid: str, use_custom: bool = False) -> str:
|
||||
md5 = hashlib.md5()
|
||||
md5.update(uid.encode("ascii"))
|
||||
@@ -152,7 +148,7 @@ def remove_stale_signatures(
|
||||
|
||||
|
||||
def find_stale_mods(config: Path) -> list[int]:
|
||||
def push_record(rec: str) -> list:
|
||||
def push_record(rec: str) -> list[int]:
|
||||
record = fqip_to_record(rec)
|
||||
if record is None:
|
||||
return []
|
||||
|
||||
@@ -111,7 +111,7 @@ def get_netmask() -> str:
|
||||
|
||||
|
||||
# TODO: rename
|
||||
def test_ip(suffix: int, port: int, event: threading.Event) -> dict | None:
|
||||
def test_ip(suffix: int, port: int, event: threading.Event) -> dict[str, Any] | None:
|
||||
if event.is_set():
|
||||
return None
|
||||
netmask = get_netmask()
|
||||
@@ -139,7 +139,9 @@ def sanitize(name: str) -> str:
|
||||
return name
|
||||
|
||||
|
||||
def parse_json(json: list) -> list:
|
||||
def parse_json(
|
||||
json: list[dict[str, Any]],
|
||||
) -> list[tuple[str, str, str, str, int, int, int, str, int, int, str, bool]]:
|
||||
"""
|
||||
Server metadata is underspecified and server operators
|
||||
tend to insert random garbage in the headers. In case
|
||||
@@ -507,14 +509,14 @@ def record_to_fqip(record: Record) -> str:
|
||||
return f"{record.ip}:{record.gameport}:{record.qport}"
|
||||
|
||||
|
||||
def response_to_fqip(res: dict) -> str:
|
||||
def response_to_fqip(res: dict[str, Any]) -> str:
|
||||
ip = res["addr"].split(":")[0]
|
||||
gameport = res["gameport"]
|
||||
qport = res["addr"].split(":")[1]
|
||||
return f"{ip}:{gameport}:{qport}"
|
||||
|
||||
|
||||
def response_to_record(res: dict) -> Record:
|
||||
def response_to_record(res: dict[str, Any]) -> Record:
|
||||
ip = res["addr"].split(":")[0]
|
||||
gameport = res["gameport"]
|
||||
qport = res["addr"].split(":")[1]
|
||||
|
||||
@@ -173,11 +173,12 @@ class Shortcuts:
|
||||
self.save_shortcuts()
|
||||
|
||||
def save_shortcuts(self) -> None:
|
||||
try:
|
||||
backup = self.shortcuts_path.with_suffix(".vdf.bak")
|
||||
shutil.copy(self.shortcuts_path, backup)
|
||||
except Exception as e:
|
||||
logger.critical(e)
|
||||
if self.shortcuts_path.is_file():
|
||||
try:
|
||||
backup = self.shortcuts_path.with_suffix(".vdf.bak")
|
||||
shutil.copy(self.shortcuts_path, backup)
|
||||
except Exception as e:
|
||||
logger.critical(e)
|
||||
try:
|
||||
with open(self.shortcuts_path, "wb") as f:
|
||||
vdf.binary_dump(self.shortcuts, f)
|
||||
|
||||
+35
-7
@@ -12,8 +12,9 @@ from typing import Any, Generator, Union
|
||||
from warnings import deprecated
|
||||
|
||||
from dzgui.api.acf import ACF
|
||||
from dzgui.const.endpoints import VALIDATE
|
||||
from dzgui.init.prereqs import has_any_client
|
||||
from dzgui.init.proc import has_cmd
|
||||
from dzgui.init.prereqs import has_flatpak_client, has_steam_client
|
||||
from dzgui.const.constants import (
|
||||
APPID_DAYZ,
|
||||
APPID_DAYZ_EXP,
|
||||
@@ -23,11 +24,10 @@ from dzgui.const.constants import (
|
||||
DEBIAN_STEAM_PATH,
|
||||
DEFAULT_STEAM_PATH,
|
||||
DOWNLOADS_PATH,
|
||||
FLATPAK_CMD,
|
||||
FLATPAK_STEAM_PATH,
|
||||
LIBRARYFOLDERS_PATH,
|
||||
UBUNTU_STEAM_PATH,
|
||||
REQUEST_TIMEOUT,
|
||||
UBUNTU_STEAM_PATH,
|
||||
VDF_PATH,
|
||||
)
|
||||
from dzgui.const.endpoints import (
|
||||
@@ -84,7 +84,9 @@ def get_steam_paths() -> list[tuple[Path, str]]:
|
||||
}
|
||||
|
||||
for path in DEFAULT_PATH, FLATPAK_PATH, UBUNTU_PATH, DEBIAN_PATH:
|
||||
if path is FLATPAK_PATH and has_cmd(FLATPAK_CMD) is False:
|
||||
if path is FLATPAK_PATH and has_flatpak_client() is False:
|
||||
continue
|
||||
if path is not FLATPAK_PATH and has_steam_client() is False:
|
||||
continue
|
||||
if path.joinpath(VDF_PATH).is_file():
|
||||
paths.append((path, human[path]))
|
||||
@@ -315,10 +317,9 @@ def unsubscribe(key: str, mod: int) -> None:
|
||||
update_workshop(key, mod, UNSUB_ENDPOINT)
|
||||
|
||||
|
||||
@deprecated("Use subscribe()")
|
||||
def enqueue_mod(client: str, mod: str, appid: int) -> None:
|
||||
def validate_mod(client: str, mod: str) -> None:
|
||||
client_args = concat_bash_args(client)
|
||||
subprocess.Popen([*client_args, "+workshop_download_item", str(appid), mod])
|
||||
subprocess.Popen([*client_args, "+workshop_download_item", str(APPID_DAYZ), mod])
|
||||
|
||||
|
||||
@deprecated("Cf. https://github.com/ValveSoftware/steam-for-linux/issues/9672")
|
||||
@@ -388,6 +389,26 @@ def get_running_app() -> int | None:
|
||||
return None
|
||||
|
||||
|
||||
def get_last_updated(path: Path, appid: int) -> str | None:
|
||||
root_path = get_app_path(path, appid)
|
||||
acf = root_path.joinpath(f"steamapps/appmanifest_{appid}.acf")
|
||||
try:
|
||||
return ACF(acf).get_last_updated()
|
||||
except Exception as e:
|
||||
logger.critical(e)
|
||||
return None
|
||||
|
||||
|
||||
def get_last_played(path: Path, appid: int) -> str | None:
|
||||
root_path = get_app_path(path, appid)
|
||||
acf = root_path.joinpath(f"steamapps/appmanifest_{appid}.acf")
|
||||
try:
|
||||
return ACF(acf).get_last_played()
|
||||
except Exception as e:
|
||||
logger.critical(e)
|
||||
return None
|
||||
|
||||
|
||||
def get_app_allows_downloads(path: Path, appid: int) -> bool:
|
||||
try:
|
||||
root_path = get_app_path(path, appid)
|
||||
@@ -499,3 +520,10 @@ def get_app_name(appid: int) -> str | None:
|
||||
except Exception as e:
|
||||
logger.debug(e)
|
||||
return None
|
||||
|
||||
|
||||
@deprecated("Currently unused")
|
||||
def validate_dayz(client: str) -> None:
|
||||
client_args = concat_bash_args(client)
|
||||
uri = VALIDATE + str(APPID_DAYZ)
|
||||
subprocess.Popen([*client_args, uri])
|
||||
+24
-19
@@ -2,12 +2,17 @@ import logging
|
||||
import os
|
||||
import shutil
|
||||
|
||||
from importlib import resources
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
from warnings import deprecated
|
||||
|
||||
|
||||
from dzgui.const.constants import APP_NAME, STEAM_ICON, STEAM_ICONS_PATH
|
||||
from dzgui.const.constants import (
|
||||
APP_NAME,
|
||||
APP_NAME_LOWER,
|
||||
FLATPAK_ROOT_PATH,
|
||||
ICONS_PATH,
|
||||
)
|
||||
from dzgui.const.enum import Preferences
|
||||
from dzgui.config.query import lookup
|
||||
from dzgui.config.userprefs import UserPrefs
|
||||
@@ -18,7 +23,7 @@ from dzgui.init.migrate import (
|
||||
migrate_cols_file,
|
||||
)
|
||||
from dzgui.init.flock import lock_acquire
|
||||
from dzgui.init.prereqs import has_any_client
|
||||
from dzgui.init.prereqs import has_flatpak_client, has_steam_client
|
||||
from dzgui.strings import boot
|
||||
|
||||
from dzgui.util.deck import is_steam_deck, is_game_mode
|
||||
@@ -26,7 +31,7 @@ from dzgui.util.dirs import make_parents
|
||||
from dzgui.util.localize import set_locale
|
||||
from dzgui.util.map_count import test_map_count
|
||||
from dzgui.util.redact import RedactionFilter, REDACTION_PATTERNS
|
||||
from dzgui.util.strings import init, steam_icon_missing
|
||||
from dzgui.util.strings import init
|
||||
|
||||
from dzgui.views.base import App
|
||||
from dzgui.views.dialogs.boot import BootWindow
|
||||
@@ -99,29 +104,24 @@ def copy_bare_configs(config: "Path", resolution: "Path") -> tuple[bool, bool]:
|
||||
return (config_changed, state_changed)
|
||||
|
||||
|
||||
@deprecated("Currently unused")
|
||||
def load_steam_icon(config: Path) -> None:
|
||||
def load_steam_icon() -> None:
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk # noqa
|
||||
|
||||
default = lookup(config, Preferences.DEFAULT)
|
||||
icon_path = Path(default).joinpath(STEAM_ICONS_PATH)
|
||||
try:
|
||||
traversable = resources.files(APP_NAME_LOWER).joinpath(ICONS_PATH)
|
||||
icons = Path(str(traversable))
|
||||
|
||||
theme = Gtk.IconTheme.get_default()
|
||||
paths = theme.get_search_path()
|
||||
theme.set_search_path(paths + [str(icon_path)])
|
||||
paths = theme.get_search_path()
|
||||
icons = theme.list_icons(None)
|
||||
if STEAM_ICON not in icons:
|
||||
logger.warning(steam_icon_missing)
|
||||
theme.set_search_path(paths + [str(icons)])
|
||||
except Exception as e:
|
||||
print(e)
|
||||
logger.warning(e)
|
||||
logger.critical(e)
|
||||
|
||||
|
||||
def load_gui(version: str, is_debug: bool) -> None:
|
||||
def build_gui(version: str, is_debug: bool) -> App | None:
|
||||
lock = lock_acquire() # noqa
|
||||
|
||||
set_locale()
|
||||
@@ -151,7 +151,7 @@ def load_gui(version: str, is_debug: bool) -> None:
|
||||
wizard = SetupWizard(_is_steam_deck, XDG, xdg_paths)
|
||||
|
||||
if not wizard.is_setup_complete():
|
||||
return
|
||||
return None
|
||||
|
||||
setup_logger(XDG.debug)
|
||||
with open(XDG.debug, "w") as f:
|
||||
@@ -161,7 +161,11 @@ def load_gui(version: str, is_debug: bool) -> None:
|
||||
# TODO: sudo escalation dialog
|
||||
# count = get_map_count()
|
||||
# TODO: move into module
|
||||
if has_any_client() is False:
|
||||
if FLATPAK_ROOT_PATH in lookup(XDG.config, Preferences.DEFAULT):
|
||||
has_client = has_flatpak_client()
|
||||
else:
|
||||
has_client = has_steam_client()
|
||||
if has_client is False:
|
||||
EarlyAlertDialog(init.requires_steam)
|
||||
|
||||
bootwin = BootWindow(XDG, version)
|
||||
@@ -180,5 +184,6 @@ def load_gui(version: str, is_debug: bool) -> None:
|
||||
use_miles=use_miles,
|
||||
)
|
||||
print(boot.all_ok)
|
||||
load_steam_icon()
|
||||
app = App(prefs)
|
||||
app.run(None)
|
||||
return app
|
||||
+102
-3
@@ -3,6 +3,9 @@ import logging
|
||||
import re
|
||||
import requests
|
||||
import shutil
|
||||
import socket
|
||||
import sqlite3
|
||||
import time
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
@@ -22,15 +25,16 @@ def find_download_url(url: str) -> str | None:
|
||||
return str(urls[0])
|
||||
|
||||
|
||||
def get_ipdb(ips_path: Path) -> None:
|
||||
def get_ipdb(db_path: Path) -> None:
|
||||
try:
|
||||
url = find_download_url(DB_IP)
|
||||
if url is None:
|
||||
return
|
||||
|
||||
ips_path = db_path.with_suffix(".csv")
|
||||
month_file = db_path.parent / ".month"
|
||||
date = find_date(url)
|
||||
month_file = ips_path.parent / ".month"
|
||||
if ips_path.exists() and month_file.exists():
|
||||
if db_path.exists() and month_file.exists():
|
||||
old_date = month_file.read_text().rstrip("\n")
|
||||
if old_date == date:
|
||||
logger.info(f"IP DB date matches: {date}")
|
||||
@@ -45,6 +49,9 @@ def get_ipdb(ips_path: Path) -> None:
|
||||
logger.info("Stripping IPv6 records")
|
||||
strip_ipv6(ips_path)
|
||||
|
||||
create_table(db_path, ips_path)
|
||||
ips_path.unlink()
|
||||
|
||||
with open(month_file, "w") as f:
|
||||
f.write(date)
|
||||
logger.info(f"Wrote {date} to {month_file}")
|
||||
@@ -108,3 +115,95 @@ def strip_ipv6(path: Path) -> None:
|
||||
out.write(merged)
|
||||
path.unlink()
|
||||
alt_path.rename(path)
|
||||
|
||||
|
||||
def ip_to_int(ip: str) -> int:
|
||||
return int.from_bytes(socket.inet_aton(ip), "big")
|
||||
|
||||
|
||||
def marshal_rows(ip_file: Path) -> list[tuple[int, int, str, str]]:
|
||||
YIELD = 0.003
|
||||
SLEEP = 0.001
|
||||
with open(ip_file, "r") as f:
|
||||
rows: list[tuple[int, int, str, str]] = []
|
||||
# NOTE: yield to GIL periodically to reduce load on spinner
|
||||
last_yield = time.monotonic()
|
||||
for row in f:
|
||||
start, end, lat, lon = row.rstrip("\n").split(",")
|
||||
int_start = ip_to_int(start)
|
||||
int_end = ip_to_int(end)
|
||||
rows.append((int_start, int_end, lat, lon))
|
||||
|
||||
now = time.monotonic()
|
||||
if (now - last_yield) >= YIELD:
|
||||
time.sleep(SLEEP)
|
||||
last_yield = time.monotonic()
|
||||
return rows
|
||||
|
||||
|
||||
def create_table(db_file: Path, ip_file: Path) -> None:
|
||||
create = """
|
||||
CREATE TABLE IF NOT EXISTS ips (
|
||||
start_range INTEGER PRIMARY KEY,
|
||||
end_range INTEGER,
|
||||
latitude REAL,
|
||||
longitude REAL
|
||||
) WITHOUT ROWID;
|
||||
"""
|
||||
insert = """
|
||||
INSERT INTO ips
|
||||
(start_range, end_range, latitude, longitude)
|
||||
values(?, ?, ?, ?)
|
||||
"""
|
||||
|
||||
if db_file.exists():
|
||||
db_file.unlink()
|
||||
|
||||
conn = sqlite3.connect(":memory:")
|
||||
c = conn.cursor()
|
||||
|
||||
try:
|
||||
c.execute(create)
|
||||
rows = marshal_rows(ip_file)
|
||||
c.executemany(insert, rows)
|
||||
logger.info(f"Inserted {len(rows)} IP records into database")
|
||||
conn.commit()
|
||||
with sqlite3.connect(db_file) as dest:
|
||||
conn.backup(dest)
|
||||
logger.info(f"Serialized IP records to database: '{db_file}'")
|
||||
conn.close()
|
||||
except Exception as e:
|
||||
logger.critical(e)
|
||||
|
||||
|
||||
def read_from_table(db_path: Path, ip: str) -> tuple[float, float]:
|
||||
if not db_path.exists():
|
||||
raise FileNotFoundError(f"No file found at the location '{db_path}")
|
||||
|
||||
conn = sqlite3.connect(db_path)
|
||||
c = conn.cursor()
|
||||
|
||||
int_ip = ip_to_int(ip)
|
||||
|
||||
query = """
|
||||
SELECT end_range, latitude, longitude
|
||||
FROM ips
|
||||
WHERE start_range <= ?
|
||||
ORDER BY start_range DESC
|
||||
LIMIT 1
|
||||
"""
|
||||
index = """
|
||||
CREATE INDEX IF NOT EXISTS start_index
|
||||
ON ips(start_range);
|
||||
"""
|
||||
try:
|
||||
c.execute(index)
|
||||
conn.commit()
|
||||
c.execute(query, (int_ip,))
|
||||
row = c.fetchone()
|
||||
end, lat, lon = row
|
||||
conn.close()
|
||||
return lat, lon
|
||||
except Exception as e:
|
||||
logger.critical(e)
|
||||
raise e
|
||||
+5
-5
@@ -22,7 +22,7 @@ class Xdg:
|
||||
version: Path
|
||||
system: Path
|
||||
debug: Path
|
||||
ips: Path
|
||||
db: Path
|
||||
filters: Path
|
||||
shortcut: Path
|
||||
|
||||
@@ -51,7 +51,7 @@ def is_writeable(path_str: str) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def get_xdg_paths() -> dict:
|
||||
def get_xdg_paths() -> dict[str, Path]:
|
||||
home = Path.home()
|
||||
xdg_paths = {
|
||||
"XDG_CONFIG_HOME": home / ".config",
|
||||
@@ -73,7 +73,7 @@ def get_xdg_paths() -> dict:
|
||||
return resolved_paths
|
||||
|
||||
|
||||
def parse_filepaths(xdg: dict) -> Xdg:
|
||||
def parse_filepaths(xdg: dict[str, Path]) -> Xdg:
|
||||
config = xdg["XDG_CONFIG_HOME"]
|
||||
state = xdg["XDG_STATE_HOME"]
|
||||
share = xdg["XDG_DATA_HOME"]
|
||||
@@ -88,7 +88,7 @@ def parse_filepaths(xdg: dict) -> Xdg:
|
||||
resolution = state / "dzg.res.json"
|
||||
history = state / "dzg.history"
|
||||
versions = state / "dzg.versions"
|
||||
ips = state / "ips.csv"
|
||||
db = state / "ips.db"
|
||||
filters = state / "dzg.filters.json"
|
||||
|
||||
shortcut = share / APP_NAME_LOWER
|
||||
@@ -102,7 +102,7 @@ def parse_filepaths(xdg: dict) -> Xdg:
|
||||
versions,
|
||||
system,
|
||||
debug,
|
||||
ips,
|
||||
db,
|
||||
filters,
|
||||
shortcut,
|
||||
)
|
||||
@@ -51,7 +51,7 @@ INPUT_KEYBOARD = "input-keyboard-symbolic"
|
||||
LIST_ADD = "list-add-symbolic"
|
||||
REFRESH_ICON = "view-refresh-symbolic"
|
||||
SEARCH_ICON = "system-search-symbolic"
|
||||
STEAM_ICON = "steam_tray_mono"
|
||||
STEAM_ICON = "steam-tray-symbolic"
|
||||
VIEW_CONCEAL = "view-conceal-symbolic"
|
||||
VIEW_REVEAL = "view-reveal-symbolic"
|
||||
WARNING = "dialog-warning-symbolic"
|
||||
@@ -80,6 +80,7 @@ DEBUG_LOG = f"{APP_NAME}_DEBUG.LOG"
|
||||
SYSTEM_LOG = f"{APP_NAME}_SYSTEM.LOG"
|
||||
CHANGELOG_PATH = "data/CHANGELOG.md"
|
||||
IMAGES_PATH = "data/images"
|
||||
ICONS_PATH = "data/images/gtk"
|
||||
|
||||
HERO_PATH = "data/images/_hero.png"
|
||||
CSS_PATH = "data/app.css"
|
||||
@@ -89,13 +90,13 @@ FLATPAK_ROOT_PATH = ".var/app/com.valvesoftware.Steam"
|
||||
FLATPAK_STEAM_PATH = FLATPAK_ROOT_PATH + "/data/Steam"
|
||||
UBUNTU_STEAM_PATH = ".steam/steam"
|
||||
DEBIAN_STEAM_PATH = ".steam/debian-installation"
|
||||
STEAM_ICONS_PATH = "public"
|
||||
|
||||
LOG_FILTERS = ("CRITICAL", "WARNING", "INFO", "DEBUG")
|
||||
|
||||
TMP_PATH = "/tmp"
|
||||
TMP_TARBALL = "/tmp/dzgui.tar.gz"
|
||||
TMP_EXE = "/tmp/dzgui/dzgui"
|
||||
LOCK_FILE = "/tmp/dzgui.flock"
|
||||
|
||||
DAYZ_COMMUNITY_ROOT = "Missions"
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ COORDS_API = "http://ip-api.com/json"
|
||||
# steam URIs
|
||||
WORKSHOP = "steam://url/CommunityFilePage/"
|
||||
OPEN_URL = "steam://openurl/"
|
||||
VALIDATE = "steam://validate/"
|
||||
|
||||
# user facing
|
||||
GITHUB_PAGES = "https://aclist.github.io/dzgui/index"
|
||||
|
||||
+6
-1
@@ -157,6 +157,7 @@ class ContextMenu(EnumWithAttrs):
|
||||
COPY_SERVER_IP = {"label": strings.copy_ip}
|
||||
COPY_SERVER_NAME = {"label": strings.copy_name}
|
||||
UNSUB_MOD = {"label": strings.unsub_mod}
|
||||
VALIDATE_MOD = {"label": strings.validate_mod}
|
||||
OPEN_WORKSHOP = {"label": strings.open_workshop}
|
||||
REFRESH_PLAYERS = {"label": strings.refresh_players}
|
||||
REMOVE_HISTORY = {"label": strings.remove_history}
|
||||
@@ -172,7 +173,7 @@ class ContextMenuGroup(Enum):
|
||||
"""
|
||||
|
||||
SERVER_MOD = (ContextMenu.OPEN_WORKSHOP,)
|
||||
MOD = (ContextMenu.OPEN_WORKSHOP, ContextMenu.UNSUB_MOD)
|
||||
MOD = (ContextMenu.OPEN_WORKSHOP, ContextMenu.UNSUB_MOD, ContextMenu.VALIDATE_MOD)
|
||||
MOD_OFFLINE = (None,)
|
||||
LOG = (ContextMenu.COPY_LOG_CLIPBOARD,)
|
||||
SERVER_BROWSER = (
|
||||
@@ -246,6 +247,10 @@ class ModButton(EnumWithAttrs):
|
||||
"label": strings.mod_panel.select_stale,
|
||||
"tooltip": strings.mod_panel.select_stale_tooltip,
|
||||
}
|
||||
VALIDATE = {
|
||||
"label": strings.mod_panel.validate,
|
||||
"tooltip": strings.mod_panel.validate_tooltip,
|
||||
}
|
||||
|
||||
|
||||
class ButtonType(EnumWithAttrs):
|
||||
|
||||
@@ -111,6 +111,10 @@ class Emitter(GObject.GObject):
|
||||
def check_toggled(self, label: str, state: bool) -> None:
|
||||
pass
|
||||
|
||||
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=())
|
||||
def servers_timed_out(self) -> None:
|
||||
pass
|
||||
|
||||
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=(object,))
|
||||
def servers_loaded(self, tag: "ServerTab") -> None:
|
||||
pass
|
||||
|
||||
+16
-6
@@ -77,7 +77,7 @@ class AppNavigation:
|
||||
|
||||
class Controller(GObject.GObject):
|
||||
def __init__(self) -> None:
|
||||
self.dist_cache: dict[str, "Haversine"] = {}
|
||||
self.dist_cache: dict[str, "Haversine" | None] = {}
|
||||
self.mediator = AppNavigation()
|
||||
|
||||
self.prefs: UserPrefs
|
||||
@@ -102,6 +102,16 @@ class Controller(GObject.GObject):
|
||||
except AttributeError:
|
||||
logger.critical(f"{attr} is not a valid AppNavigation attribute.")
|
||||
|
||||
def validate_mods(
|
||||
self, treeview: Union["ModTreeView", "OfflineModTreeView", None] = None
|
||||
) -> None:
|
||||
if treeview is None:
|
||||
view = self.mediator.modtreeview
|
||||
else:
|
||||
view = treeview
|
||||
mod_man = view.get_mod_man()
|
||||
mod_man.validate_mods()
|
||||
|
||||
def get_prefs(self) -> UserPrefs:
|
||||
return self.prefs
|
||||
|
||||
@@ -264,7 +274,7 @@ class Controller(GObject.GObject):
|
||||
except Exception as e:
|
||||
dialog = ExceptionDialog(self, str(e))
|
||||
dialog.show_all()
|
||||
dialog.run()
|
||||
dialog.run() # type: ignore[no-untyped-call]
|
||||
|
||||
def select_colorized(self) -> None:
|
||||
mod_man = self.mediator.modtreeview.get_mod_man()
|
||||
@@ -298,7 +308,7 @@ class Controller(GObject.GObject):
|
||||
except Exception as e:
|
||||
dialog = ExceptionDialog(self, str(e))
|
||||
dialog.show_all()
|
||||
dialog.run()
|
||||
dialog.run() # type: ignore[no-untyped-call]
|
||||
|
||||
def update_steam_api_key(self, text: str) -> None:
|
||||
self.config_man.update_steam_api_key(text)
|
||||
@@ -345,17 +355,17 @@ class Controller(GObject.GObject):
|
||||
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, Union["Haversine", None]]:
|
||||
return self.dist_cache
|
||||
|
||||
def set_dist_cache(self, addr: str, haversine: "Haversine") -> None:
|
||||
def set_dist_cache(self, addr: str, haversine: Union["Haversine", None]) -> None:
|
||||
self.dist_cache[addr] = haversine
|
||||
|
||||
def get_map_store(self) -> Gtk.ListStore:
|
||||
filter_man = self.get_filter_man()
|
||||
return filter_man.get_map_store()
|
||||
|
||||
def get_enabled_filters(self) -> dict:
|
||||
def get_enabled_filters(self) -> dict[str, bool]:
|
||||
filter_man = self.get_filter_man()
|
||||
return filter_man.get_filters()
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 130 KiB After Width: | Height: | Size: 361 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.3.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M568 320C568 457 456.8 568 319.6 568C205.8 568 110 491.7 80.6 387.6L175.8 426.9C182.2 459 210.7 483.3 244.7 483.3C283.9 483.3 316.6 450.9 314.9 409.8L399.4 349.6C451.5 350.9 495.2 308.7 495.2 256.1C495.2 204.5 453.2 162.6 401.5 162.6C349.8 162.6 307.8 204.6 307.8 256.1L307.8 257.3L248.6 343C233.1 342.1 217.9 346.4 205.1 355.1L72 300.1C82.2 172.4 189.1 72 319.6 72C456.8 72 568 183 568 320zM227.7 448.3L197.2 435.7C202.8 447.3 212.5 456.5 224.4 461.5C251.3 472.7 282.2 459.9 293.4 433.1C298.8 420.1 298.9 405.8 293.5 392.8C288.1 379.8 278 369.6 265 364.2C252.1 358.8 238.3 359 226.1 363.6L257.6 376.6C277.4 384.8 286.8 407.5 278.5 427.3C270.2 447.2 247.5 456.5 227.7 448.3zM401.5 193.8C435.9 193.8 463.8 221.7 463.8 256.1C463.8 290.5 435.9 318.4 401.5 318.4C367.1 318.4 339.2 290.5 339.2 256.1C339.2 221.7 367.1 193.8 401.5 193.8zM401.6 302.8C427.4 302.8 448.4 281.8 448.4 256C448.4 230.2 427.4 209.2 401.6 209.2C375.8 209.2 354.8 230.2 354.8 256C354.8 281.8 375.8 302.8 401.6 302.8z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -10,10 +10,10 @@ if TYPE_CHECKING:
|
||||
logger = logging.getLogger(APP_NAME)
|
||||
|
||||
|
||||
def get_local_coords(path: "Path") -> Coords | None:
|
||||
def get_local_coords(db_path: "Path") -> Coords | None:
|
||||
try:
|
||||
my_ip = get_local_ip()
|
||||
return get_coords(path, my_ip)
|
||||
return get_coords(db_path, my_ip)
|
||||
except Exception as e:
|
||||
logger.warn(e)
|
||||
return None
|
||||
+3
-2
@@ -5,8 +5,9 @@ import sys
|
||||
from pathlib import Path
|
||||
from typing import TextIO
|
||||
|
||||
from dzgui.views.dialogs.early_alert import EarlyAlertDialog
|
||||
from dzgui.const.constants import LOCK_FILE
|
||||
from dzgui.util.strings import cannot_acquire_lock
|
||||
from dzgui.views.dialogs.early_alert import EarlyAlertDialog
|
||||
|
||||
|
||||
def lock_release(lock: TextIO) -> None:
|
||||
@@ -15,7 +16,7 @@ def lock_release(lock: TextIO) -> None:
|
||||
|
||||
|
||||
def lock_acquire() -> TextIO:
|
||||
lockfile = Path("/tmp/dzgui.flock")
|
||||
lockfile = Path(LOCK_FILE)
|
||||
lock = open(lockfile, "w")
|
||||
try:
|
||||
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
|
||||
@@ -3,11 +3,14 @@ import shutil
|
||||
import subprocess
|
||||
|
||||
from dzgui.const.constants import APP_NAME, FLATPAK_APPID, FLATPAK_CMD, STEAM_CMD
|
||||
from dzgui.init.proc import is_sandboxed
|
||||
|
||||
logger = logging.getLogger(APP_NAME)
|
||||
|
||||
|
||||
def has_flatpak_client() -> bool:
|
||||
if is_sandboxed():
|
||||
return True
|
||||
try:
|
||||
res = subprocess.run(
|
||||
[FLATPAK_CMD, "list", "--columns=application"],
|
||||
|
||||
+1
-1
Submodule dzgui/lib/dayzquery updated: 07483b88ed...fe413df8e8.
+5
-2
@@ -42,6 +42,9 @@ def main() -> None:
|
||||
test_libgi_missing()
|
||||
|
||||
# NOTE: only import ligbirepository modules after the above check
|
||||
from dzgui.app_init import load_gui
|
||||
from dzgui.app_init import build_gui
|
||||
|
||||
load_gui(version, args.debug)
|
||||
app = build_gui(version, args.debug)
|
||||
if app is None:
|
||||
sys.exit(0)
|
||||
app.run(None)
|
||||
@@ -162,7 +162,7 @@ class ConfigManager:
|
||||
trace = traceback.format_exc()
|
||||
dialog = ExceptionDialog(self.controller, trace)
|
||||
dialog.show_all()
|
||||
dialog.run()
|
||||
dialog.run() # type: ignore[no-untyped-call]
|
||||
|
||||
def save_res_and_quit(self, tv: "ServerTreeView", window: "OuterWindow") -> None:
|
||||
columns = tv.get_columns()
|
||||
|
||||
@@ -298,22 +298,22 @@ class ConnectionManager:
|
||||
|
||||
def _present_modlist_dialog(self, mods: list[list[str]]) -> None:
|
||||
dialog = ServerModDialog(self.controller, mods)
|
||||
dialog.run()
|
||||
dialog.run() # type: ignore[no-untyped-call]
|
||||
|
||||
def _present_details_dialog(self, details: Servers.Details) -> None:
|
||||
dialog = ServerDetailsDialog(self.controller, details)
|
||||
dialog.run()
|
||||
dialog.run() # type: ignore[no-untyped-call]
|
||||
|
||||
def _workshop_timeout(self, msg: str) -> None:
|
||||
dialog = ExceptionDialog(self.controller, msg)
|
||||
dialog.show_all()
|
||||
dialog.run()
|
||||
dialog.run() # type: ignore[no-untyped-call]
|
||||
|
||||
def _server_timeout(self) -> None:
|
||||
dialog = ExceptionDialog(self.controller, server_timeout)
|
||||
dialog.set_details_buffer(kb.DZG_006)
|
||||
dialog.show_all()
|
||||
dialog.run()
|
||||
dialog.run() # type: ignore[no-untyped-call]
|
||||
|
||||
def get_debug_args(self) -> str:
|
||||
addr = f"{self.record.ip}:{self.record.gameport}"
|
||||
|
||||
@@ -44,7 +44,7 @@ class ContextMenuManager:
|
||||
# UNTHREADED
|
||||
case ContextMenu.ADD_NOTE:
|
||||
dialog = NoteDialog(self.controller)
|
||||
dialog.run()
|
||||
dialog.run() # type: ignore[no-untyped-call]
|
||||
case ContextMenu.COPY_SERVER_IP:
|
||||
self.copy_server_ip()
|
||||
case ContextMenu.COPY_SERVER_NAME:
|
||||
@@ -75,10 +75,14 @@ class ContextMenuManager:
|
||||
|
||||
if isinstance(self.treeview, (ModTreeView, OfflineModTreeView)):
|
||||
match action:
|
||||
case ContextMenu.UNSUB_MOD:
|
||||
self.controller.unsub_mods(self.treeview)
|
||||
case ContextMenu.OPEN_WORKSHOP:
|
||||
self.open_mod_page()
|
||||
case ContextMenu.UNSUB_MOD:
|
||||
self.controller.unsub_mods(self.treeview)
|
||||
case ContextMenu.VALIDATE_MOD:
|
||||
self.controller.validate_mods(self.treeview)
|
||||
case _:
|
||||
pass
|
||||
|
||||
if isinstance(self.treeview, LogTreeView):
|
||||
match action:
|
||||
|
||||
@@ -51,7 +51,6 @@ class FilterManager:
|
||||
self.active_map = (0, all_maps)
|
||||
self.prior_map = all_maps
|
||||
|
||||
self.filters: list
|
||||
self.enabled_filters = dict(self.default_filters)
|
||||
|
||||
def get_tooltips(self) -> dict[str, str]:
|
||||
@@ -75,11 +74,11 @@ class FilterManager:
|
||||
def set_active_keyword(self, word: str) -> None:
|
||||
self.active_keyword = word
|
||||
|
||||
def get_default_filters(self) -> dict:
|
||||
def get_default_filters(self) -> dict[str, bool]:
|
||||
"""Deep copy of defaults"""
|
||||
return dict(self.default_filters)
|
||||
|
||||
def get_filters(self) -> dict:
|
||||
def get_filters(self) -> dict[str, bool]:
|
||||
return self.enabled_filters
|
||||
|
||||
def set_filter(self, label: str, state: bool) -> None:
|
||||
@@ -116,7 +115,7 @@ class FilterManager:
|
||||
def get_unique_maps(self) -> list[str]:
|
||||
return [row[0] for row in self.map_store if row[0] != "All maps"]
|
||||
|
||||
def get_all_filters(self) -> tuple:
|
||||
def get_all_filters(self) -> tuple[str, ...]:
|
||||
map_name = self.get_active_map_name()
|
||||
enabled = self.get_filters()
|
||||
kw = self.get_active_keyword()
|
||||
|
||||
+27
-3
@@ -4,7 +4,7 @@ import time
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from dzgui.api.steam import unsubscribe
|
||||
from dzgui.api.steam import unsubscribe, validate_mod
|
||||
from dzgui.api.mods import (
|
||||
get_delimited_mods,
|
||||
find_stale_mods,
|
||||
@@ -85,7 +85,7 @@ class ModManager:
|
||||
total_mods = len(self.store)
|
||||
self.emitter.emit("mods_updated", msg, total_mods)
|
||||
|
||||
def unsub_mods(self) -> None:
|
||||
def get_selected_mods(self) -> list[tuple[str, Gtk.TreeIter]]:
|
||||
sel = self.treeview.get_selection()
|
||||
model, pathlist = sel.get_selected_rows()
|
||||
# NOTE: reverse when multiple selection
|
||||
@@ -96,6 +96,15 @@ class ModManager:
|
||||
continue
|
||||
mod, _iter = res
|
||||
mods.append((mod, _iter))
|
||||
return mods
|
||||
|
||||
def validate_mods(self) -> None:
|
||||
mods = self.get_selected_mods()
|
||||
self.thread_man.set_job_count(len(mods))
|
||||
self.validate_all_mods(mods)
|
||||
|
||||
def unsub_mods(self) -> None:
|
||||
mods = self.get_selected_mods()
|
||||
self.thread_man.set_job_count(len(mods))
|
||||
self.unsub_all_mods(mods)
|
||||
|
||||
@@ -109,6 +118,21 @@ class ModManager:
|
||||
mod = model.get_value(tree_iter, 2)
|
||||
return mod, tree_iter
|
||||
|
||||
def validate_atomic_mod(self, mod: str) -> None:
|
||||
# NOTE: propagation to dayz exp will happen during
|
||||
# subsequent symlink phase on connect
|
||||
client = self.controller.query_config(Preferences.CLIENT)
|
||||
validate_mod(client, mod)
|
||||
|
||||
@call_on_thread(dialogs.validating_mods)
|
||||
def validate_all_mods(self, mods: list[tuple[str, Gtk.TreeIter]]) -> None:
|
||||
for mod, _iter in mods:
|
||||
self.validate_atomic_mod(mod)
|
||||
self.thread_man.increment_dialog()
|
||||
time.sleep(API_RATE_LIMIT)
|
||||
func = StoredFunc(lambda: None)
|
||||
self.thread_man.set_cleanup_func(func)
|
||||
|
||||
@call_on_thread(dialogs.deleting_mods)
|
||||
def unsub_all_mods(self, mods: list[tuple[str, Gtk.TreeIter]]) -> None:
|
||||
for mod, _iter in mods:
|
||||
@@ -197,7 +221,7 @@ class ModManager:
|
||||
total_size += mod[3]
|
||||
return total_mods, total_size
|
||||
|
||||
def _on_stale_mods_found(self, stale_mods: list) -> None:
|
||||
def _on_stale_mods_found(self, stale_mods: list[int]) -> None:
|
||||
"""Manipulates attached ListStore in the main event loop"""
|
||||
model = self.treeview.get_model()
|
||||
if model is None:
|
||||
|
||||
@@ -52,7 +52,7 @@ class OfflineManager:
|
||||
is_valid = is_mission(folder)
|
||||
self.emitter.emit("custom_mission_loaded", str(folder), is_valid)
|
||||
|
||||
def find_custom_mods(self, callback: Callable) -> None:
|
||||
def find_custom_mods(self, callback: Callable[[], None]) -> None:
|
||||
folder = self.open_folderpicker(dialogs.custom_mod_dialog)
|
||||
window = self.controller.get_window()
|
||||
window.set_sensitive(False)
|
||||
|
||||
@@ -22,8 +22,8 @@ logger = logging.getLogger(APP_NAME)
|
||||
|
||||
def call_on_thread(
|
||||
dialog_str: str, show_dialog: bool = True, show_cancel: bool = False
|
||||
) -> Callable:
|
||||
def decorator(func: Callable) -> Callable:
|
||||
) -> Callable[..., Any]:
|
||||
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
|
||||
@wraps(func)
|
||||
def wrapper(*args: Any, **kwargs: Any) -> None:
|
||||
self = args[0]
|
||||
@@ -42,7 +42,7 @@ def call_on_thread(
|
||||
|
||||
|
||||
class StoredFunc:
|
||||
def __init__(self, func: Callable, *args: Any, **kwargs: Any) -> None:
|
||||
def __init__(self, func: Callable[..., Any], *args: Any, **kwargs: Any) -> None:
|
||||
sig = inspect.signature(func)
|
||||
self.func = func
|
||||
self.bindings = sig.bind(*args, **kwargs)
|
||||
|
||||
@@ -60,9 +60,9 @@ class UpdateManager:
|
||||
def _on_update_success(self) -> None:
|
||||
msg = dialogs.update_success
|
||||
dialog = QuitDialog(self.controller, msg)
|
||||
dialog.run()
|
||||
dialog.run() # type: ignore[no-untyped-call]
|
||||
|
||||
def _on_update_failure(self, msg: str) -> None:
|
||||
dialog = ExceptionDialog(self.controller, msg)
|
||||
dialog.show_all()
|
||||
dialog.run()
|
||||
dialog.run() # type: ignore[no-untyped-call]
|
||||
@@ -11,7 +11,7 @@ from gi.repository.Gtk import ListStore, TreeIter # noqa E402
|
||||
from gi.repository import GObject # noqa E402
|
||||
|
||||
GTYPE_TO_PYTHON = {
|
||||
GObject.type_from_name(GObject.type_name(ptype)): ptype
|
||||
GObject.type_from_name(GObject.type_name(ptype)): ptype # type: ignore[no-untyped-call]
|
||||
for ptype in (int, float, str, bool, object)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import re
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, TYPE_CHECKING, Union
|
||||
|
||||
from dzgui.const.enum import FilterMode
|
||||
@@ -16,6 +17,22 @@ DAY_REG = r"([0][6-9]|[1][0-7])"
|
||||
NIGHT_REG = r"([0][0-5]|[1][8-9]|[2][0-3])"
|
||||
|
||||
|
||||
@dataclass(slots=True, frozen=True)
|
||||
class ServerRow:
|
||||
name: str
|
||||
mapname: str
|
||||
view: str
|
||||
time: str
|
||||
players: int
|
||||
max_players: int
|
||||
queue: int
|
||||
ip: str
|
||||
qport: int
|
||||
ping: str
|
||||
provider: str
|
||||
modded: bool
|
||||
|
||||
|
||||
class ProxyModelManager:
|
||||
"""
|
||||
Manages access to cached FastInsertListStore resources and
|
||||
@@ -30,7 +47,7 @@ class ProxyModelManager:
|
||||
|
||||
def __init__(self, filter_man: "FilterManager") -> None:
|
||||
self.filter_cache: dict[
|
||||
tuple[str],
|
||||
tuple[str, ...],
|
||||
list[tuple[str, str, str, str, int, int, int, str, int, int, str, bool]],
|
||||
] = {}
|
||||
|
||||
@@ -51,9 +68,6 @@ class ProxyModelManager:
|
||||
return False
|
||||
return True
|
||||
|
||||
def append_row(self, row: list) -> None:
|
||||
self.proxy_model.append(row)
|
||||
|
||||
def append_row_to_control(
|
||||
self, row: tuple[str, str, str, str, int, int, int, str, int, int, str, bool]
|
||||
) -> None:
|
||||
@@ -67,7 +81,6 @@ class ProxyModelManager:
|
||||
self.proxy_model[treeiter][4] = playercount.players
|
||||
self.proxy_model[treeiter][6] = playercount.queue
|
||||
|
||||
# TODO: use dataclass for record rows
|
||||
def append_row_to_history(
|
||||
self,
|
||||
history: tuple[str, str, str, str, int, int, int, str, int, int, str, bool],
|
||||
@@ -247,7 +260,7 @@ class ProxyModelManager:
|
||||
rows = [row for row in rows if row[11]]
|
||||
return rows
|
||||
|
||||
def filter_toggle_on(self, filters: tuple[str]) -> list:
|
||||
def filter_toggle_on(self, filters: tuple[str, ...]) -> list:
|
||||
"""Effectively applies all filters"""
|
||||
self.set_filtered(self.control_model)
|
||||
self.set_filtered(self.filter_map(filters))
|
||||
@@ -258,7 +271,7 @@ class ProxyModelManager:
|
||||
self.set_filtered(self.filter_toggle_off(filters, f))
|
||||
return self.filtered
|
||||
|
||||
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
|
||||
|
||||
def set_filtered(self, rows: list | None) -> None:
|
||||
|
||||
+36
-10
@@ -92,6 +92,7 @@ class ServerModelManager:
|
||||
job = Servers.query_api
|
||||
params = Servers.params
|
||||
servers = []
|
||||
|
||||
with ThreadPoolExecutor() as executor:
|
||||
futures = [executor.submit(job, key, APPID_DAYZ, param) for param in params]
|
||||
for future in as_completed(futures):
|
||||
@@ -200,19 +201,43 @@ class ServerModelManager:
|
||||
|
||||
@call_on_thread(dialog.querying)
|
||||
def add_by_ip(self, addr: str) -> None:
|
||||
res = Servers.query_by_ip(addr)
|
||||
self._parse_single_record(res)
|
||||
self.preserve_on_fail = True
|
||||
try:
|
||||
res = Servers.query_by_ip(addr)
|
||||
self._parse_single_record(res)
|
||||
except Exception as e:
|
||||
logger.warning(e)
|
||||
self.thread_man.set_cleanup_func(
|
||||
StoredFunc(self._cleanup_on_failure), destroy_first=True
|
||||
)
|
||||
return
|
||||
|
||||
@call_on_thread(dialog.querying)
|
||||
def add_by_record(self, record: Servers.Record) -> None:
|
||||
self.preserve_on_fail = True
|
||||
# NOTE: rationale: a raw Record as shown in server browser may resolve to a different IP
|
||||
res = Servers.query_by_record(record)
|
||||
self._parse_single_record(res)
|
||||
try:
|
||||
res = Servers.query_by_record(record)
|
||||
self._parse_single_record(res)
|
||||
except Exception as e:
|
||||
logger.warning(e)
|
||||
self.thread_man.set_cleanup_func(
|
||||
StoredFunc(self._cleanup_on_failure), destroy_first=True
|
||||
)
|
||||
return
|
||||
|
||||
@call_on_thread(dialog.querying)
|
||||
def remove_by_record(self, record: Servers.Record) -> None:
|
||||
res = Servers.query_by_record(record)
|
||||
self._parse_single_record(res, delete=True)
|
||||
self.preserve_on_fail = True
|
||||
try:
|
||||
res = Servers.query_by_record(record)
|
||||
self._parse_single_record(res, delete=True)
|
||||
except Exception as e:
|
||||
logger.warning(e)
|
||||
self.thread_man.set_cleanup_func(
|
||||
StoredFunc(self._cleanup_on_failure), destroy_first=True
|
||||
)
|
||||
return
|
||||
|
||||
def update_history(
|
||||
self,
|
||||
@@ -279,7 +304,6 @@ class ServerModelManager:
|
||||
proxy_man.update_playercount(self.playercount)
|
||||
|
||||
def _parse_single_record(self, response: "A2SInfo", delete: bool = False) -> None:
|
||||
self.preserve_on_fail = True
|
||||
try:
|
||||
row = response.as_row()
|
||||
except Exception as e:
|
||||
@@ -408,12 +432,14 @@ class ServerModelManager:
|
||||
if show_dialog:
|
||||
dialog = ExceptionDialog(self.controller, dialogs.load_error_lan)
|
||||
dialog.show_all()
|
||||
dialog.run()
|
||||
dialog.run() # type: ignore[no-untyped-call]
|
||||
|
||||
def _cleanup_on_failure(self, show_dialog: bool = True) -> None:
|
||||
# TODO: disable map, keyword, and filter widgets if model is None
|
||||
# signal driven (servers_empty, servers_failed_to_load)
|
||||
|
||||
self.emitter.emit("servers_timed_out")
|
||||
|
||||
if self.preserve_on_fail is False:
|
||||
self.tv.set_model(None)
|
||||
filter_man = self.tv.get_filter_man()
|
||||
@@ -425,14 +451,14 @@ class ServerModelManager:
|
||||
dialog = ExceptionDialog(self.controller, server_timeout)
|
||||
dialog.set_details_buffer(kb.DZG_006)
|
||||
dialog.show_all()
|
||||
dialog.run()
|
||||
dialog.run() # type: ignore[no-untyped-call]
|
||||
|
||||
def _push_data(self, data: list[Any]) -> None:
|
||||
self._get_proxy_man().push(data)
|
||||
self._sort_unique_maps(data)
|
||||
self.thread_man.set_cleanup_func(StoredFunc(self._cleanup_on_success))
|
||||
|
||||
def _sort_unique_maps(self, data: list) -> None:
|
||||
def _sort_unique_maps(self, data: list[Any]) -> None:
|
||||
u_maps = set([row[1] for row in data])
|
||||
self._set_new_maps(sorted(u_maps))
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ update_success = "Updated successfully. Please exit and relaunch."
|
||||
load_error_lan = "Failed to find any servers on your network.\nCheck the server query port or your firewall settings."
|
||||
fetching_mods = "Fetching mod metadata"
|
||||
deleting_mods = "Unsubscribing mods"
|
||||
validating_mods = "Validating mods"
|
||||
scanning_mods = "Scanning mods"
|
||||
parsing_mods = "Parsing mods"
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ desc_flatpak_path = (
|
||||
desc_ubuntu_path = "This is the default Steam path on <b>Ubuntu-based systems</b>."
|
||||
desc_debian_path = "This is the default Steam path on <b>Debian-based systems</b>."
|
||||
no_valid_paths = (
|
||||
"No valid Steam paths found on system. Please install Steam to continue."
|
||||
"No valid Steam path or Steam command found on system. Please install Steam to continue."
|
||||
)
|
||||
button_scan = "Scan for Steam"
|
||||
|
||||
|
||||
+5
-4
@@ -59,7 +59,7 @@ class CalcDist:
|
||||
self,
|
||||
addr: str,
|
||||
enum: "ServerTab",
|
||||
result_queue: "Queue",
|
||||
result_queue: "Queue[tuple[str, Haversine | None, ServerTab]]",
|
||||
controller: "Controller",
|
||||
cache: dict[str, Haversine],
|
||||
) -> None:
|
||||
@@ -72,7 +72,7 @@ class CalcDist:
|
||||
self.ip = self.addr
|
||||
|
||||
dist = self.compare(self.ip)
|
||||
self.result_queue.put([self.addr, dist, self.enum])
|
||||
self.result_queue.put((self.addr, dist, self.enum))
|
||||
|
||||
def compare(self, remote_ip: str) -> Haversine | None:
|
||||
prefs = self.controller.get_prefs()
|
||||
@@ -80,8 +80,9 @@ class CalcDist:
|
||||
if local is None:
|
||||
return None
|
||||
try:
|
||||
remote = get_coords(prefs.paths.ips, remote_ip)
|
||||
except Exception:
|
||||
remote = get_coords(prefs.paths.db, remote_ip)
|
||||
except Exception as e:
|
||||
logger.critical(e)
|
||||
return None
|
||||
|
||||
haversine = Haversine(local.lat, local.lon, remote.lat, remote.lon)
|
||||
|
||||
+11
-3
@@ -1,12 +1,16 @@
|
||||
import logging
|
||||
import subprocess
|
||||
|
||||
from dzgui.const.constants import APP_NAME, FLATPAK_APPID, GTK_APP_ID
|
||||
from typing import TYPE_CHECKING
|
||||
from dzgui.const.constants import APP_NAME, FLATPAK_APPID, GTK_APP_ID, TMP_PATH
|
||||
|
||||
logger = logging.getLogger(APP_NAME)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
|
||||
def apply_overrides(xdg: dict[str, str]) -> None:
|
||||
|
||||
def apply_overrides(xdg: dict[str, "Path"]) -> None:
|
||||
try:
|
||||
# NOTE: D-Bus registration required for sandbox
|
||||
args = [
|
||||
@@ -18,7 +22,11 @@ def apply_overrides(xdg: dict[str, str]) -> None:
|
||||
]
|
||||
subprocess.run([*args], capture_output=True)
|
||||
|
||||
for path in xdg.values():
|
||||
paths: list[str] = []
|
||||
paths.extend([str(path) for path in xdg.values()])
|
||||
paths.append(TMP_PATH)
|
||||
|
||||
for path in paths:
|
||||
args = [
|
||||
"flatpak",
|
||||
"--user",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from dzgui.util.localize import number
|
||||
from dzgui.strings.generic import selectable_tree
|
||||
@@ -60,7 +61,7 @@ def format_server_mods(mods: int) -> str:
|
||||
return f"Found {mods:n} {plural}. {workshop}"
|
||||
|
||||
|
||||
def format_player_count(model: Gtk.TreeModel | None, control: list) -> str:
|
||||
def format_player_count(model: Gtk.TreeModel | None, control: list[Any]) -> str:
|
||||
players = 0
|
||||
hits: int
|
||||
status: str
|
||||
|
||||
+15
-46
@@ -1,4 +1,5 @@
|
||||
import a2s
|
||||
from ipaddress import ip_address
|
||||
import requests
|
||||
import shutil
|
||||
import subprocess
|
||||
@@ -7,6 +8,7 @@ from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from dzgui.api.servers import Record
|
||||
from dzgui.config.ipdb import read_from_table
|
||||
from dzgui.const.constants import REQUEST_TIMEOUT
|
||||
from dzgui.const.endpoints import IP_ECHO
|
||||
|
||||
@@ -29,58 +31,25 @@ class ReservedIPError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def ip2list(ip: str) -> list:
|
||||
def ip2list(ip: str) -> list[int]:
|
||||
return [int(digit) for digit in ip.split(".")]
|
||||
|
||||
|
||||
# TODO: tests
|
||||
def is_reserved(digits: list) -> bool:
|
||||
reserved = [0, 10, 127]
|
||||
|
||||
if digits[0] in reserved:
|
||||
return True
|
||||
if digits[0] == 192 and digits[1] == 168:
|
||||
return True
|
||||
return False
|
||||
def is_usable(ip: str) -> bool:
|
||||
address = ip_address(ip)
|
||||
if address.is_reserved or address.is_private:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def get_coords(ips: "Path", ip: str) -> Coords:
|
||||
split = ip2list(ip)
|
||||
|
||||
if is_reserved(split):
|
||||
def get_coords(db_path: "Path", ip: str) -> Coords:
|
||||
if not is_usable(ip):
|
||||
raise ReservedIPError(f"{ip} is a reserved IP address.")
|
||||
|
||||
prefix = f"^{split[0]}.{split[1]}."
|
||||
|
||||
if shutil.which("rg") is not None:
|
||||
args = ["/usr/bin/rg", prefix, str(ips)]
|
||||
else:
|
||||
args = ["/usr/bin/grep", "-E", prefix, str(ips)]
|
||||
proc = subprocess.run(args, capture_output=True, text=True)
|
||||
|
||||
if proc.returncode != 0:
|
||||
raise GeolocationError("Failed to split records")
|
||||
|
||||
ip_list = proc.stdout.splitlines()
|
||||
|
||||
for address in ip_list:
|
||||
fields = address.split(",")
|
||||
upper = fields[1]
|
||||
|
||||
upper_digits = ip2list(upper)
|
||||
if ip == upper:
|
||||
return Coords(float(fields[-2]), float(fields[-1]))
|
||||
|
||||
if split[2] > upper_digits[2]:
|
||||
continue
|
||||
|
||||
if split[2] == upper_digits[2]:
|
||||
if split[3] > upper_digits[3]:
|
||||
continue
|
||||
|
||||
if split[2] <= upper_digits[2]:
|
||||
return Coords(float(fields[-2]), float(fields[-1]))
|
||||
raise GeolocationError("No matching records found")
|
||||
try:
|
||||
lat, lon = read_from_table(db_path, ip)
|
||||
return Coords(lat, lon)
|
||||
except Exception:
|
||||
raise GeolocationError("No matching records found")
|
||||
|
||||
|
||||
def get_local_ip() -> str:
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
from pathlib import Path
|
||||
from typing import cast, TYPE_CHECKING
|
||||
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk, Gdk, GLib # noqa E402
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dzgui.controllers.mc import Controller
|
||||
from dzgui.views.base import OuterWindow
|
||||
from dzgui.views.mixins.context_mixin import ContextMixin
|
||||
from gi.repository import GdkPixbuf
|
||||
|
||||
# TODO: randomize/mock PII distance values
|
||||
|
||||
|
||||
class IdleScreenshot:
|
||||
def __init__(
|
||||
self, controller: "Controller", window: "OuterWindow", scrot_path: Path
|
||||
) -> None:
|
||||
self.window = window
|
||||
self.controller = controller
|
||||
self.scrot_path = scrot_path
|
||||
|
||||
clock = window.get_frame_clock()
|
||||
if clock is None:
|
||||
raise TypeError("Widget not realized, FrameClock not initialized")
|
||||
clock.connect("after-paint", self.callback)
|
||||
|
||||
def callback(self, clock: Gdk.FrameClock) -> None:
|
||||
GLib.timeout_add(1000, self.check_idle)
|
||||
|
||||
def check_idle(self) -> bool:
|
||||
tv = self.controller.get_active_treeview()
|
||||
loaded = tv.is_loaded()
|
||||
if loaded is True:
|
||||
self.emit_key_press(tv)
|
||||
GLib.timeout_add(1000, self.save_and_quit)
|
||||
return True
|
||||
return False
|
||||
|
||||
def emit_key_press(self, widget: "ContextMixin") -> None:
|
||||
window = widget.get_parent_window()
|
||||
if window is None:
|
||||
raise TypeError(f"Failed to find a valid parent window for {widget}")
|
||||
event = Gdk.EventKey.new(Gdk.EventType.KEY_PRESS)
|
||||
# NOTE: GTK introspection returns Gdk.Event type
|
||||
event_key = cast(Gdk.EventKey, event)
|
||||
event_key.keyval = Gdk.KEY_l
|
||||
event_key.state = Gdk.ModifierType.CONTROL_MASK
|
||||
event_key.window = window
|
||||
event_key.time = Gdk.CURRENT_TIME
|
||||
event_key.send_event = True
|
||||
# TODO: suppress errors from synthetic key events
|
||||
widget.present_menu(widget, event)
|
||||
|
||||
def save_and_quit(self) -> None:
|
||||
self.save_pixbuf()
|
||||
self.controller.save_res_and_quit()
|
||||
|
||||
def save_pixbuf(self) -> None:
|
||||
pixbuf = self.capture_application()
|
||||
if pixbuf:
|
||||
pixbuf.savev(str(self.scrot_path), "png", [], [])
|
||||
|
||||
def capture_application(self) -> "GdkPixbuf.Pixbuf":
|
||||
# NOTE: composited context menu over X11 window
|
||||
root = Gdk.get_default_root_window()
|
||||
|
||||
xwindow = self.window.get_window()
|
||||
if xwindow is None:
|
||||
raise ValueError("Failed to get a valid X11 window")
|
||||
index, x, y = xwindow.get_origin()
|
||||
width, height = self.window.get_size()
|
||||
|
||||
application = Gdk.pixbuf_get_from_window(root, x, y, width, height)
|
||||
if application is None:
|
||||
raise ValueError("Failed to get a valid GdkPixbuf")
|
||||
return application
|
||||
@@ -31,6 +31,7 @@ show_details = "Show server details"
|
||||
refresh_players = "Refresh player count"
|
||||
open_workshop = "Open in Steam Workshop"
|
||||
unsub_mod = "Unsubscribe mod"
|
||||
validate_mod = "Validate mod"
|
||||
copy_name = "Copy name to clipboard"
|
||||
copy_ip = "Copy IP to clipboard"
|
||||
copy_log = "Copy record(s) to clipboard"
|
||||
@@ -200,6 +201,8 @@ class ModPanelStrings:
|
||||
clear_highlights: str
|
||||
select_stale: str
|
||||
select_stale_tooltip: str
|
||||
validate: str
|
||||
validate_tooltip: str
|
||||
|
||||
|
||||
@dataclass(slots=True, frozen=True)
|
||||
@@ -297,7 +300,7 @@ class Options:
|
||||
init = Init(
|
||||
is_steam_running="Is Steam running? DZGUI must be run on top of Steam.",
|
||||
is_dayz_running="Is DayZ already running? DZGUI cannot launch DayZ if another process is using it.",
|
||||
requires_steam="DZGUI requires that Steam or Flatpak Steam be installed on the system.",
|
||||
requires_steam="Your chosen client (Steam/Flatpak Steam) must be installed on the system.",
|
||||
)
|
||||
|
||||
mod_panel = ModPanelStrings(
|
||||
@@ -319,6 +322,8 @@ mod_panel = ModPanelStrings(
|
||||
clear_highlights="Clears highlights and reverts the table to a default state",
|
||||
select_stale="Select stale",
|
||||
select_stale_tooltip="Only selects highlighted stale mods",
|
||||
validate="Validate selected",
|
||||
validate_tooltip="Validates selected mods and updates them if necessary",
|
||||
)
|
||||
|
||||
|
||||
|
||||
+20
-2
@@ -1,7 +1,7 @@
|
||||
import logging
|
||||
import signal
|
||||
|
||||
from typing import TYPE_CHECKING, Literal, Self
|
||||
from typing import Any, Literal, Self, TYPE_CHECKING
|
||||
|
||||
from dzgui.const.constants import (
|
||||
APP_NAME,
|
||||
@@ -47,7 +47,7 @@ logger = logging.getLogger(APP_NAME)
|
||||
|
||||
|
||||
class OuterWindow(Gtk.ApplicationWindow):
|
||||
def __init__(self, app: Gtk.Application) -> None:
|
||||
def __init__(self, app: "App") -> None:
|
||||
super().__init__(
|
||||
application=app, title=APP_NAME, border_width=10, icon_name=APP_NAME_LOWER
|
||||
)
|
||||
@@ -78,6 +78,15 @@ class OuterWindow(Gtk.ApplicationWindow):
|
||||
MainController.loaded = True
|
||||
MainController.populate_model(MainController.get_active_treeview())
|
||||
|
||||
try:
|
||||
from dzgui.util.save_screenshot import IdleScreenshot
|
||||
|
||||
scrot = app.get_automation_param("screenshot_path")
|
||||
self.fullscreen()
|
||||
IdleScreenshot(MainController, self, scrot)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def _on_delete_event(self, window: "OuterWindow", event: Gdk.EventKey) -> None:
|
||||
self.halt_proc_and_quit()
|
||||
|
||||
@@ -291,6 +300,14 @@ class App(ColorAwareApp, Gtk.Application): # type: ignore
|
||||
self._setup_signals()
|
||||
self.connect("activate", self._on_activate)
|
||||
|
||||
self.automation_params: dict[str, Any] = {}
|
||||
|
||||
def get_automation_param(self, key: str) -> Any:
|
||||
return self.automation_params[key]
|
||||
|
||||
def set_automation_param(self, key: str, value: Any) -> None:
|
||||
self.automation_params[key] = value
|
||||
|
||||
def _on_activate(self, app: Self) -> None:
|
||||
self.win = OuterWindow(app)
|
||||
|
||||
@@ -331,4 +348,5 @@ class App(ColorAwareApp, Gtk.Application): # type: ignore
|
||||
) -> None:
|
||||
self.win.halt_proc_and_quit()
|
||||
|
||||
|
||||
MainController = Controller()
|
||||
@@ -1,10 +1,8 @@
|
||||
from typing import Callable, Literal, Self, TYPE_CHECKING, Union
|
||||
|
||||
from dzgui.util.clip import copy_clipboard
|
||||
from dzgui.util.format import pluralize
|
||||
from dzgui.strings import buttons
|
||||
from dzgui.util.strings import (
|
||||
alert_button_tooltip,
|
||||
atomic_buttons,
|
||||
)
|
||||
from dzgui.const.constants import (
|
||||
@@ -14,7 +12,6 @@ from dzgui.const.constants import (
|
||||
LIST_ADD,
|
||||
REFRESH_ICON,
|
||||
STEAM_ICON,
|
||||
WARNING,
|
||||
WEB_BROWSER,
|
||||
)
|
||||
from dzgui.strings import connect_panel
|
||||
@@ -64,7 +61,7 @@ class IconButton(Gtk.Button):
|
||||
self.icon = Icon(icon, margin_start=margin_start, margin_end=margin_end)
|
||||
self.set_image(self.icon)
|
||||
self.set_image_position(position)
|
||||
self.set_focus_on_click(False)
|
||||
self.set_focus_on_click(False) # type: ignore[no-untyped-call]
|
||||
self.set_always_show_image(True)
|
||||
|
||||
def swap_icon(self, icon: str) -> None:
|
||||
@@ -101,15 +98,14 @@ class LargeIconTextButton(IconButton):
|
||||
|
||||
|
||||
class ClipboardButton(IconTextButton):
|
||||
def __init__(self, controller: Union["Controller", None], func: Callable) -> None:
|
||||
def __init__(self, func: Callable[[], str]) -> None:
|
||||
super().__init__(CLIPBOARD, atomic_buttons.copy)
|
||||
self.controller = controller
|
||||
self.connect("clicked", self._on_button_clicked, func)
|
||||
|
||||
# TODO: strings
|
||||
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[[], str]) -> None:
|
||||
def revert() -> Literal[False]:
|
||||
self.revert_icon()
|
||||
return False
|
||||
@@ -120,10 +116,9 @@ class ClipboardButton(IconTextButton):
|
||||
GLib.timeout_add(600, revert)
|
||||
|
||||
|
||||
# TODO: determine when controller would be passed to this button or drop
|
||||
class CopyIpButton(ClipboardButton):
|
||||
def __init__(self, controller: Union["Controller", None], func: Callable) -> None:
|
||||
super().__init__(controller, func)
|
||||
def __init__(self, func: Callable[[], str]) -> None:
|
||||
super().__init__(func)
|
||||
|
||||
# TODO: strings
|
||||
self.set_tooltip_text("Copy IP to clipboard")
|
||||
@@ -163,18 +158,24 @@ class RefreshButton(IconTextButton):
|
||||
self.set_tooltip_text(atomic_buttons.refresh_tooltip)
|
||||
|
||||
self.connect("clicked", self._on_refresh_clicked)
|
||||
emitter.connect("servers_loaded", self.start_decrement)
|
||||
emitter.connect("servers_loaded", self._on_load)
|
||||
emitter.connect("servers_timed_out", self._on_timeout)
|
||||
|
||||
def _on_timeout(self, emitter: "Emitter") -> None:
|
||||
self.is_clicked = True
|
||||
self.start_decrement()
|
||||
|
||||
def _on_load(self, emitter: "Emitter", tab: "ServerTab") -> None:
|
||||
self.start_decrement()
|
||||
|
||||
def _on_refresh_clicked(self, button: Self) -> None:
|
||||
"""Spawned in a thread"""
|
||||
if self.controller.get_active_treeview().is_loaded() is False:
|
||||
return
|
||||
self.is_clicked = True
|
||||
self.controller.refresh_tree()
|
||||
# TODO: get server tab enum
|
||||
# if LAN tab, reload existing entries in place
|
||||
|
||||
def start_decrement(self, emitter: "Emitter", tab: "ServerTab") -> None:
|
||||
def start_decrement(self) -> None:
|
||||
if self.is_clicked is False:
|
||||
return
|
||||
if self.loading:
|
||||
@@ -246,29 +247,6 @@ class AddButton(IconTextButton):
|
||||
self.set_tooltip_text(connect_panel.add_tooltip)
|
||||
|
||||
|
||||
class LoggerAlertsButton(IconTextButton):
|
||||
def __init__(self, warnings: int, errors: int) -> None:
|
||||
warnings_text = ""
|
||||
errors_text = ""
|
||||
separator = ""
|
||||
if warnings > 0:
|
||||
warnings_plural = pluralize("warnings", warnings)
|
||||
warnings_text = f"{warnings} {warnings_plural}"
|
||||
if errors > 0:
|
||||
errors_plural = pluralize("errors", errors)
|
||||
errors_text = f" {errors} {errors_plural}"
|
||||
if warnings + errors > 1:
|
||||
separator = ","
|
||||
concat = warnings_text + separator + errors_text
|
||||
super().__init__(
|
||||
icon=WARNING,
|
||||
label=f"Loaded with{concat}",
|
||||
)
|
||||
self.set_halign(Gtk.Align.END)
|
||||
self.set_hexpand(True)
|
||||
self.set_tooltip_text(alert_button_tooltip)
|
||||
|
||||
|
||||
class SpinnerButton(Gtk.Button):
|
||||
def __init__(self, label: str) -> None:
|
||||
super().__init__()
|
||||
|
||||
@@ -187,7 +187,7 @@ class FavPanel(Gtk.Frame):
|
||||
|
||||
self.fav_button = SteamConnectButton()
|
||||
self.fav_button.connect("clicked", self._on_connect_clicked)
|
||||
self.copy_button = CopyIpButton(self.controller, self.get_fav_ip)
|
||||
self.copy_button = CopyIpButton(self.get_fav_ip)
|
||||
|
||||
# TODO: strings
|
||||
self.unset_button = CloseButton("Unset")
|
||||
|
||||
@@ -38,7 +38,7 @@ class ValidatedEntry(Gtk.Entry):
|
||||
def __init__(
|
||||
self,
|
||||
controller: "Controller",
|
||||
func: Callable,
|
||||
func: Callable[[str], bool],
|
||||
placeholder_text: str = "",
|
||||
tooltip_text: str = "",
|
||||
) -> None:
|
||||
@@ -46,7 +46,6 @@ class ValidatedEntry(Gtk.Entry):
|
||||
hexpand=True, placeholder_text=placeholder_text, tooltip_text=tooltip_text
|
||||
)
|
||||
|
||||
self.func: Callable
|
||||
self.set_validation_func(func)
|
||||
self.controller = controller
|
||||
|
||||
@@ -72,7 +71,7 @@ class ValidatedEntry(Gtk.Entry):
|
||||
self.emit("string_validated", False)
|
||||
remove_class(self, self.classname)
|
||||
|
||||
def set_validation_func(self, func: Callable) -> None:
|
||||
def set_validation_func(self, func: Callable[[str], bool]) -> None:
|
||||
self.func = func
|
||||
|
||||
def insert_icon(self) -> None:
|
||||
@@ -141,7 +140,7 @@ class APIEntry(Gtk.Box):
|
||||
def __init__(self) -> None:
|
||||
super().__init__(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
|
||||
|
||||
self.func: Callable | None = None
|
||||
self.func: Callable[[str], None] | None = None
|
||||
|
||||
# TODO: strings
|
||||
self.entry = Gtk.Entry(
|
||||
@@ -222,7 +221,7 @@ class APIEntry(Gtk.Box):
|
||||
res = self.func(text)
|
||||
return res
|
||||
|
||||
def set_validation_func(self, func: Callable | None) -> None:
|
||||
def set_validation_func(self, func: Callable[[str], None] | None) -> None:
|
||||
self.func = func
|
||||
|
||||
def disable_button(self) -> None:
|
||||
|
||||
@@ -30,7 +30,7 @@ class ButtonGrid(Gtk.Grid):
|
||||
def __init__(
|
||||
self,
|
||||
controller: "Controller",
|
||||
defaults: dict[str, str],
|
||||
defaults: dict[str, bool],
|
||||
tooltips: dict[str, str],
|
||||
) -> None:
|
||||
super().__init__(
|
||||
@@ -42,7 +42,10 @@ class ButtonGrid(Gtk.Grid):
|
||||
self.checks: list[Gtk.CheckButton] = []
|
||||
|
||||
flowbox = Gtk.FlowBox(
|
||||
halign=Gtk.Align.CENTER, min_children_per_line=3, max_children_per_line=3, selection_mode=Gtk.SelectionMode.NONE
|
||||
halign=Gtk.Align.CENTER,
|
||||
min_children_per_line=3,
|
||||
max_children_per_line=3,
|
||||
selection_mode=Gtk.SelectionMode.NONE,
|
||||
)
|
||||
|
||||
for check in defaults.keys():
|
||||
@@ -93,7 +96,7 @@ class ButtonGrid(Gtk.Grid):
|
||||
logger.info(f"User toggled button '{label}' to {state}")
|
||||
self.emitter.emit("check_toggled", label, state)
|
||||
|
||||
def get_checkboxes(self) -> list:
|
||||
def get_checkboxes(self) -> list[Gtk.CheckButton]:
|
||||
return self.checks
|
||||
|
||||
|
||||
@@ -305,6 +308,7 @@ class FilterPanel(Gtk.Box):
|
||||
Gdk.KEY_0: 9,
|
||||
Gdk.KEY_minus: 10,
|
||||
Gdk.KEY_backslash: 11,
|
||||
Gdk.KEY_apostrophe: 12,
|
||||
}
|
||||
if keyval not in mappings:
|
||||
return False
|
||||
@@ -313,6 +317,8 @@ class FilterPanel(Gtk.Box):
|
||||
return True
|
||||
|
||||
def toggle_check(self, digit: int) -> None:
|
||||
if not self.button_grid.is_sensitive():
|
||||
return
|
||||
checks = self.button_grid.get_checkboxes()
|
||||
check = checks[digit]
|
||||
state = check.get_active()
|
||||
|
||||
@@ -50,6 +50,7 @@ class ModSelectionPanel(Gtk.Box):
|
||||
ModButton.SELECT_ALL,
|
||||
ModButton.UNSELECT_ALL,
|
||||
ModButton.UNSUB_SELECTED,
|
||||
ModButton.VALIDATE,
|
||||
)
|
||||
for button in buttons:
|
||||
b = EnumeratedModButton(button)
|
||||
@@ -114,3 +115,5 @@ class ModSelectionPanel(Gtk.Box):
|
||||
self.swap_sensitive(False)
|
||||
case ModButton.SELECT_STALE:
|
||||
self.controller.select_colorized()
|
||||
case ModButton.VALIDATE:
|
||||
self.controller.validate_mods()
|
||||
@@ -2,7 +2,6 @@ from typing import Self, Union, TYPE_CHECKING
|
||||
|
||||
from dzgui.const.enum import NotebookPage, ServerTab
|
||||
from dzgui.util.strings import esc_to_return, question_to_return
|
||||
from dzgui.views.components.buttons import LoggerAlertsButton
|
||||
|
||||
import gi
|
||||
|
||||
@@ -47,9 +46,6 @@ class Statusbar(Gtk.Grid):
|
||||
self.emitter.connect("servers_loaded", self._on_servers_loaded)
|
||||
self.emitter.connect("mods_updated", self._on_mods_updated)
|
||||
|
||||
def _on_alerts_clicked(self, button: LoggerAlertsButton) -> None:
|
||||
self.controller.populate_log()
|
||||
|
||||
def _on_mods_updated(self, emitter: "Emitter", msg: str, mods: int) -> None:
|
||||
self.set_by_context(NotebookPage.MODS, msg)
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ class BootDialog(ColorAwareApp, Gtk.Dialog): # type: ignore
|
||||
self.error_label = Gtk.Label(margin_start=15, margin_end=15)
|
||||
|
||||
# TODO: abstract class
|
||||
self.copy_button = ClipboardButton(None, lambda: self.error_label.get_text())
|
||||
self.copy_button = ClipboardButton(self.error_label.get_text)
|
||||
self.button_hbox = Gtk.Box(
|
||||
orientation=Gtk.Orientation.HORIZONTAL, halign=Gtk.Align.CENTER, spacing=10
|
||||
)
|
||||
@@ -132,9 +132,9 @@ class BootDialog(ColorAwareApp, Gtk.Dialog): # type: ignore
|
||||
preboot.signatures,
|
||||
False,
|
||||
),
|
||||
(StoredFunc(get_ipdb, self.xdg.ips), preboot.geo, False),
|
||||
(StoredFunc(get_ipdb, self.xdg.db), preboot.geo, False),
|
||||
# TODO: handle IP DB failure and use coords fallback
|
||||
(StoredFunc(get_local_coords, self.xdg.ips), preboot.coords, True),
|
||||
(StoredFunc(get_local_coords, self.xdg.db), preboot.coords, True),
|
||||
(StoredFunc(check_updates, self.version), preboot.updates, True),
|
||||
]
|
||||
self.results: list[Any] = []
|
||||
|
||||
@@ -10,7 +10,7 @@ from dzgui.views.components.labels import BoldLabel
|
||||
from dzgui.views.components.box import VBox, HBox
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk, GLib, GObject # type: ignore # noqa E402
|
||||
from gi.repository import Gtk, GLib, GObject # noqa E402
|
||||
|
||||
DAY_START_TIME = time(hour=6, minute=0, second=0)
|
||||
DAY_END_TIME = time(hour=18, minute=0, second=0)
|
||||
|
||||
@@ -46,7 +46,7 @@ class AbortDialog(ColorAwareApp, Gtk.MessageDialog): # type: ignore
|
||||
|
||||
self.connect("response", self._on_response)
|
||||
|
||||
self.run()
|
||||
self.run() # type: ignore[no-untyped-call]
|
||||
self.destroy()
|
||||
|
||||
def _on_response(self, dialog: Self, response: Gtk.ResponseType) -> None:
|
||||
|
||||
@@ -16,14 +16,14 @@ class FilePicker(Gtk.FileChooserDialog):
|
||||
transient_for=parent,
|
||||
resizable=True,
|
||||
)
|
||||
self.add_buttons("_Cancel", Gtk.ResponseType.CANCEL)
|
||||
self.add_buttons("_Save", Gtk.ResponseType.OK)
|
||||
self.add_buttons("_Cancel", Gtk.ResponseType.CANCEL) # type: ignore[no-untyped-call]
|
||||
self.add_buttons("_Save", Gtk.ResponseType.OK) # type: ignore[no-untyped-call]
|
||||
self.set_default_response(Gtk.ResponseType.OK)
|
||||
|
||||
self.set_current_name(picker.placeholder)
|
||||
|
||||
def pick_file(self) -> Path | None:
|
||||
res = self.run()
|
||||
res = self.run() # type: ignore[no-untyped-call]
|
||||
if res in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
|
||||
self.destroy()
|
||||
return None
|
||||
@@ -43,12 +43,12 @@ class FolderPicker(Gtk.FileChooserDialog):
|
||||
parent=parent,
|
||||
resizable=True,
|
||||
)
|
||||
self.add_buttons("_Cancel", Gtk.ResponseType.CANCEL)
|
||||
self.add_buttons("_OK", Gtk.ResponseType.OK)
|
||||
self.add_buttons("_Cancel", Gtk.ResponseType.CANCEL) # type: ignore[no-untyped-call]
|
||||
self.add_buttons("_OK", Gtk.ResponseType.OK) # type: ignore[no-untyped-call]
|
||||
self.set_default_response(Gtk.ResponseType.OK)
|
||||
|
||||
def pick_folder(self) -> Path | None:
|
||||
res = self.run()
|
||||
res = self.run() # type: ignore[no-untyped-call]
|
||||
if res in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
|
||||
self.destroy()
|
||||
return None
|
||||
|
||||
@@ -187,7 +187,7 @@ class TextBufferDialog(GenericDialog):
|
||||
secondary=secondary,
|
||||
)
|
||||
self.text = text
|
||||
copy_button = ClipboardButton(controller, self.get_text)
|
||||
copy_button = ClipboardButton(self.get_text)
|
||||
self.add_action_widget(copy_button, Gtk.ResponseType.NONE)
|
||||
self.add_button("OK", Gtk.ResponseType.OK)
|
||||
self.connect("response", self._on_response)
|
||||
|
||||
@@ -22,7 +22,7 @@ from gi.repository import Gtk, GLib, Pango # noqa E402
|
||||
|
||||
|
||||
class Assistant(Gtk.Assistant):
|
||||
def __init__(self, is_deck: bool, paths: dict[str, str]):
|
||||
def __init__(self, is_deck: bool, paths: dict[str, Path]):
|
||||
super().__init__()
|
||||
if is_deck:
|
||||
# NOTE: deemed to be "safe" dimensions that exclude taskbar size
|
||||
@@ -63,7 +63,7 @@ class Assistant(Gtk.Assistant):
|
||||
|
||||
|
||||
class Window(Gtk.Window):
|
||||
def __init__(self, is_deck: bool, paths: dict[str, str]) -> None:
|
||||
def __init__(self, is_deck: bool, paths: dict[str, Path]) -> None:
|
||||
super().__init__(title=APP_NAME, icon_name=APP_NAME)
|
||||
self.assistant = Assistant(is_deck, paths)
|
||||
|
||||
@@ -121,7 +121,7 @@ class CheckboxWithPath(CheckboxWithLabel):
|
||||
|
||||
|
||||
class UninstallPage(ScrolledWizardPage):
|
||||
def __init__(self, paths: dict[str, str], pyapp: str | None):
|
||||
def __init__(self, paths: dict[str, Path], pyapp: str | None):
|
||||
super().__init__(heading=uninstall.pg1_heading, description=uninstall.pg1_blurb)
|
||||
|
||||
self.pyapp = pyapp
|
||||
@@ -219,7 +219,7 @@ class UninstallPage(ScrolledWizardPage):
|
||||
|
||||
|
||||
class UninstallWizard(ColorAwareApp, Gtk.Application): # type: ignore
|
||||
def __init__(self, is_deck: bool, paths: dict[str, str]) -> None:
|
||||
def __init__(self, is_deck: bool, paths: dict[str, Path]) -> None:
|
||||
super().__init__()
|
||||
config = Path(paths["XDG_CONFIG_HOME"])
|
||||
if not config.is_dir() or not config.joinpath("config.json").is_file():
|
||||
|
||||
@@ -14,6 +14,7 @@ from dzgui.const.constants import (
|
||||
HERO_PATH,
|
||||
FLATPAK_ROOT_PATH,
|
||||
LEGACY_CONFIG_PATH,
|
||||
TMP_PATH,
|
||||
)
|
||||
from dzgui.const.boilerplate import config_boilerplate
|
||||
from dzgui.const.endpoints import STEAM_API_SETUP
|
||||
@@ -167,7 +168,12 @@ class NotificationFrame(Gtk.Frame):
|
||||
|
||||
class APIValidationPage(EnumeratedWizardPage):
|
||||
def __init__(
|
||||
self, enum: PageNum, heading: str, description: str, link: str, func: Callable
|
||||
self,
|
||||
enum: PageNum,
|
||||
heading: str,
|
||||
description: str,
|
||||
link: str,
|
||||
func: Callable[[str], None],
|
||||
) -> None:
|
||||
super().__init__(
|
||||
enum=enum,
|
||||
@@ -420,7 +426,7 @@ class PreferencesPage(EnumeratedWizardPage):
|
||||
|
||||
|
||||
class FlatpakOverridePage(EnumeratedWizardPage):
|
||||
def __init__(self, xdg_paths: dict[str, str]) -> None:
|
||||
def __init__(self, xdg_paths: dict[str, Path]) -> None:
|
||||
super().__init__(
|
||||
enum=PageNum.FLATPAK,
|
||||
heading=wizard.heading_flatpak,
|
||||
@@ -432,6 +438,8 @@ class FlatpakOverridePage(EnumeratedWizardPage):
|
||||
store = Gtk.ListStore(str)
|
||||
for path in xdg_paths.values():
|
||||
store.append((str(path),))
|
||||
store.append((TMP_PATH,))
|
||||
|
||||
tree = SimpleTree(store, wizard.tree_column)
|
||||
tree.set_halign(Gtk.Align.CENTER)
|
||||
|
||||
@@ -468,7 +476,7 @@ class CompletionPage(EnumeratedWizardPage):
|
||||
|
||||
|
||||
class Assistant(Gtk.Assistant):
|
||||
def __init__(self, is_deck: bool, XDG: "Xdg", xdg_paths: dict[str, str]):
|
||||
def __init__(self, is_deck: bool, XDG: "Xdg", xdg_paths: dict[str, Path]):
|
||||
super().__init__()
|
||||
if is_deck:
|
||||
# NOTE: deemed to be "safe" dimensions that exclude taskbar size
|
||||
@@ -773,7 +781,7 @@ class SteamPathPage(EnumeratedWizardPage):
|
||||
|
||||
|
||||
class SetupWizard(ColorAwareApp, Gtk.Application): # type: ignore
|
||||
def __init__(self, is_deck: bool, XDG: "Xdg", xdg_paths: dict[str, str]) -> None:
|
||||
def __init__(self, is_deck: bool, XDG: "Xdg", xdg_paths: dict[str, Path]) -> None:
|
||||
super().__init__()
|
||||
GLib.set_prgname(APP_NAME)
|
||||
self.win = Window(is_deck, XDG, xdg_paths)
|
||||
@@ -784,7 +792,7 @@ class SetupWizard(ColorAwareApp, Gtk.Application): # type: ignore
|
||||
|
||||
|
||||
class Window(Gtk.Window):
|
||||
def __init__(self, is_deck: bool, XDG: "Xdg", xdg_paths: dict[str, str]) -> None:
|
||||
def __init__(self, is_deck: bool, XDG: "Xdg", xdg_paths: dict[str, Path]) -> None:
|
||||
super().__init__(title=APP_NAME, icon_name=APP_NAME)
|
||||
self.assistant = Assistant(is_deck, XDG, xdg_paths)
|
||||
|
||||
|
||||
@@ -15,11 +15,7 @@ logger = logging.getLogger(APP_NAME)
|
||||
|
||||
|
||||
class ContextMixin(TreeView):
|
||||
def present_menu(
|
||||
self,
|
||||
widget: Gtk.Widget,
|
||||
event: Gdk.EventButton | Gdk.EventKey,
|
||||
) -> bool:
|
||||
def present_menu(self, widget: Gtk.Widget, event: Gdk.Event) -> bool:
|
||||
|
||||
if self.menu is None:
|
||||
return False
|
||||
@@ -28,7 +24,7 @@ class ContextMixin(TreeView):
|
||||
|
||||
match event.type:
|
||||
case Gdk.EventType.BUTTON_PRESS:
|
||||
if event.button != 3:
|
||||
if event.button != Gdk.BUTTON_SECONDARY: # type: ignore[comparison-overlap]
|
||||
return False
|
||||
self._process_button_event(event) # type: ignore
|
||||
case Gdk.EventType.KEY_PRESS:
|
||||
|
||||
@@ -13,6 +13,7 @@ if TYPE_CHECKING:
|
||||
from dzgui.controllers.mc import Controller
|
||||
|
||||
|
||||
# TODO: refactor into autopopulated ListStores
|
||||
class Keybindings(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore
|
||||
"""
|
||||
Notebook page holding a prearranged grid
|
||||
@@ -35,7 +36,7 @@ class Keybindings(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore
|
||||
|
||||
self.connect("key-press-event", self._on_keypress)
|
||||
|
||||
def build_keys(self, items: list) -> Gtk.Grid:
|
||||
def build_keys(self, items: list[dict[str, str]]) -> Gtk.Grid:
|
||||
grid = Gtk.Grid(row_spacing=10, column_homogeneous=True)
|
||||
grid.set_halign(Gtk.Align.START)
|
||||
row, col = 1, 0
|
||||
@@ -72,7 +73,7 @@ class Keybindings(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore
|
||||
row += 1
|
||||
return grid
|
||||
|
||||
def build_sidebar(self, categories: list) -> Gtk.Grid:
|
||||
def build_sidebar(self, categories: list[str]) -> Gtk.Grid:
|
||||
row, col = 0, 0
|
||||
w, h = 1, 1
|
||||
sidebar = Gtk.Grid(row_homogeneous=True, orientation=Gtk.Orientation.VERTICAL)
|
||||
@@ -83,7 +84,7 @@ class Keybindings(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore
|
||||
sidebar.attach(label, col, row, w, h)
|
||||
return sidebar
|
||||
|
||||
def build_grid(self, items: list) -> Gtk.Grid:
|
||||
def build_grid(self, items: list[dict[str, str]]) -> Gtk.Grid:
|
||||
grid = Gtk.Grid(
|
||||
row_spacing=20,
|
||||
halign=Gtk.Align.CENTER,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
from pathlib import Path
|
||||
from typing import Callable, TYPE_CHECKING
|
||||
from typing import Any, Callable, TYPE_CHECKING
|
||||
|
||||
from dzgui.api import pefile as PeFile
|
||||
from dzgui.api.steam import get_last_updated
|
||||
|
||||
from dzgui.config import query
|
||||
from dzgui.const.constants import (
|
||||
@@ -54,7 +55,7 @@ class SubmitField(Gtk.Box):
|
||||
) -> None:
|
||||
super().__init__(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
|
||||
|
||||
self.callback: Callable
|
||||
self.callback: Callable[[], None]
|
||||
self.controller = controller
|
||||
self.context = context
|
||||
|
||||
@@ -142,7 +143,7 @@ class SubmitField(Gtk.Box):
|
||||
return
|
||||
self.save_option()
|
||||
|
||||
def set_callback(self, callback: Callable) -> None:
|
||||
def set_callback(self, callback: Callable[[], None]) -> None:
|
||||
self.callback = callback
|
||||
|
||||
def save_option(self) -> None:
|
||||
@@ -209,7 +210,7 @@ class NameSubmitField(SubmitField):
|
||||
Preferences.NAME,
|
||||
private=False,
|
||||
)
|
||||
self.entry.set_width_chars(30) # type: ignore
|
||||
self.entry.set_width_chars(30)
|
||||
self.set_callback(self.save_player_name)
|
||||
|
||||
def save_player_name(self) -> None:
|
||||
@@ -340,8 +341,10 @@ class Options(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore
|
||||
[LeftLabel(options.flatpak_label), self.flatpak_toggle],
|
||||
)
|
||||
|
||||
self.dayz_version_label = Gtk.Label(label=strings.null)
|
||||
self.dayz_exp_version_label = Gtk.Label(label=strings.null)
|
||||
self.dayz_version_label = Gtk.Label(label=strings.null, halign=Gtk.Align.START)
|
||||
self.dayz_exp_version_label = Gtk.Label(
|
||||
label=strings.null, halign=Gtk.Align.START
|
||||
)
|
||||
|
||||
version_rows = [
|
||||
[LeftLabel(APPNAME_DAYZ), self.dayz_version_label],
|
||||
@@ -403,7 +406,8 @@ class Options(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore
|
||||
def _on_link_button_clicked(self, button: Gtk.Button, url: str) -> None:
|
||||
open_links.open_link_by_url(url)
|
||||
|
||||
def _make_grid(self, rows: list) -> Gtk.Grid:
|
||||
# TODO: use FlowBox
|
||||
def _make_grid(self, rows: list[list[Any]]) -> Gtk.Grid:
|
||||
grid = Gtk.Grid(
|
||||
orientation=Gtk.Orientation.VERTICAL,
|
||||
column_spacing=10,
|
||||
@@ -436,7 +440,7 @@ class Options(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore
|
||||
if prefs.paths.config.is_file() is False:
|
||||
dialog = ExceptionDialog(self.controller, strings.config_not_found)
|
||||
dialog.show_all()
|
||||
dialog.run()
|
||||
dialog.run() # type: ignore[no-untyped-call]
|
||||
raise OSError(f"Config file '{prefs.paths.config}' not found")
|
||||
|
||||
config = query.get_config(prefs.paths.config)
|
||||
@@ -467,10 +471,18 @@ class Options(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore
|
||||
dayz_version = PeFile.get_pretty_version(steam_path, APPID_DAYZ)
|
||||
if dayz_version is None:
|
||||
dayz_version = strings.null
|
||||
else:
|
||||
last_updated = get_last_updated(steam_path, APPID_DAYZ)
|
||||
if last_updated is not None:
|
||||
dayz_version += f" (updated at {last_updated})"
|
||||
|
||||
dayz_exp_version = PeFile.get_pretty_version(steam_path, APPID_DAYZ_EXP)
|
||||
if dayz_exp_version is None:
|
||||
dayz_exp_version = strings.null
|
||||
else:
|
||||
last_updated = get_last_updated(steam_path, APPID_DAYZ_EXP)
|
||||
if last_updated is not None:
|
||||
dayz_exp_version += f" (updated at {last_updated})"
|
||||
|
||||
self.dayz_version_label.set_text(dayz_version)
|
||||
self.dayz_exp_version_label.set_text(dayz_exp_version)
|
||||
|
||||
@@ -231,7 +231,7 @@ class PreConnectionAssistant(Gtk.Box):
|
||||
args = self.controller.get_debug_args()
|
||||
d = DebugDialog(self.controller, args)
|
||||
d.show_all()
|
||||
d.run()
|
||||
d.run() # type: ignore[no-untyped-call]
|
||||
|
||||
def _on_ok_clicked(self, button: Gtk.Button) -> None:
|
||||
self.controller.update_and_connect()
|
||||
|
||||
@@ -79,11 +79,6 @@ class TreeView(CursorMixin, Gtk.TreeView): # type: ignore
|
||||
index = path.get_indices()[0]
|
||||
return index
|
||||
|
||||
def get_selected_records(self) -> list:
|
||||
sel = self.get_selection()
|
||||
model, rows = sel.get_selected_rows()
|
||||
return [model[row] for row in rows]
|
||||
|
||||
def _on_keypress(self, treeview: Gtk.TreeView, event: Gdk.EventKey) -> bool:
|
||||
from dzgui.views.trees.tree_servers import ServerTreeView
|
||||
|
||||
@@ -108,14 +103,9 @@ class TreeView(CursorMixin, Gtk.TreeView): # type: ignore
|
||||
|
||||
def _on_key_release(self, treeview: Gtk.TreeView, event: Gdk.EventKey) -> None:
|
||||
"""
|
||||
Suppresses spamming on keydown
|
||||
TODO: explain this better
|
||||
Adds debounce to key press/release events to prevent spamming
|
||||
when rapidly navigating through records
|
||||
"""
|
||||
# TODO: multisel
|
||||
# if event.keyval is Gdk.KEY_space:
|
||||
# it = self.get_focused_row_iter()
|
||||
# self.get_selection().select_iter(it)
|
||||
# return True
|
||||
|
||||
model = self.get_model()
|
||||
if model is None:
|
||||
@@ -150,9 +140,11 @@ class TreeView(CursorMixin, Gtk.TreeView): # type: ignore
|
||||
self.set_cursor(path)
|
||||
|
||||
def get_value_at_index(self, index: int) -> Any:
|
||||
(model, pathlist) = self.get_model_and_pathlist()
|
||||
model, pathlist = self.get_model_and_pathlist()
|
||||
if len(pathlist) < 1:
|
||||
return ""
|
||||
if model is None:
|
||||
return ""
|
||||
path = pathlist[0]
|
||||
tree_iter = model.get_iter(path)
|
||||
value = model.get_value(tree_iter, index)
|
||||
@@ -166,20 +158,16 @@ class TreeView(CursorMixin, Gtk.TreeView): # type: ignore
|
||||
sel = self.get_selection()
|
||||
self._on_tree_selection_changed(sel)
|
||||
|
||||
def get_model_and_pathlist(self) -> tuple:
|
||||
def get_model_and_pathlist(
|
||||
self,
|
||||
) -> tuple[Gtk.TreeModel | None, list[Gtk.TreePath]]:
|
||||
select = self.get_selection()
|
||||
sels = select.get_selected_rows()
|
||||
(model, pathlist) = sels
|
||||
model: Gtk.TreeModel | None
|
||||
pathlist: list[Gtk.TreePath]
|
||||
model, pathlist = sels
|
||||
return (model, pathlist)
|
||||
|
||||
# @deprecated("Currently unused")
|
||||
# def get_mpath(self) -> Optional[Gtk.TreePath]:
|
||||
# (model, pathlist) = self.get_model_and_pathlist()
|
||||
# if len(pathlist) < 1:
|
||||
# return None
|
||||
# path = pathlist[0]
|
||||
# return path
|
||||
|
||||
def _on_row_activated(
|
||||
self,
|
||||
treeview: Gtk.TreeView,
|
||||
@@ -194,15 +182,6 @@ class TreeView(CursorMixin, Gtk.TreeView): # type: ignore
|
||||
return True
|
||||
return False
|
||||
|
||||
# @deprecated("unused")
|
||||
# def get_selected_row(self) -> Optional[Gtk.TreeModelRow]:
|
||||
# ind = self.get_selected_row_index()
|
||||
# model = self.get_model()
|
||||
# if model is None:
|
||||
# return None
|
||||
# row = model[ind]
|
||||
# return row
|
||||
|
||||
def get_col_value_by_path_index(self, path: Gtk.TreePath, index: int) -> Any:
|
||||
model = self.get_model()
|
||||
if model is None:
|
||||
|
||||
@@ -32,6 +32,7 @@ if TYPE_CHECKING:
|
||||
from dzgui.controllers.mc import Controller
|
||||
from dzgui.controllers.emitter import Emitter
|
||||
from dzgui.model.model_factory import FastInsertListStore
|
||||
from dzgui.util.dist import Haversine
|
||||
|
||||
QUEUE_CHECK_DELAY = 200
|
||||
|
||||
@@ -64,7 +65,7 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore
|
||||
|
||||
self.queue_id: int
|
||||
self.handler_id: int
|
||||
self.queue: Queue = Queue()
|
||||
self.queue: Queue[tuple[str, "Haversine" | None, ServerTab]] = Queue()
|
||||
|
||||
self.seen_cache: list[Record] = []
|
||||
|
||||
@@ -406,7 +407,7 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore
|
||||
path = model.get_path(it)
|
||||
row_index = path.get_indices()[0]
|
||||
try:
|
||||
start, end = self.get_visible_range()
|
||||
start, end = self.get_visible_range() # type: ignore[no-untyped-call]
|
||||
except Exception:
|
||||
return
|
||||
if row_index >= start[0] <= end[0]:
|
||||
|
||||
+4
-3
@@ -4,7 +4,7 @@ description = "DayZ server browser and mod manager for Linux"
|
||||
authors = [
|
||||
{name = "aclist"}
|
||||
]
|
||||
version = "7.0.0b28"
|
||||
version = "7.0.0b30"
|
||||
license = "GPL-3.0-or-later"
|
||||
license-files = ["LICENSE"]
|
||||
readme = "README.md"
|
||||
@@ -104,16 +104,17 @@ python-a2s = { path = "./dzgui/lib/a2s", editable = true }
|
||||
dayzquery = { path = "./dzgui/lib/dayzquery", editable = true }
|
||||
|
||||
[tool.uv]
|
||||
config-settings-package = { pygobject-stubs = { config = "Gtk3,Gdk3,Soup2" } }
|
||||
config-settings-package = { pygobject-stubs = { config = "Gtk3,Gdk3" } }
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
markers = [
|
||||
"apitest: checks remote endpoints",
|
||||
"config: config file keys/values",
|
||||
"realconfig: depends on actual generated local config file",
|
||||
"flatpak: depends on local flatpak installation",
|
||||
"mods: tests mod metadata/link creation",
|
||||
"pefile: validate PE files",
|
||||
"post_install: requires a completed installation",
|
||||
"realconfig: depends on actual generated local config file",
|
||||
"redact: log redaction mechanisms",
|
||||
"slow: long-running tests",
|
||||
"webtest: depends on remote endpoint",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from pathlib import Path
|
||||
from shared import get_git_root
|
||||
|
||||
BOILERPLATE = """
|
||||
This file includes the combined licenses to the following components:
|
||||
@@ -40,7 +40,7 @@ class CombinedLicense:
|
||||
|
||||
def concat_license() -> str:
|
||||
combined = CombinedLicense()
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
root = get_git_root()
|
||||
licenses = root.joinpath("docs/licenses")
|
||||
|
||||
main_license = root.joinpath(LICENSE_FILE)
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
par = Path(__file__).parent.parent
|
||||
clog = par / "CHANGELOG.md"
|
||||
target = par / "dzgui/data/CHANGELOG.md"
|
||||
from shared import get_git_root
|
||||
|
||||
root = get_git_root()
|
||||
clog = root.joinpath("CHANGELOG.md")
|
||||
target = root.joinpath("dzgui/data/CHANGELOG.md")
|
||||
|
||||
shutil.copy(clog, target)
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
from shared import get_git_root
|
||||
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
|
||||
def get_entrypoint() -> None:
|
||||
root = get_git_root()
|
||||
p = root.joinpath("pyproject.toml")
|
||||
with open(p, "rb") as f:
|
||||
t = tomllib.load(f)
|
||||
|
||||
+3
-2
@@ -3,14 +3,15 @@ import requests
|
||||
import subprocess
|
||||
import tarfile
|
||||
|
||||
from pathlib import Path
|
||||
from urllib.parse import unquote
|
||||
|
||||
from shared import get_git_root
|
||||
|
||||
BUILD = "3.13.9"
|
||||
DATE = "20251014"
|
||||
CPYTHON_BUILD = f"cpython-{BUILD}"
|
||||
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
root = get_git_root()
|
||||
build_dir = root.joinpath("build")
|
||||
cpython_dir = build_dir.joinpath(CPYTHON_BUILD)
|
||||
|
||||
|
||||
+2
-1
@@ -9,6 +9,7 @@ from pathlib import Path
|
||||
from concat_licenses import concat_license
|
||||
from parse_toml import get_entrypoint
|
||||
from prebuild import get_pyapp, rebuild_cpython
|
||||
from shared import get_git_root
|
||||
|
||||
|
||||
def update_uname(info: tarfile.TarInfo) -> tarfile.TarInfo:
|
||||
@@ -17,7 +18,7 @@ def update_uname(info: tarfile.TarInfo) -> tarfile.TarInfo:
|
||||
return info
|
||||
|
||||
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
root = get_git_root()
|
||||
dist_dir = root.joinpath("dist")
|
||||
build_dir = root.joinpath("build")
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
from pathlib import Path
|
||||
|
||||
from dzgui.app_init import build_gui
|
||||
from shared import get_git_root
|
||||
|
||||
|
||||
def get_image_path() -> Path:
|
||||
return get_git_root().joinpath("dzgui/data/images/example.png")
|
||||
|
||||
|
||||
p = get_image_path()
|
||||
app = build_gui("", False)
|
||||
app.set_automation_param("screenshot_path", p)
|
||||
app.run(None)
|
||||
@@ -0,0 +1,11 @@
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def get_git_root() -> Path:
|
||||
proc = subprocess.run(
|
||||
["git", "rev-parse", "--show-toplevel"], capture_output=True, text=True
|
||||
)
|
||||
if proc.returncode != 0:
|
||||
raise OSError("Failed to parse git root")
|
||||
return Path(proc.stdout.rstrip())
|
||||
@@ -0,0 +1,53 @@
|
||||
"AppState"
|
||||
{
|
||||
"appid" "444"
|
||||
"Universe" "1"
|
||||
"name" "TEST APP"
|
||||
"StateFlags" "4"
|
||||
"installdir" "TESTAPP"
|
||||
"LastUpdated" "1792595373"
|
||||
"LastPlayed" "0"
|
||||
"SizeOnDisk" "0"
|
||||
"StagingSize" "0"
|
||||
"buildid" "0"
|
||||
"LastOwner" "0"
|
||||
"DownloadType" "2"
|
||||
"UpdateResult" "0"
|
||||
"BytesToDownload" "0"
|
||||
"BytesDownloaded" "0"
|
||||
"BytesToStage" "0"
|
||||
"BytesStaged" "0"
|
||||
"TargetBuildID" "0"
|
||||
"AutoUpdateBehavior" "0"
|
||||
"AllowOtherDownloadsWhileRunning" "0"
|
||||
"ScheduledAutoUpdate" "0"
|
||||
"FullValidateAfterNextUpdate" "1"
|
||||
"InstalledDepots"
|
||||
{
|
||||
"3330"
|
||||
{
|
||||
"manifest" "0"
|
||||
"size" "0"
|
||||
}
|
||||
}
|
||||
"SharedDepots"
|
||||
{
|
||||
"228983" "228980"
|
||||
"228985" "228980"
|
||||
"228988" "228980"
|
||||
"228990" "228980"
|
||||
"229004" "228980"
|
||||
}
|
||||
"UserConfig"
|
||||
{
|
||||
"language" "english"
|
||||
"platform_override_dest" "linux"
|
||||
"platform_override_source" "windows"
|
||||
}
|
||||
"MountedConfig"
|
||||
{
|
||||
"language" "english"
|
||||
"platform_override_dest" "linux"
|
||||
"platform_override_source" "windows"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
"AppState"
|
||||
{
|
||||
"appid" "555"
|
||||
"Universe" "1"
|
||||
"name" "TEST APP"
|
||||
"StateFlags" "4"
|
||||
"installdir" "TESTAPP"
|
||||
"LastUpdated" "0"
|
||||
"LastPlayed" "0"
|
||||
"SizeOnDisk" "0"
|
||||
"StagingSize" "0"
|
||||
"buildid" "0"
|
||||
"LastOwner" "0"
|
||||
"DownloadType" "2"
|
||||
"UpdateResult" "0"
|
||||
"BytesToDownload" "0"
|
||||
"BytesDownloaded" "0"
|
||||
"BytesToStage" "0"
|
||||
"BytesStaged" "0"
|
||||
"TargetBuildID" "0"
|
||||
"AutoUpdateBehavior" "0"
|
||||
"AllowOtherDownloadsWhileRunning" "0"
|
||||
"ScheduledAutoUpdate" "0"
|
||||
"FullValidateAfterNextUpdate" "1"
|
||||
"InstalledDepots"
|
||||
{
|
||||
"3330"
|
||||
{
|
||||
"manifest" "0"
|
||||
"size" "0"
|
||||
}
|
||||
}
|
||||
"SharedDepots"
|
||||
{
|
||||
"228983" "228980"
|
||||
"228985" "228980"
|
||||
"228988" "228980"
|
||||
"228990" "228980"
|
||||
"229004" "228980"
|
||||
}
|
||||
"UserConfig"
|
||||
{
|
||||
"language" "english"
|
||||
"platform_override_dest" "linux"
|
||||
"platform_override_source" "windows"
|
||||
}
|
||||
"MountedConfig"
|
||||
{
|
||||
"language" "english"
|
||||
"platform_override_dest" "linux"
|
||||
"platform_override_source" "windows"
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import pytest
|
||||
from _pytest.monkeypatch import MonkeyPatch
|
||||
from pathlib import Path
|
||||
|
||||
from dzgui.api.steam import get_app_allows_downloads
|
||||
from dzgui.api.steam import get_app_allows_downloads, get_last_updated
|
||||
from tests.fixtures import fixture_path
|
||||
|
||||
pytestmark = pytest.mark.apitest
|
||||
@@ -43,6 +43,14 @@ def patch_api() -> None:
|
||||
mp.undo()
|
||||
|
||||
|
||||
def test_no_last_update(steam_path) -> None:
|
||||
assert get_last_updated(steam_path, 555) is None
|
||||
|
||||
|
||||
def test_last_update(steam_path) -> None:
|
||||
assert get_last_updated(steam_path, 444) == "2026-10-22 00:09:33"
|
||||
|
||||
|
||||
def test_app_allows_downloads(steam_path) -> None:
|
||||
assert get_app_allows_downloads(steam_path, 111) is True
|
||||
|
||||
|
||||
@@ -36,7 +36,6 @@ def keys():
|
||||
|
||||
# TODO: use a static fixture instead of system config
|
||||
@pytest.fixture
|
||||
@pytest.mark.realconfig
|
||||
def config():
|
||||
paths = get_xdg_paths()
|
||||
xdg = parse_filepaths(paths)
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import configparser
|
||||
import pytest
|
||||
|
||||
from dzgui.const.constants import FLATPAK_APPID, GTK_APP_ID, TMP_PATH
|
||||
from dzgui.config.xdg import get_xdg_paths
|
||||
from dzgui.util.flatpak import apply_overrides
|
||||
|
||||
PATHS_KEY = "Context"
|
||||
PATHS_SUBKEY = "filesystems"
|
||||
BUS_KEY = "Session Bus Policy"
|
||||
|
||||
pytestmark = pytest.mark.flatpak
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def xdg_paths() -> dict[str, str]:
|
||||
return get_xdg_paths()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def override(xdg_paths) -> None:
|
||||
apply_overrides(xdg_paths)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def config(xdg_paths) -> configparser.ConfigParser:
|
||||
conf_path = xdg_paths["XDG_DATA_HOME"].parent.joinpath(
|
||||
f"flatpak/overrides/{FLATPAK_APPID}"
|
||||
)
|
||||
config = configparser.ConfigParser()
|
||||
config.read(conf_path)
|
||||
return config
|
||||
|
||||
|
||||
def test_session_bus(override, config) -> None:
|
||||
bus = config[BUS_KEY][GTK_APP_ID]
|
||||
assert bus == "own"
|
||||
|
||||
|
||||
def test_paths(override, xdg_paths, config) -> None:
|
||||
expect: list[str] = []
|
||||
expect.append(TMP_PATH)
|
||||
expect.extend([*xdg_paths.values()])
|
||||
paths = config[PATHS_KEY][PATHS_SUBKEY].split(";")
|
||||
assert all(str(path) in paths for path in expect)
|
||||
+44
-7
@@ -1,16 +1,40 @@
|
||||
from dzgui.config.ipdb import get_ipdb
|
||||
from pathlib import Path
|
||||
import os
|
||||
import sqlite3
|
||||
import pytest
|
||||
import tempfile
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from dzgui.const.endpoints import DB_IP
|
||||
from dzgui.config.ipdb import (
|
||||
create_table,
|
||||
find_download_url,
|
||||
serialize,
|
||||
strip_ipv6,
|
||||
unzip,
|
||||
)
|
||||
|
||||
|
||||
def get_db_len(path: Path) -> int:
|
||||
conn = sqlite3.connect(path)
|
||||
c = conn.cursor()
|
||||
c.execute("SELECT COUNT(*) from ips")
|
||||
db_len = c.fetchone()[0]
|
||||
return db_len
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def csv() -> Path:
|
||||
csv = Path(tempfile.NamedTemporaryFile().name)
|
||||
url = find_download_url(DB_IP)
|
||||
tmp = serialize(url)
|
||||
unzip(tmp, csv)
|
||||
strip_ipv6(csv)
|
||||
return csv
|
||||
|
||||
@pytest.mark.slow
|
||||
def test_stripping():
|
||||
d = tempfile.TemporaryDirectory()
|
||||
path = Path(d.name).joinpath("ips.csv")
|
||||
get_ipdb(path)
|
||||
with open(path, "rb") as f:
|
||||
def test_stripping(csv):
|
||||
with open(csv, "rb") as f:
|
||||
first = f.readline().decode().rstrip()
|
||||
try:
|
||||
f.seek(-2, os.SEEK_END)
|
||||
@@ -25,3 +49,16 @@ def test_stripping():
|
||||
|
||||
assert first_els[0] == "0.0.0.0"
|
||||
assert last_els[0] == "224.0.0.0"
|
||||
|
||||
|
||||
@pytest.mark.slow
|
||||
def test_row_parity(csv) -> None:
|
||||
rows = csv.read_text().splitlines()
|
||||
|
||||
db = Path(tempfile.NamedTemporaryFile().name)
|
||||
create_table(db, csv)
|
||||
|
||||
csv_len = len(rows)
|
||||
db_len = get_db_len(db)
|
||||
|
||||
assert csv_len == db_len
|
||||
@@ -1,3 +1,5 @@
|
||||
import pytest
|
||||
|
||||
from dzgui.api.servers import Details
|
||||
from dzgui.views.dialogs.servers import ServerDetailsDialog
|
||||
|
||||
@@ -21,6 +23,7 @@ class MockController:
|
||||
return None
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings("ignore::DeprecationWarning")
|
||||
def test_html_escaping() -> None:
|
||||
name = "Test server"
|
||||
desc = "<Server description> Game & mods"
|
||||
@@ -31,5 +34,7 @@ def test_html_escaping() -> None:
|
||||
controller = MockController()
|
||||
dialog = ServerDetailsDialog(controller, details)
|
||||
dialog.destroy()
|
||||
while Gtk.events_pending():
|
||||
Gtk.main_iteration()
|
||||
|
||||
assert dialog.description.get_text() == desc
|
||||
@@ -0,0 +1,47 @@
|
||||
import pytest
|
||||
|
||||
from dzgui.model.model_factory import FastInsertListStore
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def heterogeneous_list() -> None:
|
||||
return [["a", 0, True], ["b", 1, False]]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mismatched_length_list() -> None:
|
||||
return [["a", 0, True], ["b", 1, False, 0]]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def extended_list(heterogeneous_list) -> None:
|
||||
fs = FastInsertListStore(str, int, bool)
|
||||
fs.extend(heterogeneous_list)
|
||||
return fs
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def appended_list(heterogeneous_list) -> None:
|
||||
fs = FastInsertListStore(str, int, bool)
|
||||
for row in heterogeneous_list:
|
||||
fs.append(row)
|
||||
return fs
|
||||
|
||||
|
||||
@pytest.mark.parametrize("target", ["extended_list", "appended_list"])
|
||||
def test_list_parity(heterogeneous_list, request, target) -> None:
|
||||
target = request.getfixturevalue(target)
|
||||
extracted = [[el for el in row] for row in target]
|
||||
assert heterogeneous_list == extracted
|
||||
|
||||
|
||||
def test_type_disparity(heterogeneous_list) -> None:
|
||||
with pytest.raises(TypeError):
|
||||
fs = FastInsertListStore(str, bool, bool)
|
||||
fs.extend(heterogeneous_list)
|
||||
|
||||
|
||||
def test_length_disparity(mismatched_length_list) -> None:
|
||||
with pytest.raises(ValueError):
|
||||
fs = FastInsertListStore(str, int, bool)
|
||||
fs.extend(mismatched_length_list)
|
||||
@@ -1,3 +1,4 @@
|
||||
import pytest
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
@@ -9,6 +10,8 @@ from dzgui.const.constants import (
|
||||
)
|
||||
from dzgui.init import proc
|
||||
|
||||
pytestmark = pytest.mark.flatpak
|
||||
|
||||
|
||||
class TestSteamDetection(unittest.TestCase):
|
||||
def test_flatpak_detection_with_multiple_running_apps(self):
|
||||
|
||||
Reference in new issue
Block a user