Compare commits

..
Author SHA1 Message Date
aclist 80680b86b0 chore: update test decorator 2026-10-28 15:46:00 +09:00
aclist a4ac357c8f feat: validate atomic mods 2026-10-28 15:44:30 +09:00
aclist 46117ecdda Merge pull request #507 from aclist/prerelease/7.0.0b29-patch-3
fix: add path out of loop
2026-10-26 19:25:32 +09:00
aclist 30119fec46 fix: add path out of loop 2026-10-26 19:24:42 +09:00
aclist ac01818549 Merge pull request #506 from aclist/prerelease/7.0.0b29-patch-2
fix: add /tmp to wizard
2026-10-26 19:21:40 +09:00
aclist d486712a4c fix: add /tmp to wizard 2026-10-26 19:20:17 +09:00
aclist d116702145 Merge pull request #505 from aclist/prerelease/7.0.0b29-patch-1
Prerelease/7.0.0b29 patch 1
2026-10-26 19:11:15 +09:00
aclist 5bee3614bd fix: whitelist /tmp 2026-10-26 19:09:50 +09:00
aclist c73b07991f change: abstract lock file path 2026-10-26 19:09:32 +09:00
aclist 199cd07982 Merge pull request #504 from aclist/prerelease/7.0.0b29
chore: bump version
2026-10-26 18:05:52 +09:00
aclist e3d62c6749 chore: bump version 2026-10-26 17:59:18 +09:00
aclist 5134bf02fb Merge pull request #503 from aclist/fix/flatpak-fixes
fix: flatpak fixes
2026-10-26 17:22:58 +09:00
15 changed files with 82 additions and 14 deletions

No files matched your search

