feat: Murattal player enhancements & prayer schedule auto-scroll
- Murattal: Spotify-style 5-button controls [Shuffle, Prev, Play, Next, Playlist] - Murattal: Animated 7-bar equalizer visualization in player circle - Murattal: Unsplash API background with frosted glass player overlay - Murattal: Transparent AppBar with backdrop blur - Murattal: Surah playlist bottom sheet with full 114 Surah list - Murattal: Auto-play disabled on screen open, enabled on navigation - Murattal: Shuffle mode for random Surah playback - Murattal: Photographer attribution per Unsplash guidelines - Dashboard: Auto-scroll prayer schedule to next active prayer - Fix: setState lifecycle errors on Reading & Murattal screens - Setup: flutter_dotenv, cached_network_image, url_launcher deps
This commit is contained in:
891
lib/features/settings/presentation/settings_screen.dart
Normal file
891
lib/features/settings/presentation/settings_screen.dart
Normal file
@@ -0,0 +1,891 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:hive_flutter/hive_flutter.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(Icons.edit,
|
||||
size: 20, color: AppColors.primary),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// ── PREFERENCES ──
|
||||
_sectionLabel('PREFERENSI'),
|
||||
const SizedBox(height: 12),
|
||||
_settingRow(
|
||||
isDark,
|
||||
icon: Icons.dark_mode,
|
||||
iconColor: const Color(0xFF6C5CE7),
|
||||
title: 'Mode Gelap',
|
||||
trailing: IosToggle(
|
||||
value: _isDarkMode,
|
||||
onChanged: _toggleDarkMode,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
_settingRow(
|
||||
isDark,
|
||||
icon: Icons.notifications,
|
||||
iconColor: const Color(0xFFE17055),
|
||||
title: 'Notifikasi',
|
||||
trailing: IosToggle(
|
||||
value: _notificationsEnabled,
|
||||
onChanged: _toggleNotifications,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// ── CHECKLIST IBADAH ──
|
||||
_sectionLabel('CHECKLIST IBADAH'),
|
||||
const SizedBox(height: 12),
|
||||
_settingRow(
|
||||
isDark,
|
||||
icon: Icons.mosque_outlined,
|
||||
iconColor: Colors.teal,
|
||||
title: 'Tingkat Sholat Rawatib',
|
||||
subtitle: _settings.rawatibLevel == 0 ? 'Mati' : (_settings.rawatibLevel == 1 ? 'Muakkad Saja' : 'Lengkap (Semua)'),
|
||||
trailing: const Icon(Icons.chevron_right, size: 20),
|
||||
onTap: () => _showRawatibDialog(context),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
_settingRow(
|
||||
isDark,
|
||||
icon: Icons.menu_book,
|
||||
iconColor: Colors.amber,
|
||||
title: 'Target Tilawah',
|
||||
subtitle: '${_settings.tilawahTargetValue} ${_settings.tilawahTargetUnit}',
|
||||
trailing: const Icon(Icons.chevron_right, size: 20),
|
||||
onTap: () => _showTilawahDialog(context),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
_settingRow(
|
||||
isDark,
|
||||
icon: Icons.sync,
|
||||
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: Icons.library_add_check,
|
||||
iconColor: Colors.indigo,
|
||||
title: 'Amalan Tambahan',
|
||||
subtitle: 'Dzikir & Puasa Sunnah',
|
||||
trailing: const Icon(Icons.chevron_right, size: 20),
|
||||
onTap: () => _showAmalanDialog(context),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// ── PRAYER SETTINGS ──
|
||||
_sectionLabel('WAKTU SHOLAT'),
|
||||
const SizedBox(height: 12),
|
||||
_settingRow(
|
||||
isDark,
|
||||
icon: Icons.mosque,
|
||||
iconColor: AppColors.primary,
|
||||
title: 'Metode Perhitungan',
|
||||
subtitle: 'Kemenag RI',
|
||||
trailing: const Icon(Icons.chevron_right, size: 20),
|
||||
onTap: () => _showMethodDialog(context),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
_settingRow(
|
||||
isDark,
|
||||
icon: Icons.location_on,
|
||||
iconColor: const Color(0xFF00B894),
|
||||
title: 'Lokasi',
|
||||
subtitle: _displayCityName,
|
||||
trailing: const Icon(Icons.chevron_right, size: 20),
|
||||
onTap: () => _showLocationDialog(context),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
_settingRow(
|
||||
isDark,
|
||||
icon: Icons.timer,
|
||||
iconColor: const Color(0xFFFDAA5E),
|
||||
title: 'Waktu Iqamah',
|
||||
subtitle: 'Atur per waktu sholat',
|
||||
trailing: const Icon(Icons.chevron_right, size: 20),
|
||||
onTap: () => _showIqamahDialog(context),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// ── DISPLAY ──
|
||||
_sectionLabel('TAMPILAN'),
|
||||
const SizedBox(height: 12),
|
||||
_settingRow(
|
||||
isDark,
|
||||
icon: Icons.text_fields,
|
||||
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: Icons.info_outline,
|
||||
iconColor: AppColors.sage,
|
||||
title: 'Versi Aplikasi',
|
||||
subtitle: '1.0.0',
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
_settingRow(
|
||||
isDark,
|
||||
icon: Icons.favorite_outline,
|
||||
iconColor: Colors.red,
|
||||
title: 'Beri Nilai Kami',
|
||||
trailing: const Icon(Icons.chevron_right, 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(Icons.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,
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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 = [];
|
||||
|
||||
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(Icons.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;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
onSubmitted: (val) async {
|
||||
if (val.trim().isEmpty) return;
|
||||
setDialogState(() => isSearching = true);
|
||||
final res = await MyQuranSholatService.instance
|
||||
.searchCity(val.trim());
|
||||
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(Icons.info_outline, 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'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user