Skip to content

Commit b75ce93

Browse files
committed
feat(gui): add mini player AI chat
1 parent 1f57ff6 commit b75ce93

10 files changed

Lines changed: 395 additions & 36 deletions

File tree

feeluown/gui/components/dynamic_island.py

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,10 @@ class DynamicIslandStatusBar(QWidget):
6767
Uses system palette colors and reuses existing components.
6868
"""
6969

70-
def __init__(self, app: "GuiApp", parent=None):
70+
def __init__(self, app: "GuiApp", parent=None, trailing_widget=None):
7171
super().__init__(parent=parent)
7272
self._app = app
73+
self._trailing_widget = trailing_widget
7374

7475
# Hover and animation state
7576
self._hovered = False
@@ -79,6 +80,7 @@ def __init__(self, app: "GuiApp", parent=None):
7980
self._anim_to = 0 # target width
8081
self._anim_current = 0 # current animated width
8182
self._has_song = False
83+
self._visibility_suppressed = False
8284
self._compact_text = ""
8385
self._position = _number_or_zero(self._app.player.position)
8486
self._duration = _number_or_zero(self._app.player.duration)
@@ -151,6 +153,14 @@ def _setup_ui(self):
151153
)
152154
self._layout.addWidget(self._song_label, 1)
153155
self._layout.addWidget(self._control_widget)
156+
if self._trailing_widget is not None:
157+
self._trailing_widget.setParent(self)
158+
self._layout.addWidget(
159+
self._trailing_widget,
160+
0,
161+
Qt.AlignmentFlag.AlignVCenter,
162+
)
163+
self._trailing_widget.hide()
154164
self.setFixedWidth(self._calc_compact_width())
155165

156166
def _connect_signals(self):
@@ -187,6 +197,13 @@ def _connect_signals(self):
187197

188198
# ---- public methods ----
189199

200+
def set_visibility_suppressed(self, suppressed):
201+
self._visibility_suppressed = suppressed
202+
if suppressed:
203+
self.hide()
204+
else:
205+
self._sync_current_state()
206+
190207
# ---- protected slots ----
191208

192209
def _on_player_state_for_pp_btn(self, state):
@@ -230,7 +247,7 @@ def _on_metadata_changed(self, metadata):
230247

231248
state = self._app.player.state
232249
if state != State.stopped:
233-
self.show()
250+
self._show_if_allowed()
234251
if state == State.paused:
235252
self._start_expand()
236253
elif state == State.playing and not self._hovered:
@@ -242,11 +259,11 @@ def _on_player_state_changed(self, state):
242259
self.hide()
243260
elif state == State.paused:
244261
if self._has_song:
245-
self.show()
262+
self._show_if_allowed()
246263
self._start_expand()
247264
elif state == State.playing:
248265
if self._has_song:
249-
self.show()
266+
self._show_if_allowed()
250267
if not self._hovered:
251268
self._start_compact()
252269

@@ -348,16 +365,29 @@ def _compact_text_max_width(self):
348365
return max(
349366
60,
350367
(
351-
EXPANDED_WIDTH - PADDING_LEFT - PADDING_RIGHT -
368+
COMPACT_MAX_WIDTH - PADDING_LEFT - PADDING_RIGHT -
352369
COVER_COMPACT - CONTENT_SPACING
353370
),
354371
)
355372

373+
def _trailing_width(self):
374+
widget = self._trailing_widget
375+
if widget is None:
376+
return 0
377+
return CONTENT_SPACING + max(widget.width(), widget.sizeHint().width())
378+
379+
def _expanded_width(self):
380+
return EXPANDED_WIDTH + self._trailing_width()
381+
356382
def _playback_progress(self):
357383
if not self._duration:
358384
return 0
359385
return max(0.0, min(1.0, self._position / self._duration))
360386

387+
def _show_if_allowed(self):
388+
if not self._visibility_suppressed:
389+
self.show()
390+
361391
def _draw_progress_background(self, painter, rect, radius, palette):
362392
progress = self._playback_progress()
363393
if self._expansion_state != "expanded" or progress <= 0:
@@ -437,7 +467,7 @@ def _start_expand(self):
437467
return # already expanding
438468

439469
self._anim_from = self.width()
440-
self._anim_to = EXPANDED_WIDTH
470+
self._anim_to = self._expanded_width()
441471
self._anim_current = self._anim_from
442472
self._animating = True
443473
if not self._anim_timer.isActive():
@@ -495,6 +525,8 @@ def _switch_to_expanded(self):
495525
self._lyric_label.hide()
496526
self._song_label.show()
497527
self._control_widget.show()
528+
if self._trailing_widget is not None:
529+
self._trailing_widget.show()
498530
self._cover.setFixedSize(COVER_EXPANDED, COVER_EXPANDED)
499531
self._expansion_state = "expanded"
500532

@@ -505,6 +537,8 @@ def _switch_to_compact(self):
505537
self._lyric_label.show()
506538
self._song_label.hide()
507539
self._control_widget.hide()
540+
if self._trailing_widget is not None:
541+
self._trailing_widget.hide()
508542
self._cover.setFixedSize(COVER_COMPACT, COVER_COMPACT)
509543
self._expansion_state = "compact"
510544
self._set_compact_text(
@@ -513,9 +547,9 @@ def _switch_to_compact(self):
513547

514548
def _finalize_state(self):
515549
"""Finalize the state after animation completes."""
516-
if self._anim_to == EXPANDED_WIDTH:
550+
if self._anim_to == self._expanded_width():
517551
self._switch_to_expanded()
518-
self.setFixedWidth(EXPANDED_WIDTH)
552+
self.setFixedWidth(self._expanded_width())
519553
else:
520554
self._switch_to_compact()
521555
self.setFixedWidth(self._calc_compact_width())

feeluown/gui/debug.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ def create_mini_player_debug_app():
118118
playlist=_MiniPlayerDebugPlaylist(),
119119
live_lyric=_MiniPlayerDebugLiveLyric(),
120120
library=SimpleNamespace(get=lambda _source: None),
121+
ai=None,
121122
)
122123

123124

feeluown/gui/uimain/ai_chat.py

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import logging
22
import time
33
from dataclasses import dataclass
4+
from typing import TYPE_CHECKING
45
from urllib.parse import parse_qs, urlencode, urlparse
56

67
from PyQt6.QtCore import QEvent, QMargins, QRectF, QSize, Qt, QTimer, pyqtSignal
@@ -26,7 +27,6 @@
2627
)
2728

2829
from feeluown.ai import SongSuggestion
29-
from feeluown.app.gui_app import GuiApp
3030
from feeluown.gui.components.search import create_search_result_view
3131
from feeluown.gui.helpers import IS_MACOS, secondary_text_color
3232
from feeluown.gui.widgets import PlayButton, PlusButton
@@ -52,6 +52,9 @@
5252
from feeluown.i18n import t
5353
from feeluown.utils import aio
5454

55+
if TYPE_CHECKING:
56+
from feeluown.app.gui_app import GuiApp
57+
5558
logger = logging.getLogger(__name__)
5659

5760

@@ -461,16 +464,35 @@ class AIChatBox(QWidget):
461464
working_state_changed = pyqtSignal(bool)
462465
playlist_sidebar_requested = pyqtSignal()
463466

464-
def __init__(self, app, parent=None):
467+
def __init__(
468+
self,
469+
app,
470+
parent=None,
471+
status_widget=True,
472+
history_fixed_height=None,
473+
input_editor_min_height=80,
474+
input_editor_max_height=300,
475+
input_contents_margins=(14, 10, 12, 6),
476+
placeholder_text=None,
477+
):
465478
super().__init__(parent=parent)
466479
self._app = app
467480
self.copilot = self._app.ai.get_copilot()
481+
self._placeholder_text = placeholder_text
468482

469483
self.history_widget = ChatHistoryWidget(self)
470-
self._dynamic_island = DynamicIslandStatusBar(app)
484+
if history_fixed_height is not None:
485+
self.history_widget.setFixedHeight(history_fixed_height)
486+
if status_widget is True:
487+
self._dynamic_island = DynamicIslandStatusBar(app)
488+
else:
489+
self._dynamic_island = status_widget
471490
self.input_widget = ChatInputWidget(
472491
self,
473492
status_widget=self._dynamic_island,
493+
editor_min_height=input_editor_min_height,
494+
editor_max_height=input_editor_max_height,
495+
contents_margins=input_contents_margins,
474496
)
475497
self._collecting_artifacts = False
476498
self._pending_artifacts = []
@@ -605,7 +627,9 @@ def _get_active_ai_radio(self):
605627
return self._app.ai.get_active_radio()
606628

607629
def _refresh_context(self, *_):
608-
if self._get_active_ai_radio() is not None:
630+
if self._placeholder_text is not None:
631+
self.input_widget.set_placeholder(self._placeholder_text)
632+
elif self._get_active_ai_radio() is not None:
609633
self.input_widget.set_placeholder(t("ai-radio-input-placeholder"))
610634
else:
611635
self.input_widget.set_placeholder(t("ai-chat-input-placeholder"))

0 commit comments

Comments
 (0)