feat(front): 프론트 화면 개선 및 설정 저장소 추가
- front_screen_animation.dart: 프론트 화면 애니메이션 추가 - settings_repository.dart: 설정 저장소 구현 - front/widgets/: 프론트 화면 위젯 분리 - mobile_carousel_layout.dart: 모바일 레이아웃 개선 - app.dart: 앱 설정 개선 - game_text_l10n.dart: 텍스트 추가
This commit is contained in:
@@ -2,6 +2,7 @@ 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';
|
||||
|
||||
class FrontScreen extends StatelessWidget {
|
||||
const FrontScreen({
|
||||
@@ -9,6 +10,7 @@ class FrontScreen extends StatelessWidget {
|
||||
this.onNewCharacter,
|
||||
this.onLoadSave,
|
||||
this.onHallOfFame,
|
||||
this.hasSaveFile = false,
|
||||
});
|
||||
|
||||
/// "New character" 버튼 클릭 시 호출
|
||||
@@ -17,9 +19,45 @@ class FrontScreen extends StatelessWidget {
|
||||
/// "Load save" 버튼 클릭 시 호출
|
||||
final Future<void> Function(BuildContext context)? onLoadSave;
|
||||
|
||||
/// "Hall of Fame" 버튼 클릭 시 호출 (Phase 10)
|
||||
/// "Hall of Fame" 버튼 클릭 시 호출
|
||||
final void Function(BuildContext context)? onHallOfFame;
|
||||
|
||||
/// 세이브 파일 존재 여부 (새 캐릭터 시 경고용)
|
||||
final bool hasSaveFile;
|
||||
|
||||
/// 새 캐릭터 생성 시 세이브 파일 존재하면 경고 표시
|
||||
void _handleNewCharacter(BuildContext context) {
|
||||
if (hasSaveFile) {
|
||||
_showDeleteWarningDialog(context);
|
||||
} else {
|
||||
onNewCharacter?.call(context);
|
||||
}
|
||||
}
|
||||
|
||||
/// 세이브 삭제 경고 다이얼로그
|
||||
void _showDeleteWarningDialog(BuildContext context) {
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: Text(game_l10n.uiWarning),
|
||||
content: Text(game_l10n.warningDeleteSave),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(dialogContext),
|
||||
child: Text(game_l10n.buttonCancel),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(dialogContext);
|
||||
onNewCharacter?.call(context);
|
||||
},
|
||||
child: Text(game_l10n.buttonConfirm),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
@@ -35,33 +73,42 @@ class FrontScreen extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
child: SafeArea(
|
||||
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: 24),
|
||||
_ActionRow(
|
||||
onNewCharacter: onNewCharacter != null
|
||||
? () => onNewCharacter!(context)
|
||||
: () => _showPlaceholder(context),
|
||||
onLoadSave: onLoadSave != null
|
||||
? () => onLoadSave!(context)
|
||||
: () => _showPlaceholder(context),
|
||||
onHallOfFame: onHallOfFame != null
|
||||
? () => onHallOfFame!(context)
|
||||
: null,
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const _StatusCards(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// 카피라이트 푸터 (하단 고정)
|
||||
const _CopyrightFooter(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -69,16 +116,7 @@ class FrontScreen extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
void _showPlaceholder(BuildContext context) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text(
|
||||
'Core gameplay loop is coming next. See doc/progress-quest-flutter-plan.md for milestones.',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 헤더 (타이틀 + 태그) - 중앙 정렬
|
||||
class _HeroHeader extends StatelessWidget {
|
||||
const _HeroHeader({required this.theme, required this.colorScheme});
|
||||
|
||||
@@ -109,41 +147,31 @@ class _HeroHeader extends StatelessWidget {
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
// 타이틀 (중앙 정렬)
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.auto_awesome, color: colorScheme.onPrimary),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
L10n.of(context).appTitle,
|
||||
style: theme.textTheme.headlineSmall?.copyWith(
|
||||
color: colorScheme.onPrimary,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
game_l10n.frontDescription,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
color: colorScheme.onPrimary.withValues(alpha: 0.9),
|
||||
),
|
||||
),
|
||||
],
|
||||
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: [
|
||||
@@ -167,15 +195,16 @@ class _HeroHeader extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _ActionRow extends StatelessWidget {
|
||||
const _ActionRow({
|
||||
required this.onNewCharacter,
|
||||
required this.onLoadSave,
|
||||
/// 액션 버튼 (세로 배치)
|
||||
class _ActionButtons extends StatelessWidget {
|
||||
const _ActionButtons({
|
||||
this.onNewCharacter,
|
||||
this.onLoadSave,
|
||||
this.onHallOfFame,
|
||||
});
|
||||
|
||||
final VoidCallback onNewCharacter;
|
||||
final VoidCallback onLoadSave;
|
||||
final VoidCallback? onNewCharacter;
|
||||
final VoidCallback? onLoadSave;
|
||||
final VoidCallback? onHallOfFame;
|
||||
|
||||
@override
|
||||
@@ -183,148 +212,70 @@ class _ActionRow extends StatelessWidget {
|
||||
final theme = Theme.of(context);
|
||||
final l10n = L10n.of(context);
|
||||
|
||||
return Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 12,
|
||||
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: 18, vertical: 14),
|
||||
textStyle: theme.textTheme.titleMedium,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||
textStyle: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
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: 18, vertical: 14),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||
textStyle: theme.textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
TextButton.icon(
|
||||
onPressed: () => _showPlaceholder(context),
|
||||
icon: const Icon(Icons.menu_book_outlined),
|
||||
label: Text(l10n.viewBuildPlan),
|
||||
),
|
||||
// Phase 10: 명예의 전당 버튼
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _StatusCards extends StatelessWidget {
|
||||
const _StatusCards();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = L10n.of(context);
|
||||
return Column(
|
||||
children: [
|
||||
_InfoCard(
|
||||
icon: Icons.route_outlined,
|
||||
title: l10n.buildRoadmap,
|
||||
points: [
|
||||
'Port PQ 6.4 data set (Config.dfm) into Dart constants.',
|
||||
'Recreate quest/task loop with deterministic RNG + saves.',
|
||||
'Deliver offline-first storage (GZip JSON) across platforms.',
|
||||
],
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
_InfoCard(
|
||||
icon: Icons.auto_fix_high_outlined,
|
||||
title: l10n.techStack,
|
||||
points: [
|
||||
'Flutter (Material 3) with multiplatform targets enabled.',
|
||||
'path_provider + shared_preferences for local storage hooks.',
|
||||
'Strict lints with package imports enforced from day one.',
|
||||
],
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
_InfoCard(
|
||||
icon: Icons.checklist_rtl,
|
||||
title: game_l10n.frontTodayFocus,
|
||||
points: [
|
||||
'Set up scaffold + lints.',
|
||||
'Wire seed theme and initial navigation shell.',
|
||||
'Keep reference assets under example/pq for parity.',
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _InfoCard extends StatelessWidget {
|
||||
const _InfoCard({required this.title, required this.points, this.icon});
|
||||
|
||||
final String title;
|
||||
final List<String> points;
|
||||
final IconData? icon;
|
||||
/// 카피라이트 푸터
|
||||
class _CopyrightFooter extends StatelessWidget {
|
||||
const _CopyrightFooter();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
|
||||
return Card(
|
||||
elevation: 3,
|
||||
shadowColor: colorScheme.shadow.withValues(alpha: 0.2),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(18),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
if (icon != null) ...[
|
||||
Icon(icon, color: colorScheme.primary),
|
||||
const SizedBox(width: 10),
|
||||
],
|
||||
Text(
|
||||
title,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
...points.map(
|
||||
(point) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(top: 3),
|
||||
child: Icon(Icons.check_circle_outline, size: 18),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(point, style: theme.textTheme.bodyMedium),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
child: Text(
|
||||
game_l10n.copyrightText,
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurface.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 태그 칩
|
||||
class _Tag extends StatelessWidget {
|
||||
const _Tag({required this.icon, required this.label});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user