mirror of
https://github.com/aclist/dztui.git
synced 2026-08-30 03:37:00 +02:00
Compare commits
No commits in common. "ad85bb73437d686fbdbbba424124c986fbabdae9" and "401c8625adbde30e1a9f4a7c45246be83c9854e0" have entirely different histories.
ad85bb7343
...
401c8625ad
@ -1,6 +1,6 @@
|
|||||||
import hashlib
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
import re
|
import shlex
|
||||||
|
|
||||||
from concurrent.futures import wait
|
from concurrent.futures import wait
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
@ -48,29 +48,36 @@ def get_local_mods(workshop_path: Path) -> list[Path]:
|
|||||||
mods = [file for file in workshop_path.iterdir() if file.is_dir()]
|
mods = [file for file in workshop_path.iterdir() if file.is_dir()]
|
||||||
return mods
|
return mods
|
||||||
|
|
||||||
def is_mission(path: Path) -> bool:
|
|
||||||
# TODO: parse integrity of other files
|
|
||||||
file = path / "init.c"
|
|
||||||
return file.exists()
|
|
||||||
|
|
||||||
def tokenize(mod: Path) -> dict[Any] | None:
|
def parse_meta(file: Path) -> ModMeta | None:
|
||||||
file = mod.joinpath("meta.cpp")
|
mod = file / "meta.cpp"
|
||||||
delimiter=r"\s*=\s*"
|
if mod.exists() is False:
|
||||||
modmeta = {}
|
|
||||||
try:
|
|
||||||
with open(file, "r", encoding="utf-8") as file:
|
|
||||||
for line in file:
|
|
||||||
line = line.strip().rstrip(";")
|
|
||||||
if not line:
|
|
||||||
continue
|
|
||||||
els = re.split(delimiter, line, maxsplit=1)
|
|
||||||
if len(els) == 2:
|
|
||||||
key, value = els
|
|
||||||
modmeta[key.strip()] = value.strip('"')
|
|
||||||
return modmeta
|
|
||||||
except Exception as e:
|
|
||||||
logger.critical(e)
|
|
||||||
return None
|
return None
|
||||||
|
with open(file / "meta.cpp", "r") as f:
|
||||||
|
st = f.read()
|
||||||
|
lex = shlex.shlex(st)
|
||||||
|
lex.whitespace += "=;"
|
||||||
|
v = []
|
||||||
|
while True:
|
||||||
|
tok = lex.get_token()
|
||||||
|
if not tok:
|
||||||
|
break
|
||||||
|
if tok == "protocol" or tok == "publishedid":
|
||||||
|
ntok = lex.get_token()
|
||||||
|
elif tok == "timestamp":
|
||||||
|
# NOTE: some malformed .NET tick conversions result in numbers < 0
|
||||||
|
ntok = lex.get_token()
|
||||||
|
if ntok == "-":
|
||||||
|
ntok += str(lex.get_token())
|
||||||
|
elif tok == "name":
|
||||||
|
ntok = lex.get_token()
|
||||||
|
if ntok is not None:
|
||||||
|
ntok = ntok.split('"')[1]
|
||||||
|
if ntok is not None:
|
||||||
|
v.append(ntok)
|
||||||
|
meta = ModMeta(*v)
|
||||||
|
return meta
|
||||||
|
|
||||||
|
|
||||||
def get_mod_size(path: Path) -> float:
|
def get_mod_size(path: Path) -> float:
|
||||||
s = 0
|
s = 0
|
||||||
@ -83,20 +90,21 @@ def get_mod_size(path: Path) -> float:
|
|||||||
def get_custom_mods(path: Path) -> list[Any]:
|
def get_custom_mods(path: Path) -> list[Any]:
|
||||||
mods = get_local_mods(path)
|
mods = get_local_mods(path)
|
||||||
# TODO: error handling
|
# TODO: error handling
|
||||||
return parse_mods(mods, use_custom=True)
|
return parse_mods(mods)
|
||||||
|
|
||||||
|
|
||||||
def parse_mods(mods: list[Path], use_custom: bool = False) -> list[Any]:
|
def parse_mods(mods: list[Path]) -> list[Any]:
|
||||||
clean = []
|
clean = []
|
||||||
for mod in mods:
|
for mod in mods:
|
||||||
mod_dir = mod.name
|
mod_dir = mod.name
|
||||||
symlink = _hash(mod_dir, use_custom)
|
symlink = _hash(mod_dir)
|
||||||
meta = tokenize(mod)
|
# FIXME: malformed .cpp files could break this
|
||||||
|
meta = parse_meta(mod)
|
||||||
if meta is None:
|
if meta is None:
|
||||||
continue
|
continue
|
||||||
size = get_mod_size(mod)
|
size = get_mod_size(mod)
|
||||||
# NOTE: final col is cell renderer highlight toggle
|
# NOTE: final col is cell renderer highlight toggle
|
||||||
clean.append([meta["name"], symlink, mod_dir, size, False])
|
clean.append([meta.name, symlink, mod_dir, size, False])
|
||||||
clean.sort(key=lambda row: str(row[0]).casefold())
|
clean.sort(key=lambda row: str(row[0]).casefold())
|
||||||
return clean
|
return clean
|
||||||
|
|
||||||
|
|||||||
@ -156,9 +156,5 @@ class Emitter(GObject.GObject):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=(object, str, bool))
|
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=(object, str, bool))
|
||||||
def custom_mods_loaded(self, store: "FastInsertListStore", folder: str) -> None:
|
def custom_mods_loaded(self, store: "FastInsertListStore", folder: str, has_duplicates: bool) -> None:
|
||||||
pass
|
|
||||||
|
|
||||||
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=(str, bool))
|
|
||||||
def custom_mission_loaded(self, folder: str, is_valid: bool) -> None:
|
|
||||||
pass
|
pass
|
||||||
|
|||||||
@ -25,7 +25,7 @@ from dzgui.model.servers import ServerModelManager
|
|||||||
from dzgui.util.diag import write_diagnostic
|
from dzgui.util.diag import write_diagnostic
|
||||||
from dzgui.util.format import format_player_count
|
from dzgui.util.format import format_player_count
|
||||||
from dzgui.util.open_links import open_user_workshop, open_workshop_page
|
from dzgui.util.open_links import open_user_workshop, open_workshop_page
|
||||||
from dzgui.views.dialogs.filepicker import FilePicker
|
from dzgui.views.dialogs.filepicker import FilePicker, FolderPicker
|
||||||
from dzgui.views.dialogs.generic import ExceptionDialog
|
from dzgui.views.dialogs.generic import ExceptionDialog
|
||||||
|
|
||||||
import gi
|
import gi
|
||||||
@ -36,6 +36,7 @@ from gi.repository import Gtk, Gdk, GLib, GObject # noqa E402
|
|||||||
logger = logging.getLogger(APP_NAME)
|
logger = logging.getLogger(APP_NAME)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from pathlib import Path
|
||||||
from dzgui.api.servers import Record
|
from dzgui.api.servers import Record
|
||||||
from dzgui.const.enum import ServerTab
|
from dzgui.const.enum import ServerTab
|
||||||
from dzgui.managers.connection import Prerequisites
|
from dzgui.managers.connection import Prerequisites
|
||||||
@ -300,6 +301,12 @@ class Controller(GObject.GObject):
|
|||||||
dialog = ExceptionDialog(self, str(e))
|
dialog = ExceptionDialog(self, str(e))
|
||||||
dialog.run()
|
dialog.run()
|
||||||
|
|
||||||
|
def set_custom_folder(self) -> Union["Path", None]:
|
||||||
|
picker = FolderPicker(self.mediator.window)
|
||||||
|
folder = picker.pick_folder()
|
||||||
|
picker.destroy()
|
||||||
|
return folder
|
||||||
|
|
||||||
def update_api_key(self, key: Preferences, text: str) -> None:
|
def update_api_key(self, key: Preferences, text: str) -> None:
|
||||||
self.config_man.update_api_key(key, text)
|
self.config_man.update_api_key(key, text)
|
||||||
|
|
||||||
|
|||||||
@ -48,7 +48,7 @@ class ModManager:
|
|||||||
self.prefs = controller.get_prefs()
|
self.prefs = controller.get_prefs()
|
||||||
self.path = controller.query_config(Preferences.DEFAULT)
|
self.path = controller.query_config(Preferences.DEFAULT)
|
||||||
|
|
||||||
self.store: FastInsertListStore | None = None
|
self.store: FastInsertListStore
|
||||||
|
|
||||||
self.thread_man = ThreadingManager(controller)
|
self.thread_man = ThreadingManager(controller)
|
||||||
|
|
||||||
@ -68,8 +68,6 @@ class ModManager:
|
|||||||
|
|
||||||
def _on_mods_loaded(self) -> None:
|
def _on_mods_loaded(self) -> None:
|
||||||
self.treeview.set_model(self.store)
|
self.treeview.set_model(self.store)
|
||||||
if self.store is None:
|
|
||||||
return
|
|
||||||
msg = self.format_mod_statusbar()
|
msg = self.format_mod_statusbar()
|
||||||
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)
|
||||||
@ -126,8 +124,6 @@ class ModManager:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def _on_mods_deleted(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:
|
for _iter in iters:
|
||||||
self.store.remove(_iter)
|
self.store.remove(_iter)
|
||||||
remove_stale_signatures(self.prefs.paths.config, self.prefs.paths.version)
|
remove_stale_signatures(self.prefs.paths.config, self.prefs.paths.version)
|
||||||
|
|||||||
@ -1,14 +1,13 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING, Union
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from dzgui.api.mods import is_mission, get_custom_mods
|
from dzgui.api.mods import get_custom_mods
|
||||||
from dzgui.api.steam import launch_offline
|
from dzgui.api.steam import launch_offline
|
||||||
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 clone_symlinks
|
||||||
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
|
||||||
@ -30,31 +29,20 @@ class OfflineManager:
|
|||||||
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: strings
|
||||||
def get_mission(self) -> None:
|
@call_on_thread("parsing")
|
||||||
folder = self.open_folderpicker()
|
|
||||||
if folder is None:
|
|
||||||
return
|
|
||||||
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)
|
|
||||||
|
|
||||||
def get_custom_mods(self, local_mods: list[str]) -> None:
|
|
||||||
folder = self.open_folderpicker()
|
|
||||||
if folder is None:
|
|
||||||
return
|
|
||||||
self.parse_custom_mods(local_mods, folder)
|
|
||||||
|
|
||||||
@call_on_thread(dialogs.parsing_mods)
|
|
||||||
def parse_custom_mods(self, local_mods: list[str], folder: str) -> None:
|
def parse_custom_mods(self, local_mods: list[str], 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(lambda: self.emitter.emit("custom_mods_loaded", store, folder))
|
has_duplicates = False
|
||||||
|
for row in store:
|
||||||
|
if row[1] in local_mods:
|
||||||
|
row[-1]=True
|
||||||
|
has_duplicates = True
|
||||||
|
|
||||||
|
func = StoredFunc(lambda: self.emitter.emit("custom_mods_loaded", store, folder, has_duplicates))
|
||||||
self.thread_man.set_cleanup_func(func)
|
self.thread_man.set_cleanup_func(func)
|
||||||
|
|
||||||
def setup(
|
def setup(
|
||||||
@ -66,6 +54,7 @@ class OfflineManager:
|
|||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
self.appid = appid
|
self.appid = appid
|
||||||
|
self.mission_folder = mission
|
||||||
self.local_mods = local_mods
|
self.local_mods = local_mods
|
||||||
self.custom_mods = custom_mods
|
self.custom_mods = custom_mods
|
||||||
|
|
||||||
@ -90,9 +79,3 @@ class OfflineManager:
|
|||||||
combined_mods.extend(new_symlinks)
|
combined_mods.extend(new_symlinks)
|
||||||
|
|
||||||
launch_offline(client, self.appid, name, combined_mods, self.mission_folder)
|
launch_offline(client, self.appid, name, combined_mods, self.mission_folder)
|
||||||
|
|
||||||
def open_folderpicker(self) -> Union["Path", None]:
|
|
||||||
picker = FolderPicker(self.controller.get_window())
|
|
||||||
folder = picker.pick_folder()
|
|
||||||
picker.destroy()
|
|
||||||
return folder
|
|
||||||
|
|||||||
@ -9,10 +9,5 @@ load_error_lan = "Failed to find any servers on your network.\nCheck the server
|
|||||||
fetching_mods = "Fetching mod metadata"
|
fetching_mods = "Fetching mod metadata"
|
||||||
deleting_mods = "Deleting mods"
|
deleting_mods = "Deleting mods"
|
||||||
scanning_mods = "Scanning mods"
|
scanning_mods = "Scanning mods"
|
||||||
parsing_mods = "Parsing mods"
|
|
||||||
|
|
||||||
checking_api = "Checking API key"
|
checking_api = "Checking API key"
|
||||||
|
|
||||||
running = "Running"
|
|
||||||
failed = "FAILED"
|
|
||||||
ok = "OK"
|
|
||||||
|
|||||||
@ -9,6 +9,3 @@ custom_frame = "Custom mods"
|
|||||||
custom_button = "Set custom mod folder"
|
custom_button = "Set custom mod folder"
|
||||||
|
|
||||||
local_frame = "Installed mods"
|
local_frame = "Installed mods"
|
||||||
|
|
||||||
no_mods = "No valid mods found"
|
|
||||||
no_mission = "Not a valid mission"
|
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
import sys
|
import sys
|
||||||
from typing import Any, Literal, Self, TYPE_CHECKING
|
from typing import Literal, TYPE_CHECKING
|
||||||
|
|
||||||
# import time
|
# import time
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
from typing import Any, Self
|
||||||
|
|
||||||
# TODO: import dialog titles
|
# TODO: import dialog titles
|
||||||
from dzgui.api.mods import remove_stale_signatures as remove_stale
|
from dzgui.api.mods import remove_stale_signatures as remove_stale
|
||||||
@ -13,7 +14,7 @@ from dzgui.init.dayz import is_dayz_installed
|
|||||||
from dzgui.init.update import check_updates
|
from dzgui.init.update import check_updates
|
||||||
from dzgui.const.constants import EXPAND, FILL
|
from dzgui.const.constants import EXPAND, FILL
|
||||||
from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager
|
from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager
|
||||||
from dzgui.strings import preboot, dialogs
|
from dzgui.strings import preboot
|
||||||
from dzgui.util.format import format_exception
|
from dzgui.util.format import format_exception
|
||||||
from dzgui.util.strings import dialog_header
|
from dzgui.util.strings import dialog_header
|
||||||
from dzgui.util.symlink import rebuild_symlinks
|
from dzgui.util.symlink import rebuild_symlinks
|
||||||
@ -58,9 +59,6 @@ class BootDialog(Gtk.Dialog):
|
|||||||
self.view = Gtk.TreeView(enable_search=False, headers_visible=False)
|
self.view = Gtk.TreeView(enable_search=False, headers_visible=False)
|
||||||
self.view.set_model(self.store)
|
self.view.set_model(self.store)
|
||||||
|
|
||||||
# TODO: capture sigint in early dialogs
|
|
||||||
self.connect("key-press-event", self._on_keypress)
|
|
||||||
|
|
||||||
for i, column_title in enumerate(["Task", "State"]):
|
for i, column_title in enumerate(["Task", "State"]):
|
||||||
renderer = Gtk.CellRendererText()
|
renderer = Gtk.CellRendererText()
|
||||||
column = Gtk.TreeViewColumn(column_title, renderer, text=i)
|
column = Gtk.TreeViewColumn(column_title, renderer, text=i)
|
||||||
@ -140,10 +138,6 @@ class BootDialog(Gtk.Dialog):
|
|||||||
|
|
||||||
GLib.timeout_add(100, self.pulse_spinner)
|
GLib.timeout_add(100, self.pulse_spinner)
|
||||||
|
|
||||||
def _on_keypress(self, widget: Self, event: Gdk.EventKey) -> None:
|
|
||||||
if event.keyval == Gdk.KEY_Escape:
|
|
||||||
self.exit_button.emit("clicked")
|
|
||||||
|
|
||||||
def pulse_spinner(self) -> Literal[True]:
|
def pulse_spinner(self) -> Literal[True]:
|
||||||
for row in self.store:
|
for row in self.store:
|
||||||
if row[2]:
|
if row[2]:
|
||||||
@ -190,10 +184,10 @@ class BootDialog(Gtk.Dialog):
|
|||||||
self.thread_man.set_cleanup_func(callback)
|
self.thread_man.set_cleanup_func(callback)
|
||||||
|
|
||||||
def update_task(self, task: str) -> None:
|
def update_task(self, task: str) -> None:
|
||||||
self.store.append((task, dialogs.running, True, 0))
|
self.store.append((task, "Running", True, 0))
|
||||||
|
|
||||||
def update_status(self, state: Success) -> None:
|
def update_status(self, state: Success) -> None:
|
||||||
d = {Success.OK: dialogs.ok, Success.FAIL: dialogs.failed}
|
d = {Success.OK: "OK", Success.FAIL: "FAILED"}
|
||||||
msg = d[state]
|
msg = d[state]
|
||||||
self.store[len(self.store) - 1][1] = msg
|
self.store[len(self.store) - 1][1] = msg
|
||||||
self.iter_step()
|
self.iter_step()
|
||||||
@ -206,7 +200,7 @@ class BootDialog(Gtk.Dialog):
|
|||||||
it: Gtk.TreeIter,
|
it: Gtk.TreeIter,
|
||||||
data: Any,
|
data: Any,
|
||||||
) -> None:
|
) -> None:
|
||||||
if model[it][1] == dialogs.running:
|
if model[it][1] == "Running":
|
||||||
cell.set_property("visible", True)
|
cell.set_property("visible", True)
|
||||||
else:
|
else:
|
||||||
cell.set_property("visible", False)
|
cell.set_property("visible", False)
|
||||||
@ -223,9 +217,9 @@ class BootDialog(Gtk.Dialog):
|
|||||||
state = model[it][1]
|
state = model[it][1]
|
||||||
if column.get_sort_column_id() != 1:
|
if column.get_sort_column_id() != 1:
|
||||||
return
|
return
|
||||||
if state == dialogs.ok:
|
if state == "OK":
|
||||||
cell.set_property(prop, HEX_GREEN)
|
cell.set_property(prop, HEX_GREEN)
|
||||||
elif state == dialogs.failed:
|
elif state == "FAILED":
|
||||||
cell.set_property(prop, HEX_RED)
|
cell.set_property(prop, HEX_RED)
|
||||||
else:
|
else:
|
||||||
cell.set_property(prop, None)
|
cell.set_property(prop, None)
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Self, Sequence, TYPE_CHECKING, Union
|
from typing import Self, Sequence, TYPE_CHECKING
|
||||||
|
|
||||||
from dzgui.util import css
|
from dzgui.util import css
|
||||||
import dzgui.api.pefile as PeFile
|
import dzgui.api.pefile as PeFile
|
||||||
@ -62,36 +62,27 @@ class FolderHBox(HBox):
|
|||||||
super().__init__(spacing=10)
|
super().__init__(spacing=10)
|
||||||
|
|
||||||
self.set_margin_start(10)
|
self.set_margin_start(10)
|
||||||
self.set_margin_end(10)
|
self.set_margin_bottom(10)
|
||||||
self.set_margin_bottom(5)
|
|
||||||
|
|
||||||
# TODO: alternate class for left-aligned icons
|
# TODO: alternate class for left-aligned icons
|
||||||
self.button = IconTextButton(FOLDER, btn_label)
|
self.button = IconTextButton(FOLDER, btn_label)
|
||||||
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)
|
||||||
|
|
||||||
self.scrolled_label = Gtk.ScrolledWindow(propagate_natural_width=True, halign=Gtk.Align.START)
|
|
||||||
self.label = Gtk.Label()
|
self.label = Gtk.Label()
|
||||||
self.scrolled_label.add(self.label)
|
self.extend([self.button, self.label])
|
||||||
self.extend([self.button, self.scrolled_label])
|
|
||||||
|
|
||||||
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:
|
def set_label(self, label: str) -> None:
|
||||||
# TODO: strings
|
self.label.set_label(label)
|
||||||
prefix = "Current folder: "
|
|
||||||
self.label.set_label(prefix + label)
|
|
||||||
|
|
||||||
def hide_label(self) -> None:
|
|
||||||
self.label.hide()
|
|
||||||
|
|
||||||
|
|
||||||
class ModFrame(HeadingFrame):
|
class ModFrame(HeadingFrame):
|
||||||
def __init__(self, parent: OfflineLoader, controller: "Controller", label: str) -> None:
|
def __init__(self, controller: "Controller", label: str) -> None:
|
||||||
super().__init__(heading=label)
|
super().__init__(heading=label)
|
||||||
|
|
||||||
self.parent = parent
|
|
||||||
self.vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
self.vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||||
|
|
||||||
self.tree = OfflineModTreeView(controller)
|
self.tree = OfflineModTreeView(controller)
|
||||||
@ -112,38 +103,26 @@ class ModFrame(HeadingFrame):
|
|||||||
|
|
||||||
self.vbox.pack_end(self.tree_vbox, expand=True, fill=True, padding=3)
|
self.vbox.pack_end(self.tree_vbox, expand=True, fill=True, padding=3)
|
||||||
self.frame.add(self.vbox)
|
self.frame.add(self.vbox)
|
||||||
|
# self.vbox.add(self.tree_vbox)
|
||||||
|
# self.none_label = Gtk.Label(label="No mods found", halign=Gtk.Align.START)
|
||||||
|
# self.vbox.pack_end(self.none_label, expand=True, fill=True, padding=3)
|
||||||
|
# self.none_label.hide()
|
||||||
|
|
||||||
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
|
|
||||||
# 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:
|
|
||||||
self.error_label.set_label(msg)
|
|
||||||
self.error_label.show()
|
|
||||||
|
|
||||||
def hide_errors(self) -> None:
|
|
||||||
self.error_label.hide()
|
|
||||||
|
|
||||||
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
|
# TODO: warnings area
|
||||||
# name collision within custom mods
|
# valid warnings (one at a time):
|
||||||
|
# - name collision within custom mods
|
||||||
|
# - name collision b/w custom and local (permissible)
|
||||||
|
# - no mods
|
||||||
|
# - not a valid mission
|
||||||
|
|
||||||
def get_mods(self) -> list[str]:
|
def get_mods(self) -> list[str]:
|
||||||
model, treeiters = self.tree.get_selection().get_selected_rows()
|
model = self.tree.get_model()
|
||||||
if model is None:
|
if model is None:
|
||||||
return []
|
return []
|
||||||
return [model[_iter][1] for _iter in treeiters]
|
return [row[1] for row in model]
|
||||||
|
|
||||||
def collapse_tree(self) -> None:
|
def collapse_tree(self) -> None:
|
||||||
self.tree_vbox.hide()
|
self.tree_vbox.hide()
|
||||||
@ -154,7 +133,7 @@ class ModFrame(HeadingFrame):
|
|||||||
def pack(self, widget: Gtk.Widget) -> None:
|
def pack(self, widget: Gtk.Widget) -> None:
|
||||||
self.vbox.pack_start(widget, expand=False, fill=False, padding=5)
|
self.vbox.pack_start(widget, expand=False, fill=False, padding=5)
|
||||||
|
|
||||||
def set_model(self, model: Union["FastInsertListStore", None]) -> None:
|
def set_model(self, model: "FastInsertListStore") -> None:
|
||||||
self.tree.set_model(model)
|
self.tree.set_model(model)
|
||||||
self.tree.mod_man.store = model
|
self.tree.mod_man.store = model
|
||||||
self.set_cursor()
|
self.set_cursor()
|
||||||
@ -166,7 +145,6 @@ 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)
|
||||||
self.parent.check_button()
|
|
||||||
|
|
||||||
def set_cursor(self) -> None:
|
def set_cursor(self) -> None:
|
||||||
path = Gtk.TreePath.new_from_indices([0])
|
path = Gtk.TreePath.new_from_indices([0])
|
||||||
@ -178,7 +156,7 @@ class CustomModFrame(ModFrame):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self, parent: OfflineLoader, controller: "Controller", heading: str
|
self, parent: OfflineLoader, controller: "Controller", heading: str
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(parent, controller, heading)
|
super().__init__(controller, heading)
|
||||||
|
|
||||||
self.parent = parent
|
self.parent = parent
|
||||||
|
|
||||||
@ -193,62 +171,34 @@ class CustomModFrame(ModFrame):
|
|||||||
|
|
||||||
self.emitter.connect("custom_mods_loaded", self._on_custom_mods_loaded)
|
self.emitter.connect("custom_mods_loaded", self._on_custom_mods_loaded)
|
||||||
|
|
||||||
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.tree_vbox.hide()
|
|
||||||
|
|
||||||
def present_tree(self, folder: str, store: "FastInsertListStore") -> None:
|
|
||||||
self.custom_hbox.set_label(folder)
|
|
||||||
self.tree.set_model(store)
|
|
||||||
self.tree_vbox.show()
|
|
||||||
self.hide_errors()
|
|
||||||
|
|
||||||
def _on_custom_mods_loaded(
|
def _on_custom_mods_loaded(
|
||||||
self,
|
self,
|
||||||
emitter: "Emitter",
|
emitter: "Emitter",
|
||||||
store: "FastInsertListStore",
|
store: "FastInsertListStore",
|
||||||
folder: str,
|
folder: str,
|
||||||
|
has_duplicates: bool,
|
||||||
) -> None:
|
) -> None:
|
||||||
if len(store) == 0:
|
if len(store) == 0:
|
||||||
self.hide_tree()
|
# TODO: pop error dialog area
|
||||||
|
# block button access
|
||||||
|
pass
|
||||||
else:
|
else:
|
||||||
self.present_tree()
|
self.custom_hbox.set_label(folder)
|
||||||
|
self.tree.set_model(store)
|
||||||
|
self.tree_vbox.show()
|
||||||
|
|
||||||
|
if has_duplicates:
|
||||||
|
# TODO: pop relevant error
|
||||||
|
# duplicates should block button access
|
||||||
|
pass
|
||||||
|
|
||||||
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()
|
self.parent.parse_mods()
|
||||||
self.parent.offline_man.get_custom_mods(local_mods)
|
|
||||||
|
|
||||||
class MissionFrame(HeadingFrame):
|
def get_mods(self) -> list[str]:
|
||||||
def __init__(self, parent: OfflineLoader, controller: "Controller") -> None:
|
rows = self.tree.get_selection().get_selected_rows()
|
||||||
super().__init__(heading=offline.mission_frame)
|
dirs = [str(col[1]) for col in rows]
|
||||||
|
return dirs
|
||||||
self.parent = parent
|
|
||||||
self.controller = controller
|
|
||||||
self.emitter = controller.get_emitter()
|
|
||||||
|
|
||||||
self.mission_hbox = FolderHBox(offline.mission_button)
|
|
||||||
self.mission_button = self.mission_hbox.get_button()
|
|
||||||
self.mission_button.connect("clicked", self._on_mission_button_clicked)
|
|
||||||
|
|
||||||
self.warning = Gtk.Label(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)
|
|
||||||
|
|
||||||
def _on_mission_loaded(self, emitter: "Emitter", folder: str, is_valid: bool) -> None:
|
|
||||||
self.mission_hbox.set_label(folder)
|
|
||||||
if not is_valid:
|
|
||||||
self.warning.show()
|
|
||||||
self.warning.set_label(offline.no_mission)
|
|
||||||
else:
|
|
||||||
self.warning.hide()
|
|
||||||
|
|
||||||
def _on_mission_button_clicked(self, button: Gtk.Button) -> None:
|
|
||||||
self.parent.offline_man.get_mission()
|
|
||||||
|
|
||||||
|
|
||||||
class RadioFrame(HeadingFrame):
|
class RadioFrame(HeadingFrame):
|
||||||
@ -305,11 +255,20 @@ class OfflineLoader(Gtk.Box):
|
|||||||
|
|
||||||
self.add(PageHeading(offline.heading))
|
self.add(PageHeading(offline.heading))
|
||||||
|
|
||||||
self.local_frame = ModFrame(self, controller, offline.local_frame)
|
self.local_frame = ModFrame(controller, offline.local_frame)
|
||||||
self.custom_frame = CustomModFrame(self, controller, offline.custom_frame)
|
self.custom_frame = CustomModFrame(self, controller, offline.custom_frame)
|
||||||
|
|
||||||
|
# TODO: suppress symlink column
|
||||||
self.custom_tree = OfflineModTreeView(controller)
|
self.custom_tree = OfflineModTreeView(controller)
|
||||||
self.mission_frame = MissionFrame(self, controller)
|
|
||||||
|
# TODO: custom class
|
||||||
|
self.mission_hbox = FolderHBox(offline.mission_button)
|
||||||
|
self.mission_frame = HeadingFrame.new_with_widget_and_label(
|
||||||
|
self.mission_hbox, offline.mission_frame
|
||||||
|
)
|
||||||
|
self.mission_button = self.mission_hbox.get_button()
|
||||||
|
self.mission_button.connect("clicked", self._on_mission_clicked)
|
||||||
|
|
||||||
self.radio_frame = RadioFrame(controller)
|
self.radio_frame = RadioFrame(controller)
|
||||||
|
|
||||||
self.scrollable = Gtk.ScrolledWindow(
|
self.scrollable = Gtk.ScrolledWindow(
|
||||||
@ -331,7 +290,7 @@ class OfflineLoader(Gtk.Box):
|
|||||||
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(5)
|
||||||
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")
|
||||||
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)
|
||||||
self.connect("key-press-event", self._on_keypress)
|
self.connect("key-press-event", self._on_keypress)
|
||||||
@ -342,37 +301,46 @@ class OfflineLoader(Gtk.Box):
|
|||||||
self.add(self.scrollable)
|
self.add(self.scrollable)
|
||||||
self.add(self.button_box)
|
self.add(self.button_box)
|
||||||
|
|
||||||
def check_button(self) -> None:
|
def _on_mission_clicked(self, button: Gtk.Button) -> None:
|
||||||
|
# process path, cf. set_custom_folder()
|
||||||
|
# file = path / "init.c"
|
||||||
|
# if not file.exists():
|
||||||
|
# pop warning
|
||||||
|
pass
|
||||||
|
|
||||||
|
def parse_mods(self) -> None:
|
||||||
|
# TODO: check method on custom frame
|
||||||
|
# TODO: delegate folderpicker to offline manager
|
||||||
local_mods = self.local_frame.get_mods()
|
local_mods = self.local_frame.get_mods()
|
||||||
custom_mods = self.custom_frame.get_mods()
|
folder = self.controller.set_custom_folder()
|
||||||
if len(local_mods) == 0 and len(custom_mods) == 0:
|
if folder is not None:
|
||||||
self.ok.set_sensitive(False)
|
# TODO:
|
||||||
else:
|
self.offline_man.parse_custom_mods(local_mods, folder)
|
||||||
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:
|
||||||
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: "FastInsertListStore") -> None:
|
||||||
self.local_frame.set_model(store)
|
self.local_frame.set_model(store)
|
||||||
if store is None:
|
if store is None:
|
||||||
self.local_frame.start_empty()
|
self.local_frame.collapse_tree()
|
||||||
else:
|
# TODO: toggle if empty model, show warning label
|
||||||
self.local_frame.hide_errors()
|
# NOTE: there may be no local mods
|
||||||
# NOTE: suppress custom tree until explicitly loaded
|
# TODO: suppress trees if there are no mods
|
||||||
self.custom_frame.hide_all()
|
self.custom_frame.collapse_tree()
|
||||||
|
|
||||||
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
|
# TODO; block button access if no mods are selected
|
||||||
|
# TODO: consider hooking up to emitter and changing button state whenever mods are selected
|
||||||
|
|
||||||
# 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_mods = self.custom_frame.get_mods()
|
# custom_mods = self.custom_frame.get_mods()
|
||||||
# cf. api.mods._hash(uid, use_custom=True)
|
# cf. api.mods._hash(uid, use_custom=True)
|
||||||
# TODO: set up all symlinks prior to launch, including custom ones
|
|
||||||
# self.offline_man.setup(appid, mission, local_mods, custom_mods)
|
# self.offline_man.setup(appid, mission, local_mods, custom_mods)
|
||||||
pass
|
pass
|
||||||
|
|||||||
5
tests/fixtures/cpp/meta1.cpp
vendored
5
tests/fixtures/cpp/meta1.cpp
vendored
@ -1,5 +0,0 @@
|
|||||||
protocol = 1;
|
|
||||||
publishedid = 1234567890;
|
|
||||||
name = "ModName";
|
|
||||||
timestamp = 638278452000000000;
|
|
||||||
hash = "a1b2c3d4e5f6g7h8i9j0";
|
|
||||||
5
tests/fixtures/cpp/meta2.cpp
vendored
5
tests/fixtures/cpp/meta2.cpp
vendored
@ -1,5 +0,0 @@
|
|||||||
protocol = 1
|
|
||||||
publishedid = 1234567890
|
|
||||||
name = "ModName"
|
|
||||||
timestamp = 638278452000000000
|
|
||||||
hash = "a1b2c3d4e5f6g7h8i9j0"
|
|
||||||
5
tests/fixtures/cpp/meta3.cpp
vendored
5
tests/fixtures/cpp/meta3.cpp
vendored
@ -1,5 +0,0 @@
|
|||||||
protocol=1
|
|
||||||
publishedid=1234567890
|
|
||||||
name="ModName"
|
|
||||||
timestamp=638278452000000000
|
|
||||||
hash="a1b2c3d4e5f6g7h8i9j0"
|
|
||||||
5
tests/fixtures/cpp/meta4.cpp
vendored
5
tests/fixtures/cpp/meta4.cpp
vendored
@ -1,5 +0,0 @@
|
|||||||
protocol=1
|
|
||||||
publishedid=1234567890
|
|
||||||
name=ModName
|
|
||||||
timestamp=638278452000000000
|
|
||||||
hash=a1b2c3d4e5f6g7h8i9j0
|
|
||||||
5
tests/fixtures/cpp/meta5.cpp
vendored
5
tests/fixtures/cpp/meta5.cpp
vendored
@ -1,5 +0,0 @@
|
|||||||
protocol=1;
|
|
||||||
publishedid=1234567890;
|
|
||||||
name=ModName;
|
|
||||||
timestamp=638278452000000000;
|
|
||||||
hash=a1b2c3d4e5f6g7h8i9j0;
|
|
||||||
5
tests/fixtures/cpp/meta6.cpp
vendored
5
tests/fixtures/cpp/meta6.cpp
vendored
@ -1,5 +0,0 @@
|
|||||||
protocol=1
|
|
||||||
publishedid=1234567890;
|
|
||||||
name="ModName";
|
|
||||||
timestamp=638278452000000000;
|
|
||||||
hash= a1b2c3d4e5f6g7h8i9j0;
|
|
||||||
3
tests/fixtures/cpp/test.py
vendored
3
tests/fixtures/cpp/test.py
vendored
@ -1,3 +0,0 @@
|
|||||||
from dzgui.api.mods import tokenize
|
|
||||||
|
|
||||||
print(tokenize("meta4.cpp"))
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
import pytest
|
|
||||||
from dzgui.api.mods import tokenize
|
|
||||||
from tests.fixtures import fixture_path
|
|
||||||
|
|
||||||
@pytest.mark.mods
|
|
||||||
@pytest.mark.parametrize("fixture", [
|
|
||||||
fixture_path("cpp/meta1.cpp"),
|
|
||||||
fixture_path("cpp/meta2.cpp"),
|
|
||||||
fixture_path("cpp/meta3.cpp"),
|
|
||||||
fixture_path("cpp/meta4.cpp"),
|
|
||||||
fixture_path("cpp/meta5.cpp"),
|
|
||||||
fixture_path("cpp/meta6.cpp"),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
def test_modmeta(fixture: str) -> None:
|
|
||||||
meta = tokenize(fixture)
|
|
||||||
assert meta["name"] == "ModName"
|
|
||||||
Loading…
Reference in New Issue
Block a user