mirror of
https://github.com/aclist/dztui.git
synced 2026-11-02 16:12:11 +01:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd0fd07ae9 |
No files matched your search
+28
-34
@@ -1,7 +1,6 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## [7.0.0] Unreleased
|
## Added
|
||||||
### Added
|
|
||||||
- Setup wizard
|
- Setup wizard
|
||||||
- Changelog text wrapping and formatting
|
- Changelog text wrapping and formatting
|
||||||
- Changelog ships with source
|
- Changelog ships with source
|
||||||
@@ -42,11 +41,8 @@
|
|||||||
- Choose to jump into splash screen instead of server
|
- Choose to jump into splash screen instead of server
|
||||||
- Collapsible connection panel
|
- Collapsible connection panel
|
||||||
- Play offline (load mods directly)
|
- Play offline (load mods directly)
|
||||||
- Generate Steam shortcuts and cover art
|
|
||||||
- Warn user if background downloads are disabled
|
|
||||||
- Dynamic copy button
|
|
||||||
|
|
||||||
### Changed
|
## Changed
|
||||||
- Conform to PEP 440 versioning for beta versions
|
- Conform to PEP 440 versioning for beta versions
|
||||||
- Reduce padding on keys button
|
- Reduce padding on keys button
|
||||||
- Boldface breadcrumbs
|
- Boldface breadcrumbs
|
||||||
@@ -63,60 +59,58 @@
|
|||||||
- Enable LAN page Empty/Full filters on first run of app
|
- Enable LAN page Empty/Full filters on first run of app
|
||||||
- Propagate subscribed mods to Steam client
|
- Propagate subscribed mods to Steam client
|
||||||
|
|
||||||
### Dropped
|
## Dropped
|
||||||
- Battlemetrics API support
|
|
||||||
- Debug mode
|
- Debug mode
|
||||||
- Branch switching
|
- Branch switching
|
||||||
- Manual mod install mode (describe rationale)
|
- Manual mod install mode (describe rationale)
|
||||||
- Force update mods
|
- Force update mods
|
||||||
|
|
||||||
### Fixed
|
## Fixed
|
||||||
- Longstanding issue with left clicks not registering as tree selection changes after spamming keyboard input
|
- Longstanding issue with left clicks not registering as tree selection changes after spamming keyboard input
|
||||||
- Center server title text on server dialogs
|
- Center server title text on server dialogs
|
||||||
- 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
|
|
||||||
|
|
||||||
### Unreleased
|
## Unreleased
|
||||||
- Load offline mods
|
- Load offline mods
|
||||||
- Local documentation
|
- Local documentation
|
||||||
- Raw debug command in context menu
|
- Raw debug command in context menu
|
||||||
|
|
||||||
### Developer-facing
|
## Developer-facing
|
||||||
- Add pyproject.toml file
|
- Add pyproject.toml file
|
||||||
- Show deprecation warnings
|
- Show deprecation warnings
|
||||||
- Options -> Dev page
|
- Options -> Dev page
|
||||||
|
|
||||||
## [6.0.5] 2026-05-17
|
## [6.0.5] 2026-05-17
|
||||||
### Changed
|
## Changed
|
||||||
- Update symlink hash logic to align with DZGUI 7 beta
|
- Update symlink hash logic to align with DZGUI 7 beta
|
||||||
- Update A2S helper to latest commit
|
- Update A2S helper to latest commit
|
||||||
|
|
||||||
## [6.0.4] 2026-04-13
|
## [6.0.4] 2026-04-13
|
||||||
### Fixed
|
## Fixed
|
||||||
- Update A2S module checksums to support servers with missing description field
|
- Update A2S module checksums to support servers with missing description field
|
||||||
|
|
||||||
## [6.0.3] 2026-04-12
|
## [6.0.3] 2026-04-12
|
||||||
### Fixed
|
## Fixed
|
||||||
- Use GLibUnix namespace where available when setting up SIGINT callback
|
- Use GLibUnix namespace where available when setting up SIGINT callback
|
||||||
|
|
||||||
## [6.0.2] 2026-01-26
|
## [6.0.2] 2026-01-26
|
||||||
### Fixed
|
## Fixed
|
||||||
- Explicitly use Python 3.13 when calling subprocesses
|
- Explicitly use Python 3.13 when calling subprocesses
|
||||||
- Raise error correctly from module
|
- Raise error correctly from module
|
||||||
- Do not spawn dialogs from inside of thread when fetching prereqs
|
- Do not spawn dialogs from inside of thread when fetching prereqs
|
||||||
### Changed
|
## Changed
|
||||||
- Optimize time complexity of startup checks
|
- Optimize time complexity of startup checks
|
||||||
- Optimize coordinate calculation by using local records first
|
- Optimize coordinate calculation by using local records first
|
||||||
- Rename some internal functions
|
- Rename some internal functions
|
||||||
- Wrap entire startup process in dialog
|
- Wrap entire startup process in dialog
|
||||||
|
|
||||||
## [6.0.1] 2026-01-16
|
## [6.0.1] 2026-01-16
|
||||||
### Fixed
|
## Fixed
|
||||||
- Explicitly use Python 3.13 when calling subprocesses
|
- Explicitly use Python 3.13 when calling subprocesses
|
||||||
|
|
||||||
## [6.0.0] 2026-01-14
|
## [6.0.0] 2026-01-14
|
||||||
### Added
|
## Added
|
||||||
- Support DayZ Experimental
|
- Support DayZ Experimental
|
||||||
- Ping display on server tables
|
- Ping display on server tables
|
||||||
- Save descriptive text notes on a per-server basis
|
- Save descriptive text notes on a per-server basis
|
||||||
@@ -142,7 +136,7 @@
|
|||||||
- Commandline usage help text (GaryBlackbourne)
|
- Commandline usage help text (GaryBlackbourne)
|
||||||
- More descriptive help dialogs when initial dependencies are missing
|
- More descriptive help dialogs when initial dependencies are missing
|
||||||
|
|
||||||
### Fixed
|
## Fixed
|
||||||
- Script failing to start when remote endpoints are unavailable (GaryBlackbourne)
|
- Script failing to start when remote endpoints are unavailable (GaryBlackbourne)
|
||||||
- Key stickiness when quickly navigating through entries in tables
|
- Key stickiness when quickly navigating through entries in tables
|
||||||
- Servers returning malformed A2S_INFO blocking server browser from loading
|
- Servers returning malformed A2S_INFO blocking server browser from loading
|
||||||
@@ -181,7 +175,7 @@
|
|||||||
- Path discovery during first-time setup when parsing filepaths with whitespaces
|
- Path discovery during first-time setup when parsing filepaths with whitespaces
|
||||||
- First-time setup dialog continuously triggering when DayZ install path had whitespaces in it
|
- First-time setup dialog continuously triggering when DayZ install path had whitespaces in it
|
||||||
|
|
||||||
### Changed
|
## Changed
|
||||||
- Require Python 3.13
|
- Require Python 3.13
|
||||||
- Reduced global API cooldown from 60s to 30s
|
- Reduced global API cooldown from 60s to 30s
|
||||||
- Clarify dialog messages when DayZ path could not be found
|
- Clarify dialog messages when DayZ path could not be found
|
||||||
@@ -193,50 +187,50 @@
|
|||||||
- Rewrote distance calculation module (GaryBlackbourne)
|
- Rewrote distance calculation module (GaryBlackbourne)
|
||||||
- Changed preferred client setting from radio toggle to combobox
|
- Changed preferred client setting from radio toggle to combobox
|
||||||
|
|
||||||
### Dropped
|
## Dropped
|
||||||
- Ping readout in statusbar
|
- Ping readout in statusbar
|
||||||
- Extraneous information from right statusbar
|
- Extraneous information from right statusbar
|
||||||
|
|
||||||
## [5.8.3] 2026-01-04
|
## [5.8.3] 2026-01-04
|
||||||
### Fixed
|
## Fixed
|
||||||
- Normalize checksum numbers and dates
|
- Normalize checksum numbers and dates
|
||||||
|
|
||||||
## [5.8.2] 2026-01-04
|
## [5.8.2] 2026-01-04
|
||||||
### Fixed
|
## Fixed
|
||||||
- Fix regression in symlink routine introduced by previous hotfix
|
- Fix regression in symlink routine introduced by previous hotfix
|
||||||
|
|
||||||
## [5.8.1] 2025-12-27
|
## [5.8.1] 2025-12-27
|
||||||
### Fixed
|
## Fixed
|
||||||
- Path discovery during first-time setup when parsing filepaths with whitespaces
|
- Path discovery during first-time setup when parsing filepaths with whitespaces
|
||||||
- First-time setup dialog continuously triggering when DayZ install path had whitespaces in it
|
- First-time setup dialog continuously triggering when DayZ install path had whitespaces in it
|
||||||
|
|
||||||
## [5.8.0] 2025-07-06
|
## [5.8.0] 2025-07-06
|
||||||
### Added
|
## Added
|
||||||
- Filter servers by official/unofficial status
|
- Filter servers by official/unofficial status
|
||||||
- Automatically fetch geolocation records
|
- Automatically fetch geolocation records
|
||||||
### Changed
|
## Changed
|
||||||
- Updated internal versioning of helper files
|
- Updated internal versioning of helper files
|
||||||
### Fixed
|
## Fixed
|
||||||
- Corrected erroneous 2024 date in prior changelog entries
|
- Corrected erroneous 2024 date in prior changelog entries
|
||||||
|
|
||||||
## [5.7.0] 2025-04-17
|
## [5.7.0] 2025-04-17
|
||||||
### Added
|
## Added
|
||||||
- Save application dimensions when quitting and restore on subsequent boot
|
- Save application dimensions when quitting and restore on subsequent boot
|
||||||
### Fixed
|
## Fixed
|
||||||
- Issues with the window exceeding the bounds of the screen when displaying some table contexts
|
- Issues with the window exceeding the bounds of the screen when displaying some table contexts
|
||||||
### Changed
|
## Changed
|
||||||
- More direct server ping query method that should return marginally more accurate times
|
- More direct server ping query method that should return marginally more accurate times
|
||||||
|
|
||||||
## [5.6.7] 2025-04-04
|
## [5.6.7] 2025-04-04
|
||||||
### Dropped
|
## Dropped
|
||||||
- Removed extraneous pre-boot API checks that could cause error messages to be printed if the user had not set up an API key yet
|
- Removed extraneous pre-boot API checks that could cause error messages to be printed if the user had not set up an API key yet
|
||||||
|
|
||||||
## [5.6.6] 2025-03-16
|
## [5.6.6] 2025-03-16
|
||||||
### Changed
|
## Changed
|
||||||
- Update IP database records for 2025-03
|
- Update IP database records for 2025-03
|
||||||
|
|
||||||
## [5.6.5] 2025-03-04
|
## [5.6.5] 2025-03-04
|
||||||
### Fixed
|
## Fixed
|
||||||
- Livonia server results being dropped from batch queries
|
- Livonia server results being dropped from batch queries
|
||||||
|
|
||||||
## [5.6.4] 2025-02-10
|
## [5.6.4] 2025-02-10
|
||||||
|
|||||||
+2
-2
@@ -8,7 +8,7 @@ If you are a developer, you can skip to the end.
|
|||||||
# Requesting help
|
# Requesting help
|
||||||
|
|
||||||
If you encounter a problem with DZGUI, you can submit tickets on the GitHub
|
If you encounter a problem with DZGUI, you can submit tickets on the GitHub
|
||||||
[issue tracker](https://github.com/aclist/dztui/issues) under the
|
(issue tracker)[https://github.com/aclist/dztui/issues] under the
|
||||||
`troubleshooting` tag.
|
`troubleshooting` tag.
|
||||||
|
|
||||||
# How can I help the project?
|
# How can I help the project?
|
||||||
@@ -24,7 +24,7 @@ accordingly.
|
|||||||
|
|
||||||
## Submitting a ticket
|
## Submitting a ticket
|
||||||
|
|
||||||
Navigate to the GitHub [issue tracker](https://github.com/aclist/dztui/issues).
|
Navigate to the GitHub (issue tracker)[https://github.com/aclist/dztui/issues].
|
||||||
From there, follow the onscreen prompts. You will be asked questions such as:
|
From there, follow the onscreen prompts. You will be asked questions such as:
|
||||||
|
|
||||||
- What version are you using?
|
- What version are you using?
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ class WorkshopACF(ACF):
|
|||||||
def __init__(self, file: Path) -> None:
|
def __init__(self, file: Path) -> None:
|
||||||
super().__init__(file)
|
super().__init__(file)
|
||||||
|
|
||||||
|
# TODO: break into workshop parser and generic acf parser
|
||||||
def unpack(self, d: dict, lines: list[Any] = []) -> str:
|
def unpack(self, d: dict, lines: list[Any] = []) -> str:
|
||||||
t1 = "AppWorkshop"
|
t1 = "AppWorkshop"
|
||||||
t2 = ("WorkshopItemsInstalled", "WorkshopItemDetails")
|
t2 = ("WorkshopItemsInstalled", "WorkshopItemDetails")
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import logging
|
||||||
|
import requests
|
||||||
|
from typing import Any, Optional, TYPE_CHECKING
|
||||||
|
|
||||||
|
from dzgui.const.constants import APP_NAME
|
||||||
|
from dzgui.const.endpoints import BM_SERVERS
|
||||||
|
|
||||||
|
logger = logging.getLogger(APP_NAME)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from dzgui.api.servers import Record
|
||||||
|
|
||||||
|
|
||||||
|
def get_attributes(key: str, uid: int) -> Any:
|
||||||
|
# TODO: handle if key is not set
|
||||||
|
# TODO: tests for malformed IDs/values
|
||||||
|
|
||||||
|
hdr = {"Authorization": "Bearer " + key}
|
||||||
|
payload: dict[str, str] = {
|
||||||
|
"filter[game]": "dayz",
|
||||||
|
"sort": "-players",
|
||||||
|
"filter[ids][whitelist]": str(uid),
|
||||||
|
}
|
||||||
|
res = requests.get(BM_SERVERS, params=payload, headers=hdr)
|
||||||
|
res.raise_for_status()
|
||||||
|
j = res.json()["data"][0]["attributes"]
|
||||||
|
return j
|
||||||
|
|
||||||
|
|
||||||
|
def map_id_to_record(key: str, uid: int) -> Optional["Record"]:
|
||||||
|
from dzgui.api.servers import Record
|
||||||
|
|
||||||
|
try:
|
||||||
|
record = get_attributes(key, uid)
|
||||||
|
ip = record["ip"]
|
||||||
|
port = int(record["port"])
|
||||||
|
qport = int(record["portQuery"])
|
||||||
|
return Record(ip, port, qport)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
@@ -41,6 +41,21 @@ def test_ipdb() -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def test_bm_api(key: str) -> bool:
|
||||||
|
payload: dict[str, str] = {
|
||||||
|
"filter[game]": "dayz",
|
||||||
|
}
|
||||||
|
hdr = {"Authorization": "Bearer " + key}
|
||||||
|
try:
|
||||||
|
res = requests.get(
|
||||||
|
endpoints.BM_SERVERS, params=payload, headers=hdr, timeout=REQUEST_TIMEOUT
|
||||||
|
)
|
||||||
|
return is_remote_up(res)
|
||||||
|
except Exception as e:
|
||||||
|
logger.critical(e)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def is_remote_up(res: "Response") -> bool:
|
def is_remote_up(res: "Response") -> bool:
|
||||||
if res.status_code == 200:
|
if res.status_code == 200:
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import threading
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any, Optional, TYPE_CHECKING, Union
|
from typing import Any, Optional, TYPE_CHECKING, Union
|
||||||
|
|
||||||
|
from dzgui.api.bm import map_id_to_record
|
||||||
from dzgui.const.constants import APP_NAME, REQUEST_TIMEOUT
|
from dzgui.const.constants import APP_NAME, REQUEST_TIMEOUT
|
||||||
from dzgui.const.endpoints import STEAM_SERVERS
|
from dzgui.const.endpoints import STEAM_SERVERS
|
||||||
from dzgui.util.format import format_exception
|
from dzgui.util.format import format_exception
|
||||||
@@ -477,6 +478,16 @@ def query_by_ip(addr: str) -> A2SInfo:
|
|||||||
return query_by_record(record, update_gameport=True)
|
return query_by_record(record, update_gameport=True)
|
||||||
|
|
||||||
|
|
||||||
|
def query_by_id(server_id: int, key: str) -> A2SInfo:
|
||||||
|
"""
|
||||||
|
Used with numeric Battlemetrics IDs
|
||||||
|
"""
|
||||||
|
record = map_id_to_record(key, server_id)
|
||||||
|
if record is None:
|
||||||
|
return A2SInfo(Record("0", 0, 0), None)
|
||||||
|
return query_by_record(record)
|
||||||
|
|
||||||
|
|
||||||
def query_by_record(record: Record, update_gameport: bool = False) -> A2SInfo:
|
def query_by_record(record: Record, update_gameport: bool = False) -> A2SInfo:
|
||||||
try:
|
try:
|
||||||
info = a2s.info((record.ip, record.qport), 3.0)
|
info = a2s.info((record.ip, record.qport), 3.0)
|
||||||
|
|||||||
@@ -1,189 +0,0 @@
|
|||||||
import binascii
|
|
||||||
import ctypes
|
|
||||||
import logging
|
|
||||||
import shutil
|
|
||||||
import vdf # type: ignore
|
|
||||||
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from importlib import resources
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from dzgui.api.steam import find_user_id_32
|
|
||||||
from dzgui.const.constants import APP_NAME, APP_NAME_LOWER, IMAGES_PATH
|
|
||||||
from dzgui.util.dirs import copy_dzgui_to_xdg_data
|
|
||||||
from dzgui.util.strings import unknown
|
|
||||||
|
|
||||||
logger = logging.getLogger(APP_NAME)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True, frozen=True)
|
|
||||||
class ShortcutMetadata:
|
|
||||||
appname: str
|
|
||||||
appid: int
|
|
||||||
exe_path: str
|
|
||||||
start_dir: str
|
|
||||||
icon: str
|
|
||||||
|
|
||||||
|
|
||||||
class Shortcuts:
|
|
||||||
def __init__(self, steam_path: Path) -> None:
|
|
||||||
self.user_config_path: Path
|
|
||||||
self.shortcuts_path = self.find_shortcuts_path(steam_path)
|
|
||||||
self._load_shortcuts(self.shortcuts_path)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def gen_bpid(cls, uid: str) -> int:
|
|
||||||
u = uid.encode()
|
|
||||||
encoded = binascii.crc32(u)
|
|
||||||
return encoded | 0x80000000
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def gen_signed_appid(cls, appid: int) -> int:
|
|
||||||
return ctypes.c_int(appid).value
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def gen_exe_uid(cls, appname: str, exe: Path) -> str:
|
|
||||||
wrapped_exe = f'"{exe}"'
|
|
||||||
return appname + wrapped_exe
|
|
||||||
|
|
||||||
def find_appname_by_unsigned_id(self, appid: int) -> str:
|
|
||||||
# NOTE: bitmask signed int back to 32-bit CRC
|
|
||||||
# varying client versions treat case sensitivity differently
|
|
||||||
try:
|
|
||||||
for key in self.shortcuts["shortcuts"].keys():
|
|
||||||
if self.shortcuts["shortcuts"][key]["appid"] & 0xFFFFFFFF == appid:
|
|
||||||
try:
|
|
||||||
return str(self.shortcuts["shortcuts"][key]["appname"])
|
|
||||||
except Exception as e:
|
|
||||||
logger.debug(e)
|
|
||||||
try:
|
|
||||||
return str(self.shortcuts["shortcuts"][key]["AppName"])
|
|
||||||
except Exception as e:
|
|
||||||
logger.debug(e)
|
|
||||||
return unknown
|
|
||||||
except Exception as e:
|
|
||||||
logger.debug(e)
|
|
||||||
return unknown
|
|
||||||
|
|
||||||
def get_shortcuts(self) -> Any:
|
|
||||||
return self.shortcuts
|
|
||||||
|
|
||||||
def _load_shortcuts(self, path: Path) -> None:
|
|
||||||
try:
|
|
||||||
with open(path, "rb") as f:
|
|
||||||
shortcuts = vdf.binary_load(f)
|
|
||||||
self.shortcuts = shortcuts
|
|
||||||
logger.debug("Loaded shortcuts file")
|
|
||||||
except Exception as e:
|
|
||||||
logger.critical(e)
|
|
||||||
raise e
|
|
||||||
|
|
||||||
def find_shortcuts_path(self, steam_path: Path) -> Path:
|
|
||||||
uid = find_user_id_32(steam_path)
|
|
||||||
self.user_config_path = steam_path.joinpath(f"userdata/{uid}/config")
|
|
||||||
return self.user_config_path.joinpath("shortcuts.vdf")
|
|
||||||
|
|
||||||
def add_shortcut(
|
|
||||||
self, appname: str, start_dir: Path, exe_path: Path, icon: Path
|
|
||||||
) -> None:
|
|
||||||
uid = self.gen_exe_uid(appname, exe_path)
|
|
||||||
self.bpid = self.gen_bpid(uid)
|
|
||||||
self.signed_appid = self.gen_signed_appid(self.bpid)
|
|
||||||
|
|
||||||
meta = ShortcutMetadata(
|
|
||||||
appname, self.signed_appid, str(exe_path), str(start_dir), str(icon)
|
|
||||||
)
|
|
||||||
entry = self._create_entry(meta)
|
|
||||||
self._insert_at_last_index(entry)
|
|
||||||
|
|
||||||
def add_grid_images(self, images: Path) -> None:
|
|
||||||
"""
|
|
||||||
{BPID}_hero.png: hero (splash)
|
|
||||||
{BPID}p.ping: portrait boxart in library
|
|
||||||
{BPID}_logo.png: logo overlay on top of hero
|
|
||||||
{BPID}.png: legacy Big Picture/landscape image
|
|
||||||
"""
|
|
||||||
|
|
||||||
grid_path = self.user_config_path.joinpath("grid")
|
|
||||||
for img in ("_hero", "p", "_logo"):
|
|
||||||
filename = str(self.bpid) + img
|
|
||||||
dest = grid_path.joinpath(filename).with_suffix(".png")
|
|
||||||
b = images.joinpath(img).with_suffix(".png").read_bytes()
|
|
||||||
dest.write_bytes(b)
|
|
||||||
|
|
||||||
# NOTE: legacy Big Picture header
|
|
||||||
b = images.joinpath("_hero.png").read_bytes()
|
|
||||||
dest = grid_path.joinpath(str(self.bpid)).with_suffix(".png")
|
|
||||||
dest.write_bytes(b)
|
|
||||||
|
|
||||||
def _insert_at_last_index(self, entry: dict[str, Any]) -> None:
|
|
||||||
try:
|
|
||||||
last = list(self.shortcuts["shortcuts"].keys())[-1]
|
|
||||||
n = int(last) + 1
|
|
||||||
except Exception:
|
|
||||||
n = 0
|
|
||||||
self.shortcuts["shortcuts"][str(n)] = entry
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _create_entry(cls, meta: ShortcutMetadata) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
https://developer.valvesoftware.com/wiki/Add_Non-Steam_Game
|
|
||||||
https://developer.valvesoftware.com/wiki/Steam_Library_Shortcuts
|
|
||||||
|
|
||||||
Key case is not internally consistent and varies between client versions
|
|
||||||
Most keys use Pascal case, but some do not
|
|
||||||
|
|
||||||
appid: signed int CRC
|
|
||||||
Exe: absolute path to the executable, must be wrapped in literal quotes
|
|
||||||
StartDir: directory the executable starts in, generally the parent
|
|
||||||
"""
|
|
||||||
|
|
||||||
NEW_ENTRY: dict[str, Any] = {}
|
|
||||||
NEW_ENTRY["appid"] = meta.appid
|
|
||||||
NEW_ENTRY["AppName"] = meta.appname
|
|
||||||
NEW_ENTRY["Exe"] = f'"{meta.exe_path}"'
|
|
||||||
NEW_ENTRY["StartDir"] = f"{meta.start_dir}"
|
|
||||||
NEW_ENTRY["icon"] = meta.icon
|
|
||||||
NEW_ENTRY["ShortcutPath"] = ""
|
|
||||||
NEW_ENTRY["LaunchOptions"] = ""
|
|
||||||
NEW_ENTRY["IsHidden"] = 0
|
|
||||||
NEW_ENTRY["AllowDesktopConfig"] = 1
|
|
||||||
NEW_ENTRY["AllowOverlay"] = 1
|
|
||||||
NEW_ENTRY["OpenVR"] = 0
|
|
||||||
NEW_ENTRY["Devkit"] = 0
|
|
||||||
NEW_ENTRY["DevkitGameID"] = ""
|
|
||||||
NEW_ENTRY["DevkitOverrideAppID"] = 0
|
|
||||||
NEW_ENTRY["LastPlayTime"] = 0
|
|
||||||
NEW_ENTRY["FlatpakAppID"] = ""
|
|
||||||
NEW_ENTRY["sortas"] = ""
|
|
||||||
NEW_ENTRY["tags"] = {}
|
|
||||||
return NEW_ENTRY
|
|
||||||
|
|
||||||
def save_shortcuts(self) -> None:
|
|
||||||
try:
|
|
||||||
backup = self.shortcuts_path.with_suffix(".vdf.bak")
|
|
||||||
shutil.copy(self.shortcuts_path, backup)
|
|
||||||
with open(self.shortcuts_path, "wb") as f:
|
|
||||||
vdf.binary_dump(self.shortcuts, f)
|
|
||||||
except Exception as e:
|
|
||||||
logger.critical(e)
|
|
||||||
|
|
||||||
|
|
||||||
def add_steam_shortcut(steam_path: Path, exe_path: Path) -> None:
|
|
||||||
try:
|
|
||||||
start_dir = exe_path.parent
|
|
||||||
|
|
||||||
traversable = resources.files(APP_NAME_LOWER).joinpath(IMAGES_PATH)
|
|
||||||
images = Path(str(traversable))
|
|
||||||
icon = images.joinpath("icon.png")
|
|
||||||
|
|
||||||
shortcuts = Shortcuts(Path(steam_path))
|
|
||||||
shortcuts.add_shortcut(APP_NAME, start_dir, exe_path, icon)
|
|
||||||
shortcuts.save_shortcuts()
|
|
||||||
shortcuts.add_grid_images(images)
|
|
||||||
|
|
||||||
copy_dzgui_to_xdg_data(exe_path)
|
|
||||||
logger.debug("Finished creating Steam shortcut")
|
|
||||||
except Exception as e:
|
|
||||||
logger.critical(e)
|
|
||||||
+40
-56
@@ -15,7 +15,6 @@ from dzgui.const.constants import (
|
|||||||
APPID_DAYZ,
|
APPID_DAYZ,
|
||||||
APPID_DAYZ_EXP,
|
APPID_DAYZ_EXP,
|
||||||
APP_NAME,
|
APP_NAME,
|
||||||
DAYZ_BINARY,
|
|
||||||
DEBIAN_STEAM_PATH,
|
DEBIAN_STEAM_PATH,
|
||||||
DEFAULT_STEAM_PATH,
|
DEFAULT_STEAM_PATH,
|
||||||
FLATPAK_STEAM_PATH,
|
FLATPAK_STEAM_PATH,
|
||||||
@@ -32,6 +31,7 @@ from dzgui.const.endpoints import (
|
|||||||
)
|
)
|
||||||
from dzgui.strings import wizard
|
from dzgui.strings import wizard
|
||||||
from dzgui.util.bash import concat_bash_args
|
from dzgui.util.bash import concat_bash_args
|
||||||
|
from dzgui.util.strings import unknown
|
||||||
|
|
||||||
logger = logging.getLogger(APP_NAME)
|
logger = logging.getLogger(APP_NAME)
|
||||||
|
|
||||||
@@ -205,35 +205,21 @@ def launch_offline(
|
|||||||
proc = subprocess.run([*client_args, *params])
|
proc = subprocess.run([*client_args, *params])
|
||||||
return proc.returncode
|
return proc.returncode
|
||||||
|
|
||||||
def find_loginusers(path: Path) -> Path:
|
|
||||||
return path / "config" / "loginusers.vdf"
|
|
||||||
|
|
||||||
def find_user_id(path: Path) -> str | None:
|
def find_user_id(path: Path) -> str | None:
|
||||||
resolved_path = find_loginusers(path)
|
resolved_path = path / "config" / "loginusers.vdf"
|
||||||
try:
|
try:
|
||||||
with open(resolved_path, "r") as f:
|
with open(resolved_path, "r") as f:
|
||||||
v = vdf.load(f)
|
v = vdf.load(f)
|
||||||
# NOTE: beta client
|
|
||||||
# /package/beta
|
|
||||||
if len(v["users"]) == 1:
|
|
||||||
return str(list(v["users"].keys())[0])
|
|
||||||
for user in v["users"]:
|
for user in v["users"]:
|
||||||
if v["users"][user]["MostRecent"] == "1":
|
if v["users"][user]["MostRecent"] == "1":
|
||||||
return str(user)
|
return str(user)
|
||||||
return None
|
return None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(e)
|
logger.warn(e)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def find_user_id_32(path: Path) -> int:
|
|
||||||
uid = find_user_id(path)
|
|
||||||
if uid is None:
|
|
||||||
raise ValueError("Failed to parse a valid Steam32 ID")
|
|
||||||
return int(uid) & 0xFFFFFFFF
|
|
||||||
|
|
||||||
|
|
||||||
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,
|
||||||
@@ -257,6 +243,20 @@ def unsubscribe(key: str, mod: int) -> None:
|
|||||||
update_workshop(key, mod, UNSUB_ENDPOINT)
|
update_workshop(key, mod, UNSUB_ENDPOINT)
|
||||||
|
|
||||||
|
|
||||||
|
def gen_shortcut() -> None:
|
||||||
|
# TODO:
|
||||||
|
"""
|
||||||
|
during setup, prompt user to select matching user id from loginusers
|
||||||
|
show user account name, select outer steam id
|
||||||
|
steam/userdata/<steamid_32>/config/shortcuts.vdf
|
||||||
|
"""
|
||||||
|
# STEAMID_MAGIC = 76561197960265728
|
||||||
|
# STEAMID_64 - STEAMID_MAGIC = STEAMID32
|
||||||
|
# or get right-most 32 bits
|
||||||
|
# STEAMID_64 & 0xFFFFFFFF
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
@deprecated("Use subscribe()")
|
@deprecated("Use subscribe()")
|
||||||
def enqueue_mod(client: str, mod: str, appid: int) -> None:
|
def enqueue_mod(client: str, mod: str, appid: int) -> None:
|
||||||
client_args = concat_bash_args(client)
|
client_args = concat_bash_args(client)
|
||||||
@@ -309,34 +309,28 @@ def get_running_app() -> int | None:
|
|||||||
PROC_NAME = "steam"
|
PROC_NAME = "steam"
|
||||||
SUBPROC_NAME = "reaper"
|
SUBPROC_NAME = "reaper"
|
||||||
FLAG = "AppId"
|
FLAG = "AppId"
|
||||||
# NOTE: may cause conflicts if multiple apps are running
|
|
||||||
try:
|
for proc in psutil.process_iter():
|
||||||
for proc in psutil.process_iter():
|
if proc.name() == PROC_NAME:
|
||||||
if proc.name() == PROC_NAME:
|
subprocs = proc.children()
|
||||||
subprocs = proc.children()
|
filtered = (proc for proc in subprocs if proc.name() == SUBPROC_NAME)
|
||||||
filtered = (proc for proc in subprocs if proc.name() == SUBPROC_NAME)
|
try:
|
||||||
try:
|
proc = next(filtered)
|
||||||
proc = next(filtered)
|
except StopIteration:
|
||||||
except StopIteration:
|
return None
|
||||||
return None
|
args = proc.cmdline()
|
||||||
args = proc.cmdline()
|
appid = (row for row in args if FLAG in row)
|
||||||
appid = (row for row in args if FLAG in row)
|
try:
|
||||||
try:
|
return int(next(appid).split("=")[1])
|
||||||
return int(next(appid).split("=")[1])
|
except StopIteration:
|
||||||
except StopIteration:
|
return None
|
||||||
return None
|
|
||||||
except Exception as e:
|
|
||||||
logger.debug(e)
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def get_app_allows_downloads(path: Path, appid: int) -> bool:
|
def get_app_allows_downloads(path: Path, appid: int) -> bool:
|
||||||
try:
|
root_path = get_app_path(path, appid)
|
||||||
root_path = get_app_path(path, appid)
|
acf = root_path.joinpath(f"steamapps/appmanifest_{appid}.acf")
|
||||||
acf = root_path.joinpath(f"steamapps/appmanifest_{appid}.acf")
|
flag = ACF(acf).get_allows_downloads()
|
||||||
flag = ACF(acf).get_allows_downloads()
|
|
||||||
except AppNotInstalledError:
|
|
||||||
flag = 0
|
|
||||||
match flag:
|
match flag:
|
||||||
# NOTE: adheres to global client setting
|
# NOTE: adheres to global client setting
|
||||||
case 0:
|
case 0:
|
||||||
@@ -351,11 +345,9 @@ def get_app_allows_downloads(path: Path, appid: int) -> bool:
|
|||||||
case _:
|
case _:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def get_config(path: Path) -> Path:
|
def get_config(path: Path) -> Path:
|
||||||
return path.joinpath("config/config.vdf")
|
return path.joinpath("config/config.vdf")
|
||||||
|
|
||||||
|
|
||||||
def get_client_allows_downloads(path: Path) -> bool:
|
def get_client_allows_downloads(path: Path) -> bool:
|
||||||
config = get_config(path)
|
config = get_config(path)
|
||||||
try:
|
try:
|
||||||
@@ -376,16 +368,8 @@ def get_client_allows_downloads(path: Path) -> bool:
|
|||||||
|
|
||||||
|
|
||||||
def is_dayz_running() -> bool:
|
def is_dayz_running() -> bool:
|
||||||
"""Subprocesses spawned from Steam will not show up in regular process tree"""
|
appid = get_running_app()
|
||||||
procs = []
|
return appid in (APPID_DAYZ, APPID_DAYZ_EXP)
|
||||||
substring = DAYZ_BINARY
|
|
||||||
for proc in psutil.process_iter():
|
|
||||||
try:
|
|
||||||
procs.append(proc.cmdline())
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(e)
|
|
||||||
continue
|
|
||||||
return any(substring in item for sublist in procs for item in sublist)
|
|
||||||
|
|
||||||
|
|
||||||
def get_app_path(folders_path: Path, appid: int) -> Path:
|
def get_app_path(folders_path: Path, appid: int) -> Path:
|
||||||
@@ -415,13 +399,13 @@ def get_app_path(folders_path: Path, appid: int) -> Path:
|
|||||||
return Path(app_path)
|
return Path(app_path)
|
||||||
|
|
||||||
|
|
||||||
def get_app_name(appid: int) -> str | None:
|
def get_app_name(appid: int) -> str:
|
||||||
payload = {"appids": [appid]}
|
payload = {"appids": [appid]}
|
||||||
res = requests.get(APP_DETAILS, params=payload)
|
res = requests.get(APP_DETAILS, params=payload)
|
||||||
if res.status_code != 200:
|
if res.status_code != 200:
|
||||||
return None
|
return unknown
|
||||||
try:
|
try:
|
||||||
return str(res.json()[str(appid)]["data"]["name"])
|
return str(res.json()[str(appid)]["data"]["name"])
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(e)
|
logger.debug(e)
|
||||||
return None
|
return unknown
|
||||||
+7
-4
@@ -20,7 +20,6 @@ from dzgui.strings import boot
|
|||||||
|
|
||||||
# from dzgui.util.map_count import get_map_count
|
# from dzgui.util.map_count import get_map_count
|
||||||
from dzgui.util.deck import is_steam_deck, is_game_mode
|
from dzgui.util.deck import is_steam_deck, is_game_mode
|
||||||
from dzgui.util.dirs import make_parents
|
|
||||||
from dzgui.util.localize import set_locale
|
from dzgui.util.localize import set_locale
|
||||||
from dzgui.util.redact import RedactionFilter, REDACTION_PATTERNS
|
from dzgui.util.redact import RedactionFilter, REDACTION_PATTERNS
|
||||||
from dzgui.util.strings import init
|
from dzgui.util.strings import init
|
||||||
@@ -35,7 +34,10 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
logger = logging.getLogger(APP_NAME)
|
logger = logging.getLogger(APP_NAME)
|
||||||
|
|
||||||
|
|
||||||
# TODO: profile load time
|
# TODO: profile load time
|
||||||
|
def make_parents(path: "Path") -> None:
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
def setup_logger(log_path: "Path") -> None:
|
def setup_logger(log_path: "Path") -> None:
|
||||||
@@ -122,10 +124,11 @@ def load_gui(version: str, is_debug: bool) -> None:
|
|||||||
migrate_cols_file(XDG.columns)
|
migrate_cols_file(XDG.columns)
|
||||||
copy_state_files(xdg_paths["XDG_STATE_HOME"])
|
copy_state_files(xdg_paths["XDG_STATE_HOME"])
|
||||||
# TODO: add logging inside wizard
|
# TODO: add logging inside wizard
|
||||||
wizard = SetupWizard(_is_steam_deck, XDG)
|
SetupWizard(_is_steam_deck, XDG.config)
|
||||||
|
|
||||||
if not wizard.is_setup_complete():
|
# NOTE: implies that setup wizard failed or was closed
|
||||||
return
|
if has_new_config(XDG.config) is False:
|
||||||
|
return
|
||||||
|
|
||||||
setup_logger(XDG.debug)
|
setup_logger(XDG.debug)
|
||||||
with open(XDG.debug, "w") as f:
|
with open(XDG.debug, "w") as f:
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ def rc2json(file: Path) -> str:
|
|||||||
|
|
||||||
toggles = ["fullscreen"]
|
toggles = ["fullscreen"]
|
||||||
deprecated = [
|
deprecated = [
|
||||||
"api_key",
|
|
||||||
"staging_dir",
|
"staging_dir",
|
||||||
"src_path",
|
"src_path",
|
||||||
"steam_path",
|
"steam_path",
|
||||||
@@ -67,6 +66,8 @@ def rc2json(file: Path) -> str:
|
|||||||
value = str2bool(ntok)
|
value = str2bool(ntok)
|
||||||
elif tok == "preferred_client":
|
elif tok == "preferred_client":
|
||||||
tok = "client"
|
tok = "client"
|
||||||
|
elif tok == "api_key":
|
||||||
|
tok = "bm_api"
|
||||||
elif tok == "ip_list":
|
elif tok == "ip_list":
|
||||||
while True:
|
while True:
|
||||||
ntok = lex.get_token()
|
ntok = lex.get_token()
|
||||||
|
|||||||
@@ -1,43 +1,18 @@
|
|||||||
import stat
|
|
||||||
import textwrap
|
import textwrap
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from dzgui.util.dirs import copy_dzgui_to_xdg_data, find_icon_resource, make_parents
|
|
||||||
|
|
||||||
|
|
||||||
def get_share_path(exe_path: Path) -> Path:
|
|
||||||
return exe_path.parent.parent
|
|
||||||
|
|
||||||
|
|
||||||
def write_desktop_file(exe_path: Path) -> Path:
|
|
||||||
icon = find_icon_resource()
|
|
||||||
|
|
||||||
|
def write_desktop_file(share_path: Path) -> None:
|
||||||
template = f"""\
|
template = f"""\
|
||||||
[Desktop Entry]
|
[Desktop Entry]
|
||||||
Version=1.0
|
Version=1.0
|
||||||
Type=Application
|
Type=Application
|
||||||
Terminal=false
|
Terminal=false
|
||||||
Exec={exe_path}
|
Exec={share_path}/launch.sh
|
||||||
Name=DZGUI
|
Name=DZGUI
|
||||||
Comment=DayZ server browser and mod manager
|
Comment=dzgui
|
||||||
Icon={icon}
|
Icon={share_path}/dzgui
|
||||||
Categories=Game"""
|
Categories=Game"""
|
||||||
|
|
||||||
copy_dzgui_to_xdg_data(exe_path)
|
file = share_path / "dzgui.desktop"
|
||||||
|
file.write_text(textwrap.dedent(template))
|
||||||
share_path = get_share_path(exe_path)
|
|
||||||
desktop_file = share_path.joinpath("applications/dzgui.desktop")
|
|
||||||
make_parents(desktop_file)
|
|
||||||
desktop_file.write_text(textwrap.dedent(template))
|
|
||||||
desktop_file.chmod(desktop_file.stat().st_mode | stat.S_IEXEC)
|
|
||||||
return desktop_file
|
|
||||||
|
|
||||||
|
|
||||||
def write_desktop_shortcut(desktop_file: Path) -> None:
|
|
||||||
# NOTE: necessarily depends on the above (UI blocks creation without XDG entry first)
|
|
||||||
link = Path.home().joinpath("Desktop/dzgui.desktop")
|
|
||||||
make_parents(link)
|
|
||||||
if link.exists():
|
|
||||||
link.unlink()
|
|
||||||
link.symlink_to(desktop_file)
|
|
||||||
+1
-6
@@ -18,7 +18,6 @@ class Xdg:
|
|||||||
debug: Path
|
debug: Path
|
||||||
ips: Path
|
ips: Path
|
||||||
filters: Path
|
filters: Path
|
||||||
shortcut: Path
|
|
||||||
|
|
||||||
|
|
||||||
def is_writeable(path_str: str) -> bool:
|
def is_writeable(path_str: str) -> bool:
|
||||||
@@ -68,7 +67,6 @@ def get_xdg_paths() -> dict:
|
|||||||
def parse_filepaths(xdg: dict) -> Xdg:
|
def parse_filepaths(xdg: dict) -> Xdg:
|
||||||
config = xdg["XDG_CONFIG_HOME"]
|
config = xdg["XDG_CONFIG_HOME"]
|
||||||
state = xdg["XDG_STATE_HOME"]
|
state = xdg["XDG_STATE_HOME"]
|
||||||
share = xdg["XDG_DATA_HOME"]
|
|
||||||
|
|
||||||
config = config / "config.json"
|
config = config / "config.json"
|
||||||
|
|
||||||
@@ -83,8 +81,6 @@ def parse_filepaths(xdg: dict) -> Xdg:
|
|||||||
ips = state / "ips.csv"
|
ips = state / "ips.csv"
|
||||||
filters = state / "dzg.filters.json"
|
filters = state / "dzg.filters.json"
|
||||||
|
|
||||||
shortcut = share / APP_NAME_LOWER
|
|
||||||
|
|
||||||
return Xdg(
|
return Xdg(
|
||||||
config,
|
config,
|
||||||
columns,
|
columns,
|
||||||
@@ -95,6 +91,5 @@ def parse_filepaths(xdg: dict) -> Xdg:
|
|||||||
system,
|
system,
|
||||||
debug,
|
debug,
|
||||||
ips,
|
ips,
|
||||||
filters,
|
filters
|
||||||
shortcut,
|
|
||||||
)
|
)
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
Generic defaults used when initializing a config file from scratch
|
Generic defaults used when initializing a config file from scratch
|
||||||
"""
|
"""
|
||||||
config_boilerplate = {
|
config_boilerplate = {
|
||||||
|
"bm_api": "",
|
||||||
"fav_server": "",
|
"fav_server": "",
|
||||||
"fav_label": "",
|
"fav_label": "",
|
||||||
"name": "",
|
"name": "",
|
||||||
|
|||||||
@@ -72,9 +72,7 @@ LEGACY_IPS_PATH = ".local/share/dzgui/helpers/ips.csv"
|
|||||||
DEBUG_LOG = f"{APP_NAME}_DEBUG.LOG"
|
DEBUG_LOG = f"{APP_NAME}_DEBUG.LOG"
|
||||||
SYSTEM_LOG = f"{APP_NAME}_SYSTEM.LOG"
|
SYSTEM_LOG = f"{APP_NAME}_SYSTEM.LOG"
|
||||||
CHANGELOG_PATH = "data/CHANGELOG.md"
|
CHANGELOG_PATH = "data/CHANGELOG.md"
|
||||||
IMAGES_PATH = "data/images"
|
HERO_PATH = "data/images/hero.png"
|
||||||
|
|
||||||
HERO_PATH = "data/images/_hero.png"
|
|
||||||
CSS_PATH = "data/app.css"
|
CSS_PATH = "data/app.css"
|
||||||
VDF_PATH = "steamapps/libraryfolders.vdf"
|
VDF_PATH = "steamapps/libraryfolders.vdf"
|
||||||
DEFAULT_STEAM_PATH = ".local/share/Steam"
|
DEFAULT_STEAM_PATH = ".local/share/Steam"
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ SUB_ENDPOINT = "https://api.steampowered.com/IPublishedFileService/Subscribe/v1"
|
|||||||
UNSUB_ENDPOINT = "https://api.steampowered.com/IPublishedFileService/Unsubscribe/v1"
|
UNSUB_ENDPOINT = "https://api.steampowered.com/IPublishedFileService/Unsubscribe/v1"
|
||||||
APP_DETAILS = "https://store.steampowered.com/api/appdetails?"
|
APP_DETAILS = "https://store.steampowered.com/api/appdetails?"
|
||||||
|
|
||||||
|
BM_SERVERS = "https://api.battlemetrics.com/servers?"
|
||||||
GITHUB = "https://github.com/aclist"
|
GITHUB = "https://github.com/aclist"
|
||||||
GITHUB_RELEASES = "https://api.github.com/repos/aclist/dztui/releases/latest"
|
GITHUB_RELEASES = "https://api.github.com/repos/aclist/dztui/releases/latest"
|
||||||
CODEBERG_RELEASES = "https://codeberg.org/api/v1/repos/aclist/dzgui/releases/latest"
|
CODEBERG_RELEASES = "https://codeberg.org/api/v1/repos/aclist/dzgui/releases/latest"
|
||||||
@@ -26,3 +27,5 @@ GITHUB_ISSUES = "https://github.com/aclist/dzgui/issues"
|
|||||||
FORUM = "https://old.reddit.com/r/dzgui"
|
FORUM = "https://old.reddit.com/r/dzgui"
|
||||||
SPONSORS = "https://github.com/sponsors/aclist"
|
SPONSORS = "https://github.com/sponsors/aclist"
|
||||||
STEAM_API_SETUP = "https://steamcommunity.com/dev/apikey"
|
STEAM_API_SETUP = "https://steamcommunity.com/dev/apikey"
|
||||||
|
BM_API_SETUP = "https://www.battlemetrics.com/developers"
|
||||||
|
BM_BROWSE = "https://www.battlemetrics.com/servers/dayz"
|
||||||
@@ -61,6 +61,9 @@ class Preferences(EnumWithAttrs):
|
|||||||
STEAM = {
|
STEAM = {
|
||||||
"key": "steam_api",
|
"key": "steam_api",
|
||||||
}
|
}
|
||||||
|
BM = {
|
||||||
|
"key": "bm_api",
|
||||||
|
}
|
||||||
CLIENT = {
|
CLIENT = {
|
||||||
"key": "client",
|
"key": "client",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -413,7 +413,7 @@ class Controller(GObject.GObject):
|
|||||||
|
|
||||||
def add_by_str(self, addr: str) -> None:
|
def add_by_str(self, addr: str) -> None:
|
||||||
saved_tree = self.get_servers().get_saved()
|
saved_tree = self.get_servers().get_saved()
|
||||||
ServerModelManager(self, saved_tree).add_by_ip(addr)
|
ServerModelManager(self, saved_tree).add_by_str(addr)
|
||||||
|
|
||||||
def add_by_record(self, record: "Record") -> None:
|
def add_by_record(self, record: "Record") -> None:
|
||||||
saved_tree = self.get_servers().get_saved()
|
saved_tree = self.get_servers().get_saved()
|
||||||
@@ -437,8 +437,14 @@ class Controller(GObject.GObject):
|
|||||||
ServerModelManager(self, tv).remove_from_history(record)
|
ServerModelManager(self, tv).remove_from_history(record)
|
||||||
|
|
||||||
def connect_by_str(self, addr: str) -> None:
|
def connect_by_str(self, addr: str) -> None:
|
||||||
self.connection_man = ConnectionManager(self)
|
if addr.isdigit():
|
||||||
self.connection_man.connect_by_ip(addr)
|
config_man = self.get_config_man()
|
||||||
|
key = config_man.lookup(Preferences.BM)
|
||||||
|
self.connection_man = ConnectionManager(self)
|
||||||
|
self.connection_man.connect_by_id(int(addr), key)
|
||||||
|
else:
|
||||||
|
self.connection_man = ConnectionManager(self)
|
||||||
|
self.connection_man.connect_by_ip(addr)
|
||||||
|
|
||||||
def connect_by_record(self, record: "Record") -> None:
|
def connect_by_record(self, record: "Record") -> None:
|
||||||
self.connection_man = ConnectionManager(self)
|
self.connection_man = ConnectionManager(self)
|
||||||
|
|||||||
+28
-31
@@ -1,7 +1,6 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## [7.0.0] Unreleased
|
## Added
|
||||||
### Added
|
|
||||||
- Setup wizard
|
- Setup wizard
|
||||||
- Changelog text wrapping and formatting
|
- Changelog text wrapping and formatting
|
||||||
- Changelog ships with source
|
- Changelog ships with source
|
||||||
@@ -42,10 +41,8 @@
|
|||||||
- Choose to jump into splash screen instead of server
|
- Choose to jump into splash screen instead of server
|
||||||
- Collapsible connection panel
|
- Collapsible connection panel
|
||||||
- Play offline (load mods directly)
|
- Play offline (load mods directly)
|
||||||
- Generate Steam shortcuts and cover art
|
|
||||||
- Warn user if background downloads are disabled
|
|
||||||
|
|
||||||
### Changed
|
## Changed
|
||||||
- Conform to PEP 440 versioning for beta versions
|
- Conform to PEP 440 versioning for beta versions
|
||||||
- Reduce padding on keys button
|
- Reduce padding on keys button
|
||||||
- Boldface breadcrumbs
|
- Boldface breadcrumbs
|
||||||
@@ -62,58 +59,58 @@
|
|||||||
- Enable LAN page Empty/Full filters on first run of app
|
- Enable LAN page Empty/Full filters on first run of app
|
||||||
- Propagate subscribed mods to Steam client
|
- Propagate subscribed mods to Steam client
|
||||||
|
|
||||||
### Dropped
|
## Dropped
|
||||||
- Debug mode
|
- Debug mode
|
||||||
- Branch switching
|
- Branch switching
|
||||||
- Manual mod install mode (describe rationale)
|
- Manual mod install mode (describe rationale)
|
||||||
- Force update mods
|
- Force update mods
|
||||||
|
|
||||||
### Fixed
|
## Fixed
|
||||||
- Longstanding issue with left clicks not registering as tree selection changes after spamming keyboard input
|
- Longstanding issue with left clicks not registering as tree selection changes after spamming keyboard input
|
||||||
- Center server title text on server dialogs
|
- Center server title text on server dialogs
|
||||||
- Rare segfaults when changing maps (threading)
|
- Rare segfaults when changing maps (threading)
|
||||||
- Moved dialogs out of threads
|
- Moved dialogs out of threads
|
||||||
|
|
||||||
### Unreleased
|
## Unreleased
|
||||||
- Load offline mods
|
- Load offline mods
|
||||||
- Local documentation
|
- Local documentation
|
||||||
- Raw debug command in context menu
|
- Raw debug command in context menu
|
||||||
|
|
||||||
### Developer-facing
|
## Developer-facing
|
||||||
- Add pyproject.toml file
|
- Add pyproject.toml file
|
||||||
- Show deprecation warnings
|
- Show deprecation warnings
|
||||||
- Options -> Dev page
|
- Options -> Dev page
|
||||||
|
|
||||||
## [6.0.5] 2026-05-17
|
## [6.0.5] 2026-05-17
|
||||||
### Changed
|
## Changed
|
||||||
- Update symlink hash logic to align with DZGUI 7 beta
|
- Update symlink hash logic to align with DZGUI 7 beta
|
||||||
- Update A2S helper to latest commit
|
- Update A2S helper to latest commit
|
||||||
|
|
||||||
## [6.0.4] 2026-04-13
|
## [6.0.4] 2026-04-13
|
||||||
### Fixed
|
## Fixed
|
||||||
- Update A2S module checksums to support servers with missing description field
|
- Update A2S module checksums to support servers with missing description field
|
||||||
|
|
||||||
## [6.0.3] 2026-04-12
|
## [6.0.3] 2026-04-12
|
||||||
### Fixed
|
## Fixed
|
||||||
- Use GLibUnix namespace where available when setting up SIGINT callback
|
- Use GLibUnix namespace where available when setting up SIGINT callback
|
||||||
|
|
||||||
## [6.0.2] 2026-01-26
|
## [6.0.2] 2026-01-26
|
||||||
### Fixed
|
## Fixed
|
||||||
- Explicitly use Python 3.13 when calling subprocesses
|
- Explicitly use Python 3.13 when calling subprocesses
|
||||||
- Raise error correctly from module
|
- Raise error correctly from module
|
||||||
- Do not spawn dialogs from inside of thread when fetching prereqs
|
- Do not spawn dialogs from inside of thread when fetching prereqs
|
||||||
### Changed
|
## Changed
|
||||||
- Optimize time complexity of startup checks
|
- Optimize time complexity of startup checks
|
||||||
- Optimize coordinate calculation by using local records first
|
- Optimize coordinate calculation by using local records first
|
||||||
- Rename some internal functions
|
- Rename some internal functions
|
||||||
- Wrap entire startup process in dialog
|
- Wrap entire startup process in dialog
|
||||||
|
|
||||||
## [6.0.1] 2026-01-16
|
## [6.0.1] 2026-01-16
|
||||||
### Fixed
|
## Fixed
|
||||||
- Explicitly use Python 3.13 when calling subprocesses
|
- Explicitly use Python 3.13 when calling subprocesses
|
||||||
|
|
||||||
## [6.0.0] 2026-01-14
|
## [6.0.0] 2026-01-14
|
||||||
### Added
|
## Added
|
||||||
- Support DayZ Experimental
|
- Support DayZ Experimental
|
||||||
- Ping display on server tables
|
- Ping display on server tables
|
||||||
- Save descriptive text notes on a per-server basis
|
- Save descriptive text notes on a per-server basis
|
||||||
@@ -139,7 +136,7 @@
|
|||||||
- Commandline usage help text (GaryBlackbourne)
|
- Commandline usage help text (GaryBlackbourne)
|
||||||
- More descriptive help dialogs when initial dependencies are missing
|
- More descriptive help dialogs when initial dependencies are missing
|
||||||
|
|
||||||
### Fixed
|
## Fixed
|
||||||
- Script failing to start when remote endpoints are unavailable (GaryBlackbourne)
|
- Script failing to start when remote endpoints are unavailable (GaryBlackbourne)
|
||||||
- Key stickiness when quickly navigating through entries in tables
|
- Key stickiness when quickly navigating through entries in tables
|
||||||
- Servers returning malformed A2S_INFO blocking server browser from loading
|
- Servers returning malformed A2S_INFO blocking server browser from loading
|
||||||
@@ -178,7 +175,7 @@
|
|||||||
- Path discovery during first-time setup when parsing filepaths with whitespaces
|
- Path discovery during first-time setup when parsing filepaths with whitespaces
|
||||||
- First-time setup dialog continuously triggering when DayZ install path had whitespaces in it
|
- First-time setup dialog continuously triggering when DayZ install path had whitespaces in it
|
||||||
|
|
||||||
### Changed
|
## Changed
|
||||||
- Require Python 3.13
|
- Require Python 3.13
|
||||||
- Reduced global API cooldown from 60s to 30s
|
- Reduced global API cooldown from 60s to 30s
|
||||||
- Clarify dialog messages when DayZ path could not be found
|
- Clarify dialog messages when DayZ path could not be found
|
||||||
@@ -190,50 +187,50 @@
|
|||||||
- Rewrote distance calculation module (GaryBlackbourne)
|
- Rewrote distance calculation module (GaryBlackbourne)
|
||||||
- Changed preferred client setting from radio toggle to combobox
|
- Changed preferred client setting from radio toggle to combobox
|
||||||
|
|
||||||
### Dropped
|
## Dropped
|
||||||
- Ping readout in statusbar
|
- Ping readout in statusbar
|
||||||
- Extraneous information from right statusbar
|
- Extraneous information from right statusbar
|
||||||
|
|
||||||
## [5.8.3] 2026-01-04
|
## [5.8.3] 2026-01-04
|
||||||
### Fixed
|
## Fixed
|
||||||
- Normalize checksum numbers and dates
|
- Normalize checksum numbers and dates
|
||||||
|
|
||||||
## [5.8.2] 2026-01-04
|
## [5.8.2] 2026-01-04
|
||||||
### Fixed
|
## Fixed
|
||||||
- Fix regression in symlink routine introduced by previous hotfix
|
- Fix regression in symlink routine introduced by previous hotfix
|
||||||
|
|
||||||
## [5.8.1] 2025-12-27
|
## [5.8.1] 2025-12-27
|
||||||
### Fixed
|
## Fixed
|
||||||
- Path discovery during first-time setup when parsing filepaths with whitespaces
|
- Path discovery during first-time setup when parsing filepaths with whitespaces
|
||||||
- First-time setup dialog continuously triggering when DayZ install path had whitespaces in it
|
- First-time setup dialog continuously triggering when DayZ install path had whitespaces in it
|
||||||
|
|
||||||
## [5.8.0] 2025-07-06
|
## [5.8.0] 2025-07-06
|
||||||
### Added
|
## Added
|
||||||
- Filter servers by official/unofficial status
|
- Filter servers by official/unofficial status
|
||||||
- Automatically fetch geolocation records
|
- Automatically fetch geolocation records
|
||||||
### Changed
|
## Changed
|
||||||
- Updated internal versioning of helper files
|
- Updated internal versioning of helper files
|
||||||
### Fixed
|
## Fixed
|
||||||
- Corrected erroneous 2024 date in prior changelog entries
|
- Corrected erroneous 2024 date in prior changelog entries
|
||||||
|
|
||||||
## [5.7.0] 2025-04-17
|
## [5.7.0] 2025-04-17
|
||||||
### Added
|
## Added
|
||||||
- Save application dimensions when quitting and restore on subsequent boot
|
- Save application dimensions when quitting and restore on subsequent boot
|
||||||
### Fixed
|
## Fixed
|
||||||
- Issues with the window exceeding the bounds of the screen when displaying some table contexts
|
- Issues with the window exceeding the bounds of the screen when displaying some table contexts
|
||||||
### Changed
|
## Changed
|
||||||
- More direct server ping query method that should return marginally more accurate times
|
- More direct server ping query method that should return marginally more accurate times
|
||||||
|
|
||||||
## [5.6.7] 2025-04-04
|
## [5.6.7] 2025-04-04
|
||||||
### Dropped
|
## Dropped
|
||||||
- Removed extraneous pre-boot API checks that could cause error messages to be printed if the user had not set up an API key yet
|
- Removed extraneous pre-boot API checks that could cause error messages to be printed if the user had not set up an API key yet
|
||||||
|
|
||||||
## [5.6.6] 2025-03-16
|
## [5.6.6] 2025-03-16
|
||||||
### Changed
|
## Changed
|
||||||
- Update IP database records for 2025-03
|
- Update IP database records for 2025-03
|
||||||
|
|
||||||
## [5.6.5] 2025-03-04
|
## [5.6.5] 2025-03-04
|
||||||
### Fixed
|
## Fixed
|
||||||
- Livonia server results being dropped from batch queries
|
- Livonia server results being dropped from batch queries
|
||||||
|
|
||||||
## [5.6.4] 2025-02-10
|
## [5.6.4] 2025-02-10
|
||||||
|
|||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -12,7 +12,7 @@ from dzgui.const.constants import (
|
|||||||
WINDOW_DEFAULT_X,
|
WINDOW_DEFAULT_X,
|
||||||
WINDOW_DEFAULT_Y,
|
WINDOW_DEFAULT_Y,
|
||||||
)
|
)
|
||||||
from dzgui.api.probe import test_steam_api
|
from dzgui.api.probe import test_steam_api, test_bm_api
|
||||||
from dzgui.const.enum import Preferences
|
from dzgui.const.enum import Preferences
|
||||||
from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager
|
from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager
|
||||||
from dzgui.views.dialogs.generic import ExceptionDialog
|
from dzgui.views.dialogs.generic import ExceptionDialog
|
||||||
@@ -102,6 +102,8 @@ class ConfigManager:
|
|||||||
def update_api_key(self, key: Preferences, text: str) -> None:
|
def update_api_key(self, key: Preferences, text: str) -> None:
|
||||||
if key is Preferences.STEAM:
|
if key is Preferences.STEAM:
|
||||||
res = test_steam_api(text)
|
res = test_steam_api(text)
|
||||||
|
else:
|
||||||
|
res = test_bm_api(text)
|
||||||
if res is True:
|
if res is True:
|
||||||
self.update_config(key, text)
|
self.update_config(key, text)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ from typing import TYPE_CHECKING
|
|||||||
|
|
||||||
import dzgui.api.pefile as PeFile
|
import dzgui.api.pefile as PeFile
|
||||||
import dzgui.api.servers as Servers
|
import dzgui.api.servers as Servers
|
||||||
from dzgui.api.shortcuts import Shortcuts
|
|
||||||
|
|
||||||
from dzgui.api.steam import (
|
from dzgui.api.steam import (
|
||||||
connect,
|
connect,
|
||||||
@@ -103,6 +102,11 @@ class ConnectionManager:
|
|||||||
self.remote_mod_ids: list[str] = []
|
self.remote_mod_ids: list[str] = []
|
||||||
self.missing_mods: list[tuple[str, str, int, int]] = []
|
self.missing_mods: list[tuple[str, str, int, int]] = []
|
||||||
|
|
||||||
|
@call_on_thread(dialog.querying)
|
||||||
|
def connect_by_id(self, _id: int, key: str) -> None:
|
||||||
|
res = Servers.query_by_id(_id, key)
|
||||||
|
self._prepare_connection(res)
|
||||||
|
|
||||||
@call_on_thread(dialog.querying)
|
@call_on_thread(dialog.querying)
|
||||||
def connect_by_ip(self, addr: str) -> None:
|
def connect_by_ip(self, addr: str) -> None:
|
||||||
res = Servers.query_by_ip(addr)
|
res = Servers.query_by_ip(addr)
|
||||||
@@ -171,16 +175,11 @@ class ConnectionManager:
|
|||||||
if running_app is not None:
|
if running_app is not None:
|
||||||
allows_dl = get_app_allows_downloads(steam_path, running_app)
|
allows_dl = get_app_allows_downloads(steam_path, running_app)
|
||||||
running_app_name = get_app_name(running_app)
|
running_app_name = get_app_name(running_app)
|
||||||
# NOTE: for out-of-range appid, assume NSG.
|
|
||||||
# dzgui.api.steam.get_app_name() will return None
|
|
||||||
if running_app_name is None:
|
|
||||||
s = Shortcuts(steam_path)
|
|
||||||
# NOTE: returns "Unknown" if unparseable
|
|
||||||
running_app_name = s.find_appname_by_unsigned_id(running_app)
|
|
||||||
allows_downloads = (allows_dl, running_app_name)
|
allows_downloads = (allows_dl, running_app_name)
|
||||||
else:
|
else:
|
||||||
allows_downloads = (True, "")
|
allows_downloads = (True, "")
|
||||||
|
|
||||||
|
|
||||||
client_name = self.controller.get_steam_client_name()
|
client_name = self.controller.get_steam_client_name()
|
||||||
client = self.controller.query_config(Preferences.CLIENT)
|
client = self.controller.query_config(Preferences.CLIENT)
|
||||||
running = is_steam_running(client)
|
running = is_steam_running(client)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
from typing import Any, TYPE_CHECKING, Union
|
from typing import Any, TYPE_CHECKING, Union
|
||||||
|
from warnings import deprecated
|
||||||
|
|
||||||
from dzgui.const.enum import FilterMode
|
from dzgui.const.enum import FilterMode
|
||||||
from dzgui.model.model_factory import ModelFactory
|
from dzgui.model.model_factory import ModelFactory
|
||||||
@@ -85,8 +86,8 @@ class ProxyModelManager:
|
|||||||
if found is False:
|
if found is False:
|
||||||
self.control_model.append(history)
|
self.control_model.append(history)
|
||||||
|
|
||||||
if len(self.control_model) == 11:
|
# if len(self.control_model) == 11:
|
||||||
del self.control_model[0]
|
# del self.control_model[0]
|
||||||
|
|
||||||
self.filter(FilterMode.INITIAL, skip_cache=True)
|
self.filter(FilterMode.INITIAL, skip_cache=True)
|
||||||
|
|
||||||
@@ -258,7 +259,7 @@ 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")
|
@deprecated("Currently unused")
|
||||||
# def convert_model_to_list(self, model: "FastInsertListStore") -> list:
|
# def convert_model_to_list(self, model: "FastInsertListStore") -> list:
|
||||||
# return [[el for el in row] for row in model]
|
# return [[el for el in row] for row in model]
|
||||||
|
|
||||||
|
|||||||
+27
-20
@@ -15,7 +15,7 @@ from dzgui.const.constants import (
|
|||||||
from dzgui.const.enum import FilterMode, Preferences, ServerTab
|
from dzgui.const.enum import FilterMode, Preferences, ServerTab
|
||||||
from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager
|
from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager
|
||||||
from dzgui.strings import dialogs
|
from dzgui.strings import dialogs
|
||||||
from dzgui.util.strings import server_timeout, dialog
|
from dzgui.util.strings import api_warn_msg, dialog
|
||||||
from dzgui.views.dialogs.generic import ExceptionDialog
|
from dzgui.views.dialogs.generic import ExceptionDialog
|
||||||
|
|
||||||
import gi
|
import gi
|
||||||
@@ -175,29 +175,29 @@ class ServerModelManager:
|
|||||||
# TODO: wrap except
|
# TODO: wrap except
|
||||||
if self.controller.get_exit_event().is_set():
|
if self.controller.get_exit_event().is_set():
|
||||||
return
|
return
|
||||||
|
res = future.result(timeout=API_TIMEOUT)
|
||||||
self.thread_man.increment_dialog()
|
self.thread_man.increment_dialog()
|
||||||
|
# NOTE: failing entries are culled
|
||||||
"""
|
if res is None:
|
||||||
Collects futures in linear order after completion
|
# TODO: log which servers failed
|
||||||
for lock-step representation of the state file
|
continue
|
||||||
"""
|
servers.append(res)
|
||||||
for future in futures:
|
if len(servers) == 0:
|
||||||
res = future.result(timeout=API_TIMEOUT)
|
self.thread_man.set_cleanup_func(
|
||||||
# NOTE: failing entries are culled
|
StoredFunc(self._cleanup_on_failure), destroy_first=True
|
||||||
if res is None:
|
)
|
||||||
# TODO: log which servers failed
|
return
|
||||||
# query_direct() currently does not return enough information
|
|
||||||
continue
|
|
||||||
servers.append(res)
|
|
||||||
if len(servers) == 0:
|
|
||||||
self.thread_man.set_cleanup_func(
|
|
||||||
StoredFunc(self._cleanup_on_failure), destroy_first=True
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
parsed = Servers.parse_json(servers)
|
parsed = Servers.parse_json(servers)
|
||||||
self._push_data(parsed)
|
self._push_data(parsed)
|
||||||
|
|
||||||
|
@call_on_thread(dialog.querying)
|
||||||
|
def add_by_id(self, _id: str) -> None:
|
||||||
|
config_man = self.controller.get_config_man()
|
||||||
|
key = config_man.lookup(Preferences.BM)
|
||||||
|
res = Servers.query_by_id(int(_id), key)
|
||||||
|
self._parse_single_record(res)
|
||||||
|
|
||||||
@call_on_thread(dialog.querying)
|
@call_on_thread(dialog.querying)
|
||||||
def add_by_ip(self, addr: str) -> None:
|
def add_by_ip(self, addr: str) -> None:
|
||||||
res = Servers.query_by_ip(addr)
|
res = Servers.query_by_ip(addr)
|
||||||
@@ -241,6 +241,7 @@ class ServerModelManager:
|
|||||||
|
|
||||||
self.first_iteration = False
|
self.first_iteration = False
|
||||||
self.emitter.emit("load_maps", store)
|
self.emitter.emit("load_maps", store)
|
||||||
|
# self.emitter.emit("servers_loaded_init")
|
||||||
|
|
||||||
# TODO: dataclass for record rows; check for other dict annotations
|
# TODO: dataclass for record rows; check for other dict annotations
|
||||||
def add_to_history(self, data: tuple[dict[str, Any], "Record"]) -> None:
|
def add_to_history(self, data: tuple[dict[str, Any], "Record"]) -> None:
|
||||||
@@ -262,6 +263,12 @@ class ServerModelManager:
|
|||||||
proxy_man.remove_row_from_control(record)
|
proxy_man.remove_row_from_control(record)
|
||||||
self.update_history()
|
self.update_history()
|
||||||
|
|
||||||
|
def add_by_str(self, addr: str) -> None:
|
||||||
|
if addr.isdigit():
|
||||||
|
self.add_by_id(addr)
|
||||||
|
else:
|
||||||
|
self.add_by_ip(addr)
|
||||||
|
|
||||||
@call_on_thread(dialog.querying)
|
@call_on_thread(dialog.querying)
|
||||||
def update_playercount(
|
def update_playercount(
|
||||||
self, treeiter: Gtk.TreeIter, record: Servers.Record
|
self, treeiter: Gtk.TreeIter, record: Servers.Record
|
||||||
@@ -419,7 +426,7 @@ class ServerModelManager:
|
|||||||
# TODO: distinguish signals, e.g. "servers_failed_to_load", "servers_loaded_empty"
|
# TODO: distinguish signals, e.g. "servers_failed_to_load", "servers_loaded_empty"
|
||||||
# customize statusbar and dialog accordingly
|
# customize statusbar and dialog accordingly
|
||||||
if show_dialog:
|
if show_dialog:
|
||||||
dialog = ExceptionDialog(self.controller, server_timeout)
|
dialog = ExceptionDialog(self.controller, api_warn_msg)
|
||||||
dialog.run()
|
dialog.run()
|
||||||
|
|
||||||
def _push_data(self, data: list[Any]) -> None:
|
def _push_data(self, data: list[Any]) -> None:
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ 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_tooltip=(
|
connect_entry_tooltip=(
|
||||||
"- IP: format as IP:Query port\ne.g. 192.168.1.1:27016"
|
"- IP: format as IP:Query port\ne.g. 192.168.1.1:27016\n"
|
||||||
|
"- Battlemetrics: numeric server ID\ne.g. 123456"
|
||||||
)
|
)
|
||||||
connect_entry_placeholder="Enter IP (IP:Query port)"
|
connect_entry_placeholder="Enter IP (IP:Query port) or Battlemetrics ID (numerical id)"
|
||||||
|
|
||||||
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."
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
heading = "Play offline"
|
heading = "Play offline"
|
||||||
heading_disclaimer = (
|
|
||||||
"Not all mod combinations will function offline. Use at your own discretion."
|
|
||||||
)
|
|
||||||
|
|
||||||
mission_button = "Select mission folder"
|
mission_button = "Select mission folder"
|
||||||
mission_frame = "Mission"
|
mission_frame = "Mission"
|
||||||
|
|||||||
+5
-19
@@ -46,6 +46,11 @@ You must set up a Steam Web API key in order to browse the global server list.
|
|||||||
\nIf you don't have one already, it can be set up via the page below.
|
\nIf you don't have one already, it can be set up via the page below.
|
||||||
\nPlease refer to the DZGUI documentation for more instructions.
|
\nPlease refer to the DZGUI documentation for more instructions.
|
||||||
"""
|
"""
|
||||||
|
heading_bm_api = "Battlemetrics Web API key"
|
||||||
|
blurb_bm_api = """A Battlemetrics key is <b>optional</b>, but allows you to add/search for servers\n
|
||||||
|
by numeric ID on the web. For example, in the URL https://www.battlemetrics.net/servers/dayz/24819107,\n
|
||||||
|
the ID would be <b>24819107</b>.
|
||||||
|
"""
|
||||||
|
|
||||||
### PreferencesPage
|
### PreferencesPage
|
||||||
heading_prefs = "User preferences"
|
heading_prefs = "User preferences"
|
||||||
@@ -59,25 +64,6 @@ radio_mi = "mi (miles)"
|
|||||||
label_dist = "Distance display"
|
label_dist = "Distance display"
|
||||||
label_client = "Steam client"
|
label_client = "Steam client"
|
||||||
|
|
||||||
### Shortcuts page
|
|
||||||
heading_shortcuts = "Create shortcuts"
|
|
||||||
blurb_shortcuts = (
|
|
||||||
"You can optionally create shortcuts to facilitate launching DZGUI faster."
|
|
||||||
)
|
|
||||||
button_shortcuts = "Create shortcuts"
|
|
||||||
checkbox_steam_shortcut = (
|
|
||||||
"Add DZGUI to Steam library",
|
|
||||||
(
|
|
||||||
"This creates a standalone copy of DZGUI and adds it to Steam. "
|
|
||||||
"You <b>must restart Steam</b> for the shortcut to appear. This will not replace or update existing shortcuts. "
|
|
||||||
"If you update DZGUI when it is launched via Steam, updates will be applied to the Steam version."
|
|
||||||
),
|
|
||||||
)
|
|
||||||
checkbox_start_menu = (
|
|
||||||
"Add a start menu shortcut",
|
|
||||||
"Depends on local window manager settings. Will work on Steam Deck.",
|
|
||||||
)
|
|
||||||
checkbox_desktop_shortcut = ("Add a desktop shortcut", "Depends on the option above.")
|
|
||||||
|
|
||||||
### Completion page
|
### Completion page
|
||||||
heading_completion = "Setup complete"
|
heading_completion = "Setup complete"
|
||||||
|
|||||||
+1
-1
@@ -41,10 +41,10 @@ def print_mods(mods: list[int]) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def write_diagnostic(config: Path, outfile: Path) -> None:
|
def write_diagnostic(config: Path, outfile: Path) -> None:
|
||||||
|
# TODO: test availability on other distros
|
||||||
date = datetime.now().isoformat()
|
date = datetime.now().isoformat()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# FIXME: ID_LIKE not available on CachyOS
|
|
||||||
distro = platform.freedesktop_os_release()["ID_LIKE"]
|
distro = platform.freedesktop_os_release()["ID_LIKE"]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warn(e)
|
logger.warn(e)
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
import logging
|
|
||||||
import os
|
|
||||||
import shutil
|
|
||||||
|
|
||||||
from importlib import resources
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from dzgui.const.constants import APP_NAME, APP_NAME_LOWER, IMAGES_PATH
|
|
||||||
|
|
||||||
logger = logging.getLogger(APP_NAME)
|
|
||||||
|
|
||||||
|
|
||||||
def make_parents(path: "Path") -> None:
|
|
||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
|
|
||||||
def copy_dzgui_to_xdg_data(exe_path: Path) -> None:
|
|
||||||
dzgui = os.getenv("PYAPP")
|
|
||||||
if dzgui is None:
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
make_parents(exe_path)
|
|
||||||
shutil.copy(dzgui, exe_path)
|
|
||||||
except Exception as e:
|
|
||||||
logger.critical(e)
|
|
||||||
|
|
||||||
|
|
||||||
def find_icon_resource() -> "Path":
|
|
||||||
traversable = resources.files(APP_NAME_LOWER).joinpath(IMAGES_PATH)
|
|
||||||
images = Path(str(traversable))
|
|
||||||
icon = images.joinpath("icon.png")
|
|
||||||
return icon
|
|
||||||
@@ -4,9 +4,8 @@ from typing import Literal
|
|||||||
|
|
||||||
api_filter = r"(.*&key=)([^&]*)(.*)"
|
api_filter = r"(.*&key=)([^&]*)(.*)"
|
||||||
home_filter = r"(/home/)([^\s'\/]*)(.*)"
|
home_filter = r"(/home/)([^\s'\/]*)(.*)"
|
||||||
user_filter = r"(.*Steam/userdata/)([^/]*)(.*)"
|
|
||||||
REDACTED = r"\1REDACTED\3"
|
REDACTED = r"\1REDACTED\3"
|
||||||
REDACTION_PATTERNS = [api_filter, home_filter, user_filter]
|
REDACTION_PATTERNS = [api_filter, home_filter]
|
||||||
|
|
||||||
|
|
||||||
def redact_home(text: str) -> str:
|
def redact_home(text: str) -> str:
|
||||||
|
|||||||
+15
-5
@@ -100,7 +100,10 @@ steam_missing = "Local Steam installation is not set, possibly malformed config
|
|||||||
build_corrupted = (
|
build_corrupted = (
|
||||||
"Steam settings or DayZ installation may be corrupted. Try restarting Steam."
|
"Steam settings or DayZ installation may be corrupted. Try restarting Steam."
|
||||||
)
|
)
|
||||||
server_timeout = "Timed out when querying server(s). Check IP or try again later."
|
api_warn_msg = """No servers returned. Please wait and try again.
|
||||||
|
This usually indicates a transitory network timeout. If this issue persists, your API key or network may be malfunctioning.
|
||||||
|
"""
|
||||||
|
server_timeout = "Timed out when querying server, check IP or try again later."
|
||||||
server_error = (
|
server_error = (
|
||||||
"Error while contacting server, possibly timed out. Please wait and try again."
|
"Error while contacting server, possibly timed out. Please wait and try again."
|
||||||
)
|
)
|
||||||
@@ -283,8 +286,11 @@ class Thanks:
|
|||||||
class Options:
|
class Options:
|
||||||
header: str
|
header: str
|
||||||
steam_web: str
|
steam_web: str
|
||||||
|
bm_web: str
|
||||||
enter_steam: str
|
enter_steam: str
|
||||||
|
enter_bm: str
|
||||||
steam_placeholder: str
|
steam_placeholder: str
|
||||||
|
bm_placeholder: str
|
||||||
name_placeholder: str
|
name_placeholder: str
|
||||||
last_used: str
|
last_used: str
|
||||||
always_fs: str
|
always_fs: str
|
||||||
@@ -305,7 +311,7 @@ class Options:
|
|||||||
force_eventbox: str
|
force_eventbox: str
|
||||||
self_update: str
|
self_update: str
|
||||||
no_self_update: str
|
no_self_update: str
|
||||||
api_key: str
|
api_keys: str
|
||||||
prefs: str
|
prefs: str
|
||||||
mods: str
|
mods: str
|
||||||
version: str
|
version: str
|
||||||
@@ -372,8 +378,11 @@ thanks = Thanks(
|
|||||||
options = Options(
|
options = Options(
|
||||||
header="Options",
|
header="Options",
|
||||||
steam_web="Steam API page",
|
steam_web="Steam API page",
|
||||||
|
bm_web="Battlemetrics API page",
|
||||||
enter_steam="Enter your Steam API key",
|
enter_steam="Enter your Steam API key",
|
||||||
|
enter_bm="Enter your Battlemetrics API key",
|
||||||
steam_placeholder="Steam API key",
|
steam_placeholder="Steam API key",
|
||||||
|
bm_placeholder="Battlemetrics API key",
|
||||||
name_placeholder="Identifies you to other players in-game",
|
name_placeholder="Identifies you to other players in-game",
|
||||||
last_used="Last used dimensions",
|
last_used="Last used dimensions",
|
||||||
always_fs="Always fullscreen",
|
always_fs="Always fullscreen",
|
||||||
@@ -402,7 +411,7 @@ options = Options(
|
|||||||
"In-app updates are disabled when DZGUI is "
|
"In-app updates are disabled when DZGUI is "
|
||||||
"installed globally (e.g., via package manager)."
|
"installed globally (e.g., via package manager)."
|
||||||
),
|
),
|
||||||
api_key="API Key",
|
api_keys="API Keys",
|
||||||
prefs="Preferences",
|
prefs="Preferences",
|
||||||
mods="Mods",
|
mods="Mods",
|
||||||
version="Version",
|
version="Version",
|
||||||
@@ -540,9 +549,10 @@ connect_panel = ConnectPanel(
|
|||||||
connect="Connect",
|
connect="Connect",
|
||||||
add="Add",
|
add="Add",
|
||||||
add_con="Add/connect",
|
add_con="Add/connect",
|
||||||
placeholder="Enter IP (IP:Query port)",
|
placeholder="Enter IP (IP:Query port) or Battlemetrics ID (numerical id)",
|
||||||
entry_tooltip=(
|
entry_tooltip=(
|
||||||
"- IP: format as IP:Query port\ne.g. 192.168.1.1:27016"
|
"- IP: format as IP:Query port\ne.g. 192.168.1.1:27016\n"
|
||||||
|
"- Battlemetrics: numeric server ID\ne.g. 123456"
|
||||||
),
|
),
|
||||||
add_tooltip="Add to Saved Servers",
|
add_tooltip="Add to Saved Servers",
|
||||||
connect_tooltip="Connect to this server",
|
connect_tooltip="Connect to this server",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from typing import Callable, Literal, Self, TYPE_CHECKING, Union
|
from typing import Callable, Self, TYPE_CHECKING, Union
|
||||||
|
|
||||||
from dzgui.util.clip import copy_clipboard
|
from dzgui.util.clip import copy_clipboard
|
||||||
from dzgui.util.format import pluralize
|
from dzgui.util.format import pluralize
|
||||||
@@ -66,15 +66,6 @@ class IconButton(Gtk.Button):
|
|||||||
# self.set_image_position(Gtk.PositionType.RIGHT)
|
# self.set_image_position(Gtk.PositionType.RIGHT)
|
||||||
self.set_focus_on_click(False)
|
self.set_focus_on_click(False)
|
||||||
|
|
||||||
def swap_icon(self, icon: str) -> None:
|
|
||||||
start = self.icon.get_margin_start()
|
|
||||||
end = self.icon.get_margin_end()
|
|
||||||
alt_icon = Icon(icon, margin_start=start, margin_end=end)
|
|
||||||
self.set_image(alt_icon)
|
|
||||||
|
|
||||||
def revert_icon(self) -> None:
|
|
||||||
self.set_image(self.icon)
|
|
||||||
|
|
||||||
|
|
||||||
class IconTextButton(IconButton):
|
class IconTextButton(IconButton):
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -105,18 +96,11 @@ class ClipboardButton(IconTextButton):
|
|||||||
self.controller = controller
|
self.controller = controller
|
||||||
self.connect("clicked", self._on_button_clicked, func)
|
self.connect("clicked", self._on_button_clicked, func)
|
||||||
|
|
||||||
# TODO: strings
|
|
||||||
self.set_tooltip_text("Copy to clipboard")
|
self.set_tooltip_text("Copy to clipboard")
|
||||||
|
|
||||||
def _on_button_clicked(self, button: Self, func: Callable) -> None:
|
def _on_button_clicked(self, button: Self, func: Callable) -> None:
|
||||||
def revert() -> Literal[False]:
|
|
||||||
self.revert_icon()
|
|
||||||
return False
|
|
||||||
|
|
||||||
self.swap_icon("object-select-symbolic")
|
|
||||||
data = func()
|
data = func()
|
||||||
copy_clipboard(data)
|
copy_clipboard(data)
|
||||||
GLib.timeout_add(600, revert)
|
|
||||||
|
|
||||||
|
|
||||||
# TODO: determine when controller would be passed to this button or drop
|
# TODO: determine when controller would be passed to this button or drop
|
||||||
@@ -124,7 +108,6 @@ class CopyIpButton(ClipboardButton):
|
|||||||
def __init__(self, controller: Union["Controller", None], func: Callable) -> None:
|
def __init__(self, controller: Union["Controller", None], func: Callable) -> None:
|
||||||
super().__init__(controller, func)
|
super().__init__(controller, func)
|
||||||
|
|
||||||
# TODO: strings
|
|
||||||
self.set_tooltip_text("Copy IP to clipboard")
|
self.set_tooltip_text("Copy IP to clipboard")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -26,12 +26,15 @@ def validate_port(text: str) -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def validate_ip_truthy(text: str) -> bool:
|
def validate_ip_or_id(text: str) -> bool:
|
||||||
try:
|
try:
|
||||||
validate_ip(text)
|
validate_ip(text)
|
||||||
return True
|
return True
|
||||||
except Exception:
|
except Exception:
|
||||||
return False
|
if text.isdigit():
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
class ValidatedEntry(Gtk.Entry):
|
class ValidatedEntry(Gtk.Entry):
|
||||||
@@ -121,7 +124,7 @@ class IpEntry(ValidatedEntry):
|
|||||||
def __init__(self, controller: "Controller") -> None:
|
def __init__(self, controller: "Controller") -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
controller,
|
controller,
|
||||||
func=validate_ip_truthy,
|
func=validate_ip_or_id,
|
||||||
placeholder_text=connect_panel.placeholder,
|
placeholder_text=connect_panel.placeholder,
|
||||||
tooltip_text=connect_panel.entry_tooltip,
|
tooltip_text=connect_panel.entry_tooltip,
|
||||||
)
|
)
|
||||||
@@ -159,8 +162,7 @@ class APIEntry(Gtk.Box):
|
|||||||
self.entry.connect("icon-release", self._on_icon_release)
|
self.entry.connect("icon-release", self._on_icon_release)
|
||||||
self.entry.connect("activate", self._on_field_activated)
|
self.entry.connect("activate", self._on_field_activated)
|
||||||
|
|
||||||
# TODO: strings
|
self.submit = Gtk.Button(label="Submit")
|
||||||
self.submit = Gtk.Button(label="Validate")
|
|
||||||
self.submit.set_sensitive(False)
|
self.submit.set_sensitive(False)
|
||||||
self.submit.connect("clicked", self._on_submit)
|
self.submit.connect("clicked", self._on_submit)
|
||||||
|
|
||||||
|
|||||||
@@ -30,9 +30,6 @@ class InfoEventBox(Gtk.EventBox):
|
|||||||
self.connect("leave-notify-event", self._on_leave_tooltip)
|
self.connect("leave-notify-event", self._on_leave_tooltip)
|
||||||
self.add(box)
|
self.add(box)
|
||||||
|
|
||||||
def set_icon_yalign(self, offset: float) -> None:
|
|
||||||
self.icon.set_property("yalign", offset)
|
|
||||||
|
|
||||||
def _on_enter_tooltip(
|
def _on_enter_tooltip(
|
||||||
self, eventbox: Gtk.EventBox, eventcrossing: Gdk.EventCrossing
|
self, eventbox: Gtk.EventBox, eventcrossing: Gdk.EventCrossing
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|||||||
@@ -131,7 +131,6 @@ class RightPanel(Gtk.Box):
|
|||||||
return
|
return
|
||||||
self.copying = True
|
self.copying = True
|
||||||
version = self.version_label.get_text()
|
version = self.version_label.get_text()
|
||||||
# TODO: strings
|
|
||||||
self.version_label.set_text("Copied!")
|
self.version_label.set_text("Copied!")
|
||||||
copy_clipboard(version)
|
copy_clipboard(version)
|
||||||
GLib.timeout_add_seconds(1, revert)
|
GLib.timeout_add_seconds(1, revert)
|
||||||
@@ -41,7 +41,7 @@ class BootDialog(Gtk.Dialog):
|
|||||||
def __init__(self, parent: "BootWindow", xdg: "Xdg", version: str) -> None:
|
def __init__(self, parent: "BootWindow", xdg: "Xdg", version: str) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
title=dialog_header,
|
title=dialog_header,
|
||||||
transient_for=parent,
|
parent=parent,
|
||||||
modal=True,
|
modal=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class FilePicker(Gtk.FileChooserDialog):
|
|||||||
super().__init__(
|
super().__init__(
|
||||||
title=picker.title,
|
title=picker.title,
|
||||||
action=Gtk.FileChooserAction.SAVE,
|
action=Gtk.FileChooserAction.SAVE,
|
||||||
transient_for=parent,
|
parent=parent,
|
||||||
resizable=True,
|
resizable=True,
|
||||||
)
|
)
|
||||||
self.add_buttons("_Cancel", Gtk.ResponseType.CANCEL)
|
self.add_buttons("_Cancel", Gtk.ResponseType.CANCEL)
|
||||||
|
|||||||
@@ -111,22 +111,19 @@ class WaitDialog(GenericDialog):
|
|||||||
|
|
||||||
self.connect("delete-event", lambda widget, event: True)
|
self.connect("delete-event", lambda widget, event: True)
|
||||||
content = self.get_content_area()
|
content = self.get_content_area()
|
||||||
|
spinner = Gtk.Spinner()
|
||||||
self.prog = Gtk.ProgressBar()
|
self.prog = Gtk.ProgressBar()
|
||||||
|
|
||||||
spinner_box = Gtk.Box(
|
content.pack_end(self.cancel, NO_EXPAND, NO_FILL, 0)
|
||||||
orientation=Gtk.Orientation.HORIZONTAL, halign=Gtk.Align.CENTER
|
content.pack_end(spinner, NO_EXPAND, NO_FILL, 0)
|
||||||
)
|
|
||||||
spinner = Gtk.Spinner()
|
|
||||||
spinner_box.add(spinner)
|
|
||||||
|
|
||||||
if self.jobs > 1:
|
if self.jobs > 1:
|
||||||
content.pack_start(self.prog, NO_EXPAND, NO_FILL, 0)
|
content.pack_end(self.prog, NO_EXPAND, NO_FILL, 0)
|
||||||
else:
|
else:
|
||||||
content.pack_start(spinner_box, NO_EXPAND, NO_FILL, 0)
|
|
||||||
spinner.start()
|
spinner.start()
|
||||||
|
|
||||||
if show_cancel is True:
|
if show_cancel is False:
|
||||||
content.pack_start(self.cancel, NO_EXPAND, NO_FILL, 0)
|
self.connect("realize", lambda _: self.cancel.set_visible(False))
|
||||||
|
|
||||||
def update_text(self, msg: str) -> None:
|
def update_text(self, msg: str) -> None:
|
||||||
self.format_secondary_text(msg)
|
self.format_secondary_text(msg)
|
||||||
|
|||||||
+51
-153
@@ -1,13 +1,11 @@
|
|||||||
import os
|
|
||||||
import textwrap
|
import textwrap
|
||||||
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from importlib import resources
|
from importlib import resources
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Callable, Self, TYPE_CHECKING
|
from typing import Any, Callable, Self
|
||||||
|
|
||||||
from dzgui.api.probe import test_steam_api
|
from dzgui.api.probe import test_steam_api, test_bm_api
|
||||||
from dzgui.api.shortcuts import add_steam_shortcut
|
|
||||||
from dzgui.api.steam import get_steam_paths
|
from dzgui.api.steam import get_steam_paths
|
||||||
from dzgui.const.constants import (
|
from dzgui.const.constants import (
|
||||||
APP_NAME,
|
APP_NAME,
|
||||||
@@ -16,10 +14,7 @@ from dzgui.const.constants import (
|
|||||||
LEGACY_CONFIG_PATH,
|
LEGACY_CONFIG_PATH,
|
||||||
)
|
)
|
||||||
from dzgui.const.boilerplate import config_boilerplate
|
from dzgui.const.boilerplate import config_boilerplate
|
||||||
from dzgui.const.endpoints import STEAM_API_SETUP
|
from dzgui.const.endpoints import BM_API_SETUP, STEAM_API_SETUP
|
||||||
from dzgui.const.enum import Preferences
|
|
||||||
from dzgui.config import freedesktop
|
|
||||||
from dzgui.config.query import lookup
|
|
||||||
from dzgui.init.migrate import migrate_legacy_conf
|
from dzgui.init.migrate import migrate_legacy_conf
|
||||||
from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager
|
from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager
|
||||||
from dzgui.strings import wizard
|
from dzgui.strings import wizard
|
||||||
@@ -35,27 +30,17 @@ import gi
|
|||||||
gi.require_version("Gtk", "3.0")
|
gi.require_version("Gtk", "3.0")
|
||||||
from gi.repository import Gdk, Gtk, GLib, GObject, GdkPixbuf # noqa E402
|
from gi.repository import Gdk, Gtk, GLib, GObject, GdkPixbuf # noqa E402
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from dzgui.config.xdg import Xdg
|
|
||||||
|
|
||||||
|
|
||||||
class PageNum(Enum):
|
class PageNum(Enum):
|
||||||
INTRO = 1
|
INTRO = 1
|
||||||
HAS_CONFIG = 2
|
HAS_CONFIG = 2
|
||||||
STEAM_PATH = 3
|
STEAM_PATH = 3
|
||||||
STEAM_API = 4
|
STEAM_API = 4
|
||||||
USER_PREFS = 5
|
BM_API = 5
|
||||||
SHORTCUTS = 6
|
USER_PREFS = 6
|
||||||
FINAL = 7
|
FINAL = 7
|
||||||
|
|
||||||
|
|
||||||
class OptionalPageMixin:
|
|
||||||
"""Marks optional pages as advanceable"""
|
|
||||||
|
|
||||||
def _on_map(self, page: "ScrolledWizardPage") -> None:
|
|
||||||
EMITTER.emit("step_complete")
|
|
||||||
|
|
||||||
|
|
||||||
class DescriptionArea(Gtk.Box):
|
class DescriptionArea(Gtk.Box):
|
||||||
def __init__(self, text: str):
|
def __init__(self, text: str):
|
||||||
super().__init__(orientation=Gtk.Orientation.VERTICAL)
|
super().__init__(orientation=Gtk.Orientation.VERTICAL)
|
||||||
@@ -73,7 +58,7 @@ class Progress(Gtk.ProgressBar):
|
|||||||
|
|
||||||
class ScrolledWizardPage(Gtk.ScrolledWindow):
|
class ScrolledWizardPage(Gtk.ScrolledWindow):
|
||||||
def __init__(self, enum: PageNum, heading: str, description: str):
|
def __init__(self, enum: PageNum, heading: str, description: str):
|
||||||
super().__init__(overlay_scrolling=False)
|
super().__init__()
|
||||||
|
|
||||||
self.enum = enum
|
self.enum = enum
|
||||||
self.page_type: Gtk.AssistantPageType
|
self.page_type: Gtk.AssistantPageType
|
||||||
@@ -97,7 +82,6 @@ class ScrolledWizardPage(Gtk.ScrolledWindow):
|
|||||||
margin_top=50,
|
margin_top=50,
|
||||||
spacing=20,
|
spacing=20,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.add(self.box)
|
self.add(self.box)
|
||||||
self.prog = Progress()
|
self.prog = Progress()
|
||||||
self.box.pack_end(self.prog, expand=False, fill=False, padding=0)
|
self.box.pack_end(self.prog, expand=False, fill=False, padding=0)
|
||||||
@@ -157,10 +141,6 @@ class NotificationFrame(Gtk.Frame):
|
|||||||
if error:
|
if error:
|
||||||
add_class(self, "error-frame")
|
add_class(self, "error-frame")
|
||||||
|
|
||||||
def set_text(self, text: str) -> None:
|
|
||||||
wrapped = textwrap.fill(text, width=80)
|
|
||||||
self.label.set_markup(wrapped)
|
|
||||||
|
|
||||||
|
|
||||||
class APIValidationPage(ScrolledWizardPage):
|
class APIValidationPage(ScrolledWizardPage):
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -222,6 +202,23 @@ class APIValidationPage(ScrolledWizardPage):
|
|||||||
self.spinner.stop()
|
self.spinner.stop()
|
||||||
|
|
||||||
|
|
||||||
|
class BMValidationPage(APIValidationPage):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__(
|
||||||
|
enum=PageNum.BM_API,
|
||||||
|
heading=wizard.heading_bm_api,
|
||||||
|
description=wizard.blurb_bm_api,
|
||||||
|
link=BM_API_SETUP,
|
||||||
|
func=self._validate,
|
||||||
|
)
|
||||||
|
|
||||||
|
@call_on_thread("", show_dialog=False)
|
||||||
|
def _validate(self, key: str) -> None:
|
||||||
|
is_valid = test_bm_api(key.strip())
|
||||||
|
cleanup = StoredFunc(self._cleanup, is_valid, key)
|
||||||
|
self.thread_man.set_cleanup_func(cleanup)
|
||||||
|
|
||||||
|
|
||||||
class SteamValidationPage(APIValidationPage):
|
class SteamValidationPage(APIValidationPage):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
@@ -342,7 +339,6 @@ class ConfigMigrationPage(ScrolledWizardPage):
|
|||||||
def _on_import_clicked(self, button: Gtk.Button) -> None:
|
def _on_import_clicked(self, button: Gtk.Button) -> None:
|
||||||
self.grid.set_sensitive(False)
|
self.grid.set_sensitive(False)
|
||||||
try:
|
try:
|
||||||
# TODO: this could be deferred to the final page (prevents accidental destruction of dialog via ESC)
|
|
||||||
migrate_legacy_conf(self.config)
|
migrate_legacy_conf(self.config)
|
||||||
self.migrated = True
|
self.migrated = True
|
||||||
self.success_box.set_visible(True)
|
self.success_box.set_visible(True)
|
||||||
@@ -427,27 +423,23 @@ class CompletionPage(ScrolledWizardPage):
|
|||||||
|
|
||||||
|
|
||||||
class Assistant(Gtk.Assistant):
|
class Assistant(Gtk.Assistant):
|
||||||
def __init__(self, is_deck: bool, XDG: "Xdg"):
|
def __init__(self, is_deck: bool, config: Path):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
if is_deck:
|
if is_deck:
|
||||||
# NOTE: deemed to be "safe" dimensions that exclude taskbar size
|
self.fullscreen()
|
||||||
self.set_default_size(1085, 670)
|
|
||||||
else:
|
else:
|
||||||
self.set_default_size(1500, 900)
|
self.set_default_size(1500, 900)
|
||||||
|
|
||||||
self.is_binary = False if os.getenv("PYAPP") is None else True
|
self.config_path = config
|
||||||
self.config_path = XDG.config
|
|
||||||
|
|
||||||
self.config_values: dict[str, Any] = config_boilerplate
|
self.config_values: dict[str, Any] = config_boilerplate
|
||||||
|
|
||||||
self.setup_complete = False
|
|
||||||
|
|
||||||
self.page1 = IntroductionPage()
|
self.page1 = IntroductionPage()
|
||||||
self.page2 = ConfigMigrationPage(XDG.config)
|
self.page2 = ConfigMigrationPage(config)
|
||||||
self.page3 = SteamPathPage()
|
self.page3 = SteamPathPage()
|
||||||
self.page4 = SteamValidationPage()
|
self.page4 = SteamValidationPage()
|
||||||
self.page5 = PreferencesPage()
|
self.page5 = BMValidationPage()
|
||||||
self.page6 = ShortcutCreationPage(XDG.shortcut)
|
self.page6 = PreferencesPage()
|
||||||
self.page7 = CompletionPage()
|
self.page7 = CompletionPage()
|
||||||
|
|
||||||
self.set_forward_page_func(self._advance_page)
|
self.set_forward_page_func(self._advance_page)
|
||||||
@@ -468,13 +460,7 @@ class Assistant(Gtk.Assistant):
|
|||||||
self.page7,
|
self.page7,
|
||||||
):
|
):
|
||||||
# NOTE: skip config migration page if no legacy config file
|
# NOTE: skip config migration page if no legacy config file
|
||||||
if (
|
if page == self.page2 and self.has_legacy_config is False:
|
||||||
isinstance(page, ConfigMigrationPage)
|
|
||||||
and self.has_legacy_config is False
|
|
||||||
):
|
|
||||||
continue
|
|
||||||
# NOTE: disabled for now on system-provided packages
|
|
||||||
if isinstance(page, ShortcutCreationPage) and not self.is_binary:
|
|
||||||
continue
|
continue
|
||||||
self._add_page(page, page.get_page_type())
|
self._add_page(page, page.get_page_type())
|
||||||
|
|
||||||
@@ -489,38 +475,30 @@ class Assistant(Gtk.Assistant):
|
|||||||
|
|
||||||
def _advance_page(self, index: int) -> int:
|
def _advance_page(self, index: int) -> int:
|
||||||
page = self.get_nth_page(index)
|
page = self.get_nth_page(index)
|
||||||
|
# TODO: use enums
|
||||||
match page:
|
match page:
|
||||||
case IntroductionPage():
|
case self.page1:
|
||||||
pass
|
pass
|
||||||
case ConfigMigrationPage():
|
case self.page2:
|
||||||
if page.is_migrated():
|
if self.page2.is_migrated():
|
||||||
steam_path = lookup(self.config_path, Preferences.DEFAULT)
|
return self.get_n_pages() - 1
|
||||||
self.page6.set_steam_path(steam_path)
|
case self.page3:
|
||||||
offset = 1 if not self.is_binary else 2
|
|
||||||
self.setup_complete = True
|
|
||||||
return self.get_n_pages() - offset
|
|
||||||
case SteamPathPage():
|
|
||||||
self.config_values["default_steam_path"] = page.get_path_from_radio()
|
self.config_values["default_steam_path"] = page.get_path_from_radio()
|
||||||
case SteamValidationPage():
|
case self.page4:
|
||||||
self.config_values["steam_api"] = page.get_api_key()
|
self.config_values["steam_api"] = page.get_api_key()
|
||||||
case PreferencesPage():
|
case self.page5:
|
||||||
# NOTE: collects config values before advancing to last page
|
self.config_values["bm_api"] = page.get_api_key()
|
||||||
name, use_miles, client = self.page5.get_prefs()
|
# NOTE: collects config values before advancing to last page
|
||||||
|
case self.page6:
|
||||||
|
name, use_miles, client = self.page6.get_prefs()
|
||||||
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.write_config()
|
||||||
self.page6.set_steam_path(self.config_values["default_steam_path"])
|
|
||||||
self.setup_complete = True
|
|
||||||
case ShortcutCreationPage():
|
|
||||||
page.create_shortcuts()
|
|
||||||
case _:
|
case _:
|
||||||
raise AttributeError("Trying to advance a non-canonical page")
|
raise AttributeError("Trying to advance a non-canonical page")
|
||||||
return index + 1
|
return index + 1
|
||||||
|
|
||||||
def is_setup_complete(self) -> bool:
|
|
||||||
return self.setup_complete
|
|
||||||
|
|
||||||
def destroy_and_quit(self, widget: Self) -> None:
|
def destroy_and_quit(self, widget: Self) -> None:
|
||||||
self.destroy()
|
self.destroy()
|
||||||
Gtk.main_quit()
|
Gtk.main_quit()
|
||||||
@@ -557,91 +535,14 @@ class Assistant(Gtk.Assistant):
|
|||||||
bar.set_fraction(fraction)
|
bar.set_fraction(fraction)
|
||||||
bar.set_text(f"{page_num}/{total}")
|
bar.set_text(f"{page_num}/{total}")
|
||||||
|
|
||||||
if not isinstance(page, IntroductionPage):
|
# NOTE: disable forward action
|
||||||
|
# TODO: use page enums
|
||||||
|
if page == self.page5:
|
||||||
|
return
|
||||||
|
if page != self.page1:
|
||||||
EMITTER.emit("step_pending")
|
EMITTER.emit("step_pending")
|
||||||
|
|
||||||
|
|
||||||
class CheckboxWithLabel(Gtk.Box):
|
|
||||||
def __init__(self, text: str, blurb_text: str) -> None:
|
|
||||||
super().__init__(orientation=Gtk.Orientation.VERTICAL, spacing=5)
|
|
||||||
|
|
||||||
self.button = Gtk.CheckButton(label=text)
|
|
||||||
self.button.set_active(True)
|
|
||||||
label = Gtk.Label(label="", halign=Gtk.Align.START, margin_start=20)
|
|
||||||
wrapped = textwrap.fill(blurb_text, width=100)
|
|
||||||
label.set_markup(f"- {wrapped}")
|
|
||||||
|
|
||||||
for el in self.button, label:
|
|
||||||
self.add(el)
|
|
||||||
|
|
||||||
def get_checkbox(self) -> Gtk.CheckButton:
|
|
||||||
return self.button
|
|
||||||
|
|
||||||
def get_active(self) -> bool:
|
|
||||||
return self.button.get_active()
|
|
||||||
|
|
||||||
def set_active(self, state: bool) -> None:
|
|
||||||
self.button.set_active(state)
|
|
||||||
|
|
||||||
|
|
||||||
class ShortcutCreationPage(OptionalPageMixin, ScrolledWizardPage): # type: ignore
|
|
||||||
def __init__(self, shortcut: Path) -> None:
|
|
||||||
super().__init__(
|
|
||||||
enum=PageNum.SHORTCUTS,
|
|
||||||
heading=wizard.heading_shortcuts,
|
|
||||||
description=wizard.blurb_shortcuts,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.steam_path: Path
|
|
||||||
self.shortcut_path = shortcut
|
|
||||||
self.checks_area = Gtk.Box(
|
|
||||||
orientation=Gtk.Orientation.VERTICAL, spacing=10, margin_top=20
|
|
||||||
)
|
|
||||||
self.page_type = Gtk.AssistantPageType.INTRO
|
|
||||||
|
|
||||||
label, blurb = wizard.checkbox_steam_shortcut
|
|
||||||
self.steam_checkbox = CheckboxWithLabel(label, blurb)
|
|
||||||
|
|
||||||
label, blurb = wizard.checkbox_start_menu
|
|
||||||
self.start_menu_checkbox = CheckboxWithLabel(label, blurb)
|
|
||||||
cb = self.start_menu_checkbox.get_checkbox()
|
|
||||||
cb.connect("toggled", self._on_start_menu_toggled)
|
|
||||||
|
|
||||||
label, blurb = wizard.checkbox_desktop_shortcut
|
|
||||||
self.desktop_checkbox = CheckboxWithLabel(label, blurb)
|
|
||||||
|
|
||||||
for el in (
|
|
||||||
self.steam_checkbox,
|
|
||||||
self.start_menu_checkbox,
|
|
||||||
self.desktop_checkbox,
|
|
||||||
):
|
|
||||||
self.checks_area.add(el)
|
|
||||||
|
|
||||||
self.add_start(self.checks_area)
|
|
||||||
self.show_all()
|
|
||||||
self.connect("map", self._on_map)
|
|
||||||
|
|
||||||
def _on_start_menu_toggled(self, button: Gtk.CheckButton) -> None:
|
|
||||||
state = button.get_active()
|
|
||||||
if not state:
|
|
||||||
self.desktop_checkbox.set_active(state)
|
|
||||||
self.desktop_checkbox.set_sensitive(state)
|
|
||||||
|
|
||||||
def set_steam_path(self, path: Path) -> None:
|
|
||||||
self.steam_path = path
|
|
||||||
|
|
||||||
def create_shortcuts(self) -> None:
|
|
||||||
# NOTE: best-effort, permissive even on failure (page is already marked as complete)
|
|
||||||
if self.steam_checkbox.get_active():
|
|
||||||
add_steam_shortcut(self.steam_path, self.shortcut_path)
|
|
||||||
|
|
||||||
if self.start_menu_checkbox.get_active():
|
|
||||||
desktop_file = freedesktop.write_desktop_file(self.shortcut_path)
|
|
||||||
|
|
||||||
if self.desktop_checkbox.get_active():
|
|
||||||
freedesktop.write_desktop_shortcut(desktop_file)
|
|
||||||
|
|
||||||
|
|
||||||
class SteamPathPage(ScrolledWizardPage):
|
class SteamPathPage(ScrolledWizardPage):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
@@ -698,20 +599,17 @@ class SteamPathPage(ScrolledWizardPage):
|
|||||||
|
|
||||||
|
|
||||||
class SetupWizard(Gtk.Application):
|
class SetupWizard(Gtk.Application):
|
||||||
def __init__(self, is_deck: bool, XDG: "Xdg") -> None:
|
def __init__(self, is_deck: bool, config: Path) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
GLib.set_prgname(APP_NAME)
|
GLib.set_prgname(APP_NAME)
|
||||||
self.win = Window(is_deck, XDG)
|
Window(is_deck, config)
|
||||||
Gtk.main()
|
Gtk.main()
|
||||||
|
|
||||||
def is_setup_complete(self) -> int:
|
|
||||||
return self.win.assistant.is_setup_complete()
|
|
||||||
|
|
||||||
|
|
||||||
class Window(Gtk.Window):
|
class Window(Gtk.Window):
|
||||||
def __init__(self, is_deck: bool, XDG: "Xdg") -> None:
|
def __init__(self, is_deck: bool, config: Path) -> None:
|
||||||
super().__init__(title=APP_NAME, icon_name=APP_NAME)
|
super().__init__(title=APP_NAME, icon_name=APP_NAME)
|
||||||
self.assistant = Assistant(is_deck, XDG)
|
Assistant(is_deck, config)
|
||||||
|
|
||||||
|
|
||||||
class Emitter(GObject.GObject):
|
class Emitter(GObject.GObject):
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import logging
|
import logging
|
||||||
import re
|
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
from importlib import resources
|
from importlib import resources
|
||||||
@@ -33,63 +32,15 @@ class Changelog(HelpMenuMixin, ScrollableMixin, Gtk.ScrolledWindow): # type: ig
|
|||||||
|
|
||||||
# TODO: should long text be wrapped?
|
# TODO: should long text be wrapped?
|
||||||
self.controller = controller
|
self.controller = controller
|
||||||
self.box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5, margin_top=10)
|
formatted = format_pango(changelog)
|
||||||
|
self.changelog_label = Gtk.Label(valign=Gtk.Align.START, margin=15)
|
||||||
|
self.changelog_label.set_markup(formatted)
|
||||||
|
self.box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||||
|
self.box.add(self.changelog_label)
|
||||||
self.add(self.box)
|
self.add(self.box)
|
||||||
|
|
||||||
self.connect("key-press-event", self._on_keypress)
|
self.connect("key-press-event", self._on_keypress)
|
||||||
self.connect("key-press-event", self._on_esc_keypress)
|
self.connect("key-press-event", self._on_esc_keypress)
|
||||||
|
|
||||||
changes = self._parse(changelog)
|
|
||||||
self._generate_nodes(changes)
|
|
||||||
self.show_all()
|
|
||||||
|
|
||||||
def grab_content_area(self) -> None:
|
def grab_content_area(self) -> None:
|
||||||
self.grab_focus()
|
self.grab_focus()
|
||||||
|
|
||||||
def _parse_version(self, version: str) -> tuple[str, str]:
|
|
||||||
pattern = r"(^##\s\[)(.+)(\] )(.+$)"
|
|
||||||
match = re.match(pattern, version)
|
|
||||||
if match:
|
|
||||||
version = match.group(2)
|
|
||||||
date = match.group(4)
|
|
||||||
return version, date
|
|
||||||
raise ValueError("Changelog parse error")
|
|
||||||
|
|
||||||
|
|
||||||
def _generate_nodes(self, releases: list[tuple[str, list[str]]]) -> None:
|
|
||||||
for header, changes in releases:
|
|
||||||
for ind in (0, -1):
|
|
||||||
if changes[ind] == "":
|
|
||||||
del changes[ind]
|
|
||||||
text = "\n".join(changes)
|
|
||||||
formatted = format_pango(text)
|
|
||||||
|
|
||||||
label = Gtk.Label(valign=Gtk.Align.START, margin=15, halign=Gtk.Align.START)
|
|
||||||
label.set_markup(formatted)
|
|
||||||
|
|
||||||
version, date = self._parse_version(header)
|
|
||||||
button_text = f"{version} ({date})"
|
|
||||||
button = Gtk.Button(label=button_text, margin_start=5)
|
|
||||||
|
|
||||||
expander = Gtk.Expander()
|
|
||||||
expander.set_label_widget(button)
|
|
||||||
expander.add(label)
|
|
||||||
self.box.add(expander)
|
|
||||||
|
|
||||||
|
|
||||||
def _parse(self, changelog: str) -> list[tuple[str, list[str]]]:
|
|
||||||
release = ""
|
|
||||||
releases: list[tuple[str, list[str]]] = []
|
|
||||||
release_notes: list[str] = []
|
|
||||||
for line in changelog.splitlines():
|
|
||||||
if line.startswith("# "):
|
|
||||||
continue
|
|
||||||
if line.startswith("## "):
|
|
||||||
if release != "":
|
|
||||||
releases.append((release, release_notes))
|
|
||||||
release_notes = []
|
|
||||||
release = ""
|
|
||||||
release = line
|
|
||||||
continue
|
|
||||||
release_notes.append(line.rstrip())
|
|
||||||
return releases
|
|
||||||
@@ -41,18 +41,9 @@ class FolderError(Enum):
|
|||||||
FOLDER_CHANGED = 3
|
FOLDER_CHANGED = 3
|
||||||
|
|
||||||
|
|
||||||
class PageHeading(Gtk.Box):
|
class PageHeading(Gtk.Label):
|
||||||
def __init__(self, label: str, controller: "Controller") -> None:
|
def __init__(self, label: str) -> None:
|
||||||
super().__init__(
|
super().__init__(label=label, halign=Gtk.Align.CENTER)
|
||||||
halign=Gtk.Align.CENTER, orientation=Gtk.Orientation.HORIZONTAL
|
|
||||||
)
|
|
||||||
|
|
||||||
self.label = Gtk.Label(label=label)
|
|
||||||
self.eb = InfoEventBox(offline.heading_disclaimer, controller)
|
|
||||||
self.eb.set_icon_yalign(0.7)
|
|
||||||
|
|
||||||
for el in self.label, self.eb:
|
|
||||||
self.add(el)
|
|
||||||
|
|
||||||
css.add_class(self, "page-heading")
|
css.add_class(self, "page-heading")
|
||||||
|
|
||||||
@@ -443,8 +434,7 @@ class OfflineLoader(Gtk.Box):
|
|||||||
self.emitter = controller.get_emitter()
|
self.emitter = controller.get_emitter()
|
||||||
self.offline_man = OfflineManager(controller)
|
self.offline_man = OfflineManager(controller)
|
||||||
|
|
||||||
heading = PageHeading(offline.heading, controller)
|
self.add(PageHeading(offline.heading))
|
||||||
self.add(heading)
|
|
||||||
|
|
||||||
self.local_frame = ModFrame(self, controller, offline.local_frame)
|
self.local_frame = ModFrame(self, controller, offline.local_frame)
|
||||||
self.custom_frame = CustomModFrame(self, controller, offline.custom_frame)
|
self.custom_frame = CustomModFrame(self, controller, offline.custom_frame)
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ from dzgui.const.constants import (
|
|||||||
VIEW_CONCEAL,
|
VIEW_CONCEAL,
|
||||||
VIEW_REVEAL,
|
VIEW_REVEAL,
|
||||||
)
|
)
|
||||||
from dzgui.const.endpoints import STEAM_API_SETUP
|
from dzgui.const.endpoints import STEAM_API_SETUP, BM_API_SETUP
|
||||||
from dzgui.const.enum import Preferences, ServerTab
|
from dzgui.const.enum import Preferences, ServerTab
|
||||||
from dzgui.strings import errors, options
|
from dzgui.strings import errors, options
|
||||||
from dzgui.util import strings, css, open_links
|
from dzgui.util import strings, css, open_links
|
||||||
@@ -66,15 +66,23 @@ class Options(Gtk.Box):
|
|||||||
self.add(label)
|
self.add(label)
|
||||||
|
|
||||||
self.steam_entry: Gtk.Entry
|
self.steam_entry: Gtk.Entry
|
||||||
|
self.bm_entry: Gtk.Entry
|
||||||
|
|
||||||
self.steam = WebButton(label=strings.options.steam_web)
|
self.steam = WebButton(label=strings.options.steam_web)
|
||||||
self.steam.connect("clicked", self._on_link_button_clicked, STEAM_API_SETUP)
|
self.steam.connect("clicked", self._on_link_button_clicked, STEAM_API_SETUP)
|
||||||
|
|
||||||
|
self.bm = WebButton(label=strings.options.bm_web)
|
||||||
|
self.bm.connect("clicked", self._on_link_button_clicked, BM_API_SETUP)
|
||||||
|
|
||||||
self.steam_box = self._make_submit_field(
|
self.steam_box = self._make_submit_field(
|
||||||
strings.options.enter_steam, Preferences.STEAM, True
|
strings.options.enter_steam, Preferences.STEAM, True
|
||||||
)
|
)
|
||||||
|
self.bm_box = self._make_submit_field(
|
||||||
|
strings.options.enter_bm, Preferences.BM, True
|
||||||
|
)
|
||||||
api_rows = [
|
api_rows = [
|
||||||
[LeftLabel(strings.options.steam_placeholder), self.steam_box, self.steam],
|
[LeftLabel(strings.options.steam_placeholder), self.steam_box],
|
||||||
|
[LeftLabel(strings.options.bm_placeholder), self.bm_box],
|
||||||
]
|
]
|
||||||
|
|
||||||
self.player_box = self._make_submit_field(
|
self.player_box = self._make_submit_field(
|
||||||
@@ -136,6 +144,18 @@ class Options(Gtk.Box):
|
|||||||
api_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
api_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||||
api_grid = self._make_grid(api_rows)
|
api_grid = self._make_grid(api_rows)
|
||||||
api_box.add(api_grid)
|
api_box.add(api_grid)
|
||||||
|
api_links_box = Gtk.Box(
|
||||||
|
orientation=Gtk.Orientation.HORIZONTAL,
|
||||||
|
margin_start=5,
|
||||||
|
margin_end=5,
|
||||||
|
margin_top=5,
|
||||||
|
margin_bottom=10,
|
||||||
|
homogeneous=True,
|
||||||
|
spacing=10,
|
||||||
|
)
|
||||||
|
api_links_box.add(self.steam)
|
||||||
|
api_links_box.add(self.bm)
|
||||||
|
api_box.add(api_links_box)
|
||||||
|
|
||||||
prefs_grid = self._make_grid(pref_rows)
|
prefs_grid = self._make_grid(pref_rows)
|
||||||
version_grid = self._make_grid(version_rows)
|
version_grid = self._make_grid(version_rows)
|
||||||
@@ -156,7 +176,7 @@ class Options(Gtk.Box):
|
|||||||
grid.attach(developers, 1, 0, self.DEFAULT_WIDTH, self.DEFAULT_HEIGHT)
|
grid.attach(developers, 1, 0, self.DEFAULT_WIDTH, self.DEFAULT_HEIGHT)
|
||||||
|
|
||||||
for pair in [
|
for pair in [
|
||||||
(api_box, strings.options.api_key),
|
(api_box, strings.options.api_keys),
|
||||||
(prefs_grid, strings.options.prefs),
|
(prefs_grid, strings.options.prefs),
|
||||||
(version_grid, strings.options.version),
|
(version_grid, strings.options.version),
|
||||||
]:
|
]:
|
||||||
@@ -175,11 +195,13 @@ class Options(Gtk.Box):
|
|||||||
return str(model[ind][0])
|
return str(model[ind][0])
|
||||||
|
|
||||||
def block_text_entry(self) -> None:
|
def block_text_entry(self) -> None:
|
||||||
self.steam_entry.set_position(-1)
|
for entry in self.steam_entry, self.bm_entry:
|
||||||
self.steam_entry.set_can_focus(False)
|
entry.set_position(-1)
|
||||||
|
entry.set_can_focus(False)
|
||||||
|
|
||||||
def unblock_text_entry(self) -> None:
|
def unblock_text_entry(self) -> None:
|
||||||
self.steam_entry.set_can_focus(True)
|
for entry in self.steam_entry, self.bm_entry:
|
||||||
|
entry.set_can_focus(True)
|
||||||
|
|
||||||
def _on_developers_clicked(self, button: Gtk.Button) -> None:
|
def _on_developers_clicked(self, button: Gtk.Button) -> None:
|
||||||
self.controller.show_developers_page()
|
self.controller.show_developers_page()
|
||||||
@@ -212,6 +234,8 @@ class Options(Gtk.Box):
|
|||||||
|
|
||||||
if context == Preferences.STEAM:
|
if context == Preferences.STEAM:
|
||||||
self.steam_entry = entry
|
self.steam_entry = entry
|
||||||
|
else:
|
||||||
|
self.bm_entry = entry
|
||||||
|
|
||||||
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
|
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
|
||||||
box.add(entry)
|
box.add(entry)
|
||||||
@@ -258,7 +282,7 @@ class Options(Gtk.Box):
|
|||||||
case Preferences.NAME:
|
case Preferences.NAME:
|
||||||
value = entry.get_text().strip()
|
value = entry.get_text().strip()
|
||||||
self.controller.update_config(enum, value)
|
self.controller.update_config(enum, value)
|
||||||
case Preferences.STEAM:
|
case Preferences.BM | Preferences.STEAM:
|
||||||
text = "".join(entry.get_text().split())
|
text = "".join(entry.get_text().split())
|
||||||
self.controller.update_api_key(enum, text)
|
self.controller.update_api_key(enum, text)
|
||||||
|
|
||||||
@@ -274,6 +298,9 @@ class Options(Gtk.Box):
|
|||||||
def revert(self, mode: Preferences) -> None:
|
def revert(self, mode: Preferences) -> None:
|
||||||
if mode == Preferences.STEAM:
|
if mode == Preferences.STEAM:
|
||||||
self.steam_entry.set_text(self.old_steam)
|
self.steam_entry.set_text(self.old_steam)
|
||||||
|
else:
|
||||||
|
self.bm_entry.set_text(self.old_bm)
|
||||||
|
pass
|
||||||
|
|
||||||
def _on_start_tab_changed(self, combo: Gtk.ComboBoxText) -> None:
|
def _on_start_tab_changed(self, combo: Gtk.ComboBoxText) -> None:
|
||||||
_iter = combo.get_active_iter()
|
_iter = combo.get_active_iter()
|
||||||
@@ -309,6 +336,8 @@ class Options(Gtk.Box):
|
|||||||
old = self.old_name
|
old = self.old_name
|
||||||
case Preferences.STEAM:
|
case Preferences.STEAM:
|
||||||
old = self.old_steam
|
old = self.old_steam
|
||||||
|
case Preferences.BM:
|
||||||
|
old = self.old_bm
|
||||||
if text == old:
|
if text == old:
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
@@ -372,13 +401,16 @@ class Options(Gtk.Box):
|
|||||||
name = self.controller.query_config(Preferences.NAME)
|
name = self.controller.query_config(Preferences.NAME)
|
||||||
default_steam_path = self.controller.query_config(Preferences.DEFAULT)
|
default_steam_path = self.controller.query_config(Preferences.DEFAULT)
|
||||||
steam = self.controller.query_config(Preferences.STEAM)
|
steam = self.controller.query_config(Preferences.STEAM)
|
||||||
|
bm = self.controller.query_config(Preferences.BM)
|
||||||
|
|
||||||
steam_path = Path(default_steam_path)
|
steam_path = Path(default_steam_path)
|
||||||
|
|
||||||
self.old_steam = steam
|
self.old_steam = steam
|
||||||
|
self.old_bm = bm
|
||||||
self.old_name = name
|
self.old_name = name
|
||||||
|
|
||||||
self.steam_entry.set_text(steam)
|
self.steam_entry.set_text(steam)
|
||||||
|
self.bm_entry.set_text(bm)
|
||||||
p = self.player_box.get_children()[0]
|
p = self.player_box.get_children()[0]
|
||||||
if hasattr(p, "set_text"):
|
if hasattr(p, "set_text"):
|
||||||
p.set_text(name)
|
p.set_text(name)
|
||||||
@@ -396,6 +428,7 @@ class Options(Gtk.Box):
|
|||||||
for field in (
|
for field in (
|
||||||
[name, self.player_box],
|
[name, self.player_box],
|
||||||
[steam, self.steam_box],
|
[steam, self.steam_box],
|
||||||
|
[bm, self.bm_box],
|
||||||
):
|
):
|
||||||
if field[0] == "":
|
if field[0] == "":
|
||||||
field[1].get_children()[1].set_sensitive(False)
|
field[1].get_children()[1].set_sensitive(False)
|
||||||
|
|||||||
@@ -273,7 +273,7 @@ class PreConnectionAssistant(Gtk.Box):
|
|||||||
)
|
)
|
||||||
|
|
||||||
allows_dl, running_app = prereqs.allows_downloads
|
allows_dl, running_app = prereqs.allows_downloads
|
||||||
if len(prereqs.mods) > 0 and allows_dl is False:
|
if allows_dl is False:
|
||||||
msg = (
|
msg = (
|
||||||
f"The game '{running_app}' is currently running in Steam, but background downloads are not enabled.\n"
|
f"The game '{running_app}' is currently running in Steam, but background downloads are not enabled.\n"
|
||||||
"Either stop the game first, or update your global Steam settings or the game's local settings.\n"
|
"Either stop the game first, or update your global Steam settings or the game's local settings.\n"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from typing import Any, Optional, TYPE_CHECKING
|
from typing import Any, Optional, TYPE_CHECKING
|
||||||
|
from warnings import deprecated
|
||||||
|
|
||||||
from dzgui.const.constants import APP_NAME, SEPARATOR
|
from dzgui.const.constants import APP_NAME, SEPARATOR
|
||||||
from dzgui.util.keys import is_ctrl_mask, is_navkey
|
from dzgui.util.keys import is_ctrl_mask, is_navkey
|
||||||
@@ -172,7 +173,7 @@ class TreeView(CursorMixin, Gtk.TreeView): # type: ignore
|
|||||||
(model, pathlist) = sels
|
(model, pathlist) = sels
|
||||||
return (model, pathlist)
|
return (model, pathlist)
|
||||||
|
|
||||||
# @deprecated("Currently unused")
|
@deprecated("Currently unused")
|
||||||
# def get_mpath(self) -> Optional[Gtk.TreePath]:
|
# def get_mpath(self) -> Optional[Gtk.TreePath]:
|
||||||
# (model, pathlist) = self.get_model_and_pathlist()
|
# (model, pathlist) = self.get_model_and_pathlist()
|
||||||
# if len(pathlist) < 1:
|
# if len(pathlist) < 1:
|
||||||
@@ -194,7 +195,7 @@ class TreeView(CursorMixin, Gtk.TreeView): # type: ignore
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# @deprecated("unused")
|
@deprecated("unused")
|
||||||
# def get_selected_row(self) -> Optional[Gtk.TreeModelRow]:
|
# def get_selected_row(self) -> Optional[Gtk.TreeModelRow]:
|
||||||
# ind = self.get_selected_row_index()
|
# ind = self.get_selected_row_index()
|
||||||
# model = self.get_model()
|
# model = self.get_model()
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ class LogTreeView(ContextMixin, TreeView): # type: ignore
|
|||||||
model = ModelFactory().new_model_from_logfile(filepath)
|
model = ModelFactory().new_model_from_logfile(filepath)
|
||||||
_filter = model.filter_new()
|
_filter = model.filter_new()
|
||||||
_filter.set_visible_func(self._filter_rows)
|
_filter.set_visible_func(self._filter_rows)
|
||||||
sortable = Gtk.TreeModelSort.new_with_model(_filter)
|
sortable = Gtk.TreeModelSort(_filter)
|
||||||
self.set_model(sortable)
|
self.set_model(sortable)
|
||||||
_filter.refilter()
|
_filter.refilter()
|
||||||
path = Gtk.TreePath.new_from_indices([0])
|
path = Gtk.TreePath.new_from_indices([0])
|
||||||
|
|||||||
+3
-5
@@ -4,7 +4,7 @@ description = "DayZ server browser and mod manager for Linux"
|
|||||||
authors = [
|
authors = [
|
||||||
{name = "aclist"}
|
{name = "aclist"}
|
||||||
]
|
]
|
||||||
version = "7.0.0b20"
|
version = "7.0.0b15"
|
||||||
license = "GPL-3.0-or-later"
|
license = "GPL-3.0-or-later"
|
||||||
license-files = ["LICENSE"]
|
license-files = ["LICENSE"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
@@ -76,7 +76,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
line-length = 88
|
line-length = 88
|
||||||
indent-width = 4
|
indent-width = 4
|
||||||
extend-exclude = ["*lib/a2s"]
|
extend-exclude = ["*lib/a2s"]
|
||||||
lint.unfixable = ["F401", "I001"]
|
lint.unfixable = ["F401"]
|
||||||
|
|
||||||
[tool.ruff.format]
|
[tool.ruff.format]
|
||||||
quote-style = "double"
|
quote-style = "double"
|
||||||
@@ -107,13 +107,11 @@ config-settings-package = { pygobject-stubs = { config = "Gtk3,Gdk3,Soup2" } }
|
|||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
markers = [
|
markers = [
|
||||||
"apitest: checks remote endpoints",
|
|
||||||
"config: config file keys/values",
|
"config: config file keys/values",
|
||||||
"realconfig: depends on actual generated local config file",
|
|
||||||
"mods: tests mod metadata/link creation",
|
"mods: tests mod metadata/link creation",
|
||||||
"pefile: validate PE files",
|
"pefile: validate PE files",
|
||||||
"post_install: requires a completed installation",
|
"post_install: requires a completed installation",
|
||||||
"redact: log redaction mechanisms",
|
"redact: log redaction mechanisms",
|
||||||
"slow: long-running tests",
|
"slow: long-running tests",
|
||||||
"webtest: depends on remote endpoint",
|
"webtest: checks remote endpoints"
|
||||||
]
|
]
|
||||||
@@ -12,7 +12,6 @@ This file includes the combined licenses to the following components:
|
|||||||
- psutil
|
- psutil
|
||||||
- PyGObject
|
- PyGObject
|
||||||
- python-a2s
|
- python-a2s
|
||||||
- vdf
|
|
||||||
|
|
||||||
When shipped as a binary installer, these components are collectively bundled
|
When shipped as a binary installer, these components are collectively bundled
|
||||||
as a turnkey application and used to run the DZGUI utility.\n\n
|
as a turnkey application and used to run the DZGUI utility.\n\n
|
||||||
|
|||||||
-13
@@ -1,13 +0,0 @@
|
|||||||
"users"
|
|
||||||
{
|
|
||||||
"0"
|
|
||||||
{
|
|
||||||
"AccountName" "STEAMUSER"
|
|
||||||
"PersonaName" "STEAMUSER"
|
|
||||||
"RememberPassword" "0"
|
|
||||||
"WantsOfflineMode" "0"
|
|
||||||
"SkipOfflineModeWarning" "0"
|
|
||||||
"AutoLogin" "1"
|
|
||||||
"Timestamp" "0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-14
@@ -1,14 +0,0 @@
|
|||||||
"users"
|
|
||||||
{
|
|
||||||
"0"
|
|
||||||
{
|
|
||||||
"AccountName" "STEAMUSER"
|
|
||||||
"PersonaName" "STEAMUSER"
|
|
||||||
"RememberPassword" "0"
|
|
||||||
"WantsOfflineMode" "0"
|
|
||||||
"SkipOfflineModeWarning" "0"
|
|
||||||
"AllowAutoLogin" "1"
|
|
||||||
"MostRecent" "1"
|
|
||||||
"Timestamp" "0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
"users"
|
|
||||||
{
|
|
||||||
"0"
|
|
||||||
{
|
|
||||||
"AccountName" "STEAMUSER"
|
|
||||||
"PersonaName" "STEAMUSER"
|
|
||||||
"RememberPassword" "0"
|
|
||||||
"WantsOfflineMode" "0"
|
|
||||||
"SkipOfflineModeWarning" "0"
|
|
||||||
"AllowAutoLogin" "1"
|
|
||||||
"MostRecent" "0"
|
|
||||||
"Timestamp" "0"
|
|
||||||
}
|
|
||||||
"1"
|
|
||||||
{
|
|
||||||
"AccountName" "STEAMUSER2"
|
|
||||||
"PersonaName" "STEAMUSER2"
|
|
||||||
"RememberPassword" "0"
|
|
||||||
"WantsOfflineMode" "0"
|
|
||||||
"SkipOfflineModeWarning" "0"
|
|
||||||
"AllowAutoLogin" "1"
|
|
||||||
"MostRecent" "1"
|
|
||||||
"Timestamp" "0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Vendored
BIN
Binary file not shown.
Vendored
File renamed without changes.
Vendored
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -7,12 +7,13 @@ from dzgui.config.xdg import get_xdg_paths, parse_filepaths
|
|||||||
from dzgui.config import convert
|
from dzgui.config import convert
|
||||||
from tests.fixtures import fixture_path
|
from tests.fixtures import fixture_path
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.config
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def legacy_config():
|
def legacy_config():
|
||||||
return fixture_path("dztuirc_1")
|
return fixture_path("dztuirc_1")
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def unset_values():
|
def unset_values():
|
||||||
return fixture_path("dztuirc_3")
|
return fixture_path("dztuirc_3")
|
||||||
@@ -21,6 +22,7 @@ def unset_values():
|
|||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def keys():
|
def keys():
|
||||||
return [
|
return [
|
||||||
|
"bm_api",
|
||||||
"fav_server",
|
"fav_server",
|
||||||
"fav_label",
|
"fav_label",
|
||||||
"name",
|
"name",
|
||||||
@@ -36,7 +38,6 @@ def keys():
|
|||||||
|
|
||||||
# TODO: use a static fixture instead of system config
|
# TODO: use a static fixture instead of system config
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@pytest.mark.realconfig
|
|
||||||
def config():
|
def config():
|
||||||
paths = get_xdg_paths()
|
paths = get_xdg_paths()
|
||||||
xdg = parse_filepaths(paths)
|
xdg = parse_filepaths(paths)
|
||||||
@@ -44,21 +45,18 @@ def config():
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.post_install
|
@pytest.mark.post_install
|
||||||
@pytest.mark.realconfig
|
|
||||||
def test_invalid_config_value(config):
|
def test_invalid_config_value(config):
|
||||||
with pytest.raises(Exception):
|
with pytest.raises(Exception):
|
||||||
assert config["foo"] is None
|
assert config["foo"] is None
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.post_install
|
@pytest.mark.post_install
|
||||||
@pytest.mark.realconfig
|
|
||||||
def test_default_config_values(keys, config):
|
def test_default_config_values(keys, config):
|
||||||
for key in keys:
|
for key in keys:
|
||||||
assert config[key] is not None
|
assert config[key] is not None
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.post_install
|
@pytest.mark.post_install
|
||||||
@pytest.mark.realconfig
|
|
||||||
def test_contains_invalid_values(keys, config):
|
def test_contains_invalid_values(keys, config):
|
||||||
for key in config:
|
for key in config:
|
||||||
assert key in keys
|
assert key in keys
|
||||||
@@ -83,6 +81,7 @@ def test_key_conversion(legacy_config):
|
|||||||
j = convert.rc2json(legacy_config)
|
j = convert.rc2json(legacy_config)
|
||||||
j = json.loads(j)
|
j = json.loads(j)
|
||||||
keys = [
|
keys = [
|
||||||
|
"api_key",
|
||||||
"staging_dir",
|
"staging_dir",
|
||||||
"src_path",
|
"src_path",
|
||||||
"steam_path",
|
"steam_path",
|
||||||
@@ -92,7 +91,6 @@ def test_key_conversion(legacy_config):
|
|||||||
for key in keys:
|
for key in keys:
|
||||||
assert key not in j
|
assert key not in j
|
||||||
|
|
||||||
|
|
||||||
def test_missing_values(unset_values):
|
def test_missing_values(unset_values):
|
||||||
j = convert.rc2json(unset_values)
|
j = convert.rc2json(unset_values)
|
||||||
j = json.loads(j)
|
j = json.loads(j)
|
||||||
|
|||||||
@@ -1,24 +1,6 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from dzgui.api.servers import validate_ip
|
from dzgui.api.servers import validate_ip
|
||||||
from dzgui.views.components.entry import validate_ip_truthy
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"ip",
|
|
||||||
[
|
|
||||||
("foo", False),
|
|
||||||
("999", False),
|
|
||||||
("192.168.1.101", False),
|
|
||||||
("192.168.1.101:", False),
|
|
||||||
(":1", False),
|
|
||||||
("192.168.1.101:27016", True),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def test_ip_entry(ip) -> None:
|
|
||||||
string, state = ip
|
|
||||||
assert validate_ip_truthy(string) is state
|
|
||||||
|
|
||||||
|
|
||||||
def test_ip_validation() -> None:
|
def test_ip_validation() -> None:
|
||||||
ip = "192.168.1.1:100"
|
ip = "192.168.1.1:100"
|
||||||
@@ -33,7 +15,6 @@ def test_invalid_port() -> None:
|
|||||||
with pytest.raises(Exception):
|
with pytest.raises(Exception):
|
||||||
validate_ip(ip)
|
validate_ip(ip)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("port", [-1, 65536])
|
@pytest.mark.parametrize("port", [-1, 65536])
|
||||||
def test_port_out_of_range(port: int) -> None:
|
def test_port_out_of_range(port: int) -> None:
|
||||||
ip = f"192.168.1.1:{port}"
|
ip = f"192.168.1.1:{port}"
|
||||||
@@ -46,7 +27,6 @@ def test_invalid_socket() -> None:
|
|||||||
with pytest.raises(Exception):
|
with pytest.raises(Exception):
|
||||||
validate_ip(ip)
|
validate_ip(ip)
|
||||||
|
|
||||||
|
|
||||||
# TODO: ?
|
# TODO: ?
|
||||||
def test_ipdb() -> None:
|
def test_ipdb() -> None:
|
||||||
pass
|
pass
|
||||||
@@ -37,14 +37,6 @@ class RecordsListHandler(logging.Handler):
|
|||||||
"Error in directory: '/home/SENSITIVE_USERNAME'",
|
"Error in directory: '/home/SENSITIVE_USERNAME'",
|
||||||
"Error in directory: '/home/REDACTED'",
|
"Error in directory: '/home/REDACTED'",
|
||||||
),
|
),
|
||||||
(
|
|
||||||
"User directory: /home/user/.local/share/Steam/userdata/999999/grid",
|
|
||||||
"User directory: /home/REDACTED/.local/share/Steam/userdata/REDACTED/grid",
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"User directory: /drive/.local/share/Steam/userdata/999999",
|
|
||||||
"User directory: /drive/.local/share/Steam/userdata/REDACTED",
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_log_redaction(log_error: str, expect: str) -> None:
|
def test_log_redaction(log_error: str, expect: str) -> None:
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
import pytest
|
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from dzgui.api.steam import find_user_id
|
|
||||||
from tests.fixtures import fixture_path
|
|
||||||
|
|
||||||
pytestmark = pytest.mark.apitest
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"fixture, expect",
|
|
||||||
[
|
|
||||||
("api/loginusers_legacy_client.vdf", 0),
|
|
||||||
("api/loginusers_legacy_client_multiple.vdf", 1),
|
|
||||||
("api/loginusers_beta_client.vdf", 0),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def test_loginusers(monkeypatch, fixture: str, expect: int) -> None:
|
|
||||||
def mock_loginusers(path: Path) -> Path:
|
|
||||||
return fixture_path(fixture)
|
|
||||||
|
|
||||||
monkeypatch.setattr("dzgui.api.steam.find_loginusers", mock_loginusers)
|
|
||||||
uid = find_user_id(Path(""))
|
|
||||||
assert int(uid) == expect
|
|
||||||
+12
-26
@@ -1,14 +1,8 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
import dzgui.api.pefile as PeFile
|
import dzgui.api.pefile as PeFile
|
||||||
from dzgui.api.steam import (
|
from dzgui.api.steam import VDFLoadError, AppNotInstalledError, AppMovedError, get_app_path
|
||||||
VDFLoadError,
|
|
||||||
AppNotInstalledError,
|
|
||||||
AppMovedError,
|
|
||||||
get_app_path
|
|
||||||
)
|
|
||||||
|
|
||||||
from dzgui.config.query import get_config
|
from dzgui.config.query import get_config
|
||||||
from dzgui.config.xdg import get_xdg_paths, parse_filepaths
|
from dzgui.config.xdg import get_xdg_paths, parse_filepaths
|
||||||
@@ -16,7 +10,12 @@ from dzgui.const.constants import APPID_DAYZ
|
|||||||
|
|
||||||
from tests.fixtures import fixture_path
|
from tests.fixtures import fixture_path
|
||||||
|
|
||||||
pytestmark = pytest.mark.apitest
|
pytestmark = pytest.mark.pefile
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def second_drive():
|
||||||
|
return fixture_path("in_library_on_second_drive.vdf")
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@@ -27,8 +26,6 @@ def default_steam_path():
|
|||||||
return conf["default_steam_path"]
|
return conf["default_steam_path"]
|
||||||
|
|
||||||
|
|
||||||
# TODO: split into separate test file
|
|
||||||
# FIXME: use fixtures
|
|
||||||
def test_pefile_length(default_steam_path):
|
def test_pefile_length(default_steam_path):
|
||||||
try:
|
try:
|
||||||
pe_file_path = PeFile.get_pefile_path(Path(default_steam_path), APPID_DAYZ)
|
pe_file_path = PeFile.get_pefile_path(Path(default_steam_path), APPID_DAYZ)
|
||||||
@@ -55,26 +52,15 @@ def test_invalid_pefile_path():
|
|||||||
("malformed.vdf", VDFLoadError),
|
("malformed.vdf", VDFLoadError),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_not_in_library(monkeypatch, fixture, exception):
|
def test_not_in_library(fixture, exception):
|
||||||
import dzgui.api.steam as steam
|
fixture = fixture_path(fixture)
|
||||||
monkeypatch.setattr(steam, "LIBRARYFOLDERS_PATH", fixture)
|
|
||||||
|
|
||||||
folder_path = Path(fixture_path("api"))
|
|
||||||
with pytest.raises(exception):
|
with pytest.raises(exception):
|
||||||
try:
|
try:
|
||||||
get_app_path(folder_path, APPID_DAYZ)
|
get_app_path(fixture, APPID_DAYZ)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
def test_on_second_drive(second_drive):
|
||||||
def second_drive():
|
path = get_app_path(second_drive, APPID_DAYZ)
|
||||||
return "in_library_on_second_drive.vdf"
|
|
||||||
|
|
||||||
|
|
||||||
def test_on_second_drive(monkeypatch, second_drive):
|
|
||||||
import dzgui.api.steam as steam
|
|
||||||
monkeypatch.setattr(steam, "LIBRARYFOLDERS_PATH", second_drive)
|
|
||||||
folder_path = Path(fixture_path("api"))
|
|
||||||
path = get_app_path(folder_path, APPID_DAYZ)
|
|
||||||
assert path == Path("/tmp")
|
assert path == Path("/tmp")
|
||||||
@@ -13,11 +13,13 @@ def config():
|
|||||||
xdg = parse_filepaths(paths)
|
xdg = parse_filepaths(paths)
|
||||||
return get_config(xdg.config)
|
return get_config(xdg.config)
|
||||||
|
|
||||||
|
|
||||||
def test_ipdb():
|
def test_ipdb():
|
||||||
assert probe.test_ipdb()
|
assert probe.test_ipdb()
|
||||||
|
|
||||||
|
|
||||||
def test_steam(config):
|
def test_steam(config):
|
||||||
key = config["steam_api"]
|
key = config["steam_api"]
|
||||||
assert probe.test_steam_api(key)
|
assert probe.test_steam_api(key)
|
||||||
|
|
||||||
|
def test_bm(config):
|
||||||
|
key = config["bm_api"]
|
||||||
|
assert probe.test_bm_api(key)
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
import pytest
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
from _pytest.monkeypatch import MonkeyPatch
|
|
||||||
|
|
||||||
from dzgui.api.shortcuts import Shortcuts
|
|
||||||
from tests.fixtures import fixture_path
|
|
||||||
|
|
||||||
pytestmark = pytest.mark.apitest
|
|
||||||
|
|
||||||
|
|
||||||
def mock_find_shortcuts(self, steam_path: Path) -> Path:
|
|
||||||
return Path(fixture_path("api/no_shortcuts.vdf"))
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module", autouse=True)
|
|
||||||
def patch_api() -> None:
|
|
||||||
mp = MonkeyPatch()
|
|
||||||
mp.setattr("dzgui.api.shortcuts.Shortcuts.find_shortcuts_path", mock_find_shortcuts)
|
|
||||||
yield
|
|
||||||
mp.undo()
|
|
||||||
|
|
||||||
|
|
||||||
def test_no_shortcuts(monkeypatch) -> None:
|
|
||||||
s = Shortcuts(Path(""))
|
|
||||||
assert len(s.shortcuts["shortcuts"]) == 0
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def dummy_app() -> None:
|
|
||||||
d = {
|
|
||||||
"AppName": "TEST APP",
|
|
||||||
"StartDir": "TEST_DIR",
|
|
||||||
"Exe": "TEST_DIR/TEST_EXE.EXE",
|
|
||||||
"icon": "IMAGES_DIR/TEST_IMAGE.PNG",
|
|
||||||
}
|
|
||||||
return d
|
|
||||||
|
|
||||||
|
|
||||||
def test_wrap_exe(dummy_app: dict[str, str]) -> None:
|
|
||||||
s = Shortcuts(Path(""))
|
|
||||||
s.add_shortcut(*dummy_app.values())
|
|
||||||
assert s.shortcuts["shortcuts"]["0"]["Exe"][0] == '"'
|
|
||||||
assert s.shortcuts["shortcuts"]["0"]["Exe"][-1] == '"'
|
|
||||||
|
|
||||||
|
|
||||||
def test_add_shortcut(dummy_app: dict[str, str]) -> None:
|
|
||||||
s = Shortcuts(Path(""))
|
|
||||||
s.add_shortcut(*dummy_app.values())
|
|
||||||
new = s.shortcuts["shortcuts"]
|
|
||||||
ind = str(len(new) - 1)
|
|
||||||
for k, v in dummy_app.items():
|
|
||||||
if k == "Exe":
|
|
||||||
v = f'"{v}"'
|
|
||||||
assert new[ind][k] == v
|
|
||||||
|
|
||||||
|
|
||||||
def test_save_shortcut(dummy_app: dict[str, str]) -> None:
|
|
||||||
s = Shortcuts(Path(""))
|
|
||||||
s.add_shortcut(*dummy_app.values())
|
|
||||||
with tempfile.NamedTemporaryFile() as f:
|
|
||||||
tmp = Path(f.name)
|
|
||||||
s.shortcuts_path = tmp
|
|
||||||
s.save_shortcuts()
|
|
||||||
s._load_shortcuts(tmp)
|
|
||||||
assert len(s.shortcuts["shortcuts"]) == 1
|
|
||||||
|
|
||||||
|
|
||||||
def test_shortcut_crc(dummy_app: dict[str, str]) -> None:
|
|
||||||
s = Shortcuts(Path(""))
|
|
||||||
s.add_shortcut(*dummy_app.values())
|
|
||||||
for key in s.shortcuts["shortcuts"].keys():
|
|
||||||
entry = s.shortcuts["shortcuts"][key]
|
|
||||||
name = entry["AppName"]
|
|
||||||
exe = entry["Exe"]
|
|
||||||
uid = name + exe
|
|
||||||
bpid = s.gen_bpid(uid)
|
|
||||||
assert entry["appid"] & 0xFFFFFFFF == bpid
|
|
||||||
|
|
||||||
|
|
||||||
def test_reverse_crc(dummy_app: dict[str, str]) -> None:
|
|
||||||
s = Shortcuts(Path(""))
|
|
||||||
s.add_shortcut(*dummy_app.values())
|
|
||||||
for key in s.shortcuts["shortcuts"].keys():
|
|
||||||
entry = s.shortcuts["shortcuts"][key]
|
|
||||||
name = entry["AppName"]
|
|
||||||
exe = entry["Exe"]
|
|
||||||
uid = name + exe
|
|
||||||
bpid = s.gen_bpid(uid)
|
|
||||||
assert s.find_appname_by_unsigned_id(bpid) == name
|
|
||||||
Reference in new issue
Block a user