♻️ refactor(dock/components/music_player): update MusicPlayer to use PlayerContainer and handle active player changes
This commit is contained in:
6 files changed
+242
-388
No files matched your search
+1
-1
@@ -10,7 +10,7 @@ gi.require_version("Gtk", "3.0")
|
||||
APP_NAME = "modus"
|
||||
APP_NAME_CAP = "Modus"
|
||||
|
||||
ALLOWED_PLAYERS = ["vlc", "mpv", "cmus", "firefox", "spotify", "chromium"]
|
||||
ALLOWED_PLAYERS = ["vlc", "cmus", "firefox", "spotify", "chromium"]
|
||||
|
||||
NOTIF_POS_KEY = "notif_pos"
|
||||
NOTIF_POS_DEFAULT = "Top"
|
||||
|
||||
+17
-17
@@ -1,27 +1,27 @@
|
||||
$wallpaper = /home/sankalp/Pictures/wallpapers/110.jpg
|
||||
$background = 171216
|
||||
$foreground = ebdfe6
|
||||
$wallpaper = /home/sankalp/Pictures/wallpapers/77.jpg
|
||||
$background = 121318
|
||||
$foreground = e2e2e9
|
||||
|
||||
$primary = eeb4ea
|
||||
$secondary = d9bfd4
|
||||
$tertiary = f6b8aa
|
||||
$surface = 171216
|
||||
$surface_bright = 3e373c
|
||||
$outline = 998d96
|
||||
$primary = afc6ff
|
||||
$secondary = bfc6dc
|
||||
$tertiary = dfbbde
|
||||
$surface = 121318
|
||||
$surface_bright = 38393e
|
||||
$outline = 8f9099
|
||||
$error = ff5945
|
||||
|
||||
$shadow = 000000
|
||||
|
||||
$red = ffb2b9
|
||||
$green = b7d085
|
||||
$yellow = dec56e
|
||||
$blue = cebdfe
|
||||
$magenta = efb3e8
|
||||
$green = 95d5a7
|
||||
$yellow = b8cf84
|
||||
$blue = b7c4ff
|
||||
$magenta = e4b7f3
|
||||
$cyan = 82d3e2
|
||||
$white = 82d3e0
|
||||
$red_dim = ff4c5c
|
||||
$green_dim = 89ad42
|
||||
$yellow_dim = bb9b2b
|
||||
$blue_dim = 8359fc
|
||||
$magenta_dim = dd5fcf
|
||||
$green_dim = 4cb86a
|
||||
$yellow_dim = 8baa43
|
||||
$blue_dim = 5170ff
|
||||
$magenta_dim = c360e4
|
||||
$cyan_dim = 30b5ce
|
||||
@@ -1,357 +0,0 @@
|
||||
from fabric.widgets.box import Box
|
||||
from fabric.widgets.label import Label
|
||||
from fabric.widgets.centerbox import CenterBox
|
||||
from fabric.widgets.button import Button
|
||||
from fabric.widgets.stack import Stack
|
||||
|
||||
from player_service import PlayerManager, PlayerService
|
||||
from wiggle_bar import WigglyWidget
|
||||
|
||||
import icons as icons
|
||||
import info as info
|
||||
|
||||
from loguru import logger
|
||||
|
||||
|
||||
class Player(Box):
|
||||
def __init__(self, player, **kwargs):
|
||||
super().__init__(style_classes="player", orientation="v", **kwargs)
|
||||
|
||||
if not info.VERTICAL:
|
||||
# vertical class binding from unknown source
|
||||
self.remove_style_class("vertical")
|
||||
|
||||
self._player = PlayerService(player=player)
|
||||
|
||||
self.duration = 0.0
|
||||
|
||||
self._player.connect("pause", self.on_pause)
|
||||
self._player.connect("play", self.on_play)
|
||||
self._player.connect("meta-change", self.on_metadata)
|
||||
self._player.connect("shuffle-toggle", self.on_shuffle)
|
||||
self._player.connect("track-position", self.on_update_track_position)
|
||||
|
||||
self.player_name = Label(
|
||||
name=player.props.player_name,
|
||||
style_classes="player-icon",
|
||||
markup=getattr(icons, player.props.player_name, icons.disc),
|
||||
)
|
||||
|
||||
self.set_style(
|
||||
f"background-image:url('{info.HOME_DIR}/.cache/walls/low_rez.png')"
|
||||
)
|
||||
|
||||
self.song = Label(
|
||||
name="song",
|
||||
label="song",
|
||||
justification="left",
|
||||
h_align="start",
|
||||
max_chars_width=10,
|
||||
)
|
||||
self.artist = Label(
|
||||
name="artist", label="artist", justification="left", h_align="start"
|
||||
)
|
||||
self.music = Box(
|
||||
name="music",
|
||||
orientation="v",
|
||||
h_expand=True,
|
||||
v_expand=True,
|
||||
children=[self.song, self.artist],
|
||||
)
|
||||
|
||||
self.play_pause_button = Button(
|
||||
name="pause-button",
|
||||
child=Label(name="pause-label", markup=icons.play),
|
||||
style_classes="pause-track",
|
||||
tooltip_text="Exit",
|
||||
on_clicked=lambda b, *_: self.handle_play_pause(player),
|
||||
)
|
||||
|
||||
self.shuffle_button = Button(
|
||||
name="shuffle-button",
|
||||
child=Label(name="shuffle", markup=icons.shuffle),
|
||||
on_clicked=lambda b, *_: self.handle_shuffle(b, player),
|
||||
)
|
||||
|
||||
self.wiggly = WigglyWidget()
|
||||
self.wiggly.connect("on-seek", self.on_seek)
|
||||
self.gtk_wrapper = Box(
|
||||
orientation="v",
|
||||
h_expand=True,
|
||||
v_expand=True,
|
||||
h_align="fill",
|
||||
v_align="fill",
|
||||
children=self.wiggly,
|
||||
)
|
||||
|
||||
self.children = [
|
||||
Box(name="source", h_expand=True, v_expand=True, children=self.player_name),
|
||||
CenterBox(
|
||||
name="details",
|
||||
# h_expand=True,
|
||||
# v_expand=True,
|
||||
start_children=self.music,
|
||||
end_children=self.play_pause_button if not info.VERTICAL else [],
|
||||
),
|
||||
Box(
|
||||
name="controls",
|
||||
# h_expand=True,
|
||||
# v_expand=True,
|
||||
style_classes="horizontal" if not info.VERTICAL else "vertical",
|
||||
spacing=5,
|
||||
children=[
|
||||
Button(
|
||||
name="prev-button",
|
||||
child=Label(name="play-previous", markup=icons.previous),
|
||||
on_clicked=lambda b, *_: self.handle_prev(player),
|
||||
),
|
||||
CenterBox(
|
||||
name="progress-container",
|
||||
h_expand=True,
|
||||
v_expand=True,
|
||||
orientation="v",
|
||||
center_children=[self.gtk_wrapper],
|
||||
),
|
||||
Button(
|
||||
name="next-button",
|
||||
child=Label(name="play-next", markup=icons.next),
|
||||
on_clicked=lambda b, *_: self.handle_next(player),
|
||||
),
|
||||
self.shuffle_button,
|
||||
]
|
||||
if not info.VERTICAL
|
||||
else [
|
||||
CenterBox(
|
||||
orientation="v",
|
||||
h_expand=True,
|
||||
start_children=[
|
||||
CenterBox(
|
||||
h_expand=True,
|
||||
v_expand=True,
|
||||
v_align="end",
|
||||
start_children=[
|
||||
Button(
|
||||
name="prev-button",
|
||||
child=Label(
|
||||
name="play-previous", markup=icons.previous
|
||||
),
|
||||
on_clicked=lambda b, *_: self.handle_prev(
|
||||
player
|
||||
),
|
||||
),
|
||||
Button(
|
||||
name="next-button",
|
||||
child=Label(
|
||||
name="play-next", markup=icons.next
|
||||
),
|
||||
on_clicked=lambda b, *_: self.handle_next(
|
||||
player
|
||||
),
|
||||
),
|
||||
self.shuffle_button,
|
||||
],
|
||||
end_children=self.play_pause_button,
|
||||
)
|
||||
],
|
||||
end_children=CenterBox(
|
||||
name="progress-container",
|
||||
h_expand=True,
|
||||
v_expand=True,
|
||||
orientation="v",
|
||||
center_children=[self.gtk_wrapper],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
]
|
||||
|
||||
self.on_metadata(self._player, metadata=player.props.metadata, player=player)
|
||||
|
||||
def on_update_track_position(self, sender, pos, dur):
|
||||
self.duration = dur
|
||||
self.wiggly.update_value_from_signal(pos / dur)
|
||||
|
||||
def on_seek(self, sender, ratio):
|
||||
pos = ratio * self.duration # duration in seconds
|
||||
print(f"Seeking to {pos:.2f}s")
|
||||
self._player.set_position(int(pos))
|
||||
|
||||
def on_metadata(self, sender, metadata, player):
|
||||
keys = metadata.keys()
|
||||
if "xesam:artist" in keys and "xesam:title" in keys:
|
||||
_max_chars = 43 if not info.VERTICAL else 30
|
||||
song_title = metadata["xesam:title"]
|
||||
if len(song_title) > _max_chars:
|
||||
song_title = song_title[: _max_chars - 1] + "…"
|
||||
self.song.set_label(song_title)
|
||||
|
||||
artist_list = metadata["xesam:artist"]
|
||||
artist_name = artist_list[0] if artist_list else "Unknown Artist"
|
||||
if len(artist_name) > _max_chars:
|
||||
artist_name = artist_name[: _max_chars - 1] + "…"
|
||||
self.artist.set_label(artist_name)
|
||||
if "mpris:artUrl" in keys:
|
||||
self.set_style(f"background-image:url('{metadata['mpris:artUrl']}')")
|
||||
|
||||
if (
|
||||
player.props.playback_status.value_name
|
||||
== "PLAYERCTL_PLAYBACK_STATUS_PLAYING"
|
||||
):
|
||||
self.on_play(self._player)
|
||||
|
||||
if player.props.shuffle == True:
|
||||
self.shuffle_button.get_child().set_markup(icons.disable_shuffle)
|
||||
self.shuffle_button.get_child().set_name("disable-shuffle")
|
||||
else:
|
||||
self.shuffle_button.get_child().set_markup(icons.shuffle)
|
||||
self.shuffle_button.get_child().set_name("shuffle")
|
||||
|
||||
def on_pause(self, sender):
|
||||
self.play_pause_button.get_child().set_markup(icons.play)
|
||||
self.play_pause_button.get_child().set_name("pause-label")
|
||||
self.wiggly.dragging = True
|
||||
self.wiggly.update_amplitude(True)
|
||||
self.wiggly.pause = True
|
||||
self.play_pause_button.add_style_class("pause-track")
|
||||
|
||||
def on_play(self, sender):
|
||||
self.play_pause_button.get_child().set_markup(icons.pause)
|
||||
self.play_pause_button.get_child().set_name("play-label")
|
||||
self.wiggly.pause = False
|
||||
self.wiggly.dragging = False
|
||||
self.wiggly.update_amplitude(False)
|
||||
self.play_pause_button.remove_style_class("pause-track")
|
||||
|
||||
def on_shuffle(self, sender, player, status):
|
||||
print("callback status", status)
|
||||
if status == False:
|
||||
self.shuffle_button.get_child().set_markup(icons.shuffle)
|
||||
self.shuffle_button.get_child().set_name("shuffle")
|
||||
else:
|
||||
self.shuffle_button.get_child().set_markup(icons.disable_shuffle)
|
||||
self.shuffle_button.get_child().set_name("disable-shuffle")
|
||||
|
||||
self.shuffle_button.get_child().set_style("color: white")
|
||||
|
||||
def handle_next(self, player):
|
||||
player.next()
|
||||
|
||||
def handle_prev(self, player):
|
||||
player.previous()
|
||||
|
||||
def handle_play_pause(self, player):
|
||||
is_playing = self.play_pause_button.get_child().get_name() == "play-label"
|
||||
|
||||
def _set_play_ui():
|
||||
self.play_pause_button.get_child().set_markup(icons.pause)
|
||||
self.play_pause_button.remove_style_class("pause-track")
|
||||
self.play_pause_button.get_child().set_name("pause-label")
|
||||
|
||||
def _set_pause_ui():
|
||||
self.play_pause_button.get_child().set_markup(icons.play)
|
||||
self.play_pause_button.add_style_class("pause-track")
|
||||
self.play_pause_button.get_child().set_name("play-label")
|
||||
|
||||
if is_playing:
|
||||
_set_pause_ui()
|
||||
else:
|
||||
_set_play_ui()
|
||||
|
||||
try:
|
||||
player.play_pause()
|
||||
except Exception as e:
|
||||
# revert if signal failed
|
||||
if is_playing:
|
||||
_set_pause_ui()
|
||||
else:
|
||||
_set_play_ui()
|
||||
logger.warning("Failed to toggle playback:", e)
|
||||
|
||||
def handle_shuffle(self, shuffle_button, player):
|
||||
print("shuffle", player.props.shuffle)
|
||||
if player.props.shuffle == False:
|
||||
player.set_shuffle(True)
|
||||
print("setting to true", player.props.player_name)
|
||||
else:
|
||||
player.set_shuffle(False)
|
||||
shuffle_button.get_child().set_style("color: var(--outline)")
|
||||
|
||||
|
||||
class PlayerContainer(Box):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(name="player-container", orientation="v", **kwargs)
|
||||
|
||||
self.manager = PlayerManager()
|
||||
self.manager.connect("new-player", self.new_player)
|
||||
self.manager.connect("player-vanish", self.on_player_vanish)
|
||||
self.stack = Stack(
|
||||
name="player-container",
|
||||
transition_type="crossfade",
|
||||
transition_duration=100,
|
||||
children=[],
|
||||
)
|
||||
|
||||
self.player_stack = Stack(
|
||||
name="player-stack",
|
||||
transition_type="crossfade",
|
||||
transition_duration=100,
|
||||
children=[],
|
||||
)
|
||||
|
||||
self.player_switch_container = CenterBox(
|
||||
name="player-switch-container",
|
||||
orientation="h",
|
||||
style_classes="horizontal-player"
|
||||
if not info.VERTICAL
|
||||
else "vertical-player",
|
||||
center_children=[],
|
||||
)
|
||||
self.children = [self.stack, self.player_switch_container]
|
||||
self.player = []
|
||||
self.manager.init_all_players()
|
||||
|
||||
def new_player(self, manager, player):
|
||||
print(player.props.player_name, "new player")
|
||||
print(player)
|
||||
new_player = Player(player=player)
|
||||
new_player.gtk_wrapper.queue_draw()
|
||||
new_player.set_name(player.props.player_name)
|
||||
self.player.append(new_player)
|
||||
print("stacking dis bitvch")
|
||||
self.stack.add_named(new_player, player.props.player_name)
|
||||
|
||||
self.player_switch_container.add_center(
|
||||
Button(
|
||||
name=player.props.player_name,
|
||||
style_classes="player-button",
|
||||
on_clicked=lambda b: self.switch_player(player.props.player_name, b),
|
||||
)
|
||||
)
|
||||
self.update_player_list()
|
||||
|
||||
def switch_player(self, player_name, button):
|
||||
self.stack.set_visible_child_name(player_name)
|
||||
|
||||
for btn in self.player_switch_container.center_children:
|
||||
btn.remove_style_class("active")
|
||||
button.add_style_class("active")
|
||||
|
||||
def on_player_vanish(self, manager, player):
|
||||
for player_instance in self.player:
|
||||
if player_instance.get_name() == player.props.player_name:
|
||||
self.stack.remove(player_instance)
|
||||
self.player.remove(player_instance)
|
||||
for btn in self.player_switch_container.center_children:
|
||||
if btn.get_name() == player_instance.get_name():
|
||||
self.player_switch_container.remove_center(btn)
|
||||
self.update_player_list()
|
||||
break
|
||||
|
||||
def update_player_list(self):
|
||||
curr = self.stack.get_visible_child()
|
||||
for btn in self.player_switch_container.center_children:
|
||||
print(btn.get_name())
|
||||
if btn.get_name() == curr.get_name():
|
||||
btn.add_style_class("active")
|
||||
else:
|
||||
btn.remove_style_class("active")
|
||||
@@ -10,13 +10,13 @@ import config.data as data
|
||||
import utils.icons as icons
|
||||
from services.mpris import PlayerManager, PlayerService
|
||||
from utils.wayland import WaylandWindow as Window
|
||||
from player import PlayerContainer
|
||||
from modules.dock.components.player import PlayerContainer
|
||||
|
||||
|
||||
class MusicPlayerPopup(Window):
|
||||
"""Popup window that shows the full music player interface"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
def __init__(self, dock_component=None, **kwargs):
|
||||
# Get dock position for proper anchoring
|
||||
dock_position = data.DOCK_POSITION
|
||||
popup_anchor = self._get_popup_anchor(dock_position)
|
||||
@@ -34,14 +34,24 @@ class MusicPlayerPopup(Window):
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
self.dock_component = dock_component
|
||||
self.player_container = PlayerContainer()
|
||||
self.add(self.player_container)
|
||||
|
||||
# Connect player container events to sync with dock
|
||||
if self.dock_component:
|
||||
self.player_container.connect("active-player-changed", self.on_active_player_changed)
|
||||
|
||||
# Connect to escape key to close
|
||||
self.connect("key-press-event", self.on_key_press)
|
||||
self.connect("button-press-event", self.on_button_press)
|
||||
self.set_can_focus(True)
|
||||
|
||||
def on_active_player_changed(self, container, player):
|
||||
"""Handle active player change from player container"""
|
||||
if self.dock_component and player:
|
||||
self.dock_component.set_current_player(player)
|
||||
|
||||
def on_key_press(self, widget, event):
|
||||
"""Handle key press events"""
|
||||
if event.keyval == Gdk.KEY_Escape:
|
||||
@@ -140,11 +150,18 @@ class MusicPlayer(Box):
|
||||
# Set initial visibility
|
||||
self.update_visibility()
|
||||
|
||||
# Set up periodic check for active player
|
||||
from gi.repository import GLib
|
||||
self.active_player_check_timeout = GLib.timeout_add_seconds(2, self.check_active_player)
|
||||
|
||||
# Connect to destroy signal for cleanup
|
||||
self.connect("destroy", self.on_destroy)
|
||||
|
||||
def on_destroy(self, widget):
|
||||
"""Clean up popup when component is destroyed"""
|
||||
if hasattr(self, 'active_player_check_timeout'):
|
||||
from gi.repository import GLib
|
||||
GLib.source_remove(self.active_player_check_timeout)
|
||||
if self.popup:
|
||||
self.popup.destroy()
|
||||
self.popup = None
|
||||
@@ -156,6 +173,11 @@ class MusicPlayer(Box):
|
||||
# If this is our first player, make it current
|
||||
if not self.current_player:
|
||||
self.set_current_player(player)
|
||||
else:
|
||||
# If the new player is playing, switch to it
|
||||
if (player.props.playback_status.value_name == "PLAYERCTL_PLAYBACK_STATUS_PLAYING" and
|
||||
self.current_player.props.playback_status.value_name != "PLAYERCTL_PLAYBACK_STATUS_PLAYING"):
|
||||
self.set_current_player(player)
|
||||
|
||||
self.update_visibility()
|
||||
|
||||
@@ -171,13 +193,93 @@ class MusicPlayer(Box):
|
||||
self.current_player = None
|
||||
self.current_player_service = None
|
||||
|
||||
# Try to find another active player
|
||||
# This is a simple approach - in a real implementation you might want more sophisticated logic
|
||||
# Try to find another player with media content
|
||||
if hasattr(self.manager, '_manager') and hasattr(self.manager._manager, 'props'):
|
||||
for player_name in self.manager._manager.props.player_names:
|
||||
try:
|
||||
from gi.repository import Playerctl
|
||||
candidate_player = Playerctl.Player.new_from_name(player_name)
|
||||
|
||||
# Check if this player has media content
|
||||
if (hasattr(candidate_player, 'props') and
|
||||
hasattr(candidate_player.props, 'metadata')):
|
||||
metadata = candidate_player.props.metadata
|
||||
if metadata and len(metadata.keys()) > 0:
|
||||
keys = metadata.keys()
|
||||
if ("xesam:title" in keys or
|
||||
"xesam:artist" in keys or
|
||||
"mpris:length" in keys):
|
||||
print(f"[DEBUG] Switching to player with media: {candidate_player.props.player_name}")
|
||||
self.set_current_player(candidate_player)
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"[DEBUG] Error checking candidate player {player_name}: {e}")
|
||||
continue
|
||||
|
||||
self.update_visibility()
|
||||
|
||||
def get_playing_player(self):
|
||||
"""Find a currently playing player from the manager"""
|
||||
try:
|
||||
if hasattr(self.manager, '_manager') and hasattr(self.manager._manager, 'props'):
|
||||
for player_name in self.manager._manager.props.player_names:
|
||||
try:
|
||||
from gi.repository import Playerctl
|
||||
player = Playerctl.Player.new_from_name(player_name)
|
||||
if (hasattr(player, 'props') and
|
||||
hasattr(player.props, 'playback_status') and
|
||||
player.props.playback_status.value_name == "PLAYERCTL_PLAYBACK_STATUS_PLAYING"):
|
||||
print(f"[DEBUG] Found playing player: {player.props.player_name}")
|
||||
return player
|
||||
except Exception as e:
|
||||
print(f"[DEBUG] Error checking player {player_name}: {e}")
|
||||
continue
|
||||
except Exception as e:
|
||||
print(f"[DEBUG] Error in get_playing_player: {e}")
|
||||
return None
|
||||
|
||||
def check_active_player(self):
|
||||
"""Periodically check for the currently playing player and switch if needed"""
|
||||
try:
|
||||
# Find the currently playing player
|
||||
playing_player = self.get_playing_player()
|
||||
|
||||
if playing_player:
|
||||
# If we found a playing player and it's different from current, switch to it
|
||||
if (not self.current_player or
|
||||
self.current_player.props.player_name != playing_player.props.player_name):
|
||||
print(f"[DEBUG] Switching to playing player: {playing_player.props.player_name}")
|
||||
self.set_current_player(playing_player)
|
||||
elif self.current_player:
|
||||
# If no player is playing, check if current player is paused
|
||||
if (self.current_player.props.playback_status.value_name != "PLAYERCTL_PLAYBACK_STATUS_PLAYING"):
|
||||
# Current player is paused, see if there's another player available
|
||||
if hasattr(self.manager, '_manager') and hasattr(self.manager._manager, 'props'):
|
||||
for player_name in self.manager._manager.props.player_names:
|
||||
try:
|
||||
from gi.repository import Playerctl
|
||||
player = Playerctl.Player.new_from_name(player_name)
|
||||
if player.props.player_name != self.current_player.props.player_name:
|
||||
# Switch to a different available player
|
||||
print(f"[DEBUG] Switching to available player: {player.props.player_name}")
|
||||
self.set_current_player(player)
|
||||
break
|
||||
except:
|
||||
continue
|
||||
|
||||
# Update visibility based on current state
|
||||
self.update_visibility()
|
||||
|
||||
except Exception as e:
|
||||
print(f"[DEBUG] Error in check_active_player: {e}")
|
||||
|
||||
# Return True to continue the timeout
|
||||
return True
|
||||
|
||||
def set_current_player(self, player):
|
||||
"""Set the current active player"""
|
||||
print(f"[DEBUG] Setting current player to: {player.props.player_name}")
|
||||
|
||||
# Disconnect from previous player
|
||||
if self.current_player_service:
|
||||
try:
|
||||
@@ -238,6 +340,11 @@ class MusicPlayer(Box):
|
||||
if hasattr(self, '_current_track_info'):
|
||||
self.event_box.set_tooltip_text(f"{self._current_track_info} (Playing)")
|
||||
|
||||
# Trigger immediate check for active player when play state changes
|
||||
self.check_active_player()
|
||||
# Update visibility when playback state changes
|
||||
self.update_visibility()
|
||||
|
||||
def on_pause(self, service):
|
||||
"""Handle pause state"""
|
||||
# Always use generic music icon when paused
|
||||
@@ -247,6 +354,11 @@ class MusicPlayer(Box):
|
||||
if hasattr(self, '_current_track_info'):
|
||||
self.event_box.set_tooltip_text(f"{self._current_track_info} (Paused)")
|
||||
|
||||
# Trigger immediate check for active player when pause state changes
|
||||
self.check_active_player()
|
||||
# Update visibility when playback state changes
|
||||
self.update_visibility()
|
||||
|
||||
def on_metadata(self, service, metadata, player):
|
||||
"""Handle metadata changes"""
|
||||
# Update tooltip with current track info
|
||||
@@ -276,6 +388,9 @@ class MusicPlayer(Box):
|
||||
self._current_track_info = "Music Player"
|
||||
self.event_box.set_tooltip_text("Music Player")
|
||||
|
||||
# Update visibility when metadata changes (media content appears/disappears)
|
||||
self.update_visibility()
|
||||
|
||||
def update_playback_state(self):
|
||||
"""Update the visual state based on current playback"""
|
||||
if not self.current_player:
|
||||
@@ -296,7 +411,7 @@ class MusicPlayer(Box):
|
||||
else:
|
||||
# Create popup on-demand if it doesn't exist
|
||||
if not self.popup:
|
||||
self.popup = MusicPlayerPopup()
|
||||
self.popup = MusicPlayerPopup(dock_component=self)
|
||||
self.popup.show_popup()
|
||||
|
||||
def on_scroll(self, widget, event):
|
||||
@@ -320,11 +435,47 @@ class MusicPlayer(Box):
|
||||
|
||||
return True
|
||||
|
||||
def has_running_players(self):
|
||||
"""Check if there are any players that actually have media content running"""
|
||||
try:
|
||||
if hasattr(self.manager, '_manager') and hasattr(self.manager._manager, 'props'):
|
||||
for player_name in self.manager._manager.props.player_names:
|
||||
try:
|
||||
from gi.repository import Playerctl
|
||||
player = Playerctl.Player.new_from_name(player_name)
|
||||
|
||||
# Check if player has metadata indicating actual media content
|
||||
if hasattr(player, 'props') and hasattr(player.props, 'metadata'):
|
||||
metadata = player.props.metadata
|
||||
if metadata and len(metadata.keys()) > 0:
|
||||
# Check for essential metadata that indicates real media
|
||||
keys = metadata.keys()
|
||||
if ("xesam:title" in keys or
|
||||
"xesam:artist" in keys or
|
||||
"mpris:length" in keys):
|
||||
print(f"[DEBUG] Found player with media content: {player.props.player_name}")
|
||||
return True
|
||||
|
||||
# Also check if player is currently playing (even without full metadata)
|
||||
if (hasattr(player, 'props') and
|
||||
hasattr(player.props, 'playback_status') and
|
||||
player.props.playback_status.value_name == "PLAYERCTL_PLAYBACK_STATUS_PLAYING"):
|
||||
print(f"[DEBUG] Found playing player: {player.props.player_name}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"[DEBUG] Error checking player {player_name}: {e}")
|
||||
continue
|
||||
except Exception as e:
|
||||
print(f"[DEBUG] Error in has_running_players: {e}")
|
||||
|
||||
return False
|
||||
|
||||
def update_visibility(self):
|
||||
"""Update component visibility based on available players"""
|
||||
# Show component only if there are active players
|
||||
has_players = self.current_player is not None
|
||||
self.set_visible(has_players)
|
||||
"""Update component visibility based on players with actual media content"""
|
||||
# Show component only if there are players with actual media content
|
||||
has_media_players = self.has_running_players()
|
||||
self.set_visible(has_media_players)
|
||||
|
||||
# Hide in vertical mode if user preference is set
|
||||
if data.VERTICAL and hasattr(data, "DOCK_COMPONENTS_VISIBILITY"):
|
||||
|
||||
@@ -3,6 +3,7 @@ from fabric.widgets.label import Label
|
||||
from fabric.widgets.centerbox import CenterBox
|
||||
from fabric.widgets.button import Button
|
||||
from fabric.widgets.stack import Stack
|
||||
from fabric.core.service import Signal
|
||||
|
||||
from services.mpris import PlayerManager, PlayerService
|
||||
from utils.wiggle_bar import WigglyWidget
|
||||
@@ -42,7 +43,7 @@ class Player(Box):
|
||||
|
||||
# Use a fallback background image
|
||||
self.set_style(
|
||||
f"background-image:url('{data.HOME_DIR}/.cache/walls/low_rez.png')"
|
||||
f"background-image:url('{data.HOME_DIR}/.current.wall')"
|
||||
)
|
||||
|
||||
self.song = Label(
|
||||
@@ -197,6 +198,22 @@ class Player(Box):
|
||||
|
||||
if "mpris:artUrl" in keys:
|
||||
self.set_style(f"background-image:url('{metadata['mpris:artUrl']}')")
|
||||
elif "xesam:url" in keys and "youtube.com" in metadata["xesam:url"]:
|
||||
# Simple YouTube thumbnail extraction for Firefox
|
||||
import re
|
||||
url = metadata["xesam:url"]
|
||||
match = re.search(r'(?:v=|/)([a-zA-Z0-9_-]{11})', url)
|
||||
if match:
|
||||
video_id = match.group(1)
|
||||
thumbnail_url = f"https://img.youtube.com/vi/{video_id}/hqdefault.jpg"
|
||||
self.set_style(f"background-image:url('{thumbnail_url}')")
|
||||
print(f"Using YouTube thumbnail: {thumbnail_url}")
|
||||
else:
|
||||
# Fallback to default wallpaper
|
||||
self.set_style(f"background-image:url('{data.HOME_DIR}/.current.wall')")
|
||||
else:
|
||||
# Fallback to default wallpaper when no artwork is available
|
||||
self.set_style(f"background-image:url('{data.HOME_DIR}/.current.wall')")
|
||||
|
||||
if (
|
||||
player.props.playback_status.value_name
|
||||
@@ -283,6 +300,9 @@ class Player(Box):
|
||||
|
||||
|
||||
class PlayerContainer(Box):
|
||||
@Signal
|
||||
def active_player_changed(self, player) -> None: ...
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(name="player-container", orientation="v", **kwargs)
|
||||
|
||||
@@ -316,6 +336,8 @@ class PlayerContainer(Box):
|
||||
)
|
||||
self.children = [self.stack, self.player_switch_container]
|
||||
self.player = []
|
||||
self.player_objects = {} # Map player names to player objects
|
||||
self.active_player = None # Track the currently active player
|
||||
self.manager.init_all_players()
|
||||
|
||||
def new_player(self, manager, player):
|
||||
@@ -325,6 +347,7 @@ class PlayerContainer(Box):
|
||||
new_player.gtk_wrapper.queue_draw()
|
||||
new_player.set_name(player.props.player_name)
|
||||
self.player.append(new_player)
|
||||
self.player_objects[player.props.player_name] = player # Store the player object
|
||||
print("stacking player")
|
||||
self.stack.add_named(new_player, player.props.player_name)
|
||||
|
||||
@@ -335,6 +358,12 @@ class PlayerContainer(Box):
|
||||
on_clicked=lambda b: self.switch_player(player.props.player_name, b),
|
||||
)
|
||||
)
|
||||
|
||||
# If this is the first player, make it active
|
||||
if not self.active_player:
|
||||
self.active_player = player
|
||||
self.active_player_changed(player)
|
||||
|
||||
self.update_player_list()
|
||||
|
||||
def switch_player(self, player_name, button):
|
||||
@@ -344,17 +373,38 @@ class PlayerContainer(Box):
|
||||
btn.remove_style_class("active")
|
||||
button.add_style_class("active")
|
||||
|
||||
# Get the player object and emit signal
|
||||
if player_name in self.player_objects:
|
||||
self.active_player = self.player_objects[player_name]
|
||||
self.active_player_changed(self.active_player)
|
||||
|
||||
def on_player_vanish(self, manager, player):
|
||||
player_name = player.props.player_name
|
||||
|
||||
for player_instance in self.player:
|
||||
if player_instance.get_name() == player.props.player_name:
|
||||
if player_instance.get_name() == player_name:
|
||||
self.stack.remove(player_instance)
|
||||
self.player.remove(player_instance)
|
||||
for btn in self.player_switch_container.center_children:
|
||||
if btn.get_name() == player_instance.get_name():
|
||||
if btn.get_name() == player_name:
|
||||
self.player_switch_container.remove_center(btn)
|
||||
self.update_player_list()
|
||||
break
|
||||
|
||||
# Clean up player objects dictionary
|
||||
if player_name in self.player_objects:
|
||||
del self.player_objects[player_name]
|
||||
|
||||
# If the active player vanished, switch to another one
|
||||
if self.active_player and self.active_player.props.player_name == player_name:
|
||||
self.active_player = None
|
||||
# Try to find another player to make active
|
||||
if self.player_objects:
|
||||
first_player_name = next(iter(self.player_objects))
|
||||
self.active_player = self.player_objects[first_player_name]
|
||||
self.active_player_changed(self.active_player)
|
||||
|
||||
self.update_player_list()
|
||||
|
||||
def update_player_list(self):
|
||||
curr = self.stack.get_visible_child()
|
||||
if curr:
|
||||
@@ -1,8 +1,18 @@
|
||||
#music-player{
|
||||
background-color: var(--surface);
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
#music-player.invert {
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
#player-circle {
|
||||
padding: 4px;
|
||||
color: var(--surface-bright);
|
||||
border: 3px solid var(--primary);
|
||||
}
|
||||
|
||||
/*
|
||||
* ── language indicator ────────────────────────────────────────────────
|
||||
*/
|
||||
|
||||
Reference in new issue
Block a user