Compare commits

..
Author SHA1 Message Date
aclist ae13dc9a89 chore: add newline 2026-11-05 21:14:27 +09:00
aclist d5706f969c change: use base class type 2026-11-05 21:09:42 +09:00
aclist 790e34b20c chore: clear typehinting errors 2026-11-05 21:09:10 +09:00
aclist 35230ae03f feat: capture composited screenshot 2026-11-05 21:08:52 +09:00
16 changed files with 119 additions and 78 deletions

No files matched your search

+3 -10
View File
@@ -12,7 +12,6 @@ from typing import Any, Generator, Union
from warnings import deprecated
from dzgui.api.acf import ACF
from dzgui.const.endpoints import VALIDATE
from dzgui.init.prereqs import has_any_client
from dzgui.init.prereqs import has_flatpak_client, has_steam_client
from dzgui.const.constants import (
@@ -317,9 +316,10 @@ def unsubscribe(key: str, mod: int) -> None:
update_workshop(key, mod, UNSUB_ENDPOINT)
def validate_mod(client: str, mod: str) -> None:
@deprecated("Use subscribe()")
def enqueue_mod(client: str, mod: str, appid: int) -> None:
client_args = concat_bash_args(client)
subprocess.Popen([*client_args, "+workshop_download_item", str(APPID_DAYZ), mod])
subprocess.Popen([*client_args, "+workshop_download_item", str(appid), mod])
@deprecated("Cf. https://github.com/ValveSoftware/steam-for-linux/issues/9672")
@@ -500,10 +500,3 @@ def get_app_name(appid: int) -> str | None:
except Exception as e:
logger.debug(e)
return None
@deprecated("Currently unused")
def validate_dayz(client: str) -> None:
client_args = concat_bash_args(client)
uri = VALIDATE + str(APPID_DAYZ)
subprocess.Popen([*client_args, uri])
+3 -3
View File
@@ -121,7 +121,7 @@ def load_steam_icon() -> None:
logger.critical(e)
def load_gui(version: str, is_debug: bool) -> None:
def build_gui(version: str, is_debug: bool) -> App | None:
lock = lock_acquire() # noqa
set_locale()
@@ -151,7 +151,7 @@ def load_gui(version: str, is_debug: bool) -> None:
wizard = SetupWizard(_is_steam_deck, XDG, xdg_paths)
if not wizard.is_setup_complete():
return
return None
setup_logger(XDG.debug)
with open(XDG.debug, "w") as f:
@@ -186,4 +186,4 @@ def load_gui(version: str, is_debug: bool) -> None:
print(boot.all_ok)
load_steam_icon()
app = App(prefs)
app.run(None)
return app
-1
View File
@@ -18,7 +18,6 @@ COORDS_API = "http://ip-api.com/json"
# steam URIs
WORKSHOP = "steam://url/CommunityFilePage/"
OPEN_URL = "steam://openurl/"
VALIDATE = "steam://validate/"
# user facing
GITHUB_PAGES = "https://aclist.github.io/dzgui/index"
+1 -6
View File
@@ -157,7 +157,6 @@ class ContextMenu(EnumWithAttrs):
COPY_SERVER_IP = {"label": strings.copy_ip}
COPY_SERVER_NAME = {"label": strings.copy_name}
UNSUB_MOD = {"label": strings.unsub_mod}
VALIDATE_MOD = {"label": strings.validate_mod}
OPEN_WORKSHOP = {"label": strings.open_workshop}
REFRESH_PLAYERS = {"label": strings.refresh_players}
REMOVE_HISTORY = {"label": strings.remove_history}
@@ -173,7 +172,7 @@ class ContextMenuGroup(Enum):
"""
SERVER_MOD = (ContextMenu.OPEN_WORKSHOP,)
MOD = (ContextMenu.OPEN_WORKSHOP, ContextMenu.UNSUB_MOD, ContextMenu.VALIDATE_MOD)
MOD = (ContextMenu.OPEN_WORKSHOP, ContextMenu.UNSUB_MOD)
MOD_OFFLINE = (None,)
LOG = (ContextMenu.COPY_LOG_CLIPBOARD,)
SERVER_BROWSER = (
@@ -247,10 +246,6 @@ class ModButton(EnumWithAttrs):
"label": strings.mod_panel.select_stale,
"tooltip": strings.mod_panel.select_stale_tooltip,
}
VALIDATE = {
"label": strings.mod_panel.validate,
"tooltip": strings.mod_panel.validate_tooltip,
}
class ButtonType(EnumWithAttrs):
-10
View File
@@ -102,16 +102,6 @@ class Controller(GObject.GObject):
except AttributeError:
logger.critical(f"{attr} is not a valid AppNavigation attribute.")
def validate_mods(
self, treeview: Union["ModTreeView", "OfflineModTreeView", None] = None
) -> None:
if treeview is None:
view = self.mediator.modtreeview
else:
view = treeview
mod_man = view.get_mod_man()
mod_man.validate_mods()
def get_prefs(self) -> UserPrefs:
return self.prefs
+5 -2
View File
@@ -42,6 +42,9 @@ def main() -> None:
test_libgi_missing()
# NOTE: only import ligbirepository modules after the above check
from dzgui.app_init import load_gui
from dzgui.app_init import build_gui
load_gui(version, args.debug)
app = build_gui(version, args.debug)
if app is None:
sys.exit(0)
app.run(None)
+2 -6
View File
@@ -75,14 +75,10 @@ class ContextMenuManager:
if isinstance(self.treeview, (ModTreeView, OfflineModTreeView)):
match action:
case ContextMenu.OPEN_WORKSHOP:
self.open_mod_page()
case ContextMenu.UNSUB_MOD:
self.controller.unsub_mods(self.treeview)
case ContextMenu.VALIDATE_MOD:
self.controller.validate_mods(self.treeview)
case _:
pass
case ContextMenu.OPEN_WORKSHOP:
self.open_mod_page()
if isinstance(self.treeview, LogTreeView):
match action:
+2 -26
View File
@@ -4,7 +4,7 @@ import time
from pathlib import Path
from typing import TYPE_CHECKING
from dzgui.api.steam import unsubscribe, validate_mod
from dzgui.api.steam import unsubscribe
from dzgui.api.mods import (
get_delimited_mods,
find_stale_mods,
@@ -85,7 +85,7 @@ class ModManager:
total_mods = len(self.store)
self.emitter.emit("mods_updated", msg, total_mods)
def get_selected_mods(self) -> list[tuple[str, Gtk.TreeIter]]:
def unsub_mods(self) -> None:
sel = self.treeview.get_selection()
model, pathlist = sel.get_selected_rows()
# NOTE: reverse when multiple selection
@@ -96,15 +96,6 @@ class ModManager:
continue
mod, _iter = res
mods.append((mod, _iter))
return mods
def validate_mods(self) -> None:
mods = self.get_selected_mods()
self.thread_man.set_job_count(len(mods))
self.validate_all_mods(mods)
def unsub_mods(self) -> None:
mods = self.get_selected_mods()
self.thread_man.set_job_count(len(mods))
self.unsub_all_mods(mods)
@@ -118,21 +109,6 @@ class ModManager:
mod = model.get_value(tree_iter, 2)
return mod, tree_iter
def validate_atomic_mod(self, mod: str) -> None:
# NOTE: propagation to dayz exp will happen during
# subsequent symlink phase on connect
client = self.controller.query_config(Preferences.CLIENT)
validate_mod(client, mod)
@call_on_thread(dialogs.validating_mods)
def validate_all_mods(self, mods: list[tuple[str, Gtk.TreeIter]]) -> None:
for mod, _iter in mods:
self.validate_atomic_mod(mod)
self.thread_man.increment_dialog()
time.sleep(API_RATE_LIMIT)
func = StoredFunc(lambda: None)
self.thread_man.set_cleanup_func(func)
@call_on_thread(dialogs.deleting_mods)
def unsub_all_mods(self, mods: list[tuple[str, Gtk.TreeIter]]) -> None:
for mod, _iter in mods:
-1
View File
@@ -9,7 +9,6 @@ update_success = "Updated successfully. Please exit and relaunch."
load_error_lan = "Failed to find any servers on your network.\nCheck the server query port or your firewall settings."
fetching_mods = "Fetching mod metadata"
deleting_mods = "Unsubscribing mods"
validating_mods = "Validating mods"
scanning_mods = "Scanning mods"
parsing_mods = "Parsing mods"
+80
View File
@@ -0,0 +1,80 @@
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
-5
View File
@@ -31,7 +31,6 @@ show_details = "Show server details"
refresh_players = "Refresh player count"
open_workshop = "Open in Steam Workshop"
unsub_mod = "Unsubscribe mod"
validate_mod = "Validate mod"
copy_name = "Copy name to clipboard"
copy_ip = "Copy IP to clipboard"
copy_log = "Copy record(s) to clipboard"
@@ -201,8 +200,6 @@ class ModPanelStrings:
clear_highlights: str
select_stale: str
select_stale_tooltip: str
validate: str
validate_tooltip: str
@dataclass(slots=True, frozen=True)
@@ -322,8 +319,6 @@ mod_panel = ModPanelStrings(
clear_highlights="Clears highlights and reverts the table to a default state",
select_stale="Select stale",
select_stale_tooltip="Only selects highlighted stale mods",
validate="Validate selected",
validate_tooltip="Validates selected mods and updates them if necessary",
)
+20 -2
View File
@@ -1,7 +1,7 @@
import logging
import signal
from typing import TYPE_CHECKING, Literal, Self
from typing import Any, Literal, Self, TYPE_CHECKING
from dzgui.const.constants import (
APP_NAME,
@@ -47,7 +47,7 @@ logger = logging.getLogger(APP_NAME)
class OuterWindow(Gtk.ApplicationWindow):
def __init__(self, app: Gtk.Application) -> None:
def __init__(self, app: "App") -> None:
super().__init__(
application=app, title=APP_NAME, border_width=10, icon_name=APP_NAME_LOWER
)
@@ -78,6 +78,15 @@ 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()
@@ -291,6 +300,14 @@ 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)
@@ -331,4 +348,5 @@ class App(ColorAwareApp, Gtk.Application): # type: ignore
) -> None:
self.win.halt_proc_and_quit()
MainController = Controller()
-3
View File
@@ -50,7 +50,6 @@ class ModSelectionPanel(Gtk.Box):
ModButton.SELECT_ALL,
ModButton.UNSELECT_ALL,
ModButton.UNSUB_SELECTED,
ModButton.VALIDATE,
)
for button in buttons:
b = EnumeratedModButton(button)
@@ -115,5 +114,3 @@ class ModSelectionPanel(Gtk.Box):
self.swap_sensitive(False)
case ModButton.SELECT_STALE:
self.controller.select_colorized()
case ModButton.VALIDATE:
self.controller.validate_mods()
+1 -3
View File
@@ -16,9 +16,7 @@ logger = logging.getLogger(APP_NAME)
class ContextMixin(TreeView):
def present_menu(
self,
widget: Gtk.Widget,
event: Gdk.EventButton | Gdk.EventKey,
self, widget: Gtk.Widget, event: Gdk.Event
) -> bool:
if self.menu is None:
+1
View File
@@ -3,6 +3,7 @@ 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:
+1
View File
@@ -36,6 +36,7 @@ def keys():
# TODO: use a static fixture instead of system config
@pytest.fixture
@pytest.mark.realconfig
def config():
paths = get_xdg_paths()
xdg = parse_filepaths(paths)