Compare commits

...
14 Commits
Author SHA1 Message Date
aclist cab10efc96 Merge pull request #367 from aclist/prerelease/7.0.0b12
Prerelease/7.0.0b12
2026-06-17 22:22:04 +09:00
aclist 0a4efba905 docs: add user to thanks 2026-06-17 22:16:00 +09:00
aclist 1e4e09e52c docs: update changelog 2026-06-17 22:14:56 +09:00
aclist 910c068109 chore: bump version 2026-06-17 22:14:30 +09:00
aclist 1dd773a9bf Merge pull request #366 from aclist/fix/validate-offline-mods
fix: validate custom folders before launch
2026-06-17 22:13:56 +09:00
aclist f4d065d766 fix: typo 2026-06-17 22:13:17 +09:00
aclist 1fe66076c6 fix: validate custom folders before launch 2026-06-17 22:12:12 +09:00
aclist 668ec1d7a0 Merge pull request #365 from aclist/fix/unsub
Fix/unsub
2026-06-17 21:43:11 +09:00
aclist 412f0d3193 chore: clear typehinting errors
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
2026-06-17 21:42:02 +09:00
aclist 894a9b106d chore: move HBox, VBox classes 2026-06-17 21:41:34 +09:00
aclist 1b0a9c9a69 chore: drop foreground logic 2026-06-17 21:33:49 +09:00
aclist c4818d2097 fix: name collision between function signature and variable 2026-06-17 21:33:28 +09:00
aclist 69135150c7 chore: add deprecaiton warning
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
2026-06-17 03:16:44 +09:00
aclist 4d8949427b feat: acf parser 2026-06-17 03:15:57 +09:00
15 changed files with 190 additions and 66 deletions

No files matched your search

