Initial project import and stabilization baseline
This commit is contained in:
420
lib/data/local/models.dart
Normal file
420
lib/data/local/models.dart
Normal file
@@ -0,0 +1,420 @@
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
|
||||
/// Hive type adapter IDs and box names.
|
||||
class HiveBoxes {
|
||||
HiveBoxes._();
|
||||
static const String settings = 'app_settings';
|
||||
static const String prayerSchedule = 'prayer_schedule';
|
||||
}
|
||||
|
||||
/// AppSettings stored in Hive.
|
||||
@HiveType(typeId: 0)
|
||||
class AppSettings extends HiveObject {
|
||||
@HiveField(0)
|
||||
String masjidName;
|
||||
|
||||
@HiveField(1)
|
||||
String masjidAddress;
|
||||
|
||||
@HiveField(2)
|
||||
String cityIdApi; // myQuran city hash ID
|
||||
|
||||
@HiveField(3)
|
||||
String cityDisplayName;
|
||||
|
||||
@HiveField(4)
|
||||
bool showImsak;
|
||||
|
||||
@HiveField(5)
|
||||
bool showTerbit;
|
||||
|
||||
// Iqomah durations in minutes
|
||||
@HiveField(6)
|
||||
int iqomahSubuh;
|
||||
|
||||
@HiveField(7)
|
||||
int iqomahDzuhur;
|
||||
|
||||
@HiveField(8)
|
||||
int iqomahAshar;
|
||||
|
||||
@HiveField(9)
|
||||
int iqomahMaghrib;
|
||||
|
||||
@HiveField(10)
|
||||
int iqomahIsya;
|
||||
|
||||
// Pre-Adzan lead time (minutes before adzan to lock main screen)
|
||||
@HiveField(11)
|
||||
int preAdzanLead;
|
||||
|
||||
// Blank screen durations
|
||||
@HiveField(12)
|
||||
int blankScreenNormal; // minutes
|
||||
|
||||
@HiveField(13)
|
||||
int blankScreenJumat; // minutes
|
||||
|
||||
// Running text items
|
||||
@HiveField(14)
|
||||
List<String> runningTexts;
|
||||
|
||||
// Friday officers
|
||||
@HiveField(15)
|
||||
String khatibName;
|
||||
|
||||
@HiveField(16)
|
||||
String imamName;
|
||||
|
||||
// Rotation settings
|
||||
@HiveField(17)
|
||||
int mainScreenDurationSec;
|
||||
|
||||
@HiveField(18)
|
||||
int slideDurationSec;
|
||||
|
||||
// Last sync timestamp
|
||||
@HiveField(19)
|
||||
String? lastSyncDate;
|
||||
|
||||
// Slideshow image paths (local)
|
||||
@HiveField(20)
|
||||
List<String> slideshowImages;
|
||||
|
||||
// Text scaling (0=Small, 1=Medium, 2=Large)
|
||||
@HiveField(21)
|
||||
int textScaleIndex;
|
||||
|
||||
// Unsplash Background configs
|
||||
@HiveField(22)
|
||||
bool useUnsplashBackground;
|
||||
|
||||
@HiveField(23)
|
||||
String unsplashKeyword;
|
||||
|
||||
@HiveField(24)
|
||||
int unsplashRotationHours;
|
||||
|
||||
// Branded background image (local file path set by admin)
|
||||
@HiveField(25)
|
||||
String? brandedBgImage;
|
||||
|
||||
// Per-item duration for running texts (seconds each)
|
||||
@HiveField(26)
|
||||
List<int> runningTextDurations;
|
||||
|
||||
// Running text animation type: 'marquee' or 'fade'
|
||||
@HiveField(27)
|
||||
String marqueeAnimType;
|
||||
|
||||
// Granular text group scales (independent of textScaleIndex)
|
||||
// Group: Prayer card label (e.g. "SUBUH", "DZUHUR")
|
||||
@HiveField(28)
|
||||
double scaleCardLabel;
|
||||
|
||||
// Group: Prayer card body (time + iqomah text)
|
||||
@HiveField(29)
|
||||
double scaleCardBody;
|
||||
|
||||
// Group: Running text ticker at bottom
|
||||
@HiveField(30)
|
||||
double scaleRunningText;
|
||||
|
||||
AppSettings({
|
||||
this.masjidName = 'Masjid Al-Ikhlas',
|
||||
this.masjidAddress = 'Jl. Kebaikan No. 1',
|
||||
this.cityIdApi = '1218', // Default: Yogyakarta
|
||||
this.cityDisplayName = 'Kota Yogyakarta',
|
||||
this.showImsak = true,
|
||||
this.showTerbit = true,
|
||||
this.iqomahSubuh = 15,
|
||||
this.iqomahDzuhur = 10,
|
||||
this.iqomahAshar = 10,
|
||||
this.iqomahMaghrib = 7,
|
||||
this.iqomahIsya = 10,
|
||||
this.preAdzanLead = 10,
|
||||
this.blankScreenNormal = 15,
|
||||
this.blankScreenJumat = 45,
|
||||
this.runningTexts = const [
|
||||
'Mohon luruskan dan rapatkan shaf',
|
||||
'Kajian rutin setiap Ahad pagi',
|
||||
],
|
||||
this.khatibName = 'Ust. Fulan, S.Ag',
|
||||
this.imamName = 'Ust. Alan, Lc',
|
||||
this.mainScreenDurationSec = 15,
|
||||
this.slideDurationSec = 10,
|
||||
this.lastSyncDate,
|
||||
this.slideshowImages = const [],
|
||||
this.textScaleIndex = 1,
|
||||
this.useUnsplashBackground = false,
|
||||
this.unsplashKeyword = 'mosque',
|
||||
this.unsplashRotationHours = 6,
|
||||
this.brandedBgImage,
|
||||
this.runningTextDurations = const [],
|
||||
this.marqueeAnimType = 'marquee',
|
||||
this.scaleCardLabel = 1.0,
|
||||
this.scaleCardBody = 1.0,
|
||||
this.scaleRunningText = 1.0,
|
||||
});
|
||||
|
||||
AppSettings copyWith({
|
||||
String? masjidName,
|
||||
String? masjidAddress,
|
||||
String? cityIdApi,
|
||||
String? cityDisplayName,
|
||||
bool? showImsak,
|
||||
bool? showTerbit,
|
||||
int? iqomahSubuh,
|
||||
int? iqomahDzuhur,
|
||||
int? iqomahAshar,
|
||||
int? iqomahMaghrib,
|
||||
int? iqomahIsya,
|
||||
int? preAdzanLead,
|
||||
int? blankScreenNormal,
|
||||
int? blankScreenJumat,
|
||||
List<String>? runningTexts,
|
||||
String? khatibName,
|
||||
String? imamName,
|
||||
int? mainScreenDurationSec,
|
||||
int? slideDurationSec,
|
||||
String? lastSyncDate,
|
||||
List<String>? slideshowImages,
|
||||
int? textScaleIndex,
|
||||
bool? useUnsplashBackground,
|
||||
String? unsplashKeyword,
|
||||
int? unsplashRotationHours,
|
||||
String? brandedBgImage,
|
||||
List<int>? runningTextDurations,
|
||||
String? marqueeAnimType,
|
||||
double? scaleCardLabel,
|
||||
double? scaleCardBody,
|
||||
double? scaleRunningText,
|
||||
}) {
|
||||
return AppSettings(
|
||||
masjidName: masjidName ?? this.masjidName,
|
||||
masjidAddress: masjidAddress ?? this.masjidAddress,
|
||||
cityIdApi: cityIdApi ?? this.cityIdApi,
|
||||
cityDisplayName: cityDisplayName ?? this.cityDisplayName,
|
||||
showImsak: showImsak ?? this.showImsak,
|
||||
showTerbit: showTerbit ?? this.showTerbit,
|
||||
iqomahSubuh: iqomahSubuh ?? this.iqomahSubuh,
|
||||
iqomahDzuhur: iqomahDzuhur ?? this.iqomahDzuhur,
|
||||
iqomahAshar: iqomahAshar ?? this.iqomahAshar,
|
||||
iqomahMaghrib: iqomahMaghrib ?? this.iqomahMaghrib,
|
||||
iqomahIsya: iqomahIsya ?? this.iqomahIsya,
|
||||
preAdzanLead: preAdzanLead ?? this.preAdzanLead,
|
||||
blankScreenNormal: blankScreenNormal ?? this.blankScreenNormal,
|
||||
blankScreenJumat: blankScreenJumat ?? this.blankScreenJumat,
|
||||
runningTexts: runningTexts ?? this.runningTexts,
|
||||
khatibName: khatibName ?? this.khatibName,
|
||||
imamName: imamName ?? this.imamName,
|
||||
mainScreenDurationSec: mainScreenDurationSec ?? this.mainScreenDurationSec,
|
||||
slideDurationSec: slideDurationSec ?? this.slideDurationSec,
|
||||
lastSyncDate: lastSyncDate ?? this.lastSyncDate,
|
||||
slideshowImages: slideshowImages ?? this.slideshowImages,
|
||||
textScaleIndex: textScaleIndex ?? this.textScaleIndex,
|
||||
useUnsplashBackground: useUnsplashBackground ?? this.useUnsplashBackground,
|
||||
unsplashKeyword: unsplashKeyword ?? this.unsplashKeyword,
|
||||
unsplashRotationHours: unsplashRotationHours ?? this.unsplashRotationHours,
|
||||
brandedBgImage: brandedBgImage ?? this.brandedBgImage,
|
||||
runningTextDurations: runningTextDurations ?? this.runningTextDurations,
|
||||
marqueeAnimType: marqueeAnimType ?? this.marqueeAnimType,
|
||||
scaleCardLabel: scaleCardLabel ?? this.scaleCardLabel,
|
||||
scaleCardBody: scaleCardBody ?? this.scaleCardBody,
|
||||
scaleRunningText: scaleRunningText ?? this.scaleRunningText,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Adapter for AppSettings — hand-written to avoid code generation.
|
||||
class AppSettingsAdapter extends TypeAdapter<AppSettings> {
|
||||
@override
|
||||
final int typeId = 0;
|
||||
|
||||
@override
|
||||
AppSettings read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{};
|
||||
for (int i = 0; i < numOfFields; i++) {
|
||||
fields[reader.readByte()] = reader.read();
|
||||
}
|
||||
return AppSettings(
|
||||
masjidName: fields[0] as String? ?? 'Masjid Al-Ikhlas',
|
||||
masjidAddress: fields[1] as String? ?? 'Jl. Kebaikan No. 1',
|
||||
cityIdApi: fields[2] as String? ?? '1218',
|
||||
cityDisplayName: fields[3] as String? ?? 'Kota Yogyakarta',
|
||||
showImsak: fields[4] as bool? ?? true,
|
||||
showTerbit: fields[5] as bool? ?? true,
|
||||
iqomahSubuh: fields[6] as int? ?? 15,
|
||||
iqomahDzuhur: fields[7] as int? ?? 10,
|
||||
iqomahAshar: fields[8] as int? ?? 10,
|
||||
iqomahMaghrib: fields[9] as int? ?? 7,
|
||||
iqomahIsya: fields[10] as int? ?? 10,
|
||||
preAdzanLead: fields[11] as int? ?? 10,
|
||||
blankScreenNormal: fields[12] as int? ?? 15,
|
||||
blankScreenJumat: fields[13] as int? ?? 45,
|
||||
runningTexts: (fields[14] as List?)?.cast<String>() ?? const [],
|
||||
khatibName: fields[15] as String? ?? '',
|
||||
imamName: fields[16] as String? ?? '',
|
||||
mainScreenDurationSec: fields[17] as int? ?? 15,
|
||||
slideDurationSec: fields[18] as int? ?? 10,
|
||||
lastSyncDate: fields[19] as String?,
|
||||
slideshowImages: (fields[20] as List?)?.cast<String>() ?? const [],
|
||||
textScaleIndex: fields[21] as int? ?? 1,
|
||||
useUnsplashBackground: fields[22] as bool? ?? false,
|
||||
unsplashKeyword: fields[23] as String? ?? 'mosque',
|
||||
unsplashRotationHours: fields[24] as int? ?? 6,
|
||||
brandedBgImage: fields[25] as String?,
|
||||
runningTextDurations: (fields[26] as List?)?.cast<int>() ?? const [],
|
||||
marqueeAnimType: fields[27] as String? ?? 'marquee',
|
||||
scaleCardLabel: (fields[28] as num?)?.toDouble() ?? 1.0,
|
||||
scaleCardBody: (fields[29] as num?)?.toDouble() ?? 1.0,
|
||||
scaleRunningText: (fields[30] as num?)?.toDouble() ?? 1.0,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, AppSettings obj) {
|
||||
writer
|
||||
..writeByte(31)
|
||||
..writeByte(0)..write(obj.masjidName)
|
||||
..writeByte(1)..write(obj.masjidAddress)
|
||||
..writeByte(2)..write(obj.cityIdApi)
|
||||
..writeByte(3)..write(obj.cityDisplayName)
|
||||
..writeByte(4)..write(obj.showImsak)
|
||||
..writeByte(5)..write(obj.showTerbit)
|
||||
..writeByte(6)..write(obj.iqomahSubuh)
|
||||
..writeByte(7)..write(obj.iqomahDzuhur)
|
||||
..writeByte(8)..write(obj.iqomahAshar)
|
||||
..writeByte(9)..write(obj.iqomahMaghrib)
|
||||
..writeByte(10)..write(obj.iqomahIsya)
|
||||
..writeByte(11)..write(obj.preAdzanLead)
|
||||
..writeByte(12)..write(obj.blankScreenNormal)
|
||||
..writeByte(13)..write(obj.blankScreenJumat)
|
||||
..writeByte(14)..write(obj.runningTexts)
|
||||
..writeByte(15)..write(obj.khatibName)
|
||||
..writeByte(16)..write(obj.imamName)
|
||||
..writeByte(17)..write(obj.mainScreenDurationSec)
|
||||
..writeByte(18)..write(obj.slideDurationSec)
|
||||
..writeByte(19)..write(obj.lastSyncDate)
|
||||
..writeByte(20)..write(obj.slideshowImages)
|
||||
..writeByte(21)..write(obj.textScaleIndex)
|
||||
..writeByte(22)..write(obj.useUnsplashBackground)
|
||||
..writeByte(23)..write(obj.unsplashKeyword)
|
||||
..writeByte(24)..write(obj.unsplashRotationHours)
|
||||
..writeByte(25)..write(obj.brandedBgImage)
|
||||
..writeByte(26)..write(obj.runningTextDurations)
|
||||
..writeByte(27)..write(obj.marqueeAnimType)
|
||||
..writeByte(28)..write(obj.scaleCardLabel)
|
||||
..writeByte(29)..write(obj.scaleCardBody)
|
||||
..writeByte(30)..write(obj.scaleRunningText);
|
||||
}
|
||||
}
|
||||
|
||||
/// Daily prayer schedule row cached from the MyQuran API.
|
||||
@HiveType(typeId: 1)
|
||||
class DailyPrayerSchedule extends HiveObject {
|
||||
@HiveField(0)
|
||||
String date; // yyyy-MM-dd
|
||||
|
||||
@HiveField(1)
|
||||
String imsak;
|
||||
|
||||
@HiveField(2)
|
||||
String subuh;
|
||||
|
||||
@HiveField(3)
|
||||
String terbit;
|
||||
|
||||
@HiveField(4)
|
||||
String dhuha;
|
||||
|
||||
@HiveField(5)
|
||||
String dzuhur;
|
||||
|
||||
@HiveField(6)
|
||||
String ashar;
|
||||
|
||||
@HiveField(7)
|
||||
String maghrib;
|
||||
|
||||
@HiveField(8)
|
||||
String isya;
|
||||
|
||||
DailyPrayerSchedule({
|
||||
required this.date,
|
||||
required this.imsak,
|
||||
required this.subuh,
|
||||
required this.terbit,
|
||||
required this.dhuha,
|
||||
required this.dzuhur,
|
||||
required this.ashar,
|
||||
required this.maghrib,
|
||||
required this.isya,
|
||||
});
|
||||
|
||||
/// Parse time string "HH:mm" to a DateTime on the given date.
|
||||
DateTime timeToDateTime(String time, DateTime refDate) {
|
||||
final parts = time.split(':');
|
||||
return DateTime(
|
||||
refDate.year,
|
||||
refDate.month,
|
||||
refDate.day,
|
||||
int.parse(parts[0]),
|
||||
int.parse(parts[1]),
|
||||
);
|
||||
}
|
||||
|
||||
/// Get all prayer times as DateTime map for a given reference date.
|
||||
Map<String, DateTime> toDateTimeMap(DateTime refDate) => {
|
||||
'imsak': timeToDateTime(imsak, refDate),
|
||||
'subuh': timeToDateTime(subuh, refDate),
|
||||
'terbit': timeToDateTime(terbit, refDate),
|
||||
'dhuha': timeToDateTime(dhuha, refDate),
|
||||
'dzuhur': timeToDateTime(dzuhur, refDate),
|
||||
'ashar': timeToDateTime(ashar, refDate),
|
||||
'maghrib': timeToDateTime(maghrib, refDate),
|
||||
'isya': timeToDateTime(isya, refDate),
|
||||
};
|
||||
}
|
||||
|
||||
/// Adapter for DailyPrayerSchedule.
|
||||
class DailyPrayerScheduleAdapter extends TypeAdapter<DailyPrayerSchedule> {
|
||||
@override
|
||||
final int typeId = 1;
|
||||
|
||||
@override
|
||||
DailyPrayerSchedule read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{};
|
||||
for (int i = 0; i < numOfFields; i++) {
|
||||
fields[reader.readByte()] = reader.read();
|
||||
}
|
||||
return DailyPrayerSchedule(
|
||||
date: fields[0] as String? ?? '',
|
||||
imsak: fields[1] as String? ?? '00:00',
|
||||
subuh: fields[2] as String? ?? '00:00',
|
||||
terbit: fields[3] as String? ?? '00:00',
|
||||
dhuha: fields[4] as String? ?? '00:00',
|
||||
dzuhur: fields[5] as String? ?? '00:00',
|
||||
ashar: fields[6] as String? ?? '00:00',
|
||||
maghrib: fields[7] as String? ?? '00:00',
|
||||
isya: fields[8] as String? ?? '00:00',
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, DailyPrayerSchedule obj) {
|
||||
writer
|
||||
..writeByte(9)
|
||||
..writeByte(0)..write(obj.date)
|
||||
..writeByte(1)..write(obj.imsak)
|
||||
..writeByte(2)..write(obj.subuh)
|
||||
..writeByte(3)..write(obj.terbit)
|
||||
..writeByte(4)..write(obj.dhuha)
|
||||
..writeByte(5)..write(obj.dzuhur)
|
||||
..writeByte(6)..write(obj.ashar)
|
||||
..writeByte(7)..write(obj.maghrib)
|
||||
..writeByte(8)..write(obj.isya);
|
||||
}
|
||||
}
|
||||
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