feat(src/window/controlcenter): improve playback state handling and UI for multiple players

Add _on_map handler and connect "map" signal to update playing state.
Refactor button handling to use change-value signal and dynamic child management.
Update playback status notification to avoid duplicates and use current_status variable.
This commit is contained in:
sankalp committed 2026-07-03 22:59:45 +05:45
1 parent ce0746eeb2
commit f381766929
2 files changed
+210 -113

No files matched your search

+98 -69
View File
@@ -45,6 +45,9 @@ class EmbeddedExpandedPlayer(Box):
if hasattr(child, "resume"):
child.resume()
def _on_map(self, *_):
self._check_and_update_playing_state()
def destroy(self):
if hasattr(self, "player_content") and hasattr(self.player_content, "destroy"):
self.player_content.destroy()
@@ -64,6 +67,7 @@ class PlayerBoxStack(Box):
self.player_stack.children = [self.no_media_box]
self.set_visible(True)
self.mpris_manager = mpris_manager
self.connect("map", self._on_map)
connections = bulk_connect(
self.mpris_manager,
@@ -75,6 +79,9 @@ class PlayerBoxStack(Box):
for name, player in self.mpris_manager.get_all_services().items():
self.on_new_player(self.mpris_manager, name, player)
def _on_map(self, *_):
self._check_and_update_playing_state()
def destroy(self):
for obj, handler_id in self._signal_connections:
try:
@@ -321,10 +328,7 @@ class PlayerBox(Box):
self.seek_bar.connect("value-changed", self._on_scale_value_changed)
)
self._seekbar_signal_ids.append(
self.seek_bar.connect("button-press-event", self._on_seek_start)
)
self._seekbar_signal_ids.append(
self.seek_bar.connect("button-release-event", self._on_seek_end)
self.seek_bar.connect("change-value", self._on_change_value)
)
self._property_bindings.append(
self.player.bind_property("can_seek", self.seek_bar, "sensitive")
@@ -538,23 +542,31 @@ class PlayerBox(Box):
return False
def update_buttons(self, player_buttons, show_buttons):
self.stack_buttons_box.children = []
if show_buttons and len(player_buttons) > 1:
for i, button in enumerate(player_buttons):
dot_button = Button(
name="macos-player-switcher-dot",
style_classes=["macos-switcher-dot"],
)
if button.get_style_context().has_class("active"):
if len(self.stack_buttons_box.get_children()) != len(player_buttons):
self.stack_buttons_box.children = []
for i, button in enumerate(player_buttons):
dot_button = Button(
name="macos-player-switcher-dot",
style_classes=["macos-switcher-dot"],
)
dot_button.connect(
"clicked",
lambda *_, idx=i: self.player_stack.on_player_clicked_by_index(
idx
),
)
self.stack_buttons_box.children = [
*self.stack_buttons_box.children,
dot_button,
]
for i, dot_button in enumerate(self.stack_buttons_box.get_children()):
if player_buttons[i].get_style_context().has_class("active"):
dot_button.add_style_class("active")
dot_button.connect(
"clicked",
lambda *_, idx=i: self.player_stack.on_player_clicked_by_index(idx),
)
self.stack_buttons_box.children = [
*self.stack_buttons_box.children,
dot_button,
]
else:
dot_button.remove_style_class("active")
self.stack_buttons_box.show_all()
else:
self.stack_buttons_box.hide()
@@ -592,7 +604,7 @@ class PlayerBox(Box):
if "mpris:length" in keys:
duration = int(metadata["mpris:length"])
self.length_label.set_label(self.length_str(duration))
self.seek_bar.set_range(0, min(2147483647, duration))
self.seek_bar.set_range(0, duration / 1_000_000)
self.position_label.set_label(self.length_str(self.player.position or 0))
else:
self.track_title.set_label(self.player.title or "No Title")
@@ -608,6 +620,9 @@ class PlayerBox(Box):
def resume(self):
pass
def _on_map(self, *_):
self._check_and_update_playing_state()
def destroy(self):
self.exit = True
self.suspend()
@@ -638,15 +653,28 @@ class PlayerBox(Box):
except Exception as e:
logger.error(f"An error occurred: {e}")
def _on_playback_change(self, *_):
if self.exit or self.player is None:
return
status = str(self.player.playback_status).lower()
self.play_pause_icon.dynamic_file(
"player/play.svg" if status == "paused" else "player/Pause.svg"
)
if self.player_stack:
self.player_stack.on_player_playback_changed(self, status)
def _on_playback_change(self, service, *args):
if not self.exit:
current_status = str(self.player.playback_status).lower()
if getattr(self, "_last_notified_status", None) == current_status:
return
self._last_notified_status = current_status
icon_file = (
"player/play.svg" if current_status == "paused" else "player/Pause.svg"
)
if hasattr(self, "play_pause_icon"):
self.play_pause_icon.dynamic_file(icon_file)
else:
# Fallback if play_pause_icon isn't available
from utils.utils import get_relative_path
import os
self.play_pause_button.get_child().set_from_file(
os.path.join(get_relative_path("assets/icons/"), icon_file)
)
self.player_stack.on_player_playback_changed(self, current_status)
def _on_artwork_change(self, service, local_path, *_):
if self.exit:
@@ -680,36 +708,47 @@ class PlayerBox(Box):
if self.seek_bar.get_realized() and self.seek_bar.get_adjustment():
adj = self.seek_bar.get_adjustment()
if duration_micros > 0:
self.seek_bar.set_range(0, min(2147483647, duration_micros))
self.seek_bar.set_range(0, duration_micros / 1_000_000)
if not self._user_seeking and adj.get_upper() > 0:
self.seek_bar.set_value(min(2147483647, position_micros))
self.seek_bar.set_value(position_micros / 1_000_000)
except Exception as e:
logger.error(f"[_on_track_position] Error: {e}")
def _on_seek_start(self, widget, event):
self._user_seeking = True
def _clear_seeking(self):
self._user_seeking = False
return False
def _on_seek_end(self, widget, event):
self._user_seeking = False
def _do_seek(self, value):
self._debounce_seek = None
if self.player and not self.exit:
try:
self.player.seek_position(value)
except Exception:
pass
if hasattr(self, "_seek_timeout") and self._seek_timeout:
from fabric.utils import GLib
GLib.source_remove(self._seek_timeout)
from fabric.utils import GLib
self._seek_timeout = GLib.timeout_add(1000, self._clear_seeking)
return False
def _on_change_value(self, scale, scroll, value):
if not self.player or self.exit:
return False
try:
adj = widget.get_adjustment()
upper = adj.get_upper()
value = max(0, int(widget.get_value()))
duration = self._cached_duration or self.player.length or 0
if duration <= 0 and upper > 100:
duration = int(upper)
if upper > 100:
new_position = value
elif duration > 0 and upper > 0:
new_position = int(value / upper * duration)
else:
new_position = value
self.player.set_position(new_position / 1_000_000)
except Exception as e:
logger.error(f"An error occurred: {e}")
self._user_seeking = True
value = max(0.0, value)
self.position_label.set_label(self.length_str(int(value * 1_000_000)))
if hasattr(self, "_debounce_seek") and self._debounce_seek:
from fabric.utils import GLib
GLib.source_remove(self._debounce_seek)
from fabric.utils import GLib
self._debounce_seek = GLib.timeout_add(100, self._do_seek, value)
return False
def _update_duration_from_metadata(self):
@@ -717,10 +756,10 @@ class PlayerBox(Box):
duration = self._cached_duration or self.player.length or 0
if duration <= 0 and self.seek_bar.get_realized():
adj = self.seek_bar.get_adjustment()
if adj.get_upper() > 100:
duration = int(adj.get_upper())
if adj.get_upper() > 1:
duration = int(adj.get_upper() * 1_000_000)
if duration > 0:
self.seek_bar.set_range(0, min(2147483647, duration))
self.seek_bar.set_range(0, duration / 1_000_000)
self.length_label.set_label(self.length_str(duration))
return True
except Exception:
@@ -741,21 +780,8 @@ class PlayerBox(Box):
if not self.player or self.exit:
return
try:
adj = scale.get_adjustment()
upper = adj.get_upper()
value = max(0, int(scale.get_value()))
duration = self._cached_duration or self.player.length or 0
if duration <= 0 and upper > 100:
duration = int(upper)
if upper > 100:
new_position = value
elif duration > 0 and upper > 0:
new_position = int(value / upper * duration)
else:
new_position = value
self.position_label.set_label(self.length_str(new_position))
value = max(0.0, scale.get_value())
self.position_label.set_label(self.length_str(int(value * 1_000_000)))
except Exception as e:
logger.error(f"An error occurred: {e}")
@@ -773,6 +799,9 @@ class ExpandedPlayer(Window):
)
self.add_keybinding("Escape", lambda *_: self.set_visible(False))
def _on_map(self, *_):
self._check_and_update_playing_state()
def destroy(self):
if hasattr(self, "child") and hasattr(self.child, "destroy"):
self.child.destroy()
+112 -44
View File
@@ -76,6 +76,7 @@ class PlayerBoxStack(Box):
self.player_stack.children = [self.no_media_box]
self.set_visible(True)
self.mpris_manager = mpris_manager or get_shared_mpris_manager()
self.connect("map", self._on_map)
if self.mpris_manager is not None:
self._init_mpris_manager()
@@ -97,6 +98,9 @@ class PlayerBoxStack(Box):
except Exception as e:
logger.error(f"Failed to initialize PlayerBoxStack signals: {e}")
def _on_map(self, *_):
self._check_and_update_playing_state()
def destroy(self):
for obj, handler_id in self._signal_connections:
try:
@@ -211,6 +215,12 @@ class PlayerBoxStack(Box):
elif status in ["paused", "stopped"]:
self._check_and_update_playing_state()
def _update_all_player_buttons(self):
show_buttons = len(self.player_buttons) > 1
for child in self.player_stack.get_children():
if hasattr(child, "update_buttons"):
child.update_buttons(self.player_buttons, show_buttons)
def on_player_clicked_by_index(self, index):
if 0 <= index < len(self.player_buttons):
if self.current_stack_pos < len(self.player_buttons):
@@ -220,6 +230,7 @@ class PlayerBoxStack(Box):
self.player_stack.set_visible_child(
self.player_stack.get_children()[self.current_stack_pos]
)
self._update_all_player_buttons()
def on_new_player(self, mpris_manager, name, player):
if (
@@ -237,6 +248,7 @@ class PlayerBoxStack(Box):
if self.player_buttons:
self.player_buttons[self.current_stack_pos].set_style_classes(["active"])
self._check_and_update_playing_state()
self._update_all_player_buttons()
def on_lost_player(self, mpris_manager, bus_name):
player_box_to_remove = None
@@ -268,6 +280,7 @@ class PlayerBoxStack(Box):
self.player_stack.set_visible_child(
self.player_stack.get_children()[self.current_stack_pos]
)
self._update_all_player_buttons()
def make_new_player_button(self, player_box):
new_button = Button(name="player-stack-button")
@@ -358,56 +371,73 @@ class PlayerBox(Box):
on_clicked=self.player.next,
)
self.stack_buttons_box = Box(
h_expand=False,
v_expand=False,
name="macos-stack-buttons-box",
spacing=4,
orientation="h",
h_align="center",
v_align="end",
)
self.stack_buttons_box.hide()
self.children = [
CenterBox(
name="box-c",
orientation="h",
h_align="center",
start_children=[
Button(
spacing=5,
name="outer-player-box-c",
h_expand=True,
on_clicked=self._on_outer_box_clicked,
h_align="start",
child=Box(
name="inner-player-box",
h_expand=True,
v_align="center",
h_align="start",
children=[
self.image,
Box(
name="player-info-box-c",
v_align="center",
Box(
orientation="v",
children=[
CenterBox(
name="box-c",
orientation="h",
h_align="center",
start_children=[
Button(
spacing=5,
name="outer-player-box-c",
h_expand=True,
on_clicked=self._on_outer_box_clicked,
h_align="start",
child=Box(
name="inner-player-box",
h_expand=True,
v_align="center",
h_align="start",
orientation="v",
children=[
self.image,
Box(
name="track-info",
spacing=5,
orientation="v",
v_align="start",
name="player-info-box-c",
v_align="center",
h_expand=True,
h_align="start",
orientation="v",
children=[
self.track_title,
self.track_artist,
Box(
name="track-info",
spacing=5,
orientation="v",
v_align="start",
h_align="start",
children=[
self.track_title,
self.track_artist,
],
)
],
)
),
],
),
],
),
)
],
end_children=[
Box(
name="button-box-c",
h_expand=False,
spacing=2,
children=[self.play_pause_button, self.next_button],
)
)
],
end_children=[
Box(
name="button-box-c",
h_expand=False,
spacing=2,
children=[self.play_pause_button, self.next_button],
)
],
),
self.stack_buttons_box,
],
)
]
@@ -444,6 +474,36 @@ class PlayerBox(Box):
except Exception:
pass
def update_buttons(self, player_buttons, show_buttons):
if show_buttons and len(player_buttons) > 1:
if len(self.stack_buttons_box.get_children()) != len(player_buttons):
self.stack_buttons_box.children = []
for i, button in enumerate(player_buttons):
dot_button = Button(
name="macos-player-switcher-dot",
style_classes=["macos-switcher-dot"],
)
dot_button.connect(
"clicked",
lambda *_, idx=i: self.player_stack.on_player_clicked_by_index(
idx
),
)
self.stack_buttons_box.children = [
*self.stack_buttons_box.children,
dot_button,
]
for i, dot_button in enumerate(self.stack_buttons_box.get_children()):
if player_buttons[i].get_style_context().has_class("active"):
dot_button.add_style_class("active")
else:
dot_button.remove_style_class("active")
self.stack_buttons_box.show_all()
else:
self.stack_buttons_box.hide()
def _on_outer_box_clicked(self, *_):
if self.control_center and hasattr(self.control_center, "open_expanded_player"):
self.control_center.open_expanded_player()
@@ -481,15 +541,20 @@ class PlayerBox(Box):
except Exception as e:
logger.error(f"An error occurred: {e}")
def _on_playback_change(self, *_):
def _on_playback_change(self, service, *args):
if self.exit or self.player is None:
return
status = str(self.player.playback_status).lower()
current_status = str(self.player.playback_status).lower()
if getattr(self, "_last_notified_status", None) == current_status:
return
self._last_notified_status = current_status
self.play_pause_icon.dynamic_file(
"player/play.svg" if status == "paused" else "player/Pause.svg"
"player/play.svg" if current_status == "paused" else "player/Pause.svg"
)
if self.player_stack:
self.player_stack.on_player_playback_changed(self, status)
self.player_stack.on_player_playback_changed(self, current_status)
def set_image(self, service=None, path=None, *_):
if self.exit or self.player is None:
@@ -504,6 +569,9 @@ class PlayerBox(Box):
def resume(self):
self.exit = False
def _on_map(self, *_):
self._check_and_update_playing_state()
def destroy(self):
self.exit = True
for obj, handler_id in self._signal_connections: