Skip to content

Replace album popup menu with a full action sheet#190

Merged
phanan merged 5 commits into
masterfrom
feat/album-action-sheet
May 2, 2026
Merged

Replace album popup menu with a full action sheet#190
phanan merged 5 commits into
masterfrom
feat/album-action-sheet

Conversation

@phanan
Copy link
Copy Markdown
Member

@phanan phanan commented May 2, 2026

Summary

Long-pressing an AlbumCard or AlbumRow now opens an iOS-style action sheet (modeled after PlayableActionSheet) instead of the cramped single-item popup menu added in #184.

            Thumbnail
            Album name
            Artist name
   ───────────────────────────────────
   Favorite  │  Play All  │  Shuffle All
   ───────────────────────────────────
   Play Next
   Play Last
   ─── (only when applicable)
   Go to Artist
   ─── (only when canEdit)
   Edit…

Behavior

  • Favorite — state-driven label (FavoriteUndo Favorite); toggles via the new AlbumProvider.toggleFavorite (POST /favorites/toggle with { type: 'albums', id }), optimistic with rollback on failure.
  • Play All / Shuffle All — fetch the album's songs via PlayableProvider.fetchForAlbum, then audioHandler.replaceQueue(songs, shuffle: …).
  • Play Next / Play Last — fetch + queueAfterCurrent / queueToBottom per song, same overlay copy as the song-level action sheet.
  • Go to Artist — hidden when the album is by Unknown Artist or Various Artists.
  • Edit… — shown only when album.canEdit; opens the existing showEditAlbumDialog.

Model

  • Album.favorite (parsed from json['favorite'], defaults false); Album.merge propagates it so synced instances stay in sync.

Removed

  • lib/ui/widgets/album_actions_menu.dart (the old popup) and its widgets.dart export. The popup is replaced wholesale by the action sheet.

Test plan

  • flutter test — full suite green (276/276)
  • New test/ui/screens/album_action_sheet_test.dart covers the sheet's structure (album name / artist name / quick actions / state-driven Favorite label / Edit visibility / Go-to-Artist visibility for Unknown / Various / normal artists)
  • album_test.dart adds branches for favorite parsing + merge propagation
  • flutter analyze clean on touched files (the 3 pre-existing warnings in albums.dart are out of scope)
  • Smoke test: long-press an album card / row → confirm sheet structure; tap each quick action and list row; verify the Favorite icon flips after toggle and the album appears in / disappears from /favorites

Summary by CodeRabbit

  • New Features

    • Mark albums as favorites with instant UI feedback; favorites are available from a new album action sheet (long-press) that also offers Play All, Shuffle All, Play Next, Play Last, artist navigation, and Edit when allowed. Favorite option is shown only when supported by the server version.
  • Tests

    • Added tests for album favorite parsing/merge, the action sheet UI and conditional items, favorite toggle behavior, and related playback queue actions.

Long-pressing an AlbumCard or AlbumRow now opens an iOS-style action
sheet (modeled after PlayableActionSheet) instead of the cramped
inline popup menu. Structure:

  Thumbnail
  Album name
  Artist name
  --------------------------------------
  [ Favorite | Play All | Shuffle All ]
  --------------------------------------
  Play Next
  Play Last
  --- (only when applicable)
  Go to Artist
  --- (only when canEdit)
  Edit…

- Favorite: state-driven label (Favorite / Undo Favorite); toggles
  via the new AlbumProvider.toggleFavorite (POST /favorites/toggle
  with { type: 'albums', id }), optimistic with restore on failure.
- Play All / Shuffle All: fetch the album's songs via
  PlayableProvider.fetchForAlbum, then audioHandler.replaceQueue
  (with shuffle for Shuffle All).
- Play Next / Play Last: fetch + queueAfterCurrent / queueToBottom
  per song, with the same overlay messages used by the song-level
  action sheet.
- Go to Artist: hidden for Unknown / Various Artists.
- Edit…: only shown when album.canEdit; opens showEditAlbumDialog.

