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
- 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

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 requests
import subprocess
from typing import Union
from warnings import deprecated
from shlex import shlex
from pathlib import Path
from dzgui.init.prereqs import has_steam_client
from dzgui.api.mods import _hash
from dzgui.const.constants import (
APPID_DAYZ,
APP_NAME,
DEBIAN_STEAM_PATH,
DEFAULT_STEAM_PATH,
FLATPAK_STEAM_PATH,
UBUNTU_STEAM_PATH,
REQUEST_TIMEOUT,
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.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:
from dzgui.util.symlink import _hash
hashes = []
for mod in mods:
md5sum = _hash(mod)
@ -71,21 +68,26 @@ def get_local_signatures(version_file: Path) -> dict[str, int]:
for line in lines:
data = line.split(",")
_id = data[0]
mod_hash = int(data[1])
hashes[_id] = mod_hash
_hash = int(data[1])
hashes[_id] = _hash
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(
version_file: Path, remote_hashes: 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]] = []
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))
for title, _id, _hash, size in remote_hashes:
if _id not in local_hashes:
needs_update.append((title, _id, _hash, size))
elif _hash != local_hashes[_id]:
needs_update.append((title, _id, _hash, size))
else:
continue
return needs_update
@ -230,29 +232,6 @@ def vdf2json(path: Path) -> str:
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:
# TODO:
"""
@ -265,9 +244,3 @@ def gen_shortcut() -> None:
# or get right-most 32 bits
# STEAMID_64 & 0xFFFFFFFF
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"
MIN_COUNT = 1048576
RATE_LIMIT_THRESHOLD = 3
REQUEST_TIMEOUT = 10
APPNAME_DAYZ = "DayZ"

View File

@ -1,11 +1,6 @@
# internal
STEAM_PUBLISHED_FILES = (
"https://api.steampowered.com/ISteamRemoteStorage/GetPublishedFileDetails/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"
STEAM_PUBLISHED_FILES = "https://api.steampowered.com/ISteamRemoteStorage/GetPublishedFileDetails/v1/?format=json"
STEAM_SERVERS = "https://api.steampowered.com/IGameServersService/GetServerList/v1/?"
BM_SERVERS = "https://api.battlemetrics.com/servers?"
GITHUB = "https://github.com/aclist"
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_SERVER_IP = {"label": strings.copy_ip}
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}
REFRESH_PLAYERS = {"label": strings.refresh_players}
REMOVE_HISTORY = {"label": strings.remove_history}
@ -184,7 +184,7 @@ class ContextMenuGroup(Enum):
"""
SERVER_MOD = (ContextMenu.OPEN_WORKSHOP,)
MOD = (ContextMenu.OPEN_WORKSHOP, ContextMenu.UNSUB_MOD)
MOD = (ContextMenu.OPEN_WORKSHOP, ContextMenu.DELETE_MOD)
MOD_OFFLINE = (None,)
LOG = (ContextMenu.COPY_LOG_CLIPBOARD,)
SERVER_BROWSER = (
@ -250,9 +250,9 @@ class ModButton(EnumWithAttrs):
"label": strings.mod_panel.unhighlight_stale,
"tooltip": strings.mod_panel.unhighlight_stale_tooltip,
}
UNSUB_SELECTED = {
"label": strings.mod_panel.unsub_selected,
"tooltip": strings.mod_panel.unsub_selected_tooltip,
DELETE_SELECTED = {
"label": strings.mod_panel.delete_selected,
"tooltip": strings.mod_panel.delete_selected_tooltip,
}
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.toggle_mod_selection(state)
def unsub_mods(
def delete_mods(
self, treeview: Union["ModTreeView", "OfflineModTreeView", None] = None
) -> None:
if treeview is None:
@ -241,7 +241,7 @@ class Controller(GObject.GObject):
else:
view = treeview
mod_man = view.get_mod_man()
mod_man.unsub_mods()
mod_man.delete_mods()
def get_mod_store(self) -> Gtk.TreeModel | None:
return self.mediator.modtreeview.get_model()
@ -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) -> None:
self.connection_man.update_and_connect(menu_only=True)
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_connect(self) -> None:
self.connection_man.update_and_connect()
def update_and_connect(self, raise_window: bool) -> None:
self.connection_man.update_and_connect(raise_window)
def update_status(self) -> None:
self.mediator.preconnect.mark_finished()

View File

@ -40,7 +40,6 @@
- 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
@ -57,7 +56,6 @@
- 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

View File

@ -12,10 +12,10 @@ import dzgui.api.servers as Servers
from dzgui.api.steam import (
connect,
enqueue_mod,
get_remote_signatures,
get_needs_update,
load_to_menu,
subscribe,
)
from dzgui.api.mods import (
@ -30,7 +30,6 @@ from dzgui.const.constants import (
APPID_DAYZ_EXP,
APPNAME_DAYZ,
APPNAME_DAYZ_EXP_HUMAN,
RATE_LIMIT_THRESHOLD,
)
from dzgui.const.enum import NotebookPage, Preferences
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.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()
config_man = self.controller.get_config_man()
key = config_man.lookup(Preferences.STEAM)
for title, mod, stamp, size in self.missing_mods:
if self.controller.is_cancel_pending():
return
subscribe(key, int(mod))
time.sleep(RATE_LIMIT_THRESHOLD)
enqueue_mod(self.client, mod, self.appid)
# 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:
mod_path = self.workshop / mod
@ -327,8 +330,8 @@ class ConnectionManager:
self._connect_steam(menu_only)
@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:
self._update_mods(menu_only)
self._update_mods(raise_window, menu_only)
else:
self._connect_steam(menu_only)

View File

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

View File

@ -89,9 +89,8 @@ class FilterManager:
continue
self.append_map([m])
# TODO: currently unused
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:
map_name = self.get_active_map_name()

View File

@ -1,22 +1,17 @@
import logging
import time
import shutil
from pathlib import Path
from typing import TYPE_CHECKING
from dzgui.api.steam import unsubscribe
from dzgui.api.mods import (
get_delimited_mods,
get_local_mod_path,
find_stale_mods,
_hash,
remove_stale_signatures,
)
from dzgui.const.constants import (
APP_NAME,
APPID_DAYZ,
APPID_DAYZ_EXP,
RATE_LIMIT_THRESHOLD,
)
from dzgui.const.constants import APP_NAME, APPID_DAYZ, APPID_DAYZ_EXP
from dzgui.const.enum import Preferences
from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager
from dzgui.model.model_factory import FastInsertListStore, ModelFactory
@ -82,7 +77,7 @@ class ModManager:
total_mods = len(self.store)
self.emitter.emit("mods_updated", msg, total_mods)
def unsub_mods(self) -> None:
def delete_mods(self) -> None:
sel = self.treeview.get_selection()
model, pathlist = sel.get_selected_rows()
# NOTE: reverse when multiple selection
@ -93,7 +88,7 @@ class ModManager:
continue
mod, _iter = res
mods.append((mod, _iter))
self.unsub_all_mods(mods)
self.delete_mods_on_system(mods)
def get_mod_from_tree_path(
self, tree_path: Gtk.TreePath
@ -106,28 +101,24 @@ class ModManager:
return mod, tree_iter
@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:
self.unsub_atomic_mod(mod)
self.delete_single_mod(mod)
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)
def unsub_atomic_mod(self, mod: str) -> None:
config_man = self.controller.get_config_man()
key = config_man.lookup(Preferences.STEAM)
unsubscribe(key, int(mod))
def delete_single_mod(self, mod: str) -> None:
steam_path = Path(self.path)
mods_path = get_local_mod_path(steam_path)
app_path = PeFile.get_nested_app_path(steam_path, APPID_DAYZ)
try:
md5 = _hash(mod)
symlink = app_path / md5
symlink.unlink()
except Exception as e:
logger.warning(e)
shutil.rmtree(mods_path / mod)
# NOTE: second pass to unlink DAYZ_EXP mods
# TODO: test this with working APPID_DAYZ_EXP installation
try:
@ -136,9 +127,8 @@ class ModManager:
symlink.unlink()
except PeFile.AppNotInstalledError:
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:
return
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.model.model_factory import ModelFactory
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
if TYPE_CHECKING:
@ -34,18 +34,10 @@ 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:
@ -96,8 +88,6 @@ class OfflineManager:
if len(local_mods) > 0:
combined_mods.extend(local_mods)
config = self.controller.get_prefs().paths.config
rebuild_symlinks(config)
if len(custom_mods) > 0:
new_symlinks = create_custom_symlinks(

View File

@ -374,7 +374,7 @@ class ServerModelManager:
proxy = self._get_proxy_man().get_proxy_model()
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)
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."
fetching_mods = "Fetching mod metadata"
deleting_mods = "Unsubscribing mods"
deleting_mods = "Deleting mods"
scanning_mods = "Scanning mods"
parsing_mods = "Parsing mods"

View File

@ -13,7 +13,6 @@ 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."

View File

@ -52,7 +52,7 @@ show_mods = "Show server-side mods"
show_details = "Show server details"
refresh_players = "Refresh player count"
open_workshop = "Open in Steam Workshop"
unsub_mod = "Unsubscribe mod"
delete_mod = "Delete mod"
copy_name = "Copy name to clipboard"
copy_ip = "Copy IP to clipboard"
copy_log = "Copy record(s) to clipboard"
@ -215,8 +215,8 @@ class ModPanelStrings:
unhighlight_stale_tooltip: str
highlight_stale: str
highlight_stale_tooltip: str
unsub_selected: str
unsub_selected_tooltip: str
delete_selected: str
delete_selected_tooltip: str
unselect_all: str
unselect_all_tooltip: str
select_all: str
@ -337,8 +337,8 @@ mod_panel = ModPanelStrings(
"Shows locally-installed mods which are not used by any server "
"in your Saved Servers"
),
unsub_selected="Unsubscribe selected",
unsub_selected_tooltip="Unsubscribes from selected mods",
delete_selected="Delete selected",
delete_selected_tooltip="Deletes selected mods from the system",
unselect_all="Unselect all",
unselect_all_tooltip="Bulk unselects all mods",
select_all="Select all",
@ -367,7 +367,6 @@ thanks = Thanks(
"Johnofwrong",
"MatheusLasserr",
"nolan-perez",
"OnniSaarni",
"scandalouss",
"SnackSBR",
"StevelDusa",
@ -464,7 +463,7 @@ crumbs = Crumbs(
thanks="Help > Special thanks",
developers="Options > Developers",
default="Servers > ",
offline="Mods > Play offline",
offline="Mods > Play offline"
)
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:
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):

View File

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

View File

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

View File

@ -1,10 +1,5 @@
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.const.enum import NotebookPage
from dzgui.views.components.scrollable import NoOverlayScrolledWindow
from dzgui.views.trees.tree_mods import ModTreeView
@ -31,17 +26,6 @@ class Mods(Gtk.Box):
self.controller.register_widget("modtreeview", self.tree)
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(
label="Play offline",
halign=Gtk.Align.START,
@ -50,10 +34,7 @@ class Mods(Gtk.Box):
)
self.offline_button.connect("clicked", self._on_offline_clicked)
hbox.add(workshop_button)
hbox.add(self.offline_button)
self.add(hbox)
self.add(self.offline_button)
self.add(self.box)
self.connect("map", self._on_map)

View File

@ -1,6 +1,6 @@
from __future__ import annotations
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.const.constants import (
@ -16,7 +16,6 @@ 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
@ -38,7 +37,25 @@ if TYPE_CHECKING:
class FolderError(Enum):
NO_VALID_MODS = 1
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):
@ -66,10 +83,6 @@ 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}'")
@ -110,11 +123,6 @@ 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)
@ -160,10 +168,6 @@ 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()
@ -314,9 +318,6 @@ 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)
@ -380,9 +381,6 @@ 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:
@ -502,12 +500,4 @@ 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)

View File

@ -2,7 +2,7 @@ from pathlib import Path
from typing import TYPE_CHECKING
from dzgui.api import pefile as PeFile
from dzgui.api.steam import find_user_id
from dzgui.config import query
from dzgui.const.constants import (
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.strings import errors, options
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.eventbox import InfoEventBox
from dzgui.views.components.buttons import WebButton
from dzgui.views.components.frame import HeadingFrame
from dzgui.views.components.misc import ClientCombo
@ -133,6 +134,16 @@ class Options(Gtk.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_exp_version_label = Gtk.Label(label=strings.null)
@ -158,6 +169,7 @@ class Options(Gtk.Box):
api_box.add(api_links_box)
prefs_grid = self._make_grid(pref_rows)
mods_grid = self._make_grid(mod_rows)
version_grid = self._make_grid(version_rows)
col = 1
@ -178,6 +190,7 @@ class Options(Gtk.Box):
for pair in [
(api_box, strings.options.api_keys),
(prefs_grid, strings.options.prefs),
(mods_grid, strings.options.mods),
(version_grid, strings.options.version),
]:
@ -403,6 +416,9 @@ class Options(Gtk.Box):
bm = self.controller.query_config(Preferences.BM)
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_bm = bm

View File

@ -112,6 +112,18 @@ 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,
@ -121,7 +133,8 @@ 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)
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.ok.connect("clicked", self._on_ok_clicked)
@ -204,6 +217,8 @@ 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)
@ -215,10 +230,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.controller.update_and_load_to_menu(self.raise_window.get_active())
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:
self.controller.open_page(NotebookPage.SERVERS)
@ -302,6 +317,10 @@ 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."

View File

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

View File

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