✨ feat(player): improve player metadata handling and thumbnail loading with queue system
This commit is contained in:
2 files changed
+238
-375
No files matched your search
+223
-348
@@ -1,19 +1,19 @@
|
||||
import urllib.parse
|
||||
from urllib.parse import urlparse
|
||||
import re
|
||||
|
||||
from gi.repository import GLib
|
||||
from gi.repository import Playerctl, GLib
|
||||
from fabric.core.service import Signal
|
||||
from fabric.widgets.box import Box
|
||||
from fabric.widgets.button import Button
|
||||
from fabric.widgets.centerbox import CenterBox
|
||||
from fabric.widgets.label import Label
|
||||
from fabric.widgets.stack import Stack
|
||||
from fabric.widgets.eventbox import EventBox
|
||||
from fabric.widgets.image import Image
|
||||
from fabric.widgets.svg import Svg
|
||||
from loguru import logger
|
||||
|
||||
import config.data as data
|
||||
from fabric.utils import get_relative_path
|
||||
from fabric.widgets.box import Box
|
||||
from fabric.widgets.button import Button
|
||||
from fabric.widgets.centerbox import CenterBox
|
||||
from fabric.widgets.eventbox import EventBox
|
||||
from fabric.widgets.image import Image
|
||||
from fabric.widgets.label import Label
|
||||
from fabric.widgets.stack import Stack
|
||||
from fabric.widgets.svg import Svg
|
||||
from services.mpris import PlayerManager, PlayerService
|
||||
|
||||
|
||||
@@ -22,13 +22,14 @@ class Player(Box):
|
||||
super().__init__(style_classes="player-box", **kwargs)
|
||||
|
||||
self._player = PlayerService(player=player)
|
||||
self._current_thumbnail = None # Cache for current thumbnail
|
||||
self._update_queue = []
|
||||
self._is_updating = False
|
||||
self._update_timeout_id = None
|
||||
|
||||
self.duration = 0.0
|
||||
|
||||
self._player.connect("pause", self.on_pause)
|
||||
self._player.connect("play", self.on_play)
|
||||
self._player.connect("meta-change", self.on_metadata)
|
||||
self._player.connect("shuffle-toggle", self.on_shuffle)
|
||||
self._player.connect("track-position", self.on_update_track_position)
|
||||
|
||||
player.connect("metadata", self.on_direct_metadata_change)
|
||||
@@ -46,7 +47,7 @@ class Player(Box):
|
||||
justification="left",
|
||||
h_align="start",
|
||||
ellipsization="end",
|
||||
max_chars_width=15,
|
||||
max_chars_width=22,
|
||||
)
|
||||
self.artist = Label(
|
||||
name="artist",
|
||||
@@ -87,6 +88,7 @@ class Player(Box):
|
||||
f"background-image:url('{data.HOME_DIR}/.current.wall')"
|
||||
)
|
||||
|
||||
|
||||
self.children = [
|
||||
self.album_cover,
|
||||
Box(
|
||||
@@ -199,16 +201,53 @@ class Player(Box):
|
||||
return
|
||||
self.duration = dur
|
||||
|
||||
|
||||
|
||||
|
||||
def on_seek(self, sender, ratio):
|
||||
pos = ratio * self.duration # duration in seconds
|
||||
print(f"Seeking to {pos:.2f}s")
|
||||
self._player.set_position(int(pos))
|
||||
|
||||
def skip_forward(self, seconds=10):
|
||||
self._player._player.seek(seconds * 1000000)
|
||||
def on_metadata(self, sender, metadata, player):
|
||||
def _update_metadata():
|
||||
keys = metadata.keys()
|
||||
if "xesam:artist" in keys and "xesam:title" in keys:
|
||||
song_title = metadata["xesam:title"]
|
||||
self.song.set_label(song_title)
|
||||
|
||||
def skip_backward(self, seconds=10):
|
||||
self._player._player.seek(-1 * seconds * 1000000)
|
||||
artist_list = metadata["xesam:artist"]
|
||||
artist_name = artist_list[0] if artist_list else "Unknown Artist"
|
||||
self.artist.set_label(artist_name)
|
||||
|
||||
self._handle_thumbnail(metadata, player)
|
||||
|
||||
# Update play/pause state
|
||||
if (
|
||||
player.props.playback_status.value_name
|
||||
== "PLAYERCTL_PLAYBACK_STATUS_PLAYING"
|
||||
):
|
||||
self._update_play_pause_icon(True)
|
||||
else:
|
||||
self._update_play_pause_icon(False)
|
||||
|
||||
# Update shuffle state
|
||||
if self._is_shuffle_supported(player):
|
||||
if player.props.shuffle == True:
|
||||
self._update_shuffle_icon(True)
|
||||
else:
|
||||
self._update_shuffle_icon(False)
|
||||
else:
|
||||
self.shuffle_button.set_visible(False)
|
||||
|
||||
self._queue_update(_update_metadata)
|
||||
|
||||
def cleanup(self):
|
||||
"""Clean up resources to prevent memory leaks."""
|
||||
if self._update_timeout_id:
|
||||
GLib.source_remove(self._update_timeout_id)
|
||||
self._update_timeout_id = None
|
||||
self._update_queue.clear()
|
||||
self._is_updating = False
|
||||
|
||||
def _is_shuffle_supported(self, player):
|
||||
try:
|
||||
@@ -217,300 +256,152 @@ class Player(Box):
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def on_metadata(self, sender, metadata, player):
|
||||
keys = metadata.keys()
|
||||
if "xesam:artist" in keys and "xesam:title" in keys:
|
||||
song_title = metadata["xesam:title"]
|
||||
self.song.set_label(song_title)
|
||||
def _queue_update(self, update_func, *args, **kwargs):
|
||||
"""Add an update function to the queue and process if not already running."""
|
||||
self._update_queue.append((update_func, args, kwargs))
|
||||
if not self._is_updating:
|
||||
self._process_queue()
|
||||
|
||||
artist_list = metadata["xesam:artist"]
|
||||
artist_name = artist_list[0] if artist_list else "Unknown Artist"
|
||||
self.artist.set_label(artist_name)
|
||||
def _process_queue(self):
|
||||
"""Process the update queue sequentially."""
|
||||
if not self._update_queue or self._is_updating:
|
||||
return
|
||||
|
||||
self._handle_thumbnail(metadata, player)
|
||||
self._is_updating = True
|
||||
update_func, args, kwargs = self._update_queue.pop(0)
|
||||
|
||||
try:
|
||||
update_func(*args, **kwargs)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error in queue update: {e}")
|
||||
finally:
|
||||
# Schedule next update after a short delay
|
||||
if self._update_timeout_id:
|
||||
GLib.source_remove(self._update_timeout_id)
|
||||
self._update_timeout_id = GLib.timeout_add(100, self._finish_update)
|
||||
|
||||
if (
|
||||
player.props.playback_status.value_name
|
||||
== "PLAYERCTL_PLAYBACK_STATUS_PLAYING"
|
||||
):
|
||||
self.on_play(self._player)
|
||||
|
||||
if self._is_shuffle_supported(player):
|
||||
if player.props.shuffle == True:
|
||||
self._update_shuffle_icon(True)
|
||||
else:
|
||||
self._update_shuffle_icon(False)
|
||||
else:
|
||||
self.shuffle_button.set_visible(False)
|
||||
def _finish_update(self):
|
||||
"""Finish current update and process next item in queue."""
|
||||
self._is_updating = False
|
||||
self._update_timeout_id = None
|
||||
|
||||
if self._update_queue:
|
||||
self._process_queue()
|
||||
return False
|
||||
|
||||
def _handle_thumbnail(self, metadata, player):
|
||||
keys = metadata.keys()
|
||||
player_name = player.props.player_name
|
||||
|
||||
browser_players = ["firefox", "chromium", "vivaldi", "brave"]
|
||||
is_browser = player_name in browser_players
|
||||
|
||||
thumbnail_url = None
|
||||
|
||||
if "mpris:artUrl" in keys:
|
||||
art_url = metadata["mpris:artUrl"]
|
||||
if art_url and art_url.strip():
|
||||
if art_url.startswith("data:image/"):
|
||||
thumbnail_url = art_url
|
||||
elif art_url.startswith("http"):
|
||||
thumbnail_url = art_url
|
||||
elif art_url.startswith("file://"):
|
||||
thumbnail_url = art_url
|
||||
|
||||
if is_browser and not thumbnail_url and "xesam:url" in keys:
|
||||
webpage_url = metadata["xesam:url"]
|
||||
if webpage_url and webpage_url.startswith("http"):
|
||||
# Try to extract actual media thumbnail from the webpage
|
||||
thumbnail_url = self._extract_media_thumbnail(
|
||||
webpage_url, metadata, player_name
|
||||
)
|
||||
|
||||
if is_browser and not thumbnail_url:
|
||||
thumbnail_url = self._extract_from_browser_metadata(metadata, player_name)
|
||||
|
||||
if thumbnail_url:
|
||||
logger.debug(f"Setting media thumbnail for {player_name}: {thumbnail_url}")
|
||||
self.album_cover.set_style(f"background-image:url('{thumbnail_url}')")
|
||||
else:
|
||||
logger.debug(
|
||||
f"No media thumbnail found for {player_name}, using default wallpaper"
|
||||
)
|
||||
self.album_cover.set_style(
|
||||
f"background-image:url('{data.HOME_DIR}/.current.wall')"
|
||||
)
|
||||
|
||||
def _extract_media_thumbnail(self, webpage_url, metadata, player_name):
|
||||
try:
|
||||
parsed_url = urlparse(webpage_url)
|
||||
domain = parsed_url.netloc
|
||||
path = parsed_url.path
|
||||
|
||||
logger.debug(
|
||||
f"Extracting media thumbnail for {player_name} from: {webpage_url}"
|
||||
)
|
||||
|
||||
if "youtube.com" in domain or "youtu.be" in domain:
|
||||
video_id = self._extract_youtube_video_id(webpage_url)
|
||||
if video_id:
|
||||
logger.debug(f"Extracted YouTube video ID: {video_id}")
|
||||
return f"https://img.youtube.com/vi/{video_id}/maxresdefault.jpg"
|
||||
|
||||
elif "vimeo.com" in domain:
|
||||
video_id = self._extract_vimeo_video_id(webpage_url)
|
||||
if video_id:
|
||||
logger.debug(f"Extracted Vimeo video ID: {video_id}")
|
||||
return f"https://vumbnail.com/{video_id}.jpg"
|
||||
|
||||
elif "twitch.tv" in domain:
|
||||
channel_name = self._extract_twitch_channel(webpage_url)
|
||||
if channel_name:
|
||||
logger.debug(f"Extracted Twitch channel: {channel_name}")
|
||||
return f"https://static-cdn.jtvnw.net/previews-ttv/live_user_{channel_name}-1280x720.jpg"
|
||||
|
||||
return self._extract_from_metadata_fields(metadata, player_name)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to extract media thumbnail: {e}")
|
||||
return None
|
||||
|
||||
def _extract_youtube_video_id(self, url):
|
||||
try:
|
||||
if "youtube.com/watch" in url:
|
||||
query = urlparse(url).query
|
||||
params = dict(urllib.parse.parse_qsl(query))
|
||||
return params.get("v")
|
||||
elif "youtu.be/" in url:
|
||||
path = urlparse(url).path
|
||||
return path.strip("/")
|
||||
elif "youtube.com/embed/" in url:
|
||||
path = urlparse(url).path
|
||||
return path.split("/")[-1]
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to extract YouTube video ID: {e}")
|
||||
return None
|
||||
|
||||
def _extract_vimeo_video_id(self, url):
|
||||
try:
|
||||
path = urlparse(url).path
|
||||
video_id = path.strip("/")
|
||||
if video_id.isdigit():
|
||||
return video_id
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to extract Vimeo video ID: {e}")
|
||||
return None
|
||||
|
||||
def _extract_twitch_channel(self, url):
|
||||
try:
|
||||
path = urlparse(url).path
|
||||
channel = path.strip("/")
|
||||
if channel and not channel.startswith("videos"):
|
||||
return channel
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to extract Twitch channel: {e}")
|
||||
return None
|
||||
|
||||
def _extract_from_browser_metadata(self, metadata, player_name):
|
||||
try:
|
||||
"""Queue thumbnail update to prevent overlapping operations."""
|
||||
def _update_thumbnail():
|
||||
keys = metadata.keys()
|
||||
player_name = player.props.player_name
|
||||
|
||||
thumbnail_fields = [
|
||||
"mpris:artUrl",
|
||||
"xesam:artUrl",
|
||||
"xesam:albumArt",
|
||||
"xesam:thumbnail",
|
||||
"xesam:image",
|
||||
"xesam:cover",
|
||||
]
|
||||
new_thumbnail = None
|
||||
|
||||
for field in thumbnail_fields:
|
||||
if field in keys:
|
||||
value = metadata[field]
|
||||
if value and isinstance(value, str) and value.strip():
|
||||
if value.startswith(("http://", "https://", "data:image/")):
|
||||
logger.debug(f"Found thumbnail in {field}: {value}")
|
||||
return value
|
||||
# Check for MPRIS art URL first
|
||||
if "mpris:artUrl" in keys:
|
||||
art_url = metadata["mpris:artUrl"]
|
||||
if art_url and art_url.strip():
|
||||
new_thumbnail = art_url
|
||||
logger.debug(f"Found MPRIS art for {player_name}: {art_url}")
|
||||
|
||||
return None
|
||||
# Check for browser players and YouTube URLs
|
||||
elif player_name in ["firefox", "chromium", "vivaldi", "brave"] and "xesam:url" in keys:
|
||||
webpage_url = metadata["xesam:url"]
|
||||
if webpage_url and "youtube.com" in webpage_url:
|
||||
# Simple YouTube thumbnail extraction
|
||||
import re
|
||||
match = re.search(r"(?:v=|/)([a-zA-Z0-9_-]{11})", webpage_url)
|
||||
if match:
|
||||
video_id = match.group(1)
|
||||
new_thumbnail = f"https://img.youtube.com/vi/{video_id}/hqdefault.jpg"
|
||||
logger.debug(f"Found YouTube thumbnail for {player_name}: {new_thumbnail}")
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to extract from browser metadata: {e}")
|
||||
return None
|
||||
# Only update if thumbnail has changed
|
||||
if new_thumbnail and new_thumbnail != self._current_thumbnail:
|
||||
self._current_thumbnail = new_thumbnail
|
||||
self.album_cover.set_style(f"background-image:url('{new_thumbnail}')")
|
||||
logger.debug(f"Updated thumbnail for {player_name}")
|
||||
elif not new_thumbnail and self._current_thumbnail:
|
||||
# Reset to default if no thumbnail found
|
||||
self._current_thumbnail = None
|
||||
self.album_cover.set_style(f"background-image:url('{data.HOME_DIR}/.current.wall')")
|
||||
logger.debug(f"Reset to default wallpaper for {player_name}")
|
||||
|
||||
def _extract_from_metadata_fields(self, metadata, player_name):
|
||||
try:
|
||||
keys = metadata.keys()
|
||||
self._queue_update(_update_thumbnail)
|
||||
|
||||
thumbnail_fields = [
|
||||
"mpris:artUrl",
|
||||
"xesam:artUrl",
|
||||
"xesam:albumArt",
|
||||
"xesam:thumbnail",
|
||||
"xesam:image",
|
||||
"xesam:cover",
|
||||
"xesam:albumArtUrl",
|
||||
]
|
||||
|
||||
for field in thumbnail_fields:
|
||||
if field in keys:
|
||||
value = metadata[field]
|
||||
if value and isinstance(value, str) and value.strip():
|
||||
if value.startswith(
|
||||
("http://", "https://", "data:image/", "file://")
|
||||
):
|
||||
logger.debug(f"Found thumbnail in {field}: {value}")
|
||||
return value
|
||||
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to extract from metadata fields: {e}")
|
||||
return None
|
||||
|
||||
def on_pause(self, sender):
|
||||
self._update_play_pause_icon(False)
|
||||
|
||||
def on_play(self, sender):
|
||||
self._update_play_pause_icon(True)
|
||||
|
||||
def on_shuffle(self, sender, player, status):
|
||||
print("callback status", status)
|
||||
self._update_shuffle_icon(status)
|
||||
|
||||
def handle_next(self, player):
|
||||
self._player._player.next()
|
||||
self._refresh_metadata_after_delay(player)
|
||||
|
||||
if player.props.player_name in ["firefox", "chromium", "vivaldi", "brave"]:
|
||||
self._refresh_metadata_immediate(player)
|
||||
def _do_next():
|
||||
try:
|
||||
self._player._player.next()
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to go to next track: {e}")
|
||||
|
||||
self._queue_update(_do_next)
|
||||
|
||||
def handle_prev(self, player):
|
||||
self._player._player.previous()
|
||||
self._refresh_metadata_after_delay(player)
|
||||
|
||||
if player.props.player_name in ["firefox", "chromium", "vivaldi", "brave"]:
|
||||
self._refresh_metadata_immediate(player)
|
||||
|
||||
def _refresh_metadata_immediate(self, player):
|
||||
try:
|
||||
current_metadata = player.props.metadata
|
||||
if current_metadata:
|
||||
logger.debug(
|
||||
f"Immediate metadata refresh for {player.props.player_name}"
|
||||
)
|
||||
self.on_metadata(self._player, current_metadata, player)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed immediate metadata refresh: {e}")
|
||||
|
||||
def _refresh_metadata_after_delay(self, player):
|
||||
def refresh_metadata():
|
||||
def _do_prev():
|
||||
try:
|
||||
current_metadata = player.props.metadata
|
||||
if current_metadata:
|
||||
logger.debug(f"Refreshing metadata for {player.props.player_name}")
|
||||
self.on_metadata(self._player, current_metadata, player)
|
||||
else:
|
||||
logger.debug(
|
||||
f"No metadata available for {
|
||||
player.props.player_name
|
||||
}, retrying..."
|
||||
)
|
||||
GLib.timeout_add(1000, refresh_metadata)
|
||||
self._player._player.previous()
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to refresh metadata: {e}")
|
||||
logger.warning(f"Failed to go to previous track: {e}")
|
||||
|
||||
self._queue_update(_do_prev)
|
||||
|
||||
|
||||
GLib.timeout_add(500, refresh_metadata)
|
||||
|
||||
def handle_play_pause(self, player):
|
||||
is_playing = (
|
||||
self._player._player.props.playback_status.value_name
|
||||
== "PLAYERCTL_PLAYBACK_STATUS_PLAYING"
|
||||
)
|
||||
def _do_play_pause():
|
||||
is_playing = (
|
||||
self._player._player.props.playback_status.value_name
|
||||
== "PLAYERCTL_PLAYBACK_STATUS_PLAYING"
|
||||
)
|
||||
|
||||
def _set_play_ui():
|
||||
self._update_play_pause_icon(True)
|
||||
def _set_play_ui():
|
||||
self._update_play_pause_icon(True)
|
||||
|
||||
def _set_pause_ui():
|
||||
self._update_play_pause_icon(False)
|
||||
def _set_pause_ui():
|
||||
self._update_play_pause_icon(False)
|
||||
|
||||
if is_playing:
|
||||
_set_pause_ui()
|
||||
else:
|
||||
_set_play_ui()
|
||||
|
||||
try:
|
||||
self._player._player.play_pause()
|
||||
except Exception as e:
|
||||
if is_playing:
|
||||
_set_pause_ui()
|
||||
else:
|
||||
_set_play_ui()
|
||||
logger.warning("Failed to toggle playback:", e)
|
||||
|
||||
try:
|
||||
self._player._player.play_pause()
|
||||
except Exception as e:
|
||||
if is_playing:
|
||||
_set_pause_ui()
|
||||
else:
|
||||
_set_play_ui()
|
||||
logger.warning("Failed to toggle playback:", e)
|
||||
|
||||
self._queue_update(_do_play_pause)
|
||||
|
||||
def handle_shuffle(self, shuffle_button, player):
|
||||
print("shuffle", player.props.shuffle)
|
||||
def _do_shuffle():
|
||||
print("shuffle", player.props.shuffle)
|
||||
|
||||
if not self._is_shuffle_supported(player):
|
||||
print(f"Shuffle not supported for {player.props.player_name}")
|
||||
return
|
||||
if not self._is_shuffle_supported(player):
|
||||
print(f"Shuffle not supported for {player.props.player_name}")
|
||||
return
|
||||
|
||||
try:
|
||||
if player.props.shuffle == False:
|
||||
player.set_shuffle(True)
|
||||
print("setting to true", player.props.player_name)
|
||||
else:
|
||||
player.set_shuffle(False)
|
||||
except Exception as e:
|
||||
print(f"Failed to toggle shuffle for {player.props.player_name}: {e}")
|
||||
pass
|
||||
shuffle_button.get_child().set_style("color: var(--outline)")
|
||||
try:
|
||||
if player.props.shuffle == False:
|
||||
player.set_shuffle(True)
|
||||
print("setting to true", player.props.player_name)
|
||||
else:
|
||||
player.set_shuffle(False)
|
||||
except Exception as e:
|
||||
print(f"Failed to toggle shuffle for {player.props.player_name}: {e}")
|
||||
pass
|
||||
|
||||
self._queue_update(_do_shuffle)
|
||||
|
||||
|
||||
class Placheholder(Box):
|
||||
class Placeholder(Box):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(style_classes="player-box", **kwargs)
|
||||
|
||||
@@ -550,14 +441,18 @@ class Placheholder(Box):
|
||||
|
||||
|
||||
class PlayerContainer(Box):
|
||||
@Signal
|
||||
def active_player_changed(self, player: Playerctl.Player) -> None: ...
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(name="player-container", **kwargs)
|
||||
super().__init__(name="player-container", orientation="v", **kwargs)
|
||||
|
||||
self.manager = PlayerManager()
|
||||
self.manager.connect("new-player", self.new_player)
|
||||
self.manager.connect("player-vanish", self.on_player_vanish)
|
||||
self.placeholder = Placheholder()
|
||||
self.placeholder = Placeholder()
|
||||
self.stack = Stack(
|
||||
name="player-container",
|
||||
transition_type="crossfade",
|
||||
transition_duration=100,
|
||||
children=[self.placeholder],
|
||||
@@ -575,33 +470,23 @@ class PlayerContainer(Box):
|
||||
orientation="v",
|
||||
center_children=[],
|
||||
)
|
||||
self.tile_icon = Image(
|
||||
name="disc",
|
||||
style="font-size:30px; margin:0px; padding:0px;",
|
||||
icon_name="disc-symbolic",
|
||||
size=30,
|
||||
)
|
||||
|
||||
self.tile_view = CenterBox(
|
||||
v_expand=False,
|
||||
center_children=self.tile_icon,
|
||||
)
|
||||
self.event_box = EventBox(
|
||||
events=["scroll"],
|
||||
child=Box(children=[self.stack, self.player_switch_container]),
|
||||
)
|
||||
self.children = self.event_box
|
||||
self.players = []
|
||||
self.player = []
|
||||
self.player_objects = {} # Map player names to player objects
|
||||
self.active_player = None # Track the currently active player
|
||||
self.manager.init_all_players()
|
||||
self.event_box.connect("scroll-event", self.on_scroll)
|
||||
self.stack.connect("notify::visible-child", self.on_visible_child_changed)
|
||||
|
||||
def new_player(self, manager, player):
|
||||
new_player = Player(player=player)
|
||||
new_player.set_name(player.props.player_name)
|
||||
self.players.append(new_player)
|
||||
self.player.append(new_player)
|
||||
# Store the player object
|
||||
self.player_objects[player.props.player_name] = player
|
||||
self.stack.add_named(new_player, player.props.player_name)
|
||||
if len(self.players) == 1:
|
||||
if len(self.player) == 1:
|
||||
self.stack.remove(self.placeholder)
|
||||
|
||||
self.player_switch_container.add_center(
|
||||
@@ -611,6 +496,12 @@ class PlayerContainer(Box):
|
||||
on_clicked=lambda b: self.switch_player(player.props.player_name, b),
|
||||
)
|
||||
)
|
||||
|
||||
# If this is the first player, make it active
|
||||
if not self.active_player:
|
||||
self.active_player = player
|
||||
self.active_player_changed(player)
|
||||
|
||||
self.update_player_list()
|
||||
|
||||
def switch_player(self, player_name, button):
|
||||
@@ -620,63 +511,47 @@ class PlayerContainer(Box):
|
||||
btn.remove_style_class("active")
|
||||
button.add_style_class("active")
|
||||
|
||||
# Get the player object and emit signal
|
||||
if player_name in self.player_objects:
|
||||
self.active_player = self.player_objects[player_name]
|
||||
self.active_player_changed(self.active_player)
|
||||
|
||||
def on_player_vanish(self, manager, player):
|
||||
for player_instance in self.players:
|
||||
if player_instance.get_name() == player.props.player_name:
|
||||
player_name = player.props.player_name
|
||||
|
||||
for player_instance in self.player:
|
||||
if player_instance.get_name() == player_name:
|
||||
player_instance.cleanup() # Clean up resources
|
||||
self.stack.remove(player_instance)
|
||||
self.players.remove(player_instance)
|
||||
self.player.remove(player_instance)
|
||||
for btn in self.player_switch_container.center_children:
|
||||
if btn.get_name() == player_instance.get_name():
|
||||
if btn.get_name() == player_name:
|
||||
self.player_switch_container.remove_center(btn)
|
||||
self.update_player_list()
|
||||
break
|
||||
|
||||
if len(self.players) == 0:
|
||||
# Clean up player objects dictionary
|
||||
if player_name in self.player_objects:
|
||||
del self.player_objects[player_name]
|
||||
|
||||
# If the active player vanished, switch to another one
|
||||
if self.active_player and self.active_player.props.player_name == player_name:
|
||||
self.active_player = None
|
||||
# Try to find another player to make active
|
||||
if self.player_objects:
|
||||
first_player_name = next(iter(self.player_objects))
|
||||
self.active_player = self.player_objects[first_player_name]
|
||||
self.active_player_changed(self.active_player)
|
||||
|
||||
self.update_player_list()
|
||||
|
||||
if len(self.player) == 0:
|
||||
self.stack.add_named(self.placeholder, "placeholder")
|
||||
|
||||
def update_player_list(self):
|
||||
curr = self.stack.get_visible_child()
|
||||
if curr is None:
|
||||
return
|
||||
for btn in self.player_switch_container.center_children:
|
||||
print(btn.get_name())
|
||||
if btn.get_name() == curr.get_name():
|
||||
btn.add_style_class("active")
|
||||
else:
|
||||
btn.remove_style_class("active")
|
||||
|
||||
def on_scroll(self, widget, event):
|
||||
match event.direction:
|
||||
case 0:
|
||||
self.switch_relative_player(forward=False)
|
||||
case 1:
|
||||
self.switch_relative_player(forward=True)
|
||||
|
||||
def switch_relative_player(self, forward=True):
|
||||
if not self.players:
|
||||
return
|
||||
|
||||
current_player = self.stack.get_visible_child()
|
||||
if current_player is None:
|
||||
return
|
||||
|
||||
current_index = self.players.index(current_player)
|
||||
|
||||
next_index = (current_index + (1 if forward else -1)) % len(self.players)
|
||||
next_player = self.players[next_index]
|
||||
|
||||
for btn in self.player_switch_container.center_children:
|
||||
if btn.get_name() == next_player.get_name():
|
||||
self.switch_player(next_player.get_name(), btn)
|
||||
break
|
||||
|
||||
def on_visible_child_changed(self, *args):
|
||||
curr_child = self.stack.get_visible_child()
|
||||
if curr_child is None:
|
||||
return
|
||||
curr_player = curr_child.get_name()
|
||||
self.tile_icon.set_name(curr_player)
|
||||
self.tile_icon.set_icon_name(self._get_player_icon_name(curr_player))
|
||||
|
||||
def get_view(self):
|
||||
return self.tile_view
|
||||
if curr:
|
||||
for btn in self.player_switch_container.center_children:
|
||||
if btn.get_name() == curr.get_name():
|
||||
btn.add_style_class("active")
|
||||
else:
|
||||
btn.remove_style_class("active")
|
||||
+15
-27
@@ -1,11 +1,9 @@
|
||||
.player-box {
|
||||
min-height: 60px;
|
||||
min-width: 300px;
|
||||
padding: 1px;
|
||||
box-shadow:
|
||||
inset 0 0 100px rgba(148, 148, 148, 0.3),
|
||||
inset 70px 0 20px rgba(255, 255, 255, 0.3),
|
||||
inset 0px 0px 20px 5px rgba(255, 255, 255, 0.3);
|
||||
padding: 10px;
|
||||
background-color: alpha(#000, 0.3);
|
||||
box-shadow: inset 0 0 0 1px alpha(#aaa, 0.4);
|
||||
border-radius: 10px;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
@@ -14,7 +12,7 @@
|
||||
#player-container {
|
||||
min-height: 60px;
|
||||
border-radius: 10px;
|
||||
padding: 4px;
|
||||
padding: 2px;
|
||||
margin-left: 2px;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
@@ -31,12 +29,11 @@
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
#artist {
|
||||
color: var(--primary);
|
||||
}
|
||||
/* #artist { */
|
||||
/* color: var(--primary); */
|
||||
/* } */
|
||||
|
||||
#song {
|
||||
color: var(--surface);
|
||||
font-size: 15px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
@@ -52,27 +49,17 @@
|
||||
}
|
||||
|
||||
#pause-button {
|
||||
background-color: whitesmoke;
|
||||
/* background-color: whitesmoke; */
|
||||
border-radius: 16px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
#pause-label,
|
||||
#play-label {
|
||||
color: var(--shadow);
|
||||
/* color: var(--shadow); */
|
||||
font-size: 20px;
|
||||
padding: 2px;
|
||||
border-radius: 999px;
|
||||
min-width: 25px;
|
||||
}
|
||||
|
||||
#pause-label.mini,
|
||||
#play-label.mini {
|
||||
color: white;
|
||||
font-size: 20px;
|
||||
padding: 2px;
|
||||
border-radius: 999px;
|
||||
min-width: 25px;
|
||||
border-radius: 9px;
|
||||
}
|
||||
|
||||
#shuffle-button,
|
||||
@@ -80,24 +67,25 @@
|
||||
#prev-button,
|
||||
#play-pause-button {
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
padding: 0px;
|
||||
padding-left: 11px;
|
||||
padding-right: 11px;
|
||||
min-width: 25px;
|
||||
min-height: 25px;
|
||||
}
|
||||
|
||||
#shuffle-button:hover,
|
||||
#next-button:hover,
|
||||
#prev-button:hover,
|
||||
#play-pause-button:hover {
|
||||
border-radius: 100%;
|
||||
|
||||
box-shadow: inset rgba(255, 255, 255, 0.5) 0 0 10px;
|
||||
}
|
||||
|
||||
.player-button {
|
||||
border-radius: 5px;
|
||||
background-color: var(--shadow);
|
||||
/* background-color: white; */
|
||||
/* background-color: var(--shadow); */
|
||||
background-color: white;
|
||||
opacity: 1;
|
||||
min-width: 10px;
|
||||
min-height: 10px;
|
||||
|
||||
Reference in new issue
Block a user