commit ad33b01231125a0e3ba3f193f14549199bca332f Author: dwindown Date: Mon Mar 30 21:28:44 2026 +0700 Initial project import and stabilization baseline diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3820a95 --- /dev/null +++ b/.gitignore @@ -0,0 +1,45 @@ +# 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 diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..51c0bb5 --- /dev/null +++ b/.metadata @@ -0,0 +1,45 @@ +# 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: "db50e20168db8fee486b9abf32fc912de3bc5b6a" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a + base_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a + - platform: android + create_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a + base_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a + - platform: ios + create_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a + base_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a + - platform: linux + create_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a + base_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a + - platform: macos + create_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a + base_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a + - platform: web + create_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a + base_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a + - platform: windows + create_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a + base_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a + + # 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/PRD.md b/PRD.md new file mode 100644 index 0000000..f174b48 --- /dev/null +++ b/PRD.md @@ -0,0 +1,76 @@ +# Product Requirements Document: Smart Digital Prayer Clock (Jam Shalat Masjid) + +## 1. Executive Summary +**Objective**: Develop a world-class, offline-first digital signage application for mosques running continuously (24/7) on Android TV Boxes without internet dependency. +**Key Directives**: Avoid the dated, cluttered spreadsheet look of traditional digital clocks. Implement "The Sacred Horizon" design system, focusing on elegance, high visual impact, wide margins, and asymmetrical editorial layouts. + +## 2. Core Architecture & Tech Stack +* **Target Platform**: Android TV Box (Android 13/14, 4GB RAM, 64GB ROM) — 1920x1080 Landscape Display. +* **Framework**: Flutter (Latest Stable). +* **Architecture Pattern**: Clean Architecture / MVVM. +* **State Management**: Riverpod (Recommended for predictable timer-based state transitions). +* **Local Storage**: Isar (High-performance NoSQL for offline caching of monthly APIs and Settings). +* **Background Management**: `wakelock_plus` package must be implemented to prevent screen sleeping. Timer handling must be memory-leak safe. + +## 3. Design System Strategy: "The Sacred Horizon" +* **Creative North Star**: "The Celestial Anchor" - Calm, reverence, and absolute clarity. +* **Color Palette**: + * **Background (The Void)**: `#131313` + * **Primary (Living Green)**: `#88d982` (Used for current time status and active prayer highlights) + * **Secondary (Sacred Gold)**: `#e9c349` (Used for alerts, Jumu'ah highlights, running text) +* **Typography**: + * **Plus Jakarta Sans**: Display (`3.5rem - 12rem`), used for the massive clock, prayer titles. Bold and commanding. Letter-spacing tighter (`-0.02em`). + * **Manrope**: Used for functional elements (Sub-timers, info panels, marquee). Clean and readable from 2-3 meters. +* **Surfaces & Depth (The "No-Line" Rule)**: + * Strictly **NO 1px solid borders** between content. Separation is created using tonal layering (e.g. `surface_container_low` `#1c1b1b` over `#131313` background). + * Use frosted glass effects (`backdrop-filter`) for floating overlays. Give active items ambient glows instead of hard drop shadows. + +## 4. App State Machine & UI Experiences + +The application transitions through a cyclical logic based on `DateTime.now()`. + +### State 1: NORMAL / ROTATION +Automatically rotates between Main View and Slideshow based on Admin configurations using smooth `AnimatedSwitcher`. +* **Main Screen**: Asymmetrical layout. High-contrast massive clock in the center/center-left. Hijri & Gregorian dates. `Bottom Area`: 5/6 prayer cards spanning horizontally. The active/upcoming prayer is elevated using `primary_container` color. `Bottom Edge`: Continuous marquee text. +* **Slideshow Screen**: Cinematic full-screen graphic behind a subtle gradient overlay. A floating "Glass" widget anchors to the top-right showing current time and upcoming prayer countdown. Central focus is on the poster/quote text. + +### State 2: MENUJU ADZAN +* Triggered ~X minutes before the exact prayer time (configurable per prayer). +* Forced lock to Main Screen (cancels slideshow rotations). Focus shifted to the active countdown. Pulsing UI indicators. + +### State 3: ADZAN (Time Reached) +* Full-screen alert takeover. Glowing `Secondary` texts: "WAKTU ADZAN [NAMA SHALAT]". Short audio chime. + +### State 4: MENUJU IQOMAH +* Massive, dominant countdown timer taking over the screen center. "PERSIAPAN SHALAT. Luruskan dan rapatkan shaf". +* **Jumu'ah Override**: On Fridays, Dzuhur is replaced with "JUMAT". During the Iqomah countdown phase, the screen transforms into a "Friday Khutbah Information" panel—displaying Imam and Khatib names with an overarching countdown to the Khutbah itself. + +### State 5: SHALAT / BLACK SCREEN +* Post-iqomah. Screen turns to absolute minimal `Black` (`#000000`). +* Only low-opacity (10-15%) texts remain: "Shalat Sedang Berlangsung" and "Mohon Nonaktifkan Alat Komunikasi". + +### State 6: KEMBALI NORMAL +* System resets to `STATE 1` after the configured blank screen duration ends. + +## 5. Offline-First API & Admin Configuration + +The App runs 99% offline. Admin dashboard is accessed securely (e.g., hidden D-pad combo). +* **TV-Optimized Admin UX**: Driven purely by D-Pad logic. Focus management is critical (High contrast borders & states for selected inputs). Avoid heavy typing by utilizing sliders, presets, and large selection buttons. +* **Admin Features (Bento Grid Layout)**: + 1. **Identity Settings**: Mosque Name, Address. + 2. **Sync Data**: The only feature that requires internet (via mobile tethering). Button pulls 1 full month of data from public prayer API based on City ID. + 3. **Timing & Durations**: + * Pre-Adzan Lead (minutes per prayer). + * Iqomah Duration (minutes per prayer). + * Blank Screen Duration (minutes, separate for normal days and Friday). + 4. **Appearance**: Marquee text configuration, Khatib / Imam input fields for the week. + +## 6. Implementation Roadmap + +1. **Phase 1: Foundation Setup**: Initialize Flutter project, Riverpod, Isar database, and `wakelock_plus`. Define configuration models. +2. **Phase 2: Data layer & State Machine**: API fetching, offline database seed logic. Build the central `Timer` loop dictating the State Machine. +3. **Phase 3: Design System Tokens**: Translate "The Sacred Horizon" stitch designs into global Flutter themes, custom text styles, and core widgets. +4. **Phase 4: TV UI Development**: + * Build all 5 UI states (Normal/Slideshow, Adzan Alert, Iqomah Countdown, Khutbah Override, Black Screen). +5. **Phase 5: TV Admin Dashboard**: Implement the D-Pad optimized configuration interface ensuring state saves correctly to `Isar`. +6. **Phase 6: Final Polish**: Focus transition animations, memory leak testing (crucial for 24/7 runtimes), and Android APK generation. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e4d7055 --- /dev/null +++ b/README.md @@ -0,0 +1,119 @@ +# JamShalat Masjid Screen + +Smart digital prayer clock and mosque signage app built with Flutter for Android TV boxes and landscape displays. + +## Purpose + +This app is meant to run continuously inside a mosque as a full-screen prayer information display. It combines: + +- live clock and daily prayer schedule +- pre-adzan and adzan alert states +- iqomah countdown flow +- Friday (`Jumat`) special screens +- black screen during prayer +- slideshow and branded background support +- local admin panel for changing mosque identity, timing, and appearance settings + +The product direction is documented in [PRD.md](/Users/dwindown/CascadeProjects/jamshalat-masjid-screen/PRD.md). + +## Current Stack + +- Flutter +- Riverpod for app state +- Hive for local persistence +- `http` for remote APIs +- `wakelock_plus` for always-on screen behavior +- `audioplayers` for adzan and iqomah sounds + +## Current Architecture + +The current implementation is a pragmatic Flutter app centered around: + +- `lib/main.dart`: app bootstrap, Hive initialization, fullscreen setup +- `lib/providers.dart`: clock stream, settings state, screen-state machine, slideshow rotation +- `lib/data/local/models.dart`: Hive models and adapters +- `lib/data/services/`: sync, prayer API, background API, sound service +- `lib/features/home/`: public display screens +- `lib/features/admin/`: settings/admin panel + +This is not yet a strict Clean Architecture / MVVM implementation from the PRD. It is a working product foundation optimized for iteration speed. + +## Implemented Screens + +- Main screen +- Slideshow screen +- Adzan alert screen +- Iqomah countdown screen +- Friday preparation screen +- Friday khutbah screen +- Black screen during prayer +- Admin settings screen + +## Data Sources + +- Prayer times: MyQuran sholat API +- Background images: Unsplash API + +The app is designed to behave offline-first after schedule data has been synced and stored locally. + +## Assets + +Included assets currently used by the app: + +- fonts: Plus Jakarta Sans, Manrope +- sounds: `beep.mp3`, `3-detik-countdown.mp3` +- images: app icon and user-configured local slideshow/background images + +## Local Development + +### Requirements + +- Flutter SDK installed and available in `PATH` +- Android toolchain if building APKs + +### Run + +```bash +flutter pub get +flutter run +``` + +### Analyze + +```bash +flutter analyze +``` + +### Test + +```bash +flutter test +``` + +## Current Stabilization Status + +The app is in a workable development state, but not yet fully stabilized. + +Known issues at the time of writing: + +- `test/widget_test.dart` is broken placeholder boilerplate and currently fails `flutter test` +- Android manifest does not yet declare `INTERNET`, which will block API-dependent features on device +- there are multiple deprecated `withOpacity` usages reported by `flutter analyze` +- some placeholders are still hardcoded, including Hijri date text in Friday-related screens +- README and repo hygiene were incomplete and are being normalized + +## Immediate Priorities + +1. Fix test baseline so `flutter test` becomes useful again. +2. Add Android `INTERNET` permission. +3. Remove broken placeholders and obvious UI/data defects. +4. Stabilize schedule sync and runtime state transitions. +5. Continue feature work from the PRD after the baseline is reliable. + +## Notes About Git + +This workspace currently does not contain a local `.git` directory even though a remote repository exists: + +- `https://git.backoffice.biz.id/dwindown/jamshalat-masjid-screen.git` + +If needed, the local folder can be attached to that remote in a follow-up step. diff --git a/admin-ui-brief.md b/admin-ui-brief.md new file mode 100644 index 0000000..e688823 --- /dev/null +++ b/admin-ui-brief.md @@ -0,0 +1,883 @@ +# Flutter TV Admin UI: Best Practices & Implementation Guide + +## Executive Summary + +**Objective**: Build a world-class admin dashboard experience for Android TV using Flutter, maintaining the single-APK architecture while delivering excellent UX despite TV input constraints. + +**Key Insight**: Flutter is fully capable of building excellent TV dashboards. The cross-platform nature doesn't limit quality - it means we can build platform-optimized experiences with proper focus management and TV-specific widgets. + +**Target Platform**: Android TV Box (1920x1080 landscape, D-pad navigation, no touch input) + +--- + +## 1. Understanding Android TV UX Constraints + +### Critical Challenges + +``` +❌ No touch input (D-pad navigation only) +❌ Awkward text input (on-screen keyboard via remote) +❌ Screen distance (2-3 meters from viewer) +❌ Limited input methods (remote control only) +❌ No mouse hover states +❌ Small text is unreadable +``` + +### Our Advantages + +``` +✅ Flutter has excellent focus management built-in +✅ Rich animation and transition support +✅ Custom widgets are straightforward to build +✅ TV-specific packages available +✅ We control the entire experience +✅ Hardware acceleration for smooth animations +✅ Scalable UI via MediaQuery +``` + +--- + +## 2. Focus Management (The Foundation) + +Focus management is **the most critical** aspect of TV UI. All interactions flow through proper focus handling. + +### Core Principles + +1. **Always show focus**: Users must see what's currently selected +2. **Predictable navigation**: D-pad moves in expected directions +3. **Large focus targets**: Everything must be easily selectable +4. **Clear visual feedback**: Animated focus indicators +5. **Sound feedback**: Audible confirmation of focus changes + +### Implementation Pattern + +```dart +// All TV widgets should follow this pattern: +class TVWidget extends StatefulWidget { + @override + State createState() => _TVWidgetState(); +} + +class _TVWidgetState extends State { + late FocusNode _focusNode; + + @override + void initState() { + super.initState(); + _focusNode = FocusNode(); + + // Listen to focus changes for visual feedback + _focusNode.addListener(() { + setState(() { + // Update UI when focus changes + }); + + // Play sound feedback + if (_focusNode.hasFocus) { + TVSoundService().playFocusSound(); + } + }); + } + + @override + void dispose() { + _focusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Focus( + focusNode: _focusNode, + onKeyEvent: (node, event) { + // Handle D-pad navigation + return KeyEventResult.ignored; + }, + child: Builder( + builder: (context) { + final isFocused = _focusNode.hasFocus; + // Build focused/unfocused states + }, + ), + ); + } +} +``` + +--- + +## 3. TV Scaling System + +All dimensions must scale relative to screen width to ensure consistency across different TV sizes. + +```dart +class TVScaling { + static double scale(BuildContext context) { + return MediaQuery.of(context).size.width / 1920; + } + + // Helper methods for common scaled values + static double padding(BuildContext context) => 32 * scale(context); + static double gap(BuildContext context) => 24 * scale(context); + static double borderRadius(BuildContext context) => 12 * scale(context); + + // Typography scale + static double fontSizeHeading(BuildContext context) => 48 * scale(context); + static double fontSizeTitle(BuildContext context) => 36 * scale(context); + static double fontSizeBody(BuildContext context) => 28 * scale(context); + static double fontSizeCaption(BuildContext context) => 22 * scale(context); +} +``` + +--- + +## 4. TV-Optimized Form Widgets + +### A. TV TextField + +**Key Features:** +- Extra large hit targets (minimum 48x48dp) +- Clear focus indication (border + background + shadow) +- Keyboard hint for users +- Auto-scroll into view when focused +- Sound feedback on focus + +**Usage:** +```dart +TVTextField( + label: 'Nama Masjid', + value: settings.masjidName, + onChanged: (value) => updateSetting('masjidName', value), + hint: 'Masjid Ar-Rahman', + autofocus: false, +) +``` + +### B. TV Selection Field + +**Key Features:** +- Dropdown/selector pattern (avoids typing) +- Search functionality for long lists +- Clear visual hierarchy +- Keyboard navigation (arrow keys) +- Selected state indication + +**Usage:** +```dart +TVSelectionField( + label: 'Pilih Lokasi', + value: settings.cityIdApi, + options: [ + TVSelectionOption( + label: 'Kota Yogyakarta', + value: '577ef1154f3240ad5b9b413aa7346a1e', + subtitle: 'Daerah Istimewa Yogyakarta', + icon: Icons.location_city, + ), + TVSelectionOption( + label: 'Jakarta Pusat', + value: 'another_id_here', + subtitle: 'DKI Jakarta', + icon: Icons.location_city, + ), + ], + onChanged: (value) => saveLocation(value), +) +``` + +### C. TV Button + +**Key Features:** +- Large touch targets (minimum 48x48dp) +- Multiple style variants (primary, secondary, danger) +- Icon + label support +- Animated focus feedback +- Sound on press +- Keyboard activation (Enter/Select) + +**Usage:** +```dart +TVButton( + label: 'Simpan Pengaturan', + icon: Icons.save, + style: TVButtonStyle.primary, + onPressed: () => saveSettings(), +) + +// Icon-only variant +TVButton.icon( + icon: Icons.arrow_back, + label: 'Kembali', + onPressed: () => Navigator.pop(context), +) +``` + +### D. TV Toggle Switch + +**Key Features:** +- Large clickable area (entire row) +- Clear on/off state +- Animated transition +- Focus indication +- Label with description + +**Usage:** +```dart +TVSwitch( + label: 'Auto Kalibrasi Online', + subtitle: 'Sinkronisasi waktu otomatis saat online', + value: settings.autoCalibrationEnabled, + onChanged: (value) => updateSetting('autoCalibration', value), +) +``` + +--- + +## 5. Layout Patterns + +### A. Grid-Based Dashboard + +**Purpose**: Main admin navigation screen + +**Characteristics:** +- 3-column grid layout +- Large cards with icons +- Clear visual hierarchy +- Easy D-pad navigation +- Focus flows naturally left-to-right, top-to-bottom + +**Structure:** +``` +┌─────────────┬─────────────┬─────────────┐ +│ Location │ Appearance │ Settings │ +│ & Schedule│ │ │ +├─────────────┼─────────────┼─────────────┤ +│ Rotation │ About │ │ +└─────────────┴─────────────┴─────────────┘ +``` + +### B. Wizard-Style Multi-Step Forms + +**Purpose**: Break complex forms into manageable steps + +**Benefits:** +- Reduces cognitive load +- Easier navigation +- Progress indication +- Can save state between steps +- Less overwhelming than long forms + +**When to Use:** +- Initial setup wizard +- Complex configuration with multiple sections +- Settings that require multiple steps +- Data entry with validation + +**Structure:** +``` +Step 1: Identity → Step 2: Location → Step 3: Appearance → Step 4: Confirm + ↓ ↓ ↓ ↓ +[Masjid Name] [Select City] [Theme Color] [Review All] +[Address] [Sync Data] [Background] [Save & Exit] +``` + +### C. Tabbed Interface + +**Purpose**: Organize related settings + +**Benefits:** +- Clear categorization +- Easy to add new sections +- Familiar pattern from web +- Works well with D-pad + +**Tab Structure:** +1. **Identitas** - Mosque name, address +2. **Jadwal & Sync** - Location, synchronization +3. **Tampilan** - Theme, background, slideshow +4. **Rotasi** - Screen timing +5. **Pembarisan** - Updates, version +6. **Tentang** - App information + +--- + +## 6. Reducing Text Input (The Secret Sauce) + +The best TV admin interfaces **minimize typing**. This is crucial because: + +1. TV remotes are terrible for text input +2. On-screen keyboards are slow and error-prone +3. Screen distance makes typing difficult +4. User frustration increases with typing + +### Strategies to Minimize Typing + +#### A. Use Selection Instead of Text Input + +**Bad (Text Input):** +```dart +TextField( + decoration: InputDecoration(labelText: 'City ID API'), +) +``` + +**Good (Selection):** +```dart +TVSelectionField( + label: 'Pilih Kota', + value: settings.cityIdApi, + options: cityOptions, + onChanged: (value) => saveCity(value), +) +``` + +#### B. Search + Select Pattern + +For long lists (like 500+ Indonesian cities): + +```dart +TVSearchSelectField( + label: 'Cari Lokasi', + hint: 'Ketik nama kota...', + searchDelegate: MyQuranCitySearchDelegate(), + onSelect: (city) => saveCity(city), +) +``` + +#### C. Preset Options with Custom Value + +```dart +TVSelectionField( + label: 'Warna Tema', + value: settings.themeColor, + options: [ + TVSelectionOption(label: 'Hijau', value: '0xFF006400'), + TVSelectionOption(label: 'Biru', value: '0xFF0000FF'), + TVSelectionOption(label: 'Merah', value: '0xFFFF0000'), + TVSelectionOption(label: 'Emas', value: '0xFFFFD700'), + TVSelectionOption(label: 'Kustom...', value: 'custom'), + ], + onChanged: (value) { + if (value == 'custom') { + // Show color picker dialog + } else { + saveThemeColor(value); + } + }, +) +``` + +#### D. Slider for Numeric Values + +**Bad (Number Input):** +```dart +TextField( + keyboardType: TextInputType.number, + decoration: InputDecoration(labelText: 'Durasi Iqomah (menit)'), +) +``` + +**Good (Slider):** +```dart +TVSlider( + label: 'Durasi Iqomah', + value: settings.iqomahDuration.inMinutes, + min: 5, + max: 20, + divisions: 15, + unit: 'menit', + onChanged: (value) => saveIqomahDuration(Duration(minutes: value)), +) +``` + +#### E. Time Picker + +```dart +TVTimePicker( + label: 'Waktu Mulai Subuh', + value: settings.subuhStartTime, + onChanged: (value) => saveSubuhStartTime(value), +) +``` + +--- + +## 7. Visual Design Guidelines + +### Focus Indicators + +Focus indicators must be: +- **High contrast** (visible from 2-3 meters) +- **Animated** (smooth transitions, ~200ms) +- **Multi-layered** (border + background + shadow) +- **Consistent** (same pattern throughout app) + +**Recommended Focus States:** + +```dart +// Unfocused +border: 1px solid grey +background: white +shadow: none + +// Focused +border: 3px solid primary_color +background: primary_color_10_percent +shadow: primary_color_30_percent_blur_20px +``` + +### Typography Scale + +```dart +// Based on 1920px width baseline +Display (Clock): 120px // For large time displays +Heading: 48px // Screen titles +Title: 36px // Section headers, button labels +Body: 28px // Form labels, content +Caption: 22px // Metadata, hints +``` + +### Color Contrast + +- **Minimum contrast ratio**: 4.5:1 (WCAG AA) +- **Recommended contrast ratio**: 7:1 (WCAG AAA) +- **Focus indication**: Always use high contrast +- **Error states**: Red with white text +- **Success states**: Green with white text + +### Spacing System + +```dart +// Based on 8px grid system +padding_xs: 8px // Tight spacing +padding_sm: 16px // Compact +padding_md: 24px // Default +padding_lg: 32px // Comfortable +padding_xl: 48px // Generous +``` + +--- + +## 8. Navigation Patterns + +### D-Pad Navigation Flow + +``` + ↑ + ↑ + ← [Focus] → + ↓ + ↓ +``` + +**Rules:** +1. **Natural flow**: Left-to-right, top-to-bottom +2. **Wrap around**: Last item → first item in same row +3. **Section boundaries**: Clear visual separators +4. **Skip separators**: Focus should jump over dividers + +### Keyboard Shortcuts + +```dart +// Standard Android TV shortcuts +KEY_ENTER / KEY_SELECT: Activate focused item +KEY_BACK: Go back / cancel +KEY_MENU: Show context menu +KEY_MEDIA_PLAY_PAUSE: Play/pause media +KEY_MEDIA_FAST_FORWARD: Skip forward +KEY_MEDIA_REWIND: Skip back +``` + +--- + +## 9. Feedback Systems + +### Visual Feedback + +1. **Focus indicator**: Border + background + shadow +2. **Pressed state**: Scale down slightly (0.95x) +3. **Loading state**: Progress indicator + "Loading..." text +4. **Success state**: Green checkmark + success message +5. **Error state**: Red X + error message + +### Sound Feedback + +```dart +// Sound effects for different interactions +focus.mp3: Subtle "click" when focus moves +select.mp3: Confirming "ding" when item activated +error.mp3: Low "buzz" for errors +success.mp3: Pleasant "chime" for success +``` + +**Implementation:** +```dart +class TVSoundService { + static final TVSoundService _instance = TVSoundService._internal(); + factory TVSoundService() => _instance; + TVSoundService._internal(); + + final AudioPlayer _audioPlayer = AudioPlayer(); + + Future playFocus() => _audioPlayer.play(AssetSource('sounds/focus.mp3')); + Future playSelect() => _audioPlayer.play(AssetSource('sounds/select.mp3')); + Future playError() => _audioPlayer.play(AssetSource('sounds/error.mp3')); + Future playSuccess() => _audioPlayer.play(AssetSource('sounds/success.mp3')); +} +``` + +### Haptic Feedback + +```dart +// For game controllers with vibration +if (_focusNode.hasFocus) { + HapticFeedback.lightImpact(); // Subtle vibration on focus +} +``` + +--- + +## 10. Performance Considerations + +### Animation Performance + +```dart +// Use AnimatedBuilder for efficient rebuilds +AnimatedBuilder( + animation: _animation, + builder: (context, child) { + return Opacity( + opacity: _animation.value, + child: child, + ); + }, + child: ExpensiveWidget(), // Built only once +) +``` + +### Lazy Loading + +```dart +// Use ListView.builder for long lists +ListView.builder( + itemCount: items.length, + itemBuilder: (context, index) { + return TVListItem(item: items[index]); + }, +) +``` + +### Image Optimization + +```dart +// Cache images, use placeholders +CachedNetworkImage( + imageUrl: url, + placeholder: (context, url) => TVLoadingIndicator(), + errorWidget: (context, url, error) => TVErrorIcon(), +) +``` + +--- + +## 11. Accessibility + +### Screen Reader Support + +```dart +// Add semantic labels +Semantics( + label: 'Nama masjid field', + value: settings.masjidName, + hint: 'Masukkan nama masjid', + child: TVTextField(...), +) +``` + +### High Contrast Mode + +```dart +// Respect system high contrast setting +final isHighContrast = MediaQuery.of(context).highContrast; + +if (isHighContrast) { + // Use black and white only + // Remove subtle gradients + // Increase border widths +} +``` + +### Font Scaling + +```dart +// Respect user's font size preferences +final scaleFactor = MediaQuery.of(context).textScaleFactor; + +Text( + 'Hello', + style: TextStyle(fontSize: 28 * scaleFactor), +) +``` + +--- + +## 12. Implementation Roadmap + +### Phase 1: Foundation (Week 1) + +**Focus Management System** +- [ ] Create `TVFocusManager` widget +- [ ] Implement focus scope nodes +- [ ] Add focus listener utilities +- [ ] Create focus ring animation +- [ ] Test D-pad navigation + +**Scaling System** +- [ ] Create `TVScaling` utility +- [ ] Implement responsive layout helpers +- [ ] Define typography scale +- [ ] Test on different screen sizes + +### Phase 2: Core Widgets (Week 2) + +**Form Widgets** +- [ ] `TVTextField` with focus management +- [ ] `TVSelectionField` with dropdown +- [ ] `TVButton` with multiple styles +- [ ] `TVSwitch` for toggles +- [ ] `TVSlider` for numeric values + +**Layout Widgets** +- [ ] `TVDashboardTile` for grid items +- [ ] `TVListItem` for list items +- [ ] `TVCard` for containers +- [ ] `TVSection` for grouping + +### Phase 3: Advanced Features (Week 3) + +**Navigation & Layouts** +- [ ] Grid-based dashboard screen +- [ ] Wizard-style multi-step form +- [ ] Tabbed interface +- [ ] Breadcrumb navigation + +**Input Optimization** +- [ ] `TVSearchSelectField` with search +- [ ] `TVTimePicker` for time selection +- [ ] `TVDatePicker` for date selection +- [ ] `TVColorPicker` for color selection + +### Phase 4: Polish (Week 4) + +**Feedback Systems** +- [ ] Sound effects (focus, select, error, success) +- [ ] Visual feedback animations +- [ ] Loading indicators +- [ ] Error state UIs +- [ ] Success state UIs + +**Performance** +- [ ] Animation optimization +- [ ] Image caching +- [ ] Lazy loading +- [ ] Memory leak testing + +**Accessibility** +- [ ] Screen reader support +- [ ] High contrast mode +- [ ] Font scaling +- [ ] Focus indicators for color blind users + +--- + +## 13. Testing Guidelines + +### Unit Tests + +```dart +testWidgets('TVTextField should show focus indicator', (tester) async { + await tester.pumpWidget( + MaterialApp(home: TVTextField(label: 'Test')), + ); + + // Simulate focus + await tester.sendKeyEvent(LogicalKeyboardKey.tab); + await tester.pump(); + + // Verify focus indicator is visible + expect(find.byType(Border), findsOneWidget); +}); +``` + +### Integration Tests + +```dart +testWidgets('Complete admin flow should work', (tester) async { + // Build admin screen + await tester.pumpWidget(MyApp()); + + // Navigate to location settings + await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); + await tester.sendKeyEvent(LogicalKeyboardKey.enter); + await tester.pumpAndSettle(); + + // Select city + await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); + await tester.sendKeyEvent(LogicalKeyboardKey.enter); + await tester.pumpAndSettle(); + + // Verify selection was saved + expect(find.text('Kota Yogyakarta'), findsOneWidget); +}); +``` + +### Manual Testing Checklist + +- [ ] All widgets are focusable with D-pad +- [ ] Focus indicators are clearly visible +- [ ] Navigation flow is predictable +- [ ] Text input works (though awkward) +- [ ] Sound feedback plays correctly +- [ ] Animations are smooth (60fps) +- [ ] Forms can be completed without mouse/touch +- [ ] Error states are clear +- [ ] Success states provide feedback +- [ ] Back button exits correctly + +--- + +## 14. Common Pitfalls to Avoid + +### ❌ Don't + +1. **Don't use small hit targets**: Everything must be at least 48x48dp +2. **Don't rely on hover states**: TV remotes don't have hover +3. **Don't use subtle gradients**: Hard to see from distance +4. **Don't require extensive typing**: It's frustrating on TV +5. **Don't use small fonts**: Under 24px is unreadable from 2m +6. **Don't hide navigation**: Always show clear focus indicators +7. **Don't use web-only patterns**: TV ≠ web browser +8. **Don't forget sound feedback**: Crucial for accessibility +9. **Don't ignore keyboard shortcuts**: Power users expect them +10. **Don't make users backtrack**: Forward-flow navigation only + +### ✅ Do + +1. **Do make focus obvious**: Large borders, backgrounds, shadows +2. **Do provide clear feedback**: Visual + audio + haptic +3. **Do minimize text input**: Use selectors, wizards, presets +4. **Do use large fonts**: Minimum 24px, recommended 28px+ +5. **Do test on real TV**: Emulator ≠ real experience +6. **Do optimize for D-pad**: Natural navigation flow +7. **Do add sound effects: Helpful feedback +8. **Do use animations: Smooth transitions (200ms) +9. **Do provide shortcuts: Keyboard navigation +10. **Do think in steps: Break complex forms into wizards + +--- + +## 15. Recommended Packages + +```yaml +dependencies: + # TV-specific components + flutter_tencent_eui: ^latest # TV-optimized UI components + + # Enhanced focus management + flutter_focus: ^latest # Better focus control + + # Sound feedback + audioplayers: ^latest # Sound effects + + # Animations + animations: ^latest # Smooth transition helpers + + # Form validation + form_field_validator: ^latest # Reduce user errors + + # Image handling + cached_network_image: ^latest # Image caching +``` + +--- + +## 16. Success Metrics + +### UX Metrics + +- **Time to complete common tasks**: + - Change mosque name: < 30 seconds + - Change location: < 2 minutes + - Sync monthly data: < 1 minute + - Update theme: < 45 seconds + +- **Error rate**: + - Form validation errors: < 5% + - Navigation errors: < 1% + - Focus loss: < 2% + +- **User satisfaction**: + - Task completion rate: > 95% + - User-reported frustration: < 10% + - Willingness to use again: > 90% + +### Technical Metrics + +- **Performance**: + - Focus change: < 16ms (60fps) + - Screen transition: < 200ms + - Form submission: < 500ms + - No frame drops during animations + +- **Reliability**: + - No crashes in 24h operation + - No memory leaks + - No focus loss bugs + - Graceful error handling + +--- + +## 17. Conclusion + +Flutter is **fully capable** of building excellent TV admin dashboards. The key is: + +1. **Embrace TV constraints** rather than fighting them +2. **Think in focus**, not clicks +3. **Minimize text input** through smart UI choices +4. **Provide clear feedback** at every interaction +5. **Test on real hardware** with actual users + +The single-APK architecture is **totally viable** with these TV-optimized widgets. You don't need a separate phone app to deliver a great admin experience. + +### Next Steps + +1. **Start with focus management** - this is the foundation +2. **Build TV-optimized widgets** - large, clear, responsive +3. **Reduce text input** - use selectors and wizards +4. **Add polish** - sounds, animations, feedback +5. **Test thoroughly** - on real TV with real users + +Your admin dashboard will be excellent! + +--- + +## Appendix: Code Examples + +### Complete TV TextField Example + +See `/lib/ui/widgets/tv/tv_text_field.dart` for full implementation. + +### Complete TV Selection Field Example + +See `/lib/ui/widgets/tv/tv_selection_field.dart` for full implementation. + +### Complete TV Dashboard Example + +See `/lib/ui/screens/tv_admin_screen.dart` for full implementation. + +--- + +**Document Version**: 1.0 +**Last Updated**: 2025-01-XX +**Author**: Claude Code +**Status**: Ready for Implementation diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..f9b3034 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1 @@ +include: package:flutter_lints/flutter.yaml diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 0000000..be3943c --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,14 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java +.cxx/ + +# Remember to never publicly share your keystore. +# See https://flutter.dev/to/reference-keystore +key.properties +**/*.keystore +**/*.jks diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts new file mode 100644 index 0000000..de59889 --- /dev/null +++ b/android/app/build.gradle.kts @@ -0,0 +1,44 @@ +plugins { + id("com.android.application") + id("kotlin-android") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "com.jamshalat.jamshalat_masjid_screen" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17.toString() + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "com.jamshalat.jamshalat_masjid_screen" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } +} + +flutter { + source = "../.." +} diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..6c66fa9 --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/kotlin/com/jamshalat/jamshalat_masjid_screen/MainActivity.kt b/android/app/src/main/kotlin/com/jamshalat/jamshalat_masjid_screen/MainActivity.kt new file mode 100644 index 0000000..3f30ffd --- /dev/null +++ b/android/app/src/main/kotlin/com/jamshalat/jamshalat_masjid_screen/MainActivity.kt @@ -0,0 +1,5 @@ +package com.jamshalat.jamshalat_masjid_screen + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/build.gradle.kts b/android/build.gradle.kts new file mode 100644 index 0000000..dbee657 --- /dev/null +++ b/android/build.gradle.kts @@ -0,0 +1,24 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +val newBuildDir: Directory = + rootProject.layout.buildDirectory + .dir("../../build") + .get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000..fbee1d8 --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,2 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..e4ef43f --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts new file mode 100644 index 0000000..b5e1ff7 --- /dev/null +++ b/android/settings.gradle.kts @@ -0,0 +1,28 @@ +pluginManagement { + val flutterSdkPath = + run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + id("com.android.application") version "8.11.1" apply false + id("org.jetbrains.kotlin.android") version "2.2.20" apply false +} + +include(":app") + +rootProject.name = "jamshalat_masjid_screen" diff --git a/assets/fonts/Manrope-Bold.ttf b/assets/fonts/Manrope-Bold.ttf new file mode 100644 index 0000000..62a6183 Binary files /dev/null and b/assets/fonts/Manrope-Bold.ttf differ diff --git a/assets/fonts/Manrope-ExtraBold.ttf b/assets/fonts/Manrope-ExtraBold.ttf new file mode 100644 index 0000000..2fa671c Binary files /dev/null and b/assets/fonts/Manrope-ExtraBold.ttf differ diff --git a/assets/fonts/Manrope-ExtraLight.ttf b/assets/fonts/Manrope-ExtraLight.ttf new file mode 100644 index 0000000..c55745a Binary files /dev/null and b/assets/fonts/Manrope-ExtraLight.ttf differ diff --git a/assets/fonts/Manrope-Light.ttf b/assets/fonts/Manrope-Light.ttf new file mode 100644 index 0000000..8a771c2 Binary files /dev/null and b/assets/fonts/Manrope-Light.ttf differ diff --git a/assets/fonts/Manrope-Medium.ttf b/assets/fonts/Manrope-Medium.ttf new file mode 100644 index 0000000..c6d28de Binary files /dev/null and b/assets/fonts/Manrope-Medium.ttf differ diff --git a/assets/fonts/Manrope-Regular.ttf b/assets/fonts/Manrope-Regular.ttf new file mode 100644 index 0000000..9a108f1 Binary files /dev/null and b/assets/fonts/Manrope-Regular.ttf differ diff --git a/assets/fonts/Manrope-SemiBold.ttf b/assets/fonts/Manrope-SemiBold.ttf new file mode 100644 index 0000000..46a13d6 Binary files /dev/null and b/assets/fonts/Manrope-SemiBold.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/images/icon.png b/assets/images/icon.png new file mode 100644 index 0000000..6ea8cdf Binary files /dev/null and b/assets/images/icon.png differ diff --git a/assets/sounds/3-detik-countdown.mp3 b/assets/sounds/3-detik-countdown.mp3 new file mode 100755 index 0000000..2f845ac Binary files /dev/null and b/assets/sounds/3-detik-countdown.mp3 differ diff --git a/assets/sounds/beep.mp3 b/assets/sounds/beep.mp3 new file mode 100755 index 0000000..c734804 Binary files /dev/null and b/assets/sounds/beep.mp3 differ diff --git a/brief.md b/brief.md new file mode 100644 index 0000000..8572282 --- /dev/null +++ b/brief.md @@ -0,0 +1,130 @@ +# **Project Brief: Smart Digital Prayer Clock (Jam Shalat Masjid) for Android TV Box** + +**Target Platform:** Android TV Box (Android 13/14, 4GB RAM, 64GB ROM) \- Landscape Display (1080p). + +**Framework:** Flutter (Latest Stable). + +**Architecture Pattern:** Clean Architecture or MVVM. + +**State Management Suggestion:** Riverpod atau Provider (Pilih salah satu yang paling stabil). + +**Local Storage:** Hive atau Isar (Untuk caching API bulanan & Settings). + +## **1\. Project Overview** + +Aplikasi ini adalah sistem *Digital Signage* untuk masjid yang akan menyala 24/7 non-stop pada layar TV yang terhubung ke Android Box. Aplikasi ini TIDAK boleh mengandalkan internet real-time. Internet hanya tersedia sebulan sekali (via tethering HP) untuk menarik data jadwal shalat dari API (Bulk 1 bulan) dan melakukan sinkronisasi jam sistem (NTP). Selebihnya, aplikasi berjalan 100% OFFLINE membaca database lokal. + +## **2\. Core Features & App State Machine** + +Aplikasi memiliki siklus layar (*Screen State Machine*) berdasarkan waktu saat ini (DateTime.now()): + +1. **\[STATE: NORMAL / ROTATION\]** \-\> Ini adalah state *default*. Layar akan terus berotasi/berganti secara otomatis berdasarkan skema yang ditentukan di Settings. Terdapat 2 jenis screen dalam rotasi ini: + * **Screen A (Main Screen):** Khusus menampilkan Jam Digital (Current Time), Tanggal (Masehi & Hijriah), Baris Jadwal Shalat (5/6 waktu), dan Countdown detikan menuju shalat berikutnya. Memiliki background statis/dinamis yang telah ditentukan. + * **Screen B (Slideshow Screen):** Khusus menampilkan poster informasi/pengumuman dalam bentuk gambar penuh (JPG/PNG). Transisi antar gambar atau saat berpindah dari/ke Main Screen menggunakan *Fade In-Out* atau *Slide In-Out*. + * *Skema Rotasi:* Dinamis (misal: Main \-\> Slide 1 \-\> Main \-\> Slide 2 \-\> Main \-\> Slide 3, ATAU Main \-\> Slide 1 \-\> Slide 2 \-\> Main \-\> Slide 3 dst). +2. **\[STATE: MENUJU ADZAN\]** \-\> (Misal 10 menit sebelum waktu masuk) Rotasi *Slidehow* dihentikan paksa. Layar terkunci di **Main Screen**, fokus menampilkan hitung mundur menuju Adzan agar jamaah bersiap. +3. **\[STATE: ADZAN\]** \-\> Waktu masuk. Menampilkan tulisan besar "WAKTU ADZAN \[NAMA SHALAT\]", membunyikan alarm/beep pendek. +4. **\[STATE: MENUJU IQOMAH\]** \-\> Menampilkan hitung mundur Iqomah. Khusus hari Jumat, hitung mundur ini diganti tulisan "PERSIAPAN KHUTBAH". +5. **\[STATE: SHALAT/BLACK SCREEN\]** \-\> Setelah waktu Iqomah habis, layar berubah menjadi HITAM PEKAT (Blank) dengan hanya sedikit teks samar agar tidak mengganggu kekhusyukan shalat. +6. **\[STATE: KEMBALI NORMAL\]** \-\> Setelah durasi Shalat habis, layar kembali ke \[STATE: NORMAL / ROTATION\]. + +## **3\. Specific Logic Requirements** + +### **A. Waktu Shalat & Logika Hari Jumat (Crucial\!)** + +* Terdapat 6 jadwal harian: Imsak (opsional), Subuh, Terbit/Syuruq, Dzuhur, Ashar, Maghrib, Isya. +* **Logika Hari Jumat:** JIKA DateTime.now().weekday \== DateTime.friday, MAKA jadwal "Dzuhur" HARUS diubah string/labelnya menjadi "JUMAT". +* Khusus masuk waktu "JUMAT", State \[MENUJU IQOMAH\] di-bypass atau diganti dengan tampilan Nama Khatib dan Imam. + +### **B. Offline-First API Fetching** + +* Buat satu halaman **Admin/Settings Page** (tersembunyi, diakses dengan menekan pojok layar 5 kali atau via icon gembok). +* Di Admin Page, ada tombol "Sync Data Bulanan". +* Saat ditekan, fetch API \-\> Parse JSON \-\> Simpan ke Hive/Isar \-\> Update last\_sync\_date. +* Di UI utama, timer HANYA boleh membaca dari database lokal, bukan nembak ke API. + +### **C. Watchdog & Anti-Freeze (Device Specific)** + +* Wajib menggunakan package wakelock\_plus dan panggil WakelockPlus.enable() di main.dart agar TV Box tidak masuk mode *Sleep*. +* Gunakan Timer.periodic yang efisien. + +## **4\. Controllable Settings (Dynamic Variables)** + +Semua variabel di bawah ini TIDAK BOLEH *hardcoded*. Buatkan model class AppSettings yang disimpan di Local Storage: + +{ + "masjid\_name": "Masjid Al-Ikhlas", + "masjid\_address": "Jl. Kebaikan No. 1, Padang", + "city\_id\_api": "1234", + "show\_imsak": true, + "show\_terbit": true, + "iqomah\_durations": { + "subuh": 15, + "dzuhur": 10, + "ashar": 10, + "maghrib": 7, + "isya": 10 + }, + "blank\_screen\_durations": { + "normal": 15, + "jumat": 45 + }, + "running\_text": \[ + "Mohon luruskan dan rapatkan shaf", + "Kajian rutin setiap Ahad pagi" + \], + "jumat\_officers": { + "khatib": "Ust. Fulan, S.Ag", + "imam": "Ust. Alan, Lc" + }, + "theme\_color": "0xFF006400", + "main\_screen\_background": "/path/to/local/bg.jpg", + "slideshow\_images": \[ + "/path/to/local/slide1.jpg", + "/path/to/local/slide2.png" + \], + "rotation\_settings": { + "main\_screen\_duration\_sec": 15, + "slide\_duration\_sec": 10, + "sequence\_pattern": \["main", "slide\_1", "main", "slide\_2"\] + } +} + +*(Catatan untuk AI: sequence\_pattern bisa diimplementasikan sebagai logic antrean berulang yang membaca array tersebut).* + +## **5\. UI/UX Layout Structure (Landscape 16:9)** + +UI harus dibuat menjadi 2 komponen terpisah yang saling bergantian (menggunakan AnimatedSwitcher untuk transisi *Fade*): + +### **A. Komponen: Main Screen (Jam & Jadwal)** + +* **Background:** Gambar statis yang di-set dari Settings, ditambah overlay gradient gelap agar teks terbaca jelas. +* **Top Area (Header):** Nama Masjid, Alamat, dan Logo. +* **Center Area (Fokus Utama):** + * Jam Digital berukuran sangat besar (mendominasi tengah layar). + * Tanggal Masehi & Tanggal Hijriah tepat di bawah jam. + * Teks dinamis: "Menuju \[Nama Shalat Berikutnya\] \- \[Hitung Mundur HH:MM:SS\]". +* **Bottom Area (Footer):** Row horizontal berisi kotak/kartu jadwal shalat hari ini. Kotak shalat yang akan datang diberi highlight warna berbeda. +* **Bottom Edge:** Marquee (Running Text) berjalan tanpa henti. + +### **B. Komponen: Slideshow Screen (Informasi Poster)** + +* **Center Area:** Gambar poster (JPG/PNG) dirender secara *fullscreen* (atau *fit center* dengan proporsi terjaga). +* **Floating Information Widget (Overlay):** Sebuah *box* melayang di pojok layar (misal pojok kanan atas) dengan *background dark transparent* (opacity 35% \- 50%). Widget ini menampilkan: + * Jam Digital (Current Time) dalam ukuran sedang. + * Nama Shalat Mendatang (Hanya filter 5 shalat fardhu: Subuh, Dzuhur/Jumat, Ashar, Maghrib, Isya. Abaikan waktu Imsak dan Terbit di widget ini). + * Countdown Timer menuju waktu shalat tersebut. +* **Bottom Edge:** Marquee (Running Text) HARUS tetap terlihat meskipun sedang dalam mode Slideshow, agar pengumuman darurat tetap terbaca. + +## **6\. AI Output Instructions** + +Tolong generate kode dengan urutan berikut: + +1. pubspec.yaml dependencies. +2. Model class untuk Settings & Jadwal Shalat. +3. Service layer untuk Local Storage (Hive/SharedPrefs). +4. State Machine Logic (Timer provider/manager untuk handle transisi State, TERMASUK logic *Rotation Scheme* antara Main Screen dan Slideshow). +5. UI Layout: Pisahkan MainScreenWidget dan SlideshowWidget, lalu gabungkan di HomeView dengan AnimatedSwitcher. +6. Admin Screen sederhana untuk mengatur variabel Controllable Settings. + +Pastikan kodenya *null-safe* dan efisien dalam penggunaan memori (hindari memory leaks pada Timer rotasi maupun interval). \ No newline at end of file diff --git a/ios/.gitignore b/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..391a902 --- /dev/null +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + + diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..ec97fc6 --- /dev/null +++ b/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..c4855bf --- /dev/null +++ b/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/ios/Podfile b/ios/Podfile new file mode 100644 index 0000000..620e46e --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,43 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '13.0' + +# 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', '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 Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_ios_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_ios_build_settings(target) + end +end diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..032dd60 --- /dev/null +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,620 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + 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_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + 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_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.jamshalat.jamshalatMasjidScreen; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.jamshalat.jamshalatMasjidScreen.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.jamshalat.jamshalatMasjidScreen.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.jamshalat.jamshalatMasjidScreen.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + 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_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + 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_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + 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_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + 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_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + 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_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.jamshalat.jamshalatMasjidScreen; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.jamshalat.jamshalatMasjidScreen; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..e3773d4 --- /dev/null +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..c30b367 --- /dev/null +++ b/ios/Runner/AppDelegate.swift @@ -0,0 +1,16 @@ +import Flutter +import UIKit + +@main +@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } + + func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { + GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) + } +} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..dc9ada4 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..7353c41 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..6ed2d93 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..4cd7b00 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..fe73094 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..321773c Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..502f463 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..e9f5fea Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..84ac32a Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..8953cba Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..0467bf1 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/ios/Runner/Base.lproj/LaunchScreen.storyboard b/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Base.lproj/Main.storyboard b/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist new file mode 100644 index 0000000..f4f8804 --- /dev/null +++ b/ios/Runner/Info.plist @@ -0,0 +1,70 @@ + + + + + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Jamshalat Masjid Screen + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + jamshalat_masjid_screen + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneConfigurationName + flutter + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + + UIApplicationSupportsIndirectInputEvents + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/ios/Runner/Runner-Bridging-Header.h b/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/ios/Runner/SceneDelegate.swift b/ios/Runner/SceneDelegate.swift new file mode 100644 index 0000000..b9ce8ea --- /dev/null +++ b/ios/Runner/SceneDelegate.swift @@ -0,0 +1,6 @@ +import Flutter +import UIKit + +class SceneDelegate: FlutterSceneDelegate { + +} diff --git a/ios/RunnerTests/RunnerTests.swift b/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..86a7c3b --- /dev/null +++ b/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +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/lib/core/enums.dart b/lib/core/enums.dart new file mode 100644 index 0000000..8f0837e --- /dev/null +++ b/lib/core/enums.dart @@ -0,0 +1,52 @@ +/// App-wide state enums for the screen state machine. +library; + +/// The 6 states the TV display cycles through. +enum ScreenState { + /// Normal rotation between MainScreen and Slideshow. + normal, + + /// Pre-Adzan: Lock to MainScreen, show countdown. + menujuAdzan, + + /// Adzan alert: Full-screen takeover. + adzan, + + /// Iqomah countdown (or Friday Khutbah info). + menujuIqomah, + + /// Black screen during prayer. + shalat, + + /// Transitioning back to normal after prayer. + kembaliNormal, +} + +/// Named prayer slots used across the app. +enum PrayerName { + imsak, + subuh, + terbit, + dhuha, + dzuhur, + ashar, + maghrib, + isya; + + /// Display label — handles Friday override. + String displayLabel({bool isFriday = false}) { + if (this == PrayerName.dzuhur && isFriday) return 'JUMAT'; + return id[0].toUpperCase() + id.substring(1); + } + + /// Safe string identifier to replace .name + String get id => toString().split('.').last; + + /// Whether this is a fardhu prayer (has iqomah). + bool get isFardhu => + this == PrayerName.subuh || + this == PrayerName.dzuhur || + this == PrayerName.ashar || + this == PrayerName.maghrib || + this == PrayerName.isya; +} diff --git a/lib/core/hijri_date.dart b/lib/core/hijri_date.dart new file mode 100644 index 0000000..cb340c3 --- /dev/null +++ b/lib/core/hijri_date.dart @@ -0,0 +1,74 @@ +class HijriDate { + final int year; + final int month; + final int day; + + const HijriDate({ + required this.year, + required this.month, + required this.day, + }); +} + +class HijriDateFormatter { + HijriDateFormatter._(); + + static const List _monthNames = [ + 'Muharram', + 'Safar', + 'Rabiul Awal', + 'Rabiul Akhir', + 'Jumadil Awal', + 'Jumadil Akhir', + 'Rajab', + 'Syaban', + 'Ramadan', + 'Syawal', + 'Zulkaidah', + 'Zulhijah', + ]; + + static String format(DateTime date) { + final hijri = fromGregorian(date); + final monthName = _monthNames[hijri.month - 1]; + return '${hijri.day} $monthName ${hijri.year} H'; + } + + // Tabular Islamic calendar conversion. This is deterministic and avoids + // adding a new dependency just to replace the hardcoded placeholder dates. + static HijriDate fromGregorian(DateTime date) { + final jd = _gregorianToJulianDay(date.year, date.month, date.day); + + var l = jd - 1948440 + 10632; + final n = ((l - 1) ~/ 10631); + l = l - 10631 * n + 354; + + final j = (((10985 - l) ~/ 5316) * ((50 * l) ~/ 17719)) + + ((l ~/ 5670) * ((43 * l) ~/ 15238)); + + l = l - + (((30 - j) ~/ 15) * ((17719 * j) ~/ 50)) - + ((j ~/ 16) * ((15238 * j) ~/ 43)) + + 29; + + final month = (24 * l) ~/ 709; + final day = l - ((709 * month) ~/ 24); + final year = 30 * n + j - 30; + + return HijriDate(year: year, month: month, day: day); + } + + static int _gregorianToJulianDay(int year, int month, int day) { + final a = (14 - month) ~/ 12; + final y = year + 4800 - a; + final m = month + 12 * a - 3; + + return day + + ((153 * m + 2) ~/ 5) + + 365 * y + + (y ~/ 4) - + (y ~/ 100) + + (y ~/ 400) - + 32045; + } +} diff --git a/lib/core/sacred_tokens.dart b/lib/core/sacred_tokens.dart new file mode 100644 index 0000000..2c1e8b2 --- /dev/null +++ b/lib/core/sacred_tokens.dart @@ -0,0 +1,174 @@ +/// Design tokens extracted from "The Sacred Horizon" Stitch design system. +/// These are the canonical color, typography, spacing, and styling constants. +library; + +import 'package:flutter/material.dart'; + +// ────────────────────────────────────────────── +// COLOR TOKENS — "Masjid Twilight" palette +// ────────────────────────────────────────────── + +class SacredColors { + SacredColors._(); + + // Core + static const Color primary = Color(0xFF88D982); // Living Green + static const Color primaryContainer = Color(0xFF004F11); + static const Color onPrimaryContainer = Color(0xFF72C36E); + + static const Color secondary = Color(0xFFE9C349); // Sacred Gold + static const Color secondaryContainer = Color(0xFFAF8D11); + + static const Color tertiary = Color(0xFFE9C400); + static const Color tertiaryContainer = Color(0xFFC8A900); + + // Background & Surface hierarchy + static const Color background = Color(0xFF131313); // The Void + static const Color surface = Color(0xFF131313); + static const Color surfaceDim = Color(0xFF131313); + static const Color surfaceContainerLowest = Color(0xFF0E0E0E); + static const Color surfaceContainerLow = Color(0xFF1C1B1B); + static const Color surfaceContainer = Color(0xFF201F1F); + static const Color surfaceContainerHigh = Color(0xFF2A2A2A); + static const Color surfaceContainerHighest = Color(0xFF353534); + static const Color surfaceBright = Color(0xFF393939); + static const Color surfaceVariant = Color(0xFF353534); + + // On‑ tokens + static const Color onSurface = Color(0xFFE5E2E1); + static const Color onSurfaceVariant = Color(0xFFBFC9C4); + static const Color onBackground = Color(0xFFE5E2E1); + static const Color onPrimary = Color(0xFF003909); + static const Color onSecondary = Color(0xFF3C2F00); + + // Outline + static const Color outline = Color(0xFF89938F); + static const Color outlineVariant = Color(0xFF3F4945); + + // Error + static const Color error = Color(0xFFFFB4AB); + static const Color errorContainer = Color(0xFF93000A); + + // Inverse + static const Color inverseSurface = Color(0xFFE5E2E1); + static const Color inversePrimary = Color(0xFF1B6D24); + + // Special + static const Color blackScreen = Color(0xFF000000); + + // Convenience: transparent glass for overlays + static Color get glass70 => surfaceVariant.withValues(alpha: 0.70); + static Color get glass60 => surfaceVariant.withValues(alpha: 0.60); + static Color get glass35 => surfaceVariant.withValues(alpha: 0.35); +} + +// ────────────────────────────────────────────── +// TYPOGRAPHY +// ────────────────────────────────────────────── + +class SacredTypography { + SacredTypography._(); + + static const String headlineFamily = 'Plus Jakarta Sans'; + static const String bodyFamily = 'Manrope'; + + // Display — The Clock (heartbeat of the system) + static TextStyle clockDisplay(double fontSize) => TextStyle( + fontFamily: headlineFamily, + fontSize: fontSize, + fontWeight: FontWeight.w800, + letterSpacing: -0.02 * fontSize, + color: SacredColors.onSurface, + height: 1.0, + ); + + // Headline — Prayer Names + static TextStyle headline(double fontSize) => TextStyle( + fontFamily: headlineFamily, + fontSize: fontSize, + fontWeight: FontWeight.w700, + letterSpacing: -0.01 * fontSize, + color: SacredColors.onSurface, + ); + + // Title — Timings + static TextStyle title(double fontSize) => TextStyle( + fontFamily: bodyFamily, + fontSize: fontSize, + fontWeight: FontWeight.w600, + color: SacredColors.onSurface, + ); + + // Body + static TextStyle body(double fontSize) => TextStyle( + fontFamily: bodyFamily, + fontSize: fontSize, + fontWeight: FontWeight.w400, + color: SacredColors.onSurface, + ); + + // Label — metadata + static TextStyle label(double fontSize) => TextStyle( + fontFamily: bodyFamily, + fontSize: fontSize, + fontWeight: FontWeight.w500, + letterSpacing: 0.05 * fontSize, + color: SacredColors.onSurfaceVariant, + ); + + // Label caps (for TRACKING-WIDEST uppercase captions) + static TextStyle labelCaps(double fontSize) => TextStyle( + fontFamily: bodyFamily, + fontSize: fontSize, + fontWeight: FontWeight.w700, + letterSpacing: 0.2 * fontSize, + color: SacredColors.onSurfaceVariant, + ); +} + +// ────────────────────────────────────────────── +// SPACING — 8px grid +// ────────────────────────────────────────────── + +class SacredSpacing { + SacredSpacing._(); + + static const double xs = 8; + static const double sm = 16; + static const double md = 24; + static const double lg = 32; + static const double xl = 48; + static const double xxl = 64; + static const double screenEdge = 64; // 4rem minimum from edges +} + +// ────────────────────────────────────────────── +// RADII +// ────────────────────────────────────────────── + +class SacredRadii { + SacredRadii._(); + + static const double sm = 4; + static const double md = 8; + static const double lg = 12; + static const double xl = 16; + static const double full = 9999; +} + +// ────────────────────────────────────────────── +// TV SCALING +// ────────────────────────────────────────────── + +class TVScale { + TVScale._(); + + /// Scale factor relative to 1920px baseline + static double of(BuildContext context) { + return MediaQuery.of(context).size.width / 1920; + } + + static double fontSize(BuildContext context, double base) { + return base * of(context); + } +} diff --git a/lib/data/local/models.dart b/lib/data/local/models.dart new file mode 100644 index 0000000..db86daa --- /dev/null +++ b/lib/data/local/models.dart @@ -0,0 +1,420 @@ +import 'package:hive_flutter/hive_flutter.dart'; + +/// Hive type adapter IDs and box names. +class HiveBoxes { + HiveBoxes._(); + static const String settings = 'app_settings'; + static const String prayerSchedule = 'prayer_schedule'; +} + +/// AppSettings stored in Hive. +@HiveType(typeId: 0) +class AppSettings extends HiveObject { + @HiveField(0) + String masjidName; + + @HiveField(1) + String masjidAddress; + + @HiveField(2) + String cityIdApi; // myQuran city hash ID + + @HiveField(3) + String cityDisplayName; + + @HiveField(4) + bool showImsak; + + @HiveField(5) + bool showTerbit; + + // Iqomah durations in minutes + @HiveField(6) + int iqomahSubuh; + + @HiveField(7) + int iqomahDzuhur; + + @HiveField(8) + int iqomahAshar; + + @HiveField(9) + int iqomahMaghrib; + + @HiveField(10) + int iqomahIsya; + + // Pre-Adzan lead time (minutes before adzan to lock main screen) + @HiveField(11) + int preAdzanLead; + + // Blank screen durations + @HiveField(12) + int blankScreenNormal; // minutes + + @HiveField(13) + int blankScreenJumat; // minutes + + // Running text items + @HiveField(14) + List runningTexts; + + // Friday officers + @HiveField(15) + String khatibName; + + @HiveField(16) + String imamName; + + // Rotation settings + @HiveField(17) + int mainScreenDurationSec; + + @HiveField(18) + int slideDurationSec; + + // Last sync timestamp + @HiveField(19) + String? lastSyncDate; + + // Slideshow image paths (local) + @HiveField(20) + List slideshowImages; + + // Text scaling (0=Small, 1=Medium, 2=Large) + @HiveField(21) + int textScaleIndex; + + // Unsplash Background configs + @HiveField(22) + bool useUnsplashBackground; + + @HiveField(23) + String unsplashKeyword; + + @HiveField(24) + int unsplashRotationHours; + + // Branded background image (local file path set by admin) + @HiveField(25) + String? brandedBgImage; + + // Per-item duration for running texts (seconds each) + @HiveField(26) + List runningTextDurations; + + // Running text animation type: 'marquee' or 'fade' + @HiveField(27) + String marqueeAnimType; + + // Granular text group scales (independent of textScaleIndex) + // Group: Prayer card label (e.g. "SUBUH", "DZUHUR") + @HiveField(28) + double scaleCardLabel; + + // Group: Prayer card body (time + iqomah text) + @HiveField(29) + double scaleCardBody; + + // Group: Running text ticker at bottom + @HiveField(30) + double scaleRunningText; + + AppSettings({ + this.masjidName = 'Masjid Al-Ikhlas', + this.masjidAddress = 'Jl. Kebaikan No. 1', + this.cityIdApi = '1218', // Default: Yogyakarta + this.cityDisplayName = 'Kota Yogyakarta', + this.showImsak = true, + this.showTerbit = true, + this.iqomahSubuh = 15, + this.iqomahDzuhur = 10, + this.iqomahAshar = 10, + this.iqomahMaghrib = 7, + this.iqomahIsya = 10, + this.preAdzanLead = 10, + this.blankScreenNormal = 15, + this.blankScreenJumat = 45, + this.runningTexts = const [ + 'Mohon luruskan dan rapatkan shaf', + 'Kajian rutin setiap Ahad pagi', + ], + this.khatibName = 'Ust. Fulan, S.Ag', + this.imamName = 'Ust. Alan, Lc', + this.mainScreenDurationSec = 15, + this.slideDurationSec = 10, + this.lastSyncDate, + this.slideshowImages = const [], + this.textScaleIndex = 1, + this.useUnsplashBackground = false, + this.unsplashKeyword = 'mosque', + this.unsplashRotationHours = 6, + this.brandedBgImage, + this.runningTextDurations = const [], + this.marqueeAnimType = 'marquee', + this.scaleCardLabel = 1.0, + this.scaleCardBody = 1.0, + this.scaleRunningText = 1.0, + }); + + AppSettings copyWith({ + String? masjidName, + String? masjidAddress, + String? cityIdApi, + String? cityDisplayName, + bool? showImsak, + bool? showTerbit, + int? iqomahSubuh, + int? iqomahDzuhur, + int? iqomahAshar, + int? iqomahMaghrib, + int? iqomahIsya, + int? preAdzanLead, + int? blankScreenNormal, + int? blankScreenJumat, + List? runningTexts, + String? khatibName, + String? imamName, + int? mainScreenDurationSec, + int? slideDurationSec, + String? lastSyncDate, + List? slideshowImages, + int? textScaleIndex, + bool? useUnsplashBackground, + String? unsplashKeyword, + int? unsplashRotationHours, + String? brandedBgImage, + List? runningTextDurations, + String? marqueeAnimType, + double? scaleCardLabel, + double? scaleCardBody, + double? scaleRunningText, + }) { + return AppSettings( + masjidName: masjidName ?? this.masjidName, + masjidAddress: masjidAddress ?? this.masjidAddress, + cityIdApi: cityIdApi ?? this.cityIdApi, + cityDisplayName: cityDisplayName ?? this.cityDisplayName, + showImsak: showImsak ?? this.showImsak, + showTerbit: showTerbit ?? this.showTerbit, + iqomahSubuh: iqomahSubuh ?? this.iqomahSubuh, + iqomahDzuhur: iqomahDzuhur ?? this.iqomahDzuhur, + iqomahAshar: iqomahAshar ?? this.iqomahAshar, + iqomahMaghrib: iqomahMaghrib ?? this.iqomahMaghrib, + iqomahIsya: iqomahIsya ?? this.iqomahIsya, + preAdzanLead: preAdzanLead ?? this.preAdzanLead, + blankScreenNormal: blankScreenNormal ?? this.blankScreenNormal, + blankScreenJumat: blankScreenJumat ?? this.blankScreenJumat, + runningTexts: runningTexts ?? this.runningTexts, + khatibName: khatibName ?? this.khatibName, + imamName: imamName ?? this.imamName, + mainScreenDurationSec: mainScreenDurationSec ?? this.mainScreenDurationSec, + slideDurationSec: slideDurationSec ?? this.slideDurationSec, + lastSyncDate: lastSyncDate ?? this.lastSyncDate, + slideshowImages: slideshowImages ?? this.slideshowImages, + textScaleIndex: textScaleIndex ?? this.textScaleIndex, + useUnsplashBackground: useUnsplashBackground ?? this.useUnsplashBackground, + unsplashKeyword: unsplashKeyword ?? this.unsplashKeyword, + unsplashRotationHours: unsplashRotationHours ?? this.unsplashRotationHours, + brandedBgImage: brandedBgImage ?? this.brandedBgImage, + runningTextDurations: runningTextDurations ?? this.runningTextDurations, + marqueeAnimType: marqueeAnimType ?? this.marqueeAnimType, + scaleCardLabel: scaleCardLabel ?? this.scaleCardLabel, + scaleCardBody: scaleCardBody ?? this.scaleCardBody, + scaleRunningText: scaleRunningText ?? this.scaleRunningText, + ); + } +} + +/// Adapter for AppSettings — hand-written to avoid code generation. +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++) { + fields[reader.readByte()] = reader.read(); + } + return AppSettings( + masjidName: fields[0] as String? ?? 'Masjid Al-Ikhlas', + masjidAddress: fields[1] as String? ?? 'Jl. Kebaikan No. 1', + cityIdApi: fields[2] as String? ?? '1218', + cityDisplayName: fields[3] as String? ?? 'Kota Yogyakarta', + showImsak: fields[4] as bool? ?? true, + showTerbit: fields[5] as bool? ?? true, + iqomahSubuh: fields[6] as int? ?? 15, + iqomahDzuhur: fields[7] as int? ?? 10, + iqomahAshar: fields[8] as int? ?? 10, + iqomahMaghrib: fields[9] as int? ?? 7, + iqomahIsya: fields[10] as int? ?? 10, + preAdzanLead: fields[11] as int? ?? 10, + blankScreenNormal: fields[12] as int? ?? 15, + blankScreenJumat: fields[13] as int? ?? 45, + runningTexts: (fields[14] as List?)?.cast() ?? const [], + khatibName: fields[15] as String? ?? '', + imamName: fields[16] as String? ?? '', + mainScreenDurationSec: fields[17] as int? ?? 15, + slideDurationSec: fields[18] as int? ?? 10, + lastSyncDate: fields[19] as String?, + slideshowImages: (fields[20] as List?)?.cast() ?? const [], + textScaleIndex: fields[21] as int? ?? 1, + useUnsplashBackground: fields[22] as bool? ?? false, + unsplashKeyword: fields[23] as String? ?? 'mosque', + unsplashRotationHours: fields[24] as int? ?? 6, + brandedBgImage: fields[25] as String?, + runningTextDurations: (fields[26] as List?)?.cast() ?? const [], + marqueeAnimType: fields[27] as String? ?? 'marquee', + scaleCardLabel: (fields[28] as num?)?.toDouble() ?? 1.0, + scaleCardBody: (fields[29] as num?)?.toDouble() ?? 1.0, + scaleRunningText: (fields[30] as num?)?.toDouble() ?? 1.0, + ); + } + + @override + void write(BinaryWriter writer, AppSettings obj) { + writer + ..writeByte(31) + ..writeByte(0)..write(obj.masjidName) + ..writeByte(1)..write(obj.masjidAddress) + ..writeByte(2)..write(obj.cityIdApi) + ..writeByte(3)..write(obj.cityDisplayName) + ..writeByte(4)..write(obj.showImsak) + ..writeByte(5)..write(obj.showTerbit) + ..writeByte(6)..write(obj.iqomahSubuh) + ..writeByte(7)..write(obj.iqomahDzuhur) + ..writeByte(8)..write(obj.iqomahAshar) + ..writeByte(9)..write(obj.iqomahMaghrib) + ..writeByte(10)..write(obj.iqomahIsya) + ..writeByte(11)..write(obj.preAdzanLead) + ..writeByte(12)..write(obj.blankScreenNormal) + ..writeByte(13)..write(obj.blankScreenJumat) + ..writeByte(14)..write(obj.runningTexts) + ..writeByte(15)..write(obj.khatibName) + ..writeByte(16)..write(obj.imamName) + ..writeByte(17)..write(obj.mainScreenDurationSec) + ..writeByte(18)..write(obj.slideDurationSec) + ..writeByte(19)..write(obj.lastSyncDate) + ..writeByte(20)..write(obj.slideshowImages) + ..writeByte(21)..write(obj.textScaleIndex) + ..writeByte(22)..write(obj.useUnsplashBackground) + ..writeByte(23)..write(obj.unsplashKeyword) + ..writeByte(24)..write(obj.unsplashRotationHours) + ..writeByte(25)..write(obj.brandedBgImage) + ..writeByte(26)..write(obj.runningTextDurations) + ..writeByte(27)..write(obj.marqueeAnimType) + ..writeByte(28)..write(obj.scaleCardLabel) + ..writeByte(29)..write(obj.scaleCardBody) + ..writeByte(30)..write(obj.scaleRunningText); + } +} + +/// Daily prayer schedule row cached from the MyQuran API. +@HiveType(typeId: 1) +class DailyPrayerSchedule extends HiveObject { + @HiveField(0) + String date; // yyyy-MM-dd + + @HiveField(1) + String imsak; + + @HiveField(2) + String subuh; + + @HiveField(3) + String terbit; + + @HiveField(4) + String dhuha; + + @HiveField(5) + String dzuhur; + + @HiveField(6) + String ashar; + + @HiveField(7) + String maghrib; + + @HiveField(8) + String isya; + + DailyPrayerSchedule({ + required this.date, + required this.imsak, + required this.subuh, + required this.terbit, + required this.dhuha, + required this.dzuhur, + required this.ashar, + required this.maghrib, + required this.isya, + }); + + /// Parse time string "HH:mm" to a DateTime on the given date. + DateTime timeToDateTime(String time, DateTime refDate) { + final parts = time.split(':'); + return DateTime( + refDate.year, + refDate.month, + refDate.day, + int.parse(parts[0]), + int.parse(parts[1]), + ); + } + + /// Get all prayer times as DateTime map for a given reference date. + Map toDateTimeMap(DateTime refDate) => { + 'imsak': timeToDateTime(imsak, refDate), + 'subuh': timeToDateTime(subuh, refDate), + 'terbit': timeToDateTime(terbit, refDate), + 'dhuha': timeToDateTime(dhuha, refDate), + 'dzuhur': timeToDateTime(dzuhur, refDate), + 'ashar': timeToDateTime(ashar, refDate), + 'maghrib': timeToDateTime(maghrib, refDate), + 'isya': timeToDateTime(isya, refDate), + }; +} + +/// Adapter for DailyPrayerSchedule. +class DailyPrayerScheduleAdapter extends TypeAdapter { + @override + final int typeId = 1; + + @override + DailyPrayerSchedule read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = {}; + for (int i = 0; i < numOfFields; i++) { + fields[reader.readByte()] = reader.read(); + } + return DailyPrayerSchedule( + date: fields[0] as String? ?? '', + imsak: fields[1] as String? ?? '00:00', + subuh: fields[2] as String? ?? '00:00', + terbit: fields[3] as String? ?? '00:00', + dhuha: fields[4] as String? ?? '00:00', + dzuhur: fields[5] as String? ?? '00:00', + ashar: fields[6] as String? ?? '00:00', + maghrib: fields[7] as String? ?? '00:00', + isya: fields[8] as String? ?? '00:00', + ); + } + + @override + void write(BinaryWriter writer, DailyPrayerSchedule obj) { + writer + ..writeByte(9) + ..writeByte(0)..write(obj.date) + ..writeByte(1)..write(obj.imsak) + ..writeByte(2)..write(obj.subuh) + ..writeByte(3)..write(obj.terbit) + ..writeByte(4)..write(obj.dhuha) + ..writeByte(5)..write(obj.dzuhur) + ..writeByte(6)..write(obj.ashar) + ..writeByte(7)..write(obj.maghrib) + ..writeByte(8)..write(obj.isya); + } +} diff --git a/lib/data/services/myquran_service.dart b/lib/data/services/myquran_service.dart new file mode 100644 index 0000000..d1c2867 --- /dev/null +++ b/lib/data/services/myquran_service.dart @@ -0,0 +1,111 @@ +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. +/// +/// Ported directly from the jamshalat-diary project. +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 — device is offline + } + return []; + } + + /// Get prayer times for today. + /// [cityId] = myQuran city ID (hash string) + /// Returns map: {tanggal, imsak, subuh, terbit, dhuha, dzuhur, ashar, maghrib, isya} + Future?> getDailySchedule(String cityId) async { + 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) { + final jadwalMap = data['data']['jadwal'] as Map; + if (jadwalMap.isNotEmpty) { + final firstKey = jadwalMap.keys.first; + final jadwal = jadwalMap[firstKey]; + if (jadwal != null) { + final result = Map.from(jadwal.map((k, v) => MapEntry(k.toString(), v.toString()))); + result['date'] = firstKey; + return result; + } + } + } + } + } catch (e) { + // silent fallback + } + return null; + } + + + /// Get monthly prayer schedule (bulk fetch for offline caching). + /// [month] = 'yyyy-MM' format (e.g., '2024-03') + /// 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/sound_service.dart b/lib/data/services/sound_service.dart new file mode 100644 index 0000000..286dae4 --- /dev/null +++ b/lib/data/services/sound_service.dart @@ -0,0 +1,46 @@ +import 'package:audioplayers/audioplayers.dart'; +import 'package:flutter/foundation.dart'; + +class SoundService { + SoundService._(); + static final instance = SoundService._(); + + late AudioPlayer _player; + bool _initialized = false; + + void init() { + if (_initialized) return; + _player = AudioPlayer(); + // Pre-cache sounds by setting sources but not playing immediately if desired, + // though AudioCache is handled implicitly in newer audioplayers. + _player.setReleaseMode(ReleaseMode.stop); + _initialized = true; + } + + Future playAdzanBeep() async { + try { + if (!_initialized) init(); + // Plays a single beep exactly when Adzan time hits + await _player.play(AssetSource('sounds/beep.mp3')); + } catch (e) { + debugPrint('[SoundService] Error playing adzan beep: $e'); + } + } + + Future playIqomahCountdown() async { + try { + if (!_initialized) init(); + // Plays the 3-beep countdown for the last 3 seconds of Iqamah + await _player.play(AssetSource('sounds/3-detik-countdown.mp3')); + } catch (e) { + debugPrint('[SoundService] Error playing iqomah countdown: $e'); + } + } + + void dispose() { + if (_initialized) { + _player.dispose(); + _initialized = false; + } + } +} diff --git a/lib/data/services/sync_service.dart b/lib/data/services/sync_service.dart new file mode 100644 index 0000000..1c4931e --- /dev/null +++ b/lib/data/services/sync_service.dart @@ -0,0 +1,93 @@ +import 'package:hive_flutter/hive_flutter.dart'; +import 'package:intl/intl.dart'; + +import '../local/models.dart'; +import 'myquran_service.dart'; + +/// Service to sync monthly prayer data from MyQuran API → Hive. +class SyncService { + SyncService._(); + static final SyncService instance = SyncService._(); + + /// Sync current month + next month prayer data for the configured city. + /// Returns true on success. + Future syncMonthlyData() async { + final settingsBox = Hive.box(HiveBoxes.settings); + final settings = settingsBox.get('default'); + if (settings == null) return false; + + final cityId = settings.cityIdApi; + final now = DateTime.now(); + final currentMonth = DateFormat('yyyy-MM').format(now); + + // Also fetch next month for continuity + final nextMonthDate = DateTime(now.year, now.month + 1, 1); + final nextMonth = DateFormat('yyyy-MM').format(nextMonthDate); + + final api = MyQuranSholatService.instance; + final scheduleBox = Hive.box(HiveBoxes.prayerSchedule); + + var success = false; + + // Fetch current month + final currentData = await api.getMonthlySchedule(cityId, currentMonth); + if (currentData.isNotEmpty) { + for (final entry in currentData.entries) { + final jadwal = entry.value; + scheduleBox.put( + entry.key, + DailyPrayerSchedule( + date: entry.key, + imsak: jadwal['imsak'] ?? '00:00', + subuh: jadwal['subuh'] ?? '00:00', + terbit: jadwal['terbit'] ?? '00:00', + dhuha: jadwal['dhuha'] ?? '00:00', + dzuhur: jadwal['dzuhur'] ?? '00:00', + ashar: jadwal['ashar'] ?? '00:00', + maghrib: jadwal['maghrib'] ?? '00:00', + isya: jadwal['isya'] ?? '00:00', + ), + ); + } + success = true; + } + + // Fetch next month + final nextData = await api.getMonthlySchedule(cityId, nextMonth); + if (nextData.isNotEmpty) { + for (final entry in nextData.entries) { + final jadwal = entry.value; + scheduleBox.put( + entry.key, + DailyPrayerSchedule( + date: entry.key, + imsak: jadwal['imsak'] ?? '00:00', + subuh: jadwal['subuh'] ?? '00:00', + terbit: jadwal['terbit'] ?? '00:00', + dhuha: jadwal['dhuha'] ?? '00:00', + dzuhur: jadwal['dzuhur'] ?? '00:00', + ashar: jadwal['ashar'] ?? '00:00', + maghrib: jadwal['maghrib'] ?? '00:00', + isya: jadwal['isya'] ?? '00:00', + ), + ); + } + } + + if (success) { + settings.lastSyncDate = DateFormat('yyyy-MM-dd HH:mm').format(now); + await settings.save(); + } + + return success; + } + + /// Get today's prayer schedule from local Hive cache. + DailyPrayerSchedule? getTodaySchedule([DateTime? targetDate]) { + final scheduleBox = + Hive.box(HiveBoxes.prayerSchedule); + final dateToFetch = targetDate ?? DateTime.now(); + final dateStr = DateFormat('yyyy-MM-dd').format(dateToFetch); + return scheduleBox.get(dateStr); + } +} diff --git a/lib/data/services/unsplash_service.dart b/lib/data/services/unsplash_service.dart new file mode 100644 index 0000000..6ab42bb --- /dev/null +++ b/lib/data/services/unsplash_service.dart @@ -0,0 +1,48 @@ +import 'dart:convert'; +import 'package:http/http.dart' as http; + +/// Service for fetching background portraits from the Unsplash API. +class UnsplashService { + static const String _clientId = 'BkgEMpfG_ReNpVwJcbgNx30IZXhoFoWwKgwbrPU0hq4'; + static const String _baseUrl = 'https://api.unsplash.com'; + + static final UnsplashService instance = UnsplashService._(); + UnsplashService._(); + + /// Fetches a list of highly compressed landscape URLs based on the given keyword. + Future> fetchLandscapeBackgrounds(String keyword) async { + // Trim keyword and default to 'mosque' if empty + final query = keyword.trim().isEmpty ? 'mosque' : keyword.trim(); + + // Specifically requesting 'regular' size to fit 1080p elegantly while minimizing RAM overhead. + final url = Uri.parse('$_baseUrl/search/photos?query=$query&orientation=landscape&per_page=20'); + + try { + final response = await http.get( + url, + headers: { + 'Authorization': 'Client-ID $_clientId', + 'Accept-Version': 'v1', + }, + ); + + if (response.statusCode == 200) { + final data = json.decode(response.body); + final results = data['results'] as List? ?? []; + + final urls = []; + for (final item in results) { + final urlsMap = item['urls'] as Map?; + if (urlsMap != null && urlsMap.containsKey('regular')) { + urls.add(urlsMap['regular'].toString()); + } + } + return urls; + } + } catch (e) { + // Offline or error — fail silently. + } + + return []; + } +} diff --git a/lib/features/admin/admin_screen.dart b/lib/features/admin/admin_screen.dart new file mode 100644 index 0000000..4cd162e --- /dev/null +++ b/lib/features/admin/admin_screen.dart @@ -0,0 +1,1697 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:hugeicons/hugeicons.dart'; + +import '../../core/sacred_tokens.dart'; +import '../../providers.dart'; +import '../../data/services/sync_service.dart'; +import '../../data/services/myquran_service.dart'; +import 'package:file_picker/file_picker.dart'; +import 'dart:io'; + +class AdminScreen extends ConsumerStatefulWidget { + const AdminScreen({super.key}); + + @override + ConsumerState createState() => _AdminScreenState(); +} + +class _AdminScreenState extends ConsumerState { + final _masjidNameCtrl = TextEditingController(); + final _masjidAddressCtrl = TextEditingController(); + final _cityCtrl = TextEditingController(); // Displays DisplayName or CityID + + final _mainDurCtrl = TextEditingController(); + final _slideDurCtrl = TextEditingController(); + + int _selectedTab = 0; + bool _isSyncing = false; + int _textScaleIndex = 1; + List _slideshowImages = []; + bool _useUnsplash = false; + final _unsplashKeywordCtrl = TextEditingController(); + final _unsplashRotationCtrl = TextEditingController(); + + // Branded background + String? _brandedBgImage; + + // Running text repeater + String _marqueeAnimType = 'marquee'; + List _runningTexts = []; + List _runningTextDurations = []; + + // Granular text group scales + double _scaleCardLabel = 1.0; + double _scaleCardBody = 1.0; + double _scaleRunningText = 1.0; + + // Jumat fields + final _khatibCtrl = TextEditingController(); + final _imamCtrl = TextEditingController(); + + // Iqomah Jeda fields + final _iqomahSubuhCtrl = TextEditingController(); + final _iqomahDzuhurCtrl = TextEditingController(); + final _iqomahAsharCtrl = TextEditingController(); + final _iqomahMaghribCtrl = TextEditingController(); + final _iqomahIsyaCtrl = TextEditingController(); + + @override + void initState() { + super.initState(); + final settings = ref.read(settingsProvider); + _masjidNameCtrl.text = settings.masjidName; + _masjidAddressCtrl.text = settings.masjidAddress; + _cityCtrl.text = '${settings.cityDisplayName} (${settings.cityIdApi})'; + _mainDurCtrl.text = settings.mainScreenDurationSec.toString(); + _slideDurCtrl.text = settings.slideDurationSec.toString(); + _textScaleIndex = settings.textScaleIndex; + _slideshowImages = List.from(settings.slideshowImages); + _useUnsplash = settings.useUnsplashBackground; + _unsplashKeywordCtrl.text = settings.unsplashKeyword; + _unsplashRotationCtrl.text = settings.unsplashRotationHours.toString(); + _brandedBgImage = settings.brandedBgImage; + _marqueeAnimType = settings.marqueeAnimType; + _runningTexts = List.from(settings.runningTexts); + _runningTextDurations = List.from( + settings.runningTextDurations.isNotEmpty + ? settings.runningTextDurations + : List.filled(settings.runningTexts.length, 12), + ); + // Ensure durations list length matches texts + while (_runningTextDurations.length < _runningTexts.length) { + _runningTextDurations.add(12); + } + _scaleCardLabel = settings.scaleCardLabel; + _scaleCardBody = settings.scaleCardBody; + _scaleRunningText = settings.scaleRunningText; + _khatibCtrl.text = settings.khatibName; + _imamCtrl.text = settings.imamName; + + _iqomahSubuhCtrl.text = settings.iqomahSubuh.toString(); + _iqomahDzuhurCtrl.text = settings.iqomahDzuhur.toString(); + _iqomahAsharCtrl.text = settings.iqomahAshar.toString(); + _iqomahMaghribCtrl.text = settings.iqomahMaghrib.toString(); + _iqomahIsyaCtrl.text = settings.iqomahIsya.toString(); + + // Update preview live as admin types + _khatibCtrl.addListener(() => setState(() {})); + _imamCtrl.addListener(() => setState(() {})); + } + + @override + void dispose() { + _masjidNameCtrl.dispose(); + _masjidAddressCtrl.dispose(); + _cityCtrl.dispose(); + _mainDurCtrl.dispose(); + _slideDurCtrl.dispose(); + _unsplashKeywordCtrl.dispose(); + _unsplashRotationCtrl.dispose(); + _khatibCtrl.dispose(); + _imamCtrl.dispose(); + _iqomahSubuhCtrl.dispose(); + _iqomahDzuhurCtrl.dispose(); + _iqomahAsharCtrl.dispose(); + _iqomahMaghribCtrl.dispose(); + _iqomahIsyaCtrl.dispose(); + super.dispose(); + } + + Future _saveIdentity() async { + await ref.read(settingsProvider.notifier).updateSettings((s) { + s.masjidName = _masjidNameCtrl.text.trim(); + s.masjidAddress = _masjidAddressCtrl.text.trim(); + // cityId is saved instantly when selected from dialog + return s; + }); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Pengaturan berhasil disimpan', + style: GoogleFonts.manrope()), + backgroundColor: SacredColors.primaryContainer, + ), + ); + } + } + + Future _saveTampilan() async { + await ref.read(settingsProvider.notifier).updateSettings((s) { + s.textScaleIndex = _textScaleIndex; + s.slideshowImages = List.from(_slideshowImages); + s.mainScreenDurationSec = int.tryParse(_mainDurCtrl.text.trim()) ?? 15; + s.slideDurationSec = int.tryParse(_slideDurCtrl.text.trim()) ?? 10; + s.useUnsplashBackground = _useUnsplash; + s.unsplashKeyword = _unsplashKeywordCtrl.text.trim().isEmpty ? 'mosque' : _unsplashKeywordCtrl.text.trim(); + s.unsplashRotationHours = int.tryParse(_unsplashRotationCtrl.text.trim()) ?? 6; + s.brandedBgImage = _brandedBgImage; + s.runningTexts = List.from(_runningTexts); + s.runningTextDurations = List.from(_runningTextDurations); + s.marqueeAnimType = _marqueeAnimType; + s.scaleCardLabel = _scaleCardLabel; + s.scaleCardBody = _scaleCardBody; + s.scaleRunningText = _scaleRunningText; + return s; + }); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Pengaturan Tampilan berhasil disimpan', style: GoogleFonts.manrope()), + backgroundColor: SacredColors.primaryContainer, + ), + ); + } + } + + Future _saveIqomahSettings() async { + await ref.read(settingsProvider.notifier).updateSettings((s) { + s.iqomahSubuh = int.tryParse(_iqomahSubuhCtrl.text.trim()) ?? 15; + s.iqomahDzuhur = int.tryParse(_iqomahDzuhurCtrl.text.trim()) ?? 10; + s.iqomahAshar = int.tryParse(_iqomahAsharCtrl.text.trim()) ?? 10; + s.iqomahMaghrib = int.tryParse(_iqomahMaghribCtrl.text.trim()) ?? 10; + s.iqomahIsya = int.tryParse(_iqomahIsyaCtrl.text.trim()) ?? 10; + return s; + }); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Jeda Iqamah berhasil disimpan', style: GoogleFonts.manrope()), + backgroundColor: SacredColors.primaryContainer, + ), + ); + } + } + + Future _syncData() async { + setState(() => _isSyncing = true); + final success = await SyncService.instance.syncMonthlyData(); + setState(() => _isSyncing = false); + + if (mounted) { + ref.invalidate(todayScheduleProvider); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + success ? 'Sinkronisasi jadwal berhasil' : 'Sinkronisasi gagal. Periksa koneksi internet.', + style: GoogleFonts.manrope()), + backgroundColor: success ? SacredColors.primaryContainer : SacredColors.errorContainer, + ), + ); + } + } + + Future _showCitySearchDialog(double s) async { + final queryCtrl = TextEditingController(); + List> results = []; + bool isSearching = false; + + await showDialog( + context: context, + builder: (ctx) { + return StatefulBuilder( + builder: (context, setDialogState) { + return Dialog( + backgroundColor: SacredColors.surfaceContainerLowest, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(SacredRadii.xl)), + child: Container( + width: 800 * s, + height: 600 * s, + padding: EdgeInsets.all(40 * s), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Cari Kota / Kabupaten', + style: GoogleFonts.plusJakartaSans( + fontSize: 32 * s, fontWeight: FontWeight.bold, color: SacredColors.primary)), + SizedBox(height: 24 * s), + Row( + children: [ + Expanded( + child: TextField( + controller: queryCtrl, + style: GoogleFonts.manrope(fontSize: 24 * s, color: SacredColors.onSurface), + decoration: InputDecoration( + hintText: 'Misal: Yogyakarta', + filled: true, + fillColor: SacredColors.surfaceContainerLow, + border: OutlineInputBorder(borderRadius: BorderRadius.circular(SacredRadii.md)), + ), + ), + ), + SizedBox(width: 16 * s), + ElevatedButton.icon( + onPressed: () async { + final query = queryCtrl.text.trim(); + if (query.isEmpty) return; + setDialogState(() => isSearching = true); + final res = await MyQuranSholatService.instance.searchCity(query); + setDialogState(() { + results = res; + isSearching = false; + }); + }, + icon: isSearching + ? SizedBox(width: 20*s, height: 20*s, child: const CircularProgressIndicator(color: SacredColors.onPrimary, strokeWidth: 2)) + : const HugeIcon(icon: HugeIcons.strokeRoundedSearch01, color: SacredColors.onPrimary), + label: Text('CARI', style: GoogleFonts.plusJakartaSans(fontSize: 20*s, fontWeight: FontWeight.bold)), + style: ElevatedButton.styleFrom( + backgroundColor: SacredColors.primary, + foregroundColor: SacredColors.onPrimary, + padding: EdgeInsets.symmetric(horizontal: 32 * s, vertical: 24 * s), + ), + ), + ], + ), + SizedBox(height: 32 * s), + Expanded( + child: results.isEmpty && !isSearching + ? Center(child: Text('Tidak ada hasil', style: GoogleFonts.manrope(fontSize: 20 * s, color: SacredColors.onSurfaceVariant))) + : ListView.builder( + itemCount: results.length, + itemBuilder: (context, index) { + final city = results[index]; + return Padding( + padding: EdgeInsets.only(bottom: 8 * s), + child: ListTile( + title: Text(city['lokasi'] ?? '', style: GoogleFonts.plusJakartaSans(fontSize: 24 * s, color: SacredColors.onSurface)), + subtitle: Text('ID: ${city['id']}', style: GoogleFonts.manrope(fontSize: 18 * s, color: SacredColors.primary)), + tileColor: SacredColors.surfaceContainerLow, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(SacredRadii.sm)), + contentPadding: EdgeInsets.symmetric(horizontal: 24 * s, vertical: 8 * s), + onTap: () async { + final id = city['id'].toString(); + final loc = city['lokasi'].toString(); + + await ref.read(settingsProvider.notifier).updateSettings((s) { + s.cityIdApi = id; + s.cityDisplayName = loc; + return s; + }); + if (!mounted || !ctx.mounted) return; + setState(() { + _cityCtrl.text = '$loc ($id)'; + }); + Navigator.pop(ctx); + }, + ), + ); + }, + ), + ), + ], + ), + ), + ); + } + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + final size = MediaQuery.of(context).size; + final s = size.width / 1920; + + return Scaffold( + backgroundColor: SacredColors.background, + appBar: AppBar( + backgroundColor: SacredColors.surfaceContainerLowest, + title: Text( + 'PENGATURAN SISTEM', + style: GoogleFonts.plusJakartaSans( + fontSize: 24 * s, + fontWeight: FontWeight.w700, + color: SacredColors.onSurface, + letterSpacing: 2 * s, + ), + ), + iconTheme: const IconThemeData(color: SacredColors.primary), + ), + body: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Nav rail area + Container( + width: 350 * s, + color: SacredColors.surfaceContainerLow, + padding: EdgeInsets.all(32 * s), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _NavButton( + title: 'IDENTITAS MASJID', + icon: HugeIcons.strokeRoundedHome01, + isActive: _selectedTab == 0, + scale: s, + onTap: () => setState(() => _selectedTab = 0), + ), + SizedBox(height: 16 * s), + _NavButton( + title: 'JADWAL & SINKRONISASI', + icon: HugeIcons.strokeRoundedCalendar01, + isActive: _selectedTab == 1, + scale: s, + onTap: () => setState(() => _selectedTab = 1), + ), + SizedBox(height: 16 * s), + _NavButton( + title: 'TAMPILAN & MEDIA', + icon: HugeIcons.strokeRoundedImage01, + isActive: _selectedTab == 2, + scale: s, + onTap: () => setState(() => _selectedTab = 2), + ), + SizedBox(height: 16 * s), + _NavButton( + title: 'PENGATURAN JUMAT', + icon: HugeIcons.strokeRoundedCalendar01, + isActive: _selectedTab == 3, + scale: s, + onTap: () => setState(() => _selectedTab = 3), + ), + SizedBox(height: 16 * s), + _NavButton( + title: 'SIMULASI', + icon: HugeIcons.strokeRoundedClock01, + isActive: _selectedTab == 4, + scale: s, + onTap: () => setState(() => _selectedTab = 4), + ), + ], + ), + ), + + // Content area + Expanded( + child: Padding( + padding: EdgeInsets.all(64 * s), + child: _selectedTab == 0 + ? _buildIdentityTab(s) + : _selectedTab == 1 + ? _buildJadwalTab(s) + : _selectedTab == 2 + ? _buildTampilanTab(s) + : _selectedTab == 3 + ? _buildJumatTab(s) + : _buildSimulasiTab(s), + ), + ), + ], + ), + ); + } + + Future _saveJumat() async { + await ref.read(settingsProvider.notifier).updateSettings((s) { + s.khatibName = _khatibCtrl.text.trim(); + s.imamName = _imamCtrl.text.trim(); + return s; + }); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Data Jumat berhasil disimpan', style: GoogleFonts.manrope()), + backgroundColor: SacredColors.primaryContainer, + ), + ); + } + } + + Widget _buildJumatTab(double s) { + return SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Pengaturan Jumat', + style: GoogleFonts.plusJakartaSans( + fontSize: 48 * s, fontWeight: FontWeight.w700, color: SacredColors.secondary), + ), + SizedBox(height: 8 * s), + Text( + 'Data di bawah akan tampil setiap hari Jumat: pada layar utama (banner bawah jam) dan layar Persiapan Khutbah.', + style: GoogleFonts.manrope(fontSize: 16 * s, color: SacredColors.onSurfaceVariant), + ), + SizedBox(height: 40 * s), + + _adminCard(s, child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _sectionLabel('Petugas Shalat Jumat', s), + SizedBox(height: 8 * s), + Text( + 'Nama Khatib dan Imam tampil di layar utama setiap Jumat dan di layar Persiapan Khutbah.', + style: GoogleFonts.manrope(fontSize: 14 * s, color: SacredColors.onSurfaceVariant), + ), + SizedBox(height: 24 * s), + _buildTextField('Nama Khatib Minggu Ini', _khatibCtrl, s), + SizedBox(height: 16 * s), + _buildTextField('Nama Imam Minggu Ini', _imamCtrl, s), + SizedBox(height: 32 * s), + + // Preview chip + if (_khatibCtrl.text.isNotEmpty || _imamCtrl.text.isNotEmpty) ...[ + Text('Preview tampilan:', style: GoogleFonts.manrope(fontSize: 14 * s, color: SacredColors.onSurfaceVariant)), + SizedBox(height: 10 * s), + Container( + padding: EdgeInsets.all(20 * s), + decoration: BoxDecoration( + color: SacredColors.background, + borderRadius: BorderRadius.circular(SacredRadii.lg), + border: Border.all(color: SacredColors.secondary.withValues(alpha: 0.2)), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.star_rounded, color: SacredColors.secondary, size: 16 * s), + SizedBox(width: 8 * s), + Text('JUMAT MUBARAK', style: GoogleFonts.plusJakartaSans( + fontSize: 14 * s, fontWeight: FontWeight.w800, color: SacredColors.secondary, letterSpacing: 2)), + SizedBox(width: 8 * s), + Icon(Icons.star_rounded, color: SacredColors.secondary, size: 16 * s), + SizedBox(width: 24 * s), + if (_khatibCtrl.text.isNotEmpty) + Text('KHATIB ${_khatibCtrl.text}', style: GoogleFonts.manrope( + fontSize: 14 * s, color: SacredColors.onSurface)), + if (_khatibCtrl.text.isNotEmpty && _imamCtrl.text.isNotEmpty) + Text(' | ', style: GoogleFonts.manrope(fontSize: 14 * s, color: SacredColors.onSurfaceVariant)), + if (_imamCtrl.text.isNotEmpty) + Text('IMAM ${_imamCtrl.text}', style: GoogleFonts.manrope( + fontSize: 14 * s, color: SacredColors.onSurface)), + ], + ), + ), + SizedBox(height: 24 * s), + ], + + ElevatedButton.icon( + onPressed: _saveJumat, + style: ElevatedButton.styleFrom( + backgroundColor: SacredColors.secondary, + foregroundColor: Colors.black, + padding: EdgeInsets.symmetric(horizontal: 40 * s, vertical: 20 * s), + textStyle: TextStyle(fontSize: 18 * s, fontWeight: FontWeight.bold), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(SacredRadii.lg)), + ), + icon: const Icon(Icons.save_rounded), + label: const Text('SIMPAN DATA JUMAT'), + ), + ], + )), + + SizedBox(height: 32 * s), + + // Info box + _adminCard(s, child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _sectionLabel('Kapan Digunakan?', s), + SizedBox(height: 16 * s), + _infoRow(Icons.tv, 'Layar Utama (Jumat)', 'Banner bawah jam berubah ke JUMAT MUBARAK, nama khatib & imam tampil di bawahnya.', s), + SizedBox(height: 12 * s), + _infoRow(Icons.timer_outlined, 'Layar Persiapan Khutbah', 'Saat menuju iqomah Dzuhur di hari Jumat, layar menampilkan judul PERSIAPAN KHUTBAH beserta nama petugas.', s), + SizedBox(height: 12 * s), + _infoRow(Icons.info_outline, 'Durasi Blank Screen', 'Durasi Black Screen setelah shalat Jumat dapat diatur di tab Jadwal & Sinkronisasi.', s), + ], + )), + ], + ), + ); + } + + Widget _infoRow(IconData icon, String title, String desc, double s) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(icon, color: SacredColors.secondary, size: 22 * s), + SizedBox(width: 12 * s), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: GoogleFonts.manrope(fontSize: 15 * s, fontWeight: FontWeight.w700, color: SacredColors.onSurface)), + SizedBox(height: 4 * s), + Text(desc, style: GoogleFonts.manrope(fontSize: 13 * s, color: SacredColors.onSurfaceVariant)), + ], + ), + ), + ], + ); + } + + Widget _buildTampilanTab(double s) { + return SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Pengaturan Tampilan & Media', + style: GoogleFonts.plusJakartaSans( + fontSize: 48 * s, + fontWeight: FontWeight.w700, + color: SacredColors.primary, + ), + ), + SizedBox(height: 48 * s), + + // ── Row 1: General settings + Background ── + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Left: Typography & Timers + Expanded( + child: _adminCard(s, child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _sectionLabel('Tipografi & Skala Teks', s), + SizedBox(height: 12 * s), + DropdownButtonFormField( + initialValue: _textScaleIndex, + onChanged: (val) => setState(() => _textScaleIndex = val ?? 1), + items: const [ + DropdownMenuItem(value: 0, child: Text('Kecil (Small)')), + DropdownMenuItem(value: 1, child: Text('Normal (Medium)')), + DropdownMenuItem(value: 2, child: Text('Besar (Large)')), + ], + style: GoogleFonts.plusJakartaSans(fontSize: 22 * s, color: SacredColors.onSurface), + dropdownColor: SacredColors.surfaceContainerHighest, + decoration: InputDecoration( + filled: true, + fillColor: SacredColors.surfaceContainerLowest, + border: OutlineInputBorder(borderRadius: BorderRadius.circular(SacredRadii.md)), + ), + ), + SizedBox(height: 28 * s), + _buildTextField('Durasi Layar Utama (Detik)', _mainDurCtrl, s), + SizedBox(height: 24 * s), + _buildTextField('Durasi Tiap Slideshow (Detik)', _slideDurCtrl, s), + SizedBox(height: 40 * s), + + _sectionLabel('Ukuran Teks Per Kelompok', s), + SizedBox(height: 8 * s), + Text( + 'Kontrol ukuran teks secara spesifik per kelompok, terlepas dari skala global di atas.', + style: GoogleFonts.manrope(fontSize: 14 * s, color: SacredColors.onSurfaceVariant), + ), + SizedBox(height: 20 * s), + _scaleSlider( + s: s, + label: 'Label Shalat (Nama: SUBUH, DZUHUR…)', + value: _scaleCardLabel, + onChanged: (v) => setState(() => _scaleCardLabel = v), + ), + SizedBox(height: 16 * s), + _scaleSlider( + s: s, + label: 'Waktu & Iqamah pada kartu jadwal', + value: _scaleCardBody, + onChanged: (v) => setState(() => _scaleCardBody = v), + ), + SizedBox(height: 16 * s), + _scaleSlider( + s: s, + label: 'Teks Berjalan (Running Text)', + value: _scaleRunningText, + onChanged: (v) => setState(() => _scaleRunningText = v), + ), + + SizedBox(height: 40 * s), + + _sectionLabel('Background Layar Utama (Unsplash)', s), + SizedBox(height: 12 * s), + SwitchListTile( + title: Text('Gunakan Foto Unsplash API', style: GoogleFonts.plusJakartaSans(fontSize: 18 * s, color: SacredColors.onSurface)), + value: _useUnsplash, + onChanged: (val) => setState(() => _useUnsplash = val), + activeThumbColor: SacredColors.primary, + contentPadding: EdgeInsets.zero, + ), + if (_useUnsplash) ...[ + SizedBox(height: 12 * s), + _buildTextField('Kata Kunci (Contoh: mosque, architecture)', _unsplashKeywordCtrl, s), + SizedBox(height: 12 * s), + _buildTextField('Rotasi Foto (Jam)', _unsplashRotationCtrl, s), + ], + + SizedBox(height: 56 * s), + ElevatedButton.icon( + onPressed: _saveTampilan, + style: ElevatedButton.styleFrom( + backgroundColor: SacredColors.primary, + foregroundColor: SacredColors.onPrimary, + padding: EdgeInsets.symmetric(horizontal: 48 * s, vertical: 24 * s), + textStyle: TextStyle(fontSize: 18 * s, fontWeight: FontWeight.bold), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(SacredRadii.lg)), + ), + icon: HugeIcon(icon: HugeIcons.strokeRoundedFloppyDisk, color: SacredColors.onPrimary), + label: const Text('SIMPAN TAMPILAN'), + ), + ], + )), + ), + SizedBox(width: 32 * s), + + // Right: Branded Background + Slideshow + Expanded( + child: Column( + children: [ + // Branded Background Card + _adminCard(s, child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _sectionLabel('Foto Latar Utama (Branding Masjid)', s), + SizedBox(height: 16 * s), + if (_brandedBgImage != null && _brandedBgImage!.isNotEmpty) ...[ + ClipRRect( + borderRadius: BorderRadius.circular(SacredRadii.md), + child: Image.file( + File(_brandedBgImage!), + height: 120 * s, + width: double.infinity, + fit: BoxFit.cover, + ), + ), + SizedBox(height: 12 * s), + Row( + children: [ + Expanded( + child: Text( + _brandedBgImage!.split('/').last, + style: GoogleFonts.manrope(fontSize: 14 * s, color: SacredColors.onSurfaceVariant), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + IconButton( + icon: HugeIcon(icon: HugeIcons.strokeRoundedDelete01, color: SacredColors.error, size: 22 * s), + onPressed: () { + setState(() => _brandedBgImage = null); + _saveTampilan(); + }, + ), + ], + ), + ] else + Text('Belum ada foto latar masjid.', style: GoogleFonts.manrope(fontSize: 16 * s, color: SacredColors.onSurfaceVariant)), + SizedBox(height: 16 * s), + ElevatedButton.icon( + onPressed: () async { + final res = await FilePicker.platform.pickFiles(type: FileType.image); + if (res != null && res.files.single.path != null) { + setState(() => _brandedBgImage = res.files.single.path); + _saveTampilan(); + } + }, + icon: HugeIcon(icon: HugeIcons.strokeRoundedImage01, color: SacredColors.onPrimary, size: 20 * s), + label: Text('PILIH FOTO MASJID', style: TextStyle(fontSize: 16 * s)), + style: ElevatedButton.styleFrom( + backgroundColor: SacredColors.secondary, + foregroundColor: SacredColors.onSecondary, + padding: EdgeInsets.symmetric(horizontal: 24 * s, vertical: 16 * s), + ), + ), + ], + )), + SizedBox(height: 24 * s), + + // Slideshow Gallery Card + _adminCard(s, child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _sectionLabel('Galeri Gambar Slideshow', s), + ElevatedButton.icon( + onPressed: () async { + final res = await FilePicker.platform.pickFiles(type: FileType.image, allowMultiple: true); + if (res != null) { + setState(() { + for (var path in res.paths) { + if (path != null && !_slideshowImages.contains(path)) { + _slideshowImages.add(path); + } + } + }); + _saveTampilan(); + } + }, + icon: HugeIcon(icon: HugeIcons.strokeRoundedPlusSign, color: SacredColors.onSecondary, size: 18 * s), + label: Text('TAMBAH FOTO', style: TextStyle(fontSize: 14 * s)), + style: ElevatedButton.styleFrom( + backgroundColor: SacredColors.secondary, + foregroundColor: SacredColors.onSecondary, + padding: EdgeInsets.symmetric(horizontal: 20 * s, vertical: 14 * s), + ), + ), + ], + ), + SizedBox(height: 16 * s), + if (_slideshowImages.isEmpty) + Text('Belum ada gambar slideshow.', style: GoogleFonts.manrope(fontSize: 16 * s, color: SacredColors.onSurfaceVariant)) + else + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: _slideshowImages.length, + itemBuilder: (context, idx) { + final path = _slideshowImages[idx]; + return ListTile( + leading: ClipRRect( + borderRadius: BorderRadius.circular(SacredRadii.sm), + child: Image.file(File(path), width: 56 * s, height: 56 * s, fit: BoxFit.cover), + ), + title: Text(path.split('/').last, maxLines: 1, overflow: TextOverflow.ellipsis, + style: GoogleFonts.manrope(fontSize: 16 * s, color: SacredColors.onSurface)), + trailing: IconButton( + icon: HugeIcon(icon: HugeIcons.strokeRoundedDelete01, color: SacredColors.error, size: 20 * s), + onPressed: () { + setState(() => _slideshowImages.removeAt(idx)); + _saveTampilan(); + }, + ), + ); + }, + ), + ], + )), + ], + ), + ), + ], + ), + + SizedBox(height: 40 * s), + + // ── Row 2: Running Text Repeater ── + _adminCard(s, child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _sectionLabel('Running Text / Pengumuman', s), + Row( + children: [ + Text('Mode Animasi:', style: GoogleFonts.manrope(fontSize: 16 * s, color: SacredColors.onSurfaceVariant)), + SizedBox(width: 12 * s), + SegmentedButton( + segments: [ + ButtonSegment(value: 'marquee', label: Text('Marquee', style: GoogleFonts.manrope(fontSize: 16 * s))), + ButtonSegment(value: 'fade', label: Text('Fade In-Out', style: GoogleFonts.manrope(fontSize: 16 * s))), + ], + selected: {_marqueeAnimType}, + onSelectionChanged: (val) => setState(() => _marqueeAnimType = val.first), + style: ButtonStyle( + backgroundColor: WidgetStateProperty.resolveWith((states) => + states.contains(WidgetState.selected) ? SacredColors.primary : SacredColors.surfaceContainerLowest), + foregroundColor: WidgetStateProperty.resolveWith((states) => + states.contains(WidgetState.selected) ? SacredColors.onPrimary : SacredColors.onSurfaceVariant), + ), + ), + ], + ), + ], + ), + SizedBox(height: 24 * s), + + // Repeater list + if (_runningTexts.isEmpty) + Padding( + padding: EdgeInsets.symmetric(vertical: 16 * s), + child: Text('Belum ada teks. Klik TAMBAH untuk menambah baris.', style: GoogleFonts.manrope(fontSize: 16 * s, color: SacredColors.onSurfaceVariant)), + ) + else + ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: _runningTexts.length, + separatorBuilder: (_, __) => SizedBox(height: 12 * s), + itemBuilder: (context, idx) { + final textCtrl = TextEditingController(text: _runningTexts[idx]) + ..selection = TextSelection.fromPosition(TextPosition(offset: _runningTexts[idx].length)); + final durCtrl = TextEditingController(text: _runningTextDurations[idx].toString()); + return Container( + padding: EdgeInsets.all(20 * s), + decoration: BoxDecoration( + color: SacredColors.surfaceContainerLowest, + borderRadius: BorderRadius.circular(SacredRadii.md), + border: Border.all(color: SacredColors.outlineVariant.withValues(alpha: 0.3)), + ), + child: Row( + children: [ + Container( + width: 32 * s, + height: 32 * s, + alignment: Alignment.center, + decoration: BoxDecoration( + color: SacredColors.surfaceContainerHighest, + shape: BoxShape.circle, + ), + child: Text('${idx + 1}', style: GoogleFonts.manrope(fontSize: 14 * s, fontWeight: FontWeight.w700, color: SacredColors.primary)), + ), + SizedBox(width: 16 * s), + Expanded( + flex: 5, + child: TextField( + controller: textCtrl, + style: GoogleFonts.plusJakartaSans(fontSize: 20 * s, color: SacredColors.onSurface), + decoration: InputDecoration( + hintText: 'Teks pengumuman...', + filled: true, + fillColor: SacredColors.surfaceContainerLow, + border: OutlineInputBorder(borderRadius: BorderRadius.circular(SacredRadii.sm), borderSide: BorderSide.none), + isDense: true, + contentPadding: EdgeInsets.symmetric(horizontal: 16 * s, vertical: 14 * s), + ), + onChanged: (val) => _runningTexts[idx] = val, + ), + ), + SizedBox(width: 12 * s), + SizedBox( + width: 100 * s, + child: TextField( + controller: durCtrl, + keyboardType: TextInputType.number, + style: GoogleFonts.plusJakartaSans(fontSize: 20 * s, color: SacredColors.onSurface), + decoration: InputDecoration( + hintText: 'Detik', + filled: true, + fillColor: SacredColors.surfaceContainerLow, + border: OutlineInputBorder(borderRadius: BorderRadius.circular(SacredRadii.sm), borderSide: BorderSide.none), + isDense: true, + contentPadding: EdgeInsets.symmetric(horizontal: 16 * s, vertical: 14 * s), + suffixText: 'dtk', + ), + onChanged: (val) => _runningTextDurations[idx] = int.tryParse(val) ?? 12, + ), + ), + SizedBox(width: 8 * s), + IconButton( + icon: HugeIcon(icon: HugeIcons.strokeRoundedDelete01, color: SacredColors.error, size: 22 * s), + onPressed: () { + setState(() { + _runningTexts.removeAt(idx); + _runningTextDurations.removeAt(idx); + }); + }, + ), + ], + ), + ); + }, + ), + + SizedBox(height: 20 * s), + Row( + children: [ + OutlinedButton.icon( + onPressed: () { + setState(() { + _runningTexts.add(''); + _runningTextDurations.add(12); + }); + }, + icon: HugeIcon(icon: HugeIcons.strokeRoundedPlusSign, color: SacredColors.primary, size: 20 * s), + label: Text('TAMBAH BARIS', style: GoogleFonts.plusJakartaSans(fontSize: 16 * s, color: SacredColors.primary)), + style: OutlinedButton.styleFrom( + side: BorderSide(color: SacredColors.primary.withValues(alpha: 0.5)), + padding: EdgeInsets.symmetric(horizontal: 24 * s, vertical: 16 * s), + ), + ), + SizedBox(width: 16 * s), + ElevatedButton.icon( + onPressed: _saveTampilan, + style: ElevatedButton.styleFrom( + backgroundColor: SacredColors.primary, + foregroundColor: SacredColors.onPrimary, + padding: EdgeInsets.symmetric(horizontal: 32 * s, vertical: 16 * s), + ), + icon: HugeIcon(icon: HugeIcons.strokeRoundedFloppyDisk, color: SacredColors.onPrimary, size: 18 * s), + label: Text('SIMPAN TEKS', style: GoogleFonts.plusJakartaSans(fontSize: 16 * s, fontWeight: FontWeight.bold)), + ), + ], + ), + ], + )), + + SizedBox(height: 40 * s), + ], + ), + ); + } + + Widget _adminCard(double s, {required Widget child}) { + return Container( + width: double.infinity, + padding: EdgeInsets.all(36 * s), + decoration: BoxDecoration( + color: SacredColors.surfaceContainerHighest.withValues(alpha: 0.3), + borderRadius: BorderRadius.circular(SacredRadii.xl), + border: Border.all(color: SacredColors.outlineVariant.withValues(alpha: 0.2)), + ), + child: child, + ); + } + + Widget _sectionLabel(String label, double s) { + return Text( + label, + style: GoogleFonts.plusJakartaSans( + fontSize: 20 * s, + fontWeight: FontWeight.w700, + color: SacredColors.primary, + ), + ); + } + + + + Widget _buildIdentityTab(double s) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Identitas & Lokasi Masjid', + style: GoogleFonts.plusJakartaSans( + fontSize: 48 * s, + fontWeight: FontWeight.w700, + color: SacredColors.primary, + ), + ), + SizedBox(height: 48 * s), + Container( + width: 800 * s, + padding: EdgeInsets.all(40 * s), + decoration: BoxDecoration( + color: SacredColors.surfaceContainerHighest.withValues(alpha: 0.3), + borderRadius: BorderRadius.circular(SacredRadii.xl), + border: Border.all(color: SacredColors.outlineVariant.withValues(alpha: 0.2)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildTextField('Nama Masjid', _masjidNameCtrl, s), + SizedBox(height: 32 * s), + _buildTextField('Alamat Lengkap', _masjidAddressCtrl, s, maxLines: 2), + SizedBox(height: 32 * s), + + // City API Config + Text( + 'Lokasi Jadwal Shalat (MyQuran API)', + style: GoogleFonts.manrope( + fontSize: 16 * s, + fontWeight: FontWeight.w600, + color: SacredColors.onSurfaceVariant, + ), + ), + SizedBox(height: 12 * s), + Row( + children: [ + Expanded( + child: TextField( + controller: _cityCtrl, + readOnly: true, + style: GoogleFonts.plusJakartaSans(fontSize: 24 * s, color: SacredColors.onSurface), + decoration: InputDecoration( + filled: true, + fillColor: SacredColors.surfaceContainerLowest, + border: OutlineInputBorder(borderRadius: BorderRadius.circular(SacredRadii.md), borderSide: BorderSide.none), + ), + ), + ), + SizedBox(width: 16 * s), + ElevatedButton.icon( + onPressed: () => _showCitySearchDialog(s), + icon: HugeIcon(icon: HugeIcons.strokeRoundedSearch01, color: SacredColors.onPrimary), + label: Text('CARI KOTA', style: TextStyle(fontSize: 16 * s)), + style: ElevatedButton.styleFrom( + backgroundColor: SacredColors.secondary, + foregroundColor: SacredColors.onPrimary, + padding: EdgeInsets.symmetric(horizontal: 24 * s, vertical: 24 * s), + ), + ), + ], + ), + + SizedBox(height: 64 * s), + ElevatedButton.icon( + onPressed: _saveIdentity, + style: ElevatedButton.styleFrom( + backgroundColor: SacredColors.primary, + foregroundColor: SacredColors.onPrimary, + padding: EdgeInsets.symmetric(horizontal: 48 * s, vertical: 24 * s), + textStyle: TextStyle(fontSize: 20 * s, fontWeight: FontWeight.bold), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(SacredRadii.lg)), + ), + icon: HugeIcon(icon: HugeIcons.strokeRoundedFloppyDisk, color: SacredColors.onPrimary), + label: const Text('SIMPAN PERUBAHAN TULISAN'), + ), + ], + ), + ), + ], + ); + } + + Widget _buildJadwalTab(double s) { + final settings = ref.watch(settingsProvider); + final todayScheduleOption = ref.watch(todayScheduleProvider); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Jadwal & Sinkronisasi', + style: GoogleFonts.plusJakartaSans( + fontSize: 48 * s, + fontWeight: FontWeight.w700, + color: SacredColors.primary, + ), + ), + SizedBox(height: 48 * s), + + // Sync Card + Container( + width: double.infinity, + padding: EdgeInsets.all(40 * s), + decoration: BoxDecoration( + color: SacredColors.surfaceContainerLow, + borderRadius: BorderRadius.circular(SacredRadii.xl), + border: Border.all(color: SacredColors.outlineVariant.withValues(alpha: 0.4)), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Status Data Jadwal', + style: GoogleFonts.manrope(fontSize: 20 * s, fontWeight: FontWeight.w700, color: SacredColors.onSurfaceVariant, letterSpacing: 1 * s), + ), + SizedBox(height: 24 * s), + Row( + children: [ + _buildStatusRow('Terakhir Sync', settings.lastSyncDate ?? 'Belum pernah', HugeIcons.strokeRoundedClock01, s), + SizedBox(width: 48 * s), + _buildStatusRow('Sumber Data', 'api.myquran.com', HugeIcons.strokeRoundedDatabase01, s), + SizedBox(width: 48 * s), + _buildStatusRow('Lokasi Data', settings.cityDisplayName, HugeIcons.strokeRoundedLocation01, s), + ], + ), + ], + ), + ), + ElevatedButton.icon( + onPressed: _isSyncing ? null : _syncData, + style: ElevatedButton.styleFrom( + backgroundColor: SacredColors.secondary, + foregroundColor: SacredColors.onSecondary, + padding: EdgeInsets.symmetric(horizontal: 48 * s, vertical: 32 * s), + textStyle: TextStyle(fontSize: 20 * s, fontWeight: FontWeight.bold), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(SacredRadii.lg)), + ), + icon: _isSyncing + ? SizedBox(width: 24*s, height: 24*s, child: const CircularProgressIndicator(color: SacredColors.onSecondary, strokeWidth: 3)) + : HugeIcon(icon: HugeIcons.strokeRoundedCloudDownload, color: SacredColors.onSecondary), + label: Text(_isSyncing ? 'MENYINKRONKAN...' : 'SINKRONKAN DATA BULAN INI'), + ) + ], + ), + ), + + SizedBox(height: 64 * s), + + // Jeda Waktu Iqamah Settings Card + _adminCard(s, child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _sectionLabel('Jeda Waktu Iqamah (Menit)', s), + SizedBox(height: 8 * s), + Text( + 'Tentukan durasi hitung mundur dari Adzan selesai (1 menit setelah masuk waktu) hingga iqamah. Selama jeda ini, jamaah dapat melakukan shalat sunnah.', + style: GoogleFonts.manrope(fontSize: 14 * s, color: SacredColors.onSurfaceVariant), + ), + SizedBox(height: 32 * s), + Row( + children: [ + Expanded(child: _buildTextField('Iqamah Subuh', _iqomahSubuhCtrl, s)), + SizedBox(width: 16 * s), + Expanded(child: _buildTextField('Iqamah Dzuhur', _iqomahDzuhurCtrl, s)), + SizedBox(width: 16 * s), + Expanded(child: _buildTextField('Iqamah Ashar', _iqomahAsharCtrl, s)), + ], + ), + SizedBox(height: 16 * s), + Row( + children: [ + Expanded(child: _buildTextField('Iqamah Maghrib', _iqomahMaghribCtrl, s)), + SizedBox(width: 16 * s), + Expanded(child: _buildTextField('Iqamah Isya', _iqomahIsyaCtrl, s)), + SizedBox(width: 16 * s), + Expanded(child: SizedBox()), // spacer + ], + ), + SizedBox(height: 32 * s), + ElevatedButton.icon( + onPressed: _saveIqomahSettings, + style: ElevatedButton.styleFrom( + backgroundColor: SacredColors.secondary, + foregroundColor: Colors.black, + padding: EdgeInsets.symmetric(horizontal: 40 * s, vertical: 20 * s), + textStyle: TextStyle(fontSize: 18 * s, fontWeight: FontWeight.bold), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(SacredRadii.lg)), + ), + icon: const Icon(Icons.timer), + label: const Text('SIMPAN JEDA IQAMAH'), + ), + ], + )), + + SizedBox(height: 64 * s), + + Text( + 'Pratinjau Jadwal Hari Ini', + style: GoogleFonts.plusJakartaSans( + fontSize: 32 * s, + fontWeight: FontWeight.w700, + color: SacredColors.onSurface, + ), + ), + SizedBox(height: 32 * s), + + // Schedule Grid + Expanded( + child: Builder( + builder: (context) { + if (todayScheduleOption == null) { + return Center( + child: Text('Data jadwal kosong. Silakan lakukan sinkronisasi.', style: GoogleFonts.manrope(fontSize: 24 * s, color: SacredColors.error)), + ); + } + + final prayerMap = { + 'IMSAK': todayScheduleOption.imsak, + 'SUBUH': todayScheduleOption.subuh, + 'TERBIT': todayScheduleOption.terbit, + 'DHUHA': todayScheduleOption.dhuha, + 'DZUHUR': todayScheduleOption.dzuhur, + 'ASHAR': todayScheduleOption.ashar, + 'MAGHRIB': todayScheduleOption.maghrib, + 'ISYA': todayScheduleOption.isya, + }; + + return GridView.builder( + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 4, + crossAxisSpacing: 24 * s, + mainAxisSpacing: 24 * s, + childAspectRatio: 2.2, // wide rectangular Google Stitch cards + ), + itemCount: prayerMap.length, + itemBuilder: (context, index) { + final key = prayerMap.keys.elementAt(index); + final time = prayerMap[key]!; + return _buildPrayerCard(key, time, s); + }, + ); + }, + ), + ) + ], + ); + } + + Widget _buildPrayerCard(String name, String time, double s) { + return Container( + decoration: BoxDecoration( + color: SacredColors.surfaceContainerLowest.withValues(alpha: 0.5), + borderRadius: BorderRadius.circular(SacredRadii.lg), + border: Border.all(color: SacredColors.outlineVariant.withValues(alpha: 0.3)), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.2), + blurRadius: 10 * s, + offset: Offset(0, 4 * s), + ) + ] + ), + padding: EdgeInsets.symmetric(horizontal: 32 * s, vertical: 24 * s), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + name, + style: GoogleFonts.manrope( + fontSize: 18 * s, + fontWeight: FontWeight.w600, + color: SacredColors.onSurfaceVariant, + letterSpacing: 2 * s, + ), + ), + SizedBox(height: 8 * s), + Text( + time, + style: GoogleFonts.plusJakartaSans( + fontSize: 42 * s, + fontWeight: FontWeight.w800, + color: SacredColors.primary, + ), + ), + ], + ), + ); + } + + Widget _buildTextField(String label, TextEditingController ctrl, double s, {int maxLines = 1}) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: GoogleFonts.manrope( + fontSize: 16 * s, + fontWeight: FontWeight.w600, + color: SacredColors.onSurfaceVariant, + ), + ), + SizedBox(height: 12 * s), + TextField( + controller: ctrl, + maxLines: maxLines, + style: GoogleFonts.plusJakartaSans( + fontSize: 24 * s, + color: SacredColors.onSurface, + ), + decoration: InputDecoration( + filled: true, + fillColor: SacredColors.surfaceContainerLowest, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(SacredRadii.md), + borderSide: BorderSide(color: SacredColors.outlineVariant.withValues(alpha: 0.5)), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(SacredRadii.md), + borderSide: const BorderSide(color: SacredColors.primary, width: 2), + ), + ), + ), + ], + ); + } + + Widget _buildStatusRow(String label, String value, dynamic icon, double s) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: EdgeInsets.all(12 * s), + decoration: BoxDecoration( + color: SacredColors.surfaceContainerHighest, + borderRadius: BorderRadius.circular(SacredRadii.sm), + ), + child: HugeIcon(icon: icon, color: SacredColors.secondary, size: 24 * s), + ), + SizedBox(width: 16 * s), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, style: GoogleFonts.manrope(fontSize: 12 * s, color: SacredColors.onSurfaceVariant)), + Text(value, style: GoogleFonts.plusJakartaSans(fontSize: 18 * s, fontWeight: FontWeight.w600, color: SacredColors.onSurface)), + ], + ), + ], + ); + } + + Widget _scaleSlider({ + required double s, + required String label, + required double value, + required ValueChanged onChanged, + }) { + final pct = (value * 100).round(); + const step = 0.05; + const presets = [0.75, 1.0, 1.25, 1.5]; + + return Container( + padding: EdgeInsets.all(16 * s), + decoration: BoxDecoration( + color: SacredColors.surfaceContainerLowest, + borderRadius: BorderRadius.circular(SacredRadii.md), + border: Border.all(color: SacredColors.outlineVariant.withValues(alpha: 0.25)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text(label, style: GoogleFonts.manrope( + fontSize: 15 * s, fontWeight: FontWeight.w500, color: SacredColors.onSurface)), + ), + Container( + padding: EdgeInsets.symmetric(horizontal: 14 * s, vertical: 5 * s), + decoration: BoxDecoration( + color: SacredColors.primary.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(SacredRadii.sm), + ), + child: Text('$pct%', style: GoogleFonts.manrope( + fontSize: 16 * s, fontWeight: FontWeight.w800, color: SacredColors.primary)), + ), + ], + ), + SizedBox(height: 14 * s), + // TV-remote control row + Row( + children: [ + _tvStepBtn(s: s, label: '−−', onPressed: () => onChanged((value - step * 4).clamp(0.5, 2.0))), + SizedBox(width: 6 * s), + _tvStepBtn(s: s, label: '−', onPressed: () => onChanged((value - step).clamp(0.5, 2.0))), + SizedBox(width: 10 * s), + Expanded( + child: Stack( + alignment: Alignment.centerLeft, + children: [ + Container( + height: 6 * s, + decoration: BoxDecoration( + color: SacredColors.outlineVariant.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(3 * s), + ), + ), + FractionallySizedBox( + widthFactor: ((value - 0.5) / 1.5).clamp(0.0, 1.0), + child: Container( + height: 6 * s, + decoration: BoxDecoration( + color: SacredColors.primary, + borderRadius: BorderRadius.circular(3 * s), + ), + ), + ), + ], + ), + ), + SizedBox(width: 10 * s), + _tvStepBtn(s: s, label: '+', onPressed: () => onChanged((value + step).clamp(0.5, 2.0))), + SizedBox(width: 6 * s), + _tvStepBtn(s: s, label: '++', onPressed: () => onChanged((value + step * 4).clamp(0.5, 2.0))), + ], + ), + SizedBox(height: 12 * s), + // Quick preset chips + Row( + children: [ + Text('Cepat: ', style: GoogleFonts.manrope(fontSize: 12 * s, color: SacredColors.onSurfaceVariant)), + ...presets.map((p) { + final isActive = (value - p).abs() < 0.02; + return Padding( + padding: EdgeInsets.only(right: 8 * s), + child: InkWell( + focusColor: SacredColors.primary.withValues(alpha: 0.3), + borderRadius: BorderRadius.circular(SacredRadii.sm), + onTap: () => onChanged(p), + child: Container( + padding: EdgeInsets.symmetric(horizontal: 12 * s, vertical: 6 * s), + decoration: BoxDecoration( + color: isActive ? SacredColors.primary : SacredColors.surfaceContainerHighest, + borderRadius: BorderRadius.circular(SacredRadii.sm), + border: isActive ? null : Border.all(color: SacredColors.outlineVariant.withValues(alpha: 0.3)), + ), + child: Text('${(p * 100).round()}%', style: GoogleFonts.manrope( + fontSize: 13 * s, fontWeight: FontWeight.w600, + color: isActive ? SacredColors.onPrimary : SacredColors.onSurfaceVariant)), + ), + ), + ); + }), + ], + ), + SizedBox(height: 6 * s), + Text( + 'TV Remote: gunakan ↑↓ untuk pindah fokus, tekan OK pada −/+ untuk mengubah nilai.', + style: GoogleFonts.manrope(fontSize: 11 * s, color: SacredColors.onSurfaceVariant.withValues(alpha: 0.7)), + ), + ], + ), + ); + } + + Widget _tvStepBtn({required double s, required String label, required VoidCallback onPressed}) { + return Material( + color: Colors.transparent, + child: InkWell( + focusColor: SacredColors.primary.withValues(alpha: 0.35), + hoverColor: SacredColors.primary.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(SacredRadii.sm), + onTap: onPressed, + child: Container( + width: 42 * s, + height: 38 * s, + alignment: Alignment.center, + decoration: BoxDecoration( + border: Border.all(color: SacredColors.outlineVariant.withValues(alpha: 0.4)), + borderRadius: BorderRadius.circular(SacredRadii.sm), + color: SacredColors.surfaceContainerHighest, + ), + child: Text( + label, + style: GoogleFonts.manrope( + fontSize: 15 * s, + fontWeight: FontWeight.w700, + color: SacredColors.onSurface, + ), + ), + ), + ), + ); + } + + Widget _buildSimulasiTab(double s) { + return SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Mode Simulasi Pengembang', + style: GoogleFonts.plusJakartaSans( + fontSize: 48 * s, + fontWeight: FontWeight.w700, + color: SacredColors.primary, + ), + ), + SizedBox(height: 16 * s), + Text( + 'Gunakan tombol di bawah ini untuk melihat pratinjau bagaimana aplikasi bereaksi terhadap berbagai waktu dan status tanpa harus menunggu waktu sebenarnya.\nFitur ini bekerja dengan menggeser waktu aplikasi (Time Travel).', + style: GoogleFonts.manrope(fontSize: 18 * s, color: SacredColors.onSurfaceVariant), + ), + SizedBox(height: 48 * s), + + Wrap( + spacing: 24 * s, + runSpacing: 24 * s, + children: [ + _simulasiCard( + s: s, + title: 'Reset Waktu Asli', + icon: HugeIcons.strokeRoundedHome01, + desc: 'Kembali ke waktu saat ini secara sinkron dengan jam sistem.', + onTap: () => _simulateTimeOffset(Duration.zero), + ), + _simulasiCard( + s: s, + title: 'Menuju Adzan', + icon: HugeIcons.strokeRoundedClock01, + desc: 'Melompat ke 2 menit sebelum Adzan Dzuhur hari ini.', + onTap: () => _simulateEvent('pre_adzan'), + ), + _simulasiCard( + s: s, + title: 'Selama Adzan', + icon: HugeIcons.strokeRoundedMegaphone01, + desc: 'Melompat ke tepat waktu Adzan Dzuhur berkumandang.', + onTap: () => _simulateEvent('adzan'), + ), + _simulasiCard( + s: s, + title: 'Menuju Iqomah', + icon: HugeIcons.strokeRoundedTimer02, + desc: 'Melompat ke saat waktu iqomah sedang menghitung mundur (1 menit setelah Adzan).', + onTap: () => _simulateEvent('iqomah'), + ), + _simulasiCard( + s: s, + title: 'Persiapan Jumat', + icon: HugeIcons.strokeRoundedCalendar03, + desc: 'Menyimulasikan layar khusus persiapan Jumat (30 menit sebelum Adzan Dzuhur).', + onTap: () => _simulateEvent('jumat_incoming'), + ), + _simulasiCard( + s: s, + title: 'Khutbah Berlangsung', + icon: HugeIcons.strokeRoundedUserGroup, + desc: 'Menyimulasikan layar saat Khutbah sedang berlangsung tanpa hitungan mundur (2 menit setelah Adzan Dzuhur).', + onTap: () => _simulateEvent('jumat_khutbah'), + ), + _simulasiCard( + s: s, + title: 'Mode Shalat', + icon: HugeIcons.strokeRoundedMoon02, + desc: 'Layar menjadi hitam atau gelap selama shalat berlangsung.', + onTap: () => _simulateEvent('shalat'), + ), + ], + ), + ], + ), + ); + } + + Widget _simulasiCard({required double s, required String title, required dynamic icon, required String desc, required VoidCallback onTap}) { + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(SacredRadii.lg), + child: Container( + width: 320 * s, + padding: EdgeInsets.all(24 * s), + decoration: BoxDecoration( + color: SacredColors.surfaceContainerLowest, + borderRadius: BorderRadius.circular(SacredRadii.lg), + border: Border.all(color: SacredColors.outlineVariant.withValues(alpha: 0.5)), + boxShadow: [ + BoxShadow(color: Colors.black.withValues(alpha: 0.05), blurRadius: 10 * s, offset: Offset(0, 4 * s)), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + HugeIcon(icon: icon, color: SacredColors.primary, size: 40 * s), + SizedBox(height: 16 * s), + Text(title, style: GoogleFonts.plusJakartaSans(fontSize: 20 * s, fontWeight: FontWeight.bold, color: SacredColors.onSurface)), + SizedBox(height: 8 * s), + Text(desc, style: GoogleFonts.manrope(fontSize: 14 * s, color: SacredColors.onSurfaceVariant)), + ], + ), + ), + ); + } + + void _simulateTimeOffset(Duration offset) { + ref.read(mockTimeOffsetProvider.notifier).state = offset; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Offset Waktu disetel ke: ${offset.inMinutes} Menit', style: GoogleFonts.manrope())), + ); + } + + void _simulateEvent(String eventType) { + final schedule = ref.read(todayScheduleProvider); + if (schedule == null) return; + // We simulate using schedule.dzuhur + final dzuhurStr = schedule.dzuhur; + + final parts = dzuhurStr.split(':'); + final realNow = DateTime.now(); + final dzuhurTime = DateTime(realNow.year, realNow.month, realNow.day, int.parse(parts[0]), int.parse(parts[1])); + + DateTime targetTime; + + switch (eventType) { + case 'pre_adzan': + targetTime = dzuhurTime.subtract(const Duration(minutes: 2)); + break; + case 'adzan': + targetTime = dzuhurTime; + break; + case 'iqomah': + targetTime = dzuhurTime.add(const Duration(seconds: 45)); // During iqomah + break; + case 'jumat_incoming': + int diff = DateTime.friday - realNow.weekday; + DateTime nextFriday = realNow.add(Duration(days: diff)); + // Target: next Friday at dzuhur time - 30 minutes + targetTime = DateTime(nextFriday.year, nextFriday.month, nextFriday.day, dzuhurTime.hour, dzuhurTime.minute).subtract(const Duration(minutes: 30)); + break; + case 'jumat_khutbah': + int diff = DateTime.friday - realNow.weekday; + DateTime nextFriday = realNow.add(Duration(days: diff)); + // Target: next Friday at dzuhur time + 3 minutes (safely past 2-min Adzan) + targetTime = DateTime(nextFriday.year, nextFriday.month, nextFriday.day, dzuhurTime.hour, dzuhurTime.minute).add(const Duration(minutes: 3)); + break; + case 'shalat': + // Shalat mode usually happens after iqomah ends + final settings = ref.read(settingsProvider); + targetTime = dzuhurTime.add(Duration(minutes: settings.iqomahDzuhur + 1)); + break; + default: + targetTime = realNow; + } + + final offset = targetTime.difference(realNow); + _simulateTimeOffset(offset); + } +} + +class _NavButton extends StatelessWidget { + final String title; + final dynamic icon; + final bool isActive; + final double scale; + final VoidCallback onTap; + + const _NavButton({ + required this.title, + required this.icon, + required this.isActive, + required this.scale, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + final s = scale; + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(SacredRadii.lg), + child: Container( + width: double.infinity, + padding: EdgeInsets.symmetric(horizontal: 24 * s, vertical: 24 * s), + decoration: BoxDecoration( + color: isActive ? SacredColors.primaryContainer : Colors.transparent, + borderRadius: BorderRadius.circular(SacredRadii.lg), + border: isActive ? Border.all(color: SacredColors.primary.withValues(alpha: 0.3)) : null, + ), + child: Row( + children: [ + HugeIcon( + icon: icon, + color: isActive ? SacredColors.onPrimaryContainer : SacredColors.onSurfaceVariant, + size: 28 * s, + ), + SizedBox(width: 20 * s), + Expanded( + child: Text( + title, + style: GoogleFonts.plusJakartaSans( + fontSize: 18 * s, + fontWeight: FontWeight.bold, + color: isActive ? SacredColors.onPrimaryContainer : SacredColors.onSurfaceVariant, + letterSpacing: 1 * s, + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/home/adzan_screen.dart b/lib/features/home/adzan_screen.dart new file mode 100644 index 0000000..0f6fa4c --- /dev/null +++ b/lib/features/home/adzan_screen.dart @@ -0,0 +1,330 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:google_fonts/google_fonts.dart'; + +import '../../core/sacred_tokens.dart'; +import '../../providers.dart'; + +/// Full-screen Adzan alert with pulsing icon and glowing text. +class AdzanAlertScreen extends ConsumerWidget { + const AdzanAlertScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final screenData = ref.watch(screenStateProvider); + final clock = ref.watch(clockProvider).valueOrNull ?? DateTime.now(); + final schedule = ref.watch(todayScheduleProvider); + final settings = ref.watch(settingsProvider); + final size = MediaQuery.of(context).size; + final s = size.width / 1920; + + final prayerLabel = screenData.activePrayer + ?.displayLabel(isFriday: screenData.isFriday) ?? + ''; + final timeStr = + '${clock.hour.toString().padLeft(2, '0')}:${clock.minute.toString().padLeft(2, '0')}'; + final secStr = clock.second.toString().padLeft(2, '0'); + final fs = s * ref.watch(textScaleProvider); + + return Container( + color: SacredColors.background, + child: Stack( + children: [ + // Background gradient + Positioned.fill( + child: Container( + decoration: BoxDecoration( + gradient: RadialGradient( + center: Alignment.center, + radius: 0.8, + colors: [ + SacredColors.background, + SacredColors.background, + ], + ), + ), + ), + ), + + // Ghost mosque icon + Positioned( + left: 40 * s, + top: 0, + bottom: 0, + child: Center( + child: Opacity( + opacity: 0.03, + child: Icon(Icons.mosque, size: 500 * s, + color: SacredColors.onSurface), + ), + ), + ), + + // ── Header ── + Positioned( + top: 0, + left: 0, + right: 0, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: 64 * s, vertical: 24 * s), + color: SacredColors.background, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + settings.masjidName, + style: GoogleFonts.plusJakartaSans( + fontSize: 32 * s, + fontWeight: FontWeight.w700, + color: SacredColors.primary, + ), + ), + Text( + settings.masjidAddress, + style: GoogleFonts.manrope( + fontSize: 14 * fs, + fontWeight: FontWeight.w500, + color: SacredColors.secondary, + ), + ), + ], + ), + ], + ), + ), + ), + + // ── Central Alert Content ── + Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Pulsing bell icon with glow + _PulsingIcon(scale: s), + + SizedBox(height: 40 * s), + + // "WAKTU ADZAN [PRAYER]" + Text( + 'WAKTU ADZAN $prayerLabel', + style: GoogleFonts.plusJakartaSans( + fontSize: 80 * s, + fontWeight: FontWeight.w800, + color: SacredColors.secondary, + letterSpacing: -2 * s, + shadows: [ + Shadow( + blurRadius: 40 * s, + color: SacredColors.secondary.withValues(alpha: 0.4), + ), + ], + ), + ), + + SizedBox(height: 32 * s), + + // Clock in pill + Container( + padding: EdgeInsets.symmetric( + horizontal: 48 * s, vertical: 20 * s), + decoration: BoxDecoration( + color: SacredColors.surfaceContainerHighest + .withValues(alpha: 0.4), + borderRadius: BorderRadius.circular(SacredRadii.full), + border: Border.all( + color: SacredColors.outlineVariant.withValues(alpha: 0.15), + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + timeStr, + style: GoogleFonts.plusJakartaSans( + fontSize: 120 * s, + fontWeight: FontWeight.w700, + color: SacredColors.primary, + letterSpacing: -3 * s, + height: 1.0, + shadows: [ + Shadow( + blurRadius: 30 * s, + color: + SacredColors.primary.withValues(alpha: 0.3), + ), + ], + ), + ), + SizedBox(width: 12 * s), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + secStr, + style: GoogleFonts.plusJakartaSans( + fontSize: 48 * s, + fontWeight: FontWeight.w700, + color: SacredColors.onSurface + .withValues(alpha: 0.5), + ), + ), + Text( + 'WIB', + style: GoogleFonts.manrope( + fontSize: 20 * s, + fontWeight: FontWeight.w500, + color: SacredColors.primary, + letterSpacing: 4 * s, + ), + ), + ], + ), + ], + ), + ), + + SizedBox(height: 40 * s), + + // Sub-message + Text( + 'Telah masuk waktu shalat $prayerLabel.\nSegera persiapkan diri menuju masjid.', + textAlign: TextAlign.center, + style: GoogleFonts.manrope( + fontSize: 24 * fs, + fontWeight: FontWeight.w500, + color: SacredColors.onSurface.withValues(alpha: 0.8), + height: 1.5, + ), + ), + ], + ), + ), + + // ── Footer: Prayer times strip ── + if (schedule != null) + Positioned( + left: 0, + right: 0, + bottom: 0, + child: _buildFooterStrip(s, fs, schedule, screenData), + ), + ], + ), + ); + } + + Widget _buildFooterStrip( + double s, double fs, dynamic schedule, dynamic screenData) { + final prayers = { + 'Subuh': schedule.subuh, + 'Dzuhur': schedule.dzuhur, + 'Ashar': schedule.ashar, + 'Maghrib': schedule.maghrib, + 'Isya': schedule.isya, + }; + + return Container( + padding: EdgeInsets.symmetric(horizontal: 64 * s, vertical: 28 * s), + color: SacredColors.surfaceContainerLow.withValues(alpha: 0.8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: prayers.entries.map((e) { + final isActive = screenData.activePrayer?.id == e.key.toLowerCase(); + return Column( + children: [ + Text( + e.key.toUpperCase(), + style: GoogleFonts.manrope( + fontSize: 12 * fs, + fontWeight: FontWeight.w700, + color: isActive + ? SacredColors.primary + : SacredColors.onSurface.withValues(alpha: 0.4), + letterSpacing: 2 * s, + ), + ), + SizedBox(height: 4 * s), + Text( + e.value, + style: GoogleFonts.plusJakartaSans( + fontSize: isActive ? 32 * fs : 28 * fs, + fontWeight: isActive ? FontWeight.w700 : FontWeight.w600, + color: isActive + ? SacredColors.primary + : SacredColors.onSurface, + ), + ), + ], + ); + }).toList(), + ), + ); + } +} + +class _PulsingIcon extends StatefulWidget { + final double scale; + const _PulsingIcon({required this.scale}); + + @override + State<_PulsingIcon> createState() => _PulsingIconState(); +} + +class _PulsingIconState extends State<_PulsingIcon> + with SingleTickerProviderStateMixin { + late AnimationController _ctrl; + + @override + void initState() { + super.initState(); + _ctrl = AnimationController( + vsync: this, + duration: const Duration(seconds: 2), + )..repeat(reverse: true); + } + + @override + void dispose() { + _ctrl.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final s = widget.scale; + return AnimatedBuilder( + animation: _ctrl, + builder: (context, child) { + return Stack( + alignment: Alignment.center, + children: [ + Container( + width: 200 * s, + height: 200 * s, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: SacredColors.secondary + .withValues(alpha: 0.1 * _ctrl.value), + boxShadow: [ + BoxShadow( + blurRadius: 60 * s * _ctrl.value, + color: SacredColors.secondary.withValues(alpha: 0.1), + ), + ], + ), + ), + Icon( + Icons.notifications_active, + size: 120 * s, + color: SacredColors.secondary, + ), + ], + ); + }, + ); + } +} diff --git a/lib/features/home/black_screen.dart b/lib/features/home/black_screen.dart new file mode 100644 index 0000000..0e200a3 --- /dev/null +++ b/lib/features/home/black_screen.dart @@ -0,0 +1,108 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:google_fonts/google_fonts.dart'; + +import '../../core/sacred_tokens.dart'; +import '../../providers.dart'; + +/// Minimal black screen during prayer — absolute zero distraction. +class BlackScreen extends ConsumerWidget { + const BlackScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final screenData = ref.watch(screenStateProvider); + final size = MediaQuery.of(context).size; + final s = size.width / 1920; + + final prayerLabel = screenData.activePrayer + ?.displayLabel(isFriday: screenData.isFriday) ?? + ''; + + return Container( + color: SacredColors.blackScreen, + child: Stack( + children: [ + // Extremely subtle radial tonal shift + Positioned.fill( + child: Container( + decoration: BoxDecoration( + gradient: RadialGradient( + center: Alignment.center, + radius: 1.2, + colors: [ + SacredColors.primaryContainer.withValues(alpha: 0.03), + SacredColors.blackScreen, + ], + ), + ), + ), + ), + + // Center: prayer name + status (extremely dim) + Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Opacity( + opacity: 0.10, + child: Text( + prayerLabel.toUpperCase(), + style: GoogleFonts.plusJakartaSans( + fontSize: 96 * s, + fontWeight: FontWeight.w300, + color: SacredColors.onSurface, + letterSpacing: 20 * s, + ), + ), + ), + SizedBox(height: 16 * s), + Opacity( + opacity: 0.08, + child: Text( + 'SHALAT SEDANG BERLANGSUNG', + style: GoogleFonts.plusJakartaSans( + fontSize: 14 * s, + fontWeight: FontWeight.w500, + color: SacredColors.onSurface, + letterSpacing: 6 * s, + ), + ), + ), + ], + ), + ), + + // Bottom advisory — barely visible + Positioned( + bottom: 64 * s, + left: 0, + right: 0, + child: Center( + child: Opacity( + opacity: 0.06, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.phonelink_ring, + size: 14 * s, color: SacredColors.onSurface), + SizedBox(width: 8 * s), + Text( + 'MOHON NONAKTIFKAN ALAT KOMUNIKASI', + style: GoogleFonts.manrope( + fontSize: 10 * s, + fontWeight: FontWeight.w500, + color: SacredColors.onSurface, + letterSpacing: 3 * s, + ), + ), + ], + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/home/home_view.dart b/lib/features/home/home_view.dart new file mode 100644 index 0000000..97c74a1 --- /dev/null +++ b/lib/features/home/home_view.dart @@ -0,0 +1,145 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../data/services/sync_service.dart'; +import '../../data/services/sound_service.dart'; +import '../../core/enums.dart'; +import '../../core/sacred_tokens.dart'; +import '../../providers.dart'; +import 'main_screen.dart'; +import 'adzan_screen.dart'; +import 'iqomah_screen.dart'; +import 'black_screen.dart'; +import 'slideshow_screen.dart'; +import 'jumat_screen.dart'; +import 'khutbah_screen.dart'; + +/// The root view that orchestrates all screen states via AnimatedSwitcher. +class HomeView extends ConsumerStatefulWidget { + const HomeView({super.key}); + + @override + ConsumerState createState() => _HomeViewState(); +} + +class _HomeViewState extends ConsumerState { + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + _checkAutoSync(); + }); + } + + Future _checkAutoSync() async { + final schedule = ref.read(todayScheduleProvider); + if (schedule == null) { + debugPrint('[AutoSync] No schedule found for today! Starting auto-sync...'); + final success = await SyncService.instance.syncMonthlyData(); + if (success && mounted) { + debugPrint('[AutoSync] Success! Invalidating todayScheduleProvider.'); + ref.invalidate(todayScheduleProvider); + } else { + debugPrint('[AutoSync] Failed or data remained empty.'); + } + } + } + + @override + Widget build(BuildContext context) { + // Audio trigger listener + ref.listen(screenStateProvider, (previous, next) { + if (previous == null) return; + + // TRIGGER 1: Adzan Beep (Fires precisely when transitioning to Adzan) + if (previous.state != ScreenState.adzan && next.state == ScreenState.adzan) { + SoundService.instance.playAdzanBeep(); + } + + // TRIGGER 2: 3-Second Iqomah Countdown + if (next.state == ScreenState.menujuIqomah && next.iqomahRemaining != null) { + // Play precisely on the tick where it is 3 seconds. + if (previous.iqomahRemaining?.inSeconds != 3 && next.iqomahRemaining!.inSeconds == 3) { + SoundService.instance.playIqomahCountdown(); + } + } + }); + + final screenData = ref.watch(screenStateProvider); + final isMainScreen = ref.watch(isMainScreenProvider); + + // Determine which screen to display + Widget screen; + switch (screenData.state) { + case ScreenState.normal: + case ScreenState.menujuAdzan: + if (screenData.isFriday && screenData.nextPrayer?.id == 'dzuhur') { + screen = const JumatScreen(key: ValueKey('jumat')); + } else { + screen = isMainScreen + ? const MainScreen(key: ValueKey('main')) + : const SlideshowScreen(key: ValueKey('slideshow')); + } + break; + case ScreenState.kembaliNormal: + screen = const MainScreen(key: ValueKey('main')); + break; + case ScreenState.adzan: + screen = const AdzanAlertScreen(key: ValueKey('adzan')); + break; + case ScreenState.menujuIqomah: + if (screenData.isFriday && screenData.activePrayer?.id == 'dzuhur') { + screen = const KhutbahScreen(key: ValueKey('khutbah')); + } else { + screen = const IqomahScreen(key: ValueKey('iqomah')); + } + break; + case ScreenState.shalat: + screen = const BlackScreen(key: ValueKey('black')); + break; + } + + final isSimulating = ref.watch(mockTimeOffsetProvider) != Duration.zero; + + return Scaffold( + backgroundColor: SacredColors.background, + body: Stack( + children: [ + AnimatedSwitcher( + duration: const Duration(milliseconds: 800), + transitionBuilder: (child, animation) { + return FadeTransition(opacity: animation, child: child); + }, + child: screen, + ), + + if (isSimulating) + Positioned( + right: 64, + bottom: 64, + child: ElevatedButton.icon( + onPressed: () { + ref.read(mockTimeOffsetProvider.notifier).state = Duration.zero; + }, + icon: const Icon(Icons.cancel, color: Colors.white), + label: const Text( + 'BATALKAN SIMULASI', + style: TextStyle( + fontWeight: FontWeight.bold, + letterSpacing: 2, + color: Colors.white, + ), + ), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red.shade800, + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + elevation: 10, + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/home/iqomah_screen.dart b/lib/features/home/iqomah_screen.dart new file mode 100644 index 0000000..2096149 --- /dev/null +++ b/lib/features/home/iqomah_screen.dart @@ -0,0 +1,367 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:google_fonts/google_fonts.dart'; + +import '../../core/sacred_tokens.dart'; +import '../../providers.dart'; + +/// FORMAT HELPER: Duration → "MM:SS" +String _fmtCountdown(Duration d) { + final m = d.inMinutes.toString().padLeft(2, '0'); + final s = (d.inSeconds % 60).toString().padLeft(2, '0'); + return '$m:$s'; +} + +/// Iqomah countdown screen — and Friday Khutbah info override. +class IqomahScreen extends ConsumerWidget { + const IqomahScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final screenData = ref.watch(screenStateProvider); + final settings = ref.watch(settingsProvider); + final size = MediaQuery.of(context).size; + final s = size.width / 1920; + + final prayerLabel = screenData.activePrayer + ?.displayLabel(isFriday: screenData.isFriday) ?? + ''; + final countdown = screenData.iqomahRemaining ?? Duration.zero; + final isFridayDzuhur = + screenData.isFriday && screenData.activePrayer?.id == 'dzuhur'; + + return Container( + color: SacredColors.background, + child: Stack( + children: [ + // Subtle radial glow + Positioned.fill( + child: Container( + decoration: BoxDecoration( + gradient: RadialGradient( + center: Alignment.center, + radius: 0.7, + colors: [ + SacredColors.primary.withValues(alpha: 0.08), + SacredColors.background, + ], + ), + ), + ), + ), + + // ── Content ── + Padding( + padding: EdgeInsets.all(64 * s), + child: Column( + children: [ + // Header + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + settings.masjidName.toUpperCase(), + style: GoogleFonts.plusJakartaSans( + fontSize: 36 * s, + fontWeight: FontWeight.w800, + color: SacredColors.primary, + letterSpacing: -1 * s, + ), + ), + Text( + settings.masjidAddress, + style: GoogleFonts.manrope( + fontSize: 12 * s, + color: SacredColors.onSurface.withValues(alpha: 0.6), + letterSpacing: 4 * s, + ), + ), + ], + ), + ], + ), + + // ── Center: Countdown ── + Expanded( + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Prayer name pill + Text( + 'SHALAT SAAT INI', + style: GoogleFonts.manrope( + fontSize: 16 * s, + fontWeight: FontWeight.w500, + color: SacredColors.onSurface.withValues(alpha: 0.5), + letterSpacing: 8 * s, + ), + ), + SizedBox(height: 12 * s), + Container( + padding: EdgeInsets.symmetric( + horizontal: 48 * s, vertical: 16 * s), + decoration: BoxDecoration( + color: SacredColors.surfaceContainerHighest, + borderRadius: + BorderRadius.circular(SacredRadii.full), + border: Border.all( + color: SacredColors.primary.withValues(alpha: 0.1), + ), + ), + child: Text( + prayerLabel.toUpperCase(), + style: GoogleFonts.plusJakartaSans( + fontSize: 56 * s, + fontWeight: FontWeight.w800, + color: SacredColors.primary, + letterSpacing: 2 * s, + ), + ), + ), + + SizedBox(height: 32 * s), + + // Title + Text( + isFridayDzuhur + ? 'PERSIAPAN KHUTBAH' + : 'MENUJU IQOMAH', + style: GoogleFonts.plusJakartaSans( + fontSize: 36 * s, + fontWeight: FontWeight.w700, + color: SacredColors.secondary, + letterSpacing: 4 * s, + ), + ), + + SizedBox(height: 16 * s), + + // Giant timer + Text( + _fmtCountdown(countdown), + style: GoogleFonts.plusJakartaSans( + fontSize: 280 * s, + fontWeight: FontWeight.w800, + color: SacredColors.onSurface, + letterSpacing: -8 * s, + height: 1.0, + shadows: [ + Shadow( + blurRadius: 40 * s, + color: + SacredColors.primary.withValues(alpha: 0.3), + ), + ], + ), + ), + + // Pulsing status pill + _StatusPill( + label: 'SIAPKAN DIRI ANDA', + scale: s, + ), + + // Friday officers info + if (isFridayDzuhur) ...[ + SizedBox(height: 32 * s), + _FridayOfficers(settings: settings, scale: s), + ], + ], + ), + ), + ), + + // Hadith reminder + Container( + padding: EdgeInsets.all(32 * s), + decoration: BoxDecoration( + color: SacredColors.surfaceContainerLow.withValues(alpha: 0.5), + borderRadius: BorderRadius.circular(SacredRadii.xl), + border: Border( + left: BorderSide( + color: SacredColors.secondary, width: 4 * s), + ), + ), + child: Text( + '"Luruskan dan Rapatkan Shaf, Sesungguhnya lurusnya shaf termasuk kesempurnaan shalat."', + style: GoogleFonts.plusJakartaSans( + fontSize: 28 * s, + fontWeight: FontWeight.w500, + color: SacredColors.onSurface, + fontStyle: FontStyle.italic, + height: 1.5, + ), + ), + ), + ], + ), + ), + ], + ), + ); + } +} + +class _StatusPill extends StatefulWidget { + final String label; + final double scale; + const _StatusPill({required this.label, required this.scale}); + + @override + State<_StatusPill> createState() => _StatusPillState(); +} + +class _StatusPillState extends State<_StatusPill> + with SingleTickerProviderStateMixin { + late AnimationController _ctrl; + + @override + void initState() { + super.initState(); + _ctrl = AnimationController( + vsync: this, duration: const Duration(seconds: 2)) + ..repeat(reverse: true); + } + + @override + void dispose() { + _ctrl.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final s = widget.scale; + return FadeTransition( + opacity: Tween(begin: 0.5, end: 1.0).animate(_ctrl), + child: Container( + margin: EdgeInsets.only(top: 16 * s), + padding: + EdgeInsets.symmetric(horizontal: 32 * s, vertical: 12 * s), + decoration: BoxDecoration( + color: SacredColors.secondary.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(SacredRadii.full), + border: Border.all( + color: SacredColors.secondary.withValues(alpha: 0.2), + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.notifications_active, + color: SacredColors.secondary, size: 20 * s), + SizedBox(width: 8 * s), + Text( + widget.label, + style: GoogleFonts.plusJakartaSans( + fontSize: 16 * s, + fontWeight: FontWeight.w700, + color: SacredColors.secondary, + letterSpacing: 3 * s, + ), + ), + ], + ), + ), + ); + } +} + +class _FridayOfficers extends StatelessWidget { + final dynamic settings; + final double scale; + const _FridayOfficers({required this.settings, required this.scale}); + + @override + Widget build(BuildContext context) { + final s = scale; + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + _OfficerCard( + icon: Icons.person_pin, + label: 'KHATIB', + name: settings.khatibName, + color: SacredColors.primary, + scale: s, + ), + SizedBox(width: 24 * s), + _OfficerCard( + icon: Icons.timer, + label: 'IMAM', + name: settings.imamName, + color: SacredColors.secondary, + scale: s, + ), + ], + ); + } +} + +class _OfficerCard extends StatelessWidget { + final IconData icon; + final String label; + final String name; + final Color color; + final double scale; + + const _OfficerCard({ + required this.icon, + required this.label, + required this.name, + required this.color, + required this.scale, + }); + + @override + Widget build(BuildContext context) { + final s = scale; + return Container( + padding: EdgeInsets.all(24 * s), + decoration: BoxDecoration( + color: SacredColors.surfaceContainerHigh.withValues(alpha: 0.4), + borderRadius: BorderRadius.circular(SacredRadii.xl), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: EdgeInsets.all(12 * s), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(SacredRadii.lg), + ), + child: Icon(icon, color: color, size: 24 * s), + ), + SizedBox(width: 16 * s), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: GoogleFonts.manrope( + fontSize: 10 * s, + fontWeight: FontWeight.w700, + color: SacredColors.onSurface.withValues(alpha: 0.4), + letterSpacing: 3 * s, + ), + ), + Text( + name, + style: GoogleFonts.plusJakartaSans( + fontSize: 20 * s, + fontWeight: FontWeight.w700, + color: SacredColors.onSurface, + ), + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/features/home/jumat_screen.dart b/lib/features/home/jumat_screen.dart new file mode 100644 index 0000000..84e0ab9 --- /dev/null +++ b/lib/features/home/jumat_screen.dart @@ -0,0 +1,530 @@ +import 'dart:io'; +import 'dart:ui'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:intl/intl.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:hugeicons/hugeicons.dart'; + +import '../../core/hijri_date.dart'; +import '../../core/sacred_tokens.dart'; +import '../../providers.dart'; +import '../../data/local/models.dart'; +import '../admin/admin_screen.dart'; +import 'unsplash_background.dart'; + +/// A highly polished, dedicated screen displayed specifically on Fridays +/// when transitioning towards Dzuhur (Jumat) prayer. +class JumatScreen extends ConsumerWidget { + const JumatScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final clock = ref.watch(clockProvider).valueOrNull ?? DateTime.now(); + final schedule = ref.watch(todayScheduleProvider); + final settings = ref.watch(settingsProvider); + final screenData = ref.watch(screenStateProvider); + final size = MediaQuery.of(context).size; + final s = size.width / 1920; + final fs = s * ref.watch(textScaleProvider); + + final timeStr = DateFormat('HH:mm').format(clock); + final secStr = DateFormat(':ss').format(clock); + final dateGregorian = DateFormat('EEEE, d MMMM yyyy', 'en').format(clock); + final dateHijri = HijriDateFormatter.format(clock); + + final durToKhutbah = screenData.timeUntilNext ?? const Duration(minutes: 0); + final minToKhutbah = durToKhutbah.inMinutes; + + return Container( + color: SacredColors.background, + child: Stack( + children: [ + // ── Underlay: Branded local image or Unsplash ── + if (settings.brandedBgImage != null && settings.brandedBgImage!.isNotEmpty) + Positioned.fill( + child: Image.file( + File(settings.brandedBgImage!), + fit: BoxFit.cover, + color: Colors.black.withValues(alpha: 0.55), + colorBlendMode: BlendMode.darken, + ), + ) + else + const UnsplashBackground(), + + // ── Background darkness gradient ── + Positioned.fill( + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + colors: [ + SacredColors.background.withValues(alpha: 0.8), + Colors.transparent, + SacredColors.background.withValues(alpha: 0.9), + ], + stops: const [0.0, 0.4, 1.0], + ), + ), + ), + ), + + // ── Header Shell ── + Positioned( + top: 48 * s, + left: 64 * s, + right: 64 * s, + child: _buildHeader(context, settings, s), + ), + + // ── Main Content Canvas ── + Positioned.fill( + top: 140 * s, + bottom: 240 * s, + left: 64 * s, + right: 64 * s, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // ── Left Column: Clock & Primary Focus ── + Expanded( + flex: 2, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Pill + Container( + padding: EdgeInsets.symmetric(horizontal: 24 * s, vertical: 8 * s), + decoration: BoxDecoration( + color: SacredColors.secondary.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(SacredRadii.full), + border: Border.all(color: SacredColors.secondary.withValues(alpha: 0.2)), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + _PulsingDot(color: SacredColors.secondary, size: 12 * s), + SizedBox(width: 12 * s), + Text( + 'PERSIAPAN JUMAT', + style: GoogleFonts.plusJakartaSans( + fontSize: 16 * s, + fontWeight: FontWeight.w700, + color: SacredColors.secondary, + letterSpacing: 3 * s, + ), + ), + ], + ), + ), + + SizedBox(height: 16 * s), + + // Massive Clock + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + timeStr, + style: GoogleFonts.plusJakartaSans( + fontSize: 192 * s, + fontWeight: FontWeight.w800, + color: SacredColors.primary, + letterSpacing: -5 * s, + height: 1.0, + ), + ), + Padding( + padding: EdgeInsets.only(top: 24 * s), + child: Text( + secStr, + style: GoogleFonts.plusJakartaSans( + fontSize: 48 * s, + fontWeight: FontWeight.w700, + color: SacredColors.primary.withValues(alpha: 0.7), + ), + ), + ), + ], + ), + + SizedBox(height: 16 * s), + + // Dates + Text( + dateGregorian, + style: GoogleFonts.plusJakartaSans( + fontSize: 36 * s, + fontWeight: FontWeight.w700, + color: SacredColors.onSurface, + ), + ), + SizedBox(height: 4 * s), + Text( + dateHijri, + style: GoogleFonts.manrope( + fontSize: 24 * s, + fontWeight: FontWeight.w500, + color: SacredColors.secondary.withValues(alpha: 0.9), + ), + ), + ], + ), + ), + + // ── Right Column: Khutbah Info Card ── + Expanded( + flex: 1, + child: Container( + padding: EdgeInsets.all(40 * s), + decoration: BoxDecoration( + color: SacredColors.surfaceContainerHigh.withValues(alpha: 0.6), + borderRadius: BorderRadius.circular(SacredRadii.xl), + border: Border.all(color: Colors.white.withValues(alpha: 0.05)), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(SacredRadii.xl), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'NEXT EVENT', + style: GoogleFonts.manrope( + fontSize: 14 * s, + fontWeight: FontWeight.w600, + color: SacredColors.onSurfaceVariant, + letterSpacing: 3 * s, + ), + ), + HugeIcon(icon: HugeIcons.strokeRoundedSparkles, color: SacredColors.secondary, size: 24 * s), + ], + ), + SizedBox(height: 16 * s), + Text( + 'MENUJU KHUTBAH', + style: GoogleFonts.plusJakartaSans( + fontSize: 42 * s, + fontWeight: FontWeight.w800, + color: SacredColors.onSurface, + height: 1.1, + ), + ), + SizedBox(height: 32 * s), + + // Khatib Info + _buildInfoTile( + s, + icon: Icons.person_pin, + color: SacredColors.primary, + label: 'KHATIB HARI INI', + value: settings.khatibName.isEmpty ? 'Belum Diatur' : settings.khatibName + ), + SizedBox(height: 24 * s), + + // Countdown Info + _buildInfoTile( + s, + icon: Icons.timer, + color: SacredColors.secondary, + label: 'KHUTBAH DIMULAI DALAM', + value: minToKhutbah > 0 ? '~ $minToKhutbah Menit' : 'Sebentar Lagi' + ), + ], + ), + ), + ), + ), + ), + ], + ), + ), + + // ── Bottom Navigation Shell (Prayer Times) ── + if (schedule != null) + Positioned( + left: 64 * s, + right: 64 * s, + bottom: 64 * s, + child: _buildPrayerTimesRow(s, schedule), + ), + + // ── Footer Marquee Shell ── + Positioned( + left: 0, + right: 0, + bottom: 0, + child: _buildMarquee(s, fs, settings), + ), + ], + ), + ); + } + + Widget _buildHeader(BuildContext context, AppSettings settings, double s) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // Mosque Name + GestureDetector( + onTap: () => Navigator.of(context).push(MaterialPageRoute(builder: (_) => const AdminScreen())), + child: Text( + settings.masjidName, + style: GoogleFonts.plusJakartaSans( + fontSize: 32 * s, + fontWeight: FontWeight.w800, + color: SacredColors.primary, + ), + ), + ), + // Mosque Address + Row( + children: [ + HugeIcon(icon: HugeIcons.strokeRoundedMosque01, color: SacredColors.primary, size: 24 * s), + SizedBox(width: 8 * s), + Text( + settings.masjidAddress, + style: GoogleFonts.manrope( + fontSize: 18 * s, + fontWeight: FontWeight.w500, + color: SacredColors.onSurfaceVariant, + ), + ), + ], + ), + ], + ); + } + + Widget _buildInfoTile(double s, {required IconData icon, required Color color, required String label, required String value}) { + return Container( + padding: EdgeInsets.all(20 * s), + decoration: BoxDecoration( + color: SacredColors.surfaceContainerHighest.withValues(alpha: 0.4), + borderRadius: BorderRadius.circular(SacredRadii.xl), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: EdgeInsets.all(12 * s), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(SacredRadii.md), + ), + child: Icon(icon, color: color, size: 24 * s), + ), + SizedBox(width: 16 * s), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: GoogleFonts.manrope( + fontSize: 12 * s, + fontWeight: FontWeight.w700, + color: SacredColors.onSurfaceVariant, + letterSpacing: 2 * s, + ), + ), + SizedBox(height: 4 * s), + Text( + value, + style: GoogleFonts.plusJakartaSans( + fontSize: 22 * s, + fontWeight: FontWeight.w700, + color: SacredColors.onSurface, + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildPrayerTimesRow(double s, DailyPrayerSchedule schedule) { + return Container( + padding: EdgeInsets.symmetric(horizontal: 16 * s, vertical: 16 * s), + decoration: BoxDecoration( + color: SacredColors.surfaceContainerLow, + borderRadius: BorderRadius.circular(SacredRadii.xl), + boxShadow: [ + BoxShadow( + color: SacredColors.primary.withValues(alpha: 0.1), + blurRadius: 40 * s, + spreadRadius: 0, + ), + ], + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _buildTimeItem(s, 'Fajr', schedule.subuh, Icons.brightness_3, false), + _buildTimeItem(s, 'Terbit', schedule.terbit, Icons.wb_twilight, false), + _buildTimeItem(s, 'JUMAT', schedule.dzuhur, Icons.wb_sunny, true), + _buildTimeItem(s, 'Asr', schedule.ashar, Icons.sunny_snowing, false), + _buildTimeItem(s, 'Maghrib', schedule.maghrib, Icons.wb_cloudy, false), + _buildTimeItem(s, 'Isha', schedule.isya, Icons.bedtime, false), + ], + ), + ); + } + + Widget _buildTimeItem(double s, String name, String time, IconData icon, bool isJumat) { + if (isJumat) { + return Container( + padding: EdgeInsets.symmetric(horizontal: 48 * s, vertical: 12 * s), + decoration: BoxDecoration( + color: SacredColors.primary.withValues(alpha: 0.15), + border: Border.all(color: SacredColors.primary.withValues(alpha: 0.3)), + borderRadius: BorderRadius.circular(SacredRadii.xl), + ), + child: Column( + children: [ + Icon(icon, color: SacredColors.primary, size: 28 * s), + SizedBox(height: 8 * s), + Text(name, style: GoogleFonts.manrope(fontSize: 18 * s, fontWeight: FontWeight.w800, color: SacredColors.primary)), + SizedBox(height: 4 * s), + Text(time, style: GoogleFonts.plusJakartaSans(fontSize: 16 * s, fontWeight: FontWeight.w700, color: SacredColors.primary)), + ], + ), + ); + } + return Padding( + padding: EdgeInsets.symmetric(horizontal: 24 * s, vertical: 8 * s), + child: Column( + children: [ + Icon(icon, color: SacredColors.onSurface.withValues(alpha: 0.6), size: 24 * s), + SizedBox(height: 8 * s), + Text(name, style: GoogleFonts.manrope(fontSize: 16 * s, fontWeight: FontWeight.w500, color: SacredColors.onSurface.withValues(alpha: 0.6))), + SizedBox(height: 4 * s), + Text(time, style: GoogleFonts.plusJakartaSans(fontSize: 16 * s, fontWeight: FontWeight.w700, color: SacredColors.onSurface)), + ], + ), + ); + } + + Widget _buildMarquee(double s, double fs, AppSettings settings) { + // Quick custom simplified marquee or fallback to settings.runningTexts + final texts = settings.runningTexts.isEmpty + ? ["JUMAT MUBARAK: Luruskan dan rapatkan shaf. Harap non-aktifkan alat komunikasi."] + : settings.runningTexts; + + return Container( + width: double.infinity, + height: 44 * s, + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.4), + ), + child: ClipRect( + child: _JumatMarquee( + texts: texts, + s: s, + ), + ), + ); + } +} + +class _PulsingDot extends StatefulWidget { + final Color color; + final double size; + const _PulsingDot({required this.color, required this.size}); + + @override + State<_PulsingDot> createState() => _PulsingDotState(); +} + +class _PulsingDotState extends State<_PulsingDot> with SingleTickerProviderStateMixin { + late AnimationController _ctrl; + @override + void initState() { + super.initState(); + _ctrl = AnimationController(vsync: this, duration: const Duration(milliseconds: 1200))..repeat(); + } + @override + void dispose() { _ctrl.dispose(); super.dispose(); } + @override + Widget build(BuildContext context) { + return SizedBox( + width: widget.size, height: widget.size, + child: Stack( + children: [ + FadeTransition( + opacity: Tween(begin: 0.75, end: 0.0).animate(_ctrl), + child: ScaleTransition( + scale: Tween(begin: 1.0, end: 2.0).animate(_ctrl), + child: Container(decoration: BoxDecoration(shape: BoxShape.circle, color: widget.color)), + ), + ), + Center(child: Container(width: widget.size, height: widget.size, decoration: BoxDecoration(shape: BoxShape.circle, color: widget.color))), + ], + ), + ); + } +} + +class _JumatMarquee extends StatefulWidget { + final List texts; + final double s; + const _JumatMarquee({required this.texts, required this.s}); + @override + State<_JumatMarquee> createState() => _JumatMarqueeState(); +} + +class _JumatMarqueeState extends State<_JumatMarquee> with TickerProviderStateMixin { + late AnimationController _ctrl; + @override + void initState() { + super.initState(); + _ctrl = AnimationController(vsync: this, duration: const Duration(seconds: 30))..repeat(); + } + @override + void dispose() { _ctrl.dispose(); super.dispose(); } + @override + Widget build(BuildContext context) { + final joined = widget.texts.join(" • "); + final style = GoogleFonts.manrope( + fontSize: 16 * widget.s, + fontWeight: FontWeight.w600, + color: SacredColors.secondary, + letterSpacing: 2 * widget.s, + ); + + return LayoutBuilder( + builder: (context, constraints) { + return Stack( + children: [ + AnimatedBuilder( + animation: _ctrl, + builder: (ctx, child) { + // Ensure endless scroll mathematically + return Positioned( + left: -(_ctrl.value * constraints.maxWidth), + top: 0, bottom: 0, + child: Row( + children: [ + Container(alignment: Alignment.centerLeft, width: constraints.maxWidth, child: Text(joined, style: style, maxLines: 1)), + Container(alignment: Alignment.centerLeft, width: constraints.maxWidth, child: Text(joined, style: style, maxLines: 1)), + ], + ), + ); + }, + ), + ], + ); + } + ); + } +} diff --git a/lib/features/home/khutbah_screen.dart b/lib/features/home/khutbah_screen.dart new file mode 100644 index 0000000..2d2ae68 --- /dev/null +++ b/lib/features/home/khutbah_screen.dart @@ -0,0 +1,202 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:google_fonts/google_fonts.dart'; + +import '../../core/sacred_tokens.dart'; +import '../../providers.dart'; + +/// Screen displayed uniquely during the Friday Khutbah. +/// It acts like a black (shalat) screen to avoid distraction, +/// but features the active Khatib and Muadzin information and a pulsing indicator. +/// NO COUNTDOWNS are displayed. +class KhutbahScreen extends ConsumerWidget { + const KhutbahScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final settings = ref.watch(settingsProvider); + final size = MediaQuery.of(context).size; + final s = size.width / 1920; + + return Container( + color: Colors.black, + child: Stack( + children: [ + // Absolute center elements + Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Animated Pulsing Status Pill + Container( + padding: EdgeInsets.symmetric(horizontal: 40 * s, vertical: 16 * s), + decoration: BoxDecoration( + color: SacredColors.background, + borderRadius: BorderRadius.circular(SacredRadii.full), + border: Border.all(color: SacredColors.outlineVariant.withValues(alpha: 0.3)), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + _PulsingMic(size: 24 * s, color: SacredColors.secondary), + SizedBox(width: 16 * s), + Text( + 'SEDANG KHUTBAH JUMAT', + style: GoogleFonts.plusJakartaSans( + fontSize: 24 * s, + fontWeight: FontWeight.w800, + color: SacredColors.secondary, + letterSpacing: 4 * s, + ), + ), + ], + ), + ), + + SizedBox(height: 64 * s), + + // Officer Info Cards + Row( + mainAxisSize: MainAxisSize.min, + children: [ + _buildKhutbahOfficerCard( + s: s, + icon: Icons.person_pin, + label: 'KHATIB', + name: settings.khatibName.isEmpty ? 'Khatib Hari Ini' : settings.khatibName, + color: SacredColors.primary, + ), + SizedBox(width: 48 * s), + _buildKhutbahOfficerCard( + s: s, + icon: Icons.record_voice_over, + label: 'MUADZIN / IMAM', + name: settings.imamName.isEmpty ? 'Muadzin Hari Ini' : settings.imamName, + color: SacredColors.secondary, + ), + ], + ), + + SizedBox(height: 80 * s), + + Text( + '"Harap menjaga ketenangan dan menyimak Khutbah."', + style: GoogleFonts.manrope( + fontSize: 20 * s, + fontWeight: FontWeight.w500, + color: SacredColors.onSurfaceVariant.withValues(alpha: 0.5), + fontStyle: FontStyle.italic, + ), + ) + ], + ), + ), + + // Simple corner logo/name so it doesn't feel entirely dead + Positioned( + top: 48 * s, + left: 48 * s, + child: Row( + children: [ + Icon(Icons.mosque, color: SacredColors.onSurfaceVariant.withValues(alpha: 0.3), size: 24 * s), + SizedBox(width: 12 * s), + Text( + settings.masjidName, + style: GoogleFonts.plusJakartaSans( + fontSize: 16 * s, + fontWeight: FontWeight.w700, + color: SacredColors.onSurfaceVariant.withValues(alpha: 0.3), + letterSpacing: 2 * s, + ), + ), + ], + ), + ) + ], + ), + ); + } + + Widget _buildKhutbahOfficerCard({ + required double s, + required IconData icon, + required String label, + required String name, + required Color color, + }) { + return Container( + width: 400 * s, + padding: EdgeInsets.all(32 * s), + decoration: BoxDecoration( + color: SacredColors.surfaceContainerLow.withValues(alpha: 0.4), + borderRadius: BorderRadius.circular(SacredRadii.xl), + border: Border.all(color: color.withValues(alpha: 0.1)), + ), + child: Column( + children: [ + Container( + padding: EdgeInsets.all(20 * s), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.1), + shape: BoxShape.circle, + ), + child: Icon(icon, color: color, size: 40 * s), + ), + SizedBox(height: 24 * s), + Text( + label, + style: GoogleFonts.manrope( + fontSize: 14 * s, + fontWeight: FontWeight.w700, + color: SacredColors.onSurfaceVariant, + letterSpacing: 4 * s, + ), + ), + SizedBox(height: 8 * s), + Text( + name, + textAlign: TextAlign.center, + style: GoogleFonts.plusJakartaSans( + fontSize: 28 * s, + fontWeight: FontWeight.w800, + color: SacredColors.onSurface, + ), + ), + ], + ), + ); + } +} + +class _PulsingMic extends StatefulWidget { + final double size; + final Color color; + const _PulsingMic({required this.size, required this.color}); + + @override + State<_PulsingMic> createState() => _PulsingMicState(); +} + +class _PulsingMicState extends State<_PulsingMic> with SingleTickerProviderStateMixin { + late AnimationController _ctrl; + + @override + void initState() { + super.initState(); + _ctrl = AnimationController(vsync: this, duration: const Duration(seconds: 2))..repeat(reverse: true); + } + + @override + void dispose() { + _ctrl.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return FadeTransition( + opacity: Tween(begin: 0.3, end: 1.0).animate(_ctrl), + child: Icon(Icons.mic, size: widget.size, color: widget.color), + ); + } +} diff --git a/lib/features/home/main_screen.dart b/lib/features/home/main_screen.dart new file mode 100644 index 0000000..84d8368 --- /dev/null +++ b/lib/features/home/main_screen.dart @@ -0,0 +1,791 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:intl/intl.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:hugeicons/hugeicons.dart'; + +import '../../core/hijri_date.dart'; +import '../../core/sacred_tokens.dart'; +import '../../core/enums.dart'; +import '../../providers.dart'; +import '../../data/local/models.dart'; +import '../admin/admin_screen.dart'; +import 'unsplash_background.dart'; +import 'dart:io'; + +/// FORMAT HELPER: Duration → "HH:MM:SS" +String _fmtDuration(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'); + if (d.inHours > 0) return '$h:$m:$s'; + return '$m:$s'; +} + +/// The primary display — clock, prayer cards, countdown, marquee. +class MainScreen extends ConsumerWidget { + const MainScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final clock = ref.watch(clockProvider).valueOrNull ?? DateTime.now(); + final schedule = ref.watch(todayScheduleProvider); + final settings = ref.watch(settingsProvider); + final screenData = ref.watch(screenStateProvider); + final size = MediaQuery.of(context).size; + final s = size.width / 1920; + final fs = s * ref.watch(textScaleProvider); + + final timeStr = DateFormat('HH:mm').format(clock); + final secStr = DateFormat(':ss').format(clock); + final dateGregorian = DateFormat('EEEE, d MMMM yyyy', 'id').format(clock); + + return Container( + color: SacredColors.background, + child: Stack( + children: [ + // ── Underlay 1: Branded local image (highest priority if set) ── + if (settings.brandedBgImage != null && settings.brandedBgImage!.isNotEmpty) + Positioned.fill( + child: Image.file( + File(settings.brandedBgImage!), + fit: BoxFit.cover, + color: Colors.black.withValues(alpha: 0.55), + colorBlendMode: BlendMode.darken, + ), + ) + else + // ── Underlay 2: API Unsplash Landscape (fallback) ── + const UnsplashBackground(), + + // ── Background radial tint overlay ── + Positioned.fill( + child: Container( + decoration: BoxDecoration( + gradient: RadialGradient( + center: Alignment.center, + radius: 0.8, + colors: [ + SacredColors.primary.withValues(alpha: 0.15), + SacredColors.background, + ], + ), + ), + ), + ), + + // ── Vignette ── + Positioned.fill( + child: DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + SacredColors.background.withValues(alpha: 0.8), + Colors.transparent, + SacredColors.background.withValues(alpha: 0.9), + ], + stops: const [0.0, 0.4, 1.0], + ), + ), + ), + ), + + // ── Main content column ── + Padding( + padding: EdgeInsets.symmetric(horizontal: 64 * s), + child: Column( + children: [ + // ── HEADER ── + _buildHeader(context, s, fs, settings, dateGregorian), + + // ── CENTER: Clock + Countdown ── + Expanded( + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Countdown pill + if (screenData.nextPrayer != null && + screenData.timeUntilNext != null) + _buildCountdownPill(s, fs, screenData), + + SizedBox(height: 16 * s), + + // Massive Clock + Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + timeStr, + style: GoogleFonts.plusJakartaSans( + fontSize: 180 * s, + fontWeight: FontWeight.w800, + color: SacredColors.onSurface, + letterSpacing: -6 * s, + height: 1.0, + shadows: [ + Shadow( + blurRadius: 40 * s, + color: + SacredColors.primary.withValues(alpha: 0.2), + ), + ], + ), + ), + Padding( + padding: EdgeInsets.only(top: 24 * s), + child: Text( + secStr, + style: GoogleFonts.plusJakartaSans( + fontSize: 72 * s, + fontWeight: FontWeight.w700, + color: SacredColors.primary, + letterSpacing: -1 * s, + ), + ), + ), + ], + ), + + // Decorative line + Container( + width: 240 * s, + height: 2 * s, + margin: EdgeInsets.only(top: 12 * s), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Colors.transparent, + SacredColors.primary.withValues(alpha: 0.4), + Colors.transparent, + ], + ), + ), + ), + + SizedBox(height: 16 * s), + + // Date line + Text( + dateGregorian, + style: GoogleFonts.manrope( + fontSize: 24 * fs, + fontWeight: FontWeight.w500, + color: SacredColors.onSurfaceVariant, + letterSpacing: 1 * s, + ), + ), + + // Secondary times (Imsak, Terbit, Dhuha) + if (schedule != null) + Padding( + padding: EdgeInsets.only(top: 24 * s), + child: _buildSecondaryTimes(s, fs, schedule, settings), + ), + + // Removed FRIDAY SPECIAL PANEL since its handled by dedicated JumatScreen + ], + ), + ), + ), + + // ── FOOTER: Prayer Cards ── + if (schedule != null) + _buildPrayerCardsRow( + s, fs, schedule, screenData, settings, clock), + + SizedBox(height: 16 * s), + + // ── MARQUEE ── + _buildMarquee(s, fs, settings), + + SizedBox(height: 12 * s), + ], + ), + ), + ], + ), + ); + } + + Widget _buildHeader(BuildContext context, double s, double fs, AppSettings settings, String dateGregorian) { + final dateHijri = HijriDateFormatter.format(DateTime.now()); + + return Padding( + padding: EdgeInsets.only(top: 24 * s, bottom: 8 * s), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // Left: Mosque name + address (TAPPABLE FOR ADMIN PANEL) + InkWell( + onTap: () { + Navigator.of(context).push( + MaterialPageRoute(builder: (_) => const AdminScreen()), + ); + }, + borderRadius: BorderRadius.circular(8 * s), + child: Padding( + padding: EdgeInsets.all(8.0 * s), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + settings.masjidName, + style: GoogleFonts.plusJakartaSans( + fontSize: 32 * s, + fontWeight: FontWeight.w700, + color: SacredColors.primary, + letterSpacing: -0.5 * s, + ), + ), + SizedBox(height: 4 * s), + Row( + children: [ + HugeIcon( + icon: HugeIcons.strokeRoundedLocation01, + color: SacredColors.secondary, + size: 16 * s, + ), + SizedBox(width: 4 * s), + Text( + settings.masjidAddress, + style: GoogleFonts.manrope( + fontSize: 14 * fs, + fontWeight: FontWeight.w500, + color: SacredColors.onSurface.withValues(alpha: 0.7), + letterSpacing: 0.5 * s, + ), + ), + ], + ), + ], + ), + ), + ), + + // Right: Hijri date + mosque icon + Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + dateHijri, + style: GoogleFonts.plusJakartaSans( + fontSize: 20 * s, + fontWeight: FontWeight.w700, + color: SacredColors.onSurface, + ), + ), + Text( + dateGregorian.toUpperCase(), + style: GoogleFonts.manrope( + fontSize: 12 * fs, + fontWeight: FontWeight.w500, + color: SacredColors.onSurfaceVariant, + letterSpacing: 2 * s, + ), + ), + ], + ), + SizedBox(width: 16 * s), + Container( + width: 48 * s, + height: 48 * s, + decoration: BoxDecoration( + color: SacredColors.surfaceContainerHighest, + shape: BoxShape.circle, + ), + child: HugeIcon( + icon: HugeIcons.strokeRoundedHome01, + color: SacredColors.secondary, + size: 28 * s, + ), + ), + ], + ), + ], + ), + ); + } + + Widget _buildCountdownPill(double s, double fs, ScreenStateData screenData) { + return Container( + padding: + EdgeInsets.symmetric(horizontal: 24 * s, vertical: 8 * s), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(SacredRadii.full), + border: Border.all( + color: SacredColors.primary.withValues(alpha: 0.2), width: 1), + color: SacredColors.primary.withValues(alpha: 0.05), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + // Pulsing dot + _PulsingDot(color: SacredColors.secondary, size: 10 * s), + SizedBox(width: 12 * s), + Text( + 'Menuju Adzan ${screenData.nextPrayer?.displayLabel(isFriday: screenData.isFriday) ?? ""}: ' + '${_fmtDuration(screenData.timeUntilNext!)}', + style: GoogleFonts.plusJakartaSans( + fontSize: 20 * fs, + fontWeight: FontWeight.w700, + color: SacredColors.secondary, + letterSpacing: 0.5 * s, + ), + ), + ], + ), + ); + } + + Widget _buildSecondaryTimes( + double s, double fs, DailyPrayerSchedule schedule, AppSettings settings) { + final items = <_SecondaryTimeItem>[]; + if (settings.showImsak) { + items.add(_SecondaryTimeItem('Imsak', schedule.imsak)); + } + if (settings.showTerbit) { + items.add(_SecondaryTimeItem('Terbit', schedule.terbit)); + } + items.add(_SecondaryTimeItem('Dhuha', schedule.dhuha)); + + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + for (int i = 0; i < items.length; i++) ...[ + Column( + children: [ + Text( + items[i].label.toUpperCase(), + style: GoogleFonts.manrope( + fontSize: 10 * fs, + fontWeight: FontWeight.w700, + color: SacredColors.onSurfaceVariant, + letterSpacing: 3 * s, + ), + ), + SizedBox(height: 4 * s), + Text( + items[i].time, + style: GoogleFonts.plusJakartaSans( + fontSize: 28 * fs, + fontWeight: FontWeight.w600, + color: SacredColors.onSurface, + ), + ), + ], + ), + if (i < items.length - 1) ...[ + Padding( + padding: EdgeInsets.symmetric(horizontal: 24 * s), + child: Container( + width: 1, + height: 40 * s, + color: SacredColors.outlineVariant.withValues(alpha: 0.3)), + ), + ], + ], + ], + ); + } + + + + Widget _buildPrayerCardsRow(double s, double fs, DailyPrayerSchedule schedule, + ScreenStateData screenData, AppSettings settings, DateTime clock) { + final prayers = [ + _PrayerCardData(PrayerName.subuh, schedule.subuh, + 'Iqamah ${_addMinutes(schedule.subuh, settings.iqomahSubuh)}'), + _PrayerCardData(PrayerName.dzuhur, schedule.dzuhur, + 'Iqamah ${_addMinutes(schedule.dzuhur, settings.iqomahDzuhur)}'), + _PrayerCardData(PrayerName.ashar, schedule.ashar, + 'Iqamah ${_addMinutes(schedule.ashar, settings.iqomahAshar)}'), + _PrayerCardData(PrayerName.maghrib, schedule.maghrib, + 'Iqamah ${_addMinutes(schedule.maghrib, settings.iqomahMaghrib)}'), + _PrayerCardData(PrayerName.isya, schedule.isya, + 'Iqamah ${_addMinutes(schedule.isya, settings.iqomahIsya)}'), + ]; + + // Optionally insert Terbit + if (settings.showTerbit) { + prayers.insert( + 1, _PrayerCardData(PrayerName.terbit, schedule.terbit, '-')); + } + + return SizedBox( + height: (140 * s * settings.scaleCardBody).clamp(110 * s, 240 * s), + child: Row( + children: [ + for (int i = 0; i < prayers.length; i++) ...[ + Expanded( + child: _PrayerCard( + data: prayers[i], + isActive: screenData.nextPrayer == prayers[i].name, + isFriday: screenData.isFriday, + s: s, + fs: fs, + scaleLabel: settings.scaleCardLabel, + scaleBody: settings.scaleCardBody, + ), + ), + if (i < prayers.length - 1) SizedBox(width: 12 * s), + ], + ], + ), + ); + } + + Widget _buildMarquee(double s, double fs, AppSettings settings) { + final texts = settings.runningTexts; + if (texts.isEmpty) return const SizedBox.shrink(); + + // Pad durations list to match texts length + final durations = List.generate( + texts.length, + (i) => (i < settings.runningTextDurations.length) + ? settings.runningTextDurations[i] + : 12, + ); + + return Container( + width: double.infinity, + height: 44 * s, + decoration: BoxDecoration( + color: SacredColors.background.withValues(alpha: 0.9), + border: Border( + top: BorderSide( + color: SacredColors.surfaceContainerHighest.withValues(alpha: 0.1), + ), + ), + ), + child: ClipRect( + child: _RunningTextWidget( + texts: texts, + durations: durations, + animType: settings.marqueeAnimType, + style: GoogleFonts.manrope( + fontSize: 16 * fs * settings.scaleRunningText, + fontWeight: FontWeight.w500, + color: SacredColors.secondary, + letterSpacing: 0.8 * s, + ), + ), + ), + ); + } + + String _addMinutes(String time, int minutes) { + final parts = time.split(':'); + final h = int.parse(parts[0]); + final m = int.parse(parts[1]); + final dt = DateTime(2000, 1, 1, h, m).add(Duration(minutes: minutes)); + return DateFormat('HH:mm').format(dt); + } +} + +// ─── Supporting widgets ─── + +class _SecondaryTimeItem { + final String label; + final String time; + _SecondaryTimeItem(this.label, this.time); +} + +class _PrayerCardData { + final PrayerName name; + final String time; + final String iqomahLabel; + _PrayerCardData(this.name, this.time, this.iqomahLabel); +} + +class _PrayerCard extends StatelessWidget { + final _PrayerCardData data; + final bool isActive; + final bool isFriday; + final double s; + final double fs; + final double scaleLabel; // controls prayer name label size + final double scaleBody; // controls time + iqomah text size + + const _PrayerCard({ + required this.data, + required this.isActive, + required this.isFriday, + required this.s, + required this.fs, + this.scaleLabel = 1.0, + this.scaleBody = 1.0, + }); + + @override + Widget build(BuildContext context) { + final label = data.name.displayLabel(isFriday: isFriday); + + return AnimatedContainer( + duration: const Duration(milliseconds: 300), + padding: EdgeInsets.all(16 * s), + decoration: BoxDecoration( + color: isActive + ? SacredColors.primaryContainer + : SacredColors.surfaceContainerLow, + borderRadius: BorderRadius.circular(SacredRadii.xl), + border: isActive + ? Border.all(color: SacredColors.primary, width: 2 * s) + : null, + boxShadow: isActive + ? [ + BoxShadow( + color: SacredColors.primary.withValues(alpha: 0.1), + blurRadius: 40 * s, + ), + ] + : null, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + label.toUpperCase(), + style: GoogleFonts.manrope( + fontSize: 12 * fs * scaleLabel, + fontWeight: FontWeight.w700, + color: isActive + ? SacredColors.onPrimaryContainer + : SacredColors.onSurfaceVariant, + letterSpacing: 2 * s, + ), + ), + if (isActive) + Icon(Icons.notifications_active, + color: SacredColors.onPrimaryContainer, size: 16 * s), + ], + ), + Text( + data.time, + style: GoogleFonts.plusJakartaSans( + fontSize: 32 * fs * scaleBody, + fontWeight: FontWeight.w700, + color: isActive + ? SacredColors.onPrimaryContainer + : SacredColors.onSurface, + ), + ), + Text( + data.iqomahLabel, + style: GoogleFonts.manrope( + fontSize: 10 * fs * scaleBody, + color: isActive + ? SacredColors.onPrimaryContainer.withValues(alpha: 0.8) + : SacredColors.onSurfaceVariant, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + + ), + ); + } +} + +class _PulsingDot extends StatefulWidget { + final Color color; + final double size; + const _PulsingDot({required this.color, required this.size}); + + @override + State<_PulsingDot> createState() => _PulsingDotState(); +} + +class _PulsingDotState extends State<_PulsingDot> + with SingleTickerProviderStateMixin { + late AnimationController _ctrl; + + @override + void initState() { + super.initState(); + _ctrl = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 1200), + )..repeat(); + } + + @override + void dispose() { + _ctrl.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SizedBox( + width: widget.size, + height: widget.size, + child: Stack( + children: [ + FadeTransition( + opacity: Tween(begin: 0.75, end: 0.0).animate(_ctrl), + child: ScaleTransition( + scale: Tween(begin: 1.0, end: 2.0).animate(_ctrl), + child: Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + color: widget.color, + ), + ), + ), + ), + Center( + child: Container( + width: widget.size, + height: widget.size, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: widget.color, + ), + ), + ), + ], + ), + ); + } +} + +// ─── Running Text Widget (marquee + fade modes) ─── + +class _RunningTextWidget extends StatefulWidget { + final List texts; + final List durations; // per-item seconds + final String animType; // 'marquee' or 'fade' + final TextStyle style; + + const _RunningTextWidget({ + required this.texts, + required this.durations, + required this.animType, + required this.style, + }); + + @override + State<_RunningTextWidget> createState() => _RunningTextWidgetState(); +} + +class _RunningTextWidgetState extends State<_RunningTextWidget> + with TickerProviderStateMixin { + int _index = 0; + bool _disposed = false; + late AnimationController _fadeCtrl; + late AnimationController _scrollCtrl; + + @override + void initState() { + super.initState(); + _fadeCtrl = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 600), + ); + _scrollCtrl = AnimationController(vsync: this); + _startCycle(); + } + + void _startCycle() async { + try { + while (!_disposed) { + if (widget.texts.isEmpty) { + await Future.delayed(const Duration(seconds: 1)); + continue; + } + final dur = widget.durations[_index]; + + if (widget.animType == 'fade') { + if (_disposed) break; + await _fadeCtrl.forward().orCancel; + if (_disposed) break; + await Future.delayed(Duration(seconds: dur)); + if (_disposed) break; + await _fadeCtrl.reverse().orCancel; + } else { + if (_disposed) break; + _scrollCtrl.duration = Duration(seconds: dur); + _scrollCtrl.reset(); + await _scrollCtrl.forward().orCancel; + } + + if (_disposed) break; + if (mounted) { + setState(() { + _index = (_index + 1) % widget.texts.length; + }); + } + } + } on TickerCanceled { + // Widget disposed while animation was running — exit cleanly + } catch (e) { + if (!_disposed) rethrow; + } + } + + @override + void dispose() { + _disposed = true; + _fadeCtrl.dispose(); + _scrollCtrl.dispose(); + super.dispose(); + } + + + @override + Widget build(BuildContext context) { + final text = widget.texts[_index]; + + if (widget.animType == 'fade') { + return Center( + child: FadeTransition( + opacity: _fadeCtrl, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.info_outline, + color: SacredColors.secondary, size: 16), + const SizedBox(width: 8), + Text(text, style: widget.style, maxLines: 1), + ], + ), + ), + ); + } + + // Marquee mode + return AnimatedBuilder( + animation: _scrollCtrl, + builder: (context, child) { + final width = MediaQuery.of(context).size.width; + final offset = _scrollCtrl.value * (width + 600); + return Transform.translate( + offset: Offset(width - offset, 0), + child: child, + ); + }, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.info_outline, color: SacredColors.secondary, size: 16), + const SizedBox(width: 8), + Text(text, style: widget.style, maxLines: 1), + const SizedBox(width: 80), + Icon(Icons.circle, color: SacredColors.secondary.withValues(alpha: 0.4), size: 6), + const SizedBox(width: 80), + ], + ), + ); + } +} diff --git a/lib/features/home/slideshow_screen.dart b/lib/features/home/slideshow_screen.dart new file mode 100644 index 0000000..1554881 --- /dev/null +++ b/lib/features/home/slideshow_screen.dart @@ -0,0 +1,201 @@ +import 'dart:io'; +import 'dart:ui'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:google_fonts/google_fonts.dart'; + +import '../../core/sacred_tokens.dart'; +import '../../providers.dart'; + +class SlideshowScreen extends ConsumerStatefulWidget { + const SlideshowScreen({super.key}); + + @override + ConsumerState createState() => _SlideshowScreenState(); +} + +class _SlideshowScreenState extends ConsumerState { + static int _globalImageIndex = 0; + int _localImageIndex = 0; + + @override + void initState() { + super.initState(); + final settings = ref.read(settingsProvider); + if (settings.slideshowImages.isNotEmpty) { + _localImageIndex = _globalImageIndex; + _globalImageIndex = (_globalImageIndex + 1) % settings.slideshowImages.length; + } + } + + @override + Widget build(BuildContext context) { + final size = MediaQuery.of(context).size; + final s = size.width / 1920; + + final screenData = ref.watch(screenStateProvider); + final settings = ref.watch(settingsProvider); + + Widget renderImage() { + if (settings.slideshowImages.isEmpty) { + return _buildErrorImage(s); + } + + final imgPath = settings.slideshowImages[_localImageIndex % settings.slideshowImages.length]; + return Image.file( + File(imgPath), + fit: BoxFit.cover, + errorBuilder: (ctx, err, stack) => _buildErrorImage(s), + ); + } + + return Scaffold( + backgroundColor: Colors.black, + body: Stack( + fit: StackFit.expand, + children: [ + // 1. Poster Image + renderImage(), + + // 2. Subtle Dark Gradient Overlay at bottom so the "glass bar" pops out cleanly + Positioned( + left: 0, + right: 0, + bottom: 0, + height: 300 * s, + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + colors: [ + Colors.black.withValues(alpha: 0.8), + Colors.transparent, + ], + ), + ), + ), + ), + + // 3. Glassmorphic Bottom Bar (Always showing Clock and Next Prayer) + Positioned( + left: 64 * s, + right: 64 * s, + bottom: 64 * s, + child: ClipRRect( + borderRadius: BorderRadius.circular(SacredRadii.xl), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 40, sigmaY: 40), + child: Container( + padding: EdgeInsets.symmetric(horizontal: 48 * s, vertical: 32 * s), + decoration: BoxDecoration( + color: SacredColors.surfaceContainerLowest.withValues(alpha: 0.4), + borderRadius: BorderRadius.circular(SacredRadii.xl), + border: Border.all(color: Colors.white.withValues(alpha: 0.1)), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // Left: Mosque Name + Text( + settings.masjidName, + style: GoogleFonts.plusJakartaSans( + fontSize: 32 * s, + fontWeight: FontWeight.w800, + color: Colors.white, + letterSpacing: 1 * s, + ), + ), + + // Center: Clock + _buildMiniClock(s, screenData), + + // Right: Next Prayer + if (screenData.nextPrayer != null && screenData.timeUntilNext != null) + _buildNextPrayer(s, screenData), + ], + ), + ), + ), + ), + ), + ], + ), + ); + } + + Widget _buildMiniClock(double s, ScreenStateData data) { + final timeStr = "${data.now.hour.toString().padLeft(2, '0')}:${data.now.minute.toString().padLeft(2, '0')}"; + final secStr = data.now.second.toString().padLeft(2, '0'); + + return Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + timeStr, + style: GoogleFonts.plusJakartaSans( + fontSize: 64 * s, + fontWeight: FontWeight.w800, + color: Colors.white, + height: 1.0, + ), + ), + SizedBox(width: 8 * s), + Padding( + padding: EdgeInsets.only(bottom: 8 * s), + child: Text( + secStr, + style: GoogleFonts.plusJakartaSans( + fontSize: 32 * s, + fontWeight: FontWeight.w700, + color: SacredColors.primary, + ), + ), + ), + ], + ); + } + + Widget _buildNextPrayer(double s, ScreenStateData data) { + final dur = data.timeUntilNext!; + final h = dur.inHours; + final m = (dur.inMinutes % 60); + final countDownStr = h > 0 ? "-$h jam $m mnt" : "-$m mnt"; + final prayerTitle = data.nextPrayer!.id.toUpperCase(); + + return Column( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'MENJELANG $prayerTitle', + style: GoogleFonts.plusJakartaSans( + fontSize: 20 * s, + fontWeight: FontWeight.w700, + color: SacredColors.onSurfaceVariant.withValues(alpha: 0.9), + letterSpacing: 2 * s, + ), + ), + SizedBox(height: 4 * s), + Text( + countDownStr, + style: GoogleFonts.manrope( + fontSize: 28 * s, + fontWeight: FontWeight.w800, + color: SacredColors.primary, + ), + ), + ], + ); + } + + Widget _buildErrorImage(double s) { + return Container( + color: SacredColors.surfaceContainerLow, + child: Center( + child: Icon(Icons.broken_image, size: 64 * s, color: SacredColors.onSurfaceVariant), + ), + ); + } +} diff --git a/lib/features/home/unsplash_background.dart b/lib/features/home/unsplash_background.dart new file mode 100644 index 0000000..d1d6b52 --- /dev/null +++ b/lib/features/home/unsplash_background.dart @@ -0,0 +1,102 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../data/services/unsplash_service.dart'; +import '../../providers.dart'; + +/// Renders a securely rotating background using Unsplash API. +class UnsplashBackground extends ConsumerStatefulWidget { + const UnsplashBackground({super.key}); + + @override + ConsumerState createState() => _UnsplashBackgroundState(); +} + +class _UnsplashBackgroundState extends ConsumerState { + List _urls = []; + int _currentIndex = 0; + Timer? _rotationTimer; + String? _lastKeyword; + int? _lastRotationHours; + + @override + void initState() { + super.initState(); + _initDataAndTimer(); + } + + void _initDataAndTimer() async { + final settings = ref.read(settingsProvider); + _lastKeyword = settings.unsplashKeyword; + _lastRotationHours = settings.unsplashRotationHours; + + await _fetchImages(settings.unsplashKeyword); + _startTimer(settings.unsplashRotationHours); + } + + Future _fetchImages(String keyword) async { + final urls = await UnsplashService.instance.fetchLandscapeBackgrounds(keyword); + if (urls.isNotEmpty && mounted) { + setState(() { + _urls = urls; + _currentIndex = 0; + }); + } + } + + void _startTimer(int hours) { + _rotationTimer?.cancel(); + if (hours <= 0) return; + + _rotationTimer = Timer.periodic(Duration(hours: hours), (_) { + if (_urls.isNotEmpty && mounted) { + setState(() { + _currentIndex = (_currentIndex + 1) % _urls.length; + }); + } + }); + } + + @override + void dispose() { + _rotationTimer?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final settings = ref.watch(settingsProvider); + + // Watch for config changes + if (settings.unsplashKeyword != _lastKeyword) { + _lastKeyword = settings.unsplashKeyword; + // Re-fetch images organically + _fetchImages(settings.unsplashKeyword); + } + + if (settings.unsplashRotationHours != _lastRotationHours) { + _lastRotationHours = settings.unsplashRotationHours; + _startTimer(settings.unsplashRotationHours); + } + + if (!settings.useUnsplashBackground || _urls.isEmpty) { + return const SizedBox.shrink(); // Fallback to flat background handled underneath + } + + return AnimatedSwitcher( + duration: const Duration(seconds: 3), + transitionBuilder: (child, animation) => FadeTransition(opacity: animation, child: child), + child: Image.network( + _urls[_currentIndex], + key: ValueKey(_urls[_currentIndex]), + fit: BoxFit.cover, + width: double.infinity, + height: double.infinity, + // Soft opacity behind the MainScreen's dark glass vignette + color: Colors.black.withValues(alpha: 0.5), + colorBlendMode: BlendMode.darken, + ), + ); + } +} diff --git a/lib/main.dart b/lib/main.dart new file mode 100644 index 0000000..e494ad1 --- /dev/null +++ b/lib/main.dart @@ -0,0 +1,78 @@ +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/date_symbol_data_local.dart'; +import 'package:wakelock_plus/wakelock_plus.dart'; + +import 'core/sacred_tokens.dart'; +import 'data/local/models.dart'; +import 'features/home/home_view.dart'; + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + + // Landscape-only for TV + await SystemChrome.setPreferredOrientations([ + DeviceOrientation.landscapeLeft, + DeviceOrientation.landscapeRight, + ]); + + // Hide system overlays for full-screen kiosk mode + await SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); + + // Initialize Hive + await Hive.initFlutter(); + Hive.registerAdapter(AppSettingsAdapter()); + Hive.registerAdapter(DailyPrayerScheduleAdapter()); + await Hive.openBox(HiveBoxes.settings); + await Hive.openBox(HiveBoxes.prayerSchedule); + + // Seed defaults if first launch + final settingsBox = Hive.box(HiveBoxes.settings); + if (settingsBox.get('default') == null) { + await settingsBox.put('default', AppSettings()); + } + + // Initialize date formatting for Indonesian locale + await initializeDateFormatting('id_ID'); + + // Keep screen awake — CRITICAL for 24/7 TV operation + await WakelockPlus.enable(); + + runApp( + const ProviderScope( + child: JamShalatApp(), + ), + ); +} + +class JamShalatApp extends ConsumerWidget { + const JamShalatApp({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + // textScaleProvider will be used selectively in child components. + + return MaterialApp( + title: 'Jam Shalat Digital', + debugShowCheckedModeBanner: false, + theme: ThemeData( + useMaterial3: true, + fontFamily: 'Manrope', + scaffoldBackgroundColor: SacredColors.background, + colorScheme: const ColorScheme.dark( + primary: SacredColors.primary, + secondary: SacredColors.secondary, + surface: SacredColors.surface, + error: SacredColors.error, + onPrimary: SacredColors.onPrimary, + onSecondary: SacredColors.onSecondary, + onSurface: SacredColors.onSurface, + onError: Color(0xFF690005), + ), + ), + home: const HomeView(), + ); + } +} diff --git a/lib/providers.dart b/lib/providers.dart new file mode 100644 index 0000000..65a464e --- /dev/null +++ b/lib/providers.dart @@ -0,0 +1,295 @@ +import 'dart:async'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:hive_flutter/hive_flutter.dart'; + +import 'core/enums.dart'; +import 'data/local/models.dart'; +import 'data/services/sync_service.dart'; + +// ────────────────────────────────────────────── +// SIMULATION (DEVELOPER) MODE +// ────────────────────────────────────────────── +final mockTimeOffsetProvider = StateProvider((ref) => Duration.zero); + +// ────────────────────────────────────────────── +// CLOCK PROVIDER — fires every second +// ────────────────────────────────────────────── + +final clockProvider = StreamProvider((ref) { + final offset = ref.watch(mockTimeOffsetProvider); + return Stream.periodic( + const Duration(seconds: 1), + (_) => DateTime.now().add(offset), + ); +}); + +// ────────────────────────────────────────────── +// SETTINGS PROVIDER +// ────────────────────────────────────────────── + +final settingsProvider = StateNotifierProvider( + (ref) => SettingsNotifier(), +); + +class SettingsNotifier extends StateNotifier { + SettingsNotifier() : super(_loadSettings()); + + static AppSettings _loadSettings() { + final box = Hive.box(HiveBoxes.settings); + return box.get('default')?.copyWith() ?? AppSettings(); + } + + Future updateSettings(AppSettings Function(AppSettings) updater) async { + final updated = updater(state.copyWith()); + final box = Hive.box(HiveBoxes.settings); + await box.put('default', updated); + state = updated; + } + + void reload() { + state = _loadSettings(); + } +} + +// ────────────────────────────────────────────── +// TEXT SCALING PROVIDER +// ────────────────────────────────────────────── +final textScaleProvider = Provider((ref) { + final index = ref.watch(settingsProvider.select((s) => s.textScaleIndex)); + switch (index) { + case 0: return 0.85; // Small + case 2: return 1.15; // Large + case 1: + default: return 1.0; // Medium + } +}); + +// ────────────────────────────────────────────── +// TODAY'S SCHEDULE PROVIDER +// ────────────────────────────────────────────── + +final todayScheduleProvider = Provider((ref) { + // Re-read whenever clock date changes (auto-advance at midnight) + final clock = ref.watch(clockProvider).valueOrNull; + if (clock == null) return null; + return SyncService.instance.getTodaySchedule(clock); +}); + +// ────────────────────────────────────────────── +// SCREEN STATE MACHINE PROVIDER +// ────────────────────────────────────────────── + +/// Computed state that tells the UI which screen to display. +class ScreenStateData { + final ScreenState state; + final PrayerName? activePrayer; // Current or next prayer + final PrayerName? nextPrayer; + final Duration? timeUntilNext; // Countdown to next prayer time + final Duration? iqomahRemaining; // Countdown during iqomah state + final Duration? blankRemaining; // Countdown during shalat/blank state + final bool isFriday; + final DateTime now; + + const ScreenStateData({ + required this.state, + this.activePrayer, + this.nextPrayer, + this.timeUntilNext, + this.iqomahRemaining, + this.blankRemaining, + required this.isFriday, + required this.now, + }); +} + +final screenStateProvider = Provider((ref) { + final clock = ref.watch(clockProvider).valueOrNull ?? DateTime.now(); + final schedule = ref.watch(todayScheduleProvider); + final settings = ref.watch(settingsProvider); + final isFriday = clock.weekday == DateTime.friday; + + if (schedule == null) { + // No data synced yet — stay in normal mode with no countdown + return ScreenStateData( + state: ScreenState.normal, + isFriday: isFriday, + now: clock, + ); + } + + final times = schedule.toDateTimeMap(clock); + + // Build ordered list of fardhu prayer entries + final fardhList = >[ + MapEntry(PrayerName.subuh, times['subuh']!), + MapEntry(PrayerName.dzuhur, times['dzuhur']!), + MapEntry(PrayerName.ashar, times['ashar']!), + MapEntry(PrayerName.maghrib, times['maghrib']!), + MapEntry(PrayerName.isya, times['isya']!), + ]; + + int iqomahMinutes(PrayerName p) { + switch (p) { + case PrayerName.subuh: return settings.iqomahSubuh; + case PrayerName.dzuhur: return settings.iqomahDzuhur; + case PrayerName.ashar: return settings.iqomahAshar; + case PrayerName.maghrib: return settings.iqomahMaghrib; + case PrayerName.isya: return settings.iqomahIsya; + default: return 10; + } + } + + int blankMinutes() { + return isFriday ? settings.blankScreenJumat : settings.blankScreenNormal; + } + + // Check each prayer window in order (latest first for "current") + for (int i = fardhList.length - 1; i >= 0; i--) { + final prayer = fardhList[i]; + final adzanTime = prayer.value; + final preAdzanTime = + adzanTime.subtract(Duration(minutes: settings.preAdzanLead)); + final iqomahDuration = Duration(minutes: iqomahMinutes(prayer.key)); + final iqomahEnd = adzanTime.add(iqomahDuration); + final blankEnd = + iqomahEnd.add(Duration(minutes: blankMinutes())); + + // STATE: SHALAT (Black Screen) + if (clock.isAfter(iqomahEnd) && clock.isBefore(blankEnd)) { + return ScreenStateData( + state: ScreenState.shalat, + activePrayer: prayer.key, + blankRemaining: blankEnd.difference(clock), + isFriday: isFriday, + now: clock, + ); + } + + // STATE: MENUJU IQOMAH (starts after 2-min adzan alert) + final adzanAlertEnd = adzanTime.add(const Duration(minutes: 2)); + if (clock.isAfter(adzanAlertEnd) && clock.isBefore(iqomahEnd)) { + return ScreenStateData( + state: ScreenState.menujuIqomah, + activePrayer: prayer.key, + iqomahRemaining: iqomahEnd.difference(clock), + isFriday: isFriday, + now: clock, + ); + } + + // STATE: ADZAN (first 2 minutes after adzan time) + if (clock.isAfter(adzanTime) && clock.isBefore(adzanAlertEnd)) { + return ScreenStateData( + state: ScreenState.adzan, + activePrayer: prayer.key, + iqomahRemaining: iqomahEnd.difference(clock), + isFriday: isFriday, + now: clock, + ); + } + + // STATE: MENUJU ADZAN (pre-adzan lock) + if (clock.isAfter(preAdzanTime) && clock.isBefore(adzanTime)) { + return ScreenStateData( + state: ScreenState.menujuAdzan, + activePrayer: prayer.key, + nextPrayer: prayer.key, + timeUntilNext: adzanTime.difference(clock), + isFriday: isFriday, + now: clock, + ); + } + } + + // STATE: NORMAL — find next upcoming prayer for countdown + PrayerName? nextPrayer; + Duration? untilNext; + for (final prayer in fardhList) { + if (clock.isBefore(prayer.value)) { + nextPrayer = prayer.key; + untilNext = prayer.value.difference(clock); + break; + } + } + + return ScreenStateData( + state: ScreenState.normal, + nextPrayer: nextPrayer, + timeUntilNext: untilNext, + isFriday: isFriday, + now: clock, + ); +}); + +// ────────────────────────────────────────────── +// ROTATION PROVIDER (for Normal state slideshow) +// ────────────────────────────────────────────── + +/// Controls the rotation between main screen and slideshow views. +final rotationIndexProvider = + StateNotifierProvider((ref) { + return RotationNotifier(ref); +}); + +class RotationNotifier extends StateNotifier { + final Ref _ref; + Timer? _timer; + int _elapsed = 0; + + RotationNotifier(this._ref) : super(0) { + _startRotation(); + } + + void _startRotation() { + _timer?.cancel(); + _elapsed = 0; + _timer = Timer.periodic(const Duration(seconds: 1), (_) { + final screenState = _ref.read(screenStateProvider); + if (screenState.state != ScreenState.normal) { + // Don't rotate during special states, reset elapsed + _elapsed = 0; + return; + } + + _elapsed++; + final settings = _ref.read(settingsProvider); + final validSlides = settings.slideshowImages.where((i) => i.trim().isNotEmpty).toList(); + final hasContent = validSlides.isNotEmpty; + if (!hasContent) { + _elapsed = 0; + if (state != 0) state = 0; // force main screen state + return; + } + + final isMainScreen = state % 2 == 0; + final duration = isMainScreen + ? settings.mainScreenDurationSec + : settings.slideDurationSec; + + if (_elapsed >= duration) { + _elapsed = 0; + state = state + 1; + } + }); + } + + @override + void dispose() { + _timer?.cancel(); + super.dispose(); + } +} + +/// Whether we're currently showing the main screen or slideshow. +/// Returns true (main) always if no slideshow images are configured AND +/// Unsplash background is disabled — no point rotating to an empty slide. +final isMainScreenProvider = Provider((ref) { + final settings = ref.watch(settingsProvider); + final validSlides = settings.slideshowImages.where((i) => i.trim().isNotEmpty).toList(); + final hasContent = validSlides.isNotEmpty; + if (!hasContent) return true; // always stay on main screen + + final index = ref.watch(rotationIndexProvider); + // Even = main, Odd = slideshow + return index % 2 == 0; +}); diff --git a/linux/.gitignore b/linux/.gitignore new file mode 100644 index 0000000..d3896c9 --- /dev/null +++ b/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt new file mode 100644 index 0000000..e2657a8 --- /dev/null +++ b/linux/CMakeLists.txt @@ -0,0 +1,128 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.13) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "jamshalat_masjid_screen") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "com.jamshalat.jamshalat_masjid_screen") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/linux/flutter/CMakeLists.txt b/linux/flutter/CMakeLists.txt new file mode 100644 index 0000000..d5bd016 --- /dev/null +++ b/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..1830e5c --- /dev/null +++ b/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include + +void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) audioplayers_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "AudioplayersLinuxPlugin"); + audioplayers_linux_plugin_register_with_registrar(audioplayers_linux_registrar); +} diff --git a/linux/flutter/generated_plugin_registrant.h b/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..e0f0a47 --- /dev/null +++ b/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake new file mode 100644 index 0000000..e9abb91 --- /dev/null +++ b/linux/flutter/generated_plugins.cmake @@ -0,0 +1,24 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + audioplayers_linux +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/linux/runner/CMakeLists.txt b/linux/runner/CMakeLists.txt new file mode 100644 index 0000000..e97dabc --- /dev/null +++ b/linux/runner/CMakeLists.txt @@ -0,0 +1,26 @@ +cmake_minimum_required(VERSION 3.13) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the application ID. +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") diff --git a/linux/runner/main.cc b/linux/runner/main.cc new file mode 100644 index 0000000..e7c5c54 --- /dev/null +++ b/linux/runner/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/linux/runner/my_application.cc b/linux/runner/my_application.cc new file mode 100644 index 0000000..a89a973 --- /dev/null +++ b/linux/runner/my_application.cc @@ -0,0 +1,148 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Called when first Flutter frame received. +static void first_frame_cb(MyApplication* self, FlView* view) { + gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view))); +} + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "jamshalat_masjid_screen"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "jamshalat_masjid_screen"); + } + + gtk_window_set_default_size(window, 1280, 720); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments( + project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + GdkRGBA background_color; + // Background defaults to black, override it here if necessary, e.g. #00000000 + // for transparent. + gdk_rgba_parse(&background_color, "#000000"); + fl_view_set_background_color(view, &background_color); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + // Show the window when Flutter renders. + // Requires the view to be realized so we can start rendering. + g_signal_connect_swapped(view, "first-frame", G_CALLBACK(first_frame_cb), + self); + gtk_widget_realize(GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, + gchar*** arguments, + int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GApplication::startup. +static void my_application_startup(GApplication* application) { + // MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application startup. + + G_APPLICATION_CLASS(my_application_parent_class)->startup(application); +} + +// Implements GApplication::shutdown. +static void my_application_shutdown(GApplication* application) { + // MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application shutdown. + + G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application); +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = + my_application_local_command_line; + G_APPLICATION_CLASS(klass)->startup = my_application_startup; + G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + // Set the program name to the application ID, which helps various systems + // like GTK and desktop environments map this running application to its + // corresponding .desktop file. This ensures better integration by allowing + // the application to be recognized beyond its binary name. + g_set_prgname(APPLICATION_ID); + + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, "flags", + G_APPLICATION_NON_UNIQUE, nullptr)); +} diff --git a/linux/runner/my_application.h b/linux/runner/my_application.h new file mode 100644 index 0000000..db16367 --- /dev/null +++ b/linux/runner/my_application.h @@ -0,0 +1,21 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, + my_application, + MY, + APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ 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..bb79f5b --- /dev/null +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,18 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import audioplayers_darwin +import file_picker +import package_info_plus +import wakelock_plus + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin")) + FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) + FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) + WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin")) +} 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..7ef48d4 --- /dev/null +++ b/macos/Podfile.lock @@ -0,0 +1,41 @@ +PODS: + - audioplayers_darwin (0.0.1): + - Flutter + - FlutterMacOS + - file_picker (0.0.1): + - FlutterMacOS + - FlutterMacOS (1.0.0) + - package_info_plus (0.0.1): + - FlutterMacOS + - wakelock_plus (0.0.1): + - FlutterMacOS + +DEPENDENCIES: + - audioplayers_darwin (from `Flutter/ephemeral/.symlinks/plugins/audioplayers_darwin/darwin`) + - file_picker (from `Flutter/ephemeral/.symlinks/plugins/file_picker/macos`) + - FlutterMacOS (from `Flutter/ephemeral`) + - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`) + - wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`) + +EXTERNAL SOURCES: + audioplayers_darwin: + :path: Flutter/ephemeral/.symlinks/plugins/audioplayers_darwin/darwin + file_picker: + :path: Flutter/ephemeral/.symlinks/plugins/file_picker/macos + FlutterMacOS: + :path: Flutter/ephemeral + package_info_plus: + :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos + wakelock_plus: + :path: Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos + +SPEC CHECKSUMS: + audioplayers_darwin: f15e209a3e856d1a7edcf98dc029f484fead2242 + file_picker: e716a70a9fe5fd9e09ebc922d7541464289443af + FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 + package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b + wakelock_plus: 9d63063ffb7af1c215209769067c57103bde719d + +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..c89f8a6 --- /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 */ + 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 */; }; + 47B4F4A5C5D22061E38D13E5 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8EF06810A4EFF18E8C0B1901 /* Pods_RunnerTests.framework */; }; + DA30FA7FAD4D53E47A6F7536 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 28454FE461817AE52DD9A999 /* Pods_Runner.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 */ + 28454FE461817AE52DD9A999 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 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_masjid_screen.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = jamshalat_masjid_screen.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 = ""; }; + 431CBF1CC27B86E02B3D32A5 /* 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 = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 845C04D3C1A5DC79F11D9683 /* 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 = ""; }; + 8EF06810A4EFF18E8C0B1901 /* 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 = ""; }; + DF86F0BF34D35DD629D59E93 /* 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 = ""; }; + E3C93BD5B31B21D2DF0CA019 /* 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 = ""; }; + E6AB17BEABDD481DD4763442 /* 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 = ""; }; + E704E803224656BF020F39CE /* 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 = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 331C80D2294CF70F00263BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 47B4F4A5C5D22061E38D13E5 /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + DA30FA7FAD4D53E47A6F7536 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 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 */, + 465212DF52DF6C93D2D58913 /* Pods */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* jamshalat_masjid_screen.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 = ""; + }; + 465212DF52DF6C93D2D58913 /* Pods */ = { + isa = PBXGroup; + children = ( + DF86F0BF34D35DD629D59E93 /* Pods-Runner.debug.xcconfig */, + E6AB17BEABDD481DD4763442 /* Pods-Runner.release.xcconfig */, + E3C93BD5B31B21D2DF0CA019 /* Pods-Runner.profile.xcconfig */, + E704E803224656BF020F39CE /* Pods-RunnerTests.debug.xcconfig */, + 845C04D3C1A5DC79F11D9683 /* Pods-RunnerTests.release.xcconfig */, + 431CBF1CC27B86E02B3D32A5 /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 28454FE461817AE52DD9A999 /* Pods_Runner.framework */, + 8EF06810A4EFF18E8C0B1901 /* 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 = ( + 8D9CF755E4F131F3FCFE89F3 /* [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 = ( + 069A5A65409D18B8E35CE7A9 /* [CP] Check Pods Manifest.lock */, + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + 5060E581587DF54A28ECC3CD /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* jamshalat_masjid_screen.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 */ + 069A5A65409D18B8E35CE7A9 /* [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"; + }; + 5060E581587DF54A28ECC3CD /* [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; + }; + 8D9CF755E4F131F3FCFE89F3 /* [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 = E704E803224656BF020F39CE /* 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.jamshalatMasjidScreen.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/jamshalat_masjid_screen.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/jamshalat_masjid_screen"; + }; + name = Debug; + }; + 331C80DC294CF71000263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 845C04D3C1A5DC79F11D9683 /* 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.jamshalatMasjidScreen.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/jamshalat_masjid_screen.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/jamshalat_masjid_screen"; + }; + name = Release; + }; + 331C80DD294CF71000263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 431CBF1CC27B86E02B3D32A5 /* 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.jamshalatMasjidScreen.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/jamshalat_masjid_screen.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/jamshalat_masjid_screen"; + }; + 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..8424beb --- /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..42cd222 --- /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_masjid_screen + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.jamshalat.jamshalatMasjidScreen + +// 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..4ec85ba --- /dev/null +++ b/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.network.client + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + + 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..f9178c3 --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,687 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + 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: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 + url: "https://pub.dev" + source: hosted + version: "2.13.1" + audioplayers: + dependency: "direct main" + description: + name: audioplayers + sha256: a72dd459d1a48f61a6fb9c0134dba26597c9236af40639ff0eb70eb4e0baab70 + url: "https://pub.dev" + source: hosted + version: "6.6.0" + audioplayers_android: + dependency: transitive + description: + name: audioplayers_android + sha256: "60a6728277228413a85755bd3ffd6fab98f6555608923813ce383b190a360605" + url: "https://pub.dev" + source: hosted + version: "5.2.1" + audioplayers_darwin: + dependency: transitive + description: + name: audioplayers_darwin + sha256: c994b3bb3a921e4904ac40e013fbc94488e824fd7c1de6326f549943b0b44a91 + url: "https://pub.dev" + source: hosted + version: "6.4.0" + audioplayers_linux: + dependency: transitive + description: + name: audioplayers_linux + sha256: f75bce1ce864170ef5e6a2c6a61cd3339e1a17ce11e99a25bae4474ea491d001 + url: "https://pub.dev" + source: hosted + version: "4.2.1" + audioplayers_platform_interface: + dependency: transitive + description: + name: audioplayers_platform_interface + sha256: "0e2f6a919ab56d0fec272e801abc07b26ae7f31980f912f24af4748763e5a656" + url: "https://pub.dev" + source: hosted + version: "7.1.1" + audioplayers_web: + dependency: transitive + description: + name: audioplayers_web + sha256: faa8fa6587f996a6f604433b53af44c57a1407d4fe8dff5766cf63d6875e8de9 + url: "https://pub.dev" + source: hosted + version: "5.2.0" + audioplayers_windows: + dependency: transitive + description: + name: audioplayers_windows + sha256: bafff2b38b6f6d331887558ba6e0a01c9c208d9dbb3ad0005234db065122a734 + url: "https://pub.dev" + source: hosted + version: "4.3.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + url: "https://pub.dev" + source: hosted + version: "1.4.1" + 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" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937" + url: "https://pub.dev" + source: hosted + version: "0.3.5+2" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" + 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" + file_picker: + dependency: "direct main" + description: + name: file_picker + sha256: ab13ae8ef5580a411c458d6207b6774a6c237d77ac37011b13994879f68a8810 + url: "https://pub.dev" + source: hosted + version: "8.3.7" + 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_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" + url: "https://pub.dev" + source: hosted + version: "5.0.0" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: "38d1c268de9097ff59cf0e844ac38759fc78f76836d37edad06fa21e182055a0" + url: "https://pub.dev" + source: hosted + version: "2.0.34" + flutter_riverpod: + dependency: "direct main" + description: + name: flutter_riverpod + sha256: "9532ee6db4a943a1ed8383072a2e3eeda041db5657cdf6d2acecf3c21ecbe7e1" + url: "https://pub.dev" + source: hosted + version: "2.6.1" + flutter_svg: + dependency: transitive + description: + name: flutter_svg + sha256: "1ded017b39c8e15c8948ea855070a5ff8ff8b3d5e83f3446e02d6bb12add7ad9" + url: "https://pub.dev" + source: hosted + version: "2.2.4" + 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" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" + google_fonts: + dependency: "direct main" + description: + name: google_fonts + sha256: ba03d03bcaa2f6cb7bd920e3b5027181db75ab524f8891c8bc3aa603885b8055 + url: "https://pub.dev" + source: hosted + version: "6.3.3" + 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" + 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_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + hugeicons: + dependency: "direct main" + description: + name: hugeicons + sha256: d19c0e2b57ccf455dd8ef08b84da40ae6dbba898c92960a0a0ada77df7865b8a + url: "https://pub.dev" + source: hosted + version: "1.1.5" + intl: + dependency: "direct main" + description: + name: intl + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + url: "https://pub.dev" + source: hosted + version: "0.20.2" + 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: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 + url: "https://pub.dev" + source: hosted + version: "5.1.1" + 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" + meta: + dependency: transitive + description: + name: meta + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + url: "https://pub.dev" + source: hosted + version: "1.17.0" + native_toolchain_c: + dependency: transitive + description: + name: native_toolchain_c + sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572" + url: "https://pub.dev" + source: hosted + version: "0.17.6" + objective_c: + dependency: transitive + description: + name: objective_c + sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52" + url: "https://pub.dev" + source: hosted + version: "9.3.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_parsing: + dependency: transitive + description: + name: path_parsing + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + 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: "149441ca6e4f38193b2e004c0ca6376a3d11f51fa5a77552d8bd4d2b0c0912ba" + url: "https://pub.dev" + source: hosted + version: "2.2.23" + 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" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + riverpod: + dependency: transitive + description: + name: riverpod + sha256: "59062512288d3056b2321804332a13ffdd1bf16df70dcc8e506e411280a72959" + url: "https://pub.dev" + source: hosted + version: "2.6.1" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" + source: hosted + version: "1.10.2" + 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" + 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" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + uuid: + dependency: transitive + description: + name: uuid + sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" + url: "https://pub.dev" + source: hosted + version: "4.5.3" + vector_graphics: + dependency: transitive + description: + name: vector_graphics + sha256: "7076216a10d5c390315fbe536a30f1254c341e7543e6c4c8a815e591307772b1" + url: "https://pub.dev" + source: hosted + version: "1.1.20" + vector_graphics_codec: + dependency: transitive + description: + name: vector_graphics_codec + sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" + url: "https://pub.dev" + source: hosted + version: "1.1.13" + vector_graphics_compiler: + dependency: transitive + description: + name: vector_graphics_compiler + sha256: "5a88dd14c0954a5398af544651c7fb51b457a2a556949bfb25369b210ef73a74" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + 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" + wakelock_plus: + dependency: "direct main" + description: + name: wakelock_plus + sha256: "8b12256f616346910c519a35606fb69b1fe0737c06b6a447c6df43888b097f39" + url: "https://pub.dev" + source: hosted + version: "1.5.1" + wakelock_plus_platform_interface: + dependency: transitive + description: + name: wakelock_plus_platform_interface + sha256: "24b84143787220a403491c2e5de0877fbbb87baf3f0b18a2a988973863db4b03" + url: "https://pub.dev" + source: hosted + version: "1.4.0" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + 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..3c82b5f --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,74 @@ +name: jamshalat_masjid_screen +description: Smart Digital Prayer Clock for Android TV Box +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: ^2.6.1 + + # Local storage + hive_flutter: ^1.1.0 + + # HTTP + http: ^1.2.0 + + # Date/Time formatting + intl: ^0.20.0 + hugeicons: ^1.1.5 + file_picker: ^8.1.4 + + # Keep screen awake (critical for 24/7 TV) + wakelock_plus: ^1.2.8 + + # Google Fonts (Plus Jakarta Sans, Manrope) + google_fonts: ^6.2.1 + + # Audio for Adzan chime + audioplayers: ^6.1.0 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^5.0.0 + +flutter: + uses-material-design: true + + assets: + - assets/images/ + - assets/sounds/ + + fonts: + - family: Plus Jakarta Sans + fonts: + - asset: assets/fonts/PlusJakartaSans-Regular.ttf + - 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: Manrope + fonts: + - asset: assets/fonts/Manrope-ExtraLight.ttf + weight: 200 + - asset: assets/fonts/Manrope-Light.ttf + weight: 300 + - asset: assets/fonts/Manrope-Regular.ttf + - asset: assets/fonts/Manrope-Medium.ttf + weight: 500 + - asset: assets/fonts/Manrope-SemiBold.ttf + weight: 600 + - asset: assets/fonts/Manrope-Bold.ttf + weight: 700 + - asset: assets/fonts/Manrope-ExtraBold.ttf + weight: 800 diff --git a/stitch/adzan_alert_state/code.html b/stitch/adzan_alert_state/code.html new file mode 100644 index 0000000..f3c9dd6 --- /dev/null +++ b/stitch/adzan_alert_state/code.html @@ -0,0 +1,197 @@ + + + + + +Masjid Al-Mustafa - Adzan Alert + + + + + + + + + +
+
+ +
+ +
+
+

