From cc5a116efbe3bfe4be1b10595f23804440e85453 Mon Sep 17 00:00:00 2001 From: tr1x_em Date: Wed, 6 Aug 2025 15:41:33 +0530 Subject: [PATCH] Fix Expanded Player --- modules/controlcenter/expanded_player.py | 375 +++++++++++------------ modules/controlcenter/player.py | 9 + modules/dock.py | 65 ++++ styles/controlcenter.css | 155 +++++++++- styles/dock.css | 10 +- 5 files changed, 421 insertions(+), 193 deletions(-) diff --git a/modules/controlcenter/expanded_player.py b/modules/controlcenter/expanded_player.py index 47218cf..c864d4f 100644 --- a/modules/controlcenter/expanded_player.py +++ b/modules/controlcenter/expanded_player.py @@ -188,9 +188,27 @@ class PlayerBoxStack(Box): """Create a placeholder box for when no media is playing.""" fallback_cover_path = f"{data.HOME_DIR}/.current.wall" - # Album cover with fallback image - album_cover = Box(style_classes="album-image") - album_cover.set_style(f"background-image:url('{fallback_cover_path}')") + # Album cover with fallback image using Image widget + album_cover_image = Image( + name="macos-album-cover-image", + pixbuf=None, + ) + + try: + from gi.repository import GdkPixbuf + pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale( + fallback_cover_path, 80, 80, True + ) + album_cover_image.set_from_pixbuf(pixbuf) + album_cover_image.set_size_request(80, 80) + except Exception as e: + logger.warning(f"Failed to load fallback image in no-media box: {e}") + + album_cover = Box( + name="macos-album-image", + children=[album_cover_image], + ) + album_cover.set_size_request(80, 80) image_stack = Box(h_align="start", v_align="center", name="player-image-stack") image_stack.children = [album_cover] @@ -465,7 +483,7 @@ class PlayerBox(Box): self.player_stack = player_stack self.fallback_cover_path = f"{data.HOME_DIR}/.current.wall" - self.image_size = 100 # Smaller album art + self.image_size = 80 # Large album art for macOS style self.icon_size = 15 # State @@ -479,7 +497,7 @@ class PlayerBox(Box): self._download_cancelled = False # Flag to cancel downloads self._signal_connections = [] # Track signal connections - self.album_cover = Box(style_classes="album-image") + self.album_cover = Box(style_classes="macos-album-image") self.album_cover.set_style( f"background-image:url('{self.fallback_cover_path}')" ) @@ -492,9 +510,9 @@ class PlayerBox(Box): # Track Info self.track_title = Label( label="No Title", - name="player-title", + name="macos-player-title", justification="left", - max_chars_width=35, + max_chars_width=30, ellipsization="end", h_align="start", h_expand=True, @@ -502,45 +520,43 @@ class PlayerBox(Box): self.track_artist = Label( label="No Artist", - name="player-artist", + name="macos-player-artist", justification="left", - max_chars_width=35, + max_chars_width=30, ellipsization="end", h_align="start", h_expand=True, visible=True, ) - self.track_album = Label( - label="No Album", - name="player-album", - justification="left", - max_chars_width=35, - ellipsization="end", - h_align="start", - h_expand=True, - visible=True, + # Use Image widget instead of CSS background for more reliable image display + self.album_cover_image = Image( + name="macos-album-cover-image", + pixbuf=None, ) + + # Set fallback image immediately with proper scaling + try: + from gi.repository import GdkPixbuf + pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale( + self.fallback_cover_path, 80, 80, True + ) + self.album_cover_image.set_from_pixbuf(pixbuf) + self.album_cover_image.set_size_request(80, 80) + except Exception as e: + logger.warning(f"Failed to load fallback image: {e}") + + # Container box for styling with fixed size + self.album_cover = Box( + name="macos-album-image", + children=[self.album_cover_image], + ) + self.album_cover.set_size_request(80, 80) - self.track_artist = Label( - label="No Artist", - name="player-artist", - justification="left", - max_chars_width=25, - ellipsization="end", - h_align="start", - visible=True, - ) - - self.track_album = Label( - label="No Album", - name="player-album", - justification="left", - max_chars_width=25, - ellipsization="end", - h_align="start", - visible=True, + self.image_stack = Box( + h_align="start", v_align="start", name="macos-player-image-stack" ) + self.image_stack.children = [*self.image_stack.children, self.album_cover] # Seek bar should not update automatically during user interaction self._user_seeking = False @@ -550,7 +566,7 @@ class PlayerBox(Box): min_value=0, max_value=100, increments=(1, 1), - name="apple-seek-bar", + name="macos-seek-bar", size=1, h_expand=True, ) @@ -559,6 +575,76 @@ class PlayerBox(Box): self.seek_bar.connect("button-release-event", self._on_seek_end) self.player.bind("can-seek", "sensitive", self.seek_bar) + # Position and length labels for seek bar + self.position_label = Label( + label="0:00", + name="macos-position-label", + justification="left", + h_align="start", + ) + + self.length_label = Label( + label="0:00", + name="macos-length-label", + justification="right", + h_align="end", + ) + + # Labels box for position and length below seek bar + self.labels_box = Box( + name="macos-labels-box", + orientation="h", + children=[ + self.position_label, + Box(h_expand=True), # Spacer to push labels to ends + self.length_label, + ], + ) + + # Seek bar with position labels below + self.seek_box = Box( + name="macos-seek-box", + orientation="v", + spacing=2, + children=[ + self.seek_bar, + self.labels_box, + ], + ) + + # Define buttons first + self.button_box = Box( + name="macos-button-box", + h_align="center", + spacing=15, # Wider spacing for macOS look + ) + + # Define controls_box BEFORE using it in track_info + self.controls_box = Box( + name="macos-player-controls", + orientation="v", + spacing=8, + h_align="center", + children=[self.button_box], + ) + + # Track info with inline controls - more compact layout + self.track_info = Box( + name="macos-track-info", + spacing=6, # Reduced spacing + orientation="v", + v_align="start", + h_align="start", + h_expand=True, + children=[ + self.track_title, + self.track_artist, + self.seek_box, + self.controls_box, # Controls now inline with track info + ], + ) + + # Bind player properties self.player.bind_property( "title", self.track_title, @@ -578,100 +664,25 @@ class PlayerBox(Box): ), # type: ignore ) - self.player.bind_property( - "album", - self.track_album, - "label", - GObject.BindingFlags.DEFAULT, - lambda _, x: ( - re.sub(r"\r?\n", " ", x) if x != "" and x is not None else "No Album" - ), # type: ignore - ) - - # Position and length labels for seek bar - self.position_label = Label( - label="0:00", - name="position-label", - justification="left", - h_align="start", - ) - - self.length_label = Label( - label="0:00", - name="length-label", - justification="right", - h_align="end", - ) - - # Labels box for position and length below seek bar - self.labels_box = Box( - name="labels-box", - orientation="h", - children=[ - self.position_label, - Box(h_expand=True), # Spacer to push labels to ends - self.length_label, - ], - ) - - # Seek bar with position labels below - self.seek_box = Box( - name="seek-box", - orientation="v", - spacing=1, - children=[ - self.seek_bar, - self.labels_box, - ], - ) - - self.track_info = Box( - name="track-info", - spacing=3, - h_expand=True, - orientation="v", - v_align="start", - h_align="fill", - children=[ - self.track_title, - self.track_artist, - self.track_album, - self.seek_box, - ], - ) - - # Buttons - self.button_box = Box( - name="button-box", - h_align="center", - spacing=2, - ) - - # Stack switcher buttons box + # Player switcher buttons box (compact, minimal space) self.stack_buttons_box = Box( h_expand=False, - v_expand=True, - name="stack-buttons-box", - spacing=4, - orientation="v", - h_align="center", - v_align="center", + v_expand=False, + name="macos-stack-buttons-box", + spacing=4, # Reduced spacing + orientation="h", # Horizontal layout for compactness + h_align="end", + v_align="start", ) self.stack_buttons_box.hide() # Initially hidden - self.controls_box = CenterBox( - name="player-controls", - h_expand=True, - center_children=self.button_box, - ) - self.skip_next_icon = Image(icon_name="player_fwd") self.skip_prev_icon = Image(icon_name="player_rew") self.shuffle_icon = Image(icon_name="shuffle") self.play_pause_icon = Image(icon_name="player_pause") self.play_pause_button = Button( - name="player-button", + name="macos-play-button", child=self.play_pause_icon, on_clicked=self.player.play_pause, ) @@ -679,77 +690,52 @@ class PlayerBox(Box): self.player.bind_property("can_pause", self.play_pause_button, "sensitive") self.next_button = Button( - style_classes=["player-button"], + name="macos-control-button", child=self.skip_next_icon, on_clicked=self._on_player_next, ) self.player.bind_property("can_go_next", self.next_button, "sensitive") self.prev_button = Button( - style_classes=["player-button"], + name="macos-control-button", child=self.skip_prev_icon, on_clicked=self._on_player_prev, ) - self.shuffle_button = Button( - style_classes=["player-button"], - child=self.shuffle_icon, - on_clicked=self.player.toggle_shuffle, - ) - 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, ) - self.player_info_box = Box( - name="player-info-box", - h_expand=True, - v_align="center", - h_align="fill", - orientation="v", - children=[self.track_info, self.controls_box], - ) - self.inner_box = Box( - name="inner-player-box", - v_align="center", - h_align="start", - ) - # resize the inner box + # Compact macOS layout: album art on left, expanded track info+controls, minimal switcher self.outer_box = Box( - name="outer-player-box", - h_expand=True, + name="macos-outer-player-box", orientation="horizontal", + spacing=15, + h_align="start", children=[ - self.image_stack, - self.player_info_box, + self.image_stack, # Album art on left Box( + name="macos-main-section", orientation="v", + spacing=0, + h_expand=True, + v_align="center", children=[ + # Header with player switcher in corner Box( - children=Image( - icon_name=self.player.player_name, icon_size=18 - ), - h_align="end", - h_expand=False, - v_align="start", - style="margin-top: 5px; margin-right: 10px;", - tooltip_text=self.player.player_name, # type: ignore - ), - Box( - children=self.stack_buttons_box, - h_align="end", - v_expand=True, - v_align="center", - h_expand=False, - style="margin-right: 15px;", + orientation="h", + h_expand=True, + children=[ + Box(h_expand=True), # Spacer + self.stack_buttons_box, # Compact switcher in corner + ], ), + # Main content area + self.track_info, # Contains title, artist, seek bar AND controls now ], ), ], - h_align="start", ) self.children = [*self.children, self.outer_box] @@ -794,9 +780,7 @@ class PlayerBox(Box): def update_buttons(self, player_buttons, show_buttons): """Update the stack switcher buttons in this player box""" logger.info( - f"[PlayerBox] update_buttons called: show_buttons={ - show_buttons - }, num_buttons={len(player_buttons)}" + f"[PlayerBox] update_buttons called: show_buttons={show_buttons}, num_buttons={len(player_buttons)}" ) # Clear existing buttons @@ -808,37 +792,28 @@ class PlayerBox(Box): if show_buttons and len(player_buttons) > 1: logger.info(f"[PlayerBox] Creating {len(player_buttons)} stack buttons") - # Create simple dot indicators for each player + # Create macOS-style dot indicators for each player for i, button in enumerate(player_buttons): - # Create a more visible dot button + # Create a macOS-style dot button dot_button = Button( - name="player-stack-button", - style="min-width: 5px; min-height: 5px; border-radius: 8px; margin: 4px; background-color: #666; border: 1px solid #999;", + name="macos-player-switcher-dot", + style_classes=["macos-switcher-dot"], ) # Set active state based on original button if button.get_style_context().has_class("active"): dot_button.add_style_class("active") - dot_button.set_style( - "min-width: 5px; min-height: 5px; border-radius: 8px; margin: 4px; background-color: #fff; border: 1px solid #ccc;" - ) # Connect click handler to switch to this player def make_click_handler(index): - return lambda *_: self.player_stack.on_player_clicked_by_index( - index - ) + return lambda *_: self.player_stack.on_player_clicked_by_index(index) dot_button.connect("clicked", make_click_handler(i)) self.stack_buttons_box.children = [ *self.stack_buttons_box.children, dot_button, ] - logger.info( - f"[PlayerBox] Added dot button {i}, total children: { - len(self.stack_buttons_box.get_children()) - }" - ) + logger.info(f"[PlayerBox] Added dot button {i}") self.stack_buttons_box.show_all() logger.info("[PlayerBox] Stack buttons box shown") @@ -917,15 +892,37 @@ class PlayerBox(Box): self.play_pause_icon.set_from_icon_name("player_pause") def _update_image(self, image_path): - if image_path and os.path.isfile(image_path): - self.album_cover.set_style(f"background-image:url('{image_path}')") - else: - self.album_cover.set_style( - f"background-image:url('{self.fallback_cover_path}')" - ) + try: + from gi.repository import GdkPixbuf + + if image_path and os.path.isfile(image_path): + # Create scaled pixbuf to ensure proper size + pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale( + image_path, 80, 80, True + ) + self.album_cover_image.set_from_pixbuf(pixbuf) + else: + # Use fallback image with scaling + pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale( + self.fallback_cover_path, 80, 80, True + ) + self.album_cover_image.set_from_pixbuf(pixbuf) + except Exception as e: + logger.warning(f"Failed to update album cover image: {e}") + # Try simple file setting as last resort + try: + self.album_cover_image.set_from_file(self.fallback_cover_path) + self.album_cover_image.set_size_request(80, 80) + except Exception as e2: + logger.error(f"Failed to set fallback image: {e2}") def _set_image(self, *_): art_url = self.player.arturl + + # If no art URL or empty/None, use fallback + if not art_url: + self._update_image(None) + return parsed = urllib.parse.urlparse(art_url) if parsed.scheme == "file": diff --git a/modules/controlcenter/player.py b/modules/controlcenter/player.py index 27b1164..c4e1d46 100644 --- a/modules/controlcenter/player.py +++ b/modules/controlcenter/player.py @@ -447,6 +447,9 @@ class PlayerBox(Box): self.control_center = control_center self.fallback_cover_path = f"{data.HOME_DIR}/.current.wall" + # Add controls_box attribute early for compatibility + self.controls_box = Box(name="temp-controls-box") # Temporary placeholder + self.image_size = 50 self.icon_size = 15 @@ -585,6 +588,10 @@ class PlayerBox(Box): self.play_pause_button, self.next_button, ) + + # Assign button_box to controls_box for compatibility + self.controls_box = self.button_box + self.player_info_box = Box( name="player-info-box-c", v_align="center", @@ -693,6 +700,8 @@ class PlayerBox(Box): self.control_center.open_expanded_player() except Exception as e: logger.warning(f"Failed to handle outer box click: {e}") + import traceback + logger.error(f"Full traceback: {traceback.format_exc()}") def update_buttons(self, player_buttons, show_buttons): # """Update the stack switcher buttons in this player box""" diff --git a/modules/dock.py b/modules/dock.py index 4953ee0..404d25a 100644 --- a/modules/dock.py +++ b/modules/dock.py @@ -429,6 +429,8 @@ class Dock(Window): button.connect("drag-data-get", self.on_drag_data_get) button.connect("drag-data-received", self.on_drag_data_received) button.connect("enter-notify-event", self._on_child_enter) + button.connect("enter-notify-event", self._on_icon_hover_enter) + button.connect("leave-notify-event", self._on_icon_hover_leave) overlay = Overlay( overlays=Label( label=workspace, @@ -480,6 +482,69 @@ class Dock(Window): f"hyprctl dispatch focuswindow address:{instance['address']}" ) + def _on_icon_hover_enter(self, widget, event): + """Handle icon hover enter - enlarge the icon""" + widget.add_style_class("dock-icon-hover") + # Find the image widget inside the button and scale it + dock_icon = self._find_dock_icon_image(widget) + if dock_icon: + # Create a larger version of the icon + enlarged_size = int(self.icon_size * 1.3) # 30% larger + if hasattr(widget, 'desktop_app') and widget.desktop_app: + enlarged_pixbuf = widget.desktop_app.get_icon_pixbuf(size=enlarged_size) + if enlarged_pixbuf: + dock_icon.set_from_pixbuf(enlarged_pixbuf) + elif hasattr(widget, 'app_identifier'): + id_value = ( + widget.app_identifier["name"] + if isinstance(widget.app_identifier, dict) + else widget.app_identifier + ) + enlarged_pixbuf = self.icon_resolver.get_icon_pixbuf(id_value, enlarged_size) + if enlarged_pixbuf: + dock_icon.set_from_pixbuf(enlarged_pixbuf) + return False + + def _on_icon_hover_leave(self, widget, event): + """Handle icon hover leave - restore original icon size""" + widget.remove_style_class("dock-icon-hover") + # Find the image widget inside the button and restore normal size + dock_icon = self._find_dock_icon_image(widget) + if dock_icon: + # Restore original size icon + if hasattr(widget, 'desktop_app') and widget.desktop_app: + normal_pixbuf = widget.desktop_app.get_icon_pixbuf(size=self.icon_size) + if normal_pixbuf: + dock_icon.set_from_pixbuf(normal_pixbuf) + elif hasattr(widget, 'app_identifier'): + id_value = ( + widget.app_identifier["name"] + if isinstance(widget.app_identifier, dict) + else widget.app_identifier + ) + normal_pixbuf = self.icon_resolver.get_icon_pixbuf(id_value, self.icon_size) + if normal_pixbuf: + dock_icon.set_from_pixbuf(normal_pixbuf) + return False + + def _find_dock_icon_image(self, button_widget): + """Find the Image widget inside a dock button""" + def find_image_recursive(widget): + if isinstance(widget, Image): + return widget + if hasattr(widget, 'get_children'): + for child in widget.get_children(): + result = find_image_recursive(child) + if result: + return result + elif hasattr(widget, 'get_child'): + child = widget.get_child() + if child: + return find_image_recursive(child) + return None + + return find_image_recursive(button_widget) + def _on_child_enter(self, widget, event): self.is_mouse_over_dock_area = True if self.hide_id: diff --git a/styles/controlcenter.css b/styles/controlcenter.css index 4631b10..f3da8b5 100644 --- a/styles/controlcenter.css +++ b/styles/controlcenter.css @@ -226,9 +226,7 @@ border-radius: 16px; min-width: 16px; min-height: 8px; - transition: - background-color 0.1s cubic-bezier(0.5, 0.25, 0, 1.25), - transform 0.25s cubic-bezier(0.5, 0.25, 0, 1.25); + transition: background-color 0.1s cubic-bezier(0.5, 0.25, 0, 1.25); } #toggle-button:checked { @@ -554,3 +552,154 @@ 0 1px 3px alpha(#000, 0.3), 0 0 0 2px alpha(#007aff, 0.5); } + +/* macOS-style expanded player - more compact */ +#macos-outer-player-box { + min-height: 110px; /* Reduced height */ + min-width: 380px; + border-radius: 12px; + margin: 10px; + padding: 15px; /* Reduced padding */ + background-color: alpha(#000, 0.4); + box-shadow: + 0 4px 16px alpha(#000, 0.25), + inset 0 0 0 1px alpha(#fff, 0.1); +} + +#macos-main-section { + margin-left: 5px; +} + +#macos-album-image { + min-width: 80px; + min-height: 80px; + border-radius: 8px; + box-shadow: 0 2px 8px alpha(#000, 0.3); +} + +#macos-album-cover-image { + min-width: 80px; + min-height: 80px; + border-radius: 8px; +} + +#macos-player-title { + font-weight: 600; + font-size: 16px; + color: #ffffff; + margin-bottom: 2px; +} + +#macos-player-artist { + font-weight: 400; + font-size: 13px; + color: alpha(#fff, 0.8); + margin-bottom: 8px; +} + +/* macOS seek bar styling */ +#macos-seek-bar { + background-color: transparent; + margin: 8px 0 4px 0; + min-height: 20px; +} + +#macos-seek-bar trough { + min-width: 200px; + min-height: 4px; + border-radius: 2px; + background-color: alpha(#fff, 0.25); + border: none; +} + +#macos-seek-bar highlight { + background: #ffffff; + border-radius: 2px; + border: none; +} + +#macos-seek-bar slider { + background: #ffffff; + border: none; + border-radius: 50%; + min-width: 12px; + min-height: 12px; + margin: -4px; + box-shadow: 0 1px 3px alpha(#000, 0.3); + transition: all 0.1s ease; +} + +#macos-seek-bar slider:hover { + min-width: 14px; + min-height: 14px; + margin: -5px; + box-shadow: 0 2px 6px alpha(#000, 0.4); +} + +#macos-position-label, +#macos-length-label { + font-size: 11px; + color: alpha(#fff, 0.7); + font-weight: 400; +} + +/* macOS control buttons - more compact */ +#macos-button-box { + margin-top: 6px; /* Reduced top margin */ +} + +#macos-control-button { + background: transparent; + border: none; + border-radius: 50%; + min-width: 28px; /* Slightly smaller */ + min-height: 28px; + padding: 5px; /* Reduced padding */ + color: #ffffff; + transition: all 0.15s ease; +} + +#macos-control-button:hover { + background: alpha(#fff, 0.1); + box-shadow: 0 0 0 1px alpha(#fff, 0.2); +} + +#macos-play-button { + background: transparent; + border: none; + border-radius: 50%; + min-width: 36px; /* Slightly smaller */ + min-height: 36px; + padding: 7px; /* Reduced padding */ + color: #ffffff; + transition: all 0.15s ease; +} + +#macos-play-button:hover { + background: alpha(#fff, 0.15); + box-shadow: 0 0 0 1px alpha(#fff, 0.25); +} + +/* macOS player switcher dots - compact horizontal layout */ +#macos-stack-buttons-box { + margin: 2px 5px; /* Minimal margins */ +} + +.macos-switcher-dot { + background: alpha(#fff, 0.3); + border: none; + border-radius: 50%; + min-width: 6px; /* Smaller dots */ + min-height: 6px; + margin: 2px; /* Reduced margins */ + transition: all 0.2s ease; +} + +.macos-switcher-dot:hover { + background: alpha(#fff, 0.5); +} + +.macos-switcher-dot.active { + background: #ffffff; + box-shadow: 0 0 0 1px alpha(#fff, 0.3); /* Smaller glow */ +} diff --git a/styles/dock.css b/styles/dock.css index cead4b7..40554e1 100644 --- a/styles/dock.css +++ b/styles/dock.css @@ -39,12 +39,20 @@ } #dock-app-button { - padding: 4px; + padding: 6px; border-radius: 24px; + transition: all 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94); /* box-shadow: 0 0 4px alpha(var(--shadow), 0.5); */ /* transition: all 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275); */ } +#dock-app-button.dock-icon-hover { + padding: 4px; + border-radius: 28px; + background-color: alpha(#fff, 0.15); + box-shadow: 0 4px 12px alpha(#000, 0.2); +} + #dock-app-button:hover { background-color: var(--surface-bright); }