diff --git a/README.md b/README.md index eeec7040..58fdfce6 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,7 @@ Application behavior can be changed by configuring the following environment var - `NEXT_PUBLIC_HIDE_TITLE_FALLBACK_TEXT = 1` prevents showing "Untitled" for photos without titles - `NEXT_PUBLIC_IGNORE_PRIORITY_ORDER = 1` prevents `priority_order` field affecting photo order - `NEXT_PUBLIC_PUBLIC_API = 1` enables public API available at `/api` +- `NEXT_PUBLIC_ALLOW_PUBLIC_DOWNLOADS = 1` enables public image downloads - `NEXT_PUBLIC_HIDE_REPO_LINK = 1` removes footer link to repo - `NEXT_PUBLIC_HIDE_SOCIAL = 1` removes X button from share modal - `NEXT_PUBLIC_HIDE_FILM_SIMULATIONS = 1` prevents Fujifilm simulations showing up in `/grid` sidebar and CMD-K search results diff --git a/src/components/DownloadButton.tsx b/src/components/DownloadButton.tsx new file mode 100644 index 00000000..8219bf4e --- /dev/null +++ b/src/components/DownloadButton.tsx @@ -0,0 +1,46 @@ +import { MdOutlineFileDownload } from 'react-icons/md'; +import PathLoaderButton from './primitives/PathLoaderButton'; +import { clsx } from 'clsx/lite'; +import { Photo } from '@/photo'; + +export default function DownloadButton({ + photo, + dim, + className, +}: { + photo: Photo + dim?: boolean + className?: string +}) { + const {url, title} = photo; + + return ( + } + spinnerColor='dim' + styleAs='link' + shouldReplace + handleAction={async () => { + const response = await fetch(url); + const blob = await response.blob(); + const downloadUrl = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = downloadUrl; + link.download = title + ? title.replace(/[^a-z0-9]/gi, '_').toLowerCase() + : url.split('/').pop() || 'download'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + window.URL.revokeObjectURL(downloadUrl); + }} + /> + ); +} diff --git a/src/components/primitives/PathLoaderButton.tsx b/src/components/primitives/PathLoaderButton.tsx index 27a4efa8..79c1af06 100644 --- a/src/components/primitives/PathLoaderButton.tsx +++ b/src/components/primitives/PathLoaderButton.tsx @@ -10,6 +10,7 @@ export default function PathLoaderButton({ loaderDelay = 100, shouldScroll = true, shouldReplace, + handleAction, children, ...props }: { @@ -18,6 +19,7 @@ export default function PathLoaderButton({ loaderDelay?: number shouldScroll?: boolean shouldReplace?: boolean + handleAction?: () => Promise } & ComponentProps) { const router = useRouter(); @@ -46,11 +48,15 @@ export default function PathLoaderButton({ { - startTransition(() => { - if (shouldReplace) { - router.replace(path, { scroll: shouldScroll }); + startTransition(async () => { + if (handleAction) { + await handleAction(); } else { - router.push(path, { scroll: shouldScroll }); + if (shouldReplace) { + router.replace(path, { scroll: shouldScroll }); + } else { + router.push(path, { scroll: shouldScroll }); + } } }); }} diff --git a/src/photo/PhotoLarge.tsx b/src/photo/PhotoLarge.tsx index cda6ac5b..be19eb82 100644 --- a/src/photo/PhotoLarge.tsx +++ b/src/photo/PhotoLarge.tsx @@ -19,6 +19,7 @@ import { } from '@/site/paths'; import PhotoTags from '@/tag/PhotoTags'; import ShareButton from '@/components/ShareButton'; +import DownloadButton from '@/components/DownloadButton'; import PhotoCamera from '../camera/PhotoCamera'; import { cameraFromPhoto } from '@/camera'; import PhotoFilmSimulation from '@/simulation/PhotoFilmSimulation'; @@ -28,6 +29,7 @@ import PhotoLink from './PhotoLink'; import { SHOULD_PREFETCH_ALL_LINKS, SHOW_PHOTO_TITLE_FALLBACK_TEXT, + ALLOW_PUBLIC_DOWNLOADS, } from '@/site/config'; import AdminPhotoMenuClient from '@/admin/AdminPhotoMenuClient'; import { RevalidatePhoto } from './InfinitePhotoScroll'; @@ -228,6 +230,14 @@ export default function PhotoLarge({ !hasNonDateContent && isUserSignedIn && 'md:pr-7', )} /> + {ALLOW_PUBLIC_DOWNLOADS && + } {shouldShare && /api: {renderEnvVars(['NEXT_PUBLIC_PUBLIC_API'])} + + Set environment variable to {'"1"'} to enable + public downloads of photos: + {renderEnvVars(['NEXT_PUBLIC_ALLOW_PUBLIC_DOWNLOADS'])} +