"use client"; import { ChevronRight, X } from "lucide-react"; import { Children, cloneElement, forwardRef, isValidElement, useState, type HTMLAttributes, type MouseEvent, type ReactElement, type ReactNode, } from "react"; import { toast } from "sonner"; import { ReferenceIcon } from "@/components/lovart/reference-icon"; import { CanvasToolbarIcon } from "@/components/canvas/CanvasToolbarIcon"; import { Dialog, DialogContent, DialogDescription, DialogTitle, } from "@/components/ui/dialog"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; import { saveImageToolbarPreferences, useImageToolbarPreferences, type ImageToolbarPreferences, } from "@/lib/canvas-image-toolbar-preferences"; type ToolProps = HTMLAttributes & { "data-tool-key"?: string; disabled?: boolean; children?: ReactNode; }; type Tool = ReactElement; type Props = HTMLAttributes & { referencePresentation: boolean; defaultPinnedKeys: string[]; }; function toolLabel(tool: Tool) { return tool.props.title || ""; } function toolIcon(tool: Tool, size: number) { const icon = Children.toArray(tool.props.children).find(isValidElement); if ( !isValidElement<{ className?: string; style?: object; referenceSize?: number; }>(icon) ) return null; return cloneElement(icon, { className: "shrink-0", style: { width: size, height: size }, ...(icon.type === CanvasToolbarIcon ? { referenceSize: size } : {}), }); } function toolCredit(tool: Tool) { return Children.toArray(tool.props.children).find( (child) => isValidElement<{ compact?: boolean; label?: string }>(child) && child.props.compact && child.props.label ) as | ReactElement<{ compact?: boolean; label?: string; className?: string }> | undefined; } function tooltipButton(tool: Tool, showNames: boolean) { const name = toolLabel(tool); // Clone the existing action so its handler, busy state and administrator icon override remain intact. const button = cloneElement(tool, { title: undefined, "aria-label": name, children: ( <> {toolIcon(tool, 25)} {showNames && {name}} ), }); return ( {button} {name} {toolCredit(tool) && cloneElement(toolCredit(tool)!, { compact: false, className: "lovart-image-tool-cost", })} ); } export const ReferenceImageToolbar = forwardRef( function ReferenceImageToolbar( { referencePresentation, defaultPinnedKeys, children, ...rootProps }, ref ) { const saved = useImageToolbarPreferences(); const [draft, setDraft] = useState(null); const [draggedKey, setDraggedKey] = useState(null); const elements = Children.toArray(children).filter( isValidElement ) as ReactElement[]; const tools = elements.filter( (element) => element.props["data-tool-key"] && !["addToChat", "download"].includes(element.props["data-tool-key"]!) ); const toolMap = new Map( tools.map((tool) => [tool.props["data-tool-key"]!, tool]) ); const preferences = saved || { pinnedKeys: defaultPinnedKeys, showNames: false, }; const pinned = preferences.pinnedKeys.filter((key) => toolMap.has(key)); const moreTools = tools.filter( (tool) => !pinned.includes(tool.props["data-tool-key"]!) ); const addToChat = elements.find( (element) => element.props["data-tool-key"] === "addToChat" ); const download = elements.find( (element) => element.props["data-tool-key"] === "download" ); const originalMenu = elements.find( (element) => element.type === DropdownMenu ); const menuChildren = originalMenu ? (Children.toArray(originalMenu.props.children).filter( isValidElement ) as ReactElement<{ children?: ReactNode }>[]) : []; const menuTrigger = menuChildren.find( (element) => element.type === DropdownMenuTrigger ); const moreButton = menuTrigger && (Children.toArray(menuTrigger.props.children).find(isValidElement) as | Tool | undefined); const extras = menuChildren.find( (element) => element.type === DropdownMenuContent )?.props.children; const openCustomizer = () => setDraft({ pinnedKeys: [...preferences.pinnedKeys], showNames: preferences.showNames, }); const toggle = (key: string) => setDraft((current) => current ? { ...current, pinnedKeys: current.pinnedKeys.includes(key) ? current.pinnedKeys.filter((item) => item !== key) : [...current.pinnedKeys, key], } : current ); const move = (key: string, target: string) => setDraft((current) => { if (!current || key === target) return current; const next = [...current.pinnedKeys]; const from = next.indexOf(key), to = next.indexOf(target); if (from < 0 || to < 0) return current; next.splice(from, 1); next.splice(to, 0, key); return { ...current, pinnedKeys: next }; }); if (!referencePresentation) return (
{children}
); return ( <>
{addToChat && tooltipButton(addToChat, preferences.showNames)} {addToChat &&
} {pinned.map((key) => tooltipButton(toolMap.get(key)!, preferences.showNames) )} {moreButton && ( {cloneElement(moreButton, { title: undefined, "aria-label": toolLabel(moreButton), children: toolIcon(moreButton, 25), })} {toolLabel(moreButton)} )} {moreTools.map((tool) => ( tool.props.onClick?.( event as unknown as MouseEvent ) } > {toolIcon(tool, 18)} {toolLabel(tool)} {toolCredit(tool) && cloneElement(toolCredit(tool)!, { compact: false, className: "lovart-image-tool-cost", })} ))} {extras} 自定义工具栏 {download && ( <>
{tooltipButton(download, preferences.showNames)} )}
{ if (!open) setDraft(null); }} > event.stopPropagation()} onClick={(event) => event.stopPropagation()} >
自定义工具栏 选择你想在编辑栏中使用的工具
{addToChat && ( {toolIcon(addToChat, 18)} {draft?.showNames && toolLabel(addToChat)} )} {draft?.pinnedKeys .filter((key) => toolMap.has(key)) .map((key, index, keys) => { const tool = toolMap.get(key)!; return (
{ setDraggedKey(key); event.dataTransfer.setData("text/plain", key); event.dataTransfer.effectAllowed = "move"; }} onDragEnd={() => setDraggedKey(null)} onDragOver={(event) => event.preventDefault()} onDrop={(event) => { event.preventDefault(); if (draggedKey) move(draggedKey, key); setDraggedKey(null); }} >
); })}
{tools.map((tool) => { const key = tool.props["data-tool-key"]!; const isPinned = Boolean(draft?.pinnedKeys.includes(key)); return ( ); })}
); } );