feat: 초기 커밋
- Progress Quest 6.4 Flutter 포팅 프로젝트 - 게임 루프, 상태 관리, UI 구현 - 캐릭터 생성, 인벤토리, 장비, 주문 시스템 - 시장/판매/구매 메커니즘
This commit is contained in:
314
lib/src/features/front/front_screen.dart
Normal file
314
lib/src/features/front/front_screen.dart
Normal file
@@ -0,0 +1,314 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class FrontScreen extends StatelessWidget {
|
||||
const FrontScreen({super.key, this.onNewCharacter, this.onLoadSave});
|
||||
|
||||
/// "New character" 버튼 클릭 시 호출
|
||||
final void Function(BuildContext context)? onNewCharacter;
|
||||
|
||||
/// "Load save" 버튼 클릭 시 호출
|
||||
final Future<void> Function(BuildContext context)? onLoadSave;
|
||||
|
||||
@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: 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),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const _StatusCards(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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});
|
||||
|
||||
final ThemeData theme;
|
||||
final ColorScheme colorScheme;
|
||||
|
||||
@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.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(Icons.auto_awesome, color: colorScheme.onPrimary),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Ascii Never Die',
|
||||
style: theme.textTheme.headlineSmall?.copyWith(
|
||||
color: colorScheme.onPrimary,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'Offline Progress Quest (PQ 6.4) rebuilt with Flutter.',
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
color: colorScheme.onPrimary.withValues(alpha: 0.9),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: const [
|
||||
_Tag(icon: Icons.cloud_off_outlined, label: 'No network'),
|
||||
_Tag(icon: Icons.timer_outlined, label: 'Idle RPG loop'),
|
||||
_Tag(icon: Icons.storage_rounded, label: 'Local saves'),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ActionRow extends StatelessWidget {
|
||||
const _ActionRow({required this.onNewCharacter, required this.onLoadSave});
|
||||
|
||||
final VoidCallback onNewCharacter;
|
||||
final VoidCallback onLoadSave;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 12,
|
||||
children: [
|
||||
FilledButton.icon(
|
||||
onPressed: onNewCharacter,
|
||||
icon: const Icon(Icons.casino_outlined),
|
||||
label: const Text('New character'),
|
||||
style: FilledButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14),
|
||||
textStyle: theme.textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
OutlinedButton.icon(
|
||||
onPressed: onLoadSave,
|
||||
icon: const Icon(Icons.folder_open),
|
||||
label: const Text('Load save'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14),
|
||||
textStyle: theme.textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
TextButton.icon(
|
||||
onPressed: () => _showPlaceholder(context),
|
||||
icon: const Icon(Icons.menu_book_outlined),
|
||||
label: const Text('View build plan'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _StatusCards extends StatelessWidget {
|
||||
const _StatusCards();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: const [
|
||||
_InfoCard(
|
||||
icon: Icons.route_outlined,
|
||||
title: 'Build roadmap',
|
||||
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: 'Tech stack',
|
||||
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: 'Today’s focus',
|
||||
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;
|
||||
|
||||
@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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Tag extends StatelessWidget {
|
||||
const _Tag({required this.icon, required this.label});
|
||||
|
||||
final IconData icon;
|
||||
final String label;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
return Chip(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
backgroundColor: colorScheme.onPrimary.withValues(alpha: 0.14),
|
||||
avatar: Icon(icon, color: colorScheme.onPrimary, size: 16),
|
||||
label: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: colorScheme.onPrimary,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
side: BorderSide.none,
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
visualDensity: VisualDensity.compact,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user