feat: checkpoint API migration and dzikir UX updates
This commit is contained in:
@@ -65,6 +65,18 @@ class AppSettings extends HiveObject {
|
||||
@HiveField(19)
|
||||
bool simpleMode; // false = Mode Lengkap, true = Mode Simpel
|
||||
|
||||
@HiveField(20)
|
||||
String dzikirDisplayMode; // 'list' | 'focus'
|
||||
|
||||
@HiveField(21)
|
||||
String dzikirCounterButtonPosition; // 'bottomPill' | 'fabCircle'
|
||||
|
||||
@HiveField(22)
|
||||
bool dzikirAutoAdvance;
|
||||
|
||||
@HiveField(23)
|
||||
bool dzikirHapticOnCount;
|
||||
|
||||
AppSettings({
|
||||
this.userName = 'User',
|
||||
this.userEmail = '',
|
||||
@@ -86,6 +98,10 @@ class AppSettings extends HiveObject {
|
||||
this.showLatin = true,
|
||||
this.showTerjemahan = true,
|
||||
this.simpleMode = false,
|
||||
this.dzikirDisplayMode = 'list',
|
||||
this.dzikirCounterButtonPosition = 'bottomPill',
|
||||
this.dzikirAutoAdvance = true,
|
||||
this.dzikirHapticOnCount = true,
|
||||
}) : adhanEnabled = adhanEnabled ??
|
||||
{
|
||||
'fajr': true,
|
||||
|
||||
@@ -37,13 +37,17 @@ class AppSettingsAdapter extends TypeAdapter<AppSettings> {
|
||||
showLatin: fields.containsKey(17) ? fields[17] as bool? ?? true : true,
|
||||
showTerjemahan: fields.containsKey(18) ? fields[18] as bool? ?? true : true,
|
||||
simpleMode: fields.containsKey(19) ? fields[19] as bool? ?? false : false,
|
||||
dzikirDisplayMode: fields.containsKey(20) ? fields[20] as String? ?? 'list' : 'list',
|
||||
dzikirCounterButtonPosition: fields.containsKey(21) ? fields[21] as String? ?? 'bottomPill' : 'bottomPill',
|
||||
dzikirAutoAdvance: fields.containsKey(22) ? fields[22] as bool? ?? true : true,
|
||||
dzikirHapticOnCount: fields.containsKey(23) ? fields[23] as bool? ?? true : true,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, AppSettings obj) {
|
||||
writer
|
||||
..writeByte(20)
|
||||
..writeByte(24)
|
||||
..writeByte(0)
|
||||
..write(obj.userName)
|
||||
..writeByte(1)
|
||||
@@ -83,7 +87,15 @@ class AppSettingsAdapter extends TypeAdapter<AppSettings> {
|
||||
..writeByte(18)
|
||||
..write(obj.showTerjemahan)
|
||||
..writeByte(19)
|
||||
..write(obj.simpleMode);
|
||||
..write(obj.simpleMode)
|
||||
..writeByte(20)
|
||||
..write(obj.dzikirDisplayMode)
|
||||
..writeByte(21)
|
||||
..write(obj.dzikirCounterButtonPosition)
|
||||
..writeByte(22)
|
||||
..write(obj.dzikirAutoAdvance)
|
||||
..writeByte(23)
|
||||
..write(obj.dzikirHapticOnCount);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
561
lib/data/services/muslim_api_service.dart
Normal file
561
lib/data/services/muslim_api_service.dart
Normal file
@@ -0,0 +1,561 @@
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
class MuslimApiException implements Exception {
|
||||
final String message;
|
||||
const MuslimApiException(this.message);
|
||||
|
||||
@override
|
||||
String toString() => 'MuslimApiException: $message';
|
||||
}
|
||||
|
||||
/// Service for muslim.backoffice.biz.id API.
|
||||
///
|
||||
/// Exposes Quran, dzikir, doa, hadits, and enrichment data while preserving
|
||||
/// the data contract currently expected by Quran and dashboard UI widgets.
|
||||
class MuslimApiService {
|
||||
static const String _baseUrl = 'https://muslim.backoffice.biz.id';
|
||||
static final MuslimApiService instance = MuslimApiService._();
|
||||
|
||||
MuslimApiService._();
|
||||
|
||||
static const Map<String, String> qariNames = {
|
||||
'01': 'Abdullah Al-Juhany',
|
||||
'02': 'Abdul Muhsin Al-Qasim',
|
||||
'03': 'Abdurrahman As-Sudais',
|
||||
'04': 'Ibrahim Al-Dossari',
|
||||
'05': 'Misyari Rasyid Al-Afasi',
|
||||
'06': 'Yasser Al-Dosari',
|
||||
};
|
||||
|
||||
List<Map<String, dynamic>>? _surahListCache;
|
||||
final Map<int, Map<String, dynamic>> _surahCache = {};
|
||||
|
||||
List<Map<String, dynamic>>? _allAyahCache;
|
||||
List<Map<String, dynamic>>? _tafsirCache;
|
||||
List<Map<String, dynamic>>? _asbabCache;
|
||||
List<Map<String, dynamic>>? _juzCache;
|
||||
List<Map<String, dynamic>>? _themeCache;
|
||||
List<Map<String, dynamic>>? _asmaCache;
|
||||
List<Map<String, dynamic>>? _doaCache;
|
||||
List<Map<String, dynamic>>? _haditsCache;
|
||||
|
||||
final Map<String, List<Map<String, dynamic>>> _dzikirByTypeCache = {};
|
||||
final Map<String, List<Map<String, dynamic>>> _wordByWordCache = {};
|
||||
final Map<int, List<Map<String, dynamic>>> _pageAyahCache = {};
|
||||
|
||||
Future<dynamic> _getData(String path) async {
|
||||
try {
|
||||
final response = await http.get(Uri.parse('$_baseUrl$path'));
|
||||
if (response.statusCode != 200) {
|
||||
return null;
|
||||
}
|
||||
final decoded = json.decode(response.body);
|
||||
if (decoded is Map<String, dynamic>) {
|
||||
return decoded['data'];
|
||||
}
|
||||
return null;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<dynamic> _getDataOrThrow(String path) async {
|
||||
final response = await http.get(Uri.parse('$_baseUrl$path'));
|
||||
if (response.statusCode != 200) {
|
||||
throw MuslimApiException(
|
||||
'Request failed ($path): HTTP ${response.statusCode}',
|
||||
);
|
||||
}
|
||||
|
||||
final decoded = json.decode(response.body);
|
||||
if (decoded is! Map<String, dynamic>) {
|
||||
throw const MuslimApiException('Invalid API payload shape');
|
||||
}
|
||||
|
||||
final status = _asInt(decoded['status']);
|
||||
if (status != 200) {
|
||||
throw MuslimApiException('API returned non-200 status: $status');
|
||||
}
|
||||
|
||||
if (!decoded.containsKey('data')) {
|
||||
throw const MuslimApiException('API payload missing data key');
|
||||
}
|
||||
|
||||
return decoded['data'];
|
||||
}
|
||||
|
||||
int _asInt(dynamic value, {int fallback = 0}) {
|
||||
if (value is int) return value;
|
||||
if (value is num) return value.toInt();
|
||||
if (value is String) return int.tryParse(value) ?? fallback;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
String _asString(dynamic value, {String fallback = ''}) {
|
||||
if (value == null) return fallback;
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
int _asCount(dynamic value, {int fallback = 1}) {
|
||||
if (value == null) return fallback;
|
||||
if (value is int) return value;
|
||||
if (value is num) return value.toInt();
|
||||
final text = value.toString();
|
||||
final match = RegExp(r'\d+').firstMatch(text);
|
||||
if (match == null) return fallback;
|
||||
return int.tryParse(match.group(0)!) ?? fallback;
|
||||
}
|
||||
|
||||
String _stableDzikirId(String type, Map<String, dynamic> item) {
|
||||
final apiId = _asString(item['id']);
|
||||
if (apiId.isNotEmpty) {
|
||||
return '${type}_$apiId';
|
||||
}
|
||||
|
||||
final seed = [
|
||||
type,
|
||||
_asString(item['type']),
|
||||
_asString(item['arab']),
|
||||
_asString(item['indo']),
|
||||
_asString(item['ulang']),
|
||||
].join('|');
|
||||
|
||||
var hash = 0;
|
||||
for (final unit in seed.codeUnits) {
|
||||
hash = ((hash * 31) + unit) & 0x7fffffff;
|
||||
}
|
||||
return '${type}_$hash';
|
||||
}
|
||||
|
||||
String _dzikirApiType(String type) {
|
||||
switch (type) {
|
||||
case 'petang':
|
||||
return 'sore';
|
||||
default:
|
||||
return type;
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, String> _normalizeAudioMap(dynamic audioValue) {
|
||||
final audioUrl = _asString(audioValue);
|
||||
if (audioUrl.isEmpty) return {};
|
||||
return {
|
||||
'01': audioUrl,
|
||||
'02': audioUrl,
|
||||
'03': audioUrl,
|
||||
'04': audioUrl,
|
||||
'05': audioUrl,
|
||||
'06': audioUrl,
|
||||
};
|
||||
}
|
||||
|
||||
Map<String, dynamic> _mapSurahSummary(Map<String, dynamic> item) {
|
||||
final number = _asInt(item['number']);
|
||||
return {
|
||||
'nomor': number,
|
||||
'nama': _asString(item['name_short']),
|
||||
'namaLatin': _asString(item['name_id']),
|
||||
'jumlahAyat': _asInt(item['number_of_verses']),
|
||||
'tempatTurun': _asString(item['revelation_id']),
|
||||
'arti': _asString(item['translation_id']),
|
||||
'deskripsi': _asString(item['tafsir']),
|
||||
'audioFull': _normalizeAudioMap(item['audio_url']),
|
||||
};
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
'juz': _asInt(item['juz']),
|
||||
'page': _asInt(item['page']),
|
||||
'hizb': _asInt(item['hizb']),
|
||||
'theme': _asString(item['theme']),
|
||||
'asbab': _asString(item['asbab']),
|
||||
'notes': _asString(item['notes']),
|
||||
'surah': _asInt(item['surah']),
|
||||
'ayahId': _asInt(item['id']),
|
||||
};
|
||||
}
|
||||
|
||||
Future<List<Map<String, dynamic>>> getAllSurahs() async {
|
||||
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();
|
||||
return _surahListCache!;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>?> getSurah(int number) async {
|
||||
if (_surahCache.containsKey(number)) {
|
||||
return _surahCache[number];
|
||||
}
|
||||
|
||||
final surahs = await getAllSurahs();
|
||||
Map<String, dynamic>? summary;
|
||||
for (final surah in surahs) {
|
||||
if (surah['nomor'] == number) {
|
||||
summary = surah;
|
||||
break;
|
||||
}
|
||||
}
|
||||
final rawAyah = await _getData('/v1/quran/ayah/surah?id=$number');
|
||||
if (summary == null || rawAyah is! List) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final mappedAyah = rawAyah
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(_mapAyah)
|
||||
.toList();
|
||||
|
||||
final mapped = {
|
||||
...summary,
|
||||
'ayat': mappedAyah,
|
||||
};
|
||||
_surahCache[number] = mapped;
|
||||
return mapped;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>?> getDailyAyat() async {
|
||||
try {
|
||||
final now = DateTime.now();
|
||||
final dayOfYear = now.difference(DateTime(now.year, 1, 1)).inDays;
|
||||
final surahId = (dayOfYear % 114) + 1;
|
||||
final surah = await getSurah(surahId);
|
||||
if (surah == null) return null;
|
||||
|
||||
final ayat = List<Map<String, dynamic>>.from(surah['ayat'] ?? []);
|
||||
if (ayat.isEmpty) return null;
|
||||
|
||||
final ayatIndex = dayOfYear % ayat.length;
|
||||
final picked = ayat[ayatIndex];
|
||||
return {
|
||||
'surahName': surah['namaLatin'] ?? '',
|
||||
'nomorSurah': surahId,
|
||||
'nomorAyat': picked['nomorAyat'] ?? 1,
|
||||
'teksArab': picked['teksArab'] ?? '',
|
||||
'teksIndonesia': picked['teksIndonesia'] ?? '',
|
||||
};
|
||||
} 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');
|
||||
if (raw is! List) return [];
|
||||
|
||||
final mapped = raw.whereType<Map<String, dynamic>>().map((item) {
|
||||
return {
|
||||
'word': _asString(item['word']),
|
||||
'arab': _asString(item['arab']),
|
||||
'indo': _asString(item['indo']),
|
||||
};
|
||||
}).toList();
|
||||
|
||||
_wordByWordCache[key] = mapped;
|
||||
return mapped;
|
||||
}
|
||||
|
||||
Future<List<Map<String, dynamic>>> getAllAyah() async {
|
||||
if (_allAyahCache != null) return _allAyahCache!;
|
||||
final raw = await _getData('/v1/quran/ayah');
|
||||
if (raw is! List) return [];
|
||||
|
||||
_allAyahCache = raw.whereType<Map<String, dynamic>>().map((item) {
|
||||
return {
|
||||
'id': _asInt(item['id']),
|
||||
'surah': _asInt(item['surah']),
|
||||
'ayah': _asInt(item['ayah']),
|
||||
'arab': _asString(item['arab']),
|
||||
'latin': _asString(item['latin']),
|
||||
'text': _asString(item['text']),
|
||||
'juz': _asInt(item['juz']),
|
||||
'page': _asInt(item['page']),
|
||||
'hizb': _asInt(item['hizb']),
|
||||
'theme': _asString(item['theme']),
|
||||
'asbab': _asString(item['asbab']),
|
||||
};
|
||||
}).toList();
|
||||
|
||||
return _allAyahCache!;
|
||||
}
|
||||
|
||||
Future<List<Map<String, dynamic>>> getTafsirBySurah(int surahId) async {
|
||||
if (_tafsirCache == null) {
|
||||
final raw = await _getData('/v1/quran/tafsir');
|
||||
if (raw is! List) return [];
|
||||
_tafsirCache = raw.whereType<Map<String, dynamic>>().map((item) {
|
||||
return {
|
||||
'id': _asInt(item['id']),
|
||||
'ayah': _asInt(item['ayah']),
|
||||
'wajiz': _asString(item['wajiz']),
|
||||
'tahlili': _asString(item['tahlili']),
|
||||
};
|
||||
}).toList();
|
||||
}
|
||||
|
||||
final allAyah = await getAllAyah();
|
||||
if (allAyah.isEmpty || _tafsirCache == null) return [];
|
||||
|
||||
final ayahById = <int, Map<String, dynamic>>{};
|
||||
final ayahBySurahAyah = <String, Map<String, dynamic>>{};
|
||||
for (final ayah in allAyah) {
|
||||
final id = _asInt(ayah['id']);
|
||||
final surah = _asInt(ayah['surah']);
|
||||
final ayahNumber = _asInt(ayah['ayah']);
|
||||
ayahById[id] = ayah;
|
||||
ayahBySurahAyah['$surah:$ayahNumber'] = ayah;
|
||||
}
|
||||
|
||||
final result = <Map<String, dynamic>>[];
|
||||
for (final tafsir in _tafsirCache!) {
|
||||
final tafsirId = _asInt(tafsir['id']);
|
||||
final tafsirAyah = _asInt(tafsir['ayah']);
|
||||
Map<String, dynamic>? ayahMeta = ayahById[tafsirId];
|
||||
ayahMeta ??= ayahBySurahAyah['$surahId:$tafsirAyah'];
|
||||
if (ayahMeta == null) continue;
|
||||
if (ayahMeta['surah'] != surahId) continue;
|
||||
result.add({
|
||||
'nomorAyat': _asInt(ayahMeta['ayah'], fallback: tafsirAyah),
|
||||
'wajiz': tafsir['wajiz'],
|
||||
'tahlili': tafsir['tahlili'],
|
||||
});
|
||||
}
|
||||
|
||||
result.sort((a, b) => (a['nomorAyat'] as int).compareTo(b['nomorAyat'] as int));
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<List<Map<String, dynamic>>> getAsbabBySurah(int surahId) async {
|
||||
if (_asbabCache == null) {
|
||||
final raw = await _getData('/v1/quran/asbab');
|
||||
if (raw is! List) return [];
|
||||
_asbabCache = raw.whereType<Map<String, dynamic>>().map((item) {
|
||||
return {
|
||||
'id': _asInt(item['id']),
|
||||
'ayah': _asInt(item['ayah']),
|
||||
'text': _asString(item['text']),
|
||||
};
|
||||
}).toList();
|
||||
}
|
||||
|
||||
final allAyah = await getAllAyah();
|
||||
if (allAyah.isEmpty || _asbabCache == null) return [];
|
||||
|
||||
final ayahById = <int, Map<String, dynamic>>{};
|
||||
final ayahBySurahAyah = <String, Map<String, dynamic>>{};
|
||||
for (final ayah in allAyah) {
|
||||
final id = _asInt(ayah['id']);
|
||||
final surah = _asInt(ayah['surah']);
|
||||
final ayahNumber = _asInt(ayah['ayah']);
|
||||
ayahById[id] = ayah;
|
||||
ayahBySurahAyah['$surah:$ayahNumber'] = ayah;
|
||||
}
|
||||
|
||||
final result = <Map<String, dynamic>>[];
|
||||
for (final asbab in _asbabCache!) {
|
||||
final asbabId = _asInt(asbab['id']);
|
||||
final asbabAyah = _asInt(asbab['ayah']);
|
||||
Map<String, dynamic>? ayahMeta = ayahById[asbabId];
|
||||
ayahMeta ??= ayahBySurahAyah['$surahId:$asbabAyah'];
|
||||
if (ayahMeta == null) continue;
|
||||
if (ayahMeta['surah'] != surahId) continue;
|
||||
result.add({
|
||||
'nomorAyat': _asInt(ayahMeta['ayah'], fallback: asbabAyah),
|
||||
'text': asbab['text'],
|
||||
});
|
||||
}
|
||||
|
||||
result.sort((a, b) => (a['nomorAyat'] as int).compareTo(b['nomorAyat'] as int));
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<List<Map<String, dynamic>>> getJuzList() async {
|
||||
if (_juzCache != null) return _juzCache!;
|
||||
final raw = await _getData('/v1/quran/juz');
|
||||
if (raw is! List) return [];
|
||||
|
||||
_juzCache = raw.whereType<Map<String, dynamic>>().map((item) {
|
||||
return {
|
||||
'number': _asInt(item['number']),
|
||||
'name': _asString(item['name']),
|
||||
'surah_id_start': _asInt(item['surah_id_start']),
|
||||
'verse_start': _asInt(item['verse_start']),
|
||||
'surah_id_end': _asInt(item['surah_id_end']),
|
||||
'verse_end': _asInt(item['verse_end']),
|
||||
'name_start_id': _asString(item['name_start_id']),
|
||||
'name_end_id': _asString(item['name_end_id']),
|
||||
};
|
||||
}).toList();
|
||||
|
||||
return _juzCache!;
|
||||
}
|
||||
|
||||
Future<List<Map<String, dynamic>>> getAyahByPage(int page) async {
|
||||
if (_pageAyahCache.containsKey(page)) return _pageAyahCache[page]!;
|
||||
final raw = await _getData('/v1/quran/ayah/page?id=$page');
|
||||
if (raw is! List) return [];
|
||||
|
||||
final mapped = raw.whereType<Map<String, dynamic>>().map((item) {
|
||||
return {
|
||||
'surah': _asInt(item['surah']),
|
||||
'ayah': _asInt(item['ayah']),
|
||||
'arab': _asString(item['arab']),
|
||||
'text': _asString(item['text']),
|
||||
'theme': _asString(item['theme']),
|
||||
};
|
||||
}).toList();
|
||||
|
||||
_pageAyahCache[page] = mapped;
|
||||
return mapped;
|
||||
}
|
||||
|
||||
Future<List<Map<String, dynamic>>> getThemes() async {
|
||||
if (_themeCache != null) return _themeCache!;
|
||||
final raw = await _getData('/v1/quran/theme');
|
||||
if (raw is! List) return [];
|
||||
|
||||
_themeCache = raw.whereType<Map<String, dynamic>>().map((item) {
|
||||
return {
|
||||
'id': _asInt(item['id']),
|
||||
'name': _asString(item['name']),
|
||||
};
|
||||
}).toList();
|
||||
return _themeCache!;
|
||||
}
|
||||
|
||||
Future<List<Map<String, dynamic>>> searchAyah(String query) async {
|
||||
final q = query.trim().toLowerCase();
|
||||
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();
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
Future<List<Map<String, dynamic>>> getAsmaulHusna() async {
|
||||
if (_asmaCache != null) return _asmaCache!;
|
||||
final raw = await _getData('/v1/quran/asma');
|
||||
if (raw is! List) return [];
|
||||
|
||||
_asmaCache = raw.whereType<Map<String, dynamic>>().map((item) {
|
||||
return {
|
||||
'id': _asInt(item['id']),
|
||||
'arab': _asString(item['arab']),
|
||||
'latin': _asString(item['latin']),
|
||||
'indo': _asString(item['indo']),
|
||||
};
|
||||
}).toList();
|
||||
|
||||
return _asmaCache!;
|
||||
}
|
||||
|
||||
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');
|
||||
if (raw is! List) {
|
||||
if (strict) {
|
||||
throw const MuslimApiException('Invalid doa payload');
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
_doaCache = raw.whereType<Map<String, dynamic>>().map((item) {
|
||||
return {
|
||||
'judul': _asString(item['judul']),
|
||||
'arab': _asString(item['arab']),
|
||||
'indo': _asString(item['indo']),
|
||||
'source': _asString(item['source']),
|
||||
};
|
||||
}).toList();
|
||||
|
||||
return _doaCache!;
|
||||
}
|
||||
|
||||
Future<List<Map<String, dynamic>>> getHaditsList({bool strict = false}) async {
|
||||
if (_haditsCache != null) return _haditsCache!;
|
||||
final raw = strict
|
||||
? await _getDataOrThrow('/v1/hadits')
|
||||
: await _getData('/v1/hadits');
|
||||
if (raw is! List) {
|
||||
if (strict) {
|
||||
throw const MuslimApiException('Invalid hadits payload');
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
_haditsCache = raw.whereType<Map<String, dynamic>>().map((item) {
|
||||
return {
|
||||
'no': _asInt(item['no']),
|
||||
'judul': _asString(item['judul']),
|
||||
'arab': _asString(item['arab']),
|
||||
'indo': _asString(item['indo']),
|
||||
};
|
||||
}).toList();
|
||||
|
||||
return _haditsCache!;
|
||||
}
|
||||
|
||||
Future<List<Map<String, dynamic>>> getDzikirByType(
|
||||
String type, {
|
||||
bool strict = false,
|
||||
}) async {
|
||||
if (_dzikirByTypeCache.containsKey(type)) {
|
||||
return _dzikirByTypeCache[type]!;
|
||||
}
|
||||
final apiType = _dzikirApiType(type);
|
||||
final raw = strict
|
||||
? await _getDataOrThrow('/v1/dzikir?type=$apiType')
|
||||
: await _getData('/v1/dzikir?type=$apiType');
|
||||
if (raw is! List) {
|
||||
if (strict) {
|
||||
throw MuslimApiException('Invalid dzikir payload for type: $type');
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
final mapped = <Map<String, dynamic>>[];
|
||||
for (var i = 0; i < raw.length; i++) {
|
||||
final item = raw[i];
|
||||
if (item is! Map<String, dynamic>) continue;
|
||||
mapped.add({
|
||||
'id': _stableDzikirId(type, item),
|
||||
'arab': _asString(item['arab']),
|
||||
'indo': _asString(item['indo']),
|
||||
'type': _asString(item['type']),
|
||||
'ulang': _asCount(item['ulang'], fallback: 1),
|
||||
});
|
||||
}
|
||||
|
||||
_dzikirByTypeCache[type] = mapped;
|
||||
return mapped;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user