stats_screen.dart 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665
  1. import 'package:flutter/material.dart';
  2. import 'package:fl_chart/fl_chart.dart';
  3. import '../l10n/app_localizations.dart';
  4. import '../utils/colors.dart';
  5. import '../utils/score_manager.dart';
  6. import '../utils/theme_notifier.dart';
  7. class StatsScreen extends StatefulWidget {
  8. const StatsScreen({super.key});
  9. @override
  10. State<StatsScreen> createState() => _StatsScreenState();
  11. }
  12. class _StatsScreenState extends State<StatsScreen> with TickerProviderStateMixin {
  13. late TabController _tabController;
  14. bool _isLoading = true;
  15. @override
  16. void initState() {
  17. super.initState();
  18. _tabController = TabController(length: 2, vsync: this);
  19. _loadData();
  20. }
  21. @override
  22. void dispose() {
  23. _tabController.dispose();
  24. super.dispose();
  25. }
  26. Future<void> _loadData() async {
  27. // Ensure score manager is initialized
  28. await ScoreManager.initialize();
  29. setState(() {
  30. _isLoading = false;
  31. });
  32. }
  33. @override
  34. Widget build(BuildContext context) {
  35. return ThemeAwareBuilder(
  36. builder: (context, theme) {
  37. return Scaffold(
  38. backgroundColor: ZenColors.currentAppBackground,
  39. appBar: AppBar(
  40. backgroundColor: ZenColors.currentAppBackground,
  41. elevation: 0,
  42. leading: IconButton(
  43. onPressed: () => Navigator.of(context).pop(true),
  44. icon: Icon(
  45. Icons.arrow_back,
  46. color: ZenColors.currentPrimaryText,
  47. ),
  48. ),
  49. title: Text(
  50. AppLocalizations.of(context)!.statistics,
  51. style: TextStyle(
  52. color: ZenColors.currentPrimaryText,
  53. fontSize: 24,
  54. fontWeight: FontWeight.bold,
  55. ),
  56. ),
  57. centerTitle: true,
  58. bottom: TabBar(
  59. controller: _tabController,
  60. labelColor: ZenColors.currentButtonBackground,
  61. unselectedLabelColor: ZenColors.currentSecondaryText,
  62. indicatorColor: ZenColors.currentButtonBackground,
  63. tabs: [
  64. Tab(text: AppLocalizations.of(context)!.overview),
  65. Tab(text: AppLocalizations.of(context)!.charts),
  66. ],
  67. ),
  68. ),
  69. body: _isLoading
  70. ? Center(
  71. child: CircularProgressIndicator(
  72. valueColor: AlwaysStoppedAnimation<Color>(ZenColors.currentButtonBackground),
  73. ),
  74. )
  75. : TabBarView(
  76. controller: _tabController,
  77. children: [
  78. _buildOverviewTab(),
  79. _buildChartsTab(),
  80. ],
  81. ),
  82. );
  83. },
  84. );
  85. }
  86. Widget _buildOverviewTab() {
  87. return SingleChildScrollView(
  88. padding: const EdgeInsets.all(20),
  89. child: Column(
  90. crossAxisAlignment: CrossAxisAlignment.start,
  91. children: [
  92. // Quick Stats Cards
  93. _buildQuickStatsGrid(),
  94. const SizedBox(height: 30),
  95. // Achievements Section
  96. _buildAchievementsSection(),
  97. const SizedBox(height: 30),
  98. // Recent Activity
  99. _buildRecentActivitySection(),
  100. ],
  101. ),
  102. );
  103. }
  104. Widget _buildQuickStatsGrid() {
  105. return Column(
  106. crossAxisAlignment: CrossAxisAlignment.start,
  107. children: [
  108. Text(
  109. AppLocalizations.of(context)!.yourRelaxationJourney,
  110. style: TextStyle(
  111. color: ZenColors.currentPrimaryText,
  112. fontSize: 22,
  113. fontWeight: FontWeight.bold,
  114. ),
  115. ),
  116. const SizedBox(height: 15),
  117. GridView.count(
  118. crossAxisCount: 2,
  119. shrinkWrap: true,
  120. physics: const NeverScrollableScrollPhysics(),
  121. childAspectRatio: 1.5,
  122. crossAxisSpacing: 15,
  123. mainAxisSpacing: 15,
  124. children: [
  125. _buildStatCard(
  126. AppLocalizations.of(context)!.todaysPoints,
  127. ScoreManager.todayScore.toString(),
  128. Icons.today,
  129. ZenColors.currentButtonBackground,
  130. ),
  131. _buildStatCard(
  132. AppLocalizations.of(context)!.totalPoints,
  133. ScoreManager.totalScore.toString(),
  134. Icons.stars,
  135. ZenColors.red,
  136. ),
  137. _buildStatCard(
  138. AppLocalizations.of(context)!.bubblesPopped,
  139. ScoreManager.totalBubblesPopped.toString(),
  140. Icons.bubble_chart,
  141. ZenColors.navyBlue,
  142. ),
  143. _buildStatCard(
  144. AppLocalizations.of(context)!.dailyAverage,
  145. ScoreManager.averageDailyScore.toStringAsFixed(0),
  146. Icons.trending_up,
  147. Colors.green,
  148. ),
  149. _buildStatCard(
  150. AppLocalizations.of(context)!.currentStreak,
  151. AppLocalizations.of(context)!.streakDays(ScoreManager.currentStreak),
  152. Icons.local_fire_department,
  153. Colors.orange,
  154. ),
  155. _buildStatCard(
  156. AppLocalizations.of(context)!.bestDay,
  157. ScoreManager.bestDayScore.toString(),
  158. Icons.military_tech,
  159. Colors.purple,
  160. ),
  161. ],
  162. ),
  163. ],
  164. );
  165. }
  166. Widget _buildStatCard(String title, String value, IconData icon, Color color) {
  167. return Container(
  168. padding: const EdgeInsets.all(16),
  169. decoration: BoxDecoration(
  170. color: ZenColors.currentUiElements.withValues(alpha: 0.3),
  171. borderRadius: BorderRadius.circular(15),
  172. border: Border.all(
  173. color: color.withValues(alpha: 0.3),
  174. width: 1,
  175. ),
  176. ),
  177. child: Column(
  178. mainAxisAlignment: MainAxisAlignment.center,
  179. children: [
  180. Icon(
  181. icon,
  182. color: color,
  183. size: 28,
  184. ),
  185. const SizedBox(height: 8),
  186. Text(
  187. value,
  188. style: TextStyle(
  189. color: ZenColors.currentPrimaryText,
  190. fontSize: 20,
  191. fontWeight: FontWeight.bold,
  192. ),
  193. ),
  194. const SizedBox(height: 4),
  195. Text(
  196. title,
  197. style: TextStyle(
  198. color: ZenColors.currentSecondaryText,
  199. fontSize: 12,
  200. ),
  201. textAlign: TextAlign.center,
  202. ),
  203. ],
  204. ),
  205. );
  206. }
  207. Widget _buildAchievementsSection() {
  208. final achievements = _getAchievements();
  209. return Column(
  210. crossAxisAlignment: CrossAxisAlignment.start,
  211. children: [
  212. Text(
  213. AppLocalizations.of(context)!.achievements,
  214. style: TextStyle(
  215. color: ZenColors.currentPrimaryText,
  216. fontSize: 22,
  217. fontWeight: FontWeight.bold,
  218. ),
  219. ),
  220. const SizedBox(height: 15),
  221. SizedBox(
  222. height: 120,
  223. child: ListView.builder(
  224. scrollDirection: Axis.horizontal,
  225. itemCount: achievements.length,
  226. itemBuilder: (context, index) {
  227. final achievement = achievements[index];
  228. return Container(
  229. width: 100,
  230. margin: const EdgeInsets.only(right: 15),
  231. child: _buildAchievementCard(
  232. achievement['title']!,
  233. achievement['icon'] as IconData,
  234. achievement['unlocked'] as bool,
  235. ),
  236. );
  237. },
  238. ),
  239. ),
  240. ],
  241. );
  242. }
  243. Widget _buildAchievementCard(String title, IconData icon, bool unlocked) {
  244. return Container(
  245. padding: const EdgeInsets.all(12),
  246. decoration: BoxDecoration(
  247. color: unlocked
  248. ? ZenColors.currentButtonBackground.withValues(alpha: 0.2)
  249. : ZenColors.currentUiElements.withValues(alpha: 0.1),
  250. borderRadius: BorderRadius.circular(12),
  251. border: Border.all(
  252. color: unlocked
  253. ? ZenColors.currentButtonBackground.withValues(alpha: 0.5)
  254. : ZenColors.currentUiElements.withValues(alpha: 0.3),
  255. width: 1,
  256. ),
  257. ),
  258. child: Column(
  259. mainAxisAlignment: MainAxisAlignment.center,
  260. children: [
  261. Icon(
  262. icon,
  263. color: unlocked ? ZenColors.currentButtonBackground : ZenColors.currentMutedText,
  264. size: 32,
  265. ),
  266. const SizedBox(height: 8),
  267. Text(
  268. title,
  269. style: TextStyle(
  270. color: unlocked ? ZenColors.currentPrimaryText : ZenColors.currentMutedText,
  271. fontSize: 11,
  272. fontWeight: unlocked ? FontWeight.w600 : FontWeight.normal,
  273. ),
  274. textAlign: TextAlign.center,
  275. maxLines: 2,
  276. overflow: TextOverflow.ellipsis,
  277. ),
  278. ],
  279. ),
  280. );
  281. }
  282. List<Map<String, dynamic>> _getAchievements() {
  283. final totalScore = ScoreManager.totalScore;
  284. final streak = ScoreManager.currentStreak;
  285. final bubbles = ScoreManager.totalBubblesPopped;
  286. final l10n = AppLocalizations.of(context)!;
  287. return [
  288. {
  289. 'title': l10n.firstSteps,
  290. 'icon': Icons.baby_changing_station,
  291. 'unlocked': totalScore >= 10,
  292. },
  293. {
  294. 'title': l10n.zenApprentice,
  295. 'icon': Icons.self_improvement,
  296. 'unlocked': totalScore >= 100,
  297. },
  298. {
  299. 'title': l10n.bubbleMaster,
  300. 'icon': Icons.bubble_chart,
  301. 'unlocked': bubbles >= 100,
  302. },
  303. {
  304. 'title': l10n.consistent,
  305. 'icon': Icons.calendar_today,
  306. 'unlocked': streak >= 3,
  307. },
  308. {
  309. 'title': l10n.dedicated,
  310. 'icon': Icons.local_fire_department,
  311. 'unlocked': streak >= 7,
  312. },
  313. {
  314. 'title': l10n.zenMaster,
  315. 'icon': Icons.psychology,
  316. 'unlocked': totalScore >= 1000,
  317. },
  318. ];
  319. }
  320. Widget _buildRecentActivitySection() {
  321. final lastDays = ScoreManager.getLastDaysScores(7);
  322. return Column(
  323. crossAxisAlignment: CrossAxisAlignment.start,
  324. children: [
  325. Text(
  326. AppLocalizations.of(context)!.last7Days,
  327. style: TextStyle(
  328. color: ZenColors.currentPrimaryText,
  329. fontSize: 22,
  330. fontWeight: FontWeight.bold,
  331. ),
  332. ),
  333. const SizedBox(height: 15),
  334. Container(
  335. padding: const EdgeInsets.all(16),
  336. decoration: BoxDecoration(
  337. color: ZenColors.currentUiElements.withValues(alpha: 0.2),
  338. borderRadius: BorderRadius.circular(15),
  339. ),
  340. child: Column(
  341. children: lastDays.map((entry) {
  342. final date = DateTime.parse(entry.key);
  343. final dayName = _getDayName(date.weekday);
  344. final isToday = _isToday(date);
  345. return Padding(
  346. padding: const EdgeInsets.symmetric(vertical: 4),
  347. child: Row(
  348. children: [
  349. SizedBox(
  350. width: 60,
  351. child: Text(
  352. dayName,
  353. style: TextStyle(
  354. color: isToday ? ZenColors.currentButtonBackground : ZenColors.currentSecondaryText,
  355. fontSize: 14,
  356. fontWeight: isToday ? FontWeight.bold : FontWeight.normal,
  357. ),
  358. ),
  359. ),
  360. Expanded(
  361. child: Container(
  362. height: 8,
  363. margin: const EdgeInsets.symmetric(horizontal: 10),
  364. child: LinearProgressIndicator(
  365. value: entry.value / (ScoreManager.bestDayScore.clamp(1, double.infinity)),
  366. backgroundColor: ZenColors.currentUiElements.withValues(alpha: 0.3),
  367. valueColor: AlwaysStoppedAnimation<Color>(
  368. isToday ? ZenColors.currentButtonBackground : ZenColors.currentUiElements,
  369. ),
  370. ),
  371. ),
  372. ),
  373. SizedBox(
  374. width: 50,
  375. child: Text(
  376. entry.value.toString(),
  377. style: TextStyle(
  378. color: isToday ? ZenColors.currentButtonBackground : ZenColors.currentPrimaryText,
  379. fontSize: 14,
  380. fontWeight: isToday ? FontWeight.bold : FontWeight.normal,
  381. ),
  382. textAlign: TextAlign.right,
  383. ),
  384. ),
  385. ],
  386. ),
  387. );
  388. }).toList(),
  389. ),
  390. ),
  391. ],
  392. );
  393. }
  394. Widget _buildChartsTab() {
  395. return SingleChildScrollView(
  396. padding: const EdgeInsets.all(20),
  397. child: Column(
  398. crossAxisAlignment: CrossAxisAlignment.start,
  399. children: [
  400. _buildDailyChart(),
  401. const SizedBox(height: 30),
  402. _buildWeeklyChart(),
  403. ],
  404. ),
  405. );
  406. }
  407. Widget _buildDailyChart() {
  408. final lastDays = ScoreManager.getLastDaysScores(14);
  409. final maxScore = lastDays.fold(0, (max, entry) => entry.value > max ? entry.value : max).clamp(1, double.infinity);
  410. return Column(
  411. crossAxisAlignment: CrossAxisAlignment.start,
  412. children: [
  413. Text(
  414. AppLocalizations.of(context)!.dailyProgress14Days,
  415. style: TextStyle(
  416. color: ZenColors.currentPrimaryText,
  417. fontSize: 22,
  418. fontWeight: FontWeight.bold,
  419. ),
  420. ),
  421. const SizedBox(height: 15),
  422. Container(
  423. height: 300,
  424. padding: const EdgeInsets.all(16),
  425. decoration: BoxDecoration(
  426. color: ZenColors.currentUiElements.withValues(alpha: 0.2),
  427. borderRadius: BorderRadius.circular(15),
  428. ),
  429. child: LineChart(
  430. LineChartData(
  431. backgroundColor: Colors.transparent,
  432. gridData: FlGridData(
  433. show: true,
  434. drawVerticalLine: false,
  435. getDrawingHorizontalLine: (value) {
  436. return FlLine(
  437. color: ZenColors.currentUiElements.withValues(alpha: 0.3),
  438. strokeWidth: 1,
  439. );
  440. },
  441. ),
  442. titlesData: FlTitlesData(
  443. bottomTitles: AxisTitles(
  444. sideTitles: SideTitles(
  445. showTitles: true,
  446. getTitlesWidget: (value, meta) {
  447. if (value.toInt() >= 0 && value.toInt() < lastDays.length) {
  448. final date = DateTime.parse(lastDays[value.toInt()].key);
  449. return Text(
  450. '${date.day}',
  451. style: TextStyle(
  452. color: ZenColors.currentSecondaryText,
  453. fontSize: 12,
  454. ),
  455. );
  456. }
  457. return const Text('');
  458. },
  459. ),
  460. ),
  461. leftTitles: AxisTitles(
  462. sideTitles: SideTitles(
  463. showTitles: true,
  464. reservedSize: 40,
  465. getTitlesWidget: (value, meta) {
  466. return Text(
  467. value.toInt().toString(),
  468. style: TextStyle(
  469. color: ZenColors.currentSecondaryText,
  470. fontSize: 12,
  471. ),
  472. );
  473. },
  474. ),
  475. ),
  476. topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
  477. rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
  478. ),
  479. borderData: FlBorderData(show: false),
  480. minX: 0,
  481. maxX: (lastDays.length - 1).toDouble(),
  482. minY: 0,
  483. maxY: maxScore.toDouble(),
  484. lineBarsData: [
  485. LineChartBarData(
  486. spots: lastDays.asMap().entries.map((entry) {
  487. return FlSpot(entry.key.toDouble(), entry.value.value.toDouble());
  488. }).toList(),
  489. isCurved: true,
  490. gradient: LinearGradient(
  491. colors: [
  492. ZenColors.currentButtonBackground.withValues(alpha: 0.8),
  493. ZenColors.currentButtonBackground,
  494. ],
  495. ),
  496. barWidth: 3,
  497. dotData: FlDotData(
  498. show: true,
  499. getDotPainter: (spot, percent, barData, index) {
  500. return FlDotCirclePainter(
  501. radius: 4,
  502. color: ZenColors.currentButtonBackground,
  503. strokeWidth: 2,
  504. strokeColor: ZenColors.currentPrimaryText,
  505. );
  506. },
  507. ),
  508. belowBarData: BarAreaData(
  509. show: true,
  510. gradient: LinearGradient(
  511. begin: Alignment.topCenter,
  512. end: Alignment.bottomCenter,
  513. colors: [
  514. ZenColors.currentButtonBackground.withValues(alpha: 0.3),
  515. ZenColors.currentButtonBackground.withValues(alpha: 0.1),
  516. ],
  517. ),
  518. ),
  519. ),
  520. ],
  521. ),
  522. ),
  523. ),
  524. ],
  525. );
  526. }
  527. Widget _buildWeeklyChart() {
  528. final weeklyScores = ScoreManager.getWeeklyScores(8);
  529. final maxScore = weeklyScores.fold(0, (max, entry) => entry.value > max ? entry.value : max).clamp(1, double.infinity);
  530. return Column(
  531. crossAxisAlignment: CrossAxisAlignment.start,
  532. children: [
  533. Text(
  534. AppLocalizations.of(context)!.weeklySummary8Weeks,
  535. style: TextStyle(
  536. color: ZenColors.currentPrimaryText,
  537. fontSize: 22,
  538. fontWeight: FontWeight.bold,
  539. ),
  540. ),
  541. const SizedBox(height: 15),
  542. Container(
  543. height: 300,
  544. padding: const EdgeInsets.all(16),
  545. decoration: BoxDecoration(
  546. color: ZenColors.currentUiElements.withValues(alpha: 0.2),
  547. borderRadius: BorderRadius.circular(15),
  548. ),
  549. child: BarChart(
  550. BarChartData(
  551. backgroundColor: Colors.transparent,
  552. gridData: FlGridData(
  553. show: true,
  554. drawVerticalLine: false,
  555. getDrawingHorizontalLine: (value) {
  556. return FlLine(
  557. color: ZenColors.currentUiElements.withValues(alpha: 0.3),
  558. strokeWidth: 1,
  559. );
  560. },
  561. ),
  562. titlesData: FlTitlesData(
  563. bottomTitles: AxisTitles(
  564. sideTitles: SideTitles(
  565. showTitles: true,
  566. getTitlesWidget: (value, meta) {
  567. if (value.toInt() >= 0 && value.toInt() < weeklyScores.length) {
  568. return Text(
  569. 'W${value.toInt() + 1}',
  570. style: TextStyle(
  571. color: ZenColors.currentSecondaryText,
  572. fontSize: 12,
  573. ),
  574. );
  575. }
  576. return const Text('');
  577. },
  578. ),
  579. ),
  580. leftTitles: AxisTitles(
  581. sideTitles: SideTitles(
  582. showTitles: true,
  583. reservedSize: 40,
  584. getTitlesWidget: (value, meta) {
  585. return Text(
  586. value.toInt().toString(),
  587. style: TextStyle(
  588. color: ZenColors.currentSecondaryText,
  589. fontSize: 12,
  590. ),
  591. );
  592. },
  593. ),
  594. ),
  595. topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
  596. rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
  597. ),
  598. borderData: FlBorderData(show: false),
  599. maxY: maxScore.toDouble(),
  600. barGroups: weeklyScores.asMap().entries.map((entry) {
  601. return BarChartGroupData(
  602. x: entry.key,
  603. barRods: [
  604. BarChartRodData(
  605. toY: entry.value.value.toDouble(),
  606. gradient: LinearGradient(
  607. begin: Alignment.bottomCenter,
  608. end: Alignment.topCenter,
  609. colors: [
  610. ZenColors.navyBlue.withValues(alpha: 0.8),
  611. ZenColors.navyBlue,
  612. ],
  613. ),
  614. width: 20,
  615. borderRadius: const BorderRadius.only(
  616. topLeft: Radius.circular(6),
  617. topRight: Radius.circular(6),
  618. ),
  619. ),
  620. ],
  621. );
  622. }).toList(),
  623. ),
  624. ),
  625. ),
  626. ],
  627. );
  628. }
  629. String _getDayName(int weekday) {
  630. final l10n = AppLocalizations.of(context)!;
  631. final days = [l10n.monday, l10n.tuesday, l10n.wednesday, l10n.thursday, l10n.friday, l10n.saturday, l10n.sunday];
  632. return days[weekday - 1];
  633. }
  634. bool _isToday(DateTime date) {
  635. final now = DateTime.now();
  636. return date.year == now.year && date.month == now.month && date.day == now.day;
  637. }
  638. }