import 'dart:async'; 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 '../../../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/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 = AudioPlayer(); int? _playingVerseIndex; bool _isAudioLoading = false; // Display Settings bool _showLatin = true; bool _showTerjemahan = true; // 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(); _autoSyncEnabled = settings.tilawahAutoSync; _targetUnit = settings.tilawahTargetUnit; _showLatin = settings.showLatin; _showTerjemahan = settings.showTerjemahan; _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 = _verses[nextIdx]['audio']?['05'] as String?; if (audioUrl != null) { _playAudio(nextIdx, audioUrl); _attemptScroll(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 = _verses[startIdx]['audio']?['05'] as String?; if (audioUrl != null) { _playAudio(startIdx, audioUrl); _attemptScroll(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(); _stopHafalan(isDisposing: true); _audioPlayer.dispose(); _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) { // Wait for next frame so the UI finishes building the keys WidgetsBinding.instance.addPostFrameCallback((_) { _attemptScroll(targetIndex, attempts: 0); }); } } } void _attemptScroll(int targetIndex, {required int attempts}) { if (!mounted) return; final key = _verseKeys[targetIndex]; if (key != null && key.currentContext != null) { // It's built! Scroll directly to it. Scrollable.ensureVisible( key.currentContext!, duration: const Duration(milliseconds: 400), curve: Curves.easeInOut, alignment: 0.1, // Aligns slightly below the very top of the screen ); } else if (attempts < 15) { // It's not built yet. We manually nudge the scroll window. if (_scrollController.hasClients) { int firstBuiltIndex = -1; for (int i = 0; i < _verses.length; i++) { if (_verseKeys[i]?.currentContext != null) { firstBuiltIndex = i; break; } } final currentOffset = _scrollController.offset; final isScrollingUp = firstBuiltIndex != -1 && targetIndex < firstBuiltIndex; final scrollAmount = isScrollingUp ? -1000.0 : 1000.0; final maxScroll = _scrollController.position.maxScrollExtent; final newOffset = (currentOffset + scrollAmount).clamp(0.0, maxScroll); _scrollController.jumpTo(newOffset); // Wait a frame for items to build, then try again WidgetsBinding.instance.addPostFrameCallback((_) { _attemptScroll(targetIndex, attempts: attempts + 1); }); } } } 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 { await _audioPlayer.setUrl(audioUrl); _audioPlayer.play(); if (mounted) { setState(() { _isAudioLoading = false; }); } } catch (e) { 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, 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 _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); 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: settings.tilawahAutoSync, ); } 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, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), builder: (ctx) => StatefulBuilder( builder: (context, setModalState) { return Padding( padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 16), 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: 16), ], ), ); }, ), ); } @override Widget build(BuildContext context) { final trackingSession = ref.watch(tilawahTrackingProvider); final isDark = Theme.of(context).brightness == Brightness.dark; final totalVerses = _verses.length; final surahName = _surah?['namaLatin'] ?? 'Memuat...'; final surahArti = _surah?['arti'] ?? ''; final tempatTurun = _surah?['tempatTurun'] ?? ''; return Scaffold( appBar: AppBar( title: Column( children: [ Text(surahName), if (totalVerses > 0) Text( '$surahArti • $totalVerses AYAT • $tempatTurun'.toUpperCase(), 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: _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, ), // Bismillah (skip for At-Tawbah) if ((_surah?['nomor'] ?? 1) != 9) Padding( padding: const EdgeInsets.symmetric(vertical: 16), child: Column( children: [ const Text( 'بِسْمِ اللّٰهِ الرَّحْمٰنِ الرَّحِيْمِ', style: TextStyle( fontFamily: 'Amiri', fontSize: 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, ), ), ], ), ), // Verse list Expanded( child: ListView.separated( controller: _scrollController, padding: const EdgeInsets.symmetric(vertical: 16), itemCount: _verses.length, separatorBuilder: (_, __) => 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, ), ), ], ), ), itemBuilder: (context, i) { final verse = _verses[i]; final surahId = _surah!['nomor'] ?? 1; final verseId = (verse['nomorAyat'] ?? (i + 1)) as int; final lastReadKey = '${surahId}_${verseId}_lastread'; final favKey = '${surahId}_$verseId'; return ValueListenableBuilder( valueListenable: Hive.box(HiveBoxes.bookmarks).listenable(), builder: (context, box, _) { final isLastRead = box.containsKey(lastReadKey); final isFav = box.containsKey(favKey); final isPlayingThis = _playingVerseIndex == i; final isHighlighted = isLastRead || isPlayingThis; return Container( key: _verseKeys[i], color: isHighlighted ? AppColors.primary.withValues(alpha: 0.1) : Colors.transparent, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Action row Row( children: [ Container( width: 32, height: 32, decoration: BoxDecoration( color: AppColors.primary .withValues(alpha: 0.1), shape: BoxShape.circle, ), child: Center( child: Text( '${verse['nomorAyat'] ?? i + 1}', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w700, color: AppColors.primary, ), ), ), ), const Spacer(), Builder( builder: (context) { final audioUrl = verse['audio']?['05'] as String?; final isPlayingThis = _playingVerseIndex == i; return IconButton( onPressed: (audioUrl != null && audioUrl.isNotEmpty) ? () => _playAudio(i, 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), ); } ), if (_autoSyncEnabled) IconButton( onPressed: () { if (trackingSession == null) { ref.read(tilawahTrackingProvider.notifier).startTracking( surahId: _surah!['nomor'] ?? 1, surahName: _surah!['namaLatin'] ?? '', verseId: verse['nomorAyat'] ?? (i + 1), ); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Sesi Tilawah dimulai'), backgroundColor: AppColors.primary, duration: Duration(seconds: 1), ), ); } else { _showEndTrackingDialog(trackingSession, verse['nomorAyat'] ?? (i + 1)); } }, icon: Icon( trackingSession == null ? LucideIcons.flag : LucideIcons.stopCircle, color: trackingSession == null ? (isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight) : Colors.red, size: 20), ), IconButton( onPressed: () => _showBookmarkOptions(i), 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), // Arabic text SizedBox( width: double.infinity, child: Text( verse['teksArab'] ?? '', textAlign: TextAlign.right, style: const TextStyle( fontFamily: 'Amiri', fontSize: 26, fontWeight: FontWeight.w400, height: 2.0, ), ), ), if (_showLatin) ...[ const SizedBox(height: 8), // Latin transliteration Text( verse['teksLatin'] ?? '', style: const TextStyle( fontSize: 13, fontStyle: FontStyle.italic, color: AppColors.primary, ), ), ], if (_showTerjemahan) ...[ const SizedBox(height: 8), // Indonesian translation Text( verse['teksIndonesia'] ?? '', style: TextStyle( fontSize: 14, height: 1.6, color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight, ), ), ], ], ), ); }, ); }, ), ), ], ), 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 = _verses[startIdx]['audio']?['05'] as String?; if (audioUrl != null) { _playAudio(startIdx, audioUrl); _attemptScroll(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), ), ), ), ], ); } }