Compare commits

...

8 Commits

Author SHA1 Message Date
aclist
2466887014 chore: drop print statement
Some checks are pending
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
2026-06-10 06:45:51 +09:00
aclist
6050a370f8 feat: folder icons 2026-06-10 06:21:12 +09:00
aclist
d4a99f8229 feat: prepare mods in manager (WIP) 2026-06-10 06:15:27 +09:00
aclist
46fba4eef1 chore: clear typehinting errors 2026-06-10 05:45:08 +09:00
aclist
bc1118a29f feat: OfflineManager 2026-06-10 05:27:57 +09:00
aclist
775f582ea6 fix: drop extraneous arg 2026-06-10 05:27:39 +09:00
aclist
2dadd70be3 fix: pack tree and statusbar in vbox 2026-06-10 05:05:22 +09:00
aclist
3473f03350 fix: pad inner scrollbar 2026-06-10 04:51:05 +09:00
11 changed files with 145 additions and 37 deletions

View File

@ -87,15 +87,18 @@ def get_mod_size(path: Path) -> float:
return size
def get_delimited_mods(steam_path: Path) -> list[Any]:
workshop_path = get_local_mod_path(steam_path)
mods = get_local_mods(workshop_path)
def get_custom_mods(path: Path) -> list[Any]:
mods = get_local_mods(path)
# TODO: error handling
return parse_mods(mods)
def parse_mods(mods: list[Path]) -> list[Any]:
clean = []
for mod in mods:
mod_dir = mod.name
symlink = _hash(mod_dir)
# FIXME: malformed .cpp files could break this
# mention that mods may be downloading
meta = parse_meta(mod)
if meta is None:
continue
@ -106,6 +109,12 @@ def get_delimited_mods(steam_path: Path) -> list[Any]:
return clean
def get_delimited_mods(steam_path: Path) -> list[Any]:
workshop_path = get_local_mod_path(steam_path)
mods = get_local_mods(workshop_path)
return parse_mods(mods)
def get_missing_mods(local: list, remote: list) -> list:
return [mod for mod in remote if mod not in local]

View File

@ -162,7 +162,7 @@ def load_to_menu(client: str, appid: int, name: str, mods: list[str]) -> int:
proc = subprocess.run([*client_args, *params])
return proc.returncode
def launch_offline(client: str, addr: str, appid: int, name: str, mods: list[str], mission: str) -> int:
def launch_offline(client: str, appid: int, name: str, mods: list[str], mission: str) -> int:
"""Launch offline with specific mods/missions"""
concat = concat_mods(mods)
client_args = concat_bash_args(client)

View File

@ -35,6 +35,7 @@ HEX_ORANGE = "#FFAC1C"
CARET_DOWN = "go-down-symbolic"
CARET_UP = "go-up-symbolic"
CLIPBOARD = "edit-copy-symbolic"
FOLDER = "folder-symbolic"
ERROR = "dialog-error-symbolic"
HELP_BUBBLE = "help-about-symbolic"
INPUT_KEYBOARD = "input-keyboard-symbolic"

View File

@ -20,7 +20,6 @@ from dzgui.controllers.emitter import Emitter
from dzgui.managers.config import ConfigManager
from dzgui.managers.connection import ConnectionManager
from dzgui.managers.contextmenu import ContextMenuManager
from dzgui.managers.mods import ModManager
from dzgui.managers.notes import NoteManager
from dzgui.model.servers import ServerModelManager
from dzgui.util.diag import write_diagnostic
@ -236,7 +235,7 @@ class Controller(GObject.GObject):
mod_man.toggle_mod_selection(state)
def delete_mods(
self, treeview: Union["ModTreeView", "OfflineModTreeView"] = None
self, treeview: Union["ModTreeView", "OfflineModTreeView", None] = None
) -> None:
if treeview is None:
view = self.mediator.modtreeview

73
dzgui/managers/offline.py Normal file
View File

