|
|
2 months ago | |
|---|---|---|
| android | 2 months ago | |
| coverage | 2 months ago | |
| lib | 2 months ago | |
| macos | 2 months ago | |
| scripts | 2 months ago | |
| test | 2 months ago | |
| .env.example | 2 months ago | |
| .gitignore | 2 months ago | |
| .metadata | 2 months ago | |
| README.md | 2 months ago | |
| pubspec.lock | 2 months ago | |
| pubspec.yaml | 2 months ago | |
README.md
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
- Home (
lib/ui/home/home_screen.dart) - Displays local storage items and cached content - Recipes (
lib/ui/recipes/recipes_screen.dart) - Recipe collection with full CRUD operations - Favourites (
lib/ui/favourites/favourites_screen.dart) - Favorite recipes - 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
IndexedStackfor tab navigation (preserves state) - Uses
MaterialPageRoutefor 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:
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
# 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 classlib/data/local/models/item.dart- Item data modeltest/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 classlib/data/immich/models/immich_asset.dart- Asset modellib/data/immich/models/upload_response.dart- Upload response modeltest/data/immich/immich_service_test.dart- Unit tests
Key Methods: uploadImage(), fetchAssets(), getCachedAsset(), getCachedAssets()
Recipe Management
Full-featured recipe management system with offline storage and Nostr synchronization. Users can create, edit, and delete recipes with rich metadata including images, tags, ratings, and favourites.
Features
- Create Recipes: Add recipes with title, description, tags, rating (0-5 stars), favourite flag, and multiple images
- Edit Recipes: Update existing recipes with all fields
- Delete Recipes: Soft-delete recipes (marked as deleted, can be recovered)
- Image Upload: Automatic upload to Immich service with URL retrieval
- Offline-First: All recipes stored locally in SQLite database
- Nostr Sync: Recipes automatically published to Nostr as kind 30078 events
- Tag Support: Multiple tags per recipe for organization
- Rating System: 0-5 star rating for recipes
- Favourites: Mark recipes as favourites for quick access
Data Model
RecipeModel (lib/data/recipes/models/recipe_model.dart):
id: Unique identifiertitle: Recipe title (required)description: Optional descriptiontags: List of tagsrating: 0-5 star ratingisFavourite: Boolean favourite flagimageUrls: List of image URLs (from Immich)createdAt/updatedAt: TimestampsisDeleted: Soft-delete flagnostrEventId: Nostr event ID if synced
Service
RecipeService (lib/data/recipes/recipe_service.dart):
createRecipe(): Create a new recipeupdateRecipe(): Update an existing recipedeleteRecipe(): Soft-delete a recipegetRecipe(): Get a recipe by IDgetAllRecipes(): Get all recipes (with optional includeDeleted flag)getRecipesByTag(): Filter recipes by taggetFavouriteRecipes(): Get all favourite recipes
UI Screens
AddRecipeScreen (lib/ui/add_recipe/add_recipe_screen.dart):
- Form with all recipe fields
- Image picker with multi-image support
- Automatic Immich upload integration
- Validation (title required)
- Edit mode support (pass existing recipe)
RecipesScreen (lib/ui/recipes/recipes_screen.dart):
- List view with recipe cards
- Pull-to-refresh
- Edit and delete actions
- Empty state handling
- Image display with error handling
Nostr Integration
Recipes are published to Nostr as kind 30078 events (replaceable events) with the following structure:
Event Tags:
["d", "<recipe-id>"]- Replaceable event identifier["image", "<immich-url>"]- One tag per image URL["t", "<tag>"]- One tag per recipe tag["rating", "<0-5>"]- Recipe rating["favourite", "<true/false>"]- Favourite flag
Event Content: JSON object with all recipe fields:
{
"id": "recipe-123",
"title": "Recipe Title",
"description": "Recipe description",
"tags": ["tag1", "tag2"],
"rating": 4,
"isFavourite": true,
"imageUrls": ["https://..."],
"createdAt": 1234567890,
"updatedAt": 1234567890
}
Deletion: When a recipe is deleted, a kind 5 event is published referencing the original recipe event ID.
Database Schema
Recipes are stored in a SQLite database (recipes.db) with the following schema:
CREATE TABLE recipes (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
description TEXT,
tags TEXT NOT NULL, -- JSON array
rating INTEGER NOT NULL DEFAULT 0,
is_favourite INTEGER NOT NULL DEFAULT 0,
image_urls TEXT NOT NULL, -- JSON array
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
is_deleted INTEGER NOT NULL DEFAULT 0,
nostr_event_id TEXT
);
Usage
Creating a Recipe:
- Navigate to Add Recipe screen (tap + button in bottom nav)
- Fill in title (required), description, tags, rating, favourite flag
- Select images from gallery
- Images are automatically uploaded to Immich
- Tap "Save" to create recipe
- Recipe is saved locally and published to Nostr (if logged in with nsec)
Editing a Recipe:
- Open Recipes screen
- Tap on a recipe card or tap Edit icon
- Modify fields as needed
- Tap "Update" to save changes
- Changes are synced to Nostr
Deleting a Recipe:
- Open Recipes screen
- Tap Delete icon on a recipe card
- Confirm deletion
- Recipe is soft-deleted (marked as deleted)
- Deletion event is published to Nostr
Testing
Run recipe service tests:
flutter test test/data/recipes/recipe_service_test.dart
Tests cover:
- CRUD operations
- Tag filtering
- Favourite filtering
- Soft-delete functionality
- Error handling
Files
lib/data/recipes/models/recipe_model.dart- Recipe data modellib/data/recipes/recipe_service.dart- Recipe service with CRUD and Nostr synclib/ui/add_recipe/add_recipe_screen.dart- Add/Edit recipe formlib/ui/recipes/recipes_screen.dart- Recipe list displaytest/data/recipes/recipe_service_test.dart- Unit tests
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 classlib/data/nostr/models/nostr_keypair.dart- Keypair modellib/data/nostr/models/nostr_event.dart- Event modellib/data/nostr/models/nostr_relay.dart- Relay modeltest/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 classlib/data/sync/models/sync_status.dart- Status and priority enumslib/data/sync/models/sync_operation.dart- Operation modeltest/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 screenlib/ui/relay_management/relay_management_controller.dart- State management controllertest/ui/relay_management/relay_management_screen_test.dart- UI teststest/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 servicelib/data/session/models/user.dart- User modeltest/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 servicelib/data/firebase/models/firebase_config.dart- Firebase configuration modeltest/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 navlib/ui/navigation/app_router.dart- Router with route guards and route generationlib/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 screentest/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:
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
-
Create
.env.examplefile in the project root with the following template:# 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 -
Copy
.env.exampleto.envand fill in your actual values:cp .env.example .env -
Edit
.envwith 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:
.envfile (recommended) - Loaded at runtime, falls back to defaults if not found--dart-define- For environment selection:
# 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 URLprod- 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:
# Make sure APP_NAME is set in your .env file first
./scripts/set_app_name.sh
This script will:
- Read
APP_NAMEfrom your.envfile - Update
android/app/src/main/AndroidManifest.xml - 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 pxmipmap-hdpi/ic_launcher.png- 72x72 pxmipmap-xhdpi/ic_launcher.png- 96x96 pxmipmap-xxhdpi/ic_launcher.png- 144x144 pxmipmap-xxxhdpi/ic_launcher.png- 192x192 px
Instructions:
- Create your app icon as a square image (recommended: 1024x1024 px source)
- Generate all required sizes using an icon generator tool (e.g., App Icon Generator)
- Replace the existing
ic_launcher.pngfiles in eachmipmap-*directory - The icon is referenced in
AndroidManifest.xmlas@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 pxapp_icon_32.png- 32x32 pxapp_icon_64.png- 64x64 pxapp_icon_128.png- 128x128 pxapp_icon_256.png- 256x256 pxapp_icon_512.png- 512x512 pxapp_icon_1024.png- 1024x1024 px
Instructions:
- Create your app icon as a square image (recommended: 1024x1024 px source)
- Generate all required sizes
- Replace the existing PNG files in
AppIcon.appiconset/directory - The
Contents.jsonfile 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 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 - Online tool, supports multiple platforms
- IconKitchen - Google's icon generator
- MakeAppIcon - Generates all sizes from one image
- Xcode (for macOS/iOS) - Built-in asset catalog editor
Quick Setup
- Prepare your icon: Create a 1024x1024 px square PNG image
- Generate sizes: Use one of the tools above to generate all required sizes
- Replace files: Copy generated icons to the appropriate directories
- 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).
- Start emulator from Android Studio AVD Manager
- Wait for boot animation to complete and home screen appears
- Run:
flutter run
iOS Simulator (macOS)
open -a Simulator
flutter run
Running Tests
# 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