Compare commits

..

No commits in common. "f839001097fd525815406d1b1f48ff18bfe68998" and "e8b18d83a56dbc3e9d515483abaac83935ef6adc" have entirely different histories.

27 changed files with 139 additions and 314 deletions

View File

@ -57,7 +57,6 @@
- Disable overlay scrollbars on server tables - Disable overlay scrollbars on server tables
- Reduce size of geolocation DB on disk (~100MB) - Reduce size of geolocation DB on disk (~100MB)
- Enable LAN page Empty/Full filters on first run of app - Enable LAN page Empty/Full filters on first run of app
- Propagate subscribed mods to Steam client
## Dropped ## Dropped
- Debug mode - Debug mode

View File

@ -1,96 +0,0 @@
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]

View File

@ -3,25 +3,20 @@ import logging
import os import os
import requests import requests
import subprocess import subprocess
from typing import Union
from warnings import deprecated
from shlex import shlex from shlex import shlex
from pathlib import Path from pathlib import Path
from dzgui.init.prereqs import has_steam_client from dzgui.init.prereqs import has_steam_client
from dzgui.api.mods import _hash
from dzgui.const.constants import ( from dzgui.const.constants import (
APPID_DAYZ,
APP_NAME, APP_NAME,
DEBIAN_STEAM_PATH, DEBIAN_STEAM_PATH,
DEFAULT_STEAM_PATH, DEFAULT_STEAM_PATH,
FLATPAK_STEAM_PATH, FLATPAK_STEAM_PATH,
UBUNTU_STEAM_PATH, UBUNTU_STEAM_PATH,
REQUEST_TIMEOUT,
VDF_PATH, VDF_PATH,
) )
from dzgui.const.endpoints import SUB_ENDPOINT, STEAM_PUBLISHED_FILES, UNSUB_ENDPOINT from dzgui.const.endpoints import STEAM_PUBLISHED_FILES
from dzgui.strings import wizard from dzgui.strings import wizard
from dzgui.util.bash import concat_bash_args from dzgui.util.bash import concat_bash_args
@ -58,6 +53,8 @@ def get_steam_paths() -> list[tuple[Path, str]]:
def concat_mods(mods: list[str]) -> str: def concat_mods(mods: list[str]) -> str:
from dzgui.util.symlink import _hash
hashes = [] hashes = []
for mod in mods: for mod in mods:
md5sum = _hash(mod) md5sum = _hash(mod)
@ -71,21 +68,26 @@ def get_local_signatures(version_file: Path) -> dict[str, int]:
for line in lines: for line in lines:
data = line.split(",") data = line.split(",")
_id = data[0] _id = data[0]
mod_hash = int(data[1]) _hash = int(data[1])
hashes[_id] = mod_hash hashes[_id] = _hash
return hashes return hashes
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), mod])
def get_needs_update( def get_needs_update(
version_file: Path, remote_hashes: list[tuple[str, str, int, int]] version_file: Path, remote_hashes: list[tuple[str, str, int, int]]
) -> list[tuple[str, str, int, int]]: ) -> list[tuple[str, str, int, int]]:
local_stamps = get_local_signatures(version_file) local_hashes = get_local_signatures(version_file)
needs_update: list[tuple[str, str, int, int]] = [] needs_update: list[tuple[str, str, int, int]] = []
for title, _id, stamp, size in remote_hashes: for title, _id, _hash, size in remote_hashes:
if _id not in local_stamps: if _id not in local_hashes:
needs_update.append((title, _id, stamp, size)) needs_update.append((title, _id, _hash, size))
elif stamp != local_stamps[_id]: elif _hash != local_hashes[_id]:
needs_update.append((title, _id, stamp, size)) needs_update.append((title, _id, _hash, size))
else: else:
continue continue
return needs_update return needs_update
@ -230,29 +232,6 @@ def vdf2json(path: Path) -> str:
jbuf += "\n" jbuf += "\n"
def update_workshop(key: str, mod: int, endpoint: str) -> None:
payload: dict[str, Union[int, str]] = {
"publishedfileid": mod,
"appid": APPID_DAYZ,
"key": key,
"list_type": 1,
"notify_client": 1,
}
try:
res = requests.post(endpoint, params=payload, timeout=REQUEST_TIMEOUT)
res.raise_for_status()
except Exception as e:
logger.critical(e)
def subscribe(key: str, mod: int) -> None:
update_workshop(key, mod, SUB_ENDPOINT)
def unsubscribe(key: str, mod: int) -> None:
update_workshop(key, mod, UNSUB_ENDPOINT)
def gen_shortcut() -> None: def gen_shortcut() -> None:
# TODO: # TODO:
""" """
@ -265,9 +244,3 @@ def gen_shortcut() -> None:
# or get right-most 32 bits # or get right-most 32 bits
# STEAMID_64 & 0xFFFFFFFF # STEAMID_64 & 0xFFFFFFFF
pass pass
@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), mod])

