refactor(app): 앱 설정 및 공유 위젯 업데이트
- app.dart: MaterialApp 설정 개선 - retro_panel: 레트로 패널 위젯 수정
This commit is contained in:
280
lib/src/app.dart
280
lib/src/app.dart
@@ -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,10 +174,13 @@ class _AskiiNeverDieAppState extends State<AskiiNeverDieApp> {
|
|||||||
_savedGamePreview = preview;
|
_savedGamePreview = preview;
|
||||||
_isCheckingSave = false;
|
_isCheckingSave = false;
|
||||||
});
|
});
|
||||||
// 세이브 확인 완료 후 타이틀 BGM 재생
|
// 세이브 확인 완료 후 타이틀 BGM 재생 (앱이 포그라운드일 때만)
|
||||||
|
final lifecycleState = WidgetsBinding.instance.lifecycleState;
|
||||||
|
if (lifecycleState == AppLifecycleState.resumed) {
|
||||||
_audioService.playBgm('title');
|
_audioService.playBgm('title');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// plotStageCount를 Act 이름으로 변환
|
/// plotStageCount를 Act 이름으로 변환
|
||||||
String _getActName(int plotStageCount) {
|
String _getActName(int plotStageCount) {
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 스플래시 화면 (세이브 파일 확인 중) - 레트로 스타일
|
/// 스플래시 화면 (세이브 파일 확인 중) - 레트로 스타일
|
||||||
|
|||||||
@@ -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,15 +53,23 @@ 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: [
|
||||||
|
if (titleWidget != null)
|
||||||
|
_PanelTitleContainer(
|
||||||
|
useGoldBorder: useGoldBorder,
|
||||||
|
child: titleWidget!,
|
||||||
|
)
|
||||||
|
else
|
||||||
_PanelTitle(title: title!, useGoldBorder: useGoldBorder),
|
_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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user