1101 lines
38 KiB
Dart
1101 lines
38 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/myquran_sholat_service.dart';
|
|
import '../../../data/services/myquran_sholat_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();
|
|
}
|
|
|
|
void _saveSettings() {
|
|
_settings.save();
|
|
setState(() {});
|
|
}
|
|
|
|
bool get _isDarkMode => _settings.themeModeIndex != 1;
|
|
bool get _notificationsEnabled =>
|
|
_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();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
|
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: const Text('Pengaturan'),
|
|
),
|
|
body: ListView(
|
|
padding: const EdgeInsets.all(16),
|
|
children: [
|
|
// ── Profile Card ──
|
|
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: [
|
|
// Avatar
|
|
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),
|
|
|
|
// ── PREFERENCES ──
|
|
_sectionLabel('PREFERENSI'),
|
|
const SizedBox(height: 12),
|
|
_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;
|
|
_saveSettings();
|
|
},
|
|
),
|
|
),
|
|
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: 10),
|
|
_settingRow(
|
|
isDark,
|
|
icon: LucideIcons.bell,
|
|
iconColor: const Color(0xFFE17055),
|
|
title: 'Notifikasi',
|
|
trailing: IosToggle(
|
|
value: _notificationsEnabled,
|
|
onChanged: _toggleNotifications,
|
|
),
|
|
),
|
|
const SizedBox(height: 24),
|
|
|
|
// ── CHECKLIST IBADAH (always visible, even in Simple Mode per user request) ──
|
|
_sectionLabel('CHECKLIST IBADAH'),
|
|
const SizedBox(height: 12),
|
|
_settingRow(
|
|
isDark,
|
|
icon: LucideIcons.building,
|
|
iconColor: Colors.teal,
|
|
title: 'Tingkat Sholat Rawatib',
|
|
subtitle: _settings.rawatibLevel == 0 ? 'Mati' : (_settings.rawatibLevel == 1 ? 'Muakkad Saja' : 'Lengkap (Semua)'),
|
|
trailing: const Icon(LucideIcons.chevronRight, size: 20),
|
|
onTap: () => _showRawatibDialog(context),
|
|
),
|
|
const SizedBox(height: 10),
|
|
_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: 'Catat otomatis dari menu Al-Quran',
|
|
trailing: IosToggle(
|
|
value: _settings.tilawahAutoSync,
|
|
onChanged: (v) {
|
|
_settings.tilawahAutoSync = v;
|
|
_saveSettings();
|
|
|
|
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 = v;
|
|
log.save();
|
|
}
|
|
},
|
|
),
|
|
),
|
|
const SizedBox(height: 10),
|
|
_settingRow(
|
|
isDark,
|
|
icon: LucideIcons.listChecks,
|
|
iconColor: Colors.indigo,
|
|
title: 'Amalan Tambahan',
|
|
subtitle: 'Dzikir & Puasa Sunnah',
|
|
trailing: const Icon(LucideIcons.chevronRight, size: 20),
|
|
onTap: () => _showAmalanDialog(context),
|
|
),
|
|
const SizedBox(height: 24),
|
|
|
|
// ── DZIKIR DISPLAY ──
|
|
_sectionLabel('TAMPILAN DZIKIR'),
|
|
const SizedBox(height: 12),
|
|
_buildSegmentSettingCard(
|
|
isDark,
|
|
title: 'Mode Tampilan Dzikir',
|
|
subtitle: 'Pilih daftar baris atau fokus per slide',
|
|
value: _settings.dzikirDisplayMode,
|
|
options: const {
|
|
'list': 'Daftar (Baris)',
|
|
'focus': 'Fokus (Slide)',
|
|
},
|
|
onChanged: (value) {
|
|
_settings.dzikirDisplayMode = value;
|
|
_saveSettings();
|
|
},
|
|
),
|
|
if (_settings.dzikirDisplayMode == 'focus') ...[
|
|
const SizedBox(height: 10),
|
|
_buildSegmentSettingCard(
|
|
isDark,
|
|
title: 'Posisi Tombol Hitung',
|
|
subtitle: 'Atur posisi tombol pada mode fokus',
|
|
value: _settings.dzikirCounterButtonPosition,
|
|
options: const {
|
|
'bottomPill': 'Pill Bawah',
|
|
'fabCircle': 'Bulat Kanan Bawah',
|
|
},
|
|
onChanged: (value) {
|
|
_settings.dzikirCounterButtonPosition = value;
|
|
_saveSettings();
|
|
},
|
|
),
|
|
const SizedBox(height: 10),
|
|
_settingRow(
|
|
isDark,
|
|
icon: LucideIcons.arrowRight,
|
|
iconColor: const Color(0xFF00B894),
|
|
title: 'Lanjut Otomatis Saat Target Tercapai',
|
|
trailing: IosToggle(
|
|
value: _settings.dzikirAutoAdvance,
|
|
onChanged: (v) {
|
|
_settings.dzikirAutoAdvance = v;
|
|
_saveSettings();
|
|
},
|
|
),
|
|
),
|
|
],
|
|
const SizedBox(height: 10),
|
|
_settingRow(
|
|
isDark,
|
|
icon: LucideIcons.vibrate,
|
|
iconColor: const Color(0xFF6C5CE7),
|
|
title: 'Getaran Saat Hitung',
|
|
trailing: IosToggle(
|
|
value: _settings.dzikirHapticOnCount,
|
|
onChanged: (v) {
|
|
_settings.dzikirHapticOnCount = v;
|
|
_saveSettings();
|
|
},
|
|
),
|
|
),
|
|
const SizedBox(height: 24),
|
|
|
|
// ── PRAYER SETTINGS ──
|
|
_sectionLabel('WAKTU SHOLAT'),
|
|
const SizedBox(height: 12),
|
|
_settingRow(
|
|
isDark,
|
|
icon: LucideIcons.building,
|
|
iconColor: AppColors.primary,
|
|
title: 'Metode Perhitungan',
|
|
subtitle: 'Kemenag RI',
|
|
trailing: const Icon(LucideIcons.chevronRight, size: 20),
|
|
onTap: () => _showMethodDialog(context),
|
|
),
|
|
const SizedBox(height: 10),
|
|
_settingRow(
|
|
isDark,
|
|
icon: LucideIcons.mapPin,
|
|
iconColor: const Color(0xFF00B894),
|
|
title: 'Lokasi',
|
|
subtitle: _displayCityName,
|
|
trailing: const Icon(LucideIcons.chevronRight, size: 20),
|
|
onTap: () => _showLocationDialog(context),
|
|
),
|
|
const SizedBox(height: 10),
|
|
_settingRow(
|
|
isDark,
|
|
icon: LucideIcons.timer,
|
|
iconColor: const Color(0xFFFDAA5E),
|
|
title: 'Waktu Iqamah',
|
|
subtitle: 'Atur per waktu sholat',
|
|
trailing: const Icon(LucideIcons.chevronRight, size: 20),
|
|
onTap: () => _showIqamahDialog(context),
|
|
),
|
|
const SizedBox(height: 24),
|
|
|
|
// ── DISPLAY ──
|
|
_sectionLabel('TAMPILAN'),
|
|
const SizedBox(height: 12),
|
|
_settingRow(
|
|
isDark,
|
|
icon: LucideIcons.type,
|
|
iconColor: const Color(0xFF636E72),
|
|
title: 'Ukuran Font Arab',
|
|
subtitle: '${_settings.arabicFontSize.round()}pt',
|
|
trailing: SizedBox(
|
|
width: 120,
|
|
child: Slider(
|
|
value: _settings.arabicFontSize,
|
|
min: 16,
|
|
max: 40,
|
|
divisions: 12,
|
|
activeColor: AppColors.primary,
|
|
onChanged: (v) {
|
|
_settings.arabicFontSize = v;
|
|
_saveSettings();
|
|
},
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 24),
|
|
|
|
// ── ABOUT ──
|
|
_sectionLabel('TENTANG'),
|
|
const SizedBox(height: 12),
|
|
_settingRow(
|
|
isDark,
|
|
icon: LucideIcons.info,
|
|
iconColor: AppColors.sage,
|
|
title: 'Versi Aplikasi',
|
|
subtitle: '1.0.0',
|
|
),
|
|
const SizedBox(height: 10),
|
|
_settingRow(
|
|
isDark,
|
|
icon: LucideIcons.heart,
|
|
iconColor: Colors.red,
|
|
title: 'Beri Nilai Kami',
|
|
trailing: const Icon(LucideIcons.chevronRight, size: 20),
|
|
onTap: () {},
|
|
),
|
|
const SizedBox(height: 24),
|
|
|
|
// ── Reset Button ──
|
|
GestureDetector(
|
|
onTap: () => _showResetDialog(context),
|
|
child: Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(
|
|
color: Colors.red.withValues(alpha: 0.3),
|
|
width: 1.5,
|
|
),
|
|
),
|
|
child: const Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(LucideIcons.logOut, color: Colors.red, size: 20),
|
|
SizedBox(width: 8),
|
|
Text(
|
|
'Hapus Semua Data',
|
|
style: TextStyle(
|
|
color: Colors.red,
|
|
fontWeight: FontWeight.w600,
|
|
fontSize: 14,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 32),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
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;
|
|
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: [
|
|
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);
|
|
|
|
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();
|
|
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,
|
|
builder: (bCtx) => Padding(
|
|
padding: const EdgeInsets.all(24.0),
|
|
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'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|