Model:
- Album.favorite (parsed from json['favorite'], default false).
  Album.merge propagates it.

Removed lib/ui/widgets/album_actions_menu.dart (the old single-item
popup) and its widgets.dart export — no longer used.

Tests:
- New test/ui/screens/album_action_sheet_test.dart covers the sheet's
  structure (thumbnail / names / quick actions / state-driven label /
  Edit visibility / Go-to-Artist visibility).
- album_test.dart adds branches for the favorite field
  (parsing + merge propagation).
- Drop the old album_card_test long-press group and album_row_test
  — they targeted the inline menu that's gone.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 2, 2026

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

This PR introduces album favoriting by adding a favorite field to the Album model, implementing a toggleFavorite() provider method, and replacing the album context menu with a new AlbumActionSheet bottom sheet that supports toggling favorite status alongside playing, shuffling, and enqueueing tracks.

Changes

Album Favoriting & Action Sheet

Layer / File(s) Summary
Data Model
lib/models/album.dart
Album gains a favorite: bool field (defaulting to false), populated from JSON, and merged from remote albums.
Provider Logic
lib/providers/album_provider.dart
toggleFavorite(Album album) method optimistically flips the favorite flag, notifies listeners, calls the API endpoint, and reverts on failure.
New UI Component
lib/ui/screens/album_action_sheet.dart
AlbumActionSheet bottom sheet displays album info with quick actions (Favorite, Play All, Shuffle All), enqueue options (Play Next, Play Last with overlay feedback), and conditional actions ("Go to Artist" and "Edit…" based on artist name and edit permissions).
Barrel Exports
lib/ui/screens/screens.dart
album_action_sheet.dart is exported from the screens module.
Screen Integration
lib/ui/screens/albums.dart
AlbumRow now calls showAlbumActionSheet() on long-press instead of the old context menu; position tracking removed.
Widget Integration
lib/ui/widgets/album_card.dart
AlbumCard long-press now triggers showAlbumActionSheet() instead of showAlbumActionsMenu().
Removed Components
lib/ui/widgets/album_actions_menu.dart, lib/ui/widgets/widgets.dart
Old context menu implementation deleted; corresponding export removed from barrel file.
Model Tests
test/models/album_test.dart
Tests added for favorite field parsing and merging behavior.
New Action Sheet Tests
test/ui/screens/album_action_sheet_test.dart, test/ui/screens/album_action_sheet_test.mocks.dart
Comprehensive widget tests verify rendering of album info, quick actions, conditional UI elements ("Undo Favorite" toggle, "Edit…", "Go to Artist" visibility rules).
Updated Widget Tests
test/ui/widgets/album_card_test.dart, test/ui/widgets/album_card_test.mocks.dart, test/ui/widgets/album_row_test.dart
Tests for the old context menu removed; mock generation simplified for album_card_test.dart; album_row_test.dart fully deleted as its long-press assertions no longer apply.

Sequence Diagram

sequenceDiagram
    participant User
    participant AlbumCard as Album Card/Row
    participant ActionSheet as Album Action Sheet
    participant AlbumProvider as Album Provider
    participant API as API Endpoint

    User->>AlbumCard: Long-press album
    AlbumCard->>ActionSheet: Show album action sheet
    ActionSheet->>User: Display actions (Favorite, Play, etc.)
    
    User->>ActionSheet: Tap "Favorite"
    ActionSheet->>AlbumProvider: toggleFavorite(album)
    AlbumProvider->>AlbumProvider: Flip favorite optimistically
    AlbumProvider->>ActionSheet: notifyListeners()
    ActionSheet->>User: Update UI (show "Undo Favorite")
    
    AlbumProvider->>API: POST /favorites/toggle
    alt Success
        API->>AlbumProvider: 200 OK
    else Failure
        API->>AlbumProvider: Error
        AlbumProvider->>AlbumProvider: Revert favorite flag
        AlbumProvider->>ActionSheet: notifyListeners()
        ActionSheet->>User: Revert UI state
    end

    User->>ActionSheet: Tap "Play All"
    ActionSheet->>AlbumProvider: fetchForAlbum()
    AlbumProvider->>API: Fetch playables
    API->>AlbumProvider: Tracks list
    ActionSheet->>User: Replace queue & play
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 A favorite hop, a sheet that flows,
Action buttons in a row—
Tap and toggle, play with glee,
Albums blessed with new decree!
The context menu bows away,
Action sheets now hold the day.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: replacing the album popup menu with a full action sheet, which is the primary objective of this PR.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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/album-action-sheet

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
Review rate limit: 7/8 reviews remaining, refill in 7 minutes and 30 seconds.

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

