mirror of
https://github.com/aclist/dztui.git
synced 2026-10-31 15:12:48 +01:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12e91c4ba8 |
No files matched your search
@@ -52,7 +52,6 @@
|
|||||||
- Filter button tooltips
|
- Filter button tooltips
|
||||||
- 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
|
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Conform to PEP 440 versioning for beta versions
|
- Conform to PEP 440 versioning for beta versions
|
||||||
@@ -91,7 +90,6 @@
|
|||||||
- Rare segfaults when changing maps (threading)
|
- Rare segfaults when changing maps (threading)
|
||||||
- Moved dialogs out of threads
|
- Moved dialogs out of threads
|
||||||
- History table not being in lock-step with state file
|
- History table not being in lock-step with state file
|
||||||
- Current keyword filter not being applied when refreshing table
|
|
||||||
|
|
||||||
### Unreleased
|
### Unreleased
|
||||||
- Load offline mods
|
- Load offline mods
|
||||||
|
|||||||
@@ -1,23 +1,15 @@
|
|||||||
## What this is
|
## What this is
|
||||||
DZGUI is a turnkey server browser and mod manager for DayZ on Linux. It allows you to connect to both official and modded/community DayZ servers on Linux via a graphical user interface (GUI).
|
DZGUI is a turnkey browser and mod manager for DayZ on Linux. It allows you to connect to both official and modded/community DayZ servers on Linux via a graphical user interface (GUI).
|
||||||
|
|
||||||
This overcomes certain limitations in the Linux client and helps prepare the game to launch by providing features like:
|
This overcomes certain limitations in the Linux client and helps prepare the game to launch by providing features like:
|
||||||
|
|
||||||
- Search for and display global and LAN servers via an interactive table
|
- Search for and display global and LAN servers via an interactive table
|
||||||
- Supports official, third-party, and modded DayZ servers
|
- Supports official, third-party, and modded DayZ servers
|
||||||
- Automatically find and prepare mods being requested by servers
|
- Automatically find and prepare mods being requested by servers
|
||||||
|
- Add/delete/manage favorite servers by IP or ID
|
||||||
- Quick-connect to favorite/recent servers
|
- Quick-connect to favorite/recent servers
|
||||||
- Manage and delete local mods
|
- Manage local mods and symlinks
|
||||||
- And more
|
- Prepare launch options to pass to Steam
|
||||||
|
|
||||||
## Design philosophy
|
|
||||||
|
|
||||||
DZGUI follows a specific set of guidelines, enumerated below:
|
|
||||||
|
|
||||||
- Provide a free and open-source tool under GPL
|
|
||||||
- Do not use undocumented, third-party servers to aggregate data; no central "DZGUI" server
|
|
||||||
- All connections go to Steam and DayZ servers directly; the tool should function in perpetuity without intermediary endpoints
|
|
||||||
- Target Linux desktop and Steam deck specifically; not a cross-platform tool
|
|
||||||
|
|
||||||
## Setup and usage
|
## Setup and usage
|
||||||
|
|
||||||
@@ -26,7 +18,7 @@ Please refer to the documentation for installation and setup instructions:
|
|||||||
- [GitHub](https://aclist.github.io/dzgui/index.html)
|
- [GitHub](https://aclist.github.io/dzgui/index.html)
|
||||||
- [Codeberg (Mirror)](https://aclist.codeberg.page)
|
- [Codeberg (Mirror)](https://aclist.codeberg.page)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Attribution
|
## Attribution
|
||||||
|
|
||||||
|
|||||||
+29
-29
@@ -54,16 +54,19 @@ class Res:
|
|||||||
|
|
||||||
@dataclass(slots=True, frozen=True)
|
@dataclass(slots=True, frozen=True)
|
||||||
class Details:
|
class Details:
|
||||||
data: list[list[str]]
|
data: Union[list, None]
|
||||||
name: str
|
|
||||||
description: str
|
description: str
|
||||||
gametime: str
|
success: bool
|
||||||
day_accel: float
|
|
||||||
night_accel: float
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True, frozen=True)
|
@dataclass(slots=True)
|
||||||
class Record:
|
class Record:
|
||||||
|
# TODO: investigate whether this enum is still being used;
|
||||||
|
# can this be frozen?
|
||||||
|
"""
|
||||||
|
The gameport field is manipulated by the RowType.CONN_BY_IP method
|
||||||
|
"""
|
||||||
|
|
||||||
ip: str
|
ip: str
|
||||||
gameport: int
|
gameport: int
|
||||||
qport: int
|
qport: int
|
||||||
@@ -280,41 +283,43 @@ def query_direct(ip: str, qport: int, timeout: float = 3.0) -> dict[str, Any] |
|
|||||||
info = a2s.info((ip, qport), timeout)
|
info = a2s.info((ip, qport), timeout)
|
||||||
return source_info_to_dict(ip, qport, info)
|
return source_info_to_dict(ip, qport, info)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(e)
|
logger.warning(e)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def get_details(record: Record) -> Details | None:
|
def get_details(record: Record) -> Details:
|
||||||
|
# TODO: consolidate try/except statements
|
||||||
ip = record.ip
|
ip = record.ip
|
||||||
qport = record.qport
|
qport = record.qport
|
||||||
default_str = strings.none_provided
|
default_str = strings.none_provided
|
||||||
|
|
||||||
try:
|
try:
|
||||||
info = a2s.info((ip, qport))
|
info = a2s.info((ip, qport))
|
||||||
|
except (TimeoutError, a2s.exceptions.BrokenMessageError):
|
||||||
|
return Details(None, default_str, False)
|
||||||
|
try:
|
||||||
rules = dayzquery.dayz_rules((ip, qport))
|
rules = dayzquery.dayz_rules((ip, qport))
|
||||||
|
except Exception:
|
||||||
|
return Details(None, default_str, False)
|
||||||
|
|
||||||
|
try:
|
||||||
keywords = info.keywords.split(",")
|
keywords = info.keywords.split(",")
|
||||||
except Exception as e:
|
except AttributeError:
|
||||||
logger.critical(e)
|
return Details(None, default_str, False)
|
||||||
return None
|
|
||||||
|
|
||||||
battleye = strings.disabled
|
battleye = strings.disabled
|
||||||
if "battleye" in keywords:
|
if "battleye" in keywords:
|
||||||
battleye = strings.enabled
|
battleye = strings.enabled
|
||||||
|
|
||||||
name = info.server_name
|
day_accel = 0.0
|
||||||
day_accel = 1.0
|
night_accel = 0.0
|
||||||
night_accel = 1.0
|
|
||||||
gametime = "00:00"
|
|
||||||
for keyword in keywords:
|
for keyword in keywords:
|
||||||
if "etm" in keyword:
|
if "etm" in keyword:
|
||||||
day_accel = float(keyword.lstrip("etm"))
|
day_accel = float(keyword.lstrip("etm"))
|
||||||
day_accel = float(f"{day_accel:g}")
|
day_accel = float(f"{day_accel:g}")
|
||||||
if "entm" in keyword:
|
if "entm" in keywords:
|
||||||
night_accel = float(keyword.lstrip("entm"))
|
night_accel = float(keyword.lstrip("entm"))
|
||||||
night_accel = float(f"{night_accel:g}")
|
night_accel = float(f"{night_accel:g}")
|
||||||
time_reg = r"^(0[0-9]|1[0-9]|2[0-3]):([0-5][0-9]$)"
|
|
||||||
if re.match(time_reg, keyword):
|
|
||||||
gametime = keyword
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
password = info.password_protected
|
password = info.password_protected
|
||||||
@@ -341,15 +346,10 @@ def get_details(record: Record) -> Details | None:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
dlc = rules.dlc_flags
|
dlc = rules.dlc_flags
|
||||||
match dlc:
|
if dlc == 0:
|
||||||
case 0:
|
dlc = strings.none
|
||||||
dlc = strings.dlc_none
|
if dlc == 2:
|
||||||
case 2:
|
dlc = strings.dlc_frostline
|
||||||
dlc = strings.dlc_frostline
|
|
||||||
case 4:
|
|
||||||
dlc = strings.dlc_badlands
|
|
||||||
case _:
|
|
||||||
dlc = strings.unspecified
|
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
dlc = strings.unspecified
|
dlc = strings.unspecified
|
||||||
|
|
||||||
@@ -380,7 +380,7 @@ def get_details(record: Record) -> Details | None:
|
|||||||
["Version", version],
|
["Version", version],
|
||||||
]
|
]
|
||||||
|
|
||||||
return Details(rows, name, description, gametime, day_accel, night_accel)
|
return Details(rows, description, True)
|
||||||
|
|
||||||
|
|
||||||
def ping(ip: str, qport: int) -> int:
|
def ping(ip: str, qport: int) -> int:
|
||||||
|
|||||||
+9
-39
@@ -6,7 +6,6 @@ import subprocess
|
|||||||
import vdf # type: ignore
|
import vdf # type: ignore
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from requests.exceptions import HTTPError
|
|
||||||
from typing import Any, Union
|
from typing import Any, Union
|
||||||
from warnings import deprecated
|
from warnings import deprecated
|
||||||
|
|
||||||
@@ -33,7 +32,6 @@ from dzgui.const.endpoints import (
|
|||||||
UNSUB_ENDPOINT,
|
UNSUB_ENDPOINT,
|
||||||
)
|
)
|
||||||
from dzgui.strings import wizard
|
from dzgui.strings import wizard
|
||||||
from dzgui.strings.errors import api_validation_error, workshop_endpoint_error
|
|
||||||
from dzgui.util.bash import concat_bash_args
|
from dzgui.util.bash import concat_bash_args
|
||||||
|
|
||||||
logger = logging.getLogger(APP_NAME)
|
logger = logging.getLogger(APP_NAME)
|
||||||
@@ -153,9 +151,7 @@ def get_remote_signatures(mods: list[str]) -> list[tuple[str, str, int, int]]:
|
|||||||
return hashes
|
return hashes
|
||||||
|
|
||||||
|
|
||||||
def connect_debug(
|
def connect_debug(client: str, addr: str, appid: int, name: str, mods: list[str]) -> str:
|
||||||
client: str, addr: str, appid: int, name: str, mods: list[str]
|
|
||||||
) -> str:
|
|
||||||
concat = concat_mods(mods)
|
concat = concat_mods(mods)
|
||||||
client_args = concat_bash_args(client)
|
client_args = concat_bash_args(client)
|
||||||
params = [
|
params = [
|
||||||
@@ -171,7 +167,6 @@ def connect_debug(
|
|||||||
client_args.extend(params)
|
client_args.extend(params)
|
||||||
return " ".join(client_args)
|
return " ".join(client_args)
|
||||||
|
|
||||||
|
|
||||||
# TODO: set config to name=user, use official server and no mods,
|
# TODO: set config to name=user, use official server and no mods,
|
||||||
# ensure that formatted string is identical to fixture with same hash
|
# ensure that formatted string is identical to fixture with same hash
|
||||||
def connect(client: str, addr: str, appid: int, name: str, mods: list[str]) -> int:
|
def connect(client: str, addr: str, appid: int, name: str, mods: list[str]) -> int:
|
||||||
@@ -240,18 +235,14 @@ def find_user_id(path: Path) -> str | None:
|
|||||||
try:
|
try:
|
||||||
with open(resolved_path, "r") as f:
|
with open(resolved_path, "r") as f:
|
||||||
v = vdf.load(f)
|
v = vdf.load(f)
|
||||||
users = v["users"]
|
# NOTE: beta client
|
||||||
# NOTE: beta client: /package/beta
|
# /package/beta
|
||||||
last_user = str(next(iter(users)))
|
if len(v["users"]) == 1:
|
||||||
if len(users) == 1:
|
return str(list(v["users"].keys())[0])
|
||||||
return last_user
|
for user in v["users"]:
|
||||||
last_stamp = int(users[last_user]["Timestamp"])
|
if v["users"][user]["MostRecent"] == "1":
|
||||||
for user in users:
|
return str(user)
|
||||||
stamp = int(users[user]["Timestamp"])
|
return None
|
||||||
if stamp > last_stamp:
|
|
||||||
last_stamp = stamp
|
|
||||||
last_user = user
|
|
||||||
return last_user
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(e)
|
logger.debug(e)
|
||||||
return None
|
return None
|
||||||
@@ -264,16 +255,6 @@ def find_user_id_32(path: Path) -> int:
|
|||||||
return int(uid) & 0xFFFFFFFF
|
return int(uid) & 0xFFFFFFFF
|
||||||
|
|
||||||
|
|
||||||
class SteamAPIKeyError(Exception):
|
|
||||||
def __init__(self, msg: str | None = None):
|
|
||||||
super().__init__(msg or api_validation_error)
|
|
||||||
|
|
||||||
|
|
||||||
class SteamWorkshopEndpointError(Exception):
|
|
||||||
def __init__(self, msg: str | None = None):
|
|
||||||
super().__init__(msg or workshop_endpoint_error)
|
|
||||||
|
|
||||||
|
|
||||||
def update_workshop(key: str, mod: int, endpoint: str) -> None:
|
def update_workshop(key: str, mod: int, endpoint: str) -> None:
|
||||||
payload: dict[str, Union[int, str]] = {
|
payload: dict[str, Union[int, str]] = {
|
||||||
"publishedfileid": mod,
|
"publishedfileid": mod,
|
||||||
@@ -285,19 +266,8 @@ def update_workshop(key: str, mod: int, endpoint: str) -> None:
|
|||||||
try:
|
try:
|
||||||
res = requests.post(endpoint, params=payload, timeout=REQUEST_TIMEOUT)
|
res = requests.post(endpoint, params=payload, timeout=REQUEST_TIMEOUT)
|
||||||
res.raise_for_status()
|
res.raise_for_status()
|
||||||
except HTTPError as e:
|
|
||||||
logger.critical(e)
|
|
||||||
match res.status_code:
|
|
||||||
case 403:
|
|
||||||
raise SteamAPIKeyError()
|
|
||||||
case 404:
|
|
||||||
raise SteamWorkshopEndpointError()
|
|
||||||
case _:
|
|
||||||
# TODO: apply redaction filter
|
|
||||||
raise e
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.critical(e)
|
logger.critical(e)
|
||||||
raise e
|
|
||||||
|
|
||||||
|
|
||||||
def subscribe(key: str, mod: int) -> None:
|
def subscribe(key: str, mod: int) -> None:
|
||||||
|
|||||||
@@ -36,8 +36,7 @@ HEX_GREEN = "#32CD32"
|
|||||||
HEX_RED = "#FF0000"
|
HEX_RED = "#FF0000"
|
||||||
HEX_ORANGE = "#FFAC1C"
|
HEX_ORANGE = "#FFAC1C"
|
||||||
|
|
||||||
# TODO: normalize icon names
|
# FIXME: normalize icon names
|
||||||
CALCULATOR = "accessories-calculator-symbolic"
|
|
||||||
CARET_DOWN = "go-down-symbolic"
|
CARET_DOWN = "go-down-symbolic"
|
||||||
CARET_UP = "go-up-symbolic"
|
CARET_UP = "go-up-symbolic"
|
||||||
CLIPBOARD = "edit-copy-symbolic"
|
CLIPBOARD = "edit-copy-symbolic"
|
||||||
@@ -55,8 +54,6 @@ VIEW_CONCEAL = "view-conceal-symbolic"
|
|||||||
VIEW_REVEAL = "view-reveal-symbolic"
|
VIEW_REVEAL = "view-reveal-symbolic"
|
||||||
WARNING = "dialog-warning-symbolic"
|
WARNING = "dialog-warning-symbolic"
|
||||||
WEB_BROWSER = "web-browser-symbolic"
|
WEB_BROWSER = "web-browser-symbolic"
|
||||||
DAY_ICON = "weather-clear-symbolic"
|
|
||||||
NIGHT_ICON = "weather-clear-night-symbolic"
|
|
||||||
|
|
||||||
SEPARATOR = "SEPARATOR"
|
SEPARATOR = "SEPARATOR"
|
||||||
|
|
||||||
|
|||||||
+6
-10
@@ -7,12 +7,11 @@ import dzgui.util._json as JSON # noqa
|
|||||||
|
|
||||||
|
|
||||||
from dzgui.const.enum import (
|
from dzgui.const.enum import (
|
||||||
|
FilterMode,
|
||||||
|
Preferences,
|
||||||
|
NotebookPage,
|
||||||
ButtonType,
|
ButtonType,
|
||||||
ContextMenu,
|
ContextMenu,
|
||||||
FilterMode,
|
|
||||||
NotebookPage,
|
|
||||||
Preferences,
|
|
||||||
ServerTab,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
from dzgui.config.userprefs import UserPrefs
|
from dzgui.config.userprefs import UserPrefs
|
||||||
@@ -38,6 +37,7 @@ logger = logging.getLogger(APP_NAME)
|
|||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from dzgui.api.servers import Record
|
from dzgui.api.servers import Record
|
||||||
|
from dzgui.const.enum import ServerTab
|
||||||
from dzgui.managers.connection import Prerequisites
|
from dzgui.managers.connection import Prerequisites
|
||||||
from dzgui.managers.filter import FilterManager
|
from dzgui.managers.filter import FilterManager
|
||||||
from dzgui.util.dist import Haversine
|
from dzgui.util.dist import Haversine
|
||||||
@@ -498,9 +498,6 @@ class Controller(GObject.GObject):
|
|||||||
|
|
||||||
def set_active_keyword(self, keyword: str) -> None:
|
def set_active_keyword(self, keyword: str) -> None:
|
||||||
self.get_filter_man().set_active_keyword(keyword)
|
self.get_filter_man().set_active_keyword(keyword)
|
||||||
ServerModelManager(self, self.get_active_treeview()).refilter(
|
|
||||||
FilterMode.TOGGLE_ON
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_modtreeview(self) -> "ModTreeView":
|
def get_modtreeview(self) -> "ModTreeView":
|
||||||
return self.mediator.modtreeview
|
return self.mediator.modtreeview
|
||||||
@@ -526,7 +523,6 @@ class Controller(GObject.GObject):
|
|||||||
|
|
||||||
def set_start_tab(self) -> None:
|
def set_start_tab(self) -> None:
|
||||||
ind = self.config_man.get_start_tab()
|
ind = self.config_man.get_start_tab()
|
||||||
logger.info(f"Setting start tab to index {ind}")
|
|
||||||
self.get_servers().notebook.set_current_page(ind)
|
self.get_servers().notebook.set_current_page(ind)
|
||||||
|
|
||||||
def get_debug_args(self) -> str:
|
def get_debug_args(self) -> str:
|
||||||
@@ -541,8 +537,8 @@ class Controller(GObject.GObject):
|
|||||||
def update_status(self) -> None:
|
def update_status(self) -> None:
|
||||||
self.mediator.preconnect.mark_finished()
|
self.mediator.preconnect.mark_finished()
|
||||||
|
|
||||||
def get_steam_client_name(self, cmd: str) -> str:
|
def get_steam_client_name(self) -> str:
|
||||||
return self.mediator.options.get_client_name(cmd)
|
return self.mediator.options.get_client_name()
|
||||||
|
|
||||||
def present_window(self) -> None:
|
def present_window(self) -> None:
|
||||||
self.mediator.window.present_with_time(Gdk.CURRENT_TIME)
|
self.mediator.window.present_with_time(Gdk.CURRENT_TIME)
|
||||||
|
|||||||
@@ -52,7 +52,6 @@
|
|||||||
- Filter button tooltips
|
- Filter button tooltips
|
||||||
- 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
|
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Conform to PEP 440 versioning for beta versions
|
- Conform to PEP 440 versioning for beta versions
|
||||||
@@ -91,7 +90,6 @@
|
|||||||
- Rare segfaults when changing maps (threading)
|
- Rare segfaults when changing maps (threading)
|
||||||
- Moved dialogs out of threads
|
- Moved dialogs out of threads
|
||||||
- History table not being in lock-step with state file
|
- History table not being in lock-step with state file
|
||||||
- Current keyword filter not being applied when refreshing table
|
|
||||||
|
|
||||||
### Unreleased
|
### Unreleased
|
||||||
- Load offline mods
|
- Load offline mods
|
||||||
|
|||||||
@@ -24,10 +24,6 @@
|
|||||||
.left-label {
|
.left-label {
|
||||||
font-size: 1.3rem;
|
font-size: 1.3rem;
|
||||||
}
|
}
|
||||||
.server-subheading {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
.details-heading {
|
.details-heading {
|
||||||
font-size: 1.2rem
|
font-size: 1.2rem
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-9
@@ -4,24 +4,17 @@ from pathlib import Path
|
|||||||
|
|
||||||
from dzgui.api.steam import get_app_path
|
from dzgui.api.steam import get_app_path
|
||||||
from dzgui.config.query import lookup
|
from dzgui.config.query import lookup
|
||||||
from dzgui.const.constants import APPID_DAYZ, APP_NAME, APPNAME_DAYZ
|
from dzgui.const.constants import APPID_DAYZ, APP_NAME
|
||||||
from dzgui.const.enum import Preferences
|
from dzgui.const.enum import Preferences
|
||||||
|
|
||||||
from dzgui.strings.preconnect import resync
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(APP_NAME)
|
logger = logging.getLogger(APP_NAME)
|
||||||
|
|
||||||
|
|
||||||
class VDFSyncError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def is_dayz_installed(config: Path) -> None:
|
def is_dayz_installed(config: Path) -> None:
|
||||||
try:
|
try:
|
||||||
path = lookup(config, Preferences.DEFAULT)
|
path = lookup(config, Preferences.DEFAULT)
|
||||||
get_app_path(Path(path), APPID_DAYZ)
|
get_app_path(Path(path), APPID_DAYZ)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.critical(e)
|
logger.critical(e)
|
||||||
msg = resync.format(APPNAME_DAYZ)
|
raise e
|
||||||
raise VDFSyncError(msg)
|
|
||||||
+4
-9
@@ -16,15 +16,12 @@ logger = logging.getLogger(APP_NAME)
|
|||||||
|
|
||||||
|
|
||||||
def is_steam_running(cmd: str) -> bool:
|
def is_steam_running(cmd: str) -> bool:
|
||||||
is_flatpak_running = is_flatpak_steam_running()
|
if cmd == STEAM_CMD:
|
||||||
if cmd in (FLATPAK_RUN_CMD, FLATPAK_SANDBOX):
|
|
||||||
return is_flatpak_running
|
|
||||||
elif cmd == STEAM_CMD:
|
|
||||||
if is_flatpak_running:
|
|
||||||
return False
|
|
||||||
if has_cmd(STEAM_CMD) is False:
|
if has_cmd(STEAM_CMD) is False:
|
||||||
return False
|
return False
|
||||||
return is_running(STEAM_CMD)
|
return is_running(STEAM_CMD)
|
||||||
|
elif cmd == FLATPAK_RUN_CMD or FLATPAK_SANDBOX:
|
||||||
|
return is_flatpak_steam_running()
|
||||||
else:
|
else:
|
||||||
raise TypeError("Not a valid Steam client selection")
|
raise TypeError("Not a valid Steam client selection")
|
||||||
|
|
||||||
@@ -34,9 +31,7 @@ def is_steam_running(cmd: str) -> bool:
|
|||||||
def is_flatpak_steam_running() -> bool:
|
def is_flatpak_steam_running() -> bool:
|
||||||
if has_cmd(FLATPAK_CMD) is False:
|
if has_cmd(FLATPAK_CMD) is False:
|
||||||
return False
|
return False
|
||||||
proc = subprocess.check_output(
|
proc = subprocess.check_output([FLATPAK_CMD, "ps"], text=True)
|
||||||
[FLATPAK_CMD, "ps", "--columns=application"], text=True
|
|
||||||
)
|
|
||||||
lines = proc.splitlines()
|
lines = proc.splitlines()
|
||||||
if FLATPAK_APPID in lines:
|
if FLATPAK_APPID in lines:
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ from dzgui.strings.dialogs import (
|
|||||||
)
|
)
|
||||||
from dzgui.strings import kb
|
from dzgui.strings import kb
|
||||||
from dzgui.strings.server_mods import checkmark, resync
|
from dzgui.strings.server_mods import checkmark, resync
|
||||||
from dzgui.util.format import format_exception, format_mib
|
from dzgui.util.format import format_mib
|
||||||
from dzgui.util.strings import dialog, server_timeout
|
from dzgui.util.strings import dialog, server_timeout
|
||||||
from dzgui.util.symlink import rebuild_symlinks
|
from dzgui.util.symlink import rebuild_symlinks
|
||||||
from dzgui.views.dialogs.generic import ExceptionDialog
|
from dzgui.views.dialogs.generic import ExceptionDialog
|
||||||
@@ -184,8 +184,8 @@ class ConnectionManager:
|
|||||||
else:
|
else:
|
||||||
allows_downloads = (True, "")
|
allows_downloads = (True, "")
|
||||||
|
|
||||||
|
client_name = self.controller.get_steam_client_name()
|
||||||
client = self.controller.query_config(Preferences.CLIENT)
|
client = self.controller.query_config(Preferences.CLIENT)
|
||||||
client_name = self.controller.get_steam_client_name(client)
|
|
||||||
running = is_steam_running(client)
|
running = is_steam_running(client)
|
||||||
steam_proc = SteamProcess(client_name, running)
|
steam_proc = SteamProcess(client_name, running)
|
||||||
self.client = client
|
self.client = client
|
||||||
@@ -235,7 +235,7 @@ class ConnectionManager:
|
|||||||
@call_on_thread(dialog.querying)
|
@call_on_thread(dialog.querying)
|
||||||
def query_details(self, record: Servers.Record) -> None:
|
def query_details(self, record: Servers.Record) -> None:
|
||||||
details = Servers.get_details(record)
|
details = Servers.get_details(record)
|
||||||
if details is None:
|
if details.success is False:
|
||||||
self.thread_man.set_cleanup_func(
|
self.thread_man.set_cleanup_func(
|
||||||
StoredFunc(self._server_timeout), destroy_first=True
|
StoredFunc(self._server_timeout), destroy_first=True
|
||||||
)
|
)
|
||||||
@@ -300,11 +300,6 @@ class ConnectionManager:
|
|||||||
dialog = ServerDetailsDialog(self.controller, details)
|
dialog = ServerDetailsDialog(self.controller, details)
|
||||||
dialog.run()
|
dialog.run()
|
||||||
|
|
||||||
def _workshop_timeout(self, msg: str) -> None:
|
|
||||||
dialog = ExceptionDialog(self.controller, msg)
|
|
||||||
dialog.show_all()
|
|
||||||
dialog.run()
|
|
||||||
|
|
||||||
def _server_timeout(self) -> None:
|
def _server_timeout(self) -> None:
|
||||||
dialog = ExceptionDialog(self.controller, server_timeout)
|
dialog = ExceptionDialog(self.controller, server_timeout)
|
||||||
dialog.set_details_buffer(kb.DZG_006)
|
dialog.set_details_buffer(kb.DZG_006)
|
||||||
@@ -356,14 +351,8 @@ class ConnectionManager:
|
|||||||
for title, mod, stamp, size in self.missing_mods:
|
for title, mod, stamp, size in self.missing_mods:
|
||||||
if self.controller.is_cancel_pending():
|
if self.controller.is_cancel_pending():
|
||||||
return
|
return
|
||||||
try:
|
subscribe(key, int(mod))
|
||||||
subscribe(key, int(mod))
|
time.sleep(API_RATE_LIMIT)
|
||||||
time.sleep(API_RATE_LIMIT)
|
|
||||||
except Exception as e:
|
|
||||||
msg = format_exception(e)
|
|
||||||
func = StoredFunc(self._workshop_timeout, msg)
|
|
||||||
self.thread_man.set_cleanup_func(func, destroy_first=True)
|
|
||||||
return
|
|
||||||
|
|
||||||
self.thread_man.update_dialog(waiting_for_directories)
|
self.thread_man.update_dialog(waiting_for_directories)
|
||||||
for title, mod, stamp, size in self.missing_mods:
|
for title, mod, stamp, size in self.missing_mods:
|
||||||
|
|||||||
@@ -32,10 +32,10 @@ class FilterManager:
|
|||||||
# TODO: strings
|
# TODO: strings
|
||||||
self.tooltips = {
|
self.tooltips = {
|
||||||
strings.filter_1pp: "First-person perspective",
|
strings.filter_1pp: "First-person perspective",
|
||||||
strings.filter_day: "In-game time between 0600 and 1759",
|
strings.filter_day: "In-game time between 0700 and 1659",
|
||||||
strings.filter_empty: "Servers with no players",
|
strings.filter_empty: "Servers with no players",
|
||||||
strings.filter_3pp: "Third-person perspective",
|
strings.filter_3pp: "Third-person perspective",
|
||||||
strings.filter_night: "In-game time between 1800 and 0559",
|
strings.filter_night: "In-game time between 1700 and 0659",
|
||||||
strings.filter_full: "Servers with no open slots",
|
strings.filter_full: "Servers with no open slots",
|
||||||
strings.filter_official: "Bohemia official servers",
|
strings.filter_official: "Bohemia official servers",
|
||||||
strings.filter_nonascii: "Server names using non-standard, complex glyphs",
|
strings.filter_nonascii: "Server names using non-standard, complex glyphs",
|
||||||
|
|||||||
@@ -122,11 +122,7 @@ class ModManager:
|
|||||||
def unsub_atomic_mod(self, mod: str) -> None:
|
def unsub_atomic_mod(self, mod: str) -> None:
|
||||||
config_man = self.controller.get_config_man()
|
config_man = self.controller.get_config_man()
|
||||||
key = config_man.lookup(Preferences.STEAM)
|
key = config_man.lookup(Preferences.STEAM)
|
||||||
try:
|
unsubscribe(key, int(mod))
|
||||||
unsubscribe(key, int(mod))
|
|
||||||
except Exception as e:
|
|
||||||
logger.critical(e)
|
|
||||||
return
|
|
||||||
|
|
||||||
steam_path = Path(self.path)
|
steam_path = Path(self.path)
|
||||||
app_path = PeFile.get_nested_app_path(steam_path, APPID_DAYZ)
|
app_path = PeFile.get_nested_app_path(steam_path, APPID_DAYZ)
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ if TYPE_CHECKING:
|
|||||||
from dzgui.api.servers import Record
|
from dzgui.api.servers import Record
|
||||||
from dzgui.model.servers import NewPlayerCount
|
from dzgui.model.servers import NewPlayerCount
|
||||||
|
|
||||||
DAY_REG = r"([0][6-9]|[1][0-7])"
|
DAY_REG = r"([0][7-9]|[1][0-6])"
|
||||||
NIGHT_REG = r"([0][0-5]|[1][8-9]|[2][0-3])"
|
NIGHT_REG = r"([0][0-6]|[1][7-9]|[2][0-3])"
|
||||||
|
|
||||||
|
|
||||||
class ProxyModelManager:
|
class ProxyModelManager:
|
||||||
@@ -259,6 +259,10 @@ class ProxyModelManager:
|
|||||||
def set_cache(self, filters: tuple[str], filtered_rows: list[tuple]) -> None:
|
def set_cache(self, filters: tuple[str], filtered_rows: list[tuple]) -> None:
|
||||||
self.filter_cache[filters] = filtered_rows
|
self.filter_cache[filters] = filtered_rows
|
||||||
|
|
||||||
|
# @deprecated("Currently unused")
|
||||||
|
# def convert_model_to_list(self, model: "FastInsertListStore") -> list:
|
||||||
|
# return [[el for el in row] for row in model]
|
||||||
|
|
||||||
def set_filtered(self, rows: list | None) -> None:
|
def set_filtered(self, rows: list | None) -> None:
|
||||||
if rows is None:
|
if rows is None:
|
||||||
self.filtered = []
|
self.filtered = []
|
||||||
@@ -294,6 +298,4 @@ class ProxyModelManager:
|
|||||||
def push(self, data: list[Any]) -> None:
|
def push(self, data: list[Any]) -> None:
|
||||||
self.wipe_cache()
|
self.wipe_cache()
|
||||||
self.set_control(data)
|
self.set_control(data)
|
||||||
cur_keyword = self.filter_man.get_active_keyword()
|
self.filter(FilterMode.INITIAL)
|
||||||
mode = FilterMode.TOGGLE_ON if len(cur_keyword) > 0 else FilterMode.INITIAL
|
|
||||||
self.filter(mode)
|
|
||||||
@@ -21,7 +21,7 @@ from dzgui.views.dialogs.generic import ExceptionDialog
|
|||||||
import gi
|
import gi
|
||||||
|
|
||||||
gi.require_version("Gtk", "3.0")
|
gi.require_version("Gtk", "3.0")
|
||||||
from gi.repository import Gtk, GLib # noqa E402
|
from gi.repository import Gtk # noqa E402
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from dzgui.api.servers import A2SInfo, Record
|
from dzgui.api.servers import A2SInfo, Record
|
||||||
@@ -390,8 +390,6 @@ class ServerModelManager:
|
|||||||
def _cleanup_on_success(self) -> None:
|
def _cleanup_on_success(self) -> None:
|
||||||
proxy = self.proxy_man.get_proxy_model()
|
proxy = self.proxy_man.get_proxy_model()
|
||||||
self.tv.set_model(proxy)
|
self.tv.set_model(proxy)
|
||||||
if self.tv.enum == ServerTab.BROWSER:
|
|
||||||
GLib.idle_add(self.tv.wipe_cache)
|
|
||||||
if self.preserve_on_fail is True:
|
if self.preserve_on_fail is True:
|
||||||
self.proxy_man.wipe_cache()
|
self.proxy_man.wipe_cache()
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
heading_local_time = "Local time"
|
|
||||||
heading_server_time = "In-game time"
|
|
||||||
heading_target_time = "Target time"
|
|
||||||
heading_remainder_time = "Remainder until target"
|
|
||||||
heading_adjusted_time = "Adjusted local time"
|
|
||||||
calculate = "Calculate"
|
|
||||||
disclaimer = "Times shown are approximations.\nServer time may differ based on time of year in game."
|
|
||||||
@@ -1,22 +1,20 @@
|
|||||||
from dzgui.const.constants import UDP_PORT
|
add_button="Add"
|
||||||
|
add_con_heading="Add/connect"
|
||||||
add_button = "Add"
|
|
||||||
add_con_heading = "Add/connect"
|
|
||||||
add_popover = "This address is already in your Saved Servers"
|
add_popover = "This address is already in your Saved Servers"
|
||||||
add_tooltip = "Add to Saved Servers"
|
add_tooltip="Add to Saved Servers"
|
||||||
connect_button = "Connect"
|
connect_button="Connect"
|
||||||
connect_tooltip = "Connect to this server"
|
connect_tooltip="Connect to this server"
|
||||||
connect_entry_placeholder = f"Enter IP:Query port (e.g., 192.168.1.1:{UDP_PORT})"
|
connect_entry_placeholder="Enter IP:Query port (e.g., 192.168.1.1:27016)"
|
||||||
|
|
||||||
fav_heading = "Favorite server"
|
fav_heading="Favorite server"
|
||||||
favs_empty = "None set. Right click a server and select 'Set favorite' to set."
|
favs_empty="None set. Right click a server and select 'Set favorite' to set."
|
||||||
|
|
||||||
lan_heading = "LAN query port"
|
lan_heading="LAN query port"
|
||||||
lan_default_button = f"Default port ({UDP_PORT})"
|
lan_default_button="Default port (27016)"
|
||||||
lan_custom_button = "Custom port"
|
lan_custom_button="Custom port"
|
||||||
lan_scan_button = "Scan"
|
lan_scan_button="Scan"
|
||||||
lan_abort_tooltip = "Unless you have multiple DayZ servers on your LAN,\nleave this checked to get results faster"
|
lan_abort_tooltip = "Unless you have multiple DayZ servers on your LAN,\nleave this checked to get results faster"
|
||||||
lan_checkbox = "Stop scanning on first hit"
|
lan_checkbox = "Stop scanning on first hit"
|
||||||
lan_entry_placeholder = "Enter the query port (1-65535)"
|
lan_entry_placeholder="Enter the query port (1-65535)"
|
||||||
lan_entry_tooltip = "Specify the port to search for DayZ servers on the local network"
|
lan_entry_tooltip="Specify the port to search for DayZ servers on the local network"
|
||||||
lan_scan_tooltip = "Scan for servers"
|
lan_scan_tooltip="Scan for servers"
|
||||||
@@ -23,8 +23,3 @@ custom_mod_dialog = "Set location to a custom mods folder"
|
|||||||
|
|
||||||
debug_heading = "Debug"
|
debug_heading = "Debug"
|
||||||
debug_secondary = "Launch arguments"
|
debug_secondary = "Launch arguments"
|
||||||
|
|
||||||
toggle_calc = "Toggle calculator"
|
|
||||||
|
|
||||||
server_details = "Server details"
|
|
||||||
server_message = "Server message"
|
|
||||||
@@ -4,4 +4,3 @@ api_validation_error = (
|
|||||||
|
|
||||||
api_popover = "API key validation failed."
|
api_popover = "API key validation failed."
|
||||||
dayz_not_found = "Could not find a valid DayZ installation."
|
dayz_not_found = "Could not find a valid DayZ installation."
|
||||||
workshop_endpoint_error = "Steam Workshop service unavailable, try again later."
|
|
||||||
@@ -11,6 +11,8 @@ mods = "Mods"
|
|||||||
total_mods = "Total mods: "
|
total_mods = "Total mods: "
|
||||||
up_to_date = "All mods are up to date."
|
up_to_date = "All mods are up to date."
|
||||||
all_updated = "All mods updated."
|
all_updated = "All mods updated."
|
||||||
|
"If you recently installed {build} or moved it to a different drive, "
|
||||||
|
"restart Steam to allow these changes to synchronize, then try again."
|
||||||
required_space = " Need to download {0} MiB of mod updates."
|
required_space = " Need to download {0} MiB of mod updates."
|
||||||
|
|
||||||
placeholder_no_mods = "This server has no mods."
|
placeholder_no_mods = "This server has no mods."
|
||||||
@@ -34,7 +36,6 @@ steam_not_running = "'{0}' is set as the default Steam client, but it is either
|
|||||||
use_desktop_mode = "Use Desktop Mode to download mods on Steam Deck."
|
use_desktop_mode = "Use Desktop Mode to download mods on Steam Deck."
|
||||||
version_mismatch = "Local client version '{0}' does not match remote version '{1}'."
|
version_mismatch = "Local client version '{0}' does not match remote version '{1}'."
|
||||||
version_missing = "Remote server is running the build '{0}', but it is not installed."
|
version_missing = "Remote server is running the build '{0}', but it is not installed."
|
||||||
no_errors = "No errors."
|
|
||||||
|
|
||||||
|
|
||||||
# Warnings
|
# Warnings
|
||||||
|
|||||||
@@ -30,8 +30,7 @@ path_remove_prefix = "The following path will be removed:"
|
|||||||
not_installed = "It looks like DZGUI was not previously set up; nothing to uninstall."
|
not_installed = "It looks like DZGUI was not previously set up; nothing to uninstall."
|
||||||
standalone_uninstall = (
|
standalone_uninstall = (
|
||||||
"It looks like you are running the standalone version of DZGUI.\n"
|
"It looks like you are running the standalone version of DZGUI.\n"
|
||||||
"To finalize the uninstall process, invoke './dzgui self remove',\n"
|
"To finalize the uninstall process, you can remove the 'dzgui' file."
|
||||||
"then remove the 'dzgui' file."
|
|
||||||
)
|
)
|
||||||
system_uninstall = (
|
system_uninstall = (
|
||||||
"It looks like DZGUI was installed via your system package manager or directly from source code.\n"
|
"It looks like DZGUI was installed via your system package manager or directly from source code.\n"
|
||||||
|
|||||||
@@ -84,5 +84,6 @@ class CalcDist:
|
|||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
# TODO: handle failed remote dist
|
||||||
haversine = Haversine(local.lat, local.lon, remote.lat, remote.lon)
|
haversine = Haversine(local.lat, local.lon, remote.lat, remote.lon)
|
||||||
return haversine
|
return haversine
|
||||||
+9
-1
@@ -2,13 +2,14 @@ import a2s
|
|||||||
import requests
|
import requests
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
|
from warnings import deprecated
|
||||||
|
|
||||||
from dataclasses import dataclass
|
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.const.constants import REQUEST_TIMEOUT
|
from dzgui.const.constants import REQUEST_TIMEOUT
|
||||||
from dzgui.const.endpoints import IP_ECHO
|
from dzgui.const.endpoints import COORDS_API, IP_ECHO
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -126,3 +127,10 @@ def is_valid_port(port: str) -> bool:
|
|||||||
if not port.isdigit() or int(port) == 0 or int(port[0]) == 0 or int(port) > 65535:
|
if not port.isdigit() or int(port) == 0 or int(port[0]) == 0 or int(port) > 65535:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
@deprecated("use ips.csv")
|
||||||
|
def get_local_coords(ip: str) -> str:
|
||||||
|
url = COORDS_API + "/" + ip
|
||||||
|
# local res=$(curl -Ls "$url" | jq -r '"\(.lat)\n\(.lon)"')
|
||||||
|
return url
|
||||||
@@ -11,6 +11,7 @@ delimiter = "␞"
|
|||||||
# GenericDialog
|
# GenericDialog
|
||||||
main_menu = "Return to main menu"
|
main_menu = "Return to main menu"
|
||||||
exit_app = "Exit"
|
exit_app = "Exit"
|
||||||
|
server_details = "Server details"
|
||||||
modlist = "Modlist"
|
modlist = "Modlist"
|
||||||
input_required = "User input required"
|
input_required = "User input required"
|
||||||
confirm = "Confirmation"
|
confirm = "Confirmation"
|
||||||
@@ -58,6 +59,7 @@ browser_cols = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
# DetailsDialog
|
# DetailsDialog
|
||||||
|
server_message = "Server message"
|
||||||
workshop = "Enter/double click a row to open in Steam Workshop."
|
workshop = "Enter/double click a row to open in Steam Workshop."
|
||||||
|
|
||||||
# LanDialog
|
# LanDialog
|
||||||
@@ -166,12 +168,11 @@ none_provided = "None provided"
|
|||||||
disabled = "Disabled"
|
disabled = "Disabled"
|
||||||
enabled = "Enabled"
|
enabled = "Enabled"
|
||||||
null = "-"
|
null = "-"
|
||||||
|
none = "None"
|
||||||
unspecified = "not specified"
|
unspecified = "not specified"
|
||||||
|
|
||||||
# DLC
|
# DLC
|
||||||
dlc_frostline = "Frostline"
|
dlc_frostline = "Frostline"
|
||||||
dlc_badlands = "Badlands"
|
|
||||||
dlc_none = "None"
|
|
||||||
|
|
||||||
# platform
|
# platform
|
||||||
windows = "Windows"
|
windows = "Windows"
|
||||||
|
|||||||
+5
-10
@@ -2,12 +2,7 @@ import logging
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from dzgui.api.mods import get_local_mod_path, get_local_mod_ids, _hash
|
from dzgui.api.mods import get_local_mod_path, get_local_mod_ids, _hash
|
||||||
from dzgui.const.constants import (
|
from dzgui.const.constants import APPID_DAYZ, APPID_DAYZ_EXP, APP_NAME, DAYZ_COMMUNITY_ROOT
|
||||||
APPID_DAYZ,
|
|
||||||
APPID_DAYZ_EXP,
|
|
||||||
APP_NAME,
|
|
||||||
DAYZ_COMMUNITY_ROOT,
|
|
||||||
)
|
|
||||||
from dzgui.const.enum import Preferences
|
from dzgui.const.enum import Preferences
|
||||||
from dzgui.config.query import lookup
|
from dzgui.config.query import lookup
|
||||||
|
|
||||||
@@ -25,7 +20,6 @@ def expunge_link(file: Path) -> None:
|
|||||||
if str(file.stem)[:2] == "@C":
|
if str(file.stem)[:2] == "@C":
|
||||||
file.unlink()
|
file.unlink()
|
||||||
|
|
||||||
|
|
||||||
def rebuild_symlinks(config: Path) -> None:
|
def rebuild_symlinks(config: Path) -> None:
|
||||||
# TODO: pass direct path as argument
|
# TODO: pass direct path as argument
|
||||||
path = lookup(config, Preferences.DEFAULT)
|
path = lookup(config, Preferences.DEFAULT)
|
||||||
@@ -69,9 +63,10 @@ def symlink_mission(steam_path: Path, target: str) -> str:
|
|||||||
stem = path.name
|
stem = path.name
|
||||||
parent = path.parent
|
parent = path.parent
|
||||||
source = dayz_path.joinpath(DAYZ_COMMUNITY_ROOT)
|
source = dayz_path.joinpath(DAYZ_COMMUNITY_ROOT)
|
||||||
if source.exists():
|
try:
|
||||||
source.unlink()
|
source.symlink_to(parent)
|
||||||
source.symlink_to(parent)
|
except Exception:
|
||||||
|
return ""
|
||||||
return f"{DAYZ_COMMUNITY_ROOT}/{stem}"
|
return f"{DAYZ_COMMUNITY_ROOT}/{stem}"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+8
-1
@@ -1,5 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
import signal
|
import signal
|
||||||
|
import warnings
|
||||||
|
|
||||||
from typing import TYPE_CHECKING, Literal, Self
|
from typing import TYPE_CHECKING, Literal, Self
|
||||||
|
|
||||||
@@ -46,6 +47,10 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
logger = logging.getLogger(APP_NAME)
|
logger = logging.getLogger(APP_NAME)
|
||||||
|
|
||||||
|
# TODO: drop
|
||||||
|
# https://bugzilla.gnome.org/show_bug.cgi?id=708676
|
||||||
|
warnings.filterwarnings("ignore", ".*g_value_get_int", Warning)
|
||||||
|
|
||||||
|
|
||||||
class OuterWindow(Gtk.ApplicationWindow):
|
class OuterWindow(Gtk.ApplicationWindow):
|
||||||
def __init__(self, app: Gtk.Application) -> None:
|
def __init__(self, app: Gtk.Application) -> None:
|
||||||
@@ -339,6 +344,8 @@ try:
|
|||||||
theme = Gtk.IconTheme.get_default()
|
theme = Gtk.IconTheme.get_default()
|
||||||
icons = theme.list_icons(None)
|
icons = theme.list_icons(None)
|
||||||
if STEAM_ICON not in icons:
|
if STEAM_ICON not in icons:
|
||||||
logger.warning(strings.steam_icon_missing)
|
logger.warn(strings.steam_icon_missing)
|
||||||
|
warnings.warn(strings.steam_icon_missing, stacklevel=2)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(e)
|
logger.warning(e)
|
||||||
|
warnings.warn(strings.gtk_theme_missing, stacklevel=2)
|
||||||
@@ -24,7 +24,6 @@ class VBox(GenericBox):
|
|||||||
def __init__(self, spacing: int = 0) -> None:
|
def __init__(self, spacing: int = 0) -> None:
|
||||||
super().__init__(orientation=Gtk.Orientation.VERTICAL, spacing=spacing)
|
super().__init__(orientation=Gtk.Orientation.VERTICAL, spacing=spacing)
|
||||||
|
|
||||||
|
|
||||||
class ShortHBox(HBox):
|
class ShortHBox(HBox):
|
||||||
def __init__(self, widget: Gtk.Widget) -> None:
|
def __init__(self, widget: Gtk.Widget) -> None:
|
||||||
super().__init__(spacing=5)
|
super().__init__(spacing=5)
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ from dzgui.const.constants import (
|
|||||||
NO_PADDING,
|
NO_PADDING,
|
||||||
SEARCH_ICON,
|
SEARCH_ICON,
|
||||||
)
|
)
|
||||||
|
from dzgui.const.enum import FilterMode
|
||||||
|
from dzgui.model.servers import ServerModelManager
|
||||||
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
|
||||||
@@ -146,6 +148,10 @@ class KeywordEntry(Gtk.Entry):
|
|||||||
self.controller.set_active_keyword(keyword)
|
self.controller.set_active_keyword(keyword)
|
||||||
logger.info(f"User filtered by keyword '{keyword}'")
|
logger.info(f"User filtered by keyword '{keyword}'")
|
||||||
|
|
||||||
|
ServerModelManager(
|
||||||
|
self.controller, self.controller.get_active_treeview()
|
||||||
|
).refilter(FilterMode.TOGGLE_ON)
|
||||||
|
|
||||||
|
|
||||||
class FilterPanel(Gtk.Box):
|
class FilterPanel(Gtk.Box):
|
||||||
def __init__(self, controller: "Controller") -> None:
|
def __init__(self, controller: "Controller") -> None:
|
||||||
|
|||||||
@@ -9,6 +9,3 @@ class Icon(Gtk.Image):
|
|||||||
super().__init__(
|
super().__init__(
|
||||||
icon_name=name, icon_size=Gtk.IconSize.BUTTON, margin_start=l_margin
|
icon_name=name, icon_size=Gtk.IconSize.BUTTON, margin_start=l_margin
|
||||||
)
|
)
|
||||||
|
|
||||||
def set_icon_name(self, name: str) -> None:
|
|
||||||
super().set_from_icon_name(name, Gtk.IconSize.BUTTON)
|
|
||||||
@@ -15,7 +15,7 @@ class ClientCombo(Gtk.ComboBox):
|
|||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
self.client_store = Gtk.ListStore(str, str)
|
self.client_store = Gtk.ListStore(str, str)
|
||||||
self.clients = (
|
clients = (
|
||||||
(
|
(
|
||||||
options.steam_combo,
|
options.steam_combo,
|
||||||
STEAM_CMD,
|
STEAM_CMD,
|
||||||
@@ -29,7 +29,7 @@ class ClientCombo(Gtk.ComboBox):
|
|||||||
FLATPAK_SANDBOX,
|
FLATPAK_SANDBOX,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
for client in self.clients:
|
for client in clients:
|
||||||
self.client_store.append(client)
|
self.client_store.append(client)
|
||||||
self.set_model(self.client_store)
|
self.set_model(self.client_store)
|
||||||
renderer_text = Gtk.CellRendererText()
|
renderer_text = Gtk.CellRendererText()
|
||||||
@@ -37,13 +37,6 @@ class ClientCombo(Gtk.ComboBox):
|
|||||||
self.add_attribute(renderer_text, "text", 0)
|
self.add_attribute(renderer_text, "text", 0)
|
||||||
self.set_active(0)
|
self.set_active(0)
|
||||||
|
|
||||||
def get_client_human_readable(self, cmd: str) -> str:
|
|
||||||
for client in self.clients:
|
|
||||||
human, command = client
|
|
||||||
if command == cmd:
|
|
||||||
return human
|
|
||||||
raise TypeError("Not a valid Steam client selection")
|
|
||||||
|
|
||||||
class ErrorPopover(Gtk.Popover):
|
class ErrorPopover(Gtk.Popover):
|
||||||
def __init__(self, relative_to: Gtk.Widget, position: Gtk.PositionType) -> None:
|
def __init__(self, relative_to: Gtk.Widget, position: Gtk.PositionType) -> None:
|
||||||
super().__init__(relative_to=relative_to, position=position)
|
super().__init__(relative_to=relative_to, position=position)
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ class BootDialog(ColorAwareApp, Gtk.Dialog): # type: ignore
|
|||||||
margin_top=30,
|
margin_top=30,
|
||||||
margin_bottom=30,
|
margin_bottom=30,
|
||||||
)
|
)
|
||||||
self.error_label = Gtk.Label(margin_start=15, margin_end=15)
|
self.error_label = Gtk.Label()
|
||||||
|
|
||||||
# TODO: abstract class
|
# TODO: abstract class
|
||||||
self.copy_button = ClipboardButton(None, lambda: self.error_label.get_text())
|
self.copy_button = ClipboardButton(None, lambda: self.error_label.get_text())
|
||||||
|
|||||||
@@ -1,457 +0,0 @@
|
|||||||
import gi
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from datetime import datetime, time, timedelta
|
|
||||||
from typing import Literal
|
|
||||||
|
|
||||||
from dzgui.const.constants import DAY_ICON, NIGHT_ICON
|
|
||||||
from dzgui.strings import calc
|
|
||||||
from dzgui.views.components.icon import Icon
|
|
||||||
from dzgui.views.components.labels import BoldLabel
|
|
||||||
from dzgui.views.components.box import VBox, HBox
|
|
||||||
|
|
||||||
gi.require_version("Gtk", "3.0")
|
|
||||||
from gi.repository import Gtk, GLib, GObject # type: ignore # noqa E402
|
|
||||||
|
|
||||||
DAY_START_TIME = time(hour=6, minute=0, second=0)
|
|
||||||
DAY_END_TIME = time(hour=18, minute=0, second=0)
|
|
||||||
NIGHT_START_TIME = time(hour=18, minute=0, second=0)
|
|
||||||
NIGHT_END_TIME = time(hour=6, minute=0, second=0)
|
|
||||||
EXTENDED_TIME_FORMAT = "%H:%M:%S"
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True, frozen=True)
|
|
||||||
class Time:
|
|
||||||
time: str
|
|
||||||
day_accel: float
|
|
||||||
night_accel: float
|
|
||||||
|
|
||||||
|
|
||||||
class Emitter(GObject.GObject):
|
|
||||||
def __init__(self) -> None:
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=(object, object))
|
|
||||||
def local_time_incremented(self, time_now: datetime, elapsed: timedelta) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=(object,))
|
|
||||||
def server_time_incremented(self, server_time: datetime) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=())
|
|
||||||
def calculate_button_clicked(self) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=(object,))
|
|
||||||
def target_time_changed(self, time: timedelta) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=(object,))
|
|
||||||
def target_time_reset(self, time: timedelta) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=(object,))
|
|
||||||
def remaining_time_changed(self, time: datetime) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=(object,))
|
|
||||||
def adjusted_local_time_changed(self, time: datetime) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class LocalClock:
|
|
||||||
def __init__(self, emitter: Emitter) -> None:
|
|
||||||
self.emitter = emitter
|
|
||||||
self.start_time = datetime.now()
|
|
||||||
# NOTE: highest theoretical accel interval is 24 * 64
|
|
||||||
GLib.timeout_add(500, self.check_time)
|
|
||||||
|
|
||||||
def get_time(self) -> str:
|
|
||||||
return datetime.now().time().strftime(EXTENDED_TIME_FORMAT)
|
|
||||||
|
|
||||||
def get_elapsed_time(self) -> timedelta:
|
|
||||||
return datetime.now() - self.start_time
|
|
||||||
|
|
||||||
def check_time(self) -> Literal[True]:
|
|
||||||
time_now = datetime.now()
|
|
||||||
elapsed = datetime.now() - self.start_time
|
|
||||||
self.emitter.emit("local_time_incremented", time_now, elapsed)
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
class ServerClock:
|
|
||||||
def __init__(
|
|
||||||
self, server_time: Time, emitter: Emitter, local_clock: LocalClock
|
|
||||||
) -> None:
|
|
||||||
|
|
||||||
self.local_clock = local_clock
|
|
||||||
self.emitter = emitter
|
|
||||||
|
|
||||||
hour, minute = self.split_time(server_time)
|
|
||||||
self.day_accel = server_time.day_accel
|
|
||||||
self.night_accel = server_time.night_accel
|
|
||||||
|
|
||||||
self.start_time = datetime.combine(datetime.today(), time(hour, minute))
|
|
||||||
self.cur_time = datetime.combine(datetime.today(), time(hour, minute))
|
|
||||||
|
|
||||||
self._last_elapsed = self.local_clock.get_elapsed_time()
|
|
||||||
self.emitter.connect("local_time_incremented", self._on_local_time_incremented)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def split_time(server_time: Time) -> tuple[int, int]:
|
|
||||||
split = server_time.time.split(":")
|
|
||||||
return int(split[0]), int(split[1])
|
|
||||||
|
|
||||||
def is_day(self) -> bool:
|
|
||||||
if DAY_START_TIME <= self.cur_time.time() <= DAY_END_TIME:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _on_local_time_incremented(
|
|
||||||
self,
|
|
||||||
emitter: Emitter,
|
|
||||||
time: datetime,
|
|
||||||
elapsed: timedelta,
|
|
||||||
) -> Literal[True]:
|
|
||||||
delta = elapsed - self._last_elapsed
|
|
||||||
self._last_elapsed = elapsed
|
|
||||||
factor = self.get_accel_factor(self.cur_time)
|
|
||||||
self.cur_time += delta * factor
|
|
||||||
self.emitter.emit("server_time_incremented", self.cur_time)
|
|
||||||
return True
|
|
||||||
|
|
||||||
def get_accel_factor(self, dt: datetime) -> float:
|
|
||||||
return (
|
|
||||||
self.day_accel
|
|
||||||
if DAY_START_TIME <= dt.time() < DAY_END_TIME
|
|
||||||
else (self.night_accel * self.day_accel)
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_values(self) -> tuple[int, int]:
|
|
||||||
return self.cur_time.hour, self.cur_time.minute
|
|
||||||
|
|
||||||
def get_datetime(self) -> datetime:
|
|
||||||
return self.cur_time
|
|
||||||
|
|
||||||
def get_time(self) -> time:
|
|
||||||
return self.cur_time.time()
|
|
||||||
|
|
||||||
def calc_delta(self, end_time: datetime) -> timedelta:
|
|
||||||
total = 0.0
|
|
||||||
current_time = self.cur_time
|
|
||||||
|
|
||||||
if current_time > end_time:
|
|
||||||
end_time += timedelta(days=1)
|
|
||||||
|
|
||||||
while current_time < end_time:
|
|
||||||
factor = self.get_accel_factor(current_time)
|
|
||||||
day_start = datetime.combine(current_time.date(), DAY_START_TIME)
|
|
||||||
day_end = datetime.combine(current_time.date(), DAY_END_TIME)
|
|
||||||
|
|
||||||
if current_time.time() < DAY_START_TIME:
|
|
||||||
edge = day_start
|
|
||||||
elif current_time.time() < DAY_END_TIME:
|
|
||||||
edge = day_end
|
|
||||||
else:
|
|
||||||
edge = datetime.combine(
|
|
||||||
current_time.date() + timedelta(days=1), DAY_START_TIME
|
|
||||||
)
|
|
||||||
|
|
||||||
chunk_end = min(end_time, edge)
|
|
||||||
total += (chunk_end - current_time).total_seconds() / factor
|
|
||||||
current_time = chunk_end
|
|
||||||
return timedelta(seconds=total)
|
|
||||||
|
|
||||||
|
|
||||||
class RemainderClock:
|
|
||||||
def __init__(self, emitter: Emitter) -> None:
|
|
||||||
self.emitter = emitter
|
|
||||||
self.emitter.connect("target_time_changed", self._on_target_time_changed)
|
|
||||||
self.emitter.connect("target_time_reset", self._on_target_time_reset)
|
|
||||||
self.emitter.connect("local_time_incremented", self._on_local_time_incremented)
|
|
||||||
self.reset_time(True)
|
|
||||||
|
|
||||||
self.previous = timedelta(0)
|
|
||||||
|
|
||||||
def reset_time(self, zero_out: bool = False) -> None:
|
|
||||||
now = datetime.now()
|
|
||||||
second, ms = (0, 0) if zero_out else (now.second, now.microsecond)
|
|
||||||
self.time = datetime.combine(
|
|
||||||
now, time(hour=0, minute=0, second=second, microsecond=ms)
|
|
||||||
)
|
|
||||||
|
|
||||||
def _on_local_time_incremented(
|
|
||||||
self, emitter: Emitter, time: datetime, elapsed: timedelta
|
|
||||||
) -> None:
|
|
||||||
if self.time == datetime.min.time():
|
|
||||||
return
|
|
||||||
|
|
||||||
delta = elapsed - self.previous
|
|
||||||
# NOTE: clamp to midnight
|
|
||||||
self.time = max(
|
|
||||||
self.time - delta, self.time.replace(hour=0, minute=0, second=0)
|
|
||||||
)
|
|
||||||
self.previous = elapsed
|
|
||||||
self.emitter.emit("remaining_time_changed", self.time)
|
|
||||||
|
|
||||||
def _on_target_time_reset(self, emitter: Emitter, time: timedelta) -> None:
|
|
||||||
self.reset_time(True)
|
|
||||||
self.emitter.emit("remaining_time_changed", self.time)
|
|
||||||
# NOTE: restore adjusted time to current time
|
|
||||||
self.emitter.emit("adjusted_local_time_changed", datetime.now())
|
|
||||||
|
|
||||||
def _on_target_time_changed(self, emitter: Emitter, time: timedelta) -> None:
|
|
||||||
self.reset_time(True)
|
|
||||||
self.time += time
|
|
||||||
self.emitter.emit("remaining_time_changed", self.time)
|
|
||||||
|
|
||||||
def get_time(self) -> str:
|
|
||||||
return self.time.strftime(EXTENDED_TIME_FORMAT)
|
|
||||||
|
|
||||||
|
|
||||||
class AdjustedClock:
|
|
||||||
def __init__(self, emitter: Emitter) -> None:
|
|
||||||
self.emitter = emitter
|
|
||||||
self.emitter.connect("target_time_changed", self._on_target_time_changed)
|
|
||||||
self.start_time = datetime.now()
|
|
||||||
|
|
||||||
def _on_target_time_changed(self, emitter: Emitter, time: timedelta) -> None:
|
|
||||||
self.start_time = datetime.now()
|
|
||||||
adjusted = self.start_time + time
|
|
||||||
self.emitter.emit("adjusted_local_time_changed", adjusted)
|
|
||||||
|
|
||||||
def get_time(self) -> str:
|
|
||||||
return self.start_time.strftime(EXTENDED_TIME_FORMAT)
|
|
||||||
|
|
||||||
|
|
||||||
class VerticalSpinBox(Gtk.SpinButton):
|
|
||||||
def __init__(self, value: int, upper: int) -> None:
|
|
||||||
super().__init__(
|
|
||||||
adjustment=Gtk.Adjustment(
|
|
||||||
value=value,
|
|
||||||
lower=0,
|
|
||||||
upper=upper,
|
|
||||||
step_increment=1,
|
|
||||||
page_increment=1,
|
|
||||||
page_size=0,
|
|
||||||
),
|
|
||||||
orientation=Gtk.Orientation.VERTICAL,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class HourSpinBox(VerticalSpinBox):
|
|
||||||
def __init__(self, hour: int) -> None:
|
|
||||||
super().__init__(value=hour, upper=23)
|
|
||||||
|
|
||||||
|
|
||||||
class MinuteSpinBox(VerticalSpinBox):
|
|
||||||
def __init__(self, minute: int) -> None:
|
|
||||||
super().__init__(value=minute, upper=59)
|
|
||||||
|
|
||||||
self.get_adjustment().set_page_increment(10)
|
|
||||||
|
|
||||||
|
|
||||||
class TimePicker(Gtk.Box):
|
|
||||||
def __init__(self, hour: int, minute: int) -> None:
|
|
||||||
super().__init__(
|
|
||||||
orientation=Gtk.Orientation.HORIZONTAL,
|
|
||||||
halign=Gtk.Align.CENTER,
|
|
||||||
valign=Gtk.Align.CENTER,
|
|
||||||
)
|
|
||||||
self.hour_spin = HourSpinBox(hour)
|
|
||||||
self.minute_spin = MinuteSpinBox(minute)
|
|
||||||
self.add(self.hour_spin)
|
|
||||||
self.add(self.minute_spin)
|
|
||||||
|
|
||||||
self.hour_spin.connect("output", self._on_output)
|
|
||||||
self.minute_spin.connect("output", self._on_output)
|
|
||||||
|
|
||||||
def _on_output(self, spin_button: VerticalSpinBox) -> Literal[True]:
|
|
||||||
val = spin_button.get_value()
|
|
||||||
spin_button.set_text(f"{int(val):02d}")
|
|
||||||
# NOTE: indicates that rendering completed
|
|
||||||
return True
|
|
||||||
|
|
||||||
def get_values(self) -> tuple[int, int]:
|
|
||||||
hour = self.get_hour()
|
|
||||||
minute = self.get_minute()
|
|
||||||
return hour, minute
|
|
||||||
|
|
||||||
def get_hour(self) -> int:
|
|
||||||
return int(self.hour_spin.get_adjustment().get_value())
|
|
||||||
|
|
||||||
def get_minute(self) -> int:
|
|
||||||
return int(self.minute_spin.get_adjustment().get_value())
|
|
||||||
|
|
||||||
def get_time(self) -> datetime:
|
|
||||||
"""
|
|
||||||
NOTE: interpolating seconds/microseconds from the current time
|
|
||||||
may prevent abrupt visual changes when setting a new TimePicker time
|
|
||||||
while a clock tick is updating the local remainder label.
|
|
||||||
However, this can introduce false negatives when running tests due to microsecond drift.
|
|
||||||
Ideally, tests should be decoupled from this logic.
|
|
||||||
"""
|
|
||||||
d = datetime.now()
|
|
||||||
return datetime.combine(
|
|
||||||
d,
|
|
||||||
time(
|
|
||||||
hour=self.get_hour(),
|
|
||||||
minute=self.get_minute(),
|
|
||||||
second=0,
|
|
||||||
microsecond=0,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
def set_time(self, hour: int, minute: int) -> None:
|
|
||||||
self.hour_spin.get_adjustment().set_value(hour)
|
|
||||||
self.minute_spin.get_adjustment().set_value(minute)
|
|
||||||
|
|
||||||
|
|
||||||
class AdjustedTimeFrame(Gtk.Frame):
|
|
||||||
def __init__(self, emitter: Emitter) -> None:
|
|
||||||
super().__init__(margin_start=50, margin_end=50)
|
|
||||||
|
|
||||||
self.emitter = emitter
|
|
||||||
self.emitter.connect(
|
|
||||||
"adjusted_local_time_changed", self._on_adjusted_time_changed
|
|
||||||
)
|
|
||||||
self.emitter.connect("remaining_time_changed", self._on_remainder_changed)
|
|
||||||
|
|
||||||
self.remainder_clock = RemainderClock(emitter)
|
|
||||||
self.remainder_heading = BoldLabel(calc.heading_remainder_time)
|
|
||||||
self.remaining_time = Gtk.Label(label=self.remainder_clock.get_time())
|
|
||||||
|
|
||||||
self.adjusted_clock = AdjustedClock(emitter)
|
|
||||||
self.adjusted_heading = BoldLabel(calc.heading_adjusted_time)
|
|
||||||
self.adjusted_local_time = Gtk.Label(label=self.adjusted_clock.get_time())
|
|
||||||
|
|
||||||
flowbox = Gtk.FlowBox(
|
|
||||||
row_spacing=20,
|
|
||||||
column_spacing=50,
|
|
||||||
min_children_per_line=2,
|
|
||||||
max_children_per_line=2,
|
|
||||||
margin=15,
|
|
||||||
halign=Gtk.Align.CENTER,
|
|
||||||
selection_mode=Gtk.SelectionMode.NONE,
|
|
||||||
)
|
|
||||||
flowbox.add(self.remainder_heading)
|
|
||||||
flowbox.add(self.adjusted_heading)
|
|
||||||
flowbox.add(self.remaining_time)
|
|
||||||
flowbox.add(self.adjusted_local_time)
|
|
||||||
|
|
||||||
recalc = Gtk.Button(
|
|
||||||
label=calc.calculate, halign=Gtk.Align.CENTER, margin_bottom=20
|
|
||||||
)
|
|
||||||
recalc.connect("clicked", self._on_recalc_clicked)
|
|
||||||
|
|
||||||
vbox = VBox()
|
|
||||||
vbox.extend([flowbox, recalc])
|
|
||||||
self.add(vbox)
|
|
||||||
|
|
||||||
def _on_remainder_changed(self, emitter: Emitter, time: datetime) -> None:
|
|
||||||
strtime = str(time.strftime(EXTENDED_TIME_FORMAT))
|
|
||||||
self.remaining_time.set_text(strtime)
|
|
||||||
|
|
||||||
def _on_adjusted_time_changed(self, emitter: Emitter, time: datetime) -> None:
|
|
||||||
strtime = str(time.strftime(EXTENDED_TIME_FORMAT))
|
|
||||||
self.adjusted_local_time.set_text(strtime)
|
|
||||||
|
|
||||||
def _on_recalc_clicked(self, button: Gtk.Button) -> None:
|
|
||||||
self.emitter.emit("calculate_button_clicked")
|
|
||||||
|
|
||||||
|
|
||||||
class TimePickerFrame(Gtk.Frame):
|
|
||||||
def __init__(self, emitter: Emitter, stime: Time) -> None:
|
|
||||||
super().__init__(margin_start=50, margin_end=50)
|
|
||||||
|
|
||||||
self.emitter = emitter
|
|
||||||
self.emitter.connect("local_time_incremented", self._on_local_clock_increment)
|
|
||||||
self.emitter.connect("server_time_incremented", self._on_server_clock_increment)
|
|
||||||
self.emitter.connect(
|
|
||||||
"calculate_button_clicked", self._on_calculate_button_clicked
|
|
||||||
)
|
|
||||||
|
|
||||||
self.local_clock = LocalClock(self.emitter)
|
|
||||||
local_clock_time = self.local_clock.get_time()
|
|
||||||
|
|
||||||
self.server_time = ServerClock(stime, self.emitter, self.local_clock)
|
|
||||||
hour, minute = self.server_time.get_values()
|
|
||||||
self.picker = TimePicker(hour, minute)
|
|
||||||
|
|
||||||
self.local_timelabel = Gtk.Label(label=local_clock_time)
|
|
||||||
self.server_timelabel = Gtk.Label(label=stime.time, halign=Gtk.Align.CENTER)
|
|
||||||
|
|
||||||
h0 = BoldLabel(calc.heading_local_time)
|
|
||||||
h1 = BoldLabel(calc.heading_server_time)
|
|
||||||
h2 = BoldLabel(calc.heading_target_time)
|
|
||||||
|
|
||||||
self.icon_indicator = Icon(DAY_ICON)
|
|
||||||
self.update_server_icon()
|
|
||||||
|
|
||||||
server_time_box = HBox(10)
|
|
||||||
server_time_box.set_halign(Gtk.Align.CENTER)
|
|
||||||
server_time_box.extend([self.server_timelabel, self.icon_indicator])
|
|
||||||
|
|
||||||
flowbox = Gtk.FlowBox(
|
|
||||||
row_spacing=10,
|
|
||||||
column_spacing=50,
|
|
||||||
min_children_per_line=3,
|
|
||||||
max_children_per_line=3,
|
|
||||||
margin=15,
|
|
||||||
halign=Gtk.Align.CENTER,
|
|
||||||
selection_mode=Gtk.SelectionMode.NONE,
|
|
||||||
)
|
|
||||||
flowbox.add(h0)
|
|
||||||
flowbox.add(h1)
|
|
||||||
flowbox.add(h2)
|
|
||||||
flowbox.add(self.local_timelabel)
|
|
||||||
flowbox.add(server_time_box)
|
|
||||||
flowbox.add(self.picker)
|
|
||||||
|
|
||||||
self.add(flowbox)
|
|
||||||
|
|
||||||
def update_server_icon(self) -> None:
|
|
||||||
icon = DAY_ICON if self.server_time.is_day() else NIGHT_ICON
|
|
||||||
self.icon_indicator.set_icon_name(icon)
|
|
||||||
|
|
||||||
def _on_server_clock_increment(self, emitter: Emitter, time_now: datetime) -> None:
|
|
||||||
t = time_now.time().strftime("%H:%M")
|
|
||||||
self.server_timelabel.set_text(t)
|
|
||||||
self.update_server_icon()
|
|
||||||
|
|
||||||
def _on_local_clock_increment(
|
|
||||||
self, emitter: Emitter, time_now: datetime, elapsed: timedelta
|
|
||||||
) -> None:
|
|
||||||
t = time_now.time().strftime(EXTENDED_TIME_FORMAT)
|
|
||||||
self.local_timelabel.set_text(t)
|
|
||||||
|
|
||||||
def _on_calculate_button_clicked(self, button: Gtk.Button) -> None:
|
|
||||||
if (self.picker.get_values()) == (self.server_time.get_values()):
|
|
||||||
total_time = timedelta(seconds=0)
|
|
||||||
self.emitter.emit("target_time_reset", total_time)
|
|
||||||
else:
|
|
||||||
picker_time = self.picker.get_time()
|
|
||||||
total_time = self.server_time.calc_delta(picker_time)
|
|
||||||
self.emitter.emit("target_time_changed", total_time)
|
|
||||||
|
|
||||||
|
|
||||||
class ServerTimeCalculator(Gtk.ScrolledWindow):
|
|
||||||
def __init__(self, stime: Time) -> None:
|
|
||||||
super().__init__(propagate_natural_width=True)
|
|
||||||
|
|
||||||
self.emitter = Emitter()
|
|
||||||
outerbox = Gtk.Box(
|
|
||||||
halign=Gtk.Align.CENTER,
|
|
||||||
valign=Gtk.Align.START,
|
|
||||||
spacing=20,
|
|
||||||
orientation=Gtk.Orientation.VERTICAL,
|
|
||||||
)
|
|
||||||
time_picker_frame = TimePickerFrame(self.emitter, stime)
|
|
||||||
adjusted_time_frame = AdjustedTimeFrame(self.emitter)
|
|
||||||
disclaimer = Gtk.Label(
|
|
||||||
label=calc.disclaimer,
|
|
||||||
justify=Gtk.Justification.CENTER,
|
|
||||||
)
|
|
||||||
for el in time_picker_frame, adjusted_time_frame, disclaimer:
|
|
||||||
outerbox.add(el)
|
|
||||||
self.add(outerbox)
|
|
||||||
@@ -309,14 +309,6 @@ class ExceptionDialog(TextBufferDialog):
|
|||||||
content.set_spacing(0)
|
content.set_spacing(0)
|
||||||
content.add(scrollable)
|
content.add(scrollable)
|
||||||
|
|
||||||
self.connect("map", self._on_map)
|
|
||||||
|
|
||||||
def _on_map(self, dialog: Self) -> None:
|
|
||||||
button = self.get_widget_for_response(Gtk.ResponseType.OK)
|
|
||||||
if button is None:
|
|
||||||
return
|
|
||||||
button.grab_focus()
|
|
||||||
|
|
||||||
def _on_page_changed(
|
def _on_page_changed(
|
||||||
self, notebook: Gtk.Notebook, child: Gtk.Box | Gtk.ScrolledWindow, index: int
|
self, notebook: Gtk.Notebook, child: Gtk.Box | Gtk.ScrolledWindow, index: int
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|||||||
@@ -1,21 +1,20 @@
|
|||||||
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.constants import EXPAND, FILL
|
||||||
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
|
||||||
|
from dzgui.util import strings
|
||||||
from dzgui.util.format import format_hyperlinks, format_server_mods
|
from dzgui.util.format import format_hyperlinks, format_server_mods
|
||||||
from dzgui.strings import server_mods, dialogs
|
from dzgui.strings import server_mods
|
||||||
from dzgui.views.components.buttons import IconTextButton
|
|
||||||
from dzgui.views.components.box import VBox
|
|
||||||
from dzgui.views.dialogs.calc import Time, ServerTimeCalculator
|
|
||||||
from dzgui.views.dialogs.generic import GenericDialog
|
from dzgui.views.dialogs.generic import GenericDialog
|
||||||
from dzgui.views.trees.tree_base import TreeView
|
from dzgui.views.trees.tree_base import TreeView
|
||||||
|
|
||||||
|
import gi
|
||||||
|
|
||||||
gi.require_version("Gtk", "3.0")
|
gi.require_version("Gtk", "3.0")
|
||||||
from gi.repository import Gtk, Gdk, Pango # noqa
|
from gi.repository import Gtk, GLib, Gdk, GObject, Pango # noqa
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from dzgui.api.servers import Details
|
from dzgui.api.servers import Details
|
||||||
@@ -47,19 +46,12 @@ class ServerDialog(GenericDialog):
|
|||||||
self.connect("response", self._on_response)
|
self.connect("response", self._on_response)
|
||||||
self.view.connect("key-press-event", self._on_keypress)
|
self.view.connect("key-press-event", self._on_keypress)
|
||||||
|
|
||||||
self.scrollable_tree = Gtk.ScrolledWindow(vexpand=True)
|
self.scrollable_tree = Gtk.ScrolledWindow()
|
||||||
self.scrollable_tree.add(self.view)
|
self.scrollable_tree.add(self.view)
|
||||||
self.scrollable_tree.set_size_request(700, 500)
|
self.scrollable_tree.set_size_request(700, 400)
|
||||||
|
|
||||||
self.stack = Gtk.Stack()
|
|
||||||
self.content_box = VBox(10)
|
|
||||||
self.content_box.set_valign(Gtk.Align.FILL)
|
|
||||||
self.content_box.add(self.scrollable_tree)
|
|
||||||
|
|
||||||
self.stack.add_named(self.content_box, "METADATA")
|
|
||||||
|
|
||||||
self.content = self.get_content_area()
|
self.content = self.get_content_area()
|
||||||
self.content.pack_start(self.stack, expand=True, fill=True, padding=0)
|
self.content.pack_start(self.scrollable_tree, EXPAND, FILL, 0)
|
||||||
|
|
||||||
def _on_keypress(self, view: Gtk.TreeView, event: Gdk.EventKey) -> bool:
|
def _on_keypress(self, view: Gtk.TreeView, event: Gdk.EventKey) -> bool:
|
||||||
# NOTE: ESC normally unfocuses treeview instead of destroying dialog
|
# NOTE: ESC normally unfocuses treeview instead of destroying dialog
|
||||||
@@ -74,7 +66,9 @@ class ServerDialog(GenericDialog):
|
|||||||
|
|
||||||
class ServerDetailsDialog(ServerDialog):
|
class ServerDetailsDialog(ServerDialog):
|
||||||
def __init__(self, controller: "Controller", details: "Details"):
|
def __init__(self, controller: "Controller", details: "Details"):
|
||||||
super().__init__(controller, dialogs.server_details, details.name, menu=None)
|
# TODO: server name should also be packed in details struct
|
||||||
|
name = controller.get_server_name()
|
||||||
|
super().__init__(controller, strings.server_details, name, menu=None)
|
||||||
|
|
||||||
self.store = Gtk.ListStore(str, str, Pango.Weight)
|
self.store = Gtk.ListStore(str, str, Pango.Weight)
|
||||||
self.view.connect("row-activated", self._on_row_activated)
|
self.view.connect("row-activated", self._on_row_activated)
|
||||||
@@ -91,9 +85,14 @@ class ServerDetailsDialog(ServerDialog):
|
|||||||
column.set_sort_column_id(i)
|
column.set_sort_column_id(i)
|
||||||
column.set_expand(True)
|
column.set_expand(True)
|
||||||
|
|
||||||
scrollable_message = Gtk.ScrolledWindow(vexpand=True)
|
# NOTE: constrain tree area to logical height of metadata
|
||||||
desc = Gtk.Label(label=dialogs.server_message, valign=Gtk.Align.START)
|
# leaving room for server message
|
||||||
css.add_class(desc, "server-subheading")
|
self.scrollable_tree.set_size_request(700, 150)
|
||||||
|
|
||||||
|
# TODO: make "Server message" text boldface
|
||||||
|
scrollable_message = Gtk.ScrolledWindow()
|
||||||
|
desc = Gtk.Label(label=strings.server_message, valign=Gtk.Align.START)
|
||||||
|
css.add_class(desc, "details-heading")
|
||||||
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, halign=Gtk.Align.CENTER)
|
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, halign=Gtk.Align.CENTER)
|
||||||
self.description = Gtk.Label(justify=Gtk.Justification.CENTER, wrap=True)
|
self.description = Gtk.Label(justify=Gtk.Justification.CENTER, wrap=True)
|
||||||
sep = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL)
|
sep = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL)
|
||||||
@@ -101,7 +100,11 @@ class ServerDetailsDialog(ServerDialog):
|
|||||||
for el in desc, sep, self.description:
|
for el in desc, sep, self.description:
|
||||||
box.add(el)
|
box.add(el)
|
||||||
scrollable_message.add(box)
|
scrollable_message.add(box)
|
||||||
self.content_box.add(scrollable_message)
|
|
||||||
|
self.pack(scrollable_message)
|
||||||
|
|
||||||
|
if details.data is None:
|
||||||
|
return
|
||||||
|
|
||||||
for row in details.data:
|
for row in details.data:
|
||||||
self.store.append(row + [Pango.Weight.BOLD])
|
self.store.append(row + [Pango.Weight.BOLD])
|
||||||
@@ -110,32 +113,8 @@ 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.connect("clicked", self._on_calc_pressed)
|
|
||||||
calc_button.set_halign(Gtk.Align.END)
|
|
||||||
calc_button.set_valign(Gtk.Align.START)
|
|
||||||
self.content.pack_start(calc_button, False, False, 0)
|
|
||||||
self.content.reorder_child(calc_button, 2)
|
|
||||||
|
|
||||||
self.scrollable_tree.set_size_request(700, 250)
|
|
||||||
self.calc = ServerTimeCalculator(
|
|
||||||
Time(details.gametime, details.day_accel, details.night_accel)
|
|
||||||
)
|
|
||||||
self.stack.add_named(self.calc, "CALCULATOR")
|
|
||||||
|
|
||||||
self.connect("map", self._on_map)
|
|
||||||
self.show_all()
|
self.show_all()
|
||||||
|
|
||||||
def _on_map(self, dialog: Self) -> None:
|
|
||||||
if widget := self.get_widget_for_response(Gtk.ResponseType.OK):
|
|
||||||
widget.grab_focus()
|
|
||||||
|
|
||||||
def _on_calc_pressed(self, button: Gtk.Button) -> None:
|
|
||||||
if self.stack.get_visible_child_name() == "METADATA":
|
|
||||||
self.stack.set_visible_child_name("CALCULATOR")
|
|
||||||
else:
|
|
||||||
self.stack.set_visible_child_name("METADATA")
|
|
||||||
|
|
||||||
def _on_row_activated(
|
def _on_row_activated(
|
||||||
self,
|
self,
|
||||||
treeview: Gtk.TreeView,
|
treeview: Gtk.TreeView,
|
||||||
|
|||||||
@@ -496,7 +496,8 @@ class Assistant(Gtk.Assistant):
|
|||||||
if page.is_migrated():
|
if page.is_migrated():
|
||||||
sp = page.migrated_conf["default_steam_path"]
|
sp = page.migrated_conf["default_steam_path"]
|
||||||
self.page_shortcuts.set_steam_path(sp)
|
self.page_shortcuts.set_steam_path(sp)
|
||||||
self.config_values = self.page_migration.migrated_conf
|
self.write_config(self.page_migration.migrated_conf)
|
||||||
|
self.setup_complete = True
|
||||||
return self.get_final_page()
|
return self.get_final_page()
|
||||||
case SteamPathPage():
|
case SteamPathPage():
|
||||||
self.config_values["default_steam_path"] = page.get_path_from_radio()
|
self.config_values["default_steam_path"] = page.get_path_from_radio()
|
||||||
@@ -508,9 +509,11 @@ class Assistant(Gtk.Assistant):
|
|||||||
self.config_values["name"] = name
|
self.config_values["name"] = name
|
||||||
self.config_values["use_miles"] = use_miles
|
self.config_values["use_miles"] = use_miles
|
||||||
self.config_values["client"] = client
|
self.config_values["client"] = client
|
||||||
|
self.write_config(self.config_values)
|
||||||
self.page_shortcuts.set_steam_path(
|
self.page_shortcuts.set_steam_path(
|
||||||
self.config_values["default_steam_path"]
|
self.config_values["default_steam_path"]
|
||||||
)
|
)
|
||||||
|
self.setup_complete = True
|
||||||
case ShortcutCreationPage():
|
case ShortcutCreationPage():
|
||||||
page.create_shortcuts()
|
page.create_shortcuts()
|
||||||
case _:
|
case _:
|
||||||
@@ -562,10 +565,6 @@ class Assistant(Gtk.Assistant):
|
|||||||
if not isinstance(page, IntroductionPage):
|
if not isinstance(page, IntroductionPage):
|
||||||
EMITTER.emit("step_pending")
|
EMITTER.emit("step_pending")
|
||||||
|
|
||||||
if page == self.page_completion:
|
|
||||||
self.write_config(self.config_values)
|
|
||||||
self.setup_complete = True
|
|
||||||
|
|
||||||
|
|
||||||
class CheckboxWithLabel(Gtk.Box):
|
class CheckboxWithLabel(Gtk.Box):
|
||||||
def __init__(self, text: str, blurb_text: str) -> None:
|
def __init__(self, text: str, blurb_text: str) -> None:
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ class SubmitField(Gtk.Box):
|
|||||||
self.button = Gtk.Button(label=options.save_button)
|
self.button = Gtk.Button(label=options.save_button)
|
||||||
|
|
||||||
self.button.connect("clicked", self._on_save_clicked)
|
self.button.connect("clicked", self._on_save_clicked)
|
||||||
self.entry.connect("changed", self._on_text_typed)
|
self.entry.connect("insert-text", self._on_text_typed)
|
||||||
self.entry.connect("activate", self._on_field_activated)
|
self.entry.connect("activate", self._on_field_activated)
|
||||||
self.entry.get_property("buffer").connect("deleted-text", self._on_text_deleted)
|
self.entry.get_property("buffer").connect("deleted-text", self._on_text_deleted)
|
||||||
|
|
||||||
@@ -113,9 +113,13 @@ class SubmitField(Gtk.Box):
|
|||||||
def _on_text_typed(
|
def _on_text_typed(
|
||||||
self,
|
self,
|
||||||
entry: Gtk.Entry,
|
entry: Gtk.Entry,
|
||||||
|
text: str,
|
||||||
|
length: int,
|
||||||
|
pos: int,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
text = entry.get_text()
|
buffer = entry.get_property("buffer")
|
||||||
|
text = buffer.get_text() + text
|
||||||
state = self._is_valid_text(text)
|
state = self._is_valid_text(text)
|
||||||
self.button.set_sensitive(state)
|
self.button.set_sensitive(state)
|
||||||
|
|
||||||
@@ -367,8 +371,10 @@ class Options(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore
|
|||||||
|
|
||||||
self.connect("key-press-event", self._on_keypress)
|
self.connect("key-press-event", self._on_keypress)
|
||||||
|
|
||||||
def get_client_name(self, cmd: str) -> str:
|
def get_client_name(self) -> str:
|
||||||
return self.client_combo.get_client_human_readable(cmd)
|
model = self.client_combo.get_model()
|
||||||
|
ind = self.client_combo.get_active()
|
||||||
|
return str(model[ind][0])
|
||||||
|
|
||||||
def block_text_entry(self) -> None:
|
def block_text_entry(self) -> None:
|
||||||
self.steam_box.block_text_entry()
|
self.steam_box.block_text_entry()
|
||||||
|
|||||||
@@ -176,7 +176,8 @@ 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(ERROR)
|
||||||
self.error_placeholder = Placeholder(preconnect.no_errors)
|
# TODO: strings
|
||||||
|
self.error_placeholder = Placeholder("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)
|
||||||
self.error_frame = HeadingFrame.new_with_widget_and_label(
|
self.error_frame = HeadingFrame.new_with_widget_and_label(
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore
|
|||||||
self.handler_id: int
|
self.handler_id: int
|
||||||
self.queue: Queue = Queue()
|
self.queue: Queue = Queue()
|
||||||
|
|
||||||
self.seen_cache: list[Record] = []
|
self.seen_cache: list[str] = []
|
||||||
|
|
||||||
prefs = self.controller.get_prefs()
|
prefs = self.controller.get_prefs()
|
||||||
columns = prefs.paths.columns
|
columns = prefs.paths.columns
|
||||||
@@ -252,6 +252,7 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore
|
|||||||
case Gdk.KEY_c:
|
case Gdk.KEY_c:
|
||||||
self.controller.menu_action(ContextMenu.COPY_SERVER_IP, self)
|
self.controller.menu_action(ContextMenu.COPY_SERVER_IP, self)
|
||||||
case Gdk.KEY_r:
|
case Gdk.KEY_r:
|
||||||
|
# TODO: unimplemented, needs threading
|
||||||
self.controller.menu_action(ContextMenu.REFRESH_PLAYERS, self)
|
self.controller.menu_action(ContextMenu.REFRESH_PLAYERS, self)
|
||||||
else:
|
else:
|
||||||
match event.keyval:
|
match event.keyval:
|
||||||
@@ -340,24 +341,16 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore
|
|||||||
control = self.proxy_man.get_control()
|
control = self.proxy_man.get_control()
|
||||||
return model, control
|
return model, control
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
def ping_server(
|
def ping_server(
|
||||||
self,
|
|
||||||
model: "FastInsertListStore",
|
model: "FastInsertListStore",
|
||||||
_iter: Gtk.TreeIter,
|
_iter: Gtk.TreeIter,
|
||||||
record: Record,
|
ip: str,
|
||||||
|
qport: int,
|
||||||
|
ping_column: int,
|
||||||
) -> None:
|
) -> None:
|
||||||
_ping = ping(record.ip, record.qport)
|
_ping = ping(ip, qport)
|
||||||
GLib.idle_add(self._redraw, model, _iter, _ping)
|
GLib.idle_add(lambda: model.set(_iter, ping_column, _ping))
|
||||||
|
|
||||||
def _redraw(
|
|
||||||
self, model: "FastInsertListStore", _iter: Gtk.TreeIter, _ping: int
|
|
||||||
) -> None:
|
|
||||||
ping_column = 9
|
|
||||||
model.set(_iter, ping_column, _ping)
|
|
||||||
self.queue_draw()
|
|
||||||
|
|
||||||
def wipe_cache(self) -> None:
|
|
||||||
self.seen_cache = []
|
|
||||||
|
|
||||||
def _get_ping(
|
def _get_ping(
|
||||||
self,
|
self,
|
||||||
@@ -370,12 +363,13 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore
|
|||||||
|
|
||||||
addr_column = 7
|
addr_column = 7
|
||||||
qport_column = 8
|
qport_column = 8
|
||||||
|
ping_column = 9
|
||||||
|
|
||||||
addr = model.get_value(_iter, addr_column).split(":")
|
addr = model.get_value(_iter, addr_column).split(":")
|
||||||
ip = addr[0]
|
ip = addr[0]
|
||||||
gameport = int(addr[1])
|
gameport = addr[1]
|
||||||
qport = model.get_value(_iter, qport_column)
|
qport = model.get_value(_iter, qport_column)
|
||||||
record = Record(ip, gameport, qport)
|
record = f"{addr}:{gameport}:{qport}"
|
||||||
|
|
||||||
if record in self.seen_cache:
|
if record in self.seen_cache:
|
||||||
return
|
return
|
||||||
@@ -384,7 +378,7 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore
|
|||||||
thread = threading.Thread(
|
thread = threading.Thread(
|
||||||
daemon=True,
|
daemon=True,
|
||||||
target=self.ping_server,
|
target=self.ping_server,
|
||||||
args=(model, _iter, record),
|
args=(model, _iter, ip, qport, ping_column),
|
||||||
)
|
)
|
||||||
thread.start()
|
thread.start()
|
||||||
|
|
||||||
|
|||||||
+1
-2
@@ -4,7 +4,7 @@ description = "DayZ server browser and mod manager for Linux"
|
|||||||
authors = [
|
authors = [
|
||||||
{name = "aclist"}
|
{name = "aclist"}
|
||||||
]
|
]
|
||||||
version = "7.0.0b27"
|
version = "7.0.0b22"
|
||||||
license = "GPL-3.0-or-later"
|
license = "GPL-3.0-or-later"
|
||||||
license-files = ["LICENSE"]
|
license-files = ["LICENSE"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
@@ -47,7 +47,6 @@ docs = [
|
|||||||
]
|
]
|
||||||
test = [
|
test = [
|
||||||
"black==25.1.0",
|
"black==25.1.0",
|
||||||
"hypothesis==6.167.1",
|
|
||||||
"pre-commit==4.4.0",
|
"pre-commit==4.4.0",
|
||||||
"pytest==8.4.2",
|
"pytest==8.4.2",
|
||||||
"pytest-cov==7.0.0",
|
"pytest-cov==7.0.0",
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
"users"
|
|
||||||
{
|
|
||||||
"0"
|
|
||||||
{
|
|
||||||
"AccountName" "STEAMUSER"
|
|
||||||
"PersonaName" "STEAMUSER"
|
|
||||||
"RememberPassword" "0"
|
|
||||||
"WantsOfflineMode" "0"
|
|
||||||
"SkipOfflineModeWarning" "0"
|
|
||||||
"AutoLogin" "1"
|
|
||||||
"Timestamp" "1787904411"
|
|
||||||
}
|
|
||||||
"1"
|
|
||||||
{
|
|
||||||
"AccountName" "STEAMUSER2"
|
|
||||||
"PersonaName" "STEAMUSER2"
|
|
||||||
"RememberPassword" "0"
|
|
||||||
"WantsOfflineMode" "0"
|
|
||||||
"SkipOfflineModeWarning" "0"
|
|
||||||
"AutoLogin" "1"
|
|
||||||
"Timestamp" "1787904647"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
"users"
|
|
||||||
{
|
|
||||||
"0"
|
|
||||||
{
|
|
||||||
"AccountName" "STEAMUSER"
|
|
||||||
"PersonaName" "STEAMUSER"
|
|
||||||
"RememberPassword" "0"
|
|
||||||
"WantsOfflineMode" "0"
|
|
||||||
"SkipOfflineModeWarning" "0"
|
|
||||||
"AutoLogin" "1"
|
|
||||||
"Timestamp" "1797904411"
|
|
||||||
}
|
|
||||||
"1"
|
|
||||||
{
|
|
||||||
"AccountName" "STEAMUSER2"
|
|
||||||
"PersonaName" "STEAMUSER2"
|
|
||||||
"RememberPassword" "0"
|
|
||||||
"WantsOfflineMode" "0"
|
|
||||||
"SkipOfflineModeWarning" "0"
|
|
||||||
"AutoLogin" "1"
|
|
||||||
"Timestamp" "1787904647"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -20,6 +20,6 @@
|
|||||||
"SkipOfflineModeWarning" "0"
|
"SkipOfflineModeWarning" "0"
|
||||||
"AllowAutoLogin" "1"
|
"AllowAutoLogin" "1"
|
||||||
"MostRecent" "1"
|
"MostRecent" "1"
|
||||||
"Timestamp" "10"
|
"Timestamp" "0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -22,14 +22,9 @@ class MockController:
|
|||||||
|
|
||||||
|
|
||||||
def test_html_escaping() -> None:
|
def test_html_escaping() -> None:
|
||||||
name = "Test server"
|
text = "<Server description> Game & mods"
|
||||||
desc = "<Server description> Game & mods"
|
details = Details([["0", "1"]], text, True)
|
||||||
gametime = "05:30"
|
|
||||||
day_accel = 5.0
|
|
||||||
night_accel = 1.0
|
|
||||||
details = Details([["0", "1"]], name, desc, gametime, day_accel, night_accel)
|
|
||||||
controller = MockController()
|
controller = MockController()
|
||||||
dialog = ServerDetailsDialog(controller, details)
|
dialog = ServerDetailsDialog(controller, details)
|
||||||
dialog.destroy()
|
|
||||||
|
|
||||||
assert dialog.description.get_text() == desc
|
assert dialog.description.get_text() == text
|
||||||
@@ -14,8 +14,6 @@ pytestmark = pytest.mark.apitest
|
|||||||
("api/loginusers_legacy_client.vdf", 0),
|
("api/loginusers_legacy_client.vdf", 0),
|
||||||
("api/loginusers_legacy_client_multiple.vdf", 1),
|
("api/loginusers_legacy_client_multiple.vdf", 1),
|
||||||
("api/loginusers_beta_client.vdf", 0),
|
("api/loginusers_beta_client.vdf", 0),
|
||||||
("api/loginusers_beta_client_multiple.vdf", 1),
|
|
||||||
("api/loginusers_beta_client_multiple_2.vdf", 0),
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_loginusers(monkeypatch, fixture: str, expect: int) -> None:
|
def test_loginusers(monkeypatch, fixture: str, expect: int) -> None:
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
from pathlib import Path
|
|
||||||
from dzgui.config.xdg import get_xdg_paths
|
|
||||||
|
|
||||||
|
|
||||||
def test_path_fallback(monkeypatch) -> None:
|
|
||||||
envs = ["XDG_DATA_HOME", "XDG_STATE_HOME", "XDG_CONFIG_HOME", "XDG_CACHE_HOME"]
|
|
||||||
for env in envs:
|
|
||||||
monkeypatch.setenv(env, "/etc/dzgui")
|
|
||||||
paths = get_xdg_paths()
|
|
||||||
assert paths["XDG_CONFIG_HOME"] == Path.home().joinpath(".config/dzgui")
|
|
||||||
assert paths["XDG_STATE_HOME"] == Path.home().joinpath(".local/state/dzgui")
|
|
||||||
assert paths["XDG_DATA_HOME"] == Path.home().joinpath(".local/share/dzgui")
|
|
||||||
assert paths["XDG_CACHE_HOME"] == Path.home().joinpath(".cache/dzgui")
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
import unittest
|
|
||||||
from unittest.mock import patch
|
|
||||||
|
|
||||||
from dzgui.const.constants import (
|
|
||||||
FLATPAK_APPID,
|
|
||||||
FLATPAK_RUN_CMD,
|
|
||||||
FLATPAK_SANDBOX,
|
|
||||||
STEAM_CMD,
|
|
||||||
)
|
|
||||||
from dzgui.init import proc
|
|
||||||
|
|
||||||
|
|
||||||
class TestSteamDetection(unittest.TestCase):
|
|
||||||
def test_flatpak_detection_with_multiple_running_apps(self):
|
|
||||||
def flatpak_output(args, *, text):
|
|
||||||
# Default `flatpak ps` includes instance, PID, app and runtime columns.
|
|
||||||
if args == ["flatpak", "ps", "--columns=application"]:
|
|
||||||
return f"com.spotify.Client\n{FLATPAK_APPID}\n{FLATPAK_APPID}\n"
|
|
||||||
return f"3904921611\t1257689\t{FLATPAK_APPID}\torg.freedesktop.Platform\n"
|
|
||||||
|
|
||||||
with (
|
|
||||||
patch.object(proc, "has_cmd", return_value=True),
|
|
||||||
patch.object(proc.subprocess, "check_output", side_effect=flatpak_output),
|
|
||||||
):
|
|
||||||
for client in (FLATPAK_RUN_CMD, FLATPAK_SANDBOX):
|
|
||||||
with self.subTest(client=client):
|
|
||||||
self.assertTrue(proc.is_steam_running(client))
|
|
||||||
|
|
||||||
def test_flatpak_not_running(self):
|
|
||||||
with patch.object(proc, "has_cmd", return_value=True):
|
|
||||||
for output in ("", "com.spotify.Client\n", f"{FLATPAK_APPID}Helper\n"):
|
|
||||||
with (
|
|
||||||
self.subTest(output=output),
|
|
||||||
patch.object(proc.subprocess, "check_output", return_value=output),
|
|
||||||
):
|
|
||||||
self.assertFalse(proc.is_flatpak_steam_running())
|
|
||||||
|
|
||||||
def test_flatpak_not_installed(self):
|
|
||||||
with (
|
|
||||||
patch.object(proc, "has_cmd", return_value=False),
|
|
||||||
patch.object(proc.subprocess, "check_output") as check_output,
|
|
||||||
):
|
|
||||||
self.assertFalse(proc.is_flatpak_steam_running())
|
|
||||||
check_output.assert_not_called()
|
|
||||||
|
|
||||||
def test_native_steam(self):
|
|
||||||
with patch.object(proc, "has_cmd", return_value=True):
|
|
||||||
for running in (True, False):
|
|
||||||
with (
|
|
||||||
self.subTest(running=running),
|
|
||||||
patch.object(proc, "is_running", return_value=running),
|
|
||||||
):
|
|
||||||
self.assertEqual(proc.is_steam_running(STEAM_CMD), running)
|
|
||||||
|
|
||||||
def test_native_steam_while_flatpak_is_running(self):
|
|
||||||
with patch.object(proc, "is_flatpak_steam_running", return_value=True):
|
|
||||||
self.assertEqual(proc.is_steam_running(STEAM_CMD), False)
|
|
||||||
|
|
||||||
def test_invalid_client(self):
|
|
||||||
with self.assertRaises(TypeError):
|
|
||||||
proc.is_steam_running("invalid-client")
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
import pytest
|
|
||||||
import re
|
|
||||||
|
|
||||||
from dzgui.api.servers import Details, Record, get_details
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def details() -> Details:
|
|
||||||
"""
|
|
||||||
Static official server, metadata is expected to be invariate
|
|
||||||
"""
|
|
||||||
r = Record("85.190.157.111", 10400, 10401)
|
|
||||||
return get_details(r)
|
|
||||||
|
|
||||||
|
|
||||||
def test_invalid_server() -> None:
|
|
||||||
r = Record("127.0.0.1", 10400, 10401)
|
|
||||||
assert get_details(r) is None
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("key, expect", [("day_accel", 2.3), ("night_accel", 6.8)])
|
|
||||||
def test_time_accel(details, key, expect) -> None:
|
|
||||||
assert getattr(details, key) == expect
|
|
||||||
|
|
||||||
|
|
||||||
def test_gametime(details) -> None:
|
|
||||||
time_reg = r"^(0[0-9]|1[0-9]|2[0-3]):([0-5][0-9]$)"
|
|
||||||
match = re.match(time_reg, details.gametime)
|
|
||||||
assert match is not None
|
|
||||||
|
|
||||||
|
|
||||||
def test_metadata(details) -> None:
|
|
||||||
for row in details.data:
|
|
||||||
label, value = row
|
|
||||||
match label:
|
|
||||||
case "Password":
|
|
||||||
assert value == "Disabled"
|
|
||||||
case "Platform":
|
|
||||||
assert value == "Windows"
|
|
||||||
case "Battleye":
|
|
||||||
assert value == "Enabled"
|
|
||||||
case "Valve Anti-Cheat":
|
|
||||||
assert value == "Enabled"
|
|
||||||
case _:
|
|
||||||
pass
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
import pytest
|
|
||||||
|
|
||||||
from hypothesis import given, strategies as st
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
|
|
||||||
from dzgui.views.dialogs.calc import (
|
|
||||||
LocalClock,
|
|
||||||
ServerClock,
|
|
||||||
Time,
|
|
||||||
TimePicker,
|
|
||||||
DAY_START_TIME,
|
|
||||||
DAY_END_TIME,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class DummyEmitter:
|
|
||||||
def __init__(self) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def connect(self, _, __) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(params=[("05:00", 10.0), ("15:30", 5.0)])
|
|
||||||
def accel_server_clock(request) -> None:
|
|
||||||
clock_time, expect = request.param
|
|
||||||
cur_time = Time(clock_time, 5.0, 2.0)
|
|
||||||
emitter = DummyEmitter()
|
|
||||||
local_clock = LocalClock(emitter)
|
|
||||||
server_clock = ServerClock(cur_time, emitter, local_clock)
|
|
||||||
return expect, server_clock
|
|
||||||
|
|
||||||
|
|
||||||
def test_accel_factor(accel_server_clock) -> None:
|
|
||||||
"""Static test checking times at known boundaries"""
|
|
||||||
expect, clock = accel_server_clock
|
|
||||||
assert clock.get_accel_factor(clock.cur_time) == expect
|
|
||||||
|
|
||||||
|
|
||||||
def reference_calc(start, end, day_factor, night_factor):
|
|
||||||
"""
|
|
||||||
Reference implementation to test validity of UI-side calculator
|
|
||||||
Aggregates total seconds and applies accel factors after
|
|
||||||
"""
|
|
||||||
day_start = datetime.combine(start.date(), DAY_START_TIME)
|
|
||||||
day_end = datetime.combine(start.date(), DAY_END_TIME)
|
|
||||||
night_factor = day_factor * night_factor
|
|
||||||
|
|
||||||
if end < start:
|
|
||||||
end += timedelta(days=1)
|
|
||||||
|
|
||||||
day_max = max(start, day_start)
|
|
||||||
day_min = min(end, day_end)
|
|
||||||
|
|
||||||
next_day_start = datetime.combine(end.date(), DAY_START_TIME)
|
|
||||||
next_day_end = datetime.combine(end.date(), DAY_END_TIME)
|
|
||||||
|
|
||||||
range1 = (day_end - day_max).total_seconds()
|
|
||||||
range2 = (min(end, next_day_end) - next_day_start).total_seconds()
|
|
||||||
|
|
||||||
if end.date() > start.date():
|
|
||||||
day_seconds = max(0, range1) + max(0, range2)
|
|
||||||
else:
|
|
||||||
day_seconds = max(0, (day_min - day_max).total_seconds())
|
|
||||||
|
|
||||||
total_seconds = (end - start).total_seconds()
|
|
||||||
night_seconds = (total_seconds - day_seconds) / night_factor
|
|
||||||
day_seconds = day_seconds / day_factor
|
|
||||||
total = night_seconds + day_seconds
|
|
||||||
|
|
||||||
return timedelta(seconds=total)
|
|
||||||
|
|
||||||
|
|
||||||
day_accel = st.floats(
|
|
||||||
min_value=1.0, max_value=24.0, allow_nan=False, allow_infinity=False
|
|
||||||
)
|
|
||||||
night_accel = st.floats(
|
|
||||||
min_value=1.0, max_value=64.0, allow_nan=False, allow_infinity=False
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@st.composite
|
|
||||||
def time_ranges(draw):
|
|
||||||
start_time = draw(st.times())
|
|
||||||
deltas = st.timedeltas(min_value=timedelta(0), max_value=timedelta(days=1))
|
|
||||||
duration = draw(deltas)
|
|
||||||
|
|
||||||
start = datetime.combine(datetime.now(), start_time)
|
|
||||||
end = start + duration
|
|
||||||
return start, end
|
|
||||||
|
|
||||||
|
|
||||||
@given(span=time_ranges(), day_factor=day_accel, night_factor=night_accel)
|
|
||||||
def test_time_aggregation(span, day_factor, night_factor) -> None:
|
|
||||||
start, end = span
|
|
||||||
|
|
||||||
strtime = start.strftime("%H:%M")
|
|
||||||
t = Time(strtime, day_factor, night_factor)
|
|
||||||
emitter = DummyEmitter()
|
|
||||||
local_clock = LocalClock(emitter)
|
|
||||||
server_clock = ServerClock(t, emitter, local_clock)
|
|
||||||
h = end.time().hour
|
|
||||||
m = end.time().minute
|
|
||||||
picker = TimePicker(h, m)
|
|
||||||
|
|
||||||
# NOTE: truncates microseconds to mimic widget HH:MM selection behavior
|
|
||||||
start = server_clock.get_datetime()
|
|
||||||
end = picker.get_time()
|
|
||||||
actual = server_clock.calc_delta(end)
|
|
||||||
expect = reference_calc(start, end, day_factor, night_factor)
|
|
||||||
|
|
||||||
assert actual == expect
|
|
||||||
+2
-1
@@ -2,6 +2,7 @@ import re
|
|||||||
|
|
||||||
from dzgui.model.proxy_model import DAY_REG, NIGHT_REG
|
from dzgui.model.proxy_model import DAY_REG, NIGHT_REG
|
||||||
|
|
||||||
|
|
||||||
def iterate(h: str, r: str) -> None:
|
def iterate(h: str, r: str) -> None:
|
||||||
for m in range(60):
|
for m in range(60):
|
||||||
time = f"{h:02}:{m:02}"
|
time = f"{h:02}:{m:02}"
|
||||||
@@ -17,6 +18,6 @@ def test_day() -> None:
|
|||||||
|
|
||||||
def test_night() -> None:
|
def test_night() -> None:
|
||||||
for h in range(24):
|
for h in range(24):
|
||||||
if 5 < h < 18:
|
if 6 < h < 17:
|
||||||
continue
|
continue
|
||||||
iterate(h, NIGHT_REG)
|
iterate(h, NIGHT_REG)
|
||||||
Reference in new issue
Block a user