refactor(app): 앱 설정 및 공유 위젯 업데이트

- app.dart: MaterialApp 설정 개선
- retro_panel: 레트로 패널 위젯 수정
This commit is contained in:
JiWoong Sul
2026-01-19 15:50:49 +09:00
parent 19faa9ea39
commit 94c2ed1ca1
2 changed files with 179 additions and 169 deletions

View File

@@ -3,7 +3,9 @@ import 'package:flutter/material.dart';
import 'package:asciineverdie/data/game_text_l10n.dart' as game_l10n; import 'package:asciineverdie/data/game_text_l10n.dart' as game_l10n;
import 'package:asciineverdie/l10n/app_localizations.dart'; import 'package:asciineverdie/l10n/app_localizations.dart';
import 'package:asciineverdie/src/core/audio/audio_service.dart'; import 'package:asciineverdie/src/core/audio/audio_service.dart';
import 'package:asciineverdie/src/core/engine/ad_service.dart';
import 'package:asciineverdie/src/core/engine/debug_settings_service.dart'; import 'package:asciineverdie/src/core/engine/debug_settings_service.dart';
import 'package:asciineverdie/src/core/engine/iap_service.dart';
import 'package:asciineverdie/src/shared/retro_colors.dart'; import 'package:asciineverdie/src/shared/retro_colors.dart';
import 'package:asciineverdie/src/core/engine/game_mutations.dart'; import 'package:asciineverdie/src/core/engine/game_mutations.dart';
import 'package:asciineverdie/src/core/engine/progress_service.dart'; import 'package:asciineverdie/src/core/engine/progress_service.dart';
@@ -46,7 +48,8 @@ class SavedGamePreview {
final String actName; final String actName;
} }
class _AskiiNeverDieAppState extends State<AskiiNeverDieApp> { class _AskiiNeverDieAppState extends State<AskiiNeverDieApp>
with WidgetsBindingObserver {
late final GameSessionController _controller; late final GameSessionController _controller;
late final NotificationService _notificationService; late final NotificationService _notificationService;
late final SettingsRepository _settingsRepository; late final SettingsRepository _settingsRepository;
@@ -57,12 +60,15 @@ class _AskiiNeverDieAppState extends State<AskiiNeverDieApp> {
bool _isCheckingSave = true; bool _isCheckingSave = true;
bool _hasSave = false; bool _hasSave = false;
SavedGamePreview? _savedGamePreview; SavedGamePreview? _savedGamePreview;
ThemeMode _themeMode = ThemeMode.system;
HallOfFame _hallOfFame = HallOfFame.empty(); HallOfFame _hallOfFame = HallOfFame.empty();
Locale? _locale; // 사용자 선택 로케일 (null이면 시스템 기본값)
bool _isAdRemovalPurchased = false;
String? _removeAdsPrice;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
WidgetsBinding.instance.addObserver(this);
const config = PqConfig(); const config = PqConfig();
final mutations = GameMutations(config); final mutations = GameMutations(config);
final rewards = RewardService(mutations, config); final rewards = RewardService(mutations, config);
@@ -83,14 +89,33 @@ class _AskiiNeverDieAppState extends State<AskiiNeverDieApp> {
// 초기 설정 및 오디오 서비스 로드 // 초기 설정 및 오디오 서비스 로드
_loadSettings(); _loadSettings();
_audioService.init(); _audioService.init();
// 디버그 설정 서비스 초기화 (Phase 8) // IAP 서비스 초기화
DebugSettingsService.instance.initialize(); _initIAP();
// 세이브 파일 존재 여부 확인 // 세이브 파일 존재 여부 확인
_checkForExistingSave(); _checkForExistingSave();
// 명예의 전당 로드 // 명예의 전당 로드
_loadHallOfFame(); _loadHallOfFame();
} }
/// IAP 및 광고 서비스 초기화
Future<void> _initIAP() async {
await IAPService.instance.initialize();
await AdService.instance.initialize();
_updateIAPState();
}
/// IAP 상태 업데이트 (구매 여부, 가격)
void _updateIAPState() {
if (mounted) {
setState(() {
_isAdRemovalPurchased = IAPService.instance.isAdRemovalPurchased;
_removeAdsPrice = IAPService.instance.isStoreAvailable
? IAPService.instance.removeAdsPrice
: null;
});
}
}
/// 명예의 전당 로드 /// 명예의 전당 로드
Future<void> _loadHallOfFame() async { Future<void> _loadHallOfFame() async {
final hallOfFame = await _hallOfFameStorage.load(); final hallOfFame = await _hallOfFameStorage.load();
@@ -103,16 +128,26 @@ class _AskiiNeverDieAppState extends State<AskiiNeverDieApp> {
/// 저장된 설정 불러오기 /// 저장된 설정 불러오기
Future<void> _loadSettings() async { Future<void> _loadSettings() async {
final themeMode = await _settingsRepository.loadThemeMode(); // 디버그 설정 먼저 초기화 (광고/IAP 시뮬레이션 설정 동기화)
await DebugSettingsService.instance.initialize();
final localeCode = await _settingsRepository.loadLocale();
if (mounted) { if (mounted) {
setState(() => _themeMode = themeMode); setState(() {
// 저장된 로케일이 있으면 적용
if (localeCode != null) {
_locale = Locale(localeCode);
game_l10n.setGameLocale(localeCode);
}
});
} }
} }
/// 테마 모드 변경 /// 로케일 변경
void _changeThemeMode(ThemeMode mode) { void _changeLocale(String localeCode) {
setState(() => _themeMode = mode); setState(() {
_settingsRepository.saveThemeMode(mode); _locale = Locale(localeCode);
});
} }
/// 세이브 파일 존재 여부 확인 및 미리보기 정보 로드 /// 세이브 파일 존재 여부 확인 및 미리보기 정보 로드
@@ -139,8 +174,11 @@ class _AskiiNeverDieAppState extends State<AskiiNeverDieApp> {
_savedGamePreview = preview; _savedGamePreview = preview;
_isCheckingSave = false; _isCheckingSave = false;
}); });
// 세이브 확인 완료 후 타이틀 BGM 재생 // 세이브 확인 완료 후 타이틀 BGM 재생 (앱이 포그라운드일 때만)
_audioService.playBgm('title'); final lifecycleState = WidgetsBinding.instance.lifecycleState;
if (lifecycleState == AppLifecycleState.resumed) {
_audioService.playBgm('title');
}
} }
} }
@@ -159,148 +197,32 @@ class _AskiiNeverDieAppState extends State<AskiiNeverDieApp> {
@override @override
void dispose() { void dispose() {
WidgetsBinding.instance.removeObserver(this);
_controller.dispose(); _controller.dispose();
_notificationService.dispose(); _notificationService.dispose();
_audioService.dispose(); _audioService.dispose();
super.dispose(); super.dispose();
} }
/// 라이트 테마 (Classic Parchment 스타일) @override
ThemeData get _lightTheme => ThemeData( void didChangeAppLifecycleState(AppLifecycleState state) {
colorScheme: RetroColors.lightColorScheme, super.didChangeAppLifecycleState(state);
scaffoldBackgroundColor: const Color(0xFFFAF4ED), // 앱이 백그라운드로 내려가면 오디오 정지
useMaterial3: true, if (state == AppLifecycleState.paused ||
// 카드/다이얼로그 레트로 배경 state == AppLifecycleState.inactive) {
cardColor: const Color(0xFFF2E8DC), _audioService.pauseAll();
dialogTheme: const DialogThemeData( } else if (state == AppLifecycleState.resumed) {
backgroundColor: Color(0xFFF2E8DC), _audioService.resumeAll().then((_) {
titleTextStyle: TextStyle( // 복귀 후 BGM이 없고 시작 화면이면 타이틀 BGM 재생
fontFamily: 'PressStart2P', if (_audioService.currentBgm == null && !_isCheckingSave) {
fontSize: 15, _audioService.playBgm('title');
color: Color(0xFFB8860B), }
), });
), }
// 앱바 레트로 스타일 }
appBarTheme: const AppBarTheme(
backgroundColor: Color(0xFFF2E8DC),
foregroundColor: Color(0xFF1F1F28),
titleTextStyle: TextStyle(
fontFamily: 'PressStart2P',
fontSize: 15,
color: Color(0xFFB8860B),
),
),
// 버튼 테마
filledButtonTheme: FilledButtonThemeData(
style: FilledButton.styleFrom(
backgroundColor: const Color(0xFFE8DDD0),
foregroundColor: const Color(0xFF1F1F28),
textStyle: const TextStyle(
inherit: false,
fontFamily: 'PressStart2P',
fontSize: 14,
color: Color(0xFF1F1F28),
),
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
foregroundColor: const Color(0xFFB8860B),
side: const BorderSide(color: Color(0xFFB8860B), width: 2),
textStyle: const TextStyle(
inherit: false,
fontFamily: 'PressStart2P',
fontSize: 14,
color: Color(0xFFB8860B),
),
),
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
foregroundColor: const Color(0xFF4A4458),
textStyle: const TextStyle(
inherit: false,
fontFamily: 'PressStart2P',
fontSize: 14,
color: Color(0xFF4A4458),
),
),
),
// 텍스트 테마
textTheme: const TextTheme(
headlineLarge: TextStyle(
fontFamily: 'PressStart2P',
fontSize: 20,
color: Color(0xFFB8860B),
),
headlineMedium: TextStyle(
fontFamily: 'PressStart2P',
fontSize: 16,
color: Color(0xFFB8860B),
),
headlineSmall: TextStyle(
fontFamily: 'PressStart2P',
fontSize: 15,
color: Color(0xFFB8860B),
),
titleLarge: TextStyle(
fontFamily: 'PressStart2P',
fontSize: 15,
color: Color(0xFF1F1F28),
),
titleMedium: TextStyle(
fontFamily: 'PressStart2P',
fontSize: 14,
color: Color(0xFF1F1F28),
),
titleSmall: TextStyle(
fontFamily: 'PressStart2P',
fontSize: 14,
color: Color(0xFF1F1F28),
),
bodyLarge: TextStyle(fontSize: 18, color: Color(0xFF1F1F28)),
bodyMedium: TextStyle(fontSize: 17, color: Color(0xFF1F1F28)),
bodySmall: TextStyle(fontSize: 15, color: Color(0xFF1F1F28)),
labelLarge: TextStyle(
fontFamily: 'PressStart2P',
fontSize: 14,
color: Color(0xFF1F1F28),
),
labelMedium: TextStyle(
fontFamily: 'PressStart2P',
fontSize: 14,
color: Color(0xFF1F1F28),
),
labelSmall: TextStyle(
fontFamily: 'PressStart2P',
fontSize: 13,
color: Color(0xFF1F1F28),
),
),
// 칩 테마
chipTheme: const ChipThemeData(
backgroundColor: Color(0xFFE8DDD0),
labelStyle: TextStyle(
fontFamily: 'PressStart2P',
fontSize: 14,
color: Color(0xFF1F1F28),
),
side: BorderSide(color: Color(0xFF8B7355)),
),
// 리스트 타일 테마
listTileTheme: const ListTileThemeData(
textColor: Color(0xFF1F1F28),
iconColor: Color(0xFFB8860B),
),
// 프로그레스 인디케이터
progressIndicatorTheme: const ProgressIndicatorThemeData(
color: Color(0xFFB8860B),
linearTrackColor: Color(0xFFD4C4B0),
),
);
/// 다크 테마 (Dark Fantasy 스타일) /// 테마 (Dark Fantasy 스타일)
ThemeData get _darkTheme => ThemeData( ThemeData get _theme => ThemeData(
colorScheme: RetroColors.darkColorScheme, colorScheme: RetroColors.darkColorScheme,
scaffoldBackgroundColor: RetroColors.deepBrown, scaffoldBackgroundColor: RetroColors.deepBrown,
useMaterial3: true, useMaterial3: true,
@@ -440,9 +362,8 @@ class _AskiiNeverDieAppState extends State<AskiiNeverDieApp> {
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
localizationsDelegates: L10n.localizationsDelegates, localizationsDelegates: L10n.localizationsDelegates,
supportedLocales: L10n.supportedLocales, supportedLocales: L10n.supportedLocales,
theme: _lightTheme, locale: _locale, // 사용자 선택 로케일 (null이면 시스템 기본값)
darkTheme: _darkTheme, theme: _theme,
themeMode: _themeMode,
navigatorObservers: [_routeObserver], navigatorObservers: [_routeObserver],
builder: (context, child) { builder: (context, child) {
// 현재 로케일을 게임 텍스트 l10n 시스템에 동기화 // 현재 로케일을 게임 텍스트 l10n 시스템에 동기화
@@ -470,13 +391,18 @@ class _AskiiNeverDieAppState extends State<AskiiNeverDieApp> {
onHallOfFame: _navigateToHallOfFame, onHallOfFame: _navigateToHallOfFame,
onLocalArena: _navigateToArena, onLocalArena: _navigateToArena,
onSettings: _showSettings, onSettings: _showSettings,
onPurchaseRemoveAds: _purchaseRemoveAds,
onRestorePurchase: _restorePurchase,
hasSaveFile: _hasSave, hasSaveFile: _hasSave,
savedGamePreview: _savedGamePreview, savedGamePreview: _savedGamePreview,
hallOfFameCount: _hallOfFame.count, hallOfFameCount: _hallOfFame.count,
isAdRemovalPurchased: _isAdRemovalPurchased,
removeAdsPrice: _removeAdsPrice,
routeObserver: _routeObserver, routeObserver: _routeObserver,
onRefresh: () { onRefresh: () {
_checkForExistingSave(); _checkForExistingSave();
_loadHallOfFame(); _loadHallOfFame();
_updateIAPState();
}, },
); );
} }
@@ -551,8 +477,6 @@ class _AskiiNeverDieAppState extends State<AskiiNeverDieApp> {
controller: _controller, controller: _controller,
audioService: _audioService, audioService: _audioService,
forceCarouselLayout: testMode, forceCarouselLayout: testMode,
currentThemeMode: _themeMode,
onThemeModeChange: _changeThemeMode,
), ),
), ),
); );
@@ -568,8 +492,6 @@ class _AskiiNeverDieAppState extends State<AskiiNeverDieApp> {
audioService: _audioService, audioService: _audioService,
// 디버그 모드로 저장된 게임 로드 시 캐로셀 레이아웃 강제 // 디버그 모드로 저장된 게임 로드 시 캐로셀 레이아웃 강제
forceCarouselLayout: _controller.cheatsEnabled, forceCarouselLayout: _controller.cheatsEnabled,
currentThemeMode: _themeMode,
onThemeModeChange: _changeThemeMode,
), ),
), ),
) )
@@ -613,12 +535,60 @@ class _AskiiNeverDieAppState extends State<AskiiNeverDieApp> {
SettingsScreen.show( SettingsScreen.show(
context, context,
settingsRepository: _settingsRepository, settingsRepository: _settingsRepository,
currentThemeMode: _themeMode, onLocaleChange: _changeLocale,
onThemeModeChange: _changeThemeMode,
onBgmVolumeChange: _audioService.setBgmVolume, onBgmVolumeChange: _audioService.setBgmVolume,
onSfxVolumeChange: _audioService.setSfxVolume, onSfxVolumeChange: _audioService.setSfxVolume,
); );
} }
/// 광고 제거 구매
Future<void> _purchaseRemoveAds(BuildContext context) async {
final result = await IAPService.instance.purchaseRemoveAds();
_updateIAPState();
if (!context.mounted) return;
switch (result) {
case IAPResult.success:
case IAPResult.debugSimulated:
_notificationService.showInfo(game_l10n.iapPurchaseSuccess);
case IAPResult.alreadyPurchased:
_notificationService.showInfo(game_l10n.iapAlreadyPurchased);
case IAPResult.cancelled:
// 취소는 무시
break;
case IAPResult.storeUnavailable:
_notificationService.showWarning(game_l10n.iapStoreUnavailable);
case IAPResult.productNotFound:
case IAPResult.failed:
_notificationService.showWarning(game_l10n.iapPurchaseFailed);
}
}
/// 구매 복원
Future<void> _restorePurchase(BuildContext context) async {
final result = await IAPService.instance.restorePurchases();
_updateIAPState();
if (!context.mounted) return;
switch (result) {
case IAPResult.success:
case IAPResult.debugSimulated:
if (_isAdRemovalPurchased) {
_notificationService.showInfo(game_l10n.iapRestoreSuccess);
} else {
_notificationService.showInfo(game_l10n.iapRestoreFailed);
}
case IAPResult.storeUnavailable:
_notificationService.showWarning(game_l10n.iapStoreUnavailable);
case IAPResult.alreadyPurchased:
case IAPResult.cancelled:
case IAPResult.productNotFound:
case IAPResult.failed:
_notificationService.showWarning(game_l10n.iapRestoreFailed);
}
}
} }
/// 스플래시 화면 (세이브 파일 확인 중) - 레트로 스타일 /// 스플래시 화면 (세이브 파일 확인 중) - 레트로 스타일

