diff --git a/pkgs/clan-vm-manager/clan_vm_manager/assets/cLAN--black.png b/pkgs/clan-vm-manager/clan_vm_manager/assets/cLAN--black.png deleted file mode 100644 index 370d0f751..000000000 Binary files a/pkgs/clan-vm-manager/clan_vm_manager/assets/cLAN--black.png and /dev/null differ diff --git a/pkgs/clan-vm-manager/clan_vm_manager/assets/clan_black_notext.png b/pkgs/clan-vm-manager/clan_vm_manager/assets/clan_black_notext.png new file mode 100644 index 000000000..edfb5a907 Binary files /dev/null and b/pkgs/clan-vm-manager/clan_vm_manager/assets/clan_black_notext.png differ diff --git a/pkgs/clan-vm-manager/clan_vm_manager/components/gkvstore.py b/pkgs/clan-vm-manager/clan_vm_manager/components/gkvstore.py index d4795f63c..0ad3aad8e 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/components/gkvstore.py +++ b/pkgs/clan-vm-manager/clan_vm_manager/components/gkvstore.py @@ -1,6 +1,6 @@ import logging from collections.abc import Callable -from typing import Any, Generic, TypeVar +from typing import Any, Generic, TypeVar, ClassVar import gi @@ -24,6 +24,10 @@ class GKVStore(GObject.GObject, Gio.ListModel, Generic[K, V]): This class could be optimized by having the objects remember their position in the list. """ + __gsignals__: ClassVar = { + "is_ready": (GObject.SignalFlags.RUN_FIRST, None, []), + } + def __init__(self, gtype: type[V], key_gen: Callable[[V], K]) -> None: super().__init__() self.gtype = gtype diff --git a/pkgs/clan-vm-manager/clan_vm_manager/components/list_splash.py b/pkgs/clan-vm-manager/clan_vm_manager/components/list_splash.py index e69de29bb..36791df6b 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/components/list_splash.py +++ b/pkgs/clan-vm-manager/clan_vm_manager/components/list_splash.py @@ -0,0 +1,70 @@ +import logging +from typing import Callable, Optional, TypeVar + +import gi + +from clan_vm_manager import assets + +gi.require_version("Adw", "1") +from gi.repository import Adw, Gio, GObject, Gtk, GdkPixbuf + +log = logging.getLogger(__name__) + +ListItem = TypeVar("ListItem", bound=GObject.Object) +CustomStore = TypeVar("CustomStore", bound=Gio.ListModel) + + +class EmptySplash(Gtk.Box): + def __init__(self, on_join: Callable[[str], None]) -> None: + super().__init__(orientation=Gtk.Orientation.VERTICAL) + self.on_join = on_join + + vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) + clan_icon = self.load_image(str(assets.get_asset("clan_black_notext.png"))) + + if clan_icon: + image = Gtk.Image.new_from_pixbuf(clan_icon) + else: + image = Gtk.Image.new_from_icon_name("image-missing") + # same as the clamp + image.set_pixel_size(400) + image.set_opacity(0.5) + image.set_margin_top(20) + image.set_margin_bottom(10) + + vbox.append(image) + + empty_label = Gtk.Label(label="Welcome to Clan! Join your first clan.") + join_entry = Gtk.Entry() + join_entry.set_placeholder_text("clan://") + join_entry.set_hexpand(True) + + join_button = Gtk.Button(label="Join") + join_button.connect("clicked", self._on_join, join_entry) + + clamp = Adw.Clamp() + clamp.set_maximum_size(400) + vbox.append(empty_label) + hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) + hbox.append(join_entry) + hbox.append(join_button) + vbox.append(hbox) + clamp.set_child(vbox) + + self.append(clamp) + + def load_image(self, file_path: str) -> Optional[GdkPixbuf.Pixbuf]: + try: + pixbuf = GdkPixbuf.Pixbuf.new_from_file(file_path) + return pixbuf + except Exception as e: + log.error(f"Failed to load image: {e}") + return None + + def _on_join(self, button: Gtk.Button, entry: Gtk.Entry) -> None: + """ + Callback for the join button + Extracts the text from the entry and calls the on_join callback + """ + log.info(f"Splash screen: Joining {entry.get_text()}") + self.on_join(entry.get_text()) diff --git a/pkgs/clan-vm-manager/clan_vm_manager/views/list.py b/pkgs/clan-vm-manager/clan_vm_manager/views/list.py index 312ba9fcb..8eade4165 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/views/list.py +++ b/pkgs/clan-vm-manager/clan_vm_manager/views/list.py @@ -7,8 +7,9 @@ from typing import Any, TypeVar import gi from clan_cli.clan_uri import ClanURI -from clan_vm_manager import assets +from clan_vm_manager.components.gkvstore import GKVStore from clan_vm_manager.components.interfaces import ClanConfig +from clan_vm_manager.components.list_splash import EmptySplash from clan_vm_manager.components.vmobj import VMObject from clan_vm_manager.singletons.toast import ( LogToast, @@ -73,43 +74,29 @@ class ClanList(Gtk.Box): self.join_boxed_list.add_css_class("join-list") self.append(self.join_boxed_list) + clan_store = ClanStore.use().clan_store + clan_store.connect("is_ready", self.display_splash) + self.group_list = create_boxed_list( - model=ClanStore.use().clan_store, render_row=self.render_group_row + model=clan_store, render_row=self.render_group_row ) self.group_list.add_css_class("group-list") self.append(self.group_list) - # LIST SPLASH - clan_icon = assets.get_asset("clan.svg") + self.splash = EmptySplash(on_join=lambda x: self.on_join_request(x, x)) - if not icon_path: - return ico_buffer - - pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(icon_path, icon_size, icon_size) - - empty_label = Gtk.Label(label="Welcome to Clan! Join your first clan.") - join_entry = Gtk.Entry() - join_entry.set_placeholder_text("clan://") - join_entry.set_hexpand(True) - - join_button = Gtk.Button(label="Join") - join_button.connect("clicked", lambda x: (), join_entry) - - clamp = Adw.Clamp() - clamp.set_maximum_size(400) - vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) - vbox.append(empty_label) - hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) - hbox.append(join_entry) - hbox.append(join_button) - vbox.append(hbox) - clamp.set_child(vbox) - - self.append(clamp) + def display_splash(self, source: GKVStore) -> None: + print("Displaying splash") + if ( + ClanStore.use().clan_store.get_n_items() == 0 + and JoinList.use().list_store.get_n_items() == 0 + ): + self.append(self.splash) def render_group_row( self, boxed_list: Gtk.ListBox, vm_store: VMStore ) -> Gtk.Widget: + self.remove(self.splash) vm = vm_store.first() log.debug("Rendering group row for %s", vm.data.flake.flake_url) diff --git a/pkgs/clan-vm-manager/clan_vm_manager/windows/main_window.py b/pkgs/clan-vm-manager/clan_vm_manager/windows/main_window.py index 622420818..fcc801d43 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/windows/main_window.py +++ b/pkgs/clan-vm-manager/clan_vm_manager/windows/main_window.py @@ -1,5 +1,6 @@ import logging import threading +from typing import Callable import gi from clan_cli.history.list import list_history @@ -41,7 +42,9 @@ class MainWindow(Adw.ApplicationWindow): self.tray_icon: TrayIcon = TrayIcon(app) # Initialize all ClanStore - threading.Thread(target=self._populate_vms).start() + threading.Thread( + target=self._populate_vms, args=[self._set_clan_store_ready] + ).start() # Initialize all views stack_view = ViewStack.use().view @@ -65,12 +68,17 @@ class MainWindow(Adw.ApplicationWindow): self.connect("destroy", self.on_destroy) - def _populate_vms(self) -> None: + def _set_clan_store_ready(self) -> None: + ClanStore.use().clan_store.emit("is_ready") + + def _populate_vms(self, done: Callable[[], None]) -> None: # Execute `clan flakes add ` to democlan for this to work # TODO: Make list_history a generator function for entry in list_history(): GLib.idle_add(ClanStore.use().create_vm_task, entry) + GLib.idle_add(done) + def kill_vms(self) -> None: log.debug("Killing all VMs") ClanStore.use().kill_all()