import 'dart:async'; import 'package:flutter/material.dart'; import 'feedback_helper.dart'; class FadeSlidePageRoute extends PageRouteBuilder { final Widget child; FadeSlidePageRoute({required this.child}) : super( pageBuilder: (context, animation, secondaryAnimation) => child, transitionsBuilder: (context, animation, secondaryAnimation, child) { final fadeAnimation = CurvedAnimation( parent: animation, curve: Curves.easeOut, ); final slideAnimation = Tween( begin: const Offset(0.0, 0.05), end: Offset.zero, ).animate( CurvedAnimation( parent: animation, curve: Curves.easeOutCubic, ), ); return FadeTransition( opacity: fadeAnimation, child: SlideTransition( position: slideAnimation, child: child, ), ); }, transitionDuration: const Duration(milliseconds: 300), reverseTransitionDuration: const Duration(milliseconds: 200), ) { AppFeedback.subtleHaptic(); } } void showTopAlert( BuildContext context, String message, { bool isError = false, IconData? icon, }) { final overlayState = Overlay.of(context); late OverlayEntry overlayEntry; overlayEntry = OverlayEntry( builder: (context) => _TopAlertWidget( message: message, isError: isError, icon: icon, onDismiss: () { overlayEntry.remove(); }, ), ); overlayState.insert(overlayEntry); } class _TopAlertWidget extends StatefulWidget { final String message; final bool isError; final IconData? icon; final VoidCallback onDismiss; const _TopAlertWidget({ required this.message, required this.isError, this.icon, required this.onDismiss, }); @override State<_TopAlertWidget> createState() => _TopAlertWidgetState(); } class _TopAlertWidgetState extends State<_TopAlertWidget> with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation _slideAnimation; late Animation _opacityAnimation; Timer? _dismissTimer; @override void initState() { super.initState(); _controller = AnimationController( vsync: this, duration: const Duration(milliseconds: 400), ); _slideAnimation = CurvedAnimation( parent: _controller, curve: Curves.easeOutBack, ); _opacityAnimation = CurvedAnimation( parent: _controller, curve: Curves.easeIn, ); _controller.forward(); _dismissTimer = Timer(const Duration(milliseconds: 2800), () { if (mounted) { _controller.reverse().then((_) { widget.onDismiss(); }); } }); } @override void dispose() { _dismissTimer?.cancel(); _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final mediaQuery = MediaQuery.of(context); final topPadding = mediaQuery.padding.top + 16.0; IconData alertIcon = widget.icon ?? (widget.isError ? Icons.error_outline : Icons.check_circle_outline); Color backgroundColor = widget.isError ? const Color(0xFFBA1A20) : const Color(0xFF2E7D32); return AnimatedBuilder( animation: _controller, builder: (context, child) { return Positioned( top: topPadding * _slideAnimation.value - 80.0 * (1.0 - _slideAnimation.value), left: 16.0, right: 16.0, child: Opacity( opacity: _opacityAnimation.value, child: Material( color: Colors.transparent, child: Align( alignment: Alignment.topCenter, child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 450), child: Container( padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), decoration: BoxDecoration( color: backgroundColor, borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.12), blurRadius: 16, offset: const Offset(0, 6), ), ], ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( alertIcon, color: Colors.white, size: 20, ), const SizedBox(width: 12), Expanded( child: Text( widget.message, style: const TextStyle( color: Colors.white, fontSize: 14, fontWeight: FontWeight.w600, ), ), ), ], ), ), ), ), ), ), ); }, ); } } class SlideFadeEntrance extends StatefulWidget { final Widget child; final Duration delay; final Duration duration; const SlideFadeEntrance({ super.key, required this.child, this.delay = Duration.zero, this.duration = const Duration(milliseconds: 500), }); @override State createState() => _SlideFadeEntranceState(); } class _SlideFadeEntranceState extends State with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation _opacityAnimation; late Animation _slideAnimation; Timer? _timer; @override void initState() { super.initState(); _controller = AnimationController( vsync: this, duration: widget.duration, ); _opacityAnimation = Tween(begin: 0.0, end: 1.0).animate( CurvedAnimation(parent: _controller, curve: Curves.easeIn), ); _slideAnimation = Tween(begin: 12.0, end: 0.0).animate( CurvedAnimation(parent: _controller, curve: Curves.easeOutCubic), ); if (widget.delay == Duration.zero) { _controller.forward(); } else { _timer = Timer(widget.delay, () { if (mounted) { _controller.forward(); } }); } } @override void dispose() { _timer?.cancel(); _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return AnimatedBuilder( animation: _controller, builder: (context, child) { return Opacity( opacity: _opacityAnimation.value, child: Transform.translate( offset: Offset(0.0, _slideAnimation.value), child: child, ), ); }, child: widget.child, ); } } class InteractiveScale extends StatefulWidget { final Widget child; final VoidCallback? onTap; final double hoverScale; final double tapScale; const InteractiveScale({ super.key, required this.child, this.onTap, this.hoverScale = 1.03, this.tapScale = 0.96, }); @override State createState() => _InteractiveScaleState(); } class _InteractiveScaleState extends State { double _scale = 1.0; @override Widget build(BuildContext context) { return MouseRegion( onEnter: (_) => setState(() => _scale = widget.hoverScale), onExit: (_) => setState(() => _scale = 1.0), child: AnimatedScale( scale: _scale, duration: const Duration(milliseconds: 120), child: GestureDetector( behavior: HitTestBehavior.opaque, onTapDown: (_) => setState(() => _scale = widget.tapScale), onTapUp: (_) => setState(() => _scale = widget.hoverScale), onTapCancel: () => setState(() => _scale = 1.0), onTap: widget.onTap, child: widget.child, ), ), ); } }