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

@@ -4,7 +4,7 @@ import 'package:provider/provider.dart';
import '../../models/subscription_model.dart';
import '../../services/currency_util.dart';
import '../../providers/locale_provider.dart';
import '../../theme/app_colors.dart';
// import '../../theme/app_colors.dart';
/// 파이 차트에서 선택된 섹션에 표시되는 배지 위젯
class AnalysisBadge extends StatelessWidget {
@@ -26,15 +26,15 @@ class AnalysisBadge extends StatelessWidget {
width: size,
height: size,
decoration: BoxDecoration(
color: AppColors.pureWhite,
color: Theme.of(context).colorScheme.surface,
shape: BoxShape.circle,
border: Border.all(
color: borderColor,
width: 2,
),
boxShadow: const [
boxShadow: [
BoxShadow(
color: AppColors.shadowBlack,
color: Colors.black.withValues(alpha: 0.08),
blurRadius: 10,
spreadRadius: 2,
),
@@ -48,10 +48,10 @@ class AnalysisBadge extends StatelessWidget {
subscription.serviceName.length > 5
? '${subscription.serviceName.substring(0, 5)}...'
: subscription.serviceName,
style: const TextStyle(
style: TextStyle(
fontSize: 8,
fontWeight: FontWeight.bold,
color: AppColors.darkNavy,
color: Theme.of(context).colorScheme.onSurface,
),
),
const SizedBox(height: 0),
@@ -82,9 +82,9 @@ class AnalysisBadge extends StatelessWidget {
}
return Text(
displayText,
style: const TextStyle(
style: TextStyle(
fontSize: 7,
color: AppColors.navyGray,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
);
}

View File

@@ -3,10 +3,10 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:provider/provider.dart';
import '../../providers/subscription_provider.dart';
import '../../services/currency_util.dart';
import '../../theme/app_colors.dart';
import '../glassmorphism_card.dart';
// Glass 제거: Material 3 Card 사용
import '../themed_text.dart';
import '../../l10n/app_localizations.dart';
import '../../theme/color_scheme_ext.dart';
/// 이벤트 할인 현황을 보여주는 카드 위젯
class EventAnalysisCard extends StatelessWidget {
@@ -38,10 +38,17 @@ class EventAnalysisCard extends StatelessWidget {
parent: animationController,
curve: const Interval(0.6, 1.0, curve: Curves.easeOut),
)),
child: GlassmorphismCard(
blur: 10,
opacity: 0.1,
borderRadius: 16,
child: Card(
elevation: 3,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: BorderSide(
color: Theme.of(context)
.colorScheme
.outline
.withValues(alpha: 0.5),
),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
@@ -64,20 +71,18 @@ class EventAnalysisCard extends StatelessWidget {
vertical: 4,
),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [
Color(0xFFFF6B6B),
Color(0xFFFE7E7E),
],
),
color:
Theme.of(context).colorScheme.error,
borderRadius: BorderRadius.circular(4),
),
child: Row(
children: [
const FaIcon(
FaIcon(
FontAwesomeIcons.fire,
size: 12,
color: AppColors.pureWhite,
color: Theme.of(context)
.colorScheme
.onError,
),
const SizedBox(width: 4),
Text(
@@ -85,10 +90,12 @@ class EventAnalysisCard extends StatelessWidget {
.servicesInProgress(provider
.activeEventSubscriptions
.length),
style: const TextStyle(
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: AppColors.pureWhite,
color: Theme.of(context)
.colorScheme
.onError,
),
),
],
@@ -100,27 +107,24 @@ class EventAnalysisCard extends StatelessWidget {
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
const Color(0xFFFF6B6B)
.withValues(alpha: 0.1),
const Color(0xFFFF8787)
.withValues(alpha: 0.1),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
color: Theme.of(context)
.colorScheme
.error
.withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: const Color(0xFFFF6B6B)
color: Theme.of(context)
.colorScheme
.error
.withValues(alpha: 0.3),
),
),
child: Row(
children: [
const Icon(
Icon(
Icons.savings,
color: Color(0xFFFF6B6B),
color:
Theme.of(context).colorScheme.error,
size: 32,
),
const SizedBox(width: 12),
@@ -142,10 +146,12 @@ class EventAnalysisCard extends StatelessWidget {
CurrencyUtil.formatTotalAmount(
provider.calculateTotalSavings(),
),
style: const TextStyle(
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Color(0xFFFF6B6B),
color: Theme.of(context)
.colorScheme
.error,
),
),
],
@@ -173,12 +179,16 @@ class EventAnalysisCard extends StatelessWidget {
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppColors.darkNavy
color: Theme.of(context)
.colorScheme
.onSurface
.withValues(alpha: 0.05),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: AppColors.darkNavy
.withValues(alpha: 0.1),
color: Theme.of(context)
.colorScheme
.onSurfaceVariant
.withValues(alpha: 0.2),
),
),
child: Row(
@@ -207,13 +217,15 @@ class EventAnalysisCard extends StatelessWidget {
if (snapshot.hasData) {
return ThemedText(
snapshot.data!,
style: const TextStyle(
style: TextStyle(
fontSize: 12,
decoration:
TextDecoration
.lineThrough,
color: AppColors
.navyGray,
color: Theme.of(
context)
.colorScheme
.onSurfaceVariant,
),
);
}
@@ -221,10 +233,12 @@ class EventAnalysisCard extends StatelessWidget {
},
),
const SizedBox(width: 8),
const Icon(
Icon(
Icons.arrow_forward,
size: 12,
color: AppColors.navyGray,
color: Theme.of(context)
.colorScheme
.onSurfaceVariant,
),
const SizedBox(width: 8),
FutureBuilder<String>(
@@ -237,12 +251,14 @@ class EventAnalysisCard extends StatelessWidget {
if (snapshot.hasData) {
return ThemedText(
snapshot.data!,
style: const TextStyle(
style: TextStyle(
fontSize: 12,
fontWeight:
FontWeight.bold,
color:
Color(0xFF10B981),
Theme.of(context)
.colorScheme
.success,
),
);
}
@@ -260,17 +276,22 @@ class EventAnalysisCard extends StatelessWidget {
vertical: 4,
),
decoration: BoxDecoration(
color: const Color(0xFFFF6B6B)
color: Theme.of(context)
.colorScheme
.error
.withValues(alpha: 0.2),
borderRadius:
BorderRadius.circular(4),
),
child: Text(
'$discountRate${AppLocalizations.of(context).discountPercent}',
style: const TextStyle(
_formatDiscountPercent(
context, discountRate),
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Color(0xFFFF6B6B),
color: Theme.of(context)
.colorScheme
.error,
),
),
),
@@ -291,3 +312,17 @@ class EventAnalysisCard extends StatelessWidget {
);
}
}
String _formatDiscountPercent(BuildContext context, int percent) {
final raw = AppLocalizations.of(context).discountPercent;
// 우선 @ 플레이스홀더가 있으면 치환
if (raw.contains('@')) {
return raw.replaceAll('@', percent.toString());
}
// % 마커가 있으면 첫 번째 %를 숫자%로 치환
if (raw.contains('%')) {
return raw.replaceFirst('%', '$percent%');
}
// 폴백: "99% text" 형태
return '$percent% $raw';
}

View File

@@ -1,11 +1,11 @@
import 'package:flutter/material.dart';
import '../../theme/color_scheme_ext.dart';
import 'package:fl_chart/fl_chart.dart';
import 'dart:math' as math;
import 'package:provider/provider.dart';
import '../../services/currency_util.dart';
import '../../providers/locale_provider.dart';
import '../../theme/app_colors.dart';
import '../glassmorphism_card.dart';
// Glass 제거: Material 3 Card 사용
import '../themed_text.dart';
import '../../l10n/app_localizations.dart';
import '../../utils/reduce_motion.dart';
@@ -75,11 +75,13 @@ class MonthlyExpenseChartCard extends StatelessWidget {
}
// 월간 지출 차트 데이터
List<BarChartGroupData> _getMonthlyBarGroups(String locale) {
List<BarChartGroupData> _getMonthlyBarGroups(
BuildContext context, String locale) {
final List<BarChartGroupData> barGroups = [];
final calculatedMax = monthlyData.fold<double>(
0, (max, data) => math.max(max, data['totalExpense'] as double));
final maxAmount = _calculateChartMaxY(calculatedMax, locale);
final scheme = Theme.of(context).colorScheme;
for (int i = 0; i < monthlyData.length; i++) {
final data = monthlyData[i];
@@ -89,20 +91,13 @@ class MonthlyExpenseChartCard extends StatelessWidget {
barRods: [
BarChartRodData(
toY: data['totalExpense'],
gradient: LinearGradient(
colors: [
const Color(0xFF3B82F6).withValues(alpha: 0.7),
const Color(0xFF60A5FA),
],
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
),
color: scheme.primary,
width: 18,
borderRadius: BorderRadius.circular(4),
backDrawRodData: BackgroundBarChartRodData(
show: true,
toY: maxAmount,
color: AppColors.navyGray.withValues(alpha: 0.1),
color: scheme.onSurfaceVariant.withValues(alpha: 0.08),
),
),
],
@@ -132,10 +127,17 @@ class MonthlyExpenseChartCard extends StatelessWidget {
parent: animationController,
curve: const Interval(0.4, 0.9, curve: Curves.easeOut),
)),
child: GlassmorphismCard(
blur: 10,
opacity: 0.1,
borderRadius: 16,
child: Card(
elevation: 3,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: BorderSide(
color: Theme.of(context)
.colorScheme
.outline
.withValues(alpha: 0.5),
),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
@@ -168,7 +170,7 @@ class MonthlyExpenseChartCard extends StatelessWidget {
(max, data) => math.max(
max, data['totalExpense'] as double)),
locale),
barGroups: _getMonthlyBarGroups(locale),
barGroups: _getMonthlyBarGroups(context, locale),
gridData: FlGridData(
show: true,
drawVerticalLine: false,
@@ -182,8 +184,10 @@ class MonthlyExpenseChartCard extends StatelessWidget {
CurrencyUtil.getDefaultCurrency(locale)),
getDrawingHorizontalLine: (value) {
return FlLine(
color:
AppColors.navyGray.withValues(alpha: 0.1),
color: Theme.of(context)
.colorScheme
.onSurfaceVariant
.withValues(alpha: 0.1),
strokeWidth: 1,
);
},
@@ -222,14 +226,18 @@ class MonthlyExpenseChartCard extends StatelessWidget {
barTouchData: BarTouchData(
enabled: true,
touchTooltipData: BarTouchTooltipData(
tooltipBgColor: AppColors.darkNavy,
tooltipBgColor: Theme.of(context)
.colorScheme
.inverseSurface,
tooltipRoundedRadius: 8,
getTooltipItem:
(group, groupIndex, rod, rodIndex) {
return BarTooltipItem(
'${monthlyData[group.x]['monthName']}\n',
const TextStyle(
color: AppColors.pureWhite,
TextStyle(
color: Theme.of(context)
.colorScheme
.onInverseSurface,
fontWeight: FontWeight.bold,
),
children: [
@@ -239,8 +247,10 @@ class MonthlyExpenseChartCard extends StatelessWidget {
monthlyData[group.x]
['totalExpense'] as double,
locale),
style: const TextStyle(
color: Color(0xFFFBBF24),
style: TextStyle(
color: Theme.of(context)
.colorScheme
.warning,
fontSize: 14,
fontWeight: FontWeight.w500,
),

View File

@@ -4,8 +4,9 @@ import 'package:provider/provider.dart';
import '../../models/subscription_model.dart';
import '../../services/currency_util.dart';
import '../../services/exchange_rate_service.dart';
import '../../theme/app_colors.dart';
import '../glassmorphism_card.dart';
// import '../../theme/app_colors.dart';
import '../../theme/color_scheme_ext.dart';
// Glass 제거: Material 3 Card 사용
import '../themed_text.dart';
import 'analysis_badge.dart';
import '../../l10n/app_localizations.dart';
@@ -30,18 +31,19 @@ class SubscriptionPieChartCard extends StatefulWidget {
class _SubscriptionPieChartCardState extends State<SubscriptionPieChartCard> {
int _touchedIndex = -1;
late Future<List<PieChartSectionData>> _pieSectionsFuture;
// kept for compatibility previously; computation now happens per build
String? _lastLocale;
static const _chartColors = [
Color(0xFF3B82F6),
Color(0xFF10B981),
Color(0xFFF59E0B),
Color(0xFFEF4444),
Color(0xFF8B5CF6),
Color(0xFF0EA5E9),
Color(0xFFEC4899),
];
// 차트 팔레트: ColorScheme + 보조 상수(성공/경고/액센트)
List<Color> _getChartColors(ColorScheme scheme) => [
scheme.primary,
scheme.success,
scheme.warning,
scheme.error,
scheme.tertiary,
scheme.secondary,
const Color(0xFFEC4899), // accent
];
@override
void initState() {
@@ -62,7 +64,7 @@ class _SubscriptionPieChartCardState extends State<SubscriptionPieChartCard> {
void _initializeFuture() {
_lastLocale = context.read<LocaleProvider>().locale.languageCode;
_pieSectionsFuture = _getPieSections();
// no-op: Future computed on demand in build
}
bool _listEquals(List<SubscriptionModel> a, List<SubscriptionModel> b) {
@@ -85,6 +87,9 @@ class _SubscriptionPieChartCardState extends State<SubscriptionPieChartCard> {
// 현재 locale 가져오기
final locale = context.read<LocaleProvider>().locale.languageCode;
final defaultCurrency = CurrencyUtil.getDefaultCurrency(locale);
// Chart palette (capture scheme before any awaits)
final scheme = Theme.of(context).colorScheme;
final chartColors = _getChartColors(scheme);
// 개별 구독의 비율 계산을 위한 값들 (기본 통화로 환산)
List<double> sectionValues = [];
@@ -121,7 +126,7 @@ class _SubscriptionPieChartCardState extends State<SubscriptionPieChartCard> {
// 섹션 데이터 생성 (터치 상태 제외)
final sections = List.generate(widget.subscriptions.length, (i) {
final percentage = (sectionValues[i] / sectionsTotal) * 100;
final index = i % _chartColors.length;
final index = i % chartColors.length;
return PieChartSectionData(
value: sectionValues[i],
@@ -129,12 +134,12 @@ class _SubscriptionPieChartCardState extends State<SubscriptionPieChartCard> {
titleStyle: const TextStyle(
fontSize: 12.0,
fontWeight: FontWeight.bold,
color: AppColors.pureWhite,
color: Colors.white,
shadows: [
Shadow(color: Colors.black26, blurRadius: 2, offset: Offset(0, 1))
],
),
color: _chartColors[index],
color: chartColors[index],
radius: 100.0,
titlePositionPercentageOffset: 0.6,
badgeWidget: null,
@@ -150,12 +155,13 @@ class _SubscriptionPieChartCardState extends State<SubscriptionPieChartCard> {
if (index >= widget.subscriptions.length) return const SizedBox.shrink();
final subscription = widget.subscriptions[index];
final colorIndex = index % _chartColors.length;
final chartColors = _getChartColors(Theme.of(context).colorScheme);
final colorIndex = index % chartColors.length;
return IgnorePointer(
child: AnalysisBadge(
size: 40,
borderColor: _chartColors[colorIndex],
borderColor: chartColors[colorIndex],
subscription: subscription,
),
);
@@ -177,7 +183,7 @@ class _SubscriptionPieChartCardState extends State<SubscriptionPieChartCard> {
TextStyle(
fontSize: fontSize,
fontWeight: FontWeight.bold,
color: AppColors.pureWhite,
color: Colors.white,
shadows: const [
Shadow(
color: Colors.black26, blurRadius: 2, offset: Offset(0, 1))
@@ -210,10 +216,17 @@ class _SubscriptionPieChartCardState extends State<SubscriptionPieChartCard> {
parent: widget.animationController,
curve: const Interval(0.0, 0.7, curve: Curves.easeOut),
)),
child: GlassmorphismCard(
blur: 10,
opacity: 0.1,
borderRadius: 16,
child: Card(
elevation: 3,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: BorderSide(
color: Theme.of(context)
.colorScheme
.outline
.withValues(alpha: 0.5),
),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
@@ -243,20 +256,27 @@ class _SubscriptionPieChartCardState extends State<SubscriptionPieChartCard> {
vertical: 4,
),
decoration: BoxDecoration(
color: const Color(0xFFE5F2FF),
color: Theme.of(context)
.colorScheme
.primary
.withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(4),
border: Border.all(
color: const Color(0xFFBFDBFE),
color: Theme.of(context)
.colorScheme
.primary
.withValues(alpha: 0.3),
width: 1,
),
),
child: Text(
AppLocalizations.of(context)
.exchangeRateFormat(snapshot.data!),
style: const TextStyle(
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Color(0xFF3B82F6),
color:
Theme.of(context).colorScheme.primary,
),
),
);
@@ -291,7 +311,7 @@ class _SubscriptionPieChartCardState extends State<SubscriptionPieChartCard> {
: SizedBox(
height: 250,
child: FutureBuilder<List<PieChartSectionData>>(
future: _pieSectionsFuture,
future: _getPieSections(),
builder: (context, snapshot) {
if (snapshot.connectionState ==
ConnectionState.waiting) {
@@ -392,8 +412,10 @@ class _SubscriptionPieChartCardState extends State<SubscriptionPieChartCard> {
(index) {
final subscription =
widget.subscriptions[index];
final chartColors = _getChartColors(
Theme.of(context).colorScheme);
final color =
_chartColors[index % _chartColors.length];
chartColors[index % chartColors.length];
return Padding(
padding: const EdgeInsets.only(bottom: 4.0),
child: Row(

View File

@@ -6,8 +6,9 @@ import '../../models/subscription_model.dart';
import '../../services/currency_util.dart';
import '../../providers/locale_provider.dart';
import '../../utils/haptic_feedback_helper.dart';
import '../../theme/app_colors.dart';
import '../glassmorphism_card.dart';
// import '../../theme/app_colors.dart';
import '../../theme/color_scheme_ext.dart';
// Glass 제거: Material 3 Card 사용
import '../themed_text.dart';
import '../../l10n/app_localizations.dart';
@@ -44,10 +45,17 @@ class TotalExpenseSummaryCard extends StatelessWidget {
curve: const Interval(0.2, 0.8, curve: Curves.easeOut),
)),
child: RepaintBoundary(
child: GlassmorphismCard(
blur: 10,
opacity: 0.1,
borderRadius: 16,
child: Card(
elevation: 3,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: BorderSide(
color: Theme.of(context)
.colorScheme
.outline
.withValues(alpha: 0.5),
),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
@@ -85,8 +93,6 @@ class TotalExpenseSummaryCard extends StatelessWidget {
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
backgroundColor: AppColors.glassBackground
.withValues(alpha: 0.3),
margin: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
@@ -142,18 +148,24 @@ class TotalExpenseSummaryCard extends StatelessWidget {
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: AppColors.glassBackground
.withValues(alpha: 0.3),
color: Theme.of(context)
.colorScheme
.surfaceContainerHighest
.withValues(alpha: 0.4),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: AppColors.glassBorder
.withValues(alpha: 0.2),
color: Theme.of(context)
.colorScheme
.outline
.withValues(alpha: 0.3),
),
),
child: const FaIcon(
child: FaIcon(
FontAwesomeIcons.listCheck,
size: 16,
color: AppColors.primaryColor,
color: Theme.of(context)
.colorScheme
.primary,
),
),
const SizedBox(width: 12),
@@ -189,18 +201,24 @@ class TotalExpenseSummaryCard extends StatelessWidget {
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: AppColors.glassBackground
.withValues(alpha: 0.3),
color: Theme.of(context)
.colorScheme
.surfaceContainerHighest
.withValues(alpha: 0.4),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: AppColors.glassBorder
.withValues(alpha: 0.2),
color: Theme.of(context)
.colorScheme
.outline
.withValues(alpha: 0.3),
),
),
child: const FaIcon(
child: FaIcon(
FontAwesomeIcons.chartLine,
size: 16,
color: AppColors.successColor,
color: Theme.of(context)
.colorScheme
.success,
),
),
const SizedBox(width: 12),