Files
SpotiFLAC-Mobile/lib/widgets/app_snack_bar.dart
T

193 lines
5.9 KiB
Dart

import 'package:flutter/material.dart';
import 'package:spotiflac_android/l10n/l10n.dart';
import 'package:spotiflac_android/theme/mornye_theme.dart';
import 'package:spotiflac_android/widgets/mornye_chrome.dart';
/// Applies the same transient-message policy to existing snackbar call sites.
/// A new message replaces the old one instead of building a stale FIFO queue.
class AppScaffoldMessenger extends ScaffoldMessenger {
const AppScaffoldMessenger({super.key, required super.child});
@override
ScaffoldMessengerState createState() => _AppScaffoldMessengerState();
}
class _AppScaffoldMessengerState extends ScaffoldMessengerState {
@override
ScaffoldFeatureController<SnackBar, SnackBarClosedReason> showSnackBar(
SnackBar snackBar, {
AnimationStyle? snackBarAnimationStyle,
}) {
removeCurrentSnackBar();
return super.showSnackBar(
_TransientSnackBar(snackBar),
snackBarAnimationStyle: snackBarAnimationStyle,
);
}
}
class _TransientSnackBar extends SnackBar {
_TransientSnackBar(this.original)
: super(
content: original.content,
duration: original.duration > const Duration(seconds: 3)
? const Duration(seconds: 3)
: original.duration,
persist: false,
);
final SnackBar original;
@override
SnackBar withAnimation(Animation<double> newAnimation, {Key? fallbackKey}) {
// Delegate first so custom glass animations remain intact.
final animated = original.withAnimation(
newAnimation,
fallbackKey: fallbackKey,
);
return SnackBar(
key: animated.key,
content: animated.content,
backgroundColor: animated.backgroundColor,
elevation: animated.elevation,
margin: animated.margin,
padding: animated.padding,
width: animated.width,
shape: animated.shape,
hitTestBehavior: animated.hitTestBehavior,
behavior: animated.behavior,
action: animated.action,
actionOverflowThreshold: animated.actionOverflowThreshold,
showCloseIcon: animated.showCloseIcon ?? true,
closeIconColor: animated.closeIconColor,
duration: duration,
persist: false,
animation: animated.animation,
onVisible: animated.onVisible,
dismissDirection: animated.dismissDirection,
clipBehavior: animated.clipBehavior,
);
}
}
/// Reports a failure to open a local file with a plain Material snackbar.
void showCannotOpenFileSnackBar(BuildContext context, Object error) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
context.l10n.snackbarCannotOpenFile(context.friendlyError(error)),
),
),
);
}
/// Transient messages use the same glass as confirmation dialogs in Mornye.
ScaffoldFeatureController<SnackBar, SnackBarClosedReason> showAppSnackBar(
BuildContext context, {
required Widget content,
Duration duration = const Duration(seconds: 3),
SnackBarBehavior? behavior,
}) {
final messenger = ScaffoldMessenger.of(context);
if (!context.isMornye) {
return messenger.showSnackBar(
SnackBar(
content: content,
duration: duration,
behavior: behavior,
persist: false,
showCloseIcon: true,
),
);
}
final theme = Theme.of(context);
final reduceMotion = MediaQuery.disableAnimationsOf(context);
final snackBar = _MornyeSnackBar(
duration: duration,
reduceMotion: reduceMotion,
content: Theme(
data: theme,
child: MornyeGlassPanel.overlay(
radius: 24,
// Messages have no modal scrim; let more of the page show through.
tintOpacity: 0.48,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
child: DefaultTextStyle(
style: theme.textTheme.bodyMedium!.copyWith(
color: theme.colorScheme.onSurface,
),
child: Row(
children: [
Expanded(child: content),
IconButton(
tooltip: MaterialLocalizations.of(context).closeButtonTooltip,
color: theme.colorScheme.onSurface,
onPressed: messenger.hideCurrentSnackBar,
icon: const Icon(Icons.close, size: 20),
),
],
),
),
),
),
),
);
return messenger.showSnackBar(snackBar);
}
class _MornyeSnackBar extends SnackBar {
const _MornyeSnackBar({
required super.content,
required super.duration,
required this.reduceMotion,
}) : super(
behavior: SnackBarBehavior.floating,
backgroundColor: Colors.transparent,
elevation: 0,
clipBehavior: Clip.none,
persist: false,
showCloseIcon: false,
padding: EdgeInsets.zero,
margin: const EdgeInsets.fromLTRB(20, 8, 20, 12),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(24)),
),
);
final bool reduceMotion;
@override
SnackBar withAnimation(Animation<double> newAnimation, {Key? fallbackKey}) {
// Keep the messenger's controller/timer for queuing and dismissal, but
// replace Material's opacity layer with movement so blur samples the page.
return SnackBar(
key: key ?? fallbackKey,
duration: duration,
persist: false,
showCloseIcon: false,
behavior: behavior,
backgroundColor: backgroundColor,
elevation: elevation,
clipBehavior: clipBehavior,
padding: padding,
margin: margin,
shape: shape,
animation: const AlwaysStoppedAnimation(1),
content: AnimatedBuilder(
animation: newAnimation,
child: content,
builder: (context, child) => Transform.translate(
offset: Offset(
0,
reduceMotion
? 0
: 8 * (1 - Curves.easeOutCubic.transform(newAnimation.value)),
),
child: child,
),
),
);
}
}