Improve notifications, tilawah flow, and dzikir structure

This commit is contained in:
Dwindi Ramadhana
2026-05-20 19:52:15 +07:00
parent c32b56c00e
commit 5195ba19ad
19 changed files with 1056 additions and 318 deletions

View File

@@ -17,11 +17,7 @@ import '../../../data/local/models/daily_worship_log.dart';
import '../../../data/local/models/dzikir_counter.dart';
import '../../../data/local/models/dzikir_log.dart';
import '../../../data/local/models/shalat_log.dart';
import '../../../data/services/location_service.dart';
import '../../../data/services/myquran_sholat_service.dart';
import '../../../data/services/muslim_api_service.dart';
import '../../../data/services/prayer_service.dart';
import '../../dashboard/data/prayer_times_provider.dart';
class DzikirScreen extends ConsumerStatefulWidget {
final bool isSimpleModeTab;
@@ -41,20 +37,17 @@ class _DzikirScreenState extends ConsumerState<DzikirScreen>
'pagi': PageController(),
'petang': PageController(),
'harian': PageController(),
'solat': PageController(),
};
final Map<String, int> _focusPageIndex = {
'pagi': 0,
'petang': 0,
'harian': 0,
'solat': 0,
};
List<Map<String, dynamic>> _pagiItems = [];
List<Map<String, dynamic>> _petangItems = [];
List<Map<String, dynamic>> _harianItems = [];
List<Map<String, dynamic>> _sesudahSholatItems = [];
Map<String, dynamic>? _pagiIntroItem;
Map<String, dynamic>? _petangIntroItem;
bool _loading = true;
@@ -63,29 +56,19 @@ class _DzikirScreenState extends ConsumerState<DzikirScreen>
late Box<DzikirCounter> _counterBox;
late String _todayKey;
Timer? _dayResetTimer;
Timer? _solatResetTimer;
bool _refreshingSolatScope = false;
String _solatScopeKey = 'solat_bootstrap';
String _solatScopeDateKey = '';
DateTime? _nextSolatResetAt;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_tabController = TabController(length: 4, vsync: this);
_tabController = TabController(length: 3, vsync: this);
_tabController.addListener(() {
if (!mounted) return;
if (_tabController.index == 0) {
unawaited(_refreshSolatScope());
}
setState(() {});
});
_counterBox = Hive.box<DzikirCounter>(HiveBoxes.dzikirCounters);
_todayKey = _currentTodayKey();
_solatScopeDateKey = _todayKey;
_scheduleDayResetTimer();
unawaited(_refreshSolatScope(forceSetState: false));
_loadData();
}
@@ -93,7 +76,6 @@ class _DzikirScreenState extends ConsumerState<DzikirScreen>
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_dayResetTimer?.cancel();
_solatResetTimer?.cancel();
_tabController.dispose();
for (final controller in _pageControllers.values) {
controller.dispose();
@@ -106,7 +88,6 @@ class _DzikirScreenState extends ConsumerState<DzikirScreen>
if (state == AppLifecycleState.resumed) {
_refreshTodayScope();
_scheduleDayResetTimer();
unawaited(_refreshSolatScope());
}
}
@@ -131,161 +112,15 @@ class _DzikirScreenState extends ConsumerState<DzikirScreen>
if (_harianItems.isNotEmpty) {
_seedHarianProgressFromLinkedDzikir();
}
unawaited(_refreshSolatScope());
if (!mounted) return;
setState(() {});
}
void _scheduleSolatResetTimer() {
_solatResetTimer?.cancel();
final nextResetAt = _nextSolatResetAt;
if (nextResetAt == null) return;
var delay =
nextResetAt.difference(DateTime.now()) + const Duration(seconds: 1);
if (delay.isNegative) {
delay = const Duration(seconds: 1);
}
_solatResetTimer = Timer(delay, () {
unawaited(_refreshSolatScope());
});
}
Future<void> _refreshSolatScope({bool forceSetState = true}) async {
if (_refreshingSolatScope) return;
_refreshingSolatScope = true;
try {
final scope = await _loadCurrentSolatScope();
if (scope == null) return;
final scopeChanged = _solatScopeKey != scope.scopeKey;
_solatScopeKey = scope.scopeKey;
_solatScopeDateKey = scope.dateKey;
_nextSolatResetAt = scope.nextResetAt;
_scheduleSolatResetTimer();
if (scopeChanged) {
_focusPageIndex['solat'] = 0;
final controller = _pageControllers['solat'];
if (controller != null && controller.hasClients) {
controller.jumpToPage(0);
}
}
if (!mounted) return;
if (scopeChanged || forceSetState) {
setState(() {});
}
} finally {
_refreshingSolatScope = false;
}
}
Future<({String scopeKey, String dateKey, DateTime nextResetAt})?>
_loadCurrentSolatScope() async {
final now = DateTime.now();
final cityId = ref.read(selectedCityIdProvider);
final dates = [
DateTime(now.year, now.month, now.day - 1),
DateTime(now.year, now.month, now.day),
DateTime(now.year, now.month, now.day + 1),
];
final prayerEntries =
<({String prayerKey, String dateKey, DateTime time})>[];
for (final date in dates) {
final schedule = await _loadPrayerScheduleForDate(cityId, date);
final dateKey = DateFormat('yyyy-MM-dd').format(date);
for (final prayerKey in const [
'subuh',
'dzuhur',
'ashar',
'maghrib',
'isya',
]) {
final parsed = _parsePrayerDateTime(date, schedule[prayerKey]);
if (parsed == null) continue;
prayerEntries.add((
prayerKey: prayerKey,
dateKey: dateKey,
time: parsed,
));
}
}
if (prayerEntries.isEmpty) return null;
prayerEntries.sort((a, b) => a.time.compareTo(b.time));
({String prayerKey, String dateKey, DateTime time})? active;
({String prayerKey, String dateKey, DateTime time})? next;
for (final entry in prayerEntries) {
if (!entry.time.isAfter(now)) {
active = entry;
continue;
}
next = entry;
break;
}
active ??= prayerEntries.first;
next ??= prayerEntries.last;
return (
scopeKey: '${active.dateKey}_${active.prayerKey}',
dateKey: active.dateKey,
nextResetAt: next.time,
);
}
Future<Map<String, String>> _loadPrayerScheduleForDate(
String cityId,
DateTime date,
) async {
final dateKey = DateFormat('yyyy-MM-dd').format(date);
final jadwal =
await MyQuranSholatService.instance.getDailySchedule(cityId, dateKey);
if (jadwal != null) return jadwal;
return _buildFallbackPrayerSchedule(date);
}
Map<String, String> _buildFallbackPrayerSchedule(DateTime date) {
final lastKnown = LocationService.instance.getLastKnownLocation();
final lat = lastKnown?.lat ?? -6.2088;
final lng = lastKnown?.lng ?? 106.8456;
final result = PrayerService.instance.getPrayerTimes(lat, lng, date);
final timeFormat = DateFormat('HH:mm');
return {
'subuh': timeFormat.format(result.fajr),
'dzuhur': timeFormat.format(result.dhuhr),
'ashar': timeFormat.format(result.asr),
'maghrib': timeFormat.format(result.maghrib),
'isya': timeFormat.format(result.isha),
};
}
DateTime? _parsePrayerDateTime(DateTime date, String? rawTime) {
if (rawTime == null || rawTime.trim().isEmpty || rawTime == '-') {
return null;
}
final parts = rawTime.trim().split(':');
if (parts.length != 2) return null;
final hour = int.tryParse(parts[0]);
final minute = int.tryParse(parts[1]);
if (hour == null || minute == null) return null;
return DateTime(date.year, date.month, date.day, hour, minute);
}
String _counterScopeKeyForPrefix(String prefix) {
if (prefix == 'solat') return _solatScopeKey;
return _todayKey;
}
String _counterDateKeyForPrefix(String prefix) {
if (prefix == 'solat') return _solatScopeDateKey;
return _todayKey;
}
@@ -305,10 +140,6 @@ class _DzikirScreenState extends ConsumerState<DzikirScreen>
'petang',
strict: true,
);
final solat = await MuslimApiService.instance.getDzikirByType(
'solat',
strict: true,
);
if (!mounted) return;
final pagiNormalized = _normalizeRumayshoDzikir('pagi', pagi);
@@ -336,7 +167,6 @@ class _DzikirScreenState extends ConsumerState<DzikirScreen>
_pagiItems = pagiItems;
_petangItems = petangItems;
_harianItems = harianItems;
_sesudahSholatItems = solat;
_loading = false;
});
_ensureValidFocusPages();
@@ -691,7 +521,6 @@ class _DzikirScreenState extends ConsumerState<DzikirScreen>
_petangItems.length + (_petangIntroItem != null ? 1 : 0),
);
_clampFocusPageForPrefix('harian', _harianItems.length);
_clampFocusPageForPrefix('solat', _sesudahSholatItems.length);
}
void _clampFocusPageForPrefix(String prefix, int itemLength) {
@@ -866,7 +695,6 @@ class _DzikirScreenState extends ConsumerState<DzikirScreen>
labelStyle:
const TextStyle(fontWeight: FontWeight.w700, fontSize: 13),
tabs: const [
Tab(text: 'Sesudah Sholat'),
Tab(text: 'Pagi'),
Tab(text: 'Petang'),
Tab(text: 'Harian'),
@@ -883,28 +711,6 @@ class _DzikirScreenState extends ConsumerState<DzikirScreen>
: TabBarView(
controller: _tabController,
children: [
isFocusMode
? _buildFocusModeTab(
context,
isDark,
settings,
items: _sesudahSholatItems,
introItem: null,
prefix: 'solat',
title: 'Dzikir Sesudah Sholat',
subtitle:
'Dibaca setelah shalat fardhu. Hitungan akan dimulai ulang otomatis saat waktu shalat berikutnya masuk.',
)
: _buildDzikirList(
context,
isDark,
settings,
_sesudahSholatItems,
null,
'solat',
'Dzikir Sesudah Sholat',
'Dibaca setelah shalat fardhu. Hitungan akan dimulai ulang otomatis saat waktu shalat berikutnya masuk.',
),
isFocusMode
? _buildFocusModeTab(
context,
@@ -1157,9 +963,6 @@ class _DzikirScreenState extends ConsumerState<DzikirScreen>
const SizedBox(height: 16),
GestureDetector(
onTap: () async {
if (prefix == 'solat') {
await _refreshSolatScope();
}
final becameComplete = _increment(
dzikirId,
target,
@@ -1760,10 +1563,6 @@ class _DzikirScreenState extends ConsumerState<DzikirScreen>
}) async {
_refreshTodayScope();
if (items.isEmpty) return;
if (prefix == 'solat') {
await _refreshSolatScope();
if (!context.mounted) return;
}
final introOffset = introItem != null ? 1 : 0;
final currentPage =