Files
SpotiFLAC-Mobile/lib/widgets/material_player_layout.dart
T

232 lines
8.3 KiB
Dart

import 'dart:math' as math;
import 'package:audio_service/audio_service.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotiflac_android/controllers/player_metadata_controller.dart';
import 'package:spotiflac_android/providers/music_player_provider.dart';
import 'package:spotiflac_android/routes/now_playing_route.dart';
import 'package:spotiflac_android/utils/clickable_metadata.dart';
import 'package:spotiflac_android/utils/playback_seek_preview.dart';
import 'package:spotiflac_android/widgets/player_artwork.dart';
import 'package:spotiflac_android/widgets/player_gesture_regions.dart';
import 'package:spotiflac_android/widgets/player_metadata_title.dart';
import 'package:spotiflac_android/widgets/player_playback_controls.dart';
class MaterialPlayerLayout extends StatelessWidget {
const MaterialPlayerLayout({
super.key,
required this.mediaItem,
required this.metadata,
required this.seekPreview,
required this.colorScheme,
required this.showLyrics,
required this.lyricsBuilder,
required this.onOpenQueue,
});
final MediaItem mediaItem;
final PlayerMetadataController metadata;
final PlaybackSeekPreview seekPreview;
final ColorScheme colorScheme;
final bool showLyrics;
final WidgetBuilder lyricsBuilder;
final VoidCallback onOpenQueue;
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
Widget artworkAt(double artSize) => Center(
child: PlayerArtworkDragRegion(
child: Hero(
tag: kNowPlayingArtworkHeroTag,
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
child: SizedBox(
width: artSize,
height: artSize,
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 450),
switchInCurve: Curves.easeOutCubic,
switchOutCurve: Curves.easeInCubic,
transitionBuilder: (child, animation) => FadeTransition(
opacity: animation,
child: ScaleTransition(
scale: Tween(begin: 0.94, end: 1.0).animate(animation),
child: child,
),
),
child: PlayerArtwork(
key: ValueKey(
mediaItem.artUri?.toString() ?? mediaItem.id,
),
artUri: mediaItem.artUri?.toString(),
colorScheme: colorScheme,
cacheWidth:
(artSize * MediaQuery.devicePixelRatioOf(context))
.round(),
),
),
),
),
),
),
);
Widget visual() => AnimatedSwitcher(
duration: MediaQuery.disableAnimationsOf(context)
? Duration.zero
: const Duration(milliseconds: 300),
switchInCurve: Curves.easeInOutCubic,
switchOutCurve: Curves.easeInOutCubic,
child: showLyrics
? KeyedSubtree(
key: const ValueKey('material-player-lyrics'),
child: lyricsBuilder(context),
)
: LayoutBuilder(
key: const ValueKey('material-player-cover'),
builder: (context, area) => artworkAt(
math.max(
0.0,
math.min(
360.0,
math.min(area.maxWidth - 64, area.maxHeight - 24),
),
),
),
),
);
// Tablet/landscape: artwork pane left, metadata and controls right,
// instead of one narrow column in a sea of empty space.
final twoPane =
constraints.maxWidth >= 720 &&
constraints.maxWidth > constraints.maxHeight;
if (twoPane) {
return Row(
children: [
Expanded(child: visual()),
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(vertical: 16),
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight - 32,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: _metadataAndControls(context, compact: true),
),
),
),
),
],
);
}
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Column(
children: [
Expanded(child: visual()),
const SizedBox(height: 12),
PlayerQueueSwipeRegion(
onOpenQueue: onOpenQueue,
child: Column(
mainAxisSize: MainAxisSize.min,
children: _metadataAndControls(context),
),
),
],
),
);
},
);
}
/// Title/artist, seek slider, and transport buttons — shared by the
/// portrait column and the landscape right pane.
List<Widget> _metadataAndControls(
BuildContext context, {
bool compact = false,
}) {
return [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 28),
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 350),
switchInCurve: Curves.easeOutCubic,
switchOutCurve: Curves.easeInCubic,
transitionBuilder: (child, animation) => FadeTransition(
opacity: animation,
child: SlideTransition(
position: Tween(
begin: const Offset(0, 0.15),
end: Offset.zero,
).animate(animation),
child: child,
),
),
child: Row(
key: ValueKey(mediaItem.id),
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
PlayerMetadataTitle(
mediaItem: mediaItem,
metadata: metadata,
style:
(compact
? Theme.of(context).textTheme.titleLarge
: Theme.of(context).textTheme.headlineSmall)
?.copyWith(
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
),
textAlign: TextAlign.center,
maxLines: compact ? 1 : 2,
overflow: TextOverflow.ellipsis,
),
SizedBox(height: compact ? 4 : 6),
Consumer(
builder: (context, ref, _) {
final track = ref
.watch(playerCollectionTrackProvider(mediaItem))
.value;
return ClickableArtistName(
artistName: mediaItem.artist ?? '',
artistId: track?.artistId,
extensionId: track?.source,
style: Theme.of(context).textTheme.titleMedium
?.copyWith(color: colorScheme.onSurfaceVariant),
textAlign: TextAlign.center,
maxLines: 1,
overflow: TextOverflow.ellipsis,
);
},
),
],
),
),
],
),
),
),
SizedBox(height: compact ? 12 : 24),
PlayerPlaybackControls(
key: ValueKey(mediaItem.id),
mediaId: mediaItem.id,
duration: mediaItem.duration ?? Duration.zero,
seekPreview: seekPreview,
colorScheme: colorScheme,
metadata: metadata,
compact: compact,
),
];
}
}