feat: complete Simple Mode contextual routing and navigation state synchronization
This commit is contained in:
142
handoff.md
Normal file
142
handoff.md
Normal 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 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 |
|
||||
@@ -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),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
69
lib/core/widgets/tool_card.dart
Normal file
69
lib/core/widgets/tool_card.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -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),
|
||||
],
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
|
||||
@@ -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),
|
||||
],
|
||||
],
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user