Compare commits

..
Author SHA1 Message Date
aclist a97766855d chore: dclear typehinting errors
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
2026-11-05 21:27:07 +09:00
aclist 3097701e98 change: drop explicit import 2026-11-05 21:26:37 +09:00
aclist 895e08d228 fix: error handling 2026-11-05 21:26:20 +09:00
aclist e7cef215c6 drop: resolved comment
Mirror to Codeberg / mirror-to-codeberg (push) Canceled after 0s
2026-11-02 12:19:10 +09:00
aclist 90ef6cbdbb chore: add tests 2026-11-02 12:17:04 +09:00
aclist 0ec1c157f4 chore: clear typehinting errors 2026-11-01 23:45:01 +09:00
aclist 0cf6439538 chore: remove unused imports 2026-11-01 23:43:12 +09:00
aclist e3c1ef59ee feat: get last updated timestamp 2026-11-01 23:42:06 +09:00
13 changed files with 187 additions and 117 deletions

No files matched your search

+28 -3
View File
@@ -1,15 +1,18 @@
import logging
import re
from collections.abc import Iterator
from datetime import datetime
from pathlib import Path
from typing import Any
from typing import Any, TYPE_CHECKING
from warnings import deprecated
from dzgui.const.constants import APP_NAME
logger = logging.getLogger(APP_NAME)
if TYPE_CHECKING:
from collections.abc import Iterator
class ACF:
def __init__(self, file: Path) -> None:
@@ -42,7 +45,7 @@ class ACF:
def dequote(cls, s: str) -> str:
return s.rstrip('"').lstrip('"')
def parse(self, lines: Iterator[list[str]]) -> dict[str, str]:
def parse(self, lines: "Iterator[list[str]]") -> dict[str, str]:
acf: dict[str, Any] = {}
try:
while True:
@@ -69,6 +72,28 @@ class ACF:
except StopIteration:
return acf
def search_key(self, key: str) -> str:
return str(self.dict["AppState"][key])
def date_to_str(self, epoch: int) -> str | None:
if epoch == 0:
return None
return datetime.fromtimestamp(epoch).isoformat(" ")
def get_date_by_key(self, key: str) -> str | None:
try:
stamp = int(self.search_key(key))
return self.date_to_str(stamp)
except Exception as e:
logger.critical(e)
return None
def get_last_updated(self) -> str | None:
return self.get_date_by_key("LastUpdated")
def get_last_played(self) -> str | None:
return self.get_date_by_key("LastPlayed")
def get_allows_downloads(self) -> int | None:
try:
# TODO: consider coercing strs to ints on initial import
+20
View File
@@ -389,6 +389,26 @@ def get_running_app() -> int | None:
return None
def get_last_updated(path: Path, appid: int) -> str | None:
root_path = get_app_path(path, appid)
acf = root_path.joinpath(f"steamapps/appmanifest_{appid}.acf")
try:
return ACF(acf).get_last_updated()
except Exception as e:
logger.critical(e)
return None
def get_last_played(path: Path, appid: int) -> str | None:
root_path = get_app_path(path, appid)
acf = root_path.joinpath(f"steamapps/appmanifest_{appid}.acf")
try:
return ACF(acf).get_last_played()
except Exception as e:
logger.critical(e)
return None
def get_app_allows_downloads(path: Path, appid: int) -> bool:
try:
root_path = get_app_path(path, appid)
+3 -3
View File
@@ -121,7 +121,7 @@ def load_steam_icon() -> None:
logger.critical(e)
def build_gui(version: str, is_debug: bool) -> App | None:
def load_gui(version: str, is_debug: bool) -> None:
lock = lock_acquire() # noqa
set_locale()
@@ -151,7 +151,7 @@ def build_gui(version: str, is_debug: bool) -> App | None:
wizard = SetupWizard(_is_steam_deck, XDG, xdg_paths)
if not wizard.is_setup_complete():
return None
return
setup_logger(XDG.debug)
with open(XDG.debug, "w") as f:
@@ -186,4 +186,4 @@ def build_gui(version: str, is_debug: bool) -> App | None:
print(boot.all_ok)
load_steam_icon()
app = App(prefs)
return app
app.run(None)
+2 -5
View File
@@ -42,9 +42,6 @@ def main() -> None:
test_libgi_missing()
# NOTE: only import ligbirepository modules after the above check
from dzgui.app_init import build_gui
from dzgui.app_init import load_gui
app = build_gui(version, args.debug)
if app is None:
sys.exit(0)
app.run(None)
load_gui(version, args.debug)
-80
View File
@@ -1,80 +0,0 @@
from pathlib import Path
from typing import cast, TYPE_CHECKING
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, Gdk, GLib # noqa E402
if TYPE_CHECKING:
from dzgui.controllers.mc import Controller
from dzgui.views.base import OuterWindow
from dzgui.views.mixins.context_mixin import ContextMixin
from gi.repository import GdkPixbuf
# TODO: randomize/mock PII distance values
class IdleScreenshot:
def __init__(
self, controller: "Controller", window: "OuterWindow", scrot_path: Path
) -> None:
self.window = window
self.controller = controller
self.scrot_path = scrot_path
clock = window.get_frame_clock()
if clock is None:
raise TypeError("Widget not realized, FrameClock not initialized")
clock.connect("after-paint", self.callback)
def callback(self, clock: Gdk.FrameClock) -> None:
GLib.timeout_add(1000, self.check_idle)
def check_idle(self) -> bool:
tv = self.controller.get_active_treeview()
loaded = tv.is_loaded()
if loaded is True:
self.emit_key_press(tv)
GLib.timeout_add(1000, self.save_and_quit)
return True
return False
def emit_key_press(self, widget: "ContextMixin") -> None:
window = widget.get_parent_window()
if window is None:
raise TypeError(f"Failed to find a valid parent window for {widget}")
event = Gdk.EventKey.new(Gdk.EventType.KEY_PRESS)
# NOTE: GTK introspection returns Gdk.Event type
event_key = cast(Gdk.EventKey, event)
event_key.keyval = Gdk.KEY_l
event_key.state = Gdk.ModifierType.CONTROL_MASK
event_key.window = window
event_key.time = Gdk.CURRENT_TIME
event_key.send_event = True
# TODO: suppress errors from synthetic key events
widget.present_menu(widget, event)
def save_and_quit(self) -> None:
self.save_pixbuf()
self.controller.save_res_and_quit()
def save_pixbuf(self) -> None:
pixbuf = self.capture_application()
if pixbuf:
pixbuf.savev(str(self.scrot_path), "png", [], [])
def capture_application(self) -> "GdkPixbuf.Pixbuf":
# NOTE: composited context menu over X11 window
root = Gdk.get_default_root_window()
xwindow = self.window.get_window()
if xwindow is None:
raise ValueError("Failed to get a valid X11 window")
index, x, y = xwindow.get_origin()
width, height = self.window.get_size()
application = Gdk.pixbuf_get_from_window(root, x, y, width, height)
if application is None:
raise ValueError("Failed to get a valid GdkPixbuf")
return application
+2 -20
View File
@@ -1,7 +1,7 @@
import logging
import signal
from typing import Any, Literal, Self, TYPE_CHECKING
from typing import TYPE_CHECKING, Literal, Self
from dzgui.const.constants import (
APP_NAME,
@@ -47,7 +47,7 @@ logger = logging.getLogger(APP_NAME)
class OuterWindow(Gtk.ApplicationWindow):
def __init__(self, app: "App") -> None:
def __init__(self, app: Gtk.Application) -> None:
super().__init__(
application=app, title=APP_NAME, border_width=10, icon_name=APP_NAME_LOWER
)
@@ -78,15 +78,6 @@ class OuterWindow(Gtk.ApplicationWindow):
MainController.loaded = True
MainController.populate_model(MainController.get_active_treeview())
try:
from dzgui.util.save_screenshot import IdleScreenshot
scrot = app.get_automation_param("screenshot_path")
self.fullscreen()
IdleScreenshot(MainController, self, scrot)
except KeyError:
pass
def _on_delete_event(self, window: "OuterWindow", event: Gdk.EventKey) -> None:
self.halt_proc_and_quit()
@@ -300,14 +291,6 @@ class App(ColorAwareApp, Gtk.Application): # type: ignore
self._setup_signals()
self.connect("activate", self._on_activate)
self.automation_params: dict[str, Any] = {}
def get_automation_param(self, key: str) -> Any:
return self.automation_params[key]
def set_automation_param(self, key: str, value: Any) -> None:
self.automation_params[key] = value
def _on_activate(self, app: Self) -> None:
self.win = OuterWindow(app)
@@ -348,5 +331,4 @@ class App(ColorAwareApp, Gtk.Application): # type: ignore
) -> None:
self.win.halt_proc_and_quit()
MainController = Controller()
+3 -1
View File
@@ -16,7 +16,9 @@ logger = logging.getLogger(APP_NAME)
class ContextMixin(TreeView):
def present_menu(
self, widget: Gtk.Widget, event: Gdk.Event
self,
widget: Gtk.Widget,
event: Gdk.EventButton | Gdk.EventKey,
) -> bool:
if self.menu is None:
+13 -2
View File
@@ -2,6 +2,7 @@ from pathlib import Path
from typing import Callable, TYPE_CHECKING
from dzgui.api import pefile as PeFile
from dzgui.api.steam import get_last_updated
from dzgui.config import query
from dzgui.const.constants import (
@@ -340,8 +341,10 @@ class Options(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore
[LeftLabel(options.flatpak_label), self.flatpak_toggle],
)
self.dayz_version_label = Gtk.Label(label=strings.null)
self.dayz_exp_version_label = Gtk.Label(label=strings.null)
self.dayz_version_label = Gtk.Label(label=strings.null, halign=Gtk.Align.START)
self.dayz_exp_version_label = Gtk.Label(
label=strings.null, halign=Gtk.Align.START
)
version_rows = [
[LeftLabel(APPNAME_DAYZ), self.dayz_version_label],
@@ -467,10 +470,18 @@ class Options(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore
dayz_version = PeFile.get_pretty_version(steam_path, APPID_DAYZ)
if dayz_version is None:
dayz_version = strings.null
else:
last_updated = get_last_updated(steam_path, APPID_DAYZ)
if last_updated is not None:
dayz_version += f" (updated at {last_updated})"
dayz_exp_version = PeFile.get_pretty_version(steam_path, APPID_DAYZ_EXP)
if dayz_exp_version is None:
dayz_exp_version = strings.null
else:
last_updated = get_last_updated(steam_path, APPID_DAYZ_EXP)
if last_updated is not None:
dayz_exp_version += f" (updated at {last_updated})"
self.dayz_version_label.set_text(dayz_version)
self.dayz_exp_version_label.set_text(dayz_exp_version)
-1
View File
@@ -3,7 +3,6 @@ from pathlib import Path
root = Path(__file__).resolve().parents[1]
def get_entrypoint() -> None:
p = root.joinpath("pyproject.toml")
with open(p, "rb") as f:
+53
View File
@@ -0,0 +1,53 @@
"AppState"
{
"appid" "444"
"Universe" "1"
"name" "TEST APP"
"StateFlags" "4"
"installdir" "TESTAPP"
"LastUpdated" "1792595373"
"LastPlayed" "0"
"SizeOnDisk" "0"
"StagingSize" "0"
"buildid" "0"
"LastOwner" "0"
"DownloadType" "2"
"UpdateResult" "0"
"BytesToDownload" "0"
"BytesDownloaded" "0"
"BytesToStage" "0"
"BytesStaged" "0"
"TargetBuildID" "0"
"AutoUpdateBehavior" "0"
"AllowOtherDownloadsWhileRunning" "0"
"ScheduledAutoUpdate" "0"
"FullValidateAfterNextUpdate" "1"
"InstalledDepots"
{
"3330"
{
"manifest" "0"
"size" "0"
}
}
"SharedDepots"
{
"228983" "228980"
"228985" "228980"
"228988" "228980"
"228990" "228980"
"229004" "228980"
}
"UserConfig"
{
"language" "english"
"platform_override_dest" "linux"
"platform_override_source" "windows"
}
"MountedConfig"
{
"language" "english"
"platform_override_dest" "linux"
"platform_override_source" "windows"
}
}
+53
View File
@@ -0,0 +1,53 @@
"AppState"
{
"appid" "555"
"Universe" "1"
"name" "TEST APP"
"StateFlags" "4"
"installdir" "TESTAPP"
"LastUpdated" "0"
"LastPlayed" "0"
"SizeOnDisk" "0"
"StagingSize" "0"
"buildid" "0"
"LastOwner" "0"
"DownloadType" "2"
"UpdateResult" "0"
"BytesToDownload" "0"
"BytesDownloaded" "0"
"BytesToStage" "0"
"BytesStaged" "0"
"TargetBuildID" "0"
"AutoUpdateBehavior" "0"
"AllowOtherDownloadsWhileRunning" "0"
"ScheduledAutoUpdate" "0"
"FullValidateAfterNextUpdate" "1"
"InstalledDepots"
{
"3330"
{
"manifest" "0"
"size" "0"
}
}
"SharedDepots"
{
"228983" "228980"
"228985" "228980"
"228988" "228980"
"228990" "228980"
"229004" "228980"
}
"UserConfig"
{
"language" "english"
"platform_override_dest" "linux"
"platform_override_source" "windows"
}
"MountedConfig"
{
"language" "english"
"platform_override_dest" "linux"
"platform_override_source" "windows"
}
}
+9 -1
View File
@@ -3,7 +3,7 @@ import pytest
from _pytest.monkeypatch import MonkeyPatch
from pathlib import Path
from dzgui.api.steam import get_app_allows_downloads
from dzgui.api.steam import get_app_allows_downloads, get_last_updated
from tests.fixtures import fixture_path
pytestmark = pytest.mark.apitest
@@ -43,6 +43,14 @@ def patch_api() -> None:
mp.undo()
def test_no_last_update(steam_path) -> None:
assert get_last_updated(steam_path, 555) is None
def test_last_update(steam_path) -> None:
assert get_last_updated(steam_path, 444) == "2026-10-22 00:09:33"
def test_app_allows_downloads(steam_path) -> None:
assert get_app_allows_downloads(steam_path, 111) is True