diff --git a/lib/widgets/common/action_mixins/feedback.dart b/lib/widgets/common/action_mixins/feedback.dart index c302e6ebb..c077cb420 100644 --- a/lib/widgets/common/action_mixins/feedback.dart +++ b/lib/widgets/common/action_mixins/feedback.dart @@ -7,6 +7,7 @@ import 'package:aves/model/settings/settings.dart'; import 'package:aves/services/accessibility_service.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; +import 'package:aves/widgets/common/action_mixins/overlay_snack_bar.dart'; import 'package:aves/widgets/common/basic/circle.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/viewer/entry_viewer_page.dart'; @@ -58,24 +59,22 @@ mixin FeedbackMixin { (context) => SafeArea( child: Padding( padding: margin, - child: SnackBar( + child: OverlaySnackBar( content: snackBarContent, - animation: const AlwaysStoppedAnimation(1), action: action != null - ? SnackBarAction( - label: action.label, + ? TextButton( + style: ButtonStyle( + foregroundColor: MaterialStateProperty.all(Theme.of(context).snackBarTheme.actionTextColor), + ), onPressed: () { - // the regular snack bar dismiss behavior is confused - // because it expects a `Scaffold` in context, - // so we manually dimiss the overlay entry - // TODO TLAD [bug] after dismissing the overlay, tapping on the snack bar area makes the overlay visible again notificationOverlayEntry?.dismiss(); action.onPressed(); }, + child: Text(action.label), ) : null, - duration: duration, dismissDirection: DismissDirection.horizontal, + onDismiss: () => notificationOverlayEntry?.dismiss(), ), ), ), diff --git a/lib/widgets/common/action_mixins/overlay_snack_bar.dart b/lib/widgets/common/action_mixins/overlay_snack_bar.dart new file mode 100644 index 000000000..5d8f883fd --- /dev/null +++ b/lib/widgets/common/action_mixins/overlay_snack_bar.dart @@ -0,0 +1,135 @@ +import 'package:flutter/material.dart'; + +// adapted from Flutter `SnackBar` in `/material/snack_bar.dart` + +// As of Flutter v3.0.1, `SnackBar` is not customizable enough to add margin +// and ignore pointers in that area, so we use an overlay entry instead. +// This overlay entry is not below a `Scaffold` (which is expected by `SnackBar` +// and `SnackBarAction`), and is not dismissed the same way. +// This adaptation assumes the `SnackBarBehavior.floating` behavior and no animation. +class OverlaySnackBar extends StatelessWidget { + final Widget content; + final Widget? action; + final DismissDirection dismissDirection; + final VoidCallback onDismiss; + + const OverlaySnackBar({ + super.key, + required this.content, + required this.action, + required this.dismissDirection, + required this.onDismiss, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; + final snackBarTheme = theme.snackBarTheme; + final isThemeDark = theme.brightness == Brightness.dark; + final buttonColor = isThemeDark ? colorScheme.primary : colorScheme.secondary; + + final brightness = isThemeDark ? Brightness.light : Brightness.dark; + final themeBackgroundColor = isThemeDark ? colorScheme.onSurface : Color.alphaBlend(colorScheme.onSurface.withOpacity(0.80), colorScheme.surface); + final inverseTheme = theme.copyWith( + colorScheme: ColorScheme( + primary: colorScheme.onPrimary, + secondary: buttonColor, + surface: colorScheme.onSurface, + background: themeBackgroundColor, + error: colorScheme.onError, + onPrimary: colorScheme.primary, + onSecondary: colorScheme.secondary, + onSurface: colorScheme.surface, + onBackground: colorScheme.background, + onError: colorScheme.error, + brightness: brightness, + ), + ); + + final contentTextStyle = snackBarTheme.contentTextStyle ?? ThemeData(brightness: brightness).textTheme.subtitle1; + + const horizontalPadding = 16.0; + final padding = EdgeInsetsDirectional.only(start: horizontalPadding, end: action != null ? 0 : horizontalPadding); + const actionHorizontalMargin = horizontalPadding / 2; + const singleLineVerticalPadding = 14.0; + + Widget snackBar = Padding( + padding: padding, + child: Row( + children: [ + Expanded( + child: Container( + padding: const EdgeInsets.symmetric(vertical: singleLineVerticalPadding), + child: DefaultTextStyle( + style: contentTextStyle!, + child: content, + ), + ), + ), + if (action != null) + Padding( + padding: const EdgeInsets.symmetric(horizontal: actionHorizontalMargin), + child: TextButtonTheme( + data: TextButtonThemeData( + style: TextButton.styleFrom( + primary: buttonColor, + padding: const EdgeInsets.symmetric(horizontal: horizontalPadding), + ), + ), + child: action!, + ), + ), + ], + ), + ); + + final elevation = snackBarTheme.elevation ?? 6.0; + final backgroundColor = snackBarTheme.backgroundColor ?? inverseTheme.colorScheme.background; + final shape = snackBarTheme.shape ?? const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0))); + + snackBar = Material( + shape: shape, + elevation: elevation, + color: backgroundColor, + child: Theme( + data: inverseTheme, + child: snackBar, + ), + ); + + const topMargin = 5.0; + const bottomMargin = 10.0; + const horizontalMargin = 15.0; + snackBar = Padding( + padding: const EdgeInsets.fromLTRB( + horizontalMargin, + topMargin, + horizontalMargin, + bottomMargin, + ), + child: snackBar, + ); + + snackBar = SafeArea( + top: false, + bottom: false, + child: snackBar, + ); + + snackBar = Semantics( + container: true, + liveRegion: true, + onDismiss: onDismiss, + child: Dismissible( + key: const Key('dismissible'), + direction: dismissDirection, + resizeDuration: null, + onDismissed: (direction) => onDismiss(), + child: snackBar, + ), + ); + + return snackBar; + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 0c6ebc464..dd51ee048 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -146,6 +146,9 @@ flutter: # `OutputBuffer` in `/services/common/output_buffer.dart` # adapts from Flutter `_OutputBuffer` in `/foundation/consolidate_response.dart` # +# `OverlaySnackBar` in `/widgets/common/action_mixins/overlay_snack_bar.dart` +# adapts from Flutter `SnackBar` in `/material/snack_bar.dart` +# # `EagerScaleGestureRecognizer` in `/widgets/common/behaviour/eager_scale_gesture_recognizer.dart` # adapts from Flutter `ScaleGestureRecognizer` in `/gestures/scale.dart` #