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

142
handoff.md Normal file
View File

@@ -0,0 +1,142 @@
# Jamshalat Diary — Handoff Document
> Last updated: 2026-03-15
---
## Current State
The app is a Flutter-based Islamic daily companion with two operating modes:
- **Mode Lengkap** — Full features with 5 tabs: Beranda, Jadwal, Ibadah, Laporan, Alat
- **Mode Simple** — Streamlined with 5 tabs: Beranda, Jadwal, Tilawah, Murattal, Zikir
### Routing Architecture
Routes are defined in [router.dart](file:///Users/dwindown/CascadeProjects/jamshalat-diary/lib/app/router.dart). Key design:
- **Dual route paths** — Quran, Murattal, and Dzikir each exist as:
- Top-level routes (`/quran`, `/dzikir`) for Simple Mode bottom bar tabs
- Nested routes (`/tools/quran`, `/tools/dzikir`) for Full Mode Alat sub-screens
- **`parentNavigatorKey: _rootNavigatorKey`** — Used on routes that should **hide** the bottom nav bar (settings, qibla, murattal playback, all `/tools/*` sub-screens)
- **`isSimpleModeTab` flag** — Passed to screen widgets to control back button visibility
- **`ValueListenableBuilder`** — Wraps [_ScaffoldWithNav](file:///Users/dwindown/CascadeProjects/jamshalat-diary/lib/app/router.dart#174-259) and [AppBottomNavBar](file:///Users/dwindown/CascadeProjects/jamshalat-diary/lib/core/widgets/bottom_nav_bar.dart#10-108) to reactively update tabs when mode is toggled
### ⚠️ Critical: Hive Key Convention
Settings are stored under the Hive key **`'default'`**, NOT `HiveBoxes.settings`. This was a bug that was fixed across 7 files. Always use:
```dart
box.get('default')?.simpleMode ?? false;
```
### Completed Features (Steps 113)
| Step | Feature |
|---|---|
| 1 | myQuran Sholat API integration + Hive caching |
| 2 | EQuran.id API for Quran list, reading, audio, Ayat Hari Ini |
| 3 | Full Bahasa Indonesia localization |
| 4 | Ibadah Harian revamp (ShalatLog, TilawahLog, DzikirLog, PuasaLog) |
| 5 | UX/UI polish (Rawatib info, Kiblat compass, AppBar consistency) |
| 6 | Gamification point system |
| 7 | Tilawah active session tracker with cross-surah diff |
| 8 | Quran bookmarks (Last Read + Favorit Ayat) |
| 9 | Per-ayat audio playback |
| 10 | Full surah Murattal player with Qari selector |
| 11 | Murattal playlist navigation + auto-play next |
| 12 | Mode Hafalan (loop ayat range) |
| 13 | Murattal screen enhancements |
| — | Simple Mode / Full Mode dynamic routing |
| — | Lucide Icons migration |
| — | Live search debouncer |
| — | Bottom nav bar dynamic tabs |
---
## New API: muslim.backoffice.biz.id
Base URL: `https://muslim.backoffice.biz.id`
### A. Replacement — Migrate Data Sources
Replace current EQuran.id and local JSON with the user's own API for better control and reliability.
| Current Source | New Endpoint | Notes |
|---|---|---|
| EQuran.id Surah list | `GET /v1/quran/surah` | Includes `audio_url` (Mishari Alafasy) |
| EQuran.id Ayat per surah | `GET /v1/quran/ayah/surah?id={surahId}` | Arab, latin, translation, per-ayat audio, juz, page, hizb |
| Local dzikir JSON | `GET /v1/dzikir?type={pagi\|sore\|solat}` | Adds Dzikir Sesudah Sholat |
| EQuran.id random ayat | `GET /v1/quran/ayah/specific?surahId=X&ayahId=Y` | For Ayat Hari Ini |
### B. Enrichment — Enhance Existing Screens
| Feature | Endpoint | Where |
|---|---|---|
| Tafsir Kemenag | `GET /v1/quran/tafsir` | New tab on Quran Reading screen |
| Kata Per Kata (word-by-word) | `GET /v1/quran/word/ayah?surahId=X&ayahId=Y` | Toggle in Quran Reading screen |
| Browse by Juz | `GET /v1/quran/juz` | Option in Tilawah screen |
| Browse by Page (Mushaf) | `GET /v1/quran/ayah/page?id={pageId}` | Option in Tilawah screen |
| Asbabun Nuzul | `GET /v1/quran/asbab` | Inline on verses that have `asbab` reference |
| Asmaul Husna | `GET /v1/quran/asma` | Mini-feature in Alat/Beranda |
| Quran Theme index | `GET /v1/quran/theme` | Browse by Topic in Tilawah |
| Quran Search by meaning | `GET /v1/quran/ayah/find?query={word}` | Search bar in Tilawah screen |
### C. New Screens
Each new screen should be:
- Added to the **Alat** screen grid in Full Mode
- Listed in the **Beranda** Quick Actions in Simple Mode
- Routed under `/tools/{feature}` (Full Mode, hides bottom bar) and optionally as a top-level route for Simple Mode
| Screen | Endpoint | Description |
|---|---|---|
| **Kumpulan Doa** | `GET /v1/doa` | Categorized prayers (quran, hadits, harian, ibadah, haji, etc.) with search via `/v1/doa/find?query=X` |
| **Hadits Arba'in** | `GET /v1/hadits` | 42 Hadits with Arabic + Indonesian, searchable via `/v1/hadits/find?query=X` |
---
## Implementation Priority
### Phase 1: API Service Layer
- [ ] Create `muslim_api_service.dart` in `lib/data/services/`
- [ ] Base URL: `https://muslim.backoffice.biz.id`
- [ ] Implement HTTP client with error handling + caching pattern (similar to `equran_service.dart`)
### Phase 2: Replacement (migrate data sources)
- [ ] Migrate Surah listing to `/v1/quran/surah`
- [ ] Migrate Ayat reading to `/v1/quran/ayah/surah?id=X`
- [ ] Migrate Murattal audio URLs to new API `audio_url` field
- [ ] Migrate Dzikir data from local JSON to `/v1/dzikir?type=X`
- [ ] Add Dzikir Sesudah Sholat category
### Phase 3: Enrichment
- [ ] Quran Tafsir tab on reading screen
- [ ] Kata Per Kata toggle on reading screen
- [ ] Juz browser in Tilawah
- [ ] Quran search by meaning
- [ ] Asbabun Nuzul inline display
- [ ] Asmaul Husna mini-feature
### Phase 4: New Screens
- [ ] Kumpulan Doa screen + route + Alat grid entry + Beranda quick action
- [ ] Hadits Arba'in screen + route + Alat grid entry + Beranda quick action
---
## Key Files Reference
| File | Purpose |
|---|---|
| `lib/app/router.dart` | All navigation routes, dual-path routing, bottom nav logic |
| `lib/core/widgets/bottom_nav_bar.dart` | Dynamic bottom nav tabs (ValueListenableBuilder) |
| `lib/features/dashboard/presentation/dashboard_screen.dart` | Beranda with Quick Actions |
| `lib/features/tools/presentation/tools_screen.dart` | Alat grid |
| `lib/features/settings/presentation/settings_screen.dart` | Mode toggle (saves to Hive key `'default'`) |
| `lib/data/local/hive_boxes.dart` | Hive initialization + seed defaults |
| `lib/data/local/models/app_settings.dart` | Settings model with `simpleMode` field |
| `lib/data/services/equran_service.dart` | Current Quran API client (to be replaced) |
| `lib/features/quran/presentation/quran_screen.dart` | Tilawah screen |
| `lib/features/quran/presentation/quran_reading_screen.dart` | Quran reading |
| `lib/features/quran/presentation/quran_murattal_screen.dart` | Murattal player |
| `lib/features/dzikir/presentation/dzikir_screen.dart` | Dzikir counter |

View File

@@ -1,5 +1,8 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:hive_flutter/hive_flutter.dart';
import '../data/local/hive_boxes.dart';
import '../data/local/models/app_settings.dart';
import '../core/widgets/bottom_nav_bar.dart';
import '../features/dashboard/presentation/dashboard_screen.dart';
@@ -115,6 +118,47 @@ final GoRouter appRouter = GoRouter(
),
],
),
// Simple Mode Tab: Zikir
GoRoute(
path: '/dzikir',
builder: (context, state) => const DzikirScreen(isSimpleModeTab: true),
),
// Simple Mode Tab: Tilawah
GoRoute(
path: '/quran',
builder: (context, state) => const QuranScreen(isSimpleModeTab: true),
routes: [
GoRoute(
path: 'bookmarks',
builder: (context, state) => const QuranBookmarksScreen(),
),
GoRoute(
path: ':surahId',
builder: (context, state) {
final surahId = state.pathParameters['surahId']!;
final startVerse = int.tryParse(state.uri.queryParameters['startVerse'] ?? '');
return QuranReadingScreen(surahId: surahId, initialVerse: startVerse, isSimpleModeTab: true);
},
routes: [
GoRoute(
path: 'murattal',
parentNavigatorKey: _rootNavigatorKey,
builder: (context, state) {
final surahId = state.pathParameters['surahId']!;
final qariId = state.uri.queryParameters['qariId'];
final autoplay = state.uri.queryParameters['autoplay'] == 'true';
return QuranMurattalScreen(
surahId: surahId,
initialQariId: qariId,
autoPlay: autoplay,
isSimpleModeTab: true,
);
},
),
],
),
],
),
],
),
// ── Settings (pushed, no bottom nav) ──
@@ -135,41 +179,80 @@ class _ScaffoldWithNav extends StatelessWidget {
/// Maps route locations to bottom nav indices.
int _currentIndex(BuildContext context) {
final location = GoRouterState.of(context).uri.toString();
if (location.startsWith('/imsakiyah')) return 1;
if (location.startsWith('/checklist')) return 2;
if (location.startsWith('/laporan')) return 3;
if (location.startsWith('/tools')) return 4;
return 0;
final box = Hive.box<AppSettings>(HiveBoxes.settings);
final isSimpleMode = box.get('default')?.simpleMode ?? false;
if (isSimpleMode) {
if (location.startsWith('/imsakiyah')) return 1;
if (location.startsWith('/quran') && !location.contains('/murattal')) return 2;
if (location.contains('/murattal')) return 3;
if (location.startsWith('/dzikir')) return 4;
return 0;
} else {
if (location.startsWith('/imsakiyah')) return 1;
if (location.startsWith('/checklist')) return 2;
if (location.startsWith('/laporan')) return 3;
if (location.startsWith('/tools')) return 4;
return 0;
}
}
void _onTap(BuildContext context, int index) {
switch (index) {
case 0:
context.go('/');
break;
case 1:
context.go('/imsakiyah');
break;
case 2:
context.go('/checklist');
break;
case 3:
context.go('/laporan');
break;
case 4:
context.go('/tools');
break;
final box = Hive.box<AppSettings>(HiveBoxes.settings);
final isSimpleMode = box.get('default')?.simpleMode ?? false;
if (isSimpleMode) {
switch (index) {
case 0:
context.go('/');
break;
case 1:
context.go('/imsakiyah');
break;
case 2:
context.go('/quran');
break;
case 3:
context.push('/quran/1/murattal');
break;
case 4:
context.go('/dzikir');
break;
}
} else {
switch (index) {
case 0:
context.go('/');
break;
case 1:
context.go('/imsakiyah');
break;
case 2:
context.go('/checklist');
break;
case 3:
context.go('/laporan');
break;
case 4:
context.go('/tools');
break;
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: child,
bottomNavigationBar: AppBottomNavBar(
currentIndex: _currentIndex(context),
onTap: (i) => _onTap(context, i),
),
return ValueListenableBuilder<Box<AppSettings>>(
valueListenable: Hive.box<AppSettings>(HiveBoxes.settings).listenable(),
builder: (context, box, _) {
return Scaffold(
body: child,
bottomNavigationBar: AppBottomNavBar(
currentIndex: _currentIndex(context),
onTap: (i) => _onTap(context, i),
),
);
},
);
}
}

View File

@@ -1,5 +1,9 @@
import 'package:flutter/material.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:hive_flutter/hive_flutter.dart';
import '../../app/theme/app_colors.dart';
import '../../data/local/hive_boxes.dart';
import '../../data/local/models/app_settings.dart';
/// 5-tab bottom navigation bar per PRD §5.1.
/// Uses Material Symbols outlined (inactive) and filled (active).
@@ -15,52 +19,89 @@ class AppBottomNavBar extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: Theme.of(context).bottomNavigationBarTheme.backgroundColor,
border: Border(
top: BorderSide(
color: Theme.of(context).dividerColor,
width: 0.5,
return ValueListenableBuilder<Box<AppSettings>>(
valueListenable: Hive.box<AppSettings>(HiveBoxes.settings).listenable(),
builder: (context, box, _) {
final isSimpleMode = box.get('default')?.simpleMode ?? false;
final simpleItems = const [
BottomNavigationBarItem(
icon: Icon(LucideIcons.home),
activeIcon: Icon(LucideIcons.home),
label: 'Beranda',
),
),
),
child: SafeArea(
child: Padding(
padding: const EdgeInsets.only(bottom: 8),
child: BottomNavigationBar(
currentIndex: currentIndex,
onTap: onTap,
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.home_outlined),
activeIcon: Icon(Icons.home),
label: 'Beranda',
),
BottomNavigationBarItem(
icon: Icon(Icons.calendar_today_outlined),
activeIcon: Icon(Icons.calendar_today),
label: 'Jadwal',
),
BottomNavigationBarItem(
icon: Icon(Icons.rule_outlined),
activeIcon: Icon(Icons.rule),
label: 'Ibadah',
),
BottomNavigationBarItem(
icon: Icon(Icons.bar_chart_outlined),
activeIcon: Icon(Icons.bar_chart),
label: 'Laporan',
),
BottomNavigationBarItem(
icon: Icon(Icons.auto_fix_high_outlined),
activeIcon: Icon(Icons.auto_fix_high),
label: 'Alat',
),
],
BottomNavigationBarItem(
icon: Icon(LucideIcons.calendar),
activeIcon: Icon(LucideIcons.calendar),
label: 'Jadwal',
),
),
),
BottomNavigationBarItem(
icon: Icon(LucideIcons.bookOpen),
activeIcon: Icon(LucideIcons.bookOpen),
label: 'Tilawah',
),
BottomNavigationBarItem(
icon: Icon(LucideIcons.headphones),
activeIcon: Icon(LucideIcons.headphones),
label: 'Murattal',
),
BottomNavigationBarItem(
icon: Icon(LucideIcons.sparkles),
activeIcon: Icon(LucideIcons.sparkles),
label: 'Zikir',
),
];
final fullItems = const [
BottomNavigationBarItem(
icon: Icon(LucideIcons.home),
activeIcon: Icon(LucideIcons.home),
label: 'Beranda',
),
BottomNavigationBarItem(
icon: Icon(LucideIcons.calendar),
activeIcon: Icon(LucideIcons.calendar),
label: 'Jadwal',
),
BottomNavigationBarItem(
icon: Icon(LucideIcons.listChecks),
activeIcon: Icon(LucideIcons.listChecks),
label: 'Ibadah',
),
BottomNavigationBarItem(
icon: Icon(LucideIcons.barChart3),
activeIcon: Icon(LucideIcons.barChart3),
label: 'Laporan',
),
BottomNavigationBarItem(
icon: Icon(LucideIcons.wand2),
activeIcon: Icon(LucideIcons.wand2),
label: 'Alat',
),
];
return Container(
decoration: BoxDecoration(
color: Theme.of(context).bottomNavigationBarTheme.backgroundColor,
border: Border(
top: BorderSide(
color: Theme.of(context).dividerColor,
width: 0.5,
),
),
),
child: SafeArea(
child: Padding(
padding: const EdgeInsets.only(bottom: 8),
child: BottomNavigationBar(
currentIndex: currentIndex,
onTap: onTap,
items: isSimpleMode ? simpleItems : fullItems,
),
),
),
);
},
);
}
}

