Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import {
faPencil,
faTrashCan,
faUpload,
faFolderPlus,
faChevronRight,
faFolder,
faPlus,
} from "@fortawesome/pro-solid-svg-icons";
import { LoadingSpinner } from "@storyteller/ui-loading-spinner";
import { twMerge } from "tailwind-merge";
Expand All @@ -22,6 +26,11 @@ import { toast } from "@storyteller/ui-toaster";

type ModalMode = "select" | "view";

export interface GalleryFolder {
id: string;
name: string;
}

interface GalleryDraggableItemProps {
item: GalleryItem;
mode: ModalMode;
Expand All @@ -37,6 +46,10 @@ interface GalleryDraggableItemProps {
bulkSelected?: boolean;
onBulkSelectToggle?: () => void;
bulkSelectionMode?: boolean;
getBulkDragItems?: () => GalleryItem[];
folders?: GalleryFolder[];
onAddToFolder?: (itemIds: string[], folderId: string) => void;
onCreateFolderFromMenu?: () => void;
}

export const GalleryDraggableItem: React.FC<GalleryDraggableItemProps> = ({
Expand All @@ -54,9 +67,14 @@ export const GalleryDraggableItem: React.FC<GalleryDraggableItemProps> = ({
bulkSelected = false,
onBulkSelectToggle,
bulkSelectionMode = false,
getBulkDragItems,
folders = [],
onAddToFolder,
onCreateFolderFromMenu,
}) => {
const imgRef = useRef<HTMLImageElement>(null);
const dragStarted = useRef(false);
const [folderSubmenuOpen, setFolderSubmenuOpen] = useState(false);

// For freshly-completed videos the backend may still be generating the
// preview GIF, so the thumbnail URL 404s for a while. Show a spinner and
Expand Down Expand Up @@ -124,21 +142,18 @@ export const GalleryDraggableItem: React.FC<GalleryDraggableItemProps> = ({
};

const handlePointerDown = (event: React.PointerEvent<HTMLButtonElement>) => {
// Disable dragging for video items, but allow clicks in select mode
if (item.mediaClass === "video" && mode !== "select") return;
// In bulk selection mode, skip drag — clicks toggle selection
if (bulkSelectionMode) {
dragStarted.current = false;
return;
}
dragStarted.current = false;
const moveListener = (moveEvent: PointerEvent) => {
const dx = moveEvent.pageX - event.pageX;
const dy = moveEvent.pageY - event.pageY;
if (!dragStarted.current && (Math.abs(dx) > 5 || Math.abs(dy) > 5)) {
dragStarted.current = true;
if (galleryDnd && !disableTooltipAndBadge) {
galleryDnd.onPointerDown(event, item);
const bulkItems =
bulkSelectionMode && bulkSelected && getBulkDragItems
? getBulkDragItems()
: undefined;
galleryDnd.onPointerDown(event, item, bulkItems);
}
window.removeEventListener("pointermove", moveListener);
}
Expand All @@ -155,9 +170,9 @@ export const GalleryDraggableItem: React.FC<GalleryDraggableItemProps> = ({
};

const handlePointerUp = (event: React.PointerEvent) => {
// In bulk selection mode, let only handleButtonClick fire onClick
// to avoid double-toggling the selection
if (bulkSelectionMode) return;
const globalDrag = galleryDnd.getDragState();
if (globalDrag.isDragging) return;
if (
!dragStarted.current &&
(mode === "select" || !disableTooltipAndBadge)
Expand All @@ -167,12 +182,14 @@ export const GalleryDraggableItem: React.FC<GalleryDraggableItemProps> = ({
};

const handleButtonClick = (event: React.MouseEvent) => {
if (dragStarted.current) return;
const globalDrag = galleryDnd.getDragState();
if (globalDrag.isDragging) return;
if (mode === "select" || !disableTooltipAndBadge || bulkSelectionMode) {
onClick();
}
};

// Shared button content — avoids duplicating the image/thumbnail rendering
const showTooltip = !disableTooltipAndBadge && !bulkSelectionMode;

const itemButton = (
Expand All @@ -186,9 +203,9 @@ export const GalleryDraggableItem: React.FC<GalleryDraggableItemProps> = ({
: disableTooltipAndBadge
? "border-transparent hover:border-primary/80"
: "border-transparent hover:border-primary",
mode === "select" || item.mediaClass === "video" || bulkSelectionMode
mode === "select"
? "cursor-pointer"
: disableTooltipAndBadge
: disableTooltipAndBadge && !bulkSelectionMode
? "cursor-pointer"
: "cursor-grab hover:cursor-grab active:cursor-grabbing",
)}
Expand Down Expand Up @@ -259,7 +276,7 @@ export const GalleryDraggableItem: React.FC<GalleryDraggableItemProps> = ({
<FontAwesomeIcon icon={faEllipsis} className="text-base-fg" />
}
buttonClassName="h-7 w-7 p-0 rounded-full bg-ui-controls/60 hover:bg-ui-controls/90 text-base-fg border border-ui-controls-border"
panelClassName="min-w-28 p-1"
panelClassName="min-w-36 p-1"
closeOnUnhover
>
{(close) => (
Expand All @@ -279,10 +296,69 @@ export const GalleryDraggableItem: React.FC<GalleryDraggableItemProps> = ({
close();
}}
>
<FontAwesomeIcon icon={faPencil} className="text-base-fg" />
<FontAwesomeIcon icon={faPencil} className="text-base-fg w-4" />
<span>Edit image</span>
</button>
)}
{/* Add to Folder — with submenu */}
<div
className="relative"
onMouseEnter={() => setFolderSubmenuOpen(true)}
onMouseLeave={() => setFolderSubmenuOpen(false)}
>
<button
type="button"
className="flex w-full items-center justify-between gap-2 px-2 py-2 rounded-md hover:bg-ui-controls/60 text-base-fg text-sm"
onClick={(e) => {
e.stopPropagation();
setFolderSubmenuOpen((v) => !v);
}}
>
<div className="flex items-center gap-2">
<FontAwesomeIcon icon={faFolderPlus} className="text-base-fg w-4" />
<span>Add to Folder</span>
</div>
<FontAwesomeIcon icon={faChevronRight} className="text-[10px] text-base-fg/50" />
</button>
{folderSubmenuOpen && (
<div className="absolute left-full top-0 -ml-1 pl-2 z-50">
<div className="min-w-36 rounded-lg border border-ui-panel-border bg-ui-panel p-1 shadow-xl">
{folders.map((folder) => (
<button
key={folder.id}
type="button"
className="flex w-full items-center gap-2 px-2 py-1.5 rounded-md hover:bg-ui-controls/60 text-base-fg text-sm"
onClick={(e) => {
e.stopPropagation();
onAddToFolder?.([item.id], folder.id);
setFolderSubmenuOpen(false);
close();
}}
>
<FontAwesomeIcon icon={faFolder} className="text-primary text-xs" />
<span className="truncate">{folder.name}</span>
</button>
))}
{folders.length > 0 && (
<div className="mx-1.5 my-1 border-t border-ui-panel-border" />
)}
<button
type="button"
className="flex w-full items-center gap-2 px-2 py-1.5 rounded-md hover:bg-ui-controls/60 text-base-fg/70 text-sm"
onClick={(e) => {
e.stopPropagation();
setFolderSubmenuOpen(false);
close();
onCreateFolderFromMenu?.();
}}
>
<FontAwesomeIcon icon={faPlus} className="text-xs w-4" />
<span>New Folder</span>
</button>
</div>
</div>
)}
</div>
<button
type="button"
className="flex items-center gap-2 px-2 py-2 rounded-md hover:bg-ui-controls/60 text-sm"
Expand All @@ -292,7 +368,7 @@ export const GalleryDraggableItem: React.FC<GalleryDraggableItemProps> = ({
close();
}}
>
<FontAwesomeIcon icon={faTrashCan} className="text-red" />
<FontAwesomeIcon icon={faTrashCan} className="text-red w-4" />
<span className="text-red">Delete</span>
</button>
</div>
Expand Down Expand Up @@ -346,16 +422,14 @@ export const GalleryDraggableItem: React.FC<GalleryDraggableItemProps> = ({
className="-mt-3 bg-ui-controls text-base-fg border border-ui-panel-border"
content={
<div className="flex flex-col items-center text-xs whitespace-nowrap">
{item.mediaClass !== "video" && (
<span>
<span className="font-bold">Drag</span>
<span className="opacity-50">
{item.mediaClass === "dimensional"
? " to add to scene"
: " to add"}
</span>
<span>
<span className="font-bold">Drag</span>
<span className="opacity-50">
{item.mediaClass === "dimensional"
? " to add to scene"
: " to add"}
</span>
)}
</span>
<span>
<span className="font-bold">Click</span>
<span className="opacity-50"> to view</span>
Expand Down
Loading