You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
gitea c99774e42b
better search design
1 month ago
android 1st name change 2 months ago
coverage Phase 5 - Relay Management UI complete 2 months ago
lib better search design 1 month ago
macos mp4 suppor added 2 months ago
scripts app name and icons 2 2 months ago
test better tag search 2 months ago
.env.example blossom media server support added 2 months ago
.gitignore Phase 4 - synch engine complete 2 months ago
.metadata phase 0 2 months ago
README.md blossom media server support added 2 months ago
pubspec.lock mp4 suppor added 2 months ago
pubspec.yaml mp4 suppor added 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

  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 with full CRUD operations
  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:

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 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()

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 30000 events (NIP-33 parameterized replaceable 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 identifier
  • title: Recipe title (required)
  • description: Optional description
  • tags: List of tags
  • rating: 0-5 star rating
  • isFavourite: Boolean favourite flag
  • imageUrls: List of image URLs (from Immich)
  • createdAt / updatedAt: Timestamps
  • isDeleted: Soft-delete flag
  • nostrEventId: Nostr event ID if synced

Service

RecipeService (lib/data/recipes/recipe_service.dart):

  • createRecipe(): Create a new recipe
  • updateRecipe(): Update an existing recipe
  • deleteRecipe(): Soft-delete a recipe
  • getRecipe(): Get a recipe by ID
  • getAllRecipes(): Get all recipes (with optional includeDeleted flag)
  • getRecipesByTag(): Filter recipes by tag
  • getFavouriteRecipes(): 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 30000 events (NIP-33 parameterized 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:

  1. Navigate to Add Recipe screen (tap + button in bottom nav)
  2. Fill in title (required), description, tags, rating, favourite flag
  3. Select images from gallery
  4. Images are automatically uploaded to Immich
  5. Tap "Save" to create recipe
  6. Recipe is saved locally and published to Nostr (if logged in with nsec)

Editing a Recipe:

  1. Open Recipes screen
  2. Tap on a recipe card or tap Edit icon
  3. Modify fields as needed
  4. Tap "Update" to save changes
  5. Changes are synced to Nostr

Deleting a Recipe:

  1. Open Recipes screen
  2. Tap Delete icon on a recipe card
  3. Confirm deletion
  4. Recipe is soft-deleted (marked as deleted)
  5. 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 model
  • lib/data/recipes/recipe_service.dart - Recipe service with CRUD and Nostr sync
  • lib/ui/add_recipe/add_recipe_screen.dart - Add/Edit recipe form
  • lib/ui/recipes/recipes_screen.dart - Recipe list display
  • test/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 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:

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:

    # 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
    
    # Immich Enable/Disable
    # Set to 'false' to disable Immich and only use Blossom media server
    # Default: true
    IMMICH_ENABLE=true
    
    # Blossom Media Server Configuration
    # Default Blossom server URL (used when Immich is disabled or as default)
    BLOSSOM_SERVER=https://media.based21.com
    
    # 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
    
  2. Copy .env.example to .env and fill in your actual values:

    cp .env.example .env
    
  3. 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:
# 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:

# 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)
  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 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

  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)

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

Powered by TurnKey Linux.