From 00caa2894b8ad90e4053c92e5d5b12ddfd0493e6 Mon Sep 17 00:00:00 2001 From: oparada1988 Date: Fri, 26 Jun 2026 15:40:48 -0400 Subject: [PATCH 1/2] feat: add user-customizable idle icon when no media is playing --- MediaAction.py | 48 ++++++++++++++++++++++++++-- locales/de_DE.json | 5 ++- locales/en_US.json | 5 ++- main.py | 80 ++++++++++++++++++++++++++++++++++++---------- 4 files changed, 118 insertions(+), 20 deletions(-) diff --git a/MediaAction.py b/MediaAction.py index 999adce..feb53b4 100644 --- a/MediaAction.py +++ b/MediaAction.py @@ -6,7 +6,9 @@ import gi gi.require_version("Gtk", "4.0") gi.require_version("Adw", "1") -from gi.repository import Gtk, Adw +from gi.repository import Gtk, Adw, Gio + +from GtkHelper.FileDialogRow import FileDialogRow, FileDialogFilter from PIL import Image, ImageEnhance import os @@ -39,13 +41,24 @@ def get_config_rows(self) -> "list[Adw.PreferencesRow]": self.label_toggle = Adw.SwitchRow(title=self.plugin_base.lm.get("actions.media-action.show-name-switch.label"), subtitle=self.plugin_base.lm.get("actions.media-action.show-name-switch.subtitle")) self.thumbnail_toggle = Adw.SwitchRow(title=self.plugin_base.lm.get("actions.media-action.show-thumbnail-switch.label"), subtitle=self.plugin_base.lm.get("actions.media-action.show-thumbnail-switch.subtitle")) + self.idle_icon_row = FileDialogRow( + title=self.plugin_base.lm.get("actions.media-action.idle-icon.label"), + subtitle=self.plugin_base.lm.get("actions.media-action.idle-icon.subtitle"), + dialog_title=self.plugin_base.lm.get("actions.media-action.idle-icon.dialog-title"), + filters=[FileDialogFilter(name="Images", filters=["*.png", "*.jpg", "*.jpeg", "*.svg"])] + ) + self.clear_idle_icon_button = Gtk.Button(icon_name="edit-clear-symbolic", valign=Gtk.Align.CENTER) + self.idle_icon_row.add_suffix(self.clear_idle_icon_button) + self.load_config_defaults() self.player_selector.connect("notify::selected-item", self.on_change_player) self.label_toggle.connect("notify::active", self.on_toggle_label) self.thumbnail_toggle.connect("notify::active", self.on_toggle_thumbnail) + self.idle_icon_row._callback = self.on_change_idle_icon + self.clear_idle_icon_button.connect("clicked", self.on_clear_idle_icon) - return [self.player_selector, self.label_toggle, self.thumbnail_toggle] + return [self.player_selector, self.label_toggle, self.thumbnail_toggle, self.idle_icon_row] ## Custom methods def load_config_defaults(self): @@ -55,10 +68,13 @@ def load_config_defaults(self): show_label = settings.setdefault("show_label", True) show_thumbnail = settings.setdefault("show_thumbnail", True) + idle_icon = settings.setdefault("idle_icon", "") # Update ui self.label_toggle.set_active(show_label) self.thumbnail_toggle.set_active(show_thumbnail) + if idle_icon and os.path.isfile(idle_icon): + self.idle_icon_row.load_from_path(idle_icon) self.update_player_selector() def update_player_selector(self): @@ -143,6 +159,34 @@ def on_toggle_thumbnail(self, switch, *args): # Update image self.on_tick() + def on_change_idle_icon(self, gio_file: Gio.File): + settings = self.get_settings() + if settings is not None: + settings["idle_icon"] = gio_file.get_path() if gio_file else "" + self.set_settings(settings) + self.on_tick() + + def on_clear_idle_icon(self, button): + settings = self.get_settings() + if settings is not None: + settings["idle_icon"] = "" + self.set_settings(settings) + self.idle_icon_row.selected_file = None + self.idle_icon_row.file_label.set_label("") + self.on_tick() + + def get_idle_icon(self) -> Image.Image | None: + settings = self.get_settings() + if settings is None: + return None + idle_icon_path = settings.get("idle_icon", "") + if idle_icon_path and os.path.isfile(idle_icon_path): + try: + return Image.open(idle_icon_path) + except Exception as e: + pass + return None + def generate_image(self, icon:Image.Image = None, background:Image.Image=None, valign: float = 0, halign: float = 0, size: float = 1): if background is None: background = Image.new("RGBA", (self.deck_controller.deck.key_image_format()["size"]), (0, 0, 0, 0)) diff --git a/locales/de_DE.json b/locales/de_DE.json index 86cd4c2..b3b2e38 100644 --- a/locales/de_DE.json +++ b/locales/de_DE.json @@ -22,5 +22,8 @@ "settings.composite-timeout.label": "Composite-Timeout", "settings.composite-timeout.subtitle": "Verzögerung vor dem Zusammensetzen von Miniaturbildern (ms)", "settings.log-level.label": "Log-Ebene", - "settings.log-level.subtitle": "Kontrollen Sie die Ausführlichkeit der Plugin-Protokollierung" + "settings.log-level.subtitle": "Kontrollen Sie die Ausführlichkeit der Plugin-Protokollierung", + "actions.media-action.idle-icon.label": "Inaktives Icon", + "actions.media-action.idle-icon.subtitle": "Icon, das angezeigt wird, wenn keine Medien abgespielt werden", + "actions.media-action.idle-icon.dialog-title": "Inaktives Icon auswählen" } \ No newline at end of file diff --git a/locales/en_US.json b/locales/en_US.json index 4acc734..6d0b4f8 100644 --- a/locales/en_US.json +++ b/locales/en_US.json @@ -23,5 +23,8 @@ "settings.composite-timeout.label": "Composite Timeout", "settings.composite-timeout.subtitle": "Delay before compositing thumbnails (ms)", "settings.log-level.label": "Log Level", - "settings.log-level.subtitle": "Control the verbosity of plugin logging" + "settings.log-level.subtitle": "Control the verbosity of plugin logging", + "actions.media-action.idle-icon.label": "Idle Icon", + "actions.media-action.idle-icon.subtitle": "Icon to show when no media is playing", + "actions.media-action.idle-icon.dialog-title": "Select Idle Icon" } \ No newline at end of file diff --git a/main.py b/main.py index b72dd43..cd1317d 100644 --- a/main.py +++ b/main.py @@ -70,6 +70,10 @@ def update_image(self): if status == None: if self.current_status == None: self.current_status = "Playing" + idle_image = self.get_idle_icon() + if idle_image is not None: + self.set_media(image=idle_image, size=size, valign=valign) + return image = Image.open(icon_path) enhancer = ImageEnhance.Brightness(image) image = enhancer.enhance(0.6) @@ -145,6 +149,10 @@ def update_image(self): if status == None: if self.current_status == None: self.current_status = "Playing" + idle_image = self.get_idle_icon() + if idle_image is not None: + self.set_media(image=idle_image, size=size, valign=valign) + return image = Image.open(icon_path) enhancer = ImageEnhance.Brightness(image) image = enhancer.enhance(0.6) @@ -229,6 +237,10 @@ def update_image(self): if status == None: if self.current_status == None: self.current_status = "Playing" + idle_image = self.get_idle_icon() + if idle_image is not None: + self.set_media(image=idle_image, size=size, valign=valign) + return file_path = file[self.current_status] image = Image.open(file_path) enhancer = ImageEnhance.Brightness(image) @@ -290,6 +302,10 @@ def update_image(self): image = Image.open(os.path.join(self.plugin_base.PATH, "assets", "next.png")) if status == None: + idle_image = self.get_idle_icon() + if idle_image is not None: + self.set_media(image=idle_image, size=size, valign=valign) + return enhancer = ImageEnhance.Brightness(image) image = enhancer.enhance(0.6) @@ -338,6 +354,10 @@ def update_image(self): image = Image.open(os.path.join(self.plugin_base.PATH, "assets", "previous.png")) if status == None: + idle_image = self.get_idle_icon() + if idle_image is not None: + self.set_media(image=idle_image, size=size, valign=valign) + return enhancer = ImageEnhance.Brightness(image) image = enhancer.enhance(0.6) @@ -366,27 +386,47 @@ def on_tick(self): self.update_image() def update_image(self): + status = self.plugin_base.mc.status(self.get_player_name()) + if isinstance(status, list): + status = status[0] + + if status == None: + self.set_top_label("", font_size=12) + self.set_center_label("", font_size=12) + self.set_bottom_label("", font_size=12) + + idle_image = self.get_idle_icon() + if idle_image is not None: + self.set_media(image=idle_image) + else: + self.set_media(image=Image.new("RGBA", (256, 256), (255, 255, 255, 0))) + return + title = self.plugin_base.mc.title(self.get_player_name()) artist = self.plugin_base.mc.artist(self.get_player_name()) if title is not None: title = self.shorten_label(title[0], 10) - if title is not None: + if artist is not None: artist = self.shorten_label(artist[0], 10) if self.get_settings() is None: return - self.set_top_label(str(title), font_size=12) - self.set_center_label(self.get_settings().get("seperator_text", "--"), font_size=12) - self.set_bottom_label(str(artist), font_size=12) + self.set_top_label(str(title) if title is not None else "", font_size=12) + self.set_center_label(self.get_settings().get("seperator_text", "--") if (title is not None or artist is not None) else "", font_size=12) + self.set_bottom_label(str(artist) if artist is not None else "", font_size=12) ## Thumbnail thumbnail = None if self.get_settings().setdefault("show_thumbnail", True): thumbnail = self.plugin_base.mc.thumbnail(self.get_player_name()) if thumbnail == None: - thumbnail = Image.new("RGBA", (256, 256), (255, 255, 255, 0)) + idle_image = self.get_idle_icon() + if idle_image is not None: + thumbnail = idle_image + else: + thumbnail = Image.new("RGBA", (256, 256), (255, 255, 255, 0)) elif isinstance(thumbnail, list): if thumbnail[0] == None: return @@ -832,6 +872,10 @@ def get_config_rows(self) -> "list[Adw.PreferencesRow]": self.size_mode_selector.connect("notify::selected", self.on_change_size_mode) rows.append(self.size_mode_selector) # type: ignore[arg-type] + + if hasattr(self, "idle_icon_row") and self.idle_icon_row is not None: + rows.append(self.idle_icon_row) + return rows def load_size_mode_default(self): @@ -905,17 +949,21 @@ def update_image(self): if thumbnail_path is None: self.last_thumbnail_path = None - self.restore_original_background() - return - - # Load thumbnail image - try: - thumbnail = Image.open(thumbnail_path) - except (OSError, ValueError) as e: - log.error(f"Failed to load thumbnail image from {thumbnail_path}: {e}") - self.last_thumbnail_path = None - self.restore_original_background() - return + idle_image = self.get_idle_icon() + if idle_image is not None: + thumbnail = idle_image + else: + self.restore_original_background() + return + else: + # Load thumbnail image + try: + thumbnail = Image.open(thumbnail_path) + except (OSError, ValueError) as e: + log.error(f"Failed to load thumbnail image from {thumbnail_path}: {e}") + self.last_thumbnail_path = None + self.restore_original_background() + return # Track thumbnail path, background path, and position self.last_thumbnail_path = thumbnail_path From 8d6b57307ebf65f07b0fb9e8960eeba10e5d6802 Mon Sep 17 00:00:00 2001 From: oparada1988 Date: Fri, 26 Jun 2026 15:44:34 -0400 Subject: [PATCH 2/2] feat: switch idle icon selector to streamcontroller asset picker and show only filename --- MediaAction.py | 54 ++++++++++++++++++++++++++++++---------------- locales/de_DE.json | 3 ++- locales/en_US.json | 3 ++- 3 files changed, 40 insertions(+), 20 deletions(-) diff --git a/MediaAction.py b/MediaAction.py index feb53b4..03c8290 100644 --- a/MediaAction.py +++ b/MediaAction.py @@ -6,9 +6,8 @@ import gi gi.require_version("Gtk", "4.0") gi.require_version("Adw", "1") -from gi.repository import Gtk, Adw, Gio - -from GtkHelper.FileDialogRow import FileDialogRow, FileDialogFilter +from gi.repository import Gtk, Adw, Gio, GLib +import globals as gl from PIL import Image, ImageEnhance import os @@ -41,13 +40,15 @@ def get_config_rows(self) -> "list[Adw.PreferencesRow]": self.label_toggle = Adw.SwitchRow(title=self.plugin_base.lm.get("actions.media-action.show-name-switch.label"), subtitle=self.plugin_base.lm.get("actions.media-action.show-name-switch.subtitle")) self.thumbnail_toggle = Adw.SwitchRow(title=self.plugin_base.lm.get("actions.media-action.show-thumbnail-switch.label"), subtitle=self.plugin_base.lm.get("actions.media-action.show-thumbnail-switch.subtitle")) - self.idle_icon_row = FileDialogRow( + self.idle_icon_row = Adw.ActionRow( title=self.plugin_base.lm.get("actions.media-action.idle-icon.label"), - subtitle=self.plugin_base.lm.get("actions.media-action.idle-icon.subtitle"), - dialog_title=self.plugin_base.lm.get("actions.media-action.idle-icon.dialog-title"), - filters=[FileDialogFilter(name="Images", filters=["*.png", "*.jpg", "*.jpeg", "*.svg"])] ) - self.clear_idle_icon_button = Gtk.Button(icon_name="edit-clear-symbolic", valign=Gtk.Align.CENTER) + self.choose_idle_icon_button = Gtk.Button.new_from_icon_name("document-open-symbolic") + self.choose_idle_icon_button.set_valign(Gtk.Align.CENTER) + self.idle_icon_row.add_suffix(self.choose_idle_icon_button) + + self.clear_idle_icon_button = Gtk.Button.new_from_icon_name("edit-clear-symbolic") + self.clear_idle_icon_button.set_valign(Gtk.Align.CENTER) self.idle_icon_row.add_suffix(self.clear_idle_icon_button) self.load_config_defaults() @@ -55,7 +56,7 @@ def get_config_rows(self) -> "list[Adw.PreferencesRow]": self.player_selector.connect("notify::selected-item", self.on_change_player) self.label_toggle.connect("notify::active", self.on_toggle_label) self.thumbnail_toggle.connect("notify::active", self.on_toggle_thumbnail) - self.idle_icon_row._callback = self.on_change_idle_icon + self.choose_idle_icon_button.connect("clicked", self.on_choose_idle_icon_clicked) self.clear_idle_icon_button.connect("clicked", self.on_clear_idle_icon) return [self.player_selector, self.label_toggle, self.thumbnail_toggle, self.idle_icon_row] @@ -73,8 +74,7 @@ def load_config_defaults(self): # Update ui self.label_toggle.set_active(show_label) self.thumbnail_toggle.set_active(show_thumbnail) - if idle_icon and os.path.isfile(idle_icon): - self.idle_icon_row.load_from_path(idle_icon) + self.update_idle_icon_ui(idle_icon) self.update_player_selector() def update_player_selector(self): @@ -159,20 +159,38 @@ def on_toggle_thumbnail(self, switch, *args): # Update image self.on_tick() - def on_change_idle_icon(self, gio_file: Gio.File): + def update_idle_icon_ui(self, path): + if path and os.path.isfile(path): + filename = os.path.basename(path) + self.idle_icon_row.set_subtitle(filename) + self.clear_idle_icon_button.set_sensitive(True) + else: + default_text = self.plugin_base.lm.get("actions.media-action.idle-icon.default-subtitle") + self.idle_icon_row.set_subtitle("None" if default_text is None else default_text) + self.clear_idle_icon_button.set_sensitive(False) + + def on_choose_idle_icon_clicked(self, button): settings = self.get_settings() - if settings is not None: - settings["idle_icon"] = gio_file.get_path() if gio_file else "" - self.set_settings(settings) - self.on_tick() + current_val = settings.get("idle_icon", "") if settings else "" + + def on_select_callback(path): + if not path: + return + settings = self.get_settings() + if settings is not None: + settings["idle_icon"] = path + self.set_settings(settings) + self.update_idle_icon_ui(path) + self.on_tick() + + GLib.idle_add(gl.app.let_user_select_asset, current_val, on_select_callback) def on_clear_idle_icon(self, button): settings = self.get_settings() if settings is not None: settings["idle_icon"] = "" self.set_settings(settings) - self.idle_icon_row.selected_file = None - self.idle_icon_row.file_label.set_label("") + self.update_idle_icon_ui("") self.on_tick() def get_idle_icon(self) -> Image.Image | None: diff --git a/locales/de_DE.json b/locales/de_DE.json index b3b2e38..b7980eb 100644 --- a/locales/de_DE.json +++ b/locales/de_DE.json @@ -25,5 +25,6 @@ "settings.log-level.subtitle": "Kontrollen Sie die Ausführlichkeit der Plugin-Protokollierung", "actions.media-action.idle-icon.label": "Inaktives Icon", "actions.media-action.idle-icon.subtitle": "Icon, das angezeigt wird, wenn keine Medien abgespielt werden", - "actions.media-action.idle-icon.dialog-title": "Inaktives Icon auswählen" + "actions.media-action.idle-icon.dialog-title": "Inaktives Icon auswählen", + "actions.media-action.idle-icon.default-subtitle": "Keines" } \ No newline at end of file diff --git a/locales/en_US.json b/locales/en_US.json index 6d0b4f8..ca8e974 100644 --- a/locales/en_US.json +++ b/locales/en_US.json @@ -26,5 +26,6 @@ "settings.log-level.subtitle": "Control the verbosity of plugin logging", "actions.media-action.idle-icon.label": "Idle Icon", "actions.media-action.idle-icon.subtitle": "Icon to show when no media is playing", - "actions.media-action.idle-icon.dialog-title": "Select Idle Icon" + "actions.media-action.idle-icon.dialog-title": "Select Idle Icon", + "actions.media-action.idle-icon.default-subtitle": "None" } \ No newline at end of file