Files
jamshalat-diary/lib/features/imsakiyah/presentation/imsakiyah_screen.dart
dwindown faadc1865d 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
2026-03-13 15:42:17 +07:00

558 lines
21 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:go_router/go_router.dart';
import 'package:intl/intl.dart';
import '../../../app/theme/app_colors.dart';
import '../../../data/local/hive_boxes.dart';
import '../../../data/local/models/app_settings.dart';
import '../../../data/services/prayer_service.dart';
import '../../../data/services/myquran_sholat_service.dart';
import '../../dashboard/data/prayer_times_provider.dart';
class ImsakiyahScreen extends ConsumerStatefulWidget {
const ImsakiyahScreen({super.key});
@override
ConsumerState<ImsakiyahScreen> createState() => _ImsakiyahScreenState();
}
class _ImsakiyahScreenState extends ConsumerState<ImsakiyahScreen> {
int _selectedMonthIndex = 0;
late List<_MonthOption> _months;
late AppSettings _settings;
@override
void initState() {
super.initState();
final box = Hive.box<AppSettings>(HiveBoxes.settings);
_settings = box.get('default') ?? AppSettings();
_months = _generateMonths();
// Find current month
final now = DateTime.now();
for (int i = 0; i < _months.length; i++) {
if (_months[i].month == now.month && _months[i].year == now.year) {
_selectedMonthIndex = i;
break;
}
}
}
List<_MonthOption> _generateMonths() {
final now = DateTime.now();
final list = <_MonthOption>[];
for (int offset = -2; offset <= 3; offset++) {
final date = DateTime(now.year, now.month + offset, 1);
list.add(_MonthOption(
label: DateFormat('MMMM yyyy').format(date),
month: date.month,
year: date.year,
));
}
return list;
}
List<_DayRow> _createRows(Map<String, Map<String, String>>? apiData) {
final selected = _months[_selectedMonthIndex];
final daysInMonth =
DateTime(selected.year, selected.month + 1, 0).day;
final rows = <_DayRow>[];
for (int d = 1; d <= daysInMonth; d++) {
final date = DateTime(selected.year, selected.month, d);
final dateStr = DateFormat('yyyy-MM-dd').format(date);
if (apiData != null && apiData.containsKey(dateStr)) {
final times = apiData[dateStr]!;
rows.add(_DayRow(
date: date,
fajr: times['subuh'] ?? '-',
sunrise: times['terbit'] ?? '-',
dhuhr: times['dzuhur'] ?? '-',
asr: times['ashar'] ?? '-',
maghrib: times['maghrib'] ?? '-',
isha: times['isya'] ?? '-',
));
} else {
final times =
PrayerService.instance.getPrayerTimes(-6.2088, 106.8456, date);
rows.add(_DayRow(
date: date,
fajr: DateFormat('HH:mm').format(times.fajr),
sunrise: DateFormat('HH:mm').format(times.sunrise),
dhuhr: DateFormat('HH:mm').format(times.dhuhr),
asr: DateFormat('HH:mm').format(times.asr),
maghrib: DateFormat('HH:mm').format(times.maghrib),
isha: DateFormat('HH:mm').format(times.isha),
));
}
}
return rows;
}
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());
if (mounted) {
setDialogState(() {
results = res;
isSearching = false;
});
}
},
),
),
onSubmitted: (val) async {
if (val.trim().isEmpty) return;
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';
_settings.save();
// Update providers to refresh data
ref.invalidate(selectedCityIdProvider);
ref.invalidate(cityNameProvider);
Navigator.pop(ctx);
}
},
);
},
),
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text('Batal'),
),
],
),
),
);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
final today = DateTime.now();
final selectedMonth = _months[_selectedMonthIndex];
final monthArg = '${selectedMonth.year}-${selectedMonth.month.toString().padLeft(2, '0')}';
final cityNameAsync = ref.watch(cityNameProvider);
final monthlyDataAsync = ref.watch(monthlyScheduleProvider(monthArg));
return Scaffold(
appBar: AppBar(
title: const Text('Kalender Sholat'),
centerTitle: false,
actions: [
IconButton(
onPressed: () {},
icon: const Icon(Icons.notifications_outlined),
),
IconButton(
onPressed: () => context.push('/settings'),
icon: const Icon(Icons.settings_outlined),
),
const SizedBox(width: 8),
],
),
body: Column(
children: [
// ── Month Selector ──
SizedBox(
height: 48,
child: ListView.separated(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
itemCount: _months.length,
separatorBuilder: (_, __) => const SizedBox(width: 8),
itemBuilder: (context, i) {
final isSelected = i == _selectedMonthIndex;
return GestureDetector(
onTap: () => setState(() => _selectedMonthIndex = i),
child: Container(
padding:
const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
decoration: BoxDecoration(
color: isSelected
? AppColors.primary
: (isDark ? AppColors.surfaceDark : AppColors.surfaceLight),
borderRadius: BorderRadius.circular(50),
border: isSelected
? null
: Border.all(
color: isDark
? AppColors.primary.withValues(alpha: 0.2)
: AppColors.cream,
),
),
child: Center(
child: Text(
_months[i].label,
style: TextStyle(
fontSize: 13,
fontWeight:
isSelected ? FontWeight.w600 : FontWeight.w400,
color: isSelected
? AppColors.onPrimary
: (isDark
? AppColors.textSecondaryDark
: AppColors.textSecondaryLight),
),
),
),
),
);
},
),
),
const SizedBox(height: 12),
// ── Location Card ──
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: GestureDetector(
onTap: () => _showLocationDialog(context),
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.1)
: AppColors.cream,
),
),
child: Row(
children: [
const Icon(Icons.location_on,
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),
),
],
),
),
Icon(Icons.expand_more,
color: isDark
? AppColors.textSecondaryDark
: AppColors.textSecondaryLight),
],
),
),
),
),
const SizedBox(height: 16),
// ── Table Header ──
Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
decoration: BoxDecoration(
color: AppColors.primary.withValues(alpha: 0.1),
borderRadius:
const BorderRadius.vertical(top: Radius.circular(12)),
),
child: Row(
children: [
_headerCell('TGL', flex: 4),
_headerCell('SUBUH', flex: 3),
_headerCell('SYURUQ', flex: 3),
_headerCell('DZUHUR', flex: 3),
_headerCell('ASHAR', flex: 3),
_headerCell('MAGH', flex: 3),
_headerCell('ISYA', flex: 3),
],
),
),
// ── Table Body ──
Expanded(
child: monthlyDataAsync.when(
data: (apiData) {
final rows = _createRows(apiData);
return ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: rows.length,
itemBuilder: (context, i) {
final row = rows[i];
final isToday = row.date.day == today.day &&
row.date.month == today.month &&
row.date.year == today.year;
return Container(
margin: const EdgeInsets.only(bottom: 4),
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 14),
decoration: BoxDecoration(
color: isToday
? AppColors.primary
: (isDark
? AppColors.surfaceDark
: AppColors.surfaceLight),
borderRadius: BorderRadius.circular(12),
border: isToday
? null
: Border.all(
color: isDark
? AppColors.primary.withValues(alpha: 0.05)
: AppColors.cream.withValues(alpha: 0.5),
),
),
child: Row(
children: [
// Day column
Expanded(
flex: 4,
child: Column(
children: [
Text(
DateFormat('MMM')
.format(row.date)
.toUpperCase(),
style: TextStyle(
fontSize: 9,
fontWeight: FontWeight.w700,
letterSpacing: 1,
color: isToday
? AppColors.onPrimary
.withValues(alpha: 0.7)
: AppColors.sage,
),
),
Text(
'${row.date.day}',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w800,
color: isToday ? AppColors.onPrimary : null,
),
),
],
),
),
_dataCell(row.fajr, isToday, flex: 3),
_dataCell(row.sunrise, isToday, flex: 3),
_dataCell(row.dhuhr, isToday, bold: true, flex: 3),
_dataCell(row.asr, isToday, flex: 3),
_dataCell(row.maghrib, isToday, bold: true, flex: 3),
_dataCell(row.isha, isToday, flex: 3),
],
),
);
},
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error: (_, __) {
final rows = _createRows(null); // fallback
return ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: rows.length,
itemBuilder: (context, i) {
final row = rows[i];
final isToday = row.date.day == today.day &&
row.date.month == today.month &&
row.date.year == today.year;
return Container(
margin: const EdgeInsets.only(bottom: 4),
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 14),
decoration: BoxDecoration(
color: isToday
? AppColors.primary
: (isDark
? AppColors.surfaceDark
: AppColors.surfaceLight),
borderRadius: BorderRadius.circular(12),
border: isToday
? null
: Border.all(
color: isDark
? AppColors.primary.withValues(alpha: 0.05)
: AppColors.cream.withValues(alpha: 0.5),
),
),
child: Row(
children: [
Expanded(
flex: 4,
child: Column(
children: [
Text(
DateFormat('MMM')
.format(row.date)
.toUpperCase(),
style: TextStyle(
fontSize: 9,
fontWeight: FontWeight.w700,
letterSpacing: 1,
color: isToday
? AppColors.onPrimary
.withValues(alpha: 0.7)
: AppColors.sage,
),
),
Text(
'${row.date.day}',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w800,
color: isToday ? AppColors.onPrimary : null,
),
),
],
),
),
_dataCell(row.fajr, isToday, flex: 3),
_dataCell(row.sunrise, isToday, flex: 3),
_dataCell(row.dhuhr, isToday, bold: true, flex: 3),
_dataCell(row.asr, isToday, flex: 3),
_dataCell(row.maghrib, isToday, bold: true, flex: 3),
_dataCell(row.isha, isToday, flex: 3),
],
),
);
},
);
},
),
),
],
),
);
}
Widget _headerCell(String text, {int flex = 1}) {
return Expanded(
flex: flex,
child: Center(
child: Text(
text,
style: TextStyle(
fontSize: 9,
fontWeight: FontWeight.w700,
letterSpacing: 1,
color: AppColors.textSecondaryLight,
),
),
),
);
}
Widget _dataCell(String value, bool isToday,
{bool bold = false, int flex = 1}) {
return Expanded(
flex: flex,
child: Center(
child: Text(
value,
style: TextStyle(
fontSize: 12,
fontWeight: bold ? FontWeight.w700 : FontWeight.w400,
color: isToday ? AppColors.onPrimary : null,
),
),
),
);
}
}
class _MonthOption {
final String label;
final int month;
final int year;
_MonthOption({required this.label, required this.month, required this.year});
}
class _DayRow {
final DateTime date;
final String fajr, sunrise, dhuhr, asr, maghrib, isha;
_DayRow({
required this.date,
required this.fajr,
required this.sunrise,
required this.dhuhr,
required this.asr,
required this.maghrib,
required this.isha,
});
}