feat: Murattal player enhancements & prayer schedule auto-scroll
- Murattal: Spotify-style 5-button controls [Shuffle, Prev, Play, Next, Playlist] - Murattal: Animated 7-bar equalizer visualization in player circle - Murattal: Unsplash API background with frosted glass player overlay - Murattal: Transparent AppBar with backdrop blur - Murattal: Surah playlist bottom sheet with full 114 Surah list - Murattal: Auto-play disabled on screen open, enabled on navigation - Murattal: Shuffle mode for random Surah playback - Murattal: Photographer attribution per Unsplash guidelines - Dashboard: Auto-scroll prayer schedule to next active prayer - Fix: setState lifecycle errors on Reading & Murattal screens - Setup: flutter_dotenv, cached_network_image, url_launcher deps
This commit is contained in:
0
lib/features/quran/presentation/.gitkeep
Normal file
0
lib/features/quran/presentation/.gitkeep
Normal file
1
lib/features/quran/presentation/placeholder.dart
Normal file
1
lib/features/quran/presentation/placeholder.dart
Normal file
@@ -0,0 +1 @@
|
||||
// TODO: implement
|
||||
323
lib/features/quran/presentation/quran_bookmarks_screen.dart
Normal file
323
lib/features/quran/presentation/quran_bookmarks_screen.dart
Normal file
@@ -0,0 +1,323 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:go_router/go_router.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';
|
||||
|
||||
class QuranBookmarksScreen extends StatefulWidget {
|
||||
const QuranBookmarksScreen({super.key});
|
||||
|
||||
@override
|
||||
State<QuranBookmarksScreen> createState() => _QuranBookmarksScreenState();
|
||||
}
|
||||
|
||||
class _QuranBookmarksScreenState extends State<QuranBookmarksScreen> {
|
||||
bool _showLatin = true;
|
||||
bool _showTerjemahan = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final box = Hive.box<AppSettings>(HiveBoxes.settings);
|
||||
final settings = box.get('default') ?? AppSettings();
|
||||
_showLatin = settings.showLatin;
|
||||
_showTerjemahan = settings.showTerjemahan;
|
||||
}
|
||||
|
||||
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 isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Markah Al-Quran'),
|
||||
centerTitle: false,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.settings_display),
|
||||
onPressed: _showDisplaySettings,
|
||||
),
|
||||
],
|
||||
),
|
||||
body: ValueListenableBuilder(
|
||||
valueListenable: Hive.box<QuranBookmark>(HiveBoxes.bookmarks).listenable(),
|
||||
builder: (context, Box<QuranBookmark> box, _) {
|
||||
if (box.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.bookmark_border,
|
||||
size: 64,
|
||||
color: AppColors.primary.withValues(alpha: 0.3),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Belum ada markah',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: isDark ? Colors.white : Colors.black87,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Tandai ayat saat membaca Al-Quran',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Filter bookmarks
|
||||
final allBookmarks = box.values.toList();
|
||||
final lastRead = allBookmarks.where((b) => b.isLastRead).toList();
|
||||
final favorites = allBookmarks.where((b) => !b.isLastRead).toList()
|
||||
..sort((a, b) => b.savedAt.compareTo(a.savedAt));
|
||||
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
if (lastRead.isNotEmpty) ...[
|
||||
const Text(
|
||||
'TERAKHIR DIBACA',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 1.5,
|
||||
color: AppColors.sage,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildBookmarkCard(context, lastRead.first, isDark, box, isLastRead: true),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
|
||||
if (favorites.isNotEmpty) ...[
|
||||
const Text(
|
||||
'AYAT FAVORIT',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 1.5,
|
||||
color: AppColors.sage,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
...favorites.map((fav) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: _buildBookmarkCard(context, fav, isDark, box, isLastRead: false),
|
||||
)),
|
||||
],
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBookmarkCard(BuildContext context, QuranBookmark bookmark, bool isDark, Box<QuranBookmark> box, {required bool isLastRead}) {
|
||||
final dateStr = DateFormat('dd MMM yyyy, HH:mm').format(bookmark.savedAt);
|
||||
|
||||
return InkWell(
|
||||
onTap: () => context.push('/tools/quran/${bookmark.surahId}?startVerse=${bookmark.verseId}'),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: isDark ? AppColors.surfaceDark : AppColors.surfaceLight,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: isLastRead
|
||||
? AppColors.primary.withValues(alpha: 0.3)
|
||||
: (isDark ? AppColors.primary.withValues(alpha: 0.1) : AppColors.cream),
|
||||
width: isLastRead ? 1.5 : 1.0,
|
||||
),
|
||||
boxShadow: isLastRead ? [
|
||||
BoxShadow(
|
||||
color: AppColors.primary.withValues(alpha: 0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
)
|
||||
] : null,
|
||||
),
|
||||
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.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (isLastRead) ...[
|
||||
const Icon(Icons.push_pin, size: 12, color: AppColors.primary),
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
Text(
|
||||
'QS. ${bookmark.surahName}: ${bookmark.verseId}',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete_outline, color: Colors.red, size: 20),
|
||||
onPressed: () => box.delete(bookmark.key),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Text(
|
||||
bookmark.verseText,
|
||||
textAlign: TextAlign.right,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Amiri',
|
||||
fontSize: 22,
|
||||
height: 1.8,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
if (_showLatin && bookmark.verseLatin != null) ...[
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
bookmark.verseLatin!,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontStyle: FontStyle.italic,
|
||||
color: AppColors.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
if (_showTerjemahan && bookmark.verseTranslation != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
bookmark.verseTranslation!,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
height: 1.6,
|
||||
color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
if (isLastRead) ...[
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: FilledButton.icon(
|
||||
onPressed: () => context.push('/tools/quran/${bookmark.surahId}?startVerse=${bookmark.verseId}'),
|
||||
icon: const Icon(Icons.menu_book, size: 18),
|
||||
label: const Text('Lanjutkan Membaca'),
|
||||
style: FilledButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.access_time,
|
||||
size: 12,
|
||||
color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'${isLastRead ? 'Ditandai' : 'Disimpan'}: $dateStr',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
869
lib/features/quran/presentation/quran_murattal_screen.dart
Normal file
869
lib/features/quran/presentation/quran_murattal_screen.dart
Normal file
@@ -0,0 +1,869 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
import 'dart:ui';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:just_audio/just_audio.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import '../../../app/theme/app_colors.dart';
|
||||
import '../../../data/services/equran_service.dart';
|
||||
import '../../../data/services/unsplash_service.dart';
|
||||
|
||||
/// Quran Murattal (audio player) screen.
|
||||
/// Implements full Surah playback using just_audio and EQuran v2 API.
|
||||
class QuranMurattalScreen extends ConsumerStatefulWidget {
|
||||
final String surahId;
|
||||
final String? initialQariId;
|
||||
final bool autoPlay;
|
||||
const QuranMurattalScreen({
|
||||
super.key,
|
||||
required this.surahId,
|
||||
this.initialQariId,
|
||||
this.autoPlay = false,
|
||||
});
|
||||
|
||||
@override
|
||||
ConsumerState<QuranMurattalScreen> createState() =>
|
||||
_QuranMurattalScreenState();
|
||||
}
|
||||
|
||||
class _QuranMurattalScreenState extends ConsumerState<QuranMurattalScreen> {
|
||||
final AudioPlayer _audioPlayer = AudioPlayer();
|
||||
|
||||
Map<String, dynamic>? _surahData;
|
||||
bool _isLoading = true;
|
||||
|
||||
// Audio State Variables
|
||||
bool _isPlaying = false;
|
||||
bool _isBuffering = false;
|
||||
Duration _position = Duration.zero;
|
||||
Duration _duration = Duration.zero;
|
||||
|
||||
StreamSubscription? _positionSub;
|
||||
StreamSubscription? _durationSub;
|
||||
StreamSubscription? _playerStateSub;
|
||||
|
||||
// Qari State
|
||||
late String _selectedQariId;
|
||||
|
||||
// Shuffle State
|
||||
bool _isShuffleEnabled = false;
|
||||
|
||||
// Unsplash Background
|
||||
Map<String, String>? _unsplashPhoto;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_selectedQariId = widget.initialQariId ?? '05'; // Default to Misyari Rasyid Al-Afasi
|
||||
_initDataAndPlayer();
|
||||
_loadUnsplashPhoto();
|
||||
}
|
||||
|
||||
Future<void> _loadUnsplashPhoto() async {
|
||||
final photo = await UnsplashService.instance.getIslamicPhoto();
|
||||
if (mounted && photo != null) {
|
||||
setState(() => _unsplashPhoto = photo);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _initDataAndPlayer() async {
|
||||
final surahNum = int.tryParse(widget.surahId) ?? 1;
|
||||
final data = await EQuranService.instance.getSurah(surahNum);
|
||||
|
||||
if (data != null && mounted) {
|
||||
setState(() {
|
||||
_surahData = data;
|
||||
_isLoading = false;
|
||||
});
|
||||
_setupAudioStreamListeners();
|
||||
_loadAudioSource();
|
||||
} else if (mounted) {
|
||||
setState(() => _isLoading = false);
|
||||
}
|
||||
}
|
||||
|
||||
void _setupAudioStreamListeners() {
|
||||
_positionSub = _audioPlayer.positionStream.listen((pos) {
|
||||
if (mounted) setState(() => _position = pos);
|
||||
});
|
||||
|
||||
_durationSub = _audioPlayer.durationStream.listen((dur) {
|
||||
if (mounted && dur != null) setState(() => _duration = dur);
|
||||
});
|
||||
|
||||
_playerStateSub = _audioPlayer.playerStateStream.listen((state) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_isPlaying = state.playing;
|
||||
_isBuffering = state.processingState == ProcessingState.buffering ||
|
||||
state.processingState == ProcessingState.loading;
|
||||
|
||||
// Auto pause and reset to 0 when finished
|
||||
if (state.processingState == ProcessingState.completed) {
|
||||
_audioPlayer.pause();
|
||||
_audioPlayer.seek(Duration.zero);
|
||||
|
||||
// Auto-play next surah
|
||||
final currentSurah = int.tryParse(widget.surahId) ?? 1;
|
||||
if (_isShuffleEnabled) {
|
||||
final random = Random();
|
||||
int nextSurah = random.nextInt(114) + 1;
|
||||
while (nextSurah == currentSurah) {
|
||||
nextSurah = random.nextInt(114) + 1;
|
||||
}
|
||||
_navigateToSurahNumber(nextSurah, autoplay: true);
|
||||
} else if (currentSurah < 114) {
|
||||
_navigateToSurah(1);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _loadAudioSource() async {
|
||||
if (_surahData == null) return;
|
||||
|
||||
final audioUrls = _surahData!['audioFull'];
|
||||
if (audioUrls != null && audioUrls[_selectedQariId] != null) {
|
||||
try {
|
||||
await _audioPlayer.setUrl(audioUrls[_selectedQariId]);
|
||||
if (widget.autoPlay) {
|
||||
_audioPlayer.play();
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Gagal memuat audio murattal')),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_positionSub?.cancel();
|
||||
_durationSub?.cancel();
|
||||
_playerStateSub?.cancel();
|
||||
_audioPlayer.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
String _formatDuration(Duration d) {
|
||||
final minutes = d.inMinutes.remainder(60).toString().padLeft(2, '0');
|
||||
final seconds = d.inSeconds.remainder(60).toString().padLeft(2, '0');
|
||||
if (d.inHours > 0) {
|
||||
return '${d.inHours}:$minutes:$seconds';
|
||||
}
|
||||
return '$minutes:$seconds';
|
||||
}
|
||||
|
||||
void _seekRelative(int seconds) {
|
||||
final newPosition = _position + Duration(seconds: seconds);
|
||||
if (newPosition < Duration.zero) {
|
||||
_audioPlayer.seek(Duration.zero);
|
||||
} else if (newPosition > _duration) {
|
||||
_audioPlayer.seek(_duration);
|
||||
} else {
|
||||
_audioPlayer.seek(newPosition);
|
||||
}
|
||||
}
|
||||
|
||||
void _navigateToSurah(int direction) {
|
||||
final currentSurah = int.tryParse(widget.surahId) ?? 1;
|
||||
final nextSurah = currentSurah + direction;
|
||||
_navigateToSurahNumber(nextSurah, autoplay: true);
|
||||
}
|
||||
|
||||
void _navigateToSurahNumber(int surahNum, {bool autoplay = false}) {
|
||||
if (surahNum >= 1 && surahNum <= 114) {
|
||||
context.pushReplacement('/tools/quran/$surahNum/murattal?qariId=$_selectedQariId&autoplay=$autoplay');
|
||||
}
|
||||
}
|
||||
|
||||
void _showQariSelector() {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
builder: (context) {
|
||||
return SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
width: 40,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.withValues(alpha: 0.3),
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Pilih Qari',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
...EQuranService.qariNames.entries.map((entry) {
|
||||
final isSelected = entry.key == _selectedQariId;
|
||||
return ListTile(
|
||||
leading: Icon(
|
||||
isSelected ? Icons.radio_button_checked : Icons.radio_button_unchecked,
|
||||
color: isSelected ? AppColors.primary : Colors.grey,
|
||||
),
|
||||
title: Text(
|
||||
entry.value,
|
||||
style: TextStyle(
|
||||
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
|
||||
color: isSelected ? AppColors.primary : null,
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
if (!isSelected) {
|
||||
setState(() => _selectedQariId = entry.key);
|
||||
_loadAudioSource();
|
||||
}
|
||||
},
|
||||
);
|
||||
}),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _showSurahPlaylist() {
|
||||
final currentSurah = int.tryParse(widget.surahId) ?? 1;
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
builder: (context) {
|
||||
return DraggableScrollableSheet(
|
||||
initialChildSize: 0.7,
|
||||
minChildSize: 0.4,
|
||||
maxChildSize: 0.9,
|
||||
expand: false,
|
||||
builder: (context, scrollController) {
|
||||
return Column(
|
||||
children: [
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
width: 40,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.withValues(alpha: 0.3),
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Playlist Surah',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Expanded(
|
||||
child: FutureBuilder<List<Map<String, dynamic>>>(
|
||||
future: EQuranService.instance.getAllSurahs(),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
final surahs = snapshot.data!;
|
||||
return ListView.builder(
|
||||
controller: scrollController,
|
||||
itemCount: surahs.length,
|
||||
itemBuilder: (context, i) {
|
||||
final surah = surahs[i];
|
||||
final surahNum = surah['nomor'] ?? (i + 1);
|
||||
final isCurrentSurah = surahNum == currentSurah;
|
||||
return ListTile(
|
||||
leading: Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: isCurrentSurah
|
||||
? AppColors.primary
|
||||
: AppColors.primary.withValues(alpha: 0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'$surahNum',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isCurrentSurah ? Colors.white : AppColors.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
surah['namaLatin'] ?? 'Surah $surahNum',
|
||||
style: TextStyle(
|
||||
fontWeight: isCurrentSurah ? FontWeight.bold : FontWeight.normal,
|
||||
color: isCurrentSurah ? AppColors.primary : null,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
'${surah['arti'] ?? ''} • ${surah['jumlahAyat'] ?? 0} Ayat',
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
trailing: isCurrentSurah
|
||||
? Icon(Icons.graphic_eq, color: AppColors.primary, size: 20)
|
||||
: null,
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
if (!isCurrentSurah) {
|
||||
context.pushReplacement(
|
||||
'/tools/quran/$surahNum/murattal?qariId=$_selectedQariId',
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final surahName = _surahData?['namaLatin'] ?? 'Surah ${widget.surahId}';
|
||||
|
||||
final hasPhoto = _unsplashPhoto != null;
|
||||
|
||||
return Scaffold(
|
||||
extendBodyBehindAppBar: hasPhoto,
|
||||
appBar: AppBar(
|
||||
backgroundColor: hasPhoto ? Colors.transparent : null,
|
||||
elevation: hasPhoto ? 0 : null,
|
||||
iconTheme: hasPhoto ? const IconThemeData(color: Colors.white) : null,
|
||||
flexibleSpace: hasPhoto
|
||||
? ClipRect(
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
|
||||
child: Container(
|
||||
color: Colors.black.withValues(alpha: 0.2),
|
||||
),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
title: Column(
|
||||
children: [
|
||||
Text(
|
||||
'Surah $surahName',
|
||||
style: TextStyle(
|
||||
color: hasPhoto ? Colors.white : null,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'MURATTAL',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 1.5,
|
||||
color: hasPhoto ? Colors.white70 : AppColors.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
),
|
||||
body: _isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
// === FULL-BLEED BACKGROUND ===
|
||||
if (_unsplashPhoto != null)
|
||||
CachedNetworkImage(
|
||||
imageUrl: _unsplashPhoto!['imageUrl'] ?? '',
|
||||
fit: BoxFit.cover,
|
||||
placeholder: (context, url) => Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
AppColors.primary.withValues(alpha: 0.1),
|
||||
AppColors.primary.withValues(alpha: 0.05),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
errorWidget: (context, url, error) => Container(
|
||||
color: isDark ? Colors.black : Colors.grey.shade100,
|
||||
),
|
||||
)
|
||||
else
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
AppColors.primary.withValues(alpha: 0.1),
|
||||
AppColors.primary.withValues(alpha: 0.03),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Dark overlay
|
||||
if (_unsplashPhoto != null)
|
||||
Container(
|
||||
color: Colors.black.withValues(alpha: 0.35),
|
||||
),
|
||||
|
||||
// === CENTER CONTENT (Equalizer + Text) ===
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 280, // leave room for the player
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// Equalizer circle
|
||||
Container(
|
||||
width: 220,
|
||||
height: 220,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: RadialGradient(
|
||||
colors: _unsplashPhoto != null
|
||||
? [
|
||||
Colors.white.withValues(alpha: 0.15),
|
||||
Colors.white.withValues(alpha: 0.05),
|
||||
]
|
||||
: [
|
||||
AppColors.primary.withValues(alpha: 0.2),
|
||||
AppColors.primary.withValues(alpha: 0.05),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: Container(
|
||||
width: 140,
|
||||
height: 140,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: _unsplashPhoto != null
|
||||
? Colors.white.withValues(alpha: 0.1)
|
||||
: AppColors.primary.withValues(alpha: 0.12),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: List.generate(7, (i) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 2),
|
||||
child: _EqualizerBar(
|
||||
isPlaying: _isPlaying,
|
||||
index: i,
|
||||
color: _unsplashPhoto != null
|
||||
? Colors.white
|
||||
: AppColors.primary,
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
// Qari name
|
||||
Text(
|
||||
EQuranService.qariNames[_selectedQariId] ?? 'Memuat...',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: _unsplashPhoto != null ? Colors.white : null,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Memutar Surat $surahName',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: _unsplashPhoto != null
|
||||
? Colors.white70
|
||||
: AppColors.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// === FROSTED GLASS PLAYER CONTROLS ===
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
|
||||
child: BackdropFilter(
|
||||
filter: _unsplashPhoto != null
|
||||
? ImageFilter.blur(sigmaX: 20, sigmaY: 20)
|
||||
: ImageFilter.blur(sigmaX: 0, sigmaY: 0),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.fromLTRB(24, 16, 24, 48),
|
||||
decoration: BoxDecoration(
|
||||
color: _unsplashPhoto != null
|
||||
? Colors.white.withValues(alpha: 0.15)
|
||||
: (isDark ? AppColors.surfaceDark : AppColors.surfaceLight),
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
|
||||
border: _unsplashPhoto != null
|
||||
? Border(
|
||||
top: BorderSide(
|
||||
color: Colors.white.withValues(alpha: 0.2),
|
||||
width: 0.5,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Progress slider
|
||||
SliderTheme(
|
||||
data: SliderTheme.of(context).copyWith(
|
||||
trackHeight: 3,
|
||||
thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 6),
|
||||
),
|
||||
child: Slider(
|
||||
value: _position.inMilliseconds.toDouble(),
|
||||
max: _duration.inMilliseconds > 0 ? _duration.inMilliseconds.toDouble() : 1.0,
|
||||
onChanged: (v) {
|
||||
_audioPlayer.seek(Duration(milliseconds: v.round()));
|
||||
},
|
||||
activeColor: _unsplashPhoto != null ? Colors.white : AppColors.primary,
|
||||
inactiveColor: _unsplashPhoto != null
|
||||
? Colors.white.withValues(alpha: 0.2)
|
||||
: AppColors.primary.withValues(alpha: 0.15),
|
||||
),
|
||||
),
|
||||
// Time labels
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
_formatDuration(_position),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: _unsplashPhoto != null
|
||||
? Colors.white70
|
||||
: (isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
_formatDuration(_duration),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: _unsplashPhoto != null
|
||||
? Colors.white70
|
||||
: (isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// Playback controls — Spotify-style 5-button row
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
// Shuffle
|
||||
IconButton(
|
||||
onPressed: () => setState(() => _isShuffleEnabled = !_isShuffleEnabled),
|
||||
icon: Icon(
|
||||
Icons.shuffle_rounded,
|
||||
size: 24,
|
||||
color: _isShuffleEnabled
|
||||
? (_unsplashPhoto != null ? Colors.white : AppColors.primary)
|
||||
: (_unsplashPhoto != null
|
||||
? Colors.white54
|
||||
: (isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight)),
|
||||
),
|
||||
),
|
||||
// Previous Surah
|
||||
IconButton(
|
||||
onPressed: (int.tryParse(widget.surahId) ?? 1) > 1
|
||||
? () => _navigateToSurah(-1)
|
||||
: null,
|
||||
icon: Icon(
|
||||
Icons.skip_previous_rounded,
|
||||
size: 36,
|
||||
color: (int.tryParse(widget.surahId) ?? 1) > 1
|
||||
? (_unsplashPhoto != null ? Colors.white : (isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight))
|
||||
: Colors.grey.withValues(alpha: 0.2),
|
||||
),
|
||||
),
|
||||
// Play/Pause
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
if (_isPlaying) {
|
||||
_audioPlayer.pause();
|
||||
} else {
|
||||
_audioPlayer.play();
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
width: 64,
|
||||
height: 64,
|
||||
decoration: BoxDecoration(
|
||||
color: _unsplashPhoto != null
|
||||
? Colors.white
|
||||
: AppColors.primary,
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: (_unsplashPhoto != null
|
||||
? Colors.white
|
||||
: AppColors.primary)
|
||||
.withValues(alpha: 0.3),
|
||||
blurRadius: 16,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: _isBuffering
|
||||
? Padding(
|
||||
padding: const EdgeInsets.all(18.0),
|
||||
child: CircularProgressIndicator(
|
||||
color: _unsplashPhoto != null
|
||||
? Colors.black87
|
||||
: Colors.white,
|
||||
strokeWidth: 3,
|
||||
),
|
||||
)
|
||||
: Icon(
|
||||
_isPlaying
|
||||
? Icons.pause_rounded
|
||||
: Icons.play_arrow_rounded,
|
||||
size: 36,
|
||||
color: _unsplashPhoto != null
|
||||
? Colors.black87
|
||||
: AppColors.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
// Next Surah
|
||||
IconButton(
|
||||
onPressed: (int.tryParse(widget.surahId) ?? 1) < 114
|
||||
? () => _navigateToSurah(1)
|
||||
: null,
|
||||
icon: Icon(
|
||||
Icons.skip_next_rounded,
|
||||
size: 36,
|
||||
color: (int.tryParse(widget.surahId) ?? 1) < 114
|
||||
? (_unsplashPhoto != null ? Colors.white : (isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight))
|
||||
: Colors.grey.withValues(alpha: 0.2),
|
||||
),
|
||||
),
|
||||
// Playlist
|
||||
IconButton(
|
||||
onPressed: _showSurahPlaylist,
|
||||
icon: Icon(
|
||||
Icons.playlist_play_rounded,
|
||||
size: 28,
|
||||
color: _unsplashPhoto != null
|
||||
? Colors.white70
|
||||
: (isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// Qari selector trigger
|
||||
GestureDetector(
|
||||
onTap: _showQariSelector,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: _unsplashPhoto != null
|
||||
? Colors.white.withValues(alpha: 0.15)
|
||||
: AppColors.primary.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.person, size: 16,
|
||||
color: _unsplashPhoto != null ? Colors.white : AppColors.primary),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
EQuranService.qariNames[_selectedQariId] ?? 'Ganti Qari',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: _unsplashPhoto != null ? Colors.white : AppColors.primary,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Icon(Icons.expand_more,
|
||||
size: 16,
|
||||
color: _unsplashPhoto != null ? Colors.white : AppColors.primary),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// === ATTRIBUTION ===
|
||||
if (_unsplashPhoto != null)
|
||||
Positioned(
|
||||
bottom: 280,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
final url = _unsplashPhoto!['photographerUrl'];
|
||||
if (url != null && url.isNotEmpty) {
|
||||
launchUrl(Uri.parse('$url?utm_source=jamshalat_diary&utm_medium=referral'));
|
||||
}
|
||||
},
|
||||
child: Text(
|
||||
'📷 ${_unsplashPhoto!['photographerName']} / Unsplash',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: Colors.white.withValues(alpha: 0.6),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Animated equalizer bar widget for the Murattal player.
|
||||
class _EqualizerBar extends StatefulWidget {
|
||||
final bool isPlaying;
|
||||
final int index;
|
||||
final Color color;
|
||||
|
||||
const _EqualizerBar({
|
||||
required this.isPlaying,
|
||||
required this.index,
|
||||
required this.color,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_EqualizerBar> createState() => _EqualizerBarState();
|
||||
}
|
||||
|
||||
class _EqualizerBarState extends State<_EqualizerBar>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _animation;
|
||||
|
||||
// Each bar has a unique height range and speed for variety
|
||||
static const _barConfigs = [
|
||||
[0.3, 0.9, 600],
|
||||
[0.2, 1.0, 500],
|
||||
[0.4, 0.8, 700],
|
||||
[0.1, 1.0, 450],
|
||||
[0.3, 0.9, 550],
|
||||
[0.2, 0.85, 650],
|
||||
[0.35, 0.95, 480],
|
||||
];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final config = _barConfigs[widget.index % _barConfigs.length];
|
||||
final minHeight = config[0] as double;
|
||||
final maxHeight = config[1] as double;
|
||||
final durationMs = (config[2] as num).toInt();
|
||||
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: Duration(milliseconds: durationMs),
|
||||
);
|
||||
|
||||
_animation = Tween<double>(
|
||||
begin: minHeight,
|
||||
end: maxHeight,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.easeInOut,
|
||||
));
|
||||
|
||||
if (widget.isPlaying) {
|
||||
_controller.repeat(reverse: true);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant _EqualizerBar oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.isPlaying && !oldWidget.isPlaying) {
|
||||
_controller.repeat(reverse: true);
|
||||
} else if (!widget.isPlaying && oldWidget.isPlaying) {
|
||||
_controller.animateTo(0.0, duration: const Duration(milliseconds: 300));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
animation: _animation,
|
||||
builder: (context, child) {
|
||||
return Container(
|
||||
width: 6,
|
||||
height: 50 * _animation.value,
|
||||
decoration: BoxDecoration(
|
||||
color: widget.color.withValues(alpha: 0.6 + (_animation.value * 0.4)),
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
1027
lib/features/quran/presentation/quran_reading_screen.dart
Normal file
1027
lib/features/quran/presentation/quran_reading_screen.dart
Normal file
File diff suppressed because it is too large
Load Diff
260
lib/features/quran/presentation/quran_screen.dart
Normal file
260
lib/features/quran/presentation/quran_screen.dart
Normal file
@@ -0,0 +1,260 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import '../../../app/theme/app_colors.dart';
|
||||
import '../../../data/local/hive_boxes.dart';
|
||||
import '../../../data/local/models/app_settings.dart';
|
||||
import '../../../data/local/models/quran_bookmark.dart';
|
||||
import '../../../data/services/equran_service.dart';
|
||||
|
||||
class QuranScreen extends ConsumerStatefulWidget {
|
||||
const QuranScreen({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<QuranScreen> createState() => _QuranScreenState();
|
||||
}
|
||||
|
||||
class _QuranScreenState extends ConsumerState<QuranScreen> {
|
||||
List<Map<String, dynamic>> _surahs = [];
|
||||
String _searchQuery = '';
|
||||
bool _loading = true;
|
||||
|
||||
bool _showLatin = true;
|
||||
bool _showTerjemahan = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadSurahs();
|
||||
final box = Hive.box<AppSettings>(HiveBoxes.settings);
|
||||
final settings = box.get('default') ?? AppSettings();
|
||||
_showLatin = settings.showLatin;
|
||||
_showTerjemahan = settings.showTerjemahan;
|
||||
}
|
||||
|
||||
Future<void> _loadSurahs() async {
|
||||
final data = await EQuranService.instance.getAllSurahs();
|
||||
setState(() {
|
||||
_surahs = data;
|
||||
_loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
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 isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final filtered = _searchQuery.isEmpty
|
||||
? _surahs
|
||||
: _surahs
|
||||
.where((s) =>
|
||||
(s['namaLatin'] as String? ?? '')
|
||||
.toLowerCase()
|
||||
.contains(_searchQuery.toLowerCase()) ||
|
||||
(s['nama'] as String? ?? '').contains(_searchQuery))
|
||||
.toList();
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Al-Quran'),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.bookmark_outline),
|
||||
onPressed: () => context.push('/tools/quran/bookmarks'),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.settings_display),
|
||||
onPressed: _showDisplaySettings,
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
// Search bar
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 12),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: isDark ? AppColors.surfaceDark : AppColors.surfaceLight,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: isDark
|
||||
? AppColors.primary.withValues(alpha: 0.1)
|
||||
: AppColors.cream,
|
||||
),
|
||||
),
|
||||
child: TextField(
|
||||
onChanged: (v) => setState(() => _searchQuery = v),
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Cari surah...',
|
||||
prefixIcon: Icon(Icons.search,
|
||||
color: isDark
|
||||
? AppColors.textSecondaryDark
|
||||
: AppColors.textSecondaryLight),
|
||||
border: InputBorder.none,
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Surah list
|
||||
Expanded(
|
||||
child: _loading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: filtered.isEmpty
|
||||
? Center(
|
||||
child: Text(
|
||||
_searchQuery.isEmpty
|
||||
? 'Tidak dapat memuat data'
|
||||
: 'Surah tidak ditemukan',
|
||||
style: TextStyle(
|
||||
color: isDark
|
||||
? AppColors.textSecondaryDark
|
||||
: AppColors.textSecondaryLight,
|
||||
),
|
||||
),
|
||||
)
|
||||
: ValueListenableBuilder(
|
||||
valueListenable: Hive.box<QuranBookmark>(HiveBoxes.bookmarks).listenable(),
|
||||
builder: (context, box, _) {
|
||||
return ListView.separated(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
itemCount: filtered.length,
|
||||
separatorBuilder: (_, __) => Divider(
|
||||
height: 1,
|
||||
color: isDark
|
||||
? AppColors.primary.withValues(alpha: 0.08)
|
||||
: AppColors.cream,
|
||||
),
|
||||
itemBuilder: (context, i) {
|
||||
final surah = filtered[i];
|
||||
final number = surah['nomor'] ?? (i + 1);
|
||||
final nameLatin = surah['namaLatin'] ?? '';
|
||||
final nameArabic = surah['nama'] ?? '';
|
||||
final totalVerses = surah['jumlahAyat'] ?? 0;
|
||||
final tempatTurun = surah['tempatTurun'] ?? '';
|
||||
final arti = surah['arti'] ?? '';
|
||||
|
||||
final hasLastRead = box.values.any((b) => b.isLastRead && b.surahId == number);
|
||||
|
||||
return ListTile(
|
||||
onTap: () =>
|
||||
context.push('/tools/quran/$number'),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 0, vertical: 6),
|
||||
leading: Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primary
|
||||
.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'$number',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
title: Row(
|
||||
children: [
|
||||
Text(
|
||||
nameLatin,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
if (hasLastRead) ...[
|
||||
const SizedBox(width: 8),
|
||||
const Icon(Icons.push_pin, size: 14, color: AppColors.primary),
|
||||
],
|
||||
],
|
||||
),
|
||||
subtitle: Text(
|
||||
'$arti • $totalVerses Ayat • $tempatTurun',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: isDark
|
||||
? AppColors.textSecondaryDark
|
||||
: AppColors.textSecondaryLight,
|
||||
),
|
||||
),
|
||||
trailing: Text(
|
||||
nameArabic,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Amiri',
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user