From 3ede4ef8d11fb7de5b5c2f0e49fa0e8d273f1240 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Wed, 29 Apr 2026 11:04:42 +0700 Subject: [PATCH] website: download on site instead of opening new tab --- .../generation-gallery/GalleryCard.tsx | 45 +++++++++++----- .../components/lightbox/LightboxDetails.tsx | 41 +++++++++----- .../src/lib/download-media.ts | 54 +++++++++++++++++++ 3 files changed, 112 insertions(+), 28 deletions(-) create mode 100644 frontend/apps/artcraft-website/src/lib/download-media.ts diff --git a/frontend/apps/artcraft-website/src/components/generation-gallery/GalleryCard.tsx b/frontend/apps/artcraft-website/src/components/generation-gallery/GalleryCard.tsx index 527e7e1945..7033962c2b 100644 --- a/frontend/apps/artcraft-website/src/components/generation-gallery/GalleryCard.tsx +++ b/frontend/apps/artcraft-website/src/components/generation-gallery/GalleryCard.tsx @@ -11,9 +11,10 @@ import { faSpinnerThird, faVideo, } from "@fortawesome/pro-solid-svg-icons"; -import { addCorsParam, PLACEHOLDER_IMAGES } from "@storyteller/common"; +import { PLACEHOLDER_IMAGES } from "@storyteller/common"; import { Tooltip } from "@storyteller/ui-tooltip"; import { toast } from "@storyteller/ui-toaster"; +import { downloadMediaFile } from "../../lib/download-media"; import { getModelCreatorIconPath, getModelDisplayName, @@ -119,6 +120,7 @@ export const GalleryCard = memo(function GalleryCard({ const [ratio, setRatio] = useState(cached); const [shareCopied, setShareCopied] = useState(false); const [isRecreating, setIsRecreating] = useState(false); + const [isDownloading, setIsDownloading] = useState(false); const isVideo = item.mediaClass === "video"; const is3D = item.mediaClass === "dimensional"; const recreateMediaClass: RecreateMediaClass | null = isVideo @@ -277,9 +279,23 @@ export const GalleryCard = memo(function GalleryCard({ [item, onClick], ); - const downloadHref = item.fullImage - ? addCorsParam(item.fullImage) || item.fullImage - : undefined; + const handleDownload = useCallback( + async (e: React.MouseEvent) => { + e.stopPropagation(); + if (!item.fullImage || isDownloading) return; + setIsDownloading(true); + try { + await downloadMediaFile({ + url: item.fullImage, + filename: `artcraft-${item.id}`, + mediaClass: item.mediaClass, + }); + } finally { + setIsDownloading(false); + } + }, + [item.fullImage, item.id, item.mediaClass, isDownloading], + ); return (
- {downloadHref && ( + {item.fullImage && ( - e.stopPropagation()} - className="flex h-7 w-7 items-center justify-center rounded-md text-white/85 transition-colors hover:bg-white/15 hover:text-white" + )}
diff --git a/frontend/apps/artcraft-website/src/components/lightbox/LightboxDetails.tsx b/frontend/apps/artcraft-website/src/components/lightbox/LightboxDetails.tsx index 1d1ce87ef4..80d9d122c0 100644 --- a/frontend/apps/artcraft-website/src/components/lightbox/LightboxDetails.tsx +++ b/frontend/apps/artcraft-website/src/components/lightbox/LightboxDetails.tsx @@ -14,12 +14,12 @@ import { faImage, faLink, faPencil, + faSpinnerThird, faTrashCan, faUser, faXmark, } from "@fortawesome/pro-solid-svg-icons"; import { - addCorsParam, getContextImageThumbnail, THUMBNAIL_SIZES, } from "@storyteller/common"; @@ -29,6 +29,7 @@ import { getProviderDisplayName, getProviderIconByName, } from "@storyteller/model-list"; +import { downloadMediaFile } from "../../lib/download-media"; import { formatAspectRatio, formatDuration, @@ -88,6 +89,7 @@ export function LightboxDetails({ }: LightboxDetailsProps) { const promptCopy = useCopyFeedback(); const shareCopy = useCopyFeedback(); + const [isDownloading, setIsDownloading] = useState(false); const [isPromptExpanded, setIsPromptExpanded] = useState(false); const [isPromptClamped, setIsPromptClamped] = useState(false); @@ -95,6 +97,7 @@ export function LightboxDetails({ useEffect(() => { setIsPromptExpanded(false); + setIsDownloading(false); }, [mediaToken]); useEffect(() => { @@ -368,23 +371,33 @@ export function LightboxDetails({ > {shareCopy.copied ? "Copied" : "Share"} - { + if (!mediaUrl || isDownloading) return; + setIsDownloading(true); + try { + await downloadMediaFile({ + url: mediaUrl, + filename: `artcraft-${mediaToken || "media"}`, + }); + } finally { + setIsDownloading(false); + } + }} className={`w-full inline-flex items-center justify-center gap-2 rounded-md px-3 py-2 text-sm font-medium transition-colors border border-ui-panel-border ${ mediaUrl - ? "bg-ui-controls/40 hover:bg-ui-controls/60 text-white" - : "bg-ui-controls/20 text-white/60 cursor-not-allowed pointer-events-none" + ? "bg-ui-controls/40 hover:bg-ui-controls/60 text-white disabled:opacity-60" + : "bg-ui-controls/20 text-white/60 cursor-not-allowed" }`} - href={mediaUrl ? addCorsParam(mediaUrl) || mediaUrl : undefined} - download={ - mediaUrl ? `artcraft-${mediaToken || "media"}` : undefined - } - aria-disabled={!mediaUrl} - target="_blank" - rel="noopener noreferrer" > - - Download - + + {isDownloading ? "Downloading…" : "Download"} + {onRecreate && promptData.hasToken && (