mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-08-03 01:28:37 +02:00
feat(collections): unify selection-aware screens
This commit is contained in:
@@ -1,65 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Shared body for album-style screens: a [PopScope] that exits selection
|
||||
/// mode instead of popping, a scrollable track list, and a bottom-select bar
|
||||
/// that slides in over it.
|
||||
class AlbumScaffoldBody extends StatelessWidget {
|
||||
const AlbumScaffoldBody({
|
||||
super.key,
|
||||
required this.scrollController,
|
||||
required this.isSelectionMode,
|
||||
required this.onExitSelectionMode,
|
||||
required this.appBar,
|
||||
required this.trackList,
|
||||
required this.bottomBar,
|
||||
required this.bottomInset,
|
||||
required this.bottomPadding,
|
||||
});
|
||||
|
||||
final ScrollController scrollController;
|
||||
final bool isSelectionMode;
|
||||
final VoidCallback onExitSelectionMode;
|
||||
final Widget appBar;
|
||||
final Widget trackList;
|
||||
final Widget bottomBar;
|
||||
final double bottomInset;
|
||||
final double bottomPadding;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PopScope(
|
||||
canPop: !isSelectionMode,
|
||||
onPopInvokedWithResult: (didPop, result) {
|
||||
if (!didPop && isSelectionMode) {
|
||||
onExitSelectionMode();
|
||||
}
|
||||
},
|
||||
child: Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
CustomScrollView(
|
||||
controller: scrollController,
|
||||
slivers: [
|
||||
appBar,
|
||||
trackList,
|
||||
SliverToBoxAdapter(
|
||||
child: SizedBox(height: isSelectionMode ? 120 : 32),
|
||||
),
|
||||
SliverToBoxAdapter(child: SizedBox(height: bottomInset)),
|
||||
],
|
||||
),
|
||||
|
||||
AnimatedPositioned(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
curve: Curves.easeOutCubic,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: isSelectionMode ? 0 : -(200 + bottomPadding),
|
||||
child: bottomBar,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:spotiflac_android/widgets/selection_bottom_bar.dart';
|
||||
|
||||
/// Shared shell for every track-collection screen: album, playlist, local
|
||||
/// album, downloaded album and library folders.
|
||||
///
|
||||
/// Before this existed each of those screens built its own `Scaffold` +
|
||||
/// `PopScope` + selection plumbing, which is why the same four screens had three
|
||||
/// different selection-bar mechanisms and why the playlist screen ended up with
|
||||
/// none at all. Screens now supply their header, their content slivers and the
|
||||
/// bar contents; everything else is shared:
|
||||
///
|
||||
/// * back gesture exits selection mode instead of popping the route,
|
||||
/// * the selection bar is mounted in the root overlay so it floats above the
|
||||
/// shell navigation bar,
|
||||
/// * trailing space is reserved so the last row stays reachable while the bar
|
||||
/// is up.
|
||||
class CollectionScaffold extends StatefulWidget {
|
||||
const CollectionScaffold({
|
||||
super.key,
|
||||
required this.scrollController,
|
||||
required this.isSelectionMode,
|
||||
required this.onExitSelectionMode,
|
||||
required this.appBar,
|
||||
required this.slivers,
|
||||
required this.bottomInset,
|
||||
this.selectionBar,
|
||||
this.backgroundColor,
|
||||
});
|
||||
|
||||
final ScrollController scrollController;
|
||||
final bool isSelectionMode;
|
||||
final VoidCallback onExitSelectionMode;
|
||||
|
||||
/// The collapsing header sliver.
|
||||
final Widget appBar;
|
||||
|
||||
/// Content slivers below the header (track list, error card, footer, ...).
|
||||
final List<Widget> slivers;
|
||||
|
||||
/// Bottom inset needed to clear the shell navigation bar.
|
||||
final double bottomInset;
|
||||
|
||||
/// Rebuilt by the caller each frame; shown only while [isSelectionMode] is
|
||||
/// true. Null means the screen has no multi-select.
|
||||
final Widget? selectionBar;
|
||||
|
||||
final Color? backgroundColor;
|
||||
|
||||
@override
|
||||
State<CollectionScaffold> createState() => _CollectionScaffoldState();
|
||||
}
|
||||
|
||||
class _CollectionScaffoldState extends State<CollectionScaffold> {
|
||||
final SelectionOverlayController _selectionOverlay =
|
||||
SelectionOverlayController();
|
||||
final GlobalKey _selectionBarKey = GlobalKey();
|
||||
|
||||
// Used for the first frame only. Once the overlay has laid out, the spacer
|
||||
// tracks its real height so multi-row actions and large text cannot cover the
|
||||
// final collection rows.
|
||||
double _selectionBarHeight = 160;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_selectionOverlay.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _syncSelectionOverlay() {
|
||||
if (!mounted) return;
|
||||
final bar = widget.selectionBar;
|
||||
if (!widget.isSelectionMode || bar == null) {
|
||||
_selectionOverlay.hide();
|
||||
return;
|
||||
}
|
||||
_selectionOverlay.show(
|
||||
context,
|
||||
(_) => KeyedSubtree(key: _selectionBarKey, child: bar),
|
||||
);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted || !widget.isSelectionMode) return;
|
||||
final height = _selectionBarKey.currentContext?.size?.height;
|
||||
if (height == null || height <= 0) return;
|
||||
if ((height - _selectionBarHeight).abs() < 0.5) return;
|
||||
setState(() => _selectionBarHeight = height);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.isSelectionMode || _selectionOverlay.isVisible) {
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(_) => _syncSelectionOverlay(),
|
||||
);
|
||||
}
|
||||
|
||||
return PopScope(
|
||||
canPop: !widget.isSelectionMode,
|
||||
onPopInvokedWithResult: (didPop, result) {
|
||||
if (!didPop && widget.isSelectionMode) {
|
||||
widget.onExitSelectionMode();
|
||||
}
|
||||
},
|
||||
child: Scaffold(
|
||||
backgroundColor: widget.backgroundColor,
|
||||
body: CustomScrollView(
|
||||
controller: widget.scrollController,
|
||||
slivers: [
|
||||
widget.appBar,
|
||||
...widget.slivers,
|
||||
SliverToBoxAdapter(
|
||||
child: SizedBox(
|
||||
key: const ValueKey('collection-selection-spacer'),
|
||||
height: widget.isSelectionMode ? _selectionBarHeight : 32,
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(child: SizedBox(height: widget.bottomInset)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user