@ -0,0 +1,73 @@
from pathlib import Path
from typing import TYPE_CHECKING
from dzgui.api.mods import get_custom_mods
from dzgui.api.steam import launch_offline
from dzgui.const.enum import Preferences
from dzgui.managers.threading import call_on_thread, ThreadingManager
from dzgui.model.model_factory import FastInsertListStore, ModelFactory
from dzgui.strings import dialogs
from dzgui.util.symlink import clone_symlinks
if TYPE_CHECKING:
from dzgui.controllers.mc import Controller
class OfflineManager:
def __init__(
self,
controller: "Controller",
) -> None:
super().__init__()
self.controller = controller
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: strings
@call_on_thread("parsing")
def parse_custom_mods(self, folder: str) -> "FastInsertListStore":
mods = get_custom_mods(Path(folder))
store = ModelFactory().make_mod_store()
store.extend(mods)
# TODO: manipulate tree columns to only show name and size
return store
def setup(
self,
appid: int,
mission: str = "",
local_mods: list[str] | None = None,
custom_mods: list[str] | None = None,
) -> None:
self.appid = appid
self.mission_folder = mission
self.local_mods = local_mods
self.custom_mods = custom_mods
self.launch()
@call_on_thread(dialogs.waiting_for_launch)
def launch(self) -> None:
client = self.controller.query_config(Preferences.CLIENT)
name = self.controller.query_config(Preferences.NAME)
steam_path = self.controller.query_config(Preferences.DEFAULT)
new_symlinks: list[str] = []
combined_mods: list[str] = []
if self.local_mods is not None:
combined_mods.extend(self.local_mods)
if self.custom_mods is not None:
# TODO: new function that creates symlinks in game path
# based on selected mods
clone_symlinks(Path(steam_path))
combined_mods.extend(new_symlinks)
launch_offline(client, self.appid, name, combined_mods, self.mission_folder)

View File

@ -166,5 +166,5 @@ class ModelFactory:
def make_server_store(self) -> FastInsertListStore:
return self.new_model_from_class(ServerCols)
def convert_model_to_list(self, model: "FastInsertListStore") -> list:
return [[el for el in row] for row in model]
# def convert_model_to_list(self, model: "FastInsertListStore") -> list:
# return [[el for el in row] for row in model]

View File

@ -48,8 +48,10 @@ class LargeIcon(Gtk.Image):
class IconButton(Gtk.Button):
def __init__(self, icon: str, margin: int = 0) -> None:
super().__init__()
def __init__(
self, icon: str, margin: int = 0, halign: Gtk.Align = Gtk.Align.START
) -> None:
super().__init__(halign=halign)
self.icon = Icon(icon, l_margin=margin)
self.set_image(self.icon)
self.set_image_position(Gtk.PositionType.RIGHT)
@ -57,8 +59,10 @@ class IconButton(Gtk.Button):
class IconTextButton(IconButton):
def __init__(self, icon: str, label: str) -> None:
super().__init__(icon, margin=5)
def __init__(
self, icon: str, label: str, halign: Gtk.Align = Gtk.Align.START
) -> None:
super().__init__(icon, margin=5, halign=halign)
self.set_label(label)

View File

@ -1,3 +1,4 @@
from typing import Self
from dzgui.util import css
import gi
@ -21,7 +22,7 @@ class HeadingFrame(Gtk.Box):
self.add(self.frame)
@classmethod
def new_with_widget_and_label(cls, widget: Gtk.Widget, label: str) -> None:
def new_with_widget_and_label(cls, widget: Gtk.Widget, label: str) -> Self:
n = cls()
n.frame.add(widget)
n.label.set_label(label)

View File

@ -45,7 +45,7 @@ class ContextMixin(TreeView):
for row in group.value:
if row is None:
return
return False
item = self._process_dynamic_row(row)
self.context_menu.append(item)

View File