phanan added 2 commits May 2, 2026 11:03
The koel server's FavoriteableType enum uses singular values
(album / artist / podcast / radio-station / playable). I sent the
plural 'albums', which got rejected as 422 Unprocessable.
Adds 5 new tests grouped under 'actions' that verify each tap
behavior end-to-end:

- Favorite → AlbumProvider.toggleFavorite(album)
- Play All → PlayableProvider.fetchForAlbum + audioHandler.replaceQueue
- Shuffle All → fetchForAlbum + replaceQueue with shuffle: true
- Play Next → queueAfterCurrent for each song
- Play Last → queueToBottom for each song

Mirrors the mock setup used by playable_action_sheet_test
(MockKoelAudioHandler assigned to app.audioHandler, with mediaItem
stub).

Note: testing the actual /favorites/toggle request body shape
(which is what would have caught the album/albums 422 bug) needs
HTTP-layer mocking the codebase doesn't have today; the
toggleFavorite test verifies the provider method is called with the
right album, which is the most we can pin without that infra.
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: 2

🧹 Nitpick comments (1)
test/ui/screens/album_action_sheet_test.dart (1)

176-212: Consider verifyInOrder to lock down song-queuing sequence for Play Next.

The loop of verify calls on lines 190–193 and 208–211 confirms each song is queued exactly once but does not verify call order. For Play Next, this matters: queueAfterCurrent inserts at currentQueueIndex + 1, so iterating songs forward (song1 → song2 → song3) produces reverse queue order (song3 plays first). The current assertion is blind to this behavior; a future regression in iteration order would pass silently.

Using verifyInOrder documents the expected call sequence and prevents regressions. For Play Last, queueToBottom appends to the end, so forward iteration naturally produces correct order—still worth asserting for consistency.

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