View File

@@ -0,0 +1,69 @@
import 'package:flutter/material.dart';
import '../../app/theme/app_colors.dart';
class ToolCard extends StatelessWidget {
final IconData icon;
final String title;
final Color color;
final bool isDark;
final VoidCallback onTap;
const ToolCard({
super.key,
required this.icon,
required this.title,
required this.color,
required this.isDark,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
height: 140,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: isDark ? AppColors.surfaceDark : AppColors.surfaceLight,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: isDark
? color.withValues(alpha: 0.15)
: AppColors.cream,
),
boxShadow: [
BoxShadow(
color: color.withValues(alpha: 0.08),
blurRadius: 12,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: color.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(12),
),
child: Icon(icon, color: color, size: 24),
),
Text(
title,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w700,
height: 1.3,
),
),
],
),
),
);
}
}

View File

@@ -62,6 +62,9 @@ class AppSettings extends HiveObject {
@HiveField(18)
bool showTerjemahan;
@HiveField(19)
bool simpleMode; // false = Mode Lengkap, true = Mode Simpel
AppSettings({
this.userName = 'User',
this.userEmail = '',
@@ -82,6 +85,7 @@ class AppSettings extends HiveObject {
this.trackPuasa = false,
this.showLatin = true,
this.showTerjemahan = true,
this.simpleMode = false,
}) : adhanEnabled = adhanEnabled ??
{
'fajr': true,

View File

@@ -36,13 +36,14 @@ class AppSettingsAdapter extends TypeAdapter<AppSettings> {
trackPuasa: fields.containsKey(16) ? fields[16] as bool? ?? false : false,
showLatin: fields.containsKey(17) ? fields[17] as bool? ?? true : true,
showTerjemahan: fields.containsKey(18) ? fields[18] as bool? ?? true : true,
simpleMode: fields.containsKey(19) ? fields[19] as bool? ?? false : false,
);
}
@override
void write(BinaryWriter writer, AppSettings obj) {
writer
..writeByte(19)
..writeByte(20)
..writeByte(0)
..write(obj.userName)
..writeByte(1)
@@ -80,7 +81,9 @@ class AppSettingsAdapter extends TypeAdapter<AppSettings> {
..writeByte(17)
..write(obj.showLatin)
..writeByte(18)
..write(obj.showTerjemahan);
..write(obj.showTerjemahan)
..writeByte(19)
..write(obj.simpleMode);
}
@override

View File

@@ -3,6 +3,8 @@ import 'shalat_log.dart';
import 'tilawah_log.dart';
import 'dzikir_log.dart';
import 'puasa_log.dart';
import 'app_settings.dart';
import '../hive_boxes.dart';
part 'daily_worship_log.g.dart';
@@ -46,6 +48,11 @@ class DailyWorshipLog extends HiveObject {
/// Dynamically calculates the "Poin Ibadah" for this day.
int get totalPoints {
// Return 0 points if simple mode is active
final settingsBox = Hive.box<AppSettings>(HiveBoxes.settings);
final isSimpleMode = settingsBox.get('default')?.simpleMode ?? false;
if (isSimpleMode) return 0;
int points = 0;
// 1. Shalat Fardhu

View File

@@ -2,6 +2,7 @@ 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:lucide_icons/lucide_icons.dart';
import 'package:intl/intl.dart';
import '../../../app/theme/app_colors.dart';
import '../../../core/widgets/progress_bar.dart';
@@ -163,11 +164,11 @@ class _ChecklistScreenState extends ConsumerState<ChecklistScreen> {
actions: [
IconButton(
onPressed: () {},
icon: const Icon(Icons.notifications_outlined),
icon: const Icon(LucideIcons.bell),
),
IconButton(
onPressed: () => context.push('/settings'),
icon: const Icon(Icons.settings_outlined),
icon: const Icon(LucideIcons.settings),
),
const SizedBox(width: 8),
],
@@ -176,8 +177,10 @@ class _ChecklistScreenState extends ConsumerState<ChecklistScreen> {
padding: const EdgeInsets.symmetric(horizontal: 16),
children: [
const SizedBox(height: 12),
_buildProgressCard(log, isDark),
const SizedBox(height: 24),
if (!_settings.simpleMode) ...[
_buildProgressCard(log, isDark),
const SizedBox(height: 24),
],
_sectionLabel('SHOLAT FARDHU & RAWATIB'),
const SizedBox(height: 12),
..._fardhuPrayers.map((p) => _buildShalatCard(p, isDark)).toList(),
@@ -250,7 +253,7 @@ class _ChecklistScreenState extends ConsumerState<ChecklistScreen> {
),
child: Row(
children: [
const Icon(Icons.stars, color: AppColors.primary, size: 14),
const Icon(LucideIcons.star, color: AppColors.primary, size: 14),
const SizedBox(width: 4),
Text(
'${log.totalPoints} pts',
@@ -347,7 +350,7 @@ class _ChecklistScreenState extends ConsumerState<ChecklistScreen> {
: (isDark ? AppColors.primary.withValues(alpha: 0.08) : AppColors.cream.withValues(alpha: 0.5)),
borderRadius: BorderRadius.circular(12),
),
child: Icon(Icons.mosque, size: 22, color: isCompleted ? AppColors.primary : AppColors.sage),
child: Icon(LucideIcons.building, size: 22, color: isCompleted ? AppColors.primary : AppColors.sage),
),
title: Text(
'Sholat $prayerName',
@@ -414,7 +417,7 @@ class _ChecklistScreenState extends ConsumerState<ChecklistScreen> {
child: Row(
children: [
Icon(
selected ? Icons.radio_button_checked : Icons.radio_button_off,
selected ? LucideIcons.checkCircle2 : LucideIcons.circle,
size: 18,
color: selected ? AppColors.primary : Colors.grey,
),
@@ -467,7 +470,7 @@ class _ChecklistScreenState extends ConsumerState<ChecklistScreen> {
: (isDark ? AppColors.primary.withValues(alpha: 0.08) : AppColors.cream.withValues(alpha: 0.5)),
borderRadius: BorderRadius.circular(12),
),
child: Icon(Icons.menu_book, size: 22, color: log.isCompleted ? AppColors.primary : AppColors.sage),
child: Icon(LucideIcons.bookOpen, size: 22, color: log.isCompleted ? AppColors.primary : AppColors.sage),
),
const SizedBox(width: 14),
Expanded(
@@ -505,7 +508,7 @@ class _ChecklistScreenState extends ConsumerState<ChecklistScreen> {
// ── Row 2: Ayat Tracker ──
Row(
children: [
Icon(Icons.auto_stories, size: 18, color: AppColors.sage),
Icon(LucideIcons.bookOpen, size: 18, color: AppColors.sage),
const SizedBox(width: 8),
Expanded(
child: Text(
@@ -520,10 +523,10 @@ class _ChecklistScreenState extends ConsumerState<ChecklistScreen> {
if (log.autoSync)
Tooltip(
message: 'Sinkron dari Al-Quran',
child: Icon(Icons.sync, size: 16, color: AppColors.primary),
child: Icon(LucideIcons.refreshCw, size: 16, color: AppColors.primary),
),
IconButton(
icon: const Icon(Icons.remove_circle_outline, size: 20),
icon: const Icon(LucideIcons.minusCircle, size: 20),
visualDensity: VisualDensity.compact,
onPressed: log.rawAyatRead > 0
? () {
@@ -533,7 +536,7 @@ class _ChecklistScreenState extends ConsumerState<ChecklistScreen> {
: null,
),
IconButton(
icon: const Icon(Icons.add_circle_outline, size: 20, color: AppColors.primary),
icon: const Icon(LucideIcons.plusCircle, size: 20, color: AppColors.primary),
visualDensity: VisualDensity.compact,
onPressed: () {
log.rawAyatRead++;
@@ -563,7 +566,7 @@ class _ChecklistScreenState extends ConsumerState<ChecklistScreen> {
children: [
Row(
children: [
Icon(Icons.auto_awesome, size: 20, color: AppColors.sage),
Icon(LucideIcons.sparkles, size: 20, color: AppColors.sage),
const SizedBox(width: 8),
const Text('Dzikir Harian', style: TextStyle(fontWeight: FontWeight.w600, fontSize: 15)),
],
@@ -594,7 +597,7 @@ class _ChecklistScreenState extends ConsumerState<ChecklistScreen> {
),
child: Row(
children: [
const Icon(Icons.nightlight_round, size: 20, color: AppColors.sage),
const Icon(LucideIcons.moonStar, size: 20, color: AppColors.sage),
const SizedBox(width: 8),
const Expanded(child: Text('Puasa Sunnah', style: TextStyle(fontWeight: FontWeight.w600, fontSize: 15))),
DropdownButton<String>(
@@ -641,7 +644,7 @@ class _CustomCheckbox extends StatelessWidget {
borderRadius: BorderRadius.circular(6),
border: value ? null : Border.all(color: Colors.grey, width: 2),
),
child: value ? const Icon(Icons.check, size: 16, color: Colors.white) : null,
child: value ? const Icon(LucideIcons.check, size: 16, color: Colors.white) : null,
),
);
}

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

View File

@@ -2,14 +2,17 @@ import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:intl/intl.dart';
import '../../../app/theme/app_colors.dart';
import '../../../data/local/hive_boxes.dart';
import '../../../data/local/models/dzikir_counter.dart';
import '../../../data/local/models/app_settings.dart';
class DzikirScreen extends ConsumerStatefulWidget {
const DzikirScreen({super.key});
final bool isSimpleModeTab;
const DzikirScreen({super.key, this.isSimpleModeTab = false});
@override
ConsumerState<DzikirScreen> createState() => _DzikirScreenState();
@@ -85,14 +88,17 @@ class _DzikirScreenState extends ConsumerState<DzikirScreen>
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
final box = Hive.box<AppSettings>(HiveBoxes.settings);
final isSimpleMode = box.get('default')?.simpleMode ?? false;
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: !widget.isSimpleModeTab,
title: const Text('Dzikir Pagi & Petang'),
actions: [
IconButton(
onPressed: () {},
icon: const Icon(Icons.info_outline),
icon: const Icon(LucideIcons.info),
),
],
),
@@ -275,7 +281,7 @@ class _DzikirScreenState extends ConsumerState<DzikirScreen>
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
isComplete ? Icons.check : Icons.touch_app,
isComplete ? LucideIcons.check : LucideIcons.fingerprint,
size: 18,
color: isComplete
? AppColors.primary

View File

@@ -1,7 +1,9 @@
import 'dart:async';
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:lucide_icons/lucide_icons.dart';
import 'package:intl/intl.dart';
import '../../../app/theme/app_colors.dart';
import '../../../data/local/hive_boxes.dart';
@@ -94,6 +96,7 @@ class _ImsakiyahScreenState extends ConsumerState<ImsakiyahScreen> {
final searchCtrl = TextEditingController();
bool isSearching = false;
List<Map<String, dynamic>> results = [];
Timer? debounce;
showDialog(
context: context,
@@ -113,7 +116,7 @@ class _ImsakiyahScreenState extends ConsumerState<ImsakiyahScreen> {
hintText: 'Cth: Jakarta',
border: const OutlineInputBorder(),
suffixIcon: IconButton(
icon: const Icon(Icons.search),
icon: const Icon(LucideIcons.search),
onPressed: () async {
if (searchCtrl.text.trim().isEmpty) return;
setDialogState(() => isSearching = true);
@@ -128,8 +131,35 @@ class _ImsakiyahScreenState extends ConsumerState<ImsakiyahScreen> {
},
),
),
onChanged: (val) {
if (val.trim().length < 3) return;
if (debounce?.isActive ?? false) debounce!.cancel();
debounce = Timer(const Duration(milliseconds: 500), () async {
if (!mounted) return;
setDialogState(() => isSearching = true);
try {
final res = await MyQuranSholatService.instance.searchCity(val.trim());
if (mounted) {
setDialogState(() {
results = res;
});
}
} catch (e) {
debugPrint('Error searching city: $e');
} finally {
if (mounted) {
setDialogState(() {
isSearching = false;
});
}
}
});
},
onSubmitted: (val) async {
if (val.trim().isEmpty) return;
if (debounce?.isActive ?? false) debounce!.cancel();
setDialogState(() => isSearching = true);
final res = await MyQuranSholatService.instance
.searchCity(val.trim());
@@ -207,11 +237,11 @@ class _ImsakiyahScreenState extends ConsumerState<ImsakiyahScreen> {
actions: [
IconButton(
onPressed: () {},
icon: const Icon(Icons.notifications_outlined),
icon: const Icon(LucideIcons.bell),
),
IconButton(
onPressed: () => context.push('/settings'),
icon: const Icon(Icons.settings_outlined),
icon: const Icon(LucideIcons.settings),
),
const SizedBox(width: 8),
],
@@ -286,7 +316,7 @@ class _ImsakiyahScreenState extends ConsumerState<ImsakiyahScreen> {
),
child: Row(
children: [
const Icon(Icons.location_on,
const Icon(LucideIcons.mapPin,
color: AppColors.primary, size: 24),
const SizedBox(width: 12),
Expanded(
@@ -309,7 +339,7 @@ class _ImsakiyahScreenState extends ConsumerState<ImsakiyahScreen> {
],
),
),
Icon(Icons.expand_more,
Icon(LucideIcons.chevronDown,
color: isDark
? AppColors.textSecondaryDark
: AppColors.textSecondaryLight),

View File

@@ -3,9 +3,11 @@ import 'package:go_router/go_router.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:intl/intl.dart';
import 'package:lucide_icons/lucide_icons.dart';
import '../../../app/theme/app_colors.dart';
import '../../../core/widgets/progress_bar.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/checklist_item.dart';
@@ -168,6 +170,31 @@ class _LaporanScreenState extends ConsumerState<LaporanScreen>
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
final settingsBox = Hive.box<AppSettings>(HiveBoxes.settings);
final isSimpleMode = settingsBox.get('default')?.simpleMode ?? false;
if (isSimpleMode) {
return Scaffold(
appBar: AppBar(
title: const Text('Riwayat Ibadah'),
centerTitle: false,
actions: [
IconButton(
onPressed: () {},
icon: const Icon(LucideIcons.bell),
),
IconButton(
onPressed: () => context.push('/settings'),
icon: const Icon(LucideIcons.settings),
),
const SizedBox(width: 8),
],
),
body: _buildRiwayatSimpel(context, isDark),
);
}
final weekData = _getWeeklyData();
final avgPercent = _weekAverage(weekData);
final insights = _getInsights();
@@ -179,11 +206,11 @@ class _LaporanScreenState extends ConsumerState<LaporanScreen>
actions: [
IconButton(
onPressed: () {},
icon: const Icon(Icons.notifications_outlined),
icon: const Icon(LucideIcons.bell),
),
IconButton(
onPressed: () => context.push('/settings'),
icon: const Icon(Icons.settings_outlined),
icon: const Icon(LucideIcons.settings),
),
const SizedBox(width: 8),
],
@@ -283,7 +310,7 @@ class _LaporanScreenState extends ConsumerState<LaporanScreen>
color: AppColors.primary.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(10),
),
child: const Icon(Icons.stars,
child: const Icon(LucideIcons.star,
color: AppColors.primary, size: 18),
),
],
@@ -373,7 +400,7 @@ class _LaporanScreenState extends ConsumerState<LaporanScreen>
_insightCard(
context,
isDark,
icon: Icons.star,
icon: LucideIcons.star,
iconBg: AppColors.primary.withValues(alpha: 0.15),
iconColor: AppColors.primary,
label: 'PALING RAJIN',
@@ -386,7 +413,7 @@ class _LaporanScreenState extends ConsumerState<LaporanScreen>
_insightCard(
context,
isDark,
icon: Icons.trending_up,
icon: LucideIcons.trendingUp,
iconBg: const Color(0xFFFFF3E0),
iconColor: Colors.orange,
label: 'PERLU DITINGKATKAN',
@@ -520,21 +547,16 @@ class _LaporanScreenState extends ConsumerState<LaporanScreen>
);
}
Widget _buildComingSoon(BuildContext context, String period) {
Widget _buildComingSoon(BuildContext context, String title) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.bar_chart,
Icon(LucideIcons.barChart3,
size: 48, color: AppColors.primary.withValues(alpha: 0.3)),
const SizedBox(height: 12),
Text(
'Laporan $period',
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
),
const SizedBox(height: 4),
Text(
'Segera hadir',
'$title: Segera hadir',
style: TextStyle(
color: Theme.of(context).brightness == Brightness.dark
? AppColors.textSecondaryDark
@@ -544,6 +566,107 @@ class _LaporanScreenState extends ConsumerState<LaporanScreen>
),
);
}
Widget _buildRiwayatSimpel(BuildContext context, bool isDark) {
final logBox = Hive.box<DailyWorshipLog>(HiveBoxes.worshipLogs);
final now = DateTime.now();
final logs = <DailyWorshipLog>[];
// Fetch up to 14 days of history
for (int i = 0; i < 14; i++) {
final date = now.subtract(Duration(days: i));
final key = DateFormat('yyyy-MM-dd').format(date);
final log = logBox.get(key);
if (log != null && log.totalItems > 0 && log.completedCount > 0) {
logs.add(log);
}
}
if (logs.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(LucideIcons.history, size: 64, color: AppColors.sage.withValues(alpha: 0.5)),
const SizedBox(height: 16),
const Text('Belum ada riwayat ibadah', style: TextStyle(color: AppColors.sage)),
],
),
);
}
return ListView.separated(
padding: const EdgeInsets.all(16),
itemCount: logs.length,
separatorBuilder: (_, __) => const SizedBox(height: 12),
itemBuilder: (context, index) {
final log = logs[index];
final isToday = log.date == DateFormat('yyyy-MM-dd').format(now);
// Build summary text
final List<String> finished = [];
int fardhuCount = log.shalatLogs.values.where((l) => l.completed).length;
if (fardhuCount > 0) finished.add('$fardhuCount Fardhu');
if (log.tilawahLog?.isCompleted == true) finished.add('Tilawah');
if (log.dzikirLog != null) {
int d = 0;
if (log.dzikirLog!.pagi) d++;
if (log.dzikirLog!.petang) d++;
if (d > 0) finished.add('$d Dzikir');
}
return 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: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: AppColors.primary.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(12),
),
child: const Icon(LucideIcons.checkCircle2, color: AppColors.primary),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
isToday ? 'Hari Ini' : DateFormat('EEEE, d MMM yyyy').format(DateTime.parse(log.date)),
style: const TextStyle(
fontWeight: FontWeight.w700,
fontSize: 15,
),
),
const SizedBox(height: 4),
Text(
finished.isNotEmpty ? finished.join('') : 'Belum ada aktivitas',
style: TextStyle(
fontSize: 13,
color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight,
height: 1.4,
),
),
],
),
),
],
),
);
},
);
}
}
class _DayData {

View File

@@ -3,6 +3,7 @@ import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_qiblah/flutter_qiblah.dart';
import 'package:lucide_icons/lucide_icons.dart';
import '../../../app/theme/app_colors.dart';
class QiblaScreen extends ConsumerStatefulWidget {
@@ -163,7 +164,7 @@ class _QiblaScreenState extends ConsumerState<QiblaScreen> {
: AppColors.surfaceLight,
border: Border.all(color: AppColors.cream),
),
child: const Icon(Icons.arrow_back, size: 18),
child: const Icon(LucideIcons.arrowLeft, size: 18),
),
onPressed: () => Navigator.pop(context),
),
@@ -176,7 +177,7 @@ class _QiblaScreenState extends ConsumerState<QiblaScreen> {
color: isDark ? AppColors.surfaceDark : AppColors.surfaceLight,
border: Border.all(color: AppColors.cream),
),
child: Icon(isLive ? Icons.my_location : Icons.location_disabled, size: 18),
child: Icon(isLive ? LucideIcons.locate : LucideIcons.locateOff, size: 18),
),
onPressed: () {
if (isLive) {
@@ -213,7 +214,7 @@ class _QiblaScreenState extends ConsumerState<QiblaScreen> {
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.location_on,
Icon(LucideIcons.mapPin,
size: 16, color: AppColors.primary),
const SizedBox(width: 4),
Text(

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:go_router/go_router.dart';
import 'package:intl/intl.dart';
import '../../../app/theme/app_colors.dart';
@@ -91,7 +92,7 @@ class _QuranBookmarksScreenState extends State<QuranBookmarksScreen> {
centerTitle: false,
actions: [
IconButton(
icon: const Icon(Icons.settings_display),
icon: const Icon(LucideIcons.settings2),
onPressed: _showDisplaySettings,
),
],
@@ -105,7 +106,7 @@ class _QuranBookmarksScreenState extends State<QuranBookmarksScreen> {
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.bookmark_border,
LucideIcons.bookmark,
size: 64,
color: AppColors.primary.withValues(alpha: 0.3),
),
@@ -220,7 +221,7 @@ class _QuranBookmarksScreenState extends State<QuranBookmarksScreen> {
mainAxisSize: MainAxisSize.min,
children: [
if (isLastRead) ...[
const Icon(Icons.push_pin, size: 12, color: AppColors.primary),
const Icon(LucideIcons.pin, size: 12, color: AppColors.primary),
const SizedBox(width: 4),
],
Text(
@@ -235,7 +236,7 @@ class _QuranBookmarksScreenState extends State<QuranBookmarksScreen> {
),
),
IconButton(
icon: const Icon(Icons.delete_outline, color: Colors.red, size: 20),
icon: const Icon(LucideIcons.trash2, color: Colors.red, size: 20),
onPressed: () => box.delete(bookmark.key),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
@@ -287,7 +288,7 @@ class _QuranBookmarksScreenState extends State<QuranBookmarksScreen> {
width: double.infinity,
child: FilledButton.icon(
onPressed: () => context.push('/tools/quran/${bookmark.surahId}?startVerse=${bookmark.verseId}'),
icon: const Icon(Icons.menu_book, size: 18),
icon: const Icon(LucideIcons.bookOpen, size: 18),
label: const Text('Lanjutkan Membaca'),
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
@@ -301,7 +302,7 @@ class _QuranBookmarksScreenState extends State<QuranBookmarksScreen> {
Row(
children: [
Icon(
Icons.access_time,
LucideIcons.clock,
size: 12,
color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight,
),

View File

@@ -5,11 +5,15 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:just_audio/just_audio.dart';
import 'package:go_router/go_router.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../../app/theme/app_colors.dart';
import '../../../data/services/equran_service.dart';
import '../../../data/services/unsplash_service.dart';
import 'package:hive_flutter/hive_flutter.dart';
import '../../../data/local/hive_boxes.dart';
import '../../../data/local/models/app_settings.dart';
/// Quran Murattal (audio player) screen.
/// Implements full Surah playback using just_audio and EQuran v2 API.
@@ -17,11 +21,13 @@ class QuranMurattalScreen extends ConsumerStatefulWidget {
final String surahId;
final String? initialQariId;
final bool autoPlay;
final bool isSimpleModeTab;
const QuranMurattalScreen({
super.key,
required this.surahId,
this.initialQariId,
this.autoPlay = false,
this.isSimpleModeTab = false,
});
@override
@@ -217,7 +223,7 @@ class _QuranMurattalScreenState extends ConsumerState<QuranMurattalScreen> {
final isSelected = entry.key == _selectedQariId;
return ListTile(
leading: Icon(
isSelected ? Icons.radio_button_checked : Icons.radio_button_unchecked,
isSelected ? LucideIcons.checkCircle2 : LucideIcons.circle,
color: isSelected ? AppColors.primary : Colors.grey,
),
title: Text(
@@ -327,7 +333,7 @@ class _QuranMurattalScreenState extends ConsumerState<QuranMurattalScreen> {
style: const TextStyle(fontSize: 12),
),
trailing: isCurrentSurah
? Icon(Icons.graphic_eq, color: AppColors.primary, size: 20)
? Icon(LucideIcons.music, color: AppColors.primary, size: 20)
: null,
onTap: () {
Navigator.pop(context);
@@ -354,6 +360,8 @@ class _QuranMurattalScreenState extends ConsumerState<QuranMurattalScreen> {
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
final box = Hive.box<AppSettings>(HiveBoxes.settings);
final isSimpleMode = box.get('default')?.simpleMode ?? false;
final surahName = _surahData?['namaLatin'] ?? 'Surah ${widget.surahId}';
final hasPhoto = _unsplashPhoto != null;
@@ -361,6 +369,17 @@ class _QuranMurattalScreenState extends ConsumerState<QuranMurattalScreen> {
return Scaffold(
extendBodyBehindAppBar: hasPhoto,
appBar: AppBar(
leading: IconButton(
icon: Icon(Icons.arrow_back,
color: hasPhoto ? Colors.white : null),
onPressed: () {
if (widget.isSimpleModeTab) {
context.go('/');
} else {
context.pop();
}
},
),
backgroundColor: hasPhoto ? Colors.transparent : null,
elevation: hasPhoto ? 0 : null,
iconTheme: hasPhoto ? const IconThemeData(color: Colors.white) : null,
@@ -393,7 +412,6 @@ class _QuranMurattalScreenState extends ConsumerState<QuranMurattalScreen> {
),
],
),
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
@@ -607,7 +625,7 @@ class _QuranMurattalScreenState extends ConsumerState<QuranMurattalScreen> {
IconButton(
onPressed: () => setState(() => _isShuffleEnabled = !_isShuffleEnabled),
icon: Icon(
Icons.shuffle_rounded,
LucideIcons.shuffle,
size: 24,
color: _isShuffleEnabled
? (_unsplashPhoto != null ? Colors.white : AppColors.primary)
@@ -622,7 +640,7 @@ class _QuranMurattalScreenState extends ConsumerState<QuranMurattalScreen> {
? () => _navigateToSurah(-1)
: null,
icon: Icon(
Icons.skip_previous_rounded,
LucideIcons.skipBack,
size: 36,
color: (int.tryParse(widget.surahId) ?? 1) > 1
? (_unsplashPhoto != null ? Colors.white : (isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight))
@@ -669,8 +687,8 @@ class _QuranMurattalScreenState extends ConsumerState<QuranMurattalScreen> {
)
: Icon(
_isPlaying
? Icons.pause_rounded
: Icons.play_arrow_rounded,
? LucideIcons.pause
: LucideIcons.play,
size: 36,
color: _unsplashPhoto != null
? Colors.black87
@@ -684,7 +702,7 @@ class _QuranMurattalScreenState extends ConsumerState<QuranMurattalScreen> {
? () => _navigateToSurah(1)
: null,
icon: Icon(
Icons.skip_next_rounded,
LucideIcons.skipForward,
size: 36,
color: (int.tryParse(widget.surahId) ?? 1) < 114
? (_unsplashPhoto != null ? Colors.white : (isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight))
@@ -695,7 +713,7 @@ class _QuranMurattalScreenState extends ConsumerState<QuranMurattalScreen> {
IconButton(
onPressed: _showSurahPlaylist,
icon: Icon(
Icons.playlist_play_rounded,
LucideIcons.listMusic,
size: 28,
color: _unsplashPhoto != null
? Colors.white70
@@ -720,7 +738,7 @@ class _QuranMurattalScreenState extends ConsumerState<QuranMurattalScreen> {
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.person, size: 16,
Icon(LucideIcons.user, size: 16,
color: _unsplashPhoto != null ? Colors.white : AppColors.primary),
const SizedBox(width: 8),
Text(
@@ -732,7 +750,7 @@ class _QuranMurattalScreenState extends ConsumerState<QuranMurattalScreen> {
),
),
const SizedBox(width: 4),
Icon(Icons.expand_more,
Icon(LucideIcons.chevronDown,
size: 16,
color: _unsplashPhoto != null ? Colors.white : AppColors.primary),
],

View File

@@ -2,6 +2,7 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:just_audio/just_audio.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:intl/intl.dart';
@@ -17,7 +18,14 @@ import '../../../core/providers/tilawah_tracking_provider.dart';
class QuranReadingScreen extends ConsumerStatefulWidget {
final String surahId;
final int? initialVerse;
const QuranReadingScreen({super.key, required this.surahId, this.initialVerse});
final bool isSimpleModeTab;
const QuranReadingScreen({
super.key,
required this.surahId,
this.initialVerse,
this.isSimpleModeTab = false,
});
@override
ConsumerState<QuranReadingScreen> createState() => _QuranReadingScreenState();
@@ -49,6 +57,14 @@ class _QuranReadingScreenState extends ConsumerState<QuranReadingScreen> {
bool _isHafalanPlaying = false;
StreamSubscription? _playerStateSubscription;
void _navigateToMurattal() {
if (widget.isSimpleModeTab) {
context.push('/quran/${widget.surahId}/murattal');
} else {
context.push('/tools/quran/${widget.surahId}/murattal');
}
}
@override
void initState() {
super.initState();
@@ -274,7 +290,7 @@ class _QuranReadingScreenState extends ConsumerState<QuranReadingScreen> {
),
),
ListTile(
leading: const Icon(Icons.push_pin, color: AppColors.primary),
leading: const Icon(LucideIcons.pin, color: AppColors.primary),
title: const Text('Tandai Terakhir Dibaca', style: TextStyle(fontWeight: FontWeight.w600)),
subtitle: const Text('Jadikan ayat ini sebagai titik lanjut membaca anda'),
onTap: () {
@@ -284,7 +300,7 @@ class _QuranReadingScreenState extends ConsumerState<QuranReadingScreen> {
),
const Divider(height: 1),
ListTile(
leading: const Icon(Icons.favorite, color: Colors.pink),
leading: const Icon(LucideIcons.heart, color: Colors.pink),
title: const Text('Tambah ke Favorit', style: TextStyle(fontWeight: FontWeight.w600)),
subtitle: const Text('Simpan ayat ini ke daftar favorit anda'),
onTap: () {
@@ -416,7 +432,7 @@ class _QuranReadingScreenState extends ConsumerState<QuranReadingScreen> {
const SizedBox(height: 16),
Row(
children: [
const Icon(Icons.auto_stories, size: 20, color: AppColors.primary),
const Icon(LucideIcons.bookOpen, size: 20, color: AppColors.primary),
const SizedBox(width: 8),
Text('Total Dibaca: $calculatedAyat Ayat', style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 15)),
],
@@ -558,7 +574,7 @@ class _QuranReadingScreenState extends ConsumerState<QuranReadingScreen> {
actions: [
IconButton(
icon: Icon(
Icons.psychology,
LucideIcons.brain,
color: _isHafalanMode ? AppColors.primary : (isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight),
),
tooltip: 'Mode Hafalan',
@@ -572,7 +588,7 @@ class _QuranReadingScreenState extends ConsumerState<QuranReadingScreen> {
},
),
IconButton(
icon: const Icon(Icons.settings_display),
icon: const Icon(LucideIcons.settings2),
onPressed: _showDisplaySettings,
),
],
@@ -643,7 +659,7 @@ class _QuranReadingScreenState extends ConsumerState<QuranReadingScreen> {
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8),
child: Icon(Icons.diamond,
child: Icon(LucideIcons.gem,
size: 10,
color: AppColors.primary
.withValues(alpha: 0.3)),
@@ -724,8 +740,8 @@ class _QuranReadingScreenState extends ConsumerState<QuranReadingScreen> {
color: AppColors.primary,
),
)
: Icon(Icons.stop_circle, color: AppColors.primary, size: 24))
: Icon(Icons.play_circle_outline,
: Icon(LucideIcons.stopCircle, color: AppColors.primary, size: 24))
: Icon(LucideIcons.playCircle,
color: isDark
? AppColors.textSecondaryDark
: AppColors.textSecondaryLight,
@@ -755,8 +771,8 @@ class _QuranReadingScreenState extends ConsumerState<QuranReadingScreen> {
},
icon: Icon(
trackingSession == null
? Icons.flag_outlined
: Icons.stop_circle,
? LucideIcons.flag
: LucideIcons.stopCircle,
color: trackingSession == null
? (isDark
? AppColors.textSecondaryDark
@@ -767,7 +783,7 @@ class _QuranReadingScreenState extends ConsumerState<QuranReadingScreen> {
IconButton(
onPressed: () => _showBookmarkOptions(i),
icon: Icon(
isLastRead ? Icons.push_pin : (isFav ? Icons.favorite : Icons.bookmark_outline),
isLastRead ? LucideIcons.pin : (isFav ? LucideIcons.heart : LucideIcons.bookmark),
color: isLastRead
? AppColors.primary
: (isFav ? Colors.pink : (isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight)),
@@ -958,7 +974,7 @@ class _QuranReadingScreenState extends ConsumerState<QuranReadingScreen> {
],
),
child: Icon(
_isHafalanPlaying ? Icons.stop_rounded : Icons.play_arrow_rounded,
_isHafalanPlaying ? LucideIcons.square : LucideIcons.play,
color: Colors.white,
size: 28,
),
@@ -1015,7 +1031,7 @@ class _QuranReadingScreenState extends ConsumerState<QuranReadingScreen> {
);
}).toList(),
onChanged: onChanged,
icon: const Icon(Icons.expand_more, size: 16),
icon: const Icon(LucideIcons.chevronDown, size: 16),
isDense: true,
borderRadius: BorderRadius.circular(12),
),

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:hive_flutter/hive_flutter.dart';
import '../../../app/theme/app_colors.dart';
import '../../../data/local/hive_boxes.dart';
@@ -9,7 +10,8 @@ import '../../../data/local/models/quran_bookmark.dart';
import '../../../data/services/equran_service.dart';
class QuranScreen extends ConsumerStatefulWidget {
const QuranScreen({super.key});
final bool isSimpleModeTab;
const QuranScreen({super.key, this.isSimpleModeTab = false});
@override
ConsumerState<QuranScreen> createState() => _QuranScreenState();
@@ -98,6 +100,8 @@ class _QuranScreenState extends ConsumerState<QuranScreen> {
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
final box = Hive.box<AppSettings>(HiveBoxes.settings);
final isSimpleMode = box.get('default')?.simpleMode ?? false;
final filtered = _searchQuery.isEmpty
? _surahs
: _surahs
@@ -110,14 +114,15 @@ class _QuranScreenState extends ConsumerState<QuranScreen> {
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: !widget.isSimpleModeTab,
title: const Text('Al-Quran'),
actions: [
IconButton(
icon: const Icon(Icons.bookmark_outline),
icon: const Icon(LucideIcons.bookmark),
onPressed: () => context.push('/tools/quran/bookmarks'),
),
IconButton(
icon: const Icon(Icons.settings_display),
icon: const Icon(LucideIcons.settings2),
onPressed: _showDisplaySettings,
),
],
@@ -141,7 +146,7 @@ class _QuranScreenState extends ConsumerState<QuranScreen> {
onChanged: (v) => setState(() => _searchQuery = v),
decoration: InputDecoration(
hintText: 'Cari surah...',
prefixIcon: Icon(Icons.search,
prefixIcon: Icon(LucideIcons.search,
color: isDark
? AppColors.textSecondaryDark
: AppColors.textSecondaryLight),
@@ -227,7 +232,7 @@ class _QuranScreenState extends ConsumerState<QuranScreen> {
),
if (hasLastRead) ...[
const SizedBox(width: 8),
const Icon(Icons.push_pin, size: 14, color: AppColors.primary),
const Icon(LucideIcons.pin, size: 14, color: AppColors.primary),
],
],
),

View File

@@ -1,6 +1,8 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:lucide_icons/lucide_icons.dart';
import '../../../app/theme/app_colors.dart';
import '../../../core/providers/theme_provider.dart';
import '../../../core/widgets/ios_toggle.dart';
@@ -136,7 +138,7 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
),
IconButton(
onPressed: () => _showEditProfileDialog(context),
icon: Icon(Icons.edit,
icon: Icon(LucideIcons.pencil,
size: 20, color: AppColors.primary),
),
],
@@ -149,7 +151,22 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
const SizedBox(height: 12),
_settingRow(
isDark,
icon: Icons.dark_mode,
icon: LucideIcons.layoutDashboard,
iconColor: const Color(0xFF0984E3),
title: 'Mode Aplikasi',
subtitle: _settings.simpleMode ? 'Simpel — Jadwal & Al-Quran' : 'Lengkap — Dengan Checklist & Poin',
trailing: IosToggle(
value: !_settings.simpleMode,
onChanged: (v) {
_settings.simpleMode = !v;
_saveSettings();
},
),
),
const SizedBox(height: 10),
_settingRow(
isDark,
icon: LucideIcons.moon,
iconColor: const Color(0xFF6C5CE7),
title: 'Mode Gelap',
trailing: IosToggle(
@@ -160,7 +177,7 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
const SizedBox(height: 10),
_settingRow(
isDark,
icon: Icons.notifications,
icon: LucideIcons.bell,
iconColor: const Color(0xFFE17055),
title: 'Notifikasi',
trailing: IosToggle(
@@ -170,32 +187,32 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
),
const SizedBox(height: 24),
// ── CHECKLIST IBADAH ──
// ── CHECKLIST IBADAH (always visible, even in Simple Mode per user request) ──
_sectionLabel('CHECKLIST IBADAH'),
const SizedBox(height: 12),
_settingRow(
isDark,
icon: Icons.mosque_outlined,
icon: LucideIcons.building,
iconColor: Colors.teal,
title: 'Tingkat Sholat Rawatib',
subtitle: _settings.rawatibLevel == 0 ? 'Mati' : (_settings.rawatibLevel == 1 ? 'Muakkad Saja' : 'Lengkap (Semua)'),
trailing: const Icon(Icons.chevron_right, size: 20),
trailing: const Icon(LucideIcons.chevronRight, size: 20),
onTap: () => _showRawatibDialog(context),
),
const SizedBox(height: 10),
_settingRow(
isDark,
icon: Icons.menu_book,
icon: LucideIcons.bookOpen,
iconColor: Colors.amber,
title: 'Target Tilawah',
subtitle: '${_settings.tilawahTargetValue} ${_settings.tilawahTargetUnit}',
trailing: const Icon(Icons.chevron_right, size: 20),
trailing: const Icon(LucideIcons.chevronRight, size: 20),
onTap: () => _showTilawahDialog(context),
),
const SizedBox(height: 10),
_settingRow(
isDark,
icon: Icons.sync,
icon: LucideIcons.refreshCw,
iconColor: Colors.blue,
title: 'Auto-Sync Tilawah',
subtitle: 'Catat otomatis dari menu Al-Quran',
@@ -218,11 +235,11 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
const SizedBox(height: 10),
_settingRow(
isDark,
icon: Icons.library_add_check,
icon: LucideIcons.listChecks,
iconColor: Colors.indigo,
title: 'Amalan Tambahan',
subtitle: 'Dzikir & Puasa Sunnah',
trailing: const Icon(Icons.chevron_right, size: 20),
trailing: const Icon(LucideIcons.chevronRight, size: 20),
onTap: () => _showAmalanDialog(context),
),
const SizedBox(height: 24),
@@ -232,31 +249,31 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
const SizedBox(height: 12),
_settingRow(
isDark,
icon: Icons.mosque,
icon: LucideIcons.building,
iconColor: AppColors.primary,
title: 'Metode Perhitungan',
subtitle: 'Kemenag RI',
trailing: const Icon(Icons.chevron_right, size: 20),
trailing: const Icon(LucideIcons.chevronRight, size: 20),
onTap: () => _showMethodDialog(context),
),
const SizedBox(height: 10),
_settingRow(
isDark,
icon: Icons.location_on,
icon: LucideIcons.mapPin,
iconColor: const Color(0xFF00B894),
title: 'Lokasi',
subtitle: _displayCityName,
trailing: const Icon(Icons.chevron_right, size: 20),
trailing: const Icon(LucideIcons.chevronRight, size: 20),
onTap: () => _showLocationDialog(context),
),
const SizedBox(height: 10),
_settingRow(
isDark,
icon: Icons.timer,
icon: LucideIcons.timer,
iconColor: const Color(0xFFFDAA5E),
title: 'Waktu Iqamah',
subtitle: 'Atur per waktu sholat',
trailing: const Icon(Icons.chevron_right, size: 20),
trailing: const Icon(LucideIcons.chevronRight, size: 20),
onTap: () => _showIqamahDialog(context),
),
const SizedBox(height: 24),
@@ -266,7 +283,7 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
const SizedBox(height: 12),
_settingRow(
isDark,
icon: Icons.text_fields,
icon: LucideIcons.type,
iconColor: const Color(0xFF636E72),
title: 'Ukuran Font Arab',
subtitle: '${_settings.arabicFontSize.round()}pt',
@@ -292,7 +309,7 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
const SizedBox(height: 12),
_settingRow(
isDark,
icon: Icons.info_outline,
icon: LucideIcons.info,
iconColor: AppColors.sage,
title: 'Versi Aplikasi',
subtitle: '1.0.0',
@@ -300,10 +317,10 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
const SizedBox(height: 10),
_settingRow(
isDark,
icon: Icons.favorite_outline,
icon: LucideIcons.heart,
iconColor: Colors.red,
title: 'Beri Nilai Kami',
trailing: const Icon(Icons.chevron_right, size: 20),
trailing: const Icon(LucideIcons.chevronRight, size: 20),
onTap: () {},
),
const SizedBox(height: 24),
@@ -324,7 +341,7 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
child: const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.logout, color: Colors.red, size: 20),
Icon(LucideIcons.logOut, color: Colors.red, size: 20),
SizedBox(width: 8),
Text(
'Hapus Semua Data',
@@ -447,6 +464,7 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
final searchCtrl = TextEditingController();
bool isSearching = false;
List<Map<String, dynamic>> results = [];
Timer? debounce;
showDialog(
context: context,
@@ -466,7 +484,7 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
hintText: 'Cth: Jakarta',
border: const OutlineInputBorder(),
suffixIcon: IconButton(
icon: const Icon(Icons.search),
icon: const Icon(LucideIcons.search),
onPressed: () async {
if (searchCtrl.text.trim().isEmpty) return;
setDialogState(() => isSearching = true);
@@ -479,15 +497,45 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
},
),
),
onChanged: (val) {
if (val.trim().length < 3) return;
if (debounce?.isActive ?? false) debounce!.cancel();
debounce = Timer(const Duration(milliseconds: 500), () async {
if (!mounted) return;
setDialogState(() => isSearching = true);
try {
final res = await MyQuranSholatService.instance.searchCity(val.trim());
if (mounted) {
setDialogState(() {
results = res;
});
}
} catch (e) {
debugPrint('Error searching city: $e');
} finally {
if (mounted) {
setDialogState(() {
isSearching = false;
});
}
}
});
},
onSubmitted: (val) async {
if (val.trim().isEmpty) return;
if (debounce?.isActive ?? false) debounce!.cancel();
setDialogState(() => isSearching = true);
final res = await MyQuranSholatService.instance
.searchCity(val.trim());
setDialogState(() {
results = res;
isSearching = false;
});
if (mounted) {
setDialogState(() {
results = res;
isSearching = false;
});
}
},
),
const SizedBox(height: 16),
@@ -670,7 +718,7 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
const Text('Sholat Rawatib', style: TextStyle(fontSize: 18)),
const Spacer(),
IconButton(
icon: const Icon(Icons.info_outline, color: AppColors.primary),
icon: const Icon(LucideIcons.info, color: AppColors.primary),
onPressed: () {
showModalBottomSheet(
context: context,

View File

@@ -1,7 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:lucide_icons/lucide_icons.dart';
import '../../../app/theme/app_colors.dart';
import '../../../core/widgets/tool_card.dart';
import '../../../data/services/equran_service.dart';
class ToolsScreen extends ConsumerWidget {
@@ -18,11 +20,11 @@ class ToolsScreen extends ConsumerWidget {
actions: [
IconButton(
onPressed: () {},
icon: const Icon(Icons.notifications_outlined),
icon: const Icon(LucideIcons.bell),
),
IconButton(
onPressed: () => context.push('/settings'),
icon: const Icon(Icons.settings_outlined),
icon: const Icon(LucideIcons.settings),
),
const SizedBox(width: 8),
],
@@ -45,22 +47,22 @@ class ToolsScreen extends ConsumerWidget {
Row(
children: [
Expanded(
child: _ToolCard(
icon: Icons.explore,
title: 'Arah\nKiblat',
color: AppColors.primary,
child: ToolCard(
icon: LucideIcons.bookOpen,
title: 'Al-Quran\nTerjemahan',
color: const Color(0xFF00b894),
isDark: isDark,
onTap: () => context.push('/tools/qibla'),
onTap: () => context.push('/quran'),
),
),
const SizedBox(width: 12),
Expanded(
child: _ToolCard(
icon: Icons.menu_book,
title: 'Baca\nQuran',
color: const Color(0xFF4A90D9),
child: ToolCard(
icon: LucideIcons.headphones,
title: 'Quran\nMurattal',
color: const Color(0xFF7B61FF),
isDark: isDark,
onTap: () => context.push('/tools/quran'),
onTap: () => context.push('/quran/1/murattal'),
),
),
],
@@ -69,22 +71,22 @@ class ToolsScreen extends ConsumerWidget {
Row(
children: [
Expanded(
child: _ToolCard(
icon: Icons.auto_awesome,
title: 'Penghitung\nDzikir',
color: const Color(0xFFE8A838),
child: ToolCard(
icon: LucideIcons.compass,
title: 'Arah\nKiblat',
color: const Color(0xFF0984E3),
isDark: isDark,
onTap: () => context.push('/tools/dzikir'),
onTap: () => context.push('/tools/qibla'),
),
),
const SizedBox(width: 12),
Expanded(
child: _ToolCard(
icon: Icons.headphones,
title: 'Quran\nMurattal',
color: const Color(0xFF7B61FF),
child: ToolCard(
icon: LucideIcons.sparkles,
title: 'Tasbih\nDigital',
color: AppColors.primary,
isDark: isDark,
onTap: () => context.push('/tools/quran/1/murattal'),
onTap: () => context.push('/dzikir'),
),
),
],
@@ -133,7 +135,7 @@ class ToolsScreen extends ConsumerWidget {
),
),
IconButton(
icon: Icon(Icons.share,
icon: Icon(LucideIcons.share2,
size: 18,
color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight),
onPressed: () {},
@@ -183,69 +185,3 @@ class ToolsScreen extends ConsumerWidget {
);
}
}
class _ToolCard extends StatelessWidget {
final IconData icon;
final String title;
final Color color;
final bool isDark;
final VoidCallback onTap;
const _ToolCard({
required this.icon,
required this.title,
required this.color,
required this.isDark,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
height: 140,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: isDark ? AppColors.surfaceDark : AppColors.surfaceLight,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: isDark
? color.withValues(alpha: 0.15)
: AppColors.cream,
),
boxShadow: [
BoxShadow(
color: color.withValues(alpha: 0.08),
blurRadius: 12,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: color.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(12),
),
child: Icon(icon, color: color, size: 24),
),
Text(
title,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w700,
height: 1.3,
),
),
],
),
),
);
}
}

View File

@@ -741,6 +741,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.0"
lucide_icons:
dependency: "direct main"
description:
name: lucide_icons
sha256: ad24d0fd65707e48add30bebada7d90bff2a1bba0a72d6e9b19d44246b0e83c4
url: "https://pub.dev"
source: hosted
version: "0.257.0"
matcher:
dependency: transitive
description:

View File

@@ -52,6 +52,7 @@ dependencies:
flutter_dotenv: ^5.1.0
cached_network_image: ^3.3.1
url_launcher: ^6.2.5
lucide_icons: ^0.257.0
dev_dependencies:
flutter_test: