diff --git a/lib/ui/screens/playlist_details.dart b/lib/ui/screens/playlist_details.dart index 9d10af28..c9c8eed6 100644 --- a/lib/ui/screens/playlist_details.dart +++ b/lib/ui/screens/playlist_details.dart @@ -22,16 +22,9 @@ class PlaylistDetailsScreen extends StatefulWidget { } class _PlaylistDetailsScreen extends State { - late PlaylistProvider _playlistProvider; String _searchQuery = ''; final _scrollController = ScrollController(); - @override - void initState() { - super.initState(); - _playlistProvider = context.read(); - } - Widget? _buildBackgroundImage(Playlist playlist, List playables) { if (playlist.hasCover) { return SizedBox.expand( @@ -143,12 +136,6 @@ class _PlaylistDetailsScreen extends State { SliverPlayableList( playables: displayedPlayables, rightPadding: showScrollbar ? alphabetScrollbarWidth * 0.75 : 0, - onDismissed: playlist.isStandard - ? (playable) => _playlistProvider.removeFromPlaylist( - playable, - playlist: playlist, - ) - : null, ), const BottomSpace(), ], diff --git a/lib/ui/screens/playlists.dart b/lib/ui/screens/playlists.dart index 2d718796..37277e8f 100644 --- a/lib/ui/screens/playlists.dart +++ b/lib/ui/screens/playlists.dart @@ -6,7 +6,6 @@ import 'package:app/router.dart'; import 'package:app/ui/widgets/widgets.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; class PlaylistsScreen extends StatefulWidget { @@ -102,34 +101,22 @@ class _PlaylistsScreenState extends State { sliverItems.add( Card( child: Dismissible( - direction: DismissDirection.horizontal, - confirmDismiss: (direction) async { - if (direction == DismissDirection.startToEnd) { - final playableProvider = context.read(); - final songs = []; - for (final p in folderPlaylists) { - songs.addAll(await playableProvider.fetchForPlaylist(p.id)); - } - if (songs.isNotEmpty) { - for (final song in songs) { - await audioHandler.queueToBottom(song); - } - showOverlay(context, caption: 'Queued'); - } else { - showOverlay(context, caption: 'No songs found.', icon: CupertinoIcons.nosign); - } - return false; - } - final confirmed = await _confirmDeleteFolder( - context, - folder: folder, + direction: DismissDirection.startToEnd, + confirmDismiss: (_) async { + final playableProvider = context.read(); + final results = await Future.wait( + folderPlaylists.map((p) => playableProvider + .fetchForPlaylist(p.id) + .catchError((_) => [])), ); - if (confirmed) { - for (final p in folderPlaylists) { - p.folderId = null; + final songs = results.expand((s) => s).toList(); + if (songs.isNotEmpty) { + for (final song in songs) { + await audioHandler.queueToBottom(song); } - folderProvider.remove(folder); - _expandedFolders.remove(folder.id); + showOverlay(context, caption: 'Queued'); + } else { + showOverlay(context, caption: 'No songs found.', icon: CupertinoIcons.nosign); } return false; }, @@ -141,26 +128,12 @@ class _PlaylistsScreenState extends State { child: Icon(CupertinoIcons.text_badge_plus), ), ), - secondaryBackground: Container( - alignment: AlignmentDirectional.centerEnd, - color: AppColors.red, - child: const Padding( - padding: EdgeInsets.only(right: 28), - child: Icon(CupertinoIcons.delete), - ), - ), key: ValueKey(folder.id), child: _FolderRow( folder: folder, playlistCount: folderPlaylists.length, isExpanded: isExpanded, onTap: () => _toggleFolder(folder.id), - onLongPress: () => _showFolderActions( - context, - folder: folder, - playlists: folderPlaylists, - folderProvider: folderProvider, - ), ), ), ), @@ -169,9 +142,8 @@ class _PlaylistsScreenState extends State { if (isExpanded) { for (final playlist in folderPlaylists) { sliverItems.add( - _buildDismissiblePlaylist( + _buildPlaylistRow( playlist, - playlistProvider, indented: true, ), ); @@ -182,7 +154,7 @@ class _PlaylistsScreenState extends State { // Root-level playlists for (final playlist in rootPlaylists) { sliverItems.add( - _buildDismissiblePlaylist(playlist, playlistProvider), + _buildPlaylistRow(playlist), ); } @@ -249,167 +221,24 @@ class _PlaylistsScreenState extends State { ); } - Future _showFolderActions( - BuildContext context, { - required PlaylistFolder folder, - required List playlists, - required PlaylistFolderProvider folderProvider, - }) async { - HapticFeedback.mediumImpact(); - final playableProvider = context.read(); - - await showCupertinoModalPopup( - context: context, - builder: (sheetContext) => CupertinoActionSheet( - title: Text(folder.name), - actions: [ - CupertinoActionSheetAction( - onPressed: () async { - Navigator.pop(sheetContext); - final songs = []; - for (final p in playlists) { - songs.addAll(await playableProvider.fetchForPlaylist(p.id)); - } - if (songs.isNotEmpty) { - audioHandler.replaceQueue(songs); - } else { - showOverlay(context, caption: 'No songs found.', icon: CupertinoIcons.nosign); - } - }, - child: const Text('Play All'), - ), - CupertinoActionSheetAction( - onPressed: () async { - Navigator.pop(sheetContext); - final songs = []; - for (final p in playlists) { - songs.addAll(await playableProvider.fetchForPlaylist(p.id)); - } - if (songs.isNotEmpty) { - audioHandler.replaceQueue(songs, shuffle: true); - } else { - showOverlay(context, caption: 'No songs found.', icon: CupertinoIcons.nosign); - } - }, - child: const Text('Shuffle All'), - ), - CupertinoActionSheetAction( - onPressed: () { - Navigator.pop(sheetContext); - _showRenameFolder(context, folder: folder, provider: folderProvider); - }, - child: const Text('Rename'), - ), - CupertinoActionSheetAction( - isDestructiveAction: true, - onPressed: () async { - Navigator.pop(sheetContext); - final confirmed = await _confirmDeleteFolder(context, folder: folder); - if (confirmed) { - for (final p in playlists) { - p.folderId = null; - } - folderProvider.remove(folder); - _expandedFolders.remove(folder.id); - } - }, - child: const Text('Delete'), - ), - ], - cancelButton: CupertinoActionSheetAction( - onPressed: () => Navigator.pop(sheetContext), - child: const Text('Cancel'), - ), - ), - ); - } - - Future _showRenameFolder( - BuildContext context, { - required PlaylistFolder folder, - required PlaylistFolderProvider provider, - }) async { - final controller = TextEditingController(text: folder.name); - - await showFormSheet( - context, - title: 'Rename Folder', - submitLabel: 'Save', - canSubmit: () => controller.text.trim().isNotEmpty, - onSubmit: () async { - final name = controller.text.trim(); - if (name.isEmpty) return; - try { - await provider.rename(folder, name: name); - Navigator.pop(context); - } catch (_) { - showOverlay(context, - caption: 'Error', - message: 'Could not rename folder.', - icon: Icons.error_outline, - ); - } - }, - builder: (context, setState) { - return FormTextField( - controller: controller, - autofocus: true, - onChanged: (_) => setState(() {}), - ); - }, - ); - } - - Future _confirmDeleteFolder( - BuildContext context, { - required PlaylistFolder folder, - }) async { - return await showCupertinoDialog( - context: context, - builder: (context) => CupertinoAlertDialog( - title: Text('Delete "${folder.name}"?'), - content: const Text( - 'Playlists in this folder will not be deleted.', - ), - actions: [ - CupertinoDialogAction( - child: const Text('Cancel'), - onPressed: () => Navigator.pop(context, false), - ), - CupertinoDialogAction( - isDestructiveAction: true, - child: const Text('Delete'), - onPressed: () => Navigator.pop(context, true), - ), - ], - ), - ) ?? false; - } - - Widget _buildDismissiblePlaylist( - Playlist playlist, - PlaylistProvider provider, { + Widget _buildPlaylistRow( + Playlist playlist, { bool indented = false, }) { return Card( child: Dismissible( - direction: DismissDirection.horizontal, - confirmDismiss: (direction) async { - if (direction == DismissDirection.startToEnd) { - final playableProvider = context.read(); - final songs = await playableProvider.fetchForPlaylist(playlist.id); - if (songs.isNotEmpty) { - for (final song in songs) { - await audioHandler.queueToBottom(song); - } - showOverlay(context, caption: 'Queued'); - } else { - showOverlay(context, caption: 'No songs found.', icon: CupertinoIcons.nosign); + direction: DismissDirection.startToEnd, + confirmDismiss: (_) async { + final playableProvider = context.read(); + final songs = await playableProvider.fetchForPlaylist(playlist.id); + if (songs.isNotEmpty) { + for (final song in songs) { + await audioHandler.queueToBottom(song); } - return false; + showOverlay(context, caption: 'Queued'); + } else { + showOverlay(context, caption: 'No songs found.', icon: CupertinoIcons.nosign); } - final confirmed = await confirmDelete(context, playlist: playlist); - if (confirmed) provider.remove(playlist); return false; }, background: Container( @@ -420,224 +249,15 @@ class _PlaylistsScreenState extends State { child: Icon(CupertinoIcons.text_badge_plus), ), ), - secondaryBackground: Container( - alignment: AlignmentDirectional.centerEnd, - color: AppColors.red, - child: const Padding( - padding: EdgeInsets.only(right: 28), - child: Icon(CupertinoIcons.delete), - ), - ), - key: ValueKey(playlist), - child: GestureDetector( - onLongPress: () => _showPlaylistActions( - context, - playlist: playlist, - provider: provider, - ), - child: Padding( - padding: EdgeInsets.only(left: indented ? 24 : 0), - child: PlaylistRow(playlist: playlist), - ), + key: ValueKey(playlist.id), + child: Padding( + padding: EdgeInsets.only(left: indented ? 24 : 0), + child: PlaylistRow(playlist: playlist), ), ), ); } - Future _showPlaylistActions( - BuildContext context, { - required Playlist playlist, - required PlaylistProvider provider, - }) async { - HapticFeedback.mediumImpact(); - final playableProvider = context.read(); - final folderProvider = context.read(); - - await showCupertinoModalPopup( - context: context, - builder: (sheetContext) => CupertinoActionSheet( - title: Text(playlist.name), - actions: [ - CupertinoActionSheetAction( - onPressed: () async { - Navigator.pop(sheetContext); - final songs = await playableProvider.fetchForPlaylist(playlist.id); - if (songs.isNotEmpty) { - audioHandler.replaceQueue(songs); - } else { - showOverlay(context, caption: 'No songs found.', icon: CupertinoIcons.nosign); - } - }, - child: const Text('Play'), - ), - CupertinoActionSheetAction( - onPressed: () async { - Navigator.pop(sheetContext); - final songs = await playableProvider.fetchForPlaylist(playlist.id); - if (songs.isNotEmpty) { - audioHandler.replaceQueue(songs, shuffle: true); - } else { - showOverlay(context, caption: 'No songs found.', icon: CupertinoIcons.nosign); - } - }, - child: const Text('Shuffle'), - ), - CupertinoActionSheetAction( - onPressed: () async { - Navigator.pop(sheetContext); - final songs = await playableProvider.fetchForPlaylist(playlist.id); - if (songs.isNotEmpty) { - for (final song in songs) { - await audioHandler.queueToBottom(song); - } - showOverlay(context, caption: 'Queued'); - } else { - showOverlay(context, caption: 'No songs found.', icon: CupertinoIcons.nosign); - } - }, - child: const Text('Queue'), - ), - CupertinoActionSheetAction( - onPressed: () { - Navigator.pop(sheetContext); - _showEditPlaylist( - context, - playlist: playlist, - provider: provider, - folderProvider: folderProvider, - ); - }, - child: const Text('Edit'), - ), - CupertinoActionSheetAction( - isDestructiveAction: true, - onPressed: () async { - Navigator.pop(sheetContext); - final confirmed = await confirmDelete(context, playlist: playlist); - if (confirmed) provider.remove(playlist); - }, - child: const Text('Delete'), - ), - ], - cancelButton: CupertinoActionSheetAction( - onPressed: () => Navigator.pop(sheetContext), - child: const Text('Cancel'), - ), - ), - ); - } - - Future _showEditPlaylist( - BuildContext context, { - required Playlist playlist, - required PlaylistProvider provider, - required PlaylistFolderProvider folderProvider, - }) async { - final nameController = TextEditingController(text: playlist.name); - final descController = - TextEditingController(text: playlist.description ?? ''); - final folders = folderProvider.folders; - String? selectedFolderId = playlist.folderId; - - await showFormSheet( - context, - title: 'Edit Playlist', - submitLabel: 'Save', - canSubmit: () => nameController.text.trim().isNotEmpty, - onSubmit: () async { - final name = nameController.text.trim(); - if (name.isEmpty) return; - - try { - await provider.update( - playlist, - name: name, - description: descController.text.trim(), - folderId: selectedFolderId, - ); - Navigator.pop(context); - showOverlay(context, caption: 'Playlist updated'); - } catch (_) { - showOverlay(context, - caption: 'Error', - message: 'Could not update playlist.', - icon: Icons.error_outline, - ); - } - }, - builder: (context, setState) { - return Column( - children: [ - FormTextField( - controller: nameController, - placeholder: 'Playlist Name', - autofocus: true, - onChanged: (_) => setState(() {}), - ), - const SizedBox(height: 8), - FormTextField( - controller: descController, - placeholder: 'Description (optional)', - maxLines: 2, - ), - if (folders.isNotEmpty) ...[ - const SizedBox(height: 8), - FormDropdown( - value: selectedFolderId, - items: [null, ...folders.map((f) => f.id)], - labelBuilder: (id) { - if (id == null) return 'No folder'; - final folder = folders.cast().firstWhere( - (f) => f.id == id, orElse: () => null); - return folder?.name ?? 'No folder'; - }, - placeholder: 'No folder', - onChanged: (id) => setState(() => selectedFolderId = id), - ), - ], - ], - ); - }, - ); - } - - Future confirmDelete( - BuildContext context, { - required Playlist playlist, - }) async { - return await showCupertinoDialog( - context: context, - builder: (BuildContext context) { - return CupertinoAlertDialog( - title: RichText( - textAlign: TextAlign.center, - text: TextSpan( - children: [ - const TextSpan(text: 'Delete the playlist '), - TextSpan( - text: playlist.name, - style: const TextStyle(fontWeight: FontWeight.bold), - ), - const TextSpan(text: '?'), - ], - ), - ), - content: const Text('You cannot undo this action.'), - actions: [ - CupertinoDialogAction( - child: const Text('Cancel'), - onPressed: () => Navigator.pop(context, false), - ), - CupertinoDialogAction( - child: const Text('Confirm'), - isDestructiveAction: true, - onPressed: () => Navigator.pop(context, true), - ), - ], - ); - }, - ); - } } class _FolderRow extends StatelessWidget { @@ -645,7 +265,6 @@ class _FolderRow extends StatelessWidget { final int playlistCount; final bool isExpanded; final VoidCallback onTap; - final VoidCallback? onLongPress; const _FolderRow({ Key? key, @@ -653,14 +272,12 @@ class _FolderRow extends StatelessWidget { required this.playlistCount, required this.isExpanded, required this.onTap, - this.onLongPress, }) : super(key: key); @override Widget build(BuildContext context) { return InkWell( onTap: onTap, - onLongPress: onLongPress, child: ListTile( shape: Border(bottom: Divider.createBorderSide(context)), leading: Icon( diff --git a/lib/ui/screens/podcasts.dart b/lib/ui/screens/podcasts.dart index 742748ff..511f7e4b 100644 --- a/lib/ui/screens/podcasts.dart +++ b/lib/ui/screens/podcasts.dart @@ -80,12 +80,10 @@ class _PodcastScreenState extends State { if (podcasts.isEmpty) { widgets = [ - SliverToBoxAdapter( - child: NoPodcastsScreen( - onTap: () { - widget.router.showAddPodcastSheet(context); - }, - ), + navigationBar!, + const SliverFillRemaining( + hasScrollBody: false, + child: NoPodcastsScreen(), ) ]; } else { @@ -96,27 +94,9 @@ class _PodcastScreenState extends State { (BuildContext context, int index) { var podcast = podcasts[index]; - return Card( - child: Dismissible( - direction: DismissDirection.endToStart, - confirmDismiss: (_) => confirmUnsubscribe(context), - onDismissed: (_) => provider.unsubscribePodcast( - podcast, - ), - background: Container( - alignment: AlignmentDirectional.centerEnd, - color: AppColors.red, - child: const Padding( - padding: EdgeInsets.only(right: 28), - child: Icon(CupertinoIcons.delete), - ), - ), - key: ValueKey(podcast), - child: PodcastRow( - podcast: podcast, - router: widget.router, - ), - ), + return PodcastRow( + podcast: podcast, + router: widget.router, ); }, childCount: podcasts.length, @@ -134,26 +114,13 @@ class _PodcastScreenState extends State { child: CupertinoSliverNavigationBar( backgroundColor: AppColors.staticScreenHeaderBackground, largeTitle: const LargeTitle(text: 'Podcasts'), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - visualDensity: VisualDensity.compact, - onPressed: () => widget.router.showAddPodcastSheet(context), - icon: const Icon( - CupertinoIcons.add_circled, - size: 23, - ), - ), - PodcastSortButton( - currentField: _sortConfig.field, - currentOrder: _sortConfig.order, - onMenuItemSelected: (config) { - setState(() => _sortConfig = config); - AppState.set('podcast.sort', _sortConfig); - }, - ), - ], + trailing: PodcastSortButton( + currentField: _sortConfig.field, + currentOrder: _sortConfig.order, + onMenuItemSelected: (config) { + setState(() => _sortConfig = config); + AppState.set('podcast.sort', _sortConfig); + }, ), ), ), @@ -162,31 +129,6 @@ class _PodcastScreenState extends State { ); } - Future confirmUnsubscribe(BuildContext context) async { - return await showCupertinoDialog( - context: context, - builder: (BuildContext context) { - return CupertinoAlertDialog( - title: RichText( - textAlign: TextAlign.center, - text: TextSpan(text: 'Unsubscribe from this podcast?'), - ), - content: const Text('You cannot undo this action.'), - actions: [ - CupertinoDialogAction( - child: const Text('Cancel'), - onPressed: () => Navigator.pop(context, false), - ), - CupertinoDialogAction( - child: const Text('Confirm'), - isDestructiveAction: true, - onPressed: () => Navigator.pop(context, true), - ), - ], - ); - }, - ); - } } class PodcastRow extends StatelessWidget { @@ -220,26 +162,21 @@ class PodcastRow extends StatelessWidget { } class NoPodcastsScreen extends StatelessWidget { - final void Function() onTap; - - const NoPodcastsScreen({Key? key, required this.onTap}) : super(key: key); + const NoPodcastsScreen({Key? key}) : super(key: key); @override Widget build(BuildContext context) { - return Container( - height: MediaQuery.of(context).size.height, - alignment: Alignment.center, + return const Center( child: Wrap( spacing: 16.0, direction: Axis.vertical, crossAxisAlignment: WrapCrossAlignment.center, children: [ - const Icon( + Icon( CupertinoIcons.exclamationmark_square, size: 56.0, ), - const Text('No podcasts available.'), - ElevatedButton(onPressed: onTap, child: Text('Add a Podcast')), + Text('No podcasts available.'), ], ), ); @@ -256,8 +193,8 @@ class PodcastSortConfig { class PodcastSortButton extends StatelessWidget { final void Function(PodcastSortConfig sortConfig)? onMenuItemSelected; - PodcastSortField currentField; - SortOrder currentOrder; + final PodcastSortField currentField; + final SortOrder currentOrder; static const fields = { PodcastSortField.lastPlayedAt: 'Last played', @@ -266,12 +203,12 @@ class PodcastSortButton extends StatelessWidget { PodcastSortField.author: 'Author', }; - PodcastSortButton({ + const PodcastSortButton({ Key? key, required this.currentField, required this.currentOrder, this.onMenuItemSelected, - }) : super(key: key) {} + }) : super(key: key); PopupMenuItem buildMenuItem( PodcastSortField field, @@ -306,16 +243,13 @@ class PodcastSortButton extends StatelessWidget { size: 25, ), onSelected: (item) { - if (item == currentField) { - currentOrder = - currentOrder == SortOrder.asc ? SortOrder.desc : SortOrder.asc; - } else { - currentOrder = SortOrder.asc; - } + final newOrder = item == currentField + ? (currentOrder == SortOrder.asc ? SortOrder.desc : SortOrder.asc) + : SortOrder.asc; onMenuItemSelected?.call(PodcastSortConfig( field: item, - order: currentOrder, + order: newOrder, )); }, itemBuilder: (_) => diff --git a/lib/ui/screens/radio_stations.dart b/lib/ui/screens/radio_stations.dart index aaee25c7..5b2b0f25 100644 --- a/lib/ui/screens/radio_stations.dart +++ b/lib/ui/screens/radio_stations.dart @@ -1,7 +1,4 @@ -import 'dart:convert'; - import 'package:app/constants/constants.dart'; -import 'package:app/exceptions/http_response_exception.dart'; import 'package:app/models/models.dart'; import 'package:app/providers/providers.dart'; import 'package:app/ui/widgets/widgets.dart'; @@ -9,7 +6,6 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:figma_squircle/figma_squircle.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; class RadioStationsScreen extends StatefulWidget { @@ -23,6 +19,7 @@ class RadioStationsScreen extends StatefulWidget { class _RadioStationsScreenState extends State { var _loading = false; + var _errored = false; @override void initState() { @@ -32,11 +29,15 @@ class _RadioStationsScreenState extends State { Future _fetchData() async { if (_loading) return; - setState(() => _loading = true); + setState(() { + _errored = false; + _loading = true; + }); try { await context.read().fetchAll(); } catch (_) { + if (mounted) setState(() => _errored = true); } finally { if (mounted) setState(() => _loading = false); } @@ -60,12 +61,13 @@ class _RadioStationsScreenState extends State { CupertinoSliverNavigationBar( backgroundColor: AppColors.staticScreenHeaderBackground, largeTitle: const LargeTitle(text: 'Radio'), - trailing: IconButton( - onPressed: () => _showAddStation(context, provider), - icon: const Icon(CupertinoIcons.add_circled), - ), ), - if (stations.isEmpty && !_loading) + if (_errored && stations.isEmpty) + SliverFillRemaining( + hasScrollBody: false, + child: OopsBox(onRetry: _fetchData), + ) + else if (stations.isEmpty && !_loading) SliverFillRemaining( hasScrollBody: false, child: Center( @@ -85,50 +87,24 @@ class _RadioStationsScreenState extends State { ), const SizedBox(height: 8), const Text( - 'Add a station to start listening.', + 'No radio stations available.', style: TextStyle(color: Colors.white54), ), ], ), ), ) - else + else if (stations.isNotEmpty) SliverList( delegate: SliverChildBuilderDelegate( (context, index) { if (index >= stations.length) return null; final station = stations[index]; - return GestureDetector( - onLongPress: () => _showStationActions( - context, + return Card( + child: _RadioStationRow( station: station, - provider: provider, - ), - child: Card( - child: Dismissible( - direction: DismissDirection.endToStart, - confirmDismiss: (_) async { - return await _confirmDelete( - context, - station: station, - ); - }, - onDismissed: (_) => provider.remove(station), - background: Container( - alignment: AlignmentDirectional.centerEnd, - color: AppColors.red, - child: const Padding( - padding: EdgeInsets.only(right: 28), - child: Icon(CupertinoIcons.delete), - ), - ), - key: ValueKey(station.id), - child: _RadioStationRow( - station: station, - onTap: () => _playStation(station), - ), - ), + onTap: () => _playStation(station), ), ); }, @@ -168,245 +144,6 @@ class _RadioStationsScreenState extends State { } } - void _showAddStation( - BuildContext context, RadioStationProvider provider) async { - final nameController = TextEditingController(); - final urlController = TextEditingController(); - final descController = TextEditingController(); - var isPublic = false; - - await showFormSheet( - context, - title: 'Add Radio Station', - submitLabel: 'Add', - canSubmit: () => - nameController.text.trim().isNotEmpty && - urlController.text.trim().isNotEmpty, - onSubmit: () async { - final name = nameController.text.trim(); - final url = urlController.text.trim(); - if (name.isEmpty || url.isEmpty) return; - - try { - await provider.create( - name: name, - url: url, - description: descController.text.trim(), - isPublic: isPublic, - ); - Navigator.pop(context); - showOverlay(context, caption: 'Station added'); - } catch (e) { - 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, - ); - } - }, - builder: (context, setState) => Column( - children: [ - FormTextField( - controller: nameController, - placeholder: 'Station Name', - autofocus: true, - onChanged: (_) => setState(() {}), - ), - const SizedBox(height: 8), - FormTextField( - controller: urlController, - placeholder: 'Stream URL', - keyboardType: TextInputType.url, - onChanged: (_) => setState(() {}), - ), - const SizedBox(height: 8), - FormTextField( - controller: descController, - placeholder: 'Description (optional)', - maxLines: 2, - ), - const SizedBox(height: 8), - Row( - children: [ - CupertinoSwitch( - value: isPublic, - onChanged: (v) => setState(() => isPublic = v), - ), - const SizedBox(width: 8), - const Text('This station is public', - style: TextStyle(fontSize: 14)), - ], - ), - ], - ), - ); - } - - Future _showStationActions( - BuildContext context, { - required RadioStation station, - required RadioStationProvider provider, - }) async { - HapticFeedback.mediumImpact(); - - await showCupertinoModalPopup( - context: context, - builder: (sheetContext) => CupertinoActionSheet( - title: Text(station.name), - actions: [ - CupertinoActionSheetAction( - onPressed: () { - Navigator.pop(sheetContext); - _playStation(station); - }, - child: const Text('Play'), - ), - CupertinoActionSheetAction( - onPressed: () { - Navigator.pop(sheetContext); - _showEditStation(context, station: station, provider: provider); - }, - child: const Text('Edit'), - ), - CupertinoActionSheetAction( - isDestructiveAction: true, - onPressed: () async { - Navigator.pop(sheetContext); - final confirmed = - await _confirmDelete(context, station: station); - if (confirmed) provider.remove(station); - }, - child: const Text('Delete'), - ), - ], - cancelButton: CupertinoActionSheetAction( - onPressed: () => Navigator.pop(sheetContext), - child: const Text('Cancel'), - ), - ), - ); - } - - Future _showEditStation( - BuildContext context, { - required RadioStation station, - required RadioStationProvider provider, - }) async { - final nameController = TextEditingController(text: station.name); - final urlController = TextEditingController(text: station.url); - final descController = - TextEditingController(text: station.description ?? ''); - var isPublic = station.isPublic; - - await showFormSheet( - context, - title: 'Edit Radio Station', - submitLabel: 'Save', - canSubmit: () => - nameController.text.trim().isNotEmpty && - urlController.text.trim().isNotEmpty, - onSubmit: () async { - final name = nameController.text.trim(); - final url = urlController.text.trim(); - if (name.isEmpty || url.isEmpty) return; - - try { - await provider.update( - station, - name: name, - url: url, - description: descController.text.trim(), - isPublic: isPublic, - ); - Navigator.pop(context); - showOverlay(context, caption: 'Station updated'); - } catch (e) { - 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, - ); - } - }, - builder: (context, setState) => Column( - children: [ - FormTextField( - controller: nameController, - placeholder: 'Station Name', - autofocus: true, - onChanged: (_) => setState(() {}), - ), - const SizedBox(height: 8), - FormTextField( - controller: urlController, - placeholder: 'Stream URL', - keyboardType: TextInputType.url, - onChanged: (_) => setState(() {}), - ), - const SizedBox(height: 8), - FormTextField( - controller: descController, - placeholder: 'Description (optional)', - maxLines: 2, - ), - const SizedBox(height: 8), - Row( - children: [ - CupertinoSwitch( - value: isPublic, - onChanged: (v) => setState(() => isPublic = v), - ), - const SizedBox(width: 8), - const Text('This station is public', - style: TextStyle(fontSize: 14)), - ], - ), - ], - ), - ); - } - - Future _confirmDelete( - BuildContext context, { - required RadioStation station, - }) async { - return await showCupertinoDialog( - context: context, - builder: (context) => CupertinoAlertDialog( - title: Text('Delete "${station.name}"?'), - content: const Text('You cannot undo this action.'), - actions: [ - CupertinoDialogAction( - child: const Text('Cancel'), - onPressed: () => Navigator.pop(context, false), - ), - CupertinoDialogAction( - isDestructiveAction: true, - child: const Text('Delete'), - onPressed: () => Navigator.pop(context, true), - ), - ], - ), - ) ?? - false; - } } class _RadioStationRow extends StatelessWidget {