In `@test/ui/screens/album_action_sheet_test.dart` around lines 176 - 212, The
tests currently verify each song is queued exactly once but not the call order;
update the "tapping Play Next queues each song after current" test to use
verifyInOrder with a list of expected calls to
audioHandlerMock.queueAfterCurrent(...) in the exact sequence produced from
songs (use songs[0], songs[1], songs[2]) instead of the per-song loop, and
similarly replace the loop in the "tapping Play Last queues each song to the
bottom" test with verifyInOrder asserting audioHandlerMock.queueToBottom(...)
calls in forward order; keep the existing
playableProviderMock.fetchForAlbum(album.id), mount(tester, album), and the pump
calls unchanged and retain verifyNever checks for the opposite method.
🤖 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/album_action_sheet.dart`:
- Around line 147-148: The Play Next loop uses audioHandler.queueAfterCurrent
for each item which keeps inserting at the same index and reverses the album;
change the loop in the Play Next action to iterate over songs.reversed (e.g.,
for (final song in songs.reversed) { await audioHandler.queueAfterCurrent(song);
}) so each insertion preserves the original album order; keep Play Last
(queueToBottom) unchanged.
- Around line 145-155: The async onTap handlers for the Play Next and Play Last
buttons perform awaits (e.g., calling _fetchSongs() and
audioHandler.queueAfterCurrent / queueLast) and then call showOverlay(context,
...), which can run after the widget is unmounted; add a mounted guard (if
(!mounted) return;) immediately before each showOverlay call in the
PlayableActionButton onTap handlers to avoid using a disposed context (reference
the _fetchSongs function, audioHandler.queueAfterCurrent / queueLast calls, and
the showOverlay invocation when applying the change).

---

Nitpick comments:
In `@test/ui/screens/album_action_sheet_test.dart`:
- Around line 176-212: The tests currently verify each song is queued exactly
once but not the call order; update the "tapping Play Next queues each song
after current" test to use verifyInOrder with a list of expected calls to
audioHandlerMock.queueAfterCurrent(...) in the exact sequence produced from
songs (use songs[0], songs[1], songs[2]) instead of the per-song loop, and
similarly replace the loop in the "tapping Play Last queues each song to the
bottom" test with verifyInOrder asserting audioHandlerMock.queueToBottom(...)
calls in forward order; keep the existing
playableProviderMock.fetchForAlbum(album.id), mount(tester, album), and the pump
calls unchanged and retain verifyNever checks for the opposite method.
🪄 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: ea7d95a5-f8a8-433f-a813-6efbc9f3a336

📥 Commits

Reviewing files that changed from the base of the PR and between 2c5a369 and 9385697.

📒 Files selected for processing (14)
  • lib/models/album.dart
  • lib/providers/album_provider.dart
  • lib/ui/screens/album_action_sheet.dart
  • lib/ui/screens/albums.dart
  • lib/ui/screens/screens.dart
  • lib/ui/widgets/album_actions_menu.dart
  • lib/ui/widgets/album_card.dart
  • lib/ui/widgets/widgets.dart
  • test/models/album_test.dart
  • test/ui/screens/album_action_sheet_test.dart
  • test/ui/screens/album_action_sheet_test.mocks.dart
  • test/ui/widgets/album_card_test.dart
  • test/ui/widgets/album_card_test.mocks.dart
  • test/ui/widgets/album_row_test.dart
💤 Files with no reviewable changes (3)
  • lib/ui/widgets/album_actions_menu.dart
  • lib/ui/widgets/widgets.dart
  • test/ui/widgets/album_row_test.dart

Comment thread lib/ui/screens/album_action_sheet.dart
Comment thread lib/ui/screens/album_action_sheet.dart Outdated
phanan added 2 commits May 2, 2026 11:22
The favorite/unfavorite endpoint for non-song entities (album,
artist, podcast, radio station) only landed in koel 7.11.0; on older
servers the request would 404. Hide the quick action below that
version so the row collapses to Play All | Shuffle All.

- New Feature.favoriteEntities flag mapped to '7.11.0'.
- AlbumActionSheet checks Feature.favoriteEntities.isSupported() and
  conditionally renders the Favorite quick action plus its trailing
  divider.
- Tests:
  - features_test.dart covers the new flag's at-version /
    above-version / below-version / unset branches.
  - album_action_sheet_test.dart sets apiVersion=7.11.0 in setUp so
    the existing Favorite tests stay green, plus a new branch for
    the feature-off case (Favorite hidden, Play All / Shuffle All
    still present).
- Play Next iterated songs forward and called queueAfterCurrent on
  each. Since queueAfterCurrent inserts at a fixed 'after current'
  index, that landed the album in reverse order in the queue.
  Iterate songs.reversed so the queue ends up in album order.
- Add `if (!mounted) return;` before showOverlay in both Play Next
  and Play Last paths. The fetchForAlbum call is network-backed so
  the State could be disposed by the time the await resolves.
- Tighten the related tests to use verifyInOrder with the exact
  expected sequence — locks the reversed-iteration as the right
  contract for Play Next, forward order for Play Last.
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/album_action_sheet.dart (2)

151-162: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Missing mounted guards before showOverlay in both "Play Next" and "Play Last".

PlayableActionButton dismisses the sheet immediately on tap (default hideSheetOnTap: true), unmounting _AlbumActionSheetState. By the time the awaits complete and showOverlay(context, …) is reached, context is invalid. This was flagged in a previous review and remains unaddressed.

🛡️ Proposed fix — add mounted guard before each showOverlay call
                         for (final song in songs.reversed) {
                           await audioHandler.queueAfterCurrent(song);
                         }
+                        if (!mounted) return;
                         showOverlay(
                           context,
                           icon: CupertinoIcons.arrow_right_circle_fill,
                           caption: 'Queued',
                           message: 'To be played next.',
                         );
                         for (final song in songs) {
                           await audioHandler.queueToBottom(song);
                         }
+                        if (!mounted) return;
                         showOverlay(
                           context,
                           icon: CupertinoIcons.arrow_down_right_circle_fill,
                           caption: 'Queued',
                           message: 'Queued to bottom.',
                         );

Also applies to: 170-181

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

In `@lib/ui/screens/album_action_sheet.dart` around lines 151 - 162, In
_AlbumActionSheetState locate the onTap handlers for the "Play Next" and "Play
Last" PlayableActionButton implementations (the blocks that await _fetchSongs()
and call audioHandler.queueAfterCurrent(...) and audioHandler.queueEnd(...)),
and add a mounted guard before calling showOverlay(context, ...); i.e., after
the await loop finishes check if (!mounted) return; (or if (mounted) {
showOverlay(...) }) so you don't call showOverlay with an invalid context after
the sheet has been dismissed.

153-155: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Play Next queues album songs in reverse order.

Each queueAfterCurrent inserts at currentQueueIndex + 1. Successive inserts push earlier songs back, reversing the intended album track order. This was flagged in a previous review and remains unaddressed.

🐛 Proposed fix — iterate songs.reversed
-                        for (final song in songs) {
+                        for (final song in songs.reversed) {
                           await audioHandler.queueAfterCurrent(song);
                         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/ui/screens/album_action_sheet.dart` around lines 153 - 155, The loop that
