fix(music): sheet safe-area, swipe-to-dismiss, hide mini-player under sheets
This commit is contained in:
@@ -24,6 +24,7 @@ import '../../theme/mono_motion.dart';
|
||||
import '../../theme/mono_tokens.dart';
|
||||
import '../../utils/app_logger.dart';
|
||||
import '../../utils/formatters.dart';
|
||||
import '../../utils/desktop_window_padding.dart';
|
||||
import '../../utils/media_image_helper.dart';
|
||||
import '../../utils/music_navigation.dart';
|
||||
import '../../utils/platform_detector.dart';
|
||||
@@ -58,13 +59,31 @@ class NowPlayingScreen extends StatefulWidget {
|
||||
State<NowPlayingScreen> createState() => _NowPlayingScreenState();
|
||||
}
|
||||
|
||||
class _NowPlayingScreenState extends State<NowPlayingScreen> with ContextMenuTapMixin<NowPlayingScreen> {
|
||||
class _NowPlayingScreenState extends State<NowPlayingScreen>
|
||||
with ContextMenuTapMixin<NowPlayingScreen>, SingleTickerProviderStateMixin {
|
||||
MusicPlaybackService? _service;
|
||||
StreamSubscription<Object>? _errorsSub;
|
||||
|
||||
bool _showLyrics = false;
|
||||
final Map<String, Future<Lyrics?>> _lyricsCache = {};
|
||||
|
||||
/// Key for getting a context below this screen's own OverlaySheetHost —
|
||||
/// the State's context sits ABOVE it, so sheet calls made with `context`
|
||||
/// would miss the host and fall back to a hostless modal sheet.
|
||||
final GlobalKey _overlayChildKey = GlobalKey();
|
||||
|
||||
BuildContext get _sheetContext => _overlayChildKey.currentContext ?? context;
|
||||
|
||||
/// Swipe-down-to-dismiss (mobile portrait). The offset feeds a
|
||||
/// ValueListenableBuilder-wrapped Transform so drag frames never rebuild
|
||||
/// the screen.
|
||||
final ValueNotifier<double> _dismissDrag = ValueNotifier<double>(0);
|
||||
late final AnimationController _dismissSettle = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
)..addListener(_onDismissSettleTick);
|
||||
double _dismissSettleFrom = 0;
|
||||
|
||||
final FocusNode _seekFocusNode = FocusNode(debugLabel: 'now_playing_seek');
|
||||
final FocusNode _overflowFocusNode = FocusNode(debugLabel: 'now_playing_overflow');
|
||||
final FocusNode _playPauseFocusNode = FocusNode(debugLabel: 'now_playing_play_pause');
|
||||
@@ -92,6 +111,8 @@ class _NowPlayingScreenState extends State<NowPlayingScreen> with ContextMenuTap
|
||||
@override
|
||||
void dispose() {
|
||||
_errorsSub?.cancel();
|
||||
_dismissSettle.dispose();
|
||||
_dismissDrag.dispose();
|
||||
_seekFocusNode.dispose();
|
||||
_overflowFocusNode.dispose();
|
||||
_playPauseFocusNode.dispose();
|
||||
@@ -119,6 +140,39 @@ class _NowPlayingScreenState extends State<NowPlayingScreen> with ContextMenuTap
|
||||
|
||||
void _pop() => Navigator.pop(context);
|
||||
|
||||
// Swipe-down anywhere above the transport controls closes the screen —
|
||||
// the standard music-player gesture. The content follows the finger;
|
||||
// releasing past the threshold pops (the route's reverse slide+fade
|
||||
// continues the motion from where the finger left off), otherwise the
|
||||
// content settles back.
|
||||
|
||||
void _onDismissSettleTick() {
|
||||
_dismissDrag.value = _dismissSettleFrom * (1 - Curves.easeOutCubic.transform(_dismissSettle.value));
|
||||
}
|
||||
|
||||
void _onDismissDragStart(DragStartDetails details) => _dismissSettle.stop();
|
||||
|
||||
void _onDismissDragUpdate(DragUpdateDetails details) {
|
||||
_dismissDrag.value = math.max(0, _dismissDrag.value + details.delta.dy);
|
||||
}
|
||||
|
||||
void _onDismissDragEnd(DragEndDetails details) {
|
||||
final offset = _dismissDrag.value;
|
||||
if (offset <= 0) return;
|
||||
// Same thresholds as the overlay sheet system's drag-to-dismiss.
|
||||
if (offset > MediaQuery.sizeOf(context).height * 0.25 || (details.primaryVelocity ?? 0) > 500) {
|
||||
_pop();
|
||||
} else {
|
||||
_onDismissDragCancel();
|
||||
}
|
||||
}
|
||||
|
||||
void _onDismissDragCancel() {
|
||||
if (_dismissDrag.value <= 0) return;
|
||||
_dismissSettleFrom = _dismissDrag.value;
|
||||
_dismissSettle.forward(from: 0);
|
||||
}
|
||||
|
||||
/// Artist line tap — the track's grandparent is the artist. Mirrors the
|
||||
/// album screen's artist link (fetch, then navigate; soft-fail).
|
||||
Future<void> _openArtist(MediaItem track) async {
|
||||
@@ -139,7 +193,7 @@ class _NowPlayingScreenState extends State<NowPlayingScreen> with ContextMenuTap
|
||||
final service = context.read<MusicPlaybackService>();
|
||||
final timed = service.sleepTimerActive && !service.sleepTimerEndOfTrack;
|
||||
final selected = await OverlaySheetController.showAdaptive<String>(
|
||||
context,
|
||||
_sheetContext,
|
||||
showDragHandle: true,
|
||||
builder: (context) => AppMenuSheet<String>(
|
||||
title: t.music.sleepTimer,
|
||||
@@ -231,7 +285,15 @@ class _NowPlayingScreenState extends State<NowPlayingScreen> with ContextMenuTap
|
||||
// the sheet, otherwise the route pops natively — audio continues).
|
||||
return OverlaySheetHost(
|
||||
canPop: true,
|
||||
child: Scaffold(backgroundColor: tk.bg, body: content),
|
||||
child: Scaffold(
|
||||
key: _overlayChildKey,
|
||||
backgroundColor: tk.bg,
|
||||
body: ValueListenableBuilder<double>(
|
||||
valueListenable: _dismissDrag,
|
||||
builder: (context, offset, child) => Transform.translate(offset: Offset(0, offset), child: child),
|
||||
child: content,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -240,7 +302,7 @@ class _NowPlayingScreenState extends State<NowPlayingScreen> with ContextMenuTap
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
Widget _buildPortraitLayout(MusicPlaybackService service, MediaItem track, MediaServerClient? client) {
|
||||
return Column(
|
||||
Widget upper = Column(
|
||||
children: [
|
||||
_buildTopBar(track, service.playContext?.title),
|
||||
Expanded(
|
||||
@@ -250,6 +312,24 @@ class _NowPlayingScreenState extends State<NowPlayingScreen> with ContextMenuTap
|
||||
),
|
||||
),
|
||||
Padding(padding: const EdgeInsets.fromLTRB(32, 8, 32, 0), child: _buildTrackInfo(track, centered: true)),
|
||||
],
|
||||
);
|
||||
// Touch only — desktop keeps mouse drags for text selection and the
|
||||
// lyrics pane. A drag starting on the lyrics scrollable still scrolls
|
||||
// (the descendant recognizer wins the arena).
|
||||
if (!PlatformDetector.isDesktopOS()) {
|
||||
upper = GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onVerticalDragStart: _onDismissDragStart,
|
||||
onVerticalDragUpdate: _onDismissDragUpdate,
|
||||
onVerticalDragEnd: _onDismissDragEnd,
|
||||
onVerticalDragCancel: _onDismissDragCancel,
|
||||
child: upper,
|
||||
);
|
||||
}
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(child: upper),
|
||||
Padding(padding: const EdgeInsets.fromLTRB(16, 8, 16, 0), child: _buildSeekBar()),
|
||||
_buildTransportRow(service),
|
||||
_buildUtilityRow(showQueueButton: true),
|
||||
@@ -279,8 +359,7 @@ class _NowPlayingScreenState extends State<NowPlayingScreen> with ContextMenuTap
|
||||
_buildTrackInfo(track, centered: false),
|
||||
const SizedBox(height: 8),
|
||||
_buildSeekBar(),
|
||||
_buildTransportRow(service),
|
||||
_buildUtilityRow(showQueueButton: false),
|
||||
_buildWideControlBand(service),
|
||||
const SizedBox(height: 12),
|
||||
// Inline queue panel — same widget the queue sheet uses.
|
||||
Expanded(
|
||||
@@ -373,32 +452,120 @@ class _NowPlayingScreenState extends State<NowPlayingScreen> with ContextMenuTap
|
||||
/// screen's element asserts; the screen already watches the service.
|
||||
Widget _buildTopBar(MediaItem track, String? playContextTitle) {
|
||||
final tk = tokens(context);
|
||||
// macOS pins the traffic lights at y=21 (16pt buttons → center 29, see
|
||||
// WindowUtilsPlugin.customButtonPositions); a fixed 58px row centers the
|
||||
// close button on that line regardless of the platform visual density
|
||||
// shrinking the IconButton box.
|
||||
final isMacOS = Theme.of(context).platform == TargetPlatform.macOS;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(8, 4, 8, 0),
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: AppIcon(Symbols.keyboard_arrow_down_rounded, fill: 1, color: tk.text),
|
||||
tooltip: t.common.close,
|
||||
onPressed: _pop,
|
||||
),
|
||||
Expanded(
|
||||
child: playContextTitle == null || playContextTitle.isEmpty
|
||||
? const SizedBox.shrink()
|
||||
: Text(
|
||||
t.music.playingFrom(title: playContextTitle),
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 1,
|
||||
overflow: .ellipsis,
|
||||
style: TextStyle(fontSize: 13, color: tk.textMuted),
|
||||
),
|
||||
),
|
||||
_buildOverflowButton(track),
|
||||
],
|
||||
padding: EdgeInsets.fromLTRB(8, isMacOS ? 0 : 4, 8, 0),
|
||||
child: SizedBox(
|
||||
height: isMacOS ? 58 : null,
|
||||
child: Row(
|
||||
children: [
|
||||
// Inset past the macOS traffic lights — this screen is a fullscreen
|
||||
// route, so the close button would otherwise sit underneath them.
|
||||
DesktopAppBarHelper.buildAdjustedLeading(
|
||||
IconButton(
|
||||
icon: AppIcon(Symbols.keyboard_arrow_down_rounded, fill: 1, color: tk.text),
|
||||
tooltip: t.common.close,
|
||||
onPressed: _pop,
|
||||
),
|
||||
context: context,
|
||||
)!,
|
||||
Expanded(
|
||||
child: playContextTitle == null || playContextTitle.isEmpty
|
||||
? const SizedBox.shrink()
|
||||
: Text(
|
||||
t.music.playingFrom(title: playContextTitle),
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 1,
|
||||
overflow: .ellipsis,
|
||||
style: TextStyle(fontSize: 13, color: tk.textMuted),
|
||||
),
|
||||
),
|
||||
_buildOverflowButton(track),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Wide-layout control band: the transport row stays exactly centered
|
||||
/// (equal-width flanks), with lyrics + a desktop volume slider as a
|
||||
/// right-aligned cluster that scales down instead of overflowing when the
|
||||
/// pane is narrow.
|
||||
Widget _buildWideControlBand(MusicPlaybackService service) {
|
||||
final tk = tokens(context);
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
final cluster = Row(
|
||||
mainAxisSize: .min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: AppIcon(
|
||||
Symbols.lyrics_rounded,
|
||||
fill: 1,
|
||||
size: 22,
|
||||
color: _showLyrics ? colorScheme.primary : tk.textMuted,
|
||||
),
|
||||
tooltip: t.music.lyrics,
|
||||
onPressed: _toggleLyrics,
|
||||
),
|
||||
if (PlatformDetector.isDesktop(context)) ...[const SizedBox(width: 4), _buildVolumeCluster(service)],
|
||||
],
|
||||
);
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
const Expanded(child: SizedBox()),
|
||||
_buildTransportRow(service),
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: .centerRight,
|
||||
child: FittedBox(fit: BoxFit.scaleDown, child: cluster),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Desktop volume control (0–100), persisted by the service across
|
||||
/// sessions. Mono styling matches the seek bar: text-colored active track
|
||||
/// on outline.
|
||||
Widget _buildVolumeCluster(MusicPlaybackService service) {
|
||||
final tk = tokens(context);
|
||||
final icon = service.volume <= 0
|
||||
? Symbols.volume_off_rounded
|
||||
: service.volume < 50
|
||||
? Symbols.volume_down_rounded
|
||||
: Symbols.volume_up_rounded;
|
||||
return Row(
|
||||
mainAxisSize: .min,
|
||||
children: [
|
||||
AppIcon(icon, fill: 1, size: 20, color: tk.textMuted),
|
||||
SizedBox(
|
||||
width: 140,
|
||||
child: SliderTheme(
|
||||
data: SliderTheme.of(context).copyWith(
|
||||
trackHeight: 3,
|
||||
activeTrackColor: tk.text,
|
||||
inactiveTrackColor: tk.outline,
|
||||
thumbColor: tk.text,
|
||||
thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 6),
|
||||
overlayShape: const RoundSliderOverlayShape(overlayRadius: 12),
|
||||
),
|
||||
child: Slider(
|
||||
value: service.volume.clamp(0.0, 100.0),
|
||||
max: 100,
|
||||
onChanged: (value) => unawaited(service.setVolume(value)),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// ⋮ — the current track's standard context menu plus the Sleep timer
|
||||
/// entry. On TV ([focusable]) it joins the d-pad chain above the seek bar.
|
||||
Widget _buildOverflowButton(MediaItem track, {bool focusable = false}) {
|
||||
@@ -639,13 +806,13 @@ class _NowPlayingScreenState extends State<NowPlayingScreen> with ContextMenuTap
|
||||
if (showQueueButton)
|
||||
FocusableAction(
|
||||
debugLabel: 'np_queue',
|
||||
onPressed: () => unawaited(showQueueSheet(context)),
|
||||
onPressed: () => unawaited(showQueueSheet(_sheetContext)),
|
||||
builder: (context, state) => _transportIcon(
|
||||
state,
|
||||
icon: Symbols.queue_music_rounded,
|
||||
active: false,
|
||||
tooltip: t.music.queue,
|
||||
onPressed: () => unawaited(showQueueSheet(context)),
|
||||
onPressed: () => unawaited(showQueueSheet(_sheetContext)),
|
||||
size: 22,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -580,8 +580,12 @@ class MediaContextMenuState extends State<MediaContextMenu> {
|
||||
_openedFromKeyboard = false;
|
||||
|
||||
if (useBottomSheet) {
|
||||
// Present from the menu's own context: it sits at the trigger widget,
|
||||
// below any screen-level OverlaySheetHost, while callers often pass a
|
||||
// screen context from ABOVE its host (which would skip the host and
|
||||
// fall back to a hostless modal sheet).
|
||||
selected = await OverlaySheetController.showAdaptive<String>(
|
||||
context,
|
||||
this.context,
|
||||
showDragHandle: true,
|
||||
builder: (context) => AppMenuSheet<String>(
|
||||
title: _itemDisplayTitle(),
|
||||
@@ -966,10 +970,11 @@ class MediaContextMenuState extends State<MediaContextMenu> {
|
||||
loadingShown = false;
|
||||
}
|
||||
|
||||
if (fileInfo != null && context.mounted) {
|
||||
// Show file info bottom sheet
|
||||
if (fileInfo != null && context.mounted && mounted) {
|
||||
// Show file info bottom sheet, presented from the menu's own context
|
||||
// so a screen-level OverlaySheetHost is found (see _showContextMenu).
|
||||
await OverlaySheetController.showAdaptive(
|
||||
context,
|
||||
this.context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => FileInfoBottomSheet(fileInfo: fileInfo, title: item.displayTitle),
|
||||
);
|
||||
@@ -1308,8 +1313,11 @@ class MediaContextMenuState extends State<MediaContextMenu> {
|
||||
}
|
||||
|
||||
Future<void> _showRatingSheet(BuildContext context, MediaItem item, MediaServerClient client) async {
|
||||
if (!mounted) return;
|
||||
// Presented from the menu's own context so a screen-level
|
||||
// OverlaySheetHost is found (see _showContextMenu).
|
||||
await OverlaySheetController.showAdaptive(
|
||||
context,
|
||||
this.context,
|
||||
showDragHandle: true,
|
||||
builder: (context) => RatingBottomSheet(
|
||||
item: item,
|
||||
|
||||
@@ -19,6 +19,7 @@ import '../../utils/video_player_navigation.dart';
|
||||
import '../app_icon.dart';
|
||||
import '../media_context_menu.dart';
|
||||
import '../optimized_media_image.dart';
|
||||
import '../overlay_sheet.dart';
|
||||
|
||||
/// Suppresses the mini-player while the top PAGE route of the profile
|
||||
/// navigator is a full-screen playback surface (the video player or the
|
||||
@@ -177,12 +178,18 @@ class _MusicMiniPlayerOverlayState extends State<MusicMiniPlayerOverlay> {
|
||||
if (_lastTrack == null) return const SizedBox.shrink();
|
||||
|
||||
final suppress = context.read<MusicUiRouteObserver?>()?.suppress ?? _noSuppression;
|
||||
return ValueListenableBuilder<bool>(
|
||||
valueListenable: suppress,
|
||||
builder: (context, suppressed, _) {
|
||||
final visible = track != null && !suppressed && !_dismissed;
|
||||
final openSheets = OverlaySheetController.openSheetCount;
|
||||
return ListenableBuilder(
|
||||
listenable: Listenable.merge([suppress, openSheets]),
|
||||
builder: (context, _) {
|
||||
final hasSession = track != null && !suppress.value && !_dismissed;
|
||||
// Sheets render inside the routes' subtrees, BELOW this overlay —
|
||||
// hide the card while any sheet is up so it never floats over one.
|
||||
final visible = hasSession && openSheets.value == 0;
|
||||
final useSideNav = PlatformDetector.shouldUseSideNavigation(context);
|
||||
_reportOverlayHeight(visible ? _cardHeight + (useSideNav ? 32 : 24) : 0);
|
||||
// Report off the session (not sheet) state so the underlying
|
||||
// screen's scroll padding doesn't jump while a sheet is open.
|
||||
_reportOverlayHeight(hasSession ? _cardHeight + (useSideNav ? 32 : 24) : 0);
|
||||
|
||||
final Widget child = visible
|
||||
? _MiniPlayerCard(
|
||||
@@ -270,6 +277,10 @@ class _MiniPlayerCardState extends State<_MiniPlayerCard> with ContextMenuTapMix
|
||||
height: _MusicMiniPlayerOverlayState._cardHeight,
|
||||
child: Stack(
|
||||
children: [
|
||||
// Played fraction tints the card background itself — the card
|
||||
// fills up as the track progresses (clipped by the Material's
|
||||
// rounded corners above).
|
||||
const Positioned.fill(child: _MiniPlayerProgress()),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Row(
|
||||
@@ -343,7 +354,6 @@ class _MiniPlayerCardState extends State<_MiniPlayerCard> with ContextMenuTapMix
|
||||
],
|
||||
),
|
||||
),
|
||||
const Positioned(left: 0, right: 0, bottom: 0, height: 2, child: _MiniPlayerProgress()),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -376,8 +386,9 @@ class _MiniPlayerCardState extends State<_MiniPlayerCard> with ContextMenuTapMix
|
||||
}
|
||||
}
|
||||
|
||||
/// Isolated progress leaf — positionStream ticks rebuild only this 2px line,
|
||||
/// never the card above it.
|
||||
/// Isolated progress leaf — positionStream ticks rebuild only this
|
||||
/// background layer, never the card content above it. The played fraction
|
||||
/// renders as a subtle full-height tint that fills the card left-to-right.
|
||||
class _MiniPlayerProgress extends StatelessWidget {
|
||||
const _MiniPlayerProgress();
|
||||
|
||||
@@ -396,7 +407,7 @@ class _MiniPlayerProgress extends StatelessWidget {
|
||||
child: FractionallySizedBox(
|
||||
widthFactor: fraction,
|
||||
heightFactor: 1,
|
||||
child: ColoredBox(color: tk.text.withValues(alpha: 0.9)),
|
||||
child: ColoredBox(color: tk.text.withValues(alpha: 0.08)),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -46,6 +46,12 @@ class OverlaySheetController {
|
||||
return context.dependOnInheritedWidgetOfExactType<_OverlaySheetScope>()?.controller;
|
||||
}
|
||||
|
||||
/// Number of sheets currently open across all hosts (and [showAdaptive]
|
||||
/// modal fallbacks). Sheets render inside their host's subtree, so chrome
|
||||
/// mounted above the navigator (the music mini-player) can never sit under
|
||||
/// them — such chrome listens here and hides itself while this is nonzero.
|
||||
static final ValueNotifier<int> openSheetCount = ValueNotifier<int>(0);
|
||||
|
||||
/// Whether a sheet is currently showing (including while animating closed).
|
||||
bool get isOpen => _state._isOpen;
|
||||
|
||||
@@ -108,7 +114,7 @@ class OverlaySheetController {
|
||||
FocusNode? initialFocusNode,
|
||||
Alignment alignment = Alignment.bottomCenter,
|
||||
bool showDragHandle = false,
|
||||
}) {
|
||||
}) async {
|
||||
final controller = maybeOf(context);
|
||||
if (controller != null) {
|
||||
return controller.show<T>(
|
||||
@@ -130,14 +136,22 @@ class OverlaySheetController {
|
||||
final isDesktop = size.width > 600;
|
||||
return BoxConstraints(maxWidth: isDesktop ? 700 : double.infinity, maxHeight: size.height * 0.75);
|
||||
}();
|
||||
return showModalBottomSheet<T>(
|
||||
context: context,
|
||||
builder: builder,
|
||||
constraints: effectiveConstraints,
|
||||
backgroundColor: backgroundColor ?? Theme.of(context).colorScheme.surface,
|
||||
barrierColor: Colors.black54,
|
||||
isScrollControlled: isScrollControlled,
|
||||
);
|
||||
openSheetCount.value++;
|
||||
try {
|
||||
return await showModalBottomSheet<T>(
|
||||
context: context,
|
||||
// The host path insets its sheet by the bottom safe area; mirror that
|
||||
// here so the last row clears the home indicator / gesture nav bar.
|
||||
builder: (context) => SafeArea(top: false, child: builder(context)),
|
||||
constraints: effectiveConstraints,
|
||||
backgroundColor: backgroundColor ?? Theme.of(context).colorScheme.surface,
|
||||
barrierColor: Colors.black54,
|
||||
isScrollControlled: isScrollControlled,
|
||||
showDragHandle: showDragHandle,
|
||||
);
|
||||
} finally {
|
||||
openSheetCount.value--;
|
||||
}
|
||||
}
|
||||
|
||||
/// Push a sub-page using the overlay system if available, otherwise fall
|
||||
@@ -146,12 +160,20 @@ class OverlaySheetController {
|
||||
BuildContext context, {
|
||||
required WidgetBuilder builder,
|
||||
FocusNode? initialFocusNode,
|
||||
}) {
|
||||
}) async {
|
||||
final controller = maybeOf(context);
|
||||
if (controller != null) {
|
||||
return controller.push<T>(builder: builder, initialFocusNode: initialFocusNode);
|
||||
}
|
||||
return showModalBottomSheet<T>(context: context, builder: builder);
|
||||
openSheetCount.value++;
|
||||
try {
|
||||
return await showModalBottomSheet<T>(
|
||||
context: context,
|
||||
builder: (context) => SafeArea(top: false, child: builder(context)),
|
||||
);
|
||||
} finally {
|
||||
openSheetCount.value--;
|
||||
}
|
||||
}
|
||||
|
||||
/// Close the sheet entirely. Uses overlay controller if available,
|
||||
@@ -270,6 +292,9 @@ class _OverlaySheetHostState extends State<OverlaySheetHost> with SingleTickerPr
|
||||
entry.completer.complete(null);
|
||||
}
|
||||
}
|
||||
// A host torn down mid-sheet (or mid-close animation) never reaches the
|
||||
// close completion below — release its slot in the global count here.
|
||||
if (_isOpen) OverlaySheetController.openSheetCount.value--;
|
||||
_sheetFocusScopeNode.dispose();
|
||||
_slideCurve.dispose();
|
||||
_animationController.dispose();
|
||||
@@ -314,7 +339,10 @@ class _OverlaySheetHostState extends State<OverlaySheetHost> with SingleTickerPr
|
||||
_dragOffset = 0;
|
||||
_isDragging = false;
|
||||
});
|
||||
if (!wasOpen) widget.onOpenChanged?.call(true);
|
||||
if (!wasOpen) {
|
||||
widget.onOpenChanged?.call(true);
|
||||
OverlaySheetController.openSheetCount.value++;
|
||||
}
|
||||
|
||||
BackKeyUpSuppressor.clearSuppression();
|
||||
_animationController.forward(from: 0);
|
||||
@@ -376,6 +404,7 @@ class _OverlaySheetHostState extends State<OverlaySheetHost> with SingleTickerPr
|
||||
_sheetHorizontalAnchor = null;
|
||||
});
|
||||
widget.onOpenChanged?.call(false);
|
||||
OverlaySheetController.openSheetCount.value--;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user