Skip to content

Convert form dialogs to bottom sheets#145

Merged
phanan merged 1 commit into
masterfrom
feat/form-sheets
Mar 28, 2026
Merged

Convert form dialogs to bottom sheets#145
phanan merged 1 commit into
masterfrom
feat/form-sheets

Conversation

@phanan
Copy link
Copy Markdown
Member

@phanan phanan commented Mar 28, 2026

Summary

  • Replace CupertinoAlertDialog with a reusable showFormSheet bottom sheet (85% height) for all create/edit forms
  • Converted: create playlist, edit playlist, create playlist folder, rename folder, add radio station, edit radio station, add podcast
  • New FormTextField widget: hint when unfocused, floating white label on focus, highlight focus border, white text
  • New FormDropdown widget (inline combo box) replacing the folder selection action sheet
  • Subtle submit button styling matching the dark theme

Test plan

  • Create a new playlist — verify sheet slides up, fields show hint, focus shows label + border
  • Edit a playlist — verify pre-filled fields, folder dropdown as combo box, save works
  • Create/rename a playlist folder — verify sheet and submission
  • Add/edit a radio station — verify all fields including public toggle
  • Add a podcast — verify URL field and submission
  • Verify label only appears on focus, disappears on blur
  • Cancel on each sheet — verify it dismisses without side effects

Summary by CodeRabbit

  • New Features

    • Added a reusable form sheet UI with styled form inputs (text field and dropdown) and consistent sheet layout.
    • New sheet-driven entry points for adding podcasts, playlists, folders, and radio stations.
  • Refactor

    • Replaced many dialog-based add/edit flows with the new form sheet—standardized submit validation, submission flow, cancel behavior, and success/error overlays.
    • Unified folder selection via a dropdown component and exported the new sheet widgets.
  • Tests

    • Updated widget tests and timings to reflect dialog → sheet changes.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 28, 2026

Caution

Review failed

Pull request was closed or merged during review

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

Replaces multiple Cupertino alert dialogs with a reusable bottom-sheet form API (showFormSheet) and migrates podcast, playlist, folder, playlist-edit, and radio station add/edit flows to use that sheet; adds FormTextField/FormDropdown widgets, updates exports, and adjusts tests to target sheets.

Changes

Cohort / File(s) Summary
New Form Sheet Component
lib/ui/widgets/form_sheet.dart, lib/ui/widgets/widgets.dart
Add showFormSheet(...), _FormSheet implementation, submit-state handling, FormTextField and FormDropdown widgets; re-export form_sheet.dart from widgets barrel.
Add Podcast
lib/ui/screens/add_podcast_sheet.dart, lib/router.dart
Remove AddPodcastSheet widget; add showAddPodcastDialog(BuildContext) and delegate AppRouter.showAddPodcastSheet to it; move presentation/submission into sheet flow and overlay-based error/success handling.
Playlist & Folder Creation
lib/ui/screens/create_playlist_sheet.dart, lib/ui/screens/create_playlist_folder_sheet.dart
Replace Cupertino dialog flows with showFormSheet calls; introduce canSubmit and onSubmit semantics, trim inputs, show overlays on success/error, and switch fields to FormTextField.
Playlists (edit/rename) & Radio Stations (add/edit)
lib/ui/screens/playlists.dart, lib/ui/screens/radio_stations.dart
Migrate dialog-based rename/edit/add flows to showFormSheet; replace action-sheet folder pickers with FormDropdown, centralize async submission and error overlay handling, and wire field onChanged to sheet state.
Tests Updated
test/ui/screens/create_playlist_folder_test.dart, test/ui/screens/create_playlist_sheet_test.dart
Test harness wrapped in Scaffold; renamed expectations from "dialog"→"sheet"; target TextField instead of CupertinoTextField; added pumpAndSettle() where needed and removed obsolete empty-name test.

Sequence Diagram

sequenceDiagram
    participant Caller as Screen / Caller
    participant API as showFormSheet()
    participant Sheet as _FormSheet (widget)
    participant Builder as Form Builder
    participant Submit as onSubmit Handler

    Caller->>API: call showFormSheet(context, title, builder, onSubmit, canSubmit)
    API->>Sheet: open modal bottom sheet (transparent bg, 85% height)
    Sheet->>Sheet: render header, drag handle, scrollable form
    Sheet->>Builder: invoke builder(context, setState)
    Builder->>Sheet: provide form widgets (FormTextField, FormDropdown)
    User->>Builder: enter/modify data
    Builder->>Sheet: onChanged -> setState()
    Sheet->>Sheet: evaluate canSubmit()
    User->>Sheet: tap Submit
    Sheet->>Sheet: if canSubmit true -> set _submitting = true (show spinner)
    Sheet->>Submit: await onSubmit()
    Submit->>Sheet: return success or throw error
    Sheet->>Sheet: set _submitting = false
    Sheet->>Caller: Navigator.pop() (close sheet) on success
    Sheet->>Caller: showOverlay(success/error)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰
I hopped in with a sheet so neat,
Handles, fields, and a comfy seat,
Tap submit, a spinner whirls,
Overlays cheer for boys and girls,
A rabbit clap—new forms complete!

🚥 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 directly describes the main change: replacing CupertinoAlertDialog-based forms with a bottom sheet implementation across the codebase.
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/form-sheets

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.

@phanan phanan force-pushed the feat/form-sheets branch 5 times, most recently from 3b4b809 to d961514 Compare March 28, 2026 14:35
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: 6

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

37-62: Add coverage for the disabled Create state.

This suite exercises open/create/cancel, but it doesn't assert the new canSubmit guard. Please verify that Create starts disabled and only enables after non-whitespace input so the new behavior is protected.

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

In `@test/ui/screens/create_playlist_folder_test.dart` around lines 37 - 62, Add a
new test (or update the existing 'creates a folder successfully') to assert the
disabled Create state: after pumping the sheet (tap 'Open' and pumpAndSettle)
verify the Create button (find.text('Create')) is initially disabled (cannot be
tapped or has disabled semantics), then enter only whitespace into the
CupertinoTextField and assert Create remains disabled, and finally enter
non-whitespace text (e.g., 'My Folder') and assert Create becomes enabled and
triggers folderProviderMock.create(name: 'My Folder') when tapped; reference the
test name(s) and widgets CupertinoTextField, find.text('Create'), and
folderProviderMock.create to locate where to add these assertions.
🤖 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/add_podcast_sheet.dart`:
- Around line 17-37: The onSubmit handler currently calls Navigator.pop(context)
in both success and the catch block which closes the sheet on error; update the
onSubmit logic so that Navigator.pop(context) is only invoked after a successful
await podcastProvider.add(url: url) (i.e. move the pop into the try after add
and before showOverlay for success) and remove the Navigator.pop call from the
catch block so the sheet remains open on failure; keep showOverlay usage and the
existing error message logic and continue to reference urlController,
podcastProvider.add, showOverlay, and Navigator.pop to locate the changes.

In `@lib/ui/screens/create_playlist_folder_sheet.dart`:
- Around line 20-26: The sheet is being dismissed inside the catch block which
discards the entered name and gives no feedback; update the folder creation flow
so only folderProvider.create(name: name) success path calls
Navigator.pop(context) and showOverlay(context, caption: 'Folder created'), and
in the catch block keep the sheet open and surface the error (e.g., showOverlay
or a visible error widget) instead of calling Navigator.pop; apply the same
change to the new radio and playlist submit handlers so they do not call
Navigator.pop on failure and instead display the error to the user.

In `@lib/ui/screens/create_playlist_sheet.dart`:
- Around line 20-34: The onSubmit flow currently allows the Cancel button
(controlled by _submitting in form_sheet.dart) to remain enabled and silently
pops the sheet on error; wrap the async playlistProvider.create call with
setting _submitting = true before awaiting and _submitting = false in a finally
block, disable the Cancel button when _submitting is true, only call
Navigator.pop(context) on successful creation, and in the catch block keep the
sheet open and call showOverlay(context, caption: <error message or
e.toString()>) so users see the failure and can retry; also ensure any
Navigator.pop is guarded (e.g., only after success) so we don't pop the
underlying page if the sheet was dismissed during the await.

In `@lib/ui/screens/playlists.dart`:
- Around line 562-564: The label builder currently calls folders.firstWhere((f)
=> f.id == playlist.folderId).name which can throw if playlist.folderId refers
to a deleted folder; move the defensive handling into that label builder by
replacing the direct firstWhere call with a safe lookup (e.g., use
folders.firstWhereOrNull((f) => f.id == playlist.folderId)?.name ?? '<deleted
folder>' or check folders.any((f)=>f.id==playlist.folderId) before calling
firstWhere) and keep the earlier selectedFolder/selectedFolderId logic
unchanged; update any references to selectedFolder/playlist.folderId in the
label-building code to use the safe result.

In `@lib/ui/widgets/form_sheet.dart`:
- Around line 20-29: The form body inside _FormSheet currently lives in a
fixed-height SizedBox and can overflow; modify the widget tree in _FormSheet
(likely its build method where you render title, builder(context) and the
actions row) so the central form content is wrapped with Expanded ->
SingleChildScrollView (add appropriate Padding/physics as needed) to make it
scrollable, while leaving the actions Row (submit/cancel buttons) as a sibling
pinned at the bottom; ensure you call the provided builder(...) inside the
SingleChildScrollView so submitLabel, onSubmit and canSubmit logic remains
unchanged.
- Around line 7-13: Change showFormSheet to give submit handlers the sheet's
BuildContext and to disable the Cancel button while submitting: update the
onSubmit parameter type from Future<void> Function() to Future<void>
Function(BuildContext sheetContext), add an isSubmitting state inside
showFormSheet (managed via the existing StateSetter), set the Cancel button's
onPressed to null when isSubmitting is true, and when the submit handler is
invoked call await onSubmit(sheetContext) so the sheet can be dismissed safely
from the sheet context (or the sheet can call Navigator.of(sheetContext).pop()
after await), ensuring callers update their onSubmit implementations to accept
the sheet context.

---

Nitpick comments:
In `@test/ui/screens/create_playlist_folder_test.dart`:
- Around line 37-62: Add a new test (or update the existing 'creates a folder
successfully') to assert the disabled Create state: after pumping the sheet (tap
'Open' and pumpAndSettle) verify the Create button (find.text('Create')) is
initially disabled (cannot be tapped or has disabled semantics), then enter only
whitespace into the CupertinoTextField and assert Create remains disabled, and
finally enter non-whitespace text (e.g., 'My Folder') and assert Create becomes
enabled and triggers folderProviderMock.create(name: 'My Folder') when tapped;
reference the test name(s) and widgets CupertinoTextField, find.text('Create'),
and folderProviderMock.create to locate where to add these assertions.
🪄 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: c45fc2d9-e38a-4a2c-98ac-bc2185f78cf1

📥 Commits

Reviewing files that changed from the base of the PR and between 5d127d6 and 946622f.

📒 Files selected for processing (10)
  • lib/router.dart
  • lib/ui/screens/add_podcast_sheet.dart
  • lib/ui/screens/create_playlist_folder_sheet.dart
  • lib/ui/screens/create_playlist_sheet.dart
  • lib/ui/screens/playlists.dart
  • lib/ui/screens/radio_stations.dart
  • lib/ui/widgets/form_sheet.dart
  • lib/ui/widgets/widgets.dart
  • test/ui/screens/create_playlist_folder_test.dart
  • test/ui/screens/create_playlist_sheet_test.dart

Comment thread lib/ui/screens/add_podcast_sheet.dart
Comment on lines +20 to +26
try {
await folderProvider.create(name: name);
Navigator.pop(context);
showOverlay(context, caption: 'Folder created');
} catch (_) {
Navigator.pop(context);
}
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 dismiss the form on failed creates.

This catch drops the entered name and gives the user no feedback, so any transient failure forces them to start over. Please keep the sheet open, surface the error, and only pop after folderProvider.create succeeds. The same dismiss-on-error pattern also shows up in the new radio and playlist submit handlers in this PR.

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

In `@lib/ui/screens/create_playlist_folder_sheet.dart` around lines 20 - 26, The
sheet is being dismissed inside the catch block which discards the entered name
and gives no feedback; update the folder creation flow so only
folderProvider.create(name: name) success path calls Navigator.pop(context) and
showOverlay(context, caption: 'Folder created'), and in the catch block keep the
sheet open and surface the error (e.g., showOverlay or a visible error widget)
instead of calling Navigator.pop; apply the same change to the new radio and
playlist submit handlers so they do not call Navigator.pop on failure and
instead display the error to the user.

Comment thread lib/ui/screens/create_playlist_sheet.dart
Comment thread lib/ui/screens/playlists.dart Outdated
Comment on lines +7 to +13
Future<void> showFormSheet(
BuildContext context, {
required String title,
required Widget Function(BuildContext context, StateSetter setState) builder,
required String submitLabel,
required Future<void> Function() onSubmit,
bool Function()? canSubmit,
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

🧩 Analysis chain

🏁 Script executed:

cat -n lib/ui/widgets/form_sheet.dart

Repository: koel/player

Length of output: 9894


Dismissal ownership and Cancel button behavior create a race condition risk.

onSubmit has no access to the sheet context, forcing callers to dismiss using the presenter context. Meanwhile, the Cancel button remains active during submission—a user can dismiss the sheet mid-save, and any subsequent context.pop() from onSubmit's completion handler will pop the underlying page instead. Either automatically dismiss the sheet after onSubmit completes, or pass the sheet's BuildContext to onSubmit and disable Cancel while submitting.

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

In `@lib/ui/widgets/form_sheet.dart` around lines 7 - 13, Change showFormSheet to
give submit handlers the sheet's BuildContext and to disable the Cancel button
while submitting: update the onSubmit parameter type from Future<void>
Function() to Future<void> Function(BuildContext sheetContext), add an
isSubmitting state inside showFormSheet (managed via the existing StateSetter),
set the Cancel button's onPressed to null when isSubmitting is true, and when
the submit handler is invoked call await onSubmit(sheetContext) so the sheet can
be dismissed safely from the sheet context (or the sheet can call
Navigator.of(sheetContext).pop() after await), ensuring callers update their
onSubmit implementations to accept the sheet context.

Comment thread lib/ui/widgets/form_sheet.dart
@phanan phanan force-pushed the feat/form-sheets branch from d961514 to 81dad18 Compare March 28, 2026 14: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 (5)
lib/ui/screens/playlists.dart (2)

543-554: Silent failure on error - consider user feedback.

Consistent with the other form sheets, consider adding error feedback when playlist update fails.

♻️ Proposed fix to add error feedback
         try {
           await provider.update(
             playlist,
             name: name,
             description: descController.text.trim(),
             folderId: selectedFolderId,
           );
           Navigator.pop(context);
           showOverlay(context, caption: 'Playlist updated');
         } catch (_) {
           Navigator.pop(context);
+          showOverlay(
+            context,
+            caption: 'Failed to update playlist',
+            icon: CupertinoIcons.exclamationmark_triangle,
+          );
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/ui/screens/playlists.dart` around lines 543 - 554, The catch block
currently swallows errors silently after calling Navigator.pop; modify the
try/catch around provider.update (the call to provider.update(playlist,...)) to
catch the exception as a variable (e.g., catch (e)) and provide user feedback by
calling showOverlay (or another UI error notifier) after Navigator.pop with a
descriptive error caption such as "Failed to update playlist" and include the
exception message (e.toString()) for clarity; ensure you still pop the sheet
(Navigator.pop(context)) before showing the error overlay.

