feat: complete Simple Mode contextual routing and navigation state synchronization

This commit is contained in:
dwindown
2026-03-15 07:24:13 +07:00
parent faadc1865d
commit 25728583b3
21 changed files with 1095 additions and 320 deletions

View File

@@ -4,10 +4,14 @@ 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/theme/app_colors.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/services/equran_service.dart';
import '../data/prayer_times_provider.dart';
class DashboardScreen extends ConsumerStatefulWidget {
@@ -19,18 +23,31 @@ class DashboardScreen extends ConsumerStatefulWidget {
class _DashboardScreenState extends ConsumerState<DashboardScreen> {
Timer? _countdownTimer;
Duration _countdown = Duration.zero;
String _nextPrayerName = '';
final ValueNotifier<Duration> _countdown = ValueNotifier(Duration.zero);
final ValueNotifier<String> _nextPrayerName = ValueNotifier('');
final ScrollController _prayerScrollController = ScrollController();
bool _hasAutoScrolled = false;
DaySchedule? _currentSchedule;
bool get _isSimpleMode {
final box = Hive.box<AppSettings>(HiveBoxes.settings);
final settings = box.get('default');
return settings?.simpleMode ?? false;
}
@override
void dispose() {
_countdownTimer?.cancel();
_prayerScrollController.dispose();
_countdown.dispose();
_nextPrayerName.dispose();
super.dispose();
}
void _startCountdown(DaySchedule schedule) {
if (_currentSchedule == schedule) return;
_currentSchedule = schedule;
_countdownTimer?.cancel();
_updateCountdown(schedule);
_countdownTimer = Timer.periodic(const Duration(seconds: 1), (_) {
@@ -49,11 +66,9 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
if (target.isBefore(now)) {
target = target.add(const Duration(days: 1));
}
setState(() {
_nextPrayerName = next.name;
_countdown = target.difference(now);
if (_countdown.isNegative) _countdown = Duration.zero;
});
_nextPrayerName.value = next.name;
final diff = target.difference(now);
_countdown.value = diff.isNegative ? Duration.zero : diff;
}
}
}
@@ -69,7 +84,14 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
final prayerTimesAsync = ref.watch(prayerTimesProvider);
ref.listen<AsyncValue<DaySchedule?>>(prayerTimesProvider, (previous, next) {
next.whenData((schedule) {
if (schedule != null) {
_startCountdown(schedule);
}
});
});
return Scaffold(
body: SafeArea(
@@ -81,23 +103,40 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
const SizedBox(height: 8),
_buildHeader(context, isDark),
const SizedBox(height: 20),
prayerTimesAsync.when(
data: (schedule) {
if (schedule != null) {
_startCountdown(schedule);
return _buildHeroCard(context, schedule);
}
return _buildHeroCardPlaceholder(context);
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),
);
},
loading: () => _buildHeroCardPlaceholder(context),
error: (_, __) => _buildHeroCardPlaceholder(context),
),
const SizedBox(height: 24),
_buildPrayerTimesSection(context, prayerTimesAsync),
const SizedBox(height: 24),
_buildChecklistSummary(context, isDark),
const SizedBox(height: 24),
_buildWeeklyProgress(context, isDark),
Consumer(
builder: (context, ref, child) {
final prayerTimesAsync = ref.watch(prayerTimesProvider);
return _buildPrayerTimesSection(context, prayerTimesAsync);
},
),
// Checklist & Weekly Progress (hidden in Simple Mode)
if (!_isSimpleMode) ...[
const SizedBox(height: 24),
_buildChecklistSummary(context, isDark),
const SizedBox(height: 24),
_buildWeeklyProgress(context, isDark),
] else ...[
const SizedBox(height: 24),
_buildQuickActions(context, isDark),
const SizedBox(height: 24),
_buildAyatHariIni(context, isDark),
],
const SizedBox(height: 24),
],
),
@@ -117,7 +156,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
border: Border.all(color: AppColors.primary, width: 2),
color: AppColors.primary.withValues(alpha: 0.2),
),
child: const Icon(Icons.person, size: 20, color: AppColors.primary),
child: const Icon(LucideIcons.user, size: 20, color: AppColors.primary),
),
const SizedBox(width: 12),
Expanded(
@@ -146,7 +185,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
IconButton(
onPressed: () {},
icon: Icon(
Icons.notifications_outlined,
LucideIcons.bell,
color: isDark
? AppColors.textSecondaryDark
: AppColors.textSecondaryLight,
@@ -155,7 +194,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
IconButton(
onPressed: () => context.push('/settings'),
icon: Icon(
Icons.settings_outlined,
LucideIcons.settings,
color: isDark
? AppColors.textSecondaryDark
: AppColors.textSecondaryLight,
@@ -169,9 +208,6 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
Widget _buildHeroCard(BuildContext context, DaySchedule schedule) {
final next = schedule.nextPrayer;
final name = _nextPrayerName.isNotEmpty
? _nextPrayerName
: (next?.name ?? 'Isya');
final time = next?.time ?? '--:--';
return Container(
@@ -207,7 +243,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
children: [
Row(
children: [
Icon(Icons.schedule,
Icon(LucideIcons.clock,
size: 16,
color: AppColors.onPrimary.withValues(alpha: 0.8)),
const SizedBox(width: 6),
@@ -223,22 +259,35 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
],
),
const SizedBox(height: 8),
Text(
'$name$time',
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.w800,
color: AppColors.onPrimary,
),
ValueListenableBuilder<String>(
valueListenable: _nextPrayerName,
builder: (context, prayerName, _) {
final name = prayerName.isNotEmpty
? prayerName
: (next?.name ?? 'Isya');
return Text(
'$name$time',
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.w800,
color: AppColors.onPrimary,
),
);
},
),
const SizedBox(height: 4),
Text(
'Hitung mundur: ${_formatCountdown(_countdown)}',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
color: AppColors.onPrimary.withValues(alpha: 0.8),
),
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
@@ -264,7 +313,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
child: const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.explore, size: 18, color: Colors.white),
Icon(LucideIcons.compass, size: 18, color: Colors.white),
SizedBox(width: 8),
Text(
'Arah Kiblat',
@@ -288,7 +337,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
shape: BoxShape.circle,
),
child: const Icon(
Icons.volume_up,
LucideIcons.volume2,
color: AppColors.onPrimary,
size: 22,
),
@@ -342,7 +391,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
),
child: Text(
prayerTimesAsync.value?.isTomorrow == true ? 'BESOK' : 'HARI INI',
style: TextStyle(
style: const TextStyle(
color: AppColors.primary,
fontSize: 10,
fontWeight: FontWeight.w700,
@@ -371,7 +420,8 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
final p = prayers[i];
final icon = _prayerIcon(p.name);
// Auto-scroll to active prayer on first build
if (p.isActive && i > 0) {
if (p.isActive && i > 0 && !_hasAutoScrolled) {
_hasAutoScrolled = true;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_prayerScrollController.hasClients) {
final targetOffset = i * 124.0; // 112 width + 12 gap
@@ -405,17 +455,17 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
IconData _prayerIcon(String name) {
switch (name) {
case 'Subuh':
return Icons.wb_twilight;
return LucideIcons.sunrise;
case 'Dzuhur':
return Icons.wb_sunny;
return LucideIcons.sun;
case 'Ashar':
return Icons.filter_drama;
return LucideIcons.cloudSun;
case 'Maghrib':
return Icons.wb_twilight;
return LucideIcons.sunset;
case 'Isya':
return Icons.dark_mode;
return LucideIcons.moon;
default:
return Icons.schedule;
return LucideIcons.clock;
}
}
@@ -436,7 +486,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
String amalanText = 'Belum ada data';
if (log != null) {
List<String> aList = [];
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');
@@ -556,7 +606,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
child: Row(
children: [
Icon(
completed ? Icons.check_circle : Icons.radio_button_unchecked,
completed ? LucideIcons.checkCircle2 : LucideIcons.circle,
color: AppColors.primary,
size: 22,
),
@@ -670,4 +720,184 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
],
);
}
Widget _buildQuickActions(BuildContext context, bool isDark) {
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),
Row(
children: [
Expanded(
child: ToolCard(
icon: LucideIcons.bookOpen,
title: 'Al-Quran\nTerjemahan',
color: const Color(0xFF00B894),
isDark: isDark,
onTap: () {
final isSimple = Hive.box<AppSettings>(HiveBoxes.settings).get('default')?.simpleMode ?? false;
if (isSimple) {
context.go('/quran');
} else {
context.push('/tools/quran');
}
},
),
),
const SizedBox(width: 12),
Expanded(
child: ToolCard(
icon: LucideIcons.headphones,
title: 'Quran\nMurattal',
color: const Color(0xFF7B61FF),
isDark: isDark,
onTap: () {
final isSimple = Hive.box<AppSettings>(HiveBoxes.settings).get('default')?.simpleMode ?? false;
if (isSimple) {
context.go('/quran/1/murattal');
} else {
context.push('/tools/quran/1/murattal');
}
},
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: ToolCard(
icon: LucideIcons.compass,
title: 'Arah\nKiblat',
color: const Color(0xFF0984E3),
isDark: isDark,
onTap: () {
final isSimple = Hive.box<AppSettings>(HiveBoxes.settings).get('default')?.simpleMode ?? false;
if (isSimple) {
context.push('/qibla');
} else {
context.push('/tools/qibla');
}
},
),
),
const SizedBox(width: 12),
Expanded(
child: ToolCard(
icon: LucideIcons.sparkles,
title: 'Tasbih\nDigital',
color: AppColors.primary,
isDark: isDark,
onTap: () {
final isSimple = Hive.box<AppSettings>(HiveBoxes.settings).get('default')?.simpleMode ?? false;
if (isSimple) {
context.go('/dzikir');
} else {
context.push('/tools/dzikir');
}
},
),
),
],
),
],
);
}
Widget _buildAyatHariIni(BuildContext context, bool isDark) {
return FutureBuilder<Map<String, dynamic>?>(
future: EQuranService.instance.getDailyAyat(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: isDark ? AppColors.primary.withValues(alpha: 0.08) : const Color(0xFFF5F9F0),
borderRadius: BorderRadius.circular(16),
),
child: const Center(child: CircularProgressIndicator()),
);
}
if (!snapshot.hasData || snapshot.data == null) {
return const SizedBox.shrink();
}
final data = snapshot.data!;
return Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: isDark ? AppColors.primary.withValues(alpha: 0.08) : const Color(0xFFF5F9F0),
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'AYAT HARI INI',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w700,
letterSpacing: 1.5,
color: AppColors.sage,
),
),
Icon(LucideIcons.quote,
size: 20,
color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight),
],
),
const SizedBox(height: 16),
Align(
alignment: Alignment.centerRight,
child: Text(
data['teksArab'] ?? '',
style: const TextStyle(
fontFamily: 'Amiri',
fontSize: 24,
height: 1.8,
),
textAlign: TextAlign.right,
),
),
const SizedBox(height: 16),
Text(
'"${data['teksIndonesia'] ?? ''}"',
style: TextStyle(
fontSize: 14,
fontStyle: FontStyle.italic,
height: 1.5,
color: isDark ? Colors.white : Colors.black87,
),
),
const SizedBox(height: 12),
Text(
'QS. ${data['surahName']}: ${data['nomorAyat']}',
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: AppColors.primary,
),
),
],
),
);
},
);
}
}