viewer: fixed snack bar margin on page transition

This commit is contained in:
Thibault Deckers 2024-04-01 23:00:34 +02:00
parent 9f9cdebc9e
commit 034a816704
2 changed files with 62 additions and 12 deletions

View file

@ -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<EdgeInsets?> snackBarMarginOverrideNotifier = ValueNotifier(null);
static EdgeInsets snackBarMarginDefault(BuildContext context) {
final mq = context.read<MediaQueryData>();
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<EdgeInsets?>(
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

View file

@ -63,7 +63,7 @@ class EntryViewerStack extends StatefulWidget {
State<EntryViewerStack> createState() => _EntryViewerStackState();
}
class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewControllerMixin, FeedbackMixin, TickerProviderStateMixin {
class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewControllerMixin, FeedbackMixin, TickerProviderStateMixin, RouteAware {
final Floating _floating = Floating();
late int _currentEntryIndex;
late ValueNotifier<int> _currentVerticalPage;
@ -184,6 +184,7 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
@override
void dispose() {
AvesApp.pageRouteObserver.unsubscribe(this);
_floating.dispose();
cleanEntryControllers(entryNotifier.value);
_videoActionDelegate.dispose();
@ -287,6 +288,41 @@ class _EntryViewerStackState extends State<EntryViewerStack> 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<EntryViewerStack> with EntryViewContr
void _onVerticalPageChanged(int page) {
_currentVerticalPage.value = page;
_overrideSnackBarMargin();
switch (page) {
case transitionPage:
dismissFeedback(context);