stats_screen.dart 27 KB

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