diff --git a/lib/widgets/common/action_mixins/feedback.dart b/lib/widgets/common/action_mixins/feedback.dart index 0ad9821f9..7ccf6725e 100644 --- a/lib/widgets/common/action_mixins/feedback.dart +++ b/lib/widgets/common/action_mixins/feedback.dart @@ -11,8 +11,8 @@ import 'package:aves/widgets/common/action_mixins/overlay_snack_bar.dart'; import 'package:aves/widgets/common/basic/circle.dart'; import 'package:aves/widgets/common/basic/text/change_highlight.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/common/extensions/media_query.dart'; import 'package:aves/widgets/common/extensions/theme.dart'; -import 'package:aves/widgets/viewer/entry_viewer_page.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:overlay_support/overlay_support.dart'; @@ -22,6 +22,13 @@ import 'package:provider/provider.dart'; enum FeedbackType { info, warn } mixin FeedbackMixin { + static final ValueNotifier snackBarMarginOverrideNotifier = ValueNotifier(null); + + static EdgeInsets snackBarMarginDefault(BuildContext context) { + final mq = context.read(); + return EdgeInsets.only(bottom: max(mq.effectiveBottomPadding, mq.systemGestureInsets.bottom)); + } + void dismissFeedback(BuildContext context) => ScaffoldMessenger.of(context).hideCurrentSnackBar(); void showFeedback(BuildContext context, FeedbackType type, String message, [SnackBarAction? action]) { @@ -53,10 +60,7 @@ mixin FeedbackMixin { stop: action != null ? start.add(duration) : null, ); - if (context.currentRouteName == EntryViewerPage.routeName) { - // avoid interactive widgets at the bottom of the page - final margin = EntryViewerPage.snackBarMargin(context); - + if (snackBarMarginOverrideNotifier.value != null) { // as of Flutter v2.10.4, `SnackBar` can only be positioned at the bottom, // and space under the snack bar `margin` does not receive gestures // (because it is used by the `Dismissible` wrapping the snack bar) @@ -65,8 +69,15 @@ mixin FeedbackMixin { notificationOverlayEntry = showOverlayNotification( (context) => SafeArea( bottom: false, - child: Padding( - padding: margin, + child: ValueListenableBuilder( + valueListenable: snackBarMarginOverrideNotifier, + builder: (context, margin, child) { + return AnimatedPadding( + padding: margin ?? snackBarMarginDefault(context), + duration: ADurations.pageTransitionAnimation, + child: child, + ); + }, child: OverlaySnackBar( content: snackBarContent, action: action != null @@ -346,6 +357,7 @@ class _FeedbackMessageState extends State<_FeedbackMessage> with SingleTickerPro final contentTextFontSize = contentTextStyle.fontSize ?? theme.textTheme.bodyMedium!.fontSize!; final timerChangeShadowColor = colorScheme.primary; + final remainingDurationAnimation = _remainingDurationMillis; return Row( children: [ if (widget.type == FeedbackType.warn) ...[ @@ -356,16 +368,17 @@ class _FeedbackMessageState extends State<_FeedbackMessage> with SingleTickerPro const SizedBox(width: 8), ], Expanded(child: Text(widget.message)), - if (_remainingDurationMillis != null) ...[ + if (remainingDurationAnimation != null) ...[ const SizedBox(width: 16), AnimatedBuilder( - animation: _remainingDurationMillis!, + animation: remainingDurationAnimation, builder: (context, child) { - final remainingDurationMillis = _remainingDurationMillis!.value; + final remainingDurationMillis = remainingDurationAnimation.value; + final totalDurationMillis = _totalDurationMillis; return CircularIndicator( radius: 16, lineWidth: 2, - percent: remainingDurationMillis / _totalDurationMillis!, + percent: totalDurationMillis != null && totalDurationMillis > 0 ? remainingDurationMillis / totalDurationMillis : 0, background: Colors.grey, // progress color is provided by the caller, // because we cannot use the app context theme here diff --git a/lib/widgets/viewer/entry_viewer_stack.dart b/lib/widgets/viewer/entry_viewer_stack.dart index 9b8661f9b..75d06532b 100644 --- a/lib/widgets/viewer/entry_viewer_stack.dart +++ b/lib/widgets/viewer/entry_viewer_stack.dart @@ -63,7 +63,7 @@ class EntryViewerStack extends StatefulWidget { State createState() => _EntryViewerStackState(); } -class _EntryViewerStackState extends State with EntryViewControllerMixin, FeedbackMixin, TickerProviderStateMixin { +class _EntryViewerStackState extends State with EntryViewControllerMixin, FeedbackMixin, TickerProviderStateMixin, RouteAware { final Floating _floating = Floating(); late int _currentEntryIndex; late ValueNotifier _currentVerticalPage; @@ -184,6 +184,7 @@ class _EntryViewerStackState extends State with EntryViewContr @override void dispose() { + AvesApp.pageRouteObserver.unsubscribe(this); _floating.dispose(); cleanEntryControllers(entryNotifier.value); _videoActionDelegate.dispose(); @@ -287,6 +288,41 @@ class _EntryViewerStackState extends State with EntryViewContr ); } + // route aware + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + final route = ModalRoute.of(context); + if (route is PageRoute) { + AvesApp.pageRouteObserver.subscribe(this, route); + } + } + + @override + void didPopNext() => _overrideSnackBarMargin(); + + @override + void didPush() => _overrideSnackBarMargin(); + + @override + void didPop() => _resetSnackBarMargin(); + + @override + void didPushNext() => _resetSnackBarMargin(); + + void _overrideSnackBarMargin() { + if (isViewingImage) { + FeedbackMixin.snackBarMarginOverrideNotifier.value = EdgeInsets.only(bottom: ViewerBottomOverlay.actionSafeHeight(context)); + } else { + FeedbackMixin.snackBarMarginOverrideNotifier.value = FeedbackMixin.snackBarMarginDefault(context); + } + } + + void _resetSnackBarMargin() => FeedbackMixin.snackBarMarginOverrideNotifier.value = null; + + // lifecycle + void _onAppLifecycleStateChanged() { switch (AvesApp.lifecycleStateNotifier.value) { case AppLifecycleState.inactive: @@ -662,6 +698,7 @@ class _EntryViewerStackState extends State with EntryViewContr void _onVerticalPageChanged(int page) { _currentVerticalPage.value = page; + _overrideSnackBarMargin(); switch (page) { case transitionPage: dismissFeedback(context);