mirror of
https://github.com/aclist/dztui.git
synced 2026-08-29 03:06:56 +02:00
feat: custom symlink creation in thread
This commit is contained in:
parent
a8b7ea5008
commit
e1cb682e74
@ -162,9 +162,12 @@ def load_to_menu(client: str, appid: int, name: str, mods: list[str]) -> int:
|
|||||||
proc = subprocess.run([*client_args, *params])
|
proc = subprocess.run([*client_args, *params])
|
||||||
return proc.returncode
|
return proc.returncode
|
||||||
|
|
||||||
def launch_offline(client: 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"""
|
"""Launch offline with specific mods/missions"""
|
||||||
concat = concat_mods(mods)
|
symlinks = ";".join(mods)
|
||||||
client_args = concat_bash_args(client)
|
client_args = concat_bash_args(client)
|
||||||
params = [
|
params = [
|
||||||
"-applaunch",
|
"-applaunch",
|
||||||
@ -173,12 +176,13 @@ def launch_offline(client: str, appid: int, name: str, mods: list[str], mission:
|
|||||||
"-nosplash",
|
"-nosplash",
|
||||||
"-skipintro",
|
"-skipintro",
|
||||||
f"-name={name}",
|
f"-name={name}",
|
||||||
f"-mod={concat}",
|
f"-mod={symlinks}",
|
||||||
f"-mission={mission}"
|
f"-mission={mission}",
|
||||||
]
|
]
|
||||||
proc = subprocess.run([*client_args, *params])
|
proc = subprocess.run([*client_args, *params])
|
||||||
return proc.returncode
|
return proc.returncode
|
||||||
|
|
||||||
|
|
||||||
def find_user_id(path: Path) -> str | None:
|
def find_user_id(path: Path) -> str | None:
|
||||||
resolved_path = path / "config" / "loginusers.vdf"
|
resolved_path = path / "config" / "loginusers.vdf"
|
||||||
try:
|
try:
|
||||||
|
|||||||
@ -32,10 +32,12 @@ HEX_GREEN = "#32CD32"
|
|||||||
HEX_RED = "#FF0000"
|
HEX_RED = "#FF0000"
|
||||||
HEX_ORANGE = "#FFAC1C"
|
HEX_ORANGE = "#FFAC1C"
|
||||||
|
|
||||||
|
# FIXME: normalize icon names
|
||||||
CARET_DOWN = "go-down-symbolic"
|
CARET_DOWN = "go-down-symbolic"
|
||||||
CARET_UP = "go-up-symbolic"
|
CARET_UP = "go-up-symbolic"
|
||||||
CLIPBOARD = "edit-copy-symbolic"
|
CLIPBOARD = "edit-copy-symbolic"
|
||||||
FOLDER = "folder-symbolic"
|
FOLDER = "folder-symbolic"
|
||||||
|
EDIT_DELETE = "edit-delete-symbolic"
|
||||||
ERROR = "dialog-error-symbolic"
|
ERROR = "dialog-error-symbolic"
|
||||||
HELP_BUBBLE = "help-about-symbolic"
|
HELP_BUBBLE = "help-about-symbolic"
|
||||||
INPUT_KEYBOARD = "input-keyboard-symbolic"
|
INPUT_KEYBOARD = "input-keyboard-symbolic"
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING, Union
|
||||||
|
|
||||||
import gi
|
import gi
|
||||||
|
|
||||||
@ -9,6 +9,7 @@ from gi.repository import Gdk, GObject # noqa E402
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from dzgui.const.enum import NotebookPage, ServerTab
|
from dzgui.const.enum import NotebookPage, ServerTab
|
||||||
from dzgui.model.model_factory import FastInsertListStore
|
from dzgui.model.model_factory import FastInsertListStore
|
||||||
|
from dzgui.views.pages.offline import FolderHBox
|
||||||
|
|
||||||
|
|
||||||
# TODO: if servers fail to load, may leave dangling widgets waiting for a signal
|
# TODO: if servers fail to load, may leave dangling widgets waiting for a signal
|
||||||
@ -160,6 +161,14 @@ class Emitter(GObject.GObject):
|
|||||||
def custom_mods_loaded(self, store: "FastInsertListStore", folder: str) -> None:
|
def custom_mods_loaded(self, store: "FastInsertListStore", folder: str) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=(object,))
|
||||||
|
def custom_mods_unloaded(self, widget: "FolderHBox") -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=(str, bool))
|
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=(str, bool))
|
||||||
def custom_mission_loaded(self, folder: str, is_valid: bool) -> None:
|
def custom_mission_loaded(self, folder: str, is_valid: bool) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=())
|
||||||
|
def invalid_custom_mods(self) -> None:
|
||||||
|
pass
|
||||||
|
|||||||
@ -1,17 +1,23 @@
|
|||||||
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING, Union
|
from typing import Callable, TYPE_CHECKING, Union
|
||||||
|
|
||||||
|
import dzgui.api.pefile as PeFile
|
||||||
from dzgui.api.mods import is_mission, get_custom_mods
|
from dzgui.api.mods import is_mission, get_custom_mods
|
||||||
from dzgui.api.steam import launch_offline
|
from dzgui.api.steam import launch_offline
|
||||||
|
from dzgui.const.constants import APP_NAME
|
||||||
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 ModelFactory
|
from dzgui.model.model_factory import ModelFactory
|
||||||
from dzgui.strings import dialogs
|
from dzgui.strings import dialogs
|
||||||
from dzgui.util.symlink import clone_symlinks
|
from dzgui.util.symlink import create_custom_symlinks
|
||||||
from dzgui.views.dialogs.filepicker import FolderPicker
|
from dzgui.views.dialogs.filepicker import FolderPicker
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from dzgui.controllers.mc import Controller
|
from dzgui.controllers.mc import Controller
|
||||||
|
from dzgui.model.model_factory import FastInsertListStore
|
||||||
|
|
||||||
|
logger = logging.getLogger(APP_NAME)
|
||||||
|
|
||||||
|
|
||||||
class OfflineManager:
|
class OfflineManager:
|
||||||
@ -35,65 +41,73 @@ class OfflineManager:
|
|||||||
if folder is None:
|
if folder is None:
|
||||||
return
|
return
|
||||||
is_valid = is_mission(folder)
|
is_valid = is_mission(folder)
|
||||||
# TODO: stop storing values in here
|
|
||||||
if is_valid:
|
|
||||||
# TODO: str
|
|
||||||
self.mission_folder = str(folder)
|
|
||||||
self.emitter.emit("custom_mission_loaded", str(folder), is_valid)
|
self.emitter.emit("custom_mission_loaded", str(folder), is_valid)
|
||||||
|
|
||||||
def get_custom_mods(self, local_mods: list[str]) -> None:
|
def find_custom_mods(self, callback: Callable) -> None:
|
||||||
folder = self.open_folderpicker(dialogs.custom_mod_dialog)
|
folder = self.open_folderpicker(dialogs.custom_mod_dialog)
|
||||||
|
window = self.controller.get_window()
|
||||||
|
window.set_sensitive(False)
|
||||||
if folder is None:
|
if folder is None:
|
||||||
|
window.set_sensitive(True)
|
||||||
return
|
return
|
||||||
self.parse_custom_mods(local_mods, folder)
|
# NOTE: starts spinner in calling UI
|
||||||
|
callback()
|
||||||
|
self.parse_custom_mods(folder)
|
||||||
|
|
||||||
@call_on_thread(dialogs.parsing_mods)
|
@call_on_thread(dialogs.parsing_mods, show_dialog=False)
|
||||||
def parse_custom_mods(self, local_mods: list[str], folder: str) -> None:
|
def parse_custom_mods(self, folder: str) -> None:
|
||||||
mods = get_custom_mods(Path(folder))
|
mods = get_custom_mods(Path(folder))
|
||||||
store = ModelFactory().make_mod_store()
|
store = ModelFactory().make_mod_store()
|
||||||
store.extend(mods)
|
store.extend(mods)
|
||||||
|
func = StoredFunc(self.post_mod_loading, store, folder)
|
||||||
func = StoredFunc(
|
|
||||||
lambda: self.emitter.emit("custom_mods_loaded", store, folder)
|
|
||||||
)
|
|
||||||
self.thread_man.set_cleanup_func(func)
|
self.thread_man.set_cleanup_func(func)
|
||||||
|
|
||||||
def setup(
|
def post_mod_loading(self, store: "FastInsertListStore", folder: str) -> None:
|
||||||
self,
|
window = self.controller.get_window()
|
||||||
appid: int,
|
window.set_sensitive(True)
|
||||||
mission: str = "",
|
self.emitter.emit("custom_mods_loaded", store, folder)
|
||||||
local_mods: list[str] | None = None,
|
|
||||||
custom_mods: list[str] | None = None,
|
|
||||||
) -> None:
|
|
||||||
|
|
||||||
self.appid = appid
|
|
||||||
self.local_mods = local_mods
|
|
||||||
self.custom_mods = custom_mods
|
|
||||||
|
|
||||||
self.launch()
|
|
||||||
|
|
||||||
@call_on_thread(dialogs.waiting_for_launch)
|
@call_on_thread(dialogs.waiting_for_launch)
|
||||||
def launch(self) -> None:
|
def launch(
|
||||||
|
self,
|
||||||
|
appid: int,
|
||||||
|
mission: str,
|
||||||
|
local_mods: list[str],
|
||||||
|
custom_folder: str,
|
||||||
|
custom_mods: list[str],
|
||||||
|
) -> None:
|
||||||
|
|
||||||
|
# NOTE: local_mods and custom_mods are lists of symlinks
|
||||||
client = self.controller.query_config(Preferences.CLIENT)
|
client = self.controller.query_config(Preferences.CLIENT)
|
||||||
name = self.controller.query_config(Preferences.NAME)
|
name = self.controller.query_config(Preferences.NAME)
|
||||||
steam_path = self.controller.query_config(Preferences.DEFAULT)
|
steam_path = self.controller.query_config(Preferences.DEFAULT)
|
||||||
|
|
||||||
new_symlinks: list[str] = []
|
|
||||||
|
|
||||||
combined_mods: 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:
|
if len(local_mods) > 0:
|
||||||
# TODO: new function that creates symlinks in game path
|
combined_mods.extend(local_mods)
|
||||||
# based on selected mods
|
|
||||||
clone_symlinks(Path(steam_path))
|
if len(custom_mods) > 0:
|
||||||
|
new_symlinks = create_custom_symlinks(
|
||||||
|
Path(steam_path), Path(custom_folder), custom_mods
|
||||||
|
)
|
||||||
combined_mods.extend(new_symlinks)
|
combined_mods.extend(new_symlinks)
|
||||||
|
launch_offline(client, appid, name, combined_mods, mission)
|
||||||
launch_offline(client, self.appid, name, combined_mods, self.mission_folder)
|
|
||||||
|
|
||||||
def open_folderpicker(self, title: str) -> Union["Path", None]:
|
def open_folderpicker(self, title: str) -> Union["Path", None]:
|
||||||
picker = FolderPicker(self.controller.get_window(), title)
|
picker = FolderPicker(self.controller.get_window(), title)
|
||||||
folder = picker.pick_folder()
|
folder = picker.pick_folder()
|
||||||
picker.destroy()
|
picker.destroy()
|
||||||
return folder
|
return folder
|
||||||
|
|
||||||
|
def has_dayz_exp(self) -> bool:
|
||||||
|
try:
|
||||||
|
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)
|
||||||
|
if dayz_exp is None:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(e)
|
||||||
|
return False
|
||||||
|
|||||||
@ -12,3 +12,9 @@ local_frame = "Installed mods"
|
|||||||
|
|
||||||
no_mods = "No valid mods found"
|
no_mods = "No valid mods found"
|
||||||
no_mission = "Not a valid mission"
|
no_mission = "Not a valid mission"
|
||||||
|
|
||||||
|
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."
|
||||||
|
|
||||||
|
folder_prefix = "<b>Current folder: </b>"
|
||||||
|
unset_button = "Unset folder"
|
||||||
|
|||||||
@ -12,10 +12,13 @@ logger = logging.getLogger(APP_NAME)
|
|||||||
|
|
||||||
|
|
||||||
def rebuild_symlinks(config: Path) -> None:
|
def rebuild_symlinks(config: Path) -> None:
|
||||||
|
# TODO: pass direct path as argument
|
||||||
path = lookup(config, Preferences.DEFAULT)
|
path = lookup(config, Preferences.DEFAULT)
|
||||||
steam_path = Path(path)
|
steam_path = Path(path)
|
||||||
dayz_path = PeFile.get_nested_app_path(steam_path, APPID_DAYZ)
|
dayz_path = PeFile.get_nested_app_path(steam_path, APPID_DAYZ)
|
||||||
# NOTE: unlink stale symlinks
|
# NOTE: unlink stale symlinks
|
||||||
|
# TODO: expunge ephemeral (@C) links (possibility of name collisions)
|
||||||
|
# if str(file)[:2] == "@C": file.unlink()
|
||||||
for file in dayz_path.iterdir():
|
for file in dayz_path.iterdir():
|
||||||
if file.is_symlink() and file.exists() is False:
|
if file.is_symlink() and file.exists() is False:
|
||||||
file.unlink()
|
file.unlink()
|
||||||
@ -30,6 +33,21 @@ def rebuild_symlinks(config: Path) -> None:
|
|||||||
clone_symlinks(steam_path)
|
clone_symlinks(steam_path)
|
||||||
|
|
||||||
|
|
||||||
|
def create_custom_symlinks(steam_path: Path, custom_dir: Path, uids: list[str]) -> None:
|
||||||
|
dayz_path = PeFile.get_nested_app_path(steam_path, APPID_DAYZ)
|
||||||
|
links = []
|
||||||
|
for uid in uids:
|
||||||
|
md5sum = _hash(uid, use_custom=True)
|
||||||
|
source = dayz_path.joinpath(md5sum)
|
||||||
|
target = custom_dir.joinpath(uid)
|
||||||
|
links.append(md5sum)
|
||||||
|
if source.exists() is False:
|
||||||
|
source.symlink_to(target)
|
||||||
|
clone_symlinks(steam_path)
|
||||||
|
return links
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def clone_symlinks(steam_path: Path) -> None:
|
def clone_symlinks(steam_path: Path) -> None:
|
||||||
"""
|
"""
|
||||||
Shares symlinks between builds. Used after any symlink operation
|
Shares symlinks between builds. Used after any symlink operation
|
||||||
|
|||||||
@ -54,7 +54,12 @@ class FolderPicker(Gtk.FileChooserDialog):
|
|||||||
return None
|
return None
|
||||||
if res == Gtk.ResponseType.OK:
|
if res == Gtk.ResponseType.OK:
|
||||||
folder = self.get_current_folder()
|
folder = self.get_current_folder()
|
||||||
if folder is not None:
|
if folder is None:
|
||||||
self.destroy()
|
try:
|
||||||
return Path(folder)
|
folder = Path.from_uri(self.get_uri())
|
||||||
|
except Exception:
|
||||||
|
# FIXME
|
||||||
|
return Path("/")
|
||||||
|
self.destroy()
|
||||||
|
return Path(folder)
|
||||||
return None
|
return None
|
||||||
|
|||||||
@ -1,20 +1,22 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from pathlib import Path
|
from enum import Enum
|
||||||
from typing import Self, Sequence, TYPE_CHECKING, Union
|
from typing import Self, Sequence, TYPE_CHECKING, Union
|
||||||
|
|
||||||
from dzgui.util import css
|
from dzgui.util import css
|
||||||
import dzgui.api.pefile as PeFile
|
|
||||||
from dzgui.const.constants import (
|
from dzgui.const.constants import (
|
||||||
APPID_DAYZ,
|
APPID_DAYZ,
|
||||||
APPID_DAYZ_EXP,
|
APPID_DAYZ_EXP,
|
||||||
APPNAME_DAYZ,
|
APPNAME_DAYZ,
|
||||||
APPNAME_DAYZ_EXP_HUMAN,
|
APPNAME_DAYZ_EXP_HUMAN,
|
||||||
|
EDIT_DELETE,
|
||||||
|
ERROR,
|
||||||
FOLDER,
|
FOLDER,
|
||||||
)
|
)
|
||||||
from dzgui.const.enum import NotebookPage, Preferences
|
from dzgui.const.enum import NotebookPage, Preferences
|
||||||
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.buttons import IconTextButton
|
from dzgui.views.components.buttons import Icon, IconTextButton
|
||||||
|
from dzgui.views.components.eventbox import InfoEventBox
|
||||||
from dzgui.views.components.frame import HeadingFrame
|
from dzgui.views.components.frame import HeadingFrame
|
||||||
from dzgui.views.components.scrollable import NoOverlayScrolledWindow
|
from dzgui.views.components.scrollable import NoOverlayScrolledWindow
|
||||||
from dzgui.views.trees.tree_mods import OfflineModTreeView
|
from dzgui.views.trees.tree_mods import OfflineModTreeView
|
||||||
@ -23,7 +25,7 @@ from dzgui.views.trees.tree_mods import OfflineModTreeView
|
|||||||
import gi
|
import gi
|
||||||
|
|
||||||
gi.require_version("Gtk", "3.0")
|
gi.require_version("Gtk", "3.0")
|
||||||
from gi.repository import Gtk, Gdk # noqa
|
from gi.repository import Gtk, Gdk, GObject # noqa
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from dzgui.controllers.mc import Controller
|
from dzgui.controllers.mc import Controller
|
||||||
@ -31,6 +33,11 @@ if TYPE_CHECKING:
|
|||||||
from dzgui.model.model_factory import FastInsertListStore
|
from dzgui.model.model_factory import FastInsertListStore
|
||||||
|
|
||||||
|
|
||||||
|
class FolderError(Enum):
|
||||||
|
NO_VALID_MODS = 1
|
||||||
|
NO_VALID_MISSION = 2
|
||||||
|
|
||||||
|
|
||||||
class GenericBox(Gtk.Box):
|
class GenericBox(Gtk.Box):
|
||||||
def __init__(self, orientation: Gtk.Orientation, spacing: int = 0) -> None:
|
def __init__(self, orientation: Gtk.Orientation, spacing: int = 0) -> None:
|
||||||
super().__init__(orientation=orientation, spacing=spacing)
|
super().__init__(orientation=orientation, spacing=spacing)
|
||||||
@ -57,15 +64,40 @@ class PageHeading(Gtk.Label):
|
|||||||
css.add_class(self, "page-heading")
|
css.add_class(self, "page-heading")
|
||||||
|
|
||||||
|
|
||||||
|
class ErrorPopover(Gtk.Popover):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__(position=Gtk.PositionType.RIGHT)
|
||||||
|
|
||||||
|
self.hbox = HBox()
|
||||||
|
self.label = Gtk.Label(label="", margin_start=10, margin_end=10)
|
||||||
|
error_icon = Icon(ERROR, margin_start=10)
|
||||||
|
self.hbox.extend([error_icon, self.label])
|
||||||
|
self.add(self.hbox)
|
||||||
|
self.show_all()
|
||||||
|
self.popdown()
|
||||||
|
|
||||||
|
def set_label(self, error: FolderError, msg: str) -> None:
|
||||||
|
match error:
|
||||||
|
case FolderError.NO_VALID_MODS:
|
||||||
|
prefix = offline.no_mods
|
||||||
|
case FolderError.NO_VALID_MISSION:
|
||||||
|
prefix = offline.no_mission
|
||||||
|
self.label.set_label(f"{prefix}: '{msg}'")
|
||||||
|
|
||||||
|
|
||||||
class FolderHBox(HBox):
|
class FolderHBox(HBox):
|
||||||
def __init__(self, btn_label: str) -> None:
|
def __init__(self, controller: "Controller", btn_label: str, eb_text: str) -> None:
|
||||||
super().__init__(spacing=10)
|
super().__init__(spacing=10)
|
||||||
|
|
||||||
self.set_margin_start(10)
|
self.set_margin_start(5)
|
||||||
self.set_margin_end(10)
|
self.set_margin_end(10)
|
||||||
self.set_margin_bottom(5)
|
self.set_margin_bottom(5)
|
||||||
|
|
||||||
# TODO: alternate class for left-aligned icons
|
self.folder = ""
|
||||||
|
self.controller = controller
|
||||||
|
self.emitter = controller.get_emitter()
|
||||||
|
|
||||||
|
self.eb = InfoEventBox(eb_text, controller)
|
||||||
self.button = IconTextButton(FOLDER, btn_label, Gtk.PositionType.LEFT)
|
self.button = IconTextButton(FOLDER, btn_label, Gtk.PositionType.LEFT)
|
||||||
self.button.set_halign(Gtk.Align.START)
|
self.button.set_halign(Gtk.Align.START)
|
||||||
self.button.set_image_position(Gtk.PositionType.LEFT)
|
self.button.set_image_position(Gtk.PositionType.LEFT)
|
||||||
@ -75,19 +107,69 @@ class FolderHBox(HBox):
|
|||||||
)
|
)
|
||||||
self.label = Gtk.Label()
|
self.label = Gtk.Label()
|
||||||
self.scrolled_label.add(self.label)
|
self.scrolled_label.add(self.label)
|
||||||
self.extend([self.button, self.scrolled_label])
|
|
||||||
|
self.unset_button = IconTextButton(EDIT_DELETE, offline.unset_button)
|
||||||
|
self.unset_button.connect("clicked", self._on_unset_clicked)
|
||||||
|
|
||||||
|
self.spinner = Gtk.Spinner()
|
||||||
|
self.extend(
|
||||||
|
[self.eb, self.button, self.spinner, self.scrolled_label, self.unset_button]
|
||||||
|
)
|
||||||
|
|
||||||
|
self.pop = ErrorPopover()
|
||||||
|
self.pop.set_relative_to(self.button)
|
||||||
|
|
||||||
|
self.connect("map", self._on_map)
|
||||||
|
self.connect("unmap", self._on_unmap)
|
||||||
|
|
||||||
|
def start_spinner(self) -> None:
|
||||||
|
self.spinner.show()
|
||||||
|
self.spinner.start()
|
||||||
|
|
||||||
|
def stop_spinner(self) -> None:
|
||||||
|
self.spinner.hide()
|
||||||
|
self.spinner.stop()
|
||||||
|
|
||||||
|
def get_spinner(self) -> None:
|
||||||
|
return self.spinner
|
||||||
|
|
||||||
|
def _on_unmap(self, widget: Self) -> None:
|
||||||
|
self.unset_button.hide()
|
||||||
|
self.label.set_label("")
|
||||||
|
self.scrolled_label.hide()
|
||||||
|
|
||||||
|
def _on_map(self, widget: Self) -> None:
|
||||||
|
self.spinner.hide()
|
||||||
|
self.scrolled_label.show()
|
||||||
|
self.unset_button.hide()
|
||||||
|
|
||||||
|
def _on_unset_clicked(self, button: Gtk.Button) -> None:
|
||||||
|
self.label.set_label("")
|
||||||
|
self.unset_button.hide()
|
||||||
|
# FIXME: rename signal
|
||||||
|
self.emitter.emit("custom_mods_unloaded", self)
|
||||||
|
|
||||||
def get_button(self) -> Gtk.Button:
|
def get_button(self) -> Gtk.Button:
|
||||||
return self.button
|
return self.button
|
||||||
|
|
||||||
def set_label(self, label: str) -> None:
|
|
||||||
# TODO: strings
|
|
||||||
prefix = "Current folder: "
|
|
||||||
self.label.set_label(prefix + label)
|
|
||||||
|
|
||||||
def hide_label(self) -> None:
|
def hide_label(self) -> None:
|
||||||
self.label.hide()
|
self.label.hide()
|
||||||
|
|
||||||
|
def get_folder(self) -> str:
|
||||||
|
return self.folder
|
||||||
|
|
||||||
|
def set_folder(self, folder: str) -> None:
|
||||||
|
prefix = offline.folder_prefix
|
||||||
|
self.folder = folder
|
||||||
|
self.label.set_markup(prefix + folder)
|
||||||
|
self.label.show()
|
||||||
|
self.unset_button.show()
|
||||||
|
|
||||||
|
def present_error(self, error: FolderError, msg: str) -> None:
|
||||||
|
self.unset_button.hide()
|
||||||
|
self.pop.set_label(error, msg)
|
||||||
|
self.pop.popup()
|
||||||
|
|
||||||
|
|
||||||
class ModFrame(HeadingFrame):
|
class ModFrame(HeadingFrame):
|
||||||
def __init__(
|
def __init__(
|
||||||
@ -120,39 +202,29 @@ class ModFrame(HeadingFrame):
|
|||||||
sel = self.tree.get_selection()
|
sel = self.tree.get_selection()
|
||||||
sel.connect("changed", self._on_selection_changed)
|
sel.connect("changed", self._on_selection_changed)
|
||||||
|
|
||||||
# TODO: inherit icons from warnings area in preconnect dialog
|
self.connect("unmap", self._on_unmap)
|
||||||
# TODO: strings
|
|
||||||
self.error_label = Gtk.Label(
|
|
||||||
label="No mods found",
|
|
||||||
halign=Gtk.Align.START,
|
|
||||||
margin_start=10,
|
|
||||||
margin_bottom=5,
|
|
||||||
)
|
|
||||||
self.vbox.pack_end(self.error_label, expand=True, fill=True, padding=3)
|
|
||||||
|
|
||||||
def set_error(self, msg: str) -> None:
|
def _on_unmap(self, widget: Self) -> None:
|
||||||
self.error_label.set_label(msg)
|
self.tree.set_model(None)
|
||||||
self.error_label.show()
|
self.tree_vbox.hide()
|
||||||
|
|
||||||
def hide_errors(self) -> None:
|
def start(self, store: "FastInsertListStore") -> None:
|
||||||
self.error_label.hide()
|
self.tree.set_model(store)
|
||||||
|
self.show_tree()
|
||||||
def start_empty(self) -> None:
|
|
||||||
self.error_label.show()
|
|
||||||
self.collapse_tree()
|
|
||||||
|
|
||||||
def hide_all(self) -> None:
|
|
||||||
self.error_label.hide()
|
|
||||||
self.collapse_tree()
|
|
||||||
|
|
||||||
# TODO: warnings area
|
|
||||||
# name collision within custom mods
|
|
||||||
|
|
||||||
def get_mods(self) -> list[str]:
|
def get_mods(self) -> list[str]:
|
||||||
model, treeiters = self.tree.get_selection().get_selected_rows()
|
model, treeiters = self.tree.get_selection().get_selected_rows()
|
||||||
if model is None:
|
if model is None:
|
||||||
return []
|
return []
|
||||||
return [model[_iter][1] for _iter in treeiters]
|
# TODO: include mod paths for custom mods
|
||||||
|
if type(self) is CustomModFrame:
|
||||||
|
return [model[_iter][2] for _iter in treeiters]
|
||||||
|
else:
|
||||||
|
# NOTE: pre-existing, canonical symlinks to published mods
|
||||||
|
return [model[_iter][1] for _iter in treeiters]
|
||||||
|
|
||||||
|
def show_tree(self) -> None:
|
||||||
|
self.tree_vbox.show()
|
||||||
|
|
||||||
def collapse_tree(self) -> None:
|
def collapse_tree(self) -> None:
|
||||||
self.tree_vbox.hide()
|
self.tree_vbox.hide()
|
||||||
@ -175,6 +247,8 @@ class ModFrame(HeadingFrame):
|
|||||||
else:
|
else:
|
||||||
status = f"Mods selected: {len(rows)}"
|
status = f"Mods selected: {len(rows)}"
|
||||||
self.status.set_label(status)
|
self.status.set_label(status)
|
||||||
|
# TODO: cleaner delegation
|
||||||
|
# simply send int value or use emitter
|
||||||
self.parent.check_button()
|
self.parent.check_button()
|
||||||
|
|
||||||
def set_cursor(self) -> None:
|
def set_cursor(self) -> None:
|
||||||
@ -194,25 +268,39 @@ class CustomModFrame(ModFrame):
|
|||||||
self.controller = controller
|
self.controller = controller
|
||||||
self.emitter = controller.get_emitter()
|
self.emitter = controller.get_emitter()
|
||||||
|
|
||||||
# TODO: descriptive text here explaining how this area works
|
self.custom_hbox = FolderHBox(
|
||||||
self.custom_hbox = FolderHBox(offline.custom_button)
|
controller, offline.custom_button, offline.custom_eventbox
|
||||||
|
)
|
||||||
self.custom_hbox.get_button().connect("clicked", self._on_custom_button_clicked)
|
self.custom_hbox.get_button().connect("clicked", self._on_custom_button_clicked)
|
||||||
|
|
||||||
self.pack(self.custom_hbox)
|
self.pack(self.custom_hbox)
|
||||||
|
|
||||||
self.emitter.connect("custom_mods_loaded", self._on_custom_mods_loaded)
|
self.emitter.connect("custom_mods_loaded", self._on_custom_mods_loaded)
|
||||||
|
self.emitter.connect("custom_mods_unloaded", self._on_custom_mods_unloaded)
|
||||||
|
|
||||||
|
self.connect("map", self._on_map)
|
||||||
|
|
||||||
|
def _on_map(self, widget: Self) -> None:
|
||||||
|
self.hide_tree()
|
||||||
|
|
||||||
|
def _on_custom_mods_unloaded(self, emitter: "Emitter", widget: FolderHBox) -> None:
|
||||||
|
# TODO: kludgy workaround for generic button emitting global signal
|
||||||
|
if widget == self.custom_hbox:
|
||||||
|
self.hide_tree()
|
||||||
|
|
||||||
def hide_tree(self) -> None:
|
def hide_tree(self) -> None:
|
||||||
# TODO: no mods message is different from collision message
|
|
||||||
self.set_error(offline.no_mods)
|
|
||||||
self.custom_hbox.hide_label()
|
self.custom_hbox.hide_label()
|
||||||
|
self.tree.set_model(None)
|
||||||
self.tree_vbox.hide()
|
self.tree_vbox.hide()
|
||||||
|
|
||||||
|
def present_error(self, folder: str) -> None:
|
||||||
|
self.hide_tree()
|
||||||
|
self.custom_hbox.present_error(FolderError.NO_VALID_MODS, folder)
|
||||||
|
|
||||||
def present_tree(self, store: "FastInsertListStore", folder: str) -> None:
|
def present_tree(self, store: "FastInsertListStore", folder: str) -> None:
|
||||||
self.custom_hbox.set_label(folder)
|
self.custom_hbox.set_folder(folder)
|
||||||
self.tree.set_model(store)
|
self.tree.set_model(store)
|
||||||
self.tree_vbox.show()
|
self.tree_vbox.show()
|
||||||
self.hide_errors()
|
|
||||||
|
|
||||||
def _on_custom_mods_loaded(
|
def _on_custom_mods_loaded(
|
||||||
self,
|
self,
|
||||||
@ -220,14 +308,19 @@ class CustomModFrame(ModFrame):
|
|||||||
store: "FastInsertListStore",
|
store: "FastInsertListStore",
|
||||||
folder: str,
|
folder: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
self.custom_hbox.stop_spinner()
|
||||||
if len(store) == 0:
|
if len(store) == 0:
|
||||||
self.hide_tree()
|
self.present_error(folder)
|
||||||
else:
|
else:
|
||||||
self.present_tree(store, folder)
|
self.present_tree(store, folder)
|
||||||
|
|
||||||
def _on_custom_button_clicked(self, button: Gtk.Button) -> None:
|
def _on_custom_button_clicked(self, button: Gtk.Button) -> None:
|
||||||
local_mods = self.get_mods()
|
callback = self.custom_hbox.start_spinner
|
||||||
self.parent.offline_man.get_custom_mods(local_mods)
|
# TODO: cleaner delegation
|
||||||
|
self.parent.offline_man.find_custom_mods(callback)
|
||||||
|
|
||||||
|
def get_folder(self) -> str:
|
||||||
|
return self.custom_hbox.get_folder()
|
||||||
|
|
||||||
|
|
||||||
class MissionFrame(HeadingFrame):
|
class MissionFrame(HeadingFrame):
|
||||||
@ -238,35 +331,33 @@ class MissionFrame(HeadingFrame):
|
|||||||
self.controller = controller
|
self.controller = controller
|
||||||
self.emitter = controller.get_emitter()
|
self.emitter = controller.get_emitter()
|
||||||
|
|
||||||
self.mission_hbox = FolderHBox(offline.mission_button)
|
self.mission_hbox = FolderHBox(
|
||||||
|
controller, offline.mission_button, offline.mission_eventbox
|
||||||
|
)
|
||||||
self.mission_button = self.mission_hbox.get_button()
|
self.mission_button = self.mission_hbox.get_button()
|
||||||
self.mission_button.connect("clicked", self._on_mission_button_clicked)
|
self.mission_button.connect("clicked", self._on_mission_button_clicked)
|
||||||
|
|
||||||
self.warning = Gtk.Label(
|
self.frame.add(self.mission_hbox)
|
||||||
halign=Gtk.Align.START, margin_start=10, margin_bottom=5
|
|
||||||
)
|
|
||||||
self.vbox = VBox()
|
|
||||||
self.vbox.extend([self.mission_hbox, self.warning])
|
|
||||||
self.frame.add(self.vbox)
|
|
||||||
|
|
||||||
self.emitter.connect("custom_mission_loaded", self._on_mission_loaded)
|
self.emitter.connect("custom_mission_loaded", self._on_mission_loaded)
|
||||||
|
|
||||||
def _on_mission_loaded(
|
def _on_mission_loaded(
|
||||||
self, emitter: "Emitter", folder: str, is_valid: bool
|
self, emitter: "Emitter", folder: str, is_valid: bool
|
||||||
) -> None:
|
) -> None:
|
||||||
self.mission_hbox.set_label(folder)
|
if is_valid:
|
||||||
if not is_valid:
|
self.mission_hbox.set_folder(folder)
|
||||||
self.warning.show()
|
|
||||||
self.warning.set_label(offline.no_mission)
|
|
||||||
else:
|
else:
|
||||||
self.warning.hide()
|
self.mission_hbox.present_error(FolderError.NO_VALID_MISSION, folder)
|
||||||
|
|
||||||
def _on_mission_button_clicked(self, button: Gtk.Button) -> None:
|
def _on_mission_button_clicked(self, button: Gtk.Button) -> None:
|
||||||
self.parent.offline_man.get_mission()
|
self.parent.offline_man.get_mission()
|
||||||
|
|
||||||
|
def get_mission(self) -> str:
|
||||||
|
return self.mission_hbox.get_folder()
|
||||||
|
|
||||||
|
|
||||||
class RadioFrame(HeadingFrame):
|
class RadioFrame(HeadingFrame):
|
||||||
def __init__(self, controller: "Controller") -> None:
|
def __init__(self, parent: OfflineLoader, controller: "Controller") -> None:
|
||||||
super().__init__(heading=offline.version)
|
super().__init__(heading=offline.version)
|
||||||
|
|
||||||
self.controller = controller
|
self.controller = controller
|
||||||
@ -286,13 +377,10 @@ class RadioFrame(HeadingFrame):
|
|||||||
|
|
||||||
self.frame.add(self.radio_box)
|
self.frame.add(self.radio_box)
|
||||||
|
|
||||||
# TODO: abstract out of here
|
# TODO: cleaner delegation
|
||||||
default_steam_path = self.controller.query_config(Preferences.DEFAULT)
|
has_dayz_exp = parent.offline_man.has_dayz_exp()
|
||||||
steam_path = Path(default_steam_path)
|
|
||||||
dayz_exp = PeFile.get_pretty_version(steam_path, APPID_DAYZ_EXP)
|
|
||||||
|
|
||||||
self.dayz.connect("toggled", self._on_radio_toggled)
|
self.dayz.connect("toggled", self._on_radio_toggled)
|
||||||
if dayz_exp is None:
|
if has_dayz_exp is False:
|
||||||
self.dayz_exp.set_sensitive(False)
|
self.dayz_exp.set_sensitive(False)
|
||||||
|
|
||||||
def _on_radio_toggled(self, radio: Gtk.RadioButton) -> None:
|
def _on_radio_toggled(self, radio: Gtk.RadioButton) -> None:
|
||||||
@ -315,6 +403,7 @@ class OfflineLoader(Gtk.Box):
|
|||||||
|
|
||||||
self.controller = controller
|
self.controller = controller
|
||||||
self.controller.register_widget("offline_loader", self)
|
self.controller.register_widget("offline_loader", self)
|
||||||
|
self.emitter = controller.get_emitter()
|
||||||
self.offline_man = OfflineManager(controller)
|
self.offline_man = OfflineManager(controller)
|
||||||
|
|
||||||
self.add(PageHeading(offline.heading))
|
self.add(PageHeading(offline.heading))
|
||||||
@ -324,7 +413,7 @@ class OfflineLoader(Gtk.Box):
|
|||||||
|
|
||||||
self.custom_tree = OfflineModTreeView(controller)
|
self.custom_tree = OfflineModTreeView(controller)
|
||||||
self.mission_frame = MissionFrame(self, controller)
|
self.mission_frame = MissionFrame(self, controller)
|
||||||
self.radio_frame = RadioFrame(controller)
|
self.radio_frame = RadioFrame(self, controller)
|
||||||
|
|
||||||
self.scrollable = Gtk.ScrolledWindow(
|
self.scrollable = Gtk.ScrolledWindow(
|
||||||
vexpand=True, propagate_natural_height=True
|
vexpand=True, propagate_natural_height=True
|
||||||
@ -343,7 +432,7 @@ class OfflineLoader(Gtk.Box):
|
|||||||
# TODO: share ConnectBox class with preconnect dialog?
|
# TODO: share ConnectBox class with preconnect dialog?
|
||||||
self.button_box = HBox(spacing=5)
|
self.button_box = HBox(spacing=5)
|
||||||
self.button_box.set_halign(Gtk.Align.END)
|
self.button_box.set_halign(Gtk.Align.END)
|
||||||
self.button_box.set_margin_top(5)
|
self.button_box.set_margin_top(15)
|
||||||
self.back = Gtk.Button(label="Back")
|
self.back = Gtk.Button(label="Back")
|
||||||
self.ok = Gtk.Button(label="Launch", sensitive=False)
|
self.ok = Gtk.Button(label="Launch", sensitive=False)
|
||||||
self.back.connect("clicked", self._on_back_clicked)
|
self.back.connect("clicked", self._on_back_clicked)
|
||||||
@ -365,28 +454,24 @@ class OfflineLoader(Gtk.Box):
|
|||||||
self.ok.set_sensitive(True)
|
self.ok.set_sensitive(True)
|
||||||
|
|
||||||
def _on_keypress(self, widget: Self, event: Gdk.EventKey) -> None:
|
def _on_keypress(self, widget: Self, event: Gdk.EventKey) -> None:
|
||||||
|
# FIXME: widget is not always in focus
|
||||||
if event.keyval == Gdk.KEY_Escape:
|
if event.keyval == Gdk.KEY_Escape:
|
||||||
self.back.emit("clicked")
|
self.back.emit("clicked")
|
||||||
|
|
||||||
def populate(self, store: Union["FastInsertListStore", None]) -> None:
|
def populate(self, store: Union["FastInsertListStore", None]) -> None:
|
||||||
self.local_frame.set_model(store)
|
|
||||||
if store is None:
|
|
||||||
self.local_frame.start_empty()
|
|
||||||
else:
|
|
||||||
self.local_frame.hide_errors()
|
|
||||||
# NOTE: suppress custom tree until explicitly loaded
|
# NOTE: suppress custom tree until explicitly loaded
|
||||||
self.custom_frame.hide_all()
|
if store is None:
|
||||||
|
return
|
||||||
|
self.local_frame.start(store)
|
||||||
|
# self.custom_frame.start(store)
|
||||||
|
|
||||||
def _on_back_clicked(self, button: Gtk.Button) -> None:
|
def _on_back_clicked(self, button: Gtk.Button) -> None:
|
||||||
self.controller.open_page(NotebookPage.MODS)
|
self.controller.open_page(NotebookPage.MODS)
|
||||||
|
|
||||||
def _on_ok_clicked(self, button: Gtk.Button) -> None:
|
def _on_ok_clicked(self, button: Gtk.Button) -> None:
|
||||||
# TODO: grab all values in one pass
|
appid = self.radio_frame.get_appid()
|
||||||
# appid = self.radio_frame.get_appid()
|
mission = self.mission_frame.get_mission()
|
||||||
# mission = self.mission_frame.get_mission()
|
local_mods = self.local_frame.get_mods()
|
||||||
# local_mods = self.local_frame.get_mods()
|
custom_folder = self.custom_frame.get_folder()
|
||||||
# custom_mods = self.custom_frame.get_mods()
|
custom_mods = self.custom_frame.get_mods()
|
||||||
# cf. api.mods._hash(uid, use_custom=True)
|
self.offline_man.launch(appid, mission, local_mods, custom_folder, custom_mods)
|
||||||
# TODO: set up all symlinks prior to launch, including custom ones
|
|
||||||
# self.offline_man.setup(appid, mission, local_mods, custom_mods)
|
|
||||||
pass
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user