Masjid Al-Mustafa

+

Jl. Merdeka No. 45, Jakarta

+
+
+
+15 Sya'ban 1445 H +Kamis, 15 Juni 2024 +
+
+
+ +
+ +
+
+ + notifications_active + +
+ +
+

+ WAKTU ADZAN ASHAR +

+
+ +
+
+ + 15:28 + +
+32 +WIB +
+
+
+ +

+ Telah masuk waktu shalat Ashar untuk wilayah DKI Jakarta dan sekitarnya. + Segera persiapkan diri menuju masjid. +

+
+ +
+ +
+
+Subuh +04:42 +
+
+Dzuhur +11:58 +
+ +
+Ashar +15:18 +
+
+Maghrib +17:54 +
+
+Isya +19:08 +
+
+Syuruq +05:58 +
+
+ +
+
+

+ Pemberitahuan: Luruskan dan Rapatkan Saf • Infaq Masjid: Bank Syariah 12345678 • Jadwal Shalat Akurat - Kemenag RI • Mohon nonaktifkan suara handphone anda • Bersihkan diri sebelum menghadap Sang Pencipta. +

+
+
+
+ +
+mosque +
+ \ No newline at end of file diff --git a/stitch/adzan_alert_state/screen.png b/stitch/adzan_alert_state/screen.png new file mode 100644 index 0000000..9489805 Binary files /dev/null and b/stitch/adzan_alert_state/screen.png differ diff --git a/stitch/friday_khutbah_information_screen/code.html b/stitch/friday_khutbah_information_screen/code.html new file mode 100644 index 0000000..9217de1 --- /dev/null +++ b/stitch/friday_khutbah_information_screen/code.html @@ -0,0 +1,218 @@ + + + + + +Friday Khutbah Info - Al-Masjid Al-Haram + + + + + + + + + +
+
+
+mosque +

