| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781 |
- import 'package:flutter/material.dart';
- import 'package:fl_chart/fl_chart.dart';
- import '../l10n/app_localizations.dart';
- import '../utils/colors.dart';
- import '../utils/score_manager.dart';
- import '../utils/theme_notifier.dart';
- import '../game/audio/audio_manager.dart';
- class StatsScreen extends StatefulWidget {
- const StatsScreen({super.key});
- @override
- State<StatsScreen> createState() => _StatsScreenState();
- }
- class _StatsScreenState extends State<StatsScreen>
- with TickerProviderStateMixin {
- late TabController _tabController;
- bool _isLoading = true;
- @override
- void initState() {
- super.initState();
- _tabController = TabController(length: 2, vsync: this);
- _loadData();
- // Start menu music when entering stats screen
- WidgetsBinding.instance.addPostFrameCallback((_) {
- AudioManager().playMenuMusic();
- });
- }
- @override
- void dispose() {
- _tabController.dispose();
- super.dispose();
- }
- Future<void> _loadData() async {
- // Ensure score manager is initialized
- await ScoreManager.initialize();
- setState(() {
- _isLoading = false;
- });
- }
- @override
- Widget build(BuildContext context) {
- return ThemeAwareBuilder(
- builder: (context, theme) {
- return Scaffold(
- backgroundColor: ZenColors.currentAppBackground,
- appBar: AppBar(
- backgroundColor: ZenColors.currentAppBackground,
- elevation: 0,
- leading: IconButton(
- onPressed: () => Navigator.of(context).pop(true),
- icon: Icon(Icons.arrow_back, color: ZenColors.currentPrimaryText),
- ),
- title: Text(
- AppLocalizations.of(context)!.statistics,
- style: TextStyle(
- color: ZenColors.currentPrimaryText,
- fontSize: 24,
- fontWeight: FontWeight.bold,
- ),
- ),
- centerTitle: true,
- bottom: TabBar(
- controller: _tabController,
- labelColor: ZenColors.currentButtonBackground,
- unselectedLabelColor: ZenColors.currentSecondaryText,
- indicatorColor: ZenColors.currentButtonBackground,
- tabs: [
- Tab(text: AppLocalizations.of(context)!.overview),
- Tab(text: AppLocalizations.of(context)!.charts),
- ],
- ),
- ),
- body:
- _isLoading
- ? Center(
- child: CircularProgressIndicator(
- valueColor: AlwaysStoppedAnimation<Color>(
- ZenColors.currentButtonBackground,
- ),
- ),
- )
- : TabBarView(
- controller: _tabController,
- children: [_buildOverviewTab(), _buildChartsTab()],
- ),
- );
- },
- );
- }
- Widget _buildOverviewTab() {
- return SingleChildScrollView(
- padding: const EdgeInsets.all(20),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- // Quick Stats Cards
- _buildQuickStatsGrid(),
- const SizedBox(height: 30),
- // Achievements Section
- _buildAchievementsSection(),
- const SizedBox(height: 30),
- // Recent Activity
- _buildRecentActivitySection(),
- ],
- ),
- );
- }
- Widget _buildQuickStatsGrid() {
- return Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- AppLocalizations.of(context)!.yourRelaxationJourney,
- style: TextStyle(
- color: ZenColors.currentPrimaryText,
- fontSize: 22,
- fontWeight: FontWeight.bold,
- ),
- ),
- const SizedBox(height: 15),
- LayoutBuilder(
- builder: (context, constraints) {
- final screenWidth = constraints.maxWidth;
- final crossAxisCount = screenWidth < 600 ? 2 : 3;
- final childAspectRatio = screenWidth < 400 ? 1.3 : 1.5;
- return GridView.count(
- crossAxisCount: crossAxisCount,
- shrinkWrap: true,
- physics: const NeverScrollableScrollPhysics(),
- childAspectRatio: childAspectRatio,
- crossAxisSpacing: 15,
- mainAxisSpacing: 15,
- children: [
- _buildStatCard(
- AppLocalizations.of(context)!.todaysPoints,
- ScoreManager.todayScore.toString(),
- Icons.today,
- ZenColors.currentButtonBackground,
- ),
- _buildStatCard(
- AppLocalizations.of(context)!.totalPoints,
- ScoreManager.totalScore.toString(),
- Icons.stars,
- ZenColors.red,
- ),
- _buildStatCard(
- AppLocalizations.of(context)!.bubblesPopped,
- ScoreManager.totalBubblesPopped.toString(),
- Icons.bubble_chart,
- ZenColors.navyBlue,
- ),
- _buildStatCard(
- AppLocalizations.of(context)!.dailyAverage,
- ScoreManager.averageDailyScore.toStringAsFixed(0),
- Icons.trending_up,
- Colors.green,
- ),
- _buildStatCard(
- AppLocalizations.of(context)!.currentStreak,
- AppLocalizations.of(
- context,
- )!.streakDays(ScoreManager.currentStreak),
- Icons.local_fire_department,
- Colors.orange,
- ),
- _buildStatCard(
- AppLocalizations.of(context)!.bestDay,
- ScoreManager.bestDayScore.toString(),
- Icons.military_tech,
- Colors.purple,
- ),
- ],
- );
- },
- ),
- ],
- );
- }
- Widget _buildStatCard(
- String title,
- String value,
- IconData icon,
- Color color,
- ) {
- return Container(
- padding: const EdgeInsets.all(16),
- decoration: BoxDecoration(
- color: ZenColors.currentUiElements.withValues(alpha: 0.3),
- borderRadius: BorderRadius.circular(15),
- border: Border.all(color: color.withValues(alpha: 0.3), width: 1),
- ),
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- Icon(icon, color: color, size: 28),
- const SizedBox(height: 8),
- Text(
- value,
- style: TextStyle(
- color: ZenColors.currentPrimaryText,
- fontSize: 20,
- fontWeight: FontWeight.bold,
- ),
- ),
- const SizedBox(height: 4),
- Text(
- title,
- style: TextStyle(
- color: ZenColors.currentSecondaryText,
- fontSize: 12,
- ),
- textAlign: TextAlign.center,
- ),
- ],
- ),
- );
- }
- Widget _buildAchievementsSection() {
- final achievements = _getAchievements();
- return Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- AppLocalizations.of(context)!.achievements,
- style: TextStyle(
- color: ZenColors.currentPrimaryText,
- fontSize: 22,
- fontWeight: FontWeight.bold,
- ),
- ),
- const SizedBox(height: 15),
- SizedBox(
- height: 120,
- child: ListView.builder(
- scrollDirection: Axis.horizontal,
- itemCount: achievements.length,
- itemBuilder: (context, index) {
- final achievement = achievements[index];
- return Container(
- width: 100,
- margin: const EdgeInsets.only(right: 15),
- child: _buildAchievementCard(
- achievement['title']!,
- achievement['icon'] as IconData,
- achievement['unlocked'] as bool,
- ),
- );
- },
- ),
- ),
- ],
- );
- }
- Widget _buildAchievementCard(String title, IconData icon, bool unlocked) {
- return Container(
- padding: const EdgeInsets.all(12),
- decoration: BoxDecoration(
- color:
- unlocked
- ? ZenColors.currentButtonBackground.withValues(alpha: 0.2)
- : ZenColors.currentUiElements.withValues(alpha: 0.1),
- borderRadius: BorderRadius.circular(12),
- border: Border.all(
- color:
- unlocked
- ? ZenColors.currentButtonBackground.withValues(alpha: 0.5)
- : ZenColors.currentUiElements.withValues(alpha: 0.3),
- width: 1,
- ),
- ),
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- Icon(
- icon,
- color:
- unlocked
- ? ZenColors.currentButtonBackground
- : ZenColors.currentMutedText,
- size: 32,
- ),
- const SizedBox(height: 8),
- Text(
- title,
- style: TextStyle(
- color:
- unlocked
- ? ZenColors.currentPrimaryText
- : ZenColors.currentMutedText,
- fontSize: 11,
- fontWeight: unlocked ? FontWeight.w600 : FontWeight.normal,
- ),
- textAlign: TextAlign.center,
- maxLines: 2,
- overflow: TextOverflow.ellipsis,
- ),
- ],
- ),
- );
- }
- List<Map<String, dynamic>> _getAchievements() {
- final totalScore = ScoreManager.totalScore;
- final streak = ScoreManager.currentStreak;
- final bubbles = ScoreManager.totalBubblesPopped;
- final l10n = AppLocalizations.of(context)!;
- return [
- {
- 'title': l10n.firstSteps,
- 'icon': Icons.baby_changing_station,
- 'unlocked': totalScore >= 10,
- },
- {
- 'title': l10n.zenApprentice,
- 'icon': Icons.self_improvement,
- 'unlocked': totalScore >= 100,
- },
- {
- 'title': l10n.bubbleMaster,
- 'icon': Icons.bubble_chart,
- 'unlocked': bubbles >= 100,
- },
- {
- 'title': l10n.consistent,
- 'icon': Icons.calendar_today,
- 'unlocked': streak >= 3,
- },
- {
- 'title': l10n.dedicated,
- 'icon': Icons.local_fire_department,
- 'unlocked': streak >= 7,
- },
- {
- 'title': l10n.zenMaster,
- 'icon': Icons.psychology,
- 'unlocked': totalScore >= 1000,
- },
- ];
- }
- Widget _buildRecentActivitySection() {
- final lastDays = ScoreManager.getLastDaysScores(7);
- return Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- AppLocalizations.of(context)!.last7Days,
- style: TextStyle(
- color: ZenColors.currentPrimaryText,
- fontSize: 22,
- fontWeight: FontWeight.bold,
- ),
- ),
- const SizedBox(height: 15),
- Container(
- padding: const EdgeInsets.all(16),
- decoration: BoxDecoration(
- color: ZenColors.currentUiElements.withValues(alpha: 0.2),
- borderRadius: BorderRadius.circular(15),
- ),
- child: Column(
- children:
- lastDays.map((entry) {
- final date = DateTime.parse(entry.key);
- final dayName = _getDayName(date.weekday);
- final isToday = _isToday(date);
- return Padding(
- padding: const EdgeInsets.symmetric(vertical: 4),
- child: LayoutBuilder(
- builder: (context, constraints) {
- final screenWidth = constraints.maxWidth;
- final dayWidth =
- screenWidth * 0.2; // 20% of available width
- final scoreWidth =
- screenWidth * 0.15; // 15% of available width
- return Row(
- children: [
- SizedBox(
- width: dayWidth,
- child: Text(
- dayName,
- style: TextStyle(
- color:
- isToday
- ? ZenColors.currentButtonBackground
- : ZenColors.currentSecondaryText,
- fontSize: 14,
- fontWeight:
- isToday
- ? FontWeight.bold
- : FontWeight.normal,
- ),
- overflow: TextOverflow.ellipsis,
- ),
- ),
- Expanded(
- child: Container(
- height: 8,
- margin: const EdgeInsets.symmetric(
- horizontal: 10,
- ),
- child: LinearProgressIndicator(
- value:
- entry.value /
- (ScoreManager.bestDayScore.clamp(
- 1,
- double.infinity,
- )),
- backgroundColor: ZenColors.currentUiElements
- .withValues(alpha: 0.3),
- valueColor: AlwaysStoppedAnimation<Color>(
- isToday
- ? ZenColors.currentButtonBackground
- : ZenColors.currentUiElements,
- ),
- ),
- ),
- ),
- SizedBox(
- width: scoreWidth,
- child: Text(
- entry.value.toString(),
- style: TextStyle(
- color:
- isToday
- ? ZenColors.currentButtonBackground
- : ZenColors.currentPrimaryText,
- fontSize: 14,
- fontWeight:
- isToday
- ? FontWeight.bold
- : FontWeight.normal,
- ),
- textAlign: TextAlign.right,
- overflow: TextOverflow.ellipsis,
- ),
- ),
- ],
- );
- },
- ),
- );
- }).toList(),
- ),
- ),
- ],
- );
- }
- Widget _buildChartsTab() {
- return SingleChildScrollView(
- padding: const EdgeInsets.all(20),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- _buildDailyChart(),
- const SizedBox(height: 30),
- _buildWeeklyChart(),
- ],
- ),
- );
- }
- Widget _buildDailyChart() {
- final lastDays = ScoreManager.getLastDaysScores(14);
- final maxScore = lastDays
- .fold(0, (max, entry) => entry.value > max ? entry.value : max)
- .clamp(1, double.infinity);
- return Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- AppLocalizations.of(context)!.dailyProgress14Days,
- style: TextStyle(
- color: ZenColors.currentPrimaryText,
- fontSize: 22,
- fontWeight: FontWeight.bold,
- ),
- ),
- const SizedBox(height: 15),
- LayoutBuilder(
- builder: (context, constraints) {
- final chartHeight = constraints.maxWidth < 400 ? 250.0 : 300.0;
- final labelFontSize = constraints.maxWidth < 400 ? 10.0 : 12.0;
- return Container(
- height: chartHeight,
- padding: const EdgeInsets.all(16),
- decoration: BoxDecoration(
- color: ZenColors.currentUiElements.withValues(alpha: 0.2),
- borderRadius: BorderRadius.circular(15),
- ),
- child: LineChart(
- LineChartData(
- backgroundColor: Colors.transparent,
- gridData: FlGridData(
- show: true,
- drawVerticalLine: false,
- getDrawingHorizontalLine: (value) {
- return FlLine(
- color: ZenColors.currentUiElements.withValues(
- alpha: 0.3,
- ),
- strokeWidth: 1,
- );
- },
- ),
- titlesData: FlTitlesData(
- bottomTitles: AxisTitles(
- sideTitles: SideTitles(
- showTitles: true,
- reservedSize: 30,
- interval: lastDays.length > 10 ? 2 : 1,
- getTitlesWidget: (value, meta) {
- if (value.toInt() >= 0 &&
- value.toInt() < lastDays.length) {
- final date = DateTime.parse(
- lastDays[value.toInt()].key,
- );
- return Transform.rotate(
- angle: constraints.maxWidth < 400 ? -0.5 : 0,
- child: Text(
- '${date.day}',
- style: TextStyle(
- color: ZenColors.currentSecondaryText,
- fontSize: labelFontSize,
- ),
- ),
- );
- }
- return const Text('');
- },
- ),
- ),
- leftTitles: AxisTitles(
- sideTitles: SideTitles(
- showTitles: true,
- reservedSize: 40,
- getTitlesWidget: (value, meta) {
- return Text(
- value.toInt().toString(),
- style: TextStyle(
- color: ZenColors.currentSecondaryText,
- fontSize: labelFontSize,
- ),
- );
- },
- ),
- ),
- topTitles: const AxisTitles(
- sideTitles: SideTitles(showTitles: false),
- ),
- rightTitles: const AxisTitles(
- sideTitles: SideTitles(showTitles: false),
- ),
- ),
- borderData: FlBorderData(show: false),
- minX: 0,
- maxX: (lastDays.length - 1).toDouble(),
- minY: 0,
- maxY: maxScore.toDouble(),
- lineBarsData: [
- LineChartBarData(
- spots:
- lastDays.asMap().entries.map((entry) {
- return FlSpot(
- entry.key.toDouble(),
- entry.value.value.toDouble(),
- );
- }).toList(),
- isCurved: true,
- gradient: LinearGradient(
- colors: [
- ZenColors.currentButtonBackground.withValues(
- alpha: 0.8,
- ),
- ZenColors.currentButtonBackground,
- ],
- ),
- barWidth: 3,
- dotData: FlDotData(
- show: true,
- getDotPainter: (spot, percent, barData, index) {
- return FlDotCirclePainter(
- radius: 4,
- color: ZenColors.currentButtonBackground,
- strokeWidth: 2,
- strokeColor: ZenColors.currentPrimaryText,
- );
- },
- ),
- belowBarData: BarAreaData(
- show: true,
- gradient: LinearGradient(
- begin: Alignment.topCenter,
- end: Alignment.bottomCenter,
- colors: [
- ZenColors.currentButtonBackground.withValues(
- alpha: 0.3,
- ),
- ZenColors.currentButtonBackground.withValues(
- alpha: 0.1,
- ),
- ],
- ),
- ),
- ),
- ],
- ),
- ),
- );
- },
- ),
- ],
- );
- }
- Widget _buildWeeklyChart() {
- final weeklyScores = ScoreManager.getWeeklyScores(8);
- final maxScore = weeklyScores
- .fold(0, (max, entry) => entry.value > max ? entry.value : max)
- .clamp(1, double.infinity);
- return Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- AppLocalizations.of(context)!.weeklySummary8Weeks,
- style: TextStyle(
- color: ZenColors.currentPrimaryText,
- fontSize: 22,
- fontWeight: FontWeight.bold,
- ),
- ),
- const SizedBox(height: 15),
- LayoutBuilder(
- builder: (context, constraints) {
- final chartHeight = constraints.maxWidth < 400 ? 250.0 : 300.0;
- final labelFontSize = constraints.maxWidth < 400 ? 10.0 : 12.0;
- return Container(
- height: chartHeight,
- padding: const EdgeInsets.all(16),
- decoration: BoxDecoration(
- color: ZenColors.currentUiElements.withValues(alpha: 0.2),
- borderRadius: BorderRadius.circular(15),
- ),
- child: BarChart(
- BarChartData(
- backgroundColor: Colors.transparent,
- gridData: FlGridData(
- show: true,
- drawVerticalLine: false,
- getDrawingHorizontalLine: (value) {
- return FlLine(
- color: ZenColors.currentUiElements.withValues(
- alpha: 0.3,
- ),
- strokeWidth: 1,
- );
- },
- ),
- titlesData: FlTitlesData(
- bottomTitles: AxisTitles(
- sideTitles: SideTitles(
- showTitles: true,
- reservedSize: 30,
- getTitlesWidget: (value, meta) {
- if (value.toInt() >= 0 &&
- value.toInt() < weeklyScores.length) {
- return Text(
- 'W${value.toInt() + 1}',
- style: TextStyle(
- color: ZenColors.currentSecondaryText,
- fontSize: labelFontSize,
- ),
- );
- }
- return const Text('');
- },
- ),
- ),
- leftTitles: AxisTitles(
- sideTitles: SideTitles(
- showTitles: true,
- reservedSize: 40,
- getTitlesWidget: (value, meta) {
- return Text(
- value.toInt().toString(),
- style: TextStyle(
- color: ZenColors.currentSecondaryText,
- fontSize: labelFontSize,
- ),
- );
- },
- ),
- ),
- topTitles: const AxisTitles(
- sideTitles: SideTitles(showTitles: false),
- ),
- rightTitles: const AxisTitles(
- sideTitles: SideTitles(showTitles: false),
- ),
- ),
- borderData: FlBorderData(show: false),
- maxY: maxScore.toDouble(),
- barGroups:
- weeklyScores.asMap().entries.map((entry) {
- return BarChartGroupData(
- x: entry.key,
- barRods: [
- BarChartRodData(
- toY: entry.value.value.toDouble(),
- gradient: LinearGradient(
- begin: Alignment.bottomCenter,
- end: Alignment.topCenter,
- colors: [
- ZenColors.navyBlue.withValues(alpha: 0.8),
- ZenColors.navyBlue,
- ],
- ),
- width: 20,
- borderRadius: const BorderRadius.only(
- topLeft: Radius.circular(6),
- topRight: Radius.circular(6),
- ),
- ),
- ],
- );
- }).toList(),
- ),
- ),
- );
- },
- ),
- ],
- );
- }
- String _getDayName(int weekday) {
- final l10n = AppLocalizations.of(context)!;
- final days = [
- l10n.monday,
- l10n.tuesday,
- l10n.wednesday,
- l10n.thursday,
- l10n.friday,
- l10n.saturday,
- l10n.sunday,
- ];
- return days[weekday - 1];
- }
- bool _isToday(DateTime date) {
- final now = DateTime.now();
- return date.year == now.year &&
- date.month == now.month &&
- date.day == now.day;
- }
- }
|