import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../state/providers.dart'; import '../subsonic/models.dart'; import '../theme/tokens.dart'; import '../widgets/toast.dart'; /// Bottom sheet to add [songs] to an existing playlist or a new one. No-op when /// offline (playlist mutations require the server). Future showAddToPlaylistSheet( BuildContext context, { required List songs, }) { return showModalBottomSheet( context: context, backgroundColor: TimbreColors.background, isScrollControlled: true, builder: (_) => _AddToPlaylistSheet(songs: songs), ); } class _AddToPlaylistSheet extends ConsumerWidget { const _AddToPlaylistSheet({required this.songs}); final List songs; @override Widget build(BuildContext context, WidgetRef ref) { final accent = Theme.of(context).colorScheme.primary; // Only your own playlists — the server rejects adding tracks to another // user's shared playlist. final playlists = ref.watch(myPlaylistsProvider); final connected = ref.watch(subsonicClientProvider) != null; return SafeArea( child: Padding( padding: const EdgeInsets.symmetric(vertical: TimbreSpacing.lg), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: TimbreSpacing.xl), child: Text('Add to playlist', style: TextStyle(color: accent, fontWeight: FontWeight.w700)), ), const SizedBox(height: TimbreSpacing.md), if (!connected) const Padding( padding: EdgeInsets.all(TimbreSpacing.xl), child: Text('Connect to a server to manage playlists.', style: TextStyle(color: TimbreColors.dimmed)), ) else ...[ _Tile( icon: Icons.add, label: 'New playlist…', accent: accent, onTap: () => _createAndAdd(context, ref), ), Flexible( child: ListView( shrinkWrap: true, children: [ for (final p in playlists) _Tile( icon: Icons.queue_music, label: p.name, trailing: p.songCount != null ? '${p.songCount}' : null, onTap: () async { await ref .read(playlistsProvider.notifier) .addTracks(p.id, songs); if (context.mounted) { Navigator.of(context).pop(); _toast(context, 'Added to ${p.name}'); } }, ), ], ), ), ], ], ), ), ); } Future _createAndAdd(BuildContext context, WidgetRef ref) async { final name = await promptPlaylistName(context, title: 'New playlist'); if (name == null || name.isEmpty) return; final id = await ref.read(playlistsProvider.notifier).create(name); if (id != null) { await ref.read(playlistsProvider.notifier).addTracks(id, songs); } if (context.mounted) { Navigator.of(context).pop(); _toast(context, id != null ? 'Added to $name' : 'Could not create playlist'); } } } class _Tile extends StatelessWidget { const _Tile({ required this.icon, required this.label, required this.onTap, this.trailing, this.accent, }); final IconData icon; final String label; final VoidCallback onTap; final String? trailing; final Color? accent; @override Widget build(BuildContext context) { return InkWell( onTap: onTap, child: Container( constraints: const BoxConstraints(minHeight: TimbreSpacing.minTouchTarget), padding: const EdgeInsets.symmetric(horizontal: TimbreSpacing.xl), child: Row( children: [ Icon(icon, size: 20, color: accent ?? TimbreColors.dimmed), const SizedBox(width: TimbreSpacing.lg), Expanded( child: Text(label, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle(color: accent ?? TimbreColors.foreground)), ), if (trailing != null) Text(trailing!, style: const TextStyle(color: TimbreColors.dimmed)), ], ), ), ); } } /// Shared name prompt used by create / rename flows. Returns the trimmed name /// or null if cancelled. Future promptPlaylistName( BuildContext context, { required String title, String initial = '', }) { final controller = TextEditingController(text: initial); return showDialog( context: context, builder: (ctx) { final accent = Theme.of(ctx).colorScheme.primary; return AlertDialog( backgroundColor: TimbreColors.surface, title: Text(title), content: TextField( controller: controller, autofocus: true, style: const TextStyle(color: TimbreColors.foreground), decoration: const InputDecoration( hintText: 'Playlist name', hintStyle: TextStyle(color: TimbreColors.dimmed), ), onSubmitted: (v) => Navigator.pop(ctx, v.trim()), ), actions: [ TextButton( onPressed: () => Navigator.pop(ctx), child: const Text('Cancel')), TextButton( onPressed: () => Navigator.pop(ctx, controller.text.trim()), child: Text('Save', style: TextStyle(color: accent)), ), ], ); }, ); } void _toast(BuildContext context, String message) => showToast(context, message);