1185 lines
41 KiB
Dart
1185 lines
41 KiB
Dart
import 'dart:async';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:intl/intl.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
import 'package:hive_flutter/hive_flutter.dart';
|
|
import 'package:lucide_icons/lucide_icons.dart';
|
|
import '../../../app/icons/app_icons.dart';
|
|
import '../../../app/theme/app_colors.dart';
|
|
import '../../../core/widgets/arabic_text.dart';
|
|
import '../../../core/widgets/ayat_today_card.dart';
|
|
import '../../../core/widgets/notification_bell_button.dart';
|
|
import '../../../core/widgets/prayer_time_card.dart';
|
|
import '../../../core/widgets/tool_card.dart';
|
|
import '../../../data/local/hive_boxes.dart';
|
|
import '../../../data/local/models/app_settings.dart';
|
|
import '../../../data/local/models/daily_worship_log.dart';
|
|
import '../../../data/local/models/quran_bookmark.dart';
|
|
import '../../../data/services/notification_service.dart';
|
|
import '../data/prayer_times_provider.dart';
|
|
|
|
class DashboardScreen extends ConsumerStatefulWidget {
|
|
const DashboardScreen({super.key});
|
|
|
|
@override
|
|
ConsumerState<DashboardScreen> createState() => _DashboardScreenState();
|
|
}
|
|
|
|
class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
|
Timer? _countdownTimer;
|
|
final ValueNotifier<Duration> _countdown = ValueNotifier(Duration.zero);
|
|
final ValueNotifier<String> _nextPrayerName = ValueNotifier('');
|
|
final ValueNotifier<String> _nextPrayerTime = ValueNotifier('');
|
|
final ScrollController _prayerScrollController = ScrollController();
|
|
bool _hasAutoScrolled = false;
|
|
String? _lastAutoScrollPrayerKey;
|
|
DaySchedule? _currentSchedule;
|
|
|
|
bool get _isSimpleMode {
|
|
final box = Hive.box<AppSettings>(HiveBoxes.settings);
|
|
final settings = box.get('default');
|
|
return settings?.simpleMode ?? false;
|
|
}
|
|
|
|
bool get _isAdhanEnabled {
|
|
final box = Hive.box<AppSettings>(HiveBoxes.settings);
|
|
final settings = box.get('default');
|
|
return settings?.adhanEnabled.values.any((v) => v) ?? false;
|
|
}
|
|
|
|
Future<void> _toggleAdhanFromHero() async {
|
|
final box = Hive.box<AppSettings>(HiveBoxes.settings);
|
|
final settings = box.get('default') ?? AppSettings();
|
|
final nextEnabled = !settings.adhanEnabled.values.any((v) => v);
|
|
|
|
settings.adhanEnabled.updateAll((key, _) => nextEnabled);
|
|
await settings.save();
|
|
|
|
if (!nextEnabled) {
|
|
await NotificationService.instance.cancelAllPending();
|
|
}
|
|
ref.invalidate(prayerTimesProvider);
|
|
unawaited(ref.read(prayerTimesProvider.future));
|
|
|
|
if (!mounted) return;
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(
|
|
nextEnabled
|
|
? 'Notifikasi adzan diaktifkan'
|
|
: 'Notifikasi adzan dinonaktifkan',
|
|
),
|
|
),
|
|
);
|
|
setState(() {});
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_countdownTimer?.cancel();
|
|
_prayerScrollController.dispose();
|
|
_countdown.dispose();
|
|
_nextPrayerName.dispose();
|
|
_nextPrayerTime.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
void _startCountdown(DaySchedule schedule) {
|
|
if (_currentSchedule == schedule) return;
|
|
if (_currentSchedule?.date != schedule.date) {
|
|
_hasAutoScrolled = false;
|
|
_lastAutoScrollPrayerKey = null;
|
|
}
|
|
_currentSchedule = schedule;
|
|
|
|
_countdownTimer?.cancel();
|
|
_updateCountdown(schedule);
|
|
_countdownTimer = Timer.periodic(const Duration(seconds: 1), (_) {
|
|
_updateCountdown(schedule);
|
|
});
|
|
}
|
|
|
|
void _updateCountdown(DaySchedule schedule) {
|
|
final now = DateTime.now();
|
|
final next = _resolveNextPrayer(schedule, now);
|
|
if (next == null) {
|
|
_nextPrayerName.value = '';
|
|
_nextPrayerTime.value = '';
|
|
_countdown.value = Duration.zero;
|
|
return;
|
|
}
|
|
|
|
_nextPrayerName.value = next.name;
|
|
_nextPrayerTime.value = next.time;
|
|
final diff = next.target.difference(now);
|
|
_countdown.value = diff.isNegative ? Duration.zero : diff;
|
|
}
|
|
|
|
({String name, String time, DateTime target})? _resolveNextPrayer(
|
|
DaySchedule schedule, DateTime now) {
|
|
final scheduleDate = DateTime.tryParse(schedule.date) ??
|
|
DateTime(now.year, now.month, now.day);
|
|
final entries = <({String name, String time, DateTime target})>[];
|
|
|
|
const orderedPrayers = <MapEntry<String, String>>[
|
|
MapEntry('Subuh', 'subuh'),
|
|
MapEntry('Dzuhur', 'dzuhur'),
|
|
MapEntry('Ashar', 'ashar'),
|
|
MapEntry('Maghrib', 'maghrib'),
|
|
MapEntry('Isya', 'isya'),
|
|
];
|
|
|
|
for (final prayer in orderedPrayers) {
|
|
final time = (schedule.times[prayer.value] ?? '').trim();
|
|
final parts = _parseHourMinute(time);
|
|
if (parts == null) continue;
|
|
final target = DateTime(scheduleDate.year, scheduleDate.month,
|
|
scheduleDate.day, parts.$1, parts.$2);
|
|
entries.add((name: prayer.key, time: time, target: target));
|
|
}
|
|
|
|
if (entries.isEmpty) return null;
|
|
|
|
for (final entry in entries) {
|
|
if (!entry.target.isBefore(now)) {
|
|
return entry;
|
|
}
|
|
}
|
|
|
|
final first = entries.first;
|
|
return (
|
|
name: first.name,
|
|
time: first.time,
|
|
target: first.target.add(const Duration(days: 1)),
|
|
);
|
|
}
|
|
|
|
(int, int)? _parseHourMinute(String value) {
|
|
final match = RegExp(r'(\d{1,2}):(\d{2})').firstMatch(value);
|
|
if (match == null) return null;
|
|
final hour = int.tryParse(match.group(1) ?? '');
|
|
final minute = int.tryParse(match.group(2) ?? '');
|
|
if (hour == null || minute == null) return null;
|
|
if (hour < 0 || hour > 23 || minute < 0 || minute > 59) return null;
|
|
return (hour, minute);
|
|
}
|
|
|
|
String _formatCountdown(Duration d) {
|
|
final h = d.inHours.toString().padLeft(2, '0');
|
|
final m = (d.inMinutes % 60).toString().padLeft(2, '0');
|
|
final s = (d.inSeconds % 60).toString().padLeft(2, '0');
|
|
return '$h:$m:$s';
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
final isDark = theme.brightness == Brightness.dark;
|
|
|
|
ref.listen<AsyncValue<DaySchedule?>>(prayerTimesProvider, (previous, next) {
|
|
next.whenData((schedule) {
|
|
if (schedule != null) {
|
|
_startCountdown(schedule);
|
|
}
|
|
});
|
|
});
|
|
|
|
return Scaffold(
|
|
body: SafeArea(
|
|
child: SingleChildScrollView(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const SizedBox(height: 8),
|
|
_buildHeader(context, isDark),
|
|
const SizedBox(height: 20),
|
|
Consumer(
|
|
builder: (context, ref, child) {
|
|
final prayerTimesAsync = ref.watch(prayerTimesProvider);
|
|
return prayerTimesAsync.when(
|
|
data: (schedule) {
|
|
if (schedule != null) {
|
|
return _buildHeroCard(context, schedule);
|
|
}
|
|
return _buildHeroCardPlaceholder(context);
|
|
},
|
|
loading: () => _buildHeroCardPlaceholder(context),
|
|
error: (_, __) => _buildHeroCardPlaceholder(context),
|
|
);
|
|
},
|
|
),
|
|
const SizedBox(height: 24),
|
|
Consumer(
|
|
builder: (context, ref, child) {
|
|
final prayerTimesAsync = ref.watch(prayerTimesProvider);
|
|
return _buildPrayerTimesSection(context, prayerTimesAsync);
|
|
},
|
|
),
|
|
const SizedBox(height: 24),
|
|
_buildLastReadQuranCard(context, isDark),
|
|
// Checklist & Weekly Progress (hidden in Simple Mode)
|
|
if (!_isSimpleMode) ...[
|
|
_buildChecklistSummary(context, isDark),
|
|
const SizedBox(height: 24),
|
|
_buildWeeklyProgress(context, isDark),
|
|
] else ...[
|
|
_buildQuickActions(context, isDark),
|
|
const SizedBox(height: 24),
|
|
_buildAyatHariIni(context, isDark),
|
|
],
|
|
const SizedBox(height: 24),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
String _quranReadingRoute(QuranBookmark bookmark) {
|
|
final base = _isSimpleMode ? '/quran' : '/tools/quran';
|
|
return '$base/${bookmark.surahId}?startVerse=${bookmark.verseId}';
|
|
}
|
|
|
|
Widget _buildLastReadQuranCard(BuildContext context, bool isDark) {
|
|
return ValueListenableBuilder<Box<QuranBookmark>>(
|
|
valueListenable:
|
|
Hive.box<QuranBookmark>(HiveBoxes.bookmarks).listenable(),
|
|
builder: (context, box, _) {
|
|
final lastRead = box.values
|
|
.where((bookmark) => bookmark.isLastRead)
|
|
.toList()
|
|
..sort((a, b) => b.savedAt.compareTo(a.savedAt));
|
|
|
|
if (lastRead.isEmpty) {
|
|
return const SizedBox.shrink();
|
|
}
|
|
|
|
final bookmark = lastRead.first;
|
|
final arabic = bookmark.verseText.trim();
|
|
final translation = (bookmark.verseTranslation ?? '').trim();
|
|
final dateLabel = DateFormat('dd MMM • HH:mm').format(bookmark.savedAt);
|
|
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
'LANJUTKAN TILAWAH',
|
|
style: TextStyle(
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w700,
|
|
letterSpacing: 1.5,
|
|
color: AppColors.sage,
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
InkWell(
|
|
onTap: () => context.push(_quranReadingRoute(bookmark)),
|
|
borderRadius: BorderRadius.circular(20),
|
|
child: Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(18),
|
|
decoration: BoxDecoration(
|
|
color:
|
|
isDark ? AppColors.surfaceDark : AppColors.surfaceLight,
|
|
borderRadius: BorderRadius.circular(20),
|
|
border: Border.all(
|
|
color: AppColors.primary.withValues(alpha: 0.22),
|
|
),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: AppColors.primary
|
|
.withValues(alpha: isDark ? 0.10 : 0.08),
|
|
blurRadius: 18,
|
|
offset: const Offset(0, 8),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Container(
|
|
width: 44,
|
|
height: 44,
|
|
decoration: BoxDecoration(
|
|
color: AppColors.primary.withValues(alpha: 0.12),
|
|
borderRadius: BorderRadius.circular(14),
|
|
),
|
|
child: const Icon(
|
|
LucideIcons.bookOpen,
|
|
size: 20,
|
|
color: AppColors.primary,
|
|
),
|
|
),
|
|
const SizedBox(width: 14),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'QS. ${bookmark.surahName}: ${bookmark.verseId}',
|
|
style: const TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w800,
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
'Terakhir dibaca $dateLabel',
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w600,
|
|
color: isDark
|
|
? AppColors.textSecondaryDark
|
|
: AppColors.textSecondaryLight,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Icon(
|
|
LucideIcons.chevronRight,
|
|
size: 18,
|
|
color: isDark
|
|
? AppColors.textSecondaryDark
|
|
: AppColors.textSecondaryLight,
|
|
),
|
|
],
|
|
),
|
|
if (arabic.isNotEmpty) ...[
|
|
const SizedBox(height: 16),
|
|
ArabicText(
|
|
arabic,
|
|
baseFontSize: 21,
|
|
height: 1.75,
|
|
maxLines: 2,
|
|
overflow: TextOverflow.ellipsis,
|
|
textAlign: TextAlign.right,
|
|
),
|
|
],
|
|
if (translation.isNotEmpty) ...[
|
|
const SizedBox(height: 10),
|
|
Text(
|
|
translation,
|
|
maxLines: 2,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: TextStyle(
|
|
fontSize: 13,
|
|
height: 1.5,
|
|
color: isDark
|
|
? AppColors.textSecondaryDark
|
|
: AppColors.textSecondaryLight,
|
|
),
|
|
),
|
|
],
|
|
const SizedBox(height: 16),
|
|
Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
|
decoration: BoxDecoration(
|
|
color: AppColors.primary.withValues(alpha: 0.12),
|
|
borderRadius: BorderRadius.circular(999),
|
|
),
|
|
child: const Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(
|
|
LucideIcons.bookMarked,
|
|
size: 16,
|
|
color: AppColors.primary,
|
|
),
|
|
SizedBox(width: 8),
|
|
Text(
|
|
'Lanjutkan Membaca',
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w700,
|
|
color: AppColors.primary,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 24),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildHeader(BuildContext context, bool isDark) {
|
|
return Row(
|
|
children: [
|
|
Container(
|
|
width: 40,
|
|
height: 40,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
border: Border.all(color: AppColors.primary, width: 2),
|
|
color: AppColors.primary.withValues(alpha: 0.2),
|
|
),
|
|
child:
|
|
const Icon(LucideIcons.user, size: 20, color: AppColors.primary),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Selamat datang,',
|
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
|
color: isDark
|
|
? AppColors.textSecondaryDark
|
|
: AppColors.textSecondaryLight,
|
|
),
|
|
),
|
|
Text(
|
|
"Assalamu'alaikum",
|
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
|
fontWeight: FontWeight.w700,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Row(
|
|
children: [
|
|
NotificationBellButton(
|
|
iconColor: isDark
|
|
? AppColors.textSecondaryDark
|
|
: AppColors.textSecondaryLight,
|
|
),
|
|
IconButton(
|
|
onPressed: () => context.push('/settings'),
|
|
icon: AppIcon(
|
|
glyph: AppIcons.settings,
|
|
color: isDark
|
|
? AppColors.textSecondaryDark
|
|
: AppColors.textSecondaryLight,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildHeroCard(BuildContext context, DaySchedule schedule) {
|
|
final initialNext = _resolveNextPrayer(schedule, DateTime.now());
|
|
final fallbackPrayerName = initialNext?.name ?? 'Isya';
|
|
final fallbackTime = initialNext?.time ?? '--:--';
|
|
final isAdhanEnabled = _isAdhanEnabled;
|
|
|
|
return Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(20),
|
|
decoration: BoxDecoration(
|
|
color: AppColors.primary,
|
|
borderRadius: BorderRadius.circular(24),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: AppColors.primary.withValues(alpha: 0.3),
|
|
blurRadius: 20,
|
|
offset: const Offset(0, 8),
|
|
),
|
|
],
|
|
),
|
|
child: Stack(
|
|
clipBehavior: Clip.none,
|
|
children: [
|
|
Positioned(
|
|
top: -6,
|
|
right: -4,
|
|
child: IgnorePointer(
|
|
child: Opacity(
|
|
opacity: 0.22,
|
|
child: Image.asset(
|
|
'assets/images/blob.png',
|
|
width: 140,
|
|
height: 140,
|
|
fit: BoxFit.contain,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Icon(LucideIcons.clock,
|
|
size: 16,
|
|
color: AppColors.onPrimary.withValues(alpha: 0.8)),
|
|
const SizedBox(width: 6),
|
|
Text(
|
|
'SHOLAT BERIKUTNYA',
|
|
style: TextStyle(
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w700,
|
|
letterSpacing: 1.5,
|
|
color: AppColors.onPrimary.withValues(alpha: 0.8),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 8),
|
|
AnimatedBuilder(
|
|
animation: Listenable.merge([_nextPrayerName, _nextPrayerTime]),
|
|
builder: (context, _) {
|
|
final name = _nextPrayerName.value.isNotEmpty
|
|
? _nextPrayerName.value
|
|
: fallbackPrayerName;
|
|
final time = _nextPrayerTime.value.isNotEmpty
|
|
? _nextPrayerTime.value
|
|
: fallbackTime;
|
|
return Text(
|
|
'$name — $time',
|
|
style: const TextStyle(
|
|
fontSize: 28,
|
|
fontWeight: FontWeight.w800,
|
|
color: AppColors.onPrimary,
|
|
),
|
|
);
|
|
},
|
|
),
|
|
const SizedBox(height: 4),
|
|
ValueListenableBuilder<Duration>(
|
|
valueListenable: _countdown,
|
|
builder: (context, countdown, _) {
|
|
return Text(
|
|
'Hitung mundur: ${_formatCountdown(countdown)}',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w400,
|
|
color: AppColors.onPrimary.withValues(alpha: 0.8),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
const SizedBox(height: 4),
|
|
// City name
|
|
Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
AppIcon(
|
|
glyph: AppIcons.location,
|
|
size: 14,
|
|
color: AppColors.onPrimary.withValues(alpha: 0.7),
|
|
),
|
|
const SizedBox(width: 6),
|
|
Text(
|
|
schedule.cityName,
|
|
style: TextStyle(
|
|
fontSize: 13,
|
|
color: AppColors.onPrimary.withValues(alpha: 0.7),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 16),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: GestureDetector(
|
|
onTap: () => context.push('/tools/qibla'),
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
|
decoration: BoxDecoration(
|
|
color: AppColors.onPrimary,
|
|
borderRadius: BorderRadius.circular(50),
|
|
),
|
|
child: const Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
AppIcon(
|
|
glyph: AppIcons.qibla,
|
|
size: 18,
|
|
color: AppColors.primary,
|
|
),
|
|
SizedBox(width: 8),
|
|
Text(
|
|
'Arah Kiblat',
|
|
style: TextStyle(
|
|
color: AppColors.primary,
|
|
fontWeight: FontWeight.w600,
|
|
fontSize: 14,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
GestureDetector(
|
|
onTap: _toggleAdhanFromHero,
|
|
child: Container(
|
|
width: 48,
|
|
height: 48,
|
|
decoration: BoxDecoration(
|
|
color: isAdhanEnabled
|
|
? Colors.white.withValues(alpha: 0.2)
|
|
: Colors.white.withValues(alpha: 0.12),
|
|
shape: BoxShape.circle,
|
|
border: Border.all(
|
|
color: Colors.white.withValues(
|
|
alpha: isAdhanEnabled ? 0.0 : 0.35,
|
|
),
|
|
),
|
|
),
|
|
child: Icon(
|
|
isAdhanEnabled
|
|
? LucideIcons.volume2
|
|
: LucideIcons.volumeX,
|
|
color: AppColors.onPrimary,
|
|
size: 22,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildHeroCardPlaceholder(BuildContext context) {
|
|
return Container(
|
|
width: double.infinity,
|
|
height: 180,
|
|
padding: const EdgeInsets.all(20),
|
|
decoration: BoxDecoration(
|
|
color: AppColors.primary,
|
|
borderRadius: BorderRadius.circular(24),
|
|
),
|
|
child: const Center(
|
|
child: CircularProgressIndicator(color: AppColors.onPrimary),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildPrayerTimesSection(
|
|
BuildContext context, AsyncValue<DaySchedule?> prayerTimesAsync) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
prayerTimesAsync.value?.isTomorrow == true
|
|
? 'Jadwal Sholat Besok'
|
|
: 'Jadwal Sholat Hari Ini',
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: Theme.of(context)
|
|
.textTheme
|
|
.titleMedium
|
|
?.copyWith(fontWeight: FontWeight.w700),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
|
decoration: BoxDecoration(
|
|
color: AppColors.primary.withValues(alpha: 0.1),
|
|
borderRadius: BorderRadius.circular(50),
|
|
),
|
|
child: Text(
|
|
prayerTimesAsync.value?.isTomorrow == true
|
|
? 'BESOK'
|
|
: 'HARI INI',
|
|
style: const TextStyle(
|
|
color: AppColors.primary,
|
|
fontSize: 10,
|
|
fontWeight: FontWeight.w700,
|
|
letterSpacing: 1.5,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
SizedBox(
|
|
height: 110,
|
|
child: prayerTimesAsync.when(
|
|
data: (schedule) {
|
|
if (schedule == null) return const SizedBox();
|
|
final activePrayerName =
|
|
_resolveNextPrayer(schedule, DateTime.now())?.name;
|
|
final activePrayerKey = activePrayerName == null
|
|
? null
|
|
: '${schedule.date}:$activePrayerName';
|
|
final prayers = schedule.prayerList
|
|
.where(
|
|
(p) => ['Subuh', 'Dzuhur', 'Ashar', 'Maghrib', 'Isya']
|
|
.contains(p.name),
|
|
)
|
|
.toList();
|
|
return ListView.separated(
|
|
controller: _prayerScrollController,
|
|
scrollDirection: Axis.horizontal,
|
|
itemCount: prayers.length,
|
|
separatorBuilder: (_, __) => const SizedBox(width: 12),
|
|
itemBuilder: (context, i) {
|
|
final p = prayers[i];
|
|
final icon = _prayerIcon(p.name);
|
|
final isActive = p.name == activePrayerName;
|
|
// Auto-scroll to active prayer on first build
|
|
if (isActive &&
|
|
i > 0 &&
|
|
(!_hasAutoScrolled ||
|
|
_lastAutoScrollPrayerKey != activePrayerKey)) {
|
|
_hasAutoScrolled = true;
|
|
_lastAutoScrollPrayerKey = activePrayerKey;
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
if (_prayerScrollController.hasClients) {
|
|
final targetOffset = i * 124.0; // 112 width + 12 gap
|
|
_prayerScrollController.animateTo(
|
|
targetOffset.clamp(0,
|
|
_prayerScrollController.position.maxScrollExtent),
|
|
duration: const Duration(milliseconds: 400),
|
|
curve: Curves.easeOut,
|
|
);
|
|
}
|
|
});
|
|
}
|
|
return PrayerTimeCard(
|
|
prayerName: p.name,
|
|
time: p.time,
|
|
icon: icon,
|
|
isActive: isActive,
|
|
);
|
|
},
|
|
);
|
|
},
|
|
loading: () => const Center(child: CircularProgressIndicator()),
|
|
error: (_, __) => const Center(child: Text('Gagal memuat jadwal')),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
IconData _prayerIcon(String name) {
|
|
switch (name) {
|
|
case 'Subuh':
|
|
return LucideIcons.sunrise;
|
|
case 'Dzuhur':
|
|
return LucideIcons.sun;
|
|
case 'Ashar':
|
|
return LucideIcons.cloudSun;
|
|
case 'Maghrib':
|
|
return LucideIcons.sunset;
|
|
case 'Isya':
|
|
return LucideIcons.moon;
|
|
default:
|
|
return LucideIcons.clock;
|
|
}
|
|
}
|
|
|
|
Widget _buildChecklistSummary(BuildContext context, bool isDark) {
|
|
final todayKey = DateFormat('yyyy-MM-dd').format(DateTime.now());
|
|
final box = Hive.box<DailyWorshipLog>(HiveBoxes.worshipLogs);
|
|
final log = box.get(todayKey);
|
|
|
|
final points = log?.totalPoints ?? 0;
|
|
// We can assume a max "excellent" day is around 150 points for the progress ring scale
|
|
final percent = (points / 150).clamp(0.0, 1.0);
|
|
|
|
// Prepare dynamic preview lines
|
|
int fardhuCompleted = 0;
|
|
if (log != null) {
|
|
fardhuCompleted = log.shalatLogs.values.where((l) => l.completed).length;
|
|
}
|
|
|
|
String amalanText = 'Belum ada data';
|
|
if (log != null) {
|
|
final List<String> aList = [];
|
|
if (log.tilawahLog?.isCompleted == true) aList.add('Tilawah');
|
|
if (log.puasaLog?.completed == true) aList.add('Puasa');
|
|
if (log.dzikirLog?.pagi == true) aList.add('Dzikir');
|
|
if (aList.isNotEmpty) {
|
|
amalanText = aList.join(', ');
|
|
}
|
|
}
|
|
|
|
return Container(
|
|
padding: const EdgeInsets.all(20),
|
|
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: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text('Poin Ibadah Hari Ini',
|
|
style: Theme.of(context)
|
|
.textTheme
|
|
.titleMedium
|
|
?.copyWith(fontWeight: FontWeight.w700)),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
'Kumpulkan poin dengan konsisten!',
|
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
|
color: isDark
|
|
? AppColors.textSecondaryDark
|
|
: AppColors.textSecondaryLight,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
SizedBox(
|
|
width: 48,
|
|
height: 48,
|
|
child: Stack(
|
|
alignment: Alignment.center,
|
|
children: [
|
|
CircularProgressIndicator(
|
|
value: percent,
|
|
strokeWidth: 4,
|
|
backgroundColor:
|
|
AppColors.primary.withValues(alpha: 0.15),
|
|
valueColor: const AlwaysStoppedAnimation<Color>(
|
|
AppColors.primary),
|
|
),
|
|
Text(
|
|
'$points',
|
|
style: const TextStyle(
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w800,
|
|
color: AppColors.primary,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 16),
|
|
_checklistPreviewItem(context, isDark, 'Sholat Fardhu',
|
|
'$fardhuCompleted dari 5 selesai', fardhuCompleted == 5),
|
|
const SizedBox(height: 8),
|
|
_checklistPreviewItem(
|
|
context, isDark, 'Amalan Selesai', amalanText, points > 50),
|
|
const SizedBox(height: 16),
|
|
GestureDetector(
|
|
onTap: () => context.go('/checklist'),
|
|
child: Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
|
decoration: BoxDecoration(
|
|
color: AppColors.primary.withValues(alpha: 0.1),
|
|
borderRadius: BorderRadius.circular(50),
|
|
),
|
|
child: const Center(
|
|
child: Text(
|
|
'Lihat Semua Checklist',
|
|
style: TextStyle(
|
|
color: AppColors.primary,
|
|
fontWeight: FontWeight.w600,
|
|
fontSize: 14,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _checklistPreviewItem(BuildContext context, bool isDark, String title,
|
|
String subtitle, bool completed) {
|
|
return Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: isDark
|
|
? AppColors.primary.withValues(alpha: 0.05)
|
|
: AppColors.backgroundLight,
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Icon(
|
|
completed ? LucideIcons.checkCircle2 : LucideIcons.circle,
|
|
color: AppColors.primary,
|
|
size: 22,
|
|
),
|
|
const SizedBox(width: 12),
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(title,
|
|
style: Theme.of(context)
|
|
.textTheme
|
|
.bodyMedium
|
|
?.copyWith(fontWeight: FontWeight.w600)),
|
|
Text(subtitle,
|
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
|
color: isDark
|
|
? AppColors.textSecondaryDark
|
|
: AppColors.textSecondaryLight,
|
|
)),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildWeeklyProgress(BuildContext context, bool isDark) {
|
|
final box = Hive.box<DailyWorshipLog>(HiveBoxes.worshipLogs);
|
|
final now = DateTime.now();
|
|
|
|
// Reverse so today is on the far right (index 6)
|
|
final last7Days =
|
|
List.generate(7, (i) => now.subtract(Duration(days: 6 - i)));
|
|
final daysLabels = ['Sen', 'Sel', 'Rab', 'Kam', 'Jum', 'Sab', 'Min'];
|
|
|
|
final weekPoints = <int>[];
|
|
for (final d in last7Days) {
|
|
final k = DateFormat('yyyy-MM-dd').format(d);
|
|
final l = box.get(k);
|
|
weekPoints.add(l?.totalPoints ?? 0);
|
|
}
|
|
|
|
// Find the max points acquired this week to scale the bars, with a minimum floor of 50
|
|
final maxPts = weekPoints.reduce((a, b) => a > b ? a : b).clamp(50, 300);
|
|
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text('Progres Poin Mingguan',
|
|
style: Theme.of(context)
|
|
.textTheme
|
|
.titleMedium
|
|
?.copyWith(fontWeight: FontWeight.w700)),
|
|
const SizedBox(height: 12),
|
|
Container(
|
|
padding: const EdgeInsets.all(20),
|
|
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(
|
|
crossAxisAlignment: CrossAxisAlignment.end,
|
|
children: List.generate(7, (i) {
|
|
final val = weekPoints[i];
|
|
final ratio = (val / maxPts).clamp(0.1, 1.0);
|
|
final labelColor = i == 6
|
|
? AppColors.primary
|
|
: (isDark
|
|
? AppColors.textSecondaryDark
|
|
: AppColors.textSecondaryLight);
|
|
|
|
return Expanded(
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 4),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
SizedBox(
|
|
height: 96,
|
|
child: Align(
|
|
alignment: Alignment.bottomCenter,
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Text(
|
|
'$val',
|
|
style: TextStyle(
|
|
fontSize: 10,
|
|
fontWeight: FontWeight.w700,
|
|
color: labelColor,
|
|
),
|
|
),
|
|
const SizedBox(height: 6),
|
|
Container(
|
|
width: 24,
|
|
height: 80 * ratio,
|
|
decoration: BoxDecoration(
|
|
color: val > 0
|
|
? AppColors.primary.withValues(
|
|
alpha: 0.2 + ratio * 0.8,
|
|
)
|
|
: AppColors.primary
|
|
.withValues(alpha: 0.1),
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
daysLabels[
|
|
last7Days[i].weekday - 1], // Correct localized day
|
|
style: TextStyle(
|
|
fontSize: 10,
|
|
fontWeight:
|
|
i == 6 ? FontWeight.w800 : FontWeight.w600,
|
|
color: labelColor,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildQuickActions(BuildContext context, bool isDark) {
|
|
final isSimpleMode = _isSimpleMode;
|
|
final cards = <Widget>[
|
|
if (!isSimpleMode)
|
|
ToolCard(
|
|
icon: AppIcons.quran,
|
|
title: "Al-Qur'an\nTerjemahan",
|
|
color: const Color(0xFF00B894),
|
|
isDark: isDark,
|
|
onTap: () => context.push('/tools/quran'),
|
|
),
|
|
ToolCard(
|
|
icon: AppIcons.murattal,
|
|
title: "Qur'an\nMurattal",
|
|
color: const Color(0xFF7B61FF),
|
|
isDark: isDark,
|
|
onTap: () {
|
|
if (isSimpleMode) {
|
|
context.go('/quran/1/murattal');
|
|
} else {
|
|
context.push('/tools/quran/1/murattal');
|
|
}
|
|
},
|
|
),
|
|
if (!isSimpleMode)
|
|
ToolCard(
|
|
icon: AppIcons.dzikir,
|
|
title: 'Dzikir\nHarian',
|
|
color: AppColors.primary,
|
|
isDark: isDark,
|
|
onTap: () => context.push('/tools/dzikir'),
|
|
),
|
|
ToolCard(
|
|
icon: AppIcons.doa,
|
|
title: 'Kumpulan\nDoa',
|
|
color: const Color(0xFFE17055),
|
|
isDark: isDark,
|
|
onTap: () {
|
|
if (isSimpleMode) {
|
|
context.push('/doa');
|
|
} else {
|
|
context.push('/tools/doa');
|
|
}
|
|
},
|
|
),
|
|
ToolCard(
|
|
icon: AppIcons.hadits,
|
|
title: "Hadits\nArba'in",
|
|
color: const Color(0xFF6C5CE7),
|
|
isDark: isDark,
|
|
onTap: () {
|
|
if (isSimpleMode) {
|
|
context.push('/hadits');
|
|
} else {
|
|
context.push('/tools/hadits');
|
|
}
|
|
},
|
|
),
|
|
ToolCard(
|
|
icon: AppIcons.quranEnrichment,
|
|
title: "Pendalaman\nAl-Qur'an",
|
|
color: const Color(0xFF00CEC9),
|
|
isDark: isDark,
|
|
onTap: () {
|
|
if (isSimpleMode) {
|
|
context.push('/quran/enrichment');
|
|
} else {
|
|
context.push('/tools/quran/enrichment');
|
|
}
|
|
},
|
|
),
|
|
];
|
|
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
'AKSES CEPAT',
|
|
style: TextStyle(
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w700,
|
|
letterSpacing: 1.5,
|
|
color: AppColors.sage,
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
_buildQuickActionsGrid(cards),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildQuickActionsGrid(List<Widget> cards) {
|
|
const spacing = 12.0;
|
|
return LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
final cardWidth = (constraints.maxWidth - spacing) / 2;
|
|
return Wrap(
|
|
spacing: spacing,
|
|
runSpacing: spacing,
|
|
children: [
|
|
for (final card in cards) SizedBox(width: cardWidth, child: card),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildAyatHariIni(BuildContext context, bool isDark) {
|
|
return const AyatTodayCard(
|
|
headerText: 'AYAT HARI INI',
|
|
headerStyle: TextStyle(
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w700,
|
|
letterSpacing: 1.5,
|
|
color: AppColors.sage,
|
|
),
|
|
);
|
|
}
|
|
}
|