Files
minstrel/flutter_client/lib/player/now_playing_screen.dart
T
bvandeusen c8baaa5329 feat(flutter): wire TrackActionsButton into 4 surfaces
- TrackRow gains an actions: bool = true param; renders the button
  after duration. Album detail + Search + History/Liked tabs all
  consume TrackRow so they pick up the menu transitively.
- CompactTrackCard (home Most-played) gets the button at the end of
  its inner row.
- _PlaylistTrackRow in playlist_detail_screen gets the button next
  to duration; skipped for unavailable rows (no track id).
- NowPlayingScreen renders the button next to the title with
  hideQueueActions: true (Play next / Add to queue suppressed since
  the menu's track IS the playing one). artistId is left empty since
  it's not stashed in MediaItem.extras — Go-to-artist will route to
  /artists/ which is benign for v1; a follow-up could stash it.

Closes Tier 1 follow-up #2 (track-level actions menu).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 13:43:12 -04:00

107 lines
3.9 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../models/track.dart';
import '../shared/widgets/track_actions/track_actions_button.dart';
import '../theme/theme_extension.dart';
import 'player_provider.dart';
class NowPlayingScreen extends ConsumerWidget {
const NowPlayingScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final fs = Theme.of(context).extension<FabledSwordTheme>()!;
final media = ref.watch(mediaItemProvider).value;
final playback = ref.watch(playbackStateProvider).value;
if (media == null) {
return const Scaffold(body: Center(child: Text('Nothing playing.')));
}
final pos = playback?.updatePosition ?? Duration.zero;
final dur = media.duration ?? Duration.zero;
return Scaffold(
backgroundColor: fs.obsidian,
appBar: AppBar(
backgroundColor: fs.obsidian,
leading: IconButton(
icon: Icon(Icons.expand_more, color: fs.parchment),
onPressed: () => Navigator.of(context).pop(),
),
actions: [
IconButton(
icon: Icon(Icons.queue_music, color: fs.parchment),
tooltip: 'Queue',
onPressed: () => GoRouter.of(context).push('/queue'),
),
],
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(children: [
const Spacer(),
Container(
width: 280, height: 280, color: fs.slate,
),
const SizedBox(height: 24),
Row(mainAxisAlignment: MainAxisAlignment.center, children: [
Flexible(
child: Text(
media.title,
style: TextStyle(color: fs.parchment, fontSize: 22),
overflow: TextOverflow.ellipsis,
),
),
TrackActionsButton(
track: TrackRef(
id: media.id,
title: media.title,
albumId: (media.extras?['album_id'] as String?) ?? '',
albumTitle: media.album ?? '',
artistId: '', // not stashed in MediaItem.extras today
artistName: media.artist ?? '',
durationSec: media.duration?.inSeconds ?? 0,
streamUrl: '',
),
hideQueueActions: true,
),
]),
Text(media.artist ?? '', style: TextStyle(color: fs.ash, fontSize: 14)),
const SizedBox(height: 16),
Slider(
activeColor: fs.accent,
min: 0,
max: dur.inMilliseconds.toDouble().clamp(1, double.infinity),
value: pos.inMilliseconds.toDouble().clamp(0, dur.inMilliseconds.toDouble()),
onChanged: (v) => ref.read(audioHandlerProvider).seek(Duration(milliseconds: v.toInt())),
),
Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [
IconButton(
icon: Icon(Icons.skip_previous, color: fs.parchment, size: 32),
onPressed: () => ref.read(audioHandlerProvider).skipToPrevious(),
),
IconButton(
iconSize: 56,
icon: Icon(playback?.playing == true ? Icons.pause_circle_filled : Icons.play_circle_filled, color: fs.accent),
onPressed: () {
final h = ref.read(audioHandlerProvider);
if (playback?.playing == true) { h.pause(); } else { h.play(); }
},
),
IconButton(
icon: Icon(Icons.skip_next, color: fs.parchment, size: 32),
onPressed: () => ref.read(audioHandlerProvider).skipToNext(),
),
]),
const Spacer(),
]),
),
),
);
}
}