From ce367608199e3edea10c46c3dbb714c128b9d9c9 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Fri, 15 May 2026 23:03:41 -0400 Subject: [PATCH] =?UTF-8?q?feat(playlists):=20#417=20=E2=80=94=20"Refreshe?= =?UTF-8?q?d=20=E2=80=A6"=20subtitle=20on=20system=20tiles?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the last buildable item of the #411 system-playlists-v2 umbrella. System playlists atomic-replace on rebuild, so created_at (already on the wire โ€” no server change) is the last-rotated time. Surface it as a small tile subtitle so users see how fresh a mix is: "Refreshed just now / today / yesterday / N days ago / Mon D". - web PlaylistCard: refreshedLabel() + a muted footer line, shown only when system_variant != null. Unparseable/empty timestamp โ†’ suppressed (web test fixtures use created_at:'' so no test churn). - flutter PlaylistCard: mirrored _refreshedLabel() + subtitle under the system badge for isSystem playlists. Friendly wording deliberately distinct from HistoryRow's "m/h ago"; per-surface helper per the project's existing relative-time convention. CI-pending; closes with the umbrella on device-check. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../lib/playlists/widgets/playlist_card.dart | 31 +++++++++++++++++++ web/src/lib/components/PlaylistCard.svelte | 24 ++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/flutter_client/lib/playlists/widgets/playlist_card.dart b/flutter_client/lib/playlists/widgets/playlist_card.dart index cf439056..c7a0a2c5 100644 --- a/flutter_client/lib/playlists/widgets/playlist_card.dart +++ b/flutter_client/lib/playlists/widgets/playlist_card.dart @@ -122,6 +122,16 @@ class PlaylistCard extends ConsumerWidget { ), ), ), + if (playlist.isSystem && _refreshedLabel(playlist.createdAt) != '') + Padding( + padding: const EdgeInsets.only(top: 2), + child: Text( + _refreshedLabel(playlist.createdAt), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle(color: fs.ash, fontSize: 11), + ), + ), ]), ), ), @@ -131,6 +141,27 @@ class PlaylistCard extends ConsumerWidget { String get _refreshLabel => 'Refresh ${playlist.name}'; + /// #417: system playlists atomic-replace on rebuild, so createdAt + /// is the last-rotated time. Friendly wording mirrors the web + /// PlaylistCard. Empty string when the timestamp is unparseable. + String _refreshedLabel(String iso) { + final t = DateTime.tryParse(iso); + if (t == null) return ''; + final now = DateTime.now(); + final mins = now.difference(t).inMinutes; + if (mins < 5) return 'Refreshed just now'; + final startOfToday = DateTime(now.year, now.month, now.day); + if (!t.isBefore(startOfToday)) return 'Refreshed today'; + final days = startOfToday.difference(DateTime(t.year, t.month, t.day)).inDays; + if (days <= 1) return 'Refreshed yesterday'; + if (days < 7) return 'Refreshed $days days ago'; + const months = [ + 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec', + ]; + return 'Refreshed ${months[t.month - 1]} ${t.day}'; + } + /// Forces a server-side rebuild of this system playlist, then /// invalidates the aggregate list so the rotated UUID + new tracks /// land on the next read. ScaffoldMessenger is captured before the diff --git a/web/src/lib/components/PlaylistCard.svelte b/web/src/lib/components/PlaylistCard.svelte index 1436afca..cd401158 100644 --- a/web/src/lib/components/PlaylistCard.svelte +++ b/web/src/lib/components/PlaylistCard.svelte @@ -17,6 +17,27 @@ // (#411 R2) โ€” no hardcoded kind list here. const refreshable = $derived(playlist.refreshable === true); + // #417: system playlists atomic-replace on rebuild, so created_at + // is the last-rotated time. Surface it so users know how fresh the + // mix is. Friendly wording, not the HistoryRow "m/h ago" style. + function refreshedLabel(iso: string): string { + const t = new Date(iso); + if (Number.isNaN(t.getTime())) return ''; + const now = new Date(); + const mins = Math.floor((now.getTime() - t.getTime()) / 60000); + if (mins < 5) return 'Refreshed just now'; + const startOfToday = new Date(now); + startOfToday.setHours(0, 0, 0, 0); + const days = Math.floor((startOfToday.getTime() - t.getTime()) / 86400000) + 1; + if (t.getTime() >= startOfToday.getTime()) return 'Refreshed today'; + if (days <= 1) return 'Refreshed yesterday'; + if (days < 7) return `Refreshed ${days} days ago`; + return `Refreshed ${t.toLocaleDateString(undefined, { month: 'short', day: 'numeric' })}`; + } + const refreshed = $derived( + playlist.system_variant != null ? refreshedLabel(playlist.created_at) : '' + ); + const queryClient = useQueryClient(); let starting = $state(false); @@ -153,6 +174,9 @@ ยท by {playlist.owner_username} {/if} + {#if refreshed} +
{refreshed}
+ {/if}