From 2486d84d6389e54e94cf98dcced63360e7e81960 Mon Sep 17 00:00:00 2001 From: JiWoong Sul Date: Tue, 30 Dec 2025 18:31:08 +0900 Subject: [PATCH] =?UTF-8?q?feat(ui):=20=ED=99=94=EB=A9=B4=EB=93=A4?= =?UTF-8?q?=EC=97=90=20=EB=A0=88=ED=8A=B8=EB=A1=9C=20UI=20=EC=8A=A4?= =?UTF-8?q?=ED=83=80=EC=9D=BC=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - front_screen: 레트로 패널 및 버튼 스타일 - game_play_screen: 레트로 색상 및 초기 BGM 로직 개선 - mobile_carousel_layout: 레트로 테마 적용 - carousel_nav_bar: 골드 액센트 색상 적용 --- lib/src/features/front/front_screen.dart | 327 ++++++++---------- lib/src/features/game/game_play_screen.dart | 57 ++- .../game/layouts/mobile_carousel_layout.dart | 34 +- .../game/widgets/carousel_nav_bar.dart | 46 +-- 4 files changed, 249 insertions(+), 215 deletions(-) diff --git a/lib/src/features/front/front_screen.dart b/lib/src/features/front/front_screen.dart index bc960f1..519ec12 100644 --- a/lib/src/features/front/front_screen.dart +++ b/lib/src/features/front/front_screen.dart @@ -3,6 +3,8 @@ import 'package:flutter/material.dart'; import 'package:askiineverdie/data/game_text_l10n.dart' as game_l10n; import 'package:askiineverdie/l10n/app_localizations.dart'; import 'package:askiineverdie/src/features/front/widgets/hero_vs_boss_animation.dart'; +import 'package:askiineverdie/src/shared/retro_colors.dart'; +import 'package:askiineverdie/src/shared/widgets/retro_widgets.dart'; class FrontScreen extends StatelessWidget { const FrontScreen({ @@ -60,142 +62,120 @@ class FrontScreen extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = Theme.of(context); - final colorScheme = theme.colorScheme; - return Scaffold( - body: Container( - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [colorScheme.surfaceContainerHighest, colorScheme.surface], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - ), - child: SafeArea( - child: Column( - children: [ - // 스크롤 영역 (헤더, 애니메이션, 버튼) - Expanded( - child: Center( - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 960), - child: SingleChildScrollView( - padding: const EdgeInsets.all(24), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - _HeroHeader(theme: theme, colorScheme: colorScheme), - const SizedBox(height: 20), - const HeroVsBossAnimation(), - const SizedBox(height: 24), - _ActionButtons( - onNewCharacter: onNewCharacter != null - ? () => _handleNewCharacter(context) - : null, - onLoadSave: onLoadSave != null - ? () => onLoadSave!(context) - : null, - onHallOfFame: onHallOfFame != null - ? () => onHallOfFame!(context) - : null, - ), - ], - ), + backgroundColor: RetroColors.deepBrown, + body: SafeArea( + child: Column( + children: [ + // 스크롤 영역 (헤더, 애니메이션, 버튼) + Expanded( + child: Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 800), + child: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const _RetroHeader(), + const SizedBox(height: 16), + const _AnimationPanel(), + const SizedBox(height: 16), + _ActionButtons( + onNewCharacter: onNewCharacter != null + ? () => _handleNewCharacter(context) + : null, + onLoadSave: onLoadSave != null + ? () => onLoadSave!(context) + : null, + onHallOfFame: onHallOfFame != null + ? () => onHallOfFame!(context) + : null, + ), + ], ), ), ), ), - // 카피라이트 푸터 (하단 고정) - const _CopyrightFooter(), - ], - ), + ), + // 카피라이트 푸터 (하단 고정) + const _CopyrightFooter(), + ], ), ), ); } } -/// 헤더 (타이틀 + 태그) - 중앙 정렬 -class _HeroHeader extends StatelessWidget { - const _HeroHeader({required this.theme, required this.colorScheme}); - - final ThemeData theme; - final ColorScheme colorScheme; +/// 레트로 스타일 헤더 (타이틀 + 태그) +class _RetroHeader extends StatelessWidget { + const _RetroHeader(); @override Widget build(BuildContext context) { - return DecoratedBox( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(18), - gradient: LinearGradient( - colors: [ - colorScheme.primary.withValues(alpha: 0.9), - colorScheme.primaryContainer, - ], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - boxShadow: [ - BoxShadow( - color: colorScheme.primary.withValues(alpha: 0.18), - blurRadius: 18, - offset: const Offset(0, 10), - ), - ], - ), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 24), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - // 타이틀 (중앙 정렬) - Row( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - Icon(Icons.auto_awesome, color: colorScheme.onPrimary), - const SizedBox(width: 10), - Text( - L10n.of(context).appTitle, - style: theme.textTheme.headlineSmall?.copyWith( - color: colorScheme.onPrimary, - fontWeight: FontWeight.w700, - ), - ), - ], - ), - const SizedBox(height: 14), - // 태그 (중앙 정렬) - Builder( - builder: (context) { - final l10n = L10n.of(context); - return Wrap( - alignment: WrapAlignment.center, - spacing: 8, - runSpacing: 8, - children: [ - _Tag( - icon: Icons.cloud_off_outlined, - label: l10n.tagNoNetwork, - ), - _Tag(icon: Icons.timer_outlined, label: l10n.tagIdleRpg), - _Tag( - icon: Icons.storage_rounded, - label: l10n.tagLocalSaves, + final l10n = L10n.of(context); + return RetroGoldPanel( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20), + child: Column( + children: [ + // 타이틀 (픽셀 폰트) + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.auto_awesome, color: RetroColors.gold, size: 20), + const SizedBox(width: 12), + Text( + l10n.appTitle, + style: const TextStyle( + fontFamily: 'PressStart2P', + fontSize: 14, + color: RetroColors.gold, + shadows: [ + Shadow( + color: RetroColors.goldDark, + offset: Offset(2, 2), ), ], - ); - }, - ), - ], - ), + ), + ), + ], + ), + const SizedBox(height: 16), + // 태그 (레트로 스타일) + Wrap( + alignment: WrapAlignment.center, + spacing: 8, + runSpacing: 8, + children: [ + _RetroTag(icon: Icons.cloud_off_outlined, label: l10n.tagNoNetwork), + _RetroTag(icon: Icons.timer_outlined, label: l10n.tagIdleRpg), + _RetroTag(icon: Icons.storage_rounded, label: l10n.tagLocalSaves), + ], + ), + ], ), ); } } -/// 액션 버튼 (세로 배치) +/// 애니메이션 패널 +class _AnimationPanel extends StatelessWidget { + const _AnimationPanel(); + + @override + Widget build(BuildContext context) { + return RetroPanel( + title: 'BATTLE', + padding: const EdgeInsets.all(8), + child: const AspectRatio( + aspectRatio: 16 / 9, + child: HeroVsBossAnimation(), + ), + ); + } +} + +/// 액션 버튼 (레트로 스타일) class _ActionButtons extends StatelessWidget { const _ActionButtons({ this.onNewCharacter, @@ -209,48 +189,39 @@ class _ActionButtons extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = Theme.of(context); final l10n = L10n.of(context); - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - // 새 캐릭터 (Primary) - FilledButton.icon( - onPressed: onNewCharacter, - icon: const Icon(Icons.casino_outlined), - label: Text(l10n.newCharacter), - style: FilledButton.styleFrom( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), - textStyle: theme.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w600, + return RetroPanel( + title: 'MENU', + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // 새 캐릭터 (Primary) + RetroTextButton( + text: l10n.newCharacter, + icon: Icons.casino_outlined, + onPressed: onNewCharacter, + ), + const SizedBox(height: 12), + // 불러오기 (Secondary) + RetroTextButton( + text: l10n.loadSave, + icon: Icons.folder_open, + onPressed: onLoadSave, + isPrimary: false, + ), + const SizedBox(height: 12), + // 명예의 전당 + if (onHallOfFame != null) + RetroTextButton( + text: game_l10n.uiHallOfFame, + icon: Icons.emoji_events_outlined, + onPressed: onHallOfFame, + isPrimary: false, ), - ), - ), - const SizedBox(height: 12), - // 불러오기 (Secondary) - OutlinedButton.icon( - onPressed: onLoadSave, - icon: const Icon(Icons.folder_open), - label: Text(l10n.loadSave), - style: OutlinedButton.styleFrom( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), - textStyle: theme.textTheme.titleMedium, - ), - ), - const SizedBox(height: 12), - // 명예의 전당 (Tertiary) - if (onHallOfFame != null) - TextButton.icon( - onPressed: onHallOfFame, - icon: const Icon(Icons.emoji_events_outlined), - label: Text(game_l10n.uiHallOfFame), - style: TextButton.styleFrom( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14), - textStyle: theme.textTheme.titleMedium, - ), - ), - ], + ], + ), ); } } @@ -261,43 +232,51 @@ class _CopyrightFooter extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = Theme.of(context); return Padding( - padding: const EdgeInsets.symmetric(vertical: 16), + padding: const EdgeInsets.symmetric(vertical: 12), child: Text( game_l10n.copyrightText, textAlign: TextAlign.center, - style: theme.textTheme.bodySmall?.copyWith( - color: theme.colorScheme.onSurface.withValues(alpha: 0.5), + style: const TextStyle( + fontFamily: 'PressStart2P', + fontSize: 6, + color: RetroColors.textDisabled, ), ), ); } } -/// 태그 칩 -class _Tag extends StatelessWidget { - const _Tag({required this.icon, required this.label}); +/// 레트로 태그 칩 +class _RetroTag extends StatelessWidget { + const _RetroTag({required this.icon, required this.label}); final IconData icon; final String label; @override Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - // 어두운 배경에 잘 보이도록 대비되는 색상 사용 - final tagColor = colorScheme.onPrimaryContainer; - return Chip( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), - backgroundColor: colorScheme.primaryContainer.withValues(alpha: 0.8), - avatar: Icon(icon, color: tagColor, size: 16), - label: Text( - label, - style: TextStyle(color: tagColor, fontWeight: FontWeight.w600), + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), + decoration: BoxDecoration( + color: RetroColors.panelBgLight, + border: Border.all(color: RetroColors.panelBorderInner, width: 1), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, color: RetroColors.gold, size: 12), + const SizedBox(width: 6), + Text( + label, + style: const TextStyle( + fontFamily: 'PressStart2P', + fontSize: 7, + color: RetroColors.textLight, + ), + ), + ], ), - side: BorderSide.none, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - visualDensity: VisualDensity.compact, ); } } diff --git a/lib/src/features/game/game_play_screen.dart b/lib/src/features/game/game_play_screen.dart index c8c8a63..783849f 100644 --- a/lib/src/features/game/game_play_screen.dart +++ b/lib/src/features/game/game_play_screen.dart @@ -36,6 +36,7 @@ import 'package:askiineverdie/src/features/game/widgets/statistics_dialog.dart'; import 'package:askiineverdie/src/features/game/widgets/help_dialog.dart'; import 'package:askiineverdie/src/core/storage/settings_repository.dart'; import 'package:askiineverdie/src/core/audio/audio_service.dart'; +import 'package:askiineverdie/src/shared/retro_colors.dart'; /// 게임 진행 화면 (Main.dfm 기반 3패널 레이아웃) /// @@ -829,7 +830,7 @@ class _GamePlayScreenState extends State ); } - // 기존 데스크톱 레이아웃 + // 기존 데스크톱 레이아웃 (레트로 스타일) return NotificationOverlay( key: localeKey, notificationService: _notificationService, @@ -846,8 +847,17 @@ class _GamePlayScreenState extends State } }, child: Scaffold( + backgroundColor: RetroColors.deepBrown, appBar: AppBar( - title: Text(L10n.of(context).progressQuestTitle(state.traits.name)), + backgroundColor: RetroColors.darkBrown, + title: Text( + L10n.of(context).progressQuestTitle(state.traits.name), + style: const TextStyle( + fontFamily: 'PressStart2P', + fontSize: 12, + color: RetroColors.gold, + ), + ), actions: [ // 치트 버튼 (디버그용) if (widget.controller.cheatsEnabled) ...[ @@ -955,6 +965,11 @@ class _GamePlayScreenState extends State final l10n = L10n.of(context); return Card( margin: const EdgeInsets.all(4), + color: RetroColors.panelBg, + shape: RoundedRectangleBorder( + side: const BorderSide(color: RetroColors.panelBorderOuter, width: 2), + borderRadius: BorderRadius.circular(0), + ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ @@ -1023,6 +1038,11 @@ class _GamePlayScreenState extends State final l10n = L10n.of(context); return Card( margin: const EdgeInsets.all(4), + color: RetroColors.panelBg, + shape: RoundedRectangleBorder( + side: const BorderSide(color: RetroColors.panelBorderOuter, width: 2), + borderRadius: BorderRadius.circular(0), + ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ @@ -1069,6 +1089,11 @@ class _GamePlayScreenState extends State final l10n = L10n.of(context); return Card( margin: const EdgeInsets.all(4), + color: RetroColors.panelBg, + shape: RoundedRectangleBorder( + side: const BorderSide(color: RetroColors.panelBorderOuter, width: 2), + borderRadius: BorderRadius.circular(0), + ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ @@ -1112,13 +1137,20 @@ class _GamePlayScreenState extends State Widget _buildPanelHeader(String title) { return Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - color: Theme.of(context).colorScheme.primaryContainer, + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), + decoration: const BoxDecoration( + color: RetroColors.darkBrown, + border: Border( + bottom: BorderSide(color: RetroColors.gold, width: 2), + ), + ), child: Text( - title, - style: TextStyle( + title.toUpperCase(), + style: const TextStyle( + fontFamily: 'PressStart2P', + fontSize: 8, fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.onPrimaryContainer, + color: RetroColors.gold, ), ), ); @@ -1126,8 +1158,15 @@ class _GamePlayScreenState extends State Widget _buildSectionHeader(String title) { return Padding( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), - child: Text(title, style: Theme.of(context).textTheme.labelSmall), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + child: Text( + title.toUpperCase(), + style: const TextStyle( + fontFamily: 'PressStart2P', + fontSize: 6, + color: RetroColors.textDisabled, + ), + ), ); } diff --git a/lib/src/features/game/layouts/mobile_carousel_layout.dart b/lib/src/features/game/layouts/mobile_carousel_layout.dart index 784950b..fbfd380 100644 --- a/lib/src/features/game/layouts/mobile_carousel_layout.dart +++ b/lib/src/features/game/layouts/mobile_carousel_layout.dart @@ -15,6 +15,7 @@ import 'package:askiineverdie/src/features/game/pages/story_page.dart'; import 'package:askiineverdie/src/features/game/widgets/carousel_nav_bar.dart'; import 'package:askiineverdie/src/features/game/widgets/combat_log.dart'; import 'package:askiineverdie/src/features/game/widgets/enhanced_animation_panel.dart'; +import 'package:askiineverdie/src/shared/retro_colors.dart'; /// 모바일 캐로셀 레이아웃 /// @@ -358,6 +359,7 @@ class _MobileCarouselLayoutState extends State { showModalBottomSheet( context: context, isScrollControlled: true, + backgroundColor: RetroColors.panelBg, constraints: BoxConstraints( maxHeight: MediaQuery.of(context).size.height * 0.7, ), @@ -366,19 +368,22 @@ class _MobileCarouselLayoutState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ - // 헤더 + // 헤더 (레트로 스타일) Container( padding: const EdgeInsets.all(16), width: double.infinity, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primaryContainer, + decoration: const BoxDecoration( + color: RetroColors.darkBrown, + border: Border( + bottom: BorderSide(color: RetroColors.gold, width: 2), + ), ), - child: Text( - l10n.menuOptions, + child: const Text( + 'OPTIONS', style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - color: Theme.of(context).colorScheme.onPrimaryContainer, + fontFamily: 'PressStart2P', + fontSize: 12, + color: RetroColors.gold, ), ), ), @@ -555,12 +560,21 @@ class _MobileCarouselLayoutState extends State { final state = widget.state; return Scaffold( + backgroundColor: RetroColors.deepBrown, appBar: AppBar( - title: Text(L10n.of(context).progressQuestTitle(state.traits.name)), + backgroundColor: RetroColors.darkBrown, + title: Text( + L10n.of(context).progressQuestTitle(state.traits.name), + style: const TextStyle( + fontFamily: 'PressStart2P', + fontSize: 10, + color: RetroColors.gold, + ), + ), actions: [ // 옵션 버튼 IconButton( - icon: const Icon(Icons.settings), + icon: const Icon(Icons.settings, color: RetroColors.gold), onPressed: () => _showOptionsMenu(context), tooltip: l10n.menuOptions, ), diff --git a/lib/src/features/game/widgets/carousel_nav_bar.dart b/lib/src/features/game/widgets/carousel_nav_bar.dart index 39a2cf8..88e3a62 100644 --- a/lib/src/features/game/widgets/carousel_nav_bar.dart +++ b/lib/src/features/game/widgets/carousel_nav_bar.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:askiineverdie/data/game_text_l10n.dart' as l10n; +import 'package:askiineverdie/src/shared/retro_colors.dart'; /// 캐로셀 페이지 인덱스 enum CarouselPage { @@ -13,7 +14,7 @@ enum CarouselPage { story, // 6: 스토리 } -/// 캐로셀 네비게이션 바 +/// 캐로셀 네비게이션 바 (레트로 스타일) /// /// 7개의 페이지 버튼을 표시하고 현재 페이지를 하이라이트. /// 버튼 탭 시 해당 페이지로 이동. @@ -31,10 +32,12 @@ class CarouselNavBar extends StatelessWidget { Widget build(BuildContext context) { return Container( height: 56, - padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceContainerHighest, - border: Border(top: BorderSide(color: Theme.of(context).dividerColor)), + padding: const EdgeInsets.symmetric(horizontal: 2, vertical: 4), + decoration: const BoxDecoration( + color: RetroColors.darkBrown, + border: Border( + top: BorderSide(color: RetroColors.gold, width: 2), + ), ), child: Row( children: CarouselPage.values.map((page) { @@ -52,7 +55,7 @@ class CarouselNavBar extends StatelessWidget { } } -/// 개별 네비게이션 버튼 +/// 개별 네비게이션 버튼 (레트로 스타일) class _NavButton extends StatelessWidget { const _NavButton({ required this.page, @@ -67,33 +70,32 @@ class _NavButton extends StatelessWidget { @override Widget build(BuildContext context) { final (icon, label) = _getIconAndLabel(page); - final theme = Theme.of(context); - final color = isSelected - ? theme.colorScheme.primary - : theme.colorScheme.onSurfaceVariant; + final color = isSelected ? RetroColors.gold : RetroColors.textDisabled; + final bgColor = isSelected + ? RetroColors.panelBgLight + : Colors.transparent; - return InkWell( + return GestureDetector( onTap: onTap, - borderRadius: BorderRadius.circular(8), child: Container( + margin: const EdgeInsets.symmetric(horizontal: 1), padding: const EdgeInsets.symmetric(vertical: 4), - decoration: isSelected - ? BoxDecoration( - color: theme.colorScheme.primaryContainer.withValues( - alpha: 0.5, - ), - borderRadius: BorderRadius.circular(8), - ) - : null, + decoration: BoxDecoration( + color: bgColor, + border: isSelected + ? Border.all(color: RetroColors.gold, width: 1) + : null, + ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(icon, size: 20, color: color), + Icon(icon, size: 18, color: color), const SizedBox(height: 2), Text( label, style: TextStyle( - fontSize: 9, + fontFamily: 'PressStart2P', + fontSize: 5, fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, color: color, ),