Add mobile save sheet with same-origin photo downloads.
Phones cannot land downloads in the photo library, so the gallery opens a share sheet for a cacheable preview while streaming via a dedicated download endpoint and recording preview sizes. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -15,6 +15,7 @@ import {
|
||||
MasonryGrid,
|
||||
} from '@/components/gallery/GalleryLayout';
|
||||
import { useDownloads } from '@/components/gallery/useDownloads';
|
||||
import SaveSheet, { SavePhoto, useMobileSave } from '@/components/gallery/SaveSheet';
|
||||
import Lightbox from '@/components/Lightbox';
|
||||
import LoginModal from '@/components/LoginModal';
|
||||
import {
|
||||
@@ -53,12 +54,30 @@ export default function GalleryClient({ slug, eventSlug, initial }: GalleryClien
|
||||
download: es ? 'Descargar' : 'Download',
|
||||
downloading: es ? 'Descargando…' : 'Downloading…',
|
||||
};
|
||||
const downloadPhoto = (photo: Photo) =>
|
||||
|
||||
// On a phone a download cannot reach the photo library, so the button
|
||||
// opens a sheet offering the shareable preview instead (SaveSheet).
|
||||
// Desktop keeps downloading the original straight away.
|
||||
const mobileSave = useMobileSave();
|
||||
const [savePhoto, setSavePhoto] = useState<SavePhoto | null>(null);
|
||||
|
||||
const downloadPhoto = (photo: Photo) => {
|
||||
if (mobileSave) {
|
||||
setSavePhoto({
|
||||
id: photo.id,
|
||||
previewUrl: photo.urls.download || photo.urls.preview || photo.urls.original,
|
||||
originalUrl: photo.urls.downloadOriginal || photo.urls.original,
|
||||
previewSize: photo.previewSizeBytes,
|
||||
originalSize: photo.sizeBytes,
|
||||
});
|
||||
return;
|
||||
}
|
||||
downloads.start({
|
||||
id: photo.id,
|
||||
url: photo.urls.original,
|
||||
filename: photo.originalFilename,
|
||||
});
|
||||
};
|
||||
|
||||
// Server-rendered public galleries need no client fetch. Everything else
|
||||
// (link/ticket/private) is fetched here with the share token and/or the
|
||||
@@ -235,10 +254,12 @@ export default function GalleryClient({ slug, eventSlug, initial }: GalleryClien
|
||||
}
|
||||
|
||||
const readyPhotos = photos.filter((p) => p.status === 'ready' && p.urls.thumb);
|
||||
// previewUrl is the download endpoint's preview: rendering and saving from
|
||||
// the same URL is what lets "Save photo" be answered by the HTTP cache.
|
||||
const lightboxItems = readyPhotos.map((p) => ({
|
||||
id: p.id,
|
||||
previewUrl: p.urls.preview || p.urls.original,
|
||||
downloadUrl: p.urls.original,
|
||||
previewUrl: p.urls.download || p.urls.preview || p.urls.original,
|
||||
downloadUrl: p.urls.downloadOriginal || p.urls.original,
|
||||
filename: p.originalFilename,
|
||||
thumbUrl: p.urls.thumb,
|
||||
}));
|
||||
@@ -263,7 +284,11 @@ export default function GalleryClient({ slug, eventSlug, initial }: GalleryClien
|
||||
// first photo. Falls back to a navy gradient with no image.
|
||||
const coverPhoto =
|
||||
readyPhotos.find((p) => p.id === gallery.coverPhotoId) || readyPhotos[0] || null;
|
||||
const heroUrl = coverPhoto ? coverPhoto.urls.preview || coverPhoto.urls.thumb : null;
|
||||
// Same preview URL the lightbox uses, so the cover photo's bytes are
|
||||
// fetched once for both.
|
||||
const heroUrl = coverPhoto
|
||||
? coverPhoto.urls.download || coverPhoto.urls.preview || coverPhoto.urls.thumb
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -338,15 +363,20 @@ export default function GalleryClient({ slug, eventSlug, initial }: GalleryClien
|
||||
index={lightboxIndex}
|
||||
onClose={() => setLightboxIndex(null)}
|
||||
onNavigate={setLightboxIndex}
|
||||
onDownload={(it) =>
|
||||
downloads.start({ id: it.id, url: it.downloadUrl, filename: it.filename })
|
||||
}
|
||||
onDownload={(it) => {
|
||||
const photo = readyPhotos.find((p) => p.id === it.id);
|
||||
if (photo) downloadPhoto(photo);
|
||||
}}
|
||||
downloadingId={
|
||||
lightboxItems.find((it) => downloads.isPending(it.id))?.id ?? null
|
||||
}
|
||||
downloadLabels={downloadLabels}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Shared by both entry points: the tile button and the lightbox's.
|
||||
Fetches nothing until it is open. */}
|
||||
<SaveSheet photo={savePhoto} onClose={() => setSavePhoto(null)} />
|
||||
</GalleryContainer>
|
||||
|
||||
{/* Call to action: send attendees to their dashboard, everyone else to
|
||||
|
||||
Reference in New Issue
Block a user