feat(ui): 게임 화면 및 UI 컴포넌트 개선
- front_screen: 프론트 화면 UI 업데이트 - game_play_screen: 게임 플레이 화면 수정 - game_session_controller: 세션 관리 로직 개선 - mobile_carousel_layout: 모바일 캐러셀 레이아웃 개선 - enhanced_animation_panel: 애니메이션 패널 업데이트 - help_dialog: 도움말 다이얼로그 수정 - return_rewards_dialog: 복귀 보상 다이얼로그 개선 - new_character_screen: 새 캐릭터 화면 수정 - settings_screen: 설정 화면 업데이트
This commit is contained in:
@@ -2,6 +2,7 @@ import 'dart:io' show Platform;
|
||||
|
||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
|
||||
import 'package:asciineverdie/data/game_text_l10n.dart' as game_l10n;
|
||||
import 'package:asciineverdie/l10n/app_localizations.dart';
|
||||
@@ -18,9 +19,13 @@ class FrontScreen extends StatefulWidget {
|
||||
this.onHallOfFame,
|
||||
this.onLocalArena,
|
||||
this.onSettings,
|
||||
this.onPurchaseRemoveAds,
|
||||
this.onRestorePurchase,
|
||||
this.hasSaveFile = false,
|
||||
this.savedGamePreview,
|
||||
this.hallOfFameCount = 0,
|
||||
this.isAdRemovalPurchased = false,
|
||||
this.removeAdsPrice,
|
||||
this.routeObserver,
|
||||
this.onRefresh,
|
||||
});
|
||||
@@ -40,6 +45,12 @@ class FrontScreen extends StatefulWidget {
|
||||
/// "Settings" 버튼 클릭 시 호출 (언어, 테마, 사운드)
|
||||
final void Function(BuildContext context)? onSettings;
|
||||
|
||||
/// "광고 제거" 구매 버튼 클릭 시 호출
|
||||
final Future<void> Function(BuildContext context)? onPurchaseRemoveAds;
|
||||
|
||||
/// "구매 복원" 버튼 클릭 시 호출
|
||||
final Future<void> Function(BuildContext context)? onRestorePurchase;
|
||||
|
||||
/// 세이브 파일 존재 여부 (새 캐릭터 시 경고용)
|
||||
final bool hasSaveFile;
|
||||
|
||||
@@ -49,6 +60,12 @@ class FrontScreen extends StatefulWidget {
|
||||
/// 명예의 전당 캐릭터 수 (아레나 활성화 조건: 2명 이상)
|
||||
final int hallOfFameCount;
|
||||
|
||||
/// 광고 제거 구매 여부
|
||||
final bool isAdRemovalPurchased;
|
||||
|
||||
/// 광고 제거 상품 가격 (null이면 스토어 비활성)
|
||||
final String? removeAdsPrice;
|
||||
|
||||
/// RouteObserver (화면 복귀 시 갱신용)
|
||||
final RouteObserver<ModalRoute<void>>? routeObserver;
|
||||
|
||||
@@ -132,8 +149,6 @@ class _FrontScreenState extends State<FrontScreen> with RouteAware {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const _RetroHeader(),
|
||||
const SizedBox(height: 16),
|
||||
const _AnimationPanel(),
|
||||
const SizedBox(height: 16),
|
||||
_ActionButtons(
|
||||
@@ -154,8 +169,17 @@ class _FrontScreenState extends State<FrontScreen> with RouteAware {
|
||||
onSettings: widget.onSettings != null
|
||||
? () => widget.onSettings!(context)
|
||||
: null,
|
||||
onPurchaseRemoveAds:
|
||||
widget.onPurchaseRemoveAds != null
|
||||
? () => widget.onPurchaseRemoveAds!(context)
|
||||
: null,
|
||||
onRestorePurchase: widget.onRestorePurchase != null
|
||||
? () => widget.onRestorePurchase!(context)
|
||||
: null,
|
||||
savedGamePreview: widget.savedGamePreview,
|
||||
hallOfFameCount: widget.hallOfFameCount,
|
||||
isAdRemovalPurchased: widget.isAdRemovalPurchased,
|
||||
removeAdsPrice: widget.removeAdsPrice,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -172,58 +196,7 @@ class _FrontScreenState extends State<FrontScreen> with RouteAware {
|
||||
}
|
||||
}
|
||||
|
||||
/// 레트로 스타일 헤더 (타이틀 + 태그)
|
||||
class _RetroHeader extends StatelessWidget {
|
||||
const _RetroHeader();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = L10n.of(context);
|
||||
return RetroGoldPanel(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20),
|
||||
child: Column(
|
||||
children: [
|
||||
// 타이틀 (픽셀 폰트)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.auto_awesome, color: RetroColors.gold, size: 20),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
l10n.appTitle,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 13,
|
||||
color: RetroColors.gold,
|
||||
shadows: [
|
||||
Shadow(color: RetroColors.goldDark, offset: Offset(2, 2)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// 태그 (레트로 스타일)
|
||||
Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
_RetroTag(
|
||||
icon: Icons.cloud_off_outlined,
|
||||
label: l10n.tagNoNetwork,
|
||||
),
|
||||
_RetroTag(icon: Icons.timer_outlined, label: l10n.tagIdleRpg),
|
||||
_RetroTag(icon: Icons.storage_rounded, label: l10n.tagLocalSaves),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 애니메이션 패널
|
||||
/// 애니메이션 패널 (금색 테두리 + 아이콘+타이틀)
|
||||
class _AnimationPanel extends StatelessWidget {
|
||||
const _AnimationPanel();
|
||||
|
||||
@@ -238,8 +211,25 @@ class _AnimationPanel extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RetroPanel(
|
||||
title: 'BATTLE',
|
||||
return RetroGoldPanel(
|
||||
titleWidget: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.auto_awesome, color: RetroColors.gold, size: 18),
|
||||
const SizedBox(width: 10),
|
||||
const Text(
|
||||
'ASCII NEVER DIE',
|
||||
style: TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 14,
|
||||
color: RetroColors.gold,
|
||||
shadows: [
|
||||
Shadow(color: RetroColors.goldDark, offset: Offset(2, 2)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: AspectRatio(
|
||||
aspectRatio: _getAspectRatio(),
|
||||
@@ -257,8 +247,12 @@ class _ActionButtons extends StatelessWidget {
|
||||
this.onHallOfFame,
|
||||
this.onLocalArena,
|
||||
this.onSettings,
|
||||
this.onPurchaseRemoveAds,
|
||||
this.onRestorePurchase,
|
||||
this.savedGamePreview,
|
||||
this.hallOfFameCount = 0,
|
||||
this.isAdRemovalPurchased = false,
|
||||
this.removeAdsPrice,
|
||||
});
|
||||
|
||||
final VoidCallback? onNewCharacter;
|
||||
@@ -266,8 +260,12 @@ class _ActionButtons extends StatelessWidget {
|
||||
final VoidCallback? onHallOfFame;
|
||||
final VoidCallback? onLocalArena;
|
||||
final VoidCallback? onSettings;
|
||||
final VoidCallback? onPurchaseRemoveAds;
|
||||
final VoidCallback? onRestorePurchase;
|
||||
final SavedGamePreview? savedGamePreview;
|
||||
final int hallOfFameCount;
|
||||
final bool isAdRemovalPurchased;
|
||||
final String? removeAdsPrice;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -323,6 +321,24 @@ class _ActionButtons extends StatelessWidget {
|
||||
onPressed: onSettings,
|
||||
isPrimary: false,
|
||||
),
|
||||
// IAP 구매 (광고 제거) - 스토어 사용 가능하고 미구매 상태일 때만 표시
|
||||
if (removeAdsPrice != null && !isAdRemovalPurchased) ...[
|
||||
const SizedBox(height: 20),
|
||||
const Divider(color: RetroColors.panelBorderInner, height: 1),
|
||||
const SizedBox(height: 12),
|
||||
_IapPurchaseButton(
|
||||
price: removeAdsPrice!,
|
||||
onPurchase: onPurchaseRemoveAds,
|
||||
onRestore: onRestorePurchase,
|
||||
),
|
||||
],
|
||||
// 이미 구매된 경우 표시
|
||||
if (isAdRemovalPurchased) ...[
|
||||
const SizedBox(height: 20),
|
||||
const Divider(color: RetroColors.panelBorderInner, height: 1),
|
||||
const SizedBox(height: 12),
|
||||
_PurchasedBadge(),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -370,50 +386,322 @@ class _CopyrightFooter extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
child: Text(
|
||||
game_l10n.copyrightText,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 7,
|
||||
color: RetroColors.textDisabled,
|
||||
child: FutureBuilder<PackageInfo>(
|
||||
future: PackageInfo.fromPlatform(),
|
||||
builder: (context, snapshot) {
|
||||
final version = snapshot.data?.version ?? '';
|
||||
final versionSuffix = version.isNotEmpty ? ' v$version' : '';
|
||||
return Text(
|
||||
'${game_l10n.copyrightText}$versionSuffix',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 7,
|
||||
color: RetroColors.textDisabled,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// IAP 구매 버튼 (광고 제거)
|
||||
class _IapPurchaseButton extends StatelessWidget {
|
||||
const _IapPurchaseButton({
|
||||
required this.price,
|
||||
this.onPurchase,
|
||||
this.onRestore,
|
||||
});
|
||||
|
||||
final String price;
|
||||
final VoidCallback? onPurchase;
|
||||
final VoidCallback? onRestore;
|
||||
|
||||
void _showPurchaseDialog(BuildContext context) {
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (dialogContext) => _IapPurchaseDialog(
|
||||
price: price,
|
||||
onPurchase: () {
|
||||
Navigator.pop(dialogContext);
|
||||
onPurchase?.call();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// 구매 버튼 (클릭 시 팝업)
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xFF4A3B2A), Color(0xFF3D2E1F)],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
border: Border.all(color: RetroColors.gold, width: 2),
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () => _showPurchaseDialog(context),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.block, color: RetroColors.gold, size: 24),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
game_l10n.iapRemoveAds,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 12,
|
||||
color: RetroColors.gold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
game_l10n.iapRemoveAdsDesc,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 9,
|
||||
color: RetroColors.textDisabled,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// 화살표 아이콘 (상세 보기)
|
||||
const Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
color: RetroColors.gold,
|
||||
size: 16,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
// 복원 버튼
|
||||
Center(
|
||||
child: TextButton(
|
||||
onPressed: onRestore,
|
||||
child: Text(
|
||||
game_l10n.iapRestorePurchase,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 9,
|
||||
color: RetroColors.textDisabled,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// IAP 구매 팝업 다이얼로그
|
||||
class _IapPurchaseDialog extends StatelessWidget {
|
||||
const _IapPurchaseDialog({required this.price, this.onPurchase});
|
||||
|
||||
final String price;
|
||||
final VoidCallback? onPurchase;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Dialog(
|
||||
backgroundColor: RetroColors.deepBrown,
|
||||
shape: RoundedRectangleBorder(
|
||||
side: const BorderSide(color: RetroColors.gold, width: 2),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 360),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// 타이틀
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.star, color: RetroColors.gold, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
game_l10n.iapBenefitTitle,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 12,
|
||||
color: RetroColors.gold,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Icon(Icons.star, color: RetroColors.gold, size: 20),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
// 혜택 목록
|
||||
_BenefitItem(icon: Icons.block, text: game_l10n.iapBenefit1),
|
||||
const SizedBox(height: 8),
|
||||
_BenefitItem(icon: Icons.flash_on, text: game_l10n.iapBenefit2),
|
||||
const SizedBox(height: 8),
|
||||
_BenefitItem(icon: Icons.undo, text: game_l10n.iapBenefit3),
|
||||
const SizedBox(height: 8),
|
||||
_BenefitItem(icon: Icons.casino, text: game_l10n.iapBenefit4),
|
||||
const SizedBox(height: 8),
|
||||
_BenefitItem(icon: Icons.speed, text: game_l10n.iapBenefit5),
|
||||
const SizedBox(height: 8),
|
||||
_BenefitItem(icon: Icons.inventory_2, text: game_l10n.iapBenefit6),
|
||||
const SizedBox(height: 20),
|
||||
// 가격 + 구매 버튼
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xFF5A4B3A), Color(0xFF4A3B2A)],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
border: Border.all(color: RetroColors.gold, width: 2),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: onPurchase,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
game_l10n.iapPurchaseButton,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 12,
|
||||
color: RetroColors.gold,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: RetroColors.gold,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
price,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 11,
|
||||
color: RetroColors.deepBrown,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// 취소 버튼
|
||||
Center(
|
||||
child: TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(
|
||||
game_l10n.buttonCancel,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 10,
|
||||
color: RetroColors.textDisabled,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 레트로 태그 칩
|
||||
class _RetroTag extends StatelessWidget {
|
||||
const _RetroTag({required this.icon, required this.label});
|
||||
/// 혜택 항목 위젯
|
||||
class _BenefitItem extends StatelessWidget {
|
||||
const _BenefitItem({required this.icon, required this.text});
|
||||
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final String text;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: RetroColors.panelBgLight,
|
||||
border: Border.all(color: RetroColors.panelBorderInner, width: 1),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, color: RetroColors.gold, size: 12),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
label,
|
||||
return Row(
|
||||
children: [
|
||||
Icon(icon, color: RetroColors.expGreen, size: 18),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
text,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 11,
|
||||
fontSize: 10,
|
||||
color: RetroColors.textLight,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 이미 구매됨 뱃지
|
||||
class _PurchasedBadge extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: RetroColors.panelBgLight,
|
||||
border: Border.all(color: RetroColors.expGreen, width: 2),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.check_circle, color: RetroColors.expGreen, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
game_l10n.iapAlreadyPurchased,
|
||||
style: TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 11,
|
||||
color: RetroColors.expGreen,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user