mirror of
https://github.com/aclist/dztui.git
synced 2026-11-10 20:10:38 +01:00
Compare commits
75
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3dc37561be | ||
|
|
13786591b3 | ||
|
|
9eb1342877 | ||
|
|
3f29484895 | ||
|
|
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 | ||
|
|
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,8 @@ 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.
|
||||
|
||||
## Disclaimer
|
||||
|
||||
DZGUI is beta-quality software and is provided as-is.
|
||||
+28
-3
@@ -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
|
||||
|
||||
@@ -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
|
||||
+3
-3
@@ -22,7 +22,7 @@ class Xdg:
|
||||
version: Path
|
||||
system: Path
|
||||
debug: Path
|
||||
ips: Path
|
||||
db: Path
|
||||
filters: Path
|
||||
shortcut: Path
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -3,8 +3,8 @@ from typing import TYPE_CHECKING
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository.Gtk import ListStore # noqa E402
|
||||
from gi.repository import Gdk, GObject # noqa E402
|
||||
from gi.repository.Gtk import ListStore
|
||||
from gi.repository import GObject
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dzgui.const.enum import NotebookPage, ServerTab
|
||||
@@ -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
|
||||
|
||||
+11
-1
@@ -32,7 +32,7 @@ from dzgui.views.dialogs.generic import ExceptionDialog
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk, Gdk, GLib, GObject # noqa E402
|
||||
from gi.repository import Gtk, Gdk, GLib, GObject
|
||||
|
||||
logger = logging.getLogger(APP_NAME)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
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)
|
||||
@@ -55,11 +55,6 @@ from dzgui.util.symlink import rebuild_symlinks
|
||||
from dzgui.views.dialogs.generic import ExceptionDialog
|
||||
from dzgui.views.dialogs.servers import ServerDetailsDialog, ServerModDialog
|
||||
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk, GLib # noqa E402
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dzgui.api.servers import A2SInfo, Record
|
||||
from dzgui.controllers.mc import Controller
|
||||
|
||||
@@ -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:
|
||||
|
||||
+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,
|
||||
@@ -30,7 +30,7 @@ import dzgui.api.pefile as PeFile
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk # noqa E402
|
||||
from gi.repository import Gtk
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dzgui.controllers.mc import Controller
|
||||
@@ -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:
|
||||
|
||||
@@ -15,7 +15,7 @@ if TYPE_CHECKING:
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk, GLib # noqa E402
|
||||
from gi.repository import GLib
|
||||
|
||||
logger = logging.getLogger(APP_NAME)
|
||||
|
||||
|
||||
@@ -15,11 +15,6 @@ from dzgui.strings import dialogs
|
||||
from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager
|
||||
from dzgui.views.dialogs.generic import ExceptionDialog, QuitDialog
|
||||
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk, GLib # noqa E402
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dzgui.controllers.mc import Controller
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ from dzgui.util.strings import delimiter
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository.Gtk import ListStore, TreeIter # noqa E402
|
||||
from gi.repository import GObject # noqa E402
|
||||
from gi.repository.Gtk import ListStore, TreeIter
|
||||
from gi.repository import GObject
|
||||
|
||||
GTYPE_TO_PYTHON = {
|
||||
GObject.type_from_name(GObject.type_name(ptype)): ptype
|
||||
|
||||
+34
-8
@@ -21,7 +21,7 @@ from dzgui.views.dialogs.generic import ExceptionDialog
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk, GLib # noqa E402
|
||||
from gi.repository import Gtk, GLib
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dzgui.api.servers import A2SInfo, Record
|
||||
@@ -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:
|
||||
@@ -414,6 +438,8 @@ class ServerModelManager:
|
||||
# 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()
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
+1
-2
@@ -1,8 +1,7 @@
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
gi.require_version("Gdk", "3.0")
|
||||
from gi.repository import Gtk, Gdk # noqa E402
|
||||
from gi.repository import Gtk, Gdk
|
||||
|
||||
|
||||
def copy_clipboard(text: str) -> None:
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ from dzgui.const.constants import APP_NAME_LOWER, CSS_PATH
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk, Gdk # noqa E402
|
||||
from gi.repository import Gtk, Gdk
|
||||
|
||||
|
||||
def add_class(widget: Gtk.Widget, label: str) -> None:
|
||||
|
||||
+3
-7
@@ -7,11 +7,6 @@ from dzgui.const.constants import APP_NAME
|
||||
from dzgui.util.ip import get_coords
|
||||
from dzgui.util.localize import number
|
||||
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk # noqa E402
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from queue import Queue
|
||||
from dzgui.const.enum import ServerTab
|
||||
@@ -80,8 +75,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)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import logging
|
||||
import subprocess
|
||||
|
||||
from dzgui.const.constants import APP_NAME, FLATPAK_APPID, GTK_APP_ID
|
||||
from dzgui.const.constants import APP_NAME, FLATPAK_APPID, GTK_APP_ID, TMP_PATH
|
||||
|
||||
logger = logging.getLogger(APP_NAME)
|
||||
|
||||
@@ -18,7 +18,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([*xdg.values()])
|
||||
paths.append(TMP_PATH)
|
||||
|
||||
for path in paths:
|
||||
args = [
|
||||
"flatpak",
|
||||
"--user",
|
||||
|
||||
@@ -7,7 +7,7 @@ from dzgui.util.strings import no_mods, no_servers, workshop
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk # noqa E402
|
||||
from gi.repository import Gtk
|
||||
|
||||
|
||||
def format_pango(text: str) -> str:
|
||||
|
||||
+14
-45
@@ -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
|
||||
|
||||
@@ -33,54 +35,21 @@ def ip2list(ip: str) -> list:
|
||||
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:
|
||||
|
||||
+4
-1
@@ -1,6 +1,8 @@
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gdk # noqa E402
|
||||
from gi.repository import Gdk
|
||||
|
||||
|
||||
def is_navkey(key: int) -> bool:
|
||||
nav_keys = [
|
||||
@@ -18,6 +20,7 @@ def is_navkey(key: int) -> bool:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def is_ctrl_mask(event: Gdk.EventKey) -> bool:
|
||||
if event.state is Gdk.ModifierType.CONTROL_MASK:
|
||||
return True
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk # noqa E402
|
||||
from gi.repository import Gtk
|
||||
|
||||
|
||||
def set_surrounding_margins(widget: Gtk.Widget, margin: int) -> None:
|
||||
"""
|
||||
|
||||
@@ -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 Gdk, GLib
|
||||
|
||||
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",
|
||||
)
|
||||
|
||||
|
||||
|
||||
+21
-3
@@ -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,
|
||||
@@ -38,7 +38,7 @@ import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
gi.require_version("GLibUnix", "2.0")
|
||||
from gi.repository import Gtk, GLib, GLibUnix, Gdk # type: ignore # noqa E402
|
||||
from gi.repository import Gtk, GLib, GLibUnix, Gdk # type: ignore
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dzgui.config.userprefs import UserPrefs
|
||||
@@ -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()
|
||||
@@ -7,7 +7,7 @@ from dzgui.const.enum import ButtonType
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk, Gdk # noqa E402
|
||||
from gi.repository import Gtk, Gdk
|
||||
|
||||
logger = logging.getLogger(APP_NAME)
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ from dzgui.strings import connect_panel
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk, Gdk, GLib # noqa E402
|
||||
from gi.repository import Gtk, GLib
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from gi.repository import GLib
|
||||
@@ -163,18 +163,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:
|
||||
|
||||
@@ -18,7 +18,7 @@ from dzgui.views.components.labels import BoldLabel
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk, Gdk # noqa E402
|
||||
from gi.repository import Gtk, Gdk
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dzgui.controllers.mc import Controller
|
||||
|
||||
@@ -7,7 +7,7 @@ from dzgui.util.format import embolden
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk, GLib, Gdk, GObject, Pango # noqa E402
|
||||
from gi.repository import Gtk
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dzgui.controllers.mc import Controller
|
||||
|
||||
@@ -9,7 +9,7 @@ from dzgui.strings.errors import api_popover
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk, Gdk, GObject # noqa E402
|
||||
from gi.repository import Gtk, Gdk, GObject
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dzgui.controllers.mc import Controller
|
||||
|
||||
@@ -6,7 +6,7 @@ from dzgui.const.constants import HELP_BUBBLE
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk, Gdk # noqa E402
|
||||
from gi.repository import Gtk, Gdk
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dzgui.controllers.mc import Controller
|
||||
|
||||
@@ -17,7 +17,7 @@ from dzgui.views.components.labels import BoldLabel
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk, Gdk, Pango, GLib # noqa E402
|
||||
from gi.repository import Gtk, Gdk, Pango, GLib
|
||||
|
||||
logger = logging.getLogger(APP_NAME)
|
||||
|
||||
@@ -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():
|
||||
@@ -313,6 +316,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()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk # noqa E402
|
||||
from gi.repository import Gtk
|
||||
|
||||
|
||||
class Icon(Gtk.Image):
|
||||
|
||||
@@ -3,7 +3,7 @@ from dzgui.util.format import embolden
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk # noqa E402
|
||||
from gi.repository import Gtk
|
||||
|
||||
|
||||
class BoldLabel(Gtk.Label):
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk, GLib # noqa E402
|
||||
from gi.repository import Gtk
|
||||
|
||||
|
||||
class MapsCombo(Gtk.EntryCompletion):
|
||||
|
||||
@@ -8,7 +8,7 @@ from dzgui.views.components.labels import BoldLabel
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk # noqa E402
|
||||
from gi.repository import Gtk
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dzgui.controllers.mc import Controller
|
||||
@@ -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()
|
||||
@@ -23,7 +23,7 @@ from dzgui.views.components.buttons import IconTextButton, RefreshButton, KeysBu
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk, Gdk, GLib # noqa E402
|
||||
from gi.repository import Gtk, Gdk, GLib
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dzgui.controllers.mc import Controller
|
||||
|
||||
@@ -7,7 +7,7 @@ from dzgui.views.components.buttons import LoggerAlertsButton
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk # noqa E402
|
||||
from gi.repository import Gtk
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dzgui.const.enum import ServerTab
|
||||
|
||||
@@ -2,8 +2,9 @@ from typing import Literal
|
||||
from dzgui.util import css
|
||||
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk, GLib # noqa E402
|
||||
from gi.repository import Gtk, GLib
|
||||
|
||||
|
||||
class Toast(Gtk.EventBox):
|
||||
|
||||
@@ -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
|
||||
|
||||
DAY_START_TIME = time(hour=6, minute=0, second=0)
|
||||
DAY_END_TIME = time(hour=18, minute=0, second=0)
|
||||
|
||||
@@ -8,7 +8,7 @@ from dzgui.views.mixins.colorscheme import ColorAwareApp
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk # noqa E402
|
||||
from gi.repository import Gtk
|
||||
|
||||
|
||||
class AbortDialog(ColorAwareApp, Gtk.MessageDialog): # type: ignore
|
||||
|
||||
@@ -2,10 +2,10 @@ from pathlib import Path
|
||||
|
||||
from dzgui.util.strings import picker
|
||||
|
||||
import gi # noqa E402
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk # noqa E402
|
||||
from gi.repository import Gtk
|
||||
|
||||
|
||||
class FilePicker(Gtk.FileChooserDialog):
|
||||
|
||||
@@ -8,7 +8,7 @@ from dzgui.views.components.buttons import ClipboardButton
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk, GLib, Gdk, GObject, Pango # noqa E402
|
||||
from gi.repository import Gtk, GLib
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dzgui.controllers.mc import Controller
|
||||
|
||||
@@ -9,7 +9,7 @@ from dzgui.views.components.buttons import SteamTextButton
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk, GLib, Gdk, GObject, Pango # noqa E402
|
||||
from gi.repository import Gtk
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
@@ -3,10 +3,10 @@ from typing import Self, TYPE_CHECKING
|
||||
from dzgui.views.components.entry import ValidatedEntry
|
||||
from dzgui.views.dialogs.generic import GenericDialog
|
||||
|
||||
import gi # noqa E402
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gdk, Gtk # noqa E402
|
||||
from gi.repository import Gtk
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dzgui.controllers.mc import Controller
|
||||
|
||||
@@ -18,7 +18,7 @@ from dzgui.views.mixins.colorscheme import ColorAwareApp
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk, GLib, Pango # noqa E402
|
||||
from gi.repository import Gtk, GLib, Pango
|
||||
|
||||
|
||||
class Assistant(Gtk.Assistant):
|
||||
|
||||
@@ -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
|
||||
@@ -34,7 +35,7 @@ from dzgui.views.trees.tree_simple import SimpleTree
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gdk, Gtk, GLib, GObject, GdkPixbuf # noqa E402
|
||||
from gi.repository import Gtk, GLib, GObject, GdkPixbuf
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dzgui.config.xdg import Xdg
|
||||
@@ -432,6 +433,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)
|
||||
|
||||
|
||||
@@ -9,16 +9,14 @@ from dzgui.views.trees.tree_base import TreeView
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk, GLib, Gdk, GObject, Pango # noqa E402
|
||||
from gi.repository import Gtk, Gdk
|
||||
|
||||
logger = logging.getLogger(APP_NAME)
|
||||
|
||||
|
||||
class ContextMixin(TreeView):
|
||||
def present_menu(
|
||||
self,
|
||||
widget: Gtk.Widget,
|
||||
event: Gdk.EventButton | Gdk.EventKey,
|
||||
self, widget: Gtk.Widget, event: Gdk.Event
|
||||
) -> bool:
|
||||
|
||||
if self.menu is None:
|
||||
|
||||
@@ -5,7 +5,7 @@ from dzgui.util.keys import is_ctrl_mask
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk, GLib, Gdk, GObject, Pango # noqa E402
|
||||
from gi.repository import Gtk, Gdk
|
||||
|
||||
|
||||
class CursorMixin:
|
||||
|
||||
@@ -3,7 +3,7 @@ from dzgui.views.trees.tree_base import TreeView
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk, GLib, Gdk, GObject, Pango # noqa E402
|
||||
from gi.repository import Gtk
|
||||
|
||||
|
||||
class ModsMixin:
|
||||
|
||||
@@ -15,7 +15,7 @@ from dzgui.views.mixins.scrollable_mixin import ScrollableMixin
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk # noqa E402
|
||||
from gi.repository import Gtk
|
||||
|
||||
logger = logging.getLogger(APP_NAME)
|
||||
|
||||
|
||||
@@ -7,10 +7,10 @@ from dzgui.util.strings import developers
|
||||
from dzgui.views.components.labels import BoldLabel
|
||||
from dzgui.views.trees.tree_base import TreeView
|
||||
|
||||
import gi # noqa E402
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk # noqa E402
|
||||
from gi.repository import Gtk
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dzgui.controllers.mc import Controller
|
||||
|
||||
@@ -4,7 +4,7 @@ from dzgui.views.trees.tree_menu import MenuTreeView
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk # noqa E402
|
||||
from gi.repository import Gtk
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dzgui.controllers.mc import Controller
|
||||
|
||||
@@ -7,7 +7,7 @@ from dzgui.util.css import add_class
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk # noqa E402
|
||||
from gi.repository import Gtk
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dzgui.controllers.mc import Controller
|
||||
|
||||
@@ -9,7 +9,7 @@ from dzgui.views.trees.tree_log import LogTreeView
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk # noqa E402
|
||||
from gi.repository import Gtk
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dzgui.controllers.mc import Controller
|
||||
|
||||
@@ -2,6 +2,7 @@ from pathlib import Path
|
||||
from typing import 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 (
|
||||
@@ -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],
|
||||
@@ -467,10 +470,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)
|
||||
|
||||
@@ -18,7 +18,7 @@ from dzgui.views.trees.tree_server_mods import ServerModTreeView
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gdk, Gtk # noqa E402
|
||||
from gi.repository import Gdk, Gtk
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
@@ -10,7 +10,7 @@ from dzgui.util.strings import server_labels
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk, Gdk, GLib # noqa E402
|
||||
from gi.repository import Gtk, Gdk
|
||||
|
||||
logger = logging.getLogger(APP_NAME)
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from dzgui.views.mixins.scrollable_mixin import ScrollableMixin
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk # noqa E402
|
||||
from gi.repository import Gtk
|
||||
|
||||
# TODO: wrap and center long values
|
||||
if TYPE_CHECKING:
|
||||
|
||||
@@ -108,14 +108,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:
|
||||
|
||||
@@ -12,7 +12,7 @@ from dzgui.views.mixins.context_mixin import ContextMixin
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk, GLib, Gdk, GObject, Pango # noqa E402
|
||||
from gi.repository import Gtk, Gdk
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
|
||||
+2
-1
@@ -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"
|
||||
@@ -78,6 +78,7 @@ line-length = 88
|
||||
indent-width = 4
|
||||
extend-exclude = ["*lib/a2s"]
|
||||
lint.unfixable = ["F401", "I001"]
|
||||
lint.ignore = ["E402"]
|
||||
|
||||
[tool.ruff.format]
|
||||
quote-style = "double"
|
||||
|
||||
@@ -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())
|
||||
@@ -1,38 +0,0 @@
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def iterate(search_str: str) -> None:
|
||||
# TODO: exclude files from .gitignore
|
||||
print()
|
||||
print(f"Searching for '{search_str}'")
|
||||
print()
|
||||
files = Path(".").rglob("*")
|
||||
ignore = [
|
||||
"scripts/",
|
||||
".git",
|
||||
"egg",
|
||||
"CHANGELOG",
|
||||
".mypy",
|
||||
"build/",
|
||||
"test.py",
|
||||
"bugs",
|
||||
"pyproject.toml",
|
||||
]
|
||||
for file in files:
|
||||
if file.is_dir():
|
||||
continue
|
||||
if any(s in str(file) for s in ignore):
|
||||
continue
|
||||
try:
|
||||
text = file.read_text()
|
||||
if search_str in text:
|
||||
c = text.count(search_str)
|
||||
if 0 < c < 2:
|
||||
print(file, c)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
|
||||
imports = ["Pango", "GObject", "GLib", "Gdk", "Gtk"]
|
||||
for i in imports:
|
||||
iterate(i)
|
||||
@@ -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)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import configparser
|
||||
import pytest
|
||||
|
||||
from dzgui.const.constants import FLATPAK_APPID, GTK_APP_ID
|
||||
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
|
||||
|
||||
@@ -38,6 +38,8 @@ def test_session_bus(override, config) -> None:
|
||||
|
||||
|
||||
def test_paths(override, xdg_paths, config) -> None:
|
||||
expect = xdg_paths
|
||||
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.values())
|
||||
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
|
||||
@@ -6,7 +6,7 @@ from dzgui.views.dialogs.servers import ServerDetailsDialog
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk # noqa E402
|
||||
from gi.repository import Gtk
|
||||
|
||||
|
||||
class MockController:
|
||||
|
||||
@@ -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)
|
||||
Reference in new issue
Block a user