View File

@ -4,7 +4,6 @@ UDP_PORT = 27016
VM_FILE = "/proc/sys/vm/max_map_count" VM_FILE = "/proc/sys/vm/max_map_count"
MIN_COUNT = 1048576 MIN_COUNT = 1048576
RATE_LIMIT_THRESHOLD = 3
REQUEST_TIMEOUT = 10 REQUEST_TIMEOUT = 10
APPNAME_DAYZ = "DayZ" APPNAME_DAYZ = "DayZ"

View File

@ -1,11 +1,6 @@
# internal # internal
STEAM_PUBLISHED_FILES = ( STEAM_PUBLISHED_FILES = "https://api.steampowered.com/ISteamRemoteStorage/GetPublishedFileDetails/v1/?format=json"
"https://api.steampowered.com/ISteamRemoteStorage/GetPublishedFileDetails/v1" STEAM_SERVERS = "https://api.steampowered.com/IGameServersService/GetServerList/v1/?"
)
STEAM_SERVERS = "https://api.steampowered.com/IGameServersService/GetServerList/v1"
SUB_ENDPOINT = "https://api.steampowered.com/IPublishedFileService/Subscribe/v1"
UNSUB_ENDPOINT = "https://api.steampowered.com/IPublishedFileService/Unsubscribe/v1"
BM_SERVERS = "https://api.battlemetrics.com/servers?" BM_SERVERS = "https://api.battlemetrics.com/servers?"
GITHUB = "https://github.com/aclist" GITHUB = "https://github.com/aclist"
GITHUB_RELEASES = "https://api.github.com/repos/aclist/dztui/releases/latest" GITHUB_RELEASES = "https://api.github.com/repos/aclist/dztui/releases/latest"

View File

