feat(web,flutter): #401 — now-playing highlight on TrackRow + PlaylistTrackRow

Cross-platform consistency: when a track is currently playing, its row
on the album / liked / history / search / playlist detail surfaces
gets the same accent-border + bg-lift treatment that QueueTrackRow
applies on the queue panel. Closes the inconsistency caught during
the #375 DRY audit (the queue row had a "you are here" indicator;
other track-row surfaces did not).

Web — TrackRow.svelte + PlaylistTrackRow.svelte:
  isCurrent = player.current?.id === track.id
  → border-l-2 border-l-accent bg-surface-hover when true.
  PlaylistTrackRow additionally gates on !isUnavailable so deleted
  rows never match a phantom playing id.

Flutter — TrackRow + _PlaylistTrackRow:
  TrackRow becomes a ConsumerWidget, watches mediaItemProvider, and
  wraps its InkWell in a Container whose BoxDecoration carries the
  fs.iron background + 2px fs.accent left border when current. Title
  text also shifts to fs.accent and FontWeight.w500. Same pattern for
  _PlaylistTrackRow.

No "Now playing" pill on these surfaces — the player bar already
names the track, so the accent band alone reads as enough cue.

For #401 / #356 umbrella.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-13 14:05:33 -04:00
parent e282766268
commit bb9e979876
4 changed files with 135 additions and 80 deletions
@@ -233,51 +233,69 @@ class _Header extends ConsumerWidget {
}
}
class _PlaylistTrackRow extends StatelessWidget {
class _PlaylistTrackRow extends ConsumerWidget {
const _PlaylistTrackRow({required this.row, required this.onTap});
final PlaylistTrack row;
final VoidCallback? onTap;
@override
Widget build(BuildContext context) {
Widget build(BuildContext context, WidgetRef ref) {
final fs = Theme.of(context).extension<FabledSwordTheme>()!;
final mins = (row.durationSec ~/ 60).toString().padLeft(2, '0');
final secs = (row.durationSec % 60).toString().padLeft(2, '0');
final color = row.isAvailable ? fs.parchment : fs.ash;
return InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
child: Row(children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
row.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: color,
fontSize: 14,
decoration: row.isAvailable
? null
: TextDecoration.lineThrough,
// "Now playing" highlight — matches _QueueRow and TrackRow so the
// user sees which playlist row is current without reading the
// player bar. Unavailable rows never match.
final currentId = ref.watch(mediaItemProvider).value?.id;
final isCurrent = row.isAvailable &&
currentId != null &&
currentId == row.trackId;
final baseColor = row.isAvailable ? fs.parchment : fs.ash;
final titleColor = isCurrent ? fs.accent : baseColor;
return Container(
decoration: BoxDecoration(
color: isCurrent ? fs.iron : null,
border: isCurrent
? Border(left: BorderSide(color: fs.accent, width: 2))
: null,
),
child: InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
child: Row(children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
row.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: titleColor,
fontSize: 14,
fontWeight:
isCurrent ? FontWeight.w500 : FontWeight.w400,
decoration: row.isAvailable
? null
: TextDecoration.lineThrough,
),
),
),
Text(
'${row.artistName} · ${row.albumTitle}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(color: fs.ash, fontSize: 12),
),
],
Text(
'${row.artistName} · ${row.albumTitle}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(color: fs.ash, fontSize: 12),
),
],
),
),
),
Text('$mins:$secs', style: TextStyle(color: fs.ash, fontSize: 12)),
if (row.trackId != null)
TrackActionsButton(track: _toTrackRef(row)),
]),
Text('$mins:$secs', style: TextStyle(color: fs.ash, fontSize: 12)),
if (row.trackId != null)
TrackActionsButton(track: _toTrackRef(row)),
]),
),
),
);
}