From cf446b29e328284f810325d922379ff85c5602c5 Mon Sep 17 00:00:00 2001
From: si1k <19499950+si1k@users.noreply.github.com>
Date: Mon, 19 Aug 2024 15:14:38 -0400
Subject: [PATCH] Adding public download button option
---
README.md | 1 +
src/components/DownloadButton.tsx | 46 +++++++++++++++++++
.../primitives/PathLoaderButton.tsx | 14 ++++--
src/photo/PhotoLarge.tsx | 10 ++++
src/site/SiteChecklistClient.tsx | 10 ++++
src/site/config.ts | 3 ++
6 files changed, 80 insertions(+), 4 deletions(-)
create mode 100644 src/components/DownloadButton.tsx
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'])}
+