@ -168,7 +168,7 @@ class ContextMenu(EnumWithAttrs):
COPY_LOG_CLIPBOARD = {"label": strings.copy_log} COPY_LOG_CLIPBOARD = {"label": strings.copy_log}
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} DELETE_MOD = {"label": strings.delete_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}
@ -184,7 +184,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.DELETE_MOD)
MOD_OFFLINE = (None,) MOD_OFFLINE = (None,)
LOG = (ContextMenu.COPY_LOG_CLIPBOARD,) LOG = (ContextMenu.COPY_LOG_CLIPBOARD,)
SERVER_BROWSER = ( SERVER_BROWSER = (
@ -250,9 +250,9 @@ class ModButton(EnumWithAttrs):
"label": strings.mod_panel.unhighlight_stale, "label": strings.mod_panel.unhighlight_stale,
"tooltip": strings.mod_panel.unhighlight_stale_tooltip, "tooltip": strings.mod_panel.unhighlight_stale_tooltip,
} }
UNSUB_SELECTED = { DELETE_SELECTED = {
"label": strings.mod_panel.unsub_selected, "label": strings.mod_panel.delete_selected,
"tooltip": strings.mod_panel.unsub_selected_tooltip, "tooltip": strings.mod_panel.delete_selected_tooltip,
} }
SELECT_STALE = { SELECT_STALE = {
"label": strings.mod_panel.select_stale, "label": strings.mod_panel.select_stale,

View File

@ -233,7 +233,7 @@ class Controller(GObject.GObject):
mod_man = self.mediator.modtreeview.get_mod_man() mod_man = self.mediator.modtreeview.get_mod_man()
mod_man.toggle_mod_selection(state) mod_man.toggle_mod_selection(state)
def unsub_mods( def delete_mods(
self, treeview: Union["ModTreeView", "OfflineModTreeView", None] = None self, treeview: Union["ModTreeView", "OfflineModTreeView", None] = None
) -> None: ) -> None:
if treeview is None: if treeview is None:
@ -241,7 +241,7 @@ class Controller(GObject.GObject):
else: else:
view = treeview view = treeview
mod_man = view.get_mod_man() mod_man = view.get_mod_man()
mod_man.unsub_mods() mod_man.delete_mods()
def get_mod_store(self) -> Gtk.TreeModel | None: def get_mod_store(self) -> Gtk.TreeModel | None:
return self.mediator.modtreeview.get_model() return self.mediator.modtreeview.get_model()
@ -523,11 +523,11 @@ class Controller(GObject.GObject):
ind = self.config_man.get_start_tab() ind = self.config_man.get_start_tab()
self.get_servers().notebook.set_current_page(ind) self.get_servers().notebook.set_current_page(ind)
def update_and_load_to_menu(self) -> None: def update_and_load_to_menu(self, raise_window: bool) -> None:
self.connection_man.update_and_connect(menu_only=True) self.connection_man.update_and_connect(raise_window, menu_only=True)
def update_and_connect(self) -> None: def update_and_connect(self, raise_window: bool) -> None:
self.connection_man.update_and_connect() self.connection_man.update_and_connect(raise_window)
def update_status(self) -> None: def update_status(self) -> None:
self.mediator.preconnect.mark_finished() self.mediator.preconnect.mark_finished()

View File

@ -40,7 +40,6 @@
- Preboot progress dialog - Preboot progress dialog
- Choose to jump into splash screen instead of server - Choose to jump into splash screen instead of server
- Collapsible connection panel - Collapsible connection panel
- Play offline (load mods directly)
## Changed ## Changed
- Conform to PEP 440 versioning for beta versions - Conform to PEP 440 versioning for beta versions
@ -57,7 +56,6 @@
- Disable overlay scrollbars on server tables - Disable overlay scrollbars on server tables
- Reduce size of geolocation DB on disk (~100MB) - Reduce size of geolocation DB on disk (~100MB)
- Enable LAN page Empty/Full filters on first run of app - Enable LAN page Empty/Full filters on first run of app
- Propagate subscribed mods to Steam client
## Dropped ## Dropped
- Debug mode - Debug mode

View File

@ -12,10 +12,10 @@ import dzgui.api.servers as Servers
from dzgui.api.steam import ( from dzgui.api.steam import (
connect, connect,
enqueue_mod,
get_remote_signatures, get_remote_signatures,
get_needs_update, get_needs_update,
load_to_menu, load_to_menu,
subscribe,
) )
from dzgui.api.mods import ( from dzgui.api.mods import (
@ -30,7 +30,6 @@ from dzgui.const.constants import (
APPID_DAYZ_EXP, APPID_DAYZ_EXP,
APPNAME_DAYZ, APPNAME_DAYZ,
APPNAME_DAYZ_EXP_HUMAN, APPNAME_DAYZ_EXP_HUMAN,
RATE_LIMIT_THRESHOLD,
) )
from dzgui.const.enum import NotebookPage, Preferences from dzgui.const.enum import NotebookPage, Preferences
from dzgui.init.proc import is_dayz_running, is_steam_running from dzgui.init.proc import is_dayz_running, is_steam_running
@ -294,16 +293,20 @@ class ConnectionManager:
self.controller.add_to_history(self.history, self.record) self.controller.add_to_history(self.history, self.record)
self.controller.open_page(NotebookPage.SERVERS) self.controller.open_page(NotebookPage.SERVERS)
def _update_mods(self, menu_only: bool = False) -> None: def _update_mods(self, raise_window: bool, menu_only: bool = False) -> None:
# NOTE: fast enqueue all mods in auto mode
prefs = self.controller.get_prefs() prefs = self.controller.get_prefs()
config_man = self.controller.get_config_man()
key = config_man.lookup(Preferences.STEAM)
for title, mod, stamp, size in self.missing_mods: for title, mod, stamp, size in self.missing_mods:
if self.controller.is_cancel_pending(): if self.controller.is_cancel_pending():
return return
subscribe(key, int(mod)) enqueue_mod(self.client, mod, self.appid)
time.sleep(RATE_LIMIT_THRESHOLD) # NOTE: prevents rate limiting
time.sleep(3)
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: for title, mod, stamp, size in self.missing_mods:
mod_path = self.workshop / mod mod_path = self.workshop / mod
@ -327,8 +330,8 @@ class ConnectionManager:
self._connect_steam(menu_only) self._connect_steam(menu_only)
@call_on_thread(waiting_for_mods, show_cancel=True) @call_on_thread(waiting_for_mods, show_cancel=True)
def update_and_connect(self, menu_only: bool = False) -> None: def update_and_connect(self, raise_window: bool, menu_only: bool = False) -> None:
if len(self.missing_mods) > 0: if len(self.missing_mods) > 0:
self._update_mods(menu_only) self._update_mods(raise_window, menu_only)
else: else:
self._connect_steam(menu_only) self._connect_steam(menu_only)

View File

@ -80,8 +80,8 @@ class ContextMenuManager:
if isinstance(self.treeview, (ModTreeView, OfflineModTreeView)): if isinstance(self.treeview, (ModTreeView, OfflineModTreeView)):
match action: match action:
case ContextMenu.UNSUB_MOD: case ContextMenu.DELETE_MOD:
self.controller.unsub_mods(self.treeview) self.controller.delete_mods(self.treeview)
case ContextMenu.OPEN_WORKSHOP: case ContextMenu.OPEN_WORKSHOP:
self.open_mod_page() self.open_mod_page()

View File

@ -89,9 +89,8 @@ class FilterManager:
continue continue
self.append_map([m]) self.append_map([m])
# TODO: currently unused
def get_unique_maps(self) -> list[str]: def get_unique_maps(self) -> list[str]:
return [row[0] for row in self.map_store if row[0] != "All maps"] return [row[0] for row in self.map_store if row != "All maps"]
def get_all_filters(self) -> tuple: def get_all_filters(self) -> tuple:
map_name = self.get_active_map_name() map_name = self.get_active_map_name()

View File

@ -1,22 +1,17 @@
import logging import logging
import time import shutil
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.mods import ( from dzgui.api.mods import (
get_delimited_mods, get_delimited_mods,
get_local_mod_path,
find_stale_mods, find_stale_mods,
_hash, _hash,
remove_stale_signatures, remove_stale_signatures,
) )
from dzgui.const.constants import ( from dzgui.const.constants import APP_NAME, APPID_DAYZ, APPID_DAYZ_EXP
APP_NAME,
APPID_DAYZ,
APPID_DAYZ_EXP,
RATE_LIMIT_THRESHOLD,
)
from dzgui.const.enum import Preferences from dzgui.const.enum import Preferences
from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager
from dzgui.model.model_factory import FastInsertListStore, ModelFactory from dzgui.model.model_factory import FastInsertListStore, ModelFactory
@ -82,7 +77,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 delete_mods(self) -> None:
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
@ -93,7 +88,7 @@ class ModManager:
continue continue
mod, _iter = res mod, _iter = res
mods.append((mod, _iter)) mods.append((mod, _iter))
self.unsub_all_mods(mods) self.delete_mods_on_system(mods)
def get_mod_from_tree_path( def get_mod_from_tree_path(
self, tree_path: Gtk.TreePath self, tree_path: Gtk.TreePath
@ -106,28 +101,24 @@ class ModManager:
return mod, tree_iter return mod, tree_iter
@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 delete_mods_on_system(self, mods: list[tuple[str, Gtk.TreeIter]]) -> None:
for mod, _iter in mods: for mod, _iter in mods:
self.unsub_atomic_mod(mod) self.delete_single_mod(mod)
iters = [_iter for mod, _iter in mods] iters = [_iter for mod, _iter in mods]
func = StoredFunc(self._on_mods_unsubbed, iters) func = StoredFunc(self._on_mods_deleted, iters)
self.thread_man.set_cleanup_func(func) self.thread_man.set_cleanup_func(func)
def unsub_atomic_mod(self, mod: str) -> None: def delete_single_mod(self, mod: str) -> None:
config_man = self.controller.get_config_man()
key = config_man.lookup(Preferences.STEAM)
unsubscribe(key, int(mod))
steam_path = Path(self.path) steam_path = Path(self.path)
mods_path = get_local_mod_path(steam_path)
app_path = PeFile.get_nested_app_path(steam_path, APPID_DAYZ) app_path = PeFile.get_nested_app_path(steam_path, APPID_DAYZ)
try:
md5 = _hash(mod) md5 = _hash(mod)
symlink = app_path / md5 symlink = app_path / md5
symlink.unlink() symlink.unlink()
except Exception as e: shutil.rmtree(mods_path / mod)
logger.warning(e)
# NOTE: second pass to unlink DAYZ_EXP mods # NOTE: second pass to unlink DAYZ_EXP mods
# TODO: test this with working APPID_DAYZ_EXP installation # TODO: test this with working APPID_DAYZ_EXP installation
try: try:
@ -136,9 +127,8 @@ class ModManager:
symlink.unlink() symlink.unlink()
except PeFile.AppNotInstalledError: except PeFile.AppNotInstalledError:
pass pass
time.sleep(RATE_LIMIT_THRESHOLD)
def _on_mods_unsubbed(self, iters: list[Gtk.TreeIter]) -> None: def _on_mods_deleted(self, iters: list[Gtk.TreeIter]) -> None:
if self.store is None: if self.store is None:
return return
for _iter in iters: for _iter in iters:

View File

@ -12,7 +12,7 @@ from dzgui.init.proc import is_dayz_running
from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager
from dzgui.model.model_factory import ModelFactory from dzgui.model.model_factory import ModelFactory
from dzgui.strings import dialogs from dzgui.strings import dialogs
from dzgui.util.symlink import create_custom_symlinks, rebuild_symlinks, symlink_mission from dzgui.util.symlink import create_custom_symlinks, symlink_mission
from dzgui.views.dialogs.filepicker import FolderPicker from dzgui.views.dialogs.filepicker import FolderPicker
if TYPE_CHECKING: if TYPE_CHECKING:
@ -34,18 +34,10 @@ class OfflineManager:
self.thread_man = ThreadingManager(controller) self.thread_man = ThreadingManager(controller)
self.appid: int self.appid: int
self.mission_folder: str
self.local_mods: list[str] | None self.local_mods: list[str] | None
self.custom_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: def get_mission(self) -> None:
folder = self.open_folderpicker(dialogs.mission_dialog) folder = self.open_folderpicker(dialogs.mission_dialog)
if folder is None: if folder is None:
@ -96,8 +88,6 @@ class OfflineManager:
if len(local_mods) > 0: if len(local_mods) > 0:
combined_mods.extend(local_mods) combined_mods.extend(local_mods)
config = self.controller.get_prefs().paths.config
rebuild_symlinks(config)
if len(custom_mods) > 0: if len(custom_mods) > 0:
new_symlinks = create_custom_symlinks( new_symlinks = create_custom_symlinks(

View File

@ -374,7 +374,7 @@ class ServerModelManager:
proxy = self._get_proxy_man().get_proxy_model() proxy = self._get_proxy_man().get_proxy_model()
self.tv.set_model(proxy) self.tv.set_model(proxy)
if self.controller.get_active_treeview().get_enum() == ServerTab.SAVED: if self.controller.get_active_treeview().get_enum == ServerTab.SAVED:
self.emitter.emit("servers_loaded", self.enum) self.emitter.emit("servers_loaded", self.enum)
filter_man = self.tv.get_filter_man() filter_man = self.tv.get_filter_man()

View File

@ -7,7 +7,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 = "Deleting mods"
scanning_mods = "Scanning mods" scanning_mods = "Scanning mods"
parsing_mods = "Parsing mods" parsing_mods = "Parsing mods"

View File

@ -13,7 +13,6 @@ local_frame = "Installed mods"
no_local_mods = "No local mods found" no_local_mods = "No local mods found"
no_mods = "No valid mods found" no_mods = "No valid mods found"
no_mission = "Not a valid mission" 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." 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." mission_eventbox = "Select a single mission folder containing an init.c file."

View File

@ -52,7 +52,7 @@ show_mods = "Show server-side mods"
show_details = "Show server details" 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" delete_mod = "Delete 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"
@ -215,8 +215,8 @@ class ModPanelStrings:
unhighlight_stale_tooltip: str unhighlight_stale_tooltip: str
highlight_stale: str highlight_stale: str
highlight_stale_tooltip: str highlight_stale_tooltip: str
unsub_selected: str delete_selected: str
unsub_selected_tooltip: str delete_selected_tooltip: str
unselect_all: str unselect_all: str
unselect_all_tooltip: str unselect_all_tooltip: str
select_all: str select_all: str
@ -337,8 +337,8 @@ mod_panel = ModPanelStrings(
"Shows locally-installed mods which are not used by any server " "Shows locally-installed mods which are not used by any server "
"in your Saved Servers" "in your Saved Servers"
), ),
unsub_selected="Unsubscribe selected", delete_selected="Delete selected",
unsub_selected_tooltip="Unsubscribes from selected mods", delete_selected_tooltip="Deletes selected mods from the system",
unselect_all="Unselect all", unselect_all="Unselect all",
unselect_all_tooltip="Bulk unselects all mods", unselect_all_tooltip="Bulk unselects all mods",
select_all="Select all", select_all="Select all",
@ -367,7 +367,6 @@ thanks = Thanks(
"Johnofwrong", "Johnofwrong",
"MatheusLasserr", "MatheusLasserr",
"nolan-perez", "nolan-perez",
"OnniSaarni",
"scandalouss", "scandalouss",
"SnackSBR", "SnackSBR",
"StevelDusa", "StevelDusa",
@ -464,7 +463,7 @@ crumbs = Crumbs(
thanks="Help > Special thanks", thanks="Help > Special thanks",
developers="Options > Developers", developers="Options > Developers",
default="Servers > ", default="Servers > ",
offline="Mods > Play offline", offline="Mods > Play offline"
) )
no_mods = "No local mods found." no_mods = "No local mods found."

View File

@ -1,25 +0,0 @@
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)

View File

@ -208,8 +208,6 @@ class SteamWorkshopButton(SteamTextButton):
def __init__(self) -> None: def __init__(self) -> None:
super().__init__(label=buttons.workshop) super().__init__(label=buttons.workshop)
self.set_tooltip_text(buttons.workshop_tooltip) self.set_tooltip_text(buttons.workshop_tooltip)
self.set_margin_top(10)
self.set_margin_bottom(10)
class AddButton(IconTextButton): class AddButton(IconTextButton):

View File

@ -20,8 +20,7 @@ gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, Gdk # noqa E402 from gi.repository import Gtk, Gdk # noqa E402
if TYPE_CHECKING: if TYPE_CHECKING:
from dzgui.controllers.mc import Controller from dzgui.controllers.mc import Controller, Emitter
from dzgui.controllers.emitter import Emitter
COLS = 1 COLS = 1
ROWS = 1 ROWS = 1

View File

@ -11,8 +11,7 @@ gi.require_version("Gtk", "3.0")
from gi.repository import Gtk # noqa E402 from gi.repository import Gtk # noqa E402
if TYPE_CHECKING: if TYPE_CHECKING:
from dzgui.controllers.mc import Controller from dzgui.controllers.mc import Controller, Emitter
from dzgui.controllers.emitter import Emitter
class EnumeratedModButton(Gtk.Button): class EnumeratedModButton(Gtk.Button):
@ -49,7 +48,7 @@ class ModSelectionPanel(Gtk.Box):
buttons = ( buttons = (
ModButton.SELECT_ALL, ModButton.SELECT_ALL,
ModButton.UNSELECT_ALL, ModButton.UNSELECT_ALL,
ModButton.UNSUB_SELECTED, ModButton.DELETE_SELECTED,
) )
for button in buttons: for button in buttons:
b = EnumeratedModButton(button) b = EnumeratedModButton(button)
@ -101,8 +100,8 @@ class ModSelectionPanel(Gtk.Box):
self.controller.toggle_mod_selection(True) self.controller.toggle_mod_selection(True)
case ModButton.UNSELECT_ALL: case ModButton.UNSELECT_ALL:
self.controller.toggle_mod_selection(False) self.controller.toggle_mod_selection(False)
case ModButton.UNSUB_SELECTED: case ModButton.DELETE_SELECTED:
self.controller.unsub_mods() self.controller.delete_mods()
case ModButton.HIGHLIGHT_STALE: case ModButton.HIGHLIGHT_STALE:
self.controller.highlight_stale() self.controller.highlight_stale()
case ModButton.UNHIGHLIGHT_STALE: case ModButton.UNHIGHLIGHT_STALE:

View File

@ -1,10 +1,5 @@
from pathlib import Path
from typing import Self, TYPE_CHECKING from typing import Self, TYPE_CHECKING
from dzgui.api.steam import find_user_id from dzgui.const.enum import NotebookPage
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.components.scrollable import NoOverlayScrolledWindow
from dzgui.views.trees.tree_mods import ModTreeView from dzgui.views.trees.tree_mods import ModTreeView
@ -31,17 +26,6 @@ class Mods(Gtk.Box):
self.controller.register_widget("modtreeview", self.tree) self.controller.register_widget("modtreeview", self.tree)
self.emitter = controller.get_emitter() self.emitter = controller.get_emitter()
# TODO: move
default_steam_path = self.controller.query_config(Preferences.DEFAULT)
steam_path = Path(default_steam_path)
uid = find_user_id(steam_path)
pretty_uid = "" if uid is None else uid
hbox = HBox(spacing=10)
workshop_button = SteamWorkshopButton()
workshop_button.connect(
"clicked", lambda _: self.controller.open_user_workshop(pretty_uid)
)
self.offline_button = Gtk.Button( self.offline_button = Gtk.Button(
label="Play offline", label="Play offline",
halign=Gtk.Align.START, halign=Gtk.Align.START,
@ -50,10 +34,7 @@ class Mods(Gtk.Box):
) )
self.offline_button.connect("clicked", self._on_offline_clicked) self.offline_button.connect("clicked", self._on_offline_clicked)
hbox.add(workshop_button) self.add(self.offline_button)
hbox.add(self.offline_button)
self.add(hbox)
self.add(self.box) self.add(self.box)
self.connect("map", self._on_map) self.connect("map", self._on_map)

View File

@ -1,6 +1,6 @@
from __future__ import annotations from __future__ import annotations
from enum import Enum from enum import Enum
from typing import Self, TYPE_CHECKING, Union from typing import Self, Sequence, TYPE_CHECKING, Union
from dzgui.util import css from dzgui.util import css
from dzgui.const.constants import ( from dzgui.const.constants import (
@ -16,7 +16,6 @@ from dzgui.const.constants import (
from dzgui.const.enum import ContextMenuGroup, NotebookPage from dzgui.const.enum import ContextMenuGroup, NotebookPage
from dzgui.managers.offline import OfflineManager from dzgui.managers.offline import OfflineManager
from dzgui.strings import generic, offline 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.buttons import Icon, IconTextButton
from dzgui.views.components.eventbox import InfoEventBox from dzgui.views.components.eventbox import InfoEventBox
from dzgui.views.components.frame import HeadingFrame from dzgui.views.components.frame import HeadingFrame
@ -38,7 +37,25 @@ if TYPE_CHECKING:
class FolderError(Enum): class FolderError(Enum):
NO_VALID_MODS = 1 NO_VALID_MODS = 1
NO_VALID_MISSION = 2 NO_VALID_MISSION = 2
FOLDER_CHANGED = 3
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)
class PageHeading(Gtk.Label): class PageHeading(Gtk.Label):
@ -66,10 +83,6 @@ class ErrorPopover(Gtk.Popover):
prefix = offline.no_mods prefix = offline.no_mods
case FolderError.NO_VALID_MISSION: case FolderError.NO_VALID_MISSION:
prefix = offline.no_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}'") self.label.set_label(f"{prefix}: '{msg}'")
@ -110,11 +123,6 @@ class FolderHBox(HBox):
self.pop.set_relative_to(self.button) self.pop.set_relative_to(self.button)
self.pop.connect("unmap", lambda _: self.grab_focus()) 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("map", self._on_map)
self.connect("unmap", self._on_unmap) self.connect("unmap", self._on_unmap)
@ -160,10 +168,6 @@ class FolderHBox(HBox):
self.unset_button.show() self.unset_button.show()
def present_error(self, error: FolderError, msg: str) -> None: 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.folder = ""
self.label.set_text("") self.label.set_text("")
self.unset_button.hide() self.unset_button.hide()
@ -314,9 +318,6 @@ class CustomModFrame(ModFrame):
self.tree_vbox.hide() self.tree_vbox.hide()
self.no_mods.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: def present_error(self, folder: str) -> None:
self.hide_tree() self.hide_tree()
self.custom_hbox.present_error(FolderError.NO_VALID_MODS, folder) self.custom_hbox.present_error(FolderError.NO_VALID_MODS, folder)
@ -380,9 +381,6 @@ class MissionFrame(HeadingFrame):
def get_mission(self) -> str: def get_mission(self) -> str:
return self.mission_hbox.get_folder() 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): class RadioFrame(HeadingFrame):
def __init__(self, parent: OfflineLoader, controller: "Controller") -> None: def __init__(self, parent: OfflineLoader, controller: "Controller") -> None:
@ -502,12 +500,4 @@ class OfflineLoader(Gtk.Box):
local_mods = self.local_frame.get_mods() local_mods = self.local_frame.get_mods()
custom_folder = self.custom_frame.get_folder() custom_folder = self.custom_frame.get_folder()
custom_mods = self.custom_frame.get_mods() 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) self.offline_man.launch(appid, mission, local_mods, custom_folder, custom_mods)

View File

@ -2,7 +2,7 @@ from pathlib import Path
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from dzgui.api import pefile as PeFile from dzgui.api import pefile as PeFile
from dzgui.api.steam import find_user_id
from dzgui.config import query from dzgui.config import query
from dzgui.const.constants import ( from dzgui.const.constants import (
APPID_DAYZ, APPID_DAYZ,
@ -19,8 +19,9 @@ from dzgui.const.endpoints import STEAM_API_SETUP, BM_API_SETUP
from dzgui.const.enum import Preferences, ServerTab from dzgui.const.enum import Preferences, ServerTab
from dzgui.strings import errors, options from dzgui.strings import errors, options
from dzgui.util import strings, css, open_links from dzgui.util import strings, css, open_links
from dzgui.views.components.buttons import SteamWorkshopButton
from dzgui.views.components.labels import LeftLabel from dzgui.views.components.labels import LeftLabel
from dzgui.views.components.eventbox import InfoEventBox
from dzgui.views.components.buttons import WebButton from dzgui.views.components.buttons import WebButton
from dzgui.views.components.frame import HeadingFrame from dzgui.views.components.frame import HeadingFrame
from dzgui.views.components.misc import ClientCombo from dzgui.views.components.misc import ClientCombo
@ -133,6 +134,16 @@ class Options(Gtk.Box):
[LeftLabel(strings.options.name), self.player_box], [LeftLabel(strings.options.name), self.player_box],
] ]
eb = InfoEventBox(options.workshop_eventbox, controller)
workshop_button = SteamWorkshopButton()
workshop_button.connect(
"clicked", lambda _: self.controller.open_user_workshop(self.uid)
)
mod_rows = [
[LeftLabel(options.workshop_label), workshop_button, eb],
]
self.dayz_version_label = Gtk.Label(label=strings.null) self.dayz_version_label = Gtk.Label(label=strings.null)
self.dayz_exp_version_label = Gtk.Label(label=strings.null) self.dayz_exp_version_label = Gtk.Label(label=strings.null)
@ -158,6 +169,7 @@ class Options(Gtk.Box):
api_box.add(api_links_box) api_box.add(api_links_box)
prefs_grid = self._make_grid(pref_rows) prefs_grid = self._make_grid(pref_rows)
mods_grid = self._make_grid(mod_rows)
version_grid = self._make_grid(version_rows) version_grid = self._make_grid(version_rows)
col = 1 col = 1
@ -178,6 +190,7 @@ class Options(Gtk.Box):
for pair in [ for pair in [
(api_box, strings.options.api_keys), (api_box, strings.options.api_keys),
(prefs_grid, strings.options.prefs), (prefs_grid, strings.options.prefs),
(mods_grid, strings.options.mods),
(version_grid, strings.options.version), (version_grid, strings.options.version),
]: ]:
@ -403,6 +416,9 @@ class Options(Gtk.Box):
bm = self.controller.query_config(Preferences.BM) bm = self.controller.query_config(Preferences.BM)
steam_path = Path(default_steam_path) steam_path = Path(default_steam_path)
# NOTE: this is a best effort guess at the most recent user
uid = find_user_id(steam_path)
self.uid = "" if uid is None else uid
self.old_steam = steam self.old_steam = steam
self.old_bm = bm self.old_bm = bm

View File

@ -112,6 +112,18 @@ class PreConnectionAssistant(Gtk.Box):
tooltip_text=preconnect.connect_last_tooltip, 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( self.button_box = Gtk.Box(
orientation=Gtk.Orientation.VERTICAL, orientation=Gtk.Orientation.VERTICAL,
valign=Gtk.Align.END, valign=Gtk.Align.END,
@ -121,7 +133,8 @@ class PreConnectionAssistant(Gtk.Box):
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5) box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5)
for button in self.back, self.ok, self.connect_last: for button in self.back, self.ok, self.connect_last:
box.add(button) box.add(button)
self.button_box.add(box) for el in self.raise_window, box:
self.button_box.add(el)
self.back.connect("clicked", self._on_back_clicked) self.back.connect("clicked", self._on_back_clicked)
self.ok.connect("clicked", self._on_ok_clicked) self.ok.connect("clicked", self._on_ok_clicked)
@ -204,6 +217,8 @@ class PreConnectionAssistant(Gtk.Box):
for child in widgets: for child in widgets:
child.set_visible(True) child.set_visible(True)
self.raise_window.set_visible(False)
self.raise_window.set_sensitive(False)
self.ok.set_sensitive(True) self.ok.set_sensitive(True)
self.ok.set_label(preconnect.update_mods) self.ok.set_label(preconnect.update_mods)
@ -215,10 +230,10 @@ class PreConnectionAssistant(Gtk.Box):
self.ok.emit("clicked") self.ok.emit("clicked")
def _on_connect_last_clicked(self, button: Gtk.Button) -> None: def _on_connect_last_clicked(self, button: Gtk.Button) -> None:
self.controller.update_and_load_to_menu() self.controller.update_and_load_to_menu(self.raise_window.get_active())
def _on_ok_clicked(self, button: Gtk.Button) -> None: def _on_ok_clicked(self, button: Gtk.Button) -> None:
self.controller.update_and_connect() self.controller.update_and_connect(self.raise_window.get_active())
def _on_back_clicked(self, button: Gtk.Button) -> None: def _on_back_clicked(self, button: Gtk.Button) -> None:
self.controller.open_page(NotebookPage.SERVERS) self.controller.open_page(NotebookPage.SERVERS)
@ -302,6 +317,10 @@ class PreConnectionAssistant(Gtk.Box):
if prereqs.required_space == 0: if prereqs.required_space == 0:
self.ok.set_label(preconnect.connect) 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) pretty = number(prereqs.required_space)
suffix = f" Need to download {pretty} MiB of mod updates." suffix = f" Need to download {pretty} MiB of mod updates."

View File

@ -16,7 +16,7 @@ logger = logging.getLogger(APP_NAME)
if TYPE_CHECKING: if TYPE_CHECKING:
from dzgui.controllers.mc import Controller from dzgui.controllers.mc import Controller
from dzgui.controllers.emitter import Emitter from dzgui.controllers.mc import Emitter
class ScrollableTree(Gtk.ScrolledWindow): class ScrollableTree(Gtk.ScrolledWindow):

View File

@ -4,7 +4,7 @@ description = "DayZ server browser and mod manager for Linux"
authors = [ authors = [
{name = "aclist"} {name = "aclist"}
] ]
version = "7.0.0b13" version = "7.0.0b11"
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
license-files = ["LICENSE"] license-files = ["LICENSE"]
readme = "README.md" readme = "README.md"