commit faadc1865db17eb7b689b83ef702c44542fe2fb0 Author: dwindown Date: Fri Mar 13 15:42:17 2026 +0700 feat: Murattal player enhancements & prayer schedule auto-scroll - Murattal: Spotify-style 5-button controls [Shuffle, Prev, Play, Next, Playlist] - Murattal: Animated 7-bar equalizer visualization in player circle - Murattal: Unsplash API background with frosted glass player overlay - Murattal: Transparent AppBar with backdrop blur - Murattal: Surah playlist bottom sheet with full 114 Surah list - Murattal: Auto-play disabled on screen open, enabled on navigation - Murattal: Shuffle mode for random Surah playback - Murattal: Photographer attribution per Unsplash guidelines - Dashboard: Auto-scroll prayer schedule to next active prayer - Fix: setState lifecycle errors on Reading & Murattal screens - Setup: flutter_dotenv, cached_network_image, url_launcher deps diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f6f9c2c --- /dev/null +++ b/.gitignore @@ -0,0 +1,48 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ +/coverage/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release + +# Environment variables +.env diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..5ba6a80 --- /dev/null +++ b/.metadata @@ -0,0 +1,33 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "ff37bef603469fb030f2b72995ab929ccfc227f0" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: ff37bef603469fb030f2b72995ab929ccfc227f0 + base_revision: ff37bef603469fb030f2b72995ab929ccfc227f0 + - platform: macos + create_revision: ff37bef603469fb030f2b72995ab929ccfc227f0 + base_revision: ff37bef603469fb030f2b72995ab929ccfc227f0 + - platform: web + create_revision: ff37bef603469fb030f2b72995ab929ccfc227f0 + base_revision: ff37bef603469fb030f2b72995ab929ccfc227f0 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/PRD.md b/PRD.md new file mode 100644 index 0000000..887cbdb --- /dev/null +++ b/PRD.md @@ -0,0 +1,884 @@ +# Product Requirements Document +## Jamshalat Diary — Islamic Worship Companion App + +**Version:** 1.0 +**Date:** March 2026 +**Platform:** Flutter (iOS + Android) +**Status:** Draft + +--- + +## Table of Contents + +1. [Overview](#1-overview) +2. [Target Users](#2-target-users) +3. [Design System](#3-design-system) +4. [App Architecture](#4-app-architecture) +5. [Navigation](#5-navigation) +6. [Feature Specifications](#6-feature-specifications) + - 6.1 Dashboard + - 6.2 Prayer Calendar (Imsakiyah) + - 6.3 Daily Checklist + - 6.4 Dzikir + - 6.5 Reports (Laporan) + - 6.6 Qibla Finder + - 6.7 Quran Reading + - 6.8 Quran Murattal + - 6.9 Settings +7. [Data Model](#7-data-model) +8. [Third-Party Dependencies](#8-third-party-dependencies) +9. [Non-Functional Requirements](#9-non-functional-requirements) +10. [Permissions](#10-permissions) +11. [Out of Scope (v1.0)](#11-out-of-scope-v10) + +--- + +## 1. Overview + +**Jamshalat Diary** is an offline-first Muslim daily worship companion app built in Flutter. It helps Muslim users track their daily prayers, worship habits, and spiritual growth with a clean, modern UI that supports both light and dark themes. + +**Core value proposition:** +- Never miss a prayer — real-time prayer time countdowns with Adhan/Iqamah notifications +- Track daily worship completion — checklist for Sholat, Tilawah, Dzikir +- Build consistent habits — weekly/monthly progress reports +- All-in-one Islamic toolkit — Qibla compass, Quran reader, Murattal player, Dzikir counter + +--- + +## 2. Target Users + +**Primary:** Muslim adults (18–45) who want to improve consistency in daily worship +**Secondary:** Parents tracking worship habits for the family + +**User goals:** +- Know exact prayer times for their location, offline +- Get reminded for Adhan and Iqamah +- Log daily ibadah (worship) completion +- Read Quran and perform guided Dzikir +- Find Qibla direction while traveling +- Review worship quality over time + +--- + +## 3. Design System + +### 3.1 Color Tokens + +| Token | Light Value | Dark Value | Usage | +|---|---|---|---| +| `primary` | `#70df20` | `#70df20` | Active states, CTAs, progress fills | +| `background` | `#f7f8f6` | `#182111` | App background | +| `surface` | `#ffffff` | `#1e2a14` | Cards, bottom nav | +| `sage` | `#728764` | `#728764` | Secondary text, section labels | +| `cream` | `#f2f4f0` | — | Dividers, borders (light mode) | +| `on-primary` | `#0a1a00` | `#0a1a00` | Text on primary bg | +| `text-primary` | `#1a2a0a` | `#f2f4f0` | Body text | +| `text-secondary` | `#64748b` | `#94a3b8` | Captions, labels | +| `error` | `#ef4444` | `#f87171` | Error states | +| `success` | `#22c55e` | `#4ade80` | Success/completed states | + +### 3.2 Typography + +| Style | Font | Weight | Size | Usage | +|---|---|---|---|---| +| `displayLarge` | Plus Jakarta Sans | 800 (ExtraBold) | 32sp | Hero numbers (next prayer) | +| `headlineMedium` | Plus Jakarta Sans | 700 (Bold) | 24sp | Screen titles, section headers | +| `titleMedium` | Plus Jakarta Sans | 600 (SemiBold) | 16sp | Card titles, nav labels | +| `bodyLarge` | Plus Jakarta Sans | 400 (Regular) | 16sp | Body text | +| `bodySmall` | Plus Jakarta Sans | 400 (Regular) | 12sp | Captions, timestamps | +| `labelSmall` | Plus Jakarta Sans | 700 (Bold) | 10sp | Uppercase tags, section labels | +| `arabicBody` | Amiri | 400 (Regular) | 24sp | Quran verses, Dzikir Arabic text | +| `arabicLarge` | Amiri | 700 (Bold) | 28sp | Surah headings in Arabic | + +### 3.3 Spacing & Shape + +| Token | Value | Flutter | +|---|---|---| +| `radiusSm` | 8dp | `BorderRadius.circular(8)` | +| `radiusMd` | 12dp | `BorderRadius.circular(12)` | +| `radiusLg` | 16dp | `BorderRadius.circular(16)` | +| `radiusXl` | 24dp | `BorderRadius.circular(24)` | +| `radiusFull` | 9999dp | `StadiumBorder()` | +| `spacingXs` | 4dp | — | +| `spacingSm` | 8dp | — | +| `spacingMd` | 16dp | — | +| `spacingLg` | 24dp | — | +| `spacingXl` | 32dp | — | + +### 3.4 Iconography + +- **Library:** Material Symbols Outlined (via `material_symbols_icons` Flutter package) +- **Filled variant:** Used for active bottom nav tab icons only +- **Size:** 24dp default, 20dp for compact rows, 32dp for feature section icons + +### 3.5 Dark Mode + +- Toggled via user preference in Settings (stored locally) +- Options: **Light**, **Dark**, **System (Auto)** +- Implemented via Flutter `ThemeMode` — `ThemeMode.light`, `ThemeMode.dark`, `ThemeMode.system` +- All color tokens have explicit light and dark values; no transparency hacks + +--- + +## 4. App Architecture + +### 4.1 Structure + +``` +lib/ +├── main.dart +├── app/ +│ ├── app.dart # MaterialApp.router + theme + locale +│ ├── router.dart # GoRouter — shell route for bottom nav +│ └── theme/ +│ ├── app_theme.dart # ThemeData light + dark +│ ├── app_colors.dart # AppColors class with static consts +│ └── app_text_styles.dart # TextTheme definitions +├── core/ +│ ├── widgets/ +│ │ ├── bottom_nav_bar.dart +│ │ ├── prayer_time_card.dart +│ │ ├── section_header.dart +│ │ ├── ios_toggle.dart +│ │ ├── progress_bar.dart +│ │ └── circular_progress_indicator_custom.dart +│ ├── utils/ +│ │ ├── date_utils.dart +│ │ ├── prayer_utils.dart # Hijri date conversion helpers +│ │ └── arabic_utils.dart # RTL text helpers +│ └── providers/ +│ └── theme_provider.dart # ThemeMode state via Riverpod +├── features/ +│ ├── dashboard/ +│ │ ├── data/ +│ │ ├── domain/ +│ │ └── presentation/ +│ │ ├── dashboard_screen.dart +│ │ └── widgets/ +│ ├── imsakiyah/ +│ ├── checklist/ +│ ├── dzikir/ +│ ├── laporan/ +│ ├── qibla/ +│ ├── quran/ +│ │ ├── presentation/ +│ │ │ ├── quran_screen.dart # Surah list +│ │ │ ├── quran_reading_screen.dart +│ │ │ └── quran_murattal_screen.dart +│ └── settings/ +└── data/ + ├── local/ + │ ├── hive_boxes.dart # Box names constants + │ └── adapters/ # Hive TypeAdapters + └── services/ + ├── prayer_service.dart # Adhan calculation + ├── location_service.dart # GPS + last known + ├── notification_service.dart + └── quran_service.dart # Local JSON asset +``` + +### 4.2 State Management + +**Riverpod** (flutter_riverpod + riverpod_annotation + riverpod_generator) + +- Every feature has its own `*_provider.dart` +- Async data via `AsyncNotifierProvider` +- UI state (loading, error, data) handled via `AsyncValue` +- Theme mode via `StateProvider` + +### 4.3 Local Storage + +**Hive** for offline-first persistence: + +| Box | Key | Value type | +|---|---|---| +| `settings` | String key | dynamic | +| `checklist_items` | date string (yyyy-MM-dd) | `List` | +| `worship_logs` | date string | `WorshipLog` | +| `dzikir_counters` | dzikir ID | int count | +| `cached_prayer_times` | `lat_lng_date` | `PrayerTimes` | + +--- + +## 5. Navigation + +### 5.1 Bottom Navigation Bar (5 tabs) + +| Index | Label | Icon (inactive) | Icon (active) | Route | +|---|---|---|---|---| +| 0 | Home | `home_outlined` | `home` (filled) | `/` | +| 1 | Calendar | `calendar_today_outlined` | `calendar_today` | `/imsakiyah` | +| 2 | Checklist | `rule_outlined` | `rule` (filled) | `/checklist` | +| 3 | Reports | `bar_chart_outlined` | `bar_chart` | `/laporan` | +| 4 | Tools | `auto_fix_high_outlined` | `auto_fix_high` (filled) | `/tools` | + +- Bottom nav is persistent across all 5 tabs (shell route via GoRouter) +- Safe area padding (bottom) applied to nav bar (`pb: 24dp`) +- Active tab: `primary` text + filled icon +- Inactive tab: `text-secondary` color + +### 5.2 Full Route Map + +``` +/ → DashboardScreen + /qibla → QiblaScreen (push, no bottom nav) +/imsakiyah → ImsakiyahScreen +/checklist → ChecklistScreen +/laporan → LaporanScreen +/tools → ToolsScreen (hub for Dzikir, Quran, Qibla) + /tools/dzikir → DzikirScreen + /tools/quran → QuranListScreen + /tools/quran/:surahId → QuranReadingScreen + /tools/quran/:surahId/murattal → QuranMurattalScreen + /tools/qibla → QiblaScreen +/settings → SettingsScreen (push from Dashboard header) +``` + +--- + +## 6. Feature Specifications + +--- + +### 6.1 Dashboard + +**Route:** `/` +**Description:** App home screen showing next prayer, daily prayer schedule, checklist summary, and weekly progress. + +#### UI Components + +**Header (sticky)** +- Left: Circular user avatar (40dp, primary border 2dp) + greeting text ("Welcome back," / user name "Assalamu'alaikum, [Name]") +- Right: Notification bell `IconButton` → opens notification settings + +**Next Prayer Hero Card** +- Full-width card, `bg-primary`, `borderRadius: 24dp`, `padding: 20dp`, `boxShadow: lg` +- Content: + - Label: "Next Prayer" with `schedule` icon + - Title: `"{PrayerName} at {HH:mm}"` — `displayLarge` weight 800 + - Subtitle: `"Countdown: HH:mm:ss"` — live countdown timer updating every second + - Actions row: + - "View Qibla" button (`black bg`, `white text`, `explore icon`, `borderRadius: full`) + - Volume/mute toggle button (`white/30 backdrop-blur`, `rounded-full`) + - Decorative: white/20 blurred circle blob (top-right, size 120dp) + +**Prayer Times Horizontal Scroll** +- Section title: "Daily Prayer Times" + "Today" chip (`primary/10 bg`, `primary text`) +- Horizontal `ListView.builder`, no scroll indicator, `padding: 16dp horizontal` +- Each card: `width: 112dp`, `borderRadius: 16dp`, `border: primary/10` + - Icon (Material Symbol): Fajr→`wb_twilight`, Dhuhr→`wb_sunny`, Asr→`filter_drama`, Maghrib→`wb_twilight`, Isha→`dark_mode` + - Prayer name (`sm font-bold`) + - Time string (`xs text-secondary`) +- **Active prayer card:** `bg-primary/10`, `border-2 border-primary`, primary text color +- **Adhan/Iqamah variant:** Active card shows notification badge (small bell icon, white circle, shadow, absolute top-right) + +**Daily Checklist Summary Card** +- Title: "Today's Checklist" + subtitle "{n} dari {total} Ibadah selesai" +- Circular SVG progress indicator (48dp, `strokeWidth: 4`, `primary` stroke) +- Percentage text centered inside circle +- 2 preview items: "Sholat Fardhu (4 of 5)" and "Tilawah Quran (1 Juz)" + - Completed items: `check_circle` icon in `primary` color +- "View Full Checklist" CTA button → navigates to `/checklist` + +**Weekly Progress Chart** +- Title: "Weekly Progress" +- `Container` with `bg-surface`, `borderRadius: 16dp`, `padding: 20dp` +- 7 vertical bars (Mon–Sun): each bar is `Expanded`, `bg-primary/20`, with `primary` fill overlay proportional to completion % +- Day labels below each bar: `10sp`, `font-bold`, `text-secondary` + +#### Behavior + +- Countdown timer refreshes every 1 second via `Timer.periodic` +- Prayer times are calculated from device location via `adhan` package on app start +- If location unavailable, use last cached location; if none, prompt user +- Notification badge on prayer card: shown when Adhan has been called (within current window) +- Tapping the Next Prayer card → navigates to Imsakiyah screen + +#### Acceptance Criteria + +- [ ] Correct prayer times displayed for device's current GPS location +- [ ] Countdown shows real-time seconds tick +- [ ] Active prayer card highlighted with `primary` border and color +- [ ] Checklist summary reflects today's actual completion state from local storage +- [ ] Weekly chart bars reflect daily worship logs from past 7 days +- [ ] Works fully offline (cached prayer times used when no internet) + +--- + +### 6.2 Prayer Calendar (Imsakiyah) + +**Route:** `/imsakiyah` +**Description:** Full Hijri-calendar view of prayer times, organized by month, with location selection. + +#### UI Components + +**Header** +- Back arrow + "Prayer Calendar" (centered, `headlineMedium`) + `more_vert` menu button + +**Month Selector** +- Horizontal scrolling chip row (no scrollbar) +- Selected: `bg-primary`, `text-slate-900`, `font-semibold`, `borderRadius: full` +- Unselected: `bg-surface`, `text-secondary`, `borderRadius: full` +- Months shown in Hijri format: "Ramadan 1445H", "Shawwal 1445H", etc. + +**Location Card** +- `bg-surface`, `borderRadius: 16dp`, `border: 1dp` +- `location_on` icon (primary) + "Your Location" label + city name (e.g., "Jakarta, Indonesia") +- `expand_more` chevron — tapping opens city search/picker + +**Prayer Times Table** +- 7-column grid: Day | Fajr | Sunrise | Dhuhr | Asr | Maghrib | Isha +- Header row: `bg-primary/10`, `10sp font-bold uppercase tracking-wider text-secondary` +- Data rows: alternating subtle bg for readability +- Today's row: highlighted with `primary/5` background, bold text + +#### Behavior + +- Loads prayer times for entire selected month using `adhan` package +- Location defaults to last GPS fix; editable via city search +- City search: local bundled list of major Indonesian cities (offline), Geocoding API optional +- Changing month or location recalculates and re-renders table +- Scroll position resets to today's row on initial load + +#### Acceptance Criteria + +- [ ] Displays complete monthly prayer timetable for selected Hijri month +- [ ] Today's row is visually highlighted +- [ ] Month chip scroll updates table data +- [ ] Location change triggers recalculation +- [ ] Works offline with bundled city coordinates + +--- + +### 6.3 Daily Checklist + +**Route:** `/checklist` +**Description:** Daily worship completion tracker with custom checklist items and progress visualization. + +#### UI Components + +**Header** +- "Daily Worship" (`headlineMedium`) + date string ("Tuesday, 24 Oct") + calendar icon button (date picker) + +**Progress Card** +- `bg-slate-900 / bg-primary/10 (dark)`, `borderRadius: 16dp`, `padding: 20dp` +- Decorative `auto_awesome` icon (64sp, opacity 10%, top-right) +- "Today's Goal" label (`xs uppercase tracking-wider text-slate-400`) +- Percentage: "{n}% Complete" (`displayLarge font-bold white`) +- "{completed} / {total} Tasks" (`primary color xs`) +- Progress bar: `h-12dp`, `bg-white/10`, `primary fill`, `borderRadius: full` +- Motivational quote text below bar (`xs text-slate-300`) + +**Task List** + +Section header: "Religious Tasks" (`sm font-bold uppercase tracking-widest text-secondary`) + +Each checklist item: +- `Container` with `bg-surface / bg-primary/5 (dark)`, `borderRadius: 12dp`, `border: 1dp` +- Custom checkbox: 24dp square, `border-2 border-primary/30`, `borderRadius: 6dp` + - Checked state: `bg-primary`, white checkmark SVG inside +- Task label: `bodyLarge font-medium` +- Optional: sub-label (e.g., target count for Tilawah) + +**Default checklist items (seeded on first launch):** + +| Item | Category | Default Target | +|---|---|---| +| Sholat Fajr | Sholat Fardhu | 1x | +| Sholat Dhuhr | Sholat Fardhu | 1x | +| Sholat Asr | Sholat Fardhu | 1x | +| Sholat Maghrib | Sholat Fardhu | 1x | +| Sholat Isha | Sholat Fardhu | 1x | +| Tilawah Quran | Tilawah | 1 Juz | +| Dzikir Pagi | Dzikir | 1 session | +| Dzikir Petang | Dzikir | 1 session | +| Sholat Sunnah Rawatib | Sunnah | 1x | +| Shodaqoh | Charity | 1x | + +#### Behavior + +- Checklist resets daily at midnight (new date key in Hive) +- Checking/unchecking an item updates Hive immediately (no "save" button) +- Progress card and percentage update reactively via Riverpod +- Motivational quotes rotate from a bundled list +- User can add/remove/reorder checklist items (edit mode) +- Completion data written to `worship_logs` for use by Reports feature + +#### Acceptance Criteria + +- [ ] Default 10 items seeded on first launch +- [ ] Checking item updates progress bar + percentage in real time +- [ ] Data persists across app restarts (Hive) +- [ ] New empty checklist created automatically on date change +- [ ] Historical completion accessible by Reports feature + +--- + +### 6.4 Dzikir + +**Route:** `/tools/dzikir` +**Description:** Guided morning and evening remembrance (Dzikir) with Arabic text, transliteration, translation, and tap counter. + +#### UI Components + +**Header (sticky)** +- `back arrow` + "Dzikir Pagi & Petang" (centered, `titleLarge font-bold`) + `info` icon button +- `bg-surface/80`, `backdropFilter: blur(12dp)`, `borderBottom: 1dp primary/10` + +**Tab Bar** +- 2 tabs: "Pagi" (Morning) and "Petang" (Evening) +- Active tab: `border-bottom-2 border-primary`, `text-primary`, `font-semibold` +- Inactive tab: `text-secondary` + +**Hero Banner** +- `text-center`, `padding: 32dp vertical` +- `bg-gradient(primary/5 → transparent, top → bottom)` +- Title: "Dzikir Pagi / Petang" (`headlineMedium font-bold`) +- Subtitle: context text in Indonesian (`bodySmall text-secondary max-width: 280dp`) + +**Dzikir Cards (scrollable list)** +- Each card: `bg-surface`, `borderRadius: 16dp`, `border: 1dp primary/10`, `padding: 20dp`, `margin: 8dp bottom` +- **Arabic text** (`Amiri font`, `24sp`, `RTL direction`, `line-height: 2.0`, `text-right`) +- **Transliteration** (`bodySmall`, `italic`, `text-secondary`, `mt: 12dp`) +- **Translation (Indonesian)** (`bodyMedium`, `text-primary`, `mt: 8dp`) +- **Counter row:** + - "Dibaca: {count} / {target}x" label + - `+` tap button (`bg-primary/10`, `text-primary`, `borderRadius: full`, `size: 40dp`) + - Counter increments on tap; fills to target + - When target reached: button becomes `check_circle` (green), card shows completion glow + +#### Behavior + +- Default content: bundled local JSON with standard Dzikir Pagi (~20 items) and Dzikir Petang (~20 items) +- Counter state persisted per dzikir per session in Hive (`dzikir_counters` box) +- Counter resets daily (tied to date) +- "Pagi" tab auto-selected between Fajr and Dhuhr; "Petang" between Maghrib and Isha; user can override +- Info button → bottom sheet with brief explanation of dzikir practice + +#### Acceptance Criteria + +- [ ] Arabic text renders correctly with Amiri font, RTL direction +- [ ] Tap counter increments and persists within the day +- [ ] Counter resets the next day +- [ ] Tab switches between Pagi and Petang content +- [ ] Completion state shown when all counters reach target + +--- + +### 6.5 Reports (Laporan) + +**Route:** `/laporan` +**Description:** Visual analytics of worship completion across weekly, monthly, and yearly timeframes. + +#### UI Components + +**Header** +- Back arrow + "Worship Quality Report" (centered) + `share` icon button + +**Tab Bar** +- 3 tabs: Weekly · Monthly · Yearly +- Active: `border-bottom-2 border-primary text-primary` +- Tab bar: `border-bottom: 1dp` + +**Main Chart Card** +- `bg-surface`, `borderRadius: 16dp`, `border: 1dp`, `padding: 20dp` +- Header row: + - Left: `analytics` icon badge (`bg-primary/10`, `primary`, `borderRadius: 12dp`, `40dp size`) + - Center: "Daily Completion" label (`bodySmall text-secondary`) + percentage (`displayLarge font-bold`) + - Right: trend chip: `trending_up` icon + delta % (`text-emerald-500` if positive) +- **Bar Chart:** + - `height: 160dp`, flex row of 7 bars (weekly) or 30/12 bars + - Each bar: `Expanded`, `bg-primary/20` track, `bg-primary` fill overlay (proportional to % complete) + - `borderRadius: top only (full)` + - Day/date labels below (`10sp font-bold text-secondary uppercase`) +- Tapping a bar → shows tooltip with exact date + completion % + +**Summary Stats Row (below chart)** +- 3 stat cards in a row: + - Best streak: "{n} days" + `local_fire_department` icon + - Average: "{n}%" + `percent` icon + - Total completed: "{n} tasks" + `check_circle` icon + +#### Behavior + +- Weekly: shows past 7 days (Mon–Sun of current week) +- Monthly: shows all days of current month (30/31 bars, may need scroll) +- Yearly: shows 12 months as bars +- Data sourced from `worship_logs` Hive box (daily completion records) +- Share button: generates a shareable image/text summary of current period stats + +#### Acceptance Criteria + +- [ ] Correct bar heights proportional to actual daily completion % +- [ ] Tab switching updates chart data and labels +- [ ] Tooltip on bar tap shows date + details +- [ ] Summary stats calculated correctly from logs +- [ ] Empty state when no logs exist ("Start tracking to see your progress") + +--- + +### 6.6 Qibla Finder + +**Route:** `/tools/qibla` or `/qibla` (accessible from Dashboard hero card) +**Description:** Compass-based Qibla direction finder showing the direction of Mecca. + +#### UI Components + +**Header** +- Back arrow + "Qibla Finder" (centered) + `my_location` button (top-right, re-centers GPS) + +**Main Content (centered)** +- Location display: `location_on` icon (primary) + city name + Qibla degree ("142.3° from North") +- **Compass widget:** + - Circular compass with N/S/E/W labels + - Rotating needle pointing to Qibla direction + - Mosque silhouette icon overlaid at compass center (fade-to-transparent mask top) + - Green pointer / arrow indicating Qibla direction +- Accuracy indicator: "High accuracy" / "Low accuracy" based on sensor confidence + +#### Behavior + +- Uses `flutter_qiblah` package for Qibla calculation + `flutter_compass` for device heading +- Rotates compass ring based on device orientation (sensor stream) +- Displays degree from North +- If location permission denied → prompt to enable, fallback to manual city entry +- Mosque silhouette uses a local SVG/image asset with `ShaderMask` fade + +#### Acceptance Criteria + +- [ ] Compass rotates smoothly with device orientation +- [ ] Qibla arrow points to correct direction based on GPS coordinates +- [ ] Works offline (no internet needed for calculation) +- [ ] Graceful fallback if compass sensor unavailable + +--- + +### 6.7 Quran Reading + +**Route:** `/tools/quran` → `/tools/quran/:surahId` +**Description:** Full Quran reader with Arabic text, Indonesian translation, and verse-by-verse display. + +#### UI Components + +**Surah List Screen (`/tools/quran`)** +- Search bar at top +- `ListView` of 114 Surahs: number badge + Arabic name + Latin name + verse count + Juz info + +**Reading Screen (`/tools/quran/:surahId`)** + +Header (sticky, `bg-surface/80 backdrop-blur`): +- Back arrow +- Center column: Surah name (Arabic, `Amiri`) + "Juz {n}" label + verse count +- `more_vert` menu (bookmarks, jump to verse, settings) + +**Bismillah banner:** Centered, Arabic font, before verse 1 (except Surah 9) + +**Verse Cards:** +- Each verse: `Container`, `bg-surface`, `borderRadius: 12dp`, `padding: 16dp`, `border: 1dp` +- Verse number badge: small circle, `bg-primary/10`, `primary text`, left-aligned +- **Arabic text:** `Amiri`, `28sp`, `RTL`, `line-height: 2.2`, right-aligned, full width +- **Transliteration** (optional, toggled in settings): `bodySmall italic text-secondary` +- **Indonesian translation:** `bodyMedium text-primary`, left-aligned, `mt: 8dp` +- Verse action row: `bookmark`, `share`, `play` (Murattal) icons + +#### Behavior + +- Quran data: bundled local JSON asset (`assets/quran/quran_id.json`) — 114 surahs, Arabic + Indonesian translation +- Reading position persisted per Surah (last verse read) +- Bookmarks stored in Hive +- "Play" icon on verse → navigates to Murattal screen for that Surah starting at that verse +- Font size adjustable via settings (stored preference) + +#### Acceptance Criteria + +- [ ] All 114 Surahs accessible +- [ ] Arabic text renders with Amiri font, correct RTL layout +- [ ] Indonesian translation displayed below each verse +- [ ] Reading position saved across app restarts +- [ ] Bookmarking a verse persists in Hive +- [ ] Works fully offline + +--- + +### 6.8 Quran Murattal + +**Route:** `/tools/quran/:surahId/murattal` +**Description:** Audio recitation player synchronized with Quran text display. + +#### UI Components + +**Header:** Same as Quran Reading (Surah name + Juz info) + +**Quran Text:** Same as Reading screen — synchronized verse highlight follows audio playback + +**Audio Player (bottom persistent panel)** +- Reciter name + surah name +- Progress slider (current position / total duration) +- `skip_previous` | `replay_10` | Play/Pause (`play_circle` / `pause_circle`, 56dp) | `forward_10` | `skip_next` +- Playback speed selector (0.75x, 1x, 1.25x, 1.5x) +- Sleep timer button + +#### Behavior + +- Audio source: Bundled MP3s for commonly used Surahs (short ones: Al-Fatihah, Juz Amma) — OR streamed from a free Quran audio API (e.g., mp3quran.net) +- Currently playing verse highlighted with `primary/10 bg` + left border accent +- Auto-scrolls to current verse during playback +- Background audio playback (continues when app backgrounded) +- Notification media controls shown in system tray during playback + +#### Acceptance Criteria + +- [ ] Audio plays and pauses correctly +- [ ] Current verse highlighted in sync with audio (best effort with timed segments) +- [ ] Background playback works (audio continues when screen off) +- [ ] System notification with media controls displayed during playback +- [ ] Playback speed adjustment works + +--- + +### 6.9 Settings + +**Route:** `/settings` +**Description:** User profile, notification preferences, display preferences, and app info. + +#### UI Components + +**Header (sticky)** +- Back arrow + "Settings" (`titleLarge font-bold`) + no right action + +**Profile Section** +- Avatar: 64dp circle, `bg-primary/20`, `border-2 border-primary`, initials or photo +- Name: (`titleMedium font-bold`) +- Email: (`bodySmall text-secondary`) +- Edit button (`text-primary`, `edit` icon, top-right of card) → edit name/email inline + +**Notification Settings Group** + +Label: "NOTIFICATIONS" (`labelSmall uppercase tracking-wider sage color`) + +Rows (separated by thin divider): +- **Adhan Notification** — per prayer toggle: Fajr, Dhuhr, Asr, Maghrib, Isha +- **Iqamah Reminder** — offset in minutes (default: 10 min, stepper or picker) +- **Daily Checklist Reminder** — time picker (default: 9:00 AM) + +Each row: `leading icon (bg-primary/10, rounded-lg, 40dp)` + `label + subtitle` + **iOS-style toggle** + +**iOS-style toggle spec:** +- Size: `51dp × 31dp` +- Track: `bg-cream (off)` / `bg-primary (on)`, `borderRadius: full` +- Thumb: `27dp` white circle, `shadow-md`, animates left↔right on toggle +- Implemented via `AnimatedContainer` + `GestureDetector` + +**Display Settings Group** + +Label: "DISPLAY" + +Rows: +- **Dark Mode**: Light / Dark / Auto (3-way segmented control or cycle toggle) +- **Font Size**: Small / Medium / Large (affects Quran + Dzikir text) +- **Language**: Indonesian / English (UI language, not Quran translation) + +**About Group** + +Label: "ABOUT" + +Rows: +- App Version: "Jamshalat Diary v1.0.0" +- Privacy Policy (launches in-app browser) +- Rate the App (links to store) +- Contact / Feedback + +#### Behavior + +- All settings persisted in Hive `settings` box immediately on change +- Dark mode change applies instantly (no restart needed) via ThemeMode Riverpod provider +- Notification toggles register/unregister `flutter_local_notifications` channels +- Iqamah offset: default 10 minutes, adjustable per prayer +- Profile name/email stored locally only (no backend account system in v1.0) + +#### Acceptance Criteria + +- [ ] All toggle states persisted and restored on restart +- [ ] Dark mode applies instantly with animation +- [ ] Adhan/Iqamah notifications schedule correctly based on calculated prayer times +- [ ] Notifications cancel when their toggle is turned off +- [ ] iOS-style toggle animation is smooth (no jank) + +--- + +## 7. Data Model + +### 7.1 Hive Boxes & Schemas + +```dart +// Settings box (key-value) +class AppSettings { + String userName; // 'Alex Rivers' + String userEmail; // 'alex@example.com' + ThemeMode themeMode; // ThemeMode.system + double arabicFontSize; // 24.0 + String uiLanguage; // 'id' | 'en' + Map adhanEnabled; // {'fajr': true, 'dhuhr': true, ...} + Map iqamahOffset; // {'fajr': 15, 'dhuhr': 10, ...} in minutes + DateTime? checklistReminderTime; + double? lastLat; + double? lastLng; + String? lastCityName; +} + +// Checklist box +@HiveType(typeId: 1) +class ChecklistItem { + String id; // UUID + String title; // 'Sholat Fajr' + String category; // 'sholat_fardhu' + String? subtitle; // '1 Juz' + int sortOrder; + bool isCustom; // false for default items +} + +// Daily log box (keyed by 'yyyy-MM-dd') +@HiveType(typeId: 2) +class DailyWorshipLog { + String date; // '2026-03-06' + Map completedItems; // {'item_id': true} + int totalItems; + int completedCount; + double completionPercent; +} + +// Dzikir counter box (keyed by 'dzikir_id:yyyy-MM-dd') +@HiveType(typeId: 3) +class DzikirCounter { + String dzikirId; + String date; + int count; + int target; +} + +// Bookmarks box +@HiveType(typeId: 4) +class QuranBookmark { + int surahId; + int verseId; + String surahName; + String verseText; // Arabic snippet + DateTime savedAt; +} + +// Cached prayer times box (keyed by 'lat_lng_yyyy-MM-dd') +@HiveType(typeId: 5) +class CachedPrayerTimes { + String key; + double lat; + double lng; + String date; + DateTime fajr; + DateTime sunrise; + DateTime dhuhr; + DateTime asr; + DateTime maghrib; + DateTime isha; +} +``` + +--- + +## 8. Third-Party Dependencies + +| Package | Version | Purpose | +|---|---|---| +| `flutter_riverpod` | ^2.x | State management | +| `riverpod_annotation` | ^2.x | Code gen for providers | +| `go_router` | ^13.x | Declarative navigation | +| `hive_flutter` | ^1.x | Local key-value storage | +| `hive_generator` | ^2.x | Hive TypeAdapter codegen | +| `adhan` | ^1.x | Prayer time calculation (offline) | +| `geolocator` | ^11.x | GPS location | +| `geocoding` | ^3.x | Reverse geocoding (city name) | +| `flutter_qiblah` | ^2.x | Qibla direction calculation | +| `flutter_compass` | ^0.x | Device compass heading | +| `flutter_local_notifications` | ^17.x | Adhan + Iqamah notifications | +| `just_audio` | ^0.x | Quran Murattal audio playback | +| `audio_service` | ^0.x | Background audio + media controls | +| `google_fonts` | ^6.x | Plus Jakarta Sans | +| `material_symbols_icons` | ^4.x | Material Symbols icon set | +| `build_runner` | ^2.x | Code generation runner | + +**Quran Data Sources:** +- Arabic text + Indonesian translation: bundled as `assets/quran/quran_id.json` (local, offline) +- Audio (Murattal): streamed from `https://server6.mp3quran.net/` (Mishari Al-Afasy) or bundled short Surahs + +--- + +## 9. Non-Functional Requirements + +### 9.1 Performance + +- App cold start: < 2 seconds on mid-range devices (Snapdragon 700 series / Apple A14) +- Bottom nav tab switch: < 150ms +- Prayer time calculation: < 50ms (synchronous, offline) +- Quran verse list render: use `ListView.builder` (lazy loading), never `ListView` with all children + +### 9.2 Offline-First + +- Core features work with no internet: Dashboard, Checklist, Dzikir, Reports, Qibla, Quran Reading, Settings +- Quran Murattal: short Surahs bundled; longer Surahs require connectivity (graceful fallback shown) +- Prayer times: cached per location+date in Hive; recalculated on location change + +### 9.3 Accessibility + +- Minimum touch target: 48×48dp for all interactive elements +- All icons have semantic labels (`Semantics(label: ...)`) +- Arabic text has `textDirection: TextDirection.rtl` explicitly set +- Sufficient contrast ratios for both light and dark themes (WCAG AA) + +### 9.4 Dark Mode + +- All screens must look correct in both light and dark mode +- No hardcoded `Colors.white` or `Colors.black` — use `AppColors` tokens only +- Theme transitions are animated (Material 3 `AnimatedTheme`) + +### 9.5 Localization + +- v1.0: Indonesian (id) as primary language, English (en) as secondary +- Use `flutter_localizations` + `intl` package for date/number formatting +- All UI strings in `AppLocalizations` (ARB files); no hardcoded Indonesian strings in widgets + +--- + +## 10. Permissions + +| Permission | Reason | Timing | +|---|---|---| +| `ACCESS_FINE_LOCATION` (Android) / `NSLocationWhenInUseUsageDescription` (iOS) | Prayer time calculation, Qibla direction | On first Dashboard load | +| `POST_NOTIFICATIONS` (Android 13+) | Adhan and Iqamah notifications | On Settings → Notifications first toggle | +| `SCHEDULE_EXACT_ALARM` (Android 12+) | Exact Adhan notification timing | When notification enabled | +| Background audio (iOS: `audio` background mode) | Murattal background playback | On Murattal first play | + +**Permission UX:** +- Never request permission without explaining why first (show rationale bottom sheet) +- Graceful degradation: if location denied → manual city picker; if notifications denied → remind once, then respect + +--- + +## 11. Out of Scope (v1.0) + +The following features are **explicitly excluded** from v1.0 to keep scope focused: + +- User accounts / cloud sync (no backend, local-only) +- Social features (sharing worship progress to social media, leaderboards) +- Full Quran audio library (only short Surahs bundled/streamed) +- Quran memorization (Hafalan) mode +- Hijri calendar widget beyond Imsakiyah +- Community / forum features +- Multi-user / family tracking +- Wear OS / watchOS companion +- Widgets (home screen app widget) +- Apple Watch prayer time complication +- In-app purchases or subscriptions +- Push notifications from a server (only local scheduled notifications) + +--- + +*PRD v1.0 — Jamshalat Diary — March 2026* diff --git a/README.md b/README.md new file mode 100644 index 0000000..e9585b6 --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ +# jamshalat_diary + +A new Flutter project. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Learn Flutter](https://docs.flutter.dev/get-started/learn-flutter) +- [Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Flutter learning resources](https://docs.flutter.dev/reference/learning-resources) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/TASKLIST.md b/TASKLIST.md new file mode 100644 index 0000000..e9b3e5c --- /dev/null +++ b/TASKLIST.md @@ -0,0 +1,341 @@ +# Jamshalat Diary — Phase-Based Implementation Tasklist + +**Project:** Jamshalat Diary Flutter App +**PRD Reference:** `PRD.md` +**Design Reference:** `stitch/` folder (18 screens) +**Last Updated:** March 2026 + +--- + +## How to Use This Document + +- **Status legend:** `[ ]` Not started · `[~]` In progress · `[x]` Done · `[-]` Skipped/deferred +- **Dependency notation:** Tasks marked with `⚠️ depends on: [task ref]` must not start until that task is complete +- **Assignable units:** Each task is atomic enough to delegate to a single agent/developer +- **Phase gates:** Do not start a phase until all `[x]` required tasks in the previous phase are complete + +--- + +## Phase 1 — Foundation +> **Goal:** Runnable Flutter app with navigation shell, design system, and theme engine wired up. +> **Gate to Phase 2:** App boots, bottom nav switches tabs, light/dark toggle works. + +### 1.1 Project Bootstrap +- [ ] **1.1.1** Create Flutter project: `flutter create jamshalat_diary --org com.jamshalat --platforms android,ios` +- [ ] **1.1.2** Add all dependencies to `pubspec.yaml` (see PRD §8 — Third-Party Dependencies) +- [ ] **1.1.3** Run `flutter pub get` and verify zero conflicts +- [ ] **1.1.4** Configure `analysis_options.yaml` with strict linting rules (`flutter_lints` + `prefer_const_constructors`) +- [ ] **1.1.5** Set up folder structure as defined in PRD §4.1 (create all `lib/` directories and placeholder files) +- [ ] **1.1.6** Add font assets to `pubspec.yaml`: Google Fonts (Plus Jakarta Sans via `google_fonts` package) + Amiri (bundled TTF in `assets/fonts/`) +- [ ] **1.1.7** Add asset directories to `pubspec.yaml`: `assets/fonts/`, `assets/quran/`, `assets/dzikir/`, `assets/images/` +- [ ] **1.1.8** Configure Android `minSdkVersion: 21`, `targetSdkVersion: 34` in `android/app/build.gradle` +- [ ] **1.1.9** Configure iOS deployment target: 13.0 in `ios/Podfile` + +### 1.2 Design System +- [ ] **1.2.1** Create `lib/app/theme/app_colors.dart` — all color tokens from PRD §3.1 (light + dark values as static consts) +- [ ] **1.2.2** Create `lib/app/theme/app_text_styles.dart` — full `TextTheme` from PRD §3.2 (Plus Jakarta Sans weights, Amiri definitions) +- [ ] **1.2.3** Create `lib/app/theme/app_theme.dart` — `ThemeData` for light and dark using tokens from 1.2.1 + 1.2.2; Material 3 enabled +- [ ] **1.2.4** Create `lib/core/widgets/ios_toggle.dart` — custom iOS-style toggle widget (51×31dp, animated thumb, `AnimatedContainer` + `GestureDetector`) per PRD §6.9 +- [ ] **1.2.5** Create `lib/core/widgets/section_header.dart` — reusable section label widget (uppercase, tracking-wider, sage color) +- [ ] **1.2.6** Create `lib/core/widgets/progress_bar.dart` — reusable linear progress bar (primary fill, configurable height + borderRadius) +- [ ] **1.2.7** Verify `AppColors` against all 18 stitch screens — no hardcoded colors in any widget + +### 1.3 Navigation Shell +- [ ] **1.3.1** Add `go_router` and set up `lib/app/router.dart` with `ShellRoute` for bottom nav tabs +- [ ] **1.3.2** Define all routes from PRD §5.2 in `router.dart` (top-level + nested) +- [ ] **1.3.3** Create `lib/core/widgets/bottom_nav_bar.dart` — 5-tab nav bar per PRD §5.1 spec (active/inactive states, Material Symbols filled/outlined) +- [ ] **1.3.4** Create stub screens for all 9 features (empty `Scaffold` + `AppBar` with feature name) so routing is testable +- [ ] **1.3.5** Create `lib/app/app.dart` — `MaterialApp.router` wired to `router.dart` + `themeMode` from Riverpod provider + +### 1.4 Theme Engine +- [ ] **1.4.1** Create `lib/core/providers/theme_provider.dart` — `StateProvider` initialized from Hive settings +- [ ] **1.4.2** Wrap `MaterialApp` in `ProviderScope`; consume `themeProvider` for `themeMode` +- [ ] **1.4.3** Verify light ↔ dark toggle visually applies instantly on all stub screens +- [ ] **1.4.4** Create `lib/main.dart` — initializes Hive, runs `ProviderScope` + `App` + +--- + +## Phase 2 — Core Data Layer +> **Goal:** All services and local storage ready. Features can read/write data. +> **Gate to Phase 3:** Prayer times calculate correctly for Jakarta coords, Hive boxes open without errors, notifications schedule in emulator. +> ⚠️ Depends on: Phase 1 complete + +### 2.1 Hive Setup +- [ ] **2.1.1** Create all 6 Hive `@HiveType` model classes from PRD §7.1: `AppSettings`, `ChecklistItem`, `DailyWorshipLog`, `DzikirCounter`, `QuranBookmark`, `CachedPrayerTimes` +- [ ] **2.1.2** Run `build_runner` to generate Hive `TypeAdapter` files (`*.g.dart`) +- [ ] **2.1.3** Create `lib/data/local/hive_boxes.dart` — box name constants + `initHive()` function that opens all boxes +- [ ] **2.1.4** Call `initHive()` in `main.dart` before `runApp()` +- [ ] **2.1.5** Seed default `AppSettings` on first launch (if settings box is empty) +- [ ] **2.1.6** Seed default 10 `ChecklistItem` entries on first launch (per PRD §6.3 default items table) + +### 2.2 Prayer Time Service +- [ ] **2.2.1** Create `lib/data/services/prayer_service.dart` using `adhan` package + - Method: `PrayerTimes getPrayerTimes(double lat, double lng, DateTime date)` + - Calculation method: Muslim World League (default), configurable in settings + - Returns: Fajr, Sunrise, Dhuhr, Asr, Maghrib, Isha as `DateTime` +- [ ] **2.2.2** Implement cache logic: before calculating, check `CachedPrayerTimes` Hive box for existing `lat_lng_date` key; write result to box after calculation +- [ ] **2.2.3** Create `lib/features/dashboard/data/prayer_times_provider.dart` — `AsyncNotifierProvider` that fetches location then returns today's `PrayerTimes` +- [ ] **2.2.4** Unit test: `getPrayerTimes(lat: -6.2088, lng: 106.8456, date: 2026-03-06)` → verify Maghrib ≈ 18:05 WIB + +### 2.3 Location Service +- [ ] **2.3.1** Create `lib/data/services/location_service.dart` + - Method: `Future getCurrentLocation()` — requests permission, gets GPS fix + - Method: `Position? getLastKnownLocation()` — reads from `AppSettings` Hive box + - Method: `Future getCityName(double lat, double lng)` — reverse geocode via `geocoding` package +- [ ] **2.3.2** Implement fallback chain: GPS → last saved → manual city picker +- [ ] **2.3.3** Save successful GPS fix to `AppSettings` (lat, lng, cityName) after each successful fetch +- [ ] **2.3.4** Add `ACCESS_FINE_LOCATION` permission to Android `AndroidManifest.xml` +- [ ] **2.3.5** Add `NSLocationWhenInUseUsageDescription` to iOS `Info.plist` + +### 2.4 Notification Service +- [ ] **2.4.1** Create `lib/data/services/notification_service.dart` using `flutter_local_notifications` + - Method: `Future init()` — initializes channels (Android: high-priority Adhan channel) + - Method: `Future scheduleAdhan(Prayer prayer, DateTime time)` — schedules exact alarm + - Method: `Future scheduleIqamah(Prayer prayer, DateTime adhanTime, int offsetMinutes)` — schedules iqamah reminder + - Method: `Future cancelAll()` — cancels all pending notifications + - Method: `Future rescheduleAll(PrayerTimes times, AppSettings settings)` — cancels + re-schedules based on current settings +- [ ] **2.4.2** Add `SCHEDULE_EXACT_ALARM` + `POST_NOTIFICATIONS` permissions to Android manifest +- [ ] **2.4.3** Add iOS background modes for notifications in `Info.plist` +- [ ] **2.4.4** Call `rescheduleAll()` on app start and whenever settings or location changes +- [ ] **2.4.5** Manual test: enable Fajr notification in settings → verify notification fires at correct time in emulator + +### 2.5 Quran Data Service +- [ ] **2.5.1** Source or create `assets/quran/quran_id.json` — 114 Surahs, each entry: `{ id, nameArabic, nameLatin, verseCount, juzStart, verses: [{id, arabic, transliteration, translation_id}] }` +- [ ] **2.5.2** Create `lib/data/services/quran_service.dart` + - Method: `Future> getAllSurahs()` — loads + parses JSON asset (cached in memory after first load) + - Method: `Future getSurah(int id)` — returns single surah with all verses +- [ ] **2.5.3** Verify JSON loads correctly; log verse count for Surah 2 (Al-Baqarah) = 286 verses + +### 2.6 Dzikir Data Service +- [ ] **2.6.1** Create `assets/dzikir/dzikir_pagi.json` and `assets/dzikir/dzikir_petang.json` — each entry: `{ id, arabic, transliteration, translation, target_count, source }` +- [ ] **2.6.2** Create `lib/data/services/dzikir_service.dart` + - Method: `Future> getDzikir(DzikirType type)` — loads from JSON asset + - Method: `Map getCountersForDate(String date)` — reads from Hive `dzikir_counters` box + - Method: `void increment(String dzikirId, String date)` — writes to Hive + +--- + +## Phase 3 — Dashboard & Prayer Calendar +> **Goal:** The two primary daily-use screens are fully functional. +> **Gate to Phase 4:** Dashboard shows live countdown, correct prayer times, checklist summary. Imsakiyah shows full monthly table. +> ⚠️ Depends on: Phase 2 complete + +### 3.1 Dashboard Screen +- [ ] **3.1.1** Build `DashboardScreen` sticky header: avatar + greeting + notification bell (taps → Settings) +- [ ] **3.1.2** Build Next Prayer Hero Card: bg-primary card, prayer name+time (displayLarge), live countdown timer widget + - Countdown: `StreamBuilder` on `Stream.periodic(Duration(seconds: 1))` computing time to next prayer +- [ ] **3.1.3** Build Adhan/Iqamah badge: notification bell icon absolutely positioned on active prayer card (shown when Adhan has been called, hidden otherwise) +- [ ] **3.1.4** Build "View Qibla" button on hero card → navigates to `/tools/qibla` +- [ ] **3.1.5** Build Prayer Times horizontal scroll: `ListView.builder` horizontal, 5 prayer cards (Fajr–Isha), active card highlighted +- [ ] **3.1.6** Build Today's Checklist Summary Card: circular SVG progress + 2 preview items + "View Full Checklist" CTA +- [ ] **3.1.7** Build Weekly Progress bar chart: 7 bars (Mon–Sun), data from `worship_logs` Hive box +- [ ] **3.1.8** Wire `prayer_times_provider` to all prayer time displays +- [ ] **3.1.9** Wire checklist summary to today's `DailyWorshipLog` from Hive +- [ ] **3.1.10** Verify screen matches `stitch/dashboard_active_nav/screen.png` visually + +### 3.2 Prayer Calendar (Imsakiyah) Screen +- [ ] **3.2.1** Build Imsakiyah screen header: back + "Prayer Calendar" + `more_vert` +- [ ] **3.2.2** Build Hijri month selector: horizontal scroll chip row; compute current + surrounding Hijri months +- [ ] **3.2.3** Build location card: shows current city name + `expand_more` tapping opens city search bottom sheet +- [ ] **3.2.4** Build city search bottom sheet: `TextField` + `ListView` of bundled Indonesian cities (local JSON asset) +- [ ] **3.2.5** Build prayer times table: `GridView` 7-column, header row, data rows for all days of selected Hijri month +- [ ] **3.2.6** Highlight today's row with `primary/5` background +- [ ] **3.2.7** Wire month selector + location to `PrayerService` — recalculate on change +- [ ] **3.2.8** Auto-scroll to today's row on screen open +- [ ] **3.2.9** Verify screen matches `stitch/imsakiyah_active_nav/screen.png` visually + +--- + +## Phase 4 — Worship Tracking +> **Goal:** Checklist, Dzikir, and Reports fully functional with persistent data. +> **Gate to Phase 5:** Checklist persists across restarts, Dzikir counter works, Reports chart renders from real log data. +> ⚠️ Depends on: Phase 2 complete (data layer), Phase 3 (worship_logs written by Dashboard) + +### 4.1 Daily Checklist Screen +- [ ] **4.1.1** Build Checklist header: "Daily Worship" + date string + calendar icon (date picker for viewing past days) +- [ ] **4.1.2** Build Progress Card: dark bg, `auto_awesome` decoration, percentage text, progress bar, motivational quote +- [ ] **4.1.3** Build task list: `ListView` of `ChecklistItem` widgets with custom checkbox +- [ ] **4.1.4** Implement custom checkbox widget: 24dp, primary border, animated check SVG on tap +- [ ] **4.1.5** Wire tap → update `DailyWorshipLog` in Hive → Riverpod provider invalidate → progress card updates reactively +- [ ] **4.1.6** Implement daily reset: on date change, create new empty `DailyWorshipLog` for new date +- [ ] **4.1.7** Write completion data to `worship_logs` box (for Reports feature to consume) +- [ ] **4.1.8** Verify screen matches `stitch/checklist_active_nav/screen.png` and `stitch/checklist_dark_mode/screen.png` + +### 4.2 Dzikir Screen +- [ ] **4.2.1** Build Dzikir header: back + "Dzikir Pagi & Petang" + info button (bottom sheet explanation) +- [ ] **4.2.2** Build Pagi/Petang tab bar with animated underline indicator +- [ ] **4.2.3** Build hero banner: gradient bg + title + subtitle +- [ ] **4.2.4** Build Dzikir card: Arabic text (Amiri, RTL, 24sp), transliteration (italic), translation, counter row +- [ ] **4.2.5** Implement tap counter: `+` button → increments count in Hive → rebuilds counter row reactively +- [ ] **4.2.6** Implement completion state: when `count >= target`, button becomes `check_circle`, card shows subtle primary glow +- [ ] **4.2.7** Implement smart tab pre-selection: Pagi between Fajr–Dhuhr, Petang between Maghrib–Isha +- [ ] **4.2.8** Verify Arabic text renders correctly RTL, no overflow, no visual glitches +- [ ] **4.2.9** Verify screen matches `stitch/dzikir_active_nav/screen.png` and `stitch/dzikir_dark_mode/screen.png` + +### 4.3 Reports (Laporan) Screen +- [ ] **4.3.1** Build Reports header: back + "Worship Quality Report" + share button +- [ ] **4.3.2** Build Weekly/Monthly/Yearly tab bar +- [ ] **4.3.3** Build main chart card: analytics badge + completion % + trend chip +- [ ] **4.3.4** Build bar chart widget: custom `CustomPainter` or `Column`+`Expanded` bars, proportional heights from log data +- [ ] **4.3.5** Implement tap-on-bar tooltip: `OverlayEntry` or `Tooltip` showing date + exact % on tap +- [ ] **4.3.6** Build summary stats row: Best streak, Average %, Total completed +- [ ] **4.3.7** Implement streak calculation: consecutive days with completion > 0% from `worship_logs` +- [ ] **4.3.8** Implement share button: generate shareable text summary (no image generation in v1.0) +- [ ] **4.3.9** Build empty state: illustration + "Start tracking to see your progress" when no logs exist +- [ ] **4.3.10** Verify screen matches `stitch/laporan_active_nav/screen.png` and `stitch/laporan_dark_mode/screen.png` + +--- + +## Phase 5 — Islamic Tools +> **Goal:** Qibla, Quran Reader, and Murattal player fully functional. +> **Gate to Phase 6:** Qibla compass rotates with device, Quran displays all 114 Surahs, audio plays in background. +> ⚠️ Depends on: Phase 2 complete (data layer) + +### 5.1 Qibla Finder Screen +- [ ] **5.1.1** Build Qibla header: back + "Qibla Finder" + `my_location` button +- [ ] **5.1.2** Build location + degree display: city name + `"{n}°} from North"` label +- [ ] **5.1.3** Build compass widget: circular ring with N/S/E/W labels, `AnimatedRotation` driven by `flutter_compass` stream +- [ ] **5.1.4** Build Qibla arrow: overlaid pointer that stays fixed to calculated Qibla bearing while compass ring rotates +- [ ] **5.1.5** Build mosque silhouette overlay: `ShaderMask` with gradient mask (fade-to-transparent at top) +- [ ] **5.1.6** Build accuracy indicator: "High accuracy" / "Low accuracy" label based on sensor data +- [ ] **5.1.7** Build permission-denied state: explanation card + "Open Settings" button +- [ ] **5.1.8** Verify compass direction is correct for Jakarta coordinates (Qibla ≈ 295° from North) + +### 5.2 Quran List Screen +- [ ] **5.2.1** Build Quran list screen header: search bar at top +- [ ] **5.2.2** Build Surah list: `ListView.builder` of 114 items + - Each item: number badge (primary/10 bg) + Arabic name (Amiri) + Latin name + verse count + Juz +- [ ] **5.2.3** Implement search filter: filters by Latin name or Surah number in real time + +### 5.3 Quran Reading Screen +- [ ] **5.3.1** Build reading screen sticky header: back + Surah name (Arabic, Amiri) + Juz info + `more_vert` +- [ ] **5.3.2** Build Bismillah banner (shown for all Surahs except Surah 9 — At-Tawbah) +- [ ] **5.3.3** Build verse card: verse number badge + Arabic text (Amiri, 28sp, RTL) + transliteration + translation +- [ ] **5.3.4** Build verse action row: `bookmark`, `share`, `play` icons per verse +- [ ] **5.3.5** Implement bookmarking: tap bookmark icon → save `QuranBookmark` to Hive → icon toggles to filled +- [ ] **5.3.6** Implement reading position persistence: save last scroll position (verse index) to Hive per Surah +- [ ] **5.3.7** Wire `play` icon on verse → navigate to `/tools/quran/:surahId/murattal` starting at that verse +- [ ] **5.3.8** Verify Arabic text is RTL, no clipping, line-height comfortable (2.2 per PRD) +- [ ] **5.3.9** Verify screen matches `stitch/quran_reading_active_nav/screen.png` and `stitch/quran_dark_mode/screen.png` + +### 5.4 Quran Murattal Screen +- [ ] **5.4.1** Initialize `just_audio` player + `audio_service` for background playback +- [ ] **5.4.2** Build Murattal screen: inherits Quran Reading layout + adds audio player panel at bottom +- [ ] **5.4.3** Build audio player panel: reciter name + progress slider + transport controls (`skip_previous`, `replay_10`, play/pause, `forward_10`, `skip_next`) +- [ ] **5.4.4** Build playback speed selector: 0.75x / 1x / 1.25x / 1.5x chips +- [ ] **5.4.5** Implement verse highlight sync: highlight current verse based on audio position (timed segments per Surah) +- [ ] **5.4.6** Implement auto-scroll: `ScrollController` scrolls to current verse during playback +- [ ] **5.4.7** Configure background audio: `audio_service` handler, system media controls in notification tray +- [ ] **5.4.8** Bundle short Surahs (Juz Amma: Surah 78–114) as local MP3 assets +- [ ] **5.4.9** Implement streaming fallback for longer Surahs (mp3quran.net API) +- [ ] **5.4.10** Build offline fallback state for streamed Surahs: "Connect to internet to play this Surah" +- [ ] **5.4.11** Verify screen matches `stitch/quran_murattal_active_nav/screen.png` + +--- + +## Phase 6 — Settings & System Polish +> **Goal:** Settings screen complete, all toggles wired, notifications working end-to-end, dark mode seamless. +> **Gate to Phase 7:** All settings persist, dark mode switches instantly, Adhan notification fires correctly, no hardcoded colors remain. +> ⚠️ Depends on: Phase 4 + Phase 5 complete + +### 6.1 Settings Screen +- [ ] **6.1.1** Build Settings header: back + "Settings" title +- [ ] **6.1.2** Build Profile section: avatar + name + email + edit button (inline editing) +- [ ] **6.1.3** Build Notifications group: per-prayer Adhan toggles (5 prayers) using `IosToggle` widget +- [ ] **6.1.4** Build Iqamah offset row: per-prayer minute offset picker (stepper, default 10 min) +- [ ] **6.1.5** Build Daily Checklist Reminder row: time picker, default 9:00 AM +- [ ] **6.1.6** Build Display group: Dark Mode 3-way control (Light/Dark/Auto), Font Size selector, Language selector +- [ ] **6.1.7** Build About group: App Version, Privacy Policy (in-app WebView), Rate App link, Feedback +- [ ] **6.1.8** Wire all toggles → write to `AppSettings` Hive box → trigger side effects (notification reschedule, theme change) +- [ ] **6.1.9** Verify screen matches `stitch/settings_with_dark_mode_option/screen.png` and `stitch/settings_dark_mode/screen.png` + +### 6.2 Dark Mode Polish +- [ ] **6.2.1** Audit ALL screens in dark mode against `stitch/*_dark_mode/screen.png` — fix any color token mismatches +- [ ] **6.2.2** Verify no hardcoded `Colors.white`, `Colors.black`, or raw hex strings remain in widget code (grep codebase) +- [ ] **6.2.3** Verify `AnimatedTheme` transition is smooth (no flash/jank on toggle) +- [ ] **6.2.4** Test system auto mode: app follows device dark mode setting correctly + +### 6.3 Notification End-to-End +- [ ] **6.3.1** Verify Adhan notifications fire at correct prayer times on Android emulator (API 34) +- [ ] **6.3.2** Verify Adhan notifications fire correctly on iOS simulator (iOS 16+) +- [ ] **6.3.3** Verify notifications are cancelled when prayer toggle is turned off in Settings +- [ ] **6.3.4** Verify Iqamah notification fires `n` minutes after Adhan (configurable offset) +- [ ] **6.3.5** Verify notifications reschedule correctly after location changes (new prayer times → cancel old → schedule new) +- [ ] **6.3.6** Verify tapping notification → opens app on Dashboard screen + +### 6.4 Accessibility Pass +- [ ] **6.4.1** Audit all `IconButton`s and small tap targets: minimum 48×48dp (add `SizedBox` wrappers where needed) +- [ ] **6.4.2** Add `Semantics(label: ...)` to all icon-only buttons across all screens +- [ ] **6.4.3** Verify all Arabic text has explicit `textDirection: TextDirection.rtl` +- [ ] **6.4.4** Verify contrast ratios for all text on primary bg (`#70df20` background with dark text = check) +- [ ] **6.4.5** Test with TalkBack (Android) and VoiceOver (iOS) — all interactive elements must be reachable + +### 6.5 Localization +- [ ] **6.5.1** Set up `flutter_localizations` + `intl` in `pubspec.yaml` +- [ ] **6.5.2** Create `lib/l10n/app_id.arb` (Indonesian) and `lib/l10n/app_en.arb` (English) with all UI strings +- [ ] **6.5.3** Replace all hardcoded Indonesian strings in widgets with `AppLocalizations.of(context).*` +- [ ] **6.5.4** Verify language switch in Settings changes all UI strings (no restart required) + +--- + +## Phase 7 — QA & Release Prep +> **Goal:** App is production-ready. All screens verified, performance validated, store assets ready. +> **Gate to ship:** Zero critical bugs, performance targets met, store listing complete. +> ⚠️ Depends on: Phase 6 complete + +### 7.1 Integration Testing +- [ ] **7.1.1** Write integration test: full checklist flow (open app → check 5 items → verify progress = 50% → restart → verify persisted) +- [ ] **7.1.2** Write integration test: prayer time accuracy for 5 major Indonesian cities (Jakarta, Surabaya, Medan, Makassar, Denpasar) +- [ ] **7.1.3** Write integration test: dark mode toggle persists across app restart +- [ ] **7.1.4** Write integration test: Dzikir counter increments and resets on next day +- [ ] **7.1.5** Manual test: Quran reads all 114 Surahs without crash (test Surah 2 — largest Surah) +- [ ] **7.1.6** Manual test: Murattal audio plays + background playback continues when screen locked + +### 7.2 Performance Audit +- [ ] **7.2.1** Profile cold start time on mid-range Android device — must be < 2 seconds +- [ ] **7.2.2** Profile bottom nav tab switches — must be < 150ms +- [ ] **7.2.3** Profile Quran Surah 2 scroll (286 verses) — must maintain 60fps +- [ ] **7.2.4** Run `flutter analyze` — zero warnings, zero errors +- [ ] **7.2.5** Run `flutter test` — all unit tests pass + +### 7.3 App Assets & Store Prep +- [ ] **7.3.1** Create app icon (1024×1024px): mosque/compass motif with `#70df20` primary color +- [ ] **7.3.2** Apply app icon via `flutter_launcher_icons` package (all densities, adaptive icon for Android) +- [ ] **7.3.3** Create splash screen via `flutter_native_splash` package (white/dark bg, centered logo) +- [ ] **7.3.4** Set app name: "Jamshalat Diary" in `AndroidManifest.xml` and `Info.plist` +- [ ] **7.3.5** Set bundle ID: `com.jamshalat.diary` on both platforms +- [ ] **7.3.6** Configure release signing (Android keystore, iOS certificates) — document in private README +- [ ] **7.3.7** Build release APK: `flutter build apk --release` — verify no build errors +- [ ] **7.3.8** Build iOS release: `flutter build ipa --release` — verify no build errors +- [ ] **7.3.9** Write Play Store listing: app description (Indonesian + English), screenshots (1 per key screen), tags +- [ ] **7.3.10** Write App Store listing: same content, App Store Connect metadata + +--- + +## Progress Tracker + +| Phase | Total Tasks | Done | Remaining | Status | +|---|---|---|---|---| +| Phase 1 — Foundation | 22 | 0 | 22 | Not started | +| Phase 2 — Data Layer | 21 | 0 | 21 | Not started | +| Phase 3 — Dashboard & Calendar | 18 | 0 | 18 | Not started | +| Phase 4 — Worship Tracking | 28 | 0 | 28 | Not started | +| Phase 5 — Islamic Tools | 27 | 0 | 27 | Not started | +| Phase 6 — Settings & Polish | 22 | 0 | 22 | Not started | +| Phase 7 — QA & Release | 20 | 0 | 20 | Not started | +| **TOTAL** | **158** | **0** | **158** | 🔴 Not started | + +--- + +## Quick Reference — Key Files + +| File | Purpose | +|---|---| +| `PRD.md` | Full product requirements — source of truth | +| `stitch/*/screen.png` | Visual reference for each screen | +| `stitch/*/code.html` | HTML implementation reference for each screen | +| `TASKLIST.md` | This document — execution plan | + +--- + +*TASKLIST v1.0 — Jamshalat Diary — March 2026* diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..90e4a7f --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,14 @@ +include: package:flutter_lints/flutter.yaml + +linter: + rules: + prefer_const_constructors: true + prefer_const_declarations: true + prefer_const_literals_to_create_immutables: true + prefer_final_fields: true + prefer_final_locals: true + avoid_print: true + avoid_unnecessary_containers: true + sized_box_for_whitespace: true + use_key_in_widget_constructors: true + prefer_single_quotes: true diff --git a/android/app/build.gradle b/android/app/build.gradle new file mode 100644 index 0000000..72c2e0d --- /dev/null +++ b/android/app/build.gradle @@ -0,0 +1,11 @@ +android { + compileSdkVersion 34 + + defaultConfig { + applicationId "com.jamshalat.diary" + minSdkVersion 21 + targetSdkVersion 34 + versionCode 1 + versionName "1.0.0" + } +} \ No newline at end of file diff --git a/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java b/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java new file mode 100644 index 0000000..3c492ae --- /dev/null +++ b/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java @@ -0,0 +1,84 @@ +package io.flutter.plugins; + +import androidx.annotation.Keep; +import androidx.annotation.NonNull; +import io.flutter.Log; + +import io.flutter.embedding.engine.FlutterEngine; + +/** + * Generated file. Do not edit. + * This file is generated by the Flutter tool based on the + * plugins that support the Android platform. + */ +@Keep +public final class GeneratedPluginRegistrant { + private static final String TAG = "GeneratedPluginRegistrant"; + public static void registerWith(@NonNull FlutterEngine flutterEngine) { + try { + flutterEngine.getPlugins().add(new com.ryanheise.audioservice.AudioServicePlugin()); + } catch (Exception e) { + Log.e(TAG, "Error registering plugin audio_service, com.ryanheise.audioservice.AudioServicePlugin", e); + } + try { + flutterEngine.getPlugins().add(new com.ryanheise.audio_session.AudioSessionPlugin()); + } catch (Exception e) { + Log.e(TAG, "Error registering plugin audio_session, com.ryanheise.audio_session.AudioSessionPlugin", e); + } + try { + flutterEngine.getPlugins().add(new com.hemanthraj.fluttercompass.FlutterCompassPlugin()); + } catch (Exception e) { + Log.e(TAG, "Error registering plugin flutter_compass, com.hemanthraj.fluttercompass.FlutterCompassPlugin", e); + } + try { + flutterEngine.getPlugins().add(new com.hemanthraj.fluttercompass.FlutterCompassPlugin()); + } catch (Exception e) { + Log.e(TAG, "Error registering plugin flutter_compass_v2, com.hemanthraj.fluttercompass.FlutterCompassPlugin", e); + } + try { + flutterEngine.getPlugins().add(new com.dexterous.flutterlocalnotifications.FlutterLocalNotificationsPlugin()); + } catch (Exception e) { + Log.e(TAG, "Error registering plugin flutter_local_notifications, com.dexterous.flutterlocalnotifications.FlutterLocalNotificationsPlugin", e); + } + try { + flutterEngine.getPlugins().add(new ml.medyas.flutter_qiblah.FlutterQiblahPlugin()); + } catch (Exception e) { + Log.e(TAG, "Error registering plugin flutter_qiblah, ml.medyas.flutter_qiblah.FlutterQiblahPlugin", e); + } + try { + flutterEngine.getPlugins().add(new com.baseflow.geocoding.GeocodingPlugin()); + } catch (Exception e) { + Log.e(TAG, "Error registering plugin geocoding_android, com.baseflow.geocoding.GeocodingPlugin", e); + } + try { + flutterEngine.getPlugins().add(new com.baseflow.geolocator.GeolocatorPlugin()); + } catch (Exception e) { + Log.e(TAG, "Error registering plugin geolocator_android, com.baseflow.geolocator.GeolocatorPlugin", e); + } + try { + flutterEngine.getPlugins().add(new com.ryanheise.just_audio.JustAudioPlugin()); + } catch (Exception e) { + Log.e(TAG, "Error registering plugin just_audio, com.ryanheise.just_audio.JustAudioPlugin", e); + } + try { + flutterEngine.getPlugins().add(new dev.fluttercommunity.plus.packageinfo.PackageInfoPlugin()); + } catch (Exception e) { + Log.e(TAG, "Error registering plugin package_info_plus, dev.fluttercommunity.plus.packageinfo.PackageInfoPlugin", e); + } + try { + flutterEngine.getPlugins().add(new io.flutter.plugins.pathprovider.PathProviderPlugin()); + } catch (Exception e) { + Log.e(TAG, "Error registering plugin path_provider_android, io.flutter.plugins.pathprovider.PathProviderPlugin", e); + } + try { + flutterEngine.getPlugins().add(new com.tekartik.sqflite.SqflitePlugin()); + } catch (Exception e) { + Log.e(TAG, "Error registering plugin sqflite_android, com.tekartik.sqflite.SqflitePlugin", e); + } + try { + flutterEngine.getPlugins().add(new io.flutter.plugins.urllauncher.UrlLauncherPlugin()); + } catch (Exception e) { + Log.e(TAG, "Error registering plugin url_launcher_android, io.flutter.plugins.urllauncher.UrlLauncherPlugin", e); + } + } +} diff --git a/android/local.properties b/android/local.properties new file mode 100644 index 0000000..f34f6c9 --- /dev/null +++ b/android/local.properties @@ -0,0 +1,2 @@ +sdk.dir=/Users/dwindown/Library/Android/sdk +flutter.sdk=/Users/dwindown/FlutterDev/flutter \ No newline at end of file diff --git a/assets/dzikir/dzikir_pagi.json b/assets/dzikir/dzikir_pagi.json new file mode 100644 index 0000000..44cd779 --- /dev/null +++ b/assets/dzikir/dzikir_pagi.json @@ -0,0 +1,58 @@ +[ + { + "id": "pagi_01", + "arabic": "أَعُوذُ بِاللَّهِ مِنَ الشَّيْطَانِ الرَّجِيمِ", + "transliteration": "A'udzu billahi minasy syaithanir rajiim", + "translation": "Aku berlindung kepada Allah dari godaan syaitan yang terkutuk", + "target_count": 1, + "source": "HR. Abu Dawud" + }, + { + "id": "pagi_02", + "arabic": "بِسْمِ اللَّهِ الرَّحْمَنِ الرَّحِيمِ", + "transliteration": "Bismillahir rahmaanir rahiim", + "translation": "Dengan nama Allah Yang Maha Pengasih lagi Maha Penyayang", + "target_count": 1, + "source": "" + }, + { + "id": "pagi_03", + "arabic": "أَصْبَحْنَا وَأَصْبَحَ الْمُلْكُ لِلَّهِ، وَالْحَمْدُ لِلَّهِ", + "transliteration": "Ashbahnaa wa ashbahal mulku lillah, walhamdu lillah", + "translation": "Kami telah memasuki waktu pagi dan kerajaan hanya milik Allah, segala puji bagi Allah", + "target_count": 1, + "source": "HR. Muslim" + }, + { + "id": "pagi_04", + "arabic": "اللَّهُمَّ بِكَ أَصْبَحْنَا وَبِكَ أَمْسَيْنَا وَبِكَ نَحْيَا وَبِكَ نَمُوتُ وَإِلَيْكَ النُّشُورُ", + "transliteration": "Allahumma bika ashbahnaa wa bika amsainaa wa bika nahyaa wa bika namuutu wa ilaikan nusyuur", + "translation": "Ya Allah, dengan rahmat dan pertolongan-Mu kami memasuki waktu pagi, dan dengan rahmat-Mu kami memasuki waktu petang", + "target_count": 1, + "source": "HR. Tirmidzi" + }, + { + "id": "pagi_05", + "arabic": "سُبْحَانَ اللَّهِ وَبِحَمْدِهِ", + "transliteration": "Subhanallahi wa bihamdihi", + "translation": "Maha Suci Allah dan dengan memuji-Nya", + "target_count": 100, + "source": "HR. Muslim" + }, + { + "id": "pagi_06", + "arabic": "لاَ إِلَهَ إِلاَّ اللَّهُ وَحْدَهُ لاَ شَرِيكَ لَهُ", + "transliteration": "Laa ilaaha illallahu wahdahu laa syariika lahu", + "translation": "Tiada Tuhan selain Allah semata, tidak ada sekutu bagi-Nya", + "target_count": 10, + "source": "HR. Bukhari & Muslim" + }, + { + "id": "pagi_07", + "arabic": "أَسْتَغْفِرُ اللَّهَ وَأَتُوبُ إِلَيْهِ", + "transliteration": "Astaghfirullaha wa atuubu ilaihi", + "translation": "Aku memohon ampun kepada Allah dan bertaubat kepada-Nya", + "target_count": 100, + "source": "HR. Bukhari & Muslim" + } +] \ No newline at end of file diff --git a/assets/dzikir/dzikir_petang.json b/assets/dzikir/dzikir_petang.json new file mode 100644 index 0000000..8c5a2a0 --- /dev/null +++ b/assets/dzikir/dzikir_petang.json @@ -0,0 +1,58 @@ +[ + { + "id": "petang_01", + "arabic": "أَعُوذُ بِاللَّهِ مِنَ الشَّيْطَانِ الرَّجِيمِ", + "transliteration": "A'udzu billahi minasy syaithanir rajiim", + "translation": "Aku berlindung kepada Allah dari godaan syaitan yang terkutuk", + "target_count": 1, + "source": "HR. Abu Dawud" + }, + { + "id": "petang_02", + "arabic": "بِسْمِ اللَّهِ الرَّحْمَنِ الرَّحِيمِ", + "transliteration": "Bismillahir rahmaanir rahiim", + "translation": "Dengan nama Allah Yang Maha Pengasih lagi Maha Penyayang", + "target_count": 1, + "source": "" + }, + { + "id": "petang_03", + "arabic": "أَمْسَيْنَا وَأَمْسَى الْمُلْكُ لِلَّهِ، وَالْحَمْدُ لِلَّهِ", + "transliteration": "Amsainaa wa amsal mulku lillah, walhamdu lillah", + "translation": "Kami telah memasuki waktu petang dan kerajaan hanya milik Allah, segala puji bagi Allah", + "target_count": 1, + "source": "HR. Muslim" + }, + { + "id": "petang_04", + "arabic": "اللَّهُمَّ بِكَ أَمْسَيْنَا وَبِكَ أَصْبَحْنَا وَبِكَ نَحْيَا وَبِكَ نَمُوتُ وَإِلَيْكَ الْمَصِيرُ", + "transliteration": "Allahumma bika amsainaa wa bika ashbahnaa wa bika nahyaa wa bika namuutu wa ilaikal mashiir", + "translation": "Ya Allah, dengan rahmat-Mu kami memasuki waktu petang, dan dengan rahmat-Mu kami memasuki waktu pagi", + "target_count": 1, + "source": "HR. Tirmidzi" + }, + { + "id": "petang_05", + "arabic": "سُبْحَانَ اللَّهِ وَبِحَمْدِهِ", + "transliteration": "Subhanallahi wa bihamdihi", + "translation": "Maha Suci Allah dan dengan memuji-Nya", + "target_count": 100, + "source": "HR. Muslim" + }, + { + "id": "petang_06", + "arabic": "لاَ إِلَهَ إِلاَّ اللَّهُ وَحْدَهُ لاَ شَرِيكَ لَهُ", + "transliteration": "Laa ilaaha illallahu wahdahu laa syariika lahu", + "translation": "Tiada Tuhan selain Allah semata, tidak ada sekutu bagi-Nya", + "target_count": 10, + "source": "HR. Bukhari & Muslim" + }, + { + "id": "petang_07", + "arabic": "أَسْتَغْفِرُ اللَّهَ وَأَتُوبُ إِلَيْهِ", + "transliteration": "Astaghfirullaha wa atuubu ilaihi", + "translation": "Aku memohon ampun kepada Allah dan bertaubat kepada-Nya", + "target_count": 100, + "source": "HR. Bukhari & Muslim" + } +] \ No newline at end of file diff --git a/assets/fonts/Amiri-Bold.ttf b/assets/fonts/Amiri-Bold.ttf new file mode 100644 index 0000000..b97c24d --- /dev/null +++ b/assets/fonts/Amiri-Bold.ttf @@ -0,0 +1 @@ +Placeholder for Amiri-Bold.ttf diff --git a/assets/fonts/Amiri-Regular.ttf b/assets/fonts/Amiri-Regular.ttf new file mode 100644 index 0000000..e44f124 Binary files /dev/null and b/assets/fonts/Amiri-Regular.ttf differ diff --git a/assets/fonts/PlusJakartaSans-Bold.ttf b/assets/fonts/PlusJakartaSans-Bold.ttf new file mode 100644 index 0000000..4642b77 --- /dev/null +++ b/assets/fonts/PlusJakartaSans-Bold.ttf @@ -0,0 +1,1469 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Page not found · GitHub · GitHub + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ Skip to content + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+ + + + + +
+ + + + + + + + + +
+
+ + + +
+
+ +
+
+ 404 “This is not the web page you are looking for” + + + + + + + + + + + + +
+
+ +
+
+ +
+ + +
+
+ +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + diff --git a/assets/fonts/PlusJakartaSans-ExtraBold.ttf b/assets/fonts/PlusJakartaSans-ExtraBold.ttf new file mode 100644 index 0000000..75b0d4a --- /dev/null +++ b/assets/fonts/PlusJakartaSans-ExtraBold.ttf @@ -0,0 +1,1469 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Page not found · GitHub · GitHub + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ Skip to content + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+ + + + + +
+ + + + + + + + + +
+
+ + + +
+
+ +
+
+ 404 “This is not the web page you are looking for” + + + + + + + + + + + + +
+
+ +
+
+ +
+ + +
+
+ +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + diff --git a/assets/fonts/PlusJakartaSans-Regular.ttf b/assets/fonts/PlusJakartaSans-Regular.ttf new file mode 100644 index 0000000..7070136 --- /dev/null +++ b/assets/fonts/PlusJakartaSans-Regular.ttf @@ -0,0 +1,1469 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Page not found · GitHub · GitHub + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ Skip to content + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+ + + + + +
+ + + + + + + + + +
+
+ + + +
+
+ +
+
+ 404 “This is not the web page you are looking for” + + + + + + + + + + + + +
+
+ +
+
+ +
+ + +
+
+ +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + diff --git a/assets/fonts/PlusJakartaSans-SemiBold.ttf b/assets/fonts/PlusJakartaSans-SemiBold.ttf new file mode 100644 index 0000000..d29f89d --- /dev/null +++ b/assets/fonts/PlusJakartaSans-SemiBold.ttf @@ -0,0 +1,1469 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Page not found · GitHub · GitHub + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ Skip to content + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+ + + + + +
+ + + + + + + + + +
+
+ + + +
+
+ +
+
+ 404 “This is not the web page you are looking for” + + + + + + + + + + + + +
+
+ +
+
+ +
+ + +
+
+ +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + diff --git a/assets/quran/quran_id.json b/assets/quran/quran_id.json new file mode 100644 index 0000000..fe3893d --- /dev/null +++ b/assets/quran/quran_id.json @@ -0,0 +1,100 @@ +[ + { + "id": 1, + "name_arabic": "الفاتحة", + "name_latin": "Al-Fatihah", + "verse_count": 7, + "juz_start": 1, + "revelation_type": "Meccan", + "verses": [ + { + "id": 1, + "arabic": "بِسْمِ اللَّهِ الرَّحْمَـٰنِ الرَّحِيمِ", + "transliteration": "Bismillaahir Rahmaanir Raheem", + "translation_id": "Dengan nama Allah Yang Maha Pengasih lagi Maha Penyayang" + }, + { + "id": 2, + "arabic": "الْحَمْدُ لِلَّهِ رَبِّ الْعَالَمِينَ", + "transliteration": "Alhamdu lillaahi Rabbil 'aalameen", + "translation_id": "Segala puji bagi Allah, Tuhan seluruh alam" + }, + { + "id": 3, + "arabic": "الرَّحْمَـٰنِ الرَّحِيمِ", + "transliteration": "Ar-Rahmaanir-Raheem", + "translation_id": "Yang Maha Pengasih lagi Maha Penyayang" + }, + { + "id": 4, + "arabic": "مَالِكِ يَوْمِ الدِّينِ", + "transliteration": "Maaliki Yawmid-Deen", + "translation_id": "Pemilik hari pembalasan" + }, + { + "id": 5, + "arabic": "إِيَّاكَ نَعْبُدُ وَإِيَّاكَ نَسْتَعِينُ", + "transliteration": "Iyyaaka na'budu wa lyyaaka nasta'een", + "translation_id": "Hanya kepada Engkaulah kami menyembah dan hanya kepada Engkaulah kami mohon pertolongan" + }, + { + "id": 6, + "arabic": "اهْدِنَا الصِّرَاطَ الْمُسْتَقِيمَ", + "transliteration": "Ihdinas-Siraatal-Mustaqeem", + "translation_id": "Tunjukilah kami jalan yang lurus" + }, + { + "id": 7, + "arabic": "صِرَاطَ الَّذِينَ أَنْعَمْتَ عَلَيْهِمْ غَيْرِ الْمَغْضُوبِ عَلَيْهِمْ وَلَا الضَّالِّينَ", + "transliteration": "Siraatal-lazeena an'amta 'alaihim ghayril-maghdoobi 'alaihim wa lad-daaalleen", + "translation_id": "Yaitu jalan orang-orang yang telah Engkau beri nikmat kepadanya; bukan jalan mereka yang dimurkai dan bukan pula jalan mereka yang sesat" + } + ] + }, + { + "id": 114, + "name_arabic": "النَّاس", + "name_latin": "An-Nas", + "verse_count": 6, + "juz_start": 30, + "revelation_type": "Meccan", + "verses": [ + { + "id": 1, + "arabic": "قُلْ أَعُوذُ بِرَبِّ النَّاسِ", + "transliteration": "Qul a'uudzu birabbinnaas", + "translation_id": "Katakanlah: Aku berlindung kepada Tuhannya manusia" + }, + { + "id": 2, + "arabic": "مَلِكِ النَّاسِ", + "transliteration": "Malikinnaas", + "translation_id": "Raja manusia" + }, + { + "id": 3, + "arabic": "إِلَـٰهِ النَّاسِ", + "transliteration": "Ilaahinnaas", + "translation_id": "Sembahan manusia" + }, + { + "id": 4, + "arabic": "مِن شَرِّ الْوَسْوَاسِ الْخَنَّاسِ", + "transliteration": "Min syarril waswaasil khannaas", + "translation_id": "Dari kejahatan bisikan syaitan yang biasa bersembunyi" + }, + { + "id": 5, + "arabic": "الَّذِي يُوَسْوِسُ فِي صُدُورِ النَّاسِ", + "transliteration": "Allazii yuwaswisu fii suduurinnaas", + "translation_id": "Yang membisikkan kejahatan ke dalam dada manusia" + }, + { + "id": 6, + "arabic": "مِنَ الْجِنَّةِ وَالنَّاسِ", + "transliteration": "Minal jinnati wannaas", + "translation_id": "Dari golongan jin dan manusia" + } + ] + } +] \ No newline at end of file diff --git a/ios/Flutter/Generated.xcconfig b/ios/Flutter/Generated.xcconfig new file mode 100644 index 0000000..94693c2 --- /dev/null +++ b/ios/Flutter/Generated.xcconfig @@ -0,0 +1,14 @@ +// This is a generated file; do not edit or check into version control. +FLUTTER_ROOT=/Users/dwindown/FlutterDev/flutter +FLUTTER_APPLICATION_PATH=/Users/dwindown/CascadeProjects/jamshalat-diary +COCOAPODS_PARALLEL_CODE_SIGN=true +FLUTTER_TARGET=lib/main.dart +FLUTTER_BUILD_DIR=build +FLUTTER_BUILD_NAME=1.0.0 +FLUTTER_BUILD_NUMBER=1 +EXCLUDED_ARCHS[sdk=iphonesimulator*]=i386 +EXCLUDED_ARCHS[sdk=iphoneos*]=armv7 +DART_OBFUSCATION=false +TRACK_WIDGET_CREATION=true +TREE_SHAKE_ICONS=false +PACKAGE_CONFIG=.dart_tool/package_config.json diff --git a/ios/Flutter/ephemeral/flutter_lldb_helper.py b/ios/Flutter/ephemeral/flutter_lldb_helper.py new file mode 100644 index 0000000..a88caf9 --- /dev/null +++ b/ios/Flutter/ephemeral/flutter_lldb_helper.py @@ -0,0 +1,32 @@ +# +# Generated file, do not edit. +# + +import lldb + +def handle_new_rx_page(frame: lldb.SBFrame, bp_loc, extra_args, intern_dict): + """Intercept NOTIFY_DEBUGGER_ABOUT_RX_PAGES and touch the pages.""" + base = frame.register["x0"].GetValueAsAddress() + page_len = frame.register["x1"].GetValueAsUnsigned() + + # Note: NOTIFY_DEBUGGER_ABOUT_RX_PAGES will check contents of the + # first page to see if handled it correctly. This makes diagnosing + # misconfiguration (e.g. missing breakpoint) easier. + data = bytearray(page_len) + data[0:8] = b'IHELPED!' + + error = lldb.SBError() + frame.GetThread().GetProcess().WriteMemory(base, data, error) + if not error.Success(): + print(f'Failed to write into {base}[+{page_len}]', error) + return + +def __lldb_init_module(debugger: lldb.SBDebugger, _): + target = debugger.GetDummyTarget() + # Caveat: must use BreakpointCreateByRegEx here and not + # BreakpointCreateByName. For some reasons callback function does not + # get carried over from dummy target for the later. + bp = target.BreakpointCreateByRegex("^NOTIFY_DEBUGGER_ABOUT_RX_PAGES$") + bp.SetScriptCallbackFunction('{}.handle_new_rx_page'.format(__name__)) + bp.SetAutoContinue(True) + print("-- LLDB integration loaded --") diff --git a/ios/Flutter/ephemeral/flutter_lldbinit b/ios/Flutter/ephemeral/flutter_lldbinit new file mode 100644 index 0000000..e3ba6fb --- /dev/null +++ b/ios/Flutter/ephemeral/flutter_lldbinit @@ -0,0 +1,5 @@ +# +# Generated file, do not edit. +# + +command script import --relative-to-command-file flutter_lldb_helper.py diff --git a/ios/Flutter/flutter_export_environment.sh b/ios/Flutter/flutter_export_environment.sh new file mode 100755 index 0000000..0f03ae0 --- /dev/null +++ b/ios/Flutter/flutter_export_environment.sh @@ -0,0 +1,13 @@ +#!/bin/sh +# This is a generated file; do not edit or check into version control. +export "FLUTTER_ROOT=/Users/dwindown/FlutterDev/flutter" +export "FLUTTER_APPLICATION_PATH=/Users/dwindown/CascadeProjects/jamshalat-diary" +export "COCOAPODS_PARALLEL_CODE_SIGN=true" +export "FLUTTER_TARGET=lib/main.dart" +export "FLUTTER_BUILD_DIR=build" +export "FLUTTER_BUILD_NAME=1.0.0" +export "FLUTTER_BUILD_NUMBER=1" +export "DART_OBFUSCATION=false" +export "TRACK_WIDGET_CREATION=true" +export "TREE_SHAKE_ICONS=false" +export "PACKAGE_CONFIG=.dart_tool/package_config.json" diff --git a/ios/Podfile b/ios/Podfile new file mode 100644 index 0000000..204cfd8 --- /dev/null +++ b/ios/Podfile @@ -0,0 +1 @@ +platform :ios, '13.0' \ No newline at end of file diff --git a/ios/Runner/GeneratedPluginRegistrant.h b/ios/Runner/GeneratedPluginRegistrant.h new file mode 100644 index 0000000..7a89092 --- /dev/null +++ b/ios/Runner/GeneratedPluginRegistrant.h @@ -0,0 +1,19 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GeneratedPluginRegistrant_h +#define GeneratedPluginRegistrant_h + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface GeneratedPluginRegistrant : NSObject ++ (void)registerWithRegistry:(NSObject*)registry; +@end + +NS_ASSUME_NONNULL_END +#endif /* GeneratedPluginRegistrant_h */ diff --git a/ios/Runner/GeneratedPluginRegistrant.m b/ios/Runner/GeneratedPluginRegistrant.m new file mode 100644 index 0000000..766e65e --- /dev/null +++ b/ios/Runner/GeneratedPluginRegistrant.m @@ -0,0 +1,98 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#import "GeneratedPluginRegistrant.h" + +#if __has_include() +#import +#else +@import audio_service; +#endif + +#if __has_include() +#import +#else +@import audio_session; +#endif + +#if __has_include() +#import +#else +@import flutter_compass; +#endif + +#if __has_include() +#import +#else +@import flutter_compass_v2; +#endif + +#if __has_include() +#import +#else +@import flutter_local_notifications; +#endif + +#if __has_include() +#import +#else +@import flutter_qiblah; +#endif + +#if __has_include() +#import +#else +@import geocoding_ios; +#endif + +#if __has_include() +#import +#else +@import geolocator_apple; +#endif + +#if __has_include() +#import +#else +@import just_audio; +#endif + +#if __has_include() +#import +#else +@import package_info_plus; +#endif + +#if __has_include() +#import +#else +@import sqflite_darwin; +#endif + +#if __has_include() +#import +#else +@import url_launcher_ios; +#endif + +@implementation GeneratedPluginRegistrant + ++ (void)registerWithRegistry:(NSObject*)registry { + [AudioServicePlugin registerWithRegistrar:[registry registrarForPlugin:@"AudioServicePlugin"]]; + [AudioSessionPlugin registerWithRegistrar:[registry registrarForPlugin:@"AudioSessionPlugin"]]; + [FlutterCompassPlugin registerWithRegistrar:[registry registrarForPlugin:@"FlutterCompassPlugin"]]; + [FlutterCompassPlugin registerWithRegistrar:[registry registrarForPlugin:@"FlutterCompassPlugin"]]; + [FlutterLocalNotificationsPlugin registerWithRegistrar:[registry registrarForPlugin:@"FlutterLocalNotificationsPlugin"]]; + [FlutterQiblahPlugin registerWithRegistrar:[registry registrarForPlugin:@"FlutterQiblahPlugin"]]; + [GeocodingPlugin registerWithRegistrar:[registry registrarForPlugin:@"GeocodingPlugin"]]; + [GeolocatorPlugin registerWithRegistrar:[registry registrarForPlugin:@"GeolocatorPlugin"]]; + [JustAudioPlugin registerWithRegistrar:[registry registrarForPlugin:@"JustAudioPlugin"]]; + [FPPPackageInfoPlusPlugin registerWithRegistrar:[registry registrarForPlugin:@"FPPPackageInfoPlusPlugin"]]; + [SqflitePlugin registerWithRegistrar:[registry registrarForPlugin:@"SqflitePlugin"]]; + [URLLauncherPlugin registerWithRegistrar:[registry registrarForPlugin:@"URLLauncherPlugin"]]; +} + +@end diff --git a/lib/app/app.dart b/lib/app/app.dart new file mode 100644 index 0000000..09a29aa --- /dev/null +++ b/lib/app/app.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../core/providers/theme_provider.dart'; +import 'router.dart'; +import 'theme/app_theme.dart'; + +/// Root MaterialApp.router wired to GoRouter + ThemeMode from Riverpod. +class App extends ConsumerWidget { + const App({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final themeMode = ref.watch(themeProvider); + + return MaterialApp.router( + title: 'Jamshalat Diary', + debugShowCheckedModeBanner: false, + theme: AppTheme.light, + darkTheme: AppTheme.dark, + themeMode: themeMode, + routerConfig: appRouter, + ); + } +} diff --git a/lib/app/router.dart b/lib/app/router.dart new file mode 100644 index 0000000..44a8d4c --- /dev/null +++ b/lib/app/router.dart @@ -0,0 +1,175 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +import '../core/widgets/bottom_nav_bar.dart'; +import '../features/dashboard/presentation/dashboard_screen.dart'; +import '../features/imsakiyah/presentation/imsakiyah_screen.dart'; +import '../features/checklist/presentation/checklist_screen.dart'; +import '../features/laporan/presentation/laporan_screen.dart'; +import '../features/tools/presentation/tools_screen.dart'; +import '../features/dzikir/presentation/dzikir_screen.dart'; +import '../features/qibla/presentation/qibla_screen.dart'; +import '../features/quran/presentation/quran_screen.dart'; +import '../features/quran/presentation/quran_reading_screen.dart'; +import '../features/quran/presentation/quran_murattal_screen.dart'; +import '../features/quran/presentation/quran_bookmarks_screen.dart'; +import '../features/settings/presentation/settings_screen.dart'; + +/// Navigation key for the shell navigator (bottom-nav screens). +final _rootNavigatorKey = GlobalKey(); +final _shellNavigatorKey = GlobalKey(); + +/// GoRouter configuration per PRD §5.2. +final GoRouter appRouter = GoRouter( + navigatorKey: _rootNavigatorKey, + initialLocation: '/', + routes: [ + // ── Shell route (bottom nav persists) ── + ShellRoute( + navigatorKey: _shellNavigatorKey, + builder: (context, state, child) => _ScaffoldWithNav(child: child), + routes: [ + GoRoute( + path: '/', + pageBuilder: (context, state) => const NoTransitionPage( + child: DashboardScreen(), + ), + routes: [ + GoRoute( + path: 'qibla', + parentNavigatorKey: _rootNavigatorKey, + builder: (context, state) => const QiblaScreen(), + ), + ], + ), + GoRoute( + path: '/imsakiyah', + pageBuilder: (context, state) => const NoTransitionPage( + child: ImsakiyahScreen(), + ), + ), + GoRoute( + path: '/checklist', + pageBuilder: (context, state) => const NoTransitionPage( + child: ChecklistScreen(), + ), + ), + GoRoute( + path: '/laporan', + pageBuilder: (context, state) => const NoTransitionPage( + child: LaporanScreen(), + ), + ), + GoRoute( + path: '/tools', + pageBuilder: (context, state) => const NoTransitionPage( + child: ToolsScreen(), + ), + routes: [ + GoRoute( + path: 'dzikir', + parentNavigatorKey: _rootNavigatorKey, + builder: (context, state) => const DzikirScreen(), + ), + GoRoute( + path: 'quran', + parentNavigatorKey: _rootNavigatorKey, + builder: (context, state) => const QuranScreen(), + routes: [ + GoRoute( + path: 'bookmarks', + parentNavigatorKey: _rootNavigatorKey, + builder: (context, state) => const QuranBookmarksScreen(), + ), + GoRoute( + path: ':surahId', + parentNavigatorKey: _rootNavigatorKey, + builder: (context, state) { + final surahId = state.pathParameters['surahId']!; + final startVerse = int.tryParse(state.uri.queryParameters['startVerse'] ?? ''); + return QuranReadingScreen(surahId: surahId, initialVerse: startVerse); + }, + 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, + ); + }, + ), + ], + ), + ], + ), + GoRoute( + path: 'qibla', + parentNavigatorKey: _rootNavigatorKey, + builder: (context, state) => const QiblaScreen(), + ), + ], + ), + ], + ), + // ── Settings (pushed, no bottom nav) ── + GoRoute( + path: '/settings', + parentNavigatorKey: _rootNavigatorKey, + builder: (context, state) => const SettingsScreen(), + ), + ], +); + +/// Scaffold wrapper that provides the persistent bottom nav bar. +class _ScaffoldWithNav extends StatelessWidget { + const _ScaffoldWithNav({required this.child}); + + final Widget child; + + /// 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; + } + + 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; + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: child, + bottomNavigationBar: AppBottomNavBar( + currentIndex: _currentIndex(context), + onTap: (i) => _onTap(context, i), + ), + ); + } +} diff --git a/lib/app/theme/.gitkeep b/lib/app/theme/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/lib/app/theme/app_colors.dart b/lib/app/theme/app_colors.dart new file mode 100644 index 0000000..15be735 --- /dev/null +++ b/lib/app/theme/app_colors.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; + +/// All color tokens from PRD §3.1 — light and dark values as static constants. +class AppColors { + AppColors._(); + + // ── Primary ── + static const Color primary = Color(0xFF70DF20); + static const Color onPrimary = Color(0xFF0A1A00); + + // ── Background ── + static const Color backgroundLight = Color(0xFFF7F8F6); + static const Color backgroundDark = Color(0xFF182111); + + // ── Surface ── + static const Color surfaceLight = Color(0xFFFFFFFF); + static const Color surfaceDark = Color(0xFF1E2A14); + + // ── Sage (secondary text / section labels) ── + static const Color sage = Color(0xFF728764); + + // ── Cream (dividers, borders — light mode only) ── + static const Color cream = Color(0xFFF2F4F0); + + // ── Text ── + static const Color textPrimaryLight = Color(0xFF1A2A0A); + static const Color textPrimaryDark = Color(0xFFF2F4F0); + static const Color textSecondaryLight = Color(0xFF64748B); + static const Color textSecondaryDark = Color(0xFF94A3B8); + + // ── Semantic ── + static const Color errorLight = Color(0xFFEF4444); + static const Color errorDark = Color(0xFFF87171); + static const Color successLight = Color(0xFF22C55E); + static const Color successDark = Color(0xFF4ADE80); + + // ── Convenience helpers for theme building ── + + static ColorScheme get lightColorScheme => ColorScheme.light( + primary: primary, + onPrimary: onPrimary, + surface: surfaceLight, + onSurface: textPrimaryLight, + error: errorLight, + onError: Colors.white, + secondary: sage, + onSecondary: Colors.white, + ); + + static ColorScheme get darkColorScheme => ColorScheme.dark( + primary: primary, + onPrimary: onPrimary, + surface: surfaceDark, + onSurface: textPrimaryDark, + error: errorDark, + onError: Colors.black, + secondary: sage, + onSecondary: Colors.white, + ); +} diff --git a/lib/app/theme/app_text_styles.dart b/lib/app/theme/app_text_styles.dart new file mode 100644 index 0000000..dd66573 --- /dev/null +++ b/lib/app/theme/app_text_styles.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; + +/// Typography definitions from PRD §3.2. +/// Plus Jakarta Sans (bundled) for UI text, Amiri (bundled) for Arabic content. +class AppTextStyles { + AppTextStyles._(); + + static const String _fontFamily = 'PlusJakartaSans'; + + /// Builds the full TextTheme for the app using bundled Plus Jakarta Sans. + static const TextTheme textTheme = TextTheme( + displayLarge: TextStyle( + fontFamily: _fontFamily, + fontSize: 32, + fontWeight: FontWeight.w800, + ), + headlineMedium: TextStyle( + fontFamily: _fontFamily, + fontSize: 24, + fontWeight: FontWeight.w700, + ), + titleLarge: TextStyle( + fontFamily: _fontFamily, + fontSize: 20, + fontWeight: FontWeight.w700, + ), + titleMedium: TextStyle( + fontFamily: _fontFamily, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + bodyLarge: TextStyle( + fontFamily: _fontFamily, + fontSize: 16, + fontWeight: FontWeight.w400, + ), + bodyMedium: TextStyle( + fontFamily: _fontFamily, + fontSize: 14, + fontWeight: FontWeight.w400, + ), + bodySmall: TextStyle( + fontFamily: _fontFamily, + fontSize: 12, + fontWeight: FontWeight.w400, + ), + labelSmall: TextStyle( + fontFamily: _fontFamily, + fontSize: 10, + fontWeight: FontWeight.w700, + letterSpacing: 1.5, + ), + ); + + // ── Arabic text styles (Amiri — bundled font) ── + + static const TextStyle arabicBody = TextStyle( + fontFamily: 'Amiri', + fontSize: 24, + fontWeight: FontWeight.w400, + height: 2.0, + ); + + static const TextStyle arabicLarge = TextStyle( + fontFamily: 'Amiri', + fontSize: 28, + fontWeight: FontWeight.w700, + height: 2.2, + ); +} diff --git a/lib/app/theme/app_theme.dart b/lib/app/theme/app_theme.dart new file mode 100644 index 0000000..3751ecd --- /dev/null +++ b/lib/app/theme/app_theme.dart @@ -0,0 +1,90 @@ +import 'package:flutter/material.dart'; +import 'app_colors.dart'; +import 'app_text_styles.dart'; + +/// ThemeData for light and dark modes, Material 3 enabled. +class AppTheme { + AppTheme._(); + + static ThemeData get light => ThemeData( + useMaterial3: true, + brightness: Brightness.light, + colorScheme: AppColors.lightColorScheme, + scaffoldBackgroundColor: AppColors.backgroundLight, + textTheme: AppTextStyles.textTheme.apply( + bodyColor: AppColors.textPrimaryLight, + displayColor: AppColors.textPrimaryLight, + ), + appBarTheme: const AppBarTheme( + backgroundColor: Colors.transparent, + elevation: 0, + scrolledUnderElevation: 0, + centerTitle: true, + iconTheme: IconThemeData(color: AppColors.textPrimaryLight), + titleTextStyle: TextStyle( + color: AppColors.textPrimaryLight, + fontSize: 20, + fontWeight: FontWeight.w700, + ), + ), + bottomNavigationBarTheme: const BottomNavigationBarThemeData( + backgroundColor: AppColors.surfaceLight, + selectedItemColor: AppColors.primary, + unselectedItemColor: AppColors.textSecondaryLight, + type: BottomNavigationBarType.fixed, + elevation: 0, + ), + cardTheme: CardThemeData( + color: AppColors.surfaceLight, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + side: BorderSide( + color: AppColors.cream, + ), + ), + ), + dividerColor: AppColors.cream, + ); + + static ThemeData get dark => ThemeData( + useMaterial3: true, + brightness: Brightness.dark, + colorScheme: AppColors.darkColorScheme, + scaffoldBackgroundColor: AppColors.backgroundDark, + textTheme: AppTextStyles.textTheme.apply( + bodyColor: AppColors.textPrimaryDark, + displayColor: AppColors.textPrimaryDark, + ), + appBarTheme: const AppBarTheme( + backgroundColor: Colors.transparent, + elevation: 0, + scrolledUnderElevation: 0, + centerTitle: true, + iconTheme: IconThemeData(color: AppColors.textPrimaryDark), + titleTextStyle: TextStyle( + color: AppColors.textPrimaryDark, + fontSize: 20, + fontWeight: FontWeight.w700, + ), + ), + bottomNavigationBarTheme: const BottomNavigationBarThemeData( + backgroundColor: AppColors.surfaceDark, + selectedItemColor: AppColors.primary, + unselectedItemColor: AppColors.textSecondaryDark, + type: BottomNavigationBarType.fixed, + elevation: 0, + ), + cardTheme: CardThemeData( + color: AppColors.surfaceDark, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + side: BorderSide( + color: AppColors.primary.withValues(alpha: 0.1), + ), + ), + ), + dividerColor: AppColors.surfaceDark, + ); +} diff --git a/lib/app/theme/theme.dart b/lib/app/theme/theme.dart new file mode 100644 index 0000000..fdeb8d2 --- /dev/null +++ b/lib/app/theme/theme.dart @@ -0,0 +1,4 @@ +// Barrel file for theme exports. +export 'app_colors.dart'; +export 'app_text_styles.dart'; +export 'app_theme.dart'; diff --git a/lib/core/providers/.gitkeep b/lib/core/providers/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/lib/core/providers/placeholder.dart b/lib/core/providers/placeholder.dart new file mode 100644 index 0000000..1039d4c --- /dev/null +++ b/lib/core/providers/placeholder.dart @@ -0,0 +1 @@ +// TODO: implement diff --git a/lib/core/providers/theme_provider.dart b/lib/core/providers/theme_provider.dart new file mode 100644 index 0000000..d6f5683 --- /dev/null +++ b/lib/core/providers/theme_provider.dart @@ -0,0 +1,12 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:hive_flutter/hive_flutter.dart'; +import '../../data/local/hive_boxes.dart'; +import '../../data/local/models/app_settings.dart'; + +/// Theme mode state provider. +final themeProvider = StateProvider((ref) { + final box = Hive.box(HiveBoxes.settings); + final settings = box.get('default'); + return settings?.themeModeIndex == 1 ? ThemeMode.light : ThemeMode.dark; +}); diff --git a/lib/core/providers/tilawah_tracking_provider.dart b/lib/core/providers/tilawah_tracking_provider.dart new file mode 100644 index 0000000..93a3f86 --- /dev/null +++ b/lib/core/providers/tilawah_tracking_provider.dart @@ -0,0 +1,54 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +/// Models an active reading session for Tilawah +class TilawahSession { + final int startSurahId; + final String startSurahName; + final int startVerseId; + + const TilawahSession({ + required this.startSurahId, + required this.startSurahName, + required this.startVerseId, + }); + + TilawahSession copyWith({ + int? startSurahId, + String? startSurahName, + int? startVerseId, + }) { + return TilawahSession( + startSurahId: startSurahId ?? this.startSurahId, + startSurahName: startSurahName ?? this.startSurahName, + startVerseId: startVerseId ?? this.startVerseId, + ); + } +} + +/// A state notifier to manage the global start state of a reading session. +/// If state is null, no active tracking is occurring. +class TilawahTrackingNotifier extends StateNotifier { + TilawahTrackingNotifier() : super(null); + + /// Start a new tracking session + void startTracking({ + required int surahId, + required String surahName, + required int verseId + }) { + state = TilawahSession( + startSurahId: surahId, + startSurahName: surahName, + startVerseId: verseId, + ); + } + + /// Stop tracking (after recording) + void stopTracking() { + state = null; + } +} + +final tilawahTrackingProvider = StateNotifierProvider((ref) { + return TilawahTrackingNotifier(); +}); diff --git a/lib/core/utils/.gitkeep b/lib/core/utils/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/lib/core/utils/placeholder.dart b/lib/core/utils/placeholder.dart new file mode 100644 index 0000000..1039d4c --- /dev/null +++ b/lib/core/utils/placeholder.dart @@ -0,0 +1 @@ +// TODO: implement diff --git a/lib/core/widgets/.gitkeep b/lib/core/widgets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/lib/core/widgets/bottom_nav_bar.dart b/lib/core/widgets/bottom_nav_bar.dart new file mode 100644 index 0000000..caf7581 --- /dev/null +++ b/lib/core/widgets/bottom_nav_bar.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; +import '../../app/theme/app_colors.dart'; + +/// 5-tab bottom navigation bar per PRD §5.1. +/// Uses Material Symbols outlined (inactive) and filled (active). +class AppBottomNavBar extends StatelessWidget { + const AppBottomNavBar({ + super.key, + required this.currentIndex, + required this.onTap, + }); + + final int currentIndex; + final ValueChanged onTap; + + @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, + ), + ), + ), + 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', + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/core/widgets/ios_toggle.dart b/lib/core/widgets/ios_toggle.dart new file mode 100644 index 0000000..1f0a420 --- /dev/null +++ b/lib/core/widgets/ios_toggle.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; +import '../../app/theme/app_colors.dart'; + +/// Custom iOS-style toggle switch (51×31dp) per PRD §6.9. +/// Uses AnimatedContainer + GestureDetector for smooth animation. +class IosToggle extends StatelessWidget { + const IosToggle({ + super.key, + required this.value, + required this.onChanged, + }); + + final bool value; + final ValueChanged onChanged; + + static const double _width = 51.0; + static const double _height = 31.0; + static const double _thumbSize = 27.0; + static const double _thumbPadding = 2.0; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () => onChanged(!value), + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + curve: Curves.easeInOut, + width: _width, + height: _height, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(_height / 2), + color: value ? AppColors.primary : AppColors.cream, + ), + child: AnimatedAlign( + duration: const Duration(milliseconds: 200), + curve: Curves.easeInOut, + alignment: value ? Alignment.centerRight : Alignment.centerLeft, + child: Container( + width: _thumbSize, + height: _thumbSize, + margin: const EdgeInsets.all(_thumbPadding), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.15), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/core/widgets/placeholder.dart b/lib/core/widgets/placeholder.dart new file mode 100644 index 0000000..1039d4c --- /dev/null +++ b/lib/core/widgets/placeholder.dart @@ -0,0 +1 @@ +// TODO: implement diff --git a/lib/core/widgets/prayer_time_card.dart b/lib/core/widgets/prayer_time_card.dart new file mode 100644 index 0000000..1c357c5 --- /dev/null +++ b/lib/core/widgets/prayer_time_card.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; +import '../../app/theme/app_colors.dart'; + +/// Reusable prayer time card widget for the horizontal scroll on Dashboard. +/// Will be fully implemented in Phase 3. +class PrayerTimeCard extends StatelessWidget { + const PrayerTimeCard({ + super.key, + required this.prayerName, + required this.time, + required this.icon, + this.isActive = false, + }); + + final String prayerName; + final String time; + final IconData icon; + final bool isActive; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Container( + width: 112, + padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 12), + decoration: BoxDecoration( + color: isActive + ? AppColors.primary.withValues(alpha: 0.1) + : theme.cardTheme.color, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: isActive + ? AppColors.primary + : AppColors.primary.withValues(alpha: 0.1), + width: isActive ? 2 : 1, + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + icon, + size: 24, + color: isActive ? AppColors.primary : AppColors.sage, + ), + const SizedBox(height: 8), + Text( + prayerName, + style: theme.textTheme.bodySmall?.copyWith( + fontWeight: FontWeight.bold, + color: isActive ? AppColors.primary : null, + ), + ), + const SizedBox(height: 4), + Text( + time, + style: theme.textTheme.bodySmall?.copyWith( + color: isActive + ? AppColors.primary + : theme.textTheme.bodySmall?.color, + ), + ), + ], + ), + ); + } +} diff --git a/lib/core/widgets/progress_bar.dart b/lib/core/widgets/progress_bar.dart new file mode 100644 index 0000000..9d5bf26 --- /dev/null +++ b/lib/core/widgets/progress_bar.dart @@ -0,0 +1,69 @@ +import 'package:flutter/material.dart'; +import '../../app/theme/app_colors.dart'; + +/// Reusable linear progress bar with primary fill. +/// Configurable height, borderRadius, and value (0.0–1.0). +class AppProgressBar extends StatelessWidget { + const AppProgressBar({ + super.key, + required this.value, + this.height = 12.0, + this.borderRadius, + this.backgroundColor, + this.fillColor, + }); + + /// Progress value from 0.0 to 1.0. + final double value; + + /// Height of the bar. Default 12dp. + final double height; + + /// Border radius. Defaults to stadium (full). + final BorderRadius? borderRadius; + + /// Background track color. Defaults to white/10 (dark) or primary/10 (light). + final Color? backgroundColor; + + /// Fill color. Defaults to AppColors.primary. + final Color? fillColor; + + @override + Widget build(BuildContext context) { + final isDark = Theme.of(context).brightness == Brightness.dark; + final trackColor = backgroundColor ?? + (isDark + ? Colors.white.withValues(alpha: 0.1) + : AppColors.primary.withValues(alpha: 0.1)); + final fill = fillColor ?? AppColors.primary; + final radius = borderRadius ?? BorderRadius.circular(height / 2); + + return ClipRRect( + borderRadius: radius, + child: SizedBox( + height: height, + child: Stack( + children: [ + // Track + Container( + decoration: BoxDecoration( + color: trackColor, + borderRadius: radius, + ), + ), + // Fill + FractionallySizedBox( + widthFactor: value.clamp(0.0, 1.0), + child: Container( + decoration: BoxDecoration( + color: fill, + borderRadius: radius, + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/core/widgets/section_header.dart b/lib/core/widgets/section_header.dart new file mode 100644 index 0000000..f7dd219 --- /dev/null +++ b/lib/core/widgets/section_header.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import '../../app/theme/app_colors.dart'; + +/// Reusable uppercase section label (e.g. "NOTIFICATIONS", "DISPLAY"). +/// Uses sage color, tracking-wider, bold weight per PRD §3.2 labelSmall. +class SectionHeader extends StatelessWidget { + const SectionHeader({ + super.key, + required this.title, + this.trailing, + }); + + final String title; + final Widget? trailing; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + title.toUpperCase(), + style: Theme.of(context).textTheme.labelSmall?.copyWith( + color: AppColors.sage, + letterSpacing: 1.5, + ), + ), + if (trailing != null) trailing!, + ], + ), + ); + } +} diff --git a/lib/data/local/adapters/.gitkeep b/lib/data/local/adapters/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/lib/data/local/adapters/placeholder.dart b/lib/data/local/adapters/placeholder.dart new file mode 100644 index 0000000..1039d4c --- /dev/null +++ b/lib/data/local/adapters/placeholder.dart @@ -0,0 +1 @@ +// TODO: implement diff --git a/lib/data/local/hive_boxes.dart b/lib/data/local/hive_boxes.dart new file mode 100644 index 0000000..f960d26 --- /dev/null +++ b/lib/data/local/hive_boxes.dart @@ -0,0 +1,119 @@ +import 'package:flutter/foundation.dart'; +import 'package:hive_flutter/hive_flutter.dart'; +import 'models/app_settings.dart'; +import 'models/checklist_item.dart'; +import 'models/daily_worship_log.dart'; +import 'models/dzikir_counter.dart'; +import 'models/quran_bookmark.dart'; +import 'models/cached_prayer_times.dart'; +import 'models/shalat_log.dart'; +import 'models/tilawah_log.dart'; +import 'models/dzikir_log.dart'; +import 'models/puasa_log.dart'; + +/// Box name constants for Hive. +class HiveBoxes { + HiveBoxes._(); + + static const String settings = 'settings'; + static const String checklistItems = 'checklist_items'; + static const String worshipLogs = 'worship_logs'; + static const String dzikirCounters = 'dzikir_counters'; + static const String bookmarks = 'bookmarks'; + static const String cachedPrayerTimes = 'cached_prayer_times'; +} + +/// Initialize Hive and open all boxes. +Future initHive() async { + await Hive.initFlutter(); + + // Register adapters + Hive.registerAdapter(AppSettingsAdapter()); + Hive.registerAdapter(ChecklistItemAdapter()); + Hive.registerAdapter(DailyWorshipLogAdapter()); + Hive.registerAdapter(DzikirCounterAdapter()); + Hive.registerAdapter(QuranBookmarkAdapter()); + Hive.registerAdapter(CachedPrayerTimesAdapter()); + Hive.registerAdapter(ShalatLogAdapter()); + Hive.registerAdapter(TilawahLogAdapter()); + Hive.registerAdapter(DzikirLogAdapter()); + Hive.registerAdapter(PuasaLogAdapter()); + + // Open boxes + try { + await Hive.openBox(HiveBoxes.settings); + } catch (e) { + debugPrint('Settings box corrupted, resetting: $e'); + if (Hive.isBoxOpen(HiveBoxes.settings)) { + await Hive.box(HiveBoxes.settings).close(); + } + await Hive.deleteBoxFromDisk(HiveBoxes.settings); + await Hive.openBox(HiveBoxes.settings); + } + + await Hive.openBox(HiveBoxes.checklistItems); + final worshipBox = await Hive.openBox(HiveBoxes.worshipLogs); + await Hive.openBox(HiveBoxes.dzikirCounters); + await Hive.openBox(HiveBoxes.bookmarks); + await Hive.openBox(HiveBoxes.cachedPrayerTimes); + + // MIGRATION: Delete legacy logs that crash due to type casts (Map vs Map) + final keysToDelete = []; + for (final key in worshipBox.keys) { + try { + final log = worshipBox.get(key); + if (log != null) { + log.shalatLogs.values.toList(); // Force evaluation + } + } catch (_) { + keysToDelete.add(key); + } + } + + if (keysToDelete.isNotEmpty) { + await worshipBox.deleteAll(keysToDelete); + debugPrint('Deleted ${keysToDelete.length} legacy worship logs.'); + } +} + +/// Seeds default settings and checklist items on first launch. +Future seedDefaults() async { + // Seed AppSettings + final settingsBox = Hive.box(HiveBoxes.settings); + if (settingsBox.isEmpty) { + await settingsBox.put('default', AppSettings()); + } + + // Seed default checklist items + final checklistBox = Hive.box(HiveBoxes.checklistItems); + if (checklistBox.isEmpty) { + final defaults = [ + ChecklistItem( + id: 'fajr', title: 'Sholat Fajr', category: 'sholat_fardhu', sortOrder: 0), + ChecklistItem( + id: 'dhuhr', title: 'Sholat Dhuhr', category: 'sholat_fardhu', sortOrder: 1), + ChecklistItem( + id: 'asr', title: 'Sholat Asr', category: 'sholat_fardhu', sortOrder: 2), + ChecklistItem( + id: 'maghrib', title: 'Sholat Maghrib', category: 'sholat_fardhu', sortOrder: 3), + ChecklistItem( + id: 'isha', title: 'Sholat Isha', category: 'sholat_fardhu', sortOrder: 4), + ChecklistItem( + id: 'tilawah', title: 'Tilawah Quran', category: 'tilawah', + subtitle: '1 Juz', sortOrder: 5), + ChecklistItem( + id: 'dzikir_pagi', title: 'Dzikir Pagi', category: 'dzikir', + subtitle: '1 session', sortOrder: 6), + ChecklistItem( + id: 'dzikir_petang', title: 'Dzikir Petang', category: 'dzikir', + subtitle: '1 session', sortOrder: 7), + ChecklistItem( + id: 'rawatib', title: 'Sholat Sunnah Rawatib', category: 'sunnah', sortOrder: 8), + ChecklistItem( + id: 'shodaqoh', title: 'Shodaqoh', category: 'charity', sortOrder: 9), + ]; + for (final item in defaults) { + await checklistBox.put(item.id, item); + } + } +} diff --git a/lib/data/local/models/app_settings.dart b/lib/data/local/models/app_settings.dart new file mode 100644 index 0000000..ccc5798 --- /dev/null +++ b/lib/data/local/models/app_settings.dart @@ -0,0 +1,101 @@ +import 'package:hive_flutter/hive_flutter.dart'; + +part 'app_settings.g.dart'; + +/// User settings stored in Hive. +@HiveType(typeId: 0) +class AppSettings extends HiveObject { + @HiveField(0) + String userName; + + @HiveField(1) + String userEmail; + + @HiveField(2) + int themeModeIndex; // 0=system, 1=light, 2=dark + + @HiveField(3) + double arabicFontSize; + + @HiveField(4) + String uiLanguage; // 'id' or 'en' + + @HiveField(5) + Map adhanEnabled; + + @HiveField(6) + Map iqamahOffset; // minutes + + @HiveField(7) + String? checklistReminderTime; // HH:mm format + + @HiveField(8) + double? lastLat; + + @HiveField(9) + double? lastLng; + + @HiveField(10) + String? lastCityName; + + @HiveField(11) + int rawatibLevel; // 0 = Off, 1 = Muakkad Only, 2 = Full + + @HiveField(12) + int tilawahTargetValue; + + @HiveField(13) + String tilawahTargetUnit; // 'Juz', 'Halaman', 'Ayat' + + @HiveField(14) + bool tilawahAutoSync; + + @HiveField(15) + bool trackDzikir; + + @HiveField(16) + bool trackPuasa; + + @HiveField(17) + bool showLatin; + + @HiveField(18) + bool showTerjemahan; + + AppSettings({ + this.userName = 'User', + this.userEmail = '', + this.themeModeIndex = 0, + this.arabicFontSize = 24.0, + this.uiLanguage = 'id', + Map? adhanEnabled, + Map? iqamahOffset, + this.checklistReminderTime = '09:00', + this.lastLat, + this.lastLng, + this.lastCityName, + this.rawatibLevel = 1, // Default to Muakkad + this.tilawahTargetValue = 1, + this.tilawahTargetUnit = 'Juz', + this.tilawahAutoSync = false, + this.trackDzikir = true, + this.trackPuasa = false, + this.showLatin = true, + this.showTerjemahan = true, + }) : adhanEnabled = adhanEnabled ?? + { + 'fajr': true, + 'dhuhr': true, + 'asr': true, + 'maghrib': true, + 'isha': true, + }, + iqamahOffset = iqamahOffset ?? + { + 'fajr': 15, + 'dhuhr': 10, + 'asr': 10, + 'maghrib': 5, + 'isha': 10, + }; +} diff --git a/lib/data/local/models/app_settings.g.dart b/lib/data/local/models/app_settings.g.dart new file mode 100644 index 0000000..b077c53 --- /dev/null +++ b/lib/data/local/models/app_settings.g.dart @@ -0,0 +1,95 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'app_settings.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class AppSettingsAdapter extends TypeAdapter { + @override + final int typeId = 0; + + @override + AppSettings read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return AppSettings( + userName: fields.containsKey(0) ? fields[0] as String? ?? '' : '', + userEmail: fields.containsKey(1) ? fields[1] as String? ?? '' : '', + themeModeIndex: fields.containsKey(2) ? fields[2] as int? ?? 0 : 0, + arabicFontSize: fields.containsKey(3) ? fields[3] as double? ?? 26.0 : 26.0, + uiLanguage: fields.containsKey(4) ? fields[4] as String? ?? 'id' : 'id', + adhanEnabled: fields.containsKey(5) ? (fields[5] as Map?)?.cast() : null, + iqamahOffset: fields.containsKey(6) ? (fields[6] as Map?)?.cast() : null, + checklistReminderTime: fields.containsKey(7) ? fields[7] as String? : null, + lastLat: fields.containsKey(8) ? fields[8] as double? : null, + lastLng: fields.containsKey(9) ? fields[9] as double? : null, + lastCityName: fields.containsKey(10) ? fields[10] as String? : null, + rawatibLevel: fields.containsKey(11) ? fields[11] as int? ?? 1 : 1, + tilawahTargetValue: fields.containsKey(12) ? fields[12] as int? ?? 1 : 1, + tilawahTargetUnit: fields.containsKey(13) ? fields[13] as String? ?? 'Juz' : 'Juz', + tilawahAutoSync: fields.containsKey(14) ? fields[14] as bool? ?? false : false, + trackDzikir: fields.containsKey(15) ? fields[15] as bool? ?? true : true, + 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, + ); + } + + @override + void write(BinaryWriter writer, AppSettings obj) { + writer + ..writeByte(19) + ..writeByte(0) + ..write(obj.userName) + ..writeByte(1) + ..write(obj.userEmail) + ..writeByte(2) + ..write(obj.themeModeIndex) + ..writeByte(3) + ..write(obj.arabicFontSize) + ..writeByte(4) + ..write(obj.uiLanguage) + ..writeByte(5) + ..write(obj.adhanEnabled) + ..writeByte(6) + ..write(obj.iqamahOffset) + ..writeByte(7) + ..write(obj.checklistReminderTime) + ..writeByte(8) + ..write(obj.lastLat) + ..writeByte(9) + ..write(obj.lastLng) + ..writeByte(10) + ..write(obj.lastCityName) + ..writeByte(11) + ..write(obj.rawatibLevel) + ..writeByte(12) + ..write(obj.tilawahTargetValue) + ..writeByte(13) + ..write(obj.tilawahTargetUnit) + ..writeByte(14) + ..write(obj.tilawahAutoSync) + ..writeByte(15) + ..write(obj.trackDzikir) + ..writeByte(16) + ..write(obj.trackPuasa) + ..writeByte(17) + ..write(obj.showLatin) + ..writeByte(18) + ..write(obj.showTerjemahan); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is AppSettingsAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/lib/data/local/models/cached_prayer_times.dart b/lib/data/local/models/cached_prayer_times.dart new file mode 100644 index 0000000..cc28dbb --- /dev/null +++ b/lib/data/local/models/cached_prayer_times.dart @@ -0,0 +1,50 @@ +import 'package:hive_flutter/hive_flutter.dart'; + +part 'cached_prayer_times.g.dart'; + +/// Cached prayer times for a specific location + date. +@HiveType(typeId: 5) +class CachedPrayerTimes extends HiveObject { + @HiveField(0) + String key; // 'lat_lng_yyyy-MM-dd' + + @HiveField(1) + double lat; + + @HiveField(2) + double lng; + + @HiveField(3) + String date; + + @HiveField(4) + DateTime fajr; + + @HiveField(5) + DateTime sunrise; + + @HiveField(6) + DateTime dhuhr; + + @HiveField(7) + DateTime asr; + + @HiveField(8) + DateTime maghrib; + + @HiveField(9) + DateTime isha; + + CachedPrayerTimes({ + required this.key, + required this.lat, + required this.lng, + required this.date, + required this.fajr, + required this.sunrise, + required this.dhuhr, + required this.asr, + required this.maghrib, + required this.isha, + }); +} diff --git a/lib/data/local/models/cached_prayer_times.g.dart b/lib/data/local/models/cached_prayer_times.g.dart new file mode 100644 index 0000000..3a9f0b0 --- /dev/null +++ b/lib/data/local/models/cached_prayer_times.g.dart @@ -0,0 +1,68 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'cached_prayer_times.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class CachedPrayerTimesAdapter extends TypeAdapter { + @override + final int typeId = 5; + + @override + CachedPrayerTimes read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return CachedPrayerTimes( + key: fields[0] as String, + lat: fields[1] as double, + lng: fields[2] as double, + date: fields[3] as String, + fajr: fields[4] as DateTime, + sunrise: fields[5] as DateTime, + dhuhr: fields[6] as DateTime, + asr: fields[7] as DateTime, + maghrib: fields[8] as DateTime, + isha: fields[9] as DateTime, + ); + } + + @override + void write(BinaryWriter writer, CachedPrayerTimes obj) { + writer + ..writeByte(10) + ..writeByte(0) + ..write(obj.key) + ..writeByte(1) + ..write(obj.lat) + ..writeByte(2) + ..write(obj.lng) + ..writeByte(3) + ..write(obj.date) + ..writeByte(4) + ..write(obj.fajr) + ..writeByte(5) + ..write(obj.sunrise) + ..writeByte(6) + ..write(obj.dhuhr) + ..writeByte(7) + ..write(obj.asr) + ..writeByte(8) + ..write(obj.maghrib) + ..writeByte(9) + ..write(obj.isha); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is CachedPrayerTimesAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/lib/data/local/models/checklist_item.dart b/lib/data/local/models/checklist_item.dart new file mode 100644 index 0000000..a5bee18 --- /dev/null +++ b/lib/data/local/models/checklist_item.dart @@ -0,0 +1,34 @@ +import 'package:hive_flutter/hive_flutter.dart'; + +part 'checklist_item.g.dart'; + +/// A single checklist item definition (template, not daily state). +@HiveType(typeId: 1) +class ChecklistItem extends HiveObject { + @HiveField(0) + String id; + + @HiveField(1) + String title; + + @HiveField(2) + String category; + + @HiveField(3) + String? subtitle; + + @HiveField(4) + int sortOrder; + + @HiveField(5) + bool isCustom; + + ChecklistItem({ + required this.id, + required this.title, + required this.category, + this.subtitle, + required this.sortOrder, + this.isCustom = false, + }); +} diff --git a/lib/data/local/models/checklist_item.g.dart b/lib/data/local/models/checklist_item.g.dart new file mode 100644 index 0000000..b690531 --- /dev/null +++ b/lib/data/local/models/checklist_item.g.dart @@ -0,0 +1,56 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'checklist_item.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class ChecklistItemAdapter extends TypeAdapter { + @override + final int typeId = 1; + + @override + ChecklistItem read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return ChecklistItem( + id: fields[0] as String, + title: fields[1] as String, + category: fields[2] as String, + subtitle: fields[3] as String?, + sortOrder: fields[4] as int, + isCustom: fields[5] as bool, + ); + } + + @override + void write(BinaryWriter writer, ChecklistItem obj) { + writer + ..writeByte(6) + ..writeByte(0) + ..write(obj.id) + ..writeByte(1) + ..write(obj.title) + ..writeByte(2) + ..write(obj.category) + ..writeByte(3) + ..write(obj.subtitle) + ..writeByte(4) + ..write(obj.sortOrder) + ..writeByte(5) + ..write(obj.isCustom); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ChecklistItemAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/lib/data/local/models/daily_worship_log.dart b/lib/data/local/models/daily_worship_log.dart new file mode 100644 index 0000000..e36dd90 --- /dev/null +++ b/lib/data/local/models/daily_worship_log.dart @@ -0,0 +1,86 @@ +import 'package:hive_flutter/hive_flutter.dart'; +import 'shalat_log.dart'; +import 'tilawah_log.dart'; +import 'dzikir_log.dart'; +import 'puasa_log.dart'; + +part 'daily_worship_log.g.dart'; + +/// Daily worship completion log, keyed by date string 'yyyy-MM-dd'. +@HiveType(typeId: 2) +class DailyWorshipLog extends HiveObject { + @HiveField(0) + String date; + + @HiveField(1) + Map shalatLogs; // e.g., 'subuh' -> ShalatLog + + @HiveField(5) + TilawahLog? tilawahLog; + + @HiveField(6) + DzikirLog? dzikirLog; + + @HiveField(7) + PuasaLog? puasaLog; + + @HiveField(2) + int totalItems; + + @HiveField(3) + int completedCount; + + @HiveField(4) + double completionPercent; + + DailyWorshipLog({ + required this.date, + Map? shalatLogs, + this.tilawahLog, + this.dzikirLog, + this.puasaLog, + this.totalItems = 0, + this.completedCount = 0, + this.completionPercent = 0.0, + }) : shalatLogs = shalatLogs ?? {}; + + /// Dynamically calculates the "Poin Ibadah" for this day. + int get totalPoints { + int points = 0; + + // 1. Shalat Fardhu + for (final sLog in shalatLogs.values) { + if (sLog.completed) { + if (sLog.location == 'Masjid') { + points += 25; + } else { + points += 10; + } + } + if (sLog.qabliyah == true) points += 5; + if (sLog.badiyah == true) points += 5; + } + + // 2. Tilawah + if (tilawahLog != null) { + // 1 point per Ayat read + points += tilawahLog!.rawAyatRead; + + // Bonus 20 points for completing daily target + if (tilawahLog!.isCompleted) { + points += 20; + } + } + + // 3. Dzikir & Puasa + if (dzikirLog != null) { + if (dzikirLog!.pagi) points += 10; + if (dzikirLog!.petang) points += 10; + } + if (puasaLog != null && puasaLog!.completed) { + points += 30; + } + + return points; + } +} diff --git a/lib/data/local/models/daily_worship_log.g.dart b/lib/data/local/models/daily_worship_log.g.dart new file mode 100644 index 0000000..fd73916 --- /dev/null +++ b/lib/data/local/models/daily_worship_log.g.dart @@ -0,0 +1,70 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'daily_worship_log.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class DailyWorshipLogAdapter extends TypeAdapter { + @override + final int typeId = 2; + + @override + DailyWorshipLog read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + Map? parsedShalatLogs; + try { + parsedShalatLogs = (fields[1] as Map?)?.cast(); + } catch (_) { + // If casting fails (e.g. it was the old Map), ignore it. + parsedShalatLogs = {}; + } + + return DailyWorshipLog( + date: fields[0] as String, + shalatLogs: parsedShalatLogs, + tilawahLog: fields[5] as TilawahLog?, + dzikirLog: fields[6] as DzikirLog?, + puasaLog: fields[7] as PuasaLog?, + totalItems: fields[2] as int, + completedCount: fields[3] as int, + completionPercent: fields[4] as double, + ); + } + + @override + void write(BinaryWriter writer, DailyWorshipLog obj) { + writer + ..writeByte(8) + ..writeByte(0) + ..write(obj.date) + ..writeByte(1) + ..write(obj.shalatLogs) + ..writeByte(5) + ..write(obj.tilawahLog) + ..writeByte(6) + ..write(obj.dzikirLog) + ..writeByte(7) + ..write(obj.puasaLog) + ..writeByte(2) + ..write(obj.totalItems) + ..writeByte(3) + ..write(obj.completedCount) + ..writeByte(4) + ..write(obj.completionPercent); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is DailyWorshipLogAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/lib/data/local/models/dzikir_counter.dart b/lib/data/local/models/dzikir_counter.dart new file mode 100644 index 0000000..3b0fdc7 --- /dev/null +++ b/lib/data/local/models/dzikir_counter.dart @@ -0,0 +1,26 @@ +import 'package:hive_flutter/hive_flutter.dart'; + +part 'dzikir_counter.g.dart'; + +/// Counter for a single dzikir item on a specific date. +@HiveType(typeId: 3) +class DzikirCounter extends HiveObject { + @HiveField(0) + String dzikirId; + + @HiveField(1) + String date; + + @HiveField(2) + int count; + + @HiveField(3) + int target; + + DzikirCounter({ + required this.dzikirId, + required this.date, + this.count = 0, + required this.target, + }); +} diff --git a/lib/data/local/models/dzikir_counter.g.dart b/lib/data/local/models/dzikir_counter.g.dart new file mode 100644 index 0000000..6d927f5 --- /dev/null +++ b/lib/data/local/models/dzikir_counter.g.dart @@ -0,0 +1,50 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'dzikir_counter.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class DzikirCounterAdapter extends TypeAdapter { + @override + final int typeId = 3; + + @override + DzikirCounter read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return DzikirCounter( + dzikirId: fields[0] as String, + date: fields[1] as String, + count: fields[2] as int, + target: fields[3] as int, + ); + } + + @override + void write(BinaryWriter writer, DzikirCounter obj) { + writer + ..writeByte(4) + ..writeByte(0) + ..write(obj.dzikirId) + ..writeByte(1) + ..write(obj.date) + ..writeByte(2) + ..write(obj.count) + ..writeByte(3) + ..write(obj.target); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is DzikirCounterAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/lib/data/local/models/dzikir_log.dart b/lib/data/local/models/dzikir_log.dart new file mode 100644 index 0000000..fcdfc04 --- /dev/null +++ b/lib/data/local/models/dzikir_log.dart @@ -0,0 +1,17 @@ +import 'package:hive/hive.dart'; + +part 'dzikir_log.g.dart'; + +@HiveType(typeId: 9) +class DzikirLog { + @HiveField(0) + bool pagi; + + @HiveField(1) + bool petang; + + DzikirLog({ + this.pagi = false, + this.petang = false, + }); +} diff --git a/lib/data/local/models/dzikir_log.g.dart b/lib/data/local/models/dzikir_log.g.dart new file mode 100644 index 0000000..b303676 --- /dev/null +++ b/lib/data/local/models/dzikir_log.g.dart @@ -0,0 +1,44 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'dzikir_log.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class DzikirLogAdapter extends TypeAdapter { + @override + final int typeId = 9; + + @override + DzikirLog read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return DzikirLog( + pagi: fields[0] as bool, + petang: fields[1] as bool, + ); + } + + @override + void write(BinaryWriter writer, DzikirLog obj) { + writer + ..writeByte(2) + ..writeByte(0) + ..write(obj.pagi) + ..writeByte(1) + ..write(obj.petang); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is DzikirLogAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/lib/data/local/models/puasa_log.dart b/lib/data/local/models/puasa_log.dart new file mode 100644 index 0000000..a8830a2 --- /dev/null +++ b/lib/data/local/models/puasa_log.dart @@ -0,0 +1,17 @@ +import 'package:hive/hive.dart'; + +part 'puasa_log.g.dart'; + +@HiveType(typeId: 10) +class PuasaLog { + @HiveField(0) + String? jenisPuasa; // 'Senin', 'Kamis', 'Ayyamul Bidh', 'Daud', etc. + + @HiveField(1) + bool completed; + + PuasaLog({ + this.jenisPuasa, + this.completed = false, + }); +} diff --git a/lib/data/local/models/puasa_log.g.dart b/lib/data/local/models/puasa_log.g.dart new file mode 100644 index 0000000..8bcd571 --- /dev/null +++ b/lib/data/local/models/puasa_log.g.dart @@ -0,0 +1,44 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'puasa_log.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class PuasaLogAdapter extends TypeAdapter { + @override + final int typeId = 10; + + @override + PuasaLog read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return PuasaLog( + jenisPuasa: fields[0] as String?, + completed: fields[1] as bool, + ); + } + + @override + void write(BinaryWriter writer, PuasaLog obj) { + writer + ..writeByte(2) + ..writeByte(0) + ..write(obj.jenisPuasa) + ..writeByte(1) + ..write(obj.completed); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is PuasaLogAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/lib/data/local/models/quran_bookmark.dart b/lib/data/local/models/quran_bookmark.dart new file mode 100644 index 0000000..b1bcd9d --- /dev/null +++ b/lib/data/local/models/quran_bookmark.dart @@ -0,0 +1,42 @@ +import 'package:hive_flutter/hive_flutter.dart'; + +part 'quran_bookmark.g.dart'; + +/// A bookmarked Quran verse. +@HiveType(typeId: 4) +class QuranBookmark extends HiveObject { + @HiveField(0) + int surahId; + + @HiveField(1) + int verseId; + + @HiveField(2) + String surahName; + + @HiveField(3) + String verseText; // Arabic snippet + + @HiveField(4) + DateTime savedAt; + + @HiveField(5) + bool isLastRead; + + @HiveField(6) + String? verseLatin; + + @HiveField(7) + String? verseTranslation; + + QuranBookmark({ + required this.surahId, + required this.verseId, + required this.surahName, + required this.verseText, + required this.savedAt, + this.isLastRead = false, + this.verseLatin, + this.verseTranslation, + }); +} diff --git a/lib/data/local/models/quran_bookmark.g.dart b/lib/data/local/models/quran_bookmark.g.dart new file mode 100644 index 0000000..0d9de9e --- /dev/null +++ b/lib/data/local/models/quran_bookmark.g.dart @@ -0,0 +1,62 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'quran_bookmark.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class QuranBookmarkAdapter extends TypeAdapter { + @override + final int typeId = 4; + + @override + QuranBookmark read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return QuranBookmark( + surahId: fields[0] as int, + verseId: fields[1] as int, + surahName: fields[2] as String, + verseText: fields[3] as String, + savedAt: fields[4] as DateTime, + isLastRead: fields[5] as bool? ?? false, + verseLatin: fields[6] as String?, + verseTranslation: fields[7] as String?, + ); + } + + @override + void write(BinaryWriter writer, QuranBookmark obj) { + writer + ..writeByte(8) + ..writeByte(0) + ..write(obj.surahId) + ..writeByte(1) + ..write(obj.verseId) + ..writeByte(2) + ..write(obj.surahName) + ..writeByte(3) + ..write(obj.verseText) + ..writeByte(4) + ..write(obj.savedAt) + ..writeByte(5) + ..write(obj.isLastRead) + ..writeByte(6) + ..write(obj.verseLatin) + ..writeByte(7) + ..write(obj.verseTranslation); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is QuranBookmarkAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/lib/data/local/models/shalat_log.dart b/lib/data/local/models/shalat_log.dart new file mode 100644 index 0000000..8380933 --- /dev/null +++ b/lib/data/local/models/shalat_log.dart @@ -0,0 +1,25 @@ +import 'package:hive/hive.dart'; + +part 'shalat_log.g.dart'; + +@HiveType(typeId: 7) +class ShalatLog { + @HiveField(0) + bool completed; + + @HiveField(1) + String? location; // 'Rumah' or 'Masjid' + + @HiveField(2) + bool? qabliyah; + + @HiveField(3) + bool? badiyah; + + ShalatLog({ + this.completed = false, + this.location, + this.qabliyah, + this.badiyah, + }); +} diff --git a/lib/data/local/models/shalat_log.g.dart b/lib/data/local/models/shalat_log.g.dart new file mode 100644 index 0000000..941e6c2 --- /dev/null +++ b/lib/data/local/models/shalat_log.g.dart @@ -0,0 +1,50 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'shalat_log.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class ShalatLogAdapter extends TypeAdapter { + @override + final int typeId = 7; + + @override + ShalatLog read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return ShalatLog( + completed: fields[0] as bool, + location: fields[1] as String?, + qabliyah: fields[2] as bool?, + badiyah: fields[3] as bool?, + ); + } + + @override + void write(BinaryWriter writer, ShalatLog obj) { + writer + ..writeByte(4) + ..writeByte(0) + ..write(obj.completed) + ..writeByte(1) + ..write(obj.location) + ..writeByte(2) + ..write(obj.qabliyah) + ..writeByte(3) + ..write(obj.badiyah); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ShalatLogAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/lib/data/local/models/tilawah_log.dart b/lib/data/local/models/tilawah_log.dart new file mode 100644 index 0000000..0f5fec5 --- /dev/null +++ b/lib/data/local/models/tilawah_log.dart @@ -0,0 +1,35 @@ +import 'package:hive/hive.dart'; + +part 'tilawah_log.g.dart'; + +@HiveType(typeId: 8) +class TilawahLog { + @HiveField(0) + int targetValue; + + @HiveField(1) + String targetUnit; // 'Juz', 'Halaman', 'Ayat' + + @HiveField(2) + int currentProgress; + + @HiveField(3) + bool autoSync; + + @HiveField(4) + int rawAyatRead; + + @HiveField(5) + bool targetCompleted; + + TilawahLog({ + this.targetValue = 1, + this.targetUnit = 'Juz', + this.currentProgress = 0, + this.autoSync = false, + this.rawAyatRead = 0, + this.targetCompleted = false, + }); + + bool get isCompleted => targetCompleted; +} diff --git a/lib/data/local/models/tilawah_log.g.dart b/lib/data/local/models/tilawah_log.g.dart new file mode 100644 index 0000000..1f67aef --- /dev/null +++ b/lib/data/local/models/tilawah_log.g.dart @@ -0,0 +1,56 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'tilawah_log.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class TilawahLogAdapter extends TypeAdapter { + @override + final int typeId = 8; + + @override + TilawahLog read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return TilawahLog( + targetValue: fields[0] as int, + targetUnit: fields[1] as String, + currentProgress: fields[2] as int, + autoSync: fields[3] as bool, + rawAyatRead: fields[4] as int? ?? 0, + targetCompleted: fields[5] as bool? ?? false, + ); + } + + @override + void write(BinaryWriter writer, TilawahLog obj) { + writer + ..writeByte(6) + ..writeByte(0) + ..write(obj.targetValue) + ..writeByte(1) + ..write(obj.targetUnit) + ..writeByte(2) + ..write(obj.currentProgress) + ..writeByte(3) + ..write(obj.autoSync) + ..writeByte(4) + ..write(obj.rawAyatRead) + ..writeByte(5) + ..write(obj.targetCompleted); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is TilawahLogAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/lib/data/services/.gitkeep b/lib/data/services/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/lib/data/services/dzikir_service.dart b/lib/data/services/dzikir_service.dart new file mode 100644 index 0000000..d55b65b --- /dev/null +++ b/lib/data/services/dzikir_service.dart @@ -0,0 +1,107 @@ +import 'dart:convert'; +import 'package:flutter/services.dart'; +import 'package:hive_flutter/hive_flutter.dart'; +import '../local/hive_boxes.dart'; +import '../local/models/dzikir_counter.dart'; +import 'package:intl/intl.dart'; + +/// Represents a single dzikir item from the bundled JSON. +class DzikirItem { + final String id; + final String arabic; + final String transliteration; + final String translation; + final int targetCount; + final String? source; + + DzikirItem({ + required this.id, + required this.arabic, + required this.transliteration, + required this.translation, + required this.targetCount, + this.source, + }); + + factory DzikirItem.fromJson(Map json) { + return DzikirItem( + id: json['id'] as String, + arabic: json['arabic'] as String? ?? '', + transliteration: json['transliteration'] as String? ?? '', + translation: json['translation'] as String? ?? '', + targetCount: json['target_count'] as int? ?? 1, + source: json['source'] as String?, + ); + } +} + +/// Types of dzikir sessions. +enum DzikirType { pagi, petang } + +/// Service to load dzikir data and manage counters. +class DzikirService { + DzikirService._(); + static final DzikirService instance = DzikirService._(); + + final Map> _cache = {}; + + /// Load dzikir items from bundled JSON. + Future> getDzikir(DzikirType type) async { + if (_cache.containsKey(type)) return _cache[type]!; + + final path = type == DzikirType.pagi + ? 'assets/dzikir/dzikir_pagi.json' + : 'assets/dzikir/dzikir_petang.json'; + + try { + final jsonString = await rootBundle.loadString(path); + final List data = json.decode(jsonString); + _cache[type] = + data.map((d) => DzikirItem.fromJson(d as Map)).toList(); + } catch (_) { + _cache[type] = []; + } + + return _cache[type]!; + } + + /// Get counters for a specific date from Hive. + Map getCountersForDate(String date) { + final box = Hive.box(HiveBoxes.dzikirCounters); + final result = {}; + + for (final key in box.keys) { + final counter = box.get(key); + if (counter != null && counter.date == date) { + result[counter.dzikirId] = counter.count; + } + } + + return result; + } + + /// Increment a dzikir counter for a specific ID on a specific date. + Future increment(String dzikirId, String date, int target) async { + final box = Hive.box(HiveBoxes.dzikirCounters); + final key = '${dzikirId}_$date'; + + final existing = box.get(key); + if (existing != null) { + existing.count = (existing.count + 1).clamp(0, target); + await existing.save(); + } else { + await box.put( + key, + DzikirCounter( + dzikirId: dzikirId, + date: date, + count: 1, + target: target, + ), + ); + } + } + + /// Get today's date string. + String get todayKey => DateFormat('yyyy-MM-dd').format(DateTime.now()); +} diff --git a/lib/data/services/equran_service.dart b/lib/data/services/equran_service.dart new file mode 100644 index 0000000..575f429 --- /dev/null +++ b/lib/data/services/equran_service.dart @@ -0,0 +1,108 @@ +import 'dart:convert'; +import 'package:http/http.dart' as http; + +/// Service for EQuran.id v2 API. +/// Provides complete Quran data: Arabic, Indonesian translation, +/// tafsir, and audio from 6 qari. +class EQuranService { + static const String _baseUrl = 'https://equran.id/api/v2'; + static final EQuranService instance = EQuranService._(); + EQuranService._(); + + // In-memory cache + List>? _surahListCache; + + /// Get list of all 114 surahs. + Future>> getAllSurahs() async { + if (_surahListCache != null) return _surahListCache!; + try { + final response = await http.get(Uri.parse('$_baseUrl/surat')); + if (response.statusCode == 200) { + final data = json.decode(response.body); + if (data['code'] == 200) { + _surahListCache = + List>.from(data['data']); + return _surahListCache!; + } + } + } catch (e) { + // silent fallback + } + return []; + } + + /// Get full surah with all ayat, audio, etc. + /// Returns the full surah data object. + Future?> getSurah(int number) async { + try { + final response = + await http.get(Uri.parse('$_baseUrl/surat/$number')); + if (response.statusCode == 200) { + final data = json.decode(response.body); + if (data['code'] == 200) { + return Map.from(data['data']); + } + } + } catch (e) { + // silent fallback + } + return null; + } + + /// Get tafsir for a surah. + Future?> getTafsir(int number) async { + try { + final response = + await http.get(Uri.parse('$_baseUrl/tafsir/$number')); + if (response.statusCode == 200) { + final data = json.decode(response.body); + if (data['code'] == 200) { + return Map.from(data['data']); + } + } + } catch (e) { + // silent fallback + } + return null; + } + + /// Get deterministic daily ayat from API + Future?> getDailyAyat() async { + try { + final now = DateTime.now(); + final dayOfYear = int.parse(now.difference(DateTime(now.year, 1, 1)).inDays.toString()); + + // Pick surah 1-114 + int surahId = (dayOfYear % 114) + 1; + + final surahData = await getSurah(surahId); + if (surahData != null && surahData['ayat'] != null) { + int totalAyat = surahData['jumlahAyat'] ?? 1; + int ayatIndex = dayOfYear % totalAyat; + + final targetAyat = surahData['ayat'][ayatIndex]; + + return { + 'surahName': surahData['namaLatin'], + 'nomorSurah': surahId, + 'nomorAyat': targetAyat['nomorAyat'], + 'teksArab': targetAyat['teksArab'], + 'teksIndonesia': targetAyat['teksIndonesia'], + }; + } + } catch (e) { + // silent fallback + } + return null; + } + + /// Available qari names mapped to audio key index. + static const Map qariNames = { + '01': 'Abdullah Al-Juhany', + '02': 'Abdul Muhsin Al-Qasim', + '03': 'Abdurrahman As-Sudais', + '04': 'Ibrahim Al-Dossari', + '05': 'Misyari Rasyid Al-Afasi', + '06': 'Yasser Al-Dosari', + }; +} diff --git a/lib/data/services/location_service.dart b/lib/data/services/location_service.dart new file mode 100644 index 0000000..36e6e50 --- /dev/null +++ b/lib/data/services/location_service.dart @@ -0,0 +1,86 @@ +import 'package:geolocator/geolocator.dart'; +import 'package:geocoding/geocoding.dart' as geocoding; +import 'package:hive_flutter/hive_flutter.dart'; +import '../local/hive_boxes.dart'; +import '../local/models/app_settings.dart'; + +/// Location service with GPS + fallback to last known location. +class LocationService { + LocationService._(); + static final LocationService instance = LocationService._(); + + /// Request permission and get current GPS location. + Future getCurrentLocation() async { + bool serviceEnabled = await Geolocator.isLocationServiceEnabled(); + if (!serviceEnabled) return null; + + LocationPermission permission = await Geolocator.checkPermission(); + if (permission == LocationPermission.denied) { + permission = await Geolocator.requestPermission(); + if (permission == LocationPermission.denied) return null; + } + + if (permission == LocationPermission.deniedForever) return null; + + try { + final position = await Geolocator.getCurrentPosition( + locationSettings: const LocationSettings( + accuracy: LocationAccuracy.medium, + timeLimit: Duration(seconds: 10), + ), + ); + + // Save to settings for fallback + await _saveLastKnown(position.latitude, position.longitude); + return position; + } catch (_) { + return null; + } + } + + /// Get last known location from Hive settings. + ({double lat, double lng, String? cityName})? getLastKnownLocation() { + final settingsBox = Hive.box(HiveBoxes.settings); + final settings = settingsBox.get('default'); + if (settings?.lastLat != null && settings?.lastLng != null) { + return ( + lat: settings!.lastLat!, + lng: settings.lastLng!, + cityName: settings.lastCityName, + ); + } + return null; + } + + /// Reverse geocode to get city name from coordinates. + Future getCityName(double lat, double lng) async { + try { + final placemarks = await geocoding.placemarkFromCoordinates(lat, lng); + if (placemarks.isNotEmpty) { + final place = placemarks.first; + final city = place.locality ?? place.subAdministrativeArea ?? 'Unknown'; + final country = place.country ?? ''; + return '$city, $country'; + } + } catch (_) { + // Geocoding may fail offline — return coords + } + return '${lat.toStringAsFixed(2)}, ${lng.toStringAsFixed(2)}'; + } + + /// Save last known position to Hive. + Future _saveLastKnown(double lat, double lng) async { + final settingsBox = Hive.box(HiveBoxes.settings); + final settings = settingsBox.get('default'); + if (settings != null) { + settings.lastLat = lat; + settings.lastLng = lng; + try { + settings.lastCityName = await getCityName(lat, lng); + } catch (_) { + // Ignore geocoding errors + } + await settings.save(); + } + } +} diff --git a/lib/data/services/myquran_sholat_service.dart b/lib/data/services/myquran_sholat_service.dart new file mode 100644 index 0000000..c303ee7 --- /dev/null +++ b/lib/data/services/myquran_sholat_service.dart @@ -0,0 +1,108 @@ +import 'dart:convert'; +import 'package:http/http.dart' as http; + +/// Service for myQuran.com v3 Sholat API. +/// Provides Kemenag-accurate prayer times for Indonesian cities. +class MyQuranSholatService { + static const String _baseUrl = 'https://api.myquran.com/v3/sholat'; + static final MyQuranSholatService instance = MyQuranSholatService._(); + MyQuranSholatService._(); + + /// Search for a city/kabupaten by name. + /// Returns list of {id, lokasi}. + Future>> searchCity(String query) async { + try { + final response = await http.get( + Uri.parse('$_baseUrl/kota/cari/$query'), + ); + if (response.statusCode == 200) { + final data = json.decode(response.body); + if (data['status'] == true) { + return List>.from(data['data']); + } + } + } catch (e) { + // silent fallback + } + return []; + } + + /// Get prayer times for a specific city and date. + /// [cityId] = myQuran city ID (hash string) + /// [date] = 'yyyy-MM-dd' format + /// Returns map: {tanggal, imsak, subuh, terbit, dhuha, dzuhur, ashar, maghrib, isya} + Future?> getDailySchedule( + String cityId, String date) async { + try { + final response = await http.get( + Uri.parse('$_baseUrl/jadwal/$cityId/$date'), + ); + if (response.statusCode == 200) { + final data = json.decode(response.body); + if (data['status'] == true) { + final jadwal = data['data']['jadwal'][date]; + if (jadwal != null) { + return Map.from( + jadwal.map((k, v) => MapEntry(k.toString(), v.toString())), + ); + } + } + } + } catch (e) { + // silent fallback + } + return null; + } + + /// Get monthly prayer schedule. + /// [month] = 'yyyy-MM' format + /// Returns map of date → jadwal. + Future>> getMonthlySchedule( + String cityId, String month) async { + try { + final response = await http.get( + Uri.parse('$_baseUrl/jadwal/$cityId/$month'), + ); + if (response.statusCode == 200) { + final data = json.decode(response.body); + if (data['status'] == true) { + final jadwalMap = data['data']['jadwal'] as Map; + final result = >{}; + for (final entry in jadwalMap.entries) { + result[entry.key] = Map.from( + (entry.value as Map).map( + (k, v) => MapEntry(k.toString(), v.toString())), + ); + } + return result; + } + } + } catch (e) { + // silent fallback + } + return {}; + } + + /// Get city info (kabko, prov) from a jadwal response. + Future?> getCityInfo(String cityId) async { + final today = + DateTime.now().toIso8601String().substring(0, 10); + try { + final response = await http.get( + Uri.parse('$_baseUrl/jadwal/$cityId/$today'), + ); + if (response.statusCode == 200) { + final data = json.decode(response.body); + if (data['status'] == true) { + return { + 'kabko': data['data']['kabko']?.toString() ?? '', + 'prov': data['data']['prov']?.toString() ?? '', + }; + } + } + } catch (e) { + // silent fallback + } + return null; + } +} diff --git a/lib/data/services/notification_service.dart b/lib/data/services/notification_service.dart new file mode 100644 index 0000000..7aa5866 --- /dev/null +++ b/lib/data/services/notification_service.dart @@ -0,0 +1,98 @@ +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:timezone/timezone.dart' as tz; + +/// Notification service for Adhan and Iqamah notifications. +class NotificationService { + NotificationService._(); + static final NotificationService instance = NotificationService._(); + + final FlutterLocalNotificationsPlugin _plugin = + FlutterLocalNotificationsPlugin(); + + bool _initialized = false; + + /// Initialize notification channels. + Future init() async { + if (_initialized) return; + + const androidSettings = AndroidInitializationSettings('@mipmap/ic_launcher'); + const darwinSettings = DarwinInitializationSettings( + requestAlertPermission: true, + requestBadgePermission: true, + requestSoundPermission: true, + ); + + const settings = InitializationSettings( + android: androidSettings, + iOS: darwinSettings, + macOS: darwinSettings, + ); + + await _plugin.initialize(settings); + _initialized = true; + } + + /// Schedule an Adhan notification at a specific time. + Future scheduleAdhan({ + required int id, + required String prayerName, + required DateTime time, + }) async { + await _plugin.zonedSchedule( + id, + 'Adhan - $prayerName', + 'It\'s time for $prayerName prayer', + tz.TZDateTime.from(time, tz.local), + const NotificationDetails( + android: AndroidNotificationDetails( + 'adhan_channel', + 'Adhan Notifications', + channelDescription: 'Prayer time adhan notifications', + importance: Importance.high, + priority: Priority.high, + ), + iOS: DarwinNotificationDetails( + presentAlert: true, + presentBadge: true, + presentSound: true, + ), + ), + androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle, + ); + } + + /// Schedule an Iqamah reminder notification. + Future scheduleIqamah({ + required int id, + required String prayerName, + required DateTime adhanTime, + required int offsetMinutes, + }) async { + final iqamahTime = adhanTime.add(Duration(minutes: offsetMinutes)); + await _plugin.zonedSchedule( + id + 100, // Offset IDs for iqamah + 'Iqamah - $prayerName', + 'Iqamah for $prayerName in $offsetMinutes minutes', + tz.TZDateTime.from(iqamahTime, tz.local), + const NotificationDetails( + android: AndroidNotificationDetails( + 'iqamah_channel', + 'Iqamah Reminders', + channelDescription: 'Iqamah reminder notifications', + importance: Importance.defaultImportance, + priority: Priority.defaultPriority, + ), + iOS: DarwinNotificationDetails( + presentAlert: true, + presentSound: true, + ), + ), + androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle, + ); + } + + /// Cancel all pending notifications. + Future cancelAll() async { + await _plugin.cancelAll(); + } +} diff --git a/lib/data/services/placeholder.dart b/lib/data/services/placeholder.dart new file mode 100644 index 0000000..1039d4c --- /dev/null +++ b/lib/data/services/placeholder.dart @@ -0,0 +1 @@ +// TODO: implement diff --git a/lib/data/services/prayer_service.dart b/lib/data/services/prayer_service.dart new file mode 100644 index 0000000..079c3fa --- /dev/null +++ b/lib/data/services/prayer_service.dart @@ -0,0 +1,126 @@ +import 'package:adhan/adhan.dart' as adhan; +import 'package:hive_flutter/hive_flutter.dart'; +import '../local/hive_boxes.dart'; +import '../local/models/cached_prayer_times.dart'; +import 'package:intl/intl.dart'; + +/// Result object for prayer times. +class PrayerTimesResult { + final DateTime fajr; + final DateTime sunrise; + final DateTime dhuhr; + final DateTime asr; + final DateTime maghrib; + final DateTime isha; + + PrayerTimesResult({ + required this.fajr, + required this.sunrise, + required this.dhuhr, + required this.asr, + required this.maghrib, + required this.isha, + }); +} + +/// Prayer time calculation service using the adhan package. +class PrayerService { + PrayerService._(); + static final PrayerService instance = PrayerService._(); + + /// Calculate prayer times for a given location and date. + /// Uses cache if available; writes to cache after calculation. + PrayerTimesResult getPrayerTimes(double lat, double lng, DateTime date) { + final dateKey = DateFormat('yyyy-MM-dd').format(date); + final cacheKey = '${lat.toStringAsFixed(4)}_${lng.toStringAsFixed(4)}_$dateKey'; + + // Check cache + final cacheBox = Hive.box(HiveBoxes.cachedPrayerTimes); + final cached = cacheBox.get(cacheKey); + if (cached != null) { + return PrayerTimesResult( + fajr: cached.fajr, + sunrise: cached.sunrise, + dhuhr: cached.dhuhr, + asr: cached.asr, + maghrib: cached.maghrib, + isha: cached.isha, + ); + } + + // Calculate using adhan package + final coordinates = adhan.Coordinates(lat, lng); + final dateComponents = adhan.DateComponents(date.year, date.month, date.day); + final params = adhan.CalculationMethod.muslim_world_league.getParameters(); + params.madhab = adhan.Madhab.shafi; + + final prayerTimes = adhan.PrayerTimes(coordinates, dateComponents, params); + + final result = PrayerTimesResult( + fajr: prayerTimes.fajr!, + sunrise: prayerTimes.sunrise!, + dhuhr: prayerTimes.dhuhr!, + asr: prayerTimes.asr!, + maghrib: prayerTimes.maghrib!, + isha: prayerTimes.isha!, + ); + + // Cache result + cacheBox.put( + cacheKey, + CachedPrayerTimes( + key: cacheKey, + lat: lat, + lng: lng, + date: dateKey, + fajr: result.fajr, + sunrise: result.sunrise, + dhuhr: result.dhuhr, + asr: result.asr, + maghrib: result.maghrib, + isha: result.isha, + ), + ); + + return result; + } + + /// Get the next prayer name and time from now. + MapEntry? getNextPrayer(PrayerTimesResult times) { + final now = DateTime.now(); + final entries = { + 'Fajr': times.fajr, + 'Dhuhr': times.dhuhr, + 'Asr': times.asr, + 'Maghrib': times.maghrib, + 'Isha': times.isha, + }; + + for (final entry in entries.entries) { + if (entry.value.isAfter(now)) { + return entry; + } + } + return null; // All prayers passed for today + } + + /// Get the current active prayer (the last prayer whose time has passed). + String? getCurrentPrayer(PrayerTimesResult times) { + final now = DateTime.now(); + String? current; + + if (now.isAfter(times.isha)) { + current = 'Isha'; + } else if (now.isAfter(times.maghrib)) { + current = 'Maghrib'; + } else if (now.isAfter(times.asr)) { + current = 'Asr'; + } else if (now.isAfter(times.dhuhr)) { + current = 'Dhuhr'; + } else if (now.isAfter(times.fajr)) { + current = 'Fajr'; + } + + return current; + } +} diff --git a/lib/data/services/quran_service.dart b/lib/data/services/quran_service.dart new file mode 100644 index 0000000..37e4c29 --- /dev/null +++ b/lib/data/services/quran_service.dart @@ -0,0 +1,98 @@ +import 'dart:convert'; +import 'package:flutter/services.dart'; + +/// Represents a single Surah with its verses. +class Surah { + final int id; + final String nameArabic; + final String nameLatin; + final int verseCount; + final int juzStart; + final String revelationType; + final List verses; + + Surah({ + required this.id, + required this.nameArabic, + required this.nameLatin, + required this.verseCount, + this.juzStart = 1, + this.revelationType = 'Meccan', + this.verses = const [], + }); + + factory Surah.fromJson(Map json) { + return Surah( + id: json['id'] as int, + nameArabic: json['name_arabic'] as String? ?? '', + nameLatin: json['name_latin'] as String? ?? '', + verseCount: json['verse_count'] as int? ?? 0, + juzStart: json['juz_start'] as int? ?? 1, + revelationType: json['revelation_type'] as String? ?? 'Meccan', + verses: (json['verses'] as List?) + ?.map((v) => Verse.fromJson(v as Map)) + .toList() ?? + [], + ); + } +} + +/// A single Quran verse. +class Verse { + final int id; + final String arabic; + final String? transliteration; + final String translationId; + + Verse({ + required this.id, + required this.arabic, + this.transliteration, + required this.translationId, + }); + + factory Verse.fromJson(Map json) { + return Verse( + id: json['id'] as int, + arabic: json['arabic'] as String? ?? '', + transliteration: json['transliteration'] as String?, + translationId: json['translation_id'] as String? ?? '', + ); + } +} + +/// Service to load Quran data from bundled JSON asset. +class QuranService { + QuranService._(); + static final QuranService instance = QuranService._(); + + List? _cachedSurahs; + + /// Load all 114 Surahs from local JSON. Cached in memory after first load. + Future> getAllSurahs() async { + if (_cachedSurahs != null) return _cachedSurahs!; + + try { + final jsonString = + await rootBundle.loadString('assets/quran/quran_id.json'); + final List data = json.decode(jsonString); + _cachedSurahs = data + .map((s) => Surah.fromJson(s as Map)) + .toList(); + } catch (_) { + _cachedSurahs = []; + } + + return _cachedSurahs!; + } + + /// Get a single Surah by ID. + Future getSurah(int id) async { + final surahs = await getAllSurahs(); + try { + return surahs.firstWhere((s) => s.id == id); + } catch (_) { + return null; + } + } +} diff --git a/lib/data/services/unsplash_service.dart b/lib/data/services/unsplash_service.dart new file mode 100644 index 0000000..e3037ad --- /dev/null +++ b/lib/data/services/unsplash_service.dart @@ -0,0 +1,83 @@ +import 'dart:convert'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:http/http.dart' as http; +import 'package:hive_flutter/hive_flutter.dart'; + +/// Service for fetching Islamic-themed photos from Unsplash. +/// Implements aggressive caching to minimize API usage (1 request/day). +class UnsplashService { + static const String _baseUrl = 'https://api.unsplash.com'; + static const String _cacheBoxName = 'unsplash_cache'; + static const String _cacheKey = 'cached_photo'; + static const String _cacheTimestampKey = 'cached_timestamp'; + static const Duration _cacheTTL = Duration(hours: 24); + + static final UnsplashService instance = UnsplashService._(); + UnsplashService._(); + + // In-memory cache for the current session + Map? _memoryCache; + + /// Get a cached or fresh Islamic photo. + /// Returns a map with keys: 'imageUrl', 'photographerName', 'photographerUrl', 'unsplashUrl' + Future?> getIslamicPhoto() async { + // 1. Check memory cache + if (_memoryCache != null) return _memoryCache; + + // 2. Check Hive cache + final box = await Hive.openBox(_cacheBoxName); + final cachedData = box.get(_cacheKey); + final cachedTimestamp = box.get(_cacheTimestampKey); + + if (cachedData != null && cachedTimestamp != null) { + final cachedTime = DateTime.fromMillisecondsSinceEpoch(cachedTimestamp); + if (DateTime.now().difference(cachedTime) < _cacheTTL) { + _memoryCache = Map.from(json.decode(cachedData)); + return _memoryCache; + } + } + + // 3. Fetch from API + final photo = await _fetchFromApi(); + if (photo != null) { + // Cache in Hive + await box.put(_cacheKey, json.encode(photo)); + await box.put(_cacheTimestampKey, DateTime.now().millisecondsSinceEpoch); + _memoryCache = photo; + } + + return photo; + } + + Future?> _fetchFromApi() async { + final accessKey = dotenv.env['UNSPLASH_ACCESS_KEY']; + if (accessKey == null || accessKey.isEmpty || accessKey == 'YOUR_ACCESS_KEY_HERE') { + return null; + } + + try { + final queries = ['masjid', 'kaabah', 'mosque', 'islamic architecture']; + // Rotate query based on the day of year for variety + final dayOfYear = DateTime.now().difference(DateTime(DateTime.now().year, 1, 1)).inDays; + final query = queries[dayOfYear % queries.length]; + + final response = await http.get( + Uri.parse('$_baseUrl/photos/random?query=$query&orientation=portrait&content_filter=high'), + headers: {'Authorization': 'Client-ID $accessKey'}, + ); + + if (response.statusCode == 200) { + final data = json.decode(response.body); + return { + 'imageUrl': data['urls']?['regular'] ?? '', + 'photographerName': data['user']?['name'] ?? 'Unknown', + 'photographerUrl': data['user']?['links']?['html'] ?? '', + 'unsplashUrl': data['links']?['html'] ?? '', + }; + } + } catch (e) { + // Silent fallback — show the equalizer without background + } + return null; + } +} diff --git a/lib/features/checklist/presentation/.gitkeep b/lib/features/checklist/presentation/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/lib/features/checklist/presentation/checklist_screen.dart b/lib/features/checklist/presentation/checklist_screen.dart new file mode 100644 index 0000000..da80aa5 --- /dev/null +++ b/lib/features/checklist/presentation/checklist_screen.dart @@ -0,0 +1,648 @@ +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:intl/intl.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/shalat_log.dart'; +import '../../../data/local/models/tilawah_log.dart'; +import '../../../data/local/models/dzikir_log.dart'; +import '../../../data/local/models/puasa_log.dart'; + +class ChecklistScreen extends ConsumerStatefulWidget { + const ChecklistScreen({super.key}); + + @override + ConsumerState createState() => _ChecklistScreenState(); +} + +class _ChecklistScreenState extends ConsumerState { + late String _todayKey; + late Box _logBox; + late Box _settingsBox; + late AppSettings _settings; + + final List _fardhuPrayers = ['Subuh', 'Dzuhur', 'Ashar', 'Maghrib', 'Isya']; + + @override + void initState() { + super.initState(); + _todayKey = DateFormat('yyyy-MM-dd').format(DateTime.now()); + _logBox = Hive.box(HiveBoxes.worshipLogs); + _settingsBox = Hive.box(HiveBoxes.settings); + _settings = _settingsBox.get('default') ?? AppSettings(); + _ensureLogExists(); + } + + void _ensureLogExists() { + if (!_logBox.containsKey(_todayKey)) { + final shalatLogs = {}; + for (final p in _fardhuPrayers) { + shalatLogs[p.toLowerCase()] = ShalatLog(); + } + + _logBox.put( + _todayKey, + DailyWorshipLog( + date: _todayKey, + shalatLogs: shalatLogs, + tilawahLog: TilawahLog( + targetValue: _settings.tilawahTargetValue, + targetUnit: _settings.tilawahTargetUnit, + autoSync: _settings.tilawahAutoSync, + ), + dzikirLog: _settings.trackDzikir ? DzikirLog() : null, + puasaLog: _settings.trackPuasa ? PuasaLog() : null, + ), + ); + } + } + + DailyWorshipLog get _todayLog => _logBox.get(_todayKey)!; + + void _recalculateProgress() { + final log = _todayLog; + + // Lazily attach Dzikir and Puasa if user toggles them mid-day + if (_settings.trackDzikir && log.dzikirLog == null) log.dzikirLog = DzikirLog(); + if (_settings.trackPuasa && log.puasaLog == null) log.puasaLog = PuasaLog(); + + int total = 0; + int completed = 0; + + // Shalat + for (final p in _fardhuPrayers) { + final pKey = p.toLowerCase(); + final sLog = log.shalatLogs[pKey]; + if (sLog != null) { + total++; + if (sLog.completed) completed++; + + if (hasQabliyah(pKey, _settings.rawatibLevel)) { + total++; + if (sLog.qabliyah == true) completed++; + } + if (hasBadiyah(pKey, _settings.rawatibLevel)) { + total++; + if (sLog.badiyah == true) completed++; + } + } + } + + // Tilawah + if (log.tilawahLog != null) { + total++; + if (log.tilawahLog!.isCompleted) completed++; + } + + // Dzikir + if (_settings.trackDzikir && log.dzikirLog != null) { + total += 2; + if (log.dzikirLog!.pagi) completed++; + if (log.dzikirLog!.petang) completed++; + } + + // Puasa + if (_settings.trackPuasa && log.puasaLog != null) { + total++; + if (log.puasaLog!.completed) completed++; + } + + log.totalItems = total; + log.completedCount = completed; + log.completionPercent = total > 0 ? completed / total : 0.0; + log.save(); + setState(() {}); + } + + bool hasQabliyah(String prayer, int level) { + if (level == 0) return false; + if (prayer == 'subuh') return true; + if (prayer == 'dzuhur') return true; + if (prayer == 'ashar') return level == 2; // Ghairu Muakkad + if (prayer == 'maghrib') return level == 2; // Ghairu Muakkad + if (prayer == 'isya') return level == 2; // Ghairu Muakkad + return false; + } + + bool hasBadiyah(String prayer, int level) { + if (level == 0) return false; + if (prayer == 'subuh') return false; + if (prayer == 'dzuhur') return true; + if (prayer == 'ashar') return false; + if (prayer == 'maghrib') return true; + if (prayer == 'isya') return true; + return false; + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final isDark = theme.brightness == Brightness.dark; + final log = _todayLog; + + return Scaffold( + appBar: AppBar( + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Ibadah Harian'), + Text( + DateFormat('EEEE, d MMM yyyy').format(DateTime.now()), + style: theme.textTheme.bodySmall?.copyWith( + color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight, + ), + ), + ], + ), + centerTitle: false, + actions: [ + IconButton( + onPressed: () {}, + icon: const Icon(Icons.notifications_outlined), + ), + IconButton( + onPressed: () => context.push('/settings'), + icon: const Icon(Icons.settings_outlined), + ), + const SizedBox(width: 8), + ], + ), + body: ListView( + padding: const EdgeInsets.symmetric(horizontal: 16), + children: [ + const SizedBox(height: 12), + _buildProgressCard(log, isDark), + const SizedBox(height: 24), + _sectionLabel('SHOLAT FARDHU & RAWATIB'), + const SizedBox(height: 12), + ..._fardhuPrayers.map((p) => _buildShalatCard(p, isDark)).toList(), + const SizedBox(height: 24), + _sectionLabel('TILAWAH AL-QURAN'), + const SizedBox(height: 12), + _buildTilawahCard(isDark), + if (_settings.trackDzikir || _settings.trackPuasa) ...[ + const SizedBox(height: 24), + _sectionLabel('AMALAN TAMBAHAN'), + const SizedBox(height: 12), + ], + if (_settings.trackDzikir) _buildDzikirCard(isDark), + if (_settings.trackPuasa) _buildPuasaCard(isDark), + const SizedBox(height: 32), + ], + ), + ); + } + + Widget _sectionLabel(String text) { + return Text( + text, + style: const TextStyle( + fontSize: 11, + fontWeight: FontWeight.w700, + letterSpacing: 1.5, + color: AppColors.sage, + ), + ); + } + + Widget _buildProgressCard(DailyWorshipLog log, bool isDark) { + final percent = log.completionPercent; + final remaining = log.totalItems - log.completedCount; + + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: isDark ? AppColors.surfaceDark : const Color(0xFF2B3441), + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.1), + blurRadius: 16, + offset: const Offset(0, 8), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "POIN HARI INI", + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.w700, + letterSpacing: 1.5, + color: AppColors.primary.withValues(alpha: 0.8), + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), + decoration: BoxDecoration( + color: AppColors.primary.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + const Icon(Icons.stars, color: AppColors.primary, size: 14), + const SizedBox(width: 4), + Text( + '${log.totalPoints} pts', + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w700, + color: AppColors.primary, + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 12), + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + '${(percent * 100).round()}%', + style: const TextStyle( + fontSize: 42, + fontWeight: FontWeight.w800, + color: Colors.white, + height: 1.1, + ), + ), + const SizedBox(width: 8), + const Padding( + padding: EdgeInsets.only(bottom: 8), + child: Text( + 'Selesai', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w400, + color: Colors.white70, + ), + ), + ), + ], + ), + const SizedBox(height: 16), + AppProgressBar( + value: percent, + height: 8, + backgroundColor: Colors.white.withValues(alpha: 0.15), + fillColor: AppColors.primary, + ), + const SizedBox(height: 12), + Text( + remaining == 0 && log.totalItems > 0 + ? 'MasyaAllah! Poin maksimal tercapai hari ini! 🎉' + : 'Kumpulkan poin lebih banyak dengan Sholat di Masjid dan amalan sunnah lainnya!', + style: TextStyle( + fontSize: 13, + color: Colors.white.withValues(alpha: 0.7), + ), + ), + ], + ), + ); + } + + Widget _buildShalatCard(String prayerName, bool isDark) { + final pKey = prayerName.toLowerCase(); + final log = _todayLog.shalatLogs[pKey]; + if (log == null) return const SizedBox.shrink(); + + final hasQab = hasQabliyah(pKey, _settings.rawatibLevel); + final hasBad = hasBadiyah(pKey, _settings.rawatibLevel); + final isCompleted = log.completed; + + return Container( + margin: const EdgeInsets.only(bottom: 12), + decoration: BoxDecoration( + color: isDark ? AppColors.surfaceDark : AppColors.surfaceLight, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: isCompleted + ? AppColors.primary.withValues(alpha: 0.3) + : (isDark ? AppColors.primary.withValues(alpha: 0.08) : AppColors.cream), + ), + ), + child: Theme( + data: Theme.of(context).copyWith(dividerColor: Colors.transparent), + child: ExpansionTile( + tilePadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + leading: Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: isCompleted + ? AppColors.primary.withValues(alpha: 0.15) + : (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), + ), + title: Text( + 'Sholat $prayerName', + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 16, + color: isCompleted && isDark ? AppColors.textSecondaryDark : null, + decoration: isCompleted ? TextDecoration.lineThrough : null, + ), + ), + subtitle: log.location != null + ? Text('Di ${log.location}', style: const TextStyle(fontSize: 12, color: AppColors.primary)) + : null, + trailing: _CustomCheckbox( + value: isCompleted, + onChanged: (v) { + log.completed = v ?? false; + _recalculateProgress(); + }, + ), + childrenPadding: const EdgeInsets.only(left: 16, right: 16, bottom: 16), + children: [ + const Divider(), + const SizedBox(height: 8), + // Location Radio + Row( + children: [ + const Text('Pelaksanaan:', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600)), + const SizedBox(width: 16), + _radioOption('Masjid', log, () { + log.location = 'Masjid'; + log.completed = true; // Auto-check parent + _recalculateProgress(); + }), + const SizedBox(width: 16), + _radioOption('Rumah', log, () { + log.location = 'Rumah'; + log.completed = true; // Auto-check parent + _recalculateProgress(); + }), + ], + ), + if (hasQab || hasBad) const SizedBox(height: 12), + if (hasQab) + _sunnahRow('Qabliyah $prayerName', log.qabliyah ?? false, (v) { + log.qabliyah = v; + _recalculateProgress(); + }), + if (hasBad) + _sunnahRow('Ba\'diyah $prayerName', log.badiyah ?? false, (v) { + log.badiyah = v; + _recalculateProgress(); + }), + ], + ), + ), + ); + } + + Widget _radioOption(String title, ShalatLog log, VoidCallback onTap) { + final selected = log.location == title; + return GestureDetector( + onTap: onTap, + child: Row( + children: [ + Icon( + selected ? Icons.radio_button_checked : Icons.radio_button_off, + size: 18, + color: selected ? AppColors.primary : Colors.grey, + ), + const SizedBox(width: 4), + Text(title, style: TextStyle(fontSize: 13, color: selected ? AppColors.primary : null)), + ], + ), + ); + } + + Widget _sunnahRow(String title, bool value, ValueChanged onChanged) { + return Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(title, style: const TextStyle(fontSize: 14)), + _CustomCheckbox(value: value, onChanged: onChanged), + ], + ), + ); + } + + Widget _buildTilawahCard(bool isDark) { + final log = _todayLog.tilawahLog; + if (log == null) return const SizedBox.shrink(); + + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: isDark ? AppColors.surfaceDark : AppColors.surfaceLight, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: log.isCompleted + ? AppColors.primary.withValues(alpha: 0.3) + : (isDark ? AppColors.primary.withValues(alpha: 0.08) : AppColors.cream), + ), + ), + child: Column( + children: [ + // ── Row 1: Target + Checkbox ── + Row( + children: [ + Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: log.isCompleted + ? AppColors.primary.withValues(alpha: 0.15) + : (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), + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Tilawah', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: log.isCompleted && isDark ? AppColors.textSecondaryDark : null, + decoration: log.isCompleted ? TextDecoration.lineThrough : null, + ), + ), + Text( + 'Target: ${log.targetValue} ${log.targetUnit}', + style: const TextStyle(fontSize: 12, color: AppColors.primary), + ), + ], + ), + ), + _CustomCheckbox( + value: log.targetCompleted, + onChanged: (v) { + log.targetCompleted = v ?? false; + _recalculateProgress(); + }, + ), + ], + ), + const SizedBox(height: 12), + const Divider(height: 1), + const SizedBox(height: 12), + // ── Row 2: Ayat Tracker ── + Row( + children: [ + Icon(Icons.auto_stories, size: 18, color: AppColors.sage), + const SizedBox(width: 8), + Expanded( + child: Text( + 'Sudah Baca: ${log.rawAyatRead} Ayat', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight, + ), + ), + ), + if (log.autoSync) + Tooltip( + message: 'Sinkron dari Al-Quran', + child: Icon(Icons.sync, size: 16, color: AppColors.primary), + ), + IconButton( + icon: const Icon(Icons.remove_circle_outline, size: 20), + visualDensity: VisualDensity.compact, + onPressed: log.rawAyatRead > 0 + ? () { + log.rawAyatRead--; + _recalculateProgress(); + } + : null, + ), + IconButton( + icon: const Icon(Icons.add_circle_outline, size: 20, color: AppColors.primary), + visualDensity: VisualDensity.compact, + onPressed: () { + log.rawAyatRead++; + _recalculateProgress(); + }, + ), + ], + ), + ], + ), + ); + } + + Widget _buildDzikirCard(bool isDark) { + final log = _todayLog.dzikirLog; + if (log == null) return const SizedBox.shrink(); + + return Container( + margin: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: isDark ? AppColors.surfaceDark : AppColors.surfaceLight, + borderRadius: BorderRadius.circular(16), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.auto_awesome, size: 20, color: AppColors.sage), + const SizedBox(width: 8), + const Text('Dzikir Harian', style: TextStyle(fontWeight: FontWeight.w600, fontSize: 15)), + ], + ), + const SizedBox(height: 12), + _sunnahRow('Dzikir Pagi', log.pagi, (v) { + log.pagi = v ?? false; + _recalculateProgress(); + }), + _sunnahRow('Dzikir Petang', log.petang, (v) { + log.petang = v ?? false; + _recalculateProgress(); + }), + ], + ), + ); + } + + Widget _buildPuasaCard(bool isDark) { + final log = _todayLog.puasaLog; + if (log == null) return const SizedBox.shrink(); + + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: isDark ? AppColors.surfaceDark : AppColors.surfaceLight, + borderRadius: BorderRadius.circular(16), + ), + child: Row( + children: [ + const Icon(Icons.nightlight_round, size: 20, color: AppColors.sage), + const SizedBox(width: 8), + const Expanded(child: Text('Puasa Sunnah', style: TextStyle(fontWeight: FontWeight.w600, fontSize: 15))), + DropdownButton( + value: log.jenisPuasa, + hint: const Text('Jenis', style: TextStyle(fontSize: 12)), + underline: const SizedBox(), + items: ['Senin', 'Kamis', 'Ayyamul Bidh', 'Daud', 'Lainnya'] + .map((e) => DropdownMenuItem(value: e, child: Text(e, style: const TextStyle(fontSize: 13)))) + .toList(), + onChanged: (v) { + log.jenisPuasa = v; + _recalculateProgress(); + }, + ), + const SizedBox(width: 8), + _CustomCheckbox( + value: log.completed, + onChanged: (v) { + log.completed = v ?? false; + _recalculateProgress(); + }, + ), + ], + ), + ); + } +} + +class _CustomCheckbox extends StatelessWidget { + final bool value; + final ValueChanged onChanged; + + const _CustomCheckbox({required this.value, required this.onChanged}); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () => onChanged(!value), + child: Container( + width: 24, + height: 24, + decoration: BoxDecoration( + color: value ? AppColors.primary : Colors.transparent, + 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, + ), + ); + } +} diff --git a/lib/features/checklist/presentation/placeholder.dart b/lib/features/checklist/presentation/placeholder.dart new file mode 100644 index 0000000..1039d4c --- /dev/null +++ b/lib/features/checklist/presentation/placeholder.dart @@ -0,0 +1 @@ +// TODO: implement diff --git a/lib/features/dashboard/data/.gitkeep b/lib/features/dashboard/data/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/lib/features/dashboard/data/placeholder.dart b/lib/features/dashboard/data/placeholder.dart new file mode 100644 index 0000000..1039d4c --- /dev/null +++ b/lib/features/dashboard/data/placeholder.dart @@ -0,0 +1 @@ +// TODO: implement diff --git a/lib/features/dashboard/data/prayer_times_provider.dart b/lib/features/dashboard/data/prayer_times_provider.dart new file mode 100644 index 0000000..36a9658 --- /dev/null +++ b/lib/features/dashboard/data/prayer_times_provider.dart @@ -0,0 +1,210 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:hive_flutter/hive_flutter.dart'; +import 'package:intl/intl.dart'; +import '../../../data/services/myquran_sholat_service.dart'; +import '../../../data/services/prayer_service.dart'; +import '../../../data/services/location_service.dart'; +import '../../../data/local/hive_boxes.dart'; +import '../../../data/local/models/app_settings.dart'; + +/// Represents a single prayer time entry. +class PrayerTimeEntry { + final String name; + final String time; // "HH:mm" + final bool isActive; + PrayerTimeEntry({ + required this.name, + required this.time, + this.isActive = false, + }); +} + +/// Full day prayer schedule from myQuran API. +class DaySchedule { + final String cityName; + final String province; + final String date; // yyyy-MM-dd + final String tanggal; // formatted date from API + final Map times; // {imsak, subuh, terbit, dhuha, dzuhur, ashar, maghrib, isya} + + DaySchedule({ + required this.cityName, + required this.province, + required this.date, + required this.tanggal, + required this.times, + }); + + /// Is this schedule for tomorrow? + bool get isTomorrow { + final todayStr = DateFormat('yyyy-MM-dd').format(DateTime.now()); + return date.compareTo(todayStr) > 0; + } + + /// Get prayer time entries as a list. + List get prayerList { + final now = DateTime.now(); + final formatter = DateFormat('HH:mm'); + final currentTime = formatter.format(now); + + final prayers = [ + PrayerTimeEntry(name: 'Imsak', time: times['imsak'] ?? '-'), + PrayerTimeEntry(name: 'Subuh', time: times['subuh'] ?? '-'), + PrayerTimeEntry(name: 'Terbit', time: times['terbit'] ?? '-'), + PrayerTimeEntry(name: 'Dhuha', time: times['dhuha'] ?? '-'), + PrayerTimeEntry(name: 'Dzuhur', time: times['dzuhur'] ?? '-'), + PrayerTimeEntry(name: 'Ashar', time: times['ashar'] ?? '-'), + PrayerTimeEntry(name: 'Maghrib', time: times['maghrib'] ?? '-'), + PrayerTimeEntry(name: 'Isya', time: times['isya'] ?? '-'), + ]; + + // Find the next prayer + int activeIndex = -1; + if (isTomorrow) { + // User specifically requested to show tomorrow's Subuh as upcoming + activeIndex = 1; // 0=Imsak, 1=Subuh + } else { + for (int i = 0; i < prayers.length; i++) { + if (prayers[i].time != '-' && prayers[i].time.compareTo(currentTime) > 0) { + activeIndex = i; + break; + } + } + } + + if (activeIndex >= 0) { + prayers[activeIndex] = PrayerTimeEntry( + name: prayers[activeIndex].name, + time: prayers[activeIndex].time, + isActive: true, + ); + } + + return prayers; + } + + /// Get the next prayer name and time. + PrayerTimeEntry? get nextPrayer { + final list = prayerList; + for (final p in list) { + if (p.isActive) return p; + } + // If none active and it's today, all prayers have passed + return null; + } +} + +/// Default Jakarta city ID from myQuran API. +const _defaultCityId = '58a2fc6ed39fd083f55d4182bf88826d'; + +/// Provider for the user's selected city ID (stored in Hive settings). +final selectedCityIdProvider = StateProvider((ref) { + final box = Hive.box(HiveBoxes.settings); + final settings = box.get('default'); + final stored = settings?.lastCityName ?? ''; + if (stored.contains('|')) { + return stored.split('|').last; + } + return _defaultCityId; +}); + +/// Provider for today's prayer times using myQuran API. +final prayerTimesProvider = FutureProvider((ref) async { + final cityId = ref.watch(selectedCityIdProvider); + final today = DateFormat('yyyy-MM-dd').format(DateTime.now()); + + DaySchedule? schedule; + + // Try API first + final jadwal = + await MyQuranSholatService.instance.getDailySchedule(cityId, today); + + if (jadwal != null) { + final cityInfo = await MyQuranSholatService.instance.getCityInfo(cityId); + schedule = DaySchedule( + cityName: cityInfo?['kabko'] ?? 'Jakarta', + province: cityInfo?['prov'] ?? 'DKI Jakarta', + date: today, + tanggal: jadwal['tanggal'] ?? today, + times: jadwal, + ); + } + + // Check if all prayers today have passed + if (schedule != null && !schedule.isTomorrow && schedule.nextPrayer == null) { + // All prayers passed, fetch tomorrow's schedule + final tomorrow = DateTime.now().add(const Duration(days: 1)); + final tomorrowStr = DateFormat('yyyy-MM-dd').format(tomorrow); + + final tmrwJadwal = + await MyQuranSholatService.instance.getDailySchedule(cityId, tomorrowStr); + + if (tmrwJadwal != null) { + final cityInfo = await MyQuranSholatService.instance.getCityInfo(cityId); + schedule = DaySchedule( + cityName: cityInfo?['kabko'] ?? 'Jakarta', + province: cityInfo?['prov'] ?? 'DKI Jakarta', + date: tomorrowStr, + tanggal: tmrwJadwal['tanggal'] ?? tomorrowStr, + times: tmrwJadwal, + ); + } + } + + if (schedule != null) { + return schedule; + } + + // Fallback to adhan package + final position = await LocationService.instance.getCurrentLocation(); + double lat = position?.latitude ?? -6.2088; + double lng = position?.longitude ?? 106.8456; + + final result = PrayerService.instance.getPrayerTimes(lat, lng, DateTime.now()); + if (result != null) { + final timeFormat = DateFormat('HH:mm'); + return DaySchedule( + cityName: 'Jakarta', + province: 'DKI Jakarta', + date: today, + tanggal: DateFormat('EEEE, dd/MM/yyyy').format(DateTime.now()), + times: { + 'imsak': timeFormat.format(result.fajr.subtract(const Duration(minutes: 10))), + 'subuh': timeFormat.format(result.fajr), + 'terbit': timeFormat.format(result.sunrise), + 'dhuha': timeFormat.format(result.sunrise.add(const Duration(minutes: 15))), + 'dzuhur': timeFormat.format(result.dhuhr), + 'ashar': timeFormat.format(result.asr), + 'maghrib': timeFormat.format(result.maghrib), + 'isya': timeFormat.format(result.isha), + }, + ); + } + + return null; +}); + +/// Provider for monthly prayer schedule (for Imsakiyah screen). +final monthlyScheduleProvider = + FutureProvider.family>, String>( + (ref, month) async { + final cityId = ref.watch(selectedCityIdProvider); + return MyQuranSholatService.instance.getMonthlySchedule(cityId, month); +}); + +/// Provider for current city name. +final cityNameProvider = FutureProvider((ref) async { + final box = Hive.box(HiveBoxes.settings); + final settings = box.get('default'); + final stored = settings?.lastCityName ?? ''; + if (stored.contains('|')) { + return stored.split('|').first; + } + + final cityId = ref.watch(selectedCityIdProvider); + final info = await MyQuranSholatService.instance.getCityInfo(cityId); + if (info != null) { + return '${info['kabko']}, ${info['prov']}'; + } + return 'Kota Jakarta, DKI Jakarta'; +}); diff --git a/lib/features/dashboard/domain/.gitkeep b/lib/features/dashboard/domain/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/lib/features/dashboard/domain/placeholder.dart b/lib/features/dashboard/domain/placeholder.dart new file mode 100644 index 0000000..1039d4c --- /dev/null +++ b/lib/features/dashboard/domain/placeholder.dart @@ -0,0 +1 @@ +// TODO: implement diff --git a/lib/features/dashboard/presentation/dashboard_screen.dart b/lib/features/dashboard/presentation/dashboard_screen.dart new file mode 100644 index 0000000..08cb89b --- /dev/null +++ b/lib/features/dashboard/presentation/dashboard_screen.dart @@ -0,0 +1,673 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +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 '../../../app/theme/app_colors.dart'; +import '../../../core/widgets/prayer_time_card.dart'; +import '../../../data/local/hive_boxes.dart'; +import '../../../data/local/models/daily_worship_log.dart'; +import '../data/prayer_times_provider.dart'; + +class DashboardScreen extends ConsumerStatefulWidget { + const DashboardScreen({super.key}); + + @override + ConsumerState createState() => _DashboardScreenState(); +} + +class _DashboardScreenState extends ConsumerState { + Timer? _countdownTimer; + Duration _countdown = Duration.zero; + String _nextPrayerName = ''; + final ScrollController _prayerScrollController = ScrollController(); + + @override + void dispose() { + _countdownTimer?.cancel(); + _prayerScrollController.dispose(); + super.dispose(); + } + + void _startCountdown(DaySchedule schedule) { + _countdownTimer?.cancel(); + _updateCountdown(schedule); + _countdownTimer = Timer.periodic(const Duration(seconds: 1), (_) { + _updateCountdown(schedule); + }); + } + + void _updateCountdown(DaySchedule schedule) { + final next = schedule.nextPrayer; + if (next != null && next.time != '-') { + final parts = next.time.split(':'); + if (parts.length == 2) { + final now = DateTime.now(); + var target = DateTime(now.year, now.month, now.day, + int.parse(parts[0]), int.parse(parts[1])); + 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; + }); + } + } + } + + String _formatCountdown(Duration d) { + final h = d.inHours.toString().padLeft(2, '0'); + final m = (d.inMinutes % 60).toString().padLeft(2, '0'); + final s = (d.inSeconds % 60).toString().padLeft(2, '0'); + return '$h:$m:$s'; + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final isDark = theme.brightness == Brightness.dark; + final prayerTimesAsync = ref.watch(prayerTimesProvider); + + return Scaffold( + body: SafeArea( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + 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); + }, + 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), + const SizedBox(height: 24), + ], + ), + ), + ), + ); + } + + Widget _buildHeader(BuildContext context, bool isDark) { + return Row( + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + shape: BoxShape.circle, + 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), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Selamat datang,', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: isDark + ? AppColors.textSecondaryDark + : AppColors.textSecondaryLight, + ), + ), + Text( + "Assalamu'alaikum", + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w700, + ), + ), + ], + ), + ), + Row( + children: [ + IconButton( + onPressed: () {}, + icon: Icon( + Icons.notifications_outlined, + color: isDark + ? AppColors.textSecondaryDark + : AppColors.textSecondaryLight, + ), + ), + IconButton( + onPressed: () => context.push('/settings'), + icon: Icon( + Icons.settings_outlined, + color: isDark + ? AppColors.textSecondaryDark + : AppColors.textSecondaryLight, + ), + ), + ], + ), + ], + ); + } + + Widget _buildHeroCard(BuildContext context, DaySchedule schedule) { + final next = schedule.nextPrayer; + final name = _nextPrayerName.isNotEmpty + ? _nextPrayerName + : (next?.name ?? 'Isya'); + final time = next?.time ?? '--:--'; + + return Container( + width: double.infinity, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: AppColors.primary, + borderRadius: BorderRadius.circular(24), + boxShadow: [ + BoxShadow( + color: AppColors.primary.withValues(alpha: 0.3), + blurRadius: 20, + offset: const Offset(0, 8), + ), + ], + ), + child: Stack( + children: [ + Positioned( + top: -20, + right: -20, + child: Container( + width: 120, + height: 120, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.white.withValues(alpha: 0.15), + ), + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.schedule, + size: 16, + color: AppColors.onPrimary.withValues(alpha: 0.8)), + const SizedBox(width: 6), + Text( + 'SHOLAT BERIKUTNYA', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w700, + letterSpacing: 1.5, + color: AppColors.onPrimary.withValues(alpha: 0.8), + ), + ), + ], + ), + const SizedBox(height: 8), + 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), + ), + ), + const SizedBox(height: 4), + // City name + Text( + '📍 ${schedule.cityName}', + style: TextStyle( + fontSize: 13, + color: AppColors.onPrimary.withValues(alpha: 0.7), + ), + ), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: GestureDetector( + onTap: () => context.push('/tools/qibla'), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 12), + decoration: BoxDecoration( + color: AppColors.onPrimary, + borderRadius: BorderRadius.circular(50), + ), + child: const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.explore, size: 18, color: Colors.white), + SizedBox(width: 8), + Text( + 'Arah Kiblat', + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.w600, + fontSize: 14, + ), + ), + ], + ), + ), + ), + ), + const SizedBox(width: 12), + Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.2), + shape: BoxShape.circle, + ), + child: const Icon( + Icons.volume_up, + color: AppColors.onPrimary, + size: 22, + ), + ), + ], + ), + ], + ), + ], + ), + ); + } + + Widget _buildHeroCardPlaceholder(BuildContext context) { + return Container( + width: double.infinity, + height: 180, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: AppColors.primary, + borderRadius: BorderRadius.circular(24), + ), + child: const Center( + child: CircularProgressIndicator(color: AppColors.onPrimary), + ), + ); + } + + Widget _buildPrayerTimesSection( + BuildContext context, AsyncValue prayerTimesAsync) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + prayerTimesAsync.value?.isTomorrow == true + ? 'Jadwal Sholat Besok' + : 'Jadwal Sholat Hari Ini', + style: Theme.of(context) + .textTheme + .titleMedium + ?.copyWith(fontWeight: FontWeight.w700)), + Container( + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 4), + decoration: BoxDecoration( + color: AppColors.primary.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(50), + ), + child: Text( + prayerTimesAsync.value?.isTomorrow == true ? 'BESOK' : 'HARI INI', + style: TextStyle( + color: AppColors.primary, + fontSize: 10, + fontWeight: FontWeight.w700, + letterSpacing: 1.5, + ), + ), + ), + ], + ), + const SizedBox(height: 12), + SizedBox( + height: 110, + child: prayerTimesAsync.when( + data: (schedule) { + if (schedule == null) return const SizedBox(); + final prayers = schedule.prayerList.where( + (p) => ['Subuh', 'Dzuhur', 'Ashar', 'Maghrib', 'Isya'] + .contains(p.name), + ).toList(); + return ListView.separated( + controller: _prayerScrollController, + scrollDirection: Axis.horizontal, + itemCount: prayers.length, + separatorBuilder: (_, __) => const SizedBox(width: 12), + itemBuilder: (context, i) { + final p = prayers[i]; + final icon = _prayerIcon(p.name); + // Auto-scroll to active prayer on first build + if (p.isActive && i > 0) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_prayerScrollController.hasClients) { + final targetOffset = i * 124.0; // 112 width + 12 gap + _prayerScrollController.animateTo( + targetOffset.clamp(0, _prayerScrollController.position.maxScrollExtent), + duration: const Duration(milliseconds: 400), + curve: Curves.easeOut, + ); + } + }); + } + return PrayerTimeCard( + prayerName: p.name, + time: p.time, + icon: icon, + isActive: p.isActive, + ); + }, + ); + }, + loading: () => + const Center(child: CircularProgressIndicator()), + error: (_, __) => + const Center(child: Text('Gagal memuat jadwal')), + ), + ), + ], + ); + } + + IconData _prayerIcon(String name) { + switch (name) { + case 'Subuh': + return Icons.wb_twilight; + case 'Dzuhur': + return Icons.wb_sunny; + case 'Ashar': + return Icons.filter_drama; + case 'Maghrib': + return Icons.wb_twilight; + case 'Isya': + return Icons.dark_mode; + default: + return Icons.schedule; + } + } + + Widget _buildChecklistSummary(BuildContext context, bool isDark) { + final todayKey = DateFormat('yyyy-MM-dd').format(DateTime.now()); + final box = Hive.box(HiveBoxes.worshipLogs); + final log = box.get(todayKey); + + final points = log?.totalPoints ?? 0; + // We can assume a max "excellent" day is around 150 points for the progress ring scale + final percent = (points / 150).clamp(0.0, 1.0); + + // Prepare dynamic preview lines + int fardhuCompleted = 0; + if (log != null) { + fardhuCompleted = log.shalatLogs.values.where((l) => l.completed).length; + } + + String amalanText = 'Belum ada data'; + if (log != null) { + 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'); + if (aList.isNotEmpty) { + amalanText = aList.join(', '); + } + } + + return Container( + padding: const EdgeInsets.all(20), + 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: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Poin Ibadah Hari Ini', + style: Theme.of(context) + .textTheme + .titleMedium + ?.copyWith(fontWeight: FontWeight.w700)), + const SizedBox(height: 4), + Text( + 'Kumpulkan poin dengan konsisten!', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: isDark + ? AppColors.textSecondaryDark + : AppColors.textSecondaryLight, + ), + ), + ], + ), + ), + SizedBox( + width: 48, + height: 48, + child: Stack( + alignment: Alignment.center, + children: [ + CircularProgressIndicator( + value: percent, + strokeWidth: 4, + backgroundColor: + AppColors.primary.withValues(alpha: 0.15), + valueColor: const AlwaysStoppedAnimation( + AppColors.primary), + ), + Text( + '$points', + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w800, + color: AppColors.primary, + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 16), + _checklistPreviewItem( + context, isDark, 'Sholat Fardhu', '$fardhuCompleted dari 5 selesai', fardhuCompleted == 5), + const SizedBox(height: 8), + _checklistPreviewItem( + context, isDark, 'Amalan Selesai', amalanText, points > 50), + const SizedBox(height: 16), + GestureDetector( + onTap: () => context.go('/checklist'), + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 12), + decoration: BoxDecoration( + color: AppColors.primary.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(50), + ), + child: const Center( + child: Text( + 'Lihat Semua Checklist', + style: TextStyle( + color: AppColors.primary, + fontWeight: FontWeight.w600, + fontSize: 14, + ), + ), + ), + ), + ), + ], + ), + ); + } + + Widget _checklistPreviewItem(BuildContext context, bool isDark, String title, + String subtitle, bool completed) { + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: isDark + ? AppColors.primary.withValues(alpha: 0.05) + : AppColors.backgroundLight, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + Icon( + completed ? Icons.check_circle : Icons.radio_button_unchecked, + color: AppColors.primary, + size: 22, + ), + const SizedBox(width: 12), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, + style: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith(fontWeight: FontWeight.w600)), + Text(subtitle, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: isDark + ? AppColors.textSecondaryDark + : AppColors.textSecondaryLight, + )), + ], + ), + ], + ), + ); + } + + Widget _buildWeeklyProgress(BuildContext context, bool isDark) { + final box = Hive.box(HiveBoxes.worshipLogs); + final now = DateTime.now(); + + // Reverse so today is on the far right (index 6) + final last7Days = List.generate(7, (i) => now.subtract(Duration(days: 6 - i))); + final daysLabels = ['Sen', 'Sel', 'Rab', 'Kam', 'Jum', 'Sab', 'Min']; + + final weekPoints = []; + for (final d in last7Days) { + final k = DateFormat('yyyy-MM-dd').format(d); + final l = box.get(k); + weekPoints.add(l?.totalPoints ?? 0); + } + + // Find the max points acquired this week to scale the bars, with a minimum floor of 50 + final maxPts = weekPoints.reduce((a, b) => a > b ? a : b).clamp(50, 300); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Progres Poin Mingguan', + style: Theme.of(context) + .textTheme + .titleMedium + ?.copyWith(fontWeight: FontWeight.w700)), + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.all(20), + 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( + crossAxisAlignment: CrossAxisAlignment.end, + children: List.generate(7, (i) { + final val = weekPoints[i]; + final ratio = (val / maxPts).clamp(0.1, 1.0); + + return Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + height: 80, + child: Align( + alignment: Alignment.bottomCenter, + child: Container( + width: 24, + height: 80 * ratio, + decoration: BoxDecoration( + color: val > 0 + ? AppColors.primary.withValues( + alpha: 0.2 + ratio * 0.8) + : AppColors.primary.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(12), + ), + ), + ), + ), + const SizedBox(height: 8), + Text( + daysLabels[last7Days[i].weekday - 1], // Correct localized day + style: TextStyle( + fontSize: 10, + fontWeight: i == 6 ? FontWeight.w800 : FontWeight.w600, + color: i == 6 + ? AppColors.primary + : (isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight), + ), + ), + ], + ), + ), + ); + }), + ), + ), + ], + ); + } +} diff --git a/lib/features/dashboard/presentation/widgets/.gitkeep b/lib/features/dashboard/presentation/widgets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/lib/features/dashboard/presentation/widgets/placeholder.dart b/lib/features/dashboard/presentation/widgets/placeholder.dart new file mode 100644 index 0000000..1039d4c --- /dev/null +++ b/lib/features/dashboard/presentation/widgets/placeholder.dart @@ -0,0 +1 @@ +// TODO: implement diff --git a/lib/features/dzikir/presentation/.gitkeep b/lib/features/dzikir/presentation/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/lib/features/dzikir/presentation/dzikir_screen.dart b/lib/features/dzikir/presentation/dzikir_screen.dart new file mode 100644 index 0000000..0afc2cc --- /dev/null +++ b/lib/features/dzikir/presentation/dzikir_screen.dart @@ -0,0 +1,306 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.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'; + +class DzikirScreen extends ConsumerStatefulWidget { + const DzikirScreen({super.key}); + + @override + ConsumerState createState() => _DzikirScreenState(); +} + +class _DzikirScreenState extends ConsumerState + with SingleTickerProviderStateMixin { + late TabController _tabController; + List> _pagiItems = []; + List> _petangItems = []; + late Box _counterBox; + late String _todayKey; + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 2, vsync: this); + _counterBox = Hive.box(HiveBoxes.dzikirCounters); + _todayKey = DateFormat('yyyy-MM-dd').format(DateTime.now()); + _loadData(); + } + + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + + Future _loadData() async { + final pagiJson = + await rootBundle.loadString('assets/dzikir/dzikir_pagi.json'); + final petangJson = + await rootBundle.loadString('assets/dzikir/dzikir_petang.json'); + setState(() { + _pagiItems = List>.from(json.decode(pagiJson)); + _petangItems = List>.from(json.decode(petangJson)); + }); + } + + DzikirCounter _getCounter(String dzikirId, int target) { + final key = '${dzikirId}_$_todayKey'; + return _counterBox.get(key) ?? + DzikirCounter( + dzikirId: dzikirId, + date: _todayKey, + count: 0, + target: target, + ); + } + + void _increment(String dzikirId, int target) { + final key = '${dzikirId}_$_todayKey'; + var counter = _counterBox.get(key); + if (counter == null) { + counter = DzikirCounter( + dzikirId: dzikirId, + date: _todayKey, + count: 1, + target: target, + ); + _counterBox.put(key, counter); + } else { + if (counter.count < counter.target) { + counter.count++; + counter.save(); + } + } + setState(() {}); + // Haptic feedback + HapticFeedback.lightImpact(); + } + + @override + Widget build(BuildContext context) { + final isDark = Theme.of(context).brightness == Brightness.dark; + + return Scaffold( + appBar: AppBar( + title: const Text('Dzikir Pagi & Petang'), + actions: [ + IconButton( + onPressed: () {}, + icon: const Icon(Icons.info_outline), + ), + ], + ), + body: Column( + children: [ + // Tabs + Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + child: TabBar( + controller: _tabController, + labelColor: AppColors.primary, + unselectedLabelColor: isDark + ? AppColors.textSecondaryDark + : AppColors.textSecondaryLight, + indicatorColor: AppColors.primary, + indicatorWeight: 3, + labelStyle: + const TextStyle(fontWeight: FontWeight.w700, fontSize: 14), + tabs: const [ + Tab(text: 'Pagi'), + Tab(text: 'Petang'), + ], + ), + ), + Expanded( + child: TabBarView( + controller: _tabController, + children: [ + _buildDzikirList(context, isDark, _pagiItems, 'pagi', + 'Dzikir Pagi', 'Dibaca setelah shalat Shubuh hingga terbit matahari'), + _buildDzikirList(context, isDark, _petangItems, 'petang', + 'Dzikir Petang', 'Dibaca setelah shalat Ashar hingga terbenam matahari'), + ], + ), + ), + ], + ), + ); + } + + Widget _buildDzikirList(BuildContext context, bool isDark, + List> items, String prefix, String title, String subtitle) { + if (items.isEmpty) { + return const Center(child: CircularProgressIndicator()); + } + + return ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: items.length + 1, // +1 for header + itemBuilder: (context, index) { + if (index == 0) { + return Padding( + padding: const EdgeInsets.only(bottom: 20), + child: Column( + children: [ + Text(title, + style: const TextStyle( + fontSize: 22, fontWeight: FontWeight.w800)), + const SizedBox(height: 4), + Text( + subtitle, + textAlign: TextAlign.center, + style: TextStyle( + color: isDark + ? AppColors.textSecondaryDark + : AppColors.textSecondaryLight, + fontSize: 13, + ), + ), + ], + ), + ); + } + + final item = items[index - 1]; + final dzikirId = '${prefix}_${item['id']}'; + final target = (item['count'] as num?)?.toInt() ?? 1; + final counter = _getCounter(dzikirId, target); + final isComplete = counter.count >= counter.target; + + return Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: isDark ? AppColors.surfaceDark : AppColors.surfaceLight, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: isComplete + ? AppColors.primary.withValues(alpha: 0.3) + : (isDark + ? AppColors.primary.withValues(alpha: 0.08) + : AppColors.cream), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header row: count badge + number + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: AppColors.primary.withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(50), + ), + child: Text( + '$target KALI', + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.w700, + color: AppColors.primary, + ), + ), + ), + Text( + '${(index).toString().padLeft(2, '0')}', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: isDark + ? AppColors.textSecondaryDark + : AppColors.textSecondaryLight, + ), + ), + ], + ), + const SizedBox(height: 16), + // Arabic text + SizedBox( + width: double.infinity, + child: Text( + item['arabic'] ?? '', + textAlign: TextAlign.right, + style: const TextStyle( + fontFamily: 'Amiri', + fontSize: 24, + height: 2.0, + ), + ), + ), + const SizedBox(height: 12), + // Transliteration + Text( + item['transliteration'] ?? '', + style: TextStyle( + fontSize: 13, + fontStyle: FontStyle.italic, + color: AppColors.primary, + ), + ), + const SizedBox(height: 8), + // Translation + Text( + '"${item['translation'] ?? ''}"', + style: TextStyle( + fontSize: 13, + color: isDark + ? AppColors.textSecondaryDark + : AppColors.textSecondaryLight, + ), + ), + const SizedBox(height: 16), + // Counter button + GestureDetector( + onTap: () => _increment(dzikirId, target), + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 14), + decoration: BoxDecoration( + color: isComplete + ? AppColors.primary.withValues(alpha: 0.15) + : AppColors.primary, + borderRadius: BorderRadius.circular(50), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + isComplete ? Icons.check : Icons.touch_app, + size: 18, + color: isComplete + ? AppColors.primary + : AppColors.onPrimary, + ), + const SizedBox(width: 8), + Text( + '${counter.count} / $target', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w700, + color: isComplete + ? AppColors.primary + : AppColors.onPrimary, + ), + ), + ], + ), + ), + ), + ], + ), + ), + ); + }, + ); + } +} diff --git a/lib/features/dzikir/presentation/placeholder.dart b/lib/features/dzikir/presentation/placeholder.dart new file mode 100644 index 0000000..1039d4c --- /dev/null +++ b/lib/features/dzikir/presentation/placeholder.dart @@ -0,0 +1 @@ +// TODO: implement diff --git a/lib/features/imsakiyah/presentation/.gitkeep b/lib/features/imsakiyah/presentation/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/lib/features/imsakiyah/presentation/imsakiyah_screen.dart b/lib/features/imsakiyah/presentation/imsakiyah_screen.dart new file mode 100644 index 0000000..2089291 --- /dev/null +++ b/lib/features/imsakiyah/presentation/imsakiyah_screen.dart @@ -0,0 +1,557 @@ +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:intl/intl.dart'; +import '../../../app/theme/app_colors.dart'; +import '../../../data/local/hive_boxes.dart'; +import '../../../data/local/models/app_settings.dart'; +import '../../../data/services/prayer_service.dart'; +import '../../../data/services/myquran_sholat_service.dart'; +import '../../dashboard/data/prayer_times_provider.dart'; + +class ImsakiyahScreen extends ConsumerStatefulWidget { + const ImsakiyahScreen({super.key}); + + @override + ConsumerState createState() => _ImsakiyahScreenState(); +} + +class _ImsakiyahScreenState extends ConsumerState { + int _selectedMonthIndex = 0; + late List<_MonthOption> _months; + late AppSettings _settings; + + @override + void initState() { + super.initState(); + final box = Hive.box(HiveBoxes.settings); + _settings = box.get('default') ?? AppSettings(); + _months = _generateMonths(); + // Find current month + final now = DateTime.now(); + for (int i = 0; i < _months.length; i++) { + if (_months[i].month == now.month && _months[i].year == now.year) { + _selectedMonthIndex = i; + break; + } + } + } + + List<_MonthOption> _generateMonths() { + final now = DateTime.now(); + final list = <_MonthOption>[]; + for (int offset = -2; offset <= 3; offset++) { + final date = DateTime(now.year, now.month + offset, 1); + list.add(_MonthOption( + label: DateFormat('MMMM yyyy').format(date), + month: date.month, + year: date.year, + )); + } + return list; + } + + List<_DayRow> _createRows(Map>? apiData) { + final selected = _months[_selectedMonthIndex]; + final daysInMonth = + DateTime(selected.year, selected.month + 1, 0).day; + final rows = <_DayRow>[]; + + for (int d = 1; d <= daysInMonth; d++) { + final date = DateTime(selected.year, selected.month, d); + final dateStr = DateFormat('yyyy-MM-dd').format(date); + + if (apiData != null && apiData.containsKey(dateStr)) { + final times = apiData[dateStr]!; + rows.add(_DayRow( + date: date, + fajr: times['subuh'] ?? '-', + sunrise: times['terbit'] ?? '-', + dhuhr: times['dzuhur'] ?? '-', + asr: times['ashar'] ?? '-', + maghrib: times['maghrib'] ?? '-', + isha: times['isya'] ?? '-', + )); + } else { + final times = + PrayerService.instance.getPrayerTimes(-6.2088, 106.8456, date); + rows.add(_DayRow( + date: date, + fajr: DateFormat('HH:mm').format(times.fajr), + sunrise: DateFormat('HH:mm').format(times.sunrise), + dhuhr: DateFormat('HH:mm').format(times.dhuhr), + asr: DateFormat('HH:mm').format(times.asr), + maghrib: DateFormat('HH:mm').format(times.maghrib), + isha: DateFormat('HH:mm').format(times.isha), + )); + } + } + return rows; + } + + void _showLocationDialog(BuildContext context) { + final searchCtrl = TextEditingController(); + bool isSearching = false; + List> results = []; + + showDialog( + context: context, + builder: (ctx) => StatefulBuilder( + builder: (ctx, setDialogState) => AlertDialog( + insetPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 24), + title: const Text('Cari Kota/Kabupaten'), + content: SizedBox( + width: MediaQuery.of(context).size.width * 0.85, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + controller: searchCtrl, + autofocus: true, + decoration: InputDecoration( + hintText: 'Cth: Jakarta', + border: const OutlineInputBorder(), + suffixIcon: IconButton( + icon: const Icon(Icons.search), + onPressed: () async { + if (searchCtrl.text.trim().isEmpty) return; + setDialogState(() => isSearching = true); + final res = await MyQuranSholatService.instance + .searchCity(searchCtrl.text.trim()); + if (mounted) { + setDialogState(() { + results = res; + isSearching = false; + }); + } + }, + ), + ), + onSubmitted: (val) async { + if (val.trim().isEmpty) return; + setDialogState(() => isSearching = true); + final res = await MyQuranSholatService.instance + .searchCity(val.trim()); + if (mounted) { + setDialogState(() { + results = res; + isSearching = false; + }); + } + }, + ), + const SizedBox(height: 16), + if (isSearching) + const Center(child: CircularProgressIndicator()) + else if (results.isEmpty) + const Text('Tidak ada hasil', style: TextStyle(color: Colors.grey)) + else + SizedBox( + height: 200, + width: double.maxFinite, + child: ListView.builder( + shrinkWrap: true, + itemCount: results.length, + itemBuilder: (context, i) { + final city = results[i]; + return ListTile( + title: Text(city['lokasi'] ?? ''), + onTap: () { + final id = city['id']; + final name = city['lokasi']; + if (id != null && name != null) { + _settings.lastCityName = '$name|$id'; + _settings.save(); + + // Update providers to refresh data + ref.invalidate(selectedCityIdProvider); + ref.invalidate(cityNameProvider); + + Navigator.pop(ctx); + } + }, + ); + }, + ), + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx), + child: const Text('Batal'), + ), + ], + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final isDark = theme.brightness == Brightness.dark; + final today = DateTime.now(); + + final selectedMonth = _months[_selectedMonthIndex]; + final monthArg = '${selectedMonth.year}-${selectedMonth.month.toString().padLeft(2, '0')}'; + final cityNameAsync = ref.watch(cityNameProvider); + final monthlyDataAsync = ref.watch(monthlyScheduleProvider(monthArg)); + + return Scaffold( + appBar: AppBar( + title: const Text('Kalender Sholat'), + centerTitle: false, + actions: [ + IconButton( + onPressed: () {}, + icon: const Icon(Icons.notifications_outlined), + ), + IconButton( + onPressed: () => context.push('/settings'), + icon: const Icon(Icons.settings_outlined), + ), + const SizedBox(width: 8), + ], + ), + body: Column( + children: [ + // ── Month Selector ── + SizedBox( + height: 48, + child: ListView.separated( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + itemCount: _months.length, + separatorBuilder: (_, __) => const SizedBox(width: 8), + itemBuilder: (context, i) { + final isSelected = i == _selectedMonthIndex; + return GestureDetector( + onTap: () => setState(() => _selectedMonthIndex = i), + child: Container( + padding: + const EdgeInsets.symmetric(horizontal: 20, vertical: 8), + decoration: BoxDecoration( + color: isSelected + ? AppColors.primary + : (isDark ? AppColors.surfaceDark : AppColors.surfaceLight), + borderRadius: BorderRadius.circular(50), + border: isSelected + ? null + : Border.all( + color: isDark + ? AppColors.primary.withValues(alpha: 0.2) + : AppColors.cream, + ), + ), + child: Center( + child: Text( + _months[i].label, + style: TextStyle( + fontSize: 13, + fontWeight: + isSelected ? FontWeight.w600 : FontWeight.w400, + color: isSelected + ? AppColors.onPrimary + : (isDark + ? AppColors.textSecondaryDark + : AppColors.textSecondaryLight), + ), + ), + ), + ), + ); + }, + ), + ), + const SizedBox(height: 12), + + // ── Location Card ── + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: GestureDetector( + onTap: () => _showLocationDialog(context), + child: 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: [ + const Icon(Icons.location_on, + color: AppColors.primary, size: 24), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Lokasi Anda', + style: theme.textTheme.bodySmall?.copyWith( + color: isDark + ? AppColors.textSecondaryDark + : AppColors.textSecondaryLight, + ), + ), + Text( + cityNameAsync.value ?? 'Jakarta, Indonesia', + style: const TextStyle( + fontWeight: FontWeight.w600, fontSize: 15), + ), + ], + ), + ), + Icon(Icons.expand_more, + color: isDark + ? AppColors.textSecondaryDark + : AppColors.textSecondaryLight), + ], + ), + ), + ), + ), + const SizedBox(height: 16), + + // ── Table Header ── + Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + decoration: BoxDecoration( + color: AppColors.primary.withValues(alpha: 0.1), + borderRadius: + const BorderRadius.vertical(top: Radius.circular(12)), + ), + child: Row( + children: [ + _headerCell('TGL', flex: 4), + _headerCell('SUBUH', flex: 3), + _headerCell('SYURUQ', flex: 3), + _headerCell('DZUHUR', flex: 3), + _headerCell('ASHAR', flex: 3), + _headerCell('MAGH', flex: 3), + _headerCell('ISYA', flex: 3), + ], + ), + ), + + // ── Table Body ── + Expanded( + child: monthlyDataAsync.when( + data: (apiData) { + final rows = _createRows(apiData); + return ListView.builder( + padding: const EdgeInsets.symmetric(horizontal: 16), + itemCount: rows.length, + itemBuilder: (context, i) { + final row = rows[i]; + final isToday = row.date.day == today.day && + row.date.month == today.month && + row.date.year == today.year; + + return Container( + margin: const EdgeInsets.only(bottom: 4), + padding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 14), + decoration: BoxDecoration( + color: isToday + ? AppColors.primary + : (isDark + ? AppColors.surfaceDark + : AppColors.surfaceLight), + borderRadius: BorderRadius.circular(12), + border: isToday + ? null + : Border.all( + color: isDark + ? AppColors.primary.withValues(alpha: 0.05) + : AppColors.cream.withValues(alpha: 0.5), + ), + ), + child: Row( + children: [ + // Day column + Expanded( + flex: 4, + child: Column( + children: [ + Text( + DateFormat('MMM') + .format(row.date) + .toUpperCase(), + style: TextStyle( + fontSize: 9, + fontWeight: FontWeight.w700, + letterSpacing: 1, + color: isToday + ? AppColors.onPrimary + .withValues(alpha: 0.7) + : AppColors.sage, + ), + ), + Text( + '${row.date.day}', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w800, + color: isToday ? AppColors.onPrimary : null, + ), + ), + ], + ), + ), + _dataCell(row.fajr, isToday, flex: 3), + _dataCell(row.sunrise, isToday, flex: 3), + _dataCell(row.dhuhr, isToday, bold: true, flex: 3), + _dataCell(row.asr, isToday, flex: 3), + _dataCell(row.maghrib, isToday, bold: true, flex: 3), + _dataCell(row.isha, isToday, flex: 3), + ], + ), + ); + }, + ); + }, + loading: () => const Center(child: CircularProgressIndicator()), + error: (_, __) { + final rows = _createRows(null); // fallback + return ListView.builder( + padding: const EdgeInsets.symmetric(horizontal: 16), + itemCount: rows.length, + itemBuilder: (context, i) { + final row = rows[i]; + final isToday = row.date.day == today.day && + row.date.month == today.month && + row.date.year == today.year; + + return Container( + margin: const EdgeInsets.only(bottom: 4), + padding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 14), + decoration: BoxDecoration( + color: isToday + ? AppColors.primary + : (isDark + ? AppColors.surfaceDark + : AppColors.surfaceLight), + borderRadius: BorderRadius.circular(12), + border: isToday + ? null + : Border.all( + color: isDark + ? AppColors.primary.withValues(alpha: 0.05) + : AppColors.cream.withValues(alpha: 0.5), + ), + ), + child: Row( + children: [ + Expanded( + flex: 4, + child: Column( + children: [ + Text( + DateFormat('MMM') + .format(row.date) + .toUpperCase(), + style: TextStyle( + fontSize: 9, + fontWeight: FontWeight.w700, + letterSpacing: 1, + color: isToday + ? AppColors.onPrimary + .withValues(alpha: 0.7) + : AppColors.sage, + ), + ), + Text( + '${row.date.day}', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w800, + color: isToday ? AppColors.onPrimary : null, + ), + ), + ], + ), + ), + _dataCell(row.fajr, isToday, flex: 3), + _dataCell(row.sunrise, isToday, flex: 3), + _dataCell(row.dhuhr, isToday, bold: true, flex: 3), + _dataCell(row.asr, isToday, flex: 3), + _dataCell(row.maghrib, isToday, bold: true, flex: 3), + _dataCell(row.isha, isToday, flex: 3), + ], + ), + ); + }, + ); + }, + ), + ), + ], + ), + ); + } + + Widget _headerCell(String text, {int flex = 1}) { + return Expanded( + flex: flex, + child: Center( + child: Text( + text, + style: TextStyle( + fontSize: 9, + fontWeight: FontWeight.w700, + letterSpacing: 1, + color: AppColors.textSecondaryLight, + ), + ), + ), + ); + } + + Widget _dataCell(String value, bool isToday, + {bool bold = false, int flex = 1}) { + return Expanded( + flex: flex, + child: Center( + child: Text( + value, + style: TextStyle( + fontSize: 12, + fontWeight: bold ? FontWeight.w700 : FontWeight.w400, + color: isToday ? AppColors.onPrimary : null, + ), + ), + ), + ); + } +} + +class _MonthOption { + final String label; + final int month; + final int year; + _MonthOption({required this.label, required this.month, required this.year}); +} + +class _DayRow { + final DateTime date; + final String fajr, sunrise, dhuhr, asr, maghrib, isha; + _DayRow({ + required this.date, + required this.fajr, + required this.sunrise, + required this.dhuhr, + required this.asr, + required this.maghrib, + required this.isha, + }); +} diff --git a/lib/features/imsakiyah/presentation/placeholder.dart b/lib/features/imsakiyah/presentation/placeholder.dart new file mode 100644 index 0000000..1039d4c --- /dev/null +++ b/lib/features/imsakiyah/presentation/placeholder.dart @@ -0,0 +1 @@ +// TODO: implement diff --git a/lib/features/laporan/presentation/.gitkeep b/lib/features/laporan/presentation/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/lib/features/laporan/presentation/laporan_screen.dart b/lib/features/laporan/presentation/laporan_screen.dart new file mode 100644 index 0000000..a33fb23 --- /dev/null +++ b/lib/features/laporan/presentation/laporan_screen.dart @@ -0,0 +1,566 @@ +import 'package:flutter/material.dart'; +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 '../../../app/theme/app_colors.dart'; +import '../../../core/widgets/progress_bar.dart'; +import '../../../data/local/hive_boxes.dart'; +import '../../../data/local/models/daily_worship_log.dart'; +import '../../../data/local/models/checklist_item.dart'; + +class LaporanScreen extends ConsumerStatefulWidget { + const LaporanScreen({super.key}); + + @override + ConsumerState createState() => _LaporanScreenState(); +} + +class _LaporanScreenState extends ConsumerState + with SingleTickerProviderStateMixin { + late TabController _tabController; + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 3, vsync: this); + _tabController.addListener(() => setState(() {})); + } + + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + + /// Get the last 7 days' point data. + List<_DayData> _getWeeklyData() { + final logBox = Hive.box(HiveBoxes.worshipLogs); + final now = DateTime.now(); + final data = <_DayData>[]; + + for (int i = 6; i >= 0; i--) { + final date = now.subtract(Duration(days: i)); + final key = DateFormat('yyyy-MM-dd').format(date); + final log = logBox.get(key); + data.add(_DayData( + label: DateFormat('E').format(date).substring(0, 3), + value: (log?.totalPoints ?? 0).toDouble(), // Use points instead of % + isToday: i == 0, + )); + } + return data; + } + + /// Get average points for the week. + double _weekAverage(List<_DayData> data) { + if (data.isEmpty) return 0; + final sum = data.fold(0, (s, d) => s + d.value); + return sum / data.length; + } + + /// Find best and worst performing items. + _InsightPair _getInsights() { + final logBox = Hive.box(HiveBoxes.worshipLogs); + final now = DateTime.now(); + + final completionCounts = {}; + final totalCounts = {}; + int daysChecked = 0; + + for (int i = 0; i < 7; 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) { + daysChecked++; + + // Fardhu + totalCounts['fardhu'] = (totalCounts['fardhu'] ?? 0) + 5; + int completedFardhu = log.shalatLogs.values.where((l) => l.completed).length; + completionCounts['fardhu'] = (completionCounts['fardhu'] ?? 0) + completedFardhu; + + // Rawatib + int rawatibTotal = 0; + int rawatibCompleted = 0; + for (var sLog in log.shalatLogs.values) { + if (sLog.qabliyah != null) { rawatibTotal++; if (sLog.qabliyah!) rawatibCompleted++; } + if (sLog.badiyah != null) { rawatibTotal++; if (sLog.badiyah!) rawatibCompleted++; } + } + if (rawatibTotal > 0) { + totalCounts['rawatib'] = (totalCounts['rawatib'] ?? 0) + rawatibTotal; + completionCounts['rawatib'] = (completionCounts['rawatib'] ?? 0) + rawatibCompleted; + } + + // Tilawah + if (log.tilawahLog != null) { + totalCounts['tilawah'] = (totalCounts['tilawah'] ?? 0) + 1; + if (log.tilawahLog!.isCompleted) { + completionCounts['tilawah'] = (completionCounts['tilawah'] ?? 0) + 1; + } + } + + // Dzikir + if (log.dzikirLog != null) { + totalCounts['dzikir'] = (totalCounts['dzikir'] ?? 0) + 2; + int dCompleted = (log.dzikirLog!.pagi ? 1 : 0) + (log.dzikirLog!.petang ? 1 : 0); + completionCounts['dzikir'] = (completionCounts['dzikir'] ?? 0) + dCompleted; + } + + // Puasa + if (log.puasaLog != null) { + totalCounts['puasa'] = (totalCounts['puasa'] ?? 0) + 1; + if (log.puasaLog!.completed) { + completionCounts['puasa'] = (completionCounts['puasa'] ?? 0) + 1; + } + } + } + } + + if (daysChecked == 0 || totalCounts.isEmpty) { + return _InsightPair( + best: _InsightItem(title: 'Sholat Fardhu', percent: 0), + worst: _InsightItem(title: 'Belum Ada Data', percent: 0), + ); + } + + String bestId = totalCounts.keys.first; + String worstId = totalCounts.keys.first; + double bestRate = -1.0; + double worstRate = 2.0; + + for (final id in totalCounts.keys) { + final total = totalCounts[id]!; + final completed = completionCounts[id] ?? 0; + final rate = completed / total; + if (rate > bestRate) { + bestRate = rate; + bestId = id; + } + if (rate < worstRate) { + worstRate = rate; + worstId = id; + } + } + + final idToTitle = { + 'fardhu': 'Sholat Fardhu', + 'rawatib': 'Sholat Rawatib', + 'tilawah': 'Tilawah Quran', + 'dzikir': 'Dzikir Harian', + 'puasa': 'Puasa Sunnah', + }; + + return _InsightPair( + best: _InsightItem( + title: idToTitle[bestId] ?? bestId, + percent: (bestRate * 100).round(), + ), + worst: _InsightItem( + title: idToTitle[worstId] ?? worstId, + percent: (worstRate * 100).round(), + ), + ); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final isDark = theme.brightness == Brightness.dark; + final weekData = _getWeeklyData(); + final avgPercent = _weekAverage(weekData); + final insights = _getInsights(); + + return Scaffold( + appBar: AppBar( + title: const Text('Laporan Kualitas Ibadah'), + centerTitle: false, + actions: [ + IconButton( + onPressed: () {}, + icon: const Icon(Icons.notifications_outlined), + ), + IconButton( + onPressed: () => context.push('/settings'), + icon: const Icon(Icons.settings_outlined), + ), + const SizedBox(width: 8), + ], + ), + body: Column( + children: [ + // ── Tab Bar ── + Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: isDark + ? AppColors.primary.withValues(alpha: 0.1) + : AppColors.cream, + ), + ), + ), + child: TabBar( + controller: _tabController, + labelColor: AppColors.primary, + unselectedLabelColor: isDark + ? AppColors.textSecondaryDark + : AppColors.textSecondaryLight, + indicatorColor: AppColors.primary, + indicatorWeight: 3, + labelStyle: const TextStyle( + fontWeight: FontWeight.w700, + fontSize: 14, + ), + tabs: const [ + Tab(text: 'Mingguan'), + Tab(text: 'Bulanan'), + Tab(text: 'Tahunan'), + ], + ), + ), + // ── Tab Content ── + Expanded( + child: TabBarView( + controller: _tabController, + children: [ + _buildWeeklyView(context, isDark, weekData, avgPercent, insights), + _buildComingSoon(context, 'Bulanan'), + _buildComingSoon(context, 'Tahunan'), + ], + ), + ), + ], + ), + ); + } + + Widget _buildWeeklyView( + BuildContext context, + bool isDark, + List<_DayData> weekData, + double avgPercent, + _InsightPair insights, + ) { + return SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // ── Completion Card ── + Container( + width: double.infinity, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: isDark ? AppColors.surfaceDark : AppColors.surfaceLight, + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: isDark + ? AppColors.primary.withValues(alpha: 0.1) + : AppColors.cream, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Poin Rata-Rata Harian', + style: TextStyle( + fontSize: 13, + color: isDark + ? AppColors.textSecondaryDark + : AppColors.textSecondaryLight, + ), + ), + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: AppColors.primary.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(10), + ), + child: const Icon(Icons.stars, + color: AppColors.primary, size: 18), + ), + ], + ), + const SizedBox(height: 4), + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + '${avgPercent.round()} pt', + style: const TextStyle( + fontSize: 36, + fontWeight: FontWeight.w800, + height: 1.1, + ), + ), + ], + ), + const SizedBox(height: 20), + // ── Bar Chart ── + SizedBox( + height: 140, + child: Builder( + builder: (context) { + final maxPts = weekData.map((d) => d.value).fold(0.0, (a, b) => a > b ? a : b).clamp(50.0, 300.0); + + return Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: weekData.map((d) { + final ratio = (d.value / maxPts).clamp(0.05, 1.0); + return Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Flexible( + child: Container( + width: double.infinity, + height: 120 * ratio, + decoration: BoxDecoration( + color: d.isToday + ? AppColors.primary + : AppColors.primary + .withValues(alpha: 0.3 + ratio * 0.4), + borderRadius: BorderRadius.circular(6), + ), + ), + ), + const SizedBox(height: 8), + Text( + d.label, + style: TextStyle( + fontSize: 10, + fontWeight: d.isToday + ? FontWeight.w700 + : FontWeight.w400, + color: d.isToday + ? AppColors.primary + : (isDark + ? AppColors.textSecondaryDark + : AppColors.textSecondaryLight), + ), + ), + ], + ), + ), + ); + }).toList(), + ); + } + ), + ), + ], + ), + ), + const SizedBox(height: 24), + + // ── Insights ── + Text('Wawasan', + style: Theme.of(context) + .textTheme + .titleMedium + ?.copyWith(fontWeight: FontWeight.w700)), + const SizedBox(height: 12), + // Best performing + _insightCard( + context, + isDark, + icon: Icons.star, + iconBg: AppColors.primary.withValues(alpha: 0.15), + iconColor: AppColors.primary, + label: 'PALING RAJIN', + title: insights.best.title, + percent: insights.best.percent, + percentColor: AppColors.primary, + ), + const SizedBox(height: 10), + // Needs improvement + _insightCard( + context, + isDark, + icon: Icons.trending_up, + iconBg: const Color(0xFFFFF3E0), + iconColor: Colors.orange, + label: 'PERLU DITINGKATKAN', + title: insights.worst.title, + percent: insights.worst.percent, + percentColor: Colors.orange, + ), + const SizedBox(height: 24), + + // ── Motivational Quote ── + 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: [ + Text( + '❝', + style: TextStyle( + fontSize: 32, + color: AppColors.primary, + height: 0.8, + ), + ), + const SizedBox(height: 4), + Text( + '"Amal yang paling dicintai Allah adalah yang paling konsisten, meskipun sedikit."', + style: TextStyle( + fontSize: 15, + fontStyle: FontStyle.italic, + height: 1.5, + color: isDark ? Colors.white : Colors.black87, + ), + ), + const SizedBox(height: 12), + Text( + '— Shahih Bukhari', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: isDark + ? AppColors.textSecondaryDark + : AppColors.textSecondaryLight, + ), + ), + ], + ), + ), + const SizedBox(height: 24), + ], + ), + ); + } + + Widget _insightCard( + BuildContext context, + bool isDark, { + required IconData icon, + required Color iconBg, + required Color iconColor, + required String label, + required String title, + required int percent, + required Color percentColor, + }) { + 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: 44, + height: 44, + decoration: BoxDecoration( + color: iconBg, + borderRadius: BorderRadius.circular(12), + ), + child: Icon(icon, color: iconColor, size: 22), + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle( + fontSize: 9, + fontWeight: FontWeight.w700, + letterSpacing: 1.5, + color: isDark + ? AppColors.textSecondaryDark + : AppColors.textSecondaryLight, + ), + ), + const SizedBox(height: 2), + Text( + title, + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + Text( + '$percent%', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w800, + color: percentColor, + ), + ), + ], + ), + ); + } + + Widget _buildComingSoon(BuildContext context, String period) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.bar_chart, + 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', + style: TextStyle( + color: Theme.of(context).brightness == Brightness.dark + ? AppColors.textSecondaryDark + : AppColors.textSecondaryLight), + ), + ], + ), + ); + } +} + +class _DayData { + final String label; + final double value; + final bool isToday; + _DayData({required this.label, required this.value, this.isToday = false}); +} + +class _InsightItem { + final String title; + final int percent; + _InsightItem({required this.title, required this.percent}); +} + +class _InsightPair { + final _InsightItem best; + final _InsightItem worst; + _InsightPair({required this.best, required this.worst}); +} diff --git a/lib/features/laporan/presentation/placeholder.dart b/lib/features/laporan/presentation/placeholder.dart new file mode 100644 index 0000000..1039d4c --- /dev/null +++ b/lib/features/laporan/presentation/placeholder.dart @@ -0,0 +1 @@ +// TODO: implement diff --git a/lib/features/qibla/presentation/.gitkeep b/lib/features/qibla/presentation/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/lib/features/qibla/presentation/placeholder.dart b/lib/features/qibla/presentation/placeholder.dart new file mode 100644 index 0000000..1039d4c --- /dev/null +++ b/lib/features/qibla/presentation/placeholder.dart @@ -0,0 +1 @@ +// TODO: implement diff --git a/lib/features/qibla/presentation/qibla_screen.dart b/lib/features/qibla/presentation/qibla_screen.dart new file mode 100644 index 0000000..6c0da82 --- /dev/null +++ b/lib/features/qibla/presentation/qibla_screen.dart @@ -0,0 +1,391 @@ +import 'dart:io' show Platform; +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 '../../../app/theme/app_colors.dart'; + +class QiblaScreen extends ConsumerStatefulWidget { + const QiblaScreen({super.key}); + + @override + ConsumerState createState() => _QiblaScreenState(); +} + +class _QiblaScreenState extends ConsumerState { + // Fallback simulated data for environments without compass hardware (like macOS emulator) + double _qiblaAngle = 295.0; // Default Jakarta to Mecca + String _direction = 'NW'; + bool _hasHardwareSupport = false; + + late final Future _deviceSupport = _checkDeviceSupport(); + + Future _checkDeviceSupport() async { + if (Platform.isAndroid || Platform.isIOS) { + try { + return await FlutterQiblah.androidDeviceSensorSupport(); + } catch (e) { + return false; + } + } + return false; + } + + @override + void initState() { + super.initState(); + // Pre-calculate static fallback + _calculateStaticQibla(); + } + + void _calculateStaticQibla() { + // Default to Jakarta coordinates + const lat = -6.2088; + const lng = 106.8456; + // Mecca coordinates + const meccaLat = 21.4225; + const meccaLng = 39.8262; + + // Calculate qibla direction + final dLng = (meccaLng - lng) * math.pi / 180; + final lat1 = lat * math.pi / 180; + final lat2 = meccaLat * math.pi / 180; + + final y = math.sin(dLng) * math.cos(lat2); + final x = math.cos(lat1) * math.sin(lat2) - + math.sin(lat1) * math.cos(lat2) * math.cos(dLng); + + var bearing = math.atan2(y, x) * 180 / math.pi; + bearing = (bearing + 360) % 360; + + setState(() { + _qiblaAngle = bearing; + _updateDirectionText(bearing); + }); + } + + void _updateDirectionText(double angle) { + if (angle >= 337.5 || angle < 22.5) { + _direction = 'N'; + } else if (angle < 67.5) { + _direction = 'NE'; + } else if (angle < 112.5) { + _direction = 'E'; + } else if (angle < 157.5) { + _direction = 'SE'; + } else if (angle < 202.5) { + _direction = 'S'; + } else if (angle < 247.5) { + _direction = 'SW'; + } else if (angle < 292.5) { + _direction = 'W'; + } else { + _direction = 'NW'; + } + } + + @override + Widget build(BuildContext context) { + final isDark = Theme.of(context).brightness == Brightness.dark; + + return FutureBuilder( + future: _deviceSupport, + builder: (_, AsyncSnapshot snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Scaffold(body: Center(child: CircularProgressIndicator())); + } + + // If device has a compass sensor (true on physical phones) + if (snapshot.data == true) { + return _buildLiveQibla(context, isDark); + } + + // If device lacks compass (macOS/emulators) + return _buildSimulatedQibla(context, isDark); + }, + ); + } + + Widget _buildLiveQibla(BuildContext context, bool isDark) { + return StreamBuilder( + stream: FlutterQiblah.qiblahStream, + builder: (_, AsyncSnapshot snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Scaffold(body: Center(child: CircularProgressIndicator())); + } + + final qiblahDirection = snapshot.data; + if (qiblahDirection == null) { + return Scaffold(body: Center(child: Text('Menunggu sensor arah...', style: TextStyle(color: isDark ? Colors.white : Colors.black)))); + } + + _updateDirectionText(qiblahDirection.qiblah); + + return _buildQiblaLayout( + context: context, + isDark: isDark, + angleRad: qiblahDirection.qiblah * (math.pi / 180), + displayAngle: qiblahDirection.qiblah, + isLive: true, + ); + }, + ); + } + + Widget _buildSimulatedQibla(BuildContext context, bool isDark) { + return _buildQiblaLayout( + context: context, + isDark: isDark, + angleRad: _qiblaAngle * (math.pi / 180), + displayAngle: _qiblaAngle, + isLive: false, + ); + } + + Widget _buildQiblaLayout({ + required BuildContext context, + required bool isDark, + required double angleRad, + required double displayAngle, + required bool isLive, + }) { + + return Scaffold( + appBar: AppBar( + title: const Text('Qibla Finder'), + leading: IconButton( + icon: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: isDark + ? AppColors.surfaceDark + : AppColors.surfaceLight, + border: Border.all(color: AppColors.cream), + ), + child: const Icon(Icons.arrow_back, size: 18), + ), + onPressed: () => Navigator.pop(context), + ), + actions: [ + IconButton( + icon: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: isDark ? AppColors.surfaceDark : AppColors.surfaceLight, + border: Border.all(color: AppColors.cream), + ), + child: Icon(isLive ? Icons.my_location : Icons.location_disabled, size: 18), + ), + onPressed: () { + if (isLive) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Menggunakan sensor perangkat aktual')), + ); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Mode Simulasi: Hardware kompas tidak terdeteksi')), + ); + } + }, + ), + ], + ), + body: Container( + width: double.infinity, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: isDark + ? [AppColors.backgroundDark, AppColors.surfaceDark] + : [ + AppColors.backgroundLight, + AppColors.primary.withValues(alpha: 0.05), + ], + ), + ), + child: Column( + children: [ + const SizedBox(height: 32), + // Location label + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.location_on, + size: 16, color: AppColors.primary), + const SizedBox(width: 4), + Text( + 'Mecca, Saudi Arabia', + style: TextStyle( + fontSize: 14, + color: isDark + ? AppColors.textSecondaryDark + : AppColors.textSecondaryLight, + ), + ), + ], + ), + const SizedBox(height: 8), + // Degree + direction + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + '${displayAngle.round()}°', + style: const TextStyle( + fontSize: 48, + fontWeight: FontWeight.w800, + ), + ), + const SizedBox(width: 12), + Text( + _direction, + style: TextStyle( + fontSize: 48, + fontWeight: FontWeight.w800, + color: AppColors.primary, + ), + ), + ], + ), + const SizedBox(height: 32), + // Compass + Expanded( + child: Center( + child: SizedBox( + width: 300, + height: 300, + child: CustomPaint( + painter: _CompassPainter( + qiblaAngle: angleRad, + isDark: isDark, + ), + ), + ), + ), + ), + const SizedBox(height: 16), + // Calibration status + Container( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 10), + decoration: BoxDecoration( + color: isLive + ? AppColors.primary.withValues(alpha: 0.1) + : Colors.orange.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(50), + ), + child: Text( + isLive ? 'SENSOR AKTIF' : 'MODE SIMULASI (TIDAK ADA SENSOR)', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w700, + letterSpacing: 1.5, + color: isLive ? AppColors.primary : Colors.orange, + ), + ), + ), + const SizedBox(height: 48), + ], + ), + ), + ); + } +} + +class _CompassPainter extends CustomPainter { + final double qiblaAngle; + final bool isDark; + + _CompassPainter({required this.qiblaAngle, required this.isDark}); + + @override + void paint(Canvas canvas, Size size) { + final center = Offset(size.width / 2, size.height / 2); + final radius = size.width / 2 - 8; + + // Outer circle + final outerPaint = Paint() + ..color = AppColors.primary.withValues(alpha: 0.15) + ..style = PaintingStyle.stroke + ..strokeWidth = 2; + canvas.drawCircle(center, radius, outerPaint); + + // Inner dashed circle + final innerPaint = Paint() + ..color = AppColors.primary.withValues(alpha: 0.08) + ..style = PaintingStyle.stroke + ..strokeWidth = 1; + canvas.drawCircle(center, radius * 0.7, innerPaint); + + // Cross lines + final crossPaint = Paint() + ..color = AppColors.primary.withValues(alpha: 0.1) + ..strokeWidth = 1; + canvas.drawLine( + Offset(center.dx, center.dy - radius), + Offset(center.dx, center.dy + radius), + crossPaint); + canvas.drawLine( + Offset(center.dx - radius, center.dy), + Offset(center.dx + radius, center.dy), + crossPaint); + // Diagonals + final diagOffset = radius * 0.707; + canvas.drawLine( + Offset(center.dx - diagOffset, center.dy - diagOffset), + Offset(center.dx + diagOffset, center.dy + diagOffset), + crossPaint); + canvas.drawLine( + Offset(center.dx + diagOffset, center.dy - diagOffset), + Offset(center.dx - diagOffset, center.dy + diagOffset), + crossPaint); + + // Center dot + final centerDotPaint = Paint() + ..color = AppColors.primary.withValues(alpha: 0.3) + ..style = PaintingStyle.stroke + ..strokeWidth = 2; + canvas.drawCircle(center, 6, centerDotPaint); + + // Qibla direction line + final qiblaEndX = center.dx + radius * 0.85 * math.cos(qiblaAngle - math.pi / 2); + final qiblaEndY = center.dy + radius * 0.85 * math.sin(qiblaAngle - math.pi / 2); + + // Glow effect + final glowPaint = Paint() + ..color = AppColors.primary.withValues(alpha: 0.3) + ..strokeWidth = 6 + ..strokeCap = StrokeCap.round; + canvas.drawLine(center, Offset(qiblaEndX, qiblaEndY), glowPaint); + + // Main line + final linePaint = Paint() + ..color = AppColors.primary + ..strokeWidth = 3 + ..strokeCap = StrokeCap.round; + canvas.drawLine(center, Offset(qiblaEndX, qiblaEndY), linePaint); + + // Qibla icon circle at end + final iconPaint = Paint()..color = AppColors.primary; + canvas.drawCircle(Offset(qiblaEndX, qiblaEndY), 16, iconPaint); + + // Kaaba icon (simplified) + final kaabaPaint = Paint() + ..color = Colors.white + ..style = PaintingStyle.fill; + canvas.drawRect( + Rect.fromCenter( + center: Offset(qiblaEndX, qiblaEndY), + width: 12, + height: 12, + ), + kaabaPaint, + ); + } + + @override + bool shouldRepaint(covariant _CompassPainter oldDelegate) => + qiblaAngle != oldDelegate.qiblaAngle; +} diff --git a/lib/features/quran/presentation/.gitkeep b/lib/features/quran/presentation/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/lib/features/quran/presentation/placeholder.dart b/lib/features/quran/presentation/placeholder.dart new file mode 100644 index 0000000..1039d4c --- /dev/null +++ b/lib/features/quran/presentation/placeholder.dart @@ -0,0 +1 @@ +// TODO: implement diff --git a/lib/features/quran/presentation/quran_bookmarks_screen.dart b/lib/features/quran/presentation/quran_bookmarks_screen.dart new file mode 100644 index 0000000..53b47fd --- /dev/null +++ b/lib/features/quran/presentation/quran_bookmarks_screen.dart @@ -0,0 +1,323 @@ +import 'package:flutter/material.dart'; +import 'package:hive_flutter/hive_flutter.dart'; +import 'package:go_router/go_router.dart'; +import 'package:intl/intl.dart'; +import '../../../app/theme/app_colors.dart'; +import '../../../data/local/hive_boxes.dart'; +import '../../../data/local/models/quran_bookmark.dart'; +import '../../../data/local/models/app_settings.dart'; + +class QuranBookmarksScreen extends StatefulWidget { + const QuranBookmarksScreen({super.key}); + + @override + State createState() => _QuranBookmarksScreenState(); +} + +class _QuranBookmarksScreenState extends State { + bool _showLatin = true; + bool _showTerjemahan = true; + + @override + void initState() { + super.initState(); + final box = Hive.box(HiveBoxes.settings); + final settings = box.get('default') ?? AppSettings(); + _showLatin = settings.showLatin; + _showTerjemahan = settings.showTerjemahan; + } + + void _showDisplaySettings() { + showModalBottomSheet( + context: context, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + builder: (ctx) => StatefulBuilder( + builder: (context, setModalState) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 16), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Pengaturan Tampilan', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 16), + SwitchListTile( + title: const Text('Tampilkan Latin'), + value: _showLatin, + activeColor: AppColors.primary, + onChanged: (val) { + setModalState(() => _showLatin = val); + setState(() => _showLatin = val); + final box = Hive.box(HiveBoxes.settings); + final settings = box.get('default') ?? AppSettings(); + settings.showLatin = val; + settings.save(); + }, + ), + SwitchListTile( + title: const Text('Tampilkan Terjemahan'), + value: _showTerjemahan, + activeColor: AppColors.primary, + onChanged: (val) { + setModalState(() => _showTerjemahan = val); + setState(() => _showTerjemahan = val); + final box = Hive.box(HiveBoxes.settings); + final settings = box.get('default') ?? AppSettings(); + settings.showTerjemahan = val; + settings.save(); + }, + ), + const SizedBox(height: 16), + ], + ), + ); + }, + ), + ); + } + + @override + Widget build(BuildContext context) { + final isDark = Theme.of(context).brightness == Brightness.dark; + + return Scaffold( + appBar: AppBar( + title: const Text('Markah Al-Quran'), + centerTitle: false, + actions: [ + IconButton( + icon: const Icon(Icons.settings_display), + onPressed: _showDisplaySettings, + ), + ], + ), + body: ValueListenableBuilder( + valueListenable: Hive.box(HiveBoxes.bookmarks).listenable(), + builder: (context, Box box, _) { + if (box.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.bookmark_border, + size: 64, + color: AppColors.primary.withValues(alpha: 0.3), + ), + const SizedBox(height: 16), + Text( + 'Belum ada markah', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: isDark ? Colors.white : Colors.black87, + ), + ), + const SizedBox(height: 8), + Text( + 'Tandai ayat saat membaca Al-Quran', + style: TextStyle( + fontSize: 14, + color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight, + ), + textAlign: TextAlign.center, + ), + ], + ), + ); + } + + // Filter bookmarks + final allBookmarks = box.values.toList(); + final lastRead = allBookmarks.where((b) => b.isLastRead).toList(); + final favorites = allBookmarks.where((b) => !b.isLastRead).toList() + ..sort((a, b) => b.savedAt.compareTo(a.savedAt)); + + return ListView( + padding: const EdgeInsets.all(16), + children: [ + if (lastRead.isNotEmpty) ...[ + const Text( + 'TERAKHIR DIBACA', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w700, + letterSpacing: 1.5, + color: AppColors.sage, + ), + ), + const SizedBox(height: 12), + _buildBookmarkCard(context, lastRead.first, isDark, box, isLastRead: true), + const SizedBox(height: 24), + ], + + if (favorites.isNotEmpty) ...[ + const Text( + 'AYAT FAVORIT', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w700, + letterSpacing: 1.5, + color: AppColors.sage, + ), + ), + const SizedBox(height: 12), + ...favorites.map((fav) => Padding( + padding: const EdgeInsets.only(bottom: 12), + child: _buildBookmarkCard(context, fav, isDark, box, isLastRead: false), + )), + ], + ], + ); + }, + ), + ); + } + + Widget _buildBookmarkCard(BuildContext context, QuranBookmark bookmark, bool isDark, Box box, {required bool isLastRead}) { + final dateStr = DateFormat('dd MMM yyyy, HH:mm').format(bookmark.savedAt); + + return InkWell( + onTap: () => context.push('/tools/quran/${bookmark.surahId}?startVerse=${bookmark.verseId}'), + borderRadius: BorderRadius.circular(16), + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: isDark ? AppColors.surfaceDark : AppColors.surfaceLight, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: isLastRead + ? AppColors.primary.withValues(alpha: 0.3) + : (isDark ? AppColors.primary.withValues(alpha: 0.1) : AppColors.cream), + width: isLastRead ? 1.5 : 1.0, + ), + boxShadow: isLastRead ? [ + BoxShadow( + color: AppColors.primary.withValues(alpha: 0.05), + blurRadius: 10, + offset: const Offset(0, 4), + ) + ] : null, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: AppColors.primary.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (isLastRead) ...[ + const Icon(Icons.push_pin, size: 12, color: AppColors.primary), + const SizedBox(width: 4), + ], + Text( + 'QS. ${bookmark.surahName}: ${bookmark.verseId}', + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w700, + color: AppColors.primary, + ), + ), + ], + ), + ), + IconButton( + icon: const Icon(Icons.delete_outline, color: Colors.red, size: 20), + onPressed: () => box.delete(bookmark.key), + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + ), + ], + ), + const SizedBox(height: 16), + Align( + alignment: Alignment.centerRight, + child: Text( + bookmark.verseText, + textAlign: TextAlign.right, + style: const TextStyle( + fontFamily: 'Amiri', + fontSize: 22, + height: 1.8, + ), + ), + ), + + if (_showLatin && bookmark.verseLatin != null) ...[ + const SizedBox(height: 12), + Text( + bookmark.verseLatin!, + style: const TextStyle( + fontSize: 13, + fontStyle: FontStyle.italic, + color: AppColors.primary, + ), + ), + ], + + if (_showTerjemahan && bookmark.verseTranslation != null) ...[ + const SizedBox(height: 8), + Text( + bookmark.verseTranslation!, + style: TextStyle( + fontSize: 14, + height: 1.6, + color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight, + ), + ), + ], + + const SizedBox(height: 16), + + if (isLastRead) ...[ + SizedBox( + width: double.infinity, + child: FilledButton.icon( + onPressed: () => context.push('/tools/quran/${bookmark.surahId}?startVerse=${bookmark.verseId}'), + icon: const Icon(Icons.menu_book, size: 18), + label: const Text('Lanjutkan Membaca'), + style: FilledButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + ), + ), + ), + const SizedBox(height: 12), + ], + + Row( + children: [ + Icon( + Icons.access_time, + size: 12, + color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight, + ), + const SizedBox(width: 4), + Text( + '${isLastRead ? 'Ditandai' : 'Disimpan'}: $dateStr', + style: TextStyle( + fontSize: 10, + 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 new file mode 100644 index 0000000..9d0c235 --- /dev/null +++ b/lib/features/quran/presentation/quran_murattal_screen.dart @@ -0,0 +1,869 @@ +import 'dart:async'; +import 'dart:math'; +import 'dart:ui'; +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: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'; + +/// Quran Murattal (audio player) screen. +/// Implements full Surah playback using just_audio and EQuran v2 API. +class QuranMurattalScreen extends ConsumerStatefulWidget { + final String surahId; + final String? initialQariId; + final bool autoPlay; + const QuranMurattalScreen({ + super.key, + required this.surahId, + this.initialQariId, + this.autoPlay = false, + }); + + @override + ConsumerState createState() => + _QuranMurattalScreenState(); +} + +class _QuranMurattalScreenState extends ConsumerState { + final AudioPlayer _audioPlayer = AudioPlayer(); + + Map? _surahData; + bool _isLoading = true; + + // Audio State Variables + bool _isPlaying = false; + bool _isBuffering = false; + Duration _position = Duration.zero; + Duration _duration = Duration.zero; + + StreamSubscription? _positionSub; + StreamSubscription? _durationSub; + StreamSubscription? _playerStateSub; + + // Qari State + late String _selectedQariId; + + // Shuffle State + bool _isShuffleEnabled = false; + + // Unsplash Background + Map? _unsplashPhoto; + + @override + void initState() { + super.initState(); + _selectedQariId = widget.initialQariId ?? '05'; // Default to Misyari Rasyid Al-Afasi + _initDataAndPlayer(); + _loadUnsplashPhoto(); + } + + Future _loadUnsplashPhoto() async { + final photo = await UnsplashService.instance.getIslamicPhoto(); + if (mounted && photo != null) { + setState(() => _unsplashPhoto = photo); + } + } + + Future _initDataAndPlayer() async { + final surahNum = int.tryParse(widget.surahId) ?? 1; + final data = await EQuranService.instance.getSurah(surahNum); + + if (data != null && mounted) { + setState(() { + _surahData = data; + _isLoading = false; + }); + _setupAudioStreamListeners(); + _loadAudioSource(); + } else if (mounted) { + setState(() => _isLoading = false); + } + } + + void _setupAudioStreamListeners() { + _positionSub = _audioPlayer.positionStream.listen((pos) { + if (mounted) setState(() => _position = pos); + }); + + _durationSub = _audioPlayer.durationStream.listen((dur) { + if (mounted && dur != null) setState(() => _duration = dur); + }); + + _playerStateSub = _audioPlayer.playerStateStream.listen((state) { + if (!mounted) return; + setState(() { + _isPlaying = state.playing; + _isBuffering = state.processingState == ProcessingState.buffering || + state.processingState == ProcessingState.loading; + + // Auto pause and reset to 0 when finished + if (state.processingState == ProcessingState.completed) { + _audioPlayer.pause(); + _audioPlayer.seek(Duration.zero); + + // Auto-play next surah + final currentSurah = int.tryParse(widget.surahId) ?? 1; + if (_isShuffleEnabled) { + final random = Random(); + int nextSurah = random.nextInt(114) + 1; + while (nextSurah == currentSurah) { + nextSurah = random.nextInt(114) + 1; + } + _navigateToSurahNumber(nextSurah, autoplay: true); + } else if (currentSurah < 114) { + _navigateToSurah(1); + } + } + }); + }); + } + + Future _loadAudioSource() async { + if (_surahData == null) return; + + final audioUrls = _surahData!['audioFull']; + if (audioUrls != null && audioUrls[_selectedQariId] != null) { + try { + await _audioPlayer.setUrl(audioUrls[_selectedQariId]); + if (widget.autoPlay) { + _audioPlayer.play(); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Gagal memuat audio murattal')), + ); + } + } + } + } + + @override + void dispose() { + _positionSub?.cancel(); + _durationSub?.cancel(); + _playerStateSub?.cancel(); + _audioPlayer.dispose(); + super.dispose(); + } + + String _formatDuration(Duration d) { + final minutes = d.inMinutes.remainder(60).toString().padLeft(2, '0'); + final seconds = d.inSeconds.remainder(60).toString().padLeft(2, '0'); + if (d.inHours > 0) { + return '${d.inHours}:$minutes:$seconds'; + } + return '$minutes:$seconds'; + } + + void _seekRelative(int seconds) { + final newPosition = _position + Duration(seconds: seconds); + if (newPosition < Duration.zero) { + _audioPlayer.seek(Duration.zero); + } else if (newPosition > _duration) { + _audioPlayer.seek(_duration); + } else { + _audioPlayer.seek(newPosition); + } + } + + void _navigateToSurah(int direction) { + final currentSurah = int.tryParse(widget.surahId) ?? 1; + final nextSurah = currentSurah + direction; + _navigateToSurahNumber(nextSurah, autoplay: true); + } + + void _navigateToSurahNumber(int surahNum, {bool autoplay = false}) { + if (surahNum >= 1 && surahNum <= 114) { + context.pushReplacement('/tools/quran/$surahNum/murattal?qariId=$_selectedQariId&autoplay=$autoplay'); + } + } + + void _showQariSelector() { + showModalBottomSheet( + context: context, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + builder: (context) { + return SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 12), + Container( + width: 40, + height: 4, + decoration: BoxDecoration( + color: Colors.grey.withValues(alpha: 0.3), + borderRadius: BorderRadius.circular(2), + ), + ), + const SizedBox(height: 16), + const Text( + 'Pilih Qari', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + ...EQuranService.qariNames.entries.map((entry) { + final isSelected = entry.key == _selectedQariId; + return ListTile( + leading: Icon( + isSelected ? Icons.radio_button_checked : Icons.radio_button_unchecked, + color: isSelected ? AppColors.primary : Colors.grey, + ), + title: Text( + entry.value, + style: TextStyle( + fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, + color: isSelected ? AppColors.primary : null, + ), + ), + onTap: () { + Navigator.pop(context); + if (!isSelected) { + setState(() => _selectedQariId = entry.key); + _loadAudioSource(); + } + }, + ); + }), + const SizedBox(height: 16), + ], + ), + ); + }, + ); + } + + void _showSurahPlaylist() { + final currentSurah = int.tryParse(widget.surahId) ?? 1; + showModalBottomSheet( + context: context, + isScrollControlled: true, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + builder: (context) { + return DraggableScrollableSheet( + initialChildSize: 0.7, + minChildSize: 0.4, + maxChildSize: 0.9, + expand: false, + builder: (context, scrollController) { + return Column( + children: [ + const SizedBox(height: 12), + Container( + width: 40, + height: 4, + decoration: BoxDecoration( + color: Colors.grey.withValues(alpha: 0.3), + borderRadius: BorderRadius.circular(2), + ), + ), + const SizedBox(height: 16), + const Text( + 'Playlist Surah', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Expanded( + child: FutureBuilder>>( + future: EQuranService.instance.getAllSurahs(), + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const Center(child: CircularProgressIndicator()); + } + final surahs = snapshot.data!; + return ListView.builder( + controller: scrollController, + itemCount: surahs.length, + itemBuilder: (context, i) { + final surah = surahs[i]; + final surahNum = surah['nomor'] ?? (i + 1); + final isCurrentSurah = surahNum == currentSurah; + return ListTile( + leading: Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: isCurrentSurah + ? AppColors.primary + : AppColors.primary.withValues(alpha: 0.1), + shape: BoxShape.circle, + ), + child: Center( + child: Text( + '$surahNum', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: isCurrentSurah ? Colors.white : AppColors.primary, + ), + ), + ), + ), + title: Text( + surah['namaLatin'] ?? 'Surah $surahNum', + style: TextStyle( + fontWeight: isCurrentSurah ? FontWeight.bold : FontWeight.normal, + color: isCurrentSurah ? AppColors.primary : null, + ), + ), + subtitle: Text( + '${surah['arti'] ?? ''} • ${surah['jumlahAyat'] ?? 0} Ayat', + style: const TextStyle(fontSize: 12), + ), + trailing: isCurrentSurah + ? Icon(Icons.graphic_eq, color: AppColors.primary, size: 20) + : null, + onTap: () { + Navigator.pop(context); + if (!isCurrentSurah) { + context.pushReplacement( + '/tools/quran/$surahNum/murattal?qariId=$_selectedQariId', + ); + } + }, + ); + }, + ); + }, + ), + ), + ], + ); + }, + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + final isDark = Theme.of(context).brightness == Brightness.dark; + final surahName = _surahData?['namaLatin'] ?? 'Surah ${widget.surahId}'; + + final hasPhoto = _unsplashPhoto != null; + + return Scaffold( + extendBodyBehindAppBar: hasPhoto, + appBar: AppBar( + backgroundColor: hasPhoto ? Colors.transparent : null, + elevation: hasPhoto ? 0 : null, + iconTheme: hasPhoto ? const IconThemeData(color: Colors.white) : null, + flexibleSpace: hasPhoto + ? ClipRect( + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), + child: Container( + color: Colors.black.withValues(alpha: 0.2), + ), + ), + ) + : null, + title: Column( + children: [ + Text( + 'Surah $surahName', + style: TextStyle( + color: hasPhoto ? Colors.white : null, + ), + ), + Text( + 'MURATTAL', + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.w700, + letterSpacing: 1.5, + color: hasPhoto ? Colors.white70 : AppColors.primary, + ), + ), + ], + ), + + ), + body: _isLoading + ? const Center(child: CircularProgressIndicator()) + : Stack( + fit: StackFit.expand, + children: [ + // === FULL-BLEED BACKGROUND === + if (_unsplashPhoto != null) + CachedNetworkImage( + imageUrl: _unsplashPhoto!['imageUrl'] ?? '', + fit: BoxFit.cover, + placeholder: (context, url) => Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + AppColors.primary.withValues(alpha: 0.1), + AppColors.primary.withValues(alpha: 0.05), + ], + ), + ), + ), + errorWidget: (context, url, error) => Container( + color: isDark ? Colors.black : Colors.grey.shade100, + ), + ) + else + Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + AppColors.primary.withValues(alpha: 0.1), + AppColors.primary.withValues(alpha: 0.03), + ], + ), + ), + ), + + // Dark overlay + if (_unsplashPhoto != null) + Container( + color: Colors.black.withValues(alpha: 0.35), + ), + + // === CENTER CONTENT (Equalizer + Text) === + Positioned( + top: 0, + left: 0, + right: 0, + bottom: 280, // leave room for the player + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Equalizer circle + Container( + width: 220, + height: 220, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: RadialGradient( + colors: _unsplashPhoto != null + ? [ + Colors.white.withValues(alpha: 0.15), + Colors.white.withValues(alpha: 0.05), + ] + : [ + AppColors.primary.withValues(alpha: 0.2), + AppColors.primary.withValues(alpha: 0.05), + ], + ), + ), + child: Center( + child: Container( + width: 140, + height: 140, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: _unsplashPhoto != null + ? Colors.white.withValues(alpha: 0.1) + : AppColors.primary.withValues(alpha: 0.12), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: List.generate(7, (i) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 2), + child: _EqualizerBar( + isPlaying: _isPlaying, + index: i, + color: _unsplashPhoto != null + ? Colors.white + : AppColors.primary, + ), + ); + }), + ), + ), + ), + ), + const SizedBox(height: 32), + // Qari name + Text( + EQuranService.qariNames[_selectedQariId] ?? 'Memuat...', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w700, + color: _unsplashPhoto != null ? Colors.white : null, + ), + ), + const SizedBox(height: 4), + Text( + 'Memutar Surat $surahName', + style: TextStyle( + fontSize: 14, + color: _unsplashPhoto != null + ? Colors.white70 + : AppColors.primary, + ), + ), + ], + ), + ), + ), + + // === FROSTED GLASS PLAYER CONTROLS === + Positioned( + bottom: 0, + left: 0, + right: 0, + child: ClipRRect( + borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), + child: BackdropFilter( + filter: _unsplashPhoto != null + ? ImageFilter.blur(sigmaX: 20, sigmaY: 20) + : ImageFilter.blur(sigmaX: 0, sigmaY: 0), + child: Container( + padding: const EdgeInsets.fromLTRB(24, 16, 24, 48), + decoration: BoxDecoration( + color: _unsplashPhoto != null + ? Colors.white.withValues(alpha: 0.15) + : (isDark ? AppColors.surfaceDark : AppColors.surfaceLight), + borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), + border: _unsplashPhoto != null + ? Border( + top: BorderSide( + color: Colors.white.withValues(alpha: 0.2), + width: 0.5, + ), + ) + : null, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Progress slider + SliderTheme( + data: SliderTheme.of(context).copyWith( + trackHeight: 3, + thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 6), + ), + child: Slider( + value: _position.inMilliseconds.toDouble(), + max: _duration.inMilliseconds > 0 ? _duration.inMilliseconds.toDouble() : 1.0, + onChanged: (v) { + _audioPlayer.seek(Duration(milliseconds: v.round())); + }, + activeColor: _unsplashPhoto != null ? Colors.white : AppColors.primary, + inactiveColor: _unsplashPhoto != null + ? Colors.white.withValues(alpha: 0.2) + : AppColors.primary.withValues(alpha: 0.15), + ), + ), + // Time labels + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + _formatDuration(_position), + style: TextStyle( + fontSize: 12, + color: _unsplashPhoto != null + ? Colors.white70 + : (isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight), + ), + ), + Text( + _formatDuration(_duration), + style: TextStyle( + fontSize: 12, + color: _unsplashPhoto != null + ? Colors.white70 + : (isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight), + ), + ), + ], + ), + ), + const SizedBox(height: 16), + // Playback controls — Spotify-style 5-button row + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + // Shuffle + IconButton( + onPressed: () => setState(() => _isShuffleEnabled = !_isShuffleEnabled), + icon: Icon( + Icons.shuffle_rounded, + size: 24, + color: _isShuffleEnabled + ? (_unsplashPhoto != null ? Colors.white : AppColors.primary) + : (_unsplashPhoto != null + ? Colors.white54 + : (isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight)), + ), + ), + // Previous Surah + IconButton( + onPressed: (int.tryParse(widget.surahId) ?? 1) > 1 + ? () => _navigateToSurah(-1) + : null, + icon: Icon( + Icons.skip_previous_rounded, + size: 36, + color: (int.tryParse(widget.surahId) ?? 1) > 1 + ? (_unsplashPhoto != null ? Colors.white : (isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight)) + : Colors.grey.withValues(alpha: 0.2), + ), + ), + // Play/Pause + GestureDetector( + onTap: () { + if (_isPlaying) { + _audioPlayer.pause(); + } else { + _audioPlayer.play(); + } + }, + child: Container( + width: 64, + height: 64, + decoration: BoxDecoration( + color: _unsplashPhoto != null + ? Colors.white + : AppColors.primary, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: (_unsplashPhoto != null + ? Colors.white + : AppColors.primary) + .withValues(alpha: 0.3), + blurRadius: 16, + offset: const Offset(0, 4), + ), + ], + ), + child: _isBuffering + ? Padding( + padding: const EdgeInsets.all(18.0), + child: CircularProgressIndicator( + color: _unsplashPhoto != null + ? Colors.black87 + : Colors.white, + strokeWidth: 3, + ), + ) + : Icon( + _isPlaying + ? Icons.pause_rounded + : Icons.play_arrow_rounded, + size: 36, + color: _unsplashPhoto != null + ? Colors.black87 + : AppColors.onPrimary, + ), + ), + ), + // Next Surah + IconButton( + onPressed: (int.tryParse(widget.surahId) ?? 1) < 114 + ? () => _navigateToSurah(1) + : null, + icon: Icon( + Icons.skip_next_rounded, + size: 36, + color: (int.tryParse(widget.surahId) ?? 1) < 114 + ? (_unsplashPhoto != null ? Colors.white : (isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight)) + : Colors.grey.withValues(alpha: 0.2), + ), + ), + // Playlist + IconButton( + onPressed: _showSurahPlaylist, + icon: Icon( + Icons.playlist_play_rounded, + size: 28, + color: _unsplashPhoto != null + ? Colors.white70 + : (isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight), + ), + ), + ], + ), + const SizedBox(height: 16), + // Qari selector trigger + GestureDetector( + onTap: _showQariSelector, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 16, vertical: 10), + decoration: BoxDecoration( + color: _unsplashPhoto != null + ? Colors.white.withValues(alpha: 0.15) + : AppColors.primary.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(50), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.person, size: 16, + color: _unsplashPhoto != null ? Colors.white : AppColors.primary), + const SizedBox(width: 8), + Text( + EQuranService.qariNames[_selectedQariId] ?? 'Ganti Qari', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: _unsplashPhoto != null ? Colors.white : AppColors.primary, + ), + ), + const SizedBox(width: 4), + Icon(Icons.expand_more, + size: 16, + color: _unsplashPhoto != null ? Colors.white : AppColors.primary), + ], + ), + ), + ), + ], + ), + ), + ), + ), + ), + + // === ATTRIBUTION === + if (_unsplashPhoto != null) + Positioned( + bottom: 280, + left: 0, + right: 0, + child: GestureDetector( + onTap: () { + final url = _unsplashPhoto!['photographerUrl']; + if (url != null && url.isNotEmpty) { + launchUrl(Uri.parse('$url?utm_source=jamshalat_diary&utm_medium=referral')); + } + }, + child: Text( + '📷 ${_unsplashPhoto!['photographerName']} / Unsplash', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 10, + color: Colors.white.withValues(alpha: 0.6), + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ], + ), + ); + } +} + +/// Animated equalizer bar widget for the Murattal player. +class _EqualizerBar extends StatefulWidget { + final bool isPlaying; + final int index; + final Color color; + + const _EqualizerBar({ + required this.isPlaying, + required this.index, + required this.color, + }); + + @override + State<_EqualizerBar> createState() => _EqualizerBarState(); +} + +class _EqualizerBarState extends State<_EqualizerBar> + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _animation; + + // Each bar has a unique height range and speed for variety + static const _barConfigs = [ + [0.3, 0.9, 600], + [0.2, 1.0, 500], + [0.4, 0.8, 700], + [0.1, 1.0, 450], + [0.3, 0.9, 550], + [0.2, 0.85, 650], + [0.35, 0.95, 480], + ]; + + @override + void initState() { + super.initState(); + final config = _barConfigs[widget.index % _barConfigs.length]; + final minHeight = config[0] as double; + final maxHeight = config[1] as double; + final durationMs = (config[2] as num).toInt(); + + _controller = AnimationController( + vsync: this, + duration: Duration(milliseconds: durationMs), + ); + + _animation = Tween( + begin: minHeight, + end: maxHeight, + ).animate(CurvedAnimation( + parent: _controller, + curve: Curves.easeInOut, + )); + + if (widget.isPlaying) { + _controller.repeat(reverse: true); + } + } + + @override + void didUpdateWidget(covariant _EqualizerBar oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.isPlaying && !oldWidget.isPlaying) { + _controller.repeat(reverse: true); + } else if (!widget.isPlaying && oldWidget.isPlaying) { + _controller.animateTo(0.0, duration: const Duration(milliseconds: 300)); + } + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: _animation, + builder: (context, child) { + return Container( + width: 6, + height: 50 * _animation.value, + decoration: BoxDecoration( + color: widget.color.withValues(alpha: 0.6 + (_animation.value * 0.4)), + borderRadius: BorderRadius.circular(3), + ), + ); + }, + ); + } +} diff --git a/lib/features/quran/presentation/quran_reading_screen.dart b/lib/features/quran/presentation/quran_reading_screen.dart new file mode 100644 index 0000000..f6340ac --- /dev/null +++ b/lib/features/quran/presentation/quran_reading_screen.dart @@ -0,0 +1,1027 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:just_audio/just_audio.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/quran_bookmark.dart'; +import '../../../data/local/models/app_settings.dart'; +import '../../../data/local/models/daily_worship_log.dart'; +import '../../../data/local/models/tilawah_log.dart'; +import '../../../data/services/equran_service.dart'; +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}); + + @override + ConsumerState createState() => _QuranReadingScreenState(); +} + +class _QuranReadingScreenState extends ConsumerState { + Map? _surah; + List> _verses = []; + bool _loading = true; + bool _autoSyncEnabled = false; + String _targetUnit = 'Juz'; + final ScrollController _scrollController = ScrollController(); + final Map _verseKeys = {}; + + final AudioPlayer _audioPlayer = AudioPlayer(); + int? _playingVerseIndex; + bool _isAudioLoading = false; + + // Display Settings + bool _showLatin = true; + bool _showTerjemahan = true; + + // Hafalan State + bool _isHafalanMode = false; + int _hafalanStartAyat = 1; + int _hafalanEndAyat = 1; + int _hafalanLoopCount = 1; // 0 = Tak Terbatas + int _currentLoop = 0; + bool _isHafalanPlaying = false; + StreamSubscription? _playerStateSubscription; + + @override + void initState() { + super.initState(); + _loadSurah(); + + final settingsBox = Hive.box(HiveBoxes.settings); + final settings = settingsBox.get('default') ?? AppSettings(); + _autoSyncEnabled = settings.tilawahAutoSync; + _targetUnit = settings.tilawahTargetUnit; + _showLatin = settings.showLatin; + _showTerjemahan = settings.showTerjemahan; + + _playerStateSubscription = _audioPlayer.playerStateStream.listen((state) { + if (state.processingState == ProcessingState.completed) { + if (mounted) { + if (_isHafalanMode && _isHafalanPlaying && _playingVerseIndex != null) { + _handleHafalanNext(); + } else { + setState(() { + _playingVerseIndex = null; + _isAudioLoading = false; + }); + } + } + } + }); + } + + void _handleHafalanNext() { + // Current verse index is 0-based. EndAyat is 1-based. + final endIdx = _hafalanEndAyat - 1; + final startIdx = _hafalanStartAyat - 1; + + if (_playingVerseIndex! < endIdx) { + // Move to next ayat in sequence + final nextIdx = _playingVerseIndex! + 1; + final audioUrl = _verses[nextIdx]['audio']?['05'] as String?; + if (audioUrl != null) { + _playAudio(nextIdx, audioUrl); + _attemptScroll(nextIdx, attempts: 0); + } else { + _stopHafalan(); + } + } else { + // Reached the end of the sequence. Loop or Stop. + _currentLoop++; + if (_hafalanLoopCount == 0 || _currentLoop < _hafalanLoopCount) { + // Loop again! + final audioUrl = _verses[startIdx]['audio']?['05'] as String?; + if (audioUrl != null) { + _playAudio(startIdx, audioUrl); + _attemptScroll(startIdx, attempts: 0); + } else { + _stopHafalan(); + } + } else { + // Finished all loops + _stopHafalan(); + } + } + } + + void _stopHafalan({bool isDisposing = false}) { + _audioPlayer.stop(); + if (isDisposing) return; + if (mounted) { + setState(() { + _isHafalanPlaying = false; + _playingVerseIndex = null; + _isAudioLoading = false; + _currentLoop = 0; + }); + } + } + + @override + void dispose() { + _playerStateSubscription?.cancel(); + _stopHafalan(isDisposing: true); + _audioPlayer.dispose(); + _scrollController.dispose(); + super.dispose(); + } + + Future _loadSurah() async { + final surahNum = int.tryParse(widget.surahId) ?? 1; + final data = await EQuranService.instance.getSurah(surahNum); + if (data != null) { + setState(() { + _surah = data; + final ayatList = List>.from(data['ayat'] ?? []); + _verses = ayatList; + _verseKeys.clear(); + for (int i = 0; i < ayatList.length; i++) { + _verseKeys[i] = GlobalKey(); + } + _loading = false; + }); + _scrollToInitialVerse(); + } else { + setState(() => _loading = false); + } + } + + void _scrollToInitialVerse() { + if (widget.initialVerse != null && widget.initialVerse! > 0) { + final targetIndex = widget.initialVerse! - 1; + if (targetIndex >= 0 && targetIndex < _verses.length) { + // Wait for next frame so the UI finishes building the keys + WidgetsBinding.instance.addPostFrameCallback((_) { + _attemptScroll(targetIndex, attempts: 0); + }); + } + } + } + + void _attemptScroll(int targetIndex, {required int attempts}) { + if (!mounted) return; + + final key = _verseKeys[targetIndex]; + if (key != null && key.currentContext != null) { + // It's built! Scroll directly to it. + Scrollable.ensureVisible( + key.currentContext!, + duration: const Duration(milliseconds: 400), + curve: Curves.easeInOut, + alignment: 0.1, // Aligns slightly below the very top of the screen + ); + } else if (attempts < 15) { + // It's not built yet. We manually nudge the scroll window. + if (_scrollController.hasClients) { + int firstBuiltIndex = -1; + for (int i = 0; i < _verses.length; i++) { + if (_verseKeys[i]?.currentContext != null) { + firstBuiltIndex = i; + break; + } + } + + final currentOffset = _scrollController.offset; + final isScrollingUp = firstBuiltIndex != -1 && targetIndex < firstBuiltIndex; + final scrollAmount = isScrollingUp ? -1000.0 : 1000.0; + + final maxScroll = _scrollController.position.maxScrollExtent; + final newOffset = (currentOffset + scrollAmount).clamp(0.0, maxScroll); + + _scrollController.jumpTo(newOffset); + + // Wait a frame for items to build, then try again + WidgetsBinding.instance.addPostFrameCallback((_) { + _attemptScroll(targetIndex, attempts: attempts + 1); + }); + } + } + } + + Future _playAudio(int verseIndex, String audioUrl) async { + if (_playingVerseIndex == verseIndex) { + await _audioPlayer.stop(); + if (mounted) { + setState(() { + _playingVerseIndex = null; + _isAudioLoading = false; + }); + } + return; + } + + if (mounted) { + setState(() { + _playingVerseIndex = verseIndex; + _isAudioLoading = true; + }); + } + + try { + await _audioPlayer.setUrl(audioUrl); + _audioPlayer.play(); + if (mounted) { + setState(() { + _isAudioLoading = false; + }); + } + } catch (e) { + if (mounted) { + setState(() { + _playingVerseIndex = null; + _isAudioLoading = false; + }); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Text('Gagal memuat audio ayat ini'), + backgroundColor: Colors.red.shade400, + ), + ); + } + } + } + + Future _showBookmarkOptions(int verseIndex) async { + if (_surah == null || verseIndex >= _verses.length) return; + + final verse = _verses[verseIndex]; + final surahId = _surah!['nomor'] ?? 1; + final verseId = verseIndex + 1; + + showModalBottomSheet( + context: context, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + builder: (ctx) => SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + margin: const EdgeInsets.only(top: 8, bottom: 8), + width: 40, + height: 4, + decoration: BoxDecoration( + color: Colors.grey.withValues(alpha: 0.3), + borderRadius: BorderRadius.circular(2), + ), + ), + ListTile( + leading: const Icon(Icons.push_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: () { + Navigator.pop(ctx); + _saveBookmark(surahId, verseId, verse, isLastRead: true); + }, + ), + const Divider(height: 1), + ListTile( + leading: const Icon(Icons.favorite, 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: () { + Navigator.pop(ctx); + _saveBookmark(surahId, verseId, verse, isLastRead: false); + }, + ), + const SizedBox(height: 8), + ], + ), + ), + ); + } + + Future _saveBookmark(int surahId, int verseId, Map verse, {required bool isLastRead}) async { + final box = Hive.box(HiveBoxes.bookmarks); + + // If setting as Last Read, we must clear any prior Last Read flags globally + if (isLastRead) { + final keysToDelete = []; + for (final key in box.keys) { + final b = box.get(key); + if (b != null && b.isLastRead) { + keysToDelete.add(key); + } + } + await box.deleteAll(keysToDelete); + } + + // Save the new bookmark + final bookmark = QuranBookmark( + surahId: surahId, + verseId: verseId, + surahName: _surah!['namaLatin'] ?? _surah!['nama'] ?? '', + verseText: verse['teksArab'] ?? '', + savedAt: DateTime.now(), + isLastRead: isLastRead, + verseLatin: verse['teksLatin'], + verseTranslation: verse['teksIndonesia'], + ); + + // Create a unique key. If favoriting an ayat that's already favorite, it overwrites. + // If the ayat is LastRead, we give it a special key so it can coexist with a favorite copy if they want. + final keySuffix = isLastRead ? '_lastread' : ''; + await box.put('${surahId}_$verseId$keySuffix', bookmark); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(isLastRead ? 'Disimpan sebagai Terakhir Dibaca' : 'Disimpan ke Favorit'), + backgroundColor: isLastRead ? AppColors.primary : Colors.pink, + duration: const Duration(seconds: 1), + ), + ); + } + } Future _showEndTrackingDialog(TilawahSession session, int endVerseId) async { + final endSurahId = _surah!['nomor'] ?? 1; + final endSurahName = _surah!['namaLatin'] ?? ''; + + int calculatedAyat = 0; + + if (session.startSurahId == endSurahId) { + // Same surah + calculatedAyat = (endVerseId - session.startVerseId).abs() + 1; + } else { + // Cross surah calculation + final allSurahs = await EQuranService.instance.getAllSurahs(); + if (allSurahs.isNotEmpty) { + int startSurahIdx = allSurahs.indexWhere((s) => s['nomor'] == session.startSurahId); + int endSurahIdx = allSurahs.indexWhere((s) => s['nomor'] == endSurahId); + + // Ensure chronological calculation + if (startSurahIdx > endSurahIdx) { + final tempIdx = startSurahIdx; startSurahIdx = endSurahIdx; endSurahIdx = tempIdx; + } + + final startSurahData = allSurahs[startSurahIdx]; + final int totalAyatInStart = (startSurahData['jumlahAyat'] as num?)?.toInt() ?? 1; + + calculatedAyat += (totalAyatInStart - session.startVerseId) + 1; // Ayats inside StartSurah + + for (int i = startSurahIdx + 1; i < endSurahIdx; i++) { + calculatedAyat += (allSurahs[i]['jumlahAyat'] as int? ?? 0); // Intermediate Surahs + } + + calculatedAyat += endVerseId; // Ayats inside EndSurah + } else { + calculatedAyat = 1; // Fallback + } + } + + if (!mounted) return; + + showDialog( + context: context, + barrierDismissible: false, + builder: (ctx) => AlertDialog( + title: const Text('Catat Sesi Tilawah', style: TextStyle(fontWeight: FontWeight.bold)), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: AppColors.primary.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('Mulai:', style: TextStyle(fontSize: 13)), + Text('${session.startSurahName} : ${session.startVerseId}', style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13)), + ] + ), + const Padding(padding: EdgeInsets.symmetric(vertical: 4), child: Divider()), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('Selesai:', style: TextStyle(fontSize: 13)), + Text('$endSurahName : $endVerseId', style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13)), + ] + ), + ] + ) + ), + const SizedBox(height: 16), + Row( + children: [ + const Icon(Icons.auto_stories, size: 20, color: AppColors.primary), + const SizedBox(width: 8), + Text('Total Dibaca: $calculatedAyat Ayat', style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 15)), + ], + ), + ], + ), + actions: [ + TextButton( + onPressed: () { + ref.invalidate(tilawahTrackingProvider); + Navigator.pop(ctx); + }, + child: const Text('Batal', style: TextStyle(color: Colors.red)), + ), + FilledButton( + onPressed: () { + final todayKey = DateFormat('yyyy-MM-dd').format(DateTime.now()); + final logBox = Hive.box(HiveBoxes.worshipLogs); + var log = logBox.get(todayKey); + + if (log == null) { + log = DailyWorshipLog(date: todayKey); + logBox.put(todayKey, log); + } + + if (log.tilawahLog == null) { + final settingsBox = Hive.box(HiveBoxes.settings); + final settings = settingsBox.get('default') ?? AppSettings(); + log.tilawahLog = TilawahLog( + targetValue: settings.tilawahTargetValue, + targetUnit: settings.tilawahTargetUnit, + autoSync: settings.tilawahAutoSync, + ); + } + + log.tilawahLog!.rawAyatRead += calculatedAyat; + log.save(); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('$calculatedAyat Ayat dicatat!'), + backgroundColor: AppColors.primary, + duration: const Duration(seconds: 2), + ), + ); + } + + ref.invalidate(tilawahTrackingProvider); + Navigator.pop(ctx); + }, + child: const Text('Simpan'), + ), + ], + ), + ); + } + + void _showDisplaySettings() { + showModalBottomSheet( + context: context, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + builder: (ctx) => StatefulBuilder( + builder: (context, setModalState) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 16), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Pengaturan Tampilan', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 16), + SwitchListTile( + title: const Text('Tampilkan Latin'), + value: _showLatin, + activeColor: AppColors.primary, + onChanged: (val) { + setModalState(() => _showLatin = val); + setState(() => _showLatin = val); + final box = Hive.box(HiveBoxes.settings); + final settings = box.get('default') ?? AppSettings(); + settings.showLatin = val; + settings.save(); + }, + ), + SwitchListTile( + title: const Text('Tampilkan Terjemahan'), + value: _showTerjemahan, + activeColor: AppColors.primary, + onChanged: (val) { + setModalState(() => _showTerjemahan = val); + setState(() => _showTerjemahan = val); + final box = Hive.box(HiveBoxes.settings); + final settings = box.get('default') ?? AppSettings(); + settings.showTerjemahan = val; + settings.save(); + }, + ), + const SizedBox(height: 16), + ], + ), + ); + }, + ), + ); + } + + @override + Widget build(BuildContext context) { + final trackingSession = ref.watch(tilawahTrackingProvider); + final isDark = Theme.of(context).brightness == Brightness.dark; + final totalVerses = _verses.length; + final surahName = _surah?['namaLatin'] ?? 'Memuat...'; + final surahArti = _surah?['arti'] ?? ''; + final tempatTurun = _surah?['tempatTurun'] ?? ''; + + return Scaffold( + appBar: AppBar( + title: Column( + children: [ + Text(surahName), + if (totalVerses > 0) + Text( + '$surahArti • $totalVerses AYAT • $tempatTurun'.toUpperCase(), + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.w700, + letterSpacing: 1.2, + color: AppColors.primary, + ), + ), + ], + ), + actions: [ + IconButton( + icon: Icon( + Icons.psychology, + color: _isHafalanMode ? AppColors.primary : (isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight), + ), + tooltip: 'Mode Hafalan', + onPressed: () { + setState(() { + _isHafalanMode = !_isHafalanMode; + if (!_isHafalanMode) { + _stopHafalan(); + } + }); + }, + ), + IconButton( + icon: const Icon(Icons.settings_display), + onPressed: _showDisplaySettings, + ), + ], + ), + + body: _loading + ? const Center(child: CircularProgressIndicator()) + : _verses.isEmpty + ? const Center(child: Text('Tidak dapat memuat surah')) + : Column( + children: [ + // Progress bar + LinearProgressIndicator( + value: 1.0, + backgroundColor: + AppColors.primary.withValues(alpha: 0.1), + valueColor: + AlwaysStoppedAnimation(AppColors.primary), + minHeight: 3, + ), + // Bismillah (skip for At-Tawbah) + if ((_surah?['nomor'] ?? 1) != 9) + Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Column( + children: [ + const Text( + 'بِسْمِ اللّٰهِ الرَّحْمٰنِ الرَّحِيْمِ', + style: TextStyle( + fontFamily: 'Amiri', + fontSize: 26, + ), + ), + const SizedBox(height: 4), + Text( + '"Dengan nama Allah Yang Maha Pengasih, Maha Penyayang."', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontStyle: FontStyle.italic, + color: isDark + ? AppColors.textSecondaryDark + : AppColors.textSecondaryLight, + ), + ), + ], + ), + ), + // Verse list + Expanded( + child: ListView.separated( + controller: _scrollController, + padding: const EdgeInsets.symmetric(vertical: 16), + itemCount: _verses.length, + separatorBuilder: (_, __) => Padding( + padding: + const EdgeInsets.symmetric(vertical: 16, horizontal: 16), + child: Row( + children: [ + Expanded( + child: Divider( + color: isDark + ? AppColors.primary + .withValues(alpha: 0.1) + : AppColors.cream, + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 8), + child: Icon(Icons.diamond, + size: 10, + color: AppColors.primary + .withValues(alpha: 0.3)), + ), + Expanded( + child: Divider( + color: isDark + ? AppColors.primary + .withValues(alpha: 0.1) + : AppColors.cream, + ), + ), + ], + ), + ), + itemBuilder: (context, i) { + final verse = _verses[i]; + final surahId = _surah!['nomor'] ?? 1; + final verseId = (verse['nomorAyat'] ?? (i + 1)) as int; + final lastReadKey = '${surahId}_${verseId}_lastread'; + final favKey = '${surahId}_$verseId'; + + return ValueListenableBuilder( + valueListenable: Hive.box(HiveBoxes.bookmarks).listenable(), + builder: (context, box, _) { + final isLastRead = box.containsKey(lastReadKey); + final isFav = box.containsKey(favKey); + final isPlayingThis = _playingVerseIndex == i; + final isHighlighted = isLastRead || isPlayingThis; + + return Container( + key: _verseKeys[i], + color: isHighlighted + ? AppColors.primary.withValues(alpha: 0.1) + : Colors.transparent, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Action row + Row( + children: [ + Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: AppColors.primary + .withValues(alpha: 0.1), + shape: BoxShape.circle, + ), + child: Center( + child: Text( + '${verse['nomorAyat'] ?? i + 1}', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w700, + color: AppColors.primary, + ), + ), + ), + ), + const Spacer(), + Builder( + builder: (context) { + final audioUrl = verse['audio']?['05'] as String?; + final isPlayingThis = _playingVerseIndex == i; + return IconButton( + onPressed: (audioUrl != null && audioUrl.isNotEmpty) + ? () => _playAudio(i, audioUrl) + : null, + icon: isPlayingThis + ? (_isAudioLoading + ? SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + color: AppColors.primary, + ), + ) + : Icon(Icons.stop_circle, color: AppColors.primary, size: 24)) + : Icon(Icons.play_circle_outline, + color: isDark + ? AppColors.textSecondaryDark + : AppColors.textSecondaryLight, + size: 20), + ); + } + ), + if (_autoSyncEnabled) + IconButton( + onPressed: () { + if (trackingSession == null) { + ref.read(tilawahTrackingProvider.notifier).startTracking( + surahId: _surah!['nomor'] ?? 1, + surahName: _surah!['namaLatin'] ?? '', + verseId: verse['nomorAyat'] ?? (i + 1), + ); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Sesi Tilawah dimulai'), + backgroundColor: AppColors.primary, + duration: Duration(seconds: 1), + ), + ); + } else { + _showEndTrackingDialog(trackingSession, verse['nomorAyat'] ?? (i + 1)); + } + }, + icon: Icon( + trackingSession == null + ? Icons.flag_outlined + : Icons.stop_circle, + color: trackingSession == null + ? (isDark + ? AppColors.textSecondaryDark + : AppColors.textSecondaryLight) + : Colors.red, + size: 20), + ), + IconButton( + onPressed: () => _showBookmarkOptions(i), + icon: Icon( + isLastRead ? Icons.push_pin : (isFav ? Icons.favorite : Icons.bookmark_outline), + color: isLastRead + ? AppColors.primary + : (isFav ? Colors.pink : (isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight)), + size: 20 + ), + ), + ], + ), + const SizedBox(height: 12), + // Arabic text + SizedBox( + width: double.infinity, + child: Text( + verse['teksArab'] ?? '', + textAlign: TextAlign.right, + style: const TextStyle( + fontFamily: 'Amiri', + fontSize: 26, + height: 2.0, + ), + ), + ), + if (_showLatin) ...[ + const SizedBox(height: 8), + // Latin transliteration + Text( + verse['teksLatin'] ?? '', + style: const TextStyle( + fontSize: 13, + fontStyle: FontStyle.italic, + color: AppColors.primary, + ), + ), + ], + if (_showTerjemahan) ...[ + const SizedBox(height: 8), + // Indonesian translation + Text( + verse['teksIndonesia'] ?? '', + style: TextStyle( + fontSize: 14, + height: 1.6, + color: isDark + ? AppColors.textSecondaryDark + : AppColors.textSecondaryLight, + ), + ), + ], + ], + ), + ); + }, + ); + }, + ), + ), + ], + ), + bottomNavigationBar: _isHafalanMode ? _buildHafalanControlBar() : null, + ); + } + + Widget _buildHafalanControlBar() { + final isDark = Theme.of(context).brightness == Brightness.dark; + + // Ensure logical bounds just in case + if (_hafalanStartAyat > _verses.length) _hafalanStartAyat = _verses.length; + if (_hafalanEndAyat < _hafalanStartAyat) _hafalanEndAyat = _hafalanStartAyat; + if (_hafalanEndAyat > _verses.length) _hafalanEndAyat = _verses.length; + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: isDark ? AppColors.surfaceDark : AppColors.surfaceLight, + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.05), + offset: const Offset(0, -4), + blurRadius: 16, + ), + ], + borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), + ), + child: SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'Mode Hafalan', + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), + ), + if (_isHafalanPlaying && _hafalanLoopCount > 0) + Text( + 'Loop: ${_currentLoop + 1} / $_hafalanLoopCount', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: AppColors.primary, + ), + ) + else if (_isHafalanPlaying) + Text( + 'Loop: ${_currentLoop + 1} / ∞', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: AppColors.primary, + ), + ), + ], + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + // Start Ayat + _buildHafalanDropdown( + label: 'Mulai', + value: _hafalanStartAyat, + items: List.generate(_verses.length, (i) => i + 1), + onChanged: _isHafalanPlaying + ? null + : (val) { + setState(() { + _hafalanStartAyat = val!; + if (_hafalanEndAyat < val) _hafalanEndAyat = val; + }); + }, + ), + const Text('-', style: TextStyle(color: Colors.grey)), + // End Ayat + _buildHafalanDropdown( + label: 'Sampai', + value: _hafalanEndAyat, + items: List.generate( + _verses.length - _hafalanStartAyat + 1, + (i) => _hafalanStartAyat + i + ), + onChanged: _isHafalanPlaying + ? null + : (val) => setState(() => _hafalanEndAyat = val!), + ), + const SizedBox(width: 8), + // Loop Count + _buildHafalanDropdown( + label: 'Ulangi', + value: _hafalanLoopCount, + items: [1, 3, 5, 7, 0], // 0 = infinite + displayMap: {0: '∞', 1: '1x', 3: '3x', 5: '5x', 7: '7x'}, + onChanged: _isHafalanPlaying + ? null + : (val) => setState(() => _hafalanLoopCount = val!), + ), + const SizedBox(width: 16), + // Play/Stop Button + GestureDetector( + onTap: () { + if (_isHafalanPlaying) { + _stopHafalan(); + } else { + setState(() { + _isHafalanPlaying = true; + _currentLoop = 0; + }); + final startIdx = _hafalanStartAyat - 1; + final audioUrl = _verses[startIdx]['audio']?['05'] as String?; + if (audioUrl != null) { + _playAudio(startIdx, audioUrl); + _attemptScroll(startIdx, attempts: 0); + } + } + }, + child: Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: AppColors.primary, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: AppColors.primary.withValues(alpha: 0.3), + blurRadius: 8, + offset: const Offset(0, 4), + ), + ], + ), + child: Icon( + _isHafalanPlaying ? Icons.stop_rounded : Icons.play_arrow_rounded, + color: Colors.white, + size: 28, + ), + ), + ), + ], + ), + ], + ), + ), + ); + } + + Widget _buildHafalanDropdown({ + required String label, + required T value, + required List items, + required void Function(T?)? onChanged, + Map? displayMap, + }) { + final isDark = Theme.of(context).brightness == Brightness.dark; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + label, + style: TextStyle( + fontSize: 10, + color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight, + ), + ), + const SizedBox(height: 4), + Container( + height: 36, + padding: const EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + color: isDark ? AppColors.backgroundDark : AppColors.backgroundLight, + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: AppColors.primary.withValues(alpha: 0.2), + ), + ), + child: DropdownButtonHideUnderline( + child: DropdownButton( + value: value, + items: items.map((e) { + return DropdownMenuItem( + value: e, + child: Text( + displayMap != null ? displayMap[e]! : e.toString(), + style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold), + ), + ); + }).toList(), + onChanged: onChanged, + icon: const Icon(Icons.expand_more, 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 new file mode 100644 index 0000000..9dcbf83 --- /dev/null +++ b/lib/features/quran/presentation/quran_screen.dart @@ -0,0 +1,260 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.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'; +import '../../../data/local/models/quran_bookmark.dart'; +import '../../../data/services/equran_service.dart'; + +class QuranScreen extends ConsumerStatefulWidget { + const QuranScreen({super.key}); + + @override + ConsumerState createState() => _QuranScreenState(); +} + +class _QuranScreenState extends ConsumerState { + List> _surahs = []; + String _searchQuery = ''; + bool _loading = true; + + bool _showLatin = true; + bool _showTerjemahan = true; + + @override + void initState() { + super.initState(); + _loadSurahs(); + final box = Hive.box(HiveBoxes.settings); + final settings = box.get('default') ?? AppSettings(); + _showLatin = settings.showLatin; + _showTerjemahan = settings.showTerjemahan; + } + + Future _loadSurahs() async { + final data = await EQuranService.instance.getAllSurahs(); + setState(() { + _surahs = data; + _loading = false; + }); + } + + void _showDisplaySettings() { + showModalBottomSheet( + context: context, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + builder: (ctx) => StatefulBuilder( + builder: (context, setModalState) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 16), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Pengaturan Tampilan', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 16), + SwitchListTile( + title: const Text('Tampilkan Latin'), + value: _showLatin, + activeColor: AppColors.primary, + onChanged: (val) { + setModalState(() => _showLatin = val); + setState(() => _showLatin = val); + final box = Hive.box(HiveBoxes.settings); + final settings = box.get('default') ?? AppSettings(); + settings.showLatin = val; + settings.save(); + }, + ), + SwitchListTile( + title: const Text('Tampilkan Terjemahan'), + value: _showTerjemahan, + activeColor: AppColors.primary, + onChanged: (val) { + setModalState(() => _showTerjemahan = val); + setState(() => _showTerjemahan = val); + final box = Hive.box(HiveBoxes.settings); + final settings = box.get('default') ?? AppSettings(); + settings.showTerjemahan = val; + settings.save(); + }, + ), + const SizedBox(height: 16), + ], + ), + ); + }, + ), + ); + } + + @override + Widget build(BuildContext context) { + final isDark = Theme.of(context).brightness == Brightness.dark; + final filtered = _searchQuery.isEmpty + ? _surahs + : _surahs + .where((s) => + (s['namaLatin'] as String? ?? '') + .toLowerCase() + .contains(_searchQuery.toLowerCase()) || + (s['nama'] as String? ?? '').contains(_searchQuery)) + .toList(); + + return Scaffold( + appBar: AppBar( + title: const Text('Al-Quran'), + actions: [ + IconButton( + icon: const Icon(Icons.bookmark_outline), + onPressed: () => context.push('/tools/quran/bookmarks'), + ), + IconButton( + icon: const Icon(Icons.settings_display), + onPressed: _showDisplaySettings, + ), + ], + ), + body: Column( + children: [ + // Search bar + Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 12), + child: Container( + decoration: BoxDecoration( + color: isDark ? AppColors.surfaceDark : AppColors.surfaceLight, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: isDark + ? AppColors.primary.withValues(alpha: 0.1) + : AppColors.cream, + ), + ), + child: TextField( + onChanged: (v) => setState(() => _searchQuery = v), + decoration: InputDecoration( + hintText: 'Cari surah...', + prefixIcon: Icon(Icons.search, + color: isDark + ? AppColors.textSecondaryDark + : AppColors.textSecondaryLight), + border: InputBorder.none, + contentPadding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + ), + ), + ), + ), + // Surah list + Expanded( + child: _loading + ? const Center(child: CircularProgressIndicator()) + : filtered.isEmpty + ? Center( + child: Text( + _searchQuery.isEmpty + ? 'Tidak dapat memuat data' + : 'Surah tidak ditemukan', + style: TextStyle( + color: isDark + ? AppColors.textSecondaryDark + : AppColors.textSecondaryLight, + ), + ), + ) + : ValueListenableBuilder( + valueListenable: Hive.box(HiveBoxes.bookmarks).listenable(), + builder: (context, box, _) { + return ListView.separated( + padding: const EdgeInsets.symmetric(horizontal: 16), + itemCount: filtered.length, + separatorBuilder: (_, __) => Divider( + height: 1, + color: isDark + ? AppColors.primary.withValues(alpha: 0.08) + : AppColors.cream, + ), + itemBuilder: (context, i) { + final surah = filtered[i]; + final number = surah['nomor'] ?? (i + 1); + final nameLatin = surah['namaLatin'] ?? ''; + final nameArabic = surah['nama'] ?? ''; + final totalVerses = surah['jumlahAyat'] ?? 0; + final tempatTurun = surah['tempatTurun'] ?? ''; + final arti = surah['arti'] ?? ''; + + final hasLastRead = box.values.any((b) => b.isLastRead && b.surahId == number); + + return ListTile( + onTap: () => + context.push('/tools/quran/$number'), + contentPadding: const EdgeInsets.symmetric( + horizontal: 0, vertical: 6), + leading: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: AppColors.primary + .withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(10), + ), + child: Center( + child: Text( + '$number', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w700, + color: AppColors.primary, + ), + ), + ), + ), + title: Row( + children: [ + Text( + nameLatin, + style: const TextStyle( + fontWeight: FontWeight.w600, + fontSize: 15, + ), + ), + if (hasLastRead) ...[ + const SizedBox(width: 8), + const Icon(Icons.push_pin, size: 14, color: AppColors.primary), + ], + ], + ), + subtitle: Text( + '$arti • $totalVerses Ayat • $tempatTurun', + style: TextStyle( + fontSize: 12, + color: isDark + ? AppColors.textSecondaryDark + : AppColors.textSecondaryLight, + ), + ), + trailing: Text( + nameArabic, + style: const TextStyle( + fontFamily: 'Amiri', + fontSize: 18, + ), + ), + ); + }, + ); + }, + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/settings/presentation/.gitkeep b/lib/features/settings/presentation/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/lib/features/settings/presentation/placeholder.dart b/lib/features/settings/presentation/placeholder.dart new file mode 100644 index 0000000..1039d4c --- /dev/null +++ b/lib/features/settings/presentation/placeholder.dart @@ -0,0 +1 @@ +// TODO: implement diff --git a/lib/features/settings/presentation/settings_screen.dart b/lib/features/settings/presentation/settings_screen.dart new file mode 100644 index 0000000..d487757 --- /dev/null +++ b/lib/features/settings/presentation/settings_screen.dart @@ -0,0 +1,891 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:hive_flutter/hive_flutter.dart'; +import '../../../app/theme/app_colors.dart'; +import '../../../core/providers/theme_provider.dart'; +import '../../../core/widgets/ios_toggle.dart'; +import '../../../data/local/hive_boxes.dart'; +import '../../../data/local/models/app_settings.dart'; +import '../../../data/services/myquran_sholat_service.dart'; +import '../../../data/services/myquran_sholat_service.dart'; +import '../../dashboard/data/prayer_times_provider.dart'; +import 'package:intl/intl.dart'; +import '../../../data/local/models/daily_worship_log.dart'; + +class SettingsScreen extends ConsumerStatefulWidget { + const SettingsScreen({super.key}); + + @override + ConsumerState createState() => _SettingsScreenState(); +} + +class _SettingsScreenState extends ConsumerState { + late AppSettings _settings; + + @override + void initState() { + super.initState(); + final box = Hive.box(HiveBoxes.settings); + _settings = box.get('default') ?? AppSettings(); + } + + void _saveSettings() { + _settings.save(); + setState(() {}); + } + + bool get _isDarkMode => _settings.themeModeIndex != 1; + bool get _notificationsEnabled => + _settings.adhanEnabled.values.any((v) => v); + + String get _displayCityName { + final stored = _settings.lastCityName ?? 'Jakarta'; + if (stored.contains('|')) { + return stored.split('|').first; + } + return stored; + } + + void _toggleDarkMode(bool value) { + _settings.themeModeIndex = value ? 2 : 1; + _saveSettings(); + ref.read(themeProvider.notifier).state = + value ? ThemeMode.dark : ThemeMode.light; + } + + void _toggleNotifications(bool value) { + _settings.adhanEnabled.updateAll((key, _) => value); + _saveSettings(); + } + + @override + Widget build(BuildContext context) { + final isDark = Theme.of(context).brightness == Brightness.dark; + + return Scaffold( + appBar: AppBar( + title: const Text('Pengaturan'), + ), + body: ListView( + padding: const EdgeInsets.all(16), + children: [ + // ── Profile Card ── + 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: [ + // Avatar + Container( + width: 56, + height: 56, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: LinearGradient( + colors: [ + AppColors.primary, + AppColors.primary.withValues(alpha: 0.6), + ], + ), + ), + child: Center( + child: Text( + _settings.userName.isNotEmpty + ? _settings.userName[0].toUpperCase() + : 'U', + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.w700, + color: Colors.white, + ), + ), + ), + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + _settings.userName, + style: const TextStyle( + fontSize: 17, + fontWeight: FontWeight.w700, + ), + ), + if (_settings.userEmail.isNotEmpty) + Text( + _settings.userEmail, + style: TextStyle( + fontSize: 13, + color: isDark + ? AppColors.textSecondaryDark + : AppColors.textSecondaryLight, + ), + ), + ], + ), + ), + IconButton( + onPressed: () => _showEditProfileDialog(context), + icon: Icon(Icons.edit, + size: 20, color: AppColors.primary), + ), + ], + ), + ), + const SizedBox(height: 24), + + // ── PREFERENCES ── + _sectionLabel('PREFERENSI'), + const SizedBox(height: 12), + _settingRow( + isDark, + icon: Icons.dark_mode, + iconColor: const Color(0xFF6C5CE7), + title: 'Mode Gelap', + trailing: IosToggle( + value: _isDarkMode, + onChanged: _toggleDarkMode, + ), + ), + const SizedBox(height: 10), + _settingRow( + isDark, + icon: Icons.notifications, + iconColor: const Color(0xFFE17055), + title: 'Notifikasi', + trailing: IosToggle( + value: _notificationsEnabled, + onChanged: _toggleNotifications, + ), + ), + const SizedBox(height: 24), + + // ── CHECKLIST IBADAH ── + _sectionLabel('CHECKLIST IBADAH'), + const SizedBox(height: 12), + _settingRow( + isDark, + icon: Icons.mosque_outlined, + 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), + onTap: () => _showRawatibDialog(context), + ), + const SizedBox(height: 10), + _settingRow( + isDark, + icon: Icons.menu_book, + iconColor: Colors.amber, + title: 'Target Tilawah', + subtitle: '${_settings.tilawahTargetValue} ${_settings.tilawahTargetUnit}', + trailing: const Icon(Icons.chevron_right, size: 20), + onTap: () => _showTilawahDialog(context), + ), + const SizedBox(height: 10), + _settingRow( + isDark, + icon: Icons.sync, + iconColor: Colors.blue, + title: 'Auto-Sync Tilawah', + subtitle: 'Catat otomatis dari menu Al-Quran', + trailing: IosToggle( + value: _settings.tilawahAutoSync, + onChanged: (v) { + _settings.tilawahAutoSync = v; + _saveSettings(); + + final todayKey = DateFormat('yyyy-MM-dd').format(DateTime.now()); + final logBox = Hive.box(HiveBoxes.worshipLogs); + final log = logBox.get(todayKey); + if (log != null && log.tilawahLog != null) { + log.tilawahLog!.autoSync = v; + log.save(); + } + }, + ), + ), + const SizedBox(height: 10), + _settingRow( + isDark, + icon: Icons.library_add_check, + iconColor: Colors.indigo, + title: 'Amalan Tambahan', + subtitle: 'Dzikir & Puasa Sunnah', + trailing: const Icon(Icons.chevron_right, size: 20), + onTap: () => _showAmalanDialog(context), + ), + const SizedBox(height: 24), + + // ── PRAYER SETTINGS ── + _sectionLabel('WAKTU SHOLAT'), + const SizedBox(height: 12), + _settingRow( + isDark, + icon: Icons.mosque, + iconColor: AppColors.primary, + title: 'Metode Perhitungan', + subtitle: 'Kemenag RI', + trailing: const Icon(Icons.chevron_right, size: 20), + onTap: () => _showMethodDialog(context), + ), + const SizedBox(height: 10), + _settingRow( + isDark, + icon: Icons.location_on, + iconColor: const Color(0xFF00B894), + title: 'Lokasi', + subtitle: _displayCityName, + trailing: const Icon(Icons.chevron_right, size: 20), + onTap: () => _showLocationDialog(context), + ), + const SizedBox(height: 10), + _settingRow( + isDark, + icon: Icons.timer, + iconColor: const Color(0xFFFDAA5E), + title: 'Waktu Iqamah', + subtitle: 'Atur per waktu sholat', + trailing: const Icon(Icons.chevron_right, size: 20), + onTap: () => _showIqamahDialog(context), + ), + const SizedBox(height: 24), + + // ── DISPLAY ── + _sectionLabel('TAMPILAN'), + const SizedBox(height: 12), + _settingRow( + isDark, + icon: Icons.text_fields, + iconColor: const Color(0xFF636E72), + title: 'Ukuran Font Arab', + subtitle: '${_settings.arabicFontSize.round()}pt', + trailing: SizedBox( + width: 120, + child: Slider( + value: _settings.arabicFontSize, + min: 16, + max: 40, + divisions: 12, + activeColor: AppColors.primary, + onChanged: (v) { + _settings.arabicFontSize = v; + _saveSettings(); + }, + ), + ), + ), + const SizedBox(height: 24), + + // ── ABOUT ── + _sectionLabel('TENTANG'), + const SizedBox(height: 12), + _settingRow( + isDark, + icon: Icons.info_outline, + iconColor: AppColors.sage, + title: 'Versi Aplikasi', + subtitle: '1.0.0', + ), + const SizedBox(height: 10), + _settingRow( + isDark, + icon: Icons.favorite_outline, + iconColor: Colors.red, + title: 'Beri Nilai Kami', + trailing: const Icon(Icons.chevron_right, size: 20), + onTap: () {}, + ), + const SizedBox(height: 24), + + // ── Reset Button ── + GestureDetector( + onTap: () => _showResetDialog(context), + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: Colors.red.withValues(alpha: 0.3), + width: 1.5, + ), + ), + child: const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.logout, color: Colors.red, size: 20), + SizedBox(width: 8), + Text( + 'Hapus Semua Data', + style: TextStyle( + color: Colors.red, + fontWeight: FontWeight.w600, + fontSize: 14, + ), + ), + ], + ), + ), + ), + const SizedBox(height: 32), + ], + ), + ); + } + + Widget _sectionLabel(String text) { + return Text( + text, + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w700, + letterSpacing: 1.5, + color: AppColors.sage, + ), + ); + } + + Widget _settingRow( + bool isDark, { + required IconData icon, + required Color iconColor, + required String title, + String? subtitle, + Widget? trailing, + VoidCallback? onTap, + }) { + return GestureDetector( + onTap: onTap, + child: 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.08) + : AppColors.cream, + ), + ), + child: Row( + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: iconColor.withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(10), + ), + child: Icon(icon, color: iconColor, size: 20), + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + ), + ), + if (subtitle != null) + Text( + subtitle, + style: TextStyle( + fontSize: 12, + color: isDark + ? AppColors.textSecondaryDark + : AppColors.textSecondaryLight, + ), + ), + ], + ), + ), + if (trailing != null) trailing, + ], + ), + ), + ); + } + + void _showMethodDialog(BuildContext context) { + showDialog( + context: context, + builder: (ctx) => AlertDialog( + insetPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 24), + title: const Text('Metode Perhitungan'), + content: SizedBox( + width: MediaQuery.of(context).size.width * 0.85, + child: const Text( + 'Aplikasi ini menggunakan data resmi dari Kementerian Agama RI (Kemenag) melalui API myQuran.\n\nData Kemenag sudah standar dan akurat untuk seluruh wilayah Indonesia, sehingga tidak perlu diubah.', + ), + ), + actions: [ + FilledButton( + onPressed: () => Navigator.pop(ctx), + child: const Text('Tutup'), + ), + ], + ), + ); + } + + void _showLocationDialog(BuildContext context) { + final searchCtrl = TextEditingController(); + bool isSearching = false; + List> results = []; + + showDialog( + context: context, + builder: (ctx) => StatefulBuilder( + builder: (ctx, setDialogState) => AlertDialog( + insetPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 24), + title: const Text('Cari Kota/Kabupaten'), + content: SizedBox( + width: MediaQuery.of(context).size.width * 0.85, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + controller: searchCtrl, + autofocus: true, + decoration: InputDecoration( + hintText: 'Cth: Jakarta', + border: const OutlineInputBorder(), + suffixIcon: IconButton( + icon: const Icon(Icons.search), + onPressed: () async { + if (searchCtrl.text.trim().isEmpty) return; + setDialogState(() => isSearching = true); + final res = await MyQuranSholatService.instance + .searchCity(searchCtrl.text.trim()); + setDialogState(() { + results = res; + isSearching = false; + }); + }, + ), + ), + onSubmitted: (val) async { + if (val.trim().isEmpty) return; + setDialogState(() => isSearching = true); + final res = await MyQuranSholatService.instance + .searchCity(val.trim()); + setDialogState(() { + results = res; + isSearching = false; + }); + }, + ), + const SizedBox(height: 16), + if (isSearching) + const Center(child: CircularProgressIndicator()) + else if (results.isEmpty) + const Text('Tidak ada hasil', style: TextStyle(color: Colors.grey)) + else + SizedBox( + height: 200, + width: double.maxFinite, + child: ListView.builder( + shrinkWrap: true, + itemCount: results.length, + itemBuilder: (context, i) { + final city = results[i]; + return ListTile( + title: Text(city['lokasi'] ?? ''), + onTap: () { + final id = city['id']; + final name = city['lokasi']; + if (id != null && name != null) { + _settings.lastCityName = '$name|$id'; + _saveSettings(); + + // Update providers to refresh data + ref.invalidate(selectedCityIdProvider); + ref.invalidate(cityNameProvider); + + Navigator.pop(ctx); + } + }, + ); + }, + ), + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx), + child: const Text('Batal'), + ), + ], + ), + ), + ); + } + + void _showEditProfileDialog(BuildContext context) { + final nameCtrl = TextEditingController(text: _settings.userName); + final emailCtrl = TextEditingController(text: _settings.userEmail); + showDialog( + context: context, + builder: (ctx) => AlertDialog( + insetPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 24), + title: const Text('Edit Profil'), + content: SizedBox( + width: MediaQuery.of(context).size.width * 0.85, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + controller: nameCtrl, + decoration: const InputDecoration( + labelText: 'Nama', + border: OutlineInputBorder(), + ), + ), + const SizedBox(height: 12), + TextField( + controller: emailCtrl, + decoration: const InputDecoration( + labelText: 'Email', + border: OutlineInputBorder(), + ), + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx), + child: const Text('Batal'), + ), + FilledButton( + onPressed: () { + _settings.userName = nameCtrl.text.trim(); + _settings.userEmail = emailCtrl.text.trim(); + _saveSettings(); + Navigator.pop(ctx); + }, + child: const Text('Simpan'), + ), + ], + ), + ); + } + + void _showIqamahDialog(BuildContext context) { + final offsets = Map.from(_settings.iqamahOffset); + showDialog( + context: context, + builder: (ctx) => StatefulBuilder( + builder: (ctx, setDialogState) => AlertDialog( + insetPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 24), + title: const Text('Waktu Iqamah (menit)'), + content: SizedBox( + width: MediaQuery.of(context).size.width * 0.85, + child: Column( + mainAxisSize: MainAxisSize.min, + children: offsets.entries.map((e) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + children: [ + SizedBox( + width: 80, + child: Text( + e.key[0].toUpperCase() + e.key.substring(1), + style: const TextStyle(fontWeight: FontWeight.w600), + ), + ), + Expanded( + child: Slider( + value: e.value.toDouble(), + min: 0, + max: 30, + divisions: 30, + label: '${e.value} min', + activeColor: AppColors.primary, + onChanged: (v) { + setDialogState(() { + offsets[e.key] = v.round(); + }); + }, + ), + ), + SizedBox( + width: 40, + child: Text( + '${e.value}m', + style: const TextStyle(fontWeight: FontWeight.w600), + ), + ), + ], + ), + ); + }).toList(), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx), + child: const Text('Batal'), + ), + FilledButton( + onPressed: () { + _settings.iqamahOffset = offsets; + _saveSettings(); + Navigator.pop(ctx); + }, + child: const Text('Simpan'), + ), + ], + ), + ), + ); + } + + void _showRawatibDialog(BuildContext context) { + int tempLevel = _settings.rawatibLevel; + showDialog( + context: context, + builder: (ctx) => StatefulBuilder( + builder: (ctx, setDialogState) => AlertDialog( + title: Row( + children: [ + const Text('Sholat Rawatib', style: TextStyle(fontSize: 18)), + const Spacer(), + IconButton( + icon: const Icon(Icons.info_outline, color: AppColors.primary), + onPressed: () { + showModalBottomSheet( + context: context, + builder: (bCtx) => Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Informasi Sholat Rawatib', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + const SizedBox(height: 16), + const Text('Muakkad (Sangat Ditekankan)', style: TextStyle(fontWeight: FontWeight.bold, color: AppColors.primary)), + const SizedBox(height: 8), + const Text('Total 10 atau 12 Rakaat:'), + const Padding( + padding: EdgeInsets.only(left: 12, top: 4), + child: Text('• 2 Rakaat sebelum Subuh\n• 2 atau 4 Rakaat sebelum Dzuhur\n• 2 Rakaat sesudah Dzuhur\n• 2 Rakaat sesudah Maghrib\n• 2 Rakaat sesudah Isya', style: TextStyle(height: 1.5)), + ), + const SizedBox(height: 16), + const Text('Ghairu Muakkad (Tambahan)', style: TextStyle(fontWeight: FontWeight.bold, color: AppColors.primary)), + const SizedBox(height: 8), + const Padding( + padding: EdgeInsets.only(left: 12), + child: Text('• Tambahan 2 Rakaat sesudah Dzuhur\n• 4 Rakaat sebelum Ashar\n• 2 Rakaat sebelum Maghrib\n• 2 Rakaat sebelum Isya', style: TextStyle(height: 1.5)), + ), + const SizedBox(height: 24), + ], + ), + ), + ); + }, + ), + ], + ), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + RadioListTile( + title: const Text('Mati (Tanpa Rawatib)'), + value: 0, + groupValue: tempLevel, + onChanged: (v) => setDialogState(() => tempLevel = v!), + ), + RadioListTile( + title: const Text('Muakkad Saja'), + value: 1, + groupValue: tempLevel, + onChanged: (v) => setDialogState(() => tempLevel = v!), + ), + RadioListTile( + title: const Text('Lengkap (Semua)'), + value: 2, + groupValue: tempLevel, + onChanged: (v) => setDialogState(() => tempLevel = v!), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx), + child: const Text('Batal'), + ), + FilledButton( + onPressed: () { + _settings.rawatibLevel = tempLevel; + _saveSettings(); + Navigator.pop(ctx); + }, + child: const Text('Simpan'), + ), + ], + ), + ), + ); + } + + void _showTilawahDialog(BuildContext context) { + final qtyCtrl = TextEditingController(text: _settings.tilawahTargetValue.toString()); + String tempUnit = _settings.tilawahTargetUnit; + + showDialog( + context: context, + builder: (ctx) => StatefulBuilder( + builder: (ctx, setDialogState) => AlertDialog( + title: const Text('Target Tilawah Harian'), + content: Row( + children: [ + Expanded( + flex: 1, + child: TextField( + controller: qtyCtrl, + keyboardType: TextInputType.number, + decoration: const InputDecoration(border: OutlineInputBorder()), + ), + ), + const SizedBox(width: 16), + Expanded( + flex: 2, + child: DropdownButtonFormField( + value: tempUnit, + decoration: const InputDecoration(border: OutlineInputBorder()), + items: ['Juz', 'Halaman', 'Ayat'].map((u) => DropdownMenuItem(value: u, child: Text(u))).toList(), + onChanged: (v) => setDialogState(() => tempUnit = v!), + ), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx), + child: const Text('Batal'), + ), + FilledButton( + onPressed: () { + final qty = int.tryParse(qtyCtrl.text.trim()) ?? 1; + _settings.tilawahTargetValue = qty > 0 ? qty : 1; + _settings.tilawahTargetUnit = tempUnit; + _saveSettings(); + + // Update today's active checklist immediately + final todayKey = DateFormat('yyyy-MM-dd').format(DateTime.now()); + final logBox = Hive.box(HiveBoxes.worshipLogs); + final log = logBox.get(todayKey); + if (log != null && log.tilawahLog != null) { + log.tilawahLog!.targetValue = _settings.tilawahTargetValue; + log.tilawahLog!.targetUnit = _settings.tilawahTargetUnit; + log.save(); + } + + Navigator.pop(ctx); + }, + child: const Text('Simpan'), + ), + ], + ), + ), + ); + } + + void _showAmalanDialog(BuildContext context) { + bool tDzikir = _settings.trackDzikir; + bool tPuasa = _settings.trackPuasa; + + showDialog( + context: context, + builder: (ctx) => StatefulBuilder( + builder: (ctx, setDialogState) => AlertDialog( + title: const Text('Amalan Tambahan'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SwitchListTile( + title: const Text('Dzikir Pagi & Petang'), + value: tDzikir, + onChanged: (v) => setDialogState(() => tDzikir = v), + ), + SwitchListTile( + title: const Text('Puasa Sunnah'), + value: tPuasa, + onChanged: (v) => setDialogState(() => tPuasa = v), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx), + child: const Text('Batal'), + ), + FilledButton( + onPressed: () { + _settings.trackDzikir = tDzikir; + _settings.trackPuasa = tPuasa; + _saveSettings(); + Navigator.pop(ctx); + }, + child: const Text('Simpan'), + ), + ], + ), + ), + ); + } + + void _showResetDialog(BuildContext context) { + showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: const Text('Hapus Semua Data?'), + content: const Text( + 'Ini akan menghapus semua riwayat ibadah, marka quran, penghitung dzikir, dan mereset pengaturan. Tindakan ini tidak dapat dibatalkan.', + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx), + child: const Text('Batal'), + ), + FilledButton( + style: FilledButton.styleFrom(backgroundColor: Colors.red), + onPressed: () async { + await Hive.box(HiveBoxes.worshipLogs).clear(); + await Hive.box(HiveBoxes.bookmarks).clear(); + await Hive.box(HiveBoxes.dzikirCounters).clear(); + final box = Hive.box(HiveBoxes.settings); + await box.clear(); + await box.put('default', AppSettings()); + setState(() { + _settings = box.get('default')!; + }); + ref.read(themeProvider.notifier).state = ThemeMode.system; + if (ctx.mounted) Navigator.pop(ctx); + }, + child: const Text('Hapus'), + ), + ], + ), + ); + } +} diff --git a/lib/features/tools/presentation/tools_screen.dart b/lib/features/tools/presentation/tools_screen.dart new file mode 100644 index 0000000..ed52cbe --- /dev/null +++ b/lib/features/tools/presentation/tools_screen.dart @@ -0,0 +1,251 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import '../../../app/theme/app_colors.dart'; +import '../../../data/services/equran_service.dart'; + +class ToolsScreen extends ConsumerWidget { + const ToolsScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final isDark = Theme.of(context).brightness == Brightness.dark; + + return Scaffold( + appBar: AppBar( + title: const Text('Alat Islami'), + centerTitle: false, + actions: [ + IconButton( + onPressed: () {}, + icon: const Icon(Icons.notifications_outlined), + ), + IconButton( + onPressed: () => context.push('/settings'), + icon: const Icon(Icons.settings_outlined), + ), + const SizedBox(width: 8), + ], + ), + body: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + 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: Icons.explore, + title: 'Arah\nKiblat', + color: AppColors.primary, + isDark: isDark, + onTap: () => context.push('/tools/qibla'), + ), + ), + const SizedBox(width: 12), + Expanded( + child: _ToolCard( + icon: Icons.menu_book, + title: 'Baca\nQuran', + color: const Color(0xFF4A90D9), + isDark: isDark, + onTap: () => context.push('/tools/quran'), + ), + ), + ], + ), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: _ToolCard( + icon: Icons.auto_awesome, + title: 'Penghitung\nDzikir', + color: const Color(0xFFE8A838), + isDark: isDark, + onTap: () => context.push('/tools/dzikir'), + ), + ), + const SizedBox(width: 12), + Expanded( + child: _ToolCard( + icon: Icons.headphones, + title: 'Quran\nMurattal', + color: const Color(0xFF7B61FF), + isDark: isDark, + onTap: () => context.push('/tools/quran/1/murattal'), + ), + ), + ], + ), + const SizedBox(height: 32), + // Ayat Hari Ini + 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(); // Hide if error/no internet + } + + 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: [ + Text( + 'Ayat Hari Ini', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight, + ), + ), + IconButton( + icon: Icon(Icons.share, + size: 18, + color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight), + onPressed: () {}, + ), + ], + ), + const SizedBox(height: 12), + Align( + alignment: Alignment.centerRight, + child: Text( + data['teksArab'] ?? '', + style: const TextStyle( + fontFamily: 'Amiri', + fontSize: 24, + height: 1.8, + ), + textAlign: TextAlign.right, + ), + ), + const SizedBox(height: 12), + 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, + ), + ), + ], + ), + ); + }, + ), + ], + ), + ), + ); + } +} + +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/lib/main.dart b/lib/main.dart new file mode 100644 index 0000000..d48d48f --- /dev/null +++ b/lib/main.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; + +import 'app/app.dart'; +import 'data/local/hive_boxes.dart'; + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + + // Initialize Hive and open all boxes + await initHive(); + + // Load environment variables + await dotenv.load(fileName: '.env'); + + // Seed default settings and checklist items on first launch + await seedDefaults(); + + runApp( + const ProviderScope( + child: App(), + ), + ); +} diff --git a/macos/.gitignore b/macos/.gitignore new file mode 100644 index 0000000..746adbb --- /dev/null +++ b/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/macos/Flutter/Flutter-Debug.xcconfig b/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 0000000..4b81f9b --- /dev/null +++ b/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/Flutter-Release.xcconfig b/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 0000000..5caa9d1 --- /dev/null +++ b/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 0000000..b77b233 --- /dev/null +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,26 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import audio_service +import audio_session +import flutter_local_notifications +import geolocator_apple +import just_audio +import package_info_plus +import sqflite_darwin +import url_launcher_macos + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + AudioServicePlugin.register(with: registry.registrar(forPlugin: "AudioServicePlugin")) + AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin")) + FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) + GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin")) + JustAudioPlugin.register(with: registry.registrar(forPlugin: "JustAudioPlugin")) + FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) + SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) +} diff --git a/macos/Podfile b/macos/Podfile new file mode 100644 index 0000000..ff5ddb3 --- /dev/null +++ b/macos/Podfile @@ -0,0 +1,42 @@ +platform :osx, '10.15' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/macos/Podfile.lock b/macos/Podfile.lock new file mode 100644 index 0000000..db3d2cf --- /dev/null +++ b/macos/Podfile.lock @@ -0,0 +1,68 @@ +PODS: + - audio_service (0.0.1): + - Flutter + - FlutterMacOS + - audio_session (0.0.1): + - FlutterMacOS + - flutter_local_notifications (0.0.1): + - FlutterMacOS + - FlutterMacOS (1.0.0) + - geolocator_apple (1.2.0): + - Flutter + - FlutterMacOS + - just_audio (0.0.1): + - Flutter + - FlutterMacOS + - package_info_plus (0.0.1): + - FlutterMacOS + - sqflite_darwin (0.0.4): + - Flutter + - FlutterMacOS + - url_launcher_macos (0.0.1): + - FlutterMacOS + +DEPENDENCIES: + - audio_service (from `Flutter/ephemeral/.symlinks/plugins/audio_service/darwin`) + - audio_session (from `Flutter/ephemeral/.symlinks/plugins/audio_session/macos`) + - flutter_local_notifications (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos`) + - FlutterMacOS (from `Flutter/ephemeral`) + - geolocator_apple (from `Flutter/ephemeral/.symlinks/plugins/geolocator_apple/darwin`) + - just_audio (from `Flutter/ephemeral/.symlinks/plugins/just_audio/darwin`) + - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`) + - sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`) + - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) + +EXTERNAL SOURCES: + audio_service: + :path: Flutter/ephemeral/.symlinks/plugins/audio_service/darwin + audio_session: + :path: Flutter/ephemeral/.symlinks/plugins/audio_session/macos + flutter_local_notifications: + :path: Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos + FlutterMacOS: + :path: Flutter/ephemeral + geolocator_apple: + :path: Flutter/ephemeral/.symlinks/plugins/geolocator_apple/darwin + just_audio: + :path: Flutter/ephemeral/.symlinks/plugins/just_audio/darwin + package_info_plus: + :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos + sqflite_darwin: + :path: Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin + url_launcher_macos: + :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos + +SPEC CHECKSUMS: + audio_service: cab6c1a0eaf01b5a35b567e11fa67d3cc1956910 + audio_session: 728ae3823d914f809c485d390274861a24b0904e + flutter_local_notifications: 14e285ca39907db50704f7f46c9ab7a526bd7ead + FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 + geolocator_apple: 66b711889fd333205763b83c9dcf0a57a28c7afd + just_audio: a42c63806f16995daf5b219ae1d679deb76e6a79 + package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b + sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d + url_launcher_macos: 175a54c831f4375a6cf895875f716ee5af3888ce + +PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009 + +COCOAPODS: 1.12.0 diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..ffe8bd3 --- /dev/null +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,801 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 0C90C3ED62E3E14394A23EE5 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6A4B57B5283BA4F62AC20241 /* Pods_Runner.framework */; }; + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + DDE68F59044EBC73D03E0962 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7DF3757EFF54A1EC85BA5E22 /* Pods_RunnerTests.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 00FDE0E819DF753D953FEBB2 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 06DAA92E91CF7851957A0E28 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 07D5D0934671F750DA630F1D /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 0BB6B1FDF75FA1C8F8165DEB /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 1F212DF96DD0BB5851DDFC62 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* jamshalat_diary.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = jamshalat_diary.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 6A4B57B5283BA4F62AC20241 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 7DF3757EFF54A1EC85BA5E22 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + E6FD2B4E523D8881848DBBE0 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 331C80D2294CF70F00263BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + DDE68F59044EBC73D03E0962 /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 0C90C3ED62E3E14394A23EE5 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 0534C72D1883289C9A7D2A97 /* Pods */ = { + isa = PBXGroup; + children = ( + 0BB6B1FDF75FA1C8F8165DEB /* Pods-Runner.debug.xcconfig */, + 07D5D0934671F750DA630F1D /* Pods-Runner.release.xcconfig */, + 00FDE0E819DF753D953FEBB2 /* Pods-Runner.profile.xcconfig */, + 1F212DF96DD0BB5851DDFC62 /* Pods-RunnerTests.debug.xcconfig */, + 06DAA92E91CF7851957A0E28 /* Pods-RunnerTests.release.xcconfig */, + E6FD2B4E523D8881848DBBE0 /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + 331C80D6294CF71000263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C80D7294CF71000263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 331C80D6294CF71000263BE5 /* RunnerTests */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + 0534C72D1883289C9A7D2A97 /* Pods */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* jamshalat_diary.app */, + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 6A4B57B5283BA4F62AC20241 /* Pods_Runner.framework */, + 7DF3757EFF54A1EC85BA5E22 /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C80D4294CF70F00263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 9B95A5421B9DABEF4DB6101B /* [CP] Check Pods Manifest.lock */, + 331C80D1294CF70F00263BE5 /* Sources */, + 331C80D2294CF70F00263BE5 /* Frameworks */, + 331C80D3294CF70F00263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C80DA294CF71000263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 2DC954CD4D756DB6686B4570 /* [CP] Check Pods Manifest.lock */, + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + 7C73BC09BD737FBB6206AB8D /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* jamshalat_diary.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C80D4294CF70F00263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 33CC10EC2044A3C60003C045; + }; + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 331C80D4294CF70F00263BE5 /* RunnerTests */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C80D3294CF70F00263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 2DC954CD4D756DB6686B4570 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; + 7C73BC09BD737FBB6206AB8D /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 9B95A5421B9DABEF4DB6101B /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C80D1294CF70F00263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; + }; + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 331C80DB294CF71000263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 1F212DF96DD0BB5851DDFC62 /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.jamshalat.jamshalatDiary.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/jamshalat_diary.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/jamshalat_diary"; + }; + name = Debug; + }; + 331C80DC294CF71000263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 06DAA92E91CF7851957A0E28 /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.jamshalat.jamshalatDiary.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/jamshalat_diary.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/jamshalat_diary"; + }; + name = Release; + }; + 331C80DD294CF71000263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = E6FD2B4E523D8881848DBBE0 /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.jamshalat.jamshalatDiary.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/jamshalat_diary.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/jamshalat_diary"; + }; + name = Profile; + }; + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C80DB294CF71000263BE5 /* Debug */, + 331C80DC294CF71000263BE5 /* Release */, + 331C80DD294CF71000263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..a1d053e --- /dev/null +++ b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macos/Runner.xcworkspace/contents.xcworkspacedata b/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/macos/Runner/AppDelegate.swift b/macos/Runner/AppDelegate.swift new file mode 100644 index 0000000..b3c1761 --- /dev/null +++ b/macos/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Cocoa +import FlutterMacOS + +@main +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } +} diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..a2ec33f --- /dev/null +++ b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000..82b6f9d Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 0000000..13b35eb Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 0000000..0a3f5fa Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 0000000..bdb5722 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 0000000..f083318 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 0000000..326c0e7 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 0000000..2f1632c Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/macos/Runner/Base.lproj/MainMenu.xib b/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 0000000..80e867a --- /dev/null +++ b/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macos/Runner/Configs/AppInfo.xcconfig b/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 0000000..0ca264f --- /dev/null +++ b/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = jamshalat_diary + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.jamshalat.jamshalatDiary + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2026 com.jamshalat. All rights reserved. diff --git a/macos/Runner/Configs/Debug.xcconfig b/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 0000000..36b0fd9 --- /dev/null +++ b/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/macos/Runner/Configs/Release.xcconfig b/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 0000000..dff4f49 --- /dev/null +++ b/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/macos/Runner/Configs/Warnings.xcconfig b/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 0000000..42bcbf4 --- /dev/null +++ b/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/macos/Runner/DebugProfile.entitlements b/macos/Runner/DebugProfile.entitlements new file mode 100644 index 0000000..08c3ab1 --- /dev/null +++ b/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + com.apple.security.network.client + + + diff --git a/macos/Runner/Info.plist b/macos/Runner/Info.plist new file mode 100644 index 0000000..4789daa --- /dev/null +++ b/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/macos/Runner/MainFlutterWindow.swift b/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 0000000..3cc05eb --- /dev/null +++ b/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/macos/Runner/Release.entitlements b/macos/Runner/Release.entitlements new file mode 100644 index 0000000..852fa1a --- /dev/null +++ b/macos/Runner/Release.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/macos/RunnerTests/RunnerTests.swift b/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..61f3bd1 --- /dev/null +++ b/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Cocoa +import FlutterMacOS +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..ae8bc8e --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,1303 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" + url: "https://pub.dev" + source: hosted + version: "67.0.0" + adhan: + dependency: "direct main" + description: + name: adhan + sha256: e1d3363c8127dc614947ecf312e458fbf4783af3db8e3b465c4a286eb37374ac + url: "https://pub.dev" + source: hosted + version: "2.0.0+1" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" + url: "https://pub.dev" + source: hosted + version: "6.4.1" + analyzer_plugin: + dependency: transitive + description: + name: analyzer_plugin + sha256: "9661b30b13a685efaee9f02e5d01ed9f2b423bd889d28a304d02d704aee69161" + url: "https://pub.dev" + source: hosted + version: "0.11.3" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + audio_service: + dependency: "direct main" + description: + name: audio_service + sha256: cb122c7c2639d2a992421ef96b67948ad88c5221da3365ccef1031393a76e044 + url: "https://pub.dev" + source: hosted + version: "0.18.18" + audio_service_platform_interface: + dependency: transitive + description: + name: audio_service_platform_interface + sha256: "6283782851f6c8b501b60904a32fc7199dc631172da0629d7301e66f672ab777" + url: "https://pub.dev" + source: hosted + version: "0.1.3" + audio_service_web: + dependency: transitive + description: + name: audio_service_web + sha256: b8ea9243201ee53383157fbccf13d5d2a866b5dda922ec19d866d1d5d70424df + url: "https://pub.dev" + source: hosted + version: "0.1.4" + audio_session: + dependency: transitive + description: + name: audio_session + sha256: "8f96a7fecbb718cb093070f868b4cdcb8a9b1053dce342ff8ab2fde10eb9afb7" + url: "https://pub.dev" + source: hosted + version: "0.2.2" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + build: + dependency: transitive + description: + name: build + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + build_config: + dependency: transitive + description: + name: build_config + sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: bf05f6e12cfea92d3c09308d7bcdab1906cd8a179b023269eed00c071004b957 + url: "https://pub.dev" + source: hosted + version: "4.1.1" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" + url: "https://pub.dev" + source: hosted + version: "2.4.13" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0 + url: "https://pub.dev" + source: hosted + version: "7.3.2" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: "6ae8a6435a8c6520c7077b107e77f1fb4ba7009633259a4d49a8afd8e7efc5e9" + url: "https://pub.dev" + source: hosted + version: "8.12.4" + cached_network_image: + dependency: "direct main" + description: + name: cached_network_image + sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916" + url: "https://pub.dev" + source: hosted + version: "3.4.1" + cached_network_image_platform_interface: + dependency: transitive + description: + name: cached_network_image_platform_interface + sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" + url: "https://pub.dev" + source: hosted + version: "4.1.1" + cached_network_image_web: + dependency: transitive + description: + name: cached_network_image_web + sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + characters: + dependency: transitive + description: + name: characters + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + url: "https://pub.dev" + source: hosted + version: "1.4.1" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" + url: "https://pub.dev" + source: hosted + version: "2.0.4" + ci: + dependency: transitive + description: + name: ci + sha256: "145d095ce05cddac4d797a158bc4cf3b6016d1fe63d8c3d2fbd7212590adca13" + url: "https://pub.dev" + source: hosted + version: "0.1.0" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c + url: "https://pub.dev" + source: hosted + version: "0.4.2" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + code_assets: + dependency: transitive + description: + name: code_assets + sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: "6a6cab2ba4680d6423f34a9b972a4c9a94ebe1b62ecec4e1a1f2cba91fd1319d" + url: "https://pub.dev" + source: hosted + version: "4.11.1" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" + custom_lint: + dependency: transitive + description: + name: custom_lint + sha256: "22bd87a362f433ba6aae127a7bac2838645270737f3721b180916d7c5946cb5d" + url: "https://pub.dev" + source: hosted + version: "0.5.11" + custom_lint_core: + dependency: transitive + description: + name: custom_lint_core + sha256: "2952837953022de610dacb464f045594854ced6506ac7f76af28d4a6490e189b" + url: "https://pub.dev" + source: hosted + version: "0.5.14" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" + url: "https://pub.dev" + source: hosted + version: "2.3.6" + dbus: + dependency: transitive + description: + name: dbus + sha256: d0c98dcd4f5169878b6cf8f6e0a52403a9dff371a3e2f019697accbf6f44a270 + url: "https://pub.dev" + source: hosted + version: "0.7.12" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_cache_manager: + dependency: transitive + description: + name: flutter_cache_manager + sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" + url: "https://pub.dev" + source: hosted + version: "3.4.1" + flutter_compass: + dependency: "direct main" + description: + name: flutter_compass + sha256: "1b4d7e6c95a675ec8482b5c9c9ccf1ebf0ced3dbec59dce28ad609da953de850" + url: "https://pub.dev" + source: hosted + version: "0.8.1" + flutter_compass_v2: + dependency: transitive + description: + name: flutter_compass_v2 + sha256: c17b1533a43f5192e73d2f475ec2c142cc1e193eba6a9b49433d1ab68d70606b + url: "https://pub.dev" + source: hosted + version: "1.0.3" + flutter_dotenv: + dependency: "direct main" + description: + name: flutter_dotenv + sha256: b7c7be5cd9f6ef7a78429cabd2774d3c4af50e79cb2b7593e3d5d763ef95c61b + url: "https://pub.dev" + source: hosted + version: "5.2.1" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + flutter_local_notifications: + dependency: "direct main" + description: + name: flutter_local_notifications + sha256: "0d9035862236fe38250fe1644d7ed3b8254e34a21b2c837c9f539fbb3bba5ef1" + url: "https://pub.dev" + source: hosted + version: "21.0.0" + flutter_local_notifications_linux: + dependency: transitive + description: + name: flutter_local_notifications_linux + sha256: e0f25e243c6c44c825bbbc6b2b2e76f7d9222362adcfe9fd780bf01923c840bd + url: "https://pub.dev" + source: hosted + version: "8.0.0" + flutter_local_notifications_platform_interface: + dependency: transitive + description: + name: flutter_local_notifications_platform_interface + sha256: e7db3d5b49c2b7ecc68deba4aaaa67a348f92ee0fef34c8e4b4459dbef0d7307 + url: "https://pub.dev" + source: hosted + version: "11.0.0" + flutter_local_notifications_windows: + dependency: transitive + description: + name: flutter_local_notifications_windows + sha256: "3a2654ba104fbb52c618ebed9def24ef270228470718c43b3a6afcd5c81bef0c" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_qiblah: + dependency: "direct main" + description: + name: flutter_qiblah + sha256: ef343208c2d30ab71f10ec04773099a90bf56f9e6887251a2b70af7fec3ca3ed + url: "https://pub.dev" + source: hosted + version: "3.2.0" + flutter_riverpod: + dependency: "direct main" + description: + name: flutter_riverpod + sha256: "2fd9f58a39b7269cb3495b09245000fcd267243518157a7c2f832189fb64f013" + url: "https://pub.dev" + source: hosted + version: "3.0.0-dev.3" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + freezed_annotation: + dependency: transitive + description: + name: freezed_annotation + sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 + url: "https://pub.dev" + source: hosted + version: "2.4.4" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" + geoclue: + dependency: transitive + description: + name: geoclue + sha256: c2a998c77474fc57aa00c6baa2928e58f4b267649057a1c76738656e9dbd2a7f + url: "https://pub.dev" + source: hosted + version: "0.1.1" + geocoding: + dependency: "direct main" + description: + name: geocoding + sha256: "606be036287842d779d7ec4e2f6c9435fc29bbbd3c6da6589710f981d8852895" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + geocoding_android: + dependency: transitive + description: + name: geocoding_android + sha256: ba810da90d6633cbb82bbab630e5b4a3b7d23503263c00ae7f1ef0316dcae5b9 + url: "https://pub.dev" + source: hosted + version: "4.0.1" + geocoding_ios: + dependency: transitive + description: + name: geocoding_ios + sha256: "18ab1c8369e2b0dcb3a8ccc907319334f35ee8cf4cfef4d9c8e23b13c65cb825" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + geocoding_platform_interface: + dependency: transitive + description: + name: geocoding_platform_interface + sha256: "8c2c8226e5c276594c2e18bfe88b19110ed770aeb7c1ab50ede570be8b92229b" + url: "https://pub.dev" + source: hosted + version: "3.2.0" + geolocator: + dependency: "direct main" + description: + name: geolocator + sha256: "79939537046c9025be47ec645f35c8090ecadb6fe98eba146a0d25e8c1357516" + url: "https://pub.dev" + source: hosted + version: "14.0.2" + geolocator_android: + dependency: transitive + description: + name: geolocator_android + sha256: "179c3cb66dfa674fc9ccbf2be872a02658724d1c067634e2c427cf6df7df901a" + url: "https://pub.dev" + source: hosted + version: "5.0.2" + geolocator_apple: + dependency: transitive + description: + name: geolocator_apple + sha256: dbdd8789d5aaf14cf69f74d4925ad1336b4433a6efdf2fce91e8955dc921bf22 + url: "https://pub.dev" + source: hosted + version: "2.3.13" + geolocator_linux: + dependency: transitive + description: + name: geolocator_linux + sha256: d64112a205931926f4363bb6bd48f14cb38e7326833041d170615586cd143797 + url: "https://pub.dev" + source: hosted + version: "0.2.4" + geolocator_platform_interface: + dependency: transitive + description: + name: geolocator_platform_interface + sha256: "30cb64f0b9adcc0fb36f628b4ebf4f731a2961a0ebd849f4b56200205056fe67" + url: "https://pub.dev" + source: hosted + version: "4.2.6" + geolocator_web: + dependency: transitive + description: + name: geolocator_web + sha256: b1ae9bdfd90f861fde8fd4f209c37b953d65e92823cb73c7dee1fa021b06f172 + url: "https://pub.dev" + source: hosted + version: "4.1.3" + geolocator_windows: + dependency: transitive + description: + name: geolocator_windows + sha256: "175435404d20278ffd220de83c2ca293b73db95eafbdc8131fe8609be1421eb6" + url: "https://pub.dev" + source: hosted + version: "0.2.5" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" + go_router: + dependency: "direct main" + description: + name: go_router + sha256: "7974313e217a7771557add6ff2238acb63f635317c35fa590d348fb238f00896" + url: "https://pub.dev" + source: hosted + version: "17.1.0" + google_fonts: + dependency: "direct main" + description: + name: google_fonts + sha256: db9df7a5898d894eeda4c78143f35c30a243558be439518972366880b80bf88e + url: "https://pub.dev" + source: hosted + version: "8.0.2" + graphs: + dependency: transitive + description: + name: graphs + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + gsettings: + dependency: transitive + description: + name: gsettings + sha256: "1b0ce661f5436d2db1e51f3c4295a49849f03d304003a7ba177d01e3a858249c" + url: "https://pub.dev" + source: hosted + version: "0.2.8" + hive: + dependency: transitive + description: + name: hive + sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" + url: "https://pub.dev" + source: hosted + version: "2.2.3" + hive_flutter: + dependency: "direct main" + description: + name: hive_flutter + sha256: dca1da446b1d808a51689fb5d0c6c9510c0a2ba01e22805d492c73b68e33eecc + url: "https://pub.dev" + source: hosted + version: "1.1.0" + hive_generator: + dependency: "direct dev" + description: + name: hive_generator + sha256: "06cb8f58ace74de61f63500564931f9505368f45f98958bd7a6c35ba24159db4" + url: "https://pub.dev" + source: hosted + version: "2.0.1" + hooks: + dependency: transitive + description: + name: hooks + sha256: e79ed1e8e1929bc6ecb6ec85f0cb519c887aa5b423705ded0d0f2d9226def388 + url: "https://pub.dev" + source: hosted + version: "1.0.2" + http: + dependency: "direct main" + description: + name: http + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" + url: "https://pub.dev" + source: hosted + version: "1.6.0" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 + url: "https://pub.dev" + source: hosted + version: "3.2.2" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + intl: + dependency: "direct main" + description: + name: intl + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + url: "https://pub.dev" + source: hosted + version: "0.20.2" + io: + dependency: transitive + description: + name: io + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b + url: "https://pub.dev" + source: hosted + version: "1.0.5" + js: + dependency: transitive + description: + name: js + sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" + url: "https://pub.dev" + source: hosted + version: "0.7.2" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.dev" + source: hosted + version: "4.9.0" + json_serializable: + dependency: "direct dev" + description: + name: json_serializable + sha256: ea1432d167339ea9b5bb153f0571d0039607a873d6e04e0117af043f14a1fd4b + url: "https://pub.dev" + source: hosted + version: "6.8.0" + just_audio: + dependency: "direct main" + description: + name: just_audio + sha256: "9694e4734f515f2a052493d1d7e0d6de219ee0427c7c29492e246ff32a219908" + url: "https://pub.dev" + source: hosted + version: "0.10.5" + just_audio_platform_interface: + dependency: transitive + description: + name: just_audio_platform_interface + sha256: "2532c8d6702528824445921c5ff10548b518b13f808c2e34c2fd54793b999a6a" + url: "https://pub.dev" + source: hosted + version: "4.6.0" + just_audio_web: + dependency: transitive + description: + name: just_audio_web + sha256: "6ba8a2a7e87d57d32f0f7b42856ade3d6a9fbe0f1a11fabae0a4f00bb73f0663" + url: "https://pub.dev" + source: hosted + version: "0.4.16" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" + url: "https://pub.dev" + source: hosted + version: "6.1.0" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 + url: "https://pub.dev" + source: hosted + version: "0.12.19" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + url: "https://pub.dev" + source: hosted + version: "0.13.0" + material_symbols_icons: + dependency: "direct main" + description: + name: material_symbols_icons + sha256: c62b15f2b3de98d72cbff0148812f5ef5159f05e61fc9f9a089ec2bb234df082 + url: "https://pub.dev" + source: hosted + version: "4.2906.0" + meta: + dependency: transitive + description: + name: meta + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + url: "https://pub.dev" + source: hosted + version: "1.17.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + native_toolchain_c: + dependency: transitive + description: + name: native_toolchain_c + sha256: "92b2ca62c8bd2b8d2f267cdfccf9bfbdb7322f778f8f91b3ce5b5cda23a3899f" + url: "https://pub.dev" + source: hosted + version: "0.17.5" + objective_c: + dependency: transitive + description: + name: objective_c + sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52" + url: "https://pub.dev" + source: hosted + version: "9.3.0" + octo_image: + dependency: transitive + description: + name: octo_image + sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.dev" + source: hosted + version: "2.2.0" + package_info_plus: + dependency: transitive + description: + name: package_info_plus + sha256: f69da0d3189a4b4ceaeb1a3defb0f329b3b352517f52bed4290f83d4f06bc08d + url: "https://pub.dev" + source: hosted + version: "9.0.0" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e + url: "https://pub.dev" + source: hosted + version: "2.2.22" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" + url: "https://pub.dev" + source: hosted + version: "2.6.0" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" + url: "https://pub.dev" + source: hosted + version: "7.0.2" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + pool: + dependency: transitive + description: + name: pool + sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" + url: "https://pub.dev" + source: hosted + version: "1.5.2" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" + url: "https://pub.dev" + source: hosted + version: "1.5.0" + riverpod: + dependency: transitive + description: + name: riverpod + sha256: "0f41a697a17609a7ac18e5fe0d5bdbe4c1ff7e7da6523baf46a203df0c44eaf2" + url: "https://pub.dev" + source: hosted + version: "3.0.0-dev.3" + riverpod_analyzer_utils: + dependency: transitive + description: + name: riverpod_analyzer_utils + sha256: b6e782db97522de3ad797210bd3babbdb0a67da899aaa6ffbb6572108bdbf48d + url: "https://pub.dev" + source: hosted + version: "1.0.0-dev.1" + riverpod_annotation: + dependency: "direct main" + description: + name: riverpod_annotation + sha256: "79452c7ba2e8f48c7309c73be5aaa101eec5fe7948dfd26659b883fb276858b4" + url: "https://pub.dev" + source: hosted + version: "3.0.0-dev.3" + riverpod_generator: + dependency: "direct dev" + description: + name: riverpod_generator + sha256: "9f3cb7b43e9151fef1cc80031b3ad9fb5d0fe64577cc18e1627061d743823213" + url: "https://pub.dev" + source: hosted + version: "3.0.0-dev.11" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" + url: "https://pub.dev" + source: hosted + version: "0.27.7" + shelf: + dependency: transitive + description: + name: shelf + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + url: "https://pub.dev" + source: hosted + version: "1.4.2" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" + url: "https://pub.dev" + source: hosted + version: "1.5.0" + source_helper: + dependency: transitive + description: + name: source_helper + sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c" + url: "https://pub.dev" + source: hosted + version: "1.3.5" + source_span: + dependency: transitive + description: + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" + source: hosted + version: "1.10.2" + sqflite: + dependency: transitive + description: + name: sqflite + sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03 + url: "https://pub.dev" + source: hosted + version: "2.4.2" + sqflite_android: + dependency: transitive + description: + name: sqflite_android + sha256: ecd684501ebc2ae9a83536e8b15731642b9570dc8623e0073d227d0ee2bfea88 + url: "https://pub.dev" + source: hosted + version: "2.4.2+2" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6" + url: "https://pub.dev" + source: hosted + version: "2.5.6" + sqflite_darwin: + dependency: transitive + description: + name: sqflite_darwin + sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + sqflite_platform_interface: + dependency: transitive + description: + name: sqflite_platform_interface + sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + state_notifier: + dependency: transitive + description: + name: state_notifier + sha256: b8677376aa54f2d7c58280d5a007f9e8774f1968d1fb1c096adcb4792fba29bb + url: "https://pub.dev" + source: hosted + version: "1.0.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 + url: "https://pub.dev" + source: hosted + version: "2.1.1" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0 + url: "https://pub.dev" + source: hosted + version: "3.4.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" + url: "https://pub.dev" + source: hosted + version: "0.7.10" + timezone: + dependency: "direct main" + description: + name: timezone + sha256: "784a5e34d2eb62e1326f24d6f600aaaee452eb8ca8ef2f384a59244e292d158b" + url: "https://pub.dev" + source: hosted + version: "0.11.0" + timing: + dependency: transitive + description: + name: timing + sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 + url: "https://pub.dev" + source: hosted + version: "6.3.2" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "767344bf3063897b5cf0db830e94f904528e6dd50a6dfaf839f0abf509009611" + url: "https://pub.dev" + source: hosted + version: "6.3.28" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0" + url: "https://pub.dev" + source: hosted + version: "6.4.1" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a + url: "https://pub.dev" + source: hosted + version: "3.2.2" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" + url: "https://pub.dev" + source: hosted + version: "3.2.5" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: d0412fcf4c6b31ecfdb7762359b7206ffba3bbffd396c6d9f9c4616ece476c1f + url: "https://pub.dev" + source: hosted + version: "2.4.2" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" + url: "https://pub.dev" + source: hosted + version: "3.1.5" + uuid: + dependency: "direct main" + description: + name: uuid + sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" + url: "https://pub.dev" + source: hosted + version: "4.5.3" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + url: "https://pub.dev" + source: hosted + version: "15.0.2" + watcher: + dependency: transitive + description: + name: watcher + sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 + url: "https://pub.dev" + source: hosted + version: "3.0.3" + win32: + dependency: transitive + description: + name: win32 + sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e + url: "https://pub.dev" + source: hosted + version: "5.15.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" + url: "https://pub.dev" + source: hosted + version: "6.6.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" +sdks: + dart: ">=3.10.3 <4.0.0" + flutter: ">=3.38.4" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..971b5e6 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,91 @@ +name: jamshalat_diary +description: Islamic worship companion app +publish_to: 'none' +version: 1.0.0+1 + +environment: + sdk: '>=3.0.0 <4.0.0' + +dependencies: + flutter: + sdk: flutter + flutter_localizations: + sdk: flutter + + # State management + flutter_riverpod: ^3.0.0-dev.3 + riverpod_annotation: ^3.0.0-dev.3 + + # Navigation + go_router: ^17.1.0 + + # Local storage + hive_flutter: ^1.1.0 + + # Prayer times + adhan: ^2.0.0+1 + + # Location + geolocator: ^14.0.0 + geocoding: ^4.0.0 + + # Qibla + flutter_qiblah: ^3.0.0 + flutter_compass: ^0.8.1 + + # Notifications + flutter_local_notifications: ^21.0.0 + + # Audio + just_audio: ^0.10.5 + audio_service: ^0.18.13 + + # Fonts & Icons + google_fonts: ^8.0.2 + material_symbols_icons: ^4.2719.3 + + # Utils + intl: ^0.20.0 + uuid: ^4.4.0 + timezone: ^0.11.0 + http: ^1.2.0 + flutter_dotenv: ^5.1.0 + cached_network_image: ^3.3.1 + url_launcher: ^6.2.5 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^6.0.0 + build_runner: ^2.4.9 + riverpod_generator: ^3.0.0-dev.11 + hive_generator: ^2.0.1 + json_serializable: ^6.8.0 + +flutter: + uses-material-design: true + + assets: + - assets/fonts/ + - assets/quran/ + - assets/dzikir/ + - assets/images/ + - .env + + fonts: + - family: PlusJakartaSans + fonts: + - asset: assets/fonts/PlusJakartaSans-Regular.ttf + weight: 400 + - asset: assets/fonts/PlusJakartaSans-SemiBold.ttf + weight: 600 + - asset: assets/fonts/PlusJakartaSans-Bold.ttf + weight: 700 + - asset: assets/fonts/PlusJakartaSans-ExtraBold.ttf + weight: 800 + - family: Amiri + fonts: + - asset: assets/fonts/Amiri-Regular.ttf + - asset: assets/fonts/Amiri-Bold.ttf + weight: 700 + diff --git a/stitch/checklist_active_nav/code.html b/stitch/checklist_active_nav/code.html new file mode 100644 index 0000000..0445aee --- /dev/null +++ b/stitch/checklist_active_nav/code.html @@ -0,0 +1,188 @@ + + + + + +Daily Worship Checklist + + + + + + + + + +
+ +
+
+
+

Daily Worship

+

Tuesday, 24 Oct

+
+
+calendar_today +
+
+ +
+
+auto_awesome +
+
+
+
+

Today's Goal

+

66% Complete

+
+
+

4 / 6 Tasks

+
+
+
+
+
+

Almost there! Two more tasks to finish your daily routine.

+
+
+
+ +
+

Religious Tasks

+ +
+
+mosque +
+
+

Shalat 5 Waktu

+

Compulsory prayers

+
+
+ +
+
+ +
+
+menu_book +
+
+

Baca Al-Quran

+

Min. 2 pages per day

+
+
+ +
+
+ +
+
+nights_stay +
+
+

Tahajjud

+

Night prayer

+
+
+ +
+
+ +
+
+wb_sunny +
+
+

Dhuha

+

Morning prayer

+
+
+ +
+
+ +
+
+volunteer_activism +
+
+

Sedekah

+

Daily charity

+
+
+ +
+
+ +
+
+reorder +
+
+

Rawatib

+

Sunnah prayers

+
+
+ +
+
+
+ + +
+ \ No newline at end of file diff --git a/stitch/checklist_active_nav/screen.png b/stitch/checklist_active_nav/screen.png new file mode 100644 index 0000000..b548a6e Binary files /dev/null and b/stitch/checklist_active_nav/screen.png differ diff --git a/stitch/checklist_dark_mode/code.html b/stitch/checklist_dark_mode/code.html new file mode 100644 index 0000000..69605d2 --- /dev/null +++ b/stitch/checklist_dark_mode/code.html @@ -0,0 +1,150 @@ + + + + + + + + + + + + + + + +
+
+
+arrow_back +
+

Checklist

+
+ +
+
+
+ +
+ +
+
+
+

Today's Progress

+

68% Completed

+
+

12 of 18 tasks

+
+
+
+
+
+ +
+
+

Today's Tasks

+5 Pending +
+
+ + + + +
+
+ +
+

Upcoming

+
+
+calendar_today +

Prepare design handover

+
+
+calendar_today +

Monthly review meeting

+
+
+
+ + +
+ + + \ No newline at end of file diff --git a/stitch/checklist_dark_mode/screen.png b/stitch/checklist_dark_mode/screen.png new file mode 100644 index 0000000..8a6ab77 Binary files /dev/null and b/stitch/checklist_dark_mode/screen.png differ diff --git a/stitch/dashboard_active_nav/code.html b/stitch/dashboard_active_nav/code.html new file mode 100644 index 0000000..b8c8e57 --- /dev/null +++ b/stitch/dashboard_active_nav/code.html @@ -0,0 +1,223 @@ + + + + + + + + + + +Islamic Diary App + + + + +
+
+
+ +
+
+

Welcome back,

+

Assalamu'alaikum, Akhmad

+
+
+ +
+
+ +
+
+
+
+
+
+schedule +

Next Prayer

+
+

Maghrib at 17:55

+

Countdown: 01:42:10

+
+ + +
+
+
+
+ +
+
+

Daily Prayer Times

+Today +
+
+ +
+wb_twilight +

Fajr

+

04:32

+
+ +
+wb_sunny +

Dhuhr

+

12:05

+
+ +
+filter_drama +

Asr

+

15:20

+
+ +
+wb_twilight +

Maghrib

+

17:55

+
+ +
+dark_mode +

Isha

+

19:10

+
+
+
+ +
+
+
+
+

Today's Checklist

+

7 dari 10 Ibadah selesai

+
+
+ + + +70% +
+
+
+
+check_circle +
+

Sholat Fardhu

+

4 of 5 complete

+
+
+
+check_circle +
+

Tilawah Quran

+

1 Juz targeted

+
+
+ +
+
+
+ +
+

Weekly Progress

+
+
+
+
+
+Mon +
+
+
+
+
+Tue +
+
+
+
+
+Wed +
+
+
+
+
+Thu +
+
+
+
+
+Fri +
+
+
+
+
+Sat +
+
+
+
+
+Sun +
+
+
+
+ + + \ No newline at end of file diff --git a/stitch/dashboard_active_nav/screen.png b/stitch/dashboard_active_nav/screen.png new file mode 100644 index 0000000..30cc15f Binary files /dev/null and b/stitch/dashboard_active_nav/screen.png differ diff --git a/stitch/dashboard_dark_mode/code.html b/stitch/dashboard_dark_mode/code.html new file mode 100644 index 0000000..8bb2138 --- /dev/null +++ b/stitch/dashboard_dark_mode/code.html @@ -0,0 +1,179 @@ + + + + + +Islamic Dashboard - Dark Mode + + + + + + + + + +
+
+
+person +
+
+

Assalamu Alaikum,

+

Ahmed Hassan

+
+
+
+ + +
+
+
+
+
+
+Islamic Art Header +
+

Next Prayer

+

Dhuhr

+
+schedule +12:45 PM +
+

-02:45:12 remaining

+
+
+
+
+
+

Daily Prayers

+ +
+
+
+
+
+wb_twilight +
+
+

Fajr

+

05:22 AM

+
+
+notifications_off +
+
+
+
+wb_sunny +
+
+

Dhuhr

+

12:45 PM

+
+
+notifications_active +
+
+
+
+sunny +
+
+

Asr

+

04:15 PM

+
+
+notifications +
+
+
+
+

Quick Actions

+
+ + +
+
+
+
+
+

Verse of the Day

+share +
+

+ "So remember Me; I will remember you. And be grateful to Me and do not deny Me." +

+

Surah Al-Baqarah 2:152

+
+
+
+ + \ No newline at end of file diff --git a/stitch/dashboard_dark_mode/screen.png b/stitch/dashboard_dark_mode/screen.png new file mode 100644 index 0000000..f12c02d Binary files /dev/null and b/stitch/dashboard_dark_mode/screen.png differ diff --git a/stitch/dashboard_with_adhan_iqamah_logic/code.html b/stitch/dashboard_with_adhan_iqamah_logic/code.html new file mode 100644 index 0000000..a254b3c --- /dev/null +++ b/stitch/dashboard_with_adhan_iqamah_logic/code.html @@ -0,0 +1,225 @@ + + + + + + + + + + +Islamic Diary App + + + + +
+
+
+ +
+
+

Welcome back,

+

Assalamu'alaikum, Akhmad

+
+
+ +
+
+ +
+
+
+
+
+
+schedule +

Next Prayer

+
+

Maghrib at 17:55

+

Countdown: 01:42:10

+
+ + +
+
+
+
+ +
+
+

Daily Prayer Times

+Today +
+
+ +
+wb_twilight +

Fajr

+

04:32

+
+ +
+wb_sunny +

Dhuhr

+

12:05

+
+ +
+filter_drama +

Asr

+

15:20

+
+ +
+wb_twilight +

Maghrib

+

17:55

+
+notifications +
+ +
+dark_mode +

Isha

+

19:10

+
+
+
+ +
+
+
+
+

Today's Checklist

+

7 dari 10 Ibadah selesai

+
+
+ + + +70% +
+
+
+
+check_circle +
+

Sholat Fardhu

+

4 of 5 complete

+
+
+
+check_circle +
+

Tilawah Quran

+

1 Juz targeted

+
+
+ +
+
+
+ +
+

Weekly Progress

+
+
+
+
+
+Mon +
+
+
+
+
+Tue +
+
+
+
+
+Wed +
+
+
+
+
+Thu +
+
+
+
+
+Fri +
+
+
+
+
+Sat +
+
+
+
+
+Sun +
+
+
+
+ + + \ No newline at end of file diff --git a/stitch/dashboard_with_adhan_iqamah_logic/screen.png b/stitch/dashboard_with_adhan_iqamah_logic/screen.png new file mode 100644 index 0000000..15f1016 Binary files /dev/null and b/stitch/dashboard_with_adhan_iqamah_logic/screen.png differ diff --git a/stitch/dzikir_active_nav/code.html b/stitch/dzikir_active_nav/code.html new file mode 100644 index 0000000..816f74b --- /dev/null +++ b/stitch/dzikir_active_nav/code.html @@ -0,0 +1,177 @@ + + + + + + + + + + + + + + + +
+ +
+
+
+arrow_back +
+

Dzikir Pagi & Petang

+
+ +
+
+ + +
+ +
+ +
+

Dzikir Pagi

+

+ Dibaca setelah shalat Shubuh hingga terbit matahari +

+
+ +
+ +
+
+1 Kali +01 +
+
+ أَعُوذُ بِاللَّهِ مِنَ الشَّيْطَانِ الرَّجِيمِ. بِسْمِ اللَّهِ الرَّحْمَنِ الرَّحِيمِ. اللّهُ لاَ إِلَهَ إِلاَّ هُوَ الْحَيُّ الْقَيُّومُ +
+
+

+ Allahu laa ilaha illa huwal hayyul qayyum... +

+

+ "Allah, tidak ada Tuhan (yang berhak disembah) melainkan Dia Yang Hidup kekal lagi terus menerus mengurus (makhluk-Nya)..." +

+
+ + +
+ +
+
+3 Kali +02 +
+
+ بِسْمِ اللَّهِ الرَّحْمَنِ الرَّحِيمِ. قُلْ هُوَ اللَّهُ أَحَدٌ. اللَّهُ الصَّمَدُ +
+
+

+ Bismillahir rahmanir rahim. Qul huwallahu ahad... +

+

+ "Katakanlah: 'Dialah Allah, Yang Maha Esa. Allah adalah Tuhan yang bergantung kepada-Nya segala sesuatu'." +

+
+ + +
+ +
+
+100 Kali +03 +
+
+ سُبْحَانَ اللهِ وَبِحَمْدِهِ +
+
+

+ Subhanallahi wa bihamdihi +

+

+ "Maha Suci Allah dan segala puji bagi-Nya." +

+
+ + +
+
+
+ + + +
+ +
+
+ \ No newline at end of file diff --git a/stitch/dzikir_active_nav/screen.png b/stitch/dzikir_active_nav/screen.png new file mode 100644 index 0000000..6c08992 Binary files /dev/null and b/stitch/dzikir_active_nav/screen.png differ diff --git a/stitch/dzikir_dark_mode/code.html b/stitch/dzikir_dark_mode/code.html new file mode 100644 index 0000000..b932706 --- /dev/null +++ b/stitch/dzikir_dark_mode/code.html @@ -0,0 +1,133 @@ + + + + + +Dzikir Counter - Dark Mode + + + + + + + + + + +
+
+arrow_back +
+
+

Morning Adhkar

+

Dzikir Al-Ma'thurat

+
+
+ +
+
+ +
+ +
+
+
+

Current Progress

+

15 / 33

+
+
+ 45% Complete +
+
+
+
+
+
+ +
+
+

+ سُبْحَانَ اللهِ وَبِحَمْدِهِ +

+

+ "Glory be to Allah and His is the praise." +

+

+ Recite 100 times in the morning and evening +

+
+ + + +
+
+ + + \ No newline at end of file diff --git a/stitch/dzikir_dark_mode/screen.png b/stitch/dzikir_dark_mode/screen.png new file mode 100644 index 0000000..12de6ef Binary files /dev/null and b/stitch/dzikir_dark_mode/screen.png differ diff --git a/stitch/imsakiyah_active_nav/code.html b/stitch/imsakiyah_active_nav/code.html new file mode 100644 index 0000000..08e173b --- /dev/null +++ b/stitch/imsakiyah_active_nav/code.html @@ -0,0 +1,191 @@ + + + + + +Prayer Calendar - Imsakiyah + + + + + + + + + +
+ +
+
+arrow_back +
+

Prayer Calendar

+
+ +
+
+ +
+
+ + + +
+
+location_on +
+

Your Location

+

Jakarta, Indonesia

+
+expand_more +
+
+ +
+
+Day +Fajr +Sun +Dhuhr +Asr +Magh +Isha +
+
+ +
+ +
+
+MAR +12 +
+04:42 +05:58 +12:04 +15:12 +18:08 +19:17 +
+ +
+
+MAR +13 +
+04:42 +05:58 +12:04 +15:11 +18:07 +19:16 +
+
+
+MAR +14 +
+04:41 +05:58 +12:03 +15:11 +18:07 +19:16 +
+
+
+MAR +15 +
+04:41 +05:58 +12:03 +15:10 +18:06 +19:15 +
+
+
+MAR +16 +
+04:41 +05:58 +12:03 +15:09 +18:06 +19:15 +
+
+
+MAR +17 +
+04:41 +05:57 +12:02 +15:08 +18:05 +19:14 +
+
+
+MAR +18 +
+04:41 +05:57 +12:02 +15:08 +18:05 +19:14 +
+
+ + +
+ \ No newline at end of file diff --git a/stitch/imsakiyah_active_nav/screen.png b/stitch/imsakiyah_active_nav/screen.png new file mode 100644 index 0000000..7493d3f Binary files /dev/null and b/stitch/imsakiyah_active_nav/screen.png differ diff --git a/stitch/imsakiyah_dark_mode/code.html b/stitch/imsakiyah_dark_mode/code.html new file mode 100644 index 0000000..c696253 --- /dev/null +++ b/stitch/imsakiyah_dark_mode/code.html @@ -0,0 +1,162 @@ + + + + + + + + + + + + + +
+
+ +
+

Imsakiyah 1445 H

+

Jakarta, Indonesia

+
+ +
+
+
+
+

Tuesday, March 12

+

1 Ramadan 1445 H

+
+
+
+
+
+wb_twilight +
+
+

Imsak

+

Preparation for fasting

+
+
+

04:32

+
+
+
+
+wb_sunny +
+
+

Subuh

+

Morning prayer

+
+
+

04:42

+
+
+
+
+light_mode +
+
+

Dzuhur

+

Noon prayer

+
+
+

12:05

+
+
+
+
+partly_cloudy_day +
+
+

Ashar

+

Afternoon prayer

+
+
+

15:12

+
+
+
+
+wb_twilight +
+
+

Maghrib

+

Breaking of fast

+
+
+
+

18:10

+NEXT +
+
+
+
+
+dark_mode +
+
+

Isya

+

Night prayer

+
+
+

19:19

+
+
+
+
+info +

Fasting Tip

+
+

+ Stay hydrated during Suhoor and Iftar by drinking plenty of water and eating water-rich fruits like watermelon. +

+
+
+ + \ No newline at end of file diff --git a/stitch/imsakiyah_dark_mode/screen.png b/stitch/imsakiyah_dark_mode/screen.png new file mode 100644 index 0000000..e6f5826 Binary files /dev/null and b/stitch/imsakiyah_dark_mode/screen.png differ diff --git a/stitch/kiblat_dark_mode/code.html b/stitch/kiblat_dark_mode/code.html new file mode 100644 index 0000000..3b6b2e7 --- /dev/null +++ b/stitch/kiblat_dark_mode/code.html @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + +
+ +

Qibla Direction

+
+ +
+
+ +
+ +
+

128° SE

+
+location_on +

Makkah, Saudi Arabia

+
+
+ +
+ +
+ +
+ +
+
+ +
+
+
+
+
+ +N +S +W +E + +
+
+mosque +
+
+
+
+
+ +
+
+

High Accuracy

+
+
+ + + \ No newline at end of file diff --git a/stitch/kiblat_dark_mode/screen.png b/stitch/kiblat_dark_mode/screen.png new file mode 100644 index 0000000..12a6082 Binary files /dev/null and b/stitch/kiblat_dark_mode/screen.png differ diff --git a/stitch/laporan_active_nav/code.html b/stitch/laporan_active_nav/code.html new file mode 100644 index 0000000..b29c433 --- /dev/null +++ b/stitch/laporan_active_nav/code.html @@ -0,0 +1,182 @@ + + + + + + + + + + + + + + +
+ +
+ +

Worship Quality Report

+ +
+ + +
+ +
+
+
+

Daily Completion

+
+

85%

+ +trending_up 5% + +
+
+
+analytics +
+
+
+
+
+
+
+Mon +
+
+
+
+
+Tue +
+
+
+
+
+Wed +
+
+
+
+
+Thu +
+
+
+
+
+Fri +
+
+
+
+
+Sat +
+
+
+
+
+Sun +
+
+
+ +
+

Insights

+
+ +
+
+stars +
+
+

Paling Rajin

+

Shalat Dhuha

+
+100% +
+ +
+
+trending_down +
+
+

Perlu Ditingkatkan

+

Sedekah

+
+45% +
+
+
+ +
+format_quote +
+

+ "The most beloved of deeds to Allah are those that are most consistent, even if they are small." +

+

— Sahih Bukhari

+
+
+
+ + +
+ \ No newline at end of file diff --git a/stitch/laporan_active_nav/screen.png b/stitch/laporan_active_nav/screen.png new file mode 100644 index 0000000..83f45af Binary files /dev/null and b/stitch/laporan_active_nav/screen.png differ diff --git a/stitch/laporan_dark_mode/code.html b/stitch/laporan_dark_mode/code.html new file mode 100644 index 0000000..dd86fbc --- /dev/null +++ b/stitch/laporan_dark_mode/code.html @@ -0,0 +1,200 @@ + + + + + + + + + + + + + + +
+ +
+
+arrow_back +
+

Reports

+
+ +
+
+ + + +
+

Activity Overview

+ +
+
+
+

Steps Count

+

54,230

+
+

Last 7 days

+ +trending_up + +12.5% + +
+
+ +
+
+

Mon

+
+

Tue

+
+

Wed

+
+

Thu

+
+

Fri

+
+

Sat

+
+

Sun

+
+
+
+ +
+
+
+favorite +

Avg Heart Rate

+
+

72 bpm

+
+
+
+
+
+
+local_fire_department +

Calories Burned

+
+

1,450 kcal

+
+
+
+
+
+ +
+
+
+
+

Sleep Quality

+

7h 45m

+
+
Optimal
+
+
+
+
+
+
+
+
+
+
+
+11 PM +2 AM +5 AM +7 AM +
+
+
+
+ + +
+ \ No newline at end of file diff --git a/stitch/laporan_dark_mode/screen.png b/stitch/laporan_dark_mode/screen.png new file mode 100644 index 0000000..0a401aa Binary files /dev/null and b/stitch/laporan_dark_mode/screen.png differ diff --git a/stitch/qibla_active_nav/code.html b/stitch/qibla_active_nav/code.html new file mode 100644 index 0000000..55ea363 --- /dev/null +++ b/stitch/qibla_active_nav/code.html @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + +
+ +
+ +

Qibla Finder

+ +
+ +
+ +
+
+location_on +

Mecca, Saudi Arabia

+
+

292° NW

+
+ +
+ +
+
+
+ +
+
+
+
+
+
+ +
+ +
+mosque +
+ +
+
+ +
+
+ +
+

Compass Calibrated

+
+
+ +
+
+
+ + +
+ \ No newline at end of file diff --git a/stitch/qibla_active_nav/screen.png b/stitch/qibla_active_nav/screen.png new file mode 100644 index 0000000..4562091 Binary files /dev/null and b/stitch/qibla_active_nav/screen.png differ diff --git a/stitch/quran_dark_mode/code.html b/stitch/quran_dark_mode/code.html new file mode 100644 index 0000000..e5c8c7d --- /dev/null +++ b/stitch/quran_dark_mode/code.html @@ -0,0 +1,185 @@ + + + + + + + + + + + + + + + +
+
+ +
+

Al-Fatihah

+

The Opening

+
+
+ +
+
+ +
+ + +
+
+ +
+ +
+
+
+ 1 +
+
+ + +
+
+
+

بِسْمِ اللَّهِ الرَّحْمَٰنِ الرَّحِيمِ

+
+
+

1:1

+

In the name of Allah, the Entirely Merciful, the Especially Merciful.

+
+
+ +
+
+
+ 2 +
+
+ + +
+
+
+

الْحَمْدُ لِلَّهِ رَبِّ الْعَالَمِينَ

+
+
+

1:2

+

[All] praise is [due] to Allah, Lord of the worlds -

+
+
+ +
+
+
+ 3 +
+
+ + +
+
+
+

الرَّحْمَٰنِ الرَّحِيمِ

+
+
+

1:3

+

The Entirely Merciful, the Especially Merciful,

+
+
+
+ +
+ +
+
+music_note +
+
+

Mishary Rashid Alafasy

+

Playing: Surah Al-Fatihah

+
+
+ + + +
+
+ + +
+ \ No newline at end of file diff --git a/stitch/quran_dark_mode/screen.png b/stitch/quran_dark_mode/screen.png new file mode 100644 index 0000000..fd37ac0 Binary files /dev/null and b/stitch/quran_dark_mode/screen.png differ diff --git a/stitch/quran_murattal_active_nav/code.html b/stitch/quran_murattal_active_nav/code.html new file mode 100644 index 0000000..262e3d7 --- /dev/null +++ b/stitch/quran_murattal_active_nav/code.html @@ -0,0 +1,219 @@ + + + + + + + + + + + + + + + +
+ +
+ +
+

Al-Fatihah

+

7 Ayat • Mekkah

+
+ +
+ +
+
+
+Ayat 1 +dari 7 +
+
+ + +
+
+
+
+
+
+ +
+ +
+

بِسْمِ اللَّهِ الرَّحْمَٰنِ الرَّحِيمِ

+

"Dengan nama Allah Yang Maha Pengasih, Maha Penyayang."

+
+ +
+
+
+
+ 1 +
+ + +
+
+

ٱلْحَمْدُ لِلَّهِ رَبِّ ٱلْعَـٰلَمِينَ

+
+
+
+

Segala puji bagi Allah, Tuhan semesta alam.

+
+
+ +
+
+
+
+
+ +
+
+
+
+ 2 +
+ +
+
+

ٱلرَّحْمَـٰنِ ٱلرَّحِيمِ

+
+
+
+

Maha Pengasih, Maha Penyayang.

+
+
+ +
+
+
+
+ 3 +
+ +
+
+

مَـٰلِكِ يَوْمِ ٱلدِّينِ

+
+
+
+

Pemilik hari pembalasan.

+
+
+ +
+
+ + + +
+
+
+bar_chart +
+
+
+
+
+
+

Mishary Rashid Alafasy

+
+

Playing Al-Fatihah

+ +1:24 +
+
+
+ + + +
+
+
+ \ No newline at end of file diff --git a/stitch/quran_murattal_active_nav/screen.png b/stitch/quran_murattal_active_nav/screen.png new file mode 100644 index 0000000..dce3f50 Binary files /dev/null and b/stitch/quran_murattal_active_nav/screen.png differ diff --git a/stitch/quran_reading_active_nav/code.html b/stitch/quran_reading_active_nav/code.html new file mode 100644 index 0000000..672976f --- /dev/null +++ b/stitch/quran_reading_active_nav/code.html @@ -0,0 +1,212 @@ + + + + + + + + + + + + + + + +
+ +
+ +
+

Al-Fatihah

+

7 Ayat • Mekkah

+
+ +
+ +
+
+
+Ayat 1 +dari 7 +
+
+ + +
+
+
+
+
+
+ +
+ +
+

بِسْمِ اللَّهِ الرَّحْمَٰنِ الرَّحِيمِ

+

"Dengan nama Allah Yang Maha Pengasih, Maha Penyayang."

+
+ +
+
+
+
+ 1 +
+ + +
+
+

ٱلْحَمْدُ لِلَّهِ رَبِّ ٱلْعَـٰلَمِينَ

+
+
+
+

Segala puji bagi Allah, Tuhan semesta alam.

+
+
+ +
+
+
+
+
+ +
+
+
+
+ 2 +
+ +
+
+

ٱلرَّحْمَـٰنِ ٱلرَّحِيمِ

+
+
+
+

Maha Pengasih, Maha Penyayang.

+
+
+ +
+
+
+
+ 3 +
+ +
+
+

مَـٰلِكِ يَوْمِ ٱلدِّينِ

+
+
+
+

Pemilik hari pembalasan.

+
+
+ +
+
+ + + +
+
+equalizer +
+
+

Mishary Rashid Alafasy

+

Playing Al-Fatihah

+
+
+ + + +
+
+
+ \ No newline at end of file diff --git a/stitch/quran_reading_active_nav/screen.png b/stitch/quran_reading_active_nav/screen.png new file mode 100644 index 0000000..e7f8706 Binary files /dev/null and b/stitch/quran_reading_active_nav/screen.png differ diff --git a/stitch/settings_dark_mode/code.html b/stitch/settings_dark_mode/code.html new file mode 100644 index 0000000..b233335 --- /dev/null +++ b/stitch/settings_dark_mode/code.html @@ -0,0 +1,157 @@ + + + + + +Settings - Dark Mode + + + + + + + + + + +
+
+ +

Settings

+
+
+
+ +
+ +
+
+Profile +
+
+

Alex Rivers

+

alex.rivers@example.com

+
+ +
+ +

Preferences

+ +
+
+
+dark_mode +
+

Dark Mode

+
+
+ +
+
+ +
+
+
+notifications +
+

Notifications

+
+
+ +
+
+ +

Account

+ + +
+ +
+
+ + + \ No newline at end of file diff --git a/stitch/settings_dark_mode/screen.png b/stitch/settings_dark_mode/screen.png new file mode 100644 index 0000000..01f9859 Binary files /dev/null and b/stitch/settings_dark_mode/screen.png differ diff --git a/stitch/settings_with_dark_mode_option/code.html b/stitch/settings_with_dark_mode_option/code.html new file mode 100644 index 0000000..4c1210d --- /dev/null +++ b/stitch/settings_with_dark_mode_option/code.html @@ -0,0 +1,229 @@ + + + + + + + + + + +Settings - Deen App + + + + +
+
+ +

Settings

+
+
+
+ +
+

Notifications

+
+ +
+
+
+

Fajr Adhan

+

Notification for dawn prayer

+
+ +
+
+
+

Dhuhr Adhan

+

Notification for noon prayer

+
+ +
+
+
+

Asr Adhan

+

Notification for afternoon prayer

+
+ +
+
+
+

Maghrib & Isha

+

Evening and Night prayer alerts

+
+ +
+ +
+
+

Iqamah Countdown

+

Time remaining until congregational prayer

+
+ +
+
+
+
+ +
+

Display Settings

+
+
+
+
+play_circle +

Murattal Player

+
+ +
+
+
+translate +

Show Translation

+
+ +
+
+
+format_size +

Arabic Font Size

+
+ +
+Small +Medium +Large +
+
+
+
+dark_mode +

Dark Mode

+
+ +
+
+
+ +
+

Daily Checklist

+
+
+ + +
+
+
+ +
+

App Info

+
+
+ +
+

Version

+

2.4.0 (Build 102)

+
+ +
+
+
+
+ + + \ No newline at end of file diff --git a/stitch/settings_with_dark_mode_option/screen.png b/stitch/settings_with_dark_mode_option/screen.png new file mode 100644 index 0000000..b931ce9 Binary files /dev/null and b/stitch/settings_with_dark_mode_option/screen.png differ diff --git a/test/widget_test.dart b/test/widget_test.dart new file mode 100644 index 0000000..06e3891 --- /dev/null +++ b/test/widget_test.dart @@ -0,0 +1,30 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:jamshalat_diary/main.dart'; + +void main() { + testWidgets('Counter increments smoke test', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const MyApp()); + + // Verify that our counter starts at 0. + expect(find.text('0'), findsOneWidget); + expect(find.text('1'), findsNothing); + + // Tap the '+' icon and trigger a frame. + await tester.tap(find.byIcon(Icons.add)); + await tester.pump(); + + // Verify that our counter has incremented. + expect(find.text('0'), findsNothing); + expect(find.text('1'), findsOneWidget); + }); +} diff --git a/web/favicon.png b/web/favicon.png new file mode 100644 index 0000000..8aaa46a Binary files /dev/null and b/web/favicon.png differ diff --git a/web/icons/Icon-192.png b/web/icons/Icon-192.png new file mode 100644 index 0000000..b749bfe Binary files /dev/null and b/web/icons/Icon-192.png differ diff --git a/web/icons/Icon-512.png b/web/icons/Icon-512.png new file mode 100644 index 0000000..88cfd48 Binary files /dev/null and b/web/icons/Icon-512.png differ diff --git a/web/icons/Icon-maskable-192.png b/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000..eb9b4d7 Binary files /dev/null and b/web/icons/Icon-maskable-192.png differ diff --git a/web/icons/Icon-maskable-512.png b/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000..d69c566 Binary files /dev/null and b/web/icons/Icon-maskable-512.png differ diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..441b864 --- /dev/null +++ b/web/index.html @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + jamshalat_diary + + + + + + + diff --git a/web/manifest.json b/web/manifest.json new file mode 100644 index 0000000..e34c780 --- /dev/null +++ b/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "jamshalat_diary", + "short_name": "jamshalat_diary", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +}