import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/pods/notification.dart'; import 'package:island/services/responsive.dart'; import 'package:island/widgets/notification_item.dart'; import 'package:styled_widget/styled_widget.dart'; class NotificationOverlay extends HookConsumerWidget { const NotificationOverlay({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final notifications = ref.watch(notificationStateProvider); final isDesktop = isWideScreen(context); final devicePadding = MediaQuery.paddingOf(context); final topOffset = devicePadding.top + ((!kIsWeb && (Platform.isMacOS || Platform.isLinux || Platform.isWindows)) ? 40 : 16); if (notifications.isEmpty) { return const SizedBox.shrink(); } final itemWidth = isDesktop ? 420.0 : MediaQuery.sizeOf(context).width; if (isDesktop) { return Positioned( top: topOffset, left: 0, right: 0, child: Material( color: Colors.transparent, child: Column( spacing: 8, mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.end, mainAxisSize: MainAxisSize.min, children: notifications.asMap().entries.map((entry) { final item = entry.value; return AnimatedNotificationItem( key: Key(item.id), item: item, isDesktop: true, margin: EdgeInsets.symmetric(horizontal: 16), onDismiss: () { ref.read(notificationStateProvider.notifier).dismiss(item.id); }, ); }).toList(), ), ).width(itemWidth).alignment(Alignment.topRight), ); } else { // Non-desktop: use Stack with overlapping const double overlap = 20.0; return Positioned( top: topOffset, left: 0, right: 0, child: Material( color: Colors.transparent, child: SizedBox( height: MediaQuery.sizeOf(context).height, child: Stack( alignment: Alignment.topCenter, children: notifications.asMap().entries.map((entry) { final index = entry.key; final item = entry.value; return Positioned( top: index * overlap, left: 16, right: 16, child: Container( decoration: BoxDecoration( boxShadow: [ BoxShadow(color: Colors.black54, blurRadius: 4.0 + index * 2.0), ], ), child: AnimatedNotificationItem( key: Key(item.id), item: item, isDesktop: false, onDismiss: () { ref .read(notificationStateProvider.notifier) .dismiss(item.id); }, ), ), ); }).toList(), ), ), ).width(itemWidth).alignment(Alignment.topCenter), ); } } } class AnimatedNotificationItem extends HookConsumerWidget { final NotificationItem item; final VoidCallback onDismiss; final bool isDesktop; final EdgeInsets? margin; const AnimatedNotificationItem({ super.key, required this.item, required this.onDismiss, required this.isDesktop, this.margin, }); @override Widget build(BuildContext context, WidgetRef ref) { final animationController = useAnimationController( duration: const Duration(milliseconds: 300), reverseDuration: const Duration(milliseconds: 250), ); final progressController = useAnimationController(duration: item.duration); final curvedAnimation = CurvedAnimation( parent: animationController, curve: Curves.easeOutCubic, ); final slideTween = Tween( begin: isDesktop ? Offset(1.0, 0.0) : Offset(0.0, -1.0), end: Offset.zero, ); final progressAnimation = Tween( begin: 1.0, end: 0.0, ).animate(progressController); useEffect(() { animationController.forward(); progressController.forward(); return null; }, []); useEffect(() { if (item.dismissed) { animationController.reverse().then((_) { ref.read(notificationStateProvider.notifier).remove(item.id); }); } return null; }, [item.dismissed]); return SlideTransition( position: slideTween.animate(curvedAnimation), child: SizeTransition( sizeFactor: curvedAnimation, axis: Axis.vertical, child: Padding( padding: margin ?? EdgeInsets.zero, child: NotificationItemWidget( item: item, isDesktop: isDesktop, onDismiss: onDismiss, progress: progressAnimation, ), ), ), ); } }