calls audioHandler.queueAfterCurrent(song) is inserting each song at
currentQueueIndex + 1 so iterating songs in forward order reverses the album;
change the iteration to append in original order by iterating songs.reversed (or
otherwise ensure you insert from last to first) when calling queueAfterCurrent
on audioHandler so tracks keep their intended album order; update the loop that
references songs and queueAfterCurrent accordingly.
🤖 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/album_action_sheet.dart`:
- Around line 151-162: In _AlbumActionSheetState locate the onTap handlers for
the "Play Next" and "Play Last" PlayableActionButton implementations (the blocks
that await _fetchSongs() and call audioHandler.queueAfterCurrent(...) and
audioHandler.queueEnd(...)), and add a mounted guard before calling
showOverlay(context, ...); i.e., after the await loop finishes check if
(!mounted) return; (or if (mounted) { showOverlay(...) }) so you don't call
showOverlay with an invalid context after the sheet has been dismissed.
- Around line 153-155: The loop that calls audioHandler.queueAfterCurrent(song)
is inserting each song at currentQueueIndex + 1 so iterating songs in forward
order reverses the album; change the iteration to append in original order by
iterating songs.reversed (or otherwise ensure you insert from last to first)
when calling queueAfterCurrent on audioHandler so tracks keep their intended
album order; update the loop that references songs and queueAfterCurrent
accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a5831c88-88f6-42d0-85d1-445703618959

📥 Commits

Reviewing files that changed from the base of the PR and between 9385697 and 00a7d19.

📒 Files selected for processing (4)
  • lib/ui/screens/album_action_sheet.dart
  • lib/utils/features.dart
  • test/ui/screens/album_action_sheet_test.dart
  • test/utils/features_test.dart
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/ui/screens/album_action_sheet_test.dart

@phanan phanan merged commit 4c1ae08 into master May 2, 2026
1 of 2 checks passed
@phanan phanan deleted the feat/album-action-sheet branch May 2, 2026 09:32
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