feat: adopt material 3 theme and billing adjustments

This commit is contained in:
JiWoong Sul
2025-09-16 14:30:14 +09:00
parent a01d9092ba
commit 44850a53cc
85 changed files with 2957 additions and 2776 deletions

View File

@@ -5,7 +5,7 @@ import '../widgets/add_subscription/add_subscription_header.dart';
import '../widgets/add_subscription/add_subscription_form.dart';
import '../widgets/add_subscription/add_subscription_event_section.dart';
import '../widgets/add_subscription/add_subscription_save_button.dart';
import '../theme/app_colors.dart';
// import '../theme/app_colors.dart';
/// 새로운 구독을 추가하는 화면
class AddSubscriptionScreen extends StatefulWidget {
@@ -45,7 +45,7 @@ class _AddSubscriptionScreenState extends State<AddSubscriptionScreen>
_controller.scrollController.addListener(_onScroll);
return Scaffold(
backgroundColor: AppColors.backgroundColor,
backgroundColor: Theme.of(context).colorScheme.surface,
extendBodyBehindAppBar: true,
appBar: AddSubscriptionAppBar(
controller: _controller,

View File

@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/app_lock_provider.dart';
import '../theme/app_colors.dart';
// import '../theme/app_colors.dart';
class AppLockScreen extends StatelessWidget {
const AppLockScreen({super.key});
@@ -13,26 +13,26 @@ class AppLockScreen extends StatelessWidget {
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icon(
Icons.lock_outline,
size: 80,
color: AppColors.navyGray,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
const SizedBox(height: 24),
const Text(
Text(
'앱이 잠겨 있습니다',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: AppColors.darkNavy,
color: Theme.of(context).colorScheme.onSurface,
),
),
const SizedBox(height: 16),
const Text(
Text(
'생체 인증으로 잠금을 해제하세요',
style: TextStyle(
fontSize: 16,
color: AppColors.navyGray,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 32),
@@ -41,15 +41,16 @@ class AppLockScreen extends StatelessWidget {
final appLock = context.read<AppLockProvider>();
final success = await appLock.authenticate();
if (!success && context.mounted) {
final cs = Theme.of(context).colorScheme;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
SnackBar(
content: Text(
'인증에 실패했습니다. 다시 시도해주세요.',
style: TextStyle(
color: AppColors.pureWhite,
color: cs.onPrimary,
),
),
backgroundColor: AppColors.dangerColor,
backgroundColor: cs.error,
),
);
}

View File

@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/category_provider.dart';
import '../theme/app_colors.dart';
// import '../theme/app_colors.dart';
import '../l10n/app_localizations.dart';
class CategoryManagementScreen extends StatefulWidget {
@@ -43,13 +43,13 @@ class _CategoryManagementScreenState extends State<CategoryManagementScreen> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text(
title: Text(
'카테고리 관리',
style: TextStyle(
color: AppColors.pureWhite,
),
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: Theme.of(context).colorScheme.onPrimary,
),
),
backgroundColor: AppColors.primaryColor,
backgroundColor: Theme.of(context).colorScheme.primary,
),
body: Consumer<CategoryProvider>(
builder: (context, provider, child) {
@@ -66,10 +66,12 @@ class _CategoryManagementScreenState extends State<CategoryManagementScreen> {
children: [
TextFormField(
controller: _nameController,
decoration: const InputDecoration(
decoration: InputDecoration(
labelText: '카테고리 이름',
labelStyle: TextStyle(
color: AppColors.navyGray,
color: Theme.of(context)
.colorScheme
.onSurfaceVariant,
),
),
validator: (value) {
@@ -81,11 +83,13 @@ class _CategoryManagementScreenState extends State<CategoryManagementScreen> {
),
const SizedBox(height: 16),
DropdownButtonFormField<String>(
value: _selectedColor,
decoration: const InputDecoration(
initialValue: _selectedColor,
decoration: InputDecoration(
labelText: '색상 선택',
labelStyle: TextStyle(
color: AppColors.navyGray,
color: Theme.of(context)
.colorScheme
.onSurfaceVariant,
),
),
items: [
@@ -93,32 +97,42 @@ class _CategoryManagementScreenState extends State<CategoryManagementScreen> {
value: '#1976D2',
child: Text(
AppLocalizations.of(context).colorBlue,
style: const TextStyle(
color: AppColors.darkNavy))),
style: TextStyle(
color: Theme.of(context)
.colorScheme
.onSurface))),
DropdownMenuItem(
value: '#4CAF50',
child: Text(
AppLocalizations.of(context).colorGreen,
style: const TextStyle(
color: AppColors.darkNavy))),
style: TextStyle(
color: Theme.of(context)
.colorScheme
.onSurface))),
DropdownMenuItem(
value: '#FF9800',
child: Text(
AppLocalizations.of(context).colorOrange,
style: const TextStyle(
color: AppColors.darkNavy))),
style: TextStyle(
color: Theme.of(context)
.colorScheme
.onSurface))),
DropdownMenuItem(
value: '#F44336',
child: Text(
AppLocalizations.of(context).colorRed,
style: const TextStyle(
color: AppColors.darkNavy))),
style: TextStyle(
color: Theme.of(context)
.colorScheme
.onSurface))),
DropdownMenuItem(
value: '#9C27B0',
child: Text(
AppLocalizations.of(context).colorPurple,
style: const TextStyle(
color: AppColors.darkNavy))),
style: TextStyle(
color: Theme.of(context)
.colorScheme
.onSurface))),
],
onChanged: (value) {
setState(() {
@@ -128,39 +142,51 @@ class _CategoryManagementScreenState extends State<CategoryManagementScreen> {
),
const SizedBox(height: 16),
DropdownButtonFormField<String>(
value: _selectedIcon,
decoration: const InputDecoration(
initialValue: _selectedIcon,
decoration: InputDecoration(
labelText: '아이콘 선택',
labelStyle: TextStyle(
color: AppColors.navyGray,
color: Theme.of(context)
.colorScheme
.onSurfaceVariant,
),
),
items: const [
items: [
DropdownMenuItem(
value: 'subscriptions',
child: Text('구독',
style:
TextStyle(color: AppColors.darkNavy))),
style: TextStyle(
color: Theme.of(context)
.colorScheme
.onSurface))),
DropdownMenuItem(
value: 'movie',
child: Text('영화',
style:
TextStyle(color: AppColors.darkNavy))),
style: TextStyle(
color: Theme.of(context)
.colorScheme
.onSurface))),
DropdownMenuItem(
value: 'music_note',
child: Text('음악',
style:
TextStyle(color: AppColors.darkNavy))),
style: TextStyle(
color: Theme.of(context)
.colorScheme
.onSurface))),
DropdownMenuItem(
value: 'fitness_center',
child: Text('운동',
style:
TextStyle(color: AppColors.darkNavy))),
style: TextStyle(
color: Theme.of(context)
.colorScheme
.onSurface))),
DropdownMenuItem(
value: 'shopping_cart',
child: Text('쇼핑',
style:
TextStyle(color: AppColors.darkNavy))),
style: TextStyle(
color: Theme.of(context)
.colorScheme
.onSurface))),
],
onChanged: (value) {
setState(() {
@@ -171,12 +197,7 @@ class _CategoryManagementScreenState extends State<CategoryManagementScreen> {
const SizedBox(height: 16),
ElevatedButton(
onPressed: _addCategory,
child: const Text(
'카테고리 추가',
style: TextStyle(
color: AppColors.pureWhite,
),
),
child: const Text('카테고리 추가'),
),
],
),
@@ -201,8 +222,8 @@ class _CategoryManagementScreenState extends State<CategoryManagementScreen> {
title: Text(
provider.getLocalizedCategoryName(
context, category.name),
style: const TextStyle(
color: AppColors.darkNavy,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
),
),
trailing: IconButton(

View File

@@ -7,7 +7,7 @@ import '../widgets/detail/detail_form_section.dart';
import '../widgets/detail/detail_event_section.dart';
import '../widgets/detail/detail_url_section.dart';
import '../widgets/detail/detail_action_buttons.dart';
import '../theme/app_colors.dart';
// import '../theme/app_colors.dart';
import '../l10n/app_localizations.dart';
/// 구독 상세 정보를 표시하고 편집할 수 있는 화면
@@ -50,7 +50,7 @@ class _DetailScreenState extends State<DetailScreen>
return ChangeNotifierProvider<DetailScreenController>.value(
value: _controller,
child: Scaffold(
backgroundColor: AppColors.backgroundColor,
backgroundColor: Theme.of(context).colorScheme.surface,
body: CustomScrollView(
controller: _controller.scrollController,
slivers: [
@@ -77,17 +77,16 @@ class _DetailScreenState extends State<DetailScreen>
vertical: 12,
),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
baseColor.withValues(alpha: 0.15),
baseColor.withValues(alpha: 0.08),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
color: Theme.of(context)
.colorScheme
.surfaceContainerHighest
.withValues(alpha: 0.4),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: baseColor.withValues(alpha: 0.2),
color: Theme.of(context)
.colorScheme
.outline
.withValues(alpha: 0.3),
width: 1,
),
),
@@ -111,9 +110,9 @@ class _DetailScreenState extends State<DetailScreen>
Text(
AppLocalizations.of(context)
.changesAppliedAfterSave,
style: const TextStyle(
style: TextStyle(
fontSize: 14,
color: AppColors.darkNavy,
color: Theme.of(context).colorScheme.onSurface,
),
),
],

View File

@@ -3,7 +3,8 @@ import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import '../providers/app_lock_provider.dart';
import '../providers/navigation_provider.dart';
import '../theme/app_colors.dart';
// import '../theme/app_colors.dart';
import '../theme/color_scheme_ext.dart';
import '../routes/app_routes.dart';
import 'analysis_screen.dart';
import 'app_lock_screen.dart';
@@ -11,7 +12,6 @@ import 'settings_screen.dart';
import 'sms_scan_screen.dart';
import '../utils/animation_controller_helper.dart';
import '../widgets/floating_navigation_bar.dart';
import '../widgets/glassmorphic_scaffold.dart';
import '../widgets/home_content.dart';
import '../l10n/app_localizations.dart';
import '../utils/platform_helper.dart';
@@ -162,33 +162,34 @@ class _MainScreenState extends State<MainScreen>
if (result == true) {
// 상단에 스낵바 표시
if (!context.mounted) return;
final cs = Theme.of(context).colorScheme;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
const Icon(
Icon(
Icons.check_circle,
color: AppColors.pureWhite,
color: cs.onPrimary,
size: 20,
),
const SizedBox(width: 12),
Text(
AppLocalizations.of(context).subscriptionAdded,
style: const TextStyle(
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: AppColors.pureWhite,
color: cs.onPrimary,
),
),
],
),
backgroundColor: AppColors.successColor,
backgroundColor: cs.success,
behavior: SnackBarBehavior.floating,
margin: EdgeInsets.only(
top: MediaQuery.of(context).padding.top + 8, // 더 상단으로
top: MediaQuery.of(context).padding.top + 8,
left: 16,
right: 16,
bottom: MediaQuery.of(context).size.height - 100, // 더 상단으로
bottom: MediaQuery.of(context).size.height - 100,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
@@ -223,8 +224,7 @@ class _MainScreenState extends State<MainScreen>
Widget build(BuildContext context) {
final navigationProvider = context.watch<NavigationProvider>();
// 메인 그라데이션 사용
List<Color> backgroundGradient = AppColors.mainGradient;
// 그라데이션 제거: 단색 배경 사용
// 현재 인덱스가 유효한지 확인
int currentIndex = navigationProvider.currentIndex;
@@ -232,25 +232,31 @@ class _MainScreenState extends State<MainScreen>
currentIndex = 0; // 추가 버튼은 홈으로 표시
}
return GlassmorphicScaffold(
body: IndexedStack(
index: PlatformHelper.isIOS
? (currentIndex == 3 ? 3 : currentIndex) // iOS: 설정화면은 인덱스 3
: (currentIndex == 3
? 3
: currentIndex == 4
? 4
: currentIndex), // Android: 기존 로직
children: _screens,
),
backgroundGradient: backgroundGradient,
useFloatingNavBar: true,
floatingNavBarIndex: navigationProvider.currentIndex,
onFloatingNavBarTapped: (index) {
_handleNavigation(index, context);
},
enableParticles: false,
enableWaveAnimation: false,
return Stack(
children: [
Positioned.fill(
child: Container(color: Theme.of(context).colorScheme.surface),
),
Scaffold(
extendBody: true,
extendBodyBehindAppBar: true,
body: IndexedStack(
index: PlatformHelper.isIOS
? (currentIndex == 3 ? 3 : currentIndex) // iOS: 설정화면은 인덱스 3
: (currentIndex == 3
? 3
: currentIndex == 4
? 4
: currentIndex), // Android: 기존 로직
children: _screens,
),
),
FloatingNavigationBar(
selectedIndex: navigationProvider.currentIndex,
isVisible: true,
onItemTapped: (index) => _handleNavigation(index, context),
),
],
);
}
}

View File

@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart' as permission;
import '../theme/app_colors.dart';
import '../widgets/glassmorphism_card.dart';
// Material colors only
// Glass 제거: Material 3 Card 사용
import '../routes/app_routes.dart';
import '../l10n/app_localizations.dart';
import '../services/sms_service.dart';
@@ -92,12 +92,13 @@ class _SmsPermissionScreenState extends State<SmsPermissionScreen> {
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.sms, size: 64, color: AppColors.primaryColor),
Icon(Icons.sms,
size: 64, color: Theme.of(context).colorScheme.primary),
const SizedBox(height: 16),
Text(
loc.smsPermissionTitle,
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
color: AppColors.textPrimary,
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.bold,
),
),
@@ -105,24 +106,39 @@ class _SmsPermissionScreenState extends State<SmsPermissionScreen> {
Text(
loc.smsPermissionRequired,
textAlign: TextAlign.center,
style: const TextStyle(color: AppColors.textSecondary),
style: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant),
),
const SizedBox(height: 16),
GlassmorphismCard(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(loc.smsPermissionReasonTitle,
style: const TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
Text(loc.smsPermissionReasonBody),
const SizedBox(height: 12),
Text(loc.smsPermissionScopeTitle,
style: const TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
Text(loc.smsPermissionScopeBody),
],
Card(
elevation: 1,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(
color: Theme.of(context)
.colorScheme
.outline
.withValues(alpha: 0.5),
),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(loc.smsPermissionReasonTitle,
style:
const TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
Text(loc.smsPermissionReasonBody),
const SizedBox(height: 12),
Text(loc.smsPermissionScopeTitle,
style:
const TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
Text(loc.smsPermissionScopeBody),
],
),
),
),
const SizedBox(height: 24),

View File

@@ -1,7 +1,7 @@
import 'dart:async';
import 'dart:ui';
import 'package:flutter/material.dart';
import '../theme/app_colors.dart';
// import '../theme/app_colors.dart';
import '../services/sms_service.dart';
import '../utils/platform_helper.dart';
import '../routes/app_routes.dart';
@@ -90,8 +90,6 @@ class _SplashScreenState extends State<SplashScreen>
(reduced ? 1200 : 2000); // 축소 시 더 짧게
final delay = (random % 10) / 10 * 2000; // 0-2초 사이의 지연시간
int colorIndex = (random + i) % AppColors.blueGradient.length;
_particles.add({
'size': size,
'x': x,
@@ -99,7 +97,7 @@ class _SplashScreenState extends State<SplashScreen>
'opacity': opacity,
'duration': duration,
'delay': delay,
'color': AppColors.blueGradient[colorIndex],
// color computed at render from ColorScheme.primary
});
}
}
@@ -137,23 +135,15 @@ class _SplashScreenState extends State<SplashScreen>
return Scaffold(
body: Stack(
children: [
// 배경 그라디언트
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
AppColors.dayGradient[0],
AppColors.dayGradient[1],
],
),
),
),
// 단색 배경
Container(color: Theme.of(context).colorScheme.surface),
// 글래스모피즘 오버레이
Container(
decoration: BoxDecoration(
color: AppColors.pureWhite.withValues(alpha: 0.05),
color: Theme.of(context)
.colorScheme
.onSurface
.withValues(alpha: 0.05),
),
),
Stack(
@@ -180,11 +170,14 @@ class _SplashScreenState extends State<SplashScreen>
width: particle['size'],
height: particle['size'],
decoration: BoxDecoration(
color: particle['color'],
color: Theme.of(context).colorScheme.primary,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: particle['color'].withValues(alpha: 0.3),
color: Theme.of(context)
.colorScheme
.primary
.withValues(alpha: 0.3),
blurRadius: 10,
spreadRadius: 1,
),
@@ -195,43 +188,23 @@ class _SplashScreenState extends State<SplashScreen>
);
}).toList(),
// 상단 원형 그라데이션
// 상단 원형 장식 제거(단색 배경 유지)
Positioned(
top: -size.height * 0.2,
right: -size.width * 0.2,
child: Container(
child: SizedBox(
width: size.width * 0.8,
height: size.width * 0.8,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: RadialGradient(
colors: [
AppColors.pureWhite.withValues(alpha: 0.1),
AppColors.pureWhite.withValues(alpha: 0.0),
],
stops: const [0.2, 1.0],
),
),
),
),
// 하단 원형 그라데이션
// 하단 원형 장식 제거
Positioned(
bottom: -size.height * 0.1,
left: -size.width * 0.3,
child: Container(
child: SizedBox(
width: size.width * 0.9,
height: size.width * 0.9,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: RadialGradient(
colors: [
AppColors.pureWhite.withValues(alpha: 0.07),
AppColors.pureWhite.withValues(alpha: 0.0),
],
stops: const [0.4, 1.0],
),
),
),
),
@@ -271,62 +244,32 @@ class _SplashScreenState extends State<SplashScreen>
reduced: 8)),
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
AppColors.pureWhite
.withValues(alpha: 0.2),
AppColors.pureWhite
.withValues(alpha: 0.1),
],
),
color: Theme.of(context)
.colorScheme
.surface
.withValues(alpha: 0.6),
borderRadius:
BorderRadius.circular(30),
border: Border.all(
color: AppColors.pureWhite
.withValues(alpha: 0.3),
color: Theme.of(context)
.colorScheme
.outline
.withValues(alpha: 0.2),
width: 1.5,
),
boxShadow: [
BoxShadow(
color:
AppColors.shadowBlack,
spreadRadius: 0,
blurRadius:
ReduceMotion.scale(
context,
normal: 30,
reduced: 12),
offset: const Offset(0, 10),
),
],
),
child: Center(
child: AnimatedBuilder(
animation:
_animationController,
builder: (context, _) {
return ShaderMask(
blendMode:
BlendMode.srcIn,
shaderCallback: (bounds) =>
const LinearGradient(
colors: AppColors
.blueGradient,
begin:
Alignment.topLeft,
end: Alignment
.bottomRight,
).createShader(bounds),
child: Icon(
Icons
.subscriptions_outlined,
size: 64,
color:
Theme.of(context)
.primaryColor,
),
return Icon(
Icons
.subscriptions_outlined,
size: 64,
color: Theme.of(context)
.colorScheme
.primary,
);
}),
),
@@ -356,7 +299,9 @@ class _SplashScreenState extends State<SplashScreen>
style: TextStyle(
fontSize: 36,
fontWeight: FontWeight.bold,
color: AppColors.primaryColor
color: Theme.of(context)
.colorScheme
.primary
.withValues(alpha: 0.9),
letterSpacing: 1.2,
),
@@ -382,7 +327,9 @@ class _SplashScreenState extends State<SplashScreen>
AppLocalizations.of(context).appSubtitle,
style: TextStyle(
fontSize: 16,
color: AppColors.primaryColor
color: Theme.of(context)
.colorScheme
.primary
.withValues(alpha: 0.7),
letterSpacing: 0.5,
),
@@ -404,18 +351,22 @@ class _SplashScreenState extends State<SplashScreen>
height: 60,
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: AppColors.pureWhite
color: Theme.of(context)
.colorScheme
.onSurface
.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(50),
border: Border.all(
color: AppColors.pureWhite
color: Theme.of(context)
.colorScheme
.onSurface
.withValues(alpha: 0.2),
width: 1,
),
),
child: const CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(
AppColors.pureWhite),
child: CircularProgressIndicator(
color:
Theme.of(context).colorScheme.primary,
strokeWidth: 3,
),
),
@@ -436,7 +387,10 @@ class _SplashScreenState extends State<SplashScreen>
'© 2025 NatureBridgeAI. All rights reserved.',
style: TextStyle(
fontSize: 12,
color: AppColors.pureWhite.withValues(alpha: 0.6),
color: Theme.of(context)
.colorScheme
.onSurface
.withValues(alpha: 0.6),
letterSpacing: 0.5,
),
),