Files
Modus/modules/dock/components/music_player.py
T
2025-06-29 23:41:21 +05:45

334 lines
12 KiB
Python

from fabric.widgets.box import Box
from fabric.widgets.button import Button
from fabric.widgets.circularprogressbar import CircularProgressBar
from fabric.widgets.eventbox import EventBox
from fabric.widgets.label import Label
from fabric.widgets.overlay import Overlay
from gi.repository import Gdk
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
class MusicPlayerPopup(Window):
"""Popup window that shows the full music player interface"""
def __init__(self, **kwargs):
# Get dock position for proper anchoring
dock_position = data.DOCK_POSITION
popup_anchor = self._get_popup_anchor(dock_position)
margin = self._get_popup_margin(dock_position)
super().__init__(
name="music-player-popup",
layer="top",
anchor=popup_anchor,
margin=margin,
exclusive=False,
keyboard_mode="on-demand",
visible=False,
all_visible=False,
**kwargs,
)
self.player_container = PlayerContainer()
self.add(self.player_container)
# 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_key_press(self, widget, event):
"""Handle key press events"""
if event.keyval == Gdk.KEY_Escape:
self.hide_popup()
return True
return False
def show_popup(self):
"""Show the music player popup"""
self.set_visible(True)
self.grab_focus()
def _get_popup_anchor(self, dock_position):
"""Get popup anchor based on dock position"""
anchor_map = {
"Top": "top",
"Bottom": "bottom",
"Left": "left",
"Right": "right",
}
return anchor_map.get(dock_position, "bottom")
def _get_popup_margin(self, dock_position):
"""Get popup margin based on dock position"""
margin_map = {
"Top": "60px 10px 10px 10px",
"Bottom": "10px 10px 60px 10px",
"Left": "10px 10px 10px 60px",
"Right": "10px 60px 10px 10px",
}
return margin_map.get(dock_position, "10px 10px 60px 10px")
def on_button_press(self, widget, event):
"""Handle button press events"""
return False
def hide_popup(self):
"""Hide the music player popup"""
self.set_visible(False)
class MusicPlayer(Box):
"""Music player component for the dock with circular progress bar"""
def __init__(self, **kwargs):
super().__init__(name="music-player", **kwargs)
# Initialize player manager
self.manager = PlayerManager()
self.manager.connect("new-player", self.on_new_player)
self.manager.connect("player-vanish", self.on_player_vanish)
# Current player tracking
self.current_player = None
self.current_player_service = None
self.duration = 0.0
self.position = 0.0
self._current_track_info = "Music Player"
# Create circular progress bar
self.progress_bar = CircularProgressBar(
name="player-circle",
size=28,
line_width=2,
start_angle=150,
end_angle=390,
value=0.0, # Initialize with 0 progress
)
# Create music icon label - always use generic music icon
self.music_label = Label(name="music-label", markup=icons.music)
# Create button for the music icon
self.music_button = Button(
on_clicked=self.on_music_clicked, child=self.music_label
)
# Create event box for scroll events (volume control)
self.event_box = EventBox(
events=["scroll", "smooth-scroll"],
child=Overlay(child=self.progress_bar, overlays=self.music_button),
)
# Connect scroll events for seeking
self.event_box.connect("scroll-event", self.on_scroll)
self.add_events(Gdk.EventMask.SCROLL_MASK | Gdk.EventMask.SMOOTH_SCROLL_MASK)
self.add(self.event_box)
# Popup window will be created on-demand
self.popup = None
# Initialize players
self.manager.init_all_players()
# Set initial visibility
self.update_visibility()
# 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 self.popup:
self.popup.destroy()
self.popup = None
def on_new_player(self, manager, player):
"""Handle new player detection"""
print(f"Music dock component: New player detected - {player.props.player_name}")
# If this is our first player, make it current
if not self.current_player:
self.set_current_player(player)
self.update_visibility()
def on_player_vanish(self, manager, player):
"""Handle player disappearing"""
print(f"Music dock component: Player vanished - {player.props.player_name}")
# If the current player vanished, find another one
if (
self.current_player
and self.current_player.props.player_name == player.props.player_name
):
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
self.update_visibility()
def set_current_player(self, player):
"""Set the current active player"""
# Disconnect from previous player
if self.current_player_service:
try:
self.current_player_service.disconnect_by_func(self.on_track_position)
self.current_player_service.disconnect_by_func(self.on_play)
self.current_player_service.disconnect_by_func(self.on_pause)
self.current_player_service.disconnect_by_func(self.on_metadata)
except:
pass
self.current_player = player
self.current_player_service = PlayerService(player=player)
# Connect to player events
self.current_player_service.connect("track-position", self.on_track_position)
self.current_player_service.connect("play", self.on_play)
self.current_player_service.connect("pause", self.on_pause)
self.current_player_service.connect("meta-change", self.on_metadata)
# Always use generic music icon, not player-specific icons
self.music_label.set_markup(icons.music)
# Update initial state
self.update_playback_state()
def on_track_position(self, service, pos, dur):
"""Handle track position updates"""
self.position = pos
self.duration = dur
if dur > 0:
progress = pos / dur
# Use value property instead of set_percentage for CircularProgressBar
self.progress_bar.value = progress
# Update tooltip with timestamp info
pos_min = int(pos // 60)
pos_sec = int(pos % 60)
dur_min = int(dur // 60)
dur_sec = int(dur % 60)
timestamp_text = f"{pos_min:02d}:{pos_sec:02d} / {dur_min:02d}:{dur_sec:02d}"
# Get current track info if available
if hasattr(self, '_current_track_info'):
tooltip = f"{self._current_track_info}\n{timestamp_text}"
else:
tooltip = f"Music Player\n{timestamp_text}"
self.event_box.set_tooltip_text(tooltip)
else:
self.progress_bar.value = 0
self.event_box.set_tooltip_text("Music Player")
def on_play(self, service):
"""Handle play state"""
self.music_label.set_markup(icons.music)
self.progress_bar.remove_style_class("paused")
# Update tooltip to show playing state
if hasattr(self, '_current_track_info'):
self.event_box.set_tooltip_text(f"{self._current_track_info} (Playing)")
def on_pause(self, service):
"""Handle pause state"""
# Always use generic music icon when paused
self.music_label.set_markup(icons.music)
self.progress_bar.add_style_class("paused")
# Update tooltip to show paused state
if hasattr(self, '_current_track_info'):
self.event_box.set_tooltip_text(f"{self._current_track_info} (Paused)")
def on_metadata(self, service, metadata, player):
"""Handle metadata changes"""
# Update tooltip with current track info
keys = metadata.keys()
if "xesam:artist" in keys and "xesam:title" in keys:
artist = (
metadata["xesam:artist"][0]
if metadata["xesam:artist"]
else "Unknown Artist"
)
title = metadata["xesam:title"]
self._current_track_info = f"{artist} - {title}"
# If we have position info, include timestamp
if hasattr(self, 'position') and hasattr(self, 'duration') and self.duration > 0:
pos_min = int(self.position // 60)
pos_sec = int(self.position % 60)
dur_min = int(self.duration // 60)
dur_sec = int(self.duration % 60)
timestamp_text = f"{pos_min:02d}:{pos_sec:02d} / {dur_min:02d}:{dur_sec:02d}"
tooltip = f"{self._current_track_info}\n{timestamp_text}"
else:
tooltip = self._current_track_info
self.event_box.set_tooltip_text(tooltip)
else:
self._current_track_info = "Music Player"
self.event_box.set_tooltip_text("Music Player")
def update_playback_state(self):
"""Update the visual state based on current playback"""
if not self.current_player:
return
if (
self.current_player.props.playback_status.value_name
== "PLAYERCTL_PLAYBACK_STATUS_PLAYING"
):
self.on_play(None)
else:
self.on_pause(None)
def on_music_clicked(self, button):
"""Handle music button click - show popup"""
if self.popup and self.popup.get_visible():
self.popup.hide_popup()
else:
# Create popup on-demand if it doesn't exist
if not self.popup:
self.popup = MusicPlayerPopup()
self.popup.show_popup()
def on_scroll(self, widget, event):
"""Handle scroll events for seeking"""
if not self.current_player or self.duration <= 0:
return False
# Calculate seek amount (5 seconds per scroll)
seek_amount = 5.0
if event.direction == Gdk.ScrollDirection.UP:
new_position = min(self.position + seek_amount, self.duration)
elif event.direction == Gdk.ScrollDirection.DOWN:
new_position = max(self.position - seek_amount, 0)
else:
return False
# Seek to new position
if self.current_player_service:
self.current_player_service.set_position(int(new_position))
return True
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)
# Hide in vertical mode if user preference is set
if data.VERTICAL and hasattr(data, "DOCK_COMPONENTS_VISIBILITY"):
music_visible = data.DOCK_COMPONENTS_VISIBILITY.get("music_player", True)
if not music_visible:
self.set_visible(False)