Compare commits

...
Author SHA1 Message Date
aclist 2f92c2a44f chore: clear typehinting errors 2026-10-22 10:12:38 +09:00
aclist da48834fa7 feat: load Steam icon into search paths 2026-10-22 10:11:55 +09:00
aclist 197c2eba06 fix: always show icon 2026-10-22 10:11:13 +09:00
aclist 3c52072eb5 Merge pull request #493 from aclist/feat/paused-downloads
Mirror to Codeberg / mirror-to-codeberg (push) Canceled after 0s
feat: paused downloads
2026-10-21 10:26:51 +09:00
aclist bf69936891 Merge pull request #495 from aclist/fix/vdf-timestamp
fix: support changes to VDF timestamp
2026-10-21 10:22:35 +09:00
aclist fa180b750a Merge pull request #494 from aclist/change/unmodded-filter
change: separate modded into unmodded
2026-10-21 10:20:46 +09:00
aclist 96d50ffafa chore: resolve pytest errors 2026-10-18 07:44:31 +09:00
aclist 6c801ffec5 chore: update tests 2026-10-18 07:44:14 +09:00
aclist 5eb70a1eb8 fix: support changes to VDF format 2026-10-18 07:35:17 +09:00
aclist a3fff40f8f change: separate modded into unmodded 2026-10-18 01:54:59 +09:00
aclist 343630d2da chore: update placeholder appid 2026-10-18 01:52:43 +09:00
aclist d63c7ce3ec chore: add test 2026-10-18 01:50:10 +09:00
aclist f7ad357e6f feat: print paused download warning on preconnect 2026-10-18 01:49:14 +09:00
aclist 035a01fe34 feat: check for pending patches 2026-10-18 01:48:42 +09:00
aclist 7dd14804aa Merge pull request #491 from aclist/fix/dword-parse
Mirror to Codeberg / mirror-to-codeberg (push) Canceled after 0s
fix: dword parse
2026-10-16 00:09:26 +09:00
19 changed files with 117 additions and 30 deletions

No files matched your search

