diff --git a/handoff.md b/handoff.md new file mode 100644 index 0000000..4322502 --- /dev/null +++ b/handoff.md @@ -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 1–13) + +| 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 | diff --git a/lib/app/router.dart b/lib/app/router.dart index 44a8d4c..ec112a0 100644 --- a/lib/app/router.dart +++ b/lib/app/router.dart @@ -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(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(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>( + valueListenable: Hive.box(HiveBoxes.settings).listenable(), + builder: (context, box, _) { + return Scaffold( + body: child, + bottomNavigationBar: AppBottomNavBar( + currentIndex: _currentIndex(context), + onTap: (i) => _onTap(context, i), + ), + ); + }, ); } } diff --git a/lib/core/widgets/bottom_nav_bar.dart b/lib/core/widgets/bottom_nav_bar.dart index caf7581..a60d28f 100644 --- a/lib/core/widgets/bottom_nav_bar.dart +++ b/lib/core/widgets/bottom_nav_bar.dart @@ -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>( + valueListenable: Hive.box(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, + ), + ), + ), + ); + }, ); } } diff --git a/lib/core/widgets/tool_card.dart b/lib/core/widgets/tool_card.dart new file mode 100644 index 0000000..3952fc7 --- /dev/null +++ b/lib/core/widgets/tool_card.dart @@ -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, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/data/local/models/app_settings.dart b/lib/data/local/models/app_settings.dart index ccc5798..9d42cf0 100644 --- a/lib/data/local/models/app_settings.dart +++ b/lib/data/local/models/app_settings.dart @@ -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, diff --git a/lib/data/local/models/app_settings.g.dart b/lib/data/local/models/app_settings.g.dart index b077c53..c6a0cdf 100644 --- a/lib/data/local/models/app_settings.g.dart +++ b/lib/data/local/models/app_settings.g.dart @@ -36,13 +36,14 @@ class AppSettingsAdapter extends TypeAdapter { 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 { ..writeByte(17) ..write(obj.showLatin) ..writeByte(18) - ..write(obj.showTerjemahan); + ..write(obj.showTerjemahan) + ..writeByte(19) + ..write(obj.simpleMode); } @override diff --git a/lib/data/local/models/daily_worship_log.dart b/lib/data/local/models/daily_worship_log.dart index e36dd90..f329cec 100644 --- a/lib/data/local/models/daily_worship_log.dart +++ b/lib/data/local/models/daily_worship_log.dart @@ -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(HiveBoxes.settings); + final isSimpleMode = settingsBox.get('default')?.simpleMode ?? false; + if (isSimpleMode) return 0; + int points = 0; // 1. Shalat Fardhu diff --git a/lib/features/checklist/presentation/checklist_screen.dart b/lib/features/checklist/presentation/checklist_screen.dart index da80aa5..103c657 100644 --- a/lib/features/checklist/presentation/checklist_screen.dart +++ b/lib/features/checklist/presentation/checklist_screen.dart @@ -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 { 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 { 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 { ), 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 { : (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 { 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 { : (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 { // ── 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 { 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 { : 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 { 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 { ), 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( @@ -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, ), ); } diff --git a/lib/features/dashboard/presentation/dashboard_screen.dart b/lib/features/dashboard/presentation/dashboard_screen.dart index 08cb89b..120063f 100644 --- a/lib/features/dashboard/presentation/dashboard_screen.dart +++ b/lib/features/dashboard/presentation/dashboard_screen.dart @@ -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 { Timer? _countdownTimer; - Duration _countdown = Duration.zero; - String _nextPrayerName = ''; + final ValueNotifier _countdown = ValueNotifier(Duration.zero); + final ValueNotifier _nextPrayerName = ValueNotifier(''); final ScrollController _prayerScrollController = ScrollController(); + bool _hasAutoScrolled = false; + DaySchedule? _currentSchedule; + + bool get _isSimpleMode { + final box = Hive.box(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 { 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 { Widget build(BuildContext context) { final theme = Theme.of(context); final isDark = theme.brightness == Brightness.dark; - final prayerTimesAsync = ref.watch(prayerTimesProvider); + + ref.listen>(prayerTimesProvider, (previous, next) { + next.whenData((schedule) { + if (schedule != null) { + _startCountdown(schedule); + } + }); + }); return Scaffold( body: SafeArea( @@ -81,23 +103,40 @@ class _DashboardScreenState extends ConsumerState { 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 { 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 { IconButton( onPressed: () {}, icon: Icon( - Icons.notifications_outlined, + LucideIcons.bell, color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight, @@ -155,7 +194,7 @@ class _DashboardScreenState extends ConsumerState { 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 { 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 { 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 { ], ), const SizedBox(height: 8), - Text( - '$name — $time', - style: const TextStyle( - fontSize: 28, - fontWeight: FontWeight.w800, - color: AppColors.onPrimary, - ), + ValueListenableBuilder( + 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( + 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 { 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 { shape: BoxShape.circle, ), child: const Icon( - Icons.volume_up, + LucideIcons.volume2, color: AppColors.onPrimary, size: 22, ), @@ -342,7 +391,7 @@ class _DashboardScreenState extends ConsumerState { ), 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 { 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 { 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 { String amalanText = 'Belum ada data'; if (log != null) { - List aList = []; + final List 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 { 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 { ], ); } + + 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(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(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(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(HiveBoxes.settings).get('default')?.simpleMode ?? false; + if (isSimple) { + context.go('/dzikir'); + } else { + context.push('/tools/dzikir'); + } + }, + ), + ), + ], + ), + ], + ); + } + + Widget _buildAyatHariIni(BuildContext context, bool isDark) { + return FutureBuilder?>( + 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, + ), + ), + ], + ), + ); + }, + ); + } } diff --git a/lib/features/dzikir/presentation/dzikir_screen.dart b/lib/features/dzikir/presentation/dzikir_screen.dart index 0afc2cc..8436359 100644 --- a/lib/features/dzikir/presentation/dzikir_screen.dart +++ b/lib/features/dzikir/presentation/dzikir_screen.dart @@ -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 createState() => _DzikirScreenState(); @@ -85,14 +88,17 @@ class _DzikirScreenState extends ConsumerState @override Widget build(BuildContext context) { final isDark = Theme.of(context).brightness == Brightness.dark; + final box = Hive.box(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 mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( - isComplete ? Icons.check : Icons.touch_app, + isComplete ? LucideIcons.check : LucideIcons.fingerprint, size: 18, color: isComplete ? AppColors.primary diff --git a/lib/features/imsakiyah/presentation/imsakiyah_screen.dart b/lib/features/imsakiyah/presentation/imsakiyah_screen.dart index 2089291..600f32e 100644 --- a/lib/features/imsakiyah/presentation/imsakiyah_screen.dart +++ b/lib/features/imsakiyah/presentation/imsakiyah_screen.dart @@ -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 { final searchCtrl = TextEditingController(); bool isSearching = false; List> results = []; + Timer? debounce; showDialog( context: context, @@ -113,7 +116,7 @@ class _ImsakiyahScreenState extends ConsumerState { 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 { }, ), ), + 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 { 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 { ), 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 { ], ), ), - Icon(Icons.expand_more, + Icon(LucideIcons.chevronDown, color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight), diff --git a/lib/features/laporan/presentation/laporan_screen.dart b/lib/features/laporan/presentation/laporan_screen.dart index a33fb23..21e2e95 100644 --- a/lib/features/laporan/presentation/laporan_screen.dart +++ b/lib/features/laporan/presentation/laporan_screen.dart @@ -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 Widget build(BuildContext context) { final theme = Theme.of(context); final isDark = theme.brightness == Brightness.dark; + + final settingsBox = Hive.box(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 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 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 _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 _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 ); } - 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 ), ); } + + Widget _buildRiwayatSimpel(BuildContext context, bool isDark) { + final logBox = Hive.box(HiveBoxes.worshipLogs); + final now = DateTime.now(); + final logs = []; + + // 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 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 { diff --git a/lib/features/qibla/presentation/qibla_screen.dart b/lib/features/qibla/presentation/qibla_screen.dart index 6c0da82..d9d718d 100644 --- a/lib/features/qibla/presentation/qibla_screen.dart +++ b/lib/features/qibla/presentation/qibla_screen.dart @@ -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 { : 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 { 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 { Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.location_on, + Icon(LucideIcons.mapPin, size: 16, color: AppColors.primary), const SizedBox(width: 4), Text( diff --git a/lib/features/quran/presentation/quran_bookmarks_screen.dart b/lib/features/quran/presentation/quran_bookmarks_screen.dart index 53b47fd..d13812e 100644 --- a/lib/features/quran/presentation/quran_bookmarks_screen.dart +++ b/lib/features/quran/presentation/quran_bookmarks_screen.dart @@ -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 { 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 { 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 { 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 { ), ), 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 { 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 { Row( children: [ Icon( - Icons.access_time, + LucideIcons.clock, size: 12, color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight, ), diff --git a/lib/features/quran/presentation/quran_murattal_screen.dart b/lib/features/quran/presentation/quran_murattal_screen.dart index 9d0c235..5a3a1a7 100644 --- a/lib/features/quran/presentation/quran_murattal_screen.dart +++ b/lib/features/quran/presentation/quran_murattal_screen.dart @@ -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 { 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 { 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 { @override Widget build(BuildContext context) { final isDark = Theme.of(context).brightness == Brightness.dark; + final box = Hive.box(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 { 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 { ), ], ), - ), body: _isLoading ? const Center(child: CircularProgressIndicator()) @@ -607,7 +625,7 @@ class _QuranMurattalScreenState extends ConsumerState { 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 { ? () => _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 { ) : 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 { ? () => _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 { 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 { 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 { ), ), const SizedBox(width: 4), - Icon(Icons.expand_more, + Icon(LucideIcons.chevronDown, size: 16, color: _unsplashPhoto != null ? Colors.white : AppColors.primary), ], diff --git a/lib/features/quran/presentation/quran_reading_screen.dart b/lib/features/quran/presentation/quran_reading_screen.dart index f6340ac..972b344 100644 --- a/lib/features/quran/presentation/quran_reading_screen.dart +++ b/lib/features/quran/presentation/quran_reading_screen.dart @@ -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 createState() => _QuranReadingScreenState(); @@ -49,6 +57,14 @@ class _QuranReadingScreenState extends ConsumerState { 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 { ), ), 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 { ), 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 { 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 { 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 { }, ), IconButton( - icon: const Icon(Icons.settings_display), + icon: const Icon(LucideIcons.settings2), onPressed: _showDisplaySettings, ), ], @@ -643,7 +659,7 @@ class _QuranReadingScreenState extends ConsumerState { 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 { 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 { }, 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 { 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 { ], ), 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 { ); }).toList(), onChanged: onChanged, - icon: const Icon(Icons.expand_more, size: 16), + icon: const Icon(LucideIcons.chevronDown, size: 16), isDense: true, borderRadius: BorderRadius.circular(12), ), diff --git a/lib/features/quran/presentation/quran_screen.dart b/lib/features/quran/presentation/quran_screen.dart index 9dcbf83..321f2b0 100644 --- a/lib/features/quran/presentation/quran_screen.dart +++ b/lib/features/quran/presentation/quran_screen.dart @@ -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 createState() => _QuranScreenState(); @@ -98,6 +100,8 @@ class _QuranScreenState extends ConsumerState { @override Widget build(BuildContext context) { final isDark = Theme.of(context).brightness == Brightness.dark; + final box = Hive.box(HiveBoxes.settings); + final isSimpleMode = box.get('default')?.simpleMode ?? false; final filtered = _searchQuery.isEmpty ? _surahs : _surahs @@ -110,14 +114,15 @@ class _QuranScreenState extends ConsumerState { 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 { 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 { ), 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), ], ], ), diff --git a/lib/features/settings/presentation/settings_screen.dart b/lib/features/settings/presentation/settings_screen.dart index d487757..10114be 100644 --- a/lib/features/settings/presentation/settings_screen.dart +++ b/lib/features/settings/presentation/settings_screen.dart @@ -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 { ), 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 { 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 { 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 { ), 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 { 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 { 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 { 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 { 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 { 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 { 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 { final searchCtrl = TextEditingController(); bool isSearching = false; List> results = []; + Timer? debounce; showDialog( context: context, @@ -466,7 +484,7 @@ class _SettingsScreenState extends ConsumerState { 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 { }, ), ), + 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 { 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, diff --git a/lib/features/tools/presentation/tools_screen.dart b/lib/features/tools/presentation/tools_screen.dart index ed52cbe..7e6c2c8 100644 --- a/lib/features/tools/presentation/tools_screen.dart +++ b/lib/features/tools/presentation/tools_screen.dart @@ -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, - ), - ), - ], - ), - ), - ); - } -} diff --git a/pubspec.lock b/pubspec.lock index ae8bc8e..0d32f70 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -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: diff --git a/pubspec.yaml b/pubspec.yaml index 971b5e6..5b5411b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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: