Initial project import and stabilization baseline
This commit is contained in:
111
lib/data/services/myquran_service.dart
Normal file
111
lib/data/services/myquran_service.dart
Normal file
@@ -0,0 +1,111 @@
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
/// Service for myQuran.com v3 Sholat API.
|
||||
/// Provides Kemenag-accurate prayer times for Indonesian cities.
|
||||
///
|
||||
/// Ported directly from the jamshalat-diary project.
|
||||
class MyQuranSholatService {
|
||||
static const String _baseUrl = 'https://api.myquran.com/v3/sholat';
|
||||
static final MyQuranSholatService instance = MyQuranSholatService._();
|
||||
MyQuranSholatService._();
|
||||
|
||||
/// Search for a city/kabupaten by name.
|
||||
/// Returns list of {id, lokasi}.
|
||||
Future<List<Map<String, dynamic>>> searchCity(String query) async {
|
||||
try {
|
||||
final response = await http.get(
|
||||
Uri.parse('$_baseUrl/kota/cari/$query'),
|
||||
);
|
||||
if (response.statusCode == 200) {
|
||||
final data = json.decode(response.body);
|
||||
if (data['status'] == true) {
|
||||
return List<Map<String, dynamic>>.from(data['data']);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// silent fallback — device is offline
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/// Get prayer times for today.
|
||||
/// [cityId] = myQuran city ID (hash string)
|
||||
/// Returns map: {tanggal, imsak, subuh, terbit, dhuha, dzuhur, ashar, maghrib, isya}
|
||||
Future<Map<String, String>?> getDailySchedule(String cityId) async {
|
||||
try {
|
||||
final response = await http.get(Uri.parse('$_baseUrl/jadwal/$cityId/today'));
|
||||
if (response.statusCode == 200) {
|
||||
final data = json.decode(response.body);
|
||||
if (data['status'] == true) {
|
||||
final jadwalMap = data['data']['jadwal'] as Map<String, dynamic>;
|
||||
if (jadwalMap.isNotEmpty) {
|
||||
final firstKey = jadwalMap.keys.first;
|
||||
final jadwal = jadwalMap[firstKey];
|
||||
if (jadwal != null) {
|
||||
final result = Map<String, String>.from(jadwal.map((k, v) => MapEntry(k.toString(), v.toString())));
|
||||
result['date'] = firstKey;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// silent fallback
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/// Get monthly prayer schedule (bulk fetch for offline caching).
|
||||
/// [month] = 'yyyy-MM' format (e.g., '2024-03')
|
||||
/// Returns map of date → jadwal.
|
||||
Future<Map<String, Map<String, String>>> getMonthlySchedule(
|
||||
String cityId, String month) async {
|
||||
try {
|
||||
final response = await http.get(
|
||||
Uri.parse('$_baseUrl/jadwal/$cityId/$month'),
|
||||
);
|
||||
if (response.statusCode == 200) {
|
||||
final data = json.decode(response.body);
|
||||
if (data['status'] == true) {
|
||||
final jadwalMap = data['data']['jadwal'] as Map<String, dynamic>;
|
||||
final result = <String, Map<String, String>>{};
|
||||
for (final entry in jadwalMap.entries) {
|
||||
result[entry.key] = Map<String, String>.from(
|
||||
(entry.value as Map).map(
|
||||
(k, v) => MapEntry(k.toString(), v.toString())),
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// silent fallback
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
/// Get city info (kabko, prov) from a jadwal response.
|
||||
Future<Map<String, String>?> getCityInfo(String cityId) async {
|
||||
final today =
|
||||
DateTime.now().toIso8601String().substring(0, 10);
|
||||
try {
|
||||
final response = await http.get(
|
||||
Uri.parse('$_baseUrl/jadwal/$cityId/$today'),
|
||||
);
|
||||
if (response.statusCode == 200) {
|
||||
final data = json.decode(response.body);
|
||||
if (data['status'] == true) {
|
||||
return {
|
||||
'kabko': data['data']['kabko']?.toString() ?? '',
|
||||
'prov': data['data']['prov']?.toString() ?? '',
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// silent fallback
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
46
lib/data/services/sound_service.dart
Normal file
46
lib/data/services/sound_service.dart
Normal file
@@ -0,0 +1,46 @@
|
||||
import 'package:audioplayers/audioplayers.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
class SoundService {
|
||||
SoundService._();
|
||||
static final instance = SoundService._();
|
||||
|
||||
late AudioPlayer _player;
|
||||
bool _initialized = false;
|
||||
|
||||
void init() {
|
||||
if (_initialized) return;
|
||||
_player = AudioPlayer();
|
||||
// Pre-cache sounds by setting sources but not playing immediately if desired,
|
||||
// though AudioCache is handled implicitly in newer audioplayers.
|
||||
_player.setReleaseMode(ReleaseMode.stop);
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
Future<void> playAdzanBeep() async {
|
||||
try {
|
||||
if (!_initialized) init();
|
||||
// Plays a single beep exactly when Adzan time hits
|
||||
await _player.play(AssetSource('sounds/beep.mp3'));
|
||||
} catch (e) {
|
||||
debugPrint('[SoundService] Error playing adzan beep: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> playIqomahCountdown() async {
|
||||
try {
|
||||
if (!_initialized) init();
|
||||
// Plays the 3-beep countdown for the last 3 seconds of Iqamah
|
||||
await _player.play(AssetSource('sounds/3-detik-countdown.mp3'));
|
||||
} catch (e) {
|
||||
debugPrint('[SoundService] Error playing iqomah countdown: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
if (_initialized) {
|
||||
_player.dispose();
|
||||
_initialized = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
93
lib/data/services/sync_service.dart
Normal file
93
lib/data/services/sync_service.dart
Normal file
@@ -0,0 +1,93 @@
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import '../local/models.dart';
|
||||
import 'myquran_service.dart';
|
||||
|
||||
/// Service to sync monthly prayer data from MyQuran API → Hive.
|
||||
class SyncService {
|
||||
SyncService._();
|
||||
static final SyncService instance = SyncService._();
|
||||
|
||||
/// Sync current month + next month prayer data for the configured city.
|
||||
/// Returns true on success.
|
||||
Future<bool> syncMonthlyData() async {
|
||||
final settingsBox = Hive.box<AppSettings>(HiveBoxes.settings);
|
||||
final settings = settingsBox.get('default');
|
||||
if (settings == null) return false;
|
||||
|
||||
final cityId = settings.cityIdApi;
|
||||
final now = DateTime.now();
|
||||
final currentMonth = DateFormat('yyyy-MM').format(now);
|
||||
|
||||
// Also fetch next month for continuity
|
||||
final nextMonthDate = DateTime(now.year, now.month + 1, 1);
|
||||
final nextMonth = DateFormat('yyyy-MM').format(nextMonthDate);
|
||||
|
||||
final api = MyQuranSholatService.instance;
|
||||
final scheduleBox = Hive.box<DailyPrayerSchedule>(HiveBoxes.prayerSchedule);
|
||||
|
||||
var success = false;
|
||||
|
||||
// Fetch current month
|
||||
final currentData = await api.getMonthlySchedule(cityId, currentMonth);
|
||||
if (currentData.isNotEmpty) {
|
||||
for (final entry in currentData.entries) {
|
||||
final jadwal = entry.value;
|
||||
scheduleBox.put(
|
||||
entry.key,
|
||||
DailyPrayerSchedule(
|
||||
date: entry.key,
|
||||
imsak: jadwal['imsak'] ?? '00:00',
|
||||
subuh: jadwal['subuh'] ?? '00:00',
|
||||
terbit: jadwal['terbit'] ?? '00:00',
|
||||
dhuha: jadwal['dhuha'] ?? '00:00',
|
||||
dzuhur: jadwal['dzuhur'] ?? '00:00',
|
||||
ashar: jadwal['ashar'] ?? '00:00',
|
||||
maghrib: jadwal['maghrib'] ?? '00:00',
|
||||
isya: jadwal['isya'] ?? '00:00',
|
||||
),
|
||||
);
|
||||
}
|
||||
success = true;
|
||||
}
|
||||
|
||||
// Fetch next month
|
||||
final nextData = await api.getMonthlySchedule(cityId, nextMonth);
|
||||
if (nextData.isNotEmpty) {
|
||||
for (final entry in nextData.entries) {
|
||||
final jadwal = entry.value;
|
||||
scheduleBox.put(
|
||||
entry.key,
|
||||
DailyPrayerSchedule(
|
||||
date: entry.key,
|
||||
imsak: jadwal['imsak'] ?? '00:00',
|
||||
subuh: jadwal['subuh'] ?? '00:00',
|
||||
terbit: jadwal['terbit'] ?? '00:00',
|
||||
dhuha: jadwal['dhuha'] ?? '00:00',
|
||||
dzuhur: jadwal['dzuhur'] ?? '00:00',
|
||||
ashar: jadwal['ashar'] ?? '00:00',
|
||||
maghrib: jadwal['maghrib'] ?? '00:00',
|
||||
isya: jadwal['isya'] ?? '00:00',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (success) {
|
||||
settings.lastSyncDate = DateFormat('yyyy-MM-dd HH:mm').format(now);
|
||||
await settings.save();
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
/// Get today's prayer schedule from local Hive cache.
|
||||
DailyPrayerSchedule? getTodaySchedule([DateTime? targetDate]) {
|
||||
final scheduleBox =
|
||||
Hive.box<DailyPrayerSchedule>(HiveBoxes.prayerSchedule);
|
||||
final dateToFetch = targetDate ?? DateTime.now();
|
||||
final dateStr = DateFormat('yyyy-MM-dd').format(dateToFetch);
|
||||
return scheduleBox.get(dateStr);
|
||||
}
|
||||
}
|
||||
48
lib/data/services/unsplash_service.dart
Normal file
48
lib/data/services/unsplash_service.dart
Normal file
@@ -0,0 +1,48 @@
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
/// Service for fetching background portraits from the Unsplash API.
|
||||
class UnsplashService {
|
||||
static const String _clientId = 'BkgEMpfG_ReNpVwJcbgNx30IZXhoFoWwKgwbrPU0hq4';
|
||||
static const String _baseUrl = 'https://api.unsplash.com';
|
||||
|
||||
static final UnsplashService instance = UnsplashService._();
|
||||
UnsplashService._();
|
||||
|
||||
/// Fetches a list of highly compressed landscape URLs based on the given keyword.
|
||||
Future<List<String>> fetchLandscapeBackgrounds(String keyword) async {
|
||||
// Trim keyword and default to 'mosque' if empty
|
||||
final query = keyword.trim().isEmpty ? 'mosque' : keyword.trim();
|
||||
|
||||
// Specifically requesting 'regular' size to fit 1080p elegantly while minimizing RAM overhead.
|
||||
final url = Uri.parse('$_baseUrl/search/photos?query=$query&orientation=landscape&per_page=20');
|
||||
|
||||
try {
|
||||
final response = await http.get(
|
||||
url,
|
||||
headers: {
|
||||
'Authorization': 'Client-ID $_clientId',
|
||||
'Accept-Version': 'v1',
|
||||
},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = json.decode(response.body);
|
||||
final results = data['results'] as List<dynamic>? ?? [];
|
||||
|
||||
final urls = <String>[];
|
||||
for (final item in results) {
|
||||
final urlsMap = item['urls'] as Map<String, dynamic>?;
|
||||
if (urlsMap != null && urlsMap.containsKey('regular')) {
|
||||
urls.add(urlsMap['regular'].toString());
|
||||
}
|
||||
}
|
||||
return urls;
|
||||
}
|
||||
} catch (e) {
|
||||
// Offline or error — fail silently.
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user