Polish navigation, Quran flows, and sharing UX

This commit is contained in:
Dwindi Ramadhana
2026-03-18 00:07:10 +07:00
parent a049129a35
commit 2d09b5b356
59 changed files with 11835 additions and 3184 deletions

View File

@@ -6,6 +6,7 @@ import 'package:go_router/go_router.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:intl/intl.dart';
import '../../../app/theme/app_colors.dart';
import '../../../core/widgets/notification_bell_button.dart';
import '../../../data/local/hive_boxes.dart';
import '../../../data/local/models/app_settings.dart';
import '../../../data/services/prayer_service.dart';
@@ -56,8 +57,7 @@ class _ImsakiyahScreenState extends ConsumerState<ImsakiyahScreen> {
List<_DayRow> _createRows(Map<String, Map<String, String>>? apiData) {
final selected = _months[_selectedMonthIndex];
final daysInMonth =
DateTime(selected.year, selected.month + 1, 0).day;
final daysInMonth = DateTime(selected.year, selected.month + 1, 0).day;
final rows = <_DayRow>[];
for (int d = 1; d <= daysInMonth; d++) {
@@ -102,7 +102,8 @@ class _ImsakiyahScreenState extends ConsumerState<ImsakiyahScreen> {
context: context,
builder: (ctx) => StatefulBuilder(
builder: (ctx, setDialogState) => AlertDialog(
insetPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 24),
insetPadding:
const EdgeInsets.symmetric(horizontal: 20, vertical: 24),
title: const Text('Cari Kota/Kabupaten'),
content: SizedBox(
width: MediaQuery.of(context).size.width * 0.85,
@@ -123,7 +124,7 @@ class _ImsakiyahScreenState extends ConsumerState<ImsakiyahScreen> {
final res = await MyQuranSholatService.instance
.searchCity(searchCtrl.text.trim());
if (mounted) {
setDialogState(() {
setDialogState(() {
results = res;
isSearching = false;
});
@@ -133,21 +134,23 @@ class _ImsakiyahScreenState extends ConsumerState<ImsakiyahScreen> {
),
onChanged: (val) {
if (val.trim().length < 3) return;
if (debounce?.isActive ?? false) debounce!.cancel();
debounce = Timer(const Duration(milliseconds: 500), () async {
debounce =
Timer(const Duration(milliseconds: 500), () async {
if (!mounted) return;
setDialogState(() => isSearching = true);
try {
final res = await MyQuranSholatService.instance.searchCity(val.trim());
final res = await MyQuranSholatService.instance
.searchCity(val.trim());
if (mounted) {
setDialogState(() {
results = res;
});
}
} catch (e) {
debugPrint('Error searching city: $e');
debugPrint('Error searching city: $e');
} finally {
if (mounted) {
setDialogState(() {
@@ -175,7 +178,8 @@ class _ImsakiyahScreenState extends ConsumerState<ImsakiyahScreen> {
if (isSearching)
const Center(child: CircularProgressIndicator())
else if (results.isEmpty)
const Text('Tidak ada hasil', style: TextStyle(color: Colors.grey))
const Text('Tidak ada hasil',
style: TextStyle(color: Colors.grey))
else
SizedBox(
height: 200,
@@ -193,11 +197,11 @@ class _ImsakiyahScreenState extends ConsumerState<ImsakiyahScreen> {
if (id != null && name != null) {
_settings.lastCityName = '$name|$id';
_settings.save();
// Update providers to refresh data
ref.invalidate(selectedCityIdProvider);
ref.invalidate(cityNameProvider);
Navigator.pop(ctx);
}
},
@@ -224,9 +228,11 @@ class _ImsakiyahScreenState extends ConsumerState<ImsakiyahScreen> {
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
final today = DateTime.now();
const tableBottomSpacing = 28.0;
final selectedMonth = _months[_selectedMonthIndex];
final monthArg = '${selectedMonth.year}-${selectedMonth.month.toString().padLeft(2, '0')}';
final monthArg =
'${selectedMonth.year}-${selectedMonth.month.toString().padLeft(2, '0')}';
final cityNameAsync = ref.watch(cityNameProvider);
final monthlyDataAsync = ref.watch(monthlyScheduleProvider(monthArg));
@@ -235,10 +241,7 @@ class _ImsakiyahScreenState extends ConsumerState<ImsakiyahScreen> {
title: const Text('Kalender Sholat'),
centerTitle: false,
actions: [
IconButton(
onPressed: () {},
icon: const Icon(LucideIcons.bell),
),
const NotificationBellButton(),
IconButton(
onPressed: () => context.push('/settings'),
icon: const Icon(LucideIcons.settings),
@@ -266,7 +269,9 @@ class _ImsakiyahScreenState extends ConsumerState<ImsakiyahScreen> {
decoration: BoxDecoration(
color: isSelected
? AppColors.primary
: (isDark ? AppColors.surfaceDark : AppColors.surfaceLight),
: (isDark
? AppColors.surfaceDark
: AppColors.surfaceLight),
borderRadius: BorderRadius.circular(50),
border: isSelected
? null
@@ -306,7 +311,8 @@ class _ImsakiyahScreenState extends ConsumerState<ImsakiyahScreen> {
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: isDark ? AppColors.surfaceDark : AppColors.surfaceLight,
color:
isDark ? AppColors.surfaceDark : AppColors.surfaceLight,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: isDark
@@ -314,40 +320,40 @@ class _ImsakiyahScreenState extends ConsumerState<ImsakiyahScreen> {
: AppColors.cream,
),
),
child: Row(
children: [
const Icon(LucideIcons.mapPin,
color: AppColors.primary, size: 24),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Lokasi Anda',
style: theme.textTheme.bodySmall?.copyWith(
color: isDark
? AppColors.textSecondaryDark
: AppColors.textSecondaryLight,
child: Row(
children: [
const Icon(LucideIcons.mapPin,
color: AppColors.primary, size: 24),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Lokasi Anda',
style: theme.textTheme.bodySmall?.copyWith(
color: isDark
? AppColors.textSecondaryDark
: AppColors.textSecondaryLight,
),
),
),
Text(
cityNameAsync.value ?? 'Jakarta, Indonesia',
style: const TextStyle(
fontWeight: FontWeight.w600, fontSize: 15),
),
],
Text(
cityNameAsync.value ?? 'Jakarta, Indonesia',
style: const TextStyle(
fontWeight: FontWeight.w600, fontSize: 15),
),
],
),
),
),
Icon(LucideIcons.chevronDown,
color: isDark
? AppColors.textSecondaryDark
: AppColors.textSecondaryLight),
],
Icon(LucideIcons.chevronDown,
color: isDark
? AppColors.textSecondaryDark
: AppColors.textSecondaryLight),
],
),
),
),
),
),
const SizedBox(height: 16),
// ── Table Header ──
@@ -378,7 +384,12 @@ class _ImsakiyahScreenState extends ConsumerState<ImsakiyahScreen> {
data: (apiData) {
final rows = _createRows(apiData);
return ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 16),
padding: const EdgeInsets.fromLTRB(
16,
0,
16,
tableBottomSpacing,
),
itemCount: rows.length,
itemBuilder: (context, i) {
final row = rows[i];
@@ -453,7 +464,12 @@ class _ImsakiyahScreenState extends ConsumerState<ImsakiyahScreen> {
error: (_, __) {
final rows = _createRows(null); // fallback
return ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 16),
padding: const EdgeInsets.fromLTRB(
16,
0,
16,
tableBottomSpacing,
),
itemCount: rows.length,
itemBuilder: (context, i) {
final row = rows[i];
@@ -536,7 +552,7 @@ class _ImsakiyahScreenState extends ConsumerState<ImsakiyahScreen> {
child: Center(
child: Text(
text,
style: TextStyle(
style: const TextStyle(
fontSize: 9,
fontWeight: FontWeight.w700,
letterSpacing: 1,