Al-Masjid Al-Haram

+
+
+123 Sacred Way, Holy City +Jumu'ah Mubarak +
+
+
+ +
+ +
+
12:45
+
Friday, 12 July 2024
+
+ +
+
+ +
+Special Service +

KHUTBAH
JUMAT

+
+ +
+ +
+Khatib +

KH. Ahmad Dahlan

+
+ +
+Imam +

Ust. Mansur

+
+
+ +
+
+volunteer_activism +
+
+

Siapkan Infaq Terbaik Anda

+

Every contribution supports the house of Allah.

+
+
+
+ +
+
+ +
+
+timer +

Iqamah in 15:00

+
+
+
+
+
+ + + +
+
+
+ + Assalamu Alaikum: Please silence your mobile phones before prayer begins. Friday Khutbah starts at 1:00 PM.    •    + Assalamu Alaikum: Please silence your mobile phones before prayer begins. Friday Khutbah starts at 1:00 PM.    •    + + + Assalamu Alaikum: Please silence your mobile phones before prayer begins. Friday Khutbah starts at 1:00 PM.    •    + Assalamu Alaikum: Please silence your mobile phones before prayer begins. Friday Khutbah starts at 1:00 PM.    •    + +
+
+
+ \ No newline at end of file diff --git a/stitch/friday_khutbah_information_screen/screen.png b/stitch/friday_khutbah_information_screen/screen.png new file mode 100644 index 0000000..9ddac13 Binary files /dev/null and b/stitch/friday_khutbah_information_screen/screen.png differ diff --git a/stitch/full_admin_settings_panel/code.html b/stitch/full_admin_settings_panel/code.html new file mode 100644 index 0000000..a834030 --- /dev/null +++ b/stitch/full_admin_settings_panel/code.html @@ -0,0 +1,413 @@ + + + + + +The Sacred Horizon | Admin Panel + + + + + + + + + + + +
+
+The Sacred Horizon +
+System Configuration +
+
+
+ + + +
+ +
+
+ +
+ +
+ +
+
+
+
+id_card +

