From 95879919996b4695bc6c48c46649132f03906085 Mon Sep 17 00:00:00 2001 From: Ponshu Developer Date: Mon, 6 Apr 2026 17:04:20 +0900 Subject: [PATCH] =?UTF-8?q?feat(ponshu-room):=20Phase=203=20UI=20=E2=80=94?= =?UTF-8?q?=20Bento=20Grid=20stats=20+=20progress=20bar=20animation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - activity_stats: Bento Grid layout (大カード×1 + 小カード×2) with AppColors tokens - level_title_card: TweenAnimationBuilder progress bar (0→value, 900ms easeOutCubic) - version bump: 1.0.20+31 → 1.0.21+32 Co-Authored-By: Claude Sonnet 4.6 --- lib/widgets/gamification/activity_stats.dart | 190 ++++++++++++------ .../gamification/level_title_card.dart | 25 ++- pubspec.yaml | 2 +- 3 files changed, 146 insertions(+), 71 deletions(-) diff --git a/lib/widgets/gamification/activity_stats.dart b/lib/widgets/gamification/activity_stats.dart index 17a3106..67d1e80 100644 --- a/lib/widgets/gamification/activity_stats.dart +++ b/lib/widgets/gamification/activity_stats.dart @@ -1,10 +1,9 @@ - import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lucide_icons/lucide_icons.dart'; import '../../providers/sake_list_provider.dart'; import '../../models/schema/item_type.dart'; - +import '../../theme/app_colors.dart'; class ActivityStats extends ConsumerWidget { const ActivityStats({super.key}); @@ -12,78 +11,147 @@ class ActivityStats extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final allSakeAsync = ref.watch(allSakeItemsProvider); - + return allSakeAsync.when( data: (sakes) { - // Phase D5: Exclude set products from activity statistics (sets contain multiple items) - final individualSakes = sakes.where((s) => s.itemType == ItemType.sake).toList(); + final individualSakes = sakes.where((s) => s.itemType == ItemType.sake).toList(); - final totalSakes = individualSakes.length; - final favoriteCount = individualSakes.where((s) => s.userData.isFavorite).length; + final totalSakes = individualSakes.length; + final favoriteCount = individualSakes.where((s) => s.userData.isFavorite).length; - // Recording Days - final dates = individualSakes.map((s) { - final d = s.metadata.createdAt; - return DateTime(d.year, d.month, d.day); - }).toSet(); - final recordingDays = dates.length; - - return Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Theme.of(context).cardColor, - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: Theme.of(context).dividerColor.withValues(alpha: 0.1), + final dates = individualSakes.map((s) { + final d = s.metadata.createdAt; + return DateTime(d.year, d.month, d.day); + }).toSet(); + final recordingDays = dates.length; + + final appColors = Theme.of(context).extension()!; + + // Bento Grid: 総登録数を大カード、お気に入り・撮影日数を小カード2枚横並び + return Column( + children: [ + // 大カード — 総登録数 + _BentoCard( + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: appColors.brandPrimary.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Icon(LucideIcons.wine, size: 28, color: appColors.brandPrimary), + ), + const SizedBox(width: 16), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '$totalSakes本', + style: TextStyle( + fontSize: 32, + fontWeight: FontWeight.w900, + color: appColors.brandPrimary, + height: 1.0, + ), + ), + const SizedBox(height: 4), + Text( + '総登録数', + style: TextStyle( + fontSize: 12, + color: appColors.textSecondary, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ], ), ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'あなたの活動深度', - style: Theme.of(context).textTheme.titleSmall?.copyWith( - fontWeight: FontWeight.bold, + const SizedBox(height: 8), + // 小カード2枚横並び + Row( + children: [ + Expanded( + child: _BentoCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(LucideIcons.heart, size: 20, color: appColors.brandAccent), + const SizedBox(height: 8), + Text( + '$favoriteCount本', + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.w900, + color: appColors.textPrimary, + height: 1.0, + ), + ), + const SizedBox(height: 4), + Text( + 'お気に入り', + style: TextStyle(fontSize: 11, color: appColors.textSecondary), + ), + ], + ), ), - ), - const SizedBox(height: 16), - Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - _buildStatItem(context, '総登録数', '$totalSakes本', LucideIcons.wine), - _buildStatItem(context, 'お気に入り', '$favoriteCount本', LucideIcons.heart), - _buildStatItem(context, '撮影日数', '$recordingDays日', LucideIcons.calendar), - // _buildStatItem(context, '平均価格', '¥$avgPrice', LucideIcons.banknote), // Hidden per user request - ], - ), - ], - ), - ); + ), + const SizedBox(width: 8), + Expanded( + child: _BentoCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(LucideIcons.calendar, size: 20, color: appColors.iconDefault), + const SizedBox(height: 8), + Text( + '$recordingDays日', + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.w900, + color: appColors.textPrimary, + height: 1.0, + ), + ), + const SizedBox(height: 4), + Text( + '撮影日数', + style: TextStyle(fontSize: 11, color: appColors.textSecondary), + ), + ], + ), + ), + ), + ], + ), + ], + ); }, loading: () => const SizedBox.shrink(), error: (_, _) => const SizedBox.shrink(), ); } - - Widget _buildStatItem(BuildContext context, String label, String value, IconData icon) { - return Column( - children: [ - Icon(icon, size: 20, color: Colors.grey[400]), - const SizedBox(height: 8), - Text( - value, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w900, - color: Theme.of(context).colorScheme.onSurface, - ), +} + +class _BentoCard extends StatelessWidget { + const _BentoCard({required this.child}); + final Widget child; + + @override + Widget build(BuildContext context) { + return Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Theme.of(context).cardColor, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: Theme.of(context).dividerColor.withValues(alpha: 0.1), ), - const SizedBox(height: 4), - Text( - label, - style: TextStyle(fontSize: 10, color: Colors.grey[600]), - ), - ], + ), + child: child, ); } } diff --git a/lib/widgets/gamification/level_title_card.dart b/lib/widgets/gamification/level_title_card.dart index 18b49e3..8dc3fba 100644 --- a/lib/widgets/gamification/level_title_card.dart +++ b/lib/widgets/gamification/level_title_card.dart @@ -115,15 +115,22 @@ class LevelTitleCard extends ConsumerWidget { ), const SizedBox(height: 20), - // Progress Bar - ClipRRect( - borderRadius: BorderRadius.circular(4), - child: LinearProgressIndicator( - value: progress, - minHeight: 8, - backgroundColor: appColors.divider, - valueColor: AlwaysStoppedAnimation(appColors.brandPrimary), - ), + // Progress Bar (animated 0 → actual value on mount) + TweenAnimationBuilder( + tween: Tween(begin: 0.0, end: progress), + duration: const Duration(milliseconds: 900), + curve: Curves.easeOutCubic, + builder: (context, value, _) { + return ClipRRect( + borderRadius: BorderRadius.circular(4), + child: LinearProgressIndicator( + value: value, + minHeight: 8, + backgroundColor: appColors.divider, + valueColor: AlwaysStoppedAnimation(appColors.brandPrimary), + ), + ); + }, ), const SizedBox(height: 8), diff --git a/pubspec.yaml b/pubspec.yaml index e7e3941..bfda51b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.0.20+31 +version: 1.0.21+32 environment: sdk: ^3.10.1