diff --git a/actions/Dial.py b/actions/Dial.py index 4207f31..f2f5c38 100644 --- a/actions/Dial.py +++ b/actions/Dial.py @@ -1,6 +1,5 @@ from src.backend.DeckManagement.InputIdentifier import Input from src.backend.PluginManager.ActionBase import ActionBase -from src.backend.PluginManager.ActionBase import ActionBase from src.backend.DeckManagement.DeckController import DeckController from src.backend.PageManagement.Page import Page from src.backend.PluginManager.PluginBase import PluginBase @@ -9,13 +8,12 @@ from loguru import logger as log from fuzzywuzzy import fuzz import math - import os + class Dial(ActionBase): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.plugin_base.volume_actions.append(self) def on_ready(self): @@ -25,7 +23,6 @@ def on_ready(self): def on_tick(self): index = self.get_index() - inputs = self.plugin_base.pulse.sink_input_list() if index < len(inputs): self.update_labels() @@ -38,27 +35,21 @@ def clear(self): self.set_center_label(None) def event_callback(self, event, data): - # Toggle mute inputs = self.plugin_base.pulse.sink_input_list() - index = self.get_index() if index >= len(inputs): return - + if event == Input.Dial.Events.SHORT_UP: mute = inputs[index].mute == 0 self.plugin_base.pulse.mute(obj=inputs[index], mute=mute) elif event == Input.Dial.Events.TURN_CW: - volume = inputs[index].volume.value_flat - volume += self.plugin_base.volume_increment - + volume = inputs[index].volume.value_flat + self.plugin_base.volume_increment self.plugin_base.pulse.volume_set_all_chans(obj=inputs[index], vol=min(1, volume)) elif event == Input.Dial.Events.TURN_CCW: - volume = inputs[index].volume.value_flat - volume -= self.plugin_base.volume_increment - + volume = inputs[index].volume.value_flat - self.plugin_base.volume_increment self.plugin_base.pulse.volume_set_all_chans(obj=inputs[index], vol=max(0, volume)) self.update_labels() @@ -66,22 +57,19 @@ def event_callback(self, event, data): def get_index(self) -> int: start_index = self.plugin_base.start_index own_index = int(self.input_ident.json_identifier) - index = start_index + own_index - return index + return start_index + own_index def update_labels(self): inputs = self.plugin_base.pulse.sink_input_list() index = self.get_index() - + if inputs[index].mute == 0: - # Display volume % if input is not muted - volumeLabel = str(math.ceil(inputs[index].volume.value_flat*100)) + "%" + volumeLabel = str(math.ceil(inputs[index].volume.value_flat * 100)) + "%" labelColor = [255, 255, 255] else: - # Display "muted" text if input is muted volumeLabel = "- " + self.plugin_base.lm.get("input.muted").upper() + " -" labelColor = [255, 0, 0] - + self.set_top_label(text=volumeLabel, color=labelColor, font_size=16) - self.set_center_label(text=inputs[index].name, font_size=18) + self.set_center_label(text=self.plugin_base.get_display_name(inputs[index]), font_size=18) diff --git a/actions/MuteKey.py b/actions/MuteKey.py index 42500df..eec70c9 100644 --- a/actions/MuteKey.py +++ b/actions/MuteKey.py @@ -1,5 +1,4 @@ from src.backend.PluginManager.ActionBase import ActionBase -from src.backend.PluginManager.ActionBase import ActionBase from src.backend.DeckManagement.DeckController import DeckController from src.backend.PageManagement.Page import Page from src.backend.PluginManager.PluginBase import PluginBase @@ -10,10 +9,11 @@ import os + class MuteKey(ActionBase): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - + self.plugin_base.volume_actions.append(self) def on_ready(self): @@ -26,7 +26,12 @@ def on_tick(self): inputs = self.plugin_base.pulse.sink_input_list() if index < len(inputs): - self.set_label(text=inputs[index].name, position="center", font_size=10) + # Better stream/app name (avoids generic "AudioStream", shows e.g. "ESO", "YouTube") + self.set_label( + text=self.plugin_base.get_display_name(inputs[index]), + position="center", + font_size=10 + ) else: self.clear() @@ -41,12 +46,12 @@ def on_key_down(self): index = self.get_index() if index >= len(inputs): return - + mute = inputs[index].mute == 0 self.plugin_base.pulse.mute(obj=inputs[index], mute=mute) def get_index(self) -> int: start_index = self.plugin_base.start_index own_index = self.input_ident.coords[0] - index = start_index + own_index - 1 # -1 because we want to ignore the first column containing the navigation keys - return index \ No newline at end of file + index = start_index + own_index - 1 # -1 because we want to ignore the first column containing the navigation keys + return index diff --git a/actions/OpenVolumeMixer.py b/actions/OpenVolumeMixer.py index 7e266e0..8a2d92a 100644 --- a/actions/OpenVolumeMixer.py +++ b/actions/OpenVolumeMixer.py @@ -1,5 +1,4 @@ from src.backend.PluginManager.ActionBase import ActionBase -from src.backend.PluginManager.ActionBase import ActionBase from src.backend.DeckManagement.DeckController import DeckController from src.backend.PageManagement.Page import Page from src.backend.PluginManager.PluginBase import PluginBase @@ -16,6 +15,7 @@ gi.require_version("Adw", "1") from gi.repository import Gtk, Adw + class OpenVolumeMixer(ActionBase): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -39,26 +39,85 @@ def on_key_down(self): page = gl.page_manager.get_page(path=page_path, deck_controller=self.deck_controller) if page is None: log.error("Could not create volume mixer page object. Consider reinstalling the plugin.") + return self.plugin_base.original_page_path = self.deck_controller.active_page.json_path self.deck_controller.load_page(page) def get_config_rows(self) -> list: + # Increments (%) self.increments_row = Adw.SpinRow.new_with_range(min=0, max=100, step=5) self.increments_row.set_title("Increments (%):") # Load default - settings = self.get_settings() + settings = self.get_settings() or {} self.increments_row.set_value(settings.get("increments", 10)) self.plugin_base.volume_increment = self.increments_row.get_value() / 100 # Connect signal self.increments_row.connect("changed", self.on_increments_change) - return [self.increments_row] - + # --- Label mode (plugin-wide) --- + self.label_mode_row = Adw.ComboRow() + self.label_mode_row.set_title(self.plugin_base.lm.get("settings.label_mode.title")) + self.label_mode_row.set_subtitle(self.plugin_base.lm.get("settings.label_mode.subtitle")) + + # Values used for storage + self._label_mode_values = ["auto", "app", "media", "app+media"] + + model = Gtk.StringList.new([ + self.plugin_base.lm.get("settings.label_mode.auto"), + self.plugin_base.lm.get("settings.label_mode.app"), + self.plugin_base.lm.get("settings.label_mode.media"), + self.plugin_base.lm.get("settings.label_mode.app_media"), + ]) + self.label_mode_row.set_model(model) + + # Load current plugin setting (global) + try: + ps = self.plugin_base.get_settings() or {} + current = (ps.get("label_mode", "auto") or "auto").lower() + except Exception: + current = "auto" + + selected = 0 + if current in self._label_mode_values: + selected = self._label_mode_values.index(current) + self.label_mode_row.set_selected(selected) + + self.label_mode_row.connect("notify::selected", self.on_label_mode_changed) + + return [self.increments_row, self.label_mode_row] + def on_increments_change(self, row): - settings = self.get_settings() + settings = self.get_settings() or {} settings["increments"] = row.get_value() self.plugin_base.volume_increment = row.get_value() / 100 - self.set_settings(settings) \ No newline at end of file + self.set_settings(settings) + + def on_label_mode_changed(self, row, _pspec=None): + try: + idx = row.get_selected() + except Exception: + idx = 0 + + mode = "auto" + try: + if hasattr(self, "_label_mode_values") and 0 <= idx < len(self._label_mode_values): + mode = self._label_mode_values[idx] + except Exception: + mode = "auto" + + # Store in plugin settings (global for all mixer keys) + try: + ps = self.plugin_base.get_settings() or {} + ps["label_mode"] = mode + self.plugin_base.set_settings(ps) + except Exception as ex: + log.error(f"Failed to store label_mode: {ex}") + + # Refresh all labels immediately + try: + self.plugin_base.refresh_volume_actions() + except Exception: + pass diff --git a/actions/VolumeDownKey.py b/actions/VolumeDownKey.py index bfd4b0a..b77ef36 100644 --- a/actions/VolumeDownKey.py +++ b/actions/VolumeDownKey.py @@ -99,4 +99,4 @@ def show_state(self, state: int) -> None: with Image.open(self.icon_path) as image: enhancer = ImageEnhance.Brightness(image) image = enhancer.enhance(brightness) - self.set_media(image=image.copy, size=0.8, valign=-1) \ No newline at end of file + self.set_media(image=image.copy(), size=0.8, valign=-1) diff --git a/locales/de_DE.json b/locales/de_DE.json index 18f00bc..607efba 100644 --- a/locales/de_DE.json +++ b/locales/de_DE.json @@ -21,5 +21,11 @@ "plugin.name": "Lautstärkemixer", "actions.hotkey.recorder.confirm-text": "Bestätigen", "actions.hotkey.recorder.clear-text": "Zurücksetzen", - "input.muted": "Stumm" + "input.muted": "Stumm", + "settings.label_mode.title": "Label-Quelle", + "settings.label_mode.subtitle": "Was soll als Stream-/Programmname auf dem Deck stehen", + "settings.label_mode.auto": "Auto (Media bevorzugen, sonst App)", + "settings.label_mode.app": "Anwendung", + "settings.label_mode.media": "Media / Stream-Titel", + "settings.label_mode.app_media": "App + Media" } diff --git a/locales/en_US.json b/locales/en_US.json index 51a05aa..021677b 100644 --- a/locales/en_US.json +++ b/locales/en_US.json @@ -21,5 +21,11 @@ "plugin.name": "Volume Mixer", "actions.hotkey.recorder.confirm-text": "Confirm", "actions.hotkey.recorder.clear-text": "Clear", - "input.muted": "Muted" + "input.muted": "Muted", + "settings.label_mode.title": "Label source", + "settings.label_mode.subtitle": "What should be shown as the stream name on the deck", + "settings.label_mode.auto": "Auto (prefer Media, fallback App)", + "settings.label_mode.app": "Application", + "settings.label_mode.media": "Media / Stream title", + "settings.label_mode.app_media": "App + Media" } diff --git a/locales/fr_FR.json b/locales/fr_FR.json index a6738bc..26ea83c 100644 --- a/locales/fr_FR.json +++ b/locales/fr_FR.json @@ -3,5 +3,11 @@ "open-browser.url.title": "URL :", "open-browser.new-window": "Ouvrir dans une nouvelle fenêtre", "delay.entry.title": "Délai (s) :", - "delay.entry.subtitle": "Le délai avant que la prochaine action" -} \ No newline at end of file + "delay.entry.subtitle": "Le délai avant que la prochaine action", + "settings.label_mode.title": "Source du libellé", + "settings.label_mode.subtitle": "Texte affiché comme nom du flux sur le deck", + "settings.label_mode.auto": "Auto (Media, sinon App)", + "settings.label_mode.app": "Application", + "settings.label_mode.media": "Media / Titre du flux", + "settings.label_mode.app_media": "App + Media" +} diff --git a/main.py b/main.py index ce765da..00da906 100644 --- a/main.py +++ b/main.py @@ -16,6 +16,7 @@ from loguru import logger as log from PIL import Image, ImageEnhance import math +import re import threading import subprocess import time @@ -25,7 +26,6 @@ from src.backend.DeckManagement.DeckController import DeckController from src.backend.PageManagement.Page import Page - import pulsectl from plugins.com_core447_VolumeMixer.actions.OpenVolumeMixer import OpenVolumeMixer from plugins.com_core447_VolumeMixer.actions.ExitVolumeMixer import ExitVolumeMixer @@ -40,7 +40,6 @@ sys.path.append(os.path.dirname(__file__)) - class VolumeMixer(PluginBase): def __init__(self): super().__init__() @@ -158,12 +157,9 @@ def __init__(self): app_version="1.0.0-alpha" ) - self.register_page(os.path.join(self.PATH, "pages", "VolumeMixer.json")) self.register_page(os.path.join(self.PATH, "pages", "VolumeMixerSDPlus.json")) - - def init_vars(self): self.lm = self.locale_manager self.lm.set_to_os_default() @@ -174,3 +170,84 @@ def init_vars(self): self.pulse = pulsectl.Pulse("stream-controller", threading_lock=True) self.volume_increment = 0.05 self.volume_actions: list[ActionBase] = [] + + # Label mode for stream names shown on the deck + # Values: auto | app | media | app+media + self._generic_media_regex = re.compile( + r"^(audio\s*stream\s*#?\d+|audiostream\s*#?\d+|playback\s*stream\s*#?\d+)$", + re.IGNORECASE + ) + try: + ps = self.get_settings() or {} + if "label_mode" not in ps: + ps["label_mode"] = "auto" + self.set_settings(ps) + except Exception as ex: + log.debug(f"Could not init plugin settings: {ex}") + + def refresh_volume_actions(self) -> None: + """Refresh labels/icons on all mixer actions.""" + for a in list(self.volume_actions): + try: + a.on_tick() + except Exception: + pass + + def get_label_mode(self) -> str: + try: + return (self.get_settings() or {}).get("label_mode", "auto") + except Exception: + return "auto" + + def _get_sink_input_proplist(self, sink_input) -> dict: + proplist = getattr(sink_input, "proplist", None) + if isinstance(proplist, dict): + return proplist + proplist = getattr(sink_input, "properties", None) + if isinstance(proplist, dict): + return proplist + return {} + + def get_display_name(self, sink_input) -> str: + """Return the text shown on the deck for a given sink_input.""" + proplist = self._get_sink_input_proplist(sink_input) + + fallback = getattr(sink_input, "name", "") or "" + app = ( + proplist.get("application.name") + or proplist.get("application.process.binary") + or proplist.get("application.process.user") + or "" + ) + media = ( + proplist.get("media.name") + or proplist.get("node.description") + or proplist.get("application.icon_name") + or "" + ) + + mode = (self.get_label_mode() or "auto").strip().lower() + + def is_generic_media(s: str) -> bool: + s2 = (s or "").strip() + if not s2: + return True + return bool(self._generic_media_regex.match(s2)) + + if mode == "app": + return app or fallback + if mode == "media": + return media or fallback + if mode in ("app+media", "app_media", "app-media"): + if app and media and app.strip().lower() != media.strip().lower(): + return f"{app}: {media}" + return app or media or fallback + + # auto (default) + if media and not is_generic_media(media): + return media + if app: + return app + if media: + return media + return fallback