import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:asciineverdie/data/game_text_l10n.dart' as game_l10n; import 'package:asciineverdie/src/core/storage/settings_repository.dart'; /// 통합 설정 화면 /// /// 언어, 테마, 사운드, 애니메이션 속도 등 모든 설정을 한 곳에서 관리 class SettingsScreen extends StatefulWidget { const SettingsScreen({ super.key, required this.settingsRepository, required this.currentThemeMode, required this.onThemeModeChange, this.onLocaleChange, this.onBgmVolumeChange, this.onSfxVolumeChange, this.onCreateTestCharacter, }); final SettingsRepository settingsRepository; final ThemeMode currentThemeMode; final void Function(ThemeMode mode) onThemeModeChange; final void Function(String locale)? onLocaleChange; /// BGM 볼륨 변경 콜백 (AudioService 연동용) final void Function(double volume)? onBgmVolumeChange; /// SFX 볼륨 변경 콜백 (AudioService 연동용) final void Function(double volume)? onSfxVolumeChange; /// 테스트 캐릭터 생성 콜백 (디버그 모드 전용) /// /// 현재 캐릭터를 레벨 100으로 만들어 명예의 전당에 등록하고 세이브 삭제 final Future Function()? onCreateTestCharacter; @override State createState() => _SettingsScreenState(); /// 설정 화면을 모달 바텀시트로 표시 static Future show( BuildContext context, { required SettingsRepository settingsRepository, required ThemeMode currentThemeMode, required void Function(ThemeMode mode) onThemeModeChange, void Function(String locale)? onLocaleChange, void Function(double volume)? onBgmVolumeChange, void Function(double volume)? onSfxVolumeChange, Future Function()? onCreateTestCharacter, }) { return showModalBottomSheet( context: context, isScrollControlled: true, useSafeArea: true, builder: (context) => DraggableScrollableSheet( initialChildSize: 0.7, minChildSize: 0.5, maxChildSize: 0.95, expand: false, builder: (context, scrollController) => SettingsScreen( settingsRepository: settingsRepository, currentThemeMode: currentThemeMode, onThemeModeChange: onThemeModeChange, onLocaleChange: onLocaleChange, onBgmVolumeChange: onBgmVolumeChange, onSfxVolumeChange: onSfxVolumeChange, onCreateTestCharacter: onCreateTestCharacter, ), ), ); } } class _SettingsScreenState extends State { double _bgmVolume = 0.7; double _sfxVolume = 0.8; double _animationSpeed = 1.0; bool _isLoading = true; @override void initState() { super.initState(); _loadSettings(); } Future _loadSettings() async { final bgm = await widget.settingsRepository.loadBgmVolume(); final sfx = await widget.settingsRepository.loadSfxVolume(); final speed = await widget.settingsRepository.loadAnimationSpeed(); if (mounted) { setState(() { _bgmVolume = bgm; _sfxVolume = sfx; _animationSpeed = speed; _isLoading = false; }); } } @override Widget build(BuildContext context) { final theme = Theme.of(context); if (_isLoading) { return const Center(child: CircularProgressIndicator()); } return Container( decoration: BoxDecoration( color: theme.scaffoldBackgroundColor, borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), ), child: Column( children: [ // 핸들 바 Container( margin: const EdgeInsets.only(top: 8), width: 40, height: 4, decoration: BoxDecoration( color: theme.colorScheme.onSurface.withValues(alpha: 0.3), borderRadius: BorderRadius.circular(2), ), ), // 헤더 Padding( padding: const EdgeInsets.all(16), child: Row( children: [ Icon(Icons.settings, color: theme.colorScheme.primary), const SizedBox(width: 8), Text(game_l10n.uiSettings, style: theme.textTheme.titleLarge), const Spacer(), IconButton( icon: const Icon(Icons.close), onPressed: () => Navigator.of(context).pop(), ), ], ), ), const Divider(height: 1), // 설정 목록 Expanded( child: ListView( padding: const EdgeInsets.all(16), children: [ // 테마 설정 _buildSectionTitle(game_l10n.uiTheme), _buildThemeSelector(), const SizedBox(height: 24), // 언어 설정 _buildSectionTitle(game_l10n.uiLanguage), _buildLanguageSelector(), const SizedBox(height: 24), // 사운드 설정 _buildSectionTitle(game_l10n.uiSound), _buildVolumeSlider( label: game_l10n.uiBgmVolume, value: _bgmVolume, icon: Icons.music_note, onChanged: (value) { setState(() => _bgmVolume = value); widget.settingsRepository.saveBgmVolume(value); widget.onBgmVolumeChange?.call(value); }, ), const SizedBox(height: 8), _buildVolumeSlider( label: game_l10n.uiSfxVolume, value: _sfxVolume, icon: Icons.volume_up, onChanged: (value) { setState(() => _sfxVolume = value); widget.settingsRepository.saveSfxVolume(value); widget.onSfxVolumeChange?.call(value); }, ), const SizedBox(height: 24), // 애니메이션 속도 _buildSectionTitle(game_l10n.uiAnimationSpeed), _buildAnimationSpeedSlider(), const SizedBox(height: 24), // 정보 _buildSectionTitle(game_l10n.uiAbout), _buildAboutCard(), // 디버그 섹션 (디버그 모드에서만 표시) if (kDebugMode && widget.onCreateTestCharacter != null) ...[ const SizedBox(height: 24), _buildSectionTitle('Debug'), _buildDebugSection(), ], ], ), ), ], ), ); } Widget _buildDebugSection() { return Card( color: Theme.of(context).colorScheme.errorContainer.withValues(alpha: 0.3), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon( Icons.bug_report, color: Theme.of(context).colorScheme.error, ), const SizedBox(width: 8), Text( 'Developer Tools', style: Theme.of(context).textTheme.titleSmall?.copyWith( color: Theme.of(context).colorScheme.error, ), ), ], ), const SizedBox(height: 12), Text( '현재 캐릭터를 레벨 100으로 수정하여 명예의 전당에 등록합니다. ' '등록 후 현재 세이브 파일이 삭제됩니다.', style: Theme.of(context).textTheme.bodySmall, ), const SizedBox(height: 12), SizedBox( width: double.infinity, child: ElevatedButton.icon( onPressed: _handleCreateTestCharacter, icon: const Icon(Icons.science), label: const Text('Create Test Character'), style: ElevatedButton.styleFrom( backgroundColor: Theme.of(context).colorScheme.error, foregroundColor: Theme.of(context).colorScheme.onError, ), ), ), ], ), ), ); } Future _handleCreateTestCharacter() async { // 확인 다이얼로그 표시 final confirmed = await showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Create Test Character?'), content: const Text( '현재 캐릭터가 레벨 100으로 변환되어 명예의 전당에 등록됩니다.\n\n' '⚠️ 현재 세이브 파일이 삭제됩니다.\n' '이 작업은 되돌릴 수 없습니다.', ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(false), child: const Text('Cancel'), ), ElevatedButton( onPressed: () => Navigator.of(context).pop(true), style: ElevatedButton.styleFrom( backgroundColor: Theme.of(context).colorScheme.error, foregroundColor: Theme.of(context).colorScheme.onError, ), child: const Text('Create'), ), ], ), ); if (confirmed == true && mounted) { await widget.onCreateTestCharacter?.call(); if (mounted) { Navigator.of(context).pop(); // 설정 화면 닫기 } } } Widget _buildSectionTitle(String title) { return Padding( padding: const EdgeInsets.only(bottom: 8), child: Text( title, style: Theme.of( context, ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold), ), ); } Widget _buildThemeSelector() { return Card( child: Padding( padding: const EdgeInsets.all(8), child: Row( children: [ _buildThemeOption( icon: Icons.light_mode, label: game_l10n.uiThemeLight, mode: ThemeMode.light, ), _buildThemeOption( icon: Icons.dark_mode, label: game_l10n.uiThemeDark, mode: ThemeMode.dark, ), _buildThemeOption( icon: Icons.brightness_auto, label: game_l10n.uiThemeSystem, mode: ThemeMode.system, ), ], ), ), ); } Widget _buildThemeOption({ required IconData icon, required String label, required ThemeMode mode, }) { final isSelected = widget.currentThemeMode == mode; final theme = Theme.of(context); return Expanded( child: InkWell( onTap: () => widget.onThemeModeChange(mode), borderRadius: BorderRadius.circular(8), child: Container( padding: const EdgeInsets.symmetric(vertical: 12), decoration: BoxDecoration( color: isSelected ? theme.colorScheme.primaryContainer : Colors.transparent, borderRadius: BorderRadius.circular(8), ), child: Column( children: [ Icon( icon, color: isSelected ? theme.colorScheme.onPrimaryContainer : theme.colorScheme.onSurface, ), const SizedBox(height: 4), Text( label, style: TextStyle( fontSize: 12, fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, color: isSelected ? theme.colorScheme.onPrimaryContainer : theme.colorScheme.onSurface, ), ), ], ), ), ), ); } Widget _buildLanguageSelector() { final currentLocale = game_l10n.currentGameLocale; final languages = [ ('en', 'English', '🇺🇸'), ('ko', '한국어', '🇰🇷'), ('ja', '日本語', '🇯🇵'), ]; return Card( child: Column( children: languages.map((lang) { final isSelected = currentLocale == lang.$1; return ListTile( leading: Text(lang.$3, style: const TextStyle(fontSize: 24)), title: Text(lang.$2), trailing: isSelected ? Icon( Icons.check, color: Theme.of(context).colorScheme.primary, ) : null, onTap: () { game_l10n.setGameLocale(lang.$1); widget.settingsRepository.saveLocale(lang.$1); widget.onLocaleChange?.call(lang.$1); setState(() {}); }, ); }).toList(), ), ); } Widget _buildVolumeSlider({ required String label, required double value, required IconData icon, required void Function(double) onChanged, }) { final theme = Theme.of(context); final percentage = (value * 100).round(); return Card( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Row( children: [ Icon( value == 0 ? Icons.volume_off : icon, color: theme.colorScheme.primary, ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [Text(label), Text('$percentage%')], ), Slider(value: value, onChanged: onChanged, divisions: 10), ], ), ), ], ), ), ); } Widget _buildAnimationSpeedSlider() { final theme = Theme.of(context); final speedLabel = switch (_animationSpeed) { <= 0.6 => game_l10n.uiSpeedSlow, >= 1.4 => game_l10n.uiSpeedFast, _ => game_l10n.uiSpeedNormal, }; return Card( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Row( children: [ Icon(Icons.speed, color: theme.colorScheme.primary), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(game_l10n.uiAnimationSpeed), Text(speedLabel), ], ), Slider( value: _animationSpeed, min: 0.5, max: 2.0, divisions: 6, onChanged: (value) { setState(() => _animationSpeed = value); widget.settingsRepository.saveAnimationSpeed(value); }, ), ], ), ), ], ), ), ); } Widget _buildAboutCard() { return Card( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'ASCII NEVER DIE', style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: 8), Text( game_l10n.uiAboutDescription, style: Theme.of(context).textTheme.bodySmall, ), const SizedBox(height: 8), Text( 'v1.0.0', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of(context).colorScheme.outline, ), ), ], ), ), ); } }