feat: complete Simple Mode contextual routing and navigation state synchronization
This commit is contained in:
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user