@ -3,11 +3,19 @@ from typing import Self, Sequence, TYPE_CHECKING
from dzgui.util import css
import dzgui.api.pefile as PeFile
from dzgui.const.constants import APPID_DAYZ, APPID_DAYZ_EXP, APPNAME_DAYZ, APPNAME_DAYZ_EXP_HUMAN
from dzgui.const.constants import (
APPID_DAYZ,
APPID_DAYZ_EXP,
APPNAME_DAYZ,
APPNAME_DAYZ_EXP_HUMAN,
FOLDER,
)
from dzgui.const.enum import NotebookPage, Preferences
from dzgui.managers.offline import OfflineManager
from dzgui.strings import offline
from dzgui.views.components.scrollable import NoOverlayScrolledWindow
from dzgui.views.components.buttons import IconTextButton
from dzgui.views.components.frame import HeadingFrame
from dzgui.views.components.scrollable import NoOverlayScrolledWindow
from dzgui.views.trees.tree_mods import OfflineModTreeView
@ -54,8 +62,9 @@ class FolderHBox(HBox):
self.set_margin_start(10)
self.set_margin_bottom(10)
# TODO: use IconButton with folder-symbolic
self.button = Gtk.Button(label=btn_label, halign=Gtk.Align.START)
# TODO: alternate class for left-aligned icons
self.button = IconTextButton(FOLDER, btn_label)
self.button.set_image_position(Gtk.PositionType.LEFT)
self.label = Gtk.Label()
self.extend([self.button, self.label])
@ -76,26 +85,30 @@ class ModFrame(HeadingFrame):
self.tree.set_model(None)
self.scrolled = NoOverlayScrolledWindow()
self.scrolled.set_margin_end(10)
self.scrolled.set_size_request(600, 400)
self.scrolled.add(self.tree)
self.status = Gtk.Label(
halign=Gtk.Align.START, margin_start=5, margin_top=3, margin_bottom=3
)
self.vbox.pack_end(self.status, expand=False, fill=False, padding=0)
self.vbox.pack_end(self.scrolled, expand=False, fill=False, padding=0)
self.tree_vbox = VBox()
self.tree_vbox.extend([self.scrolled, self.status])
self.vbox.add(self.tree_vbox)
self.frame.add(self.vbox)
sel = self.tree.get_selection()
sel.connect("changed", self._on_selection_changed)
def collapse_tree(self) -> None:
self.scrolled.hide()
self.tree_vbox.hide()
def get_tree(self) -> OfflineModTreeView:
return self.tree
def pack_start(self, widget: Gtk.Widget) -> None:
def pack(self, widget: Gtk.Widget) -> None:
self.vbox.pack_start(widget, expand=False, fill=False, padding=5)
def set_model(self, model: "FastInsertListStore") -> None:
@ -105,12 +118,17 @@ class ModFrame(HeadingFrame):
def _on_selection_changed(self, sel: Gtk.TreeSelection) -> None:
model, rows = sel.get_selected_rows()
status = f"Mods selected: {len(rows)}"
if len(rows) == 0:
# TODO recycle (util.format)
status = "Ctrl-click to select multiple; Shift-click to select a range."
else:
status = f"Mods selected: {len(rows)}"
self.status.set_label(status)
def set_cursor(self) -> None:
path = Gtk.TreePath.new_from_indices([0])
self.tree.set_cursor(path)
self.tree.get_selection().unselect_all()
class CustomModFrame(ModFrame):
@ -122,16 +140,20 @@ class CustomModFrame(ModFrame):
self.custom_hbox = FolderHBox(offline.custom_button)
self.custom_hbox.get_button().connect("clicked", self._on_custom_button_clicked)
self.pack_start(self.custom_hbox)
self.pack(self.custom_hbox)
def _on_custom_button_clicked(self, button: Gtk.Button) -> None:
# TODO: recycle for mission folder
# TODO: propagate results back to parent
folder = self.controller.set_custom_folder()
if folder is not None:
# TODO: CustomModManager
self.custom_hbox.set_label(str(folder))
def get_mods(self) -> list[str]:
rows = self.tree.get_selection().get_selected_rows()
dirs = [str(col[0]) for col in rows]
return dirs
class RadioFrame(HeadingFrame):
def __init__(self, controller: "Controller") -> None:
@ -140,7 +162,7 @@ class RadioFrame(HeadingFrame):
self.controller = controller
self.id_map = {APPNAME_DAYZ: APPID_DAYZ, APPNAME_DAYZ_EXP_HUMAN: APPID_DAYZ_EXP}
self.appid: APPID_DAYZ
self.appid = APPID_DAYZ
self.dayz = Gtk.RadioButton.new_with_label(None, APPNAME_DAYZ)
self.dayz_exp = Gtk.RadioButton.new_with_label_from_widget(
@ -154,6 +176,7 @@ class RadioFrame(HeadingFrame):
self.frame.add(self.radio_box)
# TODO: abstract out of here
default_steam_path = self.controller.query_config(Preferences.DEFAULT)
steam_path = Path(default_steam_path)
dayz_exp = PeFile.get_pretty_version(steam_path, APPID_DAYZ_EXP)
@ -180,16 +203,15 @@ class OfflineLoader(Gtk.Box):
margin_end=10,
)
# TODO: spacing between inner and outer scrollbars
self.controller = controller
self.controller.register_widget("offline_loader", self)
self.offline_man = OfflineManager(controller)
self.add(PageHeading(offline.heading))
self.local_frame = ModFrame(controller, offline.local_frame)
self.custom_frame = CustomModFrame(controller, offline.custom_frame)
# TODO: use ModelFactory
# TODO: suppress symlink column
self.custom_tree = OfflineModTreeView(controller)
@ -244,10 +266,9 @@ class OfflineLoader(Gtk.Box):
self.controller.open_page(NotebookPage.MODS)
def _on_ok_clicked(self, button: Gtk.Button) -> None:
appid = self.radio_frame.get_appid()
"""
- collect symlinks to selected mods
cf. rebuild_symlinks()
- create symlinks for custom mods
- collect mission folder
"""
# appid = self.radio_frame.get_appid()
# mission = self.mission_frame.get_mission()
# local_mods = self.local_frame.get_mods()
# custom_mods = self.custom_frame.get_mods()
# self.offline_man.setup(appid, mission, local_mods, custom_mods)
pass

View File

@ -154,7 +154,7 @@ class PreConnectionAssistant(Gtk.Box):
self.progress_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=15)
self.progress_box.add(self.mod_count)
self.scrolled = Gtk.ScrolledWindow()
self.scrolled = Gtk.ScrolledWindow(margin_end=10)
self.scrolled.add(self.tree)
self.scrolled.set_size_request(600, 400)