- Automatic browser geolocation capture on event creation - Reverse geocoding via Nominatim API for place names - Full-text search with SQLite FTS5 - Calendar view for browsing past entries - DateNavigator component for day navigation - SearchModal with Ctrl+K shortcut - QuickAddWidget with Ctrl+J shortcut - Starlight documentation site with GitHub Pages deployment - Multiple AI provider support (Groq, OpenAI, Anthropic, Ollama, LM Studio) - Multi-user registration support BREAKING: Events now include latitude/longitude/placeName fields
16 KiB
16 KiB
Media Gallery Feature for DearDiary
Overview
This document outlines the comprehensive feature specification for adding media gallery capabilities to DearDiary - an AI-powered daily journaling app. The gallery will allow users to view, manage, and organize all their photos and voice recordings in one centralized location.
Feature Description
Core Functionality
-
Media Grid View
- Responsive masonry or grid layout displaying all media items
- Support for both photos and voice recordings
- Visual distinction between media types (photo thumbnails vs audio waveform icons)
- Lazy loading for performance with large collections
- Infinite scroll pagination
-
Audio Player
- Inline audio player for voice recordings
- Play/pause controls
- Progress bar with seek functionality
- Duration display
- Waveform visualization (optional enhancement)
-
Filtering & Organization
- Filter by media type (photos, voice recordings, all)
- Filter by date range (today, this week, this month, custom range)
- Search by date
- Sort by date (newest/oldest)
-
Thumbnail Generation
- Automatic thumbnail generation for photos
- Multiple sizes: small (150px), medium (300px), large (600px)
- Server-side processing using sharp or similar library
- Cache thumbnails for performance
-
Lightbox View
- Full-screen photo viewer
- Navigation between photos (previous/next)
- Zoom functionality
- Photo metadata display (EXIF data)
- Close on escape key or click outside
-
Storage Management
- Display total storage used
- Show individual file sizes
- Bulk delete functionality
- Delete confirmation dialogs
- Storage quota warnings
-
Media Metadata
- EXIF data extraction for photos (camera, date taken, location, etc.)
- Audio duration and format info
- File size and dimensions
- Creation date
-
Privacy Considerations
- All media stored locally or on user's own object storage
- No third-party media processing
- Optional encryption at rest
- Clear deletion removes all related data
Storage Architecture
Option 1: Local File Storage (Recommended for Self-Hosted)
/data/media/
/{userId}/
/{date}/
/photos/
{eventId}.jpg # Original
{eventId}_thumb.jpg # Thumbnail
{eventId}_medium.jpg # Medium size
/voice/
{eventId}.webm # Voice recording
Pros:
- Simple to implement
- No additional costs
- Full control over data
- Easy backup
Cons:
- Requires server-side file management
- No CDN for fast delivery
- Storage limited to server capacity
Implementation:
- Use
sharplibrary for image processing - Generate thumbnails on upload or on-demand
- Serve via static file middleware
Option 2: Object Storage (S3-compatible)
Pros:
- Scalable storage
- Built-in CDN integration
- High availability
- Offload bandwidth
Cons:
- Additional complexity and cost
- Requires S3-compatible service
- More complex authentication
Recommendation: Start with local storage for v1, design for S3 compatibility in v2.
Database Schema Changes
New Media Model
model Media {
id String @id @default(uuid())
userId String
eventId String?
type String // "photo" | "voice"
fileName String
originalName String
mimeType String
fileSize Int // bytes
duration Int? // seconds (for audio)
width Int? // pixels
height Int? // pixels
thumbnailPath String?
mediumPath String?
exifData String? // JSON string
storageType String @default("local")
storageKey String? // S3 key if using object storage
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
event Event? @relation(fields: [eventId], references: [id], onDelete: SetNull)
@@index([userId, type])
@@index([userId, createdAt])
@@index([eventId])
}
Updated Event Model
model Event {
id String @id @default(uuid())
userId String
date String
type String // "event" | "text" | "photo" | "voice" | "health"
content String
metadata String?
mediaId String? // Link to Media
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
media Media? @relation(fields: [mediaId], references: [id], onDelete: SetNull)
@@index([userId, date])
@@index([date])
@@index([type])
}
Storage Statistics (Optional)
model StorageStats {
userId String @id
totalBytes Int @default(0)
photoBytes Int @default(0)
voiceBytes Int @default(0)
photoCount Int @default(0)
voiceCount Int @default(0)
lastUpdated DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
API Endpoints Needed
Media Management
// Upload media (photo or voice)
POST /api/v1/media/upload
Body: multipart/form-data
- file: File
- type: "photo" | "voice"
- date: string (YYYY-MM-DD)
- eventId?: string (optional link to existing event)
- metadata?: object (optional additional metadata)
// Get media item by ID
GET /api/v1/media/:id
// Delete media item
DELETE /api/v1/media/:id
// Bulk delete media
POST /api/v1/media/bulk-delete
Body: { ids: string[] }
// Get media thumbnail
GET /api/v1/media/:id/thumbnail/:size
- size: "small" | "medium" | "large"
// Stream media file
GET /api/v1/media/:id/stream
// Get EXIF data
GET /api/v1/media/:id/exif
// Get storage usage stats
GET /api/v1/media/stats
// List all media with filters
GET /api/v1/media
Query params:
- type?: "photo" | "voice" | "all"
- startDate?: string (YYYY-MM-DD)
- endDate?: string (YYYY-MM-DD)
- page?: number
- limit?: number
- sort?: "newest" | "oldest"
Integration with Existing Events
// Create event with media
POST /api/v1/events
Body: {
date, type, content, metadata,
media: { type, file }
}
// Add media to existing event
POST /api/v1/events/:id/media
Body: multipart/form-data with file
// Get event with media
GET /api/v1/events/:id
Response includes linked media
UI/UX Design Patterns
Gallery Page Layout
┌─────────────────────────────────────────────────────┐
│ DearDiary [Settings] │
├─────────────────────────────────────────────────────┤
│ [Dashboard] [Today] [History] [Diary] [Gallery] │
├─────────────────────────────────────────────────────┤
│ ┌─────────────────────────────────────────────┐ │
│ │ 📷 Photos 🎤 Voice | Jan 2026 ▼ │ │
│ │ Total: 2.3 GB | 156 photos | 42 voice │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │
│ │ │ │ │ │ │ │ │ │ │ │
│ │ 📷 │ │ 📷 │ │ 🎤 │ │ 📷 │ │ 📷 │ │
│ │ │ │ │ │ │ │ │ │ │ │
│ └──────┘ └──────┘ └──────┘ └──────┘ └──────┘ │
│ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │
│ │ │ │ │ │ │ │ │ │ │ │
│ │ 📷 │ │ 🎤 │ │ 📷 │ │ 📷 │ │ 🎤 │ │
│ │ │ │ │ │ │ │ │ │ │ │
│ └──────┘ └──────┘ └──────┘ └──────┘ └──────┘ │
│ │
│ [Load More...] │
└─────────────────────────────────────────────────────┘
Filter Bar Components
- Type Toggle: Segmented button control (All | Photos | Voice)
- Date Picker: Month/Year dropdown with custom range option
- Search: Date input or quick filters (Today, This Week, This Month, Year)
Photo Card Component
┌─────────────────┐
│ │
│ [Thumbnail] │
│ │
├─────────────────┤
│ Jan 15, 2026 │
│ 2.4 MB • 4032×3024 │
└─────────────────┘
Voice Card Component
┌─────────────────────────────┐
│ 🎤 Voice Recording │
│ ━━━━━━━━━●━━━━━━━━━━━━━ │
│ 0:45 / 2:30 │
│ Jan 15, 2026 • 1.2 MB │
└─────────────────────────────┘
Lightbox Component
┌─────────────────────────────────────────────────────┐
│ ← │ 📷 Photo Title │ ⋮ │ → │
│ │ │ │
│ │ │ │
│ │ [Full Size Image] │ │
│ │ │ │
│ │ │ │
├─────┴──────────────────────────┴────────────────┤
│ 📅 Jan 15, 2026 📷 4032×3024 📏 2.4 MB │
│ 📷 iPhone 15 Pro 📍 San Francisco │
└─────────────────────────────────────────────────────┘
Storage Management Panel (Settings or Modal)
┌─────────────────────────────────────────┐
│ Storage Usage │
│ │
│ ████████████████████░░░░░░░░░ 2.3/10GB│
│ │
│ 📷 Photos: 1.8 GB (156 files) │
│ 🎤 Voice: 500 MB (42 files) │
│ │
│ [Delete Old Media] │
│ [Download All] │
│ [Clear All Media] │
└─────────────────────────────────────────┘
Design Tokens (Tailwind)
- Background:
bg-slate-900 - Cards:
bg-slate-800 - Borders:
border-slate-700 - Primary:
text-purple-400 - Muted:
text-slate-400 - Hover:
hover:bg-slate-700 - Active:
bg-purple-600
Implementation Complexity
Phase 1: Core Gallery (Estimated: 8-12 hours)
- Database schema updates (Media model)
- File upload endpoint with storage
- Media listing API with filtering
- Basic grid UI with thumbnails
- Type filtering UI
Phase 2: Media Viewing (Estimated: 6-8 hours)
- Thumbnail generation service
- Lightbox component
- Audio player component
- Photo navigation (prev/next)
- EXIF extraction service
Phase 3: Storage Management (Estimated: 4-6 hours)
- Storage stats calculation
- Delete single media
- Bulk delete functionality
- Storage visualization UI
- Delete confirmation dialogs
Phase 4: Advanced Features (Estimated: 8-10 hours)
- Date range filtering
- Infinite scroll pagination
- Search by date
- Object storage support (S3)
- Media encryption at rest
Total Estimated Time: 26-36 hours
Priority Recommendation
High Priority (Phase 1 + Phase 2)
- Target: MVP Gallery functionality
- Rationale: Core value proposition - users need to see their media
- Features:
- Grid view of media
- Basic type filtering
- Photo lightbox
- Audio playback
- Thumbnail generation
Medium Priority (Phase 3)
- Target: Storage management
- Rationale: Important for self-hosted users with limited storage
- Features:
- Storage usage display
- Delete functionality
- Bulk operations
Low Priority (Phase 4)
- Target: Advanced features
- Rationale: Nice-to-have enhancements
- Features:
- Object storage
- Date range filters
- Advanced search
Migration Strategy
Backward Compatibility
- Existing
Event.mediaPathfield can be migrated to newMediamodel - Create migration script to:
- Create Media records from existing mediaPath values
- Generate thumbnails for photos
- Update Event records to reference Media
- Deprecate (not remove) mediaPath field
Data Migration Script
// Pseudocode for migration
async function migrateMedia() {
const events = await prisma.event.findMany({
where: { mediaPath: { not: null } }
});
for (const event of events) {
// Create Media record
const media = await prisma.media.create({
data: {
userId: event.userId,
eventId: event.id,
type: event.type === 'voice' ? 'voice' : 'photo',
filePath: event.mediaPath,
// ... extract metadata
}
});
// Update event to link to media
await prisma.event.update({
where: { id: event.id },
data: { mediaId: media.id, mediaPath: null }
});
}
}
Testing Considerations
-
Unit Tests
- Thumbnail generation
- EXIF parsing
- Storage size calculation
- Date filtering logic
-
Integration Tests
- Upload flow
- Media listing with filters
- Delete operations
- Event-media linking
-
UI Tests
- Grid responsive layout
- Lightbox navigation
- Audio player controls
- Filter interactions
Performance Considerations
-
Thumbnail Caching
- Generate on upload, store in cache
- Serve static thumbnails directly
-
Pagination
- Limit initial load to 20-50 items
- Infinite scroll for more
-
Lazy Loading
- Use intersection observer for images
- Load thumbnails first, full-res on demand
-
Database Indexing
- Index on userId + type for filtering
- Index on userId + createdAt for sorting
- Index on eventId for event-media lookups
Security Considerations
-
File Validation
- Verify MIME types server-side
- Limit file sizes (max 50MB for photos, 20MB for audio)
- Sanitize filenames
-
Access Control
- Users can only access their own media
- API key authentication required for all media endpoints
-
Storage Security
- Store outside web root
- Optional encryption for sensitive data
- Secure file deletion (overwrite before unlink)
References
- Sharp - Image processing library
- EXIF Parser - EXIF data extraction
- Web Audio API - Audio playback
- React Player - Audio/video player React component