From dc1e096f659da55089ec824c5a540f2b49b2708b Mon Sep 17 00:00:00 2001 From: tenzin dhakar Date: Sat, 27 Jun 2026 11:31:35 +0530 Subject: [PATCH 01/25] chore(dependencies): update package versions and improve UI components - Updated 'meta' package to version 1.17.0 and 'test_api' package to version 0.7.7 in pubspec.lock. - Modified Podfile.lock to reflect updated checksums for several dependencies, including Firebase and connectivity packages. - Enhanced UI in edit_routine_screen.dart by adding padding and background styling to the Done button. - Improved routine_filled_state.dart and routine_item_card.dart for better visual presentation and consistency in item cards. - Refactored routine_time_block.dart to enhance layout and interaction for routine items, including a delete button and improved card styling. --- ios/Podfile.lock | 54 +++++++------- .../screens/edit_routine_screen.dart | 20 +++-- .../widgets/routine_filled_state.dart | 27 +++++-- .../widgets/routine_item_card.dart | 24 +++--- .../widgets/routine_time_block.dart | 73 +++++++++++++++---- pubspec.lock | 8 +- 6 files changed, 138 insertions(+), 68 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 1a9d4156..7e845945 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -365,18 +365,18 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: airbridge-ios-sdk: 6049688ddcfafa85da5ab35b943430c3309ca97b - airbridge_flutter_sdk: 79d47074aae010182e265e9bdac9c904d9a01f4d - app_links: 585674be3c6661708e6cd794ab4f39fb9d8356f9 - audio_service: cab6c1a0eaf01b5a35b567e11fa67d3cc1956910 - audio_session: 19e9480dbdd4e5f6c4543826b2e8b0e4ab6145fe + airbridge_flutter_sdk: 31632f81cacf04d55acc4e6ef17f536e389a8d0c + app_links: 3dbc685f76b1693c66a6d9dd1e9ab6f73d97dc0a + audio_service: aa99a6ba2ae7565996015322b0bb024e1d25c6fd + audio_session: 9bb7f6c970f21241b19f5a3658097ae459681ba0 Auth0: 2876d0c36857422eda9cb580a6cc896c7d14cb36 - auth0_flutter: 0f4846524696ef8441bcb96ceab7bfdbe31b8a05 - connectivity_plus: 2a701ffec2c0ae28a48cf7540e279787e77c447d + auth0_flutter: 031042ca6cf84f1b21611062b33ea7ec4bf3bc52 + connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd Firebase: a8539b633d474fbeb654c7043f9c1649e274045b - firebase_analytics: 444603cf97c21326b60115cd18d28f4a56bd78f6 - firebase_core: 2f51bd38a4e012634cf229f81dc9e2506df32f1c - firebase_crashlytics: 251d4c57b75af742297bb7a39d50b40b9caf88cc - firebase_messaging: 3be95fbe2341c658933c431bfc01462de1c984ed + firebase_analytics: 5e356f558625ce2d8b21deff00946b56d6e8d0d5 + firebase_core: fc23178af8ea070194d09031ae4198a9608a3d22 + firebase_crashlytics: 344bb168f55aee1086c6cdd0b105a9db018cd344 + firebase_messaging: 1fb9402b2037a14b4725773fce04300cbaeb9e0c FirebaseAnalytics: 9c9fa7915fc52ea03077000d5a7b6a8947b2d76e FirebaseCore: 2e86a4ea1684d4381707069e4a6d89ac808e901e FirebaseCoreExtension: 10d2a627977b39418759ad88ada80fbbd34f1c4f @@ -387,38 +387,38 @@ SPEC CHECKSUMS: FirebaseRemoteConfigInterop: 7e3d57ce4b1e958bb1d15403faa7178f46bbb5b7 FirebaseSessions: acfe7eadca47cda94ac86592737204581bb1abf6 Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 - flutter_image_compress_common: ec1d45c362c9d30a3f6a0426c297f47c52007e3e - flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4 - flutter_local_notifications: ff50f8405aaa0ccdc7dcfb9022ca192e8ad9688f - flutter_secure_storage_darwin: 557817588b80e60213cbecb573c45c76b788018d - flutter_timezone: ac3da59ac941ff1c98a2e1f0293420e020120282 + flutter_image_compress_common: 1697a328fd72bfb335507c6bca1a65fa5ad87df1 + flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99 + flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb + flutter_secure_storage_darwin: acdb3f316ed05a3e68f856e0353b133eec373a23 + flutter_timezone: 7c838e17ffd4645d261e87037e5bebf6d38fe544 GoogleAdsOnDeviceConversion: 80ce443fa1b4b5750913d53a04ecda644ff57744 GoogleAppMeasurement: a6d37949071d456e9147dac6789c4342e0e7a8c5 GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 GoogleUtilities: 4f2618a4a1e762a1ee134a1e2323bba9843e06da - image_gallery_saver_plus: 782ade975fe6a4600b53e7c1983e3a2979d1e9e5 - image_picker_ios: 4f2f91b01abdb52842a8e277617df877e40f905b - just_audio: a42c63806f16995daf5b219ae1d679deb76e6a79 + image_gallery_saver_plus: e597bf65a7846979417a3eae0763b71b6dfec6c3 + image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326 + just_audio: 4e391f57b79cad2b0674030a00453ca5ce817eed JWTDecode: 7dae24cb9bf9b608eae61e5081029ec169bb5527 libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8 Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94 - package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4 - path_provider_foundation: 0b743cbb62d8e47eab856f09262bb8c1ddcfe6ba - permission_handler_apple: ee2fe0fd04551b304eb002714ff067c371c822ed + package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 + path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880 + permission_handler_apple: 92d754bbaa7361d436db2d6c3c1c2a0fdcec462e PostHog: 82eee1ba92efc8144b06b6e4412abba5438c8fda - posthog_flutter: 70c295afe8f9eb52e99eefe031af0001bdf9b735 + posthog_flutter: 343db84a70d44b12fa0fe2fc35bbb58098074bd4 PromisesObjC: 752c3227f599e3467650e47ea36f433eeb10c273 PromisesSwift: 217dea0fd5d2ad65222a109c48698add13cc1c5b SDWebImage: e9fc87c1aab89a8ab1bbd74eba378c6f53be8abf SDWebImageWebPCoder: 0e06e365080397465cc73a7a9b472d8a3bd0f377 - share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f - shared_preferences_foundation: 5086985c1d43c5ba4d5e69a4e8083a389e2909e6 + share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a + shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb SimpleKeychain: 768cf43ae778b1c21816e94dddf01bb8ee96a075 - sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d - url_launcher_ios: bb13df5870e8c4234ca12609d04010a21be43dfa - webview_flutter_wkwebview: 29eb20d43355b48fe7d07113835b9128f84e3af4 + sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 + url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b + webview_flutter_wkwebview: 8ebf4fded22593026f7dbff1fbff31ea98573c8d PODFILE CHECKSUM: d93c32cc7d677c987ce520e85932c4ff335f3ec2 diff --git a/lib/features/practice/presentation/screens/edit_routine_screen.dart b/lib/features/practice/presentation/screens/edit_routine_screen.dart index 70c45b27..37584467 100644 --- a/lib/features/practice/presentation/screens/edit_routine_screen.dart +++ b/lib/features/practice/presentation/screens/edit_routine_screen.dart @@ -1503,12 +1503,20 @@ class _DoneButton extends StatelessWidget { HapticFeedback.lightImpact(); onTap(); }, - child: Text( - label, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: isDark ? AppColors.textPrimaryDark : AppColors.textPrimary, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), + decoration: BoxDecoration( + color: isDark ? AppColors.surfaceVariantDark : AppColors.grey100, + borderRadius: BorderRadius.circular(20), + ), + child: Text( + label, + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w500, + color: + isDark ? AppColors.textPrimaryDark : AppColors.textPrimary, + ), ), ), ), diff --git a/lib/features/practice/presentation/widgets/routine_filled_state.dart b/lib/features/practice/presentation/widgets/routine_filled_state.dart index 65b81f53..fea00f4b 100644 --- a/lib/features/practice/presentation/widgets/routine_filled_state.dart +++ b/lib/features/practice/presentation/widgets/routine_filled_state.dart @@ -373,14 +373,26 @@ class _RoutineBlockSection extends ConsumerWidget { ), const SizedBox(height: 8), for (int i = 0; i < block.items.length; i++) ...[ - _buildItemCard(context, ref, block.items[i]), - if (i < block.items.length - 1) const Divider(height: 1, indent: 80), - ], - if (block.items.isNotEmpty) - const Padding( - padding: EdgeInsets.only(top: 8.0), - child: Divider(height: 1), + Container( + margin: EdgeInsets.only( + bottom: i < block.items.length - 1 ? 8.0 : 0, + ), + decoration: BoxDecoration( + color: isDark + ? AppColors.cardBackgroundDark + : AppColors.cardBackgroundLight, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: isDark ? AppColors.cardBorderDark : AppColors.grey100, + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: _buildItemCard(context, ref, block.items[i]), + ), ), + ], + const SizedBox(height: 8), ], ); } @@ -391,6 +403,7 @@ class _RoutineBlockSection extends ConsumerWidget { coverImage: item.coverImage, type: item.type, planTitle: item.currentPlanTitle, + imageSize: 56, onTap: () => _onItemTap(context, ref, item), onPlanTap: item.currentPlanId != null diff --git a/lib/features/practice/presentation/widgets/routine_item_card.dart b/lib/features/practice/presentation/widgets/routine_item_card.dart index 1690119a..fc3d6a76 100644 --- a/lib/features/practice/presentation/widgets/routine_item_card.dart +++ b/lib/features/practice/presentation/widgets/routine_item_card.dart @@ -29,6 +29,9 @@ class RoutineItemCard extends StatelessWidget { /// Optional callback for the circular plan navigation button on the right. final VoidCallback? onPlanTap; + /// Size of the cover image/icon placeholder. Defaults to 74. + final double imageSize; + const RoutineItemCard({ super.key, required this.title, @@ -42,6 +45,7 @@ class RoutineItemCard extends StatelessWidget { this.planTitle, this.trailing, this.onPlanTap, + this.imageSize = 74, }); @override @@ -85,15 +89,15 @@ class RoutineItemCard extends StatelessWidget { borderRadius: BorderRadius.circular(10), child: Image.asset( AppAssets.recitationCoverDefault, - width: 74, - height: 74, + width: imageSize, + height: imageSize, fit: BoxFit.cover, ), ) else if (type == RoutineItemType.timer) Container( - width: 74, - height: 74, + width: imageSize, + height: imageSize, decoration: BoxDecoration( color: isDark ? AppColors.surfaceVariantDark @@ -102,7 +106,7 @@ class RoutineItemCard extends StatelessWidget { ), child: Icon( PhosphorIconsRegular.timer, - size: 32, + size: imageSize * 0.45, color: isDark ? AppColors.textTertiaryDark : AppColors.textSecondary, @@ -110,8 +114,8 @@ class RoutineItemCard extends StatelessWidget { ) else if (type == RoutineItemType.accumulator) Container( - width: 74, - height: 74, + width: imageSize, + height: imageSize, decoration: BoxDecoration( color: isDark ? AppColors.surfaceVariantDark @@ -120,7 +124,7 @@ class RoutineItemCard extends StatelessWidget { ), child: Icon( PhosphorIconsRegular.circlesThree, - size: 32, + size: imageSize * 0.45, color: isDark ? AppColors.textTertiaryDark : AppColors.textSecondary, @@ -133,8 +137,8 @@ class RoutineItemCard extends StatelessWidget { (imageUrl != null && imageUrl!.isNotEmpty ? ResponsiveImage.uniform(imageUrl!) : null), - width: 74, - height: 74, + width: imageSize, + height: imageSize, fit: BoxFit.cover, borderRadius: BorderRadius.circular(10), ), diff --git a/lib/features/practice/presentation/widgets/routine_time_block.dart b/lib/features/practice/presentation/widgets/routine_time_block.dart index 5d1597de..f674c946 100644 --- a/lib/features/practice/presentation/widgets/routine_time_block.dart +++ b/lib/features/practice/presentation/widgets/routine_time_block.dart @@ -99,25 +99,70 @@ class RoutineTimeBlock extends StatelessWidget { proxyDecorator: (child, index, animation) { return Material( elevation: 2, - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(12), child: child, ); }, itemBuilder: (context, i) { final item = items[i]; - return Column( + return Padding( key: ValueKey(item.id), - mainAxisSize: MainAxisSize.min, - children: [ - RoutineItemCard( - title: item.title, - coverImage: item.coverImage, - type: item.type, - onDelete: () => _confirmDeleteItem(context, i), - reorderIndex: i, - ), - const Divider(height: 1, indent: 140), - ], + padding: const EdgeInsets.only(bottom: 8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // Minus button — outside the card + GestureDetector( + onTap: () => _confirmDeleteItem(context, i), + child: Container( + width: 28, + height: 28, + margin: const EdgeInsets.only(right: 12), + decoration: BoxDecoration( + color: isDark + ? AppColors.surfaceVariantDark + : AppColors.grey100, + shape: BoxShape.circle, + ), + child: Center( + child: Icon( + AppAssets.minus, + size: 14, + color: isDark + ? AppColors.textPrimaryDark + : AppColors.textPrimary, + ), + ), + ), + ), + // White card containing item content + drag handle + Expanded( + child: Container( + decoration: BoxDecoration( + color: isDark + ? AppColors.cardBackgroundDark + : AppColors.cardBackgroundLight, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: isDark + ? AppColors.cardBorderDark + : AppColors.grey100, + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: RoutineItemCard( + title: item.title, + coverImage: item.coverImage, + type: item.type, + reorderIndex: i, + imageSize: 56, + ), + ), + ), + ), + ], + ), ); }, ), @@ -268,7 +313,7 @@ class _AddSessionButton extends StatelessWidget { onTap: onTap, borderRadius: BorderRadius.circular(12), child: Padding( - padding: const EdgeInsets.only(left: 54), + padding: const EdgeInsets.only(left: 40), child: Row( children: [ Container( diff --git a/pubspec.lock b/pubspec.lock index 5f1b34a2..9c6a11c5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1161,10 +1161,10 @@ packages: dependency: transitive description: name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" mime: dependency: transitive description: @@ -1687,10 +1687,10 @@ packages: dependency: transitive description: name: test_api - sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 url: "https://pub.dev" source: hosted - version: "0.7.6" + version: "0.7.7" tibetan_calendar: dependency: "direct main" description: From dd708e8417a081e2226e518316b5ccdd4bda9be7 Mon Sep 17 00:00:00 2001 From: tenzin dhakar Date: Sat, 27 Jun 2026 13:36:45 +0530 Subject: [PATCH 02/25] feat(session_selection): add MantraSessionSelection and integrate into routine management - Introduced MantraSessionSelection class to represent mantra selections from the Malas tab. - Updated EditRoutineScreen to handle mantra selections and prevent duplicate entries in routine blocks. - Enhanced SelectSessionScreen to include a new tab for Malas, allowing users to select mantras for their routines. --- .../data/models/session_selection.dart | 8 + .../screens/edit_routine_screen.dart | 42 + .../screens/select_session_screen.dart | 740 ++++++++++++------ 3 files changed, 567 insertions(+), 223 deletions(-) diff --git a/lib/features/practice/data/models/session_selection.dart b/lib/features/practice/data/models/session_selection.dart index 431f3b1e..862924cc 100644 --- a/lib/features/practice/data/models/session_selection.dart +++ b/lib/features/practice/data/models/session_selection.dart @@ -1,4 +1,5 @@ import 'package:flutter_pecha/features/home/domain/entities/series.dart'; +import 'package:flutter_pecha/features/mala/domain/entities/mantra.dart'; import 'package:flutter_pecha/features/plans/domain/entities/plan.dart'; import 'package:flutter_pecha/features/recitation/data/models/recitation_model.dart'; import 'package:flutter_pecha/features/timer/domain/entities/preset_timer.dart'; @@ -46,3 +47,10 @@ class TimerSessionSelection extends SessionSelection { const TimerSessionSelection(this.timer); } + +/// Represents a mantra/accumulator selection from the Malas tab. +class MantraSessionSelection extends SessionSelection { + final Mantra mantra; + + const MantraSessionSelection(this.mantra); +} diff --git a/lib/features/practice/presentation/screens/edit_routine_screen.dart b/lib/features/practice/presentation/screens/edit_routine_screen.dart index 37584467..08e9c5c3 100644 --- a/lib/features/practice/presentation/screens/edit_routine_screen.dart +++ b/lib/features/practice/presentation/screens/edit_routine_screen.dart @@ -1108,6 +1108,8 @@ class _EditRoutineScreenState extends ConsumerState { await _handleSeriesEnrollmentFromSelection(blockIndex, series); case TimerSessionSelection(:final timer): await _addTimerToBlock(blockIndex, timer); + case MantraSessionSelection(:final mantra): + await _addAccumulatorToBlock(blockIndex, mantra); } } finally { _isSelectingSession = false; @@ -1188,6 +1190,46 @@ class _EditRoutineScreenState extends ConsumerState { } } + Future _addAccumulatorToBlock(int blockIndex, Mantra mantra) async { + if (blockIndex < 0 || blockIndex >= _blocks.length) return; + final block = _blocks[blockIndex]; + + final duplicateInBlock = block.items.any( + (item) => + item.id == mantra.presetId && + item.type == RoutineItemType.accumulator, + ); + if (duplicateInBlock) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(context.l10n.duplicateItem), + duration: const Duration(seconds: 2), + ), + ); + } + return; + } + + final language = ref.read(contentLanguageProvider); + final newItem = RoutineItem( + id: mantra.presetId, + title: mantra.displayTitle(language), + type: RoutineItemType.accumulator, + enrolledAt: DateTime.now(), + ); + setState(() => block.items.add(newItem)); + + try { + await _syncBlock(block); + } catch (e) { + if (mounted) { + setState(() => block.items.remove(newItem)); + _showErrorSnackBar(_mapError(e)); + } + } + } + /// Enrolls the user in [series] (if not already enrolled) and adds the /// series to the tapped [blockIndex]. /// diff --git a/lib/features/practice/presentation/screens/select_session_screen.dart b/lib/features/practice/presentation/screens/select_session_screen.dart index c9a436a0..8fab1be6 100644 --- a/lib/features/practice/presentation/screens/select_session_screen.dart +++ b/lib/features/practice/presentation/screens/select_session_screen.dart @@ -1,33 +1,32 @@ import 'package:flutter/material.dart'; -import 'package:flutter_pecha/core/l10n/generated/app_localizations.dart'; +import 'package:flutter_pecha/core/config/locale/locale_notifier.dart'; import 'package:flutter_pecha/core/constants/app_assets.dart'; import 'package:flutter_pecha/core/theme/app_colors.dart'; import 'package:flutter_pecha/core/utils/app_logger.dart'; import 'package:flutter_pecha/core/widgets/responsive_cover_image.dart'; -import 'package:flutter_pecha/shared/domain/value_objects/responsive_image.dart'; import 'package:flutter_pecha/core/widgets/error_state_widget.dart'; import 'package:flutter_pecha/core/widgets/skeletons/skeletons.dart'; +import 'package:flutter_pecha/features/mala/domain/entities/mantra.dart'; +import 'package:flutter_pecha/features/mala/presentation/providers/mala_providers.dart'; import 'package:flutter_pecha/features/practice/data/models/session_selection.dart'; import 'package:flutter_pecha/features/practice/domain/entities/practice_item.dart'; import 'package:flutter_pecha/features/practice/domain/entities/practice_items_tab.dart'; import 'package:flutter_pecha/features/practice/presentation/providers/practice_items_paginated_provider.dart'; +import 'package:flutter_pecha/features/recitation/data/models/recitation_model.dart'; import 'package:flutter_pecha/features/recitation/presentation/providers/recitations_providers.dart'; import 'package:flutter_pecha/features/recitation/presentation/widgets/recitation_list_skeleton.dart'; +import 'package:flutter_pecha/features/timer/domain/entities/preset_timer.dart'; +import 'package:flutter_pecha/features/timer/presentation/providers/timers_providers.dart'; +import 'package:flutter_pecha/shared/domain/value_objects/responsive_image.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; +import 'package:intl/intl.dart'; +import 'package:phosphor_flutter/phosphor_flutter.dart'; final _logger = AppLogger('SelectSessionScreen'); -/// Combined screen for selecting either a Plan, Series, or Recitation to add -/// to the routine. Returns a [SessionSelection] subtype that the caller -/// (`EditRoutineScreen`) dispatches on. -/// -/// Plans/series come from `GET /practice/items` (page-based). Recitations -/// remain on their own list endpoint. Every item is always shown — there is no -/// list-level filtering. Per-timeblock duplicate rules (a plan can't be added -/// twice to the same block; a series can't be added to a block that already -/// holds all of its active plans) are enforced by the caller -/// (`EditRoutineScreen`) when an item is selected. +/// Session picker with four tabs: Plans · Chants · Malas · Timers. +/// Returns a [SessionSelection] subtype that [EditRoutineScreen] dispatches on. class SelectSessionScreen extends ConsumerStatefulWidget { const SelectSessionScreen({super.key}); @@ -38,23 +37,17 @@ class SelectSessionScreen extends ConsumerStatefulWidget { class _SelectSessionScreenState extends ConsumerState with SingleTickerProviderStateMixin { - /// Tab used by the practice picker. Both plans and series live on the same - /// "Add Plan" tab; recitations get their own tab. static const PracticeItemsTab _practiceTab = PracticeItemsTab.all; late TabController _tabController; final ScrollController _plansScrollController = ScrollController(); - /// ID of the item currently being enrolled/saved (null if idle). - /// Reserved for future inline-loading affordances; today's flow pops - /// immediately, so this stays null in normal use. final String? _enrollingItemId = null; @override void initState() { super.initState(); - _logger.debug('🚀 initState() called'); - _tabController = TabController(length: 2, vsync: this); + _tabController = TabController(length: 4, vsync: this); _plansScrollController.addListener(_onPlansScroll); } @@ -84,15 +77,24 @@ class _SelectSessionScreenState extends ConsumerState Navigator.of(context).pop(selection); } - Future _onRecitationSelected(dynamic recitation) async { + void _onRecitationSelected(RecitationModel recitation) { if (_enrollingItemId != null) return; - Navigator.of(context).pop(RecitationSessionSelection(recitation)); + Navigator.of(context).pop( + RecitationSessionSelection(recitation), + ); + } + + void _onMantraSelected(Mantra mantra) { + Navigator.of(context).pop(MantraSessionSelection(mantra)); + } + + void _onTimerSelected(PresetTimer timer) { + Navigator.of(context).pop(TimerSessionSelection(timer)); } @override Widget build(BuildContext context) { - _logger.debug('🎨 ===== BUILD STARTED ====='); - final localizations = AppLocalizations.of(context)!; + final isDark = Theme.of(context).brightness == Brightness.dark; return Scaffold( appBar: AppBar( @@ -100,67 +102,73 @@ class _SelectSessionScreenState extends ConsumerState icon: const Icon(AppAssets.arrowLeft), onPressed: () => context.pop(), ), - title: Text( - localizations.routine_add_session, - style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18), + title: const Text( + 'Add to session', + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18), ), scrolledUnderElevation: 0, centerTitle: true, + actions: [ + IconButton( + icon: const Icon(Icons.search, size: 22), + onPressed: () {}, + ), + ], bottom: TabBar( controller: _tabController, - tabs: [ - Tab(text: localizations.routine_add_plan), - Tab(text: localizations.routine_add_recitation), + tabs: const [ + Tab(text: 'Plans'), + Tab(text: 'Chants'), + Tab(text: 'Malas'), + Tab(text: 'Timers'), ], labelStyle: const TextStyle( fontWeight: FontWeight.bold, - fontSize: 16, + fontSize: 14, ), unselectedLabelStyle: const TextStyle( fontWeight: FontWeight.normal, - fontSize: 16, + fontSize: 14, ), - labelColor: - Theme.of(context).brightness == Brightness.dark - ? Colors.white - : Colors.black, - unselectedLabelColor: - Theme.of(context).brightness == Brightness.dark - ? Colors.white.withValues(alpha: 0.5) - : Colors.black.withValues(alpha: 0.5), + labelColor: isDark ? AppColors.textPrimaryDark : AppColors.textPrimary, + unselectedLabelColor: isDark + ? AppColors.textTertiaryDark + : AppColors.textSecondary, + indicatorColor: Colors.blue, + indicatorSize: TabBarIndicatorSize.tab, + dividerColor: Colors.transparent, ), ), body: TabBarView( controller: _tabController, children: [ - _PracticeItemsTab( + _PlansTab( tab: _practiceTab, scrollController: _plansScrollController, enrollingItemId: _enrollingItemId, onItemSelected: _onPracticeItemSelected, ), - _RecitationsTab( + _ChantsTab( enrollingItemId: _enrollingItemId, onRecitationSelected: _onRecitationSelected, ), + _MalasTab(onMantraSelected: _onMantraSelected), + _TimersTab(onTimerSelected: _onTimerSelected), ], ), ); } } -/// Tab content for the practice picker (plans + series). -/// -/// No list-level filtering: every plan and series the API returns is shown. -/// Whether an item can actually be added to the chosen timeblock is decided by -/// the caller (`EditRoutineScreen`) on selection. -class _PracticeItemsTab extends ConsumerWidget { +// ─── Plans tab ─────────────────────────────────────────────────────────────── + +class _PlansTab extends ConsumerWidget { final PracticeItemsTab tab; final ScrollController scrollController; final String? enrollingItemId; final void Function(PracticeItem item) onItemSelected; - const _PracticeItemsTab({ + const _PlansTab({ required this.tab, required this.scrollController, required this.enrollingItemId, @@ -169,11 +177,10 @@ class _PracticeItemsTab extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final localizations = AppLocalizations.of(context)!; final itemsState = ref.watch(practiceItemsPaginatedProvider(tab)); _logger.debug( - '📋 _PracticeItemsTab BUILD: ${itemsState.items.length} items, ' + '📋 _PlansTab BUILD: ${itemsState.items.length} items, ' 'isLoading: ${itemsState.isLoading}, error: ${itemsState.error}', ); @@ -184,9 +191,8 @@ class _PracticeItemsTab extends ConsumerWidget { if (itemsState.error != null && itemsState.items.isEmpty) { return ErrorStateWidget( error: itemsState.error!, - onRetry: - () => - ref.read(practiceItemsPaginatedProvider(tab).notifier).retry(), + onRetry: () => + ref.read(practiceItemsPaginatedProvider(tab).notifier).retry(), customMessage: 'Unable to load plans.\nPlease try again later.', ); } @@ -196,241 +202,529 @@ class _PracticeItemsTab extends ConsumerWidget { if (items.isEmpty && !itemsState.isLoading) { return Center( child: Text( - localizations.no_plans_found, + 'No plans found', style: TextStyle(color: AppColors.textSecondary), ), ); } - return ListView.separated( + return ListView.builder( controller: scrollController, - padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 16.0), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), itemCount: items.length + (itemsState.hasMore ? 1 : 0), - separatorBuilder: (_, __) => const Divider(height: 1), itemBuilder: (context, index) { if (index == items.length) { return Padding( - padding: const EdgeInsets.symmetric(vertical: 16.0), + padding: const EdgeInsets.symmetric(vertical: 16), child: Center( - child: - itemsState.isLoadingMore - ? const CircularProgressIndicator() - : const SizedBox.shrink(), + child: itemsState.isLoadingMore + ? const CircularProgressIndicator() + : const SizedBox.shrink(), ), ); } final item = items[index]; - return _buildItemTile(item); + return Padding( + padding: const EdgeInsets.only(bottom: 8), + child: _buildPlanCard(context, item), + ); }, ); } - Widget _buildItemTile(PracticeItem item) { + Widget _buildPlanCard(BuildContext context, PracticeItem item) { + final isDark = Theme.of(context).brightness == Brightness.dark; + switch (item) { case PracticePlanItem(:final plan): - final isEnrolling = enrollingItemId == plan.id; - return _SessionListTile( - title: plan.title, - subtitle: null, - coverImage: plan.coverImage, - isLoading: isEnrolling, - isDisabled: enrollingItemId != null, - onTap: () => onItemSelected(item), + final dateRange = _formatDateRange( + plan.startDate, + plan.startDate?.add(Duration(days: plan.totalDays)), ); + return _SessionCard( + isDark: isDark, + onTap: enrollingItemId == null ? () => onItemSelected(item) : null, + child: Row( + children: [ + _CoverImage(image: plan.coverImage, size: 56), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + plan.title, + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + if (dateRange != null) ...[ + const SizedBox(height: 3), + Text( + dateRange, + style: TextStyle( + fontSize: 13, + color: isDark + ? AppColors.textTertiaryDark + : AppColors.textSecondary, + ), + ), + ], + ], + ), + ), + ], + ), + ); + case PracticeSeriesItem(:final series): - final isEnrolling = enrollingItemId == series.id; - return _SessionListTile( - title: series.title, - subtitle: - series.subTitle?.isNotEmpty == true ? series.subTitle : null, - coverImage: series.coverImage, - isLoading: isEnrolling, - isDisabled: enrollingItemId != null, - onTap: () => onItemSelected(item), + final dateRange = _formatDateRange(series.startDate, series.endDate); + final subtitle = series.subTitle?.isNotEmpty == true + ? series.subTitle + : dateRange; + return _SessionCard( + isDark: isDark, + onTap: enrollingItemId == null ? () => onItemSelected(item) : null, + child: Row( + children: [ + _CoverImage(image: series.coverImage, size: 56), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + series.title, + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + if (subtitle != null) ...[ + const SizedBox(height: 3), + Text( + subtitle, + style: TextStyle( + fontSize: 13, + color: isDark + ? AppColors.textTertiaryDark + : AppColors.textSecondary, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ], + ), + ), + ], + ), ); } } + + static String? _formatDateRange(DateTime? start, DateTime? end) { + if (start == null) return null; + final fmt = DateFormat('MMM d'); + final startStr = fmt.format(start.toLocal()); + if (end == null) return startStr; + return '$startStr - ${fmt.format(end.toLocal())}'; + } } -/// Tab content for displaying and selecting recitations. -/// Filters out recitations that are already saved or in the routine. -class _RecitationsTab extends ConsumerWidget { +// ─── Chants tab ────────────────────────────────────────────────────────────── + +class _ChantsTab extends ConsumerWidget { final String? enrollingItemId; - final void Function(dynamic recitation) onRecitationSelected; + final void Function(RecitationModel recitation) onRecitationSelected; - const _RecitationsTab({ + const _ChantsTab({ required this.enrollingItemId, required this.onRecitationSelected, }); @override Widget build(BuildContext context, WidgetRef ref) { - final localizations = AppLocalizations.of(context)!; + final isDark = Theme.of(context).brightness == Brightness.dark; final recitationsAsync = ref.watch(recitationsFutureProvider); return recitationsAsync.when( loading: () => const RecitationListSkeleton(), - error: - (error, _) => Center( - child: Text( - localizations.recitations_no_content, - style: TextStyle(color: AppColors.textSecondary), - ), - ), - data: (recitationsEither) { - return recitationsEither.fold( - (failure) => Center( - child: Text( - localizations.recitations_no_content, - style: TextStyle(color: AppColors.textSecondary), - ), + error: (_, __) => Center( + child: Text( + 'Unable to load chants', + style: TextStyle(color: AppColors.textSecondary), + ), + ), + data: (recitationsEither) => recitationsEither.fold( + (_) => Center( + child: Text( + 'Unable to load chants', + style: TextStyle(color: AppColors.textSecondary), ), - (recitations) { - if (recitations.isEmpty) { - return Center( - child: Text( - localizations.recitations_no_content, - style: TextStyle(color: AppColors.textSecondary), + ), + (recitations) { + if (recitations.isEmpty) { + return Center( + child: Text( + 'No chants found', + style: TextStyle(color: AppColors.textSecondary), + ), + ); + } + + return ListView.builder( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + itemCount: recitations.length, + itemBuilder: (context, index) { + final recitation = recitations[index]; + final description = recitation.firstSegment?.content; + + return Padding( + padding: const EdgeInsets.only(bottom: 8), + child: _SessionCard( + isDark: isDark, + onTap: enrollingItemId == null + ? () => onRecitationSelected(recitation) + : null, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 4, + height: description != null ? null : 44, + constraints: const BoxConstraints(minHeight: 44), + decoration: BoxDecoration( + color: isDark + ? AppColors.textTertiaryDark + : AppColors.grey400, + borderRadius: BorderRadius.circular(2), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + recitation.title, + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + if (description != null && + description.isNotEmpty) ...[ + const SizedBox(height: 4), + Text( + description, + style: TextStyle( + fontSize: 13, + color: isDark + ? AppColors.textTertiaryDark + : AppColors.textSecondary, + ), + maxLines: 3, + overflow: TextOverflow.ellipsis, + ), + ], + ], + ), + ), + ], + ), ), ); - } + }, + ); + }, + ), + ); + } +} + +// ─── Malas tab ─────────────────────────────────────────────────────────────── + +class _MalasTab extends ConsumerWidget { + final void Function(Mantra mantra) onMantraSelected; - return ListView.separated( - padding: const EdgeInsets.symmetric( - horizontal: 20.0, - vertical: 16.0, + const _MalasTab({required this.onMantraSelected}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final isDark = Theme.of(context).brightness == Brightness.dark; + final language = ref.watch(localeProvider).languageCode; + final catalogueAsync = ref.watch(malaCatalogueProvider); + + return catalogueAsync.when( + loading: () => const Center(child: CircularProgressIndicator()), + error: (_, __) => Center( + child: Text( + 'Unable to load malas', + style: TextStyle(color: AppColors.textSecondary), + ), + ), + data: (either) => either.fold( + (_) => Center( + child: Text( + 'Unable to load malas', + style: TextStyle(color: AppColors.textSecondary), + ), + ), + (mantras) { + if (mantras.isEmpty) { + return Center( + child: Text( + 'No malas found', + style: TextStyle(color: AppColors.textSecondary), ), - itemCount: recitations.length, - separatorBuilder: (_, __) => const Divider(height: 1), - itemBuilder: (context, index) { - final recitation = recitations[index]; - final isEnrolling = enrollingItemId == recitation.textId; - - return _SessionListTile( - title: recitation.title, - subtitle: null, - imageUrl: AppAssets.recitationCoverDefault, - isLoading: isEnrolling, - isDisabled: enrollingItemId != null, - onTap: () => onRecitationSelected(recitation), - ); - }, ); - }, - ); - }, + } + + return ListView.builder( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + itemCount: mantras.length, + itemBuilder: (context, index) { + final mantra = mantras[index]; + final imageUrl = + mantra.beadImageUrl ?? mantra.mantra?.beadImageUrl; + final title = mantra.displayTitle(language); + + return Padding( + padding: const EdgeInsets.only(bottom: 8), + child: _SessionCard( + isDark: isDark, + onTap: () => onMantraSelected(mantra), + child: Row( + children: [ + _CircularImage(imageUrl: imageUrl, size: 52), + const SizedBox(width: 12), + Expanded( + child: Text( + title, + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + ); + }, + ); + }, + ), ); } } -/// Reusable list tile for session selection (plans, series, recitations). -/// Supports loading and disabled states for enrollment/save feedback. -class _SessionListTile extends StatelessWidget { - final String title; - final String? subtitle; - final ResponsiveImage? coverImage; - final String? imageUrl; - final VoidCallback onTap; - final bool isLoading; - final bool isDisabled; - - const _SessionListTile({ - required this.title, - required this.subtitle, - this.coverImage, - this.imageUrl, - required this.onTap, - this.isLoading = false, - this.isDisabled = false, - }); +// ─── Timers tab ────────────────────────────────────────────────────────────── + +class _TimersTab extends ConsumerWidget { + final void Function(PresetTimer timer) onTimerSelected; + + const _TimersTab({required this.onTimerSelected}); @override - Widget build(BuildContext context) { - return InkWell( - onTap: isDisabled ? null : onTap, - child: AnimatedOpacity( - opacity: isDisabled && !isLoading ? 0.5 : 1.0, - duration: const Duration(milliseconds: 200), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 12.0), - child: Row( - children: [ - ClipRRect( - borderRadius: BorderRadius.circular(8), - child: - coverImage != null && !coverImage!.isEmpty - ? ResponsiveCoverImage( - image: coverImage, - width: 56, - height: 56, - fit: BoxFit.cover, - borderRadius: BorderRadius.circular(8), - ) - : imageUrl?.trim().isNotEmpty == true - ? ResponsiveCoverImage( - image: ResponsiveImage.uniform(imageUrl!), - width: 56, - height: 56, - fit: BoxFit.cover, + Widget build(BuildContext context, WidgetRef ref) { + final isDark = Theme.of(context).brightness == Brightness.dark; + final timersAsync = ref.watch(presetTimersFutureProvider); + + return timersAsync.when( + loading: () => const Center(child: CircularProgressIndicator()), + error: (_, __) => Center( + child: Text( + 'Unable to load timers', + style: TextStyle(color: AppColors.textSecondary), + ), + ), + data: (either) => either.fold( + (_) => Center( + child: Text( + 'Unable to load timers', + style: TextStyle(color: AppColors.textSecondary), + ), + ), + (timers) { + if (timers.isEmpty) { + return Center( + child: Text( + 'No timers found', + style: TextStyle(color: AppColors.textSecondary), + ), + ); + } + + return ListView.builder( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + itemCount: timers.length, + itemBuilder: (context, index) { + final timer = timers[index]; + + return Padding( + padding: const EdgeInsets.only(bottom: 8), + child: _SessionCard( + isDark: isDark, + onTap: () => onTimerSelected(timer), + child: Row( + children: [ + Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: isDark + ? AppColors.surfaceVariantDark + : AppColors.grey100, borderRadius: BorderRadius.circular(8), - ) - : Container( - width: 56, - height: 56, - decoration: BoxDecoration( - color: AppColors.grey100, - borderRadius: BorderRadius.circular(8), - ), - child: Icon( - Icons.music_note, - color: AppColors.textSecondary, - size: 24, + border: Border.all( + color: isDark + ? AppColors.cardBorderDark + : AppColors.grey300, ), ), - ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, + child: Icon( + PhosphorIconsRegular.timer, + size: 22, + color: isDark + ? AppColors.textTertiaryDark + : AppColors.textSecondary, + ), ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - if (subtitle != null && subtitle!.isNotEmpty) ...[ - const SizedBox(height: 2), - Text( - subtitle!, - style: TextStyle( - fontSize: 14, - color: AppColors.textSecondary, + const SizedBox(width: 12), + Expanded( + child: Text( + timer.name, + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), - maxLines: 1, - overflow: TextOverflow.ellipsis, ), ], - ], - ), - ), - if (isLoading) - const Padding( - padding: EdgeInsets.only(left: 12.0), - child: SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator(strokeWidth: 2), ), ), - ], + ); + }, + ); + }, + ), + ); + } +} + +// ─── Shared card wrapper ────────────────────────────────────────────────────── + +class _SessionCard extends StatelessWidget { + final Widget child; + final bool isDark; + final VoidCallback? onTap; + + const _SessionCard({ + required this.child, + required this.isDark, + this.onTap, + }); + + @override + Widget build(BuildContext context) { + return Material( + color: isDark ? AppColors.cardBackgroundDark : AppColors.cardBackgroundLight, + borderRadius: BorderRadius.circular(12), + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(12), + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: isDark ? AppColors.cardBorderDark : AppColors.grey100, + ), ), + child: child, ), ), ); } } + +// ─── Shared image widgets ───────────────────────────────────────────────────── + +/// Rounded-rectangle cover image or grey placeholder. +class _CoverImage extends StatelessWidget { + final ResponsiveImage? image; + final double size; + + const _CoverImage({this.image, required this.size}); + + @override + Widget build(BuildContext context) { + if (image != null && !image!.isEmpty) { + return ResponsiveCoverImage( + image: image, + width: size, + height: size, + fit: BoxFit.cover, + borderRadius: BorderRadius.circular(8), + ); + } + + return Container( + width: size, + height: size, + decoration: BoxDecoration( + color: AppColors.grey100, + borderRadius: BorderRadius.circular(8), + ), + ); + } +} + +/// Circular image for malas, falls back to a grey circle. +class _CircularImage extends StatelessWidget { + final String? imageUrl; + final double size; + + const _CircularImage({this.imageUrl, required this.size}); + + @override + Widget build(BuildContext context) { + if (imageUrl != null && imageUrl!.isNotEmpty) { + return ClipOval( + child: Image.network( + imageUrl!, + width: size, + height: size, + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => _placeholder(), + ), + ); + } + return _placeholder(); + } + + Widget _placeholder() { + return Container( + width: size, + height: size, + decoration: const BoxDecoration( + color: AppColors.grey100, + shape: BoxShape.circle, + ), + ); + } +} From 70986e241ea0ce8b7a6d8ea31ee0d777a4f1ef12 Mon Sep 17 00:00:00 2001 From: tenzin dhakar Date: Sat, 27 Jun 2026 16:20:27 +0530 Subject: [PATCH 03/25] chore(dependencies): update firebase_messaging and refactor screens - Updated firebase_messaging to a new version in Podfile.lock. - Refactored logic in edit_routine_screen.dart to improve block handling and button visibility. - Enhanced select_session_screen.dart by integrating CachedNetworkImage for better image rendering and improved placeholder handling. --- ios/Podfile.lock | 2 +- .../screens/edit_routine_screen.dart | 14 +++----- .../screens/select_session_screen.dart | 32 +++++++++++-------- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 04c85a39..8ee2da07 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -376,7 +376,7 @@ SPEC CHECKSUMS: firebase_analytics: 5e356f558625ce2d8b21deff00946b56d6e8d0d5 firebase_core: fc23178af8ea070194d09031ae4198a9608a3d22 firebase_crashlytics: 344bb168f55aee1086c6cdd0b105a9db018cd344 - firebase_messaging: 1fb9402b2037a14b4725773fce04300cbaeb9e0c + firebase_messaging: 870b2939a7f7cc52bc38ca0250127c45b42f4bad FirebaseAnalytics: 9c9fa7915fc52ea03077000d5a7b6a8947b2d76e FirebaseCore: 2e86a4ea1684d4381707069e4a6d89ac808e901e FirebaseCoreExtension: 10d2a627977b39418759ad88ada80fbbd34f1c4f diff --git a/lib/features/practice/presentation/screens/edit_routine_screen.dart b/lib/features/practice/presentation/screens/edit_routine_screen.dart index 08e9c5c3..2b325886 100644 --- a/lib/features/practice/presentation/screens/edit_routine_screen.dart +++ b/lib/features/practice/presentation/screens/edit_routine_screen.dart @@ -105,9 +105,6 @@ class _EditRoutineScreenState extends ConsumerState { /// Sequential queue so API calls never overlap or race. Future _opQueue = Future.value(); - bool get _isLastBlockEmpty => - _blocks.isNotEmpty && _blocks.last.items.isEmpty; - bool get _hasEmptyBlocks => _blocks.any((b) => b.items.isEmpty); @override @@ -980,13 +977,15 @@ class _EditRoutineScreenState extends ConsumerState { } bool get _isAtMaxBlocks => !canAddBlock(_blocks.length); - bool get _shouldShowAddButton => !_isLastBlockEmpty && !_isAtMaxBlocks; + bool get _shouldShowAddButton => !_hasEmptyBlocks && !_isAtMaxBlocks; int _calculateListItemCount() { return _shouldShowAddButton ? _blocks.length + 1 : _blocks.length; } void _addBlock() { + if (_hasEmptyBlocks) return; + if (_isAtMaxBlocks) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -1348,9 +1347,7 @@ class _EditRoutineScreenState extends ConsumerState { ); } if (widget.initialTimer != null) { - injectedTimerBlock = _injectInitialTimer( - widget.initialTimer!, - ); + injectedTimerBlock = _injectInitialTimer(widget.initialTimer!); } if (widget.initialSeries != null) { injectedSeriesBlock = _injectInitialSeries( @@ -1556,8 +1553,7 @@ class _DoneButton extends StatelessWidget { style: TextStyle( fontSize: 15, fontWeight: FontWeight.w500, - color: - isDark ? AppColors.textPrimaryDark : AppColors.textPrimary, + color: isDark ? AppColors.textPrimaryDark : AppColors.textPrimary, ), ), ), diff --git a/lib/features/practice/presentation/screens/select_session_screen.dart b/lib/features/practice/presentation/screens/select_session_screen.dart index 8fab1be6..d0f957f9 100644 --- a/lib/features/practice/presentation/screens/select_session_screen.dart +++ b/lib/features/practice/presentation/screens/select_session_screen.dart @@ -1,3 +1,4 @@ +import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_pecha/core/config/locale/locale_notifier.dart'; import 'package:flutter_pecha/core/constants/app_assets.dart'; @@ -497,6 +498,9 @@ class _MalasTab extends ConsumerWidget { final imageUrl = mantra.beadImageUrl ?? mantra.mantra?.beadImageUrl; final title = mantra.displayTitle(language); + _logger.debug( + '🪬 Mala[$index] title=$title imageUrl=$imageUrl', + ); return Padding( padding: const EdgeInsets.only(bottom: 8), @@ -694,7 +698,8 @@ class _CoverImage extends StatelessWidget { } } -/// Circular image for malas, falls back to a grey circle. +/// Circular image for malas — uses CachedNetworkImage so bead art renders +/// correctly (same approach as MalaBeads). class _CircularImage extends StatelessWidget { final String? imageUrl; final double size; @@ -705,26 +710,25 @@ class _CircularImage extends StatelessWidget { Widget build(BuildContext context) { if (imageUrl != null && imageUrl!.isNotEmpty) { return ClipOval( - child: Image.network( - imageUrl!, + child: CachedNetworkImage( + imageUrl: imageUrl!, width: size, height: size, fit: BoxFit.cover, - errorBuilder: (_, __, ___) => _placeholder(), + placeholder: (_, __) => _placeholder(), + errorWidget: (_, __, ___) => _placeholder(), ), ); } return _placeholder(); } - Widget _placeholder() { - return Container( - width: size, - height: size, - decoration: const BoxDecoration( - color: AppColors.grey100, - shape: BoxShape.circle, - ), - ); - } + Widget _placeholder() => Container( + width: size, + height: size, + decoration: const BoxDecoration( + color: AppColors.grey100, + shape: BoxShape.circle, + ), + ); } From 2af0d1341e617a12f42460dfa52194d51b1995f7 Mon Sep 17 00:00:00 2001 From: tenzin dhakar Date: Sat, 27 Jun 2026 16:39:21 +0530 Subject: [PATCH 04/25] feat(localization): integrate localization into SelectSessionScreen - Added localization support by importing AppLocalizations. - Updated screen titles and tab labels to use localized strings for better internationalization. - Enhanced timer display to include localized minute text. --- .../screens/select_session_screen.dart | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/lib/features/practice/presentation/screens/select_session_screen.dart b/lib/features/practice/presentation/screens/select_session_screen.dart index d0f957f9..1381d62e 100644 --- a/lib/features/practice/presentation/screens/select_session_screen.dart +++ b/lib/features/practice/presentation/screens/select_session_screen.dart @@ -23,6 +23,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:intl/intl.dart'; import 'package:phosphor_flutter/phosphor_flutter.dart'; +import 'package:flutter_pecha/core/l10n/generated/app_localizations.dart'; final _logger = AppLogger('SelectSessionScreen'); @@ -96,6 +97,7 @@ class _SelectSessionScreenState extends ConsumerState @override Widget build(BuildContext context) { final isDark = Theme.of(context).brightness == Brightness.dark; + final localizations = AppLocalizations.of(context)!; return Scaffold( appBar: AppBar( @@ -103,8 +105,8 @@ class _SelectSessionScreenState extends ConsumerState icon: const Icon(AppAssets.arrowLeft), onPressed: () => context.pop(), ), - title: const Text( - 'Add to session', + title: Text( + localizations.routine_add_session, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18), ), scrolledUnderElevation: 0, @@ -117,11 +119,11 @@ class _SelectSessionScreenState extends ConsumerState ], bottom: TabBar( controller: _tabController, - tabs: const [ - Tab(text: 'Plans'), - Tab(text: 'Chants'), - Tab(text: 'Malas'), - Tab(text: 'Timers'), + tabs: [ + Tab(text: localizations.home_plans), + Tab(text: localizations.home_chants), + Tab(text: localizations.home_mala), + Tab(text: localizations.home_timer), ], labelStyle: const TextStyle( fontWeight: FontWeight.bold, @@ -544,6 +546,7 @@ class _TimersTab extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final isDark = Theme.of(context).brightness == Brightness.dark; + final localizations = AppLocalizations.of(context)!; final timersAsync = ref.watch(presetTimersFutureProvider); return timersAsync.when( @@ -609,7 +612,7 @@ class _TimersTab extends ConsumerWidget { const SizedBox(width: 12), Expanded( child: Text( - timer.name, + '${timer.displayMinutes} ${localizations.timer_min}', style: const TextStyle( fontSize: 15, fontWeight: FontWeight.w600, From 352d70f7f3906a21f4f2f420b7fa3bb11572b5d8 Mon Sep 17 00:00:00 2001 From: ta4tsering Date: Sat, 27 Jun 2026 23:04:58 +0530 Subject: [PATCH 05/25] feat(ci): auto-bump app version on prod release from commits/branches MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Production releases (push to main) now derive the next semver from the Conventional Commits / branch names landed since the last release tag and bump pubspec.yaml automatically — replacing the manual "chore: bump version". feat! / fix! / BREAKING CHANGE, or a breaking|major/* branch -> major feat:, or a feat|feature/* branch -> minor everything else (fix, chore, …) and the no-convention case -> patch How it fits together: - ci/scripts/bump_version.sh computes the level and rewrites pubspec.yaml (build metadata preserved; CI still sets the real build number). - release-prod.yml gains a `version` job that runs the script, commits the bump back to main with [skip ci], and feeds the new version to both build jobs; the GitHub Release is tagged from it. - build-android.yml / build-ios.yml gain an optional, backward-compatible `version_name` input (falls back to reading pubspec when absent, so dev releases and any other callers are unchanged). The version flows from pubspec via --build-name to the Play Store build, the App Store build, AND the in-app Settings page (PackageInfo), so this single bump keeps all three in sync — no Dart/native changes needed. CONTRIBUTING.md documents the commit/branch conventions for contributors. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/build-android.yml | 5 +- .github/workflows/build-ios.yml | 5 +- .github/workflows/release-prod.yml | 49 ++++++++++++++-- CONTRIBUTING.md | 75 +++++++++++++++++++++++++ ci/scripts/bump_version.sh | 87 +++++++++++++++++++++++++++++ 5 files changed, 214 insertions(+), 7 deletions(-) create mode 100644 CONTRIBUTING.md create mode 100755 ci/scripts/bump_version.sh diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml index 06ddd1cd..daabfcab 100644 --- a/.github/workflows/build-android.yml +++ b/.github/workflows/build-android.yml @@ -15,6 +15,7 @@ on: play_track: { required: false, type: string, default: internal } environment: { required: false, type: string, default: '' } upload_to_store: { required: false, type: boolean, default: true } + version_name: { required: false, type: string, default: '' } # CI-computed semver; falls back to pubspec when empty permissions: contents: read @@ -88,7 +89,9 @@ jobs: CM_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} CM_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} run: | - VERSION_NAME=$(grep '^version:' pubspec.yaml | sed 's/version: //' | cut -d'+' -f1) + # Prefer the CI-computed version (release-prod), else fall back to pubspec. + VERSION_NAME="${{ inputs.version_name }}" + [ -z "$VERSION_NAME" ] && VERSION_NAME=$(grep '^version:' pubspec.yaml | sed 's/version: //' | cut -d'+' -f1) OFFSET="${{ vars.BUILD_NUMBER_OFFSET || '100' }}" BUILD_NUMBER=$(( ${{ github.run_number }} + OFFSET )) echo "Building $VERSION_NAME (+$BUILD_NUMBER)" diff --git a/.github/workflows/build-ios.yml b/.github/workflows/build-ios.yml index 2e5497aa..318589a9 100644 --- a/.github/workflows/build-ios.yml +++ b/.github/workflows/build-ios.yml @@ -20,6 +20,7 @@ on: ios_bundle_id: { required: true, type: string } # org.pecha.app[.dev] environment: { required: false, type: string, default: '' } upload_to_store: { required: false, type: boolean, default: true } + version_name: { required: false, type: string, default: '' } # CI-computed semver; falls back to pubspec when empty permissions: contents: read @@ -164,7 +165,9 @@ jobs: env: AIRBRIDGE_SDK_TOKEN: ${{ secrets.AIRBRIDGE_SDK_TOKEN }} run: | - VERSION_NAME=$(grep '^version:' pubspec.yaml | sed 's/version: //' | cut -d'+' -f1) + # Prefer the CI-computed version (release-prod), else fall back to pubspec. + VERSION_NAME="${{ inputs.version_name }}" + [ -z "$VERSION_NAME" ] && VERSION_NAME=$(grep '^version:' pubspec.yaml | sed 's/version: //' | cut -d'+' -f1) OFFSET="${{ vars.BUILD_NUMBER_OFFSET || '100' }}" BUILD_NUMBER=$(( ${{ github.run_number }} + OFFSET )) echo "Building $VERSION_NAME (+$BUILD_NUMBER)" diff --git a/.github/workflows/release-prod.yml b/.github/workflows/release-prod.yml index aa1b77f6..6127cba4 100644 --- a/.github/workflows/release-prod.yml +++ b/.github/workflows/release-prod.yml @@ -16,7 +16,43 @@ permissions: contents: write # needed to create the GitHub Release + tag jobs: + # Decide the next semantic version from the commits / branch names landed + # since the last release tag (Conventional Commits, PATCH fallback), write it + # to pubspec.yaml and commit it back. Both store builds and the in-app + # settings page read the version from pubspec via --build-name, so this single + # bump keeps all three in sync. See CONTRIBUTING.md for the conventions. + version: + name: 🔖 Bump version + runs-on: ubuntu-latest + permissions: + contents: write + outputs: + version_name: ${{ steps.bump.outputs.version_name }} + steps: + - name: 📂 Checkout (full history + tags) + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: 🔢 Compute & apply version bump + id: bump + run: bash ci/scripts/bump_version.sh + + - name: ⬆️ Commit bumped pubspec.yaml + run: | + if git diff --quiet -- pubspec.yaml; then + echo "pubspec.yaml unchanged — nothing to commit."; exit 0 + fi + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add pubspec.yaml + # [skip ci] so this bump commit does not re-trigger the release. + git commit -m "chore(release): bump version to ${{ steps.bump.outputs.version_name }} [skip ci]" + git push origin HEAD:${{ github.ref_name }} + android: + needs: version uses: ./.github/workflows/build-android.yml with: flavor: prod @@ -26,9 +62,11 @@ jobs: play_track: internal # bump to "production" when ready to ship live environment: production upload_to_store: true + version_name: ${{ needs.version.outputs.version_name }} secrets: inherit ios: + needs: version uses: ./.github/workflows/build-ios.yml with: flavor: prod @@ -38,23 +76,24 @@ jobs: ios_bundle_id: org.pecha.app environment: production upload_to_store: true + version_name: ${{ needs.version.outputs.version_name }} secrets: inherit github_release: name: 📦 GitHub Release - needs: [ android, ios ] + needs: [ version, android, ios ] runs-on: ubuntu-latest permissions: contents: write steps: - - name: 📂 Checkout + - name: 📂 Checkout (post-bump main) uses: actions/checkout@v4 + with: + ref: ${{ github.ref_name }} # tag the commit that carries the bumped version - name: 🏷️ Compute tag id: ver - run: | - V=$(grep '^version:' pubspec.yaml | sed 's/version: //' | cut -d'+' -f1) - echo "tag=v$V-build-${{ github.run_number }}" >> "$GITHUB_OUTPUT" + run: echo "tag=v${{ needs.version.outputs.version_name }}-build-${{ github.run_number }}" >> "$GITHUB_OUTPUT" - name: ⬇️ Download build artifacts uses: actions/download-artifact@v4 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..87fa373e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,75 @@ +# Contributing + +## Versioning & releases + +The app version (`version:` in [`pubspec.yaml`](pubspec.yaml), e.g. `2.9.2+92`) +is **bumped automatically** by CI when changes are released to **production** +(a push/merge to `main` runs the *🚀 Release • Prod* workflow). You do **not** +edit the version by hand. + +That single bump propagates everywhere — there is no other place to update: + +- **Play Store** build → `versionName` (`android/app/build.gradle.kts`) +- **App Store / TestFlight** build → `CFBundleShortVersionString` (`ios/Runner/Info.plist`) +- **In-app Settings page** → the "Version X.Y.Z" label (`PackageInfo` → `appVersionLabelProvider`) + +All three read the version from `flutter build --build-name`, which CI feeds +from the bumped `pubspec.yaml`. + +> Dev/TestFlight builds off `develop` do **not** bump the version — they reuse +> the current version name with a fresh build number. The semantic version only +> advances on a production release. + +### How the bump size is decided + +CI inspects every commit (and merged branch name) since the previous release tag +and picks the **highest** bump it finds. The version is `MAJOR.MINOR.PATCH`: + +| Bump | Version part | Triggered by a commit… | …or a branch named | +| ----- | ------------ | ---------------------- | ------------------ | +| **major** | `X` → `X+1.0.0` | `feat!:` / `fix!:` (any `type!:`), or a `BREAKING CHANGE:` footer | `breaking/*`, `major/*` | +| **minor** | `Y` → `X.Y+1.0` | `feat:` / `feat(scope):` | `feat/*`, `feature/*` | +| **patch** | `Z` → `X.Y.Z+1` | `fix:`, `perf:`, `refactor:`, `chore:`, `docs:`, … | anything else | + +**If no convention is detected at all, the release still ships as a `patch` bump.** +So the conventions are how you *opt into* a bigger bump — nothing breaks if they +are missed, you just get a patch. + +### Commit message format ([Conventional Commits](https://www.conventionalcommits.org)) + +``` +[(optional scope)][!]: + +[optional body] + +[optional BREAKING CHANGE: ] +``` + +Examples: + +``` +feat: add verse-of-the-day widget # → minor (2.9.2 → 2.10.0) +fix(timer): stop preset card overflow # → patch (2.9.2 → 2.9.3) +feat!: drop legacy onboarding route # → major (2.9.2 → 3.0.0) +chore: bump dependencies # → patch +``` + +Common `type` values: `feat`, `fix`, `perf`, `refactor`, `docs`, `style`, +`test`, `build`, `ci`, `chore`. + +### Branch naming + +Use a `type/short-description` prefix; it is used as a fallback signal when the +commit subjects on a PR are not conventional: + +``` +feat/verse-of-day feature/new-routine # → minor +fix/login-crash fix/ci-ios-archive # → patch +breaking/remove-v1-api # → major +``` + +### Build numbers + +The build number (the `+N` after the version, and the store build number) is set +by CI as `github.run_number + BUILD_NUMBER_OFFSET` (repo variable, default `100`) +and is always unique. You don't manage it manually. diff --git a/ci/scripts/bump_version.sh b/ci/scripts/bump_version.sh new file mode 100755 index 00000000..f6bc5f1e --- /dev/null +++ b/ci/scripts/bump_version.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash +# --------------------------------------------------------------------------- +# Auto-bumps the semantic version (the "X.Y.Z" version name) in pubspec.yaml +# based on the Conventional Commits / branch names landed since the last +# release tag. Falls back to a PATCH bump when no convention is detected. +# +# major (X+1.0.0) : a commit "!:" or a "BREAKING CHANGE" footer, +# or a branch named breaking/* or major/* +# minor (X.Y+1.0) : a "feat:"/"feat(scope):" commit, +# or a branch named feat/* or feature/* +# patch (X.Y.Z+1) : anything else (fix, chore, docs, refactor, …) and the +# fallback when no convention is present at all +# +# The "+build" metadata is preserved as-is — CI sets the real, monotonic build +# number at build time (github.run_number + offset), so pubspec's build value +# is only a placeholder. +# +# Outputs (when $GITHUB_OUTPUT is set, i.e. on CI): +# version_name= level= version= +# --------------------------------------------------------------------------- +set -euo pipefail + +PUBSPEC="${PUBSPEC_PATH:-pubspec.yaml}" + +# --- current version ------------------------------------------------------- +current_line="$(grep -E '^version:' "$PUBSPEC" | head -n1)" +current="${current_line#version:}" +current="${current// /}" # strip spaces -> "X.Y.Z+build" +name="${current%%+*}" # "X.Y.Z" +build="" +[ "$current" != "$name" ] && build="${current#*+}" # "build" (empty if no +) + +IFS='.' read -r major minor patch <<<"$name" +: "${major:=0}" "${minor:=0}" "${patch:=0}" + +# --- commit range to inspect (since the last release tag) ------------------ +last_tag="$(git describe --tags --abbrev=0 --match 'v*' 2>/dev/null || true)" +if [ -n "$last_tag" ]; then + range="${last_tag}..HEAD" +else + range="HEAD" # no tags yet: inspect all history +fi + +subjects="$(git log --format='%s' "$range" 2>/dev/null || true)" +bodies="$(git log --format='%b' "$range" 2>/dev/null || true)" +# Branch name behind each merge commit, e.g. "Merge pull request #1 from org/feat/x" -> "feat/x" +branches="$(git log --merges --format='%s' "$range" 2>/dev/null | sed -nE 's/.*from [^/]+\/(.+)$/\1/p' || true)" + +# --- decide the bump level (highest wins) ---------------------------------- +level="patch" + +if printf '%s\n' "$subjects" | grep -qiE '^[[:space:]]*(feat|feature)(\([^)]+\))?!?:' \ + || printf '%s\n' "$branches" | grep -qiE '^(feat|feature)/'; then + level="minor" +fi + +if printf '%s\n' "$subjects" | grep -qiE '^[[:space:]]*[a-z]+(\([^)]+\))?!:' \ + || printf '%s\n' "$bodies" | grep -qiE 'BREAKING[ -]CHANGE' \ + || printf '%s\n' "$branches" | grep -qiE '^(breaking|major)/'; then + level="major" +fi + +case "$level" in + major) major=$((major + 1)); minor=0; patch=0 ;; + minor) minor=$((minor + 1)); patch=0 ;; + patch) patch=$((patch + 1)) ;; +esac + +new_name="${major}.${minor}.${patch}" +new_version="$new_name" +[ -n "$build" ] && new_version="${new_name}+${build}" + +# --- rewrite the top-level version line in pubspec.yaml -------------------- +NEW_VERSION="$new_version" perl -i -pe 'if (!$done && /^version:/) { $_ = "version: $ENV{NEW_VERSION}\n"; $done = 1 }' "$PUBSPEC" + +echo "Last release tag : ${last_tag:-}" +echo "Bump level : $level" +echo "Old version : $current" +echo "New version : $new_version" + +if [ -n "${GITHUB_OUTPUT:-}" ]; then + { + echo "version_name=$new_name" + echo "version=$new_version" + echo "level=$level" + } >>"$GITHUB_OUTPUT" +fi From bff2b559214b428f15a03ac4eb50366425b0aa53 Mon Sep 17 00:00:00 2001 From: tenzin dhakar Date: Mon, 29 Jun 2026 12:20:03 +0530 Subject: [PATCH 06/25] chore: add caretLeft icon to AppAssets; refactor PlanNavigationBottomBar to use AppAssets for icons --- ios/Podfile.lock | 54 +++++++++---------- lib/core/constants/app_assets.dart | 1 + .../plan_navigation_bottom_bar.dart | 40 +++----------- pubspec.lock | 8 +-- 4 files changed, 40 insertions(+), 63 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 850b5925..8ee2da07 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -365,18 +365,18 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: airbridge-ios-sdk: 6049688ddcfafa85da5ab35b943430c3309ca97b - airbridge_flutter_sdk: 79d47074aae010182e265e9bdac9c904d9a01f4d - app_links: 585674be3c6661708e6cd794ab4f39fb9d8356f9 - audio_service: cab6c1a0eaf01b5a35b567e11fa67d3cc1956910 - audio_session: 19e9480dbdd4e5f6c4543826b2e8b0e4ab6145fe + airbridge_flutter_sdk: 31632f81cacf04d55acc4e6ef17f536e389a8d0c + app_links: 3dbc685f76b1693c66a6d9dd1e9ab6f73d97dc0a + audio_service: aa99a6ba2ae7565996015322b0bb024e1d25c6fd + audio_session: 9bb7f6c970f21241b19f5a3658097ae459681ba0 Auth0: 2876d0c36857422eda9cb580a6cc896c7d14cb36 - auth0_flutter: 0f4846524696ef8441bcb96ceab7bfdbe31b8a05 - connectivity_plus: 2a701ffec2c0ae28a48cf7540e279787e77c447d + auth0_flutter: 031042ca6cf84f1b21611062b33ea7ec4bf3bc52 + connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd Firebase: a8539b633d474fbeb654c7043f9c1649e274045b - firebase_analytics: 444603cf97c21326b60115cd18d28f4a56bd78f6 - firebase_core: 2f51bd38a4e012634cf229f81dc9e2506df32f1c - firebase_crashlytics: 251d4c57b75af742297bb7a39d50b40b9caf88cc - firebase_messaging: c8d9730ad2cf85f50f1e16be82ffebcb9a20a792 + firebase_analytics: 5e356f558625ce2d8b21deff00946b56d6e8d0d5 + firebase_core: fc23178af8ea070194d09031ae4198a9608a3d22 + firebase_crashlytics: 344bb168f55aee1086c6cdd0b105a9db018cd344 + firebase_messaging: 870b2939a7f7cc52bc38ca0250127c45b42f4bad FirebaseAnalytics: 9c9fa7915fc52ea03077000d5a7b6a8947b2d76e FirebaseCore: 2e86a4ea1684d4381707069e4a6d89ac808e901e FirebaseCoreExtension: 10d2a627977b39418759ad88ada80fbbd34f1c4f @@ -387,38 +387,38 @@ SPEC CHECKSUMS: FirebaseRemoteConfigInterop: 7e3d57ce4b1e958bb1d15403faa7178f46bbb5b7 FirebaseSessions: acfe7eadca47cda94ac86592737204581bb1abf6 Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 - flutter_image_compress_common: ec1d45c362c9d30a3f6a0426c297f47c52007e3e - flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4 - flutter_local_notifications: ff50f8405aaa0ccdc7dcfb9022ca192e8ad9688f - flutter_secure_storage_darwin: 557817588b80e60213cbecb573c45c76b788018d - flutter_timezone: ac3da59ac941ff1c98a2e1f0293420e020120282 + flutter_image_compress_common: 1697a328fd72bfb335507c6bca1a65fa5ad87df1 + flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99 + flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb + flutter_secure_storage_darwin: acdb3f316ed05a3e68f856e0353b133eec373a23 + flutter_timezone: 7c838e17ffd4645d261e87037e5bebf6d38fe544 GoogleAdsOnDeviceConversion: 80ce443fa1b4b5750913d53a04ecda644ff57744 GoogleAppMeasurement: a6d37949071d456e9147dac6789c4342e0e7a8c5 GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 GoogleUtilities: 4f2618a4a1e762a1ee134a1e2323bba9843e06da - image_gallery_saver_plus: 782ade975fe6a4600b53e7c1983e3a2979d1e9e5 - image_picker_ios: 4f2f91b01abdb52842a8e277617df877e40f905b - just_audio: a42c63806f16995daf5b219ae1d679deb76e6a79 + image_gallery_saver_plus: e597bf65a7846979417a3eae0763b71b6dfec6c3 + image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326 + just_audio: 4e391f57b79cad2b0674030a00453ca5ce817eed JWTDecode: 7dae24cb9bf9b608eae61e5081029ec169bb5527 libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8 Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94 - package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4 - path_provider_foundation: 0b743cbb62d8e47eab856f09262bb8c1ddcfe6ba - permission_handler_apple: ee2fe0fd04551b304eb002714ff067c371c822ed + package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 + path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880 + permission_handler_apple: 92d754bbaa7361d436db2d6c3c1c2a0fdcec462e PostHog: 82eee1ba92efc8144b06b6e4412abba5438c8fda - posthog_flutter: 70c295afe8f9eb52e99eefe031af0001bdf9b735 + posthog_flutter: 343db84a70d44b12fa0fe2fc35bbb58098074bd4 PromisesObjC: 752c3227f599e3467650e47ea36f433eeb10c273 PromisesSwift: 217dea0fd5d2ad65222a109c48698add13cc1c5b SDWebImage: e9fc87c1aab89a8ab1bbd74eba378c6f53be8abf SDWebImageWebPCoder: 0e06e365080397465cc73a7a9b472d8a3bd0f377 - share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f - shared_preferences_foundation: 5086985c1d43c5ba4d5e69a4e8083a389e2909e6 + share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a + shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb SimpleKeychain: 768cf43ae778b1c21816e94dddf01bb8ee96a075 - sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d - url_launcher_ios: bb13df5870e8c4234ca12609d04010a21be43dfa - webview_flutter_wkwebview: 29eb20d43355b48fe7d07113835b9128f84e3af4 + sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 + url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b + webview_flutter_wkwebview: 8ebf4fded22593026f7dbff1fbff31ea98573c8d PODFILE CHECKSUM: d93c32cc7d677c987ce520e85932c4ff335f3ec2 diff --git a/lib/core/constants/app_assets.dart b/lib/core/constants/app_assets.dart index 8541ff93..f1d6a0d9 100644 --- a/lib/core/constants/app_assets.dart +++ b/lib/core/constants/app_assets.dart @@ -71,6 +71,7 @@ class AppAssets { static const IconData caretRight = PhosphorIconsRegular.caretRight; static const IconData caretDown = PhosphorIconsRegular.caretDown; static const IconData caretUp = PhosphorIconsRegular.caretUp; + static const IconData caretLeft = PhosphorIconsRegular.caretLeft; static const IconData arrowSquareOut = PhosphorIconsRegular.arrowSquareOut; static const IconData arrowLeft = PhosphorIconsRegular.arrowLeft; static const IconData lock = PhosphorIconsRegular.lock; diff --git a/lib/features/plans/presentation/widgets/plan_navigation/plan_navigation_bottom_bar.dart b/lib/features/plans/presentation/widgets/plan_navigation/plan_navigation_bottom_bar.dart index ea689623..cc4ecc42 100644 --- a/lib/features/plans/presentation/widgets/plan_navigation/plan_navigation_bottom_bar.dart +++ b/lib/features/plans/presentation/widgets/plan_navigation/plan_navigation_bottom_bar.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_pecha/core/constants/app_assets.dart'; import 'package:flutter_pecha/core/extensions/context_ext.dart'; import 'package:flutter_pecha/features/reader/data/models/navigation_context.dart'; @@ -84,8 +85,7 @@ class PlanNavigationBottomBar extends StatelessWidget { center: _TitleText(text: title, fontFamily: fallbackTitleFontFamily), trailing: () => _NavigationButton( - icon: Icons.check, - isEnabled: true, + icon: AppAssets.check, onTap: onFinish, ), ); @@ -118,8 +118,7 @@ class PlanNavigationBottomBar extends StatelessWidget { leading: hasPrevious ? () => _NavigationButton( - icon: Icons.chevron_left, - isEnabled: true, + icon: AppAssets.caretLeft, onTap: onPreviousTap ?? onPop, ) : null, @@ -127,13 +126,11 @@ class PlanNavigationBottomBar extends StatelessWidget { () => hasNext ? _NavigationButton( - icon: Icons.chevron_right, - isEnabled: true, + icon: AppAssets.caretRight, onTap: onNextTap ?? onPop, ) : _NavigationButton( - icon: Icons.check, - isEnabled: true, + icon: AppAssets.check, onTap: onFinishedTap ?? onPop, ), ); @@ -196,39 +193,18 @@ class _TitleText extends StatelessWidget { class _NavigationButton extends StatelessWidget { final IconData icon; - final bool isEnabled; final VoidCallback onTap; const _NavigationButton({ required this.icon, - required this.isEnabled, required this.onTap, }); @override Widget build(BuildContext context) { - final color = - isEnabled - ? Theme.of(context).colorScheme.onSurface - : Theme.of(context).colorScheme.onSurface.withAlpha(80); - - return Material( - color: Colors.transparent, - child: InkWell( - onTap: onTap, - borderRadius: BorderRadius.circular(24), - child: Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all( - color: color.withAlpha(isEnabled ? 100 : 50), - width: 1, - ), - ), - child: Icon(icon, size: 24, color: color), - ), - ), + return IconButton( + onPressed: onTap, + icon: Icon(icon, size: 24), ); } } diff --git a/pubspec.lock b/pubspec.lock index 852277f7..8feaa84c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1161,10 +1161,10 @@ packages: dependency: transitive description: name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" mime: dependency: transitive description: @@ -1687,10 +1687,10 @@ packages: dependency: transitive description: name: test_api - sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 url: "https://pub.dev" source: hosted - version: "0.7.6" + version: "0.7.7" tibetan_calendar: dependency: "direct main" description: From 627e3e833e8bc0250a4cb9cc177c32c931a4fc85 Mon Sep 17 00:00:00 2001 From: tenzin dhakar Date: Mon, 29 Jun 2026 12:48:19 +0530 Subject: [PATCH 07/25] refactor(plan_text_screen): enhance theme management for improved UI consistency. did mainly the white backgroud. --- .../screens/plan_text_screen.dart | 29 +++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/lib/features/plans/presentation/screens/plan_text_screen.dart b/lib/features/plans/presentation/screens/plan_text_screen.dart index a59f16a5..814683a4 100644 --- a/lib/features/plans/presentation/screens/plan_text_screen.dart +++ b/lib/features/plans/presentation/screens/plan_text_screen.dart @@ -109,21 +109,37 @@ class _PlanTextScreenState extends ConsumerState { // ─── Build ───────────────────────────────────────────────────────────── + ThemeData _readerTheme(BuildContext context) { + final theme = Theme.of(context); + if (theme.brightness != Brightness.light) return theme; + + return theme.copyWith( + scaffoldBackgroundColor: Colors.white, + appBarTheme: theme.appBarTheme.copyWith(backgroundColor: Colors.white), + ); + } + @override Widget build(BuildContext context) { final currentItem = widget.navigationContext.currentItem; final fontSize = ref.watch(fontSizeProvider); + final readerTheme = _readerTheme(context); if (currentItem == null || currentItem.inlineContent == null) { - return _buildMissingContentScaffold(context); + return Theme( + data: readerTheme, + child: _buildMissingContentScaffold(context), + ); } final canSwipe = widget.navigationContext.canSwipe; - return Scaffold( - backgroundColor: Theme.of(context).scaffoldBackgroundColor, - appBar: _buildAppBar(context, currentItem.title), - body: GestureDetector( + return Theme( + data: readerTheme, + child: Scaffold( + backgroundColor: readerTheme.scaffoldBackgroundColor, + appBar: _buildAppBar(context, currentItem.title), + body: GestureDetector( behavior: HitTestBehavior.opaque, onHorizontalDragStart: canSwipe ? _onDragStart : null, onHorizontalDragUpdate: canSwipe ? _onDragUpdate : null, @@ -180,6 +196,7 @@ class _PlanTextScreenState extends ConsumerState { ), ), ), + ), ); } @@ -204,7 +221,9 @@ class _PlanTextScreenState extends ConsumerState { Widget _buildMissingContentScaffold(BuildContext context) { return Scaffold( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, appBar: AppBar( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, leading: IconButton( icon: const Icon(AppAssets.arrowLeft), onPressed: () => context.pop(), From 73499167846814044d0cbc895ff6244a810aeb3e Mon Sep 17 00:00:00 2001 From: tentamdin Date: Mon, 29 Jun 2026 13:30:18 +0530 Subject: [PATCH 08/25] feat(bookmarks): implement bookmark toggle functionality across various screens - Added a new bookmark toggle feature in the SeriesMoreBottomSheet, ReaderMoreBottomSheet, TimerMoreBottomSheet, and MalaSettingsSheet. - Updated the BookmarkController to handle toggling bookmarks instead of just creating them. - Introduced a new BookmarkExistsResult model to check if a bookmark exists before toggling. - Refactored related UI components to reflect bookmark state changes with appropriate icons and loading indicators. - Enhanced the bookmark functionality in the BookmarkRemoteDatasource and BookmarkRepository for better error handling and state management. --- lib/core/constants/app_assets.dart | 1 + .../screens/series_detail_screen.dart | 8 +- .../widgets/series_more_bottom_sheet.dart | 83 ++++++++++++---- .../widgets/mala_settings_sheet.dart | 22 +++-- .../bookmark_remote_datasource.dart | 24 +++++ .../practice/data/models/bookmark_models.dart | 15 +++ .../repositories/bookmark_repository.dart | 17 ++++ .../controllers/bookmark_controller.dart | 99 ++++++++++++------- .../providers/bookmark_providers.dart | 60 +++++++++++ .../screens/bookmarks_screen.dart | 10 ++ .../presentation/screens/reader_screen.dart | 10 +- .../reader_actions/segement_action_bar.dart | 19 +++- .../reader_more_bottom_sheet.dart | 66 ++++++++++--- .../widgets/timer_more_bottom_sheet.dart | 36 ++++--- 14 files changed, 367 insertions(+), 103 deletions(-) diff --git a/lib/core/constants/app_assets.dart b/lib/core/constants/app_assets.dart index 8541ff93..9e7659be 100644 --- a/lib/core/constants/app_assets.dart +++ b/lib/core/constants/app_assets.dart @@ -86,6 +86,7 @@ class AppAssets { static const IconData trash = PhosphorIconsRegular.trash; static const IconData fileText = PhosphorIconsRegular.fileText; static const IconData bookmarkSimple = PhosphorIconsRegular.bookmarkSimple; + static const IconData bookmarkSimpleFill = PhosphorIconsFill.bookmarkSimple; static const IconData speakerSimpleHigh = PhosphorIconsRegular.speakerSimpleHigh; static const IconData vibrate = PhosphorIconsRegular.vibrate; diff --git a/lib/features/home/presentation/screens/series_detail_screen.dart b/lib/features/home/presentation/screens/series_detail_screen.dart index 2beb7930..2290c227 100644 --- a/lib/features/home/presentation/screens/series_detail_screen.dart +++ b/lib/features/home/presentation/screens/series_detail_screen.dart @@ -10,7 +10,6 @@ import 'package:flutter_pecha/features/home/presentation/providers/series_provid import 'package:flutter_pecha/features/home/presentation/widgets/plan_list_view.dart'; import 'package:flutter_pecha/features/home/presentation/widgets/series_more_bottom_sheet.dart'; import 'package:flutter_pecha/features/plans/presentation/providers/user_plans_provider.dart'; -import 'package:flutter_pecha/features/practice/presentation/controllers/bookmark_controller.dart'; import 'package:flutter_pecha/shared/utils/helper_functions.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; @@ -152,12 +151,9 @@ class SeriesDetailScreen extends ConsumerWidget { void _openMoreSheet(BuildContext context, WidgetRef ref, Series series) { showSeriesMoreBottomSheet( context, + seriesId: series.id, + seriesName: series.title, onAddToPractices: () => _onAddToPractices(context, ref, series), - onBookmark: - () => BookmarkController( - ref: ref, - context: context, - ).bookmarkSeries(series.id, name: series.title), ); } diff --git a/lib/features/home/presentation/widgets/series_more_bottom_sheet.dart b/lib/features/home/presentation/widgets/series_more_bottom_sheet.dart index 7df7aedf..ba636e85 100644 --- a/lib/features/home/presentation/widgets/series_more_bottom_sheet.dart +++ b/lib/features/home/presentation/widgets/series_more_bottom_sheet.dart @@ -1,30 +1,64 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_pecha/core/constants/app_assets.dart'; +import 'package:flutter_pecha/core/extensions/context_ext.dart'; +import 'package:flutter_pecha/features/practice/data/datasource/bookmark_remote_datasource.dart'; +import 'package:flutter_pecha/features/practice/presentation/controllers/bookmark_controller.dart'; +import 'package:flutter_pecha/features/practice/presentation/providers/bookmark_providers.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; /// Bottom sheet opened from the three-dot (⋮) button on the series detail /// screen. /// /// Contains: /// • "+ Add to my practices" action -/// • Bookmark action -/// -/// Both callbacks fire after the sheet is dismissed so any feedback -/// (snackbar / login drawer) is shown on the underlying screen rather than -/// behind the closing modal. -class SeriesMoreBottomSheet extends StatelessWidget { +/// • Bookmark toggle action +class SeriesMoreBottomSheet extends ConsumerStatefulWidget { const SeriesMoreBottomSheet({ super.key, + required this.seriesId, + required this.seriesName, this.onAddToPractices, - this.onBookmark, }); + final String seriesId; + final String seriesName; final VoidCallback? onAddToPractices; - final VoidCallback? onBookmark; + + @override + ConsumerState createState() => + _SeriesMoreBottomSheetState(); +} + +class _SeriesMoreBottomSheetState extends ConsumerState { + bool _isBookmarking = false; + + BookmarkTarget get _bookmarkTarget => BookmarkTarget( + type: BookmarkType.series, + sourceId: widget.seriesId, + ); + + Future _toggleBookmark() async { + if (_isBookmarking) return; + setState(() => _isBookmarking = true); + try { + final nav = Navigator.of(context); + await BookmarkController(ref: ref, context: context).toggleSeries( + widget.seriesId, + name: widget.seriesName, + ); + if (mounted) nav.pop(); + } finally { + if (mounted) setState(() => _isBookmarking = false); + } + } @override Widget build(BuildContext context) { final theme = Theme.of(context); + final l10n = context.l10n; + final bookmarkAsync = ref.watch(bookmarkExistsProvider(_bookmarkTarget)); + final isBookmarked = bookmarkAsync.valueOrNull?.exists ?? false; return SafeArea( top: false, @@ -55,22 +89,33 @@ class SeriesMoreBottomSheet extends StatelessWidget { onTap: () { HapticFeedback.lightImpact(); Navigator.of(context).pop(); - onAddToPractices?.call(); + widget.onAddToPractices?.call(); }, ), // ── Bookmark ─────────────────────────────────────────────────── _SectionDivider(theme: theme), ListTile( - leading: Icon( - AppAssets.bookmarkSimple, - color: theme.colorScheme.onSurface, - ), - title: Text('Bookmark', style: theme.textTheme.bodyLarge), + leading: + _isBookmarking + ? SizedBox( + width: 22, + height: 22, + child: CircularProgressIndicator( + strokeWidth: 2, + color: theme.colorScheme.onSurface, + ), + ) + : Icon( + isBookmarked + ? AppAssets.bookmarkSimpleFill + : AppAssets.bookmarkSimple, + color: theme.colorScheme.onSurface, + ), + title: Text(l10n.bookmark, style: theme.textTheme.bodyLarge), onTap: () { HapticFeedback.lightImpact(); - Navigator.of(context).pop(); - onBookmark?.call(); + _toggleBookmark(); }, ), @@ -93,8 +138,9 @@ class _SectionDivider extends StatelessWidget { /// Shows the series "more" bottom sheet. void showSeriesMoreBottomSheet( BuildContext context, { + required String seriesId, + required String seriesName, VoidCallback? onAddToPractices, - VoidCallback? onBookmark, }) { showModalBottomSheet( context: context, @@ -105,8 +151,9 @@ void showSeriesMoreBottomSheet( useRootNavigator: true, builder: (_) => SeriesMoreBottomSheet( + seriesId: seriesId, + seriesName: seriesName, onAddToPractices: onAddToPractices, - onBookmark: onBookmark, ), ); } diff --git a/lib/features/mala/presentation/widgets/mala_settings_sheet.dart b/lib/features/mala/presentation/widgets/mala_settings_sheet.dart index 5def3a38..01654268 100644 --- a/lib/features/mala/presentation/widgets/mala_settings_sheet.dart +++ b/lib/features/mala/presentation/widgets/mala_settings_sheet.dart @@ -9,7 +9,9 @@ import 'package:flutter_pecha/core/widgets/destructive_confirmation_dialog.dart' import 'package:flutter_pecha/features/mala/domain/entities/mantra.dart'; import 'package:flutter_pecha/features/mala/presentation/providers/mala_providers.dart'; import 'package:flutter_pecha/features/mala/presentation/providers/mala_settings_provider.dart'; +import 'package:flutter_pecha/features/practice/data/datasource/bookmark_remote_datasource.dart'; import 'package:flutter_pecha/features/practice/presentation/controllers/bookmark_controller.dart'; +import 'package:flutter_pecha/features/practice/presentation/providers/bookmark_providers.dart'; import 'package:flutter_pecha/shared/widgets/app_toggle_switch.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; @@ -37,6 +39,12 @@ class MalaSettingsSheet extends ConsumerWidget { final settings = ref.watch(malaSettingsProvider); final settingsNotifier = ref.read(malaSettingsProvider.notifier); final isOnline = ref.watch(connectivityNotifierProvider); + final bookmarkAsync = ref.watch( + bookmarkExistsProvider( + BookmarkTarget(type: BookmarkType.accumulator, sourceId: mantra.presetId), + ), + ); + final isBookmarked = bookmarkAsync.valueOrNull?.exists ?? false; final dividerColor = isDark ? AppColors.cardBorderDark : AppColors.grey300; const destructiveColor = Color(0xFFB03027); @@ -67,9 +75,12 @@ class MalaSettingsSheet extends ConsumerWidget { ), Divider(height: 1, color: dividerColor), _MalaSettingsTile( - icon: AppAssets.bookmarkSimple, + icon: + isBookmarked + ? AppAssets.bookmarkSimpleFill + : AppAssets.bookmarkSimple, label: l10n.mala_add_to_bookmark, - onTap: () => _onAddToBookmark(context, ref), + onTap: () => _onToggleBookmark(context, ref), ), Divider(height: 1, color: dividerColor), _MalaSettingsToggleTile( @@ -111,14 +122,11 @@ class MalaSettingsSheet extends ConsumerWidget { router.pushNamed('edit-routine', extra: {'initialMantra': mantra}); } - Future _onAddToBookmark(BuildContext context, WidgetRef ref) async { + Future _onToggleBookmark(BuildContext context, WidgetRef ref) async { final language = ref.read(contentLanguageProvider); final navigator = Navigator.of(context); - // The controller handles the guest → login-drawer flow and shows its own - // success/error snackbar, so it must run before the sheet is dismissed - // (its context drives both). The sheet is closed once the call resolves. - await BookmarkController(ref: ref, context: context).bookmarkMala( + await BookmarkController(ref: ref, context: context).toggleMala( mantra.presetId, name: mantra.displayTitle(language), ); diff --git a/lib/features/practice/data/datasource/bookmark_remote_datasource.dart b/lib/features/practice/data/datasource/bookmark_remote_datasource.dart index 9afff76d..4ae3592a 100644 --- a/lib/features/practice/data/datasource/bookmark_remote_datasource.dart +++ b/lib/features/practice/data/datasource/bookmark_remote_datasource.dart @@ -104,6 +104,30 @@ class BookmarkRemoteDatasource { return all; } + /// GET /users/me/bookmarks/exists + /// + /// [sourceId] – entity id (text, segment, timer, accumulator, series, …) + /// [type] – bookmark kind; pass when known so the check is unambiguous + Future checkBookmarkExists({ + required String sourceId, + BookmarkType? type, + }) async { + final response = await dio.get( + '/users/me/bookmarks/exists', + queryParameters: { + 'source_id': sourceId, + if (type != null) 'type': type.value, + }, + ); + final data = response.data; + if (data is! Map) { + throw const FormatException( + 'Unexpected /users/me/bookmarks/exists payload type', + ); + } + return BookmarkExistsResult.fromJson(data); + } + /// DELETE /users/me/bookmarks/{bookmarkId} Future deleteBookmark(String bookmarkId) async { await dio.delete('/users/me/bookmarks/$bookmarkId'); diff --git a/lib/features/practice/data/models/bookmark_models.dart b/lib/features/practice/data/models/bookmark_models.dart index 5391b655..1eccdb97 100644 --- a/lib/features/practice/data/models/bookmark_models.dart +++ b/lib/features/practice/data/models/bookmark_models.dart @@ -1,5 +1,20 @@ import 'package:flutter_pecha/shared/domain/value_objects/responsive_image.dart'; +/// Response from `GET /users/me/bookmarks/exists`. +class BookmarkExistsResult { + final bool exists; + final String? id; + + const BookmarkExistsResult({required this.exists, this.id}); + + factory BookmarkExistsResult.fromJson(Map json) { + return BookmarkExistsResult( + exists: json['exists'] as bool? ?? false, + id: json['id'] as String?, + ); + } +} + /// Models for `GET /users/me/bookmarks`. /// /// Each row embeds a bookmark-specific object for its type (`text` / `plan` / diff --git a/lib/features/practice/data/repositories/bookmark_repository.dart b/lib/features/practice/data/repositories/bookmark_repository.dart index 24f6deba..170b6a54 100644 --- a/lib/features/practice/data/repositories/bookmark_repository.dart +++ b/lib/features/practice/data/repositories/bookmark_repository.dart @@ -37,6 +37,23 @@ class BookmarkRepository { } } + Future> checkBookmarkExists({ + required String sourceId, + BookmarkType? type, + }) async { + try { + final result = await remoteDatasource.checkBookmarkExists( + sourceId: sourceId, + type: type, + ); + return Right(result); + } catch (e) { + return Left( + ExceptionMapper.map(e, context: 'Failed to check bookmark status'), + ); + } + } + Future> deleteBookmark(String bookmarkId) async { try { await remoteDatasource.deleteBookmark(bookmarkId); diff --git a/lib/features/practice/presentation/controllers/bookmark_controller.dart b/lib/features/practice/presentation/controllers/bookmark_controller.dart index baa30c4b..ec3eb370 100644 --- a/lib/features/practice/presentation/controllers/bookmark_controller.dart +++ b/lib/features/practice/presentation/controllers/bookmark_controller.dart @@ -6,10 +6,11 @@ import 'package:flutter_pecha/features/practice/data/datasource/bookmark_remote_ import 'package:flutter_pecha/features/practice/presentation/providers/bookmark_providers.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -/// Controller for creating bookmarks. +/// Controller for bookmark create/remove (toggle) operations. /// /// Mirrors the [RecitationSaveController] pattern: -/// guest → show login drawer, authenticated → POST and show feedback. +/// guest → show login drawer, authenticated → check exists → POST or DELETE, +/// then invalidate bookmark caches. class BookmarkController { final _logger = AppLogger('BookmarkController'); final WidgetRef ref; @@ -17,41 +18,34 @@ class BookmarkController { BookmarkController({required this.ref, required this.context}); - /// Create a TEXT bookmark for a full text (used from the reader "more" sheet). - Future bookmarkText(String textId) => - _createBookmark(type: BookmarkType.text, sourceId: textId); + /// Toggle a TEXT bookmark for a full text (reader "more" sheet). + Future toggleText(String textId) => + toggle(type: BookmarkType.text, sourceId: textId); - /// Create a VERSE bookmark for a selected segment. - Future bookmarkVerse(String segmentId) => - _createBookmark(type: BookmarkType.verse, sourceId: segmentId); + /// Toggle a VERSE bookmark for a selected segment. + Future toggleVerse(String segmentId) => + toggle(type: BookmarkType.verse, sourceId: segmentId); - /// Create a TIMER bookmark for a preset timer. - Future bookmarkTimer(String timerId) => - _createBookmark(type: BookmarkType.timer, sourceId: timerId); + /// Toggle a TIMER bookmark for a preset timer. + Future toggleTimer(String timerId) => + toggle(type: BookmarkType.timer, sourceId: timerId); - /// Create an ACCUMULATOR bookmark for a preset mala/mantra. - /// - /// [name] is the localized mantra title, stored so the bookmarks list can - /// label the entry without a follow-up lookup. - Future bookmarkMala(String accumulatorId, {String? name}) => - _createBookmark( + /// Toggle an ACCUMULATOR bookmark for a preset mala/mantra. + Future toggleMala(String accumulatorId, {String? name}) => toggle( type: BookmarkType.accumulator, sourceId: accumulatorId, name: name, ); - /// Create a SERIES bookmark. - /// - /// [name] is the series title, stored so the bookmarks list can label the - /// entry without a follow-up lookup. - Future bookmarkSeries(String seriesId, {String? name}) => - _createBookmark( + /// Toggle a SERIES bookmark. + Future toggleSeries(String seriesId, {String? name}) => toggle( type: BookmarkType.series, sourceId: seriesId, name: name, ); - Future _createBookmark({ + /// Creates or removes a bookmark depending on current saved state. + Future toggle({ required BookmarkType type, required String sourceId, String? name, @@ -62,23 +56,50 @@ class BookmarkController { return; } + final target = BookmarkTarget(type: type, sourceId: sourceId); + final repository = ref.read(bookmarkRepositoryProvider); + try { - final result = await ref.read(bookmarkRepositoryProvider).createBookmark( - type: type, - sourceId: sourceId, - name: name, - ); - result.fold( + final existsResult = await repository.checkBookmarkExists( + sourceId: sourceId, + type: type, + ); + + await existsResult.fold( (failure) => throw Exception(failure.message), - (_) => _showSuccessSnackBar(), + (exists) async { + if (exists.exists) { + final bookmarkId = exists.id; + if (bookmarkId == null || bookmarkId.isEmpty) { + throw Exception('Bookmark exists but id is missing'); + } + final deleteResult = await repository.deleteBookmark(bookmarkId); + deleteResult.fold( + (failure) => throw Exception(failure.message), + (_) => _showRemovedSnackBar(), + ); + } else { + final createResult = await repository.createBookmark( + type: type, + sourceId: sourceId, + name: name, + ); + createResult.fold( + (failure) => throw Exception(failure.message), + (_) => _showSavedSnackBar(), + ); + } + }, ); + + invalidateBookmarkCaches(ref, target: target); } catch (e, st) { - _logger.error('Error creating bookmark', e, st); + _logger.error('Error toggling bookmark', e, st); _showErrorSnackBar(); } } - void _showSuccessSnackBar() { + void _showSavedSnackBar() { if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar( const SnackBar( @@ -88,11 +109,21 @@ class BookmarkController { ); } + void _showRemovedSnackBar() { + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Bookmark removed'), + duration: Duration(seconds: 2), + ), + ); + } + void _showErrorSnackBar() { if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar( const SnackBar( - content: Text('Failed to save bookmark'), + content: Text('Failed to update bookmark'), duration: Duration(seconds: 2), ), ); diff --git a/lib/features/practice/presentation/providers/bookmark_providers.dart b/lib/features/practice/presentation/providers/bookmark_providers.dart index fb0800a6..82b57147 100644 --- a/lib/features/practice/presentation/providers/bookmark_providers.dart +++ b/lib/features/practice/presentation/providers/bookmark_providers.dart @@ -1,6 +1,8 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter_pecha/core/config/locale/locale_notifier.dart'; import 'package:flutter_pecha/core/di/core_providers.dart'; import 'package:flutter_pecha/core/utils/app_logger.dart'; +import 'package:flutter_pecha/features/auth/presentation/providers/state_providers.dart'; import 'package:flutter_pecha/features/practice/data/datasource/bookmark_remote_datasource.dart'; import 'package:flutter_pecha/features/practice/data/models/bookmark_models.dart'; import 'package:flutter_pecha/features/practice/data/repositories/bookmark_repository.dart'; @@ -14,6 +16,64 @@ final bookmarkRepositoryProvider = Provider((ref) { ); }); +/// Identifies a bookmarkable entity for exists checks and cache invalidation. +@immutable +class BookmarkTarget { + final BookmarkType type; + final String sourceId; + + const BookmarkTarget({required this.type, required this.sourceId}); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is BookmarkTarget && type == other.type && sourceId == other.sourceId; + + @override + int get hashCode => Object.hash(type, sourceId); +} + +/// Maps list-item types to create/check types. [BookmarkItemType.plan] has no +/// in-app toggle entry point. +BookmarkType? bookmarkTypeFromItem(BookmarkItemType type) => switch (type) { + BookmarkItemType.text => BookmarkType.text, + BookmarkItemType.verse => BookmarkType.verse, + BookmarkItemType.series => BookmarkType.series, + BookmarkItemType.accumulator => BookmarkType.accumulator, + BookmarkItemType.timer => BookmarkType.timer, + BookmarkItemType.plan => null, +}; + +/// Whether [target] is bookmarked for the signed-in user. +/// +/// Guests always resolve to `exists: false` without hitting the network. +final bookmarkExistsProvider = FutureProvider.autoDispose + .family((ref, target) async { + final auth = ref.watch(authProvider); + if (auth.isGuest || !auth.isLoggedIn) { + return const BookmarkExistsResult(exists: false); + } + + final result = await ref + .watch(bookmarkRepositoryProvider) + .checkBookmarkExists( + sourceId: target.sourceId, + type: target.type, + ); + return result.fold( + (failure) => throw Exception(failure.message), + (exists) => exists, + ); + }); + +/// Refreshes bookmark list and optional per-item exists state after mutations. +void invalidateBookmarkCaches(WidgetRef ref, {BookmarkTarget? target}) { + if (target != null) { + ref.invalidate(bookmarkExistsProvider(target)); + } + ref.invalidate(bookmarksProvider); +} + /// The tabs shown on the bookmarks screen, in display order. enum BookmarkTab { all, plans, mala, timers, texts } diff --git a/lib/features/practice/presentation/screens/bookmarks_screen.dart b/lib/features/practice/presentation/screens/bookmarks_screen.dart index 606d2e80..6d0d7471 100644 --- a/lib/features/practice/presentation/screens/bookmarks_screen.dart +++ b/lib/features/practice/presentation/screens/bookmarks_screen.dart @@ -54,6 +54,16 @@ class _BookmarksScreenState extends ConsumerState final messenger = ScaffoldMessenger.of(context); final ok = await ref.read(bookmarksProvider.notifier).remove(bookmark); if (!mounted) return; + if (ok) { + final type = bookmarkTypeFromItem(bookmark.type); + if (type != null) { + ref.invalidate( + bookmarkExistsProvider( + BookmarkTarget(type: type, sourceId: bookmark.sourceId), + ), + ); + } + } messenger.showSnackBar( SnackBar( content: Text(ok ? 'Bookmark removed' : 'Failed to remove bookmark'), diff --git a/lib/features/reader/presentation/screens/reader_screen.dart b/lib/features/reader/presentation/screens/reader_screen.dart index bdb9cc50..af14f87a 100644 --- a/lib/features/reader/presentation/screens/reader_screen.dart +++ b/lib/features/reader/presentation/screens/reader_screen.dart @@ -7,7 +7,6 @@ import 'package:flutter_pecha/features/plans/presentation/widgets/plan_navigatio import 'package:flutter_pecha/features/plans/presentation/widgets/plan_navigation/plan_navigator.dart'; import 'package:flutter_pecha/features/plans/presentation/widgets/plan_navigation/plan_segment_audio_controller.dart'; import 'package:flutter_pecha/features/plans/presentation/widgets/plan_navigation/plan_subtask_completion.dart'; -import 'package:flutter_pecha/features/practice/presentation/controllers/bookmark_controller.dart'; import 'package:flutter_pecha/features/reader/constants/reader_constants.dart'; import 'package:flutter_pecha/features/reader/data/models/navigation_context.dart'; import 'package:flutter_pecha/features/reader/data/models/reader_slot_config.dart'; @@ -539,22 +538,15 @@ class _ReaderScreenState extends ConsumerState showReaderMoreBottomSheet( context, + textId: widget.textId, showAddToPractices: showAddToPractices, onAddToPractices: showAddToPractices ? () => _openRoutineWithRecitation(context, textDetail) : null, - onBookmark: () => _bookmarkText(context), ); } - /// Bookmarks the current text. Invoked after the "more" sheet has been - /// dismissed, using the reader's own context so the success/login feedback - /// isn't drawn behind the closing modal. - void _bookmarkText(BuildContext context) { - BookmarkController(ref: ref, context: context).bookmarkText(widget.textId); - } - void _openRoutineWithRecitation(BuildContext context, TextDetail textDetail) { context.push( AppRoutes.practiceEditRoutine, diff --git a/lib/features/reader/presentation/widgets/reader_actions/segement_action_bar.dart b/lib/features/reader/presentation/widgets/reader_actions/segement_action_bar.dart index 57c8b30c..c0a10358 100644 --- a/lib/features/reader/presentation/widgets/reader_actions/segement_action_bar.dart +++ b/lib/features/reader/presentation/widgets/reader_actions/segement_action_bar.dart @@ -3,7 +3,9 @@ import 'package:flutter/services.dart'; import 'package:flutter_pecha/core/constants/app_assets.dart'; import 'package:flutter_pecha/core/deep_linking/deep_link_url_builder.dart'; import 'package:flutter_pecha/core/extensions/context_ext.dart'; +import 'package:flutter_pecha/features/practice/data/datasource/bookmark_remote_datasource.dart'; import 'package:flutter_pecha/features/practice/presentation/controllers/bookmark_controller.dart'; +import 'package:flutter_pecha/features/practice/presentation/providers/bookmark_providers.dart'; import 'package:flutter_pecha/features/reader/presentation/providers/reader_notifier.dart'; import 'package:flutter_pecha/features/texts/data/models/segment.dart'; import 'package:flutter_pecha/shared/utils/helper_functions.dart'; @@ -62,7 +64,7 @@ class _SegmentActionBarState extends ConsumerState { await BookmarkController( ref: ref, context: context, - ).bookmarkVerse(widget.segment.segmentId); + ).toggleVerse(widget.segment.segmentId); } finally { if (mounted) setState(() => _isBookmarking = false); } @@ -92,6 +94,16 @@ class _SegmentActionBarState extends ConsumerState { return const SizedBox.shrink(); } + final bookmarkAsync = ref.watch( + bookmarkExistsProvider( + BookmarkTarget( + type: BookmarkType.verse, + sourceId: widget.segment.segmentId, + ), + ), + ); + final isBookmarked = bookmarkAsync.valueOrNull?.exists ?? false; + return _ResourcesPanel( onDismiss: widget.onClose, copyButton: _IconActionButton( @@ -109,7 +121,10 @@ class _SegmentActionBarState extends ConsumerState { onClose: widget.onClose, ), bookmarkButton: _IconActionButton( - icon: AppAssets.bookmarkSimple, + icon: + isBookmarked + ? AppAssets.bookmarkSimpleFill + : AppAssets.bookmarkSimple, label: localizations.bookmark, isLoading: _isBookmarking, onTap: _handleBookmark, diff --git a/lib/features/reader/presentation/widgets/reader_app_bar/reader_more_bottom_sheet.dart b/lib/features/reader/presentation/widgets/reader_app_bar/reader_more_bottom_sheet.dart index fa17eb24..943cfbb9 100644 --- a/lib/features/reader/presentation/widgets/reader_app_bar/reader_more_bottom_sheet.dart +++ b/lib/features/reader/presentation/widgets/reader_app_bar/reader_more_bottom_sheet.dart @@ -1,6 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_pecha/core/constants/app_assets.dart'; +import 'package:flutter_pecha/core/extensions/context_ext.dart'; +import 'package:flutter_pecha/features/practice/data/datasource/bookmark_remote_datasource.dart'; +import 'package:flutter_pecha/features/practice/presentation/controllers/bookmark_controller.dart'; +import 'package:flutter_pecha/features/practice/presentation/providers/bookmark_providers.dart'; import 'package:flutter_pecha/features/reader/presentation/widgets/reader_app_bar/reader_font_size_bottom_sheet.dart'; import 'package:flutter_pecha/features/texts/presentation/providers/font_size_notifier.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -10,28 +14,46 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; /// Contains: /// • Font-size A / A buttons /// • "+ Add to my practices" action -/// • Bookmark action +/// • Bookmark toggle action class ReaderMoreBottomSheet extends ConsumerStatefulWidget { const ReaderMoreBottomSheet({ super.key, + required this.textId, required this.showAddToPractices, this.onAddToPractices, - this.onBookmark, }); + final String textId; final bool showAddToPractices; final VoidCallback? onAddToPractices; - /// Invoked after the sheet is dismissed so any feedback (snackbar / login - /// drawer) is shown on the underlying screen rather than behind the modal. - final VoidCallback? onBookmark; - @override ConsumerState createState() => _ReaderMoreBottomSheetState(); } class _ReaderMoreBottomSheetState extends ConsumerState { + bool _isBookmarking = false; + + BookmarkTarget get _bookmarkTarget => BookmarkTarget( + type: BookmarkType.text, + sourceId: widget.textId, + ); + + Future _toggleBookmark() async { + if (_isBookmarking) return; + setState(() => _isBookmarking = true); + try { + final nav = Navigator.of(context); + await BookmarkController(ref: ref, context: context).toggleText( + widget.textId, + ); + if (mounted) nav.pop(); + } finally { + if (mounted) setState(() => _isBookmarking = false); + } + } + // ── font size helpers ────────────────────────────────────────────────────── int _stepIndex(double fontSize) { @@ -55,6 +77,9 @@ class _ReaderMoreBottomSheetState extends ConsumerState { @override Widget build(BuildContext context) { final theme = Theme.of(context); + final l10n = context.l10n; + final bookmarkAsync = ref.watch(bookmarkExistsProvider(_bookmarkTarget)); + final isBookmarked = bookmarkAsync.valueOrNull?.exists ?? false; final fontSize = ref.watch(fontSizeProvider); final stepIndex = _stepIndex(fontSize); @@ -148,15 +173,26 @@ class _ReaderMoreBottomSheetState extends ConsumerState { // ── Bookmark ─────────────────────────────────────────────────── _SectionDivider(theme: theme), ListTile( - leading: Icon( - AppAssets.bookmarkSimple, - color: theme.colorScheme.onSurface, - ), - title: Text('Bookmark', style: theme.textTheme.bodyLarge), + leading: + _isBookmarking + ? SizedBox( + width: 22, + height: 22, + child: CircularProgressIndicator( + strokeWidth: 2, + color: theme.colorScheme.onSurface, + ), + ) + : Icon( + isBookmarked + ? AppAssets.bookmarkSimpleFill + : AppAssets.bookmarkSimple, + color: theme.colorScheme.onSurface, + ), + title: Text(l10n.bookmark, style: theme.textTheme.bodyLarge), onTap: () { HapticFeedback.lightImpact(); - Navigator.of(context).pop(); - widget.onBookmark?.call(); + _toggleBookmark(); }, ), @@ -232,9 +268,9 @@ class _FontSizeButton extends StatelessWidget { /// Shows the reader "more" bottom sheet. void showReaderMoreBottomSheet( BuildContext context, { + required String textId, required bool showAddToPractices, VoidCallback? onAddToPractices, - VoidCallback? onBookmark, }) { showModalBottomSheet( context: context, @@ -244,9 +280,9 @@ void showReaderMoreBottomSheet( ), builder: (_) => ReaderMoreBottomSheet( + textId: textId, showAddToPractices: showAddToPractices, onAddToPractices: onAddToPractices, - onBookmark: onBookmark, ), ); } diff --git a/lib/features/timer/presentation/widgets/timer_more_bottom_sheet.dart b/lib/features/timer/presentation/widgets/timer_more_bottom_sheet.dart index 8efd55ef..3e966c69 100644 --- a/lib/features/timer/presentation/widgets/timer_more_bottom_sheet.dart +++ b/lib/features/timer/presentation/widgets/timer_more_bottom_sheet.dart @@ -1,7 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_pecha/core/constants/app_assets.dart'; +import 'package:flutter_pecha/core/extensions/context_ext.dart'; +import 'package:flutter_pecha/features/practice/data/datasource/bookmark_remote_datasource.dart'; import 'package:flutter_pecha/features/practice/presentation/controllers/bookmark_controller.dart'; +import 'package:flutter_pecha/features/practice/presentation/providers/bookmark_providers.dart'; import 'package:flutter_pecha/features/timer/domain/entities/preset_timer.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -9,7 +12,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; /// /// Contains: /// • "+ Add to my practices" action -/// • Bookmark action +/// • Bookmark toggle action class TimerMoreBottomSheet extends ConsumerStatefulWidget { const TimerMoreBottomSheet({ super.key, @@ -28,14 +31,20 @@ class TimerMoreBottomSheet extends ConsumerStatefulWidget { class _TimerMoreBottomSheetState extends ConsumerState { bool _isBookmarking = false; - Future _bookmark() async { + BookmarkTarget get _bookmarkTarget => BookmarkTarget( + type: BookmarkType.timer, + sourceId: widget.timer.id, + ); + + Future _toggleBookmark() async { if (_isBookmarking) return; setState(() => _isBookmarking = true); try { - await BookmarkController( - ref: ref, - context: context, - ).bookmarkTimer(widget.timer.id); + final nav = Navigator.of(context); + await BookmarkController(ref: ref, context: context).toggleTimer( + widget.timer.id, + ); + if (mounted) nav.pop(); } finally { if (mounted) setState(() => _isBookmarking = false); } @@ -44,6 +53,9 @@ class _TimerMoreBottomSheetState extends ConsumerState { @override Widget build(BuildContext context) { final theme = Theme.of(context); + final l10n = context.l10n; + final bookmarkAsync = ref.watch(bookmarkExistsProvider(_bookmarkTarget)); + final isBookmarked = bookmarkAsync.valueOrNull?.exists ?? false; return SafeArea( top: false, @@ -92,15 +104,15 @@ class _TimerMoreBottomSheetState extends ConsumerState { ), ) : Icon( - AppAssets.bookmarkSimple, + isBookmarked + ? AppAssets.bookmarkSimpleFill + : AppAssets.bookmarkSimple, color: theme.colorScheme.onSurface, ), - title: Text('Bookmark', style: theme.textTheme.bodyLarge), - onTap: () async { + title: Text(l10n.bookmark, style: theme.textTheme.bodyLarge), + onTap: () { HapticFeedback.lightImpact(); - final nav = Navigator.of(context); - await _bookmark(); - if (mounted) nav.pop(); + _toggleBookmark(); }, ), From 242e52a778f13e9ec2ef80578cb5ec5a9393f871 Mon Sep 17 00:00:00 2001 From: tenzin dhakar Date: Mon, 29 Jun 2026 13:44:45 +0530 Subject: [PATCH 09/25] feat: add useRootNavigator option to LoginDrawer and update MoreScreen integration. the bottom up sheet now convers up the navigation bar everywhere on the app. --- .../auth/presentation/widgets/login_drawer.dart | 13 +++++++------ lib/features/more/presentation/more_screen.dart | 5 ++++- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/lib/features/auth/presentation/widgets/login_drawer.dart b/lib/features/auth/presentation/widgets/login_drawer.dart index 0001099a..cad36a6c 100644 --- a/lib/features/auth/presentation/widgets/login_drawer.dart +++ b/lib/features/auth/presentation/widgets/login_drawer.dart @@ -11,10 +11,15 @@ class LoginDrawer extends ConsumerStatefulWidget { const LoginDrawer({super.key}); /// Show the login drawer as a bottom sheet - static Future show(BuildContext context, WidgetRef ref) { + static Future show( + BuildContext context, + WidgetRef ref, { + bool useRootNavigator = false, + }) { return showModalBottomSheet( context: context, isScrollControlled: true, + useRootNavigator: useRootNavigator, backgroundColor: Colors.transparent, isDismissible: true, enableDrag: true, @@ -98,11 +103,7 @@ class _LoginDrawerState extends ConsumerState ), ), // App logo - Image.asset( - AppAssets.weBuddhistLogo, - height: 80, - width: 80, - ), + Image.asset(AppAssets.weBuddhistLogo, height: 80, width: 80), const SizedBox(height: 24), // Title Text( diff --git a/lib/features/more/presentation/more_screen.dart b/lib/features/more/presentation/more_screen.dart index cd998c6e..05871ba7 100644 --- a/lib/features/more/presentation/more_screen.dart +++ b/lib/features/more/presentation/more_screen.dart @@ -133,7 +133,9 @@ class MoreScreen extends ConsumerWidget { context, icon: AppAssets.signIn, title: localizations.sign_in, - onTap: () => LoginDrawer.show(context, ref), + onTap: + () => + LoginDrawer.show(context, ref, useRootNavigator: true), ), ] else ...[ _buildSettingsRow( @@ -306,6 +308,7 @@ class MoreScreen extends ConsumerWidget { borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), isScrollControlled: true, + useRootNavigator: true, builder: (sheetContext) { final theme = Theme.of(sheetContext); final selected = currentLocale ?? Localizations.localeOf(sheetContext); From a7f9e6019969ac58254abc6432876932ebc35ec9 Mon Sep 17 00:00:00 2001 From: tentamdin Date: Mon, 29 Jun 2026 16:52:11 +0530 Subject: [PATCH 10/25] feat(bookmarks): enhance bookmark functionality with caching and prefetching - Introduced a caching mechanism for bookmark existence checks to improve UI responsiveness. - Added prefetching of bookmark states in various screens, including SeriesDetailScreen, MalaScreen, and ReaderScreen. - Refactored bookmark-related providers to streamline the retrieval and management of bookmark states. - Updated UI components to utilize the new caching and prefetching logic for better performance and user experience. --- .../screens/series_detail_screen.dart | 13 ++ .../widgets/series_more_bottom_sheet.dart | 3 +- .../presentation/screens/mala_screen.dart | 14 +- .../widgets/mala_settings_sheet.dart | 5 +- .../controllers/bookmark_controller.dart | 121 +++++++----- .../providers/bookmark_providers.dart | 173 +++++++++++++++++- .../screens/bookmarks_screen.dart | 7 +- .../presentation/screens/reader_screen.dart | 8 + .../reader_actions/segement_action_bar.dart | 5 +- .../reader_more_bottom_sheet.dart | 3 +- .../screens/preset_timers_screen.dart | 18 +- .../widgets/timer_more_bottom_sheet.dart | 3 +- 12 files changed, 300 insertions(+), 73 deletions(-) diff --git a/lib/features/home/presentation/screens/series_detail_screen.dart b/lib/features/home/presentation/screens/series_detail_screen.dart index 2290c227..8bcdde1c 100644 --- a/lib/features/home/presentation/screens/series_detail_screen.dart +++ b/lib/features/home/presentation/screens/series_detail_screen.dart @@ -10,6 +10,8 @@ import 'package:flutter_pecha/features/home/presentation/providers/series_provid import 'package:flutter_pecha/features/home/presentation/widgets/plan_list_view.dart'; import 'package:flutter_pecha/features/home/presentation/widgets/series_more_bottom_sheet.dart'; import 'package:flutter_pecha/features/plans/presentation/providers/user_plans_provider.dart'; +import 'package:flutter_pecha/features/practice/data/datasource/bookmark_remote_datasource.dart'; +import 'package:flutter_pecha/features/practice/presentation/providers/bookmark_providers.dart'; import 'package:flutter_pecha/shared/utils/helper_functions.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; @@ -39,6 +41,17 @@ class SeriesDetailScreen extends ConsumerWidget { ) ?? series; + if (resolvedSeries != null) { + ref.watch( + prefetchBookmarkExistsProvider( + BookmarkTarget( + type: BookmarkType.series, + sourceId: resolvedSeries.id, + ), + ), + ); + } + return Scaffold( backgroundColor: Theme.of(context).scaffoldBackgroundColor, body: SafeArea( diff --git a/lib/features/home/presentation/widgets/series_more_bottom_sheet.dart b/lib/features/home/presentation/widgets/series_more_bottom_sheet.dart index ba636e85..b0597a7e 100644 --- a/lib/features/home/presentation/widgets/series_more_bottom_sheet.dart +++ b/lib/features/home/presentation/widgets/series_more_bottom_sheet.dart @@ -57,8 +57,7 @@ class _SeriesMoreBottomSheetState extends ConsumerState { Widget build(BuildContext context) { final theme = Theme.of(context); final l10n = context.l10n; - final bookmarkAsync = ref.watch(bookmarkExistsProvider(_bookmarkTarget)); - final isBookmarked = bookmarkAsync.valueOrNull?.exists ?? false; + final isBookmarked = ref.watch(isBookmarkedProvider(_bookmarkTarget)); return SafeArea( top: false, diff --git a/lib/features/mala/presentation/screens/mala_screen.dart b/lib/features/mala/presentation/screens/mala_screen.dart index b6bbe9ba..dcadbe7b 100644 --- a/lib/features/mala/presentation/screens/mala_screen.dart +++ b/lib/features/mala/presentation/screens/mala_screen.dart @@ -10,6 +10,8 @@ import 'package:flutter_pecha/features/mala/presentation/widgets/mala_beads.dart import 'package:flutter_pecha/features/mala/presentation/widgets/mala_skeleton.dart'; import 'package:flutter_pecha/features/mala/presentation/widgets/mantra_switcher.dart'; import 'package:flutter_pecha/features/mala/presentation/widgets/mala_settings_sheet.dart'; +import 'package:flutter_pecha/features/practice/data/datasource/bookmark_remote_datasource.dart'; +import 'package:flutter_pecha/features/practice/presentation/providers/bookmark_providers.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; @@ -91,6 +93,10 @@ class _MalaScreenState extends ConsumerState { ); } + void _openMalaSettings(BuildContext context, Mantra mantra) { + MalaSettingsSheet.show(context, mantra: mantra); + } + Widget _buildLoaded(BuildContext context, List mantras) { if (mantras.isEmpty) { return const _MalaAppBarScaffold( @@ -111,6 +117,12 @@ class _MalaScreenState extends ConsumerState { final mantra = mantras[_index]; _trackOpened(mantra); + ref.watch( + prefetchBookmarkExistsProvider( + BookmarkTarget(type: BookmarkType.accumulator, sourceId: mantra.presetId), + ), + ); + final language = Localizations.localeOf(context).languageCode; final counter = ref.watch(malaCounterProvider(mantra)); final notifier = ref.read(malaCounterProvider(mantra).notifier); @@ -120,7 +132,7 @@ class _MalaScreenState extends ConsumerState { children: [ _MalaAppBar( title: mantra.displayTitle(language), - onMorePressed: () => MalaSettingsSheet.show(context, mantra: mantra), + onMorePressed: () => _openMalaSettings(context, mantra), ), Expanded( child: Padding( diff --git a/lib/features/mala/presentation/widgets/mala_settings_sheet.dart b/lib/features/mala/presentation/widgets/mala_settings_sheet.dart index 01654268..39a1fbf9 100644 --- a/lib/features/mala/presentation/widgets/mala_settings_sheet.dart +++ b/lib/features/mala/presentation/widgets/mala_settings_sheet.dart @@ -39,12 +39,11 @@ class MalaSettingsSheet extends ConsumerWidget { final settings = ref.watch(malaSettingsProvider); final settingsNotifier = ref.read(malaSettingsProvider.notifier); final isOnline = ref.watch(connectivityNotifierProvider); - final bookmarkAsync = ref.watch( - bookmarkExistsProvider( + final isBookmarked = ref.watch( + isBookmarkedProvider( BookmarkTarget(type: BookmarkType.accumulator, sourceId: mantra.presetId), ), ); - final isBookmarked = bookmarkAsync.valueOrNull?.exists ?? false; final dividerColor = isDark ? AppColors.cardBorderDark : AppColors.grey300; const destructiveColor = Color(0xFFB03027); diff --git a/lib/features/practice/presentation/controllers/bookmark_controller.dart b/lib/features/practice/presentation/controllers/bookmark_controller.dart index ec3eb370..b7198035 100644 --- a/lib/features/practice/presentation/controllers/bookmark_controller.dart +++ b/lib/features/practice/presentation/controllers/bookmark_controller.dart @@ -3,14 +3,15 @@ import 'package:flutter_pecha/core/utils/app_logger.dart'; import 'package:flutter_pecha/features/auth/presentation/providers/state_providers.dart'; import 'package:flutter_pecha/features/auth/presentation/widgets/login_drawer.dart'; import 'package:flutter_pecha/features/practice/data/datasource/bookmark_remote_datasource.dart'; +import 'package:flutter_pecha/features/practice/data/models/bookmark_models.dart'; +import 'package:flutter_pecha/features/practice/data/repositories/bookmark_repository.dart'; import 'package:flutter_pecha/features/practice/presentation/providers/bookmark_providers.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; /// Controller for bookmark create/remove (toggle) operations. /// -/// Mirrors the [RecitationSaveController] pattern: -/// guest → show login drawer, authenticated → check exists → POST or DELETE, -/// then invalidate bookmark caches. +/// Uses the pre-warmed exists cache for a single API call per tap, with +/// optimistic UI that reverts on failure. class BookmarkController { final _logger = AppLogger('BookmarkController'); final WidgetRef ref; @@ -18,33 +19,28 @@ class BookmarkController { BookmarkController({required this.ref, required this.context}); - /// Toggle a TEXT bookmark for a full text (reader "more" sheet). Future toggleText(String textId) => toggle(type: BookmarkType.text, sourceId: textId); - /// Toggle a VERSE bookmark for a selected segment. Future toggleVerse(String segmentId) => toggle(type: BookmarkType.verse, sourceId: segmentId); - /// Toggle a TIMER bookmark for a preset timer. Future toggleTimer(String timerId) => toggle(type: BookmarkType.timer, sourceId: timerId); - /// Toggle an ACCUMULATOR bookmark for a preset mala/mantra. Future toggleMala(String accumulatorId, {String? name}) => toggle( type: BookmarkType.accumulator, sourceId: accumulatorId, name: name, ); - /// Toggle a SERIES bookmark. Future toggleSeries(String seriesId, {String? name}) => toggle( type: BookmarkType.series, sourceId: seriesId, name: name, ); - /// Creates or removes a bookmark depending on current saved state. + /// Optimistically toggles bookmark state, then POST or DELETE (one call). Future toggle({ required BookmarkType type, required String sourceId, @@ -58,47 +54,78 @@ class BookmarkController { final target = BookmarkTarget(type: type, sourceId: sourceId); final repository = ref.read(bookmarkRepositoryProvider); + final cache = ref.read(bookmarkExistsCacheProvider.notifier); + final previous = readBookmarkStatus(ref, target); + final wasBookmarked = previous.exists; - try { - final existsResult = await repository.checkBookmarkExists( - sourceId: sourceId, - type: type, - ); + cache.set( + target, + BookmarkExistsResult( + exists: !wasBookmarked, + id: wasBookmarked ? null : previous.id, + ), + ); - await existsResult.fold( - (failure) => throw Exception(failure.message), - (exists) async { - if (exists.exists) { - final bookmarkId = exists.id; - if (bookmarkId == null || bookmarkId.isEmpty) { - throw Exception('Bookmark exists but id is missing'); - } - final deleteResult = await repository.deleteBookmark(bookmarkId); - deleteResult.fold( - (failure) => throw Exception(failure.message), - (_) => _showRemovedSnackBar(), - ); - } else { - final createResult = await repository.createBookmark( - type: type, - sourceId: sourceId, - name: name, - ); - createResult.fold( - (failure) => throw Exception(failure.message), - (_) => _showSavedSnackBar(), - ); - } - }, - ); + try { + if (wasBookmarked) { + final bookmarkId = await _resolveBookmarkId( + repository: repository, + target: target, + cachedId: previous.id, + ); + final deleteResult = await repository.deleteBookmark(bookmarkId); + deleteResult.fold( + (failure) => throw Exception(failure.message), + (_) {}, + ); + cache.set(target, const BookmarkExistsResult(exists: false)); + _showRemovedSnackBar(); + } else { + final createResult = await repository.createBookmark( + type: type, + sourceId: sourceId, + name: name, + ); + createResult.fold( + (failure) => throw Exception(failure.message), + (_) {}, + ); + cache.set(target, const BookmarkExistsResult(exists: true)); + _showSavedSnackBar(); + } - invalidateBookmarkCaches(ref, target: target); + ref.invalidate(bookmarksProvider); } catch (e, st) { _logger.error('Error toggling bookmark', e, st); - _showErrorSnackBar(); + cache.set(target, previous); + _showErrorSnackBar(wasBookmarked); } } + /// Uses cached id when available; falls back to exists check only if needed. + Future _resolveBookmarkId({ + required BookmarkRepository repository, + required BookmarkTarget target, + required String? cachedId, + }) async { + if (cachedId != null && cachedId.isNotEmpty) return cachedId; + + final existsResult = await repository.checkBookmarkExists( + sourceId: target.sourceId, + type: target.type, + ); + return existsResult.fold( + (failure) => throw Exception(failure.message), + (exists) { + final id = exists.id; + if (!exists.exists || id == null || id.isEmpty) { + throw Exception('Bookmark exists but id is missing'); + } + return id; + }, + ); + } + void _showSavedSnackBar() { if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar( @@ -119,12 +146,14 @@ class BookmarkController { ); } - void _showErrorSnackBar() { + void _showErrorSnackBar(bool wasBookmarked) { if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Failed to update bookmark'), - duration: Duration(seconds: 2), + SnackBar( + content: Text( + 'Failed to ${wasBookmarked ? 'remove' : 'save'} bookmark', + ), + duration: const Duration(seconds: 2), ), ); } diff --git a/lib/features/practice/presentation/providers/bookmark_providers.dart b/lib/features/practice/presentation/providers/bookmark_providers.dart index 82b57147..b60b275f 100644 --- a/lib/features/practice/presentation/providers/bookmark_providers.dart +++ b/lib/features/practice/presentation/providers/bookmark_providers.dart @@ -44,9 +44,80 @@ BookmarkType? bookmarkTypeFromItem(BookmarkItemType type) => switch (type) { BookmarkItemType.plan => null, }; +/// Finds a bookmark row matching [target] in an already-loaded list. +BookmarkDTO? findBookmarkInList( + List bookmarks, + BookmarkTarget target, +) { + for (final bookmark in bookmarks) { + if (bookmarkTypeFromItem(bookmark.type) == target.type && + bookmark.sourceId == target.sourceId) { + return bookmark; + } + } + return null; +} + +/// In-memory exists cache so UI can render instantly after the first lookup. +class BookmarkExistsCacheNotifier + extends StateNotifier> { + BookmarkExistsCacheNotifier() : super(const {}); + + void set(BookmarkTarget target, BookmarkExistsResult result) { + state = {...state, target: result}; + } + + void clear(BookmarkTarget target) { + if (!state.containsKey(target)) return; + final next = Map.from(state); + next.remove(target); + state = next; + } +} + +final bookmarkExistsCacheProvider = StateNotifierProvider< + BookmarkExistsCacheNotifier, + Map>((ref) { + return BookmarkExistsCacheNotifier(); +}); + +/// Seeds the exists cache from a loaded bookmarks list (positive hits only). +void applyBookmarkListCache( + BookmarkExistsCacheNotifier notifier, + List bookmarks, + BookmarkTarget target, { + required bool alreadyCached, +}) { + if (alreadyCached) return; + + final hit = findBookmarkInList(bookmarks, target); + if (hit != null) { + notifier.set(target, BookmarkExistsResult(exists: true, id: hit.id)); + } +} + +void warmBookmarkExistsCacheFromList(Ref ref, BookmarkTarget target) { + if (ref.read(bookmarkExistsCacheProvider).containsKey(target)) return; + + // Only reuse the list when it is already loaded — never read [bookmarksProvider] + // to spawn a full paginated fetch just for a exists warm-up. + if (!ref.exists(bookmarksProvider)) return; + + final listState = ref.read(bookmarksProvider); + if (listState.isLoading) return; + + applyBookmarkListCache( + ref.read(bookmarkExistsCacheProvider.notifier), + listState.bookmarks, + target, + alreadyCached: false, + ); +} + /// Whether [target] is bookmarked for the signed-in user. /// -/// Guests always resolve to `exists: false` without hitting the network. +/// Checks the in-memory cache first, then the loaded bookmarks list, then +/// `GET /users/me/bookmarks/exists`. Guests resolve to `exists: false`. final bookmarkExistsProvider = FutureProvider.autoDispose .family((ref, target) async { final auth = ref.watch(authProvider); @@ -54,21 +125,111 @@ final bookmarkExistsProvider = FutureProvider.autoDispose return const BookmarkExistsResult(exists: false); } + final cached = ref.read(bookmarkExistsCacheProvider)[target]; + if (cached != null) return cached; + + warmBookmarkExistsCacheFromList(ref, target); + final fromList = ref.read(bookmarkExistsCacheProvider)[target]; + if (fromList != null) return fromList; + final result = await ref - .watch(bookmarkRepositoryProvider) + .read(bookmarkRepositoryProvider) .checkBookmarkExists( sourceId: target.sourceId, type: target.type, ); return result.fold( (failure) => throw Exception(failure.message), - (exists) => exists, + (exists) { + ref.read(bookmarkExistsCacheProvider.notifier).set(target, exists); + ref.keepAlive(); + return exists; + }, ); }); -/// Refreshes bookmark list and optional per-item exists state after mutations. -void invalidateBookmarkCaches(WidgetRef ref, {BookmarkTarget? target}) { - if (target != null) { +/// Starts loading bookmark exists state early so sheets open with the right icon. +/// +/// Uses [ref.watch] (not listen) so [bookmarkExistsProvider] actually runs while +/// the host screen is mounted. +final prefetchBookmarkExistsProvider = + Provider.autoDispose.family((ref, target) { + final auth = ref.watch(authProvider); + if (auth.isGuest || !auth.isLoggedIn) return; + + warmBookmarkExistsCacheFromList(ref, target); + + // If the bookmarks list is already in memory, re-warm when it finishes loading. + if (ref.exists(bookmarksProvider)) { + ref.listen(bookmarksProvider, (_, next) { + if (next.isLoading) return; + applyBookmarkListCache( + ref.read(bookmarkExistsCacheProvider.notifier), + next.bookmarks, + target, + alreadyCached: ref.read(bookmarkExistsCacheProvider).containsKey(target), + ); + }, fireImmediately: true); + } + + ref.watch(bookmarkExistsProvider(target)); + }); + +/// Synchronous bookmark-filled state for UI — prefers cache over async loading. +final isBookmarkedProvider = Provider.autoDispose.family( + (ref, target) { + ref.watch(prefetchBookmarkExistsProvider(target)); + + final cached = ref.watch(bookmarkExistsCacheProvider)[target]; + if (cached != null) return cached.exists; + + return ref.watch(bookmarkExistsProvider(target)).maybeWhen( + data: (result) => result.exists, + orElse: () => false, + ); + }, +); + +/// Reads the best-known bookmark status synchronously (cache → list → async data). +BookmarkExistsResult readBookmarkStatus(WidgetRef ref, BookmarkTarget target) { + final auth = ref.read(authProvider); + if (auth.isGuest || !auth.isLoggedIn) { + return const BookmarkExistsResult(exists: false); + } + + if (!ref.read(bookmarkExistsCacheProvider).containsKey(target)) { + if (ref.exists(bookmarksProvider)) { + final listState = ref.read(bookmarksProvider); + if (!listState.isLoading) { + applyBookmarkListCache( + ref.read(bookmarkExistsCacheProvider.notifier), + listState.bookmarks, + target, + alreadyCached: false, + ); + } + } + } + + final cached = ref.read(bookmarkExistsCacheProvider)[target]; + if (cached != null) return cached; + + return ref.read(bookmarkExistsProvider(target)).maybeWhen( + data: (result) => result, + orElse: () => const BookmarkExistsResult(exists: false), + ); +} + +/// Refreshes the bookmarks list after a mutation. Pass [exists] to commit cache. +void invalidateBookmarkCaches( + WidgetRef ref, { + BookmarkTarget? target, + BookmarkExistsResult? exists, +}) { + if (target != null && exists != null) { + ref.read(bookmarkExistsCacheProvider.notifier).set(target, exists); + } else if (target != null) { + ref.read(bookmarkExistsCacheProvider.notifier).clear(target); ref.invalidate(bookmarkExistsProvider(target)); } ref.invalidate(bookmarksProvider); diff --git a/lib/features/practice/presentation/screens/bookmarks_screen.dart b/lib/features/practice/presentation/screens/bookmarks_screen.dart index 6d0d7471..a895108e 100644 --- a/lib/features/practice/presentation/screens/bookmarks_screen.dart +++ b/lib/features/practice/presentation/screens/bookmarks_screen.dart @@ -57,10 +57,9 @@ class _BookmarksScreenState extends ConsumerState if (ok) { final type = bookmarkTypeFromItem(bookmark.type); if (type != null) { - ref.invalidate( - bookmarkExistsProvider( - BookmarkTarget(type: type, sourceId: bookmark.sourceId), - ), + ref.read(bookmarkExistsCacheProvider.notifier).set( + BookmarkTarget(type: type, sourceId: bookmark.sourceId), + const BookmarkExistsResult(exists: false), ); } } diff --git a/lib/features/reader/presentation/screens/reader_screen.dart b/lib/features/reader/presentation/screens/reader_screen.dart index af14f87a..07bbffbe 100644 --- a/lib/features/reader/presentation/screens/reader_screen.dart +++ b/lib/features/reader/presentation/screens/reader_screen.dart @@ -7,6 +7,8 @@ import 'package:flutter_pecha/features/plans/presentation/widgets/plan_navigatio import 'package:flutter_pecha/features/plans/presentation/widgets/plan_navigation/plan_navigator.dart'; import 'package:flutter_pecha/features/plans/presentation/widgets/plan_navigation/plan_segment_audio_controller.dart'; import 'package:flutter_pecha/features/plans/presentation/widgets/plan_navigation/plan_subtask_completion.dart'; +import 'package:flutter_pecha/features/practice/data/datasource/bookmark_remote_datasource.dart'; +import 'package:flutter_pecha/features/practice/presentation/providers/bookmark_providers.dart'; import 'package:flutter_pecha/features/reader/constants/reader_constants.dart'; import 'package:flutter_pecha/features/reader/data/models/navigation_context.dart'; import 'package:flutter_pecha/features/reader/data/models/reader_slot_config.dart'; @@ -187,6 +189,12 @@ class _ReaderScreenState extends ConsumerState @override Widget build(BuildContext context) { + ref.watch( + prefetchBookmarkExistsProvider( + BookmarkTarget(type: BookmarkType.text, sourceId: widget.textId), + ), + ); + final state = ref.watch(readerNotifierProvider(_params)); final notifier = ref.read(readerNotifierProvider(_params).notifier); final readerTheme = _readerTheme(context); diff --git a/lib/features/reader/presentation/widgets/reader_actions/segement_action_bar.dart b/lib/features/reader/presentation/widgets/reader_actions/segement_action_bar.dart index c0a10358..6506702f 100644 --- a/lib/features/reader/presentation/widgets/reader_actions/segement_action_bar.dart +++ b/lib/features/reader/presentation/widgets/reader_actions/segement_action_bar.dart @@ -94,15 +94,14 @@ class _SegmentActionBarState extends ConsumerState { return const SizedBox.shrink(); } - final bookmarkAsync = ref.watch( - bookmarkExistsProvider( + final isBookmarked = ref.watch( + isBookmarkedProvider( BookmarkTarget( type: BookmarkType.verse, sourceId: widget.segment.segmentId, ), ), ); - final isBookmarked = bookmarkAsync.valueOrNull?.exists ?? false; return _ResourcesPanel( onDismiss: widget.onClose, diff --git a/lib/features/reader/presentation/widgets/reader_app_bar/reader_more_bottom_sheet.dart b/lib/features/reader/presentation/widgets/reader_app_bar/reader_more_bottom_sheet.dart index 943cfbb9..23774018 100644 --- a/lib/features/reader/presentation/widgets/reader_app_bar/reader_more_bottom_sheet.dart +++ b/lib/features/reader/presentation/widgets/reader_app_bar/reader_more_bottom_sheet.dart @@ -78,8 +78,7 @@ class _ReaderMoreBottomSheetState extends ConsumerState { Widget build(BuildContext context) { final theme = Theme.of(context); final l10n = context.l10n; - final bookmarkAsync = ref.watch(bookmarkExistsProvider(_bookmarkTarget)); - final isBookmarked = bookmarkAsync.valueOrNull?.exists ?? false; + final isBookmarked = ref.watch(isBookmarkedProvider(_bookmarkTarget)); final fontSize = ref.watch(fontSizeProvider); final stepIndex = _stepIndex(fontSize); diff --git a/lib/features/timer/presentation/screens/preset_timers_screen.dart b/lib/features/timer/presentation/screens/preset_timers_screen.dart index 5f81a9e1..5a86491e 100644 --- a/lib/features/timer/presentation/screens/preset_timers_screen.dart +++ b/lib/features/timer/presentation/screens/preset_timers_screen.dart @@ -3,6 +3,8 @@ import 'package:flutter_pecha/core/config/router/app_routes.dart'; import 'package:flutter_pecha/core/constants/app_assets.dart'; import 'package:flutter_pecha/core/extensions/context_ext.dart'; import 'package:flutter_pecha/core/widgets/error_state_widget.dart'; +import 'package:flutter_pecha/features/practice/data/datasource/bookmark_remote_datasource.dart'; +import 'package:flutter_pecha/features/practice/presentation/providers/bookmark_providers.dart'; import 'package:flutter_pecha/features/timer/domain/entities/preset_timer.dart'; import 'package:flutter_pecha/features/timer/presentation/providers/timers_providers.dart'; import 'package:flutter_pecha/features/timer/presentation/widgets/preset_timer_card.dart'; @@ -124,13 +126,13 @@ class PresetTimersScreen extends ConsumerWidget { } } -class _PresetTimersGrid extends StatelessWidget { +class _PresetTimersGrid extends ConsumerWidget { const _PresetTimersGrid({required this.timers, required this.minLabel}); final List timers; final String minLabel; - void _openMoreSheet(BuildContext context, PresetTimer timer) { + void _openMoreSheet(BuildContext context, WidgetRef ref, PresetTimer timer) { showTimerMoreBottomSheet( context, timer: timer, @@ -142,7 +144,15 @@ class _PresetTimersGrid extends StatelessWidget { } @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + for (final timer in timers) { + ref.watch( + prefetchBookmarkExistsProvider( + BookmarkTarget(type: BookmarkType.timer, sourceId: timer.id), + ), + ); + } + return Padding( padding: const EdgeInsets.all(PresetTimersScreen._horizontalPadding), child: GridView.builder( @@ -160,7 +170,7 @@ class _PresetTimersGrid extends StatelessWidget { timer: timer, minLabel: minLabel, onTap: () => context.push('/home/timers/active', extra: timer), - onMoreTap: () => _openMoreSheet(context, timer), + onMoreTap: () => _openMoreSheet(context, ref, timer), ); }, ), diff --git a/lib/features/timer/presentation/widgets/timer_more_bottom_sheet.dart b/lib/features/timer/presentation/widgets/timer_more_bottom_sheet.dart index 3e966c69..7c8a5404 100644 --- a/lib/features/timer/presentation/widgets/timer_more_bottom_sheet.dart +++ b/lib/features/timer/presentation/widgets/timer_more_bottom_sheet.dart @@ -54,8 +54,7 @@ class _TimerMoreBottomSheetState extends ConsumerState { Widget build(BuildContext context) { final theme = Theme.of(context); final l10n = context.l10n; - final bookmarkAsync = ref.watch(bookmarkExistsProvider(_bookmarkTarget)); - final isBookmarked = bookmarkAsync.valueOrNull?.exists ?? false; + final isBookmarked = ref.watch(isBookmarkedProvider(_bookmarkTarget)); return SafeArea( top: false, From 8a87695aa4efb6e44299bc59db794b44c21573f0 Mon Sep 17 00:00:00 2001 From: TenzDelek Date: Mon, 29 Jun 2026 17:09:33 +0530 Subject: [PATCH 11/25] fix --- .../presentation/widgets/upcoming_events_list.dart | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/features/calendar/presentation/widgets/upcoming_events_list.dart b/lib/features/calendar/presentation/widgets/upcoming_events_list.dart index 95f24681..4662ef5d 100644 --- a/lib/features/calendar/presentation/widgets/upcoming_events_list.dart +++ b/lib/features/calendar/presentation/widgets/upcoming_events_list.dart @@ -30,10 +30,20 @@ class _UpcomingEventsListState extends ConsumerState { final theme = Theme.of(context); final l10n = context.l10n; final focusedMonth = ref.watch(focusedCalendarMonthProvider); + final now = DateTime.now(); + final today = DateTime(now.year, now.month, now.day); final events = ref .watch(monthEventsProvider(focusedMonth)) .where(showsInUpcomingEvents) + .where((event) { + final eventDay = DateTime( + event.date.year, + event.date.month, + event.date.day, + ); + return !eventDay.isBefore(today); + }) .toList(); if (events.isEmpty) return const SizedBox.shrink(); From b7aa9c6839e6c31293fe02e391ebc6a1bc74a624 Mon Sep 17 00:00:00 2001 From: tentamdin Date: Mon, 29 Jun 2026 17:12:58 +0530 Subject: [PATCH 12/25] refactor(bookmarks): update toggle methods to return success status - Modified toggle methods in BookmarkController to return a boolean indicating success or failure. - Updated UI components in SeriesMoreBottomSheet, ReaderMoreBottomSheet, TimerMoreBottomSheet, and MalaSettingsSheet to handle the new return value, ensuring navigation only occurs on successful toggles. - Improved user experience by preventing navigation when the toggle action is blocked by guest login. --- .../widgets/series_more_bottom_sheet.dart | 10 +++++----- .../widgets/mala_settings_sheet.dart | 7 +++++-- .../controllers/bookmark_controller.dart | 20 ++++++++++++------- .../reader_more_bottom_sheet.dart | 9 +++++---- .../widgets/timer_more_bottom_sheet.dart | 9 +++++---- 5 files changed, 33 insertions(+), 22 deletions(-) diff --git a/lib/features/home/presentation/widgets/series_more_bottom_sheet.dart b/lib/features/home/presentation/widgets/series_more_bottom_sheet.dart index b0597a7e..3be113ce 100644 --- a/lib/features/home/presentation/widgets/series_more_bottom_sheet.dart +++ b/lib/features/home/presentation/widgets/series_more_bottom_sheet.dart @@ -43,11 +43,11 @@ class _SeriesMoreBottomSheetState extends ConsumerState { setState(() => _isBookmarking = true); try { final nav = Navigator.of(context); - await BookmarkController(ref: ref, context: context).toggleSeries( - widget.seriesId, - name: widget.seriesName, - ); - if (mounted) nav.pop(); + final didToggle = await BookmarkController( + ref: ref, + context: context, + ).toggleSeries(widget.seriesId, name: widget.seriesName); + if (mounted && didToggle) nav.pop(); } finally { if (mounted) setState(() => _isBookmarking = false); } diff --git a/lib/features/mala/presentation/widgets/mala_settings_sheet.dart b/lib/features/mala/presentation/widgets/mala_settings_sheet.dart index 39a1fbf9..c9d1e0f9 100644 --- a/lib/features/mala/presentation/widgets/mala_settings_sheet.dart +++ b/lib/features/mala/presentation/widgets/mala_settings_sheet.dart @@ -125,12 +125,15 @@ class MalaSettingsSheet extends ConsumerWidget { final language = ref.read(contentLanguageProvider); final navigator = Navigator.of(context); - await BookmarkController(ref: ref, context: context).toggleMala( + final didToggle = await BookmarkController( + ref: ref, + context: context, + ).toggleMala( mantra.presetId, name: mantra.displayTitle(language), ); - if (context.mounted) navigator.pop(); + if (context.mounted && didToggle) navigator.pop(); } Future _onResetCount(BuildContext context, WidgetRef ref) async { diff --git a/lib/features/practice/presentation/controllers/bookmark_controller.dart b/lib/features/practice/presentation/controllers/bookmark_controller.dart index b7198035..5bdb7995 100644 --- a/lib/features/practice/presentation/controllers/bookmark_controller.dart +++ b/lib/features/practice/presentation/controllers/bookmark_controller.dart @@ -19,29 +19,33 @@ class BookmarkController { BookmarkController({required this.ref, required this.context}); - Future toggleText(String textId) => + Future toggleText(String textId) => toggle(type: BookmarkType.text, sourceId: textId); - Future toggleVerse(String segmentId) => + Future toggleVerse(String segmentId) => toggle(type: BookmarkType.verse, sourceId: segmentId); - Future toggleTimer(String timerId) => + Future toggleTimer(String timerId) => toggle(type: BookmarkType.timer, sourceId: timerId); - Future toggleMala(String accumulatorId, {String? name}) => toggle( + Future toggleMala(String accumulatorId, {String? name}) => toggle( type: BookmarkType.accumulator, sourceId: accumulatorId, name: name, ); - Future toggleSeries(String seriesId, {String? name}) => toggle( + Future toggleSeries(String seriesId, {String? name}) => toggle( type: BookmarkType.series, sourceId: seriesId, name: name, ); /// Optimistically toggles bookmark state, then POST or DELETE (one call). - Future toggle({ + /// + /// Returns `false` when the guest login gate blocked the action (the login + /// drawer is already visible). Callers that dismiss a modal after bookmarking + /// must skip [Navigator.pop] in that case so they do not pop the drawer. + Future toggle({ required BookmarkType type, required String sourceId, String? name, @@ -49,7 +53,7 @@ class BookmarkController { final authState = ref.read(authProvider); if (authState.isGuest) { LoginDrawer.show(context, ref); - return; + return false; } final target = BookmarkTarget(type: type, sourceId: sourceId); @@ -100,6 +104,8 @@ class BookmarkController { cache.set(target, previous); _showErrorSnackBar(wasBookmarked); } + + return true; } /// Uses cached id when available; falls back to exists check only if needed. diff --git a/lib/features/reader/presentation/widgets/reader_app_bar/reader_more_bottom_sheet.dart b/lib/features/reader/presentation/widgets/reader_app_bar/reader_more_bottom_sheet.dart index 23774018..4ea62773 100644 --- a/lib/features/reader/presentation/widgets/reader_app_bar/reader_more_bottom_sheet.dart +++ b/lib/features/reader/presentation/widgets/reader_app_bar/reader_more_bottom_sheet.dart @@ -45,10 +45,11 @@ class _ReaderMoreBottomSheetState extends ConsumerState { setState(() => _isBookmarking = true); try { final nav = Navigator.of(context); - await BookmarkController(ref: ref, context: context).toggleText( - widget.textId, - ); - if (mounted) nav.pop(); + final didToggle = await BookmarkController( + ref: ref, + context: context, + ).toggleText(widget.textId); + if (mounted && didToggle) nav.pop(); } finally { if (mounted) setState(() => _isBookmarking = false); } diff --git a/lib/features/timer/presentation/widgets/timer_more_bottom_sheet.dart b/lib/features/timer/presentation/widgets/timer_more_bottom_sheet.dart index 7c8a5404..47b55954 100644 --- a/lib/features/timer/presentation/widgets/timer_more_bottom_sheet.dart +++ b/lib/features/timer/presentation/widgets/timer_more_bottom_sheet.dart @@ -41,10 +41,11 @@ class _TimerMoreBottomSheetState extends ConsumerState { setState(() => _isBookmarking = true); try { final nav = Navigator.of(context); - await BookmarkController(ref: ref, context: context).toggleTimer( - widget.timer.id, - ); - if (mounted) nav.pop(); + final didToggle = await BookmarkController( + ref: ref, + context: context, + ).toggleTimer(widget.timer.id); + if (mounted && didToggle) nav.pop(); } finally { if (mounted) setState(() => _isBookmarking = false); } From 92c0697d201c6b61b3fbdfad2d448c5ec1a33c63 Mon Sep 17 00:00:00 2001 From: tentamdin Date: Mon, 29 Jun 2026 17:15:49 +0530 Subject: [PATCH 13/25] feat(practice): integrate guest login handling in bookmarks navigation - Added authentication check to prevent navigation for guest users when accessing bookmarks. - Introduced LoginDrawer to prompt guest users for login before proceeding to bookmarks. - Cleaned up commented-out code related to snack bar notifications for a more streamlined implementation. --- .../screens/practice_explore_screen.dart | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/features/practice/presentation/screens/practice_explore_screen.dart b/lib/features/practice/presentation/screens/practice_explore_screen.dart index 14dd7e08..6be1697b 100644 --- a/lib/features/practice/presentation/screens/practice_explore_screen.dart +++ b/lib/features/practice/presentation/screens/practice_explore_screen.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_pecha/core/extensions/context_ext.dart'; +import 'package:flutter_pecha/features/auth/presentation/providers/state_providers.dart'; +import 'package:flutter_pecha/features/auth/presentation/widgets/login_drawer.dart'; import 'package:flutter_pecha/features/practice/presentation/providers/practice_explore_providers.dart'; import 'package:flutter_pecha/features/practice/presentation/widgets/practice_accumulations_section.dart'; import 'package:flutter_pecha/features/practice/presentation/widgets/practice_chants_section.dart'; @@ -63,13 +65,11 @@ class _PracticeExploreScreenState extends ConsumerState { icon: Icons.bookmark_border, variant: PracticeActionButtonVariant.outlined, onTap: () { + if (ref.read(authProvider).isGuest) { + LoginDrawer.show(context, ref); + return; + } context.pushNamed('bookmarks'); - // ScaffoldMessenger.of(context).showSnackBar( - // SnackBar( - // content: Text(context.l10n.mala_action_coming_soon), - // duration: const Duration(seconds: 2), - // ), - // ); }, ), ), From 7ff39557c80bfc57eb3759d5ea92e45613c2a68c Mon Sep 17 00:00:00 2001 From: TenzDelek Date: Mon, 29 Jun 2026 17:21:22 +0530 Subject: [PATCH 14/25] fix --- .../presentation/widgets/calendar_month_nav.dart | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/lib/features/calendar/presentation/widgets/calendar_month_nav.dart b/lib/features/calendar/presentation/widgets/calendar_month_nav.dart index 22eb9309..78d940b5 100644 --- a/lib/features/calendar/presentation/widgets/calendar_month_nav.dart +++ b/lib/features/calendar/presentation/widgets/calendar_month_nav.dart @@ -15,17 +15,19 @@ class CalendarMonthNav extends ConsumerWidget { final theme = Theme.of(context); final l10n = context.l10n; final focusedMonth = ref.watch(focusedCalendarMonthProvider); - final service = ref.watch(tibetanCalendarServiceProvider); final monthTitle = DateFormat.yMMMM( dateFormatLocale(context), ).format(focusedMonth); - // Lunar month label for the focused month, sampled at its mid-point so a - // lunar-month boundary near the start/end doesn't mislabel the page. - final midLunar = service.fromWestern( - DateTime(focusedMonth.year, focusedMonth.month, 15), - ); - final lunarSubtitle = lunarMonthLabel(context, l10n, midLunar.month); + final today = dateOnly(DateTime.now()); + final isCurrentMonth = + today.year == focusedMonth.year && today.month == focusedMonth.month; + final sampleDate = + isCurrentMonth + ? today + : DateTime(focusedMonth.year, focusedMonth.month + 1, 0); + final lunarMonth = ref.watch(resolvedDayProvider(sampleDate)).lunarMonth; + final lunarSubtitle = lunarMonthLabel(context, l10n, lunarMonth); void shift(int months) { ref.read(focusedCalendarMonthProvider.notifier).state = DateTime( From 94f6737b72daad0e5fd97c4479ef26526968e02e Mon Sep 17 00:00:00 2001 From: TenzDelek Date: Mon, 29 Jun 2026 17:27:30 +0530 Subject: [PATCH 15/25] fallback --- assets/images/buddha.jpeg | Bin 0 -> 8330083 bytes lib/core/constants/app_assets.dart | 1 + .../widgets/verse_of_day_content.dart | 1 + 3 files changed, 2 insertions(+) create mode 100644 assets/images/buddha.jpeg diff --git a/assets/images/buddha.jpeg b/assets/images/buddha.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..f442292e13f62f8dbe9d7786bb84a46e240ab33c GIT binary patch literal 8330083 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�@@xrVUZADLkzoZp84-rg8qv^5E6m6@ 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