stats_screen.dart 21 KB

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