mirror of
https://github.com/inventree/inventree-app.git
synced 2026-05-20 14:46:42 +00:00
Snackbar displays at top (#820)
- Closes https://github.com/inventree/inventree-app/issues/626
This commit is contained in:
+157
-33
@@ -1,3 +1,5 @@
|
|||||||
|
import "dart:async";
|
||||||
|
|
||||||
import "package:flutter/material.dart";
|
import "package:flutter/material.dart";
|
||||||
import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
|
import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
|
||||||
import "package:one_context/one_context.dart";
|
import "package:one_context/one_context.dart";
|
||||||
@@ -5,8 +7,10 @@ import "package:one_context/one_context.dart";
|
|||||||
import "package:inventree/helpers.dart";
|
import "package:inventree/helpers.dart";
|
||||||
import "package:inventree/l10.dart";
|
import "package:inventree/l10.dart";
|
||||||
|
|
||||||
|
OverlayEntry? _currentSnackOverlay;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Display a configurable 'snackbar' at the bottom of the screen
|
* Display a configurable 'snackbar' at the top of the screen
|
||||||
*/
|
*/
|
||||||
void showSnackIcon(
|
void showSnackIcon(
|
||||||
String text, {
|
String text, {
|
||||||
@@ -19,23 +23,21 @@ void showSnackIcon(
|
|||||||
|
|
||||||
// Escape quickly if we do not have context
|
// Escape quickly if we do not have context
|
||||||
if (!hasContext()) {
|
if (!hasContext()) {
|
||||||
// Debug message for unit testing
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
BuildContext? context = OneContext().context;
|
BuildContext? context = OneContext().context;
|
||||||
|
if (context == null) return;
|
||||||
|
|
||||||
if (context != null) {
|
// Dismiss any currently visible snack immediately
|
||||||
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
_currentSnackOverlay?.remove();
|
||||||
}
|
_currentSnackOverlay = null;
|
||||||
|
|
||||||
Color backgroundColor = Colors.deepOrange;
|
Color backgroundColor = Colors.deepOrange;
|
||||||
|
|
||||||
// Make some selections based on the "success" value
|
|
||||||
if (success != null && success == true) {
|
if (success != null && success == true) {
|
||||||
backgroundColor = Colors.lightGreen;
|
backgroundColor = Colors.lightGreen;
|
||||||
|
|
||||||
// Select an icon if we do not have an action
|
|
||||||
if (icon == null && onAction == null) {
|
if (icon == null && onAction == null) {
|
||||||
icon = TablerIcons.circle_check;
|
icon = TablerIcons.circle_check;
|
||||||
}
|
}
|
||||||
@@ -47,35 +49,157 @@ void showSnackIcon(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _action = actionText ?? L10().details;
|
final String actionLabel = actionText ?? L10().details;
|
||||||
|
final Duration duration = Duration(seconds: onAction == null ? 5 : 10);
|
||||||
|
|
||||||
List<Widget> childs = [Text(text), Spacer()];
|
late OverlayEntry entry;
|
||||||
|
entry = OverlayEntry(
|
||||||
if (icon != null) {
|
builder: (ctx) => _TopSnackBar(
|
||||||
childs.add(Icon(icon));
|
text: text,
|
||||||
}
|
icon: icon,
|
||||||
|
|
||||||
OneContext().showSnackBar(
|
|
||||||
builder: (context) => SnackBar(
|
|
||||||
content: GestureDetector(
|
|
||||||
child: Row(children: childs),
|
|
||||||
onTap: () {
|
|
||||||
ScaffoldMessenger.of(context!).hideCurrentSnackBar();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
backgroundColor: backgroundColor,
|
backgroundColor: backgroundColor,
|
||||||
showCloseIcon: true,
|
onAction: onAction,
|
||||||
action: onAction == null
|
actionText: actionLabel,
|
||||||
? null
|
duration: duration,
|
||||||
: SnackBarAction(
|
onDismiss: () {
|
||||||
label: _action,
|
entry.remove();
|
||||||
onPressed: () {
|
if (_currentSnackOverlay == entry) {
|
||||||
// Immediately dismiss the notification
|
_currentSnackOverlay = null;
|
||||||
ScaffoldMessenger.of(context!).hideCurrentSnackBar();
|
}
|
||||||
onAction();
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
duration: Duration(seconds: onAction == null ? 5 : 10),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
_currentSnackOverlay = entry;
|
||||||
|
Overlay.of(context).insert(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TopSnackBar extends StatefulWidget {
|
||||||
|
const _TopSnackBar({
|
||||||
|
required this.text,
|
||||||
|
required this.backgroundColor,
|
||||||
|
required this.duration,
|
||||||
|
required this.onDismiss,
|
||||||
|
this.icon,
|
||||||
|
this.onAction,
|
||||||
|
this.actionText,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String text;
|
||||||
|
final IconData? icon;
|
||||||
|
final Color backgroundColor;
|
||||||
|
final Function()? onAction;
|
||||||
|
final String? actionText;
|
||||||
|
final Duration duration;
|
||||||
|
final VoidCallback onDismiss;
|
||||||
|
|
||||||
|
@override
|
||||||
|
_TopSnackBarState createState() => _TopSnackBarState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TopSnackBarState extends State<_TopSnackBar>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late final AnimationController _controller;
|
||||||
|
late final Animation<Offset> _slideAnimation;
|
||||||
|
Timer? _timer;
|
||||||
|
bool _dismissed = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
_controller = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
);
|
||||||
|
|
||||||
|
_slideAnimation = Tween<Offset>(
|
||||||
|
begin: const Offset(0, -1),
|
||||||
|
end: Offset.zero,
|
||||||
|
).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOut));
|
||||||
|
|
||||||
|
_controller.forward();
|
||||||
|
_timer = Timer(widget.duration, _dismiss);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_timer?.cancel();
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _dismiss() {
|
||||||
|
if (_dismissed) return;
|
||||||
|
_dismissed = true;
|
||||||
|
_timer?.cancel();
|
||||||
|
_timer = null;
|
||||||
|
_controller.reverse().then((_) {
|
||||||
|
if (mounted) widget.onDismiss();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Positioned(
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
child: SlideTransition(
|
||||||
|
position: _slideAnimation,
|
||||||
|
child: Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: ColoredBox(
|
||||||
|
color: widget.backgroundColor,
|
||||||
|
child: SafeArea(
|
||||||
|
bottom: false,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: _dismiss,
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
widget.text,
|
||||||
|
style: const TextStyle(color: Colors.white),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (widget.icon != null)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 8),
|
||||||
|
child: Icon(widget.icon, color: Colors.white),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (widget.onAction != null)
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
_dismiss();
|
||||||
|
widget.onAction!();
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
widget.actionText ?? "",
|
||||||
|
style: const TextStyle(color: Colors.white),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.close, color: Colors.white),
|
||||||
|
onPressed: _dismiss,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user