import 'dart:async'; import 'package:audio_service/audio_service.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:lucide_icons/lucide_icons.dart'; import 'package:just_audio/just_audio.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:intl/intl.dart'; import '../../../app/theme/app_colors.dart'; import '../../../core/services/app_audio_player.dart'; import '../../../core/widgets/arabic_text.dart'; import '../../../data/local/hive_boxes.dart'; import '../../../data/local/models/quran_bookmark.dart'; import '../../../data/local/models/app_settings.dart'; import '../../../data/local/models/daily_worship_log.dart'; import '../../../data/local/models/shalat_log.dart'; import '../../../data/local/models/tilawah_log.dart'; import '../../../data/services/muslim_api_service.dart'; import '../../../core/providers/tilawah_tracking_provider.dart'; class QuranReadingScreen extends ConsumerStatefulWidget { final String surahId; final int? initialVerse; final bool isSimpleModeTab; const QuranReadingScreen({ super.key, required this.surahId, this.initialVerse, this.isSimpleModeTab = false, }); @override ConsumerState createState() => _QuranReadingScreenState(); } class _QuranReadingScreenState extends ConsumerState { Map? _surah; List> _verses = []; bool _loading = true; bool _autoSyncEnabled = false; String _targetUnit = 'Juz'; final ScrollController _scrollController = ScrollController(); final Map _verseKeys = {}; final AudioPlayer _audioPlayer = AppAudioPlayer.instance; int? _playingVerseIndex; bool _isAudioLoading = false; // Display Settings bool _showLatin = true; bool _showTerjemahan = true; double _arabicFontSize = 24.0; // Hafalan State bool _isHafalanMode = false; int _hafalanStartAyat = 1; int _hafalanEndAyat = 1; int _hafalanLoopCount = 1; // 0 = Tak Terbatas int _currentLoop = 0; bool _isHafalanPlaying = false; StreamSubscription? _playerStateSubscription; void _navigateToMurattal() { if (widget.isSimpleModeTab) { context.push('/quran/${widget.surahId}/murattal'); } else { context.push('/tools/quran/${widget.surahId}/murattal'); } } @override void initState() { super.initState(); _loadSurah(); final settingsBox = Hive.box(HiveBoxes.settings); final settings = settingsBox.get('default') ?? AppSettings(); if (!widget.isSimpleModeTab && !settings.tilawahAutoSync) { settings.tilawahAutoSync = true; if (settings.isInBox) { settings.save(); } else { settingsBox.put('default', settings); } } _autoSyncEnabled = !widget.isSimpleModeTab || settings.tilawahAutoSync; _targetUnit = settings.tilawahTargetUnit; _showLatin = settings.showLatin; _showTerjemahan = settings.showTerjemahan; _arabicFontSize = settings.arabicFontSize; _playerStateSubscription = _audioPlayer.playerStateStream.listen((state) { if (state.processingState == ProcessingState.completed) { if (mounted) { if (_isHafalanMode && _isHafalanPlaying && _playingVerseIndex != null) { _handleHafalanNext(); } else { setState(() { _playingVerseIndex = null; _isAudioLoading = false; }); } } } }); } void _handleHafalanNext() { // Current verse index is 0-based. EndAyat is 1-based. final endIdx = _hafalanEndAyat - 1; final startIdx = _hafalanStartAyat - 1; if (_playingVerseIndex! < endIdx) { // Move to next ayat in sequence final nextIdx = _playingVerseIndex! + 1; final audioUrl = _resolveVerseAudioUrl(_verses[nextIdx]); if (audioUrl != null) { _playAudio(nextIdx, audioUrl); _scrollToVerse(nextIdx, attempts: 0); } else { _stopHafalan(); } } else { // Reached the end of the sequence. Loop or Stop. _currentLoop++; if (_hafalanLoopCount == 0 || _currentLoop < _hafalanLoopCount) { // Loop again! final audioUrl = _resolveVerseAudioUrl(_verses[startIdx]); if (audioUrl != null) { _playAudio(startIdx, audioUrl); _scrollToVerse(startIdx, attempts: 0); } else { _stopHafalan(); } } else { // Finished all loops _stopHafalan(); } } } void _stopHafalan({bool isDisposing = false}) { _audioPlayer.stop(); if (isDisposing) return; if (mounted) { setState(() { _isHafalanPlaying = false; _playingVerseIndex = null; _isAudioLoading = false; _currentLoop = 0; }); } } @override void dispose() { _playerStateSubscription?.cancel(); if (_playingVerseIndex != null || _isHafalanPlaying || _isAudioLoading) { _stopHafalan(isDisposing: true); } _scrollController.dispose(); super.dispose(); } Future _loadSurah() async { final surahNum = int.tryParse(widget.surahId) ?? 1; final data = await MuslimApiService.instance.getSurah(surahNum); if (!mounted) return; if (data != null) { setState(() { _surah = data; final ayatList = List>.from(data['ayat'] ?? []); _verses = ayatList; _verseKeys.clear(); for (int i = 0; i < ayatList.length; i++) { _verseKeys[i] = GlobalKey(); } _loading = false; }); _scrollToInitialVerse(); } else { setState(() => _loading = false); } } void _scrollToInitialVerse() { if (widget.initialVerse != null && widget.initialVerse! > 0) { final targetIndex = widget.initialVerse! - 1; if (targetIndex >= 0 && targetIndex < _verses.length) { WidgetsBinding.instance.addPostFrameCallback((_) { _scrollToVerse(targetIndex, attempts: 0); }); } } } void _scrollToVerse(int targetIndex, {required int attempts}) { if (!mounted) return; final key = _verseKeys[targetIndex]; if (key != null && key.currentContext != null) { Scrollable.ensureVisible( key.currentContext!, duration: const Duration(milliseconds: 400), curve: Curves.easeInOut, alignment: 0.08, ); return; } if (attempts < 6) { WidgetsBinding.instance.addPostFrameCallback((_) { _scrollToVerse(targetIndex, attempts: attempts + 1); }); } } String? _resolveVerseAudioUrl(Map verse, {String preferredQariId = '05'}) { final rawAudio = verse['audio']; if (rawAudio is Map) { final preferred = rawAudio[preferredQariId]; final preferredUrl = _coerceAudioUrl(preferred); if (preferredUrl != null) { return _normalizePlayableUrl(preferredUrl); } for (final value in rawAudio.values) { final resolved = _coerceAudioUrl(value); if (resolved != null) { return _normalizePlayableUrl(resolved); } } return null; } final direct = _coerceAudioUrl(rawAudio); if (direct != null) { return _normalizePlayableUrl(direct); } return null; } String? _coerceAudioUrl(dynamic rawValue) { if (rawValue == null) return null; if (rawValue is String) { final trimmed = rawValue.trim(); if (trimmed.isEmpty || trimmed.toLowerCase() == 'null') return null; // Backward-compat for previously cached map-to-string payloads. if (trimmed.startsWith('{') && trimmed.contains(':')) { final match = RegExp(r'https?://[^,\s}\]]+').firstMatch(trimmed); if (match != null && match.groupCount >= 0) { final mappedUrl = match.group(0); if (mappedUrl != null && mappedUrl.isNotEmpty) { return mappedUrl; } } } return trimmed; } if (rawValue is Map) { final url = _coerceAudioUrl(rawValue['url']); if (url != null) return url; final src = _coerceAudioUrl(rawValue['src']); if (src != null) return src; final audio = _coerceAudioUrl(rawValue['audio']); if (audio != null) return audio; } return null; } String _normalizePlayableUrl(String rawUrl) { final trimmed = rawUrl.trim(); if (trimmed.startsWith('//')) { return 'https:$trimmed'; } if (trimmed.startsWith('http://')) { return 'https://${trimmed.substring(7)}'; } if (trimmed.startsWith('/')) { return 'https://muslim.backoffice.biz.id$trimmed'; } return trimmed; } Future _playAudio(int verseIndex, String audioUrl) async { if (_playingVerseIndex == verseIndex) { await _audioPlayer.stop(); if (mounted) { setState(() { _playingVerseIndex = null; _isAudioLoading = false; }); } return; } if (mounted) { setState(() { _playingVerseIndex = verseIndex; _isAudioLoading = true; }); } try { final verse = _verses[verseIndex]; final verseNumber = (verse['nomorAyat'] ?? verseIndex + 1) as int; final surahName = _surah?['namaLatin'] ?? 'Surah ${widget.surahId}'; await _audioPlayer.setAudioSource( AudioSource.uri( Uri.parse(audioUrl), tag: MediaItem( id: 'ayah_${widget.surahId}_$verseNumber', album: "Al-Qur'an Ayat", title: 'Surah $surahName • Ayat $verseNumber', artist: 'Tilawah', ), ), ); if (mounted && _playingVerseIndex == verseIndex) { setState(() { _isAudioLoading = false; }); } unawaited( _audioPlayer.play().catchError((error, st) { debugPrint('Ayat audio playback error: $error'); debugPrint('$st'); if (!mounted) return; setState(() { _playingVerseIndex = null; _isAudioLoading = false; }); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: const Text('Gagal memutar audio ayat'), backgroundColor: Colors.red.shade400, ), ); }), ); } catch (e, st) { debugPrint('Ayat audio load failed: $audioUrl'); debugPrint('Ayat audio error: $e'); debugPrint('$st'); if (mounted) { setState(() { _playingVerseIndex = null; _isAudioLoading = false; }); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: const Text('Gagal memuat audio ayat ini'), backgroundColor: Colors.red.shade400, ), ); } } } Future _showBookmarkOptions(int verseIndex) async { if (_surah == null || verseIndex >= _verses.length) return; final verse = _verses[verseIndex]; final surahId = _surah!['nomor'] ?? 1; final verseId = verseIndex + 1; showModalBottomSheet( context: context, useSafeArea: true, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), builder: (ctx) => SafeArea( child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( margin: const EdgeInsets.only(top: 8, bottom: 8), width: 40, height: 4, decoration: BoxDecoration( color: Colors.grey.withValues(alpha: 0.3), borderRadius: BorderRadius.circular(2), ), ), ListTile( leading: const Icon(LucideIcons.pin, color: AppColors.primary), title: const Text('Tandai Terakhir Dibaca', style: TextStyle(fontWeight: FontWeight.w600)), subtitle: const Text( 'Jadikan ayat ini sebagai titik lanjut membaca anda'), onTap: () { Navigator.pop(ctx); _saveBookmark(surahId, verseId, verse, isLastRead: true); }, ), const Divider(height: 1), ListTile( leading: const Icon(LucideIcons.heart, color: Colors.pink), title: const Text('Tambah ke Favorit', style: TextStyle(fontWeight: FontWeight.w600)), subtitle: const Text('Simpan ayat ini ke daftar favorit anda'), onTap: () { Navigator.pop(ctx); _saveBookmark(surahId, verseId, verse, isLastRead: false); }, ), const SizedBox(height: 8), ], ), ), ); } Future?> _getTafsirByVerse({ required int surahId, required int verseNumber, }) async { final tafsirItems = await MuslimApiService.instance.getTafsirBySurah(surahId); for (final item in tafsirItems) { if ((item['nomorAyat'] as num?)?.toInt() == verseNumber) { final wajiz = _sanitizeEnrichmentText(item['wajiz']); final tahlili = _sanitizeEnrichmentText(item['tahlili']); if (wajiz != null || tahlili != null) { return { ...item, 'wajiz': wajiz ?? '', 'tahlili': tahlili ?? '', }; } return null; } } return null; } String? _sanitizeEnrichmentText(dynamic raw) { final text = raw?.toString().trim() ?? ''; if (text.isEmpty) return null; final normalized = text.toLowerCase(); const invalidValues = { '0', '-', '—', 'null', 'undefined', 'n/a', '[]', '{}', }; if (invalidValues.contains(normalized)) return null; return text; } void _showTafsirDrawer({ required int surahId, required int verseNumber, }) { final surahName = _surah?['namaLatin']?.toString() ?? 'Surah $surahId'; final future = _getTafsirByVerse(surahId: surahId, verseNumber: verseNumber); showModalBottomSheet( context: context, isScrollControlled: true, useSafeArea: true, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), builder: (ctx) { final isDark = Theme.of(ctx).brightness == Brightness.dark; return FractionallySizedBox( heightFactor: 0.76, child: FutureBuilder?>( future: future, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); } if (snapshot.hasError) { return Center( child: Padding( padding: const EdgeInsets.all(24), child: Text( 'Gagal memuat tafsir ayat ini.', textAlign: TextAlign.center, style: TextStyle( color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight, ), ), ), ); } final data = snapshot.data; final wajiz = data?['wajiz']?.toString().trim() ?? ''; final tahlili = data?['tahlili']?.toString().trim() ?? ''; final hasContent = wajiz.isNotEmpty || tahlili.isNotEmpty; return SafeArea( top: false, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Center( child: Container( margin: const EdgeInsets.only(top: 10, bottom: 10), width: 40, height: 4, decoration: BoxDecoration( borderRadius: BorderRadius.circular(2), color: (isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight) .withValues(alpha: 0.3), ), ), ), Padding( padding: const EdgeInsets.fromLTRB(16, 4, 16, 8), child: Row( children: [ const Icon(LucideIcons.fileText, size: 18), const SizedBox(width: 8), Expanded( child: Text( 'Tafsir • $surahName : $verseNumber', style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w700, ), ), ), ], ), ), const Divider(height: 1), Expanded( child: !hasContent ? Center( child: Padding( padding: const EdgeInsets.all(24), child: Text( 'Tafsir untuk ayat ini belum tersedia.', textAlign: TextAlign.center, style: TextStyle( color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight, ), ), ), ) : ListView( padding: const EdgeInsets.fromLTRB(16, 12, 16, 24), children: [ if (wajiz.isNotEmpty) ...[ Text( 'Tafsir Wajiz', style: TextStyle( fontSize: 13, fontWeight: FontWeight.w700, color: AppColors.primary, letterSpacing: 0.4, ), ), const SizedBox(height: 8), Text( wajiz, style: TextStyle( fontSize: 14, height: 1.6, color: isDark ? AppColors.textPrimaryDark : AppColors.textPrimaryLight, ), ), const SizedBox(height: 16), ], if (tahlili.isNotEmpty) ...[ Text( 'Tafsir Tahlili', style: TextStyle( fontSize: 13, fontWeight: FontWeight.w700, color: AppColors.primary, letterSpacing: 0.4, ), ), const SizedBox(height: 8), Text( tahlili, style: TextStyle( fontSize: 14, height: 1.6, color: isDark ? AppColors.textPrimaryDark : AppColors.textPrimaryLight, ), ), ], ], ), ), ], ), ); }, ), ); }, ); } Future _saveBookmark( int surahId, int verseId, Map verse, {required bool isLastRead}) async { final box = Hive.box(HiveBoxes.bookmarks); // If setting as Last Read, we must clear any prior Last Read flags globally if (isLastRead) { final keysToDelete = []; for (final key in box.keys) { final b = box.get(key); if (b != null && b.isLastRead) { keysToDelete.add(key); } } await box.deleteAll(keysToDelete); } // Save the new bookmark final bookmark = QuranBookmark( surahId: surahId, verseId: verseId, surahName: _surah!['namaLatin'] ?? _surah!['nama'] ?? '', verseText: verse['teksArab'] ?? '', savedAt: DateTime.now(), isLastRead: isLastRead, verseLatin: verse['teksLatin'], verseTranslation: verse['teksIndonesia'], ); // Create a unique key. If favoriting an ayat that's already favorite, it overwrites. // If the ayat is LastRead, we give it a special key so it can coexist with a favorite copy if they want. final keySuffix = isLastRead ? '_lastread' : ''; await box.put('${surahId}_$verseId$keySuffix', bookmark); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(isLastRead ? 'Disimpan sebagai Terakhir Dibaca' : 'Disimpan ke Favorit'), backgroundColor: isLastRead ? AppColors.primary : Colors.pink, duration: const Duration(seconds: 1), ), ); } } Future _showEndTrackingDialog( TilawahSession session, int endVerseId) async { final endSurahId = _surah!['nomor'] ?? 1; final endSurahName = _surah!['namaLatin'] ?? ''; int calculatedAyat = 0; if (session.startSurahId == endSurahId) { // Same surah calculatedAyat = (endVerseId - session.startVerseId).abs() + 1; } else { // Cross surah calculation final allSurahs = await MuslimApiService.instance.getAllSurahs(); if (allSurahs.isNotEmpty) { int startSurahIdx = allSurahs.indexWhere((s) => s['nomor'] == session.startSurahId); int endSurahIdx = allSurahs.indexWhere((s) => s['nomor'] == endSurahId); if (startSurahIdx < 0 || endSurahIdx < 0) { calculatedAyat = (endVerseId - session.startVerseId).abs() + 1; } else { // Ensure chronological calculation if (startSurahIdx > endSurahIdx) { final tempIdx = startSurahIdx; startSurahIdx = endSurahIdx; endSurahIdx = tempIdx; } final startSurahData = allSurahs[startSurahIdx]; final int totalAyatInStart = (startSurahData['jumlahAyat'] as num?)?.toInt() ?? 1; calculatedAyat += (totalAyatInStart - session.startVerseId) + 1; // Ayats inside StartSurah for (int i = startSurahIdx + 1; i < endSurahIdx; i++) { calculatedAyat += (allSurahs[i]['jumlahAyat'] as int? ?? 0); // Intermediate Surahs } calculatedAyat += endVerseId; // Ayats inside EndSurah } } else { calculatedAyat = 1; // Fallback } } if (!mounted) return; showDialog( context: context, barrierDismissible: false, builder: (ctx) => AlertDialog( title: const Text('Catat Sesi Tilawah', style: TextStyle(fontWeight: FontWeight.bold)), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: AppColors.primary.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8), ), child: Column(children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text('Mulai:', style: TextStyle(fontSize: 13)), Text( '${session.startSurahName} : ${session.startVerseId}', style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 13)), ]), const Padding( padding: EdgeInsets.symmetric(vertical: 4), child: Divider()), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text('Selesai:', style: TextStyle(fontSize: 13)), Text('$endSurahName : $endVerseId', style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 13)), ]), ])), const SizedBox(height: 16), Row( children: [ const Icon(LucideIcons.bookOpen, size: 20, color: AppColors.primary), const SizedBox(width: 8), Text('Total Dibaca: $calculatedAyat Ayat', style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 15)), ], ), ], ), actions: [ TextButton( onPressed: () { ref.invalidate(tilawahTrackingProvider); Navigator.pop(ctx); }, child: const Text('Batal', style: TextStyle(color: Colors.red)), ), FilledButton( onPressed: () { final todayKey = DateFormat('yyyy-MM-dd').format(DateTime.now()); final logBox = Hive.box(HiveBoxes.worshipLogs); var log = logBox.get(todayKey); if (log == null) { log = DailyWorshipLog( date: todayKey, shalatLogs: { 'subuh': ShalatLog(), 'dzuhur': ShalatLog(), 'ashar': ShalatLog(), 'maghrib': ShalatLog(), 'isya': ShalatLog(), }, ); logBox.put(todayKey, log); } if (log.tilawahLog == null) { final settingsBox = Hive.box(HiveBoxes.settings); final settings = settingsBox.get('default') ?? AppSettings(); log.tilawahLog = TilawahLog( targetValue: settings.tilawahTargetValue, targetUnit: settings.tilawahTargetUnit, autoSync: _autoSyncEnabled, ); } log.tilawahLog!.rawAyatRead += calculatedAyat; log.save(); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('$calculatedAyat Ayat dicatat!'), backgroundColor: AppColors.primary, duration: const Duration(seconds: 2), ), ); } ref.invalidate(tilawahTrackingProvider); Navigator.pop(ctx); }, child: const Text('Simpan'), ), ], ), ); } void _showDisplaySettings() { showModalBottomSheet( context: context, isScrollControlled: true, useSafeArea: true, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), builder: (ctx) => StatefulBuilder( builder: (context, setModalState) { final keyboardInset = MediaQuery.of(context).viewInsets.bottom; return Padding( padding: EdgeInsets.fromLTRB(16, 20, 16, 20 + keyboardInset), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Pengaturan Tampilan', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), const SizedBox(height: 16), SwitchListTile( title: const Text('Tampilkan Latin'), value: _showLatin, activeColor: AppColors.primary, onChanged: (val) { setModalState(() => _showLatin = val); setState(() => _showLatin = val); final box = Hive.box(HiveBoxes.settings); final settings = box.get('default') ?? AppSettings(); settings.showLatin = val; settings.save(); }, ), SwitchListTile( title: const Text('Tampilkan Terjemahan'), value: _showTerjemahan, activeColor: AppColors.primary, onChanged: (val) { setModalState(() => _showTerjemahan = val); setState(() => _showTerjemahan = val); final box = Hive.box(HiveBoxes.settings); final settings = box.get('default') ?? AppSettings(); settings.showTerjemahan = val; settings.save(); }, ), const SizedBox(height: 8), const Text('Ukuran Font Arab'), Slider( value: _arabicFontSize, min: 16, max: 40, divisions: 12, label: '${_arabicFontSize.round()}pt', activeColor: AppColors.primary, onChanged: (val) { setModalState(() => _arabicFontSize = val); setState(() => _arabicFontSize = val); final box = Hive.box(HiveBoxes.settings); final settings = box.get('default') ?? AppSettings(); settings.arabicFontSize = val; settings.save(); }, ), const SizedBox(height: 16), ], ), ); }, ), ); } Widget _buildVerseSeparator({required bool isDark}) { return Padding( padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16), child: Row( children: [ Expanded( child: Divider( color: isDark ? AppColors.primary.withValues(alpha: 0.1) : AppColors.cream, ), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 8), child: Icon( LucideIcons.gem, size: 10, color: AppColors.primary.withValues(alpha: 0.3), ), ), Expanded( child: Divider( color: isDark ? AppColors.primary.withValues(alpha: 0.1) : AppColors.cream, ), ), ], ), ); } Widget _buildBismillahSection({required bool isDark}) { return Padding( padding: const EdgeInsets.fromLTRB(16, 16, 16, 4), child: Column( children: [ ArabicText( 'بِسْمِ اللّٰهِ الرَّحْمٰنِ الرَّحِيْمِ', baseFontSize: 26, fontWeight: FontWeight.w400, ), const SizedBox(height: 4), Text( '"Dengan nama Allah Yang Maha Pengasih, Maha Penyayang."', textAlign: TextAlign.center, style: TextStyle( fontSize: 12, fontStyle: FontStyle.italic, color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight, ), ), ], ), ); } Widget _buildVerseItem({ required int verseIndex, required bool isDark, required TilawahSession? trackingSession, required bool isLastRead, required bool isFav, }) { final verse = _verses[verseIndex]; final surahId = _surah!['nomor'] ?? 1; final verseId = (verse['nomorAyat'] ?? (verseIndex + 1)) as int; final isPlayingThis = _playingVerseIndex == verseIndex; final isHighlighted = isLastRead || isPlayingThis; return RepaintBoundary( child: Container( key: _verseKeys[verseIndex], color: isHighlighted ? AppColors.primary.withValues(alpha: 0.1) : Colors.transparent, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( width: 32, height: 32, decoration: BoxDecoration( color: AppColors.primary.withValues(alpha: 0.1), shape: BoxShape.circle, ), child: Center( child: Text( '$verseId', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w700, color: AppColors.primary, ), ), ), ), const Spacer(), Builder( builder: (context) { final audioUrl = _resolveVerseAudioUrl(verse); final isPlayingThis = _playingVerseIndex == verseIndex; return IconButton( onPressed: (audioUrl != null && audioUrl.isNotEmpty) ? () => _playAudio(verseIndex, audioUrl) : null, icon: isPlayingThis ? (_isAudioLoading ? SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, color: AppColors.primary, ), ) : Icon( LucideIcons.stopCircle, color: AppColors.primary, size: 24, )) : Icon( LucideIcons.playCircle, color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight, size: 20, ), ); }, ), IconButton( onPressed: () => _showTafsirDrawer( surahId: surahId, verseNumber: verseId, ), tooltip: 'Tafsir ayat', icon: Icon( LucideIcons.fileText, color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight, size: 20, ), ), if (_autoSyncEnabled) IconButton( onPressed: () { if (trackingSession == null) { ref .read(tilawahTrackingProvider.notifier) .startTracking( surahId: _surah!['nomor'] ?? 1, surahName: _surah!['namaLatin'] ?? '', verseId: verse['nomorAyat'] ?? (verseIndex + 1), ); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Sesi Tilawah dimulai'), backgroundColor: AppColors.primary, duration: Duration(seconds: 1), ), ); } else { _showEndTrackingDialog( trackingSession, verse['nomorAyat'] ?? (verseIndex + 1), ); } }, icon: Icon( trackingSession == null ? LucideIcons.flag : LucideIcons.stopCircle, color: trackingSession == null ? (isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight) : Colors.red, size: 20, ), ), IconButton( onPressed: () => _showBookmarkOptions(verseIndex), icon: Icon( isLastRead ? LucideIcons.pin : (isFav ? LucideIcons.heart : LucideIcons.bookmark), color: isLastRead ? AppColors.primary : (isFav ? Colors.pink : (isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight)), size: 20, ), ), ], ), const SizedBox(height: 12), SizedBox( width: double.infinity, child: ArabicText( verse['teksArab'] ?? '', textAlign: TextAlign.right, baseFontSize: 26, fontWeight: FontWeight.w400, height: 2.0, ), ), if (_showLatin) ...[ const SizedBox(height: 8), Text( verse['teksLatin'] ?? '', style: const TextStyle( fontSize: 13, fontStyle: FontStyle.italic, color: AppColors.primary, ), ), ], if (_showTerjemahan) ...[ const SizedBox(height: 8), Text( verse['teksIndonesia'] ?? '', style: TextStyle( fontSize: 14, height: 1.6, color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight, ), ), ], ], ), ), ); } @override Widget build(BuildContext context) { final trackingSession = ref.watch(tilawahTrackingProvider); final isDark = Theme.of(context).brightness == Brightness.dark; final needsSystemBottomInset = !_isHafalanMode; final totalVerses = _verses.length; final surahName = _surah?['namaLatin'] ?? 'Memuat...'; final surahArti = _surah?['arti'] ?? ''; final tempatTurun = _surah?['tempatTurun'] ?? ''; final showBismillah = (_surah?['nomor'] ?? 1) != 9; return Scaffold( appBar: AppBar( centerTitle: false, title: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( surahName, textAlign: TextAlign.start, ), if (totalVerses > 0) Text( '$surahArti • $totalVerses AYAT • $tempatTurun'.toUpperCase(), textAlign: TextAlign.start, style: TextStyle( fontSize: 10, fontWeight: FontWeight.w700, letterSpacing: 1.2, color: AppColors.primary, ), ), ], ), actions: [ IconButton( icon: const Icon(LucideIcons.headphones), tooltip: 'Murattal Surah', onPressed: _navigateToMurattal, ), IconButton( icon: Icon( LucideIcons.brain, color: _isHafalanMode ? AppColors.primary : (isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight), ), tooltip: 'Mode Hafalan', onPressed: () { setState(() { _isHafalanMode = !_isHafalanMode; if (!_isHafalanMode) { _stopHafalan(); } }); }, ), IconButton( icon: const Icon(LucideIcons.settings2), onPressed: _showDisplaySettings, ), ], ), body: SafeArea( top: false, bottom: needsSystemBottomInset, child: _loading ? const Center(child: CircularProgressIndicator()) : _verses.isEmpty ? const Center(child: Text('Tidak dapat memuat surah')) : Column( children: [ // Progress bar LinearProgressIndicator( value: 1.0, backgroundColor: AppColors.primary.withValues(alpha: 0.1), valueColor: AlwaysStoppedAnimation(AppColors.primary), minHeight: 3, ), // Verse list Expanded( child: ValueListenableBuilder( valueListenable: Hive.box(HiveBoxes.bookmarks) .listenable(), builder: (context, Box box, _) { final favoriteKeys = {}; final lastReadKeys = {}; for (final entry in box.toMap().entries) { final key = entry.key.toString(); final bookmark = entry.value; if (bookmark.isLastRead) { lastReadKeys.add(key); } else { favoriteKeys.add(key); } } return SingleChildScrollView( controller: _scrollController, padding: EdgeInsets.fromLTRB( 0, 16, 0, needsSystemBottomInset ? 28 : 16, ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ if (showBismillah) ...[ _buildBismillahSection(isDark: isDark), const SizedBox(height: 8), ], for (int verseIndex = 0; verseIndex < _verses.length; verseIndex++) ...[ if (showBismillah || verseIndex > 0) _buildVerseSeparator(isDark: isDark), _buildVerseItem( verseIndex: verseIndex, isDark: isDark, trackingSession: trackingSession, isLastRead: lastReadKeys.contains( '${_surah!['nomor']}_${(_verses[verseIndex]['nomorAyat'] ?? (verseIndex + 1)) as int}_lastread', ), isFav: favoriteKeys.contains( '${_surah!['nomor']}_${(_verses[verseIndex]['nomorAyat'] ?? (verseIndex + 1)) as int}', ), ), ], ], ), ); }, ), ), ], ), ), bottomNavigationBar: _isHafalanMode ? _buildHafalanControlBar() : null, ); } Widget _buildHafalanControlBar() { final isDark = Theme.of(context).brightness == Brightness.dark; // Ensure logical bounds just in case if (_hafalanStartAyat > _verses.length) _hafalanStartAyat = _verses.length; if (_hafalanEndAyat < _hafalanStartAyat) _hafalanEndAyat = _hafalanStartAyat; if (_hafalanEndAyat > _verses.length) _hafalanEndAyat = _verses.length; return Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( color: isDark ? AppColors.surfaceDark : AppColors.surfaceLight, boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.05), offset: const Offset(0, -4), blurRadius: 16, ), ], borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), ), child: SafeArea( child: Column( mainAxisSize: MainAxisSize.min, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( 'Mode Hafalan', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), ), if (_isHafalanPlaying && _hafalanLoopCount > 0) Text( 'Loop: ${_currentLoop + 1} / $_hafalanLoopCount', style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: AppColors.primary, ), ) else if (_isHafalanPlaying) Text( 'Loop: ${_currentLoop + 1} / ∞', style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: AppColors.primary, ), ), ], ), const SizedBox(height: 16), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ // Start Ayat _buildHafalanDropdown( label: 'Mulai', value: _hafalanStartAyat, items: List.generate(_verses.length, (i) => i + 1), onChanged: _isHafalanPlaying ? null : (val) { setState(() { _hafalanStartAyat = val!; if (_hafalanEndAyat < val) _hafalanEndAyat = val; }); }, ), const Text('-', style: TextStyle(color: Colors.grey)), // End Ayat _buildHafalanDropdown( label: 'Sampai', value: _hafalanEndAyat, items: List.generate(_verses.length - _hafalanStartAyat + 1, (i) => _hafalanStartAyat + i), onChanged: _isHafalanPlaying ? null : (val) => setState(() => _hafalanEndAyat = val!), ), const SizedBox(width: 8), // Loop Count _buildHafalanDropdown( label: 'Ulangi', value: _hafalanLoopCount, items: [1, 3, 5, 7, 0], // 0 = infinite displayMap: {0: '∞', 1: '1x', 3: '3x', 5: '5x', 7: '7x'}, onChanged: _isHafalanPlaying ? null : (val) => setState(() => _hafalanLoopCount = val!), ), const SizedBox(width: 16), // Play/Stop Button GestureDetector( onTap: () { if (_isHafalanPlaying) { _stopHafalan(); } else { setState(() { _isHafalanPlaying = true; _currentLoop = 0; }); final startIdx = _hafalanStartAyat - 1; final audioUrl = _resolveVerseAudioUrl(_verses[startIdx]); if (audioUrl != null) { _playAudio(startIdx, audioUrl); _scrollToVerse(startIdx, attempts: 0); } } }, child: Container( width: 48, height: 48, decoration: BoxDecoration( color: AppColors.primary, shape: BoxShape.circle, boxShadow: [ BoxShadow( color: AppColors.primary.withValues(alpha: 0.3), blurRadius: 8, offset: const Offset(0, 4), ), ], ), child: Icon( _isHafalanPlaying ? LucideIcons.square : LucideIcons.play, color: Colors.white, size: 28, ), ), ), ], ), ], ), ), ); } Widget _buildHafalanDropdown({ required String label, required T value, required List items, required void Function(T?)? onChanged, Map? displayMap, }) { final isDark = Theme.of(context).brightness == Brightness.dark; return Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( label, style: TextStyle( fontSize: 10, color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight, ), ), const SizedBox(height: 4), Container( height: 36, padding: const EdgeInsets.symmetric(horizontal: 12), decoration: BoxDecoration( color: isDark ? AppColors.backgroundDark : AppColors.backgroundLight, borderRadius: BorderRadius.circular(8), border: Border.all( color: AppColors.primary.withValues(alpha: 0.2), ), ), child: DropdownButtonHideUnderline( child: DropdownButton( value: value, items: items.map((e) { return DropdownMenuItem( value: e, child: Text( displayMap != null ? displayMap[e]! : e.toString(), style: const TextStyle( fontSize: 14, fontWeight: FontWeight.bold), ), ); }).toList(), onChanged: onChanged, icon: const Icon(LucideIcons.chevronDown, size: 16), isDense: true, borderRadius: BorderRadius.circular(12), ), ), ), ], ); } }