refactor(ui): mobile_carousel_layout.dart 분할 (1220→689 LOC)

- RetroSelectDialog, RetroOptionItem: 선택 다이얼로그
- RetroSoundDialog: 사운드 설정 다이얼로그
- RetroConfirmDialog: 확인 다이얼로그
- RetroMenuSection, RetroMenuItem, RetroSpeedChip: 메뉴 위젯
This commit is contained in:
JiWoong Sul
2026-01-21 17:33:52 +09:00
parent c577f9deed
commit c5eaecfa6a
5 changed files with 606 additions and 578 deletions

View File

@@ -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,
),
),
],
),
),
],
),
),
);
}
} }

View File

@@ -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,
),
),
],
),
),
],
),
),
);
}
}

View 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),
],
),
),
),
);
}
}

View 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),
),
],
);
}
}

View 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,
),
),
],
),
),
);
}
}