Files
jamshalat-diary/lib/features/quran/presentation/quran_reading_screen.dart
2026-03-16 00:30:32 +07:00

1058 lines
41 KiB
Dart

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<QuranReadingScreen> createState() => _QuranReadingScreenState();
}
class _QuranReadingScreenState extends ConsumerState<QuranReadingScreen> {
Map<String, dynamic>? _surah;
List<Map<String, dynamic>> _verses = [];
bool _loading = true;
bool _autoSyncEnabled = false;
String _targetUnit = 'Juz';
final ScrollController _scrollController = ScrollController();
final Map<int, GlobalKey> _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<AppSettings>(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<void> _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<Map<String, dynamic>>.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<void> _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<void> _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<void> _saveBookmark(int surahId, int verseId, Map<String, dynamic> verse, {required bool isLastRead}) async {
final box = Hive.box<QuranBookmark>(HiveBoxes.bookmarks);
// If setting as Last Read, we must clear any prior Last Read flags globally
if (isLastRead) {
final keysToDelete = <dynamic>[];
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<void> _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<DailyWorshipLog>(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<AppSettings>(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<AppSettings>(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<AppSettings>(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<Color>(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<QuranBookmark>(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<int>(
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<int>(
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<int>(
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<T>({
required String label,
required T value,
required List<T> items,
required void Function(T?)? onChanged,
Map<T, String>? 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<T>(
value: value,
items: items.map((e) {
return DropdownMenuItem<T>(
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),
),
),
),
],
);
}
}