|
|
@@ -127,6 +127,7 @@ class _JellyfinAlbumScreenState extends State<JellyfinAlbumScreen> {
|
|
|
child: AspectRatio(
|
|
|
aspectRatio: 1,
|
|
|
child: Container(
|
|
|
+ key: const ValueKey('jellyfin-album-cover'),
|
|
|
clipBehavior: Clip.antiAlias,
|
|
|
decoration: BoxDecoration(
|
|
|
borderRadius: BorderRadius.circular(24),
|
|
|
@@ -146,7 +147,7 @@ class _JellyfinAlbumScreenState extends State<JellyfinAlbumScreen> {
|
|
|
);
|
|
|
}
|
|
|
|
|
|
- Widget controls() {
|
|
|
+ Widget controls({double? minHeight}) {
|
|
|
final error = playback.error;
|
|
|
final durationMs = media.durationMs > 0
|
|
|
? media.durationMs
|
|
|
@@ -160,7 +161,7 @@ class _JellyfinAlbumScreenState extends State<JellyfinAlbumScreen> {
|
|
|
final canSeek = active && durationMs > 0;
|
|
|
final canToggle = !playback.loading;
|
|
|
|
|
|
- return Column(
|
|
|
+ final content = Column(
|
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
children: [
|
|
|
@@ -226,106 +227,120 @@ class _JellyfinAlbumScreenState extends State<JellyfinAlbumScreen> {
|
|
|
],
|
|
|
),
|
|
|
],
|
|
|
- const SizedBox(height: 14),
|
|
|
- Container(
|
|
|
- padding: const EdgeInsets.fromLTRB(18, 12, 18, 14),
|
|
|
- decoration: BoxDecoration(
|
|
|
- color: colors.surface,
|
|
|
- borderRadius: BorderRadius.circular(20),
|
|
|
- border: Border.all(color: colors.border),
|
|
|
- ),
|
|
|
- child: Column(
|
|
|
- mainAxisSize: MainAxisSize.min,
|
|
|
+ ],
|
|
|
+ );
|
|
|
+ final card = Container(
|
|
|
+ key: const ValueKey('jellyfin-album-progress-card'),
|
|
|
+ padding: const EdgeInsets.fromLTRB(18, 12, 18, 14),
|
|
|
+ decoration: BoxDecoration(
|
|
|
+ color: colors.surface,
|
|
|
+ borderRadius: BorderRadius.circular(20),
|
|
|
+ border: Border.all(color: colors.border),
|
|
|
+ ),
|
|
|
+ child: Column(
|
|
|
+ mainAxisSize: MainAxisSize.min,
|
|
|
+ children: [
|
|
|
+ Row(
|
|
|
children: [
|
|
|
- Row(
|
|
|
- children: [
|
|
|
- Expanded(
|
|
|
- child: SliderTheme(
|
|
|
- data: SliderTheme.of(context).copyWith(
|
|
|
- trackHeight: 4,
|
|
|
- activeTrackColor: colors.accent,
|
|
|
- inactiveTrackColor: colors.border,
|
|
|
- thumbColor: colors.accent,
|
|
|
- overlayColor: colors.accent.withValues(alpha: 0.12),
|
|
|
- thumbShape: const RoundSliderThumbShape(
|
|
|
- enabledThumbRadius: 7,
|
|
|
- ),
|
|
|
- overlayShape: const RoundSliderOverlayShape(
|
|
|
- overlayRadius: 16,
|
|
|
- ),
|
|
|
- ),
|
|
|
- child: Slider(
|
|
|
- min: 0,
|
|
|
- max: durationMs > 0 ? durationMs.toDouble() : 1,
|
|
|
- value: durationMs > 0 ? rawPosition : 0,
|
|
|
- onChanged: canSeek
|
|
|
- ? (value) {
|
|
|
- setState(() => _seekDragMs = value);
|
|
|
- }
|
|
|
- : null,
|
|
|
- onChangeEnd: canSeek
|
|
|
- ? (value) {
|
|
|
- setState(() => _seekDragMs = null);
|
|
|
- unawaited(
|
|
|
- playback.seek(
|
|
|
- Duration(milliseconds: value.round()),
|
|
|
- ),
|
|
|
- );
|
|
|
- }
|
|
|
- : null,
|
|
|
- ),
|
|
|
+ Expanded(
|
|
|
+ child: SliderTheme(
|
|
|
+ data: SliderTheme.of(context).copyWith(
|
|
|
+ trackHeight: 4,
|
|
|
+ activeTrackColor: colors.accent,
|
|
|
+ inactiveTrackColor: colors.border,
|
|
|
+ thumbColor: colors.accent,
|
|
|
+ overlayColor: colors.accent.withValues(alpha: 0.12),
|
|
|
+ thumbShape: const RoundSliderThumbShape(
|
|
|
+ enabledThumbRadius: 8,
|
|
|
),
|
|
|
- ),
|
|
|
- const SizedBox(width: 10),
|
|
|
- Text(
|
|
|
- '${_formatDuration(position)} / ${_formatDuration(total)}',
|
|
|
- style: theme.textTheme.bodySmall?.copyWith(
|
|
|
- color: colors.inkMuted,
|
|
|
- fontFeatures: const [FontFeature.tabularFigures()],
|
|
|
+ overlayShape: const RoundSliderOverlayShape(
|
|
|
+ overlayRadius: 18,
|
|
|
),
|
|
|
),
|
|
|
- ],
|
|
|
- ),
|
|
|
- const SizedBox(height: 10),
|
|
|
- Row(
|
|
|
- mainAxisAlignment: MainAxisAlignment.center,
|
|
|
- children: [
|
|
|
- _AlbumTransportButton(
|
|
|
- asset: 'assets/icons/skip-back.svg',
|
|
|
- semanticLabel: 'Previous track',
|
|
|
- enabled: active && playback.canGoPrevious,
|
|
|
- onTap: () => unawaited(playback.previous()),
|
|
|
- ),
|
|
|
- const SizedBox(width: 18),
|
|
|
- _AlbumTransportButton(
|
|
|
- asset: playing
|
|
|
- ? 'assets/icons/pause.svg'
|
|
|
- : 'assets/icons/play.svg',
|
|
|
- semanticLabel: playing ? 'Pause music' : 'Play music',
|
|
|
- primary: true,
|
|
|
- enabled: canToggle,
|
|
|
- onTap: () {
|
|
|
- if (playback.loading) return;
|
|
|
- if (active) {
|
|
|
- unawaited(playback.toggle());
|
|
|
- } else {
|
|
|
- unawaited(playback.playAlbum(widget.album));
|
|
|
- }
|
|
|
- },
|
|
|
+ child: Slider(
|
|
|
+ min: 0,
|
|
|
+ max: durationMs > 0 ? durationMs.toDouble() : 1,
|
|
|
+ value: durationMs > 0 ? rawPosition : 0,
|
|
|
+ onChanged: canSeek
|
|
|
+ ? (value) {
|
|
|
+ setState(() => _seekDragMs = value);
|
|
|
+ }
|
|
|
+ : null,
|
|
|
+ onChangeEnd: canSeek
|
|
|
+ ? (value) {
|
|
|
+ setState(() => _seekDragMs = null);
|
|
|
+ unawaited(
|
|
|
+ playback.seek(
|
|
|
+ Duration(milliseconds: value.round()),
|
|
|
+ ),
|
|
|
+ );
|
|
|
+ }
|
|
|
+ : null,
|
|
|
),
|
|
|
- const SizedBox(width: 18),
|
|
|
- _AlbumTransportButton(
|
|
|
- asset: 'assets/icons/skip-forward.svg',
|
|
|
- semanticLabel: 'Next track',
|
|
|
- enabled: active && playback.canGoNext,
|
|
|
- onTap: () => unawaited(playback.next()),
|
|
|
- ),
|
|
|
- ],
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ const SizedBox(width: 10),
|
|
|
+ Text(
|
|
|
+ '${_formatDuration(position)} / ${_formatDuration(total)}',
|
|
|
+ style: theme.textTheme.bodySmall?.copyWith(
|
|
|
+ color: colors.inkMuted,
|
|
|
+ fontFeatures: const [FontFeature.tabularFigures()],
|
|
|
+ ),
|
|
|
),
|
|
|
],
|
|
|
),
|
|
|
- ),
|
|
|
- ],
|
|
|
+ const SizedBox(height: 10),
|
|
|
+ Row(
|
|
|
+ mainAxisAlignment: MainAxisAlignment.center,
|
|
|
+ children: [
|
|
|
+ _AlbumTransportButton(
|
|
|
+ asset: 'assets/icons/skip-back.svg',
|
|
|
+ semanticLabel: 'Previous track',
|
|
|
+ enabled: active && playback.canGoPrevious,
|
|
|
+ onTap: () => unawaited(playback.previous()),
|
|
|
+ ),
|
|
|
+ const SizedBox(width: 20),
|
|
|
+ _AlbumTransportButton(
|
|
|
+ asset: playing
|
|
|
+ ? 'assets/icons/pause.svg'
|
|
|
+ : 'assets/icons/play.svg',
|
|
|
+ semanticLabel: playing ? 'Pause music' : 'Play music',
|
|
|
+ primary: true,
|
|
|
+ enabled: canToggle,
|
|
|
+ onTap: () {
|
|
|
+ if (playback.loading) return;
|
|
|
+ if (active) {
|
|
|
+ unawaited(playback.toggle());
|
|
|
+ } else {
|
|
|
+ unawaited(playback.playAlbum(widget.album));
|
|
|
+ }
|
|
|
+ },
|
|
|
+ ),
|
|
|
+ const SizedBox(width: 20),
|
|
|
+ _AlbumTransportButton(
|
|
|
+ asset: 'assets/icons/skip-forward.svg',
|
|
|
+ semanticLabel: 'Next track',
|
|
|
+ enabled: active && playback.canGoNext,
|
|
|
+ onTap: () => unawaited(playback.next()),
|
|
|
+ ),
|
|
|
+ ],
|
|
|
+ ),
|
|
|
+ ],
|
|
|
+ ),
|
|
|
+ );
|
|
|
+ if (minHeight == null) {
|
|
|
+ return Column(
|
|
|
+ crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
|
+ mainAxisSize: MainAxisSize.min,
|
|
|
+ children: [content, const SizedBox(height: 14), card],
|
|
|
+ );
|
|
|
+ }
|
|
|
+ return SizedBox(
|
|
|
+ height: minHeight,
|
|
|
+ child: Column(
|
|
|
+ crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
|
+ children: [content, const Spacer(), card],
|
|
|
+ ),
|
|
|
);
|
|
|
}
|
|
|
|
|
|
@@ -419,7 +434,8 @@ class _JellyfinAlbumScreenState extends State<JellyfinAlbumScreen> {
|
|
|
const SizedBox(width: 22),
|
|
|
SizedBox(
|
|
|
width: controlsWidth,
|
|
|
- child: SingleChildScrollView(child: controls()),
|
|
|
+ height: coverWidth,
|
|
|
+ child: controls(minHeight: coverWidth),
|
|
|
),
|
|
|
],
|
|
|
);
|
|
|
@@ -436,7 +452,7 @@ class _JellyfinAlbumScreenState extends State<JellyfinAlbumScreen> {
|
|
|
children: [
|
|
|
SizedBox(width: coverWidth, child: cover()),
|
|
|
const SizedBox(width: 18),
|
|
|
- Expanded(child: controls()),
|
|
|
+ Expanded(child: controls(minHeight: coverWidth)),
|
|
|
],
|
|
|
),
|
|
|
const SizedBox(height: 16),
|
|
|
@@ -638,7 +654,7 @@ class _AlbumVolumeCard extends StatelessWidget {
|
|
|
semanticLabel: system.isMuted ? 'Unmute' : 'Mute',
|
|
|
pressedScale: 0.94,
|
|
|
child: Container(
|
|
|
- height: 36,
|
|
|
+ height: 40,
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 10),
|
|
|
decoration: BoxDecoration(
|
|
|
color: system.isMuted
|
|
|
@@ -658,7 +674,7 @@ class _AlbumVolumeCard extends StatelessWidget {
|
|
|
system.isMuted
|
|
|
? 'assets/icons/volume-x.svg'
|
|
|
: 'assets/icons/volume-2.svg',
|
|
|
- size: 16,
|
|
|
+ size: 18,
|
|
|
color: system.isMuted ? colors.accent : colors.inkMuted,
|
|
|
),
|
|
|
const SizedBox(width: 6),
|
|
|
@@ -703,8 +719,8 @@ class _AlbumVolumeStep extends StatelessWidget {
|
|
|
semanticLabel: semanticLabel,
|
|
|
pressedScale: 0.92,
|
|
|
child: Container(
|
|
|
- width: 36,
|
|
|
- height: 36,
|
|
|
+ width: 40,
|
|
|
+ height: 40,
|
|
|
decoration: BoxDecoration(
|
|
|
color: colors.surfaceStrong,
|
|
|
shape: BoxShape.circle,
|
|
|
@@ -713,7 +729,7 @@ class _AlbumVolumeStep extends StatelessWidget {
|
|
|
alignment: Alignment.center,
|
|
|
child: Icon(
|
|
|
icon,
|
|
|
- size: 18,
|
|
|
+ size: 20,
|
|
|
color: onTap == null ? colors.inkFaint : colors.inkMuted,
|
|
|
),
|
|
|
),
|
|
|
@@ -739,7 +755,7 @@ class _AlbumTransportButton extends StatelessWidget {
|
|
|
@override
|
|
|
Widget build(BuildContext context) {
|
|
|
final colors = context.colors;
|
|
|
- final size = primary ? 68.0 : 54.0;
|
|
|
+ final size = primary ? 80.0 : 62.0;
|
|
|
return Opacity(
|
|
|
opacity: enabled ? 1 : 0.4,
|
|
|
child: Pressable(
|
|
|
@@ -757,7 +773,7 @@ class _AlbumTransportButton extends StatelessWidget {
|
|
|
alignment: Alignment.center,
|
|
|
child: AppIcon(
|
|
|
asset,
|
|
|
- size: primary ? 28 : 22,
|
|
|
+ size: primary ? 32 : 26,
|
|
|
color: primary ? colors.accentInk : colors.ink,
|
|
|
),
|
|
|
),
|