+10 -3
View File
@@ -12,6 +12,7 @@ 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
from dzgui.const.endpoints import VALIDATE
from dzgui.init.prereqs import has_any_client from dzgui.init.prereqs import has_any_client
from dzgui.init.prereqs import has_flatpak_client, has_steam_client from dzgui.init.prereqs import has_flatpak_client, has_steam_client
from dzgui.const.constants import ( from dzgui.const.constants import (
@@ -316,10 +317,9 @@ def unsubscribe(key: str, mod: int) -> None:
update_workshop(key, mod, UNSUB_ENDPOINT) update_workshop(key, mod, UNSUB_ENDPOINT)
@deprecated("Use subscribe()") def validate_mod(client: str, mod: str) -> None:
def enqueue_mod(client: str, mod: str, appid: int) -> None:
client_args = concat_bash_args(client) client_args = concat_bash_args(client)
subprocess.Popen([*client_args, "+workshop_download_item", str(appid), mod]) subprocess.Popen([*client_args, "+workshop_download_item", str(APPID_DAYZ), mod])
@deprecated("Cf. https://github.com/ValveSoftware/steam-for-linux/issues/9672") @deprecated("Cf. https://github.com/ValveSoftware/steam-for-linux/issues/9672")
@@ -500,3 +500,10 @@ def get_app_name(appid: int) -> str | None:
except Exception as e: except Exception as e:
logger.debug(e) logger.debug(e)
return None 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])
+1
View File
@@ -96,6 +96,7 @@ LOG_FILTERS = ("CRITICAL", "WARNING", "INFO", "DEBUG")
TMP_PATH = "/tmp" TMP_PATH = "/tmp"
TMP_TARBALL = "/tmp/dzgui.tar.gz" TMP_TARBALL = "/tmp/dzgui.tar.gz"
TMP_EXE = "/tmp/dzgui/dzgui" TMP_EXE = "/tmp/dzgui/dzgui"
LOCK_FILE = "/tmp/dzgui.flock"
DAYZ_COMMUNITY_ROOT = "Missions" DAYZ_COMMUNITY_ROOT = "Missions"
+1
View File
@@ -18,6 +18,7 @@ COORDS_API = "http://ip-api.com/json"
# steam URIs # steam URIs
WORKSHOP = "steam://url/CommunityFilePage/" WORKSHOP = "steam://url/CommunityFilePage/"
OPEN_URL = "steam://openurl/" OPEN_URL = "steam://openurl/"
VALIDATE = "steam://validate/"
# user facing # user facing
GITHUB_PAGES = "https://aclist.github.io/dzgui/index" GITHUB_PAGES = "https://aclist.github.io/dzgui/index"
+6 -1
View File
@@ -157,6 +157,7 @@ class ContextMenu(EnumWithAttrs):
COPY_SERVER_IP = {"label": strings.copy_ip} COPY_SERVER_IP = {"label": strings.copy_ip}
COPY_SERVER_NAME = {"label": strings.copy_name} COPY_SERVER_NAME = {"label": strings.copy_name}
UNSUB_MOD = {"label": strings.unsub_mod} UNSUB_MOD = {"label": strings.unsub_mod}
VALIDATE_MOD = {"label": strings.validate_mod}
OPEN_WORKSHOP = {"label": strings.open_workshop} OPEN_WORKSHOP = {"label": strings.open_workshop}
REFRESH_PLAYERS = {"label": strings.refresh_players} REFRESH_PLAYERS = {"label": strings.refresh_players}
REMOVE_HISTORY = {"label": strings.remove_history} REMOVE_HISTORY = {"label": strings.remove_history}
@@ -172,7 +173,7 @@ class ContextMenuGroup(Enum):
""" """
SERVER_MOD = (ContextMenu.OPEN_WORKSHOP,) SERVER_MOD = (ContextMenu.OPEN_WORKSHOP,)
MOD = (ContextMenu.OPEN_WORKSHOP, ContextMenu.UNSUB_MOD) MOD = (ContextMenu.OPEN_WORKSHOP, ContextMenu.UNSUB_MOD, ContextMenu.VALIDATE_MOD)
MOD_OFFLINE = (None,) MOD_OFFLINE = (None,)
LOG = (ContextMenu.COPY_LOG_CLIPBOARD,) LOG = (ContextMenu.COPY_LOG_CLIPBOARD,)
SERVER_BROWSER = ( SERVER_BROWSER = (
@@ -246,6 +247,10 @@ class ModButton(EnumWithAttrs):
"label": strings.mod_panel.select_stale, "label": strings.mod_panel.select_stale,
"tooltip": strings.mod_panel.select_stale_tooltip, "tooltip": strings.mod_panel.select_stale_tooltip,
} }
VALIDATE = {
"label": strings.mod_panel.validate,
"tooltip": strings.mod_panel.validate_tooltip,
}
class ButtonType(EnumWithAttrs): class ButtonType(EnumWithAttrs):
+10
View File
@@ -102,6 +102,16 @@ class Controller(GObject.GObject):
except AttributeError: except AttributeError:
logger.critical(f"{attr} is not a valid AppNavigation attribute.") 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: def get_prefs(self) -> UserPrefs:
return self.prefs return self.prefs
+3 -2
View File
@@ -5,8 +5,9 @@ import sys
from pathlib import Path from pathlib import Path
from typing import TextIO from typing import TextIO
from dzgui.views.dialogs.early_alert import EarlyAlertDialog from dzgui.const.constants import LOCK_FILE
from dzgui.util.strings import cannot_acquire_lock from dzgui.util.strings import cannot_acquire_lock
from dzgui.views.dialogs.early_alert import EarlyAlertDialog
def lock_release(lock: TextIO) -> None: def lock_release(lock: TextIO) -> None:
@@ -15,7 +16,7 @@ def lock_release(lock: TextIO) -> None:
def lock_acquire() -> TextIO: def lock_acquire() -> TextIO:
lockfile = Path("/tmp/dzgui.flock") lockfile = Path(LOCK_FILE)
lock = open(lockfile, "w") lock = open(lockfile, "w")
try: try:
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB) fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
+6 -2
View File
@@ -75,10 +75,14 @@ class ContextMenuManager:
if isinstance(self.treeview, (ModTreeView, OfflineModTreeView)): if isinstance(self.treeview, (ModTreeView, OfflineModTreeView)):
match action: match action:
case ContextMenu.UNSUB_MOD:
self.controller.unsub_mods(self.treeview)
case ContextMenu.OPEN_WORKSHOP: case ContextMenu.OPEN_WORKSHOP:
self.open_mod_page() 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
if isinstance(self.treeview, LogTreeView): if isinstance(self.treeview, LogTreeView):
match action: match action:
+26 -2
View File
@@ -4,7 +4,7 @@ import time
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from dzgui.api.steam import unsubscribe from dzgui.api.steam import unsubscribe, validate_mod
from dzgui.api.mods import ( from dzgui.api.mods import (
get_delimited_mods, get_delimited_mods,
find_stale_mods, find_stale_mods,
@@ -85,7 +85,7 @@ class ModManager:
total_mods = len(self.store) total_mods = len(self.store)
self.emitter.emit("mods_updated", msg, total_mods) self.emitter.emit("mods_updated", msg, total_mods)
def unsub_mods(self) -> None: def get_selected_mods(self) -> list[tuple[str, Gtk.TreeIter]]:
sel = self.treeview.get_selection() sel = self.treeview.get_selection()
model, pathlist = sel.get_selected_rows() model, pathlist = sel.get_selected_rows()
# NOTE: reverse when multiple selection # NOTE: reverse when multiple selection
@@ -96,6 +96,15 @@ class ModManager:
continue continue
mod, _iter = res mod, _iter = res
mods.append((mod, _iter)) 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.thread_man.set_job_count(len(mods))
self.unsub_all_mods(mods) self.unsub_all_mods(mods)
@@ -109,6 +118,21 @@ class ModManager:
mod = model.get_value(tree_iter, 2) mod = model.get_value(tree_iter, 2)
return mod, tree_iter 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) @call_on_thread(dialogs.deleting_mods)
def unsub_all_mods(self, mods: list[tuple[str, Gtk.TreeIter]]) -> None: def unsub_all_mods(self, mods: list[tuple[str, Gtk.TreeIter]]) -> None:
for mod, _iter in mods: for mod, _iter in mods:
+1
View File
@@ -9,6 +9,7 @@ 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." 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" fetching_mods = "Fetching mod metadata"
deleting_mods = "Unsubscribing mods" deleting_mods = "Unsubscribing mods"
validating_mods = "Validating mods"
scanning_mods = "Scanning mods" scanning_mods = "Scanning mods"
parsing_mods = "Parsing mods" parsing_mods = "Parsing mods"
+6 -2
View File
@@ -1,7 +1,7 @@
import logging import logging
import subprocess import subprocess
from dzgui.const.constants import APP_NAME, FLATPAK_APPID, GTK_APP_ID from dzgui.const.constants import APP_NAME, FLATPAK_APPID, GTK_APP_ID, TMP_PATH
logger = logging.getLogger(APP_NAME) logger = logging.getLogger(APP_NAME)
@@ -18,7 +18,11 @@ def apply_overrides(xdg: dict[str, str]) -> None:
] ]
subprocess.run([*args], capture_output=True) subprocess.run([*args], capture_output=True)
for path in xdg.values(): paths: list[str] = []
paths.extend([*xdg.values()])
paths.append(TMP_PATH)
for path in paths:
args = [ args = [
"flatpak", "flatpak",
"--user", "--user",
+5
View File
@@ -31,6 +31,7 @@ show_details = "Show server details"
refresh_players = "Refresh player count" refresh_players = "Refresh player count"
open_workshop = "Open in Steam Workshop" open_workshop = "Open in Steam Workshop"
unsub_mod = "Unsubscribe mod" unsub_mod = "Unsubscribe mod"
validate_mod = "Validate mod"
copy_name = "Copy name to clipboard" copy_name = "Copy name to clipboard"
copy_ip = "Copy IP to clipboard" copy_ip = "Copy IP to clipboard"
copy_log = "Copy record(s) to clipboard" copy_log = "Copy record(s) to clipboard"
@@ -200,6 +201,8 @@ class ModPanelStrings:
clear_highlights: str clear_highlights: str
select_stale: str select_stale: str
select_stale_tooltip: str select_stale_tooltip: str
validate: str
validate_tooltip: str
@dataclass(slots=True, frozen=True) @dataclass(slots=True, frozen=True)
@@ -319,6 +322,8 @@ mod_panel = ModPanelStrings(
clear_highlights="Clears highlights and reverts the table to a default state", clear_highlights="Clears highlights and reverts the table to a default state",
select_stale="Select stale", select_stale="Select stale",
select_stale_tooltip="Only selects highlighted stale mods", select_stale_tooltip="Only selects highlighted stale mods",
validate="Validate selected",
validate_tooltip="Validates selected mods and updates them if necessary",
) )
+3
View File
@@ -50,6 +50,7 @@ class ModSelectionPanel(Gtk.Box):
ModButton.SELECT_ALL, ModButton.SELECT_ALL,
ModButton.UNSELECT_ALL, ModButton.UNSELECT_ALL,
ModButton.UNSUB_SELECTED, ModButton.UNSUB_SELECTED,
ModButton.VALIDATE,
) )
for button in buttons: for button in buttons:
b = EnumeratedModButton(button) b = EnumeratedModButton(button)
@@ -114,3 +115,5 @@ class ModSelectionPanel(Gtk.Box):
self.swap_sensitive(False) self.swap_sensitive(False)
case ModButton.SELECT_STALE: case ModButton.SELECT_STALE:
self.controller.select_colorized() self.controller.select_colorized()
case ModButton.VALIDATE:
self.controller.validate_mods()
+3
View File
@@ -14,6 +14,7 @@ from dzgui.const.constants import (
HERO_PATH, HERO_PATH,
FLATPAK_ROOT_PATH, FLATPAK_ROOT_PATH,
LEGACY_CONFIG_PATH, LEGACY_CONFIG_PATH,
TMP_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 STEAM_API_SETUP
@@ -432,6 +433,8 @@ class FlatpakOverridePage(EnumeratedWizardPage):
store = Gtk.ListStore(str) store = Gtk.ListStore(str)
for path in xdg_paths.values(): for path in xdg_paths.values():
store.append((str(path),)) store.append((str(path),))
store.append((TMP_PATH,))
tree = SimpleTree(store, wizard.tree_column) tree = SimpleTree(store, wizard.tree_column)
tree.set_halign(Gtk.Align.CENTER) tree.set_halign(Gtk.Align.CENTER)
+1 -1
View File
@@ -4,7 +4,7 @@ description = "DayZ server browser and mod manager for Linux"
authors = [ authors = [
{name = "aclist"} {name = "aclist"}
] ]
version = "7.0.0b28" version = "7.0.0b29"
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
license-files = ["LICENSE"] license-files = ["LICENSE"]
readme = "README.md" readme = "README.md"
-1
View File
@@ -36,7 +36,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)