mirror of
https://github.com/aclist/dztui.git
synced 2026-10-31 15:12:48 +01:00
feat: catch cancel and shutdown events
This commit is contained in:
1 parent
319966f8e1
commit
2e5860650f
9 files changed
+94
-40
No files matched your search
+11
-1
@@ -47,6 +47,7 @@ if TYPE_CHECKING:
|
|||||||
from dzgui.views.components.filter_panel import FilterPanel
|
from dzgui.views.components.filter_panel import FilterPanel
|
||||||
from dzgui.views.components.right_panel import RightPanel
|
from dzgui.views.components.right_panel import RightPanel
|
||||||
from dzgui.views.components.statusbar import Statusbar
|
from dzgui.views.components.statusbar import Statusbar
|
||||||
|
from dzgui.views.pages.options import Options
|
||||||
from dzgui.views.pages.preconnect import PreConnectionAssistant
|
from dzgui.views.pages.preconnect import PreConnectionAssistant
|
||||||
from dzgui.views.pages.servers import ServerNotebook
|
from dzgui.views.pages.servers import ServerNotebook
|
||||||
from dzgui.views.trees.tree_log import LogTreeView
|
from dzgui.views.trees.tree_log import LogTreeView
|
||||||
@@ -90,6 +91,7 @@ class Controller(GObject.GObject):
|
|||||||
self.pending_jobs = 1
|
self.pending_jobs = 1
|
||||||
|
|
||||||
self.exit_event = threading.Event()
|
self.exit_event = threading.Event()
|
||||||
|
self.cancel_event = threading.Event()
|
||||||
self.connection_man: ConnectionManager
|
self.connection_man: ConnectionManager
|
||||||
|
|
||||||
def get_emitter(self) -> Emitter:
|
def get_emitter(self) -> Emitter:
|
||||||
@@ -463,6 +465,15 @@ 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 set_cancel_event(self) -> None:
|
||||||
|
self.cancel_event.set()
|
||||||
|
|
||||||
|
def get_cancel_event(self) -> threading.Event:
|
||||||
|
return self.cancel_event
|
||||||
|
|
||||||
|
def clear_cancel_event(self) -> None:
|
||||||
|
self.cancel_event.clear()
|
||||||
|
|
||||||
def open_connection_assistant(self, prereqs: "Prerequisites") -> None:
|
def open_connection_assistant(self, prereqs: "Prerequisites") -> None:
|
||||||
self.open_page(NotebookPage.CONNECTION)
|
self.open_page(NotebookPage.CONNECTION)
|
||||||
self.mediator.preconnect.populate(prereqs)
|
self.mediator.preconnect.populate(prereqs)
|
||||||
@@ -479,4 +490,3 @@ class Controller(GObject.GObject):
|
|||||||
|
|
||||||
def get_steam_client_name(self) -> str:
|
def get_steam_client_name(self) -> str:
|
||||||
return self.mediator.options.get_client_name()
|
return self.mediator.options.get_client_name()
|
||||||
|
|
||||||
+5
-5
@@ -1,27 +1,26 @@
|
|||||||
import psutil
|
import psutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import shutil
|
import shutil
|
||||||
import sys
|
|
||||||
|
|
||||||
from dzgui.const.constants import (
|
from dzgui.const.constants import (
|
||||||
DAYZ_BINARY,
|
DAYZ_BINARY,
|
||||||
STEAM_CMD,
|
STEAM_CMD,
|
||||||
FLATPAK_APPID,
|
FLATPAK_APPID,
|
||||||
|
FLATPAK_CMD,
|
||||||
FLATPAK_RUN_CMD,
|
FLATPAK_RUN_CMD,
|
||||||
FLATPAK_SANDBOX,
|
FLATPAK_SANDBOX,
|
||||||
)
|
)
|
||||||
from dzgui.views.dialogs.early_alert import EarlyAlertDialog
|
|
||||||
from dzgui.util.strings import init
|
|
||||||
|
|
||||||
|
|
||||||
# TODO: move to util.proc
|
# TODO: move to util.proc
|
||||||
def is_dayz_running() -> bool:
|
def is_dayz_running() -> bool:
|
||||||
|
"""Subprocesses spawned from Steam will not show up in regular process tree"""
|
||||||
procs = []
|
procs = []
|
||||||
substring = DAYZ_BINARY
|
substring = DAYZ_BINARY
|
||||||
for proc in psutil.process_iter():
|
for proc in psutil.process_iter():
|
||||||
try:
|
try:
|
||||||
procs.append(proc.cmdline())
|
procs.append(proc.cmdline())
|
||||||
except Exception as e:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
return any(substring in item for sublist in procs for item in sublist)
|
return any(substring in item for sublist in procs for item in sublist)
|
||||||
|
|
||||||
@@ -37,10 +36,11 @@ def is_steam_running(cmd: str) -> bool:
|
|||||||
raise TypeError("Not a valid Steam client selection")
|
raise TypeError("Not a valid Steam client selection")
|
||||||
|
|
||||||
|
|
||||||
|
# CHORE: test alternate clients
|
||||||
def is_flatpak_steam_running() -> bool:
|
def is_flatpak_steam_running() -> bool:
|
||||||
if has_cmd(FLATPAK_CMD) is False:
|
if has_cmd(FLATPAK_CMD) is False:
|
||||||
return False
|
return False
|
||||||
proc = subprocess.check_output([FLATPAK_CMD, "ps"], capture_output=True, text=True)
|
proc = subprocess.check_output([FLATPAK_CMD, "ps"], text=True)
|
||||||
lines = proc.stdout.splitlines()
|
lines = proc.stdout.splitlines()
|
||||||
if FLATPAK_APPID in lines:
|
if FLATPAK_APPID in lines:
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -75,7 +75,6 @@ class Prerequisites:
|
|||||||
dayz_running: bool
|
dayz_running: bool
|
||||||
steam_proc: SteamProcess
|
steam_proc: SteamProcess
|
||||||
mods: list[str]
|
mods: list[str]
|
||||||
missing_mods: int
|
|
||||||
foreground_cmd: str | None
|
foreground_cmd: str | None
|
||||||
game_mode: bool
|
game_mode: bool
|
||||||
|
|
||||||
@@ -184,7 +183,6 @@ class ConnectionManager:
|
|||||||
dayz_running=dayz_running,
|
dayz_running=dayz_running,
|
||||||
steam_proc=steam_proc,
|
steam_proc=steam_proc,
|
||||||
mods=remote_mods,
|
mods=remote_mods,
|
||||||
missing_mods=len(self.missing_mods),
|
|
||||||
foreground_cmd=self.foreground_cmd, # TODO: change to bool
|
foreground_cmd=self.foreground_cmd, # TODO: change to bool
|
||||||
game_mode=game_mode,
|
game_mode=game_mode,
|
||||||
)
|
)
|
||||||
@@ -245,7 +243,6 @@ class ConnectionManager:
|
|||||||
dialog = ExceptionDialog(self.controller, server_timeout)
|
dialog = ExceptionDialog(self.controller, server_timeout)
|
||||||
dialog.run()
|
dialog.run()
|
||||||
|
|
||||||
# TODO: threading
|
|
||||||
def _connect_steam(self) -> None:
|
def _connect_steam(self) -> None:
|
||||||
addr = f"{self.record.ip}:{self.record.gameport}"
|
addr = f"{self.record.ip}:{self.record.gameport}"
|
||||||
playername = self.controller.query_config(Preferences.NAME)
|
playername = self.controller.query_config(Preferences.NAME)
|
||||||
@@ -256,8 +253,12 @@ class ConnectionManager:
|
|||||||
self.thread_man.set_cleanup_func(func)
|
self.thread_man.set_cleanup_func(func)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
self.thread_man.show_cancel(False)
|
||||||
self.thread_man.update_dialog(waiting_for_launch)
|
self.thread_man.update_dialog(waiting_for_launch)
|
||||||
while True:
|
while True:
|
||||||
|
if self.controller.get_exit_event().is_set():
|
||||||
|
# TODO: some facility to also close spawned steam process
|
||||||
|
return
|
||||||
if is_dayz_running():
|
if is_dayz_running():
|
||||||
break
|
break
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
@@ -290,6 +291,12 @@ class ConnectionManager:
|
|||||||
while True:
|
while True:
|
||||||
# NOTE: mods will finish at the same time
|
# NOTE: mods will finish at the same time
|
||||||
# TODO: check for early cancel event
|
# TODO: check for early cancel event
|
||||||
|
# TODO: check global sigint event
|
||||||
|
if self.controller.get_exit_event().is_set():
|
||||||
|
return
|
||||||
|
if self.controller.get_cancel_event().is_set():
|
||||||
|
self.controller.clear_cancel_event()
|
||||||
|
return
|
||||||
cur_size = get_mod_dir_size(mod_path)
|
cur_size = get_mod_dir_size(mod_path)
|
||||||
if cur_size == size:
|
if cur_size == size:
|
||||||
break
|
break
|
||||||
@@ -298,11 +305,11 @@ class ConnectionManager:
|
|||||||
update_signatures(self.missing_mods, prefs.paths.version)
|
update_signatures(self.missing_mods, prefs.paths.version)
|
||||||
# TODO: get config path or just push steam path directly
|
# TODO: get config path or just push steam path directly
|
||||||
rebuild_symlinks(prefs.paths.config)
|
rebuild_symlinks(prefs.paths.config)
|
||||||
|
self._connect_steam()
|
||||||
|
|
||||||
@call_on_thread("Waiting for Steam to update mods")
|
@call_on_thread("Waiting for Steam to update mods", show_cancel=True)
|
||||||
def update_and_connect(self, raise_window: bool) -> None:
|
def update_and_connect(self, raise_window: bool) -> None:
|
||||||
if len(self.missing_mods) > 0:
|
if len(self.missing_mods) > 0:
|
||||||
self._update_mods(raise_window)
|
self._update_mods(raise_window)
|
||||||
self._connect_steam()
|
|
||||||
else:
|
else:
|
||||||
self._connect_steam()
|
self._connect_steam()
|
||||||
@@ -20,7 +20,9 @@ from gi.repository import Gtk, GLib # noqa E402
|
|||||||
logger = logging.getLogger(APP_NAME)
|
logger = logging.getLogger(APP_NAME)
|
||||||
|
|
||||||
|
|
||||||
def call_on_thread(dialog_str: str, show_dialog: bool = True) -> Callable:
|
def call_on_thread(
|
||||||
|
dialog_str: str, show_dialog: bool = True, show_cancel: bool = False
|
||||||
|
) -> Callable:
|
||||||
def decorator(func: Callable) -> Callable:
|
def decorator(func: Callable) -> Callable:
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
def wrapper(*args: Any, **kwargs: Any) -> None:
|
def wrapper(*args: Any, **kwargs: Any) -> None:
|
||||||
@@ -32,7 +34,7 @@ def call_on_thread(dialog_str: str, show_dialog: bool = True) -> Callable:
|
|||||||
raise TypeError(
|
raise TypeError(
|
||||||
"Attribute 'thread_man' must be of type 'ThreadingManager'"
|
"Attribute 'thread_man' must be of type 'ThreadingManager'"
|
||||||
)
|
)
|
||||||
self.thread_man.call_on_thread(dialog_str, stored, show_dialog)
|
self.thread_man.call_on_thread(dialog_str, stored, show_dialog, show_cancel)
|
||||||
|
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
@@ -57,15 +59,22 @@ class ThreadingManager:
|
|||||||
self.destroy_first = False
|
self.destroy_first = False
|
||||||
|
|
||||||
def call_on_thread(
|
def call_on_thread(
|
||||||
self, dialog_str: str, func: StoredFunc, show_dialog: bool = True
|
self,
|
||||||
|
dialog_str: str,
|
||||||
|
func: StoredFunc,
|
||||||
|
show_dialog: bool = True,
|
||||||
|
show_cancel: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
def callback() -> None:
|
def callback() -> None:
|
||||||
func.call()
|
func.call()
|
||||||
GLib.idle_add(self._destroy_on_idle)
|
GLib.idle_add(self._destroy_on_idle)
|
||||||
|
|
||||||
if show_dialog:
|
if show_dialog:
|
||||||
self.wait_dialog = WaitDialog(self.controller, dialog_str, jobs=self.jobs)
|
self.wait_dialog = WaitDialog(
|
||||||
|
self.controller, dialog_str, jobs=self.jobs, show_cancel=show_cancel
|
||||||
|
)
|
||||||
self.wait_dialog.show_all()
|
self.wait_dialog.show_all()
|
||||||
|
|
||||||
thread = threading.Thread(target=callback)
|
thread = threading.Thread(target=callback)
|
||||||
thread.start()
|
thread.start()
|
||||||
|
|
||||||
@@ -113,3 +122,6 @@ class ThreadingManager:
|
|||||||
|
|
||||||
def get_wait_dialog(self) -> WaitDialog:
|
def get_wait_dialog(self) -> WaitDialog:
|
||||||
return self.wait_dialog
|
return self.wait_dialog
|
||||||
|
|
||||||
|
def show_cancel(self, state: bool) -> None:
|
||||||
|
GLib.idle_add(self.wait_dialog.show_cancel, state)
|
||||||
@@ -64,7 +64,7 @@ class ServerModCols:
|
|||||||
@dataclass(slots=True, frozen=True)
|
@dataclass(slots=True, frozen=True)
|
||||||
class MenuCols:
|
class MenuCols:
|
||||||
name: str
|
name: str
|
||||||
hidden: object # GObject.TYPE_PYOBJECT
|
hidden: object
|
||||||
|
|
||||||
|
|
||||||
class FastInsertListStore(ListStore):
|
class FastInsertListStore(ListStore):
|
||||||
@@ -108,6 +108,7 @@ class FastInsertListStore(ListStore):
|
|||||||
self.append(row)
|
self.append(row)
|
||||||
|
|
||||||
def append(self, row: list[Any] | tuple[Any, ...] | None = ...) -> TreeIter:
|
def append(self, row: list[Any] | tuple[Any, ...] | None = ...) -> TreeIter:
|
||||||
|
# FIXME: argument cannot be none
|
||||||
"""
|
"""
|
||||||
Optimized for speed, but makes no assurances about row homogeneity
|
Optimized for speed, but makes no assurances about row homogeneity
|
||||||
and may segfault if types and length are not identical to ListStore.
|
and may segfault if types and length are not identical to ListStore.
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ class Statusbar(Gtk.Grid):
|
|||||||
|
|
||||||
self.set_by_context(enum, bar)
|
self.set_by_context(enum, bar)
|
||||||
|
|
||||||
|
# TODO: unused
|
||||||
def _on_notebook_page_returned(
|
def _on_notebook_page_returned(
|
||||||
self, statusbar: Self, prior_context: NotebookPage
|
self, statusbar: Self, prior_context: NotebookPage
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -109,8 +110,9 @@ class Statusbar(Gtk.Grid):
|
|||||||
context: Union["ServerTab", NotebookPage],
|
context: Union["ServerTab", NotebookPage],
|
||||||
) -> None:
|
) -> None:
|
||||||
self.spinner.stop()
|
self.spinner.stop()
|
||||||
|
# TODO: CalcDist is being called when table is not loaded
|
||||||
if dist is None:
|
if dist is None:
|
||||||
self.set_by_context(context, self.playercount)
|
self.set_by_context(context, "")
|
||||||
else:
|
else:
|
||||||
pretty = self.append_distance(dist)
|
pretty = self.append_distance(dist)
|
||||||
self.set_by_context(context, pretty)
|
self.set_by_context(context, pretty)
|
||||||
|
|||||||
@@ -87,7 +87,13 @@ class NotifyDialog(GenericDialog):
|
|||||||
|
|
||||||
|
|
||||||
class WaitDialog(GenericDialog):
|
class WaitDialog(GenericDialog):
|
||||||
def __init__(self, controller: "Controller", secondary: str, jobs: int = 1):
|
def __init__(
|
||||||
|
self,
|
||||||
|
controller: "Controller",
|
||||||
|
secondary: str,
|
||||||
|
jobs: int = 1,
|
||||||
|
show_cancel: bool = False,
|
||||||
|
):
|
||||||
super().__init__(
|
super().__init__(
|
||||||
controller=controller,
|
controller=controller,
|
||||||
text=strings.wait,
|
text=strings.wait,
|
||||||
@@ -96,19 +102,29 @@ class WaitDialog(GenericDialog):
|
|||||||
secondary=secondary,
|
secondary=secondary,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.controller = controller
|
||||||
self.jobs = jobs
|
self.jobs = jobs
|
||||||
self.cur_job = 1
|
self.cur_job = 1
|
||||||
|
|
||||||
|
self.cancel = Gtk.Button(label="Cancel", halign=Gtk.Align.CENTER)
|
||||||
|
self.cancel.connect("clicked", lambda _: self.controller.set_cancel_event())
|
||||||
|
|
||||||
self.connect("delete-event", lambda widget, event: True)
|
self.connect("delete-event", lambda widget, event: True)
|
||||||
content = self.get_content_area()
|
content = self.get_content_area()
|
||||||
spinner = Gtk.Spinner()
|
spinner = Gtk.Spinner()
|
||||||
self.prog = Gtk.ProgressBar()
|
self.prog = Gtk.ProgressBar()
|
||||||
|
|
||||||
|
content.pack_end(self.cancel, NO_EXPAND, NO_FILL, 0)
|
||||||
content.pack_end(spinner, NO_EXPAND, NO_FILL, 0)
|
content.pack_end(spinner, NO_EXPAND, NO_FILL, 0)
|
||||||
|
|
||||||
if self.jobs > 1:
|
if self.jobs > 1:
|
||||||
content.pack_end(self.prog, NO_EXPAND, NO_FILL, 0)
|
content.pack_end(self.prog, NO_EXPAND, NO_FILL, 0)
|
||||||
else:
|
else:
|
||||||
spinner.start()
|
spinner.start()
|
||||||
|
|
||||||
|
if show_cancel is False:
|
||||||
|
self.connect("realize", lambda _: self.cancel.set_visible(False))
|
||||||
|
|
||||||
def update_text(self, msg: str) -> None:
|
def update_text(self, msg: str) -> None:
|
||||||
self.format_secondary_text(msg)
|
self.format_secondary_text(msg)
|
||||||
|
|
||||||
@@ -119,6 +135,9 @@ class WaitDialog(GenericDialog):
|
|||||||
self.prog.set_fraction(fraction)
|
self.prog.set_fraction(fraction)
|
||||||
self.cur_job += 1
|
self.cur_job += 1
|
||||||
|
|
||||||
|
def show_cancel(self, state: bool) -> None:
|
||||||
|
self.cancel.set_visible(state)
|
||||||
|
|
||||||
|
|
||||||
class QuitDialog(GenericDialog):
|
class QuitDialog(GenericDialog):
|
||||||
def __init__(self, controller: "Controller", secondary: str):
|
def __init__(self, controller: "Controller", secondary: str):
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ class ServerModDialog(ServerDialog):
|
|||||||
columns = [
|
columns = [
|
||||||
server_mods.mod,
|
server_mods.mod,
|
||||||
server_mods.mod_id,
|
server_mods.mod_id,
|
||||||
server_mods.installed,
|
server_mods.up_to_date,
|
||||||
]
|
]
|
||||||
for i, column_title in enumerate(columns):
|
for i, column_title in enumerate(columns):
|
||||||
renderer = Gtk.CellRendererText(ellipsize=Pango.EllipsizeMode.END)
|
renderer = Gtk.CellRendererText(ellipsize=Pango.EllipsizeMode.END)
|
||||||
|
|||||||
@@ -203,8 +203,6 @@ class PreConnectionAssistant(Gtk.ScrolledWindow):
|
|||||||
child.set_visible(True)
|
child.set_visible(True)
|
||||||
self.raise_window.set_visible(False)
|
self.raise_window.set_visible(False)
|
||||||
|
|
||||||
# TODO: enable button if wmctrl or xdotool is available
|
|
||||||
# TODO: check this at boot time and pass in via connection manager
|
|
||||||
self.raise_window.set_sensitive(False)
|
self.raise_window.set_sensitive(False)
|
||||||
self.ok.set_sensitive(True)
|
self.ok.set_sensitive(True)
|
||||||
self.ok.set_label(preconnect.update_mods)
|
self.ok.set_label(preconnect.update_mods)
|
||||||
@@ -228,8 +226,6 @@ class PreConnectionAssistant(Gtk.ScrolledWindow):
|
|||||||
warnings: list[str] = []
|
warnings: list[str] = []
|
||||||
errors: list[str] = []
|
errors: list[str] = []
|
||||||
|
|
||||||
# TODO: check availability of currently selected steam cmd
|
|
||||||
|
|
||||||
"""Errors"""
|
"""Errors"""
|
||||||
if prereqs.binary_missing:
|
if prereqs.binary_missing:
|
||||||
errors.append(
|
errors.append(
|
||||||
@@ -273,6 +269,17 @@ class PreConnectionAssistant(Gtk.ScrolledWindow):
|
|||||||
self.error_placeholder.set_visible(False)
|
self.error_placeholder.set_visible(False)
|
||||||
self.ok.set_sensitive(False)
|
self.ok.set_sensitive(False)
|
||||||
|
|
||||||
|
def _hide_mod_area(self) -> None:
|
||||||
|
self.scrolled.set_visible(False)
|
||||||
|
self.progress_box.set_visible(False)
|
||||||
|
self.mods_placeholder.set_visible(True)
|
||||||
|
|
||||||
|
def _show_mod_area(self) -> None:
|
||||||
|
self.scrolled.set_visible(True)
|
||||||
|
self.progress_box.set_visible(True)
|
||||||
|
self.mods_placeholder.set_visible(False)
|
||||||
|
self.tree.grab_focus()
|
||||||
|
|
||||||
def populate(self, prereqs: "Prerequisites") -> None:
|
def populate(self, prereqs: "Prerequisites") -> None:
|
||||||
mods = prereqs.mods
|
mods = prereqs.mods
|
||||||
self.tree.populate(mods)
|
self.tree.populate(mods)
|
||||||
@@ -281,27 +288,23 @@ class PreConnectionAssistant(Gtk.ScrolledWindow):
|
|||||||
name = prereqs.name
|
name = prereqs.name
|
||||||
self.title.set_text(name)
|
self.title.set_text(name)
|
||||||
|
|
||||||
if prereqs.foreground_cmd is not None:
|
suffix = "All mods are up to date."
|
||||||
self.raise_window.set_sensitive(True)
|
|
||||||
|
|
||||||
if prereqs.missing_mods < 1:
|
|
||||||
self.ok.set_label(preconnect.connect)
|
|
||||||
if total_mods < 1:
|
if total_mods < 1:
|
||||||
self.scrolled.set_visible(False)
|
self._hide_mod_area()
|
||||||
self.progress_box.set_visible(False)
|
|
||||||
self.mods_placeholder.set_visible(True)
|
|
||||||
else:
|
else:
|
||||||
self.scrolled.set_visible(True)
|
self._show_mod_area()
|
||||||
self.progress_box.set_visible(True)
|
|
||||||
self.raise_window.set_visible(True)
|
|
||||||
self.mods_placeholder.set_visible(False)
|
|
||||||
self.tree.grab_focus()
|
|
||||||
|
|
||||||
# TODO: strings
|
if prereqs.required_space == 0:
|
||||||
suffix = "All mods are up to date."
|
self.ok.set_label(preconnect.connect)
|
||||||
if prereqs.required_space > 0:
|
self.raise_window.set_visible(False)
|
||||||
pretty = number(prereqs.required_space)
|
else:
|
||||||
suffix = f" Need to download {pretty} MiB of mod updates."
|
self.raise_window.set_visible(True)
|
||||||
|
if prereqs.foreground_cmd is not None:
|
||||||
|
self.raise_window.set_sensitive(True)
|
||||||
|
|
||||||
|
pretty = number(prereqs.required_space)
|
||||||
|
suffix = f" Need to download {pretty} MiB of mod updates."
|
||||||
prefix = preconnect.total_mods
|
prefix = preconnect.total_mods
|
||||||
self.mod_count.set_text(f"{prefix}{str(total_mods)}.{suffix}")
|
self.mod_count.set_text(f"{prefix}{str(total_mods)}.{suffix}")
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user