- 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
538 lines
16 KiB
Markdown
538 lines
16 KiB
Markdown
# 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
|