import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:intl/intl.dart'; import 'package:lucide_icons/lucide_icons.dart'; import '../../../app/theme/app_colors.dart'; import '../../../data/local/hive_boxes.dart'; import '../../../data/local/models/app_settings.dart'; import '../../../data/local/models/dzikir_counter.dart'; import '../../../data/services/muslim_api_service.dart'; class DzikirScreen extends ConsumerStatefulWidget { final bool isSimpleModeTab; const DzikirScreen({super.key, this.isSimpleModeTab = false}); @override ConsumerState createState() => _DzikirScreenState(); } class _DzikirScreenState extends ConsumerState with SingleTickerProviderStateMixin { late TabController _tabController; final Map _pageControllers = { 'pagi': PageController(), 'petang': PageController(), 'solat': PageController(), }; final Map _focusPageIndex = { 'pagi': 0, 'petang': 0, 'solat': 0, }; List> _pagiItems = []; List> _petangItems = []; List> _sesudahSholatItems = []; bool _loading = true; String? _error; late Box _counterBox; late String _todayKey; @override void initState() { super.initState(); _tabController = TabController(length: 3, vsync: this); _tabController.addListener(() { if (!mounted) return; setState(() {}); }); _counterBox = Hive.box(HiveBoxes.dzikirCounters); _todayKey = DateFormat('yyyy-MM-dd').format(DateTime.now()); _loadData(); } @override void dispose() { _tabController.dispose(); for (final controller in _pageControllers.values) { controller.dispose(); } super.dispose(); } Future _loadData() async { setState(() { _loading = true; _error = null; }); try { final pagi = await MuslimApiService.instance.getDzikirByType( 'pagi', strict: true, ); final petang = await MuslimApiService.instance.getDzikirByType( 'petang', strict: true, ); final solat = await MuslimApiService.instance.getDzikirByType( 'solat', strict: true, ); if (!mounted) return; setState(() { _pagiItems = pagi; _petangItems = petang; _sesudahSholatItems = solat; _loading = false; }); _ensureValidFocusPages(); } catch (_) { if (!mounted) return; setState(() { _loading = false; _error = 'Gagal memuat dzikir dari server'; }); } } void _ensureValidFocusPages() { _clampFocusPageForPrefix('pagi', _pagiItems.length); _clampFocusPageForPrefix('petang', _petangItems.length); _clampFocusPageForPrefix('solat', _sesudahSholatItems.length); } void _clampFocusPageForPrefix(String prefix, int itemLength) { final maxIndex = itemLength > 0 ? itemLength - 1 : 0; final current = _focusPageIndex[prefix] ?? 0; final next = current > maxIndex ? maxIndex : current; _focusPageIndex[prefix] = next; final controller = _pageControllers[prefix]; if (controller == null || !controller.hasClients) return; if (controller.page?.round() == next) return; WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted || !controller.hasClients) return; controller.jumpToPage(next); }); } DzikirCounter _getCounter(String dzikirId, int target) { final key = '${dzikirId}_$_todayKey'; return _counterBox.get(key) ?? DzikirCounter( dzikirId: dzikirId, date: _todayKey, count: 0, target: target, ); } bool _increment( String dzikirId, int target, { required bool hapticEnabled, }) { final key = '${dzikirId}_$_todayKey'; var counter = _counterBox.get(key); final wasComplete = counter != null && counter.count >= counter.target; if (counter == null) { counter = DzikirCounter( dzikirId: dzikirId, date: _todayKey, count: 1, target: target, ); _counterBox.put(key, counter); } else if (counter.count < counter.target) { counter.count++; counter.save(); } final isCompleteNow = counter.count >= counter.target; if (hapticEnabled) { HapticFeedback.lightImpact(); } setState(() {}); return !wasComplete && isCompleteNow; } @override Widget build(BuildContext context) { final isDark = Theme.of(context).brightness == Brightness.dark; return ValueListenableBuilder>( valueListenable: Hive.box(HiveBoxes.settings).listenable(keys: ['default']), builder: (_, settingsBox, __) { final settings = settingsBox.get('default') ?? AppSettings(); final isFocusMode = settings.dzikirDisplayMode == 'focus'; return Scaffold( appBar: AppBar( automaticallyImplyLeading: !widget.isSimpleModeTab, title: const Text('Dzikir Harian'), actions: [ IconButton( onPressed: _loadData, icon: const Icon(LucideIcons.refreshCw), tooltip: 'Muat ulang', ), ], bottom: TabBar( controller: _tabController, labelColor: AppColors.primary, unselectedLabelColor: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight, indicatorColor: AppColors.primary, indicatorWeight: 3, labelStyle: const TextStyle(fontWeight: FontWeight.w700, fontSize: 13), tabs: const [ Tab(text: 'Pagi'), Tab(text: 'Petang'), Tab(text: 'Sesudah Sholat'), ], ), ), body: _loading ? const Center(child: CircularProgressIndicator()) : _error != null ? _buildErrorState(isDark) : TabBarView( controller: _tabController, children: [ isFocusMode ? _buildFocusModeTab( context, isDark, settings, items: _pagiItems, prefix: 'pagi', title: 'Dzikir Pagi', subtitle: 'Dibaca setelah shalat Subuh hingga terbit matahari.', ) : _buildDzikirList( context, isDark, settings, _pagiItems, 'pagi', 'Dzikir Pagi', 'Dibaca setelah shalat Subuh hingga terbit matahari.', ), isFocusMode ? _buildFocusModeTab( context, isDark, settings, items: _petangItems, prefix: 'petang', title: 'Dzikir Petang', subtitle: 'Dibaca setelah Ashar hingga terbenam matahari.', ) : _buildDzikirList( context, isDark, settings, _petangItems, 'petang', 'Dzikir Petang', 'Dibaca setelah Ashar hingga terbenam matahari.', ), isFocusMode ? _buildFocusModeTab( context, isDark, settings, items: _sesudahSholatItems, prefix: 'solat', title: 'Dzikir Sesudah Sholat', subtitle: 'Dibaca setelah shalat fardhu sesuai kebutuhan.', ) : _buildDzikirList( context, isDark, settings, _sesudahSholatItems, 'solat', 'Dzikir Sesudah Sholat', 'Dibaca setelah shalat fardhu sesuai kebutuhan.', ), ], ), ); }, ); } Widget _buildErrorState(bool isDark) { return Center( child: Padding( padding: const EdgeInsets.all(24), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon( LucideIcons.wifiOff, size: 42, color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight, ), const SizedBox(height: 12), Text( _error!, textAlign: TextAlign.center, style: TextStyle( color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight, ), ), ], ), ), ); } Widget _buildDzikirList( BuildContext context, bool isDark, AppSettings settings, List> items, String prefix, String title, String subtitle, ) { if (items.isEmpty) { return _buildEmptyState( isDark, title: 'Belum ada data dzikir', subtitle: 'Data untuk tab ini belum tersedia.', ); } return ListView.builder( padding: const EdgeInsets.all(16), itemCount: items.length + 1, itemBuilder: (context, index) { if (index == 0) { return Padding( padding: const EdgeInsets.only(bottom: 20), child: Column( children: [ Text( title, style: const TextStyle( fontSize: 22, fontWeight: FontWeight.w800, ), ), const SizedBox(height: 4), Text( subtitle, textAlign: TextAlign.center, style: TextStyle( color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight, fontSize: 13, ), ), ], ), ); } final item = items[index - 1]; final dzikirId = _resolveDzikirId(item, prefix, index - 1); final target = (item['ulang'] as num?)?.toInt() ?? 1; final counter = _getCounter(dzikirId, target); final isComplete = counter.count >= counter.target; return Padding( padding: const EdgeInsets.only(bottom: 16), child: Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: isDark ? AppColors.surfaceDark : AppColors.surfaceLight, borderRadius: BorderRadius.circular(16), border: Border.all( color: isComplete ? AppColors.primary.withValues(alpha: 0.3) : (isDark ? AppColors.primary.withValues(alpha: 0.08) : AppColors.cream), ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Container( padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 4, ), decoration: BoxDecoration( color: AppColors.primary.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(50), ), child: Text( '$target KALI', style: TextStyle( fontSize: 10, fontWeight: FontWeight.w700, color: AppColors.primary, ), ), ), Text( '${(index).toString().padLeft(2, '0')}', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight, ), ), ], ), const SizedBox(height: 16), SizedBox( width: double.infinity, child: Text( item['arab']?.toString() ?? '', textAlign: TextAlign.right, style: const TextStyle( fontFamily: 'Amiri', fontSize: 24, fontWeight: FontWeight.w400, height: 2.0, ), ), ), const SizedBox(height: 10), Text( '"${item['indo']?.toString() ?? ''}"', style: TextStyle( fontSize: 13, color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight, height: 1.5, ), ), const SizedBox(height: 16), GestureDetector( onTap: () => _increment( dzikirId, target, hapticEnabled: settings.dzikirHapticOnCount, ), child: Container( width: double.infinity, padding: const EdgeInsets.symmetric(vertical: 14), decoration: BoxDecoration( color: isComplete ? AppColors.primary.withValues(alpha: 0.15) : AppColors.primary, borderRadius: BorderRadius.circular(50), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( isComplete ? LucideIcons.check : LucideIcons.fingerprint, size: 18, color: isComplete ? AppColors.primary : AppColors.onPrimary, ), const SizedBox(width: 8), Text( isComplete ? 'Selesai' : '${counter.count} / $target', style: TextStyle( fontSize: 15, fontWeight: FontWeight.w700, color: isComplete ? AppColors.primary : AppColors.onPrimary, ), ), ], ), ), ), ], ), ), ); }, ); } Widget _buildFocusModeTab( BuildContext context, bool isDark, AppSettings settings, { required List> items, required String prefix, required String title, required String subtitle, }) { if (items.isEmpty) { return _buildEmptyState( isDark, title: 'Belum ada data dzikir', subtitle: 'Data untuk tab ini belum tersedia.', ); } final controller = _pageControllers[prefix]!; final rawCurrent = _focusPageIndex[prefix] ?? 0; final currentIndex = rawCurrent.clamp(0, items.length - 1); final currentItem = items[currentIndex]; final currentId = _resolveDzikirId(currentItem, prefix, currentIndex); final currentTarget = (currentItem['ulang'] as num?)?.toInt() ?? 1; final currentCounter = _getCounter(currentId, currentTarget); final isComplete = currentCounter.count >= currentCounter.target; return Padding( padding: const EdgeInsets.fromLTRB(16, 16, 16, 16), child: Column( children: [ Text( title, style: const TextStyle(fontSize: 22, fontWeight: FontWeight.w800), ), const SizedBox(height: 4), Text( subtitle, textAlign: TextAlign.center, style: TextStyle( color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight, fontSize: 13, ), ), const SizedBox(height: 12), Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( color: AppColors.primary.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(50), ), child: Text( 'Item ${currentIndex + 1} dari ${items.length}', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w700, color: AppColors.primary, ), ), ), const SizedBox(height: 12), Expanded( child: Stack( children: [ PageView.builder( controller: controller, itemCount: items.length, onPageChanged: (index) { setState(() { _focusPageIndex[prefix] = index; }); }, itemBuilder: (context, index) { final item = items[index]; final dzikirId = _resolveDzikirId(item, prefix, index); final target = (item['ulang'] as num?)?.toInt() ?? 1; final counter = _getCounter(dzikirId, target); final complete = counter.count >= counter.target; return Padding( padding: const EdgeInsets.only(bottom: 92), child: _buildFocusCard( isDark, item: item, index: index, target: target, counter: counter, isComplete: complete, ), ); }, ), if (settings.dzikirCounterButtonPosition == 'fabCircle') Positioned( right: 8, bottom: 12, child: _buildFocusCounterFab( isDark, isComplete: isComplete, label: isComplete ? 'Selesai' : '${currentCounter.count}/$currentTarget', onTap: () => _onFocusCounterTap( context, settings, prefix, items, ), ), ) else Positioned( left: 0, right: 0, bottom: 12, child: _buildFocusCounterPill( isComplete: isComplete, label: isComplete ? 'Selesai' : '${currentCounter.count} / $currentTarget', onTap: () => _onFocusCounterTap( context, settings, prefix, items, ), ), ), ], ), ), ], ), ); } Widget _buildFocusCard( bool isDark, { required Map item, required int index, required int target, required DzikirCounter counter, required bool isComplete, }) { return Container( width: double.infinity, padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: isDark ? AppColors.surfaceDark : AppColors.surfaceLight, borderRadius: BorderRadius.circular(20), border: Border.all( color: isComplete ? AppColors.primary.withValues(alpha: 0.3) : (isDark ? AppColors.primary.withValues(alpha: 0.08) : AppColors.cream), ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: AppColors.primary.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(50), ), child: Text( '$target KALI', style: TextStyle( fontSize: 10, fontWeight: FontWeight.w700, color: AppColors.primary, ), ), ), Text( '${(index + 1).toString().padLeft(2, '0')}', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight, ), ), ], ), const SizedBox(height: 20), Expanded( child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( width: double.infinity, child: Text( item['arab']?.toString() ?? '', textAlign: TextAlign.right, style: const TextStyle( fontFamily: 'Amiri', fontSize: 28, fontWeight: FontWeight.w400, height: 2.0, ), ), ), const SizedBox(height: 14), Text( '"${item['indo']?.toString() ?? ''}"', style: TextStyle( fontSize: 14, color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight, height: 1.6, ), ), const SizedBox(height: 12), if (isComplete) Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 6, ), decoration: BoxDecoration( color: AppColors.primary.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(50), ), child: Text( 'Selesai (${counter.count}/$target)', style: TextStyle( color: AppColors.primary, fontWeight: FontWeight.w700, fontSize: 12, ), ), ), ], ), ), ), ], ), ); } Widget _buildFocusCounterPill({ required bool isComplete, required String label, required VoidCallback onTap, }) { return GestureDetector( onTap: onTap, child: Container( margin: const EdgeInsets.symmetric(horizontal: 8), padding: const EdgeInsets.symmetric(vertical: 14), decoration: BoxDecoration( color: isComplete ? AppColors.primary.withValues(alpha: 0.15) : AppColors.primary, borderRadius: BorderRadius.circular(50), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( isComplete ? LucideIcons.check : LucideIcons.fingerprint, size: 18, color: isComplete ? AppColors.primary : AppColors.onPrimary, ), const SizedBox(width: 8), Text( label, style: TextStyle( fontSize: 15, fontWeight: FontWeight.w700, color: isComplete ? AppColors.primary : AppColors.onPrimary, ), ), ], ), ), ); } Widget _buildFocusCounterFab( bool isDark, { required bool isComplete, required String label, required VoidCallback onTap, }) { return GestureDetector( onTap: onTap, child: Container( width: 72, height: 72, decoration: BoxDecoration( shape: BoxShape.circle, color: isComplete ? AppColors.primary.withValues(alpha: 0.15) : AppColors.primary, boxShadow: [ BoxShadow( color: (isDark ? Colors.black : Colors.black26) .withValues(alpha: 0.14), blurRadius: 18, offset: const Offset(0, 6), ), ], ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( isComplete ? LucideIcons.check : LucideIcons.fingerprint, size: 18, color: isComplete ? AppColors.primary : AppColors.onPrimary, ), const SizedBox(height: 2), Text( label, style: TextStyle( fontSize: 10, fontWeight: FontWeight.w700, color: isComplete ? AppColors.primary : AppColors.onPrimary, ), textAlign: TextAlign.center, ), ], ), ), ); } void _onFocusCounterTap( BuildContext context, AppSettings settings, String prefix, List> items, ) { if (items.isEmpty) return; final currentIndex = (_focusPageIndex[prefix] ?? 0).clamp(0, items.length - 1); final item = items[currentIndex]; final dzikirId = _resolveDzikirId(item, prefix, currentIndex); final target = (item['ulang'] as num?)?.toInt() ?? 1; final becameComplete = _increment( dzikirId, target, hapticEnabled: settings.dzikirHapticOnCount, ); if (!becameComplete) return; final isLast = currentIndex == items.length - 1; if (settings.dzikirAutoAdvance && !isLast) { final controller = _pageControllers[prefix]; if (controller != null && controller.hasClients) { controller.nextPage( duration: const Duration(milliseconds: 240), curve: Curves.easeOut, ); } return; } if (isLast) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Semua dzikir pada tab ini selesai'), duration: Duration(seconds: 2), ), ); } } String _resolveDzikirId(Map item, String prefix, int index) { final rawId = item['id']?.toString(); if (rawId != null && rawId.isNotEmpty) { return rawId; } return '${prefix}_${index + 1}'; } Widget _buildEmptyState( bool isDark, { required String title, required String subtitle, }) { return Center( child: Padding( padding: const EdgeInsets.all(24), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon( LucideIcons.inbox, size: 42, color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight, ), const SizedBox(height: 12), Text( title, style: const TextStyle(fontWeight: FontWeight.w700, fontSize: 15), textAlign: TextAlign.center, ), const SizedBox(height: 6), Text( subtitle, style: TextStyle( color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight, ), textAlign: TextAlign.center, ), ], ), ), ); } }