diff --git a/config/assets/icons/player/Pause.svg b/config/assets/icons/player/Pause.svg index e5a5c9a..f95a5b8 100644 --- a/config/assets/icons/player/Pause.svg +++ b/config/assets/icons/player/Pause.svg @@ -1,3 +1,7 @@ - - - + + + + Layer 1 + + + \ No newline at end of file diff --git a/modules/controlcenter/expanded_player.py b/modules/controlcenter/expanded_player.py index 52f48d4..178130c 100644 --- a/modules/controlcenter/expanded_player.py +++ b/modules/controlcenter/expanded_player.py @@ -18,6 +18,7 @@ from fabric.utils.helpers import get_relative_path from fabric.widgets.image import Image from fabric.widgets.overlay import Overlay from fabric.widgets.stack import Stack +from fabric.widgets.svg import Svg from gi.repository import GLib, GObject from loguru import logger @@ -159,7 +160,7 @@ class PlayerBoxStack(Box): self.mpris_manager = mpris_manager - # Track connections for cleanup + # Track connections for cleanup - store (object, handler_id) tuples connections = bulk_connect( self.mpris_manager, { @@ -167,7 +168,9 @@ class PlayerBoxStack(Box): "player-vanished": self.on_lost_player, }, ) - self._signal_connections.extend(connections) + # Store as (object, handler_id) tuples + for handler_id in connections: + self._signal_connections.append((self.mpris_manager, handler_id)) for player in self.mpris_manager.players: # type: ignore logger.info( @@ -178,9 +181,9 @@ class PlayerBoxStack(Box): def destroy(self): """Clean up resources when the widget is destroyed.""" # Disconnect all signal connections - for connection in self._signal_connections: + for obj, handler_id in self._signal_connections: try: - connection.disconnect() + obj.disconnect(handler_id) except Exception as e: logger.warning(f"Failed to disconnect signal: {e}") self._signal_connections.clear() @@ -688,21 +691,24 @@ class PlayerBox(Box): self.stack_buttons_box.hide() # Initially hidden # Create SVG icons from player directory - self.skip_next_icon = Image( - image_file=get_relative_path("../../config/assets/icons/player/fwd.svg"), - icon_size=16, + self.skip_next_icon = Svg( + name="btn", + style_classes=["control-buttons"], + svg_file=get_relative_path("../../config/assets/icons/player/fwd.svg"), ) - self.skip_prev_icon = Image( - image_file=get_relative_path("../../config/assets/icons/player/Rewind.svg"), - icon_size=16, + self.skip_prev_icon = Svg( + name="btn", + style_classes=["control-buttons"], + svg_file=get_relative_path("../../config/assets/icons/player/Rewind.svg"), ) - self.shuffle_icon = Image(icon_name="shuffle") # Keep existing shuffle icon - self.play_pause_icon = Image( - image_file=get_relative_path("../../config/assets/icons/player/Pause.svg"), - icon_size=20, + self.play_pause_icon = Svg( + name="btn", + style_classes=["control-buttons"], + svg_file=get_relative_path("../../config/assets/icons/player/Pause.svg"), ) self.play_pause_button = Button( + style_classes=["control-buttons"], name="macos-play-button", child=self.play_pause_icon, on_clicked=self.player.play_pause, @@ -711,6 +717,7 @@ class PlayerBox(Box): self.player.bind_property("can_pause", self.play_pause_button, "sensitive") self.next_button = Button( + style_classes=["control-buttons"], name="macos-control-button", child=self.skip_next_icon, on_clicked=self._on_player_next, @@ -720,6 +727,7 @@ class PlayerBox(Box): self.prev_button = Button( name="macos-control-button", child=self.skip_prev_icon, + style_classes=["control-buttons"], on_clicked=self._on_player_prev, ) self.button_box.children = ( @@ -762,7 +770,7 @@ class PlayerBox(Box): self.children = [*self.children, self.outer_box] - # Track signal connections for cleanup + # Track signal connections for cleanup - store (object, handler_id) tuples connections = bulk_connect( self.player, { @@ -772,10 +780,15 @@ class PlayerBox(Box): "notify::metadata": self._on_metadata, }, ) - self._signal_connections.extend(connections) + # Store as (object, handler_id) tuples + for handler_id in connections: + self._signal_connections.append((self.player, handler_id)) def destroy(self): """Clean up all resources when the widget is destroyed.""" + # Set exit flag FIRST to stop any running timers + self.exit = True + # Cancel any ongoing downloads immediately self._download_cancelled = True @@ -787,9 +800,9 @@ class PlayerBox(Box): pass # Disconnect all signal connections - for connection in self._signal_connections: + for obj, handler_id in self._signal_connections: try: - connection.disconnect() + obj.disconnect(handler_id) except Exception as e: logger.warning(f"Failed to disconnect signal: {e}") self._signal_connections.clear() @@ -1047,15 +1060,29 @@ class PlayerBox(Box): 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 - position = self.player.position - self.position_label.set_label(self.length_str(position)) + # Additional safety checks to prevent GTK errors + if not hasattr(self, "seek_bar") or self.seek_bar is None: + return False # Stop the timer - # 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) + try: + # Check if the seek bar widget is still valid + if not self.seek_bar.get_realized(): + return False # Widget is destroyed, stop timer + + 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) + + except Exception as e: + # If any error occurs (widget destroyed, etc), stop the timer + logger.warning(f"Seek bar update failed, stopping timer: {e}") + return False return True @@ -1072,26 +1099,30 @@ class PlayerBox(Box): def _on_scale_value_changed(self, scale: Scale): """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: + 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)) self.player.position = new_position self.position_label.set_label(self.length_str(new_position)) - except Exception: + 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)) + try: + self.position_label.set_label(self.length_str(new_position)) + except Exception: + logger.warning(f"Failed to update position label: {e}") @cooldown(0.1) def _on_scale_move(self, scale: Scale, event, pos: int): - self.player.position = pos - self.position_label.set_label(self.length_str(pos)) - self.seek_bar.set_value(pos) + try: + if not self.exit and self.player: + self.player.position = pos + self.position_label.set_label(self.length_str(pos)) + self.seek_bar.set_value(pos) + except Exception as e: + logger.warning(f"Failed to update seek position: {e}") class Thing(Box): diff --git a/modules/controlcenter/main.py b/modules/controlcenter/main.py index f37f26e..5033846 100644 --- a/modules/controlcenter/main.py +++ b/modules/controlcenter/main.py @@ -288,16 +288,26 @@ class ModusControlCenter(Window): Box( orientation="horizontal", spacing=8, - v_expand=False, + v_expand=True, + h_expand=True, children=[ self.volume_scale, Button( name="per-app-volume-button", - child=Svg( - svg_file=get_relative_path( - "../../config/assets/icons/player/audio-switcher.svg" - ), - name="per-app-volume-icon", + child=Box( + h_align="center", + v_align="center", + h_expand=True, + size=(32, 32), + v_expand=True, + children=[ + Svg( + svg_file=get_relative_path( + "../../config/assets/icons/player/audio-switcher.svg" + ), + size=34, + ) + ], ), on_clicked=self.open_per_app_volume, ), diff --git a/modules/controlcenter/player.py b/modules/controlcenter/player.py index 0ee0b86..7b8f068 100644 --- a/modules/controlcenter/player.py +++ b/modules/controlcenter/player.py @@ -16,6 +16,7 @@ from fabric.widgets.image import Image from fabric.widgets.label import Label from fabric.widgets.overlay import Overlay from fabric.widgets.stack import Stack +from fabric.widgets.svg import Svg from gi.repository import GLib, GObject from loguru import logger @@ -84,7 +85,7 @@ class PlayerBoxStack(Box): self.mpris_manager = mpris_manager - # Track connections for cleanup + # Track connections for cleanup - store (object, handler_id) tuples connections = bulk_connect( self.mpris_manager, { @@ -92,7 +93,9 @@ class PlayerBoxStack(Box): "player-vanished": self.on_lost_player, }, ) - self._signal_connections.extend(connections) + # Store as (object, handler_id) tuples + for handler_id in connections: + self._signal_connections.append((self.mpris_manager, handler_id)) for player in self.mpris_manager.players: # type: ignore logger.info( @@ -103,9 +106,9 @@ class PlayerBoxStack(Box): def destroy(self): """Clean up resources when the widget is destroyed.""" # Disconnect all signal connections - for connection in self._signal_connections: + for obj, handler_id in self._signal_connections: try: - connection.disconnect() + obj.disconnect(handler_id) except Exception as e: logger.warning(f"Failed to disconnect signal: {e}") self._signal_connections.clear() @@ -567,18 +570,15 @@ class PlayerBox(Box): ) # Create SVG icons with consistent sizing - self.skip_next_icon = Image( - image_file=get_relative_path("../../config/assets/icons/player/fwd.svg"), - icon_size=16 + self.skip_next_icon = Svg( + name="control-buttons", + size=(28, 28), + svg_file=get_relative_path("../../config/assets/icons/player/fwd.svg"), ) - self.skip_prev_icon = Image( - image_file=get_relative_path("../../config/assets/icons/player/Rewind.svg"), - icon_size=16 - ) - self.shuffle_icon = Image(icon_name="shuffle", icon_size=16) - self.play_pause_icon = Image( - image_file=get_relative_path("../../config/assets/icons/player/Pause.svg"), - icon_size=18 + self.play_pause_icon = Svg( + name="control-buttons", + size=(28, 28), + svg_file=get_relative_path("../../config/assets/icons/player/Pause.svg"), ) # Fixed size buttons to prevent layout shifts @@ -588,39 +588,19 @@ class PlayerBox(Box): on_clicked=self.player.play_pause, ) # Set consistent button size - self.play_pause_button.set_size_request(32, 32) self.player.bind_property("can_pause", self.play_pause_button, "sensitive") self.next_button = Button( - style_classes=["player-button"], + name="player-button", child=self.skip_next_icon, on_clicked=self._on_player_next, ) # Set consistent button size - self.next_button.set_size_request(32, 32) + # self.next_button.set_size_request(32, 32) self.player.bind_property("can_go_next", self.next_button, "sensitive") - self.prev_button = Button( - style_classes=["player-button"], - child=self.skip_prev_icon, - on_clicked=self._on_prev_button_click, - ) - # Set consistent button size - self.prev_button.set_size_request(32, 32) - - self.shuffle_button = Button( - style_classes=["player-button"], - child=self.shuffle_icon, - on_clicked=self.player.toggle_shuffle, - ) - # Set consistent button size - self.shuffle_button.set_size_request(32, 32) - self.player.bind_property("can_shuffle", self.shuffle_button, "sensitive") - self.button_box.children = ( - # self.shuffle_button, - # self.prev_button, self.play_pause_button, self.next_button, ) @@ -682,7 +662,7 @@ class PlayerBox(Box): self.box, ] - # Track signal connections for cleanup + # Track signal connections for cleanup - store (object, handler_id) tuples connections = bulk_connect( self.player, { @@ -692,7 +672,9 @@ class PlayerBox(Box): "notify::metadata": self._on_metadata, }, ) - self._signal_connections.extend(connections) + # Store as (object, handler_id) tuples + for handler_id in connections: + self._signal_connections.append((self.player, handler_id)) def destroy(self): """Clean up all resources when the widget is destroyed.""" @@ -700,9 +682,9 @@ class PlayerBox(Box): self._download_cancelled = True # Disconnect all signal connections - for connection in self._signal_connections: + for obj, handler_id in self._signal_connections: try: - connection.disconnect() + obj.disconnect(handler_id) except Exception as e: logger.warning(f"Failed to disconnect signal: {e}") self._signal_connections.clear() @@ -782,24 +764,6 @@ class PlayerBox(Box): self.shuffle_icon.style_classes = [] self.shuffle_icon.add_style_class("shuffle-off") - def play_pause(self, player, status): - status = player.get_property("playback-status") - - if status == "paused": - self.play_pause_icon.set_from_file( - get_relative_path("../../config/assets/icons/player/play.svg") - ) - - if status == "playing": - self.play_pause_icon.set_from_file( - get_relative_path("../../config/assets/icons/player/Pause.svg") - ) - # Notify the player stack that this player started playing - if self.player_stack and hasattr( - self.player_stack, "on_player_playback_changed" - ): - self.player_stack.on_player_playback_changed(self, status) - def _on_playback_change(self, player, status): status = player.get_property("playback-status") diff --git a/styles/controlcenter.css b/styles/controlcenter.css index 465f584..fa23627 100644 --- a/styles/controlcenter.css +++ b/styles/controlcenter.css @@ -474,9 +474,8 @@ /* Circle button for per-app volume */ #per-app-volume-button { background-color: alpha(#fff, 0.15); - border: 2px solid alpha(#999, 0.6); + border: 1px solid alpha(#999, 0.6); border-radius: 50%; - min-width: 30px; } #per-app-volume-button:hover { @@ -678,6 +677,7 @@ border: none; border-radius: 50%; min-width: 28px; /* Slightly smaller */ + opacity: 0.7; min-height: 28px; padding: 5px; /* Reduced padding */ color: #ffffff; @@ -685,8 +685,7 @@ } #macos-control-button:hover { - background: alpha(#fff, 0.1); - box-shadow: 0 0 0 1px alpha(#fff, 0.2); + opacity: 1; } #macos-play-button { @@ -694,6 +693,7 @@ border: none; border-radius: 50%; min-width: 36px; /* Slightly smaller */ + opacity: 0.7; min-height: 36px; padding: 7px; /* Reduced padding */ color: #ffffff; @@ -701,8 +701,7 @@ } #macos-play-button:hover { - background: alpha(#fff, 0.15); - box-shadow: 0 0 0 1px alpha(#fff, 0.25); + opacity: 1; } /* macOS player switcher dots - compact horizontal layout */ diff --git a/styles/player.css b/styles/player.css index a857430..79f9023 100644 --- a/styles/player.css +++ b/styles/player.css @@ -103,20 +103,23 @@ } .player-button { - min-width: 20px; - min-height: 20px; padding: 1px; - border-radius: 100%; -} - -.player-button:hover { - box-shadow: inset rgba(255, 255, 255, 0.5) 0 0 10px; } #player-box #player-controls #button-box .player-button:disabled { color: #5f5f5f; } +#player-button { + opacity: 0.5; +} +#player-button:hover { + opacity: 0.9; +} + +#btn svg { + margin: 0 2px; +} .album-image-c { min-width: 50px; min-height: 50px;