+1
View File
@@ -57,6 +57,7 @@
- Disable overlay scrollbars on server tables
- Reduce size of geolocation DB on disk (~100MB)
- Enable LAN page Empty/Full filters on first run of app
- Propagate subscribed mods to Steam client
## Dropped
- Debug mode
+96
View File
@@ -0,0 +1,96 @@
import re
from collections.abc import Iterator
from typing import Any
from warnings import deprecated
@deprecated("Use dzgui.api.steam.unsubscribe()")
class WorkshopACF:
def __init__(self, file: str) -> None:
super().__init__()
self.dict: dict[str, Any]
self.load(file)
def as_dict(self) -> dict[str, Any]:
return self.dict
def load(self, file: str) -> None:
delimiter = r"\t\t"
lines = []
with open(file, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line:
continue
els = re.split(delimiter, line, maxsplit=1)
lines.append(els)
self.dict = self.parse(iter(lines))
def parse(self, lines: Iterator[list[str]]) -> dict[str, str]:
acf: dict[str, Any] = {}
try:
while True:
line = next(lines)
if len(line) == 1:
key = line[0]
if key == "{":
continue
return acf
elif key == "}":
return acf
else:
key = self.dequote(key)
acf[key] = self.parse(lines)
elif len(line) == 2:
k, v = line
k = self.dequote(k)
v = self.dequote(v)
try:
n = list(acf.keys())[-1]
acf[n][k] = v
except Exception:
acf[k] = v
except StopIteration:
return acf
def unpack(self, d: dict, lines: list[Any] = []) -> str:
t1 = "AppWorkshop"
t2 = ("WorkshopItemsInstalled", "WorkshopItemDetails")
for k, v in d.items():
if type(v) is dict:
if k in t1:
self.indent = 0
elif k in t2:
self.indent = 1
else:
self.indent = 2
pref = "\t" * self.indent
lines.append(pref + self.enquote(k))
lines.append(pref + "{")
self.unpack(v, lines)
lines.append(pref + "}")
else:
pref = "\t" * (self.indent + 1)
lines.append(pref + self.enquote(k) + "\t\t" + self.enquote(v))
s = ""
for line in lines:
s += line + "\n"
return s
def to_file(self, file: str) -> None:
s = self.unpack(self.dict)
with open(file, "w") as f:
f.write(s)
@classmethod
def enquote(cls, s: str) -> str:
return f'"{s}"'
@classmethod
def dequote(cls, s: str) -> str:
return s.rstrip('"').lstrip('"')
def delete(self, modid: int) -> None:
del self.dict["AppWorkshop"]["WorkshopItemsInstalled"][modid]
del self.dict["AppWorkshop"]["WorkshopItemDetails"][modid]
+10 -8
View File
@@ -71,21 +71,21 @@ def get_local_signatures(version_file: Path) -> dict[str, int]:
for line in lines:
data = line.split(",")
_id = data[0]
_hash = int(data[1])
hashes[_id] = _hash
mod_hash = int(data[1])
hashes[_id] = mod_hash
return hashes
def get_needs_update(
version_file: Path, remote_hashes: list[tuple[str, str, int, int]]
) -> list[tuple[str, str, int, int]]:
local_hashes = get_local_signatures(version_file)
local_stamps = get_local_signatures(version_file)
needs_update: list[tuple[str, str, int, int]] = []
for title, _id, mod_hash, size in remote_hashes:
if _id not in local_hashes:
needs_update.append((title, _id, mod_hash, size))
elif _hash != local_hashes[_id]:
needs_update.append((title, _id, mod_hash, size))
for title, _id, stamp, size in remote_hashes:
if _id not in local_stamps:
needs_update.append((title, _id, stamp, size))
elif stamp != local_stamps[_id]:
needs_update.append((title, _id, stamp, size))
else:
continue
return needs_update
@@ -252,6 +252,7 @@ def subscribe(key: str, mod: int) -> None:
def unsubscribe(key: str, mod: int) -> None:
update_workshop(key, mod, UNSUB_ENDPOINT)
def gen_shortcut() -> None:
# TODO:
"""
@@ -265,6 +266,7 @@ def gen_shortcut() -> None:
# STEAMID_64 & 0xFFFFFFFF
pass
@deprecated("Use subscribe()")
def enqueue_mod(client: str, mod: str, appid: int) -> None:
client_args = concat_bash_args(client)
+4 -4
View File
@@ -523,11 +523,11 @@ class Controller(GObject.GObject):
ind = self.config_man.get_start_tab()
self.get_servers().notebook.set_current_page(ind)
def update_and_load_to_menu(self, raise_window: bool) -> None:
self.connection_man.update_and_connect(raise_window, menu_only=True)
def update_and_load_to_menu(self) -> None:
self.connection_man.update_and_connect(menu_only=True)
def update_and_connect(self, raise_window: bool) -> None:
self.connection_man.update_and_connect(raise_window)
def update_and_connect(self) -> None:
self.connection_man.update_and_connect()
def update_status(self) -> None:
self.mediator.preconnect.mark_finished()
+2
View File
@@ -40,6 +40,7 @@
- Preboot progress dialog
- Choose to jump into splash screen instead of server
- Collapsible connection panel
- Play offline (load mods directly)
## Changed
- Conform to PEP 440 versioning for beta versions
@@ -56,6 +57,7 @@
- Disable overlay scrollbars on server tables
- Reduce size of geolocation DB on disk (~100MB)
- Enable LAN page Empty/Full filters on first run of app
- Propagate subscribed mods to Steam client
## Dropped
- Debug mode
+3 -7
View File
@@ -294,7 +294,7 @@ class ConnectionManager:
self.controller.add_to_history(self.history, self.record)
self.controller.open_page(NotebookPage.SERVERS)
def _update_mods(self, raise_window: bool, menu_only: bool = False) -> None:
def _update_mods(self, menu_only: bool = False) -> None:
prefs = self.controller.get_prefs()
config_man = self.controller.get_config_man()
key = config_man.lookup(Preferences.STEAM)
@@ -305,10 +305,6 @@ class ConnectionManager:
subscribe(key, int(mod))
time.sleep(RATE_LIMIT_THRESHOLD)
if raise_window is True:
logger.info("Bringing window to foreground")
GLib.idle_add(self.controller.present_window)
for title, mod, stamp, size in self.missing_mods:
mod_path = self.workshop / mod
@@ -331,8 +327,8 @@ class ConnectionManager:
self._connect_steam(menu_only)
@call_on_thread(waiting_for_mods, show_cancel=True)
def update_and_connect(self, raise_window: bool, menu_only: bool = False) -> None:
def update_and_connect(self, menu_only: bool = False) -> None:
if len(self.missing_mods) > 0:
self._update_mods(raise_window, menu_only)
self._update_mods(menu_only)
else:
self._connect_steam(menu_only)
+9 -1
View File
@@ -34,10 +34,18 @@ class OfflineManager:
self.thread_man = ThreadingManager(controller)
self.appid: int
self.mission_folder: str
self.local_mods: list[str] | None
self.custom_mods: list[str] | None
# TODO: more robust test
def is_custom_folder_valid(self, folder: str) -> bool:
return Path(folder).exists()
# TODO: set properties as members of offline manager on demand and check them here
# rather than passing strings again
def is_mission_valid(self, folder: str) -> bool:
return is_mission(Path(folder))
def get_mission(self) -> None:
folder = self.open_folderpicker(dialogs.mission_dialog)
if folder is None:
+1
View File
@@ -13,6 +13,7 @@ local_frame = "Installed mods"
no_local_mods = "No local mods found"
no_mods = "No valid mods found"
no_mission = "Not a valid mission"
folder_changed = "Folder missing or changed on disk"
custom_eventbox = "Set the root folder. Mods must be in atomic folders and have a meta.cpp file at a minimum."
mission_eventbox = "Select a single mission folder containing an init.c file."
+1
View File
@@ -367,6 +367,7 @@ thanks = Thanks(
"Johnofwrong",
"MatheusLasserr",
"nolan-perez",
"OnniSaarni",
"scandalouss",
"SnackSBR",
"StevelDusa",
+25
View File
@@ -0,0 +1,25 @@
from typing import Sequence
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk # noqa
class GenericBox(Gtk.Box):
def __init__(self, orientation: Gtk.Orientation, spacing: int = 0) -> None:
super().__init__(orientation=orientation, spacing=spacing)
def extend(self, els: Sequence[Gtk.Widget]) -> None:
for el in els:
self.add(el)
class HBox(GenericBox):
def __init__(self, spacing: int = 0) -> None:
super().__init__(orientation=Gtk.Orientation.HORIZONTAL, spacing=spacing)
class VBox(GenericBox):
def __init__(self, spacing: int = 0) -> None:
super().__init__(orientation=Gtk.Orientation.VERTICAL, spacing=spacing)
+2
View File
@@ -208,6 +208,8 @@ class SteamWorkshopButton(SteamTextButton):
def __init__(self) -> None:
super().__init__(label=buttons.workshop)
self.set_tooltip_text(buttons.workshop_tooltip)
self.set_margin_top(10)
self.set_margin_bottom(10)
class AddButton(IconTextButton):
+2 -3
View File
@@ -3,6 +3,7 @@ from pathlib import Path
from typing import Self, TYPE_CHECKING
from dzgui.api.steam import find_user_id
from dzgui.const.enum import NotebookPage, Preferences
from dzgui.views.components.box import HBox
from dzgui.views.components.buttons import SteamWorkshopButton
from dzgui.views.components.scrollable import NoOverlayScrolledWindow
from dzgui.views.trees.tree_mods import ModTreeView
@@ -36,10 +37,8 @@ class Mods(Gtk.Box):
uid = find_user_id(steam_path)
pretty_uid = "" if uid is None else uid
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
hbox = HBox(spacing=10)
workshop_button = SteamWorkshopButton()
workshop_button.set_margin_top(10)
workshop_button.set_margin_bottom(10)
workshop_button.connect(
"clicked", lambda _: self.controller.open_user_workshop(pretty_uid)
)
+30 -20
View File
@@ -1,6 +1,6 @@
from __future__ import annotations
from enum import Enum
from typing import Self, Sequence, TYPE_CHECKING, Union
from typing import Self, TYPE_CHECKING, Union
from dzgui.util import css
from dzgui.const.constants import (
@@ -16,6 +16,7 @@ from dzgui.const.constants import (
from dzgui.const.enum import ContextMenuGroup, NotebookPage
from dzgui.managers.offline import OfflineManager
from dzgui.strings import generic, offline
from dzgui.views.components.box import HBox, VBox
from dzgui.views.components.buttons import Icon, IconTextButton
from dzgui.views.components.eventbox import InfoEventBox
from dzgui.views.components.frame import HeadingFrame
@@ -37,25 +38,7 @@ if TYPE_CHECKING:
class FolderError(Enum):
NO_VALID_MODS = 1
NO_VALID_MISSION = 2
class GenericBox(Gtk.Box):
def __init__(self, orientation: Gtk.Orientation, spacing: int = 0) -> None:
super().__init__(orientation=orientation, spacing=spacing)
def extend(self, els: Sequence[Gtk.Widget]) -> None:
for el in els:
self.add(el)
class HBox(GenericBox):
def __init__(self, spacing: int = 0) -> None:
super().__init__(orientation=Gtk.Orientation.HORIZONTAL, spacing=spacing)
class VBox(GenericBox):
def __init__(self, spacing: int = 0) -> None:
super().__init__(orientation=Gtk.Orientation.VERTICAL, spacing=spacing)
FOLDER_CHANGED = 3
class PageHeading(Gtk.Label):
@@ -83,6 +66,10 @@ class ErrorPopover(Gtk.Popover):
prefix = offline.no_mods
case FolderError.NO_VALID_MISSION:
prefix = offline.no_mission
case FolderError.FOLDER_CHANGED:
prefix = offline.folder_changed
self.label.set_label(prefix)
return
self.label.set_label(f"{prefix}: '{msg}'")
@@ -123,6 +110,11 @@ class FolderHBox(HBox):
self.pop.set_relative_to(self.button)
self.pop.connect("unmap", lambda _: self.grab_focus())
self.sidepop = ErrorPopover()
self.sidepop.set_position(Gtk.PositionType.BOTTOM)
self.sidepop.set_relative_to(self.scrolled_label)
self.sidepop.connect("unmap", lambda _: self.grab_focus())
self.connect("map", self._on_map)
self.connect("unmap", self._on_unmap)
@@ -168,6 +160,10 @@ class FolderHBox(HBox):
self.unset_button.show()
def present_error(self, error: FolderError, msg: str) -> None:
if error == FolderError.FOLDER_CHANGED:
self.sidepop.set_label(error, msg)
self.sidepop.popup()
return
self.folder = ""
self.label.set_text("")
self.unset_button.hide()
@@ -318,6 +314,9 @@ class CustomModFrame(ModFrame):
self.tree_vbox.hide()
self.no_mods.hide()
def present_folder_changed(self, folder: str) -> None:
self.custom_hbox.present_error(FolderError.FOLDER_CHANGED, folder)
def present_error(self, folder: str) -> None:
self.hide_tree()
self.custom_hbox.present_error(FolderError.NO_VALID_MODS, folder)
@@ -381,6 +380,9 @@ class MissionFrame(HeadingFrame):
def get_mission(self) -> str:
return self.mission_hbox.get_folder()
def present_folder_changed(self, folder: str) -> None:
self.mission_hbox.present_error(FolderError.FOLDER_CHANGED, folder)
class RadioFrame(HeadingFrame):
def __init__(self, parent: OfflineLoader, controller: "Controller") -> None:
@@ -500,4 +502,12 @@ class OfflineLoader(Gtk.Box):
local_mods = self.local_frame.get_mods()
custom_folder = self.custom_frame.get_folder()
custom_mods = self.custom_frame.get_mods()
if custom_folder and not self.offline_man.is_custom_folder_valid(custom_folder):
self.custom_frame.present_folder_changed(mission)
return
if mission and not self.offline_man.is_mission_valid(mission):
self.mission_frame.present_folder_changed(mission)
return
self.offline_man.launch(appid, mission, local_mods, custom_folder, custom_mods)
+3 -22
View File
@@ -112,18 +112,6 @@ class PreConnectionAssistant(Gtk.Box):
tooltip_text=preconnect.connect_last_tooltip,
)
# TODO: abstract
self.raise_window = Gtk.CheckButton(
label="Foreground DZGUI while downloading",
halign=Gtk.Align.END,
hexpand=True,
valign=Gtk.Align.END,
visible=False,
has_tooltip=True,
sensitive=False,
tooltip_text="Foreground the DZGUI window after mod downloads are queued",
active=True,
)
self.button_box = Gtk.Box(
orientation=Gtk.Orientation.VERTICAL,
valign=Gtk.Align.END,
@@ -133,8 +121,7 @@ class PreConnectionAssistant(Gtk.Box):
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5)
for button in self.back, self.ok, self.connect_last:
box.add(button)
for el in self.raise_window, box:
self.button_box.add(el)
self.button_box.add(box)
self.back.connect("clicked", self._on_back_clicked)
self.ok.connect("clicked", self._on_ok_clicked)
@@ -217,8 +204,6 @@ class PreConnectionAssistant(Gtk.Box):
for child in widgets:
child.set_visible(True)
self.raise_window.set_visible(False)
self.raise_window.set_sensitive(False)
self.ok.set_sensitive(True)
self.ok.set_label(preconnect.update_mods)
@@ -230,10 +215,10 @@ class PreConnectionAssistant(Gtk.Box):
self.ok.emit("clicked")
def _on_connect_last_clicked(self, button: Gtk.Button) -> None:
self.controller.update_and_load_to_menu(self.raise_window.get_active())
self.controller.update_and_load_to_menu()
def _on_ok_clicked(self, button: Gtk.Button) -> None:
self.controller.update_and_connect(self.raise_window.get_active())
self.controller.update_and_connect()
def _on_back_clicked(self, button: Gtk.Button) -> None:
self.controller.open_page(NotebookPage.SERVERS)
@@ -317,10 +302,6 @@ class PreConnectionAssistant(Gtk.Box):
if prereqs.required_space == 0:
self.ok.set_label(preconnect.connect)
self.raise_window.set_visible(False)
else:
self.raise_window.set_visible(True)
self.raise_window.set_sensitive(True)
pretty = number(prereqs.required_space)
suffix = f" Need to download {pretty} MiB of mod updates."
+1 -1
View File
@@ -4,7 +4,7 @@ description = "DayZ server browser and mod manager for Linux"
authors = [
{name = "aclist"}
]
version = "7.0.0b11"
version = "7.0.0b12"
license = "GPL-3.0-or-later"
license-files = ["LICENSE"]
readme = "README.md"