View File

@@ -14,7 +14,11 @@ class RetroPanel extends StatelessWidget {
this.borderWidth = 3.0, this.borderWidth = 3.0,
this.useGoldBorder = false, this.useGoldBorder = false,
this.title, this.title,
}); this.titleWidget,
}) : assert(
title == null || titleWidget == null,
'title과 titleWidget 중 하나만 사용 가능',
);
/// 패널 내부 컨텐츠 /// 패널 내부 컨텐츠
final Widget child; final Widget child;
@@ -34,6 +38,9 @@ class RetroPanel extends StatelessWidget {
/// 패널 타이틀 (상단에 표시) /// 패널 타이틀 (상단에 표시)
final String? title; final String? title;
/// 커스텀 타이틀 위젯 (title 대신 사용)
final Widget? titleWidget;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final painter = useGoldBorder final painter = useGoldBorder
@@ -46,16 +53,24 @@ class RetroPanel extends StatelessWidget {
fillColor: backgroundColor, fillColor: backgroundColor,
); );
final hasTitle = title != null || titleWidget != null;
return CustomPaint( return CustomPaint(
painter: painter, painter: painter,
child: Padding( child: Padding(
padding: EdgeInsets.all(borderWidth).add(padding), padding: EdgeInsets.all(borderWidth).add(padding),
child: title != null child: hasTitle
? Column( ? Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
_PanelTitle(title: title!, useGoldBorder: useGoldBorder), if (titleWidget != null)
_PanelTitleContainer(
useGoldBorder: useGoldBorder,
child: titleWidget!,
)
else
_PanelTitle(title: title!, useGoldBorder: useGoldBorder),
const SizedBox(height: 8), const SizedBox(height: 8),
Flexible(child: child), Flexible(child: child),
], ],
@@ -73,6 +88,33 @@ class _PanelTitle extends StatelessWidget {
final String title; final String title;
final bool useGoldBorder; final bool useGoldBorder;
@override
Widget build(BuildContext context) {
return _PanelTitleContainer(
useGoldBorder: useGoldBorder,
child: Text(
title.toUpperCase(),
style: TextStyle(
fontFamily: 'PressStart2P',
fontSize: 14,
color: useGoldBorder ? RetroColors.gold : RetroColors.textLight,
letterSpacing: 1,
),
),
);
}
}
/// 패널 타이틀 컨테이너 (커스텀 위젯용)
class _PanelTitleContainer extends StatelessWidget {
const _PanelTitleContainer({
required this.useGoldBorder,
required this.child,
});
final bool useGoldBorder;
final Widget child;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
@@ -90,15 +132,7 @@ class _PanelTitle extends StatelessWidget {
), ),
), ),
), ),
child: Text( child: child,
title.toUpperCase(),
style: TextStyle(
fontFamily: 'PressStart2P',
fontSize: 14,
color: useGoldBorder ? RetroColors.gold : RetroColors.textLight,
letterSpacing: 1,
),
),
); );
} }
} }
@@ -110,11 +144,16 @@ class RetroGoldPanel extends StatelessWidget {
required this.child, required this.child,
this.padding = const EdgeInsets.all(12), this.padding = const EdgeInsets.all(12),
this.title, this.title,
}); this.titleWidget,
}) : assert(
title == null || titleWidget == null,
'title과 titleWidget 중 하나만 사용 가능',
);
final Widget child; final Widget child;
final EdgeInsets padding; final EdgeInsets padding;
final String? title; final String? title;
final Widget? titleWidget;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -122,6 +161,7 @@ class RetroGoldPanel extends StatelessWidget {
useGoldBorder: true, useGoldBorder: true,
padding: padding, padding: padding,
title: title, title: title,
titleWidget: titleWidget,
child: child, child: child,
); );
} }