b2bbf7efb5
@Jasper2213 reported non-technical clients struggle to get downloaded
photos into their Photos / Gallery app — current flow goes through the
Files folder, requires unzipping for the bulk download, and is hard to
explain over email. Browsers can't write directly to the OS Photos app
(it's a protected location), but navigator.share({ files: [...] }) opens
the native share sheet which on iOS includes "Save Image" and on Android
includes "Save to Photos" / "Save image" — exactly the affordance non-
technical users are looking for.
Plumbed through three layers:
1. galleryService — new savePhotoToDevice(slug, photoId, filename).
Fetches the photo blob, probes navigator.canShare({ files: [file] })
with a representative File (some browsers return true for empty
files arrays even when they won't accept a non-empty one), and:
- shares if supported,
- falls back to the existing <a download> path otherwise.
AbortError on share() means the user dismissed the sheet — that's
a choice, not a failure, so no fallback. Any other error falls
through to a regular download so the user still gets the file.
Refactored the existing downloadPhoto to share the fetch + trigger
helpers (no behaviour change for the other 3 callers; they keep
the regular download path).
2. useGallery — new useSavePhotoToDevice() hook next to the existing
useDownloadPhoto(). Onsuccess toast omitted because the share-sheet
path doesn't finish from this code's perspective — the OS UI takes
over and the user picks the destination, so "Photo downloaded" is
misleading. Fallback path stays silent to keep the two flows
symmetrical (the file appearing in Downloads is its own signal).
3. PhotoLightbox — swap the existing useDownloadPhoto call site to
useSavePhotoToDevice. No UI change. Desktop unchanged. Other
download buttons (PhotoGrid, PhotoGridWithLayouts, GalleryView
bulk) still use useDownloadPhoto — scoping this PR to the
lightbox download button per the discussion thread.
Browser support:
- iOS Safari 15+: Web Share Files → "Save Image" → Photos ✓
- Chrome Android: Web Share Files → "Save to Photos" / "Save" ✓
- Desktop Chrome: canShare returns false → regular download ✓
- Desktop Safari: canShare returns false → regular download ✓
- Firefox (any): no Web Share File support → regular download ✓
No new tests — the flow is browser-API-driven; jsdom doesn't model
navigator.share or canShare, so a meaningful unit test would mostly
exercise the mock rather than the contract. Verified the build is
clean (tsc --noEmit + vite build both pass).
Refs: #531