feat(flutter): #396 — full-screen now-playing polish
Three of the four locked items (1, 2, 4); item 6 (swipe-tabs) stays deferred until server-side lyrics ingestion exists. 1. Dominant-color gradient backdrop. New album_color_extractor.dart wraps the existing AlbumCoverCache: extracts the dominant color via PaletteGenerator over the local file, caches in-memory keyed by album_id. Top 55% of the screen carries the color (0.55 alpha → fs.obsidian) so controls below stay legible. AnimatedContainer tweens the gradient across track changes. 2. Hero transition for cover art (mini bar → full screen). Stable kPlayerCoverHeroTag (not media.id keyed) so the transition works regardless of what's playing and isn't racy if media swaps mid-tap. flightShuttleBuilder renders the destination's Hero widget for the whole flight, which reads as a clean grow rather than a swap. 4. Crossfade on track change. AnimatedSwitcher around the album art, title, and artist+album text block, all keyed by media.id so the switcher fades between old and new on each track-change rebuild. Pairs with the AnimatedContainer gradient so the whole "what's playing" zone changes in lockstep. palette_generator: ^0.3.3 added. For #396 / #356 umbrella. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,7 @@ import '../likes/like_button.dart';
|
||||
import '../models/track.dart';
|
||||
import '../shared/widgets/track_actions/track_actions_button.dart';
|
||||
import '../theme/theme_extension.dart';
|
||||
import 'now_playing_screen.dart' show kPlayerCoverHeroTag;
|
||||
import 'player_provider.dart';
|
||||
|
||||
/// Compact player bar mounted at the bottom of the app shell. Mini
|
||||
@@ -88,29 +89,40 @@ class _TrackInfo extends StatelessWidget {
|
||||
final fs = Theme.of(context).extension<FabledSwordTheme>()!;
|
||||
final artistName = (media.artist ?? '').trim();
|
||||
|
||||
// Cover is wrapped in a Hero with the shared kPlayerCoverHeroTag so
|
||||
// tapping the mini bar to expand into NowPlayingScreen animates the
|
||||
// artwork from this 48dp footprint to the full-screen size rather
|
||||
// than fade-cutting. Tag is stable per-route (not keyed by media.id)
|
||||
// so the transition works regardless of what's playing.
|
||||
Widget cover;
|
||||
if (media.artUri != null) {
|
||||
cover = Image(
|
||||
image: media.artUri!.isScheme('file')
|
||||
? FileImage(File.fromUri(media.artUri!)) as ImageProvider
|
||||
: NetworkImage(media.artUri.toString()),
|
||||
width: 48,
|
||||
height: 48,
|
||||
// Without a fit, Image paints the source at its intrinsic
|
||||
// resolution inside the 48dp box — a thumbnail-sized cover
|
||||
// would render as a tiny inset. Cover stretches/crops to fill
|
||||
// the box uniformly.
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, __, ___) =>
|
||||
Container(width: 48, height: 48, color: fs.slate),
|
||||
);
|
||||
} else {
|
||||
cover = Container(width: 48, height: 48, color: fs.slate);
|
||||
}
|
||||
return Row(
|
||||
// Default centering vertically aligns the like/kebab buttons
|
||||
// against the album art (48dp) — visually they span the title +
|
||||
// artist block instead of sitting on the title baseline.
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
if (media.artUri != null)
|
||||
Image(
|
||||
image: media.artUri!.isScheme('file')
|
||||
? FileImage(File.fromUri(media.artUri!)) as ImageProvider
|
||||
: NetworkImage(media.artUri.toString()),
|
||||
width: 48,
|
||||
height: 48,
|
||||
// Without a fit, Image paints the source at its intrinsic
|
||||
// resolution inside the 48dp box — a thumbnail-sized cover
|
||||
// would render as a tiny inset. Cover stretches/crops to
|
||||
// fill the box uniformly.
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, __, ___) =>
|
||||
Container(width: 48, height: 48, color: fs.slate),
|
||||
)
|
||||
else
|
||||
Container(width: 48, height: 48, color: fs.slate),
|
||||
Hero(
|
||||
tag: kPlayerCoverHeroTag,
|
||||
child: cover,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
|
||||
Reference in New Issue
Block a user