Files
asciinevrdie/lib/src/features/game/widgets/notification_overlay.dart
JiWoong Sul ff24f2bb55 style(ui): 폰트 크기 및 레이아웃 조정
- 전역 테마 폰트 크기 증가 (가독성 개선)
- 위젯 레이아웃 미세 조정
2026-01-05 19:42:09 +09:00

287 lines
9.2 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:asciineverdie/src/core/notification/notification_service.dart';
import 'package:asciineverdie/src/shared/retro_colors.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 (accentColor, icon, asciiIcon) = _getStyleForType(context, notification.type);
final panelBg = RetroColors.panelBgOf(context);
final borderColor = RetroColors.borderOf(context);
final surface = RetroColors.surfaceOf(context);
final textPrimary = RetroColors.textPrimaryOf(context);
final textMuted = RetroColors.textMutedOf(context);
return GestureDetector(
onTap: onDismiss,
child: Container(
decoration: BoxDecoration(
color: panelBg,
border: Border(
top: BorderSide(color: accentColor, width: 3),
left: BorderSide(color: accentColor, width: 3),
bottom: BorderSide(color: borderColor, width: 3),
right: BorderSide(color: borderColor, width: 3),
),
boxShadow: [
BoxShadow(
color: accentColor.withValues(alpha: 0.4),
blurRadius: 12,
spreadRadius: 2,
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// 헤더 바
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
color: accentColor.withValues(alpha: 0.3),
child: Row(
children: [
// ASCII 아이콘
Text(
asciiIcon,
style: TextStyle(
fontFamily: 'JetBrainsMono',
fontSize: 12,
color: accentColor,
fontWeight: FontWeight.bold,
),
),
const SizedBox(width: 8),
// 타입 표시
Expanded(
child: Text(
_getTypeLabel(notification.type),
style: TextStyle(
fontFamily: 'PressStart2P',
fontSize: 9,
color: accentColor,
letterSpacing: 1,
),
),
),
// 닫기 버튼
GestureDetector(
onTap: onDismiss,
child: Text(
'[X]',
style: TextStyle(
fontFamily: 'PressStart2P',
fontSize: 9,
color: textMuted,
),
),
),
],
),
),
// 본문
Padding(
padding: const EdgeInsets.all(12),
child: Row(
children: [
// 아이콘 박스
Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: surface,
border: Border.all(color: accentColor, width: 2),
),
child: Icon(icon, color: accentColor, size: 20),
),
const SizedBox(width: 12),
// 텍스트
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
notification.title,
style: TextStyle(
fontFamily: 'PressStart2P',
fontSize: 9,
color: textPrimary,
),
),
if (notification.subtitle != null) ...[
const SizedBox(height: 4),
Text(
notification.subtitle!,
style: TextStyle(
fontFamily: 'PressStart2P',
fontSize: 9,
color: textMuted,
),
),
],
],
),
),
],
),
),
],
),
),
);
}
/// 알림 타입별 레트로 스타일 (강조 색상, 아이콘, ASCII 아이콘)
(Color, IconData, String) _getStyleForType(
BuildContext context,
NotificationType type,
) {
final gold = RetroColors.goldOf(context);
final exp = RetroColors.expOf(context);
final mp = RetroColors.mpOf(context);
final hp = RetroColors.hpOf(context);
return switch (type) {
NotificationType.levelUp => (gold, Icons.arrow_upward, ''),
NotificationType.questComplete => (exp, Icons.check, ''),
NotificationType.actComplete => (mp, Icons.flag, ''),
NotificationType.newSpell => (const Color(0xFF9966FF), Icons.auto_fix_high, ''),
NotificationType.newEquipment => (const Color(0xFFFF9933), Icons.shield, ''),
NotificationType.bossDefeat => (hp, Icons.whatshot, ''),
NotificationType.gameSaved => (exp, Icons.save, '💾'),
NotificationType.info => (mp, Icons.info_outline, ''),
NotificationType.warning => (const Color(0xFFFFCC00), Icons.warning, ''),
};
}
/// 알림 타입 라벨
String _getTypeLabel(NotificationType type) {
return switch (type) {
NotificationType.levelUp => 'LEVEL UP',
NotificationType.questComplete => 'QUEST DONE',
NotificationType.actComplete => 'ACT CLEAR',
NotificationType.newSpell => 'NEW SPELL',
NotificationType.newEquipment => 'NEW ITEM',
NotificationType.bossDefeat => 'BOSS SLAIN',
NotificationType.gameSaved => 'SAVED',
NotificationType.info => 'INFO',
NotificationType.warning => 'WARNING',
};
}
}