+21 -3
View File
@@ -1,13 +1,14 @@
import logging import logging
import os import os
import psutil import psutil
import re
import requests import requests
import subprocess import subprocess
import vdf # type: ignore import vdf # type: ignore
from pathlib import Path from pathlib import Path
from requests.exceptions import HTTPError from requests.exceptions import HTTPError
from typing import Any, Union from typing import Any, Generator, Union
from warnings import deprecated from warnings import deprecated
from dzgui.api.acf import ACF from dzgui.api.acf import ACF
@@ -20,6 +21,7 @@ from dzgui.const.constants import (
DAYZ_LAUNCHER, DAYZ_LAUNCHER,
DEBIAN_STEAM_PATH, DEBIAN_STEAM_PATH,
DEFAULT_STEAM_PATH, DEFAULT_STEAM_PATH,
DOWNLOADS_PATH,
FLATPAK_STEAM_PATH, FLATPAK_STEAM_PATH,
LIBRARYFOLDERS_PATH, LIBRARYFOLDERS_PATH,
UBUNTU_STEAM_PATH, UBUNTU_STEAM_PATH,
@@ -245,9 +247,9 @@ def find_user_id(path: Path) -> str | None:
last_user = str(next(iter(users))) last_user = str(next(iter(users)))
if len(users) == 1: if len(users) == 1:
return last_user return last_user
last_stamp = int(users[last_user]["Timestamp"]) last_stamp = int(users[last_user]["timestamp"])
for user in users: for user in users:
stamp = int(users[user]["Timestamp"]) stamp = int(users[user]["timestamp"])
if stamp > last_stamp: if stamp > last_stamp:
last_stamp = stamp last_stamp = stamp
last_user = user last_user = user
@@ -439,6 +441,22 @@ def is_dayz_running() -> bool:
return any(s in item for sublist in procs for item in sublist for s in substring) return any(s in item for sublist in procs for item in sublist for s in substring)
def get_pending_downloads(steam_path: Path, appid: int) -> Generator[Path, None, None]:
root_path = get_app_path(steam_path, appid)
downloads_dir = root_path / DOWNLOADS_PATH
files = downloads_dir.iterdir()
return files
def is_dayz_downloading(steam_path: Path, appid: int) -> bool:
files = get_pending_downloads(steam_path, appid)
reg = re.compile(rf"^state_(?:{APPID_DAYZ_EXP}|{APPID_DAYZ})_.*\.patch$")
pending = [f for f in files if reg.match(f.name)]
if len(pending) > 0:
return True
return False
def get_app_path(folders_path: Path, appid: int) -> Path: def get_app_path(folders_path: Path, appid: int) -> Path:
app_path = None app_path = None
+28 -2
View File
@@ -1,10 +1,13 @@
import logging import logging
import os import os
import shutil import shutil
from pathlib import Path
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from warnings import deprecated
from dzgui.const.constants import APP_NAME from dzgui.const.constants import APP_NAME, STEAM_ICON, STEAM_ICONS_PATH
from dzgui.const.enum import Preferences from dzgui.const.enum import Preferences
from dzgui.config.query import lookup from dzgui.config.query import lookup
from dzgui.config.userprefs import UserPrefs from dzgui.config.userprefs import UserPrefs
@@ -23,7 +26,7 @@ from dzgui.util.dirs import make_parents
from dzgui.util.localize import set_locale from dzgui.util.localize import set_locale
from dzgui.util.map_count import test_map_count from dzgui.util.map_count import test_map_count
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, steam_icon_missing
from dzgui.views.base import App from dzgui.views.base import App
from dzgui.views.dialogs.boot import BootWindow from dzgui.views.dialogs.boot import BootWindow
@@ -96,6 +99,28 @@ def copy_bare_configs(config: "Path", resolution: "Path") -> tuple[bool, bool]:
return (config_changed, state_changed) return (config_changed, state_changed)
@deprecated("Currently unused")
def load_steam_icon(config: Path) -> None:
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk # noqa
default = lookup(config, Preferences.DEFAULT)
icon_path = Path(default).joinpath(STEAM_ICONS_PATH)
try:
theme = Gtk.IconTheme.get_default()
paths = theme.get_search_path()
theme.set_search_path(paths + [str(icon_path)])
paths = theme.get_search_path()
icons = theme.list_icons(None)
if STEAM_ICON not in icons:
logger.warning(steam_icon_missing)
except Exception as e:
print(e)
logger.warning(e)
def load_gui(version: str, is_debug: bool) -> None: def load_gui(version: str, is_debug: bool) -> None:
lock = lock_acquire() # noqa lock = lock_acquire() # noqa
@@ -143,6 +168,7 @@ def load_gui(version: str, is_debug: bool) -> None:
local_coords, latest_release = bootwin.get_results() local_coords, latest_release = bootwin.get_results()
use_miles = lookup(XDG.config, Preferences.DIST) use_miles = lookup(XDG.config, Preferences.DIST)
prefs = UserPrefs( prefs = UserPrefs(
is_steam_deck=_is_steam_deck, is_steam_deck=_is_steam_deck,
is_game_mode=_is_game_mode, is_game_mode=_is_game_mode,
+2
View File
@@ -15,6 +15,7 @@ DAYZ_BINARY = "DayZ_x64.exe"
DAYZ_LAUNCHER = "DayZLauncher.exe" DAYZ_LAUNCHER = "DayZLauncher.exe"
LIBRARYFOLDERS_PATH = "steamapps/libraryfolders.vdf" LIBRARYFOLDERS_PATH = "steamapps/libraryfolders.vdf"
DOWNLOADS_PATH = "steamapps/downloading"
WORKSHOP_PATH = "steamapps/workshop/content/" + str(APPID_DAYZ) WORKSHOP_PATH = "steamapps/workshop/content/" + str(APPID_DAYZ)
STEAM_CMD = "steam" STEAM_CMD = "steam"
@@ -87,6 +88,7 @@ DEFAULT_STEAM_PATH = ".local/share/Steam"
FLATPAK_STEAM_PATH = ".var/app/com.valvesoftware.Steam/data/Steam" FLATPAK_STEAM_PATH = ".var/app/com.valvesoftware.Steam/data/Steam"
UBUNTU_STEAM_PATH = ".steam/steam" UBUNTU_STEAM_PATH = ".steam/steam"
DEBIAN_STEAM_PATH = ".steam/debian-installation" DEBIAN_STEAM_PATH = ".steam/debian-installation"
STEAM_ICONS_PATH = "public"
LOG_FILTERS = ("CRITICAL", "WARNING", "INFO", "DEBUG") LOG_FILTERS = ("CRITICAL", "WARNING", "INFO", "DEBUG")
+4
View File
@@ -14,6 +14,7 @@ from dzgui.api.shortcuts import Shortcuts
from dzgui.api.steam import ( from dzgui.api.steam import (
connect, connect,
connect_debug, connect_debug,
is_dayz_downloading,
get_app_allows_downloads, get_app_allows_downloads,
get_app_name, get_app_name,
get_needs_update, get_needs_update,
@@ -84,6 +85,7 @@ class Prerequisites:
available_space: float available_space: float
passworded: bool passworded: bool
dayz_running: bool dayz_running: bool
download_paused: bool
steam_proc: SteamProcess steam_proc: SteamProcess
mods: list[list[str]] mods: list[list[str]]
game_mode: bool game_mode: bool
@@ -169,6 +171,7 @@ class ConnectionManager:
free_mib = format_mib(free) free_mib = format_mib(free)
dayz_running = is_dayz_running() dayz_running = is_dayz_running()
download_paused = is_dayz_downloading(steam_path, self.appid)
running_app = get_running_app() running_app = get_running_app()
if running_app is not None: if running_app is not None:
@@ -210,6 +213,7 @@ class ConnectionManager:
available_space=free_mib, available_space=free_mib,
passworded=info.password_protected, passworded=info.password_protected,
dayz_running=dayz_running, dayz_running=dayz_running,
download_paused=download_paused,
steam_proc=steam_proc, steam_proc=steam_proc,
mods=remote_mods, mods=remote_mods,
game_mode=game_mode, game_mode=game_mode,
+4 -2
View File
@@ -22,11 +22,12 @@ class FilterManager:
strings.filter_night: True, strings.filter_night: True,
strings.filter_full: show_full, strings.filter_full: show_full,
strings.filter_official: True, strings.filter_official: True,
strings.filter_nonascii: False, strings.filter_modded: True,
strings.filter_lowpop: True, strings.filter_lowpop: True,
strings.filter_unofficial: True, strings.filter_unofficial: True,
strings.filter_unmodded: True,
strings.filter_duplicate: False, strings.filter_duplicate: False,
strings.filter_modded: True, strings.filter_nonascii: False,
} }
# TODO: strings # TODO: strings
@@ -43,6 +44,7 @@ class FilterManager:
strings.filter_unofficial: "Third-party servers", strings.filter_unofficial: "Third-party servers",
strings.filter_duplicate: "Duplicates of existing servers (usually spoofed)", strings.filter_duplicate: "Duplicates of existing servers (usually spoofed)",
strings.filter_modded: "Servers with one or more mods", strings.filter_modded: "Servers with one or more mods",
strings.filter_unmodded: "Servers with no mods",
} }
self.active_keyword = "" self.active_keyword = ""
+2
View File
@@ -243,6 +243,8 @@ class ProxyModelManager:
rows = [row for row in rows if (row[4] / row[5] * 100) > 30] rows = [row for row in rows if (row[4] / row[5] * 100) > 30]
case strings.filter_modded: case strings.filter_modded:
rows = [row for row in rows if not row[11]] rows = [row for row in rows if not row[11]]
case strings.filter_unmodded:
rows = [row for row in rows if row[11]]
return rows return rows
def filter_toggle_on(self, filters: tuple[str]) -> list: def filter_toggle_on(self, filters: tuple[str]) -> list:
+4
View File
@@ -47,3 +47,7 @@ running_app = (
"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"
"Otherwise, mods may be queued for download but never update." "Otherwise, mods may be queued for download but never update."
) )
downloads_paused = (
"It looks like DayZ may have a pending update on Steam that was paused.\n"
"To enqueue mods for download, unpause any pending DayZ update and allow it to complete."
)
+1
View File
@@ -156,6 +156,7 @@ filter_duplicate = "Duplicate"
filter_official = "Official" filter_official = "Official"
filter_unofficial = "Unofficial" filter_unofficial = "Unofficial"
filter_modded = "Modded" filter_modded = "Modded"
filter_unmodded = "Unmodded"
# maps # maps
all_maps = "All maps" all_maps = "All maps"
+1 -11
View File
@@ -7,12 +7,11 @@ from dzgui.const.constants import (
APP_NAME, APP_NAME,
APP_NAME_LOWER, APP_NAME_LOWER,
GTK_APP_ID, GTK_APP_ID,
STEAM_ICON,
) )
from dzgui.const.enum import NotebookPage from dzgui.const.enum import NotebookPage
from dzgui.controllers.emitter import Emitter from dzgui.controllers.emitter import Emitter
from dzgui.controllers.mc import Controller from dzgui.controllers.mc import Controller
from dzgui.util import css, strings from dzgui.util import css
from dzgui.util.keys import is_ctrl_mask from dzgui.util.keys import is_ctrl_mask
from dzgui.views.components.connect_panel import ConnectPanel from dzgui.views.components.connect_panel import ConnectPanel
from dzgui.views.components.crumbs import Breadcrumbs from dzgui.views.components.crumbs import Breadcrumbs
@@ -332,13 +331,4 @@ class App(ColorAwareApp, Gtk.Application): # type: ignore
) -> None: ) -> None:
self.win.halt_proc_and_quit() self.win.halt_proc_and_quit()
MainController = Controller() MainController = Controller()
try:
theme = Gtk.IconTheme.get_default()
icons = theme.list_icons(None)
if STEAM_ICON not in icons:
logger.warning(strings.steam_icon_missing)
except Exception as e:
logger.warning(e)
+1
View File
@@ -65,6 +65,7 @@ class IconButton(Gtk.Button):
self.set_image(self.icon) self.set_image(self.icon)
self.set_image_position(position) self.set_image_position(position)
self.set_focus_on_click(False) self.set_focus_on_click(False)
self.set_always_show_image(True)
def swap_icon(self, icon: str) -> None: def swap_icon(self, icon: str) -> None:
start = self.icon.get_margin_start() start = self.icon.get_margin_start()
+2
View File
@@ -279,6 +279,8 @@ class PreConnectionAssistant(Gtk.Box):
warnings.append(preconnect.protected_server) warnings.append(preconnect.protected_server)
if prereqs.dayz_running is True: if prereqs.dayz_running is True:
warnings.append(preconnect.dayz_running) warnings.append(preconnect.dayz_running)
if len(prereqs.mods) > 0 and prereqs.download_paused:
warnings.append(preconnect.downloads_paused)
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 len(prereqs.mods) > 0 and allows_dl is False:
+1 -1
View File
@@ -8,6 +8,6 @@
"WantsOfflineMode" "0" "WantsOfflineMode" "0"
"SkipOfflineModeWarning" "0" "SkipOfflineModeWarning" "0"
"AutoLogin" "1" "AutoLogin" "1"
"Timestamp" "0" "timestamp" "0"
} }
} }
+2 -2
View File
@@ -8,7 +8,7 @@
"WantsOfflineMode" "0" "WantsOfflineMode" "0"
"SkipOfflineModeWarning" "0" "SkipOfflineModeWarning" "0"
"AutoLogin" "1" "AutoLogin" "1"
"Timestamp" "1787904411" "timestamp" "1787904411"
} }
"1" "1"
{ {
@@ -18,6 +18,6 @@
"WantsOfflineMode" "0" "WantsOfflineMode" "0"
"SkipOfflineModeWarning" "0" "SkipOfflineModeWarning" "0"
"AutoLogin" "1" "AutoLogin" "1"
"Timestamp" "1787904647" "timestamp" "1787904647"
} }
} }
+2 -2
View File
@@ -8,7 +8,7 @@
"WantsOfflineMode" "0" "WantsOfflineMode" "0"
"SkipOfflineModeWarning" "0" "SkipOfflineModeWarning" "0"
"AutoLogin" "1" "AutoLogin" "1"
"Timestamp" "1797904411" "timestamp" "1797904411"
} }
"1" "1"
{ {
@@ -18,6 +18,6 @@
"WantsOfflineMode" "0" "WantsOfflineMode" "0"
"SkipOfflineModeWarning" "0" "SkipOfflineModeWarning" "0"
"AutoLogin" "1" "AutoLogin" "1"
"Timestamp" "1787904647" "timestamp" "1787904647"
} }
} }
+1 -1
View File
@@ -9,6 +9,6 @@
"SkipOfflineModeWarning" "0" "SkipOfflineModeWarning" "0"
"AllowAutoLogin" "1" "AllowAutoLogin" "1"
"MostRecent" "1" "MostRecent" "1"
"Timestamp" "0" "timestamp" "0"
} }
} }
+2 -2
View File
@@ -9,7 +9,7 @@
"SkipOfflineModeWarning" "0" "SkipOfflineModeWarning" "0"
"AllowAutoLogin" "1" "AllowAutoLogin" "1"
"MostRecent" "0" "MostRecent" "0"
"Timestamp" "0" "timestamp" "0"
} }
"1" "1"
{ {
@@ -20,6 +20,6 @@
"SkipOfflineModeWarning" "0" "SkipOfflineModeWarning" "0"
"AllowAutoLogin" "1" "AllowAutoLogin" "1"
"MostRecent" "1" "MostRecent" "1"
"Timestamp" "10" "timestamp" "10"
} }
} }
+3 -4
View File
@@ -10,7 +10,7 @@ gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, Gio, GLib # noqa from gi.repository import Gtk, Gio, GLib # noqa
class TestWidget(ColorAwareApp, Gtk.Button): class DummyWidget(ColorAwareApp, Gtk.Button):
def __init__(self) -> None: def __init__(self) -> None:
super().__init__() super().__init__()
@@ -22,11 +22,10 @@ class TestWidget(ColorAwareApp, Gtk.Button):
@pytest.fixture @pytest.fixture
def widget() -> TestWidget: def widget() -> DummyWidget:
return TestWidget() return DummyWidget()
@pytest.mark.integration
@pytest.mark.parametrize("key, expect", [("default", False), ("prefer-dark", True)]) @pytest.mark.parametrize("key, expect", [("default", False), ("prefer-dark", True)])
def test_light_mode(widget, key: str, expect: bool) -> None: def test_light_mode(widget, key: str, expect: bool) -> None:
local_pref = subprocess.run( local_pref = subprocess.run(
+9
View File
@@ -3,6 +3,7 @@ import pytest
from pathlib import Path from pathlib import Path
from dzgui.api.steam import find_user_id from dzgui.api.steam import find_user_id
from dzgui.const.constants import DEFAULT_STEAM_PATH
from tests.fixtures import fixture_path from tests.fixtures import fixture_path
pytestmark = pytest.mark.apitest pytestmark = pytest.mark.apitest
@@ -25,3 +26,11 @@ def test_loginusers(monkeypatch, fixture: str, expect: int) -> None:
monkeypatch.setattr("dzgui.api.steam.find_loginusers", mock_loginusers) monkeypatch.setattr("dzgui.api.steam.find_loginusers", mock_loginusers)
uid = find_user_id(Path("")) uid = find_user_id(Path(""))
assert int(uid) == expect assert int(uid) == expect
@pytest.mark.realconfig
def test_live_loginusers() -> None:
"""Added on 2026-10-17 following changes to VDF keys"""
steam = Path.home().joinpath(DEFAULT_STEAM_PATH)
uid = find_user_id(steam)
assert uid is not None
+27
View File
@@ -0,0 +1,27 @@
import pytest
from pathlib import Path
from dzgui.api.steam import is_dayz_downloading
@pytest.mark.parametrize(
"file_list, expect",
[
(["state_221100_1111.patch"], True),
(["state_730_1111.patch"], False),
(["state_1024020_1111.patch"], True),
(["221100"], False),
(["221100", "state_221100_1111.patch"], True),
(["state_221100_1111.patch", "state_1024020_1111.patch"], True),
(["state_221100_1111"], False),
],
)
def test_dayz_patch_pending(monkeypatch, file_list, expect) -> None:
def mock_get_pending_downloads(steam_path: Path, appid: int) -> None:
files = [Path(file) for file in file_list]
return files
monkeypatch.setattr(
"dzgui.api.steam.get_pending_downloads", mock_get_pending_downloads
)
assert is_dayz_downloading(Path("/tmp"), Path("/tmp")) == expect