mirror of
https://github.com/aclist/dztui.git
synced 2026-11-09 19:40:38 +01:00
Compare commits
37
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72e3d95808 | ||
|
|
370edbe01b | ||
|
|
0c41654645 | ||
|
|
903f8fc31d | ||
|
|
6b583f68d6 | ||
|
|
3b3ccd0de6 | ||
|
|
70aacaa866 | ||
|
|
7582ab4d3b | ||
|
|
225a2a148e | ||
|
|
9117655ebd | ||
|
|
fdf92dca40 | ||
|
|
a97766855d | ||
|
|
3097701e98 | ||
|
|
895e08d228 | ||
|
|
08a6f698a5 | ||
|
|
78be8c93b7 | ||
|
|
8a2889d15d | ||
|
|
1a33f71716 | ||
|
|
596d3a59ac | ||
|
|
e7cef215c6 | ||
|
|
12daf041ff | ||
|
|
90ef6cbdbb | ||
|
|
2cc26b6573 | ||
|
|
0ec1c157f4 | ||
|
|
0cf6439538 | ||
|
|
e3c1ef59ee | ||
|
|
78144e9127 | ||
|
|
33b8dfba70 | ||
|
|
abf0060cac | ||
|
|
95daa9163b | ||
|
|
ff068e5396 | ||
|
|
80680b86b0 | ||
|
|
a4ac357c8f | ||
|
|
728b8b8242 | ||
|
|
3b51392d39 | ||
|
|
159e2da355 | ||
|
|
b83a333cea |
No files matched your search
@@ -53,6 +53,10 @@
|
|||||||
- Expand/collapse changelog versions
|
- Expand/collapse changelog versions
|
||||||
- Support local light/dark mode changes on system theme
|
- Support local light/dark mode changes on system theme
|
||||||
- Server time calculator
|
- 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
|
### Changed
|
||||||
- Conform to PEP 440 versioning for beta versions
|
- Conform to PEP 440 versioning for beta versions
|
||||||
@@ -74,6 +78,7 @@
|
|||||||
- Enable LAN page Empty/Full filters on first run of app
|
- Enable LAN page Empty/Full filters on first run of app
|
||||||
- Propagate subscribed mods to Steam client
|
- Propagate subscribed mods to Steam client
|
||||||
- Make j/k keys in context menus follow same wraparound logic as down arrow/up arrow
|
- Make j/k keys in context menus follow same wraparound logic as down arrow/up arrow
|
||||||
|
- Use SQLite DB for geolocation records
|
||||||
|
|
||||||
### Dropped
|
### Dropped
|
||||||
- Battlemetrics API support
|
- Battlemetrics API support
|
||||||
@@ -92,6 +97,8 @@
|
|||||||
- Moved dialogs out of threads
|
- Moved dialogs out of threads
|
||||||
- History table not being in lock-step with state file
|
- History table not being in lock-step with state file
|
||||||
- Current keyword filter not being applied when refreshing table
|
- 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
|
### Unreleased
|
||||||
- Load offline mods
|
- Load offline mods
|
||||||
|
|||||||
+28
-3
@@ -1,15 +1,18 @@
|
|||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from collections.abc import Iterator
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any, TYPE_CHECKING
|
||||||
from warnings import deprecated
|
from warnings import deprecated
|
||||||
|
|
||||||
from dzgui.const.constants import APP_NAME
|
from dzgui.const.constants import APP_NAME
|
||||||
|
|
||||||
logger = logging.getLogger(APP_NAME)
|
logger = logging.getLogger(APP_NAME)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from collections.abc import Iterator
|
||||||
|
|
||||||
|
|
||||||
class ACF:
|
class ACF:
|
||||||
def __init__(self, file: Path) -> None:
|
def __init__(self, file: Path) -> None:
|
||||||
@@ -42,7 +45,7 @@ class ACF:
|
|||||||
def dequote(cls, s: str) -> str:
|
def dequote(cls, s: str) -> str:
|
||||||
return s.rstrip('"').lstrip('"')
|
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] = {}
|
acf: dict[str, Any] = {}
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
@@ -69,6 +72,28 @@ class ACF:
|
|||||||
except StopIteration:
|
except StopIteration:
|
||||||
return acf
|
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:
|
def get_allows_downloads(self) -> int | None:
|
||||||
try:
|
try:
|
||||||
# TODO: consider coercing strs to ints on initial import
|
# TODO: consider coercing strs to ints on initial import
|
||||||
|
|||||||
+30
-3
@@ -12,6 +12,7 @@ from typing import Any, Generator, Union
|
|||||||
from warnings import deprecated
|
from warnings import deprecated
|
||||||
|
|
||||||
from dzgui.api.acf import ACF
|
from dzgui.api.acf import ACF
|
||||||
|
from dzgui.const.endpoints import VALIDATE
|
||||||
from dzgui.init.prereqs import has_any_client
|
from dzgui.init.prereqs import has_any_client
|
||||||
from dzgui.init.prereqs import has_flatpak_client, has_steam_client
|
from dzgui.init.prereqs import has_flatpak_client, has_steam_client
|
||||||
from dzgui.const.constants import (
|
from dzgui.const.constants import (
|
||||||
@@ -316,10 +317,9 @@ def unsubscribe(key: str, mod: int) -> None:
|
|||||||
update_workshop(key, mod, UNSUB_ENDPOINT)
|
update_workshop(key, mod, UNSUB_ENDPOINT)
|
||||||
|
|
||||||
|
|
||||||
@deprecated("Use subscribe()")
|
def validate_mod(client: str, mod: str) -> None:
|
||||||
def enqueue_mod(client: str, mod: str, appid: int) -> None:
|
|
||||||
client_args = concat_bash_args(client)
|
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")
|
@deprecated("Cf. https://github.com/ValveSoftware/steam-for-linux/issues/9672")
|
||||||
@@ -389,6 +389,26 @@ def get_running_app() -> int | None:
|
|||||||
return 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:
|
def get_app_allows_downloads(path: Path, appid: int) -> bool:
|
||||||
try:
|
try:
|
||||||
root_path = get_app_path(path, appid)
|
root_path = get_app_path(path, appid)
|
||||||
@@ -500,3 +520,10 @@ def get_app_name(appid: int) -> str | None:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(e)
|
logger.debug(e)
|
||||||
return None
|
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])
|
||||||
+102
-3
@@ -3,6 +3,9 @@ import logging
|
|||||||
import re
|
import re
|
||||||
import requests
|
import requests
|
||||||
import shutil
|
import shutil
|
||||||
|
import socket
|
||||||
|
import sqlite3
|
||||||
|
import time
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -22,15 +25,16 @@ def find_download_url(url: str) -> str | None:
|
|||||||
return str(urls[0])
|
return str(urls[0])
|
||||||
|
|
||||||
|
|
||||||
def get_ipdb(ips_path: Path) -> None:
|
def get_ipdb(db_path: Path) -> None:
|
||||||
try:
|
try:
|
||||||
url = find_download_url(DB_IP)
|
url = find_download_url(DB_IP)
|
||||||
if url is None:
|
if url is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
ips_path = db_path.with_suffix(".csv")
|
||||||
|
month_file = db_path.parent / ".month"
|
||||||
date = find_date(url)
|
date = find_date(url)
|
||||||
month_file = ips_path.parent / ".month"
|
if db_path.exists() and month_file.exists():
|
||||||
if ips_path.exists() and month_file.exists():
|
|
||||||
old_date = month_file.read_text().rstrip("\n")
|
old_date = month_file.read_text().rstrip("\n")
|
||||||
if old_date == date:
|
if old_date == date:
|
||||||
logger.info(f"IP DB date matches: {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")
|
logger.info("Stripping IPv6 records")
|
||||||
strip_ipv6(ips_path)
|
strip_ipv6(ips_path)
|
||||||
|
|
||||||
|
create_table(db_path, ips_path)
|
||||||
|
ips_path.unlink()
|
||||||
|
|
||||||
with open(month_file, "w") as f:
|
with open(month_file, "w") as f:
|
||||||
f.write(date)
|
f.write(date)
|
||||||
logger.info(f"Wrote {date} to {month_file}")
|
logger.info(f"Wrote {date} to {month_file}")
|
||||||
@@ -108,3 +115,95 @@ def strip_ipv6(path: Path) -> None:
|
|||||||
out.write(merged)
|
out.write(merged)
|
||||||
path.unlink()
|
path.unlink()
|
||||||
alt_path.rename(path)
|
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
|
version: Path
|
||||||
system: Path
|
system: Path
|
||||||
debug: Path
|
debug: Path
|
||||||
ips: Path
|
db: Path
|
||||||
filters: Path
|
filters: Path
|
||||||
shortcut: Path
|
shortcut: Path
|
||||||
|
|
||||||
@@ -88,7 +88,7 @@ def parse_filepaths(xdg: dict) -> Xdg:
|
|||||||
resolution = state / "dzg.res.json"
|
resolution = state / "dzg.res.json"
|
||||||
history = state / "dzg.history"
|
history = state / "dzg.history"
|
||||||
versions = state / "dzg.versions"
|
versions = state / "dzg.versions"
|
||||||
ips = state / "ips.csv"
|
db = state / "ips.db"
|
||||||
filters = state / "dzg.filters.json"
|
filters = state / "dzg.filters.json"
|
||||||
|
|
||||||
shortcut = share / APP_NAME_LOWER
|
shortcut = share / APP_NAME_LOWER
|
||||||
@@ -102,7 +102,7 @@ def parse_filepaths(xdg: dict) -> Xdg:
|
|||||||
versions,
|
versions,
|
||||||
system,
|
system,
|
||||||
debug,
|
debug,
|
||||||
ips,
|
db,
|
||||||
filters,
|
filters,
|
||||||
shortcut,
|
shortcut,
|
||||||
)
|
)
|
||||||
@@ -37,28 +37,6 @@ HEX_GREEN = "#32CD32"
|
|||||||
HEX_RED = "#FF0000"
|
HEX_RED = "#FF0000"
|
||||||
HEX_ORANGE = "#FFAC1C"
|
HEX_ORANGE = "#FFAC1C"
|
||||||
|
|
||||||
# TODO: normalize icon names
|
|
||||||
CALCULATOR = "accessories-calculator-symbolic"
|
|
||||||
CARET_DOWN = "go-down-symbolic"
|
|
||||||
CARET_UP = "go-up-symbolic"
|
|
||||||
CLIPBOARD = "edit-copy-symbolic"
|
|
||||||
CLOSE = "window-close-symbolic"
|
|
||||||
FOLDER = "folder-symbolic"
|
|
||||||
EDIT_DELETE = "edit-delete-symbolic"
|
|
||||||
ERROR = "dialog-error-symbolic"
|
|
||||||
HELP_BUBBLE = "help-about-symbolic"
|
|
||||||
INPUT_KEYBOARD = "input-keyboard-symbolic"
|
|
||||||
LIST_ADD = "list-add-symbolic"
|
|
||||||
REFRESH_ICON = "view-refresh-symbolic"
|
|
||||||
SEARCH_ICON = "system-search-symbolic"
|
|
||||||
STEAM_ICON = "steam-tray-symbolic"
|
|
||||||
VIEW_CONCEAL = "view-conceal-symbolic"
|
|
||||||
VIEW_REVEAL = "view-reveal-symbolic"
|
|
||||||
WARNING = "dialog-warning-symbolic"
|
|
||||||
WEB_BROWSER = "web-browser-symbolic"
|
|
||||||
DAY_ICON = "weather-clear-symbolic"
|
|
||||||
NIGHT_ICON = "weather-clear-night-symbolic"
|
|
||||||
|
|
||||||
SEPARATOR = "SEPARATOR"
|
SEPARATOR = "SEPARATOR"
|
||||||
|
|
||||||
# TODO: drop
|
# TODO: drop
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ COORDS_API = "http://ip-api.com/json"
|
|||||||
# steam URIs
|
# steam URIs
|
||||||
WORKSHOP = "steam://url/CommunityFilePage/"
|
WORKSHOP = "steam://url/CommunityFilePage/"
|
||||||
OPEN_URL = "steam://openurl/"
|
OPEN_URL = "steam://openurl/"
|
||||||
|
VALIDATE = "steam://validate/"
|
||||||
|
|
||||||
# user facing
|
# user facing
|
||||||
GITHUB_PAGES = "https://aclist.github.io/dzgui/index"
|
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_IP = {"label": strings.copy_ip}
|
||||||
COPY_SERVER_NAME = {"label": strings.copy_name}
|
COPY_SERVER_NAME = {"label": strings.copy_name}
|
||||||
UNSUB_MOD = {"label": strings.unsub_mod}
|
UNSUB_MOD = {"label": strings.unsub_mod}
|
||||||
|
VALIDATE_MOD = {"label": strings.validate_mod}
|
||||||
OPEN_WORKSHOP = {"label": strings.open_workshop}
|
OPEN_WORKSHOP = {"label": strings.open_workshop}
|
||||||
REFRESH_PLAYERS = {"label": strings.refresh_players}
|
REFRESH_PLAYERS = {"label": strings.refresh_players}
|
||||||
REMOVE_HISTORY = {"label": strings.remove_history}
|
REMOVE_HISTORY = {"label": strings.remove_history}
|
||||||
@@ -172,7 +173,7 @@ class ContextMenuGroup(Enum):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
SERVER_MOD = (ContextMenu.OPEN_WORKSHOP,)
|
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,)
|
MOD_OFFLINE = (None,)
|
||||||
LOG = (ContextMenu.COPY_LOG_CLIPBOARD,)
|
LOG = (ContextMenu.COPY_LOG_CLIPBOARD,)
|
||||||
SERVER_BROWSER = (
|
SERVER_BROWSER = (
|
||||||
@@ -246,6 +247,10 @@ class ModButton(EnumWithAttrs):
|
|||||||
"label": strings.mod_panel.select_stale,
|
"label": strings.mod_panel.select_stale,
|
||||||
"tooltip": strings.mod_panel.select_stale_tooltip,
|
"tooltip": strings.mod_panel.select_stale_tooltip,
|
||||||
}
|
}
|
||||||
|
VALIDATE = {
|
||||||
|
"label": strings.mod_panel.validate,
|
||||||
|
"tooltip": strings.mod_panel.validate_tooltip,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class ButtonType(EnumWithAttrs):
|
class ButtonType(EnumWithAttrs):
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
ICON_CALCULATOR = "accessories-calculator-symbolic"
|
||||||
|
ICON_CARET_DOWN = "go-down-symbolic"
|
||||||
|
ICON_CARET_UP = "go-up-symbolic"
|
||||||
|
ICON_CLIPBOARD = "edit-copy-symbolic"
|
||||||
|
ICON_CLOSE = "window-close-symbolic"
|
||||||
|
ICON_DAY = "weather-clear-symbolic"
|
||||||
|
ICON_EDIT_DELETE = "edit-delete-symbolic"
|
||||||
|
ICON_ERROR = "dialog-error-symbolic"
|
||||||
|
ICON_FOLDER = "folder-symbolic"
|
||||||
|
ICON_HELP_BUBBLE = "help-about-symbolic"
|
||||||
|
ICON_INPUT_KEYBOARD = "input-keyboard-symbolic"
|
||||||
|
ICON_LIST_ADD = "list-add-symbolic"
|
||||||
|
ICON_NIGHT = "weather-clear-night-symbolic"
|
||||||
|
ICON_REFRESH = "view-refresh-symbolic"
|
||||||
|
ICON_SEARCH = "system-search-symbolic"
|
||||||
|
ICON_STEAM = "steam-tray-symbolic"
|
||||||
|
ICON_VIEW_CONCEAL = "view-conceal-symbolic"
|
||||||
|
ICON_VIEW_REVEAL = "view-reveal-symbolic"
|
||||||
|
ICON_WARNING = "dialog-warning-symbolic"
|
||||||
|
ICON_WEB_BROWSER = "web-browser-symbolic"
|
||||||
@@ -102,6 +102,16 @@ class Controller(GObject.GObject):
|
|||||||
except AttributeError:
|
except AttributeError:
|
||||||
logger.critical(f"{attr} is not a valid AppNavigation attribute.")
|
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:
|
def get_prefs(self) -> UserPrefs:
|
||||||
return self.prefs
|
return self.prefs
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 130 KiB After Width: | Height: | Size: 361 KiB |
@@ -10,10 +10,10 @@ if TYPE_CHECKING:
|
|||||||
logger = logging.getLogger(APP_NAME)
|
logger = logging.getLogger(APP_NAME)
|
||||||
|
|
||||||
|
|
||||||
def get_local_coords(path: "Path") -> Coords | None:
|
def get_local_coords(db_path: "Path") -> Coords | None:
|
||||||
try:
|
try:
|
||||||
my_ip = get_local_ip()
|
my_ip = get_local_ip()
|
||||||
return get_coords(path, my_ip)
|
return get_coords(db_path, my_ip)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warn(e)
|
logger.warn(e)
|
||||||
return None
|
return None
|
||||||
+1
-1
Submodule dzgui/lib/dayzquery updated: 07483b88ed...fe413df8e8.
@@ -75,10 +75,14 @@ class ContextMenuManager:
|
|||||||
|
|
||||||
if isinstance(self.treeview, (ModTreeView, OfflineModTreeView)):
|
if isinstance(self.treeview, (ModTreeView, OfflineModTreeView)):
|
||||||
match action:
|
match action:
|
||||||
case ContextMenu.UNSUB_MOD:
|
|
||||||
self.controller.unsub_mods(self.treeview)
|
|
||||||
case ContextMenu.OPEN_WORKSHOP:
|
case ContextMenu.OPEN_WORKSHOP:
|
||||||
self.open_mod_page()
|
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):
|
if isinstance(self.treeview, LogTreeView):
|
||||||
match action:
|
match action:
|
||||||
|
|||||||
+26
-2
@@ -4,7 +4,7 @@ import time
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from dzgui.api.steam import unsubscribe
|
from dzgui.api.steam import unsubscribe, validate_mod
|
||||||
from dzgui.api.mods import (
|
from dzgui.api.mods import (
|
||||||
get_delimited_mods,
|
get_delimited_mods,
|
||||||
find_stale_mods,
|
find_stale_mods,
|
||||||
@@ -85,7 +85,7 @@ class ModManager:
|
|||||||
total_mods = len(self.store)
|
total_mods = len(self.store)
|
||||||
self.emitter.emit("mods_updated", msg, total_mods)
|
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()
|
sel = self.treeview.get_selection()
|
||||||
model, pathlist = sel.get_selected_rows()
|
model, pathlist = sel.get_selected_rows()
|
||||||
# NOTE: reverse when multiple selection
|
# NOTE: reverse when multiple selection
|
||||||
@@ -96,6 +96,15 @@ class ModManager:
|
|||||||
continue
|
continue
|
||||||
mod, _iter = res
|
mod, _iter = res
|
||||||
mods.append((mod, _iter))
|
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.thread_man.set_job_count(len(mods))
|
||||||
self.unsub_all_mods(mods)
|
self.unsub_all_mods(mods)
|
||||||
|
|
||||||
@@ -109,6 +118,21 @@ class ModManager:
|
|||||||
mod = model.get_value(tree_iter, 2)
|
mod = model.get_value(tree_iter, 2)
|
||||||
return mod, tree_iter
|
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)
|
@call_on_thread(dialogs.deleting_mods)
|
||||||
def unsub_all_mods(self, mods: list[tuple[str, Gtk.TreeIter]]) -> None:
|
def unsub_all_mods(self, mods: list[tuple[str, Gtk.TreeIter]]) -> None:
|
||||||
for mod, _iter in mods:
|
for mod, _iter in mods:
|
||||||
|
|||||||
+30
-7
@@ -201,19 +201,43 @@ class ServerModelManager:
|
|||||||
|
|
||||||
@call_on_thread(dialog.querying)
|
@call_on_thread(dialog.querying)
|
||||||
def add_by_ip(self, addr: str) -> None:
|
def add_by_ip(self, addr: str) -> None:
|
||||||
res = Servers.query_by_ip(addr)
|
self.preserve_on_fail = True
|
||||||
self._parse_single_record(res)
|
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)
|
@call_on_thread(dialog.querying)
|
||||||
def add_by_record(self, record: Servers.Record) -> None:
|
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
|
# NOTE: rationale: a raw Record as shown in server browser may resolve to a different IP
|
||||||
res = Servers.query_by_record(record)
|
try:
|
||||||
self._parse_single_record(res)
|
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)
|
@call_on_thread(dialog.querying)
|
||||||
def remove_by_record(self, record: Servers.Record) -> None:
|
def remove_by_record(self, record: Servers.Record) -> None:
|
||||||
res = Servers.query_by_record(record)
|
self.preserve_on_fail = True
|
||||||
self._parse_single_record(res, delete=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(
|
def update_history(
|
||||||
self,
|
self,
|
||||||
@@ -280,7 +304,6 @@ class ServerModelManager:
|
|||||||
proxy_man.update_playercount(self.playercount)
|
proxy_man.update_playercount(self.playercount)
|
||||||
|
|
||||||
def _parse_single_record(self, response: "A2SInfo", delete: bool = False) -> None:
|
def _parse_single_record(self, response: "A2SInfo", delete: bool = False) -> None:
|
||||||
self.preserve_on_fail = True
|
|
||||||
try:
|
try:
|
||||||
row = response.as_row()
|
row = response.as_row()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -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."
|
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"
|
fetching_mods = "Fetching mod metadata"
|
||||||
deleting_mods = "Unsubscribing mods"
|
deleting_mods = "Unsubscribing mods"
|
||||||
|
validating_mods = "Validating mods"
|
||||||
scanning_mods = "Scanning mods"
|
scanning_mods = "Scanning mods"
|
||||||
parsing_mods = "Parsing mods"
|
parsing_mods = "Parsing mods"
|
||||||
|
|
||||||
|
|||||||
+3
-2
@@ -80,8 +80,9 @@ class CalcDist:
|
|||||||
if local is None:
|
if local is None:
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
remote = get_coords(prefs.paths.ips, remote_ip)
|
remote = get_coords(prefs.paths.db, remote_ip)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
|
logger.critical(e)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
haversine = Haversine(local.lat, local.lon, remote.lat, remote.lon)
|
haversine = Haversine(local.lat, local.lon, remote.lat, remote.lon)
|
||||||
|
|||||||
+14
-45
@@ -1,4 +1,5 @@
|
|||||||
import a2s
|
import a2s
|
||||||
|
from ipaddress import ip_address
|
||||||
import requests
|
import requests
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
@@ -7,6 +8,7 @@ from dataclasses import dataclass
|
|||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from dzgui.api.servers import Record
|
from dzgui.api.servers import Record
|
||||||
|
from dzgui.config.ipdb import read_from_table
|
||||||
from dzgui.const.constants import REQUEST_TIMEOUT
|
from dzgui.const.constants import REQUEST_TIMEOUT
|
||||||
from dzgui.const.endpoints import IP_ECHO
|
from dzgui.const.endpoints import IP_ECHO
|
||||||
|
|
||||||
@@ -33,54 +35,21 @@ def ip2list(ip: str) -> list:
|
|||||||
return [int(digit) for digit in ip.split(".")]
|
return [int(digit) for digit in ip.split(".")]
|
||||||
|
|
||||||
|
|
||||||
# TODO: tests
|
def is_usable(ip: str) -> bool:
|
||||||
def is_reserved(digits: list) -> bool:
|
address = ip_address(ip)
|
||||||
reserved = [0, 10, 127]
|
if address.is_reserved or address.is_private:
|
||||||
|
return False
|
||||||
if digits[0] in reserved:
|
return True
|
||||||
return True
|
|
||||||
if digits[0] == 192 and digits[1] == 168:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def get_coords(ips: "Path", ip: str) -> Coords:
|
def get_coords(db_path: "Path", ip: str) -> Coords:
|
||||||
split = ip2list(ip)
|
if not is_usable(ip):
|
||||||
|
|
||||||
if is_reserved(split):
|
|
||||||
raise ReservedIPError(f"{ip} is a reserved IP address.")
|
raise ReservedIPError(f"{ip} is a reserved IP address.")
|
||||||
|
try:
|
||||||
prefix = f"^{split[0]}.{split[1]}."
|
lat, lon = read_from_table(db_path, ip)
|
||||||
|
return Coords(lat, lon)
|
||||||
if shutil.which("rg") is not None:
|
except Exception:
|
||||||
args = ["/usr/bin/rg", prefix, str(ips)]
|
raise GeolocationError("No matching records found")
|
||||||
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")
|
|
||||||
|
|
||||||
|
|
||||||
def get_local_ip() -> str:
|
def get_local_ip() -> str:
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ show_details = "Show server details"
|
|||||||
refresh_players = "Refresh player count"
|
refresh_players = "Refresh player count"
|
||||||
open_workshop = "Open in Steam Workshop"
|
open_workshop = "Open in Steam Workshop"
|
||||||
unsub_mod = "Unsubscribe mod"
|
unsub_mod = "Unsubscribe mod"
|
||||||
|
validate_mod = "Validate mod"
|
||||||
copy_name = "Copy name to clipboard"
|
copy_name = "Copy name to clipboard"
|
||||||
copy_ip = "Copy IP to clipboard"
|
copy_ip = "Copy IP to clipboard"
|
||||||
copy_log = "Copy record(s) to clipboard"
|
copy_log = "Copy record(s) to clipboard"
|
||||||
@@ -200,6 +201,8 @@ class ModPanelStrings:
|
|||||||
clear_highlights: str
|
clear_highlights: str
|
||||||
select_stale: str
|
select_stale: str
|
||||||
select_stale_tooltip: str
|
select_stale_tooltip: str
|
||||||
|
validate: str
|
||||||
|
validate_tooltip: str
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True, frozen=True)
|
@dataclass(slots=True, frozen=True)
|
||||||
@@ -319,6 +322,8 @@ mod_panel = ModPanelStrings(
|
|||||||
clear_highlights="Clears highlights and reverts the table to a default state",
|
clear_highlights="Clears highlights and reverts the table to a default state",
|
||||||
select_stale="Select stale",
|
select_stale="Select stale",
|
||||||
select_stale_tooltip="Only selects highlighted stale mods",
|
select_stale_tooltip="Only selects highlighted stale mods",
|
||||||
|
validate="Validate selected",
|
||||||
|
validate_tooltip="Validates selected mods and updates them if necessary",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,15 +7,15 @@ from dzgui.util.strings import (
|
|||||||
alert_button_tooltip,
|
alert_button_tooltip,
|
||||||
atomic_buttons,
|
atomic_buttons,
|
||||||
)
|
)
|
||||||
from dzgui.const.constants import (
|
from dzgui.const.icons import (
|
||||||
CLIPBOARD,
|
ICON_CLIPBOARD,
|
||||||
CLOSE,
|
ICON_CLOSE,
|
||||||
INPUT_KEYBOARD,
|
ICON_INPUT_KEYBOARD,
|
||||||
LIST_ADD,
|
ICON_LIST_ADD,
|
||||||
REFRESH_ICON,
|
ICON_REFRESH,
|
||||||
STEAM_ICON,
|
ICON_STEAM,
|
||||||
WARNING,
|
ICON_WARNING,
|
||||||
WEB_BROWSER,
|
ICON_WEB_BROWSER,
|
||||||
)
|
)
|
||||||
from dzgui.strings import connect_panel
|
from dzgui.strings import connect_panel
|
||||||
|
|
||||||
@@ -102,7 +102,7 @@ class LargeIconTextButton(IconButton):
|
|||||||
|
|
||||||
class ClipboardButton(IconTextButton):
|
class ClipboardButton(IconTextButton):
|
||||||
def __init__(self, controller: Union["Controller", None], func: Callable) -> None:
|
def __init__(self, controller: Union["Controller", None], func: Callable) -> None:
|
||||||
super().__init__(CLIPBOARD, atomic_buttons.copy)
|
super().__init__(ICON_CLIPBOARD, atomic_buttons.copy)
|
||||||
self.controller = controller
|
self.controller = controller
|
||||||
self.connect("clicked", self._on_button_clicked, func)
|
self.connect("clicked", self._on_button_clicked, func)
|
||||||
|
|
||||||
@@ -131,7 +131,7 @@ class CopyIpButton(ClipboardButton):
|
|||||||
|
|
||||||
class CloseButton(IconTextButton):
|
class CloseButton(IconTextButton):
|
||||||
def __init__(self, label: str) -> None:
|
def __init__(self, label: str) -> None:
|
||||||
super().__init__(icon=CLOSE, label=label)
|
super().__init__(icon=ICON_CLOSE, label=label)
|
||||||
|
|
||||||
# TODO: strings
|
# TODO: strings
|
||||||
self.set_tooltip_text("Unset this server as favorite")
|
self.set_tooltip_text("Unset this server as favorite")
|
||||||
@@ -139,14 +139,14 @@ class CloseButton(IconTextButton):
|
|||||||
|
|
||||||
class WebButton(IconTextButton):
|
class WebButton(IconTextButton):
|
||||||
def __init__(self, label: str) -> None:
|
def __init__(self, label: str) -> None:
|
||||||
super().__init__(icon=WEB_BROWSER, label=label)
|
super().__init__(icon=ICON_WEB_BROWSER, label=label)
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class RefreshButton(IconTextButton):
|
class RefreshButton(IconTextButton):
|
||||||
def __init__(self, controller: "Controller") -> None:
|
def __init__(self, controller: "Controller") -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
icon=REFRESH_ICON,
|
icon=ICON_REFRESH,
|
||||||
label=atomic_buttons.refresh,
|
label=atomic_buttons.refresh,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -211,7 +211,7 @@ class RefreshButton(IconTextButton):
|
|||||||
|
|
||||||
class KeysButton(IconTextButton):
|
class KeysButton(IconTextButton):
|
||||||
def __init__(self, controller: "Controller") -> None:
|
def __init__(self, controller: "Controller") -> None:
|
||||||
super().__init__(icon=INPUT_KEYBOARD, label=atomic_buttons.keys)
|
super().__init__(icon=ICON_INPUT_KEYBOARD, label=atomic_buttons.keys)
|
||||||
|
|
||||||
self.controller = controller
|
self.controller = controller
|
||||||
self.set_margin_top(10)
|
self.set_margin_top(10)
|
||||||
@@ -228,7 +228,7 @@ class KeysButton(IconTextButton):
|
|||||||
|
|
||||||
class SteamConnectButton(LargeIconTextButton):
|
class SteamConnectButton(LargeIconTextButton):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__(icon=STEAM_ICON, label=connect_panel.connect_button)
|
super().__init__(icon=ICON_STEAM, label=connect_panel.connect_button)
|
||||||
self.set_tooltip_text(connect_panel.connect_tooltip)
|
self.set_tooltip_text(connect_panel.connect_tooltip)
|
||||||
|
|
||||||
|
|
||||||
@@ -248,7 +248,7 @@ class SteamWorkshopButton(SteamTextButton):
|
|||||||
|
|
||||||
class AddButton(IconTextButton):
|
class AddButton(IconTextButton):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__(icon=LIST_ADD, label=connect_panel.add_button)
|
super().__init__(icon=ICON_LIST_ADD, label=connect_panel.add_button)
|
||||||
self.set_tooltip_text(connect_panel.add_tooltip)
|
self.set_tooltip_text(connect_panel.add_tooltip)
|
||||||
|
|
||||||
|
|
||||||
@@ -267,7 +267,7 @@ class LoggerAlertsButton(IconTextButton):
|
|||||||
separator = ","
|
separator = ","
|
||||||
concat = warnings_text + separator + errors_text
|
concat = warnings_text + separator + errors_text
|
||||||
super().__init__(
|
super().__init__(
|
||||||
icon=WARNING,
|
icon=ICON_WARNING,
|
||||||
label=f"Loaded with{concat}",
|
label=f"Loaded with{concat}",
|
||||||
)
|
)
|
||||||
self.set_halign(Gtk.Align.END)
|
self.set_halign(Gtk.Align.END)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from typing import Self, TYPE_CHECKING
|
from typing import Self, TYPE_CHECKING
|
||||||
|
|
||||||
from dzgui.const.constants import CARET_DOWN, CARET_UP, UDP_PORT
|
from dzgui.const.icons import ICON_CARET_DOWN, ICON_CARET_UP
|
||||||
|
from dzgui.const.constants import UDP_PORT
|
||||||
from dzgui.model.servers import ServerModelManager
|
from dzgui.model.servers import ServerModelManager
|
||||||
from dzgui.strings import connect_panel
|
from dzgui.strings import connect_panel
|
||||||
from dzgui.util.keys import is_ctrl_mask
|
from dzgui.util.keys import is_ctrl_mask
|
||||||
@@ -30,7 +31,7 @@ ROWS = 1
|
|||||||
|
|
||||||
class DockButton(IconButton):
|
class DockButton(IconButton):
|
||||||
def __init__(self, child: Gtk.Widget) -> None:
|
def __init__(self, child: Gtk.Widget) -> None:
|
||||||
super().__init__(CARET_DOWN)
|
super().__init__(ICON_CARET_DOWN)
|
||||||
|
|
||||||
self.expanded = True
|
self.expanded = True
|
||||||
self.child = child
|
self.child = child
|
||||||
@@ -43,12 +44,12 @@ class DockButton(IconButton):
|
|||||||
self.set_tooltip_text(self.collapse_text)
|
self.set_tooltip_text(self.collapse_text)
|
||||||
|
|
||||||
def collapse(self) -> None:
|
def collapse(self) -> None:
|
||||||
self.set_image(Icon(CARET_UP))
|
self.set_image(Icon(ICON_CARET_UP))
|
||||||
self.child.hide()
|
self.child.hide()
|
||||||
self.set_tooltip_text(self.expand_text)
|
self.set_tooltip_text(self.expand_text)
|
||||||
|
|
||||||
def expand(self) -> None:
|
def expand(self) -> None:
|
||||||
self.set_image(Icon(CARET_DOWN))
|
self.set_image(Icon(ICON_CARET_DOWN))
|
||||||
self.child.show()
|
self.child.show()
|
||||||
self.set_tooltip_text(self.collapse_text)
|
self.set_tooltip_text(self.collapse_text)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from typing import Any, Callable, TYPE_CHECKING
|
from typing import Any, Callable, TYPE_CHECKING
|
||||||
|
|
||||||
from dzgui.api.servers import validate_ip
|
from dzgui.api.servers import validate_ip
|
||||||
from dzgui.const.constants import VIEW_CONCEAL, VIEW_REVEAL
|
from dzgui.const.icons import ICON_VIEW_CONCEAL, ICON_VIEW_REVEAL
|
||||||
from dzgui.util.css import add_class, remove_class
|
from dzgui.util.css import add_class, remove_class
|
||||||
from dzgui.strings import connect_panel
|
from dzgui.strings import connect_panel
|
||||||
from dzgui.strings.errors import api_popover
|
from dzgui.strings.errors import api_popover
|
||||||
@@ -147,7 +147,7 @@ class APIEntry(Gtk.Box):
|
|||||||
self.entry = Gtk.Entry(
|
self.entry = Gtk.Entry(
|
||||||
width_chars=60, hexpand=True, placeholder_text="Enter API key"
|
width_chars=60, hexpand=True, placeholder_text="Enter API key"
|
||||||
)
|
)
|
||||||
self.entry.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, VIEW_REVEAL)
|
self.entry.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, ICON_VIEW_REVEAL)
|
||||||
self.entry.set_icon_activatable(Gtk.EntryIconPosition.SECONDARY, True)
|
self.entry.set_icon_activatable(Gtk.EntryIconPosition.SECONDARY, True)
|
||||||
self.entry.set_visibility(False)
|
self.entry.set_visibility(False)
|
||||||
self.entry.connect("changed", self._on_text_changed)
|
self.entry.connect("changed", self._on_text_changed)
|
||||||
@@ -206,9 +206,9 @@ class APIEntry(Gtk.Box):
|
|||||||
) -> None:
|
) -> None:
|
||||||
visible = widget.get_visibility()
|
visible = widget.get_visibility()
|
||||||
if visible:
|
if visible:
|
||||||
icon, state = VIEW_REVEAL, False
|
icon, state = ICON_VIEW_REVEAL, False
|
||||||
else:
|
else:
|
||||||
icon, state = VIEW_CONCEAL, True
|
icon, state = ICON_VIEW_CONCEAL, True
|
||||||
widget.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, icon)
|
widget.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, icon)
|
||||||
widget.set_visibility(state)
|
widget.set_visibility(state)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from dzgui.const.enum import NotebookPage
|
from dzgui.const.enum import NotebookPage
|
||||||
from dzgui.const.constants import HELP_BUBBLE
|
from dzgui.const.icons import ICON_HELP_BUBBLE
|
||||||
|
|
||||||
import gi
|
import gi
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ class InfoEventBox(Gtk.EventBox):
|
|||||||
self.text = text
|
self.text = text
|
||||||
|
|
||||||
self.icon = Gtk.Image.new_from_icon_name(
|
self.icon = Gtk.Image.new_from_icon_name(
|
||||||
HELP_BUBBLE, Gtk.IconSize.LARGE_TOOLBAR
|
ICON_HELP_BUBBLE, Gtk.IconSize.LARGE_TOOLBAR
|
||||||
)
|
)
|
||||||
self.icon.set_opacity(0.8)
|
self.icon.set_opacity(0.8)
|
||||||
box = Gtk.Box()
|
box = Gtk.Box()
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ from dzgui.const.constants import (
|
|||||||
NO_EXPAND,
|
NO_EXPAND,
|
||||||
NO_FILL,
|
NO_FILL,
|
||||||
NO_PADDING,
|
NO_PADDING,
|
||||||
SEARCH_ICON,
|
|
||||||
)
|
)
|
||||||
|
from dzgui.const.icons import ICON_SEARCH
|
||||||
from dzgui.util.format import format_exception
|
from dzgui.util.format import format_exception
|
||||||
from dzgui.util.strings import all_maps
|
from dzgui.util.strings import all_maps
|
||||||
from dzgui.views.components.maps_combo import MapsCombo
|
from dzgui.views.components.maps_combo import MapsCombo
|
||||||
@@ -109,7 +109,7 @@ class KeywordEntry(Gtk.Entry):
|
|||||||
self.connect("key-press-event", self._on_keypress)
|
self.connect("key-press-event", self._on_keypress)
|
||||||
self.connect("icon-release", lambda *args: self.activate())
|
self.connect("icon-release", lambda *args: self.activate())
|
||||||
|
|
||||||
self.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, SEARCH_ICON)
|
self.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, ICON_SEARCH)
|
||||||
self.set_icon_activatable(Gtk.EntryIconPosition.SECONDARY, True)
|
self.set_icon_activatable(Gtk.EntryIconPosition.SECONDARY, True)
|
||||||
|
|
||||||
self.emitter.connect("load_maps", self._on_maps_loaded)
|
self.emitter.connect("load_maps", self._on_maps_loaded)
|
||||||
@@ -313,6 +313,8 @@ class FilterPanel(Gtk.Box):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def toggle_check(self, digit: int) -> None:
|
def toggle_check(self, digit: int) -> None:
|
||||||
|
if not self.button_grid.is_sensitive():
|
||||||
|
return
|
||||||
checks = self.button_grid.get_checkboxes()
|
checks = self.button_grid.get_checkboxes()
|
||||||
check = checks[digit]
|
check = checks[digit]
|
||||||
state = check.get_active()
|
state = check.get_active()
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
from typing import Self
|
from typing import Self
|
||||||
from warnings import deprecated
|
from warnings import deprecated
|
||||||
|
|
||||||
from dzgui.const.constants import ERROR, FLATPAK_RUN_CMD, FLATPAK_SANDBOX, STEAM_CMD
|
from dzgui.const.constants import FLATPAK_RUN_CMD, FLATPAK_SANDBOX, STEAM_CMD
|
||||||
|
from dzgui.const.icons import ICON_ERROR
|
||||||
from dzgui.init.prereqs import has_steam_client, has_flatpak_client
|
from dzgui.init.prereqs import has_steam_client, has_flatpak_client
|
||||||
from dzgui.strings import options
|
from dzgui.strings import options
|
||||||
from dzgui.views.components.box import HBox
|
from dzgui.views.components.box import HBox
|
||||||
@@ -73,7 +74,7 @@ class ErrorPopover(Gtk.Popover):
|
|||||||
|
|
||||||
self.hbox = HBox()
|
self.hbox = HBox()
|
||||||
self.label = Gtk.Label(label="", margin_start=10, margin_end=10)
|
self.label = Gtk.Label(label="", margin_start=10, margin_end=10)
|
||||||
error_icon = Icon(ERROR, margin_start=10)
|
error_icon = Icon(ICON_ERROR, margin_start=10)
|
||||||
self.hbox.extend([error_icon, self.label])
|
self.hbox.extend([error_icon, self.label])
|
||||||
self.add(self.hbox)
|
self.add(self.hbox)
|
||||||
self.show_all()
|
self.show_all()
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ class ModSelectionPanel(Gtk.Box):
|
|||||||
ModButton.SELECT_ALL,
|
ModButton.SELECT_ALL,
|
||||||
ModButton.UNSELECT_ALL,
|
ModButton.UNSELECT_ALL,
|
||||||
ModButton.UNSUB_SELECTED,
|
ModButton.UNSUB_SELECTED,
|
||||||
|
ModButton.VALIDATE,
|
||||||
)
|
)
|
||||||
for button in buttons:
|
for button in buttons:
|
||||||
b = EnumeratedModButton(button)
|
b = EnumeratedModButton(button)
|
||||||
@@ -114,3 +115,5 @@ class ModSelectionPanel(Gtk.Box):
|
|||||||
self.swap_sensitive(False)
|
self.swap_sensitive(False)
|
||||||
case ModButton.SELECT_STALE:
|
case ModButton.SELECT_STALE:
|
||||||
self.controller.select_colorized()
|
self.controller.select_colorized()
|
||||||
|
case ModButton.VALIDATE:
|
||||||
|
self.controller.validate_mods()
|
||||||
@@ -132,9 +132,9 @@ class BootDialog(ColorAwareApp, Gtk.Dialog): # type: ignore
|
|||||||
preboot.signatures,
|
preboot.signatures,
|
||||||
False,
|
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
|
# 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),
|
(StoredFunc(check_updates, self.version), preboot.updates, True),
|
||||||
]
|
]
|
||||||
self.results: list[Any] = []
|
self.results: list[Any] = []
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from dataclasses import dataclass
|
|||||||
from datetime import datetime, time, timedelta
|
from datetime import datetime, time, timedelta
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
|
|
||||||
from dzgui.const.constants import DAY_ICON, NIGHT_ICON
|
from dzgui.const.icons import ICON_DAY, ICON_NIGHT
|
||||||
from dzgui.strings import calc
|
from dzgui.strings import calc
|
||||||
from dzgui.views.components.icon import Icon
|
from dzgui.views.components.icon import Icon
|
||||||
from dzgui.views.components.labels import BoldLabel
|
from dzgui.views.components.labels import BoldLabel
|
||||||
@@ -385,7 +385,7 @@ class TimePickerFrame(Gtk.Frame):
|
|||||||
h1 = BoldLabel(calc.heading_server_time)
|
h1 = BoldLabel(calc.heading_server_time)
|
||||||
h2 = BoldLabel(calc.heading_target_time)
|
h2 = BoldLabel(calc.heading_target_time)
|
||||||
|
|
||||||
self.icon_indicator = Icon(DAY_ICON)
|
self.icon_indicator = Icon(ICON_DAY)
|
||||||
self.update_server_icon()
|
self.update_server_icon()
|
||||||
|
|
||||||
server_time_box = HBox(10)
|
server_time_box = HBox(10)
|
||||||
@@ -411,7 +411,7 @@ class TimePickerFrame(Gtk.Frame):
|
|||||||
self.add(flowbox)
|
self.add(flowbox)
|
||||||
|
|
||||||
def update_server_icon(self) -> None:
|
def update_server_icon(self) -> None:
|
||||||
icon = DAY_ICON if self.server_time.is_day() else NIGHT_ICON
|
icon = ICON_DAY if self.server_time.is_day() else ICON_NIGHT
|
||||||
self.icon_indicator.set_icon_name(icon)
|
self.icon_indicator.set_icon_name(icon)
|
||||||
|
|
||||||
def _on_server_clock_increment(self, emitter: Emitter, time_now: datetime) -> None:
|
def _on_server_clock_increment(self, emitter: Emitter, time_now: datetime) -> None:
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import gi
|
|||||||
import html
|
import html
|
||||||
from typing import Self, TYPE_CHECKING
|
from typing import Self, TYPE_CHECKING
|
||||||
|
|
||||||
from dzgui.const.constants import CALCULATOR
|
from dzgui.const.icons import ICON_CALCULATOR
|
||||||
from dzgui.const.enum import ContextMenuGroup
|
from dzgui.const.enum import ContextMenuGroup
|
||||||
from dzgui.model.model_factory import ModelFactory
|
from dzgui.model.model_factory import ModelFactory
|
||||||
from dzgui.util import css
|
from dzgui.util import css
|
||||||
@@ -110,7 +110,7 @@ class ServerDetailsDialog(ServerDialog):
|
|||||||
text = format_hyperlinks(text)
|
text = format_hyperlinks(text)
|
||||||
self.description.set_markup(html.escape(text))
|
self.description.set_markup(html.escape(text))
|
||||||
|
|
||||||
calc_button = IconTextButton(CALCULATOR, dialogs.toggle_calc)
|
calc_button = IconTextButton(ICON_CALCULATOR, dialogs.toggle_calc)
|
||||||
calc_button.connect("clicked", self._on_calc_pressed)
|
calc_button.connect("clicked", self._on_calc_pressed)
|
||||||
calc_button.set_halign(Gtk.Align.END)
|
calc_button.set_halign(Gtk.Align.END)
|
||||||
calc_button.set_valign(Gtk.Align.START)
|
calc_button.set_valign(Gtk.Align.START)
|
||||||
|
|||||||
@@ -3,14 +3,12 @@ from enum import Enum
|
|||||||
from typing import Self, TYPE_CHECKING, Union
|
from typing import Self, TYPE_CHECKING, Union
|
||||||
|
|
||||||
from dzgui.util import css
|
from dzgui.util import css
|
||||||
|
from dzgui.const.icons import ICON_EDIT_DELETE, ICON_FOLDER, ICON_WARNING
|
||||||
from dzgui.const.constants import (
|
from dzgui.const.constants import (
|
||||||
APPID_DAYZ,
|
APPID_DAYZ,
|
||||||
APPID_DAYZ_EXP,
|
APPID_DAYZ_EXP,
|
||||||
APPNAME_DAYZ,
|
APPNAME_DAYZ,
|
||||||
APPNAME_DAYZ_EXP_HUMAN,
|
APPNAME_DAYZ_EXP_HUMAN,
|
||||||
EDIT_DELETE,
|
|
||||||
FOLDER,
|
|
||||||
WARNING,
|
|
||||||
)
|
)
|
||||||
from dzgui.const.enum import ContextMenuGroup, NotebookPage
|
from dzgui.const.enum import ContextMenuGroup, NotebookPage
|
||||||
from dzgui.managers.offline import OfflineManager
|
from dzgui.managers.offline import OfflineManager
|
||||||
@@ -87,7 +85,7 @@ class FolderHBox(HBox):
|
|||||||
self.emitter = controller.get_emitter()
|
self.emitter = controller.get_emitter()
|
||||||
|
|
||||||
self.eb = InfoEventBox(eb_text, controller)
|
self.eb = InfoEventBox(eb_text, controller)
|
||||||
self.button = IconTextButton(FOLDER, btn_label, Gtk.PositionType.LEFT)
|
self.button = IconTextButton(ICON_FOLDER, btn_label, Gtk.PositionType.LEFT)
|
||||||
self.button.set_halign(Gtk.Align.START)
|
self.button.set_halign(Gtk.Align.START)
|
||||||
self.button.set_image_position(Gtk.PositionType.LEFT)
|
self.button.set_image_position(Gtk.PositionType.LEFT)
|
||||||
|
|
||||||
@@ -99,7 +97,7 @@ class FolderHBox(HBox):
|
|||||||
self.label = Gtk.Label()
|
self.label = Gtk.Label()
|
||||||
self.scrolled_label.add(self.label)
|
self.scrolled_label.add(self.label)
|
||||||
|
|
||||||
self.unset_button = IconTextButton(EDIT_DELETE, offline.unset_button)
|
self.unset_button = IconTextButton(ICON_EDIT_DELETE, offline.unset_button)
|
||||||
self.unset_button.connect("clicked", self._on_unset_clicked)
|
self.unset_button.connect("clicked", self._on_unset_clicked)
|
||||||
|
|
||||||
self.spinner = Gtk.Spinner()
|
self.spinner = Gtk.Spinner()
|
||||||
@@ -195,7 +193,7 @@ class ModFrame(HeadingFrame):
|
|||||||
self.tree_vbox.extend([self.scrolled, self.status])
|
self.tree_vbox.extend([self.scrolled, self.status])
|
||||||
|
|
||||||
self.no_mods = HBox(spacing=10)
|
self.no_mods = HBox(spacing=10)
|
||||||
no_mods_icon = Icon(WARNING, margin_start=10)
|
no_mods_icon = Icon(ICON_WARNING, margin_start=10)
|
||||||
no_mods_label = Gtk.Label(label=offline.no_local_mods, halign=Gtk.Align.START)
|
no_mods_label = Gtk.Label(label=offline.no_local_mods, halign=Gtk.Align.START)
|
||||||
self.no_mods.extend([no_mods_icon, no_mods_label])
|
self.no_mods.extend([no_mods_icon, no_mods_label])
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from pathlib import Path
|
|||||||
from typing import Callable, TYPE_CHECKING
|
from typing import Callable, TYPE_CHECKING
|
||||||
|
|
||||||
from dzgui.api import pefile as PeFile
|
from dzgui.api import pefile as PeFile
|
||||||
|
from dzgui.api.steam import get_last_updated
|
||||||
|
|
||||||
from dzgui.config import query
|
from dzgui.config import query
|
||||||
from dzgui.const.constants import (
|
from dzgui.const.constants import (
|
||||||
@@ -15,9 +16,8 @@ from dzgui.const.constants import (
|
|||||||
NO_EXPAND,
|
NO_EXPAND,
|
||||||
NO_FILL,
|
NO_FILL,
|
||||||
NO_PADDING,
|
NO_PADDING,
|
||||||
VIEW_CONCEAL,
|
|
||||||
VIEW_REVEAL,
|
|
||||||
)
|
)
|
||||||
|
from dzgui.const.icons import ICON_VIEW_CONCEAL, ICON_VIEW_REVEAL
|
||||||
from dzgui.const.endpoints import STEAM_API_SETUP
|
from dzgui.const.endpoints import STEAM_API_SETUP
|
||||||
from dzgui.const.enum import Preferences, ServerTab
|
from dzgui.const.enum import Preferences, ServerTab
|
||||||
from dzgui.strings import options
|
from dzgui.strings import options
|
||||||
@@ -66,7 +66,7 @@ class SubmitField(Gtk.Box):
|
|||||||
# TODO: audit for all applicable callbacks
|
# TODO: audit for all applicable callbacks
|
||||||
if private:
|
if private:
|
||||||
self.entry.set_icon_from_icon_name(
|
self.entry.set_icon_from_icon_name(
|
||||||
Gtk.EntryIconPosition.SECONDARY, VIEW_REVEAL
|
Gtk.EntryIconPosition.SECONDARY, ICON_VIEW_REVEAL
|
||||||
)
|
)
|
||||||
self.entry.set_icon_activatable(Gtk.EntryIconPosition.SECONDARY, True)
|
self.entry.set_icon_activatable(Gtk.EntryIconPosition.SECONDARY, True)
|
||||||
self.entry.connect("icon-release", self._on_icon_release)
|
self.entry.connect("icon-release", self._on_icon_release)
|
||||||
@@ -130,9 +130,9 @@ class SubmitField(Gtk.Box):
|
|||||||
) -> None:
|
) -> None:
|
||||||
visible = widget.get_visibility()
|
visible = widget.get_visibility()
|
||||||
if visible:
|
if visible:
|
||||||
icon, state = VIEW_REVEAL, False
|
icon, state = ICON_VIEW_REVEAL, False
|
||||||
else:
|
else:
|
||||||
icon, state = VIEW_CONCEAL, True
|
icon, state = ICON_VIEW_CONCEAL, True
|
||||||
widget.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, icon)
|
widget.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, icon)
|
||||||
widget.set_visibility(state)
|
widget.set_visibility(state)
|
||||||
|
|
||||||
@@ -340,8 +340,10 @@ class Options(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore
|
|||||||
[LeftLabel(options.flatpak_label), self.flatpak_toggle],
|
[LeftLabel(options.flatpak_label), self.flatpak_toggle],
|
||||||
)
|
)
|
||||||
|
|
||||||
self.dayz_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)
|
self.dayz_exp_version_label = Gtk.Label(
|
||||||
|
label=strings.null, halign=Gtk.Align.START
|
||||||
|
)
|
||||||
|
|
||||||
version_rows = [
|
version_rows = [
|
||||||
[LeftLabel(APPNAME_DAYZ), self.dayz_version_label],
|
[LeftLabel(APPNAME_DAYZ), self.dayz_version_label],
|
||||||
@@ -467,10 +469,18 @@ class Options(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore
|
|||||||
dayz_version = PeFile.get_pretty_version(steam_path, APPID_DAYZ)
|
dayz_version = PeFile.get_pretty_version(steam_path, APPID_DAYZ)
|
||||||
if dayz_version is None:
|
if dayz_version is None:
|
||||||
dayz_version = strings.null
|
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)
|
dayz_exp_version = PeFile.get_pretty_version(steam_path, APPID_DAYZ_EXP)
|
||||||
if dayz_exp_version is None:
|
if dayz_exp_version is None:
|
||||||
dayz_exp_version = strings.null
|
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_version_label.set_text(dayz_version)
|
||||||
self.dayz_exp_version_label.set_text(dayz_exp_version)
|
self.dayz_exp_version_label.set_text(dayz_exp_version)
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Self, Sequence, TYPE_CHECKING
|
from typing import Self, Sequence, TYPE_CHECKING
|
||||||
|
|
||||||
from dzgui.const.constants import (
|
from dzgui.const.icons import (
|
||||||
ERROR,
|
ICON_ERROR,
|
||||||
WARNING,
|
ICON_WARNING,
|
||||||
)
|
)
|
||||||
from dzgui.const.enum import NotebookPage
|
from dzgui.const.enum import NotebookPage
|
||||||
from dzgui.util.css import add_class
|
from dzgui.util.css import add_class
|
||||||
@@ -166,7 +166,7 @@ class PreConnectionAssistant(Gtk.Box):
|
|||||||
|
|
||||||
# TODO: abstract into components
|
# TODO: abstract into components
|
||||||
self.warning_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
self.warning_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||||
self.warning_tree = MaskedTree(WARNING)
|
self.warning_tree = MaskedTree(ICON_WARNING)
|
||||||
self.warning_placeholder = Placeholder(preconnect.placeholder_no_warnings)
|
self.warning_placeholder = Placeholder(preconnect.placeholder_no_warnings)
|
||||||
self.warning_box.add(self.warning_tree)
|
self.warning_box.add(self.warning_tree)
|
||||||
self.warning_box.add(self.warning_placeholder)
|
self.warning_box.add(self.warning_placeholder)
|
||||||
@@ -175,7 +175,7 @@ class PreConnectionAssistant(Gtk.Box):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.error_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
self.error_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||||
self.error_tree = MaskedTree(ERROR)
|
self.error_tree = MaskedTree(ICON_ERROR)
|
||||||
self.error_placeholder = Placeholder(preconnect.no_errors)
|
self.error_placeholder = Placeholder(preconnect.no_errors)
|
||||||
self.error_box.add(self.error_tree)
|
self.error_box.add(self.error_tree)
|
||||||
self.error_box.add(self.error_placeholder)
|
self.error_box.add(self.error_placeholder)
|
||||||
|
|||||||
@@ -108,14 +108,9 @@ class TreeView(CursorMixin, Gtk.TreeView): # type: ignore
|
|||||||
|
|
||||||
def _on_key_release(self, treeview: Gtk.TreeView, event: Gdk.EventKey) -> None:
|
def _on_key_release(self, treeview: Gtk.TreeView, event: Gdk.EventKey) -> None:
|
||||||
"""
|
"""
|
||||||
Suppresses spamming on keydown
|
Adds debounce to key press/release events to prevent spamming
|
||||||
TODO: explain this better
|
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()
|
model = self.get_model()
|
||||||
if model is None:
|
if model is None:
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ description = "DayZ server browser and mod manager for Linux"
|
|||||||
authors = [
|
authors = [
|
||||||
{name = "aclist"}
|
{name = "aclist"}
|
||||||
]
|
]
|
||||||
version = "7.0.0b29"
|
version = "7.0.0b30"
|
||||||
license = "GPL-3.0-or-later"
|
license = "GPL-3.0-or-later"
|
||||||
license-files = ["LICENSE"]
|
license-files = ["LICENSE"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from dzgui.app_init import build_gui
|
||||||
|
|
||||||
|
|
||||||
|
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())
|
||||||
|
|
||||||
|
|
||||||
|
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,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 _pytest.monkeypatch import MonkeyPatch
|
||||||
from pathlib import Path
|
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
|
from tests.fixtures import fixture_path
|
||||||
|
|
||||||
pytestmark = pytest.mark.apitest
|
pytestmark = pytest.mark.apitest
|
||||||
@@ -43,6 +43,14 @@ def patch_api() -> None:
|
|||||||
mp.undo()
|
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:
|
def test_app_allows_downloads(steam_path) -> None:
|
||||||
assert get_app_allows_downloads(steam_path, 111) is True
|
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
|
# TODO: use a static fixture instead of system config
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@pytest.mark.realconfig
|
|
||||||
def config():
|
def config():
|
||||||
paths = get_xdg_paths()
|
paths = get_xdg_paths()
|
||||||
xdg = parse_filepaths(paths)
|
xdg = parse_filepaths(paths)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import configparser
|
import configparser
|
||||||
import pytest
|
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.config.xdg import get_xdg_paths
|
||||||
from dzgui.util.flatpak import apply_overrides
|
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:
|
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(";")
|
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 os
|
||||||
|
import sqlite3
|
||||||
import pytest
|
import pytest
|
||||||
import tempfile
|
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
|
@pytest.mark.slow
|
||||||
def test_stripping():
|
def test_stripping(csv):
|
||||||
d = tempfile.TemporaryDirectory()
|
with open(csv, "rb") as f:
|
||||||
path = Path(d.name).joinpath("ips.csv")
|
|
||||||
get_ipdb(path)
|
|
||||||
with open(path, "rb") as f:
|
|
||||||
first = f.readline().decode().rstrip()
|
first = f.readline().decode().rstrip()
|
||||||
try:
|
try:
|
||||||
f.seek(-2, os.SEEK_END)
|
f.seek(-2, os.SEEK_END)
|
||||||
@@ -25,3 +49,16 @@ def test_stripping():
|
|||||||
|
|
||||||
assert first_els[0] == "0.0.0.0"
|
assert first_els[0] == "0.0.0.0"
|
||||||
assert last_els[0] == "224.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
|
||||||
@@ -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