Identitas Masjid

+
+
+
+ + +
+
+ + +
+
+
+
+
+
+distance +

Konfigurasi Lokasi

+
+
+
+
+

City ID API

+

1218

+

Yogyakarta, ID

+
+
+

Timezone

+

WIB

+

Asia/Jakarta

+
+
+

Coordinates

+

-7.7956° S

+

110.3695° E

+
+
+
+search + +
+
+
+ +
+
+
+
+cloud_sync +

Sinkronisasi Data

+
+
+
+
+

Sync Period

+

Oct - Nov 2024

+
+
+

Active

+

2 days ago

+
+
+
+
+
+
+

Automated monthly data updates ensure your prayer timings remain perfectly aligned with local astronomical data.

+
+
+
+ +
+
+ +
+
+
+timer +

Konfigurasi Waktu & Jeda Shalat

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Nama ShalatPre-Adzan Lead (min)Iqomah DurationBlank Screen (min)Status
Subuh +
+ +Minutes +
+
OPTIMIZED
DzuhurSTANDARD
AsharSTANDARD
MaghribFAST-TRACK
IsyaSTANDARD
+
+
+
+ +
+
+
+auto_awesome +

Konfigurasi Tampilan (Noor Green)

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

Noor Green Accent

+

Primary theme color active across all displays.

