mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-27 11:51:56 +02:00
fix(album): overlay selection bar above navbar
This commit is contained in:
@@ -82,6 +82,9 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen>
|
||||
String? _headerVideoUrl;
|
||||
String? _headerImageUrl;
|
||||
List<String> _audioTraits = const [];
|
||||
OverlayEntry? _selectionOverlayEntry;
|
||||
List<Track> _selectionOverlayTracks = const [];
|
||||
double _selectionOverlayBottomPadding = 0;
|
||||
|
||||
String _effectiveMetadataProviderIdFromAlbumId() {
|
||||
if (widget.extensionId != null && widget.extensionId!.isNotEmpty) {
|
||||
@@ -130,6 +133,24 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen>
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_hideSelectionOverlay();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void exitSelectionMode() {
|
||||
super.exitSelectionMode();
|
||||
_hideSelectionOverlay();
|
||||
}
|
||||
|
||||
@override
|
||||
void toggleSelection(String itemId) {
|
||||
super.toggleSelection(itemId);
|
||||
if (!isSelectionMode) _hideSelectionOverlay();
|
||||
}
|
||||
|
||||
Future<void> _fetchTracks() async {
|
||||
setState(() => _isLoading = true);
|
||||
try {
|
||||
@@ -333,6 +354,13 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen>
|
||||
_trackSelectionId(tracks[index], index),
|
||||
});
|
||||
|
||||
if (isSelectionMode || _selectionOverlayEntry != null) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
_syncSelectionOverlay(tracks: tracks, bottomPadding: bottomPadding);
|
||||
});
|
||||
}
|
||||
|
||||
return PopScope(
|
||||
canPop: !isSelectionMode,
|
||||
onPopInvokedWithResult: (didPop, result) {
|
||||
@@ -340,51 +368,31 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen>
|
||||
},
|
||||
child: Scaffold(
|
||||
backgroundColor: pageBackgroundColor,
|
||||
body: Stack(
|
||||
children: [
|
||||
CustomScrollView(
|
||||
controller: scrollController,
|
||||
slivers: [
|
||||
_buildAppBar(context, colorScheme, pageBackgroundColor),
|
||||
if (_isLoading)
|
||||
const SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: AlbumTrackListSkeleton(itemCount: 10),
|
||||
),
|
||||
),
|
||||
if (_error != null)
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: ErrorCard(
|
||||
error: _error!,
|
||||
colorScheme: colorScheme,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!_isLoading && _error == null && tracks.isNotEmpty) ...[
|
||||
_buildTrackList(context, colorScheme, tracks),
|
||||
_buildAlbumFooter(context, colorScheme, tracks),
|
||||
],
|
||||
SliverToBoxAdapter(
|
||||
child: SizedBox(
|
||||
height: (isSelectionMode ? 140 : 32) + bottomInset,
|
||||
),
|
||||
body: CustomScrollView(
|
||||
controller: scrollController,
|
||||
slivers: [
|
||||
_buildAppBar(context, colorScheme, pageBackgroundColor),
|
||||
if (_isLoading)
|
||||
const SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: AlbumTrackListSkeleton(itemCount: 10),
|
||||
),
|
||||
],
|
||||
),
|
||||
AnimatedPositioned(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
curve: Curves.easeOutCubic,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: isSelectionMode ? 0 : -(200 + bottomPadding),
|
||||
child: _buildSelectionBottomBar(
|
||||
context,
|
||||
colorScheme,
|
||||
tracks,
|
||||
bottomPadding,
|
||||
),
|
||||
if (_error != null)
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: ErrorCard(error: _error!, colorScheme: colorScheme),
|
||||
),
|
||||
),
|
||||
if (!_isLoading && _error == null && tracks.isNotEmpty) ...[
|
||||
_buildTrackList(context, colorScheme, tracks),
|
||||
_buildAlbumFooter(context, colorScheme, tracks),
|
||||
],
|
||||
SliverToBoxAdapter(
|
||||
child: SizedBox(
|
||||
height: (isSelectionMode ? 140 : 32) + bottomInset,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -651,8 +659,51 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen>
|
||||
tracks[index],
|
||||
];
|
||||
|
||||
void _hideSelectionOverlay() {
|
||||
_selectionOverlayEntry?.remove();
|
||||
_selectionOverlayEntry = null;
|
||||
}
|
||||
|
||||
void _syncSelectionOverlay({
|
||||
required List<Track> tracks,
|
||||
required double bottomPadding,
|
||||
}) {
|
||||
if (!isSelectionMode) {
|
||||
_hideSelectionOverlay();
|
||||
return;
|
||||
}
|
||||
|
||||
_selectionOverlayTracks = tracks;
|
||||
_selectionOverlayBottomPadding = bottomPadding;
|
||||
if (_selectionOverlayEntry != null) {
|
||||
_selectionOverlayEntry!.markNeedsBuild();
|
||||
return;
|
||||
}
|
||||
|
||||
final overlay = Overlay.of(context, rootOverlay: true);
|
||||
_selectionOverlayEntry = OverlayEntry(
|
||||
builder: (overlayContext) => Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: AnimatedSelectionBottomBar(
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: _buildSelectionBottomBar(
|
||||
overlayContext,
|
||||
Theme.of(overlayContext).colorScheme,
|
||||
_selectionOverlayTracks,
|
||||
_selectionOverlayBottomPadding,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
overlay.insert(_selectionOverlayEntry!);
|
||||
}
|
||||
|
||||
Widget _buildSelectionBottomBar(
|
||||
BuildContext context,
|
||||
BuildContext barContext,
|
||||
ColorScheme colorScheme,
|
||||
List<Track> tracks,
|
||||
double bottomPadding,
|
||||
@@ -680,7 +731,7 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen>
|
||||
width: double.infinity,
|
||||
child: SelectionActionButton(
|
||||
icon: Icons.download_rounded,
|
||||
label: '${context.l10n.dialogDownload} ($selectedCount)',
|
||||
label: '${barContext.l10n.dialogDownload} ($selectedCount)',
|
||||
onPressed: selectedCount == 0
|
||||
? null
|
||||
: () => _downloadSelected(context, tracks),
|
||||
|
||||
@@ -77,7 +77,7 @@ extension _QueueTabSelectionActions on _QueueTabState {
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: _AnimatedOverlayBottomBar(
|
||||
child: AnimatedSelectionBottomBar(
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: _buildSelectionBottomBar(
|
||||
@@ -127,7 +127,7 @@ extension _QueueTabSelectionActions on _QueueTabState {
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: _AnimatedOverlayBottomBar(
|
||||
child: AnimatedSelectionBottomBar(
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: _buildPlaylistSelectionBottomBar(
|
||||
|
||||
@@ -90,57 +90,9 @@ class _FilterChip extends StatelessWidget {
|
||||
onSelected: (_) => onTap(),
|
||||
showCheckmark: false,
|
||||
backgroundColor: settingsGroupColor(context),
|
||||
side: BorderSide(color: colorScheme.outlineVariant.withValues(alpha: 0.6)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AnimatedOverlayBottomBar extends StatefulWidget {
|
||||
final Widget child;
|
||||
|
||||
const _AnimatedOverlayBottomBar({required this.child});
|
||||
|
||||
@override
|
||||
State<_AnimatedOverlayBottomBar> createState() =>
|
||||
_AnimatedOverlayBottomBarState();
|
||||
}
|
||||
|
||||
class _AnimatedOverlayBottomBarState extends State<_AnimatedOverlayBottomBar>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _controller;
|
||||
late final Animation<Offset> _slideAnimation;
|
||||
late final Animation<double> _fadeAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 240),
|
||||
);
|
||||
final curve = CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.easeOutCubic,
|
||||
);
|
||||
_slideAnimation = Tween<Offset>(
|
||||
begin: const Offset(0, 0.08),
|
||||
end: Offset.zero,
|
||||
).animate(curve);
|
||||
_fadeAnimation = Tween<double>(begin: 0, end: 1).animate(curve);
|
||||
_controller.forward();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FadeTransition(
|
||||
opacity: _fadeAnimation,
|
||||
child: SlideTransition(position: _slideAnimation, child: widget.child),
|
||||
side: BorderSide(
|
||||
color: colorScheme.outlineVariant.withValues(alpha: 0.6),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,57 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:spotiflac_android/l10n/l10n.dart';
|
||||
|
||||
/// Entrance animation shared by selection bars mounted in the root overlay.
|
||||
class AnimatedSelectionBottomBar extends StatefulWidget {
|
||||
const AnimatedSelectionBottomBar({super.key, required this.child});
|
||||
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
State<AnimatedSelectionBottomBar> createState() =>
|
||||
_AnimatedSelectionBottomBarState();
|
||||
}
|
||||
|
||||
class _AnimatedSelectionBottomBarState extends State<AnimatedSelectionBottomBar>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _controller;
|
||||
late final Animation<Offset> _slideAnimation;
|
||||
late final Animation<double> _fadeAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 240),
|
||||
);
|
||||
final curve = CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.easeOutCubic,
|
||||
);
|
||||
_slideAnimation = Tween<Offset>(
|
||||
begin: const Offset(0, 0.08),
|
||||
end: Offset.zero,
|
||||
).animate(curve);
|
||||
_fadeAnimation = Tween<double>(begin: 0, end: 1).animate(curve);
|
||||
_controller.forward();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FadeTransition(
|
||||
opacity: _fadeAnimation,
|
||||
child: SlideTransition(position: _slideAnimation, child: widget.child),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Shared chrome for the multi-select bottom bar: rounded surface, drag
|
||||
/// handle, close button, "N selected" header and select-all toggle.
|
||||
/// The screen-specific action buttons go in [children].
|
||||
|
||||
Reference in New Issue
Block a user