From 45c72993f3ba123fcbe085e3437ef27660443826 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Fri, 15 May 2026 06:55:37 -0400 Subject: [PATCH] feat(flutter): replace Download with Regenerate on system playlist detail MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per operator decision: in the playlist detail header, system playlists (For You / Discover) now show a Regenerate button where user playlists keep Download. Offline-download is intentionally dropped for system playlists — operator chose the literal swap. - Regenerate calls PlaylistsApi.refreshSystem(variant), invalidates playlistsListProvider (home row tile rebinds to the rotated UUID), and pushReplacement's to /playlists/ so the open detail screen rebinds instead of 404-ing on the stale id. - Null id (empty library) and errors surface as snackbars. - User playlists are unchanged (Download + Play). The home-card kebab (#416, 7a04370) stays — web has refresh in both the detail view and the home tile, so this matches web parity. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../lib/playlists/playlist_detail_screen.dart | 75 ++++++++++++++----- 1 file changed, 57 insertions(+), 18 deletions(-) diff --git a/flutter_client/lib/playlists/playlist_detail_screen.dart b/flutter_client/lib/playlists/playlist_detail_screen.dart index 50c4d12b..957abd9d 100644 --- a/flutter_client/lib/playlists/playlist_detail_screen.dart +++ b/flutter_client/lib/playlists/playlist_detail_screen.dart @@ -263,24 +263,32 @@ class _Header extends ConsumerWidget { ), const Spacer(), if (playable.isNotEmpty) ...[ - OutlinedButton.icon( - key: const Key('download_playlist_button'), - onPressed: () { - final mgr = ref.read(audioCacheManagerProvider); - for (final t in playable) { - final id = t.trackId ?? ''; - if (id.isEmpty) continue; - // Fire-and-forget; downloads happen in background. - // ignore: unawaited_futures - mgr.pin(id, source: CacheSource.autoPlaylist); - } - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Downloading ${playable.length} tracks…')), - ); - }, - icon: const Icon(Icons.download, size: 16), - label: const Text('Download'), - ), + if (p.isSystem) + OutlinedButton.icon( + key: const Key('regenerate_playlist_button'), + onPressed: () => _regenerate(context, ref), + icon: const Icon(Icons.refresh, size: 16), + label: const Text('Regenerate'), + ) + else + OutlinedButton.icon( + key: const Key('download_playlist_button'), + onPressed: () { + final mgr = ref.read(audioCacheManagerProvider); + for (final t in playable) { + final id = t.trackId ?? ''; + if (id.isEmpty) continue; + // Fire-and-forget; downloads happen in background. + // ignore: unawaited_futures + mgr.pin(id, source: CacheSource.autoPlaylist); + } + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Downloading ${playable.length} tracks…')), + ); + }, + icon: const Icon(Icons.download, size: 16), + label: const Text('Download'), + ), const SizedBox(width: 8), FilledButton.icon( onPressed: () { @@ -299,6 +307,37 @@ class _Header extends ConsumerWidget { ]), ); } + + /// Forces a server-side rebuild of this system playlist. The + /// rebuild rotates the playlist UUID, so the old detail route now + /// 404s — rebind by pushReplacement-ing to the new id. The + /// aggregate list is invalidated too so the home row's tile points + /// at the fresh UUID. ScaffoldMessenger + router captured before + /// the await so we don't touch a stale BuildContext after. + Future _regenerate(BuildContext context, WidgetRef ref) async { + final messenger = ScaffoldMessenger.of(context); + final router = GoRouter.of(context); + final p = detail.playlist; + try { + final api = await ref.read(playlistsApiProvider.future); + final newId = await api.refreshSystem(p.systemVariant!); + ref.invalidate(playlistsListProvider); + if (newId == null) { + messenger.showSnackBar(const SnackBar( + content: Text('Nothing to build yet — library is empty.'), + )); + return; + } + messenger.showSnackBar( + SnackBar(content: Text('${p.name} regenerated')), + ); + router.pushReplacement('/playlists/$newId'); + } catch (e) { + messenger.showSnackBar( + SnackBar(content: Text("Couldn't regenerate: $e")), + ); + } + } } class _PlaylistTrackRow extends ConsumerWidget {