feat: add payment card grouping and analysis

This commit is contained in:
JiWoong Sul
2025-11-14 16:53:41 +09:00
parent cba7d082bd
commit 132ae758de
40 changed files with 2846 additions and 522 deletions

View File

@@ -1,16 +1,18 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/subscription_provider.dart';
import '../providers/category_provider.dart';
import '../utils/subscription_category_helper.dart';
import '../widgets/native_ad_widget.dart';
import '../widgets/main_summary_card.dart';
import '../widgets/subscription_list_widget.dart';
import '../widgets/empty_state_widget.dart';
// import '../theme/app_colors.dart';
import '../l10n/app_localizations.dart';
import 'package:shared_preferences/shared_preferences.dart';
class HomeContent extends StatelessWidget {
import '../l10n/app_localizations.dart';
import '../providers/category_provider.dart';
import '../providers/payment_card_provider.dart';
import '../providers/subscription_provider.dart';
import '../utils/subscription_grouping_helper.dart';
import '../widgets/empty_state_widget.dart';
import '../widgets/main_summary_card.dart';
import '../widgets/native_ad_widget.dart';
import '../widgets/subscription_list_widget.dart';
class HomeContent extends StatefulWidget {
final AnimationController fadeController;
final AnimationController rotateController;
final AnimationController slideController;
@@ -31,10 +33,53 @@ class HomeContent extends StatelessWidget {
});
@override
Widget build(BuildContext context) {
final provider = context.watch<SubscriptionProvider>();
State<HomeContent> createState() => _HomeContentState();
}
if (provider.isLoading) {
class _HomeContentState extends State<HomeContent> {
static const _groupingPrefKey = 'home_grouping_mode';
SubscriptionGroupingMode _groupingMode = SubscriptionGroupingMode.category;
@override
void initState() {
super.initState();
_loadGroupingPreference();
}
Future<void> _loadGroupingPreference() async {
final prefs = await SharedPreferences.getInstance();
final stored = prefs.getString(_groupingPrefKey);
if (stored == 'paymentCard') {
setState(() {
_groupingMode = SubscriptionGroupingMode.paymentCard;
});
}
}
Future<void> _saveGroupingPreference(SubscriptionGroupingMode mode) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(
_groupingPrefKey,
mode == SubscriptionGroupingMode.paymentCard
? 'paymentCard'
: 'category');
}
void _updateGroupingMode(SubscriptionGroupingMode mode) {
if (_groupingMode == mode) return;
setState(() {
_groupingMode = mode;
});
_saveGroupingPreference(mode);
}
@override
Widget build(BuildContext context) {
final subscriptionProvider = context.watch<SubscriptionProvider>();
final categoryProvider = context.watch<CategoryProvider>();
final paymentCardProvider = context.watch<PaymentCardProvider>();
if (subscriptionProvider.isLoading) {
return Center(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(
@@ -44,32 +89,30 @@ class HomeContent extends StatelessWidget {
);
}
if (provider.subscriptions.isEmpty) {
if (subscriptionProvider.subscriptions.isEmpty) {
return EmptyStateWidget(
fadeController: fadeController,
rotateController: rotateController,
slideController: slideController,
onAddPressed: onAddPressed,
fadeController: widget.fadeController,
rotateController: widget.rotateController,
slideController: widget.slideController,
onAddPressed: widget.onAddPressed,
);
}
// 카테고리별 구독 구분
final categoryProvider =
Provider.of<CategoryProvider>(context, listen: false);
final categorizedSubscriptions =
SubscriptionCategoryHelper.categorizeSubscriptions(
provider.subscriptions,
categoryProvider,
context,
final groupedSubscriptions = SubscriptionGroupingHelper.buildGroups(
context: context,
subscriptions: subscriptionProvider.subscriptions,
mode: _groupingMode,
categoryProvider: categoryProvider,
paymentCardProvider: paymentCardProvider,
);
return RefreshIndicator(
onRefresh: () async {
await provider.refreshSubscriptions();
await subscriptionProvider.refreshSubscriptions();
},
color: Theme.of(context).colorScheme.primary,
child: CustomScrollView(
controller: scrollController,
controller: widget.scrollController,
physics: const BouncingScrollPhysics(),
slivers: [
SliverToBoxAdapter(
@@ -86,13 +129,13 @@ class HomeContent extends StatelessWidget {
begin: const Offset(0, 0.2),
end: Offset.zero,
).animate(CurvedAnimation(
parent: slideController, curve: Curves.easeOutCubic)),
parent: widget.slideController, curve: Curves.easeOutCubic)),
child: MainScreenSummaryCard(
provider: provider,
fadeController: fadeController,
pulseController: pulseController,
waveController: waveController,
slideController: slideController,
provider: subscriptionProvider,
fadeController: widget.fadeController,
pulseController: widget.pulseController,
waveController: widget.waveController,
slideController: widget.slideController,
),
),
),
@@ -107,7 +150,8 @@ class HomeContent extends StatelessWidget {
begin: const Offset(-0.2, 0),
end: Offset.zero,
).animate(CurvedAnimation(
parent: slideController, curve: Curves.easeOutCubic)),
parent: widget.slideController,
curve: Curves.easeOutCubic)),
child: Text(
AppLocalizations.of(context).mySubscriptions,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
@@ -120,12 +164,13 @@ class HomeContent extends StatelessWidget {
begin: const Offset(0.2, 0),
end: Offset.zero,
).animate(CurvedAnimation(
parent: slideController, curve: Curves.easeOutCubic)),
parent: widget.slideController,
curve: Curves.easeOutCubic)),
child: Row(
children: [
Text(
AppLocalizations.of(context)
.subscriptionCount(provider.subscriptions.length),
AppLocalizations.of(context).subscriptionCount(
subscriptionProvider.subscriptions.length),
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
@@ -145,9 +190,33 @@ class HomeContent extends StatelessWidget {
),
),
),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
child: Wrap(
spacing: 8,
children: [
ChoiceChip(
label: Text(AppLocalizations.of(context).category),
selected:
_groupingMode == SubscriptionGroupingMode.category,
onSelected: (_) =>
_updateGroupingMode(SubscriptionGroupingMode.category),
),
ChoiceChip(
label: Text(AppLocalizations.of(context).paymentCard),
selected:
_groupingMode == SubscriptionGroupingMode.paymentCard,
onSelected: (_) => _updateGroupingMode(
SubscriptionGroupingMode.paymentCard),
),
],
),
),
),
SubscriptionListWidget(
categorizedSubscriptions: categorizedSubscriptions,
fadeController: fadeController,
groups: groupedSubscriptions,
fadeController: widget.fadeController,
),
SliverToBoxAdapter(
child: SizedBox(