Skip to content

Inline queue in Now Playing screen#148

Merged
phanan merged 1 commit into
masterfrom
feat/inline-queue-now-playing
Mar 29, 2026
Merged

Inline queue in Now Playing screen#148
phanan merged 1 commit into
masterfrom
feat/inline-queue-now-playing

Conversation

@phanan
Copy link
Copy Markdown
Member

@phanan phanan commented Mar 28, 2026

Summary

  • The Now Playing screen is now a single scrollable view — swipe up past the controls to reveal the queue
  • Removed the queue icon button from the bottom row (no more separate Queue screen navigation)
  • Queue features preserved: reorderable (drag handles), swipe-to-delete, clear button
  • Queue header with "Queue" title and "Clear" action

Test plan

  • Open Now Playing screen — verify controls look the same as before
  • Swipe up — queue list appears below the controls
  • Reorder songs in the queue via drag handles
  • Swipe left on a queued song to remove it
  • Tap "Clear" to empty the queue
  • Swipe down on the Now Playing screen to dismiss it (still works)

Summary by CodeRabbit

  • New Features

    • Inline queue on Now Playing with a “Queue” header and conditional “Clear” action.
    • Drag to reorder queue items and swipe to remove individual items.
    • Displays “No items queued.” when empty; removed separate queue navigation button.
  • UI/UX Improvements

    • Now Playing and queue combined into a single, scrollable layout that adapts to queue content.
    • Scroll-driven pull-to-dismiss replaces prior vertical-drag dismissal for smoother, more consistent behavior.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 28, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Subscribed to audioHandler.queue, resolved MediaItem IDs to Playable via provider into _queuePlayables; replaced vertical-drag dismissal with scroll-driven overscroll dismissal using a NotificationListener and _dragOffset; converted layout to CustomScrollView + slivers and added an inline reorderable, dismissible queue; added _scrollController and updated dispose order.

Changes

Cohort / File(s) Summary
Now Playing — UI & Queue Behavior
lib/ui/screens/now_playing.dart
Subscribe/map audioHandler.queue_queuePlayables via provider; add _scrollController, _isDismissing, _dragOffset and dispose it before unsubscribeAll(); replace Column with CustomScrollView/slivers (SliverToBoxAdapter, conditional queue header); inline SliverReorderableList for queue; wrap rows in Dismissible (end-to-start) calling audioHandler.removeQueueItemAt; reorder calls audioHandler.moveQueueItem; replace vertical-drag gesture handlers with scroll-driven dismissal via NotificationListener<ScrollNotification>.

Sequence Diagram(s)