+
+
+
+
+
+
+
+
+
+system_update_alt +

Auto-Update System

+
+
+
+
+

Auto Online Calibration

+

Keep time-drift within <1ms

+
+ +
+
+
+

Beta Features

+

Early access to new visuals

+
+ +
+
+
+
+
+
+

Version Status

+

v4.2.0-stable

+
+ + + UP TO DATE + +
+ +
+
+
+
+
+ + + \ No newline at end of file diff --git a/stitch/full_admin_settings_panel/screen.png b/stitch/full_admin_settings_panel/screen.png new file mode 100644 index 0000000..4f6f2f2 Binary files /dev/null and b/stitch/full_admin_settings_panel/screen.png differ diff --git a/stitch/iqomah_countdown_screen/code.html b/stitch/iqomah_countdown_screen/code.html new file mode 100644 index 0000000..bc8d2f6 --- /dev/null +++ b/stitch/iqomah_countdown_screen/code.html @@ -0,0 +1,235 @@ + + + + + + + + + + + + + + +
+
+
+ +
+ +
+
+

+ MASJID AL-MUSTAFA +

+

The Celestial Anchor

+
+
+
+15:42 +Kamis, 24 Oktober 2024 +
+
+21 Rabi'ul Akhir 1446 H +Waktu Indonesia Barat +
+
+volume_up +settings +
+
+
+ +
+ +
+Shalat Saat Ini +
+

