collective

Browse / github.com/storytold/artcraft / patch

feat(artcraft): add Open downloads folder action to gallery and lightbox

feat(artcraft): add Open downloads folder action to gallery and lightbox ## Summary Surfaces the existing `download_directory_reveal_command` in the desktop UI so users can open their configured downloads folder without leaving the app: - **My Library (gallery modal)**: folder icon button in the header toolbar, next to Refresh - **Lightbox**: \"Open downloads folder\" action in the media panel - Both resolve the user's configured preferred download directory, so they follow Settings → Downloads changes The gallery/lightbox props are optional (`onOpenDownloadsFolder?: () => void | Promise<void>`), so web builds are unaffected — only the desktop app's `TopBar` wir

Status
open
Score
0.00
Adopters
0 other operators
Author
theCosmicCrafter (imported from pull request #2557, open)
Base
800217ed04d10fc12706c28e6e91ae2a6b286092
Size
+84 −11 in 6 files
Review
pending
Shared
2026-10-28
Id
e447de3cfa1e478eb2581632

How it went

No other operator has reported using it yet.

Diff

raw · crates/desktop/artcraft/src/core/commands/download/download_directory_reveal_command.rs frontend/apps/artcraft/app/src/components/generation-feed/desktopMediaActions.ts frontend/apps/artcraft/app/src/components/signaled/TopBar/TopBar.tsx frontend/libs/components/gallery-modal/src/lib/gallery-modal.tsx frontend/libs/components/lightbox-modal/src/lib/lightbox-modal.spec.tsx frontend/libs/components/lightbox-modal/src/lib/lightbox-modal.tsx

diff --git a/crates/desktop/artcraft/src/core/commands/download/download_directory_reveal_command.rs b/crates/desktop/artcraft/src/core/commands/download/download_directory_reveal_command.rs--- a/crates/desktop/artcraft/src/core/commands/download/download_directory_reveal_command.rs+++ b/crates/desktop/artcraft/src/core/commands/download/download_directory_reveal_command.rs@@ -41,7 +41,7 @@ pub async fn download_directory_reveal_command(   ).await;    if let Err(err) = result {-    format!("Error revealing download dir: {:?}", err);+    error!("Error revealing download dir: {:?}", err);     return Err("error revealing download dir".into())   } @@ -61,9 +61,9 @@ pub async fn handle_request(       .preferred_download_directory       .download_directory(app_data_root); -  info!("Revealing item in directory: {:?}", download_directory);+  info!("Opening download directory: {:?}", download_directory); -  app.opener().reveal_item_in_dir(download_directory)+  app.opener().open_path(download_directory.to_string_lossy().to_string(), None::<&str>)       .map_err(|err| anyhow!("Failed to open directory: {:?}", err))?;    Ok(())diff --git a/frontend/apps/artcraft/app/src/components/generation-feed/desktopMediaActions.ts b/frontend/apps/artcraft/app/src/components/generation-feed/desktopMediaActions.ts--- a/frontend/apps/artcraft/app/src/components/generation-feed/desktopMediaActions.ts+++ b/frontend/apps/artcraft/app/src/components/generation-feed/desktopMediaActions.ts@@ -6,6 +6,7 @@ import { import type { Prompts } from "@storyteller/api"; import {   DownloadUrl,+  DownloadDirectoryReveal,   useModelsStore,   downloadUrlToPath,   pickDownloadDirectory,@@ -126,6 +127,16 @@ export async function downloadMediaFilesToFolder(   return saved > 0; } +/** Open the user's downloads folder in the OS file manager. */+export async function openDownloadsFolder() {+  try {+    await DownloadDirectoryReveal();+  } catch (error) {+    console.error(">>> Failed to open downloads folder:", error);+    toast.error("Could not open downloads folder");+  }+}+ /** Seed the video page with `url` as the starting image and switch to it. */ export async function applyMakeVideoFromImage(url: string, mediaToken?: string) {   try {diff --git a/frontend/apps/artcraft/app/src/components/signaled/TopBar/TopBar.tsx b/frontend/apps/artcraft/app/src/components/signaled/TopBar/TopBar.tsx--- a/frontend/apps/artcraft/app/src/components/signaled/TopBar/TopBar.tsx+++ b/frontend/apps/artcraft/app/src/components/signaled/TopBar/TopBar.tsx@@ -47,6 +47,7 @@ import {   applyMakeVideoFromImage,   applyRecreateFromPromptData,   downloadMediaFileToDisk,+  openDownloadsFolder, } from "~/components/generation-feed/desktopMediaActions"; import { useStoryboardStore } from "~/pages/PageStoryboard"; import { useSceneStore } from "@storyteller/ui-pagedraw";@@ -704,6 +705,7 @@ export const TopBar = ({ pageName }: Props) => {       <GalleryModal         mode="view"         onDownloadClicked={downloadFile}+        onOpenDownloadsFolder={openDownloadsFolder}         onEditClicked={handleEditFromGallery}         onTurnIntoVideoClicked={handleTurnIntoVideoFromGallery}         onRemoveBackgroundClicked={handleRemoveBackgroundFromGallery}diff --git a/frontend/libs/components/gallery-modal/src/lib/gallery-modal.tsx b/frontend/libs/components/gallery-modal/src/lib/gallery-modal.tsx--- a/frontend/libs/components/gallery-modal/src/lib/gallery-modal.tsx+++ b/frontend/libs/components/gallery-modal/src/lib/gallery-modal.tsx@@ -64,6 +64,7 @@ import {   BoxIcon,   EllipsisIcon,   FolderIcon,+  FolderOpenIcon,   FolderPlusIcon,   GlobeIcon,   Grid3x3Icon,@@ -281,6 +282,7 @@ interface GalleryModalProps {    *  of a Retry link. */   onLoginClick?: () => void;   onDownloadClicked?: (url: string, mediaClass?: string, model?: string) => Promise<void>;+  onOpenDownloadsFolder?: () => void | Promise<void>;   onAddToSceneClicked?: (     url: string,     media_id: string | undefined,@@ -585,6 +587,7 @@ export const GalleryModal = React.memo(     useSelectedLoading,     onLoginClick,     onDownloadClicked,+    onOpenDownloadsFolder,     onAddToSceneClicked,     isOpen,     forceFilter,@@ -2838,6 +2841,22 @@ export const GalleryModal = React.memo(                       />                     </Button>                   </Tooltip>+                  {mode === "view" && onOpenDownloadsFolder && (+                    <Tooltip+                      position="top"+                      content="Open downloads folder"+                      closeOnClick={true}+                    >+                      <Button+                        variant="action"+                        onClick={onOpenDownloadsFolder}+                        className="relative z-[51] h-9 w-9 p-2.5"+                        aria-label="Open downloads folder"+                      >+                        <FolderOpenIcon className="text-lg text-base-fg" />+                      </Button>+                    </Tooltip>+                  )}                   {/* Image fit toggle button */}                   <Tooltip                     position="top"@@ -3693,6 +3712,7 @@ export const GalleryModal = React.memo(             downloadUrl={imageUrl}             mediaId={lightboxImageSignal.value?.id}             onDownloadClicked={onDownloadClicked}+            onOpenDownloadsFolder={onOpenDownloadsFolder}             onAddToSceneClicked={onAddToSceneClicked}             mediaClass={lightboxImageSignal.value?.mediaClass}             onEditClicked={onEditClicked}diff --git a/frontend/libs/components/lightbox-modal/src/lib/lightbox-modal.spec.tsx b/frontend/libs/components/lightbox-modal/src/lib/lightbox-modal.spec.tsx--- a/frontend/libs/components/lightbox-modal/src/lib/lightbox-modal.spec.tsx+++ b/frontend/libs/components/lightbox-modal/src/lib/lightbox-modal.spec.tsx@@ -1,12 +1,32 @@-import { render } from '@testing-library/react';+import { fireEvent, render, screen, waitFor } from "@testing-library/react";+import { describe, expect, it, vi } from "vitest";+import LightboxModal from "./lightbox-modal"; -import LightboxModal from './lightbox-modal';+vi.mock("embla-carousel-react", () => ({+  default: () => [vi.fn(), null],+})); -describe('LightboxModal', () => {-  -  it('should render successfully', () => {-    const { baseElement } = render(<LightboxModal />);+describe("LightboxModal", () => {+  it("should render successfully", () => {+    const { baseElement } = render(+      <LightboxModal isOpen={false} onClose={() => {}} onCloseGallery={() => {}} />,+    );     expect(baseElement).toBeTruthy();   });-  ++  it("opens the configured downloads folder from the media actions", async () => {+    const onOpenDownloadsFolder = vi.fn(async () => {});+    render(+      <LightboxModal+        isOpen+        onClose={() => {}}+        onCloseGallery={() => {}}+        imageUrl="data:image/png;base64,"+        onOpenDownloadsFolder={onOpenDownloadsFolder}+      />,+    );++    fireEvent.click(screen.getByRole("button", { name: "Open downloads folder" }));+    await waitFor(() => expect(onOpenDownloadsFolder).toHaveBeenCalledOnce());+  }); });diff --git a/frontend/libs/components/lightbox-modal/src/lib/lightbox-modal.tsx b/frontend/libs/components/lightbox-modal/src/lib/lightbox-modal.tsx--- a/frontend/libs/components/lightbox-modal/src/lib/lightbox-modal.tsx+++ b/frontend/libs/components/lightbox-modal/src/lib/lightbox-modal.tsx@@ -2,7 +2,7 @@ import { Modal } from "@storyteller/ui-modal"; import { Tooltip } from "@storyteller/ui-tooltip"; import { Button } from "@storyteller/ui-button"; import dayjs from "dayjs";-import { ArrowDownToLineIcon, BoxIcon, CheckIcon, ChevronLeftIcon, ChevronRightIcon, CopyIcon, GlobeIcon, ImageIcon, InfoIcon, LinkIcon, LogOutIcon, MusicIcon, PauseIcon, PencilIcon, PlayIcon, RotateCwIcon, SearchIcon, Trash2Icon, UserIcon, VideoIcon, WandSparklesIcon } from "lucide-react";+import { ArrowDownToLineIcon, BoxIcon, CheckIcon, ChevronLeftIcon, ChevronRightIcon, CopyIcon, FolderOpenIcon, GlobeIcon, ImageIcon, InfoIcon, LinkIcon, LogOutIcon, MusicIcon, PauseIcon, PencilIcon, PlayIcon, RotateCwIcon, SearchIcon, Trash2Icon, UserIcon, VideoIcon, WandSparklesIcon } from "lucide-react"; import { DynamicIcon } from "@storyteller/icons"; import { MediaFileDelete } from "@storyteller/tauri-api"; import { LoadingSpinner } from "@storyteller/ui-loading-spinner";@@ -61,6 +61,7 @@ interface LightboxModalProps {   downloadUrl?: string;   mediaId?: string;   onDownloadClicked?: (url: string, mediaClass?: string, model?: string) => Promise<void>;+  onOpenDownloadsFolder?: () => void | Promise<void>;   onAddToSceneClicked?: (     url: string,     media_id: string | undefined,@@ -111,6 +112,7 @@ export function LightboxModal({   downloadUrl, // cdn url of the image   mediaId, // media id of the image   onDownloadClicked,+  onOpenDownloadsFolder,   onAddToSceneClicked,   mediaClass,   onEditClicked,@@ -1184,6 +1186,24 @@ export function LightboxModal({                   </Button>                 )} +                {onOpenDownloadsFolder && (+                  <Button+                    className={+                      derivedMediaClass === "image"+                        ? "w-full py-1.5 text-[13px]"+                        : "w-full col-span-2 py-1.5 text-[13px]"+                    }+                    variant="secondary"+                    icon={FolderOpenIcon}+                    onClick={async (e) => {+                      e.stopPropagation();+                      await onOpenDownloadsFolder();+                    }}+                  >+                    Open downloads folder+                  </Button>+                )}+                 {onAddToSceneClicked && actionUrl && (                   <Button                     className="w-full py-1.5 text-[13px]"