sequenceDiagram
    participant UI as NowPlayingScreen
    participant Provider as PlayableProvider
    participant Audio as AudioHandler

    UI->>Audio: subscribe to queue stream
    Audio-->>UI: queue update (List<MediaItem>)
    UI->>Provider: byId(id) for each MediaItem
    Provider-->>UI: Playable or null
    UI->>UI: build _queuePlayables (filter nulls) & setState

    alt user swipes a queue row (end-to-start)
        UI->>Audio: removeQueueItemAt(index)
        Audio-->>UI: updated queue stream
    end

    alt user reorders a queue row
        UI->>Audio: moveQueueItem(fromIndex, toIndex)
        Audio-->>UI: updated queue stream
    end

    alt user overscrolls to dismiss screen
        UI->>UI: NotificationListener accumulates _dragOffset/_isDismissing
        UI-->>UI: pop route when threshold met or reset offset
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐇
I hop where slivers softly play,
map each id to light array,
swipe a leaf and off it goes,
nudge a song where reorder flows,
I tidy queues and bound away.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Inline queue in Now Playing screen' directly and concisely summarizes the main change: integrating the queue functionality into the Now Playing screen instead of having a separate Queue screen.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/inline-queue-now-playing

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/ui/screens/now_playing.dart`:
- Around line 275-279: Rows use ValueKey(_queuePlayables[index]) which collides
when the same Playable object (or id) appears multiple times; change the key for
PlayableRow (and any surrounding Dismissible/reorderable child) to a per-entry
unique key — e.g., combine the playable id with the queue position or use the
queue entry's unique id (e.g., "${playable.id}-$index" or an ObjectKey built
from the entry) so each queue entry has a distinct key and row state/reordering
works correctly.
- Around line 50-57: The current subscriber filters out nulls from
_playableProvider.byId which shifts indices; change the state to preserve null
placeholders so indices remain aligned with audioHandler.queue: update
_queuePlayables to be List<Playable?> and in the subscribe callback set
_queuePlayables = value.map((item) => _playableProvider.byId(item.id)).toList()
(remove the .where/.cast). Also audit any dismiss/reorder handlers (the
callbacks that use _queuePlayables indices) to tolerate nullable entries (guard
before using a Playable) so they act on the correct queue index even when the
Playable is unresolved.
- Around line 114-116: The GestureDetector wrapping the view (handlers:
_onVerticalDragUpdate and _onVerticalDragEnd) competes with the inner
CustomScrollView for vertical drag gestures; replace or refactor this pattern to
use Flutter's DraggableScrollableSheet so the sheet and its scrollable child
share gesture/scroll coordination and a shared ScrollController, or if you must
keep GestureDetector, implement coordinated gesture handling by wiring a
ScrollController to CustomScrollView and conditionally delegating vertical drags
(e.g., only trigger _onVerticalDrag* when the scroll position is at the top and
the drag is downward); update references in the now_playing.dart widget to use
DraggableScrollableSheet (or the conditional delegation) and ensure handlers
_onVerticalDragUpdate/_onVerticalDragEnd are removed or adapted accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5a098579-ba99-4ca6-9abf-99e3517c675e

📥 Commits

Reviewing files that changed from the base of the PR and between 836880b and f0e9606.

📒 Files selected for processing (1)
  • lib/ui/screens/now_playing.dart

Comment on lines +50 to +57
subscribe(audioHandler.queue.listen((List<MediaItem> value) {
setState(() {
_queuePlayables = value
.map((item) => _playableProvider.byId(item.id))
.where((p) => p != null)
.cast<Playable>()
.toList();
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don’t filter queue entries out of state.

PlayableProvider.byId() can return null here. Because this widget only refreshes when audioHandler.queue emits, any unresolved MediaItem disappears from the inline queue until the queue changes again. That also shifts the rendered indices away from the real queue indices, so the later dismiss/reorder callbacks can mutate the wrong item.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/ui/screens/now_playing.dart` around lines 50 - 57, The current subscriber
filters out nulls from _playableProvider.byId which shifts indices; change the
state to preserve null placeholders so indices remain aligned with
audioHandler.queue: update _queuePlayables to be List<Playable?> and in the
subscribe callback set _queuePlayables = value.map((item) =>
_playableProvider.byId(item.id)).toList() (remove the .where/.cast). Also audit
any dismiss/reorder handlers (the callbacks that use _queuePlayables indices) to
tolerate nullable entries (guard before using a Playable) so they act on the
correct queue index even when the Playable is unresolved.

