A local-first film photography organizer that runs entirely in your browser. No cloud, no subscription, no tracking — your photos and metadata stay on your device.
- Local-first: All data stored in browser — your photos never leave your device
- Dual-layer storage: LocalStorage for metadata, Cache API + IndexedDB for thumbnails
- Bulk import: Import entire folders of film scans at once
- Bulk metadata: Add film stock, camera, and subject to multiple photos
- Smart export: Export renamed files based on metadata
- EXIF support: Read and write EXIF data automatically
- Offline-first: Works without internet connection
- Progressive loading: Photos show instantly, thumbnails load in background
- Web Worker thumbnails: Thumbnail generation off main thread for responsiveness
- Node.js 18+
- A modern browser (Chrome, Firefox, Safari, Edge)
cd fotoflo
npm install
npm run devnpm run build # Production build
npm run preview # Preview build
npm run test # Run testsWorks on Vercel, Netlify, or any static host:
git push origin main
# Import in Vercel/Netlify┌─────────────────────────────────────────────────────┐
│ FotoFlo App │
├─────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Sidebar │ │ PhotoGrid │ │ Toolbar │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │
│ └──────────────┼──────────────┘ │
│ │ │
│ ┌───────▼────────┐ │
│ │ +page.svelte │ │
│ │ (coordination)│ │
│ └───────┬────────┘ │
│ │ │
│ ┌───────▼────────┐ │
│ │ fotoflo.svelte.ts │ │
│ │ (store) │ │
│ └───────┬────────┘ │
│ │ │
│ ┌─────────────────┼─────────────────┐ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌────────┐ ┌────────────┐ ┌────────────────┐ │
│ │LocalStorage│ │ IndexedDB │ │ Cache API │ │
│ │(metadata) │ │(blobs) │ │ (thumbnails)│ │
│ └────────┘ └────────────┘ └────────────────┘ │
│ │
└─────────────────────────────────────────────────────┘
LocalStorage (5MB limit, sync)
- Photo metadata
- Settings
IndexedDB (large storage, async)
- File handles (for re-export)
- Fallback thumbnail storage
- Blob data
Cache API (primary thumbnail storage)
- Native blob support
- Better quota management
- Faster than IndexedDB for blobs
src/
├── lib/
│ ├── stores/
│ │ ├── fotoflo.svelte.ts # Main store (~750 lines)
│ │ └── fotoflo.test.ts # Store tests
│ ├── workers/
│ │ └── thumbnail.worker.ts # Off-thread thumbnail gen
│ ├── utils/
│ │ └── exif.ts # EXIF read/write
│ ├── import.ts # Unified import module
│ └── types.ts # Centralized types
├── components/
│ ├── PhotoGrid.svelte # Grid display
│ ├── Toolbar.svelte # Action toolbar
│ ├── Sidebar.svelte # Filters
│ ├── Viewer.svelte # Single photo view
│ ├── ImportModal.svelte # Import dialog
│ └── BulkMetaModal.svelte # Bulk edit dialog
└── routes/
└── +page.svelte # Main app page
User Selects Files/Folder
│
▼
┌─────────────────┐
│ import.ts │ ← Validate, EXIF extract, deduplicate
└────────┬────────┘
│
▼
┌─────────────────────┐
│ fotoflo.svelte.ts │ ← Store photos, generate thumbnails
└────────┬────────────┘
│
┌────┴────┬────────────┐
▼ ▼ ▼
LocalStorage IndexedDB Cache API
(metadata) (handles) (thumbnails)
| Layer | Technology | Purpose |
|---|---|---|
| Framework | Svelte 5 + SvelteKit | UI + routing |
| State | Svelte 5 $state() | Reactive state |
| Storage | LocalStorage + IndexedDB + Cache API | Persistence |
| EXIF | exifreader + piexifjs | Metadata |
| UI | Custom CSS + NeoDialog | Glass-morphism |
| Testing | Vitest | Unit tests |
| Build | Vite | Bundler |
-
Add to
FotoFloStateintypes.ts:filterNewField: string | null;
-
Add setter in
fotoflo.svelte.ts:function setFilterNewField(value: string | null) { state.filterNewField = value; }
-
Add to
getFilteredPhotos():if (state.filterNewField) { result = result.filter(p => p.field === state.filterNewField); }
-
Export from store return object
-
Add to
Photointerface intypes.ts:newField?: string;
-
Update metadata modal
-
Update EXIF export if needed
npm run test # Run all tests
npm run test:watch # Watch modeTests cover:
- Store logic (filtering, sorting, collections)
- Import module (deduplication, ID generation)
- Type validation
- No analytics, tracking, or cookies
- All data stays local
- No server communication
MIT
Built with ❤️ using Svelte 5