feat(ui): Phase 8 실시간 피드백 시스템 구현
- StatsPanel: 스탯 변화 애니메이션 (증감 표시) - CombatLog: 전투 이벤트 로그 위젯 - NotificationService: 큐 기반 알림 관리 - NotificationOverlay: 레벨업/퀘스트 완료 팝업 알림 - GamePlayScreen: 새 위젯 통합
This commit is contained in:
218
lib/src/features/game/widgets/notification_overlay.dart
Normal file
218
lib/src/features/game/widgets/notification_overlay.dart
Normal file
@@ -0,0 +1,218 @@
|
||||
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<NotificationOverlay> createState() => _NotificationOverlayState();
|
||||
}
|
||||
|
||||
class _NotificationOverlayState extends State<NotificationOverlay>
|
||||
with SingleTickerProviderStateMixin {
|
||||
GameNotification? _currentNotification;
|
||||
late AnimationController _animationController;
|
||||
late Animation<Offset> _slideAnimation;
|
||||
late Animation<double> _fadeAnimation;
|
||||
|
||||
StreamSubscription<GameNotification>? _notificationSub;
|
||||
StreamSubscription<void>? _dismissSub;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_animationController = AnimationController(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_slideAnimation = Tween<Offset>(
|
||||
begin: const Offset(0, -1),
|
||||
end: Offset.zero,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: Curves.easeOutBack,
|
||||
));
|
||||
|
||||
_fadeAnimation = Tween<double>(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(
|
||||
top: MediaQuery.of(context).padding.top + 16,
|
||||
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,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user