ASHAR

+
+
+ +
+ +
+

+ MENUJU IQOMAH +

+
+
+ 04:59 +
+
+ +
+notifications_active +SIAPKAN DIRI ANDA +
+
+ +
+
+

+ "Luruskan dan Rapatkan Shaf, Sesungguhnya lurusnya shaf termasuk kesempurnaan shalat." +

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

Subuh

+

04:12

+
+
+
+
+light_mode +
+
+

Dzuhur

+

11:45

+
+
+
+
+dark_mode +
+
+

Ashar (Sekarang)

+

15:02

+
+
+
+
+wb_twilight +
+
+

Maghrib

+

17:54

+
+
+
+ +
+
+
+
+campaign +Kajian Rutin Ba'da Maghrib: Memahami Kitab Riyadhus Shalihin bersama Ust. Ahmad Fauzi +
+
+mosque +Jadwal Petugas Jumu'ah: Khotib - KH. Hasan Basri, Imam - Ust. Rizky Ramadhan +
+
+favorite +Infaq Pembangunan Tahap II: Silahkan salurkan melalui Rekening BSI (451) 7123456789 +
+
+
+
+
+
+ \ No newline at end of file diff --git a/stitch/iqomah_countdown_screen/screen.png b/stitch/iqomah_countdown_screen/screen.png new file mode 100644 index 0000000..9e6a304 Binary files /dev/null and b/stitch/iqomah_countdown_screen/screen.png differ diff --git a/stitch/main_screen_friday_jumat_state/code.html b/stitch/main_screen_friday_jumat_state/code.html new file mode 100644 index 0000000..e034b24 --- /dev/null +++ b/stitch/main_screen_friday_jumat_state/code.html @@ -0,0 +1,250 @@ + + + + + + + + + + + + + + +
+ +
+
+ +
+
+Al-Masjid Al-Haram +
+
+
+mosque +123 Sacred Way, Holy City +
+
+
+ +
+ +
+
+
+ + + + +PERSIAPAN JUMAT +
+

