diff --git a/lib/src/core/notification/notification_service.dart b/lib/src/core/notification/notification_service.dart new file mode 100644 index 0000000..926ec81 --- /dev/null +++ b/lib/src/core/notification/notification_service.dart @@ -0,0 +1,149 @@ +import 'dart:async'; + +/// 알림 타입 (Notification Type) +enum NotificationType { + levelUp, // 레벨업 + questComplete, // 퀘스트 완료 + actComplete, // 막(Act) 완료 + newSpell, // 새 주문 습득 + newEquipment, // 새 장비 획득 + bossDefeat, // 보스 처치 +} + +/// 게임 알림 데이터 (Game Notification) +class GameNotification { + const GameNotification({ + required this.type, + required this.title, + this.subtitle, + this.data, + this.duration = const Duration(seconds: 3), + }); + + final NotificationType type; + final String title; + final String? subtitle; + final Map? data; + final Duration duration; +} + +/// 알림 서비스 (Phase 8: 이벤트 기반 알림 관리) +/// +/// 게임 이벤트(레벨업, 퀘스트 완료 등)를 큐에 추가하고 +/// 순차적으로 UI에 표시 +class NotificationService { + NotificationService(); + + final _notificationController = + StreamController.broadcast(); + final _dismissController = StreamController.broadcast(); + + /// 알림 스트림 (Notification Stream) + Stream get notifications => _notificationController.stream; + + /// 알림 닫기 스트림 + Stream get dismissals => _dismissController.stream; + + /// 알림 큐 (대기 중인 알림) + final List _queue = []; + bool _isShowing = false; + + /// 알림 추가 (Add Notification) + void show(GameNotification notification) { + _queue.add(notification); + _processQueue(); + } + + /// 레벨업 알림 (Level Up Notification) + void showLevelUp(int newLevel) { + show(GameNotification( + type: NotificationType.levelUp, + title: 'LEVEL UP!', + subtitle: 'Level $newLevel', + data: {'level': newLevel}, + duration: const Duration(seconds: 2), + )); + } + + /// 퀘스트 완료 알림 + void showQuestComplete(String questName) { + show(GameNotification( + type: NotificationType.questComplete, + title: 'QUEST COMPLETE!', + subtitle: questName, + data: {'quest': questName}, + duration: const Duration(seconds: 2), + )); + } + + /// 막 완료 알림 (Act Complete) + void showActComplete(int actNumber) { + show(GameNotification( + type: NotificationType.actComplete, + title: 'ACT $actNumber COMPLETE!', + duration: const Duration(seconds: 3), + )); + } + + /// 새 주문 알림 + void showNewSpell(String spellName) { + show(GameNotification( + type: NotificationType.newSpell, + title: 'NEW SPELL!', + subtitle: spellName, + data: {'spell': spellName}, + duration: const Duration(seconds: 2), + )); + } + + /// 새 장비 알림 + void showNewEquipment(String equipmentName, String slot) { + show(GameNotification( + type: NotificationType.newEquipment, + title: 'NEW EQUIPMENT!', + subtitle: equipmentName, + data: {'equipment': equipmentName, 'slot': slot}, + duration: const Duration(seconds: 2), + )); + } + + /// 보스 처치 알림 + void showBossDefeat(String bossName) { + show(GameNotification( + type: NotificationType.bossDefeat, + title: 'BOSS DEFEATED!', + subtitle: bossName, + data: {'boss': bossName}, + duration: const Duration(seconds: 3), + )); + } + + /// 큐 처리 (Process Queue) + void _processQueue() { + if (_isShowing || _queue.isEmpty) return; + + _isShowing = true; + final notification = _queue.removeAt(0); + _notificationController.add(notification); + + // 지정된 시간 후 자동 닫기 + Future.delayed(notification.duration, () { + _dismissController.add(null); + _isShowing = false; + _processQueue(); // 다음 알림 처리 + }); + } + + /// 현재 알림 즉시 닫기 + void dismiss() { + _dismissController.add(null); + _isShowing = false; + _processQueue(); + } + + /// 서비스 정리 (Dispose) + void dispose() { + _notificationController.close(); + _dismissController.close(); + } +} diff --git a/lib/src/features/game/game_play_screen.dart b/lib/src/features/game/game_play_screen.dart index 4190e32..bf415e9 100644 --- a/lib/src/features/game/game_play_screen.dart +++ b/lib/src/features/game/game_play_screen.dart @@ -4,8 +4,11 @@ import 'package:askiineverdie/l10n/app_localizations.dart'; import 'package:askiineverdie/src/core/animation/ascii_animation_type.dart'; import 'package:askiineverdie/src/core/l10n/game_data_l10n.dart'; import 'package:askiineverdie/src/core/model/game_state.dart'; +import 'package:askiineverdie/src/core/notification/notification_service.dart'; import 'package:askiineverdie/src/core/util/pq_logic.dart' as pq_logic; import 'package:askiineverdie/src/features/game/game_session_controller.dart'; +import 'package:askiineverdie/src/features/game/widgets/notification_overlay.dart'; +import 'package:askiineverdie/src/features/game/widgets/stats_panel.dart'; import 'package:askiineverdie/src/features/game/widgets/task_progress_panel.dart'; /// 게임 진행 화면 (Main.dfm 기반 3패널 레이아웃) @@ -24,6 +27,9 @@ class _GamePlayScreenState extends State with WidgetsBindingObserver { AsciiAnimationType? _specialAnimation; + // Phase 8: 알림 서비스 (Notification Service) + late final NotificationService _notificationService; + // 이전 상태 추적 (레벨업/퀘스트/Act 완료 감지용) int _lastLevel = 0; int _lastQuestCount = 0; @@ -33,6 +39,7 @@ class _GamePlayScreenState extends State // 레벨업 감지 if (state.traits.level > _lastLevel && _lastLevel > 0) { _specialAnimation = AsciiAnimationType.levelUp; + _notificationService.showLevelUp(state.traits.level); _resetSpecialAnimationAfterFrame(); } _lastLevel = state.traits.level; @@ -40,6 +47,13 @@ class _GamePlayScreenState extends State // 퀘스트 완료 감지 if (state.progress.questCount > _lastQuestCount && _lastQuestCount > 0) { _specialAnimation = AsciiAnimationType.questComplete; + // 완료된 퀘스트 이름 가져오기 + final completedQuest = state.progress.questHistory + .where((q) => q.isComplete) + .lastOrNull; + if (completedQuest != null) { + _notificationService.showQuestComplete(completedQuest.caption); + } _resetSpecialAnimationAfterFrame(); } _lastQuestCount = state.progress.questCount; @@ -48,6 +62,7 @@ class _GamePlayScreenState extends State if (state.progress.plotStageCount > _lastPlotStageCount && _lastPlotStageCount > 0) { _specialAnimation = AsciiAnimationType.actComplete; + _notificationService.showActComplete(state.progress.plotStageCount - 1); _resetSpecialAnimationAfterFrame(); } _lastPlotStageCount = state.progress.plotStageCount; @@ -67,6 +82,7 @@ class _GamePlayScreenState extends State @override void initState() { super.initState(); + _notificationService = NotificationService(); widget.controller.addListener(_onControllerChanged); WidgetsBinding.instance.addObserver(this); @@ -81,6 +97,7 @@ class _GamePlayScreenState extends State @override void dispose() { + _notificationService.dispose(); WidgetsBinding.instance.removeObserver(this); widget.controller.removeListener(_onControllerChanged); super.dispose(); @@ -154,19 +171,21 @@ class _GamePlayScreenState extends State return const Scaffold(body: Center(child: CircularProgressIndicator())); } - return PopScope( - canPop: false, - onPopInvokedWithResult: (didPop, result) async { - if (didPop) return; - final shouldPop = await _onPopInvoked(); - if (shouldPop && context.mounted) { - await widget.controller.pause(saveOnStop: false); - if (context.mounted) { - Navigator.of(context).pop(); + return NotificationOverlay( + notificationService: _notificationService, + child: PopScope( + canPop: false, + onPopInvokedWithResult: (didPop, result) async { + if (didPop) return; + final shouldPop = await _onPopInvoked(); + if (shouldPop && context.mounted) { + await widget.controller.pause(saveOnStop: false); + if (context.mounted) { + Navigator.of(context).pop(); + } } - } - }, - child: Scaffold( + }, + child: Scaffold( appBar: AppBar( title: Text(L10n.of(context).progressQuestTitle(state.traits.name)), actions: [ @@ -230,6 +249,7 @@ class _GamePlayScreenState extends State ), ], ), + ), ), ); } @@ -248,9 +268,9 @@ class _GamePlayScreenState extends State _buildSectionHeader(l10n.traits), _buildTraitsList(state), - // Stats 목록 + // Stats 목록 (Phase 8: 애니메이션 변화 표시) _buildSectionHeader(l10n.stats), - Expanded(flex: 2, child: _buildStatsList(state)), + Expanded(flex: 2, child: StatsPanel(stats: state.stats)), // Experience 바 _buildSectionHeader(l10n.experience), @@ -425,40 +445,6 @@ class _GamePlayScreenState extends State ); } - Widget _buildStatsList(GameState state) { - final l10n = L10n.of(context); - final stats = [ - (l10n.statStr, state.stats.str), - (l10n.statCon, state.stats.con), - (l10n.statDex, state.stats.dex), - (l10n.statInt, state.stats.intelligence), - (l10n.statWis, state.stats.wis), - (l10n.statCha, state.stats.cha), - (l10n.statHpMax, state.stats.hpMax), - (l10n.statMpMax, state.stats.mpMax), - ]; - - return ListView.builder( - itemCount: stats.length, - padding: const EdgeInsets.symmetric(horizontal: 8), - itemBuilder: (context, index) { - final stat = stats[index]; - return Row( - children: [ - SizedBox( - width: 50, - child: Text(stat.$1, style: const TextStyle(fontSize: 11)), - ), - Text( - '${stat.$2}', - style: const TextStyle(fontSize: 11, fontWeight: FontWeight.bold), - ), - ], - ); - }, - ); - } - Widget _buildSpellsList(GameState state) { if (state.spellBook.spells.isEmpty) { return Center( diff --git a/lib/src/features/game/widgets/combat_log.dart b/lib/src/features/game/widgets/combat_log.dart new file mode 100644 index 0000000..15a0783 --- /dev/null +++ b/lib/src/features/game/widgets/combat_log.dart @@ -0,0 +1,150 @@ +import 'package:flutter/material.dart'; + +/// 전투 로그 엔트리 (Combat Log Entry) +class CombatLogEntry { + const CombatLogEntry({ + required this.message, + required this.timestamp, + this.type = CombatLogType.normal, + }); + + final String message; + final DateTime timestamp; + final CombatLogType type; +} + +/// 로그 타입에 따른 스타일 구분 +enum CombatLogType { + normal, // 일반 메시지 + damage, // 피해 입힘 + heal, // 회복 + levelUp, // 레벨업 + questComplete, // 퀘스트 완료 + loot, // 전리품 획득 + spell, // 주문 습득 +} + +/// 전투 로그 위젯 (Phase 8: 실시간 전투 이벤트 표시) +/// +/// 최근 전투 이벤트를 스크롤 가능한 리스트로 표시 +class CombatLog extends StatefulWidget { + const CombatLog({super.key, required this.entries, this.maxEntries = 50}); + + final List entries; + final int maxEntries; + + @override + State createState() => _CombatLogState(); +} + +class _CombatLogState extends State { + final ScrollController _scrollController = ScrollController(); + int _previousLength = 0; + + @override + void didUpdateWidget(CombatLog oldWidget) { + super.didUpdateWidget(oldWidget); + + // 새 로그 추가 시 자동 스크롤 + if (widget.entries.length > _previousLength) { + _scrollToBottom(); + } + _previousLength = widget.entries.length; + } + + void _scrollToBottom() { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_scrollController.hasClients) { + _scrollController.animateTo( + _scrollController.position.maxScrollExtent, + duration: const Duration(milliseconds: 200), + curve: Curves.easeOut, + ); + } + }); + } + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return ListView.builder( + controller: _scrollController, + itemCount: widget.entries.length, + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + itemBuilder: (context, index) { + final entry = widget.entries[index]; + return _LogEntryTile(entry: entry); + }, + ); + } +} + +/// 개별 로그 엔트리 타일 +class _LogEntryTile extends StatelessWidget { + const _LogEntryTile({required this.entry}); + + final CombatLogEntry entry; + + @override + Widget build(BuildContext context) { + final (color, icon) = _getStyleForType(entry.type); + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 1), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 타임스탬프(timestamp) + Text( + _formatTime(entry.timestamp), + style: TextStyle( + fontSize: 10, + color: Theme.of(context).colorScheme.outline, + fontFamily: 'monospace', + ), + ), + const SizedBox(width: 4), + // 아이콘 + if (icon != null) + Padding( + padding: const EdgeInsets.only(right: 4), + child: Icon(icon, size: 12, color: color), + ), + // 메시지 + Expanded( + child: Text( + entry.message, + style: TextStyle( + fontSize: 11, + color: color ?? Theme.of(context).colorScheme.onSurface, + ), + ), + ), + ], + ), + ); + } + + String _formatTime(DateTime time) { + return '${time.hour.toString().padLeft(2, '0')}:' + '${time.minute.toString().padLeft(2, '0')}:' + '${time.second.toString().padLeft(2, '0')}'; + } + + (Color?, IconData?) _getStyleForType(CombatLogType type) { + return switch (type) { + CombatLogType.normal => (null, null), + CombatLogType.damage => (Colors.red.shade300, Icons.local_fire_department), + CombatLogType.heal => (Colors.green.shade300, Icons.healing), + CombatLogType.levelUp => (Colors.amber, Icons.arrow_upward), + CombatLogType.questComplete => (Colors.blue.shade300, Icons.check_circle), + CombatLogType.loot => (Colors.orange.shade300, Icons.inventory_2), + CombatLogType.spell => (Colors.purple.shade300, Icons.auto_fix_high), + }; + } +} diff --git a/lib/src/features/game/widgets/notification_overlay.dart b/lib/src/features/game/widgets/notification_overlay.dart new file mode 100644 index 0000000..c43c35b --- /dev/null +++ b/lib/src/features/game/widgets/notification_overlay.dart @@ -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 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( + 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, + ), + }; + } +} diff --git a/lib/src/features/game/widgets/stats_panel.dart b/lib/src/features/game/widgets/stats_panel.dart new file mode 100644 index 0000000..c97996b --- /dev/null +++ b/lib/src/features/game/widgets/stats_panel.dart @@ -0,0 +1,209 @@ +import 'package:flutter/material.dart'; + +import 'package:askiineverdie/l10n/app_localizations.dart'; +import 'package:askiineverdie/src/core/model/game_state.dart'; + +/// 스탯 표시 패널 (Phase 8: 실시간 변화 표시) +/// +/// 장비 변경이나 버프 시 스탯 변화량을 애니메이션으로 표시 +class StatsPanel extends StatefulWidget { + const StatsPanel({super.key, required this.stats}); + + final Stats stats; + + @override + State createState() => _StatsPanelState(); +} + +class _StatsPanelState extends State + with SingleTickerProviderStateMixin { + // 변화량 맵 (스탯 이름 -> 변화량) + final Map _statChanges = {}; + + // 변화 애니메이션 컨트롤러 + late AnimationController _animationController; + late Animation _fadeAnimation; + + @override + void initState() { + super.initState(); + + _animationController = AnimationController( + duration: const Duration(milliseconds: 2000), + vsync: this, + ); + + _fadeAnimation = Tween(begin: 1.0, end: 0.0).animate( + CurvedAnimation( + parent: _animationController, + curve: const Interval(0.5, 1.0, curve: Curves.easeOut), + ), + ); + } + + @override + void didUpdateWidget(StatsPanel oldWidget) { + super.didUpdateWidget(oldWidget); + + // 스탯 변화 감지 + _detectChanges(oldWidget.stats, widget.stats); + } + + void _detectChanges(Stats oldStats, Stats newStats) { + final changes = {}; + + if (newStats.str != oldStats.str) { + changes['str'] = newStats.str - oldStats.str; + } + if (newStats.con != oldStats.con) { + changes['con'] = newStats.con - oldStats.con; + } + if (newStats.dex != oldStats.dex) { + changes['dex'] = newStats.dex - oldStats.dex; + } + if (newStats.intelligence != oldStats.intelligence) { + changes['int'] = newStats.intelligence - oldStats.intelligence; + } + if (newStats.wis != oldStats.wis) { + changes['wis'] = newStats.wis - oldStats.wis; + } + if (newStats.cha != oldStats.cha) { + changes['cha'] = newStats.cha - oldStats.cha; + } + if (newStats.hpMax != oldStats.hpMax) { + changes['hpMax'] = newStats.hpMax - oldStats.hpMax; + } + if (newStats.mpMax != oldStats.mpMax) { + changes['mpMax'] = newStats.mpMax - oldStats.mpMax; + } + + if (changes.isNotEmpty) { + setState(() { + _statChanges + ..clear() + ..addAll(changes); + }); + + // 애니메이션 재시작 + _animationController + ..reset() + ..forward().then((_) { + if (mounted) { + setState(() { + _statChanges.clear(); + }); + } + }); + } + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final l10n = L10n.of(context); + final stats = [ + ('str', l10n.statStr, widget.stats.str), + ('con', l10n.statCon, widget.stats.con), + ('dex', l10n.statDex, widget.stats.dex), + ('int', l10n.statInt, widget.stats.intelligence), + ('wis', l10n.statWis, widget.stats.wis), + ('cha', l10n.statCha, widget.stats.cha), + ('hpMax', l10n.statHpMax, widget.stats.hpMax), + ('mpMax', l10n.statMpMax, widget.stats.mpMax), + ]; + + return ListView.builder( + itemCount: stats.length, + padding: const EdgeInsets.symmetric(horizontal: 8), + itemBuilder: (context, index) { + final stat = stats[index]; + final change = _statChanges[stat.$1]; + + return _StatRow( + label: stat.$2, + value: stat.$3, + change: change, + fadeAnimation: _fadeAnimation, + ); + }, + ); + } +} + +/// 개별 스탯 행 위젯 +class _StatRow extends StatelessWidget { + const _StatRow({ + required this.label, + required this.value, + this.change, + required this.fadeAnimation, + }); + + final String label; + final int value; + final int? change; + final Animation fadeAnimation; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + SizedBox( + width: 50, + child: Text(label, style: const TextStyle(fontSize: 11)), + ), + Text( + '$value', + style: const TextStyle(fontSize: 11, fontWeight: FontWeight.bold), + ), + if (change != null) ...[ + const SizedBox(width: 4), + AnimatedBuilder( + animation: fadeAnimation, + builder: (context, child) { + return Opacity( + opacity: fadeAnimation.value, + child: _ChangeIndicator(change: change!), + ); + }, + ), + ], + ], + ); + } +} + +/// 변화량 표시 위젯 +class _ChangeIndicator extends StatelessWidget { + const _ChangeIndicator({required this.change}); + + final int change; + + @override + Widget build(BuildContext context) { + final isPositive = change > 0; + final color = isPositive ? Colors.green : Colors.red; + final icon = isPositive ? Icons.arrow_upward : Icons.arrow_downward; + final text = isPositive ? '+$change' : '$change'; + + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, size: 10, color: color), + Text( + text, + style: TextStyle( + fontSize: 10, + color: color, + fontWeight: FontWeight.bold, + ), + ), + ], + ); + } +}