# App Boilerplate A modular, offline-first Flutter boilerplate for apps that store, sync, and share media and metadata across centralized (Immich, Firebase) and decentralized (Nostr) systems. ## Navigation & UI Scaffold The app uses a custom bottom navigation bar with 4 main tabs and a centered Add Recipe button: ### Main Navigation Tabs 1. **Home** (`lib/ui/home/home_screen.dart`) - Displays local storage items and cached content 2. **Recipes** (`lib/ui/recipes/recipes_screen.dart`) - Recipe collection (ready for LocalStorageService integration) 3. **Favourites** (`lib/ui/favourites/favourites_screen.dart`) - Favorite recipes 4. **User/Session** (`lib/ui/session/session_screen.dart`) - User session management and login ### Add Recipe Button - **Add Recipe** - Centered button in the bottom navigation bar - Opens full-screen Add Recipe form (`lib/ui/add_recipe/add_recipe_screen.dart`) - Positioned in the center of the bottom navigation bar (between Recipes and Favourites) - Ready for ImmichService integration for image uploads ### Settings & Relay Management - **Settings Icon** (cog) - Appears in the top-right AppBar of all main screens - Tapping it navigates to **Relay Management** screen (`lib/ui/relay_management/`) - Accessible from any screen for global relay configuration ### Navigation Architecture - **MainNavigationScaffold** (`lib/ui/navigation/main_navigation_scaffold.dart`) - Main app shell with bottom nav - **AppRouter** (`lib/ui/navigation/app_router.dart`) - Named route management for full-screen navigation - **PrimaryAppBar** (`lib/ui/shared/primary_app_bar.dart`) - Shared AppBar widget with settings icon - Uses `IndexedStack` for tab navigation (preserves state) - Uses `MaterialPageRoute` for full-screen navigation (Add Recipe, Relay Management) ### Screen Structure ``` lib/ui/ ├── home/ # Home screen ├── recipes/ # Recipes screen ├── add_recipe/ # Add Recipe screen ├── favourites/ # Favourites screen ├── session/ # User/Session screen ├── relay_management/ # Relay Management (accessible via settings icon) ├── shared/ # Shared UI components (PrimaryAppBar) ├── navigation/ # Navigation components └── _legacy/ # Archived screens (Immich, Nostr Events, Settings) ``` ### Testing Run navigation tests: ```bash flutter test test/ui/navigation/main_navigation_scaffold_test.dart ``` Tests verify: - Bottom navigation bar displays correctly - Tab switching works - Add Recipe button (centered in bottom nav) opens Add Recipe screen - Settings icon appears in all AppBars - Settings icon navigates to Relay Management ## Quick Start ```bash # Install dependencies flutter pub get # Run tests flutter test # Run app flutter run ``` ## Local Storage & Caching Service for local storage and caching operations. Provides CRUD operations for items stored in SQLite, image caching with automatic download/storage, and cache hit/miss handling. Modular design with no UI dependencies. **Files:** - `lib/data/local/local_storage_service.dart` - Main service class - `lib/data/local/models/item.dart` - Item data model - `test/data/local/local_storage_service_test.dart` - Unit tests **Key Methods:** `initialize()`, `insertItem()`, `getItem()`, `getAllItems()`, `updateItem()`, `deleteItem()`, `getCachedImage()`, `clearImageCache()`, `close()` ## Immich Integration Service for interacting with Immich API. Uploads images, fetches asset lists, and automatically stores metadata in local database. Offline-first design allows access to cached metadata without network. **Configuration:** Edit `lib/config/config_loader.dart` to set `immichBaseUrl` and `immichApiKey` for dev/prod environments (lines 40-41 for dev, lines 47-48 for prod). **Files:** - `lib/data/immich/immich_service.dart` - Main service class - `lib/data/immich/models/immich_asset.dart` - Asset model - `lib/data/immich/models/upload_response.dart` - Upload response model - `test/data/immich/immich_service_test.dart` - Unit tests **Key Methods:** `uploadImage()`, `fetchAssets()`, `getCachedAsset()`, `getCachedAssets()` ## Nostr Integration Service for decentralized metadata synchronization using Nostr protocol. Generates keypairs, publishes events, and syncs metadata across multiple relays. Modular design allows testing without real relay connections. **Files:** - `lib/data/nostr/nostr_service.dart` - Main service class - `lib/data/nostr/models/nostr_keypair.dart` - Keypair model - `lib/data/nostr/models/nostr_event.dart` - Event model - `lib/data/nostr/models/nostr_relay.dart` - Relay model - `test/data/nostr/nostr_service_test.dart` - Unit tests **Key Methods:** `generateKeyPair()`, `addRelay()`, `connectRelay()`, `publishEvent()`, `syncMetadata()`, `dispose()` ## Sync Engine Engine for coordinating data synchronization between local storage, Immich, and Nostr. Handles conflict resolution, offline queuing, and automatic retries. Processes operations by priority with configurable conflict resolution strategies. **Files:** - `lib/data/sync/sync_engine.dart` - Main sync engine class - `lib/data/sync/models/sync_status.dart` - Status and priority enums - `lib/data/sync/models/sync_operation.dart` - Operation model - `test/data/sync/sync_engine_test.dart` - Unit and integration tests **Key Methods:** `syncToImmich()`, `syncFromImmich()`, `syncToNostr()`, `syncAll()`, `queueOperation()`, `resolveConflict()`, `getPendingOperations()` **Conflict Resolution:** `useLocal`, `useRemote`, `useLatest`, `merge` - set via `setConflictResolution()` ## Relay Management UI User interface for managing Nostr relays. View configured relays, add/remove relays, monitor connection health, and trigger manual syncs. Modular design with controller-based state management for testability. **Files:** - `lib/ui/relay_management/relay_management_screen.dart` - Main UI screen - `lib/ui/relay_management/relay_management_controller.dart` - State management controller - `test/ui/relay_management/relay_management_screen_test.dart` - UI tests - `test/ui/relay_management/relay_management_controller_test.dart` - Controller tests **Key Features:** Add/remove relays, connect/disconnect, health monitoring, manual sync trigger, error handling **Usage:** Tap the settings (cog) icon in the top-right AppBar of any main screen to navigate to Relay Management. ## User Session Management Service for managing user sessions, login, logout, and session isolation. Provides per-user data isolation with separate storage paths and cache directories. Clears cached data on logout and integrates with local storage and sync engine. **Files:** - `lib/data/session/session_service.dart` - Main session management service - `lib/data/session/models/user.dart` - User model - `test/data/session/session_service_test.dart` - Unit tests **Key Methods:** `login()`, `logout()`, `switchSession()`, `getCurrentUserDbPath()`, `getCurrentUserCacheDir()` **Features:** Per-user storage isolation, cache clearing on logout, session switching with data preservation, integration with local storage **Usage:** Initialize `SessionService` with `LocalStorageService` and optional `SyncEngine`. Call `login()` to start a session, `logout()` to end it, and `switchSession()` to change users. ## Firebase Layer Optional Firebase integration providing cloud sync, storage, authentication, push notifications, and analytics. Fully modular - can be enabled or disabled without affecting offline-first functionality. Integrates with local storage and session management to maintain offline-first behavior. **Files:** - `lib/data/firebase/firebase_service.dart` - Main Firebase service - `lib/data/firebase/models/firebase_config.dart` - Firebase configuration model - `test/data/firebase/firebase_service_test.dart` - Unit tests **Key Methods:** `initialize()`, `loginWithEmailPassword()`, `logout()`, `syncItemsToFirestore()`, `syncItemsFromFirestore()`, `uploadFile()`, `getFcmToken()`, `logEvent()` **Features:** Firestore cloud sync, Firebase Storage for media, Firebase Auth for authentication, Firebase Cloud Messaging for push notifications, Firebase Analytics for analytics, all optional and modular **Usage:** Create `FirebaseService` with `FirebaseConfig` (disabled by default). Pass to `SessionService` for automatic sync on login/logout. Initialize Firebase with `initialize()` before use. All services gracefully handle being disabled. **Note:** Firebase requires actual Firebase project setup with `google-services.json` (Android) and `GoogleService-Info.plist` (iOS) configuration files. The service handles missing configuration gracefully and maintains offline-first behavior. ## Navigation & UI Scaffold Complete navigation structure connecting all main modules with bottom navigation bar. Includes route guards for protected screens and placeholder screens ready for custom UI implementation. **Files:** - `lib/ui/navigation/main_navigation_scaffold.dart` - Main navigation scaffold with bottom nav - `lib/ui/navigation/app_router.dart` - Router with route guards and route generation - `lib/ui/home/home_screen.dart` - Home screen (local storage items) - `lib/ui/immich/immich_screen.dart` - Immich media screen (placeholder) - `lib/ui/nostr_events/nostr_events_screen.dart` - Nostr events screen (placeholder) - `lib/ui/session/session_screen.dart` - Session management (login/logout) - `lib/ui/settings/settings_screen.dart` - Settings screen - `test/ui/navigation/main_navigation_scaffold_test.dart` - Navigation tests **Navigation Structure:** - **Home** - Local storage and cached content (no auth required) - **Immich** - Immich media integration (requires login) - **Nostr Events** - Nostr events display (requires login) - **Session** - User login/logout (no auth required) - **Settings** - App settings and Relay Management access (no auth required) **Route Guards:** Immich and Nostr Events screens require authentication. Unauthenticated users see a login prompt with option to navigate to Session screen. **Usage:** The app automatically uses `MainNavigationScaffold` after initialization. All services are passed to the scaffold for dependency injection. Customize placeholder screens by editing the respective screen files in `lib/ui/`. **Running UI Tests:** ```bash flutter test test/ui/navigation/main_navigation_scaffold_test.dart ``` ## Configuration **Configuration uses `.env` files for sensitive values (API keys, URLs) with fallback defaults in `lib/config/config_loader.dart`.** ### Setup .env File 1. Create `.env.example` file in the project root with the following template: ```bash # Application Configuration APP_NAME=app_boilerplate # Immich Configuration IMMICH_BASE_URL=https://photos.satoshinakamoto.win IMMICH_API_KEY_DEV=your-dev-api-key-here IMMICH_API_KEY_PROD=your-prod-api-key-here # Nostr Relays (comma-separated list) NOSTR_RELAYS_DEV=wss://nostrum.satoshinakamoto.win,wss://nos.lol NOSTR_RELAYS_PROD=wss://relay.damus.io # API Configuration API_BASE_URL_DEV=https://api-dev.example.com API_BASE_URL_PROD=https://api.example.com # Logging ENABLE_LOGGING_DEV=true ENABLE_LOGGING_PROD=false # Firebase Configuration (optional) FIREBASE_ENABLED=false FIREBASE_FIRESTORE_ENABLED=true FIREBASE_STORAGE_ENABLED=true FIREBASE_AUTH_ENABLED=true FIREBASE_MESSAGING_ENABLED=true FIREBASE_ANALYTICS_ENABLED=true ``` 3. Copy `.env.example` to `.env` and fill in your actual values: ```bash cp .env.example .env ``` 4. Edit `.env` with your actual configuration values. **Important:** The `.env` file is in `.gitignore` and should never be committed to version control. Only commit `.env.example` as a template. ### Environment Variables The app uses: - **`.env` file** (recommended) - Loaded at runtime, falls back to defaults if not found - **`--dart-define`** - For environment selection: ```bash # Set environment at runtime flutter run --dart-define=ENV=prod ``` If `.env` file is not found or variables are missing, the app uses default values from `lib/config/config_loader.dart`. ### Available Environments - `dev` - Development (default): Logging enabled, dev API URL - `prod` - Production: Logging disabled, production API URL ## App Name Configuration # 1. Set APP_NAME in your .env file # 2. Run the script ./scripts/set_app_name.sh To make this easier, you can use the provided script that reads `APP_NAME` from `.env` and automatically updates all platform files: ```bash # Make sure APP_NAME is set in your .env file first ./scripts/set_app_name.sh ``` This script will: 1. Read `APP_NAME` from your `.env` file 2. Update `android/app/src/main/AndroidManifest.xml` 3. Update `macos/Runner/Configs/AppInfo.xcconfig` **Manual Setup:** If you prefer to set it manually, just update the files listed above directly. ## App Icon Configuration App icons are platform-specific and must be placed in the correct directories with the correct formats. ### Android Icons **Location:** `android/app/src/main/res/mipmap-*/` **Format:** PNG files **Required Sizes:** - `mipmap-mdpi/ic_launcher.png` - 48x48 px - `mipmap-hdpi/ic_launcher.png` - 72x72 px - `mipmap-xhdpi/ic_launcher.png` - 96x96 px - `mipmap-xxhdpi/ic_launcher.png` - 144x144 px - `mipmap-xxxhdpi/ic_launcher.png` - 192x192 px **Instructions:** 1. Create your app icon as a square image (recommended: 1024x1024 px source) 2. Generate all required sizes using an icon generator tool (e.g., [App Icon Generator](https://www.appicon.co/)) 3. Replace the existing `ic_launcher.png` files in each `mipmap-*` directory 4. The icon is referenced in `AndroidManifest.xml` as `@mipmap/ic_launcher` **Best Practices:** - Use PNG format (no transparency for launcher icons on some Android versions) - Keep icon simple and recognizable at small sizes - Follow Material Design guidelines for Android icons - Ensure icon works on both light and dark backgrounds ### macOS Icons **Location:** `macos/Runner/Assets.xcassets/AppIcon.appiconset/` **Format:** PNG files **Required Sizes:** - `app_icon_16.png` - 16x16 px - `app_icon_32.png` - 32x32 px - `app_icon_64.png` - 64x64 px - `app_icon_128.png` - 128x128 px - `app_icon_256.png` - 256x256 px - `app_icon_512.png` - 512x512 px - `app_icon_1024.png` - 1024x1024 px **Instructions:** 1. Create your app icon as a square image (recommended: 1024x1024 px source) 2. Generate all required sizes 3. Replace the existing PNG files in `AppIcon.appiconset/` directory 4. The `Contents.json` file defines which sizes map to which files - update if needed **Best Practices:** - Use PNG format - macOS icons can have transparency - Follow macOS Human Interface Guidelines - Icon should be recognizable at 16x16 size ### iOS Icons (if adding iOS support) **Location:** `ios/Runner/Assets.xcassets/AppIcon.appiconset/` **Format:** PNG files (no transparency for some sizes) **Required Sizes:** iOS requires many sizes. Use Xcode's App Icon set or a tool like [App Icon Generator](https://www.appicon.co/) to generate all required sizes automatically. **Best Practices:** - Use PNG format - Some sizes require no transparency (check iOS guidelines) - Follow iOS Human Interface Guidelines - Generate all sizes from a 1024x1024 px source ### Icon Generation Tools Recommended tools for generating all required icon sizes: - [AppIcon.co](https://www.appicon.co/) - Online tool, supports multiple platforms - [IconKitchen](https://icon.kitchen/) - Google's icon generator - [MakeAppIcon](https://makeappicon.com/) - Generates all sizes from one image - Xcode (for macOS/iOS) - Built-in asset catalog editor ### Quick Setup 1. **Prepare your icon:** Create a 1024x1024 px square PNG image 2. **Generate sizes:** Use one of the tools above to generate all required sizes 3. **Replace files:** Copy generated icons to the appropriate directories 4. **Test:** Run the app and verify icons appear correctly ## Running the App ### Android Emulator **Important:** Wait for emulator to fully boot (30-60 seconds on cold boot). 1. Start emulator from Android Studio AVD Manager 2. Wait for boot animation to complete and home screen appears 3. Run: `flutter run` ### iOS Simulator (macOS) ```bash open -a Simulator flutter run ``` ## Running Tests ```bash # Run all tests flutter test # Run with coverage flutter test --coverage ``` **Important:** Tests must be run separately - `flutter run` does not execute tests. ## Project Structure ``` lib/ ├── config/ │ ├── app_config.dart │ └── config_loader.dart ├── data/ │ ├── local/ │ │ ├── local_storage_service.dart │ │ └── models/ │ │ └── item.dart │ ├── immich/ │ │ ├── immich_service.dart │ │ └── models/ │ │ ├── immich_asset.dart │ │ └── upload_response.dart │ ├── nostr/ │ │ ├── nostr_service.dart │ │ └── models/ │ │ ├── nostr_keypair.dart │ │ ├── nostr_event.dart │ │ └── nostr_relay.dart │ ├── session/ │ │ ├── session_service.dart │ │ └── models/ │ │ └── user.dart │ ├── firebase/ │ │ ├── firebase_service.dart │ │ └── models/ │ │ └── firebase_config.dart │ └── sync/ │ ├── sync_engine.dart │ └── models/ │ ├── sync_status.dart │ └── sync_operation.dart ├── ui/ │ ├── navigation/ │ │ ├── main_navigation_scaffold.dart │ │ └── app_router.dart │ ├── home/ │ │ └── home_screen.dart │ ├── immich/ │ │ └── immich_screen.dart │ ├── nostr_events/ │ │ └── nostr_events_screen.dart │ ├── session/ │ │ └── session_screen.dart │ ├── settings/ │ │ └── settings_screen.dart │ └── relay_management/ │ ├── relay_management_screen.dart │ └── relay_management_controller.dart └── main.dart test/ ├── config/ │ └── config_loader_test.dart └── data/ ├── local/ │ └── local_storage_service_test.dart ├── immich/ │ └── immich_service_test.dart ├── nostr/ │ └── nostr_service_test.dart ├── session/ │ └── session_service_test.dart ├── firebase/ │ └── firebase_service_test.dart ├── sync/ │ └── sync_engine_test.dart └── ui/ ├── navigation/ │ └── main_navigation_scaffold_test.dart └── relay_management/ ├── relay_management_screen_test.dart └── relay_management_controller_test.dart ```