Polish navigation, Quran flows, and sharing UX
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
class MuslimApiException implements Exception {
|
||||
@@ -138,7 +139,7 @@ class MuslimApiService {
|
||||
}
|
||||
|
||||
Map<String, String> _normalizeAudioMap(dynamic audioValue) {
|
||||
final audioUrl = _asString(audioValue);
|
||||
final audioUrl = _extractAudioUrl(audioValue);
|
||||
if (audioUrl.isEmpty) return {};
|
||||
return {
|
||||
'01': audioUrl,
|
||||
@@ -150,6 +151,59 @@ class MuslimApiService {
|
||||
};
|
||||
}
|
||||
|
||||
String _extractAudioUrl(dynamic value) {
|
||||
if (value == null) return '';
|
||||
if (value is String) return value.trim();
|
||||
if (value is Map) {
|
||||
final direct = _asString(value['url']).trim();
|
||||
if (direct.isNotEmpty) return direct;
|
||||
final src = _asString(value['src']).trim();
|
||||
if (src.isNotEmpty) return src;
|
||||
final audio = _asString(value['audio']).trim();
|
||||
if (audio.isNotEmpty) return audio;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
String _normalizeQariKey(dynamic rawKey) {
|
||||
if (rawKey == null) return '';
|
||||
if (rawKey is int) return rawKey.toString().padLeft(2, '0');
|
||||
if (rawKey is num) return rawKey.toInt().toString().padLeft(2, '0');
|
||||
|
||||
final text = rawKey.toString().trim();
|
||||
if (text.isEmpty) return '';
|
||||
|
||||
final digits = text.replaceAll(RegExp(r'[^0-9]'), '');
|
||||
if (digits.isNotEmpty) {
|
||||
final parsed = int.tryParse(digits);
|
||||
if (parsed != null) return parsed.toString().padLeft(2, '0');
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
Map<String, String> _normalizeAyahAudioMap(dynamic audioValue) {
|
||||
if (audioValue is Map) {
|
||||
final normalized = <String, String>{};
|
||||
audioValue.forEach((rawKey, rawValue) {
|
||||
final key = _normalizeQariKey(rawKey);
|
||||
final url = _extractAudioUrl(rawValue);
|
||||
if (key.isNotEmpty && url.isNotEmpty) {
|
||||
normalized[key] = url;
|
||||
}
|
||||
});
|
||||
|
||||
if (normalized.isNotEmpty) {
|
||||
final fallbackUrl = normalized.values.first;
|
||||
for (final qariId in qariNames.keys) {
|
||||
normalized.putIfAbsent(qariId, () => fallbackUrl);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
|
||||
return _normalizeAudioMap(audioValue);
|
||||
}
|
||||
|
||||
Map<String, dynamic> _mapSurahSummary(Map<String, dynamic> item) {
|
||||
final number = _asInt(item['number']);
|
||||
return {
|
||||
@@ -165,20 +219,12 @@ class MuslimApiService {
|
||||
}
|
||||
|
||||
Map<String, dynamic> _mapAyah(Map<String, dynamic> item) {
|
||||
final audio = _asString(item['audio']);
|
||||
return {
|
||||
'nomorAyat': _asInt(item['ayah']),
|
||||
'teksArab': _asString(item['arab']),
|
||||
'teksLatin': _asString(item['latin']),
|
||||
'teksIndonesia': _asString(item['text']),
|
||||
'audio': {
|
||||
'01': audio,
|
||||
'02': audio,
|
||||
'03': audio,
|
||||
'04': audio,
|
||||
'05': audio,
|
||||
'06': audio,
|
||||
},
|
||||
'audio': _normalizeAyahAudioMap(item['audio'] ?? item['audio_url']),
|
||||
'juz': _asInt(item['juz']),
|
||||
'page': _asInt(item['page']),
|
||||
'hizb': _asInt(item['hizb']),
|
||||
@@ -194,10 +240,8 @@ class MuslimApiService {
|
||||
if (_surahListCache != null) return _surahListCache!;
|
||||
final raw = await _getData('/v1/quran/surah');
|
||||
if (raw is! List) return [];
|
||||
_surahListCache = raw
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(_mapSurahSummary)
|
||||
.toList();
|
||||
_surahListCache =
|
||||
raw.whereType<Map<String, dynamic>>().map(_mapSurahSummary).toList();
|
||||
return _surahListCache!;
|
||||
}
|
||||
|
||||
@@ -219,10 +263,8 @@ class MuslimApiService {
|
||||
return null;
|
||||
}
|
||||
|
||||
final mappedAyah = rawAyah
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(_mapAyah)
|
||||
.toList();
|
||||
final mappedAyah =
|
||||
rawAyah.whereType<Map<String, dynamic>>().map(_mapAyah).toList();
|
||||
|
||||
final mapped = {
|
||||
...summary,
|
||||
@@ -257,11 +299,58 @@ class MuslimApiService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<Map<String, dynamic>>> getWordByWord(int surahId, int ayahId) async {
|
||||
Future<Map<String, dynamic>?> getRandomAyat({
|
||||
int? excludeSurahNumber,
|
||||
int? excludeAyahNumber,
|
||||
}) async {
|
||||
try {
|
||||
final allAyah = await getAllAyah();
|
||||
if (allAyah.isEmpty) return null;
|
||||
|
||||
final surahs = await getAllSurahs();
|
||||
if (surahs.isEmpty) return null;
|
||||
|
||||
final surahNames = <int, String>{
|
||||
for (final surah in surahs)
|
||||
_asInt(surah['nomor']): _asString(surah['namaLatin']),
|
||||
};
|
||||
|
||||
final filtered = allAyah.where((ayah) {
|
||||
final surahNumber = _asInt(ayah['surah']);
|
||||
final ayahNumber = _asInt(ayah['ayah']);
|
||||
final isExcluded = excludeSurahNumber != null &&
|
||||
excludeAyahNumber != null &&
|
||||
surahNumber == excludeSurahNumber &&
|
||||
ayahNumber == excludeAyahNumber;
|
||||
if (isExcluded) return false;
|
||||
|
||||
return _asString(ayah['arab']).trim().isNotEmpty &&
|
||||
_asString(ayah['text']).trim().isNotEmpty;
|
||||
}).toList();
|
||||
|
||||
final candidates = filtered.isNotEmpty ? filtered : allAyah;
|
||||
final picked = candidates[Random().nextInt(candidates.length)];
|
||||
final surahNumber = _asInt(picked['surah']);
|
||||
|
||||
return {
|
||||
'surahName': surahNames[surahNumber] ?? '',
|
||||
'nomorSurah': surahNumber,
|
||||
'nomorAyat': _asInt(picked['ayah'], fallback: 1),
|
||||
'teksArab': _asString(picked['arab']),
|
||||
'teksIndonesia': _asString(picked['text']),
|
||||
};
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<Map<String, dynamic>>> getWordByWord(
|
||||
int surahId, int ayahId) async {
|
||||
final key = '$surahId:$ayahId';
|
||||
if (_wordByWordCache.containsKey(key)) return _wordByWordCache[key]!;
|
||||
|
||||
final raw = await _getData('/v1/quran/word/ayah?surahId=$surahId&ayahId=$ayahId');
|
||||
final raw =
|
||||
await _getData('/v1/quran/word/ayah?surahId=$surahId&ayahId=$ayahId');
|
||||
if (raw is! List) return [];
|
||||
|
||||
final mapped = raw.whereType<Map<String, dynamic>>().map((item) {
|
||||
@@ -342,7 +431,8 @@ class MuslimApiService {
|
||||
});
|
||||
}
|
||||
|
||||
result.sort((a, b) => (a['nomorAyat'] as int).compareTo(b['nomorAyat'] as int));
|
||||
result.sort(
|
||||
(a, b) => (a['nomorAyat'] as int).compareTo(b['nomorAyat'] as int));
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -386,7 +476,8 @@ class MuslimApiService {
|
||||
});
|
||||
}
|
||||
|
||||
result.sort((a, b) => (a['nomorAyat'] as int).compareTo(b['nomorAyat'] as int));
|
||||
result.sort(
|
||||
(a, b) => (a['nomorAyat'] as int).compareTo(b['nomorAyat'] as int));
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -449,12 +540,17 @@ class MuslimApiService {
|
||||
if (q.isEmpty) return [];
|
||||
|
||||
final allAyah = await getAllAyah();
|
||||
final results = allAyah.where((item) {
|
||||
final text = _asString(item['text']).toLowerCase();
|
||||
final latin = _asString(item['latin']).toLowerCase();
|
||||
final arab = _asString(item['arab']);
|
||||
return text.contains(q) || latin.contains(q) || arab.contains(query.trim());
|
||||
}).take(50).toList();
|
||||
final results = allAyah
|
||||
.where((item) {
|
||||
final text = _asString(item['text']).toLowerCase();
|
||||
final latin = _asString(item['latin']).toLowerCase();
|
||||
final arab = _asString(item['arab']);
|
||||
return text.contains(q) ||
|
||||
latin.contains(q) ||
|
||||
arab.contains(query.trim());
|
||||
})
|
||||
.take(50)
|
||||
.toList();
|
||||
|
||||
return results;
|
||||
}
|
||||
@@ -478,9 +574,8 @@ class MuslimApiService {
|
||||
|
||||
Future<List<Map<String, dynamic>>> getDoaList({bool strict = false}) async {
|
||||
if (_doaCache != null) return _doaCache!;
|
||||
final raw = strict
|
||||
? await _getDataOrThrow('/v1/doa')
|
||||
: await _getData('/v1/doa');
|
||||
final raw =
|
||||
strict ? await _getDataOrThrow('/v1/doa') : await _getData('/v1/doa');
|
||||
if (raw is! List) {
|
||||
if (strict) {
|
||||
throw const MuslimApiException('Invalid doa payload');
|
||||
@@ -500,7 +595,8 @@ class MuslimApiService {
|
||||
return _doaCache!;
|
||||
}
|
||||
|
||||
Future<List<Map<String, dynamic>>> getHaditsList({bool strict = false}) async {
|
||||
Future<List<Map<String, dynamic>>> getHaditsList(
|
||||
{bool strict = false}) async {
|
||||
if (_haditsCache != null) return _haditsCache!;
|
||||
final raw = strict
|
||||
? await _getDataOrThrow('/v1/hadits')
|
||||
|
||||
Reference in New Issue
Block a user