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
This commit is contained in:
537
todo/gallery.md
Normal file
537
todo/gallery.md
Normal file
@@ -0,0 +1,537 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user