1849 lines
64 KiB
Dart
1849 lines
64 KiB
Dart
import 'dart:async';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:hive_flutter/hive_flutter.dart';
|
|
import 'package:lucide_icons/lucide_icons.dart';
|
|
import '../../../app/theme/app_colors.dart';
|
|
import '../../../core/providers/theme_provider.dart';
|
|
import '../../../core/widgets/ios_toggle.dart';
|
|
import '../../../data/local/hive_boxes.dart';
|
|
import '../../../data/local/models/app_settings.dart';
|
|
import '../../../data/services/location_service.dart';
|
|
import '../../../data/services/myquran_sholat_service.dart';
|
|
import '../../../data/services/notification_service.dart';
|
|
import '../../dashboard/data/prayer_times_provider.dart';
|
|
import 'package:intl/intl.dart';
|
|
import '../../../data/local/models/daily_worship_log.dart';
|
|
|
|
class SettingsScreen extends ConsumerStatefulWidget {
|
|
const SettingsScreen({super.key});
|
|
|
|
@override
|
|
ConsumerState<SettingsScreen> createState() => _SettingsScreenState();
|
|
}
|
|
|
|
class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
|
late AppSettings _settings;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
final box = Hive.box<AppSettings>(HiveBoxes.settings);
|
|
_settings = box.get('default') ?? AppSettings();
|
|
_enforceTilawahAutoSyncIfNeeded();
|
|
}
|
|
|
|
void _saveSettings() {
|
|
_settings.save();
|
|
setState(() {});
|
|
}
|
|
|
|
void _syncTodayTilawahAutoSync(bool value) {
|
|
final todayKey = DateFormat('yyyy-MM-dd').format(DateTime.now());
|
|
final logBox = Hive.box<DailyWorshipLog>(HiveBoxes.worshipLogs);
|
|
final log = logBox.get(todayKey);
|
|
if (log != null && log.tilawahLog != null) {
|
|
log.tilawahLog!.autoSync = value;
|
|
log.save();
|
|
}
|
|
}
|
|
|
|
void _enforceTilawahAutoSyncIfNeeded() {
|
|
if (_settings.simpleMode || _settings.tilawahAutoSync) return;
|
|
_settings.tilawahAutoSync = true;
|
|
if (_settings.isInBox) {
|
|
_settings.save();
|
|
} else {
|
|
Hive.box<AppSettings>(HiveBoxes.settings).put('default', _settings);
|
|
}
|
|
_syncTodayTilawahAutoSync(true);
|
|
}
|
|
|
|
bool get _isDarkMode => _settings.themeModeIndex != 1;
|
|
bool get _prayerAlarmEnabled => _settings.adhanEnabled.values.any((v) => v);
|
|
|
|
String get _displayCityName {
|
|
final stored = _settings.lastCityName ?? 'Jakarta';
|
|
if (stored.contains('|')) {
|
|
return stored.split('|').first;
|
|
}
|
|
return stored;
|
|
}
|
|
|
|
void _toggleDarkMode(bool value) {
|
|
_settings.themeModeIndex = value ? 2 : 1;
|
|
_saveSettings();
|
|
ref.read(themeProvider.notifier).state =
|
|
value ? ThemeMode.dark : ThemeMode.light;
|
|
}
|
|
|
|
void _toggleNotifications(bool value) {
|
|
_settings.adhanEnabled.updateAll((key, _) => value);
|
|
_saveSettings();
|
|
if (!value) {
|
|
unawaited(NotificationService.instance.cancelAllPending());
|
|
}
|
|
ref.invalidate(prayerTimesProvider);
|
|
unawaited(ref.read(prayerTimesProvider.future));
|
|
}
|
|
|
|
void _toggleGlobalAlerts(bool value) {
|
|
_settings.alertsEnabled = value;
|
|
_saveSettings();
|
|
unawaited(NotificationService.instance.syncHabitNotifications(
|
|
settings: _settings,
|
|
));
|
|
ref.invalidate(prayerTimesProvider);
|
|
unawaited(ref.read(prayerTimesProvider.future));
|
|
}
|
|
|
|
void _toggleInbox(bool value) {
|
|
_settings.inboxEnabled = value;
|
|
_saveSettings();
|
|
}
|
|
|
|
void _resyncPrayerNotifications() {
|
|
ref.invalidate(prayerTimesProvider);
|
|
unawaited(ref.read(prayerTimesProvider.future));
|
|
}
|
|
|
|
String _normalizeDzikirDisplayMode(String raw) {
|
|
if (raw == 'slide') return 'focus';
|
|
return raw;
|
|
}
|
|
|
|
String _normalizeDzikirCounterButtonPosition(String raw) {
|
|
if (raw == 'fullwidth') return 'bottomPill';
|
|
if (raw == 'circle') return 'fabCircle';
|
|
return raw;
|
|
}
|
|
|
|
Future<void> _showQuietHoursDialog(BuildContext context) async {
|
|
final startController =
|
|
TextEditingController(text: _settings.quietHoursStart);
|
|
final endController = TextEditingController(text: _settings.quietHoursEnd);
|
|
|
|
await showDialog(
|
|
context: context,
|
|
builder: (ctx) {
|
|
return AlertDialog(
|
|
title: const Text('Jam Tenang'),
|
|
content: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
TextField(
|
|
controller: startController,
|
|
keyboardType: TextInputType.datetime,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Mulai (HH:mm)',
|
|
),
|
|
),
|
|
const SizedBox(height: 10),
|
|
TextField(
|
|
controller: endController,
|
|
keyboardType: TextInputType.datetime,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Selesai (HH:mm)',
|
|
),
|
|
),
|
|
],
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(ctx),
|
|
child: const Text('Batal'),
|
|
),
|
|
FilledButton(
|
|
onPressed: () {
|
|
final start = startController.text.trim();
|
|
final end = endController.text.trim();
|
|
final valid = RegExp(r'^\d{1,2}:\d{2}$');
|
|
if (!valid.hasMatch(start) || !valid.hasMatch(end)) {
|
|
return;
|
|
}
|
|
_settings.quietHoursStart = start;
|
|
_settings.quietHoursEnd = end;
|
|
_saveSettings();
|
|
Navigator.pop(ctx);
|
|
},
|
|
child: const Text('Simpan'),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Future<void> _showPushCapDialog(BuildContext context) async {
|
|
final controller = TextEditingController(
|
|
text: _settings.maxNonPrayerPushPerDay.toString(),
|
|
);
|
|
|
|
await showDialog(
|
|
context: context,
|
|
builder: (ctx) {
|
|
return AlertDialog(
|
|
title: const Text('Batas Push Non-Sholat'),
|
|
content: TextField(
|
|
controller: controller,
|
|
keyboardType: TextInputType.number,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Maksimal per hari',
|
|
),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(ctx),
|
|
child: const Text('Batal'),
|
|
),
|
|
FilledButton(
|
|
onPressed: () {
|
|
final value = int.tryParse(controller.text.trim());
|
|
if (value == null || value < 0 || value > 20) return;
|
|
_settings.maxNonPrayerPushPerDay = value;
|
|
_saveSettings();
|
|
Navigator.pop(ctx);
|
|
},
|
|
child: const Text('Simpan'),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Future<void> _showChecklistReminderTimeDialog(BuildContext context) async {
|
|
final controller = TextEditingController(
|
|
text: _settings.checklistReminderTime ?? '09:00',
|
|
);
|
|
|
|
await showDialog(
|
|
context: context,
|
|
builder: (ctx) {
|
|
return AlertDialog(
|
|
title: const Text('Waktu Pengingat Checklist'),
|
|
content: TextField(
|
|
controller: controller,
|
|
keyboardType: TextInputType.datetime,
|
|
decoration: const InputDecoration(
|
|
labelText: 'HH:mm',
|
|
),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(ctx),
|
|
child: const Text('Batal'),
|
|
),
|
|
FilledButton(
|
|
onPressed: () {
|
|
final raw = controller.text.trim();
|
|
if (!RegExp(r'^\d{1,2}:\d{2}$').hasMatch(raw)) return;
|
|
_settings.checklistReminderTime = raw;
|
|
_saveSettings();
|
|
unawaited(NotificationService.instance.syncHabitNotifications(
|
|
settings: _settings,
|
|
));
|
|
Navigator.pop(ctx);
|
|
},
|
|
child: const Text('Simpan'),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
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
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
|
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: const Text('Pengaturan'),
|
|
),
|
|
body: SafeArea(
|
|
top: false,
|
|
bottom: true,
|
|
child: ListView(
|
|
padding: const EdgeInsets.all(16),
|
|
children: [
|
|
// ── Top-level items ──
|
|
Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: isDark ? AppColors.surfaceDark : AppColors.surfaceLight,
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(
|
|
color: isDark
|
|
? AppColors.primary.withValues(alpha: 0.1)
|
|
: AppColors.cream,
|
|
),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
width: 56,
|
|
height: 56,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
gradient: LinearGradient(
|
|
colors: [
|
|
AppColors.primary,
|
|
AppColors.primary.withValues(alpha: 0.6),
|
|
],
|
|
),
|
|
),
|
|
child: Center(
|
|
child: Text(
|
|
_settings.userName.isNotEmpty
|
|
? _settings.userName[0].toUpperCase()
|
|
: 'U',
|
|
style: const TextStyle(
|
|
fontSize: 24,
|
|
fontWeight: FontWeight.w700,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 14),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
_settings.userName,
|
|
style: const TextStyle(
|
|
fontSize: 17,
|
|
fontWeight: FontWeight.w700,
|
|
),
|
|
),
|
|
if (_settings.userEmail.isNotEmpty)
|
|
Text(
|
|
_settings.userEmail,
|
|
style: TextStyle(
|
|
fontSize: 13,
|
|
color: isDark
|
|
? AppColors.textSecondaryDark
|
|
: AppColors.textSecondaryLight,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
IconButton(
|
|
onPressed: () => _showEditProfileDialog(context),
|
|
icon: Icon(LucideIcons.pencil,
|
|
size: 20, color: AppColors.primary),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 24),
|
|
_settingRow(
|
|
isDark,
|
|
icon: LucideIcons.layoutDashboard,
|
|
iconColor: const Color(0xFF0984E3),
|
|
title: 'Mode Aplikasi',
|
|
subtitle: _settings.simpleMode
|
|
? 'Simpel — Jadwal & Al-Quran'
|
|
: 'Lengkap — Dengan Checklist & Poin',
|
|
trailing: IosToggle(
|
|
value: !_settings.simpleMode,
|
|
onChanged: (v) {
|
|
_settings.simpleMode = !v;
|
|
if (v) {
|
|
_settings.tilawahAutoSync = true;
|
|
}
|
|
_saveSettings();
|
|
_syncTodayTilawahAutoSync(_settings.tilawahAutoSync);
|
|
},
|
|
),
|
|
),
|
|
const SizedBox(height: 10),
|
|
_settingRow(
|
|
isDark,
|
|
icon: LucideIcons.moon,
|
|
iconColor: const Color(0xFF6C5CE7),
|
|
title: 'Mode Gelap',
|
|
trailing: IosToggle(
|
|
value: _isDarkMode,
|
|
onChanged: _toggleDarkMode,
|
|
),
|
|
),
|
|
const SizedBox(height: 24),
|
|
|
|
_sectionLabel('GRUP PENGATURAN'),
|
|
const SizedBox(height: 12),
|
|
_buildGroupEntry(
|
|
isDark,
|
|
icon: LucideIcons.clock3,
|
|
iconColor: const Color(0xFF0984E3),
|
|
title: 'Shalat & Waktu',
|
|
subtitle: 'Kota, alarm shalat, iqamah, metode',
|
|
onTap: () => _openSettingsGroup(
|
|
context,
|
|
title: 'Shalat & Waktu',
|
|
childrenBuilder: _buildShalatGroupItems,
|
|
),
|
|
),
|
|
const SizedBox(height: 10),
|
|
_buildGroupEntry(
|
|
isDark,
|
|
icon: LucideIcons.bell,
|
|
iconColor: const Color(0xFFE17055),
|
|
title: 'Notifikasi',
|
|
subtitle: 'Reminder, quiet hours, inbox, ringkasan',
|
|
onTap: () => _openSettingsGroup(
|
|
context,
|
|
title: 'Notifikasi',
|
|
childrenBuilder: _buildNotificationGroupItems,
|
|
),
|
|
),
|
|
const SizedBox(height: 10),
|
|
_buildGroupEntry(
|
|
isDark,
|
|
icon: LucideIcons.bookOpen,
|
|
iconColor: Colors.amber,
|
|
title: 'Tilawah',
|
|
subtitle: 'Target, auto-sync, auto lanjut surah',
|
|
onTap: () => _openSettingsGroup(
|
|
context,
|
|
title: 'Tilawah',
|
|
childrenBuilder: _buildTilawahGroupItems,
|
|
),
|
|
),
|
|
const SizedBox(height: 10),
|
|
_buildGroupEntry(
|
|
isDark,
|
|
icon: LucideIcons.sparkles,
|
|
iconColor: Colors.indigo,
|
|
title: 'Dzikir',
|
|
subtitle: 'Mode tampilan, tombol, haptic',
|
|
onTap: () => _openSettingsGroup(
|
|
context,
|
|
title: 'Dzikir',
|
|
childrenBuilder: _buildDzikirGroupItems,
|
|
),
|
|
),
|
|
const SizedBox(height: 10),
|
|
_buildGroupEntry(
|
|
isDark,
|
|
icon: LucideIcons.palette,
|
|
iconColor: const Color(0xFF6C5CE7),
|
|
title: 'Tampilan',
|
|
subtitle: 'Mode gelap dan ukuran font arab',
|
|
onTap: () => _openSettingsGroup(
|
|
context,
|
|
title: 'Tampilan',
|
|
childrenBuilder: _buildDisplayGroupItems,
|
|
),
|
|
),
|
|
const SizedBox(height: 10),
|
|
_buildGroupEntry(
|
|
isDark,
|
|
icon: LucideIcons.shieldAlert,
|
|
iconColor: Colors.red,
|
|
title: 'Akun & Data',
|
|
subtitle: 'Profil, versi aplikasi, reset data',
|
|
onTap: () => _openSettingsGroup(
|
|
context,
|
|
title: 'Akun & Data',
|
|
childrenBuilder: _buildAccountDataGroupItems,
|
|
),
|
|
),
|
|
const SizedBox(height: 32),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
void _openSettingsGroup(
|
|
BuildContext context, {
|
|
required String title,
|
|
required List<Widget> Function(bool isDark) childrenBuilder,
|
|
}) {
|
|
Navigator.of(context).push(
|
|
MaterialPageRoute(
|
|
builder: (ctx) {
|
|
final isDark = Theme.of(ctx).brightness == Brightness.dark;
|
|
return Scaffold(
|
|
appBar: AppBar(title: Text(title)),
|
|
body: ValueListenableBuilder<Box<AppSettings>>(
|
|
valueListenable: Hive.box<AppSettings>(HiveBoxes.settings)
|
|
.listenable(keys: ['default']),
|
|
builder: (_, settingsBox, __) {
|
|
_settings = settingsBox.get('default') ?? AppSettings();
|
|
return SafeArea(
|
|
top: false,
|
|
child: ListView(
|
|
padding: const EdgeInsets.all(16),
|
|
children: [
|
|
...childrenBuilder(isDark),
|
|
const SizedBox(height: 24),
|
|
Text(
|
|
'Tips: Pengaturan difokuskan per kategori agar lebih mudah dicari.',
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: isDark
|
|
? AppColors.textSecondaryDark
|
|
: AppColors.textSecondaryLight,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
List<Widget> _buildShalatGroupItems(bool isDark) {
|
|
return [
|
|
_settingRow(
|
|
isDark,
|
|
icon: LucideIcons.mapPin,
|
|
iconColor: const Color(0xFF00B894),
|
|
title: 'Kota',
|
|
subtitle: _displayCityName,
|
|
trailing: const Icon(LucideIcons.chevronRight, size: 20),
|
|
onTap: () => _showLocationDialog(context),
|
|
),
|
|
const SizedBox(height: 10),
|
|
_settingRow(
|
|
isDark,
|
|
icon: LucideIcons.bell,
|
|
iconColor: const Color(0xFFE17055),
|
|
title: 'Alarm Sholat',
|
|
trailing: IosToggle(
|
|
value: _prayerAlarmEnabled,
|
|
onChanged: _toggleNotifications,
|
|
),
|
|
),
|
|
const SizedBox(height: 10),
|
|
_settingRow(
|
|
isDark,
|
|
icon: LucideIcons.timer,
|
|
iconColor: const Color(0xFF0984E3),
|
|
title: 'Jeda Iqamah',
|
|
trailing: const Icon(LucideIcons.chevronRight, size: 20),
|
|
onTap: () => _showIqamahDialog(context),
|
|
),
|
|
const SizedBox(height: 10),
|
|
_settingRow(
|
|
isDark,
|
|
icon: LucideIcons.compass,
|
|
iconColor: AppColors.sage,
|
|
title: 'Metode Perhitungan',
|
|
subtitle: 'Kemenag (myQuran)',
|
|
trailing: const Icon(LucideIcons.chevronRight, size: 20),
|
|
onTap: () => _showMethodDialog(context),
|
|
),
|
|
const SizedBox(height: 10),
|
|
_settingRow(
|
|
isDark,
|
|
icon: LucideIcons.building,
|
|
iconColor: Colors.teal,
|
|
title: 'Tingkat Sholat Rawatib',
|
|
subtitle: _settings.rawatibLevel == 0
|
|
? 'Mati'
|
|
: (_settings.rawatibLevel == 1 ? 'Muakkad Saja' : 'Lengkap'),
|
|
trailing: const Icon(LucideIcons.chevronRight, size: 20),
|
|
onTap: () => _showRawatibDialog(context),
|
|
),
|
|
];
|
|
}
|
|
|
|
List<Widget> _buildNotificationGroupItems(bool isDark) {
|
|
return [
|
|
_settingRow(
|
|
isDark,
|
|
icon: LucideIcons.alertCircle,
|
|
iconColor: const Color(0xFF00B894),
|
|
title: 'Peringatan Non-Sholat',
|
|
trailing: IosToggle(
|
|
value: _settings.alertsEnabled,
|
|
onChanged: _toggleGlobalAlerts,
|
|
),
|
|
),
|
|
const SizedBox(height: 10),
|
|
_settingRow(
|
|
isDark,
|
|
icon: LucideIcons.inbox,
|
|
iconColor: const Color(0xFF6C5CE7),
|
|
title: 'Kotak Masuk Pesan',
|
|
trailing: IosToggle(
|
|
value: _settings.inboxEnabled,
|
|
onChanged: _toggleInbox,
|
|
),
|
|
),
|
|
const SizedBox(height: 10),
|
|
_settingRow(
|
|
isDark,
|
|
icon: LucideIcons.checkSquare,
|
|
iconColor: const Color(0xFF2D98DA),
|
|
title: 'Pengingat Checklist Harian',
|
|
subtitle: _settings.checklistReminderTime,
|
|
trailing: IosToggle(
|
|
value: _settings.dailyChecklistReminderEnabled,
|
|
onChanged: (v) {
|
|
_settings.dailyChecklistReminderEnabled = v;
|
|
_saveSettings();
|
|
unawaited(NotificationService.instance.syncHabitNotifications(
|
|
settings: _settings,
|
|
));
|
|
},
|
|
),
|
|
onTap: () => _showChecklistReminderTimeDialog(context),
|
|
),
|
|
const SizedBox(height: 10),
|
|
_settingRow(
|
|
isDark,
|
|
icon: LucideIcons.siren,
|
|
iconColor: const Color(0xFFC0392B),
|
|
title: 'Pengingat Lapor Shalat',
|
|
subtitle: _settings.shalatReportReminderEnabled ? 'Aktif' : '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',
|
|
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 • ${_settings.shalatReportReminderRepeatIntervalMinutes} menit',
|
|
trailing: const Icon(LucideIcons.chevronRight, size: 20),
|
|
onTap: () => _showShalatReportRepeatDialog(context),
|
|
),
|
|
const SizedBox(height: 10),
|
|
_settingRow(
|
|
isDark,
|
|
icon: LucideIcons.moonStar,
|
|
iconColor: const Color(0xFF636E72),
|
|
title: 'Jam Tenang',
|
|
subtitle: '${_settings.quietHoursStart} - ${_settings.quietHoursEnd}',
|
|
trailing: const Icon(LucideIcons.chevronRight, size: 20),
|
|
onTap: () => _showQuietHoursDialog(context),
|
|
),
|
|
const SizedBox(height: 10),
|
|
_settingRow(
|
|
isDark,
|
|
icon: LucideIcons.gauge,
|
|
iconColor: const Color(0xFFE17055),
|
|
title: 'Batas Push Non-Sholat',
|
|
subtitle: '${_settings.maxNonPrayerPushPerDay} per hari',
|
|
trailing: const Icon(LucideIcons.chevronRight, size: 20),
|
|
onTap: () => _showPushCapDialog(context),
|
|
),
|
|
];
|
|
}
|
|
|
|
List<Widget> _buildTilawahGroupItems(bool isDark) {
|
|
final forceTilawahAutoSync = !_settings.simpleMode;
|
|
return [
|
|
_settingRow(
|
|
isDark,
|
|
icon: LucideIcons.bookOpen,
|
|
iconColor: Colors.amber,
|
|
title: 'Target Tilawah',
|
|
subtitle:
|
|
'${_settings.tilawahTargetValue} ${_settings.tilawahTargetUnit}',
|
|
trailing: const Icon(LucideIcons.chevronRight, size: 20),
|
|
onTap: () => _showTilawahDialog(context),
|
|
),
|
|
const SizedBox(height: 10),
|
|
_settingRow(
|
|
isDark,
|
|
icon: LucideIcons.refreshCw,
|
|
iconColor: Colors.blue,
|
|
title: 'Auto-Sync Tilawah',
|
|
subtitle: forceTilawahAutoSync
|
|
? 'Mode Lengkap: selalu aktif'
|
|
: 'Catat otomatis dari menu Al-Quran',
|
|
trailing: IgnorePointer(
|
|
ignoring: forceTilawahAutoSync,
|
|
child: Opacity(
|
|
opacity: forceTilawahAutoSync ? 0.78 : 1,
|
|
child: IosToggle(
|
|
value: forceTilawahAutoSync ? true : _settings.tilawahAutoSync,
|
|
onChanged: (v) {
|
|
_settings.tilawahAutoSync = v;
|
|
_saveSettings();
|
|
_syncTodayTilawahAutoSync(v);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 10),
|
|
_settingRow(
|
|
isDark,
|
|
icon: LucideIcons.arrowRightCircle,
|
|
iconColor: Colors.green,
|
|
title: 'Lanjut Surah Otomatis',
|
|
trailing: IosToggle(
|
|
value: _settings.tilawahAutoContinueNextSurah,
|
|
onChanged: (v) {
|
|
_settings.tilawahAutoContinueNextSurah = v;
|
|
_saveSettings();
|
|
},
|
|
),
|
|
),
|
|
];
|
|
}
|
|
|
|
List<Widget> _buildDzikirGroupItems(bool isDark) {
|
|
final displayMode =
|
|
_normalizeDzikirDisplayMode(_settings.dzikirDisplayMode);
|
|
final counterButtonPosition = _normalizeDzikirCounterButtonPosition(
|
|
_settings.dzikirCounterButtonPosition,
|
|
);
|
|
return [
|
|
_buildSegmentSettingCard(
|
|
isDark,
|
|
title: 'Mode Tampilan Dzikir',
|
|
subtitle: 'Pilih tampilan dzikir: daftar atau slide fokus',
|
|
value: displayMode,
|
|
options: const {'list': 'Daftar', 'focus': 'Slide'},
|
|
onChanged: (value) {
|
|
_settings.dzikirDisplayMode = value;
|
|
_saveSettings();
|
|
},
|
|
),
|
|
if (displayMode == 'focus') ...[
|
|
const SizedBox(height: 10),
|
|
_buildSegmentSettingCard(
|
|
isDark,
|
|
title: 'Posisi Tombol Hitung',
|
|
subtitle: 'Pilih letak tombol hitung pada mode slide',
|
|
value: counterButtonPosition,
|
|
options: const {
|
|
'bottomPill': 'Pill Bawah',
|
|
'fabCircle': 'Lingkaran Kanan',
|
|
},
|
|
onChanged: (value) {
|
|
_settings.dzikirCounterButtonPosition = value;
|
|
_saveSettings();
|
|
},
|
|
),
|
|
],
|
|
const SizedBox(height: 10),
|
|
_settingRow(
|
|
isDark,
|
|
icon: LucideIcons.vibrate,
|
|
iconColor: const Color(0xFF8E44AD),
|
|
title: 'Haptic Saat Hitung',
|
|
subtitle: 'Aktifkan getaran kecil saat tombol hitung ditekan',
|
|
trailing: IosToggle(
|
|
value: _settings.dzikirHapticOnCount,
|
|
onChanged: (v) {
|
|
_settings.dzikirHapticOnCount = v;
|
|
_saveSettings();
|
|
},
|
|
),
|
|
),
|
|
const SizedBox(height: 10),
|
|
_settingRow(
|
|
isDark,
|
|
icon: LucideIcons.skipForward,
|
|
iconColor: const Color(0xFF16A085),
|
|
title: 'Auto-Advance Dzikir',
|
|
subtitle: 'Aktifkan lanjut otomatis saat hitungan dzikir selesai',
|
|
trailing: IosToggle(
|
|
value: _settings.dzikirAutoAdvance,
|
|
onChanged: (v) {
|
|
_settings.dzikirAutoAdvance = v;
|
|
_saveSettings();
|
|
},
|
|
),
|
|
),
|
|
const SizedBox(height: 10),
|
|
_settingRow(
|
|
isDark,
|
|
icon: LucideIcons.listChecks,
|
|
iconColor: Colors.indigo,
|
|
title: 'Amalan Tambahan',
|
|
subtitle: 'Kelola dzikir tambahan dan puasa sunnah',
|
|
trailing: const Icon(LucideIcons.chevronRight, size: 20),
|
|
onTap: () => _showAmalanDialog(context),
|
|
),
|
|
];
|
|
}
|
|
|
|
List<Widget> _buildDisplayGroupItems(bool isDark) {
|
|
return [
|
|
_settingRow(
|
|
isDark,
|
|
icon: LucideIcons.moon,
|
|
iconColor: const Color(0xFF6C5CE7),
|
|
title: 'Mode Gelap',
|
|
trailing: IosToggle(
|
|
value: _isDarkMode,
|
|
onChanged: _toggleDarkMode,
|
|
),
|
|
),
|
|
const SizedBox(height: 10),
|
|
_settingRow(
|
|
isDark,
|
|
icon: LucideIcons.type,
|
|
iconColor: const Color(0xFF636E72),
|
|
title: 'Ukuran Font Arab',
|
|
subtitle: '${_settings.arabicFontSize.round()}pt',
|
|
),
|
|
const SizedBox(height: 10),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
|
decoration: BoxDecoration(
|
|
color: isDark ? AppColors.surfaceDark : AppColors.surfaceLight,
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(
|
|
color: isDark
|
|
? AppColors.primary.withValues(alpha: 0.08)
|
|
: AppColors.cream,
|
|
),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
const Text(
|
|
'18',
|
|
style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600),
|
|
),
|
|
Expanded(
|
|
child: Slider(
|
|
value: _settings.arabicFontSize,
|
|
min: 18,
|
|
max: 40,
|
|
divisions: 22,
|
|
label: '${_settings.arabicFontSize.round()}',
|
|
activeColor: AppColors.primary,
|
|
onChanged: (v) {
|
|
_settings.arabicFontSize = v;
|
|
_saveSettings();
|
|
},
|
|
),
|
|
),
|
|
const Text(
|
|
'40',
|
|
style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
];
|
|
}
|
|
|
|
List<Widget> _buildAccountDataGroupItems(bool isDark) {
|
|
return [
|
|
_settingRow(
|
|
isDark,
|
|
icon: LucideIcons.user,
|
|
iconColor: AppColors.primary,
|
|
title: 'Profil',
|
|
subtitle: _settings.userName,
|
|
trailing: const Icon(LucideIcons.chevronRight, size: 20),
|
|
onTap: () => _showEditProfileDialog(context),
|
|
),
|
|
const SizedBox(height: 10),
|
|
_settingRow(
|
|
isDark,
|
|
icon: LucideIcons.info,
|
|
iconColor: AppColors.sage,
|
|
title: 'Versi Aplikasi',
|
|
subtitle: '1.0.8',
|
|
),
|
|
const SizedBox(height: 10),
|
|
_settingRow(
|
|
isDark,
|
|
icon: LucideIcons.trash2,
|
|
iconColor: Colors.red,
|
|
title: 'Reset Semua Data',
|
|
subtitle: 'Hapus riwayat dan reset pengaturan',
|
|
trailing: const Icon(LucideIcons.chevronRight, size: 20),
|
|
onTap: () => _showResetDialog(context),
|
|
),
|
|
];
|
|
}
|
|
|
|
Widget _buildGroupEntry(
|
|
bool isDark, {
|
|
required IconData icon,
|
|
required Color iconColor,
|
|
required String title,
|
|
required String subtitle,
|
|
required VoidCallback onTap,
|
|
}) {
|
|
return _settingRow(
|
|
isDark,
|
|
icon: icon,
|
|
iconColor: iconColor,
|
|
title: title,
|
|
subtitle: subtitle,
|
|
trailing: const Icon(LucideIcons.chevronRight, size: 20),
|
|
onTap: onTap,
|
|
);
|
|
}
|
|
|
|
Widget _sectionLabel(String text) {
|
|
return Text(
|
|
text,
|
|
style: TextStyle(
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w700,
|
|
letterSpacing: 1.5,
|
|
color: AppColors.sage,
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _settingRow(
|
|
bool isDark, {
|
|
required IconData icon,
|
|
required Color iconColor,
|
|
required String title,
|
|
String? subtitle,
|
|
Widget? trailing,
|
|
VoidCallback? onTap,
|
|
}) {
|
|
return GestureDetector(
|
|
onTap: onTap,
|
|
child: Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: isDark ? AppColors.surfaceDark : AppColors.surfaceLight,
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(
|
|
color: isDark
|
|
? AppColors.primary.withValues(alpha: 0.08)
|
|
: AppColors.cream,
|
|
),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
width: 40,
|
|
height: 40,
|
|
decoration: BoxDecoration(
|
|
color: iconColor.withValues(alpha: 0.12),
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
child: Icon(icon, color: iconColor, size: 20),
|
|
),
|
|
const SizedBox(width: 14),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
title,
|
|
style: const TextStyle(
|
|
fontSize: 15,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
if (subtitle != null)
|
|
Text(
|
|
subtitle,
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: isDark
|
|
? AppColors.textSecondaryDark
|
|
: AppColors.textSecondaryLight,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
if (trailing != null) trailing,
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildSegmentSettingCard(
|
|
bool isDark, {
|
|
required String title,
|
|
String? subtitle,
|
|
required String value,
|
|
required Map<String, String> options,
|
|
required ValueChanged<String> onChanged,
|
|
}) {
|
|
return Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: isDark ? AppColors.surfaceDark : AppColors.surfaceLight,
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(
|
|
color: isDark
|
|
? AppColors.primary.withValues(alpha: 0.08)
|
|
: AppColors.cream,
|
|
),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
title,
|
|
style: const TextStyle(
|
|
fontSize: 15,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
if (subtitle != null) ...[
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
subtitle,
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: isDark
|
|
? AppColors.textSecondaryDark
|
|
: AppColors.textSecondaryLight,
|
|
),
|
|
),
|
|
],
|
|
const SizedBox(height: 12),
|
|
Container(
|
|
padding: const EdgeInsets.all(4),
|
|
decoration: BoxDecoration(
|
|
color:
|
|
isDark ? AppColors.backgroundDark : AppColors.backgroundLight,
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(
|
|
color: isDark
|
|
? AppColors.primary.withValues(alpha: 0.08)
|
|
: AppColors.cream,
|
|
),
|
|
),
|
|
child: Row(
|
|
children: options.entries.map((entry) {
|
|
final selected = value == entry.key;
|
|
return Expanded(
|
|
child: GestureDetector(
|
|
onTap: () => onChanged(entry.key),
|
|
child: AnimatedContainer(
|
|
duration: const Duration(milliseconds: 160),
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 8,
|
|
vertical: 10,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color:
|
|
selected ? AppColors.primary : Colors.transparent,
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
child: Text(
|
|
entry.value,
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w700,
|
|
color: selected
|
|
? AppColors.onPrimary
|
|
: (isDark
|
|
? AppColors.textPrimaryDark
|
|
: AppColors.textPrimaryLight),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}).toList(),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showMethodDialog(BuildContext context) {
|
|
showDialog(
|
|
context: context,
|
|
builder: (ctx) => AlertDialog(
|
|
insetPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 24),
|
|
title: const Text('Metode Perhitungan'),
|
|
content: SizedBox(
|
|
width: MediaQuery.of(context).size.width * 0.85,
|
|
child: const Text(
|
|
'Aplikasi ini menggunakan data resmi dari Kementerian Agama RI (Kemenag) melalui API myQuran.\n\nData Kemenag sudah standar dan akurat untuk seluruh wilayah Indonesia, sehingga tidak perlu diubah.',
|
|
),
|
|
),
|
|
actions: [
|
|
FilledButton(
|
|
onPressed: () => Navigator.pop(ctx),
|
|
child: const Text('Tutup'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showLocationDialog(BuildContext context) {
|
|
final searchCtrl = TextEditingController();
|
|
bool isSearching = false;
|
|
bool isDetecting = false;
|
|
String? currentCityLabel;
|
|
String? currentCityId;
|
|
String? currentCityError;
|
|
List<Map<String, dynamic>> results = [];
|
|
Timer? debounce;
|
|
|
|
showDialog(
|
|
context: context,
|
|
builder: (ctx) => StatefulBuilder(
|
|
builder: (ctx, setDialogState) => AlertDialog(
|
|
insetPadding:
|
|
const EdgeInsets.symmetric(horizontal: 20, vertical: 24),
|
|
title: const Text('Cari Kota/Kabupaten'),
|
|
content: SizedBox(
|
|
width: MediaQuery.of(context).size.width * 0.85,
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: AppColors.primary.withValues(alpha: 0.08),
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(
|
|
color: AppColors.primary.withValues(alpha: 0.2),
|
|
),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
const Icon(
|
|
LucideIcons.mapPin,
|
|
size: 16,
|
|
color: AppColors.primary,
|
|
),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: Text(
|
|
currentCityLabel == null
|
|
? 'Deteksi lokasi saat ini'
|
|
: 'Anda di $currentCityLabel',
|
|
style: const TextStyle(
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
),
|
|
if (isDetecting)
|
|
const SizedBox(
|
|
width: 16,
|
|
height: 16,
|
|
child: CircularProgressIndicator(strokeWidth: 2),
|
|
),
|
|
],
|
|
),
|
|
if (currentCityError != null) ...[
|
|
const SizedBox(height: 6),
|
|
Text(
|
|
currentCityError!,
|
|
style: const TextStyle(
|
|
fontSize: 12,
|
|
color: Colors.red,
|
|
),
|
|
),
|
|
],
|
|
const SizedBox(height: 8),
|
|
Row(
|
|
children: [
|
|
TextButton(
|
|
onPressed: isDetecting
|
|
? null
|
|
: () async {
|
|
setDialogState(() {
|
|
isDetecting = true;
|
|
currentCityError = null;
|
|
});
|
|
try {
|
|
final pos = await LocationService.instance
|
|
.getCurrentLocation();
|
|
final fallbackLocation = pos == null
|
|
? LocationService.instance
|
|
.getLastKnownLocation()
|
|
: null;
|
|
final lat = pos?.latitude ??
|
|
fallbackLocation?.lat;
|
|
final lng = pos?.longitude ??
|
|
fallbackLocation?.lng;
|
|
if (lat == null || lng == null) {
|
|
setDialogState(() {
|
|
currentCityError =
|
|
'Lokasi tidak tersedia. Pastikan izin lokasi aktif.';
|
|
isDetecting = false;
|
|
});
|
|
return;
|
|
}
|
|
final detectedLabel =
|
|
(await LocationService.instance
|
|
.getCityName(lat, lng))
|
|
.split(',')
|
|
.first
|
|
.trim();
|
|
final resolved = await LocationService
|
|
.instance
|
|
.resolveMyQuranCityFromCoordinates(
|
|
lat: lat,
|
|
lng: lng,
|
|
);
|
|
if (resolved == null) {
|
|
setDialogState(() {
|
|
final fallbackCity =
|
|
detectedLabel.isEmpty
|
|
? 'lokasi saat ini'
|
|
: detectedLabel;
|
|
currentCityError =
|
|
'Lokasi Anda terdeteksi di $fallbackCity. Kami belum menemukan ID kota yang cocok di data jadwal. Silakan cari manual kota terdekat.';
|
|
isDetecting = false;
|
|
});
|
|
return;
|
|
}
|
|
setDialogState(() {
|
|
currentCityId = resolved.id;
|
|
currentCityLabel = resolved.name;
|
|
isDetecting = false;
|
|
});
|
|
} catch (_) {
|
|
setDialogState(() {
|
|
currentCityError =
|
|
'Deteksi lokasi gagal. Coba lagi.';
|
|
isDetecting = false;
|
|
});
|
|
}
|
|
},
|
|
child: const Text('Deteksi Lokasi'),
|
|
),
|
|
const SizedBox(width: 8),
|
|
if (currentCityId != null && currentCityLabel != null)
|
|
FilledButton(
|
|
onPressed: () {
|
|
_settings.lastCityName =
|
|
'$currentCityLabel|$currentCityId';
|
|
_saveSettings();
|
|
ref.invalidate(selectedCityIdProvider);
|
|
ref.invalidate(cityNameProvider);
|
|
ref.invalidate(prayerTimesProvider);
|
|
unawaited(ref.read(prayerTimesProvider.future));
|
|
Navigator.pop(ctx);
|
|
},
|
|
child: const Text('Gunakan Lokasi Ini'),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
TextField(
|
|
controller: searchCtrl,
|
|
autofocus: true,
|
|
decoration: InputDecoration(
|
|
hintText: 'Cth: Jakarta',
|
|
border: const OutlineInputBorder(),
|
|
suffixIcon: IconButton(
|
|
icon: const Icon(LucideIcons.search),
|
|
onPressed: () async {
|
|
if (searchCtrl.text.trim().isEmpty) return;
|
|
setDialogState(() => isSearching = true);
|
|
final res = await MyQuranSholatService.instance
|
|
.searchCity(searchCtrl.text.trim());
|
|
setDialogState(() {
|
|
results = res;
|
|
isSearching = false;
|
|
});
|
|
},
|
|
),
|
|
),
|
|
onChanged: (val) {
|
|
if (val.trim().length < 3) return;
|
|
|
|
if (debounce?.isActive ?? false) debounce!.cancel();
|
|
debounce =
|
|
Timer(const Duration(milliseconds: 500), () async {
|
|
if (!mounted) return;
|
|
setDialogState(() => isSearching = true);
|
|
|
|
try {
|
|
final res = await MyQuranSholatService.instance
|
|
.searchCity(val.trim());
|
|
if (mounted) {
|
|
setDialogState(() {
|
|
results = res;
|
|
});
|
|
}
|
|
} catch (e) {
|
|
debugPrint('Error searching city: $e');
|
|
} finally {
|
|
if (mounted) {
|
|
setDialogState(() {
|
|
isSearching = false;
|
|
});
|
|
}
|
|
}
|
|
});
|
|
},
|
|
onSubmitted: (val) async {
|
|
if (val.trim().isEmpty) return;
|
|
if (debounce?.isActive ?? false) debounce!.cancel();
|
|
setDialogState(() => isSearching = true);
|
|
final res = await MyQuranSholatService.instance
|
|
.searchCity(val.trim());
|
|
|
|
if (mounted) {
|
|
setDialogState(() {
|
|
results = res;
|
|
isSearching = false;
|
|
});
|
|
}
|
|
},
|
|
),
|
|
const SizedBox(height: 16),
|
|
if (isSearching)
|
|
const Center(child: CircularProgressIndicator())
|
|
else if (results.isEmpty)
|
|
const Text('Tidak ada hasil',
|
|
style: TextStyle(color: Colors.grey))
|
|
else
|
|
SizedBox(
|
|
height: 200,
|
|
width: double.maxFinite,
|
|
child: ListView.builder(
|
|
shrinkWrap: true,
|
|
itemCount: results.length,
|
|
itemBuilder: (context, i) {
|
|
final city = results[i];
|
|
return ListTile(
|
|
title: Text(city['lokasi'] ?? ''),
|
|
onTap: () {
|
|
final id = city['id'];
|
|
final name = city['lokasi'];
|
|
if (id != null && name != null) {
|
|
_settings.lastCityName = '$name|$id';
|
|
_saveSettings();
|
|
|
|
// Update providers to refresh data
|
|
ref.invalidate(selectedCityIdProvider);
|
|
ref.invalidate(cityNameProvider);
|
|
ref.invalidate(prayerTimesProvider);
|
|
unawaited(ref.read(prayerTimesProvider.future));
|
|
|
|
Navigator.pop(ctx);
|
|
}
|
|
},
|
|
);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(ctx),
|
|
child: const Text('Batal'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showEditProfileDialog(BuildContext context) {
|
|
final nameCtrl = TextEditingController(text: _settings.userName);
|
|
final emailCtrl = TextEditingController(text: _settings.userEmail);
|
|
showDialog(
|
|
context: context,
|
|
builder: (ctx) => AlertDialog(
|
|
insetPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 24),
|
|
title: const Text('Edit Profil'),
|
|
content: SizedBox(
|
|
width: MediaQuery.of(context).size.width * 0.85,
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
TextField(
|
|
controller: nameCtrl,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Nama',
|
|
border: OutlineInputBorder(),
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
TextField(
|
|
controller: emailCtrl,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Email',
|
|
border: OutlineInputBorder(),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(ctx),
|
|
child: const Text('Batal'),
|
|
),
|
|
FilledButton(
|
|
onPressed: () {
|
|
_settings.userName = nameCtrl.text.trim();
|
|
_settings.userEmail = emailCtrl.text.trim();
|
|
_saveSettings();
|
|
Navigator.pop(ctx);
|
|
},
|
|
child: const Text('Simpan'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showIqamahDialog(BuildContext context) {
|
|
final offsets = Map<String, int>.from(_settings.iqamahOffset);
|
|
showDialog(
|
|
context: context,
|
|
builder: (ctx) => StatefulBuilder(
|
|
builder: (ctx, setDialogState) => AlertDialog(
|
|
insetPadding:
|
|
const EdgeInsets.symmetric(horizontal: 20, vertical: 24),
|
|
title: const Text('Waktu Iqamah (menit)'),
|
|
content: SizedBox(
|
|
width: MediaQuery.of(context).size.width * 0.85,
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: offsets.entries.map((e) {
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
|
child: Row(
|
|
children: [
|
|
SizedBox(
|
|
width: 80,
|
|
child: Text(
|
|
e.key[0].toUpperCase() + e.key.substring(1),
|
|
style: const TextStyle(fontWeight: FontWeight.w600),
|
|
),
|
|
),
|
|
Expanded(
|
|
child: Slider(
|
|
value: e.value.toDouble(),
|
|
min: 0,
|
|
max: 30,
|
|
divisions: 30,
|
|
label: '${e.value} min',
|
|
activeColor: AppColors.primary,
|
|
onChanged: (v) {
|
|
setDialogState(() {
|
|
offsets[e.key] = v.round();
|
|
});
|
|
},
|
|
),
|
|
),
|
|
SizedBox(
|
|
width: 40,
|
|
child: Text(
|
|
'${e.value}m',
|
|
style: const TextStyle(fontWeight: FontWeight.w600),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}).toList(),
|
|
),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(ctx),
|
|
child: const Text('Batal'),
|
|
),
|
|
FilledButton(
|
|
onPressed: () {
|
|
_settings.iqamahOffset = offsets;
|
|
_saveSettings();
|
|
ref.invalidate(prayerTimesProvider);
|
|
unawaited(ref.read(prayerTimesProvider.future));
|
|
Navigator.pop(ctx);
|
|
},
|
|
child: const Text('Simpan'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showRawatibDialog(BuildContext context) {
|
|
int tempLevel = _settings.rawatibLevel;
|
|
showDialog(
|
|
context: context,
|
|
builder: (ctx) => StatefulBuilder(
|
|
builder: (ctx, setDialogState) => AlertDialog(
|
|
title: Row(
|
|
children: [
|
|
const Text('Sholat Rawatib', style: TextStyle(fontSize: 18)),
|
|
const Spacer(),
|
|
IconButton(
|
|
icon: const Icon(LucideIcons.info, color: AppColors.primary),
|
|
onPressed: () {
|
|
showModalBottomSheet(
|
|
context: context,
|
|
isScrollControlled: true,
|
|
useSafeArea: true,
|
|
builder: (bCtx) {
|
|
final keyboardInset =
|
|
MediaQuery.of(bCtx).viewInsets.bottom;
|
|
return Padding(
|
|
padding:
|
|
EdgeInsets.fromLTRB(24, 24, 24, 24 + keyboardInset),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text('Informasi Sholat Rawatib',
|
|
style: TextStyle(
|
|
fontSize: 18, fontWeight: FontWeight.bold)),
|
|
const SizedBox(height: 16),
|
|
const Text('Muakkad (Sangat Ditekankan)',
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
color: AppColors.primary)),
|
|
const SizedBox(height: 8),
|
|
const Text('Total 10 atau 12 Rakaat:'),
|
|
const Padding(
|
|
padding: EdgeInsets.only(left: 12, top: 4),
|
|
child: Text(
|
|
'• 2 Rakaat sebelum Subuh\n• 2 atau 4 Rakaat sebelum Dzuhur\n• 2 Rakaat sesudah Dzuhur\n• 2 Rakaat sesudah Maghrib\n• 2 Rakaat sesudah Isya',
|
|
style: TextStyle(height: 1.5)),
|
|
),
|
|
const SizedBox(height: 16),
|
|
const Text('Ghairu Muakkad (Tambahan)',
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
color: AppColors.primary)),
|
|
const SizedBox(height: 8),
|
|
const Padding(
|
|
padding: EdgeInsets.only(left: 12),
|
|
child: Text(
|
|
'• Tambahan 2 Rakaat sesudah Dzuhur\n• 4 Rakaat sebelum Ashar\n• 2 Rakaat sebelum Maghrib\n• 2 Rakaat sebelum Isya',
|
|
style: TextStyle(height: 1.5)),
|
|
),
|
|
const SizedBox(height: 24),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
content: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
RadioListTile<int>(
|
|
title: const Text('Mati (Tanpa Rawatib)'),
|
|
value: 0,
|
|
groupValue: tempLevel,
|
|
onChanged: (v) => setDialogState(() => tempLevel = v!),
|
|
),
|
|
RadioListTile<int>(
|
|
title: const Text('Muakkad Saja'),
|
|
value: 1,
|
|
groupValue: tempLevel,
|
|
onChanged: (v) => setDialogState(() => tempLevel = v!),
|
|
),
|
|
RadioListTile<int>(
|
|
title: const Text('Lengkap (Semua)'),
|
|
value: 2,
|
|
groupValue: tempLevel,
|
|
onChanged: (v) => setDialogState(() => tempLevel = v!),
|
|
),
|
|
],
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(ctx),
|
|
child: const Text('Batal'),
|
|
),
|
|
FilledButton(
|
|
onPressed: () {
|
|
_settings.rawatibLevel = tempLevel;
|
|
_saveSettings();
|
|
Navigator.pop(ctx);
|
|
},
|
|
child: const Text('Simpan'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showTilawahDialog(BuildContext context) {
|
|
final qtyCtrl =
|
|
TextEditingController(text: _settings.tilawahTargetValue.toString());
|
|
String tempUnit = _settings.tilawahTargetUnit;
|
|
|
|
showDialog(
|
|
context: context,
|
|
builder: (ctx) => StatefulBuilder(
|
|
builder: (ctx, setDialogState) => AlertDialog(
|
|
title: const Text('Target Tilawah Harian'),
|
|
content: Row(
|
|
children: [
|
|
Expanded(
|
|
flex: 1,
|
|
child: TextField(
|
|
controller: qtyCtrl,
|
|
keyboardType: TextInputType.number,
|
|
decoration:
|
|
const InputDecoration(border: OutlineInputBorder()),
|
|
),
|
|
),
|
|
const SizedBox(width: 16),
|
|
Expanded(
|
|
flex: 2,
|
|
child: DropdownButtonFormField<String>(
|
|
value: tempUnit,
|
|
decoration:
|
|
const InputDecoration(border: OutlineInputBorder()),
|
|
items: ['Juz', 'Halaman', 'Ayat']
|
|
.map((u) => DropdownMenuItem(value: u, child: Text(u)))
|
|
.toList(),
|
|
onChanged: (v) => setDialogState(() => tempUnit = v!),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(ctx),
|
|
child: const Text('Batal'),
|
|
),
|
|
FilledButton(
|
|
onPressed: () {
|
|
final qty = int.tryParse(qtyCtrl.text.trim()) ?? 1;
|
|
_settings.tilawahTargetValue = qty > 0 ? qty : 1;
|
|
_settings.tilawahTargetUnit = tempUnit;
|
|
_saveSettings();
|
|
|
|
// Update today's active checklist immediately
|
|
final todayKey =
|
|
DateFormat('yyyy-MM-dd').format(DateTime.now());
|
|
final logBox = Hive.box<DailyWorshipLog>(HiveBoxes.worshipLogs);
|
|
final log = logBox.get(todayKey);
|
|
if (log != null && log.tilawahLog != null) {
|
|
log.tilawahLog!.targetValue = _settings.tilawahTargetValue;
|
|
log.tilawahLog!.targetUnit = _settings.tilawahTargetUnit;
|
|
log.save();
|
|
}
|
|
|
|
Navigator.pop(ctx);
|
|
},
|
|
child: const Text('Simpan'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showAmalanDialog(BuildContext context) {
|
|
bool tDzikir = _settings.trackDzikir;
|
|
bool tPuasa = _settings.trackPuasa;
|
|
|
|
showDialog(
|
|
context: context,
|
|
builder: (ctx) => StatefulBuilder(
|
|
builder: (ctx, setDialogState) => AlertDialog(
|
|
title: const Text('Amalan Tambahan'),
|
|
content: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
SwitchListTile(
|
|
title: const Text('Dzikir Pagi & Petang'),
|
|
value: tDzikir,
|
|
onChanged: (v) => setDialogState(() => tDzikir = v),
|
|
),
|
|
SwitchListTile(
|
|
title: const Text('Puasa Sunnah'),
|
|
value: tPuasa,
|
|
onChanged: (v) => setDialogState(() => tPuasa = v),
|
|
),
|
|
],
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(ctx),
|
|
child: const Text('Batal'),
|
|
),
|
|
FilledButton(
|
|
onPressed: () {
|
|
_settings.trackDzikir = tDzikir;
|
|
_settings.trackPuasa = tPuasa;
|
|
_saveSettings();
|
|
Navigator.pop(ctx);
|
|
},
|
|
child: const Text('Simpan'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showResetDialog(BuildContext context) {
|
|
showDialog(
|
|
context: context,
|
|
builder: (ctx) => AlertDialog(
|
|
title: const Text('Hapus Semua Data?'),
|
|
content: const Text(
|
|
'Ini akan menghapus semua riwayat ibadah, marka quran, penghitung dzikir, dan mereset pengaturan. Tindakan ini tidak dapat dibatalkan.',
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(ctx),
|
|
child: const Text('Batal'),
|
|
),
|
|
FilledButton(
|
|
style: FilledButton.styleFrom(backgroundColor: Colors.red),
|
|
onPressed: () async {
|
|
await Hive.box(HiveBoxes.worshipLogs).clear();
|
|
await Hive.box(HiveBoxes.bookmarks).clear();
|
|
await Hive.box(HiveBoxes.dzikirCounters).clear();
|
|
final box = Hive.box<AppSettings>(HiveBoxes.settings);
|
|
await box.clear();
|
|
await box.put('default', AppSettings());
|
|
setState(() {
|
|
_settings = box.get('default')!;
|
|
});
|
|
ref.read(themeProvider.notifier).state = ThemeMode.system;
|
|
if (ctx.mounted) Navigator.pop(ctx);
|
|
},
|
|
child: const Text('Hapus'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|