2025-06-06 16:45:40 +03:00
|
|
|
|
import 'dart:async';
|
|
|
|
|
|
|
|
|
|
|
|
import 'package:flutter/material.dart';
|
2025-06-19 12:51:57 +03:00
|
|
|
|
import 'package:friflex_starter/app/theme/app_colors_scheme.dart';
|
2025-06-06 16:45:40 +03:00
|
|
|
|
import 'package:friflex_starter/app/ui_kit/app_box.dart';
|
|
|
|
|
|
|
|
|
|
|
|
/// {@template app_snackbar}
|
|
|
|
|
|
/// Перечисление для типов снекбаров
|
|
|
|
|
|
/// Используется для определения цвета и назначения снекбара
|
|
|
|
|
|
/// {@endtemplate}
|
|
|
|
|
|
enum TypeSnackBar {
|
|
|
|
|
|
/// Снекбар с успехом
|
|
|
|
|
|
success,
|
|
|
|
|
|
|
|
|
|
|
|
/// Снекбар с ошибкой
|
|
|
|
|
|
error,
|
2025-06-07 19:03:13 +03:00
|
|
|
|
|
|
|
|
|
|
/// Снекбар с информацией
|
|
|
|
|
|
info,
|
2025-06-06 16:45:40 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// {@template app_snackbar}
|
|
|
|
|
|
/// Менеджер для управления снекбарами
|
|
|
|
|
|
/// Используется для показа снекбаров в верхней части экрана
|
|
|
|
|
|
/// Имеет статические методы для показа снекбаров с разными типами
|
|
|
|
|
|
/// [showError] - для показа снекбара с ошибкой,
|
|
|
|
|
|
/// [showSuccess] - для показа снекбара с успехом
|
|
|
|
|
|
/// {@endtemplate}
|
|
|
|
|
|
class AppSnackBar extends StatefulWidget {
|
|
|
|
|
|
/// {@macro app_snackbar}
|
|
|
|
|
|
const AppSnackBar._({
|
|
|
|
|
|
required this.message,
|
|
|
|
|
|
required this.type,
|
|
|
|
|
|
required this.displayDuration,
|
|
|
|
|
|
this.onDismiss,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
/// Сообщение, которое будет отображаться в снекбаре
|
|
|
|
|
|
final String message;
|
|
|
|
|
|
|
|
|
|
|
|
/// Тип снекбара, определяющий его цвет и назначение
|
|
|
|
|
|
final TypeSnackBar type;
|
|
|
|
|
|
|
|
|
|
|
|
/// Продолжительность отображения снекбара
|
|
|
|
|
|
final Duration displayDuration;
|
|
|
|
|
|
|
|
|
|
|
|
/// Функция, вызываемая при закрытии снекбара
|
|
|
|
|
|
final VoidCallback? onDismiss;
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
|
State<AppSnackBar> createState() => _AppSnackBarState();
|
|
|
|
|
|
|
|
|
|
|
|
/// Показать снекбар с ошибкой
|
|
|
|
|
|
/// [context] - контекст, в котором будет показан снекбар
|
|
|
|
|
|
/// [message] - сообщение, которое будет отображаться в снекбаре
|
|
|
|
|
|
/// [displayDuration] - продолжительность отображения снекбара
|
|
|
|
|
|
/// По умолчанию 3 секунды
|
|
|
|
|
|
static void showError(
|
|
|
|
|
|
BuildContext context, {
|
|
|
|
|
|
required String message,
|
|
|
|
|
|
Duration displayDuration = const Duration(seconds: 3),
|
|
|
|
|
|
}) {
|
|
|
|
|
|
_show(
|
|
|
|
|
|
context: context,
|
|
|
|
|
|
message: message,
|
|
|
|
|
|
type: TypeSnackBar.error,
|
|
|
|
|
|
displayDuration: displayDuration,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-07 19:03:13 +03:00
|
|
|
|
/// Показать снекбар с информацией
|
|
|
|
|
|
/// [context] - контекст, в котором будет показан снекбар
|
|
|
|
|
|
/// [message] - сообщение, которое будет отображаться в снекбаре
|
|
|
|
|
|
/// [displayDuration] - продолжительность отображения снекбара
|
|
|
|
|
|
/// По умолчанию 3 секунды
|
|
|
|
|
|
static void showInfo(
|
|
|
|
|
|
BuildContext context, {
|
|
|
|
|
|
required String message,
|
|
|
|
|
|
Duration displayDuration = const Duration(seconds: 3),
|
|
|
|
|
|
}) {
|
|
|
|
|
|
_show(
|
|
|
|
|
|
context: context,
|
|
|
|
|
|
message: message,
|
|
|
|
|
|
type: TypeSnackBar.info,
|
|
|
|
|
|
displayDuration: displayDuration,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-06 16:45:40 +03:00
|
|
|
|
/// Показать снекбар с успехом
|
|
|
|
|
|
/// [context] - контекст, в котором будет показан снекбар
|
|
|
|
|
|
/// [message] - сообщение, которое будет отображаться в снекбаре
|
|
|
|
|
|
/// [displayDuration] - продолжительность отображения снекбара
|
|
|
|
|
|
/// По умолчанию 3 секунды
|
|
|
|
|
|
static void showSuccess({
|
|
|
|
|
|
required BuildContext context,
|
|
|
|
|
|
required String message,
|
|
|
|
|
|
Duration displayDuration = const Duration(seconds: 3),
|
|
|
|
|
|
}) {
|
|
|
|
|
|
_show(
|
|
|
|
|
|
context: context,
|
|
|
|
|
|
message: message,
|
|
|
|
|
|
type: TypeSnackBar.success,
|
|
|
|
|
|
displayDuration: displayDuration,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Приватный метод для показа снекбара
|
|
|
|
|
|
/// Используется статическими методами [showError] и [showSuccess]
|
|
|
|
|
|
static void _show({
|
|
|
|
|
|
required BuildContext context,
|
|
|
|
|
|
required String message,
|
|
|
|
|
|
required TypeSnackBar type,
|
|
|
|
|
|
required Duration displayDuration,
|
|
|
|
|
|
}) {
|
|
|
|
|
|
// Удаляем предыдущий снекбар
|
|
|
|
|
|
_removeCurrentSnackBar();
|
|
|
|
|
|
|
|
|
|
|
|
if (!context.mounted) return;
|
|
|
|
|
|
|
|
|
|
|
|
final overlay = Overlay.of(context);
|
|
|
|
|
|
final overlayEntry = OverlayEntry(
|
|
|
|
|
|
builder: (context) => AppSnackBar._(
|
|
|
|
|
|
message: message,
|
|
|
|
|
|
type: type,
|
|
|
|
|
|
displayDuration: displayDuration,
|
|
|
|
|
|
onDismiss: _removeCurrentSnackBar,
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
_currentSnackBar = overlayEntry;
|
|
|
|
|
|
overlay.insert(overlayEntry);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static OverlayEntry? _currentSnackBar;
|
|
|
|
|
|
|
|
|
|
|
|
static void _removeCurrentSnackBar() {
|
|
|
|
|
|
_currentSnackBar?.remove();
|
|
|
|
|
|
_currentSnackBar = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
class _AppSnackBarState extends State<AppSnackBar>
|
|
|
|
|
|
with SingleTickerProviderStateMixin {
|
|
|
|
|
|
late AnimationController _animationController;
|
|
|
|
|
|
late Animation<double> _slideAnimation;
|
|
|
|
|
|
Timer? _dismissTimer;
|
|
|
|
|
|
bool _isInitialized = false;
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
|
void initState() {
|
|
|
|
|
|
super.initState();
|
|
|
|
|
|
_animationController = AnimationController(
|
|
|
|
|
|
duration: const Duration(milliseconds: 300),
|
|
|
|
|
|
vsync: this,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
|
void didChangeDependencies() {
|
|
|
|
|
|
super.didChangeDependencies();
|
|
|
|
|
|
if (!_isInitialized) {
|
|
|
|
|
|
_initAnimation();
|
|
|
|
|
|
_startDismissTimer();
|
|
|
|
|
|
_isInitialized = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Инициализация анимации для снекбара
|
|
|
|
|
|
/// Используется для определения начальной и конечной позиции снекбара
|
|
|
|
|
|
/// Начальная позиция - за пределами экрана, конечная - 15 пикселей ниже верхнего отступа
|
|
|
|
|
|
void _initAnimation() {
|
|
|
|
|
|
final topPadding = MediaQuery.of(context).padding.top;
|
|
|
|
|
|
// Начальная позиция снекбара - за пределами экрана
|
|
|
|
|
|
final startPosition = -200.0;
|
|
|
|
|
|
// Конечная позиция снекбара - 15 пикселей ниже верхнего отступа
|
|
|
|
|
|
final endPosition = topPadding + 15;
|
|
|
|
|
|
// Создание анимации с использованием Tween
|
|
|
|
|
|
_slideAnimation = Tween<double>(begin: startPosition, end: endPosition)
|
|
|
|
|
|
.animate(
|
|
|
|
|
|
CurvedAnimation(parent: _animationController, curve: Curves.easeOut),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2025-11-17 11:51:42 +03:00
|
|
|
|
unawaited(_animationController.forward());
|
2025-06-06 16:45:40 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Запуск таймера для автоматического закрытия снекбара
|
2025-08-08 10:59:17 +03:00
|
|
|
|
/// Таймер срабатывает по истечении widget.displayDuration
|
2025-06-06 16:45:40 +03:00
|
|
|
|
/// и вызывает метод [_dismiss] для закрытия снекбара
|
|
|
|
|
|
void _startDismissTimer() {
|
|
|
|
|
|
_dismissTimer = Timer(widget.displayDuration, _dismiss);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Закрытие снекбара
|
|
|
|
|
|
/// Отменяет таймер, если он существует, и запускает обратную анимацию
|
2025-08-08 10:59:17 +03:00
|
|
|
|
/// После завершения анимации вызывает функцию widget.onDismiss, если она задана
|
2025-06-06 16:45:40 +03:00
|
|
|
|
/// Если виджет не смонтирован, ничего не делает
|
|
|
|
|
|
void _dismiss() {
|
|
|
|
|
|
if (!mounted) return;
|
|
|
|
|
|
|
|
|
|
|
|
_dismissTimer?.cancel();
|
2025-11-17 11:51:42 +03:00
|
|
|
|
unawaited(
|
|
|
|
|
|
_animationController.reverse().then((_) {
|
|
|
|
|
|
if (mounted) {
|
|
|
|
|
|
widget.onDismiss?.call();
|
|
|
|
|
|
}
|
|
|
|
|
|
}),
|
|
|
|
|
|
);
|
2025-06-06 16:45:40 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
|
void dispose() {
|
|
|
|
|
|
_dismissTimer?.cancel();
|
|
|
|
|
|
_animationController.dispose();
|
|
|
|
|
|
super.dispose();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
|
return AnimatedBuilder(
|
|
|
|
|
|
animation: _slideAnimation,
|
|
|
|
|
|
builder: (context, child) {
|
|
|
|
|
|
return Positioned(
|
2025-06-07 18:28:31 +03:00
|
|
|
|
left: 0,
|
|
|
|
|
|
right: 0,
|
2025-06-06 16:45:40 +03:00
|
|
|
|
top: _slideAnimation.value,
|
|
|
|
|
|
child: Material(
|
2025-06-07 18:28:31 +03:00
|
|
|
|
child: GestureDetector(
|
|
|
|
|
|
onTap: _dismiss,
|
|
|
|
|
|
behavior: HitTestBehavior.opaque,
|
|
|
|
|
|
child: Container(
|
|
|
|
|
|
constraints: const BoxConstraints(maxWidth: 350),
|
|
|
|
|
|
margin: const EdgeInsets.symmetric(horizontal: 16),
|
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
|
color: _getBackgroundColor(widget.type),
|
|
|
|
|
|
borderRadius: BorderRadius.circular(16),
|
|
|
|
|
|
),
|
|
|
|
|
|
padding: const EdgeInsets.symmetric(
|
|
|
|
|
|
vertical: 12,
|
|
|
|
|
|
horizontal: 16,
|
|
|
|
|
|
),
|
|
|
|
|
|
child: Row(
|
|
|
|
|
|
children: [
|
|
|
|
|
|
_Icon(type: widget.type),
|
|
|
|
|
|
const WBox(10),
|
|
|
|
|
|
Flexible(
|
|
|
|
|
|
child: Text(
|
|
|
|
|
|
widget.message,
|
2025-11-17 11:51:42 +03:00
|
|
|
|
style: const TextStyle(color: Colors.white),
|
2025-06-07 18:28:31 +03:00
|
|
|
|
maxLines: 3,
|
|
|
|
|
|
overflow: TextOverflow.ellipsis,
|
2025-06-06 16:45:40 +03:00
|
|
|
|
),
|
2025-06-07 18:28:31 +03:00
|
|
|
|
),
|
|
|
|
|
|
],
|
2025-06-06 16:45:40 +03:00
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Получение цвета фона снекбара в зависимости от его типа
|
|
|
|
|
|
/// Возвращает [Color] в зависимости от типа снекбара
|
|
|
|
|
|
/// [TypeSnackBar.success] - цвет успеха
|
|
|
|
|
|
/// [TypeSnackBar.error] - цвет ошибки
|
|
|
|
|
|
Color _getBackgroundColor(TypeSnackBar type) {
|
|
|
|
|
|
return switch (type) {
|
2025-06-19 12:51:57 +03:00
|
|
|
|
TypeSnackBar.success => context.appColors.successSnackbarBackground,
|
|
|
|
|
|
TypeSnackBar.error => context.appColors.errorSnackbarBackground,
|
|
|
|
|
|
TypeSnackBar.info => context.appColors.infoSnackbarBackground,
|
2025-06-06 16:45:40 +03:00
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-06-07 18:28:31 +03:00
|
|
|
|
|
2025-06-06 16:45:40 +03:00
|
|
|
|
/// {@template _Icon}
|
|
|
|
|
|
/// Виджет для отображения иконки в снекбаре
|
|
|
|
|
|
/// Используется для отображения иконки в зависимости от типа снекбара
|
|
|
|
|
|
/// {@endtemplate}
|
|
|
|
|
|
class _Icon extends StatelessWidget {
|
|
|
|
|
|
/// {@macro _Icon}
|
|
|
|
|
|
/// Создает экземпляр иконки для снекбара
|
|
|
|
|
|
/// Принимает [type] - тип снекбара, определяющий иконку
|
|
|
|
|
|
/// Используется для отображения иконки успеха или ошибки
|
|
|
|
|
|
const _Icon({required this.type});
|
|
|
|
|
|
|
|
|
|
|
|
/// Тип снекбара, определяющий иконку
|
|
|
|
|
|
final TypeSnackBar type;
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
|
return switch (type) {
|
2025-11-17 11:51:42 +03:00
|
|
|
|
TypeSnackBar.success => const Icon(
|
2025-06-06 16:45:40 +03:00
|
|
|
|
Icons.check_circle,
|
|
|
|
|
|
color: Colors.white,
|
|
|
|
|
|
size: 32,
|
|
|
|
|
|
),
|
2025-11-17 11:51:42 +03:00
|
|
|
|
TypeSnackBar.error => const Icon(
|
|
|
|
|
|
Icons.error,
|
|
|
|
|
|
color: Colors.white,
|
|
|
|
|
|
size: 32,
|
|
|
|
|
|
),
|
|
|
|
|
|
TypeSnackBar.info => const Icon(
|
|
|
|
|
|
Icons.info,
|
|
|
|
|
|
color: Colors.white,
|
|
|
|
|
|
size: 32,
|
|
|
|
|
|
),
|
2025-06-06 16:45:40 +03:00
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|