Files
asciinevrdie/lib/src/features/game/widgets/notification_overlay.dart
JiWoong Sul e6af7dd91a feat(ui): 반응형 레이아웃 및 저장 시스템 개선
## 반응형 레이아웃
- app.dart: 화면 크기별 레이아웃 분기 로직 추가 (+173 라인)
- game_play_screen.dart: 반응형 UI 구조 개선
- layouts/, pages/ 디렉토리 추가 (새 레이아웃 시스템)
- carousel_nav_bar.dart: 캐러셀 네비게이션 바 추가
- enhanced_animation_panel.dart: 향상된 애니메이션 패널

## 저장 시스템
- save_manager.dart: 저장 관리 기능 확장
- save_repository.dart: 저장소 인터페이스 개선
- save_service.dart: 저장 서비스 로직 추가

## UI 개선
- notification_service.dart: 알림 시스템 기능 확장
- notification_overlay.dart: 오버레이 UI 개선
- equipment_stats_panel.dart: 장비 스탯 패널 개선
- cinematic_view.dart: 시네마틱 뷰 개선
- new_character_screen.dart: 캐릭터 생성 화면 개선

## 다국어
- game_text_l10n.dart: 텍스트 추가 (+182 라인)

## 테스트
- 관련 테스트 파일 업데이트
2025-12-23 17:52:43 +09:00

236 lines
6.6 KiB
Dart

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(
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,
),
};
}
}