diff --git a/modules/controlcenter/expanded_player.py b/modules/controlcenter/expanded_player.py index 9cd57e8..47218cf 100644 --- a/modules/controlcenter/expanded_player.py +++ b/modules/controlcenter/expanded_player.py @@ -50,6 +50,64 @@ def cleanup_old_cache_files(): pass # Ignore all errors in cleanup +class EmbeddedExpandedPlayer(Box): + """Embedded expanded player widget for use inside control center.""" + + def __init__(self, control_center, **kwargs): + super().__init__( + orientation="vertical", + h_expand=True, + name="embedded-expanded-player", + **kwargs, + ) + + self.control_center = control_center + self.mpris_manager = MprisPlayerManager() + + # Create back button + self.back_button = Button( + name="back-button", + child=Label(label="← Back"), + on_clicked=self._on_back_clicked, + ) + + # Create expanded player content + self.player_content = PlayerBoxStack(self.mpris_manager) + + self.children = [ + Box( + orientation="horizontal", + h_expand=True, + style_classes="menu", + children=[ + self.back_button, + Box(h_expand=True), # Spacer + Label(label="Now Playing", style_classes="title"), + Box(h_expand=True), # Spacer + ], + ), + self.player_content, + ] + + def _on_back_clicked(self, *_): + """Handle back button click""" + if self.control_center and hasattr( + self.control_center, "close_expanded_player" + ): + self.control_center.close_expanded_player() + + def refresh(self): + """Refresh the player content""" + # This will automatically update as MPRIS players change + pass + + def destroy(self): + """Clean up resources""" + if hasattr(self.player_content, "destroy"): + self.player_content.destroy() + super().destroy() + + class PlayerBoxStack(Box): """A widget that displays the current player information.""" @@ -187,6 +245,7 @@ class PlayerBoxStack(Box): v_align="center", h_align="start", orientation="v", + h_expand=True, children=[track_info, controls_box], ) @@ -406,12 +465,13 @@ class PlayerBox(Box): self.player_stack = player_stack self.fallback_cover_path = f"{data.HOME_DIR}/.current.wall" - self.image_size = 120 + self.image_size = 100 # Smaller album art self.icon_size = 15 # State self.exit = False self.skipped = False + self._user_seeking = False # Flag to prevent choppy seeking # Memory management self.temp_artwork_files = [] # Track temp files for cleanup @@ -434,16 +494,39 @@ class PlayerBox(Box): label="No Title", name="player-title", justification="left", - max_chars_width=15, + max_chars_width=35, ellipsization="end", h_align="start", + h_expand=True, ) self.track_artist = Label( label="No Artist", name="player-artist", justification="left", - max_chars_width=15, + max_chars_width=35, + ellipsization="end", + h_align="start", + h_expand=True, + visible=True, + ) + + self.track_album = Label( + label="No Album", + name="player-album", + justification="left", + max_chars_width=35, + ellipsization="end", + h_align="start", + h_expand=True, + visible=True, + ) + + self.track_artist = Label( + label="No Artist", + name="player-artist", + justification="left", + max_chars_width=25, ellipsization="end", h_align="start", visible=True, @@ -453,22 +536,27 @@ class PlayerBox(Box): label="No Album", name="player-album", justification="left", - max_chars_width=15, + max_chars_width=25, ellipsization="end", h_align="start", visible=True, ) + # Seek bar should not update automatically during user interaction + self._user_seeking = False + self.seek_bar = Scale( value=0, min_value=0, max_value=100, - increments=(5, 5), - name="seek-bar", - size=2, + increments=(1, 1), + name="apple-seek-bar", + size=1, h_expand=True, ) self.seek_bar.connect("value-changed", self._on_scale_value_changed) + self.seek_bar.connect("button-press-event", self._on_seek_start) + self.seek_bar.connect("button-release-event", self._on_seek_end) self.player.bind("can-seek", "sensitive", self.seek_bar) self.player.bind_property( @@ -519,7 +607,6 @@ class PlayerBox(Box): self.labels_box = Box( name="labels-box", orientation="h", - h_expand=True, children=[ self.position_label, Box(h_expand=True), # Spacer to push labels to ends @@ -531,7 +618,7 @@ class PlayerBox(Box): self.seek_box = Box( name="seek-box", orientation="v", - spacing=2, + spacing=1, children=[ self.seek_bar, self.labels_box, @@ -540,10 +627,11 @@ class PlayerBox(Box): self.track_info = Box( name="track-info", - spacing=5, + spacing=3, + h_expand=True, orientation="v", v_align="start", - h_align="start", + h_align="fill", children=[ self.track_title, self.track_artist, @@ -561,6 +649,8 @@ class PlayerBox(Box): # Stack switcher buttons box self.stack_buttons_box = Box( + h_expand=False, + v_expand=True, name="stack-buttons-box", spacing=4, orientation="v", @@ -571,6 +661,7 @@ class PlayerBox(Box): self.controls_box = CenterBox( name="player-controls", + h_expand=True, center_children=self.button_box, ) @@ -614,8 +705,9 @@ class PlayerBox(Box): ) self.player_info_box = Box( name="player-info-box", + h_expand=True, v_align="center", - h_align="start", + h_align="fill", orientation="v", children=[self.track_info, self.controls_box], ) @@ -628,32 +720,39 @@ class PlayerBox(Box): # resize the inner box self.outer_box = Box( name="outer-player-box", + h_expand=True, + orientation="horizontal", + children=[ + self.image_stack, + self.player_info_box, + Box( + orientation="v", + children=[ + Box( + children=Image( + icon_name=self.player.player_name, icon_size=18 + ), + h_align="end", + h_expand=False, + v_align="start", + style="margin-top: 5px; margin-right: 10px;", + tooltip_text=self.player.player_name, # type: ignore + ), + Box( + children=self.stack_buttons_box, + h_align="end", + v_expand=True, + v_align="center", + h_expand=False, + style="margin-right: 15px;", + ), + ], + ), + ], h_align="start", ) - self.overlay_box = Overlay( - child=self.outer_box, - overlays=[ - self.inner_box, - self.player_info_box, - self.image_stack, - Box( - children=Image(icon_name=self.player.player_name, icon_size=18), - h_align="end", - v_align="start", - style="margin-top: 5px; margin-right: 10px;", - tooltip_text=self.player.player_name, # type: ignore - ), - Box( - children=self.stack_buttons_box, - h_align="end", - v_align="center", - style="margin-right: 15px;", - ), - ], - ) - - self.children = [*self.children, self.overlay_box] + self.children = [*self.children, self.outer_box] # Track signal connections for cleanup connections = bulk_connect( @@ -770,7 +869,10 @@ class PlayerBox(Box): if duration: self.length_label.set_label(self.length_str(duration)) - self.seek_bar.set_range(0, duration) + # Clamp duration to avoid 32-bit integer overflow in the scale widget + max_int32 = 2147483647 # 2^31 - 1 + safe_duration = min(max_int32, duration) + self.seek_bar.set_range(0, safe_duration) invoke_repeater(1000, self._move_seekbar) @@ -919,20 +1021,47 @@ class PlayerBox(Box): return None def _move_seekbar(self, *_): - if self.player is None or self.exit: - return False + if self.player is None or self.exit or self._user_seeking: + return True # Continue the timer but don't update while user is seeking - self.position_label.set_label(self.length_str(self.player.position)) - self.seek_bar.set_value(self.player.position) + position = self.player.position + self.position_label.set_label(self.length_str(position)) + + # Only update seek bar if user is not currently seeking + if not self._user_seeking: + # Clamp position to avoid 32-bit integer overflow + max_int32 = 2147483647 # 2^31 - 1 + safe_position = min(max_int32, position) if position else 0 + self.seek_bar.set_value(safe_position) return True + def _on_seek_start(self, widget, event): + """User started seeking - disable automatic updates""" + self._user_seeking = True + return False + + def _on_seek_end(self, widget, event): + """User finished seeking - re-enable automatic updates""" + self._user_seeking = False + return False def _on_scale_value_changed(self, scale: Scale): - """Handle seek bar value changes.""" - if self.player and not self.exit: - new_position = scale.get_value() - self.player.position = new_position - self.position_label.set_label(self.length_str(new_position)) + """Handle seek bar value changes - only when user is seeking""" + if self.player and not self.exit and self._user_seeking: + new_position = int(scale.get_value()) + # Clamp to 32-bit signed integer range to avoid overflow + max_int32 = 2147483647 # 2^31 - 1 + min_int32 = -2147483648 # -2^31 + new_position = max(min_int32, min(max_int32, new_position)) + try: + self.player.position = new_position + self.position_label.set_label(self.length_str(new_position)) + except Exception as e: + # If setting position fails, just update the label + self.position_label.set_label(self.length_str(new_position)) + except Exception: + # If setting position fails, just update the label + self.position_label.set_label(self.length_str(new_position)) @cooldown(0.1) def _on_scale_move(self, scale: Scale, event, pos: int): diff --git a/modules/controlcenter/main.py b/modules/controlcenter/main.py index 7a93f5c..5eea222 100644 --- a/modules/controlcenter/main.py +++ b/modules/controlcenter/main.py @@ -17,6 +17,7 @@ from utils.roam import audio_service, modus_service from widgets.wayland import WaylandWindow as Window from modules.controlcenter.player import PlayerBoxStack from modules.controlcenter.per_app_volume import PerAppVolumeControl +from modules.controlcenter.expanded_player import EmbeddedExpandedPlayer from services.mpris import MprisPlayerManager brightness_service = Brightness.get_initial() @@ -47,6 +48,7 @@ class ModusControlCenter(Window): self._bluetooth_initialized = False self._music_initialized = False self._per_app_volume_initialized = False + self._expanded_player_initialized = False # Store references for cleanup self._signal_connections = [] @@ -54,6 +56,7 @@ class ModusControlCenter(Window): self._bluetooth_man = None self._music_widget_content = None self._per_app_volume_widget = None + self._expanded_player_widget = None self.add_keybinding("Escape", self.hide_controlcenter) @@ -132,6 +135,7 @@ class ModusControlCenter(Window): self.has_bluetooth_open = False self.has_wifi_open = False self.has_per_app_volume_open = False + self.has_expanded_player_open = False self.bluetooth_svg = Svg( name="bluetooth-icon", @@ -304,11 +308,13 @@ class ModusControlCenter(Window): self.bluetooth_widgets = None self.wifi_widgets = None self.per_app_volume_widgets = None + self.expanded_player_widgets = None self.center_box = CenterBox(start_children=[self.widgets]) self.bluetooth_center_box = None self.wifi_center_box = None self.per_app_volume_center_box = None + self.expanded_player_center_box = None self.widgets.set_size_request(300, -1) @@ -427,6 +433,23 @@ class ModusControlCenter(Window): self.per_app_volume_center_box = CenterBox(start_children=[self.per_app_volume_widgets]) self.per_app_volume_center_box.set_size_request(300, -1) + def _ensure_expanded_player_widgets(self): + """Lazy load expanded player widgets""" + if self.expanded_player_widgets is None: + if self._expanded_player_widget is None: + self._expanded_player_widget = EmbeddedExpandedPlayer(self) + + self.expanded_player_widgets = Box( + orientation="vertical", + h_expand=True, + name="control-center-widgets", + children=[ + self._expanded_player_widget, + ], + ) + self.expanded_player_center_box = CenterBox(start_children=[self.expanded_player_widgets]) + self.expanded_player_center_box.set_size_request(300, -1) + def set_dont_disturb(self, *_): self.focus_mode = not self.focus_mode modus_service.dont_disturb = self.focus_mode @@ -521,6 +544,18 @@ class ModusControlCenter(Window): idle_add(lambda *_: self.set_children(self.center_box)) self.has_per_app_volume_open = False + def open_expanded_player(self, *_): + self._ensure_expanded_player_widgets() + idle_add(lambda *_: self.set_children(self.expanded_player_center_box)) + self.has_expanded_player_open = True + # Refresh the player when opening + if self._expanded_player_widget: + self._expanded_player_widget.refresh() + + def close_expanded_player(self, *_): + idle_add(lambda *_: self.set_children(self.center_box)) + self.has_expanded_player_open = False + def _set_mousecapture(self, visible: bool): if visible: # Lazy load music widget when becoming visible @@ -531,6 +566,7 @@ class ModusControlCenter(Window): self.close_bluetooth() self.close_wifi() self.close_per_app_volume() + self.close_expanded_player() def volume_changed( self, @@ -633,5 +669,7 @@ class ModusControlCenter(Window): self._music_widget_content.destroy() if self._per_app_volume_widget: self._per_app_volume_widget.destroy() + if self._expanded_player_widget: + self._expanded_player_widget.destroy() super().destroy() diff --git a/modules/controlcenter/player.py b/modules/controlcenter/player.py index 7b27358..27b1164 100644 --- a/modules/controlcenter/player.py +++ b/modules/controlcenter/player.py @@ -12,7 +12,6 @@ from fabric.utils import ( from fabric.widgets.box import Box from fabric.widgets.button import Button from fabric.widgets.image import Image -from modules.controlcenter.expanded_player import ExpandedPlayer from fabric.widgets.label import Label from fabric.widgets.overlay import Overlay from fabric.widgets.stack import Stack @@ -21,7 +20,6 @@ from loguru import logger from services.mpris import MprisPlayer, MprisPlayerManager import config.data as data -from widgets.mousecapture import DropDownMouseCapture CACHE_DIR = f"{data.CACHE_DIR}/media" @@ -449,11 +447,6 @@ class PlayerBox(Box): self.control_center = control_center self.fallback_cover_path = f"{data.HOME_DIR}/.current.wall" - # Create expanded player and mouse capture with weak references to avoid circular refs - self.expanded_player = None - self.ex_mousecapture = None - self._create_expanded_player() - self.image_size = 50 self.icon_size = 15 @@ -658,34 +651,6 @@ class PlayerBox(Box): ) self._signal_connections.extend(connections) - def _create_expanded_player(self): - """Create expanded player components with proper cleanup handling.""" - try: - self.expanded_player = ExpandedPlayer() - self.ex_mousecapture = DropDownMouseCapture( - layer="top", child_window=self.expanded_player - ) - except Exception as e: - logger.warning(f"Failed to create expanded player: {e}") - self.expanded_player = None - self.ex_mousecapture = None - - def _on_outer_box_clicked(self, *_): - """Handle outer box click with proper error handling.""" - try: - # Close control center if it exists - if self.control_center and hasattr( - self.control_center, "hide_controlcenter" - ): - self.control_center.hide_controlcenter() - - # Open expanded player if available - if self.expanded_player and self.ex_mousecapture: - self.expanded_player.set_visible(True) - self.ex_mousecapture.toggle_mousecapture() - except Exception as e: - logger.warning(f"Failed to handle outer box click: {e}") - def destroy(self): """Clean up all resources when the widget is destroyed.""" # Cancel any ongoing downloads @@ -702,17 +667,6 @@ class PlayerBox(Box): # Clean up temp files self._cleanup_temp_files() - # Clean up UI components - try: - if self.ex_mousecapture: - self.ex_mousecapture.destroy() - self.ex_mousecapture = None - if self.expanded_player: - self.expanded_player.destroy() - self.expanded_player = None - except Exception as e: - logger.warning(f"Failed to cleanup UI components: {e}") - super().destroy() def __del__(self): @@ -723,21 +677,23 @@ class PlayerBox(Box): pass # Ignore errors during cleanup in destructor def _on_prev_button_click(self, *_): - """Handle prev button click: close control center then open expanded player""" + """Handle prev button click: open expanded player in control center""" try: - # Close control center first - if self.control_center and hasattr( - self.control_center, "hide_controlcenter" - ): - self.control_center.hide_controlcenter() - - # Then open expanded player - if self.expanded_player and self.ex_mousecapture: - self.expanded_player.set_visible(True) - self.ex_mousecapture.toggle_mousecapture() + # Open expanded player in control center instead of new window + if self.control_center and hasattr(self.control_center, "open_expanded_player"): + self.control_center.open_expanded_player() except Exception as e: logger.warning(f"Failed to handle prev button click: {e}") + def _on_outer_box_clicked(self, *_): + """Handle outer box click with proper error handling.""" + try: + # Open expanded player in control center instead of new window + if self.control_center and hasattr(self.control_center, "open_expanded_player"): + self.control_center.open_expanded_player() + except Exception as e: + logger.warning(f"Failed to handle outer box click: {e}") + def update_buttons(self, player_buttons, show_buttons): # """Update the stack switcher buttons in this player box""" pass @@ -908,6 +864,5 @@ class PlayerBox(Box): GLib.idle_add(self._update_image, local_arturl) def close_bluetooth(self, *args): - """Close Bluetooth control center""" - if self.ex_mousecapture: - self.ex_mousecapture.hide_child_window() + """Placeholder method for compatibility""" + pass diff --git a/styles/controlcenter.css b/styles/controlcenter.css index d259752..4631b10 100644 --- a/styles/controlcenter.css +++ b/styles/controlcenter.css @@ -496,3 +496,61 @@ color: #007aff; text-shadow: none; } + +/* Apple-style seek bar for expanded player */ +#apple-seek-bar { + background-color: transparent; + margin-top: 8px; + margin-bottom: 4px; + min-height: 20px; +} + +#apple-seek-bar trough { + min-width: 28px; + min-height: 3px; + border-radius: 1.5px; + background-color: alpha(#fff, 0.3); + border: none; + box-shadow: inset 0 1px 1px alpha(#000, 0.1); +} + +#apple-seek-bar highlight { + background: #007aff; + border-radius: 1.5px; + border: none; + box-shadow: 0 0 2px alpha(#007aff, 0.4); +} + +#apple-seek-bar slider { + background: #ffffff; + border: 1px solid alpha(#007aff, 0.8); + border-radius: 50%; + min-width: 12px; + min-height: 12px; + margin: -4.5px; + box-shadow: + 0 1px 3px alpha(#000, 0.2), + 0 0 0 1px alpha(#fff, 0.8); + transition: all 0.1s ease; +} + +#apple-seek-bar slider:hover { + background: #ffffff; + border-color: #007aff; + min-width: 14px; + min-height: 14px; + margin: -5.5px; + box-shadow: + 0 2px 6px alpha(#000, 0.25), + 0 0 0 2px alpha(#007aff, 0.3); +} + +#apple-seek-bar slider:active { + background: #f8f9fa; + min-width: 13px; + min-height: 13px; + margin: -5px; + box-shadow: + 0 1px 3px alpha(#000, 0.3), + 0 0 0 2px alpha(#007aff, 0.5); +} diff --git a/styles/player.css b/styles/player.css index f311fe4..82359eb 100644 --- a/styles/player.css +++ b/styles/player.css @@ -57,7 +57,7 @@ margin-left: 10px; } #player-info-box { - margin-left: 130px; + margin-left: 10px; } #player-title-c { @@ -129,13 +129,13 @@ #seek-bar slider { background-image: none; background-color: transparent; - padding: 2px; + padding: 0px; } #seek-bar scale { background-color: transparent; margin-top: 10px; - border-radius: 20px; + border-radius: 10px; } #seek-bar trough {