Files
jamshalat-diary/lib/features/dashboard/presentation/dashboard_screen.dart
2026-03-18 00:07:10 +07:00

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,
),
);
}
}