# 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 1. **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 2. **Audio Player** - Inline audio player for voice recordings - Play/pause controls - Progress bar with seek functionality - Duration display - Waveform visualization (optional enhancement) 3. **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) 4. **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 5. **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 6. **Storage Management** - Display total storage used - Show individual file sizes - Bulk delete functionality - Delete confirmation dialogs - Storage quota warnings 7. **Media Metadata** - EXIF data extraction for photos (camera, date taken, location, etc.) - Audio duration and format info - File size and dimensions - Creation date 8. **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 `sharp` library 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 ```prisma 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 ```prisma 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) ```prisma 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.mediaPath` field can be migrated to new `Media` model - Create migration script to: 1. Create Media records from existing mediaPath values 2. Generate thumbnails for photos 3. Update Event records to reference Media 4. Deprecate (not remove) mediaPath field ### Data Migration Script ```typescript // 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 1. **Unit Tests** - Thumbnail generation - EXIF parsing - Storage size calculation - Date filtering logic 2. **Integration Tests** - Upload flow - Media listing with filters - Delete operations - Event-media linking 3. **UI Tests** - Grid responsive layout - Lightbox navigation - Audio player controls - Filter interactions --- ## Performance Considerations 1. **Thumbnail Caching** - Generate on upload, store in cache - Serve static thumbnails directly 2. **Pagination** - Limit initial load to 20-50 items - Infinite scroll for more 3. **Lazy Loading** - Use intersection observer for images - Load thumbnails first, full-res on demand 4. **Database Indexing** - Index on userId + type for filtering - Index on userId + createdAt for sorting - Index on eventId for event-media lookups --- ## Security Considerations 1. **File Validation** - Verify MIME types server-side - Limit file sizes (max 50MB for photos, 20MB for audio) - Sanitize filenames 2. **Access Control** - Users can only access their own media - API key authentication required for all media endpoints 3. **Storage Security** - Store outside web root - Optional encryption for sensitive data - Secure file deletion (overwrite before unlink) --- ## References - [Sharp](https://sharp.pixelplumbing.com/) - Image processing library - [EXIF Parser](https://github.com/mifi/exiftool-vendored) - EXIF data extraction - [Web Audio API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API) - Audio playback - [React Player](https://github.com/cookpete/react-player) - Audio/video player React component