+ 11:42 +

+
+
+

Friday, 24 May 2024

+

16 Dhu al-Qi'dah 1445 AH

+
+
+
+
+ +
+
+
+
+Next Event +auto_awesome +
+

MENUJU KHUTBAH

+
+
+
+
+person_pin +
+
+

Khatib Today

+

Sheikh Abdur-Rahman Al-Sudais

+
+
+
+
+timer +
+
+

Khutbah Starts In

+

~ 18 Minutes

+
+
+
+
+
+
+ + + +
+
+
+ + JUMAT MUBARAK: Luruskan dan rapatkan shaf. Harap non-aktifkan alat komunikasi. + + + Assalamu Alaikum: Please silence your mobile phones before prayer begins. Friday Khutbah starts at 1:00 PM. + + + JUMAT MUBARAK: Luruskan dan rapatkan shaf. Harap non-aktifkan alat komunikasi. + + + Assalamu Alaikum: Please silence your mobile phones before prayer begins. Friday Khutbah starts at 1:00 PM. + +
+ + +
+
+ \ No newline at end of file diff --git a/stitch/main_screen_friday_jumat_state/screen.png b/stitch/main_screen_friday_jumat_state/screen.png new file mode 100644 index 0000000..6087d25 Binary files /dev/null and b/stitch/main_screen_friday_jumat_state/screen.png differ diff --git a/stitch/main_screen_normal_view/code.html b/stitch/main_screen_normal_view/code.html new file mode 100644 index 0000000..aa020fb --- /dev/null +++ b/stitch/main_screen_normal_view/code.html @@ -0,0 +1,243 @@ + + + + + +Masjid Al-Mustafa - Digital Prayer Clock + + + + + + + + + +
+ +
+
+ +
+
+