339-344: Missing success feedback for folder rename.

Unlike the other form sheets (Folder created, Playlist added, Playlist updated), the rename folder flow doesn't show a success overlay. Consider adding one for consistency.

♻️ Proposed fix to add success feedback
       onSubmit: () async {
         final name = controller.text.trim();
         if (name.isEmpty) return;
         await provider.rename(folder, name: name);
         Navigator.pop(context);
+        showOverlay(context, caption: 'Folder renamed');
       },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/ui/screens/playlists.dart` around lines 339 - 344, The rename flow in
onSubmit calls provider.rename(...) and pops the sheet but lacks the success
overlay shown elsewhere; after the await provider.rename(folder, name: name)
call, invoke the same success feedback used by the other form sheets (the
overlay/snackbar routine used for "Folder created"/"Playlist added"/"Playlist
updated") with a message like "Folder renamed" and then call
Navigator.pop(context); this will keep behavior consistent with the other flows.
lib/ui/screens/create_playlist_sheet.dart (1)

23-33: Silent failure on error - consider user feedback.

When playlist creation fails, the sheet dismisses without informing the user. For consistency with the podcast addition flow, consider showing an error overlay.

♻️ Proposed fix to add error feedback
       try {
         await playlistProvider.create(
           name: name,
           description: descController.text.trim(),
           folderId: selectedFolderId,
         );
         Navigator.pop(context);
         showOverlay(context, caption: 'Playlist added');
       } catch (_) {
         Navigator.pop(context);
+        showOverlay(
+          context,
+          caption: 'Failed to create playlist',
+          icon: CupertinoIcons.exclamationmark_triangle,
+        );
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/ui/screens/create_playlist_sheet.dart` around lines 23 - 33, The catch
block in the create playlist sheet silently dismisses the sheet on failure;
update the error handling around playlistProvider.create (in
create_playlist_sheet.dart) to present user feedback by calling showOverlay with
an error caption (e.g., "Failed to add playlist" or include the exception
message) before or after Navigator.pop(context), and optionally log the caught
error; specifically modify the catch (_) to capture the exception (catch (e))
and call showOverlay(context, caption: 'Failed to add playlist:
${e.toString()}') or a safe generic message, keeping existing
Navigator.pop(context) behavior.
lib/ui/screens/create_playlist_folder_sheet.dart (1)

19-25: Silent failure on error - consider user feedback.

When the folder creation fails, the sheet is dismissed without informing the user. Unlike the podcast addition flow which shows an error overlay, this silently swallows the exception. Consider adding error feedback for consistency.

♻️ Proposed fix to add error feedback
       try {
         await folderProvider.create(name: name);
         Navigator.pop(context);
         showOverlay(context, caption: 'Folder created');
       } catch (_) {
         Navigator.pop(context);
+        showOverlay(
+          context,
+          caption: 'Failed to create folder',
+          icon: CupertinoIcons.exclamationmark_triangle,
+        );
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/ui/screens/create_playlist_folder_sheet.dart` around lines 19 - 25, The
catch block after calling folderProvider.create(name: name) currently swallows
errors and just dismisses the sheet; update the catch to capture the exception
(e.g., catch (e)) and show user-facing error feedback before/after calling
Navigator.pop(context)—use the existing showOverlay function to display a clear
error caption like "Failed to create folder: <error message>" (and mark it as an
error if supported) so failure mirrors the podcast flow and surfaces the
exception to the user.
lib/ui/widgets/form_sheet.dart (1)

54-137: Consider clarifying state ownership between _FormSheetState and StatefulBuilder.

_submitting is a field of _FormSheetState (line 55), but you're using StatefulBuilder's setState (line 68) to trigger rebuilds when modifying it (lines 115, 120). This works because the assignment happens within the closure before the rebuild, but it conflates two state management approaches.

One option is to capture a reference to the outer setState and use it for _submitting:

♻️ Possible refactor to clarify state ownership
 class _FormSheetState extends State<_FormSheet> {
   var _submitting = false;
+  late final StateSetter _outerSetState = setState;

   `@override`
   Widget build(BuildContext context) {
     return GradientDecoratedContainer(
       child: Padding(
         ...
         child: StatefulBuilder(
-          builder: (context, setState) {
+          builder: (context, innerSetState) {
             return Column(
               ...
                         onPressed: _submitting || ...
                             ? null
                             : () async {
-                                setState(() => _submitting = true);
+                                _outerSetState(() => _submitting = true);
                                 try {
                                   await widget.onSubmit();
                                 } finally {
                                   if (mounted) {
-                                    setState(() => _submitting = false);
+                                    _outerSetState(() => _submitting = false);
                                   }
                                 }
                               },
               ...
-              widget.builder(context, setState),
+              widget.builder(context, innerSetState),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/ui/widgets/form_sheet.dart` around lines 54 - 137, The StatefulBuilder's
builder currently shadows the State.setState parameter named setState, causing
ambiguity when updating the outer _submitting field; rename the builder's second
parameter (e.g., to innerSetState) so widget.builder(context, innerSetState)
keeps using the local builder state while calls that update _submitting use the
outer setState (now unshadowed) — update the two places that set _submitting
(setting true before await and setting false in finally) to call the outer
setState so state ownership is clear.
🤖 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/radio_stations.dart`:
- Around line 333-335: The edit flow currently swallows errors in the catch (_)
block (inside the edit sheet handler) by only calling Navigator.pop(context);
instead of notifying the user; change the catch to capture the exception (e.g.
catch (e) or catch (e, st)) and reuse the same error-extraction and display
logic used by _showAddStation (lines ~198-214) to surface the error
overlay/message to the user; keep the sheet open or close it only after showing
the overlay per the add flow so users see the failure instead of silently losing
their changes.

---

Nitpick comments:
In `@lib/ui/screens/create_playlist_folder_sheet.dart`:
- Around line 19-25: The catch block after calling folderProvider.create(name:
name) currently swallows errors and just dismisses the sheet; update the catch
to capture the exception (e.g., catch (e)) and show user-facing error feedback
before/after calling Navigator.pop(context)—use the existing showOverlay
function to display a clear error caption like "Failed to create folder: <error
message>" (and mark it as an error if supported) so failure mirrors the podcast
flow and surfaces the exception to the user.

In `@lib/ui/screens/create_playlist_sheet.dart`:
- Around line 23-33: The catch block in the create playlist sheet silently
dismisses the sheet on failure; update the error handling around
playlistProvider.create (in create_playlist_sheet.dart) to present user feedback
by calling showOverlay with an error caption (e.g., "Failed to add playlist" or
include the exception message) before or after Navigator.pop(context), and
optionally log the caught error; specifically modify the catch (_) to capture
the exception (catch (e)) and call showOverlay(context, caption: 'Failed to add
playlist: ${e.toString()}') or a safe generic message, keeping existing
Navigator.pop(context) behavior.

In `@lib/ui/screens/playlists.dart`:
- Around line 543-554: The catch block currently swallows errors silently after
calling Navigator.pop; modify the try/catch around provider.update (the call to
provider.update(playlist,...)) to catch the exception as a variable (e.g., catch
(e)) and provide user feedback by calling showOverlay (or another UI error
notifier) after Navigator.pop with a descriptive error caption such as "Failed
to update playlist" and include the exception message (e.toString()) for
clarity; ensure you still pop the sheet (Navigator.pop(context)) before showing
the error overlay.
- Around line 339-344: The rename flow in onSubmit calls provider.rename(...)
and pops the sheet but lacks the success overlay shown elsewhere; after the
await provider.rename(folder, name: name) call, invoke the same success feedback
used by the other form sheets (the overlay/snackbar routine used for "Folder
created"/"Playlist added"/"Playlist updated") with a message like "Folder
renamed" and then call Navigator.pop(context); this will keep behavior
consistent with the other flows.

In `@lib/ui/widgets/form_sheet.dart`:
- Around line 54-137: The StatefulBuilder's builder currently shadows the
State.setState parameter named setState, causing ambiguity when updating the
outer _submitting field; rename the builder's second parameter (e.g., to
innerSetState) so widget.builder(context, innerSetState) keeps using the local
builder state while calls that update _submitting use the outer setState (now
unshadowed) — update the two places that set _submitting (setting true before
await and setting false in finally) to call the outer setState so state
ownership is clear.
🪄 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: 2c60d9f9-b2f0-4669-9112-41c4397e209d

📥 Commits

Reviewing files that changed from the base of the PR and between 5d127d6 and ccae9b3.

⛔ Files ignored due to path filters (1)
  • pubspec.lock is excluded by !**/*.lock
📒 Files selected for processing (10)
  • lib/router.dart
  • lib/ui/screens/add_podcast_sheet.dart
  • lib/ui/screens/create_playlist_folder_sheet.dart
  • lib/ui/screens/create_playlist_sheet.dart
  • lib/ui/screens/playlists.dart
  • lib/ui/screens/radio_stations.dart
  • lib/ui/widgets/form_sheet.dart
  • lib/ui/widgets/widgets.dart
  • test/ui/screens/create_playlist_folder_test.dart
  • test/ui/screens/create_playlist_sheet_test.dart

Comment thread lib/ui/screens/radio_stations.dart Outdated
@phanan phanan force-pushed the feat/form-sheets branch 2 times, most recently from 3eb6c4b to d25cc62 Compare March 28, 2026 14:44
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

♻️ Duplicate comments (4)
lib/ui/screens/playlists.dart (1)

531-532: ⚠️ Potential issue | 🟡 Minor

Normalize a missing folder id before binding the dropdown.

If playlist.folderId no longer exists in folderProvider.folders, selectedFolderId is fed into DropdownButton as a value that is not present in items. That can assert in debug, and when there are no folders left it silently resubmits the stale id. Coerce the initial selection to null when the folder is missing.

🛡️ Minimal fix
     final folders = folderProvider.folders;
     String? selectedFolderId = playlist.folderId;
+    if (selectedFolderId != null &&
+        !folders.any((f) => f.id == selectedFolderId)) {
+      selectedFolderId = null;
+    }

Also applies to: 548-549, 573-575

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

In `@lib/ui/screens/playlists.dart` around lines 531 - 532, Normalize the initial
dropdown selection by coercing playlist.folderId to null when it is not present
in the current folderProvider.folders before binding to the DropdownButton:
replace direct use of String? selectedFolderId = playlist.folderId with logic
that sets selectedFolderId = playlist.folderId if folderProvider.folders
contains that id, otherwise selectedFolderId = null; apply the same fix to the
other identical occurrences that read playlist.folderId for dropdown binding
(the other DropdownButton / selectedFolderId patterns) so the DropdownButton
value is never a stale id missing from folderProvider.folders.
lib/ui/widgets/form_sheet.dart (2)

8-15: ⚠️ Potential issue | 🟠 Major

Pass the sheet context into onSubmit and block dismissal while saving.

Every new caller in this PR pops with the presenter context after awaiting provider work, and Cancel stays active while _submitting. If the sheet is gone before the await completes, that later pop can hit the page underneath instead of the sheet.

🔒 Safer API shape
-  required Future<void> Function() onSubmit,
+  required Future<void> Function(BuildContext sheetContext) onSubmit,
...
-  final Future<void> Function() onSubmit;
+  final Future<void> Function(BuildContext sheetContext) onSubmit;
...
-                        onPressed: () => Navigator.pop(context),
+                        onPressed:
+                            _submitting ? null : () => Navigator.pop(context),
...
-                                  await widget.onSubmit();
+                                  await widget.onSubmit(context);

Also applies to: 36-40, 97-125

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

In `@lib/ui/widgets/form_sheet.dart` around lines 8 - 15, Change the showFormSheet
API so the onSubmit callback receives the sheet BuildContext (e.g. replace
Future<void> Function() onSubmit with Future<void> Function(BuildContext
sheetContext) onSubmit) and, inside showFormSheet (the submit button handler),
set a local _submitting state true, call await onSubmit(sheetContext), and only
reset and close after that completes; also prevent dismissal while saving by
disabling barrierDismissible (or wrapping content in WillPopScope to block back)
and disabling the Cancel button when _submitting is true. Apply the same pattern
to the other sheet usages referenced (lines noted in the review) so callers use
the provided sheetContext for any Navigator.pop calls to avoid popping the
underlying page.

69-93: ⚠️ Potential issue | 🟠 Major

Make the form body scrollable.

The sheet is fixed at 85% height, but the form content is inserted directly into a Column. Multi-field sheets like playlist/radio edit will overflow once the keyboard or larger text scale reduces the available space.

📱 Suggested layout fix
-            return Column(
-              mainAxisSize: MainAxisSize.min,
+            return Column(
+              mainAxisSize: MainAxisSize.max,
               crossAxisAlignment: CrossAxisAlignment.stretch,
               children: [
@@
-                widget.builder(context, setState),
-                const SizedBox(height: 20),
+                Expanded(
+                  child: SingleChildScrollView(
+                    padding: const EdgeInsets.only(bottom: 20),
+                    child: widget.builder(context, setState),
+                  ),
+                ),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/ui/widgets/form_sheet.dart` around lines 69 - 93, The Column currently
places widget.builder(...) directly which causes overflow; wrap the
variable-height form area in a scrollable flexible container so the sheet can
scroll when space is tight. Specifically, in the FormSheet build where the
Column is returned, replace the static insertion of widget.builder(context,
setState) and the adjacent SizedBoxes with a Flexible (or Expanded) containing a
SingleChildScrollView (and a Padding if needed) that hosts the builder output so
the form body becomes scrollable and shrinks/grows with available space; keep
the drag handle and title outside this scrollable region.
lib/ui/screens/add_podcast_sheet.dart (1)

25-36: ⚠️ Potential issue | 🟠 Major

Keep the podcast sheet open when add fails.

The catch path dismisses the sheet on every error, which throws away the RSS URL the user just entered. showOverlay is independent of the sheet, so remove the pop on failure and let the user retry in place.

💡 Minimal fix
-      } catch (e) {
-        Navigator.pop(context);
+      } catch (e) {
         final message =
             e is HttpResponseException && e.response.statusCode == 409
                 ? 'You are already subscribed to this podcast.'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/ui/screens/add_podcast_sheet.dart` around lines 25 - 36, The catch block
in add_podcast_sheet.dart currently calls Navigator.pop(context) which closes
the Add Podcast sheet on any error and loses the entered RSS URL; remove the
Navigator.pop(context) call from the error handling path (leave any
Navigator.pop on the success path intact), so that on exception you only call
showOverlay(...) with the error message (and still handle the 409-specific
message), allowing the sheet to stay open for user retry.
🤖 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/playlists.dart`:
- Around line 543-554: The catch block currently calls Navigator.pop(context)
which closes the edit sheet on failure and loses the user's edits; change the
error path so you do NOT call Navigator.pop(context) in the catch, and instead
surface the failure (e.g., call showOverlay or a SnackBar with the caught error
message) so the form (fields: name, descController, selectedFolderId) remains
open for retry; keep the successful-path Navigator.pop(context) and
showOverlay('Playlist updated') after provider.update(playlist, ...) only.

In `@lib/ui/screens/radio_stations.dart`:
- Around line 189-215: The catch blocks currently call Navigator.pop(context)
which closes the station form even on validation/network failure; remove the
Navigator.pop(context) calls from the catch handlers so the form remains open
and the user's input is preserved (keep the success path that calls
Navigator.pop after provider.create completes), still show the error overlay
with message/icon in the catch, and rely on the existing finally in
showFormSheet to reset the spinner; make the same change for the duplicate catch
at the other create/edit block that mirrors lines 323-335 (i.e., eliminate
popping on failure while keeping success pop and error overlay).

---

Duplicate comments:
In `@lib/ui/screens/add_podcast_sheet.dart`:
- Around line 25-36: The catch block in add_podcast_sheet.dart currently calls
Navigator.pop(context) which closes the Add Podcast sheet on any error and loses
the entered RSS URL; remove the Navigator.pop(context) call from the error
handling path (leave any Navigator.pop on the success path intact), so that on
exception you only call showOverlay(...) with the error message (and still
handle the 409-specific message), allowing the sheet to stay open for user
retry.

In `@lib/ui/screens/playlists.dart`:
- Around line 531-532: Normalize the initial dropdown selection by coercing
playlist.folderId to null when it is not present in the current
folderProvider.folders before binding to the DropdownButton: replace direct use
of String? selectedFolderId = playlist.folderId with logic that sets
selectedFolderId = playlist.folderId if folderProvider.folders contains that id,
otherwise selectedFolderId = null; apply the same fix to the other identical
occurrences that read playlist.folderId for dropdown binding (the other
DropdownButton / selectedFolderId patterns) so the DropdownButton value is never
a stale id missing from folderProvider.folders.

In `@lib/ui/widgets/form_sheet.dart`:
- Around line 8-15: Change the showFormSheet API so the onSubmit callback
receives the sheet BuildContext (e.g. replace Future<void> Function() onSubmit
with Future<void> Function(BuildContext sheetContext) onSubmit) and, inside
showFormSheet (the submit button handler), set a local _submitting state true,
call await onSubmit(sheetContext), and only reset and close after that
completes; also prevent dismissal while saving by disabling barrierDismissible
(or wrapping content in WillPopScope to block back) and disabling the Cancel
button when _submitting is true. Apply the same pattern to the other sheet
usages referenced (lines noted in the review) so callers use the provided
sheetContext for any Navigator.pop calls to avoid popping the underlying page.
- Around line 69-93: The Column currently places widget.builder(...) directly
which causes overflow; wrap the variable-height form area in a scrollable
flexible container so the sheet can scroll when space is tight. Specifically, in
the FormSheet build where the Column is returned, replace the static insertion
of widget.builder(context, setState) and the adjacent SizedBoxes with a Flexible
(or Expanded) containing a SingleChildScrollView (and a Padding if needed) that
hosts the builder output so the form body becomes scrollable and shrinks/grows
with available space; keep the drag handle and title outside this scrollable
region.
🪄 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: 969acdaf-3cde-4eb7-b71a-45a84090667d

📥 Commits

Reviewing files that changed from the base of the PR and between 946622f and d961514.

⛔ Files ignored due to path filters (1)
  • pubspec.lock is excluded by !**/*.lock
📒 Files selected for processing (10)
  • lib/router.dart
  • lib/ui/screens/add_podcast_sheet.dart
  • lib/ui/screens/create_playlist_folder_sheet.dart
  • lib/ui/screens/create_playlist_sheet.dart
  • lib/ui/screens/playlists.dart
  • lib/ui/screens/radio_stations.dart
  • lib/ui/widgets/form_sheet.dart
  • lib/ui/widgets/widgets.dart
  • test/ui/screens/create_playlist_folder_test.dart
  • test/ui/screens/create_playlist_sheet_test.dart
✅ Files skipped from review due to trivial changes (1)
  • lib/ui/widgets/widgets.dart
🚧 Files skipped from review as they are similar to previous changes (5)
  • lib/router.dart
  • test/ui/screens/create_playlist_sheet_test.dart
  • test/ui/screens/create_playlist_folder_test.dart
  • lib/ui/screens/create_playlist_folder_sheet.dart
  • lib/ui/screens/create_playlist_sheet.dart

Comment thread lib/ui/screens/playlists.dart
Comment thread lib/ui/screens/radio_stations.dart
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 (2)
lib/ui/widgets/form_sheet.dart (2)

222-225: Focus border color doesn't match PR objectives.

The PR objectives specify AppColors.highlight for the focus border, but line 224 uses Colors.white54. Consider using AppColors.highlight for consistency with the stated design:

         focusedBorder: OutlineInputBorder(
           borderRadius: BorderRadius.circular(8),
-          borderSide: const BorderSide(color: Colors.white54, width: 1),
+          borderSide: const BorderSide(color: AppColors.highlight, width: 1),
         ),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/ui/widgets/form_sheet.dart` around lines 222 - 225, The focusedBorder
OutlineInputBorder currently uses Colors.white54 but should use the theme
highlight color; update the BorderSide color in the focusedBorder (the
OutlineInputBorder inside the FormSheet widget / the input decoration where
focusedBorder is defined) to AppColors.highlight and ensure AppColors is
imported or available in that file so the focus state uses the PR-specified
AppColors.highlight.

112-125: State management mixes widget state with StatefulBuilder setState.

_submitting is declared on _FormSheetState (line 55) but mutated via the StatefulBuilder's setState (lines 117, 122). This works because the variable is captured in closure and StatefulBuilder rebuilds its subtree, but it's architecturally confusing. Consider either using _FormSheetState.setState directly or moving _submitting into a ValueNotifier scoped to the builder.

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

In `@lib/ui/widgets/form_sheet.dart` around lines 112 - 125, The code mutates the
_submitting field of _FormSheetState from inside a StatefulBuilder's setState,
which mixes state owners and is confusing; update the button handler to mutate
_submitting via the owning State's setState (call _FormSheetState.setState)
instead of the StatefulBuilder's setState, or alternatively move _submitting
into a ValueNotifier<bool> scoped to the builder and update/listen to that
notifier; ensure references to widget.canSubmit and widget.onSubmit remain
unchanged and that the mounted check and final reset of _submitting (or
notifier.value) happen after await widget.onSubmit() completes.
🤖 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/create_playlist_sheet.dart`:
- Around line 55-58: The labelBuilder closure currently uses
folders.firstWhere((f) => f.id == id) which can throw a StateError if folders
changes; update labelBuilder in create_playlist_sheet.dart to safely handle
missing matches by either using firstWhere with an orElse that returns a
fallback Folder (or null-handling) or by using collection's firstWhereOrNull and
then returning a default string (e.g., 'No folder' or empty) when the result is
null; ensure you reference the labelBuilder and folders variables and handle the
null/fallback case before accessing .name.

---

Nitpick comments:
In `@lib/ui/widgets/form_sheet.dart`:
- Around line 222-225: The focusedBorder OutlineInputBorder currently uses
Colors.white54 but should use the theme highlight color; update the BorderSide
color in the focusedBorder (the OutlineInputBorder inside the FormSheet widget /
the input decoration where focusedBorder is defined) to AppColors.highlight and
ensure AppColors is imported or available in that file so the focus state uses
the PR-specified AppColors.highlight.
- Around line 112-125: The code mutates the _submitting field of _FormSheetState
from inside a StatefulBuilder's setState, which mixes state owners and is
confusing; update the button handler to mutate _submitting via the owning
State's setState (call _FormSheetState.setState) instead of the
StatefulBuilder's setState, or alternatively move _submitting into a
ValueNotifier<bool> scoped to the builder and update/listen to that notifier;
ensure references to widget.canSubmit and widget.onSubmit remain unchanged and
that the mounted check and final reset of _submitting (or notifier.value) happen
after await widget.onSubmit() completes.
🪄 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: b0aaa770-5e84-4ab6-97d0-017ba85881f2

📥 Commits

Reviewing files that changed from the base of the PR and between d961514 and d25cc62.

⛔ Files ignored due to path filters (1)
  • pubspec.lock is excluded by !**/*.lock
📒 Files selected for processing (10)
  • lib/router.dart
  • lib/ui/screens/add_podcast_sheet.dart
  • lib/ui/screens/create_playlist_folder_sheet.dart
  • lib/ui/screens/create_playlist_sheet.dart
  • lib/ui/screens/playlists.dart
  • lib/ui/screens/radio_stations.dart
  • lib/ui/widgets/form_sheet.dart
  • lib/ui/widgets/widgets.dart
  • test/ui/screens/create_playlist_folder_test.dart
  • test/ui/screens/create_playlist_sheet_test.dart
✅ Files skipped from review due to trivial changes (1)
  • lib/ui/widgets/widgets.dart
🚧 Files skipped from review as they are similar to previous changes (6)
  • lib/router.dart
  • test/ui/screens/create_playlist_sheet_test.dart
  • lib/ui/screens/create_playlist_folder_sheet.dart
  • test/ui/screens/create_playlist_folder_test.dart
  • lib/ui/screens/playlists.dart
  • lib/ui/screens/radio_stations.dart

Comment thread lib/ui/screens/create_playlist_sheet.dart
@phanan phanan force-pushed the feat/form-sheets branch from d25cc62 to e9624a2 Compare March 28, 2026 15: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

♻️ Duplicate comments (9)
lib/ui/screens/add_podcast_sheet.dart (1)

25-37: ⚠️ Potential issue | 🟠 Major

Sheet dismisses on error, discarding the typed URL.

The catch block calls Navigator.pop(context) before showing the error overlay, losing the user's typed podcast URL. Keep the sheet open on failure so users can correct the URL and retry.

🛠️ Suggested fix
       } catch (e) {
-        Navigator.pop(context);
         final message =
             e is HttpResponseException && e.response.statusCode == 409
                 ? 'You are already subscribed to this podcast.'
                 : 'Something wrong happened. Please try again.';
         showOverlay(
           context,
           caption: 'Error',
           message: message,
           icon: CupertinoIcons.exclamationmark_triangle,
         );
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/ui/screens/add_podcast_sheet.dart` around lines 25 - 37, The catch block
currently calls Navigator.pop(context) which closes the add-podcast sheet and
discards the typed URL; remove that Navigator.pop(context) call in the error
path and only call showOverlay(...) to display the error so the sheet remains
open for the user to correct the URL; ensure any successful path still invokes
Navigator.pop(context) as before and that the error handling around e, message,
and showOverlay stays intact.
lib/ui/screens/playlists.dart (2)

576-579: ⚠️ Potential issue | 🟡 Minor

firstWhere without orElse can throw if folder is deleted.

Same issue as in create_playlist_sheet.dart: if playlist.folderId references a folder that no longer exists, firstWhere throws StateError. Use a safe lookup.

🛠️ Suggested fix
                 labelBuilder: (id) {
                   if (id == null) return 'No folder';
-                  return folders.firstWhere((f) => f.id == id).name;
+                  final folder = folders.cast<PlaylistFolder?>().firstWhere(
+                    (f) => f?.id == id,
+                    orElse: () => null,
+                  );
+                  return folder?.name ?? 'Unknown folder';
                 },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/ui/screens/playlists.dart` around lines 576 - 579, The labelBuilder
closure uses folders.firstWhere(...) which will throw a StateError if the
referenced folder id is missing; change the lookup in labelBuilder to a safe
lookup (e.g., use firstWhere with an orElse that returns null or a fallback
object, or use folders.indexWhere and check for -1) and return a default string
like 'Unknown folder' or 'No folder' when not found; update the labelBuilder
lambda and any related code that assumes a non-null folder (referencing folders,
folder.id, folder.name, and labelBuilder) to handle the missing-folder case
safely.

552-554: ⚠️ Potential issue | 🟠 Major

Edit playlist silently discards changes on error.

The catch block dismisses the sheet without feedback, causing users to lose their edited name, description, and folder selection with no indication of failure. Keep the sheet open and show an error message.

🛠️ Suggested fix
         } catch (_) {
-          Navigator.pop(context);
+          showOverlay(
+            context,
+            caption: 'Error',
+            message: 'Could not update playlist.',
+            icon: CupertinoIcons.exclamationmark_triangle,
+          );
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/ui/screens/playlists.dart` around lines 552 - 554, The catch block
currently swallows errors and calls Navigator.pop(context), which closes the
edit sheet and discards the user's edits; instead remove the
Navigator.pop(context) from the catch, keep the sheet open, and display an error
to the user (e.g. use ScaffoldMessenger.of(context).showSnackBar(...) or
showDialog(...) with a clear message and optionally the error details). Update
the catch handler that surrounds the edit/save logic in playlists.dart so it
logs the error and shows the SnackBar/dialog (referencing the existing catch
block that calls Navigator.pop(context)) rather than dismissing the sheet.
lib/ui/screens/create_playlist_sheet.dart (2)

31-33: ⚠️ Potential issue | 🟠 Major

Silent error handling discards user input without feedback.

The catch block dismisses the sheet without showing any error message, causing users to lose their entered name, description, and folder selection with no indication of what went wrong. Keep the sheet open on failure and display an error message so users can retry.

🛠️ Suggested fix
       } catch (_) {
-        Navigator.pop(context);
+        showOverlay(
+          context,
+          caption: 'Error',
+          message: 'Could not create playlist.',
+          icon: CupertinoIcons.exclamationmark_triangle,
+        );
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/ui/screens/create_playlist_sheet.dart` around lines 31 - 33, The catch
block currently swallows errors and calls Navigator.pop(context), discarding
user input; remove the Navigator.pop(context) from the catch and instead surface
the error to the user (e.g., use ScaffoldMessenger.of(context).showSnackBar(...)
or set an error state and call setState to display an inline error message in
the CreatePlaylistSheet), leaving any TextEditingController values intact so the
sheet stays open and the user can retry; update the catch that wraps the
playlist creation logic (the block containing Navigator.pop(context)) to show
the error message and preserve input rather than dismissing the sheet.

55-58: ⚠️ Potential issue | 🟡 Minor

firstWhere without orElse can throw if folder is deleted.

If folders is mutated between when items is built and when labelBuilder is called (e.g., a folder is deleted), firstWhere throws StateError. Use a safe lookup pattern.

🛠️ Suggested fix
               labelBuilder: (id) {
                 if (id == null) return 'No folder';
-                return folders.firstWhere((f) => f.id == id).name;
+                final folder = folders.cast<PlaylistFolder?>().firstWhere(
+                  (f) => f?.id == id,
+                  orElse: () => null,
+                );
+                return folder?.name ?? 'Unknown folder';
               },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/ui/screens/create_playlist_sheet.dart` around lines 55 - 58, The
labelBuilder closure risks throwing StateError when folders.firstWhere can't
find a match; update labelBuilder (the closure using folders and id) to perform
a safe lookup instead of plain firstWhere — e.g., use firstWhere with an orElse
that returns null/placeholder or use indexWhere to check existence before
accessing .name, and return a fallback like 'No folder' or 'Unknown folder' when
no folder with the given id exists.
lib/ui/widgets/form_sheet.dart (2)

69-93: ⚠️ Potential issue | 🟠 Major

Form content lacks scroll mechanism and may overflow.

The Column at line 69 uses mainAxisSize: MainAxisSize.min, but it's constrained by a fixed 85% height SizedBox. When the keyboard appears or with larger forms (multiple fields, multiline inputs), the content will overflow. Wrap the form content (line 92) in Expanded + SingleChildScrollView while keeping the action buttons pinned at the bottom.

🛠️ Suggested structure
               return Column(
-                mainAxisSize: MainAxisSize.min,
                 crossAxisAlignment: CrossAxisAlignment.stretch,
                 children: [
                   Center(
                     child: Container(
                       width: 36,
                       height: 4,
                       decoration: BoxDecoration(
                         color: Colors.white24,
                         borderRadius: BorderRadius.circular(2),
                       ),
                     ),
                   ),
                   const SizedBox(height: 20),
                   Text(
                     widget.title,
                     style: const TextStyle(
                       fontSize: 24,
                       fontWeight: FontWeight.bold,
                     ),
                   ),
                   const SizedBox(height: 28),
-                  widget.builder(context, setState),
-                  const SizedBox(height: 32),
+                  Expanded(
+                    child: SingleChildScrollView(
+                      child: widget.builder(context, setState),
+                    ),
+                  ),
+                  const SizedBox(height: 16),
                   Row(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/ui/widgets/form_sheet.dart` around lines 69 - 93, The Column used to
render the sheet header and body (the Container drag handle, title Text and
widget.builder) can overflow because mainAxisSize is min inside the fixed-height
sheet; wrap the variable form content returned by widget.builder in an Expanded
(or Flexible) containing a SingleChildScrollView (or ListView) so the form
becomes scrollable when the keyboard appears or the form is large, and keep the
action buttons section outside that scrollable area (i.e., pinned after the
Expanded) so buttons remain at the bottom; update the build that composes Column
and widget.builder accordingly.

96-104: ⚠️ Potential issue | 🟠 Major

Cancel button remains active during submission, risking navigation issues.

The Cancel button's onPressed is not disabled when _submitting is true. If the user taps Cancel while onSubmit is awaiting, the sheet closes but any subsequent Navigator.pop(context) in the caller's onSubmit completion will pop the underlying page instead of the already-dismissed sheet.

🛠️ Suggested fix to disable Cancel during submission
                     Expanded(
                       child: CupertinoButton(
                         padding: const EdgeInsets.symmetric(vertical: 14),
-                        onPressed: () => Navigator.pop(context),
+                        onPressed: _submitting ? null : () => Navigator.pop(context),
                         child: const Text(
                           'Cancel',
                           style: TextStyle(color: Colors.white54),
                         ),
                       ),
                     ),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/ui/widgets/form_sheet.dart` around lines 96 - 104, The Cancel button's
onPressed must be disabled while _submitting is true to prevent premature
Navigator.pop(context) and double-pop from onSubmit; update the CupertinoButton
(the Cancel button) so its onPressed is conditional (e.g. onPressed: _submitting
? null : () => Navigator.pop(context)) or otherwise ignores taps when
_submitting is true, ensuring the button is visually/behaviorally disabled
during the async onSubmit flow.
lib/ui/screens/radio_stations.dart (2)

333-335: ⚠️ Potential issue | 🟠 Major

Edit flow silently discards changes on error.

Unlike the add flow which shows an error overlay, the edit flow's catch block only pops the sheet without any user feedback. Users won't know their update failed and will lose their edits. Show an error message and keep the sheet open.

🛠️ Suggested fix
         } catch (_) {
-          Navigator.pop(context);
+          showOverlay(
+            context,
+            caption: 'Error',
+            message: 'Could not update station.',
+            icon: CupertinoIcons.exclamationmark_triangle,
+          );
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/ui/screens/radio_stations.dart` around lines 333 - 335, The edit flow's
catch block currently swallows errors and calls Navigator.pop(context) which
closes the sheet without feedback; change the catch to capture the exception
(e.g., catch (e, st)), remove the Navigator.pop(context) call, and instead show
the same user-facing error overlay used by the add flow (replace with the
project's error UI helper used elsewhere — e.g., showErrorOverlay or the
add-flow's error call) passing the exception message/details so the user sees
the failure and the sheet remains open for retry; ensure you update the catch
surrounding the edit save/update routine (where Navigator.pop(context) is
called) to implement this behavior.

198-215: ⚠️ Potential issue | 🟠 Major

Add flow dismisses sheet on error, discarding user input.

While error feedback is shown via showOverlay, the sheet is still dismissed via Navigator.pop(context) at line 199 before the overlay appears. This loses the user's typed station name, URL, and description. Keep the sheet open on failure so users can correct and retry.

🛠️ Suggested fix
         } catch (e) {
-          Navigator.pop(context);
           var message = 'Something went wrong.';
           if (e is HttpResponseException) {
             try {
               final body = jsonDecode(e.response.body);
               if (body['message'] != null) {
                 message = body['message'];
               }
             } catch (_) {}
           }
           showOverlay(
             context,
             caption: 'Error',
             message: message,
             icon: CupertinoIcons.exclamationmark_triangle,
           );
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/ui/screens/radio_stations.dart` around lines 198 - 215, The catch block
currently calls Navigator.pop(context) which dismisses the input sheet before
showing the error overlay and discarding user input; remove or move the
Navigator.pop(context) out of this catch block and only call it after a
successful save (e.g., where create/update completes), so that on exceptions
(caught in this catch handling for HttpResponseException and others) the sheet
remains open and showOverlay(...) displays the error to let the user correct
inputs; look for the catch surrounding the save logic and the usages of
Navigator.pop(context), showOverlay, and HttpResponseException to relocate the
dismissal to the success path.
🧹 Nitpick comments (1)
test/ui/screens/create_playlist_folder_test.dart (1)

36-61: Consider adding a test for the disabled submit button when name is empty.

The previous test "does not create with empty name" was removed. While canSubmit now prevents submission at the sheet level (the button is disabled when input is empty), there's no test verifying this behavior. Consider adding a test that confirms the Create button is disabled when the text field is empty.

📝 Example test
testWidgets('submit button is disabled when name is empty', (tester) async {
  await tester.pumpWidget(buildTestApp());
  await tester.tap(find.text('Open'));
  await tester.pumpAndSettle();

  // Find the Create button and verify it's disabled
  final createButton = find.widgetWithText(CupertinoButton, 'Create');
  final button = tester.widget<CupertinoButton>(createButton);
  expect(button.onPressed, isNull);
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/ui/screens/create_playlist_folder_test.dart` around lines 36 - 61, Add a
new widget test in create_playlist_folder_test.dart that verifies the Create
button is disabled when the folder name is empty: open the sheet the same way
the other tests do (tap 'Open' and pumpAndSettle), locate the Create button with
find.widgetWithText(CupertinoButton, 'Create') and assert the widget's onPressed
is null; reference the test name like 'submit button is disabled when name is
empty' and the sheet's form that uses the canSubmit logic so the test covers the
disabled-submit behavior.
🤖 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/playlists.dart`:
- Around line 339-344: The onSubmit handler for the rename sheet calls
provider.rename(folder, name: name) with no error handling; wrap that call in a
try-catch inside the onSubmit of _showRenameFolder (the closure passed to
showFormSheet) so that on success you still call Navigator.pop(context), but on
failure you catch the exception, surface a user-facing error (e.g., via
ScaffoldMessenger.of(context).showSnackBar or similar) and do not call
Navigator.pop so the sheet remains open for retry; reference the
controller.text, provider.rename(...) call and Navigator.pop(context) when
updating the handler.

---

Duplicate comments:
In `@lib/ui/screens/add_podcast_sheet.dart`:
- Around line 25-37: The catch block currently calls Navigator.pop(context)
which closes the add-podcast sheet and discards the typed URL; remove that
Navigator.pop(context) call in the error path and only call showOverlay(...) to
display the error so the sheet remains open for the user to correct the URL;
ensure any successful path still invokes Navigator.pop(context) as before and
that the error handling around e, message, and showOverlay stays intact.

In `@lib/ui/screens/create_playlist_sheet.dart`:
- Around line 31-33: The catch block currently swallows errors and calls
Navigator.pop(context), discarding user input; remove the Navigator.pop(context)
from the catch and instead surface the error to the user (e.g., use
ScaffoldMessenger.of(context).showSnackBar(...) or set an error state and call
setState to display an inline error message in the CreatePlaylistSheet), leaving
any TextEditingController values intact so the sheet stays open and the user can
retry; update the catch that wraps the playlist creation logic (the block
containing Navigator.pop(context)) to show the error message and preserve input
rather than dismissing the sheet.
- Around line 55-58: The labelBuilder closure risks throwing StateError when
folders.firstWhere can't find a match; update labelBuilder (the closure using
folders and id) to perform a safe lookup instead of plain firstWhere — e.g., use
firstWhere with an orElse that returns null/placeholder or use indexWhere to
check existence before accessing .name, and return a fallback like 'No folder'
or 'Unknown folder' when no folder with the given id exists.

In `@lib/ui/screens/playlists.dart`:
- Around line 576-579: The labelBuilder closure uses folders.firstWhere(...)
which will throw a StateError if the referenced folder id is missing; change the
lookup in labelBuilder to a safe lookup (e.g., use firstWhere with an orElse
that returns null or a fallback object, or use folders.indexWhere and check for
-1) and return a default string like 'Unknown folder' or 'No folder' when not
found; update the labelBuilder lambda and any related code that assumes a
non-null folder (referencing folders, folder.id, folder.name, and labelBuilder)
to handle the missing-folder case safely.
- Around line 552-554: The catch block currently swallows errors and calls
Navigator.pop(context), which closes the edit sheet and discards the user's
edits; instead remove the Navigator.pop(context) from the catch, keep the sheet
open, and display an error to the user (e.g. use
ScaffoldMessenger.of(context).showSnackBar(...) or showDialog(...) with a clear
message and optionally the error details). Update the catch handler that
surrounds the edit/save logic in playlists.dart so it logs the error and shows
the SnackBar/dialog (referencing the existing catch block that calls
Navigator.pop(context)) rather than dismissing the sheet.

In `@lib/ui/screens/radio_stations.dart`:
- Around line 333-335: The edit flow's catch block currently swallows errors and
calls Navigator.pop(context) which closes the sheet without feedback; change the
catch to capture the exception (e.g., catch (e, st)), remove the
Navigator.pop(context) call, and instead show the same user-facing error overlay
used by the add flow (replace with the project's error UI helper used elsewhere
— e.g., showErrorOverlay or the add-flow's error call) passing the exception
message/details so the user sees the failure and the sheet remains open for
retry; ensure you update the catch surrounding the edit save/update routine
(where Navigator.pop(context) is called) to implement this behavior.
- Around line 198-215: The catch block currently calls Navigator.pop(context)
which dismisses the input sheet before showing the error overlay and discarding
user input; remove or move the Navigator.pop(context) out of this catch block
and only call it after a successful save (e.g., where create/update completes),
so that on exceptions (caught in this catch handling for HttpResponseException
and others) the sheet remains open and showOverlay(...) displays the error to
let the user correct inputs; look for the catch surrounding the save logic and
the usages of Navigator.pop(context), showOverlay, and HttpResponseException to
relocate the dismissal to the success path.

In `@lib/ui/widgets/form_sheet.dart`:
- Around line 69-93: The Column used to render the sheet header and body (the
Container drag handle, title Text and widget.builder) can overflow because
mainAxisSize is min inside the fixed-height sheet; wrap the variable form
content returned by widget.builder in an Expanded (or Flexible) containing a
SingleChildScrollView (or ListView) so the form becomes scrollable when the
keyboard appears or the form is large, and keep the action buttons section
outside that scrollable area (i.e., pinned after the Expanded) so buttons remain
at the bottom; update the build that composes Column and widget.builder
accordingly.
- Around line 96-104: The Cancel button's onPressed must be disabled while
_submitting is true to prevent premature Navigator.pop(context) and double-pop
from onSubmit; update the CupertinoButton (the Cancel button) so its onPressed
is conditional (e.g. onPressed: _submitting ? null : () =>
Navigator.pop(context)) or otherwise ignores taps when _submitting is true,
ensuring the button is visually/behaviorally disabled during the async onSubmit
flow.

---

Nitpick comments:
In `@test/ui/screens/create_playlist_folder_test.dart`:
- Around line 36-61: Add a new widget test in create_playlist_folder_test.dart
that verifies the Create button is disabled when the folder name is empty: open
the sheet the same way the other tests do (tap 'Open' and pumpAndSettle), locate
the Create button with find.widgetWithText(CupertinoButton, 'Create') and assert
the widget's onPressed is null; reference the test name like 'submit button is
disabled when name is empty' and the sheet's form that uses the canSubmit logic
so the test covers the disabled-submit behavior.
🪄 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: 1b4c6ed5-874d-45ad-8f7f-d6b517206b62

📥 Commits

Reviewing files that changed from the base of the PR and between d25cc62 and e9624a2.

⛔ Files ignored due to path filters (1)
  • pubspec.lock is excluded by !**/*.lock
📒 Files selected for processing (10)
  • lib/router.dart
  • lib/ui/screens/add_podcast_sheet.dart
  • lib/ui/screens/create_playlist_folder_sheet.dart
  • lib/ui/screens/create_playlist_sheet.dart
  • lib/ui/screens/playlists.dart
  • lib/ui/screens/radio_stations.dart
  • lib/ui/widgets/form_sheet.dart
  • lib/ui/widgets/widgets.dart
  • test/ui/screens/create_playlist_folder_test.dart
  • test/ui/screens/create_playlist_sheet_test.dart
✅ Files skipped from review due to trivial changes (1)
  • lib/ui/widgets/widgets.dart
🚧 Files skipped from review as they are similar to previous changes (2)
  • test/ui/screens/create_playlist_sheet_test.dart
  • lib/ui/screens/create_playlist_folder_sheet.dart

Comment thread lib/ui/screens/playlists.dart
Replace CupertinoAlertDialog with a reusable showFormSheet bottom sheet
for all create/edit forms: playlist, playlist folder, radio station,
and podcast. Consistent style with text fields, combo box, and action
buttons across all forms.
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