feat(flutter): full player — combined action row above seek

Move shuffle / repeat / queue out from below the play controls and
combine with the like + kebab into a single row sitting just above
the seek bar. Title row drops back to title-only (truly centered now,
no Stack needed since nothing competes for the row's right edge).

Layout order is now: art → title → artist → album → [shuffle, repeat,
queue, like, kebab] → seek → prev/play/next.

The "queue" icon in the top-right of the AppBar stays for now —
redundant with the new row but matches what users have already
muscle-memoried for opening the queue from any player state.
This commit is contained in:
2026-05-11 17:25:27 -04:00
parent 8c00ee21c7
commit a7f35a5d6d
@@ -113,20 +113,21 @@ class _NowPlayingScreenState extends ConsumerState<NowPlayingScreen> {
overflow: TextOverflow.ellipsis,
),
],
const SizedBox(height: 32),
_SeekRow(position: pos, duration: dur, fs: fs, ref: ref),
const SizedBox(height: 24),
_PrimaryControls(
fs: fs,
ref: ref,
isPlaying: isPlaying,
),
const SizedBox(height: 24),
_SecondaryControls(
fs: fs,
actions: actions,
shuffleOn: shuffleOn,
repeatMode: repeatMode,
media: media,
),
const SizedBox(height: 8),
_SeekRow(position: pos, duration: dur, fs: fs, ref: ref),
const SizedBox(height: 24),
_PrimaryControls(
fs: fs,
ref: ref,
isPlaying: isPlaying,
),
const SizedBox(height: 24),
],
@@ -219,48 +220,14 @@ class _TitleRow extends StatelessWidget {
@override
Widget build(BuildContext context) {
// Stack: title centered absolutely in the row; like + kebab pinned
// to the right edge. Padding on the title equals the actions' width
// so it stays optically centered without colliding with them.
return SizedBox(
height: 32,
child: Stack(
alignment: Alignment.center,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 64),
child: Text(
media.title,
style: TextStyle(color: fs.parchment, fontSize: 22),
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
),
),
Positioned(
right: 0,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
LikeButton(kind: LikeKind.track, id: media.id, size: 22),
TrackActionsButton(
track: TrackRef(
id: media.id,
title: media.title,
albumId: (media.extras?['album_id'] as String?) ?? '',
albumTitle: media.album ?? '',
artistId: (media.extras?['artist_id'] as String?) ?? '',
artistName: media.artist ?? '',
durationSec: media.duration?.inSeconds ?? 0,
streamUrl: '',
),
hideQueueActions: true,
),
],
),
),
],
),
// Title-only — like + kebab moved into _SecondaryControls above
// the seek bar so they share the same row as shuffle/repeat/queue.
return Text(
media.title,
style: TextStyle(color: fs.parchment, fontSize: 22),
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
);
}
}
@@ -365,17 +332,23 @@ class _PrimaryControls extends StatelessWidget {
}
}
/// Action row sitting just above the seek bar. Holds shuffle / repeat
/// / queue plus the like + kebab that used to live in the title row,
/// so the title can sit truly centered above and this row carries
/// every per-track action in one place.
class _SecondaryControls extends StatelessWidget {
const _SecondaryControls({
required this.fs,
required this.actions,
required this.shuffleOn,
required this.repeatMode,
required this.media,
});
final FabledSwordTheme fs;
final PlayerActions actions;
final bool shuffleOn;
final AudioServiceRepeatMode repeatMode;
final MediaItem media;
@override
Widget build(BuildContext context) {
@@ -411,6 +384,20 @@ class _SecondaryControls extends StatelessWidget {
icon: Icon(Icons.queue_music, color: fs.ash),
onPressed: () => GoRouter.of(context).push('/queue'),
),
LikeButton(kind: LikeKind.track, id: media.id, size: 22),
TrackActionsButton(
track: TrackRef(
id: media.id,
title: media.title,
albumId: (media.extras?['album_id'] as String?) ?? '',
albumTitle: media.album ?? '',
artistId: (media.extras?['artist_id'] as String?) ?? '',
artistName: media.artist ?? '',
durationSec: media.duration?.inSeconds ?? 0,
streamUrl: '',
),
hideQueueActions: true,
),
],
);
}