Comment thread lib/ui/screens/now_playing.dart Outdated
Comment on lines +275 to +279
key: ValueKey(_queuePlayables[index]),
child: PlayableRow(
index: index,
key: ValueKey(_queuePlayables[index]),
playable: _queuePlayables[index],
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

These row keys collide when the same track is queued twice.

PlayableProvider.byId() returns the same cached Playable for a given id, so two queue entries for the same song end up with identical ValueKeys here. Dismissible and reorderable children need unique per-entry keys; otherwise you can get assertions or row state being reused for the wrong item.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/ui/screens/now_playing.dart` around lines 275 - 279, Rows use
ValueKey(_queuePlayables[index]) which collides when the same Playable object
(or id) appears multiple times; change the key for PlayableRow (and any
surrounding Dismissible/reorderable child) to a per-entry unique key — e.g.,
combine the playable id with the queue position or use the queue entry's unique
id (e.g., "${playable.id}-$index" or an ObjectKey built from the entry) so each
queue entry has a distinct key and row state/reordering works correctly.

@phanan phanan force-pushed the feat/inline-queue-now-playing branch 4 times, most recently from ea7934f to f9f8456 Compare March 28, 2026 20:23
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
lib/ui/screens/now_playing.dart (2)

33-33: ⚠️ Potential issue | 🟠 Major

Keep the inline queue state 1:1 with audioHandler.queue.

PlayableProvider.byId() is nullable, and filtering misses out here makes unresolved items disappear until the queue mutates again. It also shifts rendered indices away from the real queue, so Line 269 and Line 288 can remove or reorder the wrong entry because removeQueueItemAt() / moveQueueItem() index into the full queue (lib/audio_handler.dart:295-310). Preserve nullable placeholders or keep the underlying MediaItem alongside the resolved Playable.

Also applies to: 50-57

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/ui/screens/now_playing.dart` at line 33, The inline queue stored in
_queuePlayables is being filtered by PlayableProvider.byId(), dropping nulls and
desynchronizing indices from audioHandler.queue; change the construction/updates
of _queuePlayables to preserve placeholders for unresolved entries (e.g., keep
null or a sentinel for entries where PlayableProvider.byId(id) returns null) or
maintain a parallel list of the original MediaItem entries so the index mapping
is 1:1 with audioHandler.queue; ensure removeQueueItemAt() and moveQueueItem()
continue to index into the full queue (audioHandler.queue) while using the
preserved placeholders or MediaItem list to render items and resolve Playable
lazily.

279-283: ⚠️ Potential issue | 🟠 Major

Use a per-entry key for both queue widgets.

ValueKey(_queuePlayables[index]) collides as soon as the same track is queued twice, so Dismissible and PlayableRow can reuse state for the wrong row or hit duplicate-key assertions. Once you keep the actual queue entry in state, key both widgets from that entry instead of the resolved Playable. Quick repro: queue the same song twice, then dismiss/reorder both rows.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/ui/screens/now_playing.dart` around lines 279 - 283, The current keys use
ValueKey(_queuePlayables[index]) which collides when the same Playable is queued
twice; update both the Dismissible and the PlayableRow to use the unique queue
entry object (or its unique id) as the key instead of the resolved Playable.
Locate where Dismissible (the parent) and PlayableRow are instantiated and
replace ValueKey(_queuePlayables[index]) with a per-entry key such as
ValueKey(queueEntry) or ValueKey(queueEntry.id) using the queue entry variable
you hold in state (the same entry reference for both widgets) so each queued
instance has a unique key.
🧹 Nitpick comments (1)
lib/ui/screens/now_playing.dart (1)

137-143: Prefer a minimum height here instead of a fixed height.

The controls section is hard-capped at screenHeight - 80, so on short viewports or larger text sizes the Column can overflow even though the page itself scrolls. Using a minimum height keeps the peek effect on normal screens but still lets the controls grow when needed. Quick check: test this on a small device / landscape with larger text scaling.

Suggested tweak
-                      child: Container(
-                        // Show the queue header + half of the first song peeking
-                        // when the queue has items; full height when empty.
-                        height: _queuePlayables.isEmpty
-                            ? screenHeight
-                            : screenHeight - 80,
+                      child: Container(
+                        // Show the queue header + half of the first song peeking
+                        // when the queue has items; full height when empty.
+                        constraints: BoxConstraints(
+                          minHeight: _queuePlayables.isEmpty
+                              ? screenHeight
+                              : screenHeight - 80,
+                        ),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/ui/screens/now_playing.dart` around lines 137 - 143, Replace the fixed
height on the Container that uses _queuePlayables and screenHeight with a
minimum height constraint so the controls can grow on small viewports or large
text sizes; specifically, remove the height: ... line in the Container and use
constraints (BoxConstraints with minHeight set to _queuePlayables.isEmpty ?
screenHeight : screenHeight - 80) so the peek effect remains but the Column can
expand when needed while keeping the existing padding and layout.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@lib/ui/screens/now_playing.dart`:
- Line 33: The inline queue stored in _queuePlayables is being filtered by
PlayableProvider.byId(), dropping nulls and desynchronizing indices from
audioHandler.queue; change the construction/updates of _queuePlayables to
preserve placeholders for unresolved entries (e.g., keep null or a sentinel for
entries where PlayableProvider.byId(id) returns null) or maintain a parallel
list of the original MediaItem entries so the index mapping is 1:1 with
audioHandler.queue; ensure removeQueueItemAt() and moveQueueItem() continue to
index into the full queue (audioHandler.queue) while using the preserved
placeholders or MediaItem list to render items and resolve Playable lazily.
- Around line 279-283: The current keys use ValueKey(_queuePlayables[index])
which collides when the same Playable is queued twice; update both the
Dismissible and the PlayableRow to use the unique queue entry object (or its
unique id) as the key instead of the resolved Playable. Locate where Dismissible
(the parent) and PlayableRow are instantiated and replace
ValueKey(_queuePlayables[index]) with a per-entry key such as
ValueKey(queueEntry) or ValueKey(queueEntry.id) using the queue entry variable
you hold in state (the same entry reference for both widgets) so each queued
instance has a unique key.

---

Nitpick comments:
In `@lib/ui/screens/now_playing.dart`:
- Around line 137-143: Replace the fixed height on the Container that uses
_queuePlayables and screenHeight with a minimum height constraint so the
controls can grow on small viewports or large text sizes; specifically, remove
the height: ... line in the Container and use constraints (BoxConstraints with
minHeight set to _queuePlayables.isEmpty ? screenHeight : screenHeight - 80) so
the peek effect remains but the Column can expand when needed while keeping the
existing padding and layout.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cbc6e3cd-286d-4e8f-86ff-dfa01f0f5321

📥 Commits

Reviewing files that changed from the base of the PR and between f0e9606 and 4e72d89.

📒 Files selected for processing (1)
  • lib/ui/screens/now_playing.dart

@phanan phanan force-pushed the feat/inline-queue-now-playing branch 2 times, most recently from 593bc2b to 8093949 Compare March 28, 2026 20:36
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
lib/ui/screens/now_playing.dart (2)

163-165: Consider extracting magic number 106 to a named constant.

The calculation screenHeight - 106 represents the queue header plus partial song preview height. A named constant would improve readability and maintainability.

♻️ Suggested change
+  // Height of queue header + half of first visible queue item
+  static const _queuePeekOffset = 106.0;
+
   `@override`
   Widget build(BuildContext context) {
     ...
     height: _queuePlayables.isEmpty
         ? screenHeight
-        : screenHeight - 106,
+        : screenHeight - _queuePeekOffset,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/ui/screens/now_playing.dart` around lines 163 - 165, Extract the magic
number 106 into a clearly named constant (e.g., kQueueHeaderAndPreviewHeight or
queueHeaderPreviewHeight) and use that constant in the calculation instead of
the literal; update the expression in the widget using _queuePlayables and
screenHeight to read screenHeight - kQueueHeaderAndPreviewHeight (or your chosen
name) so the intent of "queue header plus partial song preview height" is
explicit and maintainable.

76-84: Potential state leak if user scrolls back down during drag-to-dismiss.

Once _isDraggingDown is true, any ScrollUpdateNotification with non-null dragDetails continues to update _dragOffset. If the user reverses direction and scrolls back into content (positive delta), _dragOffset will accumulate those deltas rather than reset to normal scroll behavior. Consider resetting _isDraggingDown = false and _dragOffset = 0 when the scroll offset becomes positive.

♻️ Suggested fix
     if (_isDraggingDown && notification is ScrollUpdateNotification) {
+      // If scrolled back into content, exit drag-to-dismiss mode
+      if (_scrollController.offset > 0) {
+        setState(() {
+          _isDraggingDown = false;
+          _dragOffset = 0;
+        });
+        return false;
+      }
       if (notification.dragDetails != null) {
         setState(() {
           _dragOffset = (_dragOffset + notification.dragDetails!.delta.dy)
               .clamp(0.0, double.infinity);
         });
         return true;
       }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/ui/screens/now_playing.dart` around lines 76 - 84, The
ScrollUpdateNotification handler currently only accumulates drag deltas while
_isDraggingDown is true causing _dragOffset to keep growing if the user scrolls
back into content; update the ScrollUpdateNotification branch (the block
checking notification is ScrollUpdateNotification and notification.dragDetails)
to detect when the incoming drag delta or resulting scroll offset is positive
and in that case set _isDraggingDown = false and _dragOffset = 0 (and call
setState) so normal scrolling resumes; keep the existing logic for applying
negative deltas when still dragging down.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@lib/ui/screens/now_playing.dart`:
- Around line 163-165: Extract the magic number 106 into a clearly named
constant (e.g., kQueueHeaderAndPreviewHeight or queueHeaderPreviewHeight) and
use that constant in the calculation instead of the literal; update the
expression in the widget using _queuePlayables and screenHeight to read
screenHeight - kQueueHeaderAndPreviewHeight (or your chosen name) so the intent
of "queue header plus partial song preview height" is explicit and maintainable.
- Around line 76-84: The ScrollUpdateNotification handler currently only
accumulates drag deltas while _isDraggingDown is true causing _dragOffset to
keep growing if the user scrolls back into content; update the
ScrollUpdateNotification branch (the block checking notification is
ScrollUpdateNotification and notification.dragDetails) to detect when the
incoming drag delta or resulting scroll offset is positive and in that case set
_isDraggingDown = false and _dragOffset = 0 (and call setState) so normal
scrolling resumes; keep the existing logic for applying negative deltas when
still dragging down.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 92ff47ba-baa0-4650-a5ae-261c27ed9522

📥 Commits

Reviewing files that changed from the base of the PR and between 4e72d89 and 593bc2b.

📒 Files selected for processing (1)
  • lib/ui/screens/now_playing.dart

@phanan phanan force-pushed the feat/inline-queue-now-playing branch 2 times, most recently from be0e842 to 385c122 Compare March 28, 2026 20:41
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
lib/ui/screens/now_playing.dart (1)

142-144: Consider extracting magic number 106 as a named constant.

The value 106 represents "queue header + half of first song" but is hardcoded. If the queue header or row height changes, this calculation will be incorrect.

♻️ Suggested approach
// At class level or constants file
static const _queuePeekHeight = 106.0; // queue header (56) + half row (~50)

// In build
height: _queuePlayables.isEmpty
    ? screenHeight
    : screenHeight - _queuePeekHeight,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/ui/screens/now_playing.dart` around lines 142 - 144, Extract the magic
number 106 used in the height calculation into a named constant (e.g., static
const double _queuePeekHeight = 106.0) placed at the class level (or a shared
constants file) and replace the literal in the ternary expression that computes
height (the expression referencing _queuePlayables and screenHeight) with
screenHeight - _queuePeekHeight so the meaning ("queue header + half of first
song") is explicit and easy to update when row/header sizes change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/ui/screens/now_playing.dart`:
- Around line 267-268: The dismiss handler currently uses the build-time index
(onDismissed -> audioHandler.removeQueueItemAt(index)), which can be wrong if
the queue stream updates during the dismiss animation; instead capture the
identity of the item and use that to remove it and ensure the Dismissible uses a
stable unique key. Concretely: in the widget build, capture final item =
queue[index] (or its unique id) and change onDismissed to remove by item
identity (or find the current index via queue.indexWhere(id matches) before
calling removeQueueItemAt), and update the Dismissible key to use that stable id
rather than the list index to avoid key collisions.

---

Nitpick comments:
In `@lib/ui/screens/now_playing.dart`:
- Around line 142-144: Extract the magic number 106 used in the height
calculation into a named constant (e.g., static const double _queuePeekHeight =
106.0) placed at the class level (or a shared constants file) and replace the
literal in the ternary expression that computes height (the expression
referencing _queuePlayables and screenHeight) with screenHeight -
_queuePeekHeight so the meaning ("queue header + half of first song") is
explicit and easy to update when row/header sizes change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4240e8bb-bfee-45d3-9c2a-9774c6a88806

📥 Commits

Reviewing files that changed from the base of the PR and between 8093949 and be0e842.

📒 Files selected for processing (1)
  • lib/ui/screens/now_playing.dart

Comment thread lib/ui/screens/now_playing.dart Outdated
@phanan phanan force-pushed the feat/inline-queue-now-playing branch from 385c122 to 298667b Compare March 28, 2026 21:04
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
lib/ui/screens/now_playing.dart (2)

51-57: ⚠️ Potential issue | 🟠 Major

Keep queue entries index-aligned with audioHandler.queue.

At Line 53–57, filtering out unresolved Playables changes list length/order, so UI indices no longer match queue indices used later for remove/reorder operations. This can remove or move the wrong item.

Suggested fix
-  var _queuePlayables = <Playable>[];
+  var _queuePlayables = <Playable?>[];

   subscribe(audioHandler.queue.listen((List<MediaItem> value) {
     setState(() {
       _queuePlayables = value
           .map((item) => _playableProvider.byId(item.id))
-          .where((p) => p != null)
-          .cast<Playable>()
           .toList();
     });
   }));

Then guard nullable entries in row rendering/actions so queue indices remain 1:1 with audioHandler.queue.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/ui/screens/now_playing.dart` around lines 51 - 57, The current mapping in
the audioHandler.queue.listen callback filters out null Playable entries (via
.where and .cast), breaking index alignment with audioHandler.queue; change the
subscription so _queuePlayables captures the same-length list (map to
_playableProvider.byId(item.id) and call toList() without filtering) and make
_queuePlayables a List<Playable?> (nullable) so null slots are preserved; then
update row rendering and action handlers (the widget that reads _queuePlayables
by index and functions like remove/reorder) to guard for null (skip render or
show placeholder and disable actions) so UI indices remain 1:1 with
audioHandler.queue.

274-291: ⚠️ Potential issue | 🟠 Major

Use stable per-entry keys and avoid build-time index removal.

At Line 277 and Line 287/Line 290, two issues combine:

  1. ValueKey(_queuePlayables[index]) collides when duplicate tracks map to the same Playable.
  2. removeQueueItemAt(index) uses a captured index that can be stale by dismissal time.
Suggested fix
 itemBuilder: (BuildContext context, int index) {
-  return Dismissible(
+  final queueEntry = audioHandler.queue.value[index];
+  return Dismissible(
     direction: DismissDirection.endToStart,
     onDismissed: (_) async {
-      await audioHandler.removeQueueItemAt(index);
+      final currentIndex = audioHandler.queue.value.indexOf(queueEntry);
+      if (currentIndex >= 0) {
+        await audioHandler.removeQueueItemAt(currentIndex);
+      }
     },
@@
-    key: ValueKey(_queuePlayables[index]),
+    key: ObjectKey(queueEntry),
     child: PlayableRow(
       index: index,
-      key: ValueKey(_queuePlayables[index]),
+      key: ObjectKey(queueEntry),
       playable: _queuePlayables[index],
       listContext: PlayableListContext.queue,
     ),
   );
 },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/ui/screens/now_playing.dart` around lines 274 - 291, The Dismissible and
PlayableRow are using unstable keys and a captured build-time index: replace
ValueKey(_queuePlayables[index]) with a stable per-item key (e.g.
ValueKey(playable.id) or another unique identifier on the Playable) for both the
Dismissible and PlayableRow, and stop using the captured index inside the
onDismissed closure; instead resolve the current item at dismissal time (e.g.
find the current index by searching _queuePlayables or the audioHandler.queue
for the playable.id) and then call audioHandler.removeQueueItemAt(foundIndex) or
use a remove-by-id/removeQueueItem API if available, referencing Dismissible,
PlayableRow, _queuePlayables, ValueKey, and removeQueueItemAt in your changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/ui/screens/now_playing.dart`:
- Line 296: The UI's onReorder currently calls audioHandler.moveQueueItem which
only mutates queue.value, so update moveQueueItem (in lib/audio_handler.dart) to
also reorder the underlying player's playback sequence (call the player
backend's reorder method or sequence API) so the playback order and displayed
queue stay in sync; perform the player reorder first (or in a
transactional/try/catch) and only emit the updated queue.value after the player
confirms success, and ensure errors roll back or log appropriately so
next/previous behavior matches the inline queue.

---

Duplicate comments:
In `@lib/ui/screens/now_playing.dart`:
- Around line 51-57: The current mapping in the audioHandler.queue.listen
callback filters out null Playable entries (via .where and .cast), breaking
index alignment with audioHandler.queue; change the subscription so
_queuePlayables captures the same-length list (map to
_playableProvider.byId(item.id) and call toList() without filtering) and make
_queuePlayables a List<Playable?> (nullable) so null slots are preserved; then
update row rendering and action handlers (the widget that reads _queuePlayables
by index and functions like remove/reorder) to guard for null (skip render or
show placeholder and disable actions) so UI indices remain 1:1 with
audioHandler.queue.
- Around line 274-291: The Dismissible and PlayableRow are using unstable keys
and a captured build-time index: replace ValueKey(_queuePlayables[index]) with a
stable per-item key (e.g. ValueKey(playable.id) or another unique identifier on
the Playable) for both the Dismissible and PlayableRow, and stop using the
captured index inside the onDismissed closure; instead resolve the current item
at dismissal time (e.g. find the current index by searching _queuePlayables or
the audioHandler.queue for the playable.id) and then call
audioHandler.removeQueueItemAt(foundIndex) or use a remove-by-id/removeQueueItem
API if available, referencing Dismissible, PlayableRow, _queuePlayables,
ValueKey, and removeQueueItemAt in your changes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 69dc596f-ddb6-4002-b38f-1880b289b836

📥 Commits

Reviewing files that changed from the base of the PR and between be0e842 and 298667b.

📒 Files selected for processing (1)
  • lib/ui/screens/now_playing.dart

),
);
},
onReorder: audioHandler.moveQueueItem,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Reorder callback can desync displayed queue from playback order.

At Line 296, onReorder: audioHandler.moveQueueItem delegates to lib/audio_handler.dart Line 304–310, which only mutates queue.value and emits it. If playback sequence is not reordered in the player backend as well, next/previous playback order can diverge from the inline queue order.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/ui/screens/now_playing.dart` at line 296, The UI's onReorder currently
calls audioHandler.moveQueueItem which only mutates queue.value, so update
moveQueueItem (in lib/audio_handler.dart) to also reorder the underlying
player's playback sequence (call the player backend's reorder method or sequence
API) so the playback order and displayed queue stay in sync; perform the player
reorder first (or in a transactional/try/catch) and only emit the updated
queue.value after the player confirms success, and ensure errors roll back or
log appropriately so next/previous behavior matches the inline queue.

@phanan phanan force-pushed the feat/inline-queue-now-playing branch 9 times, most recently from eda6bf5 to bebbd3a Compare March 29, 2026 11:51
Replace the queue icon button with an inline queue list below the
now playing controls. Users can swipe up to see the queue — the
full queue is rendered as a SliverReorderableList below the player
controls in a single scrollable view.

- Reorderable queue with drag handles
- Swipe to remove from queue
- Clear queue button
- Queue header with song count
- Removed the queue navigation icon button
@phanan phanan force-pushed the feat/inline-queue-now-playing branch from bebbd3a to b385bae Compare March 29, 2026 11:57
@phanan phanan merged commit 4980405 into master Mar 29, 2026
2 checks passed
@phanan phanan deleted the feat/inline-queue-now-playing branch March 29, 2026 12:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant