feat: preconnect dialog warnings (WIP)

This commit is contained in:
aclist 2026-05-07 02:47:42 +09:00
parent 7523bffbd8
commit e2022fdee7
9 changed files with 256 additions and 107 deletions

View File

@ -88,6 +88,17 @@ class A2SInfo:
qport = self.record.qport qport = self.record.qport
return source_info_to_dict(ip, qport, self.info) return source_info_to_dict(ip, qport, self.info)
def is_modded(self) -> bool:
if self.info is None:
raise ValueError("Cannot call this method on Nonetype")
try:
kw = self.info.keywords.split(",")
state = True if "mod" in kw else False
return state
except Exception as e:
logger.warning(e)
raise e
def get_netmask() -> str: def get_netmask() -> str:
hostname = os.uname()[1] hostname = os.uname()[1]

View File

@ -84,6 +84,9 @@ def get_needs_update(
def get_remote_signatures(mods: list[str]) -> list[tuple[str, int]]: def get_remote_signatures(mods: list[str]) -> list[tuple[str, int]]:
"""
Attempts to continue connecting even if signatures are empty
"""
payload: dict[str, str] = {} payload: dict[str, str] = {}
payload["itemcount"] = len(mods) payload["itemcount"] = len(mods)
for i, mod in enumerate(mods): for i, mod in enumerate(mods):

View File

@ -36,9 +36,9 @@ 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 dayzquery import DayzMod from dzgui.api.servers import Record
from dzgui.api.servers import A2SInfo, Record
from dzgui.const.enum import ServerTab from dzgui.const.enum import ServerTab
from dzgui.managers.connection import Prerequisites
from dzgui.managers.filter import FilterManager from dzgui.managers.filter import FilterManager
from dzgui.util.dist import Haversine from dzgui.util.dist import Haversine
from dzgui.views.base import Notebook, Grid, OuterWindow from dzgui.views.base import Notebook, Grid, OuterWindow
@ -88,6 +88,7 @@ class Controller(GObject.GObject):
self.pending_jobs = 1 self.pending_jobs = 1
self.exit_event = threading.Event() self.exit_event = threading.Event()
self.connection_man: ConnectionManager
def get_emitter(self) -> Emitter: def get_emitter(self) -> Emitter:
return self.emitter return self.emitter
@ -104,6 +105,7 @@ class Controller(GObject.GObject):
def set_prefs(self, prefs: UserPrefs) -> None: def set_prefs(self, prefs: UserPrefs) -> None:
self.config_man = ConfigManager(prefs, self) self.config_man = ConfigManager(prefs, self)
self.notes_man = NoteManager(self, prefs.paths.notes) self.notes_man = NoteManager(self, prefs.paths.notes)
self.prefs = prefs self.prefs = prefs
def query_config(self, key: Preferences) -> Any: def query_config(self, key: Preferences) -> Any:
@ -393,12 +395,15 @@ class Controller(GObject.GObject):
if addr.isdigit(): if addr.isdigit():
config_man = self.get_config_man() config_man = self.get_config_man()
key = config_man.lookup(Preferences.BM) key = config_man.lookup(Preferences.BM)
ConnectionManager(self).connect_by_id(int(addr), key) self.connection_man = ConnectionManager(self)
self.connection_man.connect_by_id(int(addr), key)
else: else:
ConnectionManager(self).connect_by_ip(addr) self.connection_man = ConnectionManager(self)
self.connection_man.connect_by_ip(addr)
def connect_by_record(self, record: "Record") -> None: def connect_by_record(self, record: "Record") -> None:
ConnectionManager(self).connect_by_record(record) self.connection_man = ConnectionManager(self)
self.connection_man.connect_by_record(record)
def get_details(self, record: "Record") -> None: def get_details(self, record: "Record") -> None:
ConnectionManager(self).query_details(record) ConnectionManager(self).query_details(record)
@ -458,10 +463,13 @@ class Controller(GObject.GObject):
def set_exit_event(self) -> None: def set_exit_event(self) -> None:
self.exit_event.set() self.exit_event.set()
def open_connection_assistant(self, res: "A2SInfo", mods: list["DayzMod"]) -> None: def open_connection_assistant(self, prereqs: "Prerequisites") -> None:
self.open_page(NotebookPage.CONNECTION) self.open_page(NotebookPage.CONNECTION)
self.mediator.preconnect.populate(res, mods) self.mediator.preconnect.populate(prereqs)
def set_start_tab(self) -> None: def set_start_tab(self) -> None:
ind = self.config_man.get_start_tab() ind = self.config_man.get_start_tab()
self.get_servers().notebook.set_current_page(ind) self.get_servers().notebook.set_current_page(ind)
def update_and_connect(self) -> None:
self.connection_man.update_and_connect()

View File

@ -6,14 +6,19 @@ from dzgui.views.dialogs.early_alert import EarlyAlertDialog
from dzgui.util.strings import init from dzgui.util.strings import init
def is_dayz_running() -> None: # TODO: simplify
def is_dayz_running(dialog: bool = False) -> None:
if dialog is False:
return is_running(DAYZ_BINARY)
if is_running(DAYZ_BINARY) is True: if is_running(DAYZ_BINARY) is True:
EarlyAlertDialog(init.is_dayz_running) EarlyAlertDialog(init.is_dayz_running)
sys.exit(1) sys.exit(1)
def is_steam_running() -> None: def is_steam_running(dialog: bool = False) -> None:
# TODO: check proc name of flatpak steam # TODO: check proc name of flatpak steam
if dialog is False:
return is_running(STEAM_CMD)
if is_running(STEAM_CMD) is False: if is_running(STEAM_CMD) is False:
EarlyAlertDialog(init.is_steam_running) EarlyAlertDialog(init.is_steam_running)
sys.exit(1) sys.exit(1)

View File

@ -130,8 +130,8 @@ def main() -> None:
EarlyAlertDialog(init.requires_steam) EarlyAlertDialog(init.requires_steam)
is_dayz_installed(XDG.config) is_dayz_installed(XDG.config)
is_dayz_running() is_dayz_running(dialog=True)
is_steam_running() is_steam_running(dialog=True)
# NOTE: clear versions file of unlinked mods # NOTE: clear versions file of unlinked mods
rebuild_symlinks(XDG.config) rebuild_symlinks(XDG.config)

View File

@ -1,6 +1,8 @@
import logging import logging
import shutil import shutil
from dataclasses import dataclass
from packaging.version import Version
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
@ -9,9 +11,17 @@ import dzgui.api.servers as Servers
from dzgui.api.steam import get_remote_signatures, get_needs_update from dzgui.api.steam import get_remote_signatures, get_needs_update
from dzgui.api.mods import get_local_mod_ids from dzgui.api.mods import get_local_mod_ids
from dzgui.const.constants import APP_NAME from dzgui.const.constants import (
APP_NAME,
APPID_DAYZ,
APPID_DAYZ_EXP,
APPNAME_DAYZ,
APPNAME_DAYZ_EXP,
)
from dzgui.const.enum import Preferences from dzgui.const.enum import Preferences
from dzgui.init.proc import is_dayz_running, is_steam_running
from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager
from dzgui.util.format import format_mib
from dzgui.util.strings import dialog, server_timeout, checkmark from dzgui.util.strings import dialog, server_timeout, checkmark
from dzgui.views.dialogs.generic import ExceptionDialog from dzgui.views.dialogs.generic import ExceptionDialog
from dzgui.views.dialogs.servers import ServerDetailsDialog, ServerModDialog from dzgui.views.dialogs.servers import ServerDetailsDialog, ServerModDialog
@ -22,18 +32,40 @@ gi.require_version("Gtk", "3.0")
from gi.repository import Gtk # noqa E402 from gi.repository import Gtk # noqa E402
if TYPE_CHECKING: if TYPE_CHECKING:
from dzgui.api.servers import A2SInfo from dzgui.api.servers import A2SInfo, Record
from dzgui.controllers.mc import Controller from dzgui.controllers.mc import Controller
logger = logging.getLogger(APP_NAME) logger = logging.getLogger(APP_NAME)
@dataclass(slots=True, frozen=True)
class Prerequisites:
name: str
appid: int
local_version: Version
remote_version: Version
build: str
binary_missing: bool
required_space: float
available_space: float
passworded: bool
dayz_running: bool
steam_running: bool
mods: list[str]
class ConnectionManager: class ConnectionManager:
def __init__(self, controller: "Controller") -> None: def __init__(self, controller: "Controller") -> None:
self.controller = controller self.controller = controller
self.thread_man = ThreadingManager(controller) self.thread_man = ThreadingManager(controller)
self.appid: int
self.record: Record
self.remote_mod_ids: list[str] = []
self.missing_mods: list[str] = []
@call_on_thread(dialog.querying) @call_on_thread(dialog.querying)
def connect_by_id(self, _id: int, key: str) -> None: def connect_by_id(self, _id: int, key: str) -> None:
res = Servers.query_by_id(_id, key) res = Servers.query_by_id(_id, key)
@ -56,52 +88,66 @@ class ConnectionManager:
return return
record = res.get_record() record = res.get_record()
try: info = res.get_info()
remote_mods = self._query_modlist(record)
remote_mod_ids = [mod[1] for mod in remote_mods] # NOTE: store metadata for later connection
except Exception as e: self.appid = info.game_id
print(e) self.record = record
self.thread_man.set_cleanup_func(failure_func, destroy_first=True)
return builds = {APPID_DAYZ: APPNAME_DAYZ, APPID_DAYZ_EXP: APPNAME_DAYZ_EXP}
build = builds[self.appid]
binary_missing = False
required_mib = 0.0
free_mib = 0.0
steam_path = Path(self.controller.query_config(Preferences.DEFAULT)) steam_path = Path(self.controller.query_config(Preferences.DEFAULT))
local_version = PeFile.get_pretty_version(steam_path, info.game_id)
if local_version is None:
local_version = "0.0.0"
binary_missing = True
hashes = get_remote_signatures(remote_mod_ids) remote_mods: list[str, str, str] = []
version_file = self.controller.get_prefs().paths.version if res.is_modded():
needs_update = get_needs_update(version_file, hashes) try:
remote_mods = self._query_modlist(record)
self.remote_mod_ids = [mod[1] for mod in remote_mods]
except Exception as e:
logger.warning(e)
self.thread_man.set_cleanup_func(failure_func, destroy_first=True)
return
# TODO: store mods that need update in class object for referencing later hashes = get_remote_signatures(self.remote_mod_ids)
# TODO: store remote destination to connect to version_file = self.controller.get_prefs().paths.version
self.missing_mods = get_needs_update(version_file, hashes)
# missing mods should be the totality of all mods with no signature if local_version is not None:
# missing = get_missing_mods(local_mod_ids, remote_mod_ids) pefile_path = PeFile.get_pefile_path(steam_path, info.game_id)
# print(missing) total, used, free = shutil.disk_usage(pefile_path)
# TODO: when downloading mods, create symlinks if missing if len(self.missing_mods) > 0:
required_size = sum(int(row[2]) for row in self.missing_mods)
required_mib = format_mib(required_size)
free_mib = format_mib(required_size)
# TODO: get missing mod sizes, warn if not enough space dayz_running = is_dayz_running()
info = res.get_info() steam_running = is_steam_running()
try: # TODO: is dayz downloading
dayz_path = PeFile.get_pefile_path(steam_path, info.game_id)
# TODO: handle missing path; do not calculate size if appid is missing
total, used, free = shutil.disk_usage(dayz_path)
if len(needs_update) > 0:
# TODO: generic mib function
required_size = sum(int(row[2]) for row in needs_update)
required_mib = round(required_size / (1024**2), 3)
free_mib = round(free / (1024**2), 3)
print(required_mib)
print(free_mib)
except Exception:
# TODO: if this fails, need to show missing build warning, not failure func
# build up list of warnings/errors
# logger.warning(e)
self.thread_man.set_cleanup_func(failure_func, destroy_first=True)
# TODO: number separator func prereqs = Prerequisites(
# TODO: pack a final PreReq struct with pre-processed values name=info.server_name,
appid=info.game_id,
local_version=Version(local_version),
remote_version=Version(info.version),
build=build,
binary_missing=binary_missing,
required_space=required_mib,
available_space=free_mib,
passworded=info.password_protected,
dayz_running=dayz_running,
steam_running=steam_running,
mods=remote_mods,
)
# TODO: connection assistant only receives user-facing warnings and list of mods func = StoredFunc(self.controller.open_connection_assistant, prereqs)
func = StoredFunc(self.controller.open_connection_assistant, res, remote_mods)
self.thread_man.set_cleanup_func(func, destroy_first=True) self.thread_man.set_cleanup_func(func, destroy_first=True)
@call_on_thread(dialog.querying) @call_on_thread(dialog.querying)
@ -157,10 +203,24 @@ class ConnectionManager:
dialog = ExceptionDialog(self.controller, server_timeout) dialog = ExceptionDialog(self.controller, server_timeout)
dialog.run() dialog.run()
def connect(self) -> None:
print(self.record.ip)
print(self.record.gameport)
print(self.appid)
# TODO: convert mod ids to symlink hashes
# steam api, concat mods
# TODO: custom threading with glib idle callback
def update_mods(self) -> None: def update_mods(self) -> None:
print(self.missing_mods)
# TODO: when downloading mods, create symlinks if missing
# TODO: pack a final PreReq struct with pre-processed values
# self.needs_update # self.needs_update
# then connect
pass pass
def connect(self) -> None: def update_and_connect(self) -> None:
# steam api, concat mods if len(self.missing_mods) > 0:
pass self.update_mods()
else:
self.connect()

View File

@ -1,4 +1,5 @@
update_mods = "Update mods and connect" update_mods = "Update mods and connect"
connect = "Connect"
back = "Back" back = "Back"
cancel = "Cancel" cancel = "Cancel"
warnings = "Warnings" warnings = "Warnings"

View File

@ -51,6 +51,10 @@ def format_mods(size: int, mods: int) -> str:
return f"Found {mods:n} {plural} taking up {l_size} MiB. {suffix}" return f"Found {mods:n} {plural} taking up {l_size} MiB. {suffix}"
def format_mib(bits: int) -> float:
return round(bits / (1024**2), 3)
def format_server_mods(mods: int) -> str: def format_server_mods(mods: int) -> str:
plural = pluralize("mods", mods) plural = pluralize("mods", mods)
return f"Found {mods:n} {plural}. {workshop}" return f"Found {mods:n} {plural}. {workshop}"

View File

@ -1,16 +1,12 @@
from dataclasses import dataclass from dataclasses import dataclass
from typing import Self, TYPE_CHECKING from typing import Self, Sequence, TYPE_CHECKING
from dzgui.const.constants import ( from dzgui.const.constants import (
APPID_DAYZ,
APPID_DAYZ_EXP,
APPNAME_DAYZ,
APPNAME_DAYZ_EXP,
ERROR, ERROR,
WARNING, WARNING,
) )
from dayzquery import DayzMod
from dzgui.util.css import add_class from dzgui.util.css import add_class
from dzgui.util.localize import number
from dzgui.strings import preconnect from dzgui.strings import preconnect
from dzgui.views.components.frame import HeadingFrame from dzgui.views.components.frame import HeadingFrame
from dzgui.views.trees.tree_server_mods import ServerModTreeView from dzgui.views.trees.tree_server_mods import ServerModTreeView
@ -22,7 +18,7 @@ from gi.repository import Gdk, Gtk # type: ignore # noqa E402
if TYPE_CHECKING: if TYPE_CHECKING:
from dzgui.api.servers import A2SInfo from dzgui.managers.connection import Prerequisites
from dzgui.controllers.mc import Controller from dzgui.controllers.mc import Controller
@ -41,6 +37,17 @@ class Errors:
no_dayz: bool no_dayz: bool
class Placeholder(Gtk.Label):
def __init__(self, text: str) -> None:
super().__init__(
label=text,
halign=Gtk.Align.START,
valign=Gtk.Align.START,
margin_start=10,
margin_bottom=5,
)
class MaskedTree(Gtk.TreeView): class MaskedTree(Gtk.TreeView):
def __init__(self, icon: str) -> None: def __init__(self, icon: str) -> None:
super().__init__(headers_visible=False, can_focus=False) super().__init__(headers_visible=False, can_focus=False)
@ -64,7 +71,12 @@ class MaskedTree(Gtk.TreeView):
self.append_column(text_column) self.append_column(text_column)
add_class(self, "masked-tree") add_class(self, "masked-tree")
def append(self, items: list[str]) -> None: def append(self, item: Sequence[str]) -> None:
if len(item) > 1:
raise ValueError("This method only accepts one item")
self.store.append([self.icon, item])
def extend(self, items: list[str]) -> None:
self.store.clear() self.store.clear()
for item in items: for item in items:
self.store.append([self.icon, item]) self.store.append([self.icon, item])
@ -87,11 +99,11 @@ class PreConnectionAssistant(Gtk.ScrolledWindow):
self.controller.register_widget("preconnect", self) self.controller.register_widget("preconnect", self)
# TODO: dynamic button text if no mods needed
self.back = Gtk.Button(label=preconnect.back, halign=Gtk.Align.START) self.back = Gtk.Button(label=preconnect.back, halign=Gtk.Align.START)
self.cancel = Gtk.Button( self.cancel = Gtk.Button(
label=preconnect.cancel, halign=Gtk.Align.END, sensitive=False, hexpand=True label=preconnect.cancel, halign=Gtk.Align.END, sensitive=False, hexpand=True
) )
# TODO: dynamic button text if no mods needed
self.ok = Gtk.Button(label=preconnect.update_mods, halign=Gtk.Align.END) self.ok = Gtk.Button(label=preconnect.update_mods, halign=Gtk.Align.END)
self.button_box = Gtk.Box( self.button_box = Gtk.Box(
@ -111,7 +123,6 @@ class PreConnectionAssistant(Gtk.ScrolledWindow):
self.tree = ServerModTreeView(self.controller) self.tree = ServerModTreeView(self.controller)
self.mod_count = Gtk.Label(label="", halign=Gtk.Align.START, margin_start=5) self.mod_count = Gtk.Label(label="", halign=Gtk.Align.START, margin_start=5)
# TODO: live count of remaining downloads # TODO: live count of remaining downloads
# "Steam is downloading: {mod_name}" # "Steam is downloading: {mod_name}"
# mention whether manual or auto mod is active # mention whether manual or auto mod is active
@ -124,16 +135,30 @@ class PreConnectionAssistant(Gtk.ScrolledWindow):
self.tree_box.add(self.scrolled) self.tree_box.add(self.scrolled)
self.tree_box.add(self.mod_count) self.tree_box.add(self.mod_count)
# TODO: strings
self.mods_placeholder = Placeholder("This server has no mods.")
self.tree_box.add(self.mods_placeholder)
self.tree_frame = HeadingFrame(self.tree_box, preconnect.mods) self.tree_frame = HeadingFrame(self.tree_box, preconnect.mods)
# TODO: abstract into components # TODO: abstract into components
self.warning_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
self.warning_tree = MaskedTree(WARNING) self.warning_tree = MaskedTree(WARNING)
self.warning_frame = HeadingFrame(self.warning_tree, preconnect.warnings) # TODO: strings
self.warning_placeholder = Placeholder("No warnings.")
self.warning_box.add(self.warning_tree)
self.warning_box.add(self.warning_placeholder)
self.warning_frame = HeadingFrame(self.warning_box, preconnect.warnings)
self.error_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
self.error_tree = MaskedTree(ERROR) self.error_tree = MaskedTree(ERROR)
self.error_frame = HeadingFrame(self.error_tree, preconnect.errors) # TODO: strings
self.error_placeholder = Placeholder("No errors.")
self.error_box.add(self.error_tree)
self.error_box.add(self.error_placeholder)
self.error_frame = HeadingFrame(self.error_box, preconnect.errors)
self.box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self.box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
self.box.add(self.title) self.box.add(self.title)
self.box.add(self.tree_frame) self.box.add(self.tree_frame)
self.box.add(self.warning_frame) self.box.add(self.warning_frame)
@ -146,8 +171,15 @@ class PreConnectionAssistant(Gtk.ScrolledWindow):
self.connect("map", self._on_map) self.connect("map", self._on_map)
def _on_map(self, widget: Self) -> None: def _on_map(self, widget: Self) -> None:
self.tree_frame.set_visible(True) widgets = (
self.mod_count.set_visible(True) self.tree_frame,
self.mod_count,
self.error_placeholder,
self.warning_placeholder,
)
for widget in widgets:
widget.set_visible(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:
if event.keyval == Gdk.KEY_Escape: if event.keyval == Gdk.KEY_Escape:
@ -155,59 +187,84 @@ class PreConnectionAssistant(Gtk.ScrolledWindow):
def _on_ok_clicked(self, button: Gtk.Button) -> None: def _on_ok_clicked(self, button: Gtk.Button) -> None:
# TODO: update mod store in place with spinner/toast # TODO: update mod store in place with spinner/toast
# no dialog
# TODO: cancel mod downloads # TODO: cancel mod downloads
# TODO: add to history file and list store
# TODO: concat mods
self.controller.update_and_connect()
pass pass
def _on_back_clicked(self, button: Gtk.Button) -> None: def _on_back_clicked(self, button: Gtk.Button) -> None:
page = self.controller.get_prior_page() page = self.controller.get_prior_page()
self.controller.open_page(page) self.controller.open_page(page)
def populate(self, res: "A2SInfo", mods: list["DayzMod"]) -> None: def _process_warnings(self, prereqs: "Prerequisites") -> None:
warnings: list[str] = []
errors: list[str] = []
if prereqs.binary_missing:
errors.append(
f"Remote server is running the build '{prereqs.build}', but it is not installed"
)
elif prereqs.local_version != prereqs.remote_version:
print("versions do not match")
errors.append(
f"Local client version '{prereqs.local_version}' does not match remote version '{prereqs.remote_version}'"
)
if prereqs.required_space > prereqs.available_space:
required_pretty = number(prereqs.required_space)
available_pretty = number(prereqs.available_space)
errors.append(
f"Need to update {required_pretty} MiB of mods, but installation path only has {available_pretty} MiB"
)
if prereqs.passworded:
warnings.append(
"Protected: you will be prompted for a password when connecting to this server"
)
if prereqs.dayz_running is True:
warnings.append(
"It looks like DayZ is already running in the background. Exit DayZ before connecting"
)
if prereqs.steam_running is False:
warnings.append(
"It looks like Steam is not running. Launch Steam before connecting"
)
self.add_warnings(warnings)
self.add_errors(errors)
if len(warnings) > 0:
self.warning_placeholder.set_visible(False)
if len(errors) > 0:
self.error_placeholder.set_visible(False)
self.ok.set_sensitive(False)
def populate(self, prereqs: "Prerequisites") -> None:
mods = prereqs.mods
self.tree.populate(mods) self.tree.populate(mods)
total = len(mods) total_mods = len(mods)
self._set_warnings() name = prereqs.name
info = res.get_info()
name = info.server_name
self.title.set_text(name) self.title.set_text(name)
if total < 1:
self.tree_frame.set_visible(False) if total_mods < 1:
self.scrolled.set_visible(False)
self.mod_count.set_visible(False) self.mod_count.set_visible(False)
return self.mods_placeholder.set_visible(True)
self.ok.set_label(preconnect.connect)
else: else:
self.tree.set_visible(True) self.scrolled.set_visible(True)
self.mod_count.set_visible(True) self.mod_count.set_visible(True)
self.mods_placeholder.set_visible(False)
# TODO: print no. of mods that need updating # TODO: print no. of mods that need updating
prefix = preconnect.total_mods prefix = preconnect.total_mods
self.mod_count.set_text(f"{prefix}{str(total)}") self.mod_count.set_text(f"{prefix}{str(total_mods)}")
""" self._process_warnings(prereqs)
blocking warning types:
- build mismatch
- version mismatch
- not enough drive space
passing warning types:
- dayz is running
- steam is not running
- server has password
"""
# TODO: if errors > 1, disable buttons
def _set_warnings(self) -> None: def add_errors(self, errors: list[str]) -> None:
self.warning_tree.append(["Password protected", "Some other error", "Error 3"]) self.error_tree.extend(errors)
self.error_tree.append(["Password protected", "Some other error", "Error 3"])
pass
def download_mods(self) -> None: def add_warnings(self, warnings: list[str]) -> None:
pass self.warning_tree.extend(warnings)
def connect_server(self) -> None:
# TODO: add to history file and list store
# TODO: concat mods
"""
spawn dialog in thread
watch for subprocess
return to prior page when finished
"""
pass