Files
deardiary/todo/gallery.md
lotherk 0bdd71a4ed feat: v0.1.0 - geolocation capture, calendar, search, Starlight docs site
- 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
2026-03-27 02:27:55 +00:00

16 KiB
Raw Permalink Blame History

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

/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

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

┌─────────────────────────────────────────────────────┐
│  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

  • 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

// 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