Improve notifications, tilawah flow, and dzikir structure
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
@@ -14,6 +16,7 @@ import '../../../data/local/models/shalat_log.dart';
|
||||
import '../../../data/local/models/tilawah_log.dart';
|
||||
import '../../../data/local/models/dzikir_log.dart';
|
||||
import '../../../data/local/models/puasa_log.dart';
|
||||
import '../../../data/services/notification_service.dart';
|
||||
|
||||
class ChecklistScreen extends ConsumerStatefulWidget {
|
||||
const ChecklistScreen({super.key});
|
||||
@@ -72,6 +75,47 @@ class _ChecklistScreenState extends ConsumerState<ChecklistScreen> {
|
||||
|
||||
DailyWorshipLog get _todayLog => _logBox.get(_todayKey)!;
|
||||
|
||||
Future<void> _cancelShalatReportReminderIfCompleted(String prayerKey) async {
|
||||
final sLog = _todayLog.shalatLogs[prayerKey];
|
||||
if (sLog == null || !sLog.completed) return;
|
||||
final cityId = _resolveCityId();
|
||||
final canonical = _canonicalPrayerKey(prayerKey);
|
||||
if (canonical == null) return;
|
||||
await NotificationService.instance.cancelShalatReportReminders(
|
||||
cityId: cityId,
|
||||
dateKey: _todayKey,
|
||||
canonicalPrayer: canonical,
|
||||
repeatCount: _settings.shalatReportReminderRepeatCount,
|
||||
);
|
||||
}
|
||||
|
||||
String _resolveCityId() {
|
||||
final stored = _settings.lastCityName ?? '';
|
||||
if (stored.contains('|')) return stored.split('|').last;
|
||||
return '58a2fc6ed39fd083f55d4182bf88826d';
|
||||
}
|
||||
|
||||
String? _canonicalPrayerKey(String key) {
|
||||
switch (key) {
|
||||
case 'subuh':
|
||||
case 'fajr':
|
||||
return 'fajr';
|
||||
case 'dzuhur':
|
||||
case 'dhuhr':
|
||||
return 'dhuhr';
|
||||
case 'ashar':
|
||||
case 'asr':
|
||||
return 'asr';
|
||||
case 'maghrib':
|
||||
return 'maghrib';
|
||||
case 'isya':
|
||||
case 'isha':
|
||||
return 'isha';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
void _recalculateProgress() {
|
||||
final log = _todayLog;
|
||||
|
||||
@@ -386,6 +430,9 @@ class _ChecklistScreenState extends ConsumerState<ChecklistScreen> {
|
||||
onChanged: (v) {
|
||||
log.completed = v ?? false;
|
||||
_recalculateProgress();
|
||||
if (log.completed) {
|
||||
unawaited(_cancelShalatReportReminderIfCompleted(pKey));
|
||||
}
|
||||
},
|
||||
),
|
||||
childrenPadding:
|
||||
@@ -404,12 +451,14 @@ class _ChecklistScreenState extends ConsumerState<ChecklistScreen> {
|
||||
log.location = 'Masjid';
|
||||
log.completed = true; // Auto-check parent
|
||||
_recalculateProgress();
|
||||
unawaited(_cancelShalatReportReminderIfCompleted(pKey));
|
||||
}),
|
||||
const SizedBox(width: 16),
|
||||
_radioOption('Rumah', log, () {
|
||||
log.location = 'Rumah';
|
||||
log.completed = true; // Auto-check parent
|
||||
_recalculateProgress();
|
||||
unawaited(_cancelShalatReportReminderIfCompleted(pKey));
|
||||
}),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -219,15 +219,36 @@ Future<void> _syncAdhanNotifications(
|
||||
);
|
||||
}
|
||||
|
||||
final schedulesByDate = <String, Map<String, String>>{
|
||||
schedule.date: schedule.times,
|
||||
final schedulesByDate = <String, Map<String, String>>{};
|
||||
final today = DateTime.now();
|
||||
final startDate = DateTime(today.year, today.month, today.day);
|
||||
final endDate = startDate.add(const Duration(days: 35));
|
||||
|
||||
final monthKeys = <String>{
|
||||
DateFormat('yyyy-MM').format(startDate),
|
||||
DateFormat('yyyy-MM').format(endDate),
|
||||
};
|
||||
|
||||
final baseDate = DateTime.tryParse(schedule.date);
|
||||
if (baseDate != null) {
|
||||
final nextDate = DateFormat('yyyy-MM-dd')
|
||||
.format(baseDate.add(const Duration(days: 1)));
|
||||
if (!schedulesByDate.containsKey(nextDate)) {
|
||||
for (final monthKey in monthKeys) {
|
||||
final monthly = await MyQuranSholatService.instance
|
||||
.getMonthlySchedule(cityId, monthKey);
|
||||
for (final entry in monthly.entries) {
|
||||
final date = DateTime.tryParse(entry.key);
|
||||
if (date == null) continue;
|
||||
final normalized = DateTime(date.year, date.month, date.day);
|
||||
if (normalized.isBefore(startDate) || normalized.isAfter(endDate)) {
|
||||
continue;
|
||||
}
|
||||
schedulesByDate[entry.key] = entry.value;
|
||||
}
|
||||
}
|
||||
|
||||
if (schedulesByDate.isEmpty) {
|
||||
schedulesByDate[schedule.date] = schedule.times;
|
||||
final baseDate = DateTime.tryParse(schedule.date);
|
||||
if (baseDate != null) {
|
||||
final nextDate = DateFormat('yyyy-MM-dd')
|
||||
.format(baseDate.add(const Duration(days: 1)));
|
||||
final nextSchedule = await MyQuranSholatService.instance
|
||||
.getDailySchedule(cityId, nextDate);
|
||||
if (nextSchedule != null) {
|
||||
@@ -241,6 +262,11 @@ Future<void> _syncAdhanNotifications(
|
||||
adhanEnabled: settings.adhanEnabled,
|
||||
iqamahOffset: settings.iqamahOffset,
|
||||
schedulesByDate: schedulesByDate,
|
||||
reportReminderEnabled: settings.shalatReportReminderEnabled,
|
||||
reportReminderDelayMinutes: settings.shalatReportReminderDelayMinutes,
|
||||
reportReminderRepeatCount: settings.shalatReportReminderRepeatCount,
|
||||
reportReminderRepeatIntervalMinutes:
|
||||
settings.shalatReportReminderRepeatIntervalMinutes,
|
||||
);
|
||||
await NotificationService.instance.syncHabitNotifications(
|
||||
settings: settings,
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -71,6 +71,13 @@ class _QuranReadingScreenState extends ConsumerState<QuranReadingScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
void _navigateToSurah(int surahNumber, {int? startVerse}) {
|
||||
final base = widget.isSimpleModeTab ? '/quran' : '/tools/quran';
|
||||
final verseQuery =
|
||||
(startVerse != null && startVerse > 0) ? '?startVerse=$startVerse' : '';
|
||||
context.pushReplacement('$base/$surahNumber$verseQuery');
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -679,6 +686,7 @@ class _QuranReadingScreenState extends ConsumerState<QuranReadingScreen> {
|
||||
TilawahSession session, int endVerseId) async {
|
||||
final endSurahId = _surah!['nomor'] ?? 1;
|
||||
final endSurahName = _surah!['namaLatin'] ?? '';
|
||||
final isLastAyat = endVerseId == _verses.length;
|
||||
|
||||
int calculatedAyat = 0;
|
||||
|
||||
@@ -723,115 +731,155 @@ class _QuranReadingScreenState extends ConsumerState<QuranReadingScreen> {
|
||||
}
|
||||
|
||||
if (!mounted) return;
|
||||
bool saveAsLastRead = true;
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('Catat Sesi Tilawah',
|
||||
style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primary.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
builder: (ctx) => StatefulBuilder(
|
||||
builder: (context, setModalState) => AlertDialog(
|
||||
title: const Text('Catat Sesi Tilawah',
|
||||
style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primary.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text('Mulai:', style: TextStyle(fontSize: 13)),
|
||||
Text(
|
||||
'${session.startSurahName} : ${session.startVerseId}',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold, fontSize: 13)),
|
||||
]),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 4),
|
||||
child: Divider()),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text('Selesai:',
|
||||
style: TextStyle(fontSize: 13)),
|
||||
Text('$endSurahName : $endVerseId',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold, fontSize: 13)),
|
||||
]),
|
||||
])),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(LucideIcons.bookOpen,
|
||||
size: 20, color: AppColors.primary),
|
||||
const SizedBox(width: 8),
|
||||
Text('Total Dibaca: $calculatedAyat Ayat',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold, fontSize: 15)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
CheckboxListTile(
|
||||
value: saveAsLastRead,
|
||||
onChanged: (value) {
|
||||
setModalState(() => saveAsLastRead = value ?? true);
|
||||
},
|
||||
contentPadding: EdgeInsets.zero,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
title: const Text(
|
||||
'Simpan juga sebagai Terakhir Dibaca',
|
||||
style: TextStyle(fontSize: 13),
|
||||
),
|
||||
child: Column(children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text('Mulai:', style: TextStyle(fontSize: 13)),
|
||||
Text(
|
||||
'${session.startSurahName} : ${session.startVerseId}',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold, fontSize: 13)),
|
||||
]),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 4),
|
||||
child: Divider()),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text('Selesai:', style: TextStyle(fontSize: 13)),
|
||||
Text('$endSurahName : $endVerseId',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold, fontSize: 13)),
|
||||
]),
|
||||
])),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(LucideIcons.bookOpen,
|
||||
size: 20, color: AppColors.primary),
|
||||
const SizedBox(width: 8),
|
||||
Text('Total Dibaca: $calculatedAyat Ayat',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold, fontSize: 15)),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
ref.invalidate(tilawahTrackingProvider);
|
||||
Navigator.pop(ctx);
|
||||
},
|
||||
child: const Text('Batal', style: TextStyle(color: Colors.red)),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () async {
|
||||
final settingsBox = Hive.box<AppSettings>(HiveBoxes.settings);
|
||||
final settings = settingsBox.get('default') ?? AppSettings();
|
||||
final todayKey =
|
||||
DateFormat('yyyy-MM-dd').format(DateTime.now());
|
||||
final logBox = Hive.box<DailyWorshipLog>(HiveBoxes.worshipLogs);
|
||||
var log = logBox.get(todayKey);
|
||||
|
||||
if (log == null) {
|
||||
log = DailyWorshipLog(
|
||||
date: todayKey,
|
||||
shalatLogs: <String, ShalatLog>{
|
||||
'subuh': ShalatLog(),
|
||||
'dzuhur': ShalatLog(),
|
||||
'ashar': ShalatLog(),
|
||||
'maghrib': ShalatLog(),
|
||||
'isya': ShalatLog(),
|
||||
},
|
||||
);
|
||||
logBox.put(todayKey, log);
|
||||
}
|
||||
|
||||
if (log.tilawahLog == null) {
|
||||
log.tilawahLog = TilawahLog(
|
||||
targetValue: settings.tilawahTargetValue,
|
||||
targetUnit: settings.tilawahTargetUnit,
|
||||
autoSync: _autoSyncEnabled,
|
||||
);
|
||||
}
|
||||
|
||||
log.tilawahLog!.rawAyatRead += calculatedAyat;
|
||||
log.save();
|
||||
|
||||
if (saveAsLastRead) {
|
||||
final verse = _verses.firstWhere(
|
||||
(v) => (v['nomorAyat'] ?? 0) == endVerseId,
|
||||
orElse: () => <String, dynamic>{},
|
||||
);
|
||||
if (verse.isNotEmpty) {
|
||||
await _saveBookmark(
|
||||
endSurahId,
|
||||
endVerseId,
|
||||
verse,
|
||||
isLastRead: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('$calculatedAyat Ayat dicatat!'),
|
||||
backgroundColor: AppColors.primary,
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
ref.invalidate(tilawahTrackingProvider);
|
||||
Navigator.pop(ctx);
|
||||
|
||||
final isLastSurah = endSurahId >= 114;
|
||||
if (settings.tilawahAutoContinueNextSurah &&
|
||||
isLastAyat &&
|
||||
!isLastSurah) {
|
||||
_navigateToSurah(endSurahId + 1, startVerse: 1);
|
||||
}
|
||||
},
|
||||
child: const Text('Simpan'),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
ref.invalidate(tilawahTrackingProvider);
|
||||
Navigator.pop(ctx);
|
||||
},
|
||||
child: const Text('Batal', style: TextStyle(color: Colors.red)),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
final todayKey = DateFormat('yyyy-MM-dd').format(DateTime.now());
|
||||
final logBox = Hive.box<DailyWorshipLog>(HiveBoxes.worshipLogs);
|
||||
var log = logBox.get(todayKey);
|
||||
|
||||
if (log == null) {
|
||||
log = DailyWorshipLog(
|
||||
date: todayKey,
|
||||
shalatLogs: <String, ShalatLog>{
|
||||
'subuh': ShalatLog(),
|
||||
'dzuhur': ShalatLog(),
|
||||
'ashar': ShalatLog(),
|
||||
'maghrib': ShalatLog(),
|
||||
'isya': ShalatLog(),
|
||||
},
|
||||
);
|
||||
logBox.put(todayKey, log);
|
||||
}
|
||||
|
||||
if (log.tilawahLog == null) {
|
||||
final settingsBox = Hive.box<AppSettings>(HiveBoxes.settings);
|
||||
final settings = settingsBox.get('default') ?? AppSettings();
|
||||
log.tilawahLog = TilawahLog(
|
||||
targetValue: settings.tilawahTargetValue,
|
||||
targetUnit: settings.tilawahTargetUnit,
|
||||
autoSync: _autoSyncEnabled,
|
||||
);
|
||||
}
|
||||
|
||||
log.tilawahLog!.rawAyatRead += calculatedAyat;
|
||||
log.save();
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('$calculatedAyat Ayat dicatat!'),
|
||||
backgroundColor: AppColors.primary,
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
ref.invalidate(tilawahTrackingProvider);
|
||||
Navigator.pop(ctx);
|
||||
},
|
||||
child: const Text('Simpan'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1156,6 +1204,42 @@ class _QuranReadingScreenState extends ConsumerState<QuranReadingScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTrackingSessionBanner({
|
||||
required bool isDark,
|
||||
required TilawahSession session,
|
||||
}) {
|
||||
final currentSurah = _surah?['namaLatin']?.toString() ?? widget.surahId;
|
||||
return Container(
|
||||
margin: const EdgeInsets.fromLTRB(16, 12, 16, 4),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primary.withValues(alpha: isDark ? 0.16 : 0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: AppColors.primary.withValues(alpha: 0.25),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(LucideIcons.flag, size: 16, color: AppColors.primary),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Sesi aktif: ${session.startSurahName}:${session.startVerseId} -> $currentSurah',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: isDark
|
||||
? AppColors.textPrimaryDark
|
||||
: AppColors.textPrimaryLight,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final trackingSession = ref.watch(tilawahTrackingProvider);
|
||||
@@ -1271,6 +1355,11 @@ class _QuranReadingScreenState extends ConsumerState<QuranReadingScreen> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (trackingSession != null)
|
||||
_buildTrackingSessionBanner(
|
||||
isDark: isDark,
|
||||
session: trackingSession,
|
||||
),
|
||||
if (showBismillah) ...[
|
||||
_buildBismillahSection(isDark: isDark),
|
||||
const SizedBox(height: 8),
|
||||
@@ -1292,6 +1381,25 @@ class _QuranReadingScreenState extends ConsumerState<QuranReadingScreen> {
|
||||
),
|
||||
),
|
||||
],
|
||||
if ((_surah?['nomor'] as int? ?? 1) < 114)
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
16, 18, 16, 0),
|
||||
child: FilledButton.icon(
|
||||
onPressed: () {
|
||||
final nextSurah =
|
||||
(_surah?['nomor'] as int? ?? 1) +
|
||||
1;
|
||||
_navigateToSurah(nextSurah,
|
||||
startVerse: 1);
|
||||
},
|
||||
icon:
|
||||
const Icon(LucideIcons.arrowRight),
|
||||
label: Text(trackingSession == null
|
||||
? 'Lanjut ke Surah Berikutnya'
|
||||
: 'Lanjut Surah (Sesi Tetap Aktif)'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -101,6 +101,11 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
||||
_saveSettings();
|
||||
}
|
||||
|
||||
void _resyncPrayerNotifications() {
|
||||
ref.invalidate(prayerTimesProvider);
|
||||
unawaited(ref.read(prayerTimesProvider.future));
|
||||
}
|
||||
|
||||
Future<void> _showQuietHoursDialog(BuildContext context) async {
|
||||
final startController =
|
||||
TextEditingController(text: _settings.quietHoursStart);
|
||||
@@ -236,6 +241,97 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showShalatReportDelayDialog(BuildContext context) async {
|
||||
final controller = TextEditingController(
|
||||
text: _settings.shalatReportReminderDelayMinutes.toString(),
|
||||
);
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('Jeda Pengingat Lapor Shalat'),
|
||||
content: TextField(
|
||||
controller: controller,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Menit setelah waktu shalat',
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
child: const Text('Batal'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
final value = int.tryParse(controller.text.trim());
|
||||
if (value == null || value < 5 || value > 240) return;
|
||||
_settings.shalatReportReminderDelayMinutes = value;
|
||||
_saveSettings();
|
||||
_resyncPrayerNotifications();
|
||||
Navigator.pop(ctx);
|
||||
},
|
||||
child: const Text('Simpan'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showShalatReportRepeatDialog(BuildContext context) async {
|
||||
final repeatController = TextEditingController(
|
||||
text: _settings.shalatReportReminderRepeatCount.toString(),
|
||||
);
|
||||
final intervalController = TextEditingController(
|
||||
text: _settings.shalatReportReminderRepeatIntervalMinutes.toString(),
|
||||
);
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('Pengulangan Pengingat Lapor'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextField(
|
||||
controller: repeatController,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Jumlah ulang (0-5)',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
TextField(
|
||||
controller: intervalController,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Jeda antar ulang (menit)',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
child: const Text('Batal'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
final repeats = int.tryParse(repeatController.text.trim());
|
||||
final interval = int.tryParse(intervalController.text.trim());
|
||||
if (repeats == null || repeats < 0 || repeats > 5) return;
|
||||
if (interval == null || interval < 5 || interval > 180) return;
|
||||
_settings.shalatReportReminderRepeatCount = repeats;
|
||||
_settings.shalatReportReminderRepeatIntervalMinutes = interval;
|
||||
_saveSettings();
|
||||
_resyncPrayerNotifications();
|
||||
Navigator.pop(ctx);
|
||||
},
|
||||
child: const Text('Simpan'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
@@ -446,6 +542,46 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
_settingRow(
|
||||
isDark,
|
||||
icon: LucideIcons.siren,
|
||||
iconColor: const Color(0xFFC0392B),
|
||||
title: 'Pengingat Lapor Shalat',
|
||||
subtitle: _settings.shalatReportReminderEnabled
|
||||
? 'Aktif • ${_settings.shalatReportReminderDelayMinutes} menit setelah adzan'
|
||||
: 'Nonaktif',
|
||||
trailing: IosToggle(
|
||||
value: _settings.shalatReportReminderEnabled,
|
||||
onChanged: (v) {
|
||||
_settings.shalatReportReminderEnabled = v;
|
||||
_saveSettings();
|
||||
_resyncPrayerNotifications();
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
_settingRow(
|
||||
isDark,
|
||||
icon: LucideIcons.clock3,
|
||||
iconColor: const Color(0xFF16A085),
|
||||
title: 'Jeda Pengingat Lapor',
|
||||
subtitle:
|
||||
'${_settings.shalatReportReminderDelayMinutes} menit setelah waktu shalat',
|
||||
trailing: const Icon(LucideIcons.chevronRight, size: 20),
|
||||
onTap: () => _showShalatReportDelayDialog(context),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
_settingRow(
|
||||
isDark,
|
||||
icon: LucideIcons.repeat,
|
||||
iconColor: const Color(0xFF8E44AD),
|
||||
title: 'Ulangi Pengingat Lapor',
|
||||
subtitle:
|
||||
'${_settings.shalatReportReminderRepeatCount}x • tiap ${_settings.shalatReportReminderRepeatIntervalMinutes} menit',
|
||||
trailing: const Icon(LucideIcons.chevronRight, size: 20),
|
||||
onTap: () => _showShalatReportRepeatDialog(context),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
_settingRow(
|
||||
isDark,
|
||||
icon: LucideIcons.moonStar,
|
||||
@@ -521,6 +657,21 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
_settingRow(
|
||||
isDark,
|
||||
icon: LucideIcons.arrowRightCircle,
|
||||
iconColor: Colors.green,
|
||||
title: 'Lanjut Surah Otomatis',
|
||||
subtitle: 'Saat simpan sesi di ayat terakhir',
|
||||
trailing: IosToggle(
|
||||
value: _settings.tilawahAutoContinueNextSurah,
|
||||
onChanged: (v) {
|
||||
_settings.tilawahAutoContinueNextSurah = v;
|
||||
_saveSettings();
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
_settingRow(
|
||||
isDark,
|
||||
icon: LucideIcons.listChecks,
|
||||
|
||||
Reference in New Issue
Block a user