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 @@
-
+
\ 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;