feat(playlists): #417 — "Refreshed …" subtitle on system tiles
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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}
|
||||
</div>
|
||||
{#if refreshed}
|
||||
<div class="truncate text-xs text-text-muted">{refreshed}</div>
|
||||
{/if}
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user