2a6087af27
Media playback now survives navigation. A GlobalMediaPlayer singleton owns VLC players and exposes StateFlows. Composables are thin viewports. NowPlayingBar has full controls (volume, mute, save, fullscreen). GlobalFullscreenOverlay renders video fullscreen above all screens. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
469 lines
24 KiB
Markdown
469 lines
24 KiB
Markdown
# Brainstorm: Desktop Media — Full Parity
|
|
|
|
**Date:** 2026-03-16
|
|
**Status:** Draft
|
|
**Branch:** TBD (`feat/desktop-media`)
|
|
|
|
## What We're Building
|
|
|
|
Full media functionality for Amethyst Desktop — display, upload, gallery, lightbox, video playback, encrypted media, and desktop-native UX. Feature parity with Android Amethyst's media stack, adapted for mouse-first desktop interaction.
|
|
|
|
### Scope
|
|
|
|
| Feature | Included | Notes |
|
|
|---------|----------|-------|
|
|
| Image display in notes | Yes | Coil3 AsyncImage, blurhash previews |
|
|
| Video playback | Yes | VLCJ with bundled libvlc |
|
|
| Blossom upload | Yes | Extract to commons, Blossom-only (no NIP-96) |
|
|
| Drag-drop / clipboard paste | Yes | Essential desktop UX from day 1 |
|
|
| Lightbox / zoom | Yes | Full-screen media viewer with zoom |
|
|
| Image gallery carousel | Yes | Multi-image posts |
|
|
| Profile gallery (NIP-68) | Yes | Kind 20 picture posts, create + view |
|
|
| Video events (NIP-71) | Yes | Kind 21/22 display |
|
|
| Encrypted media (NIP-17 DMs) | Yes | Full send + receive |
|
|
| Media server management | Yes | Blossom server list (kind 10063) |
|
|
| Audio/voice playback | Yes | MP3, OGG, FLAC, WAV |
|
|
| Media compression | Yes | Desktop-adapted (Java ImageIO) |
|
|
| Blurhash generation on upload | Yes | Already in commons/ |
|
|
| EXIF stripping | Yes | Privacy: strip metadata before upload |
|
|
| Alt text / accessibility | Yes | NIP-92 imeta alt field |
|
|
|
|
### Out of Scope (for now)
|
|
|
|
- NIP-96 upload (deprecated, Blossom replaces it)
|
|
- Voice recording (microphone capture — desktop-specific, complex)
|
|
- Picture-in-picture video
|
|
- Live streaming (NIP-53)
|
|
- Torrent/magnet distribution
|
|
|
|
## Why This Approach
|
|
|
|
### Blossom Only (No NIP-96)
|
|
|
|
NIP-96 is officially marked "unrecommended: replaced by blossom APIs" in the NIP registry. Building desktop from scratch gives us the opportunity to skip legacy protocol support entirely.
|
|
|
|
**Blossom advantages:**
|
|
- Content-addressed (SHA-256) — files portable across servers
|
|
- Native mirroring (BUD-04) — upload once, mirror to N servers
|
|
- Server-side optimization (BUD-05) — `/media` endpoint
|
|
- Payment support (BUD-07) — Cashu/Lightning
|
|
- Clean URI scheme (BUD-10) — `blossom:<hash>.<ext>`
|
|
|
|
### Extraction Strategy — Layered Architecture
|
|
|
|
Android's BlossomUploader is tightly coupled to Android (`Context`, `Uri`, `ContentResolver`). We need to separate the HTTP upload protocol from platform file access.
|
|
|
|
**Layer 1: commons/commonMain — Pure Blossom protocol client**
|
|
- `BlossomClient` — HTTP PUT /upload, /mirror, /media, DELETE, GET /list. Takes `InputStream` + metadata. Returns `BlobDescriptor`. No platform deps.
|
|
- `BlossomAuthHelper` — Creates kind 24242 auth events, base64-encodes for Authorization header
|
|
- `BlossomServerDiscovery` — Queries kind 10063, resolves server list, caches
|
|
- `MediaUploadResult` — Result data class (already platform-agnostic)
|
|
- `UploadOrchestrator` — Coordinates upload to server + optional optimization + mirroring
|
|
- `MultiUploadOrchestrator` — Manages parallel upload of multiple files
|
|
- `MediaUploadTracker` — StateFlow-based progress tracking
|
|
- `ServerHeadCache` — BUD-06 pre-flight response cache
|
|
|
|
**Layer 2: commons/commonMain — expect/actual for platform file operations**
|
|
```
|
|
expect fun readFileBytes(path: String): ByteArray
|
|
expect fun computeFileSha256(path: String): String
|
|
expect fun getMimeType(path: String): String?
|
|
expect fun getFileSize(path: String): Long
|
|
expect class MediaMetadataExtractor {
|
|
fun extractDimensions(path: String): Pair<Int, Int>?
|
|
fun extractBlurhash(path: String): String?
|
|
}
|
|
expect class MediaCompressor {
|
|
fun compressImage(path: String, quality: Float): String
|
|
fun stripExif(path: String): String
|
|
}
|
|
```
|
|
|
|
**Layer 3: Platform actuals**
|
|
- `androidMain/` — Uses `ContentResolver`, `Uri`, Android Bitmap, `MediaMetadataRetriever`
|
|
- `jvmMain/` — Uses `java.io.File`, Java ImageIO, `metadata-extractor`, BufferedImage→blurhash
|
|
|
|
**Layer 4: Platform UI (desktopApp/ and amethyst/)**
|
|
- File pickers, drag-drop handlers, video players, lightbox composables
|
|
|
|
This design lets any future client (iOS, web) reuse Layer 1 + 2 by providing Layer 3 actuals.
|
|
|
|
### Coil3 for Image Loading
|
|
|
|
Coil3 officially supports Compose Multiplatform including JVM/Desktop. Android Amethyst already uses Coil3. Benefits:
|
|
- Disk + memory caching
|
|
- Custom fetchers (Blossom URI, blurhash, base64)
|
|
- Crossfade animations
|
|
- SVG support
|
|
|
|
### VLCJ for Video
|
|
|
|
ExoPlayer is Android-only (Media3). VLCJ wraps VLC's libvlc via JNA — supports every format VLC does (mp4, webm, m3u8, mkv, etc.). Compose integration via `SwingPanel` or offscreen rendering. We bundle libvlc with the app (~100MB) for zero user setup.
|
|
|
|
## Key Decisions
|
|
|
|
| Decision | Choice | Rationale |
|
|
|----------|--------|-----------|
|
|
| Upload protocol | Blossom only | NIP-96 deprecated |
|
|
| Upload code location | commons/commonMain | Share with Android |
|
|
| Image loading | Coil3 | Already used on Android, KMP support |
|
|
| Video playback | VLCJ (bundled libvlc) | ExoPlayer is Android-only |
|
|
| Desktop input | Drag-drop + paste + file picker | Essential for desktop UX |
|
|
| Encrypted media | Full support | DM file sharing parity |
|
|
| Profile gallery | Yes (NIP-68 kind 20) | Create + view |
|
|
|
|
## Existing Codebase Audit
|
|
|
|
### Already Shared (commons/)
|
|
|
|
| Component | Location | Status |
|
|
|-----------|----------|--------|
|
|
| BlurHashDecoder/Encoder | `commons/blurhash/` | Ready |
|
|
| PlatformImage (expect/actual) | `commons/blurhash/PlatformImage.kt` | Ready (JVM uses BufferedImage) |
|
|
| BitmapUtils (JVM) | `commons/blurhash/BitmapUtils.jvm.kt` | Ready |
|
|
| Base64ImagePlatform (JVM) | `commons/base64Image/` | Ready |
|
|
| RichTextParser | `commons/richtext/RichTextParser.kt` | Ready (classifies URLs as image/video) |
|
|
| MediaContentModels | `commons/richtext/MediaContentModels.kt` | Ready (MediaUrlImage, MediaUrlVideo, etc.) |
|
|
| UrlParser | `commons/richtext/UrlParser.kt` | Ready |
|
|
| Image extensions list | RichTextParser companion | png, jpg, gif, bmp, jpeg, webp, svg, avif |
|
|
| Video extensions list | RichTextParser companion | mp4, avi, wmv, mpg, amv, webm, mov + audio |
|
|
|
|
### In Quartz (protocol layer, shared)
|
|
|
|
| Component | Location | Status |
|
|
|-----------|----------|--------|
|
|
| BlossomAuthorizationEvent | `quartz/nipB7Blossom/` | Ready (kind 24242) |
|
|
| BlossomServersEvent | `quartz/nipB7Blossom/` | Ready (kind 10063) |
|
|
| BlossomUri | `quartz/nipB7Blossom/` | Ready (blossom: URI parsing) |
|
|
| BlossomUploadResult | `quartz/nipB7Blossom/` | Ready |
|
|
| FileHeaderEvent (NIP-94) | `quartz/nip94FileMetadata/` | Ready (kind 1063) |
|
|
| BlurhashTag | `quartz/nip94FileMetadata/tags/` | Ready |
|
|
| DimensionTag | `quartz/nip94FileMetadata/tags/` | Ready |
|
|
| IMetaTag/Builder (NIP-92) | `quartz/nip92IMeta/` | Ready |
|
|
| PictureEvent (NIP-68) | `quartz/nip68Picture/` | Ready (kind 20) |
|
|
| VideoEvent (NIP-71) | `quartz/nip71Video/` | Ready (kind 21/22/34235/34236) |
|
|
| ProfileGalleryEntryEvent | `quartz/experimental/profileGallery/` | Ready |
|
|
| FileServersEvent (NIP-96) | `quartz/nip96FileStorage/` | Exists but we're skipping NIP-96 |
|
|
| ChatMessageEncryptedFileHeaderEvent | `quartz/nip17Dm/files/` | Ready (encrypted DM files) |
|
|
|
|
### Android-Only (needs extraction or desktop equivalent)
|
|
|
|
| Component | Location | Action |
|
|
|-----------|----------|--------|
|
|
| BlossomUploader | `amethyst/service/uploads/blossom/` | Extract HTTP logic to commons |
|
|
| Nip96Uploader | `amethyst/service/uploads/nip96/` | Skip (Blossom only) |
|
|
| UploadOrchestrator | `amethyst/service/uploads/` | Extract to commons |
|
|
| MultiOrchestrator | `amethyst/service/uploads/` | Extract to commons |
|
|
| MediaUploadResult | `amethyst/service/uploads/` | Extract (already platform-agnostic) |
|
|
| MediaCompressor | `amethyst/service/uploads/` | Desktop equivalent (Java ImageIO) |
|
|
| BlurhashMetadataCalculator | `amethyst/service/uploads/` | Desktop equivalent (Java ImageIO + commons blurhash) |
|
|
| BlossomServerResolver | `amethyst/service/uploads/blossom/bud10/` | Extract to commons |
|
|
| ServerHeadCache | `amethyst/service/uploads/blossom/bud10/` | Extract to commons |
|
|
| ImageLoaderSetup | `amethyst/service/images/` | Desktop Coil3 config |
|
|
| BlossomFetcher | `amethyst/service/images/` | Extract to commons (Coil3 fetcher for blossom: URIs) |
|
|
| BlurHashFetcher | `amethyst/service/images/` | Extract to commons (Coil3 fetcher for blurhash) |
|
|
| Base64Fetcher | `amethyst/service/images/` | Extract to commons |
|
|
| ZoomableContentView | `amethyst/ui/components/` | Desktop lightbox equivalent |
|
|
| ZoomableContentDialog | `amethyst/ui/components/` | Desktop lightbox dialog |
|
|
| ImageGallery | `amethyst/ui/components/` | Extract carousel to commons |
|
|
| MyAsyncImage | `amethyst/ui/components/` | Desktop equivalent |
|
|
| VideoView/VideoViewInner | `amethyst/service/playback/` | Desktop video player (VLCJ) |
|
|
| ExoPlayerPool/Builder | `amethyst/service/playback/` | Desktop player pool equivalent |
|
|
| MediaAspectRatioCache | `amethyst/model/` | Extract to commons |
|
|
| NewMediaView/Model | `amethyst/ui/actions/` | Desktop media post composer |
|
|
| MediaUploadTracker | `amethyst/ui/actions/uploads/` | Extract to commons |
|
|
| SelectFromGallery | `amethyst/ui/actions/uploads/` | Desktop file picker |
|
|
| ShowImageUploadItem | `amethyst/ui/actions/uploads/` | Extract upload preview to commons |
|
|
| ChatFileSender/Uploader | `amethyst/ui/screen/chats/` | Desktop encrypted upload |
|
|
| BlossomServersViewModel | `amethyst/ui/actions/mediaServers/` | Extract to commons |
|
|
| GalleryThumb | `amethyst/ui/screen/profile/gallery/` | Desktop gallery grid |
|
|
| PictureDisplay | `amethyst/ui/note/types/` | Extract to commons |
|
|
| VideoDisplay | `amethyst/ui/note/types/` | Desktop video renderer |
|
|
| VoiceTrack | `amethyst/ui/note/types/` | Desktop audio player |
|
|
|
|
### Desktop-Specific (new code)
|
|
|
|
| Component | Location | Notes |
|
|
|-----------|----------|-------|
|
|
| DesktopImageLoader setup | `desktopApp/` or `commons/jvmMain/` | Coil3 disk cache config for desktop |
|
|
| DesktopVideoPlayer | `desktopApp/` | VLCJ wrapper composable |
|
|
| DesktopFilePicker | `desktopApp/` | JFileChooser / AWT FileDialog |
|
|
| DragDropHandler | `desktopApp/` | Compose Desktop DnD API |
|
|
| ClipboardPasteHandler | `desktopApp/` | AWT clipboard image reading |
|
|
| DesktopMediaCompressor | `commons/jvmMain/` | Java ImageIO-based compression |
|
|
| DesktopBlurhashCalculator | `commons/jvmMain/` | BufferedImage → blurhash |
|
|
| DesktopExifStripper | `commons/jvmMain/` | metadata-extractor (lossless EXIF removal) |
|
|
| MediaScreen (desktop) | `desktopApp/` | Desktop media gallery/feed layout |
|
|
| UploadDialog (desktop) | `desktopApp/` | Upload progress, server selection, alt text |
|
|
|
|
## Blossom Protocol Implementation
|
|
|
|
### Upload Flow (BUD-02)
|
|
|
|
```
|
|
1. User drops/pastes/picks file
|
|
2. Client computes SHA-256 locally
|
|
3. (Optional) HEAD /upload pre-flight check (BUD-06)
|
|
4. Sign kind 24242 auth event (BUD-11) with t=upload
|
|
5. PUT /upload with binary body + Authorization header
|
|
6. Server returns BlobDescriptor {url, sha256, size, type, uploaded}
|
|
7. (Optional) PUT /media for server-side optimization (BUD-05)
|
|
8. (Optional) PUT /mirror to additional servers (BUD-04)
|
|
9. Client adds imeta tag to note event (NIP-92)
|
|
```
|
|
|
|
### Server Discovery (BUD-03)
|
|
|
|
```
|
|
1. Query user's kind 10063 event from relays
|
|
2. Parse server URLs from ["server", "https://..."] tags
|
|
3. Cache server list
|
|
4. Upload to preferred server(s)
|
|
5. When URL breaks: extract SHA-256 from URL → try other servers
|
|
```
|
|
|
|
### Auth (BUD-11)
|
|
|
|
```kotlin
|
|
// Kind 24242 event structure
|
|
BlossomAuthorizationEvent(
|
|
content = "Upload Blob",
|
|
tags = [
|
|
["t", "upload"],
|
|
["x", "<sha256>"], // scope to specific blob
|
|
["expiration", "<timestamp>"], // required
|
|
["server", "cdn.example.com"] // scope to server
|
|
]
|
|
)
|
|
// Sent as: Authorization: Nostr <base64(signedEvent)>
|
|
```
|
|
|
|
## NIP Coverage
|
|
|
|
| NIP | What | How We Use It |
|
|
|-----|------|---------------|
|
|
| NIP-92 | Media Attachments (imeta tag) | Attach metadata to media URLs in notes |
|
|
| NIP-94 | File Metadata (kind 1063) | File header events, metadata tags |
|
|
| NIP-B7 | Blossom Media | Server discovery, URL fallback |
|
|
| NIP-68 | Picture Events (kind 20) | Picture-first posts, profile gallery |
|
|
| NIP-71 | Video Events (kind 21/22) | Video display and metadata |
|
|
| NIP-17 | Private DMs (encrypted files) | Encrypted media in DMs |
|
|
|
|
## Desktop UX Patterns
|
|
|
|
### Drag & Drop
|
|
- Drop zone on compose area
|
|
- Visual feedback (border highlight, preview)
|
|
- Multiple files → MultiOrchestrator
|
|
- Accept: images, videos, audio files
|
|
|
|
### Clipboard Paste (Ctrl+V)
|
|
- Detect image data in clipboard
|
|
- Auto-create temp file → upload flow
|
|
- Screenshot workflow: PrtScn → paste → upload
|
|
|
|
### File Picker
|
|
- OS-native dialog (JFileChooser on desktop)
|
|
- Filter by supported media types
|
|
- Multiple file selection
|
|
|
|
### Lightbox
|
|
- Click image → full-screen overlay
|
|
- Mouse wheel zoom + pan
|
|
- Arrow keys for gallery navigation
|
|
- Esc to close
|
|
- Save to disk option
|
|
|
|
### Upload Progress
|
|
- Inline progress indicator per file
|
|
- Server selection dropdown (from kind 10063 list)
|
|
- Alt text input field
|
|
- Compression toggle
|
|
- Preview before send
|
|
|
|
## Phases
|
|
|
|
### Phase 1: Image Display Foundation
|
|
**Goal:** Images render in desktop notes with blurhash previews.
|
|
|
|
| Task | Module | Details |
|
|
|------|--------|---------|
|
|
| Desktop Coil3 ImageLoader setup | `desktopApp/` | DiskCache (OS-appropriate path), MemoryCache, OkHttp network, custom fetchers |
|
|
| Extract BlossomFetcher | `amethyst/` → `commons/` | Coil3 fetcher for `blossom:` URIs |
|
|
| Extract BlurHashFetcher | `amethyst/` → `commons/` | Coil3 fetcher for blurhash placeholder rendering |
|
|
| Extract Base64Fetcher | `amethyst/` → `commons/` | Coil3 fetcher for base64 data URIs |
|
|
| Desktop inline image rendering | `desktopApp/` | AsyncImage in note content, aspect ratio handling |
|
|
| MediaAspectRatioCache extraction | `amethyst/` → `commons/` | LruCache for URL→aspect ratio (replace Android LruCache with common impl) |
|
|
|
|
**Deliverable:** Notes in desktop feed display inline images with blurhash placeholders.
|
|
**Verifiable:** Run desktop app, navigate to feed with image posts, images load with blue/gray previews → full images.
|
|
|
|
### Phase 2: Blossom Upload Protocol Extraction
|
|
**Goal:** Shared upload client in commons, usable by Android and Desktop.
|
|
|
|
| Task | Module | Details |
|
|
|------|--------|---------|
|
|
| Create `BlossomClient` | `commons/commonMain/` | HTTP PUT /upload, /mirror, /media. Takes InputStream. Returns BlobDescriptor. |
|
|
| Create `BlossomAuthHelper` | `commons/commonMain/` | Kind 24242 event creation + base64 encoding |
|
|
| Create `BlossomServerDiscovery` | `commons/commonMain/` | Kind 10063 query, server list resolution |
|
|
| Create expect/actual file operations | `commons/` | `readFileBytes`, `computeFileSha256`, `getMimeType`, `getFileSize` |
|
|
| Create expect/actual `MediaMetadataExtractor` | `commons/` | Dimensions, blurhash computation |
|
|
| Create expect/actual `MediaCompressor` | `commons/` | JPEG quality, EXIF stripping (metadata-extractor) |
|
|
| Extract `UploadOrchestrator` | `amethyst/` → `commons/` | Multi-server coordination using BlossomClient |
|
|
| Extract `MultiUploadOrchestrator` | `amethyst/` → `commons/` | Parallel file upload management |
|
|
| Extract `MediaUploadResult` | `amethyst/` → `commons/` | Already platform-agnostic |
|
|
| Migrate Android to use commons upload | `amethyst/` | Android actuals + wire up to existing UploadOrchestrator callers |
|
|
| Extract `BlossomServersViewModel` | `amethyst/` → `commons/` | Server list state management |
|
|
|
|
**Deliverable:** `./gradlew :commons:jvmTest` passes with upload unit tests. Android still works.
|
|
**Verifiable:** Android upload flow unchanged. Desktop can call BlossomClient to upload a file.
|
|
|
|
### Phase 3: Desktop Upload UX
|
|
**Goal:** Upload media from desktop via file picker, drag-drop, and clipboard paste.
|
|
|
|
| Task | Module | Details |
|
|
|------|--------|---------|
|
|
| Desktop file picker | `desktopApp/` | JFileChooser with media type filters, multi-select |
|
|
| Drag-and-drop handler | `desktopApp/` | `dragAndDropTarget` + `awtTransferable` + `javaFileListFlavor` |
|
|
| Clipboard paste handler | `desktopApp/` | AWT Toolkit clipboard, `DataFlavor.imageFlavor`, temp file creation |
|
|
| Upload dialog composable | `desktopApp/` | Progress bar, server selector (kind 10063), alt text field, compression toggle |
|
|
| Upload preview | `desktopApp/` or `commons/` | Thumbnail preview before upload |
|
|
| Wire up to compose screen | `desktopApp/` | Add media button to note composer, connect upload flow |
|
|
|
|
**Deliverable:** Desktop user can drag image → see preview → upload to Blossom → post note with imeta.
|
|
**Verifiable:** Drop file on compose area, see upload progress, note publishes with embedded image.
|
|
|
|
### Phase 4: Video Playback
|
|
**Goal:** Videos play inline in desktop notes.
|
|
|
|
| Task | Module | Details |
|
|
|------|--------|---------|
|
|
| Add VLCJ + vlc-setup plugin | `desktopApp/build.gradle.kts` | `ir.mahozad.vlc-setup`, VLCJ 4.8.x dep |
|
|
| DesktopVideoPlayer composable | `desktopApp/` | SwingPanel + EmbeddedMediaPlayerComponent |
|
|
| Video controls overlay | `desktopApp/` | Play/pause, seek bar, volume, fullscreen toggle |
|
|
| Video in note rendering | `desktopApp/` | Replace URL-only display with inline player |
|
|
| Video upload support | `desktopApp/` | Accept video files in upload flow (Phase 3) |
|
|
|
|
**Deliverable:** Video posts play inline in desktop feed.
|
|
**Verifiable:** Navigate to note with mp4/webm URL, video plays with controls.
|
|
|
|
### Phase 5: Lightbox & Gallery
|
|
**Goal:** Full-screen media viewing with zoom and gallery navigation.
|
|
|
|
| Task | Module | Details |
|
|
|------|--------|---------|
|
|
| Lightbox overlay composable | `desktopApp/` or `commons/` | Full-screen overlay, semi-transparent backdrop |
|
|
| Zoom + pan | `desktopApp/` | Mouse wheel zoom, click-drag pan (use zoomable lib or custom) |
|
|
| Gallery carousel | `commons/commonMain/` | Multi-image navigation (arrow keys + swipe indicators) |
|
|
| Save to disk | `desktopApp/` | Right-click or button → save image/video to local filesystem |
|
|
| Keyboard shortcuts | `desktopApp/` | Esc close, Left/Right navigate, +/- zoom |
|
|
|
|
**Deliverable:** Click any image → fullscreen lightbox with zoom, multi-image gallery navigation.
|
|
**Verifiable:** Click image in note, zooms to fullscreen. Arrow keys cycle images. Esc closes.
|
|
|
|
### Phase 6: Encrypted Media (DM Files)
|
|
**Goal:** Send and receive encrypted files in NIP-17 DMs.
|
|
|
|
| Task | Module | Details |
|
|
|------|--------|---------|
|
|
| Extract encryption logic | `amethyst/` → `commons/` | NostrCipher usage for file encrypt/decrypt |
|
|
| Desktop encrypted upload flow | `desktopApp/` | Pick file → encrypt → Blossom upload → send encrypted event |
|
|
| Desktop encrypted display | `desktopApp/` | Receive encrypted file event → download → decrypt → display |
|
|
| Chat file upload dialog | `desktopApp/` | Similar to Phase 3 upload dialog but in DM context |
|
|
|
|
**Deliverable:** Desktop DM users can send/receive encrypted images and files.
|
|
**Verifiable:** Send image in DM from desktop, receive on Android (and vice versa).
|
|
|
|
### Phase 7: Profile Gallery (NIP-68)
|
|
**Goal:** View and create picture-first posts (kind 20). Profile gallery tab.
|
|
|
|
| Task | Module | Details |
|
|
|------|--------|---------|
|
|
| Picture event display | `desktopApp/` | Kind 20 renderer with image-first layout |
|
|
| Profile gallery tab | `desktopApp/` | Grid of user's picture posts |
|
|
| Picture post composer | `desktopApp/` | Create kind 20 events with multiple images + imeta |
|
|
| Gallery entry events | `desktopApp/` | ProfileGalleryEntryEvent support |
|
|
|
|
**Deliverable:** Desktop profile shows gallery tab. Users can create Instagram-style picture posts.
|
|
**Verifiable:** View profile → gallery tab shows image grid. Create picture post → visible on Android.
|
|
|
|
### Phase 8: Media Server Management
|
|
**Goal:** UI for managing Blossom server list (kind 10063).
|
|
|
|
| Task | Module | Details |
|
|
|------|--------|---------|
|
|
| Server list settings screen | `desktopApp/` | View/add/remove/reorder Blossom servers |
|
|
| Server status checking | `commons/` | HEAD request to verify server availability |
|
|
| Default server selection | `desktopApp/` | Choose preferred upload server |
|
|
| Publish kind 10063 | `commons/` | Update server list on relays |
|
|
|
|
**Deliverable:** Desktop settings page to manage Blossom servers.
|
|
**Verifiable:** Add server → appears in upload dialog dropdown. Remove server → no longer used.
|
|
|
|
### Phase 9: Audio Playback
|
|
**Goal:** Play audio tracks (MP3, OGG, FLAC) in notes.
|
|
|
|
| Task | Module | Details |
|
|
|------|--------|---------|
|
|
| Audio player composable | `desktopApp/` | VLCJ audio-only mode (no video surface needed) |
|
|
| Waveform visualization | `desktopApp/` | Optional: visual waveform for voice messages |
|
|
| Audio in note rendering | `desktopApp/` | Play/pause button + progress bar inline |
|
|
|
|
**Deliverable:** Audio files play inline in notes.
|
|
**Verifiable:** Note with MP3 URL shows audio player, plays on click.
|
|
|
|
### Phase Dependency Graph
|
|
|
|
```
|
|
Phase 1 (Images) ─────┬──→ Phase 5 (Lightbox)
|
|
│
|
|
Phase 2 (Upload) ──────┼──→ Phase 3 (Desktop UX) ──→ Phase 6 (Encrypted)
|
|
│
|
|
├──→ Phase 7 (Gallery)
|
|
│
|
|
└──→ Phase 8 (Server Mgmt)
|
|
|
|
Phase 4 (Video) ────────────→ Phase 9 (Audio)
|
|
|
|
Independent: Phase 1, 2, 4 can run in parallel
|
|
```
|
|
|
|
## Assumptions
|
|
|
|
1. **Coil3 JVM/Desktop is production-ready** — Coil3 3.x advertises Compose Multiplatform support. Verify actual JVM desktop stability before committing.
|
|
2. **VLCJ + Compose SwingPanel works** — SwingPanel embeds Swing components in Compose. VLCJ renders to a Canvas/Panel. Need spike to confirm smooth integration (no flickering, proper resizing).
|
|
3. **OkHttp in commons is fine** — BlossomUploader uses OkHttp for HTTP. Both Android and Desktop are JVM, so OkHttp works in `commons/jvmAndroid/` or `commons/commonMain/` (OkHttp has KMP support). If iOS is ever targeted, this becomes an issue.
|
|
4. **Extracting to commons won't break Android** — Moving upload code from `amethyst/` to `commons/` requires updating Android imports. Must ensure Android's Koin DI and lifecycle wiring still works.
|
|
5. **libvlc can be bundled per-platform** — Compose Desktop packaging plugin supports native lib bundling. Need to verify for macOS (dylib), Linux (.so), Windows (.dll).
|
|
|
|
## Risks
|
|
|
|
| Risk | Impact | Mitigation |
|
|
|------|--------|------------|
|
|
| VLCJ SwingPanel flicker | Video unusable | Spike test early (Phase 4 is independent) |
|
|
| Commons extraction breaks Android | Regression | Run Android build after each extraction |
|
|
| Coil3 JVM disk cache bugs | Missing images | Fall back to OkHttp manual caching |
|
|
| libvlc bundle size (~100MB) | Large app | Consider optional download or separate installer |
|
|
| Scope creep (15 features) | Never ships | Phases exist for a reason — ship Phase 1-3 first |
|
|
|
|
## Resolved Questions
|
|
|
|
1. **Video player** — VLCJ with bundled libvlc. Ship libvlc with the app (~100MB) for zero user setup. Full format support (mp4, webm, m3u8, mkv, etc.).
|
|
|
|
2. **EXIF stripping** — Use Drew Noakes' `metadata-extractor` library. Surgically strip EXIF while preserving image quality (no re-encoding loss).
|
|
|
|
3. **Upload concurrency** — Higher parallelism than Android by default (desktop has more resources). Upload to multiple Blossom servers simultaneously. Make concurrent upload count user-configurable in settings.
|
|
|
|
4. **Coil3 disk cache** — Use OS-appropriate paths: macOS `~/Library/Caches/AmethystDesktop`, Linux `$XDG_CACHE_HOME/AmethystDesktop` (default `~/.cache/`), Windows `%LOCALAPPDATA%/AmethystDesktop/cache`. Use `maxSizeBytes(1GB)` not `maxSizePercent` (that needs Android Context).
|
|
|
|
5. **Compose Desktop DnD** — `Modifier.dragAndDropTarget` is experimental (`@ExperimentalFoundationApi`) in 1.7.x. Uses `event.awtTransferable` + `DataFlavor.javaFileListFlavor` on desktop. Old `onExternalDrag` deprecated, removed in 1.8.0. API works but expect minor changes.
|
|
|
|
6. **Media compression** — JPEG: `ImageWriteParam.compressionQuality` (0.0-1.0). PNG: lossless deflate level. WebP: use `org.sejda.imageio:webp-imageio` for lossy/lossless write (~3MB native libs per platform). Start with JPEG/PNG re-encoding, add WebP output later.
|
|
|
|
7. **libvlc bundling** — Use `ir.mahozad.vlc-setup` Gradle plugin. Downloads and bundles libvlc per platform. Targets VLC 3.x + VLCJ 4.8.x. Compose Desktop integration via `SwingPanel`.
|
|
|
|
## Open Questions
|
|
|
|
1. **vlc-setup Apple Silicon** — Does the vlc-setup plugin support arm64 macOS, or only x86_64? Need to verify.
|
|
2. **Compose DnD macOS quirks** — Does `awtTransferable` properly deliver file URIs on macOS, or are there Finder-specific issues?
|