Masjid Al-Mustafa

+
+location_on +Jl. Merdeka No. 45, Jakarta +
+
+
+
+

12 Ramadhan 1445 H

+

22 Maret 2024

+
+
+mosque +
+
+
+ +
+ +
+
+ + + + +Menuju Adzan Ashar: 15 menit 23 detik +
+
+

+ 14:45:28 +

+
+
+
+
+Terbit +05:58 +
+
+
+Imsak +04:32 +
+
+
+Dhuha +06:24 +
+
+
+
+ +
+
+ +
+Subuh +
+04:42 +Iqamah 04:52 +
+
+ +
+Terbit +
+05:58 +- +
+
+ +
+Dzuhur +
+12:04 +Iqamah 12:14 +
+
+ +
+
+Ashar +notifications_active +
+
+15:18 +Iqamah 15:28 +
+
+ +
+Maghrib +
+18:08 +Iqamah 18:18 +
+
+ +
+Isya +
+19:17 +Iqamah 19:27 +
+
+
+
+ +
+
+
+
+info +Pemberitahuan: Luruskan dan Rapatkan Saf Shalat Berjamaah +
+
+volunteer_activism +Infaq Masjid: Bank Syariah 12345678 a.n. Masjid Al-Mustafa +
+
+campaign +Jadwal Shalat Akurat - Kemenag RI +
+ +
+info +Pemberitahuan: Luruskan dan Rapatkan Saf Shalat Berjamaah +
+
+
+
+ +
+ \ No newline at end of file diff --git a/stitch/main_screen_normal_view/screen.png b/stitch/main_screen_normal_view/screen.png new file mode 100644 index 0000000..05dbffe Binary files /dev/null and b/stitch/main_screen_normal_view/screen.png differ diff --git a/stitch/noor_digital_horizon/DESIGN.md b/stitch/noor_digital_horizon/DESIGN.md new file mode 100644 index 0000000..5cf0bdf --- /dev/null +++ b/stitch/noor_digital_horizon/DESIGN.md @@ -0,0 +1,88 @@ +```markdown +# Design System Document: The Sacred Horizon + +## 1. Overview & Creative North Star +**Creative North Star: "The Celestial Anchor"** + +This design system moves away from the "utility-first" clutter of traditional digital clocks. Instead, it adopts a High-End Editorial approach that treats the 1920x1080 canvas as a grand architectural space. The goal is to create a sense of calm, reverence, and absolute clarity. + +By leveraging **intentional asymmetry**, we avoid the rigid, "Excel-sheet" look of standard prayer displays. The layout utilizes "breathing room" (negative space) to draw the eye toward the most critical information—the current time and the upcoming prayer—while secondary data remains elegantly nested in the periphery. This is a system of depth, light, and silence. + +--- + +## 2. Colors +The palette is rooted in the "Masjid Twilight"—a sophisticated blend of deep forest tones and luminous metallic accents. + +### Color Tokens +- **Primary (The Living Green):** `#88d982` (Active state/Current prayer) +- **Secondary (The Sacred Gold):** `#e9c349` (Special alerts/Jumu'ah/Highlights) +- **Background (The Void):** `#131313` (The base canvas) +- **Surface Hierarchy:** + - `surface_container_low`: `#1c1b1b` (Section backgrounds) + - `surface_container_highest`: `#353534` (Elevated prayer cards) + +### The "No-Line" Rule +Traditional 1px borders are strictly prohibited. Sectioning must be achieved through **background color shifts**. For example, a sidebar for "Upcoming Events" should be defined by a shift from `surface` to `surface_container_low`, rather than a stroke. This ensures the UI feels like a single, cohesive piece of carved stone rather than a digital assembly. + +### The "Glass & Gradient" Rule +To add "soul" to the dark theme, use subtle radial gradients on the main clock background (transitioning from `surface` to `primary_container` at 5% opacity). For slideshow overlays, use `surface_variant` at 60% opacity with a `24px` backdrop blur to create a premium "frosted glass" effect. + +--- + +## 3. Typography +We utilize a pairing of **Plus Jakarta Sans** for authority and **Manrope** for functional elegance. + +- **The Clock (display-lg):** Plus Jakarta Sans, `3.5rem`. This is the heartbeat of the system. It must be bold and legible from 20 feet away. +- **Prayer Names (headline-lg):** Plus Jakarta Sans, `2rem`. Used for the "Current Prayer" title to command respect. +- **Timings (title-lg):** Manrope, `1.375rem`. Clean, rhythmic numerals that prioritize quick scanning. +- **Labels (label-md):** Manrope, `0.75rem`. Used for secondary metadata (e.g., "Iqamah," "Sunrise"). + +**Editorial Hint:** Use `letter-spacing: -0.02em` on Display styles to give the typography a custom, "tight" high-fashion feel. + +--- + +## 4. Elevation & Depth +Depth is not communicated via shadows, but through **Tonal Layering**. + +- **The Layering Principle:** + 1. Base Layer: `surface` (The deep background). + 2. Content Zones: `surface_container_low` (Large zones for prayer lists). + 3. Interactive/Active Elements: `surface_container_highest` (The "active" prayer card). +- **The "Ghost Border" Fallback:** If high-sunlight environments require more definition, use the `outline_variant` token at **15% opacity**. Never use 100% opaque lines. +- **Ambient Glow:** Instead of a drop shadow, use a subtle outer glow for the active prayer card using the `primary` color at 10% opacity and a `40px` blur. This simulates the "Nur" (light) of the current time. + +--- + +## 5. Components + +### Prayer Time Cards +- **Structure:** Use `Roundedness Scale: xl (0.75rem)`. +- **Styling:** Inactive cards use `surface_container_low`. The active card transitions to `primary_container` with `on_primary_container` text. +- **Spacing:** Use `Spacing Scale: 6 (2rem)` for internal padding to ensure the text has room to breathe. + +### Slideshow Overlays (Hadith/Announcements) +- **Styling:** Floating containers using Glassmorphism. +- **Backdrop:** `surface_variant` @ 70% opacity + `backdrop-filter: blur(12px)`. +- **Layout:** Positioned asymmetrically (e.g., bottom-left) to allow the background imagery to remain visible. + +### Status Indicators (Adhan/Iqamah Countdown) +- **Styling:** Use a "Pill" shape (`Roundedness: full`). +- **Colors:** Pulsing `secondary` (Gold) for the 10-minute countdown to Iqamah. This creates urgency without using "Error" reds, which can feel aggressive in a prayer space. + +### Lists & Tables +- **Rule:** Forbid divider lines. +- **Execution:** Use `Spacing Scale: 4 (1.4rem)` as a vertical gutter between list items. Use the `surface_container_lowest` for alternating "Zebra" stripes only if the list exceeds 7 items. + +--- + +## 6. Do's and Don'ts + +### Do: +- **Prioritize the Clock:** The current time should always be the highest contrast element on the screen. +- **Use "Golden" Accents:** Reserve the `secondary` (Gold) color for the most sacred or time-sensitive information (e.g., Jumu'ah time). +- **Embrace Wide Margins:** Treat the 1080p screen like a gallery wall. Keep a minimum margin of `Spacing: 12 (4rem)` from the screen edges. + +### Don't: +- **Avoid Pure White:** Never use `#FFFFFF`. Use `on_surface` (`#e5e2e1`) for text to prevent "light bleed" and eye strain on large TV monitors. +- **No Sharp Corners:** Avoid the `none` or `sm` roundedness tokens. This system should feel organic and welcoming, not clinical. +- **No Center-Alignment Obsession:** Use left-aligned blocks for prayer times to create a sophisticated, asymmetric editorial grid. \ No newline at end of file diff --git a/stitch/shalat_black_screen_updated/code.html b/stitch/shalat_black_screen_updated/code.html new file mode 100644 index 0000000..afaa360 --- /dev/null +++ b/stitch/shalat_black_screen_updated/code.html @@ -0,0 +1,127 @@ + + + + + +The Sacred Horizon - Shalat State + + + + + + + + + + + + + + + + +
+ +
+ +
+

+ ASHAR +

+
+ +
+

+ Shalat Sedang Berlangsung +

+
+
+ +
+
+phonelink_ring + + Mohon Nonaktifkan Alat Komunikasi + +
+
+ + +
+
+
+
+ + + \ No newline at end of file diff --git a/stitch/shalat_black_screen_updated/screen.png b/stitch/shalat_black_screen_updated/screen.png new file mode 100644 index 0000000..98dc072 Binary files /dev/null and b/stitch/shalat_black_screen_updated/screen.png differ diff --git a/stitch/slideshow_screen_rotation/code.html b/stitch/slideshow_screen_rotation/code.html new file mode 100644 index 0000000..c198867 --- /dev/null +++ b/stitch/slideshow_screen_rotation/code.html @@ -0,0 +1,176 @@ + + + + + +Masjid Al-Mustafa - Digital Signage + + + + + + + + + +
+Islamic infographic + +
+
+ +
+
+

Masjid Al-Mustafa

+

Jl. Merdeka No. 45, Jakarta

+
+ +
+
+14:50 +WIB Jakarta +
+
+
+schedule +Ashar +
+
+
+10:23 +
+
+
+
+ + +
+ +
+

Keutamaan Sedekah

+

+ "Sedekah itu dapat menghapus dosa sebagaimana air memadamkan api." +

+
+
+volunteer_activism +
+Infaq Hari Ini: Rp 4.500.000 +
+
+
+ +
+
+ +
+campaign +INFO +
+ +
+
+Pemberitahuan: Luruskan dan Rapatkan Saf + +Infaq Masjid: Bank Syariah 12345678 + +Kajian Rutin: Setiap Selasa Ba'da Maghrib + +Jadwal Shalat Akurat - Kemenag RI + +Mohon Matikan Ponsel Selama Di Dalam Masjid +
+
+ + +
+
+ +
+ \ No newline at end of file diff --git a/stitch/slideshow_screen_rotation/screen.png b/stitch/slideshow_screen_rotation/screen.png new file mode 100644 index 0000000..2b24454 Binary files /dev/null and b/stitch/slideshow_screen_rotation/screen.png differ diff --git a/test/widget_test.dart b/test/widget_test.dart new file mode 100644 index 0000000..4955133 --- /dev/null +++ b/test/widget_test.dart @@ -0,0 +1,40 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:jamshalat_masjid_screen/core/enums.dart'; +import 'package:jamshalat_masjid_screen/data/local/models.dart'; + +void main() { + group('PrayerName display labels', () { + test('uses Jumat label for Friday dzuhur', () { + expect( + PrayerName.dzuhur.displayLabel(isFriday: true), + 'JUMAT', + ); + expect( + PrayerName.dzuhur.displayLabel(isFriday: false), + 'Dzuhur', + ); + }); + + test('marks fardhu prayers correctly', () { + expect(PrayerName.subuh.isFardhu, isTrue); + expect(PrayerName.imsak.isFardhu, isFalse); + }); + }); + + group('AppSettings copyWith', () { + test('preserves defaults and overrides selected fields', () { + final settings = AppSettings(); + final updated = settings.copyWith( + masjidName: 'Masjid Raya', + cityIdApi: '1301', + iqomahDzuhur: 12, + ); + + expect(updated.masjidName, 'Masjid Raya'); + expect(updated.cityIdApi, '1301'); + expect(updated.iqomahDzuhur, 12); + expect(updated.masjidAddress, settings.masjidAddress); + expect(updated.runningTexts, settings.runningTexts); + }); + }); +} 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..3b7c541 --- /dev/null +++ b/web/index.html @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + jamshalat_masjid_screen + + + + + + + diff --git a/web/manifest.json b/web/manifest.json new file mode 100644 index 0000000..42a3fc8 --- /dev/null +++ b/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "jamshalat_masjid_screen", + "short_name": "jamshalat_masjid_screen", + "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" + } + ] +} diff --git a/windows/.gitignore b/windows/.gitignore new file mode 100644 index 0000000..d492d0d --- /dev/null +++ b/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt new file mode 100644 index 0000000..5d049d2 --- /dev/null +++ b/windows/CMakeLists.txt @@ -0,0 +1,108 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(jamshalat_masjid_screen LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "jamshalat_masjid_screen") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(VERSION 3.14...3.25) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/windows/flutter/CMakeLists.txt b/windows/flutter/CMakeLists.txt new file mode 100644 index 0000000..903f489 --- /dev/null +++ b/windows/flutter/CMakeLists.txt @@ -0,0 +1,109 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + ${FLUTTER_TARGET_PLATFORM} $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..09e8e2c --- /dev/null +++ b/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,14 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include + +void RegisterPlugins(flutter::PluginRegistry* registry) { + AudioplayersWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("AudioplayersWindowsPlugin")); +} diff --git a/windows/flutter/generated_plugin_registrant.h b/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..dc139d8 --- /dev/null +++ b/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake new file mode 100644 index 0000000..375535c --- /dev/null +++ b/windows/flutter/generated_plugins.cmake @@ -0,0 +1,24 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + audioplayers_windows +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/windows/runner/CMakeLists.txt b/windows/runner/CMakeLists.txt new file mode 100644 index 0000000..394917c --- /dev/null +++ b/windows/runner/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/windows/runner/Runner.rc b/windows/runner/Runner.rc new file mode 100644 index 0000000..923354f --- /dev/null +++ b/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.jamshalat" "\0" + VALUE "FileDescription", "jamshalat_masjid_screen" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "jamshalat_masjid_screen" "\0" + VALUE "LegalCopyright", "Copyright (C) 2026 com.jamshalat. All rights reserved." "\0" + VALUE "OriginalFilename", "jamshalat_masjid_screen.exe" "\0" + VALUE "ProductName", "jamshalat_masjid_screen" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/windows/runner/flutter_window.cpp b/windows/runner/flutter_window.cpp new file mode 100644 index 0000000..955ee30 --- /dev/null +++ b/windows/runner/flutter_window.cpp @@ -0,0 +1,71 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { + this->Show(); + }); + + // Flutter can complete the first frame before the "show window" callback is + // registered. The following call ensures a frame is pending to ensure the + // window is shown. It is a no-op if the first frame hasn't completed yet. + flutter_controller_->ForceRedraw(); + + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/windows/runner/flutter_window.h b/windows/runner/flutter_window.h new file mode 100644 index 0000000..6da0652 --- /dev/null +++ b/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/windows/runner/main.cpp b/windows/runner/main.cpp new file mode 100644 index 0000000..c6ce34d --- /dev/null +++ b/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.Create(L"jamshalat_masjid_screen", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/windows/runner/resource.h b/windows/runner/resource.h new file mode 100644 index 0000000..66a65d1 --- /dev/null +++ b/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/windows/runner/resources/app_icon.ico b/windows/runner/resources/app_icon.ico new file mode 100644 index 0000000..c04e20c Binary files /dev/null and b/windows/runner/resources/app_icon.ico differ diff --git a/windows/runner/runner.exe.manifest b/windows/runner/runner.exe.manifest new file mode 100644 index 0000000..153653e --- /dev/null +++ b/windows/runner/runner.exe.manifest @@ -0,0 +1,14 @@ + + + + + PerMonitorV2 + + + + + + + + + diff --git a/windows/runner/utils.cpp b/windows/runner/utils.cpp new file mode 100644 index 0000000..3a0b465 --- /dev/null +++ b/windows/runner/utils.cpp @@ -0,0 +1,65 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + unsigned int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr) + -1; // remove the trailing null character + int input_length = (int)wcslen(utf16_string); + std::string utf8_string; + if (target_length == 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + input_length, utf8_string.data(), target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/windows/runner/utils.h b/windows/runner/utils.h new file mode 100644 index 0000000..3879d54 --- /dev/null +++ b/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/windows/runner/win32_window.cpp b/windows/runner/win32_window.cpp new file mode 100644 index 0000000..60608d0 --- /dev/null +++ b/windows/runner/win32_window.cpp @@ -0,0 +1,288 @@ +#include "win32_window.h" + +#include +#include + +#include "resource.h" + +namespace { + +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +/// Registry key for app theme preference. +/// +/// A value of 0 indicates apps should use dark mode. A non-zero or missing +/// value indicates apps should use light mode. +constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; +constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + } + FreeLibrary(user32_module); +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registrar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::Create(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + UpdateTheme(window); + + return OnCreate(); +} + +bool Win32Window::Show() { + return ShowWindow(window_handle_, SW_SHOWNORMAL); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} + +void Win32Window::UpdateTheme(HWND const window) { + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, + RRF_RT_REG_DWORD, nullptr, &light_mode, + &light_mode_size); + + if (result == ERROR_SUCCESS) { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/windows/runner/win32_window.h b/windows/runner/win32_window.h new file mode 100644 index 0000000..e901dde --- /dev/null +++ b/windows/runner/win32_window.h @@ -0,0 +1,102 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates a win32 window with |title| that is positioned and sized using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring& title, const Point& origin, const Size& size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_