♻️ refactor(dock/components/music_player): update MusicPlayer to use PlayerContainer and handle active player changes

This commit is contained in:
sankalp committed 2025-06-30 01:07:50 +05:45
1 parent 0b652ff87a
commit 994b44f515
6 files changed
+242 -388

No files matched your search

+1 -1
View File
@@ -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
View File
@@ -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
-357
View File
@@ -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")
+160 -9
View File
@@ -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:
+10
View File
@@ -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 ────────────────────────────────────────────────
*/