feat(flutter): replace Download with Regenerate on system playlist detail
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/<newId> 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) <noreply@anthropic.com>
This commit is contained in:
@@ -263,24 +263,32 @@ class _Header extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
if (playable.isNotEmpty) ...[
|
if (playable.isNotEmpty) ...[
|
||||||
OutlinedButton.icon(
|
if (p.isSystem)
|
||||||
key: const Key('download_playlist_button'),
|
OutlinedButton.icon(
|
||||||
onPressed: () {
|
key: const Key('regenerate_playlist_button'),
|
||||||
final mgr = ref.read(audioCacheManagerProvider);
|
onPressed: () => _regenerate(context, ref),
|
||||||
for (final t in playable) {
|
icon: const Icon(Icons.refresh, size: 16),
|
||||||
final id = t.trackId ?? '';
|
label: const Text('Regenerate'),
|
||||||
if (id.isEmpty) continue;
|
)
|
||||||
// Fire-and-forget; downloads happen in background.
|
else
|
||||||
// ignore: unawaited_futures
|
OutlinedButton.icon(
|
||||||
mgr.pin(id, source: CacheSource.autoPlaylist);
|
key: const Key('download_playlist_button'),
|
||||||
}
|
onPressed: () {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
final mgr = ref.read(audioCacheManagerProvider);
|
||||||
SnackBar(content: Text('Downloading ${playable.length} tracks…')),
|
for (final t in playable) {
|
||||||
);
|
final id = t.trackId ?? '';
|
||||||
},
|
if (id.isEmpty) continue;
|
||||||
icon: const Icon(Icons.download, size: 16),
|
// Fire-and-forget; downloads happen in background.
|
||||||
label: const Text('Download'),
|
// 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),
|
const SizedBox(width: 8),
|
||||||
FilledButton.icon(
|
FilledButton.icon(
|
||||||
onPressed: () {
|
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<void> _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 {
|
class _PlaylistTrackRow extends ConsumerWidget {
|
||||||
|
|||||||
Reference in New Issue
Block a user