import 'dart:async'; import 'package:flutter/material.dart'; import 'package:askiineverdie/src/core/notification/notification_service.dart'; /// 알림 오버레이 위젯 (Phase 8: 팝업/토스트 알림) /// /// 화면 상단에 알림을 슬라이드 인/아웃 애니메이션으로 표시 class NotificationOverlay extends StatefulWidget { const NotificationOverlay({ super.key, required this.notificationService, required this.child, }); final NotificationService notificationService; final Widget child; @override State createState() => _NotificationOverlayState(); } class _NotificationOverlayState extends State with SingleTickerProviderStateMixin { GameNotification? _currentNotification; late AnimationController _animationController; late Animation _slideAnimation; late Animation _fadeAnimation; StreamSubscription? _notificationSub; StreamSubscription? _dismissSub; @override void initState() { super.initState(); _animationController = AnimationController( duration: const Duration(milliseconds: 300), vsync: this, ); // 하단에서 슬라이드 인/아웃 _slideAnimation = Tween(begin: const Offset(0, 1), end: Offset.zero) .animate( CurvedAnimation( parent: _animationController, curve: Curves.easeOutBack, ), ); _fadeAnimation = Tween(begin: 0, end: 1).animate( CurvedAnimation(parent: _animationController, curve: Curves.easeIn), ); _notificationSub = widget.notificationService.notifications.listen( _onNotification, ); _dismissSub = widget.notificationService.dismissals.listen(_onDismiss); } void _onNotification(GameNotification notification) { setState(() => _currentNotification = notification); _animationController.forward(); } void _onDismiss(void _) { _animationController.reverse().then((_) { if (mounted) { setState(() => _currentNotification = null); } }); } @override void dispose() { _notificationSub?.cancel(); _dismissSub?.cancel(); _animationController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Stack( children: [ widget.child, if (_currentNotification != null) Positioned( bottom: MediaQuery.of(context).padding.bottom + 80, left: 16, right: 16, child: SlideTransition( position: _slideAnimation, child: FadeTransition( opacity: _fadeAnimation, child: _NotificationCard( notification: _currentNotification!, onDismiss: widget.notificationService.dismiss, ), ), ), ), ], ); } } /// 알림 카드 위젯 class _NotificationCard extends StatelessWidget { const _NotificationCard({ required this.notification, required this.onDismiss, }); final GameNotification notification; final VoidCallback onDismiss; @override Widget build(BuildContext context) { final (bgColor, icon, iconColor) = _getStyleForType(notification.type); return Material( elevation: 8, borderRadius: BorderRadius.circular(12), color: bgColor, child: InkWell( onTap: onDismiss, borderRadius: BorderRadius.circular(12), child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Row( children: [ // 아이콘 Container( width: 40, height: 40, decoration: BoxDecoration( color: iconColor.withValues(alpha: 0.2), shape: BoxShape.circle, ), child: Icon(icon, color: iconColor, size: 24), ), const SizedBox(width: 12), // 텍스트 Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( notification.title, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: Colors.white, ), ), if (notification.subtitle != null) ...[ const SizedBox(height: 2), Text( notification.subtitle!, style: TextStyle( fontSize: 13, color: Colors.white.withValues(alpha: 0.8), ), ), ], ], ), ), // 닫기 버튼 IconButton( icon: const Icon(Icons.close, color: Colors.white70, size: 20), onPressed: onDismiss, padding: EdgeInsets.zero, constraints: const BoxConstraints(minWidth: 32, minHeight: 32), ), ], ), ), ), ); } (Color, IconData, Color) _getStyleForType(NotificationType type) { return switch (type) { NotificationType.levelUp => ( const Color(0xFF1565C0), Icons.trending_up, Colors.amber, ), NotificationType.questComplete => ( const Color(0xFF2E7D32), Icons.check_circle, Colors.lightGreen, ), NotificationType.actComplete => ( const Color(0xFF6A1B9A), Icons.flag, Colors.purpleAccent, ), NotificationType.newSpell => ( const Color(0xFF4527A0), Icons.auto_fix_high, Colors.deepPurpleAccent, ), NotificationType.newEquipment => ( const Color(0xFFE65100), Icons.shield, Colors.orange, ), NotificationType.bossDefeat => ( const Color(0xFFC62828), Icons.whatshot, Colors.redAccent, ), NotificationType.gameSaved => ( const Color(0xFF00695C), Icons.save, Colors.tealAccent, ), NotificationType.info => ( const Color(0xFF0277BD), Icons.info_outline, Colors.lightBlueAccent, ), NotificationType.warning => ( const Color(0xFFF57C00), Icons.warning_amber, Colors.amber, ), }; } }