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:
2 files changed
+210
-113
No files matched your search
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in new issue
Block a user