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