refactor(ui): mobile_carousel_layout.dart 분할 (1220→689 LOC)
- RetroSelectDialog, RetroOptionItem: 선택 다이얼로그 - RetroSoundDialog: 사운드 설정 다이얼로그 - RetroConfirmDialog: 확인 다이얼로그 - RetroMenuSection, RetroMenuItem, RetroSpeedChip: 메뉴 위젯
This commit is contained in:
@@ -15,7 +15,11 @@ import 'package:asciineverdie/src/features/game/pages/skills_page.dart';
|
|||||||
import 'package:asciineverdie/src/features/game/pages/story_page.dart';
|
import 'package:asciineverdie/src/features/game/pages/story_page.dart';
|
||||||
import 'package:asciineverdie/src/features/game/widgets/carousel_nav_bar.dart';
|
import 'package:asciineverdie/src/features/game/widgets/carousel_nav_bar.dart';
|
||||||
import 'package:asciineverdie/src/features/game/widgets/combat_log.dart';
|
import 'package:asciineverdie/src/features/game/widgets/combat_log.dart';
|
||||||
|
import 'package:asciineverdie/src/features/game/widgets/dialogs/retro_confirm_dialog.dart';
|
||||||
|
import 'package:asciineverdie/src/features/game/widgets/dialogs/retro_select_dialog.dart';
|
||||||
|
import 'package:asciineverdie/src/features/game/widgets/dialogs/retro_sound_dialog.dart';
|
||||||
import 'package:asciineverdie/src/features/game/widgets/enhanced_animation_panel.dart';
|
import 'package:asciineverdie/src/features/game/widgets/enhanced_animation_panel.dart';
|
||||||
|
import 'package:asciineverdie/src/features/game/widgets/menu/retro_menu_widgets.dart';
|
||||||
import 'package:asciineverdie/src/shared/retro_colors.dart';
|
import 'package:asciineverdie/src/shared/retro_colors.dart';
|
||||||
import 'package:asciineverdie/src/shared/widgets/retro_widgets.dart';
|
import 'package:asciineverdie/src/shared/widgets/retro_widgets.dart';
|
||||||
|
|
||||||
@@ -177,7 +181,7 @@ class _MobileCarouselLayoutState extends State<MobileCarouselLayout> {
|
|||||||
void _showLanguageDialog(BuildContext context) {
|
void _showLanguageDialog(BuildContext context) {
|
||||||
showDialog<void>(
|
showDialog<void>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => _RetroSelectDialog(
|
builder: (context) => RetroSelectDialog(
|
||||||
title: l10n.menuLanguage.toUpperCase(),
|
title: l10n.menuLanguage.toUpperCase(),
|
||||||
children: [
|
children: [
|
||||||
_buildLanguageOption(context, 'en', l10n.languageEnglish, '🇺🇸'),
|
_buildLanguageOption(context, 'en', l10n.languageEnglish, '🇺🇸'),
|
||||||
@@ -195,7 +199,7 @@ class _MobileCarouselLayoutState extends State<MobileCarouselLayout> {
|
|||||||
String flag,
|
String flag,
|
||||||
) {
|
) {
|
||||||
final isSelected = l10n.currentGameLocale == locale;
|
final isSelected = l10n.currentGameLocale == locale;
|
||||||
return _RetroOptionItem(
|
return RetroOptionItem(
|
||||||
label: label.toUpperCase(),
|
label: label.toUpperCase(),
|
||||||
prefix: flag,
|
prefix: flag,
|
||||||
isSelected: isSelected,
|
isSelected: isSelected,
|
||||||
@@ -224,7 +228,7 @@ class _MobileCarouselLayoutState extends State<MobileCarouselLayout> {
|
|||||||
showDialog<void>(
|
showDialog<void>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => StatefulBuilder(
|
builder: (context) => StatefulBuilder(
|
||||||
builder: (context, setDialogState) => _RetroSoundDialog(
|
builder: (context, setDialogState) => RetroSoundDialog(
|
||||||
bgmVolume: bgmVolume,
|
bgmVolume: bgmVolume,
|
||||||
sfxVolume: sfxVolume,
|
sfxVolume: sfxVolume,
|
||||||
onBgmChanged: (double value) {
|
onBgmChanged: (double value) {
|
||||||
@@ -244,7 +248,7 @@ class _MobileCarouselLayoutState extends State<MobileCarouselLayout> {
|
|||||||
void _showDeleteConfirmDialog(BuildContext context) {
|
void _showDeleteConfirmDialog(BuildContext context) {
|
||||||
showDialog<void>(
|
showDialog<void>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => _RetroConfirmDialog(
|
builder: (context) => RetroConfirmDialog(
|
||||||
title: l10n.confirmDeleteTitle.toUpperCase(),
|
title: l10n.confirmDeleteTitle.toUpperCase(),
|
||||||
message: l10n.confirmDeleteMessage,
|
message: l10n.confirmDeleteMessage,
|
||||||
confirmText: l10n.buttonConfirm.toUpperCase(),
|
confirmText: l10n.buttonConfirm.toUpperCase(),
|
||||||
@@ -262,14 +266,11 @@ class _MobileCarouselLayoutState extends State<MobileCarouselLayout> {
|
|||||||
Future<void> _showTestCharacterDialog(BuildContext context) async {
|
Future<void> _showTestCharacterDialog(BuildContext context) async {
|
||||||
final confirmed = await showDialog<bool>(
|
final confirmed = await showDialog<bool>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => _RetroConfirmDialog(
|
builder: (context) => RetroConfirmDialog(
|
||||||
title: 'CREATE TEST CHARACTER?',
|
title: L10n.of(context).debugCreateTestCharacterTitle,
|
||||||
message: '현재 캐릭터가 레벨 100으로 변환되어\n'
|
message: L10n.of(context).debugCreateTestCharacterMessage,
|
||||||
'명예의 전당에 등록됩니다.\n\n'
|
confirmText: L10n.of(context).createButton,
|
||||||
'⚠️ 현재 세이브 파일이 삭제됩니다.\n'
|
cancelText: L10n.of(context).cancel.toUpperCase(),
|
||||||
'이 작업은 되돌릴 수 없습니다.',
|
|
||||||
confirmText: 'CREATE',
|
|
||||||
cancelText: 'CANCEL',
|
|
||||||
onConfirm: () => Navigator.of(context).pop(true),
|
onConfirm: () => Navigator.of(context).pop(true),
|
||||||
onCancel: () => Navigator.of(context).pop(false),
|
onCancel: () => Navigator.of(context).pop(false),
|
||||||
),
|
),
|
||||||
@@ -326,7 +327,7 @@ class _MobileCarouselLayoutState extends State<MobileCarouselLayout> {
|
|||||||
Icon(Icons.settings, color: gold, size: 18),
|
Icon(Icons.settings, color: gold, size: 18),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
'OPTIONS',
|
L10n.of(context).optionsTitle,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontFamily: 'PressStart2P',
|
fontFamily: 'PressStart2P',
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
@@ -350,10 +351,10 @@ class _MobileCarouselLayoutState extends State<MobileCarouselLayout> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// === 게임 제어 ===
|
// === 게임 제어 ===
|
||||||
const _RetroMenuSection(title: 'CONTROL'),
|
RetroMenuSection(title: L10n.of(context).controlSection),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
// 일시정지/재개
|
// 일시정지/재개
|
||||||
_RetroMenuItem(
|
RetroMenuItem(
|
||||||
icon: widget.isPaused ? Icons.play_arrow : Icons.pause,
|
icon: widget.isPaused ? Icons.play_arrow : Icons.pause,
|
||||||
iconColor: widget.isPaused
|
iconColor: widget.isPaused
|
||||||
? RetroColors.expOf(context)
|
? RetroColors.expOf(context)
|
||||||
@@ -368,7 +369,7 @@ class _MobileCarouselLayoutState extends State<MobileCarouselLayout> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
// 속도 조절
|
// 속도 조절
|
||||||
_RetroMenuItem(
|
RetroMenuItem(
|
||||||
icon: Icons.speed,
|
icon: Icons.speed,
|
||||||
iconColor: gold,
|
iconColor: gold,
|
||||||
label: l10n.menuSpeed.toUpperCase(),
|
label: l10n.menuSpeed.toUpperCase(),
|
||||||
@@ -377,10 +378,10 @@ class _MobileCarouselLayoutState extends State<MobileCarouselLayout> {
|
|||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// === 정보 ===
|
// === 정보 ===
|
||||||
const _RetroMenuSection(title: 'INFO'),
|
RetroMenuSection(title: L10n.of(context).infoSection),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
if (widget.onShowStatistics != null)
|
if (widget.onShowStatistics != null)
|
||||||
_RetroMenuItem(
|
RetroMenuItem(
|
||||||
icon: Icons.bar_chart,
|
icon: Icons.bar_chart,
|
||||||
iconColor: RetroColors.mpOf(context),
|
iconColor: RetroColors.mpOf(context),
|
||||||
label: l10n.uiStatistics.toUpperCase(),
|
label: l10n.uiStatistics.toUpperCase(),
|
||||||
@@ -391,7 +392,7 @@ class _MobileCarouselLayoutState extends State<MobileCarouselLayout> {
|
|||||||
),
|
),
|
||||||
if (widget.onShowHelp != null) ...[
|
if (widget.onShowHelp != null) ...[
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
_RetroMenuItem(
|
RetroMenuItem(
|
||||||
icon: Icons.help_outline,
|
icon: Icons.help_outline,
|
||||||
iconColor: RetroColors.expOf(context),
|
iconColor: RetroColors.expOf(context),
|
||||||
label: l10n.uiHelp.toUpperCase(),
|
label: l10n.uiHelp.toUpperCase(),
|
||||||
@@ -404,9 +405,9 @@ class _MobileCarouselLayoutState extends State<MobileCarouselLayout> {
|
|||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// === 설정 ===
|
// === 설정 ===
|
||||||
const _RetroMenuSection(title: 'SETTINGS'),
|
RetroMenuSection(title: L10n.of(context).settingsSection),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
_RetroMenuItem(
|
RetroMenuItem(
|
||||||
icon: Icons.language,
|
icon: Icons.language,
|
||||||
iconColor: RetroColors.mpOf(context),
|
iconColor: RetroColors.mpOf(context),
|
||||||
label: l10n.menuLanguage.toUpperCase(),
|
label: l10n.menuLanguage.toUpperCase(),
|
||||||
@@ -419,7 +420,7 @@ class _MobileCarouselLayoutState extends State<MobileCarouselLayout> {
|
|||||||
if (widget.onBgmVolumeChange != null ||
|
if (widget.onBgmVolumeChange != null ||
|
||||||
widget.onSfxVolumeChange != null) ...[
|
widget.onSfxVolumeChange != null) ...[
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
_RetroMenuItem(
|
RetroMenuItem(
|
||||||
icon: widget.bgmVolume == 0 && widget.sfxVolume == 0
|
icon: widget.bgmVolume == 0 && widget.sfxVolume == 0
|
||||||
? Icons.volume_off
|
? Icons.volume_off
|
||||||
: Icons.volume_up,
|
: Icons.volume_up,
|
||||||
@@ -435,9 +436,9 @@ class _MobileCarouselLayoutState extends State<MobileCarouselLayout> {
|
|||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// === 저장/종료 ===
|
// === 저장/종료 ===
|
||||||
const _RetroMenuSection(title: 'SAVE / EXIT'),
|
RetroMenuSection(title: L10n.of(context).saveExitSection),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
_RetroMenuItem(
|
RetroMenuItem(
|
||||||
icon: Icons.save,
|
icon: Icons.save,
|
||||||
iconColor: RetroColors.mpOf(context),
|
iconColor: RetroColors.mpOf(context),
|
||||||
label: l10n.menuSave.toUpperCase(),
|
label: l10n.menuSave.toUpperCase(),
|
||||||
@@ -450,7 +451,7 @@ class _MobileCarouselLayoutState extends State<MobileCarouselLayout> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
_RetroMenuItem(
|
RetroMenuItem(
|
||||||
icon: Icons.refresh,
|
icon: Icons.refresh,
|
||||||
iconColor: RetroColors.warningOf(context),
|
iconColor: RetroColors.warningOf(context),
|
||||||
label: l10n.menuNewGame.toUpperCase(),
|
label: l10n.menuNewGame.toUpperCase(),
|
||||||
@@ -461,7 +462,7 @@ class _MobileCarouselLayoutState extends State<MobileCarouselLayout> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
_RetroMenuItem(
|
RetroMenuItem(
|
||||||
icon: Icons.exit_to_app,
|
icon: Icons.exit_to_app,
|
||||||
iconColor: RetroColors.hpOf(context),
|
iconColor: RetroColors.hpOf(context),
|
||||||
label: localizations.exitGame.toUpperCase(),
|
label: localizations.exitGame.toUpperCase(),
|
||||||
@@ -474,38 +475,38 @@ class _MobileCarouselLayoutState extends State<MobileCarouselLayout> {
|
|||||||
// === 치트 섹션 (디버그 모드에서만) ===
|
// === 치트 섹션 (디버그 모드에서만) ===
|
||||||
if (widget.cheatsEnabled) ...[
|
if (widget.cheatsEnabled) ...[
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_RetroMenuSection(
|
RetroMenuSection(
|
||||||
title: 'DEBUG CHEATS',
|
title: L10n.of(context).debugCheatsTitle,
|
||||||
color: RetroColors.hpOf(context),
|
color: RetroColors.hpOf(context),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
_RetroMenuItem(
|
RetroMenuItem(
|
||||||
icon: Icons.fast_forward,
|
icon: Icons.fast_forward,
|
||||||
iconColor: RetroColors.hpOf(context),
|
iconColor: RetroColors.hpOf(context),
|
||||||
label: 'SKIP TASK (L+1)',
|
label: L10n.of(context).debugSkipTask,
|
||||||
subtitle: '태스크 즉시 완료',
|
subtitle: L10n.of(context).debugSkipTaskDesc,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
widget.onCheatTask?.call();
|
widget.onCheatTask?.call();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
_RetroMenuItem(
|
RetroMenuItem(
|
||||||
icon: Icons.skip_next,
|
icon: Icons.skip_next,
|
||||||
iconColor: RetroColors.hpOf(context),
|
iconColor: RetroColors.hpOf(context),
|
||||||
label: 'SKIP QUEST (Q!)',
|
label: L10n.of(context).debugSkipQuest,
|
||||||
subtitle: '퀘스트 즉시 완료',
|
subtitle: L10n.of(context).debugSkipQuestDesc,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
widget.onCheatQuest?.call();
|
widget.onCheatQuest?.call();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
_RetroMenuItem(
|
RetroMenuItem(
|
||||||
icon: Icons.double_arrow,
|
icon: Icons.double_arrow,
|
||||||
iconColor: RetroColors.hpOf(context),
|
iconColor: RetroColors.hpOf(context),
|
||||||
label: 'SKIP ACT (P!)',
|
label: L10n.of(context).debugSkipAct,
|
||||||
subtitle: '액트 즉시 완료',
|
subtitle: L10n.of(context).debugSkipActDesc,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
widget.onCheatPlot?.call();
|
widget.onCheatPlot?.call();
|
||||||
@@ -516,16 +517,16 @@ class _MobileCarouselLayoutState extends State<MobileCarouselLayout> {
|
|||||||
// === 디버그 도구 섹션 ===
|
// === 디버그 도구 섹션 ===
|
||||||
if (kDebugMode && widget.onCreateTestCharacter != null) ...[
|
if (kDebugMode && widget.onCreateTestCharacter != null) ...[
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_RetroMenuSection(
|
RetroMenuSection(
|
||||||
title: 'DEBUG TOOLS',
|
title: L10n.of(context).debugToolsTitle,
|
||||||
color: RetroColors.warningOf(context),
|
color: RetroColors.warningOf(context),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
_RetroMenuItem(
|
RetroMenuItem(
|
||||||
icon: Icons.science,
|
icon: Icons.science,
|
||||||
iconColor: RetroColors.warningOf(context),
|
iconColor: RetroColors.warningOf(context),
|
||||||
label: 'CREATE TEST CHARACTER',
|
label: L10n.of(context).debugCreateTestCharacter,
|
||||||
subtitle: '레벨 100 캐릭터를 명예의 전당에 등록',
|
subtitle: L10n.of(context).debugCreateTestCharacterDesc,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
_showTestCharacterDialog(context);
|
_showTestCharacterDialog(context);
|
||||||
@@ -553,7 +554,7 @@ class _MobileCarouselLayoutState extends State<MobileCarouselLayout> {
|
|||||||
final isSpeedBoostActive = widget.isSpeedBoostActive;
|
final isSpeedBoostActive = widget.isSpeedBoostActive;
|
||||||
final adSpeed = widget.adSpeedMultiplier;
|
final adSpeed = widget.adSpeedMultiplier;
|
||||||
|
|
||||||
return _RetroSpeedChip(
|
return RetroSpeedChip(
|
||||||
speed: adSpeed,
|
speed: adSpeed,
|
||||||
isSelected: isSpeedBoostActive,
|
isSelected: isSpeedBoostActive,
|
||||||
isAdBased: !isSpeedBoostActive && !widget.isPaidUser,
|
isAdBased: !isSpeedBoostActive && !widget.isPaidUser,
|
||||||
@@ -685,539 +686,4 @@ class _MobileCarouselLayoutState extends State<MobileCarouselLayout> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
// 레트로 스타일 옵션 메뉴 위젯들
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
/// 메뉴 섹션 타이틀
|
|
||||||
class _RetroMenuSection extends StatelessWidget {
|
|
||||||
const _RetroMenuSection({required this.title, this.color});
|
|
||||||
|
|
||||||
final String title;
|
|
||||||
final Color? color;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final gold = color ?? RetroColors.goldOf(context);
|
|
||||||
return Row(
|
|
||||||
children: [
|
|
||||||
Container(width: 4, height: 14, color: gold),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Text(
|
|
||||||
title,
|
|
||||||
style: TextStyle(
|
|
||||||
fontFamily: 'PressStart2P',
|
|
||||||
fontSize: 9,
|
|
||||||
color: gold,
|
|
||||||
letterSpacing: 1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 메뉴 아이템
|
|
||||||
class _RetroMenuItem extends StatelessWidget {
|
|
||||||
const _RetroMenuItem({
|
|
||||||
required this.icon,
|
|
||||||
required this.iconColor,
|
|
||||||
required this.label,
|
|
||||||
this.value,
|
|
||||||
this.subtitle,
|
|
||||||
this.trailing,
|
|
||||||
this.onTap,
|
|
||||||
});
|
|
||||||
|
|
||||||
final IconData icon;
|
|
||||||
final Color iconColor;
|
|
||||||
final String label;
|
|
||||||
final String? value;
|
|
||||||
final String? subtitle;
|
|
||||||
final Widget? trailing;
|
|
||||||
final VoidCallback? onTap;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final border = RetroColors.borderOf(context);
|
|
||||||
final panelBg = RetroColors.panelBgOf(context);
|
|
||||||
|
|
||||||
return GestureDetector(
|
|
||||||
onTap: onTap,
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: panelBg,
|
|
||||||
border: Border.all(color: border, width: 1),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Icon(icon, size: 16, color: iconColor),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
label,
|
|
||||||
style: TextStyle(
|
|
||||||
fontFamily: 'PressStart2P',
|
|
||||||
fontSize: 16,
|
|
||||||
color: RetroColors.textPrimaryOf(context),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (subtitle != null) ...[
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text(
|
|
||||||
subtitle!,
|
|
||||||
style: TextStyle(
|
|
||||||
fontFamily: 'PressStart2P',
|
|
||||||
fontSize: 12,
|
|
||||||
color: RetroColors.textMutedOf(context),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (value != null)
|
|
||||||
Text(
|
|
||||||
value!,
|
|
||||||
style: TextStyle(
|
|
||||||
fontFamily: 'PressStart2P',
|
|
||||||
fontSize: 14,
|
|
||||||
color: RetroColors.goldOf(context),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (trailing != null) trailing!,
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 속도 선택 칩
|
|
||||||
class _RetroSpeedChip extends StatelessWidget {
|
|
||||||
const _RetroSpeedChip({
|
|
||||||
required this.speed,
|
|
||||||
required this.isSelected,
|
|
||||||
required this.onTap,
|
|
||||||
this.isAdBased = false,
|
|
||||||
this.isDisabled = false,
|
|
||||||
});
|
|
||||||
|
|
||||||
final int speed;
|
|
||||||
final bool isSelected;
|
|
||||||
final VoidCallback onTap;
|
|
||||||
final bool isAdBased;
|
|
||||||
|
|
||||||
/// 비활성 상태 (반투명, 탭 무시)
|
|
||||||
final bool isDisabled;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final gold = RetroColors.goldOf(context);
|
|
||||||
final warning = RetroColors.warningOf(context);
|
|
||||||
final border = RetroColors.borderOf(context);
|
|
||||||
|
|
||||||
// 비활성 상태면 반투명 처리
|
|
||||||
final opacity = isDisabled ? 0.4 : 1.0;
|
|
||||||
|
|
||||||
final Color bgColor;
|
|
||||||
final Color textColor;
|
|
||||||
final Color borderColor;
|
|
||||||
|
|
||||||
if (isSelected) {
|
|
||||||
bgColor = isAdBased
|
|
||||||
? warning.withValues(alpha: 0.3 * opacity)
|
|
||||||
: gold.withValues(alpha: 0.3 * opacity);
|
|
||||||
textColor = (isAdBased ? warning : gold).withValues(alpha: opacity);
|
|
||||||
borderColor = (isAdBased ? warning : gold).withValues(alpha: opacity);
|
|
||||||
} else if (isAdBased) {
|
|
||||||
bgColor = Colors.transparent;
|
|
||||||
textColor = warning.withValues(alpha: opacity);
|
|
||||||
borderColor = warning.withValues(alpha: opacity);
|
|
||||||
} else {
|
|
||||||
bgColor = Colors.transparent;
|
|
||||||
textColor = RetroColors.textMutedOf(context).withValues(alpha: opacity);
|
|
||||||
borderColor = border.withValues(alpha: opacity);
|
|
||||||
}
|
|
||||||
|
|
||||||
return GestureDetector(
|
|
||||||
// 비활성 상태면 탭 무시
|
|
||||||
onTap: isDisabled ? null : onTap,
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: bgColor,
|
|
||||||
border: Border.all(color: borderColor, width: isSelected ? 2 : 1),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
if (isAdBased && !isSelected && !isDisabled)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(right: 2),
|
|
||||||
child: Text(
|
|
||||||
'▶',
|
|
||||||
style: TextStyle(fontSize: 7, color: warning),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
'${speed}x',
|
|
||||||
style: TextStyle(
|
|
||||||
fontFamily: 'PressStart2P',
|
|
||||||
fontSize: 8,
|
|
||||||
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
|
|
||||||
color: textColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 선택 다이얼로그
|
|
||||||
class _RetroSelectDialog extends StatelessWidget {
|
|
||||||
const _RetroSelectDialog({required this.title, required this.children});
|
|
||||||
|
|
||||||
final String title;
|
|
||||||
final List<Widget> children;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final background = RetroColors.backgroundOf(context);
|
|
||||||
final gold = RetroColors.goldOf(context);
|
|
||||||
|
|
||||||
return Dialog(
|
|
||||||
backgroundColor: Colors.transparent,
|
|
||||||
child: Container(
|
|
||||||
constraints: const BoxConstraints(maxWidth: 320),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: background,
|
|
||||||
border: Border.all(color: gold, width: 2),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
// 타이틀
|
|
||||||
Container(
|
|
||||||
width: double.infinity,
|
|
||||||
padding: const EdgeInsets.all(12),
|
|
||||||
color: gold.withValues(alpha: 0.2),
|
|
||||||
child: Text(
|
|
||||||
title,
|
|
||||||
style: TextStyle(
|
|
||||||
fontFamily: 'PressStart2P',
|
|
||||||
fontSize: 10,
|
|
||||||
color: gold,
|
|
||||||
),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// 옵션 목록
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(12),
|
|
||||||
child: Column(children: children),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 선택 옵션 아이템
|
|
||||||
class _RetroOptionItem extends StatelessWidget {
|
|
||||||
const _RetroOptionItem({
|
|
||||||
required this.label,
|
|
||||||
required this.isSelected,
|
|
||||||
required this.onTap,
|
|
||||||
this.prefix,
|
|
||||||
});
|
|
||||||
|
|
||||||
final String label;
|
|
||||||
final bool isSelected;
|
|
||||||
final VoidCallback onTap;
|
|
||||||
final String? prefix;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final gold = RetroColors.goldOf(context);
|
|
||||||
final border = RetroColors.borderOf(context);
|
|
||||||
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
|
||||||
child: GestureDetector(
|
|
||||||
onTap: onTap,
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: isSelected ? gold.withValues(alpha: 0.15) : Colors.transparent,
|
|
||||||
border: Border.all(
|
|
||||||
color: isSelected ? gold : border,
|
|
||||||
width: isSelected ? 2 : 1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
if (prefix != null) ...[
|
|
||||||
Text(prefix!, style: const TextStyle(fontSize: 16)),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
],
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
label,
|
|
||||||
style: TextStyle(
|
|
||||||
fontFamily: 'PressStart2P',
|
|
||||||
fontSize: 18,
|
|
||||||
color: isSelected ? gold : RetroColors.textPrimaryOf(context),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (isSelected) Icon(Icons.check, size: 16, color: gold),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 사운드 설정 다이얼로그
|
|
||||||
class _RetroSoundDialog extends StatelessWidget {
|
|
||||||
const _RetroSoundDialog({
|
|
||||||
required this.bgmVolume,
|
|
||||||
required this.sfxVolume,
|
|
||||||
required this.onBgmChanged,
|
|
||||||
required this.onSfxChanged,
|
|
||||||
});
|
|
||||||
|
|
||||||
final double bgmVolume;
|
|
||||||
final double sfxVolume;
|
|
||||||
final ValueChanged<double> onBgmChanged;
|
|
||||||
final ValueChanged<double> onSfxChanged;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final background = RetroColors.backgroundOf(context);
|
|
||||||
final gold = RetroColors.goldOf(context);
|
|
||||||
|
|
||||||
return Dialog(
|
|
||||||
backgroundColor: Colors.transparent,
|
|
||||||
child: Container(
|
|
||||||
constraints: const BoxConstraints(maxWidth: 360),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: background,
|
|
||||||
border: Border.all(color: gold, width: 2),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
// 타이틀
|
|
||||||
Container(
|
|
||||||
width: double.infinity,
|
|
||||||
padding: const EdgeInsets.all(12),
|
|
||||||
color: gold.withValues(alpha: 0.2),
|
|
||||||
child: Text(
|
|
||||||
'SOUND',
|
|
||||||
style: TextStyle(
|
|
||||||
fontFamily: 'PressStart2P',
|
|
||||||
fontSize: 10,
|
|
||||||
color: gold,
|
|
||||||
),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// 슬라이더
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
_buildVolumeSlider(
|
|
||||||
context,
|
|
||||||
icon: bgmVolume == 0 ? Icons.music_off : Icons.music_note,
|
|
||||||
label: 'BGM',
|
|
||||||
value: bgmVolume,
|
|
||||||
onChanged: onBgmChanged,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
_buildVolumeSlider(
|
|
||||||
context,
|
|
||||||
icon: sfxVolume == 0 ? Icons.volume_off : Icons.volume_up,
|
|
||||||
label: 'SFX',
|
|
||||||
value: sfxVolume,
|
|
||||||
onChanged: onSfxChanged,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// 확인 버튼
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
|
||||||
child: SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: RetroTextButton(
|
|
||||||
text: 'OK',
|
|
||||||
onPressed: () => Navigator.pop(context),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildVolumeSlider(
|
|
||||||
BuildContext context, {
|
|
||||||
required IconData icon,
|
|
||||||
required String label,
|
|
||||||
required double value,
|
|
||||||
required ValueChanged<double> onChanged,
|
|
||||||
}) {
|
|
||||||
final gold = RetroColors.goldOf(context);
|
|
||||||
final border = RetroColors.borderOf(context);
|
|
||||||
final percentage = (value * 100).round();
|
|
||||||
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Icon(icon, size: 18, color: gold),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Text(
|
|
||||||
label,
|
|
||||||
style: TextStyle(
|
|
||||||
fontFamily: 'PressStart2P',
|
|
||||||
fontSize: 16,
|
|
||||||
color: RetroColors.textPrimaryOf(context),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Spacer(),
|
|
||||||
Text(
|
|
||||||
'$percentage%',
|
|
||||||
style: TextStyle(
|
|
||||||
fontFamily: 'PressStart2P',
|
|
||||||
fontSize: 16,
|
|
||||||
color: gold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
SliderTheme(
|
|
||||||
data: SliderThemeData(
|
|
||||||
trackHeight: 8,
|
|
||||||
activeTrackColor: gold,
|
|
||||||
inactiveTrackColor: border,
|
|
||||||
thumbColor: RetroColors.goldLightOf(context),
|
|
||||||
thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 8),
|
|
||||||
overlayColor: gold.withValues(alpha: 0.2),
|
|
||||||
trackShape: const RectangularSliderTrackShape(),
|
|
||||||
),
|
|
||||||
child: Slider(value: value, onChanged: onChanged, divisions: 10),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 확인 다이얼로그
|
|
||||||
class _RetroConfirmDialog extends StatelessWidget {
|
|
||||||
const _RetroConfirmDialog({
|
|
||||||
required this.title,
|
|
||||||
required this.message,
|
|
||||||
required this.confirmText,
|
|
||||||
required this.cancelText,
|
|
||||||
required this.onConfirm,
|
|
||||||
required this.onCancel,
|
|
||||||
});
|
|
||||||
|
|
||||||
final String title;
|
|
||||||
final String message;
|
|
||||||
final String confirmText;
|
|
||||||
final String cancelText;
|
|
||||||
final VoidCallback onConfirm;
|
|
||||||
final VoidCallback onCancel;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final background = RetroColors.backgroundOf(context);
|
|
||||||
final gold = RetroColors.goldOf(context);
|
|
||||||
|
|
||||||
return Dialog(
|
|
||||||
backgroundColor: Colors.transparent,
|
|
||||||
child: Container(
|
|
||||||
constraints: const BoxConstraints(maxWidth: 360),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: background,
|
|
||||||
border: Border.all(color: gold, width: 3),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
// 타이틀
|
|
||||||
Container(
|
|
||||||
width: double.infinity,
|
|
||||||
padding: const EdgeInsets.all(12),
|
|
||||||
color: gold.withValues(alpha: 0.2),
|
|
||||||
child: Text(
|
|
||||||
title,
|
|
||||||
style: TextStyle(
|
|
||||||
fontFamily: 'PressStart2P',
|
|
||||||
fontSize: 10,
|
|
||||||
color: gold,
|
|
||||||
),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// 메시지
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: Text(
|
|
||||||
message,
|
|
||||||
style: TextStyle(
|
|
||||||
fontFamily: 'PressStart2P',
|
|
||||||
fontSize: 16,
|
|
||||||
color: RetroColors.textPrimaryOf(context),
|
|
||||||
height: 1.8,
|
|
||||||
),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// 버튼
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.fromLTRB(12, 0, 12, 12),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: RetroTextButton(
|
|
||||||
text: cancelText,
|
|
||||||
isPrimary: false,
|
|
||||||
onPressed: onCancel,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Expanded(
|
|
||||||
child: RetroTextButton(
|
|
||||||
text: confirmText,
|
|
||||||
onPressed: onConfirm,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:asciineverdie/src/shared/retro_colors.dart';
|
||||||
|
import 'package:asciineverdie/src/shared/widgets/retro_widgets.dart';
|
||||||
|
|
||||||
|
/// 레트로 스타일 확인 다이얼로그
|
||||||
|
class RetroConfirmDialog extends StatelessWidget {
|
||||||
|
const RetroConfirmDialog({
|
||||||
|
super.key,
|
||||||
|
required this.title,
|
||||||
|
required this.message,
|
||||||
|
required this.confirmText,
|
||||||
|
required this.cancelText,
|
||||||
|
required this.onConfirm,
|
||||||
|
required this.onCancel,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
final String message;
|
||||||
|
final String confirmText;
|
||||||
|
final String cancelText;
|
||||||
|
final VoidCallback onConfirm;
|
||||||
|
final VoidCallback onCancel;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final background = RetroColors.backgroundOf(context);
|
||||||
|
final gold = RetroColors.goldOf(context);
|
||||||
|
|
||||||
|
return Dialog(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
child: Container(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 360),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: background,
|
||||||
|
border: Border.all(color: gold, width: 3),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
// 타이틀
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
color: gold.withValues(alpha: 0.2),
|
||||||
|
child: Text(
|
||||||
|
title,
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'PressStart2P',
|
||||||
|
fontSize: 10,
|
||||||
|
color: gold,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// 메시지
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Text(
|
||||||
|
message,
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'PressStart2P',
|
||||||
|
fontSize: 16,
|
||||||
|
color: RetroColors.textPrimaryOf(context),
|
||||||
|
height: 1.8,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// 버튼
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(12, 0, 12, 12),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: RetroTextButton(
|
||||||
|
text: cancelText,
|
||||||
|
isPrimary: false,
|
||||||
|
onPressed: onCancel,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: RetroTextButton(
|
||||||
|
text: confirmText,
|
||||||
|
onPressed: onConfirm,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
115
lib/src/features/game/widgets/dialogs/retro_select_dialog.dart
Normal file
115
lib/src/features/game/widgets/dialogs/retro_select_dialog.dart
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:asciineverdie/src/shared/retro_colors.dart';
|
||||||
|
|
||||||
|
/// 레트로 스타일 선택 다이얼로그
|
||||||
|
class RetroSelectDialog extends StatelessWidget {
|
||||||
|
const RetroSelectDialog({
|
||||||
|
super.key,
|
||||||
|
required this.title,
|
||||||
|
required this.children,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
final List<Widget> children;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final background = RetroColors.backgroundOf(context);
|
||||||
|
final gold = RetroColors.goldOf(context);
|
||||||
|
|
||||||
|
return Dialog(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
child: Container(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 320),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: background,
|
||||||
|
border: Border.all(color: gold, width: 2),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
// 타이틀
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
color: gold.withValues(alpha: 0.2),
|
||||||
|
child: Text(
|
||||||
|
title,
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'PressStart2P',
|
||||||
|
fontSize: 10,
|
||||||
|
color: gold,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// 옵션 목록
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
child: Column(children: children),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 선택 옵션 아이템
|
||||||
|
class RetroOptionItem extends StatelessWidget {
|
||||||
|
const RetroOptionItem({
|
||||||
|
super.key,
|
||||||
|
required this.label,
|
||||||
|
required this.isSelected,
|
||||||
|
required this.onTap,
|
||||||
|
this.prefix,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String label;
|
||||||
|
final bool isSelected;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
final String? prefix;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final gold = RetroColors.goldOf(context);
|
||||||
|
final border = RetroColors.borderOf(context);
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: onTap,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isSelected ? gold.withValues(alpha: 0.15) : Colors.transparent,
|
||||||
|
border: Border.all(
|
||||||
|
color: isSelected ? gold : border,
|
||||||
|
width: isSelected ? 2 : 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
if (prefix != null) ...[
|
||||||
|
Text(prefix!, style: const TextStyle(fontSize: 16)),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
],
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'PressStart2P',
|
||||||
|
fontSize: 18,
|
||||||
|
color: isSelected ? gold : RetroColors.textPrimaryOf(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (isSelected) Icon(Icons.check, size: 16, color: gold),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
146
lib/src/features/game/widgets/dialogs/retro_sound_dialog.dart
Normal file
146
lib/src/features/game/widgets/dialogs/retro_sound_dialog.dart
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:asciineverdie/l10n/app_localizations.dart';
|
||||||
|
import 'package:asciineverdie/src/shared/retro_colors.dart';
|
||||||
|
import 'package:asciineverdie/src/shared/widgets/retro_widgets.dart';
|
||||||
|
|
||||||
|
/// 레트로 스타일 사운드 설정 다이얼로그
|
||||||
|
class RetroSoundDialog extends StatelessWidget {
|
||||||
|
const RetroSoundDialog({
|
||||||
|
super.key,
|
||||||
|
required this.bgmVolume,
|
||||||
|
required this.sfxVolume,
|
||||||
|
required this.onBgmChanged,
|
||||||
|
required this.onSfxChanged,
|
||||||
|
});
|
||||||
|
|
||||||
|
final double bgmVolume;
|
||||||
|
final double sfxVolume;
|
||||||
|
final ValueChanged<double> onBgmChanged;
|
||||||
|
final ValueChanged<double> onSfxChanged;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final background = RetroColors.backgroundOf(context);
|
||||||
|
final gold = RetroColors.goldOf(context);
|
||||||
|
|
||||||
|
return Dialog(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
child: Container(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 360),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: background,
|
||||||
|
border: Border.all(color: gold, width: 2),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
// 타이틀
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
color: gold.withValues(alpha: 0.2),
|
||||||
|
child: Text(
|
||||||
|
L10n.of(context).soundTitle,
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'PressStart2P',
|
||||||
|
fontSize: 10,
|
||||||
|
color: gold,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// 슬라이더
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
_buildVolumeSlider(
|
||||||
|
context,
|
||||||
|
icon: bgmVolume == 0 ? Icons.music_off : Icons.music_note,
|
||||||
|
label: L10n.of(context).bgmLabel,
|
||||||
|
value: bgmVolume,
|
||||||
|
onChanged: onBgmChanged,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_buildVolumeSlider(
|
||||||
|
context,
|
||||||
|
icon: sfxVolume == 0 ? Icons.volume_off : Icons.volume_up,
|
||||||
|
label: L10n.of(context).sfxLabel,
|
||||||
|
value: sfxVolume,
|
||||||
|
onChanged: onSfxChanged,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// 확인 버튼
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
||||||
|
child: SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: RetroTextButton(
|
||||||
|
text: L10n.of(context).ok,
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildVolumeSlider(
|
||||||
|
BuildContext context, {
|
||||||
|
required IconData icon,
|
||||||
|
required String label,
|
||||||
|
required double value,
|
||||||
|
required ValueChanged<double> onChanged,
|
||||||
|
}) {
|
||||||
|
final gold = RetroColors.goldOf(context);
|
||||||
|
final border = RetroColors.borderOf(context);
|
||||||
|
final percentage = (value * 100).round();
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(icon, size: 18, color: gold),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'PressStart2P',
|
||||||
|
fontSize: 16,
|
||||||
|
color: RetroColors.textPrimaryOf(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
Text(
|
||||||
|
'$percentage%',
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'PressStart2P',
|
||||||
|
fontSize: 16,
|
||||||
|
color: gold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
SliderTheme(
|
||||||
|
data: SliderThemeData(
|
||||||
|
trackHeight: 8,
|
||||||
|
activeTrackColor: gold,
|
||||||
|
inactiveTrackColor: border,
|
||||||
|
thumbColor: RetroColors.goldLightOf(context),
|
||||||
|
thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 8),
|
||||||
|
overlayColor: gold.withValues(alpha: 0.2),
|
||||||
|
trackShape: const RectangularSliderTrackShape(),
|
||||||
|
),
|
||||||
|
child: Slider(value: value, onChanged: onChanged, divisions: 10),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
204
lib/src/features/game/widgets/menu/retro_menu_widgets.dart
Normal file
204
lib/src/features/game/widgets/menu/retro_menu_widgets.dart
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:asciineverdie/src/shared/retro_colors.dart';
|
||||||
|
|
||||||
|
/// 메뉴 섹션 타이틀
|
||||||
|
class RetroMenuSection extends StatelessWidget {
|
||||||
|
const RetroMenuSection({
|
||||||
|
super.key,
|
||||||
|
required this.title,
|
||||||
|
this.color,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
final Color? color;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final gold = color ?? RetroColors.goldOf(context);
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Container(width: 4, height: 14, color: gold),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'PressStart2P',
|
||||||
|
fontSize: 9,
|
||||||
|
color: gold,
|
||||||
|
letterSpacing: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 메뉴 아이템
|
||||||
|
class RetroMenuItem extends StatelessWidget {
|
||||||
|
const RetroMenuItem({
|
||||||
|
super.key,
|
||||||
|
required this.icon,
|
||||||
|
required this.iconColor,
|
||||||
|
required this.label,
|
||||||
|
this.value,
|
||||||
|
this.subtitle,
|
||||||
|
this.trailing,
|
||||||
|
this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
final IconData icon;
|
||||||
|
final Color iconColor;
|
||||||
|
final String label;
|
||||||
|
final String? value;
|
||||||
|
final String? subtitle;
|
||||||
|
final Widget? trailing;
|
||||||
|
final VoidCallback? onTap;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final border = RetroColors.borderOf(context);
|
||||||
|
final panelBg = RetroColors.panelBgOf(context);
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: onTap,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: panelBg,
|
||||||
|
border: Border.all(color: border, width: 1),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(icon, size: 16, color: iconColor),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'PressStart2P',
|
||||||
|
fontSize: 16,
|
||||||
|
color: RetroColors.textPrimaryOf(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (subtitle != null) ...[
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
subtitle!,
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'PressStart2P',
|
||||||
|
fontSize: 12,
|
||||||
|
color: RetroColors.textMutedOf(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (value != null)
|
||||||
|
Text(
|
||||||
|
value!,
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'PressStart2P',
|
||||||
|
fontSize: 14,
|
||||||
|
color: RetroColors.goldOf(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (trailing != null) trailing!,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 속도 선택 칩
|
||||||
|
///
|
||||||
|
/// - 배속 토글 버튼
|
||||||
|
/// - 부스트 활성화 중: 반투명, 비활성
|
||||||
|
/// - 부스트 비활성화: 불투명, 활성
|
||||||
|
class RetroSpeedChip extends StatelessWidget {
|
||||||
|
const RetroSpeedChip({
|
||||||
|
super.key,
|
||||||
|
required this.speed,
|
||||||
|
required this.isSelected,
|
||||||
|
required this.onTap,
|
||||||
|
this.isAdBased = false,
|
||||||
|
this.isDisabled = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
final int speed;
|
||||||
|
final bool isSelected;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
final bool isAdBased;
|
||||||
|
|
||||||
|
/// 비활성 상태 (반투명, 탭 무시)
|
||||||
|
final bool isDisabled;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final gold = RetroColors.goldOf(context);
|
||||||
|
final warning = RetroColors.warningOf(context);
|
||||||
|
final border = RetroColors.borderOf(context);
|
||||||
|
|
||||||
|
// 비활성 상태면 반투명 처리
|
||||||
|
final opacity = isDisabled ? 0.4 : 1.0;
|
||||||
|
|
||||||
|
final Color bgColor;
|
||||||
|
final Color textColor;
|
||||||
|
final Color borderColor;
|
||||||
|
|
||||||
|
if (isSelected) {
|
||||||
|
bgColor = isAdBased
|
||||||
|
? warning.withValues(alpha: 0.3 * opacity)
|
||||||
|
: gold.withValues(alpha: 0.3 * opacity);
|
||||||
|
textColor = (isAdBased ? warning : gold).withValues(alpha: opacity);
|
||||||
|
borderColor = (isAdBased ? warning : gold).withValues(alpha: opacity);
|
||||||
|
} else if (isAdBased) {
|
||||||
|
bgColor = Colors.transparent;
|
||||||
|
textColor = warning.withValues(alpha: opacity);
|
||||||
|
borderColor = warning.withValues(alpha: opacity);
|
||||||
|
} else {
|
||||||
|
bgColor = Colors.transparent;
|
||||||
|
textColor = RetroColors.textMutedOf(context).withValues(alpha: opacity);
|
||||||
|
borderColor = border.withValues(alpha: opacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
// 비활성 상태면 탭 무시
|
||||||
|
onTap: isDisabled ? null : onTap,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: bgColor,
|
||||||
|
border: Border.all(color: borderColor, width: isSelected ? 2 : 1),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (isAdBased && !isSelected && !isDisabled)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 2),
|
||||||
|
child: Text(
|
||||||
|
'▶',
|
||||||
|
style: TextStyle(fontSize: 7, color: warning),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'${speed}x',
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'PressStart2P',
|
||||||
|
fontSize: 8,
|
||||||
|
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
|
||||||
|
color: textColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user